@kal-elsam/kairo-runtime 0.16.0 → 0.17.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 +50 -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 +475 -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 +683 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1263 -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 +1063 -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 +444 -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,41 @@
|
|
|
1
|
+
// Real subscription quota pressure — how much headroom is actually left
|
|
2
|
+
// on each provider's account right now (from the same usage probes
|
|
3
|
+
// service.js already fetches for /usage). This is a PROVIDER-level
|
|
4
|
+
// signal, not a per-model efficiency measurement: quota is account-wide,
|
|
5
|
+
// so it is modeled here as its own ProviderCapacity map, keyed by
|
|
6
|
+
// adapterId — never copied into the per-model capability registry as if
|
|
7
|
+
// it were evidence about an individual model. Two models from the same
|
|
8
|
+
// provider share the exact same real capacity entry; there is no way to
|
|
9
|
+
// represent "this model is more quota-efficient than that one" here,
|
|
10
|
+
// because that isn't a real, measurable thing — only the provider's
|
|
11
|
+
// account has quota.
|
|
12
|
+
//
|
|
13
|
+
// EFFICIENT TEAM (model-intelligence.js) resolves this separately from
|
|
14
|
+
// its per-model EFFICIENCY_DIMENSIONS, and deliberately checks it LAST —
|
|
15
|
+
// only when no real per-model signal (consumption, cost, duration, price,
|
|
16
|
+
// throughput) distinguishes otherwise-adequate candidates. A provider's
|
|
17
|
+
// spare quota must never, by itself, decide who wins a role over a model
|
|
18
|
+
// with genuinely better per-task economics.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} ProviderCapacity
|
|
22
|
+
* @property {string} adapterId
|
|
23
|
+
* @property {number} [quotaRemainingPercent]
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds a real ProviderCapacity map from the same remaining-percent
|
|
28
|
+
* figures already computed for /usage — one entry per adapter that
|
|
29
|
+
* reported a real number, never a fabricated one for an adapter with no
|
|
30
|
+
* data.
|
|
31
|
+
* @param {Record<string, number|null|undefined>} remainingPercentByAdapter
|
|
32
|
+
* @returns {Record<string, ProviderCapacity>}
|
|
33
|
+
*/
|
|
34
|
+
export function buildProviderCapacity(remainingPercentByAdapter) {
|
|
35
|
+
const capacity = {};
|
|
36
|
+
for (const [adapterId, remainingPercent] of Object.entries(remainingPercentByAdapter ?? {})) {
|
|
37
|
+
if (remainingPercent == null) continue;
|
|
38
|
+
capacity[adapterId] = { adapterId, quotaRemainingPercent: remainingPercent };
|
|
39
|
+
}
|
|
40
|
+
return capacity;
|
|
41
|
+
}
|
|
@@ -18,16 +18,35 @@ import {
|
|
|
18
18
|
workSnapshotPublishSchema
|
|
19
19
|
} from "./work-snapshot-tool.js";
|
|
20
20
|
import { resolveMcpWorkspaceCwd } from "./resolve-mcp-workspace.js";
|
|
21
|
+
import {
|
|
22
|
+
WORKSPACE_BINDING_CODES,
|
|
23
|
+
resolveWorkspaceWriteBinding
|
|
24
|
+
} from "./workspace-binding.js";
|
|
21
25
|
|
|
22
|
-
/** Sole MCP write tool for companion snapshots. */
|
|
26
|
+
/** Sole MCP write tool for companion snapshots. Bound servers only. */
|
|
23
27
|
export const KAIRO_MCP_WRITE_TOOLS = Object.freeze(["kairo_publish_work_snapshot"]);
|
|
24
28
|
|
|
25
|
-
export const
|
|
29
|
+
export const KAIRO_MCP_READ_TOOLS = Object.freeze([
|
|
26
30
|
"kairo_status", "kairo_runs", "kairo_alerts", "kairo_gentle_status",
|
|
27
|
-
"kairo_graph_query", "kairo_graph_path", "kairo_context_summary", "kairo_fleet"
|
|
31
|
+
"kairo_graph_query", "kairo_graph_path", "kairo_context_summary", "kairo_fleet"
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export const KAIRO_MCP_TOOLS = Object.freeze([
|
|
35
|
+
...KAIRO_MCP_READ_TOOLS,
|
|
28
36
|
...KAIRO_MCP_WRITE_TOOLS
|
|
29
37
|
]);
|
|
30
38
|
|
|
39
|
+
export function mcpWorkspaceBinding(deps = {}) {
|
|
40
|
+
return resolveWorkspaceWriteBinding({
|
|
41
|
+
workspaceBound: deps.workspaceBound === true,
|
|
42
|
+
cwdExplicit: deps.cwdExplicit === true,
|
|
43
|
+
cwd: deps.cwd,
|
|
44
|
+
processCwd: deps.processCwd ?? process.cwd(),
|
|
45
|
+
userHome: deps.userHome,
|
|
46
|
+
env: deps.env ?? process.env
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
const empty = z.object({});
|
|
32
51
|
export const mcpSchemas = Object.freeze({
|
|
33
52
|
empty,
|
|
@@ -105,11 +124,14 @@ function graphEnvelope(result) {
|
|
|
105
124
|
|
|
106
125
|
export function createToolHandlers(deps = {}) {
|
|
107
126
|
const homeDir = deps.homeDir ?? resolveHomeDir();
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
const env = deps.env ?? process.env;
|
|
128
|
+
const binding = mcpWorkspaceBinding({ ...deps, env });
|
|
129
|
+
const cwd = binding.writable
|
|
130
|
+
? binding.cwd
|
|
131
|
+
: resolveMcpWorkspaceCwd({
|
|
132
|
+
cwd: deps.cwdExplicit === true ? deps.cwd : (binding.bound ? undefined : deps.cwd),
|
|
133
|
+
env: binding.bound ? {} : env
|
|
134
|
+
});
|
|
113
135
|
const listRuns = deps.listRuns ?? ((o) => listRunRecords(homeDir, o));
|
|
114
136
|
const listAlertRows = deps.listAlerts ?? ((o) => listAlerts({ homeDir, ...o }));
|
|
115
137
|
const listReviews = deps.listReviews ?? (() => listReviewReceipts({ homeDir, limit: 20 }));
|
|
@@ -238,20 +260,26 @@ export function createToolHandlers(deps = {}) {
|
|
|
238
260
|
});
|
|
239
261
|
}
|
|
240
262
|
},
|
|
241
|
-
kairo_publish_work_snapshot:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
263
|
+
kairo_publish_work_snapshot: async (args = {}) => {
|
|
264
|
+
if (!binding.writable) {
|
|
265
|
+
const code = binding.code ?? WORKSPACE_BINDING_CODES.UNBOUND;
|
|
266
|
+
return mcpResult({ ok: false, code, data: null, diagnostics: [code], isError: true });
|
|
267
|
+
}
|
|
268
|
+
return createPublishWorkSnapshotHandler({
|
|
269
|
+
homeDir,
|
|
270
|
+
cwd: binding.cwd,
|
|
271
|
+
now: deps.now,
|
|
272
|
+
writeAtomic: deps.writeAtomic,
|
|
273
|
+
publishWorkSnapshot: deps.publishWorkSnapshot,
|
|
274
|
+
mcpResult
|
|
275
|
+
})(args);
|
|
276
|
+
}
|
|
249
277
|
};
|
|
250
278
|
}
|
|
251
279
|
|
|
252
280
|
export function registerKairoMcpTools(registerTool, deps = {}) {
|
|
253
281
|
const h = createToolHandlers(deps);
|
|
254
|
-
|
|
282
|
+
const catalog = [
|
|
255
283
|
["kairo_status", "Read-only control-plane + companion summary", empty],
|
|
256
284
|
["kairo_runs", "Read-only run list", mcpSchemas.runs],
|
|
257
285
|
["kairo_alerts", "Read-only alert list", mcpSchemas.alerts],
|
|
@@ -265,7 +293,12 @@ export function registerKairoMcpTools(registerTool, deps = {}) {
|
|
|
265
293
|
"Publish kairo.work-snapshot/v1 for the runtime workspace (enrolls conversation)",
|
|
266
294
|
mcpSchemas.workSnapshotPublish
|
|
267
295
|
]
|
|
268
|
-
]
|
|
296
|
+
];
|
|
297
|
+
const bound = deps.workspaceBound === true;
|
|
298
|
+
for (const [name, description, inputSchema] of catalog) {
|
|
299
|
+
if (bound ? !KAIRO_MCP_WRITE_TOOLS.includes(name) : KAIRO_MCP_WRITE_TOOLS.includes(name)) continue;
|
|
300
|
+
registerTool(name, { description, inputSchema }, h[name]);
|
|
301
|
+
}
|
|
269
302
|
return h;
|
|
270
303
|
}
|
|
271
304
|
|
|
@@ -18,13 +18,15 @@ export function resolveWorkSnapshotRulePath(homeDir = homedir()) {
|
|
|
18
18
|
|
|
19
19
|
export const WORK_SNAPSHOT_RULE_BODY = `# Kairo work snapshot
|
|
20
20
|
|
|
21
|
-
After each significant turn, publish the true work state with MCP \`kairo_publish_work_snapshot
|
|
21
|
+
After each significant turn, publish the true work state with MCP \`kairo_publish_work_snapshot\` **only on \`kairo-workspace\`** (\`kairo mcp --workspace-bound --cwd <absolute-folder>\`; unique \`WORKSPACE_FOLDER_PATHS\` may bind):
|
|
22
22
|
|
|
23
23
|
- Required: \`conversationId\`, \`provider\` (\`cursor\`), \`goal\`, \`now\`, \`next\`
|
|
24
24
|
- Optional: \`progress\` (≤3), \`blockers\`, \`delegations\` (only real ones)
|
|
25
|
-
- Workspace identity is derived by Kairo from the
|
|
25
|
+
- Workspace identity is derived by Kairo from the bound process — never send \`projectKey\`, paths, or \`cwd\`
|
|
26
26
|
- Never invent work. Never send prompts, transcripts, or tool dumps
|
|
27
27
|
- Reuse the same \`conversationId\` for later turns in this chat
|
|
28
|
+
- If the publish tool is missing, stop. The global \`kairo\` MCP does not register it. Do not retry with paths.
|
|
29
|
+
- On the bound server, \`workspace_ambiguous\` or \`workspace_mismatch\` also means stop. Never retry with paths.
|
|
28
30
|
`;
|
|
29
31
|
|
|
30
32
|
export function buildWorkSnapshotRuleFile() {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit workspace binding for MCP writes.
|
|
3
|
+
* Agents never supply paths. VSCODE_CWD never authorizes writes.
|
|
4
|
+
* Canonical `--cwd` may match process.cwd() or the unique
|
|
5
|
+
* WORKSPACE_FOLDER_PATHS entry (Cursor stdio has no cwd field).
|
|
6
|
+
*/
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { statSync } from "node:fs";
|
|
10
|
+
import { canonicalizeProjectPath, projectKeyForPath } from "../next/project-key.js";
|
|
11
|
+
import { parseWorkspaceFolderPaths } from "./resolve-mcp-workspace.js";
|
|
12
|
+
|
|
13
|
+
export const WORKSPACE_BINDING_CODES = Object.freeze({
|
|
14
|
+
UNBOUND: "workspace_unbound",
|
|
15
|
+
AMBIGUOUS: "workspace_ambiguous",
|
|
16
|
+
MISMATCH: "workspace_mismatch"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function fail(code, bound = false) {
|
|
20
|
+
return { writable: false, bound, cwd: null, projectKey: null, code };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isForbiddenWriteRoot(canonical, userHome) {
|
|
24
|
+
const root = canonicalizeProjectPath("/");
|
|
25
|
+
if (canonical === root || canonical === "/") return true;
|
|
26
|
+
return canonical === canonicalizeProjectPath(userHome);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tryCanonical(pathValue) {
|
|
30
|
+
try {
|
|
31
|
+
return canonicalizeProjectPath(pathValue);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uniqueFolderCanonical(env) {
|
|
38
|
+
const folders = parseWorkspaceFolderPaths(env.WORKSPACE_FOLDER_PATHS);
|
|
39
|
+
if (folders.length > 1) return { code: WORKSPACE_BINDING_CODES.AMBIGUOUS, canonical: null };
|
|
40
|
+
if (folders.length === 0) return { code: null, canonical: null };
|
|
41
|
+
const canonical = tryCanonical(folders[0]);
|
|
42
|
+
return { code: canonical ? null : WORKSPACE_BINDING_CODES.UNBOUND, canonical };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveWorkspaceWriteBinding({
|
|
46
|
+
workspaceBound = false,
|
|
47
|
+
cwdExplicit = false,
|
|
48
|
+
cwd,
|
|
49
|
+
processCwd = process.cwd(),
|
|
50
|
+
userHome = homedir(),
|
|
51
|
+
env = process.env
|
|
52
|
+
} = {}) {
|
|
53
|
+
if (!workspaceBound) return fail(WORKSPACE_BINDING_CODES.UNBOUND);
|
|
54
|
+
if (!cwdExplicit || typeof cwd !== "string" || !cwd.trim()) {
|
|
55
|
+
return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const canonical = tryCanonical(resolve(processCwd, cwd));
|
|
59
|
+
if (!canonical) return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
|
|
60
|
+
try {
|
|
61
|
+
if (!statSync(canonical).isDirectory()) {
|
|
62
|
+
return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
|
|
66
|
+
}
|
|
67
|
+
if (isForbiddenWriteRoot(canonical, userHome)) {
|
|
68
|
+
return fail(WORKSPACE_BINDING_CODES.MISMATCH, true);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const folder = uniqueFolderCanonical(env);
|
|
72
|
+
if (folder.code) return fail(folder.code, true);
|
|
73
|
+
|
|
74
|
+
const processCanonical = tryCanonical(processCwd);
|
|
75
|
+
const matchesProcess = processCanonical != null && canonical === processCanonical;
|
|
76
|
+
const matchesFolder = folder.canonical != null && canonical === folder.canonical;
|
|
77
|
+
if (!matchesProcess && !matchesFolder) {
|
|
78
|
+
return fail(WORKSPACE_BINDING_CODES.MISMATCH, true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
writable: true,
|
|
83
|
+
bound: true,
|
|
84
|
+
cwd: canonical,
|
|
85
|
+
projectKey: projectKeyForPath(canonical),
|
|
86
|
+
code: null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VSIX write MCP: publish only. Global/unbound reads stay on `kairo mcp`.
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
5
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
6
|
+
import {
|
|
7
|
+
createPublishWorkSnapshotHandler,
|
|
8
|
+
workSnapshotPublishSchema
|
|
9
|
+
} from "./work-snapshot-tool.js";
|
|
10
|
+
import {
|
|
11
|
+
WORKSPACE_BINDING_CODES,
|
|
12
|
+
resolveWorkspaceWriteBinding
|
|
13
|
+
} from "./workspace-binding.js";
|
|
14
|
+
|
|
15
|
+
export function parseWorkspaceMcpArgv(argv = []) {
|
|
16
|
+
const args = [...argv];
|
|
17
|
+
if (args[0] === "mcp") args.shift();
|
|
18
|
+
let workspaceBound = false;
|
|
19
|
+
let cwd;
|
|
20
|
+
let cwdExplicit = false;
|
|
21
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
22
|
+
if (args[i] === "--workspace-bound") workspaceBound = true;
|
|
23
|
+
else if (args[i] === "--cwd") {
|
|
24
|
+
cwd = args[++i];
|
|
25
|
+
cwdExplicit = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { workspaceBound, cwd, cwdExplicit };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mcpResult({ ok, code, data = null, diagnostics = [], isError = false }) {
|
|
32
|
+
const structuredContent = { ok, code, data, diagnostics };
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
35
|
+
structuredContent, ...(isError ? { isError: true } : {})
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function startWorkspaceMcp(argv = process.argv.slice(2), deps = {}) {
|
|
40
|
+
const parsed = parseWorkspaceMcpArgv(argv);
|
|
41
|
+
const merged = { ...deps, ...parsed };
|
|
42
|
+
const binding = resolveWorkspaceWriteBinding({
|
|
43
|
+
workspaceBound: merged.workspaceBound === true,
|
|
44
|
+
cwdExplicit: merged.cwdExplicit === true,
|
|
45
|
+
cwd: merged.cwd,
|
|
46
|
+
processCwd: merged.processCwd ?? process.cwd(),
|
|
47
|
+
userHome: merged.userHome,
|
|
48
|
+
env: merged.env ?? process.env
|
|
49
|
+
});
|
|
50
|
+
const publish = createPublishWorkSnapshotHandler({
|
|
51
|
+
homeDir: merged.homeDir, cwd: binding.cwd, now: merged.now,
|
|
52
|
+
writeAtomic: merged.writeAtomic, publishWorkSnapshot: merged.publishWorkSnapshot,
|
|
53
|
+
mcpResult
|
|
54
|
+
});
|
|
55
|
+
const serve = deps.serveStdio ?? serveStdio;
|
|
56
|
+
return serve(() => {
|
|
57
|
+
const server = new (deps.McpServer ?? McpServer)({ name: "kairo-workspace", version: "0.8.0" });
|
|
58
|
+
const register = deps.registerTool ?? ((name, config, handler) => server.registerTool(name, config, handler));
|
|
59
|
+
register("kairo_publish_work_snapshot", {
|
|
60
|
+
description: "Publish kairo.work-snapshot/v1 for the runtime workspace (enrolls conversation)",
|
|
61
|
+
inputSchema: workSnapshotPublishSchema
|
|
62
|
+
}, async (args = {}) => {
|
|
63
|
+
if (!binding.writable) {
|
|
64
|
+
const code = binding.code ?? WORKSPACE_BINDING_CODES.UNBOUND;
|
|
65
|
+
return mcpResult({ ok: false, code, data: null, diagnostics: [code], isError: true });
|
|
66
|
+
}
|
|
67
|
+
return publish(args);
|
|
68
|
+
});
|
|
69
|
+
return server;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const invoked = process.argv[1] ?? "";
|
|
74
|
+
if (/(?:kairo-workspace\.cjs|workspace-mcp-entry\.js)$/.test(invoked)) void startWorkspaceMcp();
|
|
@@ -227,7 +227,14 @@ export async function runMcpCli(options = {}) {
|
|
|
227
227
|
if (action === "serve" || action == null) {
|
|
228
228
|
const { runKairoMcp } = await import("./mcp/kairo-mcp.js");
|
|
229
229
|
return runKairoMcp({
|
|
230
|
-
cwd: resolveMcpServeCwd(options),
|
|
230
|
+
cwd: resolveMcpServeCwd(options) ?? options.cwd,
|
|
231
|
+
cwdExplicit: options.cwdExplicit === true,
|
|
232
|
+
workspaceBound: options.workspaceBound === true,
|
|
233
|
+
processCwd: options.processCwd ?? process.cwd(),
|
|
234
|
+
homeDir: options.homeDir,
|
|
235
|
+
env: options.env,
|
|
236
|
+
registerTool: options.registerTool,
|
|
237
|
+
serveStdio: options.serveStdio,
|
|
231
238
|
packageRoot: options.packageRoot,
|
|
232
239
|
packageName: options.packageName,
|
|
233
240
|
version: options.version
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Real per-model benchmark scores from Artificial Analysis's official Data
|
|
2
|
+
// API (verified live against https://artificialanalysis.ai/api/v2/data/llms/models
|
|
3
|
+
// — not scraped from their leaderboard website, which has no stable JSON
|
|
4
|
+
// endpoint). Fails closed like every other observability probe: no API key
|
|
5
|
+
// or a failed fetch never fabricates a score, it falls back to the last
|
|
6
|
+
// successfully cached snapshot (marked "cached", with its real age) or
|
|
7
|
+
// "unknown" if there's no cache either.
|
|
8
|
+
|
|
9
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
import { harnessHomePaths } from "../paths.js";
|
|
12
|
+
import { writeAtomicJson } from "../runtime/write-atomic-json.js";
|
|
13
|
+
|
|
14
|
+
export const SOURCE = "artificial-analysis api v2 (data/llms/models)";
|
|
15
|
+
const API_URL = "https://artificialanalysis.ai/api/v2/data/llms/models";
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
17
|
+
|
|
18
|
+
function normalizeModel(entry) {
|
|
19
|
+
const evaluations = entry.evaluations ?? {};
|
|
20
|
+
return {
|
|
21
|
+
slug: entry.slug,
|
|
22
|
+
name: entry.name,
|
|
23
|
+
creator: entry.model_creator?.slug ?? null,
|
|
24
|
+
intelligenceIndex: evaluations.artificial_analysis_intelligence_index ?? null,
|
|
25
|
+
codingIndex: evaluations.artificial_analysis_coding_index ?? null,
|
|
26
|
+
mathIndex: evaluations.artificial_analysis_math_index ?? null,
|
|
27
|
+
// Real per-benchmark scores the free API already returns alongside the
|
|
28
|
+
// composite indices above — verified live (not documented anywhere as
|
|
29
|
+
// a free-tier feature, an earlier assumption here was wrong). These
|
|
30
|
+
// matter because the composite intelligenceIndex can blur a real,
|
|
31
|
+
// benchmark-specific near-tie: e.g. two models 18% apart on the
|
|
32
|
+
// composite can be 0.2 points apart on GPQA specifically. Real,
|
|
33
|
+
// 0-1 scale values as AA reports them — never rescaled or blended.
|
|
34
|
+
gpqa: evaluations.gpqa ?? null,
|
|
35
|
+
hle: evaluations.hle ?? null,
|
|
36
|
+
sciCode: evaluations.scicode ?? null,
|
|
37
|
+
mmluPro: evaluations.mmlu_pro ?? null,
|
|
38
|
+
liveCodeBench: evaluations.livecodebench ?? null,
|
|
39
|
+
ifBench: evaluations.ifbench ?? null,
|
|
40
|
+
terminalBenchHard: evaluations.terminalbench_hard ?? null,
|
|
41
|
+
terminalBenchV2: evaluations.terminalbench_v2_1 ?? null,
|
|
42
|
+
tau2: evaluations.tau2 ?? null,
|
|
43
|
+
tauBanking: evaluations.tau_banking ?? null,
|
|
44
|
+
// Real reported numbers, not derived scores — used to flag the
|
|
45
|
+
// cheapest/fastest real option among what you actually have access to,
|
|
46
|
+
// never blended into a single invented composite.
|
|
47
|
+
priceInputPerMTok: entry.pricing?.price_1m_input_tokens ?? null,
|
|
48
|
+
priceOutputPerMTok: entry.pricing?.price_1m_output_tokens ?? null,
|
|
49
|
+
outputTokensPerSecond: entry.median_output_tokens_per_second ?? null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ageLabel(fetchedAtIso) {
|
|
54
|
+
const fetchedAt = new Date(fetchedAtIso ?? "").getTime();
|
|
55
|
+
if (!Number.isFinite(fetchedAt)) return null;
|
|
56
|
+
const hours = (Date.now() - fetchedAt) / 3_600_000;
|
|
57
|
+
if (hours < 1) return "<1h";
|
|
58
|
+
if (hours < 48) return `${Math.round(hours)}h`;
|
|
59
|
+
return `${Math.round(hours / 24)}d`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readCache(homeDir, deps) {
|
|
63
|
+
const read = deps.readFile ?? readFile;
|
|
64
|
+
try {
|
|
65
|
+
const raw = await read(harnessHomePaths(homeDir).modelIntelligencePath, "utf8");
|
|
66
|
+
const doc = JSON.parse(raw);
|
|
67
|
+
if (!Array.isArray(doc?.models) || typeof doc.fetchedAt !== "string") return null;
|
|
68
|
+
return doc;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function writeCache(homeDir, doc, deps) {
|
|
75
|
+
const mkdirImpl = deps.mkdir ?? mkdir;
|
|
76
|
+
const writeJson = deps.writeAtomicJson ?? writeAtomicJson;
|
|
77
|
+
const path = harnessHomePaths(homeDir).modelIntelligencePath;
|
|
78
|
+
await mkdirImpl(dirname(path), { recursive: true });
|
|
79
|
+
await writeJson(path, doc);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fromCache(cache, error) {
|
|
83
|
+
if (!cache) return { status: "unknown", source: SOURCE, fetchedAt: null, age: null, models: [], error };
|
|
84
|
+
return { status: "cached", source: SOURCE, fetchedAt: cache.fetchedAt, age: ageLabel(cache.fetchedAt), models: cache.models, error };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {object} [options]
|
|
89
|
+
* @param {string|null} [options.apiKey] - defaults to ARTIFICIAL_ANALYSIS_API_KEY; never hardcode a key
|
|
90
|
+
* @param {string} options.homeDir - required; the cache lives under this harness home
|
|
91
|
+
* @param {typeof fetch} [options.fetchImpl]
|
|
92
|
+
* @param {number} [options.timeoutMs]
|
|
93
|
+
*/
|
|
94
|
+
export async function readArtificialAnalysisModels({
|
|
95
|
+
apiKey = process.env.ARTIFICIAL_ANALYSIS_API_KEY ?? null,
|
|
96
|
+
homeDir,
|
|
97
|
+
fetchImpl = fetch,
|
|
98
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
99
|
+
...deps
|
|
100
|
+
} = {}) {
|
|
101
|
+
if (!apiKey) return fromCache(await readCache(homeDir, deps), "no API key configured");
|
|
102
|
+
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetchImpl(API_URL, { headers: { "x-api-key": apiKey }, signal: controller.signal });
|
|
107
|
+
if (!response.ok) throw new Error(`artificial analysis api returned ${response.status}`);
|
|
108
|
+
const payload = await response.json();
|
|
109
|
+
const models = Array.isArray(payload?.data) ? payload.data.map(normalizeModel) : [];
|
|
110
|
+
const fetchedAt = new Date().toISOString();
|
|
111
|
+
await writeCache(homeDir, { fetchedAt, models }, deps).catch(() => {});
|
|
112
|
+
return { status: "live", source: SOURCE, fetchedAt, age: "<1h", models, error: null };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return fromCache(await readCache(homeDir, deps), error?.message ?? String(error));
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Claude Code's CLI has no model-discovery command (`claude --help` exposes
|
|
2
|
+
// no `models` subcommand, unlike Codex's `model/list` RPC or OpenCode's
|
|
3
|
+
// `models` command) — verified by inspecting its full --help output, not
|
|
4
|
+
// assumed. So there is no live, per-account catalog to read.
|
|
5
|
+
//
|
|
6
|
+
// This is the documented model catalog only — current as of this file's
|
|
7
|
+
// last update — NOT a live entitlement check. It intentionally carries
|
|
8
|
+
// `status: "documented"` (never "measured") so callers can't mistake it for
|
|
9
|
+
// verified data the way Codex/OpenCode/Cursor's catalogs are.
|
|
10
|
+
const SOURCE = "documented catalog (no live discovery command exists for claude)";
|
|
11
|
+
|
|
12
|
+
const DOCUMENTED_MODELS = [
|
|
13
|
+
{ id: "claude-opus-5", displayName: "Claude Opus 5" },
|
|
14
|
+
{ id: "claude-sonnet-5", displayName: "Claude Sonnet 5" },
|
|
15
|
+
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5" },
|
|
16
|
+
{ id: "claude-fable-5-1", displayName: "Claude Fable 5.1" },
|
|
17
|
+
{ id: "claude-fable-5", displayName: "Claude Fable 5" },
|
|
18
|
+
{ id: "claude-opus-4-8", displayName: "Claude Opus 4.8" },
|
|
19
|
+
{ id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
|
|
20
|
+
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6" },
|
|
21
|
+
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" }
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the documented Claude model catalog. Always synchronous and
|
|
26
|
+
* always `status: "documented"` — there is nothing to fail closed on since
|
|
27
|
+
* no live read is attempted.
|
|
28
|
+
*/
|
|
29
|
+
export function readClaudeModels() {
|
|
30
|
+
return { status: "documented", source: SOURCE, models: DOCUMENTED_MODELS.slice(), error: null };
|
|
31
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Claude Code's `/usage` is a local_command — intercepted client-side before
|
|
4
|
+
// it reaches the model, so `claude -p "/usage" --output-format json` returns
|
|
5
|
+
// real session/weekly percentages at zero cost (total_cost_usd: 0, all token
|
|
6
|
+
// counts 0). This mirrors codex-usage.js's app-server approach: a real,
|
|
7
|
+
// zero-cost, local read, never a fabricated quota.
|
|
8
|
+
const SOURCE = 'claude -p "/usage" (local_command, zero-cost)';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
10
|
+
|
|
11
|
+
function unknown(error = null) {
|
|
12
|
+
return {
|
|
13
|
+
status: "unknown",
|
|
14
|
+
source: SOURCE,
|
|
15
|
+
windows: [],
|
|
16
|
+
primary: null,
|
|
17
|
+
secondary: null,
|
|
18
|
+
raw: null,
|
|
19
|
+
error: error ? String(error) : null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses lines like:
|
|
25
|
+
* "Current session: 0% used · resets Sep 11 at 7:09pm (America/Mexico_City)"
|
|
26
|
+
* "Current week (all models): 7% used · resets Sep 13 at 7:59am (America/Mexico_City)"
|
|
27
|
+
* into normalized usage windows. Unrecognized lines are skipped, not guessed.
|
|
28
|
+
*/
|
|
29
|
+
export function parseClaudeUsageText(text) {
|
|
30
|
+
const windows = [];
|
|
31
|
+
for (const line of String(text ?? "").split("\n")) {
|
|
32
|
+
const match = line.match(/^(.+?):\s*(\d+)%\s*used(?:\s*·\s*resets\s*(.+))?\s*$/);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
const usedPercent = Number(match[2]);
|
|
35
|
+
if (!Number.isFinite(usedPercent)) continue;
|
|
36
|
+
windows.push({
|
|
37
|
+
label: match[1].trim(),
|
|
38
|
+
usedPercent,
|
|
39
|
+
remainingPercent: 100 - usedPercent,
|
|
40
|
+
resetsAt: match[3] ? match[3].trim() : null
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return windows;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reads Claude Code's own real session/weekly usage percentages via its
|
|
48
|
+
* `/usage` local_command, without starting a model turn. Fail-closed: any
|
|
49
|
+
* spawn error, timeout, malformed JSON, or unparseable response yields
|
|
50
|
+
* `unknown`, never a fabricated percentage.
|
|
51
|
+
*/
|
|
52
|
+
export async function readClaudeUsage({
|
|
53
|
+
spawn = defaultSpawn,
|
|
54
|
+
cwd = process.cwd(),
|
|
55
|
+
env = process.env,
|
|
56
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
57
|
+
} = {}) {
|
|
58
|
+
let child;
|
|
59
|
+
try {
|
|
60
|
+
child = spawn("claude", ["-p", "/usage", "--output-format", "json"], {
|
|
61
|
+
cwd,
|
|
62
|
+
env,
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return unknown(error?.message ?? error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let stdout = "";
|
|
71
|
+
let finished = false;
|
|
72
|
+
const timer = setTimeout(() => finish(unknown("claude -p \"/usage\" timed out")), timeoutMs);
|
|
73
|
+
|
|
74
|
+
function finish(result) {
|
|
75
|
+
if (finished) return;
|
|
76
|
+
finished = true;
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
79
|
+
resolve(result);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
83
|
+
child.on("error", (error) => finish(unknown(error?.message ?? error)));
|
|
84
|
+
child.on("close", () => {
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(stdout);
|
|
88
|
+
} catch {
|
|
89
|
+
finish(unknown("malformed JSON from claude -p \"/usage\""));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (parsed?.local_command !== "usage" || typeof parsed?.result !== "string") {
|
|
93
|
+
finish(unknown("unexpected response shape from claude -p \"/usage\""));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const windows = parseClaudeUsageText(parsed.result);
|
|
97
|
+
if (windows.length === 0) {
|
|
98
|
+
finish(unknown("no usage windows parsed from claude -p \"/usage\""));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
finish({
|
|
102
|
+
status: "measured",
|
|
103
|
+
source: SOURCE,
|
|
104
|
+
windows,
|
|
105
|
+
primary: windows[0] ?? null,
|
|
106
|
+
secondary: windows[1] ?? null,
|
|
107
|
+
raw: parsed.result,
|
|
108
|
+
error: null
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|