engramx 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/README.md +161 -17
  3. package/dist/{aider-context-BC5R2ZTA.js → aider-context-6IDE3R7U.js} +1 -1
  4. package/dist/check-2Z3MPZEJ.js +12 -0
  5. package/dist/{chunk-PEH54LYC.js → chunk-645NBY6L.js} +42 -5
  6. package/dist/chunk-73IBCRFI.js +215 -0
  7. package/dist/{chunk-SJT7VS2G.js → chunk-B4UOE64J.js} +46 -11
  8. package/dist/chunk-FKY6HIT2.js +99 -0
  9. package/dist/{chunk-533LR4I7.js → chunk-G4U3QOOW.js} +13 -97
  10. package/dist/chunk-RJC6RNXJ.js +1405 -0
  11. package/dist/chunk-RM2TBOVW.js +121 -0
  12. package/dist/chunk-SMU4WR3D.js +187 -0
  13. package/dist/{chunk-C6GBUOAL.js → chunk-VLTWBTQ7.js} +14 -15
  14. package/dist/chunk-XVYE4OX2.js +232 -0
  15. package/dist/chunk-ZUC6OXSL.js +178 -0
  16. package/dist/cli.js +818 -1533
  17. package/dist/{core-6IY5L6II.js → core-77F2BVYV.js} +2 -2
  18. package/dist/{cursor-mdc-GJ7E5LDD.js → cursor-mdc-EEO7PYZ3.js} +1 -1
  19. package/dist/{exporter-GWU2GF23.js → exporter-ZYJ4WM2F.js} +1 -1
  20. package/dist/{importer-V62NGZRK.js → importer-4UWQDH4W.js} +1 -1
  21. package/dist/index.js +3 -3
  22. package/dist/install-YVMVCFQW.js +121 -0
  23. package/dist/mcp-client-ROOJF76V.js +9 -0
  24. package/dist/mcp-config-QD4NPVXB.js +12 -0
  25. package/dist/{migrate-UKCO6BUU.js → migrate-KJ5K5NWO.js} +1 -1
  26. package/dist/notify-5POGKMRX.js +36 -0
  27. package/dist/{plugin-loader-STTGYIL5.js → plugin-loader-SQQB6V74.js} +69 -23
  28. package/dist/report-C3GTM3HY.js +12 -0
  29. package/dist/resolver-H7GXVP73.js +21 -0
  30. package/dist/serve.js +5 -4
  31. package/dist/{server-KUG7U6SG.js → server-2ZQKXJ5M.js} +74 -4
  32. package/dist/{windsurf-rules-C7SVDHBL.js → windsurf-rules-XF7MYF6J.js} +1 -1
  33. package/dist/wizard-UH27IO4I.js +274 -0
  34. package/package.json +3 -2
  35. package/dist/{tuner-KFNNGKG3.js → tuner-Y2YENAZC.js} +3 -3
package/dist/cli.js CHANGED
@@ -1,28 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ESTIMATED_TOKENS_PER_READ_DENY,
4
- formatHudStatus,
5
4
  formatStatsSummary,
6
- getComponentStatus,
7
5
  summarizeHookLog
8
- } from "./chunk-533LR4I7.js";
9
- import {
10
- readConfig
11
- } from "./chunk-22INHMKB.js";
6
+ } from "./chunk-FKY6HIT2.js";
12
7
  import {
13
8
  logHookEvent,
14
9
  readHookLog
15
10
  } from "./chunk-KL6NSPVA.js";
11
+ import {
12
+ formatInstallDiff,
13
+ installEngramHooks,
14
+ uninstallEngramHooks
15
+ } from "./chunk-SMU4WR3D.js";
16
+ import {
17
+ formatHudStatus,
18
+ getComponentStatus
19
+ } from "./chunk-G4U3QOOW.js";
20
+ import {
21
+ resolveRichPacket,
22
+ warmAllProviders
23
+ } from "./chunk-RJC6RNXJ.js";
24
+ import "./chunk-22INHMKB.js";
16
25
  import {
17
26
  autogen,
18
27
  install,
19
28
  status,
20
29
  uninstall
21
- } from "./chunk-C6GBUOAL.js";
30
+ } from "./chunk-VLTWBTQ7.js";
22
31
  import {
23
32
  benchmark,
24
33
  computeKeywordIDF,
25
34
  extractFile,
35
+ formatThousands,
26
36
  getDbPath,
27
37
  getFileContext,
28
38
  getStore,
@@ -32,26 +42,25 @@ import {
32
42
  mistakes,
33
43
  path,
34
44
  query,
35
- renderFileStructure,
36
45
  stats,
37
46
  toPosixPath
38
- } from "./chunk-SJT7VS2G.js";
39
- import "./chunk-PEH54LYC.js";
47
+ } from "./chunk-B4UOE64J.js";
48
+ import "./chunk-645NBY6L.js";
40
49
 
41
50
  // src/cli.ts
42
51
  import { Command } from "commander";
43
52
  import chalk2 from "chalk";
44
53
  import {
45
- existsSync as existsSync9,
46
- readFileSync as readFileSync5,
54
+ existsSync as existsSync7,
55
+ readFileSync as readFileSync4,
47
56
  writeFileSync as writeFileSync2,
48
57
  mkdirSync,
49
58
  unlinkSync,
50
59
  copyFileSync,
51
60
  renameSync as renameSync2
52
61
  } from "fs";
53
- import { dirname as dirname4, join as join9, resolve as pathResolve } from "path";
54
- import { fileURLToPath as fileURLToPath2 } from "url";
62
+ import { dirname as dirname3, join as join7, resolve as pathResolve2 } from "path";
63
+ import { fileURLToPath } from "url";
55
64
  import { homedir } from "os";
56
65
 
57
66
  // src/intercept/safety.ts
@@ -356,1145 +365,6 @@ function buildSessionContextResponse(eventName, additionalContext) {
356
365
  };
357
366
  }
358
367
 
