engramx 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  install,
5
5
  status,
6
6
  uninstall
7
- } from "./chunk-ESPAWLH6.js";
7
+ } from "./chunk-LH2ZID5Z.js";
8
8
  import {
9
9
  benchmark,
10
10
  computeKeywordIDF,
@@ -18,9 +18,10 @@ import {
18
18
  mistakes,
19
19
  path,
20
20
  query,
21
+ renderFileStructure,
21
22
  stats,
22
23
  toPosixPath
23
- } from "./chunk-R46DNLNR.js";
24
+ } from "./chunk-V5VQQ3SF.js";
24
25
 
25
26
  // src/cli.ts
26
27
  import { Command } from "commander";
@@ -79,6 +80,9 @@ function isHookDisabled(projectRoot) {
79
80
  }
80
81
  }
81
82
 
83
+ // src/intercept/handlers/read.ts
84
+ import { relative } from "path";
85
+
82
86
  // src/intercept/context.ts
83
87
  import { existsSync as existsSync2, realpathSync, statSync } from "fs";
84
88
  import { dirname, isAbsolute, join as join2, resolve, sep } from "path";
@@ -336,6 +340,739 @@ function buildSessionContextResponse(eventName, additionalContext) {
336
340
  };
337
341
  }
338
342
 
343
+ // src/providers/types.ts
344
+ var PROVIDER_PRIORITY = [
345
+ "engram:structure",
346
+ "engram:mistakes",
347
+ "mempalace",
348
+ "context7",
349
+ "engram:git",
350
+ "obsidian"
351
+ ];
352
+ var DEFAULT_CACHE_TTL_SEC = 3600;
353
+
354
+ // src/providers/engram-structure.ts
355
+ var structureProvider = {
356
+ name: "engram:structure",
357
+ label: "STRUCTURE",
358
+ tier: 1,
359
+ tokenBudget: 250,
360
+ timeoutMs: 500,
361
+ async resolve(filePath, context) {
362
+ try {
363
+ const store = await getStore(context.projectRoot);
364
+ try {
365
+ const result = renderFileStructure(store, filePath);
366
+ if (!result || result.nodeCount === 0) return null;
367
+ return {
368
+ provider: "engram:structure",
369
+ content: result.text,
370
+ confidence: result.avgConfidence,
371
+ cached: false
372
+ };
373
+ } finally {
374
+ store.close();
375
+ }
376
+ } catch {
377
+ return null;
378
+ }
379
+ },
380
+ async isAvailable() {
381
+ return true;
382
+ }
383
+ };
384
+
385
+ // src/providers/engram-mistakes.ts
386
+ var mistakesProvider = {
387
+ name: "engram:mistakes",
388
+ label: "KNOWN ISSUES",
389
+ tier: 1,
390
+ tokenBudget: 50,
391
+ timeoutMs: 200,
392
+ async resolve(filePath, context) {
393
+ try {
394
+ const store = await getStore(context.projectRoot);
395
+ try {
396
+ const allMistakes = store.getNodesByFile(filePath).filter((n) => n.kind === "mistake");
397
+ if (allMistakes.length === 0) return null;
398
+ const lines = allMistakes.slice(0, 5).map((m) => ` ! ${m.label} (flagged ${formatAge(m.lastVerified)})`).join("\n");
399
+ return {
400
+ provider: "engram:mistakes",
401
+ content: lines,
402
+ confidence: 0.95,
403
+ cached: false
404
+ };
405
+ } finally {
406
+ store.close();
407
+ }
408
+ } catch {
409
+ return null;
410
+ }
411
+ },
412
+ async isAvailable() {
413
+ return true;
414
+ }
415
+ };
416
+ function formatAge(timestampMs) {
417
+ if (timestampMs === 0) return "unknown";
418
+ const days = Math.floor((Date.now() - timestampMs) / (1e3 * 60 * 60 * 24));
419
+ if (days === 0) return "today";
420
+ if (days === 1) return "yesterday";
421
+ if (days < 30) return `${days}d ago`;
422
+ return `${Math.floor(days / 30)}mo ago`;
423
+ }
424
+
425
+ // src/providers/engram-git.ts
426
+ import { execFileSync } from "child_process";
427
+ var gitProvider = {
428
+ name: "engram:git",
429
+ label: "CHANGES",
430
+ tier: 1,
431
+ tokenBudget: 50,
432
+ timeoutMs: 200,
433
+ async resolve(filePath, context) {
434
+ try {
435
+ const cwd = context.projectRoot;
436
+ const lastLog = git(
437
+ ["log", "-1", "--format=%ar|%an|%s", "--", filePath],
438
+ cwd
439
+ );
440
+ if (!lastLog) return null;
441
+ const [timeAgo, author, message] = lastLog.split("|", 3);
442
+ const recentCount = git(
443
+ [
444
+ "rev-list",
445
+ "--count",
446
+ "--since=30.days",
447
+ "HEAD",
448
+ "--",
449
+ filePath
450
+ ],
451
+ cwd
452
+ );
453
+ const churnNote = context.churnRate > 0.3 ? "high churn" : context.churnRate > 0.1 ? "moderate" : "stable";
454
+ const parts = [
455
+ ` Last modified: ${timeAgo} by ${author} (${truncate(message, 50)})`,
456
+ ` Churn: ${context.churnRate.toFixed(2)} (${churnNote}) | ${recentCount || "0"} changes in 30d`
457
+ ];
458
+ return {
459
+ provider: "engram:git",
460
+ content: parts.join("\n"),
461
+ confidence: 0.9,
462
+ cached: false
463
+ };
464
+ } catch {
465
+ return null;
466
+ }
467
+ },
468
+ async isAvailable() {
469
+ try {
470
+ execFileSync("git", ["--version"], {
471
+ encoding: "utf-8",
472
+ timeout: 2e3
473
+ });
474
+ return true;
475
+ } catch {
476
+ return false;
477
+ }
478
+ }
479
+ };
480
+ function git(args, cwd) {
481
+ try {
482
+ return execFileSync("git", args, {
483
+ cwd,
484
+ encoding: "utf-8",
485
+ timeout: 3e3,
486
+ maxBuffer: 1024 * 1024
487
+ }).trim();
488
+ } catch {
489
+ return "";
490
+ }
491
+ }
492
+ function truncate(s, max) {
493
+ return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
494
+ }
495
+
496
+ // src/providers/mempalace.ts
497
+ import { execFile } from "child_process";
498
+ var MAX_SEARCH_RESULTS = 3;
499
+ var mempalaceProvider = {
500
+ name: "mempalace",
501
+ label: "DECISIONS",
502
+ tier: 2,
503
+ tokenBudget: 100,
504
+ timeoutMs: 200,
505
+ async resolve(filePath, context) {
506
+ try {
507
+ const store = await getStore(context.projectRoot);
508
+ try {
509
+ const cached = store.getCachedContextForProvider(
510
+ "mempalace",
511
+ filePath
512
+ );
513
+ if (cached) {
514
+ return {
515
+ provider: "mempalace",
516
+ content: cached.content,
517
+ confidence: 0.8,
518
+ cached: true
519
+ };
520
+ }
521
+ } finally {
522
+ store.close();
523
+ }
524
+ const query2 = buildQuery(filePath, context);
525
+ const raw = await searchMempalace(query2);
526
+ if (!raw) return null;
527
+ const content = formatResults(raw);
528
+ if (!content) return null;
529
+ const store2 = await getStore(context.projectRoot);
530
+ try {
531
+ store2.setCachedContext(
532
+ "mempalace",
533
+ filePath,
534
+ content,
535
+ DEFAULT_CACHE_TTL_SEC,
536
+ query2
537
+ );
538
+ store2.save();
539
+ } finally {
540
+ store2.close();
541
+ }
542
+ return {
543
+ provider: "mempalace",
544
+ content,
545
+ confidence: 0.8,
546
+ cached: false
547
+ };
548
+ } catch {
549
+ return null;
550
+ }
551
+ },
552
+ async warmup(projectRoot) {
553
+ const start = Date.now();
554
+ const entries = [];
555
+ try {
556
+ const store = await getStore(projectRoot);
557
+ let projectName;
558
+ try {
559
+ projectName = store.getStat("project_name") ?? projectRoot.split("/").pop() ?? "";
560
+ } finally {
561
+ store.close();
562
+ }
563
+ if (!projectName) {
564
+ return { provider: "mempalace", entries, durationMs: Date.now() - start };
565
+ }
566
+ const raw = await searchMempalace(
567
+ `${projectName} decisions architecture patterns`
568
+ );
569
+ if (!raw) {
570
+ return { provider: "mempalace", entries, durationMs: Date.now() - start };
571
+ }
572
+ const content = formatResults(raw);
573
+ if (content) {
574
+ entries.push({ filePath: "__project__", content });
575
+ }
576
+ } catch {
577
+ }
578
+ return { provider: "mempalace", entries, durationMs: Date.now() - start };
579
+ },
580
+ async isAvailable() {
581
+ try {
582
+ const result = await execFilePromise("mcp-mempalace", [
583
+ "mempalace-status"
584
+ ]);
585
+ return result.includes("palace") || result.includes("drawers");
586
+ } catch {
587
+ return false;
588
+ }
589
+ }
590
+ };
591
+ function buildQuery(filePath, context) {
592
+ const fileName = filePath.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
593
+ const importTerms = context.imports.slice(0, 3).join(" ");
594
+ return `${fileName} ${importTerms}`.trim();
595
+ }
596
+ function searchMempalace(query2) {
597
+ return new Promise((resolve7) => {
598
+ const timeout = setTimeout(() => resolve7(null), 3e3);
599
+ execFile(
600
+ "mcp-mempalace",
601
+ ["mempalace-search", "--query", query2],
602
+ { encoding: "utf-8", timeout: 3e3, maxBuffer: 1024 * 1024 },
603
+ (err, stdout) => {
604
+ clearTimeout(timeout);
605
+ if (err || !stdout.trim()) {
606
+ resolve7(null);
607
+ return;
608
+ }
609
+ resolve7(stdout.trim());
610
+ }
611
+ );
612
+ });
613
+ }
614
+ function formatResults(raw) {
615
+ try {
616
+ const parsed = JSON.parse(raw);
617
+ const results = Array.isArray(parsed) ? parsed : parsed?.results ?? parsed?.drawers ?? [];
618
+ if (results.length === 0) return null;
619
+ const lines = results.slice(0, MAX_SEARCH_RESULTS).map((r) => {
620
+ const content = r.content ?? r.text ?? r.summary ?? "";
621
+ const truncated = content.split(/\s+/).slice(0, 30).join(" ");
622
+ return ` - ${truncated}`;
623
+ }).filter((l) => l.length > 4);
624
+ return lines.length > 0 ? lines.join("\n") : null;
625
+ } catch {
626
+ const lines = raw.split("\n").filter((l) => l.trim()).slice(0, MAX_SEARCH_RESULTS).map((l) => ` - ${l.trim().slice(0, 120)}`);
627
+ return lines.length > 0 ? lines.join("\n") : null;
628
+ }
629
+ }
630
+ function execFilePromise(cmd, args) {
631
+ return new Promise((resolve7, reject) => {
632
+ execFile(
633
+ cmd,
634
+ args,
635
+ { encoding: "utf-8", timeout: 3e3 },
636
+ (err, stdout) => {
637
+ if (err) reject(err);
638
+ else resolve7(stdout.trim());
639
+ }
640
+ );
641
+ });
642
+ }
643
+
644
+ // src/providers/context7.ts
645
+ import { execFile as execFile2 } from "child_process";
646
+ var LIBRARY_CACHE_TTL = 4 * 3600;
647
+ var context7Provider = {
648
+ name: "context7",
649
+ label: "LIBRARY",
650
+ tier: 2,
651
+ tokenBudget: 100,
652
+ timeoutMs: 200,
653
+ async resolve(filePath, context) {
654
+ if (context.imports.length === 0) return null;
655
+ try {
656
+ const store = await getStore(context.projectRoot);
657
+ try {
658
+ const cached = store.getCachedContextForProvider("context7", filePath);
659
+ if (cached) {
660
+ return {
661
+ provider: "context7",
662
+ content: cached.content,
663
+ confidence: 0.85,
664
+ cached: true
665
+ };
666
+ }
667
+ } finally {
668
+ store.close();
669
+ }
670
+ const primaryImport = context.imports[0];
671
+ const docs = await queryContext7(primaryImport);
672
+ if (!docs) return null;
673
+ const content = formatDocs(primaryImport, docs);
674
+ if (!content) return null;
675
+ const store2 = await getStore(context.projectRoot);
676
+ try {
677
+ store2.setCachedContext(
678
+ "context7",
679
+ filePath,
680
+ content,
681
+ LIBRARY_CACHE_TTL,
682
+ primaryImport
683
+ );
684
+ store2.save();
685
+ } finally {
686
+ store2.close();
687
+ }
688
+ return {
689
+ provider: "context7",
690
+ content,
691
+ confidence: 0.85,
692
+ cached: false
693
+ };
694
+ } catch {
695
+ return null;
696
+ }
697
+ },
698
+ async warmup(projectRoot) {
699
+ const start = Date.now();
700
+ const entries = [];
701
+ try {
702
+ const store = await getStore(projectRoot);
703
+ let importEdges;
704
+ try {
705
+ const allEdges = store.getAllEdges();
706
+ importEdges = allEdges.filter((e) => e.relation === "imports").map((e) => ({ source: e.sourceFile, target: e.target }));
707
+ } finally {
708
+ store.close();
709
+ }
710
+ const packages = [
711
+ ...new Set(
712
+ importEdges.map((e) => {
713
+ const parts = e.target.split("::");
714
+ return parts[parts.length - 1];
715
+ }).filter(isExternalPackage)
716
+ )
717
+ ].slice(0, 10);
718
+ for (const pkg of packages) {
719
+ const docs = await queryContext7(pkg);
720
+ if (docs) {
721
+ const content = formatDocs(pkg, docs);
722
+ if (content) {
723
+ const files = importEdges.filter((e) => e.target.includes(pkg)).map((e) => e.source);
724
+ for (const file of [...new Set(files)]) {
725
+ entries.push({ filePath: file, content });
726
+ }
727
+ }
728
+ }
729
+ }
730
+ } catch {
731
+ }
732
+ return { provider: "context7", entries, durationMs: Date.now() - start };
733
+ },
734
+ async isAvailable() {
735
+ try {
736
+ const result = await execFilePromise2("mcp-context7", ["--list"]);
737
+ return result.includes("resolve-library-id");
738
+ } catch {
739
+ return false;
740
+ }
741
+ }
742
+ };
743
+ function isExternalPackage(name) {
744
+ if (!name) return false;
745
+ if (name.startsWith(".") || name.startsWith("/")) return false;
746
+ if ([
747
+ "fs",
748
+ "path",
749
+ "os",
750
+ "url",
751
+ "http",
752
+ "https",
753
+ "crypto",
754
+ "stream",
755
+ "util",
756
+ "events",
757
+ "child_process",
758
+ "node:fs",
759
+ "node:path",
760
+ "node:os",
761
+ "node:url",
762
+ "node:http",
763
+ "node:https",
764
+ "node:crypto",
765
+ "node:stream",
766
+ "node:util",
767
+ "node:events",
768
+ "node:child_process"
769
+ ].includes(name))
770
+ return false;
771
+ return true;
772
+ }
773
+ function queryContext7(packageName) {
774
+ return new Promise((resolve7) => {
775
+ const timeout = setTimeout(() => resolve7(null), 5e3);
776
+ execFile2(
777
+ "mcp-context7",
778
+ [
779
+ "query-docs",
780
+ "--context7CompatibleLibraryID",
781
+ packageName,
782
+ "--topic",
783
+ "API reference quick start"
784
+ ],
785
+ { encoding: "utf-8", timeout: 5e3, maxBuffer: 2 * 1024 * 1024 },
786
+ (err, stdout) => {
787
+ clearTimeout(timeout);
788
+ if (err || !stdout.trim()) {
789
+ resolve7(null);
790
+ return;
791
+ }
792
+ resolve7(stdout.trim());
793
+ }
794
+ );
795
+ });
796
+ }
797
+ function formatDocs(pkg, raw) {
798
+ const truncated = raw.slice(0, 400);
799
+ const lines = truncated.split("\n").filter((l) => l.trim()).slice(0, 5).map((l) => ` ${l.trim()}`);
800
+ if (lines.length === 0) return null;
801
+ return ` ${pkg}:
802
+ ${lines.join("\n")}`;
803
+ }
804
+ function execFilePromise2(cmd, args) {
805
+ return new Promise((resolve7, reject) => {
806
+ execFile2(
807
+ cmd,
808
+ args,
809
+ { encoding: "utf-8", timeout: 3e3 },
810
+ (err, stdout) => {
811
+ if (err) reject(err);
812
+ else resolve7(stdout.trim());
813
+ }
814
+ );
815
+ });
816
+ }
817
+
818
+ // src/providers/obsidian.ts
819
+ var OBSIDIAN_PORT = 27124;
820
+ var OBSIDIAN_BASE = `http://127.0.0.1:${OBSIDIAN_PORT}`;
821
+ var obsidianProvider = {
822
+ name: "obsidian",
823
+ label: "PROJECT NOTES",
824
+ tier: 2,
825
+ tokenBudget: 50,
826
+ timeoutMs: 200,
827
+ async resolve(filePath, context) {
828
+ try {
829
+ const store = await getStore(context.projectRoot);
830
+ try {
831
+ const cached = store.getCachedContextForProvider("obsidian", filePath);
832
+ if (cached) {
833
+ return {
834
+ provider: "obsidian",
835
+ content: cached.content,
836
+ confidence: 0.7,
837
+ cached: true
838
+ };
839
+ }
840
+ } finally {
841
+ store.close();
842
+ }
843
+ const projectName = context.projectRoot.split("/").pop() ?? "";
844
+ const fileName = filePath.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
845
+ const query2 = `${projectName} ${fileName}`;
846
+ const results = await searchObsidian(query2);
847
+ if (!results) return null;
848
+ const content = formatResults2(results);
849
+ if (!content) return null;
850
+ const store2 = await getStore(context.projectRoot);
851
+ try {
852
+ store2.setCachedContext(
853
+ "obsidian",
854
+ filePath,
855
+ content,
856
+ DEFAULT_CACHE_TTL_SEC,
857
+ query2
858
+ );
859
+ store2.save();
860
+ } finally {
861
+ store2.close();
862
+ }
863
+ return {
864
+ provider: "obsidian",
865
+ content,
866
+ confidence: 0.7,
867
+ cached: false
868
+ };
869
+ } catch {
870
+ return null;
871
+ }
872
+ },
873
+ async warmup(projectRoot) {
874
+ const start = Date.now();
875
+ const entries = [];
876
+ try {
877
+ const projectName = projectRoot.split("/").pop() ?? "";
878
+ if (!projectName) {
879
+ return { provider: "obsidian", entries, durationMs: Date.now() - start };
880
+ }
881
+ const results = await searchObsidian(
882
+ `${projectName} architecture design decisions`
883
+ );
884
+ if (results) {
885
+ const content = formatResults2(results);
886
+ if (content) {
887
+ entries.push({ filePath: "__project__", content });
888
+ }
889
+ }
890
+ } catch {
891
+ }
892
+ return { provider: "obsidian", entries, durationMs: Date.now() - start };
893
+ },
894
+ async isAvailable() {
895
+ try {
896
+ const response = await fetchWithTimeout(
897
+ `${OBSIDIAN_BASE}/`,
898
+ 1e3
899
+ );
900
+ return response.ok;
901
+ } catch {
902
+ return false;
903
+ }
904
+ }
905
+ };
906
+ async function searchObsidian(query2) {
907
+ try {
908
+ const response = await fetchWithTimeout(
909
+ `${OBSIDIAN_BASE}/search/simple/?query=${encodeURIComponent(query2)}`,
910
+ 2e3
911
+ );
912
+ if (!response.ok) return null;
913
+ const data = await response.json();
914
+ if (!Array.isArray(data) || data.length === 0) return null;
915
+ return data.slice(0, 3);
916
+ } catch {
917
+ return null;
918
+ }
919
+ }
920
+ function formatResults2(results) {
921
+ if (results.length === 0) return null;
922
+ const lines = results.slice(0, 3).map((r) => {
923
+ const name = r.filename.replace(/\.md$/, "");
924
+ return ` Related: ${name}`;
925
+ });
926
+ return lines.join("\n");
927
+ }
928
+ async function fetchWithTimeout(url, timeoutMs) {
929
+ const controller = new AbortController();
930
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
931
+ try {
932
+ return await fetch(url, { signal: controller.signal });
933
+ } finally {
934
+ clearTimeout(timer);
935
+ }
936
+ }
937
+
938
+ // src/providers/resolver.ts
939
+ var ALL_PROVIDERS = [
940
+ structureProvider,
941
+ mistakesProvider,
942
+ gitProvider,
943
+ mempalaceProvider,
944
+ context7Provider,
945
+ obsidianProvider
946
+ ];
947
+ var TOTAL_TOKEN_BUDGET = 600;
948
+ function estimateTokens(text) {
949
+ return Math.ceil(text.length / 4);
950
+ }
951
+ async function resolveRichPacket(filePath, context, enabledProviders) {
952
+ const start = Date.now();
953
+ const providers = ALL_PROVIDERS.filter((p) => {
954
+ if (enabledProviders && !enabledProviders.includes(p.name)) return false;
955
+ return true;
956
+ });
957
+ const available = await filterAvailable(providers);
958
+ if (available.length === 0) return null;
959
+ const settled = await Promise.allSettled(
960
+ available.map((p) => resolveWithTimeout(p, filePath, context))
961
+ );
962
+ const results = [];
963
+ for (const outcome of settled) {
964
+ if (outcome.status === "fulfilled" && outcome.value) {
965
+ results.push(outcome.value);
966
+ }
967
+ }
968
+ if (results.length === 0) return null;
969
+ const sorted = results.sort((a, b) => {
970
+ const aIdx = PROVIDER_PRIORITY.indexOf(a.provider);
971
+ const bIdx = PROVIDER_PRIORITY.indexOf(b.provider);
972
+ return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
973
+ });
974
+ const sections = [];
975
+ let totalTokens = 0;
976
+ for (const result of sorted) {
977
+ const sectionTokens = estimateTokens(result.content);
978
+ if (totalTokens + sectionTokens > TOTAL_TOKEN_BUDGET) {
979
+ break;
980
+ }
981
+ const provider = ALL_PROVIDERS.find((p) => p.name === result.provider);
982
+ const label = provider?.label ?? result.provider.toUpperCase();
983
+ const cacheTag = result.cached ? ", cached" : "";
984
+ sections.push(`${label} (${result.provider}${cacheTag}):
985
+ ${result.content}`);
986
+ totalTokens += sectionTokens;
987
+ }
988
+ if (sections.length === 0) return null;
989
+ const providerNames = sorted.filter((_, i) => i < sections.length).map((r) => r.provider);
990
+ const isEnrichment = enabledProviders && !enabledProviders.includes("engram:structure");
991
+ const header = isEnrichment ? `[engram] Additional context (${providerNames.length} providers, ~${totalTokens} tokens)` : `[engram] Rich context for ${filePath} (${providerNames.length} providers, ~${totalTokens} tokens)`;
992
+ const text = `${header}
993
+
994
+ ${sections.join("\n\n")}`;
995
+ return {
996
+ text,
997
+ providerCount: providerNames.length,
998
+ providers: providerNames,
999
+ estimatedTokens: totalTokens + estimateTokens(header),
1000
+ durationMs: Date.now() - start
1001
+ };
1002
+ }
1003
+ async function warmAllProviders(projectRoot, enabledProviders) {
1004
+ const start = Date.now();
1005
+ const warmed = [];
1006
+ const tier2 = ALL_PROVIDERS.filter(
1007
+ (p) => p.tier === 2 && p.warmup && (!enabledProviders || enabledProviders.includes(p.name))
1008
+ );
1009
+ const available = await filterAvailable(tier2);
1010
+ const settled = await Promise.allSettled(
1011
+ available.map(async (p) => {
1012
+ try {
1013
+ const result = await withTimeout2(p.warmup(projectRoot), 5e3);
1014
+ if (result && result.entries.length > 0) {
1015
+ const { getStore: getStore2 } = await import("./core-VUVXLXZN.js");
1016
+ const store = await getStore2(projectRoot);
1017
+ try {
1018
+ store.warmCache(
1019
+ result.provider,
1020
+ [...result.entries],
1021
+ result.provider === "context7" ? 4 * 3600 : 3600
1022
+ );
1023
+ store.save();
1024
+ } finally {
1025
+ store.close();
1026
+ }
1027
+ warmed.push(p.name);
1028
+ }
1029
+ } catch {
1030
+ }
1031
+ })
1032
+ );
1033
+ return { warmed, durationMs: Date.now() - start };
1034
+ }
1035
+ var availabilityCache = /* @__PURE__ */ new Map();
1036
+ async function filterAvailable(providers) {
1037
+ const checks = providers.map(async (p) => {
1038
+ let available = availabilityCache.get(p.name);
1039
+ if (available === void 0) {
1040
+ try {
1041
+ const timeout = p.tier === 1 ? 200 : 500;
1042
+ available = await withTimeout2(p.isAvailable(), timeout);
1043
+ } catch {
1044
+ available = false;
1045
+ }
1046
+ availabilityCache.set(p.name, available);
1047
+ }
1048
+ return { provider: p, available };
1049
+ });
1050
+ const settled = await Promise.all(checks);
1051
+ return settled.filter((c) => c.available).map((c) => c.provider);
1052
+ }
1053
+ async function resolveWithTimeout(provider, filePath, context) {
1054
+ try {
1055
+ return await withTimeout2(
1056
+ provider.resolve(filePath, context),
1057
+ provider.timeoutMs
1058
+ );
1059
+ } catch {
1060
+ return null;
1061
+ }
1062
+ }
1063
+ function withTimeout2(promise, ms) {
1064
+ return new Promise((resolve7, reject) => {
1065
+ const timer = setTimeout(() => reject(new Error("timeout")), ms);
1066
+ promise.then((val) => {
1067
+ clearTimeout(timer);
1068
+ resolve7(val);
1069
+ }).catch((err) => {
1070
+ clearTimeout(timer);
1071
+ reject(err);
1072
+ });
1073
+ });
1074
+ }
1075
+
339
1076
  // src/intercept/handlers/read.ts
