@kal-elsam/kairo-runtime 0.11.0 → 0.13.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 (71) hide show
  1. package/global-template/components/agent-skills/LICENSE +21 -0
  2. package/global-template/components/agent-skills/PROVENANCE.md +26 -0
  3. package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
  4. package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
  5. package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
  6. package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
  7. package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
  8. package/global-template/components/catalog.json +29 -0
  9. package/package.json +5 -2
  10. package/scripts/cockpit-smoke.mjs +2 -1
  11. package/src/cli.js +136 -8
  12. package/src/global/component-builders.js +3 -1
  13. package/src/global/components/agent-skills.js +27 -0
  14. package/src/global/ink/cockpit-control-center.js +107 -4
  15. package/src/global/ink/cockpit-scan.js +20 -2
  16. package/src/global/ink/ecosystem-updates-display.js +37 -0
  17. package/src/global/ink/launch-input.js +32 -1
  18. package/src/global/ink/obsidian-vault-display.js +37 -0
  19. package/src/global/ink/orchestrator-app.js +2 -1
  20. package/src/global/ink/orchestrator-state.js +17 -2
  21. package/src/global/ink/system-resources-display.js +109 -0
  22. package/src/global/ink/use-orchestrator-data.js +40 -4
  23. package/src/global/ink/ux/live-overview.js +11 -1
  24. package/src/global/mcp/kairo-mcp.js +230 -0
  25. package/src/global/observability/build-companion-snapshot.js +302 -0
  26. package/src/global/observability/build-observability-snapshot.js +24 -0
  27. package/src/global/observability/ecosystem-updates.js +224 -0
  28. package/src/global/observability/gentle-bundle-export.js +71 -0
  29. package/src/global/observability/gentle-bundle-import.js +122 -0
  30. package/src/global/observability/gentle-probe.js +155 -0
  31. package/src/global/observability/graphify-ops.js +133 -0
  32. package/src/global/observability/graphify-parse-cache.js +90 -0
  33. package/src/global/observability/graphify-probe.js +185 -0
  34. package/src/global/observability/hermes-activity.js +163 -0
  35. package/src/global/observability/hermes-probe.js +171 -0
  36. package/src/global/observability/index.js +124 -0
  37. package/src/global/observability/obsidian-knowledge-preview.js +214 -0
  38. package/src/global/observability/obsidian-knowledge-views.js +227 -0
  39. package/src/global/observability/obsidian-publisher.js +181 -0
  40. package/src/global/observability/obsidian-status.js +76 -0
  41. package/src/global/observability/obsidian-vault.js +259 -0
  42. package/src/global/observability/passive-snapshot-flight.js +93 -0
  43. package/src/global/observability/probe-contract.js +38 -0
  44. package/src/global/observability/probe-registry.js +30 -0
  45. package/src/global/observability/resource-advisor.js +71 -0
  46. package/src/global/observability/system-resources.js +171 -0
  47. package/src/global/runtime/alerts/alert-cli.js +31 -0
  48. package/src/global/runtime/alerts/alert-store.js +29 -6
  49. package/src/global/runtime/alerts/alert-validate.js +25 -1
  50. package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
  51. package/src/global/runtime/execution-adapters/claude.js +2 -1
  52. package/src/global/runtime/execution-adapters/codex.js +2 -1
  53. package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
  54. package/src/global/runtime/execution-adapters/cursor.js +2 -1
  55. package/src/global/runtime/execution-adapters/opencode.js +2 -1
  56. package/src/global/runtime/execution-adapters/pi.js +2 -1
  57. package/src/global/runtime/review/index.js +1 -1
  58. package/src/global/runtime/review/review-cli.js +113 -3
  59. package/src/global/runtime/review/review-git.js +142 -11
  60. package/src/global/runtime/review/review-patch.js +2 -0
  61. package/src/global/runtime/review/review-receipts.js +12 -7
  62. package/src/global/runtime/review/review-runner.js +2 -2
  63. package/src/global/runtime/review/review-types.js +8 -5
  64. package/src/global/runtime/review/review-validate.js +5 -1
  65. package/src/global/runtime/run-cli.js +2 -0
  66. package/src/global/runtime/run-manager.js +39 -18
  67. package/src/global/runtime/run-permissions.js +231 -0
  68. package/src/global/runtime/run-profile.js +2 -0
  69. package/src/global/runtime/run-supervisor.js +77 -37
  70. package/src/global/runtime/run-types.js +2 -0
  71. package/src/global/updates-cli.js +41 -0
