engramx 2.0.2 → 2.1.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,11 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ formatInstallDiff,
4
+ installEngramHooks,
5
+ uninstallEngramHooks
6
+ } from "./chunk-SMU4WR3D.js";
2
7
  import {
3
8
  ESTIMATED_TOKENS_PER_READ_DENY,
4
- formatHudStatus,
5
9
  formatStatsSummary,
6
- getComponentStatus,
7
10
  summarizeHookLog
8
- } from "./chunk-533LR4I7.js";
11
+ } from "./chunk-XFE6ZANP.js";
12
+ import {
13
+ formatHudStatus,
14
+ getComponentStatus
15
+ } from "./chunk-G4U3QOOW.js";
9
16
  import {
10
17
  readConfig
11
18
  } from "./chunk-22INHMKB.js";
@@ -18,11 +25,12 @@ import {
18
25
  install,
19
26
  status,
20
27
  uninstall
21
- } from "./chunk-C6GBUOAL.js";
28
+ } from "./chunk-4XA6ENNL.js";
22
29
  import {
23
30
  benchmark,
24
31
  computeKeywordIDF,
25
32
  extractFile,
33
+ formatThousands,
26
34
  getDbPath,
27
35
  getFileContext,
28
36
  getStore,
@@ -35,7 +43,7 @@ import {
35
43
  renderFileStructure,
36
44
  stats,
37
45
  toPosixPath
38
- } from "./chunk-SJT7VS2G.js";
46
+ } from "./chunk-ZVWRIVWQ.js";
39
47
  import "./chunk-PEH54LYC.js";
40
48
 
41
49
  // src/cli.ts
@@ -50,7 +58,7 @@ import {
50
58
  copyFileSync,
51
59
  renameSync as renameSync2
52
60
  } from "fs";
53
- import { dirname as dirname4, join as join9, resolve as pathResolve } from "path";
61
+ import { dirname as dirname4, join as join9, resolve as pathResolve2 } from "path";
54
62
  import { fileURLToPath as fileURLToPath2 } from "url";
55
63
  import { homedir } from "os";
56
64
 