340
1077
  var READ_CONFIDENCE_THRESHOLD = 0.7;
341
1078
  async function handleRead(payload) {
@@ -356,11 +1093,81 @@ async function handleRead(payload) {
356
1093
  if (!fileCtx.found || fileCtx.codeNodeCount === 0) return PASSTHROUGH;
357
1094
  if (fileCtx.isStale) return PASSTHROUGH;
358
1095
  if (fileCtx.confidence < READ_CONFIDENCE_THRESHOLD) return PASSTHROUGH;
1096
+ const relPath = relative(ctx.projectRoot, ctx.absPath).replaceAll("\\", "/");
1097
+ try {
1098
+ const nodeContext = await buildNodeContext(
1099
+ ctx.projectRoot,
1100
+ relPath,
1101
+ fileCtx
1102
+ );
1103
+ const enrichmentProviders = [
1104
+ "engram:mistakes",
1105
+ "engram:git",
1106
+ "mempalace",
1107
+ "context7",
1108
+ "obsidian"
1109
+ ];
1110
+ const richPacket = await withRichTimeout(
1111
+ resolveRichPacket(relPath, nodeContext, enrichmentProviders),
1112
+ 1500
1113
+ );
1114
+ if (richPacket && richPacket.providerCount > 0) {
1115
+ const enrichedText = `${fileCtx.summary}
1116
+
1117
+ ${richPacket.text}`;
1118
+ return buildDenyResponse(enrichedText);
1119
+ }
1120
+ } catch {
1121
+ }
359
1122
  return buildDenyResponse(fileCtx.summary);
360
1123
  }
1124
+ async function buildNodeContext(projectRoot, relPath, fileCtx) {
1125
+ const store = await getStore(projectRoot);
1126
+ try {
1127
+ const nodes = store.getNodesByFile(relPath);
1128
+ const edges = store.getEdgesForNodes(nodes.map((n) => n.id));
1129
+ const imports = edges.filter((e) => e.relation === "imports").map((e) => {
1130
+ const parts = e.target.split("::");
1131
+ return parts[parts.length - 1];
1132
+ }).filter((name) => name && !name.startsWith(".") && !name.startsWith("/"));
1133
+ const baseName = relPath.replace(/\.\w+$/, "");
1134
+ const testPatterns = [
1135
+ `${baseName}.test`,
1136
+ `${baseName}.spec`,
1137
+ `tests/${baseName.split("/").pop()}`
1138
+ ];
1139
+ const hasTests = testPatterns.some(
1140
+ (pattern) => store.searchNodes(pattern, 1).length > 0
1141
+ );
1142
+ const fileNode = nodes.find((n) => n.kind === "file");
1143
+ const churnRate = fileNode?.metadata?.churn_rate ?? 0;
1144
+ return {
1145
+ filePath: relPath,
1146
+ projectRoot,
1147
+ nodeIds: nodes.map((n) => n.id),
1148
+ imports: [...new Set(imports)],
1149
+ hasTests,
1150
+ churnRate
1151
+ };
1152
+ } finally {
1153
+ store.close();
1154
+ }
1155
+ }
1156
+ function withRichTimeout(promise, ms) {
1157
+ return new Promise((resolve7) => {
1158
+ const timer = setTimeout(() => resolve7(null), ms);
1159
+ promise.then((val) => {
1160
+ clearTimeout(timer);
1161
+ resolve7(val);
1162
+ }).catch(() => {
1163
+ clearTimeout(timer);
1164
+ resolve7(null);
1165
+ });
1166
+ });
1167
+ }
361
1168
 
362
1169
  // src/intercept/handlers/edit-write.ts
363
- import { relative, resolve as resolvePath } from "path";
1170
+ import { relative as relative2, resolve as resolvePath } from "path";
364
1171
  var MAX_LANDMINES_IN_WARNING = 5;
365
1172
  function formatLandmineWarning(projectRelativeFile, mistakeList) {
366
1173
  const header = `[engram landmines] ${mistakeList.length} past mistake${mistakeList.length === 1 ? "" : "s"} recorded for ${projectRelativeFile}:`;
@@ -383,7 +1190,7 @@ async function handleEditOrWrite(payload) {
383
1190
  if (isContentUnsafeForIntercept(ctx.absPath)) return PASSTHROUGH;
384
1191
  if (isHookDisabled(ctx.projectRoot)) return PASSTHROUGH;
385
1192
  const relPath = toPosixPath(
386
- relative(resolvePath(ctx.projectRoot), ctx.absPath)
1193
+ relative2(resolvePath(ctx.projectRoot), ctx.absPath)
387
1194
  );
388
1195
  if (!relPath || relPath.startsWith("..")) return PASSTHROUGH;
389
1196
  let found;
@@ -443,10 +1250,10 @@ async function handleBash(payload) {
443
1250
 
444
1251
  // src/intercept/handlers/session-start.ts
445
1252
  import { existsSync as existsSync3, readFileSync } from "fs";
446
- import { execFile } from "child_process";
1253
+ import { execFile as execFile3 } from "child_process";
447
1254
  import { promisify } from "util";
448
1255
  import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "path";
449
- var execFileAsync = promisify(execFile);
1256
+ var execFileAsync = promisify(execFile3);
450
1257
  var MAX_GOD_NODES = 10;
451
1258
  var MAX_LANDMINES_IN_BRIEF = 3;
452
1259
  function readGitBranch(projectRoot) {
@@ -588,6 +1395,8 @@ async function handleSessionStart(payload) {
588
1395
  }))
589
1396
  });
590
1397
  const fullText = mempalaceContext ? text + "\n\n" + mempalaceContext : text;
1398
+ warmAllProviders(projectRoot).catch(() => {
1399
+ });
591
1400
  return buildSessionContextResponse("SessionStart", fullText);
592
1401
  } catch {
593
1402
  return PASSTHROUGH;
@@ -1064,7 +1873,7 @@ function extractPreToolDecision(result) {
1064
1873
 
1065
1874
  // src/watcher.ts
1066
1875
  import { watch, existsSync as existsSync5, statSync as statSync3 } from "fs";
1067
- import { resolve as resolve5, relative as relative2, extname } from "path";
1876
+ import { resolve as resolve5, relative as relative3, extname } from "path";
1068
1877
  var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1069
1878
  ".ts",
1070
1879
  ".tsx",
@@ -1105,7 +1914,7 @@ async function reindexFile(absPath, projectRoot) {
1105
1914
  } catch {
1106
1915
  return 0;
1107
1916
  }
1108
- const relPath = toPosixPath(relative2(projectRoot, absPath));
1917
+ const relPath = toPosixPath(relative3(projectRoot, absPath));
1109
1918
  if (shouldIgnore(relPath)) return 0;
1110
1919
  const store = await getStore(projectRoot);
1111
1920
  try {
@@ -1132,7 +1941,7 @@ function watchProject(projectRoot, options = {}) {
1132
1941
  watcher.on("change", (_eventType, filename) => {
1133
1942
  if (typeof filename !== "string") return;
1134
1943
  const absPath = resolve5(root, filename);
1135
- const relPath = toPosixPath(relative2(root, absPath));
1944
+ const relPath = toPosixPath(relative3(root, absPath));
1136
1945
  if (shouldIgnore(relPath)) return;
1137
1946
  const ext = extname(filename).toLowerCase();
1138
1947
  if (!WATCHABLE_EXTENSIONS.has(ext)) return;
@@ -1450,6 +2259,7 @@ var ENGRAM_HOOK_EVENTS = [
1450
2259
  var ENGRAM_PRETOOL_MATCHER = "Read|Edit|Write|Bash";
1451
2260
  var DEFAULT_ENGRAM_COMMAND = "engram intercept";
1452
2261
  var DEFAULT_HOOK_TIMEOUT_SEC = 5;
2262
+ var DEFAULT_STATUSLINE_COMMAND = "engram hud-label";
1453
2263
  function buildEngramHookEntries(command = DEFAULT_ENGRAM_COMMAND, timeout = DEFAULT_HOOK_TIMEOUT_SEC) {
1454
2264
  const baseCmd = {
1455
2265
  type: "command",
@@ -1519,10 +2329,14 @@ function installEngramHooks(settings, command = DEFAULT_ENGRAM_COMMAND) {
1519
2329
  hooksClone[event] = [...eventArr, entries[event]];
1520
2330
  added.push(event);
1521
2331
  }
2332
+ const hasStatusLine = settings.statusLine && typeof settings.statusLine === "object" && typeof settings.statusLine.command === "string" && settings.statusLine.command.length > 0;
2333
+ const statusLineAdded = !hasStatusLine;
2334
+ const statusLine = hasStatusLine ? settings.statusLine : { type: "command", command: DEFAULT_STATUSLINE_COMMAND };
1522
2335
  return {
1523
- updated: { ...settings, hooks: hooksClone },
2336
+ updated: { ...settings, hooks: hooksClone, statusLine },
1524
2337
  added,
1525
- alreadyPresent
2338
+ alreadyPresent,
2339
+ statusLineAdded
1526
2340
  };
1527
2341
  }
1528
2342
  function uninstallEngramHooks(settings) {
@@ -1545,7 +2359,11 @@ function uninstallEngramHooks(settings) {
1545
2359
  } else {
1546
2360
  updatedSettings.hooks = hooksClone;
1547
2361
  }
1548
- return { updated: updatedSettings, removed };
2362
+ const statusLineRemoved = typeof updatedSettings.statusLine?.command === "string" && updatedSettings.statusLine.command.includes("engram hud-label");
2363
+ if (statusLineRemoved) {
2364
+ delete updatedSettings.statusLine;
2365
+ }
2366
+ return { updated: updatedSettings, removed, statusLineRemoved };
1549
2367
  }
1550
2368
  function isKnownEngramEvent(event) {
1551
2369
  return ENGRAM_HOOK_EVENTS.includes(event);
@@ -1569,6 +2387,13 @@ function formatInstallDiff(before, after) {
1569
2387
  }
1570
2388
  }
1571
2389
  }
2390
+ const hadStatusLine = before.statusLine?.command;
2391
+ const hasStatusLineNow = after.statusLine?.command;
2392
+ if (!hadStatusLine && hasStatusLineNow?.includes("engram hud-label")) {
2393
+ lines.push(`+ statusLine: engram hud-label (HUD enabled)`);
2394
+ } else if (hadStatusLine?.includes("engram hud-label") && !hasStatusLineNow) {
2395
+ lines.push(`- statusLine: engram hud-label (HUD removed)`);
2396
+ }
1572
2397
  return lines.length > 0 ? lines.join("\n") : "(no changes)";
1573
2398
  }
1574
2399
 
@@ -2066,7 +2891,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
2066
2891
  \u{1F4CC} engram install-hook (scope: ${opts.scope})`)
2067
2892
  );
2068
2893
  console.log(chalk2.dim(` Target: ${settingsPath}`));
2069
- if (result.added.length === 0) {
2894
+ if (result.added.length === 0 && !result.statusLineAdded) {
2070
2895
  console.log(
2071
2896
  chalk2.yellow(
2072
2897
  `
@@ -2108,12 +2933,19 @@ program.command("install-hook").description("Install engram hook entries into Cl
2108
2933
  );
2109
2934
  process.exit(1);
2110
2935
  }
2111
- console.log(
2112
- chalk2.green(
2113
- `
2936
+ if (result.added.length > 0) {
2937
+ console.log(
2938
+ chalk2.green(
2939
+ `
2114
2940
  \u2705 Installed ${result.added.length} hook event${result.added.length === 1 ? "" : "s"}: ${result.added.join(", ")}`
2115
- )
2116
- );
2941
+ )
2942
+ );
2943
+ }
2944
+ if (result.statusLineAdded) {
2945
+ console.log(
2946
+ chalk2.green(" \u2705 StatusLine: engram hud-label (HUD visible in Claude Code)")
2947
+ );
2948
+ }
2117
2949
  if (result.alreadyPresent.length > 0) {
2118
2950
  console.log(
2119
2951
  chalk2.dim(
@@ -2151,7 +2983,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
2151
2983
  process.exit(1);
2152
2984
  }
2153
2985
  const result = uninstallEngramHooks(existing);
2154
- if (result.removed.length === 0) {
2986
+ if (result.removed.length === 0 && !result.statusLineRemoved) {
2155
2987
  console.log(
2156
2988
  chalk2.yellow(`
2157
2989
  No engram hooks found in ${settingsPath}.`)
@@ -2164,12 +2996,19 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
2164
2996
  const tmpPath = settingsPath + ".engram-tmp";
2165
2997
  writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
2166
2998
  renameSync3(tmpPath, settingsPath);
2167
- console.log(
2168
- chalk2.green(
2169
- `
2999
+ if (result.removed.length > 0) {
3000
+ console.log(
3001
+ chalk2.green(
3002
+ `
2170
3003
  \u2705 Removed engram hooks from ${result.removed.length} event${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`
2171
- )
2172
- );
3004
+ )
3005
+ );
3006
+ }
3007
+ if (result.statusLineRemoved) {
3008
+ console.log(
3009
+ chalk2.green(" \u2705 Removed engram statusLine (HUD)")
3010
+ );
3011
+ }
2173
3012
  console.log(chalk2.dim(` Backup: ${backupPath}`));
2174
3013
  } catch (err) {
2175
3014
  console.error(