@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
@@ -9,11 +9,13 @@ import { formatCliCommand } from "./brand/cli.js";
9
9
  import { requireIntegrationProvider } from "./integrations/provider-registry.js";
10
10
  import { ensureIntegrationProvidersRegistered } from "./integrations/index.js";
11
11
  import { ENGRAM_INTEGRATION_STATUS } from "./integrations/engram-evidence.js";
12
- import { SDD_HEALTH } from "./integrations/sdd-evidence.js";
12
+ import { SDD_HEALTH, SDD_FILE_OUTCOMES } from "./integrations/sdd-evidence.js";
13
13
  import {
14
- hasSuccessfulSddRollbackMutations, recordSddMaterialization, reconcileSddStateAfterRollback
14
+ hasSuccessfulSddRollbackMutations, recordSddMaterialization, reconcileSddStateAfterRollback,
15
+ adoptedHashesFromState, clearSddAdoptionsForPaths
15
16
  } from "./integrations/sdd-state.js";
16
17
  import { loadSddReceipt } from "./integrations/sdd-receipts.js";
18
+ import { buildSddSkillResolutions } from "./integrations/sdd-resolutions.js";
17
19
 
18
20
  const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
19
21
 
@@ -169,14 +171,24 @@ async function buildProviderContext(options, { homeDir, componentId }) {
169
171
  packageRoot: options.packageRoot ?? DEFAULT_PACKAGE_ROOT,
170
172
  persona: options.persona ?? state?.sdd?.persona ?? "off",
171
173
  personaAgentIds: state?.sdd?.personaAgentIds ?? [],
172
- trackedFiles
174
+ trackedFiles,
175
+ adoptedFiles: adoptedHashesFromState(state?.sdd),
176
+ overwriteConflicts: Boolean(options.overwriteConflicts)
173
177
  };
174
178
  }
175
179
 
176
180
  async function persistSddReceiptState(homeDir, receipt) {
177
181
  const paths = harnessHomePaths(homeDir);
178
182
  const state = (await readGlobalState(paths.statePath)) ?? {};
179
- await writeGlobalState(paths.statePath, recordSddMaterialization(state, { receipt }));
183
+ let next = recordSddMaterialization(state, { receipt });
184
+ const appliedPaths = (receipt.files ?? [])
185
+ .filter((file) => file.outcome === SDD_FILE_OUTCOMES.APPLIED)
186
+ .map((file) => file.destinationPath)
187
+ .filter(Boolean);
188
+ if (appliedPaths.length) {
189
+ next = clearSddAdoptionsForPaths(next, appliedPaths);
190
+ }
191
+ await writeGlobalState(paths.statePath, next);
180
192
  }
181
193
 
