@kal-elsam/kairo-runtime 0.13.1 → 0.15.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +12 -10
  3. package/bin/kairo-runtime.js +0 -0
  4. package/bin/kairo.js +0 -0
  5. package/package.json +5 -1
  6. package/scripts/cockpit-smoke.mjs +14 -10
  7. package/src/cli.js +182 -11
  8. package/src/global/check-resolutions.js +31 -0
  9. package/src/global/cli-help.js +23 -4
  10. package/src/global/component-ecosystem-checks.js +2 -0
  11. package/src/global/component-integration-cli.js +29 -10
  12. package/src/global/components-resolve-cli.js +246 -0
  13. package/src/global/connection-actions.js +147 -0
  14. package/src/global/connections.js +269 -0
  15. package/src/global/fleet-configure-plan.js +123 -0
  16. package/src/global/fleet-configure.js +303 -0
  17. package/src/global/fleet-models.js +188 -0
  18. package/src/global/fleet-set.js +219 -0
  19. package/src/global/fleet-shared.js +38 -0
  20. package/src/global/ink/cockpit-controller.js +2 -4
  21. package/src/global/ink/cockpit-enter.js +3 -1
  22. package/src/global/ink/cockpit-focus.js +7 -1
  23. package/src/global/ink/cockpit-models.js +33 -21
  24. package/src/global/ink/cockpit-palette.js +8 -3
  25. package/src/global/ink/cockpit-views.js +9 -2
  26. package/src/global/ink/orchestrator-app.js +42 -4
  27. package/src/global/ink/ux/live-overview.js +53 -26
  28. package/src/global/ink/ux/overview-actions.js +80 -0
  29. package/src/global/ink/ux/overview-needs.js +1 -1
  30. package/src/global/integrations/engram-evidence.js +7 -2
  31. package/src/global/integrations/sdd-apply.js +17 -7
  32. package/src/global/integrations/sdd-evidence.js +22 -3
  33. package/src/global/integrations/sdd-plan.js +21 -3
  34. package/src/global/integrations/sdd-resolutions.js +73 -0
  35. package/src/global/integrations/sdd-state.js +69 -0
  36. package/src/global/integrations/sdd-verify.js +9 -4
  37. package/src/global/mcp/kairo-mcp.js +56 -5
  38. package/src/global/mcp/resolve-mcp-workspace.js +51 -0
  39. package/src/global/mcp/work-snapshot-rule.js +89 -0
  40. package/src/global/mcp/work-snapshot-tool.js +49 -0
  41. package/src/global/mcp-install.js +239 -0
  42. package/src/global/next/next-cli.js +35 -0
  43. package/src/global/next/next-report.js +145 -0
  44. package/src/global/next/project-key.js +36 -0
  45. package/src/global/next/publish-work-snapshot.js +116 -0
  46. package/src/global/next/work-enroll.js +91 -0
  47. package/src/global/next/work-snapshot.js +216 -0
  48. package/src/global/observability/fleet-activity.js +197 -0
  49. package/src/global/observability/fleet-models-catalog.js +137 -0
  50. package/src/global/observability/fleet-platforms.js +166 -0
  51. package/src/global/observability/fleet-probe.js +229 -0
  52. package/src/global/paths.js +2 -1
  53. package/src/global/self-update.js +216 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Declared fleet topology (orchestrator → minions + models) + optional live activity.