359
- // src/providers/types.ts
360
- var PROVIDER_PRIORITY = [
361
- "engram:ast",
362
- "engram:structure",
363
- "engram:mistakes",
364
- "mempalace",
365
- "context7",
366
- "engram:git",
367
- "obsidian",
368
- "engram:lsp"
369
- ];
370
- var DEFAULT_CACHE_TTL_SEC = 3600;
371
-
372
- // src/providers/ast.ts
373
- import { readFileSync } from "fs";
374
-
375
- // src/providers/grammar-loader.ts
376
- import { existsSync as existsSync3 } from "fs";
377
- import { join as join3, dirname as dirname2 } from "path";
378
- import { createRequire } from "module";
379
- import { fileURLToPath } from "url";
380
- var require2 = createRequire(import.meta.url);
381
- var parserCache = /* @__PURE__ */ new Map();
382
- var tsParserInit = false;
383
- var EXT_TO_LANG = {
384
- ts: "typescript",
385
- tsx: "tsx",
386
- js: "javascript",
387
- jsx: "javascript",
388
- mjs: "javascript",
389
- cjs: "javascript",
390
- py: "python",
391
- go: "go",
392
- rs: "rust",
393
- rb: "ruby",
394
- java: "java",
395
- c: "c",
396
- cpp: "cpp",
397
- h: "c",
398
- hpp: "cpp",
399
- php: "php"
400
- };
401
- var LANG_TO_PKG = {
402
- typescript: "tree-sitter-typescript",
403
- tsx: "tree-sitter-typescript",
404
- javascript: "tree-sitter-javascript",
405
- python: "tree-sitter-python",
406
- go: "tree-sitter-go",
407
- rust: "tree-sitter-rust"
408
- };
409
- function getSupportedLang(filePath) {
410
- const ext = filePath.split(".").pop()?.toLowerCase();
411
- return ext ? EXT_TO_LANG[ext] ?? null : null;
412
- }
413
- function findGrammarWasm(lang) {
414
- const pkg = LANG_TO_PKG[lang];
415
- if (!pkg) return null;
416
- const wasmName = lang === "tsx" ? "tree-sitter-tsx.wasm" : `tree-sitter-${lang}.wasm`;
417
- const candidates = [];
418
- try {
419
- const here = dirname2(fileURLToPath(import.meta.url));
420
- candidates.push(join3(here, "..", "grammars", wasmName));
421
- candidates.push(join3(here, "grammars", wasmName));
422
- } catch {
423
- }
424
- try {
425
- const here = dirname2(fileURLToPath(import.meta.url));
426
- candidates.push(join3(here, "..", "..", "node_modules", pkg, wasmName));
427
- } catch {
428
- }
429
- try {
430
- const pkgMain = require2.resolve(`${pkg}/package.json`);
431
- const pkgDir = dirname2(pkgMain);
432
- candidates.push(join3(pkgDir, wasmName));
433
- } catch {
434
- }
435
- return candidates.find((c) => existsSync3(c)) ?? null;
436
- }
437
- async function getParser(lang) {
438
- const cached = parserCache.get(lang);
439
- if (cached) return cached;
440
- try {
441
- const { Parser, Language } = await import("web-tree-sitter");
442
- if (!tsParserInit) {
443
- await Parser.init();
444
- tsParserInit = true;
445
- }
446
- const wasmPath = findGrammarWasm(lang);
447
- if (!wasmPath) return null;
448
- const language = await Language.load(wasmPath);
449
- const parser = new Parser();
450
- parser.setLanguage(language);
451
- parserCache.set(lang, parser);
452
- return parser;
453
- } catch {
454
- return null;
455
- }
456
- }
457
-
458
- // src/providers/ast.ts
459
- function extractParams(node) {
460
- const paramsNode = node.childForFieldName("parameters") ?? node.childForFieldName("formal_parameters");
461
- if (!paramsNode) return "";
462
- return paramsNode.text.replace(/\n/g, " ").replace(/\s+/g, " ").slice(0, 80).trim();
463
- }
464
- function extractSymbols(rootNode) {
465
- const symbols = [];
466
- function visit(node) {
467
- switch (node.type) {
468
- // ── Functions ───────────────────────────────────────────────
469
- case "function_declaration":
470
- case "function_definition": {
471
- const nameNode = node.childForFieldName("name");
472
- if (nameNode) {
473
- symbols.push({
474
- name: nameNode.text,
475
- kind: "function",
476
- line: node.startPosition.row + 1,
477
- params: extractParams(node)
478
- });
479
- }
480
- break;
481
- }
482
- // ── Classes ─────────────────────────────────────────────────
483
- case "class_declaration":
484
- case "class_definition": {
485
- const nameNode = node.childForFieldName("name");
486
- if (nameNode) {
487
- symbols.push({
488
- name: nameNode.text,
489
- kind: "class",
490
- line: node.startPosition.row + 1
491
- });
492
- }
493
- break;
494
- }
495
- // ── Methods ─────────────────────────────────────────────────
496
- case "method_definition":
497
- case "method_declaration": {
498
- const nameNode = node.childForFieldName("name");
499
- if (nameNode) {
500
- symbols.push({
501
- name: nameNode.text,
502
- kind: "method",
503
- line: node.startPosition.row + 1,
504
- params: extractParams(node)
505
- });
506
- }
507
- break;
508
- }
509
- // ── TypeScript interfaces ────────────────────────────────────
510
- case "interface_declaration": {
511
- const nameNode = node.childForFieldName("name");
512
- if (nameNode) {
513
- symbols.push({
514
- name: nameNode.text,
515
- kind: "interface",
516
- line: node.startPosition.row + 1
517
- });
518
- }
519
- break;
520
- }
521
- // ── TypeScript type aliases ──────────────────────────────────
522
- case "type_alias_declaration": {
523
- const nameNode = node.childForFieldName("name");
524
- if (nameNode) {
525
- symbols.push({
526
- name: nameNode.text,
527
- kind: "type",
528
- line: node.startPosition.row + 1
529
- });
530
- }
531
- break;
532
- }
533
- // ── Exported variable declarations (incl. arrow functions) ──
534
- case "lexical_declaration":
535
- case "variable_declaration": {
536
- for (let i = 0; i < node.childCount; i++) {
537
- const child = node.child(i);
538
- if (!child || child.type !== "variable_declarator") continue;
539
- const nameNode = child.childForFieldName("name");
540
- const valueNode = child.childForFieldName("value");
541
- if (!nameNode) continue;
542
- const isArrow = valueNode?.type === "arrow_function" || valueNode?.type === "function";
543
- symbols.push({
544
- name: nameNode.text,
545
- kind: isArrow ? "function" : "variable",
546
- line: node.startPosition.row + 1,
547
- params: isArrow && valueNode ? extractParams(valueNode) : void 0
548
- });
549
- }
550
- break;
551
- }
552
- default:
553
- break;
554
- }
555
- for (let i = 0; i < node.childCount; i++) {
556
- const child = node.child(i);
557
- if (child) visit(child);
558
- }
559
- }
560
- visit(rootNode);
561
- return symbols;
562
- }
563
- function formatSymbols(symbols, tokenBudget) {
564
- const lines = symbols.map((s) => {
565
- const params = s.params !== void 0 ? `(${s.params})` : "";
566
- return `${s.kind.toUpperCase()} ${s.name}${params} L${s.line}`;
567
- });
568
- const charBudget = tokenBudget * 4;
569
- let text = lines.join("\n");
570
- if (text.length > charBudget) {
571
- text = text.slice(0, charBudget).trimEnd() + "\n... (truncated)";
572
- }
573
- return text;
574
- }
575
- var astProvider = {
576
- name: "engram:ast",
577
- label: "AST STRUCTURE",
578
- tier: 1,
579
- tokenBudget: 300,
580
- timeoutMs: 200,
581
- async resolve(filePath, _context) {
582
- const lang = getSupportedLang(filePath);
583
- if (!lang) return null;
584
- const parser = await getParser(lang);
585
- if (!parser) return null;
586
- try {
587
- const source = readFileSync(filePath, "utf-8");
588
- const tree = parser.parse(source);
589
- if (!tree) return null;
590
- const symbols = extractSymbols(tree.rootNode);
591
- if (symbols.length === 0) return null;
592
- return {
593
- provider: "engram:ast",
594
- content: formatSymbols(symbols, this.tokenBudget),
595
- confidence: 1,
596
- cached: false
597
- };
598
- } catch {
599
- return null;
600
- }
601
- },
602
- async isAvailable() {
603
- try {
604
- const { Parser } = await import("web-tree-sitter");
605
- await Parser.init();
606
- return true;
607
- } catch {
608
- return false;
609
- }
610
- }
611
- };
612
-
613
- // src/providers/engram-structure.ts
614
- var structureProvider = {
615
- name: "engram:structure",
616
- label: "STRUCTURE",
617
- tier: 1,
618
- tokenBudget: 250,
619
- timeoutMs: 500,
620
- async resolve(filePath, context) {
621
- try {
622
- const store = await getStore(context.projectRoot);
623
- try {
624
- const result = renderFileStructure(store, filePath);
625
- if (!result || result.nodeCount === 0) return null;
626
- return {
627
- provider: "engram:structure",
628
- content: result.text,
629
- confidence: result.avgConfidence,
630
- cached: false
631
- };
632
- } finally {
633
- store.close();
634
- }
635
- } catch {
636
- return null;
637
- }
638
- },
639
- async isAvailable() {
640
- return true;
641
- }
642
- };
643
-
644
- // src/providers/engram-mistakes.ts
645
- var mistakesProvider = {
646
- name: "engram:mistakes",
647
- label: "KNOWN ISSUES",
648
- tier: 1,
649
- tokenBudget: 50,
650
- timeoutMs: 200,
651
- async resolve(filePath, context) {
652
- try {
653
- const store = await getStore(context.projectRoot);
654
- try {
655
- const allMistakes = store.getNodesByFile(filePath).filter((n) => n.kind === "mistake");
656
- if (allMistakes.length === 0) return null;
657
- const lines = allMistakes.slice(0, 5).map((m) => ` ! ${m.label} (flagged ${formatAge(m.lastVerified)})`).join("\n");
658
- return {
659
- provider: "engram:mistakes",
660
- content: lines,
661
- confidence: 0.95,
662
- cached: false
663
- };
664
- } finally {
665
- store.close();
666
- }
667
- } catch {
668
- return null;
669
- }
670
- },
671
- async isAvailable() {
672
- return true;
673
- }
674
- };
675
- function formatAge(timestampMs) {
676
- if (timestampMs === 0) return "unknown";
677
- const days = Math.floor((Date.now() - timestampMs) / (1e3 * 60 * 60 * 24));
678
- if (days === 0) return "today";
679
- if (days === 1) return "yesterday";
680
- if (days < 30) return `${days}d ago`;
681
- return `${Math.floor(days / 30)}mo ago`;
682
- }
683
-
684
- // src/providers/engram-git.ts
685
- import { execFileSync } from "child_process";
686
- var gitProvider = {
687
- name: "engram:git",
688
- label: "CHANGES",
689
- tier: 1,
690
- tokenBudget: 50,
691
- timeoutMs: 200,
692
- async resolve(filePath, context) {
693
- try {
694
- const cwd = context.projectRoot;
695
- const lastLog = git(
696
- ["log", "-1", "--format=%ar|%an|%s", "--", filePath],
697
- cwd
698
- );
699
- if (!lastLog) return null;
700
- const [timeAgo, author, message] = lastLog.split("|", 3);
701
- const recentCount = git(
702
- [
703
- "rev-list",
704
- "--count",
705
- "--since=30.days",
706
- "HEAD",
707
- "--",
708
- filePath
709
- ],
710
- cwd
711
- );
712
- const churnNote = context.churnRate > 0.3 ? "high churn" : context.churnRate > 0.1 ? "moderate" : "stable";
713
- const parts = [
714
- ` Last modified: ${timeAgo} by ${author} (${truncate(message, 50)})`,
715
- ` Churn: ${context.churnRate.toFixed(2)} (${churnNote}) | ${recentCount || "0"} changes in 30d`
716
- ];
717
- return {
718
- provider: "engram:git",
719
- content: parts.join("\n"),
720
- confidence: 0.9,
721
- cached: false
722
- };
723
- } catch {
724
- return null;
725
- }
726
- },
727
- async isAvailable() {
728
- try {
729
- execFileSync("git", ["--version"], {
730
- encoding: "utf-8",
731
- timeout: 2e3
732
- });
733
- return true;
734
- } catch {
735
- return false;
736
- }
737
- }
738
- };
739
- function git(args, cwd) {
740
- try {
741
- return execFileSync("git", args, {
742
- cwd,
743
- encoding: "utf-8",
744
- timeout: 3e3,
745
- maxBuffer: 1024 * 1024
746
- }).trim();
747
- } catch {
748
- return "";
749
- }
750
- }
751
- function truncate(s, max) {
752
- return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
753
- }
754
-
755
- // src/providers/mempalace.ts
756
- import { execFile } from "child_process";
757
- var MAX_SEARCH_RESULTS = 3;
758
- var mempalaceProvider = {
759
- name: "mempalace",
760
- label: "DECISIONS",
761
- tier: 2,
762
- tokenBudget: 100,
763
- timeoutMs: 200,
764
- async resolve(filePath, context) {
765
- try {
766
- const store = await getStore(context.projectRoot);
767
- try {
768
- const cached = store.getCachedContextForProvider(
769
- "mempalace",
770
- filePath
771
- );
772
- if (cached) {
773
- return {
774
- provider: "mempalace",
775
- content: cached.content,
776
- confidence: 0.8,
777
- cached: true
778
- };
779
- }
780
- } finally {
781
- store.close();
782
- }
783
- const query2 = buildQuery(filePath, context);
784
- const raw = await searchMempalace(query2);
785
- if (!raw) return null;
786
- const content = formatResults(raw);
787
- if (!content) return null;
788
- const store2 = await getStore(context.projectRoot);
789
- try {
790
- store2.setCachedContext(
791
- "mempalace",
792
- filePath,
793
- content,
794
- DEFAULT_CACHE_TTL_SEC,
795
- query2
796
- );
797
- store2.save();
798
- } finally {
799
- store2.close();
800
- }
801
- return {
802
- provider: "mempalace",
803
- content,
804
- confidence: 0.8,
805
- cached: false
806
- };
807
- } catch {
808
- return null;
809
- }
810
- },
811
- async warmup(projectRoot) {
812
- const start = Date.now();
813
- const entries = [];
814
- try {
815
- const store = await getStore(projectRoot);
816
- let projectName;
817
- try {
818
- projectName = store.getStat("project_name") ?? projectRoot.split("/").pop() ?? "";
819
- } finally {
820
- store.close();
821
- }
822
- if (!projectName) {
823
- return { provider: "mempalace", entries, durationMs: Date.now() - start };
824
- }
825
- const raw = await searchMempalace(
826
- `${projectName} decisions architecture patterns`
827
- );
828
- if (!raw) {
829
- return { provider: "mempalace", entries, durationMs: Date.now() - start };
830
- }
831
- const content = formatResults(raw);
832
- if (content) {
833
- entries.push({ filePath: "__project__", content });
834
- }
835
- } catch {
836
- }
837
- return { provider: "mempalace", entries, durationMs: Date.now() - start };
838
- },
839
- async isAvailable() {
840
- try {
841
- const result = await execFilePromise("mcp-mempalace", [
842
- "mempalace-status"
843
- ]);
844
- return result.includes("palace") || result.includes("drawers");
845
- } catch {
846
- return false;
847
- }
848
- }
849
- };
850
- function buildQuery(filePath, context) {
851
- const fileName = filePath.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
852
- const importTerms = context.imports.slice(0, 3).join(" ");
853
- return `${fileName} ${importTerms}`.trim();
854
- }
855
- function searchMempalace(query2) {
856
- return new Promise((resolve7) => {
857
- const timeout = setTimeout(() => resolve7(null), 3e3);
858
- execFile(
859
- "mcp-mempalace",
860
- ["mempalace-search", "--query", query2],
861
- { encoding: "utf-8", timeout: 3e3, maxBuffer: 1024 * 1024 },
862
- (err, stdout) => {
863
- clearTimeout(timeout);
864
- if (err || !stdout.trim()) {
865
- resolve7(null);
866
- return;
867
- }
868
- resolve7(stdout.trim());
869
- }
870
- );
871
- });
872
- }
873
- function formatResults(raw) {
874
- try {
875
- const parsed = JSON.parse(raw);
876
- const results = Array.isArray(parsed) ? parsed : parsed?.results ?? parsed?.drawers ?? [];
877
- if (results.length === 0) return null;
878
- const lines = results.slice(0, MAX_SEARCH_RESULTS).map((r) => {
879
- const content = r.content ?? r.text ?? r.summary ?? "";
880
- const truncated = content.split(/\s+/).slice(0, 30).join(" ");
881
- return ` - ${truncated}`;
882
- }).filter((l) => l.length > 4);
883
- return lines.length > 0 ? lines.join("\n") : null;
884
- } catch {
885
- const lines = raw.split("\n").filter((l) => l.trim()).slice(0, MAX_SEARCH_RESULTS).map((l) => ` - ${l.trim().slice(0, 120)}`);
886
- return lines.length > 0 ? lines.join("\n") : null;
887
- }
888
- }
889
- function execFilePromise(cmd, args) {
890
- return new Promise((resolve7, reject) => {
891
- execFile(
892
- cmd,
893
- args,
894
- { encoding: "utf-8", timeout: 3e3 },
895
- (err, stdout) => {
896
- if (err) reject(err);
897
- else resolve7(stdout.trim());
898
- }
899
- );
900
- });
901
- }
902
-
903
- // src/providers/context7.ts
904
- import { execFile as execFile2 } from "child_process";
905
- var LIBRARY_CACHE_TTL = 4 * 3600;
906
- var context7Provider = {
907
- name: "context7",
908
- label: "LIBRARY",
909
- tier: 2,
910
- tokenBudget: 100,
911
- timeoutMs: 200,
912
- async resolve(filePath, context) {
913
- if (context.imports.length === 0) return null;
914
- try {
915
- const store = await getStore(context.projectRoot);
916
- try {
917
- const cached = store.getCachedContextForProvider("context7", filePath);
918
- if (cached) {
919
- return {
920
- provider: "context7",
921
- content: cached.content,
922
- confidence: 0.85,
923
- cached: true
924
- };
925
- }
926
- } finally {
927
- store.close();
928
- }
929
- const primaryImport = context.imports[0];
930
- const docs = await queryContext7(primaryImport);
931
- if (!docs) return null;
932
- const content = formatDocs(primaryImport, docs);
933
- if (!content) return null;
934
- const store2 = await getStore(context.projectRoot);
935
- try {
936
- store2.setCachedContext(
937
- "context7",
938
- filePath,
939
- content,
940
- LIBRARY_CACHE_TTL,
941
- primaryImport
942
- );
943
- store2.save();
944
- } finally {
945
- store2.close();
946
- }
947
- return {
948
- provider: "context7",
949
- content,
950
- confidence: 0.85,
951
- cached: false
952
- };
953
- } catch {
954
- return null;
955
- }
956
- },
957
- async warmup(projectRoot) {
958
- const start = Date.now();
959
- const entries = [];
960
- try {
961
- const store = await getStore(projectRoot);
962
- let importEdges;
963
- try {
964
- const allEdges = store.getAllEdges();
965
- importEdges = allEdges.filter((e) => e.relation === "imports").map((e) => ({ source: e.sourceFile, target: e.target }));
966
- } finally {
967
- store.close();
968
- }
969
- const packages = [
970
- ...new Set(
971
- importEdges.map((e) => {
972
- const parts = e.target.split("::");
973
- return parts[parts.length - 1];
974
- }).filter(isExternalPackage)
975
- )
976
- ].slice(0, 10);
977
- for (const pkg of packages) {
978
- const docs = await queryContext7(pkg);
979
- if (docs) {
980
- const content = formatDocs(pkg, docs);
981
- if (content) {
982
- const files = importEdges.filter((e) => e.target.includes(pkg)).map((e) => e.source);
983
- for (const file of [...new Set(files)]) {
984
- entries.push({ filePath: file, content });
985
- }
986
- }
987
- }
988
- }
989
- } catch {
990
- }
991
- return { provider: "context7", entries, durationMs: Date.now() - start };
992
- },
993
- async isAvailable() {
994
- try {
995
- const result = await execFilePromise2("mcp-context7", ["--list"]);
996
- return result.includes("resolve-library-id");
997
- } catch {
998
- return false;
999
- }
1000
- }
1001
- };
1002
- function isExternalPackage(name) {
1003
- if (!name) return false;
1004
- if (name.startsWith(".") || name.startsWith("/")) return false;
1005
- if ([
1006
- "fs",
1007
- "path",
1008
- "os",
1009
- "url",
1010
- "http",
1011
- "https",
1012
- "crypto",
1013
- "stream",
1014
- "util",
1015
- "events",
1016
- "child_process",
1017
- "node:fs",
1018
- "node:path",
1019
- "node:os",
1020
- "node:url",
1021
- "node:http",
1022
- "node:https",
1023
- "node:crypto",
1024
- "node:stream",
1025
- "node:util",
1026
- "node:events",
1027
- "node:child_process"
1028
- ].includes(name))
1029
- return false;
1030
- return true;
1031
- }
1032
- function queryContext7(packageName) {
1033
- return new Promise((resolve7) => {
1034
- const timeout = setTimeout(() => resolve7(null), 5e3);
1035
- execFile2(
1036
- "mcp-context7",
1037
- [
1038
- "query-docs",
1039
- "--context7CompatibleLibraryID",
1040
- packageName,
1041
- "--topic",
1042
- "API reference quick start"
1043
- ],
1044
- { encoding: "utf-8", timeout: 5e3, maxBuffer: 2 * 1024 * 1024 },
1045
- (err, stdout) => {
1046
- clearTimeout(timeout);
1047
- if (err || !stdout.trim()) {
1048
- resolve7(null);
1049
- return;
1050
- }
1051
- resolve7(stdout.trim());
1052
- }
1053
- );
1054
- });
1055
- }
1056
- function formatDocs(pkg, raw) {
1057
- const truncated = raw.slice(0, 400);
1058
- const lines = truncated.split("\n").filter((l) => l.trim()).slice(0, 5).map((l) => ` ${l.trim()}`);
1059
- if (lines.length === 0) return null;
1060
- return ` ${pkg}:
1061
- ${lines.join("\n")}`;
1062
- }
1063
- function execFilePromise2(cmd, args) {
1064
- return new Promise((resolve7, reject) => {
1065
- execFile2(
1066
- cmd,
1067
- args,
1068
- { encoding: "utf-8", timeout: 3e3 },
1069
- (err, stdout) => {
1070
- if (err) reject(err);
1071
- else resolve7(stdout.trim());
1072
- }
1073
- );
1074
- });
1075
- }
1076
-
1077
- // src/providers/obsidian.ts
1078
- var OBSIDIAN_PORT = 27124;
1079
- var OBSIDIAN_BASE = `http://127.0.0.1:${OBSIDIAN_PORT}`;
1080
- var obsidianProvider = {
1081
- name: "obsidian",
1082
- label: "PROJECT NOTES",
1083
- tier: 2,
1084
- tokenBudget: 50,
1085
- timeoutMs: 200,
1086
- async resolve(filePath, context) {
1087
- try {
1088
- const store = await getStore(context.projectRoot);
1089
- try {
1090
- const cached = store.getCachedContextForProvider("obsidian", filePath);
1091
- if (cached) {
1092
- return {
1093
- provider: "obsidian",
1094
- content: cached.content,
1095
- confidence: 0.7,
1096
- cached: true
1097
- };
1098
- }
1099
- } finally {
1100
- store.close();
1101
- }
1102
- const projectName = context.projectRoot.split("/").pop() ?? "";
1103
- const fileName = filePath.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
1104
- const query2 = `${projectName} ${fileName}`;
1105
- const results = await searchObsidian(query2);
1106
- if (!results) return null;
1107
- const content = formatResults2(results);
1108
- if (!content) return null;
1109
- const store2 = await getStore(context.projectRoot);
1110
- try {
1111
- store2.setCachedContext(
1112
- "obsidian",
1113
- filePath,
1114
- content,
1115
- DEFAULT_CACHE_TTL_SEC,
1116
- query2
1117
- );
1118
- store2.save();
1119
- } finally {
1120
- store2.close();
1121
- }
1122
- return {
1123
- provider: "obsidian",
1124
- content,
1125
- confidence: 0.7,
1126
- cached: false
1127
- };
1128
- } catch {
1129
- return null;
1130
- }
1131
- },
1132
- async warmup(projectRoot) {
1133
- const start = Date.now();
1134
- const entries = [];
1135
- try {
1136
- const projectName = projectRoot.split("/").pop() ?? "";
1137
- if (!projectName) {
1138
- return { provider: "obsidian", entries, durationMs: Date.now() - start };
1139
- }
1140
- const results = await searchObsidian(
1141
- `${projectName} architecture design decisions`
1142
- );
1143
- if (results) {
1144
- const content = formatResults2(results);
1145
- if (content) {
1146
- entries.push({ filePath: "__project__", content });
1147
- }
1148
- }
1149
- } catch {
1150
- }
1151
- return { provider: "obsidian", entries, durationMs: Date.now() - start };
1152
- },
1153
- async isAvailable() {
1154
- try {
1155
- const response = await fetchWithTimeout(
1156
- `${OBSIDIAN_BASE}/`,
1157
- 1e3
1158
- );
1159
- return response.ok;
1160
- } catch {
1161
- return false;
1162
- }
1163
- }
1164
- };
1165
- async function searchObsidian(query2) {
1166
- try {
1167
- const response = await fetchWithTimeout(
1168
- `${OBSIDIAN_BASE}/search/simple/?query=${encodeURIComponent(query2)}`,
1169
- 2e3
1170
- );
1171
- if (!response.ok) return null;
1172
- const data = await response.json();
1173
- if (!Array.isArray(data) || data.length === 0) return null;
1174
- return data.slice(0, 3);
1175
- } catch {
1176
- return null;
1177
- }
1178
- }
1179
- function formatResults2(results) {
1180
- if (results.length === 0) return null;
1181
- const lines = results.slice(0, 3).map((r) => {
1182
- const name = r.filename.replace(/\.md$/, "");
1183
- return ` Related: ${name}`;
1184
- });
1185
- return lines.join("\n");
1186
- }
1187
- async function fetchWithTimeout(url, timeoutMs) {
1188
- const controller = new AbortController();
1189
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1190
- try {
1191
- return await fetch(url, { signal: controller.signal });
1192
- } finally {
1193
- clearTimeout(timer);
1194
- }
1195
- }
1196
-
1197
- // src/providers/lsp-connection.ts
1198
- import { connect } from "net";
1199
- import { existsSync as existsSync4 } from "fs";
1200
- import { tmpdir } from "os";
1201
- import { join as join4 } from "path";
1202
- function candidateSockets() {
1203
- const uid = process.getuid?.() ?? 0;
1204
- const tmp = tmpdir();
1205
- return [
1206
- // TypeScript language server (used by VS Code)
1207
- join4(tmp, `tsserver-${uid}.sock`),
1208
- // Generic LSP socket (some editors, e.g. Helix)
1209
- join4(tmp, "lsp-server.sock"),
1210
- // TypeScript language server alternate path
1211
- join4(tmp, "typescript-language-server.sock"),
1212
- // Pyright (Python)
1213
- join4(tmp, `pyright-${uid}.sock`),
1214
- // rust-analyzer
1215
- join4(tmp, "rust-analyzer.sock")
1216
- ];
1217
- }
1218
- var LspConnection = class _LspConnection {
1219
- socket = null;
1220
- _requestId = 0;
1221
- /**
1222
- * Attempt to connect to any currently-running LSP server socket.
1223
- * Returns null — not throws — if no socket is found or connection fails.
1224
- * Timeout per candidate: 500ms.
1225
- */
1226
- static async tryConnect() {
1227
- const candidates = candidateSockets().filter((p) => existsSync4(p));
1228
- if (candidates.length === 0) return null;
1229
- for (const path2 of candidates) {
1230
- try {
1231
- const conn = new _LspConnection();
1232
- await conn._connect(path2);
1233
- return conn;
1234
- } catch {
1235
- continue;
1236
- }
1237
- }
1238
- return null;
1239
- }
1240
- /** Internal: open a socket to the given path with a 500ms timeout. */
1241
- _connect(socketPath) {
1242
- return new Promise((resolve7, reject) => {
1243
- const socket = connect(socketPath);
1244
- const timeout = setTimeout(() => {
1245
- socket.destroy();
1246
- reject(new Error("LSP connect timeout"));
1247
- }, 500);
1248
- socket.on("connect", () => {
1249
- clearTimeout(timeout);
1250
- this.socket = socket;
1251
- resolve7();
1252
- });
1253
- socket.on("error", (err) => {
1254
- clearTimeout(timeout);
1255
- reject(err);
1256
- });
1257
- });
1258
- }
1259
- /**
1260
- * Request hover info for a position.
1261
- *
1262
- * Stub: returns null. A full implementation would send a JSON-RPC
1263
- * textDocument/hover request and parse the response. Left as a stub
1264
- * because the response requires a request/response correlation loop
1265
- * over a streaming socket — non-trivial, and out of scope for v0.5.x.
1266
- * The provider benefits from the availability check alone.
1267
- */
1268
- async hover(_filePath, _line, _character) {
1269
- if (!this.socket) return null;
1270
- return null;
1271
- }
1272
- /**
1273
- * Fetch diagnostics for a file.
1274
- *
1275
- * Stub: returns []. A full implementation would use the
1276
- * textDocument/diagnostic pull request (LSP 3.17+) or subscribe to
1277
- * publishDiagnostics push notifications. Deferred to a future sprint.
1278
- */
1279
- async getDiagnostics(_filePath) {
1280
- if (!this.socket) return [];
1281
- return [];
1282
- }
1283
- /** Whether this connection has a live socket. */
1284
- get connected() {
1285
- return this.socket !== null && !this.socket.destroyed;
1286
- }
1287
- /** Close and destroy the socket. Safe to call multiple times. */
1288
- close() {
1289
- this.socket?.destroy();
1290
- this.socket = null;
1291
- }
1292
- };
1293
-
1294
- // src/providers/lsp.ts
1295
- var cachedConnection = void 0;
1296
- async function getConnection() {
1297
- if (cachedConnection instanceof LspConnection) {
1298
- if (cachedConnection.connected) return cachedConnection;
1299
- cachedConnection.close();
1300
- cachedConnection = void 0;
1301
- }
1302
- if (cachedConnection === null) return null;
1303
- cachedConnection = await LspConnection.tryConnect();
1304
- return cachedConnection;
1305
- }
1306
- var lspProvider = {
1307
- name: "engram:lsp",
1308
- label: "LSP CONTEXT",
1309
- tier: 1,
1310
- tokenBudget: 100,
1311
- timeoutMs: 100,
1312
- async resolve(filePath, _context) {
1313
- try {
1314
- const conn = await getConnection();
1315
- if (!conn) return null;
1316
- const hover = await conn.hover(filePath, 0, 0);
1317
- if (!hover?.contents) return null;
1318
- const content = typeof hover.contents === "string" ? hover.contents : JSON.stringify(hover.contents);
1319
- const charBudget = this.tokenBudget * 4;
1320
- const truncated = content.length > charBudget ? content.slice(0, charBudget) + "..." : content;
1321
- return {
1322
- provider: "engram:lsp",
1323
- content: truncated,
1324
- confidence: 0.95,
1325
- cached: false
1326
- };
1327
- } catch {
1328
- return null;
1329
- }
1330
- },
1331
- async isAvailable() {
1332
- try {
1333
- const conn = await getConnection();
1334
- return conn !== null;
1335
- } catch {
1336
- return false;
1337
- }
1338
- }
1339
- };
1340
-
1341
- // src/providers/resolver.ts
1342
- var BUILTIN_PROVIDERS = [
1343
- astProvider,
1344
- structureProvider,
1345
- mistakesProvider,
1346
- gitProvider,
1347
- mempalaceProvider,
1348
- context7Provider,
1349
- obsidianProvider,
1350
- lspProvider
1351
- ];
1352
- var BUILTIN_NAMES = new Set(BUILTIN_PROVIDERS.map((p) => p.name));
1353
- async function getAllProviders() {
1354
- const { getLoadedPlugins } = await import("./plugin-loader-STTGYIL5.js");
1355
- const { loaded } = await getLoadedPlugins();
1356
- const safePlugins = loaded.filter((p) => !BUILTIN_NAMES.has(p.name));
1357
- return [...BUILTIN_PROVIDERS, ...safePlugins];
1358
- }
1359
- var ALL_PROVIDERS = BUILTIN_PROVIDERS;
1360
- function estimateTokens(text) {
1361
- return Math.ceil(text.length / 4);
1362
- }
1363
- async function resolveRichPacket(filePath, context, enabledProviders) {
1364
- const start = Date.now();
1365
- let allProviders;
1366
- try {
1367
- allProviders = await getAllProviders();
1368
- } catch {
1369
- allProviders = BUILTIN_PROVIDERS;
1370
- }
1371
- const providers = allProviders.filter((p) => {
1372
- if (enabledProviders && !enabledProviders.includes(p.name)) return false;
1373
- return true;
1374
- });
1375
- const available = await filterAvailable(providers);
1376
- if (available.length === 0) return null;
1377
- const settled = await Promise.allSettled(
1378
- available.map((p) => resolveWithTimeout(p, filePath, context))
1379
- );
1380
- const results = [];
1381
- for (const outcome of settled) {
1382
- if (outcome.status === "fulfilled" && outcome.value) {
1383
- results.push(outcome.value);
1384
- }
1385
- }
1386
- if (results.length === 0) return null;
1387
- const hasAst = results.some((r) => r.provider === "engram:ast");
1388
- const deduped = hasAst ? results.filter((r) => r.provider !== "engram:structure") : results;
1389
- const sorted = deduped.sort((a, b) => {
1390
- const aIdx = PROVIDER_PRIORITY.indexOf(a.provider);
1391
- const bIdx = PROVIDER_PRIORITY.indexOf(b.provider);
1392
- return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
1393
- });
1394
- const config = readConfig(context.projectRoot);
1395
- const budget = config.totalTokenBudget;
1396
- const sections = [];
1397
- let totalTokens = 0;
1398
- for (const result of sorted) {
1399
- const sectionTokens = estimateTokens(result.content);
1400
- if (totalTokens + sectionTokens > budget) {
1401
- break;
1402
- }
1403
- const provider = allProviders.find((p) => p.name === result.provider);
1404
- const label = provider?.label ?? result.provider.toUpperCase();
1405
- const cacheTag = result.cached ? ", cached" : "";
1406
- sections.push(`${label} (${result.provider}${cacheTag}):
1407
- ${result.content}`);
1408
- totalTokens += sectionTokens;
1409
- }
1410
- if (sections.length === 0) return null;
1411
- const providerNames = sorted.filter((_, i) => i < sections.length).map((r) => r.provider);
1412
- const isEnrichment = enabledProviders && !enabledProviders.includes("engram:structure");
1413
- const header = isEnrichment ? `[engram] Additional context (${providerNames.length} providers, ~${totalTokens} tokens)` : `[engram] Rich context for ${filePath} (${providerNames.length} providers, ~${totalTokens} tokens)`;
1414
- const text = `${header}
1415
-
1416
- ${sections.join("\n\n")}`;
1417
- return {
1418
- text,
1419
- providerCount: providerNames.length,
1420
- providers: providerNames,
1421
- estimatedTokens: totalTokens + estimateTokens(header),
1422
- durationMs: Date.now() - start
1423
- };
1424
- }
1425
- async function warmAllProviders(projectRoot, enabledProviders) {
1426
- const start = Date.now();
1427
- const warmed = [];
1428
- const tier2 = ALL_PROVIDERS.filter(
1429
- (p) => p.tier === 2 && p.warmup && (!enabledProviders || enabledProviders.includes(p.name))
1430
- );
1431
- const available = await filterAvailable(tier2);
1432
- const settled = await Promise.allSettled(
1433
- available.map(async (p) => {
1434
- try {
1435
- const result = await withTimeout2(p.warmup(projectRoot), 5e3);
1436
- if (result && result.entries.length > 0) {
1437
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
1438
- const store = await getStore2(projectRoot);
1439
- try {
1440
- store.warmCache(
1441
- result.provider,
1442
- [...result.entries],
1443
- result.provider === "context7" ? 4 * 3600 : 3600
1444
- );
1445
- store.save();
1446
- } finally {
1447
- store.close();
1448
- }
1449
- warmed.push(p.name);
1450
- }
1451
- } catch {
1452
- }
1453
- })
1454
- );
1455
- return { warmed, durationMs: Date.now() - start };
1456
- }
1457
- var availabilityCache = /* @__PURE__ */ new Map();
1458
- async function filterAvailable(providers) {
1459
- const checks = providers.map(async (p) => {
1460
- let available = availabilityCache.get(p.name);
1461
- if (available === void 0) {
1462
- try {
1463
- const timeout = p.tier === 1 ? 200 : 500;
1464
- available = await withTimeout2(p.isAvailable(), timeout);
1465
- } catch {
1466
- available = false;
1467
- }
1468
- availabilityCache.set(p.name, available);
1469
- }
1470
- return { provider: p, available };
1471
- });
1472
- const settled = await Promise.all(checks);
1473
- return settled.filter((c) => c.available).map((c) => c.provider);
1474
- }
1475
- async function resolveWithTimeout(provider, filePath, context) {
1476
- try {
1477
- return await withTimeout2(
1478
- provider.resolve(filePath, context),
1479
- provider.timeoutMs
1480
- );
1481
- } catch {
1482
- return null;
1483
- }
1484
- }
1485
- function withTimeout2(promise, ms) {
1486
- return new Promise((resolve7, reject) => {
1487
- const timer = setTimeout(() => reject(new Error("timeout")), ms);
1488
- promise.then((val) => {
1489
- clearTimeout(timer);
1490
- resolve7(val);
1491
- }).catch((err) => {
1492
- clearTimeout(timer);
1493
- reject(err);
1494
- });
1495
- });
1496
- }
1497
-
1498
368
  // src/intercept/handlers/read.ts
