@harness-mix/cli 0.1.4 → 0.1.5
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 +8 -0
- package/README.md +26 -96
- package/docs/chatgpt-web-sidebar.md +7 -1
- package/docs/cross-platform.md +15 -0
- package/docs/harness-handoff-design.md +377 -0
- package/docs/harness-management.md +28 -1
- package/docs/native-codex.md +21 -10
- package/output/native-build/harness-mix-shim.exe +0 -0
- package/output/native-build/renderer-extension.js +972 -35
- package/package.json +18 -9
- package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
- package/scripts/codex-accounts-test.cjs +81 -0
- package/scripts/codex-accounts-ui-smoke.cjs +70 -0
- package/scripts/e2e-integrations.cjs +52 -0
- package/scripts/e2e-native-host.cjs +39 -0
- package/scripts/handoff-checkpoint-test.cjs +83 -0
- package/scripts/install-native.cjs +4 -1
- package/scripts/integrations-test.cjs +64 -0
- package/scripts/integrations-ui-smoke.cjs +63 -0
- package/scripts/native-protocol-test.cjs +78 -4
- package/scripts/native-secret-test.cjs +2 -2
- package/scripts/native-shim-test.cjs +20 -4
- package/scripts/native-ui-smoke.cjs +58 -2
- package/scripts/publish-native-package.cjs +8 -5
- package/scripts/support/integrations-mcp-fixture.cjs +15 -0
- package/scripts/switch-harness-test.cjs +27 -1
- package/src/main/adapters/acp.js +3 -3
- package/src/main/adapters/antigravity.js +10 -13
- package/src/main/adapters/claude.js +10 -3
- package/src/main/adapters/codex-app-server.js +20 -13
- package/src/main/adapters/codex.js +6 -4
- package/src/main/adapters/dsh.js +2 -1
- package/src/main/adapters/grok.js +4 -3
- package/src/main/adapters/managed-mcp.js +12 -0
- package/src/main/adapters/native-acp.js +4 -4
- package/src/main/adapters/opencode.js +9 -3
- package/src/main/adapters/pi.js +1 -0
- package/src/main/harness-adapter/manifest.js +17 -2
- package/src/main/host/handoff-access.js +53 -0
- package/src/main/host/handoff-checkpoints.js +206 -0
- package/src/main/host/handoff-mcp.cjs +38 -0
- package/src/main/host/handoff-tools.js +13 -0
- package/src/main/host/handoff.js +28 -6
- package/src/main/host/integrations.js +201 -0
- package/src/main/host/runtime.js +75 -35
- package/src/main/host/store.js +4 -4
- package/src/main/native/codex-accounts.js +260 -0
- package/src/main/native/host.js +1 -1
- package/src/main/native/launcher.js +2 -2
- package/src/main/native/protocol.js +74 -23
- package/src/main/native/redact.js +1 -1
- package/src/main/native/rs/crates/shim/src/main.rs +155 -12
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +80 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -1
- package/src/native-ui/renderer-extension/src/index.ts +28 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
- package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +159 -40
- package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +83 -0
- package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
- package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +26 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
- package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
- package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +102 -0
- package/src/native-ui/renderer-extension/src/settings/localization.ts +30 -13
- package/src/native-ui/renderer-extension/src/settings/pages.ts +47 -18
- package/src/native-ui/renderer-extension/src/settings/shell.css +13 -0
- package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
- package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +18 -0
- package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
- package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
- package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
- package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.js +1 -1
- package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
- package/src/native-ui/shared-contracts/src/index.ts +8 -1
- package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
|
@@ -21,9 +21,23 @@ async function main() {
|
|
|
21
21
|
runtime.adapters.set('pi', adapter); runtime.status.pi = { available: true };
|
|
22
22
|
const events = [];
|
|
23
23
|
const section = { id: 'test-pinned-section', name: 'Pinned', appearance: null };
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const officialRequests = [];
|
|
25
|
+
const bridge = new NativeProtocol(runtime, event => events.push(event), async (method, params) => {
|
|
26
|
+
officialRequests.push({ method, params });
|
|
27
|
+
if (method === 'threadSection/list') return { data: [section], nextCursor: null };
|
|
28
|
+
if (method === 'account/read') return { account: { type: 'chatgpt', email: 'native@example.com', planType: 'plus' }, requiresOpenaiAuth: true };
|
|
29
|
+
if (method === 'account/rateLimits/read') return {
|
|
30
|
+
rateLimits: {
|
|
31
|
+
primary: { usedPercent: 33, windowDurationMins: 300, resetsAt: 1_800_000_000 },
|
|
32
|
+
secondary: { usedPercent: 29, windowDurationMins: 10080, resetsAt: 1_800_604_800 },
|
|
33
|
+
},
|
|
34
|
+
rateLimitResetCredits: { availableCount: 0, credits: [] },
|
|
35
|
+
};
|
|
36
|
+
if (method === 'account/login/start') return { type: 'chatgptDeviceCode', loginId: 'native-login', verificationUrl: 'https://auth.example.com/device', userCode: 'ABCD-EFGH' };
|
|
37
|
+
if (method === 'account/login/cancel') return { status: 'canceled' };
|
|
38
|
+
if (method === 'account/logout') return {};
|
|
39
|
+
if (method === 'account/rateLimitResetCredit/consume') return { outcome: 'reset' };
|
|
40
|
+
throw new Error(`Unexpected official request: ${method}`);
|
|
27
41
|
});
|
|
28
42
|
try {
|
|
29
43
|
const catalogModels = [{ id: 'shared', provider: 'a' }, { id: 'shared', provider: 'b' }, { id: 'unique', provider: 'a' }];
|
|
@@ -51,6 +65,48 @@ async function main() {
|
|
|
51
65
|
const schemaPath = path.join(root, 'schemas.cjs');
|
|
52
66
|
await esbuild.build({ entryPoints: ['src/native-ui/shared-contracts/src/index.ts'], outfile: schemaPath, bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent' });
|
|
53
67
|
const schemas = require(schemaPath);
|
|
68
|
+
const version = await bridge.request('harness-mix/runtime/version');
|
|
69
|
+
assert.equal(version.version, require('../package.json').version, 'About page reads the installed package version');
|
|
70
|
+
const accounts = await bridge.request('codexhost/account/list');
|
|
71
|
+
schemas.codexAccountListResultSchema.parse(accounts);
|
|
72
|
+
assert.equal(accounts.accounts[0].email, 'native@example.com', 'Official account/read is projected into Account management');
|
|
73
|
+
assert.equal(accounts.accounts[0].authenticated, true);
|
|
74
|
+
assert.equal(accounts.accounts[0].management, 'native');
|
|
75
|
+
const accountUsage = await bridge.request('codexhost/account/usage/inspect', { accountId: 'official-codex' });
|
|
76
|
+
schemas.codexAccountUsageResultSchema.parse(accountUsage);
|
|
77
|
+
assert.equal(accountUsage.usage.planFiveHourUsedPercent, 33);
|
|
78
|
+
assert.equal(accountUsage.usage.planSevenDayUsedPercent, 29);
|
|
79
|
+
assert.equal(accountUsage.accountCredits.periodType, 'five_hour');
|
|
80
|
+
assert.equal(accountUsage.accountCredits.productUsage[0].product, '7-day window');
|
|
81
|
+
const login = await bridge.request('codexhost/account/login/start', { accountId: 'official-codex' });
|
|
82
|
+
schemas.codexAccountLoginStartResultSchema.parse(login);
|
|
83
|
+
assert.equal(login.userCode, 'ABCD-EFGH');
|
|
84
|
+
assert.deepEqual(await bridge.request('codexhost/account/login/cancel', { accountId: 'official-codex', loginId: 'native-login' }), { cancelled: true });
|
|
85
|
+
const loggedOut = await bridge.request('codexhost/account/logout');
|
|
86
|
+
schemas.codexAccountMutationResultSchema.parse(loggedOut);
|
|
87
|
+
assert.equal(loggedOut.account.authenticated, false);
|
|
88
|
+
let openedCodexThread = null;
|
|
89
|
+
const codexAdapter = {
|
|
90
|
+
manifest: { id: 'codex', name: 'Codex', capabilities: { streaming: true, models: true, approvals: true, questions: true, resume: true } },
|
|
91
|
+
async describe() { return { models: [{ id: 'gpt-test', name: 'GPT Test', provider: 'openai' }], thinkingLevels: [], permissionModes: [] }; },
|
|
92
|
+
async open(input) { openedCodexThread = input.thread; return { nativeSessionId: 'isolated-native-session', model: input.thread.options.model }; },
|
|
93
|
+
async send() {}, async cancel() {}, async close() {},
|
|
94
|
+
};
|
|
95
|
+
runtime.adapters.set('codex', codexAdapter);
|
|
96
|
+
runtime.status.codex = { available: true };
|
|
97
|
+
bridge.codexAccounts.close();
|
|
98
|
+
bridge.codexAccounts = {
|
|
99
|
+
executionContext(accountId) {
|
|
100
|
+
assert.equal(accountId, 'account-work');
|
|
101
|
+
return { accountId, codexHome: path.join(root, 'isolated-codex-home') };
|
|
102
|
+
},
|
|
103
|
+
close() {},
|
|
104
|
+
};
|
|
105
|
+
const isolated = await bridge.request('thread/start', { cwd: root, model: 'gpt-test', __codexhostAccountId: 'account-work' });
|
|
106
|
+
assert.equal(decodeRoute(isolated.thread.model).harnessId, 'codex-harness', 'An isolated Codex Account becomes an owned Codex adapter Thread');
|
|
107
|
+
assert.equal(openedCodexThread.options.accountId, 'account-work');
|
|
108
|
+
assert.equal(openedCodexThread.options.codexHome, path.join(root, 'isolated-codex-home'));
|
|
109
|
+
assert.equal(openedCodexThread.options.model.id, 'gpt-test');
|
|
54
110
|
const inspection = await bridge.inspect('pi');
|
|
55
111
|
schemas.harnessInspectionSchema.parse(inspection);
|
|
56
112
|
assert.equal(inspection.catalog.defaultThinkingOptionId, 'high', 'Declared default thinking level projects to the catalog');
|
|
@@ -142,7 +198,25 @@ async function main() {
|
|
|
142
198
|
assert.ok(runtime.execution.isRunning(threadId) && runtime.threads.find(t => t.id === threadId).currentTurn.id === current, 'Stale steering never cancels the running Turn');
|
|
143
199
|
emit({ kind: 'completed', finalAnswer: true });
|
|
144
200
|
await wait(() => !runtime.execution.isRunning(threadId) && !runtime.threads.find(t => t.id === threadId).reviewPending && !runtime.sending.has(threadId));
|
|
145
|
-
assert.equal(await bridge.request('thread/start', { model: 'official-model' }), undefined, 'Official Codex
|
|
201
|
+
assert.equal(await bridge.request('thread/start', { model: 'official-model' }), undefined, 'Official Codex thread/start passes through');
|
|
202
|
+
for (const [method, params] of [
|
|
203
|
+
['thread/read', { threadId: 'official-thread', includeTurns: true }],
|
|
204
|
+
['thread/resume', { threadId: 'official-thread' }],
|
|
205
|
+
['turn/start', { threadId: 'official-thread', input: [{ type: 'text', text: 'continue' }] }],
|
|
206
|
+
['turn/interrupt', { threadId: 'official-thread', turnId: 'official-turn' }],
|
|
207
|
+
['thread/fork', { threadId: 'official-thread', lastTurnId: 'official-turn' }],
|
|
208
|
+
['thread/compact/start', { threadId: 'official-thread' }],
|
|
209
|
+
]) {
|
|
210
|
+
assert.equal(await bridge.request(method, params), undefined, `Official Codex ${method} passes through`);
|
|
211
|
+
}
|
|
212
|
+
assert.deepEqual(await bridge.request('codexhost/thread/ownership/list', { threadIds: ['official-thread', threadId] }), {
|
|
213
|
+
threads: [
|
|
214
|
+
{ threadId: 'official-thread', owner: 'codex' },
|
|
215
|
+
{ threadId, owner: 'external', harnessId: 'pi' },
|
|
216
|
+
],
|
|
217
|
+
}, 'Official and Harness-managed threads keep separate ownership');
|
|
218
|
+
const runtimeInspection = await bridge.request('harness-mix/runtime/inspect');
|
|
219
|
+
assert.deepEqual(runtimeInspection.codex, { mode: 'official-passthrough', managedRoute: 'codex-harness' });
|
|
146
220
|
adapter.listCommands = async () => [{ id: 'compact', action: 'execute', label: 'Compact' }];
|
|
147
221
|
let compactCalls = 0;
|
|
148
222
|
adapter.executeCommand = async (_session, id, hooks) => {
|
|
@@ -20,8 +20,8 @@ const helper = [
|
|
|
20
20
|
assert.equal(redactText('ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), '[redacted]');
|
|
21
21
|
assert.equal(redactText('skeleton key'), 'skeleton key'); // no false positive on prose
|
|
22
22
|
assert.deepEqual(
|
|
23
|
-
redact({ token: 'x', name: 'ok', nested: { api_key: 'y', list: ['Bearer zzzzzzzzzzzz'] } }),
|
|
24
|
-
{ token: '[redacted]', name: 'ok', nested: { api_key: '[redacted]', list: ['[redacted]'] } },
|
|
23
|
+
redact({ token: 'x', name: 'ok', userCode: 'ABCD-EFGH', loginId: 'login-1', verificationUrl: 'https://auth.example/device', nested: { api_key: 'y', list: ['Bearer zzzzzzzzzzzz'] } }),
|
|
24
|
+
{ token: '[redacted]', name: 'ok', userCode: '[redacted]', loginId: '[redacted]', verificationUrl: '[redacted]', nested: { api_key: '[redacted]', list: ['[redacted]'] } },
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
if (!helper) {
|
|
@@ -10,6 +10,7 @@ const { terminateTree } = require('../src/main/native/process-utils');
|
|
|
10
10
|
async function main() {
|
|
11
11
|
const work = fs.mkdtempSync(path.join(os.tmpdir(), 'hm shim spaces-'));
|
|
12
12
|
const host = path.join(work, 'fixture host.cjs');
|
|
13
|
+
const stockProbe = path.join(work, 'stock probe.cjs');
|
|
13
14
|
const env = { ...process.env, HARNESS_MIX_NODE_PATH: process.execPath,
|
|
14
15
|
CODEXHOST_STOCK_CODEX_PATH: process.execPath, HARNESS_MIX_HOST_SCRIPT: host, CODEX_CLI_PATH: 'must-be-removed' };
|
|
15
16
|
let child;
|
|
@@ -19,8 +20,23 @@ if (process.env.CODEX_CLI_PATH) process.exit(90);
|
|
|
19
20
|
readline.createInterface({ input: process.stdin }).on('line', line => {
|
|
20
21
|
process.stdout.write(JSON.stringify({ text: line, args: process.argv.slice(2) }) + '\\n');
|
|
21
22
|
}).on('close', () => process.exit(0));`);
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
fs.writeFileSync(stockProbe, `process.stdout.write(JSON.stringify(process.argv.slice(1))); process.exit(0);`);
|
|
24
|
+
const passthrough = args => execFileSync(nativePaths().shim, args, { env, encoding: 'utf8', timeout: 10000 }).trim();
|
|
25
|
+
assert.equal(passthrough(['--version']), process.version);
|
|
26
|
+
const probeEnv = { ...env, NODE_OPTIONS: [process.env.NODE_OPTIONS, `--require=${JSON.stringify(stockProbe)}`].filter(Boolean).join(' ') };
|
|
27
|
+
const probeStock = args => JSON.parse(execFileSync(nativePaths().shim, args, { env: probeEnv, encoding: 'utf8', timeout: 10000 }));
|
|
28
|
+
const assertStock = (args, message) => {
|
|
29
|
+
const observed = probeStock(args);
|
|
30
|
+
assert.equal(observed.length, args.length, message);
|
|
31
|
+
assert.equal(path.basename(observed[0]), args[0], message);
|
|
32
|
+
assert.deepEqual(observed.slice(1), args.slice(1), message);
|
|
33
|
+
};
|
|
34
|
+
assertStock(['app-server', 'proxy'], 'app-server proxy stays on the official CLI');
|
|
35
|
+
assertStock(['app-server', 'daemon'], 'app-server daemon stays on the official CLI');
|
|
36
|
+
assertStock(['app-server', '--future-option'], 'unknown app-server forms fail open to the official CLI');
|
|
37
|
+
assertStock(['exec', 'app-server'], 'an argument named app-server is not treated as the subcommand');
|
|
38
|
+
const serverArgs = ['-c', 'feature.label=argument with spaces', 'app-server', '--listen', 'stdio://', '--analytics-default-enabled'];
|
|
39
|
+
child = spawn(nativePaths().shim, serverArgs, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
24
40
|
let stdout = '', stderr = '';
|
|
25
41
|
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
26
42
|
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
@@ -31,8 +47,8 @@ readline.createInterface({ input: process.stdin }).on('line', line => {
|
|
|
31
47
|
});
|
|
32
48
|
child.stdin.end('你好 Linux/macOS\n');
|
|
33
49
|
assert.equal(await done, 0, stderr);
|
|
34
|
-
assert.deepEqual(JSON.parse(stdout), { text: '你好 Linux/macOS', args:
|
|
35
|
-
console.log('PASS:
|
|
50
|
+
assert.deepEqual(JSON.parse(stdout), { text: '你好 Linux/macOS', args: serverArgs });
|
|
51
|
+
console.log('PASS: precise app-server routing, official management passthrough, Unicode stdio, arguments, environment and EOF');
|
|
36
52
|
} finally {
|
|
37
53
|
if (child && child.exitCode === null) await terminateTree(child.pid);
|
|
38
54
|
fs.rmSync(work, { recursive: true, force: true });
|
|
@@ -12,7 +12,7 @@ app.whenReady().then(async () => {
|
|
|
12
12
|
const icons = getAllIconsDictionary();
|
|
13
13
|
icons.harnesses['claude-code'] = icons.harnesses.claude;
|
|
14
14
|
icons.harnesses['deepseek-harness'] = icons.harnesses.dsh;
|
|
15
|
-
const bundle = await esbuild.build({ stdin: { contents: `export { createRendererAgentIcon } from './src/native-ui/renderer-extension/src/renderer-agent-icon.ts'; export { mountRendererModelPicker, renderRendererModelPicker } from './src/native-ui/renderer-extension/src/renderer-model-picker.ts'; export { restoredThreadOwnership } from './src/native-ui/renderer-extension/src/renderer-binding-probe.ts'; export { installRendererSidebarAgentIcons } from './src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts';`, resolveDir: process.cwd() }, bundle: true, alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') }, platform: 'browser', format: 'iife', globalName: 'NativeUI', write: false, loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' }, logLevel: 'silent' });
|
|
15
|
+
const bundle = await esbuild.build({ stdin: { contents: `export { createRendererAgentIcon } from './src/native-ui/renderer-extension/src/renderer-agent-icon.ts'; export { mountRendererAgentPicker, renderRendererAgentPicker } from './src/native-ui/renderer-extension/src/renderer-agent-picker.ts'; export { mountRendererModelPicker, renderRendererModelPicker } from './src/native-ui/renderer-extension/src/renderer-model-picker.ts'; export { mountRendererHarnessHandoff } from './src/native-ui/renderer-extension/src/renderer-harness-handoff.ts'; export { restoredThreadOwnership } from './src/native-ui/renderer-extension/src/renderer-binding-probe.ts'; export { installRendererSidebarAgentIcons } from './src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts'; export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { aboutPage } from './src/native-ui/renderer-extension/src/settings/pages.ts'; export { rendererSettingsMessages } from './src/native-ui/renderer-extension/src/settings/localization.ts'; export { installRendererSettingsShell } from './src/native-ui/renderer-extension/src/settings/shell.ts';`, resolveDir: process.cwd() }, bundle: true, alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') }, platform: 'browser', format: 'iife', globalName: 'NativeUI', write: false, loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' }, logLevel: 'silent' });
|
|
16
16
|
const win = new BrowserWindow({ show: false, width: 900, height: 650, webPreferences: { contextIsolation: true, sandbox: true, offscreen: true, backgroundThrottling: false } });
|
|
17
17
|
await win.loadURL('data:text/html,<html><head><style>body{background:%2317191d;color:white;font:16px Arial;padding:40px}button{background:%23272a30;color:white;border:0;padding:12px}img{vertical-align:middle}main{display:flex;gap:25px;margin-bottom:40px}</style></head><body><h2>Harness Mix native components</h2><main></main></body></html>');
|
|
18
18
|
await win.webContents.executeJavaScript(`globalThis.__HARNESS_MIX_ICONS__=${JSON.stringify(icons)};globalThis.__HARNESS_MIX_MODEL_FAMILIES__=${JSON.stringify(MODEL_FAMILIES.map(f => ({ id: f.id, pattern: f.regex.source })))};${bundle.outputFiles[0].text}`);
|
|
@@ -82,13 +82,69 @@ app.whenReady().then(async () => {
|
|
|
82
82
|
assert.deepEqual(result.restored.map(item => item.agent), ['pi','claude-code','deepseek-harness','antigravity']);
|
|
83
83
|
assert.deepEqual(result.restored.map(item => item.model), inspections.map(item => item.effectiveModel));
|
|
84
84
|
for (const id of ['harnesses/codex','harnesses/pi','harnesses/claude-code','harnesses/deepseek-harness','harnesses/antigravity','models/claude','models/deepseek','models/gemini']) assert.ok(result.icons.includes(id), id);
|
|
85
|
+
const handoffView = await win.webContents.executeJavaScript(`(() => {
|
|
86
|
+
globalThis.handoffRequest = null;
|
|
87
|
+
globalThis.testHandoff = NativeUI.mountRendererHarnessHandoff('smoke', request => { globalThis.handoffRequest = request; });
|
|
88
|
+
globalThis.testAgentPicker = NativeUI.mountRendererAgentPicker('handoff-entry', ['codex','pi','claude-code'], target => testHandoff.open('pi', target, 'zh-CN'), () => {}, () => {});
|
|
89
|
+
document.body.append(testAgentPicker.root);
|
|
90
|
+
NativeUI.renderRendererAgentPicker(testAgentPicker, {agent:'pi',phase:'locked'}, 'ready', false, {pi:'ready','claude-code':'ready'});
|
|
91
|
+
testAgentPicker.trigger.click();
|
|
92
|
+
testAgentPicker.options['claude-code'].button.click();
|
|
93
|
+
testHandoff.note.value = '按上一位给出的方案继续执行';
|
|
94
|
+
return { open:testHandoff.dialog.open, title:testHandoff.dialog.querySelector('h2').textContent, text:testHandoff.dialog.textContent, pickerLabel:testAgentPicker.trigger.getAttribute('aria-label'), targetEnabled:!testAgentPicker.options['claude-code'].button.disabled };
|
|
95
|
+
})()`);
|
|
96
|
+
assert.equal(handoffView.open, true);
|
|
97
|
+
assert.equal(handoffView.title, '接力当前任务');
|
|
98
|
+
assert.match(handoffView.text, /Pi.*Claude Code/s);
|
|
99
|
+
assert.match(handoffView.text, /对话记录和文件现场/);
|
|
100
|
+
assert.match(handoffView.pickerLabel, /Hand off task from Pi/);
|
|
101
|
+
assert.equal(handoffView.targetEnabled, true);
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
103
|
+
fs.writeFileSync(path.join(out, 'harness-handoff.png'), (await win.webContents.capturePage()).toPNG());
|
|
104
|
+
const handoffRequest = await win.webContents.executeJavaScript(`(() => { testHandoff.confirm.click(); const request = globalThis.handoffRequest; testHandoff.close(); testHandoff.dispose(); return request; })()`);
|
|
105
|
+
assert.deepEqual(handoffRequest, {
|
|
106
|
+
from:'pi', to:'claude-code', note:'按上一位给出的方案继续执行', intent:'continue',
|
|
107
|
+
includes:{ conversation:true, plan:true, evidence:true, files:true, unresolved:true },
|
|
108
|
+
});
|
|
109
|
+
await win.webContents.executeJavaScript('testAgentPicker.dispose()');
|
|
85
110
|
await win.webContents.executeJavaScript('testModelPicker.trigger.click();document.querySelector("main").style.flexWrap="wrap"');
|
|
86
111
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
87
112
|
fs.writeFileSync(path.join(out, 'native-components.png'), (await win.webContents.capturePage()).toPNG());
|
|
88
113
|
await win.webContents.executeJavaScript(`document.body.style.background='#fff';document.body.style.color='#17191d';document.querySelector('main').style.flexWrap='wrap'`);
|
|
89
114
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
90
115
|
fs.writeFileSync(path.join(out, 'native-components-light.png'), (await win.webContents.capturePage()).toPNG());
|
|
116
|
+
const settingsResult = await win.webContents.executeJavaScript(`(async () => {
|
|
117
|
+
document.body.innerHTML = '';
|
|
118
|
+
const messages = NativeUI.rendererSettingsMessages('zh-CN');
|
|
119
|
+
const account = { accountId:'official-codex', label:'native@example.com', email:'native@example.com', planType:'plus', codexHome:'C:/Users/test/.codex', active:true, isDefault:true, authenticated:true, management:'native' };
|
|
120
|
+
const client = {
|
|
121
|
+
listCodexAccounts: async () => ({ accounts:[account] }),
|
|
122
|
+
refreshCodexAccounts: async () => ({ accounts:[account] }),
|
|
123
|
+
inspectCodexAccountUsage: async () => ({ accountId:account.accountId, usage:{planFiveHourUsedPercent:33,planSevenDayUsedPercent:29}, accountCredits:{usedPercent:33,periodType:'five_hour',productUsage:[{product:'7-day window',usagePercent:29}]} }),
|
|
124
|
+
createCodexAccount: async () => ({account}), deleteCodexAccount: async () => ({deletedAccountId:account.accountId}), activateCodexAccount: async () => ({account}),
|
|
125
|
+
startCodexAccountLogin: async () => { throw new Error('not expected'); }, cancelCodexAccountLogin: async () => ({cancelled:true}), logoutCodexAccount: async () => ({account:{...account,email:undefined,authenticated:false}}),
|
|
126
|
+
};
|
|
127
|
+
const shell = NativeUI.installRendererSettingsShell([
|
|
128
|
+
NativeUI.createAccountsSettingsPage(messages, () => client),
|
|
129
|
+
NativeUI.aboutPage(messages, () => ({ readCurrentVersion:async () => ({version:'0.1.4'}), checkUpdate:async () => { throw new Error('not expected'); } })),
|
|
130
|
+
], messages, document);
|
|
131
|
+
shell.openSettings(undefined, 'accounts');
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
133
|
+
const shadow = shell.root.shadowRoot;
|
|
134
|
+
return { text:shadow.textContent, addHidden:[...shadow.querySelectorAll('button')].find(button => button.textContent.includes('添加账号'))?.hidden };
|
|
135
|
+
})()`);
|
|
136
|
+
assert.match(settingsResult.text, /native.*example\.com/s);
|
|
137
|
+
assert.match(settingsResult.text, /Plus/);
|
|
138
|
+
assert.equal(settingsResult.addHidden, true);
|
|
139
|
+
fs.writeFileSync(path.join(out, 'settings-account.png'), (await win.webContents.capturePage()).toPNG());
|
|
140
|
+
const aboutText = await win.webContents.executeJavaScript(`(async () => {
|
|
141
|
+
globalThis.__codexhostSettingsShellV1.openSettings(undefined, 'about');
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
143
|
+
return globalThis.__codexhostSettingsShellV1.root.shadowRoot.textContent;
|
|
144
|
+
})()`);
|
|
145
|
+
assert.match(aboutText, /v0\.1\.4/);
|
|
146
|
+
fs.writeFileSync(path.join(out, 'settings-about.png'), (await win.webContents.capturePage()).toPNG());
|
|
91
147
|
win.destroy();
|
|
92
|
-
console.log('PASS: real Electron native picker
|
|
148
|
+
console.log('PASS: real Electron native picker, graphical Harness handoff, official Account/Usage, live About version and all project Harness/model SVGs load');
|
|
93
149
|
app.quit();
|
|
94
150
|
}).catch(error => { console.error(error); app.exit(1); });
|
|
@@ -14,13 +14,16 @@ const pkg = path.join(work, 'package');
|
|
|
14
14
|
fs.mkdirSync(path.join(pkg, 'bin'), { recursive: true });
|
|
15
15
|
const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
|
16
16
|
const names = platform === 'win32' ? ['harness-mix-shim.exe', 'harness-mix-appx.exe', 'harness-mix-secret.exe'] : ['harness-mix-shim'];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const sourceOnly = flags.includes('--source-only');
|
|
18
|
+
if (!sourceOnly) {
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
const source = path.join(output, name);
|
|
21
|
+
if (!fs.existsSync(source)) throw new Error(`Missing ${source}; build the target runtime first (or use --source-only)`);
|
|
22
|
+
fs.copyFileSync(source, path.join(pkg, 'bin', name));
|
|
23
|
+
}
|
|
21
24
|
}
|
|
22
25
|
fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({
|
|
23
|
-
name: `@harness-mix/native-${key}`, version, description: `Harness Mix native runtime for ${key}`,
|
|
26
|
+
name: `@harness-mix/native-${key}`, version, description: `Harness Mix native runtime for ${key}${sourceOnly ? ' (source-only; build locally)' : ''}`,
|
|
24
27
|
license: 'Apache-2.0', os: [platform], cpu: [arch], files: ['bin'], publishConfig: { access: 'public' },
|
|
25
28
|
}, null, 2) + '\n');
|
|
26
29
|
fs.writeFileSync(path.join(pkg, 'README.md'), `# @harness-mix/native-${key}\n\nNative runtime selected automatically by @harness-mix/cli.\n`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Local read-only MCP server used by the native integration acceptance probe.
|
|
2
|
+
const readline = require('node:readline');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
readline.createInterface({ input: process.stdin }).on('line', line => {
|
|
5
|
+
let request; try { request = JSON.parse(line); } catch { return; }
|
|
6
|
+
if (process.argv[2]) fs.appendFileSync(process.argv[2], `${request.method}\n`);
|
|
7
|
+
if (request.id === undefined) return;
|
|
8
|
+
let result;
|
|
9
|
+
if (request.method === 'initialize') result = { protocolVersion: request.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: 'harness-mix-integration-probe', version: '1.0.0' } };
|
|
10
|
+
else if (request.method === 'tools/list') result = { tools: [{ name: 'integration_probe', description: 'Return a fixed test token without side effects.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } }] };
|
|
11
|
+
else if (request.method === 'tools/call') result = { content: [{ type: 'text', text: 'HARNESS_MIX_MCP_OK' }] };
|
|
12
|
+
else if (request.method === 'ping') result = {};
|
|
13
|
+
else { process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Unknown method' } })}\n`); return; }
|
|
14
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: request.id, result })}\n`);
|
|
15
|
+
});
|
|
@@ -2,6 +2,7 @@ const assert = require('node:assert/strict');
|
|
|
2
2
|
const fs = require('node:fs/promises');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { HostRuntime } = require('../src/main/host/runtime');
|
|
5
|
+
const { CAP, buildHandoffContext } = require('../src/main/host/handoff');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 跨 Harness 原地切换(switchHarness)脚本级测试:
|
|
@@ -33,6 +34,19 @@ function fakeHarness(id, name) {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
async function main() {
|
|
37
|
+
const longSolution = `方案开头-${'x'.repeat(5000)}-方案结尾`;
|
|
38
|
+
const optimized = buildHandoffContext({
|
|
39
|
+
cwd: 'E:/project', title: '登录重构', messages: [
|
|
40
|
+
{ role: 'user', text: '先分析问题' },
|
|
41
|
+
{ role: 'assistant', text: '初步判断' },
|
|
42
|
+
{ role: 'user', text: '给出完整方案' },
|
|
43
|
+
{ role: 'assistant', text: longSolution },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
assert.deepEqual(optimized.conversationTail.map(message => message.role), ['user', 'assistant', 'user', 'assistant']);
|
|
47
|
+
assert.equal(optimized.conversationTail.at(-1).text, longSolution, '最新方案应获得更大的交接预算');
|
|
48
|
+
assert.ok(optimized.conversationTail.reduce((sum, message) => sum + message.text.length, 0) <= CAP.totalChars);
|
|
49
|
+
|
|
36
50
|
await fs.mkdir('output', { recursive: true });
|
|
37
51
|
const root = await fs.mkdtemp(path.resolve('output/switch-harness-'));
|
|
38
52
|
const rt = new HostRuntime({ dataDirectory: path.join(root, 'data') });
|
|
@@ -71,6 +85,12 @@ async function main() {
|
|
|
71
85
|
assert.equal(thread.harnessChain[0].nativeSessionId, aNativeId);
|
|
72
86
|
assert.equal(thread.pendingHandoff.fromHarnessId, 'a');
|
|
73
87
|
assert.equal(thread.pendingHandoff.note, '优先把测试补完');
|
|
88
|
+
assert.match(thread.pendingHandoff.checkpointId, /^handoff_[a-f0-9]{24}$/);
|
|
89
|
+
const firstCheckpointId = thread.pendingHandoff.checkpointId;
|
|
90
|
+
const checkpointBeforeSend = rt.handoffs.get(thread.id, firstCheckpointId);
|
|
91
|
+
assert.equal(checkpointBeforeSend.intent, 'continue');
|
|
92
|
+
assert.equal(checkpointBeforeSend.fileState.files.some(file => file.path === 'src/login.ts' && file.changeType === 'added'), true);
|
|
93
|
+
assert.equal(rt.handoffs.owned(thread.id, firstCheckpointId).status, 'checkpoint-created');
|
|
74
94
|
assert.equal(thread.restore, false, '新 Harness 上是全新原生会话');
|
|
75
95
|
assert.equal(rt.sessions.has(thread.id), false, '切换后旧原生会话已关闭');
|
|
76
96
|
assert.equal(a.state.closed, 1);
|
|
@@ -87,11 +107,12 @@ async function main() {
|
|
|
87
107
|
assert.match(b.state.sent[0], /untrusted historical data/);
|
|
88
108
|
assert.match(b.state.sent[0], /帮我重构登录模块/);
|
|
89
109
|
assert.match(b.state.sent[0], /Alpha 回复/);
|
|
90
|
-
assert.match(b.state.sent[0], /
|
|
110
|
+
assert.match(b.state.sent[0], /"path":"src\/login\.ts","changeType":"added"/);
|
|
91
111
|
assert.match(b.state.sent[0], /优先把测试补完/);
|
|
92
112
|
const visible = thread.messages.filter(m => m.role === 'user').at(-1);
|
|
93
113
|
assert.equal(visible.text, '继续', '信封不进用户可见消息');
|
|
94
114
|
assert.equal(thread.pendingHandoff, undefined, '信封发送成功后标记已清除');
|
|
115
|
+
assert.equal(rt.handoffs.owned(thread.id, firstCheckpointId).status, 'active');
|
|
95
116
|
|
|
96
117
|
// 5) 第二轮起不再注入
|
|
97
118
|
await rt.send(thread.id, '第二步');
|
|
@@ -123,6 +144,9 @@ async function main() {
|
|
|
123
144
|
await settle(thread.id);
|
|
124
145
|
await assert.rejects(rt.switchHarness(thread.id, 'a'), /已经在该 Harness/);
|
|
125
146
|
await assert.rejects(rt.switchHarness(thread.id, 'Nope'), /未知 Harness/);
|
|
147
|
+
rt.status.b = { available: false, detail: '测试连接不可用' };
|
|
148
|
+
await assert.rejects(rt.switchHarness(thread.id, 'b'), /测试连接不可用/);
|
|
149
|
+
rt.status.b = { available: true };
|
|
126
150
|
const draft = await rt.createThread({ harnessId: 'a', cwd: root, ephemeral: true });
|
|
127
151
|
await assert.rejects(rt.switchHarness(draft.id, 'b'), /草稿任务/);
|
|
128
152
|
const child = await rt.createThread({ harnessId: 'a', cwd: root, parentThreadId: thread.id });
|
|
@@ -138,6 +162,8 @@ async function main() {
|
|
|
138
162
|
// 重启恢复的真正契约是持久化的 harnessId + nativeSessionId(指向 A 的原生会话)。
|
|
139
163
|
assert.equal(persisted.nativeSessionId, aNativeId);
|
|
140
164
|
assert.equal(persisted.restore, undefined);
|
|
165
|
+
const persistedCheckpoints = JSON.parse(await fs.readFile(path.join(root, 'data', 'handoff', 'checkpoints.json'), 'utf8'));
|
|
166
|
+
assert.equal(persistedCheckpoints.some(row => row.checkpointId === firstCheckpointId && row.status === 'active'), true);
|
|
141
167
|
|
|
142
168
|
console.log('PASS: in-place harness switch → one-shot context envelope → switch-back native resume, guards and persistence');
|
|
143
169
|
} finally { await rt.close(); }
|
package/src/main/adapters/acp.js
CHANGED
|
@@ -53,11 +53,10 @@ function acpAdapter({ id, name, bin, args, executable = false, images = false, f
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
async describe() { return { models: null, thinkingLevels: [], permissionModes: [] }; },
|
|
56
|
-
async open({ thread, emit, diagnostic = () => {}, collaboration }) {
|
|
56
|
+
async open({ thread, emit, diagnostic = () => {}, collaboration, managedMcp = [] }) {
|
|
57
57
|
const cli = command(args);
|
|
58
58
|
const session = { cwd: thread.cwd, nativeSessionId: null, collaborationEnabled: !!collaboration,
|
|
59
|
-
mcpServers:
|
|
60
|
-
env: Object.entries(collaboration.env).map(([name, value]) => ({ name, value })) }] : [],
|
|
59
|
+
mcpServers: require('./managed-mcp').acpServers(managedMcp, collaboration),
|
|
61
60
|
state: { configOptions: [], models: null, modes: null, commands: [], usage: undefined, loading: true }, pendingApprovals: new Map(), tools: new Map(), emit };
|
|
62
61
|
session.process = new JsonlProcess(cli.command, cli.args, { cwd: thread.cwd }, {
|
|
63
62
|
onDiagnostic: diagnostic,
|
|
@@ -186,6 +185,7 @@ function acpAdapter({ id, name, bin, args, executable = false, images = false, f
|
|
|
186
185
|
};
|
|
187
186
|
return adapter;
|
|
188
187
|
}
|
|
188
|
+
manifest.integrations = { mcp: true };
|
|
189
189
|
return { manifest, create };
|
|
190
190
|
}
|
|
191
191
|
module.exports = { acpAdapter, catalog };
|
|
@@ -639,27 +639,21 @@ class QuestionBridge {
|
|
|
639
639
|
CODEXHOST_AGY_QUESTION_CLIENT: `"${clientPath.replaceAll('\\', '/')}"`,
|
|
640
640
|
};
|
|
641
641
|
|
|
642
|
-
if (options.collaboration) {
|
|
642
|
+
if (options.collaboration || options.managedMcp?.length) {
|
|
643
643
|
const pluginDir = path.join(dir, '.agents', 'plugins', 'harness-mix');
|
|
644
644
|
const plainPluginDir = path.join(dir, 'plugins', 'harness-mix');
|
|
645
645
|
await fs.promises.mkdir(pluginDir, { recursive: true });
|
|
646
646
|
await fs.promises.mkdir(plainPluginDir, { recursive: true });
|
|
647
647
|
const pluginManifest = JSON.stringify({ name: 'harness-mix-collaboration', description: 'Harness Mix Multi-agent Collaboration' }, null, 2);
|
|
648
648
|
const mcpConfig = JSON.stringify({
|
|
649
|
-
mcpServers:
|
|
650
|
-
'harness-mix': {
|
|
651
|
-
command: options.collaboration.command,
|
|
652
|
-
args: options.collaboration.args,
|
|
653
|
-
env: options.collaboration.env,
|
|
654
|
-
},
|
|
655
|
-
},
|
|
649
|
+
mcpServers: require('./managed-mcp').namedServers(options.managedMcp, options.collaboration),
|
|
656
650
|
}, null, 2);
|
|
657
651
|
await fs.promises.writeFile(path.join(pluginDir, 'plugin.json'), pluginManifest, 'utf8');
|
|
658
652
|
await fs.promises.writeFile(path.join(pluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
|
|
659
653
|
await fs.promises.writeFile(path.join(plainPluginDir, 'plugin.json'), pluginManifest, 'utf8');
|
|
660
654
|
await fs.promises.writeFile(path.join(plainPluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
|
|
661
655
|
await fs.promises.writeFile(path.join(dir, '.agents', 'mcp_config.json'), mcpConfig, 'utf8');
|
|
662
|
-
Object.assign(bridge.environment, options.collaboration
|
|
656
|
+
Object.assign(bridge.environment, options.collaboration?.env || {});
|
|
663
657
|
}
|
|
664
658
|
|
|
665
659
|
return bridge;
|
|
@@ -823,7 +817,7 @@ function create(emit, options = {}) {
|
|
|
823
817
|
});
|
|
824
818
|
},
|
|
825
819
|
|
|
826
|
-
async open({ thread, emit: emitEvent, diagnostic, collaboration }) {
|
|
820
|
+
async open({ thread, emit: emitEvent, diagnostic, collaboration, managedMcp = [] }) {
|
|
827
821
|
// Do not start a second agy process while a real turn may be starting.
|
|
828
822
|
// Quota is loaded only by the explicit account/usage refresh path below.
|
|
829
823
|
await restoreLegacyClipboardAttachments(thread);
|
|
@@ -836,7 +830,8 @@ function create(emit, options = {}) {
|
|
|
836
830
|
model,
|
|
837
831
|
thinkingLevel,
|
|
838
832
|
permissionMode,
|
|
839
|
-
collaboration: collaboration || null,
|
|
833
|
+
collaboration: collaboration || null,
|
|
834
|
+
managedMcp,
|
|
840
835
|
collaborationEnabled: !!collaboration,
|
|
841
836
|
activeTurn: null,
|
|
842
837
|
bridge: null,
|
|
@@ -858,7 +853,8 @@ function create(emit, options = {}) {
|
|
|
858
853
|
const bridge = await QuestionBridge.create({
|
|
859
854
|
approvals,
|
|
860
855
|
emit: emitEvent,
|
|
861
|
-
collaboration: session.collaboration,
|
|
856
|
+
collaboration: session.collaboration,
|
|
857
|
+
managedMcp: session.managedMcp,
|
|
862
858
|
});
|
|
863
859
|
session.bridge = bridge;
|
|
864
860
|
|
|
@@ -1349,7 +1345,8 @@ function create(emit, options = {}) {
|
|
|
1349
1345
|
};
|
|
1350
1346
|
}
|
|
1351
1347
|
|
|
1352
|
-
|
|
1348
|
+
manifest.integrations = { mcp: true };
|
|
1349
|
+
module.exports = {
|
|
1353
1350
|
manifest,
|
|
1354
1351
|
create,
|
|
1355
1352
|
parseModelsOutput,
|
|
@@ -245,7 +245,7 @@ async function loadSdk() {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/** 建立一个常驻 SDK 会话(open 与 fork 共用):构造 query、启动事件泵 */
|
|
248
|
-
function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelId, effort, emit, collaboration, onPlanLimit }) {
|
|
248
|
+
function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelId, effort, emit, collaboration, managedMcp = [], onPlanLimit }) {
|
|
249
249
|
const input = new MessageQueue();
|
|
250
250
|
const session = {
|
|
251
251
|
nativeSessionId: resumeId || newSessionId,
|
|
@@ -265,7 +265,8 @@ function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelI
|
|
|
265
265
|
prompt: input,
|
|
266
266
|
options: {
|
|
267
267
|
cwd,
|
|
268
|
-
|
|
268
|
+
mcpServers: require('./managed-mcp').namedServers(managedMcp, collaboration),
|
|
269
|
+
settingSources: ['user', 'project', 'local'],
|
|
269
270
|
...(resumeId ? { resume: resumeId } : {}),
|
|
270
271
|
...(!resumeId && newSessionId ? { sessionId: newSessionId } : {}),
|
|
271
272
|
...(effort ? { effort } : {}),
|
|
@@ -373,7 +374,11 @@ function create() {
|
|
|
373
374
|
: { available: true, detail: "Agent SDK 就绪(内置原生 CLI;未检测到独立 claude 命令)" };
|
|
374
375
|
},
|
|
375
376
|
|
|
376
|
-
async
|
|
377
|
+
async inspectIntegrations(session) {
|
|
378
|
+
const rows = await session.query.mcpServerStatus();
|
|
379
|
+
return rows.map(row => ({ name: row.name, status: row.status, tools: (row.tools || []).map(t => t.name) }));
|
|
380
|
+
},
|
|
381
|
+
async open({ thread, emit, collaboration, managedMcp }) {
|
|
377
382
|
const sdk = await loadSdk();
|
|
378
383
|
return spawnSession(sdk, {
|
|
379
384
|
cwd: thread.cwd,
|
|
@@ -384,6 +389,7 @@ function create() {
|
|
|
384
389
|
modelId: thread.options?.model?.id,
|
|
385
390
|
emit,
|
|
386
391
|
collaboration,
|
|
392
|
+
managedMcp,
|
|
387
393
|
onPlanLimit,
|
|
388
394
|
});
|
|
389
395
|
},
|
|
@@ -509,6 +515,7 @@ function create() {
|
|
|
509
515
|
};
|
|
510
516
|
}
|
|
511
517
|
|
|
518
|
+
manifest.integrations = { mcp: true, skills: { global: ['.claude/skills'], project: ['.claude/skills'], overrides: { '.claude/skills': { env: 'CLAUDE_CONFIG_DIR', suffix: 'skills' } } } };
|
|
512
519
|
module.exports = {
|
|
513
520
|
manifest,
|
|
514
521
|
create,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { JsonlProcess, cliSpawn } = require('../host/jsonl');
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const shared = new Map();
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A single native Codex app-server connection shared by every Codex thread.
|
|
@@ -8,16 +8,18 @@ let shared;
|
|
|
8
8
|
* traffic to the Adapter session that registered the native thread id.
|
|
9
9
|
*/
|
|
10
10
|
class CodexAppServer {
|
|
11
|
-
constructor(diagnostic) {
|
|
11
|
+
constructor(diagnostic, codexHome) {
|
|
12
12
|
this.refs = 0;
|
|
13
13
|
this.routes = new Map();
|
|
14
14
|
this.notifications = new Set();
|
|
15
15
|
this.diagnostics = new Set(diagnostic ? [diagnostic] : []);
|
|
16
16
|
this.closed = false;
|
|
17
|
+
this.codexHome = codexHome ? require('node:path').resolve(codexHome) : null;
|
|
17
18
|
// The Desktop-bundled CLI and the PATH CLI can be different versions.
|
|
18
19
|
const executable = process.env.HARNESS_MIX_CODEX_EXECUTABLE || process.env.CODEXHOST_STOCK_CODEX_PATH;
|
|
19
20
|
const { command, args } = executable ? { command: executable, args: ['app-server', '--listen', 'stdio://'] } : cliSpawn('codex', ['app-server', '--stdio']);
|
|
20
|
-
|
|
21
|
+
const env = { ...process.env, ...(this.codexHome ? { CODEX_HOME: this.codexHome } : {}) };
|
|
22
|
+
this.process = new JsonlProcess(command, args, { env }, {
|
|
21
23
|
onEvent: (message) => this.#notification(message),
|
|
22
24
|
onRequest: (message) => this.#request(message),
|
|
23
25
|
onDiagnostic: (line) => this.#diagnostic(line),
|
|
@@ -36,16 +38,21 @@ class CodexAppServer {
|
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
static async acquire(diagnostic) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
static async acquire(diagnostic, codexHome) {
|
|
42
|
+
const key = codexHome ? require('node:path').resolve(codexHome).toLowerCase() : '<default>';
|
|
43
|
+
let server = shared.get(key);
|
|
44
|
+
if (!server || server.closed) {
|
|
45
|
+
server = new CodexAppServer(diagnostic, codexHome);
|
|
46
|
+
server.sharedKey = key;
|
|
47
|
+
shared.set(key, server);
|
|
48
|
+
} else if (diagnostic) server.diagnostics.add(diagnostic);
|
|
49
|
+
server.refs++;
|
|
43
50
|
try {
|
|
44
|
-
await
|
|
45
|
-
return
|
|
51
|
+
await server.ready;
|
|
52
|
+
return server;
|
|
46
53
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
if (
|
|
54
|
+
server.refs--;
|
|
55
|
+
if (server.refs === 0) server.stop();
|
|
49
56
|
throw error;
|
|
50
57
|
}
|
|
51
58
|
}
|
|
@@ -78,7 +85,7 @@ class CodexAppServer {
|
|
|
78
85
|
this.routes.clear();
|
|
79
86
|
this.notifications.clear();
|
|
80
87
|
this.process.stop();
|
|
81
|
-
if (shared === this) shared
|
|
88
|
+
if (this.sharedKey && shared.get(this.sharedKey) === this) shared.delete(this.sharedKey);
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
#route(message) {
|
|
@@ -112,7 +119,7 @@ class CodexAppServer {
|
|
|
112
119
|
this.closed = true;
|
|
113
120
|
for (const route of this.routes.values()) route.onExit?.(error);
|
|
114
121
|
this.routes.clear();
|
|
115
|
-
if (shared === this) shared
|
|
122
|
+
if (this.sharedKey && shared.get(this.sharedKey) === this) shared.delete(this.sharedKey);
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
|
|
@@ -356,10 +356,11 @@ function create() {
|
|
|
356
356
|
: { available: true, detail: String(result.stdout).trim() };
|
|
357
357
|
},
|
|
358
358
|
|
|
359
|
-
async open({ thread, emit, diagnostic, collaboration }) {
|
|
360
|
-
const host = await CodexAppServer.acquire(diagnostic);
|
|
359
|
+
async open({ thread, emit, diagnostic, collaboration, managedMcp = [] }) {
|
|
360
|
+
const host = await CodexAppServer.acquire(diagnostic, thread.options?.codexHome);
|
|
361
361
|
try {
|
|
362
|
-
const
|
|
362
|
+
const servers = require('./managed-mcp').namedServers(managedMcp, collaboration);
|
|
363
|
+
const options = { ...threadOptions(thread), ...(Object.keys(servers).length ? { config: Object.fromEntries(Object.entries(servers).map(([name, value]) => [`mcp_servers.${name}`, value])) } : {}) };
|
|
363
364
|
const result = thread.restore
|
|
364
365
|
? await host.request('thread/resume', { threadId: thread.nativeSessionId, ...options })
|
|
365
366
|
: await host.request('thread/start', options);
|
|
@@ -521,7 +522,7 @@ function create() {
|
|
|
521
522
|
async fork(source, { emit, diagnostic, message }) {
|
|
522
523
|
const lastTurnId = message?.coreTurn?.nativeTurnRef?.turnId ?? message?.coreTurn?.nativeTurnRef?.checkpointId;
|
|
523
524
|
if (message && !lastTurnId) throw new Error('这条回复缺少 Codex 原生 Turn ID,无法精确分支');
|
|
524
|
-
const host = await CodexAppServer.acquire(diagnostic);
|
|
525
|
+
const host = await CodexAppServer.acquire(diagnostic, source.options?.codexHome);
|
|
525
526
|
try {
|
|
526
527
|
const result = await host.request('thread/fork', {
|
|
527
528
|
threadId: source.nativeSessionId,
|
|
@@ -556,4 +557,5 @@ function create() {
|
|
|
556
557
|
};
|
|
557
558
|
}
|
|
558
559
|
|
|
560
|
+
manifest.integrations = { mcp: true, skills: { global: ['.agents/skills'], project: ['.agents/skills'] } };
|
|
559
561
|
module.exports = { manifest, create, projectNotification, queueRequest, usageView, modelView };
|
package/src/main/adapters/dsh.js
CHANGED
|
@@ -431,7 +431,7 @@ function create() {
|
|
|
431
431
|
}).create();
|
|
432
432
|
const openWeb = web.open.bind(web);
|
|
433
433
|
web.open = async context => {
|
|
434
|
-
if (!context.collaboration) return openWeb(context);
|
|
434
|
+
if (!context.collaboration && !context.managedMcp?.length) return openWeb(context);
|
|
435
435
|
const session = await acp.open(context);
|
|
436
436
|
session.dshAcpLead = true;
|
|
437
437
|
return session;
|
|
@@ -443,4 +443,5 @@ function create() {
|
|
|
443
443
|
return web;
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
manifest.integrations = { mcp: true };
|
|
446
447
|
module.exports = { manifest, create, projectWireEvent, flattenCatalog };
|