@kal-elsam/kairo-runtime 0.14.0 → 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.
- package/CHANGELOG.md +84 -0
- package/bin/kairo-runtime.js +0 -0
- package/bin/kairo.js +0 -0
- package/package.json +1 -1
- package/src/cli.js +172 -8
- package/src/global/check-resolutions.js +31 -0
- package/src/global/cli-help.js +19 -2
- package/src/global/component-ecosystem-checks.js +2 -0
- package/src/global/component-integration-cli.js +29 -10
- package/src/global/components-resolve-cli.js +246 -0
- package/src/global/connection-actions.js +147 -0
- package/src/global/connections.js +269 -0
- package/src/global/fleet-configure-plan.js +123 -0
- package/src/global/fleet-configure.js +303 -0
- package/src/global/fleet-models.js +188 -0
- package/src/global/fleet-set.js +219 -0
- package/src/global/fleet-shared.js +38 -0
- package/src/global/ink/cockpit-controller.js +1 -1
- package/src/global/ink/cockpit-models.js +4 -1
- package/src/global/ink/orchestrator-app.js +21 -2
- package/src/global/ink/ux/live-overview.js +5 -11
- package/src/global/ink/ux/overview-needs.js +1 -1
- package/src/global/integrations/engram-evidence.js +7 -2
- package/src/global/integrations/sdd-apply.js +17 -7
- package/src/global/integrations/sdd-evidence.js +22 -3
- package/src/global/integrations/sdd-plan.js +21 -3
- package/src/global/integrations/sdd-resolutions.js +73 -0
- package/src/global/integrations/sdd-state.js +69 -0
- package/src/global/integrations/sdd-verify.js +9 -4
- package/src/global/mcp/kairo-mcp.js +56 -5
- package/src/global/mcp/resolve-mcp-workspace.js +51 -0
- package/src/global/mcp/work-snapshot-rule.js +89 -0
- package/src/global/mcp/work-snapshot-tool.js +49 -0
- package/src/global/mcp-install.js +239 -0
- package/src/global/next/next-cli.js +35 -0
- package/src/global/next/next-report.js +145 -0
- package/src/global/next/project-key.js +36 -0
- package/src/global/next/publish-work-snapshot.js +116 -0
- package/src/global/next/work-enroll.js +91 -0
- package/src/global/next/work-snapshot.js +216 -0
- package/src/global/observability/fleet-activity.js +197 -0
- package/src/global/observability/fleet-models-catalog.js +137 -0
- package/src/global/observability/fleet-platforms.js +166 -0
- package/src/global/observability/fleet-probe.js +229 -0
- 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
|
+
}
|