engramx 0.5.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ logHookEvent,
4
+ readConfig,
5
+ readHookLog
6
+ } from "./chunk-CEAANHHX.js";
2
7
  import {
3
8
  autogen,
4
9
  install,
5
10
  status,
6
11
  uninstall
7
- } from "./chunk-KEV4LNM6.js";
12
+ } from "./chunk-QOG4K427.js";
8
13
  import {
9
14
  benchmark,
10
15
  computeKeywordIDF,
@@ -21,22 +26,24 @@ import {
21
26
  renderFileStructure,
22
27
  stats,
23
28
  toPosixPath
24
- } from "./chunk-IIFAAYDO.js";
29
+ } from "./chunk-6SFMVYUN.js";
30
+ import "./chunk-SBHGK5WA.js";
25
31
 
26
32
  // src/cli.ts
27
33
  import { Command } from "commander";
28
34
  import chalk2 from "chalk";
29
35
  import {
30
- existsSync as existsSync8,
31
- readFileSync as readFileSync5,
32
- writeFileSync as writeFileSync2,
36
+ existsSync as existsSync10,
37
+ readFileSync as readFileSync6,
38
+ writeFileSync as writeFileSync3,
33
39
  mkdirSync,
34
40
  unlinkSync,
35
41
  copyFileSync,
36
- renameSync as renameSync3
42
+ renameSync as renameSync2
37
43
  } from "fs";
38
- import { dirname as dirname3, join as join8, resolve as pathResolve } from "path";
39
- import { homedir } from "os";
44
+ import { dirname as dirname4, join as join10, resolve as pathResolve } from "path";
45
+ import { fileURLToPath as fileURLToPath2 } from "url";
46
+ import { homedir as homedir2 } from "os";
40
47
 
41
48
  // src/intercept/safety.ts
42
49
  import { existsSync } from "fs";
