engramx 0.4.3 → 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/README.md +72 -31
- package/dist/{chunk-ESPAWLH6.js → chunk-LH2ZID5Z.js} +1 -1
- package/dist/{chunk-R46DNLNR.js → chunk-V5VQQ3SF.js} +255 -21
- package/dist/cli.js +876 -27
- package/dist/{core-WTKXDUDO.js → core-VUVXLXZN.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/serve.js +1 -1
- package/package.json +2 -3
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
install,
|
|
5
5
|
status,
|
|
6
6
|
uninstall
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
|
@@ -1774,12 +2599,22 @@ program.command("dashboard").alias("hud").description("Live terminal dashboard s
|
|
|
1774
2599
|
});
|
|
1775
2600
|
});
|
|
1776
2601
|
program.command("hud-label").description("Output JSON label for Claude HUD --extra-cmd (fast, <20ms)").argument("[path]", "Project directory", ".").action(async (projectPath) => {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2602
|
+
let resolvedPath = pathResolve(projectPath);
|
|
2603
|
+
let found = false;
|
|
2604
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
2605
|
+
if (existsSync8(join8(resolvedPath, ".engram", "graph.db"))) {
|
|
2606
|
+
found = true;
|
|
2607
|
+
break;
|
|
2608
|
+
}
|
|
2609
|
+
const parent = dirname3(resolvedPath);
|
|
2610
|
+
if (parent === resolvedPath) break;
|
|
2611
|
+
resolvedPath = parent;
|
|
2612
|
+
}
|
|
2613
|
+
if (!found) {
|
|
1780
2614
|
console.log('{"label":""}');
|
|
1781
2615
|
return;
|
|
1782
2616
|
}
|
|
2617
|
+
const logPath = join8(resolvedPath, ".engram", "hook-log.jsonl");
|
|
1783
2618
|
if (!existsSync8(logPath)) {
|
|
1784
2619
|
console.log('{"label":"\u26A1engram \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 ready"}');
|
|
1785
2620
|
return;
|
|
@@ -2056,7 +2891,7 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
2056
2891
|
\u{1F4CC} engram install-hook (scope: ${opts.scope})`)
|
|
2057
2892
|
);
|
|
2058
2893
|
console.log(chalk2.dim(` Target: ${settingsPath}`));
|
|
2059
|
-
if (result.added.length === 0) {
|
|
2894
|
+
if (result.added.length === 0 && !result.statusLineAdded) {
|
|
2060
2895
|
console.log(
|
|
2061
2896
|
chalk2.yellow(
|
|
2062
2897
|
`
|
|
@@ -2098,12 +2933,19 @@ program.command("install-hook").description("Install engram hook entries into Cl
|
|
|
2098
2933
|
);
|
|
2099
2934
|
process.exit(1);
|
|
2100
2935
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2936
|
+
if (result.added.length > 0) {
|
|
2937
|
+
console.log(
|
|
2938
|
+
chalk2.green(
|
|
2939
|
+
`
|
|
2104
2940
|
\u2705 Installed ${result.added.length} hook event${result.added.length === 1 ? "" : "s"}: ${result.added.join(", ")}`
|
|
2105
|
-
|
|
2106
|
-
|
|
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
|
+
}
|
|
2107
2949
|
if (result.alreadyPresent.length > 0) {
|
|
2108
2950
|
console.log(
|
|
2109
2951
|
chalk2.dim(
|
|
@@ -2141,7 +2983,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
2141
2983
|
process.exit(1);
|
|
2142
2984
|
}
|
|
2143
2985
|
const result = uninstallEngramHooks(existing);
|
|
2144
|
-
if (result.removed.length === 0) {
|
|
2986
|
+
if (result.removed.length === 0 && !result.statusLineRemoved) {
|
|
2145
2987
|
console.log(
|
|
2146
2988
|
chalk2.yellow(`
|
|
2147
2989
|
No engram hooks found in ${settingsPath}.`)
|
|
@@ -2154,12 +2996,19 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
|
|
|
2154
2996
|
const tmpPath = settingsPath + ".engram-tmp";
|
|
2155
2997
|
writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
|
|
2156
2998
|
renameSync3(tmpPath, settingsPath);
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2999
|
+
if (result.removed.length > 0) {
|
|
3000
|
+
console.log(
|
|
3001
|
+
chalk2.green(
|
|
3002
|
+
`
|
|
2160
3003
|
\u2705 Removed engram hooks from ${result.removed.length} event${result.removed.length === 1 ? "" : "s"}: ${result.removed.join(", ")}`
|
|
2161
|
-
|
|
2162
|
-
|
|
3004
|
+
)
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
if (result.statusLineRemoved) {
|
|
3008
|
+
console.log(
|
|
3009
|
+
chalk2.green(" \u2705 Removed engram statusLine (HUD)")
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
2163
3012
|
console.log(chalk2.dim(` Backup: ${backupPath}`));
|
|
2164
3013
|
} catch (err) {
|
|
2165
3014
|
console.error(
|