3
+ * Read-only probes — writes go through fleet-set.js with consent.
4
+ */
5
+ import { access, readFile } from "node:fs/promises";
6
+ import { constants as fsConstants } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { resolveHomeDir } from "../paths.js";
9
+ import { isExecutableAvailable } from "../cli-probe.js";
10
+ import { buildOpenCodeActivity } from "./fleet-activity.js";
11
+ import {
12
+ buildClaudeFleet,
13
+ buildCodexFleet,
14
+ buildCursorAgentsFleet
15
+ } from "./fleet-platforms.js";
16
+
17
+ const VARIANT_SUFFIX_RE = /-(?:cheap|zen)$/i;
18
+
19
+ const MINION_ROLES = Object.freeze({
20
+ "sdd-apply": "executor",
21
+ "sdd-explore": "explorer",
22
+ "sdd-verify": "verifier",
23
+ "sdd-design": "designer",
24
+ "sdd-propose": "proposer",
25
+ "sdd-spec": "specifier",
26
+ "sdd-tasks": "planner",
27
+ "sdd-archive": "archiver",
28
+ "sdd-onboard": "onboarder",
29
+ "sdd-init": "initializer"
30
+ });
31
+
32
+ async function pathExists(path) {
33
+ try {
34
+ await access(path, fsConstants.F_OK);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ function shortModel(model) {
42
+ if (typeof model !== "string" || !model) return null;
43
+ const slash = model.lastIndexOf("/");
44
+ return slash >= 0 ? model.slice(slash + 1) : model;
45
+ }
46
+
47
+ function agentEntries(agents) {
48
+ if (!agents || typeof agents !== "object" || Array.isArray(agents)) return [];
49
+ return Object.entries(agents).map(([id, raw]) => ({
50
+ id: String(id),
51
+ mode: typeof raw?.mode === "string" ? raw.mode : null,
52
+ model: typeof raw?.model === "string" ? raw.model : null
53
+ }));
54
+ }
55
+
56
+ function pickOrchestrator(entries) {
57
+ const gentle = entries.find((e) => e.id === "gentle-orchestrator" && e.mode === "primary");
58
+ if (gentle) return gentle;
59
+ return entries.find((e) => e.mode === "primary") ?? null;
60
+ }
61
+
62
+ function isSddMinion(entry, { includeVariants = false } = {}) {
63
+ if (!entry?.id?.startsWith("sdd-")) return false;
64
+ if (entry.mode !== "subagent") return false;
65
+ if (!includeVariants && VARIANT_SUFFIX_RE.test(entry.id)) return false;
66
+ return true;
67
+ }
68
+
69
+ function minionRole(id) {
70
+ return MINION_ROLES[id] ?? (id.startsWith("sdd-") ? "specialist" : "minion");
71
+ }
72
+
73
+ export function parseOpenCodeFleet(config, { includeVariants = false } = {}) {
74
+ const defaultModel = typeof config?.model === "string" ? config.model : null;
75
+ const entries = agentEntries(config?.agent ?? config?.agents);
76
+ if (entries.length === 0) {
77
+ return {
78
+ platform: "opencode",
79
+ orchestrator: null,
80
+ minions: [],
81
+ opaque: false,
82
+ writable: true,
83
+ source: "opencode.json"
84
+ };
85
+ }
86
+
87
+ const orch = pickOrchestrator(entries);
88
+ const orchModel = orch?.model ?? defaultModel;
89
+ const minions = entries
90
+ .filter((e) => isSddMinion(e, { includeVariants }))
91
+ .map((e) => {
92
+ const model = e.model ?? defaultModel;
93
+ return {
94
+ id: e.id,
95
+ model,
96
+ modelShort: shortModel(model),
97
+ role: minionRole(e.id),
98
+ mode: e.mode
99
+ };
100
+ })
101
+ .sort((a, b) => a.id.localeCompare(b.id));
102
+
103
+ return {
104
+ platform: "opencode",
105
+ orchestrator: orch
106
+ ? {
107
+ id: orch.id,
108
+ model: orchModel,
109
+ modelShort: shortModel(orchModel),
110
+ mode: orch.mode ?? "primary",
111
+ opaque: false
112
+ }
113
+ : null,
114
+ minions,
115
+ opaque: false,
116
+ writable: true,
117
+ source: "opencode.json"
118
+ };
119
+ }
120
+
121
+ async function readJson(path, read = readFile) {
122
+ try {
123
+ return JSON.parse(await read(path, "utf8"));
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ export async function buildFleetReport({
130
+ homeDir = resolveHomeDir(),
131
+ includeVariants = false,
132
+ includeActivity = true,
133
+ read = readFile,
134
+ exists = pathExists,
135
+ gentleAvailable = () => isExecutableAvailable("gentle-ai"),
136
+ buildActivity = buildOpenCodeActivity,
137
+ buildClaude = buildClaudeFleet,
138
+ buildCodex = buildCodexFleet,
139
+ buildCursor = buildCursorAgentsFleet
140
+ } = {}) {
141
+ const fleets = [];
142
+ const openCodePath = join(homeDir, ".config", "opencode", "opencode.json");
143
+ if (await exists(openCodePath)) {
144
+ const config = await readJson(openCodePath, read);
145
+ if (config) {
146
+ const fleet = parseOpenCodeFleet(config, { includeVariants });
147
+ fleet.configPath = openCodePath;
148
+ fleets.push(fleet);
149
+ }
150
+ }
151
+
152
+ if (await exists(join(homeDir, ".cursor"))) {
153
+ fleets.push(await buildCursor({ homeDir, read }));
154
+ }
155
+
156
+ if (await exists(join(homeDir, ".claude"))) {
157
+ const fleet = await buildClaude({ homeDir, read });
158
+ if (fleet) fleets.push(fleet);
159
+ }
160
+
161
+ if (await exists(join(homeDir, ".codex"))) {
162
+ const fleet = await buildCodex({ homeDir, read });
163
+ if (fleet) fleets.push(fleet);
164
+ }
165
+
166
+ const activity = includeActivity
167
+ ? await buildActivity({ homeDir })
168
+ : null;
169
+
170
+ return {
171
+ ok: true,
172
+ kind: "declared+activity",
173
+ note: "Declared config topology + OpenCode live activity when available.",
174
+ orchestratorAuthority: gentleAvailable() ? "gentle-ai" : null,
175
+ fleets,
176
+ activity,
177
+ generatedAt: new Date().toISOString()
178
+ };
179
+ }
180
+
181
+ export function formatFleetText(report, { verbose = false } = {}) {
182
+ const lines = ["Fleet floor", ""];
183
+ if (report.orchestratorAuthority) {
184
+ lines.push(`Authority · ${report.orchestratorAuthority}`);
185
+ lines.push("");
186
+ }
187
+ for (const fleet of report.fleets ?? []) {
188
+ const orch = fleet.orchestrator;
189
+ const modelBit = orch?.opaque
190
+ ? "opaque · IDE-managed"
191
+ : (orch?.modelShort ?? orch?.model ?? "—");
192
+ const minionCount = (fleet.minions ?? []).length;
193
+ lines.push(`${fleet.platform} · ${orch?.id ?? "—"} · ${modelBit}`);
194
+ if (verbose) {
195
+ for (const m of fleet.minions ?? []) {
196
+ const opaque = m.opaque ? " · opaque" : "";
197
+ lines.push(` ${m.id} · ${m.modelShort ?? m.model ?? "—"} · ${m.role}${opaque}`);
198
+ }
199
+ if (fleet.note) lines.push(` note: ${fleet.note}`);
200
+ } else if (minionCount > 0) {
201
+ lines.push(` ${minionCount} minions · kairo fleet --verbose`);
202
+ }
203
+ lines.push("");
204
+ }
205
+
206
+ const act = report.activity;
207
+ if (act?.available) {
208
+ const active = (act.agents ?? []).filter((a) => a.state === "active");
209
+ if (active.length === 0) {
210
+ lines.push("Working floor · quiet (no live OpenCode sessions)");
211
+ lines.push("");
212
+ } else {
213
+ lines.push(`Working floor · ${active.length} live`);
214
+ for (const a of active) {
215
+ lines.push(` ● ${a.id} · ${a.modelShort ?? a.model ?? "—"}`);
216
+ }
217
+ lines.push("");
218
+ }
219
+ } else if (act && !act.available) {
220
+ lines.push(`Working floor · unavailable`);
221
+ lines.push("");
222
+ }
223
+
224
+ if ((report.fleets ?? []).length === 0) {
225
+ lines.push("No agent platforms detected.");
226
+ }
227
+ lines.push(report.note ?? "");
228
+ return lines.join("\n").trimEnd();
229
+ }
@@ -23,7 +23,8 @@ export function harnessHomePaths(homeDir) {
23
23
  monitorDir: join(root, "monitor"),
24
24
  monitorStatePath: join(root, "monitor", "state.json"),
25
25
  coreDir: join(root, "core"),
26
- backupsDir: join(root, "backups")
26
+ backupsDir: join(root, "backups"),
27
+ sessionsDir: join(root, "sessions")
27
28
  };
28
29
  }
29
30
 
@@ -0,0 +1,216 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fetchPublishedVersion } from "./npm-registry.js";
3
+ import { formatCliCommand } from "./brand/cli.js";
4
+ import { commandHeader } from "./brand/index.js";
5
+
6
+ /**
7
+ * @param {string} version
8
+ * @returns {number[]}
9
+ */
10
+ export function parseSemver(version) {
11
+ const match = String(version).trim().match(/^(\d+)\.(\d+)\.(\d+)/);
12
+ if (!match) return null;
13
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
14
+ }
15
+
16
+ /**
17
+ * @param {string} a
18
+ * @param {string} b
19
+ * @returns {-1|0|1|null}
20
+ */
21
+ export function compareSemver(a, b) {
22
+ const left = parseSemver(a);
23
+ const right = parseSemver(b);
24
+ if (!left || !right) return null;
25
+ for (let i = 0; i < 3; i += 1) {
26
+ if (left[i] < right[i]) return -1;
27
+ if (left[i] > right[i]) return 1;
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ /**
33
+ * @param {NodeJS.ProcessEnv} [env]
34
+ * @returns {"npm"|"pnpm"|"yarn"|"bun"}
35
+ */
36
+ export function detectInstallPackageManager(env = process.env) {
37
+ const execPath = env.npm_execpath ?? "";
38
+ const userAgent = env.npm_config_user_agent ?? "";
39
+ if (execPath.includes("pnpm") || userAgent.startsWith("pnpm/")) return "pnpm";
40
+ if (execPath.includes("yarn") || userAgent.startsWith("yarn/")) return "yarn";
41
+ if (execPath.includes("bun") || userAgent.startsWith("bun/")) return "bun";
42
+ return "npm";
43
+ }
44
+
45
+ /**
46
+ * @param {string} packageName
47
+ * @param {string} version
48
+ * @param {"npm"|"pnpm"|"yarn"|"bun"} manager
49
+ * @returns {{ command: string, args: string[], display: string }}
50
+ */
51
+ export function buildGlobalInstallSpec(packageName, version, manager = "npm") {
52
+ const spec = `${packageName}@${version}`;
53
+ switch (manager) {
54
+ case "pnpm":
55
+ return {
56
+ command: "pnpm",
57
+ args: ["add", "-g", spec],
58
+ display: `pnpm add -g ${spec}`
59
+ };
60
+ case "yarn":
61
+ return {
62
+ command: "yarn",
63
+ args: ["global", "add", spec],
64
+ display: `yarn global add ${spec}`
65
+ };
66
+ case "bun":
67
+ return {
68
+ command: "bun",
69
+ args: ["add", "-g", spec],
70
+ display: `bun add -g ${spec}`
71
+ };
72
+ default:
73
+ return {
74
+ command: "npm",
75
+ args: ["install", "-g", spec],
76
+ display: `npm install -g ${spec}`
77
+ };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @param {object} options
83
+ * @param {string} options.packageName
84
+ * @param {string} options.cliVersion
85
+ * @param {boolean} [options.yes]
86
+ * @param {boolean} [options.json]
87
+ * @param {typeof fetchPublishedVersion} [options.fetchVersion]
88
+ * @param {typeof detectInstallPackageManager} [options.detectManager]
89
+ * @param {(cmd: string, args: string[]) => Promise<{ status: number, stdout: string, stderr: string }>} [options.runCommand]
90
+ */
91
+ export async function runSelfUpdate({
92
+ packageName,
93
+ cliVersion,
94
+ yes = false,
95
+ json = false,
96
+ fetchVersion = fetchPublishedVersion,
97
+ detectManager = detectInstallPackageManager,
98
+ runCommand = defaultRunCommand
99
+ } = {}) {
100
+ if (!packageName) throw new Error("packageName is required.");
101
+ if (!cliVersion) throw new Error("cliVersion is required.");
102
+
103
+ const latestVersion = await fetchVersion(packageName);
104
+ const cmp = compareSemver(cliVersion, latestVersion);
105
+ const manager = detectManager();
106
+ const install = buildGlobalInstallSpec(packageName, latestVersion, manager);
107
+
108
+ /** @type {"current"|"behind"|"ahead"|"unknown"} */
109
+ let state = "unknown";
110
+ if (cmp === 0) state = "current";
111
+ else if (cmp === -1) state = "behind";
112
+ else if (cmp === 1) state = "ahead";
113
+
114
+ const report = {
115
+ ok: true,
116
+ state,
117
+ installedVersion: cliVersion,
118
+ latestVersion,
119
+ packageName,
120
+ manager,
121
+ installCommand: install.display,
122
+ applied: false,
123
+ wrote: false
124
+ };
125
+
126
+ if (state === "current") {
127
+ report.nextAction = "Already up to date.";
128
+ } else if (state === "ahead") {
129
+ report.nextAction = `Local ${cliVersion} is newer than npm ${latestVersion}.`;
130
+ } else if (state === "behind") {
131
+ report.nextAction = yes
132
+ ? `Updating ${cliVersion} → ${latestVersion}…`
133
+ : `Update available: ${cliVersion} → ${latestVersion}. Run "${formatCliCommand("update --yes")}" or: ${install.display}`;
134
+ } else {
135
+ report.nextAction = `Could not compare versions. Install manually: ${install.display}`;
136
+ }
137
+
138
+ if (json) {
139
+ if (yes && state === "behind") {
140
+ const result = await runCommand(install.command, install.args);
141
+ report.applied = result.status === 0;
142
+ report.wrote = result.status === 0;
143
+ report.ok = result.status === 0;
144
+ report.installStatus = result.status;
145
+ report.stderr = result.stderr.trim() || undefined;
146
+ report.nextAction = result.status === 0
147
+ ? `Updated to ${latestVersion}.`
148
+ : `Install failed (exit ${result.status}). Try: ${install.display}`;
149
+ }
150
+ console.log(JSON.stringify(report));
151
+ return report;
152
+ }
153
+
154
+ console.log(commandHeader("update — Kairo Runtime"));
155
+ console.log(`Installed: ${cliVersion}`);
156
+ console.log(`Latest: ${latestVersion}`);
157
+ console.log(`Status: ${state}`);
158
+
159
+ if (state === "current") {
160
+ console.log("\nAlready up to date.");
161
+ return report;
162
+ }
163
+
164
+ if (state === "ahead") {
165
+ console.log(`\n${report.nextAction}`);
166
+ return report;
167
+ }
168
+
169
+ if (!yes) {
170
+ console.log(`\nUpdate available: ${cliVersion} → ${latestVersion}`);
171
+ console.log(`Run: ${formatCliCommand("update --yes")}`);
172
+ console.log(`Or: ${install.display}`);
173
+ return report;
174
+ }
175
+
176
+ console.log(`\nInstalling ${install.display}…`);
177
+ const result = await runCommand(install.command, install.args);
178
+ report.applied = result.status === 0;
179
+ report.wrote = result.status === 0;
180
+ report.ok = result.status === 0;
181
+ report.installStatus = result.status;
182
+ if (result.stdout.trim()) console.log(result.stdout.trim());
183
+ if (result.stderr.trim()) console.error(result.stderr.trim());
184
+ if (result.status !== 0) {
185
+ throw new Error(`Self-update failed (exit ${result.status}). Try: ${install.display}`);
186
+ }
187
+ console.log(`\nUpdated to ${latestVersion}. Run "${formatCliCommand("--version")}" to confirm.`);
188
+ report.nextAction = `Updated to ${latestVersion}.`;
189
+ return report;
190
+ }
191
+
192
+ function defaultRunCommand(command, args) {
193
+ return new Promise((resolve) => {
194
+ const child = spawn(command, args, {
195
+ stdio: ["ignore", "pipe", "pipe"],
196
+ shell: false,
197
+ env: process.env
198
+ });
199
+ let stdout = "";
200
+ let stderr = "";
201
+ child.stdout?.setEncoding("utf8");
202
+ child.stderr?.setEncoding("utf8");
203
+ child.stdout?.on("data", (chunk) => {
204
+ stdout += chunk;
205
+ });
206
+ child.stderr?.on("data", (chunk) => {
207
+ stderr += chunk;
208
+ });
209
+ child.on("error", (error) => {
210
+ resolve({ status: 1, stdout, stderr: error.message });
211
+ });
212
+ child.on("close", (code) => {
213
+ resolve({ status: code ?? 1, stdout, stderr });
214
+ });
215
+ });
216
+ }