@@ -1434,7 +1442,7 @@ async function warmAllProviders(projectRoot, enabledProviders) {
1434
1442
  try {
1435
1443
  const result = await withTimeout2(p.warmup(projectRoot), 5e3);
1436
1444
  if (result && result.entries.length > 0) {
1437
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
1445
+ const { getStore: getStore2 } = await import("./core-TSXA5XZH.js");
1438
1446
  const store = await getStore2(projectRoot);
1439
1447
  try {
1440
1448
  store.warmCache(
@@ -1969,6 +1977,219 @@ ${result.text}`;
1969
1977
  return buildSessionContextResponse("UserPromptSubmit", text);
1970
1978
  }
1971
1979
 
1980
+ // src/intercept/handlers/bash-postool.ts
1981
+ import { isAbsolute as isAbsolute2, resolve as pathResolve } from "path";
1982
+ var MAX_COMMAND_LEN = 500;
1983
+ var BASIC_UNSAFE = /[|&;()$`*?[\]{}"']/;
1984
+ var SUBSHELL = /\$\(|`|<\(|>\(/;
1985
+ function parseFileOps(command, cwd) {
1986
+ if (!command || typeof command !== "string") return [];
1987
+ if (command.length > MAX_COMMAND_LEN) return [];
1988
+ if (SUBSHELL.test(command)) return [];
1989
+ const trimmed = command.trim();
1990
+ if (!trimmed) return [];
1991
+ const redirectMatch = /\s+(>>?)\s+(\S+)\s*$/.exec(trimmed);
1992
+ if (redirectMatch) {
1993
+ const head = trimmed.slice(0, redirectMatch.index);
1994
+ const dest = redirectMatch[2];
1995
+ if (BASIC_UNSAFE.test(head)) return [];
1996
+ if (dest.startsWith("-") || dest.length === 0) return [];
1997
+ return [{ action: "reindex", path: absolutize(dest, cwd) }];
1998
+ }
1999
+ if (BASIC_UNSAFE.test(trimmed)) return [];
2000
+ const tokens = trimmed.split(/\s+/);
2001
+ if (tokens.length === 0) return [];
2002
+ const first = tokens[0];
2003
+ if (first === "git" && tokens.length >= 3) {
2004
+ const sub = tokens[1];
2005
+ if (sub === "rm") return parseRm(tokens.slice(2), cwd);
2006
+ if (sub === "mv") return parseMv(tokens.slice(2), cwd);
2007
+ return [];
2008
+ }
2009
+ if (first === "rm") return parseRm(tokens.slice(1), cwd);
2010
+ if (first === "mv") return parseMv(tokens.slice(1), cwd);
2011
+ if (first === "cp") return parseCp(tokens.slice(1), cwd);
2012
+ return [];
2013
+ }
2014
+ function absolutize(path2, cwd) {
2015
+ if (isAbsolute2(path2)) return path2;
2016
+ return pathResolve(cwd, path2);
2017
+ }
2018
+ function isFlagLike(tok) {
2019
+ return tok.startsWith("-");
2020
+ }
2021
+ function parseRm(args, cwd) {
2022
+ const paths = args.filter((t) => !isFlagLike(t));
2023
+ if (paths.length === 0) return [];
2024
+ return paths.map((p) => ({ action: "prune", path: absolutize(p, cwd) }));
2025
+ }
2026
+ function parseMv(args, cwd) {
2027
+ const paths = args.filter((t) => !isFlagLike(t));
2028
+ if (paths.length !== 2) return [];
2029
+ const [src, dst] = paths;
2030
+ return [
2031
+ { action: "prune", path: absolutize(src, cwd) },
2032
+ { action: "reindex", path: absolutize(dst, cwd) }
2033
+ ];
2034
+ }
2035
+ function parseCp(args, cwd) {
2036
+ const paths = args.filter((t) => !isFlagLike(t));
2037
+ if (paths.length !== 2) return [];
2038
+ const [, dst] = paths;
2039
+ return [{ action: "reindex", path: absolutize(dst, cwd) }];
2040
+ }
2041
+ function handleBashPostTool(payload) {
2042
+ if (payload.tool_name !== "Bash") return { ops: [] };
2043
+ const cmd = payload.tool_input?.command;
2044
+ if (!cmd || typeof cmd !== "string") return { ops: [] };
2045
+ try {
2046
+ const ops = parseFileOps(cmd, payload.cwd);
2047
+ return { ops };
2048
+ } catch {
2049
+ return { ops: [] };
2050
+ }
2051
+ }
2052
+
2053
+ // src/watcher.ts
2054
+ import { watch, existsSync as existsSync6, statSync as statSync2 } from "fs";
2055
+ import { resolve as resolve3, relative as relative3, extname } from "path";
2056
+ var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2057
+ ".ts",
2058
+ ".tsx",
2059
+ ".js",
2060
+ ".jsx",
2061
+ ".py",
2062
+ ".go",
2063
+ ".rs",
2064
+ ".java",
2065
+ ".c",
2066
+ ".cpp",
2067
+ ".cs",
2068
+ ".rb"
2069
+ ]);
2070
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2071
+ ".engram",
2072
+ "node_modules",
2073
+ ".git",
2074
+ "dist",
2075
+ "build",
2076
+ ".next",
2077
+ "__pycache__",
2078
+ ".venv",
2079
+ "target",
2080
+ "vendor"
2081
+ ]);
2082
+ var DEBOUNCE_MS = 300;
2083
+ function shouldIgnore(relPath) {
2084
+ const parts = relPath.split(/[/\\]/);
2085
+ return parts.some((p) => IGNORED_DIRS.has(p));
2086
+ }
2087
+ async function syncFile(absPath, projectRoot) {
2088
+ const ext = extname(absPath).toLowerCase();
2089
+ if (!WATCHABLE_EXTENSIONS.has(ext)) return { action: "skipped", count: 0 };
2090
+ const relPath = toPosixPath(relative3(projectRoot, absPath));
2091
+ if (shouldIgnore(relPath)) return { action: "skipped", count: 0 };
2092
+ if (!existsSync6(absPath)) {
2093
+ const store2 = await getStore(projectRoot);
2094
+ try {
2095
+ const prior = store2.countBySourceFile(relPath);
2096
+ if (prior === 0) return { action: "skipped", count: 0 };
2097
+ store2.deleteBySourceFile(relPath);
2098
+ return { action: "pruned", count: prior };
2099
+ } finally {
2100
+ store2.close();
2101
+ }
2102
+ }
2103
+ try {
2104
+ if (statSync2(absPath).isDirectory()) return { action: "skipped", count: 0 };
2105
+ } catch {
2106
+ return { action: "skipped", count: 0 };
2107
+ }
2108
+ const store = await getStore(projectRoot);
2109
+ try {
2110
+ store.deleteBySourceFile(relPath);
2111
+ const { nodes, edges } = extractFile(absPath, projectRoot);
2112
+ if (nodes.length > 0 || edges.length > 0) {
2113
+ store.bulkUpsert(nodes, edges);
2114
+ }
2115
+ return { action: "indexed", count: nodes.length };
2116
+ } finally {
2117
+ store.close();
2118
+ }
2119
+ }
2120
+ function formatReindexLine(result, displayPath) {
2121
+ if (result.action === "indexed") {
2122
+ return `engram: reindexed ${displayPath} (${formatThousands(result.count)} nodes)`;
2123
+ }
2124
+ if (result.action === "pruned") {
2125
+ return `engram: pruned ${displayPath} (${formatThousands(result.count)} nodes)`;
2126
+ }
2127
+ return null;
2128
+ }
2129
+ async function runReindexHook(payload) {
2130
+ try {
2131
+ if (payload === null || typeof payload !== "object") return;
2132
+ const p = payload;
2133
+ const cwd = p.cwd;
2134
+ if (typeof cwd !== "string" || !isValidCwd(cwd)) return;
2135
+ const toolInput = p.tool_input;
2136
+ if (toolInput === null || typeof toolInput !== "object") return;
2137
+ const filePath = toolInput.file_path;
2138
+ if (typeof filePath !== "string" || filePath.length === 0) return;
2139
+ const absPath = resolve3(cwd, filePath);
2140
+ const projectRoot = findProjectRoot(absPath);
2141
+ if (projectRoot === null) return;
2142
+ await syncFile(absPath, projectRoot);
2143
+ } catch {
2144
+ }
2145
+ }
2146
+ function watchProject(projectRoot, options = {}) {
2147
+ const root = resolve3(projectRoot);
2148
+ const controller = new AbortController();
2149
+ if (!existsSync6(getDbPath(root))) {
2150
+ throw new Error(
2151
+ `engram: no graph found at ${root}. Run 'engram init' first.`
2152
+ );
2153
+ }
2154
+ const debounceTimers = /* @__PURE__ */ new Map();
2155
+ const watcher = watch(root, { recursive: true, signal: controller.signal });
2156
+ const handleEvent = (_eventType, filename) => {
2157
+ if (typeof filename !== "string") return;
2158
+ const absPath = resolve3(root, filename);
2159
+ const relPath = toPosixPath(relative3(root, absPath));
2160
+ if (shouldIgnore(relPath)) return;
2161
+ const ext = extname(filename).toLowerCase();
2162
+ if (!WATCHABLE_EXTENSIONS.has(ext)) return;
2163
+ const existing = debounceTimers.get(absPath);
2164
+ if (existing) clearTimeout(existing);
2165
+ debounceTimers.set(
2166
+ absPath,
2167
+ setTimeout(async () => {
2168
+ debounceTimers.delete(absPath);
2169
+ try {
2170
+ const result = await syncFile(absPath, root);
2171
+ if (result.action === "indexed" && result.count > 0) {
2172
+ options.onReindex?.(relPath, result.count);
2173
+ } else if (result.action === "pruned") {
2174
+ options.onDelete?.(relPath, result.count);
2175
+ }
2176
+ } catch (err) {
2177
+ options.onError?.(
2178
+ err instanceof Error ? err : new Error(String(err))
2179
+ );
2180
+ }
2181
+ }, DEBOUNCE_MS)
2182
+ );
2183
+ };
2184
+ watcher.on("change", handleEvent);
2185
+ watcher.on("rename", handleEvent);
2186
+ watcher.on("error", (err) => {
2187
+ options.onError?.(err instanceof Error ? err : new Error(String(err)));
2188
+ });
2189
+ options.onReady?.();
2190
+ return controller;
2191
+ }
2192
+
1972
2193
  // src/intercept/handlers/post-tool.ts
1973
2194
  function extractFilePath(toolName, toolInput) {
1974
2195
  if (!toolInput) return void 0;
@@ -2019,13 +2240,34 @@ async function handlePostTool(payload) {
2019
2240
  outputSize,
2020
2241
  success: !hasError
2021
2242
  });
2243
+ if (toolName === "Bash" && !hasError && process.env.ENGRAM_AUTO_REINDEX === "1") {
2244
+ void reindexBashOps(payload, projectRoot).catch(() => {
2245
+ });
2246
+ }
2022
2247
  } catch {
2023
2248
  }
2024
2249
  return PASSTHROUGH;
2025
2250
  }
2251
+ async function reindexBashOps(payload, projectRoot) {
2252
+ const result = handleBashPostTool({
2253
+ tool_name: payload.tool_name ?? "",
2254
+ tool_input: payload.tool_input ?? {},
2255
+ cwd: payload.cwd
2256
+ });
2257
+ if (result.ops.length === 0) return;
2258
+ for (const op of result.ops) {
2259
+ await runOp(op, projectRoot);
2260
+ }
2261
+ }
2262
+ async function runOp(op, projectRoot) {
2263
+ try {
2264
+ await syncFile(op.path, projectRoot);
2265
+ } catch {
2266
+ }
2267
+ }
2026
2268
 
2027
2269
  // src/intercept/handlers/pre-compact.ts
2028
- import { basename as basename2, resolve as resolve3 } from "path";
2270
+ import { basename as basename2, resolve as resolve4 } from "path";
2029
2271
  var MAX_GOD_NODES_COMPACT = 5;
2030
2272
  var MAX_LANDMINES_COMPACT = 3;
2031
2273
  function formatCompactBrief(args) {
@@ -2075,7 +2317,7 @@ async function handlePreCompact(payload) {
2075
2317
  }))
2076
2318
  ]);
2077
2319
  if (graphStats.nodes === 0 && gods.length === 0) return PASSTHROUGH;
2078
- const projectName = basename2(resolve3(projectRoot));
2320
+ const projectName = basename2(resolve4(projectRoot));
2079
2321
  const text = formatCompactBrief({
2080
2322
  projectName,
2081
2323
  nodeCount: graphStats.nodes,
@@ -2097,7 +2339,7 @@ async function handlePreCompact(payload) {
2097
2339
  }
2098
2340
 
2099
2341
  // src/intercept/handlers/cwd-changed.ts
2100
- import { basename as basename3, resolve as resolve4 } from "path";
2342
+ import { basename as basename3, resolve as resolve5 } from "path";
2101
2343
  var MAX_GOD_NODES_SWITCH = 5;
2102
2344
  async function handleCwdChanged(payload) {
2103
2345
  if (payload.hook_event_name !== "CwdChanged") return PASSTHROUGH;
@@ -2121,7 +2363,7 @@ async function handleCwdChanged(payload) {
2121
2363
  }))
2122
2364
  ]);
2123
2365
  if (graphStats.nodes === 0) return PASSTHROUGH;
2124
- const projectName = basename3(resolve4(projectRoot));
2366
+ const projectName = basename3(resolve5(projectRoot));
2125
2367
  const lines = [];
2126
2368
  lines.push(
2127
2369
  `[engram] Project switched to ${projectName} (${graphStats.nodes} nodes, ${graphStats.edges} edges)`
@@ -2238,106 +2480,6 @@ function extractPreToolDecision(result) {
2238
2480
  return "passthrough";
2239
2481
  }
2240
2482
 
2241
- // src/watcher.ts
2242
- import { watch, existsSync as existsSync6, statSync as statSync2 } from "fs";
2243
- import { resolve as resolve5, relative as relative3, extname } from "path";
2244
- var WATCHABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2245
- ".ts",
2246
- ".tsx",
2247
- ".js",
2248
- ".jsx",
2249
- ".py",
2250
- ".go",
2251
- ".rs",
2252
- ".java",
2253
- ".c",
2254
- ".cpp",
2255
- ".cs",
2256
- ".rb"
2257
- ]);
2258
- var IGNORED_DIRS = /* @__PURE__ */ new Set([
2259
- ".engram",
2260
- "node_modules",
2261
- ".git",
2262
- "dist",
2263
- "build",
2264
- ".next",
2265
- "__pycache__",
2266
- ".venv",
2267
- "target",
2268
- "vendor"
2269
- ]);
2270
- var DEBOUNCE_MS = 300;
2271
- function shouldIgnore(relPath) {
2272
- const parts = relPath.split(/[/\\]/);
2273
- return parts.some((p) => IGNORED_DIRS.has(p));
2274
- }
2275
- async function reindexFile(absPath, projectRoot) {
2276
- const ext = extname(absPath).toLowerCase();
2277
- if (!WATCHABLE_EXTENSIONS.has(ext)) return 0;
2278
- if (!existsSync6(absPath)) return 0;
2279
- try {
2280
- if (statSync2(absPath).isDirectory()) return 0;
2281
- } catch {
2282
- return 0;
2283
- }
2284
- const relPath = toPosixPath(relative3(projectRoot, absPath));
2285
- if (shouldIgnore(relPath)) return 0;
2286
- const store = await getStore(projectRoot);
2287
- try {
2288
- store.deleteBySourceFile(relPath);
2289
- const { nodes, edges } = extractFile(absPath, projectRoot);
2290
- if (nodes.length > 0 || edges.length > 0) {
2291
- store.bulkUpsert(nodes, edges);
2292
- }
2293
- return nodes.length;
2294
- } finally {
2295
- store.close();
2296
- }
2297
- }
2298
- function watchProject(projectRoot, options = {}) {
2299
- const root = resolve5(projectRoot);
2300
- const controller = new AbortController();
2301
- if (!existsSync6(getDbPath(root))) {
2302
- throw new Error(
2303
- `engram: no graph found at ${root}. Run 'engram init' first.`
2304
- );
2305
- }
2306
- const debounceTimers = /* @__PURE__ */ new Map();
2307
- const watcher = watch(root, { recursive: true, signal: controller.signal });
2308
- watcher.on("change", (_eventType, filename) => {
2309
- if (typeof filename !== "string") return;
2310
- const absPath = resolve5(root, filename);
2311
- const relPath = toPosixPath(relative3(root, absPath));
2312
- if (shouldIgnore(relPath)) return;
2313
- const ext = extname(filename).toLowerCase();
2314
- if (!WATCHABLE_EXTENSIONS.has(ext)) return;
2315
- const existing = debounceTimers.get(absPath);
2316
- if (existing) clearTimeout(existing);
2317
- debounceTimers.set(
2318
- absPath,
2319
- setTimeout(async () => {
2320
- debounceTimers.delete(absPath);
2321
- try {
2322
- const count = await reindexFile(absPath, root);
2323
- if (count > 0) {
2324
- options.onReindex?.(relPath, count);
2325
- }
2326
- } catch (err) {
2327
- options.onError?.(
2328
- err instanceof Error ? err : new Error(String(err))
2329
- );
2330
- }
2331
- }, DEBOUNCE_MS)
2332
- );
2333
- });
2334
- watcher.on("error", (err) => {
2335
- options.onError?.(err instanceof Error ? err : new Error(String(err)));
2336
- });
2337
- options.onReady?.();
2338
- return controller;
2339
- }
2340
-
2341
2483
  // src/dashboard.ts
2342
2484
  import chalk from "chalk";
2343
2485
  import { existsSync as existsSync7, statSync as statSync3 } from "fs";
@@ -2353,9 +2495,7 @@ function bar(pct, width = 20) {
2353
2495
  const empty = width - filled;
2354
2496
  return AMBER("\u2588".repeat(filled)) + DIM("\u2591".repeat(empty));
2355
2497
  }
2356
- function fmt(n) {
2357
- return n.toLocaleString();
2358
- }
2498
+ var fmt = formatThousands;
2359
2499
  function topFiles(entries, n) {
2360
2500
  const counts = /* @__PURE__ */ new Map();
2361
2501
  for (const e of entries) {
@@ -2522,156 +2662,6 @@ async function handleCursorBeforeReadFile(payload) {
2522
2662
  }
2523
2663
  }
2524
2664
 
2525
- // src/intercept/installer.ts
2526
- var ENGRAM_HOOK_EVENTS = [
2527
- "PreToolUse",
2528
- "PostToolUse",
2529
- "SessionStart",
2530
- "UserPromptSubmit",
2531
- "PreCompact",
2532
- "CwdChanged"
2533
- ];
2534
- var ENGRAM_PRETOOL_MATCHER = "Read|Edit|Write|Bash";
2535
- var DEFAULT_ENGRAM_COMMAND = "engram intercept";
2536
- var DEFAULT_HOOK_TIMEOUT_SEC = 5;
2537
- var DEFAULT_STATUSLINE_COMMAND = "engram hud-label";
2538
- function buildEngramHookEntries(command = DEFAULT_ENGRAM_COMMAND, timeout = DEFAULT_HOOK_TIMEOUT_SEC) {
2539
- const baseCmd = {
2540
- type: "command",
2541
- command,
2542
- timeout
2543
- };
2544
- return {
2545
- PreToolUse: {
2546
- matcher: ENGRAM_PRETOOL_MATCHER,
2547
- hooks: [baseCmd]
2548
- },
2549
- PostToolUse: {
2550
- // Match all tools — PostToolUse is an observer for any completion.
2551
- matcher: ".*",
2552
- hooks: [baseCmd]
2553
- },
2554
- SessionStart: {
2555
- // No matcher — SessionStart has no tool name.
2556
- hooks: [baseCmd]
2557
- },
2558
- UserPromptSubmit: {
2559
- // No matcher — UserPromptSubmit has no tool name.
2560
- hooks: [baseCmd]
2561
- },
2562
- PreCompact: {
2563
- // No matcher — PreCompact has no tool name.
2564
- hooks: [baseCmd]
2565
- },
2566
- CwdChanged: {
2567
- // No matcher — CwdChanged has no tool name.
2568
- hooks: [baseCmd]
2569
- }
2570
- };
2571
- }
2572
- function isEngramHookEntry(entry) {
2573
- if (entry === null || typeof entry !== "object") return false;
2574
- const e = entry;
2575
- if (!Array.isArray(e.hooks)) return false;
2576
- for (const h of e.hooks) {
2577
- if (h === null || typeof h !== "object") continue;
2578
- const cmd = h.command;
2579
- if (typeof cmd === "string" && cmd.includes("engram intercept")) {
2580
- return true;
2581
- }
2582
- }
2583
- return false;
2584
- }
2585
- function installEngramHooks(settings, command = DEFAULT_ENGRAM_COMMAND) {
2586
- const entries = buildEngramHookEntries(command);
2587
- const added = [];
2588
- const alreadyPresent = [];
2589
- const hooksClone = {};
2590
- const existingHooks = settings.hooks ?? {};
2591
- for (const [key, value] of Object.entries(existingHooks)) {
2592
- if (Array.isArray(value)) {
2593
- hooksClone[key] = value.map((entry) => ({ ...entry }));
2594
- }
2595
- }
2596
- for (const event of ENGRAM_HOOK_EVENTS) {
2597
- const eventArr = hooksClone[event] ?? [];
2598
- const hasEngram = eventArr.some((e) => isEngramHookEntry(e));
2599
- if (hasEngram) {
2600
- alreadyPresent.push(event);
2601
- hooksClone[event] = eventArr;
2602
- continue;
2603
- }
2604
- hooksClone[event] = [...eventArr, entries[event]];
2605
- added.push(event);
2606
- }
2607
- const hasStatusLine = settings.statusLine && typeof settings.statusLine === "object" && typeof settings.statusLine.command === "string" && settings.statusLine.command.length > 0;
2608
- const statusLineAdded = !hasStatusLine;
2609
- const statusLine = hasStatusLine ? settings.statusLine : { type: "command", command: DEFAULT_STATUSLINE_COMMAND };
2610
- return {
2611
- updated: { ...settings, hooks: hooksClone, statusLine },
2612
- added,
2613
- alreadyPresent,
2614
- statusLineAdded
2615
- };
2616
- }
2617
- function uninstallEngramHooks(settings) {
2618
- const removed = [];
2619
- const existingHooks = settings.hooks ?? {};
2620
- const hooksClone = {};
2621
- for (const [event, arr] of Object.entries(existingHooks)) {
2622
- if (!Array.isArray(arr)) continue;
2623
- const filtered = arr.filter((entry) => !isEngramHookEntry(entry));
2624
- if (filtered.length !== arr.length && isKnownEngramEvent(event)) {
2625
- removed.push(event);
2626
- }
2627
- if (filtered.length > 0) {
2628
- hooksClone[event] = filtered;
2629
- }
2630
- }
2631
- const updatedSettings = { ...settings };
2632
- if (Object.keys(hooksClone).length === 0) {
2633
- delete updatedSettings.hooks;
2634
- } else {
2635
- updatedSettings.hooks = hooksClone;
2636
- }
2637
- const statusLineRemoved = typeof updatedSettings.statusLine?.command === "string" && updatedSettings.statusLine.command.includes("engram hud-label");
2638
- if (statusLineRemoved) {
2639
- delete updatedSettings.statusLine;
2640
- }
2641
- return { updated: updatedSettings, removed, statusLineRemoved };
2642
- }
2643
- function isKnownEngramEvent(event) {
2644
- return ENGRAM_HOOK_EVENTS.includes(event);
2645
- }
2646
- function formatInstallDiff(before, after) {
2647
- const lines = [];
2648
- const beforeHooks = before.hooks ?? {};
2649
- const afterHooks = after.hooks ?? {};
2650
- for (const event of ENGRAM_HOOK_EVENTS) {
2651
- const beforeArr = beforeHooks[event] ?? [];
2652
- const afterArr = afterHooks[event] ?? [];
2653
- if (beforeArr.length === afterArr.length) continue;
2654
- lines.push(`+ ${event}: ${beforeArr.length} \u2192 ${afterArr.length} entries`);
2655
- const added = afterArr.filter((entry) => isEngramHookEntry(entry));
2656
- const beforeHasEngram = beforeArr.some((entry) => isEngramHookEntry(entry));
2657
- if (!beforeHasEngram && added.length > 0) {
2658
- for (const entry of added) {
2659
- const matcher = entry.matcher ? ` matcher=${JSON.stringify(entry.matcher)}` : "";
2660
- const cmds = entry.hooks.map((h) => h.command).join(", ");
2661
- lines.push(` + {${matcher} command="${cmds}"}`);
2662
- }
2663
- }
2664
- }
2665
- const hadStatusLine = before.statusLine?.command;
2666
- const hasStatusLineNow = after.statusLine?.command;
2667
- if (!hadStatusLine && hasStatusLineNow?.includes("engram hud-label")) {
2668
- lines.push(`+ statusLine: engram hud-label (HUD enabled)`);
2669
- } else if (hadStatusLine?.includes("engram hud-label") && !hasStatusLineNow) {
2670
- lines.push(`- statusLine: engram hud-label (HUD removed)`);
2671
- }
2672
- return lines.length > 0 ? lines.join("\n") : "(no changes)";
2673
- }
2674
-
2675
2665
  // src/intercept/memory-md.ts
2676
2666
  import {
2677
2667
  existsSync as existsSync8,
@@ -2786,6 +2776,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2786
2776
  ).option("--from-ccs", "Import .context/index.md (CCS) into graph after init").option(
2787
2777
  "--incremental",
2788
2778
  "Skip unchanged files (mtime-based). Dramatically faster on re-index of large repos."
2779
+ ).option(
2780
+ "--with-hook",
2781
+ "Also install the Sentinel hook into Claude Code settings.local.json (idempotent)"
2789
2782
  ).action(async (projectPath, opts) => {
2790
2783
  console.log(chalk2.dim(opts.incremental ? "\u{1F50D} Scanning changed files..." : "\u{1F50D} Scanning codebase..."));
2791
2784
  const result = await init(projectPath, {
@@ -2796,7 +2789,7 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2796
2789
  chalk2.green("\u{1F333} AST extraction complete") + chalk2.dim(` (${result.timeMs}ms, 0 tokens used)`)
2797
2790
  );
2798
2791
  console.log(
2799
- ` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${result.totalLines.toLocaleString()} lines)`
2792
+ ` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${formatThousands(result.totalLines)} lines)`
2800
2793
  );
2801
2794
  if (result.incremental && result.skippedFiles && result.skippedFiles > 0) {
2802
2795
  console.log(chalk2.dim(` ${result.skippedFiles} unchanged files skipped (incremental mode)`));
@@ -2813,12 +2806,12 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2813
2806
  \u{1F4CA} Token savings: ${chalk2.bold(bench.reductionVsRelevant + "x")} fewer tokens vs relevant files (${bench.reductionVsFull}x vs full corpus)`)
2814
2807
  );
2815
2808
  console.log(
2816
- chalk2.dim(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens | Graph query: ~${bench.avgQueryTokens.toLocaleString()} tokens`)
2809
+ chalk2.dim(` Full corpus: ~${formatThousands(bench.naiveFullCorpus)} tokens | Graph query: ~${formatThousands(bench.avgQueryTokens)} tokens`)
2817
2810
  );
2818
2811
  }
2819
2812
  console.log(chalk2.green("\n\u2705 Ready. Your AI now has persistent memory."));
2820
2813
  console.log(chalk2.dim(" Graph stored in .engram/graph.db"));
2821
- const resolvedProject = pathResolve(projectPath);
2814
+ const resolvedProject = pathResolve2(projectPath);
2822
2815
  const localSettings = join9(resolvedProject, ".claude", "settings.local.json");
2823
2816
  const projectSettings = join9(resolvedProject, ".claude", "settings.json");
2824
2817
  const hasHooks = existsSync9(localSettings) && readFileSync5(localSettings, "utf-8").includes("engram intercept") || existsSync9(projectSettings) && readFileSync5(projectSettings, "utf-8").includes("engram intercept");
@@ -2834,9 +2827,59 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2834
2827
  )
2835
2828
  );
2836
2829
  }
2830
+ if (opts.withHook) {
2831
+ const localSettingsPath = join9(
2832
+ pathResolve2(projectPath),
2833
+ ".claude",
2834
+ "settings.local.json"
2835
+ );
2836
+ let settings = {};
2837
+ if (existsSync9(localSettingsPath)) {
2838
+ try {
2839
+ const raw = readFileSync5(localSettingsPath, "utf-8");
2840
+ settings = raw.trim() ? JSON.parse(raw) : {};
2841
+ } catch {
2842
+ console.log(
2843
+ chalk2.yellow(
2844
+ "\n \u26A0 --with-hook: settings.local.json is invalid JSON, skipping hook install."
2845
+ )
2846
+ );
2847
+ settings = {};
2848
+ }
2849
+ }
2850
+ const hookResult = installEngramHooks(settings);
2851
+ if (hookResult.added.length > 0 || hookResult.statusLineAdded) {
2852
+ try {
2853
+ mkdirSync(dirname4(localSettingsPath), { recursive: true });
2854
+ writeFileSync2(
2855
+ localSettingsPath,
2856
+ JSON.stringify(hookResult.updated, null, 2) + "\n"
2857
+ );
2858
+ console.log(
2859
+ chalk2.green(
2860
+ `
2861
+ \u2705 --with-hook: installed ${hookResult.added.length} hook event${hookResult.added.length === 1 ? "" : "s"} into .claude/settings.local.json`
2862
+ )
2863
+ );
2864
+ } catch (err) {
2865
+ console.log(
2866
+ chalk2.yellow(
2867
+ `
2868
+ \u26A0 --with-hook: write failed (${err.message})`
2869
+ )
2870
+ );
2871
+ }
2872
+ } else {
2873
+ console.log(
2874
+ chalk2.dim(
2875
+ "\n --with-hook: Sentinel hook already installed, nothing to do."
2876
+ )
2877
+ );
2878
+ }
2879
+ }
2837
2880
  if (opts.fromCcs) {
2838
- const { importCcs } = await import("./importer-V62NGZRK.js");
2839
- const resolvedProjectPath = pathResolve(projectPath);
2881
+ const { importCcs } = await import("./importer-3Q5M6QBL.js");
2882
+ const resolvedProjectPath = pathResolve2(projectPath);
2840
2883
  const ccsResult = await importCcs(resolvedProjectPath);
2841
2884
  if (ccsResult.nodesCreated > 0) {
2842
2885
  console.log(
@@ -2850,7 +2893,7 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2850
2893
  }
2851
2894
  });
2852
2895
  program.command("watch").description("Watch project for file changes and re-index incrementally").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2853
- const resolvedPath = pathResolve(projectPath);
2896
+ const resolvedPath = pathResolve2(projectPath);
2854
2897
  console.log(
2855
2898
  chalk2.dim("\u{1F441} Watching ") + chalk2.white(resolvedPath) + chalk2.dim(" for changes...")
2856
2899
  );
@@ -2860,6 +2903,11 @@ program.command("watch").description("Watch project for file changes and re-inde
2860
2903
  chalk2.green(" \u21BB ") + chalk2.white(filePath) + chalk2.dim(` (${nodeCount} nodes)`)
2861
2904
  );
2862
2905
  },
2906
+ onDelete: (filePath, prunedCount) => {
2907
+ console.log(
2908
+ chalk2.yellow(" \xD7 ") + chalk2.white(filePath) + chalk2.dim(` pruned (${prunedCount} nodes)`)
2909
+ );
2910
+ },
2863
2911
  onError: (err) => {
2864
2912
  console.error(chalk2.red(" \u2717 ") + err.message);
2865
2913
  },
@@ -2875,8 +2923,68 @@ program.command("watch").description("Watch project for file changes and re-inde
2875
2923
  await new Promise(() => {
2876
2924
  });
2877
2925
  });
2926
+ program.command("reindex").description("Re-index a single file into the knowledge graph").argument("<file>", "File path (absolute or relative to --project)").option("-p, --project <path>", "Project directory", ".").option("--verbose", "Print stack traces on error", false).action(
2927
+ async (file, opts) => {
2928
+ const root = pathResolve2(opts.project);
2929
+ if (!existsSync9(join9(root, ".engram", "graph.db"))) {
2930
+ console.error(
2931
+ `engram: no graph found at ${root}. Run 'engram init' first.`
2932
+ );
2933
+ process.exit(1);
2934
+ }
2935
+ const absFile = pathResolve2(root, file);
2936
+ try {
2937
+ const result = await syncFile(absFile, root);
2938
+ const line = formatReindexLine(result, file);
2939
+ if (line !== null) console.log(line);
2940
+ process.exitCode = 0;
2941
+ } catch (err) {
2942
+ const msg = err instanceof Error ? err.message : String(err);
2943
+ console.error(`engram: ${msg}`);
2944
+ if (opts.verbose && err instanceof Error && err.stack) {
2945
+ console.error(err.stack);
2946
+ }
2947
+ process.exit(1);
2948
+ }
2949
+ }
2950
+ );
2951
+ program.command("reindex-hook").description(
2952
+ "PostToolUse hook entry point: reads JSON from stdin, reindexes tool_input.file_path (always exits 0)"
2953
+ ).action(async () => {
2954
+ const stdinTimeout = setTimeout(() => {
2955
+ process.exit(0);
2956
+ }, 3e3);
2957
+ stdinTimeout.unref();
2958
+ let input = "";
2959
+ let stdinFailed = false;
2960
+ try {
2961
+ for await (const chunk of process.stdin) {
2962
+ input += chunk;
2963
+ if (input.length > 1e6) break;
2964
+ }
2965
+ } catch {
2966
+ stdinFailed = true;
2967
+ }
2968
+ clearTimeout(stdinTimeout);
2969
+ if (stdinFailed || !input.trim()) {
2970
+ process.exitCode = 0;
2971
+ return;
2972
+ }
2973
+ let payload;
2974
+ try {
2975
+ payload = JSON.parse(input);
2976
+ } catch {
2977
+ process.exitCode = 0;
2978
+ return;
2979
+ }
2980
+ try {
2981
+ await runReindexHook(payload);
2982
+ } catch {
2983
+ }
2984
+ process.exitCode = 0;
2985
+ });
2878
2986
  program.command("dashboard").alias("hud").description("Live terminal dashboard showing hook activity and token savings").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2879
- const resolvedPath = pathResolve(projectPath);
2987
+ const resolvedPath = pathResolve2(projectPath);
2880
2988
  const dbPath = join9(resolvedPath, ".engram", "graph.db");
2881
2989
  if (!existsSync9(dbPath)) {
2882
2990
  console.error(
@@ -2895,7 +3003,7 @@ program.command("dashboard").alias("hud").description("Live terminal dashboard s
2895
3003
  });
2896
3004
  });
2897
3005
  program.command("hud-label").description("Output JSON label for Claude HUD --extra-cmd (fast, <20ms)").argument("[path]", "Project directory", ".").action(async (projectPath) => {
2898
- let resolvedPath = pathResolve(projectPath);
3006
+ let resolvedPath = pathResolve2(projectPath);
2899
3007
  let found = false;
2900
3008
  for (let depth = 0; depth < 20; depth++) {
2901
3009
  if (existsSync9(join9(resolvedPath, ".engram", "graph.db"))) {
@@ -2991,8 +3099,8 @@ program.command("stats").description("Show knowledge graph statistics and token
2991
3099
  if (bench.naiveFullCorpus > 0) {
2992
3100
  console.log(`
2993
3101
  ${chalk2.cyan("Token savings:")}`);
2994
- console.log(` Full corpus: ~${bench.naiveFullCorpus.toLocaleString()} tokens`);
2995
- console.log(` Avg query: ~${bench.avgQueryTokens.toLocaleString()} tokens`);
3102
+ console.log(` Full corpus: ~${formatThousands(bench.naiveFullCorpus)} tokens`);
3103
+ console.log(` Avg query: ~${formatThousands(bench.avgQueryTokens)} tokens`);
2996
3104
  console.log(` vs relevant: ${chalk2.bold.cyan(bench.reductionVsRelevant + "x")} fewer tokens`);
2997
3105
  console.log(` vs full: ${chalk2.bold.cyan(bench.reductionVsFull + "x")} fewer tokens`);
2998
3106
  }
@@ -3036,8 +3144,8 @@ program.command("mistakes").description("List known mistakes extracted from past
3036
3144
  program.command("bench").description("Run token reduction benchmark").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3037
3145
  const result = await benchmark(opts.project);
3038
3146
  console.log(chalk2.bold("\n\u26A1 engram token reduction benchmark\n"));
3039
- console.log(` Full corpus: ~${result.naiveFullCorpus.toLocaleString()} tokens`);
3040
- console.log(` Avg graph query: ~${result.avgQueryTokens.toLocaleString()} tokens`);
3147
+ console.log(` Full corpus: ~${formatThousands(result.naiveFullCorpus)} tokens`);
3148
+ console.log(` Avg graph query: ~${formatThousands(result.avgQueryTokens)} tokens`);
3041
3149
  console.log(` vs relevant: ${chalk2.bold.green(result.reductionVsRelevant + "x")} fewer tokens`);
3042
3150
  console.log(` vs full corpus: ${chalk2.bold.green(result.reductionVsFull + "x")} fewer tokens
3043
3151
  `);
@@ -3065,7 +3173,7 @@ program.command("gen").description("Generate CLAUDE.md / .cursorrules section fr
3065
3173
  }
3066
3174
  );
3067
3175
  program.command("gen-mdc").description("Generate .cursor/rules/engram-context.mdc from knowledge graph").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3068
- const { generateCursorMdc } = await import("./cursor-mdc-GJ7E5LDD.js");
3176
+ const { generateCursorMdc } = await import("./cursor-mdc-VEOFFDVO.js");
3069
3177
  const result = await generateCursorMdc(opts.project);
3070
3178
  console.log(
3071
3179
  chalk2.green(
@@ -3073,11 +3181,15 @@ program.command("gen-mdc").description("Generate .cursor/rules/engram-context.md
3073
3181
  )
3074
3182
  );
3075
3183
  if (opts.watch) {
3076
- watchProject(pathResolve(opts.project), {
3184
+ watchProject(pathResolve2(opts.project), {
3077
3185
  onReindex: async () => {
3078
3186
  const r = await generateCursorMdc(opts.project);
3079
3187
  console.log(chalk2.dim(` \u21BB Regenerated MDC (${r.nodes} nodes)`));
3080
3188
  },
3189
+ onDelete: async () => {
3190
+ const r = await generateCursorMdc(opts.project);
3191
+ console.log(chalk2.dim(` \xD7 Regenerated MDC (${r.nodes} nodes)`));
3192
+ },
3081
3193
  onError: (err) => console.error(chalk2.red(err.message)),
3082
3194
  onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3083
3195
  });
@@ -3086,8 +3198,8 @@ program.command("gen-mdc").description("Generate .cursor/rules/engram-context.md
3086
3198
  }
3087
3199
  });
3088
3200
  program.command("gen-ccs").description("Export knowledge graph as .context/index.md (CCS format)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3089
- const { exportCcs } = await import("./exporter-GWU2GF23.js");
3090
- const result = await exportCcs(pathResolve(opts.project));
3201
+ const { exportCcs } = await import("./exporter-AWXS34AS.js");
3202
+ const result = await exportCcs(pathResolve2(opts.project));
3091
3203
  console.log(
3092
3204
  chalk2.green(
3093
3205
  `\u2705 Generated ${result.filePath} (${result.sectionsWritten} sections, ${result.nodesExported} nodes)`
@@ -3095,19 +3207,23 @@ program.command("gen-ccs").description("Export knowledge graph as .context/index
3095
3207
  );
3096
3208
  });
3097
3209
  program.command("gen-aider").description("Generate .aider-context.md from knowledge graph").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3098
- const { generateAiderContext } = await import("./aider-context-BC5R2ZTA.js");
3099
- const result = await generateAiderContext(pathResolve(opts.project));
3210
+ const { generateAiderContext } = await import("./aider-context-J557IHIP.js");
3211
+ const result = await generateAiderContext(pathResolve2(opts.project));
3100
3212
  console.log(
3101
3213
  chalk2.green(
3102
3214
  `\u2705 Generated ${result.filePath} (${result.sections} sections, ${result.nodes} nodes)`
3103
3215
  )
3104
3216
  );
3105
3217
  if (opts.watch) {
3106
- watchProject(pathResolve(opts.project), {
3218
+ watchProject(pathResolve2(opts.project), {
3107
3219
  onReindex: async () => {
3108
3220
  const r = await generateAiderContext(opts.project);
3109
3221
  console.log(chalk2.dim(` \u21BB Regenerated .aider-context.md (${r.nodes} nodes)`));
3110
3222
  },
3223
+ onDelete: async () => {
3224
+ const r = await generateAiderContext(opts.project);
3225
+ console.log(chalk2.dim(` \xD7 Regenerated .aider-context.md (${r.nodes} nodes)`));
3226
+ },
3111
3227
  onError: (err) => console.error(chalk2.red(err.message)),
3112
3228
  onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3113
3229
  });
@@ -3116,19 +3232,23 @@ program.command("gen-aider").description("Generate .aider-context.md from knowle
3116
3232
  }
3117
3233
  });
3118
3234
  program.command("gen-windsurfrules").description("Generate .windsurfrules from knowledge graph (Windsurf IDE)").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3119
- const { generateWindsurfRules } = await import("./windsurf-rules-C7SVDHBL.js");
3120
- const result = await generateWindsurfRules(pathResolve(opts.project));
3235
+ const { generateWindsurfRules } = await import("./windsurf-rules-RWPKBHRD.js");
3236
+ const result = await generateWindsurfRules(pathResolve2(opts.project));
3121
3237
  console.log(
3122
3238
  chalk2.green(
3123
3239
  `\u2705 Generated ${result.filePath} (${result.sections} sections, ${result.nodes} nodes)`
3124
3240
  )
3125
3241
  );
3126
3242
  if (opts.watch) {
3127
- watchProject(pathResolve(opts.project), {
3243
+ watchProject(pathResolve2(opts.project), {
3128
3244
  onReindex: async () => {
3129
3245
  const r = await generateWindsurfRules(opts.project);
3130
3246
  console.log(chalk2.dim(` \u21BB Regenerated .windsurfrules (${r.nodes} nodes)`));
3131
3247
  },
3248
+ onDelete: async () => {
3249
+ const r = await generateWindsurfRules(opts.project);
3250
+ console.log(chalk2.dim(` \xD7 Regenerated .windsurfrules (${r.nodes} nodes)`));
3251
+ },
3132
3252
  onError: (err) => console.error(chalk2.red(err.message)),
3133
3253
  onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3134
3254
  });
@@ -3137,7 +3257,7 @@ program.command("gen-windsurfrules").description("Generate .windsurfrules from k
3137
3257
  }
3138
3258
  });
3139
3259
  function resolveSettingsPath(scope, projectPath) {
3140
- const absProject = pathResolve(projectPath);
3260
+ const absProject = pathResolve2(projectPath);
3141
3261
  switch (scope) {
3142
3262
  case "local":
3143
3263
  return join9(absProject, ".claude", "settings.local.json");
@@ -3226,7 +3346,11 @@ program.command("cursor-intercept").description(
3226
3346
  }
3227
3347
  process.exit(0);
3228
3348
  });
3229
- program.command("install-hook").description("Install engram hook entries into Claude Code settings").option("--scope <scope>", "local | project | user", "local").option("--dry-run", "Show diff without writing", false).option("-p, --project <path>", "Project directory", ".").action(
3349
+ program.command("install-hook").description("Install engram hook entries into Claude Code settings").option("--scope <scope>", "local | project | user", "local").option("--dry-run", "Show diff without writing", false).option("-p, --project <path>", "Project directory", ".").option(
3350
+ "--auto-reindex",
3351
+ "Also register a PostToolUse Edit|Write|MultiEdit entry calling 'engram reindex-hook' (keeps graph fresh after every edit, #8)",
3352
+ false
3353
+ ).action(
3230
3354
  async (opts) => {
3231
3355
  const settingsPath = resolveSettingsPath(opts.scope, opts.project);
3232
3356
  if (!settingsPath) {
@@ -3256,13 +3380,20 @@ program.command("install-hook").description("Install engram hook entries into Cl
3256
3380
  process.exit(1);
3257
3381
  }
3258
3382
  }
3259
- const result = installEngramHooks(existing);
3383
+ const result = installEngramHooks(existing, void 0, {
3384
+ autoReindex: opts.autoReindex
3385
+ });
3260
3386
  console.log(
3261
3387
  chalk2.bold(`
3262
3388
  \u{1F4CC} engram install-hook (scope: ${opts.scope})`)
3263
3389
  );
3264
3390
  console.log(chalk2.dim(` Target: ${settingsPath}`));
3265
- if (result.added.length === 0 && !result.statusLineAdded) {
3391
+ if (opts.autoReindex) {
3392
+ console.log(
3393
+ chalk2.dim(" Auto-reindex: enabled (engram reindex-hook)")
3394
+ );
3395
+ }
3396
+ if (result.added.length === 0 && !result.statusLineAdded && !result.autoReindexAdded) {
3266
3397
  console.log(
3267
3398
  chalk2.yellow(
3268
3399
  `
@@ -3317,6 +3448,13 @@ program.command("install-hook").description("Install engram hook entries into Cl
3317
3448
  chalk2.green(" \u2705 StatusLine: engram hud-label (HUD visible in Claude Code)")
3318
3449
  );
3319
3450
  }
3451
+ if (result.autoReindexAdded) {
3452
+ console.log(
3453
+ chalk2.green(
3454
+ " \u2705 PostToolUse: engram reindex-hook (matcher: Edit|Write|MultiEdit)"
3455
+ )
3456
+ );
3457
+ }
3320
3458
  if (result.alreadyPresent.length > 0) {
3321
3459
  console.log(
3322
3460
  chalk2.dim(
@@ -3390,7 +3528,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3390
3528
  }
3391
3529
  });
3392
3530
  program.command("hook-stats").description("Summarize hook-log.jsonl for a project").option("-p, --project <path>", "Project directory", ".").option("--json", "Output as JSON", false).action(async (opts) => {
3393
- const absProject = pathResolve(opts.project);
3531
+ const absProject = pathResolve2(opts.project);
3394
3532
  const projectRoot = findProjectRoot(absProject) ?? absProject;
3395
3533
  const entries = readHookLog(projectRoot);
3396
3534
  const summary = summarizeHookLog(entries);
@@ -3401,8 +3539,8 @@ program.command("hook-stats").description("Summarize hook-log.jsonl for a projec
3401
3539
  console.log(formatStatsSummary(summary));
3402
3540
  });
3403
3541
  program.command("hook-preview").description("Show what the Read handler would do for a file (dry-run)").argument("<file>", "Target file path").option("-p, --project <path>", "Project directory", ".").action(async (file, opts) => {
3404
- const absProject = pathResolve(opts.project);
3405
- const absFile = pathResolve(absProject, file);
3542
+ const absProject = pathResolve2(opts.project);
3543
+ const absFile = pathResolve2(absProject, file);
3406
3544
  const payload = {
3407
3545
  hook_event_name: "PreToolUse",
3408
3546
  tool_name: "Read",
@@ -3451,7 +3589,7 @@ program.command("hook-preview").description("Show what the Read handler would do
3451
3589
  console.log(chalk2.yellow(` Decision: ${decision ?? "unknown"}`));
3452
3590
  });
3453
3591
  program.command("hook-disable").description("Disable engram hooks via kill switch (does not uninstall)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3454
- const absProject = pathResolve(opts.project);
3592
+ const absProject = pathResolve2(opts.project);
3455
3593
  const projectRoot = findProjectRoot(absProject);
3456
3594
  if (!projectRoot) {
3457
3595
  console.error(
@@ -3478,7 +3616,7 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
3478
3616
  }
3479
3617
  });
3480
3618
  program.command("hook-enable").description("Re-enable engram hooks (remove kill switch flag)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3481
- const absProject = pathResolve(opts.project);
3619
+ const absProject = pathResolve2(opts.project);
3482
3620
  const projectRoot = findProjectRoot(absProject);
3483
3621
  if (!projectRoot) {
3484
3622
  console.error(chalk2.red(`Not an engram project: ${absProject}`));
@@ -3507,7 +3645,7 @@ program.command("memory-sync").description(
3507
3645
  "Write engram's structural facts into MEMORY.md (complementary to Anthropic Auto-Dream)"
3508
3646
  ).option("-p, --project <path>", "Project directory", ".").option("--dry-run", "Print what would be written without writing", false).action(
3509
3647
  async (opts) => {
3510
- const absProject = pathResolve(opts.project);
3648
+ const absProject = pathResolve2(opts.project);
3511
3649
  const projectRoot = findProjectRoot(absProject);
3512
3650
  if (!projectRoot) {
3513
3651
  console.error(
@@ -3605,13 +3743,13 @@ program.command("stress-test").description("Run stress tests: memory, concurrenc
3605
3743
  }
3606
3744
  });
3607
3745
  program.command("server").description("Start engram HTTP REST server (binds to 127.0.0.1 only)").option("--http", "Enable HTTP server (default)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3608
- const { startHttpServer } = await import("./server-KUG7U6SG.js");
3609
- await startHttpServer(pathResolve(opts.project), parseInt(opts.port, 10));
3746
+ const { startHttpServer } = await import("./server-A6MUVKQK.js");
3747
+ await startHttpServer(pathResolve2(opts.project), parseInt(opts.port, 10));
3610
3748
  });
3611
3749
  program.command("ui").description("Open the web dashboard (auto-starts HTTP server if needed)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").option("--no-open", "Don't launch browser, just print the URL").action(async (opts) => {
3612
3750
  const port = parseInt(opts.port, 10);
3613
3751
  const publicUrl = `http://127.0.0.1:${port}/ui`;
3614
- const projectRoot = pathResolve(opts.project);
3752
+ const projectRoot = pathResolve2(opts.project);
3615
3753
  const { existsSync: existsSync10, readFileSync: readFileSync6 } = await import("fs");
3616
3754
  const pidPath = join9(projectRoot, ".engram", "http-server.pid");
3617
3755
  let alreadyRunning = false;
@@ -3666,7 +3804,7 @@ program.command("context-server").description("Start Zed-compatible context serv
3666
3804
  });
3667
3805
  program.command("tune").description("Analyze hook-log and propose provider config changes").option("-p, --project <path>", "Project directory", ".").option("--dry-run", "Show proposed changes without applying (default)").option("--apply", "Apply proposed changes to .engram/config.json").action(async (opts) => {
3668
3806
  const { analyzeTuning, applyTuning } = await import("./tuner-KFNNGKG3.js");
3669
- const proposal = analyzeTuning(pathResolve(opts.project));
3807
+ const proposal = analyzeTuning(pathResolve2(opts.project));
3670
3808
  if (proposal.changes.length === 0) {
3671
3809
  console.log(
3672
3810
  chalk2.dim(
@@ -3688,7 +3826,7 @@ program.command("tune").description("Analyze hook-log and propose provider confi
3688
3826
  );
3689
3827
  }
3690
3828
  if (opts.apply) {
3691
- applyTuning(pathResolve(opts.project), proposal);
3829
+ applyTuning(pathResolve2(opts.project), proposal);
3692
3830
  console.log(chalk2.green("\n\u2705 Changes applied to .engram/config.json"));
3693
3831
  } else {
3694
3832
  console.log(chalk2.dim("\nRun with --apply to write these changes."));
@@ -3696,9 +3834,9 @@ program.command("tune").description("Analyze hook-log and propose provider confi
3696
3834
  });
3697
3835
  var dbCmd = program.command("db").description("Database management");
3698
3836
  dbCmd.command("status").description("Show schema version and migration status").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3699
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3837
+ const { getStore: getStore2 } = await import("./core-TSXA5XZH.js");
3700
3838
  const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-UKCO6BUU.js");
3701
- const store = await getStore2(pathResolve(opts.project));
3839
+ const store = await getStore2(pathResolve2(opts.project));
3702
3840
  try {
3703
3841
  const version = getSchemaVersion(store.db);
3704
3842
  const pending = CURRENT_SCHEMA_VERSION - version;
@@ -3713,11 +3851,11 @@ dbCmd.command("status").description("Show schema version and migration status").
3713
3851
  }
3714
3852
  });
3715
3853
  dbCmd.command("migrate").description("Run pending schema migrations").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3716
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3854
+ const { getStore: getStore2 } = await import("./core-TSXA5XZH.js");
3717
3855
  const { runMigrations } = await import("./migrate-UKCO6BUU.js");
3718
- const store = await getStore2(pathResolve(opts.project));
3856
+ const store = await getStore2(pathResolve2(opts.project));
3719
3857
  try {
3720
- const dbPath = join9(pathResolve(opts.project), ".engram", "graph.db");
3858
+ const dbPath = join9(pathResolve2(opts.project), ".engram", "graph.db");
3721
3859
  const result = runMigrations(
3722
3860
  store.db,
3723
3861
  dbPath
@@ -3748,11 +3886,11 @@ dbCmd.command("rollback").description("Roll back to an earlier schema version (D
3748
3886
  console.error(chalk2.red(`Invalid version: ${opts.to}`));
3749
3887
  process.exit(1);
3750
3888
  }
3751
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3889
+ const { getStore: getStore2 } = await import("./core-TSXA5XZH.js");
3752
3890
  const { rollback, getSchemaVersion } = await import("./migrate-UKCO6BUU.js");
3753
- const store = await getStore2(pathResolve(opts.project));
3891
+ const store = await getStore2(pathResolve2(opts.project));
3754
3892
  try {
3755
- const dbPath = join9(pathResolve(opts.project), ".engram", "graph.db");
3893
+ const dbPath = join9(pathResolve2(opts.project), ".engram", "graph.db");
3756
3894
  const current = getSchemaVersion(
3757
3895
  store.db
3758
3896
  );
@@ -3837,7 +3975,7 @@ pluginCmd.command("install").description("Install a plugin by copying its .mjs f
3837
3975
  const { basename: basename6 } = await import("path");
3838
3976
  const { getPluginsDir, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-STTGYIL5.js");
3839
3977
  const { pathToFileURL } = await import("url");
3840
- const absPath = pathResolve(file);
3978
+ const absPath = pathResolve2(file);
3841
3979
  if (!existsSync9(absPath)) {
3842
3980
  console.error(chalk2.red(`File not found: ${absPath}`));
3843
3981
  process.exit(1);
@@ -3885,9 +4023,9 @@ pluginCmd.command("remove").description("Remove an installed plugin by filename"
3885
4023
  });
3886
4024
  var cacheCmd = program.command("cache").description("Inspect and manage the context cache");
3887
4025
  cacheCmd.command("stats").description("Show cache hit rate, entries, and LRU sizes").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3888
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
4026
+ const { getStore: getStore2 } = await import("./core-TSXA5XZH.js");
3889
4027
  const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3890
- const store = await getStore2(pathResolve(opts.project));
4028
+ const store = await getStore2(pathResolve2(opts.project));
3891
4029
  try {
3892
4030
  ContextCache.ensureTables(store);
3893
4031
  const cache = getContextCache();
@@ -3919,9 +4057,9 @@ cacheCmd.command("stats").description("Show cache hit rate, entries, and LRU siz
3919
4057
  }
3920
4058
  });
3921
4059
  cacheCmd.command("clear").description("Flush all cache layers (query, pattern, hot files)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3922
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
4060
+ const { getStore: getStore2 } = await import("./core-TSXA5XZH.js");
3923
4061
  const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3924
- const store = await getStore2(pathResolve(opts.project));
4062
+ const store = await getStore2(pathResolve2(opts.project));
3925
4063
  try {
3926
4064
  ContextCache.ensureTables(store);
3927
4065
  const cache = getContextCache();
@@ -3938,14 +4076,14 @@ cacheCmd.command("clear").description("Flush all cache layers (query, pattern, h
3938
4076
  }
3939
4077
  });
3940
4078
  cacheCmd.command("warm").description("Pre-warm hot file cache from access frequency (top-N)").option("-p, --project <path>", "Project directory", ".").option("-n, --limit <n>", "Number of files to warm", "20").action(async (opts) => {
3941
- const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
4079
+ const { getStore: getStore2 } = await import("./core-TSXA5XZH.js");
3942
4080
  const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3943
- const store = await getStore2(pathResolve(opts.project));
4081
+ const store = await getStore2(pathResolve2(opts.project));
3944
4082
  try {
3945
4083
  ContextCache.ensureTables(store);
3946
4084
  const cache = getContextCache();
3947
4085
  const topN = parseInt(opts.limit, 10) || 20;
3948
- const count = cache.warmHotFiles(store, pathResolve(opts.project), topN);
4086
+ const count = cache.warmHotFiles(store, pathResolve2(opts.project), topN);
3949
4087
  if (count === 0) {
3950
4088
  console.log(
3951
4089
  chalk2.dim(
@@ -3959,4 +4097,133 @@ cacheCmd.command("warm").description("Pre-warm hot file cache from access freque
3959
4097
  store.close();
3960
4098
  }
3961
4099
  });
4100
+ program.command("update").description("Check for and install the latest engram release").option("--check", "Check only \u2014 do not install", false).option("--force", "Bypass 7-day throttle cache on registry check", false).option(
4101
+ "--manager <mgr>",
4102
+ "Override package manager detection (npm | pnpm | yarn | bun)"
4103
+ ).option("--dry-run", "Print the upgrade command without executing", false).action(
4104
+ async (opts) => {
4105
+ const { checkForUpdate } = await import("./check-2Z3MPZEJ.js");
4106
+ const result = await checkForUpdate(PKG_VERSION, { force: opts.force });
4107
+ if (result.skipped) {
4108
+ if (result.fromCache === false) {
4109
+ console.log(
4110
+ chalk2.dim("Skipped (opt-out via ENGRAM_NO_UPDATE_CHECK or $CI).")
4111
+ );
4112
+ } else {
4113
+ console.log(chalk2.dim("Skipped (registry unreachable)."));
4114
+ }
4115
+ return;
4116
+ }
4117
+ const ageMin = result.checkedAt ? Math.round((Date.now() - result.checkedAt) / 6e4) : 0;
4118
+ const freshness = result.fromCache ? chalk2.dim(` (cached ${ageMin}m ago)`) : chalk2.dim(" (live)");
4119
+ console.log(
4120
+ `${chalk2.bold("engram")} ${chalk2.dim("installed:")} v${result.current} ${chalk2.dim("latest:")} ${result.latest ?? chalk2.yellow("unknown")}${freshness}`
4121
+ );
4122
+ if (!result.updateAvailable) {
4123
+ console.log(chalk2.green("\u2713 You are on the latest release."));
4124
+ return;
4125
+ }
4126
+ console.log(
4127
+ chalk2.yellow(
4128
+ `\u2B06 v${result.latest} is available \u2014 you're on v${result.current}.`
4129
+ )
4130
+ );
4131
+ if (opts.check) {
4132
+ console.log(chalk2.dim("Run `engram update` to install it."));
4133
+ return;
4134
+ }
4135
+ const { runUpgrade, manualCommand } = await import("./install-YVMVCFQW.js");
4136
+ const outcome = runUpgrade({
4137
+ dryRun: opts.dryRun,
4138
+ manager: opts.manager === "npm" || opts.manager === "pnpm" || opts.manager === "yarn" || opts.manager === "bun" ? opts.manager : void 0
4139
+ });
4140
+ if (outcome.ok) {
4141
+ console.log(chalk2.green(`\u2713 ${outcome.message}`));
4142
+ if (!opts.dryRun) {
4143
+ console.log(chalk2.dim(" Run `engram --version` to verify."));
4144
+ }
4145
+ } else {
4146
+ console.error(chalk2.red(`\u2717 ${outcome.message}`));
4147
+ if (outcome.stderrTail) {
4148
+ console.error(chalk2.dim(outcome.stderrTail));
4149
+ }
4150
+ console.error(chalk2.dim(` Manual: ${manualCommand()}`));
4151
+ process.exitCode = 1;
4152
+ }
4153
+ }
4154
+ );
4155
+ program.command("doctor").description("Component health report with remediation hints").option("-p, --project <path>", "Project directory", ".").option("-v, --verbose", "Show remediation hints for warn/fail checks", false).option("--json", "Output JSON", false).option(
4156
+ "--export",
4157
+ "Redacted JSON for bug reports (same as --json with --verbose)",
4158
+ false
4159
+ ).action(
4160
+ async (opts) => {
4161
+ const { buildReport, formatReport, exportReport } = await import("./report-C3GTM3HY.js");
4162
+ const root = pathResolve2(opts.project);
4163
+ const report = buildReport(root, PKG_VERSION);
4164
+ if (opts.json || opts.export) {
4165
+ console.log(exportReport(report));
4166
+ } else {
4167
+ console.log(formatReport(report, opts.verbose));
4168
+ }
4169
+ process.exitCode = report.overallSeverity === "ok" ? 0 : report.overallSeverity === "warn" ? 1 : 2;
4170
+ }
4171
+ );
4172
+ program.command("setup").description("Zero-friction first-run wizard (init + install-hook + doctor)").option("-p, --project <path>", "Project directory", ".").option("-y, --yes", "Accept all defaults (non-interactive)", false).option("--dry-run", "Print what would happen without touching anything", false).option(
4173
+ "--scope <scope>",
4174
+ "Hook scope for install-hook step (local | project | user)",
4175
+ "local"
4176
+ ).action(
4177
+ async (opts) => {
4178
+ const { runSetup } = await import("./wizard-AOXWMSXW.js");
4179
+ const scope = opts.scope === "local" || opts.scope === "project" || opts.scope === "user" ? opts.scope : "local";
4180
+ const result = await runSetup({
4181
+ projectPath: opts.project,
4182
+ yes: opts.yes,
4183
+ dryRun: opts.dryRun,
4184
+ engramVersion: PKG_VERSION,
4185
+ settingsScope: scope
4186
+ });
4187
+ process.exitCode = result.exitCode;
4188
+ }
4189
+ );
4190
+ var FIRST_RUN_SILENT_CMDS = /* @__PURE__ */ new Set([
4191
+ "intercept",
4192
+ "cursor-intercept",
4193
+ "hud-label",
4194
+ "setup",
4195
+ "init",
4196
+ "update",
4197
+ "doctor"
4198
+ ]);
4199
+ function maybePrintFirstRunHint() {
4200
+ if (process.env.CI) return;
4201
+ if (process.env.ENGRAM_NO_UPDATE_CHECK === "1") return;
4202
+ const subcommand = process.argv[2];
4203
+ if (!subcommand) return;
4204
+ if (FIRST_RUN_SILENT_CMDS.has(subcommand)) return;
4205
+ try {
4206
+ const cwd = process.cwd();
4207
+ if (existsSync9(join9(cwd, ".engram", "graph.db"))) return;
4208
+ const sentinel = join9(homedir(), ".engram", "first-run-shown");
4209
+ if (existsSync9(sentinel)) return;
4210
+ mkdirSync(dirname4(sentinel), { recursive: true });
4211
+ writeFileSync2(sentinel, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
4212
+ process.stderr.write(
4213
+ chalk2.dim("\u{1F4A1} ") + chalk2.yellow("First time in this repo?") + chalk2.dim(" Run ") + chalk2.white("engram setup") + chalk2.dim(" for a zero-friction install.\n")
4214
+ );
4215
+ } catch {
4216
+ }
4217
+ }
4218
+ function maybePrintUpdateHintSafe() {
4219
+ const subcommand = process.argv[2];
4220
+ if (!subcommand || FIRST_RUN_SILENT_CMDS.has(subcommand)) return;
4221
+ try {
4222
+ import("./notify-5POGKMRX.js").then((m) => m.maybePrintUpdateHint(PKG_VERSION)).catch(() => {
4223
+ });
4224
+ } catch {
4225
+ }
4226
+ }
4227
+ maybePrintFirstRunHint();
4228
+ maybePrintUpdateHintSafe();
3962
4229
  program.parse();