diff --git a/.github/workflows/sdk-gitpkg.yml b/.github/workflows/sdk-gitpkg.yml new file mode 100644 index 00000000000..8b1dee2d4fa --- /dev/null +++ b/.github/workflows/sdk-gitpkg.yml @@ -0,0 +1,88 @@ +name: sdk-gitpkg + +on: + push: + branches: + - dev + +permissions: + contents: write + +jobs: + gitpkg: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: ./.github/actions/setup-bun + + - uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Read metadata + id: meta + run: | + SHA=$(git rev-parse --short HEAD) + echo "sha=$SHA" >> $GITHUB_OUTPUT + + - name: Check release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="@opencode-ai/sdk-${{ steps.meta.outputs.sha }}-gitpkg" + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release already exists: $TAG" + exit 1 + fi + if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then + echo "Tag already exists: $TAG" + exit 1 + fi + + - name: Build sdk + run: bun run --cwd packages/sdk/js build + + - name: Prepare gitpkg exports + run: bun ./packages/sdk/js/script/gitpkg.ts + + - name: Pack sdk + working-directory: packages/sdk/js + run: bun pm pack + + - name: Rename tarball + id: pack + working-directory: packages/sdk/js + run: | + FILE=$(ls opencode-ai-sdk-*.tgz) + NAME="opencode-ai-sdk-${{ steps.meta.outputs.sha }}-gitpkg.tgz" + mv "$FILE" "$NAME" + echo "tarball=$NAME" >> $GITHUB_OUTPUT + + - name: Commit gitpkg + run: | + git config user.email "github-actions@users.noreply.github.com" + git config user.name "github-actions" + git add -f packages/sdk/js/dist + git add packages/sdk/js/package.json + git commit -m "chore: prepare sdk gitpkg ${{ steps.meta.outputs.sha }}" + + - name: Create root tag commit + run: | + git subtree split --prefix=packages/sdk/js -b sdk-gitpkg-root + + - name: Tag gitpkg + run: | + TAG="@opencode-ai/sdk-${{ steps.meta.outputs.sha }}-gitpkg" + git tag "$TAG" sdk-gitpkg-root + git push origin "$TAG" + + - name: Create release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="@opencode-ai/sdk-${{ steps.meta.outputs.sha }}-gitpkg" + FILE="packages/sdk/js/${{ steps.pack.outputs.tarball }}" + gh release create "$TAG" "$FILE" --title "$TAG" --notes "Commit: ${{ steps.meta.outputs.sha }}" diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts index 0fb2a5e9d2e..b3820a3c6d4 100644 --- a/packages/opencode/src/server/routes/experimental.ts +++ b/packages/opencode/src/server/routes/experimental.ts @@ -2,6 +2,11 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { ToolRegistry } from "../../tool/registry" +import { Session } from "../../session" +import { Agent } from "../../agent/agent" +import { Storage } from "../../storage/storage" +import { Tool } from "../../tool/tool" +import { PermissionNext } from "@/permission/next" import { Worktree } from "../../worktree" import { Instance } from "../../project/instance" import { Project } from "../../project/project" @@ -86,6 +91,90 @@ export const ExperimentalRoutes = lazy(() => ) }, ) + .post( + "/tool/execute", + describeRoute({ + summary: "Execute tool", + description: "Execute a specific tool with the provided arguments. Returns the tool output.", + operationId: "tool.execute", + responses: { + 200: { + description: "Tool execution result", + content: { + "application/json": { + schema: resolver( + z + .object({ + title: z.string(), + output: z.string(), + metadata: z.record(z.string(), z.any()).optional(), + }) + .meta({ ref: "ToolExecuteResult" }), + ), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "json", + z.object({ + sessionID: z.string().meta({ description: "Session ID for context" }), + messageID: z.string().meta({ description: "Message ID for context" }), + providerID: z.string().meta({ description: "Provider ID for tool filtering" }), + modelID: z.string().meta({ description: "Model ID for tool filtering" }), + toolID: z.string().meta({ description: "Tool ID to execute" }), + args: z.record(z.string(), z.any()).meta({ description: "Tool arguments" }), + agent: z.string().optional().meta({ description: "Agent name (optional)" }), + callID: z.string().optional().meta({ description: "Tool call ID (optional)" }), + }), + ), + async (c) => { + const body = c.req.valid("json") + const session = await Session.get(body.sessionID) + const agentName = body.agent ?? (await Agent.defaultAgent()) + const agent = await Agent.get(agentName) + if (!agent) { + throw new Storage.NotFoundError({ message: `Agent not found: ${agentName}` }) + } + + const tools = await ToolRegistry.tools({ providerID: body.providerID, modelID: body.modelID }, agent) + const tool = tools.find((t) => t.id === body.toolID) + if (!tool) { + throw new Storage.NotFoundError({ message: `Tool not found: ${body.toolID}` }) + } + + const abortController = new AbortController() + let currentMetadata: { title?: string; metadata?: Record } = {} + + const ctx: Tool.Context = { + sessionID: body.sessionID, + messageID: body.messageID, + agent: agentName, + abort: abortController.signal, + callID: body.callID, + metadata: (input) => { + currentMetadata = input + }, + ask: async (req) => { + await PermissionNext.ask({ + ...req, + sessionID: session.id, + tool: body.callID ? { messageID: body.messageID, callID: body.callID } : undefined, + ruleset: PermissionNext.merge(agent.permission, session.permission ?? []), + }) + }, + } + + const result = await tool.execute(body.args, ctx) + return c.json({ + title: result.title || currentMetadata.title || "", + output: result.output, + metadata: result.metadata, + }) + }, + ) .post( "/worktree", describeRoute({ diff --git a/packages/sdk/js/script/gitpkg.ts b/packages/sdk/js/script/gitpkg.ts new file mode 100644 index 00000000000..79e541f310f --- /dev/null +++ b/packages/sdk/js/script/gitpkg.ts @@ -0,0 +1,26 @@ +#!/usr/bin/env bun + +const dir = new URL("..", import.meta.url).pathname +process.chdir(dir) + +const pkg = await import("../package.json").then((m) => m.default) +const next = JSON.parse(JSON.stringify(pkg)) + +const items = Object.entries(next.exports) +for (const item of items) { + const key = item[0] + const value = item[1] + const data = + typeof value === "object" && value !== null && "import" in value + ? (value as { import?: unknown }).import + : undefined + const text = typeof value === "string" ? value : data + if (typeof text !== "string") continue + const file = text.replace("./src/", "./dist/").replace(".ts", "") + next.exports[key] = { + import: file + ".js", + types: file + ".d.ts", + } +} + +await Bun.write("package.json", JSON.stringify(next, null, 2)) diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index 5e3e67e1c03..7b32b726317 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -36,6 +36,9 @@ import type { ToolListData, ToolListResponses, ToolListErrors, + ToolExecuteData, + ToolExecuteErrors, + ToolExecuteResponses, InstanceDisposeData, InstanceDisposeResponses, PathGetData, @@ -390,6 +393,22 @@ class Tool extends _HeyApiClient { ...options, }) } + + /** + * Execute tool + * + * Execute a specific tool with the provided arguments. Returns the tool output. + */ + public execute(options?: Options) { + return (options?.client ?? this._client).post({ + url: "/experimental/tool/execute", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }) + } } class Instance extends _HeyApiClient { diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index ca13e5e93cf..67906451443 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1390,6 +1390,14 @@ export type ToolListItem = { export type ToolList = Array +export type ToolExecuteResult = { + title: string + output: string + metadata?: { + [key: string]: unknown + } +} + export type Path = { state: string config: string @@ -2004,6 +2012,72 @@ export type ToolListResponses = { export type ToolListResponse = ToolListResponses[keyof ToolListResponses] +export type ToolExecuteData = { + body?: { + /** + * Session ID for context + */ + sessionID: string + /** + * Message ID for context + */ + messageID: string + /** + * Provider ID for tool filtering + */ + providerID: string + /** + * Model ID for tool filtering + */ + modelID: string + /** + * Tool ID to execute + */ + toolID: string + /** + * Tool arguments + */ + args: { + [key: string]: unknown + } + /** + * Agent name (optional) + */ + agent?: string + /** + * Tool call ID (optional) + */ + callID?: string + } + path?: never + query?: { + directory?: string + } + url: "/experimental/tool/execute" +} + +export type ToolExecuteErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type ToolExecuteError = ToolExecuteErrors[keyof ToolExecuteErrors] + +export type ToolExecuteResponses = { + /** + * Tool execution result + */ + 200: ToolExecuteResult +} + +export type ToolExecuteResponse = ToolExecuteResponses[keyof ToolExecuteResponses] + export type InstanceDisposeData = { body?: never path?: never diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 6f699319965..23f9e9f3fef 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -136,6 +136,8 @@ import type { SessionUpdateResponses, SubtaskPartInput, TextPartInput, + ToolExecuteErrors, + ToolExecuteResponses, ToolIdsErrors, ToolIdsResponses, ToolListErrors, @@ -651,6 +653,57 @@ export class Tool extends HeyApiClient { ...params, }) } + + /** + * Execute tool + * + * Execute a specific tool with the provided arguments. Returns the tool output. + */ + public execute( + parameters?: { + directory?: string + sessionID?: string + messageID?: string + providerID?: string + modelID?: string + toolID?: string + args?: { + [key: string]: unknown + } + agent?: string + callID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "body", key: "sessionID" }, + { in: "body", key: "messageID" }, + { in: "body", key: "providerID" }, + { in: "body", key: "modelID" }, + { in: "body", key: "toolID" }, + { in: "body", key: "args" }, + { in: "body", key: "agent" }, + { in: "body", key: "callID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/tool/execute", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } } export class Worktree extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 04e7144eb72..206b098f5da 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1897,6 +1897,14 @@ export type ToolListItem = { export type ToolList = Array +export type ToolExecuteResult = { + title: string + output: string + metadata?: { + [key: string]: unknown + } +} + export type Worktree = { name: string branch: string @@ -2554,6 +2562,72 @@ export type ToolListResponses = { export type ToolListResponse = ToolListResponses[keyof ToolListResponses] +export type ToolExecuteData = { + body?: { + /** + * Session ID for context + */ + sessionID: string + /** + * Message ID for context + */ + messageID: string + /** + * Provider ID for tool filtering + */ + providerID: string + /** + * Model ID for tool filtering + */ + modelID: string + /** + * Tool ID to execute + */ + toolID: string + /** + * Tool arguments + */ + args: { + [key: string]: unknown + } + /** + * Agent name (optional) + */ + agent?: string + /** + * Tool call ID (optional) + */ + callID?: string + } + path?: never + query?: { + directory?: string + } + url: "/experimental/tool/execute" +} + +export type ToolExecuteErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type ToolExecuteError = ToolExecuteErrors[keyof ToolExecuteErrors] + +export type ToolExecuteResponses = { + /** + * Tool execution result + */ + 200: ToolExecuteResult +} + +export type ToolExecuteResponse = ToolExecuteResponses[keyof ToolExecuteResponses] + export type WorktreeListData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 104cedce1e5..d7c307c8645 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -834,6 +834,104 @@ ] } }, + "/experimental/tool/execute": { + "post": { + "operationId": "tool.execute", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Execute tool", + "description": "Execute a specific tool with the provided arguments. Returns the tool output.", + "responses": { + "200": { + "description": "Tool execution result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolExecuteResult" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "description": "Session ID for context", + "type": "string" + }, + "messageID": { + "description": "Message ID for context", + "type": "string" + }, + "providerID": { + "description": "Provider ID for tool filtering", + "type": "string" + }, + "toolID": { + "description": "Tool ID to execute", + "type": "string" + }, + "args": { + "description": "Tool arguments", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "agent": { + "description": "Agent name (optional)", + "type": "string" + }, + "callID": { + "description": "Tool call ID (optional)", + "type": "string" + } + }, + "required": ["sessionID", "messageID", "providerID", "toolID", "args"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.execute({\n ...\n})" + } + ] + } + }, "/experimental/worktree": { "post": { "operationId": "worktree.create", @@ -9962,6 +10060,25 @@ "$ref": "#/components/schemas/ToolListItem" } }, + "ToolExecuteResult": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "output": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["title", "output"] + }, "Worktree": { "type": "object", "properties": {