From f993541e0b538388969defc94d08c3a7af5ef873 Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 1 Sep 2025 17:15:49 -0400 Subject: [PATCH] Refactor to support multiple instances inside single opencode process (#2360) This release has a bunch of minor breaking changes if you are using opencode plugins or sdk 1. storage events have been removed (we might bring this back but had some issues) 2. concept of `app` is gone - there is a new concept called `project` and endpoints to list projects and get the current project 3. plugin receives `directory` which is cwd and `worktree` which is where the root of the project is if it's a git repo 4. the session.chat function has been renamed to session.prompt in sdk. it no longer requires model to be passed in (model is now an object) 5. every endpoint takes an optional `directory` parameter to operate as though opencode is running in that directory --- AGENTS.md | 4 + packages/opencode/src/agent/agent.ts | 4 +- packages/opencode/src/app/app.ts | 147 -- packages/opencode/src/bus/index.ts | 4 +- packages/opencode/src/cli/bootstrap.ts | 13 +- packages/opencode/src/cli/cmd/agent.ts | 11 +- packages/opencode/src/cli/cmd/auth.ts | 4 +- packages/opencode/src/cli/cmd/debug/app.ts | 20 - packages/opencode/src/cli/cmd/debug/file.ts | 6 +- packages/opencode/src/cli/cmd/debug/index.ts | 4 +- packages/opencode/src/cli/cmd/debug/lsp.ts | 6 +- .../opencode/src/cli/cmd/debug/ripgrep.ts | 12 +- packages/opencode/src/cli/cmd/debug/scrap.ts | 9 +- .../opencode/src/cli/cmd/debug/snapshot.ts | 6 +- packages/opencode/src/cli/cmd/export.ts | 76 + packages/opencode/src/cli/cmd/github.ts | 10 +- packages/opencode/src/cli/cmd/models.ts | 4 +- packages/opencode/src/cli/cmd/run.ts | 13 +- packages/opencode/src/cli/cmd/serve.ts | 30 +- packages/opencode/src/cli/cmd/tui.ts | 5 +- packages/opencode/src/command/index.ts | 4 +- packages/opencode/src/config/config.ts | 39 +- packages/opencode/src/config/hooks.ts | 56 - packages/opencode/src/file/index.ts | 48 +- packages/opencode/src/file/time.ts | 22 +- packages/opencode/src/file/watch.ts | 20 +- packages/opencode/src/format/formatter.ts | 16 +- packages/opencode/src/format/index.ts | 6 +- packages/opencode/src/index.ts | 3 + packages/opencode/src/lsp/client.ts | 7 +- packages/opencode/src/lsp/index.ts | 13 +- packages/opencode/src/lsp/server.ts | 50 +- packages/opencode/src/mcp/index.ts | 5 +- packages/opencode/src/permission/index.ts | 5 +- packages/opencode/src/plugin/index.ts | 10 +- packages/opencode/src/project/instance.ts | 27 + packages/opencode/src/project/project.ts | 93 + packages/opencode/src/project/state.ts | 34 + packages/opencode/src/provider/provider.ts | 13 +- packages/opencode/src/server/project.ts | 48 + packages/opencode/src/server/server.ts | 2265 ++++++++--------- packages/opencode/src/session/index.ts | 253 +- packages/opencode/src/session/system.ts | 18 +- packages/opencode/src/share/share.ts | 20 +- packages/opencode/src/snapshot/index.ts | 32 +- packages/opencode/src/storage/storage.ts | 209 +- packages/opencode/src/tool/bash.ts | 9 +- packages/opencode/src/tool/edit.ts | 9 +- packages/opencode/src/tool/glob.ts | 9 +- packages/opencode/src/tool/grep.ts | 5 +- packages/opencode/src/tool/ls.ts | 7 +- packages/opencode/src/tool/lsp-diagnostics.ts | 7 +- packages/opencode/src/tool/lsp-hover.ts | 7 +- packages/opencode/src/tool/multiedit.ts | 5 +- packages/opencode/src/tool/read.ts | 7 +- packages/opencode/src/tool/task.ts | 12 +- packages/opencode/src/tool/todo.ts | 16 +- packages/opencode/src/tool/write.ts | 9 +- packages/opencode/src/util/lock.ts | 98 + packages/opencode/test/tool/bash.test.ts | 6 +- packages/opencode/test/tool/tool.test.ts | 8 +- packages/plugin/src/example.ts | 2 +- packages/plugin/src/index.ts | 6 +- packages/sdk/go/.release-please-manifest.json | 2 +- packages/sdk/go/.stats.yml | 8 +- packages/sdk/go/CHANGELOG.md | 159 +- packages/sdk/go/README.md | 19 +- packages/sdk/go/agent.go | 204 ++ packages/sdk/go/agent_test.go | 38 + packages/sdk/go/api.md | 110 +- packages/sdk/go/app.go | 281 +- packages/sdk/go/app_test.go | 83 +- packages/sdk/go/client.go | 6 + packages/sdk/go/client_test.go | 24 +- packages/sdk/go/command.go | 19 +- packages/sdk/go/command_test.go | 8 +- packages/sdk/go/config.go | 64 +- packages/sdk/go/config_test.go | 8 +- packages/sdk/go/event.go | 259 +- packages/sdk/go/file.go | 85 +- packages/sdk/go/file_test.go | 40 +- packages/sdk/go/find.go | 9 +- packages/sdk/go/find_test.go | 21 +- .../internal/requestconfig/requestconfig.go | 5 + packages/sdk/go/internal/version.go | 2 +- packages/sdk/go/path.go | 80 + packages/sdk/go/path_test.go | 38 + packages/sdk/go/project.go | 136 + packages/sdk/go/project_test.go | 62 + packages/sdk/go/session.go | 456 +++- packages/sdk/go/session_test.go | 271 +- packages/sdk/go/sessionpermission.go | 18 +- packages/sdk/go/sessionpermission_test.go | 7 +- packages/sdk/go/shared/union.go | 11 + packages/sdk/go/tui.go | 194 +- packages/sdk/go/tui_test.go | 71 +- packages/sdk/go/usage_test.go | 2 +- packages/sdk/js/src/gen/sdk.gen.ts | 128 +- packages/sdk/js/src/gen/types.gen.ts | 320 ++- packages/sdk/stainless/generate.ts | 2 +- packages/sdk/stainless/stainless.yml | 27 +- packages/tui/cmd/opencode/main.go | 63 +- packages/tui/internal/app/app.go | 62 +- packages/tui/internal/app/prompt.go | 4 +- packages/tui/internal/completions/agents.go | 3 +- packages/tui/internal/completions/files.go | 2 +- .../tui/internal/components/chat/editor.go | 8 +- .../tui/internal/components/chat/message.go | 4 +- .../tui/internal/components/chat/messages.go | 2 + .../tui/internal/components/status/status.go | 14 +- packages/tui/internal/tui/tui.go | 32 +- specs/project.md | 65 + 112 files changed, 4303 insertions(+), 3159 deletions(-) delete mode 100644 packages/opencode/src/app/app.ts delete mode 100644 packages/opencode/src/cli/cmd/debug/app.ts create mode 100644 packages/opencode/src/cli/cmd/export.ts delete mode 100644 packages/opencode/src/config/hooks.ts create mode 100644 packages/opencode/src/project/instance.ts create mode 100644 packages/opencode/src/project/project.ts create mode 100644 packages/opencode/src/project/state.ts create mode 100644 packages/opencode/src/server/project.ts create mode 100644 packages/opencode/src/util/lock.ts create mode 100644 packages/sdk/go/agent.go create mode 100644 packages/sdk/go/agent_test.go create mode 100644 packages/sdk/go/path.go create mode 100644 packages/sdk/go/path_test.go create mode 100644 packages/sdk/go/project.go create mode 100644 packages/sdk/go/project_test.go create mode 100644 packages/sdk/go/shared/union.go create mode 100644 specs/project.md diff --git a/AGENTS.md b/AGENTS.md index cca69e4b..507cfea5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,3 +10,7 @@ - AVOID `let` statements - PREFER single word variable names where possible - Use as many bun apis as possible like Bun.file() + +## Debugging + +- To test opencode in the `packages/opencode` directory you can run `bun dev` diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 9cb9c48e..67338c65 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,10 +1,10 @@ -import { App } from "../app/app" import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider/provider" import { generateObject, type ModelMessage } from "ai" import PROMPT_GENERATE from "./generate.txt" import { SystemPrompt } from "../session/system" +import { Instance } from "../project/instance" import { mergeDeep } from "remeda" export namespace Agent { @@ -36,7 +36,7 @@ export namespace Agent { }) export type Info = z.infer - const state = App.state("agent", async () => { + const state = Instance.state(async () => { const cfg = await Config.get() const defaultTools = cfg.tools ?? {} const defaultPermission: Info["permission"] = { diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts deleted file mode 100644 index b54c0893..00000000 --- a/packages/opencode/src/app/app.ts +++ /dev/null @@ -1,147 +0,0 @@ -import "zod-openapi/extend" -import { Log } from "../util/log" -import { Context } from "../util/context" -import { Filesystem } from "../util/filesystem" -import { Global } from "../global" -import path from "path" -import os from "os" -import { z } from "zod" - -export namespace App { - const log = Log.create({ service: "app" }) - - export const Info = z - .object({ - hostname: z.string(), - git: z.boolean(), - path: z.object({ - home: z.string(), - config: z.string(), - data: z.string(), - root: z.string(), - cwd: z.string(), - state: z.string(), - }), - time: z.object({ - initialized: z.number().optional(), - }), - }) - .openapi({ - ref: "App", - }) - export type Info = z.infer - - const ctx = Context.create<{ - info: Info - services: Map Promise }> - }>("app") - - export const use = ctx.use - - const APP_JSON = "app.json" - - export type Input = { - cwd: string - } - - export const provideExisting = ctx.provide - export async function provide(input: Input, cb: (app: App.Info) => Promise) { - log.info("creating", { - cwd: input.cwd, - }) - const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined)) - log.info("git", { git }) - - const data = path.join(Global.Path.data, "project", git ? directory(git) : "global") - const stateFile = Bun.file(path.join(data, APP_JSON)) - const state = (await stateFile.json().catch(() => ({}))) as { - initialized: number - } - await stateFile.write(JSON.stringify(state)) - - const services = new Map< - any, - { - state: any - shutdown?: (input: any) => Promise - } - >() - - const root = git ?? input.cwd - - const info: Info = { - hostname: os.hostname(), - time: { - initialized: state.initialized, - }, - git: git !== undefined, - path: { - home: os.homedir(), - config: Global.Path.config, - state: Global.Path.state, - data, - root, - cwd: input.cwd, - }, - } - const app = { - services, - info, - } - - return ctx.provide(app, async () => { - try { - const result = await cb(app.info) - return result - } finally { - for (const [key, entry] of app.services.entries()) { - if (!entry.shutdown) continue - log.info("shutdown", { name: key }) - await entry.shutdown?.(await entry.state) - } - } - }) - } - - export function state( - key: any, - init: (app: Info) => State, - shutdown?: (state: Awaited) => Promise, - ) { - return () => { - const app = ctx.use() - const services = app.services - if (!services.has(key)) { - log.info("registering service", { name: key }) - services.set(key, { - state: init(app.info), - shutdown, - }) - } - return services.get(key)?.state as State - } - } - - export function info() { - return ctx.use().info - } - - export async function initialize() { - const { info } = ctx.use() - info.time.initialized = Date.now() - await Bun.write( - path.join(info.path.data, APP_JSON), - JSON.stringify({ - initialized: Date.now(), - }), - ) - } - - function directory(input: string): string { - return input - .split(path.sep) - .filter(Boolean) - .join("-") - .replace(/[^A-Za-z0-9_]/g, "-") - } -} diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 0353da90..be42bf80 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -1,12 +1,12 @@ import { z, type ZodType } from "zod" -import { App } from "../app/app" import { Log } from "../util/log" +import { Instance } from "../project/instance" export namespace Bus { const log = Log.create({ service: "bus" }) type Subscription = (event: any) => void - const state = App.state("bus", () => { + const state = Instance.state(() => { const subscriptions = new Map() return { diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index 9e9448b4..3bd15261 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -1,20 +1,19 @@ -import { App } from "../app/app" -import { ConfigHooks } from "../config/hooks" import { Format } from "../format" import { LSP } from "../lsp" import { Plugin } from "../plugin" +import { Instance } from "../project/instance" import { Share } from "../share/share" import { Snapshot } from "../snapshot" -export async function bootstrap(input: App.Input, cb: (app: App.Info) => Promise) { - return App.provide(input, async (app) => { +export async function bootstrap(directory: string, cb: () => Promise) { + return Instance.provide(directory, async () => { await Plugin.init() Share.init() Format.init() - ConfigHooks.init() LSP.init() Snapshot.init() - - return cb(app) + const result = await cb() + await Instance.dispose() + return result }) } diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index e1bf2fbc..ea54d0dd 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -5,25 +5,26 @@ import { Global } from "../../global" import { Agent } from "../../agent/agent" import path from "path" import matter from "gray-matter" -import { App } from "../../app/app" +import { Instance } from "../../project/instance" const AgentCreateCommand = cmd({ command: "create", describe: "create a new agent", async handler() { - await App.provide({ cwd: process.cwd() }, async (app) => { + await Instance.provide(process.cwd(), async () => { UI.empty() prompts.intro("Create agent") + const project = Instance.project let scope: "global" | "project" = "global" - if (app.git) { + if (project.vcs === "git") { const scopeResult = await prompts.select({ message: "Location", options: [ { label: "Current project", value: "project" as const, - hint: app.path.root, + hint: Instance.worktree, }, { label: "Global", @@ -116,7 +117,7 @@ const AgentCreateCommand = cmd({ const content = matter.stringify(generated.systemPrompt, frontmatter) const filePath = path.join( - scope === "global" ? Global.Path.config : path.join(app.path.root, ".opencode"), + scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"), `agent`, `${generated.identifier}.md`, ) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index eb2eb19b..7a1700b5 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -8,7 +8,7 @@ import path from "path" import os from "os" import { Global } from "../../global" import { Plugin } from "../../plugin" -import { App } from "../../app/app" +import { Instance } from "../../project/instance" export const AuthCommand = cmd({ command: "auth", @@ -74,7 +74,7 @@ export const AuthLoginCommand = cmd({ type: "string", }), async handler(args) { - await App.provide({ cwd: process.cwd() }, async () => { + await Instance.provide(process.cwd(), async () => { UI.empty() prompts.intro("Add credential") if (args.url) { diff --git a/packages/opencode/src/cli/cmd/debug/app.ts b/packages/opencode/src/cli/cmd/debug/app.ts deleted file mode 100644 index ee5b038d..00000000 --- a/packages/opencode/src/cli/cmd/debug/app.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { App } from "../../../app/app" -import { bootstrap } from "../../bootstrap" -import { cmd } from "../cmd" - -const AppInfoCommand = cmd({ - command: "info", - builder: (yargs) => yargs, - async handler() { - await bootstrap({ cwd: process.cwd() }, async () => { - const app = App.info() - console.log(JSON.stringify(app, null, 2)) - }) - }, -}) - -export const AppCommand = cmd({ - command: "app", - builder: (yargs) => yargs.command(AppInfoCommand).demandCommand(), - async handler() {}, -}) diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts index 74fb1936..898b7d39 100644 --- a/packages/opencode/src/cli/cmd/debug/file.ts +++ b/packages/opencode/src/cli/cmd/debug/file.ts @@ -11,7 +11,7 @@ const FileReadCommand = cmd({ description: "File path to read", }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { const content = await File.read(args.path) console.log(content) }) @@ -22,7 +22,7 @@ const FileStatusCommand = cmd({ command: "status", builder: (yargs) => yargs, async handler() { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { const status = await File.status() console.log(JSON.stringify(status, null, 2)) }) @@ -38,7 +38,7 @@ const FileListCommand = cmd({ description: "File path to list", }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { const files = await File.list(args.path) console.log(JSON.stringify(files, null, 2)) }) diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index d7fbc8fb..71d337e3 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -1,7 +1,6 @@ import { Global } from "../../../global" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" -import { AppCommand } from "./app" import { FileCommand } from "./file" import { LSPCommand } from "./lsp" import { RipgrepCommand } from "./ripgrep" @@ -12,7 +11,6 @@ export const DebugCommand = cmd({ command: "debug", builder: (yargs) => yargs - .command(AppCommand) .command(LSPCommand) .command(RipgrepCommand) .command(FileCommand) @@ -22,7 +20,7 @@ export const DebugCommand = cmd({ .command({ command: "wait", async handler() { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24)) }) }, diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts index 4d864db4..292c8ba6 100644 --- a/packages/opencode/src/cli/cmd/debug/lsp.ts +++ b/packages/opencode/src/cli/cmd/debug/lsp.ts @@ -14,7 +14,7 @@ const DiagnosticsCommand = cmd({ command: "diagnostics ", builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { await LSP.touchFile(args.file, true) console.log(JSON.stringify(await LSP.diagnostics(), null, 2)) }) @@ -25,7 +25,7 @@ export const SymbolsCommand = cmd({ command: "symbols ", builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { using _ = Log.Default.time("symbols") const results = await LSP.workspaceSymbol(args.query) console.log(JSON.stringify(results, null, 2)) @@ -37,7 +37,7 @@ export const DocumentSymbolsCommand = cmd({ command: "document-symbols ", builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { using _ = Log.Default.time("document-symbols") const results = await LSP.documentSymbol(args.uri) console.log(JSON.stringify(results, null, 2)) diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts index b8005c90..ec3f9cb8 100644 --- a/packages/opencode/src/cli/cmd/debug/ripgrep.ts +++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts @@ -1,5 +1,5 @@ -import { App } from "../../../app/app" import { Ripgrep } from "../../../file/ripgrep" +import { Instance } from "../../../project/instance" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" @@ -16,9 +16,8 @@ const TreeCommand = cmd({ type: "number", }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { - const app = App.info() - console.log(await Ripgrep.tree({ cwd: app.path.cwd, limit: args.limit })) + await bootstrap(process.cwd(), async () => { + console.log(await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) }) }, }) @@ -40,10 +39,9 @@ const FilesCommand = cmd({ description: "Limit number of results", }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { - const app = App.info() + await bootstrap(process.cwd(), async () => { const files = await Ripgrep.files({ - cwd: app.path.cwd, + cwd: Instance.directory, query: args.query, glob: args.glob ? [args.glob] : undefined, limit: args.limit, diff --git a/packages/opencode/src/cli/cmd/debug/scrap.ts b/packages/opencode/src/cli/cmd/debug/scrap.ts index 3ba53e3f..9ab3bb2f 100644 --- a/packages/opencode/src/cli/cmd/debug/scrap.ts +++ b/packages/opencode/src/cli/cmd/debug/scrap.ts @@ -1,7 +1,14 @@ +import { Project } from "../../../project/project" +import { Log } from "../../../util/log" import { cmd } from "../cmd" export const ScrapCommand = cmd({ command: "scrap", builder: (yargs) => yargs, - async handler() {}, + async handler() { + const timer = Log.Default.time("scrap") + const list = await Project.list() + console.log(list) + timer.stop() + }, }) diff --git a/packages/opencode/src/cli/cmd/debug/snapshot.ts b/packages/opencode/src/cli/cmd/debug/snapshot.ts index edd5fbe2..1849fe27 100644 --- a/packages/opencode/src/cli/cmd/debug/snapshot.ts +++ b/packages/opencode/src/cli/cmd/debug/snapshot.ts @@ -11,7 +11,7 @@ export const SnapshotCommand = cmd({ const TrackCommand = cmd({ command: "track", async handler() { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { console.log(await Snapshot.track()) }) }, @@ -26,7 +26,7 @@ const PatchCommand = cmd({ demandOption: true, }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { console.log(await Snapshot.patch(args.hash)) }) }, @@ -41,7 +41,7 @@ const DiffCommand = cmd({ demandOption: true, }), async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { console.log(await Snapshot.diff(args.hash)) }) }, diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts new file mode 100644 index 00000000..996522b5 --- /dev/null +++ b/packages/opencode/src/cli/cmd/export.ts @@ -0,0 +1,76 @@ +import type { Argv } from "yargs" +import { Session } from "../../session" +import { cmd } from "./cmd" +import { bootstrap } from "../bootstrap" +import { UI } from "../ui" +import * as prompts from "@clack/prompts" + +export const ExportCommand = cmd({ + command: "export [sessionID]", + describe: "export session data as JSON", + builder: (yargs: Argv) => { + return yargs.positional("sessionID", { + describe: "session id to export", + type: "string", + }) + }, + handler: async (args) => { + await bootstrap(process.cwd(), async () => { + let sessionID = args.sessionID + + if (!sessionID) { + UI.empty() + prompts.intro("Export session") + + const sessions = [] + for await (const session of Session.list()) { + sessions.push(session) + } + + if (sessions.length === 0) { + prompts.log.error("No sessions found") + prompts.outro("Done") + return + } + + sessions.sort((a, b) => b.time.updated - a.time.updated) + + const selectedSession = await prompts.autocomplete({ + message: "Select session to export", + maxItems: 10, + options: sessions.map((session) => ({ + label: session.title, + value: session.id, + hint: `${new Date(session.time.updated).toLocaleString()} • ${session.id.slice(-8)}`, + })), + }) + + if (prompts.isCancel(selectedSession)) { + throw new UI.CancelledError() + } + + sessionID = selectedSession as string + + prompts.outro("Exporting session...") + } + + try { + const sessionInfo = await Session.get(sessionID!) + const messages = await Session.messages(sessionID!) + + const exportData = { + info: sessionInfo, + messages: messages.map((msg) => ({ + info: msg.info, + parts: msg.parts, + })), + } + + console.log(JSON.stringify(exportData, null, 2)) + } catch (error) { + UI.error(`Session not found: ${sessionID!}`) + process.exit(1) + } + }) + }, +}) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index a9198944..b06189ff 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -6,7 +6,7 @@ import { map, pipe, sortBy, values } from "remeda" import { UI } from "../ui" import { cmd } from "./cmd" import { ModelsDev } from "../../provider/models" -import { App } from "../../app/app" +import { Instance } from "../../project/instance" const WORKFLOW_FILE = ".github/workflows/opencode.yml" @@ -21,7 +21,7 @@ export const GithubInstallCommand = cmd({ command: "install", describe: "install the GitHub agent", async handler() { - await App.provide({ cwd: process.cwd() }, async () => { + await Instance.provide(process.cwd(), async () => { UI.empty() prompts.intro("Install GitHub agent") const app = await getAppInfo() @@ -63,8 +63,8 @@ export const GithubInstallCommand = cmd({ } async function getAppInfo() { - const app = App.info() - if (!app.git) { + const project = Instance.project + if (project.vcs !== "git") { prompts.log.error(`Could not find git repository. Please run this command from a git repository.`) throw new UI.CancelledError() } @@ -88,7 +88,7 @@ export const GithubInstallCommand = cmd({ throw new UI.CancelledError() } const [, owner, repo] = parsed - return { owner, repo, root: app.path.root } + return { owner, repo, root: Instance.worktree } } async function promptProvider() { diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index e385a310..fffe475e 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -1,4 +1,4 @@ -import { App } from "../../app/app" +import { Instance } from "../../project/instance" import { Provider } from "../../provider/provider" import { cmd } from "./cmd" @@ -6,7 +6,7 @@ export const ModelsCommand = cmd({ command: "models", describe: "list all available models", handler: async () => { - await App.provide({ cwd: process.cwd() }, async () => { + await Instance.provide(process.cwd(), async () => { const providers = await Provider.list() for (const [providerID, provider] of Object.entries(providers)) { diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 969d9907..7a234fc5 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -74,7 +74,7 @@ export const RunCommand = cmd({ return } - await bootstrap({ cwd: process.cwd() }, async () => { + await bootstrap(process.cwd(), async () => { if (args.command) { const exists = await Command.get(args.command) if (!exists) { @@ -82,7 +82,6 @@ export const RunCommand = cmd({ return } } - const session = await (async () => { if (args.continue) { const it = Session.list() @@ -198,11 +197,13 @@ export const RunCommand = cmd({ } const messageID = Identifier.ascending("message") - const result = await Session.chat({ + const result = await Session.prompt({ sessionID: session.id, messageID, - providerID, - modelID, + model: { + providerID, + modelID, + }, agent: agent.name, parts: [ { @@ -215,7 +216,7 @@ export const RunCommand = cmd({ const isPiped = !process.stdout.isTTY if (isPiped) { - const match = result.parts.findLast((x) => x.type === "text") + const match = result.parts.findLast((x: any) => x.type === "text") as any if (match) process.stdout.write(UI.markdown(match.text)) if (errorMsg) process.stdout.write(errorMsg) } diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 1d6a7327..850dbc83 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,6 +1,4 @@ -import { Provider } from "../../provider/provider" import { Server } from "../../server/server" -import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" export const ServeCommand = cmd({ @@ -21,26 +19,14 @@ export const ServeCommand = cmd({ }), describe: "starts a headless opencode server", handler: async (args) => { - const cwd = process.cwd() - await bootstrap({ cwd }, async () => { - const providers = await Provider.list() - if (Object.keys(providers).length === 0) { - return "needs_provider" - } - - const hostname = args.hostname - const port = args.port - - const server = Server.listen({ - port, - hostname, - }) - - console.log(`opencode server listening on http://${server.hostname}:${server.port}`) - - await new Promise(() => {}) - - server.stop() + const hostname = args.hostname + const port = args.port + const server = Server.listen({ + port, + hostname, }) + console.log(`opencode server listening on http://${server.hostname}:${server.port}`) + await new Promise(() => {}) + server.stop() }, }) diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index 25d0fbcb..2011c26c 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -15,6 +15,7 @@ import { Ide } from "../../ide" import { Flag } from "../../flag/flag" import { Session } from "../../session" +import { Instance } from "../../project/instance" declare global { const OPENCODE_TUI_PATH: string @@ -79,7 +80,7 @@ export const TuiCommand = cmd({ UI.error("Failed to change directory to " + cwd) return } - const result = await bootstrap({ cwd }, async (app) => { + const result = await bootstrap(cwd, async () => { const sessionID = await (async () => { if (args.continue) { const it = Session.list() @@ -146,7 +147,7 @@ export const TuiCommand = cmd({ ...process.env, CGO_ENABLED: "0", OPENCODE_SERVER: server.url.toString(), - OPENCODE_APP_INFO: JSON.stringify(app), + OPENCODE_PROJECT: JSON.stringify(Instance.project), }, onExit: () => { server.stop() diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 97dd36a0..a9356cdd 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,6 +1,6 @@ import z from "zod" -import { App } from "../app/app" import { Config } from "../config/config" +import { Instance } from "../project/instance" export namespace Command { export const Info = z @@ -16,7 +16,7 @@ export namespace Command { }) export type Info = z.infer - const state = App.state("command", async () => { + const state = Instance.state(async () => { const cfg = await Config.get() const result: Record = {} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 804a0274..135c0e80 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -2,7 +2,6 @@ import { Log } from "../util/log" import path from "path" import os from "os" import { z } from "zod" -import { App } from "../app/app" import { Filesystem } from "../util/filesystem" import { ModelsDev } from "../provider/models" import { mergeDeep, pipe } from "remeda" @@ -14,15 +13,16 @@ import matter from "gray-matter" import { Flag } from "../flag/flag" import { Auth } from "../auth" import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" +import { Instance } from "../project/instance" export namespace Config { const log = Log.create({ service: "config" }) - export const state = App.state("config", async (app) => { + export const state = Instance.state(async () => { const auth = await Auth.all() let result = await global() for (const file of ["opencode.jsonc", "opencode.json"]) { - const found = await Filesystem.findUp(file, app.path.cwd, app.path.root) + const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree) for (const resolved of found.toReversed()) { result = mergeDeep(result, await loadFile(resolved)) } @@ -45,7 +45,7 @@ export namespace Config { result.agent = result.agent || {} const markdownAgents = [ ...(await Filesystem.globUp("agent/**/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/agent/**/*.md", app.path.cwd, app.path.root)), + ...(await Filesystem.globUp(".opencode/agent/*.md", Instance.directory, Instance.worktree)), ] for (const item of markdownAgents) { const content = await Bun.file(item).text() @@ -86,7 +86,7 @@ export namespace Config { result.mode = result.mode || {} const markdownModes = [ ...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/mode/*.md", app.path.cwd, app.path.root)), + ...(await Filesystem.globUp(".opencode/mode/*.md", Instance.directory, Instance.worktree)), ] for (const item of markdownModes) { const content = await Bun.file(item).text() @@ -100,19 +100,21 @@ export namespace Config { } const parsed = Agent.safeParse(config) if (parsed.success) { - result.mode = mergeDeep(result.mode, { - [config.name]: parsed.data, + result.agent = mergeDeep(result.mode, { + [config.name]: { + ...parsed.data, + mode: "primary" as const, + }, }) continue } - throw new InvalidError({ path: item }, { cause: parsed.error }) } // Load command markdown files result.command = result.command || {} const markdownCommands = [ ...(await Filesystem.globUp("command/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/command/*.md", app.path.cwd, app.path.root)), + ...(await Filesystem.globUp(".opencode/command/*.md", Instance.directory, Instance.worktree)), ] for (const item of markdownCommands) { const content = await Bun.file(item).text() @@ -147,7 +149,7 @@ export namespace Config { result.plugin.push( ...[ ...(await Filesystem.globUp("plugin/*.{ts,js}", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/plugin/*.{ts,js}", app.path.cwd, app.path.root)), + ...(await Filesystem.globUp(".opencode/plugin/*.{ts,js}", Instance.directory, Instance.worktree)), ].map((x) => "file://" + x), ) @@ -155,6 +157,16 @@ export namespace Config { result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION)) } + if (!result.username) result.username = os.userInfo().username + + // Handle migration from autoshare to share field + if (result.autoshare === true && !result.share) { + result.share = "auto" + } + if (result.keybinds?.messages_revert && !result.keybinds.messages_undo) { + result.keybinds.messages_undo = result.keybinds.messages_revert + } + // Handle migration from autoshare to share field if (result.autoshare === true && !result.share) { result.share = "auto" @@ -175,13 +187,6 @@ export namespace Config { result.keybinds.agent_cycle_reverse = result.keybinds.switch_agent_reverse } - if (!result.username) { - const os = await import("os") - result.username = os.userInfo().username - } - - log.info("loaded", result) - return result }) diff --git a/packages/opencode/src/config/hooks.ts b/packages/opencode/src/config/hooks.ts deleted file mode 100644 index 8772c9c6..00000000 --- a/packages/opencode/src/config/hooks.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { App } from "../app/app" -import { Bus } from "../bus" -import { File } from "../file" -import { Session } from "../session" -import { Log } from "../util/log" -import { Config } from "./config" -import path from "path" - -export namespace ConfigHooks { - const log = Log.create({ service: "config.hooks" }) - - export function init() { - log.info("init") - const app = App.info() - - Bus.subscribe(File.Event.Edited, async (payload) => { - const cfg = await Config.get() - const ext = path.extname(payload.properties.file) - for (const item of cfg.experimental?.hook?.file_edited?.[ext] ?? []) { - log.info("file_edited", { - file: payload.properties.file, - command: item.command, - }) - Bun.spawn({ - cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)), - env: item.environment, - cwd: app.path.cwd, - stdout: "ignore", - stderr: "ignore", - }) - } - }) - - Bus.subscribe(Session.Event.Idle, async (payload) => { - const cfg = await Config.get() - if (cfg.experimental?.hook?.session_completed) { - const session = await Session.get(payload.properties.sessionID) - // Only fire hook for top-level sessions (not subagent sessions) - if (session.parentID) return - - for (const item of cfg.experimental.hook.session_completed) { - log.info("session_completed", { - command: item.command, - }) - Bun.spawn({ - cmd: item.command, - cwd: App.info().path.cwd, - env: item.environment, - stdout: "ignore", - stderr: "ignore", - }) - } - } - }) - } -} diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index ea3f2d21..a6dd08a8 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -3,10 +3,10 @@ import { Bus } from "../bus" import { $ } from "bun" import { createPatch } from "diff" import path from "path" -import { App } from "../app/app" import fs from "fs" import ignore from "ignore" import { Log } from "../util/log" +import { Instance } from "../project/instance" export namespace File { const log = Log.create({ service: "file" }) @@ -46,10 +46,10 @@ export namespace File { } export async function status() { - const app = App.info() - if (!app.git) return [] + const project = Instance.project + if (project.vcs !== "git") return [] - const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text() + const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text() const changedFiles: Info[] = [] @@ -66,13 +66,17 @@ export namespace File { } } - const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text() + const untrackedOutput = await $`git ls-files --others --exclude-standard` + .cwd(Instance.directory) + .quiet() + .nothrow() + .text() if (untrackedOutput.trim()) { const untrackedFiles = untrackedOutput.trim().split("\n") for (const filepath of untrackedFiles) { try { - const content = await Bun.file(path.join(app.path.root, filepath)).text() + const content = await Bun.file(path.join(Instance.worktree, filepath)).text() const lines = content.split("\n").length changedFiles.push({ path: filepath, @@ -87,7 +91,11 @@ export namespace File { } // Get deleted files - const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text() + const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD` + .cwd(Instance.directory) + .quiet() + .nothrow() + .text() if (deletedOutput.trim()) { const deletedFiles = deletedOutput.trim().split("\n") @@ -103,23 +111,23 @@ export namespace File { return changedFiles.map((x) => ({ ...x, - path: path.relative(app.path.cwd, path.join(app.path.root, x.path)), + path: path.relative(Instance.directory, path.join(Instance.worktree, x.path)), })) } export async function read(file: string) { using _ = log.time("read", { file }) - const app = App.info() - const full = path.join(app.path.cwd, file) + const project = Instance.project + const full = path.join(Instance.directory, file) const content = await Bun.file(full) .text() .catch(() => "") .then((x) => x.trim()) - if (app.git) { - const rel = path.relative(app.path.root, full) - const diff = await $`git diff ${rel}`.cwd(app.path.root).quiet().nothrow().text() + if (project.vcs === "git") { + const rel = path.relative(Instance.worktree, full) + const diff = await $`git diff ${rel}`.cwd(Instance.worktree).quiet().nothrow().text() if (diff.trim()) { - const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text() + const original = await $`git show HEAD:${rel}`.cwd(Instance.worktree).quiet().nothrow().text() const patch = createPatch(file, original, content, "old", "new", { context: Infinity, }) @@ -131,22 +139,22 @@ export namespace File { export async function list(dir?: string) { const exclude = [".git", ".DS_Store"] - const app = App.info() + const project = Instance.project let ignored = (_: string) => false - if (app.git) { - const gitignore = Bun.file(path.join(app.path.root, ".gitignore")) + if (project.vcs === "git") { + const gitignore = Bun.file(path.join(Instance.worktree, ".gitignore")) if (await gitignore.exists()) { const ig = ignore().add(await gitignore.text()) ignored = ig.ignores.bind(ig) } } - const resolved = dir ? path.join(app.path.cwd, dir) : app.path.cwd + const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory const nodes: Node[] = [] for (const entry of await fs.promises.readdir(resolved, { withFileTypes: true })) { if (exclude.includes(entry.name)) continue const fullPath = path.join(resolved, entry.name) - const relativePath = path.relative(app.path.cwd, fullPath) - const relativeToRoot = path.relative(app.path.root, fullPath) + const relativePath = path.relative(Instance.directory, fullPath) + const relativeToRoot = path.relative(Instance.worktree, fullPath) const type = entry.isDirectory() ? "directory" : "file" nodes.push({ name: entry.name, diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 453259e8..ab973bd3 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,18 +1,20 @@ -import { App } from "../app/app" +import { Instance } from "../project/instance" import { Log } from "../util/log" export namespace FileTime { const log = Log.create({ service: "file.time" }) - export const state = App.state("tool.filetimes", () => { - const read: { - [sessionID: string]: { - [path: string]: Date | undefined + export const state = Instance.state( + () => { + const read: { + [sessionID: string]: { + [path: string]: Date | undefined + } + } = {} + return { + read, } - } = {} - return { - read, - } - }) + }, + ) export function read(sessionID: string, file: string) { log.info("read", { sessionID, file }) diff --git a/packages/opencode/src/file/watch.ts b/packages/opencode/src/file/watch.ts index 383ad6f3..587ad54d 100644 --- a/packages/opencode/src/file/watch.ts +++ b/packages/opencode/src/file/watch.ts @@ -1,9 +1,9 @@ import { z } from "zod" import { Bus } from "../bus" import fs from "fs" -import { App } from "../app/app" import { Log } from "../util/log" import { Flag } from "../flag/flag" +import { Instance } from "../project/instance" export namespace FileWatcher { const log = Log.create({ service: "file.watcher" }) @@ -17,22 +17,16 @@ export namespace FileWatcher { }), ), } - const state = App.state( - "file.watcher", + const state = Instance.state( () => { - const app = App.use() - if (!app.info.git) return {} + if (Instance.project.vcs !== "git") return {} try { - const watcher = fs.watch(app.info.path.cwd, { recursive: true }, (event, file) => { + const watcher = fs.watch(Instance.directory, { recursive: true }, (event, file) => { log.info("change", { file, event }) if (!file) return - // for some reason async local storage is lost here - // https://github.com/oven-sh/bun/issues/20754 - App.provideExisting(app, async () => { - Bus.publish(Event.Updated, { - file, - event, - }) + Bus.publish(Event.Updated, { + file, + event, }) }) return { watcher } diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 0869ef50..d2a9eee8 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,5 +1,5 @@ -import { App } from "../app/app" import { BunProc } from "../bun" +import { Instance } from "../project/instance" import { Filesystem } from "../util/filesystem" export interface Info { @@ -63,8 +63,7 @@ export const prettier: Info = { ".gql", ], async enabled() { - const app = App.info() - const items = await Filesystem.findUp("package.json", app.path.cwd, app.path.root) + const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree) for (const item of items) { const json = await Bun.file(item).json() if (json.dependencies?.prettier) return true @@ -109,10 +108,9 @@ export const biome: Info = { ".gql", ], async enabled() { - const app = App.info() const configs = ["biome.json", "biome.jsonc"] for (const config of configs) { - const found = await Filesystem.findUp(config, app.path.cwd, app.path.root) + const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree) if (found.length > 0) { return true } @@ -135,8 +133,7 @@ export const clang: Info = { command: ["clang-format", "-i", "$FILE"], extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"], async enabled() { - const app = App.info() - const items = await Filesystem.findUp(".clang-format", app.path.cwd, app.path.root) + const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree) return items.length > 0 }, } @@ -156,10 +153,9 @@ export const ruff: Info = { extensions: [".py", ".pyi"], async enabled() { if (!Bun.which("ruff")) return false - const app = App.info() const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"] for (const config of configs) { - const found = await Filesystem.findUp(config, app.path.cwd, app.path.root) + const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree) if (found.length > 0) { if (config === "pyproject.toml") { const content = await Bun.file(found[0]).text() @@ -171,7 +167,7 @@ export const ruff: Info = { } const deps = ["requirements.txt", "pyproject.toml", "Pipfile"] for (const dep of deps) { - const found = await Filesystem.findUp(dep, app.path.cwd, app.path.root) + const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree) if (found.length > 0) { const content = await Bun.file(found[0]).text() if (content.includes("ruff")) return true diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index f6a3e2c2..6cbafaef 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,4 +1,3 @@ -import { App } from "../app/app" import { Bus } from "../bus" import { File } from "../file" import { Log } from "../util/log" @@ -7,11 +6,12 @@ import path from "path" import * as Formatter from "./formatter" import { Config } from "../config/config" import { mergeDeep } from "remeda" +import { Instance } from "../project/instance" export namespace Format { const log = Log.create({ service: "format" }) - const state = App.state("format", async () => { + const state = Instance.state(async () => { const enabled: Record = {} const cfg = await Config.get() @@ -71,7 +71,7 @@ export namespace Format { try { const proc = Bun.spawn({ cmd: item.command.map((x) => x.replace("$FILE", file)), - cwd: App.info().path.cwd, + cwd: Instance.directory, env: { ...process.env, ...item.environment }, stdout: "ignore", stderr: "ignore", diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 868c96ba..e7069210 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -18,6 +18,7 @@ import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" import { GithubCommand } from "./cli/cmd/github" +import { ExportCommand } from "./cli/cmd/export" const cancel = new AbortController() @@ -80,6 +81,7 @@ const cli = yargs(hideBin(process.argv)) .command(ServeCommand) .command(ModelsCommand) .command(StatsCommand) + .command(ExportCommand) .command(GithubCommand) .fail((msg) => { if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) { @@ -105,6 +107,7 @@ try { name: e.name, message: e.message, cause: e.cause?.toString(), + stack: e.stack, }) } diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index c6ccfbb0..20c11c9f 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -1,7 +1,6 @@ import path from "path" import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" -import { App } from "../app/app" import { Log } from "../util/log" import { LANGUAGE_EXTENSIONS } from "./language" import { Bus } from "../bus" @@ -9,6 +8,7 @@ import z from "zod" import type { LSPServer } from "./server" import { NamedError } from "../util/error" import { withTimeout } from "../util/timeout" +import { Instance } from "../project/instance" export namespace LSPClient { const log = Log.create({ service: "lsp.client" }) @@ -35,7 +35,6 @@ export namespace LSPClient { } export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) { - const app = App.info() const l = log.clone().tag("serverID", input.serverID) l.info("starting client") @@ -130,7 +129,7 @@ export namespace LSPClient { }, notify: { async open(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path) + input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) const file = Bun.file(input.path) const text = await file.text() const extension = path.extname(input.path) @@ -169,7 +168,7 @@ export namespace LSPClient { return diagnostics }, async waitForDiagnostics(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path) + input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) log.info("waiting for diagnostics", input) let unsub: () => void return await withTimeout( diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 678f6021..ae73c8cf 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -1,4 +1,3 @@ -import { App } from "../app/app" import { Log } from "../util/log" import { LSPClient } from "./client" import path from "path" @@ -6,6 +5,7 @@ import { LSPServer } from "./server" import { z } from "zod" import { Config } from "../config/config" import { spawn } from "child_process" +import { Instance } from "../project/instance" export namespace LSP { const log = Log.create({ service: "lsp" }) @@ -53,8 +53,7 @@ export namespace LSP { }) export type DocumentSymbol = z.infer - const state = App.state( - "lsp", + const state = Instance.state( async () => { const clients: LSPClient.Info[] = [] const servers: Record = {} @@ -71,9 +70,9 @@ export namespace LSP { } servers[name] = { ...existing, - root: existing?.root ?? (async (_file, app) => app.path.root), + root: existing?.root ?? (async () => Instance.directory), extensions: item.extensions ?? existing.extensions, - spawn: async (_app, root) => { + spawn: async (root) => { return { process: spawn(item.command[0], item.command.slice(1), { cwd: root, @@ -117,7 +116,7 @@ export namespace LSP { const result: LSPClient.Info[] = [] for (const server of Object.values(s.servers)) { if (server.extensions.length && !server.extensions.includes(extension)) continue - const root = await server.root(file, App.info()) + const root = await server.root(file) if (!root) continue if (s.broken.has(root + server.id)) continue @@ -126,7 +125,7 @@ export namespace LSP { result.push(match) continue } - const handle = await server.spawn(App.info(), root).catch((err) => { + const handle = await server.spawn(root).catch((err) => { s.broken.add(root + server.id) log.error(`Failed to spawn LSP server ${server.id}`, { error: err }) return undefined diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index e797d900..07cf3ec5 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1,5 +1,4 @@ import { spawn, type ChildProcessWithoutNullStreams } from "child_process" -import type { App } from "../app/app" import path from "path" import { Global } from "../global" import { Log } from "../util/log" @@ -7,6 +6,7 @@ import { BunProc } from "../bun" import { $ } from "bun" import fs from "fs/promises" import { Filesystem } from "../util/filesystem" +import { Instance } from "../project/instance" import { Flag } from "../flag/flag" export namespace LSPServer { @@ -17,18 +17,18 @@ export namespace LSPServer { initialization?: Record } - type RootFunction = (file: string, app: App.Info) => Promise + type RootFunction = (file: string) => Promise const NearestRoot = (patterns: string[]): RootFunction => { - return async (file, app) => { + return async (file) => { const files = Filesystem.up({ targets: patterns, start: path.dirname(file), - stop: app.path.root, + stop: Instance.worktree, }) const first = await files.next() await files.return() - if (!first.value) return app.path.root + if (!first.value) return Instance.worktree return path.dirname(first.value) } } @@ -38,15 +38,15 @@ export namespace LSPServer { extensions: string[] global?: boolean root: RootFunction - spawn(app: App.Info, root: string): Promise + spawn(root: string): Promise } export const Typescript: Info = { id: "typescript", root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], - async spawn(app, root) { - const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {}) + async spawn(root) { + const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {}) if (!tsserver) return const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], { cwd: root, @@ -83,7 +83,7 @@ export namespace LSPServer { "nuxt.config.js", "vue.config.js", ]), - async spawn(_, root) { + async spawn(root) { let binary = Bun.which("vue-language-server") const args: string[] = [] if (!binary) { @@ -145,8 +145,8 @@ export namespace LSPServer { "package.json", ]), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"], - async spawn(app, root) { - const eslint = await Bun.resolve("eslint", app.path.cwd).catch(() => {}) + async spawn(root) { + const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {}) if (!eslint) return log.info("spawning eslint server") const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js") @@ -194,13 +194,13 @@ export namespace LSPServer { export const Gopls: Info = { id: "gopls", - root: async (file, app) => { - const work = await NearestRoot(["go.work"])(file, app) + root: async (file) => { + const work = await NearestRoot(["go.work"])(file) if (work) return work - return NearestRoot(["go.mod", "go.sum"])(file, app) + return NearestRoot(["go.mod", "go.sum"])(file) }, extensions: [".go"], - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("gopls", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) @@ -238,7 +238,7 @@ export namespace LSPServer { id: "ruby-lsp", root: NearestRoot(["Gemfile"]), extensions: [".rb", ".rake", ".gemspec", ".ru"], - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("ruby-lsp", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) @@ -279,7 +279,7 @@ export namespace LSPServer { id: "pyright", extensions: [".py", ".pyi"], root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), - async spawn(_, root) { + async spawn(root) { let binary = Bun.which("pyright-langserver") const args = [] if (!binary) { @@ -333,7 +333,7 @@ export namespace LSPServer { id: "elixir-ls", extensions: [".ex", ".exs"], root: NearestRoot(["mix.exs", "mix.lock"]), - async spawn(_, root) { + async spawn(root) { let binary = Bun.which("elixir-ls") if (!binary) { const elixirLsPath = path.join(Global.Path.bin, "elixir-ls") @@ -389,7 +389,7 @@ export namespace LSPServer { id: "zls", extensions: [".zig", ".zon"], root: NearestRoot(["build.zig"]), - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("zls", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) @@ -495,7 +495,7 @@ export namespace LSPServer { id: "csharp", root: NearestRoot([".sln", ".csproj", "global.json"]), extensions: [".cs"], - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("csharp-ls", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) @@ -533,8 +533,8 @@ export namespace LSPServer { export const RustAnalyzer: Info = { id: "rust", - root: async (file, app) => { - const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(file, app) + root: async (root) => { + const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root) if (crateRoot === undefined) { return undefined } @@ -557,13 +557,13 @@ export namespace LSPServer { currentDir = parentDir // Stop if we've gone above the app root - if (!currentDir.startsWith(app.path.root)) break + if (!currentDir.startsWith(Instance.worktree)) break } return crateRoot }, extensions: [".rs"], - async spawn(_, root) { + async spawn(root) { const bin = Bun.which("rust-analyzer") if (!bin) { log.info("rust-analyzer not found in path, please install it") @@ -581,7 +581,7 @@ export namespace LSPServer { id: "clangd", root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]), extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"], - async spawn(_, root) { + async spawn(root) { let bin = Bun.which("clangd", { PATH: process.env["PATH"] + ":" + Global.Path.bin, }) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 2ac8fb7a..702f644d 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -2,13 +2,13 @@ import { experimental_createMCPClient, type Tool } from "ai" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" -import { App } from "../app/app" import { Config } from "../config/config" import { Log } from "../util/log" import { NamedError } from "../util/error" import { z } from "zod" import { Session } from "../session" import { Bus } from "../bus" +import { Instance } from "../project/instance" export namespace MCP { const log = Log.create({ service: "mcp" }) @@ -20,8 +20,7 @@ export namespace MCP { }), ) - const state = App.state( - "mcp", + const state = Instance.state( async () => { const cfg = await Config.get() const clients: { diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index b84081ae..b0c3ccfe 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,9 +1,9 @@ -import { App } from "../app/app" import { z } from "zod" import { Bus } from "../bus" import { Log } from "../util/log" import { Identifier } from "../id/id" import { Plugin } from "../plugin" +import { Instance } from "../project/instance" export namespace Permission { const log = Log.create({ service: "permission" }) @@ -35,8 +35,7 @@ export namespace Permission { ), } - const state = App.state( - "permission", + const state = Instance.state( () => { const pending: { [sessionID: string]: { diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index c3ef06b6..a9e9803f 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -1,26 +1,28 @@ import type { Hooks, Plugin as PluginInstance } from "@opencode-ai/plugin" -import { App } from "../app/app" import { Config } from "../config/config" import { Bus } from "../bus" import { Log } from "../util/log" import { createOpencodeClient } from "@opencode-ai/sdk" import { Server } from "../server/server" import { BunProc } from "../bun" +import { Instance } from "../project/instance" import { Flag } from "../flag/flag" export namespace Plugin { const log = Log.create({ service: "plugin" }) - const state = App.state("plugin", async (app) => { + const state = Instance.state(async () => { const client = createOpencodeClient({ baseUrl: "http://localhost:4096", - fetch: async (...args) => Server.app().fetch(...args), + fetch: async (...args) => Server.App.fetch(...args), }) const config = await Config.get() const hooks = [] const input = { client, - app, + project: Instance.project, + worktree: Instance.worktree, + directory: Instance.directory, $: Bun.$, } const plugins = [...(config.plugin ?? [])] diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts new file mode 100644 index 00000000..c2afee9b --- /dev/null +++ b/packages/opencode/src/project/instance.ts @@ -0,0 +1,27 @@ +import { Context } from "../util/context" +import { Project } from "./project" +import { State } from "./state" + +const context = Context.create<{ directory: string; worktree: string; project: Project.Info }>("path") + +export const Instance = { + async provide(directory: string, cb: () => R): Promise { + const project = await Project.fromDirectory(directory) + return context.provide({ directory, worktree: project.worktree, project }, cb) + }, + get directory() { + return context.use().directory + }, + get worktree() { + return context.use().worktree + }, + get project() { + return context.use().project + }, + state(init: () => S, dispose?: (state: Awaited) => Promise): () => S { + return State.create(() => Instance.directory, init, dispose) + }, + async dispose() { + await State.dispose(Instance.directory) + }, +} diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts new file mode 100644 index 00000000..c61a5ca6 --- /dev/null +++ b/packages/opencode/src/project/project.ts @@ -0,0 +1,93 @@ +import z from "zod" +import { Filesystem } from "../util/filesystem" +import path from "path" +import { $ } from "bun" +import { Storage } from "../storage/storage" +import { Log } from "../util/log" + +export namespace Project { + const log = Log.create({ service: "project" }) + export const Info = z + .object({ + id: z.string(), + worktree: z.string(), + vcs: z.literal("git").optional(), + time: z.object({ + created: z.number(), + initialized: z.number().optional(), + }), + }) + .openapi({ + ref: "Project", + }) + export type Info = z.infer + + const cache = new Map() + export async function fromDirectory(directory: string) { + log.info("fromDirectory", { directory }) + const fn = async () => { + const matches = Filesystem.up({ targets: [".git"], start: directory }) + const git = await matches.next().then((x) => x.value) + await matches.return() + if (!git) { + const project: Info = { + id: "global", + worktree: "/", + time: { + created: Date.now(), + }, + } + await Storage.write(["project", "global"], project) + return project + } + let worktree = path.dirname(git) + const [id] = await $`git rev-list --max-parents=0 --all` + .quiet() + .nothrow() + .cwd(worktree) + .text() + .then((x) => + x + .split("\n") + .filter(Boolean) + .map((x) => x.trim()) + .toSorted(), + ) + worktree = path.dirname( + await $`git rev-parse --path-format=absolute --git-common-dir` + .quiet() + .nothrow() + .cwd(worktree) + .text() + .then((x) => x.trim()), + ) + const project: Info = { + id, + worktree, + vcs: "git", + time: { + created: Date.now(), + }, + } + await Storage.write(["project", id], project) + return project + } + if (cache.has(directory)) { + return cache.get(directory)! + } + const result = await fn() + cache.set(directory, result) + return result + } + + export async function setInitialized(projectID: string) { + await Storage.update(["project", projectID], (draft) => { + draft.time.initialized = Date.now() + }) + } + + export async function list() { + const keys = await Storage.list(["project"]) + return await Promise.all(keys.map((x) => Storage.read(x))) + } +} diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts new file mode 100644 index 00000000..2ffef3b3 --- /dev/null +++ b/packages/opencode/src/project/state.ts @@ -0,0 +1,34 @@ +export namespace State { + interface Entry { + state: any + dispose?: (state: any) => Promise + } + + const entries = new Map>() + + export function create(root: () => string, init: () => S, dispose?: (state: Awaited) => Promise) { + return () => { + const key = root() + let collection = entries.get(key) + if (!collection) { + collection = new Map() + entries.set(key, collection) + } + const exists = collection.get(init) + if (exists) return exists.state as S + const state = init() + collection.set(init, { + state, + dispose, + }) + return state + } + } + + export async function dispose(key: string) { + for (const [_, entry] of entries.get(key)?.entries() ?? []) { + if (!entry.dispose) continue + await entry.dispose(await entry.state) + } + } +} diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c60bbf79..184c2c95 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,5 +1,4 @@ import z from "zod" -import { App } from "../app/app" import { Config } from "../config/config" import { mergeDeep, sortBy } from "remeda" import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai" @@ -9,6 +8,7 @@ import { Plugin } from "../plugin" import { ModelsDev } from "./models" import { NamedError } from "../util/error" import { Auth } from "../auth" +import { Instance } from "../project/instance" export namespace Provider { const log = Log.create({ service: "provider" }) @@ -141,7 +141,7 @@ export namespace Provider { }, } - const state = App.state("provider", async () => { + const state = Instance.state(async () => { const config = await Config.get() const database = await ModelsDev.get() @@ -153,7 +153,10 @@ export namespace Provider { options: Record } } = {} - const models = new Map() + const models = new Map< + string, + { providerID: string; modelID: string; info: ModelsDev.Model; language: LanguageModel } + >() const sdk = new Map() log.info("init") @@ -362,10 +365,14 @@ export namespace Provider { const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID) log.info("found", { providerID, modelID }) s.models.set(key, { + providerID, + modelID, info, language, }) return { + modelID, + providerID, info, language, } diff --git a/packages/opencode/src/server/project.ts b/packages/opencode/src/server/project.ts new file mode 100644 index 00000000..a89061b7 --- /dev/null +++ b/packages/opencode/src/server/project.ts @@ -0,0 +1,48 @@ +import { Hono } from "hono" +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { Instance } from "../project/instance" +import { Project } from "../project/project" + +export const ProjectRoute = new Hono() + .get( + "/", + describeRoute({ + description: "List all projects", + operationId: "project.list", + responses: { + 200: { + description: "List of projects", + content: { + "application/json": { + schema: resolver(Project.Info.array()), + }, + }, + }, + }, + }), + async (c) => { + const projects = await Project.list() + return c.json(projects) + }, + ) + .get( + "/current", + describeRoute({ + description: "Get the current project", + operationId: "project.current", + responses: { + 200: { + description: "Current project", + content: { + "application/json": { + schema: resolver(Project.Info), + }, + }, + }, + }, + }), + async (c) => { + return c.json(Instance.project) + }, + ) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 27d4aa92..01ade6d1 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -7,7 +7,6 @@ import { Session } from "../session" import { resolver, validator as zValidator } from "hono-openapi/zod" import { z } from "zod" import { Provider } from "../provider/provider" -import { App } from "../app/app" import { mapValues } from "remeda" import { NamedError } from "../util/error" import { ModelsDev } from "../provider/models" @@ -18,10 +17,12 @@ import { LSP } from "../lsp" import { MessageV2 } from "../session/message-v2" import { callTui, TuiRoute } from "./tui" import { Permission } from "../permission" -import { lazy } from "../util/lazy" +import { Instance } from "../project/instance" import { Agent } from "../agent/agent" import { Auth } from "../auth" import { Command } from "../command" +import { Global } from "../global" +import { ProjectRoute } from "./project" const ERRORS = { 400: { @@ -45,1270 +46,1266 @@ const ERRORS = { export namespace Server { const log = Log.create({ service: "server" }) - export type Routes = ReturnType - export const Event = { Connected: Bus.event("server.connected", z.object({})), } - export const app = lazy(() => { - const app = new Hono() - - const result = app - .onError((err, c) => { - if (err instanceof NamedError) { - return c.json(err.toObject(), { - status: 400, - }) - } - return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), { + const app = new Hono() + export const App = app + .onError((err, c) => { + log.error("failed", { + error: err, + }) + if (err instanceof NamedError) { + return c.json(err.toObject(), { status: 400, }) + } + return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), { + status: 400, }) - .use(async (c, next) => { - const skipLogging = c.req.path === "/log" - if (!skipLogging) { - log.info("request", { - method: c.req.method, - path: c.req.path, - }) - } - const start = Date.now() - await next() - if (!skipLogging) { - log.info("response", { - duration: Date.now() - start, - }) - } + }) + .use(async (c, next) => { + const skipLogging = c.req.path === "/log" + if (!skipLogging) { + log.info("request", { + method: c.req.method, + path: c.req.path, + }) + } + const start = Date.now() + await next() + if (!skipLogging) { + log.info("response", { + duration: Date.now() - start, + }) + } + }) + .use(async (c, next) => { + const directory = c.req.query("directory") ?? process.cwd() + return Instance.provide(directory, async () => { + return next() }) - .get( - "/doc", - openAPISpecs(app, { - documentation: { - info: { - title: "opencode", - version: "0.0.3", - description: "opencode api", - }, - openapi: "3.1.1", + }) + .use(zValidator("query", z.object({ directory: z.string().optional() }))) + .get( + "/doc", + openAPISpecs(app, { + documentation: { + info: { + title: "opencode", + version: "0.0.3", + description: "opencode api", }, - }), - ) - .get( - "/event", - describeRoute({ - description: "Get events", - operationId: "event.subscribe", - responses: { - 200: { - description: "Event stream", - content: { - "text/event-stream": { - schema: resolver( - Bus.payloads().openapi({ - ref: "Event", + openapi: "3.1.1", + }, + }), + ) + .route("/project", ProjectRoute) + .get( + "/event", + describeRoute({ + description: "Get events", + operationId: "event.subscribe", + responses: { + 200: { + description: "Event stream", + content: { + "text/event-stream": { + schema: resolver( + Bus.payloads().openapi({ + ref: "Event", + }), + ), + }, + }, + }, + }, + }), + async (c) => { + log.info("event connected") + return streamSSE(c, async (stream) => { + stream.writeSSE({ + data: JSON.stringify({ + type: "server.connected", + properties: {}, + }), + }) + const unsub = Bus.subscribeAll(async (event) => { + await stream.writeSSE({ + data: JSON.stringify(event), + }) + }) + await new Promise((resolve) => { + stream.onAbort(() => { + unsub() + resolve() + log.info("event disconnected") + }) + }) + }) + }, + ) + .get( + "/config", + describeRoute({ + description: "Get config info", + operationId: "config.get", + responses: { + 200: { + description: "Get config info", + content: { + "application/json": { + schema: resolver(Config.Info), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await Config.get()) + }, + ) + .get( + "/path", + describeRoute({ + description: "Get the current path", + operationId: "path.get", + responses: { + 200: { + description: "Path", + content: { + "application/json": { + schema: resolver( + z + .object({ + state: z.string(), + config: z.string(), + worktree: z.string(), + directory: z.string(), + }) + .openapi({ + ref: "Path", }), - ), - }, + ), }, }, }, - }), - async (c) => { - log.info("event connected") - return streamSSE(c, async (stream) => { - stream.writeSSE({ - data: JSON.stringify({ - type: "server.connected", - properties: {}, - }), - }) - const unsub = Bus.subscribeAll(async (event) => { - await stream.writeSSE({ - data: JSON.stringify(event), - }) - }) - await new Promise((resolve) => { - stream.onAbort(() => { - unsub() - resolve() - log.info("event disconnected") - }) - }) - }) }, - ) - .get( - "/app", - describeRoute({ - description: "Get app info", - operationId: "app.get", - responses: { - 200: { - description: "200", - content: { - "application/json": { - schema: resolver(App.Info), - }, + }), + async (c) => { + return c.json({ + state: Global.Path.state, + config: Global.Path.config, + worktree: Instance.worktree, + directory: Instance.directory, + }) + }, + ) + .get( + "/session", + describeRoute({ + description: "List all sessions", + operationId: "session.list", + responses: { + 200: { + description: "List of sessions", + content: { + "application/json": { + schema: resolver(Session.Info.array()), }, }, }, - }), - async (c) => { - return c.json(App.info()) }, - ) - .post( - "/app/init", - describeRoute({ - description: "Initialize the app", - operationId: "app.init", - responses: { - 200: { - description: "Initialize the app", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, + }), + async (c) => { + const sessions = await Array.fromAsync(Session.list()) + sessions.sort((a, b) => b.time.updated - a.time.updated) + return c.json(sessions) + }, + ) + .get( + "/session/:id", + describeRoute({ + description: "Get session", + operationId: "session.get", + responses: { + 200: { + description: "Get session", + content: { + "application/json": { + schema: resolver(Session.Info), }, }, }, - }), - async (c) => { - await App.initialize() - return c.json(true) }, - ) - .get( - "/config", - describeRoute({ - description: "Get config info", - operationId: "config.get", - responses: { - 200: { - description: "Get config info", - content: { - "application/json": { - schema: resolver(Config.Info), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + const sessionID = c.req.valid("param").id + const session = await Session.get(sessionID) + return c.json(session) + }, + ) + .get( + "/session/:id/children", + describeRoute({ + description: "Get a session's children", + operationId: "session.children", + responses: { + 200: { + description: "List of children", + content: { + "application/json": { + schema: resolver(Session.Info.array()), }, }, }, - }), - async (c) => { - return c.json(await Config.get()) }, - ) - .get( - "/session", - describeRoute({ - description: "List all sessions", - operationId: "session.list", - responses: { - 200: { - description: "List of sessions", - content: { - "application/json": { - schema: resolver(Session.Info.array()), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + const sessionID = c.req.valid("param").id + const session = await Session.children(sessionID) + return c.json(session) + }, + ) + .post( + "/session", + describeRoute({ + description: "Create a new session", + operationId: "session.create", + responses: { + ...ERRORS, + 200: { + description: "Successfully created session", + content: { + "application/json": { + schema: resolver(Session.Info), }, }, }, - }), - async (c) => { - const sessions = await Array.fromAsync(Session.list()) - sessions.sort((a, b) => b.time.updated - a.time.updated) - return c.json(sessions) }, - ) - .get( - "/session/:id", - describeRoute({ - description: "Get session", - operationId: "session.get", - responses: { - 200: { - description: "Get session", - content: { - "application/json": { - schema: resolver(Session.Info), - }, - }, - }, - }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - const sessionID = c.req.valid("param").id - const session = await Session.get(sessionID) - return c.json(session) - }, - ) - .get( - "/session/:id/children", - describeRoute({ - description: "Get a session's children", - operationId: "session.children", - responses: { - 200: { - description: "List of children", - content: { - "application/json": { - schema: resolver(Session.Info.array()), - }, - }, - }, - }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - const sessionID = c.req.valid("param").id - const session = await Session.children(sessionID) - return c.json(session) - }, - ) - .post( - "/session", - describeRoute({ - description: "Create a new session", - operationId: "session.create", - responses: { - ...ERRORS, - 200: { - description: "Successfully created session", - content: { - "application/json": { - schema: resolver(Session.Info), - }, - }, - }, - }, - }), - zValidator( - "json", - z - .object({ - parentID: z.string().optional(), - title: z.string().optional(), - }) - .optional(), - ), - async (c) => { - const body = c.req.valid("json") ?? {} - const session = await Session.create(body.parentID, body.title) - return c.json(session) - }, - ) - .delete( - "/session/:id", - describeRoute({ - description: "Delete a session and all its data", - operationId: "session.delete", - responses: { - 200: { - description: "Successfully deleted session", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - await Session.remove(c.req.valid("param").id) - return c.json(true) - }, - ) - .patch( - "/session/:id", - describeRoute({ - description: "Update session properties", - operationId: "session.update", - responses: { - 200: { - description: "Successfully updated session", - content: { - "application/json": { - schema: resolver(Session.Info), - }, - }, - }, - }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - zValidator( - "json", - z.object({ + }), + zValidator( + "json", + z + .object({ + parentID: z.string().optional(), title: z.string().optional(), - }), - ), - async (c) => { - const sessionID = c.req.valid("param").id - const updates = c.req.valid("json") - - const updatedSession = await Session.update(sessionID, (session) => { - if (updates.title !== undefined) { - session.title = updates.title - } }) + .optional(), + ), + async (c) => { + const body = c.req.valid("json") ?? {} + const session = await Session.create(body.parentID, body.title) + return c.json(session) + }, + ) + .delete( + "/session/:id", + describeRoute({ + description: "Delete a session and all its data", + operationId: "session.delete", + responses: { + 200: { + description: "Successfully deleted session", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + await Session.remove(c.req.valid("param").id) + return c.json(true) + }, + ) + .patch( + "/session/:id", + describeRoute({ + description: "Update session properties", + operationId: "session.update", + responses: { + 200: { + description: "Successfully updated session", + content: { + "application/json": { + schema: resolver(Session.Info), + }, + }, + }, + }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + zValidator( + "json", + z.object({ + title: z.string().optional(), + }), + ), + async (c) => { + const sessionID = c.req.valid("param").id + const updates = c.req.valid("json") - return c.json(updatedSession) - }, - ) - .post( - "/session/:id/init", - describeRoute({ - description: "Analyze the app and create an AGENTS.md file", - operationId: "session.init", - responses: { - 200: { - description: "200", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, + const updatedSession = await Session.update(sessionID, (session) => { + if (updates.title !== undefined) { + session.title = updates.title + } + }) + + return c.json(updatedSession) + }, + ) + .post( + "/session/:id/init", + describeRoute({ + description: "Analyze the app and create an AGENTS.md file", + operationId: "session.init", + responses: { + 200: { + description: "200", + content: { + "application/json": { + schema: resolver(z.boolean()), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string().openapi({ description: "Session ID" }), - }), - ), - zValidator( - "json", - z.object({ - messageID: z.string(), - providerID: z.string(), - modelID: z.string(), - }), - ), - async (c) => { - const sessionID = c.req.valid("param").id - const body = c.req.valid("json") - await Session.initialize({ ...body, sessionID }) - return c.json(true) }, - ) - .post( - "/session/:id/abort", - describeRoute({ - description: "Abort a session", - operationId: "session.abort", - responses: { - 200: { - description: "Aborted session", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + }), + ), + zValidator( + "json", + z.object({ + messageID: z.string(), + providerID: z.string(), + modelID: z.string(), + }), + ), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + await Session.initialize({ ...body, sessionID }) + return c.json(true) + }, + ) + .post( + "/session/:id/abort", + describeRoute({ + description: "Abort a session", + operationId: "session.abort", + responses: { + 200: { + description: "Aborted session", + content: { + "application/json": { + schema: resolver(z.boolean()), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - return c.json(Session.abort(c.req.valid("param").id)) }, - ) - .post( - "/session/:id/share", - describeRoute({ - description: "Share a session", - operationId: "session.share", - responses: { - 200: { - description: "Successfully shared session", - content: { - "application/json": { - schema: resolver(Session.Info), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + return c.json(Session.abort(c.req.valid("param").id)) + }, + ) + .post( + "/session/:id/share", + describeRoute({ + description: "Share a session", + operationId: "session.share", + responses: { + 200: { + description: "Successfully shared session", + content: { + "application/json": { + schema: resolver(Session.Info), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - const id = c.req.valid("param").id - await Session.share(id) - const session = await Session.get(id) - return c.json(session) }, - ) - .delete( - "/session/:id/share", - describeRoute({ - description: "Unshare the session", - operationId: "session.unshare", - responses: { - 200: { - description: "Successfully unshared session", - content: { - "application/json": { - schema: resolver(Session.Info), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + const id = c.req.valid("param").id + await Session.share(id) + const session = await Session.get(id) + return c.json(session) + }, + ) + .delete( + "/session/:id/share", + describeRoute({ + description: "Unshare the session", + operationId: "session.unshare", + responses: { + 200: { + description: "Successfully unshared session", + content: { + "application/json": { + schema: resolver(Session.Info), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - const id = c.req.valid("param").id - await Session.unshare(id) - const session = await Session.get(id) - return c.json(session) }, - ) - .post( - "/session/:id/summarize", - describeRoute({ - description: "Summarize the session", - operationId: "session.summarize", - responses: { - 200: { - description: "Summarized session", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + const id = c.req.valid("param").id + await Session.unshare(id) + const session = await Session.get(id) + return c.json(session) + }, + ) + .post( + "/session/:id/summarize", + describeRoute({ + description: "Summarize the session", + operationId: "session.summarize", + responses: { + 200: { + description: "Summarized session", + content: { + "application/json": { + schema: resolver(z.boolean()), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string().openapi({ description: "Session ID" }), - }), - ), - zValidator( - "json", - z.object({ - providerID: z.string(), - modelID: z.string(), - }), - ), - async (c) => { - const id = c.req.valid("param").id - const body = c.req.valid("json") - await Session.summarize({ ...body, sessionID: id }) - return c.json(true) }, - ) - .get( - "/session/:id/message", - describeRoute({ - description: "List messages for a session", - operationId: "session.messages", - responses: { - 200: { - description: "List of messages", - content: { - "application/json": { - schema: resolver( - z - .object({ - info: MessageV2.Info, - parts: MessageV2.Part.array(), - }) - .array(), - ), - }, - }, - }, - }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), }), - zValidator( - "param", - z.object({ - id: z.string().openapi({ description: "Session ID" }), - }), - ), - async (c) => { - const messages = await Session.messages(c.req.valid("param").id) - return c.json(messages) - }, - ) - .get( - "/session/:id/message/:messageID", - describeRoute({ - description: "Get a message from a session", - operationId: "session.message", - responses: { - 200: { - description: "Message", - content: { - "application/json": { - schema: resolver( - z.object({ + ), + zValidator( + "json", + z.object({ + providerID: z.string(), + modelID: z.string(), + }), + ), + async (c) => { + const id = c.req.valid("param").id + const body = c.req.valid("json") + await Session.summarize({ ...body, sessionID: id }) + return c.json(true) + }, + ) + .get( + "/session/:id/message", + describeRoute({ + description: "List messages for a session", + operationId: "session.messages", + responses: { + 200: { + description: "List of messages", + content: { + "application/json": { + schema: resolver( + z + .object({ info: MessageV2.Info, parts: MessageV2.Part.array(), - }), - ), - }, + }) + .array(), + ), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string().openapi({ description: "Session ID" }), - messageID: z.string().openapi({ description: "Message ID" }), - }), - ), - async (c) => { - const params = c.req.valid("param") - const message = await Session.getMessage(params.id, params.messageID) - return c.json(message) }, - ) - .post( - "/session/:id/message", - describeRoute({ - description: "Create and send a new message to a session", - operationId: "session.chat", - responses: { - 200: { - description: "Created message", - content: { - "application/json": { - schema: resolver( - z.object({ - info: MessageV2.Assistant, - parts: MessageV2.Part.array(), - }), - ), - }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + }), + ), + async (c) => { + const messages = await Session.messages(c.req.valid("param").id) + return c.json(messages) + }, + ) + .get( + "/session/:id/message/:messageID", + describeRoute({ + description: "Get a message from a session", + operationId: "session.message", + responses: { + 200: { + description: "Message", + content: { + "application/json": { + schema: resolver( + z.object({ + info: MessageV2.Info, + parts: MessageV2.Part.array(), + }), + ), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string().openapi({ description: "Session ID" }), - }), - ), - zValidator("json", Session.ChatInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").id - const body = c.req.valid("json") - const msg = await Session.chat({ ...body, sessionID }) - return c.json(msg) }, - ) - .post( - "/session/:id/command", - describeRoute({ - description: "Send a new command to a session", - operationId: "session.command", - responses: { - 200: { - description: "Created message", - content: { - "application/json": { - schema: resolver( - z.object({ - info: MessageV2.Assistant, - parts: MessageV2.Part.array(), - }), - ), - }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + messageID: z.string().openapi({ description: "Message ID" }), + }), + ), + async (c) => { + const params = c.req.valid("param") + const message = await Session.getMessage(params.id, params.messageID) + return c.json(message) + }, + ) + .post( + "/session/:id/message", + describeRoute({ + description: "Create and send a new message to a session", + operationId: "session.prompt", + responses: { + 200: { + description: "Created message", + content: { + "application/json": { + schema: resolver( + z.object({ + info: MessageV2.Assistant, + parts: MessageV2.Part.array(), + }), + ), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string().openapi({ description: "Session ID" }), - }), - ), - zValidator("json", Session.CommandInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").id - const body = c.req.valid("json") - const msg = await Session.command({ ...body, sessionID }) - return c.json(msg) }, - ) - .post( - "/session/:id/shell", - describeRoute({ - description: "Run a shell command", - operationId: "session.shell", - responses: { - 200: { - description: "Created message", - content: { - "application/json": { - schema: resolver(MessageV2.Assistant), - }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + }), + ), + zValidator("json", Session.PromptInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await Session.prompt({ ...body, sessionID }) + return c.json(msg) + }, + ) + .post( + "/session/:id/command", + describeRoute({ + description: "Send a new command to a session", + operationId: "session.command", + responses: { + 200: { + description: "Created message", + content: { + "application/json": { + schema: resolver( + z.object({ + info: MessageV2.Assistant, + parts: MessageV2.Part.array(), + }), + ), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string().openapi({ description: "Session ID" }), - }), - ), - zValidator("json", Session.ShellInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").id - const body = c.req.valid("json") - const msg = await Session.shell({ ...body, sessionID }) - return c.json(msg) }, - ) - .post( - "/session/:id/revert", - describeRoute({ - description: "Revert a message", - operationId: "session.revert", - responses: { - 200: { - description: "Updated session", - content: { - "application/json": { - schema: resolver(Session.Info), - }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + }), + ), + zValidator("json", Session.CommandInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await Session.command({ ...body, sessionID }) + return c.json(msg) + }, + ) + .post( + "/session/:id/shell", + describeRoute({ + description: "Run a shell command", + operationId: "session.shell", + responses: { + 200: { + description: "Created message", + content: { + "application/json": { + schema: resolver(MessageV2.Assistant), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - zValidator("json", Session.RevertInput.omit({ sessionID: true })), - async (c) => { - const id = c.req.valid("param").id - log.info("revert", c.req.valid("json")) - const session = await Session.revert({ sessionID: id, ...c.req.valid("json") }) - return c.json(session) }, - ) - .post( - "/session/:id/unrevert", - describeRoute({ - description: "Restore all reverted messages", - operationId: "session.unrevert", - responses: { - 200: { - description: "Updated session", - content: { - "application/json": { - schema: resolver(Session.Info), - }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + }), + ), + zValidator("json", Session.ShellInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await Session.shell({ ...body, sessionID }) + return c.json(msg) + }, + ) + .post( + "/session/:id/revert", + describeRoute({ + description: "Revert a message", + operationId: "session.revert", + responses: { + 200: { + description: "Updated session", + content: { + "application/json": { + schema: resolver(Session.Info), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - const id = c.req.valid("param").id - const session = await Session.unrevert({ sessionID: id }) - return c.json(session) }, - ) - .post( - "/session/:id/permissions/:permissionID", - describeRoute({ - description: "Respond to a permission request", - responses: { - 200: { - description: "Permission processed successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + zValidator("json", Session.RevertInput.omit({ sessionID: true })), + async (c) => { + const id = c.req.valid("param").id + log.info("revert", c.req.valid("json")) + const session = await Session.revert({ sessionID: id, ...c.req.valid("json") }) + return c.json(session) + }, + ) + .post( + "/session/:id/unrevert", + describeRoute({ + description: "Restore all reverted messages", + operationId: "session.unrevert", + responses: { + 200: { + description: "Updated session", + content: { + "application/json": { + schema: resolver(Session.Info), }, }, }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - permissionID: z.string(), - }), - ), - zValidator("json", z.object({ response: Permission.Response })), - async (c) => { - const params = c.req.valid("param") - const id = params.id - const permissionID = params.permissionID - Permission.respond({ sessionID: id, permissionID, response: c.req.valid("json").response }) - return c.json(true) }, - ) - .get( - "/command", - describeRoute({ - description: "List all commands", - operationId: "command.list", - responses: { - 200: { - description: "List of commands", - content: { - "application/json": { - schema: resolver(Command.Info.array()), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + const id = c.req.valid("param").id + const session = await Session.unrevert({ sessionID: id }) + return c.json(session) + }, + ) + .post( + "/session/:id/permissions/:permissionID", + describeRoute({ + description: "Respond to a permission request", + responses: { + 200: { + description: "Permission processed successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), }, }, }, - }), - async (c) => { - const commands = await Command.list() - return c.json(commands) }, - ) - .get( - "/config/providers", - describeRoute({ - description: "List all providers", - operationId: "config.providers", - responses: { - 200: { - description: "List of providers", - content: { - "application/json": { - schema: resolver( - z.object({ - providers: ModelsDev.Provider.array(), - default: z.record(z.string(), z.string()), - }), - ), - }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + permissionID: z.string(), + }), + ), + zValidator("json", z.object({ response: Permission.Response })), + async (c) => { + const params = c.req.valid("param") + const id = params.id + const permissionID = params.permissionID + Permission.respond({ sessionID: id, permissionID, response: c.req.valid("json").response }) + return c.json(true) + }, + ) + .get( + "/command", + describeRoute({ + description: "List all commands", + operationId: "command.list", + responses: { + 200: { + description: "List of commands", + content: { + "application/json": { + schema: resolver(Command.Info.array()), }, }, }, - }), - async (c) => { - const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info)) - return c.json({ - providers: Object.values(providers), - default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), - }) }, - ) - .get( - "/find", - describeRoute({ - description: "Find text in files", - operationId: "find.text", - responses: { - 200: { - description: "Matches", - content: { - "application/json": { - schema: resolver(Ripgrep.Match.shape.data.array()), - }, + }), + async (c) => { + const commands = await Command.list() + return c.json(commands) + }, + ) + .get( + "/config/providers", + describeRoute({ + description: "List all providers", + operationId: "config.providers", + responses: { + 200: { + description: "List of providers", + content: { + "application/json": { + schema: resolver( + z.object({ + providers: ModelsDev.Provider.array(), + default: z.record(z.string(), z.string()), + }), + ), }, }, }, - }), - zValidator( - "query", - z.object({ - pattern: z.string(), - }), - ), - async (c) => { - const app = App.info() - const pattern = c.req.valid("query").pattern - const result = await Ripgrep.search({ - cwd: app.path.cwd, - pattern, - limit: 10, - }) - return c.json(result) }, - ) - .get( - "/find/file", - describeRoute({ - description: "Find files", - operationId: "find.files", - responses: { - 200: { - description: "File paths", - content: { - "application/json": { - schema: resolver(z.string().array()), - }, + }), + async (c) => { + const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info)) + return c.json({ + providers: Object.values(providers), + default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + }) + }, + ) + .get( + "/find", + describeRoute({ + description: "Find text in files", + operationId: "find.text", + responses: { + 200: { + description: "Matches", + content: { + "application/json": { + schema: resolver(Ripgrep.Match.shape.data.array()), }, }, }, - }), - zValidator( - "query", - z.object({ - query: z.string(), - }), - ), - async (c) => { - const query = c.req.valid("query").query - const app = App.info() - const result = await Ripgrep.files({ - cwd: app.path.cwd, - query, - limit: 10, - }) - return c.json(result) }, - ) - .get( - "/find/symbol", - describeRoute({ - description: "Find workspace symbols", - operationId: "find.symbols", - responses: { - 200: { - description: "Symbols", - content: { - "application/json": { - schema: resolver(LSP.Symbol.array()), - }, + }), + zValidator( + "query", + z.object({ + pattern: z.string(), + }), + ), + async (c) => { + const pattern = c.req.valid("query").pattern + const result = await Ripgrep.search({ + cwd: Instance.directory, + pattern, + limit: 10, + }) + return c.json(result) + }, + ) + .get( + "/find/file", + describeRoute({ + description: "Find files", + operationId: "find.files", + responses: { + 200: { + description: "File paths", + content: { + "application/json": { + schema: resolver(z.string().array()), }, }, }, - }), - zValidator( - "query", - z.object({ - query: z.string(), - }), - ), - async (c) => { - const query = c.req.valid("query").query - const result = await LSP.workspaceSymbol(query) - return c.json(result) }, - ) - .get( - "/file", - describeRoute({ - description: "List files and directories", - operationId: "file.list", - responses: { - 200: { - description: "Files and directories", - content: { - "application/json": { - schema: resolver(File.Node.array()), - }, + }), + zValidator( + "query", + z.object({ + query: z.string(), + }), + ), + async (c) => { + const query = c.req.valid("query").query + const result = await Ripgrep.files({ + cwd: Instance.directory, + query, + limit: 10, + }) + return c.json(result) + }, + ) + .get( + "/find/symbol", + describeRoute({ + description: "Find workspace symbols", + operationId: "find.symbols", + responses: { + 200: { + description: "Symbols", + content: { + "application/json": { + schema: resolver(LSP.Symbol.array()), }, }, }, - }), - zValidator( - "query", - z.object({ - path: z.string(), - }), - ), - async (c) => { - const path = c.req.valid("query").path - const content = await File.list(path) - return c.json(content) }, - ) - .get( - "/file/content", - describeRoute({ - description: "Read a file", - operationId: "file.read", - responses: { - 200: { - description: "File content", - content: { - "application/json": { - schema: resolver( - z.object({ - type: z.enum(["raw", "patch"]), - content: z.string(), - }), - ), - }, + }), + zValidator( + "query", + z.object({ + query: z.string(), + }), + ), + async (c) => { + const query = c.req.valid("query").query + const result = await LSP.workspaceSymbol(query) + return c.json(result) + }, + ) + .get( + "/file", + describeRoute({ + description: "List files and directories", + operationId: "file.list", + responses: { + 200: { + description: "Files and directories", + content: { + "application/json": { + schema: resolver(File.Node.array()), }, }, }, - }), - zValidator( - "query", - z.object({ - path: z.string(), - }), - ), - async (c) => { - const path = c.req.valid("query").path - const content = await File.read(path) - return c.json(content) }, - ) - .get( - "/file/status", - describeRoute({ - description: "Get file status", - operationId: "file.status", - responses: { - 200: { - description: "File status", - content: { - "application/json": { - schema: resolver(File.Info.array()), - }, + }), + zValidator( + "query", + z.object({ + path: z.string(), + }), + ), + async (c) => { + const path = c.req.valid("query").path + const content = await File.list(path) + return c.json(content) + }, + ) + .get( + "/file/content", + describeRoute({ + description: "Read a file", + operationId: "file.read", + responses: { + 200: { + description: "File content", + content: { + "application/json": { + schema: resolver( + z.object({ + type: z.enum(["raw", "patch"]), + content: z.string(), + }), + ), }, }, }, - }), - async (c) => { - const content = await File.status() - return c.json(content) }, - ) - .post( - "/log", - describeRoute({ - description: "Write a log entry to the server logs", - operationId: "app.log", - responses: { - 200: { - description: "Log entry written successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, + }), + zValidator( + "query", + z.object({ + path: z.string(), + }), + ), + async (c) => { + const path = c.req.valid("query").path + const content = await File.read(path) + return c.json(content) + }, + ) + .get( + "/file/status", + describeRoute({ + description: "Get file status", + operationId: "file.status", + responses: { + 200: { + description: "File status", + content: { + "application/json": { + schema: resolver(File.Info.array()), }, }, }, + }, + }), + async (c) => { + const content = await File.status() + return c.json(content) + }, + ) + .post( + "/log", + describeRoute({ + description: "Write a log entry to the server logs", + operationId: "app.log", + responses: { + 200: { + description: "Log entry written successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + zValidator( + "json", + z.object({ + service: z.string().openapi({ description: "Service name for the log entry" }), + level: z.enum(["debug", "info", "error", "warn"]).openapi({ description: "Log level" }), + message: z.string().openapi({ description: "Log message" }), + extra: z + .record(z.string(), z.any()) + .optional() + .openapi({ description: "Additional metadata for the log entry" }), }), - zValidator( - "json", - z.object({ - service: z.string().openapi({ description: "Service name for the log entry" }), - level: z.enum(["debug", "info", "error", "warn"]).openapi({ description: "Log level" }), - message: z.string().openapi({ description: "Log message" }), - extra: z - .record(z.string(), z.any()) - .optional() - .openapi({ description: "Additional metadata for the log entry" }), - }), - ), - async (c) => { - const { service, level, message, extra } = c.req.valid("json") - const logger = Log.create({ service }) + ), + async (c) => { + const { service, level, message, extra } = c.req.valid("json") + const logger = Log.create({ service }) - switch (level) { - case "debug": - logger.debug(message, extra) - break - case "info": - logger.info(message, extra) - break - case "error": - logger.error(message, extra) - break - case "warn": - logger.warn(message, extra) - break - } + switch (level) { + case "debug": + logger.debug(message, extra) + break + case "info": + logger.info(message, extra) + break + case "error": + logger.error(message, extra) + break + case "warn": + logger.warn(message, extra) + break + } - return c.json(true) + return c.json(true) + }, + ) + .get( + "/agent", + describeRoute({ + description: "List all agents", + operationId: "app.agents", + responses: { + 200: { + description: "List of agents", + content: { + "application/json": { + schema: resolver(Agent.Info.array()), + }, + }, + }, }, - ) - .get( - "/agent", - describeRoute({ - description: "List all agents", - operationId: "app.agents", - responses: { - 200: { - description: "List of agents", - content: { - "application/json": { - schema: resolver(Agent.Info.array()), - }, + }), + async (c) => { + const modes = await Agent.list() + return c.json(modes) + }, + ) + .post( + "/tui/append-prompt", + describeRoute({ + description: "Append prompt to the TUI", + operationId: "tui.appendPrompt", + responses: { + 200: { + description: "Prompt processed successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), }, }, }, - }), - async (c) => { - const modes = await Agent.list() - return c.json(modes) }, - ) - .post( - "/tui/append-prompt", - describeRoute({ - description: "Append prompt to the TUI", - operationId: "tui.appendPrompt", - responses: { - 200: { - description: "Prompt processed successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, + }), + zValidator( + "json", + z.object({ + text: z.string(), + }), + ), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/open-help", + describeRoute({ + description: "Open the help dialog", + operationId: "tui.openHelp", + responses: { + 200: { + description: "Help dialog opened successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), }, }, }, - }), - zValidator( - "json", - z.object({ - text: z.string(), - }), - ), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/open-help", - describeRoute({ - description: "Open the help dialog", - operationId: "tui.openHelp", - responses: { - 200: { - description: "Help dialog opened successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/open-sessions", - describeRoute({ - description: "Open the session dialog", - operationId: "tui.openSessions", - responses: { - 200: { - description: "Session dialog opened successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/open-themes", - describeRoute({ - description: "Open the theme dialog", - operationId: "tui.openThemes", - responses: { - 200: { - description: "Theme dialog opened successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/open-models", - describeRoute({ - description: "Open the model dialog", - operationId: "tui.openModels", - responses: { - 200: { - description: "Model dialog opened successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/submit-prompt", - describeRoute({ - description: "Submit the prompt", - operationId: "tui.submitPrompt", - responses: { - 200: { - description: "Prompt submitted successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/clear-prompt", - describeRoute({ - description: "Clear the prompt", - operationId: "tui.clearPrompt", - responses: { - 200: { - description: "Prompt cleared successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/execute-command", - describeRoute({ - description: "Execute a TUI command (e.g. agent_cycle)", - operationId: "tui.executeCommand", - responses: { - 200: { - description: "Command executed successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - zValidator( - "json", - z.object({ - command: z.string(), - }), - ), - async (c) => c.json(await callTui(c)), - ) - .post( - "/tui/show-toast", - describeRoute({ - description: "Show a toast notification in the TUI", - operationId: "tui.showToast", - responses: { - 200: { - description: "Toast notification shown successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - zValidator( - "json", - z.object({ - title: z.string().optional(), - message: z.string(), - variant: z.enum(["info", "success", "warning", "error"]), - }), - ), - async (c) => c.json(await callTui(c)), - ) - .route("/tui/control", TuiRoute) - .put( - "/auth/:id", - describeRoute({ - description: "Set authentication credentials", - operationId: "auth.set", - responses: { - 200: { - description: "Successfully set authentication credentials", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - ...ERRORS, - }, - }), - zValidator( - "param", - z.object({ - id: z.string(), - }), - ), - zValidator("json", Auth.Info), - async (c) => { - const id = c.req.valid("param").id - const info = c.req.valid("json") - await Auth.set(id, info) - return c.json(true) }, - ) - - return result - }) + }), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/open-sessions", + describeRoute({ + description: "Open the session dialog", + operationId: "tui.openSessions", + responses: { + 200: { + description: "Session dialog opened successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/open-themes", + describeRoute({ + description: "Open the theme dialog", + operationId: "tui.openThemes", + responses: { + 200: { + description: "Theme dialog opened successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/open-models", + describeRoute({ + description: "Open the model dialog", + operationId: "tui.openModels", + responses: { + 200: { + description: "Model dialog opened successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/submit-prompt", + describeRoute({ + description: "Submit the prompt", + operationId: "tui.submitPrompt", + responses: { + 200: { + description: "Prompt submitted successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/clear-prompt", + describeRoute({ + description: "Clear the prompt", + operationId: "tui.clearPrompt", + responses: { + 200: { + description: "Prompt cleared successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/execute-command", + describeRoute({ + description: "Execute a TUI command (e.g. agent_cycle)", + operationId: "tui.executeCommand", + responses: { + 200: { + description: "Command executed successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + zValidator( + "json", + z.object({ + command: z.string(), + }), + ), + async (c) => c.json(await callTui(c)), + ) + .post( + "/tui/show-toast", + describeRoute({ + description: "Show a toast notification in the TUI", + operationId: "tui.showToast", + responses: { + 200: { + description: "Toast notification shown successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + zValidator( + "json", + z.object({ + title: z.string().optional(), + message: z.string(), + variant: z.enum(["info", "success", "warning", "error"]), + }), + ), + async (c) => c.json(await callTui(c)), + ) + .route("/tui/control", TuiRoute) + .put( + "/auth/:id", + describeRoute({ + description: "Set authentication credentials", + operationId: "auth.set", + responses: { + 200: { + description: "Successfully set authentication credentials", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...ERRORS, + }, + }), + zValidator( + "param", + z.object({ + id: z.string(), + }), + ), + zValidator("json", Auth.Info), + async (c) => { + const id = c.req.valid("param").id + const info = c.req.valid("json") + await Auth.set(id, info) + return c.json(true) + }, + ) export async function openapi() { - const a = app() - const result = await generateSpecs(a, { + const result = await generateSpecs(App, { documentation: { info: { title: "opencode", @@ -1326,7 +1323,7 @@ export namespace Server { port: opts.port, hostname: opts.hostname, idleTimeout: 0, - fetch: app().fetch, + fetch: App.fetch, }) return server } diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 4474bf1c..d6848e95 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -20,7 +20,6 @@ import PROMPT_INITIALIZE from "../session/prompt/initialize.txt" import PROMPT_PLAN from "../session/prompt/plan.txt" import BUILD_SWITCH from "../session/prompt/build-switch.txt" -import { App } from "../app/app" import { Bus } from "../bus" import { Config } from "../config/config" import { Flag } from "../flag/flag" @@ -43,6 +42,8 @@ import { ReadTool } from "../tool/read" import { mergeDeep, pipe, splitWhen } from "remeda" import { ToolRegistry } from "../tool/registry" import { Plugin } from "../plugin" +import { Project } from "../project/project" +import { Instance } from "../project/instance" import { Agent } from "../agent/agent" import { Permission } from "../permission" import { Wildcard } from "../util/wildcard" @@ -70,6 +71,8 @@ export namespace Session { export const Info = z .object({ id: Identifier.schema("session"), + projectID: z.string(), + directory: z.string(), parentID: Identifier.schema("session").optional(), share: z .object({ @@ -134,11 +137,8 @@ export namespace Session { ), } - const state = App.state( - "session", + const state = Instance.state( () => { - const sessions = new Map() - const messages = new Map() const pending = new Map() const autoCompacting = new Map() const queued = new Map< @@ -153,8 +153,6 @@ export namespace Session { >() return { - sessions, - messages, pending, autoCompacting, queued, @@ -168,19 +166,28 @@ export namespace Session { ) export async function create(parentID?: string, title?: string) { - const result: Info = { - id: Identifier.descending("session"), - version: Installation.VERSION, + return createNext({ parentID, - title: title ?? createDefaultTitle(!!parentID), + directory: Instance.directory, + title, + }) + } + + export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) { + const result: Info = { + id: Identifier.descending("session", input.id), + version: Installation.VERSION, + projectID: Instance.project.id, + directory: input.directory, + parentID: input.parentID, + title: input.title ?? createDefaultTitle(!!input.parentID), time: { created: Date.now(), updated: Date.now(), }, } log.info("created", result) - state().sessions.set(result.id, result) - await Storage.writeJSON("session/info/" + result.id, result) + await Storage.write(["session", Instance.project.id, result.id], result) const cfg = await Config.get() if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) share(result.id) @@ -199,17 +206,12 @@ export namespace Session { } export async function get(id: string) { - const result = state().sessions.get(id) - if (result) { - return result - } - const read = await Storage.readJSON("session/info/" + id) - state().sessions.set(id, read) + const read = await Storage.read(["session", Instance.project.id, id]) return read as Info } export async function getShare(id: string) { - return Storage.readJSON("session/share/" + id) + return Storage.read(["share", id]) } export async function share(id: string) { @@ -226,7 +228,7 @@ export namespace Session { url: share.url, } }) - await Storage.writeJSON("session/share/" + id, share) + await Storage.write(["share", id], share) await Share.sync("session/info/" + id, session) for (const msg of await messages(id)) { await Share.sync("session/message/" + id + "/" + msg.info.id, msg.info) @@ -240,7 +242,7 @@ export namespace Session { export async function unshare(id: string) { const share = await getShare(id) if (!share) return - await Storage.remove("session/share/" + id) + await Storage.remove(["share", id]) await update(id, (draft) => { draft.share = undefined }) @@ -248,17 +250,15 @@ export namespace Session { } export async function update(id: string, editor: (session: Info) => void) { - const { sessions } = state() - const session = await get(id) - if (!session) return - editor(session) - session.time.updated = Date.now() - sessions.set(id, session) - await Storage.writeJSON("session/info/" + id, session) - Bus.publish(Event.Updated, { - info: session, + const project = Instance.project + const result = await Storage.update(["session", project.id, id], (draft) => { + editor(draft) + draft.time.updated = Date.now() }) - return session + Bus.publish(Event.Updated, { + info: result, + }) + return result } export async function messages(sessionID: string) { @@ -266,11 +266,11 @@ export namespace Session { info: MessageV2.Info parts: MessageV2.Part[] }[] - for (const p of await Storage.list("session/message/" + sessionID)) { - const read = await Storage.readJSON(p) + for (const p of await Storage.list(["message", sessionID])) { + const read = await Storage.read(p) result.push({ info: read, - parts: await getParts(sessionID, read.id), + parts: await getParts(read.id), }) } result.sort((a, b) => (a.info.id > b.info.id ? 1 : -1)) @@ -279,15 +279,15 @@ export namespace Session { export async function getMessage(sessionID: string, messageID: string) { return { - info: await Storage.readJSON("session/message/" + sessionID + "/" + messageID), - parts: await getParts(sessionID, messageID), + info: await Storage.read(["message", sessionID, messageID]), + parts: await getParts(messageID), } } - export async function getParts(sessionID: string, messageID: string) { + export async function getParts(messageID: string) { const result = [] as MessageV2.Part[] - for (const item of await Storage.list("session/part/" + sessionID + "/" + messageID)) { - const read = await Storage.readJSON(item) + for (const item of await Storage.list(["part", messageID])) { + const read = await Storage.read(item) result.push(read) } result.sort((a, b) => (a.id > b.id ? 1 : -1)) @@ -295,17 +295,17 @@ export namespace Session { } export async function* list() { - for (const item of await Storage.list("session/info")) { - const sessionID = path.basename(item, ".json") - yield get(sessionID) + const project = Instance.project + for (const item of await Storage.list(["session", project.id])) { + yield Storage.read(item) } } export async function children(parentID: string) { + const project = Instance.project const result = [] as Session.Info[] - for (const item of await Storage.list("session/info")) { - const sessionID = path.basename(item, ".json") - const session = await get(sessionID) + for (const item of await Storage.list(["session", project.id])) { + const session = await Storage.read(item) if (session.parentID !== parentID) continue result.push(session) } @@ -324,6 +324,7 @@ export namespace Session { } export async function remove(sessionID: string, emitEvent = true) { + const project = Instance.project try { abort(sessionID) const session = await get(sessionID) @@ -331,10 +332,13 @@ export namespace Session { await remove(child.id, false) } await unshare(sessionID).catch(() => {}) - await Storage.remove(`session/info/${sessionID}`).catch(() => {}) - await Storage.removeDir(`session/message/${sessionID}/`).catch(() => {}) - state().sessions.delete(sessionID) - state().messages.delete(sessionID) + for (const msg of await Storage.list(["message", sessionID])) { + for (const part of await Storage.list(["part", msg.at(-1)!])) { + await Storage.remove(part) + } + await Storage.remove(msg) + } + await Storage.remove(["session", project.id, sessionID]) if (emitEvent) { Bus.publish(Event.Deleted, { info: session, @@ -346,25 +350,29 @@ export namespace Session { } async function updateMessage(msg: MessageV2.Info) { - await Storage.writeJSON("session/message/" + msg.sessionID + "/" + msg.id, msg) + await Storage.write(["message", msg.sessionID, msg.id], msg) Bus.publish(MessageV2.Event.Updated, { info: msg, }) } async function updatePart(part: MessageV2.Part) { - await Storage.writeJSON(["session", "part", part.sessionID, part.messageID, part.id].join("/"), part) + await Storage.write(["part", part.messageID, part.id], part) Bus.publish(MessageV2.Event.PartUpdated, { part, }) return part } - export const ChatInput = z.object({ + export const PromptInput = z.object({ sessionID: Identifier.schema("session"), messageID: Identifier.schema("message").optional(), - providerID: z.string(), - modelID: z.string(), + model: z + .object({ + providerID: z.string(), + modelID: z.string(), + }) + .optional(), agent: z.string().optional(), system: z.string().optional(), tools: z.record(z.boolean()).optional(), @@ -403,10 +411,10 @@ export namespace Session { ]), ), }) - export type ChatInput = z.infer + export type ChatInput = z.infer - export async function chat( - input: z.infer, + export async function prompt( + input: z.infer, ): Promise<{ info: MessageV2.Assistant; parts: MessageV2.Part[] }> { const l = log.clone().tag("session", input.sessionID) l.info("chatting") @@ -421,7 +429,7 @@ export namespace Session { const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID) msgs = preserve for (const msg of remove) { - await Storage.remove(`session/message/${input.sessionID}/${msg.info.id}`) + await Storage.remove(["message", input.sessionID, msg.info.id]) await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: msg.info.id }) } const last = preserve.at(-1) @@ -430,7 +438,7 @@ export namespace Session { const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID) last.parts = preserveParts for (const part of removeParts) { - await Storage.remove(`session/part/${input.sessionID}/${last.info.id}/${part.id}`) + await Storage.remove(["part", last.info.id, part.id]) await Bus.publish(MessageV2.Event.PartRemoved, { sessionID: input.sessionID, messageID: last.info.id, @@ -451,7 +459,6 @@ export namespace Session { }, } - const app = App.info() const userParts = await Promise.all( input.parts.map(async (part): Promise => { if (part.type === "file") { @@ -649,7 +656,16 @@ export namespace Session { }) } - const model = await Provider.getModel(input.providerID, input.modelID) + const agent = await Agent.get(inputAgent) + const model = await (async () => { + if (input.model) { + return input.model + } + if (agent.model) { + return agent.model + } + return Provider.defaultModel() + })().then((x) => Provider.getModel(x.providerID, x.modelID)) let msgs = await messages(input.sessionID) const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant @@ -664,10 +680,10 @@ export namespace Session { await summarize({ sessionID: input.sessionID, - providerID: input.providerID, - modelID: input.modelID, + providerID: model.providerID, + modelID: model.info.id, }) - return chat(input) + return prompt(input) } } using abort = lock(input.sessionID) @@ -676,17 +692,17 @@ export namespace Session { if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id) if (msgs.filter((m) => m.info.role === "user").length === 1 && !session.parentID && isDefaultTitle(session.title)) { - const small = (await Provider.getSmallModel(input.providerID)) ?? model + const small = (await Provider.getSmallModel(model.providerID)) ?? model generateText({ maxOutputTokens: small.info.reasoning ? 1024 : 20, providerOptions: { - [input.providerID]: { + [model.providerID]: { ...small.info.options, - ...ProviderTransform.options(input.providerID, small.info.id, input.sessionID), + ...ProviderTransform.options(small.providerID, small.modelID, input.sessionID), }, }, messages: [ - ...SystemPrompt.title(input.providerID).map( + ...SystemPrompt.title(model.providerID).map( (x): ModelMessage => ({ role: "system", content: x, @@ -721,7 +737,6 @@ export namespace Session { }) } - const agent = await Agent.get(inputAgent) if (agent.name === "plan") { msgs.at(-1)?.parts.push({ id: Identifier.ascending("part"), @@ -744,12 +759,12 @@ export namespace Session { synthetic: true, }) } - let system = SystemPrompt.header(input.providerID) + let system = SystemPrompt.header(model.providerID) system.push( ...(() => { if (input.system) return [input.system] if (agent.prompt) return [agent.prompt] - return SystemPrompt.provider(input.modelID) + return SystemPrompt.provider(model.modelID) })(), ) system.push(...(await SystemPrompt.environment())) @@ -764,8 +779,8 @@ export namespace Session { system, mode: inputAgent, path: { - cwd: app.path.cwd, - root: app.path.root, + cwd: Instance.directory, + root: Instance.worktree, }, cost: 0, tokens: { @@ -774,8 +789,8 @@ export namespace Session { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: input.modelID, - providerID: input.providerID, + modelID: model.modelID, + providerID: model.providerID, time: { created: Date.now(), }, @@ -784,7 +799,7 @@ export namespace Session { await updateMessage(assistantMsg) await using _ = defer(async () => { if (assistantMsg.time.completed) return - await Storage.remove(`session/message/${input.sessionID}/${assistantMsg.id}`) + await Storage.remove(["session", "message", input.sessionID, assistantMsg.id]) await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: assistantMsg.id }) }) const tools: Record = {} @@ -793,10 +808,10 @@ export namespace Session { const enabledTools = pipe( agent.tools, - mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)), + mergeDeep(await ToolRegistry.enabled(model.providerID, model.modelID, agent)), mergeDeep(input.tools ?? {}), ) - for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) { + for (const item of await ToolRegistry.tools(model.providerID, model.modelID)) { if (Wildcard.all(item.id, enabledTools) === false) continue tools[item.id] = tool({ id: item.id as any, @@ -906,16 +921,16 @@ export namespace Session { "chat.params", { model: model.info, - provider: await Provider.getProvider(input.providerID), + provider: await Provider.getProvider(model.providerID), message: userMsg, }, { temperature: model.info.temperature - ? (agent.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID)) + ? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID)) : undefined, - topP: agent.topP ?? ProviderTransform.topP(input.providerID, input.modelID), + topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID), options: { - ...ProviderTransform.options(input.providerID, input.modelID, input.sessionID), + ...ProviderTransform.options(model.providerID, model.modelID, input.sessionID), ...model.info.options, ...agent.options, }, @@ -949,8 +964,8 @@ export namespace Session { role: "assistant", system, path: { - cwd: app.path.cwd, - root: app.path.root, + cwd: Instance.directory, + root: Instance.worktree, }, cost: 0, tokens: { @@ -959,8 +974,8 @@ export namespace Session { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: input.modelID, - providerID: input.providerID, + modelID: model.modelID, + providerID: model.providerID, mode: inputAgent, time: { created: Date.now(), @@ -984,7 +999,7 @@ export namespace Session { } }, headers: - input.providerID === "opencode" + model.providerID === "opencode" ? { "x-opencode-session": input.sessionID, "x-opencode-request": userMsg.id, @@ -1007,7 +1022,7 @@ export namespace Session { return false }, providerOptions: { - [input.providerID]: params.options, + [model.providerID]: params.options, }, temperature: params.temperature, topP: params.topP, @@ -1028,7 +1043,7 @@ export namespace Session { async transformParams(args) { if (args.type === "stream") { // @ts-expect-error - args.params.prompt = ProviderTransform.message(args.params.prompt, input.providerID, input.modelID) + args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID) } return args.params }, @@ -1041,7 +1056,7 @@ export namespace Session { const unprocessed = queued.find((x) => !x.processed) if (unprocessed) { unprocessed.processed = true - return chat(unprocessed.input) + return prompt(unprocessed.input) } for (const item of queued) { item.callback(result) @@ -1084,8 +1099,8 @@ export namespace Session { mode: input.agent, cost: 0, path: { - cwd: App.info().path.cwd, - root: App.info().path.root, + cwd: Instance.directory, + root: Instance.worktree, }, time: { created: Date.now(), @@ -1119,7 +1134,6 @@ export namespace Session { }, } await updatePart(part) - const app = App.info() const shell = process.env["SHELL"] ?? "bash" const shellName = path.basename(shell) @@ -1139,7 +1153,7 @@ export namespace Session { const args = isFishOrNu ? ["-c", script] : ["-c", "-l", script] const proc = spawn(shell, args, { - cwd: app.path.cwd, + cwd: Instance.directory, signal: abort.signal, detached: true, stdio: ["ignore", "pipe", "pipe"], @@ -1218,16 +1232,9 @@ export namespace Session { const fileRegex = /@([^\s]+)/g export async function command(input: CommandInput) { + log.info("command", input) const command = await Command.get(input.command) const agent = command.agent ?? input.agent ?? "build" - const fmtModel = (model: { providerID: string; modelID: string }) => `${model.providerID}/${model.modelID}` - - const model = - command.model ?? - (command.agent && (await Agent.get(command.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ?? - input.model ?? - (input.agent && (await Agent.get(input.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ?? - fmtModel(await Provider.defaultModel()) let template = command.template.replace("$ARGUMENTS", input.arguments) @@ -1257,13 +1264,11 @@ export namespace Session { }, ] as ChatInput["parts"] - const app = App.info() - for (const match of fileMatches) { const filename = match[1] const filepath = filename.startsWith("~/") ? path.join(os.homedir(), filename.slice(2)) - : path.join(app.path.cwd, filename) + : path.join(Instance.worktree, filename) parts.push({ type: "file", @@ -1273,10 +1278,18 @@ export namespace Session { }) } - return chat({ + return prompt({ sessionID: input.sessionID, messageID: input.messageID, - ...Provider.parseModel(model!), + model: (() => { + if (input.model) { + return Provider.parseModel(input.model) + } + if (command.model) { + return Provider.parseModel(command.model) + } + return undefined + })(), agent, parts, }) @@ -1550,7 +1563,7 @@ export namespace Session { error: assistantMsg.error, }) } - const p = await getParts(assistantMsg.sessionID, assistantMsg.id) + const p = await getParts(assistantMsg.id) for (const part of p) { if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") { updatePart({ @@ -1642,9 +1655,8 @@ export namespace Session { const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id) const model = await Provider.getModel(input.providerID, input.modelID) - const app = App.info() const system = [ - ...SystemPrompt.summarize(input.providerID), + ...SystemPrompt.summarize(model.providerID), ...(await SystemPrompt.environment()), ...(await SystemPrompt.custom()), ] @@ -1656,13 +1668,13 @@ export namespace Session { system, mode: "build", path: { - cwd: app.path.cwd, - root: app.path.root, + cwd: Instance.directory, + root: Instance.worktree, }, summary: true, cost: 0, modelID: input.modelID, - providerID: input.providerID, + providerID: model.providerID, tokens: { input: 0, output: 0, @@ -1771,20 +1783,21 @@ export namespace Session { providerID: string messageID: string }) { - const app = App.info() - await Session.chat({ + await Session.prompt({ sessionID: input.sessionID, messageID: input.messageID, - providerID: input.providerID, - modelID: input.modelID, + model: { + providerID: input.providerID, + modelID: input.modelID, + }, parts: [ { id: Identifier.ascending("part"), type: "text", - text: PROMPT_INITIALIZE.replace("${path}", app.path.root), + text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree), }, ], }) - await App.initialize() + await Project.setInitialized(Instance.project.id) } } diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 15bb40c8..41119c4f 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -1,8 +1,9 @@ -import { App } from "../app/app" import { Ripgrep } from "../file/ripgrep" import { Global } from "../global" import { Filesystem } from "../util/filesystem" import { Config } from "../config/config" + +import { Instance } from "../project/instance" import path from "path" import os from "os" @@ -30,21 +31,21 @@ export namespace SystemPrompt { } export async function environment() { - const app = App.info() + const project = Instance.project return [ [ `Here is some useful information about the environment you are running in:`, ``, - ` Working directory: ${app.path.cwd}`, - ` Is directory a git repo: ${app.git ? "yes" : "no"}`, + ` Working directory: ${Instance.directory}`, + ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`, ` Platform: ${process.platform}`, ` Today's date: ${new Date().toDateString()}`, ``, ``, ` ${ - app.git + project.vcs === "git" ? await Ripgrep.tree({ - cwd: app.path.cwd, + cwd: Instance.directory, limit: 200, }) : "" @@ -65,12 +66,11 @@ export namespace SystemPrompt { ] export async function custom() { - const { cwd, root } = App.info().path const config = await Config.get() const paths = new Set() for (const localRuleFile of LOCAL_RULE_FILES) { - const matches = await Filesystem.findUp(localRuleFile, cwd, root) + const matches = await Filesystem.findUp(localRuleFile, Instance.directory, Instance.worktree) if (matches.length > 0) { matches.forEach((path) => paths.add(path)) break @@ -99,7 +99,7 @@ export namespace SystemPrompt { }), ).catch(() => []) } else { - matches = await Filesystem.globUp(instruction, cwd, root).catch(() => []) + matches = await Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => []) } matches.forEach((path) => paths.add(path)) } diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts index 2996e4d9..9df862d5 100644 --- a/packages/opencode/src/share/share.ts +++ b/packages/opencode/src/share/share.ts @@ -1,7 +1,7 @@ import { Bus } from "../bus" import { Installation } from "../installation" import { Session } from "../session" -import { Storage } from "../storage/storage" +import { MessageV2 } from "../session/message-v2" import { Log } from "../util/log" export namespace Share { @@ -46,8 +46,22 @@ export namespace Share { } export function init() { - Bus.subscribe(Storage.Event.Write, async (payload) => { - await sync(payload.properties.key, payload.properties.content) + Bus.subscribe(Session.Event.Updated, async (evt) => { + await sync("session/info/" + evt.properties.info.id, evt.properties.info) + }) + Bus.subscribe(MessageV2.Event.Updated, async (evt) => { + await sync("session/message/" + evt.properties.info.sessionID + "/" + evt.properties.info.id, evt.properties.info) + }) + Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => { + await sync( + "session/part/" + + evt.properties.part.sessionID + + "/" + + evt.properties.part.messageID + + "/" + + evt.properties.part.id, + evt.properties.part, + ) }) } diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index d69fb69d..c26ac55b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -1,4 +1,3 @@ -import { App } from "../app/app" import { $ } from "bun" import path from "path" import fs from "fs/promises" @@ -6,6 +5,7 @@ import { Log } from "../util/log" import { Global } from "../global" import { z } from "zod" import { Config } from "../config/config" +import { Instance } from "../project/instance" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) @@ -25,8 +25,7 @@ export namespace Snapshot { } export async function track() { - const app = App.info() - if (!app.git) return + if (Instance.project.vcs !== "git") return const cfg = await Config.get() if (cfg.snapshot === false) return const git = gitdir() @@ -35,15 +34,15 @@ export namespace Snapshot { .env({ ...process.env, GIT_DIR: git, - GIT_WORK_TREE: app.path.root, + GIT_WORK_TREE: Instance.worktree, }) .quiet() .nothrow() log.info("initialized") } - await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow() - const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).nothrow().text() - log.info("tracking", { hash, cwd: app.path.cwd, git }) + await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() + const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Instance.directory).nothrow().text() + log.info("tracking", { hash, cwd: Instance.directory, git }) return hash.trim() } @@ -54,10 +53,9 @@ export namespace Snapshot { export type Patch = z.infer export async function patch(hash: string): Promise { - const app = App.info() const git = gitdir() - await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow() - const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(app.path.cwd).text() + await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() + const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(Instance.directory).text() return { hash, files: files @@ -65,17 +63,16 @@ export namespace Snapshot { .split("\n") .map((x) => x.trim()) .filter(Boolean) - .map((x) => path.join(app.path.root, x)), + .map((x) => path.join(Instance.worktree, x)), } } export async function restore(snapshot: string) { log.info("restore", { commit: snapshot }) - const app = App.info() const git = gitdir() await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f` .quiet() - .cwd(app.path.root) + .cwd(Instance.worktree) } export async function revert(patches: Patch[]) { @@ -87,7 +84,7 @@ export namespace Snapshot { log.info("reverting", { file, hash: item.hash }) const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}` .quiet() - .cwd(App.info().path.root) + .cwd(Instance.worktree) .nothrow() if (result.exitCode !== 0) { log.info("file not found in history, deleting", { file }) @@ -99,14 +96,13 @@ export namespace Snapshot { } export async function diff(hash: string) { - const app = App.info() const git = gitdir() - const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(app.path.root).text() + const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Instance.worktree).text() return result.trim() } function gitdir() { - const app = App.info() - return path.join(app.path.data, "snapshots") + const project = Instance.project + return path.join(Global.Path.data, "snapshot", project.id) } } diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index f4efbfdf..d7ce0495 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -1,99 +1,113 @@ import { Log } from "../util/log" -import { App } from "../app/app" -import { Bus } from "../bus" import path from "path" -import z from "zod" import fs from "fs/promises" -import { MessageV2 } from "../session/message-v2" -import { Identifier } from "../id/id" +import { Global } from "../global" +import { lazy } from "../util/lazy" +import { Lock } from "../util/lock" +import { $ } from "bun" export namespace Storage { const log = Log.create({ service: "storage" }) - export const Event = { - Write: Bus.event("storage.write", z.object({ key: z.string(), content: z.any() })), - } - type Migration = (dir: string) => Promise const MIGRATIONS: Migration[] = [ - async (dir: string) => { - try { - const files = new Bun.Glob("session/message/*/*.json").scanSync({ - cwd: dir, - absolute: true, - }) - for (const file of files) { - const content = await Bun.file(file).json() - if (!content.metadata) continue - log.info("migrating to v2 message", { file }) - try { - const result = MessageV2.fromV1(content) - await Bun.write( - file, - JSON.stringify( - { - ...result.info, - parts: result.parts, - }, - null, - 2, - ), + async (dir) => { + const project = path.resolve(dir, "../project") + for await (const projectDir of new Bun.Glob("*").scan({ cwd: project, onlyFiles: false })) { + log.info(`migrating project ${projectDir}`) + let projectID = projectDir + const fullProjectDir = path.join(project, projectDir) + let worktree = "/" + + if (projectID !== "global") { + for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({ + cwd: path.join(project, projectDir), + absolute: true, + })) { + const json = await Bun.file(msgFile).json() + worktree = json.path?.root + if (worktree) break + } + if (!worktree) continue + if (!(await fs.exists(worktree))) continue + const [id] = await $`git rev-list --max-parents=0 --all` + .quiet() + .nothrow() + .cwd(worktree) + .text() + .then((x) => + x + .split("\n") + .filter(Boolean) + .map((x) => x.trim()) + .toSorted(), ) - } catch (e) { - await fs.rename(file, file.replace("storage", "broken")) + if (!id) continue + projectID = id + + await Bun.write( + path.join(dir, "project", projectID + ".json"), + JSON.stringify({ + id, + vcs: "git", + worktree, + time: { + created: Date.now(), + initialized: Date.now(), + }, + }), + ) + + log.info(`migrating sessions for project ${projectID}`) + for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ + cwd: fullProjectDir, + absolute: true, + })) { + const dest = path.join(dir, "session", projectID, path.basename(sessionFile)) + log.info("copying", { + sessionFile, + dest, + }) + const session = await Bun.file(sessionFile).json() + await Bun.write(dest, JSON.stringify(session)) + log.info(`migrating messages for session ${session.id}`) + for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ + cwd: fullProjectDir, + absolute: true, + })) { + const dest = path.join(dir, "message", session.id, path.basename(msgFile)) + log.info("copying", { + msgFile, + dest, + }) + const message = await Bun.file(msgFile).json() + await Bun.write(dest, JSON.stringify(message)) + + log.info(`migrating parts for message ${message.id}`) + for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( + { + cwd: fullProjectDir, + absolute: true, + }, + )) { + const dest = path.join(dir, "part", message.id, path.basename(partFile)) + const part = await Bun.file(partFile).json() + log.info("copying", { + partFile, + dest, + }) + await Bun.write(dest, JSON.stringify(part)) + } + } } } - } catch {} - }, - async (dir: string) => { - const files = new Bun.Glob("session/message/*/*.json").scanSync({ - cwd: dir, - absolute: true, - }) - for (const file of files) { - try { - const { parts, ...info } = await Bun.file(file).json() - if (!parts) continue - for (const part of parts) { - const id = Identifier.ascending("part") - await Bun.write( - [dir, "session", "part", info.sessionID, info.id, id + ".json"].join("/"), - JSON.stringify({ - ...part, - id, - sessionID: info.sessionID, - messageID: info.id, - ...(part.type === "tool" ? { callID: part.id } : {}), - }), - ) - } - await Bun.write(file, JSON.stringify(info, null, 2)) - } catch (e) {} - } - }, - async (dir: string) => { - const files = new Bun.Glob("session/message/*/*.json").scanSync({ - cwd: dir, - absolute: true, - }) - for (const file of files) { - try { - const content = await Bun.file(file).json() - if (content.role === "assistant" && !content.mode) { - log.info("adding mode field to message", { file }) - content.mode = "build" - await Bun.write(file, JSON.stringify(content, null, 2)) - } - } catch (e) {} } }, ] - const state = App.state("storage", async () => { - const app = App.info() - const dir = path.normalize(path.join(app.path.data, "storage")) - await fs.mkdir(dir, { recursive: true }) + const state = lazy(async () => { + const dir = path.join(Global.Path.data, "storage") const migration = await Bun.file(path.join(dir, "migration")) .json() .then((x) => parseInt(x)) @@ -109,43 +123,46 @@ export namespace Storage { } }) - export async function remove(key: string) { + export async function remove(key: string[]) { const dir = await state().then((x) => x.dir) - const target = path.join(dir, key + ".json") + const target = path.join(dir, ...key) + ".json" await fs.unlink(target).catch(() => {}) } - export async function removeDir(key: string) { + export async function read(key: string[]) { const dir = await state().then((x) => x.dir) - const target = path.join(dir, key) - await fs.rm(target, { recursive: true, force: true }).catch(() => {}) + const target = path.join(dir, ...key) + ".json" + using _ = await Lock.read(target) + return Bun.file(target).json() as Promise } - export async function readJSON(key: string) { + export async function update(key: string[], fn: (draft: T) => void) { const dir = await state().then((x) => x.dir) - return Bun.file(path.join(dir, key + ".json")).json() as Promise + const target = path.join(dir, ...key) + ".json" + using _ = await Lock.write("storage") + const content = await Bun.file(target).json() + fn(content) + await Bun.write(target, JSON.stringify(content, null, 2)) + return content as T } - export async function writeJSON(key: string, content: T) { + export async function write(key: string[], content: T) { const dir = await state().then((x) => x.dir) - const target = path.join(dir, key + ".json") - const tmp = target + Date.now() + ".tmp" - await Bun.write(tmp, JSON.stringify(content, null, 2)) - await fs.rename(tmp, target).catch(() => {}) - await fs.unlink(tmp).catch(() => {}) - Bus.publish(Event.Write, { key, content }) + const target = path.join(dir, ...key) + ".json" + using _ = await Lock.write("storage") + await Bun.write(target, JSON.stringify(content, null, 2)) } const glob = new Bun.Glob("**/*") - export async function list(prefix: string) { + export async function list(prefix: string[]) { const dir = await state().then((x) => x.dir) try { const result = await Array.fromAsync( glob.scan({ - cwd: path.join(dir, prefix), + cwd: path.join(dir, ...prefix), onlyFiles: true, }), - ).then((items) => items.map((item) => path.join(prefix, item.slice(0, -5)))) + ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) result.sort() return result } catch { diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index da1559c5..a7b6ec24 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -3,13 +3,13 @@ import { exec } from "child_process" import { Tool } from "./tool" import DESCRIPTION from "./bash.txt" -import { App } from "../app/app" import { Permission } from "../permission" import { Filesystem } from "../util/filesystem" import { lazy } from "../util/lazy" import { Log } from "../util/log" import { Wildcard } from "../util/wildcard" import { $ } from "bun" +import { Instance } from "../project/instance" import { Agent } from "../agent/agent" const MAX_OUTPUT_LENGTH = 30_000 @@ -56,7 +56,6 @@ export const BashTool = Tool.define("bash", { }), async execute(params, ctx) { const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) - const app = App.info() const tree = await parser().then((p) => p.parse(params.command)) const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash) @@ -88,9 +87,9 @@ export const BashTool = Tool.define("bash", { .text() .then((x) => x.trim()) log.info("resolved path", { arg, resolved }) - if (resolved && !Filesystem.contains(app.path.cwd, resolved)) { + if (resolved && !Filesystem.contains(Instance.directory, resolved)) { throw new Error( - `This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`, + `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`, ) } } @@ -123,7 +122,7 @@ export const BashTool = Tool.define("bash", { } const process = exec(params.command, { - cwd: app.path.cwd, + cwd: Instance.directory, signal: ctx.abort, timeout, }) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 8be41ecf..928188c0 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -10,11 +10,11 @@ import { LSP } from "../lsp" import { createTwoFilesPatch } from "diff" import { Permission } from "../permission" import DESCRIPTION from "./edit.txt" -import { App } from "../app/app" import { File } from "../file" import { Bus } from "../bus" import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" +import { Instance } from "../project/instance" import { Agent } from "../agent/agent" export const EditTool = Tool.define("edit", { @@ -34,9 +34,8 @@ export const EditTool = Tool.define("edit", { throw new Error("oldString and newString must be different") } - const app = App.info() - const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) - if (!Filesystem.contains(app.path.cwd, filePath)) { + const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) + if (!Filesystem.contains(Instance.directory, filePath)) { throw new Error(`File ${filePath} is not in the current working directory`) } @@ -123,7 +122,7 @@ export const EditTool = Tool.define("edit", { diagnostics, diff, }, - title: `${path.relative(app.path.root, filePath)}`, + title: `${path.relative(Instance.worktree, filePath)}`, output, } }, diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 777c0693..9534f0af 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -1,9 +1,9 @@ import { z } from "zod" import path from "path" import { Tool } from "./tool" -import { App } from "../app/app" import DESCRIPTION from "./glob.txt" import { Ripgrep } from "../file/ripgrep" +import { Instance } from "../project/instance" export const GlobTool = Tool.define("glob", { description: DESCRIPTION, @@ -17,9 +17,8 @@ export const GlobTool = Tool.define("glob", { ), }), async execute(params) { - const app = App.info() - let search = params.path ?? app.path.cwd - search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search) + let search = params.path ?? Instance.directory + search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search) const limit = 100 const files = [] @@ -55,7 +54,7 @@ export const GlobTool = Tool.define("glob", { } return { - title: path.relative(app.path.root, search), + title: path.relative(Instance.worktree, search), metadata: { count: files.length, truncated, diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index cc0a290d..a8a42a82 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,9 +1,9 @@ import { z } from "zod" import { Tool } from "./tool" -import { App } from "../app/app" import { Ripgrep } from "../file/ripgrep" import DESCRIPTION from "./grep.txt" +import { Instance } from "../project/instance" export const GrepTool = Tool.define("grep", { description: DESCRIPTION, @@ -17,8 +17,7 @@ export const GrepTool = Tool.define("grep", { throw new Error("pattern is required") } - const app = App.info() - const searchPath = params.path || app.path.cwd + const searchPath = params.path || Instance.directory const rgPath = await Ripgrep.filepath() const args = ["-n", params.pattern] diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index e0f7fbbf..00dbcbd3 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -1,8 +1,8 @@ import { z } from "zod" import { Tool } from "./tool" -import { App } from "../app/app" import * as path from "path" import DESCRIPTION from "./ls.txt" +import { Instance } from "../project/instance" export const IGNORE_PATTERNS = [ "node_modules/", @@ -40,8 +40,7 @@ export const ListTool = Tool.define("list", { ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(), }), async execute(params) { - const app = App.info() - const searchPath = path.resolve(app.path.cwd, params.path || ".") + const searchPath = path.resolve(Instance.directory, params.path || ".") const glob = new Bun.Glob("**/*") const files = [] @@ -102,7 +101,7 @@ export const ListTool = Tool.define("list", { const output = `${searchPath}/\n` + renderDir(".", 0) return { - title: path.relative(app.path.root, searchPath), + title: path.relative(Instance.worktree, searchPath), metadata: { count: files.length, truncated: files.length >= LIMIT, diff --git a/packages/opencode/src/tool/lsp-diagnostics.ts b/packages/opencode/src/tool/lsp-diagnostics.ts index 19415d5a..b69e8485 100644 --- a/packages/opencode/src/tool/lsp-diagnostics.ts +++ b/packages/opencode/src/tool/lsp-diagnostics.ts @@ -2,8 +2,8 @@ import { z } from "zod" import { Tool } from "./tool" import path from "path" import { LSP } from "../lsp" -import { App } from "../app/app" import DESCRIPTION from "./lsp-diagnostics.txt" +import { Instance } from "../project/instance" export const LspDiagnosticTool = Tool.define("lsp_diagnostics", { description: DESCRIPTION, @@ -11,13 +11,12 @@ export const LspDiagnosticTool = Tool.define("lsp_diagnostics", { path: z.string().describe("The path to the file to get diagnostics."), }), execute: async (args) => { - const app = App.info() - const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path) + const normalized = path.isAbsolute(args.path) ? args.path : path.join(Instance.directory, args.path) await LSP.touchFile(normalized, true) const diagnostics = await LSP.diagnostics() const file = diagnostics[normalized] return { - title: path.relative(app.path.root, normalized), + title: path.relative(Instance.worktree, normalized), metadata: { diagnostics, }, diff --git a/packages/opencode/src/tool/lsp-hover.ts b/packages/opencode/src/tool/lsp-hover.ts index b642dd58..b33a4e80 100644 --- a/packages/opencode/src/tool/lsp-hover.ts +++ b/packages/opencode/src/tool/lsp-hover.ts @@ -2,8 +2,8 @@ import { z } from "zod" import { Tool } from "./tool" import path from "path" import { LSP } from "../lsp" -import { App } from "../app/app" import DESCRIPTION from "./lsp-hover.txt" +import { Instance } from "../project/instance" export const LspHoverTool = Tool.define("lsp_hover", { description: DESCRIPTION, @@ -13,8 +13,7 @@ export const LspHoverTool = Tool.define("lsp_hover", { character: z.number().describe("The character number to get diagnostics."), }), execute: async (args) => { - const app = App.info() - const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file) + const file = path.isAbsolute(args.file) ? args.file : path.join(Instance.directory, args.file) await LSP.touchFile(file, true) const result = await LSP.hover({ ...args, @@ -22,7 +21,7 @@ export const LspHoverTool = Tool.define("lsp_hover", { }) return { - title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character, + title: path.relative(Instance.worktree, file) + ":" + args.line + ":" + args.character, metadata: { result, }, diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts index 432039d4..8ae81ab9 100644 --- a/packages/opencode/src/tool/multiedit.ts +++ b/packages/opencode/src/tool/multiedit.ts @@ -3,7 +3,7 @@ import { Tool } from "./tool" import { EditTool } from "./edit" import DESCRIPTION from "./multiedit.txt" import path from "path" -import { App } from "../app/app" +import { Instance } from "../project/instance" export const MultiEditTool = Tool.define("multiedit", { description: DESCRIPTION, @@ -35,9 +35,8 @@ export const MultiEditTool = Tool.define("multiedit", { ) results.push(result) } - const app = App.info() return { - title: path.relative(app.path.root, params.filePath), + title: path.relative(Instance.worktree, params.filePath), metadata: { results: results.map((r) => r.metadata), }, diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 8ebbb7fd..2aaaf7a4 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -5,8 +5,8 @@ import { Tool } from "./tool" import { LSP } from "../lsp" import { FileTime } from "../file/time" import DESCRIPTION from "./read.txt" -import { App } from "../app/app" import { Filesystem } from "../util/filesystem" +import { Instance } from "../project/instance" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -23,8 +23,7 @@ export const ReadTool = Tool.define("read", { if (!path.isAbsolute(filepath)) { filepath = path.join(process.cwd(), filepath) } - const app = App.info() - if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(app.path.cwd, filepath)) { + if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) { throw new Error(`File ${filepath} is not in the current working directory`) } @@ -77,7 +76,7 @@ export const ReadTool = Tool.define("read", { FileTime.read(ctx.sessionID, filepath) return { - title: path.relative(App.info().path.root, filepath), + title: path.relative(Instance.worktree, filepath), output, metadata: { preview, diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index a959611e..5c226f13 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -51,11 +51,13 @@ export const TaskTool = Tool.define("task", async () => { ctx.abort.addEventListener("abort", () => { Session.abort(session.id) }) - const result = await Session.chat({ + const result = await Session.prompt({ messageID, sessionID: session.id, - modelID: model.modelID, - providerID: model.providerID, + model: { + modelID: model.modelID, + providerID: model.providerID, + }, agent: agent.name, tools: { todowrite: false, @@ -75,9 +77,9 @@ export const TaskTool = Tool.define("task", async () => { return { title: params.description, metadata: { - summary: result.parts.filter((x) => x.type === "tool"), + summary: result.parts.filter((x: any) => x.type === "tool"), }, - output: result.parts.findLast((x) => x.type === "text")?.text ?? "", + output: (result.parts.findLast((x: any) => x.type === "text") as any)?.text ?? "", } }, } diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index 7a11470f..abc720f0 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { Tool } from "./tool" import DESCRIPTION_WRITE from "./todowrite.txt" -import { App } from "../app/app" +import { Instance } from "../project/instance" const TodoInfo = z.object({ content: z.string().describe("Brief description of the task"), @@ -11,12 +11,14 @@ const TodoInfo = z.object({ }) type TodoInfo = z.infer -const state = App.state("todo-tool", () => { - const todos: { - [sessionId: string]: TodoInfo[] - } = {} - return todos -}) +const state = Instance.state( + () => { + const todos: { + [sessionId: string]: TodoInfo[] + } = {} + return todos + }, +) export const TodoWriteTool = Tool.define("todowrite", { description: DESCRIPTION_WRITE, diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 951ad730..dbd6e294 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -4,11 +4,11 @@ import { Tool } from "./tool" import { LSP } from "../lsp" import { Permission } from "../permission" import DESCRIPTION from "./write.txt" -import { App } from "../app/app" import { Bus } from "../bus" import { File } from "../file" import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" +import { Instance } from "../project/instance" import { Agent } from "../agent/agent" export const WriteTool = Tool.define("write", { @@ -18,9 +18,8 @@ export const WriteTool = Tool.define("write", { content: z.string().describe("The content to write to the file"), }), async execute(params, ctx) { - const app = App.info() - const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) - if (!Filesystem.contains(app.path.cwd, filepath)) { + const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) + if (!Filesystem.contains(Instance.directory, filepath)) { throw new Error(`File ${filepath} is not in the current working directory`) } @@ -62,7 +61,7 @@ export const WriteTool = Tool.define("write", { } return { - title: path.relative(app.path.root, filepath), + title: path.relative(Instance.worktree, filepath), metadata: { diagnostics, filepath, diff --git a/packages/opencode/src/util/lock.ts b/packages/opencode/src/util/lock.ts new file mode 100644 index 00000000..3aea6439 --- /dev/null +++ b/packages/opencode/src/util/lock.ts @@ -0,0 +1,98 @@ +export namespace Lock { + const locks = new Map< + string, + { + readers: number + writer: boolean + waitingReaders: (() => void)[] + waitingWriters: (() => void)[] + } + >() + + function get(key: string) { + if (!locks.has(key)) { + locks.set(key, { + readers: 0, + writer: false, + waitingReaders: [], + waitingWriters: [], + }) + } + return locks.get(key)! + } + + function process(key: string) { + const lock = locks.get(key) + if (!lock || lock.writer || lock.readers > 0) return + + // Prioritize writers to prevent starvation + if (lock.waitingWriters.length > 0) { + const nextWriter = lock.waitingWriters.shift()! + nextWriter() + return + } + + // Wake up all waiting readers + while (lock.waitingReaders.length > 0) { + const nextReader = lock.waitingReaders.shift()! + nextReader() + } + + // Clean up empty locks + if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) { + locks.delete(key) + } + } + + export async function read(key: string): Promise { + const lock = get(key) + + return new Promise((resolve) => { + if (!lock.writer && lock.waitingWriters.length === 0) { + lock.readers++ + resolve({ + [Symbol.dispose]: () => { + lock.readers-- + process(key) + }, + }) + } else { + lock.waitingReaders.push(() => { + lock.readers++ + resolve({ + [Symbol.dispose]: () => { + lock.readers-- + process(key) + }, + }) + }) + } + }) + } + + export async function write(key: string): Promise { + const lock = get(key) + + return new Promise((resolve) => { + if (!lock.writer && lock.readers === 0) { + lock.writer = true + resolve({ + [Symbol.dispose]: () => { + lock.writer = false + process(key) + }, + }) + } else { + lock.waitingWriters.push(() => { + lock.writer = true + resolve({ + [Symbol.dispose]: () => { + lock.writer = false + process(key) + }, + }) + }) + } + }) + } +} diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 43277dfa..cdbeec08 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test" -import { App } from "../../src/app/app" import path from "path" import { BashTool } from "../../src/tool/bash" import { Log } from "../../src/util/log" +import { Instance } from "../../src/project/instance" const ctx = { sessionID: "test", @@ -19,7 +19,7 @@ Log.init({ print: false }) describe("tool.bash", () => { test("basic", async () => { - await App.provide({ cwd: projectRoot }, async () => { + await Instance.provide(projectRoot, async () => { const result = await bash.execute( { command: "echo 'test'", @@ -33,7 +33,7 @@ describe("tool.bash", () => { }) test("cd ../ should fail outside of project root", async () => { - await App.provide({ cwd: projectRoot }, async () => { + await Instance.provide(projectRoot, async () => { expect( bash.execute( { diff --git a/packages/opencode/test/tool/tool.test.ts b/packages/opencode/test/tool/tool.test.ts index 313aa424..c0f6e524 100644 --- a/packages/opencode/test/tool/tool.test.ts +++ b/packages/opencode/test/tool/tool.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test" -import { App } from "../../src/app/app" import { GlobTool } from "../../src/tool/glob" import { ListTool } from "../../src/tool/ls" import path from "path" +import { Instance } from "../../src/project/instance" const ctx = { sessionID: "test", @@ -20,7 +20,7 @@ const fixturePath = path.join(__dirname, "../fixtures/example") describe("tool.glob", () => { test("truncate", async () => { - await App.provide({ cwd: projectRoot }, async () => { + await Instance.provide(projectRoot, async () => { let result = await glob.execute( { pattern: "**/*", @@ -32,7 +32,7 @@ describe("tool.glob", () => { }) }) test("basic", async () => { - await App.provide({ cwd: projectRoot }, async () => { + await Instance.provide(projectRoot, async () => { let result = await glob.execute( { pattern: "*.json", @@ -50,7 +50,7 @@ describe("tool.glob", () => { describe("tool.ls", () => { test("basic", async () => { - const result = await App.provide({ cwd: projectRoot }, async () => { + const result = await Instance.provide(projectRoot, async () => { return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx) }) diff --git a/packages/plugin/src/example.ts b/packages/plugin/src/example.ts index 998108f0..a13c0fba 100644 --- a/packages/plugin/src/example.ts +++ b/packages/plugin/src/example.ts @@ -1,6 +1,6 @@ import { Plugin } from "./index" -export const ExamplePlugin: Plugin = async ({ app, client, $ }) => { +export const ExamplePlugin: Plugin = async ({ client, $ }) => { return { permission: {}, async "chat.params"(input, output) { diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 5483607a..a00b48d1 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -1,7 +1,7 @@ import type { Event, createOpencodeClient, - App, + Project, Model, Provider, Permission, @@ -14,7 +14,9 @@ import type { BunShell } from "./shell" export type PluginInput = { client: ReturnType - app: App + project: Project + directory: string + worktree: string $: BunShell } export type Plugin = (input: PluginInput) => Promise diff --git a/packages/sdk/go/.release-please-manifest.json b/packages/sdk/go/.release-please-manifest.json index c373724d..6538ca91 100644 --- a/packages/sdk/go/.release-please-manifest.json +++ b/packages/sdk/go/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.8" + ".": "0.8.0" } \ No newline at end of file diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 58d41c72..79ec5d88 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 41 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-6d8e9dfd438cac63fc7d689ea29adfff81ff8880c2d8e1e10fc36f375a721594.yml -openapi_spec_hash: 7ac6028dd5957c67a98d91e790863c80 -config_hash: fb625e876313a9f8f31532348fa91f59 +configured_endpoints: 43 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-97b61518d8666ea7cb310af04248e00bcf8dc9753ba3c7e84471df72b3232004.yml +openapi_spec_hash: a3500531973ad999c350b87c21aa3ab8 +config_hash: 026ef000d34bf2f930e7b41e77d2d3ff diff --git a/packages/sdk/go/CHANGELOG.md b/packages/sdk/go/CHANGELOG.md index bc407fad..014933da 100644 --- a/packages/sdk/go/CHANGELOG.md +++ b/packages/sdk/go/CHANGELOG.md @@ -1,73 +1,122 @@ # Changelog -## 0.1.0-alpha.8 (2025-07-02) +## 0.8.0 (2025-09-01) -Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.7...v0.1.0-alpha.8) +Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0) ### Features -* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519)) +* **api:** api update ([ae87a71](https://github.com/sst/opencode-sdk-go/commit/ae87a71949994590ace8285a39f0991ef34b664d)) -## 0.1.0-alpha.7 (2025-06-30) +## 0.7.0 (2025-09-01) -Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.6...v0.1.0-alpha.7) +Full Changelog: [v0.6.0...v0.7.0](https://github.com/sst/opencode-sdk-go/compare/v0.6.0...v0.7.0) ### Features +* **api:** api update ([64bb1b1](https://github.com/sst/opencode-sdk-go/commit/64bb1b1ee0cbe153abc6fb7bd9703b47911724d4)) + +## 0.6.0 (2025-09-01) + +Full Changelog: [v0.5.0...v0.6.0](https://github.com/sst/opencode-sdk-go/compare/v0.5.0...v0.6.0) + +### Features + +* **api:** api update ([928e384](https://github.com/sst/opencode-sdk-go/commit/928e3843355f96899f046f002b84372281dad0c8)) + +## 0.5.0 (2025-08-31) + +Full Changelog: [v0.4.0...v0.5.0](https://github.com/sst/opencode-sdk-go/compare/v0.4.0...v0.5.0) + +### Features + +* **api:** api update ([44b281d](https://github.com/sst/opencode-sdk-go/commit/44b281d0bb39c5022a984ac9d0fca1529ccc0604)) + +## 0.4.0 (2025-08-31) + +Full Changelog: [v0.3.0...v0.4.0](https://github.com/sst/opencode-sdk-go/compare/v0.3.0...v0.4.0) + +### Features + +* **api:** api update ([fa9d6ec](https://github.com/sst/opencode-sdk-go/commit/fa9d6ec6472e62f4f6605d0a71a7aa8bf8a24559)) + +## 0.3.0 (2025-08-31) + +Full Changelog: [v0.2.0...v0.3.0](https://github.com/sst/opencode-sdk-go/compare/v0.2.0...v0.3.0) + +### Features + +* **api:** api update ([aae1c06](https://github.com/sst/opencode-sdk-go/commit/aae1c06bb5a93a1cd9c589846a84b3f16246f5da)) + +## 0.2.0 (2025-08-31) + +Full Changelog: [v0.1.0...v0.2.0](https://github.com/sst/opencode-sdk-go/compare/v0.1.0...v0.2.0) + +### Features + +* **api:** api update ([1472790](https://github.com/sst/opencode-sdk-go/commit/1472790542515f47bd46e2a9e28d8afea024cf9c)) + +## 0.1.0 (2025-08-31) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/sst/opencode-sdk-go/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** api update ([3f03ddd](https://github.com/sst/opencode-sdk-go/commit/3f03dddd5ec0de98f99ce48679077dcae9ceffd6)) +* **api:** api update ([e9f79c4](https://github.com/sst/opencode-sdk-go/commit/e9f79c4792b21ef64ab0431ffd76f5a71e04d182)) +* **api:** api update ([139a686](https://github.com/sst/opencode-sdk-go/commit/139a6862d2f0ab0c8ea791663d736868be3e96e6)) +* **api:** api update ([2ed0800](https://github.com/sst/opencode-sdk-go/commit/2ed0800b2c5b99877e9f7fde669a6c005fad6b77)) +* **api:** api update ([88a87a4](https://github.com/sst/opencode-sdk-go/commit/88a87a458f56ce0c18b502c73da933f614f56e8b)) +* **api:** api update ([0e5d65b](https://github.com/sst/opencode-sdk-go/commit/0e5d65b571e7b30dc6347e6730098878ebba3a42)) +* **api:** api update ([ba381f1](https://github.com/sst/opencode-sdk-go/commit/ba381f1e07aad24e9824df7d53befae2a644f69f)) +* **api:** api update ([3f429f5](https://github.com/sst/opencode-sdk-go/commit/3f429f5b4be5607433ef5fdc0d5bf67fe590d039)) +* **api:** api update ([9f34787](https://github.com/sst/opencode-sdk-go/commit/9f347876b35b7f898060c1a5f71c322e95978e3e)) +* **api:** api update ([379c8e0](https://github.com/sst/opencode-sdk-go/commit/379c8e00197e13aebaf2f2d61277b125f1f90011)) +* **api:** api update ([550511c](https://github.com/sst/opencode-sdk-go/commit/550511c4c5b5055ac8ff22b7b11731331bd9d088)) +* **api:** api update ([547f0c2](https://github.com/sst/opencode-sdk-go/commit/547f0c262f2df1ce83eaa7267d68be64bb29b841)) +* **api:** api update ([b7b0720](https://github.com/sst/opencode-sdk-go/commit/b7b07204bff314da24b1819c128835a43ef64065)) +* **api:** api update ([7250ffc](https://github.com/sst/opencode-sdk-go/commit/7250ffcba262b916c958ddecc2a42927982db39f)) +* **api:** api update ([17fbab7](https://github.com/sst/opencode-sdk-go/commit/17fbab73111a3eae488737c69b12370bc69c65f7)) +* **api:** api update ([1270b5c](https://github.com/sst/opencode-sdk-go/commit/1270b5cd81e6ac769dcd92ade6d877891bf51bd5)) +* **api:** api update ([a238d4a](https://github.com/sst/opencode-sdk-go/commit/a238d4abd6ed7d15f3547d27a4b6ecf4aec8431e)) +* **api:** api update ([7475655](https://github.com/sst/opencode-sdk-go/commit/7475655aca577fe4f807c2f02f92171f6a358e9c)) +* **api:** api update ([429d258](https://github.com/sst/opencode-sdk-go/commit/429d258bb56e9cdeb1528be3944bf5537ac26a96)) +* **api:** api update ([f250915](https://github.com/sst/opencode-sdk-go/commit/f2509157eaf1b453e741ee9482127cad2e3ace25)) +* **api:** api update ([5efc987](https://github.com/sst/opencode-sdk-go/commit/5efc987353801d1e772c20edf162b1c75da32743)) +* **api:** api update ([98a8350](https://github.com/sst/opencode-sdk-go/commit/98a83504f7cfc361e83314c3e79a4e9ff53f0560)) +* **api:** api update ([6da8bf8](https://github.com/sst/opencode-sdk-go/commit/6da8bf8bfe91d45991fb580753d77c5534fc0b1b)) +* **api:** api update ([f8c7148](https://github.com/sst/opencode-sdk-go/commit/f8c7148ae56143823186e2675a78e82676154956)) +* **api:** manual updates ([7cf038f](https://github.com/sst/opencode-sdk-go/commit/7cf038ffae5da1b77e1cef11b5fa166a53b467f2)) +* **api:** update via SDK Studio ([068a0eb](https://github.com/sst/opencode-sdk-go/commit/068a0eb025010da0c8d86fa1bb496a39dbedcef9)) +* **api:** update via SDK Studio ([ca651ed](https://github.com/sst/opencode-sdk-go/commit/ca651edaf71d1f3678f929287474f5bc4f1aad10)) * **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775)) * **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf)) +* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e)) +* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52)) +* **api:** update via SDK Studio ([51315fa](https://github.com/sst/opencode-sdk-go/commit/51315fa2eae424743ea79701e67d44447c44144d)) +* **api:** update via SDK Studio ([af07955](https://github.com/sst/opencode-sdk-go/commit/af0795543240aefaf04fc7663a348825541c79ed)) +* **api:** update via SDK Studio ([5e3468a](https://github.com/sst/opencode-sdk-go/commit/5e3468a0aaa5ed3b13e019c3a24e0ba9147d1675)) +* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397)) +* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844)) +* **client:** expand max streaming buffer size ([76303e5](https://github.com/sst/opencode-sdk-go/commit/76303e51067e78e732af26ced9d83b8bad7655c3)) +* **client:** support optional json html escaping ([449748f](https://github.com/sst/opencode-sdk-go/commit/449748f35a1d8cb6f91dc36d25bf9489f4f371bd)) + + +### Bug Fixes + +* **client:** process custom base url ahead of time ([9b360d6](https://github.com/sst/opencode-sdk-go/commit/9b360d642cf6f302104308af5622e17099899e5f)) +* **client:** resolve lint errors in streaming tests ([4d36cb0](https://github.com/sst/opencode-sdk-go/commit/4d36cb09fc9d436734d5dab1c499acaa88568ff7)) +* close body before retrying ([4da3f7f](https://github.com/sst/opencode-sdk-go/commit/4da3f7f372bad222a189ba3eabcfde3373166ae5)) +* don't try to deserialize as json when ResponseBodyInto is []byte ([595291f](https://github.com/sst/opencode-sdk-go/commit/595291f6dba6af472f160b9f8e3d145002f43a4a)) ### Chores * **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d)) - -## 0.1.0-alpha.6 (2025-06-28) - -Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) - -### Bug Fixes - -* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d)) - -## 0.1.0-alpha.5 (2025-06-27) - -Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) - -### Features - -* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e)) - -## 0.1.0-alpha.4 (2025-06-27) - -Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.3...v0.1.0-alpha.4) - -### Features - -* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52)) - -## 0.1.0-alpha.3 (2025-06-27) - -Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.2...v0.1.0-alpha.3) - -### Features - -* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61)) - -## 0.1.0-alpha.2 (2025-06-27) - -Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) - -### Features - -* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6)) - -## 0.1.0-alpha.1 (2025-06-27) - -Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-go/compare/v0.0.1-alpha.0...v0.1.0-alpha.1) - -### Features - -* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a)) -* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397)) -* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844)) +* **internal:** codegen related update ([6a22ce6](https://github.com/sst/opencode-sdk-go/commit/6a22ce6df155f5003e80b8a75686a9e513a5568a)) +* **internal:** fix lint script for tests ([391c482](https://github.com/sst/opencode-sdk-go/commit/391c482148ed0a77c4ad52807abeb2d540b56797)) +* **internal:** update comment in script ([b7f1c3e](https://github.com/sst/opencode-sdk-go/commit/b7f1c3e16935c71e243004b8f321d661cd8e9474)) +* lint tests ([616796b](https://github.com/sst/opencode-sdk-go/commit/616796b761704bde6be5c6c2428f28c79c7f05ff)) +* lint tests in subpackages ([50c82ff](https://github.com/sst/opencode-sdk-go/commit/50c82ff0757c973834b68adc22566b70f767b611)) +* sync repo ([2f34d5d](https://github.com/sst/opencode-sdk-go/commit/2f34d5d53e56e9cdc3df99be7ee7efc83dd977a3)) +* update @stainless-api/prism-cli to v5.15.0 ([2f24852](https://github.com/sst/opencode-sdk-go/commit/2f2485216d4f4891d1fbfbc23ff8410c2f35152a)) diff --git a/packages/sdk/go/README.md b/packages/sdk/go/README.md index 2588b614..78011182 100644 --- a/packages/sdk/go/README.md +++ b/packages/sdk/go/README.md @@ -24,7 +24,7 @@ Or to pin the version: ```sh -go get -u 'github.com/sst/opencode-sdk-go@v0.1.0-alpha.8' +go get -u 'github.com/sst/opencode-sdk-go@v0.8.0' ``` @@ -49,7 +49,7 @@ import ( func main() { client := opencode.NewClient() - sessions, err := client.Session.List(context.TODO()) + sessions, err := client.Session.List(context.TODO(), opencode.SessionListParams{}) if err != nil { panic(err.Error()) } @@ -171,7 +171,7 @@ When the API returns a non-success status code, we return an error with type To handle errors, we recommend that you use the `errors.As` pattern: ```go -_, err := client.Session.List(context.TODO()) +_, err := client.Session.List(context.TODO(), opencode.SessionListParams{}) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -198,6 +198,7 @@ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() client.Session.List( ctx, + opencode.SessionListParams{}, // This sets the per-retry timeout option.WithRequestTimeout(20*time.Second), ) @@ -231,7 +232,11 @@ client := opencode.NewClient( ) // Override per-request: -client.Session.List(context.TODO(), option.WithMaxRetries(5)) +client.Session.List( + context.TODO(), + opencode.SessionListParams{}, + option.WithMaxRetries(5), +) ``` ### Accessing raw response data (e.g. response headers) @@ -242,7 +247,11 @@ you need to examine response headers, status codes, or other details. ```go // Create a variable to store the HTTP response var response *http.Response -sessions, err := client.Session.List(context.TODO(), option.WithResponseInto(&response)) +sessions, err := client.Session.List( + context.TODO(), + opencode.SessionListParams{}, + option.WithResponseInto(&response), +) if err != nil { // handle error } diff --git a/packages/sdk/go/agent.go b/packages/sdk/go/agent.go new file mode 100644 index 00000000..5e8f4957 --- /dev/null +++ b/packages/sdk/go/agent.go @@ -0,0 +1,204 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "net/url" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// AgentService contains methods and other services that help with interacting with +// the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewAgentService] method instead. +type AgentService struct { + Options []option.RequestOption +} + +// NewAgentService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewAgentService(opts ...option.RequestOption) (r *AgentService) { + r = &AgentService{} + r.Options = opts + return +} + +// List all agents +func (r *AgentService) List(ctx context.Context, query AgentListParams, opts ...option.RequestOption) (res *[]Agent, err error) { + opts = append(r.Options[:], opts...) + path := "agent" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +type Agent struct { + BuiltIn bool `json:"builtIn,required"` + Mode AgentMode `json:"mode,required"` + Name string `json:"name,required"` + Options map[string]interface{} `json:"options,required"` + Permission AgentPermission `json:"permission,required"` + Tools map[string]bool `json:"tools,required"` + Description string `json:"description"` + Model AgentModel `json:"model"` + Prompt string `json:"prompt"` + Temperature float64 `json:"temperature"` + TopP float64 `json:"topP"` + JSON agentJSON `json:"-"` +} + +// agentJSON contains the JSON metadata for the struct [Agent] +type agentJSON struct { + BuiltIn apijson.Field + Mode apijson.Field + Name apijson.Field + Options apijson.Field + Permission apijson.Field + Tools apijson.Field + Description apijson.Field + Model apijson.Field + Prompt apijson.Field + Temperature apijson.Field + TopP apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Agent) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r agentJSON) RawJSON() string { + return r.raw +} + +type AgentMode string + +const ( + AgentModeSubagent AgentMode = "subagent" + AgentModePrimary AgentMode = "primary" + AgentModeAll AgentMode = "all" +) + +func (r AgentMode) IsKnown() bool { + switch r { + case AgentModeSubagent, AgentModePrimary, AgentModeAll: + return true + } + return false +} + +type AgentPermission struct { + Bash map[string]AgentPermissionBash `json:"bash,required"` + Edit AgentPermissionEdit `json:"edit,required"` + Webfetch AgentPermissionWebfetch `json:"webfetch"` + JSON agentPermissionJSON `json:"-"` +} + +// agentPermissionJSON contains the JSON metadata for the struct [AgentPermission] +type agentPermissionJSON struct { + Bash apijson.Field + Edit apijson.Field + Webfetch apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *AgentPermission) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r agentPermissionJSON) RawJSON() string { + return r.raw +} + +type AgentPermissionBash string + +const ( + AgentPermissionBashAsk AgentPermissionBash = "ask" + AgentPermissionBashAllow AgentPermissionBash = "allow" + AgentPermissionBashDeny AgentPermissionBash = "deny" +) + +func (r AgentPermissionBash) IsKnown() bool { + switch r { + case AgentPermissionBashAsk, AgentPermissionBashAllow, AgentPermissionBashDeny: + return true + } + return false +} + +type AgentPermissionEdit string + +const ( + AgentPermissionEditAsk AgentPermissionEdit = "ask" + AgentPermissionEditAllow AgentPermissionEdit = "allow" + AgentPermissionEditDeny AgentPermissionEdit = "deny" +) + +func (r AgentPermissionEdit) IsKnown() bool { + switch r { + case AgentPermissionEditAsk, AgentPermissionEditAllow, AgentPermissionEditDeny: + return true + } + return false +} + +type AgentPermissionWebfetch string + +const ( + AgentPermissionWebfetchAsk AgentPermissionWebfetch = "ask" + AgentPermissionWebfetchAllow AgentPermissionWebfetch = "allow" + AgentPermissionWebfetchDeny AgentPermissionWebfetch = "deny" +) + +func (r AgentPermissionWebfetch) IsKnown() bool { + switch r { + case AgentPermissionWebfetchAsk, AgentPermissionWebfetchAllow, AgentPermissionWebfetchDeny: + return true + } + return false +} + +type AgentModel struct { + ModelID string `json:"modelID,required"` + ProviderID string `json:"providerID,required"` + JSON agentModelJSON `json:"-"` +} + +// agentModelJSON contains the JSON metadata for the struct [AgentModel] +type agentModelJSON struct { + ModelID apijson.Field + ProviderID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *AgentModel) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r agentModelJSON) RawJSON() string { + return r.raw +} + +type AgentListParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [AgentListParams]'s query parameters as `url.Values`. +func (r AgentListParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/agent_test.go b/packages/sdk/go/agent_test.go new file mode 100644 index 00000000..0827df5f --- /dev/null +++ b/packages/sdk/go/agent_test.go @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestAgentListWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Agent.List(context.TODO(), opencode.AgentListParams{ + Directory: opencode.F("directory"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/sdk/go/api.md b/packages/sdk/go/api.md index 2f9eadb6..02ac42b3 100644 --- a/packages/sdk/go/api.md +++ b/packages/sdk/go/api.md @@ -12,25 +12,40 @@ Response Types: Methods: -- client.Event.List(ctx context.Context) (opencode.EventListResponse, error) +- client.Event.List(ctx context.Context, query opencode.EventListParams) (opencode.EventListResponse, error) + +# Path + +Response Types: + +- opencode.Path + +Methods: + +- client.Path.Get(ctx context.Context, query opencode.PathGetParams) (opencode.Path, error) # App Response Types: -- opencode.Agent -- opencode.App - opencode.Model - opencode.Provider - opencode.AppProvidersResponse Methods: -- client.App.Agents(ctx context.Context) ([]opencode.Agent, error) -- client.App.Get(ctx context.Context) (opencode.App, error) -- client.App.Init(ctx context.Context) (bool, error) -- client.App.Log(ctx context.Context, body opencode.AppLogParams) (bool, error) -- client.App.Providers(ctx context.Context) (opencode.AppProvidersResponse, error) +- client.App.Log(ctx context.Context, params opencode.AppLogParams) (bool, error) +- client.App.Providers(ctx context.Context, query opencode.AppProvidersParams) (opencode.AppProvidersResponse, error) + +# Agent + +Response Types: + +- opencode.Agent + +Methods: + +- client.Agent.List(ctx context.Context, query opencode.AgentListParams) ([]opencode.Agent, error) # Find @@ -50,12 +65,14 @@ Methods: Response Types: - opencode.File +- opencode.FileNode - opencode.FileReadResponse Methods: -- client.File.Read(ctx context.Context, query opencode.FileReadParams) (opencode.FileReadResponse, error) -- client.File.Status(ctx context.Context) ([]opencode.File, error) +- client.File.List(ctx context.Context, query opencode.FileListParams) ([]opencode.FileNode, error) +- client.File.Read(ctx context.Context, query opencode.FileReadParams) (opencode.FileReadResponse, error) +- client.File.Status(ctx context.Context, query opencode.FileStatusParams) ([]opencode.File, error) # Config @@ -68,7 +85,7 @@ Response Types: Methods: -- client.Config.Get(ctx context.Context) (opencode.Config, error) +- client.Config.Get(ctx context.Context, query opencode.ConfigGetParams) (opencode.Config, error) # Command @@ -78,7 +95,18 @@ Response Types: Methods: -- client.Command.List(ctx context.Context) ([]opencode.Command, error) +- client.Command.List(ctx context.Context, query opencode.CommandListParams) ([]opencode.Command, error) + +# Project + +Response Types: + +- opencode.Project + +Methods: + +- client.Project.List(ctx context.Context, query opencode.ProjectListParams) ([]opencode.Project, error) +- client.Project.Current(ctx context.Context, query opencode.ProjectCurrentParams) (opencode.Project, error) # Session @@ -115,31 +143,31 @@ Response Types: - opencode.ToolStatePending - opencode.ToolStateRunning - opencode.UserMessage -- opencode.SessionChatResponse - opencode.SessionCommandResponse - opencode.SessionMessageResponse - opencode.SessionMessagesResponse +- opencode.SessionPromptResponse Methods: -- client.Session.New(ctx context.Context, body opencode.SessionNewParams) (opencode.Session, error) -- client.Session.Update(ctx context.Context, id string, body opencode.SessionUpdateParams) (opencode.Session, error) -- client.Session.List(ctx context.Context) ([]opencode.Session, error) -- client.Session.Delete(ctx context.Context, id string) (bool, error) -- client.Session.Abort(ctx context.Context, id string) (bool, error) -- client.Session.Chat(ctx context.Context, id string, body opencode.SessionChatParams) (opencode.SessionChatResponse, error) -- client.Session.Children(ctx context.Context, id string) ([]opencode.Session, error) -- client.Session.Command(ctx context.Context, id string, body opencode.SessionCommandParams) (opencode.SessionCommandResponse, error) -- client.Session.Get(ctx context.Context, id string) (opencode.Session, error) -- client.Session.Init(ctx context.Context, id string, body opencode.SessionInitParams) (bool, error) -- client.Session.Message(ctx context.Context, id string, messageID string) (opencode.SessionMessageResponse, error) -- client.Session.Messages(ctx context.Context, id string) ([]opencode.SessionMessagesResponse, error) -- client.Session.Revert(ctx context.Context, id string, body opencode.SessionRevertParams) (opencode.Session, error) -- client.Session.Share(ctx context.Context, id string) (opencode.Session, error) -- client.Session.Shell(ctx context.Context, id string, body opencode.SessionShellParams) (opencode.AssistantMessage, error) -- client.Session.Summarize(ctx context.Context, id string, body opencode.SessionSummarizeParams) (bool, error) -- client.Session.Unrevert(ctx context.Context, id string) (opencode.Session, error) -- client.Session.Unshare(ctx context.Context, id string) (opencode.Session, error) +- client.Session.New(ctx context.Context, params opencode.SessionNewParams) (opencode.Session, error) +- client.Session.Update(ctx context.Context, id string, params opencode.SessionUpdateParams) (opencode.Session, error) +- client.Session.List(ctx context.Context, query opencode.SessionListParams) ([]opencode.Session, error) +- client.Session.Delete(ctx context.Context, id string, body opencode.SessionDeleteParams) (bool, error) +- client.Session.Abort(ctx context.Context, id string, body opencode.SessionAbortParams) (bool, error) +- client.Session.Children(ctx context.Context, id string, query opencode.SessionChildrenParams) ([]opencode.Session, error) +- client.Session.Command(ctx context.Context, id string, params opencode.SessionCommandParams) (opencode.SessionCommandResponse, error) +- client.Session.Get(ctx context.Context, id string, query opencode.SessionGetParams) (opencode.Session, error) +- client.Session.Init(ctx context.Context, id string, params opencode.SessionInitParams) (bool, error) +- client.Session.Message(ctx context.Context, id string, messageID string, query opencode.SessionMessageParams) (opencode.SessionMessageResponse, error) +- client.Session.Messages(ctx context.Context, id string, query opencode.SessionMessagesParams) ([]opencode.SessionMessagesResponse, error) +- client.Session.Prompt(ctx context.Context, id string, params opencode.SessionPromptParams) (opencode.SessionPromptResponse, error) +- client.Session.Revert(ctx context.Context, id string, params opencode.SessionRevertParams) (opencode.Session, error) +- client.Session.Share(ctx context.Context, id string, body opencode.SessionShareParams) (opencode.Session, error) +- client.Session.Shell(ctx context.Context, id string, params opencode.SessionShellParams) (opencode.AssistantMessage, error) +- client.Session.Summarize(ctx context.Context, id string, params opencode.SessionSummarizeParams) (bool, error) +- client.Session.Unrevert(ctx context.Context, id string, body opencode.SessionUnrevertParams) (opencode.Session, error) +- client.Session.Unshare(ctx context.Context, id string, body opencode.SessionUnshareParams) (opencode.Session, error) ## Permissions @@ -149,18 +177,18 @@ Response Types: Methods: -- client.Session.Permissions.Respond(ctx context.Context, id string, permissionID string, body opencode.SessionPermissionRespondParams) (bool, error) +- client.Session.Permissions.Respond(ctx context.Context, id string, permissionID string, params opencode.SessionPermissionRespondParams) (bool, error) # Tui Methods: -- client.Tui.AppendPrompt(ctx context.Context, body opencode.TuiAppendPromptParams) (bool, error) -- client.Tui.ClearPrompt(ctx context.Context) (bool, error) -- client.Tui.ExecuteCommand(ctx context.Context, body opencode.TuiExecuteCommandParams) (bool, error) -- client.Tui.OpenHelp(ctx context.Context) (bool, error) -- client.Tui.OpenModels(ctx context.Context) (bool, error) -- client.Tui.OpenSessions(ctx context.Context) (bool, error) -- client.Tui.OpenThemes(ctx context.Context) (bool, error) -- client.Tui.ShowToast(ctx context.Context, body opencode.TuiShowToastParams) (bool, error) -- client.Tui.SubmitPrompt(ctx context.Context) (bool, error) +- client.Tui.AppendPrompt(ctx context.Context, params opencode.TuiAppendPromptParams) (bool, error) +- client.Tui.ClearPrompt(ctx context.Context, body opencode.TuiClearPromptParams) (bool, error) +- client.Tui.ExecuteCommand(ctx context.Context, params opencode.TuiExecuteCommandParams) (bool, error) +- client.Tui.OpenHelp(ctx context.Context, body opencode.TuiOpenHelpParams) (bool, error) +- client.Tui.OpenModels(ctx context.Context, body opencode.TuiOpenModelsParams) (bool, error) +- client.Tui.OpenSessions(ctx context.Context, body opencode.TuiOpenSessionsParams) (bool, error) +- client.Tui.OpenThemes(ctx context.Context, body opencode.TuiOpenThemesParams) (bool, error) +- client.Tui.ShowToast(ctx context.Context, params opencode.TuiShowToastParams) (bool, error) +- client.Tui.SubmitPrompt(ctx context.Context, body opencode.TuiSubmitPromptParams) (bool, error) diff --git a/packages/sdk/go/app.go b/packages/sdk/go/app.go index 36d5be77..53a8aeb4 100644 --- a/packages/sdk/go/app.go +++ b/packages/sdk/go/app.go @@ -5,8 +5,10 @@ package opencode import ( "context" "net/http" + "net/url" "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" "github.com/sst/opencode-sdk-go/internal/param" "github.com/sst/opencode-sdk-go/internal/requestconfig" "github.com/sst/opencode-sdk-go/option" @@ -31,270 +33,22 @@ func NewAppService(opts ...option.RequestOption) (r *AppService) { return } -// List all agents -func (r *AppService) Agents(ctx context.Context, opts ...option.RequestOption) (res *[]Agent, err error) { - opts = append(r.Options[:], opts...) - path := "agent" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return -} - -// Get app info -func (r *AppService) Get(ctx context.Context, opts ...option.RequestOption) (res *App, err error) { - opts = append(r.Options[:], opts...) - path := "app" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return -} - -// Initialize the app -func (r *AppService) Init(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { - opts = append(r.Options[:], opts...) - path := "app/init" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return -} - // Write a log entry to the server logs -func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.RequestOption) (res *bool, err error) { +func (r *AppService) Log(ctx context.Context, params AppLogParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) path := "log" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // List all providers -func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption) (res *AppProvidersResponse, err error) { +func (r *AppService) Providers(ctx context.Context, query AppProvidersParams, opts ...option.RequestOption) (res *AppProvidersResponse, err error) { opts = append(r.Options[:], opts...) path := "config/providers" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } -type Agent struct { - BuiltIn bool `json:"builtIn,required"` - Mode AgentMode `json:"mode,required"` - Name string `json:"name,required"` - Options map[string]interface{} `json:"options,required"` - Permission AgentPermission `json:"permission,required"` - Tools map[string]bool `json:"tools,required"` - Description string `json:"description"` - Model AgentModel `json:"model"` - Prompt string `json:"prompt"` - Temperature float64 `json:"temperature"` - TopP float64 `json:"topP"` - JSON agentJSON `json:"-"` -} - -// agentJSON contains the JSON metadata for the struct [Agent] -type agentJSON struct { - BuiltIn apijson.Field - Mode apijson.Field - Name apijson.Field - Options apijson.Field - Permission apijson.Field - Tools apijson.Field - Description apijson.Field - Model apijson.Field - Prompt apijson.Field - Temperature apijson.Field - TopP apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *Agent) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r agentJSON) RawJSON() string { - return r.raw -} - -type AgentMode string - -const ( - AgentModeSubagent AgentMode = "subagent" - AgentModePrimary AgentMode = "primary" - AgentModeAll AgentMode = "all" -) - -func (r AgentMode) IsKnown() bool { - switch r { - case AgentModeSubagent, AgentModePrimary, AgentModeAll: - return true - } - return false -} - -type AgentPermission struct { - Bash map[string]AgentPermissionBash `json:"bash,required"` - Edit AgentPermissionEdit `json:"edit,required"` - Webfetch AgentPermissionWebfetch `json:"webfetch"` - JSON agentPermissionJSON `json:"-"` -} - -// agentPermissionJSON contains the JSON metadata for the struct [AgentPermission] -type agentPermissionJSON struct { - Bash apijson.Field - Edit apijson.Field - Webfetch apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AgentPermission) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r agentPermissionJSON) RawJSON() string { - return r.raw -} - -type AgentPermissionBash string - -const ( - AgentPermissionBashAsk AgentPermissionBash = "ask" - AgentPermissionBashAllow AgentPermissionBash = "allow" - AgentPermissionBashDeny AgentPermissionBash = "deny" -) - -func (r AgentPermissionBash) IsKnown() bool { - switch r { - case AgentPermissionBashAsk, AgentPermissionBashAllow, AgentPermissionBashDeny: - return true - } - return false -} - -type AgentPermissionEdit string - -const ( - AgentPermissionEditAsk AgentPermissionEdit = "ask" - AgentPermissionEditAllow AgentPermissionEdit = "allow" - AgentPermissionEditDeny AgentPermissionEdit = "deny" -) - -func (r AgentPermissionEdit) IsKnown() bool { - switch r { - case AgentPermissionEditAsk, AgentPermissionEditAllow, AgentPermissionEditDeny: - return true - } - return false -} - -type AgentPermissionWebfetch string - -const ( - AgentPermissionWebfetchAsk AgentPermissionWebfetch = "ask" - AgentPermissionWebfetchAllow AgentPermissionWebfetch = "allow" - AgentPermissionWebfetchDeny AgentPermissionWebfetch = "deny" -) - -func (r AgentPermissionWebfetch) IsKnown() bool { - switch r { - case AgentPermissionWebfetchAsk, AgentPermissionWebfetchAllow, AgentPermissionWebfetchDeny: - return true - } - return false -} - -type AgentModel struct { - ModelID string `json:"modelID,required"` - ProviderID string `json:"providerID,required"` - JSON agentModelJSON `json:"-"` -} - -// agentModelJSON contains the JSON metadata for the struct [AgentModel] -type agentModelJSON struct { - ModelID apijson.Field - ProviderID apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AgentModel) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r agentModelJSON) RawJSON() string { - return r.raw -} - -type App struct { - Git bool `json:"git,required"` - Hostname string `json:"hostname,required"` - Path AppPath `json:"path,required"` - Time AppTime `json:"time,required"` - JSON appJSON `json:"-"` -} - -// appJSON contains the JSON metadata for the struct [App] -type appJSON struct { - Git apijson.Field - Hostname apijson.Field - Path apijson.Field - Time apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *App) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r appJSON) RawJSON() string { - return r.raw -} - -type AppPath struct { - Config string `json:"config,required"` - Cwd string `json:"cwd,required"` - Data string `json:"data,required"` - Root string `json:"root,required"` - State string `json:"state,required"` - JSON appPathJSON `json:"-"` -} - -// appPathJSON contains the JSON metadata for the struct [AppPath] -type appPathJSON struct { - Config apijson.Field - Cwd apijson.Field - Data apijson.Field - Root apijson.Field - State apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AppPath) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r appPathJSON) RawJSON() string { - return r.raw -} - -type AppTime struct { - Initialized float64 `json:"initialized"` - JSON appTimeJSON `json:"-"` -} - -// appTimeJSON contains the JSON metadata for the struct [AppTime] -type appTimeJSON struct { - Initialized apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *AppTime) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r appTimeJSON) RawJSON() string { - return r.raw -} - type Model struct { ID string `json:"id,required"` Attachment bool `json:"attachment,required"` @@ -440,7 +194,8 @@ type AppLogParams struct { // Log message Message param.Field[string] `json:"message,required"` // Service name for the log entry - Service param.Field[string] `json:"service,required"` + Service param.Field[string] `json:"service,required"` + Directory param.Field[string] `query:"directory"` // Additional metadata for the log entry Extra param.Field[map[string]interface{}] `json:"extra"` } @@ -449,6 +204,14 @@ func (r AppLogParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [AppLogParams]'s query parameters as `url.Values`. +func (r AppLogParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + // Log level type AppLogParamsLevel string @@ -466,3 +229,15 @@ func (r AppLogParamsLevel) IsKnown() bool { } return false } + +type AppProvidersParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [AppProvidersParams]'s query parameters as `url.Values`. +func (r AppProvidersParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/app_test.go b/packages/sdk/go/app_test.go index 2ade6c7e..eb2fc92e 100644 --- a/packages/sdk/go/app_test.go +++ b/packages/sdk/go/app_test.go @@ -13,74 +13,8 @@ import ( "github.com/sst/opencode-sdk-go/option" ) -func TestAppAgents(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := opencode.NewClient( - option.WithBaseURL(baseURL), - ) - _, err := client.App.Agents(context.TODO()) - if err != nil { - var apierr *opencode.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} - -func TestAppGet(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := opencode.NewClient( - option.WithBaseURL(baseURL), - ) - _, err := client.App.Get(context.TODO()) - if err != nil { - var apierr *opencode.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} - -func TestAppInit(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := opencode.NewClient( - option.WithBaseURL(baseURL), - ) - _, err := client.App.Init(context.TODO()) - if err != nil { - var apierr *opencode.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} - func TestAppLogWithOptionalParams(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -92,9 +26,10 @@ func TestAppLogWithOptionalParams(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.App.Log(context.TODO(), opencode.AppLogParams{ - Level: opencode.F(opencode.AppLogParamsLevelDebug), - Message: opencode.F("message"), - Service: opencode.F("service"), + Level: opencode.F(opencode.AppLogParamsLevelDebug), + Message: opencode.F("message"), + Service: opencode.F("service"), + Directory: opencode.F("directory"), Extra: opencode.F(map[string]interface{}{ "foo": "bar", }), @@ -108,8 +43,8 @@ func TestAppLogWithOptionalParams(t *testing.T) { } } -func TestAppProviders(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestAppProvidersWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -120,7 +55,9 @@ func TestAppProviders(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.App.Providers(context.TODO()) + _, err := client.App.Providers(context.TODO(), opencode.AppProvidersParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { diff --git a/packages/sdk/go/client.go b/packages/sdk/go/client.go index 286408ab..c66b86bb 100644 --- a/packages/sdk/go/client.go +++ b/packages/sdk/go/client.go @@ -17,11 +17,14 @@ import ( type Client struct { Options []option.RequestOption Event *EventService + Path *PathService App *AppService + Agent *AgentService Find *FindService File *FileService Config *ConfigService Command *CommandService + Project *ProjectService Session *SessionService Tui *TuiService } @@ -46,11 +49,14 @@ func NewClient(opts ...option.RequestOption) (r *Client) { r = &Client{Options: opts} r.Event = NewEventService(opts...) + r.Path = NewPathService(opts...) r.App = NewAppService(opts...) + r.Agent = NewAgentService(opts...) r.Find = NewFindService(opts...) r.File = NewFileService(opts...) r.Config = NewConfigService(opts...) r.Command = NewCommandService(opts...) + r.Project = NewProjectService(opts...) r.Session = NewSessionService(opts...) r.Tui = NewTuiService(opts...) diff --git a/packages/sdk/go/client_test.go b/packages/sdk/go/client_test.go index 0f5b8205..d620da8e 100644 --- a/packages/sdk/go/client_test.go +++ b/packages/sdk/go/client_test.go @@ -38,7 +38,7 @@ func TestUserAgentHeader(t *testing.T) { }, }), ) - client.Session.List(context.Background()) + client.Session.List(context.Background(), opencode.SessionListParams{}) if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) { t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) } @@ -61,7 +61,7 @@ func TestRetryAfter(t *testing.T) { }, }), ) - _, err := client.Session.List(context.Background()) + _, err := client.Session.List(context.Background(), opencode.SessionListParams{}) if err == nil { t.Error("Expected there to be a cancel error") } @@ -95,7 +95,7 @@ func TestDeleteRetryCountHeader(t *testing.T) { }), option.WithHeaderDel("X-Stainless-Retry-Count"), ) - _, err := client.Session.List(context.Background()) + _, err := client.Session.List(context.Background(), opencode.SessionListParams{}) if err == nil { t.Error("Expected there to be a cancel error") } @@ -124,7 +124,7 @@ func TestOverwriteRetryCountHeader(t *testing.T) { }), option.WithHeader("X-Stainless-Retry-Count", "42"), ) - _, err := client.Session.List(context.Background()) + _, err := client.Session.List(context.Background(), opencode.SessionListParams{}) if err == nil { t.Error("Expected there to be a cancel error") } @@ -152,7 +152,7 @@ func TestRetryAfterMs(t *testing.T) { }, }), ) - _, err := client.Session.List(context.Background()) + _, err := client.Session.List(context.Background(), opencode.SessionListParams{}) if err == nil { t.Error("Expected there to be a cancel error") } @@ -174,7 +174,7 @@ func TestContextCancel(t *testing.T) { ) cancelCtx, cancel := context.WithCancel(context.Background()) cancel() - _, err := client.Session.List(cancelCtx) + _, err := client.Session.List(cancelCtx, opencode.SessionListParams{}) if err == nil { t.Error("Expected there to be a cancel error") } @@ -193,7 +193,7 @@ func TestContextCancelDelay(t *testing.T) { ) cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) defer cancel() - _, err := client.Session.List(cancelCtx) + _, err := client.Session.List(cancelCtx, opencode.SessionListParams{}) if err == nil { t.Error("expected there to be a cancel error") } @@ -218,7 +218,7 @@ func TestContextDeadline(t *testing.T) { }, }), ) - _, err := client.Session.List(deadlineCtx) + _, err := client.Session.List(deadlineCtx, opencode.SessionListParams{}) if err == nil { t.Error("expected there to be a deadline error") } @@ -262,7 +262,7 @@ func TestContextDeadlineStreaming(t *testing.T) { }, }), ) - stream := client.Event.ListStreaming(deadlineCtx) + stream := client.Event.ListStreaming(deadlineCtx, opencode.EventListParams{}) for stream.Next() { _ = stream.Current() } @@ -306,7 +306,11 @@ func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) { }, }), ) - stream := client.Event.ListStreaming(context.Background(), option.WithRequestTimeout((100 * time.Millisecond))) + stream := client.Event.ListStreaming( + context.Background(), + opencode.EventListParams{}, + option.WithRequestTimeout((100 * time.Millisecond)), + ) for stream.Next() { _ = stream.Current() } diff --git a/packages/sdk/go/command.go b/packages/sdk/go/command.go index 9ca70c3a..2638fc60 100644 --- a/packages/sdk/go/command.go +++ b/packages/sdk/go/command.go @@ -5,8 +5,11 @@ package opencode import ( "context" "net/http" + "net/url" "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" "github.com/sst/opencode-sdk-go/internal/requestconfig" "github.com/sst/opencode-sdk-go/option" ) @@ -31,10 +34,10 @@ func NewCommandService(opts ...option.RequestOption) (r *CommandService) { } // List all commands -func (r *CommandService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Command, err error) { +func (r *CommandService) List(ctx context.Context, query CommandListParams, opts ...option.RequestOption) (res *[]Command, err error) { opts = append(r.Options[:], opts...) path := "command" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -65,3 +68,15 @@ func (r *Command) UnmarshalJSON(data []byte) (err error) { func (r commandJSON) RawJSON() string { return r.raw } + +type CommandListParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [CommandListParams]'s query parameters as `url.Values`. +func (r CommandListParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/command_test.go b/packages/sdk/go/command_test.go index 5fd8c37b..781498b2 100644 --- a/packages/sdk/go/command_test.go +++ b/packages/sdk/go/command_test.go @@ -13,8 +13,8 @@ import ( "github.com/sst/opencode-sdk-go/option" ) -func TestCommandList(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestCommandListWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -25,7 +25,9 @@ func TestCommandList(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Command.List(context.TODO()) + _, err := client.Command.List(context.TODO(), opencode.CommandListParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index 59db54b9..5469fb29 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -5,11 +5,15 @@ package opencode import ( "context" "net/http" + "net/url" "reflect" "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" "github.com/sst/opencode-sdk-go/internal/requestconfig" "github.com/sst/opencode-sdk-go/option" + "github.com/sst/opencode-sdk-go/shared" "github.com/tidwall/gjson" ) @@ -33,10 +37,10 @@ func NewConfigService(opts ...option.RequestOption) (r *ConfigService) { } // Get config info -func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (res *Config, err error) { +func (r *ConfigService) Get(ctx context.Context, query ConfigGetParams, opts ...option.RequestOption) (res *Config, err error) { opts = append(r.Options[:], opts...) path := "config" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -49,8 +53,9 @@ type Config struct { // automatically Autoshare bool `json:"autoshare"` // Automatically update to the latest version - Autoupdate bool `json:"autoupdate"` - Command map[string]ConfigCommand `json:"command"` + Autoupdate bool `json:"autoupdate"` + // Command configuration, see https://opencode.ai/docs/commands + Command map[string]ConfigCommand `json:"command"` // Disable providers that are loaded automatically DisabledProviders []string `json:"disabled_providers"` Experimental ConfigExperimental `json:"experimental"` @@ -1646,10 +1651,13 @@ func (r configProviderModelsLimitJSON) RawJSON() string { } type ConfigProviderOptions struct { - APIKey string `json:"apiKey"` - BaseURL string `json:"baseURL"` - ExtraFields map[string]interface{} `json:"-,extras"` - JSON configProviderOptionsJSON `json:"-"` + APIKey string `json:"apiKey"` + BaseURL string `json:"baseURL"` + // Timeout in milliseconds for requests to this provider. Default is 300000 (5 + // minutes). Set to false to disable timeout. + Timeout ConfigProviderOptionsTimeoutUnion `json:"timeout"` + ExtraFields map[string]interface{} `json:"-,extras"` + JSON configProviderOptionsJSON `json:"-"` } // configProviderOptionsJSON contains the JSON metadata for the struct @@ -1657,6 +1665,7 @@ type ConfigProviderOptions struct { type configProviderOptionsJSON struct { APIKey apijson.Field BaseURL apijson.Field + Timeout apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -1669,6 +1678,33 @@ func (r configProviderOptionsJSON) RawJSON() string { return r.raw } +// Timeout in milliseconds for requests to this provider. Default is 300000 (5 +// minutes). Set to false to disable timeout. +// +// Union satisfied by [shared.UnionInt] or [shared.UnionBool]. +type ConfigProviderOptionsTimeoutUnion interface { + ImplementsConfigProviderOptionsTimeoutUnion() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*ConfigProviderOptionsTimeoutUnion)(nil)).Elem(), + "", + apijson.UnionVariant{ + TypeFilter: gjson.Number, + Type: reflect.TypeOf(shared.UnionInt(0)), + }, + apijson.UnionVariant{ + TypeFilter: gjson.True, + Type: reflect.TypeOf(shared.UnionBool(false)), + }, + apijson.UnionVariant{ + TypeFilter: gjson.False, + Type: reflect.TypeOf(shared.UnionBool(false)), + }, + ) +} + // Control sharing behavior:'manual' allows manual sharing via commands, 'auto' // enables automatic sharing, 'disabled' disables all sharing type ConfigShare string @@ -1967,3 +2003,15 @@ func (r McpRemoteConfigType) IsKnown() bool { } return false } + +type ConfigGetParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [ConfigGetParams]'s query parameters as `url.Values`. +func (r ConfigGetParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/config_test.go b/packages/sdk/go/config_test.go index 86e058a9..f188d7e8 100644 --- a/packages/sdk/go/config_test.go +++ b/packages/sdk/go/config_test.go @@ -13,8 +13,8 @@ import ( "github.com/sst/opencode-sdk-go/option" ) -func TestConfigGet(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestConfigGetWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -25,7 +25,9 @@ func TestConfigGet(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Config.Get(context.TODO()) + _, err := client.Config.Get(context.TODO(), opencode.ConfigGetParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { diff --git a/packages/sdk/go/event.go b/packages/sdk/go/event.go index 5eee292e..2f44f907 100644 --- a/packages/sdk/go/event.go +++ b/packages/sdk/go/event.go @@ -5,9 +5,12 @@ package opencode import ( "context" "net/http" + "net/url" "reflect" "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" "github.com/sst/opencode-sdk-go/internal/requestconfig" "github.com/sst/opencode-sdk-go/option" "github.com/sst/opencode-sdk-go/packages/ssestream" @@ -35,7 +38,7 @@ func NewEventService(opts ...option.RequestOption) (r *EventService) { } // Get events -func (r *EventService) ListStreaming(ctx context.Context, opts ...option.RequestOption) (stream *ssestream.Stream[EventListResponse]) { +func (r *EventService) ListStreaming(ctx context.Context, query EventListParams, opts ...option.RequestOption) (stream *ssestream.Stream[EventListResponse]) { var ( raw *http.Response err error @@ -43,7 +46,7 @@ func (r *EventService) ListStreaming(ctx context.Context, opts ...option.Request opts = append(r.Options[:], opts...) opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...) path := "event" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &raw, opts...) return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err) } @@ -54,16 +57,13 @@ type EventListResponse struct { // [EventListResponseEventMessageUpdatedProperties], // [EventListResponseEventMessageRemovedProperties], // [EventListResponseEventMessagePartUpdatedProperties], - // [EventListResponseEventMessagePartRemovedProperties], - // [EventListResponseEventStorageWriteProperties], [Permission], + // [EventListResponseEventMessagePartRemovedProperties], [Permission], // [EventListResponseEventPermissionRepliedProperties], // [EventListResponseEventFileEditedProperties], // [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionIdleProperties], - // [EventListResponseEventSessionErrorProperties], [interface{}], - // [EventListResponseEventFileWatcherUpdatedProperties], - // [EventListResponseEventIdeInstalledProperties]. + // [EventListResponseEventSessionErrorProperties], [interface{}]. Properties interface{} `json:"properties,required"` Type EventListResponseType `json:"type,required"` JSON eventListResponseJSON `json:"-"` @@ -101,13 +101,11 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) { // [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], // [EventListResponseEventMessagePartUpdated], // [EventListResponseEventMessagePartRemoved], -// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated], +// [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], -// [EventListResponseEventServerConnected], -// [EventListResponseEventFileWatcherUpdated], -// [EventListResponseEventIdeInstalled]. +// [EventListResponseEventServerConnected]. func (r EventListResponse) AsUnion() EventListResponseUnion { return r.union } @@ -117,13 +115,11 @@ func (r EventListResponse) AsUnion() EventListResponseUnion { // [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], // [EventListResponseEventMessagePartUpdated], // [EventListResponseEventMessagePartRemoved], -// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated], +// [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], -// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], -// [EventListResponseEventServerConnected], -// [EventListResponseEventFileWatcherUpdated] or -// [EventListResponseEventIdeInstalled]. +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or +// [EventListResponseEventServerConnected]. type EventListResponseUnion interface { implementsEventListResponse() } @@ -162,11 +158,6 @@ func init() { Type: reflect.TypeOf(EventListResponseEventMessagePartRemoved{}), DiscriminatorValue: "message.part.removed", }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(EventListResponseEventStorageWrite{}), - DiscriminatorValue: "storage.write", - }, apijson.UnionVariant{ TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventPermissionUpdated{}), @@ -207,16 +198,6 @@ func init() { Type: reflect.TypeOf(EventListResponseEventServerConnected{}), DiscriminatorValue: "server.connected", }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(EventListResponseEventFileWatcherUpdated{}), - DiscriminatorValue: "file.watcher.updated", - }, - apijson.UnionVariant{ - TypeFilter: gjson.JSON, - Type: reflect.TypeOf(EventListResponseEventIdeInstalled{}), - DiscriminatorValue: "ide.installed", - }, ) } @@ -588,68 +569,6 @@ func (r EventListResponseEventMessagePartRemovedType) IsKnown() bool { return false } -type EventListResponseEventStorageWrite struct { - Properties EventListResponseEventStorageWriteProperties `json:"properties,required"` - Type EventListResponseEventStorageWriteType `json:"type,required"` - JSON eventListResponseEventStorageWriteJSON `json:"-"` -} - -// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct -// [EventListResponseEventStorageWrite] -type eventListResponseEventStorageWriteJSON struct { - Properties apijson.Field - Type apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventStorageWriteJSON) RawJSON() string { - return r.raw -} - -func (r EventListResponseEventStorageWrite) implementsEventListResponse() {} - -type EventListResponseEventStorageWriteProperties struct { - Key string `json:"key,required"` - Content interface{} `json:"content"` - JSON eventListResponseEventStorageWritePropertiesJSON `json:"-"` -} - -// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for -// the struct [EventListResponseEventStorageWriteProperties] -type eventListResponseEventStorageWritePropertiesJSON struct { - Key apijson.Field - Content apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string { - return r.raw -} - -type EventListResponseEventStorageWriteType string - -const ( - EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write" -) - -func (r EventListResponseEventStorageWriteType) IsKnown() bool { - switch r { - case EventListResponseEventStorageWriteTypeStorageWrite: - return true - } - return false -} - type EventListResponseEventPermissionUpdated struct { Properties Permission `json:"properties,required"` Type EventListResponseEventPermissionUpdatedType `json:"type,required"` @@ -1228,143 +1147,6 @@ func (r EventListResponseEventServerConnectedType) IsKnown() bool { return false } -type EventListResponseEventFileWatcherUpdated struct { - Properties EventListResponseEventFileWatcherUpdatedProperties `json:"properties,required"` - Type EventListResponseEventFileWatcherUpdatedType `json:"type,required"` - JSON eventListResponseEventFileWatcherUpdatedJSON `json:"-"` -} - -// eventListResponseEventFileWatcherUpdatedJSON contains the JSON metadata for the -// struct [EventListResponseEventFileWatcherUpdated] -type eventListResponseEventFileWatcherUpdatedJSON struct { - Properties apijson.Field - Type apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventFileWatcherUpdated) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventFileWatcherUpdatedJSON) RawJSON() string { - return r.raw -} - -func (r EventListResponseEventFileWatcherUpdated) implementsEventListResponse() {} - -type EventListResponseEventFileWatcherUpdatedProperties struct { - Event EventListResponseEventFileWatcherUpdatedPropertiesEvent `json:"event,required"` - File string `json:"file,required"` - JSON eventListResponseEventFileWatcherUpdatedPropertiesJSON `json:"-"` -} - -// eventListResponseEventFileWatcherUpdatedPropertiesJSON contains the JSON -// metadata for the struct [EventListResponseEventFileWatcherUpdatedProperties] -type eventListResponseEventFileWatcherUpdatedPropertiesJSON struct { - Event apijson.Field - File apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventFileWatcherUpdatedProperties) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventFileWatcherUpdatedPropertiesJSON) RawJSON() string { - return r.raw -} - -type EventListResponseEventFileWatcherUpdatedPropertiesEvent string - -const ( - EventListResponseEventFileWatcherUpdatedPropertiesEventRename EventListResponseEventFileWatcherUpdatedPropertiesEvent = "rename" - EventListResponseEventFileWatcherUpdatedPropertiesEventChange EventListResponseEventFileWatcherUpdatedPropertiesEvent = "change" -) - -func (r EventListResponseEventFileWatcherUpdatedPropertiesEvent) IsKnown() bool { - switch r { - case EventListResponseEventFileWatcherUpdatedPropertiesEventRename, EventListResponseEventFileWatcherUpdatedPropertiesEventChange: - return true - } - return false -} - -type EventListResponseEventFileWatcherUpdatedType string - -const ( - EventListResponseEventFileWatcherUpdatedTypeFileWatcherUpdated EventListResponseEventFileWatcherUpdatedType = "file.watcher.updated" -) - -func (r EventListResponseEventFileWatcherUpdatedType) IsKnown() bool { - switch r { - case EventListResponseEventFileWatcherUpdatedTypeFileWatcherUpdated: - return true - } - return false -} - -type EventListResponseEventIdeInstalled struct { - Properties EventListResponseEventIdeInstalledProperties `json:"properties,required"` - Type EventListResponseEventIdeInstalledType `json:"type,required"` - JSON eventListResponseEventIdeInstalledJSON `json:"-"` -} - -// eventListResponseEventIdeInstalledJSON contains the JSON metadata for the struct -// [EventListResponseEventIdeInstalled] -type eventListResponseEventIdeInstalledJSON struct { - Properties apijson.Field - Type apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventIdeInstalled) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventIdeInstalledJSON) RawJSON() string { - return r.raw -} - -func (r EventListResponseEventIdeInstalled) implementsEventListResponse() {} - -type EventListResponseEventIdeInstalledProperties struct { - Ide string `json:"ide,required"` - JSON eventListResponseEventIdeInstalledPropertiesJSON `json:"-"` -} - -// eventListResponseEventIdeInstalledPropertiesJSON contains the JSON metadata for -// the struct [EventListResponseEventIdeInstalledProperties] -type eventListResponseEventIdeInstalledPropertiesJSON struct { - Ide apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *EventListResponseEventIdeInstalledProperties) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r eventListResponseEventIdeInstalledPropertiesJSON) RawJSON() string { - return r.raw -} - -type EventListResponseEventIdeInstalledType string - -const ( - EventListResponseEventIdeInstalledTypeIdeInstalled EventListResponseEventIdeInstalledType = "ide.installed" -) - -func (r EventListResponseEventIdeInstalledType) IsKnown() bool { - switch r { - case EventListResponseEventIdeInstalledTypeIdeInstalled: - return true - } - return false -} - type EventListResponseType string const ( @@ -1374,7 +1156,6 @@ const ( EventListResponseTypeMessageRemoved EventListResponseType = "message.removed" EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated" EventListResponseTypeMessagePartRemoved EventListResponseType = "message.part.removed" - EventListResponseTypeStorageWrite EventListResponseType = "storage.write" EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated" EventListResponseTypePermissionReplied EventListResponseType = "permission.replied" EventListResponseTypeFileEdited EventListResponseType = "file.edited" @@ -1383,14 +1164,24 @@ const ( EventListResponseTypeSessionIdle EventListResponseType = "session.idle" EventListResponseTypeSessionError EventListResponseType = "session.error" EventListResponseTypeServerConnected EventListResponseType = "server.connected" - EventListResponseTypeFileWatcherUpdated EventListResponseType = "file.watcher.updated" - EventListResponseTypeIdeInstalled EventListResponseType = "ide.installed" ) func (r EventListResponseType) IsKnown() bool { switch r { - case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeStorageWrite, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled: + case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected: return true } return false } + +type EventListParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [EventListParams]'s query parameters as `url.Values`. +func (r EventListParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/file.go b/packages/sdk/go/file.go index 0a8a4b2b..3e1b2f42 100644 --- a/packages/sdk/go/file.go +++ b/packages/sdk/go/file.go @@ -33,19 +33,27 @@ func NewFileService(opts ...option.RequestOption) (r *FileService) { return } -// Read a file -func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...option.RequestOption) (res *FileReadResponse, err error) { +// List files and directories +func (r *FileService) List(ctx context.Context, query FileListParams, opts ...option.RequestOption) (res *[]FileNode, err error) { opts = append(r.Options[:], opts...) path := "file" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } +// Read a file +func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...option.RequestOption) (res *FileReadResponse, err error) { + opts = append(r.Options[:], opts...) + path := "file/content" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + // Get file status -func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]File, err error) { +func (r *FileService) Status(ctx context.Context, query FileStatusParams, opts ...option.RequestOption) (res *[]File, err error) { opts = append(r.Options[:], opts...) path := "file/status" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } @@ -91,6 +99,47 @@ func (r FileStatus) IsKnown() bool { return false } +type FileNode struct { + Ignored bool `json:"ignored,required"` + Name string `json:"name,required"` + Path string `json:"path,required"` + Type FileNodeType `json:"type,required"` + JSON fileNodeJSON `json:"-"` +} + +// fileNodeJSON contains the JSON metadata for the struct [FileNode] +type fileNodeJSON struct { + Ignored apijson.Field + Name apijson.Field + Path apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FileNode) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r fileNodeJSON) RawJSON() string { + return r.raw +} + +type FileNodeType string + +const ( + FileNodeTypeFile FileNodeType = "file" + FileNodeTypeDirectory FileNodeType = "directory" +) + +func (r FileNodeType) IsKnown() bool { + switch r { + case FileNodeTypeFile, FileNodeTypeDirectory: + return true + } + return false +} + type FileReadResponse struct { Content string `json:"content,required"` Type FileReadResponseType `json:"type,required"` @@ -129,8 +178,22 @@ func (r FileReadResponseType) IsKnown() bool { return false } +type FileListParams struct { + Path param.Field[string] `query:"path,required"` + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [FileListParams]'s query parameters as `url.Values`. +func (r FileListParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type FileReadParams struct { - Path param.Field[string] `query:"path,required"` + Path param.Field[string] `query:"path,required"` + Directory param.Field[string] `query:"directory"` } // URLQuery serializes [FileReadParams]'s query parameters as `url.Values`. @@ -140,3 +203,15 @@ func (r FileReadParams) URLQuery() (v url.Values) { NestedFormat: apiquery.NestedQueryFormatBrackets, }) } + +type FileStatusParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [FileStatusParams]'s query parameters as `url.Values`. +func (r FileStatusParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/file_test.go b/packages/sdk/go/file_test.go index 60212ea2..2790fff9 100644 --- a/packages/sdk/go/file_test.go +++ b/packages/sdk/go/file_test.go @@ -13,8 +13,33 @@ import ( "github.com/sst/opencode-sdk-go/option" ) -func TestFileRead(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestFileListWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.File.List(context.TODO(), opencode.FileListParams{ + Path: opencode.F("path"), + Directory: opencode.F("directory"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileReadWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -26,7 +51,8 @@ func TestFileRead(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.File.Read(context.TODO(), opencode.FileReadParams{ - Path: opencode.F("path"), + Path: opencode.F("path"), + Directory: opencode.F("directory"), }) if err != nil { var apierr *opencode.Error @@ -37,8 +63,8 @@ func TestFileRead(t *testing.T) { } } -func TestFileStatus(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestFileStatusWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -49,7 +75,9 @@ func TestFileStatus(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.File.Status(context.TODO()) + _, err := client.File.Status(context.TODO(), opencode.FileStatusParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { diff --git a/packages/sdk/go/find.go b/packages/sdk/go/find.go index a993a353..e869116b 100644 --- a/packages/sdk/go/find.go +++ b/packages/sdk/go/find.go @@ -290,7 +290,8 @@ func (r findTextResponseSubmatchesMatchJSON) RawJSON() string { } type FindFilesParams struct { - Query param.Field[string] `query:"query,required"` + Query param.Field[string] `query:"query,required"` + Directory param.Field[string] `query:"directory"` } // URLQuery serializes [FindFilesParams]'s query parameters as `url.Values`. @@ -302,7 +303,8 @@ func (r FindFilesParams) URLQuery() (v url.Values) { } type FindSymbolsParams struct { - Query param.Field[string] `query:"query,required"` + Query param.Field[string] `query:"query,required"` + Directory param.Field[string] `query:"directory"` } // URLQuery serializes [FindSymbolsParams]'s query parameters as `url.Values`. @@ -314,7 +316,8 @@ func (r FindSymbolsParams) URLQuery() (v url.Values) { } type FindTextParams struct { - Pattern param.Field[string] `query:"pattern,required"` + Pattern param.Field[string] `query:"pattern,required"` + Directory param.Field[string] `query:"directory"` } // URLQuery serializes [FindTextParams]'s query parameters as `url.Values`. diff --git a/packages/sdk/go/find_test.go b/packages/sdk/go/find_test.go index e2f1caa1..901a895b 100644 --- a/packages/sdk/go/find_test.go +++ b/packages/sdk/go/find_test.go @@ -13,8 +13,8 @@ import ( "github.com/sst/opencode-sdk-go/option" ) -func TestFindFiles(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestFindFilesWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -26,7 +26,8 @@ func TestFindFiles(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.Find.Files(context.TODO(), opencode.FindFilesParams{ - Query: opencode.F("query"), + Query: opencode.F("query"), + Directory: opencode.F("directory"), }) if err != nil { var apierr *opencode.Error @@ -37,8 +38,8 @@ func TestFindFiles(t *testing.T) { } } -func TestFindSymbols(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestFindSymbolsWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -50,7 +51,8 @@ func TestFindSymbols(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.Find.Symbols(context.TODO(), opencode.FindSymbolsParams{ - Query: opencode.F("query"), + Query: opencode.F("query"), + Directory: opencode.F("directory"), }) if err != nil { var apierr *opencode.Error @@ -61,8 +63,8 @@ func TestFindSymbols(t *testing.T) { } } -func TestFindText(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestFindTextWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -74,7 +76,8 @@ func TestFindText(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.Find.Text(context.TODO(), opencode.FindTextParams{ - Pattern: opencode.F("pattern"), + Pattern: opencode.F("pattern"), + Directory: opencode.F("directory"), }) if err != nil { var apierr *opencode.Error diff --git a/packages/sdk/go/internal/requestconfig/requestconfig.go b/packages/sdk/go/internal/requestconfig/requestconfig.go index 53bf9f58..95111f75 100644 --- a/packages/sdk/go/internal/requestconfig/requestconfig.go +++ b/packages/sdk/go/internal/requestconfig/requestconfig.go @@ -464,6 +464,11 @@ func (cfg *RequestConfig) Execute() (err error) { break } + // Close the response body before retrying to prevent connection leaks + if res != nil && res.Body != nil { + res.Body.Close() + } + time.Sleep(retryDelay(res, retryCount)) } diff --git a/packages/sdk/go/internal/version.go b/packages/sdk/go/internal/version.go index 64dcebbb..3c8392e9 100644 --- a/packages/sdk/go/internal/version.go +++ b/packages/sdk/go/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.1.0-alpha.8" // x-release-please-version +const PackageVersion = "0.8.0" // x-release-please-version diff --git a/packages/sdk/go/path.go b/packages/sdk/go/path.go new file mode 100644 index 00000000..63e50262 --- /dev/null +++ b/packages/sdk/go/path.go @@ -0,0 +1,80 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "net/url" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// PathService contains methods and other services that help with interacting with +// the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewPathService] method instead. +type PathService struct { + Options []option.RequestOption +} + +// NewPathService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewPathService(opts ...option.RequestOption) (r *PathService) { + r = &PathService{} + r.Options = opts + return +} + +// Get the current path +func (r *PathService) Get(ctx context.Context, query PathGetParams, opts ...option.RequestOption) (res *Path, err error) { + opts = append(r.Options[:], opts...) + path := "path" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +type Path struct { + Config string `json:"config,required"` + Directory string `json:"directory,required"` + State string `json:"state,required"` + Worktree string `json:"worktree,required"` + JSON pathJSON `json:"-"` +} + +// pathJSON contains the JSON metadata for the struct [Path] +type pathJSON struct { + Config apijson.Field + Directory apijson.Field + State apijson.Field + Worktree apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Path) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r pathJSON) RawJSON() string { + return r.raw +} + +type PathGetParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [PathGetParams]'s query parameters as `url.Values`. +func (r PathGetParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/path_test.go b/packages/sdk/go/path_test.go new file mode 100644 index 00000000..08273ce3 --- /dev/null +++ b/packages/sdk/go/path_test.go @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestPathGetWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Path.Get(context.TODO(), opencode.PathGetParams{ + Directory: opencode.F("directory"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/sdk/go/project.go b/packages/sdk/go/project.go new file mode 100644 index 00000000..3b349dad --- /dev/null +++ b/packages/sdk/go/project.go @@ -0,0 +1,136 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "net/url" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// ProjectService contains methods and other services that help with interacting +// with the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewProjectService] method instead. +type ProjectService struct { + Options []option.RequestOption +} + +// NewProjectService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewProjectService(opts ...option.RequestOption) (r *ProjectService) { + r = &ProjectService{} + r.Options = opts + return +} + +// List all projects +func (r *ProjectService) List(ctx context.Context, query ProjectListParams, opts ...option.RequestOption) (res *[]Project, err error) { + opts = append(r.Options[:], opts...) + path := "project" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// Get the current project +func (r *ProjectService) Current(ctx context.Context, query ProjectCurrentParams, opts ...option.RequestOption) (res *Project, err error) { + opts = append(r.Options[:], opts...) + path := "project/current" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +type Project struct { + ID string `json:"id,required"` + Time ProjectTime `json:"time,required"` + Worktree string `json:"worktree,required"` + Vcs ProjectVcs `json:"vcs"` + JSON projectJSON `json:"-"` +} + +// projectJSON contains the JSON metadata for the struct [Project] +type projectJSON struct { + ID apijson.Field + Time apijson.Field + Worktree apijson.Field + Vcs apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Project) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r projectJSON) RawJSON() string { + return r.raw +} + +type ProjectTime struct { + Created float64 `json:"created,required"` + Initialized float64 `json:"initialized"` + JSON projectTimeJSON `json:"-"` +} + +// projectTimeJSON contains the JSON metadata for the struct [ProjectTime] +type projectTimeJSON struct { + Created apijson.Field + Initialized apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ProjectTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r projectTimeJSON) RawJSON() string { + return r.raw +} + +type ProjectVcs string + +const ( + ProjectVcsGit ProjectVcs = "git" +) + +func (r ProjectVcs) IsKnown() bool { + switch r { + case ProjectVcsGit: + return true + } + return false +} + +type ProjectListParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [ProjectListParams]'s query parameters as `url.Values`. +func (r ProjectListParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type ProjectCurrentParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [ProjectCurrentParams]'s query parameters as `url.Values`. +func (r ProjectCurrentParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/project_test.go b/packages/sdk/go/project_test.go new file mode 100644 index 00000000..adf3dbf1 --- /dev/null +++ b/packages/sdk/go/project_test.go @@ -0,0 +1,62 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestProjectListWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Project.List(context.TODO(), opencode.ProjectListParams{ + Directory: opencode.F("directory"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestProjectCurrentWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Project.Current(context.TODO(), opencode.ProjectCurrentParams{ + Directory: opencode.F("directory"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go index 237b490d..88d71b57 100644 --- a/packages/sdk/go/session.go +++ b/packages/sdk/go/session.go @@ -7,9 +7,11 @@ import ( "errors" "fmt" "net/http" + "net/url" "reflect" "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" "github.com/sst/opencode-sdk-go/internal/param" "github.com/sst/opencode-sdk-go/internal/requestconfig" "github.com/sst/opencode-sdk-go/option" @@ -39,119 +41,107 @@ func NewSessionService(opts ...option.RequestOption) (r *SessionService) { } // Create a new session -func (r *SessionService) New(ctx context.Context, body SessionNewParams, opts ...option.RequestOption) (res *Session, err error) { +func (r *SessionService) New(ctx context.Context, params SessionNewParams, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) path := "session" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Update session properties -func (r *SessionService) Update(ctx context.Context, id string, body SessionUpdateParams, opts ...option.RequestOption) (res *Session, err error) { +func (r *SessionService) Update(ctx context.Context, id string, params SessionUpdateParams, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, params, &res, opts...) return } // List all sessions -func (r *SessionService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Session, err error) { +func (r *SessionService) List(ctx context.Context, query SessionListParams, opts ...option.RequestOption) (res *[]Session, err error) { opts = append(r.Options[:], opts...) path := "session" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } // Delete a session and all its data -func (r *SessionService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *bool, err error) { +func (r *SessionService) Delete(ctx context.Context, id string, body SessionDeleteParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, body, &res, opts...) return } // Abort a session -func (r *SessionService) Abort(ctx context.Context, id string, opts ...option.RequestOption) (res *bool, err error) { +func (r *SessionService) Abort(ctx context.Context, id string, body SessionAbortParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/abort", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return -} - -// Create and send a new message to a session -func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *SessionChatResponse, err error) { - opts = append(r.Options[:], opts...) - if id == "" { - err = errors.New("missing required id parameter") - return - } - path := fmt.Sprintf("session/%s/message", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return } // Get a session's children -func (r *SessionService) Children(ctx context.Context, id string, opts ...option.RequestOption) (res *[]Session, err error) { +func (r *SessionService) Children(ctx context.Context, id string, query SessionChildrenParams, opts ...option.RequestOption) (res *[]Session, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/children", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } // Send a new command to a session -func (r *SessionService) Command(ctx context.Context, id string, body SessionCommandParams, opts ...option.RequestOption) (res *SessionCommandResponse, err error) { +func (r *SessionService) Command(ctx context.Context, id string, params SessionCommandParams, opts ...option.RequestOption) (res *SessionCommandResponse, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/command", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Get session -func (r *SessionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { +func (r *SessionService) Get(ctx context.Context, id string, query SessionGetParams, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } // Analyze the app and create an AGENTS.md file -func (r *SessionService) Init(ctx context.Context, id string, body SessionInitParams, opts ...option.RequestOption) (res *bool, err error) { +func (r *SessionService) Init(ctx context.Context, id string, params SessionInitParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/init", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Get a message from a session -func (r *SessionService) Message(ctx context.Context, id string, messageID string, opts ...option.RequestOption) (res *SessionMessageResponse, err error) { +func (r *SessionService) Message(ctx context.Context, id string, messageID string, query SessionMessageParams, opts ...option.RequestOption) (res *SessionMessageResponse, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") @@ -162,91 +152,103 @@ func (r *SessionService) Message(ctx context.Context, id string, messageID strin return } path := fmt.Sprintf("session/%s/message/%s", id, messageID) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return } // List messages for a session -func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) { +func (r *SessionService) Messages(ctx context.Context, id string, query SessionMessagesParams, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/message", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// Create and send a new message to a session +func (r *SessionService) Prompt(ctx context.Context, id string, params SessionPromptParams, opts ...option.RequestOption) (res *SessionPromptResponse, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/message", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Revert a message -func (r *SessionService) Revert(ctx context.Context, id string, body SessionRevertParams, opts ...option.RequestOption) (res *Session, err error) { +func (r *SessionService) Revert(ctx context.Context, id string, params SessionRevertParams, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/revert", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Share a session -func (r *SessionService) Share(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { +func (r *SessionService) Share(ctx context.Context, id string, body SessionShareParams, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/share", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return } // Run a shell command -func (r *SessionService) Shell(ctx context.Context, id string, body SessionShellParams, opts ...option.RequestOption) (res *AssistantMessage, err error) { +func (r *SessionService) Shell(ctx context.Context, id string, params SessionShellParams, opts ...option.RequestOption) (res *AssistantMessage, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/shell", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Summarize the session -func (r *SessionService) Summarize(ctx context.Context, id string, body SessionSummarizeParams, opts ...option.RequestOption) (res *bool, err error) { +func (r *SessionService) Summarize(ctx context.Context, id string, params SessionSummarizeParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/summarize", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Restore all reverted messages -func (r *SessionService) Unrevert(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { +func (r *SessionService) Unrevert(ctx context.Context, id string, body SessionUnrevertParams, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/unrevert", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return } // Unshare the session -func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { +func (r *SessionService) Unshare(ctx context.Context, id string, body SessionUnshareParams, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") return } path := fmt.Sprintf("session/%s/share", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, body, &res, opts...) return } @@ -331,7 +333,7 @@ func (r AgentPartInputParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r AgentPartInputParam) implementsSessionChatParamsPartUnion() {} +func (r AgentPartInputParam) implementsSessionPromptParamsPartUnion() {} type AgentPartInputType string @@ -707,7 +709,7 @@ func (r FilePartInputParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r FilePartInputParam) implementsSessionChatParamsPartUnion() {} +func (r FilePartInputParam) implementsSessionPromptParamsPartUnion() {} type FilePartInputType string @@ -1294,19 +1296,23 @@ func (r ReasoningPartType) IsKnown() bool { } type Session struct { - ID string `json:"id,required"` - Time SessionTime `json:"time,required"` - Title string `json:"title,required"` - Version string `json:"version,required"` - ParentID string `json:"parentID"` - Revert SessionRevert `json:"revert"` - Share SessionShare `json:"share"` - JSON sessionJSON `json:"-"` + ID string `json:"id,required"` + Directory string `json:"directory,required"` + ProjectID string `json:"projectID,required"` + Time SessionTime `json:"time,required"` + Title string `json:"title,required"` + Version string `json:"version,required"` + ParentID string `json:"parentID"` + Revert SessionRevert `json:"revert"` + Share SessionShare `json:"share"` + JSON sessionJSON `json:"-"` } // sessionJSON contains the JSON metadata for the struct [Session] type sessionJSON struct { ID apijson.Field + Directory apijson.Field + ProjectID apijson.Field Time apijson.Field Title apijson.Field Version apijson.Field @@ -1814,7 +1820,7 @@ func (r TextPartInputParam) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -func (r TextPartInputParam) implementsSessionChatParamsPartUnion() {} +func (r TextPartInputParam) implementsSessionPromptParamsPartUnion() {} type TextPartInputType string @@ -2290,29 +2296,6 @@ func (r userMessageTimeJSON) RawJSON() string { return r.raw } -type SessionChatResponse struct { - Info AssistantMessage `json:"info,required"` - Parts []Part `json:"parts,required"` - JSON sessionChatResponseJSON `json:"-"` -} - -// sessionChatResponseJSON contains the JSON metadata for the struct -// [SessionChatResponse] -type sessionChatResponseJSON struct { - Info apijson.Field - Parts apijson.Field - raw string - ExtraFields map[string]apijson.Field -} - -func (r *SessionChatResponse) UnmarshalJSON(data []byte) (err error) { - return apijson.UnmarshalRoot(data, r) -} - -func (r sessionChatResponseJSON) RawJSON() string { - return r.raw -} - type SessionCommandResponse struct { Info AssistantMessage `json:"info,required"` Parts []Part `json:"parts,required"` @@ -2382,81 +2365,116 @@ func (r sessionMessagesResponseJSON) RawJSON() string { return r.raw } +type SessionPromptResponse struct { + Info AssistantMessage `json:"info,required"` + Parts []Part `json:"parts,required"` + JSON sessionPromptResponseJSON `json:"-"` +} + +// sessionPromptResponseJSON contains the JSON metadata for the struct +// [SessionPromptResponse] +type sessionPromptResponseJSON struct { + Info apijson.Field + Parts apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionPromptResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionPromptResponseJSON) RawJSON() string { + return r.raw +} + type SessionNewParams struct { - ParentID param.Field[string] `json:"parentID"` - Title param.Field[string] `json:"title"` + Directory param.Field[string] `query:"directory"` + ParentID param.Field[string] `json:"parentID"` + Title param.Field[string] `json:"title"` } func (r SessionNewParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [SessionNewParams]'s query parameters as `url.Values`. +func (r SessionNewParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type SessionUpdateParams struct { - Title param.Field[string] `json:"title"` + Directory param.Field[string] `query:"directory"` + Title param.Field[string] `json:"title"` } func (r SessionUpdateParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } -type SessionChatParams struct { - ModelID param.Field[string] `json:"modelID,required"` - Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"` - ProviderID param.Field[string] `json:"providerID,required"` - Agent param.Field[string] `json:"agent"` - MessageID param.Field[string] `json:"messageID"` - System param.Field[string] `json:"system"` - Tools param.Field[map[string]bool] `json:"tools"` +// URLQuery serializes [SessionUpdateParams]'s query parameters as `url.Values`. +func (r SessionUpdateParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) } -func (r SessionChatParams) MarshalJSON() (data []byte, err error) { - return apijson.MarshalRoot(r) +type SessionListParams struct { + Directory param.Field[string] `query:"directory"` } -type SessionChatParamsPart struct { - Type param.Field[SessionChatParamsPartsType] `json:"type,required"` - ID param.Field[string] `json:"id"` - Filename param.Field[string] `json:"filename"` - Mime param.Field[string] `json:"mime"` - Name param.Field[string] `json:"name"` - Source param.Field[interface{}] `json:"source"` - Synthetic param.Field[bool] `json:"synthetic"` - Text param.Field[string] `json:"text"` - Time param.Field[interface{}] `json:"time"` - URL param.Field[string] `json:"url"` +// URLQuery serializes [SessionListParams]'s query parameters as `url.Values`. +func (r SessionListParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) } -func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) { - return apijson.MarshalRoot(r) +type SessionDeleteParams struct { + Directory param.Field[string] `query:"directory"` } -func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {} - -// Satisfied by [TextPartInputParam], [FilePartInputParam], [AgentPartInputParam], -// [SessionChatParamsPart]. -type SessionChatParamsPartUnion interface { - implementsSessionChatParamsPartUnion() +// URLQuery serializes [SessionDeleteParams]'s query parameters as `url.Values`. +func (r SessionDeleteParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) } -type SessionChatParamsPartsType string +type SessionAbortParams struct { + Directory param.Field[string] `query:"directory"` +} -const ( - SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text" - SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file" - SessionChatParamsPartsTypeAgent SessionChatParamsPartsType = "agent" -) +// URLQuery serializes [SessionAbortParams]'s query parameters as `url.Values`. +func (r SessionAbortParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} -func (r SessionChatParamsPartsType) IsKnown() bool { - switch r { - case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeAgent: - return true - } - return false +type SessionChildrenParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [SessionChildrenParams]'s query parameters as `url.Values`. +func (r SessionChildrenParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) } type SessionCommandParams struct { Arguments param.Field[string] `json:"arguments,required"` Command param.Field[string] `json:"command,required"` + Directory param.Field[string] `query:"directory"` Agent param.Field[string] `json:"agent"` MessageID param.Field[string] `json:"messageID"` Model param.Field[string] `json:"model"` @@ -2466,18 +2484,144 @@ func (r SessionCommandParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [SessionCommandParams]'s query parameters as `url.Values`. +func (r SessionCommandParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionGetParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [SessionGetParams]'s query parameters as `url.Values`. +func (r SessionGetParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type SessionInitParams struct { MessageID param.Field[string] `json:"messageID,required"` ModelID param.Field[string] `json:"modelID,required"` ProviderID param.Field[string] `json:"providerID,required"` + Directory param.Field[string] `query:"directory"` } func (r SessionInitParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [SessionInitParams]'s query parameters as `url.Values`. +func (r SessionInitParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionMessageParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [SessionMessageParams]'s query parameters as `url.Values`. +func (r SessionMessageParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionMessagesParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [SessionMessagesParams]'s query parameters as `url.Values`. +func (r SessionMessagesParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionPromptParams struct { + Parts param.Field[[]SessionPromptParamsPartUnion] `json:"parts,required"` + Directory param.Field[string] `query:"directory"` + Agent param.Field[string] `json:"agent"` + MessageID param.Field[string] `json:"messageID"` + Model param.Field[SessionPromptParamsModel] `json:"model"` + System param.Field[string] `json:"system"` + Tools param.Field[map[string]bool] `json:"tools"` +} + +func (r SessionPromptParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +// URLQuery serializes [SessionPromptParams]'s query parameters as `url.Values`. +func (r SessionPromptParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionPromptParamsPart struct { + Type param.Field[SessionPromptParamsPartsType] `json:"type,required"` + ID param.Field[string] `json:"id"` + Filename param.Field[string] `json:"filename"` + Mime param.Field[string] `json:"mime"` + Name param.Field[string] `json:"name"` + Source param.Field[interface{}] `json:"source"` + Synthetic param.Field[bool] `json:"synthetic"` + Text param.Field[string] `json:"text"` + Time param.Field[interface{}] `json:"time"` + URL param.Field[string] `json:"url"` +} + +func (r SessionPromptParamsPart) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r SessionPromptParamsPart) implementsSessionPromptParamsPartUnion() {} + +// Satisfied by [TextPartInputParam], [FilePartInputParam], [AgentPartInputParam], +// [SessionPromptParamsPart]. +type SessionPromptParamsPartUnion interface { + implementsSessionPromptParamsPartUnion() +} + +type SessionPromptParamsPartsType string + +const ( + SessionPromptParamsPartsTypeText SessionPromptParamsPartsType = "text" + SessionPromptParamsPartsTypeFile SessionPromptParamsPartsType = "file" + SessionPromptParamsPartsTypeAgent SessionPromptParamsPartsType = "agent" +) + +func (r SessionPromptParamsPartsType) IsKnown() bool { + switch r { + case SessionPromptParamsPartsTypeText, SessionPromptParamsPartsTypeFile, SessionPromptParamsPartsTypeAgent: + return true + } + return false +} + +type SessionPromptParamsModel struct { + ModelID param.Field[string] `json:"modelID,required"` + ProviderID param.Field[string] `json:"providerID,required"` +} + +func (r SessionPromptParamsModel) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + type SessionRevertParams struct { MessageID param.Field[string] `json:"messageID,required"` + Directory param.Field[string] `query:"directory"` PartID param.Field[string] `json:"partID"` } @@ -2485,20 +2629,82 @@ func (r SessionRevertParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [SessionRevertParams]'s query parameters as `url.Values`. +func (r SessionRevertParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionShareParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [SessionShareParams]'s query parameters as `url.Values`. +func (r SessionShareParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type SessionShellParams struct { - Agent param.Field[string] `json:"agent,required"` - Command param.Field[string] `json:"command,required"` + Agent param.Field[string] `json:"agent,required"` + Command param.Field[string] `json:"command,required"` + Directory param.Field[string] `query:"directory"` } func (r SessionShellParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [SessionShellParams]'s query parameters as `url.Values`. +func (r SessionShellParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type SessionSummarizeParams struct { ModelID param.Field[string] `json:"modelID,required"` ProviderID param.Field[string] `json:"providerID,required"` + Directory param.Field[string] `query:"directory"` } func (r SessionSummarizeParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } + +// URLQuery serializes [SessionSummarizeParams]'s query parameters as `url.Values`. +func (r SessionSummarizeParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionUnrevertParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [SessionUnrevertParams]'s query parameters as `url.Values`. +func (r SessionUnrevertParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type SessionUnshareParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [SessionUnshareParams]'s query parameters as `url.Values`. +func (r SessionUnshareParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/session_test.go b/packages/sdk/go/session_test.go index d67be255..f4cbc04b 100644 --- a/packages/sdk/go/session_test.go +++ b/packages/sdk/go/session_test.go @@ -14,7 +14,7 @@ import ( ) func TestSessionNewWithOptionalParams(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -26,8 +26,9 @@ func TestSessionNewWithOptionalParams(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.Session.New(context.TODO(), opencode.SessionNewParams{ - ParentID: opencode.F("parentID"), - Title: opencode.F("title"), + Directory: opencode.F("directory"), + ParentID: opencode.F("parentID"), + Title: opencode.F("title"), }) if err != nil { var apierr *opencode.Error @@ -39,7 +40,7 @@ func TestSessionNewWithOptionalParams(t *testing.T) { } func TestSessionUpdateWithOptionalParams(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -54,7 +55,8 @@ func TestSessionUpdateWithOptionalParams(t *testing.T) { context.TODO(), "id", opencode.SessionUpdateParams{ - Title: opencode.F("title"), + Directory: opencode.F("directory"), + Title: opencode.F("title"), }, ) if err != nil { @@ -66,8 +68,8 @@ func TestSessionUpdateWithOptionalParams(t *testing.T) { } } -func TestSessionList(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionListWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -78,7 +80,9 @@ func TestSessionList(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.List(context.TODO()) + _, err := client.Session.List(context.TODO(), opencode.SessionListParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -88,8 +92,8 @@ func TestSessionList(t *testing.T) { } } -func TestSessionDelete(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionDeleteWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -100,72 +104,11 @@ func TestSessionDelete(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.Delete(context.TODO(), "id") - if err != nil { - var apierr *opencode.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} - -func TestSessionAbort(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := opencode.NewClient( - option.WithBaseURL(baseURL), - ) - _, err := client.Session.Abort(context.TODO(), "id") - if err != nil { - var apierr *opencode.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} - -func TestSessionChatWithOptionalParams(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := opencode.NewClient( - option.WithBaseURL(baseURL), - ) - _, err := client.Session.Chat( + _, err := client.Session.Delete( context.TODO(), "id", - opencode.SessionChatParams{ - ModelID: opencode.F("modelID"), - Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.TextPartInputParam{ - Text: opencode.F("text"), - Type: opencode.F(opencode.TextPartInputTypeText), - ID: opencode.F("id"), - Synthetic: opencode.F(true), - Time: opencode.F(opencode.TextPartInputTimeParam{ - Start: opencode.F(0.000000), - End: opencode.F(0.000000), - }), - }}), - ProviderID: opencode.F("providerID"), - Agent: opencode.F("agent"), - MessageID: opencode.F("msg"), - System: opencode.F("system"), - Tools: opencode.F(map[string]bool{ - "foo": true, - }), + opencode.SessionDeleteParams{ + Directory: opencode.F("directory"), }, ) if err != nil { @@ -177,8 +120,8 @@ func TestSessionChatWithOptionalParams(t *testing.T) { } } -func TestSessionChildren(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionAbortWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -189,7 +132,41 @@ func TestSessionChildren(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.Children(context.TODO(), "id") + _, err := client.Session.Abort( + context.TODO(), + "id", + opencode.SessionAbortParams{ + Directory: opencode.F("directory"), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionChildrenWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Children( + context.TODO(), + "id", + opencode.SessionChildrenParams{ + Directory: opencode.F("directory"), + }, + ) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -200,7 +177,7 @@ func TestSessionChildren(t *testing.T) { } func TestSessionCommandWithOptionalParams(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -217,6 +194,7 @@ func TestSessionCommandWithOptionalParams(t *testing.T) { opencode.SessionCommandParams{ Arguments: opencode.F("arguments"), Command: opencode.F("command"), + Directory: opencode.F("directory"), Agent: opencode.F("agent"), MessageID: opencode.F("msg"), Model: opencode.F("model"), @@ -231,8 +209,8 @@ func TestSessionCommandWithOptionalParams(t *testing.T) { } } -func TestSessionGet(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionGetWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -243,7 +221,13 @@ func TestSessionGet(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.Get(context.TODO(), "id") + _, err := client.Session.Get( + context.TODO(), + "id", + opencode.SessionGetParams{ + Directory: opencode.F("directory"), + }, + ) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -253,8 +237,8 @@ func TestSessionGet(t *testing.T) { } } -func TestSessionInit(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionInitWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -272,6 +256,7 @@ func TestSessionInit(t *testing.T) { MessageID: opencode.F("messageID"), ModelID: opencode.F("modelID"), ProviderID: opencode.F("providerID"), + Directory: opencode.F("directory"), }, ) if err != nil { @@ -283,8 +268,8 @@ func TestSessionInit(t *testing.T) { } } -func TestSessionMessage(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionMessageWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -299,6 +284,9 @@ func TestSessionMessage(t *testing.T) { context.TODO(), "id", "messageID", + opencode.SessionMessageParams{ + Directory: opencode.F("directory"), + }, ) if err != nil { var apierr *opencode.Error @@ -309,8 +297,8 @@ func TestSessionMessage(t *testing.T) { } } -func TestSessionMessages(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionMessagesWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -321,7 +309,61 @@ func TestSessionMessages(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.Messages(context.TODO(), "id") + _, err := client.Session.Messages( + context.TODO(), + "id", + opencode.SessionMessagesParams{ + Directory: opencode.F("directory"), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionPromptWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Prompt( + context.TODO(), + "id", + opencode.SessionPromptParams{ + Parts: opencode.F([]opencode.SessionPromptParamsPartUnion{opencode.TextPartInputParam{ + Text: opencode.F("text"), + Type: opencode.F(opencode.TextPartInputTypeText), + ID: opencode.F("id"), + Synthetic: opencode.F(true), + Time: opencode.F(opencode.TextPartInputTimeParam{ + Start: opencode.F(0.000000), + End: opencode.F(0.000000), + }), + }}), + Directory: opencode.F("directory"), + Agent: opencode.F("agent"), + MessageID: opencode.F("msg"), + Model: opencode.F(opencode.SessionPromptParamsModel{ + ModelID: opencode.F("modelID"), + ProviderID: opencode.F("providerID"), + }), + System: opencode.F("system"), + Tools: opencode.F(map[string]bool{ + "foo": true, + }), + }, + ) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -332,7 +374,7 @@ func TestSessionMessages(t *testing.T) { } func TestSessionRevertWithOptionalParams(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -348,6 +390,7 @@ func TestSessionRevertWithOptionalParams(t *testing.T) { "id", opencode.SessionRevertParams{ MessageID: opencode.F("msg"), + Directory: opencode.F("directory"), PartID: opencode.F("prt"), }, ) @@ -360,8 +403,8 @@ func TestSessionRevertWithOptionalParams(t *testing.T) { } } -func TestSessionShare(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionShareWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -372,7 +415,13 @@ func TestSessionShare(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.Share(context.TODO(), "id") + _, err := client.Session.Share( + context.TODO(), + "id", + opencode.SessionShareParams{ + Directory: opencode.F("directory"), + }, + ) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -382,8 +431,8 @@ func TestSessionShare(t *testing.T) { } } -func TestSessionShell(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionShellWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -398,8 +447,9 @@ func TestSessionShell(t *testing.T) { context.TODO(), "id", opencode.SessionShellParams{ - Agent: opencode.F("agent"), - Command: opencode.F("command"), + Agent: opencode.F("agent"), + Command: opencode.F("command"), + Directory: opencode.F("directory"), }, ) if err != nil { @@ -411,8 +461,8 @@ func TestSessionShell(t *testing.T) { } } -func TestSessionSummarize(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionSummarizeWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -429,6 +479,7 @@ func TestSessionSummarize(t *testing.T) { opencode.SessionSummarizeParams{ ModelID: opencode.F("modelID"), ProviderID: opencode.F("providerID"), + Directory: opencode.F("directory"), }, ) if err != nil { @@ -440,8 +491,8 @@ func TestSessionSummarize(t *testing.T) { } } -func TestSessionUnrevert(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionUnrevertWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -452,7 +503,13 @@ func TestSessionUnrevert(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.Unrevert(context.TODO(), "id") + _, err := client.Session.Unrevert( + context.TODO(), + "id", + opencode.SessionUnrevertParams{ + Directory: opencode.F("directory"), + }, + ) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -462,8 +519,8 @@ func TestSessionUnrevert(t *testing.T) { } } -func TestSessionUnshare(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionUnshareWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -474,7 +531,13 @@ func TestSessionUnshare(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Session.Unshare(context.TODO(), "id") + _, err := client.Session.Unshare( + context.TODO(), + "id", + opencode.SessionUnshareParams{ + Directory: opencode.F("directory"), + }, + ) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { diff --git a/packages/sdk/go/sessionpermission.go b/packages/sdk/go/sessionpermission.go index 85e55bd5..4d49bd87 100644 --- a/packages/sdk/go/sessionpermission.go +++ b/packages/sdk/go/sessionpermission.go @@ -7,8 +7,10 @@ import ( "errors" "fmt" "net/http" + "net/url" "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" "github.com/sst/opencode-sdk-go/internal/param" "github.com/sst/opencode-sdk-go/internal/requestconfig" "github.com/sst/opencode-sdk-go/option" @@ -34,7 +36,7 @@ func NewSessionPermissionService(opts ...option.RequestOption) (r *SessionPermis } // Respond to a permission request -func (r *SessionPermissionService) Respond(ctx context.Context, id string, permissionID string, body SessionPermissionRespondParams, opts ...option.RequestOption) (res *bool, err error) { +func (r *SessionPermissionService) Respond(ctx context.Context, id string, permissionID string, params SessionPermissionRespondParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) if id == "" { err = errors.New("missing required id parameter") @@ -45,7 +47,7 @@ func (r *SessionPermissionService) Respond(ctx context.Context, id string, permi return } path := fmt.Sprintf("session/%s/permissions/%s", id, permissionID) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } @@ -106,13 +108,23 @@ func (r permissionTimeJSON) RawJSON() string { } type SessionPermissionRespondParams struct { - Response param.Field[SessionPermissionRespondParamsResponse] `json:"response,required"` + Response param.Field[SessionPermissionRespondParamsResponse] `json:"response,required"` + Directory param.Field[string] `query:"directory"` } func (r SessionPermissionRespondParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [SessionPermissionRespondParams]'s query parameters as +// `url.Values`. +func (r SessionPermissionRespondParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type SessionPermissionRespondParamsResponse string const ( diff --git a/packages/sdk/go/sessionpermission_test.go b/packages/sdk/go/sessionpermission_test.go index 728976be..ed396b50 100644 --- a/packages/sdk/go/sessionpermission_test.go +++ b/packages/sdk/go/sessionpermission_test.go @@ -13,8 +13,8 @@ import ( "github.com/sst/opencode-sdk-go/option" ) -func TestSessionPermissionRespond(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestSessionPermissionRespondWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -30,7 +30,8 @@ func TestSessionPermissionRespond(t *testing.T) { "id", "permissionID", opencode.SessionPermissionRespondParams{ - Response: opencode.F(opencode.SessionPermissionRespondParamsResponseOnce), + Response: opencode.F(opencode.SessionPermissionRespondParamsResponseOnce), + Directory: opencode.F("directory"), }, ) if err != nil { diff --git a/packages/sdk/go/shared/union.go b/packages/sdk/go/shared/union.go new file mode 100644 index 00000000..91c73305 --- /dev/null +++ b/packages/sdk/go/shared/union.go @@ -0,0 +1,11 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package shared + +type UnionBool bool + +func (UnionBool) ImplementsConfigProviderOptionsTimeoutUnion() {} + +type UnionInt int64 + +func (UnionInt) ImplementsConfigProviderOptionsTimeoutUnion() {} diff --git a/packages/sdk/go/tui.go b/packages/sdk/go/tui.go index ab5ed640..b7a8483f 100644 --- a/packages/sdk/go/tui.go +++ b/packages/sdk/go/tui.go @@ -5,8 +5,10 @@ package opencode import ( "context" "net/http" + "net/url" "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" "github.com/sst/opencode-sdk-go/internal/param" "github.com/sst/opencode-sdk-go/internal/requestconfig" "github.com/sst/opencode-sdk-go/option" @@ -32,103 +34,191 @@ func NewTuiService(opts ...option.RequestOption) (r *TuiService) { } // Append prompt to the TUI -func (r *TuiService) AppendPrompt(ctx context.Context, body TuiAppendPromptParams, opts ...option.RequestOption) (res *bool, err error) { +func (r *TuiService) AppendPrompt(ctx context.Context, params TuiAppendPromptParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) path := "tui/append-prompt" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Clear the prompt -func (r *TuiService) ClearPrompt(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { +func (r *TuiService) ClearPrompt(ctx context.Context, body TuiClearPromptParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) path := "tui/clear-prompt" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return } // Execute a TUI command (e.g. agent_cycle) -func (r *TuiService) ExecuteCommand(ctx context.Context, body TuiExecuteCommandParams, opts ...option.RequestOption) (res *bool, err error) { +func (r *TuiService) ExecuteCommand(ctx context.Context, params TuiExecuteCommandParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) path := "tui/execute-command" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) return } // Open the help dialog -func (r *TuiService) OpenHelp(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { +func (r *TuiService) OpenHelp(ctx context.Context, body TuiOpenHelpParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) path := "tui/open-help" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return -} - -// Open the model dialog -func (r *TuiService) OpenModels(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { - opts = append(r.Options[:], opts...) - path := "tui/open-models" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return -} - -// Open the session dialog -func (r *TuiService) OpenSessions(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { - opts = append(r.Options[:], opts...) - path := "tui/open-sessions" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return -} - -// Open the theme dialog -func (r *TuiService) OpenThemes(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { - opts = append(r.Options[:], opts...) - path := "tui/open-themes" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return -} - -// Show a toast notification in the TUI -func (r *TuiService) ShowToast(ctx context.Context, body TuiShowToastParams, opts ...option.RequestOption) (res *bool, err error) { - opts = append(r.Options[:], opts...) - path := "tui/show-toast" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return } +// Open the model dialog +func (r *TuiService) OpenModels(ctx context.Context, body TuiOpenModelsParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + path := "tui/open-models" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Open the session dialog +func (r *TuiService) OpenSessions(ctx context.Context, body TuiOpenSessionsParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + path := "tui/open-sessions" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Open the theme dialog +func (r *TuiService) OpenThemes(ctx context.Context, body TuiOpenThemesParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + path := "tui/open-themes" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Show a toast notification in the TUI +func (r *TuiService) ShowToast(ctx context.Context, params TuiShowToastParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + path := "tui/show-toast" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) + return +} + // Submit the prompt -func (r *TuiService) SubmitPrompt(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { +func (r *TuiService) SubmitPrompt(ctx context.Context, body TuiSubmitPromptParams, opts ...option.RequestOption) (res *bool, err error) { opts = append(r.Options[:], opts...) path := "tui/submit-prompt" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return } type TuiAppendPromptParams struct { - Text param.Field[string] `json:"text,required"` + Text param.Field[string] `json:"text,required"` + Directory param.Field[string] `query:"directory"` } func (r TuiAppendPromptParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [TuiAppendPromptParams]'s query parameters as `url.Values`. +func (r TuiAppendPromptParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type TuiClearPromptParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [TuiClearPromptParams]'s query parameters as `url.Values`. +func (r TuiClearPromptParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type TuiExecuteCommandParams struct { - Command param.Field[string] `json:"command,required"` + Command param.Field[string] `json:"command,required"` + Directory param.Field[string] `query:"directory"` } func (r TuiExecuteCommandParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [TuiExecuteCommandParams]'s query parameters as +// `url.Values`. +func (r TuiExecuteCommandParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type TuiOpenHelpParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [TuiOpenHelpParams]'s query parameters as `url.Values`. +func (r TuiOpenHelpParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type TuiOpenModelsParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [TuiOpenModelsParams]'s query parameters as `url.Values`. +func (r TuiOpenModelsParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type TuiOpenSessionsParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [TuiOpenSessionsParams]'s query parameters as `url.Values`. +func (r TuiOpenSessionsParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type TuiOpenThemesParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [TuiOpenThemesParams]'s query parameters as `url.Values`. +func (r TuiOpenThemesParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type TuiShowToastParams struct { - Message param.Field[string] `json:"message,required"` - Variant param.Field[TuiShowToastParamsVariant] `json:"variant,required"` - Title param.Field[string] `json:"title"` + Message param.Field[string] `json:"message,required"` + Variant param.Field[TuiShowToastParamsVariant] `json:"variant,required"` + Directory param.Field[string] `query:"directory"` + Title param.Field[string] `json:"title"` } func (r TuiShowToastParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } +// URLQuery serializes [TuiShowToastParams]'s query parameters as `url.Values`. +func (r TuiShowToastParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type TuiShowToastParamsVariant string const ( @@ -145,3 +235,15 @@ func (r TuiShowToastParamsVariant) IsKnown() bool { } return false } + +type TuiSubmitPromptParams struct { + Directory param.Field[string] `query:"directory"` +} + +// URLQuery serializes [TuiSubmitPromptParams]'s query parameters as `url.Values`. +func (r TuiSubmitPromptParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/sdk/go/tui_test.go b/packages/sdk/go/tui_test.go index cb482226..635473ba 100644 --- a/packages/sdk/go/tui_test.go +++ b/packages/sdk/go/tui_test.go @@ -13,8 +13,8 @@ import ( "github.com/sst/opencode-sdk-go/option" ) -func TestTuiAppendPrompt(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiAppendPromptWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -26,7 +26,8 @@ func TestTuiAppendPrompt(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.Tui.AppendPrompt(context.TODO(), opencode.TuiAppendPromptParams{ - Text: opencode.F("text"), + Text: opencode.F("text"), + Directory: opencode.F("directory"), }) if err != nil { var apierr *opencode.Error @@ -37,8 +38,8 @@ func TestTuiAppendPrompt(t *testing.T) { } } -func TestTuiClearPrompt(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiClearPromptWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -49,7 +50,9 @@ func TestTuiClearPrompt(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Tui.ClearPrompt(context.TODO()) + _, err := client.Tui.ClearPrompt(context.TODO(), opencode.TuiClearPromptParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -59,8 +62,8 @@ func TestTuiClearPrompt(t *testing.T) { } } -func TestTuiExecuteCommand(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiExecuteCommandWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -72,7 +75,8 @@ func TestTuiExecuteCommand(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.Tui.ExecuteCommand(context.TODO(), opencode.TuiExecuteCommandParams{ - Command: opencode.F("command"), + Command: opencode.F("command"), + Directory: opencode.F("directory"), }) if err != nil { var apierr *opencode.Error @@ -83,8 +87,8 @@ func TestTuiExecuteCommand(t *testing.T) { } } -func TestTuiOpenHelp(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiOpenHelpWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -95,7 +99,9 @@ func TestTuiOpenHelp(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Tui.OpenHelp(context.TODO()) + _, err := client.Tui.OpenHelp(context.TODO(), opencode.TuiOpenHelpParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -105,8 +111,8 @@ func TestTuiOpenHelp(t *testing.T) { } } -func TestTuiOpenModels(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiOpenModelsWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -117,7 +123,9 @@ func TestTuiOpenModels(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Tui.OpenModels(context.TODO()) + _, err := client.Tui.OpenModels(context.TODO(), opencode.TuiOpenModelsParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -127,8 +135,8 @@ func TestTuiOpenModels(t *testing.T) { } } -func TestTuiOpenSessions(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiOpenSessionsWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -139,7 +147,9 @@ func TestTuiOpenSessions(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Tui.OpenSessions(context.TODO()) + _, err := client.Tui.OpenSessions(context.TODO(), opencode.TuiOpenSessionsParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -149,8 +159,8 @@ func TestTuiOpenSessions(t *testing.T) { } } -func TestTuiOpenThemes(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiOpenThemesWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -161,7 +171,9 @@ func TestTuiOpenThemes(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Tui.OpenThemes(context.TODO()) + _, err := client.Tui.OpenThemes(context.TODO(), opencode.TuiOpenThemesParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { @@ -172,7 +184,7 @@ func TestTuiOpenThemes(t *testing.T) { } func TestTuiShowToastWithOptionalParams(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -184,9 +196,10 @@ func TestTuiShowToastWithOptionalParams(t *testing.T) { option.WithBaseURL(baseURL), ) _, err := client.Tui.ShowToast(context.TODO(), opencode.TuiShowToastParams{ - Message: opencode.F("message"), - Variant: opencode.F(opencode.TuiShowToastParamsVariantInfo), - Title: opencode.F("title"), + Message: opencode.F("message"), + Variant: opencode.F(opencode.TuiShowToastParamsVariantInfo), + Directory: opencode.F("directory"), + Title: opencode.F("title"), }) if err != nil { var apierr *opencode.Error @@ -197,8 +210,8 @@ func TestTuiShowToastWithOptionalParams(t *testing.T) { } } -func TestTuiSubmitPrompt(t *testing.T) { - t.Skip("skipped: tests are disabled for the time being") +func TestTuiSubmitPromptWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -209,7 +222,9 @@ func TestTuiSubmitPrompt(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - _, err := client.Tui.SubmitPrompt(context.TODO()) + _, err := client.Tui.SubmitPrompt(context.TODO(), opencode.TuiSubmitPromptParams{ + Directory: opencode.F("directory"), + }) if err != nil { var apierr *opencode.Error if errors.As(err, &apierr) { diff --git a/packages/sdk/go/usage_test.go b/packages/sdk/go/usage_test.go index ef7ce8bd..2652b582 100644 --- a/packages/sdk/go/usage_test.go +++ b/packages/sdk/go/usage_test.go @@ -23,7 +23,7 @@ func TestUsage(t *testing.T) { client := opencode.NewClient( option.WithBaseURL(baseURL), ) - sessions, err := client.Session.List(context.TODO()) + sessions, err := client.Session.List(context.TODO(), opencode.SessionListParams{}) if err != nil { t.Error(err) return diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index a50f5e14..ac6be13d 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -2,14 +2,16 @@ import type { Options as ClientOptions, TDataShape, Client } from "./client/index.js" import type { + ProjectListData, + ProjectListResponses, + ProjectCurrentData, + ProjectCurrentResponses, EventSubscribeData, EventSubscribeResponses, - AppGetData, - AppGetResponses, - AppInitData, - AppInitResponses, ConfigGetData, ConfigGetResponses, + PathGetData, + PathGetResponses, SessionListData, SessionListResponses, SessionCreateData, @@ -35,8 +37,8 @@ import type { SessionSummarizeResponses, SessionMessagesData, SessionMessagesResponses, - SessionChatData, - SessionChatResponses, + SessionPromptData, + SessionPromptResponses, SessionMessageData, SessionMessageResponses, SessionCommandData, @@ -120,6 +122,28 @@ class _HeyApiClient { } } +class Project extends _HeyApiClient { + /** + * List all projects + */ + public list(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/project", + ...options, + }) + } + + /** + * Get the current project + */ + public current(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/project/current", + ...options, + }) + } +} + class Event extends _HeyApiClient { /** * Get events @@ -132,52 +156,6 @@ class Event extends _HeyApiClient { } } -class App extends _HeyApiClient { - /** - * Get app info - */ - public get(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/app", - ...options, - }) - } - - /** - * Initialize the app - */ - public init(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/app/init", - ...options, - }) - } - - /** - * Write a log entry to the server logs - */ - public log(options?: Options) { - return (options?.client ?? this._client).post({ - url: "/log", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }) - } - - /** - * List all agents - */ - public agents(options?: Options) { - return (options?.client ?? this._client).get({ - url: "/agent", - ...options, - }) - } -} - class Config extends _HeyApiClient { /** * Get config info @@ -200,6 +178,18 @@ class Config extends _HeyApiClient { } } +class Path extends _HeyApiClient { + /** + * Get the current path + */ + public get(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/path", + ...options, + }) + } +} + class Session extends _HeyApiClient { /** * List all sessions @@ -340,8 +330,8 @@ class Session extends _HeyApiClient { /** * Create and send a new message to a session */ - public chat(options: Options) { - return (options.client ?? this._client).post({ + public prompt(options: Options) { + return (options.client ?? this._client).post({ url: "/session/{id}/message", ...options, headers: { @@ -490,6 +480,32 @@ class File extends _HeyApiClient { } } +class App extends _HeyApiClient { + /** + * Write a log entry to the server logs + */ + public log(options?: Options) { + return (options?.client ?? this._client).post({ + url: "/log", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }) + } + + /** + * List all agents + */ + public agents(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/agent", + ...options, + }) + } +} + class Tui extends _HeyApiClient { /** * Append prompt to the TUI @@ -630,13 +646,15 @@ export class OpencodeClient extends _HeyApiClient { }, }) } + project = new Project({ client: this._client }) event = new Event({ client: this._client }) - app = new App({ client: this._client }) config = new Config({ client: this._client }) + path = new Path({ client: this._client }) session = new Session({ client: this._client }) command = new Command({ client: this._client }) find = new Find({ client: this._client }) file = new File({ client: this._client }) + app = new App({ client: this._client }) tui = new Tui({ client: this._client }) auth = new Auth({ client: this._client }) } diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 434606d8..c56ea309 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1,5 +1,15 @@ // This file is auto-generated by @hey-api/openapi-ts +export type Project = { + id: string + worktree: string + vcs?: "git" + time: { + created: number + initialized?: number + } +} + export type Event = | ({ type: "installation.updated" @@ -19,9 +29,6 @@ export type Event = | ({ type: "message.part.removed" } & EventMessagePartRemoved) - | ({ - type: "storage.write" - } & EventStorageWrite) | ({ type: "permission.updated" } & EventPermissionUpdated) @@ -46,12 +53,6 @@ export type Event = | ({ type: "server.connected" } & EventServerConnected) - | ({ - type: "file.watcher.updated" - } & EventFileWatcherUpdated) - | ({ - type: "ide.installed" - } & EventIdeInstalled) export type EventInstallationUpdated = { type: "installation.updated" @@ -420,14 +421,6 @@ export type EventMessagePartRemoved = { } } -export type EventStorageWrite = { - type: "storage.write" - properties: { - key: string - content?: unknown - } -} - export type EventPermissionUpdated = { type: "permission.updated" properties: Permission @@ -474,6 +467,8 @@ export type EventSessionUpdated = { export type Session = { id: string + projectID: string + directory: string parentID?: string share?: { url: string @@ -533,37 +528,6 @@ export type EventServerConnected = { } } -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "rename" | "change" - } -} - -export type EventIdeInstalled = { - type: "ide.installed" - properties: { - ide: string - } -} - -export type App = { - hostname: string - git: boolean - path: { - home: string - config: string - data: string - root: string - cwd: string - state: string - } - time: { - initialized?: number - } -} - export type Config = { /** * JSON schema reference for configuration validation @@ -681,7 +645,11 @@ export type Config = { options?: { apiKey?: string baseURL?: string - [key: string]: unknown | string | undefined + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + [key: string]: unknown | string | (number | false) | undefined } } } @@ -1085,6 +1053,13 @@ export type McpRemoteConfig = { export type LayoutConfig = "auto" | "stretch" +export type Path = { + state: string + config: string + worktree: string + directory: string +} + export type _Error = { data: { [key: string]: unknown @@ -1209,10 +1184,48 @@ export type WellKnownAuth = { token: string } +export type ProjectListData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/project" +} + +export type ProjectListResponses = { + /** + * List of projects + */ + 200: Array +} + +export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] + +export type ProjectCurrentData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/project/current" +} + +export type ProjectCurrentResponses = { + /** + * Current project + */ + 200: Project +} + +export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] + export type EventSubscribeData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/event" } @@ -1225,42 +1238,12 @@ export type EventSubscribeResponses = { export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] -export type AppGetData = { - body?: never - path?: never - query?: never - url: "/app" -} - -export type AppGetResponses = { - /** - * 200 - */ - 200: App -} - -export type AppGetResponse = AppGetResponses[keyof AppGetResponses] - -export type AppInitData = { - body?: never - path?: never - query?: never - url: "/app/init" -} - -export type AppInitResponses = { - /** - * Initialize the app - */ - 200: boolean -} - -export type AppInitResponse = AppInitResponses[keyof AppInitResponses] - export type ConfigGetData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/config" } @@ -1273,10 +1256,30 @@ export type ConfigGetResponses = { export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] +export type PathGetData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/path" +} + +export type PathGetResponses = { + /** + * Path + */ + 200: Path +} + +export type PathGetResponse = PathGetResponses[keyof PathGetResponses] + export type SessionListData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/session" } @@ -1295,7 +1298,9 @@ export type SessionCreateData = { title?: string } path?: never - query?: never + query?: { + directory?: string + } url: "/session" } @@ -1322,7 +1327,9 @@ export type SessionDeleteData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}" } @@ -1340,7 +1347,9 @@ export type SessionGetData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}" } @@ -1360,7 +1369,9 @@ export type SessionUpdateData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}" } @@ -1378,7 +1389,9 @@ export type SessionChildrenData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/children" } @@ -1403,7 +1416,9 @@ export type SessionInitData = { */ id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/init" } @@ -1421,7 +1436,9 @@ export type SessionAbortData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/abort" } @@ -1439,7 +1456,9 @@ export type SessionUnshareData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/share" } @@ -1457,7 +1476,9 @@ export type SessionShareData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/share" } @@ -1481,7 +1502,9 @@ export type SessionSummarizeData = { */ id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/summarize" } @@ -1502,7 +1525,9 @@ export type SessionMessagesData = { */ id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/message" } @@ -1518,11 +1543,13 @@ export type SessionMessagesResponses = { export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses] -export type SessionChatData = { +export type SessionPromptData = { body?: { messageID?: string - providerID: string - modelID: string + model?: { + providerID: string + modelID: string + } agent?: string system?: string tools?: { @@ -1546,11 +1573,13 @@ export type SessionChatData = { */ id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/message" } -export type SessionChatResponses = { +export type SessionPromptResponses = { /** * Created message */ @@ -1560,7 +1589,7 @@ export type SessionChatResponses = { } } -export type SessionChatResponse = SessionChatResponses[keyof SessionChatResponses] +export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] export type SessionMessageData = { body?: never @@ -1574,7 +1603,9 @@ export type SessionMessageData = { */ messageID: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/message/{messageID}" } @@ -1604,7 +1635,9 @@ export type SessionCommandData = { */ id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/command" } @@ -1631,7 +1664,9 @@ export type SessionShellData = { */ id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/shell" } @@ -1652,7 +1687,9 @@ export type SessionRevertData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/revert" } @@ -1670,7 +1707,9 @@ export type SessionUnrevertData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/unrevert" } @@ -1691,7 +1730,9 @@ export type PostSessionByIdPermissionsByPermissionIdData = { id: string permissionID: string } - query?: never + query?: { + directory?: string + } url: "/session/{id}/permissions/{permissionID}" } @@ -1708,7 +1749,9 @@ export type PostSessionByIdPermissionsByPermissionIdResponse = export type CommandListData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/command" } @@ -1724,7 +1767,9 @@ export type CommandListResponse = CommandListResponses[keyof CommandListResponse export type ConfigProvidersData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/config/providers" } @@ -1746,6 +1791,7 @@ export type FindTextData = { body?: never path?: never query: { + directory?: string pattern: string } url: "/find" @@ -1780,6 +1826,7 @@ export type FindFilesData = { body?: never path?: never query: { + directory?: string query: string } url: "/find/file" @@ -1798,6 +1845,7 @@ export type FindSymbolsData = { body?: never path?: never query: { + directory?: string query: string } url: "/find/symbol" @@ -1816,6 +1864,7 @@ export type FileListData = { body?: never path?: never query: { + directory?: string path: string } url: "/file" @@ -1834,6 +1883,7 @@ export type FileReadData = { body?: never path?: never query: { + directory?: string path: string } url: "/file/content" @@ -1854,7 +1904,9 @@ export type FileReadResponse = FileReadResponses[keyof FileReadResponses] export type FileStatusData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/file/status" } @@ -1889,7 +1941,9 @@ export type AppLogData = { } } path?: never - query?: never + query?: { + directory?: string + } url: "/log" } @@ -1905,7 +1959,9 @@ export type AppLogResponse = AppLogResponses[keyof AppLogResponses] export type AppAgentsData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/agent" } @@ -1923,7 +1979,9 @@ export type TuiAppendPromptData = { text: string } path?: never - query?: never + query?: { + directory?: string + } url: "/tui/append-prompt" } @@ -1939,7 +1997,9 @@ export type TuiAppendPromptResponse = TuiAppendPromptResponses[keyof TuiAppendPr export type TuiOpenHelpData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/tui/open-help" } @@ -1955,7 +2015,9 @@ export type TuiOpenHelpResponse = TuiOpenHelpResponses[keyof TuiOpenHelpResponse export type TuiOpenSessionsData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/tui/open-sessions" } @@ -1971,7 +2033,9 @@ export type TuiOpenSessionsResponse = TuiOpenSessionsResponses[keyof TuiOpenSess export type TuiOpenThemesData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/tui/open-themes" } @@ -1987,7 +2051,9 @@ export type TuiOpenThemesResponse = TuiOpenThemesResponses[keyof TuiOpenThemesRe export type TuiOpenModelsData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/tui/open-models" } @@ -2003,7 +2069,9 @@ export type TuiOpenModelsResponse = TuiOpenModelsResponses[keyof TuiOpenModelsRe export type TuiSubmitPromptData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/tui/submit-prompt" } @@ -2019,7 +2087,9 @@ export type TuiSubmitPromptResponse = TuiSubmitPromptResponses[keyof TuiSubmitPr export type TuiClearPromptData = { body?: never path?: never - query?: never + query?: { + directory?: string + } url: "/tui/clear-prompt" } @@ -2037,7 +2107,9 @@ export type TuiExecuteCommandData = { command: string } path?: never - query?: never + query?: { + directory?: string + } url: "/tui/execute-command" } @@ -2057,7 +2129,9 @@ export type TuiShowToastData = { variant: "info" | "success" | "warning" | "error" } path?: never - query?: never + query?: { + directory?: string + } url: "/tui/show-toast" } @@ -2075,7 +2149,9 @@ export type AuthSetData = { path: { id: string } - query?: never + query?: { + directory?: string + } url: "/auth/{id}" } diff --git a/packages/sdk/stainless/generate.ts b/packages/sdk/stainless/generate.ts index 6b1877f6..22224224 100755 --- a/packages/sdk/stainless/generate.ts +++ b/packages/sdk/stainless/generate.ts @@ -8,7 +8,7 @@ console.log(process.cwd()) await $`rm -rf go` await $`bun run --conditions=development ../../opencode/src/index.ts generate > openapi.json` -await $`stl builds create --branch dev --pull --allow-empty --+target go` +await $`stl builds create --branch main --pull --allow-empty --+target go` await $`rm -rf ../go` await $`mv opencode-go/ ../go` diff --git a/packages/sdk/stainless/stainless.yml b/packages/sdk/stainless/stainless.yml index f829e228..1efe263a 100644 --- a/packages/sdk/stainless/stainless.yml +++ b/packages/sdk/stainless/stainless.yml @@ -45,18 +45,26 @@ resources: # This method is always streaming. param_discriminator: null + path: + models: + path: Path + methods: + get: get /path + app: models: app: App provider: Provider model: Model + methods: + log: post /log + providers: get /config/providers + + agent: + models: agent: Agent methods: - get: get /app - init: post /app/init - log: post /log - agents: get /agent - providers: get /config/providers + list: get /agent find: models: @@ -92,6 +100,13 @@ resources: methods: list: get /command + project: + models: + project: Project + methods: + list: get /project + current: get /project/current + session: models: session: Session @@ -132,7 +147,7 @@ resources: summarize: post /session/{id}/summarize message: get /session/{id}/message/{messageID} messages: get /session/{id}/message - chat: post /session/{id}/message + prompt: post /session/{id}/message command: post /session/{id}/command shell: post /session/{id}/shell update: patch /session/{id} diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go index f1473e8b..22841fc8 100644 --- a/packages/tui/cmd/opencode/main.go +++ b/packages/tui/cmd/opencode/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "io" "log/slog" "os" @@ -19,6 +18,7 @@ import ( "github.com/sst/opencode/internal/clipboard" "github.com/sst/opencode/internal/tui" "github.com/sst/opencode/internal/util" + "golang.org/x/sync/errgroup" ) var Version = "dev" @@ -37,14 +37,6 @@ func main() { url := os.Getenv("OPENCODE_SERVER") - appInfoStr := os.Getenv("OPENCODE_APP_INFO") - var appInfo opencode.App - err := json.Unmarshal([]byte(appInfoStr), &appInfo) - if err != nil { - slog.Error("Failed to unmarshal app info", "error", err) - os.Exit(1) - } - stat, err := os.Stdin.Stat() if err != nil { slog.Error("Failed to stat stdin", "error", err) @@ -73,17 +65,43 @@ func main() { option.WithBaseURL(url), ) - // Fetch agents from the /agent endpoint - agentsPtr, err := httpClient.App.Agents(context.Background()) + var agents []opencode.Agent + var path *opencode.Path + var project *opencode.Project + + batch := errgroup.Group{} + + batch.Go(func() error { + result, err := httpClient.Project.Current(context.Background(), opencode.ProjectCurrentParams{}) + if err != nil { + return err + } + project = result + return nil + }) + + batch.Go(func() error { + result, err := httpClient.Agent.List(context.Background(), opencode.AgentListParams{}) + if err != nil { + return err + } + agents = *result + return nil + }) + + batch.Go(func() error { + result, err := httpClient.Path.Get(context.Background(), opencode.PathGetParams{}) + if err != nil { + return err + } + path = result + return nil + }) + + err = batch.Wait() if err != nil { - slog.Error("Failed to fetch agents", "error", err) - os.Exit(1) + panic(err) } - if agentsPtr == nil { - slog.Error("No agents returned from server") - os.Exit(1) - } - agents := *agentsPtr ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -91,7 +109,7 @@ func main() { logger := slog.New(apiHandler) slog.SetDefault(logger) - slog.Debug("TUI launched", "app", appInfoStr, "agents_count", len(agents), "url", url) + slog.Debug("TUI launched") go func() { err = clipboard.Init() @@ -101,7 +119,7 @@ func main() { }() // Create main context for the application - app_, err := app.New(ctx, version, appInfo, agents, httpClient, model, prompt, agent, sessionID) + app_, err := app.New(ctx, version, project, path, agents, httpClient, model, prompt, agent, sessionID) if err != nil { panic(err) } @@ -118,12 +136,9 @@ func main() { signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) go func() { - stream := httpClient.Event.ListStreaming(ctx) + stream := httpClient.Event.ListStreaming(ctx, opencode.EventListParams{}) for stream.Next() { evt := stream.Current().AsUnion() - if _, ok := evt.(opencode.EventListResponseEventStorageWrite); ok { - continue - } program.Send(evt) } if err := stream.Err(); err != nil { diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 8aaddfbc..4b333a93 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -27,7 +27,7 @@ type Message struct { } type App struct { - Info opencode.App + Project opencode.Project Agents []opencode.Agent Providers []opencode.Provider Version string @@ -101,7 +101,8 @@ type PermissionRespondedToMsg struct { func New( ctx context.Context, version string, - appInfo opencode.App, + project *opencode.Project, + path *opencode.Path, agents []opencode.Agent, httpClient *opencode.Client, initialModel *string, @@ -109,10 +110,10 @@ func New( initialAgent *string, initialSession *string, ) (*App, error) { - util.RootPath = appInfo.Path.Root - util.CwdPath = appInfo.Path.Cwd + util.RootPath = project.Worktree + util.CwdPath, _ = os.Getwd() - configInfo, err := httpClient.Config.Get(ctx) + configInfo, err := httpClient.Config.Get(ctx, opencode.ConfigGetParams{}) if err != nil { return nil, err } @@ -121,7 +122,7 @@ func New( configInfo.Keybinds.Leader = "ctrl+x" } - appStatePath := filepath.Join(appInfo.Path.State, "tui") + appStatePath := filepath.Join(path.State, "tui") appState, err := LoadState(appStatePath) if err != nil { appState = NewState() @@ -168,9 +169,9 @@ func New( } if err := theme.LoadThemesFromDirectories( - appInfo.Path.Config, - appInfo.Path.Root, - appInfo.Path.Cwd, + path.Config, + util.RootPath, + util.CwdPath, ); err != nil { slog.Warn("Failed to load themes from directories", "error", err) } @@ -187,13 +188,13 @@ func New( slog.Debug("Loaded config", "config", configInfo) - customCommands, err := httpClient.Command.List(ctx) + customCommands, err := httpClient.Command.List(ctx, opencode.CommandListParams{}) if err != nil { return nil, err } app := &App{ - Info: appInfo, + Project: *project, Agents: agents, Version: version, StatePath: appStatePath, @@ -459,7 +460,7 @@ func findProviderByID(providers []opencode.Provider, providerID string) *opencod } func (a *App) InitializeProvider() tea.Cmd { - providersResponse, err := a.Client.App.Providers(context.Background()) + providersResponse, err := a.Client.App.Providers(context.Background(), opencode.AppProvidersParams{}) if err != nil { slog.Error("Failed to list providers", "error", err) // TODO: notify user @@ -749,12 +750,15 @@ func (a *App) CompactSession(ctx context.Context) tea.Cmd { } func (a *App) MarkProjectInitialized(ctx context.Context) error { - _, err := a.Client.App.Init(ctx) - if err != nil { - slog.Error("Failed to mark project as initialized", "error", err) - return err - } return nil + /* + _, err := a.Client.App.Init(ctx) + if err != nil { + slog.Error("Failed to mark project as initialized", "error", err) + return err + } + return nil + */ } func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) { @@ -782,12 +786,14 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) { a.Messages = append(a.Messages, message) cmds = append(cmds, func() tea.Msg { - _, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{ - ProviderID: opencode.F(a.Provider.ID), - ModelID: opencode.F(a.Model.ID), - Agent: opencode.F(a.Agent().Name), - MessageID: opencode.F(messageID), - Parts: opencode.F(message.ToSessionChatParams()), + _, err := a.Client.Session.Prompt(ctx, a.Session.ID, opencode.SessionPromptParams{ + Model: opencode.F(opencode.SessionPromptParamsModel{ + ProviderID: opencode.F(a.Provider.ID), + ModelID: opencode.F(a.Model.ID), + }), + Agent: opencode.F(a.Agent().Name), + MessageID: opencode.F(messageID), + Parts: opencode.F(message.ToSessionChatParams()), }) if err != nil { errormsg := fmt.Sprintf("failed to send message: %v", err) @@ -878,7 +884,7 @@ func (a *App) Cancel(ctx context.Context, sessionID string) error { a.compactCancel = nil } - _, err := a.Client.Session.Abort(ctx, sessionID) + _, err := a.Client.Session.Abort(ctx, sessionID, opencode.SessionAbortParams{}) if err != nil { slog.Error("Failed to cancel session", "error", err) return err @@ -887,7 +893,7 @@ func (a *App) Cancel(ctx context.Context, sessionID string) error { } func (a *App) ListSessions(ctx context.Context) ([]opencode.Session, error) { - response, err := a.Client.Session.List(ctx) + response, err := a.Client.Session.List(ctx, opencode.SessionListParams{}) if err != nil { return nil, err } @@ -899,7 +905,7 @@ func (a *App) ListSessions(ctx context.Context) ([]opencode.Session, error) { } func (a *App) DeleteSession(ctx context.Context, sessionID string) error { - _, err := a.Client.Session.Delete(ctx, sessionID) + _, err := a.Client.Session.Delete(ctx, sessionID, opencode.SessionDeleteParams{}) if err != nil { slog.Error("Failed to delete session", "error", err) return err @@ -919,7 +925,7 @@ func (a *App) UpdateSession(ctx context.Context, sessionID string, title string) } func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) { - response, err := a.Client.Session.Messages(ctx, sessionId) + response, err := a.Client.Session.Messages(ctx, sessionId, opencode.SessionMessagesParams{}) if err != nil { return nil, err } @@ -941,7 +947,7 @@ func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, er } func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) { - response, err := a.Client.App.Providers(ctx) + response, err := a.Client.App.Providers(ctx, opencode.AppProvidersParams{}) if err != nil { return nil, err } diff --git a/packages/tui/internal/app/prompt.go b/packages/tui/internal/app/prompt.go index 81c4b6a7..8701a207 100644 --- a/packages/tui/internal/app/prompt.go +++ b/packages/tui/internal/app/prompt.go @@ -204,8 +204,8 @@ func (m Message) ToPrompt() (*Prompt, error) { return nil, errors.New("unknown message type") } -func (m Message) ToSessionChatParams() []opencode.SessionChatParamsPartUnion { - parts := []opencode.SessionChatParamsPartUnion{} +func (m Message) ToSessionChatParams() []opencode.SessionPromptParamsPartUnion { + parts := []opencode.SessionPromptParamsPartUnion{} for _, part := range m.Parts { switch p := part.(type) { case opencode.TextPart: diff --git a/packages/tui/internal/completions/agents.go b/packages/tui/internal/completions/agents.go index c39fe303..d25c76d8 100644 --- a/packages/tui/internal/completions/agents.go +++ b/packages/tui/internal/completions/agents.go @@ -30,8 +30,9 @@ func (cg *agentsContextGroup) GetChildEntries( query = strings.TrimSpace(query) - agents, err := cg.app.Client.App.Agents( + agents, err := cg.app.Client.Agent.List( context.Background(), + opencode.AgentListParams{}, ) if err != nil { slog.Error("Failed to get agent list", "error", err) diff --git a/packages/tui/internal/completions/files.go b/packages/tui/internal/completions/files.go index bece89a8..d0087365 100644 --- a/packages/tui/internal/completions/files.go +++ b/packages/tui/internal/completions/files.go @@ -29,7 +29,7 @@ func (cg *filesContextGroup) GetEmptyMessage() string { func (cg *filesContextGroup) getGitFiles() []CompletionSuggestion { items := make([]CompletionSuggestion, 0) - status, _ := cg.app.Client.File.Status(context.Background()) + status, _ := cg.app.Client.File.Status(context.Background(), opencode.FileStatusParams{}) if status != nil { files := *status sort.Slice(files, func(i, j int) bool { diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 71004970..e43d297b 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -160,7 +160,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if filePath := strings.TrimSpace(strings.TrimPrefix(text, "@")); strings.HasPrefix(text, "@") && filePath != "" { statPath := filePath if !filepath.IsAbs(filePath) { - statPath = filepath.Join(m.app.Info.Path.Cwd, filePath) + statPath = filepath.Join(util.CwdPath, filePath) } if _, err := os.Stat(statPath); err == nil { attachment := m.createAttachmentFromPath(filePath) @@ -623,7 +623,7 @@ func (m *editorComponent) SetValueWithAttachments(value string) { if end > start { filePath := value[start:end] slog.Debug("test", "filePath", filePath) - if _, err := os.Stat(filepath.Join(m.app.Info.Path.Cwd, filePath)); err == nil { + if _, err := os.Stat(filepath.Join(util.CwdPath, filePath)); err == nil { slog.Debug("test", "found", true) attachment := m.createAttachmentFromFile(filePath) if attachment != nil { @@ -818,7 +818,7 @@ func (m *editorComponent) createAttachmentFromFile(filePath string) *attachment. mediaType := getMediaTypeFromExtension(ext) absolutePath := filePath if !filepath.IsAbs(filePath) { - absolutePath = filepath.Join(m.app.Info.Path.Cwd, filePath) + absolutePath = filepath.Join(util.CwdPath, filePath) } // For text files, create a simple file reference @@ -872,7 +872,7 @@ func (m *editorComponent) createAttachmentFromPath(filePath string) *attachment. mediaType := getMediaTypeFromExtension(extension) absolutePath := filePath if !filepath.IsAbs(filePath) { - absolutePath = filepath.Join(m.app.Info.Path.Cwd, filePath) + absolutePath = filepath.Join(util.CwdPath, filePath) } return &attachment.Attachment{ ID: uuid.NewString(), diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index a19d15fd..ca1a2a53 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -55,6 +55,8 @@ func WithBackgroundColor(color compat.AdaptiveColor) renderingOption { func WithNoBorder() renderingOption { return func(c *blockRenderer) { c.border = false + c.paddingLeft++ + c.paddingRight++ } } @@ -185,7 +187,7 @@ func renderContentBlock( style = style.BorderRightForeground(borderColor) } } else { - style = style.PaddingLeft(renderer.paddingLeft + 1).PaddingRight(renderer.paddingRight + 1) + style = style.PaddingLeft(renderer.paddingLeft).PaddingRight(renderer.paddingRight) } content = style.Render(content) diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index b3074bb5..053d538a 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -769,6 +769,7 @@ func (m *messagesComponent) renderView() tea.Cmd { context.Background(), m.app.CurrentPermission.SessionID, m.app.CurrentPermission.MessageID, + opencode.SessionMessageParams{}, ) if err != nil || response == nil { slog.Error("Failed to get message from child session", "error", err) @@ -1238,6 +1239,7 @@ func (m *messagesComponent) RedoLastMessage() (tea.Model, tea.Cmd) { response, err := m.app.Client.Session.Unrevert( context.Background(), m.app.Session.ID, + opencode.SessionUnrevertParams{}, ) if err != nil { slog.Error("Failed to unrevert session", "error", err) diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go index 79263782..9d2059e0 100644 --- a/packages/tui/internal/components/status/status.go +++ b/packages/tui/internal/components/status/status.go @@ -200,7 +200,7 @@ func (m *statusComponent) View() string { func (m *statusComponent) startGitWatcher() tea.Cmd { cmd := util.CmdHandler( - GitBranchUpdatedMsg{Branch: getCurrentGitBranch(m.app.Info.Path.Root)}, + GitBranchUpdatedMsg{Branch: getCurrentGitBranch(m.app.Project.Worktree)}, ) if err := m.initWatcher(); err != nil { return cmd @@ -209,7 +209,7 @@ func (m *statusComponent) startGitWatcher() tea.Cmd { } func (m *statusComponent) initWatcher() error { - gitDir := filepath.Join(m.app.Info.Path.Root, ".git") + gitDir := filepath.Join(m.app.Project.Worktree, ".git") headFile := filepath.Join(gitDir, "HEAD") if info, err := os.Stat(gitDir); err != nil || !info.IsDir() { return err @@ -226,7 +226,7 @@ func (m *statusComponent) initWatcher() error { } // Also watch the ref file if HEAD points to a ref - refFile := getGitRefFile(m.app.Info.Path.Cwd) + refFile := getGitRefFile(util.CwdPath) if refFile != headFile && refFile != "" { if _, err := os.Stat(refFile); err == nil { watcher.Add(refFile) // Ignore error, HEAD watching is sufficient @@ -247,7 +247,7 @@ func (m *statusComponent) watchForGitChanges() tea.Cmd { for { select { case event, ok := <-m.watcher.Events: - branch := getCurrentGitBranch(m.app.Info.Path.Root) + branch := getCurrentGitBranch(m.app.Project.Worktree) if !ok { return GitBranchUpdatedMsg{Branch: branch} } @@ -276,8 +276,8 @@ func (m *statusComponent) updateWatchedFiles() { if m.watcher == nil { return } - refFile := getGitRefFile(m.app.Info.Path.Root) - headFile := filepath.Join(m.app.Info.Path.Root, ".git", "HEAD") + refFile := getGitRefFile(m.app.Project.Worktree) + headFile := filepath.Join(m.app.Project.Worktree, ".git", "HEAD") if refFile != headFile && refFile != "" { if _, err := os.Stat(refFile); err == nil { // Try to add the new ref file (ignore error if already watching) @@ -330,7 +330,7 @@ func NewStatusCmp(app *app.App) StatusComponent { } homePath, err := os.UserHomeDir() - cwdPath := app.Info.Path.Cwd + cwdPath := util.CwdPath if err == nil && homePath != "" && strings.HasPrefix(cwdPath, homePath) { cwdPath = "~" + cwdPath[len(homePath):] } diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 71e5b9f7..62a647a1 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -393,7 +393,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showCompletionDialog = false // If we're in a child session, switch back to parent before sending prompt if a.app.Session.ParentID != "" { - parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID) + parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID, opencode.SessionGetParams{}) if err != nil { slog.Error("Failed to get parent session", "error", err) return a, toast.NewErrorToast("Failed to get parent session") @@ -411,7 +411,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case app.SendCommand: // If we're in a child session, switch back to parent before sending prompt if a.app.Session.ParentID != "" { - parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID) + parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID, opencode.SessionGetParams{}) if err != nil { slog.Error("Failed to get parent session", "error", err) return a, toast.NewErrorToast("Failed to get parent session") @@ -429,7 +429,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case app.SendShell: // If we're in a child session, switch back to parent before sending prompt if a.app.Session.ParentID != "" { - parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID) + parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID, opencode.SessionGetParams{}) if err != nil { slog.Error("Failed to get parent session", "error", err) return a, toast.NewErrorToast("Failed to get parent session") @@ -460,11 +460,13 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { "opencode updated to "+msg.Properties.Version+", restart to apply.", toast.WithTitle("New version installed"), ) - case opencode.EventListResponseEventIdeInstalled: - return a, toast.NewSuccessToast( - "Installed the opencode extension in "+msg.Properties.Ide, - toast.WithTitle(msg.Properties.Ide+" extension installed"), - ) + /* + case opencode.EventListResponseEventIdeInstalled: + return a, toast.NewSuccessToast( + "Installed the opencode extension in "+msg.Properties.Ide, + toast.WithTitle(msg.Properties.Ide+" extension installed"), + ) + */ case opencode.EventListResponseEventSessionDeleted: if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID { a.app.Session = &opencode.Session{} @@ -674,7 +676,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if nextMessageID == "" { // Last message - use unrevert to restore full conversation - response, err = a.app.Client.Session.Unrevert(context.Background(), a.app.Session.ID) + response, err = a.app.Client.Session.Unrevert(context.Background(), a.app.Session.ID, opencode.SessionUnrevertParams{}) } else { // Revert to next message to make target the last visible response, err = a.app.Client.Session.Revert(context.Background(), a.app.Session.ID, @@ -1183,7 +1185,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { if a.app.Session.ID == "" { return a, nil } - response, err := a.app.Client.Session.Share(context.Background(), a.app.Session.ID) + response, err := a.app.Client.Session.Share(context.Background(), a.app.Session.ID, opencode.SessionShareParams{}) if err != nil { slog.Error("Failed to share session", "error", err) return a, toast.NewErrorToast("Failed to share session") @@ -1195,7 +1197,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { if a.app.Session.ID == "" { return a, nil } - _, err := a.app.Client.Session.Unshare(context.Background(), a.app.Session.ID) + _, err := a.app.Client.Session.Unshare(context.Background(), a.app.Session.ID, opencode.SessionUnshareParams{}) if err != nil { slog.Error("Failed to unshare session", "error", err) return a, toast.NewErrorToast("Failed to unshare session") @@ -1223,7 +1225,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { var parentSession *opencode.Session if a.app.Session.ParentID != "" { parentSessionID = a.app.Session.ParentID - session, err := a.app.Client.Session.Get(context.Background(), parentSessionID) + session, err := a.app.Client.Session.Get(context.Background(), parentSessionID, opencode.SessionGetParams{}) if err != nil { slog.Error("Failed to get parent session", "error", err) return toast.NewErrorToast("Failed to get parent session") @@ -1233,7 +1235,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { parentSession = a.app.Session } - children, err := a.app.Client.Session.Children(context.Background(), parentSessionID) + children, err := a.app.Client.Session.Children(context.Background(), parentSessionID, opencode.SessionChildrenParams{}) if err != nil { slog.Error("Failed to get session children", "error", err) return toast.NewErrorToast("Failed to get session children") @@ -1281,7 +1283,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { var parentSession *opencode.Session if a.app.Session.ParentID != "" { parentSessionID = a.app.Session.ParentID - session, err := a.app.Client.Session.Get(context.Background(), parentSessionID) + session, err := a.app.Client.Session.Get(context.Background(), parentSessionID, opencode.SessionGetParams{}) if err != nil { slog.Error("Failed to get parent session", "error", err) return toast.NewErrorToast("Failed to get parent session") @@ -1291,7 +1293,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { parentSession = a.app.Session } - children, err := a.app.Client.Session.Children(context.Background(), parentSessionID) + children, err := a.app.Client.Session.Children(context.Background(), parentSessionID, opencode.SessionChildrenParams{}) if err != nil { slog.Error("Failed to get session children", "error", err) return toast.NewErrorToast("Failed to get session children") diff --git a/specs/project.md b/specs/project.md new file mode 100644 index 00000000..dd51f0e7 --- /dev/null +++ b/specs/project.md @@ -0,0 +1,65 @@ +## project + +goal is to let a single instance of opencode be able to run sessions for +multiple projects and different worktrees per project + +### api + +``` +GET /project -> Project[] + +POST /project/init -> Project + + +GET /project/:projectID/session -> Session[] + +GET /project/:projectID/session/:sessionID -> Session + +POST /project/:projectID/session -> Session +{ + id?: string + parentID?: string + directory: string +} + +DELETE /project/:projectID/session/:sessionID + +POST /project/:projectID/session/:sessionID/init + +POST /project/:projectID/session/:sessionID/abort + +POST /project/:projectID/session/:sessionID/share + +DELETE /project/:projectID/session/:sessionID/share + +POST /project/:projectID/session/:sessionID/compact + +GET /project/:projectID/session/:sessionID/message -> { info: Message, parts: Part[] }[] + +GET /project/:projectID/session/:sessionID/message/:messageID -> { info: Message, parts: Part[] } + +POST /project/:projectID/session/:sessionID/message -> { info: Message, parts: Part[] } + +POST /project/:projectID/session/:sessionID/revert -> Session + +POST /project/:projectID/session/:sessionID/unrevert -> Session + +POST /project/:projectID/session/:sessionID/permission/:permissionID -> Session + +GET /project/:projectID/session/:sessionID/find/file -> string[] + +GET /project/:projectID/session/:sessionID/file -> { type: "raw" | "patch", content: string } + +GET /project/:projectID/session/:sessionID/file/status -> File[] + +POST /log + +// These are awkward + +GET /provider?directory= -> Provider +GET /config?directory= -> Config // think only tui uses this? + +GET /project/:projectID/agent?directory= -> Agent +GET /project/:projectID/find/file?directory= -> File + +```