diff --git a/packages/core/package.json b/packages/core/package.json index fc30ad3c9..d8f771de6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,7 +18,13 @@ "exports": { "./*": "./src/*.ts" }, - "imports": {}, + "imports": { + "#sqlite": { + "bun": "./src/database/sqlite.bun.ts", + "node": "./src/database/sqlite.node.ts", + "default": "./src/database/sqlite.bun.ts" + } + }, "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", diff --git a/packages/core/src/account.ts b/packages/core/src/account.ts index 4bd11b4e1..4de8176e4 100644 --- a/packages/core/src/account.ts +++ b/packages/core/src/account.ts @@ -1 +1,101 @@ -export * from "./account/index" +export * as AccountV2 from "./account" + +import { Schema } from "effect" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" + +export const ID = Schema.String.pipe(Schema.brand("AccountID")) +export type ID = Schema.Schema.Type + +export const OrgID = Schema.String.pipe(Schema.brand("OrgID")) +export type OrgID = Schema.Schema.Type + +export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken")) +export type AccessToken = Schema.Schema.Type + +export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken")) +export type RefreshToken = Schema.Schema.Type + +export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode")) +export type DeviceCode = Schema.Schema.Type + +export const UserCode = Schema.String.pipe(Schema.brand("UserCode")) +export type UserCode = Schema.Schema.Type + +export class Info extends Schema.Class("Account")({ + id: ID, + email: Schema.String, + url: Schema.String, + active_org_id: Schema.NullOr(OrgID), +}) {} + +export class Org extends Schema.Class("Org")({ + id: OrgID, + name: Schema.String, +}) {} + +export class AccountRepoError extends Schema.TaggedErrorClass()("AccountRepoError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export class AccountServiceError extends Schema.TaggedErrorClass()("AccountServiceError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export class AccountTransportError extends Schema.TaggedErrorClass()("AccountTransportError", { + method: Schema.String, + url: Schema.String, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect), +}) { + static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError { + return new AccountTransportError({ + method: error.request.method, + url: error.request.url, + description: error.description, + cause: error.cause, + }) + } + + override get message(): string { + return [ + `Could not reach ${this.method} ${this.url}.`, + `This failed before the server returned an HTTP response.`, + this.description, + `Check your network, proxy, or VPN configuration and try again.`, + ] + .filter(Boolean) + .join("\n") + } +} + +export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError + +export class Login extends Schema.Class("Login")({ + code: DeviceCode, + user: UserCode, + url: Schema.String, + server: Schema.String, + expiry: Schema.Duration, + interval: Schema.Duration, +}) {} + +export class PollSuccess extends Schema.TaggedClass()("PollSuccess", { + email: Schema.String, +}) {} + +export class PollPending extends Schema.TaggedClass()("PollPending", {}) {} + +export class PollSlow extends Schema.TaggedClass()("PollSlow", {}) {} + +export class PollExpired extends Schema.TaggedClass()("PollExpired", {}) {} + +export class PollDenied extends Schema.TaggedClass()("PollDenied", {}) {} + +export class PollError extends Schema.TaggedClass()("PollError", { + cause: Schema.Defect, +}) {} + +export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError]) +export type PollResult = Schema.Schema.Type diff --git a/packages/core/src/account/index.ts b/packages/core/src/account/index.ts deleted file mode 100644 index 1612cbf2e..000000000 --- a/packages/core/src/account/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -export * as AccountV2 from "." - -import { Schema } from "effect" -import type * as HttpClientError from "effect/unstable/http/HttpClientError" - -export const ID = Schema.String.pipe(Schema.brand("AccountID")) -export type ID = Schema.Schema.Type - -export const OrgID = Schema.String.pipe(Schema.brand("OrgID")) -export type OrgID = Schema.Schema.Type - -export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken")) -export type AccessToken = Schema.Schema.Type - -export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken")) -export type RefreshToken = Schema.Schema.Type - -export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode")) -export type DeviceCode = Schema.Schema.Type - -export const UserCode = Schema.String.pipe(Schema.brand("UserCode")) -export type UserCode = Schema.Schema.Type - -export class Info extends Schema.Class("Account")({ - id: ID, - email: Schema.String, - url: Schema.String, - active_org_id: Schema.NullOr(OrgID), -}) {} - -export class Org extends Schema.Class("Org")({ - id: OrgID, - name: Schema.String, -}) {} - -export class AccountRepoError extends Schema.TaggedErrorClass()("AccountRepoError", { - message: Schema.String, - cause: Schema.optional(Schema.Defect), -}) {} - -export class AccountServiceError extends Schema.TaggedErrorClass()("AccountServiceError", { - message: Schema.String, - cause: Schema.optional(Schema.Defect), -}) {} - -export class AccountTransportError extends Schema.TaggedErrorClass()("AccountTransportError", { - method: Schema.String, - url: Schema.String, - description: Schema.optional(Schema.String), - cause: Schema.optional(Schema.Defect), -}) { - static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError { - return new AccountTransportError({ - method: error.request.method, - url: error.request.url, - description: error.description, - cause: error.cause, - }) - } - - override get message(): string { - return [ - `Could not reach ${this.method} ${this.url}.`, - `This failed before the server returned an HTTP response.`, - this.description, - `Check your network, proxy, or VPN configuration and try again.`, - ] - .filter(Boolean) - .join("\n") - } -} - -export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError - -export class Login extends Schema.Class("Login")({ - code: DeviceCode, - user: UserCode, - url: Schema.String, - server: Schema.String, - expiry: Schema.Duration, - interval: Schema.Duration, -}) {} - -export class PollSuccess extends Schema.TaggedClass()("PollSuccess", { - email: Schema.String, -}) {} - -export class PollPending extends Schema.TaggedClass()("PollPending", {}) {} - -export class PollSlow extends Schema.TaggedClass()("PollSlow", {}) {} - -export class PollExpired extends Schema.TaggedClass()("PollExpired", {}) {} - -export class PollDenied extends Schema.TaggedClass()("PollDenied", {}) {} - -export class PollError extends Schema.TaggedClass()("PollError", { - cause: Schema.Defect, -}) {} - -export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError]) -export type PollResult = Schema.Schema.Type diff --git a/packages/core/src/account/sql.ts b/packages/core/src/account/sql.ts index 1c0ae693e..4f45651d7 100644 --- a/packages/core/src/account/sql.ts +++ b/packages/core/src/account/sql.ts @@ -1,6 +1,6 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" -import { AccountV2 } from "." +import { AccountV2 } from "../account" import { Timestamps } from "../database/schema.sql" export const AccountTable = sqliteTable("account", { diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index 090a86ece..2840c256c 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -1,7 +1,7 @@ export * as Database from "./database" -import { SqliteClient } from "@effect/sql-sqlite-bun" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { layer as sqliteLayer } from "#sqlite" import { Context, Effect, Layer } from "effect" import { Global } from "../global" import { Flag } from "../flag/flag" @@ -31,7 +31,7 @@ const layer = Layer.effect( ) export function layerFromPath(filename: string) { - return layer.pipe(Layer.provide(SqliteClient.layer({ filename }))) + return layer.pipe(Layer.provide(sqliteLayer({ filename }))) } export const defaultLayer = Layer.unwrap( diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts new file mode 100644 index 000000000..735fcd49d --- /dev/null +++ b/packages/core/src/database/sqlite.bun.ts @@ -0,0 +1,3 @@ +import { SqliteClient } from "@effect/sql-sqlite-bun" + +export const layer = SqliteClient.layer diff --git a/packages/core/src/database/sqlite.node.ts b/packages/core/src/database/sqlite.node.ts new file mode 100644 index 000000000..acfcbda35 --- /dev/null +++ b/packages/core/src/database/sqlite.node.ts @@ -0,0 +1,3 @@ +import { NodeSqliteClient } from "@opencode-ai/effect-drizzle-sqlite/node-sqlite" + +export const layer = NodeSqliteClient.layer diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 4c57094cd..a4a5dd859 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -1,2 +1,157 @@ -export * from "./event/index" -export * as EventV2 from "./event/index" +export * as EventV2 from "./event" + +import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" +import { Location } from "./location" +import { withStatics } from "./schema" +import { Identifier } from "./util/identifier" + +export const ID = Schema.String.pipe( + Schema.brand("Event.ID"), + withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()) })), +) +export type ID = typeof ID.Type + +export type Definition = { + readonly type: Type + readonly version?: number + readonly aggregate?: string + readonly data: DataSchema +} + +export type Data = Schema.Schema.Type + +export type Payload = { + readonly id: ID + readonly type: D["type"] + readonly data: Data + readonly version?: number + readonly location?: Location.Ref + readonly metadata?: Record +} + +export type Sync = (event: Payload) => Effect.Effect + +export const registry = new Map() + +export function define(input: { + readonly type: Type + readonly version?: number + readonly aggregate?: string + readonly schema: Fields +}): Schema.Schema>>> & Definition> { + const Data = Schema.Struct(input.schema) + const Payload = Schema.Struct({ + id: ID, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + type: Schema.Literal(input.type), + version: Schema.optional(Schema.Number), + location: Schema.optional(Location.Ref), + data: Data, + }).annotate({ identifier: input.type }) + + const definition = Object.assign(Payload, { + type: input.type, + ...(input.version === undefined ? {} : { version: input.version }), + ...(input.aggregate === undefined ? {} : { aggregate: input.aggregate }), + data: Data, + }) + registry.set(input.type, definition) + return definition as Schema.Schema>>> & + Definition> +} + +export function definitions() { + return registry.values().toArray() +} + +export interface PublishOptions { + readonly id?: ID + readonly metadata?: Record +} + +export type Unsubscribe = Effect.Effect + +export interface Interface { + readonly publish: ( + definition: D, + data: Data, + options?: PublishOptions, + ) => Effect.Effect> + readonly publishEvent: (event: Payload) => Effect.Effect> + readonly subscribe: (definition: D) => Stream.Stream> + readonly all: () => Stream.Stream + readonly sync: (handler: Sync) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Event") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const all = yield* PubSub.unbounded() + const typed = new Map>() + const syncHandlers = new Array() + + const getOrCreate = (definition: Definition) => + Effect.gen(function* () { + const existing = typed.get(definition.type) + if (existing) return existing + const pubsub = yield* PubSub.unbounded() + typed.set(definition.type, pubsub) + return pubsub + }) + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + yield* PubSub.shutdown(all) + yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true }) + }), + ) + + function publishEvent(event: Payload) { + return Effect.gen(function* () { + for (const sync of syncHandlers) { + yield* sync(event as Payload) + } + const pubsub = typed.get(event.type) + if (pubsub) yield* PubSub.publish(pubsub, event as Payload) + yield* PubSub.publish(all, event as Payload) + return event + }) + } + + function publish(definition: D, data: Data, options?: PublishOptions) { + return Effect.gen(function* () { + const location = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) + const event = { + id: options?.id ?? ID.create(), + ...(options?.metadata ? { metadata: options.metadata } : {}), + type: definition.type, + ...(definition.version === undefined ? {} : { version: definition.version }), + ...(location ? { location } : {}), + data, + } as Payload + return yield* publishEvent(event) + }) + } + + const subscribe = (definition: D): Stream.Stream> => + Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe( + Stream.map((event) => event as Payload), + ) + + const streamAll = (): Stream.Stream => Stream.fromPubSub(all) + const sync = (handler: Sync): Effect.Effect => + Effect.sync(() => { + syncHandlers.push(handler) + return Effect.sync(() => { + const index = syncHandlers.indexOf(handler) + if (index >= 0) syncHandlers.splice(index, 1) + }) + }) + + return Service.of({ publish, publishEvent, subscribe, all: streamAll, sync }) + }), +) + +export const defaultLayer = layer diff --git a/packages/core/src/event/index.ts b/packages/core/src/event/index.ts deleted file mode 100644 index 12d1c48c8..000000000 --- a/packages/core/src/event/index.ts +++ /dev/null @@ -1,157 +0,0 @@ -export * as EventV2 from "." - -import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" -import { Location } from "../location" -import { withStatics } from "../schema" -import { Identifier } from "../util/identifier" - -export const ID = Schema.String.pipe( - Schema.brand("Event.ID"), - withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()) })), -) -export type ID = typeof ID.Type - -export type Definition = { - readonly type: Type - readonly version?: number - readonly aggregate?: string - readonly data: DataSchema -} - -export type Data = Schema.Schema.Type - -export type Payload = { - readonly id: ID - readonly type: D["type"] - readonly data: Data - readonly version?: number - readonly location?: Location.Ref - readonly metadata?: Record -} - -export type Sync = (event: Payload) => Effect.Effect - -export const registry = new Map() - -export function define(input: { - readonly type: Type - readonly version?: number - readonly aggregate?: string - readonly schema: Fields -}): Schema.Schema>>> & Definition> { - const Data = Schema.Struct(input.schema) - const Payload = Schema.Struct({ - id: ID, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), - type: Schema.Literal(input.type), - version: Schema.optional(Schema.Number), - location: Schema.optional(Location.Ref), - data: Data, - }).annotate({ identifier: input.type }) - - const definition = Object.assign(Payload, { - type: input.type, - ...(input.version === undefined ? {} : { version: input.version }), - ...(input.aggregate === undefined ? {} : { aggregate: input.aggregate }), - data: Data, - }) - registry.set(input.type, definition) - return definition as Schema.Schema>>> & - Definition> -} - -export function definitions() { - return registry.values().toArray() -} - -export interface PublishOptions { - readonly id?: ID - readonly metadata?: Record -} - -export type Unsubscribe = Effect.Effect - -export interface Interface { - readonly publish: ( - definition: D, - data: Data, - options?: PublishOptions, - ) => Effect.Effect> - readonly publishEvent: (event: Payload) => Effect.Effect> - readonly subscribe: (definition: D) => Stream.Stream> - readonly all: () => Stream.Stream - readonly sync: (handler: Sync) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/Event") {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const all = yield* PubSub.unbounded() - const typed = new Map>() - const syncHandlers = new Array() - - const getOrCreate = (definition: Definition) => - Effect.gen(function* () { - const existing = typed.get(definition.type) - if (existing) return existing - const pubsub = yield* PubSub.unbounded() - typed.set(definition.type, pubsub) - return pubsub - }) - - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - yield* PubSub.shutdown(all) - yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true }) - }), - ) - - function publishEvent(event: Payload) { - return Effect.gen(function* () { - for (const sync of syncHandlers) { - yield* sync(event as Payload) - } - const pubsub = typed.get(event.type) - if (pubsub) yield* PubSub.publish(pubsub, event as Payload) - yield* PubSub.publish(all, event as Payload) - return event - }) - } - - function publish(definition: D, data: Data, options?: PublishOptions) { - return Effect.gen(function* () { - const location = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) - const event = { - id: options?.id ?? ID.create(), - ...(options?.metadata ? { metadata: options.metadata } : {}), - type: definition.type, - ...(definition.version === undefined ? {} : { version: definition.version }), - ...(location ? { location } : {}), - data, - } as Payload - return yield* publishEvent(event) - }) - } - - const subscribe = (definition: D): Stream.Stream> => - Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe( - Stream.map((event) => event as Payload), - ) - - const streamAll = (): Stream.Stream => Stream.fromPubSub(all) - const sync = (handler: Sync): Effect.Effect => - Effect.sync(() => { - syncHandlers.push(handler) - return Effect.sync(() => { - const index = syncHandlers.indexOf(handler) - if (index >= 0) syncHandlers.splice(index, 1) - }) - }) - - return Service.of({ publish, publishEvent, subscribe, all: streamAll, sync }) - }), -) - -export const defaultLayer = layer diff --git a/packages/core/src/event/sql.ts b/packages/core/src/event/sql.ts index e4c5f3846..6bccc0fbb 100644 --- a/packages/core/src/event/sql.ts +++ b/packages/core/src/event/sql.ts @@ -1,5 +1,5 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" -import type { EventV2 } from "." +import type { EventV2 } from "../event" export const EventSequenceTable = sqliteTable("event_sequence", { aggregate_id: text().notNull().primaryKey(), diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index f1b19dcf9..07c7d8e7b 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -1,2 +1,56 @@ -export * from "./permission/index" -export * as PermissionV2 from "./permission/index" +export * as PermissionV2 from "./permission" + +import { Schema } from "effect" +import { Wildcard } from "./util/wildcard" +import { Identifier } from "./id/id" +import { Newtype } from "./schema" + +export class PermissionID extends Newtype()( + "PermissionID", + Schema.String.check(Schema.isStartsWith("per")), +) { + static ascending(id?: string): PermissionID { + return this.make(Identifier.ascending("permission", id)) + } +} + +export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Action" }) +export type Action = typeof Action.Type + +export const Rule = Schema.Struct({ + permission: Schema.String, + pattern: Schema.String, + action: Action, +}).annotate({ identifier: "PermissionV2.Rule" }) +export type Rule = typeof Rule.Type + +export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) +export type Ruleset = typeof Ruleset.Type + +const EDIT_TOOLS = ["edit", "write", "apply_patch"] + +export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { + return ( + rulesets + .flat() + .findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) ?? { + action: "ask", + permission, + pattern: "*", + } + ) +} + +export function merge(...rulesets: Ruleset[]): Ruleset { + return rulesets.flat() +} + +export function disabled(tools: string[], ruleset: Ruleset): Set { + return new Set( + tools.filter((tool) => { + const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool + const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission)) + return rule?.pattern === "*" && rule.action === "deny" + }), + ) +} diff --git a/packages/core/src/permission/index.ts b/packages/core/src/permission/index.ts deleted file mode 100644 index e9948e4df..000000000 --- a/packages/core/src/permission/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -export * as PermissionV2 from "." - -import { Schema } from "effect" -import { Wildcard } from "../util/wildcard" -import { Identifier } from "../id/id" -import { Newtype } from "../schema" - -export class PermissionID extends Newtype()( - "PermissionID", - Schema.String.check(Schema.isStartsWith("per")), -) { - static ascending(id?: string): PermissionID { - return this.make(Identifier.ascending("permission", id)) - } -} - -export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Action" }) -export type Action = typeof Action.Type - -export const Rule = Schema.Struct({ - permission: Schema.String, - pattern: Schema.String, - action: Action, -}).annotate({ identifier: "PermissionV2.Rule" }) -export type Rule = typeof Rule.Type - -export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) -export type Ruleset = typeof Ruleset.Type - -const EDIT_TOOLS = ["edit", "write", "apply_patch"] - -export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { - return ( - rulesets - .flat() - .findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) ?? { - action: "ask", - permission, - pattern: "*", - } - ) -} - -export function merge(...rulesets: Ruleset[]): Ruleset { - return rulesets.flat() -} - -export function disabled(tools: string[], ruleset: Ruleset): Set { - return new Set( - tools.filter((tool) => { - const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool - const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission)) - return rule?.pattern === "*" && rule.action === "deny" - }), - ) -} diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts index 188078749..eb84a73ac 100644 --- a/packages/core/src/plugin/provider.ts +++ b/packages/core/src/plugin/provider.ts @@ -1 +1,67 @@ -export { ProviderPlugins } from "./provider/index" +import { AlibabaPlugin } from "./provider/alibaba" +import { AmazonBedrockPlugin } from "./provider/amazon-bedrock" +import { AnthropicPlugin } from "./provider/anthropic" +import { AzureCognitiveServicesPlugin, AzurePlugin } from "./provider/azure" +import { CerebrasPlugin } from "./provider/cerebras" +import { CloudflareAIGatewayPlugin } from "./provider/cloudflare-ai-gateway" +import { CloudflareWorkersAIPlugin } from "./provider/cloudflare-workers-ai" +import { CoherePlugin } from "./provider/cohere" +import { DeepInfraPlugin } from "./provider/deepinfra" +import { DynamicProviderPlugin } from "./provider/dynamic" +import { GatewayPlugin } from "./provider/gateway" +import { GithubCopilotPlugin } from "./provider/github-copilot" +import { GitLabPlugin } from "./provider/gitlab" +import { GooglePlugin } from "./provider/google" +import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./provider/google-vertex" +import { GroqPlugin } from "./provider/groq" +import { KiloPlugin } from "./provider/kilo" +import { LLMGatewayPlugin } from "./provider/llmgateway" +import { MistralPlugin } from "./provider/mistral" +import { NvidiaPlugin } from "./provider/nvidia" +import { OpenAIPlugin } from "./provider/openai" +import { OpenAICompatiblePlugin } from "./provider/openai-compatible" +import { OpencodePlugin } from "./provider/opencode" +import { OpenRouterPlugin } from "./provider/openrouter" +import { PerplexityPlugin } from "./provider/perplexity" +import { SapAICorePlugin } from "./provider/sap-ai-core" +import { TogetherAIPlugin } from "./provider/togetherai" +import { VercelPlugin } from "./provider/vercel" +import { VenicePlugin } from "./provider/venice" +import { XAIPlugin } from "./provider/xai" +import { ZenmuxPlugin } from "./provider/zenmux" + +export const ProviderPlugins = [ + AlibabaPlugin, + AmazonBedrockPlugin, + AnthropicPlugin, + AzureCognitiveServicesPlugin, + AzurePlugin, + CerebrasPlugin, + CloudflareAIGatewayPlugin, + CloudflareWorkersAIPlugin, + CoherePlugin, + DeepInfraPlugin, + GatewayPlugin, + GithubCopilotPlugin, + GitLabPlugin, + GooglePlugin, + GoogleVertexAnthropicPlugin, + GoogleVertexPlugin, + GroqPlugin, + KiloPlugin, + LLMGatewayPlugin, + MistralPlugin, + NvidiaPlugin, + OpencodePlugin, + OpenAICompatiblePlugin, + OpenAIPlugin, + OpenRouterPlugin, + PerplexityPlugin, + SapAICorePlugin, + TogetherAIPlugin, + VercelPlugin, + VenicePlugin, + XAIPlugin, + ZenmuxPlugin, + DynamicProviderPlugin, +] diff --git a/packages/core/src/plugin/provider/index.ts b/packages/core/src/plugin/provider/index.ts deleted file mode 100644 index fd02d322a..000000000 --- a/packages/core/src/plugin/provider/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { AlibabaPlugin } from "./alibaba" -import { AmazonBedrockPlugin } from "./amazon-bedrock" -import { AnthropicPlugin } from "./anthropic" -import { AzureCognitiveServicesPlugin, AzurePlugin } from "./azure" -import { CerebrasPlugin } from "./cerebras" -import { CloudflareAIGatewayPlugin } from "./cloudflare-ai-gateway" -import { CloudflareWorkersAIPlugin } from "./cloudflare-workers-ai" -import { CoherePlugin } from "./cohere" -import { DeepInfraPlugin } from "./deepinfra" -import { DynamicProviderPlugin } from "./dynamic" -import { GatewayPlugin } from "./gateway" -import { GithubCopilotPlugin } from "./github-copilot" -import { GitLabPlugin } from "./gitlab" -import { GooglePlugin } from "./google" -import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./google-vertex" -import { GroqPlugin } from "./groq" -import { KiloPlugin } from "./kilo" -import { LLMGatewayPlugin } from "./llmgateway" -import { MistralPlugin } from "./mistral" -import { NvidiaPlugin } from "./nvidia" -import { OpenAIPlugin } from "./openai" -import { OpenAICompatiblePlugin } from "./openai-compatible" -import { OpencodePlugin } from "./opencode" -import { OpenRouterPlugin } from "./openrouter" -import { PerplexityPlugin } from "./perplexity" -import { SapAICorePlugin } from "./sap-ai-core" -import { TogetherAIPlugin } from "./togetherai" -import { VercelPlugin } from "./vercel" -import { VenicePlugin } from "./venice" -import { XAIPlugin } from "./xai" -import { ZenmuxPlugin } from "./zenmux" - -export const ProviderPlugins = [ - AlibabaPlugin, - AmazonBedrockPlugin, - AnthropicPlugin, - AzureCognitiveServicesPlugin, - AzurePlugin, - CerebrasPlugin, - CloudflareAIGatewayPlugin, - CloudflareWorkersAIPlugin, - CoherePlugin, - DeepInfraPlugin, - GatewayPlugin, - GithubCopilotPlugin, - GitLabPlugin, - GooglePlugin, - GoogleVertexAnthropicPlugin, - GoogleVertexPlugin, - GroqPlugin, - KiloPlugin, - LLMGatewayPlugin, - MistralPlugin, - NvidiaPlugin, - OpencodePlugin, - OpenAICompatiblePlugin, - OpenAIPlugin, - OpenRouterPlugin, - PerplexityPlugin, - SapAICorePlugin, - TogetherAIPlugin, - VercelPlugin, - VenicePlugin, - XAIPlugin, - ZenmuxPlugin, - DynamicProviderPlugin, -] diff --git a/packages/core/src/project/index.ts b/packages/core/src/project/index.ts deleted file mode 100644 index 6a4d5a69d..000000000 --- a/packages/core/src/project/index.ts +++ /dev/null @@ -1,130 +0,0 @@ -export * as Project from "." - -import path from "path" -import { Context, Effect, Layer, Schema } from "effect" -import { ChildProcess } from "effect/unstable/process" -import { AppFileSystem } from "../filesystem" -import { AppProcess } from "../process" -import { AbsolutePath, withStatics } from "../schema" -import type { Location } from "../location" - -export const ID = Schema.String.pipe( - Schema.brand("Project.ID"), - withStatics((schema) => ({ - global: schema.make("global"), - })), -) -export type ID = typeof ID.Type - -export interface Interface { - readonly create: (input: AbsolutePath) => Promise - readonly locations: (projectID: ID) => Promise - // opencode -> ["~/dev/projects/anomalyco/opencode", "~/.gitworktrees/anomalyci/opencode"] - // global -> ["~/.config/nvim", "/etc/nixos"] - - readonly resolve: (input: AbsolutePath) => Promise - // ~/dev/projects/anomalyco/opencode -> opencode - // ~/dev/projects/anomalyco/opencode/packages/core -> opencode - // ~/.gitworktrees/anomalyci/opencode -> opencode - // ~/.config/nvim -> global -} - -export class Service extends Context.Service()("@opencode/Project") {} - -interface GitResult { - readonly exitCode: number - readonly text: () => string -} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const proc = yield* AppProcess.Service - - const runGit = Effect.fn("Project.git")( - function* (args: string[], cwd: string) { - const result = yield* proc.run( - ChildProcess.make("git", args, { - cwd, - extendEnv: true, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }), - ) - return { - exitCode: result.exitCode, - text: () => result.stdout.toString("utf8"), - } satisfies GitResult - }, - Effect.catch(() => - Effect.succeed({ - exitCode: 1, - text: () => "", - } satisfies GitResult), - ), - ) - - const resolveGitPath = (cwd: string, value: string) => { - const trimmed = value.replace(/[\r\n]+$/, "") - if (!trimmed) return cwd - const normalized = AppFileSystem.windowsPath(trimmed) - if (path.isAbsolute(normalized)) return path.normalize(normalized) - return path.resolve(cwd, normalized) - } - - const readCachedProjectId = Effect.fnUntraced(function* (dir: string) { - return yield* fs.readFileString(path.join(dir, "opencode")).pipe( - Effect.map((x) => x.trim()), - Effect.map((x) => ID.make(x)), - Effect.catch(() => Effect.void), - ) - }) - - const resolve = async (input: AbsolutePath) => - Effect.runPromise( - Effect.gen(function* () { - const repoPath = yield* fs.up({ targets: [".git"], start: input }).pipe( - Effect.map((matches) => matches[0]), - Effect.catch(() => Effect.void), - ) - if (!repoPath) return ID.global - - const cwd = path.dirname(repoPath) - const parsed = yield* runGit(["rev-parse", "--git-dir", "--git-common-dir"], cwd) - if (parsed.exitCode !== 0) return (yield* readCachedProjectId(repoPath)) ?? ID.global - - const gitPaths = parsed - .text() - .split(/\r?\n/) - .map((item) => item.trim()) - .filter(Boolean) - const commonDir = gitPaths[1] ? resolveGitPath(cwd, gitPaths[1]) : undefined - if (!commonDir) return (yield* readCachedProjectId(repoPath)) ?? ID.global - - const cached = (yield* readCachedProjectId(repoPath)) ?? (yield* readCachedProjectId(commonDir)) - if (cached) return cached - - const id = (yield* runGit(["rev-list", "--max-parents=0", "HEAD"], cwd)) - .text() - .split("\n") - .map((item) => item.trim()) - .filter(Boolean) - .toSorted()[0] - - if (!id) return ID.global - yield* fs.writeFileString(path.join(commonDir, "opencode"), id).pipe(Effect.ignore) - return ID.make(id) - }), - ) - - return Service.of({ - create: async () => { - throw new Error("Project.create is not implemented") - }, - locations: async () => [], - resolve, - }) - }), -) diff --git a/packages/core/src/project/sql.ts b/packages/core/src/project/sql.ts index 50b93c1c5..e70cba7f5 100644 --- a/packages/core/src/project/sql.ts +++ b/packages/core/src/project/sql.ts @@ -1,6 +1,6 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" import { Timestamps } from "../database/schema.sql" -import { Project } from "." +import { Project } from "../project" export const ProjectTable = sqliteTable("project", { id: text().$type().primaryKey(), diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 054d3735e..1c237d3ec 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -1,2 +1,123 @@ -export * from "./provider/index" -export * as ProviderV2 from "./provider/index" +export * as ProviderV2 from "./provider" + +import { withStatics } from "./schema" +import { Schema } from "effect" + +export const ID = Schema.String.pipe( + Schema.brand("ProviderV2.ID"), + withStatics((schema) => ({ + // Well-known providers + opencode: schema.make("opencode"), + anthropic: schema.make("anthropic"), + openai: schema.make("openai"), + google: schema.make("google"), + googleVertex: schema.make("google-vertex"), + githubCopilot: schema.make("github-copilot"), + amazonBedrock: schema.make("amazon-bedrock"), + azure: schema.make("azure"), + openrouter: schema.make("openrouter"), + mistral: schema.make("mistral"), + gitlab: schema.make("gitlab"), + })), +) +export type ID = typeof ID.Type + +export const ModelID = Schema.String.pipe(Schema.brand("ModelID")) +export type ModelID = typeof ModelID.Type + +const OpenAIResponses = Schema.Struct({ + type: Schema.Literal("openai/responses"), + url: Schema.String, + websocket: Schema.optional(Schema.Boolean), +}) + +const OpenAICompletions = Schema.Struct({ + type: Schema.Literal("openai/completions"), + url: Schema.String, + reasoning: Schema.Union([ + Schema.Struct({ + type: Schema.Literal("reasoning_content"), + }), + Schema.Struct({ + type: Schema.Literal("reasoning_details"), + }), + ]).pipe(Schema.optional), +}) +export type OpenAICompletions = typeof OpenAICompletions.Type + +const AISDK = Schema.Struct({ + type: Schema.Literal("aisdk"), + package: Schema.String, + url: Schema.String.pipe(Schema.optional), +}) + +const AnthropicMessages = Schema.Struct({ + type: Schema.Literal("anthropic/messages"), + url: Schema.String, +}) + +const UnknownEndpoint = Schema.Struct({ + type: Schema.Literal("unknown"), +}) + +export const Endpoint = Schema.Union([ + UnknownEndpoint, + OpenAIResponses, + OpenAICompletions, + AnthropicMessages, + AISDK, +]).pipe(Schema.toTaggedUnion("type")) +export type Endpoint = typeof Endpoint.Type + +export const Options = Schema.Struct({ + headers: Schema.Record(Schema.String, Schema.String), + body: Schema.Record(Schema.String, Schema.Any), + aisdk: Schema.Struct({ + provider: Schema.Record(Schema.String, Schema.Any), + request: Schema.Record(Schema.String, Schema.Any), + }), +}) +export type Options = typeof Options.Type + +export class Info extends Schema.Class("ProviderV2.Info")({ + id: ID, + name: Schema.String, + enabled: Schema.Union([ + Schema.Literal(false), + Schema.Struct({ + via: Schema.Literal("env"), + name: Schema.String, + }), + Schema.Struct({ + via: Schema.Literal("account"), + service: Schema.String, + }), + Schema.Struct({ + via: Schema.Literal("custom"), + data: Schema.Record(Schema.String, Schema.Any), + }), + ]), + env: Schema.String.pipe(Schema.Array), + endpoint: Endpoint, + options: Options, +}) { + static empty(providerID: ID) { + return new Info({ + id: providerID, + name: providerID, + enabled: false, + env: [], + endpoint: { + type: "unknown", + }, + options: { + headers: {}, + body: {}, + aisdk: { + provider: {}, + request: {}, + }, + }, + }) + } +} diff --git a/packages/core/src/provider/index.ts b/packages/core/src/provider/index.ts deleted file mode 100644 index a30cc358b..000000000 --- a/packages/core/src/provider/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -export * as ProviderV2 from "." - -import { withStatics } from "../schema" -import { Schema } from "effect" - -export const ID = Schema.String.pipe( - Schema.brand("ProviderV2.ID"), - withStatics((schema) => ({ - // Well-known providers - opencode: schema.make("opencode"), - anthropic: schema.make("anthropic"), - openai: schema.make("openai"), - google: schema.make("google"), - googleVertex: schema.make("google-vertex"), - githubCopilot: schema.make("github-copilot"), - amazonBedrock: schema.make("amazon-bedrock"), - azure: schema.make("azure"), - openrouter: schema.make("openrouter"), - mistral: schema.make("mistral"), - gitlab: schema.make("gitlab"), - })), -) -export type ID = typeof ID.Type - -export const ModelID = Schema.String.pipe(Schema.brand("ModelID")) -export type ModelID = typeof ModelID.Type - -const OpenAIResponses = Schema.Struct({ - type: Schema.Literal("openai/responses"), - url: Schema.String, - websocket: Schema.optional(Schema.Boolean), -}) - -const OpenAICompletions = Schema.Struct({ - type: Schema.Literal("openai/completions"), - url: Schema.String, - reasoning: Schema.Union([ - Schema.Struct({ - type: Schema.Literal("reasoning_content"), - }), - Schema.Struct({ - type: Schema.Literal("reasoning_details"), - }), - ]).pipe(Schema.optional), -}) -export type OpenAICompletions = typeof OpenAICompletions.Type - -const AISDK = Schema.Struct({ - type: Schema.Literal("aisdk"), - package: Schema.String, - url: Schema.String.pipe(Schema.optional), -}) - -const AnthropicMessages = Schema.Struct({ - type: Schema.Literal("anthropic/messages"), - url: Schema.String, -}) - -const UnknownEndpoint = Schema.Struct({ - type: Schema.Literal("unknown"), -}) - -export const Endpoint = Schema.Union([ - UnknownEndpoint, - OpenAIResponses, - OpenAICompletions, - AnthropicMessages, - AISDK, -]).pipe(Schema.toTaggedUnion("type")) -export type Endpoint = typeof Endpoint.Type - -export const Options = Schema.Struct({ - headers: Schema.Record(Schema.String, Schema.String), - body: Schema.Record(Schema.String, Schema.Any), - aisdk: Schema.Struct({ - provider: Schema.Record(Schema.String, Schema.Any), - request: Schema.Record(Schema.String, Schema.Any), - }), -}) -export type Options = typeof Options.Type - -export class Info extends Schema.Class("ProviderV2.Info")({ - id: ID, - name: Schema.String, - enabled: Schema.Union([ - Schema.Literal(false), - Schema.Struct({ - via: Schema.Literal("env"), - name: Schema.String, - }), - Schema.Struct({ - via: Schema.Literal("account"), - service: Schema.String, - }), - Schema.Struct({ - via: Schema.Literal("custom"), - data: Schema.Record(Schema.String, Schema.Any), - }), - ]), - env: Schema.String.pipe(Schema.Array), - endpoint: Endpoint, - options: Options, -}) { - static empty(providerID: ID) { - return new Info({ - id: providerID, - name: providerID, - enabled: false, - env: [], - endpoint: { - type: "unknown", - }, - options: { - headers: {}, - body: {}, - aisdk: { - provider: {}, - request: {}, - }, - }, - }) - } -} diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index cfb9507e7..b5cee90a5 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -1,11 +1,5 @@ import { Option, Schema, SchemaGetter } from "effect" -export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath")) -export type AbsolutePath = typeof AbsolutePath.Type - -export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath")) -export type RelativePath = typeof RelativePath.Type - /** * Integer greater than zero. */ diff --git a/packages/core/src/session/index.ts b/packages/core/src/session.ts similarity index 95% rename from packages/core/src/session/index.ts rename to packages/core/src/session.ts index 965174a90..40f209227 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session.ts @@ -1,21 +1,21 @@ -export * as SessionV2 from "." +export * as SessionV2 from "./session" import { DateTime, Effect, Layer, Schema, Context } from "effect" import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "drizzle-orm" -import { AbsolutePath, RelativePath, withStatics } from "../schema" -import { Identifier } from "../util/identifier" -import { Project } from "../project" -import { WorkspaceV2 } from "../workspace" -import { ModelV2 } from "../model" -import { Location } from "../location" -import { SessionMessage } from "./message" -import type { Prompt } from "./prompt" -import { EventV2 } from "../event" -import { optionalOmitUndefined } from "../schema" -import { V2Schema } from "../v2-schema" -import { ProviderV2 } from "../provider" -import { Database } from "../database/database" -import { SessionMessageTable, SessionTable } from "./sql" +import { AbsolutePath, RelativePath, withStatics } from "./schema" +import { Identifier } from "./util/identifier" +import { Project } from "./project" +import { WorkspaceV2 } from "./workspace" +import { ModelV2 } from "./model" +import { Location } from "./location" +import { SessionMessage } from "./session/message" +import type { Prompt } from "./session/prompt" +import { EventV2 } from "./event" +import { optionalOmitUndefined } from "./schema" +import { V2Schema } from "./v2-schema" +import { ProviderV2 } from "./provider" +import { Database } from "./database/database" +import { SessionMessageTable, SessionTable } from "./session/sql" export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({ identifier: "Session.Delivery", diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index 58eecf287..f774286fd 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -4,7 +4,7 @@ import { ModelV2 } from "../model" import { NonNegativeInt } from "../schema" import { ToolOutput } from "../tool-output" import { V2Schema } from "../v2-schema" -import { SessionV2 } from "./index" +import { SessionV2 } from "../session" import { FileAttachment, Prompt } from "./prompt" export { FileAttachment } diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 795cc694c..92bc68dbf 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -4,7 +4,7 @@ import type { SessionMessage } from "./message" import type { Snapshot } from "../snapshot" import { PermissionV2 } from "../permission" import { Project } from "../project" -import type { ID } from "." +import type { ID } from "../session" import type { MessageID, PartID } from "./legacy" import { WorkspaceV2 } from "../workspace" import { Timestamps } from "../database/schema.sql" diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 0fcba7216..35a5a4cac 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -14,6 +14,7 @@ ".": "./src/index.ts", "./effect-sqlite": "./src/effect-sqlite/index.ts", "./effect-sqlite/migrator": "./src/effect-sqlite/migrator.ts", + "./node-sqlite": "./src/node-sqlite/index.ts", "./sqlite-core/effect": "./src/sqlite-core/effect/index.ts" }, "devDependencies": { diff --git a/packages/effect-drizzle-sqlite/src/node-sqlite/index.ts b/packages/effect-drizzle-sqlite/src/node-sqlite/index.ts new file mode 100644 index 000000000..237d20088 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/node-sqlite/index.ts @@ -0,0 +1,156 @@ +export * as NodeSqliteClient from "./index" + +import { DatabaseSync, type SQLInputValue } from "node:sqlite" +import { identity } from "effect/Function" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import * as Stream from "effect/Stream" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as Client from "effect/unstable/sql/SqlClient" +import type { Connection } from "effect/unstable/sql/SqlConnection" +import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import * as Statement from "effect/unstable/sql/Statement" + +const ATTR_DB_SYSTEM_NAME = "db.system.name" + +export const TypeId: TypeId = "~@opencode-ai/effect-drizzle-sqlite/NodeSqliteClient" +export type TypeId = "~@opencode-ai/effect-drizzle-sqlite/NodeSqliteClient" + +export interface SqliteClient extends Client.SqlClient { + readonly [TypeId]: TypeId + readonly config: SqliteClientConfig + readonly loadExtension: (path: string) => Effect.Effect + readonly updateValues: never +} + +export const SqliteClient = Context.Service("@opencode-ai/effect-drizzle-sqlite/NodeSqliteClient") + +export interface SqliteClientConfig { + readonly filename: string + readonly readonly?: boolean | undefined + readonly create?: boolean | undefined + readonly readwrite?: boolean | undefined + readonly disableWAL?: boolean | undefined + readonly timeout?: number | undefined + readonly allowExtension?: boolean | undefined + readonly spanAttributes?: Record | undefined + readonly transformResultNames?: ((str: string) => string) | undefined + readonly transformQueryNames?: ((str: string) => string) | undefined +} + +interface SqliteConnection extends Connection { + readonly loadExtension: (path: string) => Effect.Effect +} + +export const make = ( + options: SqliteClientConfig, +): Effect.Effect => + Effect.gen(function* () { + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) + const transformRows = options.transformResultNames + ? Statement.defaultTransforms(options.transformResultNames).array + : undefined + + const makeConnection = Effect.gen(function* () { + const db = new DatabaseSync(options.filename, { + readOnly: options.readonly, + timeout: options.timeout, + allowExtension: options.allowExtension, + enableForeignKeyConstraints: true, + open: true, + }) + yield* Effect.addFinalizer(() => Effect.sync(() => db.close())) + + if (options.disableWAL !== true && options.readonly !== true) { + db.exec("PRAGMA journal_mode = WAL;") + } + + const run = (sql: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = db.prepare(sql) + statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + try { + return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array>) + } catch (cause) { + return Effect.fail(new SqlError({ reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }) })) + } + }) + + const runValues = (sql: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = db.prepare(sql) + statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + statement.setReturnArrays(true) + try { + return Effect.succeed( + statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray>, + ) + } catch (cause) { + return Effect.fail(new SqlError({ reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }) })) + } + }) + + return identity({ + execute(sql, params, transformRows) { + return transformRows ? Effect.map(run(sql, params), transformRows) : run(sql, params) + }, + executeRaw(sql, params) { + return run(sql, params) + }, + executeValues(sql, params) { + return runValues(sql, params) + }, + executeUnprepared(sql, params, transformRows) { + return this.execute(sql, params, transformRows) + }, + executeStream() { + return Stream.die("executeStream not implemented") + }, + loadExtension: (path) => + Effect.try({ + try: () => db.loadExtension(path), + catch: (cause) => + new SqlError({ reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }) }), + }), + }) + }) + + const semaphore = yield* Semaphore.make(1) + const connection = yield* makeConnection + const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) + const transactionAcquirer = Effect.uninterruptibleMask((restore) => { + const fiber = Fiber.getCurrent()! + const scope = Context.getUnsafe(fiber.context, Scope.Scope) + return Effect.as( + Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), + connection, + ) + }) + + return Object.assign( + (yield* Client.make({ + acquirer, + compiler, + transactionAcquirer, + spanAttributes: [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"], + ], + transformRows, + })) as SqliteClient, + { + [TypeId]: TypeId as TypeId, + config: options, + loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)), + }, + ) + }) + +export const layer = (config: SqliteClientConfig): Layer.Layer => + Layer.effectContext( + Effect.map(make(config), (client) => Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client))), + ).pipe(Layer.provide(Reactivity.layer)) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts index 2ad4562d1..058ff1a05 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts @@ -1,6 +1,6 @@ import { SessionID } from "@/session/schema" import { SessionMessage } from "@opencode-ai/core/session/message" -import { SessionV2 } from "@opencode-ai/core/session/index" +import { SessionV2 } from "@opencode-ai/core/session" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts index eb71de0a8..0514ea56a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts @@ -1,4 +1,4 @@ -import { SessionV2 } from "@opencode-ai/core/session/index" +import { SessionV2 } from "@opencode-ai/core/session" import { Layer } from "effect" import { layer as v2LocationLayer } from "../groups/v2/location" import { messageHandlers } from "./v2/message" diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts index 8f29940fb..c9cfe33bc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts @@ -1,5 +1,5 @@ import { SessionMessage } from "@opencode-ai/core/session/message" -import { SessionV2 } from "@opencode-ai/core/session/index" +import { SessionV2 } from "@opencode-ai/core/session" import { Effect, Schema } from "effect" import * as DateTime from "effect/DateTime" import { HttpApiBuilder } from "effect/unstable/httpapi" diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts index 2ff8100f3..4033aa831 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -1,5 +1,5 @@ import { WorkspaceID } from "@/control-plane/schema" -import { SessionV2 } from "@opencode-ai/core/session/index" +import { SessionV2 } from "@opencode-ai/core/session" import { DateTime, Effect, Option, Schema } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" diff --git a/packages/opencode/src/session/schema.ts b/packages/opencode/src/session/schema.ts index e889a61ce..4a49d110c 100644 --- a/packages/opencode/src/session/schema.ts +++ b/packages/opencode/src/session/schema.ts @@ -1,7 +1,7 @@ import { Schema } from "effect" import { Identifier } from "@/id/id" -import { SessionV2 } from "@opencode-ai/core/session/index" +import { SessionV2 } from "@opencode-ai/core/session" import { withStatics } from "@opencode-ai/core/schema" export const SessionID = SessionV2.ID diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 869326d87..1d55dd50e 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -8,10 +8,9 @@ import { tmpdirScoped } from "../fixture/fixture" import { GlobalBus } from "../../src/bus/global" import { ProjectID } from "../../src/project/schema" import { Database } from "@/storage/db" -import { ProjectTable } from "@/project/project.sql" -import { SessionTable } from "@/session/session.sql" -import { PermissionTable } from "@/session/session.sql" -import { WorkspaceTable } from "@/control-plane/workspace.sql" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql" +import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql" import { eq } from "drizzle-orm" import { Hash } from "@opencode-ai/core/util/hash" import { SessionID } from "@/session/schema" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 3c0398e8f..fd202a5c5 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -18,7 +18,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" -import { SessionV2 } from "@opencode-ai/core/session/index" +import { SessionV2 } from "@opencode-ai/core/session" import { ModelID, ProviderID } from "../../src/provider/schema" import type { Provider } from "@/provider/provider" import * as SessionProcessorModule from "../../src/session/processor" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 6d28f20d7..17a9bc038 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -35,7 +35,7 @@ import { SessionRevert } from "../../src/session/revert" import { SessionRunState } from "../../src/session/run-state" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" -import { SessionV2 } from "@opencode-ai/core/session/index" +import { SessionV2 } from "@opencode-ai/core/session" import { Skill } from "../../src/skill" import { SystemPrompt } from "../../src/session/system" import { Shell } from "../../src/shell/shell"