mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-28 00:03:39 +00:00
chore: effect pattern lint infrastructure (#35210)
This commit is contained in:
parent
1b88ff8d53
commit
1401901529
70 changed files with 524 additions and 234 deletions
|
|
@ -14,7 +14,8 @@
|
|||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"lint:effect-patterns": "ast-grep scan --rule script/ast-grep/no-json-parse-cast.yml packages/core/src packages/server/src packages/protocol/src && ast-grep scan --rule script/ast-grep/no-nested-effect-service-yield.yml packages/core/src packages/server/src packages/protocol/src",
|
||||
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
||||
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EOL } from "node:os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Context, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Context, Effect, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect"
|
||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { createServer } from "node:http"
|
||||
import { createRoutes } from "@opencode-ai/server/routes"
|
||||
|
|
@ -60,7 +59,7 @@ export default Runtime.handler(
|
|||
if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`)
|
||||
const updater = yield* Updater.Service
|
||||
yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped)
|
||||
return yield* (input.stdio ? waitForStdinClose() : Effect.never)
|
||||
return yield* input.stdio ? waitForStdinClose() : Effect.never
|
||||
}).pipe(Effect.annotateLogs({ role: "server" })),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { EOL } from "os"
|
||||
import { Option } from "effect"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Effect } from "effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import * as Command from "effect/unstable/cli/Command"
|
||||
import { Effect, FileSystem, Scope } from "effect"
|
||||
import { Command } from "effect/unstable/cli"
|
||||
import { Spec } from "./spec"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Updater } from "../services/updater"
|
||||
import { FileSystem, Scope } from "effect"
|
||||
|
||||
export type Input<Value> =
|
||||
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
||||
|
|
@ -12,11 +11,21 @@ export type Input<Value> =
|
|||
? Input
|
||||
: never
|
||||
|
||||
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
type RuntimeHandler = (
|
||||
input: unknown,
|
||||
) => Effect.Effect<void, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
type Loader<Node extends Spec.Any> = () => Promise<{
|
||||
default: (input: Input<Node>) => Effect.Effect<void, any, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
default: (
|
||||
input: Input<Node>,
|
||||
) => Effect.Effect<void, any, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
}>
|
||||
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
type ProvidedCommand = Command.Command<
|
||||
string,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope
|
||||
>
|
||||
|
||||
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
||||
? Loader<Node>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as Command from "effect/unstable/cli/Command"
|
||||
import { Command } from "effect/unstable/cli"
|
||||
|
||||
type Options<Config extends Command.Command.Config, Commands extends ReadonlyArray<Any>> = {
|
||||
readonly description?: string
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||
import * as NodeServices from "@effect/platform-node/NodeServices"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Layer, Logger, References } from "effect"
|
||||
import { NodeFileSystem, NodeRuntime, NodeServices } from "@effect/platform-node"
|
||||
import { Effect, Layer, Logger, References } from "effect"
|
||||
import { Commands } from "./commands/commands"
|
||||
import { Runtime } from "./framework/runtime"
|
||||
import { Logging } from "@opencode-ai/core/observability/logging"
|
||||
|
|
@ -46,7 +43,11 @@ const Handlers = Runtime.handlers(Commands, {
|
|||
serve: () => import("./commands/handlers/serve"),
|
||||
})
|
||||
|
||||
Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe(
|
||||
Effect.logInfo("cli starting", {
|
||||
version: InstallationVersion,
|
||||
channel: InstallationChannel,
|
||||
local: InstallationLocal,
|
||||
}).pipe(
|
||||
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
|
||||
Effect.annotateLogs({ role: "cli" }),
|
||||
Effect.provide(Updater.layer),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as AccountV2 from "./account"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
||||
import type { HttpClientError } from "effect/unstable/http"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("AccountID"))
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@ export * as ConfigExperimental from "./experimental"
|
|||
|
||||
import { Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Policy as PolicyV2 } from "../policy"
|
||||
import { Policy } from "../policy"
|
||||
|
||||
// Each core domain exports the policy actions it supports. Adding an action to
|
||||
// this union makes it valid in authored config while keeping Policy generic.
|
||||
export const PolicyAction = Schema.Union([Catalog.PolicyActions])
|
||||
|
||||
export class Policy extends Schema.Class<Policy>("ConfigV2.Experimental.Policy")({
|
||||
...PolicyV2.Info.fields,
|
||||
class PolicyConfig extends Schema.Class<PolicyConfig>("ConfigV2.Experimental.Policy")({
|
||||
...Policy.Info.fields,
|
||||
action: PolicyAction,
|
||||
}) {}
|
||||
|
||||
export { PolicyConfig as Policy }
|
||||
|
||||
export class Experimental extends Schema.Class<Experimental>("ConfigV2.Experimental")({
|
||||
policies: Policy.pipe(Schema.Array, Schema.optional),
|
||||
policies: PolicyConfig.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,17 @@
|
|||
import type * as Arr from "effect/Array"
|
||||
import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node"
|
||||
import * as NodePath from "@effect/platform-node/NodePath"
|
||||
import * as Deferred from "effect/Deferred"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Exit from "effect/Exit"
|
||||
import * as FileSystem from "effect/FileSystem"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Path from "effect/Path"
|
||||
import * as PlatformError from "effect/PlatformError"
|
||||
import * as Predicate from "effect/Predicate"
|
||||
import type * as Scope from "effect/Scope"
|
||||
import * as Sink from "effect/Sink"
|
||||
import * as Stream from "effect/Stream"
|
||||
import * as ChildProcess from "effect/unstable/process/ChildProcess"
|
||||
import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import type { NonEmptyReadonlyArray } from "effect/Array"
|
||||
import { NodeFileSystem, NodePath, NodeSink, NodeStream } from "@effect/platform-node"
|
||||
import { Deferred, Effect, Exit, FileSystem, Layer, Path, PlatformError, Predicate, Sink, Stream } from "effect"
|
||||
import type { Scope } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import {
|
||||
ChildProcessSpawner,
|
||||
ExitCode,
|
||||
make as makeSpawner,
|
||||
make,
|
||||
makeHandle,
|
||||
ProcessId,
|
||||
type ChildProcessHandle,
|
||||
} from "effect/unstable/process/ChildProcessSpawner"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as NodeChildProcess from "node:child_process"
|
||||
import { PassThrough } from "node:stream"
|
||||
import launch from "cross-spawn"
|
||||
|
|
@ -71,7 +62,7 @@ const flatten = (command: ChildProcess.Command) => {
|
|||
if (commands.length === 0) throw new Error("flatten produced empty commands array")
|
||||
const [head, ...tail] = commands
|
||||
return {
|
||||
commands: [head, ...tail] as Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
|
||||
commands: [head, ...tail] as NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +87,7 @@ const toPlatformError = (
|
|||
|
||||
type ExitSignal = Deferred.Deferred<readonly [code: number | null, signal: NodeJS.Signals | null]>
|
||||
|
||||
export const make = Effect.gen(function* () {
|
||||
const makeCrossSpawnSpawner = Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
|
||||
|
|
@ -494,12 +485,12 @@ export const make = Effect.gen(function* () {
|
|||
},
|
||||
)
|
||||
|
||||
return makeSpawner(spawnCommand)
|
||||
return make(spawnCommand)
|
||||
})
|
||||
|
||||
const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
|
||||
ChildProcessSpawner,
|
||||
make,
|
||||
makeCrossSpawnSpawner,
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as Database from "./database"
|
||||
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { layer as sqliteLayer } from "#sqlite"
|
||||
import { layer } from "#sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Global } from "../global"
|
||||
import { Flag } from "../flag/flag"
|
||||
|
|
@ -19,7 +19,7 @@ export interface Interface {
|
|||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
const databaseLayer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDatabase
|
||||
|
|
@ -37,7 +37,7 @@ const layer = Layer.effect(
|
|||
)
|
||||
|
||||
export function layerFromPath(filename: string) {
|
||||
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
|
||||
return databaseLayer.pipe(Layer.provide(layer({ filename })))
|
||||
}
|
||||
|
||||
export function path() {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function apply(db: Database) {
|
|||
sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`,
|
||||
)
|
||||
if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations)
|
||||
if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table")
|
||||
if (tables.length > 0) return yield* Effect.die(new Error("Database is not empty and has no session table"))
|
||||
yield* db.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* schema.up(tx)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import { Database } from "bun:sqlite"
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Fiber from "effect/Fiber"
|
||||
import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
|
||||
import { identity } from "effect/Function"
|
||||
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 { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
||||
import * as Statement from "effect/unstable/sql/Statement"
|
||||
import { Sqlite } from "./sqlite"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
|
|
@ -20,7 +13,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|||
const TypeId = "~@opencode-ai/core/database/SqliteBun" as const
|
||||
type TypeId = typeof TypeId
|
||||
|
||||
interface SqliteClient extends Client.SqlClient {
|
||||
interface SqliteClient extends SqlClient.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: Config
|
||||
readonly export: Effect.Effect<Uint8Array, SqlError>
|
||||
|
|
@ -57,7 +50,7 @@ const make = (options: Config) =>
|
|||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||
const statement = native.query(query)
|
||||
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
||||
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
|
||||
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||
try {
|
||||
return Effect.succeed((statement.all(...(params as any)) ?? []) as Array<Record<string, unknown>>)
|
||||
} catch (cause) {
|
||||
|
|
@ -73,7 +66,7 @@ const make = (options: Config) =>
|
|||
Effect.withFiber<Array<unknown[]>, SqlError>((fiber) => {
|
||||
const statement = native.query(query)
|
||||
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
||||
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
|
||||
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||
try {
|
||||
return Effect.succeed((statement.values(...(params as any)) ?? []) as Array<unknown[]>)
|
||||
} catch (cause) {
|
||||
|
|
@ -130,7 +123,7 @@ const make = (options: Config) =>
|
|||
})
|
||||
|
||||
const client = Object.assign(
|
||||
(yield* Client.make({
|
||||
(yield* SqlClient.make({
|
||||
acquirer,
|
||||
compiler,
|
||||
transactionAcquirer,
|
||||
|
|
@ -166,7 +159,7 @@ const nativeLayer = (config: Config) =>
|
|||
}),
|
||||
)
|
||||
|
||||
const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
|
||||
const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
|
||||
|
||||
const drizzleLayer = Layer.effect(
|
||||
Sqlite.Drizzle,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
|
||||
import { drizzle } from "drizzle-orm/node-sqlite"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Fiber from "effect/Fiber"
|
||||
import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
|
||||
import { identity } from "effect/Function"
|
||||
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 { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
||||
import * as Statement from "effect/unstable/sql/Statement"
|
||||
import { Sqlite } from "./sqlite"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
|
|
@ -20,7 +13,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|||
const TypeId = "~@opencode-ai/core/database/SqliteNode" as const
|
||||
type TypeId = typeof TypeId
|
||||
|
||||
interface SqliteClient extends Client.SqlClient {
|
||||
interface SqliteClient extends SqlClient.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: Config
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
|
|
@ -56,7 +49,7 @@ const make = (options: Config) =>
|
|||
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
||||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||
const statement = native.prepare(query)
|
||||
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
||||
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||
try {
|
||||
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>)
|
||||
} catch (cause) {
|
||||
|
|
@ -71,7 +64,7 @@ const make = (options: Config) =>
|
|||
const runValues = (query: string, params: ReadonlyArray<unknown> = []) =>
|
||||
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
|
||||
const statement = native.prepare(query)
|
||||
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
||||
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||
statement.setReturnArrays(true)
|
||||
try {
|
||||
return Effect.succeed(
|
||||
|
|
@ -124,7 +117,7 @@ const make = (options: Config) =>
|
|||
})
|
||||
|
||||
const client = Object.assign(
|
||||
(yield* Client.make({
|
||||
(yield* SqlClient.make({
|
||||
acquirer,
|
||||
compiler,
|
||||
transactionAcquirer,
|
||||
|
|
@ -161,7 +154,7 @@ const nativeLayer = (config: Config) =>
|
|||
}),
|
||||
)
|
||||
|
||||
const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
|
||||
const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
|
||||
|
||||
const drizzleLayer = Layer.effect(
|
||||
Sqlite.Drizzle,
|
||||
|
|
|
|||
|
|
@ -98,10 +98,14 @@ export const layer = Layer.effect(
|
|||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const forms = yield* Cache.makeWith<ID, Entry>(() => Effect.die("Form cache must be used via set/getSuccess, never get"), {
|
||||
capacity: Number.MAX_SAFE_INTEGER,
|
||||
timeToLive: (exit) => (Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION),
|
||||
})
|
||||
const forms = yield* Cache.makeWith<ID, Entry>(
|
||||
() => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")),
|
||||
{
|
||||
capacity: Number.MAX_SAFE_INTEGER,
|
||||
timeToLive: (exit) =>
|
||||
Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION,
|
||||
},
|
||||
)
|
||||
|
||||
const find = Effect.fn("Form.find")(function* (id: ID) {
|
||||
return yield* Cache.getSuccess(forms, id).pipe(
|
||||
|
|
@ -131,7 +135,9 @@ export const layer = Layer.effect(
|
|||
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
||||
}
|
||||
const form: Info =
|
||||
input.mode === "form" ? { ...base, mode: "form", fields: input.fields } : { ...base, mode: "url", url: input.url }
|
||||
input.mode === "form"
|
||||
? { ...base, mode: "form", fields: input.fields }
|
||||
: { ...base, mode: "url", url: input.url }
|
||||
const entry: Entry = {
|
||||
form,
|
||||
state: { status: "pending" },
|
||||
|
|
@ -149,7 +155,9 @@ export const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const form = yield* create(input)
|
||||
const entry = yield* find(form.id).pipe(Effect.orDie)
|
||||
return yield* restore(Deferred.await(entry.deferred)).pipe(Effect.onInterrupt(() => Effect.ignore(cancel(form.id))))
|
||||
return yield* restore(Deferred.await(entry.deferred)).pipe(
|
||||
Effect.onInterrupt(() => Effect.ignore(cancel(form.id))),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
@ -301,7 +309,8 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined
|
|||
return `Form field has invalid pattern: ${field.key}`
|
||||
}
|
||||
}
|
||||
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return `Expected email for form field: ${field.key}`
|
||||
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
||||
return `Expected email for form field: ${field.key}`
|
||||
if (field.format === "uri" && !isUri(value)) return `Expected URI for form field: ${field.key}`
|
||||
if (field.format === "date" && !isDate(value)) return `Expected date for form field: ${field.key}`
|
||||
if (field.format === "date-time" && !isDateTime(value)) return `Expected date-time for form field: ${field.key}`
|
||||
|
|
@ -324,8 +333,10 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined
|
|||
if (field.type === "multiselect") {
|
||||
if (!isStringArray(value)) return `Expected string array for form field: ${field.key}`
|
||||
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
|
||||
if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}`
|
||||
if (field.maxItems !== undefined && value.length > field.maxItems) return `Too many selections for form field: ${field.key}`
|
||||
if (field.minItems !== undefined && value.length < field.minItems)
|
||||
return `Too few selections for form field: ${field.key}`
|
||||
if (field.maxItems !== undefined && value.length > field.maxItems)
|
||||
return `Too many selections for form field: ${field.key}`
|
||||
if (!field.custom && value.some((item) => !field.options.some((option) => option.value === item))) {
|
||||
return `Invalid option for form field: ${field.key}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path"
|
||||
import path, { dirname, isAbsolute, join, relative, sep } from "path"
|
||||
import { realpathSync } from "fs"
|
||||
import * as NFS from "fs/promises"
|
||||
import { readdir } from "fs/promises"
|
||||
import { lookup } from "mime-types"
|
||||
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
|
||||
import type { PlatformError } from "effect/PlatformError"
|
||||
|
|
@ -78,7 +78,7 @@ export namespace FSUtil {
|
|||
const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) {
|
||||
return yield* Effect.tryPromise({
|
||||
try: async () => {
|
||||
const entries = await NFS.readdir(dirPath, { withFileTypes: true })
|
||||
const entries = await readdir(dirPath, { withFileTypes: true })
|
||||
return entries.map(
|
||||
(e): DirEntry => ({
|
||||
name: e.name,
|
||||
|
|
@ -90,8 +90,8 @@ export namespace FSUtil {
|
|||
})
|
||||
})
|
||||
|
||||
const resolve = Effect.fn("FileSystem.resolve")(function* (path: string) {
|
||||
const resolved = pathResolve(windowsPath(path))
|
||||
const resolve = Effect.fn("FileSystem.resolve")(function* (input: string) {
|
||||
const resolved = path.resolve(windowsPath(input))
|
||||
return yield* fs.realPath(resolved).pipe(
|
||||
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)),
|
||||
Effect.orDie,
|
||||
|
|
@ -219,7 +219,7 @@ export namespace FSUtil {
|
|||
|
||||
export function normalizePath(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
const resolved = pathResolve(windowsPath(p))
|
||||
const resolved = path.resolve(windowsPath(p))
|
||||
try {
|
||||
return realpathSync.native(resolved)
|
||||
} catch {
|
||||
|
|
@ -237,7 +237,7 @@ export namespace FSUtil {
|
|||
}
|
||||
|
||||
export function resolve(p: string): string {
|
||||
const resolved = pathResolve(windowsPath(p))
|
||||
const resolved = path.resolve(windowsPath(p))
|
||||
try {
|
||||
return normalizePath(realpathSync(resolved))
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { create as createIdentifier } from "@opencode-ai/schema/identifier"
|
||||
import { create } from "@opencode-ai/schema/identifier"
|
||||
|
||||
const prefixes = {
|
||||
job: "job",
|
||||
|
|
@ -23,7 +23,7 @@ export function descending(prefix: keyof typeof prefixes, given?: string) {
|
|||
|
||||
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
||||
if (!given) {
|
||||
return create(prefixes[prefix], direction)
|
||||
return createID(prefixes[prefix], direction)
|
||||
}
|
||||
|
||||
if (!given.startsWith(prefixes[prefix])) {
|
||||
|
|
@ -32,10 +32,12 @@ function generateID(prefix: keyof typeof prefixes, direction: "descending" | "as
|
|||
return given
|
||||
}
|
||||
|
||||
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
||||
return prefix + "_" + createIdentifier(direction === "descending", timestamp)
|
||||
function createID(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
||||
return prefix + "_" + create(direction === "descending", timestamp)
|
||||
}
|
||||
|
||||
export { createID as create }
|
||||
|
||||
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
||||
export function timestamp(id: string): number {
|
||||
const prefix = id.split("_")[0]
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ const layer = Layer.effect(
|
|||
.get()
|
||||
.integrations.get(input.integrationID)
|
||||
?.methods.some((method) => method.type === "key")
|
||||
if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`)
|
||||
if (!method) return yield* Effect.die(new Error(`Key method not found: ${input.integrationID}`))
|
||||
yield* credentials.create({
|
||||
integrationID: input.integrationID,
|
||||
label: input.label,
|
||||
|
|
@ -418,7 +418,7 @@ const layer = Layer.effect(
|
|||
oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
|
||||
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
|
||||
if (!method) {
|
||||
return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`)
|
||||
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
|
||||
}
|
||||
const attemptScope = yield* Scope.fork(scope)
|
||||
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
|
||||
|
|
@ -475,7 +475,7 @@ const layer = Layer.effect(
|
|||
attempt: {
|
||||
status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
|
||||
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
|
||||
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`)
|
||||
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${attemptID}`))
|
||||
if (attempt.status === "failed") {
|
||||
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
|
||||
}
|
||||
|
|
@ -488,12 +488,13 @@ const layer = Layer.effect(
|
|||
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
|
||||
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
|
||||
})
|
||||
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`)
|
||||
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
|
||||
if (attempt.status !== "pending") return
|
||||
if (attempt.authorization.mode === "code" && input.code === undefined) {
|
||||
return yield* new CodeRequiredError({ attemptID: input.attemptID })
|
||||
}
|
||||
if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`)
|
||||
if (attempt.completing)
|
||||
return yield* Effect.die(new Error(`OAuth attempt already completing: ${input.attemptID}`))
|
||||
const callback =
|
||||
attempt.authorization.mode === "auto"
|
||||
? attempt.authorization.callback
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { QuestionV2 } from "./question"
|
|||
import { Shell } from "./shell"
|
||||
import { Reference } from "./reference"
|
||||
import { ReferenceGuidance } from "./reference/guidance"
|
||||
import * as SessionRunnerLLM from "./session/runner/llm"
|
||||
import { SessionRunnerLLM } from "./session/runner/llm"
|
||||
import { SessionRunnerModel } from "./session/runner/model"
|
||||
import { SessionCompaction } from "./session/compaction"
|
||||
import { SessionTitle } from "./session/title"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as PermissionV2 from "./permission"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
|
||||
import { Context, Deferred, Effect, Layer, Schema } from "effect"
|
||||
import { Permission } from "@opencode-ai/schema/permission"
|
||||
import { EventV2 } from "./event"
|
||||
import { Location } from "./location"
|
||||
|
|
@ -11,7 +11,9 @@ import { SessionStore } from "./session/store"
|
|||
import { Wildcard } from "./util/wildcard"
|
||||
import { PermissionSaved } from "./permission/saved"
|
||||
|
||||
export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission"
|
||||
const PermissionEffect = Permission.Effect
|
||||
export { PermissionEffect as Effect }
|
||||
export { Rule, Ruleset } from "@opencode-ai/schema/permission"
|
||||
const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
|
||||
|
||||
export const ID = Permission.ID
|
||||
|
|
@ -90,12 +92,12 @@ export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset {
|
|||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly ask: (input: AssertInput) => EffectRuntime.Effect<AskResult, SessionV2.NotFoundError>
|
||||
readonly assert: (input: AssertInput) => EffectRuntime.Effect<void, Error | SessionV2.NotFoundError>
|
||||
readonly reply: (input: ReplyInput) => EffectRuntime.Effect<void, NotFoundError>
|
||||
readonly get: (id: ID) => EffectRuntime.Effect<Request | undefined>
|
||||
readonly forSession: (sessionID: SessionV2.ID) => EffectRuntime.Effect<ReadonlyArray<Request>>
|
||||
readonly list: () => EffectRuntime.Effect<ReadonlyArray<Request>>
|
||||
readonly ask: (input: AssertInput) => Effect.Effect<AskResult, SessionV2.NotFoundError>
|
||||
readonly assert: (input: AssertInput) => Effect.Effect<void, Error | SessionV2.NotFoundError>
|
||||
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
|
||||
readonly get: (id: ID) => Effect.Effect<Request | undefined>
|
||||
readonly forSession: (sessionID: SessionV2.ID) => Effect.Effect<ReadonlyArray<Request>>
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Permission") {}
|
||||
|
|
@ -108,7 +110,7 @@ interface Pending {
|
|||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
EffectRuntime.gen(function* () {
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const location = yield* Location.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
|
|
@ -116,28 +118,25 @@ const layer = Layer.effect(
|
|||
const saved = yield* PermissionSaved.Service
|
||||
const pending = new Map<ID, Pending>()
|
||||
|
||||
yield* EffectRuntime.addFinalizer(() =>
|
||||
EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
|
||||
discard: true,
|
||||
}).pipe(
|
||||
EffectRuntime.ensuring(
|
||||
EffectRuntime.sync(() => {
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
pending.clear()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const savedRules = EffectRuntime.fnUntraced(function* () {
|
||||
const savedRules = Effect.fnUntraced(function* () {
|
||||
return (yield* saved.list({ projectID: location.project.id })).map(
|
||||
(item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }),
|
||||
)
|
||||
})
|
||||
|
||||
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (
|
||||
sessionID: SessionV2.ID,
|
||||
agentID?: AgentV2.ID,
|
||||
) {
|
||||
const configured = Effect.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID, agentID?: AgentV2.ID) {
|
||||
const session = yield* sessions.get(sessionID)
|
||||
if (!session) return yield* new SessionV2.NotFoundError({ sessionID })
|
||||
const agent = yield* agents.resolve(agentID ?? session.agent)
|
||||
|
|
@ -152,7 +151,7 @@ const layer = Layer.effect(
|
|||
return rules.filter((rule) => Wildcard.match(input.action, rule.action))
|
||||
}
|
||||
|
||||
const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) {
|
||||
const evaluateInput = Effect.fnUntraced(function* (input: AssertInput) {
|
||||
const rules = yield* configured(input.sessionID, input.agent)
|
||||
if (denied(input, rules)) return { effect: "deny" as const, rules }
|
||||
const all = [...rules, ...(yield* savedRules())]
|
||||
|
|
@ -174,29 +173,30 @@ const layer = Layer.effect(
|
|||
}
|
||||
|
||||
const create = (request: Request, agent?: AgentV2.ID) =>
|
||||
EffectRuntime.uninterruptible(
|
||||
EffectRuntime.gen(function* () {
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
|
||||
const item = { request, agent, deferred }
|
||||
if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`)
|
||||
if (pending.has(request.id))
|
||||
return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`))
|
||||
pending.set(request.id, item)
|
||||
yield* events
|
||||
.publish(Event.Asked, request)
|
||||
.pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))))
|
||||
.pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id))))
|
||||
return item
|
||||
}),
|
||||
)
|
||||
|
||||
const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) {
|
||||
const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) {
|
||||
const result = yield* evaluateInput(input)
|
||||
const value = request(input)
|
||||
if (result.effect === "ask") yield* create(value, input.agent)
|
||||
return { id: value.id, effect: result.effect }
|
||||
})
|
||||
|
||||
const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) =>
|
||||
EffectRuntime.uninterruptibleMask((restore) =>
|
||||
EffectRuntime.gen(function* () {
|
||||
const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) =>
|
||||
Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* evaluateInput(input)
|
||||
if (result.effect === "deny") {
|
||||
return yield* new DeniedError({
|
||||
|
|
@ -206,8 +206,8 @@ const layer = Layer.effect(
|
|||
if (result.effect === "allow") return
|
||||
const item = yield* create(request(input), input.agent)
|
||||
return yield* restore(Deferred.await(item.deferred)).pipe(
|
||||
EffectRuntime.ensuring(
|
||||
EffectRuntime.sync(() => {
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
pending.delete(item.request.id)
|
||||
}),
|
||||
),
|
||||
|
|
@ -216,9 +216,9 @@ const layer = Layer.effect(
|
|||
),
|
||||
)
|
||||
|
||||
const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) =>
|
||||
EffectRuntime.uninterruptible(
|
||||
EffectRuntime.gen(function* () {
|
||||
const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const existing = pending.get(input.requestID)
|
||||
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
||||
yield* events.publish(Event.Replied, {
|
||||
|
|
@ -261,7 +261,7 @@ const layer = Layer.effect(
|
|||
for (const [id, item] of pending) {
|
||||
const input = { ...item.request }
|
||||
const rules = yield* configured(item.request.sessionID, item.agent).pipe(
|
||||
EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)),
|
||||
Effect.catchTag("Session.NotFoundError", () => Effect.succeed(undefined)),
|
||||
)
|
||||
if (!rules) continue
|
||||
if (denied(input, rules)) continue
|
||||
|
|
@ -284,15 +284,15 @@ const layer = Layer.effect(
|
|||
),
|
||||
)
|
||||
|
||||
const list = EffectRuntime.fn("PermissionV2.list")(function* () {
|
||||
const list = Effect.fn("PermissionV2.list")(function* () {
|
||||
return Array.from(pending.values(), (item) => item.request)
|
||||
})
|
||||
|
||||
const get = EffectRuntime.fn("PermissionV2.get")(function* (id: ID) {
|
||||
const get = Effect.fn("PermissionV2.get")(function* (id: ID) {
|
||||
return pending.get(id)?.request
|
||||
})
|
||||
|
||||
const forSession = EffectRuntime.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) {
|
||||
const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) {
|
||||
return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const layer = Layer.effect(
|
|||
let host: Parameters<PluginDefinition["effect"]>[0]
|
||||
|
||||
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginDefinition["effect"]) {
|
||||
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
|
||||
if (loading.has(id)) return yield* Effect.die(new Error(`Plugin load cycle detected for ${id}`))
|
||||
|
||||
yield* locks.withLock(id)(
|
||||
Effect.sync(() => {
|
||||
|
|
@ -90,7 +90,7 @@ const layer = Layer.effect(
|
|||
})
|
||||
|
||||
const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
|
||||
if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`)
|
||||
if (loading.has(id)) return yield* Effect.die(new Error(`Cannot remove plugin ${id} while it is loading`))
|
||||
|
||||
yield* locks.withLock(id)(
|
||||
State.batch(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export * as PluginHost from "./host"
|
||||
|
||||
import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { AISDK } from "../aisdk"
|
||||
|
|
@ -40,7 +40,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||
workspaceID: location.workspaceID,
|
||||
project: location.project,
|
||||
})
|
||||
const locationRef = (input?: Parameters<Interface["agent"]["list"]>[0]) =>
|
||||
const locationRef = (input?: Parameters<PluginContext["agent"]["list"]>[0]) =>
|
||||
input?.location === undefined
|
||||
? undefined
|
||||
: Location.Ref.make({
|
||||
|
|
@ -305,5 +305,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||
command: runtime.session.command,
|
||||
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
||||
},
|
||||
} satisfies Interface
|
||||
} satisfies PluginContext
|
||||
})
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export interface Cell {
|
|||
|
||||
export const makeCell = (): Cell => ({})
|
||||
|
||||
const unavailable = <A, E, R>() => Effect.die("Plugin runtime is unavailable") as Effect.Effect<A, E, R>
|
||||
const unavailable = <A, E, R>() => Effect.die(new Error("Plugin runtime is unavailable")) as Effect.Effect<A, E, R>
|
||||
const require = <A, E, R>(cell: Cell, f: (runtime: Interface) => Effect.Effect<A, E, R>) =>
|
||||
Effect.suspend(() => {
|
||||
const runtime = cell.runtime
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
export * as Policy from "./policy"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { Context, Effect as EffectRuntime, Layer, Schema } from "effect"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Wildcard } from "./util/wildcard"
|
||||
import { Location } from "./location"
|
||||
|
||||
export const Effect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" })
|
||||
export type Effect = typeof Effect.Type
|
||||
const PolicyEffect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" })
|
||||
export { PolicyEffect as Effect }
|
||||
export type Effect = typeof PolicyEffect.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("Policy.Info")({
|
||||
action: Schema.String,
|
||||
effect: Effect,
|
||||
effect: PolicyEffect,
|
||||
resource: Schema.String,
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly load: (statements: Info[]) => EffectRuntime.Effect<void>
|
||||
readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect<Effect>
|
||||
readonly load: (statements: Info[]) => Effect.Effect<void>
|
||||
readonly evaluate: (action: string, resource: string, fallback: Effect) => Effect.Effect<Effect>
|
||||
readonly hasStatements: () => boolean
|
||||
}
|
||||
|
||||
|
|
@ -24,16 +25,16 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
EffectRuntime.gen(function* () {
|
||||
Effect.gen(function* () {
|
||||
let statements: Info[] = []
|
||||
yield* Location.Service
|
||||
|
||||
return Service.of({
|
||||
load: EffectRuntime.fn("Policy.load")(function* (input) {
|
||||
load: Effect.fn("Policy.load")(function* (input) {
|
||||
statements = input
|
||||
}),
|
||||
hasStatements: () => statements.length > 0,
|
||||
evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) {
|
||||
evaluate: Effect.fn("Policy.evaluate")(function* (action, resource, fallback) {
|
||||
return (
|
||||
statements.findLast(
|
||||
(statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
import * as DatabasePath from "../database/path"
|
||||
import { absoluteArrayColumn, absoluteColumn } from "../database/path"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import { ProjectSchema } from "./schema"
|
||||
|
||||
export const ProjectTable = sqliteTable("project", {
|
||||
id: text().$type<ProjectSchema.ID>().primaryKey(),
|
||||
worktree: DatabasePath.absoluteColumn().notNull(),
|
||||
worktree: absoluteColumn().notNull(),
|
||||
vcs: text(),
|
||||
name: text(),
|
||||
icon_url: text(),
|
||||
|
|
@ -13,7 +13,7 @@ export const ProjectTable = sqliteTable("project", {
|
|||
icon_color: text(),
|
||||
...Timestamps,
|
||||
time_initialized: integer(),
|
||||
sandboxes: DatabasePath.absoluteArrayColumn().notNull(),
|
||||
sandboxes: absoluteArrayColumn().notNull(),
|
||||
commands: text({ mode: "json" }).$type<{ start?: string }>(),
|
||||
})
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ export const ProjectDirectoryTable = sqliteTable(
|
|||
.$type<ProjectSchema.ID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
directory: DatabasePath.absoluteColumn().notNull(),
|
||||
directory: absoluteColumn().notNull(),
|
||||
type: text().$type<"main" | "root" | "git_worktree">(),
|
||||
strategy: text(),
|
||||
time_created: integer()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { spawn as create } from "bun-pty"
|
||||
import { spawn } from "bun-pty"
|
||||
import type { Opts, Proc } from "./pty"
|
||||
|
||||
export type { Disp, Exit, Opts, Proc } from "./pty"
|
||||
|
||||
export function spawn(file: string, args: string[], opts: Opts): Proc {
|
||||
const pty = create(file, args, opts)
|
||||
function spawnPty(file: string, args: string[], opts: Opts): Proc {
|
||||
const pty = spawn(file, args, opts)
|
||||
return {
|
||||
pid: pty.pid,
|
||||
onData(listener) {
|
||||
|
|
@ -24,3 +24,5 @@ export function spawn(file: string, args: string[], opts: Opts): Proc {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
export { spawnPty as spawn }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// ast-grep-ignore: no-star-import
|
||||
import * as pty from "@lydell/node-pty"
|
||||
import type { Opts, Proc } from "./pty"
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function matches(record: Scope, input: Scope) {
|
|||
|
||||
// Tickets are inserted via Cache.set and removed atomically via invalidateWhen. The lookup is
|
||||
// never invoked; it dies if it ever is, which would signal a misuse of the Service interface.
|
||||
const noLookup = () => Effect.die("PtyTicket cache must be used via set/invalidateWhen, never get")
|
||||
const noLookup = () => Effect.die(new Error("PtyTicket cache must be used via set/invalidateWhen, never get"))
|
||||
|
||||
// Visible for tests so the TTL can be shortened. Production uses `layer` with the default TTL.
|
||||
export const make = (ttl: Duration.Input = DEFAULT_TTL) =>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ export * as SessionCompaction from "./compaction"
|
|||
|
||||
import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Model } from "@opencode-ai/llm"
|
||||
import { Context, DateTime, Effect, Layer, Stream } from "effect"
|
||||
import type { Config } from "../config"
|
||||
import { Config as ConfigV2 } from "../config"
|
||||
import type { EventV2 } from "../event"
|
||||
import { EventV2 as EventV2Service } from "../event"
|
||||
import { Config } from "../config"
|
||||
import { EventV2 } from "../event"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { llmClient } from "../effect/app-node-platform"
|
||||
import { SessionEvent } from "./event"
|
||||
|
|
@ -311,9 +309,9 @@ const make = (dependencies: Dependencies) => {
|
|||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2Service.Service
|
||||
const events = yield* EventV2.Service
|
||||
const llm = yield* LLMClient.Service
|
||||
const config = yield* ConfigV2.Service
|
||||
const config = yield* Config.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const compaction = make({ events, llm, config: yield* config.entries() })
|
||||
|
||||
|
|
@ -336,5 +334,5 @@ export const layer = Layer.effect(
|
|||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [EventV2Service.node, llmClient, ConfigV2.node, SessionRunnerModel.node],
|
||||
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const rewrite = Effect.fnUntraced(function* (
|
|||
.returning({ sessionID: SessionContextCheckpointTable.session_id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die("Context checkpoint not found")
|
||||
if (!updated) return yield* Effect.die(new Error("Context checkpoint not found"))
|
||||
})
|
||||
|
||||
const advance = Effect.fnUntraced(function* (
|
||||
|
|
@ -127,5 +127,5 @@ const advance = Effect.fnUntraced(function* (
|
|||
.returning({ sessionID: SessionContextCheckpointTable.session_id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die("Context checkpoint not found")
|
||||
if (!updated) return yield* Effect.die(new Error("Context checkpoint not found"))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const layer = Layer.effect(
|
|||
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError>({
|
||||
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
|
||||
const session = yield* store.get(sessionID)
|
||||
if (!session) return yield* Effect.die(`Session not found: ${sessionID}`)
|
||||
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
|
||||
return yield* SessionRunner.Service.use((runner) => runner.run({ sessionID, force })).pipe(
|
||||
Effect.provide(locations.get(session.location)),
|
||||
Effect.tapCause((cause) =>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
|
|||
.pipe(
|
||||
Effect.flatMap((event) =>
|
||||
event.durable === undefined
|
||||
? Effect.die("Prompt admission event is missing aggregate sequence")
|
||||
? Effect.die(new Error("Prompt admission event is missing aggregate sequence"))
|
||||
: Effect.succeed(
|
||||
Admitted.make({
|
||||
admittedSeq: event.durable.seq,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
.where(eq(SessionTable.id, event.data.parentID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!parent) return yield* Effect.die(`Fork parent session not found: ${event.data.parentID}`)
|
||||
if (!parent) return yield* Effect.die(new Error(`Fork parent session not found: ${event.data.parentID}`))
|
||||
const boundary = event.data.messageID
|
||||
? yield* db
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
|
|
@ -172,7 +172,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
.pipe(Effect.orDie)
|
||||
: undefined
|
||||
if (event.data.messageID && !boundary)
|
||||
return yield* Effect.die(`Fork boundary message not found: ${event.data.messageID}`)
|
||||
return yield* Effect.die(new Error(`Fork boundary message not found: ${event.data.messageID}`))
|
||||
const copied = yield* db
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
|
|
@ -341,7 +341,8 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
const updateMessage = (message: SessionMessage.Message) => {
|
||||
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
|
||||
if (event.durable === undefined)
|
||||
return Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
const encoded = encodeMessage(message)
|
||||
const { id, type, ...data } = encoded
|
||||
return db
|
||||
|
|
@ -418,7 +419,7 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
}
|
||||
|
||||
function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Message) {
|
||||
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
|
||||
if (event.durable === undefined) return Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
const encoded = encodeMessage(message)
|
||||
const { id, type, ...data } = encoded
|
||||
return db
|
||||
|
|
@ -585,7 +586,8 @@ const layer = Layer.effectDiscard(
|
|||
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
|
||||
yield* events.project(SessionEvent.Prompted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInput.projectPrompted(db, {
|
||||
id: event.data.messageID,
|
||||
sessionID: event.data.sessionID,
|
||||
|
|
@ -599,7 +601,8 @@ const layer = Layer.effectDiscard(
|
|||
)
|
||||
yield* events.project(SessionEvent.PromptAdmitted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInput.projectAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.data.messageID,
|
||||
|
|
@ -670,7 +673,7 @@ const layer = Layer.effectDiscard(
|
|||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!boundary) return yield* Effect.die(`Revert boundary message not found: ${event.data.messageID}`)
|
||||
if (!boundary) return yield* Effect.die(new Error(`Revert boundary message not found: ${event.data.messageID}`))
|
||||
yield* db
|
||||
.delete(SessionMessageTable)
|
||||
.where(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export * as SessionRunnerLLM from "./llm"
|
||||
|
||||
import {
|
||||
LLM,
|
||||
LLMClient,
|
||||
|
|
@ -119,7 +121,7 @@ const layer = Layer.effect(
|
|||
const forkTitle = yield* FiberSet.makeRuntime<never, void, never>()
|
||||
const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) {
|
||||
const session = yield* store.get(sessionID)
|
||||
if (!session) return yield* Effect.die(`Session not found: ${sessionID}`)
|
||||
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
|
||||
return session
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ export * as SessionRunnerModel from "./model"
|
|||
|
||||
import { makeLocationNode } from "../../effect/app-node"
|
||||
import { type Model } from "@opencode-ai/llm"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
|
||||
import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
})
|
||||
const currentAssistantMessageID = () =>
|
||||
assistantMessageID === undefined
|
||||
? Effect.die("Tool event before assistant step start")
|
||||
? Effect.die(new Error("Tool event before assistant step start"))
|
||||
: Effect.succeed(assistantMessageID)
|
||||
|
||||
const fragments = (
|
||||
|
|
@ -95,20 +95,20 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
const chunks = new Map<string, string[]>()
|
||||
const start = (id: string) =>
|
||||
Effect.suspend(() => {
|
||||
if (chunks.has(id)) return Effect.die(`Duplicate ${name} start: ${id}`)
|
||||
if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`))
|
||||
chunks.set(id, [])
|
||||
return Effect.void
|
||||
})
|
||||
const append = (id: string, value: string) =>
|
||||
Effect.suspend(() => {
|
||||
const current = chunks.get(id)
|
||||
if (!current) return Effect.die(`${name} delta before start: ${id}`)
|
||||
if (!current) return Effect.die(new Error(`${name} delta before start: ${id}`))
|
||||
current.push(value)
|
||||
return Effect.void
|
||||
})
|
||||
const end = Effect.fnUntraced(function* (id: string, providerMetadata?: ProviderMetadata) {
|
||||
const current = chunks.get(id)
|
||||
if (!current) return yield* Effect.die(`${name} end before start: ${id}`)
|
||||
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
|
||||
yield* ended(id, current.join(""), providerMetadata)
|
||||
chunks.delete(id)
|
||||
})
|
||||
|
|
@ -144,7 +144,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
const toolInput = fragments("tool input", (callID, value) =>
|
||||
Effect.gen(function* () {
|
||||
const tool = tools.get(callID)
|
||||
if (!tool) return yield* Effect.die(`Tool input end before start: ${callID}`)
|
||||
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${callID}`))
|
||||
yield* events.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
|
|
@ -163,7 +163,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
})
|
||||
|
||||
const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
|
||||
if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`)
|
||||
if (tools.has(event.id)) return yield* Effect.die(new Error(`Duplicate tool input start: ${event.id}`))
|
||||
const assistantMessageID = yield* startAssistant()
|
||||
tools.set(event.id, {
|
||||
assistantMessageID,
|
||||
|
|
@ -185,10 +185,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
|
||||
const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
|
||||
const tool = tools.get(event.id)
|
||||
if (!tool) return yield* Effect.die(`Tool input end before start: ${event.id}`)
|
||||
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${event.id}`))
|
||||
if (tool.name !== event.name)
|
||||
return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||
if (tool.inputEnded) return yield* Effect.die(`Duplicate tool input end: ${event.id}`)
|
||||
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (tool.inputEnded) return yield* Effect.die(new Error(`Duplicate tool input end: ${event.id}`))
|
||||
yield* toolInput.end(event.id)
|
||||
})
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
|
||||
const assistantMessageIDForTool = (callID: string) => {
|
||||
const tool = tools.get(callID)
|
||||
return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(`Unknown tool call: ${callID}`)
|
||||
return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${callID}`))
|
||||
}
|
||||
|
||||
const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (
|
||||
|
|
@ -293,10 +293,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
return
|
||||
case "tool-input-delta": {
|
||||
const tool = tools.get(event.id)
|
||||
if (!tool) return yield* Effect.die(`Tool input delta before start: ${event.id}`)
|
||||
if (!tool) return yield* Effect.die(new Error(`Tool input delta before start: ${event.id}`))
|
||||
if (tool.name !== event.name)
|
||||
return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||
if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`)
|
||||
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (tool.inputEnded) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`))
|
||||
yield* toolInput.append(event.id, event.text)
|
||||
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
|
|
@ -315,8 +315,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
const tool = tools.get(event.id)!
|
||||
if (!tool.inputEnded) yield* endToolInput(event)
|
||||
if (tool.name !== event.name)
|
||||
return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||
if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`)
|
||||
return yield* Effect.die(new Error(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
|
||||
tool.called = true
|
||||
tool.providerExecuted = event.providerExecuted === true
|
||||
tool.providerMetadata = event.providerMetadata
|
||||
|
|
@ -336,12 +336,12 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
}
|
||||
case "tool-result": {
|
||||
const tool = tools.get(event.id)
|
||||
if (!tool?.called) return yield* Effect.die(`Tool result before call: ${event.id}`)
|
||||
if (!tool?.called) return yield* Effect.die(new Error(`Tool result before call: ${event.id}`))
|
||||
if (tool.name !== event.name)
|
||||
return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||
return yield* Effect.die(new Error(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (tool.settled) {
|
||||
if (event.result.type === "error") return
|
||||
return yield* Effect.die(`Duplicate tool result: ${event.id}`)
|
||||
return yield* Effect.die(new Error(`Duplicate tool result: ${event.id}`))
|
||||
}
|
||||
tool.settled = true
|
||||
const result = settledOutput(event.output, event.result)
|
||||
|
|
@ -375,10 +375,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
}
|
||||
case "tool-error": {
|
||||
const tool = tools.get(event.id)
|
||||
if (!tool?.called) return yield* Effect.die(`Tool error before call: ${event.id}`)
|
||||
if (!tool?.called) return yield* Effect.die(new Error(`Tool error before call: ${event.id}`))
|
||||
if (tool.name !== event.name)
|
||||
return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||
if (tool.settled) return yield* Effect.die(`Duplicate tool error: ${event.id}`)
|
||||
return yield* Effect.die(new Error(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool error: ${event.id}`))
|
||||
tool.settled = true
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
|
|
@ -396,7 +396,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
case "step-finish":
|
||||
yield* flush()
|
||||
assistantActive = false
|
||||
if (stepSettlement) return yield* Effect.die("Duplicate step finish")
|
||||
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
|
||||
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
|
||||
return
|
||||
case "finish":
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { sqliteTable, text, integer, index, primaryKey, real, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||
import * as DatabasePath from "../database/path"
|
||||
import { directoryColumn, pathColumn } from "../database/path"
|
||||
import { ProjectTable } from "../project/sql"
|
||||
import type { SessionMessage } from "./message"
|
||||
import type { Prompt } from "./prompt"
|
||||
|
|
@ -31,8 +31,8 @@ export const SessionTable = sqliteTable(
|
|||
workspace_id: text().$type<WorkspaceV2.ID>(),
|
||||
parent_id: text().$type<SessionSchema.ID>(),
|
||||
slug: text().notNull(),
|
||||
directory: DatabasePath.directoryColumn().notNull(),
|
||||
path: DatabasePath.pathColumn(),
|
||||
directory: directoryColumn().notNull(),
|
||||
path: pathColumn(),
|
||||
title: text().notNull(),
|
||||
version: text().notNull(),
|
||||
share_url: text(),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import path from "path"
|
|||
import { spawn, type ChildProcess } from "child_process"
|
||||
import { readFile } from "fs/promises"
|
||||
import { statSync } from "fs"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { which } from "../util/which"
|
||||
|
|
@ -46,13 +46,13 @@ export async function killTree(proc: ChildProcess, opts?: { exited?: () => boole
|
|||
|
||||
try {
|
||||
process.kill(-pid, "SIGTERM")
|
||||
await sleep(SIGKILL_TIMEOUT_MS)
|
||||
await setTimeout(SIGKILL_TIMEOUT_MS)
|
||||
if (!opts?.exited?.()) {
|
||||
process.kill(-pid, "SIGKILL")
|
||||
}
|
||||
} catch {
|
||||
proc.kill("SIGTERM")
|
||||
await sleep(SIGKILL_TIMEOUT_MS)
|
||||
await setTimeout(SIGKILL_TIMEOUT_MS)
|
||||
if (!opts?.exited?.()) {
|
||||
proc.kill("SIGKILL")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3424,9 +3424,10 @@ describe("SessionRunnerLLM", () => {
|
|||
streamStarted = undefined
|
||||
response = [LLMEvent.textStart({ id: "text-1" }), LLMEvent.textStart({ id: "text-1" })]
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(
|
||||
"Duplicate text start: text-1",
|
||||
)
|
||||
const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))
|
||||
expect(defect).toBeInstanceOf(Error)
|
||||
if (!(defect instanceof Error)) return
|
||||
expect(defect.message).toBe("Duplicate text start: text-1")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -3468,9 +3469,10 @@ describe("SessionRunnerLLM", () => {
|
|||
streamStarted = undefined
|
||||
response = [LLMEvent.toolInputDelta({ id: "call-1", name: "read", text: "{}" })]
|
||||
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(
|
||||
"Tool input delta before start: call-1",
|
||||
)
|
||||
const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))
|
||||
expect(defect).toBeInstanceOf(Error)
|
||||
if (!(defect instanceof Error)) return
|
||||
expect(defect.message).toBe("Tool input delta before start: call-1")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as ServerAuth from "./auth"
|
||||
|
||||
import { Config as EffectConfig, Context, Effect, Layer, Option, Redacted } from "effect"
|
||||
import { Context, Effect, Layer, Option, Redacted } from "effect"
|
||||
import { all, option, string, withDefault } from "effect/Config"
|
||||
|
||||
export type Credentials = {
|
||||
password?: string
|
||||
|
|
@ -27,9 +28,9 @@ export class Config extends Context.Service<Config, Info>()("@opencode/ServerAut
|
|||
this,
|
||||
Effect.gen(function* () {
|
||||
return Config.of(
|
||||
yield* EffectConfig.all({
|
||||
password: EffectConfig.string("OPENCODE_SERVER_PASSWORD").pipe(EffectConfig.option),
|
||||
username: EffectConfig.string("OPENCODE_SERVER_USERNAME").pipe(EffectConfig.withDefault("opencode")),
|
||||
yield* all({
|
||||
password: string("OPENCODE_SERVER_PASSWORD").pipe(option),
|
||||
username: string("OPENCODE_SERVER_USERNAME").pipe(withDefault("opencode")),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { Sse } from "effect/unstable/encoding"
|
||||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import * as Sse from "effect/unstable/encoding/Sse"
|
||||
import { Api } from "../api"
|
||||
|
||||
const subscriberCapacity = 256
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Location } from "@opencode-ai/core/location"
|
|||
import { Effect, Queue } from "effect"
|
||||
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import * as Socket from "effect/unstable/socket/Socket"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
import { Api } from "../api"
|
||||
import { CorsConfig, isAllowedRequestOrigin } from "../cors"
|
||||
import { ForbiddenError, PtyNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { handlers } from "./handlers"
|
|||
import { authorizationLayer } from "./middleware/authorization"
|
||||
import { schemaErrorLayer } from "./middleware/schema-error"
|
||||
import { PtyEnvironment } from "./pty-environment"
|
||||
import { layer as locationLayer } from "./location"
|
||||
import { layer } from "./location"
|
||||
import { formLocationLayer } from "./middleware/form-location"
|
||||
import { sessionLocationLayer } from "./middleware/session-location"
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
Layer.provide(handlers),
|
||||
Layer.provide(formLocationLayer),
|
||||
Layer.provide(sessionLocationLayer),
|
||||
Layer.provide(locationLayer),
|
||||
Layer.provide(layer),
|
||||
Layer.provide(authorizationLayer),
|
||||
Layer.provide(schemaErrorLayer),
|
||||
Layer.provide(auth),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
id: no-drizzle-column-name
|
||||
snapshots:
|
||||
? |
|
||||
const table = sqliteTable("session", {
|
||||
projectID: text("project_id").notNull(),
|
||||
createdAt: integer("time_created").notNull(),
|
||||
})
|
||||
: labels:
|
||||
- source: text("project_id")
|
||||
style: primary
|
||||
start: 52
|
||||
end: 70
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
id: no-effect-die-string
|
||||
snapshots:
|
||||
Effect.die("boom"):
|
||||
labels:
|
||||
- source: Effect.die("boom")
|
||||
style: primary
|
||||
start: 0
|
||||
end: 18
|
||||
- source: '"boom"'
|
||||
style: secondary
|
||||
start: 11
|
||||
end: 17
|
||||
- source: ("boom")
|
||||
style: secondary
|
||||
start: 10
|
||||
end: 18
|
||||
Effect.die(`boom ${value}`):
|
||||
labels:
|
||||
- source: Effect.die(`boom ${value}`)
|
||||
style: primary
|
||||
start: 0
|
||||
end: 27
|
||||
- source: '`boom ${value}`'
|
||||
style: secondary
|
||||
start: 11
|
||||
end: 26
|
||||
- source: (`boom ${value}`)
|
||||
style: secondary
|
||||
start: 10
|
||||
end: 27
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
id: no-import-alias
|
||||
snapshots:
|
||||
import { baz, foo as bar } from "./foo":
|
||||
labels:
|
||||
- source: foo as bar
|
||||
style: primary
|
||||
start: 14
|
||||
end: 24
|
||||
- source: bar
|
||||
style: secondary
|
||||
start: 21
|
||||
end: 24
|
||||
import { foo as bar } from "./foo":
|
||||
labels:
|
||||
- source: foo as bar
|
||||
style: primary
|
||||
start: 9
|
||||
end: 19
|
||||
- source: bar
|
||||
style: secondary
|
||||
start: 16
|
||||
end: 19
|
||||
import { foo as bar, baz } from "./foo":
|
||||
labels:
|
||||
- source: foo as bar
|
||||
style: primary
|
||||
start: 9
|
||||
end: 19
|
||||
- source: bar
|
||||
style: secondary
|
||||
start: 16
|
||||
end: 19
|
||||
import { type Foo as Bar, baz } from "./foo":
|
||||
labels:
|
||||
- source: type Foo as Bar
|
||||
style: primary
|
||||
start: 9
|
||||
end: 24
|
||||
- source: Bar
|
||||
style: secondary
|
||||
start: 21
|
||||
end: 24
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
id: no-json-parse-cast
|
||||
snapshots:
|
||||
const value = JSON.parse(input) as Record<string, unknown>:
|
||||
labels:
|
||||
- source: JSON.parse(input) as Record<string, unknown>
|
||||
style: primary
|
||||
start: 14
|
||||
end: 58
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
id: no-nested-effect-service-yield
|
||||
snapshots:
|
||||
? |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).client.run()
|
||||
})
|
||||
: labels:
|
||||
- source: (yield* Foo.Service).client.run()
|
||||
style: primary
|
||||
start: 35
|
||||
end: 68
|
||||
? |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).run()
|
||||
})
|
||||
: labels:
|
||||
- source: (yield* Foo.Service).run()
|
||||
style: primary
|
||||
start: 35
|
||||
end: 61
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
id: no-star-import
|
||||
snapshots:
|
||||
import * as Foo from "./foo":
|
||||
labels:
|
||||
- source: '* as Foo'
|
||||
style: primary
|
||||
start: 7
|
||||
end: 15
|
||||
- source: import * as Foo from "./foo"
|
||||
style: secondary
|
||||
start: 0
|
||||
end: 28
|
||||
import type * as Foo from "./foo":
|
||||
labels:
|
||||
- source: '* as Foo'
|
||||
style: primary
|
||||
start: 12
|
||||
end: 20
|
||||
- source: import type * as Foo from "./foo"
|
||||
style: secondary
|
||||
start: 0
|
||||
end: 33
|
||||
14
script/ast-grep/rule-tests/no-drizzle-column-name-test.yml
Normal file
14
script/ast-grep/rule-tests/no-drizzle-column-name-test.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
id: no-drizzle-column-name
|
||||
valid:
|
||||
- |
|
||||
const table = sqliteTable("session", {
|
||||
project_id: text().notNull(),
|
||||
time_created: integer().notNull(),
|
||||
payload: text({ mode: "json" }),
|
||||
})
|
||||
invalid:
|
||||
- |
|
||||
const table = sqliteTable("session", {
|
||||
projectID: text("project_id").notNull(),
|
||||
createdAt: integer("time_created").notNull(),
|
||||
})
|
||||
7
script/ast-grep/rule-tests/no-effect-die-string-test.yml
Normal file
7
script/ast-grep/rule-tests/no-effect-die-string-test.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
id: no-effect-die-string
|
||||
valid:
|
||||
- Effect.die(new Error("boom"))
|
||||
- Effect.fail("boom")
|
||||
invalid:
|
||||
- Effect.die("boom")
|
||||
- Effect.die(`boom ${value}`)
|
||||
13
script/ast-grep/rule-tests/no-import-alias-test.yml
Normal file
13
script/ast-grep/rule-tests/no-import-alias-test.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
id: no-import-alias
|
||||
valid:
|
||||
- import { foo } from "./foo"
|
||||
- import type { Foo } from "./foo"
|
||||
- import foo from "./foo"
|
||||
- export { foo as bar } from "./foo"
|
||||
- import type { Plugin as EffectPlugin } from "./foo"
|
||||
- import type { Foo as Bar, Baz } from "./foo"
|
||||
invalid:
|
||||
- import { foo as bar } from "./foo"
|
||||
- import { baz, foo as bar } from "./foo"
|
||||
- import { foo as bar, baz } from "./foo"
|
||||
- import { type Foo as Bar, baz } from "./foo"
|
||||
6
script/ast-grep/rule-tests/no-json-parse-cast-test.yml
Normal file
6
script/ast-grep/rule-tests/no-json-parse-cast-test.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
id: no-json-parse-cast
|
||||
valid:
|
||||
- const value = JSON.parse(input)
|
||||
- const value = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(input)
|
||||
invalid:
|
||||
- const value = JSON.parse(input) as Record<string, unknown>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
id: no-nested-effect-service-yield
|
||||
valid:
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Foo.Service
|
||||
yield* service.run()
|
||||
})
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
const db = (yield* Database.Service).db
|
||||
yield* db.run()
|
||||
})
|
||||
invalid:
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).run()
|
||||
})
|
||||
- |
|
||||
Effect.gen(function* () {
|
||||
yield* (yield* Foo.Service).client.run()
|
||||
})
|
||||
8
script/ast-grep/rule-tests/no-star-import-test.yml
Normal file
8
script/ast-grep/rule-tests/no-star-import-test.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
id: no-star-import
|
||||
valid:
|
||||
- import { Foo } from "./foo"
|
||||
- import Foo from "./foo"
|
||||
- export * as Foo from "./foo"
|
||||
invalid:
|
||||
- import * as Foo from "./foo"
|
||||
- import type * as Foo from "./foo"
|
||||
22
script/ast-grep/rules/no-drizzle-column-name.yml
Normal file
22
script/ast-grep/rules/no-drizzle-column-name.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
id: no-drizzle-column-name
|
||||
language: TypeScript
|
||||
message: Use snake_case object keys instead of explicit drizzle column names.
|
||||
severity: error
|
||||
files:
|
||||
- packages/core/src/**/sql.ts
|
||||
- packages/core/src/**/*.sql.ts
|
||||
rule:
|
||||
any:
|
||||
- pattern: text($NAME)
|
||||
- pattern: text($NAME, $$$ARGS)
|
||||
- pattern: integer($NAME)
|
||||
- pattern: integer($NAME, $$$ARGS)
|
||||
- pattern: blob($NAME)
|
||||
- pattern: blob($NAME, $$$ARGS)
|
||||
- pattern: real($NAME)
|
||||
- pattern: real($NAME, $$$ARGS)
|
||||
- pattern: numeric($NAME)
|
||||
- pattern: numeric($NAME, $$$ARGS)
|
||||
constraints:
|
||||
NAME:
|
||||
kind: string
|
||||
22
script/ast-grep/rules/no-effect-die-string.yml
Normal file
22
script/ast-grep/rules/no-effect-die-string.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
id: no-effect-die-string
|
||||
language: TypeScript
|
||||
message: die with `new Error(...)`.
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- all:
|
||||
- pattern: Effect.die($MESSAGE)
|
||||
- has:
|
||||
field: arguments
|
||||
all:
|
||||
- kind: arguments
|
||||
- has:
|
||||
kind: string
|
||||
- all:
|
||||
- pattern: Effect.die($MESSAGE)
|
||||
- has:
|
||||
field: arguments
|
||||
all:
|
||||
- kind: arguments
|
||||
- has:
|
||||
kind: template_string
|
||||
14
script/ast-grep/rules/no-import-alias.yml
Normal file
14
script/ast-grep/rules/no-import-alias.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
id: no-import-alias
|
||||
language: TypeScript
|
||||
message: Do not alias value imports. For type name collisions, alias inside a dedicated `import type` statement.
|
||||
severity: error
|
||||
rule:
|
||||
all:
|
||||
- kind: import_specifier
|
||||
- has:
|
||||
field: alias
|
||||
kind: identifier
|
||||
- not:
|
||||
inside:
|
||||
pattern: import type { $$$SPECS } from "$MOD"
|
||||
stopBy: end
|
||||
10
script/ast-grep/rules/no-star-import.yml
Normal file
10
script/ast-grep/rules/no-star-import.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
id: no-star-import
|
||||
language: TypeScript
|
||||
message: Do not use star imports.
|
||||
severity: error
|
||||
rule:
|
||||
all:
|
||||
- kind: namespace_import
|
||||
- inside:
|
||||
kind: import_statement
|
||||
stopBy: end
|
||||
4
script/ast-grep/sgconfig.yml
Normal file
4
script/ast-grep/sgconfig.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ruleDirs:
|
||||
- rules
|
||||
testConfigs:
|
||||
- testDir: rule-tests
|
||||
Loading…
Add table
Add a link
Reference in a new issue