benchgo-plugin 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/DEEPSEEK-LICENSE +21 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +17 -0
  4. package/PASEO-LICENSE +211 -0
  5. package/README.md +256 -2
  6. package/TOOL-CONFIG.md +33 -0
  7. package/VERIFICATION.md +297 -0
  8. package/assets/fonts/README.md +13 -0
  9. package/assets/fonts/manifest.json +437 -0
  10. package/assets/fonts/web/fangyuan-latin-238bc49130b1.woff2 +0 -0
  11. package/assets/fonts/web/fangyuan-u30-0fe8d99641c3.woff2 +0 -0
  12. package/assets/fonts/web/fangyuan-u32-202a573131a1.woff2 +0 -0
  13. package/assets/fonts/web/fangyuan-u4e-cc2f8b20be49.woff2 +0 -0
  14. package/assets/fonts/web/fangyuan-u4f-9189b5f2d321.woff2 +0 -0
  15. package/assets/fonts/web/fangyuan-u50-e2560e003872.woff2 +0 -0
  16. package/assets/fonts/web/fangyuan-u51-a9c5e7a8c7c4.woff2 +0 -0
  17. package/assets/fonts/web/fangyuan-u52-0cfd00b473c8.woff2 +0 -0
  18. package/assets/fonts/web/fangyuan-u53-18122a07f60a.woff2 +0 -0
  19. package/assets/fonts/web/fangyuan-u54-8ca41aa9cff2.woff2 +0 -0
  20. package/assets/fonts/web/fangyuan-u55-7bd55e14647e.woff2 +0 -0
  21. package/assets/fonts/web/fangyuan-u56-26f093e8a810.woff2 +0 -0
  22. package/assets/fonts/web/fangyuan-u57-36bde83a8378.woff2 +0 -0
  23. package/assets/fonts/web/fangyuan-u58-d1df14217098.woff2 +0 -0
  24. package/assets/fonts/web/fangyuan-u59-4e41ab22c51a.woff2 +0 -0
  25. package/assets/fonts/web/fangyuan-u5a-746a367174e6.woff2 +0 -0
  26. package/assets/fonts/web/fangyuan-u5b-3d1dd404d70c.woff2 +0 -0
  27. package/assets/fonts/web/fangyuan-u5c-fe0a273f69e7.woff2 +0 -0
  28. package/assets/fonts/web/fangyuan-u5d-af1f0a1c8d2d.woff2 +0 -0
  29. package/assets/fonts/web/fangyuan-u5e-598c65420e17.woff2 +0 -0
  30. package/assets/fonts/web/fangyuan-u5f-a92eaaa5265a.woff2 +0 -0
  31. package/assets/fonts/web/fangyuan-u60-48ee3d26462d.woff2 +0 -0
  32. package/assets/fonts/web/fangyuan-u61-7a57f35bc077.woff2 +0 -0
  33. package/assets/fonts/web/fangyuan-u62-d2d0b7b36631.woff2 +0 -0
  34. package/assets/fonts/web/fangyuan-u63-cade7c6635e6.woff2 +0 -0
  35. package/assets/fonts/web/fangyuan-u64-ed34e40d7f65.woff2 +0 -0
  36. package/assets/fonts/web/fangyuan-u65-e13e83e69c38.woff2 +0 -0
  37. package/assets/fonts/web/fangyuan-u66-1dbcfa5da147.woff2 +0 -0
  38. package/assets/fonts/web/fangyuan-u67-6b0a26b82da6.woff2 +0 -0
  39. package/assets/fonts/web/fangyuan-u68-1b8217cfceed.woff2 +0 -0
  40. package/assets/fonts/web/fangyuan-u69-673e63627790.woff2 +0 -0
  41. package/assets/fonts/web/fangyuan-u6a-3c94008a4640.woff2 +0 -0
  42. package/assets/fonts/web/fangyuan-u6b-44572c410fa0.woff2 +0 -0
  43. package/assets/fonts/web/fangyuan-u6c-1b323b366e88.woff2 +0 -0
  44. package/assets/fonts/web/fangyuan-u6d-cbc376d6fe3f.woff2 +0 -0
  45. package/assets/fonts/web/fangyuan-u6e-7f7e211e2fee.woff2 +0 -0
  46. package/assets/fonts/web/fangyuan-u6f-ff280ce052fa.woff2 +0 -0
  47. package/assets/fonts/web/fangyuan-u70-cb727eaf73b6.woff2 +0 -0
  48. package/assets/fonts/web/fangyuan-u71-0bb1747439fe.woff2 +0 -0
  49. package/assets/fonts/web/fangyuan-u72-cccd39590d69.woff2 +0 -0
  50. package/assets/fonts/web/fangyuan-u73-e67705133584.woff2 +0 -0
  51. package/assets/fonts/web/fangyuan-u74-03a4e7ac9df5.woff2 +0 -0
  52. package/assets/fonts/web/fangyuan-u75-a2d30b327e65.woff2 +0 -0
  53. package/assets/fonts/web/fangyuan-u76-c08fe029c988.woff2 +0 -0
  54. package/assets/fonts/web/fangyuan-u77-76f03be1f4cb.woff2 +0 -0
  55. package/assets/fonts/web/fangyuan-u78-5b721deaf582.woff2 +0 -0
  56. package/assets/fonts/web/fangyuan-u79-db7713c2eda4.woff2 +0 -0
  57. package/assets/fonts/web/fangyuan-u7a-3e40d3726a09.woff2 +0 -0
  58. package/assets/fonts/web/fangyuan-u7b-a8204d1c9dba.woff2 +0 -0
  59. package/assets/fonts/web/fangyuan-u7c-89bcdae046de.woff2 +0 -0
  60. package/assets/fonts/web/fangyuan-u7d-bcc45fa6351a.woff2 +0 -0
  61. package/assets/fonts/web/fangyuan-u7e-49fab96d9269.woff2 +0 -0
  62. package/assets/fonts/web/fangyuan-u7f-dc110e1ecbf3.woff2 +0 -0
  63. package/assets/fonts/web/fangyuan-u80-c5ce9be0335e.woff2 +0 -0
  64. package/assets/fonts/web/fangyuan-u81-b026091b3779.woff2 +0 -0
  65. package/assets/fonts/web/fangyuan-u82-a7c6960e875d.woff2 +0 -0
  66. package/assets/fonts/web/fangyuan-u83-4b7d1c2c50d4.woff2 +0 -0
  67. package/assets/fonts/web/fangyuan-u84-4119128100c8.woff2 +0 -0
  68. package/assets/fonts/web/fangyuan-u85-ca43dd4c5411.woff2 +0 -0
  69. package/assets/fonts/web/fangyuan-u86-54fc0e3158fb.woff2 +0 -0
  70. package/assets/fonts/web/fangyuan-u87-b64f5eb55720.woff2 +0 -0
  71. package/assets/fonts/web/fangyuan-u88-00feb1251f08.woff2 +0 -0
  72. package/assets/fonts/web/fangyuan-u89-db419435b316.woff2 +0 -0
  73. package/assets/fonts/web/fangyuan-u8a-06c11d45fb87.woff2 +0 -0
  74. package/assets/fonts/web/fangyuan-u8b-5c2f6fda436a.woff2 +0 -0
  75. package/assets/fonts/web/fangyuan-u8c-2b46d2d1f4cc.woff2 +0 -0
  76. package/assets/fonts/web/fangyuan-u8d-1a67d064000d.woff2 +0 -0
  77. package/assets/fonts/web/fangyuan-u8e-eb87baf0091c.woff2 +0 -0
  78. package/assets/fonts/web/fangyuan-u8f-96c1f0632da6.woff2 +0 -0
  79. package/assets/fonts/web/fangyuan-u90-86752dba8cbe.woff2 +0 -0
  80. package/assets/fonts/web/fangyuan-u91-0573b8548552.woff2 +0 -0
  81. package/assets/fonts/web/fangyuan-u92-f85ecf735974.woff2 +0 -0
  82. package/assets/fonts/web/fangyuan-u93-e7e0451735db.woff2 +0 -0
  83. package/assets/fonts/web/fangyuan-u94-f3957ff98bcd.woff2 +0 -0
  84. package/assets/fonts/web/fangyuan-u95-b31129d29013.woff2 +0 -0
  85. package/assets/fonts/web/fangyuan-u96-ca7f779c2c13.woff2 +0 -0
  86. package/assets/fonts/web/fangyuan-u97-100a25198b2c.woff2 +0 -0
  87. package/assets/fonts/web/fangyuan-u98-59e4b784a94f.woff2 +0 -0
  88. package/assets/fonts/web/fangyuan-u99-d35af6e30868.woff2 +0 -0
  89. package/assets/fonts/web/fangyuan-u9a-1eb6f7cdfcfe.woff2 +0 -0
  90. package/assets/fonts/web/fangyuan-u9b-8f428d81dcbb.woff2 +0 -0
  91. package/assets/fonts/web/fangyuan-u9c-e10f7f8f0274.woff2 +0 -0
  92. package/assets/fonts/web/fangyuan-u9e-2be65003b759.woff2 +0 -0
  93. package/assets/fonts/web/fangyuan-u9f-02c3d5224fdb.woff2 +0 -0
  94. package/assets/fonts/web/fangyuan-ufb-3eaee54bc476.woff2 +0 -0
  95. package/assets/fonts/web/fangyuan-ufe-f64da8a3acce.woff2 +0 -0
  96. package/assets/fonts/web/fangyuan-uff-0a6dcfb689fd.woff2 +0 -0
  97. package/client/assets/iconsax/LICENSE +16 -0
  98. package/client/assets/iconsax/SOURCE.md +14 -0
  99. package/config/routergo-deepseek.example.yml +25 -0
  100. package/config.example.json +24 -0
  101. package/cordis.patch.yml +5 -0
  102. package/lib/LINK-THIRD-PARTY-LICENSES.txt +1197 -0
  103. package/lib/client.js +3577 -0
  104. package/lib/link-web/app.css +92 -0
  105. package/lib/link-web/app.js +599 -0
  106. package/lib/link-web/index.html +1 -0
  107. package/lib/link-web/workbench.js +118 -0
  108. package/lib/link.mjs +244 -0
  109. package/lib/relay/ip-certificate.mjs +559 -0
  110. package/lib/relay/relay.example.json +8 -0
  111. package/lib/relay/run-relay-server.cjs +4100 -0
  112. package/lib/server/bootstrap.mjs +147 -0
  113. package/lib/server/bootstrap.sh +39 -0
  114. package/lib/server/manifest.json +8 -0
  115. package/lib/server/server-seed.tar.gz +0 -0
  116. package/locale/en.json +6 -0
  117. package/locale/zh-CN.json +6 -0
  118. package/openapi.json +629 -0
  119. package/package.json +104 -4
  120. package/shared/plugin-install.mjs +198 -0
  121. package/shared/tool-quick-settings.mjs +70 -0
  122. package/shared/update-feed.mjs +191 -0
  123. package/shared/update-public-key.pem +3 -0
  124. package/src/cli.mjs +102 -0
  125. package/src/commands.mjs +17 -0
  126. package/src/delegation.mjs +47 -0
  127. package/src/fonts.mjs +17 -0
  128. package/src/harness-adapter.mjs +228 -0
  129. package/src/harness-commands.mjs +39 -0
  130. package/src/harness-selection.mjs +5 -0
  131. package/src/harness-usage.mjs +31 -0
  132. package/src/isolated-launch.mjs +22 -0
  133. package/src/line-process.mjs +114 -0
  134. package/src/link-plugin.mjs +21 -0
  135. package/src/mcp.mjs +21 -0
  136. package/src/model-providers.mjs +125 -0
  137. package/src/models.mjs +37 -0
  138. package/src/native-auth.mjs +32 -0
  139. package/src/picker-catalog.mjs +82 -0
  140. package/src/plugin.mjs +138 -0
  141. package/src/projects.mjs +96 -0
  142. package/src/provider-access.mjs +11 -0
  143. package/src/providers/acp.mjs +138 -0
  144. package/src/providers/claude.mjs +115 -0
  145. package/src/providers/codex.mjs +134 -0
  146. package/src/providers/opencode.mjs +138 -0
  147. package/src/providers/pi-permissions.ts +24 -0
  148. package/src/providers/pi.mjs +97 -0
  149. package/src/questions.mjs +34 -0
  150. package/src/recent-workspace.mjs +36 -0
  151. package/src/runtime.mjs +421 -0
  152. package/src/session-routes.mjs +89 -0
  153. package/src/store.mjs +98 -0
  154. package/src/tool-catalog.mjs +22 -0
  155. package/src/tool-config.mjs +94 -0
  156. package/src/tool-install.mjs +31 -0
  157. package/src/tool-profiles.mjs +114 -0
  158. package/src/tooling.mjs +90 -0
  159. package/src/updates.mjs +156 -0
  160. package/src/usage-import-parsers.mjs +37 -0
  161. package/src/usage-import.mjs +114 -0
  162. package/src/usage-timeline.mjs +36 -0
  163. package/src/usage.mjs +142 -0
  164. package/src/web.mjs +274 -0
  165. package/voice/README.md +49 -0
  166. package/voice/STREAMING.md +141 -0
  167. package/voice/VERIFICATION.md +36 -0
  168. package/voice/client/audio-worklet.js +35 -0
  169. package/voice/locale/en.json +1 -0
  170. package/voice/locale/zh-CN.json +1 -0
  171. package/voice/src/asr.mjs +44 -0
  172. package/voice/src/calls.mjs +37 -0
  173. package/voice/src/config.mjs +139 -0
  174. package/voice/src/media.mjs +70 -0
  175. package/voice/src/plugin.mjs +10 -0
  176. package/voice/src/realtime-protocols.mjs +30 -0
  177. package/voice/src/realtime-socket.mjs +86 -0
  178. package/voice/src/realtime.mjs +130 -0
  179. package/voice/src/tts.mjs +16 -0
  180. package/voice/src/web.mjs +171 -0
