@kal-elsam/kairo-runtime 0.16.0 → 0.18.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/CHANGELOG.md +79 -0
- package/package.json +2 -1
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/ux-smoke-test.sh +3 -3
- package/src/cli.js +96 -11
- package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
- package/src/global/architect/architect-cli.js +76 -0
- package/src/global/architect/architect-codex.js +146 -0
- package/src/global/architect/architect-manager.js +125 -0
- package/src/global/architect/architect-store.js +377 -0
- package/src/global/architect/architect-types.js +47 -0
- package/src/global/cli-help.js +10 -1
- package/src/global/cockpit/app.js +493 -0
- package/src/global/cockpit/card.js +111 -0
- package/src/global/cockpit/cli.js +33 -0
- package/src/global/cockpit/gauge.js +31 -0
- package/src/global/cockpit/project-overlay.js +693 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1298 -0
- package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
- package/src/global/conversation/cli.js +53 -0
- package/src/global/conversation/codex-sandbox.js +230 -0
- package/src/global/conversation/cursor-sandbox.js +215 -0
- package/src/global/conversation/project-analysis.js +204 -0
- package/src/global/conversation/project-profile.js +178 -0
- package/src/global/conversation/project-router.js +149 -0
- package/src/global/conversation/project-strategy-store.js +64 -0
- package/src/global/conversation/project-strategy.js +514 -0
- package/src/global/conversation/sanitized-snapshot.js +169 -0
- package/src/global/conversation/secret-scanner.js +71 -0
- package/src/global/conversation/service.js +1090 -0
- package/src/global/conversation/session-store.js +75 -0
- package/src/global/conversation/transcript-store.js +79 -0
- package/src/global/conversation/ui.js +195 -0
- package/src/global/intelligence/capability-scoring.js +480 -0
- package/src/global/intelligence/execution-router.js +466 -0
- package/src/global/intelligence/kairo-telemetry-source.js +59 -0
- package/src/global/intelligence/kairobench-runner.js +85 -0
- package/src/global/intelligence/kairobench-source.js +34 -0
- package/src/global/intelligence/kairobench-tasks.js +47 -0
- package/src/global/intelligence/model-candidate-catalog.js +456 -0
- package/src/global/intelligence/model-capability-registry-sources.js +145 -0
- package/src/global/intelligence/model-capability-registry.js +125 -0
- package/src/global/intelligence/model-intelligence.js +1646 -0
- package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
- package/src/global/intelligence/quick-ask.js +149 -0
- package/src/global/intelligence/role-profiles.js +251 -0
- package/src/global/intelligence/skill-catalog.js +67 -0
- package/src/global/intelligence/subscription-pressure-source.js +41 -0
- package/src/global/mcp/kairo-mcp.js +51 -18
- package/src/global/mcp/work-snapshot-rule.js +4 -2
- package/src/global/mcp/workspace-binding.js +88 -0
- package/src/global/mcp/workspace-mcp-entry.js +74 -0
- package/src/global/mcp-install.js +8 -1
- package/src/global/observability/artificial-analysis-models.js +118 -0
- package/src/global/observability/claude-models.js +31 -0
- package/src/global/observability/claude-usage.js +112 -0
- package/src/global/observability/codex-models.js +96 -0
- package/src/global/observability/codex-usage.js +160 -0
- package/src/global/observability/cursor-auth.js +88 -0
- package/src/global/observability/cursor-models.js +101 -0
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/opencode-models.js +101 -0
- package/src/global/observability/opencode-usage.js +162 -0
- package/src/global/paths.js +49 -2
- package/src/global/profile.js +23 -1
- package/src/global/runtime/execution-adapters/claude.js +63 -30
- package/src/global/runtime/execution-adapters/codex.js +9 -2
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
- package/src/global/runtime/execution-adapters/opencode.js +83 -18
- package/src/global/runtime/execution-worktree-manager.js +924 -0
- package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
- package/src/global/runtime/execution-worktree-store.js +83 -0
- package/src/global/runtime/execution-worktree-types.js +45 -0
- package/src/global/runtime/run-events.js +38 -0
- package/src/global/runtime/run-manager.js +22 -6
- package/src/global/runtime/run-supervisor.js +41 -12
- package/src/global/runtime/usage-manager.js +96 -0
- package/src/global/runtime/usage-store.js +69 -0
- package/src/global/runtime/usage-types.js +62 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Real per-account model catalog via Codex's app-server JSON-RPC protocol
|
|
4
|
+
// (`model/list`, confirmed present in `codex app-server generate-json-schema`
|
|
5
|
+
// output and verified live) — never the static, possibly-stale documented
|
|
6
|
+
// list. Same fail-closed shape as codex-usage.js: any error yields
|
|
7
|
+
// `unknown`, never a fabricated model.
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 2500;
|
|
9
|
+
const SOURCE = "codex app-server model/list";
|
|
10
|
+
|
|
11
|
+
function unknown(error = null) {
|
|
12
|
+
return { status: "unknown", source: SOURCE, models: [], error: error ? String(error) : null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeRequest(child, id, method, params) {
|
|
16
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeModel(entry) {
|
|
20
|
+
return {
|
|
21
|
+
id: entry.id,
|
|
22
|
+
displayName: entry.displayName ?? entry.id,
|
|
23
|
+
isDefault: entry.isDefault === true,
|
|
24
|
+
hidden: entry.hidden === true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reads Codex's real, currently-available model catalog for this
|
|
30
|
+
* authenticated account — not a static documented list. Excludes hidden
|
|
31
|
+
* models by default (mirrors what Codex's own picker would offer).
|
|
32
|
+
*/
|
|
33
|
+
export async function readCodexModels({
|
|
34
|
+
spawn = defaultSpawn,
|
|
35
|
+
cwd = process.cwd(),
|
|
36
|
+
env = process.env,
|
|
37
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
38
|
+
includeHidden = false
|
|
39
|
+
} = {}) {
|
|
40
|
+
let child;
|
|
41
|
+
try {
|
|
42
|
+
child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
43
|
+
cwd,
|
|
44
|
+
env,
|
|
45
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return unknown(error?.message ?? error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
let buffer = "";
|
|
53
|
+
let listRequestId = null;
|
|
54
|
+
let finished = false;
|
|
55
|
+
const timer = setTimeout(() => finish(unknown("codex app-server timeout")), timeoutMs);
|
|
56
|
+
|
|
57
|
+
function finish(result) {
|
|
58
|
+
if (finished) return;
|
|
59
|
+
finished = true;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
62
|
+
resolve(result);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onLine(line) {
|
|
66
|
+
let message;
|
|
67
|
+
try { message = JSON.parse(line); } catch { return finish(unknown("malformed codex app-server output")); }
|
|
68
|
+
if (!message || typeof message !== "object" || message.id == null) return;
|
|
69
|
+
if (message.error) return finish(unknown(`codex app-server error: ${message.error.message ?? "request failed"}`));
|
|
70
|
+
if (message.id === 1) {
|
|
71
|
+
listRequestId = 2;
|
|
72
|
+
writeRequest(child, listRequestId, "model/list", { includeHidden });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (message.id === listRequestId) {
|
|
76
|
+
const data = Array.isArray(message.result?.data) ? message.result.data : null;
|
|
77
|
+
if (!data) return finish(unknown("codex returned no model list"));
|
|
78
|
+
finish({ status: "measured", source: SOURCE, models: data.map(normalizeModel), error: null });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
child.stdout?.on("data", (chunk) => {
|
|
83
|
+
buffer += String(chunk);
|
|
84
|
+
const lines = buffer.split(/\r?\n/);
|
|
85
|
+
buffer = lines.pop() ?? "";
|
|
86
|
+
for (const line of lines) if (line.trim()) onLine(line.trim());
|
|
87
|
+
});
|
|
88
|
+
child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
|
|
89
|
+
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
|
|
90
|
+
|
|
91
|
+
writeRequest(child, 1, "initialize", {
|
|
92
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.18.0" },
|
|
93
|
+
capabilities: {}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 2500;
|
|
4
|
+
const SOURCE = "codex app-server account/rateLimits/read";
|
|
5
|
+
|
|
6
|
+
function clampPercent(value) {
|
|
7
|
+
const number = Number(value);
|
|
8
|
+
if (!Number.isFinite(number)) return null;
|
|
9
|
+
return Math.max(0, Math.min(100, number));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function epochSecondsToIso(value) {
|
|
13
|
+
try {
|
|
14
|
+
const date = new Date(Number(value) * 1000);
|
|
15
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeWindow(name, value) {
|
|
22
|
+
if (!value || typeof value !== "object") return null;
|
|
23
|
+
const usedPercent = clampPercent(value.usedPercent ?? value.used_percentage);
|
|
24
|
+
if (usedPercent == null) return null;
|
|
25
|
+
const windowDurationMins = Number(value.windowDurationMins ?? value.window_duration_mins);
|
|
26
|
+
const resetsAt = value.resetsAt ?? value.resets_at ?? null;
|
|
27
|
+
const numericReset = (typeof resetsAt === "number" && Number.isFinite(resetsAt))
|
|
28
|
+
|| (typeof resetsAt === "string" && resetsAt.trim() !== "" && Number.isFinite(Number(resetsAt)));
|
|
29
|
+
const resetsAtIso = numericReset
|
|
30
|
+
? epochSecondsToIso(resetsAt)
|
|
31
|
+
: (typeof resetsAt === "string" ? resetsAt : null);
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
usedPercent,
|
|
35
|
+
remainingPercent: 100 - usedPercent,
|
|
36
|
+
// Short aliases keep the normalized contract convenient for consumers
|
|
37
|
+
// while the explicit Percent fields make units unambiguous in the UI.
|
|
38
|
+
used: usedPercent,
|
|
39
|
+
remaining: 100 - usedPercent,
|
|
40
|
+
windowDurationMins: Number.isFinite(windowDurationMins) ? windowDurationMins : null,
|
|
41
|
+
// The app-server contract uses epoch seconds. Preserve that value and
|
|
42
|
+
// provide an ISO projection for human-facing clients.
|
|
43
|
+
resetsAt: numericReset ? Number(resetsAt) : (typeof resetsAt === "string" ? resetsAt : null),
|
|
44
|
+
resetsAtIso
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Normalize only safe, displayable rate-limit fields. Never returns account ids/credits. */
|
|
49
|
+
export function normalizeCodexRateLimits(payload) {
|
|
50
|
+
const root = payload?.rateLimits ?? payload?.rate_limits ?? payload ?? {};
|
|
51
|
+
const primary = normalizeWindow("5h", root.primary);
|
|
52
|
+
const secondary = normalizeWindow("weekly", root.secondary);
|
|
53
|
+
// Current Codex returns this beside `rateLimits`; accept the older nested
|
|
54
|
+
// placement only as a compatibility fallback.
|
|
55
|
+
const ordinaryUsageAllowed = payload?.ordinaryUsageAllowed
|
|
56
|
+
?? payload?.ordinary_usage_allowed
|
|
57
|
+
?? root.ordinaryUsageAllowed
|
|
58
|
+
?? root.ordinary_usage_allowed;
|
|
59
|
+
const allowed = typeof ordinaryUsageAllowed === "boolean" ? ordinaryUsageAllowed : null;
|
|
60
|
+
if (!primary && !secondary && allowed == null) return null;
|
|
61
|
+
return {
|
|
62
|
+
status: allowed === false ? "exhausted" : "measured",
|
|
63
|
+
source: SOURCE,
|
|
64
|
+
ordinaryUsageAllowed: allowed,
|
|
65
|
+
windows: [primary, secondary].filter(Boolean),
|
|
66
|
+
primary,
|
|
67
|
+
secondary
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function unknown(error = null) {
|
|
72
|
+
return {
|
|
73
|
+
status: "unknown",
|
|
74
|
+
source: SOURCE,
|
|
75
|
+
ordinaryUsageAllowed: null,
|
|
76
|
+
windows: [],
|
|
77
|
+
primary: null,
|
|
78
|
+
secondary: null,
|
|
79
|
+
error: error ? String(error) : null
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeRequest(child, id, method, params) {
|
|
84
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read Codex subscription rate limits without starting a model turn.
|
|
89
|
+
* This is deliberately fail-closed: malformed output, unavailable auth, or a
|
|
90
|
+
* timeout produces `unknown`, never a fabricated quota or a PAYG fallback.
|
|
91
|
+
*/
|
|
92
|
+
export async function readCodexUsage({
|
|
93
|
+
spawn = defaultSpawn,
|
|
94
|
+
cwd = process.cwd(),
|
|
95
|
+
env = process.env,
|
|
96
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
97
|
+
} = {}) {
|
|
98
|
+
let child;
|
|
99
|
+
try {
|
|
100
|
+
child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
101
|
+
cwd,
|
|
102
|
+
env,
|
|
103
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return unknown(error?.message ?? error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
let buffer = "";
|
|
111
|
+
let nextId = 2;
|
|
112
|
+
let rateRequestId = null;
|
|
113
|
+
let finished = false;
|
|
114
|
+
const timer = setTimeout(() => finish(unknown("codex app-server timeout")), timeoutMs);
|
|
115
|
+
|
|
116
|
+
function finish(result) {
|
|
117
|
+
if (finished) return;
|
|
118
|
+
finished = true;
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
121
|
+
resolve(result);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onLine(line) {
|
|
125
|
+
let message;
|
|
126
|
+
try { message = JSON.parse(line); } catch { return finish(unknown("malformed codex app-server output")); }
|
|
127
|
+
if (!message || typeof message !== "object") return;
|
|
128
|
+
// Notifications have no id and must not affect the request state.
|
|
129
|
+
if (message.id == null) return;
|
|
130
|
+
if (message.error) return finish(unknown(`codex app-server error: ${message.error.message ?? "request failed"}`));
|
|
131
|
+
if (message.id === 1) {
|
|
132
|
+
rateRequestId = nextId++;
|
|
133
|
+
writeRequest(child, rateRequestId, "account/rateLimits/read", {
|
|
134
|
+
excludeResetCreditDetails: true,
|
|
135
|
+
supportsLunaReserve: false
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (message.id === rateRequestId) {
|
|
140
|
+
finish(normalizeCodexRateLimits(message.result) ?? unknown("codex returned unusable rate limits"));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
child.stdout?.on("data", (chunk) => {
|
|
145
|
+
buffer += String(chunk);
|
|
146
|
+
const lines = buffer.split(/\r?\n/);
|
|
147
|
+
buffer = lines.pop() ?? "";
|
|
148
|
+
for (const line of lines) if (line.trim()) onLine(line.trim());
|
|
149
|
+
});
|
|
150
|
+
child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
|
|
151
|
+
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
|
|
152
|
+
|
|
153
|
+
writeRequest(child, 1, "initialize", {
|
|
154
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.18.0" },
|
|
155
|
+
capabilities: {}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const CODEX_USAGE_SOURCE = SOURCE;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
|
|
4
|
+
// `cursor-agent status`/`whoami` do NOT reliably reflect whether a real
|
|
5
|
+
// invocation will actually work — verified empirically: on this machine
|
|
6
|
+
// `status` reports "Logged in" (exit 0) while a real `cursor-agent -p`
|
|
7
|
+
// call fails outright with "Authentication required. Please run 'agent
|
|
8
|
+
// login' first, or set CURSOR_API_KEY environment variable." `models`
|
|
9
|
+
// behaves the same way (a clean "No models available for this account."
|
|
10
|
+
// even while the session is actually unusable for real invocation).
|
|
11
|
+
//
|
|
12
|
+
// So the only decisive signal for "can Cursor actually be invoked right
|
|
13
|
+
// now" is a real invocation attempt. This probe uses the cheapest one
|
|
14
|
+
// available: a harmless -p call whose failure mode, when unauthenticated,
|
|
15
|
+
// is a local, near-instant CLI rejection (no network round trip) — it
|
|
16
|
+
// has NOT been confirmed whether a genuinely authenticated probe call
|
|
17
|
+
// itself consumes real usage, so this should only be called where a real
|
|
18
|
+
// eligibility decision is actually needed, not on a hot path.
|
|
19
|
+
//
|
|
20
|
+
// --trust is required here too (verified empirically — the probe's cwd,
|
|
21
|
+
// an OS tmpdir cursor-agent has never seen, otherwise triggers an
|
|
22
|
+
// interactive "Workspace Trust Required" prompt that blocks non-
|
|
23
|
+
// interactive use, exactly like a real Bootstrap Analysis snapshot dir
|
|
24
|
+
// would without it).
|
|
25
|
+
// A real -p call to a real model has been observed taking just over
|
|
26
|
+
// 20s — verified empirically, not assumed — so this needs real margin,
|
|
27
|
+
// not a tight bound.
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
29
|
+
const SOURCE = "cursor-agent -p (probe)";
|
|
30
|
+
const AUTH_REQUIRED_PATTERN = /authentication required/i;
|
|
31
|
+
|
|
32
|
+
function unknown(error = null) {
|
|
33
|
+
return { authenticated: false, status: "unknown", source: SOURCE, reason: error ? String(error) : null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Real, decisive probe for whether Cursor can actually be invoked right
|
|
38
|
+
* now — NOT whether `status`/`whoami` merely claim it can.
|
|
39
|
+
* @returns {Promise<{authenticated: boolean, status: "measured"|"unknown", source: string, reason: string|null}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function probeCursorAuth({
|
|
42
|
+
spawn = defaultSpawn,
|
|
43
|
+
cwd = tmpdir(),
|
|
44
|
+
env = process.env,
|
|
45
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
46
|
+
} = {}) {
|
|
47
|
+
const args = [
|
|
48
|
+
"-p", "Reply with the single word: ok", "--output-format", "json",
|
|
49
|
+
"--mode", "ask", "--sandbox", "enabled", "--workspace", cwd, "--trust"
|
|
50
|
+
];
|
|
51
|
+
let child;
|
|
52
|
+
try {
|
|
53
|
+
child = spawn("cursor-agent", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return unknown(error?.message ?? error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
let stdout = "";
|
|
60
|
+
let stderr = "";
|
|
61
|
+
let finished = false;
|
|
62
|
+
const timer = setTimeout(() => finish(unknown("cursor-agent -p probe timed out")), timeoutMs);
|
|
63
|
+
|
|
64
|
+
function finish(result) {
|
|
65
|
+
if (finished) return;
|
|
66
|
+
finished = true;
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
69
|
+
resolve(result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
73
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
74
|
+
child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
|
|
75
|
+
child.once?.("close", (code, signal) => {
|
|
76
|
+
const combined = `${stdout}\n${stderr}`;
|
|
77
|
+
if (AUTH_REQUIRED_PATTERN.test(combined)) {
|
|
78
|
+
return finish({
|
|
79
|
+
authenticated: false, status: "measured", source: SOURCE,
|
|
80
|
+
reason: "cursor-agent reports \"Authentication required\" — the CLI's session/keychain is not actually usable for a real invocation, regardless of what `status`/`models` claim."
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (signal) return finish(unknown(`cursor-agent -p probe was killed by signal ${signal}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
84
|
+
if (code !== 0) return finish(unknown(`cursor-agent -p probe exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
85
|
+
finish({ authenticated: true, status: "measured", source: SOURCE, reason: null });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Real per-account model list via `cursor-agent models`. The populated
|
|
4
|
+
// shape below was captured from the real CLI against a real authenticated
|
|
5
|
+
// account with real models enabled (one `<id> - <Display Name>` entry per
|
|
6
|
+
// line, plus a leading "Available models" header and a trailing "Tip: use
|
|
7
|
+
// --model <id>..." line) — not assumed, not the earlier placeholder
|
|
8
|
+
// parser that predated ever seeing a real populated catalog.
|
|
9
|
+
//
|
|
10
|
+
// A process crash (killed by a signal) or a non-zero exit is never
|
|
11
|
+
// reinterpreted as a clean "no models" answer — an empty/partial stdout
|
|
12
|
+
// from a crash mid-run parses identically to a genuine empty catalog, so
|
|
13
|
+
// exit status is the only real signal that tells them apart. Only a clean
|
|
14
|
+
// exit (code 0, no signal) is trusted; anything else yields `status:
|
|
15
|
+
// "unknown"` with the real stderr, never a fabricated empty catalog.
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
17
|
+
const SOURCE = "cursor-agent models";
|
|
18
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
19
|
+
const NO_MODELS_SENTINEL = /no models available/i;
|
|
20
|
+
const LOADING_LINE = /^loading models/i;
|
|
21
|
+
// Real model line shape: "<id> - <Display Name>", id has no spaces (every
|
|
22
|
+
// real id observed is a bare slug like "gpt-5.3-codex-low" or "auto").
|
|
23
|
+
// Header ("Available models") and the trailing "Tip: ..." line never
|
|
24
|
+
// match this — no line-anchored " - " separator — so they're dropped
|
|
25
|
+
// without needing to special-case their exact text.
|
|
26
|
+
const MODEL_LINE_PATTERN = /^(\S+)\s-\s(.+)$/;
|
|
27
|
+
|
|
28
|
+
function unknown(error = null) {
|
|
29
|
+
return { status: "unknown", source: SOURCE, models: [], error: error ? String(error) : null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strips the spinner's ANSI control codes and non-model lines (loading
|
|
34
|
+
* status, the "Available models" header, the trailing "Tip: ..." line),
|
|
35
|
+
* returning either an empty array (the account has no models — a real,
|
|
36
|
+
* confirmed answer) or the real `{id, displayName}` models — the same
|
|
37
|
+
* shape codex-models.js/claude-models.js already use, so callers never
|
|
38
|
+
* need to branch on catalog source.
|
|
39
|
+
*/
|
|
40
|
+
export function parseCursorModelsOutput(raw) {
|
|
41
|
+
const clean = String(raw ?? "").replace(ANSI_PATTERN, "");
|
|
42
|
+
const lines = clean.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !LOADING_LINE.test(line));
|
|
43
|
+
if (lines.some((line) => NO_MODELS_SENTINEL.test(line))) return [];
|
|
44
|
+
const models = [];
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const match = line.match(MODEL_LINE_PATTERN);
|
|
47
|
+
if (match) models.push({ id: match[1], displayName: match[2].trim() });
|
|
48
|
+
}
|
|
49
|
+
return models;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reads Cursor Agent's real, currently-available model list for this
|
|
54
|
+
* account. `status: "measured"` with an empty `models` array is itself a
|
|
55
|
+
* real answer (this account has none) — distinct from `"unknown"`, which
|
|
56
|
+
* means the read itself failed.
|
|
57
|
+
*/
|
|
58
|
+
export async function readCursorModels({
|
|
59
|
+
spawn = defaultSpawn,
|
|
60
|
+
cwd = process.cwd(),
|
|
61
|
+
env = process.env,
|
|
62
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
63
|
+
} = {}) {
|
|
64
|
+
let child;
|
|
65
|
+
try {
|
|
66
|
+
child = spawn("cursor-agent", ["models"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return unknown(error?.message ?? error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let stdout = "";
|
|
73
|
+
let stderr = "";
|
|
74
|
+
let finished = false;
|
|
75
|
+
const timer = setTimeout(() => finish(unknown("cursor-agent models timed out")), timeoutMs);
|
|
76
|
+
|
|
77
|
+
function finish(result) {
|
|
78
|
+
if (finished) return;
|
|
79
|
+
finished = true;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
82
|
+
resolve(result);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
86
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
87
|
+
child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
|
|
88
|
+
child.once?.("close", (code, signal) => {
|
|
89
|
+
// A crash (killed by a signal, e.g. a real SIGSEGV) or a non-zero
|
|
90
|
+
// exit must never be silently reinterpreted as a clean, real
|
|
91
|
+
// "no models" answer — an empty/partial stdout from a crash mid-run
|
|
92
|
+
// parses identically to a genuine empty catalog, so exit status is
|
|
93
|
+
// the only real signal that tells them apart. Only a clean exit
|
|
94
|
+
// (code 0, no signal) is trusted to mean the CLI actually finished
|
|
95
|
+
// and its stdout is a real, complete answer.
|
|
96
|
+
if (signal) return finish(unknown(`cursor-agent models was killed by signal ${signal}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
97
|
+
if (code !== 0) return finish(unknown(`cursor-agent models exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
98
|
+
finish({ status: "measured", source: SOURCE, models: parseCursorModelsOutput(stdout), error: null });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Real per-benchmark leaderboard data from Hugging Face's official Datasets
|
|
2
|
+
// API — verified live against https://huggingface.co/api/datasets/{id}/leaderboard
|
|
3
|
+
// (public, no API key needed for a public benchmark dataset). One dataset
|
|
4
|
+
// covers exactly one benchmark (e.g. cais/hle for HLE, Idavidrein/gpqa for
|
|
5
|
+
// GPQA); there is no cross-benchmark aggregate endpoint here. Fails closed
|
|
6
|
+
// like every other observability probe: a failed fetch never fabricates a
|
|
7
|
+
// result, it falls back to the last successfully cached snapshot (marked
|
|
8
|
+
// "cached", with its real age) or "unknown" if there's no cache either.
|
|
9
|
+
|
|
10
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { harnessHomePaths } from "../paths.js";
|
|
13
|
+
import { writeAtomicJson } from "../runtime/write-atomic-json.js";
|
|
14
|
+
|
|
15
|
+
export const SOURCE = "huggingface datasets api (leaderboard)";
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
17
|
+
|
|
18
|
+
function apiUrl(datasetId) {
|
|
19
|
+
return `https://huggingface.co/api/datasets/${datasetId}/leaderboard`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeEntry(entry) {
|
|
23
|
+
return {
|
|
24
|
+
modelId: entry.modelId ?? null,
|
|
25
|
+
value: typeof entry.value === "number" ? entry.value : null,
|
|
26
|
+
rank: entry.rank ?? null,
|
|
27
|
+
verified: entry.verified === true,
|
|
28
|
+
notes: entry.notes ?? null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ageLabel(fetchedAtIso) {
|
|
33
|
+
const fetchedAt = new Date(fetchedAtIso ?? "").getTime();
|
|
34
|
+
if (!Number.isFinite(fetchedAt)) return null;
|
|
35
|
+
const hours = (Date.now() - fetchedAt) / 3_600_000;
|
|
36
|
+
if (hours < 1) return "<1h";
|
|
37
|
+
if (hours < 48) return `${Math.round(hours)}h`;
|
|
38
|
+
return `${Math.round(hours / 24)}d`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readAllCaches(homeDir, deps) {
|
|
42
|
+
const read = deps.readFile ?? readFile;
|
|
43
|
+
try {
|
|
44
|
+
const raw = await read(harnessHomePaths(homeDir).huggingfaceLeaderboardPath, "utf8");
|
|
45
|
+
const doc = JSON.parse(raw);
|
|
46
|
+
return doc && typeof doc === "object" ? doc : {};
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function writeCache(homeDir, datasetId, snapshot, deps) {
|
|
53
|
+
const mkdirImpl = deps.mkdir ?? mkdir;
|
|
54
|
+
const writeJson = deps.writeAtomicJson ?? writeAtomicJson;
|
|
55
|
+
const path = harnessHomePaths(homeDir).huggingfaceLeaderboardPath;
|
|
56
|
+
const all = await readAllCaches(homeDir, deps);
|
|
57
|
+
all[datasetId] = snapshot;
|
|
58
|
+
await mkdirImpl(dirname(path), { recursive: true });
|
|
59
|
+
await writeJson(path, all);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fromCache(cache, error) {
|
|
63
|
+
if (!cache || !Array.isArray(cache.entries) || typeof cache.fetchedAt !== "string") {
|
|
64
|
+
return { status: "unknown", source: SOURCE, fetchedAt: null, age: null, entries: [], error };
|
|
65
|
+
}
|
|
66
|
+
return { status: "cached", source: SOURCE, fetchedAt: cache.fetchedAt, age: ageLabel(cache.fetchedAt), entries: cache.entries, error };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {object} options
|
|
71
|
+
* @param {string} options.datasetId - e.g. "cais/hle", "SWE-bench/SWE-bench_Verified", "Idavidrein/gpqa"
|
|
72
|
+
* @param {string} options.homeDir - required; the cache lives under this harness home
|
|
73
|
+
* @param {typeof fetch} [options.fetchImpl]
|
|
74
|
+
* @param {number} [options.timeoutMs]
|
|
75
|
+
*/
|
|
76
|
+
export async function readHuggingFaceLeaderboard({
|
|
77
|
+
datasetId, homeDir, fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS, ...deps
|
|
78
|
+
} = {}) {
|
|
79
|
+
if (!datasetId) return { status: "unknown", source: SOURCE, fetchedAt: null, age: null, entries: [], error: "datasetId is required" };
|
|
80
|
+
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetchImpl(apiUrl(datasetId), { signal: controller.signal });
|
|
85
|
+
if (!response.ok) throw new Error(`huggingface leaderboard api returned ${response.status}`);
|
|
86
|
+
const payload = await response.json();
|
|
87
|
+
const entries = Array.isArray(payload) ? payload.map(normalizeEntry) : [];
|
|
88
|
+
const fetchedAt = new Date().toISOString();
|
|
89
|
+
await writeCache(homeDir, datasetId, { fetchedAt, entries }, deps).catch(() => {});
|
|
90
|
+
return { status: "live", source: SOURCE, fetchedAt, age: "<1h", entries, error: null };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const all = await readAllCaches(homeDir, deps);
|
|
93
|
+
return fromCache(all[datasetId], error?.message ?? String(error));
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Real per-provider model catalog via `opencode models <provider> --verbose`
|
|
4
|
+
// — covers both OpenCode Go (provider "opencode-go") and OpenCode Zen
|
|
5
|
+
// (provider "opencode"), the two providers Kairo actually routes through.
|
|
6
|
+
// Same fail-closed contract as codex-models.js: any spawn/parse error or
|
|
7
|
+
// empty catalog yields `unknown`, never a fabricated model list.
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
9
|
+
|
|
10
|
+
function unknown(provider, error = null) {
|
|
11
|
+
return { status: "unknown", source: `opencode models ${provider} --verbose`, provider, models: [], error: error ? String(error) : null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses `opencode models <provider> --verbose` output: a "provider/model"
|
|
16
|
+
* header line followed by a pretty-printed JSON object, repeated per model.
|
|
17
|
+
* Strips the header lines and brace-matches the remaining text into
|
|
18
|
+
* complete JSON objects — robust to the exact indentation/whitespace the
|
|
19
|
+
* CLI uses, since it never assumes a fixed line count per model.
|
|
20
|
+
*/
|
|
21
|
+
export function parseOpenCodeModelsVerbose(text) {
|
|
22
|
+
const lines = String(text ?? "").split("\n");
|
|
23
|
+
const jsonText = lines.filter((line) => !/^[\w.-]+\/[\w.-]+$/.test(line.trim())).join("\n");
|
|
24
|
+
const models = [];
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let start = -1;
|
|
27
|
+
for (let i = 0; i < jsonText.length; i += 1) {
|
|
28
|
+
const char = jsonText[i];
|
|
29
|
+
if (char === "{") {
|
|
30
|
+
if (depth === 0) start = i;
|
|
31
|
+
depth += 1;
|
|
32
|
+
} else if (char === "}") {
|
|
33
|
+
depth -= 1;
|
|
34
|
+
if (depth === 0 && start !== -1) {
|
|
35
|
+
try { models.push(JSON.parse(jsonText.slice(start, i + 1))); } catch { /* skip an unparseable entry, don't fail the whole catalog */ }
|
|
36
|
+
start = -1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return models;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeModel(entry) {
|
|
44
|
+
return {
|
|
45
|
+
id: entry.id,
|
|
46
|
+
providerID: entry.providerID,
|
|
47
|
+
displayName: entry.name ?? entry.id,
|
|
48
|
+
status: entry.status ?? "unknown",
|
|
49
|
+
costInputPerMTok: entry.cost?.input ?? null,
|
|
50
|
+
costOutputPerMTok: entry.cost?.output ?? null,
|
|
51
|
+
contextWindow: entry.limit?.context ?? null,
|
|
52
|
+
maxOutput: entry.limit?.output ?? null,
|
|
53
|
+
supportsReasoning: entry.capabilities?.reasoning === true,
|
|
54
|
+
supportsToolCall: entry.capabilities?.toolcall === true
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reads the real, currently-listed model catalog for one OpenCode provider
|
|
60
|
+
* ("opencode-go" or "opencode" for Zen). Requires the `opencode` CLI to be
|
|
61
|
+
* installed and authenticated — same as the rest of the OpenCode adapter.
|
|
62
|
+
*/
|
|
63
|
+
export async function readOpenCodeModels({
|
|
64
|
+
provider,
|
|
65
|
+
spawn = defaultSpawn,
|
|
66
|
+
cwd = process.cwd(),
|
|
67
|
+
env = process.env,
|
|
68
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
69
|
+
} = {}) {
|
|
70
|
+
if (!provider) return unknown("unknown", "provider is required");
|
|
71
|
+
|
|
72
|
+
let child;
|
|
73
|
+
try {
|
|
74
|
+
child = spawn("opencode", ["models", provider, "--verbose"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return unknown(provider, error?.message ?? error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
let stdout = "";
|
|
81
|
+
let finished = false;
|
|
82
|
+
const timer = setTimeout(() => finish(unknown(provider, "opencode models timed out")), timeoutMs);
|
|
83
|
+
|
|
84
|
+
function finish(result) {
|
|
85
|
+
if (finished) return;
|
|
86
|
+
finished = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
89
|
+
resolve(result);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
93
|
+
child.once?.("error", (error) => finish(unknown(provider, error?.message ?? error)));
|
|
94
|
+
child.once?.("close", (code) => {
|
|
95
|
+
if (code !== 0 && !stdout.trim()) return finish(unknown(provider, `opencode models exited with code ${code}`));
|
|
96
|
+
const models = parseOpenCodeModelsVerbose(stdout).map(normalizeModel);
|
|
97
|
+
if (models.length === 0) return finish(unknown(provider, "no models parsed"));
|
|
98
|
+
finish({ status: "measured", source: `opencode models ${provider} --verbose`, provider, models, error: null });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|