182
194
  async function reconcileSddRollbackState(homeDir, result) {
@@ -230,13 +242,19 @@ export function buildEngramIntegrationChecks(inspection) {
230
242
  export function buildSddIntegrationChecks(verification) {
231
243
  const summary = verification.summary ?? {};
232
244
  const persona = verification.persona ?? {};
245
+ const skillsHealthy = verification.status === SDD_HEALTH.CONFIGURED
246
+ || verification.status === SDD_HEALTH.ADOPTED;
247
+ const skillsCheck = {
248
+ name: "sdd-core:skills",
249
+ status: skillsHealthy ? "ok" : "warning",
250
+ category: "integration",
251
+ componentId: "sdd-core",
252
+ detail: `SDD skills ${verification.status}: configured=${summary.configured ?? 0}, adopted=${summary.adopted ?? 0}, missing=${summary.missing ?? 0}, drifted=${summary.drifted ?? 0}, conflict=${summary.conflict ?? 0} (disk presence ≠ runtime active).`
253
+ };
254
+ const resolutions = buildSddSkillResolutions(verification);
255
+ if (resolutions.length) skillsCheck.resolutions = resolutions;
233
256
  return [
234
- {
235
- name: "sdd-core:skills",
236
- status: verification.status === SDD_HEALTH.CONFIGURED ? "ok" : "warning",
237
- category: "integration", componentId: "sdd-core",
238
- detail: `SDD skills ${verification.status}: configured=${summary.configured ?? 0}, missing=${summary.missing ?? 0}, drifted=${summary.drifted ?? 0}, conflict=${summary.conflict ?? 0} (disk presence ≠ runtime active).`
239
- },
257
+ skillsCheck,
240
258
  {
241
259
  name: "sdd-core:persona",
242
260
  status: persona.status === "configured" || persona.status === "off" ? "ok" : "warning",
@@ -283,6 +301,7 @@ function printSddConfigureHuman(result) {
283
301
  if (result.dryRun) return void console.log("Dry-run only — no skills materialized.");
284
302
  if (result.cancelled) return void console.log("Cancelled.");
285
303
  if (result.blocked) return void console.log(`Blocked: ${result.reason ?? "conflicts present"}`);
304
+ if (result.overwriteConflicts) console.log("Mode: overwrite-conflicts (backups before replace).");
286
305
  if (result.receipt) {
287
306
  console.log(`Receipt: ${result.receipt.id}${result.receipt.partial ? " (partial)" : ""}`);
288
307
  if (result.sessionRefreshRequired) {
@@ -0,0 +1,246 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { resolveHomeDir, harnessHomePaths } from "./paths.js";
5
+ import { readGlobalState, writeGlobalState } from "./state.js";
6
+ import { printJson } from "./json-output.js";
7
+ import { formatCliCommand } from "./brand/cli.js";
8
+ import {
9
+ assertExplicitApplyConsent,
10
+ promptApplyConfirmation,
11
+ shouldPromptApplyConfirmation
12
+ } from "./apply-confirmation.js";
13
+ import { ensureIntegrationProvidersRegistered } from "./integrations/index.js";
14
+ import { requireIntegrationProvider } from "./integrations/provider-registry.js";
15
+ import { SDD_HEALTH } from "./integrations/sdd-evidence.js";
16
+ import {
17
+ adoptedHashesFromState,
18
+ recordSddAdoptions
19
+ } from "./integrations/sdd-state.js";
20
+ import { resolveCanonicalSddSkillFile } from "./integrations/sdd-destinations.js";
21
+ import { assertComponentInstalled } from "./component-integration-cli.js";
22
+
23
+ const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
24
+
25
+ /** Minimal unified-style diff without external deps. */
26
+ export function createUnifiedDiff(fromLabel, toLabel, fromText, toText) {
27
+ const fromLines = String(fromText ?? "").split("\n");
28
+ const toLines = String(toText ?? "").split("\n");
29
+ const lines = [`--- ${fromLabel}`, `+++ ${toLabel}`];
30
+ const max = Math.max(fromLines.length, toLines.length);
31
+ let identical = true;
32
+ for (let i = 0; i < max; i += 1) {
33
+ const a = fromLines[i];
34
+ const b = toLines[i];
35
+ if (a === b) {
36
+ if (a !== undefined) lines.push(` ${a}`);
37
+ continue;
38
+ }
39
+ identical = false;
40
+ if (a !== undefined) lines.push(`-${a}`);
41
+ if (b !== undefined) lines.push(`+${b}`);
42
+ }
43
+ if (identical) lines.push(" (no textual differences)");
44
+ return lines.join("\n");
45
+ }
46
+
47
+ async function buildSddContext(options) {
48
+ const homeDir = resolveHomeDir();
49
+ await assertComponentInstalled("sdd-core", { homeDir });
50
+ ensureIntegrationProvidersRegistered();
51
+ const provider = requireIntegrationProvider("sdd-core");
52
+ const state = await readGlobalState(harnessHomePaths(homeDir).statePath);
53
+ const trackedFiles = Object.fromEntries(
54
+ (state?.sdd?.files ?? []).map((file) => [file.destinationPath, file.hash])
55
+ );
56
+ return {
57
+ homeDir,
58
+ provider,
59
+ state,
60
+ packageRoot: options.packageRoot ?? DEFAULT_PACKAGE_ROOT,
61
+ requestedAgentIds: options.adapters ?? null,
62
+ detectedAgentIds: (state?.adapters ?? []).map((entry) => entry.id),
63
+ trackedFiles,
64
+ adoptedFiles: adoptedHashesFromState(state?.sdd),
65
+ personaAgentIds: state?.sdd?.personaAgentIds ?? []
66
+ };
67
+ }
68
+
69
+ /** Adopt conflict disk hashes into state — no file writes. */
70
+ export async function runComponentsAdopt(options = {}) {
71
+ if (options.componentId !== "sdd-core") {
72
+ throw new Error(
73
+ `components adopt supports sdd-core only (got "${options.componentId}").`
74
+ );
75
+ }
76
+
77
+ const dryRun = Boolean(options.dryRun);
78
+ const yes = Boolean(options.yes);
79
+ const json = Boolean(options.json);
80
+ assertExplicitApplyConsent({
81
+ applying: !dryRun,
82
+ dryRun,
83
+ json,
84
+ yes,
85
+ interactive: null,
86
+ command: "components adopt sdd-core"
87
+ });
88
+
89
+ const ctx = await buildSddContext(options);
90
+ const verification = await ctx.provider.verify({
91
+ homeDir: ctx.homeDir,
92
+ packageRoot: ctx.packageRoot,
93
+ requestedAgentIds: ctx.requestedAgentIds,
94
+ detectedAgentIds: ctx.detectedAgentIds,
95
+ trackedFiles: ctx.trackedFiles,
96
+ adoptedFiles: ctx.adoptedFiles,
97
+ personaAgentIds: ctx.personaAgentIds
98
+ });
99
+
100
+ const conflicts = (verification.findings ?? []).filter(
101
+ (finding) => finding.status === SDD_HEALTH.CONFLICT && finding.diskHash
102
+ );
103
+ const adoptions = conflicts.map((finding) => ({
104
+ destinationPath: finding.destinationPath,
105
+ hash: finding.diskHash,
106
+ skillId: finding.skillId,
107
+ agentIds: finding.agentIds ?? [],
108
+ relativePath: finding.relativePath ?? "SKILL.md",
109
+ reason: "Adopted pre-existing disk bytes via components adopt."
110
+ }));
111
+
112
+ const result = {
113
+ provider: "sdd-core",
114
+ componentId: "sdd-core",
115
+ dryRun,
116
+ ok: true,
117
+ adopted: adoptions.length,
118
+ adoptions,
119
+ summary: verification.summary
120
+ };
121
+
122
+ if (dryRun) {
123
+ if (json) { printJson(result); return result; }
124
+ console.log(formatCliCommand("components adopt sdd-core"));
125
+ console.log(`Plan: adopt=${adoptions.length} conflict files (no writes).`);
126
+ for (const entry of adoptions) {
127
+ console.log(` adopt ${entry.skillId} → ${entry.destinationPath}`);
128
+ }
129
+ console.log("Dry-run only — no state updated.");
130
+ return result;
131
+ }
132
+
133
+ if (shouldPromptApplyConfirmation({ applying: true, dryRun, json, confirm: yes, interactive: null })) {
134
+ const accepted = await promptApplyConfirmation({
135
+ command: "components adopt sdd-core",
136
+ question: `Adopt ${adoptions.length} conflicting SDD skill file(s) as-is into Kairo state? [Y/n]: `
137
+ });
138
+ if (!accepted) {
139
+ result.cancelled = true;
140
+ result.ok = false;
141
+ if (json) printJson(result);
142
+ else console.log("Cancelled.");
143
+ return result;
144
+ }
145
+ }
146
+
147
+ if (adoptions.length) {
148
+ await writeGlobalState(
149
+ harnessHomePaths(ctx.homeDir).statePath,
150
+ recordSddAdoptions(ctx.state ?? {}, { adoptions })
151
+ );
152
+ }
153
+
154
+ result.applied = true;
155
+ if (json) { printJson(result); return result; }
156
+ console.log(formatCliCommand("components adopt sdd-core"));
157
+ console.log(`Adopted ${adoptions.length} file(s) into Kairo state (disk unchanged).`);
158
+ for (const entry of adoptions) {
159
+ console.log(` adopted ${entry.skillId} → ${entry.destinationPath}`);
160
+ }
161
+ return result;
162
+ }
163
+
164
+ /** Read-only unified diff of conflict/drifted findings vs canonical. */
165
+ export async function runComponentsDiff(options = {}) {
166
+ if (options.componentId !== "sdd-core") {
167
+ throw new Error(
168
+ `components diff supports sdd-core only (got "${options.componentId}").`
169
+ );
170
+ }
171
+
172
+ const ctx = await buildSddContext(options);
173
+ const verification = await ctx.provider.verify({
174
+ homeDir: ctx.homeDir,
175
+ packageRoot: ctx.packageRoot,
176
+ requestedAgentIds: ctx.requestedAgentIds,
177
+ detectedAgentIds: ctx.detectedAgentIds,
178
+ trackedFiles: ctx.trackedFiles,
179
+ adoptedFiles: ctx.adoptedFiles,
180
+ personaAgentIds: ctx.personaAgentIds
181
+ });
182
+
183
+ const interesting = (verification.findings ?? []).filter(
184
+ (finding) => finding.status === SDD_HEALTH.CONFLICT || finding.status === SDD_HEALTH.DRIFTED
185
+ );
186
+
187
+ const diffs = [];
188
+ for (const finding of interesting) {
189
+ let canonicalText = "";
190
+ let diskText = "";
191
+ try {
192
+ const canonicalPath = resolveCanonicalSddSkillFile(
193
+ finding.skillId,
194
+ finding.relativePath ?? "SKILL.md",
195
+ ctx.packageRoot
196
+ );
197
+ canonicalText = await readFile(canonicalPath, "utf8");
198
+ } catch (error) {
199
+ canonicalText = `/* failed to read canonical: ${error.message} */\n`;
200
+ }
201
+ try {
202
+ diskText = await readFile(finding.destinationPath, "utf8");
203
+ } catch (error) {
204
+ diskText = `/* failed to read disk: ${error.message} */\n`;
205
+ }
206
+ diffs.push({
207
+ skillId: finding.skillId,
208
+ status: finding.status,
209
+ destinationPath: finding.destinationPath,
210
+ agentIds: finding.agentIds ?? [],
211
+ unified: createUnifiedDiff(
212
+ `canonical/${finding.skillId}/${finding.relativePath ?? "SKILL.md"}`,
213
+ finding.destinationPath,
214
+ canonicalText,
215
+ diskText
216
+ )
217
+ });
218
+ }
219
+
220
+ const result = {
221
+ provider: "sdd-core",
222
+ componentId: "sdd-core",
223
+ ok: true,
224
+ count: diffs.length,
225
+ diffs,
226
+ summary: verification.summary
227
+ };
228
+
229
+ if (options.json) {
230
+ printJson(result);
231
+ return result;
232
+ }
233
+
234
+ console.log(formatCliCommand("components diff sdd-core"));
235
+ console.log(`Findings: ${diffs.length} conflict/drifted file(s).`);
236
+ if (diffs.length === 0) {
237
+ console.log("No conflicting or drifted skill files.");
238
+ return result;
239
+ }
240
+ for (const entry of diffs) {
241
+ console.log("");
242
+ console.log(`## ${entry.status} ${entry.skillId} (${(entry.agentIds ?? []).join(",")})`);
243
+ console.log(entry.unified);
244
+ }
245
+ return result;
246
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Recommended button actions per connection chip.
3
+ * Optional tools never block governance; Kairo does not brew-install externals.
4
+ */
5
+ import { formatCliCommand } from "./brand/cli.js";
6
+
7
+ function action(id, label, command, kind = "run") {
8
+ return { id, label, command, kind };
9
+ }
10
+
11
+ /** @returns {{ optional: boolean, actions: Array<{id,label,command,kind}> }} */
12
+ export function actionsForConnection(connection) {
13
+ if (!connection || typeof connection !== "object") {
14
+ return { optional: true, actions: [] };
15
+ }
16
+ const id = connection.id;
17
+ const state = connection.state ?? "unknown";
18
+
19
+ switch (id) {
20
+ case "gentle":
21
+ return {
22
+ optional: true,
23
+ actions: state === "available"
24
+ ? [action("refresh", "Refresh", null, "refresh")]
25
+ : [
26
+ action("guide-gentle", "How to install", null, "guide"),
27
+ action("refresh", "Refresh after install", null, "refresh")
28
+ ]
29
+ };
30
+ case "hermes":
31
+ return {
32
+ optional: true,
33
+ actions: state === "available"
34
+ ? [action("refresh", "Refresh", null, "refresh")]
35
+ : state === "unavailable" || state === "error"
36
+ ? [
37
+ action("guide-hermes-api", "Enable API tip", null, "guide"),
38
+ action(
39
+ "start-hermes",
40
+ "Start Hermes gateway",
41
+ "hermes gateway run",
42
+ "configure"
43
+ ),
44
+ action("refresh", "Refresh", null, "refresh")
45
+ ]
46
+ : [
47
+ action("guide-hermes", "How to install", null, "guide"),
48
+ action("refresh", "Refresh after install", null, "refresh")
49
+ ]
50
+ };
51
+ case "engram":
52
+ return {
53
+ optional: true,
54
+ actions: state === "configured"
55
+ ? [action("refresh", "Refresh", null, "refresh")]
56
+ : state === "missing"
57
+ ? [
58
+ action("guide-engram", "How to install", null, "guide"),
59
+ action("refresh", "Refresh after install", null, "refresh")
60
+ ]
61
+ : [
62
+ action(
63
+ "configure-engram",
64
+ "Configure Engram",
65
+ formatCliCommand("components configure engram-memory --dry-run"),
66
+ "configure"
67
+ ),
68
+ action("refresh", "Refresh", null, "refresh")
69
+ ]
70
+ };
71
+ case "graphify": {
72
+ const wantsUpdate = state === "stale"
73
+ || /graphify update/i.test(connection.detail ?? "")
74
+ || /No graphify-out/i.test(connection.detail ?? "");
75
+ const wantsInstall = state === "missing"
76
+ || /Install graphify/i.test(connection.detail ?? "");
77
+ return {
78
+ optional: true,
79
+ actions: wantsInstall
80
+ ? [
81
+ action("guide-graphify", "How to install", null, "guide"),
82
+ action("refresh", "Refresh after install", null, "refresh")
83
+ ]
84
+ : wantsUpdate
85
+ ? [
86
+ action("update-graph", "Update graph", "graphify update .", "configure"),
87
+ action("refresh", "Refresh", null, "refresh")
88
+ ]
89
+ : [action("refresh", "Refresh", null, "refresh")]
90
+ };
91
+ }
92
+ case "agent":
93
+ return {
94
+ optional: false,
95
+ actions: state === "connected"
96
+ ? [action("refresh", "Refresh", null, "refresh")]
97
+ : [
98
+ action("connect-agent", "Connect Agent", formatCliCommand("mcp install"), "configure"),
99
+ action("refresh", "Refresh", null, "refresh")
100
+ ]
101
+ };
102
+ default:
103
+ return { optional: true, actions: [] };
104
+ }
105
+ }
106
+
107
+ export function enrichConnection(connection) {
108
+ const { optional, actions } = actionsForConnection(connection);
109
+ return {
110
+ ...connection,
111
+ optional,
112
+ actions
113
+ };
114
+ }
115
+
116
+ /** Top-level panel buttons that are not chip-specific. */
117
+ export function buildSetupActions({ needsAttention = false, agentConnected = false } = {}) {
118
+ const actions = [];
119
+ actions.push({
120
+ id: "setup",
121
+ label: "Setup",
122
+ command: formatCliCommand("setup"),
123
+ primary: !needsAttention && !agentConnected,
124
+ detail: "Detect agents you use (Cursor, Codex, Claude, …) and prepare only those."
125
+ });
126
+ if (needsAttention) {
127
+ actions.push({
128
+ id: "repair",
129
+ label: "Repair",
130
+ command: formatCliCommand("sync"),
131
+ primary: true
132
+ });
133
+ }
134
+ if (!agentConnected) {
135
+ actions.push({
136
+ id: "connect-agent",
137
+ label: "Connect Agent",
138
+ command: formatCliCommand("mcp install"),
139
+ primary: !needsAttention
140
+ });
141
+ }
142
+ actions.push(
143
+ { id: "doctor", label: "Doctor", command: formatCliCommand("doctor"), primary: false },
144
+ { id: "refresh", label: "Refresh", command: null, primary: false }
145
+ );
146
+ return actions;
147
+ }