@kal-elsam/kairo-runtime 0.14.0 → 0.16.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 (56) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/bin/kairo-runtime.js +0 -0
  3. package/bin/kairo.js +0 -0
  4. package/package.json +1 -1
  5. package/src/cli.js +182 -8
  6. package/src/global/check-resolutions.js +31 -0
  7. package/src/global/cli-help.js +21 -2
  8. package/src/global/component-ecosystem-checks.js +2 -0
  9. package/src/global/component-integration-cli.js +29 -10
  10. package/src/global/components-resolve-cli.js +246 -0
  11. package/src/global/connection-actions.js +147 -0
  12. package/src/global/connections.js +269 -0
  13. package/src/global/control-plane/attention.js +141 -0
  14. package/src/global/control-plane/build-report.js +146 -0
  15. package/src/global/control-plane/cli.js +36 -0
  16. package/src/global/control-plane/constants.js +38 -0
  17. package/src/global/control-plane/gentle-adapters.js +183 -0
  18. package/src/global/control-plane/provider.js +69 -0
  19. package/src/global/control-plane/review-status.js +115 -0
  20. package/src/global/control-plane/sdd-status.js +49 -0
  21. package/src/global/control-plane/team.js +63 -0
  22. package/src/global/fleet-configure-plan.js +123 -0
  23. package/src/global/fleet-configure.js +303 -0
  24. package/src/global/fleet-models.js +188 -0
  25. package/src/global/fleet-set.js +219 -0
  26. package/src/global/fleet-shared.js +38 -0
  27. package/src/global/ink/cockpit-controller.js +1 -1
  28. package/src/global/ink/cockpit-models.js +4 -1
  29. package/src/global/ink/orchestrator-app.js +21 -2
  30. package/src/global/ink/ux/live-overview.js +5 -11
  31. package/src/global/ink/ux/overview-needs.js +1 -1
  32. package/src/global/integrations/engram-evidence.js +7 -2
  33. package/src/global/integrations/sdd-apply.js +17 -7
  34. package/src/global/integrations/sdd-evidence.js +22 -3
  35. package/src/global/integrations/sdd-plan.js +21 -3
  36. package/src/global/integrations/sdd-resolutions.js +73 -0
  37. package/src/global/integrations/sdd-state.js +69 -0
  38. package/src/global/integrations/sdd-verify.js +9 -4
  39. package/src/global/mcp/kairo-mcp.js +56 -5
  40. package/src/global/mcp/resolve-mcp-workspace.js +51 -0
  41. package/src/global/mcp/work-snapshot-rule.js +89 -0
  42. package/src/global/mcp/work-snapshot-tool.js +49 -0
  43. package/src/global/mcp-install.js +239 -0
  44. package/src/global/next/next-cli.js +35 -0
  45. package/src/global/next/next-report.js +145 -0
  46. package/src/global/next/project-key.js +36 -0
  47. package/src/global/next/publish-work-snapshot.js +116 -0
  48. package/src/global/next/work-enroll.js +91 -0
  49. package/src/global/next/work-snapshot.js +216 -0
  50. package/src/global/observability/fleet-activity.js +197 -0
  51. package/src/global/observability/fleet-models-catalog.js +137 -0
  52. package/src/global/observability/fleet-platforms.js +166 -0
  53. package/src/global/observability/fleet-probe.js +229 -0
  54. package/src/global/observability/gentle-probe.js +30 -2
  55. package/src/global/observability/index.js +2 -1
  56. package/src/global/paths.js +2 -1
