@kal-elsam/kairo-runtime 0.2.1 → 0.2.2

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.
@@ -0,0 +1,92 @@
1
+ import { isExecutableAvailable } from "../../cli-probe.js";
2
+
3
+ export function createExecutionAdapter({
4
+ id,
5
+ label,
6
+ executable,
7
+ capabilities,
8
+ buildLaunch,
9
+ parseEventLine = null,
10
+ checkAvailability = null,
11
+ launchable = null
12
+ }) {
13
+ return {
14
+ id,
15
+ label,
16
+ executable,
17
+ capabilities: {
18
+ structuredEvents: false,
19
+ tokens: false,
20
+ diff: false,
21
+ cancel: true,
22
+ transcript: false,
23
+ ...capabilities
24
+ },
25
+
26
+ availability(context = {}) {
27
+ if (checkAvailability) {
28
+ return checkAvailability(context);
29
+ }
30
+
31
+ const available = isExecutableAvailable(executable, { env: context.env ?? process.env });
32
+ if (!available) {
33
+ return {
34
+ available: false,
35
+ compatible: false,
36
+ launchable: false,
37
+ reason: `${label} CLI "${executable}" is not on PATH.`
38
+ };
39
+ }
40
+
41
+ if (!capabilities.structuredEvents) {
42
+ return {
43
+ available: true,
44
+ compatible: false,
45
+ launchable: launchable ?? false,
46
+ reason: `${label} can be launched but does not emit auditable structured events in v1.`
47
+ };
48
+ }
49
+
50
+ return {
51
+ available: true,
52
+ compatible: true,
53
+ launchable: launchable ?? true,
54
+ reason: null
55
+ };
56
+ },
57
+
58
+ buildLaunch(options) {
59
+ return buildLaunch(options);
60
+ },
61
+
62
+ parseEventLine(line, context = {}) {
63
+ if (!parseEventLine) return null;
64
+ return parseEventLine(line, context);
65
+ }
66
+ };
67
+ }
68
+
69
+ export function parseNdjsonLine(line) {
70
+ const trimmed = line.trim();
71
+ if (!trimmed) return null;
72
+
73
+ try {
74
+ return JSON.parse(trimmed);
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ export function buildPermissionsArgs(permissions = []) {
81
+ const normalized = new Set(permissions.map((entry) => String(entry).toLowerCase()));
82
+
83
+ if (normalized.has("all") || normalized.has("force")) {
84
+ return ["--force"];
85
+ }
86
+
87
+ if (normalized.has("yolo") || normalized.has("dangerously-skip-permissions")) {
88
+ return ["--dangerously-skip-permissions"];
89
+ }
90
+
91
+ return [];
92
+ }
@@ -0,0 +1,104 @@
1
+ import { createExecutionAdapter, parseNdjsonLine, buildPermissionsArgs } from "./create-execution-adapter.js";
2
+ import { isExecutableAvailable } from "../../cli-probe.js";
3
+
4
+ const EXECUTABLE = "cursor-agent";
5
+
6
+ function checkCursorAvailability() {
7
+ const agentAvailable = isExecutableAvailable(EXECUTABLE);
8
+ if (!agentAvailable) {
9
+ const legacy = isExecutableAvailable("cursor");
10
+ if (!legacy) {
11
+ return {
12
+ available: false,
13
+ compatible: false,
14
+ launchable: false,
15
+ reason: 'Cursor agent CLI "cursor-agent" is not on PATH. Install Cursor CLI.'
16
+ };
17
+ }
18
+ return {
19
+ available: true,
20
+ compatible: false,
21
+ launchable: false,
22
+ reason: 'Found "cursor" but Kairo v1 requires "cursor-agent" for auditable non-interactive runs.'
23
+ };
24
+ }
25
+
26
+ return {
27
+ available: true,
28
+ compatible: true,
29
+ launchable: true,
30
+ reason: null
31
+ };
32
+ }
33
+
34
+ function buildCursorLaunch({ task, cwd, model, permissions = [] }) {
35
+ const args = [
36
+ "-p",
37
+ "--output-format",
38
+ "stream-json",
39
+ ...buildPermissionsArgs(permissions),
40
+ task
41
+ ];
42
+
43
+ if (model) {
44
+ args.unshift("--model", model);
45
+ }
46
+
47
+ return {
48
+ command: EXECUTABLE,
49
+ args,
50
+ cwd,
51
+ env: process.env
52
+ };
53
+ }
54
+
55
+ function parseCursorEventLine(line) {
56
+ const parsed = parseNdjsonLine(line);
57
+ if (!parsed || typeof parsed !== "object") return null;
58
+
59
+ if (parsed.type === "assistant" && parsed.timestamp_ms == null && parsed.model_call_id) {
60
+ return null;
61
+ }
62
+
63
+ if (parsed.type === "assistant" && parsed.timestamp_ms == null && !parsed.model_call_id) {
64
+ return null;
65
+ }
66
+
67
+ if (parsed.type === "tool_call") {
68
+ const toolName = parsed.tool_name ?? parsed.name ?? parsed.tool ?? "unknown";
69
+ return {
70
+ type: "tool_call",
71
+ tool_name: toolName,
72
+ status: parsed.subtype ?? parsed.status ?? "started",
73
+ id: parsed.call_id ?? parsed.id ?? null
74
+ };
75
+ }
76
+
77
+ if (parsed.type === "result" && parsed.usage) {
78
+ return {
79
+ type: "usage",
80
+ inputTokens: parsed.usage.input_tokens ?? parsed.usage.inputTokens ?? null,
81
+ outputTokens: parsed.usage.output_tokens ?? parsed.usage.outputTokens ?? null,
82
+ totalTokens: parsed.usage.total_tokens ?? parsed.usage.totalTokens ?? null,
83
+ cost: parsed.usage.cost ?? parsed.cost ?? null
84
+ };
85
+ }
86
+
87
+ return parsed;
88
+ }
89
+
90
+ export default createExecutionAdapter({
91
+ id: "cursor",
92
+ label: "Cursor",
93
+ executable: EXECUTABLE,
94
+ capabilities: {
95
+ structuredEvents: true,
96
+ tokens: true,
97
+ diff: false,
98
+ cancel: true,
99
+ transcript: true
100
+ },
101
+ checkAvailability: checkCursorAvailability,
102
+ buildLaunch: buildCursorLaunch,
103
+ parseEventLine: parseCursorEventLine
104
+ });
@@ -0,0 +1,36 @@
1
+ import cursor from "./cursor.js";
2
+ import codex from "./codex.js";
3
+ import claude from "./claude.js";
4
+ import opencode from "./opencode.js";
5
+
6
+ const EXECUTION_ADAPTERS = [cursor, codex, claude, opencode];
7
+
8
+ export const EXECUTION_ADAPTER_IDS = EXECUTION_ADAPTERS.map((adapter) => adapter.id);
9
+
10
+ export function listExecutionAdapters() {
11
+ return [...EXECUTION_ADAPTERS];
12
+ }
13
+
14
+ export function resolveExecutionAdapter(id) {
15
+ const adapter = EXECUTION_ADAPTERS.find((candidate) => candidate.id === id);
16
+ if (!adapter) {
17
+ throw new Error(`Unknown execution adapter "${id}". Use ${EXECUTION_ADAPTER_IDS.join(", ")}.`);
18
+ }
19
+ return adapter;
20
+ }
21
+
22
+ export function inspectExecutionAdapters(context = {}) {
23
+ return EXECUTION_ADAPTERS.map((adapter) => ({
24
+ id: adapter.id,
25
+ label: adapter.label,
26
+ executable: adapter.executable,
27
+ capabilities: adapter.capabilities,
28
+ ...adapter.availability(context)
29
+ }));
30
+ }
31
+
32
+ export function listLaunchableAdapterIds(context = {}) {
33
+ return inspectExecutionAdapters(context)
34
+ .filter((provider) => provider.launchable)
35
+ .map((provider) => provider.id);
36
+ }
@@ -0,0 +1,38 @@
1
+ import { createExecutionAdapter } from "./create-execution-adapter.js";
2
+
3
+ const EXECUTABLE = "opencode";
4
+
5
+ function buildOpencodeLaunch({ task, cwd, model, permissions = [] }) {
6
+ const args = ["run", task];
7
+
8
+ if (model) {
9
+ args.unshift("--model", model);
10
+ }
11
+
12
+ if (permissions.includes("force") || permissions.includes("all")) {
13
+ args.unshift("--force");
14
+ }
15
+
16
+ return {
17
+ command: EXECUTABLE,
18
+ args,
19
+ cwd,
20
+ env: process.env
21
+ };
22
+ }
23
+
24
+ export default createExecutionAdapter({
25
+ id: "opencode",
26
+ label: "OpenCode",
27
+ executable: EXECUTABLE,
28
+ launchable: false,
29
+ capabilities: {
30
+ structuredEvents: false,
31
+ tokens: false,
32
+ diff: false,
33
+ cancel: true,
34
+ transcript: false
35
+ },
36
+ buildLaunch: buildOpencodeLaunch,
37
+ parseEventLine: null
38
+ });
@@ -0,0 +1,29 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { runPaths } from "../paths.js";
4
+
5
+ function cancelSignalPath(homeDir, runId) {
6
+ return `${runPaths(homeDir, runId).runDir}/cancel.signal.json`;
7
+ }
8
+
9
+ export async function writeCancelSignal(homeDir, runId, payload) {
10
+ const { runDir } = runPaths(homeDir, runId);
11
+ await mkdir(runDir, { recursive: true });
12
+ await writeFile(cancelSignalPath(homeDir, runId), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
13
+ }
14
+
15
+ export async function readCancelSignal(homeDir, runId) {
16
+ const path = cancelSignalPath(homeDir, runId);
17
+ if (!existsSync(path)) return null;
18
+
19
+ try {
20
+ return JSON.parse(await readFile(path, "utf8"));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export async function isRunCancelRequested(homeDir, runId) {
27
+ const signal = await readCancelSignal(homeDir, runId);
28
+ return signal?.requested === true;
29
+ }
@@ -0,0 +1,221 @@
1
+ import { resolveHomeDir } from "../paths.js";
2
+ import { resolveProfile } from "../profile.js";
3
+ import { printJson } from "../json-output.js";
4
+ import { BRAND, commandHeader } from "../brand/index.js";
5
+ import { formatCliCommand } from "../brand/cli.js";
6
+ import { inspectExecutionAdapters } from "./execution-adapters/index.js";
7
+ import {
8
+ listRunRecords,
9
+ readRunEvents,
10
+ readRunState
11
+ } from "./run-store.js";
12
+ import {
13
+ recoverRuns,
14
+ resolveRunAgent,
15
+ startRun,
16
+ stopRun
17
+ } from "./run-manager.js";
18
+ import { isActiveRunState, formatTaskLabel } from "./run-types.js";
19
+ import { resolveAgentFromProfile } from "./run-profile.js";
20
+
21
+ export async function runGlobalRun(options, packageManifest, { startRunImpl = startRun } = {}) {
22
+ const homeDir = resolveHomeDir();
23
+ await recoverRuns(homeDir);
24
+
25
+ if (!options.task) {
26
+ throw new Error(`Missing task. Use: ${formatCliCommand('run --agent <id> --task "..."')}`);
27
+ }
28
+
29
+ const profileResolved = await resolveProfile({
30
+ homeDir,
31
+ workspaceRoot: options.cwd
32
+ });
33
+
34
+ const runtime = resolveAgentFromProfile(profileResolved, options.agent);
35
+ const permissions = options.permissions ?? runtime.permissions;
36
+ const captureTranscript = options.captureTranscript || runtime.captureTranscript;
37
+
38
+ const { runId, metadata, completion } = await startRunImpl({
39
+ homeDir,
40
+ agentId: runtime.agentId,
41
+ task: options.task,
42
+ cwd: options.cwd,
43
+ model: options.model ?? runtime.model,
44
+ permissions,
45
+ captureTranscript,
46
+ cliVersion: packageManifest.version,
47
+ profile: profileResolved,
48
+ follow: options.follow,
49
+ timeoutMs: options.timeoutMs,
50
+ wait: options.wait !== false
51
+ });
52
+
53
+ if (!options.json) {
54
+ console.log(commandHeader(`run started · ${metadata.agentId} · ${runId}`));
55
+ console.log(`Task: ${formatTaskLabel(metadata)}`);
56
+ console.log(`Cwd: ${metadata.cwd}`);
57
+ if (!options.wait) {
58
+ console.log(`Follow: ${formatCliCommand(`runs show ${runId} --follow`)}`);
59
+ return { runId, metadata };
60
+ }
61
+ } else if (!options.wait) {
62
+ printJson({ runId, metadata });
63
+ return { runId, metadata };
64
+ }
65
+
66
+ if (!completion) {
67
+ if (options.json) {
68
+ printJson({ runId, metadata });
69
+ }
70
+ return { runId, metadata };
71
+ }
72
+
73
+ const final = await completion;
74
+
75
+ if (options.json) {
76
+ printJson({ runId, metadata: final });
77
+ } else {
78
+ console.log(`Run ${final.state} (exit ${final.exitCode ?? "n/a"})`);
79
+ }
80
+
81
+ return { runId, metadata: final };
82
+ }
83
+
84
+ export async function runGlobalRuns(options, packageManifest) {
85
+ const homeDir = resolveHomeDir();
86
+ await recoverRuns(homeDir);
87
+
88
+ switch (options.runsAction ?? "list") {
89
+ case "list":
90
+ return runRunsList(homeDir, options);
91
+ case "show":
92
+ return runRunsShow(homeDir, options);
93
+ case "stop":
94
+ return runRunsStop(homeDir, options);
95
+ default:
96
+ throw new Error(`Unknown runs action "${options.runsAction}". Use list, show, or stop.`);
97
+ }
98
+ }
99
+
100
+ async function runRunsList(homeDir, options) {
101
+ const runs = await listRunRecords(homeDir, {
102
+ limit: options.limit,
103
+ activeOnly: options.activeOnly
104
+ });
105
+
106
+ if (options.json) {
107
+ printJson({ runs, providers: inspectExecutionAdapters({ cwd: options.cwd }) });
108
+ return { runs };
109
+ }
110
+
111
+ console.log(commandHeader("runs"));
112
+ const active = runs.filter((run) => isActiveRunState(run.state));
113
+ console.log(`Active: ${active.length} · Total shown: ${runs.length}`);
114
+ console.log("");
115
+
116
+ for (const run of runs) {
117
+ console.log(
118
+ ` ${run.runId} ${run.state.padEnd(12)} ${run.agentId.padEnd(10)} ${formatTaskLabel(run)}`
119
+ );
120
+ }
121
+
122
+ if (runs.length === 0) {
123
+ console.log(` (no runs yet — launch with ${formatCliCommand('run --agent cursor --task "..."')})`);
124
+ }
125
+
126
+ return { runs };
127
+ }
128
+
129
+ async function runRunsShow(homeDir, options) {
130
+ if (!options.runId) {
131
+ throw new Error(`Missing run id. Use: ${formatCliCommand("runs show <runId>")}`);
132
+ }
133
+
134
+ const metadata = await readRunState(homeDir, options.runId);
135
+ if (!metadata) {
136
+ throw new Error(`Run "${options.runId}" not found.`);
137
+ }
138
+
139
+ const events = await readRunEvents(homeDir, options.runId, { limit: options.limit });
140
+
141
+ if (options.json) {
142
+ printJson({ metadata, events });
143
+ return { metadata, events };
144
+ }
145
+
146
+ console.log(commandHeader(`run ${metadata.runId}`));
147
+ console.log(`Agent: ${metadata.agentId} (${metadata.provider})`);
148
+ console.log(`State: ${metadata.state}`);
149
+ console.log(`Model: ${metadata.model ?? "default"}`);
150
+ console.log(`Cwd: ${metadata.cwd}`);
151
+ console.log(`Started: ${metadata.startedAt}`);
152
+ if (metadata.completedAt) console.log(`Completed: ${metadata.completedAt}`);
153
+ if (metadata.tokenUsage) console.log(`Tokens: ${JSON.stringify(metadata.tokenUsage)}`);
154
+ if (metadata.diffSummary) console.log(`Diff: ${JSON.stringify(metadata.diffSummary)}`);
155
+ if (metadata.error) console.log(`Error: ${metadata.error}`);
156
+ console.log("");
157
+ console.log("Events:");
158
+
159
+ for (const event of events) {
160
+ if (event.parseError) {
161
+ console.log(` [parse error line ${event.line}]`);
162
+ continue;
163
+ }
164
+ const summary = summarizeEvent(event);
165
+ console.log(` ${event.timestamp} ${event.type} ${summary}`);
166
+ if (options.follow) {
167
+ // follow is for live runs; show replays events only
168
+ }
169
+ }
170
+
171
+ return { metadata, events };
172
+ }
173
+
174
+ async function runRunsStop(homeDir, options) {
175
+ if (!options.runId) {
176
+ throw new Error(`Missing run id. Use: ${formatCliCommand("runs stop <runId>")}`);
177
+ }
178
+
179
+ const metadata = await stopRun(homeDir, options.runId);
180
+
181
+ if (options.json) {
182
+ printJson({ metadata });
183
+ return { metadata };
184
+ }
185
+
186
+ console.log(commandHeader(`run ${metadata.runId} cancelled`));
187
+ console.log(`State: ${metadata.state}`);
188
+ return { metadata };
189
+ }
190
+
191
+ function summarizeEvent(event) {
192
+ if (event.type === "agent.tool_call") {
193
+ return event.data?.tool_name ?? event.data?.name ?? "tool";
194
+ }
195
+ if (event.type === "process.stdout" || event.type === "process.stderr") {
196
+ const line = event.data?.line ?? "";
197
+ return line.length > 80 ? `${line.slice(0, 79)}…` : line;
198
+ }
199
+ if (event.type === "run.completed" || event.type === "run.failed") {
200
+ return `exit=${event.data?.exitCode ?? "n/a"}`;
201
+ }
202
+ return "";
203
+ }
204
+
205
+ export async function buildRuntimeDashboardData({ homeDir, workspaceRoot, cliVersion }) {
206
+ await recoverRuns(homeDir);
207
+ const [runs, providers, profileResolved] = await Promise.all([
208
+ listRunRecords(homeDir, { limit: 20 }),
209
+ Promise.resolve(inspectExecutionAdapters({ cwd: workspaceRoot })),
210
+ resolveProfile({ homeDir, workspaceRoot })
211
+ ]);
212
+
213
+ return {
214
+ cliVersion,
215
+ runs,
216
+ activeRuns: runs.filter((run) => isActiveRunState(run.state)),
217
+ recentRuns: runs.filter((run) => !isActiveRunState(run.state)).slice(0, 10),
218
+ providers,
219
+ profile: profileResolved
220
+ };
221
+ }
@@ -0,0 +1,144 @@
1
+ import { RUN_EVENT_TYPES } from "./run-types.js";
2
+ import { redactObject } from "./run-redact.js";
3
+
4
+ export function createRunEvent({
5
+ runId,
6
+ type,
7
+ source = "kairo",
8
+ data = {},
9
+ captureTranscript = false
10
+ }) {
11
+ return {
12
+ timestamp: new Date().toISOString(),
13
+ runId,
14
+ type,
15
+ source,
16
+ data: sanitizeEventData(data, { captureTranscript })
17
+ };
18
+ }
19
+
20
+ export function sanitizeEventData(data, { captureTranscript = false } = {}) {
21
+ const allowTranscript = captureTranscript === true;
22
+ return redactObject(data, { allowTranscript });
23
+ }
24
+
25
+ export function normalizeAdapterEvent(adapterId, raw, { captureTranscript = false } = {}) {
26
+ if (raw == null) return null;
27
+
28
+ if (typeof raw === "object" && raw.type && raw.runId) {
29
+ return {
30
+ ...raw,
31
+ data: sanitizeEventData(raw.data ?? {}, { captureTranscript })
32
+ };
33
+ }
34
+
35
+ if (typeof raw === "object" && raw.type) {
36
+ return mapStructuredEvent(adapterId, raw, { captureTranscript });
37
+ }
38
+
39
+ if (typeof raw === "string") {
40
+ return {
41
+ timestamp: new Date().toISOString(),
42
+ type: RUN_EVENT_TYPES.STDOUT,
43
+ source: adapterId,
44
+ data: sanitizeEventData({ line: raw }, { captureTranscript })
45
+ };
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ function mapStructuredEvent(adapterId, raw, { captureTranscript }) {
52
+ const base = {
53
+ timestamp: new Date().toISOString(),
54
+ source: adapterId,
55
+ data: sanitizeEventData(extractEventData(raw), { captureTranscript })
56
+ };
57
+
58
+ switch (raw.type) {
59
+ case "system":
60
+ return { ...base, type: RUN_EVENT_TYPES.SYSTEM };
61
+ case "assistant":
62
+ return { ...base, type: RUN_EVENT_TYPES.ASSISTANT };
63
+ case "tool_call":
64
+ return { ...base, type: RUN_EVENT_TYPES.TOOL_CALL };
65
+ case "tool_result":
66
+ return { ...base, type: RUN_EVENT_TYPES.TOOL_RESULT };
67
+ case "result":
68
+ return { ...base, type: RUN_EVENT_TYPES.RESULT };
69
+ case "token_usage":
70
+ case "usage":
71
+ return { ...base, type: RUN_EVENT_TYPES.TOKEN_USAGE };
72
+ case "diff":
73
+ case "diff_summary":
74
+ return { ...base, type: RUN_EVENT_TYPES.DIFF_SUMMARY };
75
+ default:
76
+ return {
77
+ ...base,
78
+ type: RUN_EVENT_TYPES.SYSTEM,
79
+ data: sanitizeEventData({ rawType: raw.type, payload: extractEventData(raw) }, { captureTranscript })
80
+ };
81
+ }
82
+ }
83
+
84
+ function extractEventData(raw) {
85
+ const { type: _type, ...rest } = raw;
86
+ return rest;
87
+ }
88
+
89
+ export function applyEventToMetadata(metadata, event) {
90
+ const next = { ...metadata, updatedAt: event.timestamp ?? new Date().toISOString() };
91
+
92
+ switch (event.type) {
93
+ case RUN_EVENT_TYPES.TOOL_CALL: {
94
+ const tool = event.data?.tool_name ?? event.data?.name ?? event.data?.tool ?? event.data?.toolName ?? "unknown";
95
+ if (!next.tools.includes(tool)) {
96
+ next.tools = [...next.tools, tool];
97
+ }
98
+ break;
99
+ }
100
+ case RUN_EVENT_TYPES.STDERR:
101
+ case RUN_EVENT_TYPES.STDOUT: {
102
+ const command = event.data?.command;
103
+ if (command && !next.commands.includes(command)) {
104
+ next.commands = [...next.commands, command];
105
+ }
106
+ break;
107
+ }
108
+ case RUN_EVENT_TYPES.TOKEN_USAGE: {
109
+ next.tokenUsage = {
110
+ input: event.data?.inputTokens ?? event.data?.input ?? next.tokenUsage?.input ?? null,
111
+ output: event.data?.outputTokens ?? event.data?.output ?? next.tokenUsage?.output ?? null,
112
+ total: event.data?.totalTokens ?? event.data?.total ?? next.tokenUsage?.total ?? null
113
+ };
114
+ if (event.data?.cost != null) {
115
+ next.cost = event.data.cost;
116
+ }
117
+ break;
118
+ }
119
+ case RUN_EVENT_TYPES.DIFF_SUMMARY: {
120
+ next.diffSummary = {
121
+ filesChanged: event.data?.filesChanged ?? event.data?.files ?? null,
122
+ insertions: event.data?.insertions ?? null,
123
+ deletions: event.data?.deletions ?? null
124
+ };
125
+ break;
126
+ }
127
+ default:
128
+ break;
129
+ }
130
+
131
+ return next;
132
+ }
133
+
134
+ export function transitionRunState(metadata, nextState, { exitCode = null, error = null } = {}) {
135
+ const now = new Date().toISOString();
136
+ return {
137
+ ...metadata,
138
+ state: nextState,
139
+ exitCode: exitCode ?? metadata.exitCode,
140
+ error: error ?? metadata.error,
141
+ updatedAt: now,
142
+ completedAt: now
143
+ };
144
+ }