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,21 @@
1
+ import { mountLink } from '../lib/link.mjs';
2
+
3
+ export const name = 'benchgo-link';
4
+ export const inject = [
5
+ 'commander',
6
+ 'commanderLinkConfig',
7
+ 'connection',
8
+ 'workspaceRegistry',
9
+ 'typertGateway',
10
+ 'sessionController',
11
+ 'webServer',
12
+ 'clientModules',
13
+ ];
14
+
15
+ export async function apply(ctx, config = {}) {
16
+ await mountLink(ctx, {
17
+ ...ctx.get('commanderLinkConfig'),
18
+ ...config,
19
+ webRoot: new URL('../lib/link-web/', import.meta.url),
20
+ });
21
+ }
package/src/mcp.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { delegationSchema } from './delegation.mjs';
3
+ // Spawned only by the native engine. Capabilities arrive through its private environment.
4
+ const lines = createInterface({ input: process.stdin });
5
+ for await (const line of lines) {
6
+ void (async () => {
7
+ let m;
8
+ try {
9
+ m = JSON.parse(line); if (m.id === undefined) return;
10
+ let result;
11
+ if (m.method === 'initialize') result = { protocolVersion: m.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: 'BenchGo', version: '0.2.0' } };
12
+ else if (m.method === 'ping') result = {};
13
+ else if (m.method === 'tools/list') result = { tools: [{ name: 'commander_delegate', description: 'Delegate to another coding engine in this same project. Provide provider and text, optionally model. Human approval required. Returns child result; if pending, call again with operation=result and sessionId to read it. Do not resubmit.', inputSchema: delegationSchema }] };
14
+ else if (m.method === 'tools/call' && m.params.name === 'commander_delegate') {
15
+ const r = await fetch(process.env.COMMANDER_DELEGATE_URL, { method: 'POST', headers: { authorization: `Bearer ${process.env.COMMANDER_DELEGATE_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify(m.params.arguments), signal: AbortSignal.timeout(240000) });
16
+ const value = await r.json(); result = { content: [{ type: 'text', text: JSON.stringify(value) }], isError: !r.ok };
17
+ } else throw new Error('Unsupported MCP method');
18
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: m.id, result }) + '\n');
19
+ } catch (error) { if (m?.id !== undefined) process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: m.id, error: { code: -32603, message: error.message } }) + '\n'); }
20
+ })();
21
+ }
@@ -0,0 +1,125 @@
1
+ import { subscriptionHarnessTool } from './provider-access.mjs';
2
+
3
+ const LIMIT = 4 * 1024 * 1024;
4
+ const forbidden = new Set(['__proto__', 'constructor', 'prototype']);
5
+ class ProviderError extends Error { constructor(message, status = 400) { super(message); this.status = status; } }
6
+ function text(value, label, max = 512) { if (typeof value !== 'string' || !value.trim() || value.length > max || /[\u0000-\u001f]/u.test(value)) throw new ProviderError('Invalid ' + label); return value.trim(); }
7
+ function endpoint(value) {
8
+ let url; try { url = new URL(text(value, 'provider URL', 2048)); } catch { throw new ProviderError('Invalid provider URL'); }
9
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new ProviderError('Provider URL must be HTTP(S), without credentials, query or fragment');
10
+ return url.href.replace(/\/$/u, '');
11
+ }
12
+ function pathValue(value, path) { for (const part of path || []) { if (forbidden.has(part) || !value || !Object.hasOwn(value, part)) return undefined; value = value[part]; } return value; }
13
+ function classification(model) {
14
+ const kinds = new Set(), metadata = [model.type, model.task, model.pipeline_tag, model.modality, model.modalities, model.capabilities, model.architecture, model.metadata].filter(value => value !== undefined);
15
+ const description = JSON.stringify(metadata).toLowerCase(), hasMetadata = /(?:^|[^a-z])(?:transcri|speech|audio|text|chat|completion|asr|tts|embedding|rerank|moderation|image|video|realtime)/u.test(JSON.stringify([model.task, model.pipeline_tag, model.modality, model.modalities, model.capabilities, model.architecture, model.metadata]).toLowerCase()) || /generation|embedding|rerank|moderation|image|audio|speech|asr|tts/u.test(String(model.type || '').toLowerCase());
16
+ if (/transcri|speech.to.text|audio.to.text|automatic.speech.recognition|"asr"/u.test(description)) kinds.add('asr');
17
+ if (/text.to.speech|speech.synth|"tts"|"speech"\s*:\s*true/u.test(description)) kinds.add('tts');
18
+ const architecture = model.architecture || model.modalities || {};
19
+ const inputs = architecture.input_modalities || architecture.input || [], outputs = architecture.output_modalities || architecture.output || [];
20
+ if (Array.isArray(inputs) && Array.isArray(outputs)) {
21
+ if (inputs.includes('audio') && outputs.includes('text') && !outputs.includes('audio')) kinds.add('asr');
22
+ if (inputs.includes('text') && outputs.includes('audio') && !outputs.includes('text')) kinds.add('tts');
23
+ }
24
+ let inferred = false;
25
+ if (!hasMetadata) {
26
+ const id = model.id.toLowerCase();
27
+ if (/whisper|transcri|(?:^|[-_/])asr(?:$|[-_/])|sensevoice|paraformer|funasr/u.test(id)) { kinds.add('asr'); inferred = true; }
28
+ if (/(?:^|[-_/])tts(?:$|[-_/])|text.to.speech|cosyvoice|kokoro|fish.speech|speech.synth/u.test(id)) { kinds.add('tts'); inferred = true; }
29
+ }
30
+ const explicitChat = /text.generation|chat|completion/u.test(description) || Array.isArray(outputs) && outputs.includes('text') && Array.isArray(inputs) && inputs.includes('text');
31
+ const nonChat = kinds.size > 0 || !explicitChat && /audio|speech|realtime|image|embedding|rerank|moderation|video/u.test(hasMetadata ? description : model.id.toLowerCase());
32
+ return { kinds: [...kinds], inferred, chat: !nonChat };
33
+ }
34
+ async function limitedJson(response, signal) {
35
+ if (Number(response.headers.get('content-length')) > LIMIT) { await response.body?.cancel(); throw new ProviderError('Model catalog exceeds 4 MiB', 502); }
36
+ if (!response.body) throw new ProviderError('Model service returned an empty response', 502);
37
+ const reader = response.body.getReader(), chunks = []; let size = 0;
38
+ const abort = () => { void reader.cancel().catch(() => {}); }; signal.addEventListener('abort', abort, { once: true });
39
+ try {
40
+ while (true) { signal.throwIfAborted(); const { value, done } = await reader.read(); signal.throwIfAborted(); if (done) break; size += value.byteLength; if (size > LIMIT) throw new ProviderError('Model catalog exceeds 4 MiB', 502); chunks.push(value); }
41
+ try { return JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { throw new ProviderError('Model service returned invalid JSON', 502); }
42
+ } finally { signal.removeEventListener('abort', abort); await reader.cancel().catch(() => {}); }
43
+ }
44
+ async function discover(config, signal) {
45
+ const timeout = AbortSignal.timeout(20000), requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout; let data;
46
+ try {
47
+ const response = await fetch(config.baseURL + '/models', { headers: { authorization: 'Bearer ' + config.apiKey, accept: 'application/json' }, signal: requestSignal, redirect: 'error' });
48
+ if (!response.ok) { await response.body?.cancel(); throw new ProviderError('Model service rejected the request (' + response.status + ')', 502); } data = await limitedJson(response, requestSignal);
49
+ } catch (error) { if (error instanceof ProviderError) throw error; throw new ProviderError(requestSignal.aborted ? 'Model request cancelled or timed out' : 'Unable to reach the model service', 502); }
50
+ const rows = Array.isArray(data?.data) ? data.data : Array.isArray(data?.models) ? data.models : null;
51
+ if (!rows || rows.length > 20000) throw new ProviderError('Model service returned an invalid catalog', 502);
52
+ const seen = new Set(), models = [];
53
+ for (const row of rows) { if (!row || typeof row.id !== 'string' || !row.id.trim() || row.id.length > 512 || /[\u0000-\u001f]/u.test(row.id) || row.id.includes(config.apiKey) || seen.has(row.id)) continue; seen.add(row.id); const info = classification(row), name = typeof row.name === 'string' && row.name.length <= 512 && !row.name.includes(config.apiKey) && !/[\u0000-\u001f]/u.test(row.name) ? row.name : row.id; models.push({ id: row.id, name, kinds: info.kinds, inferred: info.inferred }); }
54
+ return { models };
55
+ }
56
+ export function createModelProviders(ctx) {
57
+ let nativeAuth;
58
+ const entries = () => ctx.llm.listConfigurableProviders();
59
+ const document = ns => ctx.settings.describe().find(row => row.ns === ns);
60
+ const profile = entry => pathValue(document(entry.settingsNs)?.value, entry.settingsPath) || {};
61
+ const credential = async (entry, value) => { const refs = value.apiKeyEnv ? [value.apiKeyEnv] : entry.nativeAuth?.env || []; for (const ref of refs) { const hit = await ctx.credentials.resolve(ref); if (hit?.value) return hit.value; } return ''; };
62
+ const address = (entry, value) => value.baseURL || entry.defaultBaseURL || '';
63
+ const access = async (entry, value) => {
64
+ const apiKey = await credential(entry, value);
65
+ const nativeOAuth = !value.apiKeyEnv && entry.declared !== true && entry.settingsNs === 'llm-pi-ai' && entry.nativeAuth?.oauth;
66
+ let oauthEnv = false;
67
+ if (nativeOAuth && apiKey) {
68
+ oauthEnv = /^sk-ant-oat/iu.test(apiKey);
69
+ for (const ref of entry.nativeAuth.env || []) if (/oauth/iu.test(ref) && (await ctx.credentials.resolve(ref))?.value === apiKey) oauthEnv = true;
70
+ }
71
+ if (apiKey && !oauthEnv) return { id: entry.provider, authMode: 'api', configured: true };
72
+ // A named API reference is authoritative, even if another route has OAuth.
73
+ if (value.apiKeyEnv || entry.declared === true || entry.settingsNs !== 'llm-pi-ai' || !entry.nativeAuth?.oauth) return { id: entry.provider, authMode: 'unconfigured', configured: false };
74
+ const record = /^[a-z][a-z0-9-]*$/u.test(entry.provider) ? await ctx.credentials.readRecord?.('llm-pi-ai/' + entry.provider) : undefined;
75
+ const tool = subscriptionHarnessTool(entry.provider);
76
+ const native = tool && nativeAuth ? await nativeAuth.status(tool) : undefined;
77
+ const nativeLogin = native?.status === 'subscription';
78
+ const granted = nativeLogin || oauthEnv || record?.kind === 'grant' && record.payload?.type === 'oauth' && typeof record.payload.access === 'string' && record.payload.access.length > 0;
79
+ if (!granted && entry.nativeAuth.env?.length) return { id: entry.provider, authMode: 'unconfigured', configured: false };
80
+ return { id: entry.provider, authMode: 'subscription', configured: granted, harnessTools: tool ? [tool] : [], authSource: nativeLogin ? 'native-cli-login' : oauthEnv ? 'official-oauth-env' : granted ? 'official-oauth-record' : 'official-oauth-provider', ...(native ? { nativeLoginStatus: native.status } : {}), routeReason: granted ? '订阅登录仅供对应工具使用;请在 Harness 工具中使用本机登录,订阅凭据不会转换为 API 密钥。' : '此供应商需要订阅登录;请先在对应 Harness 工具中完成登录。' };
81
+ };
82
+ const service = {
83
+ setNativeAuth(value) { nativeAuth = value; },
84
+ async list() {
85
+ // describe() projects every plugin schema/config; do it once per directory read.
86
+ const documents = new Map(ctx.settings.describe().map(row => [row.ns, row.value])), list = [];
87
+ for (const entry of entries()) {
88
+ const value = pathValue(documents.get(entry.settingsNs), entry.settingsPath) || {}, baseURL = address(entry, value), identity = await access(entry, value);
89
+ if (identity.authMode !== 'subscription' && (!baseURL || !identity.configured)) continue;
90
+ list.push({ ...identity, name: value.displayName || entry.displayName || entry.provider, ...(baseURL ? { baseURL: endpoint(baseURL) } : {}), ...(entry.declared === true ? { declared: true } : {}) });
91
+ }
92
+ return list.sort((a, b) => Number(/routergo/iu.test(b.id + b.name + b.baseURL)) - Number(/routergo/iu.test(a.id + a.name + a.baseURL)) || a.name.localeCompare(b.name));
93
+ },
94
+ async accessList() {
95
+ const documents = new Map(ctx.settings.describe().map(row => [row.ns, row.value]));
96
+ return Promise.all(entries().map(entry => access(entry, pathValue(documents.get(entry.settingsNs), entry.settingsPath) || {})));
97
+ },
98
+ async describeAccess(id) {
99
+ const entry = entries().find(row => row.provider === id); if (!entry) throw new ProviderError('Provider is not configured', 404);
100
+ return access(entry, profile(entry));
101
+ },
102
+ async resolve(id) {
103
+ const entry = entries().find(row => row.provider === id); if (!entry) throw new ProviderError('Provider is not configured', 404);
104
+ const value = profile(entry);
105
+ if ((await access(entry, value)).authMode === 'subscription') throw new ProviderError('订阅登录不能转换为 API 密钥,请在对应 Harness 工具中使用本机登录。', 409);
106
+ const baseURL = endpoint(address(entry, value)), apiKey = await credential(entry, value);
107
+ if (!apiKey) throw new ProviderError('Provider credential is not configured', 409); return { baseURL, apiKey };
108
+ },
109
+ async describeRoute(id) {
110
+ const entry = entries().find(row => row.provider === id);
111
+ if (!entry) throw new ProviderError('Provider is not configured', 404);
112
+ const value = profile(entry), config = await service.resolve(id);
113
+ return { ...config, protocol: value.api || value.protocol || entry.protocol || null, credentialRef: value.apiKeyEnv || entry.nativeAuth?.env?.join('|') || '' };
114
+ },
115
+ async models(id, signal) {
116
+ const originalEntry = entries().find(row => row.provider === id), originalRef = originalEntry ? profile(originalEntry).apiKeyEnv || originalEntry.nativeAuth?.env?.join('|') || '' : '';
117
+ const config = await service.resolve(id), { models } = await discover(config, signal);
118
+ const entry = entries().find(row => row.provider === id);
119
+ if (!entry) throw new ProviderError('Provider changed; refresh the model list again', 409);
120
+ const current = profile(entry), currentRef = current.apiKeyEnv || entry.nativeAuth?.env?.join('|') || '';
121
+ if (endpoint(address(entry, current)) !== config.baseURL || currentRef !== originalRef || await credential(entry, current) !== config.apiKey) throw new ProviderError('Provider changed; refresh the model list again', 409);
122
+ return models;
123
+ },
124
+ }; return service;
125
+ }
package/src/models.mjs ADDED
@@ -0,0 +1,37 @@
1
+ import { LineProcess } from './line-process.mjs';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { ACP_TOOL_DEFAULTS } from './providers/acp.mjs';
5
+
6
+ /** Discover models from the installed engine, never from the Harness provider catalog. */
7
+ export async function discoverModels(provider, config = {}, cwd = process.cwd()) {
8
+ if (ACP_TOOL_DEFAULTS[provider]) {
9
+ // Never create empty native sessions just to open the model menu.
10
+ const model = config.managedProfile?.model;
11
+ return model ? [{ id: model, name: model }] : [];
12
+ }
13
+ if (provider === 'codex' || provider === 'pi') {
14
+ const pi = provider === 'pi';
15
+ const wire = new LineProcess(config.command ?? provider, [...(config.commandArgs ?? []), ...(pi ? ['--mode', 'rpc', '--no-session'] : ['app-server', '--listen', 'stdio://'])], { cwd, env: config.env, dialect: pi ? 'pi' : 'jsonrpc' });
16
+ try {
17
+ if (!pi) { await wire.request('initialize', { clientInfo: { name: 'commander_models', version: '0.2.0' } }); wire.notify('initialized'); }
18
+ const result = await wire.request(pi ? 'get_available_models' : 'model/list', pi ? {} : { limit: 100 });
19
+ return (pi ? result.models : result.data).map(m => ({ id: pi ? `${m.provider}/${m.id}` : m.model ?? m.id, name: m.displayName ?? m.name ?? m.model ?? m.id, ...(Number.isSafeInteger(m.contextWindow) && m.contextWindow > 0 ? { contextWindow: m.contextWindow } : {}) }));
20
+ } finally { await wire.close(); }
21
+ }
22
+ if (provider === 'claude') {
23
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
24
+ const controller = new AbortController();
25
+ const q = sdk.query({ prompt: (async function* () { await new Promise(resolve => controller.signal.addEventListener('abort', resolve, { once: true })); })(), options: { cwd, pathToClaudeCodeExecutable: config.command ?? 'claude', env: { ...process.env, ...config.env }, settingSources: ['user', 'project', 'local'] } });
26
+ let timer;
27
+ try {
28
+ const models = await Promise.race([q.supportedModels(), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Claude model discovery timed out')), 30000); })]);
29
+ return models.map(m => ({ id: m.value, name: m.displayName ?? m.value }));
30
+ } finally { clearTimeout(timer); controller.abort(); q.close(); }
31
+ }
32
+ if (provider === 'opencode') {
33
+ const { stdout } = await promisify(execFile)(config.command ?? 'opencode', [...(config.commandArgs ?? []), 'models'], { cwd, env: { ...process.env, ...config.env }, timeout: 30000, maxBuffer: 1024 * 1024 });
34
+ return stdout.split('\n').map(s => s.trim()).filter(s => /^[\w.-]+\/\S+$/.test(s)).map(id => ({ id, name: id }));
35
+ }
36
+ throw new Error(`Unknown provider: ${provider}`);
37
+ }
@@ -0,0 +1,32 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execute = promisify(execFile);
5
+ /** Ask the existing CLI; never parse/copy account files or expose raw output. */
6
+ export function createNativeAuth({ installer, executor = execute, env = process.env, cacheMs = 15000 } = {}) {
7
+ const cache = new Map(), pending = new Map();
8
+ const inspect = async tool => {
9
+ if (!['codex', 'claude'].includes(tool)) return { tool, status: 'unsupported' };
10
+ try {
11
+ const launch = await installer.resolveLaunch(tool);
12
+ if (!launch) return { tool, status: 'not-installed' };
13
+ const args = tool === 'codex' ? ['login', 'status'] : ['auth', 'status', '--json'];
14
+ const output = await executor(launch.command, [...launch.args, ...args], { env: { ...env, ...launch.env }, timeout: 5000, maxBuffer: 32768, windowsHide: true });
15
+ if (tool === 'codex') {
16
+ const text = String(output.stdout || '') + String(output.stderr || '');
17
+ return { tool, status: /logged in using chatgpt/iu.test(text) ? 'subscription' : /logged in using (?:an? )?api key/iu.test(text) ? 'api' : 'unknown' };
18
+ }
19
+ const value = JSON.parse(output.stdout);
20
+ const subscription = value.loggedIn === true && value.authMethod === 'claude.ai' && (!value.apiProvider || value.apiProvider === 'firstParty');
21
+ return { tool, status: subscription ? 'subscription' : value.loggedIn ? 'api' : 'logged-out' };
22
+ } catch (error) { return { tool, status: error.code === 1 ? 'logged-out' : 'unavailable' }; }
23
+ };
24
+ return {
25
+ async status(tool) {
26
+ const current = cache.get(tool);
27
+ if (current && Date.now() - current.time < cacheMs) return { ...current.value };
28
+ if (!pending.has(tool)) pending.set(tool, inspect(tool).then(value => { cache.set(tool, { time: Date.now(), value }); return value; }).finally(() => pending.delete(tool)));
29
+ return { ...await pending.get(tool) };
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,82 @@
1
+ import { subscriptionTool } from './provider-access.mjs';
2
+
3
+ /** Credential-free catalog shared by the official picker and native adapters. */
4
+ const PREFIX = 'dsh-route-v1:';
5
+ const cleanId = value => typeof value === 'string' && value.length > 0 && value.length <= 512 && !/[\u0000-\u001f]/u.test(value);
6
+ export function encodeRouteModelId(providerId, nativeModelId) {
7
+ if (!cleanId(providerId) || !cleanId(nativeModelId)) throw new Error('Invalid provider/model route');
8
+ return PREFIX + Buffer.from(JSON.stringify([providerId, nativeModelId]), 'utf8').toString('base64url');
9
+ }
10
+ export function decodeRouteModelId(value) {
11
+ if (typeof value !== 'string' || !value.startsWith(PREFIX)) return null;
12
+ if (value.length > 8192) throw new Error('Invalid provider/model route');
13
+ try {
14
+ const pair = JSON.parse(Buffer.from(value.slice(PREFIX.length), 'base64url').toString('utf8'));
15
+ if (!Array.isArray(pair) || pair.length !== 2 || !pair.every(cleanId) || encodeRouteModelId(...pair) !== value) throw Error();
16
+ return { providerId: pair[0], model: pair[1] };
17
+ } catch { throw new Error('Invalid provider/model route'); }
18
+ }
19
+ const positive = value => Number.isSafeInteger(value) && value > 0;
20
+ function safeReasoning(value) {
21
+ if (!Array.isArray(value?.efforts)) return undefined;
22
+ const efforts = value.efforts.filter(item => cleanId(item?.id)).map(item => ({ id: item.id, name: cleanId(item.name) ? item.name : item.id, ...(cleanId(item.description) ? { description: item.description } : {}) }));
23
+ return efforts.length ? { efforts, ...(efforts.some(item => item.id === value.defaultEffort) ? { defaultEffort: value.defaultEffort } : {}) } : undefined;
24
+ }
25
+ export function createPickerCatalog({ commander, installer, profiles, providers, routeService, llm, cacheMs = 5000 } = {}) {
26
+ let installCache, installAt = 0, installationJob, catalogJob;
27
+ const installations = async () => {
28
+ if (installCache && Date.now() - installAt < cacheMs) return installCache;
29
+ if (!installationJob) installationJob = Promise.resolve(installer?.list?.() ?? []).then(rows => { installCache = rows; installAt = Date.now(); return rows; }).finally(() => { installationJob = undefined; });
30
+ return installationJob;
31
+ };
32
+ const capability = tool => { const value = (routeService ?? commander?.routeService)?.supports?.(tool); return tool === 'deepseek' ? { supported: true } : value === true ? { supported: true } : value && typeof value === 'object' ? value : { supported: false }; };
33
+ const supports = tool => capability(tool).supported === true;
34
+ const providerModels = async providerId => {
35
+ const rows = llm?.listModels ? await llm.listModels(providerId) : await providers.models(providerId);
36
+ return Promise.all(rows.filter(row => cleanId(row?.id) && !row.kinds?.length && (!row.inputModalities || row.inputModalities.includes('text'))).map(async row => {
37
+ let resolved = row;
38
+ if (llm?.resolveModelInfo) { try { resolved = { ...row, ...await llm.resolveModelInfo(providerId, row.id) }; } catch { /* Configured directory metadata remains usable. */ } }
39
+ const contextWindow = resolved.context?.contextWindow ?? resolved.contextWindow, reasoning = safeReasoning(resolved.reasoning);
40
+ return { id: row.id, name: cleanId(row.name) ? row.name : row.id, ...(positive(contextWindow) ? { context: { contextWindow } } : {}), ...(reasoning ? { reasoning } : {}) };
41
+ }));
42
+ };
43
+ const build = async () => {
44
+ const [installed, state, configured] = await Promise.all([installations(), profiles?.list?.({ includeNative: false }) ?? {}, providers?.list?.() ?? []]);
45
+ const tools = [{ id: 'deepseek', name: 'DeepSeek', installed: true, chatSupported: true, routeSupported: true }, ...installed.map(tool => ({ id: tool.id, name: tool.name || tool.id, installed: tool.installed === true, chatSupported: tool.chatSupported === true, routeSupported: tool.chatSupported === true && supports(tool.id), ...(!tool.chatSupported ? { routeReason: '此工具暂未接入对话' } : !supports(tool.id) ? { routeReason: capability(tool.id).reason || '此工具暂未支持独立供应商路由,现有本机配置保持不变' } : {}) }))];
46
+ const allowed = new Set(tools.filter(tool => tool.installed && tool.chatSupported && tool.routeSupported).map(tool => tool.id));
47
+ // Match the official provider editor: legacy providers start eligible for
48
+ // all tools. An explicitly saved empty binding remains an intentional opt-out.
49
+ const providerRows = configured.filter(provider => cleanId(provider.id) && !provider.id.startsWith('commander-')).map(provider => {
50
+ const binding = Object.hasOwn(state.bindings ?? {}, provider.id) ? state.bindings[provider.id] : [...allowed];
51
+ const subscribed = provider.authMode === 'subscription', onlyTool = subscriptionTool(provider);
52
+ const eligibility = subscribed ? onlyTool ? [onlyTool] : [] : Array.isArray(binding) ? binding : [];
53
+ return { id: provider.id, name: cleanId(provider.name) ? provider.name : provider.id, ...(provider.declared === true ? { declared: true } : {}), ...(subscribed ? { authMode: 'subscription', harnessTools: onlyTool ? [onlyTool] : [], configured: provider.configured === true, nativeRequired: true, routeReason: provider.routeReason || '请在对应 Harness 工具中使用本机订阅登录。' } : {}), tools: [...new Set(eligibility)].map(tool => tool === 'harness' ? 'deepseek' : tool).filter(tool => allowed.has(tool)) };
54
+ });
55
+ const routes = [];
56
+ await Promise.all(providerRows.map(async provider => {
57
+ if (!provider.tools.length || provider.authMode === 'subscription' && (!provider.configured || !llm?.listModels)) return;
58
+ let rows; try { rows = await providerModels(provider.id); } catch { return; }
59
+ // API adapter effort IDs (including custom wire mappings) are not native
60
+ // CLI capabilities. Native effort stays in the tool's saved quick settings.
61
+ for (const tool of provider.tools) for (const model of rows) routes.push({ tool, providerId: provider.id, providerName: provider.name, modelId: tool === 'deepseek' ? model.id : encodeRouteModelId(provider.id, model.id), nativeModelId: model.id, name: model.name, ...(provider.authMode === 'subscription' ? { authMode: 'subscription' } : {}), ...(model.context ? { context: model.context } : {}), ...(tool === 'deepseek' && model.reasoning ? { reasoning: model.reasoning } : {}) });
62
+ }));
63
+ routes.sort((a, b) => a.tool.localeCompare(b.tool) || a.providerId.localeCompare(b.providerId) || a.nativeModelId.localeCompare(b.nativeModelId));
64
+ return { tools, providers: providerRows, routes };
65
+ };
66
+ // The official catalog asks every native adapter concurrently. Share only
67
+ // in-flight work; the next read still sees current bindings and credentials.
68
+ const list = () => {
69
+ if (!catalogJob) {
70
+ const job = build().finally(() => { if (catalogJob === job) catalogJob = undefined; });
71
+ catalogJob = job;
72
+ }
73
+ return catalogJob.then(value => structuredClone(value));
74
+ };
75
+ return {
76
+ list, decodeModelId: decodeRouteModelId, invalidate() { installCache = undefined; catalogJob = undefined; },
77
+ async models(tool) {
78
+ const catalog = await list();
79
+ return catalog.routes.filter(row => row.tool === tool).map(row => ({ provider: 'commander-' + tool, id: row.modelId, name: row.name, inputModalities: ['text'], providerId: row.providerId, providerName: row.providerName, nativeModelId: row.nativeModelId, ...(row.context ? { context: structuredClone(row.context) } : {}), ...(row.reasoning ? { reasoning: structuredClone(row.reasoning) } : {}) }));
80
+ },
81
+ };
82
+ }
package/src/plugin.mjs ADDED
@@ -0,0 +1,138 @@
1
+ import { Commander } from './runtime.mjs';
2
+ import { askNativeQuestion } from './questions.mjs';
3
+ import { mountWeb, mountUnavailableWeb } from './web.mjs';
4
+ import { mountHarnessCommands } from './harness-commands.mjs';
5
+ import { mountHarnessAdapter } from './harness-adapter.mjs';
6
+ import { apply as applyVoice } from '../voice/src/plugin.mjs';
7
+ import { createModelProviders } from './model-providers.mjs';
8
+ import { mountUsage } from './usage.mjs';
9
+ import { mountTooling } from './tooling.mjs';
10
+ import { mountUpdates } from './updates.mjs';
11
+
12
+ export const name = 'commander';
13
+ export const inject = ['tools'];
14
+
15
+ const string = description => ({ type: 'string', description });
16
+ const output = { schema: { type: 'object', additionalProperties: true }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] };
17
+
18
+ export async function apply(ctx, config = {}) {
19
+ let commander;
20
+ try { commander = await new Commander(config).open(); }
21
+ catch (error) {
22
+ ctx.logger?.error?.(`BenchGo startup failed: ${error.message}`);
23
+ ctx.inject(['connection'], child => mountUnavailableWeb(child, error));
24
+ return;
25
+ }
26
+ ctx.effect(() => () => commander.close());
27
+ ctx.provide('commander', commander);
28
+ ctx.provide('commanderLinkConfig', config.link ?? {});
29
+ ctx.inject(['connection', 'settings', 'credentials', 'llm'], async child => {
30
+ const providers = createModelProviders(child);
31
+ await mountTooling(child, commander, providers);
32
+ await applyVoice(child, { ...config.voice, providers });
33
+ });
34
+ ctx.inject(['connection'], async child => { await mountUsage(child, commander, config.usage ?? {}); await mountUpdates(child, commander); });
35
+ ctx.inject(['connection', 'workspaceRegistry'], async child => { await mountWeb(child, commander, config); });
36
+ const owners = new Map();
37
+ ctx.inject(['commands'], child => mountHarnessCommands(child, commander, { bindOwner: (id, agent) => owners.set(id, agent) }));
38
+ ctx.inject(['llm', 'agents', 'workspaceRegistry'], child => { mountHarnessAdapter(child, commander, { bindOwner: (id, agent) => owners.set(id, agent) }); });
39
+ const ownerId = exec => {
40
+ const id = exec.agent?.session?.id;
41
+ if (!id) throw new Error('BenchGo tools require a Harness Agent session');
42
+ return String(id);
43
+ };
44
+ const owned = (id, exec) => {
45
+ const session = commander.snapshot(id);
46
+ if (session.ownerSessionId !== ownerId(exec)) throw new Error('BenchGo session belongs to a different Harness session');
47
+ owners.set(id, exec.agent);
48
+ return session;
49
+ };
50
+
51
+ // A native permission is always routed to the real human approval seam.
52
+ // There is deliberately no commander_approve model tool.
53
+ const pendingApprovals = new Set();
54
+ const controllers = new Map();
55
+ const listener = event => {
56
+ if (event.type === 'approval.resolved') { controllers.get(event.data.id)?.abort(); return; }
57
+ if (event.type !== 'approval.requested') return;
58
+ // Direct browser sessions own their approval UI; never auto-deny them for lacking a Harness Agent.
59
+ if (commander.snapshot(event.sessionId).ownerSessionId?.startsWith('commander-web:')) return;
60
+ const task = (async () => {
61
+ const controller = new AbortController(); controllers.set(event.data.id, controller);
62
+ try {
63
+ const owner = commander.snapshot(event.sessionId).ownerSessionId;
64
+ const agent = owners.get(event.sessionId) ?? ctx.get('agents')?.get(owner);
65
+ const approval = ctx.get('approval');
66
+ let allow = false;
67
+ if (agent && approval && event.data.kind === 'permission') {
68
+ const result = await approval.request({
69
+ agent, toolName: `commander:${event.data.method}`, signal: controller.signal,
70
+ reason: `Native ${commander.snapshot(event.sessionId).provider} request:\n${JSON.stringify(event.data.details)}`,
71
+ });
72
+ allow = result === 'allowed-once';
73
+ }
74
+ let answer = { allow };
75
+ const questions = ctx.get('userQuestions');
76
+ if (agent && questions && event.data.kind === 'question') {
77
+ answer = await askNativeQuestion(commander.snapshot(event.sessionId).provider, event.data, request => questions.ask({ ...request, agent, signal: controller.signal }));
78
+ }
79
+ commander.approve(event.sessionId, event.data.id, answer);
80
+ } catch {
81
+ // The native timeout/interrupt may already have resolved this request.
82
+ try { commander.approve(event.sessionId, event.data.id, { allow: false }); } catch {}
83
+ } finally { controllers.delete(event.data.id); }
84
+ })();
85
+ pendingApprovals.add(task); task.finally(() => pendingApprovals.delete(task));
86
+ };
87
+ commander.on('event', listener);
88
+ ctx.effect(() => async () => {
89
+ commander.off('event', listener);
90
+ for (const controller of controllers.values()) controller.abort();
91
+ await Promise.allSettled(pendingApprovals);
92
+ });
93
+
94
+ const register = (name, description, properties, required, execute) => ctx.tools.register({
95
+ name, description,
96
+ parameters: { type: 'object', properties, required, additionalProperties: false },
97
+ output,
98
+ async execute(args, exec) { exec.signal.throwIfAborted(); return execute(args, exec); },
99
+ });
100
+ register('commander_providers', 'List native coding-agent providers and protocol capabilities. Does not prove installation or login.', {}, [], async () => ({ providers: commander.catalog() }));
101
+ register('commander_doctor', 'Check native coding-agent executables and versions. Does not inspect credentials or prove login.', {}, [], async () => ({ providers: await commander.doctor() }));
102
+ register('commander_sessions', 'List external sessions owned by this Harness conversation.', {}, [], async (_args, exec) => ({ sessions: commander.list().filter(s => s.ownerSessionId === ownerId(exec)) }));
103
+ register('commander_create', 'Create or import a persistent native coding-agent session. Starting an external coding engine requires user approval. Use commander_send for tasks.', {
104
+ provider: { type: 'string', enum: ['codex', 'claude', 'pi', 'opencode', 'gemini', 'hermes', 'grok', 'mcode'] },
105
+ cwd: string('Absolute working directory'), model: string('Optional native model ID'), providerId: string('Optional configured DSH supplier ID; requires model and creates an isolated session, never changes the local CLI profile'),
106
+ nativeId: string('Existing native thread/session handle to resume, only after its original writer is stopped'),
107
+ parentId: string('Optional BenchGo parent session for delegation'),
108
+ }, ['provider', 'cwd'], async (args, exec) => {
109
+ const ownerSessionId = ownerId(exec);
110
+ if (args.parentId) owned(args.parentId, exec);
111
+ const approval = ctx.get('approval');
112
+ if (!approval) throw new Error('A human approval service is required to start external agents');
113
+ const grant = await approval.request({ agent: exec.agent, toolName: 'commander_create', callId: exec.callId, signal: exec.signal, reason: `Start ${args.provider} in ${args.cwd}. It uses its native credentials and tool policy; native permissions will be forwarded separately.` });
114
+ if (grant !== 'allowed-once') throw new Error('External agent launch was not approved');
115
+ exec.signal.throwIfAborted();
116
+ const { providerId, ...createArgs } = args;
117
+ if (providerId && !args.model) throw new Error('An explicit model is required for a managed supplier route');
118
+ const session = await commander.create({ ...createArgs, ...(providerId ? { route: { providerId, model: args.model } } : {}), ownerSessionId });
119
+ owners.set(session.id, exec.agent);
120
+ return session;
121
+ });
122
+ register('commander_connect', 'Reconnect a previously owned native session using its saved handle. Never recreates a missing handle.', { id: string('BenchGo session ID') }, ['id'], async ({ id }, exec) => { owned(id, exec); return commander.connect(id); });
123
+ register('commander_send', 'Submit another prompt to an idle native session. Returns an enqueue receipt; inspect events/wait for completion. Reuse requestId when retrying; never resubmit an uncertain receipt blindly.', {
124
+ id: string('BenchGo session ID'), text: string('Task or follow-up text'), requestId: string('Stable unique idempotency key for this input'),
125
+ }, ['id', 'text', 'requestId'], async ({ id, text, requestId }, exec) => { owned(id, exec); return commander.submit(id, text, { requestId }); });
126
+ register('commander_events', 'Read streamed text, native tool activity and completion events after a cursor. If truncated is true, older BenchGo events have been pruned; inspect native history.', {
127
+ id: string('BenchGo session ID'), after: { type: 'integer', minimum: 0 },
128
+ }, ['id'], async ({ id, after }, exec) => { owned(id, exec); return commander.events(id, after); });
129
+ register('commander_wait', 'Wait at most 60 seconds for a native session to become idle or fail. Task continues if this wait is cancelled or times out.', {
130
+ id: string('BenchGo session ID'), timeout: { type: 'integer', minimum: 0, maximum: 60000 },
131
+ }, ['id'], async ({ id, timeout }, exec) => { owned(id, exec); return commander.wait(id, { timeout, signal: exec.signal }); });
132
+ register('commander_interrupt', 'Interrupt an active native turn. Does not roll back completed tool side effects.', { id: string('BenchGo session ID') }, ['id'], async ({ id }, exec) => { owned(id, exec); await commander.interrupt(id); return commander.snapshot(id); });
133
+ register('commander_steer', 'Steer an active native turn only if the provider supports it.', { id: string('BenchGo session ID'), text: string('Steering text') }, ['id', 'text'], async ({ id, text }, exec) => { owned(id, exec); await commander.steer(id, text); return commander.snapshot(id); });
134
+ register('commander_close', 'Release a native session process while retaining its history and handle.', { id: string('BenchGo session ID') }, ['id'], async ({ id }, exec) => { owned(id, exec); return commander.disconnect(id); });
135
+ register('commander_history', 'Read the owned session native conversation history. Requires an active connection and provider support.', { id: string('BenchGo session ID') }, ['id'], async ({ id }, exec) => { owned(id, exec); return { history: await commander.history(id) }; });
136
+ register('commander_discover', 'Discover native session handles in the owned session working directory, when the provider supports it. Stop other writers before importing.', { id: string('BenchGo session ID') }, ['id'], async ({ id }, exec) => { owned(id, exec); return { sessions: await commander.discover(id) }; });
137
+ register('commander_archive', 'Archive an owned external session and release its process, or unarchive it without automatically running tasks.', { id: string('BenchGo session ID'), archived: { type: 'boolean' } }, ['id'], async ({ id, archived = true }, exec) => { owned(id, exec); return commander.archive(id, archived); });
138
+ }
@@ -0,0 +1,96 @@
1
+ import { homedir } from 'node:os';
2
+ import { dirname, isAbsolute, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { lstat, mkdir, mkdtemp, readdir, realpath, rename, rm, stat } from 'node:fs/promises';
5
+ import { spawn } from 'node:child_process';
6
+
7
+ export function githubRepository(value) {
8
+ if (typeof value !== 'string' || value.length > 500 || !/^https:\/\/github\.com\/[A-Za-z0-9-]+\/[A-Za-z0-9_.-]+\/?$/.test(value)) throw new Error('Use an HTTPS GitHub repository URL: https://github.com/owner/repo');
9
+ const [owner, raw] = value.slice('https://github.com/'.length).split('/');
10
+ const name = raw.replace(/\.git$/, '');
11
+ if (!name || name === '.' || name === '..' || name.startsWith('.')) throw new Error('Invalid GitHub repository name');
12
+ return { owner, name, url: `https://github.com/${owner}/${name}.git` };
13
+ }
14
+
15
+ export function profileDirectory(ctx) {
16
+ const pathService = ctx.get?.('path');
17
+ if (typeof pathService?.baseDir === 'string' && isAbsolute(pathService.baseDir)) return pathService.baseDir;
18
+ // Official app-boot sets baseUrl to the active configuration/profile folder.
19
+ const base = ctx.baseUrl ?? ctx.get?.('baseUrl');
20
+ if (typeof base === 'string' && base.startsWith('file:')) return fileURLToPath(base);
21
+ throw new Error('The active Harness profile directory is unavailable');
22
+ }
23
+
24
+ async function cloneGit(url, destination, { timeout = 5 * 60_000 } = {}) {
25
+ await new Promise((resolve, reject) => {
26
+ const child = spawn('git', ['-c', 'credential.interactive=false', 'clone', '--', url, destination], {
27
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, stdio: ['ignore', 'ignore', 'pipe'], detached: process.platform !== 'win32', windowsHide: true,
28
+ });
29
+ let tail = '', timedOut = false;
30
+ child.stderr.on('data', chunk => { tail = (tail + chunk.toString()).slice(-3000); });
31
+ const timer = setTimeout(() => {
32
+ timedOut = true;
33
+ try { if (process.platform === 'win32') child.kill('SIGKILL'); else if (child.pid) process.kill(-child.pid, 'SIGKILL'); } catch (error) { if (error.code !== 'ESRCH') reject(error); }
34
+ }, timeout);
35
+ child.once('error', error => { clearTimeout(timer); reject(error); });
36
+ child.once('close', code => {
37
+ clearTimeout(timer);
38
+ if (timedOut) reject(new Error('Git clone timed out; try again'));
39
+ else if (code !== 0) reject(new Error(`Git clone failed: ${tail.trim() || `exit ${code}`}`));
40
+ else resolve();
41
+ });
42
+ });
43
+ }
44
+
45
+ export function projectActions(ctx, { runClone = cloneGit } = {}) {
46
+ let cloning = false;
47
+ return {
48
+ async browse({ path = homedir(), name = '' } = {}) {
49
+ if (typeof path !== 'string' || !isAbsolute(path) || path.length > 4096) throw new Error('Directory path must be absolute');
50
+ if (typeof name !== 'string' || name.length > 200) throw new Error('Invalid directory name filter');
51
+ const current = await realpath(path);
52
+ if (!(await stat(current)).isDirectory()) throw new Error('Path is not a directory');
53
+ const dirents = await readdir(current, { withFileTypes: true });
54
+ const entries = [];
55
+ for (const entry of dirents) {
56
+ if (entry.name.startsWith('.') || !entry.name.toLowerCase().includes(name.toLowerCase())) continue;
57
+ const child = join(current, entry.name);
58
+ if (entry.isDirectory()) entries.push({ name: entry.name, path: child });
59
+ else if (entry.isSymbolicLink()) {
60
+ try { if ((await stat(child)).isDirectory()) entries.push({ name: entry.name, path: child }); } catch {}
61
+ }
62
+ }
63
+ entries.sort((a, b) => a.name.localeCompare(b.name));
64
+ return { path: current, parent: dirname(current) === current ? null : dirname(current), home: homedir(), entries };
65
+ },
66
+ async clone({ url } = {}) {
67
+ const repo = githubRepository(url);
68
+ if (cloning) throw new Error('Another repository is being cloned; wait for it to finish');
69
+ cloning = true;
70
+ let staging, destination, reserved = false;
71
+ try {
72
+ const root = join(profileDirectory(ctx), 'workspaces');
73
+ await mkdir(root, { recursive: true });
74
+ destination = join(root, repo.name);
75
+ // Exclusive reservation forbids overwriting any pre-existing path,
76
+ // including symlinks and concurrent clones from another process.
77
+ try { await lstat(destination); throw new Error('Repository destination already exists'); }
78
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
79
+ try { await mkdir(destination); reserved = true; }
80
+ catch (error) { if (error.code === 'EEXIST') throw new Error('Repository destination already exists'); throw error; }
81
+ staging = await mkdtemp(join(root, '.clone-'));
82
+ const checkout = join(staging, 'repository');
83
+ await runClone(repo.url, checkout);
84
+ await rename(checkout, destination);
85
+ const p = await ctx.workspaceRegistry.create(await realpath(destination));
86
+ return { project: { id: p.id, path: p.path, title: p.title } };
87
+ } catch (error) {
88
+ if (reserved) await rm(destination, { recursive: true, force: true });
89
+ throw error;
90
+ } finally {
91
+ if (staging) await rm(staging, { recursive: true, force: true });
92
+ cloning = false;
93
+ }
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,11 @@
1
+ /** Authentication evidence is supplied by the official provider credential seam. */
2
+ const subscriptionTools = new Map([['openai-codex', 'codex'], ['anthropic', 'claude'], ['google-gemini-cli', 'gemini']]);
3
+ export const subscriptionHarnessTool = id => subscriptionTools.get(id);
4
+ export function subscriptionTool(provider) {
5
+ return provider?.authMode === 'subscription' && Array.isArray(provider.harnessTools) && provider.harnessTools.length === 1 ? provider.harnessTools[0] : undefined;
6
+ }
7
+ export function assertProviderToolAccess(provider, tool) {
8
+ if (!provider || provider.authMode !== 'subscription') return;
9
+ const requested = tool === 'deepseek' ? 'harness' : tool;
10
+ if (!provider.harnessTools?.includes(requested)) throw Object.assign(new Error('此订阅登录仅允许使用对应的 Harness 工具,不能转用于其他工具。'), { status: 409, code: 'SUBSCRIPTION_TOOL_MISMATCH' });
11
+ }