@@ -0,0 +1,138 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { LineProcess } from '../line-process.mjs';
3
+ import { commandCatalog, normalizeCommand } from '../commands.mjs';
4
+
5
+ // Launch arguments verified against Paseo d57096a45 acp-provider-catalog.ts.
6
+ // Original adapter implementation: ACP JSON-RPC, no filesystem/terminal client privileges.
7
+ export const ACP_TOOL_DEFAULTS = Object.freeze({
8
+ gemini: Object.freeze({ command: 'gemini', args: Object.freeze(['--acp']) }),
9
+ hermes: Object.freeze({ command: 'hermes', args: Object.freeze(['acp']) }),
10
+ grok: Object.freeze({ command: 'grok', args: Object.freeze(['agent', 'stdio']) }),
11
+ mcode: Object.freeze({ command: 'mcode', args: Object.freeze(['acp']) }),
12
+ });
13
+ const safeModels = state => Array.isArray(state?.availableModels) ? state.availableModels.filter(item => typeof item?.modelId === 'string').map(item => ({ id: item.modelId, name: item.name || item.modelId, ...(item.description ? { description: item.description } : {}) })) : [];
14
+ export class ACPProvider {
15
+ static capabilities = { stream: true, resume: true, steer: false, approvals: true, discover: true };
16
+ constructor(config = {}, toolId = 'gemini') { this.config = config; this.toolId = toolId; }
17
+ async models({ cwd = process.cwd(), nativeId } = {}) {
18
+ // Probe creates one short-lived native session when no resumable id is supplied.
19
+ const driver = await this.connect({ cwd, nativeId }, { event() {}, nativeId() {}, failure() {}, approval: async () => ({ allow: false }) });
20
+ try { return await driver.models(); } finally { await driver.close(); }
21
+ }
22
+ async connect(session, host) {
23
+ const defaults = ACP_TOOL_DEFAULTS[this.toolId];
24
+ if (!defaults) throw new Error('Unknown ACP tool');
25
+ const delegation = host.delegation;
26
+ if (delegation && ['command', 'script', 'url', 'token'].some(key => typeof delegation[key] !== 'string' || !delegation[key].trim() || delegation[key].includes(String.fromCharCode(0)))) throw new Error('ACP delegation requires a valid command, script, URL and token');
27
+ // ACP stdio MCP is baseline protocol support; HTTP/SSE flags do not gate it.
28
+ const mcpServers = delegation ? [{ name: 'commander', command: delegation.command, args: [delegation.script], env: [{ name: 'COMMANDER_DELEGATE_URL', value: delegation.url }, { name: 'COMMANDER_DELEGATE_TOKEN', value: delegation.token }] }] : [];
29
+ const wire = new LineProcess(this.config.command ?? defaults.command, [...(this.config.args ?? []), ...defaults.args], { cwd: session.cwd, env: this.config.env, timeout: this.config.timeout ?? 30_000 });
30
+ const life = new AbortController(), pendingPermissions = new Set();
31
+ let closing = false, connected = false, busy = false, caps = {}, modelsState, configOptions = [], commandList = [], commandsReady = false, commandTimer, cancelTimer, settleCommands;
32
+ const earlyUpdates = [];
33
+ const commandsGate = new Promise(resolve => { settleCommands = resolve; });
34
+ const driver = {
35
+ nativeId: session.nativeId, turnId: undefined,
36
+ async models() { return safeModels(modelsState); },
37
+ async setModel(modelId) {
38
+ if (!safeModels(modelsState).some(model => model.id === modelId)) throw new Error('Requested model is not advertised by ACP agent: ' + modelId);
39
+ const option = modelConfig();
40
+ if (option) await wire.request('session/set_config_option', { sessionId: driver.nativeId, configId: option.id, value: modelId });
41
+ else if (caps.sessionCapabilities?.setModel !== false) await wire.request('session/set_model', { sessionId: driver.nativeId, modelId });
42
+ else throw new Error('ACP agent does not support changing models in an existing session');
43
+ modelsState.currentModelId = modelId;
44
+ session.model = modelId;
45
+ },
46
+ async commands() { if (!commandsReady) await commandsGate; return commandCatalog(commandList); },
47
+ async command(value) { const cmd = normalizeCommand(value.name, value.arguments); const catalog = await driver.commands(); if (!catalog.some(item => item.name === cmd.name)) throw new Error('Unsupported native ACP command: /' + cmd.name); return driver.submit('/' + cmd.name + (cmd.arguments ? ' ' + cmd.arguments : '')); },
48
+ async submit(text) {
49
+ if (!connected || closing) throw new Error('ACP session is disconnected');
50
+ if (busy) throw new Error('ACP session is already running');
51
+ if (typeof text !== 'string' || !text.trim()) throw new Error('Prompt must not be empty');
52
+ busy = true; driver.turnId = randomUUID(); host.event('turn.started', {});
53
+ void wire.request('session/prompt', { sessionId: driver.nativeId, prompt: [{ type: 'text', text }] }, thisProvider.config.promptTimeout ?? 15 * 60_000).then(result => {
54
+ busy = false; clearTimeout(cancelTimer); cancelTimer = undefined;
55
+ if (!closing) host.event('turn.completed', { status: result?.stopReason === 'cancelled' ? 'cancelled' : 'completed', stopReason: result?.stopReason ?? null });
56
+ }).catch(async error => {
57
+ if (!closing) { host.event('turn.completed', { status: 'failed', message: error.message }); host.failure(error); await driver.close(); }
58
+ }).finally(() => { busy = false; });
59
+ },
60
+ async interrupt() {
61
+ if (busy && !closing) {
62
+ for (const request of pendingPermissions) request.abort(); wire.notify('session/cancel', { sessionId: driver.nativeId });
63
+ if (!cancelTimer) { cancelTimer = setTimeout(() => { if (busy && !closing) { const error = new Error('ACP cancellation was not acknowledged; session execution outcome may be unknown'); host.event('turn.completed', { status: 'failed', message: error.message }); host.failure(error); void driver.close(); } }, thisProvider.config.cancelTimeout ?? 8000); cancelTimer.unref(); }
64
+ }
65
+ },
66
+ async history() { throw new Error('ACP history is replayed by session/load; standalone history is not advertised.'); },
67
+ async discover() { if (!caps.sessionCapabilities?.list) throw new Error('This ACP agent does not advertise session listing'); return wire.request('session/list', { cwd: session.cwd }); },
68
+ async close() { if (closing) return; closing = true; connected = false; clearTimeout(commandTimer); clearTimeout(cancelTimer); settleCommands(); life.abort(); for (const request of pendingPermissions) request.abort(); await wire.close(); },
69
+ };
70
+ const thisProvider = this;
71
+ const modelConfig = () => configOptions.find(option => option?.category === 'model' || option?.id === 'model' || option?.id === 'model_id');
72
+ const optionModels = option => (option?.options ?? []).flatMap(item => item?.options ?? [item]).filter(item => typeof item?.value === 'string').map(item => ({ modelId: item.value, name: item.name ?? item.value }));
73
+ const update = message => {
74
+ if (message.method !== 'session/update' || closing) return;
75
+ if (!driver.nativeId) { if (earlyUpdates.length < 500) earlyUpdates.push(message); return; }
76
+ if (message.params?.sessionId !== driver.nativeId) return;
77
+ const value = message.params.update; if (!value || typeof value !== 'object') return;
78
+ switch (value.sessionUpdate) {
79
+ case 'available_commands_update': commandList = (value.availableCommands ?? []).slice(0, 500).map(cmd => ({ ...cmd, argumentHint: cmd.input?.hint ?? '' })); commandsReady = true; settleCommands(); break;
80
+ case 'config_option_update': configOptions = value.configOptions ?? []; if (modelConfig()) modelsState = { availableModels: optionModels(modelConfig()), currentModelId: modelConfig().currentValue }; break;
81
+ case 'agent_message_chunk': if (connected && busy && value.content?.type === 'text') host.event('text.delta', { text: value.content.text }); break;
82
+ case 'agent_thought_chunk': if (connected && busy && value.content?.type === 'text') host.event('item', { type: 'reasoning', text: value.content.text }); break;
83
+ case 'tool_call': case 'tool_call_update': case 'plan': if (connected && busy) host.event('item', value); break;
84
+ case 'usage_update': if (connected && busy) host.event('item', { type: 'acp.context_usage', ...value }); break;
85
+ }
86
+ };
87
+ wire.on('notification', update);
88
+ wire.on('failure', error => { if (!closing) { life.abort(); for (const request of pendingPermissions) request.abort(); host.failure(error); } });
89
+ wire.on('request', request => {
90
+ void (async () => {
91
+ if (request.method !== 'session/request_permission') { wire.reject(request.id, 'Client filesystem and terminal methods are unsupported'); return; }
92
+ const params = request.params ?? {}, once = params.options?.find(option => option.kind === 'allow_once' && typeof option.optionId === 'string');
93
+ const denied = () => ({ outcome: { outcome: 'cancelled' } });
94
+ if (!connected || closing || !busy || params.sessionId !== driver.nativeId || !once || !host.approval) { wire.respond(request.id, denied()); return; }
95
+ const controller = new AbortController(); pendingPermissions.add(controller);
96
+ const abort = () => controller.abort(); life.signal.addEventListener('abort', abort, { once: true });
97
+ const timer = setTimeout(abort, this.config.approvalTimeout ?? 120_000); timer.unref();
98
+ try {
99
+ const answer = await Promise.race([Promise.resolve().then(() => host.approval({ kind: 'permission', method: params.toolCall?.kind ?? 'tool', nativeRequestId: request.id, details: params.toolCall ?? {} }, controller.signal)), new Promise(resolve => controller.signal.addEventListener('abort', () => resolve({ allow: false }), { once: true }))]);
100
+ if (!closing && !wire.closed) wire.respond(request.id, answer?.allow === true && !controller.signal.aborted ? { outcome: { outcome: 'selected', optionId: once.optionId } } : denied());
101
+ } catch { if (!closing && !wire.closed) wire.respond(request.id, denied()); }
102
+ finally { clearTimeout(timer); pendingPermissions.delete(controller); life.signal.removeEventListener('abort', abort); }
103
+ })().catch(error => { if (!closing) host.failure(error); });
104
+ });
105
+ try {
106
+ const initialized = await wire.request('initialize', { protocolVersion: 1, clientInfo: { name: 'benchgo', version: '0.1.0' }, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false } });
107
+ if (initialized?.protocolVersion !== 1) throw new Error('Unsupported ACP protocol version');
108
+ caps = initialized.agentCapabilities ?? {};
109
+ if (session.nativeId && !caps.loadSession) throw new Error('This ACP agent does not support session resume; refusing to create a replacement session');
110
+ let result;
111
+ try {
112
+ result = await wire.request(session.nativeId ? 'session/load' : 'session/new', { ...(session.nativeId ? { sessionId: session.nativeId } : {}), cwd: session.cwd, mcpServers });
113
+ } catch (error) {
114
+ if (delegation) throw new Error('ACP session with BenchGo delegation MCP failed: ' + error.message, { cause: error });
115
+ throw error;
116
+ }
117
+ if (!session.nativeId && (typeof result?.sessionId !== 'string' || !result.sessionId)) throw new Error('ACP agent did not return a durable session id');
118
+ if (session.nativeId && result?.sessionId && result.sessionId !== session.nativeId) throw new Error('ACP resume returned a different session id');
119
+ driver.nativeId = session.nativeId ?? result.sessionId;
120
+ // Persist the real native session before optional model changes can fail.
121
+ await host.nativeId?.(driver.nativeId);
122
+ modelsState = result.models; configOptions = result.configOptions ?? [];
123
+ for (const message of earlyUpdates) update(message); earlyUpdates.length = 0;
124
+ if (!modelsState && modelConfig()) modelsState = { availableModels: optionModels(modelConfig()), currentModelId: modelConfig().currentValue };
125
+ if (session.model && session.model !== modelsState?.currentModelId) {
126
+ if (!safeModels(modelsState).some(model => model.id === session.model)) throw new Error('Requested model is not advertised by ACP agent: ' + session.model);
127
+ const option = modelConfig();
128
+ if (option) await wire.request('session/set_config_option', { sessionId: driver.nativeId, configId: option.id, value: session.model });
129
+ else await wire.request('session/set_model', { sessionId: driver.nativeId, modelId: session.model });
130
+ modelsState.currentModelId = session.model;
131
+ }
132
+ connected = true;
133
+ commandTimer = setTimeout(settleCommands, this.config.commandsTimeout ?? 300); commandTimer.unref();
134
+ return driver;
135
+ } catch (error) { await driver.close(); throw error; }
136
+ }
137
+ }
138
+ export const acpProviderClasses = Object.fromEntries(Object.keys(ACP_TOOL_DEFAULTS).map(id => [id, class extends ACPProvider { constructor(config = {}) { super(config, id); } }]));
@@ -0,0 +1,115 @@
1
+ import { commandCatalog } from '../commands.mjs';
2
+ import { isolatedClaudeHistory } from '../isolated-launch.mjs';
3
+
4
+ export class ClaudeProvider {
5
+ static capabilities = { stream: true, resume: true, steer: false, approvals: true, discover: true };
6
+ constructor(config = {}) { this.config = config; }
7
+
8
+ async connect(session, host) {
9
+ let sdk;
10
+ try { sdk = await import('@anthropic-ai/claude-agent-sdk'); }
11
+ catch { throw new Error('Claude Agent SDK is not installed; run npm install in plugins/commander'); }
12
+ const isolatedHome = this.config.managedProfile?.isolated ? this.config.env.CLAUDE_CONFIG_DIR : null;
13
+ const historyInfo = id => isolatedHome ? isolatedClaudeHistory(isolatedHome, 'info', id, session.cwd) : sdk.getSessionInfo(id, { dir: session.cwd });
14
+ if (session.nativeId && !await historyInfo(session.nativeId)) {
15
+ throw new Error('Claude native history is missing or unreadable; refusing to create a replacement session');
16
+ }
17
+ let query;
18
+ let wake;
19
+ let stopped = false;
20
+ let hasAssistantText = false;
21
+ const queue = [];
22
+ async function* input() {
23
+ while (!stopped) {
24
+ if (!queue.length) await new Promise(resolve => { wake = resolve; });
25
+ if (stopped) return;
26
+ yield queue.shift();
27
+ }
28
+ }
29
+ const driver = {
30
+ nativeId: session.nativeId,
31
+ async setModel(model) {
32
+ if (typeof query.setModel !== 'function') throw new Error('Claude Agent SDK does not support changing models in this session');
33
+ await query.setModel(model || undefined);
34
+ session.model = model || undefined;
35
+ },
36
+ async submit(text) {
37
+ hasAssistantText = false;
38
+ queue.push({ type: 'user', session_id: driver.nativeId ?? '', message: { role: 'user', content: text }, parent_tool_use_id: null });
39
+ wake?.(); wake = undefined;
40
+ },
41
+ async commands() { return commandCatalog(await query.supportedCommands()); },
42
+ async command({ name, arguments: args }) { await driver.submit(`/${name}${args ? ` ${args}` : ''}`); },
43
+ async interrupt() { await query.interrupt(); },
44
+ async close() {
45
+ stopped = true; wake?.();
46
+ query.close();
47
+ await pump;
48
+ },
49
+ };
50
+ query = sdk.query({ prompt: input(), options: {
51
+ cwd: session.cwd,
52
+ ...(session.nativeId ? { resume: session.nativeId } : {}),
53
+ ...(session.model ? { model: session.model } : {}),
54
+ pathToClaudeCodeExecutable: this.config.command ?? 'claude',
55
+ env: { ...process.env, ...this.config.env },
56
+ permissionMode: 'default',
57
+ includePartialMessages: true,
58
+ settingSources: this.config.settingSources ?? ['user', 'project', 'local'],
59
+ ...(this.config.settings ? { settings: this.config.settings } : {}),
60
+ systemPrompt: { type: 'preset', preset: 'claude_code' },
61
+ ...(host.delegation ? { mcpServers: { commander: { command: host.delegation.command, args: [host.delegation.script], env: { COMMANDER_DELEGATE_URL: host.delegation.url, COMMANDER_DELEGATE_TOKEN: host.delegation.token } } } } : {}),
62
+ canUseTool: async (toolName, toolInput, options) => {
63
+ const isQuestion = toolName === 'AskUserQuestion';
64
+ const answer = await host.approval({ kind: isQuestion ? 'question' : 'permission', method: toolName, nativeRequestId: options.toolUseID, details: isQuestion ? toolInput : { toolName, input: toolInput } }, options.signal);
65
+ return answer.allow === true ? { behavior: 'allow', updatedInput: isQuestion ? { ...toolInput, answers: answer.answers } : toolInput } : { behavior: 'deny', message: 'Denied by BenchGo user' };
66
+ },
67
+ } });
68
+ const pump = (async () => {
69
+ try {
70
+ for await (const message of query) {
71
+ if (message.parent_tool_use_id) {
72
+ host.event('subagent.event', { type: message.type, parentToolUseId: message.parent_tool_use_id, message });
73
+ continue;
74
+ }
75
+ if (message.session_id && message.session_id !== driver.nativeId) {
76
+ if (driver.nativeId) throw new Error('Claude returned a different native session');
77
+ driver.nativeId = message.session_id;
78
+ host.nativeId(message.session_id);
79
+ }
80
+ if (message.type === 'stream_event') {
81
+ if (message.event.type === 'message_start') host.event('turn.started', {});
82
+ if (message.event.type === 'content_block_delta' && message.event.delta.type === 'text_delta') { hasAssistantText = true; host.event('text.delta', { text: message.event.delta.text }); }
83
+ if (message.event.type === 'content_block_start' && message.event.content_block.type === 'tool_use') host.event('item', message.event.content_block);
84
+ } else if (message.type === 'result') {
85
+ if (!hasAssistantText && message.usage?.output_tokens === 0 && typeof message.result === 'string' && message.result.trim()) host.event('text.delta', { text: message.result });
86
+ host.event('turn.completed', { status: message.is_error ? 'failed' : 'completed', result: message.result ?? null, errors: message.errors ?? [] });
87
+ } else if (message.type === 'assistant' || message.type === 'user') {
88
+ host.event('message', { type: message.type, message: message.message });
89
+ } else if (message.type === 'system' && message.subtype === 'api_retry') {
90
+ host.event('provider.retry', { attempt: message.attempt ?? null, maxRetries: message.max_retries ?? null, delayMs: message.retry_delay_ms ?? null, status: message.error_status ?? null });
91
+ }
92
+ }
93
+ if (!stopped) host.failure(new Error('Claude session stream ended'));
94
+ } catch (error) { if (!stopped) host.failure(error); }
95
+ })();
96
+ let timer;
97
+ try {
98
+ await Promise.race([
99
+ query.initializationResult(),
100
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Claude initialization timed out')), 30_000); }),
101
+ ]);
102
+ if (this.config.managedProfile?.subscription) {
103
+ const account = await query.accountInfo();
104
+ if (!account?.subscriptionType || account.subscriptionType === 'none' || account.apiKeySource && account.apiKeySource !== 'none' || account.apiProvider && account.apiProvider !== 'firstParty') throw new Error('请先在本机 Claude Code 中完成 Claude 订阅登录,不能用 API 密钥替代订阅。');
105
+ }
106
+ } catch (error) { await driver.close(); throw error; }
107
+ finally { clearTimeout(timer); }
108
+ driver.history = async () => {
109
+ if (!driver.nativeId) throw new Error('Claude has not persisted a native session yet');
110
+ return isolatedHome ? isolatedClaudeHistory(isolatedHome, 'messages', driver.nativeId, session.cwd) : sdk.getSessionMessages(driver.nativeId, { dir: session.cwd });
111
+ };
112
+ driver.discover = () => isolatedHome ? isolatedClaudeHistory(isolatedHome, 'list', '', session.cwd) : sdk.listSessions({ dir: session.cwd, limit: 50 });
113
+ return driver;
114
+ }
115
+ }
@@ -0,0 +1,134 @@
1
+ import { LineProcess } from '../line-process.mjs';
2
+ import { createHash } from 'node:crypto';
3
+ import { commandCatalog, compactCommand } from '../commands.mjs';
4
+
5
+ export class CodexProvider {
6
+ static capabilities = { stream: true, resume: true, steer: true, approvals: true, discover: true };
7
+ constructor(config = {}) { this.config = config; }
8
+
9
+ async connect(session, host) {
10
+ const wire = new LineProcess(this.config.command ?? 'codex', [...(this.config.commandArgs ?? []), 'app-server', '--listen', 'stdio://'], { cwd: session.cwd, env: this.config.env });
11
+ let previousUsage, previousUsageKey;
12
+ const driver = {
13
+ nativeId: session.nativeId,
14
+ async setModel(model) { session.model = model || undefined; },
15
+ turnId: undefined,
16
+ compacting: false,
17
+ async commands() {
18
+ const result = await wire.request('skills/list', { cwds: [session.cwd] });
19
+ driver.skills = (result.data ?? []).flatMap(entry => entry.skills ?? []).filter(skill => skill.enabled !== false);
20
+ return commandCatalog(driver.skills.map(skill => ({ ...skill, kind: 'skill' })), [compactCommand]);
21
+ },
22
+ validateCommand({ name, arguments: args }) {
23
+ if (name === 'compact' && args) throw new Error('Codex compact does not accept arguments');
24
+ },
25
+ async command({ name, arguments: args }) {
26
+ driver.validateCommand({ name, arguments: args });
27
+ if (name === 'compact') {
28
+ driver.compacting = true;
29
+ try { await wire.request('thread/compact/start', { threadId: driver.nativeId }); }
30
+ catch (error) { driver.compacting = false; throw error; }
31
+ return;
32
+ }
33
+ const skill = driver.skills?.find(entry => entry.name === name);
34
+ if (!skill?.path) throw new Error(`Unsupported native Codex command: /${name}`);
35
+ const response = await wire.request('turn/start', { threadId: driver.nativeId, ...(session.model ? { model: session.model } : {}), input: [{ type: 'skill', name: skill.name, path: skill.path }, { type: 'text', text: `$${name}${args ? ` ${args}` : ''}` }] });
36
+ driver.turnId = response.turn.id;
37
+ },
38
+ async submit(text) {
39
+ const response = await wire.request('turn/start', {
40
+ threadId: driver.nativeId, ...(session.model ? { model: session.model } : {}), input: [{ type: 'text', text }],
41
+ });
42
+ driver.turnId = response.turn.id;
43
+ },
44
+ async steer(text) {
45
+ if (!driver.turnId) throw new Error('No native Codex turn to steer');
46
+ await wire.request('turn/steer', { threadId: driver.nativeId, expectedTurnId: driver.turnId, input: [{ type: 'text', text }] });
47
+ },
48
+ async interrupt() {
49
+ if (!driver.turnId) throw new Error('Codex has not published an active turn');
50
+ await wire.request('turn/interrupt', { threadId: driver.nativeId, turnId: driver.turnId });
51
+ },
52
+ async history() { return wire.request('thread/read', { threadId: driver.nativeId, includeTurns: true }); },
53
+ async discover() { return wire.request('thread/list', { cwd: session.cwd, limit: 50 }); },
54
+ close: () => wire.close(),
55
+ };
56
+ wire.on('failure', error => host.failure(error));
57
+ wire.on('notification', message => {
58
+ const p = message.params ?? {};
59
+ if (p.threadId && driver.nativeId && p.threadId !== driver.nativeId) return;
60
+ switch (message.method) {
61
+ case 'thread/tokenUsage/updated': {
62
+ const total = p.tokenUsage?.total, last = p.tokenUsage?.last;
63
+ if (!last || !driver.turnId) break;
64
+ const key = JSON.stringify(total ?? last);
65
+ if (key === previousUsageKey) break;
66
+ const usage = previousUsage && total ? Object.fromEntries(Object.entries(total).map(([name, value]) => [name, Number.isSafeInteger(value) && Number.isSafeInteger(previousUsage[name]) && value >= previousUsage[name] ? value - previousUsage[name] : undefined])) : last;
67
+ const nativeTurnId = p.turnId ?? driver.turnId;
68
+ const signature = createHash('sha256').update(key).digest('hex').slice(0, 24);
69
+ host.event('usage', { id: 'request:' + nativeTurnId + ':' + signature, scope: 'request', nativeTurnId, usage: { ...usage, cacheReadTokens: usage.cachedInputTokens, contextTokens: last.inputTokens, contextWindow: p.tokenUsage.modelContextWindow }, convention: 'inclusive' });
70
+ previousUsage = total; previousUsageKey = key;
71
+ break;
72
+ }
73
+ case 'thread/compacted':
74
+ if (driver.compacting) { driver.compacting = false; host.event('turn.completed', { status: 'completed', command: 'compact' }); }
75
+ break;
76
+ case 'turn/started': driver.turnId = p.turn.id; host.event('turn.started', { nativeTurnId: p.turn.id, model: session.model ?? null }); break;
77
+ case 'turn/completed':
78
+ driver.turnId = undefined;
79
+ driver.compacting = false;
80
+ host.event('turn.completed', { status: p.turn.status, error: p.turn.error ?? null }); break;
81
+ case 'item/agentMessage/delta': host.event('text.delta', { text: p.delta, itemId: p.itemId }); break;
82
+ case 'item/started': case 'item/completed':
83
+ host.event('item', { phase: message.method, item: p.item });
84
+ if (driver.compacting && message.method === 'item/completed' && p.item?.type === 'contextCompaction') { driver.compacting = false; host.event('turn.completed', { status: 'completed', command: 'compact' }); }
85
+ break;
86
+ case 'item/commandExecution/outputDelta': host.event('tool.output', { text: p.delta, itemId: p.itemId }); break;
87
+ case 'error': host.event('provider.error', p); break;
88
+ default: break;
89
+ }
90
+ });
91
+ wire.on('request', message => {
92
+ void (async () => {
93
+ const { method, params: p = {}, id } = message;
94
+ if (p.threadId && p.threadId !== driver.nativeId) return wire.reject(id, 'Wrong thread');
95
+ if (method === 'item/tool/call' && p.tool === 'commander_delegate' && host.delegate) {
96
+ try { wire.respond(id, { success: true, contentItems: [{ type: 'inputText', text: JSON.stringify(await host.delegate(p.arguments)) }] }); }
97
+ catch (error) { wire.respond(id, { success: false, contentItems: [{ type: 'inputText', text: error.message }] }); }
98
+ } else if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval') {
99
+ const answer = await host.approval({ kind: 'permission', method, nativeRequestId: id, details: p });
100
+ const available = p.availableDecisions;
101
+ let decision = answer.allow ? 'accept' : 'decline';
102
+ if (Array.isArray(available) && !available.includes(decision)) {
103
+ if (!answer.allow && available.includes('cancel')) decision = 'cancel';
104
+ else throw new Error(`Unsupported Codex approval decisions: ${JSON.stringify(available)}`);
105
+ }
106
+ wire.respond(id, { decision });
107
+ } else if (method === 'item/tool/requestUserInput' || method === 'tool/requestUserInput') {
108
+ const answer = await host.approval({ kind: 'question', method, nativeRequestId: id, details: p });
109
+ wire.respond(id, { answers: answer.answers ?? {} });
110
+ } else {
111
+ wire.reject(id, `BenchGo does not support server request ${method}`);
112
+ host.event('unsupported.request', { method });
113
+ }
114
+ })().catch(error => { try { wire.reject(message.id, error.message); } catch {} host.failure(error); });
115
+ });
116
+ try {
117
+ await wire.request('initialize', { clientInfo: { name: 'dowork_commander', version: '0.1.0' }, capabilities: { experimentalApi: true } });
118
+ wire.notify('initialized', {});
119
+ if (this.config.managedProfile?.subscription) {
120
+ const { account } = await wire.request('account/read', { refreshToken: false });
121
+ if (account?.type !== 'chatgpt') throw new Error('请先在本机 Codex 中完成 ChatGPT 登录,不能用 API 密钥替代订阅。');
122
+ }
123
+ const params = { cwd: session.cwd, approvalPolicy: 'on-request', sandbox: 'workspace-write', ...(session.model ? { model: session.model } : {}) };
124
+ const result = await wire.request(session.nativeId ? 'thread/resume' : 'thread/start', {
125
+ ...params, ...(session.nativeId ? { threadId: session.nativeId } : {}),
126
+ ...(!session.nativeId && host.delegation ? { dynamicTools: [{ name: 'commander_delegate', description: 'Delegate in the same project using provider and text, optionally model. Human approval required. Returns child result; if pending, call again with operation=result and sessionId. Do not create another child for the same task.', inputSchema: host.delegation.schema }] } : {}),
127
+ });
128
+ if (!result?.thread?.id) throw new Error('Codex returned no native thread identity');
129
+ if (session.nativeId && result.thread.id !== session.nativeId) throw new Error('Codex resumed a different thread');
130
+ driver.nativeId = result.thread.id;
131
+ return driver;
132
+ } catch (error) { await wire.close(); throw error; }
133
+ }
134
+ }
@@ -0,0 +1,138 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { commandCatalog, compactCommand } from '../commands.mjs';
5
+
6
+ // OpenCode v1 HTTP/SSE adapter. v2 has a different contract and is deliberately
7
+ // rejected rather than guessed from a successful TCP connection.
8
+ export class OpenCodeProvider {
9
+ static capabilities = { stream: true, resume: true, steer: false, approvals: true, discover: true };
10
+ constructor(config = {}) { this.config = config; }
11
+
12
+ async connect(session, host) {
13
+ const password = randomBytes(24).toString('hex');
14
+ const inline = this.config.env?.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
15
+ const existing = inline ? JSON.parse(inline) : {};
16
+ const child = spawn(this.config.command ?? 'opencode', [...(this.config.commandArgs ?? []), 'serve', '--hostname', '127.0.0.1', '--port', '0'], {
17
+ cwd: session.cwd, env: { ...process.env, ...this.config.env, OPENCODE_SERVER_PASSWORD: password,
18
+ ...(host.delegation ? { OPENCODE_CONFIG_CONTENT: JSON.stringify({ ...existing, mcp: { ...existing.mcp, commander: { type: 'local', command: [host.delegation.command, host.delegation.script], environment: { COMMANDER_DELEGATE_URL: host.delegation.url, COMMANDER_DELEGATE_TOKEN: host.delegation.token }, enabled: true } } }) } : {}),
19
+ },
20
+ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: process.platform !== 'win32',
21
+ });
22
+ const controller = new AbortController();
23
+ let closing = false;
24
+ let tail = '';
25
+ let base;
26
+ let pump;
27
+ let rejectReady;
28
+ const exited = new Promise(resolve => child.once('close', resolve));
29
+ const stop = async () => {
30
+ closing = true; controller.abort();
31
+ const kill = signal => {
32
+ try { if (child.pid) process.platform === 'win32' ? child.kill(signal) : process.kill(-child.pid, signal); }
33
+ catch (error) { if (error.code !== 'ESRCH') throw error; }
34
+ };
35
+ kill('SIGTERM');
36
+ const timer = setTimeout(() => kill('SIGKILL'), 1500);
37
+ try { await exited; await pump; } finally { clearTimeout(timer); }
38
+ };
39
+ child.on('error', error => { rejectReady?.(error); if (!closing) host.failure(error); });
40
+ child.on('close', () => { const error = new Error(`OpenCode server exited: ${tail.slice(-1000)}`); rejectReady?.(error); if (!closing) host.failure(error); });
41
+ try {
42
+ base = await new Promise((resolve, reject) => {
43
+ rejectReady = reject;
44
+ const timer = setTimeout(() => reject(new Error('OpenCode startup timed out')), 20_000);
45
+ const data = chunk => {
46
+ tail = (tail + chunk.toString()).slice(-8192);
47
+ const url = tail.match(/https?:\/\/(?:127\.0\.0\.1|localhost):\d+/)?.[0];
48
+ if (url) { clearTimeout(timer); resolve(url); }
49
+ };
50
+ child.stdout.on('data', data); child.stderr.on('data', data);
51
+ exited.then(() => clearTimeout(timer));
52
+ });
53
+ const headers = { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}`, 'content-type': 'application/json' };
54
+ const request = async (path, method = 'GET', body, timeout = 30_000) => {
55
+ const url = new URL(path, base); url.searchParams.set('directory', session.cwd);
56
+ const response = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.any([controller.signal, AbortSignal.timeout(timeout)]) });
57
+ if (!response.ok) throw new Error(`OpenCode ${method} ${path}: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
58
+ const text = await response.text(); return text ? JSON.parse(text) : null;
59
+ };
60
+ const health = await request('/global/health');
61
+ if (!health?.healthy || !/^1\./.test(health.version ?? '')) throw new Error(`Unsupported OpenCode protocol/version: ${health?.version ?? 'unknown'}; v1 required`);
62
+ const info = session.nativeId
63
+ ? await request(`/session/${encodeURIComponent(session.nativeId)}`)
64
+ : await request('/session', 'POST', { permission: [{ permission: '*', pattern: '*', action: 'ask' }] });
65
+ if (!info?.id || (session.nativeId && info.id !== session.nativeId)) throw new Error('OpenCode returned an invalid native session');
66
+ const id = encodeURIComponent(info.id);
67
+ // Open and attach SSE before accepting prompts; no startup race dropping
68
+ // the first delta/permission request.
69
+ const streamURL = new URL('/event', base); streamURL.searchParams.set('directory', session.cwd);
70
+ const response = await fetch(streamURL, { headers, signal: controller.signal });
71
+ if (!response.ok || !response.body) throw new Error('OpenCode event subscription failed');
72
+ const handle = async event => {
73
+ const p = event.properties ?? {};
74
+ const sessionId = p.sessionID ?? p.part?.sessionID ?? p.info?.sessionID;
75
+ if (sessionId !== info.id) return;
76
+ if (event.type === 'message.part.delta' && p.field === 'text') host.event('text.delta', { text: p.delta, itemId: p.partID });
77
+ else if (event.type === 'message.part.updated') {
78
+ host.event('item', { part: p.part, delta: p.delta ?? null });
79
+ if (p.part?.type === 'step-finish' && p.part.tokens) host.event('usage', { id: p.part.id, usage: p.part.tokens });
80
+ }
81
+ else if (event.type === 'session.idle') host.event('turn.completed', { status: 'completed' });
82
+ else if (event.type === 'session.error') host.event('turn.completed', { status: 'failed', error: p.error });
83
+ else if (event.type === 'permission.asked') {
84
+ const answer = await host.approval({ kind: 'permission', method: p.permission, nativeRequestId: p.id, details: p });
85
+ await request(`/permission/${encodeURIComponent(p.id)}/reply`, 'POST', { reply: answer.allow ? 'once' : 'reject' });
86
+ } else if (event.type === 'question.asked') {
87
+ const answer = await host.approval({ kind: 'question', method: event.type, nativeRequestId: p.id, details: p });
88
+ if (answer.answers) await request(`/question/${encodeURIComponent(p.id)}/reply`, 'POST', { answers: answer.answers });
89
+ else await request(`/question/${encodeURIComponent(p.id)}/reject`, 'POST', {});
90
+ }
91
+ };
92
+ pump = (async () => {
93
+ const decoder = new TextDecoder(); let buffer = '';
94
+ try {
95
+ for await (const chunk of response.body) {
96
+ buffer += decoder.decode(chunk, { stream: true });
97
+ buffer = buffer.replace(/\r\n/g, '\n');
98
+ if (buffer.length > 16 * 1024 * 1024) throw new Error('OpenCode SSE frame too large');
99
+ let boundary;
100
+ while ((boundary = buffer.indexOf('\n\n')) !== -1) {
101
+ const frame = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2);
102
+ const data = frame.split('\n').filter(line => line.startsWith('data:')).map(line => line.slice(5).trimStart()).join('\n');
103
+ if (data) void handle(JSON.parse(data)).catch(error => host.failure(error));
104
+ }
105
+ }
106
+ if (!closing) host.failure(new Error('OpenCode event stream disconnected; reconnect the session before continuing'));
107
+ } catch (error) { if (!closing) host.failure(error); }
108
+ })();
109
+ return {
110
+ nativeId: info.id,
111
+ async setModel(model) { session.model = model || undefined; },
112
+ async commands() { return commandCatalog(await request('/command'), [compactCommand, { ...compactCommand, name: 'summarize' }]); },
113
+ validateCommand({ name, arguments: args }) {
114
+ if (name !== 'compact' && name !== 'summarize') return;
115
+ if (args) throw new Error('OpenCode compact does not accept arguments');
116
+ if (!session.model?.includes('/')) throw new Error('Select an explicit provider/model before OpenCode compact');
117
+ },
118
+ async command({ name, arguments: args }) {
119
+ const model = session.model?.split('/');
120
+ if (name === 'compact' || name === 'summarize') {
121
+ if (args) throw new Error('OpenCode compact does not accept arguments');
122
+ if (!model || model.length < 2) throw new Error('Select an explicit provider/model before OpenCode compact');
123
+ await request(`/session/${id}/summarize`, 'POST', { providerID: model[0], modelID: model.slice(1).join('/') }, 30 * 60_000);
124
+ } else await request(`/session/${id}/command`, 'POST', { command: name, arguments: args, ...(session.model ? { model: session.model } : {}) }, 30 * 60_000);
125
+ },
126
+ async submit(text) {
127
+ const model = session.model?.split('/');
128
+ if (model && model.length < 2) throw new Error('OpenCode model must be provider/model');
129
+ await request(`/session/${id}/prompt_async`, 'POST', { parts: [{ type: 'text', text }], ...(model ? { model: { providerID: model[0], modelID: model.slice(1).join('/') } } : {}) });
130
+ },
131
+ async interrupt() { await request(`/session/${id}/abort`, 'POST', {}); },
132
+ async history() { return request(`/session/${id}/message`); },
133
+ async discover() { return request('/session'); },
134
+ close: stop,
135
+ };
136
+ } catch (error) { await stop(); throw error; }
137
+ }
138
+ }
@@ -0,0 +1,24 @@
1
+ // Loaded by the user's Pi runtime. Import is type-only: this plugin does not
2
+ // depend on a second installation of Pi or change its global configuration.
3
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
4
+ import { Type } from '@sinclair/typebox';
5
+
6
+ export default function commanderPermissions(pi: ExtensionAPI) {
7
+ if (process.env.COMMANDER_DELEGATE_URL) pi.registerTool({
8
+ name: 'commander_delegate', label: 'BenchGo', description: 'Delegate a task to another coding engine in the same project. Requires human approval.',
9
+ parameters: Type.Object({ provider: Type.Optional(Type.Union(['codex', 'claude', 'pi', 'opencode'].map(s => Type.Literal(s)))), text: Type.Optional(Type.String()), model: Type.Optional(Type.String()), operation: Type.Optional(Type.Union([Type.Literal('delegate'), Type.Literal('result')])), sessionId: Type.Optional(Type.String()) }),
10
+ async execute(_id, args, signal) {
11
+ const response = await fetch(process.env.COMMANDER_DELEGATE_URL!, { method: 'POST', headers: { authorization: `Bearer ${process.env.COMMANDER_DELEGATE_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify(args), signal });
12
+ const value = await response.text(); if (!response.ok) throw new Error(value);
13
+ return { content: [{ type: 'text', text: value }], details: {} };
14
+ },
15
+ });
16
+ pi.on('tool_call', async (event, ctx) => {
17
+ if (!ctx.hasUI) return { block: true, reason: 'BenchGo approval channel is unavailable' };
18
+ const allowed = await ctx.ui.confirm(
19
+ `BenchGo: ${event.toolName}`,
20
+ JSON.stringify({ toolCallId: event.toolCallId, input: event.input }),
21
+ );
22
+ if (!allowed) return { block: true, reason: 'Denied by BenchGo user' };
23
+ });
24
+ }