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