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.
- package/CHANGELOG.md +271 -0
- package/README.md +161 -17
- package/dist/{aider-context-BC5R2ZTA.js → aider-context-6IDE3R7U.js} +1 -1
- package/dist/check-2Z3MPZEJ.js +12 -0
- package/dist/{chunk-PEH54LYC.js → chunk-645NBY6L.js} +42 -5
- package/dist/chunk-73IBCRFI.js +215 -0
- package/dist/{chunk-SJT7VS2G.js → chunk-B4UOE64J.js} +46 -11
- package/dist/chunk-FKY6HIT2.js +99 -0
- package/dist/{chunk-533LR4I7.js → chunk-G4U3QOOW.js} +13 -97
- package/dist/chunk-RJC6RNXJ.js +1405 -0
- package/dist/chunk-RM2TBOVW.js +121 -0
- package/dist/chunk-SMU4WR3D.js +187 -0
- package/dist/{chunk-C6GBUOAL.js → chunk-VLTWBTQ7.js} +14 -15
- package/dist/chunk-XVYE4OX2.js +232 -0
- package/dist/chunk-ZUC6OXSL.js +178 -0
- package/dist/cli.js +818 -1533
- package/dist/{core-6IY5L6II.js → core-77F2BVYV.js} +2 -2
- package/dist/{cursor-mdc-GJ7E5LDD.js → cursor-mdc-EEO7PYZ3.js} +1 -1
- package/dist/{exporter-GWU2GF23.js → exporter-ZYJ4WM2F.js} +1 -1
- package/dist/{importer-V62NGZRK.js → importer-4UWQDH4W.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/install-YVMVCFQW.js +121 -0
- package/dist/mcp-client-ROOJF76V.js +9 -0
- package/dist/mcp-config-QD4NPVXB.js +12 -0
- package/dist/{migrate-UKCO6BUU.js → migrate-KJ5K5NWO.js} +1 -1
- package/dist/notify-5POGKMRX.js +36 -0
- package/dist/{plugin-loader-STTGYIL5.js → plugin-loader-SQQB6V74.js} +69 -23
- package/dist/report-C3GTM3HY.js +12 -0
- package/dist/resolver-H7GXVP73.js +21 -0
- package/dist/serve.js +5 -4
- package/dist/{server-KUG7U6SG.js → server-2ZQKXJ5M.js} +74 -4
- package/dist/{windsurf-rules-C7SVDHBL.js → windsurf-rules-XF7MYF6J.js} +1 -1
- package/dist/wizard-UH27IO4I.js +274 -0
- package/package.json +3 -2
- 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-
|
|
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-
|
|
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-
|
|
39
|
-
import "./chunk-
|
|
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
|
|
46
|
-
readFileSync as
|
|
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
|
|
54
|
-
import { fileURLToPath
|
|
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
|
|
1675
|
-
import { execFile
|
|
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
|
|
1678
|
-
var execFileAsync = promisify(
|
|
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 =
|
|
1686
|
-
if (
|
|
1687
|
-
const content =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
2344
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
2467
|
-
if (
|
|
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
|
|
2678
|
-
readFileSync as
|
|
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
|
|
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 =
|
|
1756
|
+
const memoryPath = join6(projectRoot, "MEMORY.md");
|
|
2755
1757
|
try {
|
|
2756
1758
|
let existing = "";
|
|
2757
|
-
if (
|
|
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 =
|
|
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
|
|
2777
|
-
var
|
|
2778
|
-
var { version: PKG_VERSION } =
|
|
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
|
|
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
|
|
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 =
|
|
2822
|
-
const localSettings =
|
|
2823
|
-
const projectSettings =
|
|
2824
|
-
const hasHooks =
|
|
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-
|
|
2839
|
-
const resolvedProjectPath =
|
|
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 =
|
|
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 =
|
|
2880
|
-
const dbPath =
|
|
2881
|
-
if (!
|
|
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 =
|
|
2018
|
+
let resolvedPath = pathResolve2(projectPath);
|
|
2899
2019
|
let found = false;
|
|
2900
2020
|
for (let depth = 0; depth < 20; depth++) {
|
|
2901
|
-
if (
|
|
2021
|
+
if (existsSync7(join7(resolvedPath, ".engram", "graph.db"))) {
|
|
2902
2022
|
found = true;
|
|
2903
2023
|
break;
|
|
2904
2024
|
}
|
|
2905
|
-
const parent =
|
|
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 =
|
|
2914
|
-
if (!
|
|
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
|
|
2995
|
-
console.log(` Avg query: ~${bench.avgQueryTokens
|
|
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
|
|
3040
|
-
console.log(` Avg graph query: ~${result.avgQueryTokens
|
|
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(
|
|
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 ${
|
|
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-
|
|
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(
|
|
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-
|
|
3090
|
-
const result = await exportCcs(
|
|
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-
|
|
3099
|
-
const result = await generateAiderContext(
|
|
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(
|
|
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-
|
|
3120
|
-
const result = await generateWindsurfRules(
|
|
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(
|
|
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 =
|
|
2278
|
+
const absProject = pathResolve2(projectPath);
|
|
3141
2279
|
switch (scope) {
|
|
3142
2280
|
case "local":
|
|
3143
|
-
return
|
|
2281
|
+
return join7(absProject, ".claude", "settings.local.json");
|
|
3144
2282
|
case "project":
|
|
3145
|
-
return
|
|
2283
|
+
return join7(absProject, ".claude", "settings.json");
|
|
3146
2284
|
case "user":
|
|
3147
|
-
return
|
|
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", ".").
|
|
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 (
|
|
2383
|
+
if (existsSync7(settingsPath)) {
|
|
3242
2384
|
try {
|
|
3243
|
-
const raw =
|
|
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 (
|
|
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(
|
|
3289
|
-
if (
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3405
|
-
const absFile =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3488
|
-
if (!
|
|
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 =
|
|
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 =
|
|
3531
|
-
if (
|
|
3532
|
-
const content =
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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-
|
|
3609
|
-
await startHttpServer(
|
|
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 =
|
|
3615
|
-
const { existsSync:
|
|
3616
|
-
const pidPath =
|
|
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 (
|
|
2774
|
+
if (existsSync8(pidPath)) {
|
|
3619
2775
|
try {
|
|
3620
|
-
const pid = parseInt(
|
|
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:
|
|
3648
|
-
|
|
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
|
|
2812
|
+
const { execFileSync } = await import("child_process");
|
|
3657
2813
|
try {
|
|
3658
|
-
|
|
2814
|
+
execFileSync("npx", ["tsx", "adapters/zed/index.ts"], {
|
|
3659
2815
|
stdio: "inherit",
|
|
3660
2816
|
shell: true,
|
|
3661
|
-
cwd:
|
|
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-
|
|
3669
|
-
const proposal = analyzeTuning(
|
|
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(
|
|
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-
|
|
3700
|
-
const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-
|
|
3701
|
-
const store = await getStore2(
|
|
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-
|
|
3717
|
-
const { runMigrations } = await import("./migrate-
|
|
3718
|
-
const store = await getStore2(
|
|
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 =
|
|
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-
|
|
3752
|
-
const { rollback, getSchemaVersion } = await import("./migrate-
|
|
3753
|
-
const store = await getStore2(
|
|
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 =
|
|
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-
|
|
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-
|
|
2994
|
+
const { getPluginsDir, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-SQQB6V74.js");
|
|
3839
2995
|
const { pathToFileURL } = await import("url");
|
|
3840
|
-
const absPath =
|
|
3841
|
-
if (!
|
|
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 =
|
|
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-
|
|
3031
|
+
const { getPluginsDir } = await import("./plugin-loader-SQQB6V74.js");
|
|
3876
3032
|
const pluginsDir = getPluginsDir();
|
|
3877
|
-
const target =
|
|
3878
|
-
if (!
|
|
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-
|
|
3044
|
+
const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
|
|
3889
3045
|
const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
|
|
3890
|
-
const store = await getStore2(
|
|
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-
|
|
3078
|
+
const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
|
|
3923
3079
|
const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
|
|
3924
|
-
const store = await getStore2(
|
|
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-
|
|
3097
|
+
const { getStore: getStore2 } = await import("./core-77F2BVYV.js");
|
|
3942
3098
|
const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
|
|
3943
|
-
const store = await getStore2(
|
|
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,
|
|
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();
|