1499
369
  var READ_CONFIDENCE_THRESHOLD = 0.7;
1500
370
  async function handleRead(payload) {
@@ -1670,27 +540,175 @@ async function handleBash(payload) {
1670
540
  });
1671
541
  }
1672
542
 
543
+ // src/intercept/handlers/mistake-guard.ts
544
+ import { relative as relative3 } from "path";
545
+ function currentGuardMode() {
546
+ const raw = process.env.ENGRAM_MISTAKE_GUARD;
547
+ if (raw === "1") return "permissive";
548
+ if (raw === "2") return "strict";
549
+ return "off";
550
+ }
551
+ function extractTargetResource(kind, toolInput) {
552
+ if (!toolInput) return null;
553
+ if (kind === "edit-write") {
554
+ const fp = toolInput.file_path;
555
+ if (typeof fp !== "string" || fp.length === 0) return null;
556
+ return { kind: "file", filePath: fp };
557
+ }
558
+ if (kind === "bash") {
559
+ const cmd = toolInput.command;
560
+ if (typeof cmd !== "string" || cmd.length === 0) return null;
561
+ return { kind: "command", command: cmd };
562
+ }
563
+ return null;
564
+ }
565
+ async function findMatchingMistakesAsync(target, projectRoot) {
566
+ if (!target) return [];
567
+ const now = Date.now();
568
+ try {
569
+ const store = await getStore(projectRoot);
570
+ try {
571
+ const matches = [];
572
+ if (target.kind === "file") {
573
+ let normalized = target.filePath;
574
+ try {
575
+ const rel = relative3(projectRoot, target.filePath);
576
+ if (rel && !rel.startsWith("..")) {
577
+ normalized = rel.split(/[\\/]/).join("/");
578
+ }
579
+ } catch {
580
+ }
581
+ const candidates = [
582
+ ...store.getNodesByFile(normalized),
583
+ ...normalized === target.filePath ? [] : store.getNodesByFile(target.filePath)
584
+ ];
585
+ const seenIds = /* @__PURE__ */ new Set();
586
+ for (const m of candidates) {
587
+ if (seenIds.has(m.id)) continue;
588
+ seenIds.add(m.id);
589
+ if (m.kind !== "mistake") continue;
590
+ if (m.validUntil !== void 0 && m.validUntil <= now) continue;
591
+ matches.push({
592
+ label: m.label,
593
+ sourceFile: m.sourceFile,
594
+ ageMs: now - m.lastVerified
595
+ });
596
+ }
597
+ } else {
598
+ const allMistakes = store.getAllNodes().filter((n) => n.kind === "mistake").filter((n) => n.validUntil === void 0 || n.validUntil > now);
599
+ if (allMistakes.length === 0) return [];
600
+ const command = target.command.toLowerCase();
601
+ for (const m of allMistakes) {
602
+ const pattern = m.metadata?.commandPattern;
603
+ const patternStr = typeof pattern === "string" ? pattern.toLowerCase() : "";
604
+ const fileStr = m.sourceFile.toLowerCase();
605
+ if (patternStr && patternStr.length > 2 && command.includes(patternStr)) {
606
+ matches.push({
607
+ label: m.label,
608
+ sourceFile: m.sourceFile,
609
+ ageMs: now - m.lastVerified
610
+ });
611
+ } else if (fileStr && fileStr.length > 3 && command.includes(fileStr)) {
612
+ matches.push({
613
+ label: m.label,
614
+ sourceFile: m.sourceFile,
615
+ ageMs: now - m.lastVerified
616
+ });
617
+ }
618
+ }
619
+ }
620
+ return matches;
621
+ } finally {
622
+ store.close();
623
+ }
624
+ } catch {
625
+ return [];
626
+ }
627
+ }
628
+ function formatAge(ms) {
629
+ if (ms < 0) return "unknown";
630
+ const days = Math.floor(ms / (1e3 * 60 * 60 * 24));
631
+ if (days === 0) return "today";
632
+ if (days === 1) return "yesterday";
633
+ if (days < 30) return `${days}d ago`;
634
+ return `${Math.floor(days / 30)}mo ago`;
635
+ }
636
+ function formatWarning(matches) {
637
+ if (matches.length === 0) return "";
638
+ const lines = matches.slice(0, 5).map((m) => ` \u26A0 ${m.label} (flagged ${formatAge(m.ageMs)}, file: ${m.sourceFile})`);
639
+ const more = matches.length > 5 ? `
640
+ \u2026 and ${matches.length - 5} more` : "";
641
+ return [
642
+ "\u26D4 engramx pre-mortem \u2014 this target has recurred as a mistake before:",
643
+ ...lines,
644
+ more
645
+ ].filter((s) => s.length > 0).join("\n");
646
+ }
647
+ async function applyMistakeGuard(rawResult, payload, kind) {
648
+ const mode = currentGuardMode();
649
+ if (mode === "off") return rawResult;
650
+ try {
651
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
652
+ const projectRoot = findProjectRoot(cwd);
653
+ if (!projectRoot) return rawResult;
654
+ const toolInput = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : void 0;
655
+ const target = extractTargetResource(kind, toolInput);
656
+ const matches = await findMatchingMistakesAsync(target, projectRoot);
657
+ if (matches.length === 0) return rawResult;
658
+ const warning = formatWarning(matches);
659
+ if (mode === "strict") {
660
+ return buildDenyResponse(warning);
661
+ }
662
+ if (rawResult && typeof rawResult === "object") {
663
+ const res = rawResult;
664
+ const hso = res.hookSpecificOutput && typeof res.hookSpecificOutput === "object" ? res.hookSpecificOutput : void 0;
665
+ const existingContext = typeof hso?.additionalContext === "string" ? hso.additionalContext : "";
666
+ const merged = existingContext ? `${warning}
667
+
668
+ ${existingContext}` : warning;
669
+ return {
670
+ ...res,
671
+ hookSpecificOutput: {
672
+ ...hso ?? {},
673
+ hookEventName: "PreToolUse",
674
+ permissionDecision: typeof hso?.permissionDecision === "string" ? hso.permissionDecision : "allow",
675
+ additionalContext: merged
676
+ }
677
+ };
678
+ }
679
+ return {
680
+ hookSpecificOutput: {
681
+ hookEventName: "PreToolUse",
682
+ permissionDecision: "allow",
683
+ additionalContext: warning
684
+ }
685
+ };
686
+ } catch {
687
+ return rawResult;
688
+ }
689
+ }
690
+
1673
691
  // src/intercept/handlers/session-start.ts
