laminark 0.1.0 → 2.21.7
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/.claude-plugin/marketplace.json +15 -0
- package/README.md +71 -36
- package/package.json +7 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/dist/hooks/handler.d.ts +1 -3
- package/plugin/dist/hooks/handler.d.ts.map +1 -1
- package/plugin/dist/hooks/handler.js +22 -310
- package/plugin/dist/hooks/handler.js.map +1 -1
- package/plugin/dist/index.d.ts +1 -3
- package/plugin/dist/index.d.ts.map +1 -1
- package/plugin/dist/index.js +392 -1895
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/{observations-CorAAc1A.d.mts → observations-Ch0nc47i.d.mts} +1 -23
- package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
- package/plugin/dist/{tool-registry-e710BvXq.mjs → tool-registry-CZ3mJ4iR.mjs} +13 -932
- package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
- package/plugin/hooks/hooks.json +6 -6
- package/plugin/scripts/README.md +1 -19
- package/plugin/scripts/bump-version.sh +3 -1
- package/plugin/scripts/ensure-deps.sh +2 -5
- package/plugin/scripts/install.sh +39 -115
- package/plugin/scripts/local-install.sh +58 -93
- package/plugin/scripts/setup-tmpdir.sh +65 -0
- package/plugin/scripts/uninstall.sh +38 -76
- package/plugin/scripts/update.sh +69 -20
- package/plugin/scripts/verify-install.sh +25 -69
- package/plugin/ui/activity.js +0 -12
- package/plugin/ui/app.js +54 -24
- package/plugin/ui/graph.js +186 -413
- package/plugin/ui/help.js +172 -876
- package/plugin/ui/index.html +242 -506
- package/plugin/ui/settings.js +17 -781
- package/plugin/ui/styles.css +44 -990
- package/plugin/ui/timeline.js +2 -2
- package/plugin/CLAUDE.md +0 -10
- package/plugin/commands/recall.md +0 -55
- package/plugin/commands/remember.md +0 -34
- package/plugin/commands/resume.md +0 -45
- package/plugin/commands/stash.md +0 -34
- package/plugin/commands/status.md +0 -33
- package/plugin/dist/observations-CorAAc1A.d.mts.map +0 -1
- package/plugin/dist/tool-registry-e710BvXq.mjs.map +0 -1
- package/plugin/laminark.db +0 -0
- package/plugin/package.json +0 -17
- package/plugin/scripts/dev-sync.sh +0 -58
- package/plugin/ui/help/activity-feed.png +0 -0
- package/plugin/ui/help/analysis-panel.png +0 -0
- package/plugin/ui/help/graph-toolbar.png +0 -0
- package/plugin/ui/help/graph-view.png +0 -0
- package/plugin/ui/help/settings.png +0 -0
- package/plugin/ui/help/timeline.png +0 -0
- package/plugin/ui/tools.js +0 -826
package/plugin/dist/index.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as isDebugEnabled, i as getProjectHash, n as getDatabaseConfig, r as getDbPath, t as getConfigDir } from "./config-t8LZeB-u.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readFileSync,
|
|
5
|
-
import {
|
|
3
|
+
import { C as rowToObservation, D as debug, E as runMigrations, O as debugTimed, S as ObservationRepository, T as MIGRATIONS, _ as SaveGuard, a as ResearchBufferRepository, b as SearchEngine, c as inferToolType, d as getNodeByNameAndType, f as getNodesByType, g as upsertNode, h as traverseFrom, i as NotificationStore, l as countEdgesForNode, m as insertEdge, n as PathRepository, o as extractServerName, p as initGraphSchema, r as initPathSchema, s as inferScope, t as ToolRegistryRepository, u as getEdgesForNode, v as jaccardSimilarity$1, w as openDatabase, x as SessionRepository, y as hybridSearch } from "./tool-registry-CZ3mJ4iR.mjs";
|
|
4
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
13
|
-
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
|
|
14
12
|
import { Worker } from "node:worker_threads";
|
|
15
13
|
import { fileURLToPath as fileURLToPath$1 } from "node:url";
|
|
14
|
+
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
|
|
16
15
|
import { Hono } from "hono";
|
|
17
16
|
import fs from "fs";
|
|
18
17
|
import { cors } from "hono/cors";
|
|
@@ -372,44 +371,6 @@ async function startServer(server) {
|
|
|
372
371
|
debug("mcp", "MCP server started on stdio transport");
|
|
373
372
|
}
|
|
374
373
|
|
|
375
|
-
//#endregion
|
|
376
|
-
//#region src/config/cross-access.ts
|
|
377
|
-
/**
|
|
378
|
-
* Cross-Project Access Configuration
|
|
379
|
-
*
|
|
380
|
-
* Per-project config that controls which other projects' memories
|
|
381
|
-
* the current project can read from. Read-only access — no writes
|
|
382
|
-
* cross projects.
|
|
383
|
-
*
|
|
384
|
-
* Config stored at: {configDir}/cross-access-{projectHash}.json
|
|
385
|
-
*/
|
|
386
|
-
const DEFAULTS$4 = { readableProjects: [] };
|
|
387
|
-
function getConfigPath(projectHash) {
|
|
388
|
-
return join(getConfigDir(), `cross-access-${projectHash}.json`);
|
|
389
|
-
}
|
|
390
|
-
function loadCrossAccessConfig(projectHash) {
|
|
391
|
-
const configPath = getConfigPath(projectHash);
|
|
392
|
-
try {
|
|
393
|
-
if (!existsSync(configPath)) return { ...DEFAULTS$4 };
|
|
394
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
395
|
-
const parsed = JSON.parse(raw);
|
|
396
|
-
return { readableProjects: Array.isArray(parsed.readableProjects) ? parsed.readableProjects.filter((h) => typeof h === "string") : [] };
|
|
397
|
-
} catch {
|
|
398
|
-
return { ...DEFAULTS$4 };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
function saveCrossAccessConfig(projectHash, config) {
|
|
402
|
-
const configPath = getConfigPath(projectHash);
|
|
403
|
-
const validated = { readableProjects: Array.isArray(config.readableProjects) ? config.readableProjects.filter((h) => typeof h === "string" && h !== projectHash) : [] };
|
|
404
|
-
writeFileSync(configPath, JSON.stringify(validated, null, 2), "utf-8");
|
|
405
|
-
}
|
|
406
|
-
function resetCrossAccessConfig(projectHash) {
|
|
407
|
-
const configPath = getConfigPath(projectHash);
|
|
408
|
-
try {
|
|
409
|
-
if (existsSync(configPath)) unlinkSync(configPath);
|
|
410
|
-
} catch {}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
374
|
//#endregion
|
|
414
375
|
//#region src/mcp/token-budget.ts
|
|
415
376
|
const TOKEN_BUDGET = 2e3;
|
|
@@ -438,82 +399,6 @@ function enforceTokenBudget(results, formatResult, budget = TOKEN_BUDGET) {
|
|
|
438
399
|
};
|
|
439
400
|
}
|
|
440
401
|
|
|
441
|
-
//#endregion
|
|
442
|
-
//#region src/config/tool-verbosity-config.ts
|
|
443
|
-
/**
|
|
444
|
-
* Tool Response Verbosity Configuration
|
|
445
|
-
*
|
|
446
|
-
* Controls how much detail MCP tool responses include.
|
|
447
|
-
* Three levels:
|
|
448
|
-
* 1 (minimal): Just confirms the tool ran
|
|
449
|
-
* 2 (standard): Shows title/key info (default)
|
|
450
|
-
* 3 (verbose): Full formatted text with all details
|
|
451
|
-
*
|
|
452
|
-
* Configuration is loaded from .laminark/tool-verbosity.json with
|
|
453
|
-
* a 5-second cache to avoid repeated disk reads.
|
|
454
|
-
*/
|
|
455
|
-
const DEFAULTS$3 = { level: 2 };
|
|
456
|
-
const CACHE_TTL_MS = 5e3;
|
|
457
|
-
let cachedConfig = null;
|
|
458
|
-
let cachedAt = 0;
|
|
459
|
-
/**
|
|
460
|
-
* Loads tool verbosity configuration from disk with a 5-second cache.
|
|
461
|
-
*/
|
|
462
|
-
function loadToolVerbosityConfig() {
|
|
463
|
-
const now = Date.now();
|
|
464
|
-
if (cachedConfig && now - cachedAt < CACHE_TTL_MS) return cachedConfig;
|
|
465
|
-
const configPath = join(getConfigDir(), "tool-verbosity.json");
|
|
466
|
-
try {
|
|
467
|
-
const content = readFileSync(configPath, "utf-8");
|
|
468
|
-
const level = JSON.parse(content).level;
|
|
469
|
-
if (level === 1 || level === 2 || level === 3) cachedConfig = { level };
|
|
470
|
-
else cachedConfig = { ...DEFAULTS$3 };
|
|
471
|
-
debug("config", "Loaded tool verbosity config", { level: cachedConfig.level });
|
|
472
|
-
} catch {
|
|
473
|
-
cachedConfig = { ...DEFAULTS$3 };
|
|
474
|
-
}
|
|
475
|
-
cachedAt = now;
|
|
476
|
-
return cachedConfig;
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Saves tool verbosity configuration to disk and invalidates cache.
|
|
480
|
-
*/
|
|
481
|
-
function saveToolVerbosityConfig(config) {
|
|
482
|
-
writeFileSync(join(getConfigDir(), "tool-verbosity.json"), JSON.stringify(config, null, 2), "utf-8");
|
|
483
|
-
cachedConfig = config;
|
|
484
|
-
cachedAt = Date.now();
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Resets tool verbosity to defaults by invalidating cache.
|
|
488
|
-
*/
|
|
489
|
-
function resetToolVerbosityConfig() {
|
|
490
|
-
cachedConfig = null;
|
|
491
|
-
cachedAt = 0;
|
|
492
|
-
return { ...DEFAULTS$3 };
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Selects the appropriate response text based on the current verbosity level.
|
|
496
|
-
*
|
|
497
|
-
* Each tool passes three pre-built strings:
|
|
498
|
-
* - minimal: Level 1 — just confirms the tool ran
|
|
499
|
-
* - standard: Level 2 — shows title/key info
|
|
500
|
-
* - verbose: Level 3 — full formatted text
|
|
501
|
-
*/
|
|
502
|
-
function formatResponse(level, minimal, standard, verbose) {
|
|
503
|
-
switch (level) {
|
|
504
|
-
case 1: return minimal;
|
|
505
|
-
case 2: return standard;
|
|
506
|
-
case 3: return verbose;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Convenience: loads config and selects the response in one call.
|
|
511
|
-
*/
|
|
512
|
-
function verboseResponse(minimal, standard, verbose) {
|
|
513
|
-
const { level } = loadToolVerbosityConfig();
|
|
514
|
-
return formatResponse(level, minimal, standard, verbose);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
402
|
//#endregion
|
|
518
403
|
//#region src/mcp/tools/recall.ts
|
|
519
404
|
function shortId(id) {
|
|
@@ -545,19 +430,19 @@ function formatTimelineGroup(date, items) {
|
|
|
545
430
|
function formatFullItem(obs) {
|
|
546
431
|
return `--- ${shortId(obs.id)} | ${obs.title ?? "untitled"} | ${obs.createdAt} ---\n${obs.content}`;
|
|
547
432
|
}
|
|
548
|
-
function prependNotifications$
|
|
433
|
+
function prependNotifications$6(notificationStore, projectHash, responseText) {
|
|
549
434
|
if (!notificationStore) return responseText;
|
|
550
435
|
const pending = notificationStore.consumePending(projectHash);
|
|
551
436
|
if (pending.length === 0) return responseText;
|
|
552
437
|
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
553
438
|
}
|
|
554
|
-
function textResponse$
|
|
439
|
+
function textResponse$7(text) {
|
|
555
440
|
return { content: [{
|
|
556
441
|
type: "text",
|
|
557
442
|
text
|
|
558
443
|
}] };
|
|
559
444
|
}
|
|
560
|
-
function errorResponse$
|
|
445
|
+
function errorResponse$3(text) {
|
|
561
446
|
return {
|
|
562
447
|
content: [{
|
|
563
448
|
type: "text",
|
|
@@ -566,15 +451,7 @@ function errorResponse$4(text) {
|
|
|
566
451
|
isError: true
|
|
567
452
|
};
|
|
568
453
|
}
|
|
569
|
-
function
|
|
570
|
-
const map = /* @__PURE__ */ new Map();
|
|
571
|
-
try {
|
|
572
|
-
const rows = db.prepare("SELECT project_hash, display_name FROM project_metadata").all();
|
|
573
|
-
for (const row of rows) map.set(row.project_hash, row.display_name ?? row.project_hash.slice(0, 8));
|
|
574
|
-
} catch {}
|
|
575
|
-
return map;
|
|
576
|
-
}
|
|
577
|
-
function registerRecall(server, db, projectHashRef, worker = null, embeddingStore = null, notificationStore = null, statusCache = null) {
|
|
454
|
+
function registerRecall(server, db, projectHash, worker = null, embeddingStore = null, notificationStore = null, statusCache = null) {
|
|
578
455
|
server.registerTool("recall", {
|
|
579
456
|
title: "Recall Memories",
|
|
580
457
|
description: "Search, view, purge, or restore memories. Search first to find matches, then act on specific results by ID.",
|
|
@@ -604,14 +481,13 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
|
|
|
604
481
|
include_purged: z.boolean().default(false).describe("Include soft-deleted items in results (needed for restore)")
|
|
605
482
|
}
|
|
606
483
|
}, async (args) => {
|
|
607
|
-
const
|
|
608
|
-
const withNotifications = (text) => textResponse$9(prependNotifications$8(notificationStore, projectHash, text));
|
|
484
|
+
const withNotifications = (text) => textResponse$7(prependNotifications$6(notificationStore, projectHash, text));
|
|
609
485
|
try {
|
|
610
486
|
const repo = new ObservationRepository(db, projectHash);
|
|
611
487
|
const searchEngine = new SearchEngine(db, projectHash);
|
|
612
488
|
const hasSearch = args.query !== void 0 || args.id !== void 0 || args.title !== void 0;
|
|
613
|
-
if (args.ids && hasSearch) return errorResponse$
|
|
614
|
-
if ((args.action === "purge" || args.action === "restore") && !args.ids && !args.id) return errorResponse$
|
|
489
|
+
if (args.ids && hasSearch) return errorResponse$3("Provide either a search query or IDs to act on, not both.");
|
|
490
|
+
if ((args.action === "purge" || args.action === "restore") && !args.ids && !args.id) return errorResponse$3(`Provide ids array or id to specify which memories to ${args.action}.`);
|
|
615
491
|
let observations = [];
|
|
616
492
|
let searchResults = null;
|
|
617
493
|
if (args.ids) {
|
|
@@ -638,30 +514,6 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
|
|
|
638
514
|
});
|
|
639
515
|
else searchResults = searchEngine.searchKeyword(args.query, { limit: args.limit });
|
|
640
516
|
observations = searchResults.map((r) => r.observation);
|
|
641
|
-
const crossConfig = loadCrossAccessConfig(projectHash);
|
|
642
|
-
if (crossConfig.readableProjects.length > 0) {
|
|
643
|
-
const nameMap = getProjectNameMap(db);
|
|
644
|
-
for (const otherHash of crossConfig.readableProjects) {
|
|
645
|
-
const otherEngine = new SearchEngine(db, otherHash);
|
|
646
|
-
let otherResults;
|
|
647
|
-
if (embeddingStore) otherResults = await hybridSearch({
|
|
648
|
-
searchEngine: otherEngine,
|
|
649
|
-
embeddingStore,
|
|
650
|
-
worker,
|
|
651
|
-
query: args.query,
|
|
652
|
-
db,
|
|
653
|
-
projectHash: otherHash,
|
|
654
|
-
options: { limit: args.limit }
|
|
655
|
-
});
|
|
656
|
-
else otherResults = otherEngine.searchKeyword(args.query, { limit: args.limit });
|
|
657
|
-
if (otherResults.length > 0) {
|
|
658
|
-
const projName = nameMap.get(otherHash) ?? otherHash.slice(0, 8);
|
|
659
|
-
for (const r of otherResults) r.observation.title = `[${projName}] ${r.observation.title ?? "untitled"}`;
|
|
660
|
-
searchResults.push(...otherResults);
|
|
661
|
-
observations.push(...otherResults.map((r) => r.observation));
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
517
|
} else if (args.title) observations = repo.getByTitle(args.title, {
|
|
666
518
|
limit: args.limit,
|
|
667
519
|
includePurged: args.include_purged
|
|
@@ -673,21 +525,8 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
|
|
|
673
525
|
if (args.kind && observations.length > 0) observations = observations.filter((obs) => obs.kind === args.kind);
|
|
674
526
|
if (observations.length === 0) return withNotifications(`No memories found matching '${args.query ?? args.title ?? args.id ?? ""}'. Try broader search terms or check the ID.`);
|
|
675
527
|
if (args.action === "view") {
|
|
676
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
677
|
-
if (verbosity === 1) {
|
|
678
|
-
const searchTerm = args.query ?? args.title ?? "query";
|
|
679
|
-
return textResponse$9(prependNotifications$8(notificationStore, projectHash, `Found ${observations.length} memories matching "${searchTerm}"`));
|
|
680
|
-
}
|
|
681
|
-
if (verbosity === 2) {
|
|
682
|
-
const lines = observations.map((obs, i) => {
|
|
683
|
-
const title = obs.title ?? "untitled";
|
|
684
|
-
return `${i + 1}. ${title}`;
|
|
685
|
-
});
|
|
686
|
-
const footer = `\n---\n${observations.length} result(s)`;
|
|
687
|
-
return textResponse$9(prependNotifications$8(notificationStore, projectHash, lines.join("\n") + footer));
|
|
688
|
-
}
|
|
689
528
|
const originalText = formatViewResponse(observations, searchResults, args.detail, args.id !== void 0).content[0].text;
|
|
690
|
-
return textResponse$
|
|
529
|
+
return textResponse$7(prependNotifications$6(notificationStore, projectHash, originalText));
|
|
691
530
|
}
|
|
692
531
|
if (args.action === "purge") {
|
|
693
532
|
const targetIds = args.ids ?? (args.id ? [args.id] : []);
|
|
@@ -719,11 +558,11 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
|
|
|
719
558
|
if (failures.length > 0) msg += ` Not found: ${failures.join(", ")}`;
|
|
720
559
|
return withNotifications(msg);
|
|
721
560
|
}
|
|
722
|
-
return errorResponse$
|
|
561
|
+
return errorResponse$3(`Unknown action: ${args.action}`);
|
|
723
562
|
} catch (err) {
|
|
724
563
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
725
564
|
debug("mcp", "recall: error", { error: message });
|
|
726
|
-
return errorResponse$
|
|
565
|
+
return errorResponse$3(`Recall error: ${message}`);
|
|
727
566
|
}
|
|
728
567
|
});
|
|
729
568
|
}
|
|
@@ -783,7 +622,7 @@ function formatViewResponse(observations, searchResults, detail, isSingleIdLooku
|
|
|
783
622
|
}
|
|
784
623
|
let footer = `---\n${observations.length} result(s) | ~${tokenEstimate} tokens | detail: ${detail}`;
|
|
785
624
|
if (truncated) footer += " | truncated (use id for full view)";
|
|
786
|
-
return textResponse$
|
|
625
|
+
return textResponse$7(`${body}\n${footer}`);
|
|
787
626
|
}
|
|
788
627
|
function buildScoreMap(searchResults) {
|
|
789
628
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -809,7 +648,7 @@ function generateTitle(content) {
|
|
|
809
648
|
* save_memory persists user-provided text as a new observation with an optional title.
|
|
810
649
|
* If title is omitted, one is auto-generated from the text content.
|
|
811
650
|
*/
|
|
812
|
-
function registerSaveMemory(server, db,
|
|
651
|
+
function registerSaveMemory(server, db, projectHash, notificationStore = null, worker = null, embeddingStore = null, statusCache = null) {
|
|
813
652
|
server.registerTool("save_memory", {
|
|
814
653
|
title: "Save Memory",
|
|
815
654
|
description: "Save a new memory observation. Provide text content and an optional title. If title is omitted, one is auto-generated from the text.",
|
|
@@ -826,7 +665,6 @@ function registerSaveMemory(server, db, projectHashRef, notificationStore = null
|
|
|
826
665
|
]).default("finding").describe("Observation kind: change, reference, finding, decision, or verification")
|
|
827
666
|
}
|
|
828
667
|
}, async (args) => {
|
|
829
|
-
const projectHash = projectHashRef.current;
|
|
830
668
|
try {
|
|
831
669
|
const repo = new ObservationRepository(db, projectHash);
|
|
832
670
|
const decision = await new SaveGuard(repo, {
|
|
@@ -855,7 +693,7 @@ function registerSaveMemory(server, db, projectHashRef, notificationStore = null
|
|
|
855
693
|
title: resolvedTitle
|
|
856
694
|
});
|
|
857
695
|
statusCache?.markDirty();
|
|
858
|
-
let responseText =
|
|
696
|
+
let responseText = `Saved memory "${resolvedTitle}" (id: ${obs.id})`;
|
|
859
697
|
if (notificationStore) {
|
|
860
698
|
const pending = notificationStore.consumePending(projectHash);
|
|
861
699
|
if (pending.length > 0) responseText = pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
@@ -876,248 +714,6 @@ function registerSaveMemory(server, db, projectHashRef, notificationStore = null
|
|
|
876
714
|
});
|
|
877
715
|
}
|
|
878
716
|
|
|
879
|
-
//#endregion
|
|
880
|
-
//#region src/ingestion/markdown-parser.ts
|
|
881
|
-
/**
|
|
882
|
-
* Parse a markdown file into discrete sections split on ## headings.
|
|
883
|
-
*
|
|
884
|
-
* - The # (level 1) heading is the doc title, used as prefix: "DocTitle > SectionHeading"
|
|
885
|
-
* - ### subsections stay within their parent ## section (not split separately)
|
|
886
|
-
* - Sections with empty content after trimming are skipped
|
|
887
|
-
* - Content before the first ## heading is skipped
|
|
888
|
-
* - ## inside fenced code blocks are not treated as headings
|
|
889
|
-
*/
|
|
890
|
-
function parseMarkdownSections(fileContent, sourceFile) {
|
|
891
|
-
const lines = fileContent.split("\n");
|
|
892
|
-
const sections = [];
|
|
893
|
-
let docTitle = "";
|
|
894
|
-
let currentHeading = "";
|
|
895
|
-
let currentLines = [];
|
|
896
|
-
let sectionIndex = 0;
|
|
897
|
-
let inCodeBlock = false;
|
|
898
|
-
for (const line of lines) {
|
|
899
|
-
if (line.trimStart().startsWith("```")) inCodeBlock = !inCodeBlock;
|
|
900
|
-
if (inCodeBlock) {
|
|
901
|
-
if (currentHeading) currentLines.push(line);
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
if (/^# (?!#)/.test(line)) {
|
|
905
|
-
docTitle = line.slice(2).trim();
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
if (/^## (?!#)/.test(line)) {
|
|
909
|
-
if (currentHeading) {
|
|
910
|
-
const content = currentLines.join("\n").trim();
|
|
911
|
-
if (content.length > 0) {
|
|
912
|
-
sections.push({
|
|
913
|
-
title: docTitle ? `${docTitle} > ${currentHeading}` : currentHeading,
|
|
914
|
-
heading: currentHeading,
|
|
915
|
-
content,
|
|
916
|
-
sourceFile,
|
|
917
|
-
sectionIndex
|
|
918
|
-
});
|
|
919
|
-
sectionIndex++;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
currentHeading = line.slice(3).trim();
|
|
923
|
-
currentLines = [];
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
if (currentHeading) currentLines.push(line);
|
|
927
|
-
}
|
|
928
|
-
if (currentHeading) {
|
|
929
|
-
const content = currentLines.join("\n").trim();
|
|
930
|
-
if (content.length > 0) sections.push({
|
|
931
|
-
title: docTitle ? `${docTitle} > ${currentHeading}` : currentHeading,
|
|
932
|
-
heading: currentHeading,
|
|
933
|
-
content,
|
|
934
|
-
sourceFile,
|
|
935
|
-
sectionIndex
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
return sections;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
//#endregion
|
|
942
|
-
//#region src/ingestion/knowledge-ingester.ts
|
|
943
|
-
/**
|
|
944
|
-
* Ingests markdown files into the knowledge store.
|
|
945
|
-
*
|
|
946
|
-
* Creates one observation per ## section, with idempotent re-ingestion
|
|
947
|
-
* that cleans up stale sections without duplication.
|
|
948
|
-
*/
|
|
949
|
-
var KnowledgeIngester = class {
|
|
950
|
-
db;
|
|
951
|
-
projectHash;
|
|
952
|
-
constructor(db, projectHash) {
|
|
953
|
-
this.db = db;
|
|
954
|
-
this.projectHash = projectHash;
|
|
955
|
-
}
|
|
956
|
-
/**
|
|
957
|
-
* Detects the knowledge directory for a project.
|
|
958
|
-
* Checks in order:
|
|
959
|
-
* 1. {projectRoot}/.planning/codebase/ (GSD output)
|
|
960
|
-
* 2. {projectRoot}/.laminark/codebase/
|
|
961
|
-
* Returns the first existing directory, or null if none exist.
|
|
962
|
-
*/
|
|
963
|
-
static detectKnowledgeDir(projectRoot) {
|
|
964
|
-
const candidates = [join(projectRoot, ".planning", "codebase"), join(projectRoot, ".laminark", "codebase")];
|
|
965
|
-
for (const candidate of candidates) try {
|
|
966
|
-
if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
|
|
967
|
-
} catch {}
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Ingests all markdown files from a directory.
|
|
972
|
-
* Reads all files async first, then runs DB operations in a single transaction.
|
|
973
|
-
*/
|
|
974
|
-
async ingestDirectory(dirPath) {
|
|
975
|
-
let files;
|
|
976
|
-
try {
|
|
977
|
-
files = await readdir(dirPath);
|
|
978
|
-
} catch {
|
|
979
|
-
return {
|
|
980
|
-
filesProcessed: 0,
|
|
981
|
-
sectionsCreated: 0,
|
|
982
|
-
sectionsRemoved: 0
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
986
|
-
const fileContents = /* @__PURE__ */ new Map();
|
|
987
|
-
for (const file of mdFiles) {
|
|
988
|
-
const filePath = join(dirPath, file);
|
|
989
|
-
try {
|
|
990
|
-
const content = await readFile(filePath, "utf-8");
|
|
991
|
-
fileContents.set(file, content);
|
|
992
|
-
} catch {}
|
|
993
|
-
}
|
|
994
|
-
let totalCreated = 0;
|
|
995
|
-
let totalRemoved = 0;
|
|
996
|
-
for (const [filename, content] of fileContents) {
|
|
997
|
-
const stats = this.ingestFileSync(filename, content);
|
|
998
|
-
totalCreated += stats.sectionsCreated;
|
|
999
|
-
totalRemoved += stats.sectionsRemoved;
|
|
1000
|
-
}
|
|
1001
|
-
return {
|
|
1002
|
-
filesProcessed: fileContents.size,
|
|
1003
|
-
sectionsCreated: totalCreated,
|
|
1004
|
-
sectionsRemoved: totalRemoved
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Ingests a single markdown file.
|
|
1009
|
-
* Wraps async file reading with sync ingestion.
|
|
1010
|
-
*/
|
|
1011
|
-
async ingestFile(filePath) {
|
|
1012
|
-
try {
|
|
1013
|
-
const content = await readFile(filePath, "utf-8");
|
|
1014
|
-
const filename = basename(filePath);
|
|
1015
|
-
return this.ingestFileSync(filename, content);
|
|
1016
|
-
} catch {
|
|
1017
|
-
return {
|
|
1018
|
-
filesProcessed: 0,
|
|
1019
|
-
sectionsCreated: 0,
|
|
1020
|
-
sectionsRemoved: 0
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
/**
|
|
1025
|
-
* Internal sync ingestion method (runs within transaction).
|
|
1026
|
-
* Implements idempotent upsert via soft-delete + recreate.
|
|
1027
|
-
*/
|
|
1028
|
-
ingestFileSync(filename, fileContent) {
|
|
1029
|
-
const sourceTag = `ingest:${filename}`;
|
|
1030
|
-
const sections = parseMarkdownSections(fileContent, filename);
|
|
1031
|
-
return this.db.transaction(() => {
|
|
1032
|
-
const repo = new ObservationRepository(this.db, this.projectHash);
|
|
1033
|
-
const sectionsRemoved = this.db.prepare(`UPDATE observations
|
|
1034
|
-
SET deleted_at = datetime('now'), updated_at = datetime('now')
|
|
1035
|
-
WHERE project_hash = ? AND source = ? AND deleted_at IS NULL`).run(this.projectHash, sourceTag).changes;
|
|
1036
|
-
let sectionsCreated = 0;
|
|
1037
|
-
for (const section of sections) {
|
|
1038
|
-
repo.createClassified({
|
|
1039
|
-
content: section.content,
|
|
1040
|
-
title: section.title,
|
|
1041
|
-
source: sourceTag,
|
|
1042
|
-
kind: "reference",
|
|
1043
|
-
sessionId: null
|
|
1044
|
-
}, "discovery");
|
|
1045
|
-
sectionsCreated++;
|
|
1046
|
-
}
|
|
1047
|
-
return {
|
|
1048
|
-
filesProcessed: 1,
|
|
1049
|
-
sectionsCreated,
|
|
1050
|
-
sectionsRemoved
|
|
1051
|
-
};
|
|
1052
|
-
})();
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
|
-
|
|
1056
|
-
//#endregion
|
|
1057
|
-
//#region src/mcp/tools/ingest-knowledge.ts
|
|
1058
|
-
/**
|
|
1059
|
-
* Registers the ingest_knowledge tool on the MCP server.
|
|
1060
|
-
*
|
|
1061
|
-
* ingest_knowledge transforms structured markdown documents into per-project
|
|
1062
|
-
* reference observations. Supports optional directory path; auto-detects
|
|
1063
|
-
* .planning/codebase/ or .laminark/codebase/ from project metadata when
|
|
1064
|
-
* directory is omitted.
|
|
1065
|
-
*/
|
|
1066
|
-
function registerIngestKnowledge(server, db, projectHashRef, notificationStore = null, statusCache = null) {
|
|
1067
|
-
server.registerTool("ingest_knowledge", {
|
|
1068
|
-
title: "Ingest Knowledge",
|
|
1069
|
-
description: "Ingest structured markdown documents from a directory into queryable per-project memories. Reads .md files, splits by ## headings, and stores each section as a reference observation. Supports .planning/codebase/ (GSD output) and .laminark/codebase/.",
|
|
1070
|
-
inputSchema: { directory: z.string().optional().describe("Directory containing .md files to ingest. If omitted, auto-detects .planning/codebase/ or .laminark/codebase/ using the project path from project_metadata.") }
|
|
1071
|
-
}, async (args) => {
|
|
1072
|
-
const projectHash = projectHashRef.current;
|
|
1073
|
-
try {
|
|
1074
|
-
let resolvedDir = args.directory;
|
|
1075
|
-
if (!resolvedDir) {
|
|
1076
|
-
const row = db.prepare("SELECT project_path FROM project_metadata WHERE project_hash = ? ORDER BY last_seen_at DESC LIMIT 1").get(projectHash);
|
|
1077
|
-
if (!row) return {
|
|
1078
|
-
content: [{
|
|
1079
|
-
type: "text",
|
|
1080
|
-
text: "Could not determine project path. Please provide the directory parameter explicitly."
|
|
1081
|
-
}],
|
|
1082
|
-
isError: true
|
|
1083
|
-
};
|
|
1084
|
-
const detected = KnowledgeIngester.detectKnowledgeDir(row.project_path);
|
|
1085
|
-
if (!detected) return {
|
|
1086
|
-
content: [{
|
|
1087
|
-
type: "text",
|
|
1088
|
-
text: "No knowledge directory found. Expected .planning/codebase/ or .laminark/codebase/ in the project root. Run /gsd:map-codebase first or provide a directory path."
|
|
1089
|
-
}],
|
|
1090
|
-
isError: true
|
|
1091
|
-
};
|
|
1092
|
-
resolvedDir = detected;
|
|
1093
|
-
}
|
|
1094
|
-
const stats = await new KnowledgeIngester(db, projectHash).ingestDirectory(resolvedDir);
|
|
1095
|
-
debug("mcp", "ingest_knowledge: completed", {
|
|
1096
|
-
directory: resolvedDir,
|
|
1097
|
-
stats
|
|
1098
|
-
});
|
|
1099
|
-
statusCache?.markDirty();
|
|
1100
|
-
let finalResponse = verboseResponse(`Ingested ${stats.filesProcessed} files: ${stats.sectionsCreated} sections created, ${stats.sectionsRemoved} stale sections removed.`, `Ingested ${stats.filesProcessed} file(s): ${stats.sectionsCreated} sections created, ${stats.sectionsRemoved} removed.`, `Ingested ${stats.filesProcessed} file(s) from ${resolvedDir}: ${stats.sectionsCreated} sections created, ${stats.sectionsRemoved} stale sections removed.`);
|
|
1101
|
-
if (notificationStore) {
|
|
1102
|
-
const pending = notificationStore.consumePending(projectHash);
|
|
1103
|
-
if (pending.length > 0) finalResponse = pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + finalResponse;
|
|
1104
|
-
}
|
|
1105
|
-
return { content: [{
|
|
1106
|
-
type: "text",
|
|
1107
|
-
text: finalResponse
|
|
1108
|
-
}] };
|
|
1109
|
-
} catch (err) {
|
|
1110
|
-
return {
|
|
1111
|
-
content: [{
|
|
1112
|
-
type: "text",
|
|
1113
|
-
text: `Failed to ingest knowledge: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
1114
|
-
}],
|
|
1115
|
-
isError: true
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
717
|
//#endregion
|
|
1122
718
|
//#region src/commands/resume.ts
|
|
1123
719
|
/**
|
|
@@ -1189,13 +785,13 @@ function formatStashes(stashes) {
|
|
|
1189
785
|
if (stashes.length <= 8) return formatDetail(stashes);
|
|
1190
786
|
return formatCompact(stashes);
|
|
1191
787
|
}
|
|
1192
|
-
function prependNotifications$
|
|
788
|
+
function prependNotifications$5(notificationStore, projectHash, responseText) {
|
|
1193
789
|
if (!notificationStore) return responseText;
|
|
1194
790
|
const pending = notificationStore.consumePending(projectHash);
|
|
1195
791
|
if (pending.length === 0) return responseText;
|
|
1196
792
|
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
1197
793
|
}
|
|
1198
|
-
function textResponse$
|
|
794
|
+
function textResponse$6(text) {
|
|
1199
795
|
return { content: [{
|
|
1200
796
|
type: "text",
|
|
1201
797
|
text
|
|
@@ -1207,7 +803,7 @@ function textResponse$8(text) {
|
|
|
1207
803
|
* Shows recently stashed context threads. Used when the user asks
|
|
1208
804
|
* "where was I?" or wants to see abandoned conversation threads.
|
|
1209
805
|
*/
|
|
1210
|
-
function registerTopicContext(server, db,
|
|
806
|
+
function registerTopicContext(server, db, projectHash, notificationStore = null) {
|
|
1211
807
|
const stashManager = new StashManager(db);
|
|
1212
808
|
server.registerTool("topic_context", {
|
|
1213
809
|
title: "Topic Context",
|
|
@@ -1217,8 +813,7 @@ function registerTopicContext(server, db, projectHashRef, notificationStore = nu
|
|
|
1217
813
|
limit: z.number().int().min(1).max(20).default(5).describe("Max threads to return")
|
|
1218
814
|
}
|
|
1219
815
|
}, async (args) => {
|
|
1220
|
-
const
|
|
1221
|
-
const withNotifications = (text) => textResponse$8(prependNotifications$7(notificationStore, projectHash, text));
|
|
816
|
+
const withNotifications = (text) => textResponse$6(prependNotifications$5(notificationStore, projectHash, text));
|
|
1222
817
|
try {
|
|
1223
818
|
debug("mcp", "topic_context: request", {
|
|
1224
819
|
query: args.query,
|
|
@@ -1230,9 +825,6 @@ function registerTopicContext(server, db, projectHashRef, notificationStore = nu
|
|
|
1230
825
|
stashes = stashes.filter((s) => s.topicLabel.toLowerCase().includes(q) || s.summary.toLowerCase().includes(q));
|
|
1231
826
|
}
|
|
1232
827
|
if (stashes.length === 0) return withNotifications("No stashed context threads found. You're working in a single thread.");
|
|
1233
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
1234
|
-
if (verbosity === 1) return withNotifications(`${stashes.length} stashed thread(s)`);
|
|
1235
|
-
if (verbosity === 2) return withNotifications(stashes.map((s, i) => `${i + 1}. ${s.topicLabel} (${timeAgo(s.createdAt)})`).join("\n"));
|
|
1236
828
|
const formatted = formatStashes(stashes);
|
|
1237
829
|
const footer = `\n---\n${stashes.length} stashed thread(s) | Use /laminark:resume {id} to restore`;
|
|
1238
830
|
debug("mcp", "topic_context: returning", { count: stashes.length });
|
|
@@ -1240,7 +832,7 @@ function registerTopicContext(server, db, projectHashRef, notificationStore = nu
|
|
|
1240
832
|
} catch (err) {
|
|
1241
833
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1242
834
|
debug("mcp", "topic_context: error", { error: message });
|
|
1243
|
-
return textResponse$
|
|
835
|
+
return textResponse$6(`Error retrieving context threads: ${message}`);
|
|
1244
836
|
}
|
|
1245
837
|
});
|
|
1246
838
|
}
|
|
@@ -1345,19 +937,19 @@ function formatAge(isoDate) {
|
|
|
1345
937
|
const months = Math.floor(days / 30);
|
|
1346
938
|
return `${months} month${months !== 1 ? "s" : ""} ago`;
|
|
1347
939
|
}
|
|
1348
|
-
function prependNotifications$
|
|
940
|
+
function prependNotifications$4(notificationStore, projectHash, responseText) {
|
|
1349
941
|
if (!notificationStore) return responseText;
|
|
1350
942
|
const pending = notificationStore.consumePending(projectHash);
|
|
1351
943
|
if (pending.length === 0) return responseText;
|
|
1352
944
|
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
1353
945
|
}
|
|
1354
|
-
function textResponse$
|
|
946
|
+
function textResponse$5(text) {
|
|
1355
947
|
return { content: [{
|
|
1356
948
|
type: "text",
|
|
1357
949
|
text
|
|
1358
950
|
}] };
|
|
1359
951
|
}
|
|
1360
|
-
function errorResponse$
|
|
952
|
+
function errorResponse$2(text) {
|
|
1361
953
|
return {
|
|
1362
954
|
content: [{
|
|
1363
955
|
type: "text",
|
|
@@ -1372,7 +964,7 @@ function errorResponse$3(text) {
|
|
|
1372
964
|
* Allows Claude to search entities by name (exact or fuzzy), filter by type,
|
|
1373
965
|
* traverse relationships to configurable depth, and see linked observations.
|
|
1374
966
|
*/
|
|
1375
|
-
function registerQueryGraph(server, db,
|
|
967
|
+
function registerQueryGraph(server, db, projectHash, notificationStore = null) {
|
|
1376
968
|
initGraphSchema(db);
|
|
1377
969
|
server.registerTool("query_graph", {
|
|
1378
970
|
title: "Query Knowledge Graph",
|
|
@@ -1385,18 +977,17 @@ function registerQueryGraph(server, db, projectHashRef, notificationStore = null
|
|
|
1385
977
|
limit: z.number().int().min(1).max(50).default(20).describe("Max root entities to return (default: 20, max: 50)")
|
|
1386
978
|
}
|
|
1387
979
|
}, async (args) => {
|
|
1388
|
-
const
|
|
1389
|
-
const withNotifications = (text) => textResponse$7(prependNotifications$6(notificationStore, projectHash, text));
|
|
980
|
+
const withNotifications = (text) => textResponse$5(prependNotifications$4(notificationStore, projectHash, text));
|
|
1390
981
|
try {
|
|
1391
982
|
debug("mcp", "query_graph: request", {
|
|
1392
983
|
query: args.query,
|
|
1393
984
|
entity_type: args.entity_type,
|
|
1394
985
|
depth: args.depth
|
|
1395
986
|
});
|
|
1396
|
-
if (args.entity_type !== void 0 && !isEntityType(args.entity_type)) return errorResponse$
|
|
987
|
+
if (args.entity_type !== void 0 && !isEntityType(args.entity_type)) return errorResponse$2(`Invalid entity_type "${args.entity_type}". Valid types: ${ENTITY_TYPES.join(", ")}`);
|
|
1397
988
|
const entityType = args.entity_type;
|
|
1398
989
|
if (args.relationship_types) {
|
|
1399
|
-
for (const rt of args.relationship_types) if (!isRelationshipType(rt)) return errorResponse$
|
|
990
|
+
for (const rt of args.relationship_types) if (!isRelationshipType(rt)) return errorResponse$2(`Invalid relationship_type "${rt}". Valid types: ${RELATIONSHIP_TYPES.join(", ")}`);
|
|
1400
991
|
}
|
|
1401
992
|
const relationshipTypes = args.relationship_types;
|
|
1402
993
|
const rootNodes = [];
|
|
@@ -1457,21 +1048,6 @@ function registerQueryGraph(server, db, projectHashRef, notificationStore = null
|
|
|
1457
1048
|
createdAt: row.created_at
|
|
1458
1049
|
});
|
|
1459
1050
|
}
|
|
1460
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
1461
|
-
if (verbosity === 1) {
|
|
1462
|
-
const totalTraversals = [...traversalsByNode.values()].reduce((sum, arr) => sum + arr.length, 0);
|
|
1463
|
-
return withNotifications(`${rootNodes.length} entities, ${totalTraversals} connections found`);
|
|
1464
|
-
}
|
|
1465
|
-
if (verbosity === 2) {
|
|
1466
|
-
const lines = [];
|
|
1467
|
-
lines.push("## Entities Found");
|
|
1468
|
-
lines.push("");
|
|
1469
|
-
for (const node of rootNodes) {
|
|
1470
|
-
const traversals = traversalsByNode.get(node.id) ?? [];
|
|
1471
|
-
lines.push(`- ${formatEntityType(node.type)} ${node.name} (${traversals.length} connections)`);
|
|
1472
|
-
}
|
|
1473
|
-
return withNotifications(lines.join("\n"));
|
|
1474
|
-
}
|
|
1475
1051
|
const formatted = formatResults(rootNodes, traversalsByNode, observations, args.query);
|
|
1476
1052
|
debug("mcp", "query_graph: returning", {
|
|
1477
1053
|
rootNodes: rootNodes.length,
|
|
@@ -1482,11 +1058,180 @@ function registerQueryGraph(server, db, projectHashRef, notificationStore = null
|
|
|
1482
1058
|
} catch (err) {
|
|
1483
1059
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1484
1060
|
debug("mcp", "query_graph: error", { error: message });
|
|
1485
|
-
return errorResponse$
|
|
1061
|
+
return errorResponse$2(`Graph query error: ${message}`);
|
|
1486
1062
|
}
|
|
1487
1063
|
});
|
|
1488
1064
|
}
|
|
1489
1065
|
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/graph/staleness.ts
|
|
1068
|
+
/**
|
|
1069
|
+
* Negation patterns: newer observation negates older one.
|
|
1070
|
+
* Matches when newer text contains negation keywords absent in older text
|
|
1071
|
+
* and both discuss similar subjects.
|
|
1072
|
+
*/
|
|
1073
|
+
const NEGATION_KEYWORDS = [
|
|
1074
|
+
"not",
|
|
1075
|
+
"don't",
|
|
1076
|
+
"no longer",
|
|
1077
|
+
"stopped",
|
|
1078
|
+
"never",
|
|
1079
|
+
"doesn't",
|
|
1080
|
+
"won't",
|
|
1081
|
+
"isn't",
|
|
1082
|
+
"aren't",
|
|
1083
|
+
"discontinued"
|
|
1084
|
+
];
|
|
1085
|
+
/**
|
|
1086
|
+
* Replacement patterns: newer observation explicitly replaces older approach.
|
|
1087
|
+
*/
|
|
1088
|
+
const REPLACEMENT_PATTERNS = [
|
|
1089
|
+
/switched\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1090
|
+
/migrated\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1091
|
+
/replaced\s+(?:\S+\s+)?with\b/i,
|
|
1092
|
+
/changed\s+from\b/i,
|
|
1093
|
+
/moved\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1094
|
+
/upgraded\s+(?:from\s+\S+\s+)?to\b/i,
|
|
1095
|
+
/swapped\s+(?:\S+\s+)?(?:for|with)\b/i
|
|
1096
|
+
];
|
|
1097
|
+
/**
|
|
1098
|
+
* Status change patterns: newer observation marks something as inactive.
|
|
1099
|
+
*/
|
|
1100
|
+
const STATUS_CHANGE_KEYWORDS = [
|
|
1101
|
+
"removed",
|
|
1102
|
+
"deleted",
|
|
1103
|
+
"deprecated",
|
|
1104
|
+
"archived",
|
|
1105
|
+
"dropped",
|
|
1106
|
+
"disabled",
|
|
1107
|
+
"decommissioned",
|
|
1108
|
+
"sunset",
|
|
1109
|
+
"abandoned"
|
|
1110
|
+
];
|
|
1111
|
+
/**
|
|
1112
|
+
* Creates the staleness_flags table if it doesn't exist.
|
|
1113
|
+
* Uses a separate table rather than modifying the observations table,
|
|
1114
|
+
* keeping staleness metadata decoupled from core observation storage.
|
|
1115
|
+
*/
|
|
1116
|
+
function initStalenessSchema(db) {
|
|
1117
|
+
db.exec(`
|
|
1118
|
+
CREATE TABLE IF NOT EXISTS staleness_flags (
|
|
1119
|
+
observation_id TEXT PRIMARY KEY,
|
|
1120
|
+
flagged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1121
|
+
reason TEXT NOT NULL,
|
|
1122
|
+
resolved INTEGER NOT NULL DEFAULT 0
|
|
1123
|
+
);
|
|
1124
|
+
CREATE INDEX IF NOT EXISTS idx_staleness_resolved ON staleness_flags(resolved);
|
|
1125
|
+
`);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Detects potential staleness (contradictions) between observations
|
|
1129
|
+
* linked to a specific entity.
|
|
1130
|
+
*
|
|
1131
|
+
* Compares consecutive observation pairs chronologically and checks for:
|
|
1132
|
+
* 1. Negation patterns (newer negates older)
|
|
1133
|
+
* 2. Replacement patterns (newer replaces older approach)
|
|
1134
|
+
* 3. Status change patterns (newer marks something as inactive)
|
|
1135
|
+
*
|
|
1136
|
+
* This is DETECTION ONLY -- no data is modified.
|
|
1137
|
+
*
|
|
1138
|
+
* @param db - better-sqlite3 Database handle
|
|
1139
|
+
* @param entityId - Graph node ID to check observations for
|
|
1140
|
+
* @returns Array of StalenessReport for each detected contradiction
|
|
1141
|
+
*/
|
|
1142
|
+
function detectStaleness(db, entityId) {
|
|
1143
|
+
const node = db.prepare("SELECT id, name, type, observation_ids FROM graph_nodes WHERE id = ?").get(entityId);
|
|
1144
|
+
if (!node) return [];
|
|
1145
|
+
const obsIds = JSON.parse(node.observation_ids);
|
|
1146
|
+
if (obsIds.length < 2) return [];
|
|
1147
|
+
const placeholders = obsIds.map(() => "?").join(", ");
|
|
1148
|
+
const observations = db.prepare(`SELECT * FROM observations WHERE id IN (${placeholders}) AND deleted_at IS NULL ORDER BY created_at ASC`).all(...obsIds).map(rowToObservation);
|
|
1149
|
+
if (observations.length < 2) return [];
|
|
1150
|
+
const reports = [];
|
|
1151
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1152
|
+
for (let i = 0; i < observations.length - 1; i++) {
|
|
1153
|
+
const older = observations[i];
|
|
1154
|
+
const newer = observations[i + 1];
|
|
1155
|
+
const reason = detectContradiction(older.content, newer.content);
|
|
1156
|
+
if (reason) reports.push({
|
|
1157
|
+
entityId: node.id,
|
|
1158
|
+
entityName: node.name,
|
|
1159
|
+
entityType: node.type,
|
|
1160
|
+
newerObservation: {
|
|
1161
|
+
id: newer.id,
|
|
1162
|
+
text: newer.content,
|
|
1163
|
+
created_at: newer.createdAt
|
|
1164
|
+
},
|
|
1165
|
+
olderObservation: {
|
|
1166
|
+
id: older.id,
|
|
1167
|
+
text: older.content,
|
|
1168
|
+
created_at: older.createdAt
|
|
1169
|
+
},
|
|
1170
|
+
reason,
|
|
1171
|
+
detectedAt: now
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
return reports;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Detects contradiction between two observation texts.
|
|
1178
|
+
* Returns a human-readable reason string, or null if no contradiction found.
|
|
1179
|
+
*/
|
|
1180
|
+
function detectContradiction(olderText, newerText) {
|
|
1181
|
+
const olderLower = olderText.toLowerCase();
|
|
1182
|
+
const newerLower = newerText.toLowerCase();
|
|
1183
|
+
const negationResult = detectNegation(olderLower, newerLower);
|
|
1184
|
+
if (negationResult) return negationResult;
|
|
1185
|
+
const replacementResult = detectReplacement(newerLower);
|
|
1186
|
+
if (replacementResult) return replacementResult;
|
|
1187
|
+
const statusResult = detectStatusChange(olderLower, newerLower);
|
|
1188
|
+
if (statusResult) return statusResult;
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Detects negation: newer text contains negation keywords that are absent
|
|
1193
|
+
* in the older text, suggesting the newer observation contradicts the older.
|
|
1194
|
+
*/
|
|
1195
|
+
function detectNegation(olderLower, newerLower) {
|
|
1196
|
+
for (const keyword of NEGATION_KEYWORDS) if (newerLower.includes(keyword) && !olderLower.includes(keyword)) return `Newer observation contains negation ("${keyword}") not present in older observation`;
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Detects replacement: newer text explicitly mentions switching/replacing.
|
|
1201
|
+
*/
|
|
1202
|
+
function detectReplacement(newerLower) {
|
|
1203
|
+
for (const pattern of REPLACEMENT_PATTERNS) {
|
|
1204
|
+
const match = newerLower.match(pattern);
|
|
1205
|
+
if (match) return `Newer observation indicates replacement ("${match[0].trim()}")`;
|
|
1206
|
+
}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Detects status change: newer text marks something as removed/deprecated
|
|
1211
|
+
* when the older text described it as active/present.
|
|
1212
|
+
*/
|
|
1213
|
+
function detectStatusChange(olderLower, newerLower) {
|
|
1214
|
+
for (const keyword of STATUS_CHANGE_KEYWORDS) if (newerLower.includes(keyword) && !olderLower.includes(keyword)) return `Newer observation indicates status change ("${keyword}")`;
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Flags an observation as stale with an advisory reason.
|
|
1219
|
+
*
|
|
1220
|
+
* This flag is advisory -- search can use it to deprioritize but never hide
|
|
1221
|
+
* the observation. The observation remains fully queryable.
|
|
1222
|
+
*
|
|
1223
|
+
* Uses INSERT OR REPLACE to allow re-flagging with an updated reason.
|
|
1224
|
+
*
|
|
1225
|
+
* @param db - better-sqlite3 Database handle
|
|
1226
|
+
* @param observationId - ID of the observation to flag
|
|
1227
|
+
* @param reason - Human-readable explanation of why it's stale
|
|
1228
|
+
*/
|
|
1229
|
+
function flagStaleObservation(db, observationId, reason) {
|
|
1230
|
+
initStalenessSchema(db);
|
|
1231
|
+
db.prepare(`INSERT OR REPLACE INTO staleness_flags (observation_id, reason, resolved)
|
|
1232
|
+
VALUES (?, ?, 0)`).run(observationId, reason);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1490
1235
|
//#endregion
|
|
1491
1236
|
//#region src/mcp/tools/graph-stats.ts
|
|
1492
1237
|
/**
|
|
@@ -1584,13 +1329,13 @@ function formatStats(stats) {
|
|
|
1584
1329
|
}
|
|
1585
1330
|
return lines.join("\n");
|
|
1586
1331
|
}
|
|
1587
|
-
function prependNotifications$
|
|
1332
|
+
function prependNotifications$3(notificationStore, projectHash, responseText) {
|
|
1588
1333
|
if (!notificationStore) return responseText;
|
|
1589
1334
|
const pending = notificationStore.consumePending(projectHash);
|
|
1590
1335
|
if (pending.length === 0) return responseText;
|
|
1591
1336
|
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
1592
1337
|
}
|
|
1593
|
-
function textResponse$
|
|
1338
|
+
function textResponse$4(text) {
|
|
1594
1339
|
return { content: [{
|
|
1595
1340
|
type: "text",
|
|
1596
1341
|
text
|
|
@@ -1603,14 +1348,13 @@ function textResponse$6(text) {
|
|
|
1603
1348
|
* type distribution, degree statistics, hotspot nodes, duplicate candidates,
|
|
1604
1349
|
* and staleness flags. No input parameters -- dashboard view.
|
|
1605
1350
|
*/
|
|
1606
|
-
function registerGraphStats(server, db,
|
|
1351
|
+
function registerGraphStats(server, db, projectHash, notificationStore = null) {
|
|
1607
1352
|
initGraphSchema(db);
|
|
1608
1353
|
server.registerTool("graph_stats", {
|
|
1609
1354
|
title: "Graph Statistics",
|
|
1610
1355
|
description: "Get knowledge graph statistics: entity counts, relationship distribution, health metrics. Use to understand the state of accumulated knowledge.",
|
|
1611
1356
|
inputSchema: {}
|
|
1612
1357
|
}, async () => {
|
|
1613
|
-
const projectHash = projectHashRef.current;
|
|
1614
1358
|
try {
|
|
1615
1359
|
debug("mcp", "graph_stats: request");
|
|
1616
1360
|
const stats = collectGraphStats(db);
|
|
@@ -1619,162 +1363,42 @@ function registerGraphStats(server, db, projectHashRef, notificationStore = null
|
|
|
1619
1363
|
nodes: stats.total_nodes,
|
|
1620
1364
|
edges: stats.total_edges
|
|
1621
1365
|
});
|
|
1622
|
-
return textResponse$
|
|
1366
|
+
return textResponse$4(prependNotifications$3(notificationStore, projectHash, formatted));
|
|
1623
1367
|
} catch (err) {
|
|
1624
1368
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1625
1369
|
debug("mcp", "graph_stats: error", { error: message });
|
|
1626
|
-
return textResponse$
|
|
1370
|
+
return textResponse$4(`Graph stats error: ${message}`);
|
|
1627
1371
|
}
|
|
1628
1372
|
});
|
|
1629
1373
|
}
|
|
1630
1374
|
|
|
1631
1375
|
//#endregion
|
|
1632
|
-
//#region src/mcp/tools/
|
|
1633
|
-
function
|
|
1634
|
-
const lines = [];
|
|
1635
|
-
lines.push("## Database Hygiene Report");
|
|
1636
|
-
lines.push(`Analyzed ${report.totalObservations.toLocaleString()} observations`);
|
|
1637
|
-
lines.push("");
|
|
1638
|
-
lines.push("### Summary");
|
|
1639
|
-
lines.push("| Tier | Count | Action |");
|
|
1640
|
-
lines.push("|------|-------|--------|");
|
|
1641
|
-
lines.push(`| High (>= 0.7) | ${report.summary.high} | Safe to purge |`);
|
|
1642
|
-
lines.push(`| Medium (0.5-0.69) | ${report.summary.medium} | Review recommended |`);
|
|
1643
|
-
if (report.summary.low > 0) lines.push(`| Low (< 0.5) | ${report.summary.low} | Kept |`);
|
|
1644
|
-
lines.push(`| Orphan graph nodes | ${report.summary.orphanNodeCount} | Dead references |`);
|
|
1645
|
-
lines.push("");
|
|
1646
|
-
if (report.candidates.length === 0) {
|
|
1647
|
-
lines.push("No candidates found matching the selected tier.");
|
|
1648
|
-
return lines.join("\n");
|
|
1649
|
-
}
|
|
1650
|
-
const bySession = /* @__PURE__ */ new Map();
|
|
1651
|
-
for (const c of report.candidates) {
|
|
1652
|
-
const key = c.sessionId ?? "(no session)";
|
|
1653
|
-
const list = bySession.get(key) ?? [];
|
|
1654
|
-
list.push(c);
|
|
1655
|
-
bySession.set(key, list);
|
|
1656
|
-
}
|
|
1657
|
-
const tierLabel = tier === "all" ? "All" : tier === "medium" ? "Medium+" : "High";
|
|
1658
|
-
lines.push(`### ${tierLabel} Confidence Candidates (showing ${report.candidates.length})`);
|
|
1659
|
-
lines.push("");
|
|
1660
|
-
for (const [sessionId, candidates] of bySession) {
|
|
1661
|
-
const sessionDate = candidates[0]?.createdAt?.substring(0, 10) ?? "";
|
|
1662
|
-
lines.push(`#### Session: ${sessionId.substring(0, 8)} (${sessionDate}, ${candidates.length} obs)`);
|
|
1663
|
-
lines.push("| ID | Kind | Source | Confidence | Signals | Preview |");
|
|
1664
|
-
lines.push("|----|------|--------|------------|---------|---------|");
|
|
1665
|
-
for (const c of candidates) {
|
|
1666
|
-
const signals = [];
|
|
1667
|
-
if (c.signals.orphaned) signals.push("orphaned");
|
|
1668
|
-
if (c.signals.islandNode) signals.push("island");
|
|
1669
|
-
if (c.signals.noiseClassified) signals.push("noise");
|
|
1670
|
-
if (c.signals.shortContent) signals.push("short");
|
|
1671
|
-
if (c.signals.autoCaptured) signals.push("auto");
|
|
1672
|
-
if (c.signals.stale) signals.push("stale");
|
|
1673
|
-
const preview = c.contentPreview.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1674
|
-
lines.push(`| ${c.shortId} | ${c.kind} | ${c.source} | ${c.confidence.toFixed(2)} | ${signals.join(",") || "-"} | ${preview} |`);
|
|
1675
|
-
}
|
|
1676
|
-
lines.push("");
|
|
1677
|
-
}
|
|
1678
|
-
if (mode === "simulate") lines.push(`_Dry run — no data modified. Use \`hygiene(mode="purge", tier="${tier}")\` to execute._`);
|
|
1679
|
-
return lines.join("\n");
|
|
1680
|
-
}
|
|
1681
|
-
function formatPurgeResult(observationsPurged, orphanNodesRemoved, tier) {
|
|
1682
|
-
const lines = [];
|
|
1683
|
-
lines.push("## Hygiene Purge Complete");
|
|
1684
|
-
lines.push(`- Tier: ${tier}`);
|
|
1685
|
-
lines.push(`- Observations soft-deleted: ${observationsPurged}`);
|
|
1686
|
-
lines.push(`- Orphan graph nodes removed: ${orphanNodesRemoved}`);
|
|
1687
|
-
return lines.join("\n");
|
|
1688
|
-
}
|
|
1689
|
-
function prependNotifications$4(notificationStore, projectHash, responseText) {
|
|
1376
|
+
//#region src/mcp/tools/status.ts
|
|
1377
|
+
function prependNotifications$2(notificationStore, projectHash, responseText) {
|
|
1690
1378
|
if (!notificationStore) return responseText;
|
|
1691
1379
|
const pending = notificationStore.consumePending(projectHash);
|
|
1692
1380
|
if (pending.length === 0) return responseText;
|
|
1693
1381
|
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
1694
1382
|
}
|
|
1695
|
-
function textResponse$
|
|
1383
|
+
function textResponse$3(text) {
|
|
1696
1384
|
return { content: [{
|
|
1697
1385
|
type: "text",
|
|
1698
1386
|
text
|
|
1699
1387
|
}] };
|
|
1700
1388
|
}
|
|
1701
|
-
function
|
|
1702
|
-
server.registerTool("
|
|
1703
|
-
title: "
|
|
1704
|
-
description: "
|
|
1705
|
-
inputSchema: {
|
|
1706
|
-
|
|
1707
|
-
tier: z.enum([
|
|
1708
|
-
"high",
|
|
1709
|
-
"medium",
|
|
1710
|
-
"all"
|
|
1711
|
-
]).default("high").describe("Which confidence tier to act on"),
|
|
1712
|
-
session_id: z.string().optional().describe("Optional: scope analysis to a single session"),
|
|
1713
|
-
limit: z.number().int().min(1).max(200).default(50).describe("Max results to return")
|
|
1714
|
-
}
|
|
1715
|
-
}, async (args) => {
|
|
1716
|
-
const projectHash = projectHashRef.current;
|
|
1389
|
+
function registerStatus(server, cache, projectHash, notificationStore = null) {
|
|
1390
|
+
server.registerTool("status", {
|
|
1391
|
+
title: "Laminark Status",
|
|
1392
|
+
description: "Show Laminark system status: connection info, memory count, token estimates, and capabilities.",
|
|
1393
|
+
inputSchema: {}
|
|
1394
|
+
}, async () => {
|
|
1717
1395
|
try {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const sessionId = args.session_id;
|
|
1721
|
-
const limit = args.limit ?? 50;
|
|
1722
|
-
debug("hygiene", "Request", {
|
|
1723
|
-
mode,
|
|
1724
|
-
tier,
|
|
1725
|
-
sessionId,
|
|
1726
|
-
limit
|
|
1727
|
-
});
|
|
1728
|
-
const report = analyzeObservations(db, projectHash, {
|
|
1729
|
-
sessionId,
|
|
1730
|
-
limit,
|
|
1731
|
-
minTier: tier === "all" ? "low" : tier
|
|
1732
|
-
});
|
|
1733
|
-
if (mode === "purge") {
|
|
1734
|
-
const result = executePurge(db, projectHash, report, tier);
|
|
1735
|
-
return textResponse$5(prependNotifications$4(notificationStore, projectHash, formatPurgeResult(result.observationsPurged, result.orphanNodesRemoved, tier)));
|
|
1736
|
-
}
|
|
1737
|
-
return textResponse$5(prependNotifications$4(notificationStore, projectHash, formatReport(report, mode, tier)));
|
|
1396
|
+
debug("mcp", "status: request (cached)");
|
|
1397
|
+
return textResponse$3(prependNotifications$2(notificationStore, projectHash, cache.getFormatted()));
|
|
1738
1398
|
} catch (err) {
|
|
1739
1399
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1740
|
-
debug("
|
|
1741
|
-
return textResponse$
|
|
1742
|
-
}
|
|
1743
|
-
});
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
//#endregion
|
|
1747
|
-
//#region src/mcp/tools/status.ts
|
|
1748
|
-
function prependNotifications$3(notificationStore, projectHash, responseText) {
|
|
1749
|
-
if (!notificationStore) return responseText;
|
|
1750
|
-
const pending = notificationStore.consumePending(projectHash);
|
|
1751
|
-
if (pending.length === 0) return responseText;
|
|
1752
|
-
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
1753
|
-
}
|
|
1754
|
-
function textResponse$4(text) {
|
|
1755
|
-
return { content: [{
|
|
1756
|
-
type: "text",
|
|
1757
|
-
text
|
|
1758
|
-
}] };
|
|
1759
|
-
}
|
|
1760
|
-
function registerStatus(server, cache, projectHashRef, notificationStore = null) {
|
|
1761
|
-
server.registerTool("status", {
|
|
1762
|
-
title: "Laminark Status",
|
|
1763
|
-
description: "Show Laminark system status: connection info, memory count, token estimates, and capabilities.",
|
|
1764
|
-
inputSchema: {}
|
|
1765
|
-
}, async () => {
|
|
1766
|
-
const projectHash = projectHashRef.current;
|
|
1767
|
-
try {
|
|
1768
|
-
debug("mcp", "status: request (cached)");
|
|
1769
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
1770
|
-
if (verbosity === 1) return textResponse$4(prependNotifications$3(notificationStore, projectHash, "Laminark: connected"));
|
|
1771
|
-
const formatted = cache.getFormatted();
|
|
1772
|
-
if (verbosity === 2) return textResponse$4(prependNotifications$3(notificationStore, projectHash, formatted.split("\n").slice(0, 8).join("\n")));
|
|
1773
|
-
return textResponse$4(prependNotifications$3(notificationStore, projectHash, formatted));
|
|
1774
|
-
} catch (err) {
|
|
1775
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1776
|
-
debug("mcp", "status: error", { error: message });
|
|
1777
|
-
return textResponse$4(`Status error: ${message}`);
|
|
1400
|
+
debug("mcp", "status: error", { error: message });
|
|
1401
|
+
return textResponse$3(`Status error: ${message}`);
|
|
1778
1402
|
}
|
|
1779
1403
|
});
|
|
1780
1404
|
}
|
|
@@ -1791,7 +1415,7 @@ function formatUptime(seconds) {
|
|
|
1791
1415
|
}
|
|
1792
1416
|
var StatusCache = class {
|
|
1793
1417
|
db;
|
|
1794
|
-
|
|
1418
|
+
projectHash;
|
|
1795
1419
|
projectPath;
|
|
1796
1420
|
hasVectorSupport;
|
|
1797
1421
|
isWorkerReady;
|
|
@@ -1800,9 +1424,9 @@ var StatusCache = class {
|
|
|
1800
1424
|
/** Uptime snapshot at the time cachedBody was built. */
|
|
1801
1425
|
builtAtUptime = 0;
|
|
1802
1426
|
dirty = false;
|
|
1803
|
-
constructor(db,
|
|
1427
|
+
constructor(db, projectHash, projectPath, hasVectorSupport, isWorkerReady) {
|
|
1804
1428
|
this.db = db;
|
|
1805
|
-
this.
|
|
1429
|
+
this.projectHash = projectHash;
|
|
1806
1430
|
this.projectPath = projectPath;
|
|
1807
1431
|
this.hasVectorSupport = hasVectorSupport;
|
|
1808
1432
|
this.isWorkerReady = isWorkerReady;
|
|
@@ -1829,7 +1453,7 @@ var StatusCache = class {
|
|
|
1829
1453
|
}
|
|
1830
1454
|
rebuild() {
|
|
1831
1455
|
try {
|
|
1832
|
-
const ph = this.
|
|
1456
|
+
const ph = this.projectHash;
|
|
1833
1457
|
const totalObs = this.db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE project_hash = ? AND deleted_at IS NULL").get(ph).cnt;
|
|
1834
1458
|
const embeddedObs = this.db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE project_hash = ? AND deleted_at IS NULL AND embedding_model IS NOT NULL").get(ph).cnt;
|
|
1835
1459
|
const deletedObs = this.db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE project_hash = ? AND deleted_at IS NOT NULL").get(ph).cnt;
|
|
@@ -1885,13 +1509,13 @@ var StatusCache = class {
|
|
|
1885
1509
|
|
|
1886
1510
|
//#endregion
|
|
1887
1511
|
//#region src/mcp/tools/discover-tools.ts
|
|
1888
|
-
function textResponse$
|
|
1512
|
+
function textResponse$2(text) {
|
|
1889
1513
|
return { content: [{
|
|
1890
1514
|
type: "text",
|
|
1891
1515
|
text
|
|
1892
1516
|
}] };
|
|
1893
1517
|
}
|
|
1894
|
-
function errorResponse$
|
|
1518
|
+
function errorResponse$1(text) {
|
|
1895
1519
|
return {
|
|
1896
1520
|
content: [{
|
|
1897
1521
|
type: "text",
|
|
@@ -1900,7 +1524,7 @@ function errorResponse$2(text) {
|
|
|
1900
1524
|
isError: true
|
|
1901
1525
|
};
|
|
1902
1526
|
}
|
|
1903
|
-
function prependNotifications$
|
|
1527
|
+
function prependNotifications$1(notificationStore, projectHash, responseText) {
|
|
1904
1528
|
if (!notificationStore) return responseText;
|
|
1905
1529
|
const pending = notificationStore.consumePending(projectHash);
|
|
1906
1530
|
if (pending.length === 0) return responseText;
|
|
@@ -1921,7 +1545,7 @@ function formatToolResult(result, index) {
|
|
|
1921
1545
|
* with optional scope filtering. Returns ranked results with scope, usage count,
|
|
1922
1546
|
* and last used timestamp metadata.
|
|
1923
1547
|
*/
|
|
1924
|
-
function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, notificationStore,
|
|
1548
|
+
function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, notificationStore, projectHash) {
|
|
1925
1549
|
server.registerTool("discover_tools", {
|
|
1926
1550
|
title: "Discover Tools",
|
|
1927
1551
|
description: "Search the tool registry to find available tools by keyword or description. Supports semantic search -- \"file manipulation\" finds tools described as \"read and write files\". Returns scope, usage count, and last used timestamp for each result.",
|
|
@@ -1935,8 +1559,7 @@ function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, n
|
|
|
1935
1559
|
limit: z.number().int().min(1).max(50).default(20).describe("Maximum results to return (default: 20)")
|
|
1936
1560
|
}
|
|
1937
1561
|
}, async (args) => {
|
|
1938
|
-
const
|
|
1939
|
-
const withNotifications = (text) => textResponse$3(prependNotifications$2(notificationStore, projectHash, text));
|
|
1562
|
+
const withNotifications = (text) => textResponse$2(prependNotifications$1(notificationStore, projectHash, text));
|
|
1940
1563
|
try {
|
|
1941
1564
|
debug("mcp", "discover_tools: request", {
|
|
1942
1565
|
query: args.query,
|
|
@@ -1974,14 +1597,14 @@ function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, n
|
|
|
1974
1597
|
} catch (err) {
|
|
1975
1598
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1976
1599
|
debug("mcp", "discover_tools: error", { error: message });
|
|
1977
|
-
return errorResponse$
|
|
1600
|
+
return errorResponse$1(`Discover tools error: ${message}`);
|
|
1978
1601
|
}
|
|
1979
1602
|
});
|
|
1980
1603
|
}
|
|
1981
1604
|
|
|
1982
1605
|
//#endregion
|
|
1983
1606
|
//#region src/mcp/tools/report-tools.ts
|
|
1984
|
-
function textResponse$
|
|
1607
|
+
function textResponse$1(text) {
|
|
1985
1608
|
return { content: [{
|
|
1986
1609
|
type: "text",
|
|
1987
1610
|
text
|
|
@@ -1994,7 +1617,7 @@ function textResponse$2(text) {
|
|
|
1994
1617
|
* each into the tool registry. Tool type, scope, and server name are inferred
|
|
1995
1618
|
* from the tool name using the same parser as PostToolUse organic discovery.
|
|
1996
1619
|
*/
|
|
1997
|
-
function registerReportTools(server, toolRegistry,
|
|
1620
|
+
function registerReportTools(server, toolRegistry, projectHash) {
|
|
1998
1621
|
server.registerTool("report_available_tools", {
|
|
1999
1622
|
title: "Report Available Tools",
|
|
2000
1623
|
description: "Register all tools available in this session with Laminark. Call this once at session start with every tool name you have access to (built-in and MCP). This populates the tool registry for discovery and routing.",
|
|
@@ -2003,7 +1626,6 @@ function registerReportTools(server, toolRegistry, projectHashRef) {
|
|
|
2003
1626
|
description: z.string().optional().describe("Brief description of the tool")
|
|
2004
1627
|
})).min(1).describe("Array of tools available in this session") }
|
|
2005
1628
|
}, async (args) => {
|
|
2006
|
-
const projectHash = projectHashRef.current;
|
|
2007
1629
|
try {
|
|
2008
1630
|
let registered = 0;
|
|
2009
1631
|
let skipped = 0;
|
|
@@ -2031,7 +1653,7 @@ function registerReportTools(server, toolRegistry, projectHashRef) {
|
|
|
2031
1653
|
registered,
|
|
2032
1654
|
skipped
|
|
2033
1655
|
});
|
|
2034
|
-
return textResponse$
|
|
1656
|
+
return textResponse$1(`Registered ${registered} tools in the tool registry.${skipped > 0 ? ` Skipped ${skipped} Laminark tools (already known).` : ""}`);
|
|
2035
1657
|
} catch (err) {
|
|
2036
1658
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2037
1659
|
debug("mcp", "report_available_tools: error", { error: message });
|
|
@@ -2048,19 +1670,19 @@ function registerReportTools(server, toolRegistry, projectHashRef) {
|
|
|
2048
1670
|
|
|
2049
1671
|
//#endregion
|
|
2050
1672
|
//#region src/mcp/tools/debug-paths.ts
|
|
2051
|
-
function prependNotifications
|
|
1673
|
+
function prependNotifications(notificationStore, projectHash, responseText) {
|
|
2052
1674
|
if (!notificationStore) return responseText;
|
|
2053
1675
|
const pending = notificationStore.consumePending(projectHash);
|
|
2054
1676
|
if (pending.length === 0) return responseText;
|
|
2055
1677
|
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
2056
1678
|
}
|
|
2057
|
-
function textResponse
|
|
1679
|
+
function textResponse(text) {
|
|
2058
1680
|
return { content: [{
|
|
2059
1681
|
type: "text",
|
|
2060
1682
|
text
|
|
2061
1683
|
}] };
|
|
2062
1684
|
}
|
|
2063
|
-
function errorResponse
|
|
1685
|
+
function errorResponse(text) {
|
|
2064
1686
|
return {
|
|
2065
1687
|
content: [{
|
|
2066
1688
|
type: "text",
|
|
@@ -2090,25 +1712,24 @@ function formatKissSummary(raw) {
|
|
|
2090
1712
|
*
|
|
2091
1713
|
* Tools: path_start, path_resolve, path_show, path_list
|
|
2092
1714
|
*/
|
|
2093
|
-
function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore,
|
|
1715
|
+
function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore, projectHash) {
|
|
2094
1716
|
server.registerTool("path_start", {
|
|
2095
1717
|
title: "Start Debug Path",
|
|
2096
1718
|
description: "Explicitly start tracking a debug path. Use when auto-detection hasn't triggered but you're actively debugging.",
|
|
2097
1719
|
inputSchema: { trigger: z.string().describe("Brief description of the issue being debugged") }
|
|
2098
1720
|
}, async (args) => {
|
|
2099
|
-
const
|
|
2100
|
-
const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
|
|
1721
|
+
const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
|
|
2101
1722
|
try {
|
|
2102
1723
|
debug("mcp", "path_start: request", { trigger: args.trigger });
|
|
2103
1724
|
const existingPathId = pathTracker.getActivePathId();
|
|
2104
1725
|
const pathId = pathTracker.startManually(args.trigger);
|
|
2105
|
-
if (!pathId) return errorResponse
|
|
1726
|
+
if (!pathId) return errorResponse("Failed to start debug path");
|
|
2106
1727
|
if (existingPathId && existingPathId === pathId) return withNotifications(`Debug path already active: ${pathId}`);
|
|
2107
|
-
return withNotifications(
|
|
1728
|
+
return withNotifications(`Debug path started: ${pathId}\nTracking: ${args.trigger}`);
|
|
2108
1729
|
} catch (err) {
|
|
2109
1730
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2110
1731
|
debug("mcp", "path_start: error", { error: message });
|
|
2111
|
-
return errorResponse
|
|
1732
|
+
return errorResponse(`path_start error: ${message}`);
|
|
2112
1733
|
}
|
|
2113
1734
|
});
|
|
2114
1735
|
server.registerTool("path_resolve", {
|
|
@@ -2116,18 +1737,17 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
|
|
|
2116
1737
|
description: "Explicitly resolve the active debug path with a resolution summary. Use when auto-detection hasn't detected resolution.",
|
|
2117
1738
|
inputSchema: { resolution: z.string().describe("What fixed the issue") }
|
|
2118
1739
|
}, async (args) => {
|
|
2119
|
-
const
|
|
2120
|
-
const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
|
|
1740
|
+
const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
|
|
2121
1741
|
try {
|
|
2122
1742
|
debug("mcp", "path_resolve: request", { resolution: args.resolution });
|
|
2123
1743
|
const pathId = pathTracker.getActivePathId();
|
|
2124
|
-
if (!pathId) return errorResponse
|
|
1744
|
+
if (!pathId) return errorResponse("No active debug path to resolve");
|
|
2125
1745
|
pathTracker.resolveManually(args.resolution);
|
|
2126
|
-
return withNotifications(
|
|
1746
|
+
return withNotifications(`Debug path resolved: ${pathId}\nResolution: ${args.resolution}\nKISS summary generating in background...`);
|
|
2127
1747
|
} catch (err) {
|
|
2128
1748
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2129
1749
|
debug("mcp", "path_resolve: error", { error: message });
|
|
2130
|
-
return errorResponse
|
|
1750
|
+
return errorResponse(`path_resolve error: ${message}`);
|
|
2131
1751
|
}
|
|
2132
1752
|
});
|
|
2133
1753
|
server.registerTool("path_show", {
|
|
@@ -2135,29 +1755,18 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
|
|
|
2135
1755
|
description: "Show a debug path with its waypoints and KISS summary.",
|
|
2136
1756
|
inputSchema: { path_id: z.string().optional().describe("Path ID to show. Omit for active path.") }
|
|
2137
1757
|
}, async (args) => {
|
|
2138
|
-
const
|
|
2139
|
-
const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
|
|
1758
|
+
const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
|
|
2140
1759
|
try {
|
|
2141
1760
|
debug("mcp", "path_show: request", { path_id: args.path_id });
|
|
2142
1761
|
let pathData;
|
|
2143
1762
|
if (args.path_id) {
|
|
2144
1763
|
pathData = pathRepo.getPath(args.path_id);
|
|
2145
|
-
if (!pathData) return errorResponse
|
|
1764
|
+
if (!pathData) return errorResponse(`Debug path not found: ${args.path_id}`);
|
|
2146
1765
|
} else {
|
|
2147
1766
|
pathData = pathRepo.getActivePath();
|
|
2148
|
-
if (!pathData) return errorResponse
|
|
1767
|
+
if (!pathData) return errorResponse("No active debug path");
|
|
2149
1768
|
}
|
|
2150
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
2151
|
-
if (verbosity === 1) return withNotifications(`Showing debug path: ${pathData.status}`);
|
|
2152
1769
|
const waypoints = pathRepo.getWaypoints(pathData.id);
|
|
2153
|
-
if (verbosity === 2) {
|
|
2154
|
-
const lines = [];
|
|
2155
|
-
lines.push(`## Debug Path: ${pathData.id}`);
|
|
2156
|
-
lines.push(`**Status:** ${pathData.status} | **Trigger:** ${pathData.trigger_summary}`);
|
|
2157
|
-
lines.push(`Waypoints: ${waypoints.length}`);
|
|
2158
|
-
if (pathData.resolution_summary) lines.push(`Resolution: ${pathData.resolution_summary}`);
|
|
2159
|
-
return withNotifications(lines.join("\n"));
|
|
2160
|
-
}
|
|
2161
1770
|
const lines = [];
|
|
2162
1771
|
lines.push(`## Debug Path: ${pathData.id}`);
|
|
2163
1772
|
lines.push(`Status: ${pathData.status}`);
|
|
@@ -2179,7 +1788,7 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
|
|
|
2179
1788
|
} catch (err) {
|
|
2180
1789
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2181
1790
|
debug("mcp", "path_show: error", { error: message });
|
|
2182
|
-
return errorResponse
|
|
1791
|
+
return errorResponse(`path_show error: ${message}`);
|
|
2183
1792
|
}
|
|
2184
1793
|
});
|
|
2185
1794
|
server.registerTool("path_list", {
|
|
@@ -2194,8 +1803,7 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
|
|
|
2194
1803
|
limit: z.number().int().min(1).max(50).default(10).describe("Max paths to return")
|
|
2195
1804
|
}
|
|
2196
1805
|
}, async (args) => {
|
|
2197
|
-
const
|
|
2198
|
-
const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
|
|
1806
|
+
const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
|
|
2199
1807
|
try {
|
|
2200
1808
|
debug("mcp", "path_list: request", {
|
|
2201
1809
|
status: args.status,
|
|
@@ -2204,768 +1812,25 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
|
|
|
2204
1812
|
let paths = pathRepo.listPaths(args.limit);
|
|
2205
1813
|
if (args.status) paths = paths.filter((p) => p.status === args.status);
|
|
2206
1814
|
if (paths.length === 0) return withNotifications("No debug paths found");
|
|
2207
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
2208
|
-
if (verbosity === 1) return withNotifications(`${paths.length} debug paths found`);
|
|
2209
1815
|
const lines = [];
|
|
2210
|
-
lines.push("## Debug Paths");
|
|
2211
|
-
lines.push("");
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
}
|
|
2229
|
-
return withNotifications(lines.join("\n"));
|
|
2230
|
-
} catch (err) {
|
|
2231
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2232
|
-
debug("mcp", "path_list: error", { error: message });
|
|
2233
|
-
return errorResponse$1(`path_list error: ${message}`);
|
|
2234
|
-
}
|
|
2235
|
-
});
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
//#endregion
|
|
2239
|
-
//#region src/mcp/tools/thought-branches.ts
|
|
2240
|
-
function prependNotifications(notificationStore, projectHash, responseText) {
|
|
2241
|
-
if (!notificationStore) return responseText;
|
|
2242
|
-
const pending = notificationStore.consumePending(projectHash);
|
|
2243
|
-
if (pending.length === 0) return responseText;
|
|
2244
|
-
return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
|
|
2245
|
-
}
|
|
2246
|
-
function textResponse(text) {
|
|
2247
|
-
return { content: [{
|
|
2248
|
-
type: "text",
|
|
2249
|
-
text
|
|
2250
|
-
}] };
|
|
2251
|
-
}
|
|
2252
|
-
function errorResponse(text) {
|
|
2253
|
-
return {
|
|
2254
|
-
content: [{
|
|
2255
|
-
type: "text",
|
|
2256
|
-
text
|
|
2257
|
-
}],
|
|
2258
|
-
isError: true
|
|
2259
|
-
};
|
|
2260
|
-
}
|
|
2261
|
-
function registerThoughtBranchTools(server, branchRepo, obsRepo, notificationStore, projectHashRef) {
|
|
2262
|
-
server.registerTool("query_branches", {
|
|
2263
|
-
title: "Query Thought Branches",
|
|
2264
|
-
description: "Search and list thought branches - coherent units of work (investigations, bug fixes, features). Use to see work history and what was investigated, fixed, or built.",
|
|
2265
|
-
inputSchema: {
|
|
2266
|
-
status: z.enum([
|
|
2267
|
-
"active",
|
|
2268
|
-
"completed",
|
|
2269
|
-
"abandoned",
|
|
2270
|
-
"merged"
|
|
2271
|
-
]).optional().describe("Filter by branch status"),
|
|
2272
|
-
branch_type: z.enum([
|
|
2273
|
-
"investigation",
|
|
2274
|
-
"bug_fix",
|
|
2275
|
-
"feature",
|
|
2276
|
-
"refactor",
|
|
2277
|
-
"research",
|
|
2278
|
-
"unknown"
|
|
2279
|
-
]).optional().describe("Filter by branch type"),
|
|
2280
|
-
limit: z.number().int().min(1).max(50).default(10).describe("Maximum results to return")
|
|
2281
|
-
}
|
|
2282
|
-
}, async (args) => {
|
|
2283
|
-
const projectHash = projectHashRef.current;
|
|
2284
|
-
const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
|
|
2285
|
-
try {
|
|
2286
|
-
debug("mcp", "query_branches: request", {
|
|
2287
|
-
status: args.status,
|
|
2288
|
-
branch_type: args.branch_type,
|
|
2289
|
-
limit: args.limit
|
|
2290
|
-
});
|
|
2291
|
-
let branches;
|
|
2292
|
-
if (args.status) branches = branchRepo.listByStatus(args.status, args.limit);
|
|
2293
|
-
else if (args.branch_type) branches = branchRepo.listByType(args.branch_type, args.limit);
|
|
2294
|
-
else branches = branchRepo.listBranches(args.limit);
|
|
2295
|
-
if (branches.length === 0) return withNotifications("No thought branches found");
|
|
2296
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
2297
|
-
if (verbosity === 1) return withNotifications(`${branches.length} branches found`);
|
|
2298
|
-
const lines = [];
|
|
2299
|
-
lines.push("## Thought Branches");
|
|
2300
|
-
lines.push("");
|
|
2301
|
-
if (verbosity === 2) {
|
|
2302
|
-
lines.push("| Status | Type | Title |");
|
|
2303
|
-
lines.push("|--------|------|-------|");
|
|
2304
|
-
for (const b of branches) {
|
|
2305
|
-
const title = b.title ? b.title.length > 50 ? b.title.slice(0, 50) + "..." : b.title : "-";
|
|
2306
|
-
lines.push(`| ${b.status} | ${b.branch_type} | ${title} |`);
|
|
2307
|
-
}
|
|
2308
|
-
} else {
|
|
2309
|
-
lines.push("| ID (short) | Status | Type | Stage | Title | Observations | Started |");
|
|
2310
|
-
lines.push("|------------|--------|------|-------|-------|-------------|---------|");
|
|
2311
|
-
for (const b of branches) {
|
|
2312
|
-
const shortId = b.id.slice(0, 8);
|
|
2313
|
-
const title = b.title ? b.title.length > 40 ? b.title.slice(0, 40) + "..." : b.title : "-";
|
|
2314
|
-
lines.push(`| ${shortId} | ${b.status} | ${b.branch_type} | ${b.arc_stage} | ${title} | ${b.observation_count} | ${b.started_at} |`);
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
return withNotifications(lines.join("\n"));
|
|
2318
|
-
} catch (err) {
|
|
2319
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2320
|
-
debug("mcp", "query_branches: error", { error: message });
|
|
2321
|
-
return errorResponse(`query_branches error: ${message}`);
|
|
2322
|
-
}
|
|
2323
|
-
});
|
|
2324
|
-
server.registerTool("show_branch", {
|
|
2325
|
-
title: "Show Thought Branch",
|
|
2326
|
-
description: "Show detailed view of a thought branch with observation timeline and arc stage annotations. Trace the full arc of a work unit.",
|
|
2327
|
-
inputSchema: { branch_id: z.string().optional().describe("Branch ID to show. Omit for active branch.") }
|
|
2328
|
-
}, async (args) => {
|
|
2329
|
-
const projectHash = projectHashRef.current;
|
|
2330
|
-
const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
|
|
2331
|
-
try {
|
|
2332
|
-
debug("mcp", "show_branch: request", { branch_id: args.branch_id });
|
|
2333
|
-
let branch;
|
|
2334
|
-
if (args.branch_id) {
|
|
2335
|
-
branch = branchRepo.getBranch(args.branch_id);
|
|
2336
|
-
if (!branch) return errorResponse(`Branch not found: ${args.branch_id}`);
|
|
2337
|
-
} else {
|
|
2338
|
-
branch = branchRepo.getActiveBranch();
|
|
2339
|
-
if (!branch) return errorResponse("No active thought branch");
|
|
2340
|
-
}
|
|
2341
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
2342
|
-
const branchTitle = branch.title ?? branch.id.slice(0, 12);
|
|
2343
|
-
if (verbosity === 1) return withNotifications(`Showing "${branchTitle}"`);
|
|
2344
|
-
const observations = branchRepo.getObservations(branch.id);
|
|
2345
|
-
if (verbosity === 2) {
|
|
2346
|
-
const lines = [];
|
|
2347
|
-
lines.push(`## ${branchTitle}`);
|
|
2348
|
-
lines.push(`**Status:** ${branch.status} | **Type:** ${branch.branch_type} | **Stage:** ${branch.arc_stage}`);
|
|
2349
|
-
if (branch.summary) lines.push(branch.summary);
|
|
2350
|
-
lines.push(`Observations: ${observations.length}`);
|
|
2351
|
-
return withNotifications(lines.join("\n"));
|
|
2352
|
-
}
|
|
2353
|
-
const lines = [];
|
|
2354
|
-
lines.push(`## Thought Branch: ${branchTitle}`);
|
|
2355
|
-
lines.push(`**ID:** ${branch.id}`);
|
|
2356
|
-
lines.push(`**Status:** ${branch.status}`);
|
|
2357
|
-
lines.push(`**Type:** ${branch.branch_type}`);
|
|
2358
|
-
lines.push(`**Arc Stage:** ${branch.arc_stage}`);
|
|
2359
|
-
lines.push(`**Started:** ${branch.started_at}`);
|
|
2360
|
-
if (branch.ended_at) lines.push(`**Ended:** ${branch.ended_at}`);
|
|
2361
|
-
if (branch.trigger_source) lines.push(`**Trigger:** ${branch.trigger_source}`);
|
|
2362
|
-
if (branch.linked_debug_path_id) lines.push(`**Linked Debug Path:** ${branch.linked_debug_path_id}`);
|
|
2363
|
-
lines.push("");
|
|
2364
|
-
const tools = Object.entries(branch.tool_pattern).sort(([, a], [, b]) => b - a);
|
|
2365
|
-
if (tools.length > 0) {
|
|
2366
|
-
lines.push("### Tool Usage");
|
|
2367
|
-
for (const [tool, count] of tools) lines.push(`- ${tool}: ${count}`);
|
|
2368
|
-
lines.push("");
|
|
2369
|
-
}
|
|
2370
|
-
if (branch.summary) {
|
|
2371
|
-
lines.push("### Summary");
|
|
2372
|
-
lines.push(branch.summary);
|
|
2373
|
-
lines.push("");
|
|
2374
|
-
}
|
|
2375
|
-
lines.push(`### Observation Timeline (${observations.length})`);
|
|
2376
|
-
for (const bo of observations) {
|
|
2377
|
-
const obs = obsRepo.getById(bo.observation_id);
|
|
2378
|
-
const content = obs ? obs.title ?? obs.content.slice(0, 100) : bo.observation_id.slice(0, 8);
|
|
2379
|
-
const stageTag = bo.arc_stage_at_add ? `[${bo.arc_stage_at_add}]` : "";
|
|
2380
|
-
const toolTag = bo.tool_name ? `(${bo.tool_name})` : "";
|
|
2381
|
-
lines.push(`${bo.sequence_order}. ${stageTag} ${toolTag} ${content}`);
|
|
2382
|
-
}
|
|
2383
|
-
return withNotifications(lines.join("\n"));
|
|
2384
|
-
} catch (err) {
|
|
2385
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2386
|
-
debug("mcp", "show_branch: error", { error: message });
|
|
2387
|
-
return errorResponse(`show_branch error: ${message}`);
|
|
2388
|
-
}
|
|
2389
|
-
});
|
|
2390
|
-
server.registerTool("branch_summary", {
|
|
2391
|
-
title: "Branch Activity Summary",
|
|
2392
|
-
description: "Summary of recent work activity grouped by time window. Shows what was investigated, fixed, built, and where work left off.",
|
|
2393
|
-
inputSchema: { hours: z.number().int().min(1).max(168).default(24).describe("Time window in hours (default 24)") }
|
|
2394
|
-
}, async (args) => {
|
|
2395
|
-
const projectHash = projectHashRef.current;
|
|
2396
|
-
const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
|
|
2397
|
-
try {
|
|
2398
|
-
debug("mcp", "branch_summary: request", { hours: args.hours });
|
|
2399
|
-
const branches = branchRepo.listRecentBranches(args.hours);
|
|
2400
|
-
if (branches.length === 0) return withNotifications(`No work branches in the last ${args.hours} hours`);
|
|
2401
|
-
const verbosity = loadToolVerbosityConfig().level;
|
|
2402
|
-
if (verbosity === 1) return withNotifications(`${branches.length} branches in ${args.hours}h`);
|
|
2403
|
-
const active = branches.filter((b) => b.status === "active");
|
|
2404
|
-
const completed = branches.filter((b) => b.status === "completed");
|
|
2405
|
-
const abandoned = branches.filter((b) => b.status === "abandoned");
|
|
2406
|
-
const lines = [];
|
|
2407
|
-
lines.push(`## Work Summary (last ${args.hours}h)`);
|
|
2408
|
-
lines.push(`**Total branches:** ${branches.length}`);
|
|
2409
|
-
lines.push("");
|
|
2410
|
-
if (active.length > 0) {
|
|
2411
|
-
lines.push("### Active");
|
|
2412
|
-
for (const b of active) {
|
|
2413
|
-
const title = b.title ?? b.id.slice(0, 8);
|
|
2414
|
-
lines.push(verbosity === 2 ? `- ${title} (${b.branch_type})` : `- **${title}** (${b.branch_type}, ${b.arc_stage}) — ${b.observation_count} obs`);
|
|
2415
|
-
}
|
|
2416
|
-
lines.push("");
|
|
2417
|
-
}
|
|
2418
|
-
if (completed.length > 0) {
|
|
2419
|
-
lines.push("### Completed");
|
|
2420
|
-
for (const b of completed) {
|
|
2421
|
-
const title = b.title ?? b.id.slice(0, 8);
|
|
2422
|
-
const summary = b.summary ? `: ${b.summary.slice(0, 100)}` : "";
|
|
2423
|
-
lines.push(verbosity === 2 ? `- ${title} (${b.branch_type})` : `- **${title}** (${b.branch_type})${summary}`);
|
|
2424
|
-
}
|
|
2425
|
-
lines.push("");
|
|
2426
|
-
}
|
|
2427
|
-
if (abandoned.length > 0) {
|
|
2428
|
-
lines.push("### Abandoned");
|
|
2429
|
-
for (const b of abandoned) {
|
|
2430
|
-
const title = b.title ?? b.id.slice(0, 8);
|
|
2431
|
-
lines.push(verbosity === 2 ? `- ${title} (${b.branch_type})` : `- **${title}** (${b.branch_type}) — ${b.observation_count} obs`);
|
|
2432
|
-
}
|
|
2433
|
-
lines.push("");
|
|
2434
|
-
}
|
|
2435
|
-
if (verbosity === 3) {
|
|
2436
|
-
const allTools = {};
|
|
2437
|
-
for (const b of branches) for (const [tool, count] of Object.entries(b.tool_pattern)) allTools[tool] = (allTools[tool] ?? 0) + count;
|
|
2438
|
-
const toolEntries = Object.entries(allTools).sort(([, a], [, b]) => b - a);
|
|
2439
|
-
if (toolEntries.length > 0) {
|
|
2440
|
-
lines.push("### Tool Distribution");
|
|
2441
|
-
for (const [tool, count] of toolEntries.slice(0, 10)) lines.push(`- ${tool}: ${count}`);
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
return withNotifications(lines.join("\n"));
|
|
2445
|
-
} catch (err) {
|
|
2446
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2447
|
-
debug("mcp", "branch_summary: error", { error: message });
|
|
2448
|
-
return errorResponse(`branch_summary error: ${message}`);
|
|
2449
|
-
}
|
|
2450
|
-
});
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
//#endregion
|
|
2454
|
-
//#region src/branches/arc-detector.ts
|
|
2455
|
-
const BUILTIN_CATEGORY = {
|
|
2456
|
-
"Read": "investigation",
|
|
2457
|
-
"Glob": "investigation",
|
|
2458
|
-
"Grep": "investigation",
|
|
2459
|
-
"WebSearch": "investigation",
|
|
2460
|
-
"WebFetch": "investigation",
|
|
2461
|
-
"Task": "investigation",
|
|
2462
|
-
"AskUserQuestion": "investigation",
|
|
2463
|
-
"Write": "write",
|
|
2464
|
-
"Edit": "write",
|
|
2465
|
-
"NotebookEdit": "write",
|
|
2466
|
-
"Bash": "verification",
|
|
2467
|
-
"EnterPlanMode": "planning",
|
|
2468
|
-
"ExitPlanMode": "planning",
|
|
2469
|
-
"TaskCreate": "planning",
|
|
2470
|
-
"TaskUpdate": "planning",
|
|
2471
|
-
"TaskList": "planning",
|
|
2472
|
-
"TaskGet": "planning",
|
|
2473
|
-
"Skill": "uncategorized"
|
|
2474
|
-
};
|
|
2475
|
-
/** Keywords matched against tool descriptions (case-insensitive). */
|
|
2476
|
-
const DESCRIPTION_RULES = [
|
|
2477
|
-
{
|
|
2478
|
-
category: "planning",
|
|
2479
|
-
keywords: /\b(plan|todo|task|roadmap|milestone|phase|design|architect)\b/i
|
|
2480
|
-
},
|
|
2481
|
-
{
|
|
2482
|
-
category: "verification",
|
|
2483
|
-
keywords: /\b(run|test|build|execute|evaluate|validate|verify|check|assert|lint|compile)\b/i
|
|
2484
|
-
},
|
|
2485
|
-
{
|
|
2486
|
-
category: "write",
|
|
2487
|
-
keywords: /\b(write|edit|create|update|save|upload|modify|delete|remove|fill|type|click|select|drag|press|submit|install|deploy|push|commit|insert|drop|replace)\b/i
|
|
2488
|
-
},
|
|
2489
|
-
{
|
|
2490
|
-
category: "investigation",
|
|
2491
|
-
keywords: /\b(read|search|query|find|list|get|fetch|browse|snapshot|screenshot|inspect|show|view|discover|status|stats|navigate|hover|recall|monitor|log|trace|debug|profile|measure|analyze|explore)\b/i
|
|
2492
|
-
}
|
|
2493
|
-
];
|
|
2494
|
-
/**
|
|
2495
|
-
* Classify a tool from its description text.
|
|
2496
|
-
* Returns null if no confident match.
|
|
2497
|
-
*/
|
|
2498
|
-
function classifyFromDescription(description) {
|
|
2499
|
-
for (const rule of DESCRIPTION_RULES) if (rule.keywords.test(description)) return rule.category;
|
|
2500
|
-
return null;
|
|
2501
|
-
}
|
|
2502
|
-
const NAME_RULES = [
|
|
2503
|
-
{
|
|
2504
|
-
category: "planning",
|
|
2505
|
-
pattern: /\b(plan|todo|task|roadmap|phase|milestone)\b/i
|
|
2506
|
-
},
|
|
2507
|
-
{
|
|
2508
|
-
category: "verification",
|
|
2509
|
-
pattern: /\b(run|test|build|exec|evaluate|validate|check|verify)\b/i
|
|
2510
|
-
},
|
|
2511
|
-
{
|
|
2512
|
-
category: "write",
|
|
2513
|
-
pattern: /\b(write|edit|create|update|save|upload|fill|type|click|select|drag|press|install)\b/i
|
|
2514
|
-
},
|
|
2515
|
-
{
|
|
2516
|
-
category: "investigation",
|
|
2517
|
-
pattern: /\b(search|query|find|list|get|read|fetch|browse|snapshot|screenshot|inspect|show|view|recall|discover|status|stats|console|network|navigate|tabs|hover)\b/i
|
|
2518
|
-
}
|
|
2519
|
-
];
|
|
2520
|
-
function classifyFromName(toolName) {
|
|
2521
|
-
const actionPart = toolName.includes("__") ? toolName.substring(toolName.lastIndexOf("__") + 2) : toolName;
|
|
2522
|
-
for (const rule of NAME_RULES) if (rule.pattern.test(actionPart)) return rule.category;
|
|
2523
|
-
if (toolName.includes("laminark")) return "investigation";
|
|
2524
|
-
return "uncategorized";
|
|
2525
|
-
}
|
|
2526
|
-
const classificationCache = /* @__PURE__ */ new Map();
|
|
2527
|
-
let lastRegistryCount = -1;
|
|
2528
|
-
/**
|
|
2529
|
-
* Re-reads the tool_registry table and classifies every tool by its
|
|
2530
|
-
* description. Only rescans when the registry row count has changed.
|
|
2531
|
-
*
|
|
2532
|
-
* Call on startup and periodically (e.g., during BranchTracker maintenance).
|
|
2533
|
-
*/
|
|
2534
|
-
function primeFromRegistry(db, projectHash) {
|
|
2535
|
-
try {
|
|
2536
|
-
const currentCount = db.prepare("SELECT COUNT(*) AS cnt FROM tool_registry").get()?.cnt ?? 0;
|
|
2537
|
-
if (currentCount === lastRegistryCount && lastRegistryCount >= 0) return;
|
|
2538
|
-
const rows = db.prepare(`
|
|
2539
|
-
SELECT name, description FROM tool_registry
|
|
2540
|
-
WHERE status = 'active'
|
|
2541
|
-
AND (scope = 'global' OR project_hash IS NULL OR project_hash = ?)
|
|
2542
|
-
`).all(projectHash);
|
|
2543
|
-
let primed = 0;
|
|
2544
|
-
for (const row of rows) {
|
|
2545
|
-
if (BUILTIN_CATEGORY[row.name]) continue;
|
|
2546
|
-
let category = null;
|
|
2547
|
-
if (row.description) category = classifyFromDescription(row.description);
|
|
2548
|
-
if (!category) category = classifyFromName(row.name);
|
|
2549
|
-
classificationCache.set(row.name, category);
|
|
2550
|
-
primed++;
|
|
2551
|
-
}
|
|
2552
|
-
lastRegistryCount = currentCount;
|
|
2553
|
-
debug("branches", "Arc detector cache primed from registry", {
|
|
2554
|
-
registryTools: rows.length,
|
|
2555
|
-
primed
|
|
2556
|
-
});
|
|
2557
|
-
} catch {}
|
|
2558
|
-
}
|
|
2559
|
-
/**
|
|
2560
|
-
* Classify any tool name into an arc category.
|
|
2561
|
-
*
|
|
2562
|
-
* Priority: built-in table > registry-primed cache > name-pattern fallback.
|
|
2563
|
-
*/
|
|
2564
|
-
function classifyTool(toolName) {
|
|
2565
|
-
const cached = classificationCache.get(toolName);
|
|
2566
|
-
if (cached) return cached;
|
|
2567
|
-
const builtin = BUILTIN_CATEGORY[toolName];
|
|
2568
|
-
if (builtin) {
|
|
2569
|
-
classificationCache.set(toolName, builtin);
|
|
2570
|
-
return builtin;
|
|
2571
|
-
}
|
|
2572
|
-
const fromName = classifyFromName(toolName);
|
|
2573
|
-
classificationCache.set(toolName, fromName);
|
|
2574
|
-
return fromName;
|
|
2575
|
-
}
|
|
2576
|
-
/**
|
|
2577
|
-
* Infers the current arc stage from tool usage pattern counts.
|
|
2578
|
-
*
|
|
2579
|
-
* Handles all tool types: builtins, MCP tools, plugins, skills, slash commands.
|
|
2580
|
-
* Uncategorized tools are excluded from ratio calculations so they don't
|
|
2581
|
-
* dilute the signal from known tools.
|
|
2582
|
-
*
|
|
2583
|
-
* @param toolPattern - Map of tool name to usage count within the branch
|
|
2584
|
-
* @param classification - Optional dominant observation classification
|
|
2585
|
-
* @returns The inferred arc stage
|
|
2586
|
-
*/
|
|
2587
|
-
function inferArcStage(toolPattern, classification) {
|
|
2588
|
-
let investigationCount = 0;
|
|
2589
|
-
let writeCount = 0;
|
|
2590
|
-
let verificationCount = 0;
|
|
2591
|
-
let planningCount = 0;
|
|
2592
|
-
let categorizedCount = 0;
|
|
2593
|
-
for (const [tool, count] of Object.entries(toolPattern)) switch (classifyTool(tool)) {
|
|
2594
|
-
case "investigation":
|
|
2595
|
-
investigationCount += count;
|
|
2596
|
-
categorizedCount += count;
|
|
2597
|
-
break;
|
|
2598
|
-
case "write":
|
|
2599
|
-
writeCount += count;
|
|
2600
|
-
categorizedCount += count;
|
|
2601
|
-
break;
|
|
2602
|
-
case "verification":
|
|
2603
|
-
verificationCount += count;
|
|
2604
|
-
categorizedCount += count;
|
|
2605
|
-
break;
|
|
2606
|
-
case "planning":
|
|
2607
|
-
planningCount += count;
|
|
2608
|
-
categorizedCount += count;
|
|
2609
|
-
break;
|
|
2610
|
-
case "uncategorized": break;
|
|
2611
|
-
}
|
|
2612
|
-
if (categorizedCount === 0) return "investigation";
|
|
2613
|
-
if (verificationCount > 0 && writeCount > 0) {
|
|
2614
|
-
if (verificationCount / categorizedCount > .2) return "verification";
|
|
2615
|
-
}
|
|
2616
|
-
if (writeCount / categorizedCount > .4) return "execution";
|
|
2617
|
-
if (planningCount > 0) {
|
|
2618
|
-
if (planningCount / categorizedCount > .1) return "planning";
|
|
2619
|
-
}
|
|
2620
|
-
if (classification === "problem" && writeCount > 0 && investigationCount > 0) return "diagnosis";
|
|
2621
|
-
return "investigation";
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
|
-
//#endregion
|
|
2625
|
-
//#region src/config/haiku-config.ts
|
|
2626
|
-
function loadHaikuConfig() {
|
|
2627
|
-
return {
|
|
2628
|
-
model: "claude-haiku-4-5-20251001",
|
|
2629
|
-
maxTokensPerCall: 1024
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
//#endregion
|
|
2634
|
-
//#region src/intelligence/haiku-client.ts
|
|
2635
|
-
/**
|
|
2636
|
-
* Shared Haiku client using Claude Agent SDK V2 session.
|
|
2637
|
-
*
|
|
2638
|
-
* Routes Haiku calls through the user's Claude Code subscription
|
|
2639
|
-
* instead of requiring a separate API key. Uses a persistent session
|
|
2640
|
-
* to avoid 12s cold-start overhead on sequential calls.
|
|
2641
|
-
*
|
|
2642
|
-
* Provides the core infrastructure for all Haiku agent modules:
|
|
2643
|
-
* - callHaiku() helper for structured prompt/response calls
|
|
2644
|
-
* - extractJsonFromResponse() for defensive JSON parsing
|
|
2645
|
-
* - Session reuse across batch processing cycles
|
|
2646
|
-
*/
|
|
2647
|
-
let _session = null;
|
|
2648
|
-
function getOrCreateSession() {
|
|
2649
|
-
if (!_session) _session = unstable_v2_createSession({
|
|
2650
|
-
model: loadHaikuConfig().model,
|
|
2651
|
-
permissionMode: "bypassPermissions",
|
|
2652
|
-
allowedTools: []
|
|
2653
|
-
});
|
|
2654
|
-
return _session;
|
|
2655
|
-
}
|
|
2656
|
-
/**
|
|
2657
|
-
* Returns whether Haiku enrichment is available.
|
|
2658
|
-
* Always true with subscription auth -- no API key check needed.
|
|
2659
|
-
*/
|
|
2660
|
-
function isHaikuEnabled() {
|
|
2661
|
-
return true;
|
|
2662
|
-
}
|
|
2663
|
-
/**
|
|
2664
|
-
* Calls Haiku with a system prompt and user content.
|
|
2665
|
-
* Returns the text content from the response.
|
|
2666
|
-
*
|
|
2667
|
-
* Uses a persistent V2 session to avoid cold-start overhead on sequential calls.
|
|
2668
|
-
* System prompt is embedded in the user message since session-level systemPrompt
|
|
2669
|
-
* is set at creation time and we need different prompts per agent.
|
|
2670
|
-
*
|
|
2671
|
-
* @param systemPrompt - Instructions for the model
|
|
2672
|
-
* @param userContent - The content to process
|
|
2673
|
-
* @param _maxTokens - Kept for signature compatibility (unused -- Agent SDK constrains output via prompts)
|
|
2674
|
-
* @throws Error if the Haiku call fails or session expires
|
|
2675
|
-
*/
|
|
2676
|
-
async function callHaiku(systemPrompt, userContent, _maxTokens) {
|
|
2677
|
-
const session = getOrCreateSession();
|
|
2678
|
-
const fullPrompt = `<instructions>\n${systemPrompt}\n</instructions>\n\n${userContent}`;
|
|
2679
|
-
try {
|
|
2680
|
-
await session.send(fullPrompt);
|
|
2681
|
-
for await (const msg of session.stream()) if (msg.type === "result") {
|
|
2682
|
-
if (msg.subtype === "success") return msg.result;
|
|
2683
|
-
const errorMsg = ("errors" in msg ? msg.errors : void 0)?.join(", ") ?? msg.subtype;
|
|
2684
|
-
throw new Error(`Haiku call failed: ${errorMsg}`);
|
|
2685
|
-
}
|
|
2686
|
-
return "";
|
|
2687
|
-
} catch (error) {
|
|
2688
|
-
try {
|
|
2689
|
-
_session?.close();
|
|
2690
|
-
} catch {}
|
|
2691
|
-
_session = null;
|
|
2692
|
-
throw error;
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
/**
|
|
2696
|
-
* Defensive JSON extraction from Haiku response text.
|
|
2697
|
-
*
|
|
2698
|
-
* Handles common LLM response quirks:
|
|
2699
|
-
* - Markdown code fences (```json ... ```)
|
|
2700
|
-
* - Explanatory text before/after JSON
|
|
2701
|
-
* - Both array and object JSON shapes
|
|
2702
|
-
*
|
|
2703
|
-
* @throws Error if no JSON structure found in text
|
|
2704
|
-
*/
|
|
2705
|
-
function extractJsonFromResponse(text) {
|
|
2706
|
-
const cleaned = text.replace(/```json\s*/g, "").replace(/```\s*/g, "");
|
|
2707
|
-
const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
|
|
2708
|
-
if (arrayMatch) return JSON.parse(arrayMatch[0]);
|
|
2709
|
-
const objMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
2710
|
-
if (objMatch) return JSON.parse(objMatch[0]);
|
|
2711
|
-
throw new Error("No JSON found in Haiku response");
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
//#endregion
|
|
2715
|
-
//#region src/branches/branch-classifier-agent.ts
|
|
2716
|
-
/**
|
|
2717
|
-
* Haiku agent for classifying thought branch type and generating title/summary.
|
|
2718
|
-
*
|
|
2719
|
-
* Uses a single Haiku call to determine:
|
|
2720
|
-
* 1. Branch type (investigation, bug_fix, feature, refactor, research)
|
|
2721
|
-
* 2. A concise title for the branch
|
|
2722
|
-
* 3. An optional summary (for completed branches)
|
|
2723
|
-
*
|
|
2724
|
-
* Follows the same pattern as haiku-classifier-agent.ts.
|
|
2725
|
-
*/
|
|
2726
|
-
const ClassifyBranchSchema = z.object({
|
|
2727
|
-
branch_type: z.enum([
|
|
2728
|
-
"investigation",
|
|
2729
|
-
"bug_fix",
|
|
2730
|
-
"feature",
|
|
2731
|
-
"refactor",
|
|
2732
|
-
"research"
|
|
2733
|
-
]),
|
|
2734
|
-
title: z.string().max(100)
|
|
2735
|
-
});
|
|
2736
|
-
const SummarizeBranchSchema = z.object({ summary: z.string().max(500) });
|
|
2737
|
-
const CLASSIFY_PROMPT = `You classify developer work branches for a knowledge management system.
|
|
2738
|
-
|
|
2739
|
-
Given a sequence of observations from a work session, determine:
|
|
2740
|
-
1. branch_type: What kind of work is this?
|
|
2741
|
-
- "investigation": Exploring code, reading docs, understanding behavior
|
|
2742
|
-
- "bug_fix": Fixing an error, test failure, or unexpected behavior
|
|
2743
|
-
- "feature": Building new functionality
|
|
2744
|
-
- "refactor": Restructuring existing code without changing behavior
|
|
2745
|
-
- "research": Looking up external resources, comparing approaches
|
|
2746
|
-
|
|
2747
|
-
2. title: A concise title (3-8 words) describing the work unit. Use imperative form.
|
|
2748
|
-
Examples: "Fix auth token refresh", "Add branch detection system", "Investigate memory leak"
|
|
2749
|
-
|
|
2750
|
-
Return JSON: {"branch_type": "...", "title": "..."}
|
|
2751
|
-
No markdown, no explanation, ONLY the JSON object.`;
|
|
2752
|
-
const SUMMARIZE_PROMPT = `You summarize completed developer work branches for a knowledge management system.
|
|
2753
|
-
|
|
2754
|
-
Given a sequence of observations from a completed work branch, write a concise summary (1-3 sentences) that captures:
|
|
2755
|
-
- What was the goal
|
|
2756
|
-
- What was done
|
|
2757
|
-
- What was the outcome
|
|
2758
|
-
|
|
2759
|
-
Return JSON: {"summary": "..."}
|
|
2760
|
-
No markdown, no explanation, ONLY the JSON object.`;
|
|
2761
|
-
/**
|
|
2762
|
-
* Classifies a branch type and generates a title from observation content.
|
|
2763
|
-
*/
|
|
2764
|
-
async function classifyBranchWithHaiku(observationTexts, toolPattern) {
|
|
2765
|
-
const parsed = extractJsonFromResponse(await callHaiku(CLASSIFY_PROMPT, [
|
|
2766
|
-
`Tool usage: ${Object.entries(toolPattern).sort(([, a], [, b]) => b - a).map(([tool, count]) => `${tool}: ${count}`).join(", ")}`,
|
|
2767
|
-
"",
|
|
2768
|
-
"Observations:",
|
|
2769
|
-
...observationTexts.slice(0, 10).map((t, i) => `${i + 1}. ${t.slice(0, 200)}`)
|
|
2770
|
-
].join("\n"), 256));
|
|
2771
|
-
return ClassifyBranchSchema.parse(parsed);
|
|
2772
|
-
}
|
|
2773
|
-
/**
|
|
2774
|
-
* Generates a completion summary for a finished branch.
|
|
2775
|
-
*/
|
|
2776
|
-
async function summarizeBranchWithHaiku(title, branchType, observationTexts) {
|
|
2777
|
-
const parsed = extractJsonFromResponse(await callHaiku(SUMMARIZE_PROMPT, [
|
|
2778
|
-
`Branch: ${title} (${branchType})`,
|
|
2779
|
-
"",
|
|
2780
|
-
"Observations:",
|
|
2781
|
-
...observationTexts.slice(0, 15).map((t, i) => `${i + 1}. ${t.slice(0, 200)}`)
|
|
2782
|
-
].join("\n"), 256));
|
|
2783
|
-
return SummarizeBranchSchema.parse(parsed);
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
//#endregion
|
|
2787
|
-
//#region src/branches/branch-tracker.ts
|
|
2788
|
-
const TIME_GAP_MS = 900 * 1e3;
|
|
2789
|
-
var BranchTracker = class {
|
|
2790
|
-
state = "idle";
|
|
2791
|
-
activeBranchId = null;
|
|
2792
|
-
activeProjectHash = null;
|
|
2793
|
-
activeSessionId = null;
|
|
2794
|
-
lastObservationTime = 0;
|
|
2795
|
-
toolPattern = {};
|
|
2796
|
-
repo;
|
|
2797
|
-
db;
|
|
2798
|
-
projectHash;
|
|
2799
|
-
constructor(repo, db, projectHash) {
|
|
2800
|
-
this.repo = repo;
|
|
2801
|
-
this.db = db;
|
|
2802
|
-
this.projectHash = projectHash;
|
|
2803
|
-
primeFromRegistry(db, projectHash);
|
|
2804
|
-
const activeBranch = repo.findRecentActiveBranch();
|
|
2805
|
-
if (activeBranch) {
|
|
2806
|
-
this.state = "tracking";
|
|
2807
|
-
this.activeBranchId = activeBranch.id;
|
|
2808
|
-
this.activeProjectHash = activeBranch.project_hash;
|
|
2809
|
-
this.activeSessionId = activeBranch.session_id;
|
|
2810
|
-
this.toolPattern = activeBranch.tool_pattern;
|
|
2811
|
-
this.lastObservationTime = new Date(activeBranch.started_at).getTime();
|
|
2812
|
-
debug("branches", "Recovered active branch from DB", { branchId: activeBranch.id });
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
/**
|
|
2816
|
-
* Process a new observation through the boundary detection state machine.
|
|
2817
|
-
* Called from HaikuProcessor after classification (Step 1.6).
|
|
2818
|
-
*/
|
|
2819
|
-
processObservation(obs) {
|
|
2820
|
-
const now = Date.now();
|
|
2821
|
-
const obsTime = new Date(obs.createdAt).getTime();
|
|
2822
|
-
const toolName = this.extractToolName(obs.source);
|
|
2823
|
-
const boundary = this.detectBoundary(obs, obsTime);
|
|
2824
|
-
if (boundary) {
|
|
2825
|
-
if (this.state === "tracking" && this.activeBranchId) this.completeBranch();
|
|
2826
|
-
this.startBranch(boundary, obs);
|
|
2827
|
-
} else if (this.state === "idle") this.startBranch("session_start", obs);
|
|
2828
|
-
if (this.activeBranchId) {
|
|
2829
|
-
const arcStage = inferArcStage(this.toolPattern, obs.classification);
|
|
2830
|
-
if (toolName) {
|
|
2831
|
-
this.toolPattern[toolName] = (this.toolPattern[toolName] ?? 0) + 1;
|
|
2832
|
-
this.repo.updateToolPattern(this.activeBranchId, this.toolPattern);
|
|
2833
|
-
}
|
|
2834
|
-
this.repo.addObservation(this.activeBranchId, obs.id, toolName, arcStage);
|
|
2835
|
-
const newStage = inferArcStage(this.toolPattern, obs.classification);
|
|
2836
|
-
this.repo.updateArcStage(this.activeBranchId, newStage);
|
|
2837
|
-
}
|
|
2838
|
-
this.lastObservationTime = obsTime || now;
|
|
2839
|
-
this.activeProjectHash = obs.projectHash;
|
|
2840
|
-
this.activeSessionId = obs.sessionId ?? this.activeSessionId;
|
|
2841
|
-
}
|
|
2842
|
-
/**
|
|
2843
|
-
* Notify the tracker of a topic shift (from TopicShiftHandler).
|
|
2844
|
-
*/
|
|
2845
|
-
onTopicShift(observationId) {
|
|
2846
|
-
if (this.state === "tracking" && this.activeBranchId) {
|
|
2847
|
-
this.completeBranch();
|
|
2848
|
-
debug("branches", "Topic shift boundary detected", { observationId });
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
/**
|
|
2852
|
-
* Link the active branch to a debug path (when PathTracker activates).
|
|
2853
|
-
*/
|
|
2854
|
-
linkDebugPath(debugPathId) {
|
|
2855
|
-
if (this.activeBranchId) {
|
|
2856
|
-
this.repo.linkDebugPath(this.activeBranchId, debugPathId);
|
|
2857
|
-
debug("branches", "Linked debug path to branch", {
|
|
2858
|
-
branchId: this.activeBranchId,
|
|
2859
|
-
debugPathId
|
|
2860
|
-
});
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
/**
|
|
2864
|
-
* Get the active branch ID (for external callers).
|
|
2865
|
-
*/
|
|
2866
|
-
getActiveBranchId() {
|
|
2867
|
-
return this.activeBranchId;
|
|
2868
|
-
}
|
|
2869
|
-
/**
|
|
2870
|
-
* Run periodic maintenance tasks:
|
|
2871
|
-
* - Classify branches with 3+ observations via Haiku
|
|
2872
|
-
* - Generate summaries for recently completed branches
|
|
2873
|
-
* - Auto-abandon stale branches (>24h)
|
|
2874
|
-
* - Link branches to debug paths
|
|
2875
|
-
*/
|
|
2876
|
-
async runMaintenance() {
|
|
2877
|
-
try {
|
|
2878
|
-
primeFromRegistry(this.db, this.projectHash);
|
|
2879
|
-
const stale = this.repo.findStaleBranches();
|
|
2880
|
-
for (const branch of stale) {
|
|
2881
|
-
this.repo.abandonBranch(branch.id);
|
|
2882
|
-
if (this.activeBranchId === branch.id) {
|
|
2883
|
-
this.state = "idle";
|
|
2884
|
-
this.activeBranchId = null;
|
|
2885
|
-
this.toolPattern = {};
|
|
2886
|
-
}
|
|
2887
|
-
debug("branches", "Auto-abandoned stale branch", { branchId: branch.id });
|
|
2888
|
-
}
|
|
2889
|
-
if (isHaikuEnabled()) {
|
|
2890
|
-
const unclassified = this.repo.findUnclassifiedBranches(3);
|
|
2891
|
-
for (const branch of unclassified) try {
|
|
2892
|
-
const observations = this.repo.getObservations(branch.id);
|
|
2893
|
-
const obsRepo = new ObservationRepository(this.db, branch.project_hash);
|
|
2894
|
-
const texts = observations.map((bo) => {
|
|
2895
|
-
const obs = obsRepo.getById(bo.observation_id);
|
|
2896
|
-
return obs ? obs.title ? `${obs.title}: ${obs.content}` : obs.content : null;
|
|
2897
|
-
}).filter((t) => t !== null);
|
|
2898
|
-
if (texts.length === 0) continue;
|
|
2899
|
-
const result = await classifyBranchWithHaiku(texts, branch.tool_pattern);
|
|
2900
|
-
this.repo.updateClassification(branch.id, result.branch_type, result.title);
|
|
2901
|
-
debug("branches", "Branch classified", {
|
|
2902
|
-
branchId: branch.id,
|
|
2903
|
-
type: result.branch_type,
|
|
2904
|
-
title: result.title
|
|
2905
|
-
});
|
|
2906
|
-
} catch (err) {
|
|
2907
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2908
|
-
debug("branches", "Branch classification failed (non-fatal)", {
|
|
2909
|
-
branchId: branch.id,
|
|
2910
|
-
error: msg
|
|
2911
|
-
});
|
|
2912
|
-
}
|
|
2913
|
-
const unsummarized = this.repo.findRecentCompletedUnsummarized(2);
|
|
2914
|
-
for (const branch of unsummarized) try {
|
|
2915
|
-
const observations = this.repo.getObservations(branch.id);
|
|
2916
|
-
const obsRepo = new ObservationRepository(this.db, branch.project_hash);
|
|
2917
|
-
const texts = observations.map((bo) => {
|
|
2918
|
-
const obs = obsRepo.getById(bo.observation_id);
|
|
2919
|
-
return obs ? obs.title ? `${obs.title}: ${obs.content}` : obs.content : null;
|
|
2920
|
-
}).filter((t) => t !== null);
|
|
2921
|
-
if (texts.length === 0) continue;
|
|
2922
|
-
const result = await summarizeBranchWithHaiku(branch.title ?? "Untitled", branch.branch_type, texts);
|
|
2923
|
-
this.repo.updateSummary(branch.id, result.summary);
|
|
2924
|
-
debug("branches", "Branch summarized", { branchId: branch.id });
|
|
2925
|
-
} catch (err) {
|
|
2926
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2927
|
-
debug("branches", "Branch summarization failed (non-fatal)", {
|
|
2928
|
-
branchId: branch.id,
|
|
2929
|
-
error: msg
|
|
2930
|
-
});
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
|
-
} catch (err) {
|
|
2934
|
-
debug("branches", "Maintenance error (non-fatal)", { error: err instanceof Error ? err.message : String(err) });
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
detectBoundary(obs, obsTime) {
|
|
2938
|
-
if (this.activeProjectHash && obs.projectHash !== this.activeProjectHash) return "project_switch";
|
|
2939
|
-
if (this.activeSessionId && obs.sessionId && obs.sessionId !== this.activeSessionId) return "session_start";
|
|
2940
|
-
if (this.lastObservationTime > 0) {
|
|
2941
|
-
if (obsTime - this.lastObservationTime > TIME_GAP_MS) return "time_gap";
|
|
2942
|
-
}
|
|
2943
|
-
return null;
|
|
2944
|
-
}
|
|
2945
|
-
startBranch(triggerSource, obs) {
|
|
2946
|
-
const branch = this.repo.createBranch(obs.sessionId ?? null, triggerSource, obs.id);
|
|
2947
|
-
this.state = "tracking";
|
|
2948
|
-
this.activeBranchId = branch.id;
|
|
2949
|
-
this.toolPattern = {};
|
|
2950
|
-
debug("branches", "New branch started", {
|
|
2951
|
-
branchId: branch.id,
|
|
2952
|
-
trigger: triggerSource
|
|
2953
|
-
});
|
|
2954
|
-
}
|
|
2955
|
-
completeBranch() {
|
|
2956
|
-
if (!this.activeBranchId) return;
|
|
2957
|
-
this.repo.completeBranch(this.activeBranchId);
|
|
2958
|
-
debug("branches", "Branch completed", { branchId: this.activeBranchId });
|
|
2959
|
-
this.state = "idle";
|
|
2960
|
-
this.activeBranchId = null;
|
|
2961
|
-
this.toolPattern = {};
|
|
2962
|
-
}
|
|
2963
|
-
extractToolName(source) {
|
|
2964
|
-
if (source.startsWith("hook:")) return source.slice(5);
|
|
2965
|
-
if (source.startsWith("mcp:")) return source.slice(4);
|
|
2966
|
-
return null;
|
|
2967
|
-
}
|
|
2968
|
-
};
|
|
1816
|
+
lines.push("## Debug Paths");
|
|
1817
|
+
lines.push("");
|
|
1818
|
+
lines.push("| ID (short) | Status | Trigger | Started | Resolved |");
|
|
1819
|
+
lines.push("|------------|--------|---------|---------|----------|");
|
|
1820
|
+
for (const p of paths) {
|
|
1821
|
+
const shortId = p.id.slice(0, 8);
|
|
1822
|
+
const trigger = p.trigger_summary.length > 50 ? p.trigger_summary.slice(0, 50) + "..." : p.trigger_summary;
|
|
1823
|
+
const resolved = p.resolved_at ?? "-";
|
|
1824
|
+
lines.push(`| ${shortId} | ${p.status} | ${trigger} | ${p.started_at} | ${resolved} |`);
|
|
1825
|
+
}
|
|
1826
|
+
return withNotifications(lines.join("\n"));
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1829
|
+
debug("mcp", "path_list: error", { error: message });
|
|
1830
|
+
return errorResponse(`path_list error: ${message}`);
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
2969
1834
|
|
|
2970
1835
|
//#endregion
|
|
2971
1836
|
//#region src/analysis/worker-bridge.ts
|
|
@@ -4701,6 +3566,96 @@ var CurationAgent = class {
|
|
|
4701
3566
|
}
|
|
4702
3567
|
};
|
|
4703
3568
|
|
|
3569
|
+
//#endregion
|
|
3570
|
+
//#region src/config/haiku-config.ts
|
|
3571
|
+
function loadHaikuConfig() {
|
|
3572
|
+
return {
|
|
3573
|
+
model: "claude-haiku-4-5-20251001",
|
|
3574
|
+
maxTokensPerCall: 1024
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
//#endregion
|
|
3579
|
+
//#region src/intelligence/haiku-client.ts
|
|
3580
|
+
/**
|
|
3581
|
+
* Shared Haiku client using Claude Agent SDK V2 session.
|
|
3582
|
+
*
|
|
3583
|
+
* Routes Haiku calls through the user's Claude Code subscription
|
|
3584
|
+
* instead of requiring a separate API key. Uses a persistent session
|
|
3585
|
+
* to avoid 12s cold-start overhead on sequential calls.
|
|
3586
|
+
*
|
|
3587
|
+
* Provides the core infrastructure for all Haiku agent modules:
|
|
3588
|
+
* - callHaiku() helper for structured prompt/response calls
|
|
3589
|
+
* - extractJsonFromResponse() for defensive JSON parsing
|
|
3590
|
+
* - Session reuse across batch processing cycles
|
|
3591
|
+
*/
|
|
3592
|
+
let _session = null;
|
|
3593
|
+
function getOrCreateSession() {
|
|
3594
|
+
if (!_session) _session = unstable_v2_createSession({
|
|
3595
|
+
model: loadHaikuConfig().model,
|
|
3596
|
+
permissionMode: "bypassPermissions",
|
|
3597
|
+
allowedTools: []
|
|
3598
|
+
});
|
|
3599
|
+
return _session;
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Returns whether Haiku enrichment is available.
|
|
3603
|
+
* Always true with subscription auth -- no API key check needed.
|
|
3604
|
+
*/
|
|
3605
|
+
function isHaikuEnabled() {
|
|
3606
|
+
return true;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Calls Haiku with a system prompt and user content.
|
|
3610
|
+
* Returns the text content from the response.
|
|
3611
|
+
*
|
|
3612
|
+
* Uses a persistent V2 session to avoid cold-start overhead on sequential calls.
|
|
3613
|
+
* System prompt is embedded in the user message since session-level systemPrompt
|
|
3614
|
+
* is set at creation time and we need different prompts per agent.
|
|
3615
|
+
*
|
|
3616
|
+
* @param systemPrompt - Instructions for the model
|
|
3617
|
+
* @param userContent - The content to process
|
|
3618
|
+
* @param _maxTokens - Kept for signature compatibility (unused -- Agent SDK constrains output via prompts)
|
|
3619
|
+
* @throws Error if the Haiku call fails or session expires
|
|
3620
|
+
*/
|
|
3621
|
+
async function callHaiku(systemPrompt, userContent, _maxTokens) {
|
|
3622
|
+
const session = getOrCreateSession();
|
|
3623
|
+
const fullPrompt = `<instructions>\n${systemPrompt}\n</instructions>\n\n${userContent}`;
|
|
3624
|
+
try {
|
|
3625
|
+
await session.send(fullPrompt);
|
|
3626
|
+
for await (const msg of session.stream()) if (msg.type === "result") {
|
|
3627
|
+
if (msg.subtype === "success") return msg.result;
|
|
3628
|
+
const errorMsg = ("errors" in msg ? msg.errors : void 0)?.join(", ") ?? msg.subtype;
|
|
3629
|
+
throw new Error(`Haiku call failed: ${errorMsg}`);
|
|
3630
|
+
}
|
|
3631
|
+
return "";
|
|
3632
|
+
} catch (error) {
|
|
3633
|
+
try {
|
|
3634
|
+
_session?.close();
|
|
3635
|
+
} catch {}
|
|
3636
|
+
_session = null;
|
|
3637
|
+
throw error;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Defensive JSON extraction from Haiku response text.
|
|
3642
|
+
*
|
|
3643
|
+
* Handles common LLM response quirks:
|
|
3644
|
+
* - Markdown code fences (```json ... ```)
|
|
3645
|
+
* - Explanatory text before/after JSON
|
|
3646
|
+
* - Both array and object JSON shapes
|
|
3647
|
+
*
|
|
3648
|
+
* @throws Error if no JSON structure found in text
|
|
3649
|
+
*/
|
|
3650
|
+
function extractJsonFromResponse(text) {
|
|
3651
|
+
const cleaned = text.replace(/```json\s*/g, "").replace(/```\s*/g, "");
|
|
3652
|
+
const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
|
|
3653
|
+
if (arrayMatch) return JSON.parse(arrayMatch[0]);
|
|
3654
|
+
const objMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
3655
|
+
if (objMatch) return JSON.parse(objMatch[0]);
|
|
3656
|
+
throw new Error("No JSON found in Haiku response");
|
|
3657
|
+
}
|
|
3658
|
+
|
|
4704
3659
|
//#endregion
|
|
4705
3660
|
//#region src/intelligence/haiku-classifier-agent.ts
|
|
4706
3661
|
/**
|
|
@@ -4751,10 +3706,10 @@ For each observation, determine:
|
|
|
4751
3706
|
- "problem": error, bug, failure, or obstacle encountered
|
|
4752
3707
|
- "solution": fix, resolution, workaround, or decision that resolved something
|
|
4753
3708
|
|
|
4754
|
-
3. debug_signal (always, even for noise): Is this related to
|
|
4755
|
-
- is_error:
|
|
3709
|
+
3. debug_signal (always, even for noise): Is this related to debugging?
|
|
3710
|
+
- is_error: Does this contain an error message, test failure, build failure, or exception?
|
|
4756
3711
|
- is_resolution: Does this indicate a successful fix, passing test, or resolved error?
|
|
4757
|
-
- waypoint_hint: If debug-related, what type? "error" (an
|
|
3712
|
+
- waypoint_hint: If debug-related, what type? "error" (hit an error), "attempt" (trying a fix), "failure" (fix didn't work), "success" (something passed), "pivot" (changing approach), "revert" (undoing a change), "discovery" (learned something), "resolution" (final fix). null if not debug-related.
|
|
4758
3713
|
- confidence: 0.0-1.0 how confident this is debug activity
|
|
4759
3714
|
|
|
4760
3715
|
Return JSON: {"signal": "noise"|"signal", "classification": "discovery"|"problem"|"solution"|null, "reason": "brief", "debug_signal": {"is_error": bool, "is_resolution": bool, "waypoint_hint": "type"|null, "confidence": 0.0-1.0}|null}
|
|
@@ -5162,7 +4117,6 @@ var HaikuProcessor = class {
|
|
|
5162
4117
|
batchSize;
|
|
5163
4118
|
concurrency;
|
|
5164
4119
|
pathTracker;
|
|
5165
|
-
branchTracker;
|
|
5166
4120
|
timer = null;
|
|
5167
4121
|
constructor(db, projectHash, opts) {
|
|
5168
4122
|
this.db = db;
|
|
@@ -5171,7 +4125,6 @@ var HaikuProcessor = class {
|
|
|
5171
4125
|
this.batchSize = opts?.batchSize ?? 10;
|
|
5172
4126
|
this.concurrency = opts?.concurrency ?? 3;
|
|
5173
4127
|
this.pathTracker = opts?.pathTracker ?? null;
|
|
5174
|
-
this.branchTracker = opts?.branchTracker ?? null;
|
|
5175
4128
|
}
|
|
5176
4129
|
start() {
|
|
5177
4130
|
if (this.timer) return;
|
|
@@ -5195,30 +4148,16 @@ var HaikuProcessor = class {
|
|
|
5195
4148
|
}
|
|
5196
4149
|
async processOnce() {
|
|
5197
4150
|
if (!isHaikuEnabled()) return;
|
|
5198
|
-
const
|
|
4151
|
+
const repo = new ObservationRepository(this.db, this.projectHash);
|
|
4152
|
+
const unclassified = repo.listUnclassified(this.batchSize);
|
|
5199
4153
|
if (unclassified.length === 0) return;
|
|
5200
4154
|
debug("haiku", "Processing unclassified observations", { count: unclassified.length });
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
if (!byProject.has(hash)) byProject.set(hash, []);
|
|
5205
|
-
byProject.get(hash).push(obs);
|
|
5206
|
-
}
|
|
5207
|
-
for (const [hash, projectObs] of byProject) {
|
|
5208
|
-
const repo = new ObservationRepository(this.db, hash);
|
|
5209
|
-
for (let i = 0; i < projectObs.length; i += this.concurrency) {
|
|
5210
|
-
const batch = projectObs.slice(i, i + this.concurrency);
|
|
5211
|
-
await Promise.all(batch.map((obs) => this.processOne(obs, repo, hash)));
|
|
5212
|
-
}
|
|
5213
|
-
}
|
|
5214
|
-
if (this.branchTracker) try {
|
|
5215
|
-
await this.branchTracker.runMaintenance();
|
|
5216
|
-
} catch (err) {
|
|
5217
|
-
debug("haiku", "Branch maintenance error (non-fatal)", { error: err instanceof Error ? err.message : String(err) });
|
|
4155
|
+
for (let i = 0; i < unclassified.length; i += this.concurrency) {
|
|
4156
|
+
const batch = unclassified.slice(i, i + this.concurrency);
|
|
4157
|
+
await Promise.all(batch.map((obs) => this.processOne(obs, repo)));
|
|
5218
4158
|
}
|
|
5219
4159
|
}
|
|
5220
|
-
async processOne(obs, repo
|
|
5221
|
-
const projectHash = obsProjectHash ?? this.projectHash;
|
|
4160
|
+
async processOne(obs, repo) {
|
|
5222
4161
|
try {
|
|
5223
4162
|
let classification;
|
|
5224
4163
|
try {
|
|
@@ -5232,23 +4171,6 @@ var HaikuProcessor = class {
|
|
|
5232
4171
|
error: msg
|
|
5233
4172
|
});
|
|
5234
4173
|
}
|
|
5235
|
-
if (this.branchTracker) try {
|
|
5236
|
-
this.branchTracker.processObservation({
|
|
5237
|
-
id: obs.id,
|
|
5238
|
-
content: obs.content,
|
|
5239
|
-
source: obs.source,
|
|
5240
|
-
projectHash: obsProjectHash ?? this.projectHash,
|
|
5241
|
-
sessionId: void 0,
|
|
5242
|
-
classification: result.classification,
|
|
5243
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5244
|
-
});
|
|
5245
|
-
} catch (branchErr) {
|
|
5246
|
-
const msg = branchErr instanceof Error ? branchErr.message : String(branchErr);
|
|
5247
|
-
debug("haiku", "Branch tracking failed (non-fatal)", {
|
|
5248
|
-
id: obs.id,
|
|
5249
|
-
error: msg
|
|
5250
|
-
});
|
|
5251
|
-
}
|
|
5252
4174
|
if (result.signal === "noise") {
|
|
5253
4175
|
repo.updateClassification(obs.id, "noise");
|
|
5254
4176
|
repo.softDelete(obs.id);
|
|
@@ -5290,7 +4212,7 @@ var HaikuProcessor = class {
|
|
|
5290
4212
|
name: entity.name,
|
|
5291
4213
|
metadata: { confidence: entity.confidence },
|
|
5292
4214
|
observation_ids: [String(obs.id)],
|
|
5293
|
-
project_hash: projectHash
|
|
4215
|
+
project_hash: this.projectHash
|
|
5294
4216
|
});
|
|
5295
4217
|
persistedNodes.push(node);
|
|
5296
4218
|
} catch {
|
|
@@ -5302,8 +4224,7 @@ var HaikuProcessor = class {
|
|
|
5302
4224
|
label: node.name,
|
|
5303
4225
|
type: node.type,
|
|
5304
4226
|
observationCount: 1,
|
|
5305
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5306
|
-
projectHash
|
|
4227
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5307
4228
|
});
|
|
5308
4229
|
debug("haiku", "Entities persisted", {
|
|
5309
4230
|
id: obs.id,
|
|
@@ -5328,7 +4249,7 @@ var HaikuProcessor = class {
|
|
|
5328
4249
|
type: rel.type,
|
|
5329
4250
|
weight: rel.confidence,
|
|
5330
4251
|
metadata: { source: "haiku" },
|
|
5331
|
-
project_hash: projectHash
|
|
4252
|
+
project_hash: this.projectHash
|
|
5332
4253
|
});
|
|
5333
4254
|
affectedNodeIds.add(sourceNode.id);
|
|
5334
4255
|
affectedNodeIds.add(targetNode.id);
|
|
@@ -5725,7 +4646,7 @@ apiRoutes.get("/graph", (c) => {
|
|
|
5725
4646
|
edgeRows = [];
|
|
5726
4647
|
}
|
|
5727
4648
|
const nodeIdSet = new Set(nodes.map((n) => n.id));
|
|
5728
|
-
const edges = edgeRows.filter((e) => nodeIdSet.has(e.source_id) && nodeIdSet.has(e.target_id)).map((row) => ({
|
|
4649
|
+
const edges = (typeFilter ? edgeRows.filter((e) => nodeIdSet.has(e.source_id) && nodeIdSet.has(e.target_id)) : edgeRows).map((row) => ({
|
|
5729
4650
|
id: row.id,
|
|
5730
4651
|
source: row.source_id,
|
|
5731
4652
|
target: row.target_id,
|
|
@@ -6303,236 +5224,6 @@ apiRoutes.get("/paths/:id", (c) => {
|
|
|
6303
5224
|
}
|
|
6304
5225
|
});
|
|
6305
5226
|
/**
|
|
6306
|
-
* GET /api/tools
|
|
6307
|
-
*
|
|
6308
|
-
* Returns all tools from tool_registry with usage stats.
|
|
6309
|
-
*/
|
|
6310
|
-
apiRoutes.get("/tools", (c) => {
|
|
6311
|
-
const db = getDb$1(c);
|
|
6312
|
-
let tools = [];
|
|
6313
|
-
try {
|
|
6314
|
-
tools = db.prepare(`
|
|
6315
|
-
SELECT id, name, tool_type, scope, status, usage_count, server_name, description, last_used_at, discovered_at
|
|
6316
|
-
FROM tool_registry
|
|
6317
|
-
ORDER BY usage_count DESC, discovered_at DESC
|
|
6318
|
-
`).all();
|
|
6319
|
-
} catch {}
|
|
6320
|
-
return c.json({ tools: tools.map((t) => ({
|
|
6321
|
-
id: t.id,
|
|
6322
|
-
name: t.name,
|
|
6323
|
-
toolType: t.tool_type,
|
|
6324
|
-
scope: t.scope,
|
|
6325
|
-
status: t.status,
|
|
6326
|
-
usageCount: t.usage_count,
|
|
6327
|
-
serverName: t.server_name,
|
|
6328
|
-
description: t.description,
|
|
6329
|
-
lastUsedAt: t.last_used_at,
|
|
6330
|
-
discoveredAt: t.discovered_at
|
|
6331
|
-
})) });
|
|
6332
|
-
});
|
|
6333
|
-
/**
|
|
6334
|
-
* GET /api/tools/flows
|
|
6335
|
-
*
|
|
6336
|
-
* Returns edges for the tool topology graph:
|
|
6337
|
-
* 1. Pre-computed routing_patterns (preceding_tools -> target_tool)
|
|
6338
|
-
* 2. Pairwise co-occurrence from tool_usage_events session sequences
|
|
6339
|
-
*/
|
|
6340
|
-
apiRoutes.get("/tools/flows", (c) => {
|
|
6341
|
-
const db = getDb$1(c);
|
|
6342
|
-
const projectFilter = getProjectHash$2(c);
|
|
6343
|
-
const edges = [];
|
|
6344
|
-
const edgeKey = /* @__PURE__ */ new Set();
|
|
6345
|
-
try {
|
|
6346
|
-
let sql = "SELECT target_tool, preceding_tools, frequency FROM routing_patterns";
|
|
6347
|
-
const params = [];
|
|
6348
|
-
if (projectFilter) {
|
|
6349
|
-
sql += " WHERE project_hash = ?";
|
|
6350
|
-
params.push(projectFilter);
|
|
6351
|
-
}
|
|
6352
|
-
sql += " ORDER BY frequency DESC LIMIT 200";
|
|
6353
|
-
const rows = db.prepare(sql).all(...params);
|
|
6354
|
-
for (const row of rows) {
|
|
6355
|
-
let preceding;
|
|
6356
|
-
try {
|
|
6357
|
-
preceding = JSON.parse(row.preceding_tools);
|
|
6358
|
-
} catch {
|
|
6359
|
-
preceding = [];
|
|
6360
|
-
}
|
|
6361
|
-
for (const src of preceding) {
|
|
6362
|
-
const key = src + "->" + row.target_tool;
|
|
6363
|
-
if (!edgeKey.has(key)) {
|
|
6364
|
-
edgeKey.add(key);
|
|
6365
|
-
edges.push({
|
|
6366
|
-
source: src,
|
|
6367
|
-
target: row.target_tool,
|
|
6368
|
-
frequency: row.frequency,
|
|
6369
|
-
edgeType: "pattern"
|
|
6370
|
-
});
|
|
6371
|
-
}
|
|
6372
|
-
}
|
|
6373
|
-
}
|
|
6374
|
-
} catch {}
|
|
6375
|
-
try {
|
|
6376
|
-
let sql = `
|
|
6377
|
-
SELECT session_id, tool_name, created_at
|
|
6378
|
-
FROM tool_usage_events
|
|
6379
|
-
WHERE session_id IS NOT NULL
|
|
6380
|
-
`;
|
|
6381
|
-
const params = [];
|
|
6382
|
-
if (projectFilter) {
|
|
6383
|
-
sql += " AND project_hash = ?";
|
|
6384
|
-
params.push(projectFilter);
|
|
6385
|
-
}
|
|
6386
|
-
sql += " ORDER BY session_id, created_at ASC LIMIT 5000";
|
|
6387
|
-
const rows = db.prepare(sql).all(...params);
|
|
6388
|
-
const pairFreq = /* @__PURE__ */ new Map();
|
|
6389
|
-
let prevSession = "";
|
|
6390
|
-
let prevTool = "";
|
|
6391
|
-
for (const row of rows) {
|
|
6392
|
-
if (row.session_id === prevSession && prevTool && prevTool !== row.tool_name) {
|
|
6393
|
-
const key = prevTool + "->" + row.tool_name;
|
|
6394
|
-
pairFreq.set(key, (pairFreq.get(key) || 0) + 1);
|
|
6395
|
-
}
|
|
6396
|
-
prevSession = row.session_id;
|
|
6397
|
-
prevTool = row.tool_name;
|
|
6398
|
-
}
|
|
6399
|
-
for (const [key, freq] of pairFreq) if (!edgeKey.has(key) && freq >= 2) {
|
|
6400
|
-
edgeKey.add(key);
|
|
6401
|
-
const [source, target] = key.split("->");
|
|
6402
|
-
edges.push({
|
|
6403
|
-
source,
|
|
6404
|
-
target,
|
|
6405
|
-
frequency: freq,
|
|
6406
|
-
edgeType: "session"
|
|
6407
|
-
});
|
|
6408
|
-
}
|
|
6409
|
-
} catch {}
|
|
6410
|
-
return c.json({ edges });
|
|
6411
|
-
});
|
|
6412
|
-
/**
|
|
6413
|
-
* GET /api/tools/:name/stats
|
|
6414
|
-
*
|
|
6415
|
-
* Returns detailed stats for a single tool.
|
|
6416
|
-
*/
|
|
6417
|
-
apiRoutes.get("/tools/:name/stats", (c) => {
|
|
6418
|
-
const db = getDb$1(c);
|
|
6419
|
-
const toolName = c.req.param("name");
|
|
6420
|
-
const projectFilter = getProjectHash$2(c);
|
|
6421
|
-
let tool;
|
|
6422
|
-
try {
|
|
6423
|
-
tool = db.prepare("SELECT id, name, tool_type, scope, status, usage_count, server_name, description, last_used_at, discovered_at FROM tool_registry WHERE name = ? ORDER BY usage_count DESC LIMIT 1").get(toolName);
|
|
6424
|
-
} catch {}
|
|
6425
|
-
if (!tool) return c.json({ error: "Tool not found" }, 404);
|
|
6426
|
-
let successRate = null;
|
|
6427
|
-
let totalEvents = 0;
|
|
6428
|
-
try {
|
|
6429
|
-
let sql = "SELECT success FROM tool_usage_events WHERE tool_name = ?";
|
|
6430
|
-
const params = [toolName];
|
|
6431
|
-
if (projectFilter) {
|
|
6432
|
-
sql += " AND project_hash = ?";
|
|
6433
|
-
params.push(projectFilter);
|
|
6434
|
-
}
|
|
6435
|
-
sql += " ORDER BY created_at DESC LIMIT 50";
|
|
6436
|
-
const events = db.prepare(sql).all(...params);
|
|
6437
|
-
totalEvents = events.length;
|
|
6438
|
-
if (totalEvents > 0) successRate = events.filter((e) => e.success === 1).length / totalEvents;
|
|
6439
|
-
} catch {}
|
|
6440
|
-
let sessionsUsedIn = 0;
|
|
6441
|
-
try {
|
|
6442
|
-
let sql = "SELECT COUNT(DISTINCT session_id) as cnt FROM tool_usage_events WHERE tool_name = ? AND session_id IS NOT NULL";
|
|
6443
|
-
const params = [toolName];
|
|
6444
|
-
if (projectFilter) {
|
|
6445
|
-
sql += " AND project_hash = ?";
|
|
6446
|
-
params.push(projectFilter);
|
|
6447
|
-
}
|
|
6448
|
-
sessionsUsedIn = db.prepare(sql).get(...params)?.cnt ?? 0;
|
|
6449
|
-
} catch {}
|
|
6450
|
-
let coOccurring = [];
|
|
6451
|
-
try {
|
|
6452
|
-
let sql = `
|
|
6453
|
-
SELECT e2.tool_name as name, COUNT(*) as count
|
|
6454
|
-
FROM tool_usage_events e1
|
|
6455
|
-
JOIN tool_usage_events e2
|
|
6456
|
-
ON e1.session_id = e2.session_id AND e1.tool_name != e2.tool_name
|
|
6457
|
-
WHERE e1.tool_name = ? AND e1.session_id IS NOT NULL
|
|
6458
|
-
`;
|
|
6459
|
-
const params = [toolName];
|
|
6460
|
-
if (projectFilter) {
|
|
6461
|
-
sql += " AND e1.project_hash = ?";
|
|
6462
|
-
params.push(projectFilter);
|
|
6463
|
-
}
|
|
6464
|
-
sql += " GROUP BY e2.tool_name ORDER BY count DESC LIMIT 10";
|
|
6465
|
-
coOccurring = db.prepare(sql).all(...params);
|
|
6466
|
-
} catch {}
|
|
6467
|
-
return c.json({
|
|
6468
|
-
tool: {
|
|
6469
|
-
id: tool.id,
|
|
6470
|
-
name: tool.name,
|
|
6471
|
-
toolType: tool.tool_type,
|
|
6472
|
-
scope: tool.scope,
|
|
6473
|
-
status: tool.status,
|
|
6474
|
-
usageCount: tool.usage_count,
|
|
6475
|
-
serverName: tool.server_name,
|
|
6476
|
-
description: tool.description,
|
|
6477
|
-
lastUsedAt: tool.last_used_at,
|
|
6478
|
-
discoveredAt: tool.discovered_at
|
|
6479
|
-
},
|
|
6480
|
-
successRate,
|
|
6481
|
-
totalEvents,
|
|
6482
|
-
sessionsUsedIn,
|
|
6483
|
-
coOccurring
|
|
6484
|
-
});
|
|
6485
|
-
});
|
|
6486
|
-
/**
|
|
6487
|
-
* GET /api/tools/sessions
|
|
6488
|
-
*
|
|
6489
|
-
* Returns recent session tool sequences for the flow strip.
|
|
6490
|
-
*/
|
|
6491
|
-
apiRoutes.get("/tools/sessions", (c) => {
|
|
6492
|
-
const db = getDb$1(c);
|
|
6493
|
-
const projectFilter = getProjectHash$2(c);
|
|
6494
|
-
const limitStr = c.req.query("limit");
|
|
6495
|
-
const limit = limitStr ? Math.min(parseInt(limitStr, 10) || 10, 30) : 10;
|
|
6496
|
-
let sessions = [];
|
|
6497
|
-
try {
|
|
6498
|
-
let sessionSql = `
|
|
6499
|
-
SELECT DISTINCT session_id FROM tool_usage_events
|
|
6500
|
-
WHERE session_id IS NOT NULL
|
|
6501
|
-
`;
|
|
6502
|
-
const sessionParams = [];
|
|
6503
|
-
if (projectFilter) {
|
|
6504
|
-
sessionSql += " AND project_hash = ?";
|
|
6505
|
-
sessionParams.push(projectFilter);
|
|
6506
|
-
}
|
|
6507
|
-
sessionSql += " ORDER BY created_at DESC LIMIT ?";
|
|
6508
|
-
sessionParams.push(limit);
|
|
6509
|
-
const sessionIds = db.prepare(sessionSql).all(...sessionParams);
|
|
6510
|
-
if (sessionIds.length > 0) {
|
|
6511
|
-
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
6512
|
-
const ids = sessionIds.map((s) => s.session_id);
|
|
6513
|
-
const eventRows = db.prepare(`
|
|
6514
|
-
SELECT session_id, tool_name, created_at
|
|
6515
|
-
FROM tool_usage_events
|
|
6516
|
-
WHERE session_id IN (${placeholders})
|
|
6517
|
-
ORDER BY session_id, created_at ASC
|
|
6518
|
-
`).all(...ids);
|
|
6519
|
-
const sessionMap = /* @__PURE__ */ new Map();
|
|
6520
|
-
for (const row of eventRows) {
|
|
6521
|
-
if (!sessionMap.has(row.session_id)) sessionMap.set(row.session_id, []);
|
|
6522
|
-
sessionMap.get(row.session_id).push({
|
|
6523
|
-
name: row.tool_name,
|
|
6524
|
-
time: row.created_at
|
|
6525
|
-
});
|
|
6526
|
-
}
|
|
6527
|
-
sessions = sessionIds.filter((s) => sessionMap.has(s.session_id)).map((s) => ({
|
|
6528
|
-
sessionId: s.session_id,
|
|
6529
|
-
tools: sessionMap.get(s.session_id)
|
|
6530
|
-
}));
|
|
6531
|
-
}
|
|
6532
|
-
} catch {}
|
|
6533
|
-
return c.json({ sessions });
|
|
6534
|
-
});
|
|
6535
|
-
/**
|
|
6536
5227
|
* Finds connected components in the graph via BFS.
|
|
6537
5228
|
* Shared by /api/graph/analysis and /api/graph/communities.
|
|
6538
5229
|
*/
|
|
@@ -6626,14 +5317,6 @@ function safeParseJson(json) {
|
|
|
6626
5317
|
*
|
|
6627
5318
|
* @module web/routes/admin
|
|
6628
5319
|
*/
|
|
6629
|
-
const __dirname = dirname(fileURLToPath$1(import.meta.url));
|
|
6630
|
-
const LAMINARK_VERSION = (() => {
|
|
6631
|
-
try {
|
|
6632
|
-
return JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")).version || "unknown";
|
|
6633
|
-
} catch {
|
|
6634
|
-
return "unknown";
|
|
6635
|
-
}
|
|
6636
|
-
})();
|
|
6637
5320
|
function getDb(c) {
|
|
6638
5321
|
return c.get("db");
|
|
6639
5322
|
}
|
|
@@ -6708,51 +5391,6 @@ adminRoutes.get("/stats", (c) => {
|
|
|
6708
5391
|
});
|
|
6709
5392
|
});
|
|
6710
5393
|
/**
|
|
6711
|
-
* GET /api/admin/system
|
|
6712
|
-
*
|
|
6713
|
-
* Returns server-scoped system info (not project-scoped).
|
|
6714
|
-
*/
|
|
6715
|
-
adminRoutes.get("/system", (c) => {
|
|
6716
|
-
const db = getDb(c);
|
|
6717
|
-
const mem = process.memoryUsage();
|
|
6718
|
-
let dbSizeBytes = 0;
|
|
6719
|
-
let pageCount = 0;
|
|
6720
|
-
let pageSize = 4096;
|
|
6721
|
-
try {
|
|
6722
|
-
const pc = db.pragma("page_count", { simple: true });
|
|
6723
|
-
const ps = db.pragma("page_size", { simple: true });
|
|
6724
|
-
pageCount = pc;
|
|
6725
|
-
pageSize = ps;
|
|
6726
|
-
dbSizeBytes = pc * ps;
|
|
6727
|
-
} catch {}
|
|
6728
|
-
let walSizeBytes = 0;
|
|
6729
|
-
try {
|
|
6730
|
-
const dbPath = db.name;
|
|
6731
|
-
if (dbPath) {
|
|
6732
|
-
const walPath = dbPath + "-wal";
|
|
6733
|
-
if (existsSync(walPath)) walSizeBytes = statSync(walPath).size;
|
|
6734
|
-
}
|
|
6735
|
-
} catch {}
|
|
6736
|
-
return c.json({
|
|
6737
|
-
laminarkVersion: LAMINARK_VERSION,
|
|
6738
|
-
nodeVersion: process.version,
|
|
6739
|
-
platform: process.platform,
|
|
6740
|
-
arch: process.arch,
|
|
6741
|
-
uptimeSeconds: Math.floor(process.uptime()),
|
|
6742
|
-
memory: {
|
|
6743
|
-
rssBytes: mem.rss,
|
|
6744
|
-
heapUsedBytes: mem.heapUsed,
|
|
6745
|
-
heapTotalBytes: mem.heapTotal
|
|
6746
|
-
},
|
|
6747
|
-
database: {
|
|
6748
|
-
sizeBytes: dbSizeBytes,
|
|
6749
|
-
walSizeBytes,
|
|
6750
|
-
pageCount,
|
|
6751
|
-
pageSize
|
|
6752
|
-
}
|
|
6753
|
-
});
|
|
6754
|
-
});
|
|
6755
|
-
/**
|
|
6756
5394
|
* POST /api/admin/reset
|
|
6757
5395
|
*
|
|
6758
5396
|
* Hard-deletes data by group inside a transaction.
|
|
@@ -6857,66 +5495,6 @@ adminRoutes.post("/reset", async (c) => {
|
|
|
6857
5495
|
scope: scoped ? "project" : "all"
|
|
6858
5496
|
});
|
|
6859
5497
|
});
|
|
6860
|
-
adminRoutes.get("/hygiene", (c) => {
|
|
6861
|
-
const db = getDb(c);
|
|
6862
|
-
const project = getProjectHash$1(c);
|
|
6863
|
-
if (!project) return c.json({ error: "No project context available" }, 400);
|
|
6864
|
-
const tier = c.req.query("tier") || "high";
|
|
6865
|
-
const sessionId = c.req.query("session_id");
|
|
6866
|
-
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
6867
|
-
const config = loadHygieneConfig();
|
|
6868
|
-
const report = analyzeObservations(db, project, {
|
|
6869
|
-
sessionId,
|
|
6870
|
-
limit,
|
|
6871
|
-
minTier: tier === "all" ? "low" : tier,
|
|
6872
|
-
config
|
|
6873
|
-
});
|
|
6874
|
-
return c.json(report);
|
|
6875
|
-
});
|
|
6876
|
-
adminRoutes.post("/hygiene/purge", async (c) => {
|
|
6877
|
-
const db = getDb(c);
|
|
6878
|
-
const project = getProjectHash$1(c);
|
|
6879
|
-
if (!project) return c.json({ error: "No project context available" }, 400);
|
|
6880
|
-
const tier = (await c.req.json()).tier || "high";
|
|
6881
|
-
const config = loadHygieneConfig();
|
|
6882
|
-
const result = executePurge(db, project, analyzeObservations(db, project, {
|
|
6883
|
-
minTier: tier === "all" ? "low" : tier,
|
|
6884
|
-
limit: 500,
|
|
6885
|
-
config
|
|
6886
|
-
}), tier);
|
|
6887
|
-
return c.json({
|
|
6888
|
-
ok: true,
|
|
6889
|
-
observationsPurged: result.observationsPurged,
|
|
6890
|
-
orphanNodesRemoved: result.orphanNodesRemoved,
|
|
6891
|
-
tier
|
|
6892
|
-
});
|
|
6893
|
-
});
|
|
6894
|
-
adminRoutes.get("/hygiene/find", (c) => {
|
|
6895
|
-
const db = getDb(c);
|
|
6896
|
-
const project = getProjectHash$1(c);
|
|
6897
|
-
if (!project) return c.json({ error: "No project context available" }, 400);
|
|
6898
|
-
const report = findAnalysis(db, project, loadHygieneConfig());
|
|
6899
|
-
return c.json(report);
|
|
6900
|
-
});
|
|
6901
|
-
adminRoutes.get("/config/hygiene", (c) => {
|
|
6902
|
-
return c.json(loadHygieneConfig());
|
|
6903
|
-
});
|
|
6904
|
-
adminRoutes.put("/config/hygiene", async (c) => {
|
|
6905
|
-
const body = await c.req.json();
|
|
6906
|
-
const configPath = join(getConfigDir(), "hygiene.json");
|
|
6907
|
-
if (body && body.__reset === true) {
|
|
6908
|
-
try {
|
|
6909
|
-
if (existsSync(configPath)) unlinkSync(configPath);
|
|
6910
|
-
} catch {}
|
|
6911
|
-
return c.json(resetHygieneConfig());
|
|
6912
|
-
}
|
|
6913
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) return c.json({ error: "Request body must be a JSON object" }, 400);
|
|
6914
|
-
const { __reset: _, ...data } = body;
|
|
6915
|
-
writeFileSync(configPath, JSON.stringify(data, null, 2), "utf-8");
|
|
6916
|
-
const validated = loadHygieneConfig();
|
|
6917
|
-
saveHygieneConfig(validated);
|
|
6918
|
-
return c.json(validated);
|
|
6919
|
-
});
|
|
6920
5498
|
adminRoutes.get("/config/topic-detection", (c) => {
|
|
6921
5499
|
return c.json(loadTopicDetectionConfig());
|
|
6922
5500
|
});
|
|
@@ -6955,39 +5533,6 @@ adminRoutes.put("/config/graph-extraction", async (c) => {
|
|
|
6955
5533
|
writeFileSync(configPath, JSON.stringify(validated, null, 2), "utf-8");
|
|
6956
5534
|
return c.json(validated);
|
|
6957
5535
|
});
|
|
6958
|
-
adminRoutes.get("/config/cross-access", (c) => {
|
|
6959
|
-
const project = c.req.query("project");
|
|
6960
|
-
if (!project) return c.json({ error: "project query parameter is required" }, 400);
|
|
6961
|
-
return c.json(loadCrossAccessConfig(project));
|
|
6962
|
-
});
|
|
6963
|
-
adminRoutes.put("/config/cross-access", async (c) => {
|
|
6964
|
-
const project = c.req.query("project");
|
|
6965
|
-
if (!project) return c.json({ error: "project query parameter is required" }, 400);
|
|
6966
|
-
const body = await c.req.json();
|
|
6967
|
-
if (body && body.__reset === true) {
|
|
6968
|
-
resetCrossAccessConfig(project);
|
|
6969
|
-
return c.json(loadCrossAccessConfig(project));
|
|
6970
|
-
}
|
|
6971
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) return c.json({ error: "Request body must be a JSON object" }, 400);
|
|
6972
|
-
saveCrossAccessConfig(project, { readableProjects: body.readableProjects || [] });
|
|
6973
|
-
return c.json(loadCrossAccessConfig(project));
|
|
6974
|
-
});
|
|
6975
|
-
adminRoutes.get("/config/tool-verbosity", (c) => {
|
|
6976
|
-
return c.json(loadToolVerbosityConfig());
|
|
6977
|
-
});
|
|
6978
|
-
adminRoutes.put("/config/tool-verbosity", async (c) => {
|
|
6979
|
-
const body = await c.req.json();
|
|
6980
|
-
if (body && body.__reset === true) {
|
|
6981
|
-
const config = resetToolVerbosityConfig();
|
|
6982
|
-
saveToolVerbosityConfig(config);
|
|
6983
|
-
return c.json(config);
|
|
6984
|
-
}
|
|
6985
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) return c.json({ error: "Request body must be a JSON object" }, 400);
|
|
6986
|
-
const level = body.level;
|
|
6987
|
-
if (level !== 1 && level !== 2 && level !== 3) return c.json({ error: "level must be 1, 2, or 3" }, 400);
|
|
6988
|
-
saveToolVerbosityConfig({ level });
|
|
6989
|
-
return c.json(loadToolVerbosityConfig());
|
|
6990
|
-
});
|
|
6991
5536
|
|
|
6992
5537
|
//#endregion
|
|
6993
5538
|
//#region src/web/server.ts
|
|
@@ -7096,46 +5641,14 @@ const noGui = process.argv.includes("--no_gui");
|
|
|
7096
5641
|
const db = openDatabase(getDatabaseConfig());
|
|
7097
5642
|
initGraphSchema(db.db);
|
|
7098
5643
|
initPathSchema(db.db);
|
|
7099
|
-
|
|
7100
|
-
_current;
|
|
7101
|
-
_lastChecked = 0;
|
|
7102
|
-
_db;
|
|
7103
|
-
static CHECK_INTERVAL_MS = 2e3;
|
|
7104
|
-
constructor(sqliteDb) {
|
|
7105
|
-
this._db = sqliteDb;
|
|
7106
|
-
this._current = this.resolve();
|
|
7107
|
-
}
|
|
7108
|
-
get current() {
|
|
7109
|
-
const now = Date.now();
|
|
7110
|
-
if (now - this._lastChecked >= LiveProjectHashRef.CHECK_INTERVAL_MS) {
|
|
7111
|
-
this._lastChecked = now;
|
|
7112
|
-
const fresh = this.resolve();
|
|
7113
|
-
if (fresh !== this._current) {
|
|
7114
|
-
debug("mcp", "Project hash refreshed from database", {
|
|
7115
|
-
old: this._current,
|
|
7116
|
-
new: fresh
|
|
7117
|
-
});
|
|
7118
|
-
this._current = fresh;
|
|
7119
|
-
}
|
|
7120
|
-
}
|
|
7121
|
-
return this._current;
|
|
7122
|
-
}
|
|
7123
|
-
resolve() {
|
|
7124
|
-
try {
|
|
7125
|
-
const row = this._db.prepare("SELECT project_hash FROM project_metadata ORDER BY last_seen_at DESC LIMIT 1").get();
|
|
7126
|
-
if (row?.project_hash) return row.project_hash;
|
|
7127
|
-
} catch {}
|
|
7128
|
-
return getProjectHash(process.cwd());
|
|
7129
|
-
}
|
|
7130
|
-
};
|
|
7131
|
-
const projectHashRef = new LiveProjectHashRef(db.db);
|
|
5644
|
+
const projectHash = getProjectHash(process.cwd());
|
|
7132
5645
|
let toolRegistry = null;
|
|
7133
5646
|
try {
|
|
7134
5647
|
toolRegistry = new ToolRegistryRepository(db.db);
|
|
7135
5648
|
} catch {
|
|
7136
5649
|
debug("mcp", "Tool registry not available (pre-migration-16)");
|
|
7137
5650
|
}
|
|
7138
|
-
const embeddingStore = db.hasVectorSupport ? new EmbeddingStore(db.db,
|
|
5651
|
+
const embeddingStore = db.hasVectorSupport ? new EmbeddingStore(db.db, projectHash) : null;
|
|
7139
5652
|
const worker = new AnalysisWorker();
|
|
7140
5653
|
worker.start().catch(() => {
|
|
7141
5654
|
debug("mcp", "Worker failed to start, keyword-only mode");
|
|
@@ -7148,7 +5661,7 @@ const adaptiveManager = new AdaptiveThresholdManager({
|
|
|
7148
5661
|
alpha: topicConfig.ewmaAlpha
|
|
7149
5662
|
});
|
|
7150
5663
|
applyConfig(topicConfig, detector, adaptiveManager);
|
|
7151
|
-
const historicalSeed = new ThresholdStore(db.db).loadHistoricalSeed(
|
|
5664
|
+
const historicalSeed = new ThresholdStore(db.db).loadHistoricalSeed(projectHash);
|
|
7152
5665
|
if (historicalSeed) {
|
|
7153
5666
|
adaptiveManager.seedFromHistory(historicalSeed.averageDistance, historicalSeed.averageVariance);
|
|
7154
5667
|
applyConfig(topicConfig, detector, adaptiveManager);
|
|
@@ -7159,7 +5672,7 @@ const notificationStore = new NotificationStore(db.db);
|
|
|
7159
5672
|
const topicShiftHandler = new TopicShiftHandler({
|
|
7160
5673
|
detector,
|
|
7161
5674
|
stashManager,
|
|
7162
|
-
observationStore: new ObservationRepository(db.db,
|
|
5675
|
+
observationStore: new ObservationRepository(db.db, projectHash),
|
|
7163
5676
|
config: topicConfig,
|
|
7164
5677
|
decisionLogger,
|
|
7165
5678
|
adaptiveManager
|
|
@@ -7174,8 +5687,7 @@ async function processUnembedded() {
|
|
|
7174
5687
|
if (!embeddingStore || !worker.isReady()) return;
|
|
7175
5688
|
const ids = embeddingStore.findUnembedded(10);
|
|
7176
5689
|
if (ids.length === 0) return;
|
|
7177
|
-
const
|
|
7178
|
-
const obsRepo = new ObservationRepository(db.db, currentHash);
|
|
5690
|
+
const obsRepo = new ObservationRepository(db.db, projectHash);
|
|
7179
5691
|
let shiftDetectedThisCycle = false;
|
|
7180
5692
|
for (const id of ids) {
|
|
7181
5693
|
const obs = obsRepo.getById(id);
|
|
@@ -7192,26 +5704,24 @@ async function processUnembedded() {
|
|
|
7192
5704
|
id,
|
|
7193
5705
|
text: obs.content.length > 120 ? obs.content.substring(0, 120) + "..." : obs.content,
|
|
7194
5706
|
sessionId: obs.sessionId ?? null,
|
|
7195
|
-
createdAt: obs.createdAt
|
|
7196
|
-
projectHash: currentHash
|
|
5707
|
+
createdAt: obs.createdAt
|
|
7197
5708
|
});
|
|
7198
5709
|
if (topicConfig.enabled && !shiftDetectedThisCycle && TOPIC_SHIFT_SOURCES.has(obs.source)) try {
|
|
7199
5710
|
const obsWithEmbedding = {
|
|
7200
5711
|
...obs,
|
|
7201
5712
|
embedding
|
|
7202
5713
|
};
|
|
7203
|
-
const result = await topicShiftHandler.handleObservation(obsWithEmbedding, obs.sessionId ?? "unknown",
|
|
5714
|
+
const result = await topicShiftHandler.handleObservation(obsWithEmbedding, obs.sessionId ?? "unknown", projectHash);
|
|
7204
5715
|
if (result.stashed && result.notification) {
|
|
7205
5716
|
shiftDetectedThisCycle = true;
|
|
7206
|
-
notificationStore.add(
|
|
5717
|
+
notificationStore.add(projectHash, result.notification);
|
|
7207
5718
|
debug("embed", "Topic shift detected, notification queued", { id });
|
|
7208
5719
|
broadcast("topic_shift", {
|
|
7209
5720
|
id: result.notification.substring(0, 32),
|
|
7210
5721
|
fromTopic: null,
|
|
7211
5722
|
toTopic: null,
|
|
7212
5723
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7213
|
-
confidence: null
|
|
7214
|
-
projectHash: currentHash
|
|
5724
|
+
confidence: null
|
|
7215
5725
|
});
|
|
7216
5726
|
}
|
|
7217
5727
|
} catch (topicErr) {
|
|
@@ -7222,7 +5732,7 @@ async function processUnembedded() {
|
|
|
7222
5732
|
}
|
|
7223
5733
|
let researchBufferForFlush = null;
|
|
7224
5734
|
try {
|
|
7225
|
-
researchBufferForFlush = new ResearchBufferRepository(db.db,
|
|
5735
|
+
researchBufferForFlush = new ResearchBufferRepository(db.db, projectHash);
|
|
7226
5736
|
} catch {}
|
|
7227
5737
|
async function processUnembeddedTools() {
|
|
7228
5738
|
if (!toolRegistry || !worker.isReady() || !db.hasVectorSupport) return;
|
|
@@ -7249,39 +5759,26 @@ const embedTimer = setInterval(() => {
|
|
|
7249
5759
|
} catch {}
|
|
7250
5760
|
statusCache.refreshIfDirty();
|
|
7251
5761
|
}, 5e3);
|
|
7252
|
-
const statusCache = new StatusCache(db.db,
|
|
5762
|
+
const statusCache = new StatusCache(db.db, projectHash, process.cwd(), db.hasVectorSupport, () => worker.isReady());
|
|
7253
5763
|
const server = createServer();
|
|
7254
|
-
registerSaveMemory(server, db.db,
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
registerHygiene(server, db.db, projectHashRef, notificationStore);
|
|
7261
|
-
registerStatus(server, statusCache, projectHashRef, notificationStore);
|
|
5764
|
+
registerSaveMemory(server, db.db, projectHash, notificationStore, worker, embeddingStore, statusCache);
|
|
5765
|
+
registerRecall(server, db.db, projectHash, worker, embeddingStore, notificationStore, statusCache);
|
|
5766
|
+
registerTopicContext(server, db.db, projectHash, notificationStore);
|
|
5767
|
+
registerQueryGraph(server, db.db, projectHash, notificationStore);
|
|
5768
|
+
registerGraphStats(server, db.db, projectHash, notificationStore);
|
|
5769
|
+
registerStatus(server, statusCache, projectHash, notificationStore);
|
|
7262
5770
|
if (toolRegistry) {
|
|
7263
|
-
registerDiscoverTools(server, toolRegistry, worker, db.hasVectorSupport, notificationStore,
|
|
7264
|
-
registerReportTools(server, toolRegistry,
|
|
5771
|
+
registerDiscoverTools(server, toolRegistry, worker, db.hasVectorSupport, notificationStore, projectHash);
|
|
5772
|
+
registerReportTools(server, toolRegistry, projectHash);
|
|
7265
5773
|
}
|
|
7266
|
-
const pathRepo = new PathRepository(db.db,
|
|
5774
|
+
const pathRepo = new PathRepository(db.db, projectHash);
|
|
7267
5775
|
const pathTracker = new PathTracker(pathRepo);
|
|
7268
|
-
registerDebugPathTools(server, pathRepo, pathTracker, notificationStore,
|
|
7269
|
-
|
|
7270
|
-
let branchTracker = null;
|
|
7271
|
-
try {
|
|
7272
|
-
branchRepo = new BranchRepository(db.db, projectHashRef.current);
|
|
7273
|
-
branchTracker = new BranchTracker(branchRepo, db.db, projectHashRef.current);
|
|
7274
|
-
const obsRepoForBranches = new ObservationRepository(db.db, projectHashRef.current);
|
|
7275
|
-
registerThoughtBranchTools(server, branchRepo, obsRepoForBranches, notificationStore, projectHashRef);
|
|
7276
|
-
} catch {
|
|
7277
|
-
debug("mcp", "Branch tracking not available (pre-migration-21)");
|
|
7278
|
-
}
|
|
7279
|
-
const haikuProcessor = new HaikuProcessor(db.db, projectHashRef.current, {
|
|
5776
|
+
registerDebugPathTools(server, pathRepo, pathTracker, notificationStore, projectHash);
|
|
5777
|
+
const haikuProcessor = new HaikuProcessor(db.db, projectHash, {
|
|
7280
5778
|
intervalMs: 3e4,
|
|
7281
5779
|
batchSize: 10,
|
|
7282
5780
|
concurrency: 3,
|
|
7283
|
-
pathTracker
|
|
7284
|
-
branchTracker
|
|
5781
|
+
pathTracker
|
|
7285
5782
|
});
|
|
7286
5783
|
startServer(server).then(() => {
|
|
7287
5784
|
haikuProcessor.start();
|
|
@@ -7296,7 +5793,7 @@ if (!noGui) {
|
|
|
7296
5793
|
const __filename = fileURLToPath(import.meta.url);
|
|
7297
5794
|
const __dirname = path.dirname(__filename);
|
|
7298
5795
|
const uiRoot = path.resolve(__dirname, "..", "ui");
|
|
7299
|
-
startWebServer(createWebServer(db.db, uiRoot,
|
|
5796
|
+
startWebServer(createWebServer(db.db, uiRoot, projectHash), webPort);
|
|
7300
5797
|
} else debug("mcp", "Web UI disabled (--no_gui)");
|
|
7301
5798
|
const curationAgent = new CurationAgent(db.db, {
|
|
7302
5799
|
intervalMs: 300 * 1e3,
|