@kage-core/kage-graph-mcp 2.0.1 → 2.1.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/dist/cli.js +128 -5
- package/dist/daemon.js +153 -0
- package/dist/kernel.js +472 -10
- package/package.json +2 -2
- package/viewer/console.js +61 -0
- package/viewer/index.html +10 -1
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ const graph_registry_js_1 = require("./graph-registry.js");
|
|
|
13
13
|
const CORE_USAGE = `Kage — code-grounded memory for coding agents
|
|
14
14
|
|
|
15
15
|
Core commands:
|
|
16
|
+
kage install [--project <dir>] one-shot: init + index + auto-wire detected agents
|
|
16
17
|
kage demo 30-second trust demo (temp dir)
|
|
17
18
|
kage scan --project <dir> 60-second truth report on any repo (zero setup)
|
|
18
19
|
kage init --project <dir> create repo memory (.agent_memory only)
|
|
@@ -23,6 +24,7 @@ Core commands:
|
|
|
23
24
|
kage verify --project <dir> check memory citations against code
|
|
24
25
|
kage setup <agent> --project <dir> --write wire your agent (claude-code, codex, cursor, ...)
|
|
25
26
|
kage doctor --project <dir> health check
|
|
27
|
+
kage repair --project <dir> fix what doctor finds (indexes, broken packets, wiring)
|
|
26
28
|
kage viewer --project <dir> local dashboard
|
|
27
29
|
|
|
28
30
|
Run 'kage help --all' for the full command list (lifecycle, CI, benchmarks, daemon, workspace).`;
|
|
@@ -32,9 +34,11 @@ Usage:
|
|
|
32
34
|
kage index --project <dir>
|
|
33
35
|
kage scan --project <dir> [--json]
|
|
34
36
|
kage demo [--project <dir>]
|
|
37
|
+
kage install [--project <dir>] [--agents a,b] [--no-agents] [--json]
|
|
35
38
|
kage init --project <dir> [--with-policy]
|
|
36
39
|
kage policy --project <dir>
|
|
37
40
|
kage doctor --project <dir>
|
|
41
|
+
kage repair --project <dir> [--json]
|
|
38
42
|
kage setup list
|
|
39
43
|
kage setup <agent> --project <dir> [--write] [--json]
|
|
40
44
|
kage setup doctor --project <dir> [--json]
|
|
@@ -105,7 +109,8 @@ Usage:
|
|
|
105
109
|
kage observe --project <dir> --event <json>
|
|
106
110
|
kage sessions --project <dir> [--json]
|
|
107
111
|
kage replay --project <dir> [--session <id>] [--limit <n>] [--json]
|
|
108
|
-
kage distill --project <dir> --session <id>
|
|
112
|
+
kage distill --project <dir> --session <id> [--auto] [--json]
|
|
113
|
+
kage resume --project <dir> [--json]
|
|
109
114
|
kage learn --project <dir> --learning <text> [--title <title>] [--type <type>] [--evidence <text>] [--verified-by <text>] [--tags a,b] [--paths a,b] [--graph-nodes a,b] [--allow-missing-paths]
|
|
110
115
|
kage feedback --project <dir> --packet <packet-id> --kind helpful|wrong|stale
|
|
111
116
|
kage capture --project <dir> --title <title> --body <body> [--type <type>] [--summary <summary>] [--tags a,b] [--paths a,b] [--stack a,b] [--graph-nodes a,b] [--allow-missing-paths]
|
|
@@ -311,6 +316,72 @@ async function main() {
|
|
|
311
316
|
process.exit(2);
|
|
312
317
|
return;
|
|
313
318
|
}
|
|
319
|
+
if (command === "install") {
|
|
320
|
+
const project = projectArg(args);
|
|
321
|
+
const agentsFlag = takeArg(args, "--agents");
|
|
322
|
+
const skipAgents = args.includes("--no-agents");
|
|
323
|
+
const json = args.includes("--json");
|
|
324
|
+
const home = (0, node_os_1.homedir)();
|
|
325
|
+
// Detection is config-dir presence, not PATH: agents like Cursor never expose a binary.
|
|
326
|
+
const probes = [
|
|
327
|
+
{ agent: "claude-code", paths: [(0, node_path_1.join)(home, ".claude.json"), (0, node_path_1.join)(home, ".claude")] },
|
|
328
|
+
{ agent: "codex", paths: [(0, node_path_1.join)(home, ".codex")] },
|
|
329
|
+
{ agent: "cursor", paths: [(0, node_path_1.join)(home, ".cursor")] },
|
|
330
|
+
{ agent: "windsurf", paths: [(0, node_path_1.join)(home, ".codeium", "windsurf")] },
|
|
331
|
+
{ agent: "gemini-cli", paths: [(0, node_path_1.join)(home, ".gemini")] },
|
|
332
|
+
{ agent: "opencode", paths: [(0, node_path_1.join)(home, ".config", "opencode"), (0, node_path_1.join)(home, ".opencode")] },
|
|
333
|
+
{ agent: "goose", paths: [(0, node_path_1.join)(home, ".config", "goose")] },
|
|
334
|
+
{ agent: "aider", paths: [(0, node_path_1.join)(home, ".aider.conf.yml")] },
|
|
335
|
+
];
|
|
336
|
+
const requested = agentsFlag
|
|
337
|
+
? agentsFlag.split(",").map((a) => a.trim()).filter((a) => kernel_js_1.SETUP_AGENTS.includes(a))
|
|
338
|
+
: null;
|
|
339
|
+
const detected = requested ?? probes.filter((p) => p.paths.some((path) => (0, node_fs_1.existsSync)(path))).map((p) => p.agent);
|
|
340
|
+
const init = (0, kernel_js_1.initProject)(project, { policy: false });
|
|
341
|
+
const wired = [];
|
|
342
|
+
if (!skipAgents) {
|
|
343
|
+
for (const agent of detected) {
|
|
344
|
+
try {
|
|
345
|
+
const result = (0, kernel_js_1.setupAgent)(agent, project, { write: true });
|
|
346
|
+
wired.push({ agent, ok: result.wrote, config_path: result.config_path ?? undefined, error: result.wrote || result.write_supported ? undefined : `config is print-only — run: kage setup ${agent} --project . and paste it` });
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
wired.push({ agent, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (json) {
|
|
354
|
+
console.log(JSON.stringify({ project_dir: init.index.projectDir, packets: init.index.packets, validation_ok: init.validation.ok, agents: wired }, null, 2));
|
|
355
|
+
if (!init.validation.ok)
|
|
356
|
+
process.exit(2);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
console.log(`Kage installed in ${init.index.projectDir}\n`);
|
|
360
|
+
console.log(" Memory .agent_memory/ created — packets are plain files, reviewable in git");
|
|
361
|
+
console.log(` Indexes ${init.index.indexes.length} built (code graph, recall, structure)`);
|
|
362
|
+
if (skipAgents) {
|
|
363
|
+
console.log(" Agents skipped (--no-agents)");
|
|
364
|
+
}
|
|
365
|
+
else if (!wired.length) {
|
|
366
|
+
console.log(" Agents none detected — wire one manually: kage setup <agent> --project . --write");
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
for (const w of wired) {
|
|
370
|
+
if (w.ok)
|
|
371
|
+
console.log(` Agents ${w.agent} ✓ wired${w.config_path ? ` (${w.config_path})` : ""}`);
|
|
372
|
+
else
|
|
373
|
+
console.log(` Agents ${w.agent} ✗ ${w.error ?? "print-only; run kage setup " + w.agent + " --project . --write"}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
console.log("\nNext:");
|
|
377
|
+
console.log(" restart your agent — memory now recalls automatically at session start");
|
|
378
|
+
console.log(" kage scan --project . 60-second Truth Report on this repo");
|
|
379
|
+
console.log(" kage viewer --project . local dashboard (gains, packets, graph)");
|
|
380
|
+
console.log("\nVersion control: commit .agent_memory/packets/, ignore .agent_memory/indexes/ and reports/.");
|
|
381
|
+
if (!init.validation.ok)
|
|
382
|
+
process.exit(2);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
314
385
|
if (command === "policy") {
|
|
315
386
|
const result = (0, kernel_js_1.installAgentPolicy)(projectArg(args));
|
|
316
387
|
console.log(`${result.created ? "Created" : result.updated ? "Updated" : "Already current"} agent policy: ${result.path}`);
|
|
@@ -340,7 +411,36 @@ async function main() {
|
|
|
340
411
|
console.log(`Warnings:\n${result.validation.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
341
412
|
console.log("\nRecall smoke test:\n");
|
|
342
413
|
console.log(result.sampleRecall);
|
|
343
|
-
if (!result.validation.ok)
|
|
414
|
+
if (!result.validation.ok) {
|
|
415
|
+
console.log("\nSomething broken? kage repair --project .");
|
|
416
|
+
process.exit(2);
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (command === "repair") {
|
|
421
|
+
const result = (0, kernel_js_1.repairProject)(projectArg(args));
|
|
422
|
+
if (args.includes("--json")) {
|
|
423
|
+
console.log(JSON.stringify(result, null, 2));
|
|
424
|
+
if (!result.ok || !result.validation.ok)
|
|
425
|
+
process.exit(2);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.log(`Kage repair — ${result.project_dir}\n`);
|
|
429
|
+
const areaLabel = { packets: "Memory", indexes: "Indexes", locks: "Locks", agents: "Agents" };
|
|
430
|
+
for (const action of result.actions) {
|
|
431
|
+
const mark = action.status === "fixed" ? "✓" : action.status === "failed" ? "✗" : "•";
|
|
432
|
+
console.log(` ${(areaLabel[action.area] ?? action.area).padEnd(12)}${mark} ${action.target} — ${action.detail}`);
|
|
433
|
+
}
|
|
434
|
+
console.log(`\n${result.fixed} fixed, ${result.skipped} already healthy, ${result.failed} failed`);
|
|
435
|
+
if (result.removed_packets.length) {
|
|
436
|
+
console.log(`\nRemoved ${result.removed_packets.length} unrecoverable packet(s) — backups kept in .agent_memory/backup/:`);
|
|
437
|
+
for (const removed of result.removed_packets)
|
|
438
|
+
console.log(` ${removed}`);
|
|
439
|
+
}
|
|
440
|
+
console.log(result.validation.ok ? "Validation: passed" : "Validation: still failing");
|
|
441
|
+
if (result.validation.errors.length)
|
|
442
|
+
console.log(`Errors:\n${result.validation.errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
443
|
+
if (!result.ok || !result.validation.ok)
|
|
344
444
|
process.exit(2);
|
|
345
445
|
return;
|
|
346
446
|
}
|
|
@@ -1831,9 +1931,18 @@ async function main() {
|
|
|
1831
1931
|
const sessionId = takeArg(args, "--session");
|
|
1832
1932
|
if (!sessionId)
|
|
1833
1933
|
usage();
|
|
1834
|
-
const
|
|
1934
|
+
const project = projectArg(args);
|
|
1935
|
+
const auto = args.includes("--auto");
|
|
1936
|
+
const result = (0, kernel_js_1.distillSession)(project, sessionId, { auto });
|
|
1835
1937
|
if (args.includes("--json"))
|
|
1836
1938
|
console.log(JSON.stringify(result, null, 2));
|
|
1939
|
+
else if (auto) {
|
|
1940
|
+
// Auto mode is quiet: no output for empty or already-captured sessions; one line otherwise.
|
|
1941
|
+
const drafted = result.candidates.filter((candidate) => candidate.ok).length;
|
|
1942
|
+
if (!result.skipped_reason && drafted > 0) {
|
|
1943
|
+
console.log(`Auto-distilled ${drafted} pending draft${drafted === 1 ? "" : "s"} from session ${sessionId}. Review with: kage review --project ${project}`);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1837
1946
|
else {
|
|
1838
1947
|
console.log(`Distilled session: ${sessionId}`);
|
|
1839
1948
|
console.log(`Observations: ${result.observations}`);
|
|
@@ -1841,10 +1950,21 @@ async function main() {
|
|
|
1841
1950
|
if (result.errors.length)
|
|
1842
1951
|
console.log(`Errors:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
1843
1952
|
}
|
|
1844
|
-
if (!result.ok)
|
|
1953
|
+
if (!result.ok && !auto)
|
|
1845
1954
|
process.exit(2);
|
|
1846
1955
|
return;
|
|
1847
1956
|
}
|
|
1957
|
+
if (command === "resume") {
|
|
1958
|
+
const result = (0, kernel_js_1.kageResume)(projectArg(args));
|
|
1959
|
+
if (args.includes("--json")) {
|
|
1960
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
// Prints nothing when there is no prior session data, so hooks can append output verbatim.
|
|
1964
|
+
if (result.has_content && result.context_block)
|
|
1965
|
+
console.log(result.context_block);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1848
1968
|
if (command === "feedback") {
|
|
1849
1969
|
const id = takeArg(args, "--packet");
|
|
1850
1970
|
const kind = takeArg(args, "--kind");
|
|
@@ -1936,7 +2056,10 @@ async function main() {
|
|
|
1936
2056
|
}
|
|
1937
2057
|
usage();
|
|
1938
2058
|
}
|
|
2059
|
+
// Remediation-first failure: lead with the message, follow with exactly ONE
|
|
2060
|
+
// copy-pasteable next command. Exit code stays 1, same as before.
|
|
1939
2061
|
main().catch((error) => {
|
|
1940
|
-
console.error(error);
|
|
2062
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2063
|
+
console.error(`\nTry:\n ${(0, kernel_js_1.remediationFor)(error)}`);
|
|
1941
2064
|
process.exit(1);
|
|
1942
2065
|
});
|
package/dist/daemon.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.viewerStaticHeaders = viewerStaticHeaders;
|
|
4
4
|
exports.viewerRedirectLocation = viewerRedirectLocation;
|
|
5
5
|
exports.viewerReportPaths = viewerReportPaths;
|
|
6
|
+
exports.startLiveFeed = startLiveFeed;
|
|
6
7
|
exports.viewerUrl = viewerUrl;
|
|
7
8
|
exports.viewerBenchmarkReport = viewerBenchmarkReport;
|
|
8
9
|
exports.daemonContextReport = daemonContextReport;
|
|
@@ -105,6 +106,152 @@ function viewerReportPaths(projectRoot) {
|
|
|
105
106
|
value: (0, node_path_1.join)(reportsDir, "value.json"),
|
|
106
107
|
};
|
|
107
108
|
}
|
|
109
|
+
const LIVE_FEED_HEARTBEAT_MS = 25_000;
|
|
110
|
+
const LIVE_FEED_DEBOUNCE_MS = 100;
|
|
111
|
+
function readPacketTitle(filePath) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf8"));
|
|
114
|
+
return typeof parsed.title === "string" && parsed.title ? parsed.title : undefined;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Streams memory/value activity to viewer clients over SSE (GET /kage/events).
|
|
121
|
+
// The engine already writes packets and the value ledger to .agent_memory/, so a
|
|
122
|
+
// filesystem watch is the lightest possible event source — no queue, no new deps.
|
|
123
|
+
function startLiveFeed(projectRoot, options = {}) {
|
|
124
|
+
const heartbeatMs = options.heartbeatMs ?? LIVE_FEED_HEARTBEAT_MS;
|
|
125
|
+
const debounceMs = options.debounceMs ?? LIVE_FEED_DEBOUNCE_MS;
|
|
126
|
+
const packetsDir = (0, node_path_1.join)(projectRoot, ".agent_memory", "packets");
|
|
127
|
+
const reportsDir = (0, node_path_1.join)(projectRoot, ".agent_memory", "reports");
|
|
128
|
+
const valuePath = (0, node_path_1.join)(reportsDir, "value.json");
|
|
129
|
+
const clients = new Set();
|
|
130
|
+
const watchers = [];
|
|
131
|
+
const timers = new Map();
|
|
132
|
+
const knownPackets = new Set();
|
|
133
|
+
try {
|
|
134
|
+
(0, node_fs_1.mkdirSync)(packetsDir, { recursive: true });
|
|
135
|
+
for (const name of (0, node_fs_1.readdirSync)(packetsDir))
|
|
136
|
+
knownPackets.add(name);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// packets dir unavailable: packet events simply won't fire
|
|
140
|
+
}
|
|
141
|
+
function readValueEvents() {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(valuePath, "utf8"));
|
|
144
|
+
return Array.isArray(parsed.events) ? parsed.events : [];
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let seenValueEvents = readValueEvents().length;
|
|
151
|
+
function broadcast(event) {
|
|
152
|
+
const payload = `data: ${JSON.stringify(event)}\n\n`;
|
|
153
|
+
for (const res of clients)
|
|
154
|
+
res.write(payload);
|
|
155
|
+
}
|
|
156
|
+
function onPacketChange(name) {
|
|
157
|
+
const filePath = (0, node_path_1.join)(packetsDir, name);
|
|
158
|
+
if (!(0, node_fs_1.existsSync)(filePath)) {
|
|
159
|
+
knownPackets.delete(name);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const isNew = !knownPackets.has(name);
|
|
163
|
+
knownPackets.add(name);
|
|
164
|
+
broadcast({
|
|
165
|
+
type: isNew ? "packet_written" : "packet_updated",
|
|
166
|
+
title: readPacketTitle(filePath) ?? name.replace(/\.json$/, ""),
|
|
167
|
+
path: (0, node_path_1.join)(".agent_memory", "packets", name),
|
|
168
|
+
ts: new Date().toISOString(),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function onValueChange() {
|
|
172
|
+
const events = readValueEvents();
|
|
173
|
+
if (events.length < seenValueEvents)
|
|
174
|
+
seenValueEvents = 0; // ledger trimmed or rewritten
|
|
175
|
+
for (const event of events.slice(seenValueEvents)) {
|
|
176
|
+
broadcast({
|
|
177
|
+
type: "value_event",
|
|
178
|
+
title: typeof event.packet_title === "string" ? event.packet_title : undefined,
|
|
179
|
+
path: (0, node_path_1.join)(".agent_memory", "reports", "value.json"),
|
|
180
|
+
event,
|
|
181
|
+
ts: typeof event.at === "string" ? event.at : new Date().toISOString(),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
seenValueEvents = events.length;
|
|
185
|
+
}
|
|
186
|
+
// fs.watch fires bursts of duplicate events for one logical write; collapse
|
|
187
|
+
// them per file with a short debounce before reading and broadcasting.
|
|
188
|
+
function debounced(key, run) {
|
|
189
|
+
const existing = timers.get(key);
|
|
190
|
+
if (existing)
|
|
191
|
+
clearTimeout(existing);
|
|
192
|
+
timers.set(key, setTimeout(() => {
|
|
193
|
+
timers.delete(key);
|
|
194
|
+
try {
|
|
195
|
+
run();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// keep the feed alive even if a read races a write
|
|
199
|
+
}
|
|
200
|
+
}, debounceMs));
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
watchers.push((0, node_fs_1.watch)(packetsDir, (_event, filename) => {
|
|
204
|
+
const name = String(filename ?? "");
|
|
205
|
+
if (!name.endsWith(".json"))
|
|
206
|
+
return;
|
|
207
|
+
debounced(`packet:${name}`, () => onPacketChange(name));
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// packets dir missing: no packet events
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
(0, node_fs_1.mkdirSync)(reportsDir, { recursive: true });
|
|
215
|
+
watchers.push((0, node_fs_1.watch)(reportsDir, (_event, filename) => {
|
|
216
|
+
if (String(filename ?? "") !== "value.json")
|
|
217
|
+
return;
|
|
218
|
+
debounced("value", onValueChange);
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// reports dir missing: no value events
|
|
223
|
+
}
|
|
224
|
+
const heartbeat = setInterval(() => {
|
|
225
|
+
for (const res of clients)
|
|
226
|
+
res.write(`: heartbeat ${Date.now()}\n\n`);
|
|
227
|
+
}, heartbeatMs);
|
|
228
|
+
heartbeat.unref();
|
|
229
|
+
function handleRequest(req, res) {
|
|
230
|
+
res.writeHead(200, {
|
|
231
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
232
|
+
"cache-control": "no-cache, no-transform",
|
|
233
|
+
connection: "keep-alive",
|
|
234
|
+
"x-accel-buffering": "no",
|
|
235
|
+
});
|
|
236
|
+
res.write(": connected\n\n");
|
|
237
|
+
clients.add(res);
|
|
238
|
+
req.on("close", () => {
|
|
239
|
+
clients.delete(res);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function close() {
|
|
243
|
+
clearInterval(heartbeat);
|
|
244
|
+
for (const timer of timers.values())
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
timers.clear();
|
|
247
|
+
for (const watcher of watchers)
|
|
248
|
+
watcher.close();
|
|
249
|
+
for (const res of clients)
|
|
250
|
+
res.end();
|
|
251
|
+
clients.clear();
|
|
252
|
+
}
|
|
253
|
+
return { handleRequest, broadcast, clientCount: () => clients.size, close };
|
|
254
|
+
}
|
|
108
255
|
function viewerUrl(host, port, projectRoot) {
|
|
109
256
|
const query = Object.entries(viewerReportPaths(projectRoot))
|
|
110
257
|
.map(([name, path]) => `${name}=${encodeURIComponent(path)}`)
|
|
@@ -633,8 +780,13 @@ async function startViewer(projectDir, options = {}) {
|
|
|
633
780
|
// non-fatal: viewer will show 404 for reports if generation fails
|
|
634
781
|
}
|
|
635
782
|
const url = viewerUrl(host, port, projectRoot);
|
|
783
|
+
const liveFeed = startLiveFeed(projectRoot);
|
|
636
784
|
const server = (0, node_http_1.createServer)((req, res) => {
|
|
637
785
|
const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
786
|
+
if (req.method === "GET" && requestUrl.pathname === "/kage/events") {
|
|
787
|
+
liveFeed.handleRequest(req, res);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
638
790
|
let filePath = null;
|
|
639
791
|
const redirectLocation = viewerRedirectLocation(requestUrl.pathname, requestUrl.search, new URL(url).search);
|
|
640
792
|
if (redirectLocation) {
|
|
@@ -673,6 +825,7 @@ async function startViewer(projectDir, options = {}) {
|
|
|
673
825
|
await new Promise((resolveListen) => server.listen(port, host, resolveListen));
|
|
674
826
|
console.log(`Kage viewer listening on ${url}`);
|
|
675
827
|
process.on("SIGTERM", () => {
|
|
828
|
+
liveFeed.close();
|
|
676
829
|
server.close(() => process.exit(0));
|
|
677
830
|
});
|
|
678
831
|
return { ok: true, project_dir: projectRoot, host, port, url };
|
package/dist/kernel.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.SETUP_AGENTS = exports.MEMORY_TYPES = exports.PACKET_SCHEMA_VERSION = void 0;
|
|
36
|
+
exports.AUTO_DISTILL_TAG = exports.SETUP_AGENTS = exports.MEMORY_TYPES = exports.PACKET_SCHEMA_VERSION = void 0;
|
|
37
37
|
exports.memoryRoot = memoryRoot;
|
|
38
38
|
exports.packetsDir = packetsDir;
|
|
39
39
|
exports.pendingDir = pendingDir;
|
|
@@ -70,6 +70,7 @@ exports.kageMemoryReconciliation = kageMemoryReconciliation;
|
|
|
70
70
|
exports.evaluateMemoryAdmission = evaluateMemoryAdmission;
|
|
71
71
|
exports.validatePacket = validatePacket;
|
|
72
72
|
exports.scanSensitiveText = scanSensitiveText;
|
|
73
|
+
exports.stripPrivateSpans = stripPrivateSpans;
|
|
73
74
|
exports.catalogDomainNodeCount = catalogDomainNodeCount;
|
|
74
75
|
exports.ensureMemoryDirs = ensureMemoryDirs;
|
|
75
76
|
exports.loadApprovedPackets = loadApprovedPackets;
|
|
@@ -140,6 +141,7 @@ exports.kageSessionCaptureReport = kageSessionCaptureReport;
|
|
|
140
141
|
exports.kageSessionReplay = kageSessionReplay;
|
|
141
142
|
exports.kageSessionLearningLedger = kageSessionLearningLedger;
|
|
142
143
|
exports.distillSession = distillSession;
|
|
144
|
+
exports.kageResume = kageResume;
|
|
143
145
|
exports.proposeFromDiff = proposeFromDiff;
|
|
144
146
|
exports.buildBranchOverlay = buildBranchOverlay;
|
|
145
147
|
exports.createReviewArtifact = createReviewArtifact;
|
|
@@ -155,6 +157,9 @@ exports.recordFeedback = recordFeedback;
|
|
|
155
157
|
exports.validateProject = validateProject;
|
|
156
158
|
exports.initProject = initProject;
|
|
157
159
|
exports.doctorProject = doctorProject;
|
|
160
|
+
exports.splitConflictSides = splitConflictSides;
|
|
161
|
+
exports.repairProject = repairProject;
|
|
162
|
+
exports.remediationFor = remediationFor;
|
|
158
163
|
exports.approvePending = approvePending;
|
|
159
164
|
exports.rejectPending = rejectPending;
|
|
160
165
|
exports.changelog = changelog;
|
|
@@ -1784,6 +1789,28 @@ function scanSensitiveText(text) {
|
|
|
1784
1789
|
];
|
|
1785
1790
|
return patterns.filter(([, pattern]) => pattern.test(text)).map(([name]) => name);
|
|
1786
1791
|
}
|
|
1792
|
+
// Privacy tags: anything wrapped in <private>...</private> is redacted to
|
|
1793
|
+
// "[private]" BEFORE a packet or observation is written, so the content never
|
|
1794
|
+
// reaches disk. Matching is case-insensitive and spans newlines; an unclosed
|
|
1795
|
+
// <private> redacts to the end of the string so a malformed tag cannot leak.
|
|
1796
|
+
const PRIVATE_SPAN_PATTERN = /<private>[\s\S]*?(?:<\/private>|$)/gi;
|
|
1797
|
+
function stripPrivateSpans(text) {
|
|
1798
|
+
if (!text || text.toLowerCase().indexOf("<private>") === -1)
|
|
1799
|
+
return text;
|
|
1800
|
+
return text.replace(PRIVATE_SPAN_PATTERN, "[private]");
|
|
1801
|
+
}
|
|
1802
|
+
function stripPrivateFromContext(context) {
|
|
1803
|
+
const sanitized = { ...context };
|
|
1804
|
+
for (const key of ["fact", "why", "trigger", "action", "verification", "risk_if_forgotten", "stale_when"]) {
|
|
1805
|
+
const value = sanitized[key];
|
|
1806
|
+
if (typeof value === "string")
|
|
1807
|
+
sanitized[key] = stripPrivateSpans(value);
|
|
1808
|
+
}
|
|
1809
|
+
if (sanitized.rejected_alternatives) {
|
|
1810
|
+
sanitized.rejected_alternatives = sanitized.rejected_alternatives.map((entry) => stripPrivateSpans(entry));
|
|
1811
|
+
}
|
|
1812
|
+
return sanitized;
|
|
1813
|
+
}
|
|
1787
1814
|
function catalogDomainNodeCount(domain) {
|
|
1788
1815
|
return domain.nodes ?? domain.node_count ?? 0;
|
|
1789
1816
|
}
|
|
@@ -1861,8 +1888,11 @@ function loadApprovedPackets(projectDir) {
|
|
|
1861
1888
|
function loadPendingPackets(projectDir) {
|
|
1862
1889
|
return loadPacketsFromDir(pendingDir(projectDir));
|
|
1863
1890
|
}
|
|
1891
|
+
// Hook-driven auto-distilled drafts carry this tag so they are distinguishable from
|
|
1892
|
+
// agent-reviewed memory and never surface in recall until a human or agent approves them.
|
|
1893
|
+
exports.AUTO_DISTILL_TAG = "auto-distill";
|
|
1864
1894
|
function recallablePendingPackets(projectDir) {
|
|
1865
|
-
return loadPendingPackets(projectDir).filter((packet) => !packet.tags.includes("diff-proposal"));
|
|
1895
|
+
return loadPendingPackets(projectDir).filter((packet) => !packet.tags.includes("diff-proposal") && !packet.tags.includes(exports.AUTO_DISTILL_TAG));
|
|
1866
1896
|
}
|
|
1867
1897
|
function writePacket(projectDir, packet, statusDir) {
|
|
1868
1898
|
const dir = statusDir === "packets" ? packetsDir(projectDir) : pendingDir(projectDir);
|
|
@@ -11951,6 +11981,15 @@ function hasStructuredEngineeringContext(packet) {
|
|
|
11951
11981
|
return Boolean(context.why || context.verification || context.risk_if_forgotten || context.stale_when || context.trigger || context.action);
|
|
11952
11982
|
}
|
|
11953
11983
|
function learn(input) {
|
|
11984
|
+
// Redact <private> spans before deriving the title/summary so private text
|
|
11985
|
+
// never leaks into derived fields; capture() re-applies the same sanitizer.
|
|
11986
|
+
input = {
|
|
11987
|
+
...input,
|
|
11988
|
+
learning: stripPrivateSpans(input.learning),
|
|
11989
|
+
title: input.title === undefined ? undefined : stripPrivateSpans(input.title),
|
|
11990
|
+
evidence: input.evidence === undefined ? undefined : stripPrivateSpans(input.evidence),
|
|
11991
|
+
verifiedBy: input.verifiedBy === undefined ? undefined : stripPrivateSpans(input.verifiedBy),
|
|
11992
|
+
};
|
|
11954
11993
|
const type = inferLearningType(input);
|
|
11955
11994
|
const title = input.title?.trim() || titleFromLearning(input.learning);
|
|
11956
11995
|
const body = [
|
|
@@ -11971,10 +12010,20 @@ function learn(input) {
|
|
|
11971
12010
|
allowMissingPaths: input.allowMissingPaths,
|
|
11972
12011
|
strictCitations: input.strictCitations,
|
|
11973
12012
|
graphNodes: input.graphNodes,
|
|
12013
|
+
pendingReview: input.pendingReview,
|
|
11974
12014
|
});
|
|
11975
12015
|
}
|
|
11976
12016
|
function capture(input) {
|
|
11977
12017
|
ensureMemoryDirs(input.projectDir);
|
|
12018
|
+
// Privacy tags: redact <private> spans from every text field before any
|
|
12019
|
+
// validation, scanning, or storage — private content must never hit disk.
|
|
12020
|
+
input = {
|
|
12021
|
+
...input,
|
|
12022
|
+
title: stripPrivateSpans(input.title),
|
|
12023
|
+
summary: input.summary === undefined ? undefined : stripPrivateSpans(input.summary),
|
|
12024
|
+
body: stripPrivateSpans(input.body),
|
|
12025
|
+
context: input.context ? stripPrivateFromContext(input.context) : input.context,
|
|
12026
|
+
};
|
|
11978
12027
|
const type = input.type ?? "reference";
|
|
11979
12028
|
if (!exports.MEMORY_TYPES.includes(type)) {
|
|
11980
12029
|
return { ok: false, errors: [`Invalid memory type: ${type}`] };
|
|
@@ -12027,7 +12076,7 @@ function capture(input) {
|
|
|
12027
12076
|
scope: "repo",
|
|
12028
12077
|
visibility: "team",
|
|
12029
12078
|
sensitivity: "internal",
|
|
12030
|
-
status: "approved",
|
|
12079
|
+
status: input.pendingReview ? "pending" : "approved",
|
|
12031
12080
|
confidence: DEFAULT_CONFIDENCE,
|
|
12032
12081
|
tags: input.tags ?? [],
|
|
12033
12082
|
paths: groundedPaths,
|
|
@@ -12068,7 +12117,7 @@ function capture(input) {
|
|
|
12068
12117
|
...packet.quality,
|
|
12069
12118
|
...evaluateMemoryQuality(input.projectDir, packet),
|
|
12070
12119
|
};
|
|
12071
|
-
const path = writePacket(input.projectDir, packet, "packets");
|
|
12120
|
+
const path = writePacket(input.projectDir, packet, input.pendingReview ? "pending" : "packets");
|
|
12072
12121
|
recordMemoryAudit(input.projectDir, "capture", [packet], {
|
|
12073
12122
|
type: packet.type,
|
|
12074
12123
|
status: packet.status,
|
|
@@ -12297,6 +12346,16 @@ Before finishing a task that changed files: kage_pr_summarize or kage_propose_fr
|
|
|
12297
12346
|
If recalled memory helped: kage_feedback helpful. If wrong or stale: kage_feedback wrong or stale."
|
|
12298
12347
|
fi
|
|
12299
12348
|
|
|
12349
|
+
# Session continuity: append a compact "previously…" digest when prior session data exists.
|
|
12350
|
+
if command -v kage >/dev/null 2>&1; then
|
|
12351
|
+
PREVIOUSLY="$(kage resume --project "$CWD" 2>/dev/null || true)"
|
|
12352
|
+
if [[ -n "$PREVIOUSLY" ]]; then
|
|
12353
|
+
POLICY="$POLICY
|
|
12354
|
+
|
|
12355
|
+
$PREVIOUSLY"
|
|
12356
|
+
fi
|
|
12357
|
+
fi
|
|
12358
|
+
|
|
12300
12359
|
KAGE_MSG="$POLICY" python3 -c "import json,os; print(json.dumps({'systemMessage': os.environ['KAGE_MSG']}))"
|
|
12301
12360
|
`;
|
|
12302
12361
|
const stopHookScript = `#!/usr/bin/env bash
|
|
@@ -12333,6 +12392,20 @@ print(d.get("agent_instruction") or "Kage memory reconciliation required before
|
|
|
12333
12392
|
fi
|
|
12334
12393
|
fi
|
|
12335
12394
|
|
|
12395
|
+
# Automatic capture fallback: if this session recorded observations but produced no new
|
|
12396
|
+
# memory packets, quietly distill them into pending drafts for later review. Best-effort;
|
|
12397
|
+
# kage distill --auto is silent on empty or already-captured sessions and never blocks.
|
|
12398
|
+
SESSION="$(printf "%s" "$PAYLOAD" | python3 -c 'import json, sys
|
|
12399
|
+
try:
|
|
12400
|
+
d = json.load(sys.stdin)
|
|
12401
|
+
except Exception:
|
|
12402
|
+
d = {}
|
|
12403
|
+
print(d.get("session_id") or d.get("sessionId") or "")
|
|
12404
|
+
' 2>/dev/null || echo "")"
|
|
12405
|
+
if [[ -n "$SESSION" && -d "$CWD/.agent_memory/observations" ]]; then
|
|
12406
|
+
kage distill --project "$CWD" --session "$SESSION" --auto --json >/dev/null 2>&1 || true
|
|
12407
|
+
fi
|
|
12408
|
+
|
|
12336
12409
|
exit 0
|
|
12337
12410
|
`;
|
|
12338
12411
|
const observeHookScript = `#!/usr/bin/env bash
|
|
@@ -12742,6 +12815,14 @@ function observationHash(projectDir, event) {
|
|
|
12742
12815
|
}
|
|
12743
12816
|
function observe(projectDir, event) {
|
|
12744
12817
|
ensureMemoryDirs(projectDir);
|
|
12818
|
+
// Privacy tags: redact <private> spans from free-text fields before hashing,
|
|
12819
|
+
// scanning, or persisting the observation record.
|
|
12820
|
+
event = {
|
|
12821
|
+
...event,
|
|
12822
|
+
text: event.text === undefined ? undefined : stripPrivateSpans(event.text),
|
|
12823
|
+
summary: event.summary === undefined ? undefined : stripPrivateSpans(event.summary),
|
|
12824
|
+
command: event.command === undefined ? undefined : stripPrivateSpans(event.command),
|
|
12825
|
+
};
|
|
12745
12826
|
const allowed = ["session_start", "user_prompt", "tool_use", "tool_result", "file_change", "command_result", "test_result", "session_end"];
|
|
12746
12827
|
if (!allowed.includes(event.type))
|
|
12747
12828
|
return { ok: false, stored: false, duplicate: false, errors: [`Invalid observation type: ${event.type}`] };
|
|
@@ -13244,8 +13325,32 @@ function kageSessionLearningLedger(projectDir, options = {}) {
|
|
|
13244
13325
|
context_block: learningLedgerContextBlock(reportWithoutBlock),
|
|
13245
13326
|
};
|
|
13246
13327
|
}
|
|
13247
|
-
|
|
13328
|
+
// Mechanical packets (branch change memory, prior auto-distilled drafts) never count as the
|
|
13329
|
+
// agent having captured memory; only deliberate captures/learns/distills suppress the
|
|
13330
|
+
// Stop-hook auto-distill fallback.
|
|
13331
|
+
function sessionAlreadyCaptured(projectDir, sessionId, observations) {
|
|
13332
|
+
const firstAt = observations[0]?.timestamp ?? "";
|
|
13333
|
+
const mechanicalTags = ["diff-proposal", "change-memory", exports.AUTO_DISTILL_TAG];
|
|
13334
|
+
return [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)].some((packet) => {
|
|
13335
|
+
if (packet.source_refs.some((ref) => ref.kind === "observation_session" && ref.session_id === sessionId))
|
|
13336
|
+
return true;
|
|
13337
|
+
if (packet.type === "repo_map" || packet.quality?.reviewer === "kage-indexer")
|
|
13338
|
+
return false; // generated by indexing
|
|
13339
|
+
if (packet.tags.some((tag) => mechanicalTags.includes(tag)))
|
|
13340
|
+
return false;
|
|
13341
|
+
return Boolean(firstAt) && packet.created_at >= firstAt;
|
|
13342
|
+
});
|
|
13343
|
+
}
|
|
13344
|
+
function distillSession(projectDir, sessionId, options = {}) {
|
|
13345
|
+
const auto = Boolean(options.auto);
|
|
13346
|
+
const mode = auto ? "auto" : "manual";
|
|
13248
13347
|
const observations = loadObservations(projectDir, sessionId);
|
|
13348
|
+
if (auto && observations.length === 0) {
|
|
13349
|
+
return { ok: true, session_id: sessionId, observations: 0, candidates: [], errors: [], mode, skipped_reason: "no_observations" };
|
|
13350
|
+
}
|
|
13351
|
+
if (auto && sessionAlreadyCaptured(projectDir, sessionId, observations)) {
|
|
13352
|
+
return { ok: true, session_id: sessionId, observations: observations.length, candidates: [], errors: [], mode, skipped_reason: "session_already_captured" };
|
|
13353
|
+
}
|
|
13249
13354
|
const candidates = [];
|
|
13250
13355
|
const errors = [];
|
|
13251
13356
|
const observationIds = observations.map((event) => event.id);
|
|
@@ -13262,13 +13367,14 @@ function distillSession(projectDir, sessionId) {
|
|
|
13262
13367
|
];
|
|
13263
13368
|
result.packet.quality = {
|
|
13264
13369
|
...result.packet.quality,
|
|
13265
|
-
distillation: "automatic_observation_candidate",
|
|
13370
|
+
distillation: auto ? "auto_distill" : "automatic_observation_candidate",
|
|
13266
13371
|
admission: evaluateMemoryAdmission(projectDir, result.packet),
|
|
13267
13372
|
suggested_review_action: suggestedAction(classifyPacket(projectDir, result.packet), result.packet.status),
|
|
13268
13373
|
};
|
|
13269
13374
|
writeJson(result.path, result.packet);
|
|
13270
13375
|
return result;
|
|
13271
13376
|
};
|
|
13377
|
+
const autoTags = auto ? [exports.AUTO_DISTILL_TAG] : [];
|
|
13272
13378
|
const commandEvents = observations.filter((event) => event.type === "command_result" && event.command);
|
|
13273
13379
|
const fileEvents = observations.filter((event) => event.type === "file_change" && event.path);
|
|
13274
13380
|
const promptEvents = observations.filter((event) => event.type === "user_prompt" && (event.text || event.summary));
|
|
@@ -13284,8 +13390,9 @@ function distillSession(projectDir, sessionId) {
|
|
|
13284
13390
|
summary: `Observed commands: ${commands.slice(0, 3).join(", ")}`,
|
|
13285
13391
|
body: `Reusable command observation distilled from session ${sessionId}:\n\n${meaningfulCommandEvents.map((item) => `- ${item.reusable.command}: ${item.reusable.learning}`).join("\n")}\n\nReview before approving as a durable runbook.`,
|
|
13286
13392
|
type: "runbook",
|
|
13287
|
-
tags: ["observed-session", "commands", "runbook"],
|
|
13393
|
+
tags: ["observed-session", "commands", "runbook", ...autoTags],
|
|
13288
13394
|
paths: unique(meaningfulCommandEvents.map((item) => item.event.path).filter(Boolean)),
|
|
13395
|
+
pendingReview: auto,
|
|
13289
13396
|
})));
|
|
13290
13397
|
}
|
|
13291
13398
|
const meaningfulFileEvents = fileEvents
|
|
@@ -13300,8 +13407,9 @@ function distillSession(projectDir, sessionId) {
|
|
|
13300
13407
|
summary: lead,
|
|
13301
13408
|
body: `Reusable file observation distilled from session ${sessionId}:\n\n${meaningfulFileEvents.map((item) => `- ${item.event.path}: ${item.learning}`).join("\n")}\n\nReview before approving as durable repo memory.`,
|
|
13302
13409
|
type: "workflow",
|
|
13303
|
-
tags: ["observed-session", "workflow"],
|
|
13410
|
+
tags: ["observed-session", "workflow", ...autoTags],
|
|
13304
13411
|
paths,
|
|
13412
|
+
pendingReview: auto,
|
|
13305
13413
|
})));
|
|
13306
13414
|
}
|
|
13307
13415
|
if (promptEvents.length) {
|
|
@@ -13312,13 +13420,94 @@ function distillSession(projectDir, sessionId) {
|
|
|
13312
13420
|
title: titleFromLearning(text),
|
|
13313
13421
|
learning: text,
|
|
13314
13422
|
evidence: `Observation session: ${sessionId}`,
|
|
13315
|
-
tags: ["observed-session", "intent"],
|
|
13423
|
+
tags: ["observed-session", "intent", ...autoTags],
|
|
13424
|
+
pendingReview: auto,
|
|
13316
13425
|
})));
|
|
13317
13426
|
}
|
|
13318
13427
|
for (const result of candidates)
|
|
13319
13428
|
if (!result.ok)
|
|
13320
13429
|
errors.push(...result.errors);
|
|
13321
|
-
return { ok: errors.length === 0, session_id: sessionId, observations: observations.length, candidates, errors };
|
|
13430
|
+
return { ok: errors.length === 0, session_id: sessionId, observations: observations.length, candidates, errors, mode };
|
|
13431
|
+
}
|
|
13432
|
+
// Session continuity: a compact "previously…" digest the SessionStart hook injects so a new
|
|
13433
|
+
// session starts with last session's context instead of cold. Empty when there is no prior data.
|
|
13434
|
+
function kageResume(projectDir) {
|
|
13435
|
+
ensureMemoryDirs(projectDir);
|
|
13436
|
+
const approved = loadApprovedPackets(projectDir);
|
|
13437
|
+
const pending = loadPendingPackets(projectDir);
|
|
13438
|
+
const observations = loadObservations(projectDir);
|
|
13439
|
+
const bySession = new Map();
|
|
13440
|
+
for (const observation of observations) {
|
|
13441
|
+
const rows = bySession.get(observation.session_id) ?? [];
|
|
13442
|
+
rows.push(observation);
|
|
13443
|
+
bySession.set(observation.session_id, rows);
|
|
13444
|
+
}
|
|
13445
|
+
const latestRows = Array.from(bySession.values())
|
|
13446
|
+
.sort((a, b) => (b.at(-1)?.timestamp ?? "").localeCompare(a.at(-1)?.timestamp ?? ""))[0];
|
|
13447
|
+
const lastSession = latestRows?.length
|
|
13448
|
+
? (() => {
|
|
13449
|
+
const sessionId = latestRows[0].session_id;
|
|
13450
|
+
const distilledTitles = [...approved, ...pending]
|
|
13451
|
+
.filter((packet) => packet.source_refs.some((ref) => ref.kind === "observation_session" && ref.session_id === sessionId))
|
|
13452
|
+
.map((packet) => packet.title);
|
|
13453
|
+
return {
|
|
13454
|
+
session_id: sessionId,
|
|
13455
|
+
first_at: latestRows[0]?.timestamp ?? "",
|
|
13456
|
+
last_at: latestRows.at(-1)?.timestamp ?? "",
|
|
13457
|
+
observations: latestRows.length,
|
|
13458
|
+
paths: unique(latestRows.map((event) => event.path).filter(Boolean)).slice(0, 6),
|
|
13459
|
+
commands: unique(latestRows.map((event) => event.command).filter(Boolean)).slice(0, 3),
|
|
13460
|
+
distilled_titles: unique(distilledTitles).slice(0, 3),
|
|
13461
|
+
};
|
|
13462
|
+
})()
|
|
13463
|
+
: undefined;
|
|
13464
|
+
const changeMemory = approved
|
|
13465
|
+
.filter((packet) => packet.tags.includes("change-memory"))
|
|
13466
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))[0];
|
|
13467
|
+
const lastChangeMemory = changeMemory
|
|
13468
|
+
? { id: changeMemory.id, title: changeMemory.title, summary: changeMemory.summary, updated_at: changeMemory.updated_at }
|
|
13469
|
+
: undefined;
|
|
13470
|
+
const pendingAutoDistilled = pending.filter((packet) => packet.tags.includes(exports.AUTO_DISTILL_TAG)).length;
|
|
13471
|
+
const reconciliation = kageMemoryReconciliation(projectDir, { limit: 5 });
|
|
13472
|
+
const reconciliationItems = reconciliation.items.map((item) => ({ packet_id: item.packet_id, title: item.title }));
|
|
13473
|
+
const hasContent = Boolean(lastSession || lastChangeMemory || pendingAutoDistilled || reconciliation.unresolved_count);
|
|
13474
|
+
const lines = [];
|
|
13475
|
+
if (hasContent) {
|
|
13476
|
+
lines.push("# Previously (Kage)");
|
|
13477
|
+
if (lastSession) {
|
|
13478
|
+
lines.push(`Last session ${lastSession.session_id} (${lastSession.observations} observation${lastSession.observations === 1 ? "" : "s"}, ended ${lastSession.last_at}).`);
|
|
13479
|
+
if (lastSession.paths.length)
|
|
13480
|
+
lines.push(`Worked on: ${lastSession.paths.join(", ")}`);
|
|
13481
|
+
if (lastSession.commands.length)
|
|
13482
|
+
lines.push(`Commands: ${lastSession.commands.join("; ")}`);
|
|
13483
|
+
if (lastSession.distilled_titles.length)
|
|
13484
|
+
lines.push(`Learned: ${lastSession.distilled_titles.join("; ")}`);
|
|
13485
|
+
}
|
|
13486
|
+
if (lastChangeMemory) {
|
|
13487
|
+
lines.push(`Change memory: ${lastChangeMemory.title} — ${lastChangeMemory.summary}`);
|
|
13488
|
+
}
|
|
13489
|
+
if (pendingAutoDistilled) {
|
|
13490
|
+
lines.push(`Pending: ${pendingAutoDistilled} auto-distilled draft${pendingAutoDistilled === 1 ? "" : "s"} awaiting review — run: kage review --project ${projectDir}`);
|
|
13491
|
+
}
|
|
13492
|
+
if (reconciliation.unresolved_count) {
|
|
13493
|
+
lines.push(`Reconcile: ${reconciliation.unresolved_count} linked memory item${reconciliation.unresolved_count === 1 ? "" : "s"} need update — run: kage reconcile --project ${projectDir}`);
|
|
13494
|
+
for (const item of reconciliationItems.slice(0, 3))
|
|
13495
|
+
lines.push(` - ${item.packet_id}: ${item.title}`);
|
|
13496
|
+
}
|
|
13497
|
+
}
|
|
13498
|
+
return {
|
|
13499
|
+
schema_version: 1,
|
|
13500
|
+
project_dir: projectDir,
|
|
13501
|
+
generated_at: nowIso(),
|
|
13502
|
+
has_content: hasContent,
|
|
13503
|
+
last_session: lastSession,
|
|
13504
|
+
last_change_memory: lastChangeMemory,
|
|
13505
|
+
pending_auto_distilled: pendingAutoDistilled,
|
|
13506
|
+
pending_total: pending.length,
|
|
13507
|
+
...(pendingAutoDistilled ? { review_command: `kage review --project ${projectDir}` } : {}),
|
|
13508
|
+
reconciliation: { unresolved_count: reconciliation.unresolved_count, items: reconciliationItems },
|
|
13509
|
+
context_block: lines.slice(0, 15).join("\n"),
|
|
13510
|
+
};
|
|
13322
13511
|
}
|
|
13323
13512
|
function createDiffChangeMemory(projectDir, summary) {
|
|
13324
13513
|
const branch = summary.branch ?? "detached";
|
|
@@ -14234,6 +14423,279 @@ function doctorProject(projectDir) {
|
|
|
14234
14423
|
sampleRecall: sampleRecall.context_block,
|
|
14235
14424
|
};
|
|
14236
14425
|
}
|
|
14426
|
+
function repairBackupDir(projectDir) {
|
|
14427
|
+
return (0, node_path_1.join)(memoryRoot(projectDir), "backup");
|
|
14428
|
+
}
|
|
14429
|
+
// Split a git merge-conflicted file into its two sides. Returns null when no
|
|
14430
|
+
// complete conflict block is present. diff3-style base sections (`|||||||`)
|
|
14431
|
+
// belong to neither side and are dropped.
|
|
14432
|
+
function splitConflictSides(content) {
|
|
14433
|
+
let section = "both";
|
|
14434
|
+
let conflicts = 0;
|
|
14435
|
+
const ours = [];
|
|
14436
|
+
const theirs = [];
|
|
14437
|
+
for (const line of content.split("\n")) {
|
|
14438
|
+
if (section === "both" && /^<{7}(\s|$)/.test(line)) {
|
|
14439
|
+
section = "ours";
|
|
14440
|
+
conflicts += 1;
|
|
14441
|
+
continue;
|
|
14442
|
+
}
|
|
14443
|
+
if (section === "ours" && /^\|{7}(\s|$)/.test(line)) {
|
|
14444
|
+
section = "base";
|
|
14445
|
+
continue;
|
|
14446
|
+
}
|
|
14447
|
+
if ((section === "ours" || section === "base") && /^={7}$/.test(line.trimEnd())) {
|
|
14448
|
+
section = "theirs";
|
|
14449
|
+
continue;
|
|
14450
|
+
}
|
|
14451
|
+
if (section === "theirs" && /^>{7}(\s|$)/.test(line)) {
|
|
14452
|
+
section = "both";
|
|
14453
|
+
continue;
|
|
14454
|
+
}
|
|
14455
|
+
if (section === "both") {
|
|
14456
|
+
ours.push(line);
|
|
14457
|
+
theirs.push(line);
|
|
14458
|
+
}
|
|
14459
|
+
else if (section === "ours")
|
|
14460
|
+
ours.push(line);
|
|
14461
|
+
else if (section === "theirs")
|
|
14462
|
+
theirs.push(line);
|
|
14463
|
+
}
|
|
14464
|
+
if (!conflicts || section !== "both")
|
|
14465
|
+
return null;
|
|
14466
|
+
return { ours: ours.join("\n"), theirs: theirs.join("\n") };
|
|
14467
|
+
}
|
|
14468
|
+
function packetRecency(packet) {
|
|
14469
|
+
return String(packet.updated_at ?? packet.created_at ?? "");
|
|
14470
|
+
}
|
|
14471
|
+
// Auto-resolve a merge-conflicted packet by keeping the newest side — but only
|
|
14472
|
+
// when that side parses as JSON. Anything less certain stays a removal.
|
|
14473
|
+
function resolveConflictedPacket(content) {
|
|
14474
|
+
const sides = splitConflictSides(content);
|
|
14475
|
+
if (!sides)
|
|
14476
|
+
return null;
|
|
14477
|
+
const candidates = [];
|
|
14478
|
+
for (const side of [sides.ours, sides.theirs]) {
|
|
14479
|
+
try {
|
|
14480
|
+
const parsed = JSON.parse(side);
|
|
14481
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
14482
|
+
candidates.push(parsed);
|
|
14483
|
+
}
|
|
14484
|
+
catch {
|
|
14485
|
+
// This side does not parse; the other side may still win.
|
|
14486
|
+
}
|
|
14487
|
+
}
|
|
14488
|
+
if (!candidates.length)
|
|
14489
|
+
return null;
|
|
14490
|
+
candidates.sort((a, b) => packetRecency(b).localeCompare(packetRecency(a)));
|
|
14491
|
+
return candidates[0];
|
|
14492
|
+
}
|
|
14493
|
+
function repairProject(projectDir, options = {}) {
|
|
14494
|
+
ensureMemoryDirs(projectDir);
|
|
14495
|
+
const actions = [];
|
|
14496
|
+
const removedPackets = [];
|
|
14497
|
+
let packetsTouched = false;
|
|
14498
|
+
// 1. Unparseable packet JSON (merge conflicts, torn writes, hand edits).
|
|
14499
|
+
// Always back up the broken original before changing anything.
|
|
14500
|
+
let brokenFound = 0;
|
|
14501
|
+
const packetDirs = [
|
|
14502
|
+
[packetsDir(projectDir), "packets"],
|
|
14503
|
+
[pendingDir(projectDir), "pending"],
|
|
14504
|
+
[publicCandidatesDir(projectDir), "public-candidates"],
|
|
14505
|
+
];
|
|
14506
|
+
for (const [dir, label] of packetDirs) {
|
|
14507
|
+
for (const path of walkFiles(dir, (candidate) => candidate.endsWith(".json"))) {
|
|
14508
|
+
const target = `${label}/${(0, node_path_1.basename)(path)}`;
|
|
14509
|
+
let raw;
|
|
14510
|
+
try {
|
|
14511
|
+
raw = (0, node_fs_1.readFileSync)(path, "utf8");
|
|
14512
|
+
}
|
|
14513
|
+
catch (error) {
|
|
14514
|
+
actions.push({ area: "packets", target, status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
14515
|
+
continue;
|
|
14516
|
+
}
|
|
14517
|
+
try {
|
|
14518
|
+
JSON.parse(raw);
|
|
14519
|
+
continue; // healthy packet
|
|
14520
|
+
}
|
|
14521
|
+
catch {
|
|
14522
|
+
// fall through to repair
|
|
14523
|
+
}
|
|
14524
|
+
brokenFound += 1;
|
|
14525
|
+
try {
|
|
14526
|
+
ensureDir(repairBackupDir(projectDir));
|
|
14527
|
+
const backupPath = (0, node_path_1.join)(repairBackupDir(projectDir), `${(0, node_path_1.basename)(path)}.broken`);
|
|
14528
|
+
(0, node_fs_1.writeFileSync)(backupPath, raw, "utf8");
|
|
14529
|
+
const resolved = resolveConflictedPacket(raw);
|
|
14530
|
+
if (resolved) {
|
|
14531
|
+
writeJson(path, resolved);
|
|
14532
|
+
packetsTouched = true;
|
|
14533
|
+
actions.push({
|
|
14534
|
+
area: "packets",
|
|
14535
|
+
target,
|
|
14536
|
+
status: "fixed",
|
|
14537
|
+
detail: `merge conflict auto-resolved, kept newest side — original saved to ${(0, node_path_1.relative)(projectDir, backupPath)}`,
|
|
14538
|
+
});
|
|
14539
|
+
}
|
|
14540
|
+
else {
|
|
14541
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
14542
|
+
packetsTouched = true;
|
|
14543
|
+
removedPackets.push(target);
|
|
14544
|
+
actions.push({
|
|
14545
|
+
area: "packets",
|
|
14546
|
+
target,
|
|
14547
|
+
status: "fixed",
|
|
14548
|
+
detail: `REMOVED unparseable packet — original preserved at ${(0, node_path_1.relative)(projectDir, backupPath)}; restore it by hand if it mattered`,
|
|
14549
|
+
});
|
|
14550
|
+
}
|
|
14551
|
+
}
|
|
14552
|
+
catch (error) {
|
|
14553
|
+
actions.push({ area: "packets", target, status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
14554
|
+
}
|
|
14555
|
+
}
|
|
14556
|
+
}
|
|
14557
|
+
if (!brokenFound) {
|
|
14558
|
+
actions.push({ area: "packets", target: "memory packets", status: "skipped", detail: "all packet files parse cleanly" });
|
|
14559
|
+
}
|
|
14560
|
+
// 2. Stale lock/temp files left behind by crashed writers, plus a daemon
|
|
14561
|
+
// status file whose pid is no longer running.
|
|
14562
|
+
let lockFindings = 0;
|
|
14563
|
+
for (const path of walkFiles(memoryRoot(projectDir), (candidate) => candidate.endsWith(".tmp") || candidate.endsWith(".lock"))) {
|
|
14564
|
+
lockFindings += 1;
|
|
14565
|
+
try {
|
|
14566
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
14567
|
+
actions.push({ area: "locks", target: (0, node_path_1.relative)(projectDir, path), status: "fixed", detail: "removed leftover temp/lock file" });
|
|
14568
|
+
}
|
|
14569
|
+
catch (error) {
|
|
14570
|
+
actions.push({ area: "locks", target: (0, node_path_1.relative)(projectDir, path), status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
14571
|
+
}
|
|
14572
|
+
}
|
|
14573
|
+
const daemonStatusPath = (0, node_path_1.join)(daemonDir(projectDir), "status.json");
|
|
14574
|
+
if ((0, node_fs_1.existsSync)(daemonStatusPath)) {
|
|
14575
|
+
let stale = true;
|
|
14576
|
+
let pidLabel = "unknown";
|
|
14577
|
+
try {
|
|
14578
|
+
const status = readJson(daemonStatusPath);
|
|
14579
|
+
if (typeof status.pid === "number") {
|
|
14580
|
+
pidLabel = String(status.pid);
|
|
14581
|
+
try {
|
|
14582
|
+
process.kill(status.pid, 0);
|
|
14583
|
+
stale = false;
|
|
14584
|
+
}
|
|
14585
|
+
catch {
|
|
14586
|
+
stale = true;
|
|
14587
|
+
}
|
|
14588
|
+
}
|
|
14589
|
+
}
|
|
14590
|
+
catch {
|
|
14591
|
+
stale = true; // unreadable status file is stale by definition
|
|
14592
|
+
}
|
|
14593
|
+
if (stale) {
|
|
14594
|
+
lockFindings += 1;
|
|
14595
|
+
try {
|
|
14596
|
+
(0, node_fs_1.unlinkSync)(daemonStatusPath);
|
|
14597
|
+
actions.push({ area: "locks", target: (0, node_path_1.relative)(projectDir, daemonStatusPath), status: "fixed", detail: `removed stale daemon status (pid ${pidLabel} is not running)` });
|
|
14598
|
+
}
|
|
14599
|
+
catch (error) {
|
|
14600
|
+
actions.push({ area: "locks", target: (0, node_path_1.relative)(projectDir, daemonStatusPath), status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
14603
|
+
else {
|
|
14604
|
+
lockFindings += 1;
|
|
14605
|
+
actions.push({ area: "locks", target: (0, node_path_1.relative)(projectDir, daemonStatusPath), status: "skipped", detail: `daemon pid ${pidLabel} is alive — left alone` });
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
if (!lockFindings) {
|
|
14609
|
+
actions.push({ area: "locks", target: "lock/temp files", status: "skipped", detail: "no stale lock or temp files" });
|
|
14610
|
+
}
|
|
14611
|
+
// 3. Missing or stale indexes — rebuild. Packet surgery above also forces a
|
|
14612
|
+
// rebuild so the catalog never disagrees with what is on disk.
|
|
14613
|
+
const expectedIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "vector-local.json", "graph.json", "code-graph.json"];
|
|
14614
|
+
const missingIndexes = expectedIndexes.filter((name) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(indexesDir(projectDir), name)));
|
|
14615
|
+
let staleCatalog = false;
|
|
14616
|
+
const catalogPath = (0, node_path_1.join)(indexesDir(projectDir), "catalog.json");
|
|
14617
|
+
if ((0, node_fs_1.existsSync)(catalogPath)) {
|
|
14618
|
+
try {
|
|
14619
|
+
const catalog = readJson(catalogPath);
|
|
14620
|
+
staleCatalog = catalog.packet_count !== loadPacketsFromDir(packetsDir(projectDir)).length;
|
|
14621
|
+
}
|
|
14622
|
+
catch {
|
|
14623
|
+
staleCatalog = true;
|
|
14624
|
+
}
|
|
14625
|
+
}
|
|
14626
|
+
if (missingIndexes.length || staleCatalog || packetsTouched) {
|
|
14627
|
+
try {
|
|
14628
|
+
const rebuilt = indexProject(projectDir);
|
|
14629
|
+
const reason = missingIndexes.length
|
|
14630
|
+
? `${missingIndexes.length} missing: ${missingIndexes.join(", ")}`
|
|
14631
|
+
: staleCatalog
|
|
14632
|
+
? "catalog was out of date"
|
|
14633
|
+
: "packets changed during repair";
|
|
14634
|
+
actions.push({ area: "indexes", target: "indexes + graphs", status: "fixed", detail: `rebuilt ${rebuilt.indexes.length} indexes (${reason})` });
|
|
14635
|
+
}
|
|
14636
|
+
catch (error) {
|
|
14637
|
+
actions.push({ area: "indexes", target: "indexes + graphs", status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
14638
|
+
}
|
|
14639
|
+
}
|
|
14640
|
+
else {
|
|
14641
|
+
actions.push({ area: "indexes", target: "indexes + graphs", status: "skipped", detail: "present and current" });
|
|
14642
|
+
}
|
|
14643
|
+
// 4. Agent wiring drift — re-run the write path ONLY for agents that are
|
|
14644
|
+
// already configured (config file exists) but whose hook scripts went
|
|
14645
|
+
// missing. Repair never wires new agents.
|
|
14646
|
+
try {
|
|
14647
|
+
const doctor = setupDoctor(projectDir, { homeDir: options.homeDir, serverPath: options.serverPath });
|
|
14648
|
+
let drifted = 0;
|
|
14649
|
+
for (const item of doctor) {
|
|
14650
|
+
// "Already configured" means the agent's config file exists AND already
|
|
14651
|
+
// mentions the Kage MCP server. A bare config (every Claude Code user
|
|
14652
|
+
// has ~/.claude.json) is NOT configured — repair never wires new agents.
|
|
14653
|
+
const configured = Boolean(item.config_path && (0, node_fs_1.existsSync)(item.config_path)) && configMentionsKage(item.config_path);
|
|
14654
|
+
if (!configured)
|
|
14655
|
+
continue;
|
|
14656
|
+
if (!item.hook_summary || item.hook_summary.ready)
|
|
14657
|
+
continue;
|
|
14658
|
+
drifted += 1;
|
|
14659
|
+
try {
|
|
14660
|
+
const rewired = setupAgent(item.agent, projectDir, { write: true, homeDir: options.homeDir, serverPath: options.serverPath });
|
|
14661
|
+
actions.push({
|
|
14662
|
+
area: "agents",
|
|
14663
|
+
target: item.agent,
|
|
14664
|
+
status: rewired.wrote ? "fixed" : "failed",
|
|
14665
|
+
detail: rewired.wrote
|
|
14666
|
+
? `re-ran setup, restored missing hooks (${item.hook_summary.missing.join(", ")})`
|
|
14667
|
+
: `setup did not write — run: kage setup ${item.agent} --project . --write`,
|
|
14668
|
+
});
|
|
14669
|
+
}
|
|
14670
|
+
catch (error) {
|
|
14671
|
+
actions.push({ area: "agents", target: item.agent, status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
14672
|
+
}
|
|
14673
|
+
}
|
|
14674
|
+
if (!drifted) {
|
|
14675
|
+
actions.push({ area: "agents", target: "agent wiring", status: "skipped", detail: "configured agents look intact" });
|
|
14676
|
+
}
|
|
14677
|
+
}
|
|
14678
|
+
catch (error) {
|
|
14679
|
+
actions.push({ area: "agents", target: "agent wiring", status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
14680
|
+
}
|
|
14681
|
+
const validation = validateProject(projectDir);
|
|
14682
|
+
const fixed = actions.filter((action) => action.status === "fixed").length;
|
|
14683
|
+
const skipped = actions.filter((action) => action.status === "skipped").length;
|
|
14684
|
+
const failed = actions.filter((action) => action.status === "failed").length;
|
|
14685
|
+
return { project_dir: projectDir, ok: failed === 0, actions, fixed, skipped, failed, removed_packets: removedPackets, validation };
|
|
14686
|
+
}
|
|
14687
|
+
// Map a CLI failure to ONE copy-pasteable next command. Pure on purpose:
|
|
14688
|
+
// remediation must be unit-testable without throwing real errors.
|
|
14689
|
+
function remediationFor(error) {
|
|
14690
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
14691
|
+
if (/ENOENT/i.test(text) && /\.agent_memory/.test(text))
|
|
14692
|
+
return "kage init --project .";
|
|
14693
|
+
if (/Unexpected token|Unexpected end of JSON|is not valid JSON|JSON\.parse|in JSON at position/i.test(text))
|
|
14694
|
+
return "kage repair --project .";
|
|
14695
|
+
if (/\bindex(es)?\b|\bgraph\b/i.test(text))
|
|
14696
|
+
return "kage index --project .";
|
|
14697
|
+
return "kage doctor --project .";
|
|
14698
|
+
}
|
|
14237
14699
|
function approvePending(projectDir, id) {
|
|
14238
14700
|
const pendingFiles = walkFiles(pendingDir(projectDir), (path) => path.endsWith(".json"));
|
|
14239
14701
|
for (const path of pendingFiles) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"node": ">=18"
|
|
50
50
|
},
|
|
51
51
|
"mcpName": "com.kage-core/kage"
|
|
52
|
-
}
|
|
52
|
+
}
|
package/viewer/console.js
CHANGED
|
@@ -780,4 +780,65 @@
|
|
|
780
780
|
fitView();
|
|
781
781
|
cancelAnimationFrame(G.raf); tick();
|
|
782
782
|
}
|
|
783
|
+
|
|
784
|
+
// ---- live feed (SSE from the local viewer daemon) ----
|
|
785
|
+
// Connects to /kage/events when this page is served by `kage viewer`. The
|
|
786
|
+
// hosted demo on GitHub Pages has no daemon: the stream never opens, so the
|
|
787
|
+
// panel stays hidden and the dashboard is otherwise unchanged.
|
|
788
|
+
var LIVE_CAP = 30;
|
|
789
|
+
var LIVE_LABEL = {
|
|
790
|
+
packet_written: ["+", "Memory written"],
|
|
791
|
+
packet_updated: ["✎", "Memory updated"],
|
|
792
|
+
recall_served: ["✓", "Recall served"],
|
|
793
|
+
stale_withheld: ["⊘", "Stale memory withheld"],
|
|
794
|
+
caller_answered: ["◆", "Graph answer served"],
|
|
795
|
+
stale_caught: ["⊘", "Stale memory caught"],
|
|
796
|
+
};
|
|
797
|
+
function liveRow(e) {
|
|
798
|
+
var kind = e.type === "value_event" ? ((e.event && e.event.kind) || "value_event") : e.type;
|
|
799
|
+
var meta = LIVE_LABEL[kind] || ["•", String(kind).replace(/_/g, " ")];
|
|
800
|
+
var row = el("div", "vev fresh " + kind);
|
|
801
|
+
row.appendChild(el("span", "vi", meta[0]));
|
|
802
|
+
var mid = el("div", "vt");
|
|
803
|
+
mid.appendChild(document.createTextNode(meta[1]));
|
|
804
|
+
if (kind === "recall_served" && e.event && e.event.tokens_saved > 0) {
|
|
805
|
+
mid.appendChild(document.createTextNode(" — saved "));
|
|
806
|
+
mid.appendChild(el("b", null, "~" + fmt(e.event.tokens_saved) + " tokens"));
|
|
807
|
+
} else if (e.title) {
|
|
808
|
+
mid.appendChild(document.createTextNode(" — "));
|
|
809
|
+
mid.appendChild(el("b", null, e.title));
|
|
810
|
+
}
|
|
811
|
+
row.appendChild(mid);
|
|
812
|
+
row.appendChild(el("span", "when", relTime(e.ts) || "just now"));
|
|
813
|
+
return row;
|
|
814
|
+
}
|
|
815
|
+
function initLive() {
|
|
816
|
+
if (typeof window.EventSource === "undefined") return;
|
|
817
|
+
var panel = document.getElementById("livePanel"), feed = document.getElementById("liveFeed");
|
|
818
|
+
if (!panel || !feed) return;
|
|
819
|
+
var es;
|
|
820
|
+
try { es = new EventSource("/kage/events"); } catch (err) { return; }
|
|
821
|
+
var opened = false;
|
|
822
|
+
es.onopen = function () {
|
|
823
|
+
opened = true;
|
|
824
|
+
panel.hidden = false;
|
|
825
|
+
if (!feed.childNodes.length) feed.appendChild(el("div", "empty", "Connected. New memories and value events stream in here live."));
|
|
826
|
+
};
|
|
827
|
+
es.onerror = function () {
|
|
828
|
+
// Never connected (no daemon behind this page): close and hide quietly.
|
|
829
|
+
// After a successful open, EventSource reconnects on its own — keep the panel.
|
|
830
|
+
if (!opened) { es.close(); panel.hidden = true; }
|
|
831
|
+
};
|
|
832
|
+
es.onmessage = function (msg) {
|
|
833
|
+
var e;
|
|
834
|
+
try { e = JSON.parse(msg.data); } catch (err) { return; }
|
|
835
|
+
if (!e || !e.type) return;
|
|
836
|
+
var placeholder = feed.querySelector(".empty");
|
|
837
|
+
if (placeholder) feed.removeChild(placeholder);
|
|
838
|
+
feed.className = "vfeed";
|
|
839
|
+
feed.insertBefore(liveRow(e), feed.firstChild);
|
|
840
|
+
while (feed.childNodes.length > LIVE_CAP) feed.removeChild(feed.lastChild);
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
initLive();
|
|
783
844
|
})();
|
package/viewer/index.html
CHANGED
|
@@ -95,6 +95,14 @@
|
|
|
95
95
|
.receipt .r-line b.warn { color: var(--warn); } .receipt .r-line b.gain { color: var(--gain); }
|
|
96
96
|
.receipt .r-foot { border-top: 1px dashed var(--line-strong); padding: 11px 24px; color: var(--faint); font: 400 11px/1.5 var(--mono); }
|
|
97
97
|
|
|
98
|
+
/* live feed (SSE from the local daemon; hidden on the hosted demo) */
|
|
99
|
+
.livedot { display: inline-block; width: 8px; height: 8px; border-radius: 99px; background: var(--gain); margin-right: 9px; animation: livepulse 2s ease infinite; }
|
|
100
|
+
@keyframes livepulse { 0%, 100% { box-shadow: 0 0 0 0 var(--gain-soft); } 50% { box-shadow: 0 0 0 6px var(--gain-soft); } }
|
|
101
|
+
.vev.fresh { animation: levin 1.2s ease; }
|
|
102
|
+
@keyframes levin { from { background: var(--gain-soft); } to { background: transparent; } }
|
|
103
|
+
.vev.packet_written .vi, .vev.packet_updated .vi { color: var(--memory); }
|
|
104
|
+
.vev.stale_caught .vi { color: var(--warn); }
|
|
105
|
+
|
|
98
106
|
/* gains timeline */
|
|
99
107
|
.vfeed { display: grid; }
|
|
100
108
|
.vev { display: grid; grid-template-columns: 20px 1fr auto; gap: 0 13px; align-items: baseline; padding: 11px 4px; border-top: 1px solid var(--line); }
|
|
@@ -298,6 +306,7 @@
|
|
|
298
306
|
<section class="section active" id="section-gains">
|
|
299
307
|
<div class="receipt" id="gainsHero"></div>
|
|
300
308
|
<div class="tiles" id="gainsTiles"></div>
|
|
309
|
+
<div class="panel" id="livePanel" hidden><h2><span class="livedot" aria-hidden="true"></span>Live <span class="sub">— memories and value events as they happen</span></h2><div id="liveFeed"></div></div>
|
|
301
310
|
<div class="panel"><h2>Value timeline <span class="sub">— every saved recall and withheld stale memory, as it happened</span></h2><div id="gainsTimeline"></div></div>
|
|
302
311
|
</section>
|
|
303
312
|
|
|
@@ -361,6 +370,6 @@
|
|
|
361
370
|
</div>
|
|
362
371
|
<div class="drawer-backdrop" id="detailBackdrop"></div>
|
|
363
372
|
<aside class="drawer" id="detail" aria-hidden="true"></aside>
|
|
364
|
-
<script src="./console.js?v=
|
|
373
|
+
<script src="./console.js?v=17"></script>
|
|
365
374
|
</body>
|
|
366
375
|
</html>
|