1674
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
1675
- import { execFile as execFile3 } from "child_process";
692
+ import { existsSync as existsSync3, readFileSync } from "fs";
693
+ import { execFile } from "child_process";
1676
694
  import { promisify } from "util";
1677
- import { basename, dirname as dirname3, join as join5, resolve as resolve2 } from "path";
1678
- var execFileAsync = promisify(execFile3);
695
+ import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "path";
696
+ var execFileAsync = promisify(execFile);
1679
697
  var MAX_GOD_NODES = 10;
1680
698
  var MAX_LANDMINES_IN_BRIEF = 3;
1681
699
  function readGitBranch(projectRoot) {
1682
700
  try {
1683
701
  let current = resolve2(projectRoot);
1684
702
  for (let depth = 0; depth < 10; depth++) {
1685
- const headPath = join5(current, ".git", "HEAD");
1686
- if (existsSync5(headPath)) {
1687
- const content = readFileSync2(headPath, "utf-8").trim();
703
+ const headPath = join3(current, ".git", "HEAD");
704
+ if (existsSync3(headPath)) {
705
+ const content = readFileSync(headPath, "utf-8").trim();
1688
706
  const refMatch = content.match(/^ref:\s+refs\/heads\/(.+)$/);
1689
707
  if (refMatch) return refMatch[1];
1690
708
  if (/^[0-9a-f]{7,40}$/i.test(content)) return "detached";
1691
709
  return null;
1692
710
  }
1693
- const parent = dirname3(current);
711
+ const parent = dirname2(current);
1694
712
  if (parent === current) return null;
1695
713
  current = parent;
1696
714
  }
@@ -1969,6 +987,219 @@ ${result.text}`;
1969
987
  return buildSessionContextResponse("UserPromptSubmit", text);
1970
988
  }
1971
989
 
990
+ // src/intercept/handlers/bash-postool.ts
991
+ import { isAbsolute as isAbsolute2, resolve as pathResolve } from "path";
992
+ var MAX_COMMAND_LEN = 500;
993
+ var BASIC_UNSAFE = /[|&;()$`*?[\]{}"']/;
994
+ var SUBSHELL = /\$\(|`|<\(|>\(/;
995
+ function parseFileOps(command, cwd) {
996
+ if (!command || typeof command !== "string") return [];
997
+ if (command.length > MAX_COMMAND_LEN) return [];
998
+ if (SUBSHELL.test(command)) return [];
999
+ const trimmed = command.trim();
1000
+ if (!trimmed) return [];
1001
+ const redirectMatch = /\s+(>>?)\s+(\S+)\s*$/.exec(trimmed);
1002
+ if (redirectMatch) {
1003
+ const head = trimmed.slice(0, redirectMatch.index);
1004
+ const dest = redirectMatch[2];
1005
+ if (BASIC_UNSAFE.test(head)) return [];
1006
+ if (dest.startsWith("-") || dest.length === 0) return [];
1007
+ return [{ action: "reindex", path: absolutize(dest, cwd) }];
1008
+ }
1009
+ if (BASIC_UNSAFE.test(trimmed)) return [];
1010
+ const tokens = trimmed.split(/\s+/);
1011
+ if (tokens.length === 0) return [];
1012
+ const first = tokens[0];
1013
+ if (first === "git" && tokens.length >= 3) {
1014
+ const sub = tokens[1];
1015
+ if (sub === "rm") return parseRm(tokens.slice(2), cwd);
1016
+ if (sub === "mv") return parseMv(tokens.slice(2), cwd);
1017
+ return [];
1018
+ }
1019
+ if (first === "rm") return parseRm(tokens.slice(1), cwd);
1020
+ if (first === "mv") return parseMv(tokens.slice(1), cwd);
1021
+ if (first === "cp") return parseCp(tokens.slice(1), cwd);
1022
+ return [];
1023
+ }
1024
+ function absolutize(path2, cwd) {
1025
+ if (isAbsolute2(path2)) return path2;
1026
+ return pathResolve(cwd, path2);
1027
+ }
1028
+ function isFlagLike(tok) {
1029
+ return tok.startsWith("-");
1030
+ }
1031
+ function parseRm(args, cwd) {
1032
+ const paths = args.filter((t) => !isFlagLike(t));
1033
+ if (paths.length === 0) return [];
1034
+ return paths.map((p) => ({ action: "prune", path: absolutize(p, cwd) }));
1035
+ }
1036
+ function parseMv(args, cwd) {
1037
+ const paths = args.filter((t) => !isFlagLike(t));
1038
+ if (paths.length !== 2) return [];
1039
+ const [src, dst] = paths;
1040
+ return [
1041
+ { action: "prune", path: absolutize(src, cwd) },
1042
+ { action: "reindex", path: absolutize(dst, cwd) }
1043
+ ];
1044
+ }
1045
+ function parseCp(args, cwd) {
1046
+ const paths = args.filter((t) => !isFlagLike(t));
1047
+ if (paths.length !== 2) return [];
1048
+ const [, dst] = paths;
1049
+ return [{ action: "reindex", path: absolutize(dst, cwd) }];
1050
+ }
1051
+ function handleBashPostTool(payload) {
1052
+ if (payload.tool_name !== "Bash") return { ops: [] };
1053
+ const cmd = payload.tool_input?.command;
1054
+ if (!cmd || typeof cmd !== "string") return { ops: [] };
1055
+ try {
1056
+ const ops = parseFileOps(cmd, payload.cwd);
1057
+ return { ops };
1058
+ } catch {
1059
+ return { ops: [] };
1060
+ }
1061
+ }
1062
+
1063
+ // src/watcher.ts
1064
+ import { watch, existsSync as existsSync4, statSync as statSync2 } from "fs";
1065
+ import { resolve as resolve3, relative as relative4, extname } from "path";
1066
+ var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1067
+ ".ts",
1068
+ ".tsx",
1069
+ ".js",
1070
+ ".jsx",
1071
+ ".py",
1072
+ ".go",
1073
+ ".rs",
1074
+ ".java",
1075
+ ".c",
1076
+ ".cpp",
1077
+ ".cs",
1078
+ ".rb"
1079
+ ]);
1080
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1081
+ ".engram",
1082
+ "node_modules",
1083
+ ".git",
1084
+ "dist",
1085
+ "build",
1086
+ ".next",
1087
+ "__pycache__",
1088
+ ".venv",
1089
+ "target",
1090
+ "vendor"
1091
+ ]);
1092
+ var DEBOUNCE_MS = 300;
1093
+ function shouldIgnore(relPath) {
1094
+ const parts = relPath.split(/[/\\]/);
1095
+ return parts.some((p) => IGNORED_DIRS.has(p));
1096
+ }
1097
+ async function syncFile(absPath, projectRoot) {
1098
+ const ext = extname(absPath).toLowerCase();
1099
+ if (!WATCHABLE_EXTENSIONS.has(ext)) return { action: "skipped", count: 0 };
1100
+ const relPath = toPosixPath(relative4(projectRoot, absPath));
1101
+ if (shouldIgnore(relPath)) return { action: "skipped", count: 0 };
1102
+ if (!existsSync4(absPath)) {
1103
+ const store2 = await getStore(projectRoot);
1104
+ try {
1105
+ const prior = store2.countBySourceFile(relPath);
1106
+ if (prior === 0) return { action: "skipped", count: 0 };
1107
+ store2.deleteBySourceFile(relPath);
1108
+ return { action: "pruned", count: prior };
1109
+ } finally {
1110
+ store2.close();
1111
+ }
1112
+ }
1113
+ try {
1114
+ if (statSync2(absPath).isDirectory()) return { action: "skipped", count: 0 };
1115
+ } catch {
1116
+ return { action: "skipped", count: 0 };
1117
+ }
1118
+ const store = await getStore(projectRoot);
1119
+ try {
1120
+ store.deleteBySourceFile(relPath);
1121
+ const { nodes, edges } = extractFile(absPath, projectRoot);
1122
+ if (nodes.length > 0 || edges.length > 0) {
1123
+ store.bulkUpsert(nodes, edges);
1124
+ }
1125
+ return { action: "indexed", count: nodes.length };
1126
+ } finally {
1127
+ store.close();
1128
+ }
1129
+ }
1130
+ function formatReindexLine(result, displayPath) {
1131
+ if (result.action === "indexed") {
1132
+ return `engram: reindexed ${displayPath} (${formatThousands(result.count)} nodes)`;
1133
+ }
1134
+ if (result.action === "pruned") {
1135
+ return `engram: pruned ${displayPath} (${formatThousands(result.count)} nodes)`;
1136
+ }
1137
+ return null;
1138
+ }
1139
+ async function runReindexHook(payload) {
1140
+ try {
1141
+ if (payload === null || typeof payload !== "object") return;
1142
+ const p = payload;
1143
+ const cwd = p.cwd;
1144
+ if (typeof cwd !== "string" || !isValidCwd(cwd)) return;
1145
+ const toolInput = p.tool_input;
1146
+ if (toolInput === null || typeof toolInput !== "object") return;
1147
+ const filePath = toolInput.file_path;
1148
+ if (typeof filePath !== "string" || filePath.length === 0) return;
1149
+ const absPath = resolve3(cwd, filePath);
1150
+ const projectRoot = findProjectRoot(absPath);
1151
+ if (projectRoot === null) return;
1152
+ await syncFile(absPath, projectRoot);
1153
+ } catch {
1154
+ }
1155
+ }
1156
+ function watchProject(projectRoot, options = {}) {
1157
+ const root = resolve3(projectRoot);
1158
+ const controller = new AbortController();
1159
+ if (!existsSync4(getDbPath(root))) {
1160
+ throw new Error(
1161
+ `engram: no graph found at ${root}. Run 'engram init' first.`
1162
+ );
1163
+ }
1164
+ const debounceTimers = /* @__PURE__ */ new Map();
1165
+ const watcher = watch(root, { recursive: true, signal: controller.signal });
1166
+ const handleEvent = (_eventType, filename) => {
1167
+ if (typeof filename !== "string") return;
1168
+ const absPath = resolve3(root, filename);
1169
+ const relPath = toPosixPath(relative4(root, absPath));
1170
+ if (shouldIgnore(relPath)) return;
1171
+ const ext = extname(filename).toLowerCase();
1172
+ if (!WATCHABLE_EXTENSIONS.has(ext)) return;
1173
+ const existing = debounceTimers.get(absPath);
1174
+ if (existing) clearTimeout(existing);
1175
+ debounceTimers.set(
1176
+ absPath,
1177
+ setTimeout(async () => {
1178
+ debounceTimers.delete(absPath);
1179
+ try {
1180
+ const result = await syncFile(absPath, root);
1181
+ if (result.action === "indexed" && result.count > 0) {
1182
+ options.onReindex?.(relPath, result.count);
1183
+ } else if (result.action === "pruned") {
1184
+ options.onDelete?.(relPath, result.count);
1185
+ }
1186
+ } catch (err) {
1187
+ options.onError?.(
1188
+ err instanceof Error ? err : new Error(String(err))
1189
+ );
1190
+ }
1191
+ }, DEBOUNCE_MS)
1192
+ );
1193
+ };
1194
+ watcher.on("change", handleEvent);
1195
+ watcher.on("rename", handleEvent);
1196
+ watcher.on("error", (err) => {
1197
+ options.onError?.(err instanceof Error ? err : new Error(String(err)));
1198
+ });
1199
+ options.onReady?.();
1200
+ return controller;
1201
+ }
1202
+
1972
1203
  // src/intercept/handlers/post-tool.ts
1973
1204
  function extractFilePath(toolName, toolInput) {
1974
1205
  if (!toolInput) return void 0;
@@ -2019,13 +1250,34 @@ async function handlePostTool(payload) {
2019
1250
  outputSize,
2020
1251
  success: !hasError
2021
1252
  });
1253
+ if (toolName === "Bash" && !hasError && process.env.ENGRAM_AUTO_REINDEX === "1") {
1254
+ void reindexBashOps(payload, projectRoot).catch(() => {
1255
+ });
1256
+ }
2022
1257
  } catch {
2023
1258
  }
2024
1259
  return PASSTHROUGH;
2025
1260
  }
1261
+ async function reindexBashOps(payload, projectRoot) {
1262
+ const result = handleBashPostTool({
1263
+ tool_name: payload.tool_name ?? "",
1264
+ tool_input: payload.tool_input ?? {},
1265
+ cwd: payload.cwd
1266
+ });
1267
+ if (result.ops.length === 0) return;
1268
+ for (const op of result.ops) {
1269
+ await runOp(op, projectRoot);
1270
+ }
1271
+ }
1272
+ async function runOp(op, projectRoot) {
1273
+ try {
1274
+ await syncFile(op.path, projectRoot);
1275
+ } catch {
1276
+ }
1277
+ }
2026
1278
 
2027
1279
  // src/intercept/handlers/pre-compact.ts
2028
- import { basename as basename2, resolve as resolve3 } from "path";
1280
+ import { basename as basename2, resolve as resolve4 } from "path";
2029
1281
  var MAX_GOD_NODES_COMPACT = 5;
2030
1282
  var MAX_LANDMINES_COMPACT = 3;
2031
1283
  function formatCompactBrief(args) {
@@ -2075,7 +1327,7 @@ async function handlePreCompact(payload) {
2075
1327
  }))
2076
1328
  ]);
2077
1329
  if (graphStats.nodes === 0 && gods.length === 0) return PASSTHROUGH;
2078
- const projectName = basename2(resolve3(projectRoot));
1330
+ const projectName = basename2(resolve4(projectRoot));
2079
1331
  const text = formatCompactBrief({
2080
1332
  projectName,
2081
1333
  nodeCount: graphStats.nodes,
@@ -2097,7 +1349,7 @@ async function handlePreCompact(payload) {
2097
1349
  }
2098
1350
 
2099
1351
  // src/intercept/handlers/cwd-changed.ts
2100
- import { basename as basename3, resolve as resolve4 } from "path";
1352
+ import { basename as basename3, resolve as resolve5 } from "path";
2101
1353
  var MAX_GOD_NODES_SWITCH = 5;
2102
1354
  async function handleCwdChanged(payload) {
2103
1355
  if (payload.hook_event_name !== "CwdChanged") return PASSTHROUGH;
@@ -2121,7 +1373,7 @@ async function handleCwdChanged(payload) {
2121
1373
  }))
2122
1374
  ]);
2123
1375
  if (graphStats.nodes === 0) return PASSTHROUGH;
2124
- const projectName = basename3(resolve4(projectRoot));
1376
+ const projectName = basename3(resolve5(projectRoot));
2125
1377
  const lines = [];
2126
1378
  lines.push(
2127
1379
  `[engram] Project switched to ${projectName} (${graphStats.nodes} nodes, ${graphStats.edges} edges)`
@@ -2198,11 +1450,13 @@ async function dispatchPreToolUse(payload) {
2198
1450
  result = await runHandler(
2199
1451
  () => handleEditOrWrite(handlerPayload)
2200
1452
  );
1453
+ result = await applyMistakeGuard(result, handlerPayload, "edit-write");
2201
1454
  break;
2202
1455
  case "Bash":
2203
1456
  result = await runHandler(
2204
1457
  () => handleBash(handlerPayload)
2205
1458
  );
1459
+ result = await applyMistakeGuard(result, handlerPayload, "bash");
2206
1460
  break;
2207
1461
  default:
2208
1462
  return PASSTHROUGH;
@@ -2238,110 +1492,10 @@ function extractPreToolDecision(result) {
2238
1492
  return "passthrough";
2239
1493
  }
2240
1494
 
2241
- // src/watcher.ts
2242
- import { watch, existsSync as existsSync6, statSync as statSync2 } from "fs";
2243
- import { resolve as resolve5, relative as relative3, extname } from "path";
2244
- var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2245
- ".ts",
2246
- ".tsx",
2247
- ".js",
2248
- ".jsx",
2249
- ".py",
2250
- ".go",
2251
- ".rs",
2252
- ".java",
2253
- ".c",
2254
- ".cpp",
2255
- ".cs",
2256
- ".rb"
2257
- ]);
2258
- var IGNORED_DIRS = /* @__PURE__ */ new Set([
2259
- ".engram",
2260
- "node_modules",
2261
- ".git",
2262
- "dist",
2263
- "build",
2264
- ".next",
2265
- "__pycache__",
2266
- ".venv",
2267
- "target",
2268
- "vendor"
2269
- ]);
2270
- var DEBOUNCE_MS = 300;
2271
- function shouldIgnore(relPath) {
2272
- const parts = relPath.split(/[/\\]/);
2273
- return parts.some((p) => IGNORED_DIRS.has(p));
2274
- }
2275
- async function reindexFile(absPath, projectRoot) {
2276
- const ext = extname(absPath).toLowerCase();
2277
- if (!WATCHABLE_EXTENSIONS.has(ext)) return 0;
2278
- if (!existsSync6(absPath)) return 0;
2279
- try {
2280
- if (statSync2(absPath).isDirectory()) return 0;
2281
- } catch {
2282
- return 0;
2283
- }
2284
- const relPath = toPosixPath(relative3(projectRoot, absPath));
2285
- if (shouldIgnore(relPath)) return 0;
2286
- const store = await getStore(projectRoot);
2287
- try {
2288
- store.deleteBySourceFile(relPath);
2289
- const { nodes, edges } = extractFile(absPath, projectRoot);
2290
- if (nodes.length > 0 || edges.length > 0) {
2291
- store.bulkUpsert(nodes, edges);
2292
- }
2293
- return nodes.length;
2294
- } finally {
2295
- store.close();
2296
- }
2297
- }
2298
- function watchProject(projectRoot, options = {}) {
2299
- const root = resolve5(projectRoot);
2300
- const controller = new AbortController();
2301
- if (!existsSync6(getDbPath(root))) {
2302
- throw new Error(
2303
- `engram: no graph found at ${root}. Run 'engram init' first.`
2304
- );
2305
- }
2306
- const debounceTimers = /* @__PURE__ */ new Map();
2307
- const watcher = watch(root, { recursive: true, signal: controller.signal });
2308
- watcher.on("change", (_eventType, filename) => {
2309
- if (typeof filename !== "string") return;
2310
- const absPath = resolve5(root, filename);
2311
- const relPath = toPosixPath(relative3(root, absPath));
2312
- if (shouldIgnore(relPath)) return;
2313
- const ext = extname(filename).toLowerCase();
2314
- if (!WATCHABLE_EXTENSIONS.has(ext)) return;
2315
- const existing = debounceTimers.get(absPath);
2316
- if (existing) clearTimeout(existing);
2317
- debounceTimers.set(
2318
- absPath,
2319
- setTimeout(async () => {
2320
- debounceTimers.delete(absPath);
2321
- try {
2322
- const count = await reindexFile(absPath, root);
2323
- if (count > 0) {
2324
- options.onReindex?.(relPath, count);
2325
- }
2326
- } catch (err) {
2327
- options.onError?.(
2328
- err instanceof Error ? err : new Error(String(err))
2329
- );
2330
- }
2331
- }, DEBOUNCE_MS)
2332
- );
2333
- });
2334
- watcher.on("error", (err) => {
2335
- options.onError?.(err instanceof Error ? err : new Error(String(err)));
2336
- });
2337
- options.onReady?.();
2338
- return controller;
2339
- }
2340
-
2341
1495
  // src/dashboard.ts
2342
1496
  import chalk from "chalk";
2343
- import { existsSync as existsSync7, statSync as statSync3 } from "fs";
2344
- import { join as join7, resolve as resolve6, basename as basename4 } from "path";
1497
+ import { existsSync as existsSync5, statSync as statSync3 } from "fs";
1498
+ import { join as join5, resolve as resolve6, basename as basename4 } from "path";
2345
1499
  var AMBER = chalk.hex("#d97706");
2346
1500
  var DIM = chalk.dim;
2347
1501
  var GREEN = chalk.green;
@@ -2353,9 +1507,7 @@ function bar(pct, width = 20) {
2353
1507
  const empty = width - filled;
2354
1508
  return AMBER("\u2588".repeat(filled)) + DIM("\u2591".repeat(empty));
2355
1509
  }
2356
- function fmt(n) {
2357
- return n.toLocaleString();
2358
- }
1510
+ var fmt = formatThousands;
2359
1511
  function topFiles(entries, n) {
2360
1512
  const counts = /* @__PURE__ */ new Map();
2361
1513
  for (const e of entries) {
@@ -2463,8 +1615,8 @@ function startDashboard(projectRoot, options = {}) {
2463
1615
  const tick = () => {
2464
1616
  if (controller.signal.aborted) return;
2465
1617
  try {
2466
- const logPath = join7(root, ".engram", "hook-log.jsonl");
2467
- if (existsSync7(logPath)) {
1618
+ const logPath = join5(root, ".engram", "hook-log.jsonl");
1619
+ if (existsSync5(logPath)) {
2468
1620
  const currentSize = statSync3(logPath).size;
2469
1621
  if (currentSize !== lastSize) {
2470
1622
  cachedEntries = readHookLog(root);
@@ -2522,165 +1674,15 @@ async function handleCursorBeforeReadFile(payload) {
2522
1674
  }
2523
1675
  }
2524
1676
 
2525
- // src/intercept/installer.ts
2526
- var ENGRAM_HOOK_EVENTS = [
2527
- "PreToolUse",
2528
- "PostToolUse",
2529
- "SessionStart",
2530
- "UserPromptSubmit",
2531
- "PreCompact",
2532
- "CwdChanged"
2533
- ];
2534
- var ENGRAM_PRETOOL_MATCHER = "Read|Edit|Write|Bash";
2535
- var DEFAULT_ENGRAM_COMMAND = "engram intercept";
2536
- var DEFAULT_HOOK_TIMEOUT_SEC = 5;
2537
- var DEFAULT_STATUSLINE_COMMAND = "engram hud-label";
2538
- function buildEngramHookEntries(command = DEFAULT_ENGRAM_COMMAND, timeout = DEFAULT_HOOK_TIMEOUT_SEC) {
2539
- const baseCmd = {
2540
- type: "command",
2541
- command,
2542
- timeout
2543
- };
2544
- return {
2545
- PreToolUse: {
2546
- matcher: ENGRAM_PRETOOL_MATCHER,
2547
- hooks: [baseCmd]
2548
- },
2549
- PostToolUse: {
2550
- // Match all tools — PostToolUse is an observer for any completion.
2551
- matcher: ".*",
2552
- hooks: [baseCmd]
2553
- },
2554
- SessionStart: {
2555
- // No matcher — SessionStart has no tool name.
2556
- hooks: [baseCmd]
2557
- },
2558
- UserPromptSubmit: {
2559
- // No matcher — UserPromptSubmit has no tool name.
2560
- hooks: [baseCmd]
2561
- },
2562
- PreCompact: {
2563
- // No matcher — PreCompact has no tool name.
2564
- hooks: [baseCmd]
2565
- },
2566
- CwdChanged: {
2567
- // No matcher — CwdChanged has no tool name.
2568
- hooks: [baseCmd]
2569
- }
2570
- };
2571
- }
2572
- function isEngramHookEntry(entry) {
2573
- if (entry === null || typeof entry !== "object") return false;
2574
- const e = entry;
2575
- if (!Array.isArray(e.hooks)) return false;
2576
- for (const h of e.hooks) {
2577
- if (h === null || typeof h !== "object") continue;
2578
- const cmd = h.command;
2579
- if (typeof cmd === "string" && cmd.includes("engram intercept")) {
2580
- return true;
2581
- }
2582
- }
2583
- return false;
2584
- }
2585
- function installEngramHooks(settings, command = DEFAULT_ENGRAM_COMMAND) {
2586
- const entries = buildEngramHookEntries(command);
2587
- const added = [];
2588
- const alreadyPresent = [];
2589
- const hooksClone = {};
2590
- const existingHooks = settings.hooks ?? {};
2591
- for (const [key, value] of Object.entries(existingHooks)) {
2592
- if (Array.isArray(value)) {
2593
- hooksClone[key] = value.map((entry) => ({ ...entry }));
2594
- }
2595
- }
2596
- for (const event of ENGRAM_HOOK_EVENTS) {
2597
- const eventArr = hooksClone[event] ?? [];
2598
- const hasEngram = eventArr.some((e) => isEngramHookEntry(e));
2599
- if (hasEngram) {
2600
- alreadyPresent.push(event);
2601
- hooksClone[event] = eventArr;
2602
- continue;
2603
- }
2604
- hooksClone[event] = [...eventArr, entries[event]];
2605
- added.push(event);
2606
- }
2607
- const hasStatusLine = settings.statusLine && typeof settings.statusLine === "object" && typeof settings.statusLine.command === "string" && settings.statusLine.command.length > 0;
2608
- const statusLineAdded = !hasStatusLine;
2609
- const statusLine = hasStatusLine ? settings.statusLine : { type: "command", command: DEFAULT_STATUSLINE_COMMAND };
2610
- return {
2611
- updated: { ...settings, hooks: hooksClone, statusLine },
2612
- added,
2613
- alreadyPresent,
2614
- statusLineAdded
2615
- };
2616
- }
2617
- function uninstallEngramHooks(settings) {
2618
- const removed = [];
2619
- const existingHooks = settings.hooks ?? {};
2620
- const hooksClone = {};
2621
- for (const [event, arr] of Object.entries(existingHooks)) {
2622
- if (!Array.isArray(arr)) continue;
2623
- const filtered = arr.filter((entry) => !isEngramHookEntry(entry));
2624
- if (filtered.length !== arr.length && isKnownEngramEvent(event)) {
2625
- removed.push(event);
2626
- }
2627
- if (filtered.length > 0) {
2628
- hooksClone[event] = filtered;
2629
- }
2630
- }
2631
- const updatedSettings = { ...settings };
2632
- if (Object.keys(hooksClone).length === 0) {
2633
- delete updatedSettings.hooks;
2634
- } else {
2635
- updatedSettings.hooks = hooksClone;
2636
- }
2637
- const statusLineRemoved = typeof updatedSettings.statusLine?.command === "string" && updatedSettings.statusLine.command.includes("engram hud-label");
2638
- if (statusLineRemoved) {
2639
- delete updatedSettings.statusLine;
2640
- }
2641
- return { updated: updatedSettings, removed, statusLineRemoved };
2642
- }
2643
- function isKnownEngramEvent(event) {
2644
- return ENGRAM_HOOK_EVENTS.includes(event);
2645
- }
2646
- function formatInstallDiff(before, after) {
2647
- const lines = [];
2648
- const beforeHooks = before.hooks ?? {};
2649
- const afterHooks = after.hooks ?? {};
2650
- for (const event of ENGRAM_HOOK_EVENTS) {
2651
- const beforeArr = beforeHooks[event] ?? [];
2652
- const afterArr = afterHooks[event] ?? [];
2653
- if (beforeArr.length === afterArr.length) continue;
2654
- lines.push(`+ ${event}: ${beforeArr.length} \u2192 ${afterArr.length} entries`);
2655
- const added = afterArr.filter((entry) => isEngramHookEntry(entry));
2656
- const beforeHasEngram = beforeArr.some((entry) => isEngramHookEntry(entry));
2657
- if (!beforeHasEngram && added.length > 0) {
2658
- for (const entry of added) {
2659
- const matcher = entry.matcher ? ` matcher=${JSON.stringify(entry.matcher)}` : "";
2660
- const cmds = entry.hooks.map((h) => h.command).join(", ");
2661
- lines.push(` + {${matcher} command="${cmds}"}`);
2662
- }
2663
- }
2664
- }
2665
- const hadStatusLine = before.statusLine?.command;
2666
- const hasStatusLineNow = after.statusLine?.command;
2667
- if (!hadStatusLine && hasStatusLineNow?.includes("engram hud-label")) {
2668
- lines.push(`+ statusLine: engram hud-label (HUD enabled)`);
2669
- } else if (hadStatusLine?.includes("engram hud-label") && !hasStatusLineNow) {
2670
- lines.push(`- statusLine: engram hud-label (HUD removed)`);
2671
- }
2672
- return lines.length > 0 ? lines.join("\n") : "(no changes)";
2673
- }
2674
-
2675
1677
  // src/intercept/memory-md.ts
2676
1678
  import {
2677
- existsSync as existsSync8,
2678
- readFileSync as readFileSync4,
1679
+ existsSync as existsSync6,
1680
+ readFileSync as readFileSync3,
2679
1681
  writeFileSync,
2680
1682
  renameSync,
2681
1683
  statSync as statSync4
2682
1684
  } from "fs";
2683
- import { join as join8 } from "path";
1685
+ import { join as join6 } from "path";
2684
1686
  var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
2685
1687
  var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
2686
1688
  var MAX_MEMORY_FILE_BYTES = 1e6;
@@ -2751,15 +1753,15 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
2751
1753
  if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
2752
1754
  return false;
2753
1755
  }
2754
- const memoryPath = join8(projectRoot, "MEMORY.md");
1756
+ const memoryPath = join6(projectRoot, "MEMORY.md");
2755
1757
  try {
2756
1758
  let existing = "";
2757
- if (existsSync8(memoryPath)) {
1759
+ if (existsSync6(memoryPath)) {
2758
1760
  const st = statSync4(memoryPath);
2759
1761
  if (st.size > MAX_MEMORY_FILE_BYTES) {
2760
1762
  return false;
2761
1763
  }
2762
- existing = readFileSync4(memoryPath, "utf-8");
1764
+ existing = readFileSync3(memoryPath, "utf-8");
2763
1765
  }
2764
1766
  const updated = upsertEngramSection(existing, engramSection);
2765
1767
  const tmpPath = memoryPath + ".engram-tmp";
@@ -2773,9 +1775,9 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
2773
1775
 
2774
1776
  // src/cli.ts
2775
1777
  import { basename as basename5 } from "path";
2776
- import { createRequire as createRequire2 } from "module";
2777
- var require3 = createRequire2(import.meta.url);
2778
- var { version: PKG_VERSION } = require3("../package.json");
1778
+ import { createRequire } from "module";
1779
+ var require2 = createRequire(import.meta.url);
1780
+ var { version: PKG_VERSION } = require2("../package.json");
2779
1781
  var program = new Command();
2780
1782
  program.name("engram").description(
2781
1783
  "Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
@@ -2786,6 +1788,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2786
1788
  ).option("--from-ccs", "Import .context/index.md (CCS) into graph after init").option(
2787
1789
  "--incremental",
2788
1790
  "Skip unchanged files (mtime-based). Dramatically faster on re-index of large repos."
1791
+ ).option(
1792
+ "--with-hook",
1793
+ "Also install the Sentinel hook into Claude Code settings.local.json (idempotent)"
2789
1794
  ).action(async (projectPath, opts) => {
2790
1795
  console.log(chalk2.dim(opts.incremental ? "\u{1F50D} Scanning changed files..." : "\u{1F50D} Scanning codebase..."));
2791
1796
  const result = await init(projectPath, {
@@ -2796,7 +1801,7 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2796
1801
  chalk2.green("\u{1F333} AST extraction complete") + chalk2.dim(` (${result.timeMs}ms, 0 tokens used)`)
2797
1802
  );
2798
1803
  console.log(
2799
- ` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${result.totalLines.toLocaleString()} lines)`
1804
+ ` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${formatThousands(result.totalLines)} lines)`
2800
1805
  );
2801
1806
  if (result.incremental && result.skippedFiles && result.skippedFiles > 0) {
2802
1807
  console.log(chalk2.dim(` ${result.skippedFiles} unchanged files skipped (incremental mode)`));
@@ -2813,15 +1818,15 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2813
1818
  \u{1F4CA} Token savings: ${chalk2.bold(bench.reductionVsRelevant + "x")} fewer tokens vs relevant files (${bench.reductionVsFull}x vs full corpus)`)
2814
1819
  );
