@harness-mix/cli 0.1.4 → 0.1.6
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/multi-agent-collaboration.md +9 -9
- package/docs/native-codex.md +21 -10
- package/output/native-build/harness-mix-shim.exe +0 -0
- package/output/native-build/renderer-extension.js +2596 -135
- package/package.json +20 -10
- 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/collaboration-test.cjs +8 -3
- package/scripts/collaboration-ui-smoke.cjs +40 -26
- 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 +123 -0
- package/scripts/integrations-ui-smoke.cjs +124 -0
- package/scripts/native-protocol-test.cjs +180 -5
- package/scripts/native-secret-test.cjs +2 -2
- package/scripts/native-shim-test.cjs +20 -4
- package/scripts/native-sidebar-test.cjs +7 -0
- package/scripts/native-skill-roots-test.cjs +90 -0
- 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/scripts/test-live-mentions.cjs +152 -0
- package/src/main/adapters/acp.js +3 -3
- package/src/main/adapters/antigravity.js +17 -17
- package/src/main/adapters/claude.js +50 -10
- package/src/main/adapters/codebuddy.js +1 -0
- package/src/main/adapters/codex-app-server.js +20 -13
- package/src/main/adapters/codex.js +25 -5
- package/src/main/adapters/cursor.js +5 -1
- package/src/main/adapters/dsh.js +9 -1
- package/src/main/adapters/grok.js +7 -3
- package/src/main/adapters/hermes.js +8 -0
- package/src/main/adapters/kiro.js +2 -1
- package/src/main/adapters/managed-mcp.js +25 -0
- package/src/main/adapters/native-acp.js +4 -4
- package/src/main/adapters/omp.js +11 -0
- package/src/main/adapters/openclaw.js +6 -0
- package/src/main/adapters/opencode.js +13 -3
- package/src/main/adapters/pi.js +5 -0
- package/src/main/adapters/qoder.js +1 -0
- package/src/main/adapters/trae.js +4 -0
- package/src/main/adapters/zcode.js +3 -0
- package/src/main/harness-adapter/manifest.js +17 -2
- package/src/main/host/collaboration.js +57 -11
- 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 +339 -0
- package/src/main/host/runtime.js +95 -15
- 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 +176 -14
- package/src/main/native/redact.js +1 -1
- package/src/main/native/rs/crates/shim/src/main.rs +155 -12
- package/src/main/native/thread-list.js +3 -0
- 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-harness-mentions.d.ts +11 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +105 -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 +6 -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 +4 -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 +115 -18
- package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +87 -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-harness-mentions.ts +556 -123
- package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +59 -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 +1474 -0
- package/src/native-ui/renderer-extension/src/settings/localization.ts +32 -13
- package/src/native-ui/renderer-extension/src/settings/pages.ts +49 -18
- package/src/native-ui/renderer-extension/src/settings/shell.css +103 -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 +21 -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 +6 -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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const { randomUUID } = require('node:crypto');
|
|
5
|
+
const { CodexAppServer } = require('../adapters/codex-app-server');
|
|
6
|
+
|
|
7
|
+
const OFFICIAL_ACCOUNT_ID = 'official-codex';
|
|
8
|
+
const ACCOUNT_ID = /^[A-Za-z0-9._~-]{1,256}$/;
|
|
9
|
+
const PLAN_TYPES = new Set(['free', 'go', 'plus', 'pro', 'prolite', 'team', 'self_serve_business_prolite', 'self_serve_business_usage_based', 'business', 'ent26', 'enterprise_cbp_automation', 'enterprise_cbp_usage_based', 'enterprise', 'edu', 'edu_plus', 'edu_pro', 'unknown']);
|
|
10
|
+
|
|
11
|
+
function isoFromUnixSeconds(value) {
|
|
12
|
+
return Number.isSafeInteger(value) && value >= 0 ? new Date(value * 1000).toISOString() : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rateLimitView(result) {
|
|
16
|
+
const snapshot = result?.rateLimits;
|
|
17
|
+
const primary = snapshot?.primary;
|
|
18
|
+
const secondary = snapshot?.secondary;
|
|
19
|
+
const usage = {};
|
|
20
|
+
if (typeof primary?.usedPercent === 'number' && primary.usedPercent >= 0 && primary.usedPercent <= 100) {
|
|
21
|
+
usage.planFiveHourUsedPercent = primary.usedPercent;
|
|
22
|
+
if (Number.isSafeInteger(primary.resetsAt) && primary.resetsAt >= 0) usage.planFiveHourResetsAtUnix = primary.resetsAt;
|
|
23
|
+
}
|
|
24
|
+
if (typeof secondary?.usedPercent === 'number' && secondary.usedPercent >= 0 && secondary.usedPercent <= 100) {
|
|
25
|
+
usage.planSevenDayUsedPercent = secondary.usedPercent;
|
|
26
|
+
if (Number.isSafeInteger(secondary.resetsAt) && secondary.resetsAt >= 0) usage.planSevenDayResetsAtUnix = secondary.resetsAt;
|
|
27
|
+
}
|
|
28
|
+
let accountCredits;
|
|
29
|
+
if (typeof primary?.usedPercent === 'number' && primary.usedPercent >= 0 && primary.usedPercent <= 100) {
|
|
30
|
+
accountCredits = {
|
|
31
|
+
usedPercent: primary.usedPercent,
|
|
32
|
+
periodType: primary.windowDurationMins === 300 ? 'five_hour' : 'unknown',
|
|
33
|
+
...(isoFromUnixSeconds(primary.resetsAt) ? { resetsAt: isoFromUnixSeconds(primary.resetsAt) } : {}),
|
|
34
|
+
};
|
|
35
|
+
if (typeof secondary?.usedPercent === 'number' && secondary.usedPercent >= 0 && secondary.usedPercent <= 100) {
|
|
36
|
+
accountCredits.productUsage = [{
|
|
37
|
+
product: secondary.windowDurationMins === 10080 ? '7-day window' : (snapshot.limitName || 'Secondary limit'),
|
|
38
|
+
usagePercent: secondary.usedPercent,
|
|
39
|
+
...(isoFromUnixSeconds(secondary.resetsAt) ? { resetsAt: isoFromUnixSeconds(secondary.resetsAt) } : {}),
|
|
40
|
+
}];
|
|
41
|
+
}
|
|
42
|
+
const reset = result?.rateLimitResetCredits;
|
|
43
|
+
if (Number.isSafeInteger(reset?.availableCount) && reset.availableCount > 0) {
|
|
44
|
+
const expiresAt = Array.isArray(reset.credits)
|
|
45
|
+
? reset.credits.map(credit => isoFromUnixSeconds(credit?.expiresAt)).filter(Boolean).slice(0, 32)
|
|
46
|
+
: [];
|
|
47
|
+
accountCredits.resetCredits = {
|
|
48
|
+
availableCount: reset.availableCount,
|
|
49
|
+
...(expiresAt.length ? { nextExpiresAt: expiresAt[0], expiresAt } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { usage: Object.keys(usage).length ? usage : null, ...(accountCredits ? { accountCredits } : {}) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function accountIdentity(account) {
|
|
57
|
+
const email = account?.type === 'chatgpt' && typeof account.email === 'string' && account.email.trim()
|
|
58
|
+
? account.email.trim() : undefined;
|
|
59
|
+
const planType = account?.type === 'chatgpt' && PLAN_TYPES.has(account.planType) ? account.planType : undefined;
|
|
60
|
+
return { email, planType, authenticated: !!account };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class CodexAccountManager {
|
|
64
|
+
constructor({ dataDirectory, requestOfficial, emit, acquireServer = CodexAppServer.acquire }) {
|
|
65
|
+
this.root = path.resolve(dataDirectory, 'codex-accounts');
|
|
66
|
+
this.profilesRoot = path.join(this.root, 'profiles');
|
|
67
|
+
this.registryFile = path.join(this.root, 'accounts.json');
|
|
68
|
+
this.requestOfficial = requestOfficial;
|
|
69
|
+
this.emit = emit;
|
|
70
|
+
this.acquireServer = acquireServer;
|
|
71
|
+
this.logins = new Map();
|
|
72
|
+
this.registry = this.#load();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#load() {
|
|
76
|
+
try {
|
|
77
|
+
const value = JSON.parse(fs.readFileSync(this.registryFile, 'utf8'));
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const accounts = (Array.isArray(value.accounts) ? value.accounts : []).filter(entry => {
|
|
80
|
+
if (!entry || !ACCOUNT_ID.test(entry.accountId) || typeof entry.label !== 'string' || !entry.label.trim() || entry.accountId === OFFICIAL_ACCOUNT_ID || seen.has(entry.accountId)) return false;
|
|
81
|
+
seen.add(entry.accountId);
|
|
82
|
+
entry.label = entry.label.trim().slice(0, 256);
|
|
83
|
+
return true;
|
|
84
|
+
}).slice(0, 127);
|
|
85
|
+
const activeAccountId = value.activeAccountId === OFFICIAL_ACCOUNT_ID || accounts.some(entry => entry.accountId === value.activeAccountId)
|
|
86
|
+
? value.activeAccountId : OFFICIAL_ACCOUNT_ID;
|
|
87
|
+
return { version: 1, activeAccountId, accounts };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error.code !== 'ENOENT') throw error;
|
|
90
|
+
return { version: 1, activeAccountId: OFFICIAL_ACCOUNT_ID, accounts: [] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#save() {
|
|
95
|
+
fs.mkdirSync(this.root, { recursive: true });
|
|
96
|
+
const temp = `${this.registryFile}.${process.pid}.tmp`;
|
|
97
|
+
fs.writeFileSync(temp, JSON.stringify(this.registry, null, 2));
|
|
98
|
+
fs.renameSync(temp, this.registryFile);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#entry(accountId) {
|
|
102
|
+
if (accountId === OFFICIAL_ACCOUNT_ID) return { accountId, label: 'Codex 官方账号', codexHome: process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), native: true };
|
|
103
|
+
const entry = this.registry.accounts.find(candidate => candidate.accountId === accountId);
|
|
104
|
+
if (!entry) throw new Error('Unknown Codex account');
|
|
105
|
+
const codexHome = path.resolve(this.profilesRoot, entry.accountId);
|
|
106
|
+
if (!codexHome.startsWith(`${path.resolve(this.profilesRoot)}${path.sep}`)) throw new Error('Invalid Codex account directory');
|
|
107
|
+
return { ...entry, codexHome, native: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async #request(entry, method, params) {
|
|
111
|
+
if (entry.native) {
|
|
112
|
+
if (!this.requestOfficial) throw new Error('Official Codex account service is unavailable');
|
|
113
|
+
return this.requestOfficial(method, params);
|
|
114
|
+
}
|
|
115
|
+
const server = await this.acquireServer(undefined, entry.codexHome);
|
|
116
|
+
try { return await server.request(method, params); }
|
|
117
|
+
finally { server.release(); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async #summary(entry, refreshToken = false) {
|
|
121
|
+
let raw = null;
|
|
122
|
+
try { raw = await this.#request(entry, 'account/read', { refreshToken }); }
|
|
123
|
+
catch { /* A signed-out profile is still a useful selectable Account slot. */ }
|
|
124
|
+
const identity = accountIdentity(raw?.account);
|
|
125
|
+
return {
|
|
126
|
+
accountId: entry.accountId,
|
|
127
|
+
label: identity.email || entry.label,
|
|
128
|
+
...(identity.email ? { email: identity.email } : {}),
|
|
129
|
+
...(identity.planType ? { planType: identity.planType } : {}),
|
|
130
|
+
codexHome: entry.codexHome,
|
|
131
|
+
active: this.registry.activeAccountId === entry.accountId,
|
|
132
|
+
isDefault: entry.native,
|
|
133
|
+
authenticated: identity.authenticated,
|
|
134
|
+
management: entry.native ? 'native' : 'isolated',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async list(refresh = false) {
|
|
139
|
+
const entries = [this.#entry(OFFICIAL_ACCOUNT_ID), ...this.registry.accounts.map(entry => this.#entry(entry.accountId))];
|
|
140
|
+
return { accounts: await Promise.all(entries.map(entry => this.#summary(entry, refresh))) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async create(label) {
|
|
144
|
+
if (this.registry.accounts.length >= 127) throw new Error('Codex Account limit reached (128 including the official account)');
|
|
145
|
+
const accountId = `account-${randomUUID().slice(0, 8)}`;
|
|
146
|
+
const entry = { accountId, label: typeof label === 'string' && label.trim() ? label.trim().slice(0, 256) : `Codex 账号 ${this.registry.accounts.length + 2}`, createdAt: Date.now() };
|
|
147
|
+
fs.mkdirSync(path.join(this.profilesRoot, accountId), { recursive: true });
|
|
148
|
+
this.registry.accounts.push(entry);
|
|
149
|
+
this.#save();
|
|
150
|
+
return { account: await this.#summary(this.#entry(accountId)) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async activate(accountId) {
|
|
154
|
+
const entry = this.#entry(accountId);
|
|
155
|
+
this.registry.activeAccountId = entry.accountId;
|
|
156
|
+
this.#save();
|
|
157
|
+
return { account: await this.#summary(entry) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async delete(accountId) {
|
|
161
|
+
const entry = this.#entry(accountId);
|
|
162
|
+
if (entry.native) throw new Error('The official Codex account cannot be deleted');
|
|
163
|
+
if ([...this.logins.values()].some(login => login.accountId === accountId)) throw new Error('Cancel account sign-in before deleting it');
|
|
164
|
+
const resolved = path.resolve(entry.codexHome);
|
|
165
|
+
const root = path.resolve(this.profilesRoot);
|
|
166
|
+
if (!resolved.startsWith(`${root}${path.sep}`)) throw new Error('Refusing to delete an account outside the managed profile directory');
|
|
167
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
168
|
+
this.registry.accounts = this.registry.accounts.filter(candidate => candidate.accountId !== accountId);
|
|
169
|
+
if (this.registry.activeAccountId === accountId) this.registry.activeAccountId = OFFICIAL_ACCOUNT_ID;
|
|
170
|
+
this.#save();
|
|
171
|
+
return { deletedAccountId: accountId };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async startLogin(accountId) {
|
|
175
|
+
const entry = this.#entry(accountId);
|
|
176
|
+
if (entry.native) {
|
|
177
|
+
const result = await this.#request(entry, 'account/login/start', { type: 'chatgptDeviceCode' });
|
|
178
|
+
return { accountId, loginId: result.loginId, verificationUrl: result.verificationUrl, userCode: result.userCode };
|
|
179
|
+
}
|
|
180
|
+
const server = await this.acquireServer(undefined, entry.codexHome);
|
|
181
|
+
let unwatch;
|
|
182
|
+
try {
|
|
183
|
+
unwatch = server.onNotification(message => {
|
|
184
|
+
if (message?.method !== 'account/login/completed') return;
|
|
185
|
+
const pending = this.logins.get(message.params?.loginId);
|
|
186
|
+
if (!pending || pending.server !== server) return;
|
|
187
|
+
this.logins.delete(message.params.loginId);
|
|
188
|
+
pending.unwatch?.();
|
|
189
|
+
pending.server.release();
|
|
190
|
+
this.emit?.({ method: 'codexhost/account/login/completed', params: {
|
|
191
|
+
accountId: pending.accountId,
|
|
192
|
+
loginId: message.params.loginId,
|
|
193
|
+
success: message.params.success === true,
|
|
194
|
+
error: typeof message.params.error === 'string' ? message.params.error : null,
|
|
195
|
+
} });
|
|
196
|
+
});
|
|
197
|
+
const result = await server.request('account/login/start', { type: 'chatgptDeviceCode' });
|
|
198
|
+
this.logins.set(result.loginId, { accountId, server, unwatch });
|
|
199
|
+
return { accountId, loginId: result.loginId, verificationUrl: result.verificationUrl, userCode: result.userCode };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
unwatch?.();
|
|
202
|
+
server.release();
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async cancelLogin(loginId) {
|
|
208
|
+
const pending = this.logins.get(loginId);
|
|
209
|
+
if (!pending) {
|
|
210
|
+
if (!this.requestOfficial) return { cancelled: false };
|
|
211
|
+
const result = await this.requestOfficial('account/login/cancel', { loginId });
|
|
212
|
+
return { cancelled: ['canceled', 'cancelled'].includes(result?.status) };
|
|
213
|
+
}
|
|
214
|
+
const result = await pending.server.request('account/login/cancel', { loginId });
|
|
215
|
+
this.logins.delete(loginId);
|
|
216
|
+
pending.unwatch?.();
|
|
217
|
+
pending.server.release();
|
|
218
|
+
return { cancelled: ['canceled', 'cancelled'].includes(result?.status) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async usage(accountId) {
|
|
222
|
+
const entry = this.#entry(accountId);
|
|
223
|
+
return { accountId, ...rateLimitView(await this.#request(entry, 'account/rateLimits/read', {})) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async consumeReset(accountId, idempotencyKey) {
|
|
227
|
+
const entry = this.#entry(accountId);
|
|
228
|
+
const result = await this.#request(entry, 'account/rateLimitResetCredit/consume', { idempotencyKey: idempotencyKey || randomUUID() });
|
|
229
|
+
const refreshed = rateLimitView(await this.#request(entry, 'account/rateLimits/read', {}));
|
|
230
|
+
return { accountId, outcome: result.outcome, ...(refreshed.accountCredits ? { accountCredits: refreshed.accountCredits } : {}) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
signedOutOfficial() {
|
|
234
|
+
const entry = this.#entry(OFFICIAL_ACCOUNT_ID);
|
|
235
|
+
return {
|
|
236
|
+
account: {
|
|
237
|
+
accountId: entry.accountId,
|
|
238
|
+
label: entry.label,
|
|
239
|
+
codexHome: entry.codexHome,
|
|
240
|
+
active: this.registry.activeAccountId === entry.accountId,
|
|
241
|
+
isDefault: true,
|
|
242
|
+
authenticated: false,
|
|
243
|
+
management: 'native',
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
executionContext(accountId) {
|
|
249
|
+
const entry = this.#entry(accountId);
|
|
250
|
+
if (entry.native) throw new Error('The official account stays on the native Codex route');
|
|
251
|
+
return { accountId: entry.accountId, codexHome: entry.codexHome };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
close() {
|
|
255
|
+
for (const pending of this.logins.values()) { pending.unwatch?.(); pending.server.release(); }
|
|
256
|
+
this.logins.clear();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = { CodexAccountManager, OFFICIAL_ACCOUNT_ID, rateLimitView };
|
package/src/main/native/host.js
CHANGED
|
@@ -148,6 +148,6 @@ async function runNativeHost() {
|
|
|
148
148
|
official.on('error', error => { console.error(error); void close(); });
|
|
149
149
|
official.on('exit', () => { void close(); });
|
|
150
150
|
await ready;
|
|
151
|
-
console.error('[Harness Mix]
|
|
151
|
+
console.error('[Harness Mix] official Codex passthrough + external Harness routes via HostRuntime/ProtocolCore');
|
|
152
152
|
}
|
|
153
153
|
module.exports = { runNativeHost };
|
|
@@ -120,7 +120,7 @@ async function launch(args = []) {
|
|
|
120
120
|
const paths = nativePaths();
|
|
121
121
|
for (const file of Object.values(paths)) if (!fs.existsSync(file)) throw new Error(`Missing ${file}; run npm run build:native`);
|
|
122
122
|
if (flags.has('--check')) {
|
|
123
|
-
console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
|
|
123
|
+
console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\ncodex_mode=official-passthrough\ncodex_managed_route=codex-harness\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
const skipUpdate = flags.has('--no-update') || process.env.HARNESS_MIX_AUTO_UPDATE === '0';
|
|
@@ -151,7 +151,7 @@ async function launch(args = []) {
|
|
|
151
151
|
CODEXHOST_DATA_DIR: env.CODEXHOST_DATA_DIR, HARNESS_MIX_NODE_PATH: process.execPath,
|
|
152
152
|
CODEXHOST_DEFAULT_AGENT: 'codex' };
|
|
153
153
|
const block = Buffer.from(Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join('\0') + '\0\0', 'utf16le').toString('base64');
|
|
154
|
-
console.log('[Harness Mix] Restarting Codex Desktop
|
|
154
|
+
console.log('[Harness Mix] Restarting Codex Desktop: official Codex passthrough + Harness Mix routes.');
|
|
155
155
|
stopDesktopProcesses(installation);
|
|
156
156
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
157
157
|
sweepLeftovers(root, dataDir);
|
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
readState,
|
|
22
22
|
} = require('./update-state');
|
|
23
23
|
const { nativeEnvironment } = require('./config');
|
|
24
|
+
const { CodexAccountManager } = require('./codex-accounts');
|
|
24
25
|
|
|
25
26
|
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
26
27
|
const PACKAGE_JSON_PATH = path.join(REPO_ROOT, 'package.json');
|
|
@@ -33,7 +34,6 @@ const routeModel = harnessId => ['pi', 'claude-code', 'deepseek-harness', 'antig
|
|
|
33
34
|
: `codexhost/plugin-v1@${Buffer.from(JSON.stringify({ harnessId })).toString('hex')}`;
|
|
34
35
|
const terminal = status => ['completed', 'cancelled', 'error', 'failed'].includes(status);
|
|
35
36
|
const turnStatus = status => ({ cancelled: 'interrupted', error: 'failed', failed: 'failed', completed: 'completed' }[status] || 'inProgress');
|
|
36
|
-
|
|
37
37
|
const HARNESS_INSTALL_COMMANDS = {
|
|
38
38
|
qoder: { win32: 'npm install -g @qoder-ai/qodercli', default: 'npm install -g @qoder-ai/qodercli' },
|
|
39
39
|
codex: { win32: 'npm install -g @openai/codex', default: 'npm install -g @openai/codex' },
|
|
@@ -193,10 +193,16 @@ class NativeProtocol {
|
|
|
193
193
|
this.runtime = runtime;
|
|
194
194
|
this.emit = emit;
|
|
195
195
|
this.requestOfficial = requestOfficial;
|
|
196
|
+
this.codexAccounts = new CodexAccountManager({
|
|
197
|
+
dataDirectory: nativeEnvironment().CODEXHOST_DATA_DIR,
|
|
198
|
+
requestOfficial,
|
|
199
|
+
emit,
|
|
200
|
+
});
|
|
196
201
|
this.approvals = new Map();
|
|
197
202
|
this.published = new Map();
|
|
198
203
|
this.steering = new Map(); // threadId -> in-flight steer promise
|
|
199
204
|
this.steerReceipts = new Map(); // `${threadId}\0${clientUserMessageId}` -> bounded delivery receipt
|
|
205
|
+
this.queues = new Map(); // threadId -> Array<{ id, input, clientUserMessageId, createdAt }>
|
|
200
206
|
this.unsubscribe = runtime.core.subscribe(({ event, projected }) => this.onCore(event, projected));
|
|
201
207
|
// Host 侧新建的线程(协作子任务等)也要通知 Desktop 侧栏,与 thread/start 同一契约
|
|
202
208
|
this.unsubscribeRuntime = runtime.subscribe(event => {
|
|
@@ -206,6 +212,13 @@ class NativeProtocol {
|
|
|
206
212
|
}
|
|
207
213
|
});
|
|
208
214
|
}
|
|
215
|
+
getQueue(threadId) {
|
|
216
|
+
if (!this.queues.has(threadId)) this.queues.set(threadId, []);
|
|
217
|
+
return this.queues.get(threadId);
|
|
218
|
+
}
|
|
219
|
+
emitQueueChanged(threadId) {
|
|
220
|
+
this.emit({ method: 'thread/queue/changed', params: { threadId } });
|
|
221
|
+
}
|
|
209
222
|
thread(id) { return this.runtime.threads.find(t => t.id === id); }
|
|
210
223
|
owns(id) { return Boolean(this.thread(id)); }
|
|
211
224
|
turn(turn) { return { id: turn.id, status: turnStatus(turn.status), error: turn.error ? { message: String(turn.error), codexErrorInfo: null, additionalDetails: null } : null,
|
|
@@ -318,12 +331,33 @@ class NativeProtocol {
|
|
|
318
331
|
return model;
|
|
319
332
|
}
|
|
320
333
|
async request(method, params = {}) {
|
|
334
|
+
if (method === 'codexhost/integrations/catalog') return this.runtime.integrations.catalog();
|
|
335
|
+
if (method === 'codexhost/integrations/list') return this.runtime.integrations.list(params);
|
|
336
|
+
if (method === 'codexhost/integrations/mcp/save') return this.runtime.integrations.save(params);
|
|
337
|
+
if (method === 'codexhost/integrations/mcp/remove') return this.runtime.integrations.remove(params);
|
|
338
|
+
if (method === 'codexhost/integrations/skill/change') return this.runtime.integrations.skillChange(params);
|
|
321
339
|
if (method === 'codexhost/harness/session-import/sources') return this.runtime.history.sources();
|
|
322
340
|
if (method === 'codexhost/harness/session-import/list') return this.runtime.history.list(params);
|
|
323
341
|
if (method === 'codexhost/harness/session-import/import') return this.runtime.history.import(params);
|
|
324
342
|
if (method === 'codexhost/collaboration/agents') return [...this.runtime.adapters.values()].map(a => ({ id: externalId(a.manifest.id), name: a.manifest.name, available: !!this.runtime.status[a.manifest.id]?.available, lead: a.manifest.capabilities?.collaborationTools === true }));
|
|
325
343
|
const thread = this.thread(params.threadId);
|
|
326
|
-
if (method === 'harness-mix/runtime/inspect') return {
|
|
344
|
+
if (method === 'harness-mix/runtime/inspect') return {
|
|
345
|
+
owner: 'harness-mix',
|
|
346
|
+
runtime: 'src/main/host/runtime.js',
|
|
347
|
+
core: 'src/main/protocol-core/protocol-core.js',
|
|
348
|
+
threads: this.runtime.threads.length,
|
|
349
|
+
codex: {
|
|
350
|
+
mode: 'official-passthrough',
|
|
351
|
+
// `codex` remains owned by the stock app-server. Only the explicit
|
|
352
|
+
// `codex-harness` route creates a HostRuntime-managed Codex worker.
|
|
353
|
+
managedRoute: 'codex-harness',
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
if (method === 'harness-mix/runtime/version') {
|
|
357
|
+
let version = '0.0.0';
|
|
358
|
+
try { version = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')).version || version; } catch {}
|
|
359
|
+
return { version };
|
|
360
|
+
}
|
|
327
361
|
// Updates
|
|
328
362
|
if (method === 'codexhost/update/check') {
|
|
329
363
|
const dataDir = nativeEnvironment().CODEXHOST_DATA_DIR;
|
|
@@ -429,8 +463,22 @@ class NativeProtocol {
|
|
|
429
463
|
return { status: null };
|
|
430
464
|
}
|
|
431
465
|
|
|
432
|
-
//
|
|
433
|
-
|
|
466
|
+
// Each extra Account is an isolated native CODEX_HOME. Harness Mix stores
|
|
467
|
+
// only the profile label/path; auth.json and token refresh remain owned by Codex.
|
|
468
|
+
if (method === 'codexhost/account/list') return this.codexAccounts.list(false);
|
|
469
|
+
if (method === 'codexhost/account/refresh') return this.codexAccounts.list(true);
|
|
470
|
+
if (method === 'codexhost/account/create') return this.codexAccounts.create(params.label);
|
|
471
|
+
if (method === 'codexhost/account/delete') return this.codexAccounts.delete(params.accountId);
|
|
472
|
+
if (method === 'codexhost/account/activate') return this.codexAccounts.activate(params.accountId);
|
|
473
|
+
if (method === 'codexhost/account/usage/inspect') return this.codexAccounts.usage(params.accountId);
|
|
474
|
+
if (method === 'codexhost/account/login/start') return this.codexAccounts.startLogin(params.accountId);
|
|
475
|
+
if (method === 'codexhost/account/login/cancel') return this.codexAccounts.cancelLogin(params.loginId);
|
|
476
|
+
if (method === 'codexhost/account/logout') {
|
|
477
|
+
if (!this.requestOfficial) throw new Error('Official Codex logout is unavailable');
|
|
478
|
+
await this.requestOfficial('account/logout', undefined);
|
|
479
|
+
return this.codexAccounts.signedOutOfficial();
|
|
480
|
+
}
|
|
481
|
+
if (method === 'codexhost/account/rate-limit-reset/consume') return this.codexAccounts.consumeReset(params.accountId, params.idempotencyKey);
|
|
434
482
|
if (method === 'codexhost/harness/account/login') {
|
|
435
483
|
const extId = params.harnessId;
|
|
436
484
|
const meta = HARNESS_AUTH_INFO[extId] || HARNESS_AUTH_INFO[ALIASES[extId]];
|
|
@@ -571,15 +619,22 @@ class NativeProtocol {
|
|
|
571
619
|
}
|
|
572
620
|
if (method === 'thread/start') {
|
|
573
621
|
const route = decodeRoute(params.model);
|
|
574
|
-
|
|
575
|
-
|
|
622
|
+
const accountContext = typeof params.__codexhostAccountId === 'string'
|
|
623
|
+
? this.codexAccounts.executionContext(params.__codexhostAccountId) : null;
|
|
624
|
+
if (!route && !accountContext) return undefined;
|
|
625
|
+
const effectiveRoute = route || { harnessId: 'codex-harness', ...(typeof params.model === 'string' ? { model: { id: params.model } } : {}) };
|
|
626
|
+
const id = ALIASES[effectiveRoute.harnessId] || effectiveRoute.harnessId;
|
|
576
627
|
if (!this.runtime.adapters.has(id)) throw new Error(`Unsupported Harness: ${id}`);
|
|
577
628
|
if (params.ephemeral && params.threadSource) throw new Error('Ephemeral background thread is not supported');
|
|
578
629
|
const isWorktree = params.worktree === true || params.options?.worktree === true;
|
|
630
|
+
const selectedModel = accountContext && typeof params.model === 'string'
|
|
631
|
+
? (await this.runtime.describe(id)).models.find(model => model.id === params.model)
|
|
632
|
+
: await this.resolveModel(id, effectiveRoute.model);
|
|
579
633
|
const created = await this.runtime.createThread({ harnessId: id, cwd: params.cwd, ephemeral: params.ephemeral === true,
|
|
580
634
|
worktree: isWorktree,
|
|
581
635
|
options: {
|
|
582
|
-
model:
|
|
636
|
+
model: selectedModel, thinking: effectiveRoute.thinkingOptionId, permissionMode: effectiveRoute.permissionModeId,
|
|
637
|
+
...(accountContext || {}),
|
|
583
638
|
...(isWorktree ? { worktree: true } : {}),
|
|
584
639
|
} });
|
|
585
640
|
if (created.error) throw new Error(created.error);
|
|
@@ -597,9 +652,83 @@ class NativeProtocol {
|
|
|
597
652
|
return undefined;
|
|
598
653
|
}
|
|
599
654
|
if (method === 'thread/read') return { thread: this.projectThread(thread, params.includeTurns !== false) };
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
655
|
+
if (method === 'thread/queue/list') {
|
|
656
|
+
const queue = this.getQueue(thread.id);
|
|
657
|
+
return {
|
|
658
|
+
data: queue.map(item => ({
|
|
659
|
+
id: item.id,
|
|
660
|
+
input: item.input,
|
|
661
|
+
clientUserMessageId: item.clientUserMessageId,
|
|
662
|
+
createdAt: item.createdAt,
|
|
663
|
+
})),
|
|
664
|
+
nextCursor: null,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (method === 'thread/queue/add') {
|
|
668
|
+
const input = Array.isArray(params.input) ? params.input : [];
|
|
669
|
+
if (!input.length) throw new Error('Queue submission requires non-empty input');
|
|
670
|
+
const queue = this.getQueue(thread.id);
|
|
671
|
+
const submission = {
|
|
672
|
+
id: randomUUID(),
|
|
673
|
+
input,
|
|
674
|
+
clientUserMessageId: typeof params.clientUserMessageId === 'string' && params.clientUserMessageId ? params.clientUserMessageId : null,
|
|
675
|
+
createdAt: Date.now(),
|
|
676
|
+
};
|
|
677
|
+
queue.push(submission);
|
|
678
|
+
this.emitQueueChanged(thread.id);
|
|
679
|
+
return { queuedSubmission: submission };
|
|
680
|
+
}
|
|
681
|
+
if (method === 'thread/queue/delete') {
|
|
682
|
+
const queue = this.getQueue(thread.id);
|
|
683
|
+
const idx = queue.findIndex(item => item.id === params.queuedSubmissionId);
|
|
684
|
+
if (idx !== -1) {
|
|
685
|
+
queue.splice(idx, 1);
|
|
686
|
+
this.emitQueueChanged(thread.id);
|
|
687
|
+
return { deleted: true };
|
|
688
|
+
}
|
|
689
|
+
return { deleted: false };
|
|
690
|
+
}
|
|
691
|
+
if (method === 'thread/queue/update') {
|
|
692
|
+
const queue = this.getQueue(thread.id);
|
|
693
|
+
const item = queue.find(it => it.id === params.queuedSubmissionId);
|
|
694
|
+
if (!item) throw new Error('Queued submission not found');
|
|
695
|
+
if (Array.isArray(params.input)) item.input = params.input;
|
|
696
|
+
this.emitQueueChanged(thread.id);
|
|
697
|
+
return { queuedSubmission: item };
|
|
698
|
+
}
|
|
699
|
+
if (method === 'thread/queue/reorder') {
|
|
700
|
+
const queue = this.getQueue(thread.id);
|
|
701
|
+
const ids = Array.isArray(params.queuedSubmissionIds) ? params.queuedSubmissionIds : [];
|
|
702
|
+
const map = new Map(queue.map(item => [item.id, item]));
|
|
703
|
+
const reordered = ids.map(id => map.get(id)).filter(Boolean);
|
|
704
|
+
for (const item of queue) {
|
|
705
|
+
if (!ids.includes(item.id)) reordered.push(item);
|
|
706
|
+
}
|
|
707
|
+
this.queues.set(thread.id, reordered);
|
|
708
|
+
this.emitQueueChanged(thread.id);
|
|
709
|
+
return {};
|
|
710
|
+
}
|
|
711
|
+
if (method === 'thread/queue/start') {
|
|
712
|
+
const queue = this.getQueue(thread.id);
|
|
713
|
+
const idx = params.queuedSubmissionId
|
|
714
|
+
? queue.findIndex(item => item.id === params.queuedSubmissionId)
|
|
715
|
+
: 0;
|
|
716
|
+
if (idx === -1 || !queue[idx]) throw new Error('Queued submission not found');
|
|
717
|
+
const [submission] = queue.splice(idx, 1);
|
|
718
|
+
this.emitQueueChanged(thread.id);
|
|
719
|
+
const { text, attachments } = await prepareInput(submission.input, thread.cwd);
|
|
720
|
+
if (attachments.length && !this.runtime.getCapabilities(thread.harnessId).conversation.attachments) {
|
|
721
|
+
throw new Error('当前 Harness 不支持图片附件');
|
|
722
|
+
}
|
|
723
|
+
if (this.runtime.execution.isRunning(thread.id)) {
|
|
724
|
+
// 正在执行中:打断当前回合并立即执行该排队消息
|
|
725
|
+
const turnId = await this.steerExclusive(thread, thread.currentTurn?.id, text, attachments);
|
|
726
|
+
const started = this.runtime.core.getTurn(turnId) || thread.currentTurn;
|
|
727
|
+
return { turn: this.turn(started) };
|
|
728
|
+
}
|
|
729
|
+
const turn = await this.startNativeTurn(thread, text, attachments);
|
|
730
|
+
return { turn: this.turn(turn) };
|
|
731
|
+
}
|
|
603
732
|
if (method === 'thread/metadata/update') return { thread: this.projectThread(await this.runtime.updateThreadMetadata(thread.id, params.gitInfo)) };
|
|
604
733
|
if (method === 'thread/section/move') {
|
|
605
734
|
if (params.sectionId !== null && (typeof params.sectionId !== 'string' || !params.sectionId)) throw new Error('Invalid sectionId');
|
|
@@ -660,7 +789,17 @@ class NativeProtocol {
|
|
|
660
789
|
}
|
|
661
790
|
if (method === 'thread/rollback') return { thread: this.projectThread(await this.runtime.rollbackThread(thread.id, params.numTurns)) };
|
|
662
791
|
if (method === 'thread/name/set') { await this.runtime.renameThread(thread.id, params.name); return {}; }
|
|
663
|
-
if (method === 'thread/archive' || method === 'thread/unarchive') {
|
|
792
|
+
if (method === 'thread/archive' || method === 'thread/unarchive') {
|
|
793
|
+
const isArchive = method === 'thread/archive';
|
|
794
|
+
await this.runtime.setThreadArchived(thread.id, isArchive);
|
|
795
|
+
if (isArchive) {
|
|
796
|
+
this.emit({ method: 'thread/archived', params: { threadId: thread.id } });
|
|
797
|
+
return {};
|
|
798
|
+
}
|
|
799
|
+
const projected = this.projectThread(thread);
|
|
800
|
+
this.emit({ method: 'thread/unarchived', params: { thread: projected } });
|
|
801
|
+
return { thread: projected };
|
|
802
|
+
}
|
|
664
803
|
if (method === 'thread/loaded/list') return { data: [...this.runtime.sessions.keys()] };
|
|
665
804
|
if (method === 'codexhost/thread/usage/inspect') {
|
|
666
805
|
const usage = params.refresh === 'exact' ? await this.runtime.refreshUsage(thread.id) : this.runtime.core.getThread(thread.id)?.usage;
|
|
@@ -697,8 +836,12 @@ class NativeProtocol {
|
|
|
697
836
|
}
|
|
698
837
|
// 原地切换 Harness:会话历史保留,下条消息携带一次性上下文信封(/switch 指令的 RPC 等价物)
|
|
699
838
|
if (method === 'codexhost/thread/harness/switch') {
|
|
700
|
-
await this.runtime.switchHarness(thread.id, ALIASES[params.harnessId] || params.harnessId, {
|
|
701
|
-
|
|
839
|
+
await this.runtime.switchHarness(thread.id, ALIASES[params.harnessId] || params.harnessId, {
|
|
840
|
+
note: typeof params.note === 'string' ? params.note : undefined,
|
|
841
|
+
intent: params.intent,
|
|
842
|
+
includes: params.includes,
|
|
843
|
+
});
|
|
844
|
+
return { threadId: thread.id, checkpointId: thread.pendingHandoff.checkpointId };
|
|
702
845
|
}
|
|
703
846
|
if (method === 'thread/fork' || method === 'codexhost/thread/fork') {
|
|
704
847
|
if (params.ephemeral || params.threadSource || params.excludeTurns) {
|
|
@@ -760,6 +903,25 @@ class NativeProtocol {
|
|
|
760
903
|
if (Array.isArray(projected.turn.itemIds)) {
|
|
761
904
|
for (const id of projected.turn.itemIds) this.published.delete(id);
|
|
762
905
|
}
|
|
906
|
+
for (const [id, app] of this.approvals.entries()) {
|
|
907
|
+
if (app.threadId === threadId) this.approvals.delete(id);
|
|
908
|
+
}
|
|
909
|
+
if (projected.turn.status === 'completed') {
|
|
910
|
+
setTimeout(async () => {
|
|
911
|
+
const queue = this.getQueue(threadId);
|
|
912
|
+
const currentThread = this.thread(threadId);
|
|
913
|
+
if (queue.length > 0 && currentThread && !this.runtime.execution.isRunning(threadId) && !this.steering.has(threadId) && !this.runtime.sending?.has(threadId)) {
|
|
914
|
+
const [submission] = queue.splice(0, 1);
|
|
915
|
+
this.emitQueueChanged(threadId);
|
|
916
|
+
try {
|
|
917
|
+
const { text, attachments } = await prepareInput(submission.input, currentThread.cwd);
|
|
918
|
+
await this.startNativeTurn(currentThread, text, attachments);
|
|
919
|
+
} catch (err) {
|
|
920
|
+
console.error(`Failed to auto-drain queued submission for thread ${threadId}:`, err);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}, 2500);
|
|
924
|
+
}
|
|
763
925
|
}
|
|
764
926
|
}
|
|
765
927
|
async startNativeTurn(thread, text, attachments, commandId) {
|
|
@@ -831,6 +993,6 @@ class NativeProtocol {
|
|
|
831
993
|
this.approvals.delete(message.id);
|
|
832
994
|
return true;
|
|
833
995
|
}
|
|
834
|
-
close() { this.unsubscribe(); this.unsubscribeRuntime(); }
|
|
996
|
+
close() { this.codexAccounts.close(); this.unsubscribe(); this.unsubscribeRuntime(); }
|
|
835
997
|
}
|
|
836
998
|
module.exports = { NativeProtocol, decodeRoute, projectItem, externalId, routeModel };
|
|
@@ -8,7 +8,7 @@ const PATTERNS = [
|
|
|
8
8
|
/xox[abpsr]-[A-Za-z0-9-]{10,}/g,
|
|
9
9
|
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, // JWT
|
|
10
10
|
];
|
|
11
|
-
const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|credential|cookie)/i;
|
|
11
|
+
const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|credential|cookie|userCode|loginId|verificationUrl)/i;
|
|
12
12
|
const REDACTED = '[redacted]';
|
|
13
13
|
|
|
14
14
|
function redactText(value) {
|