diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index cfd3958d..7fbefba4 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -33,16 +33,16 @@ export namespace Bus { "type", registry .entries() - .map(([type, def]) => - z + .map(([type, def]) => { + return z .object({ type: z.literal(type), properties: def.properties, }) .meta({ ref: "Event" + "." + def.type, - }), - ) + }) + }) .toArray() as any, ) } diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 71be1cd4..ab36087b 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -15,7 +15,8 @@ export namespace Plugin { const state = Instance.state(async () => { const client = createOpencodeClient({ baseUrl: "http://localhost:4096", - fetch: async (...args) => Server.App.fetch(...args), + // @ts-expect-error + fetch: async (...args) => Server.App().fetch(...args), }) const config = await Config.get() const hooks = [] diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index fd44b0fd..3a5d794c 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -28,6 +28,7 @@ import { zodToJsonSchema } from "zod-to-json-schema" import { SessionPrompt } from "../session/prompt" import { SessionCompaction } from "../session/compaction" import { SessionRevert } from "../session/revert" +import { lazy } from "../util/lazy" const ERRORS = { 400: { @@ -79,1343 +80,1345 @@ export namespace Server { } 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(), { + export const App = lazy(() => + 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, }) - } - 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 directory = c.req.query("directory") ?? process.cwd() - return Instance.provide(directory, async () => { - return next() - }) - }) - .use(cors()) - .get( - "/doc", - openAPIRouteHandler(app, { - documentation: { - info: { - title: "opencode", - version: "0.0.3", - description: "opencode api", - }, - openapi: "3.1.1", - }, - }), - ) - .use(validator("query", z.object({ directory: z.string().optional() }))) - .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().meta({ - ref: "Event", - }), - ), - }, - }, - }, - }, - }), - async (c) => { - log.info("event connected") - return streamSSE(c, async (stream) => { - stream.writeSSE({ - data: JSON.stringify({ - type: "server.connected", - properties: {}, - }), + .use(async (c, next) => { + const skipLogging = c.req.path === "/log" + if (!skipLogging) { + log.info("request", { + method: c.req.method, + path: c.req.path, }) - 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()) - }, - ) - .post( - "/experimental/tool/register", - describeRoute({ - description: "Register a new HTTP callback tool", - operationId: "tool.register", - responses: { - 200: { - description: "Tool registered successfully", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - ...ERRORS, - }, - }), - validator("json", HttpToolRegistration), - async (c) => { - ToolRegistry.registerHTTP(c.req.valid("json")) - return c.json(true) - }, - ) - .get( - "/experimental/tool/ids", - describeRoute({ - description: "List all tool IDs (including built-in and dynamically registered)", - operationId: "tool.ids", - responses: { - 200: { - description: "Tool IDs", - content: { - "application/json": { - schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })), - }, - }, - }, - ...ERRORS, - }, - }), - async (c) => { - return c.json(ToolRegistry.ids()) - }, - ) - .get( - "/experimental/tool", - describeRoute({ - description: "List tools with JSON schema parameters for a provider/model", - operationId: "tool.list", - responses: { - 200: { - description: "Tools", - content: { - "application/json": { - schema: resolver( - z - .array( - z - .object({ - id: z.string(), - description: z.string(), - parameters: z.any(), - }) - .meta({ ref: "ToolListItem" }), - ) - .meta({ ref: "ToolList" }), - ), - }, - }, - }, - ...ERRORS, - }, - }), - validator( - "query", - z.object({ - provider: z.string(), - model: z.string(), - }), - ), - async (c) => { - const { provider, model } = c.req.valid("query") - const tools = await ToolRegistry.tools(provider, model) - return c.json( - tools.map((t) => ({ - id: t.id, - description: t.description, - // Handle both Zod schemas and plain JSON schemas - parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters, - })), - ) - }, - ) - .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(), - }) - .meta({ - ref: "Path", - }), - ), - }, - }, - }, - }, - }), - 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) => { - 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), - }, - }, - }, - }, - }), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "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), - }, - }, - }, - }, - }), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "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), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string(), - }), - ), - validator( - "json", - z.object({ - 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 - } - }) - - 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()), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string().meta({ description: "Session ID" }), - }), - ), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - return c.json(SessionPrompt.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), - }, - }, - }, - }, - }), - validator( - "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), - }, - }, - }, - }, - }), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string().meta({ description: "Session ID" }), - }), - ), - validator( - "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 SessionCompaction.run({ ...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(MessageV2.WithParts.array()), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string().meta({ 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(), - }), - ), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string().meta({ description: "Session ID" }), - messageID: z.string().meta({ 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(), - }), - ), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string().meta({ description: "Session ID" }), - }), - ), - validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").id - const body = c.req.valid("json") - const msg = await SessionPrompt.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(), - }), - ), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string().meta({ description: "Session ID" }), - }), - ), - validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").id - const body = c.req.valid("json") - const msg = await SessionPrompt.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), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string().meta({ description: "Session ID" }), - }), - ), - validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").id - const body = c.req.valid("json") - const msg = await SessionPrompt.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), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string(), - }), - ), - validator("json", SessionRevert.RevertInput.omit({ sessionID: true })), - async (c) => { - const id = c.req.valid("param").id - log.info("revert", c.req.valid("json")) - const session = await SessionRevert.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), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string(), - }), - ), - async (c) => { - const id = c.req.valid("param").id - const session = await SessionRevert.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()), - }, - }, - }, - }, - }), - validator( - "param", - z.object({ - id: z.string(), - permissionID: z.string(), - }), - ), - validator("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 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()), - }), - ), - }, - }, - }, - }, - }), - 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()), - }, - }, - }, - }, - }), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "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(File.Content), - }, - }, - }, - }, - }), - validator( - "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()), - }, - }, - }, - }, - }), - validator( - "json", - z.object({ - service: z.string().meta({ description: "Service name for the log entry" }), - level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }), - message: z.string().meta({ description: "Log message" }), - extra: z - .record(z.string(), z.any()) - .optional() - .meta({ description: "Additional metadata for the log entry" }), - }), - ), - 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 } + 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() + }) + }) + .use(cors()) + .get( + "/doc", + openAPIRouteHandler(app, { + documentation: { + info: { + title: "opencode", + version: "0.0.3", + description: "opencode api", + }, + openapi: "3.1.1", + }, + }), + ) + .use(validator("query", z.object({ directory: z.string().optional() }))) + .route("/project", ProjectRoute) + .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()) + }, + ) + .post( + "/experimental/tool/register", + describeRoute({ + description: "Register a new HTTP callback tool", + operationId: "tool.register", + responses: { + 200: { + description: "Tool registered successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...ERRORS, + }, + }), + validator("json", HttpToolRegistration), + async (c) => { + ToolRegistry.registerHTTP(c.req.valid("json")) + return c.json(true) + }, + ) + .get( + "/experimental/tool/ids", + describeRoute({ + description: "List all tool IDs (including built-in and dynamically registered)", + operationId: "tool.ids", + responses: { + 200: { + description: "Tool IDs", + content: { + "application/json": { + schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })), + }, + }, + }, + ...ERRORS, + }, + }), + async (c) => { + return c.json(ToolRegistry.ids()) + }, + ) + .get( + "/experimental/tool", + describeRoute({ + description: "List tools with JSON schema parameters for a provider/model", + operationId: "tool.list", + responses: { + 200: { + description: "Tools", + content: { + "application/json": { + schema: resolver( + z + .array( + z + .object({ + id: z.string(), + description: z.string(), + parameters: z.any(), + }) + .meta({ ref: "ToolListItem" }), + ) + .meta({ ref: "ToolList" }), + ), + }, + }, + }, + ...ERRORS, + }, + }), + validator( + "query", + z.object({ + provider: z.string(), + model: z.string(), + }), + ), + async (c) => { + const { provider, model } = c.req.valid("query") + const tools = await ToolRegistry.tools(provider, model) + return c.json( + tools.map((t) => ({ + id: t.id, + description: t.description, + // Handle both Zod schemas and plain JSON schemas + parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters, + })), + ) + }, + ) + .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(), + }) + .meta({ + ref: "Path", + }), + ), + }, + }, + }, + }, + }), + 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) => { + 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), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "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), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "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), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string(), + }), + ), + validator( + "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(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()), + 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()), + }, }, }, }, - }, - }), - 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()), - }, - }, - }, - }, - }), - validator( - "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()), + validator( + "param", + z.object({ + id: z.string().meta({ description: "Session ID" }), + }), + ), + validator( + "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()), + }, }, }, }, - }, - }), - 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()), - }, - }, - }, - }, - }), - validator( - "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()), + validator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + return c.json(SessionPrompt.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), + }, }, }, }, - }, - }), - validator( - "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()), + validator( + "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), + }, }, }, }, - ...ERRORS, - }, - }), - validator( - "param", - z.object({ - id: z.string(), }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().meta({ description: "Session ID" }), + }), + ), + validator( + "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 SessionCompaction.run({ ...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(MessageV2.WithParts.array()), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().meta({ 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(), + }), + ), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().meta({ description: "Session ID" }), + messageID: z.string().meta({ 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(), + }), + ), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().meta({ description: "Session ID" }), + }), + ), + validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await SessionPrompt.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(), + }), + ), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().meta({ description: "Session ID" }), + }), + ), + validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await SessionPrompt.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), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().meta({ description: "Session ID" }), + }), + ), + validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await SessionPrompt.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), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string(), + }), + ), + validator("json", SessionRevert.RevertInput.omit({ sessionID: true })), + async (c) => { + const id = c.req.valid("param").id + log.info("revert", c.req.valid("json")) + const session = await SessionRevert.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), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string(), + }), + ), + async (c) => { + const id = c.req.valid("param").id + const session = await SessionRevert.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()), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string(), + permissionID: z.string(), + }), + ), + validator("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 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()), + }), + ), + }, + }, + }, + }, + }), + 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()), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "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(File.Content), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "json", + z.object({ + service: z.string().meta({ description: "Service name for the log entry" }), + level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }), + message: z.string().meta({ description: "Log message" }), + extra: z + .record(z.string(), z.any()) + .optional() + .meta({ description: "Additional metadata for the log entry" }), + }), + ), + 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 + } + + 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()), + }, + }, + }, + }, + }), + 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()), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "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()), + }, + }, + }, + }, + }), + validator( + "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, + }, + }), + validator( + "param", + z.object({ + id: z.string(), + }), + ), + validator("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) + }, + ) + .get( + "/event", + describeRoute({ + description: "Get events", + operationId: "event.subscribe", + responses: { + 200: { + description: "Event stream", + content: { + "text/event-stream": { + schema: resolver( + Bus.payloads().meta({ + 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") + }) + }) + }) + }, ), - validator("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 result = await generateSpecs(App, { + const result = await generateSpecs(App(), { documentation: { info: { title: "opencode", @@ -1433,7 +1436,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/test/tool/register.test.ts b/packages/opencode/test/tool/register.test.ts index 834d1898..351eb91d 100644 --- a/packages/opencode/test/tool/register.test.ts +++ b/packages/opencode/test/tool/register.test.ts @@ -31,7 +31,7 @@ describe("HTTP tool registration API", () => { } // Register - const registerRes = await Server.App.fetch( + const registerRes = await Server.App().fetch( makeRequest("POST", "http://localhost:4096/experimental/tool/register", toolSpec), ) expect(registerRes.status).toBe(200) @@ -39,13 +39,13 @@ describe("HTTP tool registration API", () => { expect(ok).toBe(true) // IDs should include the new tool - const idsRes = await Server.App.fetch(makeRequest("GET", "http://localhost:4096/experimental/tool/ids")) + const idsRes = await Server.App().fetch(makeRequest("GET", "http://localhost:4096/experimental/tool/ids")) expect(idsRes.status).toBe(200) const ids = (await idsRes.json()) as string[] expect(ids).toContain("http-echo") // List tools for a provider/model and check JSON Schema shape - const listRes = await Server.App.fetch( + const listRes = await Server.App().fetch( makeRequest("GET", "http://localhost:4096/experimental/tool?provider=openai&model=gpt-4o"), ) expect(listRes.status).toBe(200) @@ -105,7 +105,7 @@ describe("Plugin tool.register hook", () => { expect(allIDs).toContain("from-plugin") // Also verify via the HTTP surface - const idsRes = await Server.App.fetch(makeRequest("GET", "http://localhost:4096/experimental/tool/ids")) + const idsRes = await Server.App().fetch(makeRequest("GET", "http://localhost:4096/experimental/tool/ids")) expect(idsRes.status).toBe(200) const ids = (await idsRes.json()) as string[] expect(ids).toContain("from-plugin") @@ -168,7 +168,7 @@ test("Multiple plugins can each register tools", async () => { expect(ids).toContain("alpha-tool") expect(ids).toContain("beta-tool") - const res = await Server.App.fetch(new Request("http://localhost:4096/experimental/tool/ids")) + const res = await Server.App().fetch(new Request("http://localhost:4096/experimental/tool/ids")) expect(res.status).toBe(200) const httpIds = (await res.json()) as string[] expect(httpIds).toContain("alpha-tool") @@ -241,14 +241,14 @@ test("Plugin registers native/local tool with function execution", async () => { expect(ids).toContain("http-tool-from-same-plugin") // Verify via HTTP endpoint - const res = await Server.App.fetch(new Request("http://localhost:4096/experimental/tool/ids")) + const res = await Server.App().fetch(new Request("http://localhost:4096/experimental/tool/ids")) expect(res.status).toBe(200) const httpIds = (await res.json()) as string[] expect(httpIds).toContain("my-native-tool") expect(httpIds).toContain("http-tool-from-same-plugin") // Get tool details to verify native tool has proper structure - const toolsRes = await Server.App.fetch( + const toolsRes = await Server.App().fetch( new Request("http://localhost:4096/experimental/tool?provider=anthropic&model=claude"), ) expect(toolsRes.status).toBe(200) @@ -290,7 +290,7 @@ test("Plugin without tool.register is handled gracefully", async () => { const ids = ToolRegistry.ids() expect(ids).not.toContain("malformed-tool") - const res = await Server.App.fetch(new Request("http://localhost:4096/experimental/tool/ids")) + const res = await Server.App().fetch(new Request("http://localhost:4096/experimental/tool/ids")) expect(res.status).toBe(200) const httpIds = (await res.json()) as string[] expect(httpIds).not.toContain("malformed-tool") diff --git a/packages/sdk/go/.release-please-manifest.json b/packages/sdk/go/.release-please-manifest.json index 727e2bea..f87262aa 100644 --- a/packages/sdk/go/.release-please-manifest.json +++ b/packages/sdk/go/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.14.0" + ".": "0.15.0" } diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 9d47e52e..f4ae1670 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 43 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-0a4165f1eabf826d3092ea6b789aa527048278dcd4bd891f9e5ac890b9bcbb35.yml -openapi_spec_hash: da60e4fc813eb0f9ac3ab5f112e26bf6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-ad911ed0bdbeca62807509f364f25fcafd7a83e0b43e027ec0a85f72b7a4d963.yml +openapi_spec_hash: 15152513b4246bf4b5f8546fa6f1603f config_hash: 026ef000d34bf2f930e7b41e77d2d3ff diff --git a/packages/sdk/go/CHANGELOG.md b/packages/sdk/go/CHANGELOG.md index 9e13db9e..f82dffa4 100644 --- a/packages/sdk/go/CHANGELOG.md +++ b/packages/sdk/go/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.15.0 (2025-09-16) + +Full Changelog: [v0.14.0...v0.15.0](https://github.com/sst/opencode-sdk-go/compare/v0.14.0...v0.15.0) + +### Features + +- **api:** api update ([397048f](https://github.com/sst/opencode-sdk-go/commit/397048faca7a1de7a028edd2254a0ad7797b151f)) + ## 0.14.0 (2025-09-14) Full Changelog: [v0.13.0...v0.14.0](https://github.com/sst/opencode-sdk-go/compare/v0.13.0...v0.14.0) diff --git a/packages/sdk/go/README.md b/packages/sdk/go/README.md index 96898209..39c35837 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.14.0' +go get -u 'github.com/sst/opencode-sdk-go@v0.15.0' ``` diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index d469bdff..c79de17f 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -90,8 +90,9 @@ type Config struct { // TUI specific settings Tui ConfigTui `json:"tui"` // Custom username to display in conversations instead of system username - Username string `json:"username"` - JSON configJSON `json:"-"` + Username string `json:"username"` + Watcher ConfigWatcher `json:"watcher"` + JSON configJSON `json:"-"` } // configJSON contains the JSON metadata for the struct [Config] @@ -121,6 +122,7 @@ type configJSON struct { Tools apijson.Field Tui apijson.Field Username apijson.Field + Watcher apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -1772,6 +1774,26 @@ func (r configTuiJSON) RawJSON() string { return r.raw } +type ConfigWatcher struct { + Ignore []string `json:"ignore"` + JSON configWatcherJSON `json:"-"` +} + +// configWatcherJSON contains the JSON metadata for the struct [ConfigWatcher] +type configWatcherJSON struct { + Ignore apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigWatcher) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configWatcherJSON) RawJSON() string { + return r.raw +} + // Custom keybind configurations type KeybindsConfig struct { // Next agent diff --git a/packages/sdk/go/event.go b/packages/sdk/go/event.go index 5d3bffcc..35b4353c 100644 --- a/packages/sdk/go/event.go +++ b/packages/sdk/go/event.go @@ -64,7 +64,9 @@ type EventListResponse struct { // [EventListResponseEventSessionIdleProperties], // [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionDeletedProperties], - // [EventListResponseEventSessionErrorProperties], [interface{}]. + // [EventListResponseEventSessionErrorProperties], [interface{}], + // [EventListResponseEventFileWatcherUpdatedProperties], + // [EventListResponseEventIdeInstalledProperties]. Properties interface{} `json:"properties,required"` Type EventListResponseType `json:"type,required"` JSON eventListResponseJSON `json:"-"` @@ -107,7 +109,9 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) { // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionUpdated], // [EventListResponseEventSessionDeleted], [EventListResponseEventSessionError], -// [EventListResponseEventServerConnected]. +// [EventListResponseEventServerConnected], +// [EventListResponseEventFileWatcherUpdated], +// [EventListResponseEventIdeInstalled]. func (r EventListResponse) AsUnion() EventListResponseUnion { return r.union } @@ -121,8 +125,10 @@ func (r EventListResponse) AsUnion() EventListResponseUnion { // [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionUpdated], -// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionError] or -// [EventListResponseEventServerConnected]. +// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionError], +// [EventListResponseEventServerConnected], +// [EventListResponseEventFileWatcherUpdated] or +// [EventListResponseEventIdeInstalled]. type EventListResponseUnion interface { implementsEventListResponse() } @@ -191,6 +197,14 @@ func init() { TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventServerConnected{}), }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventFileWatcherUpdated{}), + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventIdeInstalled{}), + }, ) } @@ -1196,6 +1210,144 @@ 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 ( + EventListResponseEventFileWatcherUpdatedPropertiesEventAdd EventListResponseEventFileWatcherUpdatedPropertiesEvent = "add" + EventListResponseEventFileWatcherUpdatedPropertiesEventChange EventListResponseEventFileWatcherUpdatedPropertiesEvent = "change" + EventListResponseEventFileWatcherUpdatedPropertiesEventUnlink EventListResponseEventFileWatcherUpdatedPropertiesEvent = "unlink" +) + +func (r EventListResponseEventFileWatcherUpdatedPropertiesEvent) IsKnown() bool { + switch r { + case EventListResponseEventFileWatcherUpdatedPropertiesEventAdd, EventListResponseEventFileWatcherUpdatedPropertiesEventChange, EventListResponseEventFileWatcherUpdatedPropertiesEventUnlink: + 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 ( @@ -1214,11 +1366,13 @@ const ( EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" 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, EventListResponseTypeSessionCompacted, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionIdle, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionError, EventListResponseTypeServerConnected: + case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeSessionCompacted, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionIdle, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled: return true } return false diff --git a/packages/sdk/go/internal/version.go b/packages/sdk/go/internal/version.go index 870e575a..1f338c33 100644 --- a/packages/sdk/go/internal/version.go +++ b/packages/sdk/go/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.14.0" // x-release-please-version +const PackageVersion = "0.15.0" // x-release-please-version diff --git a/packages/sdk/js/src/gen/client/client.gen.ts b/packages/sdk/js/src/gen/client/client.gen.ts index 34a8d0be..aab8586e 100644 --- a/packages/sdk/js/src/gen/client/client.gen.ts +++ b/packages/sdk/js/src/gen/client/client.gen.ts @@ -1,6 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts import { createSseClient } from "../core/serverSentEvents.gen.js" +import type { HttpMethod } from "../core/types.gen.js" +import { getValidRequestBody } from "../core/utils.gen.js" import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js" import { buildUrl, @@ -49,12 +51,12 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts) } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body) } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === "") { + if (opts.body === undefined || opts.serializedBody === "") { opts.headers.delete("Content-Type") } @@ -69,7 +71,7 @@ export const createClient = (config: Config = {}): Client => { const requestInit: ReqInit = { redirect: "follow", ...opts, - body: opts.serializedBody, + body: getValidRequestBody(opts), } let request = new Request(url, requestInit) @@ -97,18 +99,36 @@ export const createClient = (config: Config = {}): Client => { } if (response.ok) { + const parseAs = + (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" + if (response.status === 204 || response.headers.get("Content-Length") === "0") { + let emptyData: any + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "text": + emptyData = await response[parseAs]() + break + case "formData": + emptyData = new FormData() + break + case "stream": + emptyData = response.body + break + case "json": + default: + emptyData = {} + break + } return opts.responseStyle === "data" - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, } } - const parseAs = - (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json" - let data: any switch (parseAs) { case "arrayBuffer": @@ -178,35 +198,53 @@ export const createClient = (config: Config = {}): Client => { } } - const makeMethod = (method: Required["method"]) => { - const fn = (options: RequestOptions) => request({ ...options, method }) - fn.sse = async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options) - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - url, - }) - } - return fn + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }) + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options) + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init) + for (const fn of interceptors.request._fns) { + if (fn) { + request = await fn(request, opts) + } + } + return request + }, + url, + }) } return { buildUrl, - connect: makeMethod("CONNECT"), - delete: makeMethod("DELETE"), - get: makeMethod("GET"), + connect: makeMethodFn("CONNECT"), + delete: makeMethodFn("DELETE"), + get: makeMethodFn("GET"), getConfig, - head: makeMethod("HEAD"), + head: makeMethodFn("HEAD"), interceptors, - options: makeMethod("OPTIONS"), - patch: makeMethod("PATCH"), - post: makeMethod("POST"), - put: makeMethod("PUT"), + options: makeMethodFn("OPTIONS"), + patch: makeMethodFn("PATCH"), + post: makeMethodFn("POST"), + put: makeMethodFn("PUT"), request, setConfig, - trace: makeMethod("TRACE"), + sse: { + connect: makeSseFn("CONNECT"), + delete: makeSseFn("DELETE"), + get: makeSseFn("GET"), + head: makeSseFn("HEAD"), + options: makeSseFn("OPTIONS"), + patch: makeSseFn("PATCH"), + post: makeSseFn("POST"), + put: makeSseFn("PUT"), + trace: makeSseFn("TRACE"), + }, + trace: makeMethodFn("TRACE"), } as Client } diff --git a/packages/sdk/js/src/gen/client/types.gen.ts b/packages/sdk/js/src/gen/client/types.gen.ts index db8e544c..f16d2cd5 100644 --- a/packages/sdk/js/src/gen/client/types.gen.ts +++ b/packages/sdk/js/src/gen/client/types.gen.ts @@ -20,7 +20,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType + fetch?: typeof fetch /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -128,7 +128,7 @@ export interface ClientOptions { throwOnError?: boolean } -type MethodFnBase = < +type MethodFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, @@ -137,7 +137,7 @@ type MethodFnBase = < options: Omit, "method">, ) => RequestResult -type MethodFnServerSentEvents = < +type SseFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, @@ -146,10 +146,6 @@ type MethodFnServerSentEvents = < options: Omit, "method">, ) => Promise> -type MethodFn = MethodFnBase & { - sse: MethodFnServerSentEvents -} - type RequestFn = < TData = unknown, TError = unknown, @@ -171,7 +167,7 @@ type BuildUrlFn = < options: Pick & Options, ) => string -export type Client = CoreClient & { +export type Client = CoreClient & { interceptors: Middleware } diff --git a/packages/sdk/js/src/gen/client/utils.gen.ts b/packages/sdk/js/src/gen/client/utils.gen.ts index 209bfbe8..71d52f77 100644 --- a/packages/sdk/js/src/gen/client/utils.gen.ts +++ b/packages/sdk/js/src/gen/client/utils.gen.ts @@ -162,14 +162,22 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config } +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = [] + headers.forEach((value, key) => { + entries.push([key, value]) + }) + return entries +} + export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { const mergedHeaders = new Headers() for (const header of headers) { - if (!header || typeof header !== "object") { + if (!header) { continue } - const iterator = header instanceof Headers ? header.entries() : Object.entries(header) + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header) for (const [key, value] of iterator) { if (value === null) { diff --git a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts index 8f7fac54..09ef3fb3 100644 --- a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts @@ -4,6 +4,17 @@ import type { Config } from "./types.gen.js" export type ServerSentEventsOptions = Omit & Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise /** * Callback invoked when a network or parsing error occurs during streaming. * @@ -21,6 +32,7 @@ export type ServerSentEventsOptions = Omit) => void + serializedBody?: RequestInit["body"] /** * Default retry delay in milliseconds. * @@ -64,6 +76,7 @@ export type ServerSentEventsResult({ + onRequest, onSseError, onSseEvent, responseTransformer, @@ -99,7 +112,21 @@ export const createSseClient = ({ } try { - const response = await fetch(url, { ...options, headers, signal }) + const requestInit: RequestInit = { + redirect: "follow", + ...options, + body: options.serializedBody, + headers, + signal, + } + let request = new Request(url, requestInit) + if (onRequest) { + request = await onRequest(url, requestInit) + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch + const response = await _fetch(request) if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) diff --git a/packages/sdk/js/src/gen/core/types.gen.ts b/packages/sdk/js/src/gen/core/types.gen.ts index 16408b2d..bfa77b8a 100644 --- a/packages/sdk/js/src/gen/core/types.gen.ts +++ b/packages/sdk/js/src/gen/core/types.gen.ts @@ -3,24 +3,19 @@ import type { Auth, AuthToken } from "./auth.gen.js" import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js" -export interface Client { +export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace" + +export type Client = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn - connect: MethodFn - delete: MethodFn - get: MethodFn getConfig: () => Config - head: MethodFn - options: MethodFn - patch: MethodFn - post: MethodFn - put: MethodFn request: RequestFn setConfig: (config: Config) => Config - trace: MethodFn -} +} & { + [K in HttpMethod]: MethodFn +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }) export interface Config { /** @@ -47,7 +42,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE" + method?: Uppercase /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/packages/sdk/js/src/gen/core/utils.gen.ts b/packages/sdk/js/src/gen/core/utils.gen.ts index be18c608..8a45f726 100644 --- a/packages/sdk/js/src/gen/core/utils.gen.ts +++ b/packages/sdk/js/src/gen/core/utils.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { QuerySerializer } from "./bodySerializer.gen.js" +import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen.js" import { type ArraySeparatorStyle, serializeArrayParam, @@ -107,3 +107,31 @@ export const getUrl = ({ } return url } + +export function getValidRequestBody(options: { + body?: unknown + bodySerializer?: BodySerializer | null + serializedBody?: unknown +}) { + const hasBody = options.body !== undefined + const isSerializedBody = hasBody && options.bodySerializer + + if (isSerializedBody) { + if ("serializedBody" in options) { + const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== "" + + return hasSerializedBody ? options.serializedBody : null + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== "" ? options.body : null + } + + // plain/text body + if (hasBody) { + return options.body + } + + // no body was provided + return undefined +} diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index 7e0f0dc8..f179afc1 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -6,8 +6,6 @@ import type { ProjectListResponses, ProjectCurrentData, ProjectCurrentResponses, - EventSubscribeData, - EventSubscribeResponses, ConfigGetData, ConfigGetResponses, ToolRegisterData, @@ -101,6 +99,8 @@ import type { AuthSetData, AuthSetResponses, AuthSetErrors, + EventSubscribeData, + EventSubscribeResponses, } from "./types.gen.js" import { client as _heyApiClient } from "./client.gen.js" @@ -153,18 +153,6 @@ class Project extends _HeyApiClient { } } -class Event extends _HeyApiClient { - /** - * Get events - */ - public subscribe(options?: Options) { - return (options?.client ?? this._client).get.sse({ - url: "/event", - ...options, - }) - } -} - class Config extends _HeyApiClient { /** * Get config info @@ -671,6 +659,18 @@ class Auth extends _HeyApiClient { } } +class Event extends _HeyApiClient { + /** + * Get events + */ + public subscribe(options?: Options) { + return (options?.client ?? this._client).sse.get({ + url: "/event", + ...options, + }) + } +} + export class OpencodeClient extends _HeyApiClient { /** * Respond to a permission request @@ -688,7 +688,6 @@ export class OpencodeClient extends _HeyApiClient { }) } project = new Project({ client: this._client }) - event = new Event({ client: this._client }) config = new Config({ client: this._client }) tool = new Tool({ client: this._client }) path = new Path({ client: this._client }) @@ -699,4 +698,5 @@ export class OpencodeClient extends _HeyApiClient { app = new App({ client: this._client }) tui = new Tui({ client: this._client }) auth = new Auth({ client: this._client }) + event = new Event({ client: this._client }) } diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 9e268e3b..e0cf7cdf 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -10,440 +10,6 @@ export type Project = { } } -export type EventInstallationUpdated = { - type: "installation.updated" - properties: { - version: string - } -} - -export type EventLspClientDiagnostics = { - type: "lsp.client.diagnostics" - properties: { - serverID: string - path: string - } -} - -export type UserMessage = { - id: string - sessionID: string - role: "user" - time: { - created: number - } -} - -export type ProviderAuthError = { - name: "ProviderAuthError" - data: { - providerID: string - message: string - } -} - -export type UnknownError = { - name: "UnknownError" - data: { - message: string - } -} - -export type MessageOutputLengthError = { - name: "MessageOutputLengthError" - data: { - [key: string]: unknown - } -} - -export type MessageAbortedError = { - name: "MessageAbortedError" - data: { - message: string - } -} - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError - system: Array - modelID: string - providerID: string - mode: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } -} - -export type Message = UserMessage | AssistantMessage - -export type EventMessageUpdated = { - type: "message.updated" - properties: { - info: Message - } -} - -export type EventMessageRemoved = { - type: "message.removed" - properties: { - sessionID: string - messageID: string - } -} - -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - time?: { - start: number - end?: number - } -} - -export type ReasoningPart = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number - } -} - -export type FilePartSourceText = { - value: string - start: number - end: number -} - -export type FileSource = { - text: FilePartSourceText - type: "file" - path: string -} - -export type Range = { - start: { - line: number - character: number - } - end: { - line: number - character: number - } -} - -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number -} - -export type FilePartSource = FileSource | SymbolSource - -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type ToolStatePending = { - status: "pending" -} - -export type ToolStateRunning = { - status: "running" - input: unknown - title?: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - } -} - -export type ToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - output: string - title: string - metadata: { - [key: string]: unknown - } - time: { - start: number - end: number - compacted?: number - } -} - -export type ToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end: number - } -} - -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState -} - -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" -} - -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } -} - -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string -} - -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array -} - -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type Part = - | TextPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - -export type EventMessagePartUpdated = { - type: "message.part.updated" - properties: { - part: Part - } -} - -export type EventMessagePartRemoved = { - type: "message.part.removed" - properties: { - sessionID: string - messageID: string - partID: string - } -} - -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } -} - -export type Permission = { - id: string - type: string - pattern?: string | Array - sessionID: string - messageID: string - callID?: string - title: string - metadata: { - [key: string]: unknown - } - time: { - created: number - } -} - -export type EventPermissionUpdated = { - type: "permission.updated" - properties: Permission -} - -export type EventPermissionReplied = { - type: "permission.replied" - properties: { - sessionID: string - permissionID: string - response: string - } -} - -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string - } -} - -export type Session = { - id: string - projectID: string - directory: string - parentID?: string - share?: { - url: string - } - title: string - version: string - time: { - created: number - updated: number - compacting?: number - } - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - -export type EventSessionUpdated = { - type: "session.updated" - properties: { - info: Session - } -} - -export type EventSessionDeleted = { - type: "session.deleted" - properties: { - info: Session - } -} - -export type EventSessionError = { - type: "session.error" - properties: { - sessionID?: string - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError - } -} - -export type EventServerConnected = { - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type Event = - | EventInstallationUpdated - | EventLspClientDiagnostics - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCompacted - | EventPermissionUpdated - | EventPermissionReplied - | EventFileEdited - | EventSessionIdle - | EventSessionUpdated - | EventSessionDeleted - | EventSessionError - | EventServerConnected - /** * Custom keybind configurations */ @@ -983,6 +549,297 @@ export type Path = { directory: string } +export type Session = { + id: string + projectID: string + directory: string + parentID?: string + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + } + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } +} + +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} + +export type UnknownError = { + name: "UnknownError" + data: { + message: string + } +} + +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} + +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number + } + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError + system: Array + modelID: string + providerID: string + mode: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } +} + +export type Message = UserMessage | AssistantMessage + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + time?: { + start: number + end?: number + } +} + +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } +} + +export type FilePartSourceText = { + value: string + start: number + end: number +} + +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} + +export type Range = { + start: { + line: number + character: number + } + end: { + line: number + character: number + } +} + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type FilePartSource = FileSource | SymbolSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" +} + +export type ToolStateRunning = { + status: "running" + input: unknown + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } +} + +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } +} + +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } +} + +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState +} + +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } +} + +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type Part = + | TextPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + export type TextPartInput = { id?: string type: "text" @@ -1151,6 +1008,166 @@ export type WellKnownAuth = { export type Auth = OAuth | ApiAuth | WellKnownAuth +export type EventInstallationUpdated = { + type: "installation.updated" + properties: { + version: string + } +} + +export type EventLspClientDiagnostics = { + type: "lsp.client.diagnostics" + properties: { + serverID: string + path: string + } +} + +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message + } +} + +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type EventMessagePartUpdated = { + type: "message.part.updated" + properties: { + part: Part + } +} + +export type EventMessagePartRemoved = { + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } +} + +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type Permission = { + id: string + type: string + pattern?: string | Array + sessionID: string + messageID: string + callID?: string + title: string + metadata: { + [key: string]: unknown + } + time: { + created: number + } +} + +export type EventPermissionUpdated = { + type: "permission.updated" + properties: Permission +} + +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + permissionID: string + response: string + } +} + +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string + } +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string + } +} + +export type EventSessionUpdated = { + type: "session.updated" + properties: { + info: Session + } +} + +export type EventSessionDeleted = { + type: "session.deleted" + properties: { + info: Session + } +} + +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError + } +} + +export type EventServerConnected = { + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + +export type EventIdeInstalled = { + type: "ide.installed" + properties: { + ide: string + } +} + +export type Event = + | EventInstallationUpdated + | EventLspClientDiagnostics + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionCompacted + | EventPermissionUpdated + | EventPermissionReplied + | EventFileEdited + | EventSessionIdle + | EventSessionUpdated + | EventSessionDeleted + | EventSessionError + | EventServerConnected + | EventFileWatcherUpdated + | EventIdeInstalled + export type ProjectListData = { body?: never path?: never @@ -1187,24 +1204,6 @@ export type ProjectCurrentResponses = { export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] -export type EventSubscribeData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/event" -} - -export type EventSubscribeResponses = { - /** - * Event stream - */ - 200: Event -} - -export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] - export type ConfigGetData = { body?: never path?: never @@ -2210,6 +2209,24 @@ export type AuthSetResponses = { export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] +export type EventSubscribeData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/event" +} + +export type EventSubscribeResponses = { + /** + * Event stream + */ + 200: Event +} + +export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] + export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) }