mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 17:19:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4e1de007f |
@@ -68,10 +68,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
||||
}),
|
||||
Spec.make("debug", {
|
||||
description: "Debugging and troubleshooting tools",
|
||||
commands: [
|
||||
Spec.make("agents", { description: "List all agents" }),
|
||||
Spec.make("config", { description: "Show resolved configuration" }),
|
||||
],
|
||||
commands: [Spec.make("agents", { description: "List all agents" })],
|
||||
}),
|
||||
Spec.make("console", {
|
||||
description: "Manage OpenCode Console access",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.debug.commands.config,
|
||||
Effect.fn("cli.debug.config")(function* () {
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const endpoint = found ?? (yield* Service.ensure(options))
|
||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||
const entries = yield* Effect.promise(() => client.config.get({ location: { directory: process.cwd() } }))
|
||||
process.stdout.write(JSON.stringify(entries, null, 2) + EOL)
|
||||
}),
|
||||
)
|
||||
@@ -22,7 +22,6 @@ export default Runtime.handler(Commands, (input) =>
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: Option.getOrUndefined(input.server),
|
||||
standalone: input.standalone,
|
||||
mismatch: "replace",
|
||||
onStart: (reason, previousVersion) => {
|
||||
if (reason === "version-mismatch" && preflight.begin(previousVersion)) return
|
||||
process.stderr.write(
|
||||
|
||||
@@ -10,11 +10,7 @@ export default Runtime.handler(Commands.commands.mini, (input) =>
|
||||
const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini"))
|
||||
yield* Effect.promise(async () => validateMiniTerminal())
|
||||
const serverURL = Option.getOrUndefined(input.server)
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: serverURL,
|
||||
standalone: input.standalone,
|
||||
mismatch: "replace",
|
||||
})
|
||||
const server = yield* ServerConnection.resolve({ server: serverURL, standalone: input.standalone })
|
||||
const config = yield* Config.Service
|
||||
const resolved = resolve(yield* config.get(), { terminalSuspend: process.platform !== "win32" })
|
||||
const fileSystem = yield* FileSystem.FileSystem
|
||||
|
||||
@@ -22,7 +22,6 @@ const Handlers = Runtime.handlers(Commands, {
|
||||
},
|
||||
debug: {
|
||||
agents: () => import("./commands/handlers/debug/agents"),
|
||||
config: () => import("./commands/handlers/debug/config"),
|
||||
},
|
||||
console: {
|
||||
login: () => import("./commands/handlers/console/login"),
|
||||
|
||||
@@ -42,10 +42,9 @@ export const resolve = Effect.fn("cli.server-connection.resolve")(function* (arg
|
||||
return { endpoint: yield* Standalone.start() } satisfies Resolved
|
||||
}
|
||||
|
||||
const mismatch = args.mismatch ?? "ignore"
|
||||
const options = yield* ServiceConfig.options({ checkVersion: mismatch !== "ignore" })
|
||||
const options = yield* ServiceConfig.options()
|
||||
return {
|
||||
endpoint: yield* resolveManaged({ ...options, onStart: args.onStart }, mismatch),
|
||||
endpoint: yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace"),
|
||||
service: managedService(options),
|
||||
} satisfies Resolved
|
||||
})
|
||||
|
||||
@@ -98,12 +98,12 @@ const paths = Effect.gen(function* () {
|
||||
}
|
||||
})
|
||||
|
||||
export const options = Effect.fnUntraced(function* (input: { readonly checkVersion?: boolean } = {}) {
|
||||
export const options = Effect.fnUntraced(function* () {
|
||||
const { file, legacyRegistrationFiles } = yield* paths
|
||||
yield* Effect.forEach(legacyRegistrationFiles, (legacy) => migrateRegistration(legacy, file))
|
||||
return {
|
||||
file,
|
||||
version: input.checkVersion ? OPENCODE_VERSION : undefined,
|
||||
version: OPENCODE_VERSION,
|
||||
command: [...selfCommand(), "serve", "--service"],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { OPENCODE_VERSION } from "../src/version"
|
||||
|
||||
describe("debug config command", () => {
|
||||
test("is included in troubleshooting help", async () => {
|
||||
const [debug, config] = await Promise.all([cli(["debug", "--help"]), cli(["debug", "config", "--help"])])
|
||||
|
||||
expect(debug.exitCode).toBe(0)
|
||||
expect(debug.stdout).toContain("config")
|
||||
expect(debug.stdout).toContain("Show resolved configuration")
|
||||
expect(config.exitCode).toBe(0)
|
||||
expect(config.stdout).toContain("opencode debug config [flags]")
|
||||
})
|
||||
|
||||
test("prints config entries from the invoking directory without reordering permissions", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-debug-config-"))
|
||||
const project = path.join(import.meta.dir, "..")
|
||||
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||
const entries = [
|
||||
{
|
||||
type: "document",
|
||||
path: path.join(project, "opencode.json"),
|
||||
info: {
|
||||
permissions: [
|
||||
{ action: "shell", resource: "*", effect: "ask" },
|
||||
{ action: "shell", resource: "git status", effect: "allow" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ type: "file", path: path.join(project, "opencode.json") },
|
||||
]
|
||||
let requested: URL | undefined
|
||||
const authorization: Array<string | null> = []
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(request) {
|
||||
const url = new URL(request.url)
|
||||
if (url.pathname === "/api/health") {
|
||||
return Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid })
|
||||
}
|
||||
requested = url
|
||||
authorization.push(request.headers.get("authorization"))
|
||||
return Response.json(entries)
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
await fs.mkdir(path.dirname(registration), { recursive: true })
|
||||
await fs.writeFile(
|
||||
registration,
|
||||
JSON.stringify({ version: OPENCODE_VERSION, url: server.url.toString(), pid: process.pid, password: "secret" }),
|
||||
)
|
||||
const result = await cli(["debug", "config"], project, { XDG_STATE_HOME: path.join(root, "state") })
|
||||
|
||||
expect({ exitCode: result.exitCode, stderr: result.stderr }).toEqual({ exitCode: 0, stderr: "" })
|
||||
expect(JSON.parse(result.stdout)).toEqual(entries)
|
||||
expect(requested?.pathname).toBe("/api/config")
|
||||
expect(requested?.searchParams.get("location[directory]")).toBe(project)
|
||||
expect(authorization).toEqual([`Basic ${btoa("opencode:secret")}`])
|
||||
} finally {
|
||||
server.stop(true)
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function cli(args: string[], cwd = path.join(import.meta.dir, ".."), env?: Record<string, string>) {
|
||||
const child = Bun.spawn([process.execPath, "run", path.join(import.meta.dir, "../src/index.ts"), ...args], {
|
||||
cwd,
|
||||
env: { ...process.env, ...env },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
child.exited,
|
||||
])
|
||||
return { stdout, stderr, exitCode }
|
||||
}
|
||||
@@ -55,17 +55,3 @@ test("resolution groups Effect-native lifecycle operations only for the managed
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("service options only require a matching version when requested", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-options-"))
|
||||
const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })
|
||||
const runPromise = <A, E>(effect: Effect.Effect<A, E, Global.Service | FileSystem.FileSystem | Scope.Scope>) =>
|
||||
Effect.runPromise(effect.pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer), Effect.scoped))
|
||||
|
||||
try {
|
||||
expect((await runPromise(ServiceConfig.options())).version).toBeUndefined()
|
||||
expect((await runPromise(ServiceConfig.options({ checkVersion: true }))).version).toBe(OPENCODE_VERSION)
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "@opencode-ai/protocol/client"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
import { Config } from "@opencode-ai/schema/config"
|
||||
import { Credential } from "@opencode-ai/schema/credential"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { EventLog } from "@opencode-ai/schema/event-log"
|
||||
@@ -49,7 +48,6 @@ const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmi
|
||||
const effectTypeReferences = [
|
||||
...namespaceTypes("Agent", "@opencode-ai/schema/agent", Agent),
|
||||
...namespaceTypes("Command", "@opencode-ai/schema/command", Command),
|
||||
...namespaceTypes("Config", "@opencode-ai/schema/config", Config),
|
||||
...namespaceTypes("Credential", "@opencode-ai/schema/credential", Credential),
|
||||
...namespaceTypes("Event", "@opencode-ai/schema/event", Event),
|
||||
...namespaceTypes("EventLog", "@opencode-ai/schema/event-log", EventLog),
|
||||
|
||||
@@ -38,7 +38,6 @@ import type { ProjectCopy } from "@opencode-ai/schema/project-copy"
|
||||
import type { Vcs } from "@opencode-ai/schema/vcs"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import type { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
import type { Config } from "@opencode-ai/schema/config"
|
||||
|
||||
export type Endpoint0_0Output = { readonly healthy: true; readonly version: string; readonly pid: number }
|
||||
export type HealthGetOperation<E = never> = () => Effect.Effect<Endpoint0_0Output, E>
|
||||
@@ -1596,16 +1595,6 @@ export interface WebsearchApi<E = never> {
|
||||
readonly query: WebsearchQueryOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint29_0Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint29_0Output = ReadonlyArray<Config.Entry>
|
||||
export type ConfigGetOperation<E = never> = (input?: Endpoint29_0Input) => Effect.Effect<Endpoint29_0Output, E>
|
||||
|
||||
export interface ConfigApi<E = never> {
|
||||
readonly get: ConfigGetOperation<E>
|
||||
}
|
||||
|
||||
export interface AppApi<E = never> {
|
||||
readonly health: HealthApi<E>
|
||||
readonly server: ServerApi<E>
|
||||
@@ -1636,5 +1625,4 @@ export interface AppApi<E = never> {
|
||||
readonly debug: DebugApi<E>
|
||||
readonly migration: MigrationApi<E>
|
||||
readonly websearch: WebsearchApi<E>
|
||||
readonly config: ConfigApi<E>
|
||||
}
|
||||
|
||||
@@ -220,8 +220,6 @@ import type {
|
||||
Endpoint28_0Output,
|
||||
Endpoint28_1Input,
|
||||
Endpoint28_1Output,
|
||||
Endpoint29_0Input,
|
||||
Endpoint29_0Output,
|
||||
} from "../api/api.js"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
@@ -1243,13 +1241,6 @@ const adaptGroup28 = (raw: RawClient["server.websearch"]) => ({
|
||||
query: Endpoint28_1(raw),
|
||||
})
|
||||
|
||||
const Endpoint29_0 = (raw: RawClient["server.config"]) => (input?: Endpoint29_0Input) =>
|
||||
preserveEffect<Endpoint29_0Output>()(
|
||||
raw["config.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup29 = (raw: RawClient["server.config"]) => ({ get: Endpoint29_0(raw) })
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
health: adaptGroup0(raw["server.health"]),
|
||||
server: adaptGroup1(raw["server.server"]),
|
||||
@@ -1280,7 +1271,6 @@ const adaptClient = (raw: RawClient) => ({
|
||||
debug: adaptGroup26(raw["server.debug"]),
|
||||
migration: adaptGroup27(raw["server.migration"]),
|
||||
websearch: adaptGroup28(raw["server.websearch"]),
|
||||
config: adaptGroup29(raw["server.config"]),
|
||||
})
|
||||
|
||||
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
||||
|
||||
@@ -8,7 +8,6 @@ export type {
|
||||
AppApi,
|
||||
CatalogApi,
|
||||
CommandApi,
|
||||
ConfigApi,
|
||||
EventApi,
|
||||
IntegrationApi,
|
||||
ModelApi,
|
||||
@@ -21,7 +20,6 @@ export type {
|
||||
} from "./api.js"
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Config } from "@opencode-ai/schema/config"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { Event } from "@opencode-ai/schema/event"
|
||||
export { EventLog } from "@opencode-ai/schema/event-log"
|
||||
|
||||
@@ -53,7 +53,6 @@ const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
|
||||
/** Ensure a healthy, compatible local service is running. */
|
||||
export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOptions = {}) {
|
||||
const contenders = new Set<Contender>()
|
||||
let timeouts: { readonly info: Info; readonly count: number } | undefined
|
||||
let announced = false
|
||||
let lastSpawn = 0
|
||||
let spawnDelay = 5_000
|
||||
@@ -83,18 +82,6 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
const registration = yield* registered(options.file, true)
|
||||
const info = registration.info
|
||||
const service = registration.service
|
||||
if (registration.timedOut && info !== undefined) {
|
||||
timeouts = {
|
||||
info,
|
||||
count: timeouts !== undefined && same(timeouts.info, info) ? timeouts.count + 1 : 1,
|
||||
}
|
||||
if (timeouts.count >= 3) {
|
||||
yield* announce("missing")
|
||||
yield* evict(info, options)
|
||||
timeouts = undefined
|
||||
lastSpawn = Date.now() - spawnDelay
|
||||
}
|
||||
} else timeouts = undefined
|
||||
if (service !== undefined) {
|
||||
spawnDelay = 5_000
|
||||
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
||||
@@ -195,10 +182,6 @@ type LocalService = {
|
||||
}
|
||||
|
||||
const probe = Effect.fnUntraced(function* (info: Info, allowLegacy = false) {
|
||||
return (yield* probeResult(info, allowLegacy)).service
|
||||
})
|
||||
|
||||
const probeResult = Effect.fnUntraced(function* (info: Info, allowLegacy = false) {
|
||||
const endpoint = {
|
||||
url: info.url,
|
||||
auth:
|
||||
@@ -206,53 +189,39 @@ const probeResult = Effect.fnUntraced(function* (info: Info, allowLegacy = false
|
||||
? undefined
|
||||
: { type: "basic" as const, username: "opencode", password: info.password },
|
||||
} satisfies Endpoint
|
||||
const signal = AbortSignal.timeout(2_000)
|
||||
const result = yield* Effect.promise(() =>
|
||||
const response = yield* Effect.tryPromise(() =>
|
||||
fetch(new URL("/api/health", info.url), {
|
||||
headers: headers(endpoint),
|
||||
signal,
|
||||
})
|
||||
.then(async (response) => ({ response, body: (await response.json()) as unknown }))
|
||||
.then(
|
||||
(value) => ({ value }),
|
||||
(cause: unknown) => ({ cause }),
|
||||
),
|
||||
)
|
||||
if ("cause" in result) return { service: undefined, timedOut: signal.aborted }
|
||||
const response = result.value.response
|
||||
const body = result.value.body
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
}),
|
||||
).pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
||||
if (response === undefined) return undefined
|
||||
const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
||||
const health = decodeHealth(body)
|
||||
if (Option.isSome(health)) {
|
||||
if (health.value.pid !== info.pid) return { service: undefined, timedOut: false }
|
||||
if (info.version !== undefined && health.value.version !== info.version)
|
||||
return { service: undefined, timedOut: false }
|
||||
if (health.value.pid !== info.pid) return undefined
|
||||
if (info.version !== undefined && health.value.version !== info.version) return undefined
|
||||
return {
|
||||
service: {
|
||||
info,
|
||||
endpoint,
|
||||
version: health.value.version,
|
||||
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
|
||||
legacy: false,
|
||||
} satisfies LocalService,
|
||||
timedOut: false,
|
||||
}
|
||||
info,
|
||||
endpoint,
|
||||
version: health.value.version,
|
||||
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
|
||||
legacy: false,
|
||||
} satisfies LocalService
|
||||
}
|
||||
if (
|
||||
!allowLegacy ||
|
||||
Option.isNone(decodeLegacyHealth(body)) ||
|
||||
(typeof body === "object" && body !== null && ("version" in body || "pid" in body))
|
||||
)
|
||||
return { service: undefined, timedOut: false }
|
||||
return {
|
||||
service: { info, endpoint, state: "ready", legacy: true } satisfies LocalService,
|
||||
timedOut: false,
|
||||
}
|
||||
return undefined
|
||||
return { info, endpoint, state: "ready", legacy: true } satisfies LocalService
|
||||
})
|
||||
|
||||
const registered = Effect.fnUntraced(function* (file?: string, allowLegacy = false) {
|
||||
const info = yield* read(file)
|
||||
if (info === undefined) return { info: undefined, service: undefined, timedOut: false }
|
||||
return { info, ...(yield* probeResult(info, allowLegacy)) }
|
||||
if (info === undefined) return { info: undefined, service: undefined }
|
||||
return { info, service: yield* probe(info, allowLegacy) }
|
||||
})
|
||||
|
||||
// Health-checked lookup without the version gate: lifecycle operations must be
|
||||
@@ -280,19 +249,6 @@ function same(left: Info, right: Info) {
|
||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||
}
|
||||
|
||||
const evict = Effect.fnUntraced(function* (info: Info, options: { readonly file?: string }) {
|
||||
const current = yield* read(options.file)
|
||||
if (current === undefined || !same(current, info)) return
|
||||
yield* signal(info.pid, "SIGTERM")
|
||||
const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option)
|
||||
if (Option.isSome(done)) return
|
||||
|
||||
const latest = yield* read(options.file)
|
||||
if (latest === undefined || !same(latest, info)) return
|
||||
yield* signal(info.pid, "SIGKILL")
|
||||
yield* stopped(info.pid).pipe(Effect.retry(poll))
|
||||
})
|
||||
|
||||
const kill = Effect.fnUntraced(function* (service: LocalService, options: { readonly file?: string }) {
|
||||
const requested = yield* requestStop(service)
|
||||
if (requested === "rejected") return
|
||||
|
||||
@@ -2,7 +2,6 @@ type Client = ReturnType<typeof import("./generated/client.js").make>
|
||||
|
||||
export type AgentApi = Client["agent"]
|
||||
export type CommandApi = Client["command"]
|
||||
export type ConfigApi = Client["config"]
|
||||
export type EventApi = Client["event"]
|
||||
export type IntegrationApi = Client["integration"]
|
||||
export type ModelApi = Client["model"]
|
||||
|
||||
@@ -216,8 +216,6 @@ import type {
|
||||
WebsearchProvidersOutput,
|
||||
WebsearchQueryInput,
|
||||
WebsearchQueryOutput,
|
||||
ConfigGetInput,
|
||||
ConfigGetOutput,
|
||||
} from "./types"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
@@ -1813,20 +1811,6 @@ export function make(options: ClientOptions) {
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
config: {
|
||||
get: (input?: ConfigGetInput, requestOptions?: RequestOptions) =>
|
||||
request<ConfigGetOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/config`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1698,180 +1698,6 @@ export type AgentInfo = {
|
||||
permissions: PermissionRuleset
|
||||
}
|
||||
|
||||
export type ConfigEntry =
|
||||
| {
|
||||
type: "document"
|
||||
path?: string | null
|
||||
info: {
|
||||
$schema?: string | null
|
||||
shell?: string | null
|
||||
model?: string | { providerID: string; model: string; variant?: string | null } | null
|
||||
default_agent?: string | null
|
||||
autoupdate?: boolean | "notify" | null
|
||||
share?: "manual" | "auto" | "disabled" | null
|
||||
enterprise?: { url?: string | null } | null
|
||||
username?: string | null
|
||||
permissions?: PermissionRuleset | null
|
||||
agents?: {
|
||||
[x: string]: {
|
||||
model?: string | { providerID: string; model: string; variant?: string | null } | null
|
||||
request?: { headers?: { [x: string]: string } | null; body?: { [x: string]: JsonValue } | null } | null
|
||||
system?: string | null
|
||||
description?: string | null
|
||||
mode?: "subagent" | "primary" | "all" | null
|
||||
hidden?: boolean | null
|
||||
color?: string | null
|
||||
steps?: number | null
|
||||
disabled?: boolean | null
|
||||
permissions?: PermissionRuleset | null
|
||||
}
|
||||
} | null
|
||||
snapshots?: boolean | null
|
||||
watcher?: { ignore?: Array<string> | null } | null
|
||||
formatter?:
|
||||
| boolean
|
||||
| {
|
||||
[x: string]: {
|
||||
disabled?: boolean | null
|
||||
command?: Array<string> | null
|
||||
environment?: { [x: string]: string } | null
|
||||
extensions?: Array<string> | null
|
||||
}
|
||||
}
|
||||
| null
|
||||
lsp?:
|
||||
| boolean
|
||||
| {
|
||||
[x: string]:
|
||||
| { disabled: true }
|
||||
| {
|
||||
command: Array<string>
|
||||
extensions?: Array<string> | null
|
||||
disabled?: boolean | null
|
||||
env?: { [x: string]: string } | null
|
||||
initialization?: { [x: string]: JsonValue } | null
|
||||
}
|
||||
}
|
||||
| null
|
||||
media?: {
|
||||
image?: {
|
||||
auto_resize?: boolean | null
|
||||
max_width?: number | null
|
||||
max_height?: number | null
|
||||
max_base64_bytes?: number | null
|
||||
} | null
|
||||
} | null
|
||||
tool_output?: { max_lines?: number | null; max_bytes?: number | null } | null
|
||||
mcp?: {
|
||||
timeout?: { startup?: number | null; catalog?: number | null; execution?: number | null } | null
|
||||
servers?: {
|
||||
[x: string]:
|
||||
| {
|
||||
type: "local"
|
||||
command: Array<string>
|
||||
cwd?: string | null
|
||||
environment?: { [x: string]: string } | null
|
||||
disabled?: boolean | null
|
||||
codemode?: boolean | null
|
||||
timeout?: { startup?: number | null; catalog?: number | null; execution?: number | null } | null
|
||||
}
|
||||
| {
|
||||
type: "remote"
|
||||
url: string
|
||||
headers?: { [x: string]: string } | null
|
||||
oauth?:
|
||||
| {
|
||||
client_id?: string | null
|
||||
client_secret?: string | null
|
||||
scope?: string | null
|
||||
callback_port?: number | null
|
||||
redirect_uri?: string | null
|
||||
}
|
||||
| false
|
||||
| null
|
||||
disabled?: boolean | null
|
||||
codemode?: boolean | null
|
||||
timeout?: { startup?: number | null; catalog?: number | null; execution?: number | null } | null
|
||||
}
|
||||
} | null
|
||||
} | null
|
||||
compaction?: { auto?: boolean | null; keep?: { tokens?: number | null } | null; buffer?: number | null } | null
|
||||
skills?: Array<string> | null
|
||||
commands?: {
|
||||
[x: string]: {
|
||||
template: string
|
||||
description?: string | null
|
||||
agent?: string | null
|
||||
model?: string | { providerID: string; model: string; variant?: string | null } | null
|
||||
subtask?: boolean | null
|
||||
}
|
||||
} | null
|
||||
instructions?: Array<string> | null
|
||||
references?: {
|
||||
[x: string]:
|
||||
| string
|
||||
| { repository: string; branch?: string | null; description?: string | null; hidden?: boolean | null }
|
||||
| { path: string; description?: string | null; hidden?: boolean | null }
|
||||
} | null
|
||||
websearch?: { provider: string } | null
|
||||
plugins?: Array<string | { package: string; options?: { [x: string]: JsonValue } | null }> | null
|
||||
warming?: boolean | { prompt?: string | null; interval?: string | null; duration?: string | null } | null
|
||||
providers?: {
|
||||
[x: string]: {
|
||||
name?: string | null
|
||||
env?: Array<string> | null
|
||||
package?: string | null
|
||||
settings?: { [x: string]: JsonValue } | null
|
||||
headers?: { [x: string]: string } | null
|
||||
body?: { [x: string]: JsonValue } | null
|
||||
models?: {
|
||||
[x: string]: {
|
||||
modelID?: string | null
|
||||
family?: string | null
|
||||
name?: string | null
|
||||
compatibility?: ModelCompatibility | null
|
||||
package?: string | null
|
||||
settings?: { [x: string]: JsonValue } | null
|
||||
headers?: { [x: string]: string } | null
|
||||
body?: { [x: string]: JsonValue } | null
|
||||
capabilities?: ModelCapabilities | null
|
||||
variants?: Array<{
|
||||
id: string
|
||||
settings?: { [x: string]: JsonValue } | null
|
||||
headers?: { [x: string]: string } | null
|
||||
body?: { [x: string]: JsonValue } | null
|
||||
}> | null
|
||||
cost?:
|
||||
| {
|
||||
tier?: { type: "context"; size: number } | null
|
||||
input: MoneyUSDPerMillionTokens
|
||||
output: MoneyUSDPerMillionTokens
|
||||
cache?: { read?: MoneyUSDPerMillionTokens | null; write?: MoneyUSDPerMillionTokens | null } | null
|
||||
}
|
||||
| Array<{
|
||||
tier?: { type: "context"; size: number } | null
|
||||
input: MoneyUSDPerMillionTokens
|
||||
output: MoneyUSDPerMillionTokens
|
||||
cache?: { read?: MoneyUSDPerMillionTokens | null; write?: MoneyUSDPerMillionTokens | null } | null
|
||||
}>
|
||||
| null
|
||||
disabled?: boolean | null
|
||||
limit?: { context?: number | null; input?: number | null; output?: number | null } | null
|
||||
}
|
||||
} | null
|
||||
}
|
||||
} | null
|
||||
experimental?: {
|
||||
subagent_depth?: number | null
|
||||
policies?: Array<{ action: "provider.use"; resource: string; effect: "allow" | "deny" }> | null
|
||||
} | null
|
||||
}
|
||||
}
|
||||
| { type: "directory"; path: string }
|
||||
| { type: "file"; path: string }
|
||||
| { type: "agents"; path: string }
|
||||
| { type: "claude"; path: string }
|
||||
|
||||
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
|
||||
|
||||
export type SessionPendingUser = {
|
||||
@@ -4767,11 +4593,3 @@ export type WebsearchQueryOutput = {
|
||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } }
|
||||
data: { providerID: string; results: Array<WebSearchResult> }
|
||||
}
|
||||
|
||||
export type ConfigGetInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type ConfigGetOutput = Array<ConfigEntry>
|
||||
|
||||
@@ -3,7 +3,6 @@ export type {
|
||||
AgentApi,
|
||||
CatalogApi,
|
||||
CommandApi,
|
||||
ConfigApi,
|
||||
EventApi,
|
||||
IntegrationApi,
|
||||
ModelApi,
|
||||
|
||||
@@ -34,7 +34,6 @@ async function discoverLocal(options: DiscoverOptions) {
|
||||
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
const deadline = Date.now() + 120_000
|
||||
const contenders = new Set<Contender>()
|
||||
let timeouts: { readonly info: Info; readonly count: number } | undefined
|
||||
let announced = false
|
||||
let lastSpawn = 0
|
||||
let spawnDelay = 5_000
|
||||
@@ -63,19 +62,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
while (true) {
|
||||
if (Date.now() >= deadline) throw new Error("Timed out waiting for the background service to start")
|
||||
const registration = await registered(options.file, true)
|
||||
if (registration.timedOut && registration.info !== undefined) {
|
||||
timeouts = {
|
||||
info: registration.info,
|
||||
count:
|
||||
timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
|
||||
}
|
||||
if (timeouts.count >= 3) {
|
||||
announce("missing")
|
||||
await evict(registration.info, options)
|
||||
timeouts = undefined
|
||||
lastSpawn = Date.now() - spawnDelay
|
||||
}
|
||||
} else timeouts = undefined
|
||||
|
||||
if (registration.service !== undefined) {
|
||||
spawnDelay = 5_000
|
||||
@@ -159,10 +145,6 @@ type LocalService = {
|
||||
}
|
||||
|
||||
async function probe(info: Info, allowLegacy = false): Promise<LocalService | undefined> {
|
||||
return (await probeResult(info, allowLegacy)).service
|
||||
}
|
||||
|
||||
async function probeResult(info: Info, allowLegacy = false) {
|
||||
const endpoint = {
|
||||
url: info.url,
|
||||
auth:
|
||||
@@ -170,48 +152,30 @@ async function probeResult(info: Info, allowLegacy = false) {
|
||||
? undefined
|
||||
: { type: "basic" as const, username: "opencode", password: info.password },
|
||||
} satisfies Endpoint
|
||||
const signal = AbortSignal.timeout(2_000)
|
||||
const result = await fetch(new URL("/api/health", info.url), {
|
||||
const response = await fetch(new URL("/api/health", info.url), {
|
||||
headers: headers(endpoint),
|
||||
signal,
|
||||
})
|
||||
.then(async (response) => ({
|
||||
response,
|
||||
body: (await response.json()) as ServiceHealth | { readonly healthy: true },
|
||||
}))
|
||||
.then(
|
||||
(value) => ({ value }),
|
||||
(cause: unknown) => ({ cause }),
|
||||
)
|
||||
if ("cause" in result) return { service: undefined, timedOut: signal.aborted }
|
||||
const response = result.value.response
|
||||
const body = result.value.body
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
}).catch(() => undefined)
|
||||
const body = (await response?.json().catch(() => undefined)) as ServiceHealth | { readonly healthy: true } | undefined
|
||||
if (body !== undefined && "version" in body && "pid" in body) {
|
||||
if (body.pid !== info.pid) return { service: undefined, timedOut: false }
|
||||
if (info.version !== undefined && body.version !== info.version)
|
||||
return { service: undefined, timedOut: false }
|
||||
if (body.pid !== info.pid) return undefined
|
||||
if (info.version !== undefined && body.version !== info.version) return undefined
|
||||
return {
|
||||
service: {
|
||||
info,
|
||||
endpoint,
|
||||
version: body.version,
|
||||
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
|
||||
legacy: false,
|
||||
} satisfies LocalService,
|
||||
timedOut: false,
|
||||
info,
|
||||
endpoint,
|
||||
version: body.version,
|
||||
state: response?.ok ? "ready" : response?.status === 500 ? "failed" : "waiting",
|
||||
legacy: false,
|
||||
}
|
||||
}
|
||||
if (!allowLegacy || body?.healthy !== true) return { service: undefined, timedOut: false }
|
||||
return {
|
||||
service: { info, endpoint, state: "ready", legacy: true } satisfies LocalService,
|
||||
timedOut: false,
|
||||
}
|
||||
if (!allowLegacy || body?.healthy !== true) return undefined
|
||||
return { info, endpoint, state: "ready", legacy: true }
|
||||
}
|
||||
|
||||
async function registered(file?: string, allowLegacy = false) {
|
||||
const info = await read(file)
|
||||
if (info === undefined) return { info: undefined, service: undefined, timedOut: false }
|
||||
return { info, ...(await probeResult(info, allowLegacy)) }
|
||||
if (info === undefined) return { info: undefined, service: undefined }
|
||||
return { info, service: await probe(info, allowLegacy) }
|
||||
}
|
||||
|
||||
async function find(options: { readonly file?: string }) {
|
||||
@@ -245,18 +209,6 @@ function same(left: Info, right: Info) {
|
||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||
}
|
||||
|
||||
async function evict(info: Info, options: { readonly file?: string }) {
|
||||
const current = await read(options.file)
|
||||
if (current === undefined || !same(current, info)) return
|
||||
signal(info.pid, "SIGTERM")
|
||||
if (await waitUntilStopped(info.pid)) return
|
||||
|
||||
const latest = await read(options.file)
|
||||
if (latest === undefined || !same(latest, info)) return
|
||||
signal(info.pid, "SIGKILL")
|
||||
if (!(await waitUntilStopped(info.pid))) throw new Error(`Server process ${info.pid} is still running`)
|
||||
}
|
||||
|
||||
async function kill(service: LocalService, options: { readonly file?: string }) {
|
||||
const requested = await requestStop(service)
|
||||
if (requested === "rejected") return
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Config } from "@opencode-ai/schema/config"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
@@ -11,7 +10,6 @@ const Client = await import("../src/effect")
|
||||
|
||||
test("effect entrypoint exposes canonical Schema contracts", () => {
|
||||
expect(Client.Agent).toBe(Agent)
|
||||
expect(Client.Config).toBe(Config)
|
||||
expect(Client.Model).toBe(Model)
|
||||
expect(Client.Session).toBe(Session)
|
||||
})
|
||||
|
||||
@@ -47,10 +47,6 @@ const server = Bun.serve({
|
||||
}
|
||||
if (pathname !== "/api/health") return new Response(null, { status: 404 })
|
||||
requests += 1
|
||||
if (mode === "hanging") {
|
||||
await appendFile(registration + ".requests", process.pid + "\n")
|
||||
return new Promise<Response>(() => {})
|
||||
}
|
||||
if (mode === "modern" && requests === 1) {
|
||||
await writeFile(registration + ".first-request", "")
|
||||
while (!(await Bun.file(registration + ".release").exists())) await Bun.sleep(5)
|
||||
|
||||
@@ -70,32 +70,6 @@ test("reports a failed registered service", async () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("evicts an unresponsive registered service before starting its replacement", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const existing = Bun.spawn([process.execPath, fixture, registration, "hanging"], {
|
||||
stdout: "ignore",
|
||||
stderr: "inherit",
|
||||
})
|
||||
processes.push(existing)
|
||||
await waitForFile(registration)
|
||||
const original = await Bun.file(registration).json()
|
||||
|
||||
const endpoint = await Service.ensure({
|
||||
file: registration,
|
||||
version: "test",
|
||||
command: [process.execPath, fixture, registration, "delayed", "10"],
|
||||
})
|
||||
const replacement = await Bun.file(registration).json()
|
||||
|
||||
expect((await Bun.file(registration + ".requests").text()).trim().split("\n")).toHaveLength(3)
|
||||
expect(await existing.exited).toBe(0)
|
||||
expect(replacement.pid).not.toBe(original.pid)
|
||||
expect(endpoint.url).toBe(replacement.url)
|
||||
process.kill(replacement.pid, "SIGTERM")
|
||||
await waitForExit(replacement.pid)
|
||||
}, 20_000)
|
||||
|
||||
test("requests graceful stop of the exact service instance", async () => {
|
||||
const registration = await setup("graceful")
|
||||
const info = await Bun.file(registration).json()
|
||||
|
||||
@@ -33,7 +33,6 @@ test("exposes every standard HTTP API group", () => {
|
||||
"vcs",
|
||||
"debug",
|
||||
"websearch",
|
||||
"config",
|
||||
])
|
||||
expect(Object.keys(client.debug)).toEqual(["location"])
|
||||
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
|
||||
@@ -51,34 +50,6 @@ test("exposes every standard HTTP API group", () => {
|
||||
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
||||
})
|
||||
|
||||
test("config.get returns ordered config entries for a location", async () => {
|
||||
let request: Request | undefined
|
||||
const entries = [
|
||||
{
|
||||
type: "document" as const,
|
||||
path: "/tmp/project/opencode.json",
|
||||
info: {
|
||||
permissions: [
|
||||
{ action: "shell", resource: "*", effect: "ask" as const },
|
||||
{ action: "shell", resource: "git status", effect: "allow" as const },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ type: "file" as const, path: "/tmp/project/opencode.json" },
|
||||
]
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async (input) => {
|
||||
request = input instanceof Request ? input : new Request(input)
|
||||
return Response.json(entries)
|
||||
},
|
||||
})
|
||||
|
||||
expect(await client.config.get({ location: { directory: "/tmp/project" } })).toEqual(entries)
|
||||
expect(request?.method).toBe("GET")
|
||||
expect(request?.url).toBe("http://localhost:3000/api/config?location%5Bdirectory%5D=%2Ftmp%2Fproject")
|
||||
})
|
||||
|
||||
test("websearch.query uses the public HTTP contract", async () => {
|
||||
let request: Request | undefined
|
||||
const client = OpenCode.make({
|
||||
|
||||
@@ -70,30 +70,6 @@ test("reports a failed registered service without spawning", async () => {
|
||||
expect(process.exitCode).toBe(null)
|
||||
})
|
||||
|
||||
test("evicts an unresponsive registered service before starting its replacement", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const existing = spawn(registration, "hanging")
|
||||
await waitForFile(registration)
|
||||
const original = await Bun.file(registration).json()
|
||||
|
||||
const endpoint = await run(
|
||||
Service.ensure({
|
||||
file: registration,
|
||||
version: "test",
|
||||
command: [process.execPath, fixture, registration, "delayed", "10"],
|
||||
}),
|
||||
)
|
||||
const replacement = await Bun.file(registration).json()
|
||||
|
||||
expect((await Bun.file(registration + ".requests").text()).trim().split("\n")).toHaveLength(3)
|
||||
expect(await existing.exited).toBe(0)
|
||||
expect(replacement.pid).not.toBe(original.pid)
|
||||
expect(endpoint.url).toBe(replacement.url)
|
||||
expect(await health(endpoint.url)).toEqual({ healthy: true, version: "test", pid: replacement.pid })
|
||||
process.kill(replacement.pid, "SIGTERM")
|
||||
}, 20_000)
|
||||
|
||||
test("requests graceful stop of the exact service instance", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||
"benchmark:location": "bun run script/benchmark-location.ts",
|
||||
"test": "bun test --only-failures",
|
||||
"typecheck": "tsgo -b && tsgo --noEmit -p tsconfig.tests.json"
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
|
||||
@@ -132,16 +132,14 @@ function renderMigration(name: string, sql: string) {
|
||||
return `import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: ${JSON.stringify(name)},
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
${renderStatements(sql)}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
`
|
||||
}
|
||||
|
||||
@@ -149,15 +147,13 @@ function renderSchema(sql: string) {
|
||||
return `import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
export default {
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
${renderStatements(sql)}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default schema
|
||||
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||
`
|
||||
}
|
||||
|
||||
@@ -195,10 +191,10 @@ async function formatTypescript(input: string) {
|
||||
function renderRegistry(names: string[]) {
|
||||
return `import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
export const migrations = (
|
||||
await Promise.all([
|
||||
${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
|
||||
])
|
||||
).map((module) => module.default)
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
`
|
||||
}
|
||||
|
||||
+147
-21
@@ -5,16 +5,8 @@ import path from "path"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
import { type ParseError, parse } from "jsonc-parser"
|
||||
import { Context, Effect, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect"
|
||||
import {
|
||||
AgentsDirectory,
|
||||
ClaudeDirectory,
|
||||
Directory,
|
||||
Document,
|
||||
File,
|
||||
Info,
|
||||
type Entry,
|
||||
Event,
|
||||
} from "@opencode-ai/schema/config"
|
||||
import { Permission } from "@opencode-ai/schema/permission"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Credential } from "./credential"
|
||||
import { Bus } from "./bus"
|
||||
@@ -23,11 +15,141 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Location } from "./location"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { ConfigAgent } from "./config/agent"
|
||||
import { ConfigMedia } from "./config/media"
|
||||
import { ConfigCompaction } from "./config/compaction"
|
||||
import { ConfigCommand } from "./config/command"
|
||||
import { ConfigExperimental } from "./config/experimental"
|
||||
import { ConfigFormatter } from "./config/formatter"
|
||||
import { ConfigLSP } from "./config/lsp"
|
||||
import { ConfigMCP } from "./config/mcp"
|
||||
import { ConfigModel } from "./config/model"
|
||||
import { ConfigPlugin } from "./config/plugin"
|
||||
import { ConfigProvider } from "./config/provider"
|
||||
import { ConfigReference } from "./config/reference"
|
||||
import { ConfigWebSearch } from "./config/websearch"
|
||||
import { ConfigToolOutput } from "./config/tool-output"
|
||||
import { ConfigVariable } from "./config/variable"
|
||||
import { ConfigWatcher } from "./config/watcher"
|
||||
import { ConfigWarming } from "./config/warming"
|
||||
import { ConfigV1 } from "./v1/config/config"
|
||||
import { ConfigMigrateV1 } from "./v1/config/migrate"
|
||||
import { WellKnown } from "./wellknown"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Info")({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
shell: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default shell to use for terminal and shell tool execution",
|
||||
}),
|
||||
model: ConfigModel.Selection.pipe(Schema.optional).annotate({
|
||||
description: "Default model to use when no session or agent model is selected",
|
||||
}),
|
||||
default_agent: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default primary agent to use when no session agent is selected",
|
||||
}),
|
||||
autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")])
|
||||
.pipe(Schema.optional)
|
||||
.annotate({
|
||||
description: "Automatically update or notify when a new version is available",
|
||||
}),
|
||||
share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({
|
||||
description: "Control whether sessions may be shared manually, automatically, or not at all",
|
||||
}),
|
||||
enterprise: Schema.Struct({
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
.pipe(Schema.optional)
|
||||
.annotate({
|
||||
description: "Enterprise sharing service configuration",
|
||||
}),
|
||||
username: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Username displayed in conversations and used for telemetry identity",
|
||||
}),
|
||||
permissions: Permission.Ruleset.pipe(Schema.optional).annotate({
|
||||
description: "Ordered tool permission rules applied to agent tool use",
|
||||
}),
|
||||
agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({
|
||||
description: "Named built-in agent overrides and custom agent definitions",
|
||||
}),
|
||||
snapshots: Schema.Boolean.pipe(Schema.optional).annotate({
|
||||
description: "Enable snapshots used for undo and revert behavior",
|
||||
}),
|
||||
watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({
|
||||
description: "Filesystem watcher configuration",
|
||||
}),
|
||||
formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({
|
||||
description: "Enable built-in formatters or configure formatter overrides",
|
||||
}),
|
||||
lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({
|
||||
description: "Enable built-in language servers or configure server overrides",
|
||||
}),
|
||||
media: ConfigMedia.Info.pipe(Schema.optional).annotate({
|
||||
description: "Media processing configuration",
|
||||
}),
|
||||
tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({
|
||||
description: "Tool output truncation thresholds",
|
||||
}),
|
||||
mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({
|
||||
description: "MCP server configuration",
|
||||
}),
|
||||
compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({
|
||||
description: "Conversation compaction behavior",
|
||||
}),
|
||||
skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs to discover skills from",
|
||||
}),
|
||||
commands: Schema.Record(Schema.String, ConfigCommand.Info).pipe(Schema.optional).annotate({
|
||||
description: "Named slash command definitions",
|
||||
}),
|
||||
instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs supplying ambient instructions",
|
||||
}),
|
||||
references: ConfigReference.Info.pipe(Schema.optional).annotate({
|
||||
description: "Named local directories or Git repositories available as external context",
|
||||
}),
|
||||
websearch: ConfigWebSearch.Info.pipe(Schema.optional).annotate({
|
||||
description: "Web search provider selection",
|
||||
}),
|
||||
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
||||
description: "Ordered plugin enablement directives and external package declarations",
|
||||
}),
|
||||
warming: ConfigWarming.Warming.pipe(Schema.optional).annotate({
|
||||
description: "Keep recently active sessions warm with transient model requests (default: false)",
|
||||
}),
|
||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||
experimental: ConfigExperimental.Info.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Document extends Schema.Class<Document>("Config.Document")({
|
||||
type: Schema.Literal("document"),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
info: Info,
|
||||
}) {}
|
||||
|
||||
export class Directory extends Schema.Class<Directory>("Config.Directory")({
|
||||
type: Schema.Literal("directory"),
|
||||
path: AbsolutePath,
|
||||
}) {}
|
||||
|
||||
export class File extends Schema.Class<File>("Config.File")({
|
||||
type: Schema.Literal("file"),
|
||||
path: AbsolutePath,
|
||||
}) {}
|
||||
|
||||
export class AgentsDirectory extends Schema.Class<AgentsDirectory>("Config.AgentsDirectory")({
|
||||
type: Schema.Literal("agents"),
|
||||
path: AbsolutePath,
|
||||
}) {}
|
||||
|
||||
export class ClaudeDirectory extends Schema.Class<ClaudeDirectory>("Config.ClaudeDirectory")({
|
||||
type: Schema.Literal("claude"),
|
||||
path: AbsolutePath,
|
||||
}) {}
|
||||
|
||||
export type Entry = Document | Directory | File | AgentsDirectory | ClaudeDirectory
|
||||
|
||||
export function latest<K extends keyof Info>(entries: readonly Entry[], key: K): Info[K] | undefined {
|
||||
return entries
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
@@ -174,17 +296,21 @@ export const layer = (options?: Options) => Layer.effect(
|
||||
|
||||
// We load certain files from a few other folders in the ecosystem
|
||||
const claude = [
|
||||
...new Set([
|
||||
...((yield* fs.isDir(globalClaudeDirectory)) ? [globalClaudeDirectory] : []),
|
||||
...discovered.filter((item) => path.basename(item) === ".claude"),
|
||||
]),
|
||||
].map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) }))
|
||||
...((yield* fs.isDir(globalClaudeDirectory))
|
||||
? [new ClaudeDirectory({ type: "claude", path: globalClaudeDirectory })]
|
||||
: []),
|
||||
...discovered
|
||||
.filter((item) => path.basename(item) === ".claude")
|
||||
.map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) })),
|
||||
]
|
||||
const agents = [
|
||||
...new Set([
|
||||
...((yield* fs.isDir(globalAgentsDirectory)) ? [globalAgentsDirectory] : []),
|
||||
...discovered.filter((item) => path.basename(item) === ".agents"),
|
||||
]),
|
||||
].map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) }))
|
||||
...((yield* fs.isDir(globalAgentsDirectory))
|
||||
? [new AgentsDirectory({ type: "agents", path: globalAgentsDirectory })]
|
||||
: []),
|
||||
...discovered
|
||||
.filter((item) => path.basename(item) === ".agents")
|
||||
.map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) })),
|
||||
]
|
||||
|
||||
const directories = [
|
||||
globalDirectory,
|
||||
@@ -282,7 +408,7 @@ export const layer = (options?: Options) => Layer.effect(
|
||||
if (isDeepStrictEqual(configs, next)) return
|
||||
configs = next
|
||||
yield* reconcile(next)
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
yield* bus.publish(ConfigSchema.Event.Updated, {})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export * as ConfigAgent from "./agent.js"
|
||||
export * as ConfigAgent from "./agent"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Permission } from "../permission.js"
|
||||
import { PositiveInt } from "../schema.js"
|
||||
import { ConfigModel } from "./model.js"
|
||||
import { ConfigProvider } from "./provider.js"
|
||||
import { Permission } from "@opencode-ai/schema/permission"
|
||||
import { ConfigProvider } from "./provider"
|
||||
import { ConfigModel } from "./model"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as ConfigCommand from "./command.js"
|
||||
export * as ConfigCommand from "./command"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ConfigModel } from "./model.js"
|
||||
import { ConfigModel } from "./model"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Command")({
|
||||
template: Schema.String,
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as ConfigCompaction from "./compaction.js"
|
||||
export * as ConfigCompaction from "./compaction"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { NonNegativeInt } from "../schema.js"
|
||||
import { NonNegativeInt } from "../schema"
|
||||
|
||||
export class Keep extends Schema.Class<Keep>("Config.Compaction.Keep")({
|
||||
tokens: NonNegativeInt.pipe(Schema.optional),
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
export * as ConfigExperimental from "./experimental.js"
|
||||
export * as ConfigExperimental from "./experimental"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { NonNegativeInt } from "../schema.js"
|
||||
import { ConfigPolicy } from "./policy.js"
|
||||
import { NonNegativeInt } from "../schema"
|
||||
import { ConfigPolicy } from "./policy"
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigExperimental.Info")({
|
||||
subagent_depth: NonNegativeInt.pipe(Schema.optional).annotate({
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigFormatter from "./formatter.js"
|
||||
export * as ConfigFormatter from "./formatter"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigLSP from "./lsp.js"
|
||||
export * as ConfigLSP from "./lsp"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export * as ConfigMCP from "./mcp.js"
|
||||
export * as ConfigMCP from "./mcp"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Mcp } from "../mcp.js"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
|
||||
// The MCP server config is a public wire contract (used by the mcp.add route), so it lives in
|
||||
// @opencode-ai/schema and is re-exported here.
|
||||
export const Timeout = Mcp.TimeoutConfig
|
||||
export type Timeout = Mcp.TimeoutConfig
|
||||
export const Local = Mcp.LocalConfig
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as ConfigMedia from "./media.js"
|
||||
export * as ConfigMedia from "./media"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PositiveInt } from "../schema.js"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export class Image extends Schema.Class<Image>("Config.Media.Image")({
|
||||
auto_resize: Schema.Boolean.pipe(Schema.optional),
|
||||
@@ -1,8 +1,8 @@
|
||||
export * as ConfigModel from "./model.js"
|
||||
export * as ConfigModel from "./model"
|
||||
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { Model } from "../model.js"
|
||||
import { Provider } from "../provider.js"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
|
||||
const ProviderID = Provider.ID.check(Schema.isPattern(/^[^/#]+$/))
|
||||
const ModelID = Model.ID.check(Schema.isPattern(/^[^#]+$/))
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigPlugin from "./plugin.js"
|
||||
export * as ConfigPlugin from "./plugin"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
export * as ConfigAgentPlugin from "./agent"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Document, Info, type Entry } from "@opencode-ai/schema/config"
|
||||
import { ConfigAgent } from "@opencode-ai/schema/config/agent"
|
||||
import path from "path"
|
||||
import { Effect, Option, Schema, Stream } from "effect"
|
||||
import { Agent } from "../../agent"
|
||||
import { Config } from "../../config"
|
||||
import { ConfigAgent } from "../agent"
|
||||
import { ConfigMarkdown } from "../markdown"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { ConfigAgentV1 } from "../../v1/config/agent"
|
||||
@@ -25,7 +24,7 @@ const legacySources = [
|
||||
const sourceDirectories = ["agent", "agents", "mode", "modes"] as const
|
||||
const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info)
|
||||
const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info)
|
||||
const decodeConfig = Schema.decodeUnknownOption(Info)
|
||||
const decodeConfig = Schema.decodeUnknownOption(Config.Info)
|
||||
type PathAction =
|
||||
| LocationMutation.ExternalDirectoryAuthorization["action"]
|
||||
| typeof ReadTool.name
|
||||
@@ -64,13 +63,13 @@ export const Plugin = define({
|
||||
),
|
||||
).pipe(
|
||||
Effect.map((documents) =>
|
||||
documents.filter((document): document is Document => document !== undefined),
|
||||
documents.filter((document): document is Config.Document => document !== undefined),
|
||||
),
|
||||
)
|
||||
})
|
||||
}).pipe(Effect.map((documents) => documents.flat()))
|
||||
})
|
||||
const loaded = { documents: [] as Document[] }
|
||||
const loaded = { documents: [] as Config.Document[] }
|
||||
const reload = load().pipe(
|
||||
Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))),
|
||||
Effect.andThen(ctx.agent.reload()),
|
||||
@@ -140,7 +139,7 @@ export const Plugin = define({
|
||||
|
||||
// Matches anything at or under <root>/{agent,agents,mode,modes}. No file-suffix
|
||||
// check: directory-level events such as renames carry no per-file paths.
|
||||
function isAgentSource(entries: Entry[], file: string) {
|
||||
function isAgentSource(entries: Config.Entry[], file: string) {
|
||||
return entries.some(
|
||||
(entry) =>
|
||||
entry.type === "directory" &&
|
||||
@@ -209,5 +208,5 @@ function decode(file: { directory: string; filepath: string; primary: boolean },
|
||||
}),
|
||||
)
|
||||
if (!info) return
|
||||
return new Document({ type: "document", path: file.filepath, info })
|
||||
return new Config.Document({ type: "document", path: file.filepath, info })
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
export * as ConfigCommandPlugin from "./command"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Info, type Entry } from "@opencode-ai/schema/config"
|
||||
import { ConfigCommand } from "@opencode-ai/schema/config/command"
|
||||
import path from "path"
|
||||
import { Effect, Option, Schema, Stream } from "effect"
|
||||
import { Command } from "../../command"
|
||||
import { Config } from "../../config"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { ConfigCommand } from "../command"
|
||||
import { ConfigMarkdown } from "../markdown"
|
||||
|
||||
const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
|
||||
@@ -28,7 +27,7 @@ export const Plugin = define({
|
||||
)
|
||||
}).pipe(Effect.map((documents) => documents.flat()))
|
||||
})
|
||||
const loaded = { documents: [] as { commands: Info["commands"] }[] }
|
||||
const loaded = { documents: [] as { commands: Config.Info["commands"] }[] }
|
||||
const reload = load().pipe(
|
||||
Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))),
|
||||
Effect.andThen(ctx.command.reload()),
|
||||
@@ -76,7 +75,7 @@ const sourceDirectories = ["command", "commands"] as const
|
||||
|
||||
// Matches anything at or under <root>/{command,commands}. No file-suffix check:
|
||||
// directory-level events such as renames carry no per-file paths.
|
||||
function isCommandSource(entries: Entry[], file: string) {
|
||||
function isCommandSource(entries: Config.Entry[], file: string) {
|
||||
return entries.some(
|
||||
(entry) =>
|
||||
entry.type === "directory" &&
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as ConfigPolicyPlugin from "./policy"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { Wildcard } from "../../util/wildcard"
|
||||
@@ -14,7 +13,7 @@ export const Plugin = define({
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
// User-global policy takes priority over policy authored by a repository.
|
||||
const policies = loaded.entries
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.toReversed()
|
||||
.flatMap((entry) => entry.info.experimental?.policies ?? [])
|
||||
for (const record of catalog.provider.list()) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as ConfigProviderPlugin from "./provider"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Config } from "../../config"
|
||||
@@ -108,8 +107,8 @@ export const Plugin = define({
|
||||
}),
|
||||
})
|
||||
|
||||
function configuredProviders(entries: readonly Entry[]) {
|
||||
function configuredProviders(entries: readonly Config.Entry[]) {
|
||||
return entries
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.flatMap((file) => Object.entries(file.info.providers ?? {}))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export * as ConfigReferencePlugin from "./reference"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import { ConfigReference } from "@opencode-ai/schema/config/reference"
|
||||
import path from "path"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ConfigReference } from "../reference"
|
||||
import { Reference } from "../../reference"
|
||||
import { AbsolutePath } from "../../schema"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
@@ -20,7 +19,7 @@ export const Plugin = define({
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
yield* ctx.reference.transform((draft) => {
|
||||
const entries = new Map<string, Reference.Source>()
|
||||
for (const doc of loaded.entries.filter((entry): entry is Document => entry.type === "document")) {
|
||||
for (const doc of loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")) {
|
||||
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
||||
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
||||
if (!validAlias(name)) continue
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigPolicy from "./policy.js"
|
||||
export * as ConfigPolicy from "./policy"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * as ConfigProvider from "./provider.js"
|
||||
export * as ConfigProvider from "./provider"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Money } from "../money.js"
|
||||
import { Capabilities, Compatibility, Family, ID, VariantID } from "../model.js"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Capabilities, Compatibility, Family, ID, VariantID } from "../model"
|
||||
|
||||
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigReference from "./reference.js"
|
||||
export * as ConfigReference from "./reference"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as ConfigToolOutput from "./tool-output.js"
|
||||
export * as ConfigToolOutput from "./tool-output"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PositiveInt } from "../schema.js"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.ToolOutput")({
|
||||
max_lines: PositiveInt.pipe(Schema.optional),
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigWarming from "./warming.js"
|
||||
export * as ConfigWarming from "./warming"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as ConfigWatcher from "./watcher.js"
|
||||
export * as ConfigWatcher from "./watcher"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as ConfigWebSearch from "./websearch.js"
|
||||
export * as ConfigWebSearch from "./websearch"
|
||||
|
||||
import { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
import { Schema } from "effect"
|
||||
import { WebSearch } from "../websearch.js"
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigWebSearch.Info")({
|
||||
provider: WebSearch.ID,
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
export const migrations = (
|
||||
await Promise.all([
|
||||
import("./migration/20260127222353_familiar_lady_ursula"),
|
||||
import("./migration/20260211171708_add_project_commands"),
|
||||
@@ -43,4 +43,4 @@ export const migrations: DatabaseMigration.Migration[] = (
|
||||
import("./migration/20260804233008_loose_psylocke"),
|
||||
import("./migration/20260805200742_import_legacy_credentials"),
|
||||
])
|
||||
).map((module) => module.default)
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260127222353_familiar_lady_ursula",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -104,6 +104,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260211171708_add_project_commands",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260213144116_wakeful_the_professor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -20,6 +20,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260225215848_workspace",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -16,6 +16,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260227213759_add_session_workspace_id",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -9,6 +9,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260228203230_blue_harpoon",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -27,6 +27,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260303231226_add_workspace_fields",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,6 +12,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260309230000_move_org_to_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,6 +12,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260312043431_session_message_cursor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -13,6 +13,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260323234822_events",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -23,6 +23,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260410174513_workspace-name",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -26,6 +26,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260413175956_chief_energizer",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -21,6 +21,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260423070820_add_icon_url_override",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -11,6 +11,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260427172553_slow_nightmare",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -27,6 +27,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`DROP TABLE \`session_entry\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260428004200_add_session_path",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260501142318_next_venus",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -9,6 +9,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260504145000_add_sync_owner",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260507164347_add_workspace_time",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260510033149_session_usage",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -53,6 +53,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260511000411_data_migration_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -13,6 +13,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260511173437_session-metadata",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -13,6 +13,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260601010001_normalize_storage_paths",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -19,6 +19,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260601202201_amazing_prowler",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DROP TABLE \`permission\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260602002951_lowly_union_jack",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -21,6 +21,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260602182828_add_project_directories",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -17,6 +17,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
+2
-4
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260603001617_session_message_projection_indexes",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -16,6 +16,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
+2
-4
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260603040000_session_message_projection_order",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -16,6 +16,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260603141458_session_input_inbox",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -22,6 +22,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260603160727_jittery_ezekiel_stane",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -17,6 +17,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260604172448_event_sourced_session_input",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -44,6 +44,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260605003541_add_session_context_snapshot",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -18,6 +18,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260605042240_add_context_epoch_agent",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session_context_epoch\` ADD \`agent\` text DEFAULT 'build' NOT NULL;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260611035744_credential",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -22,6 +22,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260611192811_lush_chimera",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -22,6 +22,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260612174303_project_dir_strategy",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -26,6 +26,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
+2
-4
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260622142730_simplify_session_context_epoch",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -10,6 +10,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`revision\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260622170816_reset_v2_session_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,6 +12,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260622202450_simplify_session_input",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -14,6 +14,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`DELETE FROM \`workspace\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260804233008_loose_psylocke",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -135,6 +135,4 @@ const migration: DatabaseMigration.Migration = {
|
||||
yield* tx.run(`DROP TABLE \`session_input\`;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
@@ -30,14 +30,12 @@ const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
const decodeValue = Schema.decodeUnknownOption(LegacyValue)
|
||||
const wellKnownSourcesKey = "wellknown:sources"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
export default {
|
||||
id: "20260805200742_import_legacy_credentials",
|
||||
up(tx) {
|
||||
return importLegacyCredentials(tx, path.join(Global.Path.data, "auth.json"))
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
export function importLegacyCredentials(tx: Parameters<DatabaseMigration.Migration["up"]>[0], filepath: string) {
|
||||
return Effect.gen(function* () {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
export default {
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
@@ -248,6 +248,4 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default schema
|
||||
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||
|
||||
@@ -3,7 +3,6 @@ export * as LocationWatcher from "./location-watcher"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import path from "path"
|
||||
import { Config } from "../config"
|
||||
import { Bus } from "../bus"
|
||||
@@ -42,7 +41,7 @@ const layer = Layer.effect(
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const config = (yield* configService.entries())
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.flatMap((item) => item.info.watcher?.ignore ?? [])
|
||||
const home = Protected.isHome(location.directory)
|
||||
|
||||
|
||||
@@ -82,10 +82,10 @@ export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError
|
||||
}
|
||||
}
|
||||
|
||||
export const empty: ReadonlyArray<Source> = []
|
||||
export const empty: Instructions = []
|
||||
|
||||
/** Closes a typed definition into one `Source`, so differently typed sources compose. */
|
||||
export function make<A>(source: Source.Definition<A>): ReadonlyArray<Source> {
|
||||
export function make<A>(source: Source.Definition<A>): Instructions {
|
||||
const decode = Schema.decodeUnknownOption(source.codec)
|
||||
const encode = Schema.encodeSync(source.codec)
|
||||
const initial = (value: A) => requireText(source.key, "initial", source.render.initial(value))
|
||||
@@ -121,7 +121,7 @@ export function make<A>(source: Source.Definition<A>): ReadonlyArray<Source> {
|
||||
]
|
||||
}
|
||||
|
||||
export function combine(values: ReadonlyArray<ReadonlyArray<Source>>): ReadonlyArray<Source> {
|
||||
export function combine(values: ReadonlyArray<Instructions>): Instructions {
|
||||
const sources = values.flat()
|
||||
const keys = new Set<Key>()
|
||||
for (const source of sources) {
|
||||
@@ -131,7 +131,7 @@ export function combine(values: ReadonlyArray<ReadonlyArray<Source>>): ReadonlyA
|
||||
return sources
|
||||
}
|
||||
|
||||
export function read(value: ReadonlyArray<Source>): Effect.Effect<ReadResult> {
|
||||
export function read(value: Instructions): Effect.Effect<ReadResult> {
|
||||
return Effect.forEach(
|
||||
value,
|
||||
(source) => source.read.pipe(Effect.map((observed) => ({ key: source.key, value: observed }))),
|
||||
@@ -158,7 +158,7 @@ export function diff(observed: ReadResult, previous?: Values): Effect.Effect<Adm
|
||||
return Effect.succeed({ delta, blobs })
|
||||
}
|
||||
|
||||
export function renderInitial(value: ReadonlyArray<Source>, values: Readonly<Record<string, Schema.Json>>) {
|
||||
export function renderInitial(value: Instructions, values: Readonly<Record<string, Schema.Json>>) {
|
||||
return render(
|
||||
value.flatMap((source) => {
|
||||
if (!Object.hasOwn(values, source.key)) return []
|
||||
@@ -169,7 +169,7 @@ export function renderInitial(value: ReadonlyArray<Source>, values: Readonly<Rec
|
||||
}
|
||||
|
||||
export function renderUpdate(
|
||||
value: ReadonlyArray<Source>,
|
||||
value: Instructions,
|
||||
previous: Readonly<Record<string, Schema.Json>>,
|
||||
delta: Readonly<Record<string, Option.Option<Schema.Json>>>,
|
||||
) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
ToolSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
|
||||
import { ConfigMCP } from "../config/mcp"
|
||||
|
||||
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
||||
const DEFAULT_CATALOG_TIMEOUT = 30_000
|
||||
|
||||
@@ -3,12 +3,11 @@ export * as MCP from "./index"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { McpEvent } from "@opencode-ai/schema/mcp-event"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
import { Document } from "@opencode-ai/schema/config"
|
||||
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
|
||||
import { createHash } from "node:crypto"
|
||||
import { Cause, Context, Deferred, Effect, Exit, FiberSet, Layer, Schema, Scope, Stream } from "effect"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Config } from "../config"
|
||||
import { ConfigMCP } from "../config/mcp"
|
||||
import { Credential } from "../credential"
|
||||
import { Bus } from "../bus"
|
||||
import { Form } from "../form"
|
||||
@@ -179,7 +178,7 @@ export const layer = (options?: Options) => Layer.effect(
|
||||
const fork = yield* FiberSet.makeRuntime<never, void, never>()
|
||||
yield* Effect.addFinalizer((exit) => Scope.close(root, exit))
|
||||
|
||||
const documents = (yield* config.entries()).filter((entry): entry is Document => entry.type === "document")
|
||||
const documents = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")
|
||||
// Global MCP timeout defaults, later config files overriding earlier ones.
|
||||
const timeout = Object.assign(
|
||||
{},
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprot
|
||||
import { createServer } from "node:http"
|
||||
import { Deferred, Effect } from "effect"
|
||||
import { Credential } from "@opencode-ai/schema/credential"
|
||||
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
|
||||
import { ConfigMCP } from "../config/mcp"
|
||||
import { OauthCallbackPage } from "../oauth/page"
|
||||
import type { Integration } from "../integration"
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
|
||||
import { shouldUseResponsesApi } from "@opencode-ai/ai/providers/github-copilot"
|
||||
import { Effect, Option, Schema, Semaphore, Stream } from "effect"
|
||||
import { Catalog } from "../../catalog"
|
||||
import { Credential } from "../../credential"
|
||||
@@ -141,6 +140,14 @@ const oauth = (app: App.Info) => ({
|
||||
}),
|
||||
}) satisfies IntegrationOAuthMethodRegistration
|
||||
|
||||
function shouldUseResponses(modelID: string) {
|
||||
// Copilot supports Responses for GPT-5 class models, except mini variants
|
||||
// which still need the chat-completions endpoint.
|
||||
const match = /^gpt-(\d+)/.exec(modelID)
|
||||
if (!match) return false
|
||||
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
||||
}
|
||||
|
||||
export const GithubCopilotPlugin = define({
|
||||
id: "opencode.provider.github-copilot",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
@@ -262,7 +269,7 @@ export const GithubCopilotPlugin = define({
|
||||
return
|
||||
}
|
||||
const id = evt.model.modelID ?? evt.model.id
|
||||
evt.language = shouldUseResponsesApi(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
|
||||
evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export * as PluginSupervisor from "./supervisor"
|
||||
|
||||
import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Directory, Document, Event, type Entry } from "@opencode-ai/schema/config"
|
||||
import { ConfigPlugin } from "@opencode-ai/schema/config/plugin"
|
||||
import { Event } from "@opencode-ai/schema/config"
|
||||
import { Context, Deferred, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
|
||||
import path from "path"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
@@ -10,6 +9,7 @@ import { Agent } from "../agent"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Command } from "../command"
|
||||
import { Config } from "../config"
|
||||
import { ConfigPlugin } from "../config/plugin"
|
||||
import { Credential } from "../credential"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
|
||||
@@ -83,15 +83,15 @@ function parse(input: ConfigPlugin.Plugin): Operation {
|
||||
return { type: "remove", target: input.slice(1) }
|
||||
}
|
||||
|
||||
const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Entry[]) {
|
||||
const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Config.Entry[]) {
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const discovered = yield* Effect.forEach(
|
||||
entries.filter((entry): entry is Directory => entry.type === "directory"),
|
||||
entries.filter((entry): entry is Config.Directory => entry.type === "directory"),
|
||||
(entry) => discoverDirectory(fs, entry.path),
|
||||
).pipe(Effect.map((items) => items.flat()))
|
||||
const configured = entries
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.flatMap((entry) =>
|
||||
(entry.info.plugins ?? []).map(parse).map((operation) => {
|
||||
if (operation.type === "remove") return operation
|
||||
@@ -208,7 +208,7 @@ function discoverDirectory(fs: FSUtil.Interface, directory: string) {
|
||||
|
||||
const sourceDirectories = ["plugin", "plugins"] as const
|
||||
|
||||
function isPluginSource(entries: readonly Entry[], file: string) {
|
||||
function isPluginSource(entries: readonly Config.Entry[], file: string) {
|
||||
return entries.some(
|
||||
(entry) =>
|
||||
entry.type === "directory" &&
|
||||
@@ -243,7 +243,7 @@ const layer = Layer.effect(
|
||||
const configuredChanges = yield* PubSub.unbounded<void>()
|
||||
const watched = new Set<string>()
|
||||
const watchConfiguredSources = Effect.fn("PluginSupervisor.watchConfiguredSources")(function* (
|
||||
entries: readonly Entry[],
|
||||
entries: readonly Config.Entry[],
|
||||
operations: readonly Operation[],
|
||||
) {
|
||||
for (const operation of operations) {
|
||||
|
||||
@@ -2,7 +2,6 @@ export * as SessionCompaction from "./compaction"
|
||||
|
||||
import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest, type LanguageModel } from "@opencode-ai/ai"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Document, type Entry } from "@opencode-ai/schema/config"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { Config } from "../config"
|
||||
import { Bus } from "../bus"
|
||||
@@ -149,9 +148,9 @@ const serialize = (message: SessionMessage.Info) => {
|
||||
return ""
|
||||
}
|
||||
|
||||
const settings = (documents: readonly Entry[]) => {
|
||||
const settings = (documents: readonly Config.Entry[]) => {
|
||||
const configured = documents
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.flatMap((entry) => (entry.info.compaction ? [entry.info.compaction] : []))
|
||||
return {
|
||||
auto: configured.findLast((value) => value.auto !== undefined)?.auto ?? true,
|
||||
|
||||
@@ -13,13 +13,54 @@ export abstract class NamedError extends Error {
|
||||
static create<Name extends string, Fields extends Schema.Struct.Fields>(
|
||||
name: Name,
|
||||
fields: Fields,
|
||||
): ReturnType<typeof createSchemaClass<Name, Schema.Struct<Fields>>>
|
||||
): ReturnType<typeof NamedError.createSchemaClass<Name, Schema.Struct<Fields>>>
|
||||
static create<Name extends string, DataSchema extends Schema.Top>(
|
||||
name: Name,
|
||||
data: DataSchema,
|
||||
): ReturnType<typeof createSchemaClass<Name, DataSchema>>
|
||||
): ReturnType<typeof NamedError.createSchemaClass<Name, DataSchema>>
|
||||
static create<Name extends string>(name: Name, data: Schema.Top | Schema.Struct.Fields) {
|
||||
return createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data), this)
|
||||
return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data))
|
||||
}
|
||||
|
||||
private static createSchemaClass<Name extends string, DataSchema extends Schema.Top>(name: Name, data: DataSchema) {
|
||||
const schema = Schema.Struct({
|
||||
name: Schema.Literal(name),
|
||||
data,
|
||||
}).annotate({ identifier: name })
|
||||
type Data = Schema.Schema.Type<DataSchema>
|
||||
|
||||
const result = class extends NamedError {
|
||||
public static readonly Schema = schema
|
||||
public static readonly EffectSchema = schema
|
||||
public static readonly tag = name
|
||||
|
||||
public override readonly name = name
|
||||
|
||||
constructor(
|
||||
public readonly data: Data,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(name, options)
|
||||
this.name = name
|
||||
}
|
||||
|
||||
static isInstance(input: unknown): input is InstanceType<typeof result> {
|
||||
return NamedError.hasName(input, name)
|
||||
}
|
||||
|
||||
schema() {
|
||||
return schema
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return {
|
||||
name: name,
|
||||
data: this.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.defineProperty(result, "name", { value: name })
|
||||
return result
|
||||
}
|
||||
|
||||
public static readonly Unknown = NamedError.create("UnknownError", {
|
||||
@@ -27,48 +68,3 @@ export abstract class NamedError extends Error {
|
||||
ref: Schema.optional(Schema.String),
|
||||
})
|
||||
}
|
||||
|
||||
function createSchemaClass<Name extends string, DataSchema extends Schema.Top>(
|
||||
name: Name,
|
||||
data: DataSchema,
|
||||
base: typeof NamedError = NamedError,
|
||||
) {
|
||||
const schema = Schema.Struct({
|
||||
name: Schema.Literal(name),
|
||||
data,
|
||||
}).annotate({ identifier: name })
|
||||
type Data = Schema.Schema.Type<DataSchema>
|
||||
|
||||
const result = class extends base {
|
||||
public static readonly Schema = schema
|
||||
public static readonly EffectSchema = schema
|
||||
public static readonly tag = name
|
||||
|
||||
public override readonly name = name
|
||||
|
||||
constructor(
|
||||
public readonly data: Data,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(name, options)
|
||||
this.name = name
|
||||
}
|
||||
|
||||
static isInstance(input: unknown): input is InstanceType<typeof result> {
|
||||
return base.hasName(input, name)
|
||||
}
|
||||
|
||||
schema() {
|
||||
return schema
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return {
|
||||
name: name,
|
||||
data: this.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.defineProperty(result, "name", { value: name })
|
||||
return result
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user