@kal-elsam/kairo-runtime 0.24.0 → 0.25.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
CHANGED
|
@@ -5,6 +5,22 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.25.0 — 2026-09-19 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. Cursor joins ASK.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- ASK can now call Cursor, via its real, documented read-only mode
|
|
15
|
+
(`cursor-agent --mode ask`, verified live) — never combined with
|
|
16
|
+
`--force`/`--yolo`. PROJECT TEAM roles assigned to Cursor (Architect/
|
|
17
|
+
Builder/Debugger in this project's own team) are now real ASK
|
|
18
|
+
candidates, not just execution candidates.
|
|
19
|
+
- OpenCode (Go/Zen) stays excluded from ASK: verified its CLI has no
|
|
20
|
+
portable, flag-driven read-only mode — its permission model is
|
|
21
|
+
project/user-config-driven, so a read-only call can't be guaranteed
|
|
22
|
+
safe across different installs.
|
|
23
|
+
|
|
8
24
|
## 0.24.0 — 2026-09-18 (Kairo Runtime)
|
|
9
25
|
|
|
10
26
|
Minor release. ASK mode joins PROJECT TEAM.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
@@ -21,14 +21,15 @@ import { ROLE_CAPABILITIES } from "../intelligence/role-profiles.js";
|
|
|
21
21
|
import { computeRoleEvaluations } from "../intelligence/capability-scoring.js";
|
|
22
22
|
|
|
23
23
|
// The Bootstrap Analyst investigates read-only via askProvider
|
|
24
|
-
// (intelligence/quick-ask.js), which only actually supports these
|
|
24
|
+
// (intelligence/quick-ask.js), which only actually supports these
|
|
25
25
|
// providers today — offering any other real candidate as an "alternative"
|
|
26
26
|
// here would be a menu item Kairo can't actually run. Exported: ASK mode's
|
|
27
27
|
// own real-time routing (service.js's planAsk) needs this exact same real
|
|
28
28
|
// constraint when it tries to route a plain question through a PROJECT
|
|
29
29
|
// TEAM role — a role assigned to, say, opencode-go is a real, valid team
|
|
30
|
-
// assignment, just not one askProvider can call
|
|
31
|
-
|
|
30
|
+
// assignment, just not one askProvider can call (its real CLI has no
|
|
31
|
+
// portable read-only mode — see quick-ask.js's own askProvider doc).
|
|
32
|
+
export const ASK_SUPPORTED_ADAPTERS = new Set(["codex", "claude", "cursor"]);
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* The Bootstrap Analyst as a temporary, read-only WORKFLOW — deliberately
|
|
@@ -33,6 +33,22 @@ function buildCodexExecutionEnv(sourceEnv = process.env) {
|
|
|
33
33
|
return env;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Same real scrubbing principle as Codex/Claude above — CURSOR_API_KEY/
|
|
37
|
+
// CURSOR_API_ENDPOINT are cursor-agent's own documented real auth env
|
|
38
|
+
// vars (verified via `cursor-agent -p --help`), never a guess.
|
|
39
|
+
const CURSOR_SAFE_ENV_KEYS = Object.freeze([
|
|
40
|
+
"PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE",
|
|
41
|
+
"TMPDIR", "TERM", "CURSOR_API_KEY", "CURSOR_API_ENDPOINT", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
42
|
+
"http_proxy", "https_proxy", "no_proxy", "NODE_EXTRA_CA_CERTS"
|
|
43
|
+
]);
|
|
44
|
+
function buildCursorExecutionEnv(sourceEnv = process.env) {
|
|
45
|
+
const env = Object.create(null);
|
|
46
|
+
for (const key of CURSOR_SAFE_ENV_KEYS) {
|
|
47
|
+
if (sourceEnv[key] != null && sourceEnv[key] !== "") env[key] = sourceEnv[key];
|
|
48
|
+
}
|
|
49
|
+
return env;
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
function unknown(error) {
|
|
37
53
|
return { status: "error", answer: null, error: String(error) };
|
|
38
54
|
}
|
|
@@ -102,6 +118,54 @@ function askClaude({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
|
102
118
|
});
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
/**
|
|
122
|
+
* cursor-agent's own real, documented read-only mode (verified via
|
|
123
|
+
* `cursor-agent -p --help`): "ask: Q&A style for explanations and
|
|
124
|
+
* questions (read-only)" — never combined with --force/--yolo, which
|
|
125
|
+
* would grant real write/shell access. A real invalid-model failure
|
|
126
|
+
* (verified live) exits non-zero with a plain-text error on stderr, no
|
|
127
|
+
* JSON at all — unlike Claude's/Codex's own failure shapes, so a failed
|
|
128
|
+
* JSON parse here reports the real stderr text, never a generic guess.
|
|
129
|
+
* @param {{question:string, model:string|null, cwd:string, spawn:Function, timeoutMs:number, env:object}} args
|
|
130
|
+
*/
|
|
131
|
+
function askCursor({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
132
|
+
const args = ["-p", question, "--mode", "ask", "--output-format", "json"];
|
|
133
|
+
if (model) args.push("--model", model);
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
let child;
|
|
136
|
+
try {
|
|
137
|
+
child = spawn("cursor-agent", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
resolve(unknown(error?.message ?? error));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let stdout = "";
|
|
143
|
+
let stderr = "";
|
|
144
|
+
let finished = false;
|
|
145
|
+
const clearIdleTimer = armIdleTimeout(child, timeoutMs, () => finish(unknown(`cursor-agent idle-timed out after ${timeoutMs}ms with no output`)));
|
|
146
|
+
function finish(result) {
|
|
147
|
+
if (finished) return;
|
|
148
|
+
finished = true;
|
|
149
|
+
clearIdleTimer();
|
|
150
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
151
|
+
resolve(result);
|
|
152
|
+
}
|
|
153
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
154
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
155
|
+
child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
|
|
156
|
+
child.once?.("close", (code) => {
|
|
157
|
+
let parsed;
|
|
158
|
+
try { parsed = JSON.parse(stdout); } catch {
|
|
159
|
+
return finish(unknown(stderr.trim() || `cursor-agent exited ${code} with no parseable output`));
|
|
160
|
+
}
|
|
161
|
+
if (parsed?.is_error === true || typeof parsed?.result !== "string") {
|
|
162
|
+
return finish(unknown(parsed?.result ?? stderr.trim() ?? "cursor-agent returned no answer"));
|
|
163
|
+
}
|
|
164
|
+
finish({ status: "answered", answer: parsed.result, error: null });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
105
169
|
/** @param {{question:string, model:string|null, cwd:string, spawn:Function, timeoutMs:number, env:object}} args */
|
|
106
170
|
async function askCodex({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
107
171
|
let outDir;
|
|
@@ -158,10 +222,15 @@ async function askCodex({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
|
158
222
|
|
|
159
223
|
/**
|
|
160
224
|
* Asks the given provider a real, read-only question and returns its real
|
|
161
|
-
* answer text. Supports Codex and
|
|
162
|
-
* an honest "unsupported" result rather than a guess.
|
|
225
|
+
* answer text. Supports Codex, Claude, and Cursor today; any other
|
|
226
|
+
* provider yields an honest "unsupported" result rather than a guess.
|
|
227
|
+
* OpenCode (Go/Zen) is deliberately excluded — its real CLI has no
|
|
228
|
+
* portable, CLI-flag-driven read-only mode (verified via `opencode run
|
|
229
|
+
* --help`: `--auto` only ever loosens permissions further, never
|
|
230
|
+
* restricts them), so a real read-only call can't be guaranteed safe
|
|
231
|
+
* across different users' local opencode.json permission configs.
|
|
163
232
|
* @param {object} args
|
|
164
|
-
* @param {"codex"|"claude"} args.provider
|
|
233
|
+
* @param {"codex"|"claude"|"cursor"} args.provider
|
|
165
234
|
* @param {string} args.question
|
|
166
235
|
* @param {string|null} [args.model]
|
|
167
236
|
* @param {string} args.cwd
|
|
@@ -171,5 +240,6 @@ export async function askProvider({
|
|
|
171
240
|
}) {
|
|
172
241
|
if (provider === "claude") return askClaude({ question, model, cwd, spawn, timeoutMs, env: buildClaudeExecutionEnv(sourceEnv) });
|
|
173
242
|
if (provider === "codex") return askCodex({ question, model, cwd, spawn, timeoutMs, env: buildCodexExecutionEnv(sourceEnv) });
|
|
243
|
+
if (provider === "cursor") return askCursor({ question, model, cwd, spawn, timeoutMs, env: buildCursorExecutionEnv(sourceEnv) });
|
|
174
244
|
return { status: "unsupported", answer: null, error: `ASK is not supported for provider "${provider}" yet.` };
|
|
175
245
|
}
|
|
@@ -89,7 +89,7 @@ export async function readCodexModels({
|
|
|
89
89
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
|
|
90
90
|
|
|
91
91
|
writeRequest(child, 1, "initialize", {
|
|
92
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
92
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.25.0" },
|
|
93
93
|
capabilities: {}
|
|
94
94
|
});
|
|
95
95
|
});
|
|
@@ -151,7 +151,7 @@ export async function readCodexUsage({
|
|
|
151
151
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
|
|
152
152
|
|
|
153
153
|
writeRequest(child, 1, "initialize", {
|
|
154
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
154
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.25.0" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|