@@ -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
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Connection chips for IDE panel / CLI — companion probes + agent MCP registration.
3
+ * Read-only; never mutates configs.
4
+ */
5
+ import { join } from "node:path";
6
+ import { readFile } from "node:fs/promises";
7
+ import { resolveHomeDir } from "./paths.js";
8
+ import { buildCompanionSnapshot } from "./observability/build-companion-snapshot.js";
9
+ import { runPassiveObservabilitySnapshot } from "./observability/passive-snapshot-flight.js";
10
+ import { inspectEngramIntegration } from "./integrations/engram-evidence.js";
11
+ import { resolveGitHeadSha } from "./observability/graphify-probe.js";
12
+ import { enrichConnection } from "./connection-actions.js";
13
+ import { buildFleetReport } from "./observability/fleet-probe.js";
14
+
15
+ export const CONNECTION_ACCESS = Object.freeze({
16
+ gentle: "Probe contract; export/import review bundles (import needs consent).",
17
+ hermes: "Read-only: sessions via loopback API. Never runs hermes doctor/status.",
18
+ engram: "Disk evidence + engram version; setup needs consent.",
19
+ graphify: "Read-only: query, path, explain on workspace graph.",
20
+ agent: "MCP tools for Cursor agents (status, runs, alerts, graph)."
21
+ });
22
+
23
+ export const MCP_CLIENTS = Object.freeze({
24
+ cursor: {
25
+ id: "cursor",
26
+ label: "Cursor",
27
+ configRelativePath: join(".cursor", "mcp.json")
28
+ }
29
+ });
30
+
31
+ function chip(id, label, state, access, detail) {
32
+ return enrichConnection({
33
+ id,
34
+ label,
35
+ state: typeof state === "string" && state ? state : "unknown",
36
+ access,
37
+ detail: typeof detail === "string" && detail ? detail : ""
38
+ });
39
+ }
40
+
41
+ function gentleDetail(state) {
42
+ switch (state) {
43
+ case "available":
44
+ return "Gentle AI is available for review-bundle export/import.";
45
+ case "missing":
46
+ return "Install gentle-ai separately, then Refresh.";
47
+ case "incompatible":
48
+ return "Gentle is present but the review contract is incompatible.";
49
+ case "error":
50
+ return "Gentle probe failed. Check PATH and try Doctor.";
51
+ default:
52
+ return `Gentle state: ${state}.`;
53
+ }
54
+ }
55
+
56
+ function hermesDetail(state) {
57
+ switch (state) {
58
+ case "available":
59
+ return "Hermes loopback API is reachable; sessions are read-only.";
60
+ case "missing":
61
+ return "Install Hermes Agent separately, then Refresh.";
62
+ case "auth_required":
63
+ return "Hermes API requires auth (KAIRO_HERMES_API_KEY).";
64
+ case "incompatible":
65
+ return "Hermes is present but the local API contract is incompatible.";
66
+ case "unavailable":
67
+ case "error":
68
+ return "Hermes binary found, but loopback API (http://127.0.0.1:8642) is down. Everyday chat is `hermes` (see https://hermes-ai.net/es/docs/quickstart/). For the Kairo chip: set API_SERVER_ENABLED=true in ~/.hermes/.env, run hermes gateway run, then Refresh. Optional.";
69
+ default:
70
+ return `Hermes state: ${state}.`;
71
+ }
72
+ }
73
+
74
+ function engramDetail(status) {
75
+ switch (status) {
76
+ case "configured":
77
+ return "Engram integration evidence looks configured on disk.";
78
+ case "available":
79
+ return "Engram binary found; some agents still need configure.";
80
+ case "unconfigured":
81
+ return "Engram binary found; configure via Settings or components configure.";
82
+ case "missing":
83
+ return "Engram not detected. Optional memory — governance still works.";
84
+ case "conflict":
85
+ return "Engram config conflict. Open Settings → Engram.";
86
+ case "restart_required":
87
+ return "Engram config written; restart the agent to activate MCP tools.";
88
+ case "unsupported":
89
+ return "Engram version unsupported by this Kairo contract.";
90
+ case "error":
91
+ return "Engram inspection failed.";
92
+ default:
93
+ return `Engram status: ${status}.`;
94
+ }
95
+ }
96
+
97
+ function graphifyDetail(state, graphStatus) {
98
+ if (graphStatus === "stale") {
99
+ return "Graph exists but may be stale vs git HEAD. Run graphify update .";
100
+ }
101
+ if (graphStatus === "missing") {
102
+ return "No graphify-out/graph.json. Run graphify update . in the workspace.";
103
+ }
104
+ if (state === "available" && (graphStatus === "ok" || graphStatus == null)) {
105
+ return "Graphify CLI available; graph ready for query/path/explain.";
106
+ }
107
+ if (state === "missing") {
108
+ return "Install graphify separately, then Refresh.";
109
+ }
110
+ if (state === "error" || graphStatus === "error" || graphStatus === "malformed") {
111
+ return "Graphify probe failed or graph is malformed.";
112
+ }
113
+ return `Graphify state: ${state}${graphStatus ? ` / ${graphStatus}` : ""}.`;
114
+ }
115
+
116
+ export function resolveMcpConfigPath(client = "cursor", { homeDir = resolveHomeDir() } = {}) {
117
+ const entry = MCP_CLIENTS[client] ?? MCP_CLIENTS.cursor;
118
+ return join(homeDir, entry.configRelativePath);
119
+ }
120
+
121
+ /** Healthy Cursor entry must include cwd: "." (legacy without cwd → Repair).
122
+ * Runtime workspace identity still prefers VSCODE_CWD when Cursor spawns under $HOME.
123
+ */
124
+ export function isHealthyKairoMcpEntry(entry) {
125
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
126
+ if (entry.command !== "kairo") return false;
127
+ if (!Array.isArray(entry.args) || entry.args.length !== 1 || entry.args[0] !== "mcp") {
128
+ return false;
129
+ }
130
+ return entry.cwd === ".";
131
+ }
132
+
133
+ /**
134
+ * Read-only: is Kairo registered under mcpServers.kairo for the client?
135
+ * Entries without cwd: "." are broken (Repair), not connected.
136
+ */
137
+ export async function detectAgentMcpRegistration({
138
+ client = "cursor",
139
+ homeDir = resolveHomeDir(),
140
+ readFileFn = readFile
141
+ } = {}) {
142
+ const path = resolveMcpConfigPath(client, { homeDir });
143
+ try {
144
+ const raw = await readFileFn(path, "utf8");
145
+ const parsed = JSON.parse(raw);
146
+ const entry = parsed?.mcpServers?.kairo;
147
+ if (entry && typeof entry === "object") {
148
+ if (isHealthyKairoMcpEntry(entry)) {
149
+ return {
150
+ connected: true,
151
+ state: "connected",
152
+ path,
153
+ detail: `Kairo MCP registered in ${path}. Reload Cursor MCP if tools are missing.`
154
+ };
155
+ }
156
+ return {
157
+ connected: false,
158
+ state: "error",
159
+ path,
160
+ detail: "Kairo MCP entry is unhealthy (require command kairo, args [mcp], cwd \".\"). Re-run kairo mcp install --yes."
161
+ };
162
+ }
163
+ return {
164
+ connected: false,
165
+ state: "not_connected",
166
+ path,
167
+ detail: "Kairo MCP is not registered. Click Connect Agent (opens kairo mcp install)."
168
+ };
169
+ } catch (error) {
170
+ if (error?.code === "ENOENT") {
171
+ return {
172
+ connected: false,
173
+ state: "not_connected",
174
+ path,
175
+ detail: "No Cursor MCP config yet. Click Connect Agent to create one."
176
+ };
177
+ }
178
+ return {
179
+ connected: false,
180
+ state: "error",
181
+ path,
182
+ detail: `Could not read ${path}: ${error?.message ?? error}`
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Pure adapter: companion snapshot (+ optional agent registration) → connection chips.
189
+ */
190
+ export function mapCompanionToConnections(companion = {}, agent = null) {
191
+ const gentleState = companion?.signals?.gentle?.state ?? "missing";
192
+ const hermesState = companion?.signals?.hermes?.activity?.state
193
+ ?? companion?.signals?.hermes?.state
194
+ ?? "missing";
195
+ const engramStatus = companion?.engram?.status ?? "missing";
196
+ const graphifyState = companion?.signals?.graphify?.state ?? "missing";
197
+ const graphStatus = companion?.signals?.graphify?.graphStatus ?? null;
198
+
199
+ const connections = [
200
+ chip("gentle", "Gentle", gentleState, CONNECTION_ACCESS.gentle, gentleDetail(gentleState)),
201
+ chip("hermes", "Hermes", hermesState, CONNECTION_ACCESS.hermes, hermesDetail(hermesState)),
202
+ chip("engram", "Engram", engramStatus, CONNECTION_ACCESS.engram, engramDetail(engramStatus)),
203
+ chip(
204
+ "graphify",
205
+ "Graphify",
206
+ graphStatus === "stale" ? "stale" : graphifyState,
207
+ CONNECTION_ACCESS.graphify,
208
+ graphifyDetail(graphifyState, graphStatus)
209
+ )
210
+ ];
211
+
212
+ if (agent) {
213
+ connections.push(chip(
214
+ "agent",
215
+ "Agent",
216
+ agent.state ?? (agent.connected ? "connected" : "not_connected"),
217
+ CONNECTION_ACCESS.agent,
218
+ agent.detail ?? ""
219
+ ));
220
+ }
221
+
222
+ return connections;
223
+ }
224
+
225
+ export async function buildConnectionsReport({
226
+ homeDir = resolveHomeDir(),
227
+ workspaceRoot = process.cwd(),
228
+ client = "cursor",
229
+ packageRoot = null,
230
+ packageName = null,
231
+ cliVersion = null,
232
+ buildCompanion = buildCompanionSnapshot,
233
+ buildObservability = (ctx) => runPassiveObservabilitySnapshot(ctx),
234
+ inspectEngram = inspectEngramIntegration,
235
+ detectAgent = detectAgentMcpRegistration,
236
+ resolveHead = resolveGitHeadSha,
237
+ buildFleet = buildFleetReport
238
+ } = {}) {
239
+ const cwd = workspaceRoot ?? process.cwd();
240
+ const headSha = typeof resolveHead === "function" ? resolveHead(cwd) : null;
241
+ const companion = await buildCompanion({
242
+ inspectEngram: (ctx) => inspectEngram({ homeDir, ...(ctx ?? {}) }),
243
+ buildObservability,
244
+ observabilityContext: {
245
+ cwd,
246
+ homeDir,
247
+ workspaceRoot: cwd,
248
+ headSha,
249
+ packageRoot,
250
+ packageName,
251
+ cliVersion
252
+ }
253
+ });
254
+ const agent = await detectAgent({ client, homeDir });
255
+ const connections = mapCompanionToConnections(companion, agent);
256
+ const fleet = typeof buildFleet === "function"
257
+ ? await buildFleet({ homeDir })
258
+ : { ok: true, fleets: [] };
259
+ return {
260
+ ok: companion?.ok !== false,
261
+ generatedAt: companion?.generatedAt ?? new Date().toISOString(),
262
+ client,
263
+ connections,
264
+ fleets: fleet?.fleets ?? [],
265
+ activity: fleet?.activity ?? null,
266
+ fleetNote: fleet?.note ?? "Declared config topology — not live token usage.",
267
+ orchestratorAuthority: fleet?.orchestratorAuthority ?? null
268
+ };
269
+ }