@@ -0,0 +1,133 @@
1
+ import { printJson } from "../json-output.js";
2
+ import { commandHeader } from "../brand/index.js";
3
+ import { probeCommand as defaultProbeCommand } from "../cli-probe.js";
4
+ import {
5
+ assertGraphInsideWorkspace, inspectGraphArtifact, resolveGitHeadSha, resolveGraphifyBinaryPath
6
+ } from "./graphify-probe.js";
7
+
8
+ const OPS_TIMEOUT_MS = 15_000;
9
+ const MAX_STDOUT_BYTES = 256_000;
10
+ const DEFAULT_QUERY_BUDGET = 2000;
11
+ const MAX_QUERY_BUDGET = 8000;
12
+ const OPS = new Set(["query", "path", "explain"]);
13
+
14
+ function fail(code, diagnostics, extra = {}) {
15
+ return {
16
+ ok: false, exitCode: 2, code, text: null, truncated: false, timedOut: false,
17
+ providerStatus: null, diagnostics: diagnostics.map(String),
18
+ op: null, graphPath: null, graphStatus: null, binary: null, ...extra
19
+ };
20
+ }
21
+ function clampBudget(raw) {
22
+ if (raw == null || raw === "") return DEFAULT_QUERY_BUDGET;
23
+ const n = Number(raw);
24
+ return Number.isInteger(n) && n >= 1 && n <= MAX_QUERY_BUDGET ? n : null;
25
+ }
26
+ export async function runGraphifyOp({
27
+ op, args = [], graphPath, cwd = process.cwd(), workspaceRoot = cwd,
28
+ env = process.env, budget = DEFAULT_QUERY_BUDGET, whichCommand,
29
+ probeCommand = defaultProbeCommand, inspectGraph = inspectGraphArtifact,
30
+ containPath = assertGraphInsideWorkspace, headSha = null,
31
+ resolveHead = resolveGitHeadSha,
32
+ timeoutMs = OPS_TIMEOUT_MS, maxStdoutBytes = MAX_STDOUT_BYTES
33
+ } = {}) {
34
+ if (!OPS.has(op)) return fail("invalid_request", [`Unknown graphify op "${op}".`], { op, graphPath });
35
+ if (typeof graphPath !== "string" || !graphPath.trim()) {
36
+ return fail("invalid_request", ["Missing --graph path."], { op, graphPath: graphPath ?? null });
37
+ }
38
+ let queryBudget = DEFAULT_QUERY_BUDGET;
39
+ if (op === "query") {
40
+ queryBudget = clampBudget(budget);
41
+ if (queryBudget == null) {
42
+ return fail("invalid_request", [`Invalid --budget (integer 1..${MAX_QUERY_BUDGET}).`], { op, graphPath });
43
+ }
44
+ }
45
+ const binary = resolveGraphifyBinaryPath("graphify", env, whichCommand ? { whichCommand } : {});
46
+ if (!binary) return fail("graphify_missing", ["graphify absolute binary not resolved."], { op, graphPath });
47
+
48
+ const contained = containPath(workspaceRoot, graphPath, { cwd });
49
+ if (!contained.ok) {
50
+ return fail(contained.code ?? "graph_path_outside_workspace", [
51
+ contained.code === "graph_path_outside_workspace"
52
+ ? "Resolved --graph is outside the workspace; refusing spawn."
53
+ : (contained.error ?? "Failed to resolve --graph path.")
54
+ ], { op, graphPath: contained.path, binary });
55
+ }
56
+
57
+ const head = (typeof headSha === "string" && headSha.trim())
58
+ ? headSha.trim()
59
+ : resolveHead(cwd);
60
+ const artifact = inspectGraph(contained.path, { cwd, headSha: head });
61
+ if (artifact.status === "missing" || artifact.status === "malformed" || artifact.status === "error") {
62
+ const code = artifact.status === "error" ? "graphify_error" : "graph_unavailable";
63
+ return fail(code, artifact.diagnostics?.length ? artifact.diagnostics : [`graph ${artifact.status}`], {
64
+ op, graphPath: artifact.path, graphStatus: artifact.status, binary
65
+ });
66
+ }
67
+
68
+ const argv = op === "query"
69
+ ? ["query", ...args.map(String), "--budget", String(queryBudget), "--graph", artifact.path]
70
+ : [op, ...args.map(String), "--graph", artifact.path];
71
+ let provider;
72
+ try { provider = probeCommand(binary, argv, { cwd, env, timeoutMs }); }
73
+ catch {
74
+ return fail("provider_error", ["provider_error", "spawn_interrupted"], {
75
+ op, graphPath: artifact.path, graphStatus: artifact.status, binary
76
+ });
77
+ }
78
+
79
+ const timedOut = Boolean(provider?.timedOut);
80
+ const status = provider?.status ?? null;
81
+ const raw = String(provider?.stdout ?? "");
82
+ const truncated = Buffer.byteLength(raw, "utf8") > maxStdoutBytes;
83
+ const text = truncated ? Buffer.from(raw, "utf8").subarray(0, maxStdoutBytes).toString("utf8") : raw;
84
+ const diagnostics = [...(artifact.diagnostics ?? [])];
85
+ if (truncated) diagnostics.push("stdout_truncated");
86
+ const ok = provider?.ok === true && status === 0 && timedOut !== true;
87
+ if (!ok) {
88
+ const d = ["provider_error", ...diagnostics, ...(timedOut ? ["timed_out"] : []), status != null ? `status=${status}` : "status_unknown"];
89
+ return fail(timedOut ? "timed_out" : "provider_error", d, {
90
+ op, graphPath: artifact.path, graphStatus: artifact.status, binary,
91
+ text: text || null, truncated, providerStatus: status, timedOut
92
+ });
93
+ }
94
+ return {
95
+ ok: true, exitCode: 0, code: "ok", op, graphPath: artifact.path, graphStatus: artifact.status,
96
+ text, truncated, diagnostics, providerStatus: status, timedOut: false, binary
97
+ };
98
+ }
99
+
100
+ export async function runGraphifyCli(options, _pkg, deps = {}) {
101
+ try {
102
+ if (!options.graphifyAction || !options.graphPath) {
103
+ throw new Error("Missing graphify action or --graph. Use: kairo graphify <query|path|explain> ... --graph <path>");
104
+ }
105
+ const result = await (deps.runGraphifyOp ?? runGraphifyOp)({
106
+ op: options.graphifyAction, args: options.graphifyArgs ?? [],
107
+ graphPath: options.graphPath, cwd: options.cwd ?? process.cwd(),
108
+ workspaceRoot: options.cwd ?? process.cwd(), budget: options.graphifyBudget,
109
+ headSha: options.headSha ?? deps.headSha ?? null,
110
+ resolveHead: deps.resolveHead
111
+ });
112
+ process.exitCode = result.exitCode;
113
+ if (options.json) {
114
+ printJson({
115
+ ok: result.ok, exitCode: result.exitCode, code: result.code, op: result.op,
116
+ graphPath: result.graphPath, graphStatus: result.graphStatus, text: result.text,
117
+ truncated: result.truncated, diagnostics: result.diagnostics,
118
+ providerStatus: result.providerStatus, timedOut: result.timedOut
119
+ });
120
+ } else if (result.ok) console.log(`${commandHeader(`graphify ${options.graphifyAction}`)}\n${result.text ?? ""}`);
121
+ else {
122
+ console.error(`graphify ${options.graphifyAction} failed (${result.code}).`);
123
+ for (const line of result.diagnostics ?? []) console.error(` ${line}`);
124
+ }
125
+ return result;
126
+ } catch (error) {
127
+ const message = String(error?.message ?? error);
128
+ if (options.json) printJson({ ok: false, exitCode: 2, error: message, code: error?.code ?? null });
129
+ else console.error(message);
130
+ process.exitCode = 2;
131
+ return { exitCode: 2, error };
132
+ }
133
+ }
@@ -0,0 +1,90 @@
1
+ import { statSync, realpathSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export const GRAPHIFY_PARSE_TTL_MS = 5_000;
5
+ export const GRAPHIFY_PARSE_MAX_ENTRIES = 8;
6
+
7
+ const CACHEABLE = new Set(["ok", "stale"]);
8
+
9
+ /** @type {Map<string, { value: object, expiresAt: number }>} */
10
+ const entries = new Map();
11
+
12
+ export function resetGraphifyParseCacheForTests() {
13
+ entries.clear();
14
+ }
15
+
16
+ export function graphifyParseCacheSizeForTests() {
17
+ return entries.size;
18
+ }
19
+
20
+ function touch(key, entry) {
21
+ entries.delete(key);
22
+ entries.set(key, entry);
23
+ }
24
+
25
+ function evictOldest(maxEntries) {
26
+ while (entries.size > maxEntries) {
27
+ const oldest = entries.keys().next().value;
28
+ if (oldest == null) break;
29
+ entries.delete(oldest);
30
+ }
31
+ }
32
+
33
+ export function buildGraphifyParseIdentity(resolvedPath, headSha, {
34
+ stat = (p) => statSync(p)
35
+ } = {}) {
36
+ const st = stat(resolvedPath);
37
+ return [
38
+ String(resolvedPath),
39
+ String(st.dev),
40
+ String(st.ino),
41
+ String(st.size),
42
+ String(st.mtimeMs),
43
+ String(headSha ?? "")
44
+ ].join("\0");
45
+ }
46
+
47
+ /**
48
+ * Passive ok|stale parse cache. missing|malformed|error never cached.
49
+ * Caller must pass `inspect` (usually inspectGraphArtifact) to avoid cycles.
50
+ * Graphify ops must keep calling inspectGraphArtifact directly.
51
+ */
52
+ export function inspectGraphArtifactCached(graphPath, options = {}) {
53
+ const {
54
+ inspect,
55
+ now = Date.now,
56
+ ttlMs = GRAPHIFY_PARSE_TTL_MS,
57
+ maxEntries = GRAPHIFY_PARSE_MAX_ENTRIES,
58
+ stat = (p) => statSync(p),
59
+ realpath = (p) => realpathSync(p),
60
+ cwd = process.cwd(),
61
+ headSha = null,
62
+ ...rest
63
+ } = options;
64
+
65
+ if (typeof inspect !== "function") {
66
+ throw new Error("inspectGraphArtifactCached requires options.inspect");
67
+ }
68
+
69
+ if (typeof graphPath === "string" && graphPath.trim()) {
70
+ try {
71
+ const resolved = realpath(resolve(cwd, graphPath));
72
+ const identity = buildGraphifyParseIdentity(resolved, headSha, { stat });
73
+ const hit = entries.get(identity);
74
+ if (hit && hit.expiresAt > now()) {
75
+ touch(identity, hit);
76
+ return hit.value;
77
+ }
78
+ const result = inspect(graphPath, { ...rest, cwd, headSha, realpath });
79
+ if (CACHEABLE.has(result?.status)) {
80
+ touch(identity, { value: result, expiresAt: now() + ttlMs });
81
+ evictOldest(maxEntries);
82
+ }
83
+ return result;
84
+ } catch {
85
+ /* identity unavailable — fall through */
86
+ }
87
+ }
88
+
89
+ return inspect(graphPath, { ...rest, cwd, headSha, realpath });
90
+ }
@@ -0,0 +1,185 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { isExecutableAvailable, probeCommand as defaultProbeCommand } from "../cli-probe.js";
5
+ import { normalizeProbeResult } from "./probe-contract.js";
6
+ import { inspectGraphArtifactCached } from "./graphify-parse-cache.js";
7
+
8
+ export const GRAPH_REPORT_COMMIT_PATTERN = /Built from commit:\s*`([0-9a-f]+)`/i;
9
+
10
+ /** Env keys that redirect Git away from cwd — strip before rev-parse. */
11
+ const GIT_OVERRIDE_KEYS = Object.freeze([
12
+ "GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY",
13
+ "GIT_INDEX_FILE", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CEILING_DIRECTORIES",
14
+ "GIT_NAMESPACE"
15
+ ]);
16
+
17
+ export function scrubGitOverrideEnv(env = process.env) {
18
+ const clean = { ...env };
19
+ for (const key of GIT_OVERRIDE_KEYS) delete clean[key];
20
+ return clean;
21
+ }
22
+
23
+ /** Fail-soft productive HEAD bound to workspace top-level; never throws. */
24
+ export function resolveGitHeadSha(cwd = process.cwd(), {
25
+ spawn = spawnSync, timeoutMs = 3000, env = process.env, realpath = realpathSync
26
+ } = {}) {
27
+ try {
28
+ const cleanEnv = scrubGitOverrideEnv(env);
29
+ const opts = { cwd, encoding: "utf8", timeout: timeoutMs, env: cleanEnv };
30
+ const top = spawn("git", ["rev-parse", "--show-toplevel"], opts);
31
+ if (top.status !== 0) return null;
32
+ const topLevel = String(top.stdout ?? "").trim();
33
+ if (!topLevel) return null;
34
+ let wanted;
35
+ let actual;
36
+ try {
37
+ wanted = realpath(resolve(cwd));
38
+ actual = realpath(topLevel);
39
+ } catch {
40
+ return null;
41
+ }
42
+ if (wanted !== actual) return null;
43
+ const head = spawn("git", ["rev-parse", "HEAD"], opts);
44
+ if (head.status !== 0) return null;
45
+ const sha = String(head.stdout ?? "").trim();
46
+ return /^[0-9a-f]{7,64}$/i.test(sha) ? sha : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function effectiveHeadSha(headSha, cwd, resolveHead) {
53
+ if (typeof headSha === "string" && headSha.trim()) return headSha.trim();
54
+ return resolveHead(cwd);
55
+ }
56
+
57
+ function defaultWhichAbsolute(command, env) {
58
+ if (!isExecutableAvailable(command, { env })) return "";
59
+ const which = defaultProbeCommand("which", [command], { env, timeoutMs: 3000 });
60
+ const path = String(which.stdout ?? "").trim().split(/\r?\n/)[0] ?? "";
61
+ return isAbsolute(path) ? path : "";
62
+ }
63
+
64
+ export function resolveGraphifyBinaryPath(command = "graphify", env = process.env, {
65
+ whichCommand = defaultWhichAbsolute
66
+ } = {}) {
67
+ const resolved = whichCommand(command, env) || null;
68
+ return resolved && isAbsolute(resolved) ? resolved : null;
69
+ }
70
+
71
+ function ioFail(code, status, path, err) {
72
+ return { status, path, error: status === "missing" ? null : (err?.message ?? String(err)), diagnostics: [code] };
73
+ }
74
+
75
+ /** ENOENT→missing; invalid JSON / non-object / nodes|links not arrays→malformed; else error. */
76
+ export function inspectGraphArtifact(graphPath, {
77
+ cwd = process.cwd(), readFile = (p) => readFileSync(p, "utf8"),
78
+ realpath = (p) => realpathSync(p), headSha = null
79
+ } = {}) {
80
+ if (typeof graphPath !== "string" || !graphPath.trim()) {
81
+ return { status: "error", path: null, error: "missing graph path", diagnostics: ["invalid_request"] };
82
+ }
83
+ const abs = resolve(cwd, graphPath);
84
+ let resolved;
85
+ try { resolved = realpath(abs); }
86
+ catch (err) {
87
+ const missing = err?.code === "ENOENT";
88
+ return ioFail(missing ? "graph_missing" : "realpath_error", missing ? "missing" : "error", abs, err);
89
+ }
90
+ let raw;
91
+ try { raw = readFile(resolved); }
92
+ catch (err) {
93
+ const missing = err?.code === "ENOENT";
94
+ return ioFail(missing ? "graph_missing" : "read_error", missing ? "missing" : "error", resolved, err);
95
+ }
96
+ let payload;
97
+ try { payload = JSON.parse(String(raw)); }
98
+ catch { return { status: "malformed", path: resolved, error: null, diagnostics: ["invalid_json"] }; }
99
+ if (payload == null || typeof payload !== "object" || Array.isArray(payload)
100
+ || !Array.isArray(payload.nodes) || !Array.isArray(payload.links)) {
101
+ return { status: "malformed", path: resolved, error: null, diagnostics: ["nodes_or_links_invalid"] };
102
+ }
103
+ const diagnostics = [];
104
+ let status = "ok";
105
+ if (typeof headSha === "string" && headSha) {
106
+ try {
107
+ const m = String(readFile(join(dirname(resolved), "GRAPH_REPORT.md"))).match(GRAPH_REPORT_COMMIT_PATTERN);
108
+ if (m && !(headSha.startsWith(m[1]) || m[1].startsWith(headSha))) {
109
+ status = "stale";
110
+ diagnostics.push(`stale graph=${m[1]} head=${headSha.slice(0, 8)}`);
111
+ }
112
+ } catch { /* missing report → ok */ }
113
+ }
114
+ return { status, path: resolved, error: null, diagnostics };
115
+ }
116
+
117
+ /** realpath(--graph) inside workspace; missing leaf OK if parent is inside. */
118
+ export function assertGraphInsideWorkspace(workspaceRoot, graphPath, {
119
+ cwd = process.cwd(), realpath = (p) => realpathSync(p)
120
+ } = {}) {
121
+ let root;
122
+ try { root = realpath(resolve(cwd, workspaceRoot)); }
123
+ catch (err) {
124
+ return { ok: false, code: "graphify_error", path: resolve(cwd, graphPath), root: null, error: err?.message };
125
+ }
126
+ const abs = resolve(cwd, graphPath);
127
+ let target;
128
+ try { target = realpath(abs); }
129
+ catch (err) {
130
+ if (err?.code !== "ENOENT") return { ok: false, code: "graphify_error", path: abs, root, error: err?.message };
131
+ try { target = join(realpath(dirname(abs)), basename(abs)); }
132
+ catch (e) {
133
+ return { ok: false, code: e?.code === "ENOENT" ? "graph_unavailable" : "graphify_error", path: abs, root, error: e?.message };
134
+ }
135
+ }
136
+ const rel = relative(root, target);
137
+ if (rel.startsWith("..") || isAbsolute(rel)) {
138
+ return { ok: false, code: "graph_path_outside_workspace", path: target, root };
139
+ }
140
+ return { ok: true, code: null, path: target, root };
141
+ }
142
+
143
+ export async function probeGraphify({
144
+ env = process.env, cwd = process.cwd(), whichCommand = defaultWhichAbsolute,
145
+ inspectGraph = (path, opts) => inspectGraphArtifactCached(path, {
146
+ ...opts, inspect: inspectGraphArtifact
147
+ }),
148
+ headSha = null, resolveHead = resolveGitHeadSha
149
+ } = {}) {
150
+ let path = null;
151
+ try {
152
+ path = resolveGraphifyBinaryPath("graphify", env, { whichCommand });
153
+ if (!path) {
154
+ return normalizeProbeResult({
155
+ id: "graphify", state: "missing", diagnostics: ["graphify absolute binary not resolved."],
156
+ evidence: [{ kind: "binary", path: null }]
157
+ }, "graphify");
158
+ }
159
+ const head = effectiveHeadSha(headSha, cwd, resolveHead);
160
+ const graph = inspectGraph(join(cwd, "graphify-out", "graph.json"), { cwd, headSha: head });
161
+ const state = graph.status === "error" ? "error" : "available";
162
+ return normalizeProbeResult({
163
+ id: "graphify", state, diagnostics: [...(graph.diagnostics ?? [])],
164
+ evidence: [{ kind: "binary", path }, { kind: "graph", path: graph.path, status: graph.status }],
165
+ error: graph.status === "error" ? graph.error : null
166
+ }, "graphify");
167
+ } catch (err) {
168
+ return normalizeProbeResult({
169
+ id: "graphify", state: "error", evidence: [{ kind: "binary", path }],
170
+ diagnostics: [path ? "graph inspect failed" : "graphify binary resolve failed"],
171
+ error: err?.message ?? String(err)
172
+ }, "graphify");
173
+ }
174
+ }
175
+
176
+ export function createGraphifyProbe(deps = {}) {
177
+ return {
178
+ id: "graphify",
179
+ declaredEvents: Object.freeze([]),
180
+ declaredActions: Object.freeze(["query", "path", "explain"]),
181
+ async probe(context = {}) {
182
+ return probeGraphify({ ...deps, ...context, env: context.env ?? deps.env ?? process.env });
183
+ }
184
+ };
185
+ }
@@ -0,0 +1,163 @@
1
+ import { fetchJson } from "../intelligence/http.js";
2
+
3
+ export const DEFAULT_HERMES_API_URL = "http://127.0.0.1:8642";
4
+ export const HERMES_ACTIVITY_LIMIT_DEFAULT = 20;
5
+ export const HERMES_ACTIVITY_LIMIT_MAX = 50;
6
+ export const HERMES_ACTIVITY_TIMEOUT_MS = 2000;
7
+ export const HERMES_ACTIVE_WINDOW_MS = 5 * 60 * 1000;
8
+ const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1"]);
9
+
10
+ function emptyAgg() {
11
+ return { returnedCount: 0, activeCount: 0, endedCount: 0, hasMore: false, lastActiveAt: null };
12
+ }
13
+ function out(partial) {
14
+ return { state: "error", error: null, diagnostics: [], baseUrl: null, sessions: [], aggregates: emptyAgg(), ...partial };
15
+ }
16
+ function opaque(state, diagnostics, baseUrl = null) {
17
+ return out({ state, error: state, diagnostics: diagnostics.map(String), baseUrl });
18
+ }
19
+
20
+ /** Loopback HTTP(S) only — no embedded credentials. */
21
+ export function assertHermesLoopbackUrl(raw) {
22
+ try {
23
+ const url = new URL(String(raw ?? ""));
24
+ if (url.protocol !== "http:" && url.protocol !== "https:") return { ok: false, reason: "protocol" };
25
+ if (url.username || url.password) return { ok: false, reason: "credentials" };
26
+ if (!LOOPBACK.has(url.hostname.replace(/^\[|\]$/g, ""))) return { ok: false, reason: "not_loopback" };
27
+ return { ok: true, url: url.origin };
28
+ } catch { return { ok: false, reason: "invalid_url" }; }
29
+ }
30
+
31
+ function clampLimit(limit) {
32
+ const n = Number(limit);
33
+ return Number.isFinite(n)
34
+ ? Math.min(HERMES_ACTIVITY_LIMIT_MAX, Math.max(1, Math.trunc(n)))
35
+ : HERMES_ACTIVITY_LIMIT_DEFAULT;
36
+ }
37
+
38
+ async function hermesGet(url, { apiKey, timeoutMs, fetchImpl }) {
39
+ const headers = { Accept: "application/json" };
40
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
41
+ const wrapped = (input, init = {}) => fetchImpl(input, { ...init, method: "GET", redirect: "error" });
42
+ return fetchJson(url, { method: "GET", headers, timeoutMs, fetchImpl: wrapped });
43
+ }
44
+
45
+ function classifyTransport(res) {
46
+ if (res?.status === 401 || res?.status === 403) return "auth_required";
47
+ if (res?.status >= 500 || res?.error === "request timed out") return "error";
48
+ if (res?.status === 0) return /timed?\s*out|abort/.test(String(res.error ?? "")) ? "error" : "unavailable";
49
+ return null;
50
+ }
51
+
52
+ function isJson(data) {
53
+ return data != null && typeof data === "object" && !Array.isArray(data) && !("raw" in data);
54
+ }
55
+
56
+ /** Upstream: features.session_resources + endpoints.sessions {method,path}. */
57
+ export function capabilitiesAdvertiseSessionsList(caps) {
58
+ if (caps?.features?.session_resources !== true) return false;
59
+ const ep = caps?.endpoints?.sessions;
60
+ if (ep == null || typeof ep !== "object" || Array.isArray(ep)) return false;
61
+ return String(ep.method).toUpperCase() === "GET" && ep.path === "/api/sessions";
62
+ }
63
+
64
+ function parseTs(value) {
65
+ if (value == null) return { ok: true, ms: null };
66
+ if (typeof value !== "number" || !Number.isFinite(value)) return { ok: false };
67
+ const ms = value > 1e12 ? value : value * 1000;
68
+ if (!Number.isFinite(ms) || Math.abs(ms) > 8.64e15) return { ok: false };
69
+ return { ok: true, ms };
70
+ }
71
+ function countOrZero(value) {
72
+ if (value == null) return { ok: true, n: 0 };
73
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) return { ok: false };
74
+ return { ok: true, n: value };
75
+ }
76
+ function optString(value) {
77
+ if (value == null) return { ok: true, s: null };
78
+ return typeof value === "string" ? { ok: true, s: value } : { ok: false };
79
+ }
80
+
81
+ /** Official session row. Null on bad shape/types (no coercion). */
82
+ export function normalizeHermesSession(raw, { nowMs = Date.now() } = {}) {
83
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
84
+ if (typeof raw.id !== "string" || raw.id === "") return null;
85
+ const started = parseTs(raw.started_at), ended = parseTs(raw.ended_at), lastActive = parseTs(raw.last_active);
86
+ if (!started.ok || !ended.ok || !lastActive.ok) return null;
87
+ const messageCount = countOrZero(raw.message_count), toolCallCount = countOrZero(raw.tool_call_count);
88
+ const inputTokens = countOrZero(raw.input_tokens), outputTokens = countOrZero(raw.output_tokens);
89
+ if (!messageCount.ok || !toolCallCount.ok || !inputTokens.ok || !outputTokens.ok) return null;
90
+ const source = optString(raw.source), model = optString(raw.model);
91
+ const title = optString(raw.title), endReason = optString(raw.end_reason);
92
+ if (!source.ok || !model.ok || !title.ok || !endReason.ok) return null;
93
+ const lastActiveMs = lastActive.ms ?? ended.ms ?? started.ms;
94
+ const isEnded = ended.ms != null || (endReason.s != null && endReason.s.length > 0);
95
+ const iso = (ms) => (ms == null ? null : new Date(ms).toISOString());
96
+ return {
97
+ id: raw.id, source: source.s, model: model.s, title: title.s,
98
+ startedAt: iso(started.ms), endedAt: iso(ended.ms), lastActiveAt: iso(lastActiveMs),
99
+ messageCount: messageCount.n, toolCallCount: toolCallCount.n,
100
+ tokenCount: inputTokens.n + outputTokens.n,
101
+ active: !isEnded && lastActiveMs != null && (nowMs - lastActiveMs) >= 0 && (nowMs - lastActiveMs) <= HERMES_ACTIVE_WINDOW_MS
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Read-only Hermes session activity via official local API.
107
+ * Sequence: GET /v1/capabilities → GET /api/sessions (never CLI / state.db).
108
+ */
109
+ export async function loadHermesActivity({
110
+ env = process.env,
111
+ baseUrl = env.KAIRO_HERMES_API_URL ?? DEFAULT_HERMES_API_URL,
112
+ apiKey = env.KAIRO_HERMES_API_KEY ?? null,
113
+ limit = HERMES_ACTIVITY_LIMIT_DEFAULT,
114
+ timeoutMs = HERMES_ACTIVITY_TIMEOUT_MS,
115
+ nowMs = Date.now(),
116
+ fetchImpl = globalThis.fetch
117
+ } = {}) {
118
+ const checked = assertHermesLoopbackUrl(baseUrl);
119
+ if (!checked.ok) return opaque("incompatible", [`hermes baseUrl rejected: ${checked.reason}`]);
120
+ const origin = checked.url;
121
+ const capped = clampLimit(limit);
122
+ const key = apiKey ? String(apiKey) : null;
123
+
124
+ const capsRes = await hermesGet(`${origin}/v1/capabilities`, { apiKey: key, timeoutMs, fetchImpl });
125
+ const capsFail = classifyTransport(capsRes);
126
+ if (capsFail) return opaque(capsFail, [`hermes capabilities ${capsFail}`], origin);
127
+ if (!capsRes.ok || !isJson(capsRes.data)
128
+ || capsRes.data.platform !== "hermes-agent"
129
+ || !capabilitiesAdvertiseSessionsList(capsRes.data)) {
130
+ return opaque("incompatible", ["hermes session capabilities unsupported"], origin);
131
+ }
132
+
133
+ const qs = new URLSearchParams({ limit: String(capped), offset: "0", include_children: "false" });
134
+ const sessionsRes = await hermesGet(`${origin}/api/sessions?${qs}`, { apiKey: key, timeoutMs, fetchImpl });
135
+ const sessFail = classifyTransport(sessionsRes);
136
+ if (sessFail) return opaque(sessFail, [`hermes sessions ${sessFail}`], origin);
137
+ if (!sessionsRes.ok) return opaque("error", [`hermes sessions http ${sessionsRes.status}`], origin);
138
+ const payload = sessionsRes.data;
139
+ if (!isJson(payload) || payload.object !== "list" || !Array.isArray(payload.data)) {
140
+ return opaque("incompatible", ["hermes sessions schema unknown"], origin);
141
+ }
142
+
143
+ const sessions = [];
144
+ for (const row of payload.data) {
145
+ const session = normalizeHermesSession(row, { nowMs });
146
+ if (session == null) return opaque("incompatible", ["hermes session row invalid"], origin);
147
+ sessions.push(session);
148
+ }
149
+ sessions.sort((a, b) => (Date.parse(b.lastActiveAt ?? "") || 0) - (Date.parse(a.lastActiveAt ?? "") || 0)
150
+ || String(a.id).localeCompare(String(b.id)));
151
+
152
+ const hasMore = typeof payload.has_more === "boolean" ? payload.has_more : sessions.length >= capped;
153
+
154
+ return out({
155
+ state: "available", error: null, diagnostics: [], baseUrl: origin, sessions,
156
+ aggregates: {
157
+ returnedCount: sessions.length,
158
+ activeCount: sessions.filter((s) => s.active).length,
159
+ endedCount: sessions.filter((s) => s.endedAt != null).length,
160
+ hasMore, lastActiveAt: sessions[0]?.lastActiveAt ?? null
161
+ }
162
+ });
163
+ }