@@ -342,15 +349,253 @@ function buildSessionContextResponse(eventName, additionalContext) {
342
349
 
343
350
  // src/providers/types.ts
344
351
  var PROVIDER_PRIORITY = [
352
+ "engram:ast",
345
353
  "engram:structure",
346
354
  "engram:mistakes",
347
355
  "mempalace",
348
356
  "context7",
349
357
  "engram:git",
350
- "obsidian"
358
+ "obsidian",
359
+ "engram:lsp"
351
360
  ];
352
361
  var DEFAULT_CACHE_TTL_SEC = 3600;
353
362
 
363
+ // src/providers/ast.ts
364
+ import { readFileSync } from "fs";
365
+
366
+ // src/providers/grammar-loader.ts
367
+ import { existsSync as existsSync3 } from "fs";
368
+ import { join as join3, dirname as dirname2 } from "path";
369
+ import { createRequire } from "module";
370
+ import { fileURLToPath } from "url";
371
+ var require2 = createRequire(import.meta.url);
372
+ var parserCache = /* @__PURE__ */ new Map();
373
+ var tsParserInit = false;
374
+ var EXT_TO_LANG = {
375
+ ts: "typescript",
376
+ tsx: "tsx",
377
+ js: "javascript",
378
+ jsx: "javascript",
379
+ mjs: "javascript",
380
+ cjs: "javascript",
381
+ py: "python",
382
+ go: "go",
383
+ rs: "rust",
384
+ rb: "ruby",
385
+ java: "java",
386
+ c: "c",
387
+ cpp: "cpp",
388
+ h: "c",
389
+ hpp: "cpp",
390
+ php: "php"
391
+ };
392
+ var LANG_TO_PKG = {
393
+ typescript: "tree-sitter-typescript",
394
+ tsx: "tree-sitter-typescript",
395
+ javascript: "tree-sitter-javascript",
396
+ python: "tree-sitter-python",
397
+ go: "tree-sitter-go",
398
+ rust: "tree-sitter-rust"
399
+ };
400
+ function getSupportedLang(filePath) {
401
+ const ext = filePath.split(".").pop()?.toLowerCase();
402
+ return ext ? EXT_TO_LANG[ext] ?? null : null;
403
+ }
404
+ function findGrammarWasm(lang) {
405
+ const pkg = LANG_TO_PKG[lang];
406
+ if (!pkg) return null;
407
+ const wasmName = lang === "tsx" ? "tree-sitter-tsx.wasm" : `tree-sitter-${lang}.wasm`;
408
+ const candidates = [];
409
+ try {
410
+ const here = dirname2(fileURLToPath(import.meta.url));
411
+ candidates.push(join3(here, "..", "..", "node_modules", pkg, wasmName));
412
+ candidates.push(join3(here, "..", "grammars", wasmName));
413
+ } catch {
414
+ }
415
+ try {
416
+ const pkgMain = require2.resolve(`${pkg}/package.json`);
417
+ const pkgDir = dirname2(pkgMain);
418
+ candidates.push(join3(pkgDir, wasmName));
419
+ } catch {
420
+ }
421
+ return candidates.find((c) => existsSync3(c)) ?? null;
422
+ }
423
+ async function getParser(lang) {
424
+ const cached = parserCache.get(lang);
425
+ if (cached) return cached;
426
+ try {
427
+ const { Parser, Language } = await import("web-tree-sitter");
428
+ if (!tsParserInit) {
429
+ await Parser.init();
430
+ tsParserInit = true;
431
+ }
432
+ const wasmPath = findGrammarWasm(lang);
433
+ if (!wasmPath) return null;
434
+ const language = await Language.load(wasmPath);
435
+ const parser = new Parser();
436
+ parser.setLanguage(language);
437
+ parserCache.set(lang, parser);
438
+ return parser;
439
+ } catch {
440
+ return null;
441
+ }
442
+ }
443
+
444
+ // src/providers/ast.ts
445
+ function extractParams(node) {
446
+ const paramsNode = node.childForFieldName("parameters") ?? node.childForFieldName("formal_parameters");
447
+ if (!paramsNode) return "";
448
+ return paramsNode.text.replace(/\n/g, " ").replace(/\s+/g, " ").slice(0, 80).trim();
449
+ }
450
+ function extractSymbols(rootNode) {
451
+ const symbols = [];
452
+ function visit(node) {
453
+ switch (node.type) {
454
+ // ── Functions ───────────────────────────────────────────────
455
+ case "function_declaration":
456
+ case "function_definition": {
457
+ const nameNode = node.childForFieldName("name");
458
+ if (nameNode) {
459
+ symbols.push({
460
+ name: nameNode.text,
461
+ kind: "function",
462
+ line: node.startPosition.row + 1,
463
+ params: extractParams(node)
464
+ });
465
+ }
466
+ break;
467
+ }
468
+ // ── Classes ─────────────────────────────────────────────────
469
+ case "class_declaration":
470
+ case "class_definition": {
471
+ const nameNode = node.childForFieldName("name");
472
+ if (nameNode) {
473
+ symbols.push({
474
+ name: nameNode.text,
475
+ kind: "class",
476
+ line: node.startPosition.row + 1
477
+ });
478
+ }
479
+ break;
480
+ }
481
+ // ── Methods ─────────────────────────────────────────────────
482
+ case "method_definition":
483
+ case "method_declaration": {
484
+ const nameNode = node.childForFieldName("name");
485
+ if (nameNode) {
486
+ symbols.push({
487
+ name: nameNode.text,
488
+ kind: "method",
489
+ line: node.startPosition.row + 1,
490
+ params: extractParams(node)
491
+ });
492
+ }
493
+ break;
494
+ }
495
+ // ── TypeScript interfaces ────────────────────────────────────
496
+ case "interface_declaration": {
497
+ const nameNode = node.childForFieldName("name");
498
+ if (nameNode) {
499
+ symbols.push({
500
+ name: nameNode.text,
501
+ kind: "interface",
502
+ line: node.startPosition.row + 1
503
+ });
504
+ }
505
+ break;
506
+ }
507
+ // ── TypeScript type aliases ──────────────────────────────────
508
+ case "type_alias_declaration": {
509
+ const nameNode = node.childForFieldName("name");
510
+ if (nameNode) {
511
+ symbols.push({
512
+ name: nameNode.text,
513
+ kind: "type",
514
+ line: node.startPosition.row + 1
515
+ });
516
+ }
517
+ break;
518
+ }
519
+ // ── Exported variable declarations (incl. arrow functions) ──
520
+ case "lexical_declaration":
521
+ case "variable_declaration": {
522
+ for (let i = 0; i < node.childCount; i++) {
523
+ const child = node.child(i);
524
+ if (!child || child.type !== "variable_declarator") continue;
525
+ const nameNode = child.childForFieldName("name");
526
+ const valueNode = child.childForFieldName("value");
527
+ if (!nameNode) continue;
528
+ const isArrow = valueNode?.type === "arrow_function" || valueNode?.type === "function";
529
+ symbols.push({
530
+ name: nameNode.text,
531
+ kind: isArrow ? "function" : "variable",
532
+ line: node.startPosition.row + 1,
533
+ params: isArrow && valueNode ? extractParams(valueNode) : void 0
534
+ });
535
+ }
536
+ break;
537
+ }
538
+ default:
539
+ break;
540
+ }
541
+ for (let i = 0; i < node.childCount; i++) {
542
+ const child = node.child(i);
543
+ if (child) visit(child);
544
+ }
545
+ }
546
+ visit(rootNode);
547
+ return symbols;
548
+ }
549
+ function formatSymbols(symbols, tokenBudget) {
550
+ const lines = symbols.map((s) => {
551
+ const params = s.params !== void 0 ? `(${s.params})` : "";
552
+ return `${s.kind.toUpperCase()} ${s.name}${params} L${s.line}`;
553
+ });
554
+ const charBudget = tokenBudget * 4;
555
+ let text = lines.join("\n");
556
+ if (text.length > charBudget) {
557
+ text = text.slice(0, charBudget).trimEnd() + "\n... (truncated)";
558
+ }
559
+ return text;
560
+ }
561
+ var astProvider = {
562
+ name: "engram:ast",
563
+ label: "AST STRUCTURE",
564
+ tier: 1,
565
+ tokenBudget: 300,
566
+ timeoutMs: 200,
567
+ async resolve(filePath, _context) {
568
+ const lang = getSupportedLang(filePath);
569
+ if (!lang) return null;
570
+ const parser = await getParser(lang);
571
+ if (!parser) return null;
572
+ try {
573
+ const source = readFileSync(filePath, "utf-8");
574
+ const tree = parser.parse(source);
575
+ if (!tree) return null;
576
+ const symbols = extractSymbols(tree.rootNode);
577
+ if (symbols.length === 0) return null;
578
+ return {
579
+ provider: "engram:ast",
580
+ content: formatSymbols(symbols, this.tokenBudget),
581
+ confidence: 1,
582
+ cached: false
583
+ };
584
+ } catch {
585
+ return null;
586
+ }
587
+ },
588
+ async isAvailable() {
589
+ try {
590
+ const { Parser } = await import("web-tree-sitter");
591
+ await Parser.init();
592
+ return true;
593
+ } catch {
594
+ return false;
595
+ }
596
+ }
597
+ };
598
+
354
599
  // src/providers/engram-structure.ts
355
600
  var structureProvider = {
356
601
  name: "engram:structure",
@@ -935,16 +1180,161 @@ async function fetchWithTimeout(url, timeoutMs) {
935
1180
  }
936
1181
  }
937
1182
 
1183
+ // src/providers/lsp-connection.ts
1184
+ import { connect } from "net";
1185
+ import { existsSync as existsSync4 } from "fs";
1186
+ import { tmpdir } from "os";
1187
+ import { join as join4 } from "path";
1188
+ function candidateSockets() {
1189
+ const uid = process.getuid?.() ?? 0;
1190
+ const tmp = tmpdir();
1191
+ return [
1192
+ // TypeScript language server (used by VS Code)
1193
+ join4(tmp, `tsserver-${uid}.sock`),
1194
+ // Generic LSP socket (some editors, e.g. Helix)
1195
+ join4(tmp, "lsp-server.sock"),
1196
+ // TypeScript language server alternate path
1197
+ join4(tmp, "typescript-language-server.sock"),
1198
+ // Pyright (Python)
1199
+ join4(tmp, `pyright-${uid}.sock`),
1200
+ // rust-analyzer
1201
+ join4(tmp, "rust-analyzer.sock")
1202
+ ];
1203
+ }
1204
+ var LspConnection = class _LspConnection {
1205
+ socket = null;
1206
+ _requestId = 0;
1207
+ /**
1208
+ * Attempt to connect to any currently-running LSP server socket.
1209
+ * Returns null — not throws — if no socket is found or connection fails.
1210
+ * Timeout per candidate: 500ms.
1211
+ */
1212
+ static async tryConnect() {
1213
+ const candidates = candidateSockets().filter((p) => existsSync4(p));
1214
+ if (candidates.length === 0) return null;
1215
+ for (const path2 of candidates) {
1216
+ try {
1217
+ const conn = new _LspConnection();
1218
+ await conn._connect(path2);
1219
+ return conn;
1220
+ } catch {
1221
+ continue;
1222
+ }
1223
+ }
1224
+ return null;
1225
+ }
1226
+ /** Internal: open a socket to the given path with a 500ms timeout. */
1227
+ _connect(socketPath) {
1228
+ return new Promise((resolve7, reject) => {
1229
+ const socket = connect(socketPath);
1230
+ const timeout = setTimeout(() => {
1231
+ socket.destroy();
1232
+ reject(new Error("LSP connect timeout"));
1233
+ }, 500);
1234
+ socket.on("connect", () => {
1235
+ clearTimeout(timeout);
1236
+ this.socket = socket;
1237
+ resolve7();
1238
+ });
1239
+ socket.on("error", (err) => {
1240
+ clearTimeout(timeout);
1241
+ reject(err);
1242
+ });
1243
+ });
1244
+ }
1245
+ /**
1246
+ * Request hover info for a position.
1247
+ *
1248
+ * Stub: returns null. A full implementation would send a JSON-RPC
1249
+ * textDocument/hover request and parse the response. Left as a stub
1250
+ * because the response requires a request/response correlation loop
1251
+ * over a streaming socket — non-trivial, and out of scope for v0.5.x.
1252
+ * The provider benefits from the availability check alone.
1253
+ */
1254
+ async hover(_filePath, _line, _character) {
1255
+ if (!this.socket) return null;
1256
+ return null;
1257
+ }
1258
+ /**
1259
+ * Fetch diagnostics for a file.
1260
+ *
1261
+ * Stub: returns []. A full implementation would use the
1262
+ * textDocument/diagnostic pull request (LSP 3.17+) or subscribe to
1263
+ * publishDiagnostics push notifications. Deferred to a future sprint.
1264
+ */
1265
+ async getDiagnostics(_filePath) {
1266
+ if (!this.socket) return [];
1267
+ return [];
1268
+ }
1269
+ /** Whether this connection has a live socket. */
1270
+ get connected() {
1271
+ return this.socket !== null && !this.socket.destroyed;
1272
+ }
1273
+ /** Close and destroy the socket. Safe to call multiple times. */
1274
+ close() {
1275
+ this.socket?.destroy();
1276
+ this.socket = null;
1277
+ }
1278
+ };
1279
+
1280
+ // src/providers/lsp.ts
1281
+ var cachedConnection = void 0;
1282
+ async function getConnection() {
1283
+ if (cachedConnection instanceof LspConnection) {
1284
+ if (cachedConnection.connected) return cachedConnection;
1285
+ cachedConnection.close();
1286
+ cachedConnection = void 0;
1287
+ }
1288
+ if (cachedConnection === null) return null;
1289
+ cachedConnection = await LspConnection.tryConnect();
1290
+ return cachedConnection;
1291
+ }
1292
+ var lspProvider = {
1293
+ name: "engram:lsp",
1294
+ label: "LSP CONTEXT",
1295
+ tier: 1,
1296
+ tokenBudget: 100,
1297
+ timeoutMs: 100,
1298
+ async resolve(filePath, _context) {
1299
+ try {
1300
+ const conn = await getConnection();
1301
+ if (!conn) return null;
1302
+ const hover = await conn.hover(filePath, 0, 0);
1303
+ if (!hover?.contents) return null;
1304
+ const content = typeof hover.contents === "string" ? hover.contents : JSON.stringify(hover.contents);
1305
+ const charBudget = this.tokenBudget * 4;
1306
+ const truncated = content.length > charBudget ? content.slice(0, charBudget) + "..." : content;
1307
+ return {
1308
+ provider: "engram:lsp",
1309
+ content: truncated,
1310
+ confidence: 0.95,
1311
+ cached: false
1312
+ };
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ },
1317
+ async isAvailable() {
1318
+ try {
1319
+ const conn = await getConnection();
1320
+ return conn !== null;
1321
+ } catch {
1322
+ return false;
1323
+ }
1324
+ }
1325
+ };
1326
+
938
1327
  // src/providers/resolver.ts
939
1328
  var ALL_PROVIDERS = [
1329
+ astProvider,
940
1330
  structureProvider,
941
1331
  mistakesProvider,
942
1332
  gitProvider,
943
1333
  mempalaceProvider,
944
1334
  context7Provider,
945
- obsidianProvider
1335
+ obsidianProvider,
1336
+ lspProvider
946
1337
  ];
947
- var TOTAL_TOKEN_BUDGET = 600;
948
1338
  function estimateTokens(text) {
949
1339
  return Math.ceil(text.length / 4);
950
1340
  }
@@ -966,16 +1356,20 @@ async function resolveRichPacket(filePath, context, enabledProviders) {
966
1356
  }
967
1357
  }
968
1358
  if (results.length === 0) return null;
969
- const sorted = results.sort((a, b) => {
1359
+ const hasAst = results.some((r) => r.provider === "engram:ast");
1360
+ const deduped = hasAst ? results.filter((r) => r.provider !== "engram:structure") : results;
1361
+ const sorted = deduped.sort((a, b) => {
970
1362
  const aIdx = PROVIDER_PRIORITY.indexOf(a.provider);
971
1363
  const bIdx = PROVIDER_PRIORITY.indexOf(b.provider);
972
1364
  return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
973
1365
  });
1366
+ const config = readConfig(context.projectRoot);
1367
+ const budget = config.totalTokenBudget;
974
1368
  const sections = [];
975
1369
  let totalTokens = 0;
976
1370
  for (const result of sorted) {
977
1371
  const sectionTokens = estimateTokens(result.content);
978
- if (totalTokens + sectionTokens > TOTAL_TOKEN_BUDGET) {
1372
+ if (totalTokens + sectionTokens > budget) {
979
1373
  break;
980
1374
  }
981
1375
  const provider = ALL_PROVIDERS.find((p) => p.name === result.provider);
@@ -1012,7 +1406,7 @@ async function warmAllProviders(projectRoot, enabledProviders) {
1012
1406
  try {
1013
1407
  const result = await withTimeout2(p.warmup(projectRoot), 5e3);
1014
1408
  if (result && result.entries.length > 0) {
1015
- const { getStore: getStore2 } = await import("./core-UXIP2GDR.js");
1409
+ const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
1016
1410
  const store = await getStore2(projectRoot);
1017
1411
  try {
1018
1412
  store.warmCache(
@@ -1249,10 +1643,10 @@ async function handleBash(payload) {
1249
1643
  }
1250
1644
 
1251
1645
  // src/intercept/handlers/session-start.ts
1252
- import { existsSync as existsSync3, readFileSync } from "fs";
1646
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
1253
1647
  import { execFile as execFile3 } from "child_process";
1254
1648
  import { promisify } from "util";
1255
- import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "path";
1649
+ import { basename, dirname as dirname3, join as join5, resolve as resolve2 } from "path";
1256
1650
  var execFileAsync = promisify(execFile3);
1257
1651
  var MAX_GOD_NODES = 10;
1258
1652
  var MAX_LANDMINES_IN_BRIEF = 3;
@@ -1260,15 +1654,15 @@ function readGitBranch(projectRoot) {
1260
1654
  try {
1261
1655
  let current = resolve2(projectRoot);
1262
1656
  for (let depth = 0; depth < 10; depth++) {
1263
- const headPath = join3(current, ".git", "HEAD");
1264
- if (existsSync3(headPath)) {
1265
- const content = readFileSync(headPath, "utf-8").trim();
1657
+ const headPath = join5(current, ".git", "HEAD");
1658
+ if (existsSync5(headPath)) {
1659
+ const content = readFileSync2(headPath, "utf-8").trim();
1266
1660
  const refMatch = content.match(/^ref:\s+refs\/heads\/(.+)$/);
1267
1661
  if (refMatch) return refMatch[1];
1268
1662
  if (/^[0-9a-f]{7,40}$/i.test(content)) return "detached";
1269
1663
  return null;
1270
1664
  }
1271
- const parent = dirname2(current);
1665
+ const parent = dirname3(current);
1272
1666
  if (parent === current) return null;
1273
1667
  current = parent;
1274
1668
  }
@@ -1547,61 +1941,6 @@ ${result.text}`;
1547
1941
  return buildSessionContextResponse("UserPromptSubmit", text);
1548
1942
  }
1549
1943
 
1550
- // src/intelligence/hook-log.ts
1551
- import {
1552
- appendFileSync,
1553
- existsSync as existsSync4,
1554
- renameSync,
1555
- statSync as statSync2,
1556
- readFileSync as readFileSync2
1557
- } from "fs";
1558
- import { join as join4 } from "path";
1559
- var HOOK_LOG_MAX_BYTES = 10 * 1024 * 1024;
1560
- var LOG_FILENAME = "hook-log.jsonl";
1561
- var LOG_ROTATED_FILENAME = "hook-log.jsonl.1";
1562
- function logHookEvent(projectRoot, entry) {
1563
- if (!projectRoot) return;
1564
- try {
1565
- const logPath = join4(projectRoot, ".engram", LOG_FILENAME);
1566
- rotateIfNeeded(projectRoot);
1567
- const line = JSON.stringify({
1568
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1569
- ...entry
1570
- }) + "\n";
1571
- appendFileSync(logPath, line);
1572
- } catch {
1573
- }
1574
- }
1575
- function rotateIfNeeded(projectRoot) {
1576
- try {
1577
- const logPath = join4(projectRoot, ".engram", LOG_FILENAME);
1578
- if (!existsSync4(logPath)) return;
1579
- const size = statSync2(logPath).size;
1580
- if (size < HOOK_LOG_MAX_BYTES) return;
1581
- const rotatedPath = join4(projectRoot, ".engram", LOG_ROTATED_FILENAME);
1582
- renameSync(logPath, rotatedPath);
1583
- } catch {
1584
- }
1585
- }
1586
- function readHookLog(projectRoot) {
1587
- try {
1588
- const logPath = join4(projectRoot, ".engram", LOG_FILENAME);
1589
- if (!existsSync4(logPath)) return [];
1590
- const raw = readFileSync2(logPath, "utf-8");
1591
- const entries = [];
1592
- for (const line of raw.split("\n")) {
1593
- if (!line.trim()) continue;
1594
- try {
1595
- entries.push(JSON.parse(line));
1596
- } catch {
1597
- }
1598
- }
1599
- return entries;
1600
- } catch {
1601
- return [];
1602
- }
1603
- }
1604
-
1605
1944
  // src/intercept/handlers/post-tool.ts
1606
1945
  function extractFilePath(toolName, toolInput) {
1607
1946
  if (!toolInput) return void 0;
@@ -1872,7 +2211,7 @@ function extractPreToolDecision(result) {
1872
2211
  }
1873
2212
 
1874
2213
  // src/watcher.ts
1875
- import { watch, existsSync as existsSync5, statSync as statSync3 } from "fs";
2214
+ import { watch, existsSync as existsSync6, statSync as statSync2 } from "fs";
1876
2215
  import { resolve as resolve5, relative as relative3, extname } from "path";
1877
2216
  var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1878
2217
  ".ts",
@@ -1908,9 +2247,9 @@ function shouldIgnore(relPath) {
1908
2247
  async function reindexFile(absPath, projectRoot) {
1909
2248
  const ext = extname(absPath).toLowerCase();
1910
2249
  if (!WATCHABLE_EXTENSIONS.has(ext)) return 0;
1911
- if (!existsSync5(absPath)) return 0;
2250
+ if (!existsSync6(absPath)) return 0;
1912
2251
  try {
1913
- if (statSync3(absPath).isDirectory()) return 0;
2252
+ if (statSync2(absPath).isDirectory()) return 0;
1914
2253
  } catch {
1915
2254
  return 0;
1916
2255
  }
@@ -1931,7 +2270,7 @@ async function reindexFile(absPath, projectRoot) {
1931
2270
  function watchProject(projectRoot, options = {}) {
1932
2271
  const root = resolve5(projectRoot);
1933
2272
  const controller = new AbortController();
1934
- if (!existsSync5(getDbPath(root))) {
2273
+ if (!existsSync6(getDbPath(root))) {
1935
2274
  throw new Error(
1936
2275
  `engram: no graph found at ${root}. Run 'engram init' first.`
1937
2276
  );
@@ -1973,8 +2312,8 @@ function watchProject(projectRoot, options = {}) {
1973
2312
 
1974
2313
  // src/dashboard.ts
1975
2314
  import chalk from "chalk";
1976
- import { existsSync as existsSync6, statSync as statSync4 } from "fs";
1977
- import { join as join6, resolve as resolve6, basename as basename4 } from "path";
2315
+ import { existsSync as existsSync7, statSync as statSync3 } from "fs";
2316
+ import { join as join7, resolve as resolve6, basename as basename4 } from "path";
1978
2317
 
1979
2318
  // src/intercept/stats.ts
1980
2319
  var ESTIMATED_TOKENS_PER_READ_DENY = 1200;
@@ -2188,9 +2527,9 @@ function startDashboard(projectRoot, options = {}) {
2188
2527
  const tick = () => {
2189
2528
  if (controller.signal.aborted) return;
2190
2529
  try {
2191
- const logPath = join6(root, ".engram", "hook-log.jsonl");
2192
- if (existsSync6(logPath)) {
2193
- const currentSize = statSync4(logPath).size;
2530
+ const logPath = join7(root, ".engram", "hook-log.jsonl");
2531
+ if (existsSync7(logPath)) {
2532
+ const currentSize = statSync3(logPath).size;
2194
2533
  if (currentSize !== lastSize) {
2195
2534
  cachedEntries = readHookLog(root);
2196
2535
  lastSize = currentSize;
@@ -2397,15 +2736,116 @@ function formatInstallDiff(before, after) {
2397
2736
  return lines.length > 0 ? lines.join("\n") : "(no changes)";
2398
2737
  }
2399
2738
 
2739
+ // src/intercept/component-status.ts
2740
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
2741
+ import { join as join8 } from "path";
2742
+ import { tmpdir as tmpdir2, homedir } from "os";
2743
+ function statusPath(projectRoot) {
2744
+ return join8(projectRoot, ".engram", "component-status.json");
2745
+ }
2746
+ function readCachedStatus(projectRoot) {
2747
+ const path2 = statusPath(projectRoot);
2748
+ if (!existsSync8(path2)) return null;
2749
+ try {
2750
+ const raw = JSON.parse(readFileSync4(path2, "utf-8"));
2751
+ if (Date.now() - raw.generatedAt > 3e4) return null;
2752
+ return raw;
2753
+ } catch {
2754
+ return null;
2755
+ }
2756
+ }
2757
+ function checkHttp(projectRoot) {
2758
+ return existsSync8(join8(projectRoot, ".engram", "http-server.pid"));
2759
+ }
2760
+ function checkLsp(projectRoot) {
2761
+ if (existsSync8(join8(projectRoot, ".engram", "lsp-available"))) return true;
2762
+ const tmp = tmpdir2();
2763
+ const candidates = [
2764
+ join8(tmp, "tsserver.sock"),
2765
+ join8(tmp, "typescript-language-server.sock")
2766
+ ];
2767
+ return candidates.some((c) => existsSync8(c));
2768
+ }
2769
+ function checkAst(projectRoot) {
2770
+ const grammarsDir = join8(projectRoot, "node_modules", "web-tree-sitter");
2771
+ return existsSync8(grammarsDir);
2772
+ }
2773
+ function countIdeAdapters(projectRoot) {
2774
+ let count = 0;
2775
+ if (existsSync8(join8(projectRoot, ".cursor", "rules", "engram-context.mdc"))) {
2776
+ count += 1;
2777
+ }
2778
+ const continueConfig = join8(homedir(), ".continue", "config.json");
2779
+ if (existsSync8(continueConfig)) {
2780
+ try {
2781
+ const cfg = readFileSync4(continueConfig, "utf-8");
2782
+ if (cfg.includes("engram")) count += 1;
2783
+ } catch {
2784
+ }
2785
+ }
2786
+ const zedSettings = join8(homedir(), ".config", "zed", "settings.json");
2787
+ if (existsSync8(zedSettings)) {
2788
+ try {
2789
+ const cfg = readFileSync4(zedSettings, "utf-8");
2790
+ if (cfg.includes("engram")) count += 1;
2791
+ } catch {
2792
+ }
2793
+ }
2794
+ const claudeSettings = join8(projectRoot, ".claude", "settings.local.json");
2795
+ if (existsSync8(claudeSettings)) {
2796
+ try {
2797
+ const cfg = readFileSync4(claudeSettings, "utf-8");
2798
+ if (cfg.includes("engram")) count += 1;
2799
+ } catch {
2800
+ }
2801
+ }
2802
+ return count;
2803
+ }
2804
+ function refreshComponentStatus(projectRoot) {
2805
+ const now = Date.now();
2806
+ const components = [
2807
+ { name: "http", available: checkHttp(projectRoot), checkedAt: now },
2808
+ { name: "lsp", available: checkLsp(projectRoot), checkedAt: now },
2809
+ { name: "ast", available: checkAst(projectRoot), checkedAt: now }
2810
+ ];
2811
+ const ideCount = countIdeAdapters(projectRoot);
2812
+ const report = {
2813
+ components,
2814
+ ideCount,
2815
+ generatedAt: now
2816
+ };
2817
+ try {
2818
+ writeFileSync(statusPath(projectRoot), JSON.stringify(report), "utf-8");
2819
+ } catch {
2820
+ }
2821
+ return report;
2822
+ }
2823
+ function getComponentStatus(projectRoot) {
2824
+ const cached = readCachedStatus(projectRoot);
2825
+ if (cached) return cached;
2826
+ return refreshComponentStatus(projectRoot);
2827
+ }
2828
+ function formatHudStatus(report) {
2829
+ const parts = [];
2830
+ for (const c of report.components) {
2831
+ const icon = c.available ? "\u2713" : "\u2717";
2832
+ parts.push(`${c.name.toUpperCase()} ${icon}`);
2833
+ }
2834
+ if (report.ideCount > 0) {
2835
+ parts.push(`${report.ideCount} IDE${report.ideCount > 1 ? "s" : ""}`);
2836
+ }
2837
+ return parts.join(" | ");
2838
+ }
2839
+
2400
2840
  // src/intercept/memory-md.ts
2401
2841
  import {
2402
- existsSync as existsSync7,
2403
- readFileSync as readFileSync4,
2404
- writeFileSync,
2405
- renameSync as renameSync2,
2406
- statSync as statSync5
2842
+ existsSync as existsSync9,
2843
+ readFileSync as readFileSync5,
2844
+ writeFileSync as writeFileSync2,
2845
+ renameSync,
2846
+ statSync as statSync4
2407
2847
  } from "fs";
2408
- import { join as join7 } from "path";
2848
+ import { join as join9 } from "path";
2409
2849
  var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
2410
2850
  var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
2411
2851
  var MAX_MEMORY_FILE_BYTES = 1e6;
@@ -2476,20 +2916,20 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
2476
2916
  if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
2477
2917
  return false;
2478
2918
  }
2479
- const memoryPath = join7(projectRoot, "MEMORY.md");
2919
+ const memoryPath = join9(projectRoot, "MEMORY.md");
2480
2920
  try {
2481
2921
  let existing = "";
2482
- if (existsSync7(memoryPath)) {
2483
- const st = statSync5(memoryPath);
2922
+ if (existsSync9(memoryPath)) {
2923
+ const st = statSync4(memoryPath);
2484
2924
  if (st.size > MAX_MEMORY_FILE_BYTES) {
2485
2925
  return false;
2486
2926
  }
2487
- existing = readFileSync4(memoryPath, "utf-8");
2927
+ existing = readFileSync5(memoryPath, "utf-8");
2488
2928
  }
2489
2929
  const updated = upsertEngramSection(existing, engramSection);
2490
2930
  const tmpPath = memoryPath + ".engram-tmp";
2491
- writeFileSync(tmpPath, updated);
2492
- renameSync2(tmpPath, memoryPath);
2931
+ writeFileSync2(tmpPath, updated);
2932
+ renameSync(tmpPath, memoryPath);
2493
2933
  return true;
2494
2934
  } catch {
2495
2935
  return false;
@@ -2498,9 +2938,9 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
2498
2938
 
2499
2939
  // src/cli.ts
2500
2940
  import { basename as basename5 } from "path";
2501
- import { createRequire } from "module";
2502
- var require2 = createRequire(import.meta.url);
2503
- var { version: PKG_VERSION } = require2("../package.json");
2941
+ import { createRequire as createRequire2 } from "module";
2942
+ var require3 = createRequire2(import.meta.url);
2943
+ var { version: PKG_VERSION } = require3("../package.json");
2504
2944
  var program = new Command();
2505
2945
  program.name("engram").description(
2506
2946
  "Context as infra for AI coding tools \u2014 hook-based Read/Edit interception + structural graph summaries"
@@ -2508,7 +2948,7 @@ program.name("engram").description(
2508
2948
  program.command("init").description("Scan codebase and build knowledge graph (zero LLM cost)").argument("[path]", "Project directory", ".").option(
2509
2949
  "--with-skills [dir]",
2510
2950
  "Also index Claude Code skills from ~/.claude/skills/ or a given path"
2511
- ).action(async (projectPath, opts) => {
2951
+ ).option("--from-ccs", "Import .context/index.md (CCS) into graph after init").action(async (projectPath, opts) => {
2512
2952
  console.log(chalk2.dim("\u{1F50D} Scanning codebase..."));
2513
2953
  const result = await init(projectPath, {
2514
2954
  withSkills: opts.withSkills
@@ -2537,9 +2977,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2537
2977
  console.log(chalk2.green("\n\u2705 Ready. Your AI now has persistent memory."));
2538
2978
  console.log(chalk2.dim(" Graph stored in .engram/graph.db"));
2539
2979
  const resolvedProject = pathResolve(projectPath);
2540
- const localSettings = join8(resolvedProject, ".claude", "settings.local.json");
2541
- const projectSettings = join8(resolvedProject, ".claude", "settings.json");
2542
- const hasHooks = existsSync8(localSettings) && readFileSync5(localSettings, "utf-8").includes("engram intercept") || existsSync8(projectSettings) && readFileSync5(projectSettings, "utf-8").includes("engram intercept");
2980
+ const localSettings = join10(resolvedProject, ".claude", "settings.local.json");
2981
+ const projectSettings = join10(resolvedProject, ".claude", "settings.json");
2982
+ const hasHooks = existsSync10(localSettings) && readFileSync6(localSettings, "utf-8").includes("engram intercept") || existsSync10(projectSettings) && readFileSync6(projectSettings, "utf-8").includes("engram intercept");
2543
2983
  if (!hasHooks) {
2544
2984
  console.log(
2545
2985
  chalk2.yellow("\n\u{1F4A1} Next step: ") + chalk2.white("engram install-hook") + chalk2.dim(
@@ -2552,6 +2992,20 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2552
2992
  )
2553
2993
  );
2554
2994
  }
2995
+ if (opts.fromCcs) {
2996
+ const { importCcs } = await import("./importer-LU2YFZDY.js");
2997
+ const resolvedProjectPath = pathResolve(projectPath);
2998
+ const ccsResult = await importCcs(resolvedProjectPath);
2999
+ if (ccsResult.nodesCreated > 0) {
3000
+ console.log(
3001
+ chalk2.cyan(
3002
+ ` ${ccsResult.nodesCreated} nodes imported from .context/index.md`
3003
+ )
3004
+ );
3005
+ } else {
3006
+ console.log(chalk2.dim(" --from-ccs: no .context/index.md found, skipping"));
3007
+ }
3008
+ }
2555
3009
  });
2556
3010
  program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2557
3011
  const resolvedPath = pathResolve(projectPath);
@@ -2581,8 +3035,8 @@ program.command("watch").description("Watch project for file changes and re-inde
2581
3035
  });
2582
3036
  program.command("dashboard").alias("hud").description("Live terminal dashboard showing hook activity and token savings").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2583
3037
  const resolvedPath = pathResolve(projectPath);
2584
- const dbPath = join8(resolvedPath, ".engram", "graph.db");
2585
- if (!existsSync8(dbPath)) {
3038
+ const dbPath = join10(resolvedPath, ".engram", "graph.db");
3039
+ if (!existsSync10(dbPath)) {
2586
3040
  console.error(
2587
3041
  chalk2.red("No engram graph found at ") + chalk2.white(resolvedPath)
2588
3042
  );
@@ -2602,11 +3056,11 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
2602
3056
  let resolvedPath = pathResolve(projectPath);
2603
3057
  let found = false;
2604
3058
  for (let depth = 0; depth < 20; depth++) {
2605
- if (existsSync8(join8(resolvedPath, ".engram", "graph.db"))) {
3059
+ if (existsSync10(join10(resolvedPath, ".engram", "graph.db"))) {
2606
3060
  found = true;
2607
3061
  break;
2608
3062
  }
2609
- const parent = dirname3(resolvedPath);
3063
+ const parent = dirname4(resolvedPath);
2610
3064
  if (parent === resolvedPath) break;
2611
3065
  resolvedPath = parent;
2612
3066
  }
@@ -2614,8 +3068,8 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
2614
3068
  console.log('{"label":""}');
2615
3069
  return;
2616
3070
  }
2617
- const logPath = join8(resolvedPath, ".engram", "hook-log.jsonl");
2618
- if (!existsSync8(logPath)) {
3071
+ const logPath = join10(resolvedPath, ".engram", "hook-log.jsonl");
3072
+ if (!existsSync10(logPath)) {
2619
3073
  console.log('{"label":"\u26A1engram \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 ready"}');
2620
3074
  return;
2621
3075
  }
@@ -2639,7 +3093,10 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
2639
3093
  if (filled > barWidth) filled = barWidth;
2640
3094
  if (denied > 0 && filled === 0) filled = 1;
2641
3095
  const bar2 = "\u25B0".repeat(filled) + "\u25B1".repeat(barWidth - filled);
2642
- console.log(JSON.stringify({ label: `\u26A1engram ${formatted} saved ${bar2} ${hitRate}%` }));
3096
+ const status2 = getComponentStatus(resolvedPath);
3097
+ const statusSuffix = formatHudStatus(status2);
3098
+ const label = statusSuffix ? `\u26A1engram ${formatted} saved ${bar2} ${hitRate}% | ${statusSuffix}` : `\u26A1engram ${formatted} saved ${bar2} ${hitRate}%`;
3099
+ console.log(JSON.stringify({ label }));
2643
3100
  } catch {
2644
3101
  console.log('{"label":"\u26A1engram"}');
2645
3102
  }
@@ -2765,15 +3222,66 @@ program.command("gen").description("Generate CLAUDE.md / .cursorrules section fr
2765
3222
  );
2766
3223
  }
2767
3224
  );
3225
+ 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) => {
3226
+ const { generateCursorMdc } = await import("./cursor-mdc-HWVUZUZH.js");
3227
+ const result = await generateCursorMdc(opts.project);
3228
+ console.log(
3229
+ chalk2.green(
3230
+ `\u2705 Generated ${result.filePath} (${result.sections} sections, ${result.nodes} nodes)`
3231
+ )
3232
+ );
3233
+ if (opts.watch) {
3234
+ watchProject(pathResolve(opts.project), {
3235
+ onReindex: async () => {
3236
+ const r = await generateCursorMdc(opts.project);
3237
+ console.log(chalk2.dim(` \u21BB Regenerated MDC (${r.nodes} nodes)`));
3238
+ },
3239
+ onError: (err) => console.error(chalk2.red(err.message)),
3240
+ onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3241
+ });
3242
+ await new Promise(() => {
3243
+ });
3244
+ }
3245
+ });
3246
+ program.command("gen-ccs").description("Export knowledge graph as .context/index.md (CCS format)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3247
+ const { exportCcs } = await import("./exporter-A3VSLS4U.js");
3248
+ const result = await exportCcs(pathResolve(opts.project));
3249
+ console.log(
3250
+ chalk2.green(
3251
+ `\u2705 Generated ${result.filePath} (${result.sectionsWritten} sections, ${result.nodesExported} nodes)`
3252
+ )
3253
+ );
3254
+ });
3255
+ 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) => {
3256
+ const { generateAiderContext } = await import("./aider-context-TNGSXMVY.js");
3257
+ const result = await generateAiderContext(pathResolve(opts.project));
3258
+ console.log(
3259
+ chalk2.green(
3260
+ `\u2705 Generated ${result.filePath} (${result.sections} sections, ${result.nodes} nodes)`
3261
+ )
3262
+ );
3263
+ if (opts.watch) {
3264
+ watchProject(pathResolve(opts.project), {
3265
+ onReindex: async () => {
3266
+ const r = await generateAiderContext(opts.project);
3267
+ console.log(chalk2.dim(` \u21BB Regenerated .aider-context.md (${r.nodes} nodes)`));
3268
+ },
3269
+ onError: (err) => console.error(chalk2.red(err.message)),
3270
+ onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3271
+ });
3272
+ await new Promise(() => {
3273
+ });
3274
+ }
3275
+ });
2768
3276
  function resolveSettingsPath(scope, projectPath) {
2769
3277
  const absProject = pathResolve(projectPath);
2770
3278
  switch (scope) {
2771
3279
  case "local":
2772
- return join8(absProject, ".claude", "settings.local.json");
3280
+ return join10(absProject, ".claude", "settings.local.json");
2773
3281
  case "project":
2774
- return join8(absProject, ".claude", "settings.json");
3282
+ return join10(absProject, ".claude", "settings.json");
2775
3283
  case "user":
2776
- return join8(homedir(), ".claude", "settings.json");
3284
+ return join10(homedir2(), ".claude", "settings.json");
2777
3285
  default:
2778
3286
  return null;
2779
3287
  }
@@ -2867,9 +3375,9 @@ program.command("install-hook").description("Install engram hook entries into Cl
2867
3375
  process.exit(1);
2868
3376
  }
2869
3377
  let existing = {};
2870
- if (existsSync8(settingsPath)) {
3378
+ if (existsSync10(settingsPath)) {
2871
3379
  try {
2872
- const raw = readFileSync5(settingsPath, "utf-8");
3380
+ const raw = readFileSync6(settingsPath, "utf-8");
2873
3381
  existing = raw.trim() ? JSON.parse(raw) : {};
2874
3382
  } catch (err) {
2875
3383
  console.error(
@@ -2914,18 +3422,18 @@ program.command("install-hook").description("Install engram hook entries into Cl
2914
3422
  return;
2915
3423
  }
2916
3424
  try {
2917
- mkdirSync(dirname3(settingsPath), { recursive: true });
2918
- if (existsSync8(settingsPath)) {
3425
+ mkdirSync(dirname4(settingsPath), { recursive: true });
3426
+ if (existsSync10(settingsPath)) {
2919
3427
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
2920
3428
  copyFileSync(settingsPath, backupPath);
2921
3429
  console.log(chalk2.dim(` Backup: ${backupPath}`));
2922
3430
  }
2923
3431
  const tmpPath = settingsPath + ".engram-tmp";
2924
- writeFileSync2(
3432
+ writeFileSync3(
2925
3433
  tmpPath,
2926
3434
  JSON.stringify(result.updated, null, 2) + "\n"
2927
3435
  );
2928
- renameSync3(tmpPath, settingsPath);
3436
+ renameSync2(tmpPath, settingsPath);
2929
3437
  } catch (err) {
2930
3438
  console.error(
2931
3439
  chalk2.red(`
@@ -2966,7 +3474,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
2966
3474
  console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
2967
3475
  process.exit(1);
2968
3476
  }
2969
- if (!existsSync8(settingsPath)) {
3477
+ if (!existsSync10(settingsPath)) {
2970
3478
  console.log(
2971
3479
  chalk2.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
2972
3480
  );
@@ -2974,7 +3482,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
2974
3482
  }
2975
3483
  let existing;
2976
3484
  try {
2977
- const raw = readFileSync5(settingsPath, "utf-8");
3485
+ const raw = readFileSync6(settingsPath, "utf-8");
2978
3486
  existing = raw.trim() ? JSON.parse(raw) : {};
2979
3487
  } catch (err) {
2980
3488
  console.error(
@@ -2994,8 +3502,8 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
2994
3502
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
2995
3503
  copyFileSync(settingsPath, backupPath);
2996
3504
  const tmpPath = settingsPath + ".engram-tmp";
2997
- writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
2998
- renameSync3(tmpPath, settingsPath);
3505
+ writeFileSync3(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
3506
+ renameSync2(tmpPath, settingsPath);
2999
3507
  if (result.removed.length > 0) {
3000
3508
  console.log(
3001
3509
  chalk2.green(
@@ -3089,9 +3597,9 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
3089
3597
  console.error(chalk2.dim("Run 'engram init' first."));
3090
3598
  process.exit(1);
3091
3599
  }
3092
- const flagPath = join8(projectRoot, ".engram", "hook-disabled");
3600
+ const flagPath = join10(projectRoot, ".engram", "hook-disabled");
3093
3601
  try {
3094
- writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
3602
+ writeFileSync3(flagPath, (/* @__PURE__ */ new Date()).toISOString());
3095
3603
  console.log(
3096
3604
  chalk2.green(`\u2705 engram hooks disabled for ${projectRoot}`)
3097
3605
  );
@@ -3113,8 +3621,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
3113
3621
  console.error(chalk2.red(`Not an engram project: ${absProject}`));
3114
3622
  process.exit(1);
3115
3623
  }
3116
- const flagPath = join8(projectRoot, ".engram", "hook-disabled");
3117
- if (!existsSync8(flagPath)) {
3624
+ const flagPath = join10(projectRoot, ".engram", "hook-disabled");
3625
+ if (!existsSync10(flagPath)) {
3118
3626
  console.log(
3119
3627
  chalk2.yellow(`engram hooks already enabled for ${projectRoot}`)
3120
3628
  );
@@ -3156,9 +3664,9 @@ program.command("memory-sync").description(
3156
3664
  }
3157
3665
  let branch = null;
3158
3666
  try {
3159
- const headPath = join8(projectRoot, ".git", "HEAD");
3160
- if (existsSync8(headPath)) {
3161
- const content = readFileSync5(headPath, "utf-8").trim();
3667
+ const headPath = join10(projectRoot, ".git", "HEAD");
3668
+ if (existsSync10(headPath)) {
3669
+ const content = readFileSync6(headPath, "utf-8").trim();
3162
3670
  const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
3163
3671
  if (m) branch = m[1];
3164
3672
  }
@@ -3184,7 +3692,7 @@ program.command("memory-sync").description(
3184
3692
  \u{1F4DD} engram memory-sync`)
3185
3693
  );
3186
3694
  console.log(
3187
- chalk2.dim(` Target: ${join8(projectRoot, "MEMORY.md")}`)
3695
+ chalk2.dim(` Target: ${join10(projectRoot, "MEMORY.md")}`)
3188
3696
  );
3189
3697
  if (opts.dryRun) {
3190
3698
  console.log(chalk2.cyan("\n Section to write (dry-run):\n"));
@@ -3218,4 +3726,108 @@ program.command("memory-sync").description(
3218
3726
  );
3219
3727
  }
3220
3728
  );
3729
+ 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) => {
3730
+ const { execFileSync: execFileSync2 } = await import("child_process");
3731
+ const args = ["bench/stress-test.ts"];
3732
+ if (opts.reads) args.push("--reads", String(opts.reads));
3733
+ if (opts.providers) args.push("--providers");
3734
+ if (opts.largeGraph) args.push("--large-graph");
3735
+ if (opts.nodes) args.push("--nodes", String(opts.nodes));
3736
+ if (opts.replay) args.push("--replay", opts.replay);
3737
+ if (opts.limit) args.push("--limit", String(opts.limit));
3738
+ try {
3739
+ execFileSync2("npx", ["tsx", ...args], { stdio: "inherit", shell: true, cwd: join10(dirname4(fileURLToPath2(import.meta.url)), "..") });
3740
+ } catch {
3741
+ process.exit(1);
3742
+ }
3743
+ });
3744
+ 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) => {
3745
+ const { startHttpServer } = await import("./server-I3C74ZLB.js");
3746
+ await startHttpServer(pathResolve(opts.project), parseInt(opts.port, 10));
3747
+ });
3748
+ program.command("context-server").description("Start Zed-compatible context server (JSON-RPC over stdio)").action(async () => {
3749
+ const { execFileSync: execFileSync2 } = await import("child_process");
3750
+ try {
3751
+ execFileSync2("npx", ["tsx", "adapters/zed/index.ts"], {
3752
+ stdio: "inherit",
3753
+ shell: true,
3754
+ cwd: join10(dirname4(fileURLToPath2(import.meta.url)), "..")
3755
+ });
3756
+ } catch {
3757
+ process.exit(1);
3758
+ }
3759
+ });
3760
+ 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) => {
3761
+ const { analyzeTuning, applyTuning } = await import("./tuner-2LVIEE5V.js");
3762
+ const proposal = analyzeTuning(pathResolve(opts.project));
3763
+ if (proposal.changes.length === 0) {
3764
+ console.log(
3765
+ chalk2.dim(
3766
+ `Analyzed ${proposal.entriesAnalyzed} entries (${proposal.daysSpanned} days) \u2014 no changes suggested.`
3767
+ )
3768
+ );
3769
+ return;
3770
+ }
3771
+ console.log(
3772
+ chalk2.bold(
3773
+ `Analyzing ${proposal.entriesAnalyzed} hook-log entries from last ${proposal.daysSpanned} days...
3774
+ `
3775
+ )
3776
+ );
3777
+ console.log(chalk2.bold("Proposed changes:"));
3778
+ for (const c of proposal.changes) {
3779
+ console.log(
3780
+ ` ${c.field}: ${chalk2.red(String(c.current))} \u2192 ${chalk2.green(String(c.proposed))} \u2014 ${chalk2.dim(c.reason)}`
3781
+ );
3782
+ }
3783
+ if (opts.apply) {
3784
+ applyTuning(pathResolve(opts.project), proposal);
3785
+ console.log(chalk2.green("\n\u2705 Changes applied to .engram/config.json"));
3786
+ } else {
3787
+ console.log(chalk2.dim("\nRun with --apply to write these changes."));
3788
+ }
3789
+ });
3790
+ var dbCmd = program.command("db").description("Database management");
3791
+ dbCmd.command("status").description("Show schema version and migration status").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3792
+ const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
3793
+ const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-5ZJWF2HD.js");
3794
+ const store = await getStore2(pathResolve(opts.project));
3795
+ try {
3796
+ const version = getSchemaVersion(store.db);
3797
+ const pending = CURRENT_SCHEMA_VERSION - version;
3798
+ console.log(`Schema version: ${version} (current: ${CURRENT_SCHEMA_VERSION})`);
3799
+ if (pending > 0) {
3800
+ console.log(chalk2.yellow(`${pending} pending migration(s). Run 'engram db migrate' to update.`));
3801
+ } else {
3802
+ console.log(chalk2.green("Up to date."));
3803
+ }
3804
+ } finally {
3805
+ store.close();
3806
+ }
3807
+ });
3808
+ dbCmd.command("migrate").description("Run pending schema migrations").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3809
+ const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
3810
+ const { runMigrations } = await import("./migrate-5ZJWF2HD.js");
3811
+ const store = await getStore2(pathResolve(opts.project));
3812
+ try {
3813
+ const dbPath = join10(pathResolve(opts.project), ".engram", "graph.db");
3814
+ const result = runMigrations(
3815
+ store.db,
3816
+ dbPath
3817
+ );
3818
+ store.save();
3819
+ if (result.migrationsRun === 0) {
3820
+ console.log(chalk2.green("Already up to date."));
3821
+ } else {
3822
+ console.log(
3823
+ chalk2.green(`Migrated v${result.fromVersion} \u2192 v${result.toVersion} (${result.migrationsRun} migrations)`)
3824
+ );
3825
+ if (result.backedUp) {
3826
+ console.log(chalk2.dim("Backup created."));
3827
+ }
3828
+ }
3829
+ } finally {
3830
+ store.close();
3831
+ }
3832
+ });
3221
3833
  program.parse();