2815
1820
  console.log(
2816
- chalk2.dim(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens | Graph query: ~${bench.avgQueryTokens.toLocaleString()} tokens`)
1821
+ chalk2.dim(` Full corpus: ~${formatThousands(bench.naiveFullCorpus)} tokens | Graph query: ~${formatThousands(bench.avgQueryTokens)} tokens`)
2817
1822
  );
2818
1823
  }
2819
1824
  console.log(chalk2.green("\n\u2705 Ready. Your AI now has persistent memory."));
2820
1825
  console.log(chalk2.dim(" Graph stored in .engram/graph.db"));
2821
- const resolvedProject = pathResolve(projectPath);
2822
- const localSettings = join9(resolvedProject, ".claude", "settings.local.json");
2823
- const projectSettings = join9(resolvedProject, ".claude", "settings.json");
2824
- const hasHooks = existsSync9(localSettings) && readFileSync5(localSettings, "utf-8").includes("engram intercept") || existsSync9(projectSettings) && readFileSync5(projectSettings, "utf-8").includes("engram intercept");
1826
+ const resolvedProject = pathResolve2(projectPath);
1827
+ const localSettings = join7(resolvedProject, ".claude", "settings.local.json");
1828
+ const projectSettings = join7(resolvedProject, ".claude", "settings.json");
1829
+ const hasHooks = existsSync7(localSettings) && readFileSync4(localSettings, "utf-8").includes("engram intercept") || existsSync7(projectSettings) && readFileSync4(projectSettings, "utf-8").includes("engram intercept");
2825
1830
  if (!hasHooks) {
2826
1831
  console.log(
2827
1832
  chalk2.yellow("\n\u{1F4A1} Next step: ") + chalk2.white("engram install-hook") + chalk2.dim(
@@ -2834,9 +1839,59 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2834
1839
  )
2835
1840
  );
2836
1841
  }
1842
+ if (opts.withHook) {
1843
+ const localSettingsPath = join7(
1844
+ pathResolve2(projectPath),
1845
+ ".claude",
1846
+ "settings.local.json"
1847
+ );
1848
+ let settings = {};
1849
+ if (existsSync7(localSettingsPath)) {
1850
+ try {
1851
+ const raw = readFileSync4(localSettingsPath, "utf-8");
1852
+ settings = raw.trim() ? JSON.parse(raw) : {};
1853
+ } catch {
1854
+ console.log(
1855
+ chalk2.yellow(
1856
+ "\n \u26A0 --with-hook: settings.local.json is invalid JSON, skipping hook install."
1857
+ )
1858
+ );
1859
+ settings = {};
1860
+ }
1861
+ }
1862
+ const hookResult = installEngramHooks(settings);
1863
+ if (hookResult.added.length > 0 || hookResult.statusLineAdded) {
1864
+ try {
1865
+ mkdirSync(dirname3(localSettingsPath), { recursive: true });
1866
+ writeFileSync2(
1867
+ localSettingsPath,
1868
+ JSON.stringify(hookResult.updated, null, 2) + "\n"
1869
+ );
1870
+ console.log(
1871
+ chalk2.green(
1872
+ `
1873
+ \u2705 --with-hook: installed ${hookResult.added.length} hook event${hookResult.added.length === 1 ? "" : "s"} into .claude/settings.local.json`
1874
+ )
1875
+ );
1876
+ } catch (err) {
1877
+ console.log(
1878
+ chalk2.yellow(
1879
+ `
1880
+ \u26A0 --with-hook: write failed (${err.message})`
1881
+ )
1882
+ );
1883
+ }
1884
+ } else {
1885
+ console.log(
1886
+ chalk2.dim(
1887
+ "\n --with-hook: Sentinel hook already installed, nothing to do."
1888
+ )
1889
+ );
1890
+ }
1891
+ }
2837
1892
  if (opts.fromCcs) {
2838
- const { importCcs } = await import("./importer-V62NGZRK.js");
2839
- const resolvedProjectPath = pathResolve(projectPath);
1893
+ const { importCcs } = await import("./importer-4UWQDH4W.js");
1894
+ const resolvedProjectPath = pathResolve2(projectPath);
2840
1895
  const ccsResult = await importCcs(resolvedProjectPath);
2841
1896
  if (ccsResult.nodesCreated > 0) {
2842
1897
  console.log(
@@ -2850,7 +1905,7 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2850
1905
  }
2851
1906
  });
2852
1907
  program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2853
- const resolvedPath = pathResolve(projectPath);
1908
+ const resolvedPath = pathResolve2(projectPath);
2854
1909
  console.log(
2855
1910
  chalk2.dim("\u{1F441} Watching ") + chalk2.white(resolvedPath) + chalk2.dim(" for changes...")
2856
1911
  );
@@ -2860,6 +1915,11 @@ program.command("watch").description("Watch project for file changes and re-inde
2860
1915
  chalk2.green(" \u21BB ") + chalk2.white(filePath) + chalk2.dim(` (${nodeCount} nodes)`)
2861
1916
  );
2862
1917
  },
1918
+ onDelete: (filePath, prunedCount) => {
1919
+ console.log(
1920
+ chalk2.yellow(" \xD7 ") + chalk2.white(filePath) + chalk2.dim(` pruned (${prunedCount} nodes)`)
1921
+ );
1922
+ },
2863
1923
  onError: (err) => {
2864
1924
  console.error(chalk2.red(" \u2717 ") + err.message);
2865
1925
  },
@@ -2875,10 +1935,70 @@ program.command("watch").description("Watch project for file changes and re-inde
2875
1935
  await new Promise(() => {
2876
1936
  });
2877
1937
  });
1938
+ program.command("reindex").description("Re-index a single file into the knowledge graph").argument("<file>", "File path (absolute or relative to --project)").option("-p, --project <path>", "Project directory", ".").option("--verbose", "Print stack traces on error", false).action(
1939
+ async (file, opts) => {
1940
+ const root = pathResolve2(opts.project);
1941
+ if (!existsSync7(join7(root, ".engram", "graph.db"))) {
1942
+ console.error(
1943
+ `engram: no graph found at ${root}. Run 'engram init' first.`
1944
+ );
1945
+ process.exit(1);
1946
+ }
1947
+ const absFile = pathResolve2(root, file);
1948
+ try {
1949
+ const result = await syncFile(absFile, root);
1950
+ const line = formatReindexLine(result, file);
1951
+ if (line !== null) console.log(line);
1952
+ process.exitCode = 0;
1953
+ } catch (err) {
1954
+ const msg = err instanceof Error ? err.message : String(err);
1955
+ console.error(`engram: ${msg}`);
1956
+ if (opts.verbose && err instanceof Error && err.stack) {
1957
+ console.error(err.stack);
1958
+ }
1959
+ process.exit(1);
1960
+ }
1961
+ }
1962
+ );
1963
+ program.command("reindex-hook").description(
1964
+ "PostToolUse hook entry point: reads JSON from stdin, reindexes tool_input.file_path (always exits 0)"
1965
+ ).action(async () => {
1966
+ const stdinTimeout = setTimeout(() => {
1967
+ process.exit(0);
1968
+ }, 3e3);
1969
+ stdinTimeout.unref();
1970
+ let input = "";
1971
+ let stdinFailed = false;
1972
+ try {
1973
+ for await (const chunk of process.stdin) {
1974
+ input += chunk;
1975
+ if (input.length > 1e6) break;
1976
+ }
1977
+ } catch {
1978
+ stdinFailed = true;
1979
+ }
1980
+ clearTimeout(stdinTimeout);
1981
+ if (stdinFailed || !input.trim()) {
1982
+ process.exitCode = 0;
1983
+ return;
1984
+ }
1985
+ let payload;
1986
+ try {
1987
+ payload = JSON.parse(input);
1988
+ } catch {
1989
+ process.exitCode = 0;
1990
+ return;
1991
+ }
1992
+ try {
1993
+ await runReindexHook(payload);
1994
+ } catch {
1995
+ }
1996
+ process.exitCode = 0;
1997
+ });
2878
1998
  program.command("dashboard").alias("hud").description("Live terminal dashboard showing hook activity and token savings").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2879
- const resolvedPath = pathResolve(projectPath);
2880
- const dbPath = join9(resolvedPath, ".engram", "graph.db");
2881
- if (!existsSync9(dbPath)) {
1999
+ const resolvedPath = pathResolve2(projectPath);
2000
+ const dbPath = join7(resolvedPath, ".engram", "graph.db");
2001
+ if (!existsSync7(dbPath)) {
2882
2002
  console.error(
2883
2003
  chalk2.red("No engram graph found at ") + chalk2.white(resolvedPath)
2884
2004
  );
@@ -2895,14 +2015,14 @@ program.command("dashboard").alias("hud").description("Live terminal dashboard s
2895
2015
  });
2896
2016
  });
2897
2017
  program.command("hud-label").description("Output JSON label for Claude HUD --extra-cmd (fast, <20ms)").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2898
- let resolvedPath = pathResolve(projectPath);
2018
+ let resolvedPath = pathResolve2(projectPath);
2899
2019
  let found = false;
2900
2020
  for (let depth = 0; depth < 20; depth++) {
2901
- if (existsSync9(join9(resolvedPath, ".engram", "graph.db"))) {
2021
+ if (existsSync7(join7(resolvedPath, ".engram", "graph.db"))) {
2902
2022
  found = true;
2903
2023
  break;
2904
2024
  }
2905
- const parent = dirname4(resolvedPath);
2025
+ const parent = dirname3(resolvedPath);
2906
2026
  if (parent === resolvedPath) break;
2907
2027
  resolvedPath = parent;
2908
2028
  }
@@ -2910,8 +2030,8 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
2910
2030
  console.log('{"label":""}');
2911
2031
  return;
2912
2032
  }
2913
- const logPath = join9(resolvedPath, ".engram", "hook-log.jsonl");
2914
- if (!existsSync9(logPath)) {
2033
+ const logPath = join7(resolvedPath, ".engram", "hook-log.jsonl");
2034
+ if (!existsSync7(logPath)) {
2915
2035
  console.log('{"label":"\u26A1engram \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 ready"}');
2916
2036
  return;
2917
2037
  }
@@ -2991,8 +2111,8 @@ program.command("stats").description("Show knowledge graph statistics and token
2991
2111
  if (bench.naiveFullCorpus > 0) {
2992
2112
  console.log(`
2993
2113
  ${chalk2.cyan("Token savings:")}`);
2994
- console.log(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens`);
2995
- console.log(` Avg query: ~${bench.avgQueryTokens.toLocaleString()} tokens`);
2114
+ console.log(` Full corpus: ~${formatThousands(bench.naiveFullCorpus)} tokens`);
2115
+ console.log(` Avg query: ~${formatThousands(bench.avgQueryTokens)} tokens`);
2996
2116
  console.log(` vs relevant: ${chalk2.bold.cyan(bench.reductionVsRelevant + "x")} fewer tokens`);
2997
2117
  console.log(` vs full: ${chalk2.bold.cyan(bench.reductionVsFull + "x")} fewer tokens`);
2998
2118
  }
