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 + +```