diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md new file mode 100644 index 00000000..53df06b0 --- /dev/null +++ b/.opencode/command/commit.md @@ -0,0 +1,9 @@ +commit and push + +make sure it includes a prefix like +docs: +tui: +core: +ci: +ignore: +wip: diff --git a/.opencode/command/hello.md b/.opencode/command/hello.md new file mode 100644 index 00000000..4484b514 --- /dev/null +++ b/.opencode/command/hello.md @@ -0,0 +1,8 @@ +--- +description: hello world +--- + +hey there $ARGUMENTS + +!`ls` +check out @README.md diff --git a/bun.lock b/bun.lock index eb8c53d2..06fe460b 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "cloud/core": { "name": "@opencode/cloud-core", - "version": "0.5.12", + "version": "0.5.13", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "drizzle-orm": "0.41.0", @@ -40,7 +40,7 @@ }, "cloud/function": { "name": "@opencode/cloud-function", - "version": "0.5.12", + "version": "0.5.13", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -60,7 +60,7 @@ }, "cloud/web": { "name": "@opencode/cloud-web", - "version": "0.5.12", + "version": "0.5.13", "dependencies": { "@kobalte/core": "0.13.9", "@openauthjs/solid": "0.0.0-20250322224806", @@ -79,7 +79,7 @@ }, "packages/function": { "name": "@opencode/function", - "version": "0.5.12", + "version": "0.5.13", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -94,7 +94,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.5.12", + "version": "0.5.13", "bin": { "opencode": "./bin/opencode", }, @@ -144,7 +144,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.5.12", + "version": "0.5.13", "dependencies": { "@opencode-ai/sdk": "workspace:*", }, @@ -156,7 +156,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.5.12", + "version": "0.5.13", "devDependencies": { "@hey-api/openapi-ts": "0.80.1", "@tsconfig/node22": "catalog:", @@ -165,7 +165,7 @@ }, "packages/web": { "name": "@opencode/web", - "version": "0.5.12", + "version": "0.5.13", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -204,6 +204,9 @@ "web-tree-sitter", "tree-sitter-bash", ], + "overrides": { + "zod": "3.25.76", + }, "catalog": { "@hono/zod-validator": "0.4.2", "@tsconfig/node22": "22.0.2", @@ -3063,8 +3066,6 @@ "@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@astrojs/sitemap/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="], - "@astrojs/solid-js/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], "@astrojs/solid-js/vite-plugin-solid": ["vite-plugin-solid@2.11.8", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg=="], @@ -3161,8 +3162,6 @@ "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@modelcontextprotocol/sdk/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="], - "@netlify/dev-utils/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], "@netlify/dev-utils/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -3339,8 +3338,6 @@ "astro/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], - "astro/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -3417,8 +3414,6 @@ "miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], - "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], - "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -3463,8 +3458,6 @@ "opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], - "opencontrol/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - "opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], @@ -3883,8 +3876,6 @@ "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], - "opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="], - "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], diff --git a/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json b/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json new file mode 100644 index 00000000..208b3dd9 --- /dev/null +++ b/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 14 + }, + "auditLog": "/Users/adam/code/opencode/dev/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json", + "files": [ + { + "date": 1755891797740, + "name": "/Users/adam/code/opencode/dev/logs/mcp-puppeteer-2025-08-22.log", + "hash": "dd9b1f2e98b661ba2f56b91dd9afbdb25e50adbdd52ed1b0eef1d2045235d17c" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/mcp-puppeteer-2025-08-22.log b/logs/mcp-puppeteer-2025-08-22.log new file mode 100644 index 00000000..800731d8 --- /dev/null +++ b/logs/mcp-puppeteer-2025-08-22.log @@ -0,0 +1,6 @@ +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:43:17.765"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:43:17.766"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:46:45.539"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:46:45.540"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:53:08.159"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:53:08.160"} diff --git a/opencode.json b/opencode.json index 59f14ac7..f416e91b 100644 --- a/opencode.json +++ b/opencode.json @@ -1,10 +1,7 @@ { "$schema": "https://opencode.ai/config.json", + "mcp": { - "context7": { - "type": "remote", - "url": "https://mcp.context7.com/sse" - }, "weather": { "type": "local", "command": ["opencode", "x", "@h1deya/mcp-server-weather"] diff --git a/package.json b/package.json index 0bd1560c..4e99177c 100644 --- a/package.json +++ b/package.json @@ -51,5 +51,8 @@ "tree-sitter-bash", "web-tree-sitter" ], + "overrides": { + "zod": "3.25.76" + }, "patchedDependencies": {} } diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index 008c168c..c815c732 100755 --- a/packages/opencode/script/schema.ts +++ b/packages/opencode/script/schema.ts @@ -5,6 +5,7 @@ import { Config } from "../src/config/config" import { zodToJsonSchema } from "zod-to-json-schema" const file = process.argv[2] +console.log(file) const result = zodToJsonSchema(Config.Info, { /** diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts new file mode 100644 index 00000000..97dd36a0 --- /dev/null +++ b/packages/opencode/src/command/index.ts @@ -0,0 +1,44 @@ +import z from "zod" +import { App } from "../app/app" +import { Config } from "../config/config" + +export namespace Command { + export const Info = z + .object({ + name: z.string(), + description: z.string().optional(), + agent: z.string().optional(), + model: z.string().optional(), + template: z.string(), + }) + .openapi({ + ref: "Command", + }) + export type Info = z.infer + + const state = App.state("command", async () => { + const cfg = await Config.get() + + const result: Record = {} + + for (const [name, command] of Object.entries(cfg.command ?? {})) { + result[name] = { + name, + agent: command.agent, + model: command.model, + description: command.description, + template: command.template, + } + } + + return result + }) + + export async function get(name: string) { + return state().then((x) => x[name]) + } + + export async function list() { + return state().then((x) => Object.values(x)) + } +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 13a009ad..f707d35f 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -107,6 +107,32 @@ export namespace Config { } throw new InvalidError({ path: item }, { cause: parsed.error }) } + + // Load command markdown files + result.command = result.command || {} + const markdownCommands = [ + ...(await Filesystem.globUp("command/*.md", Global.Path.config, Global.Path.config)), + ...(await Filesystem.globUp(".opencode/command/*.md", app.path.cwd, app.path.root)), + ] + for (const item of markdownCommands) { + const content = await Bun.file(item).text() + const md = matter(content) + if (!md.data) continue + + const config = { + name: path.basename(item, ".md"), + ...md.data, + template: md.content.trim(), + } + const parsed = Command.safeParse(config) + if (parsed.success) { + result.command = mergeDeep(result.command, { + [config.name]: parsed.data, + }) + continue + } + throw new InvalidError({ path: item }, { cause: parsed.error }) + } // Migrate deprecated mode field to agent field for (const [name, mode] of Object.entries(result.mode)) { result.agent = mergeDeep(result.agent ?? {}, { @@ -192,6 +218,14 @@ export namespace Config { export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")]) export type Permission = z.infer + export const Command = z.object({ + template: z.string(), + description: z.string().optional(), + agent: z.string().optional(), + model: z.string().optional(), + }) + export type Command = z.infer + export const Agent = z .object({ model: z.string().optional(), @@ -305,6 +339,7 @@ export namespace Config { theme: z.string().optional().describe("Theme name to use for the interface"), keybinds: Keybinds.optional().describe("Custom keybind configurations"), tui: TUI.optional().describe("TUI specific settings"), + command: z.record(z.string(), Command).optional(), plugin: z.string().array().optional(), snapshot: z.boolean().optional(), share: z diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2fe22c77..e37f11f0 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -36,9 +36,9 @@ export namespace Provider { }, } }, - async opencode() { + async opencode(input) { return { - autoload: true, + autoload: Object.keys(input.models).length > 0, options: {}, } }, diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 69805fbe..31951eed 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -21,6 +21,7 @@ import { Permission } from "../permission" import { lazy } from "../util/lazy" import { Agent } from "../agent/agent" import { Auth } from "../auth" +import { Command } from "../command" const ERRORS = { 400: { @@ -611,10 +612,12 @@ export namespace Server { description: "Created message", content: { "application/json": { - schema: resolver(z.object({ + schema: resolver( + z.object({ info: MessageV2.Assistant, parts: MessageV2.Part.array(), - })), + }), + ), }, }, }, @@ -634,6 +637,41 @@ export namespace Server { return c.json(msg) }, ) + .post( + "/session/:id/command", + describeRoute({ + description: "Send a new command to a session", + operationId: "session.command", + responses: { + 200: { + description: "Created message", + content: { + "application/json": { + schema: resolver( + z.object({ + info: MessageV2.Assistant, + parts: MessageV2.Part.array(), + }), + ), + }, + }, + }, + }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + }), + ), + zValidator("json", Session.CommandInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await Session.command({ ...body, sessionID }) + return c.json(msg) + }, + ) .post( "/session/:id/shell", describeRoute({ @@ -656,7 +694,7 @@ export namespace Server { id: z.string().openapi({ description: "Session ID" }), }), ), - zValidator("json", Session.CommandInput.omit({ sessionID: true })), + zValidator("json", Session.ShellInput.omit({ sessionID: true })), async (c) => { const sessionID = c.req.valid("param").id const body = c.req.valid("json") @@ -753,6 +791,27 @@ export namespace Server { 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({ diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 24cffdef..37362f34 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -47,6 +47,8 @@ import { Permission } from "../permission" import { Wildcard } from "../util/wildcard" import { ulid } from "ulid" import { defer } from "../util/defer" +import { Command } from "../command" +import { $ } from "bun" export namespace Session { const log = Log.create({ service: "session" }) @@ -1025,13 +1027,13 @@ export namespace Session { return result } - export const CommandInput = z.object({ + export const ShellInput = z.object({ sessionID: Identifier.schema("session"), agent: z.string(), command: z.string(), }) - export type CommandInput = z.infer - export async function shell(input: CommandInput) { + export type ShellInput = z.infer + export async function shell(input: ShellInput) { using abort = lock(input.sessionID) const msg: MessageV2.Assistant = { id: Identifier.ascending("message"), @@ -1155,6 +1157,72 @@ export namespace Session { return { info: msg, parts: [part] } } + export const CommandInput = z.object({ + messageID: Identifier.schema("message").optional(), + sessionID: Identifier.schema("session"), + agent: z.string().optional(), + model: z.string().optional(), + arguments: z.string(), + command: z.string(), + }) + export type CommandInput = z.infer + const bashRegex = /!`([^`]+)`/g + const fileRegex = /@([^\s]+)/g + + export async function command(input: CommandInput) { + const command = await Command.get(input.command) + const agent = input.agent ?? command.agent ?? "build" + const model = + input.model ?? + command.model ?? + (await Agent.get(agent).then((x) => (x.model ? `${x.model.providerID}/${x.model.modelID}` : undefined))) ?? + (await Provider.defaultModel().then((x) => `${x.providerID}/${x.modelID}`)) + let template = command.template.replace("$ARGUMENTS", input.arguments) + + const bash = Array.from(template.matchAll(bashRegex)) + if (bash.length > 0) { + const results = await Promise.all( + bash.map(async ([, cmd]) => { + try { + return await $`${{ raw: cmd }}`.nothrow().text() + } catch (error) { + return `Error executing command: ${error instanceof Error ? error.message : String(error)}` + } + }), + ) + let index = 0 + template = template.replace(bashRegex, () => results[index++]) + } + + const parts = [ + { + type: "text", + text: template, + }, + ] as ChatInput["parts"] + + const matches = template.matchAll(fileRegex) + const app = App.info() + + for (const match of matches) { + const file = path.join(app.path.cwd, match[1]) + parts.push({ + type: "file", + url: `file://${file}`, + filename: match[1], + mime: "text/plain", + }) + } + + return chat({ + sessionID: input.sessionID, + messageID: input.messageID, + ...Provider.parseModel(model!), + agent, + parts, + }) + } + function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) { const toolcalls: Record = {} let snapshot: string | undefined diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 5f222f03..149b4c62 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 39 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-be3e40e0bf7dde2bb15ff82d5d104418fb47fe335808a1aa6468b0be2210a88f.yml -openapi_spec_hash: c1bbb3ebd807656bd9f31a618077e76b -config_hash: eab3723c4c2232a6ba1821151259d6da +configured_endpoints: 41 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d5200eaa145f567a58daa78941ab1141dd63f5f0cfe1596d5c9ecf12d34fea35.yml +openapi_spec_hash: abeb66291dc158f2cdc90bf9945e283e +config_hash: fb625e876313a9f8f31532348fa91f59 diff --git a/packages/sdk/go/api.md b/packages/sdk/go/api.md index 5accfcb5..2f9eadb6 100644 --- a/packages/sdk/go/api.md +++ b/packages/sdk/go/api.md @@ -70,6 +70,16 @@ Methods: - client.Config.Get(ctx context.Context) (opencode.Config, error) +# Command + +Response Types: + +- opencode.Command + +Methods: + +- client.Command.List(ctx context.Context) ([]opencode.Command, error) + # Session Params Types: @@ -106,6 +116,7 @@ Response Types: - opencode.ToolStateRunning - opencode.UserMessage - opencode.SessionChatResponse +- opencode.SessionCommandResponse - opencode.SessionMessageResponse - opencode.SessionMessagesResponse @@ -118,6 +129,7 @@ Methods: - client.Session.Abort(ctx context.Context, id string) (bool, error) - client.Session.Chat(ctx context.Context, id string, body opencode.SessionChatParams) (opencode.SessionChatResponse, error) - client.Session.Children(ctx context.Context, id string) ([]opencode.Session, error) +- client.Session.Command(ctx context.Context, id string, body opencode.SessionCommandParams) (opencode.SessionCommandResponse, error) - client.Session.Get(ctx context.Context, id string) (opencode.Session, error) - client.Session.Init(ctx context.Context, id string, body opencode.SessionInitParams) (bool, error) - client.Session.Message(ctx context.Context, id string, messageID string) (opencode.SessionMessageResponse, error) diff --git a/packages/sdk/go/client.go b/packages/sdk/go/client.go index 6baf21a8..286408ab 100644 --- a/packages/sdk/go/client.go +++ b/packages/sdk/go/client.go @@ -21,6 +21,7 @@ type Client struct { Find *FindService File *FileService Config *ConfigService + Command *CommandService Session *SessionService Tui *TuiService } @@ -49,6 +50,7 @@ func NewClient(opts ...option.RequestOption) (r *Client) { r.Find = NewFindService(opts...) r.File = NewFileService(opts...) r.Config = NewConfigService(opts...) + r.Command = NewCommandService(opts...) r.Session = NewSessionService(opts...) r.Tui = NewTuiService(opts...) diff --git a/packages/sdk/go/command.go b/packages/sdk/go/command.go new file mode 100644 index 00000000..9ca70c3a --- /dev/null +++ b/packages/sdk/go/command.go @@ -0,0 +1,67 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// CommandService contains methods and other services that help with interacting +// with the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewCommandService] method instead. +type CommandService struct { + Options []option.RequestOption +} + +// NewCommandService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewCommandService(opts ...option.RequestOption) (r *CommandService) { + r = &CommandService{} + r.Options = opts + return +} + +// List all commands +func (r *CommandService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Command, err error) { + opts = append(r.Options[:], opts...) + path := "command" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type Command struct { + Name string `json:"name,required"` + Template string `json:"template,required"` + Agent string `json:"agent"` + Description string `json:"description"` + Model string `json:"model"` + JSON commandJSON `json:"-"` +} + +// commandJSON contains the JSON metadata for the struct [Command] +type commandJSON struct { + Name apijson.Field + Template apijson.Field + Agent apijson.Field + Description apijson.Field + Model apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Command) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r commandJSON) RawJSON() string { + return r.raw +} diff --git a/packages/sdk/go/command_test.go b/packages/sdk/go/command_test.go new file mode 100644 index 00000000..5fd8c37b --- /dev/null +++ b/packages/sdk/go/command_test.go @@ -0,0 +1,36 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestCommandList(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Command.List(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index aae6e5e2..59db54b9 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -49,7 +49,8 @@ type Config struct { // automatically Autoshare bool `json:"autoshare"` // Automatically update to the latest version - Autoupdate bool `json:"autoupdate"` + Autoupdate bool `json:"autoupdate"` + Command map[string]ConfigCommand `json:"command"` // Disable providers that are loaded automatically DisabledProviders []string `json:"disabled_providers"` Experimental ConfigExperimental `json:"experimental"` @@ -94,6 +95,7 @@ type configJSON struct { Agent apijson.Field Autoshare apijson.Field Autoupdate apijson.Field + Command apijson.Field DisabledProviders apijson.Field Experimental apijson.Field Formatter apijson.Field @@ -664,6 +666,32 @@ func (r ConfigAgentPlanPermissionWebfetch) IsKnown() bool { return false } +type ConfigCommand struct { + Template string `json:"template,required"` + Agent string `json:"agent"` + Description string `json:"description"` + Model string `json:"model"` + JSON configCommandJSON `json:"-"` +} + +// configCommandJSON contains the JSON metadata for the struct [ConfigCommand] +type configCommandJSON struct { + Template apijson.Field + Agent apijson.Field + Description apijson.Field + Model apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigCommand) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configCommandJSON) RawJSON() string { + return r.raw +} + type ConfigExperimental struct { Hook ConfigExperimentalHook `json:"hook"` JSON configExperimentalJSON `json:"-"` diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go index a4ad09e2..237b490d 100644 --- a/packages/sdk/go/session.go +++ b/packages/sdk/go/session.go @@ -114,6 +114,18 @@ func (r *SessionService) Children(ctx context.Context, id string, opts ...option return } +// Send a new command to a session +func (r *SessionService) Command(ctx context.Context, id string, body SessionCommandParams, opts ...option.RequestOption) (res *SessionCommandResponse, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/command", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + // Get session func (r *SessionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) @@ -2301,6 +2313,29 @@ func (r sessionChatResponseJSON) RawJSON() string { return r.raw } +type SessionCommandResponse struct { + Info AssistantMessage `json:"info,required"` + Parts []Part `json:"parts,required"` + JSON sessionCommandResponseJSON `json:"-"` +} + +// sessionCommandResponseJSON contains the JSON metadata for the struct +// [SessionCommandResponse] +type sessionCommandResponseJSON struct { + Info apijson.Field + Parts apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionCommandResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionCommandResponseJSON) RawJSON() string { + return r.raw +} + type SessionMessageResponse struct { Info Message `json:"info,required"` Parts []Part `json:"parts,required"` @@ -2419,6 +2454,18 @@ func (r SessionChatParamsPartsType) IsKnown() bool { return false } +type SessionCommandParams struct { + Arguments param.Field[string] `json:"arguments,required"` + Command param.Field[string] `json:"command,required"` + Agent param.Field[string] `json:"agent"` + MessageID param.Field[string] `json:"messageID"` + Model param.Field[string] `json:"model"` +} + +func (r SessionCommandParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + type SessionInitParams struct { MessageID param.Field[string] `json:"messageID,required"` ModelID param.Field[string] `json:"modelID,required"` diff --git a/packages/sdk/go/session_test.go b/packages/sdk/go/session_test.go index 58e68dc1..d67be255 100644 --- a/packages/sdk/go/session_test.go +++ b/packages/sdk/go/session_test.go @@ -199,6 +199,38 @@ func TestSessionChildren(t *testing.T) { } } +func TestSessionCommandWithOptionalParams(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Command( + context.TODO(), + "id", + opencode.SessionCommandParams{ + Arguments: opencode.F("arguments"), + Command: opencode.F("command"), + Agent: opencode.F("agent"), + MessageID: opencode.F("msg"), + Model: opencode.F("model"), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestSessionGet(t *testing.T) { t.Skip("skipped: tests are disabled for the time being") baseURL := "http://localhost:4010" diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index b5e05540..b00216b8 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -39,6 +39,8 @@ import type { SessionChatResponses, SessionMessageData, SessionMessageResponses, + SessionCommandData, + SessionCommandResponses, SessionShellData, SessionShellResponses, SessionRevertData, @@ -47,6 +49,8 @@ import type { SessionUnrevertResponses, PostSessionByIdPermissionsByPermissionIdData, PostSessionByIdPermissionsByPermissionIdResponses, + CommandListData, + CommandListResponses, ConfigProvidersData, ConfigProvidersResponses, FindTextData, @@ -355,6 +359,20 @@ class Session extends _HeyApiClient { }) } + /** + * Send a new command to a session + */ + public command(options: Options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/command", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + } + /** * Run a shell command */ @@ -394,6 +412,18 @@ class Session extends _HeyApiClient { } } +class Command extends _HeyApiClient { + /** + * List all commands + */ + public list(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/command", + ...options, + }) + } +} + class Find extends _HeyApiClient { /** * Find text in files @@ -592,6 +622,7 @@ export class OpencodeClient extends _HeyApiClient { app = new App({ client: this._client }) config = new Config({ client: this._client }) session = new Session({ client: this._client }) + command = new Command({ client: this._client }) find = new Find({ client: this._client }) file = new File({ client: this._client }) tui = new Tui({ client: this._client }) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 9919dc41..8e9662ad 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -585,6 +585,14 @@ export type Config = { */ scroll_speed: number } + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + } + } plugin?: Array snapshot?: boolean /** @@ -1110,6 +1118,14 @@ export type AgentPartInput = { } } +export type Command = { + name: string + description?: string + agent?: string + model?: string + template: string +} + export type Symbol = { name: string kind: number @@ -1563,6 +1579,36 @@ export type SessionMessageResponses = { export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] +export type SessionCommandData = { + body?: { + messageID?: string + agent?: string + model?: string + arguments: string + command: string + } + path: { + /** + * Session ID + */ + id: string + } + query?: never + url: "/session/{id}/command" +} + +export type SessionCommandResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] + export type SessionShellData = { body?: { agent: string @@ -1648,6 +1694,22 @@ export type PostSessionByIdPermissionsByPermissionIdResponses = { export type PostSessionByIdPermissionsByPermissionIdResponse = PostSessionByIdPermissionsByPermissionIdResponses[keyof PostSessionByIdPermissionsByPermissionIdResponses] +export type CommandListData = { + body?: never + path?: never + query?: never + url: "/command" +} + +export type CommandListResponses = { + /** + * List of commands + */ + 200: Array +} + +export type CommandListResponse = CommandListResponses[keyof CommandListResponses] + export type ConfigProvidersData = { body?: never path?: never diff --git a/packages/sdk/stainless/stainless.yml b/packages/sdk/stainless/stainless.yml index e0c040ec..3dd34a41 100644 --- a/packages/sdk/stainless/stainless.yml +++ b/packages/sdk/stainless/stainless.yml @@ -85,6 +85,12 @@ resources: methods: get: get /config + command: + models: + command: Command + methods: + list: get /command + session: models: session: Session @@ -126,6 +132,7 @@ resources: message: get /session/{id}/message/{messageID} messages: get /session/{id}/message chat: post /session/{id}/message + command: post /session/{id}/command shell: post /session/{id}/shell update: patch /session/{id} revert: post /session/{id}/revert diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 0c703c95..ecf95ff9 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -84,6 +84,10 @@ type SendPrompt = Prompt type SendShell = struct { Command string } +type SendCommand = struct { + Command string + Args string +} type SetEditorContentMsg struct { Text string } @@ -183,6 +187,11 @@ func New( slog.Debug("Loaded config", "config", configInfo) + customCommands, err := httpClient.Command.List(ctx) + if err != nil { + return nil, err + } + app := &App{ Info: appInfo, Agents: agents, @@ -194,7 +203,7 @@ func New( AgentIndex: agentIndex, Session: &opencode.Session{}, Messages: []Message{}, - Commands: commands.LoadFromConfig(configInfo), + Commands: commands.LoadFromConfig(configInfo, *customCommands), InitialModel: initialModel, InitialPrompt: initialPrompt, InitialAgent: initialAgent, @@ -793,6 +802,38 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) { return a, tea.Batch(cmds...) } +func (a *App) SendCommand(ctx context.Context, command string, args string) (*App, tea.Cmd) { + var cmds []tea.Cmd + if a.Session.ID == "" { + session, err := a.CreateSession(ctx) + if err != nil { + return a, toast.NewErrorToast(err.Error()) + } + a.Session = session + cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session})) + } + + cmds = append(cmds, func() tea.Msg { + _, err := a.Client.Session.Command( + context.Background(), + a.Session.ID, + opencode.SessionCommandParams{ + Command: opencode.F(command), + Arguments: opencode.F(args), + }, + ) + if err != nil { + slog.Error("Failed to execute command", "error", err) + return toast.NewErrorToast("Failed to execute command") + } + return nil + }) + + // The actual response will come through SSE + // For now, just return success + return a, tea.Batch(cmds...) +} + func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) { var cmds []tea.Cmd if a.Session.ID == "" { diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go index bd5d61b9..3a5287ca 100644 --- a/packages/tui/internal/commands/command.go +++ b/packages/tui/internal/commands/command.go @@ -31,6 +31,7 @@ type Command struct { Description string Keybindings []Keybinding Trigger []string + Custom bool } func (c Command) Keys() []string { @@ -96,6 +97,7 @@ func (r CommandRegistry) Sorted() []Command { }) return commands } + func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command { var matched []Command for _, command := range r.Sorted() { @@ -182,7 +184,7 @@ func parseBindings(bindings ...string) []Keybinding { return parsedBindings } -func LoadFromConfig(config *opencode.Config) CommandRegistry { +func LoadFromConfig(config *opencode.Config, customCommands []opencode.Command) CommandRegistry { defaults := []Command{ { Name: AppHelpCommand, @@ -400,6 +402,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { } registry[command.Name] = command } + for _, command := range customCommands { + registry[CommandName(command.Name)] = Command{ + Name: CommandName(command.Name), + Description: command.Description, + Trigger: []string{command.Name}, + Keybindings: []Keybinding{}, + Custom: true, + } + } + slog.Info("Loaded commands", "commands", registry) return registry } diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index c5ecdc21..0c52ca84 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -224,10 +224,17 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case dialog.CompletionSelectedMsg: switch msg.Item.ProviderID { case "commands": - commandName := strings.TrimPrefix(msg.Item.Value, "/") + command := msg.Item.RawData.(commands.Command) + if command.Custom { + m.SetValue("/" + command.PrimaryTrigger() + " ") + return m, nil + } + updated, cmd := m.Clear() m = updated.(*editorComponent) cmds = append(cmds, cmd) + + commandName := strings.TrimPrefix(msg.Item.Value, "/") cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]))) return m, tea.Batch(cmds...) case "files": @@ -481,6 +488,25 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { } var cmds []tea.Cmd + if strings.HasPrefix(value, "/") { + value = value[1:] + commandName := strings.Split(value, " ")[0] + command := m.app.Commands[commands.CommandName(commandName)] + if command.Custom { + args := strings.TrimPrefix(value, command.PrimaryTrigger()+" ") + cmds = append( + cmds, + util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}), + ) + + updated, cmd := m.Clear() + m = updated.(*editorComponent) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) + } + } + attachments := m.textarea.GetAttachments() prompt := app.Prompt{Text: value, Attachments: attachments} diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 97c52972..a299d65a 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -174,6 +174,10 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.GotoBottom() m.tail = true return m, nil + case app.SendCommand: + m.viewport.GotoBottom() + m.tail = true + return m, nil case dialog.ThemeSelectedMsg: m.cache.Clear() m.loading = true diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 26a1ba25..f7ce7982 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -408,6 +408,24 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.app, cmd = a.app.SendPrompt(context.Background(), msg) cmds = append(cmds, cmd) } + case app.SendCommand: + // If we're in a child session, switch back to parent before sending prompt + if a.app.Session.ParentID != "" { + parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID) + if err != nil { + slog.Error("Failed to get parent session", "error", err) + return a, toast.NewErrorToast("Failed to get parent session") + } + a.app.Session = parentSession + a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args) + cmds = append(cmds, tea.Sequence( + util.CmdHandler(app.SessionSelectedMsg(parentSession)), + cmd, + )) + } else { + a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args) + cmds = append(cmds, cmd) + } case app.SendShell: // If we're in a child session, switch back to parent before sending prompt if a.app.Session.ParentID != "" { diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 5e58d00e..b537b79f 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -67,14 +67,7 @@ export default defineConfig({ { label: "Usage", - items: [ - "docs/tui", - "docs/cli", - "docs/ide", - "docs/share", - "docs/github", - "docs/gitlab" - ], + items: ["docs/tui", "docs/cli", "docs/ide", "docs/share", "docs/github", "docs/gitlab"], }, { diff --git a/packages/web/src/content/docs/docs/commands.mdx b/packages/web/src/content/docs/docs/commands.mdx new file mode 100644 index 00000000..93be346c --- /dev/null +++ b/packages/web/src/content/docs/docs/commands.mdx @@ -0,0 +1,167 @@ +--- +title: Commands +description: Create custom commands for repetitive tasks. +--- + +Define custom commands to automate repetitive coding tasks. + +--- + +## Create command files + +Create markdown files in the `command/` directory to define custom commands. + +Create `.opencode/command/test.md`: + +```md +--- +description: Run tests with coverage +agent: build +model: anthropic/claude-3-5-sonnet-20241022 +--- + +Run the full test suite with coverage report and show any failures. +Focus on the failing tests and suggest fixes. +``` + +The frontmatter defines command properties. The content becomes the template. + +Use the command by typing `/` followed by the command name. + +```bash frame="none" +"/test" +``` + +--- + +## Create command files + +For complex commands, create markdown files in the `command/` directory. + +Create `.opencode/command/test.md`: + +```md +--- +description: Run tests with coverage +agent: build +model: anthropic/claude-3-5-sonnet-20241022 +--- + +Run the full test suite with coverage report and show any failures. +Focus on the failing tests and suggest fixes. +``` + +The frontmatter defines command properties. The content becomes the template. + +--- + +## Use arguments + +Pass arguments to commands using the `$ARGUMENTS` placeholder. + +```md +--- +description: Create a new component +--- + +Create a new React component named $ARGUMENTS with TypeScript support. +Include proper typing and basic structure. +``` + +Run the command with arguments: + +```bash frame="none" +"/component Button" +``` + +--- + +## Inject shell output + +Use `!`command`` to inject shell command output into your prompt. + +```md +--- +description: Analyze test coverage +--- + +Here are the current test results: +`!npm test` + +Based on these results, suggest improvements to increase coverage. +``` + +```md +--- +description: Review recent changes +--- + +Recent git commits: +`!git log --oneline -10` + +Review these changes and suggest any improvements. +``` + +Commands run in your project's root directory and their output becomes part of the prompt. + +--- + +## Reference files + +Include files in your command using `@` followed by the filename. + +```md +--- +description: Review component +--- + +Review the component in @src/components/Button.tsx. +Check for performance issues and suggest improvements. +``` + +The file content gets included in the prompt automatically. + +--- + +## Command properties + +Configure commands with these optional frontmatter properties: + +- **description**: Brief explanation of what the command does +- **agent**: Agent to use (defaults to "build") +- **model**: Specific model to use for this command + +```md +--- +description: Code review assistant +agent: build +model: anthropic/claude-3-5-sonnet-20241022 +--- + +Review the code for best practices and suggest improvements. +``` + +--- + +## Command directory + +Store command files in these locations: + +- `.opencode/command/` - Project-specific commands +- `command/` - Global commands in config directory + +Project commands take precedence over global ones. + +--- + +## Built-in commands + +opencode includes several built-in commands: + +- `/init` - Initialize project and create AGENTS.md +- `/undo` - Revert the last changes +- `/redo` - Restore reverted changes +- `/share` - Share the current conversation +- `/help` - Show available commands and keybinds + +Use `/help` to see all available commands in your setup. diff --git a/packages/web/src/content/docs/docs/index.mdx b/packages/web/src/content/docs/docs/index.mdx index 858a605b..31587850 100644 --- a/packages/web/src/content/docs/docs/index.mdx +++ b/packages/web/src/content/docs/docs/index.mdx @@ -41,26 +41,10 @@ You can also install it with the following: - **Using Node.js** - - ```bash - npm install -g opencode-ai - ``` - - - ```bash - bun install -g opencode-ai - ``` - - - ```bash - pnpm install -g opencode-ai - ``` - - - ```bash - yarn global add opencode-ai - ``` - + ```bash npm install -g opencode-ai ``` + ```bash bun install -g opencode-ai ``` + ```bash pnpm install -g opencode-ai ``` + ```bash yarn global add opencode-ai ``` - **Using Homebrew on macOS and Linux** @@ -308,4 +292,4 @@ Here's an [example conversation](https://opencode.ai/s/4XP1fce5) with opencode. And that's it! You are now a pro at using opencode. -To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), or playing around with the [opencode config](/docs/config). +To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), [creating custom commands](/docs/commands), or playing around with the [opencode config](/docs/config).