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
package/src/cli.mjs ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from 'node:readline';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { parseArgs } from 'node:util';
5
+ import { Commander } from './runtime.mjs';
6
+
7
+ const { values } = parseArgs({ options: { 'data-dir': { type: 'string' }, config: { type: 'string' }, stdio: { type: 'boolean' }, help: { type: 'boolean' } } });
8
+ if (values.help) {
9
+ console.log('benchgo [--data-dir PATH] [--config JSON_FILE] [--stdio]\nInteractive native session control. --stdio is a trusted local JSONL API, not a network server.');
10
+ process.exit(0);
11
+ }
12
+ const config = values.config ? JSON.parse(await readFile(values.config, 'utf8')) : {};
13
+ if (values['data-dir']) config.dataDir = values['data-dir'];
14
+ const commander = await new Commander(config).open();
15
+ const json = value => process.stdout.write(JSON.stringify(value) + '\n');
16
+ let selected;
17
+ let closing = false;
18
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: !values.stdio && process.stdin.isTTY });
19
+ const stop = async () => {
20
+ if (closing) return; closing = true; rl.close();
21
+ try { await commander.close(); } catch (error) { process.stderr.write(error.message + '\n'); process.exitCode = 1; }
22
+ };
23
+ process.on('SIGTERM', () => void stop());
24
+ process.on('SIGINT', () => void stop());
25
+ commander.on('fault', error => { process.stderr.write(`BenchGo persistence error: ${error.message}\n`); void stop(); });
26
+ commander.on('event', event => {
27
+ if (values.stdio) json({ event });
28
+ else if (event.sessionId === selected) {
29
+ if (event.type === 'text.delta') process.stdout.write(event.data.text);
30
+ else if (event.type === 'approval.requested') console.log(`\n[${event.data.kind}] ${event.data.id}\n${JSON.stringify(event.data.details, null, 2)}\n/approve ID yes|no 或 /answer ID JSON`);
31
+ else if (['turn.completed', 'session.error', 'input.uncertain', 'provider.retry'].includes(event.type)) console.log(`\n[${event.type}] ${JSON.stringify(event.data)}`);
32
+ }
33
+ });
34
+
35
+ async function dispatch(method, params) {
36
+ switch (method) {
37
+ case 'providers': return commander.catalog();
38
+ case 'doctor': return commander.doctor();
39
+ case 'list': return commander.list();
40
+ case 'create': return commander.create(params);
41
+ case 'connect': return commander.connect(params.id);
42
+ case 'send': return commander.submit(params.id, params.text, { requestId: params.requestId });
43
+ case 'steer': await commander.steer(params.id, params.text); return commander.snapshot(params.id);
44
+ case 'interrupt': await commander.interrupt(params.id); return commander.snapshot(params.id);
45
+ case 'approve': commander.approve(params.id, params.approvalId, params.answer); return commander.snapshot(params.id);
46
+ case 'events': return commander.events(params.id, params.after);
47
+ case 'history': return commander.history(params.id);
48
+ case 'discover': return commander.discover(params.id);
49
+ case 'wait': return commander.wait(params.id, { timeout: params.timeout });
50
+ case 'close': return commander.disconnect(params.id);
51
+ case 'archive': return commander.archive(params.id, params.archived);
52
+ default: throw new Error(`Unknown method: ${method}`);
53
+ }
54
+ }
55
+
56
+ if (!values.stdio) console.log('BenchGo\n/new codex|claude|pi|opencode|gemini|hermes|grok|mcode [cwd] /list /use ID /resume ID\n/steer TEXT /stop /approve ID yes|no /answer ID JSON\n/events [cursor] /history /discover /close /archive /quit\n输入普通文字向当前会话发送任务。原生会话导入使用 --stdio create 的 nativeId。');
57
+
58
+ // Requests can overlap in JSONL mode so an approval can be answered while a
59
+ // wait request is pending. Commander owns session mutation serialization.
60
+ const inFlight = new Set();
61
+ rl.on('line', line => {
62
+ const task = (async () => {
63
+ let request;
64
+ try {
65
+ if (values.stdio) {
66
+ request = JSON.parse(line);
67
+ const result = await dispatch(request.method, request.params ?? {});
68
+ json({ id: request.id ?? null, result });
69
+ } else {
70
+ if (!line.trim()) return;
71
+ const [command, ...rest] = line.trim().split(/\s+/); const text = rest.join(' ');
72
+ if (command === '/quit') { void stop(); return; }
73
+ let result;
74
+ if (command === '/new') { result = await commander.create({ provider: rest[0], cwd: rest.slice(1).join(' ') || process.cwd() }); selected = result.id; }
75
+ else if (command === '/list') result = commander.list();
76
+ else if (command === '/doctor') result = await commander.doctor();
77
+ else if (command === '/use') { commander.get(text); selected = text; result = commander.snapshot(selected); }
78
+ else if (command === '/resume') { selected = text; result = await commander.connect(selected); }
79
+ else {
80
+ if (!selected) throw new Error('先使用 /new 或 /use 选择会话');
81
+ if (command === '/stop') result = await dispatch('interrupt', { id: selected });
82
+ else if (command === '/steer') result = await dispatch('steer', { id: selected, text });
83
+ else if (command === '/events') result = commander.events(selected, Number(text || 0));
84
+ else if (command === '/history') result = await commander.history(selected);
85
+ else if (command === '/discover') result = await commander.discover(selected);
86
+ else if (command === '/close') result = await commander.disconnect(selected);
87
+ else if (command === '/archive') result = await commander.archive(selected);
88
+ else if (command === '/approve') result = await dispatch('approve', { id: selected, approvalId: rest[0], answer: { allow: rest[1] === 'yes' } });
89
+ else if (command === '/answer') result = await dispatch('approve', { id: selected, approvalId: rest[0], answer: JSON.parse(rest.slice(1).join(' ')) });
90
+ else if (command.startsWith('/')) throw new Error('未知命令');
91
+ else result = await commander.submit(selected, line);
92
+ }
93
+ console.log(JSON.stringify(result, null, 2));
94
+ }
95
+ } catch (error) {
96
+ if (values.stdio) json({ id: request?.id ?? null, error: { message: error.message } });
97
+ else console.error(error.message);
98
+ }
99
+ })();
100
+ inFlight.add(task); task.finally(() => inFlight.delete(task));
101
+ });
102
+ rl.on('close', () => { void stop(); });
@@ -0,0 +1,17 @@
1
+ /** Provider-native slash commands; never interpret arbitrary text as shell input. */
2
+ export function normalizeCommand(name, args = '') {
3
+ if (typeof name !== 'string' || !/^[\w.:-]{1,120}$/.test(name.replace(/^\//, ''))) throw new Error('Invalid native command name');
4
+ if (typeof args !== 'string' || args.length > 20000) throw new Error('Invalid native command arguments');
5
+ return { name: name.replace(/^\//, ''), arguments: args };
6
+ }
7
+
8
+ export const compactCommand = { name: 'compact', description: 'Compress the native conversation context', argumentHint: '', kind: 'command' };
9
+
10
+ export function commandCatalog(commands = [], builtins = []) {
11
+ const result = new Map(builtins.map(command => [command.name, command]));
12
+ for (const command of commands) {
13
+ if (!command || typeof command.name !== 'string' || !/^[\w.:-]{1,120}$/.test(command.name)) continue;
14
+ if (!result.has(command.name)) result.set(command.name, { name: command.name, description: command.description ?? '', argumentHint: command.argumentHint ?? '', kind: command.kind === 'skill' || command.source === 'skill' ? 'skill' : 'command' });
15
+ }
16
+ return [...result.values()].sort((a, b) => a.name.localeCompare(b.name));
17
+ }
@@ -0,0 +1,47 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export const delegationSchema = { type: 'object', properties: {
6
+ provider: { type: 'string', enum: ['codex', 'claude', 'pi', 'opencode', 'gemini', 'hermes', 'grok', 'mcode'] },
7
+ model: { type: 'string' }, text: { type: 'string' },
8
+ operation: { type: 'string', enum: ['delegate', 'result'] }, sessionId: { type: 'string' },
9
+ }, required: [], additionalProperties: false };
10
+
11
+ /** Per-process, per-session capability endpoint for native MCP/extension tools. No public listener. */
12
+ export class DelegationBridge {
13
+ constructor(commander, { serverFactory = createServer } = {}) { this.commander = commander; this.tokens = new Map(); this.serverFactory = serverFactory; }
14
+ async open() {
15
+ this.server = this.serverFactory(async (req, res) => {
16
+ const id = this.tokens.get(req.headers.authorization?.replace(/^Bearer /, ''));
17
+ res.setHeader('Content-Type', 'application/json');
18
+ if (!id || req.method !== 'POST' || req.url !== '/delegate') { res.writeHead(403); res.end('{}'); return; }
19
+ try {
20
+ let body = ''; for await (const chunk of req) { body += chunk; if (body.length > 65536) throw new Error('Delegation request too large'); }
21
+ res.end(JSON.stringify(await this.commander.delegate(id, JSON.parse(body))));
22
+ } catch (error) { res.writeHead(400); res.end(JSON.stringify({ error: error.message })); }
23
+ });
24
+ try {
25
+ await new Promise((resolve, reject) => {
26
+ const failed = error => { this.server.removeListener('listening', ready); this.server.removeListener('error', failed); reject(error); };
27
+ const ready = () => { this.server.removeListener('error', failed); resolve(); };
28
+ this.server.once('error', failed);
29
+ this.server.once('listening', ready);
30
+ try { this.server.listen(0, '127.0.0.1'); } catch (error) { failed(error); }
31
+ });
32
+ } catch (error) { await this.close(); throw error; }
33
+ this.url = `http://127.0.0.1:${this.server.address().port}/delegate`;
34
+ return this;
35
+ }
36
+ forSession(id) {
37
+ const token = randomBytes(32).toString('hex'); this.tokens.set(token, id);
38
+ return { url: this.url, token, command: process.execPath, script: fileURLToPath(new URL('./mcp.mjs', import.meta.url)), schema: delegationSchema };
39
+ }
40
+ revoke(id) { for (const [token, owner] of this.tokens) if (owner === id) this.tokens.delete(token); }
41
+ async close() {
42
+ this.tokens.clear();
43
+ if (!this.server) return;
44
+ this.server.closeAllConnections();
45
+ await new Promise(resolve => this.server.close(resolve));
46
+ }
47
+ }
package/src/fonts.mjs ADDED
@@ -0,0 +1,17 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ /** Fixed asset allowlist; same owner authentication as the existing Connection routes. */
4
+ export async function mountFonts(ctx) {
5
+ const root = new URL('../assets/fonts/', import.meta.url);
6
+ const manifest = JSON.parse(await readFile(new URL('manifest.json', root), 'utf8'));
7
+ for (const { file } of manifest) {
8
+ ctx.effect(() => ctx.connection.fetch.register({
9
+ path: '/api/commander-fonts/' + file, methods: ['GET'], requestBody: 'buffered',
10
+ async fetch() {
11
+ return new Response(new Uint8Array(await readFile(new URL('web/' + file, root))), {
12
+ headers: { 'content-type': 'font/woff2', 'cache-control': 'private, max-age=31536000, immutable', 'x-content-type-options': 'nosniff' },
13
+ });
14
+ },
15
+ }));
16
+ }
17
+ }
@@ -0,0 +1,228 @@
1
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
2
+ import { discoverModels } from './models.mjs';
3
+ import { singleRequestUsage } from './harness-usage.mjs';
4
+ import { decodeRouteModelId } from './picker-catalog.mjs';
5
+
6
+ const providers = { codex: 'Codex', claude: 'Claude Code', pi: 'Pi', opencode: 'OpenCode', gemini: 'Gemini CLI', hermes: 'Hermes', grok: 'Grok Build', mcode: 'MiniMax Code' };
7
+ const route = provider => `commander-${provider}`;
8
+ const inputPrefix = 'harness-user:';
9
+
10
+ /** Native agents remain the tool executor; Harness owns the conversation UI and journal. */
11
+ export function mountHarnessAdapter(ctx, commander, { bindOwner, cwd, discover = discoverModels } = {}) {
12
+ const active = new Set();
13
+ const metadata = new Map();
14
+ const jobs = new Set();
15
+ let disposed = false;
16
+ class CommanderAdapter extends LlmAdapter {
17
+ providerInfo(provider) { return { id: provider, name: providers[provider.slice('commander-'.length)] }; }
18
+ providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
19
+ async listModels(provider) {
20
+ const native = provider.slice('commander-'.length);
21
+ if (commander.pickerCatalog) {
22
+ const models = await commander.pickerCatalog.models(native);
23
+ for (const model of models) metadata.set(`${provider}/${model.id}`, model);
24
+ return models;
25
+ }
26
+ const directory = cwd ?? ctx.get('workspaceRegistry')?.list()[0]?.path;
27
+ const models = [{ provider, id: 'native-default', name: `${providers[native]} 默认模型`, inputModalities: ['text'] }];
28
+ if (directory) {
29
+ try {
30
+ const known = await discover(native, commander.providerConfig ? await commander.providerConfig(native) : commander.providers.get(native)?.config ?? {}, directory);
31
+ // Reuse an existing native connection; discovering a model never creates a session.
32
+ if (commander.drivers && commander.list) {
33
+ for (const session of commander.list().filter(row => row.provider === native && row.cwd === directory)) {
34
+ const driver = commander.drivers.get(session.id);
35
+ if (driver?.models) known.push(...await driver.models());
36
+ }
37
+ }
38
+ for (const model of known) {
39
+ if (models.some(value => value.id === model.id)) continue;
40
+ models.push({ provider, id: model.id, name: model.name, inputModalities: ['text'], ...(Number.isSafeInteger(model.contextWindow) && model.contextWindow > 0 ? { context: { contextWindow: model.contextWindow } } : {}) });
41
+ }
42
+ } catch (error) {
43
+ ctx.logger?.warn?.(`BenchGo ${native} model discovery failed: ${error.message}`);
44
+ }
45
+ }
46
+ for (const model of models) metadata.set(`${provider}/${model.id}`, model);
47
+ return models;
48
+ }
49
+ async resolveModel(provider, model) {
50
+ // Exact metadata includes native reasoning choices and supplier identity.
51
+ // A historical route can still resolve its identity after directory removal.
52
+ const binding = decodeRouteModelId(model);
53
+ if (binding) {
54
+ if (!metadata.has(`${provider}/${model}`) && commander.pickerCatalog) await this.listModels(provider);
55
+ return metadata.get(`${provider}/${model}`) ?? { provider, id: model, name: binding.model, inputModalities: ['text'] };
56
+ }
57
+ return metadata.get(`${provider}/${model}`) ?? { provider, id: model, name: model === 'native-default' ? `${this.providerInfo(provider).name} 默认模型` : model, inputModalities: ['text'] };
58
+ }
59
+ async *stream(options) {
60
+ if (disposed) throw new Error('BenchGo adapter is disposed');
61
+ if (options.purpose) throw new Error(`BenchGo native agents do not support auxiliary ${options.purpose} requests`);
62
+ if (options.reasoningEffort !== undefined) throw new Error('此工具不支持 API 模型的思考档位,请在设置 → Harness 中配置对应工具的推理强度后新建对话');
63
+ options.signal?.throwIfAborted();
64
+ const agent = ctx.agents.requireInitiator();
65
+ const owner = String(agent.session.id);
66
+ if (!options.sessionId || String(options.sessionId) !== owner) throw new Error('BenchGo requires the initiating Harness session identity');
67
+ const directory = agent.session.header.cwd;
68
+ if (!directory) throw new Error('BenchGo requires a project working directory');
69
+ const native = options.provider.slice('commander-'.length);
70
+ if (!Object.hasOwn(providers, native)) throw new Error('Unknown BenchGo provider');
71
+ const binding = decodeRouteModelId(options.model);
72
+ const nativeModel = binding?.model ?? (options.model === 'native-default' ? undefined : options.model);
73
+ const human = options.messages.filter(message => message.role === 'user' && message.source?.kind === 'user');
74
+ if (!human.length) throw new Error('BenchGo requires a durable human message');
75
+ for (const message of human) {
76
+ if (!message.id) throw new Error('BenchGo requires stable human message identities');
77
+ if (message.content.some(block => block.type !== 'text')) throw new Error('BenchGo currently supports text messages only; attachments are not supported');
78
+ }
79
+ const key = owner;
80
+ if (active.has(key)) throw new Error('A BenchGo request is already active for this conversation and model');
81
+ active.add(key);
82
+ const controller = new AbortController();
83
+ let done;
84
+ const settled = new Promise(resolve => { done = resolve; });
85
+ const job = { controller, settled }; jobs.add(job);
86
+ const abort = () => controller.abort(options.signal.reason);
87
+ options.signal?.addEventListener('abort', abort, { once: true });
88
+ let id, wake, version = 0, finished = false;
89
+ const notify = event => { if (event.sessionId === id) { version++; wake?.(); } };
90
+ let interrupting;
91
+ const cancelled = () => {
92
+ version++; wake?.();
93
+ if (id && ['running', 'waiting'].includes(commander.snapshot(id).status)) {
94
+ interrupting = commander.interrupt(id);
95
+ interrupting.catch(error => { ctx.logger?.warn?.(`BenchGo interrupt failed: ${error.message}`); });
96
+ }
97
+ };
98
+ controller.signal.addEventListener('abort', cancelled);
99
+ commander.on('event', notify);
100
+ try {
101
+ const ownedSessions = commander.list().filter(session => !session.archived && (!session.parentId || session.harnessAdopted) && session.ownerSessionId === owner);
102
+ const existing = ownedSessions.find(session => session.provider === native && session.cwd === directory && (binding
103
+ ? session.route?.providerId === binding.providerId
104
+ : !session.route && (session.model ?? 'native-default') === options.model));
105
+ if (existing && binding && (existing.route?.model !== binding.model || existing.model !== nativeModel)) {
106
+ if (['running', 'waiting', 'connecting'].includes(existing.status)) throw new Error('请等待当前轮次完成后再切换模型');
107
+ if (commander.pickerCatalog) {
108
+ const available = await commander.pickerCatalog.models(native);
109
+ if (!available.some(model => model.id === options.model)) throw new Error('工具供应商或模型已变更,请重新选择模型');
110
+ }
111
+ const driver = commander.drivers.get(existing.id);
112
+ if (driver && !driver.setModel) throw new Error(`${native} 当前连接不支持在同一会话中切换模型,请新建聊天`);
113
+ // Decoding a picker ID recovers identity only. Re-capture the full
114
+ // binding so reconnects retain validation and this session's settings.
115
+ const nextRoute = commander.routeService?.capture
116
+ ? await commander.routeService.capture({ tool: native, ...binding, ...(existing.route?.quickSettings ? { quickSettings: existing.route.quickSettings } : {}) })
117
+ : binding;
118
+ if (existing.route?.version && !nextRoute.version) throw new Error('会话绑定服务不可用,无法保存模型变更');
119
+ if (existing.route?.version && ['mode', 'baseURL', 'protocol', 'sourceProtocol', 'credentialRef'].some(key => existing.route[key] !== nextRoute[key])) throw new Error('绑定供应商的地址、协议或凭据引用已更改,请新建会话或恢复原配置。');
120
+ await driver?.setModel(nativeModel);
121
+ const record = commander.get(existing.id);
122
+ record.model = nativeModel;
123
+ record.route = nextRoute;
124
+ await commander.persist();
125
+ commander.event(existing.id, 'model.changed', { providerId: binding.providerId, model: binding.model, nativeModel });
126
+ }
127
+ if (!existing && commander.pickerCatalog) {
128
+ if (ownedSessions.length) throw new Error('此对话已绑定其他工具或供应商模型;请新建聊天,已有记录与运行配置不会被修改');
129
+ if (!binding) throw new Error('请选择已绑定供应商的工具模型;本机默认配置不会被自动用于新会话');
130
+ const available = await commander.pickerCatalog.models(native);
131
+ if (!available.some(model => model.id === options.model)) throw new Error('工具供应商或模型已变更,请重新选择模型');
132
+ }
133
+ const session = existing ?? await commander.create({ provider: native, cwd: directory, ...(nativeModel ? { model: nativeModel } : {}), ...(binding ? { route: binding } : {}), ownerSessionId: owner, title: human.at(-1).content.map(block => block.text).join('\n').slice(0, 120) });
134
+ id = session.id;
135
+ bindOwner?.(id, agent);
136
+ controller.signal.throwIfAborted();
137
+ if (['disconnected', 'closed', 'failed'].includes(commander.snapshot(id).status)) await commander.connect(id);
138
+ const record = commander.get(id);
139
+ const receipts = record.receipts;
140
+ // The terminal message id keys the whole admitted batch; preceding human messages
141
+ // are context only when entering this native route for the first time.
142
+ const latest = human.at(-1);
143
+ const requestId = `${inputPrefix}${latest.id}`;
144
+ let lastDelivered = -1;
145
+ human.forEach((message, index) => { if (Object.hasOwn(receipts, `${inputPrefix}${message.id}`)) lastDelivered = index; });
146
+ const pending = Object.hasOwn(receipts, requestId) ? [] : human.slice(lastDelivered + 1);
147
+ const text = pending.map(message => message.content.map(block => block.text).join('\n')).join('\n\n');
148
+ const prior = receipts[requestId];
149
+ if (prior?.receipt.status === 'uncertain') throw new Error('Native input acceptance is uncertain; inspect the native session before retrying');
150
+ let cursor = prior ? 0 : record.cursor;
151
+ const receipt = prior?.receipt ?? await commander.submit(id, text, { requestId });
152
+ const usageSamples = new Map();
153
+ let result = '', index = 0, started = false, completed = false;
154
+ while (!completed) {
155
+ controller.signal.throwIfAborted();
156
+ const observed = version;
157
+ const batch = commander.events(id, cursor, 500);
158
+ if (batch.truncated) throw new Error('BenchGo event history was truncated; refusing to replay an incomplete native response');
159
+ cursor = batch.cursor;
160
+ for (const event of batch.events) {
161
+ if (event.turnId !== receipt.turnId) continue;
162
+ if (event.type === 'usage') {
163
+ const sampleId = event.data.id;
164
+ // An unkeyed sample cannot prove that an update is the same request.
165
+ usageSamples.set(sampleId ?? 'unidentified:' + event.seq, event.data);
166
+ } else if (native === 'claude' && event.type === 'message' && event.data.type === 'assistant' && event.data.message?.usage) {
167
+ const message = event.data.message;
168
+ usageSamples.set(message.id ?? 'unidentified:' + event.seq, { usage: message.usage });
169
+ } else if (event.type === 'text.delta') {
170
+ if (!started) { started = true; yield { type: 'block-start', index, blockType: 'text' }; }
171
+ result += event.data.text;
172
+ yield { type: 'text-delta', index, text: event.data.text };
173
+ } else if (event.type === 'turn.completed') {
174
+ if (event.data.status !== 'completed') throw new Error(`Native ${native} turn ${event.data.status}: ${JSON.stringify(event.data.error ?? event.data.errors ?? '')}`);
175
+ if (!result && event.data.result) {
176
+ result = String(event.data.result); started = true;
177
+ yield { type: 'block-start', index, blockType: 'text' };
178
+ yield { type: 'text-delta', index, text: result };
179
+ }
180
+ completed = true; finished = true; break;
181
+ } else if (['session.error', 'provider.error', 'input.uncertain'].includes(event.type)) throw new Error(event.data.message ?? JSON.stringify(event.data));
182
+ }
183
+ if (!completed && cursor >= batch.latest) {
184
+ if (['failed', 'closed', 'disconnected'].includes(commander.snapshot(id).status)) throw new Error('Native session disconnected before completing its response');
185
+ await new Promise(resolve => { wake = resolve; if (version !== observed || controller.signal.aborted) resolve(); });
186
+ wake = undefined;
187
+ }
188
+ }
189
+ if (!started) throw new Error('Native agent completed without a text response');
190
+ yield { type: 'block-end', index, block: { type: 'text', text: result } };
191
+ // Let Harness stamp stream timing and fold tokenUsage/contextPressure.
192
+ // A replay has no live decode interval; multiple native requests are not
193
+ // one model context and remain available in the separate billing ledger.
194
+ const usage = prior ? undefined : singleRequestUsage(native, usageSamples);
195
+ if (usage) yield { type: 'usage', usage };
196
+ yield { type: 'finish', reason: { kind: 'stop' } };
197
+ } finally {
198
+ commander.off('event', notify);
199
+ controller.signal.removeEventListener('abort', cancelled);
200
+ options.signal?.removeEventListener('abort', abort);
201
+ try {
202
+ if (interrupting) await interrupting;
203
+ else if (!finished && id && ['running', 'waiting'].includes(commander.snapshot(id).status)) await commander.interrupt(id);
204
+ } finally { active.delete(key); jobs.delete(job); done(); }
205
+ }
206
+ }
207
+ }
208
+ const adapter = new CommanderAdapter();
209
+ const unregister = ctx.llm.registerAdapter(Object.keys(providers).map(route), adapter);
210
+ // Inspect original attachments before LlmRuntime converts unsupported modalities
211
+ // to text placeholders or projects durable file handles into text.
212
+ ctx.on('llm/stream', async function* (options, next) {
213
+ if (Object.keys(providers).some(provider => route(provider) === options.provider)
214
+ && options.messages.some(message => message.role === 'user' && message.source?.kind === 'user'
215
+ && message.content.some(block => block.type !== 'text'))) {
216
+ yield { type: 'finish', reason: { kind: 'error', failure: { code: 'UNSUPPORTED_ATTACHMENT', message: 'BenchGo currently supports text messages only; attachments are not supported' } } };
217
+ return;
218
+ }
219
+ yield* next();
220
+ });
221
+ ctx.effect(() => async () => {
222
+ disposed = true;
223
+ unregister();
224
+ for (const job of jobs) job.controller.abort(new Error('BenchGo adapter disposed'));
225
+ await Promise.allSettled([...jobs].map(job => job.settled));
226
+ });
227
+ return adapter;
228
+ }
@@ -0,0 +1,39 @@
1
+ import { harnessSelection } from './harness-selection.mjs';
2
+ /** Native commands use the official Harness command UI and human-only result cards. */
3
+ export function mountHarnessCommands(ctx, commander, { bindOwner } = {}) {
4
+ ctx.effect(() => ctx.commands.register({
5
+ name: 'native',
6
+ description: '查看或执行当前编码工具的原生命令',
7
+ input: { hint: '命令名与参数;留空查看可用命令' },
8
+ async handler(invocation) {
9
+ const { agent, signal, commandId } = invocation;
10
+ const selection = harnessSelection(ctx, agent);
11
+ const provider = selection.provider?.replace(/^commander-/, '');
12
+ if (!selection.provider?.startsWith('commander-')) return { kind: 'error', text: '请先在模型菜单中选择 Codex、Claude Code、Pi 或 OpenCode。' };
13
+ const id = String(agent.session.id);
14
+ const model = selection.model;
15
+ const session = commander.list().find(s => s.ownerSessionId === id && (!s.parentId || s.harnessAdopted) && !s.archived && s.provider === provider && (s.model || 'native-default') === model);
16
+ if (!session) return { kind: 'error', text: '先发送一条消息建立原生会话,再使用 /native。' };
17
+ bindOwner?.(session.id, agent);
18
+ signal.throwIfAborted();
19
+ if (['disconnected', 'closed', 'failed'].includes(session.status)) await commander.connect(session.id);
20
+ const line = invocation.rawInput.trim();
21
+ if (!line) {
22
+ const catalog = await commander.commands(session.id);
23
+ return { kind: 'success', text: catalog.length ? catalog.map(c => `/${c.name}${c.argumentHint ? ` ${c.argumentHint}` : ''}${c.description ? ` — ${c.description}` : ''}`).join('\n') : '此工具没有提供可执行的原生命令。' };
24
+ }
25
+ const match = line.match(/^(\S+)(?:\s+([\s\S]*))?$/);
26
+ const abort = () => { commander.interrupt(session.id).catch(error => ctx.logger.warn(error.message)); };
27
+ signal.addEventListener('abort', abort, { once: true });
28
+ try {
29
+ const cursor = commander.snapshot(session.id).cursor;
30
+ const receipt = await commander.command(session.id, { name: match[1], arguments: match[2] || '', requestId: `harness-command:${commandId}` });
31
+ const result = await commander.wait(session.id, { timeout: 55000, signal });
32
+ const events = commander.events(session.id, cursor, 500).events.filter(e => e.turnId === receipt.turnId);
33
+ const text = events.filter(e => e.type === 'text.delta').map(e => e.data.text).join('') || events.findLast(e => e.type === 'turn.completed')?.data.result;
34
+ if (['failed', 'closed', 'disconnected'].includes(result.status)) return { kind: 'error', text: text || '原生命令未正常完成,请检查工具状态。' };
35
+ return { kind: 'success', text: text || (['running', 'waiting', 'connecting'].includes(result.status) ? '原生命令仍在执行,请勿重复提交。' : '原生命令已完成。') };
36
+ } finally { signal.removeEventListener('abort', abort); }
37
+ },
38
+ }));
39
+ }
@@ -0,0 +1,5 @@
1
+ /** Read the official durable selection; Agent construction options may be stale. */
2
+ export function harnessSelection(ctx, agent) {
3
+ const projected = ctx.get?.('sessionProjections')?.stateOf(agent.session, 'modelSelection');
4
+ return projected?.pending ?? projected?.lastUsed ?? agent.session.requestHeader?.()?.config ?? agent.options;
5
+ }
@@ -0,0 +1,31 @@
1
+ const count = value => Number.isSafeInteger(value) && value >= 0 ? value : undefined;
2
+
3
+ /** Translate reported native counters to Harness's disjoint billing buckets. */
4
+ export function harnessUsage(provider, value = {}, convention) {
5
+ const input = count(value.inputTokens ?? value.input_tokens ?? value.input);
6
+ const outputTokens = count(value.outputTokens ?? value.output_tokens ?? value.output);
7
+ const cacheReadTokens = count(value.cacheReadTokens ?? value.cachedInputTokens ?? value.cache_read_input_tokens ?? value.cacheRead ?? value.cache?.read);
8
+ const cacheWriteTokens = count(value.cacheWriteTokens ?? value.cache_creation_input_tokens ?? value.cacheWrite ?? value.cache?.write);
9
+ if (input === undefined || outputTokens === undefined) return undefined;
10
+ // Codex's prompt includes cached input; its protocol has no cache-write bucket.
11
+ const inclusive = convention === 'inclusive' || provider === 'codex';
12
+ if (inclusive && (cacheReadTokens === undefined || cacheReadTokens > input)) return undefined;
13
+ const inputTokens = inclusive ? input - cacheReadTokens : input;
14
+ const usage = { inputTokens, outputTokens,
15
+ ...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
16
+ ...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
17
+ };
18
+ const reportedTotal = count(value.totalTokens ?? value.total_tokens ?? value.total);
19
+ if (reportedTotal !== undefined && reportedTotal >= inputTokens + outputTokens + (cacheReadTokens ?? 0) + (cacheWriteTokens ?? 0)) usage.totalTokens = reportedTotal;
20
+ const reasoningTokens = count(value.reasoningTokens ?? value.reasoningOutputTokens ?? value.reasoning_output_tokens ?? value.reasoning);
21
+ if (reasoningTokens !== undefined) usage.reasoningTokens = reasoningTokens;
22
+ return usage;
23
+ }
24
+
25
+ /** Only one identifiable request can safely occupy one Harness adapter step. */
26
+ export function singleRequestUsage(provider, samples) {
27
+ if (samples.size !== 1) return undefined;
28
+ if (String(samples.keys().next().value).startsWith('unidentified:')) return undefined;
29
+ const sample = samples.values().next().value;
30
+ return harnessUsage(provider, sample.usage, sample.convention);
31
+ }
@@ -0,0 +1,22 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // SDK history helpers read the parent environment. A separate short-lived
6
+ // process avoids ever changing process.env across concurrent A/B sessions.
7
+ export async function isolatedClaudeHistory(directory, operation, sessionId, cwd) {
8
+ if (!['info', 'messages', 'list'].includes(operation)) throw new Error('Unsupported isolated history operation');
9
+ const { stdout } = await promisify(execFile)(process.execPath, [fileURLToPath(import.meta.url), 'claude-history', operation, sessionId || '', cwd], {
10
+ env: { ...process.env, CLAUDE_CONFIG_DIR: directory }, timeout: 15_000, maxBuffer: 16 * 1024 * 1024, windowsHide: true,
11
+ });
12
+ return JSON.parse(stdout);
13
+ }
14
+
15
+ if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === 'claude-history') {
16
+ try {
17
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
18
+ const [, , , operation, sessionId, cwd] = process.argv;
19
+ const result = operation === 'info' ? await sdk.getSessionInfo(sessionId, { dir: cwd }) : operation === 'messages' ? await sdk.getSessionMessages(sessionId, { dir: cwd }) : operation === 'list' ? await sdk.listSessions({ dir: cwd, limit: 50 }) : null;
20
+ process.stdout.write(JSON.stringify(result ?? null));
21
+ } catch { process.stderr.write('Isolated Claude history read failed'); process.exitCode = 1; }
22
+ }
@@ -0,0 +1,114 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+
4
+ // The child never owns a shell. Framing, request lifetime and process lifetime
5
+ // are shared; product methods belong to the individual provider adapters.
6
+ export class LineProcess extends EventEmitter {
7
+ constructor(command, args, { cwd, env, dialect = 'jsonrpc', timeout = 30_000 } = {}) {
8
+ super();
9
+ this.dialect = dialect;
10
+ this.timeout = timeout;
11
+ this.pending = new Map();
12
+ this.sequence = 0;
13
+ this.tail = '';
14
+ this.stderr = '';
15
+ this.closed = false;
16
+ this.child = spawn(command, args, {
17
+ cwd, env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'],
18
+ windowsHide: true, detached: process.platform !== 'win32',
19
+ });
20
+ this.exited = new Promise(resolve => {
21
+ this.child.once('close', (code, signal) => {
22
+ this.fail(new Error(`Agent process exited (${code ?? signal})${this.stderr ? `: ${this.stderr.slice(-1500)}` : ''}`));
23
+ resolve();
24
+ });
25
+ });
26
+ this.child.on('error', error => this.fail(error));
27
+ this.child.stdin.on('error', error => this.fail(error));
28
+ this.child.stdout.setEncoding('utf8');
29
+ this.child.stderr.setEncoding('utf8');
30
+ this.child.stderr.on('data', chunk => { this.stderr = (this.stderr + chunk).slice(-8192); });
31
+ this.child.stdout.on('data', chunk => {
32
+ this.tail += chunk;
33
+ if (Buffer.byteLength(this.tail) > 16 * 1024 * 1024) {
34
+ this.fail(new Error('Agent exceeded JSONL frame limit'));
35
+ void this.close();
36
+ return;
37
+ }
38
+ let end;
39
+ while ((end = this.tail.indexOf('\n')) !== -1) {
40
+ const line = this.tail.slice(0, end).trim();
41
+ this.tail = this.tail.slice(end + 1);
42
+ if (!line) continue;
43
+ try { this.receive(JSON.parse(line)); }
44
+ catch (error) { this.fail(new Error(`Invalid agent protocol: ${error.message}`)); void this.close(); return; }
45
+ }
46
+ });
47
+ }
48
+
49
+ receive(message) {
50
+ const response = this.dialect === 'pi' ? message.type === 'response' : !message.method;
51
+ const waiter = response && this.pending.get(message.id);
52
+ if (waiter) {
53
+ this.pending.delete(message.id);
54
+ clearTimeout(waiter.timer);
55
+ const error = message.error || (message.success === false && 'Pi command failed');
56
+ if (error) waiter.reject(new Error(typeof error === 'string' ? error : error.message ?? JSON.stringify(error)));
57
+ else waiter.resolve(this.dialect === 'pi' ? message.data : message.result);
58
+ } else if (this.dialect === 'jsonrpc' && message.method && message.id !== undefined) {
59
+ this.emit('request', message);
60
+ } else {
61
+ this.emit('notification', message);
62
+ }
63
+ }
64
+
65
+ send(message) {
66
+ if (this.closed) throw new Error('Agent transport is closed');
67
+ this.child.stdin.write(JSON.stringify(message) + '\n');
68
+ }
69
+
70
+ request(method, params = {}, timeout = this.timeout) {
71
+ if (this.closed) return Promise.reject(new Error('Agent transport is closed'));
72
+ const id = `commander-${++this.sequence}`;
73
+ return new Promise((resolve, reject) => {
74
+ const timer = setTimeout(() => {
75
+ this.pending.delete(id);
76
+ reject(new Error(`Agent request timed out: ${method}; execution outcome may be unknown`));
77
+ }, timeout);
78
+ this.pending.set(id, { resolve, reject, timer });
79
+ try {
80
+ this.send(this.dialect === 'pi' ? { ...params, type: method, id } : { jsonrpc: '2.0', id, method, params });
81
+ } catch (error) {
82
+ clearTimeout(timer); this.pending.delete(id); reject(error);
83
+ }
84
+ });
85
+ }
86
+
87
+ notify(method, params = {}) { this.send({ jsonrpc: '2.0', method, params }); }
88
+ respond(id, result) { this.send({ jsonrpc: '2.0', id, result }); }
89
+ reject(id, message) { this.send({ jsonrpc: '2.0', id, error: { code: -32601, message } }); }
90
+
91
+ fail(error) {
92
+ if (this.closed) return;
93
+ this.closed = true;
94
+ for (const waiter of this.pending.values()) { clearTimeout(waiter.timer); waiter.reject(error); }
95
+ this.pending.clear();
96
+ this.emit('failure', error);
97
+ }
98
+
99
+ kill(signal) {
100
+ if (!this.child.pid) return;
101
+ try {
102
+ if (process.platform === 'win32') this.child.kill(signal);
103
+ else process.kill(-this.child.pid, signal);
104
+ } catch (error) { if (error.code !== 'ESRCH') throw error; }
105
+ }
106
+
107
+ async close() {
108
+ this.fail(new Error('Agent transport closed'));
109
+ this.child.stdin.end();
110
+ this.kill('SIGTERM');
111
+ const timer = setTimeout(() => this.kill('SIGKILL'), 1500);
112
+ try { await this.exited; } finally { clearTimeout(timer); }
113
+ }
114
+ }