@harness-mix/cli 0.2.2 → 0.2.4
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.
- package/CHANGELOG.md +25 -0
- package/README.md +469 -467
- package/output/native-build/desktop-controller.mjs +1 -1
- package/output/native-build/renderer-extension.js +23 -4
- package/package.json +16 -9
- package/scripts/antigravity-adapter-test.cjs +647 -626
- package/scripts/codex-adapter-test.cjs +162 -127
- package/scripts/collaboration-test.cjs +274 -262
- package/scripts/core-review-test.cjs +68 -0
- package/scripts/delegation-await-test.cjs +76 -0
- package/scripts/jsonl-stdin-test.cjs +40 -0
- package/scripts/kiro-cursor-adapters-test.cjs +124 -100
- package/scripts/native-acp-depth-test.cjs +30 -5
- package/scripts/native-protocol-test.cjs +14 -1
- package/scripts/native-update-apply-test.cjs +269 -215
- package/scripts/native-update.cjs +78 -0
- package/scripts/native-vendor-adapters-test.cjs +196 -154
- package/scripts/salvage-rollout-writes.cjs +72 -0
- package/scripts/send-cancel-race-test.cjs +80 -0
- package/scripts/send-pre-turn-cancel-test.cjs +100 -0
- package/scripts/stuck-turn-test.cjs +6 -1
- package/scripts/zcode-adapter-test.cjs +329 -0
- package/scripts/zcode-live-probe.cjs +66 -0
- package/src/main/adapters/antigravity.js +1428 -1415
- package/src/main/adapters/codex.js +656 -649
- package/src/main/adapters/native-acp-command.js +51 -48
- package/src/main/adapters/native-acp.js +47 -12
- package/src/main/adapters/qoder.js +12 -8
- package/src/main/adapters/zcode.js +921 -10
- package/src/main/harness-adapter/event-normalizer.js +5 -2
- package/src/main/host/collaboration.js +723 -715
- package/src/main/host/jsonl.js +130 -116
- package/src/main/host/runtime.js +30 -14
- package/src/main/native/config.js +9 -9
- package/src/main/native/host.js +2 -0
- package/src/main/native/launcher.js +252 -237
- package/src/main/native/process-utils.js +157 -57
- package/src/main/native/protocol.js +1221 -1177
- package/src/main/native/secure-store.js +2 -0
- package/src/main/native/update-state.js +123 -110
- package/src/main/native/updater.js +460 -394
- package/src/main/workspace/core-review.js +13 -5
- package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
|
@@ -1,48 +1,51 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const { cliSpawn } = require('../host/jsonl');
|
|
5
|
-
|
|
6
|
-
// Resolve only the named native CLI. In particular, `agent` can belong to Grok.
|
|
7
|
-
function nativeCommand(id, argv) {
|
|
8
|
-
const cursor = id === 'cursor-cli';
|
|
9
|
-
const buddy = id === 'codebuddy';
|
|
10
|
-
const specs = {
|
|
11
|
-
codebuddy: ['codebuddy', 'HARNESS_MIX_CODEBUDDY_EXECUTABLE', 'https://www.codebuddy.ai/docs/cli/overview'],
|
|
12
|
-
'cursor-cli': ['cursor-agent', 'HARNESS_MIX_CURSOR_EXECUTABLE', 'https://cursor.com/docs/cli/installation'],
|
|
13
|
-
'kiro-cli': ['kiro-cli', 'HARNESS_MIX_KIRO_EXECUTABLE', 'https://kiro.dev/docs/cli/'],
|
|
14
|
-
qoder: ['qodercli', 'HARNESS_MIX_QODER_EXECUTABLE', 'https://docs.qoder.com/cli/acp'],
|
|
15
|
-
trae: ['traecli', 'HARNESS_MIX_TRAE_EXECUTABLE', 'https://github.com/bytedance/trae-agent'],
|
|
16
|
-
cline: ['cline', 'HARNESS_MIX_CLINE_EXECUTABLE', 'https://docs.cline.bot/usage/cli-overview'],
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const { cliSpawn } = require('../host/jsonl');
|
|
5
|
+
|
|
6
|
+
// Resolve only the named native CLI. In particular, `agent` can belong to Grok.
|
|
7
|
+
function nativeCommand(id, argv) {
|
|
8
|
+
const cursor = id === 'cursor-cli';
|
|
9
|
+
const buddy = id === 'codebuddy';
|
|
10
|
+
const specs = {
|
|
11
|
+
codebuddy: ['codebuddy', 'HARNESS_MIX_CODEBUDDY_EXECUTABLE', 'https://www.codebuddy.ai/docs/cli/overview'],
|
|
12
|
+
'cursor-cli': ['cursor-agent', 'HARNESS_MIX_CURSOR_EXECUTABLE', 'https://cursor.com/docs/cli/installation'],
|
|
13
|
+
'kiro-cli': ['kiro-cli', 'HARNESS_MIX_KIRO_EXECUTABLE', 'https://kiro.dev/docs/cli/'],
|
|
14
|
+
qoder: ['qodercli', 'HARNESS_MIX_QODER_EXECUTABLE', 'https://docs.qoder.com/cli/acp'],
|
|
15
|
+
trae: ['traecli', 'HARNESS_MIX_TRAE_EXECUTABLE', 'https://github.com/bytedance/trae-agent'],
|
|
16
|
+
cline: ['cline', 'HARNESS_MIX_CLINE_EXECUTABLE', 'https://docs.cline.bot/usage/cli-overview'],
|
|
17
|
+
};
|
|
18
|
+
if (!specs[id]) throw new Error(`Unknown native CLI: ${id}`);
|
|
19
|
+
const [bin, variable, url] = specs[id];
|
|
20
|
+
const override = process.env[variable] || (buddy ? process.env.HARNESS_MIX_WORKBUDDY_EXECUTABLE : undefined);
|
|
21
|
+
if (id === 'trae' && !override) throw new Error('Trae 官方仓库未确认 ACP 服务入口;请设置已验证的 HARNESS_MIX_TRAE_EXECUTABLE,不会猜测 traecli acp serve');
|
|
22
|
+
const roots = cursor
|
|
23
|
+
? [path.join(process.env.LOCALAPPDATA || os.homedir(), 'cursor-agent'), path.join(os.homedir(), '.local', 'bin')]
|
|
24
|
+
: [path.join(process.env.LOCALAPPDATA || os.homedir(), 'Kiro-Cli'), path.join(os.homedir(), '.kiro', 'bin'), path.join(os.homedir(), '.local', 'bin')];
|
|
25
|
+
const dirs = [...roots, path.join(process.env.APPDATA || os.homedir(), 'npm'), ...(process.env.PATH || '').split(path.delimiter).filter(Boolean)];
|
|
26
|
+
const suffixes = process.platform === 'win32' ? ['.exe', '.cmd', '.ps1', ''] : [''];
|
|
27
|
+
const bundled = buddy ? [path.join(process.env.ProgramFiles || 'C:/Program Files', 'WorkBuddy/resources/app.asar.unpacked/cli/bin/codebuddy'), path.join(process.env.LOCALAPPDATA || os.homedir(), 'Programs/WorkBuddy/resources/app.asar.unpacked/cli/bin/codebuddy')] : [];
|
|
28
|
+
// Only the headless qodercli speaks ACP over stdio. The bare `qoder` on PATH
|
|
29
|
+
// is the IDE launcher: it passes the --version availability probe, but every
|
|
30
|
+
// --acp session open becomes a full GUI launch that never completes the ACP
|
|
31
|
+
// handshake. A working custom entry can still be set via
|
|
32
|
+
// HARNESS_MIX_QODER_EXECUTABLE.
|
|
33
|
+
const bins = [bin];
|
|
34
|
+
const found = override || [...dirs.flatMap(dir => bins.flatMap(name => suffixes.map(ext => path.join(dir, name + ext)))), ...bundled].find(file => fs.existsSync(file) && fs.statSync(file).isFile());
|
|
35
|
+
if (!found || !fs.existsSync(found)) throw new Error(`${bin} 未安装;请从 ${url} 安装并原生登录`);
|
|
36
|
+
if ((buddy && bundled.includes(found)) || /\.(cjs|mjs|js)$/i.test(found)) return { command: process.execPath, args: [found, ...argv] };
|
|
37
|
+
if (id === 'qoder' && /\.(cmd|ps1)$/i.test(found)) {
|
|
38
|
+
const bundle = path.join(path.dirname(found), 'node_modules/@qoder-ai/qodercli/bundle/qodercli.js');
|
|
39
|
+
if (fs.existsSync(bundle)) return { command: process.execPath, args: [bundle, ...argv] };
|
|
40
|
+
}
|
|
41
|
+
if (cursor && /\.(cmd|ps1)$/i.test(found)) {
|
|
42
|
+
const versions = path.join(path.dirname(found), 'versions');
|
|
43
|
+
const version = fs.existsSync(versions) && fs.readdirSync(versions).filter(v => /^\d{4}\.\d{2}\.\d{2}-[a-z0-9-]+$/i.test(v)).sort().reverse().find(v => ['node.exe', 'index.js'].every(f => fs.existsSync(path.join(versions, v, f))));
|
|
44
|
+
if (!version) throw new Error('Cursor launcher 缺少原生 bundle;请重新安装 Cursor CLI');
|
|
45
|
+
return { command: path.join(versions, version, 'node.exe'), args: [path.join(versions, version, 'index.js'), ...argv] };
|
|
46
|
+
}
|
|
47
|
+
if (/\.(cmd|bat)$/i.test(found)) return cliSpawn(found.replace(/\.(cmd|bat)$/i, ''), argv);
|
|
48
|
+
if (/\.ps1$/i.test(found)) return { command: 'pwsh.exe', args: ['-NoLogo', '-NoProfile', '-File', found, ...argv] };
|
|
49
|
+
return { command: found, args: argv };
|
|
50
|
+
}
|
|
51
|
+
module.exports = { nativeCommand };
|
|
@@ -27,7 +27,8 @@ function extractToolOutput(tool, update) {
|
|
|
27
27
|
const options = list => (list || []).flatMap(o => Array.isArray(o.options) ? options(o.options) : typeof o.value === 'string' ? [o] : []);
|
|
28
28
|
const finite = n => Number.isFinite(n) && n >= 0;
|
|
29
29
|
function config(s, kind) {
|
|
30
|
-
|
|
30
|
+
// qoder names its effort selector `reasoning_effort` and files it under the model category.
|
|
31
|
+
const ids = { model: ['model'], thinking: ['effortLevel', 'thought_level', 'reasoning_effort'], mode: ['mode'] }[kind];
|
|
31
32
|
return s.state.configOptions.find(o => ids.includes(o.id) || o.category === (kind === 'thinking' ? 'thought_level' : kind));
|
|
32
33
|
}
|
|
33
34
|
function catalog(s) {
|
|
@@ -237,12 +238,12 @@ function nativeAcp({ id, name, args, aliases = [], command = argv => nativeComma
|
|
|
237
238
|
async describe() { return { models: null, thinkingLevels: [], permissionModes: [] }; },
|
|
238
239
|
async describeFor(s) { return catalog(s); },
|
|
239
240
|
async listModelsFor(s) { return catalog(s).models; },
|
|
240
|
-
async open({ thread, emit, diagnostic = () => {}, collaboration, managedMcp = [] }) {
|
|
241
|
+
async open({ thread, emit, diagnostic = () => {}, collaboration, managedMcp = [] }) {
|
|
241
242
|
const s = { vendor: id, cwd: thread.cwd, nativeSessionId: thread.restore ? thread.nativeSessionId : null,
|
|
242
243
|
generation: 0, loading: true, active: false, closed: false, fault: null, emit, diagnostic,
|
|
243
244
|
environment: thread.environment || {}, tools: new Map(), charges: new Map(), confirmed: {},
|
|
244
245
|
state: { configOptions: [], models: null, modes: null, commands: [], usage: {} },
|
|
245
|
-
mcpServers: require('./managed-mcp').acpServers(managedMcp, collaboration),
|
|
246
|
+
mcpServers: require('./managed-mcp').acpServers(managedMcp, collaboration),
|
|
246
247
|
collaborationEnabled: !!collaboration,
|
|
247
248
|
};
|
|
248
249
|
s.interactions = new AcpInteractions(s, id);
|
|
@@ -256,7 +257,28 @@ function nativeAcp({ id, name, args, aliases = [], command = argv => nativeComma
|
|
|
256
257
|
} catch (error) { await adapter.close(s); throw error; }
|
|
257
258
|
},
|
|
258
259
|
async send(s, prompt, hooks, attachments) {
|
|
259
|
-
if (s.active || s.configuring
|
|
260
|
+
if (s.active || s.configuring) throw s.fault || new Error('Native session is busy or closed');
|
|
261
|
+
// A native CLI that died between turns (crash-on-exit bugs, idle
|
|
262
|
+
// reaping) used to poison the thread until a desktop restart: the
|
|
263
|
+
// session stayed cached with its fault and every later send rethrew
|
|
264
|
+
// it. Restore the native session once instead, replaying the
|
|
265
|
+
// confirmed model/thinking/permission selections.
|
|
266
|
+
if (s.fault || s.closed) {
|
|
267
|
+
const confirmed = { ...s.confirmed };
|
|
268
|
+
try {
|
|
269
|
+
++s.generation;
|
|
270
|
+
s.fault = null; s.closed = false;
|
|
271
|
+
s.interactions.close();
|
|
272
|
+
s.diagnostic?.(`${name}: native session disconnected between turns, restoring ${s.nativeSessionId}`);
|
|
273
|
+
await connect(s, true);
|
|
274
|
+
if (confirmed.model) await adapter.setModel(s, confirmed.model).catch(error => s.diagnostic?.(`${name}: model restore after reconnect failed: ${error.message}`));
|
|
275
|
+
if (confirmed.thinking) await adapter.setThinkingLevel(s, confirmed.thinking).catch(error => s.diagnostic?.(`${name}: thinking restore after reconnect failed: ${error.message}`));
|
|
276
|
+
if (confirmed.mode) await adapter.setPermissionMode(s, confirmed.mode).catch(error => s.diagnostic?.(`${name}: permission mode restore after reconnect failed: ${error.message}`));
|
|
277
|
+
} catch (error) {
|
|
278
|
+
s.fault = s.fault || error;
|
|
279
|
+
throw s.fault;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
260
282
|
if (attachments?.images?.length && (!manifest.capabilities.attachments || !s.state.agentCapabilities.promptCapabilities?.image)) throw new Error(`${name} native image input is not supported`);
|
|
261
283
|
s.active = true; s.tools.clear(); s.cancelRequested = false; s.turnAnswer = ''; s.turnProgressSeen = false; s.turnTextSeen = false;
|
|
262
284
|
s.turnSettling = false; s.turnDrainWake = null;
|
|
@@ -385,7 +407,19 @@ function nativeAcp({ id, name, args, aliases = [], command = argv => nativeComma
|
|
|
385
407
|
} else throw new Error(`Native ${kind} option unavailable`);
|
|
386
408
|
} finally { s.configuring = false; }
|
|
387
409
|
},
|
|
388
|
-
async setModel(s, model) {
|
|
410
|
+
async setModel(s, model) {
|
|
411
|
+
const previousThinking = s.confirmed.thinking;
|
|
412
|
+
await adapter.configure(s, 'model', model.id); s.model = catalog(s).models.find(m => m.id === model.id); s.confirmed.model = s.model;
|
|
413
|
+
// Some natives reset the effort selector on model switches (qodercli
|
|
414
|
+
// Max→Flash resets reasoning_effort to its default). Replay the
|
|
415
|
+
// confirmed level while the new model still offers it, so the UI
|
|
416
|
+
// selection and the native session stay in sync.
|
|
417
|
+
if (previousThinking && catalog(s).thinkingLevels.some(o => o.id === previousThinking)) {
|
|
418
|
+
try { await adapter.configure(s, 'thinking', previousThinking); s.confirmed.thinking = previousThinking; }
|
|
419
|
+
catch (error) { delete s.confirmed.thinking; s.diagnostic?.(`${name}: thinking level not restored after model switch: ${error.message}`); }
|
|
420
|
+
} else delete s.confirmed.thinking;
|
|
421
|
+
return s.model;
|
|
422
|
+
},
|
|
389
423
|
async setThinkingLevel(s, level) {
|
|
390
424
|
if (!catalog(s).thinkingLevels.some(o => o.id === level)) throw new Error('Native effort unavailable for this model');
|
|
391
425
|
await adapter.configure(s, 'thinking', level); s.confirmed.thinking = level;
|
|
@@ -415,13 +449,14 @@ function nativeAcp({ id, name, args, aliases = [], command = argv => nativeComma
|
|
|
415
449
|
} else await adapter.send(s, '/' + name, hooks);
|
|
416
450
|
},
|
|
417
451
|
async fork(source, context) {
|
|
418
|
-
if (!
|
|
419
|
-
|
|
452
|
+
if (!manifest.capabilities.fork || context.message) throw new Error('Native fork boundary unsupported');
|
|
453
|
+
// Kiro forks at a message checkpoint; Qoder forks at the session head with the standard params.
|
|
454
|
+
const checkpoint = kiro ? await require('./kiro-history').latestKiroCheckpoint(source.cwd, source.nativeSessionId) : null;
|
|
420
455
|
const probe = await adapter.open({ thread: { ...source, restore: true }, emit: () => {}, diagnostic: context.diagnostic });
|
|
421
456
|
try {
|
|
422
|
-
if (!probe.state.agentCapabilities.sessionCapabilities?.fork) throw new Error(
|
|
423
|
-
const result = await probe.request('session/fork', { sessionId: source.nativeSessionId, cwd: source.cwd, _meta: { kiro: { messageId: checkpoint } } });
|
|
424
|
-
if (!result.sessionId || result.sessionId === source.nativeSessionId) throw new Error(
|
|
457
|
+
if (!probe.state.agentCapabilities.sessionCapabilities?.fork) throw new Error(`${name} did not advertise session/fork`);
|
|
458
|
+
const result = await probe.request('session/fork', { sessionId: source.nativeSessionId, cwd: source.cwd, ...(kiro ? { _meta: { kiro: { messageId: checkpoint } } } : {}) });
|
|
459
|
+
if (!result.sessionId || result.sessionId === source.nativeSessionId) throw new Error(`${name} fork did not return a new native session`);
|
|
425
460
|
return { session: await adapter.open({ thread: { ...source, nativeSessionId: result.sessionId, restore: true }, emit: context.emit, diagnostic: context.diagnostic }) };
|
|
426
461
|
} finally { await adapter.close(probe); }
|
|
427
462
|
},
|
|
@@ -429,7 +464,7 @@ function nativeAcp({ id, name, args, aliases = [], command = argv => nativeComma
|
|
|
429
464
|
};
|
|
430
465
|
return adapter;
|
|
431
466
|
}
|
|
432
|
-
manifest.integrations = { mcp: true };
|
|
433
|
-
return { manifest, create };
|
|
467
|
+
manifest.integrations = { mcp: true };
|
|
468
|
+
return { manifest, create };
|
|
434
469
|
}
|
|
435
470
|
module.exports = { nativeAcp, catalog, project };
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
const { nativeAcp } = require('./native-acp');
|
|
2
|
-
|
|
3
|
-
// Official CLI ACP; native account and tools remain in Qoder.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
module.exports
|
|
1
|
+
const { nativeAcp } = require('./native-acp');
|
|
2
|
+
|
|
3
|
+
// Official CLI ACP; native account and tools remain in Qoder.
|
|
4
|
+
// Capabilities measured against qodercli 1.1.58: the effort selector is the
|
|
5
|
+
// `reasoning_effort` config option (xhigh/low/medium/none); session/fork takes
|
|
6
|
+
// the standard Zed params; usage_update exists only as an input schema, the CLI
|
|
7
|
+
// never emits it, so usage displays would be fabricated.
|
|
8
|
+
module.exports = nativeAcp({
|
|
9
|
+
id: 'qoder', name: 'Qoder', args: ['--acp'],
|
|
10
|
+
capabilities: { questions: false, thinkingLevels: true, usage: false, contextUsage: false, attachments: true, fork: true, compaction: false },
|
|
11
|
+
});
|
|
12
|
+
module.exports.manifest.integrations.skills = { global: ['.qoder/skills'], project: ['.qoder/skills'] };
|