@kontextmind/kxm 0.6.0
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 +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,1142 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_HARNESS = "pi";
|
|
4
|
+
export type HarnessMode = "headless" | "either";
|
|
5
|
+
export type HarnessUpdateScope = "self" | "extensions" | "models" | "all";
|
|
6
|
+
|
|
7
|
+
export interface HarnessCommandResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
code: number | null;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type OneShotPromptVia = "stdin" | "arg";
|
|
16
|
+
|
|
17
|
+
export interface OneShotUsage {
|
|
18
|
+
tokensIn?: number | null | undefined;
|
|
19
|
+
tokensOut?: number | null | undefined;
|
|
20
|
+
cacheReadTokens?: number | null | undefined;
|
|
21
|
+
cacheWriteTokens?: number | null | undefined;
|
|
22
|
+
contextTokens?: number | null | undefined;
|
|
23
|
+
costUsd?: number | null | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OneShotParsedOutput {
|
|
27
|
+
text: string;
|
|
28
|
+
usage?: OneShotUsage | undefined;
|
|
29
|
+
outcome?: string | undefined;
|
|
30
|
+
isError?: boolean | undefined;
|
|
31
|
+
errorMessage?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HarnessOneShotConfig {
|
|
35
|
+
argv: readonly string[];
|
|
36
|
+
promptVia: OneShotPromptVia;
|
|
37
|
+
outputFormat: "json" | "text" | "stream-json" | "jsonl";
|
|
38
|
+
usageParser: (stdout: string, stderr: string) => OneShotParsedOutput;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HarnessCatalogEntry {
|
|
42
|
+
id: string;
|
|
43
|
+
label: string;
|
|
44
|
+
default: boolean;
|
|
45
|
+
mode: HarnessMode;
|
|
46
|
+
commands: readonly string[];
|
|
47
|
+
versionArgs: readonly string[];
|
|
48
|
+
authArgs?: readonly string[];
|
|
49
|
+
update: {
|
|
50
|
+
self: readonly string[];
|
|
51
|
+
extensions?: readonly string[];
|
|
52
|
+
models?: readonly string[];
|
|
53
|
+
};
|
|
54
|
+
oneShot?: HarnessOneShotConfig | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface HarnessProbeOptions {
|
|
58
|
+
env?: NodeJS.ProcessEnv;
|
|
59
|
+
runCommand?: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface HarnessDispatchStatus {
|
|
64
|
+
status: "yes" | "no";
|
|
65
|
+
supported: boolean;
|
|
66
|
+
reason?: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface HarnessStatus {
|
|
70
|
+
id: string;
|
|
71
|
+
label: string;
|
|
72
|
+
default: boolean;
|
|
73
|
+
mode: HarnessMode;
|
|
74
|
+
detected: boolean;
|
|
75
|
+
authenticated: boolean | null;
|
|
76
|
+
dispatch?: HarnessDispatchStatus | undefined;
|
|
77
|
+
command?: string;
|
|
78
|
+
version?: string;
|
|
79
|
+
canUpdate: { self: boolean; extensions: boolean; models: boolean };
|
|
80
|
+
issues: readonly string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface HarnessInventory {
|
|
84
|
+
defaultHarness: typeof DEFAULT_HARNESS;
|
|
85
|
+
harnesses: readonly HarnessStatus[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const UNKNOWN_AUTH_HARNESSES = new Set(["gemini", "deepseek"]);
|
|
89
|
+
const GROK_LOGIN_LINE = "You are logged in with grok.com.";
|
|
90
|
+
/** Tab-separated `id<TAB>label` rows from the committed `agy models` probe. */
|
|
91
|
+
const AGY_MODEL_ROW = /^[a-z0-9][a-z0-9.+_-]*\t+\S/im;
|
|
92
|
+
const CODEX_CHATGPT_LINE = "Logged in using ChatGPT";
|
|
93
|
+
const CODEX_API_KEY_PREFIX = "Logged in using an API key";
|
|
94
|
+
const CODEX_NEGATIVE_LINE = "Not logged in";
|
|
95
|
+
|
|
96
|
+
/** Providers that have a dedicated native harness rule. */
|
|
97
|
+
export const NATIVE_HARNESS_PROVIDERS: Readonly<Record<string, string>> = Object.freeze({
|
|
98
|
+
claude: "anthropic",
|
|
99
|
+
codex: "openai",
|
|
100
|
+
grok: "xai",
|
|
101
|
+
agy: "google",
|
|
102
|
+
gemini: "google",
|
|
103
|
+
kimi: "moonshot",
|
|
104
|
+
deepseek: "deepseek",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** Allowlisted Pi aggregator provider prefixes. */
|
|
108
|
+
export const PI_ALLOWED_PROVIDERS: readonly string[] = Object.freeze([
|
|
109
|
+
"openrouter",
|
|
110
|
+
"nous-portal",
|
|
111
|
+
"nous",
|
|
112
|
+
"nous-proxy",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
/** Native providers Pi must not impersonate directly unless allowlisted aggregator prefix is used. */
|
|
116
|
+
export const PI_NATIVE_BRAKE_PROVIDERS: readonly string[] = Object.freeze([
|
|
117
|
+
"anthropic",
|
|
118
|
+
"openai",
|
|
119
|
+
"xai",
|
|
120
|
+
"moonshot",
|
|
121
|
+
"google",
|
|
122
|
+
"deepseek",
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
export interface HarnessModelSpec {
|
|
126
|
+
provider?: string | undefined;
|
|
127
|
+
model?: string | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface HarnessModelValidation {
|
|
131
|
+
valid: boolean;
|
|
132
|
+
issue?: "harness_unknown" | "harness_unhosted_model" | "pi_native_impersonation_blocked";
|
|
133
|
+
message?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface HarnessUpdateStep {
|
|
137
|
+
harness: string;
|
|
138
|
+
scope: Exclude<HarnessUpdateScope, "all">;
|
|
139
|
+
command: string;
|
|
140
|
+
args: readonly string[];
|
|
141
|
+
outcome: "would" | "passed" | "failed" | "skipped";
|
|
142
|
+
code?: number | null;
|
|
143
|
+
detail?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShotParsedOutput {
|
|
147
|
+
const trimmed = stdout.trim();
|
|
148
|
+
if (!trimmed) {
|
|
149
|
+
return {
|
|
150
|
+
text: "",
|
|
151
|
+
isError: true,
|
|
152
|
+
errorMessage: stderr.trim() || "empty stdout from claude",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const payload = JSON.parse(trimmed) as Record<string, unknown>;
|
|
157
|
+
const usage = (payload.usage as Record<string, unknown> | undefined) ?? {};
|
|
158
|
+
let modelUsageDetail: Record<string, unknown> | undefined;
|
|
159
|
+
if (payload.modelUsage && typeof payload.modelUsage === "object") {
|
|
160
|
+
const values = Object.values(payload.modelUsage as Record<string, unknown>);
|
|
161
|
+
if (values.length > 0 && values[0] && typeof values[0] === "object") {
|
|
162
|
+
modelUsageDetail = values[0] as Record<string, unknown>;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const tokensIn = (typeof modelUsageDetail?.inputTokens === "number" ? modelUsageDetail.inputTokens : undefined)
|
|
166
|
+
?? (typeof usage.input_tokens === "number" ? usage.input_tokens : null);
|
|
167
|
+
const tokensOut = (typeof modelUsageDetail?.outputTokens === "number" ? modelUsageDetail.outputTokens : undefined)
|
|
168
|
+
?? (typeof usage.output_tokens === "number" ? usage.output_tokens : null);
|
|
169
|
+
const cacheReadTokens = (typeof modelUsageDetail?.cacheReadInputTokens === "number" ? modelUsageDetail.cacheReadInputTokens : undefined)
|
|
170
|
+
?? (typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : null);
|
|
171
|
+
const cacheWriteTokens = (typeof modelUsageDetail?.cacheCreationInputTokens === "number" ? modelUsageDetail.cacheCreationInputTokens : undefined)
|
|
172
|
+
?? (typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : null);
|
|
173
|
+
const costUsd = typeof payload.total_cost_usd === "number"
|
|
174
|
+
? payload.total_cost_usd
|
|
175
|
+
: (typeof modelUsageDetail?.costUSD === "number" ? modelUsageDetail.costUSD : null);
|
|
176
|
+
|
|
177
|
+
const isError = Boolean(payload.is_error || payload.error);
|
|
178
|
+
const text = typeof payload.result === "string" ? payload.result : (typeof payload.text === "string" ? payload.text : "");
|
|
179
|
+
const errorMessage = isError
|
|
180
|
+
? (typeof payload.error === "string"
|
|
181
|
+
? payload.error
|
|
182
|
+
: (typeof (payload.error as Record<string, unknown> | undefined)?.message === "string"
|
|
183
|
+
? ((payload.error as Record<string, unknown>).message as string)
|
|
184
|
+
: text))
|
|
185
|
+
: undefined;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
text,
|
|
189
|
+
isError,
|
|
190
|
+
errorMessage,
|
|
191
|
+
usage: {
|
|
192
|
+
tokensIn,
|
|
193
|
+
tokensOut,
|
|
194
|
+
cacheReadTokens,
|
|
195
|
+
cacheWriteTokens,
|
|
196
|
+
contextTokens: tokensIn,
|
|
197
|
+
costUsd,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
} catch {
|
|
201
|
+
return {
|
|
202
|
+
text: trimmed,
|
|
203
|
+
usage: {},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotParsedOutput {
|
|
209
|
+
const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
210
|
+
if (lines.length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
text: "",
|
|
213
|
+
isError: true,
|
|
214
|
+
errorMessage: stderr.trim() || "empty stdout from codex",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const events: Record<string, unknown>[] = [];
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(line);
|
|
221
|
+
if (parsed && typeof parsed === "object") events.push(parsed as Record<string, unknown>);
|
|
222
|
+
} catch {
|
|
223
|
+
// non-json line ignored
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const messageTexts: string[] = [];
|
|
228
|
+
let usage: Record<string, unknown> | undefined;
|
|
229
|
+
let hasError = false;
|
|
230
|
+
let errorMessage: string | undefined;
|
|
231
|
+
|
|
232
|
+
for (const ev of events) {
|
|
233
|
+
if (ev.type === "turn.failed" || ev.type === "error") {
|
|
234
|
+
hasError = true;
|
|
235
|
+
errorMessage = typeof ev.message === "string"
|
|
236
|
+
? ev.message
|
|
237
|
+
: (typeof (ev.error as Record<string, unknown> | undefined)?.message === "string"
|
|
238
|
+
? ((ev.error as Record<string, unknown>).message as string)
|
|
239
|
+
: "turn_failed");
|
|
240
|
+
}
|
|
241
|
+
if (ev.item && typeof ev.item === "object" && (ev.item as Record<string, unknown>).type === "agent_message") {
|
|
242
|
+
const t = (ev.item as Record<string, unknown>).text;
|
|
243
|
+
if (typeof t === "string") messageTexts.push(t);
|
|
244
|
+
}
|
|
245
|
+
if (ev.type === "turn.completed" && ev.usage && typeof ev.usage === "object") {
|
|
246
|
+
usage = ev.usage as Record<string, unknown>;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const text = messageTexts.join("\n").trim();
|
|
251
|
+
const tokensIn = typeof usage?.input_tokens === "number" ? usage.input_tokens : null;
|
|
252
|
+
const tokensOut = typeof usage?.output_tokens === "number" ? usage.output_tokens : null;
|
|
253
|
+
const cacheReadTokens = typeof usage?.cached_input_tokens === "number" ? usage.cached_input_tokens : null;
|
|
254
|
+
const cacheWriteTokens = typeof usage?.cache_write_input_tokens === "number" ? usage.cache_write_input_tokens : null;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
text,
|
|
258
|
+
isError: hasError,
|
|
259
|
+
errorMessage,
|
|
260
|
+
usage: {
|
|
261
|
+
tokensIn,
|
|
262
|
+
tokensOut,
|
|
263
|
+
cacheReadTokens,
|
|
264
|
+
cacheWriteTokens,
|
|
265
|
+
contextTokens: tokensIn,
|
|
266
|
+
costUsd: null,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function parseGenericOneShotUsage(stdout: string, _stderr: string): OneShotParsedOutput {
|
|
272
|
+
const trimmed = stdout.trim();
|
|
273
|
+
try {
|
|
274
|
+
const parsed = JSON.parse(trimmed);
|
|
275
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
276
|
+
const rec = parsed as Record<string, unknown>;
|
|
277
|
+
const text = typeof rec.result === "string"
|
|
278
|
+
? rec.result
|
|
279
|
+
: (typeof rec.text === "string"
|
|
280
|
+
? rec.text
|
|
281
|
+
: (typeof rec.response === "string" ? rec.response : trimmed));
|
|
282
|
+
return { text, usage: {} };
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// not json
|
|
286
|
+
}
|
|
287
|
+
return { text: trimmed, usage: {} };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function parseKimiOneShotUsage(stdout: string, _stderr: string): OneShotParsedOutput {
|
|
291
|
+
const trimmed = stdout.trim();
|
|
292
|
+
const assistantTexts: string[] = [];
|
|
293
|
+
let isError = false;
|
|
294
|
+
let errorMessage: string | undefined;
|
|
295
|
+
|
|
296
|
+
for (const line of trimmed.split("\n")) {
|
|
297
|
+
const lineTrimmed = line.trim();
|
|
298
|
+
if (!lineTrimmed) continue;
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(lineTrimmed);
|
|
301
|
+
if (parsed && typeof parsed === "object") {
|
|
302
|
+
const rec = parsed as Record<string, unknown>;
|
|
303
|
+
if (rec.role === "assistant" && typeof rec.content === "string") {
|
|
304
|
+
assistantTexts.push(rec.content);
|
|
305
|
+
} else if (rec.role === "error" || rec.type === "error") {
|
|
306
|
+
isError = true;
|
|
307
|
+
errorMessage = typeof rec.message === "string" ? rec.message : (typeof rec.content === "string" ? rec.content : "kimi_error");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// non-JSON line (e.g. startup banner or markdown)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const text = assistantTexts.length > 0 ? assistantTexts.join("\n") : trimmed;
|
|
316
|
+
return {
|
|
317
|
+
text,
|
|
318
|
+
isError,
|
|
319
|
+
errorMessage,
|
|
320
|
+
usage: {
|
|
321
|
+
tokensIn: null,
|
|
322
|
+
tokensOut: null,
|
|
323
|
+
cacheReadTokens: null,
|
|
324
|
+
cacheWriteTokens: null,
|
|
325
|
+
contextTokens: null,
|
|
326
|
+
costUsd: null,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function parseAgyOneShotUsage(stdout: string, _stderr: string): OneShotParsedOutput {
|
|
332
|
+
const trimmed = stdout.trim();
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(trimmed);
|
|
335
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
336
|
+
const rec = parsed as Record<string, unknown>;
|
|
337
|
+
const text = typeof rec.response === "string"
|
|
338
|
+
? rec.response
|
|
339
|
+
: (typeof rec.result === "string" ? rec.result : trimmed);
|
|
340
|
+
const isError = rec.status === "ERROR";
|
|
341
|
+
const rawError = rec.error;
|
|
342
|
+
const errorMessage = isError
|
|
343
|
+
? (typeof rawError === "string"
|
|
344
|
+
? rawError
|
|
345
|
+
: (typeof (rawError as any)?.message === "string" ? (rawError as any).message : "agy_error"))
|
|
346
|
+
: undefined;
|
|
347
|
+
|
|
348
|
+
const usageRec = rec.usage && typeof rec.usage === "object" ? rec.usage as Record<string, unknown> : {};
|
|
349
|
+
const tokensIn = typeof usageRec.input_tokens === "number" ? usageRec.input_tokens : null;
|
|
350
|
+
const tokensOut = typeof usageRec.output_tokens === "number" ? usageRec.output_tokens : null;
|
|
351
|
+
const cacheReadTokens = typeof usageRec.cache_read_tokens === "number" ? usageRec.cache_read_tokens : null;
|
|
352
|
+
const contextTokens = typeof usageRec.total_tokens === "number" ? usageRec.total_tokens : tokensIn;
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
text,
|
|
356
|
+
isError,
|
|
357
|
+
errorMessage,
|
|
358
|
+
usage: {
|
|
359
|
+
tokensIn,
|
|
360
|
+
tokensOut,
|
|
361
|
+
cacheReadTokens,
|
|
362
|
+
cacheWriteTokens: null,
|
|
363
|
+
contextTokens,
|
|
364
|
+
costUsd: null,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
// not json
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
text: trimmed,
|
|
373
|
+
usage: {
|
|
374
|
+
tokensIn: null,
|
|
375
|
+
tokensOut: null,
|
|
376
|
+
cacheReadTokens: null,
|
|
377
|
+
cacheWriteTokens: null,
|
|
378
|
+
contextTokens: null,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Built-in harnesses. Unknown ids fail closed. Model lists live in `.kxm/models/*.yaml`, not here. */
|
|
384
|
+
export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
385
|
+
{
|
|
386
|
+
id: "pi",
|
|
387
|
+
label: "Pi",
|
|
388
|
+
default: true,
|
|
389
|
+
mode: "headless",
|
|
390
|
+
commands: ["pi"],
|
|
391
|
+
versionArgs: ["--version"],
|
|
392
|
+
update: {
|
|
393
|
+
self: ["update", "--self"],
|
|
394
|
+
extensions: ["update", "--extensions"],
|
|
395
|
+
models: ["update", "--models"],
|
|
396
|
+
},
|
|
397
|
+
oneShot: {
|
|
398
|
+
argv: ["-p", "--mode", "json"],
|
|
399
|
+
promptVia: "arg",
|
|
400
|
+
outputFormat: "json",
|
|
401
|
+
usageParser: parseGenericOneShotUsage,
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
id: "claude",
|
|
406
|
+
label: "Claude Code",
|
|
407
|
+
default: false,
|
|
408
|
+
mode: "either",
|
|
409
|
+
commands: ["claude"],
|
|
410
|
+
versionArgs: ["--version"],
|
|
411
|
+
authArgs: ["auth", "status"],
|
|
412
|
+
update: {
|
|
413
|
+
self: ["update"],
|
|
414
|
+
extensions: ["plugin", "update", "kxm", "-y"],
|
|
415
|
+
},
|
|
416
|
+
oneShot: {
|
|
417
|
+
argv: ["-p", "--output-format", "json"],
|
|
418
|
+
promptVia: "stdin",
|
|
419
|
+
outputFormat: "json",
|
|
420
|
+
usageParser: parseClaudeOneShotUsage,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "kimi",
|
|
425
|
+
label: "Kimi Code",
|
|
426
|
+
default: false,
|
|
427
|
+
mode: "either",
|
|
428
|
+
commands: ["kimi"],
|
|
429
|
+
versionArgs: ["--version"],
|
|
430
|
+
authArgs: ["provider", "list"],
|
|
431
|
+
update: { self: ["upgrade"] },
|
|
432
|
+
oneShot: {
|
|
433
|
+
argv: ["--output-format", "stream-json", "-p"],
|
|
434
|
+
promptVia: "arg",
|
|
435
|
+
outputFormat: "stream-json",
|
|
436
|
+
usageParser: parseKimiOneShotUsage,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "codex",
|
|
441
|
+
label: "Codex",
|
|
442
|
+
default: false,
|
|
443
|
+
mode: "either",
|
|
444
|
+
commands: ["codex"],
|
|
445
|
+
versionArgs: ["--version"],
|
|
446
|
+
authArgs: ["login", "status"],
|
|
447
|
+
update: { self: ["update"] },
|
|
448
|
+
oneShot: {
|
|
449
|
+
argv: ["exec", "--json", "-"],
|
|
450
|
+
promptVia: "stdin",
|
|
451
|
+
outputFormat: "json",
|
|
452
|
+
usageParser: parseCodexOneShotUsage,
|
|
453
|
+
},
|
|
454
|
+
},
|
|
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
|
+
{
|
|
465
|
+
id: "deepseek",
|
|
466
|
+
label: "DeepSeek",
|
|
467
|
+
default: false,
|
|
468
|
+
mode: "either",
|
|
469
|
+
commands: ["deepseek"],
|
|
470
|
+
versionArgs: ["--version"],
|
|
471
|
+
update: { self: ["update"] },
|
|
472
|
+
oneShot: {
|
|
473
|
+
argv: ["--json"],
|
|
474
|
+
promptVia: "stdin",
|
|
475
|
+
outputFormat: "json",
|
|
476
|
+
usageParser: parseGenericOneShotUsage,
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "grok",
|
|
481
|
+
label: "Grok CLI",
|
|
482
|
+
default: false,
|
|
483
|
+
mode: "either",
|
|
484
|
+
commands: ["grok"],
|
|
485
|
+
versionArgs: ["--version"],
|
|
486
|
+
authArgs: ["models"],
|
|
487
|
+
update: { self: ["update"] },
|
|
488
|
+
oneShot: {
|
|
489
|
+
argv: ["--output-format", "json"],
|
|
490
|
+
promptVia: "stdin",
|
|
491
|
+
outputFormat: "json",
|
|
492
|
+
usageParser: parseClaudeOneShotUsage,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: "agy",
|
|
497
|
+
label: "Antigravity CLI",
|
|
498
|
+
default: false,
|
|
499
|
+
mode: "either",
|
|
500
|
+
commands: ["agy"],
|
|
501
|
+
versionArgs: ["--version"],
|
|
502
|
+
authArgs: ["models"],
|
|
503
|
+
update: { self: ["update"] },
|
|
504
|
+
oneShot: {
|
|
505
|
+
argv: ["--output-format", "json", "-p"],
|
|
506
|
+
promptVia: "arg",
|
|
507
|
+
outputFormat: "json",
|
|
508
|
+
usageParser: parseAgyOneShotUsage,
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
]);
|
|
512
|
+
|
|
513
|
+
export const BUILTIN_HARNESS_IDS: readonly string[] = BUILTIN_HARNESSES.map((entry) => entry.id);
|
|
514
|
+
|
|
515
|
+
export function isKnownHarnessId(id: string, extra: Iterable<string> = []): boolean {
|
|
516
|
+
if (BUILTIN_HARNESS_IDS.includes(id)) return true;
|
|
517
|
+
for (const candidate of extra) if (candidate === id) return true;
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult {
|
|
522
|
+
return (command, args, timeoutMs) => {
|
|
523
|
+
try {
|
|
524
|
+
const result = spawnSync(command, [...args], {
|
|
525
|
+
encoding: "utf8",
|
|
526
|
+
timeout: timeoutMs,
|
|
527
|
+
windowsHide: true,
|
|
528
|
+
env,
|
|
529
|
+
});
|
|
530
|
+
if (result.error) {
|
|
531
|
+
const code = (result.error as NodeJS.ErrnoException).code;
|
|
532
|
+
return { ok: false, code: result.status, stdout: "", stderr: result.stderr?.toString() ?? "", error: code ?? result.error.message };
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
ok: result.status === 0,
|
|
536
|
+
code: result.status,
|
|
537
|
+
stdout: result.stdout?.toString() ?? "",
|
|
538
|
+
stderr: result.stderr?.toString() ?? "",
|
|
539
|
+
};
|
|
540
|
+
} catch (error) {
|
|
541
|
+
return { ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : "spawn_failed" };
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function firstLine(text: string): string | undefined {
|
|
547
|
+
const line = text.split(/\r?\n/).map((candidate) => candidate.trim()).find(Boolean);
|
|
548
|
+
return line && line.length <= 200 ? line : undefined;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function boundText(text: string, max = 4000): string | undefined {
|
|
552
|
+
const trimmed = text.trim();
|
|
553
|
+
if (!trimmed) return undefined;
|
|
554
|
+
return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max)}…`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function authLines(result: HarnessCommandResult): string[] {
|
|
558
|
+
return `${result.stdout}\n${result.stderr}`.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function parseJsonObject(text: string): Record<string, unknown> | undefined {
|
|
562
|
+
try {
|
|
563
|
+
const value: unknown = JSON.parse(text.trim());
|
|
564
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
565
|
+
} catch {
|
|
566
|
+
// Inventory auth is fail-closed unknown, not a dump of the raw body.
|
|
567
|
+
}
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function authConflictIssue(result: HarnessCommandResult): string | undefined {
|
|
572
|
+
const fields: string[] = [];
|
|
573
|
+
const okTrue = result.ok === true;
|
|
574
|
+
const codeZero = result.code === 0;
|
|
575
|
+
if (okTrue !== codeZero) fields.push("ok", "code");
|
|
576
|
+
if (okTrue && result.error) {
|
|
577
|
+
if (!fields.includes("ok")) fields.push("ok");
|
|
578
|
+
fields.push("error");
|
|
579
|
+
}
|
|
580
|
+
return fields.length > 0 ? `auth_conflict:${fields.join(",")}` : undefined;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function commandSucceeded(result: HarnessCommandResult): boolean {
|
|
584
|
+
return result.ok === true && result.code === 0 && !result.error;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function isCodexApiKeyLine(line: string): boolean {
|
|
588
|
+
return line === CODEX_API_KEY_PREFIX || line.startsWith(`${CODEX_API_KEY_PREFIX} - `);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function interpretAuth(id: string, result: HarnessCommandResult): { authenticated: boolean | null; issues: string[] } {
|
|
592
|
+
const conflict = authConflictIssue(result);
|
|
593
|
+
if (conflict) return { authenticated: null, issues: [conflict] };
|
|
594
|
+
if (result.error) return { authenticated: null, issues: ["auth_probe_error"] };
|
|
595
|
+
|
|
596
|
+
const lines = authLines(result);
|
|
597
|
+
if (id === "claude") {
|
|
598
|
+
const payload = parseJsonObject(result.stdout);
|
|
599
|
+
if (payload && payload.loggedIn === false) return { authenticated: false, issues: ["not_authenticated"] };
|
|
600
|
+
if (commandSucceeded(result) && payload && payload.loggedIn === true) return { authenticated: true, issues: [] };
|
|
601
|
+
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
602
|
+
}
|
|
603
|
+
if (id === "codex") {
|
|
604
|
+
if (lines.some((line) => line === CODEX_NEGATIVE_LINE || line.startsWith(`${CODEX_NEGATIVE_LINE} `))) {
|
|
605
|
+
return { authenticated: false, issues: ["not_authenticated"] };
|
|
606
|
+
}
|
|
607
|
+
if (commandSucceeded(result) && lines.some((line) => line === CODEX_CHATGPT_LINE)) {
|
|
608
|
+
return { authenticated: true, issues: [] };
|
|
609
|
+
}
|
|
610
|
+
if (commandSucceeded(result) && lines.some((line) => isCodexApiKeyLine(line))) {
|
|
611
|
+
return { authenticated: true, issues: ["auth_api_key"] };
|
|
612
|
+
}
|
|
613
|
+
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
614
|
+
}
|
|
615
|
+
if (id === "grok") {
|
|
616
|
+
if (commandSucceeded(result) && lines.some((line) => line === GROK_LOGIN_LINE)) return { authenticated: true, issues: [] };
|
|
617
|
+
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
618
|
+
}
|
|
619
|
+
if (id === "agy") {
|
|
620
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
621
|
+
if (!commandSucceeded(result) || !text.trim()) {
|
|
622
|
+
return { authenticated: false, issues: ["not_authenticated"] };
|
|
623
|
+
}
|
|
624
|
+
if (AGY_MODEL_ROW.test(text)) return { authenticated: true, issues: [] };
|
|
625
|
+
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
626
|
+
}
|
|
627
|
+
if (id === "kimi") {
|
|
628
|
+
if (!commandSucceeded(result)) {
|
|
629
|
+
return { authenticated: false, issues: ["not_authenticated"] };
|
|
630
|
+
}
|
|
631
|
+
if (lines.some((line) => /not logged in|no provider/i.test(line))) {
|
|
632
|
+
return { authenticated: false, issues: ["not_authenticated"] };
|
|
633
|
+
}
|
|
634
|
+
if (lines.some((line) => line.includes("managed:kimi") || line.includes("type=kimi") || line.includes("Default model:"))) {
|
|
635
|
+
return { authenticated: true, issues: [] };
|
|
636
|
+
}
|
|
637
|
+
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
638
|
+
}
|
|
639
|
+
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function resolveDispatchStatus(
|
|
643
|
+
entry: HarnessCatalogEntry,
|
|
644
|
+
detected: boolean,
|
|
645
|
+
authenticated: boolean | null,
|
|
646
|
+
issues: readonly string[],
|
|
647
|
+
): HarnessDispatchStatus {
|
|
648
|
+
if (!detected) {
|
|
649
|
+
return { status: "no", supported: false, reason: "not_detected" };
|
|
650
|
+
}
|
|
651
|
+
if (entry.id !== "pi" && !entry.oneShot) {
|
|
652
|
+
return {
|
|
653
|
+
status: "no",
|
|
654
|
+
supported: false,
|
|
655
|
+
reason: entry.id === "gemini" ? "deprecated_client" : "no_headless_mode",
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (authenticated === false) {
|
|
659
|
+
return { status: "no", supported: false, reason: "not_authenticated" };
|
|
660
|
+
}
|
|
661
|
+
if (authenticated === null) {
|
|
662
|
+
return { status: "no", supported: false, reason: issues[0] ?? "auth_unknown" };
|
|
663
|
+
}
|
|
664
|
+
return { status: "yes", supported: true };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function probeEntry(
|
|
668
|
+
entry: HarnessCatalogEntry,
|
|
669
|
+
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
670
|
+
timeoutMs: number,
|
|
671
|
+
): HarnessStatus {
|
|
672
|
+
const issues: string[] = [];
|
|
673
|
+
let detected = false;
|
|
674
|
+
let command: string | undefined;
|
|
675
|
+
let version: string | undefined;
|
|
676
|
+
for (const candidate of entry.commands) {
|
|
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
|
+
}
|
|
685
|
+
let authenticated: boolean | null = null;
|
|
686
|
+
if (!detected) authenticated = false;
|
|
687
|
+
else if (entry.id === "pi") {
|
|
688
|
+
authenticated = null;
|
|
689
|
+
issues.push("auth_context_required");
|
|
690
|
+
} else if (UNKNOWN_AUTH_HARNESSES.has(entry.id) || !entry.authArgs) {
|
|
691
|
+
authenticated = null;
|
|
692
|
+
if (UNKNOWN_AUTH_HARNESSES.has(entry.id)) issues.push("auth_unknown");
|
|
693
|
+
} else if (command) {
|
|
694
|
+
const parsed = interpretAuth(entry.id, runCommand(command, entry.authArgs, timeoutMs));
|
|
695
|
+
authenticated = parsed.authenticated;
|
|
696
|
+
issues.push(...parsed.issues);
|
|
697
|
+
}
|
|
698
|
+
const dispatch = resolveDispatchStatus(entry, detected, authenticated, issues);
|
|
699
|
+
return {
|
|
700
|
+
id: entry.id,
|
|
701
|
+
label: entry.label,
|
|
702
|
+
default: entry.default,
|
|
703
|
+
mode: entry.mode,
|
|
704
|
+
detected,
|
|
705
|
+
authenticated,
|
|
706
|
+
dispatch,
|
|
707
|
+
...(command ? { command } : {}),
|
|
708
|
+
...(version ? { version } : {}),
|
|
709
|
+
canUpdate: {
|
|
710
|
+
self: Boolean(entry.update.self.length),
|
|
711
|
+
extensions: Boolean(entry.update.extensions?.length),
|
|
712
|
+
models: Boolean(entry.update.models?.length),
|
|
713
|
+
},
|
|
714
|
+
issues,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInventory {
|
|
719
|
+
const timeoutMs = options.timeoutMs ?? 3000;
|
|
720
|
+
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
721
|
+
return {
|
|
722
|
+
defaultHarness: DEFAULT_HARNESS,
|
|
723
|
+
harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs)),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export function eligibleHarnesses(inventory: HarnessInventory): readonly string[] {
|
|
728
|
+
const eligible = inventory.harnesses
|
|
729
|
+
.filter((entry) => entry.detected && entry.authenticated === true)
|
|
730
|
+
.map((entry) => entry.id);
|
|
731
|
+
if (eligible.length === 0) throw new Error("no_authenticated_harness");
|
|
732
|
+
return eligible;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function validateHarnessModelPair(
|
|
736
|
+
harnessId: string,
|
|
737
|
+
modelSpec: string | HarnessModelSpec,
|
|
738
|
+
): HarnessModelValidation {
|
|
739
|
+
if (!isKnownHarnessId(harnessId)) {
|
|
740
|
+
return { valid: false, issue: "harness_unknown", message: `unknown harness: ${harnessId}` };
|
|
741
|
+
}
|
|
742
|
+
let provider: string | undefined;
|
|
743
|
+
let model: string | undefined;
|
|
744
|
+
if (typeof modelSpec === "string") {
|
|
745
|
+
const trimmed = modelSpec.trim();
|
|
746
|
+
if (trimmed.includes("/")) {
|
|
747
|
+
const idx = trimmed.indexOf("/");
|
|
748
|
+
provider = trimmed.slice(0, idx).toLowerCase();
|
|
749
|
+
model = trimmed.slice(idx + 1);
|
|
750
|
+
} else {
|
|
751
|
+
model = trimmed;
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
provider = modelSpec.provider?.trim().toLowerCase();
|
|
755
|
+
model = modelSpec.model?.trim();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (harnessId === "claude") {
|
|
759
|
+
if (provider && provider !== "anthropic") {
|
|
760
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness claude does not host provider ${provider}` };
|
|
761
|
+
}
|
|
762
|
+
if (model && /^(gpt|o1|o3|grok|gemini|kimi|moonshot|deepseek|qwen)-/i.test(model)) {
|
|
763
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness claude does not host model ${model}` };
|
|
764
|
+
}
|
|
765
|
+
return { valid: true };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (harnessId === "codex") {
|
|
769
|
+
if (provider && provider !== "openai") {
|
|
770
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness codex does not host provider ${provider}` };
|
|
771
|
+
}
|
|
772
|
+
if (model && /^(claude|fable|grok|gemini|kimi|moonshot|deepseek|qwen)-/i.test(model)) {
|
|
773
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness codex does not host model ${model}` };
|
|
774
|
+
}
|
|
775
|
+
return { valid: true };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (harnessId === "grok") {
|
|
779
|
+
if (provider && provider !== "xai") {
|
|
780
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness grok does not host provider ${provider}` };
|
|
781
|
+
}
|
|
782
|
+
if (model && !/^grok-/i.test(model)) {
|
|
783
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness grok only hosts grok models, received ${model}` };
|
|
784
|
+
}
|
|
785
|
+
return { valid: true };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (harnessId === "agy" || harnessId === "gemini") {
|
|
789
|
+
if (provider && provider !== "google") {
|
|
790
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness ${harnessId} does not host provider ${provider}` };
|
|
791
|
+
}
|
|
792
|
+
if (model && !/^gemini-/i.test(model)) {
|
|
793
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness ${harnessId} only hosts gemini models, received ${model}` };
|
|
794
|
+
}
|
|
795
|
+
return { valid: true };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (harnessId === "kimi") {
|
|
799
|
+
if (provider && provider !== "moonshot") {
|
|
800
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness kimi does not host provider ${provider}` };
|
|
801
|
+
}
|
|
802
|
+
if (model && !/^(kimi|moonshot)-/i.test(model)) {
|
|
803
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness kimi only hosts kimi/moonshot models, received ${model}` };
|
|
804
|
+
}
|
|
805
|
+
return { valid: true };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (harnessId === "deepseek") {
|
|
809
|
+
if (provider && provider !== "deepseek") {
|
|
810
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness deepseek does not host provider ${provider}` };
|
|
811
|
+
}
|
|
812
|
+
if (model && !/^deepseek-/i.test(model)) {
|
|
813
|
+
return { valid: false, issue: "harness_unhosted_model", message: `harness deepseek only hosts deepseek models, received ${model}` };
|
|
814
|
+
}
|
|
815
|
+
return { valid: true };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (harnessId === "pi") {
|
|
819
|
+
if (provider && PI_NATIVE_BRAKE_PROVIDERS.includes(provider)) {
|
|
820
|
+
return {
|
|
821
|
+
valid: false,
|
|
822
|
+
issue: "pi_native_impersonation_blocked",
|
|
823
|
+
message: `pi must not impersonate native provider ${provider}; use the native harness`,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (!provider && model && PI_NATIVE_BRAKE_PROVIDERS.some((p) => model!.toLowerCase().startsWith(`${p}/`))) {
|
|
827
|
+
return {
|
|
828
|
+
valid: false,
|
|
829
|
+
issue: "pi_native_impersonation_blocked",
|
|
830
|
+
message: `pi must not impersonate native model ${model}; use the native harness`,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
return { valid: true };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return { valid: true };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export interface HarnessAssignmentProbeOptions {
|
|
840
|
+
harness: string;
|
|
841
|
+
provider?: string | undefined;
|
|
842
|
+
model?: string | undefined;
|
|
843
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
844
|
+
runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult) | undefined;
|
|
845
|
+
timeoutMs?: number | undefined;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions): HarnessStatus {
|
|
849
|
+
const timeoutMs = options.timeoutMs ?? 3000;
|
|
850
|
+
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
851
|
+
const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === options.harness);
|
|
852
|
+
if (!entry) {
|
|
853
|
+
return {
|
|
854
|
+
id: options.harness,
|
|
855
|
+
label: options.harness,
|
|
856
|
+
default: false,
|
|
857
|
+
mode: "either",
|
|
858
|
+
detected: false,
|
|
859
|
+
authenticated: false,
|
|
860
|
+
dispatch: { status: "no", supported: false, reason: "harness_unknown" },
|
|
861
|
+
canUpdate: { self: false, extensions: false, models: false },
|
|
862
|
+
issues: ["harness_unknown"],
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (options.provider || options.model) {
|
|
867
|
+
const validation = validateHarnessModelPair(options.harness, { provider: options.provider, model: options.model });
|
|
868
|
+
if (!validation.valid) {
|
|
869
|
+
return {
|
|
870
|
+
id: entry.id,
|
|
871
|
+
label: entry.label,
|
|
872
|
+
default: entry.default,
|
|
873
|
+
mode: entry.mode,
|
|
874
|
+
detected: false,
|
|
875
|
+
authenticated: false,
|
|
876
|
+
dispatch: { status: "no", supported: false, reason: validation.issue ?? "harness_unhosted_model" },
|
|
877
|
+
canUpdate: {
|
|
878
|
+
self: Boolean(entry.update.self.length),
|
|
879
|
+
extensions: Boolean(entry.update.extensions?.length),
|
|
880
|
+
models: Boolean(entry.update.models?.length),
|
|
881
|
+
},
|
|
882
|
+
issues: [validation.issue ?? "harness_unhosted_model"],
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
let detected = false;
|
|
888
|
+
let command: string | undefined;
|
|
889
|
+
let version: string | undefined;
|
|
890
|
+
for (const candidate of entry.commands) {
|
|
891
|
+
const result = runCommand(candidate, entry.versionArgs, timeoutMs);
|
|
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
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!detected || !command) {
|
|
901
|
+
return {
|
|
902
|
+
id: entry.id,
|
|
903
|
+
label: entry.label,
|
|
904
|
+
default: entry.default,
|
|
905
|
+
mode: entry.mode,
|
|
906
|
+
detected: false,
|
|
907
|
+
authenticated: false,
|
|
908
|
+
dispatch: { status: "no", supported: false, reason: "not_detected" },
|
|
909
|
+
canUpdate: {
|
|
910
|
+
self: Boolean(entry.update.self.length),
|
|
911
|
+
extensions: Boolean(entry.update.extensions?.length),
|
|
912
|
+
models: Boolean(entry.update.models?.length),
|
|
913
|
+
},
|
|
914
|
+
issues: [],
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (entry.id === "pi") {
|
|
919
|
+
if (!options.provider && !options.model) {
|
|
920
|
+
return {
|
|
921
|
+
id: entry.id,
|
|
922
|
+
label: entry.label,
|
|
923
|
+
default: entry.default,
|
|
924
|
+
mode: entry.mode,
|
|
925
|
+
detected: true,
|
|
926
|
+
authenticated: null,
|
|
927
|
+
dispatch: { status: "no", supported: false, reason: "auth_context_required" },
|
|
928
|
+
command,
|
|
929
|
+
...(version ? { version } : {}),
|
|
930
|
+
canUpdate: {
|
|
931
|
+
self: Boolean(entry.update.self.length),
|
|
932
|
+
extensions: Boolean(entry.update.extensions?.length),
|
|
933
|
+
models: Boolean(entry.update.models?.length),
|
|
934
|
+
},
|
|
935
|
+
issues: ["auth_context_required"],
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const authArgs = ["auth", "check"];
|
|
940
|
+
if (options.model) {
|
|
941
|
+
authArgs.push("--model", options.model);
|
|
942
|
+
} else if (options.provider) {
|
|
943
|
+
authArgs.push("--provider", options.provider);
|
|
944
|
+
}
|
|
945
|
+
authArgs.push("--json");
|
|
946
|
+
const result = runCommand(command, authArgs, timeoutMs);
|
|
947
|
+
const parsed = parseJsonObject(result.stdout);
|
|
948
|
+
let authenticated: boolean | null = false;
|
|
949
|
+
const issues: string[] = [];
|
|
950
|
+
if (result.error) {
|
|
951
|
+
issues.push("auth_probe_error");
|
|
952
|
+
} else if (parsed && parsed.status === "ready") {
|
|
953
|
+
authenticated = true;
|
|
954
|
+
} else if (parsed && parsed.status === "not_ready") {
|
|
955
|
+
authenticated = false;
|
|
956
|
+
issues.push("not_authenticated");
|
|
957
|
+
} else if (commandSucceeded(result) && /\bready\b/i.test(`${result.stdout}\n${result.stderr}`)) {
|
|
958
|
+
authenticated = true;
|
|
959
|
+
} else {
|
|
960
|
+
authenticated = false;
|
|
961
|
+
issues.push("not_authenticated");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const dispatch = resolveDispatchStatus(entry, true, authenticated, issues);
|
|
965
|
+
|
|
966
|
+
return {
|
|
967
|
+
id: entry.id,
|
|
968
|
+
label: entry.label,
|
|
969
|
+
default: entry.default,
|
|
970
|
+
mode: entry.mode,
|
|
971
|
+
detected: true,
|
|
972
|
+
authenticated,
|
|
973
|
+
dispatch,
|
|
974
|
+
command,
|
|
975
|
+
...(version ? { version } : {}),
|
|
976
|
+
canUpdate: {
|
|
977
|
+
self: Boolean(entry.update.self.length),
|
|
978
|
+
extensions: Boolean(entry.update.extensions?.length),
|
|
979
|
+
models: Boolean(entry.update.models?.length),
|
|
980
|
+
},
|
|
981
|
+
issues,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return probeEntry(entry, runCommand, timeoutMs);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export function probeHarnessesForModel(
|
|
989
|
+
modelSpec: HarnessModelSpec,
|
|
990
|
+
options: HarnessProbeOptions = {},
|
|
991
|
+
): HarnessInventory {
|
|
992
|
+
const timeoutMs = options.timeoutMs ?? 3000;
|
|
993
|
+
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
994
|
+
return {
|
|
995
|
+
defaultHarness: DEFAULT_HARNESS,
|
|
996
|
+
harnesses: BUILTIN_HARNESSES.map((entry) =>
|
|
997
|
+
probeHarnessAssignment({
|
|
998
|
+
harness: entry.id,
|
|
999
|
+
provider: modelSpec.provider,
|
|
1000
|
+
model: modelSpec.model,
|
|
1001
|
+
runCommand,
|
|
1002
|
+
timeoutMs,
|
|
1003
|
+
})
|
|
1004
|
+
),
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function scopesFor(scope: HarnessUpdateScope, entry: HarnessCatalogEntry): Exclude<HarnessUpdateScope, "all">[] {
|
|
1009
|
+
if (scope === "self") return ["self"];
|
|
1010
|
+
if (scope === "extensions") return entry.update.extensions ? ["extensions"] : [];
|
|
1011
|
+
if (scope === "models") return entry.update.models ? ["models"] : [];
|
|
1012
|
+
return [
|
|
1013
|
+
...(entry.update.self.length ? ["self" as const] : []),
|
|
1014
|
+
...(entry.update.extensions ? ["extensions" as const] : []),
|
|
1015
|
+
...(entry.update.models ? ["models" as const] : []),
|
|
1016
|
+
];
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function argsFor(entry: HarnessCatalogEntry, scope: Exclude<HarnessUpdateScope, "all">): readonly string[] | undefined {
|
|
1020
|
+
if (scope === "self") return entry.update.self;
|
|
1021
|
+
if (scope === "extensions") return entry.update.extensions;
|
|
1022
|
+
return entry.update.models;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export function planHarnessUpdate(
|
|
1026
|
+
inventory: HarnessInventory,
|
|
1027
|
+
requested: { harness?: string; scope: HarnessUpdateScope },
|
|
1028
|
+
): HarnessUpdateStep[] {
|
|
1029
|
+
const selected = requested.harness
|
|
1030
|
+
? inventory.harnesses.filter((entry) => entry.id === requested.harness)
|
|
1031
|
+
: inventory.harnesses.filter((entry) => entry.detected);
|
|
1032
|
+
if (requested.harness && selected.length === 0) {
|
|
1033
|
+
return [{
|
|
1034
|
+
harness: requested.harness,
|
|
1035
|
+
scope: "self",
|
|
1036
|
+
command: requested.harness,
|
|
1037
|
+
args: [],
|
|
1038
|
+
outcome: "skipped",
|
|
1039
|
+
detail: isKnownHarnessId(requested.harness) ? "not_detected" : "unknown_harness",
|
|
1040
|
+
}];
|
|
1041
|
+
}
|
|
1042
|
+
const steps: HarnessUpdateStep[] = [];
|
|
1043
|
+
for (const status of selected) {
|
|
1044
|
+
const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
|
|
1045
|
+
if (!entry) continue;
|
|
1046
|
+
const scopes = scopesFor(requested.scope, entry);
|
|
1047
|
+
if (scopes.length === 0) {
|
|
1048
|
+
steps.push({
|
|
1049
|
+
harness: status.id,
|
|
1050
|
+
scope: requested.scope === "all" ? "self" : requested.scope,
|
|
1051
|
+
command: status.command ?? entry.commands[0]!,
|
|
1052
|
+
args: [],
|
|
1053
|
+
outcome: "skipped",
|
|
1054
|
+
detail: "no_updater",
|
|
1055
|
+
});
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (!status.detected || !status.command) {
|
|
1059
|
+
steps.push({
|
|
1060
|
+
harness: status.id,
|
|
1061
|
+
scope: scopes[0]!,
|
|
1062
|
+
command: entry.commands[0]!,
|
|
1063
|
+
args: argsFor(entry, scopes[0]!) ?? [],
|
|
1064
|
+
outcome: "skipped",
|
|
1065
|
+
detail: "not_detected",
|
|
1066
|
+
});
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
for (const scope of scopes) {
|
|
1070
|
+
const args = argsFor(entry, scope) ?? [];
|
|
1071
|
+
steps.push({ harness: status.id, scope, command: status.command, args, outcome: "would" });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return steps;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
export function runHarnessUpdate(
|
|
1078
|
+
steps: readonly HarnessUpdateStep[],
|
|
1079
|
+
options: HarnessProbeOptions & { dryRun?: boolean; updateTimeoutMs?: number } = {},
|
|
1080
|
+
): HarnessUpdateStep[] {
|
|
1081
|
+
if (options.dryRun) return steps.map((step) => ({ ...step, outcome: step.outcome === "would" ? "would" : step.outcome }));
|
|
1082
|
+
const timeoutMs = options.updateTimeoutMs ?? 180_000;
|
|
1083
|
+
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
1084
|
+
return steps.map((step) => {
|
|
1085
|
+
if (step.outcome !== "would") return step;
|
|
1086
|
+
const result = runCommand(step.command, step.args, timeoutMs);
|
|
1087
|
+
const detail = boundText(result.stderr) ?? boundText(result.stdout) ?? result.error;
|
|
1088
|
+
return {
|
|
1089
|
+
...step,
|
|
1090
|
+
outcome: result.ok ? "passed" : "failed",
|
|
1091
|
+
...(result.code !== undefined && result.code !== null ? { code: result.code } : {}),
|
|
1092
|
+
...(detail ? { detail } : {}),
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export function formatHarnessInventory(inventory: HarnessInventory): string {
|
|
1098
|
+
const header = [
|
|
1099
|
+
"id".padEnd(9),
|
|
1100
|
+
"default".padEnd(8),
|
|
1101
|
+
"detected".padEnd(9),
|
|
1102
|
+
"auth".padEnd(8),
|
|
1103
|
+
"dispatch".padEnd(26),
|
|
1104
|
+
"updates",
|
|
1105
|
+
].join(" ");
|
|
1106
|
+
const rows = inventory.harnesses.map((entry) => {
|
|
1107
|
+
const auth = entry.authenticated === true ? "yes" : entry.authenticated === false ? "no" : "unknown";
|
|
1108
|
+
const dispatchText = entry.dispatch
|
|
1109
|
+
? (entry.dispatch.status === "yes" ? "yes" : `no (${entry.dispatch.reason ?? "unsupported"})`)
|
|
1110
|
+
: (entry.authenticated === true ? "yes" : "no");
|
|
1111
|
+
const updates = [
|
|
1112
|
+
entry.canUpdate.self ? "self" : undefined,
|
|
1113
|
+
entry.canUpdate.extensions ? "extensions" : undefined,
|
|
1114
|
+
entry.canUpdate.models ? "models" : undefined,
|
|
1115
|
+
].filter(Boolean).join(",") || "none";
|
|
1116
|
+
const apiKeyNote = entry.issues.includes("auth_api_key") ? " API key" : "";
|
|
1117
|
+
return [
|
|
1118
|
+
entry.id.padEnd(9),
|
|
1119
|
+
(entry.default ? "yes" : "no").padEnd(8),
|
|
1120
|
+
(entry.detected ? "yes" : "no").padEnd(9),
|
|
1121
|
+
auth.padEnd(8),
|
|
1122
|
+
dispatchText.padEnd(26),
|
|
1123
|
+
`${updates}${apiKeyNote}`,
|
|
1124
|
+
].join(" ");
|
|
1125
|
+
});
|
|
1126
|
+
return [
|
|
1127
|
+
`default harness: ${inventory.defaultHarness} (omit agent harness: to use headless Pi)`,
|
|
1128
|
+
"enable/disable = Git YAML (.kxm/agents, .kxm/models) or the harness's own plugin CLI",
|
|
1129
|
+
"governed kxm skills are not auto-updated",
|
|
1130
|
+
header,
|
|
1131
|
+
...rows,
|
|
1132
|
+
].join("\n");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export function formatHarnessUpdate(steps: readonly HarnessUpdateStep[]): string {
|
|
1136
|
+
if (steps.length === 0) return "nothing to update";
|
|
1137
|
+
return steps.map((step) => {
|
|
1138
|
+
const argv = [step.command, ...step.args].join(" ");
|
|
1139
|
+
const detail = step.detail ? ` (${step.detail})` : "";
|
|
1140
|
+
return `${step.harness} ${step.scope}: ${step.outcome} ${argv}${detail}`;
|
|
1141
|
+
}).join("\n");
|
|
1142
|
+
}
|