diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 29a4c908..3c4dd6b3 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -38,7 +38,7 @@ for (const [os, arch] of targets) { await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd( "../tui", ) - await $`bun build --define OPENCODE_VERSION="'${version}'" --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui` + await $`bun build --define OPENCODE_TUI_PATH="'../../../dist/${name}/bin/tui'" --define OPENCODE_VERSION="'${version}'" --compile --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts` await $`rm -rf ./dist/${name}/bin/tui` await Bun.file(`dist/${name}/package.json`).write( JSON.stringify( diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index bb5d94b0..54cb1497 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -14,6 +14,16 @@ import { FileWatcher } from "../../file/watch" import { Mode } from "../../session/mode" import { Ide } from "../../ide" +declare global { + const OPENCODE_TUI_PATH: string +} + +if (typeof OPENCODE_TUI_PATH !== "undefined") { + await import(OPENCODE_TUI_PATH as string, { + with: { type: "file" }, + }) +} + export const TuiCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", @@ -71,16 +81,16 @@ export const TuiCommand = cmd({ let cmd = ["go", "run", "./main.go"] let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url)) - if (Bun.embeddedFiles.length > 0) { - const blob = Bun.embeddedFiles[0] as File - let binaryName = blob.name + const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File + if (tui) { + let binaryName = tui.name if (process.platform === "win32" && !binaryName.endsWith(".exe")) { binaryName += ".exe" } const binary = path.join(Global.Path.cache, "tui", binaryName) const file = Bun.file(binary) if (!(await file.exists())) { - await Bun.write(file, blob, { mode: 0o755 }) + await Bun.write(file, tui, { mode: 0o755 }) await fs.chmod(binary, 0o755) } cwd = process.cwd() diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index d4bcdda7..bb74e5e0 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -721,7 +721,7 @@ export namespace Session { sessionID: input.sessionID, abort: abort.signal, messageID: assistantMsg.id, - toolCallID: options.toolCallId, + callID: options.toolCallId, metadata: async (val) => { const match = processor.partFromToolCall(options.toolCallId) if (match && match.state.status === "running") { diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index ea845e4d..9076fada 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -3,18 +3,18 @@ import { Tool } from "./tool" import DESCRIPTION from "./bash.txt" import { App } from "../app/app" import { Permission } from "../permission" +import Parser from "tree-sitter" +import Bash from "tree-sitter-bash" import { Config } from "../config/config" - -// import Parser from "tree-sitter" -// import Bash from "tree-sitter-bash" -// import { Config } from "../config/config" +import { Filesystem } from "../util/filesystem" +import path from "path" const MAX_OUTPUT_LENGTH = 30000 const DEFAULT_TIMEOUT = 1 * 60 * 1000 const MAX_TIMEOUT = 10 * 60 * 1000 -// const parser = new Parser() -// parser.setLanguage(Bash.language as any) +const parser = new Parser() +parser.setLanguage(Bash.language as any) export const BashTool = Tool.define("bash", { description: DESCRIPTION, @@ -30,8 +30,7 @@ export const BashTool = Tool.define("bash", { async execute(params, ctx) { const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) const app = App.info() - /* - const _cfg = await Config.get() + const cfg = await Config.get() const tree = parser.parse(params.command) const permissions = (() => { const value = cfg.permission?.bash @@ -93,33 +92,16 @@ export const BashTool = Tool.define("bash", { if (needsAsk) { await Permission.ask({ - id: "bash", + type: "bash", sessionID: ctx.sessionID, messageID: ctx.messageID, - toolCallID: ctx.toolCallID, + callID: ctx.callID, title: params.command, metadata: { command: params.command, }, }) } - */ - - const cfg = await Config.get() - if (cfg.permission?.bash === "ask") - await Permission.ask({ - type: "bash", - pattern: params.command.split(" ").slice(0, 2).join(" ").trim(), - sessionID: ctx.sessionID, - messageID: ctx.messageID, - callID: ctx.toolCallID, - title: "Run this command: " + params.command, - metadata: { - command: params.command, - description: params.description, - timeout: params.timeout, - }, - }) const process = Bun.spawn({ cmd: ["bash", "-c", params.command], diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 588522cb..5bed0915 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -53,7 +53,7 @@ export const EditTool = Tool.define("edit", { type: "edit", sessionID: ctx.sessionID, messageID: ctx.messageID, - callID: ctx.toolCallID, + callID: ctx.callID, title: "Edit this file: " + filePath, metadata: { filePath, @@ -82,7 +82,7 @@ export const EditTool = Tool.define("edit", { type: "edit", sessionID: ctx.sessionID, messageID: ctx.messageID, - callID: ctx.toolCallID, + callID: ctx.callID, title: "Edit this file: " + filePath, metadata: { filePath, diff --git a/packages/opencode/src/tool/test.ts b/packages/opencode/src/tool/test.ts new file mode 100644 index 00000000..4ac81982 --- /dev/null +++ b/packages/opencode/src/tool/test.ts @@ -0,0 +1,53 @@ +import Parser from "tree-sitter"; +import Bash from "tree-sitter-bash"; + +const parser = new Parser(); +parser.setLanguage(Bash.language as any); + +const sourceCode = `cd --foo foo/bar && echo "hello" && cd ../baz`; + +const tree = parser.parse(sourceCode); + +// Function to extract commands and arguments +function extractCommands( + node: any, +): Array<{ command: string; args: string[] }> { + const commands: Array<{ command: string; args: string[] }> = []; + + function traverse(node: any) { + if (node.type === "command") { + const commandNode = node.child(0); + if (commandNode) { + const command = commandNode.text; + const args: string[] = []; + + // Extract arguments + for (let i = 1; i < node.childCount; i++) { + const child = node.child(i); + if (child && child.type === "word") { + args.push(child.text); + } + } + + commands.push({ command, args }); + } + } + + // Traverse children + for (let i = 0; i < node.childCount; i++) { + traverse(node.child(i)); + } + } + + traverse(node); + return commands; +} + +// Extract and display commands +console.log("Source code: " + sourceCode); +const commands = extractCommands(tree.rootNode); +console.log("Extracted commands:"); +commands.forEach((cmd, index) => { + console.log(`${index + 1}. Command: ${cmd.command}`); + console.log(` Args: [${cmd.args.join(", ")}]`); +}); diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index eae14414..1c71b9a7 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -7,7 +7,7 @@ export namespace Tool { export type Context = { sessionID: string messageID: string - toolCallID?: string + callID?: string abort: AbortSignal metadata(input: { title?: string; metadata?: M }): void } diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 279b7fa6..5b0028f8 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -34,7 +34,7 @@ export const WriteTool = Tool.define("write", { type: "write", sessionID: ctx.sessionID, messageID: ctx.messageID, - callID: ctx.toolCallID, + callID: ctx.callID, title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath, metadata: { filePath: filepath, diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 502f2b7b..96845178 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.0.0", + "version": "0.0.0-202507312003", "type": "module", "exports": { ".": "./dist/index.js" diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index 31208485..23bc1f91 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from './client'; -import type { EventSubscribeData, EventSubscribeResponses, AppGetData, AppGetResponses, AppInitData, AppInitResponses, ConfigGetData, ConfigGetResponses, SessionListData, SessionListResponses, SessionCreateData, SessionCreateResponses, SessionCreateErrors, SessionDeleteData, SessionDeleteResponses, SessionInitData, SessionInitResponses, SessionAbortData, SessionAbortResponses, SessionUnshareData, SessionUnshareResponses, SessionShareData, SessionShareResponses, SessionSummarizeData, SessionSummarizeResponses, SessionMessagesData, SessionMessagesResponses, SessionChatData, SessionChatResponses, SessionRevertData, SessionRevertResponses, SessionUnrevertData, SessionUnrevertResponses, ConfigProvidersData, ConfigProvidersResponses, FindTextData, FindTextResponses, FindFilesData, FindFilesResponses, FindSymbolsData, FindSymbolsResponses, FileReadData, FileReadResponses, FileStatusData, FileStatusResponses, AppLogData, AppLogResponses, AppModesData, AppModesResponses, TuiAppendPromptData, TuiAppendPromptResponses, TuiOpenHelpData, TuiOpenHelpResponses } from './types.gen'; +import type { EventSubscribeData, EventSubscribeResponses, AppGetData, AppGetResponses, AppInitData, AppInitResponses, ConfigGetData, ConfigGetResponses, SessionListData, SessionListResponses, SessionCreateData, SessionCreateResponses, SessionCreateErrors, SessionDeleteData, SessionDeleteResponses, SessionInitData, SessionInitResponses, SessionAbortData, SessionAbortResponses, SessionUnshareData, SessionUnshareResponses, SessionShareData, SessionShareResponses, SessionSummarizeData, SessionSummarizeResponses, SessionMessagesData, SessionMessagesResponses, SessionChatData, SessionChatResponses, SessionMessageData, SessionMessageResponses, SessionRevertData, SessionRevertResponses, SessionUnrevertData, SessionUnrevertResponses, PostSessionByIdPermissionsByPermissionIdData, PostSessionByIdPermissionsByPermissionIdResponses, ConfigProvidersData, ConfigProvidersResponses, FindTextData, FindTextResponses, FindFilesData, FindFilesResponses, FindSymbolsData, FindSymbolsResponses, FileReadData, FileReadResponses, FileStatusData, FileStatusResponses, AppLogData, AppLogResponses, AppModesData, AppModesResponses, TuiAppendPromptData, TuiAppendPromptResponses, TuiOpenHelpData, TuiOpenHelpResponses, TuiOpenSessionsData, TuiOpenSessionsResponses, TuiOpenThemesData, TuiOpenThemesResponses, TuiOpenModelsData, TuiOpenModelsResponses, TuiSubmitPromptData, TuiSubmitPromptResponses, TuiClearPromptData, TuiClearPromptResponses, TuiExecuteCommandData, TuiExecuteCommandResponses } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -223,6 +223,16 @@ class Session extends _HeyApiClient { }); } + /** + * Get a message from a session + */ + public message(options: Options) { + return (options.client ?? this._client).get({ + url: '/session/{id}/message/{messageID}', + ...options + }); + } + /** * Revert a message */ @@ -326,9 +336,86 @@ class Tui extends _HeyApiClient { ...options }); } + + /** + * Open the session dialog + */ + public openSessions(options?: Options) { + return (options?.client ?? this._client).post({ + url: '/tui/open-sessions', + ...options + }); + } + + /** + * Open the theme dialog + */ + public openThemes(options?: Options) { + return (options?.client ?? this._client).post({ + url: '/tui/open-themes', + ...options + }); + } + + /** + * Open the model dialog + */ + public openModels(options?: Options) { + return (options?.client ?? this._client).post({ + url: '/tui/open-models', + ...options + }); + } + + /** + * Submit the prompt + */ + public submitPrompt(options?: Options) { + return (options?.client ?? this._client).post({ + url: '/tui/submit-prompt', + ...options + }); + } + + /** + * Clear the prompt + */ + public clearPrompt(options?: Options) { + return (options?.client ?? this._client).post({ + url: '/tui/clear-prompt', + ...options + }); + } + + /** + * Execute a TUI command (e.g. switch_mode) + */ + public executeCommand(options?: Options) { + return (options?.client ?? this._client).post({ + url: '/tui/execute-command', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); + } } export class OpencodeClient extends _HeyApiClient { + /** + * Respond to a permission request + */ + public postSessionByIdPermissionsByPermissionId(options: Options) { + return (options.client ?? this._client).post({ + url: '/session/{id}/permissions/{permissionID}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } event = new Event({ client: this._client }); app = new App({ client: this._client }); config = new Config({ client: this._client }); diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 6fcd1c41..9e4475ee 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -355,12 +355,16 @@ export type EventStorageWrite = { export type EventPermissionUpdated = { type: string; - properties: PermissionInfo; + properties: Permission; }; -export type PermissionInfo = { +export type Permission = { id: string; + type: string; + pattern?: string; sessionID: string; + messageID: string; + callID?: string; title: string; metadata: { [key: string]: unknown; @@ -1193,6 +1197,34 @@ export type SessionChatResponses = { export type SessionChatResponse = SessionChatResponses[keyof SessionChatResponses]; +export type SessionMessageData = { + body?: never; + path: { + /** + * Session ID + */ + id: string; + /** + * Message ID + */ + messageID: string; + }; + query?: never; + url: '/session/{id}/message/{messageID}'; +}; + +export type SessionMessageResponses = { + /** + * Message + */ + 200: { + info: Message; + parts: Array; + }; +}; + +export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]; + export type SessionRevertData = { body?: { messageID: string; @@ -1232,6 +1264,27 @@ export type SessionUnrevertResponses = { export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses]; +export type PostSessionByIdPermissionsByPermissionIdData = { + body?: { + response: 'once' | 'always' | 'reject'; + }; + path: { + id: string; + permissionID: string; + }; + query?: never; + url: '/session/{id}/permissions/{permissionID}'; +}; + +export type PostSessionByIdPermissionsByPermissionIdResponses = { + /** + * Permission processed successfully + */ + 200: boolean; +}; + +export type PostSessionByIdPermissionsByPermissionIdResponse = PostSessionByIdPermissionsByPermissionIdResponses[keyof PostSessionByIdPermissionsByPermissionIdResponses]; + export type ConfigProvidersData = { body?: never; path?: never; @@ -1445,6 +1498,104 @@ export type TuiOpenHelpResponses = { export type TuiOpenHelpResponse = TuiOpenHelpResponses[keyof TuiOpenHelpResponses]; +export type TuiOpenSessionsData = { + body?: never; + path?: never; + query?: never; + url: '/tui/open-sessions'; +}; + +export type TuiOpenSessionsResponses = { + /** + * Session dialog opened successfully + */ + 200: boolean; +}; + +export type TuiOpenSessionsResponse = TuiOpenSessionsResponses[keyof TuiOpenSessionsResponses]; + +export type TuiOpenThemesData = { + body?: never; + path?: never; + query?: never; + url: '/tui/open-themes'; +}; + +export type TuiOpenThemesResponses = { + /** + * Theme dialog opened successfully + */ + 200: boolean; +}; + +export type TuiOpenThemesResponse = TuiOpenThemesResponses[keyof TuiOpenThemesResponses]; + +export type TuiOpenModelsData = { + body?: never; + path?: never; + query?: never; + url: '/tui/open-models'; +}; + +export type TuiOpenModelsResponses = { + /** + * Model dialog opened successfully + */ + 200: boolean; +}; + +export type TuiOpenModelsResponse = TuiOpenModelsResponses[keyof TuiOpenModelsResponses]; + +export type TuiSubmitPromptData = { + body?: never; + path?: never; + query?: never; + url: '/tui/submit-prompt'; +}; + +export type TuiSubmitPromptResponses = { + /** + * Prompt submitted successfully + */ + 200: boolean; +}; + +export type TuiSubmitPromptResponse = TuiSubmitPromptResponses[keyof TuiSubmitPromptResponses]; + +export type TuiClearPromptData = { + body?: never; + path?: never; + query?: never; + url: '/tui/clear-prompt'; +}; + +export type TuiClearPromptResponses = { + /** + * Prompt cleared successfully + */ + 200: boolean; +}; + +export type TuiClearPromptResponse = TuiClearPromptResponses[keyof TuiClearPromptResponses]; + +export type TuiExecuteCommandData = { + body?: { + command: string; + }; + path?: never; + query?: never; + url: '/tui/execute-command'; +}; + +export type TuiExecuteCommandResponses = { + /** + * Command executed successfully + */ + 200: boolean; +}; + +export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses]; + export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}); }; \ No newline at end of file