@@ -3036,8 +2156,8 @@ program.command("mistakes").description("List known mistakes extracted from past
3036
2156
  program.command("bench").description("Run token reduction benchmark").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3037
2157
  const result = await benchmark(opts.project);
3038
2158
  console.log(chalk2.bold("\n\u26A1 engram token reduction benchmark\n"));
3039
- console.log(` Full corpus: ~${result.naiveFullCorpus.toLocaleString()} tokens`);
3040
- console.log(` Avg graph query: ~${result.avgQueryTokens.toLocaleString()} tokens`);
2159
+ console.log(` Full corpus: ~${formatThousands(result.naiveFullCorpus)} tokens`);
2160
+ console.log(` Avg graph query: ~${formatThousands(result.avgQueryTokens)} tokens`);
3041
2161
  console.log(` vs relevant: ${chalk2.bold.green(result.reductionVsRelevant + "x")} fewer tokens`);
3042
2162
  console.log(` vs full corpus: ${chalk2.bold.green(result.reductionVsFull + "x")} fewer tokens
3043
2163
  `);
@@ -3050,22 +2170,28 @@ var hooks = program.command("hooks").description("Manage git hooks");
3050
2170
  hooks.command("install").description("Install post-commit and post-checkout hooks").argument("[path]", "Project directory", ".").action((p) => console.log(install(p)));
3051
2171
  hooks.command("uninstall").description("Remove engram git hooks").argument("[path]", "Project directory", ".").action((p) => console.log(uninstall(p)));
3052
2172
  hooks.command("status").description("Check if hooks are installed").argument("[path]", "Project directory", ".").action((p) => console.log(status(p)));
3053
- program.command("gen").description("Generate CLAUDE.md / .cursorrules section from graph").option("-p, --project <path>", "Project directory", ".").option("-t, --target <type>", "Target file: claude, cursor, agents").option(
2173
+ program.command("gen").description(
2174
+ "Generate CLAUDE.md + AGENTS.md (default) or a single file via --target"
2175
+ ).option("-p, --project <path>", "Project directory", ".").option(
2176
+ "-t, --target <type>",
2177
+ "Single-file target: claude, cursor, agents. Default: emit both CLAUDE.md and AGENTS.md."
2178
+ ).option(
3054
2179
  "--task <name>",
3055
2180
  "Task-aware view: general (default), bug-fix, feature, refactor"
3056
2181
  ).action(
3057
2182
  async (opts) => {
3058
2183
  const target = opts.target;
3059
2184
  const result = await autogen(opts.project, target, opts.task);
2185
+ const fileList = result.files.map((f) => chalk2.bold(f)).join(", ");
3060
2186
  console.log(
3061
2187
  chalk2.green(
3062
- `\u2705 Updated ${result.file} (${result.nodesIncluded} nodes, view: ${result.view})`
2188
+ `\u2705 Updated ${fileList} (${result.nodesIncluded} nodes, view: ${result.view})`
3063
2189
  )
3064
2190
  );
3065
2191
  }
3066
2192
  );
3067
2193
  program.command("gen-mdc").description("Generate .cursor/rules/engram-context.mdc from knowledge graph").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3068
- const { generateCursorMdc } = await import("./cursor-mdc-GJ7E5LDD.js");
2194
+ const { generateCursorMdc } = await import("./cursor-mdc-EEO7PYZ3.js");
3069
2195
  const result = await generateCursorMdc(opts.project);
3070
2196
  console.log(
3071
2197
  chalk2.green(
@@ -3073,11 +2199,15 @@ program.command("gen-mdc").description("Generate .cursor/rules/engram-context.md
3073
2199
  )
3074
2200
  );
3075
2201
  if (opts.watch) {
3076
- watchProject(pathResolve(opts.project), {
2202
+ watchProject(pathResolve2(opts.project), {
3077
2203
  onReindex: async () => {
3078
2204
  const r = await generateCursorMdc(opts.project);
3079
2205
  console.log(chalk2.dim(` \u21BB Regenerated MDC (${r.nodes} nodes)`));
3080
2206
  },
2207
+ onDelete: async () => {
2208
+ const r = await generateCursorMdc(opts.project);
2209
+ console.log(chalk2.dim(` \xD7 Regenerated MDC (${r.nodes} nodes)`));
2210
+ },
3081
2211
  onError: (err) => console.error(chalk2.red(err.message)),
3082
2212
  onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3083
2213
  });
@@ -3086,8 +2216,8 @@ program.command("gen-mdc").description("Generate .cursor/rules/engram-context.md
3086
2216
  }
3087
2217
  });
3088
2218
  program.command("gen-ccs").description("Export knowledge graph as .context/index.md (CCS format)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3089
- const { exportCcs } = await import("./exporter-GWU2GF23.js");
3090
- const result = await exportCcs(pathResolve(opts.project));
2219
+ const { exportCcs } = await import("./exporter-ZYJ4WM2F.js");
2220
+ const result = await exportCcs(pathResolve2(opts.project));
3091
2221
  console.log(
3092
2222
  chalk2.green(
3093
2223
  `\u2705 Generated ${result.filePath} (${result.sectionsWritten} sections, ${result.nodesExported} nodes)`
@@ -3095,19 +2225,23 @@ program.command("gen-ccs").description("Export knowledge graph as .context/index
3095
2225
  );
3096
2226
  });
3097
2227
  program.command("gen-aider").description("Generate .aider-context.md from knowledge graph").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3098
- const { generateAiderContext } = await import("./aider-context-BC5R2ZTA.js");
3099
- const result = await generateAiderContext(pathResolve(opts.project));
2228
+ const { generateAiderContext } = await import("./aider-context-6IDE3R7U.js");
2229
+ const result = await generateAiderContext(pathResolve2(opts.project));
3100
2230
  console.log(
3101
2231
  chalk2.green(
3102
2232
  `\u2705 Generated ${result.filePath} (${result.sections} sections, ${result.nodes} nodes)`
3103
2233
  )
3104
2234
  );
3105
2235
  if (opts.watch) {
3106
- watchProject(pathResolve(opts.project), {
2236
+ watchProject(pathResolve2(opts.project), {
3107
2237
  onReindex: async () => {
3108
2238
  const r = await generateAiderContext(opts.project);
3109
2239
  console.log(chalk2.dim(` \u21BB Regenerated .aider-context.md (${r.nodes} nodes)`));
3110
2240
  },
2241
+ onDelete: async () => {
2242
+ const r = await generateAiderContext(opts.project);
2243
+ console.log(chalk2.dim(` \xD7 Regenerated .aider-context.md (${r.nodes} nodes)`));
2244
+ },
3111
2245
  onError: (err) => console.error(chalk2.red(err.message)),
3112
2246
  onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3113
2247
  });
@@ -3116,19 +2250,23 @@ program.command("gen-aider").description("Generate .aider-context.md from knowle
3116
2250
  }
3117
2251
  });
3118
2252
  program.command("gen-windsurfrules").description("Generate .windsurfrules from knowledge graph (Windsurf IDE)").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3119
- const { generateWindsurfRules } = await import("./windsurf-rules-C7SVDHBL.js");
3120
- const result = await generateWindsurfRules(pathResolve(opts.project));
2253
+ const { generateWindsurfRules } = await import("./windsurf-rules-XF7MYF6J.js");
2254
+ const result = await generateWindsurfRules(pathResolve2(opts.project));
3121
2255
  console.log(
3122
2256
  chalk2.green(
3123
2257
  `\u2705 Generated ${result.filePath} (${result.sections} sections, ${result.nodes} nodes)`
3124
2258
  )
3125
2259
  );
3126
2260
  if (opts.watch) {
3127
- watchProject(pathResolve(opts.project), {
2261
+ watchProject(pathResolve2(opts.project), {
3128
2262
  onReindex: async () => {
3129
2263
  const r = await generateWindsurfRules(opts.project);
3130
2264
  console.log(chalk2.dim(` \u21BB Regenerated .windsurfrules (${r.nodes} nodes)`));
3131
2265
  },
2266
+ onDelete: async () => {
2267
+ const r = await generateWindsurfRules(opts.project);
2268
+ console.log(chalk2.dim(` \xD7 Regenerated .windsurfrules (${r.nodes} nodes)`));
2269
+ },
3132
2270
  onError: (err) => console.error(chalk2.red(err.message)),
3133
2271
  onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3134
2272
  });
@@ -3137,14 +2275,14 @@ program.command("gen-windsurfrules").description("Generate .windsurfrules from k
3137
2275
  }
3138
2276
  });
3139
2277
  function resolveSettingsPath(scope, projectPath) {
3140
- const absProject = pathResolve(projectPath);
2278
+ const absProject = pathResolve2(projectPath);
3141
2279
  switch (scope) {
3142
2280
  case "local":
3143
- return join9(absProject, ".claude", "settings.local.json");
2281
+ return join7(absProject, ".claude", "settings.local.json");
3144
2282
  case "project":
3145
- return join9(absProject, ".claude", "settings.json");
2283
+ return join7(absProject, ".claude", "settings.json");
3146
2284
  case "user":
3147
- return join9(homedir(), ".claude", "settings.json");
2285
+ return join7(homedir(), ".claude", "settings.json");
3148
2286
  default:
3149
2287
  return null;
3150
2288
  }
@@ -3226,7 +2364,11 @@ program.command("cursor-intercept").description(
3226
2364
  }
3227
2365
  process.exit(0);
3228
2366
  });
3229
- program.command("install-hook").description("Install engram hook entries into Claude Code settings").option("--scope <scope>", "local | project | user", "local").option("--dry-run", "Show diff without writing", false).option("-p, --project <path>", "Project directory", ".").action(
2367
+ program.command("install-hook").description("Install engram hook entries into Claude Code settings").option("--scope <scope>", "local | project | user", "local").option("--dry-run", "Show diff without writing", false).option("-p, --project <path>", "Project directory", ".").option(
2368
+ "--auto-reindex",
2369
+ "Also register a PostToolUse Edit|Write|MultiEdit entry calling 'engram reindex-hook' (keeps graph fresh after every edit, #8)",
2370
+ false
2371
+ ).action(
3230
2372
  async (opts) => {
3231
2373
  const settingsPath = resolveSettingsPath(opts.scope, opts.project);
3232
2374
  if (!settingsPath) {
@@ -3238,9 +2380,9 @@ program.command("install-hook").description("Install engram hook entries into Cl
3238
2380
  process.exit(1);
3239
2381
  }
3240
2382
  let existing = {};
3241
- if (existsSync9(settingsPath)) {
2383
+ if (existsSync7(settingsPath)) {
3242
2384
  try {
3243
- const raw = readFileSync5(settingsPath, "utf-8");
2385
+ const raw = readFileSync4(settingsPath, "utf-8");
3244
2386
  existing = raw.trim() ? JSON.parse(raw) : {};
3245
2387
  } catch (err) {
3246
2388
  console.error(
@@ -3256,13 +2398,20 @@ program.command("install-hook").description("Install engram hook entries into Cl
3256
2398
  process.exit(1);
3257
2399
  }
3258
2400
  }
3259
- const result = installEngramHooks(existing);
2401
+ const result = installEngramHooks(existing, void 0, {
2402
+ autoReindex: opts.autoReindex
2403
+ });
3260
2404
  console.log(
3261
2405
  chalk2.bold(`
3262
2406
  \u{1F4CC} engram install-hook (scope: ${opts.scope})`)
3263
2407
  );
3264
2408
  console.log(chalk2.dim(` Target: ${settingsPath}`));
3265
- if (result.added.length === 0 && !result.statusLineAdded) {
2409
+ if (opts.autoReindex) {
2410
+ console.log(
2411
+ chalk2.dim(" Auto-reindex: enabled (engram reindex-hook)")
2412
+ );
2413
+ }
2414
+ if (result.added.length === 0 && !result.statusLineAdded && !result.autoReindexAdded) {
3266
2415
  console.log(
3267
2416
  chalk2.yellow(
3268
2417
  `
@@ -3285,8 +2434,8 @@ program.command("install-hook").description("Install engram hook entries into Cl
3285
2434
  return;
3286
2435
  }
3287
2436
  try {
3288
- mkdirSync(dirname4(settingsPath), { recursive: true });
3289
- if (existsSync9(settingsPath)) {
2437
+ mkdirSync(dirname3(settingsPath), { recursive: true });
2438
+ if (existsSync7(settingsPath)) {
3290
2439
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
3291
2440
  copyFileSync(settingsPath, backupPath);
3292
2441
  console.log(chalk2.dim(` Backup: ${backupPath}`));
@@ -3317,6 +2466,13 @@ program.command("install-hook").description("Install engram hook entries into Cl
3317
2466
  chalk2.green(" \u2705 StatusLine: engram hud-label (HUD visible in Claude Code)")
3318
2467
  );
3319
2468
  }
2469
+ if (result.autoReindexAdded) {
2470
+ console.log(
2471
+ chalk2.green(
2472
+ " \u2705 PostToolUse: engram reindex-hook (matcher: Edit|Write|MultiEdit)"
2473
+ )
2474
+ );
2475
+ }
3320
2476
  if (result.alreadyPresent.length > 0) {
3321
2477
  console.log(
3322
2478
  chalk2.dim(
@@ -3337,7 +2493,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3337
2493
  console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
3338
2494
  process.exit(1);
3339
2495
  }
3340
- if (!existsSync9(settingsPath)) {
2496
+ if (!existsSync7(settingsPath)) {
3341
2497
  console.log(
3342
2498
  chalk2.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
3343
2499
  );
@@ -3345,7 +2501,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3345
2501
  }
3346
2502
  let existing;
3347
2503
  try {
3348
- const raw = readFileSync5(settingsPath, "utf-8");
2504
+ const raw = readFileSync4(settingsPath, "utf-8");
3349
2505
  existing = raw.trim() ? JSON.parse(raw) : {};
3350
2506
  } catch (err) {
3351
2507
  console.error(
@@ -3390,7 +2546,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3390
2546
  }
3391
2547
  });
3392
2548
  program.command("hook-stats").description("Summarize hook-log.jsonl for a project").option("-p, --project <path>", "Project directory", ".").option("--json", "Output as JSON", false).action(async (opts) => {
3393
- const absProject = pathResolve(opts.project);
2549
+ const absProject = pathResolve2(opts.project);
3394
2550
  const projectRoot = findProjectRoot(absProject) ?? absProject;
3395
2551
  const entries = readHookLog(projectRoot);
3396
2552
  const summary = summarizeHookLog(entries);
@@ -3401,8 +2557,8 @@ program.command("hook-stats").description("Summarize hook-log.jsonl for a projec
3401
2557
  console.log(formatStatsSummary(summary));
3402
2558
  });
3403
2559
  program.command("hook-preview").description("Show what the Read handler would do for a file (dry-run)").argument("<file>", "Target file path").option("-p, --project <path>", "Project directory", ".").action(async (file, opts) => {
3404
- const absProject = pathResolve(opts.project);
3405
- const absFile = pathResolve(absProject, file);
2560
+ const absProject = pathResolve2(opts.project);
2561
+ const absFile = pathResolve2(absProject, file);
3406
2562
  const payload = {
3407
2563
  hook_event_name: "PreToolUse",
3408
2564
  tool_name: "Read",
@@ -3451,7 +2607,7 @@ program.command("hook-preview").description("Show what the Read handler would do
3451
2607
  console.log(chalk2.yellow(` Decision: ${decision ?? "unknown"}`));
3452
2608
  });
3453
2609
  program.command("hook-disable").description("Disable engram hooks via kill switch (does not uninstall)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3454
- const absProject = pathResolve(opts.project);
2610
+ const absProject = pathResolve2(opts.project);
3455
2611
  const projectRoot = findProjectRoot(absProject);
3456
2612
  if (!projectRoot) {
3457
2613
  console.error(
@@ -3460,7 +2616,7 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
3460
2616
  console.error(chalk2.dim("Run 'engram init' first."));
3461
2617
  process.exit(1);
3462
2618
  }
3463
- const flagPath = join9(projectRoot, ".engram", "hook-disabled");
2619
+ const flagPath = join7(projectRoot, ".engram", "hook-disabled");
3464
2620
  try {
3465
2621
  writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
3466
2622
  console.log(
@@ -3478,14 +2634,14 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
3478
2634
  }
3479
2635
  });
3480
2636
  program.command("hook-enable").description("Re-enable engram hooks (remove kill switch flag)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3481
- const absProject = pathResolve(opts.project);
2637
+ const absProject = pathResolve2(opts.project);
3482
2638
  const projectRoot = findProjectRoot(absProject);
3483
2639
  if (!projectRoot) {
3484
2640
  console.error(chalk2.red(`Not an engram project: ${absProject}`));
3485
2641
  process.exit(1);
3486
2642
  }
3487
- const flagPath = join9(projectRoot, ".engram", "hook-disabled");
3488
- if (!existsSync9(flagPath)) {
2643
+ const flagPath = join7(projectRoot, ".engram", "hook-disabled");
2644
+ if (!existsSync7(flagPath)) {
3489
2645
  console.log(
3490
2646
  chalk2.yellow(`engram hooks already enabled for ${projectRoot}`)
3491
2647
  );
@@ -3507,7 +2663,7 @@ program.command("memory-sync").description(
3507
2663
  "Write engram's structural facts into MEMORY.md (complementary to Anthropic Auto-Dream)"
3508
2664
  ).option("-p, --project <path>", "Project directory", ".").option("--dry-run", "Print what would be written without writing", false).action(
3509
2665
  async (opts) => {
3510
- const absProject = pathResolve(opts.project);
2666
+ const absProject = pathResolve2(opts.project);
3511
2667
  const projectRoot = findProjectRoot(absProject);
3512
2668
  if (!projectRoot) {
3513
2669
  console.error(
@@ -3527,9 +2683,9 @@ program.command("memory-sync").description(
3527
2683
  }
3528
2684
  let branch = null;
3529
2685
  try {
3530
- const headPath = join9(projectRoot, ".git", "HEAD");
3531
- if (existsSync9(headPath)) {
3532
- const content = readFileSync5(headPath, "utf-8").trim();
2686
+ const headPath = join7(projectRoot, ".git", "HEAD");
2687
+ if (existsSync7(headPath)) {
2688
+ const content = readFileSync4(headPath, "utf-8").trim();
3533
2689
  const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
3534
2690
  if (m) branch = m[1];
3535
2691
  }
@@ -3555,7 +2711,7 @@ program.command("memory-sync").description(
3555
2711
  \u{1F4DD} engram memory-sync`)
3556
2712
  );
3557
2713
  console.log(
3558
- chalk2.dim(` Target: ${join9(projectRoot, "MEMORY.md")}`)
2714
+ chalk2.dim(` Target: ${join7(projectRoot, "MEMORY.md")}`)
3559
2715
  );
3560
2716
  if (opts.dryRun) {
3561
2717
  console.log(chalk2.cyan("\n Section to write (dry-run):\n"));
@@ -3590,7 +2746,7 @@ program.command("memory-sync").description(
3590
2746
  }
3591
2747
  );
3592
2748
  program.command("stress-test").description("Run stress tests: memory, concurrency, large-graph, hook-log replay").option("--reads <n>", "Rapid-reads test: call resolveRichPacket N times", parseInt).option("--providers", "Concurrency test: 50 parallel resolveRichPacket calls").option("--large-graph", "Large-graph test: insert N synthetic nodes and query").option("--nodes <n>", "Node count for --large-graph (default 1000)", parseInt).option("--replay <path>", "Hook-log replay: path to hook-log.jsonl").option("--limit <n>", "Entry limit for --replay (default 500)", parseInt).action(async (opts) => {
3593
- const { execFileSync: execFileSync2 } = await import("child_process");
2749
+ const { execFileSync } = await import("child_process");
3594
2750
  const args = ["bench/stress-test.ts"];
3595
2751
  if (opts.reads) args.push("--reads", String(opts.reads));
3596
2752
  if (opts.providers) args.push("--providers");
@@ -3599,25 +2755,25 @@ program.command("stress-test").description("Run stress tests: memory, concurrenc
3599
2755
  if (opts.replay) args.push("--replay", opts.replay);
3600
2756
  if (opts.limit) args.push("--limit", String(opts.limit));
3601
2757
  try {
3602
- execFileSync2("npx", ["tsx", ...args], { stdio: "inherit", shell: true, cwd: join9(dirname4(fileURLToPath2(import.meta.url)), "..") });
2758
+ execFileSync("npx", ["tsx", ...args], { stdio: "inherit", shell: true, cwd: join7(dirname3(fileURLToPath(import.meta.url)), "..") });
3603
2759
  } catch {
3604
2760
  process.exit(1);
3605
2761
  }
3606
2762
  });
3607
2763
  program.command("server").description("Start engram HTTP REST server (binds to 127.0.0.1 only)").option("--http", "Enable HTTP server (default)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3608
- const { startHttpServer } = await import("./server-KUG7U6SG.js");
3609
- await startHttpServer(pathResolve(opts.project), parseInt(opts.port, 10));
2764
+ const { startHttpServer } = await import("./server-2ZQKXJ5M.js");
2765
+ await startHttpServer(pathResolve2(opts.project), parseInt(opts.port, 10));
3610
2766
  });
3611
2767
  program.command("ui").description("Open the web dashboard (auto-starts HTTP server if needed)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").option("--no-open", "Don't launch browser, just print the URL").action(async (opts) => {
3612
2768
  const port = parseInt(opts.port, 10);
3613
2769
  const publicUrl = `http://127.0.0.1:${port}/ui`;
3614
- const projectRoot = pathResolve(opts.project);
3615
- const { existsSync: existsSync10, readFileSync: readFileSync6 } = await import("fs");
3616
- const pidPath = join9(projectRoot, ".engram", "http-server.pid");
2770
+ const projectRoot = pathResolve2(opts.project);
2771
+ const { existsSync: existsSync8, readFileSync: readFileSync5 } = await import("fs");
2772
+ const pidPath = join7(projectRoot, ".engram", "http-server.pid");
3617
2773
  let alreadyRunning = false;
3618
- if (existsSync10(pidPath)) {
2774
+ if (existsSync8(pidPath)) {
3619
2775
  try {
3620
- const pid = parseInt(readFileSync6(pidPath, "utf-8"), 10);
2776
+ const pid = parseInt(readFileSync5(pidPath, "utf-8"), 10);
3621
2777
  process.kill(pid, 0);
3622
2778
  alreadyRunning = true;
3623
2779
  } catch {
@@ -3644,8 +2800,8 @@ program.command("ui").description("Open the web dashboard (auto-starts HTTP serv
3644
2800
  const { platform } = process;
3645
2801
  const opener = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
3646
2802
  try {
3647
- const { execFile: execFile4 } = await import("child_process");
3648
- execFile4(opener, [bootUrl], { shell: platform === "win32" }, () => {
2803
+ const { execFile: execFile2 } = await import("child_process");
2804
+ execFile2(opener, [bootUrl], { shell: platform === "win32" }, () => {
3649
2805
  });
3650
2806
  } catch {
3651
2807
  console.log(chalk2.dim(` Open manually: ${bootUrl}`));
@@ -3653,20 +2809,20 @@ program.command("ui").description("Open the web dashboard (auto-starts HTTP serv
3653
2809
  }
3654
2810
  });
3655
2811
  program.command("context-server").description("Start Zed-compatible context server (JSON-RPC over stdio)").action(async () => {
3656
- const { execFileSync: execFileSync2 } = await import("child_process");
2812
+ const { execFileSync } = await import("child_process");
3657
2813
  try {
3658
- execFileSync2("npx", ["tsx", "adapters/zed/index.ts"], {
2814
+ execFileSync("npx", ["tsx", "adapters/zed/index.ts"], {
3659
2815
  stdio: "inherit",
3660
2816
  shell: true,
3661
- cwd: join9(dirname4(fileURLToPath2(import.meta.url)), "..")
2817
+ cwd: join7(dirname3(fileURLToPath(import.meta.url)), "..")
3662
2818
  });
3663
2819
  } catch {
3664
2820
  process.exit(1);
3665
2821
  }
3666
2822
  });
3667
2823
  program.command("tune").description("Analyze hook-log and propose provider config changes").option("-p, --project <path>", "Project directory", ".").option("--dry-run", "Show proposed changes without applying (default)").option("--apply", "Apply proposed changes to .engram/config.json").action(async (opts) => {
3668
- const { analyzeTuning, applyTuning } = await import("./tuner-KFNNGKG3.js");
3669
- const proposal = analyzeTuning(pathResolve(opts.project));
2824
+ const { analyzeTuning, applyTuning } = await import("./tuner-Y2YENAZC.js");
2825
+ const proposal = analyzeTuning(pathResolve2(opts.project));
3670
2826
  if (proposal.changes.length === 0) {
3671
2827
  console.log(
3672
2828
  chalk2.dim(
@@ -3688,7 +2844,7 @@ program.command("tune").description("Analyze hook-log and propose provider confi
3688
2844
  );
3689
2845
  }
3690
2846
  if (opts.apply) {
3691
- applyTuning(pathResolve(opts.project), proposal);
2847
+ applyTuning(pathResolve2(opts.project), proposal);
3692
2848
  console.log(chalk2.green("\n\u2705 Changes applied to .engram/config.json"));
3693
2849
  } else {
3694
2850
  console.log(chalk2.dim("\nRun with --apply to write these changes."));
@@ -3696,9 +2852,9 @@ program.command("tune").description("Analyze hook-log and propose provider confi
3696
2852
  });
3697
2853
  var dbCmd = program.command("db").description("Database management");
3698
2854
  dbCmd.command("status").description("Show schema version and migration status").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3699
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3700
- const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-UKCO6BUU.js");
3701
- const store = await getStore2(pathResolve(opts.project));
2855
+ const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
2856
+ const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-KJ5K5NWO.js");
2857
+ const store = await getStore2(pathResolve2(opts.project));
3702
2858
  try {
3703
2859
  const version = getSchemaVersion(store.db);
3704
2860
  const pending = CURRENT_SCHEMA_VERSION - version;
@@ -3713,11 +2869,11 @@ dbCmd.command("status").description("Show schema version and migration status").
3713
2869
  }
3714
2870
  });
3715
2871
  dbCmd.command("migrate").description("Run pending schema migrations").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3716
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3717
- const { runMigrations } = await import("./migrate-UKCO6BUU.js");
3718
- const store = await getStore2(pathResolve(opts.project));
2872
+ const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
2873
+ const { runMigrations } = await import("./migrate-KJ5K5NWO.js");
2874
+ const store = await getStore2(pathResolve2(opts.project));
3719
2875
  try {
3720
- const dbPath = join9(pathResolve(opts.project), ".engram", "graph.db");
2876
+ const dbPath = join7(pathResolve2(opts.project), ".engram", "graph.db");
3721
2877
  const result = runMigrations(
3722
2878
  store.db,
3723
2879
  dbPath
@@ -3748,11 +2904,11 @@ dbCmd.command("rollback").description("Roll back to an earlier schema version (D
3748
2904
  console.error(chalk2.red(`Invalid version: ${opts.to}`));
3749
2905
  process.exit(1);
3750
2906
  }
3751
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3752
- const { rollback, getSchemaVersion } = await import("./migrate-UKCO6BUU.js");
3753
- const store = await getStore2(pathResolve(opts.project));
2907
+ const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
2908
+ const { rollback, getSchemaVersion } = await import("./migrate-KJ5K5NWO.js");
2909
+ const store = await getStore2(pathResolve2(opts.project));
3754
2910
  try {
3755
- const dbPath = join9(pathResolve(opts.project), ".engram", "graph.db");
2911
+ const dbPath = join7(pathResolve2(opts.project), ".engram", "graph.db");
3756
2912
  const current = getSchemaVersion(
3757
2913
  store.db
3758
2914
  );
@@ -3802,7 +2958,7 @@ dbCmd.command("rollback").description("Roll back to an earlier schema version (D
3802
2958
  });
3803
2959
  var pluginCmd = program.command("plugin").description("Manage context provider plugins");
3804
2960
  pluginCmd.command("list").description("List installed provider plugins").action(async () => {
3805
- const { loadPlugins, getPluginsDir, ensurePluginsDir } = await import("./plugin-loader-STTGYIL5.js");
2961
+ const { loadPlugins, getPluginsDir, ensurePluginsDir } = await import("./plugin-loader-SQQB6V74.js");
3806
2962
  const dir = getPluginsDir();
3807
2963
  ensurePluginsDir(dir);
3808
2964
  const { loaded, failed } = await loadPlugins(dir);
@@ -3835,10 +2991,10 @@ pluginCmd.command("list").description("List installed provider plugins").action(
3835
2991
  pluginCmd.command("install").description("Install a plugin by copying its .mjs file into ~/.engram/plugins/").argument("<file>", "Path to plugin .mjs file").action(async (file) => {
3836
2992
  const { copyFileSync: copyFileSync2, statSync: statSync5 } = await import("fs");
3837
2993
  const { basename: basename6 } = await import("path");
3838
- const { getPluginsDir, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-STTGYIL5.js");
2994
+ const { getPluginsDir, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-SQQB6V74.js");
3839
2995
  const { pathToFileURL } = await import("url");
3840
- const absPath = pathResolve(file);
3841
- if (!existsSync9(absPath)) {
2996
+ const absPath = pathResolve2(file);
2997
+ if (!existsSync7(absPath)) {
3842
2998
  console.error(chalk2.red(`File not found: ${absPath}`));
3843
2999
  process.exit(1);
3844
3000
  }
@@ -3867,15 +3023,15 @@ pluginCmd.command("install").description("Install a plugin by copying its .mjs f
3867
3023
  const pluginsDir = getPluginsDir();
3868
3024
  ensurePluginsDir(pluginsDir);
3869
3025
  const destName = basename6(absPath);
3870
- const destPath = join9(pluginsDir, destName);
3026
+ const destPath = join7(pluginsDir, destName);
3871
3027
  copyFileSync2(absPath, destPath);
3872
3028
  console.log(chalk2.green(`\u2713 Installed: ${destPath}`));
3873
3029
  });
3874
3030
  pluginCmd.command("remove").description("Remove an installed plugin by filename").argument("<filename>", "Plugin filename (e.g., my-provider.mjs)").action(async (filename) => {
3875
- const { getPluginsDir } = await import("./plugin-loader-STTGYIL5.js");
3031
+ const { getPluginsDir } = await import("./plugin-loader-SQQB6V74.js");
3876
3032
  const pluginsDir = getPluginsDir();
3877
- const target = join9(pluginsDir, filename);
3878
- if (!existsSync9(target)) {
3033
+ const target = join7(pluginsDir, filename);
3034
+ if (!existsSync7(target)) {
3879
3035
  console.error(chalk2.red(`No such plugin: ${filename}`));
3880
3036
  console.log(chalk2.dim(`Plugins directory: ${pluginsDir}`));
3881
3037
  process.exit(1);
@@ -3885,9 +3041,9 @@ pluginCmd.command("remove").description("Remove an installed plugin by filename"
3885
3041
  });
3886
3042
  var cacheCmd = program.command("cache").description("Inspect and manage the context cache");
3887
3043
  cacheCmd.command("stats").description("Show cache hit rate, entries, and LRU sizes").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3888
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3044
+ const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
3889
3045
  const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3890
- const store = await getStore2(pathResolve(opts.project));
3046
+ const store = await getStore2(pathResolve2(opts.project));
3891
3047
  try {
3892
3048
  ContextCache.ensureTables(store);
3893
3049
  const cache = getContextCache();
@@ -3919,9 +3075,9 @@ cacheCmd.command("stats").description("Show cache hit rate, entries, and LRU siz
3919
3075
  }
3920
3076
  });
3921
3077
  cacheCmd.command("clear").description("Flush all cache layers (query, pattern, hot files)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3922
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3078
+ const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
3923
3079
  const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3924
- const store = await getStore2(pathResolve(opts.project));
3080
+ const store = await getStore2(pathResolve2(opts.project));
3925
3081
  try {
3926
3082
  ContextCache.ensureTables(store);
3927
3083
  const cache = getContextCache();
@@ -3938,14 +3094,14 @@ cacheCmd.command("clear").description("Flush all cache layers (query, pattern, h
3938
3094
  }
3939
3095
  });
3940
3096
  cacheCmd.command("warm").description("Pre-warm hot file cache from access frequency (top-N)").option("-p, --project <path>", "Project directory", ".").option("-n, --limit <n>", "Number of files to warm", "20").action(async (opts) => {
3941
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3097
+ const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
3942
3098
  const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3943
- const store = await getStore2(pathResolve(opts.project));
3099
+ const store = await getStore2(pathResolve2(opts.project));
3944
3100
  try {
3945
3101
  ContextCache.ensureTables(store);
3946
3102
  const cache = getContextCache();
3947
3103
  const topN = parseInt(opts.limit, 10) || 20;
3948
- const count = cache.warmHotFiles(store, pathResolve(opts.project), topN);
3104
+ const count = cache.warmHotFiles(store, pathResolve2(opts.project), topN);
3949
3105
  if (count === 0) {
3950
3106
  console.log(
3951
3107
  chalk2.dim(
@@ -3959,4 +3115,133 @@ cacheCmd.command("warm").description("Pre-warm hot file cache from access freque
3959
3115
  store.close();
3960
3116
  }
3961
3117
  });
3118
+ program.command("update").description("Check for and install the latest engram release").option("--check", "Check only \u2014 do not install", false).option("--force", "Bypass 7-day throttle cache on registry check", false).option(
3119
+ "--manager <mgr>",
3120
+ "Override package manager detection (npm | pnpm | yarn | bun)"
3121
+ ).option("--dry-run", "Print the upgrade command without executing", false).action(
3122
+ async (opts) => {
3123
+ const { checkForUpdate } = await import("./check-2Z3MPZEJ.js");
3124
+ const result = await checkForUpdate(PKG_VERSION, { force: opts.force });
3125
+ if (result.skipped) {
3126
+ if (result.fromCache === false) {
3127
+ console.log(
3128
+ chalk2.dim("Skipped (opt-out via ENGRAM_NO_UPDATE_CHECK or $CI).")
3129
+ );
3130
+ } else {
3131
+ console.log(chalk2.dim("Skipped (registry unreachable)."));
3132
+ }
3133
+ return;
3134
+ }
3135
+ const ageMin = result.checkedAt ? Math.round((Date.now() - result.checkedAt) / 6e4) : 0;
3136
+ const freshness = result.fromCache ? chalk2.dim(` (cached ${ageMin}m ago)`) : chalk2.dim(" (live)");
3137
+ console.log(
3138
+ `${chalk2.bold("engram")} ${chalk2.dim("installed:")} v${result.current} ${chalk2.dim("latest:")} ${result.latest ?? chalk2.yellow("unknown")}${freshness}`
3139
+ );
3140
+ if (!result.updateAvailable) {
3141
+ console.log(chalk2.green("\u2713 You are on the latest release."));
3142
+ return;
3143
+ }
3144
+ console.log(
3145
+ chalk2.yellow(
3146
+ `\u2B06 v${result.latest} is available \u2014 you're on v${result.current}.`
3147
+ )
3148
+ );
3149
+ if (opts.check) {
3150
+ console.log(chalk2.dim("Run `engram update` to install it."));
3151
+ return;
3152
+ }
3153
+ const { runUpgrade, manualCommand } = await import("./install-YVMVCFQW.js");
3154
+ const outcome = runUpgrade({
3155
+ dryRun: opts.dryRun,
3156
+ manager: opts.manager === "npm" || opts.manager === "pnpm" || opts.manager === "yarn" || opts.manager === "bun" ? opts.manager : void 0
3157
+ });
3158
+ if (outcome.ok) {
3159
+ console.log(chalk2.green(`\u2713 ${outcome.message}`));
3160
+ if (!opts.dryRun) {
3161
+ console.log(chalk2.dim(" Run `engram --version` to verify."));
3162
+ }
3163
+ } else {
3164
+ console.error(chalk2.red(`\u2717 ${outcome.message}`));
3165
+ if (outcome.stderrTail) {
3166
+ console.error(chalk2.dim(outcome.stderrTail));
3167
+ }
3168
+ console.error(chalk2.dim(` Manual: ${manualCommand()}`));
3169
+ process.exitCode = 1;
3170
+ }
3171
+ }
3172
+ );
3173
+ program.command("doctor").description("Component health report with remediation hints").option("-p, --project <path>", "Project directory", ".").option("-v, --verbose", "Show remediation hints for warn/fail checks", false).option("--json", "Output JSON", false).option(
3174
+ "--export",
3175
+ "Redacted JSON for bug reports (same as --json with --verbose)",
3176
+ false
3177
+ ).action(
3178
+ async (opts) => {
3179
+ const { buildReport, formatReport, exportReport } = await import("./report-C3GTM3HY.js");
3180
+ const root = pathResolve2(opts.project);
3181
+ const report = buildReport(root, PKG_VERSION);
3182
+ if (opts.json || opts.export) {
3183
+ console.log(exportReport(report));
3184
+ } else {
3185
+ console.log(formatReport(report, opts.verbose));
3186
+ }
3187
+ process.exitCode = report.overallSeverity === "ok" ? 0 : report.overallSeverity === "warn" ? 1 : 2;
3188
+ }
3189
+ );
3190
+ program.command("setup").description("Zero-friction first-run wizard (init + install-hook + doctor)").option("-p, --project <path>", "Project directory", ".").option("-y, --yes", "Accept all defaults (non-interactive)", false).option("--dry-run", "Print what would happen without touching anything", false).option(
3191
+ "--scope <scope>",
3192
+ "Hook scope for install-hook step (local | project | user)",
3193
+ "local"
3194
+ ).action(
3195
+ async (opts) => {
3196
+ const { runSetup } = await import("./wizard-UH27IO4I.js");
3197
+ const scope = opts.scope === "local" || opts.scope === "project" || opts.scope === "user" ? opts.scope : "local";
3198
+ const result = await runSetup({
3199
+ projectPath: opts.project,
3200
+ yes: opts.yes,
3201
+ dryRun: opts.dryRun,
3202
+ engramVersion: PKG_VERSION,
3203
+ settingsScope: scope
3204
+ });
3205
+ process.exitCode = result.exitCode;
3206
+ }
3207
+ );
3208
+ var FIRST_RUN_SILENT_CMDS = /* @__PURE__ */ new Set([
3209
+ "intercept",
3210
+ "cursor-intercept",
3211
+ "hud-label",
3212
+ "setup",
3213
+ "init",
3214
+ "update",
3215
+ "doctor"
3216
+ ]);
3217
+ function maybePrintFirstRunHint() {
3218
+ if (process.env.CI) return;
3219
+ if (process.env.ENGRAM_NO_UPDATE_CHECK === "1") return;
3220
+ const subcommand = process.argv[2];
3221
+ if (!subcommand) return;
3222
+ if (FIRST_RUN_SILENT_CMDS.has(subcommand)) return;
3223
+ try {
3224
+ const cwd = process.cwd();
3225
+ if (existsSync7(join7(cwd, ".engram", "graph.db"))) return;
3226
+ const sentinel = join7(homedir(), ".engram", "first-run-shown");
3227
+ if (existsSync7(sentinel)) return;
3228
+ mkdirSync(dirname3(sentinel), { recursive: true });
3229
+ writeFileSync2(sentinel, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
3230
+ process.stderr.write(
3231
+ chalk2.dim("\u{1F4A1} ") + chalk2.yellow("First time in this repo?") + chalk2.dim(" Run ") + chalk2.white("engram setup") + chalk2.dim(" for a zero-friction install.\n")
3232
+ );
3233
+ } catch {
3234
+ }
3235
+ }
3236
+ function maybePrintUpdateHintSafe() {
3237
+ const subcommand = process.argv[2];
3238
+ if (!subcommand || FIRST_RUN_SILENT_CMDS.has(subcommand)) return;
3239
+ try {
3240
+ import("./notify-5POGKMRX.js").then((m) => m.maybePrintUpdateHint(PKG_VERSION)).catch(() => {
3241
+ });
3242
+ } catch {
3243
+ }
3244
+ }
3245
+ maybePrintFirstRunHint();
3246
+ maybePrintUpdateHintSafe();
3962
3247
  program.parse();