@kontextmind/kxm 0.6.0 → 0.7.10
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/.claude-plugin/marketplace.json +1 -1
- package/.kxm/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +5 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +135 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +41620 -35578
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +8218 -2328
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +103 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/browser.ts +603 -0
- package/plugins/kxm/src/cli/context-skills.ts +373 -0
- package/plugins/kxm/src/cli/hub.ts +614 -0
- package/plugins/kxm/src/cli/roles.ts +615 -0
- package/plugins/kxm/src/cli/system.ts +906 -0
- package/plugins/kxm/src/cli/tasks.ts +364 -0
- package/plugins/kxm/src/cli/types.ts +270 -0
- package/plugins/kxm/src/cli/vnext.ts +698 -0
- package/plugins/kxm/src/cli/workflows.ts +699 -0
- package/plugins/kxm/src/cli.ts +362 -2849
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/modes.ts +348 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/ssh-remote.ts +560 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +336 -84
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/modes.schema.json +56 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { win32 as win32Path } from "node:path";
|
|
4
|
+
import { defaultSpawn } from "./vnext-oneshot-process.ts";
|
|
2
5
|
|
|
3
6
|
export const DEFAULT_HARNESS = "pi";
|
|
4
7
|
export type HarnessMode = "headless" | "either";
|
|
@@ -27,6 +30,7 @@ export interface OneShotParsedOutput {
|
|
|
27
30
|
text: string;
|
|
28
31
|
usage?: OneShotUsage | undefined;
|
|
29
32
|
outcome?: string | undefined;
|
|
33
|
+
effectiveModel?: string | undefined;
|
|
30
34
|
isError?: boolean | undefined;
|
|
31
35
|
errorMessage?: string | undefined;
|
|
32
36
|
}
|
|
@@ -35,7 +39,7 @@ export interface HarnessOneShotConfig {
|
|
|
35
39
|
argv: readonly string[];
|
|
36
40
|
promptVia: OneShotPromptVia;
|
|
37
41
|
outputFormat: "json" | "text" | "stream-json" | "jsonl";
|
|
38
|
-
usageParser: (stdout: string, stderr: string) => OneShotParsedOutput;
|
|
42
|
+
usageParser: (stdout: string, stderr: string, requestedModel?: string) => OneShotParsedOutput;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface HarnessCatalogEntry {
|
|
@@ -54,10 +58,20 @@ export interface HarnessCatalogEntry {
|
|
|
54
58
|
oneShot?: HarnessOneShotConfig | undefined;
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
export type HarnessRunCommand = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult;
|
|
62
|
+
export type HarnessRunCommandAsync = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult | Promise<HarnessCommandResult>;
|
|
63
|
+
|
|
57
64
|
export interface HarnessProbeOptions {
|
|
58
|
-
env?: NodeJS.ProcessEnv;
|
|
59
|
-
runCommand?:
|
|
60
|
-
timeoutMs?: number;
|
|
65
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
66
|
+
runCommand?: HarnessRunCommand | undefined;
|
|
67
|
+
timeoutMs?: number | undefined;
|
|
68
|
+
platform?: NodeJS.Platform | undefined;
|
|
69
|
+
existsSync?: ((path: string) => boolean) | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AsyncHarnessProbeOptions extends Omit<HarnessProbeOptions, "runCommand"> {
|
|
73
|
+
signal?: AbortSignal | undefined;
|
|
74
|
+
runCommand?: HarnessRunCommandAsync | undefined;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
export interface HarnessDispatchStatus {
|
|
@@ -73,6 +87,7 @@ export interface HarnessStatus {
|
|
|
73
87
|
mode: HarnessMode;
|
|
74
88
|
detected: boolean;
|
|
75
89
|
authenticated: boolean | null;
|
|
90
|
+
authMethod?: "claude.ai" | "ChatGPT" | "api-key" | "antigravity-oauth" | undefined;
|
|
76
91
|
dispatch?: HarnessDispatchStatus | undefined;
|
|
77
92
|
command?: string;
|
|
78
93
|
version?: string;
|
|
@@ -85,10 +100,10 @@ export interface HarnessInventory {
|
|
|
85
100
|
harnesses: readonly HarnessStatus[];
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
const UNKNOWN_AUTH_HARNESSES = new Set(["
|
|
103
|
+
const UNKNOWN_AUTH_HARNESSES = new Set(["deepseek"]);
|
|
89
104
|
const GROK_LOGIN_LINE = "You are logged in with grok.com.";
|
|
90
105
|
/** Tab-separated `id<TAB>label` rows from the committed `agy models` probe. */
|
|
91
|
-
const AGY_MODEL_ROW = /^[a-z0-9][a-z0-9
|
|
106
|
+
const AGY_MODEL_ROW = /^[a-z0-9]+(?:[._-][a-z0-9]+)+\s+\S/im;
|
|
92
107
|
const CODEX_CHATGPT_LINE = "Logged in using ChatGPT";
|
|
93
108
|
const CODEX_API_KEY_PREFIX = "Logged in using an API key";
|
|
94
109
|
const CODEX_NEGATIVE_LINE = "Not logged in";
|
|
@@ -99,7 +114,6 @@ export const NATIVE_HARNESS_PROVIDERS: Readonly<Record<string, string>> = Object
|
|
|
99
114
|
codex: "openai",
|
|
100
115
|
grok: "xai",
|
|
101
116
|
agy: "google",
|
|
102
|
-
gemini: "google",
|
|
103
117
|
kimi: "moonshot",
|
|
104
118
|
deepseek: "deepseek",
|
|
105
119
|
});
|
|
@@ -143,7 +157,11 @@ export interface HarnessUpdateStep {
|
|
|
143
157
|
detail?: string;
|
|
144
158
|
}
|
|
145
159
|
|
|
146
|
-
|
|
160
|
+
function reportedModelId(value: unknown): string | undefined {
|
|
161
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9._:/-]{0,199}$/i.test(value) ? value : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function parseClaudeOneShotUsage(stdout: string, stderr: string, requestedModel?: string): OneShotParsedOutput {
|
|
147
165
|
const trimmed = stdout.trim();
|
|
148
166
|
if (!trimmed) {
|
|
149
167
|
return {
|
|
@@ -155,26 +173,28 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
|
|
|
155
173
|
try {
|
|
156
174
|
const payload = JSON.parse(trimmed) as Record<string, unknown>;
|
|
157
175
|
const usage = (payload.usage as Record<string, unknown> | undefined) ?? {};
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
const entries = payload.modelUsage && typeof payload.modelUsage === "object" && !Array.isArray(payload.modelUsage)
|
|
177
|
+
? Object.entries(payload.modelUsage as Record<string, unknown>).filter(([, value]) => value && typeof value === "object") : [];
|
|
178
|
+
const matches = entries.filter(([model]) => model === requestedModel
|
|
179
|
+
|| (requestedModel && (model.startsWith(`${requestedModel}-`) || model.startsWith(`claude-${requestedModel}-`))));
|
|
180
|
+
const selected = matches.length === 1 ? matches[0] : entries.length === 1 ? entries[0] : undefined;
|
|
181
|
+
const modelUsageDetail = selected?.[1] as Record<string, unknown> | undefined;
|
|
182
|
+
const effectiveModel = reportedModelId(payload.model) ?? reportedModelId(selected?.[0]);
|
|
183
|
+
// Top-level usage is the attempt total; an auxiliary model is not the task's model.
|
|
184
|
+
const tokensIn = (typeof usage.input_tokens === "number" ? usage.input_tokens : undefined)
|
|
185
|
+
?? (typeof modelUsageDetail?.inputTokens === "number" ? modelUsageDetail.inputTokens : null);
|
|
186
|
+
const tokensOut = (typeof usage.output_tokens === "number" ? usage.output_tokens : undefined)
|
|
187
|
+
?? (typeof modelUsageDetail?.outputTokens === "number" ? modelUsageDetail.outputTokens : null);
|
|
188
|
+
const cacheReadTokens = (typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined)
|
|
189
|
+
?? (typeof modelUsageDetail?.cacheReadInputTokens === "number" ? modelUsageDetail.cacheReadInputTokens : null);
|
|
190
|
+
const cacheWriteTokens = (typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined)
|
|
191
|
+
?? (typeof modelUsageDetail?.cacheCreationInputTokens === "number" ? modelUsageDetail.cacheCreationInputTokens : null);
|
|
173
192
|
const costUsd = typeof payload.total_cost_usd === "number"
|
|
174
193
|
? payload.total_cost_usd
|
|
175
194
|
: (typeof modelUsageDetail?.costUSD === "number" ? modelUsageDetail.costUSD : null);
|
|
176
195
|
|
|
177
|
-
const isError = Boolean(payload.is_error || payload.error
|
|
196
|
+
const isError = Boolean(payload.is_error || payload.error
|
|
197
|
+
|| ["error", "aborted", "failed", "max_tokens", "length"].includes(String(payload.stopReason ?? payload.stop_reason ?? "")));
|
|
178
198
|
const text = typeof payload.result === "string" ? payload.result : (typeof payload.text === "string" ? payload.text : "");
|
|
179
199
|
const errorMessage = isError
|
|
180
200
|
? (typeof payload.error === "string"
|
|
@@ -186,6 +206,7 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
|
|
|
186
206
|
|
|
187
207
|
return {
|
|
188
208
|
text,
|
|
209
|
+
effectiveModel,
|
|
189
210
|
isError,
|
|
190
211
|
errorMessage,
|
|
191
212
|
usage: {
|
|
@@ -193,18 +214,25 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
|
|
|
193
214
|
tokensOut,
|
|
194
215
|
cacheReadTokens,
|
|
195
216
|
cacheWriteTokens,
|
|
196
|
-
contextTokens:
|
|
217
|
+
contextTokens: null,
|
|
197
218
|
costUsd,
|
|
198
219
|
},
|
|
199
220
|
};
|
|
200
221
|
} catch {
|
|
201
222
|
return {
|
|
202
223
|
text: trimmed,
|
|
224
|
+
isError: true,
|
|
225
|
+
errorMessage: "invalid Claude JSON response",
|
|
203
226
|
usage: {},
|
|
204
227
|
};
|
|
205
228
|
}
|
|
206
229
|
}
|
|
207
230
|
|
|
231
|
+
/** Grok's observed one-shot JSON uses the same usage/modelUsage envelope. */
|
|
232
|
+
export function parseGrokOneShotUsage(stdout: string, stderr: string, requestedModel?: string): OneShotParsedOutput {
|
|
233
|
+
return parseClaudeOneShotUsage(stdout, stderr, requestedModel);
|
|
234
|
+
}
|
|
235
|
+
|
|
208
236
|
export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotParsedOutput {
|
|
209
237
|
const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
210
238
|
if (lines.length === 0) {
|
|
@@ -238,7 +266,7 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
|
|
|
238
266
|
? ((ev.error as Record<string, unknown>).message as string)
|
|
239
267
|
: "turn_failed");
|
|
240
268
|
}
|
|
241
|
-
if (ev.item && typeof ev.item === "object" && (ev.item as Record<string, unknown>).type === "agent_message") {
|
|
269
|
+
if (ev.type === "item.completed" && ev.item && typeof ev.item === "object" && (ev.item as Record<string, unknown>).type === "agent_message") {
|
|
242
270
|
const t = (ev.item as Record<string, unknown>).text;
|
|
243
271
|
if (typeof t === "string") messageTexts.push(t);
|
|
244
272
|
}
|
|
@@ -247,7 +275,8 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
|
|
|
247
275
|
}
|
|
248
276
|
}
|
|
249
277
|
|
|
250
|
-
const text = messageTexts.
|
|
278
|
+
const text = (messageTexts.at(-1) ?? "").trim();
|
|
279
|
+
if (!events.some((event) => event.type === "turn.completed") || !text) hasError = true;
|
|
251
280
|
const tokensIn = typeof usage?.input_tokens === "number" ? usage.input_tokens : null;
|
|
252
281
|
const tokensOut = typeof usage?.output_tokens === "number" ? usage.output_tokens : null;
|
|
253
282
|
const cacheReadTokens = typeof usage?.cached_input_tokens === "number" ? usage.cached_input_tokens : null;
|
|
@@ -262,7 +291,7 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
|
|
|
262
291
|
tokensOut,
|
|
263
292
|
cacheReadTokens,
|
|
264
293
|
cacheWriteTokens,
|
|
265
|
-
contextTokens:
|
|
294
|
+
contextTokens: null,
|
|
266
295
|
costUsd: null,
|
|
267
296
|
},
|
|
268
297
|
};
|
|
@@ -279,7 +308,15 @@ export function parseGenericOneShotUsage(stdout: string, _stderr: string): OneSh
|
|
|
279
308
|
: (typeof rec.text === "string"
|
|
280
309
|
? rec.text
|
|
281
310
|
: (typeof rec.response === "string" ? rec.response : trimmed));
|
|
282
|
-
|
|
311
|
+
const u = rec.usage && typeof rec.usage === "object" ? rec.usage as Record<string, unknown> : {};
|
|
312
|
+
return { text, effectiveModel: reportedModelId(rec.model), isError: Boolean(rec.is_error || rec.error || rec.success === false), usage: {
|
|
313
|
+
tokensIn: typeof u.input_tokens === "number" ? u.input_tokens : null,
|
|
314
|
+
tokensOut: typeof u.output_tokens === "number" ? u.output_tokens : null,
|
|
315
|
+
cacheReadTokens: typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : null,
|
|
316
|
+
cacheWriteTokens: typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : null,
|
|
317
|
+
contextTokens: null,
|
|
318
|
+
costUsd: typeof rec.total_cost_usd === "number" ? rec.total_cost_usd : null,
|
|
319
|
+
} };
|
|
283
320
|
}
|
|
284
321
|
} catch {
|
|
285
322
|
// not json
|
|
@@ -381,6 +418,18 @@ export function parseAgyOneShotUsage(stdout: string, _stderr: string): OneShotPa
|
|
|
381
418
|
}
|
|
382
419
|
|
|
383
420
|
/** Built-in harnesses. Unknown ids fail closed. Model lists live in `.kxm/models/*.yaml`, not here. */
|
|
421
|
+
const READ_ONLY_ONESHOT_ARGS = Object.freeze({
|
|
422
|
+
claude: Object.freeze(["--tools", "Read,Glob,Grep", "--restricted", "--safe-mode", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands", "--no-session-persistence"]),
|
|
423
|
+
codex: Object.freeze(["--sandbox", "read-only", "--ignore-user-config", "-c", 'approval_policy="never"']),
|
|
424
|
+
grok: Object.freeze(["--sandbox", "read-only", "--permission-mode", "plan", "--tools", "Read,Glob,Grep", "--no-subagents", "--disable-web-search"]),
|
|
425
|
+
agy: Object.freeze(["--mode", "plan", "--sandbox", "--disable-slash-commands"]),
|
|
426
|
+
kimi: Object.freeze(["--plan"]),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
export function oneShotReadOnlyArgs(harness: string): readonly string[] | undefined {
|
|
430
|
+
return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness as keyof typeof READ_ONLY_ONESHOT_ARGS] : undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
384
433
|
export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
385
434
|
{
|
|
386
435
|
id: "pi",
|
|
@@ -414,7 +463,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
414
463
|
extensions: ["plugin", "update", "kxm", "-y"],
|
|
415
464
|
},
|
|
416
465
|
oneShot: {
|
|
417
|
-
argv: ["-p", "--output-format", "json"],
|
|
466
|
+
argv: ["-p", ...oneShotReadOnlyArgs("claude")!, "--output-format", "json"],
|
|
418
467
|
promptVia: "stdin",
|
|
419
468
|
outputFormat: "json",
|
|
420
469
|
usageParser: parseClaudeOneShotUsage,
|
|
@@ -430,7 +479,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
430
479
|
authArgs: ["provider", "list"],
|
|
431
480
|
update: { self: ["upgrade"] },
|
|
432
481
|
oneShot: {
|
|
433
|
-
argv: ["--output-format", "stream-json", "-p"],
|
|
482
|
+
argv: [...oneShotReadOnlyArgs("kimi")!, "--output-format", "stream-json", "-p"],
|
|
434
483
|
promptVia: "arg",
|
|
435
484
|
outputFormat: "stream-json",
|
|
436
485
|
usageParser: parseKimiOneShotUsage,
|
|
@@ -446,21 +495,12 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
446
495
|
authArgs: ["login", "status"],
|
|
447
496
|
update: { self: ["update"] },
|
|
448
497
|
oneShot: {
|
|
449
|
-
argv: ["exec", "--json", "-"],
|
|
498
|
+
argv: ["exec", ...oneShotReadOnlyArgs("codex")!, "--json", "-"],
|
|
450
499
|
promptVia: "stdin",
|
|
451
500
|
outputFormat: "json",
|
|
452
501
|
usageParser: parseCodexOneShotUsage,
|
|
453
502
|
},
|
|
454
503
|
},
|
|
455
|
-
{
|
|
456
|
-
id: "gemini",
|
|
457
|
-
label: "Gemini CLI",
|
|
458
|
-
default: false,
|
|
459
|
-
mode: "either",
|
|
460
|
-
commands: ["gemini"],
|
|
461
|
-
versionArgs: ["--version"],
|
|
462
|
-
update: { self: ["update"] },
|
|
463
|
-
},
|
|
464
504
|
{
|
|
465
505
|
id: "deepseek",
|
|
466
506
|
label: "DeepSeek",
|
|
@@ -486,10 +526,10 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
486
526
|
authArgs: ["models"],
|
|
487
527
|
update: { self: ["update"] },
|
|
488
528
|
oneShot: {
|
|
489
|
-
argv: ["--output-format", "json"],
|
|
490
|
-
promptVia: "
|
|
529
|
+
argv: [...oneShotReadOnlyArgs("grok")!, "--output-format", "json", "--single"],
|
|
530
|
+
promptVia: "arg",
|
|
491
531
|
outputFormat: "json",
|
|
492
|
-
usageParser:
|
|
532
|
+
usageParser: parseGrokOneShotUsage,
|
|
493
533
|
},
|
|
494
534
|
},
|
|
495
535
|
{
|
|
@@ -502,7 +542,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
502
542
|
authArgs: ["models"],
|
|
503
543
|
update: { self: ["update"] },
|
|
504
544
|
oneShot: {
|
|
505
|
-
argv: ["--output-format", "json", "-p"],
|
|
545
|
+
argv: [...oneShotReadOnlyArgs("agy")!, "--output-format", "json", "-p"],
|
|
506
546
|
promptVia: "arg",
|
|
507
547
|
outputFormat: "json",
|
|
508
548
|
usageParser: parseAgyOneShotUsage,
|
|
@@ -518,7 +558,55 @@ export function isKnownHarnessId(id: string, extra: Iterable<string> = []): bool
|
|
|
518
558
|
return false;
|
|
519
559
|
}
|
|
520
560
|
|
|
521
|
-
|
|
561
|
+
/** Bare catalog command ids only; never a path or shell metacharacter. */
|
|
562
|
+
export const SAFE_HARNESS_COMMAND_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
563
|
+
|
|
564
|
+
export function harnessCommandCandidates(
|
|
565
|
+
command: string,
|
|
566
|
+
platform: NodeJS.Platform = process.platform,
|
|
567
|
+
): readonly string[] {
|
|
568
|
+
if (platform !== "win32" || !SAFE_HARNESS_COMMAND_ID.test(command)) return [command];
|
|
569
|
+
return [command, `${command}.exe`, `${command}.cmd`];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function isWindowsHarnessShim(command: string): boolean {
|
|
573
|
+
return /\.(cmd|bat)$/i.test(command.replace(/\\/g, "/").split("/").pop() ?? command);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Shell only for allowlisted `name.cmd` on win32. Bare ids stay spawn-without-shell. */
|
|
577
|
+
export function harnessSpawnUsesShell(command: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
578
|
+
if (platform !== "win32") return false;
|
|
579
|
+
const base = command.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
580
|
+
return /^[A-Za-z][A-Za-z0-9_-]*\.cmd$/i.test(base);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Allowlisted npm-global inner `.exe`, relative to the directory that contains `name.cmd`. */
|
|
584
|
+
export const WIN_NPM_INNER_EXE: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
585
|
+
claude: Object.freeze(["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"]),
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
export function findWinNpmInnerExe(
|
|
589
|
+
cliId: string,
|
|
590
|
+
options: {
|
|
591
|
+
platform?: NodeJS.Platform | undefined;
|
|
592
|
+
pathEnv?: string | undefined;
|
|
593
|
+
existsSync?: ((path: string) => boolean) | undefined;
|
|
594
|
+
} = {},
|
|
595
|
+
): string | undefined {
|
|
596
|
+
if ((options.platform ?? process.platform) !== "win32") return undefined;
|
|
597
|
+
const segments = WIN_NPM_INNER_EXE[cliId];
|
|
598
|
+
if (!segments) return undefined;
|
|
599
|
+
const exists = options.existsSync ?? existsSync;
|
|
600
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
601
|
+
for (const dir of pathEnv.split(";").filter(Boolean)) {
|
|
602
|
+
if (!exists(win32Path.join(dir, `${cliId}.cmd`))) continue;
|
|
603
|
+
const inner = win32Path.join(dir, ...segments);
|
|
604
|
+
if (exists(inner)) return inner;
|
|
605
|
+
}
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function defaultRunner(env: NodeJS.ProcessEnv): HarnessRunCommand {
|
|
522
610
|
return (command, args, timeoutMs) => {
|
|
523
611
|
try {
|
|
524
612
|
const result = spawnSync(command, [...args], {
|
|
@@ -526,6 +614,7 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
|
|
|
526
614
|
timeout: timeoutMs,
|
|
527
615
|
windowsHide: true,
|
|
528
616
|
env,
|
|
617
|
+
shell: harnessSpawnUsesShell(command),
|
|
529
618
|
});
|
|
530
619
|
if (result.error) {
|
|
531
620
|
const code = (result.error as NodeJS.ErrnoException).code;
|
|
@@ -543,6 +632,37 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
|
|
|
543
632
|
};
|
|
544
633
|
}
|
|
545
634
|
|
|
635
|
+
function commandResultFromSpawn(result: Awaited<ReturnType<typeof defaultSpawn>>): HarnessCommandResult {
|
|
636
|
+
const errno = result.error && "code" in result.error
|
|
637
|
+
? (result.error as NodeJS.ErrnoException).code
|
|
638
|
+
: undefined;
|
|
639
|
+
const error = (typeof errno === "string" && errno ? errno : undefined)
|
|
640
|
+
?? result.error?.message
|
|
641
|
+
?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
|
|
642
|
+
return {
|
|
643
|
+
ok: result.code === 0 && !error,
|
|
644
|
+
code: result.code,
|
|
645
|
+
stdout: result.stdout,
|
|
646
|
+
stderr: result.stderr,
|
|
647
|
+
...(error ? { error } : {}),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function defaultAsyncRunner(env: NodeJS.ProcessEnv, signal?: AbortSignal): HarnessRunCommandAsync {
|
|
652
|
+
return async (command, args, timeoutMs) => {
|
|
653
|
+
try {
|
|
654
|
+
return commandResultFromSpawn(await defaultSpawn(command, args, {
|
|
655
|
+
env,
|
|
656
|
+
timeoutMs,
|
|
657
|
+
signal,
|
|
658
|
+
shell: harnessSpawnUsesShell(command),
|
|
659
|
+
}));
|
|
660
|
+
} catch (error) {
|
|
661
|
+
return { ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : "spawn_failed" };
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
546
666
|
function firstLine(text: string): string | undefined {
|
|
547
667
|
const line = text.split(/\r?\n/).map((candidate) => candidate.trim()).find(Boolean);
|
|
548
668
|
return line && line.length <= 200 ? line : undefined;
|
|
@@ -588,7 +708,7 @@ function isCodexApiKeyLine(line: string): boolean {
|
|
|
588
708
|
return line === CODEX_API_KEY_PREFIX || line.startsWith(`${CODEX_API_KEY_PREFIX} - `);
|
|
589
709
|
}
|
|
590
710
|
|
|
591
|
-
function interpretAuth(id: string, result: HarnessCommandResult):
|
|
711
|
+
function interpretAuth(id: string, result: HarnessCommandResult): Pick<HarnessStatus, "authenticated" | "authMethod"> & { issues: string[] } {
|
|
592
712
|
const conflict = authConflictIssue(result);
|
|
593
713
|
if (conflict) return { authenticated: null, issues: [conflict] };
|
|
594
714
|
if (result.error) return { authenticated: null, issues: ["auth_probe_error"] };
|
|
@@ -597,7 +717,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
|
|
|
597
717
|
if (id === "claude") {
|
|
598
718
|
const payload = parseJsonObject(result.stdout);
|
|
599
719
|
if (payload && payload.loggedIn === false) return { authenticated: false, issues: ["not_authenticated"] };
|
|
600
|
-
if (commandSucceeded(result) && payload && payload.loggedIn === true) return {
|
|
720
|
+
if (commandSucceeded(result) && payload && payload.loggedIn === true) return {
|
|
721
|
+
authenticated: true, issues: [],
|
|
722
|
+
...(payload.authMethod === "claude.ai" || payload.authMethod === "api-key" ? { authMethod: payload.authMethod } : {}),
|
|
723
|
+
};
|
|
601
724
|
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
602
725
|
}
|
|
603
726
|
if (id === "codex") {
|
|
@@ -605,10 +728,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
|
|
|
605
728
|
return { authenticated: false, issues: ["not_authenticated"] };
|
|
606
729
|
}
|
|
607
730
|
if (commandSucceeded(result) && lines.some((line) => line === CODEX_CHATGPT_LINE)) {
|
|
608
|
-
return { authenticated: true, issues: [] };
|
|
731
|
+
return { authenticated: true, authMethod: "ChatGPT", issues: [] };
|
|
609
732
|
}
|
|
610
733
|
if (commandSucceeded(result) && lines.some((line) => isCodexApiKeyLine(line))) {
|
|
611
|
-
return { authenticated: true, issues: ["auth_api_key"] };
|
|
734
|
+
return { authenticated: true, authMethod: "api-key", issues: ["auth_api_key"] };
|
|
612
735
|
}
|
|
613
736
|
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
614
737
|
}
|
|
@@ -621,7 +744,7 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
|
|
|
621
744
|
if (!commandSucceeded(result) || !text.trim()) {
|
|
622
745
|
return { authenticated: false, issues: ["not_authenticated"] };
|
|
623
746
|
}
|
|
624
|
-
if (AGY_MODEL_ROW.test(text)) return { authenticated: true, issues: [] };
|
|
747
|
+
if (AGY_MODEL_ROW.test(text)) return { authenticated: true, authMethod: "antigravity-oauth", issues: [] };
|
|
625
748
|
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
626
749
|
}
|
|
627
750
|
if (id === "kimi") {
|
|
@@ -652,37 +775,81 @@ export function resolveDispatchStatus(
|
|
|
652
775
|
return {
|
|
653
776
|
status: "no",
|
|
654
777
|
supported: false,
|
|
655
|
-
reason:
|
|
778
|
+
reason: "no_headless_mode",
|
|
656
779
|
};
|
|
657
780
|
}
|
|
781
|
+
if (entry.id !== "pi" && !oneShotReadOnlyArgs(entry.id)) {
|
|
782
|
+
return { status: "no", supported: false, reason: "permission_profile_unaudited" };
|
|
783
|
+
}
|
|
658
784
|
if (authenticated === false) {
|
|
659
785
|
return { status: "no", supported: false, reason: "not_authenticated" };
|
|
660
786
|
}
|
|
661
787
|
if (authenticated === null) {
|
|
662
|
-
|
|
788
|
+
const authIssue = issues.find((issue) => issue === "auth_context_required" || issue === "auth_unknown" || issue === "auth_unparsed" || issue.startsWith("auth_"));
|
|
789
|
+
return { status: "no", supported: false, reason: authIssue ?? issues[0] ?? "auth_unknown" };
|
|
663
790
|
}
|
|
664
791
|
return { status: "yes", supported: true };
|
|
665
792
|
}
|
|
666
793
|
|
|
794
|
+
function tryHarnessCommand(
|
|
795
|
+
candidate: string,
|
|
796
|
+
entry: HarnessCatalogEntry,
|
|
797
|
+
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
798
|
+
timeoutMs: number,
|
|
799
|
+
): { command: string; version: string | undefined } | undefined {
|
|
800
|
+
const result = runCommand(candidate, entry.versionArgs, timeoutMs);
|
|
801
|
+
if (result.error === "ENOENT") return undefined;
|
|
802
|
+
if (result.error && result.code === null && !result.stdout && !result.stderr) return undefined;
|
|
803
|
+
return {
|
|
804
|
+
command: candidate,
|
|
805
|
+
version: firstLine(result.stdout) ?? firstLine(result.stderr),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function detectHarnessCommand(
|
|
810
|
+
entry: HarnessCatalogEntry,
|
|
811
|
+
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
812
|
+
timeoutMs: number,
|
|
813
|
+
platform: NodeJS.Platform,
|
|
814
|
+
probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
|
|
815
|
+
): { command: string; version: string | undefined } | undefined {
|
|
816
|
+
const candidates = entry.commands.flatMap((base) => [...harnessCommandCandidates(base, platform)]);
|
|
817
|
+
for (const candidate of candidates) {
|
|
818
|
+
if (isWindowsHarnessShim(candidate)) continue;
|
|
819
|
+
const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
|
|
820
|
+
if (found) return found;
|
|
821
|
+
}
|
|
822
|
+
const inner = findWinNpmInnerExe(entry.id, {
|
|
823
|
+
platform,
|
|
824
|
+
pathEnv: probe.env?.PATH ?? process.env.PATH ?? "",
|
|
825
|
+
existsSync: probe.existsSync,
|
|
826
|
+
});
|
|
827
|
+
if (inner) {
|
|
828
|
+
const found = tryHarnessCommand(inner, entry, runCommand, timeoutMs);
|
|
829
|
+
if (found) return found;
|
|
830
|
+
}
|
|
831
|
+
for (const candidate of candidates) {
|
|
832
|
+
if (!isWindowsHarnessShim(candidate)) continue;
|
|
833
|
+
const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
|
|
834
|
+
if (found) return found;
|
|
835
|
+
}
|
|
836
|
+
return undefined;
|
|
837
|
+
}
|
|
838
|
+
|
|
667
839
|
function probeEntry(
|
|
668
840
|
entry: HarnessCatalogEntry,
|
|
669
841
|
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
670
842
|
timeoutMs: number,
|
|
843
|
+
platform: NodeJS.Platform,
|
|
844
|
+
probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
|
|
671
845
|
): HarnessStatus {
|
|
672
846
|
const issues: string[] = [];
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const result = runCommand(candidate, entry.versionArgs, timeoutMs);
|
|
678
|
-
if (result.error === "ENOENT") continue;
|
|
679
|
-
if (result.error && result.code === null && !result.stdout && !result.stderr) continue;
|
|
680
|
-
detected = true;
|
|
681
|
-
command = candidate;
|
|
682
|
-
version = firstLine(result.stdout) ?? firstLine(result.stderr);
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
847
|
+
const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, probe);
|
|
848
|
+
const detected = Boolean(found);
|
|
849
|
+
const command = found?.command;
|
|
850
|
+
const version = found?.version;
|
|
685
851
|
let authenticated: boolean | null = null;
|
|
852
|
+
let authMethod: HarnessStatus["authMethod"];
|
|
686
853
|
if (!detected) authenticated = false;
|
|
687
854
|
else if (entry.id === "pi") {
|
|
688
855
|
authenticated = null;
|
|
@@ -691,10 +858,14 @@ function probeEntry(
|
|
|
691
858
|
authenticated = null;
|
|
692
859
|
if (UNKNOWN_AUTH_HARNESSES.has(entry.id)) issues.push("auth_unknown");
|
|
693
860
|
} else if (command) {
|
|
694
|
-
|
|
861
|
+
// Auth CLIs may initialize hooks/OAuth state and exceed the short binary-detection timeout.
|
|
862
|
+
// Keep detection bounded while allowing the documented auth probe to complete.
|
|
863
|
+
const parsed = interpretAuth(entry.id, runCommand(command, entry.authArgs, Math.max(timeoutMs, 10_000)));
|
|
695
864
|
authenticated = parsed.authenticated;
|
|
865
|
+
authMethod = parsed.authMethod;
|
|
696
866
|
issues.push(...parsed.issues);
|
|
697
867
|
}
|
|
868
|
+
if (command && isWindowsHarnessShim(command)) issues.push("windows_shim");
|
|
698
869
|
const dispatch = resolveDispatchStatus(entry, detected, authenticated, issues);
|
|
699
870
|
return {
|
|
700
871
|
id: entry.id,
|
|
@@ -703,6 +874,7 @@ function probeEntry(
|
|
|
703
874
|
mode: entry.mode,
|
|
704
875
|
detected,
|
|
705
876
|
authenticated,
|
|
877
|
+
...(authMethod ? { authMethod } : {}),
|
|
706
878
|
dispatch,
|
|
707
879
|
...(command ? { command } : {}),
|
|
708
880
|
...(version ? { version } : {}),
|
|
@@ -718,15 +890,28 @@ function probeEntry(
|
|
|
718
890
|
export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInventory {
|
|
719
891
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
720
892
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
893
|
+
const platform = options.platform ?? process.platform;
|
|
721
894
|
return {
|
|
722
895
|
defaultHarness: DEFAULT_HARNESS,
|
|
723
|
-
harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs)),
|
|
896
|
+
harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs, platform, options)),
|
|
724
897
|
};
|
|
725
898
|
}
|
|
726
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Inventory probe that never uses spawnSync. Product CLI/hub/extension paths
|
|
902
|
+
* must await this so capability/auth checks cannot block the event loop.
|
|
903
|
+
*/
|
|
904
|
+
export async function probeHarnessesAsync(options: AsyncHarnessProbeOptions = {}): Promise<HarnessInventory> {
|
|
905
|
+
return replayHarnessProbe(
|
|
906
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
907
|
+
(runCommand) => probeHarnesses({ ...options, runCommand }),
|
|
908
|
+
256,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
727
912
|
export function eligibleHarnesses(inventory: HarnessInventory): readonly string[] {
|
|
728
913
|
const eligible = inventory.harnesses
|
|
729
|
-
.filter((entry) => entry.detected && entry.authenticated === true)
|
|
914
|
+
.filter((entry) => entry !== undefined && entry.detected && entry.authenticated === true)
|
|
730
915
|
.map((entry) => entry.id);
|
|
731
916
|
if (eligible.length === 0) throw new Error("no_authenticated_harness");
|
|
732
917
|
return eligible;
|
|
@@ -785,7 +970,7 @@ export function validateHarnessModelPair(
|
|
|
785
970
|
return { valid: true };
|
|
786
971
|
}
|
|
787
972
|
|
|
788
|
-
if (harnessId === "agy"
|
|
973
|
+
if (harnessId === "agy") {
|
|
789
974
|
if (provider && provider !== "google") {
|
|
790
975
|
return { valid: false, issue: "harness_unhosted_model", message: `harness ${harnessId} does not host provider ${provider}` };
|
|
791
976
|
}
|
|
@@ -799,7 +984,7 @@ export function validateHarnessModelPair(
|
|
|
799
984
|
if (provider && provider !== "moonshot") {
|
|
800
985
|
return { valid: false, issue: "harness_unhosted_model", message: `harness kimi does not host provider ${provider}` };
|
|
801
986
|
}
|
|
802
|
-
if (model && !/^(kimi|moonshot)
|
|
987
|
+
if (model && !/^(kimi|moonshot)(?:-|$)/i.test(model) && !/^kimi-for-coding(?:-|$)/i.test(model)) {
|
|
803
988
|
return { valid: false, issue: "harness_unhosted_model", message: `harness kimi only hosts kimi/moonshot models, received ${model}` };
|
|
804
989
|
}
|
|
805
990
|
return { valid: true };
|
|
@@ -843,11 +1028,14 @@ export interface HarnessAssignmentProbeOptions {
|
|
|
843
1028
|
env?: NodeJS.ProcessEnv | undefined;
|
|
844
1029
|
runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult) | undefined;
|
|
845
1030
|
timeoutMs?: number | undefined;
|
|
1031
|
+
platform?: NodeJS.Platform | undefined;
|
|
1032
|
+
existsSync?: ((path: string) => boolean) | undefined;
|
|
846
1033
|
}
|
|
847
1034
|
|
|
848
1035
|
export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions): HarnessStatus {
|
|
849
1036
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
850
1037
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
1038
|
+
const platform = options.platform ?? process.platform;
|
|
851
1039
|
const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === options.harness);
|
|
852
1040
|
if (!entry) {
|
|
853
1041
|
return {
|
|
@@ -884,18 +1072,11 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
884
1072
|
}
|
|
885
1073
|
}
|
|
886
1074
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (result.error === "ENOENT") continue;
|
|
893
|
-
if (result.error && result.code === null && !result.stdout && !result.stderr) continue;
|
|
894
|
-
detected = true;
|
|
895
|
-
command = candidate;
|
|
896
|
-
version = firstLine(result.stdout) ?? firstLine(result.stderr);
|
|
897
|
-
break;
|
|
898
|
-
}
|
|
1075
|
+
const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, options);
|
|
1076
|
+
const detected = Boolean(found);
|
|
1077
|
+
const command = found?.command;
|
|
1078
|
+
const version = found?.version;
|
|
1079
|
+
const shimIssue = command && isWindowsHarnessShim(command) ? ["windows_shim"] : [];
|
|
899
1080
|
|
|
900
1081
|
if (!detected || !command) {
|
|
901
1082
|
return {
|
|
@@ -932,7 +1113,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
932
1113
|
extensions: Boolean(entry.update.extensions?.length),
|
|
933
1114
|
models: Boolean(entry.update.models?.length),
|
|
934
1115
|
},
|
|
935
|
-
issues: ["auth_context_required"],
|
|
1116
|
+
issues: ["auth_context_required", ...shimIssue],
|
|
936
1117
|
};
|
|
937
1118
|
}
|
|
938
1119
|
|
|
@@ -960,6 +1141,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
960
1141
|
authenticated = false;
|
|
961
1142
|
issues.push("not_authenticated");
|
|
962
1143
|
}
|
|
1144
|
+
issues.push(...shimIssue);
|
|
963
1145
|
|
|
964
1146
|
const dispatch = resolveDispatchStatus(entry, true, authenticated, issues);
|
|
965
1147
|
|
|
@@ -982,7 +1164,62 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
982
1164
|
};
|
|
983
1165
|
}
|
|
984
1166
|
|
|
985
|
-
return probeEntry(entry, runCommand, timeoutMs);
|
|
1167
|
+
return probeEntry(entry, runCommand, timeoutMs, platform);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export interface AsyncHarnessAssignmentProbeOptions extends Omit<HarnessAssignmentProbeOptions, "runCommand"> {
|
|
1171
|
+
signal?: AbortSignal | undefined;
|
|
1172
|
+
runCommand?: HarnessRunCommandAsync | undefined;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
class PendingHarnessCommand {
|
|
1176
|
+
command: string;
|
|
1177
|
+
args: readonly string[];
|
|
1178
|
+
timeoutMs: number;
|
|
1179
|
+
key: string;
|
|
1180
|
+
constructor(command: string, args: readonly string[], timeoutMs: number, key: string) {
|
|
1181
|
+
this.command = command;
|
|
1182
|
+
this.args = [...args];
|
|
1183
|
+
this.timeoutMs = timeoutMs;
|
|
1184
|
+
this.key = key;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async function replayHarnessProbe<T>(
|
|
1189
|
+
run: HarnessRunCommandAsync,
|
|
1190
|
+
execute: (runCommand: HarnessRunCommand) => T,
|
|
1191
|
+
limit: number,
|
|
1192
|
+
): Promise<T> {
|
|
1193
|
+
const observed = new Map<string, HarnessCommandResult>();
|
|
1194
|
+
for (let commands = 0; commands <= limit; commands++) {
|
|
1195
|
+
try {
|
|
1196
|
+
return execute((command, args, timeoutMs) => {
|
|
1197
|
+
const key = JSON.stringify([command, args, timeoutMs]);
|
|
1198
|
+
const result = observed.get(key);
|
|
1199
|
+
if (result) return result;
|
|
1200
|
+
throw new PendingHarnessCommand(command, args, timeoutMs, key);
|
|
1201
|
+
});
|
|
1202
|
+
} catch (pending) {
|
|
1203
|
+
if (!(pending instanceof PendingHarnessCommand)) throw pending;
|
|
1204
|
+
if (commands === limit) throw new Error("auth_probe_command_limit");
|
|
1205
|
+
observed.set(pending.key, await run(pending.command, pending.args, pending.timeoutMs));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
throw new Error("auth_probe_command_limit");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Execute the existing probe policy without blocking the Runtime event loop.
|
|
1213
|
+
* Replay pure policy decisions from per-call command observations, yielding for
|
|
1214
|
+
* every missing observation. This reuses the exact auth/model/brake parsers,
|
|
1215
|
+
* deduplicates repeated detection, and never caches auth across assignments.
|
|
1216
|
+
*/
|
|
1217
|
+
export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
|
|
1218
|
+
return replayHarnessProbe(
|
|
1219
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
1220
|
+
(runCommand) => probeHarnessAssignment({ ...options, runCommand }),
|
|
1221
|
+
32,
|
|
1222
|
+
);
|
|
986
1223
|
}
|
|
987
1224
|
|
|
988
1225
|
export function probeHarnessesForModel(
|
|
@@ -991,6 +1228,7 @@ export function probeHarnessesForModel(
|
|
|
991
1228
|
): HarnessInventory {
|
|
992
1229
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
993
1230
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
1231
|
+
const platform = options.platform ?? process.platform;
|
|
994
1232
|
return {
|
|
995
1233
|
defaultHarness: DEFAULT_HARNESS,
|
|
996
1234
|
harnesses: BUILTIN_HARNESSES.map((entry) =>
|
|
@@ -1000,11 +1238,25 @@ export function probeHarnessesForModel(
|
|
|
1000
1238
|
model: modelSpec.model,
|
|
1001
1239
|
runCommand,
|
|
1002
1240
|
timeoutMs,
|
|
1241
|
+
platform,
|
|
1242
|
+
env: options.env,
|
|
1243
|
+
existsSync: options.existsSync,
|
|
1003
1244
|
})
|
|
1004
1245
|
),
|
|
1005
1246
|
};
|
|
1006
1247
|
}
|
|
1007
1248
|
|
|
1249
|
+
export async function probeHarnessesForModelAsync(
|
|
1250
|
+
modelSpec: HarnessModelSpec,
|
|
1251
|
+
options: AsyncHarnessProbeOptions = {},
|
|
1252
|
+
): Promise<HarnessInventory> {
|
|
1253
|
+
return replayHarnessProbe(
|
|
1254
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
1255
|
+
(runCommand) => probeHarnessesForModel(modelSpec, { ...options, runCommand }),
|
|
1256
|
+
256,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1008
1260
|
function scopesFor(scope: HarnessUpdateScope, entry: HarnessCatalogEntry): Exclude<HarnessUpdateScope, "all">[] {
|
|
1009
1261
|
if (scope === "self") return ["self"];
|
|
1010
1262
|
if (scope === "extensions") return entry.update.extensions ? ["extensions"] : [];
|