engramx 1.0.1 → 2.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +67 -7
  3. package/dist/{aider-context-TNGSXMVY.js → aider-context-BC5R2ZTA.js} +1 -1
  4. package/dist/cache-AK6CF3BC.js +10 -0
  5. package/dist/chunk-22INHMKB.js +31 -0
  6. package/dist/chunk-533LR4I7.js +220 -0
  7. package/dist/{chunk-QOG4K427.js → chunk-C6GBUOAL.js} +1 -1
  8. package/dist/chunk-CIQQ5Y3S.js +338 -0
  9. package/dist/chunk-KL6NSPVA.js +59 -0
  10. package/dist/{chunk-SBHGK5WA.js → chunk-PEH54LYC.js} +85 -3
  11. package/dist/{chunk-6SFMVYUN.js → chunk-SJT7VS2G.js} +127 -23
  12. package/dist/cli.js +380 -258
  13. package/dist/{core-77MHT3QV.js → core-6IY5L6II.js} +2 -2
  14. package/dist/{cursor-mdc-HWVUZUZH.js → cursor-mdc-GJ7E5LDD.js} +1 -1
  15. package/dist/{exporter-A3VSLS4U.js → exporter-GWU2GF23.js} +1 -1
  16. package/dist/grammars/tree-sitter-go.wasm +0 -0
  17. package/dist/grammars/tree-sitter-javascript.wasm +0 -0
  18. package/dist/grammars/tree-sitter-python.wasm +0 -0
  19. package/dist/grammars/tree-sitter-rust.wasm +0 -0
  20. package/dist/grammars/tree-sitter-tsx.wasm +0 -0
  21. package/dist/grammars/tree-sitter-typescript.wasm +0 -0
  22. package/dist/{importer-LU2YFZDY.js → importer-V62NGZRK.js} +1 -1
  23. package/dist/index.js +3 -3
  24. package/dist/{migrate-5ZJWF2HD.js → migrate-UKCO6BUU.js} +3 -1
  25. package/dist/plugin-loader-FCOMVOX7.js +100 -0
  26. package/dist/serve.js +2 -2
  27. package/dist/server-VBRTTECZ.js +1363 -0
  28. package/dist/{tuner-2LVIEE5V.js → tuner-KFNNGKG3.js} +4 -2
  29. package/dist/windsurf-rules-C7SVDHBL.js +59 -0
  30. package/package.json +4 -3
  31. package/dist/chunk-CEAANHHX.js +0 -88
  32. package/dist/server-I3C74ZLB.js +0 -193
package/dist/cli.js CHANGED
@@ -1,15 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ESTIMATED_TOKENS_PER_READ_DENY,
4
+ formatHudStatus,
5
+ formatStatsSummary,
6
+ getComponentStatus,
7
+ summarizeHookLog
8
+ } from "./chunk-533LR4I7.js";
9
+ import {
10
+ readConfig
11
+ } from "./chunk-22INHMKB.js";
2
12
  import {
3
13
  logHookEvent,
4
- readConfig,
5
14
  readHookLog
6
- } from "./chunk-CEAANHHX.js";
15
+ } from "./chunk-KL6NSPVA.js";
7
16
  import {
8
17
  autogen,
9
18
  install,
10
19
  status,
11
20
  uninstall
12
- } from "./chunk-QOG4K427.js";
21
+ } from "./chunk-C6GBUOAL.js";
13
22
  import {
14
23
  benchmark,
15
24
  computeKeywordIDF,
@@ -26,24 +35,24 @@ import {
26
35
  renderFileStructure,
27
36
  stats,
28
37
  toPosixPath
29
- } from "./chunk-6SFMVYUN.js";
30
- import "./chunk-SBHGK5WA.js";
38
+ } from "./chunk-SJT7VS2G.js";
39
+ import "./chunk-PEH54LYC.js";
31
40
 
32
41
  // src/cli.ts
33
42
  import { Command } from "commander";
34
43
  import chalk2 from "chalk";
35
44
  import {
36
- existsSync as existsSync10,
37
- readFileSync as readFileSync6,
38
- writeFileSync as writeFileSync3,
45
+ existsSync as existsSync9,
46
+ readFileSync as readFileSync5,
47
+ writeFileSync as writeFileSync2,
39
48
  mkdirSync,
40
49
  unlinkSync,
41
50
  copyFileSync,
42
51
  renameSync as renameSync2
43
52
  } from "fs";
44
- import { dirname as dirname4, join as join10, resolve as pathResolve } from "path";
53
+ import { dirname as dirname4, join as join9, resolve as pathResolve } from "path";
45
54
  import { fileURLToPath as fileURLToPath2 } from "url";
46
- import { homedir as homedir2 } from "os";
55
+ import { homedir } from "os";
47
56
 
48
57
  // src/intercept/safety.ts
49
58
  import { existsSync } from "fs";
@@ -408,8 +417,13 @@ function findGrammarWasm(lang) {
408
417
  const candidates = [];
409
418
  try {
410
419
  const here = dirname2(fileURLToPath(import.meta.url));
411
- candidates.push(join3(here, "..", "..", "node_modules", pkg, wasmName));
412
420
  candidates.push(join3(here, "..", "grammars", wasmName));
421
+ candidates.push(join3(here, "grammars", wasmName));
422
+ } catch {
423
+ }
424
+ try {
425
+ const here = dirname2(fileURLToPath(import.meta.url));
426
+ candidates.push(join3(here, "..", "..", "node_modules", pkg, wasmName));
413
427
  } catch {
414
428
  }
415
429
  try {
@@ -1325,7 +1339,7 @@ var lspProvider = {
1325
1339
  };
1326
1340
 
1327
1341
  // src/providers/resolver.ts
1328
- var ALL_PROVIDERS = [
1342
+ var BUILTIN_PROVIDERS = [
1329
1343
  astProvider,
1330
1344
  structureProvider,
1331
1345
  mistakesProvider,
@@ -1335,12 +1349,26 @@ var ALL_PROVIDERS = [
1335
1349
  obsidianProvider,
1336
1350
  lspProvider
1337
1351
  ];
1352
+ var BUILTIN_NAMES = new Set(BUILTIN_PROVIDERS.map((p) => p.name));
1353
+ async function getAllProviders() {
1354
+ const { getLoadedPlugins } = await import("./plugin-loader-FCOMVOX7.js");
1355
+ const { loaded } = await getLoadedPlugins();
1356
+ const safePlugins = loaded.filter((p) => !BUILTIN_NAMES.has(p.name));
1357
+ return [...BUILTIN_PROVIDERS, ...safePlugins];
1358
+ }
1359
+ var ALL_PROVIDERS = BUILTIN_PROVIDERS;
1338
1360
  function estimateTokens(text) {
1339
1361
  return Math.ceil(text.length / 4);
1340
1362
  }
1341
1363
  async function resolveRichPacket(filePath, context, enabledProviders) {
1342
1364
  const start = Date.now();
1343
- const providers = ALL_PROVIDERS.filter((p) => {
1365
+ let allProviders;
1366
+ try {
1367
+ allProviders = await getAllProviders();
1368
+ } catch {
1369
+ allProviders = BUILTIN_PROVIDERS;
1370
+ }
1371
+ const providers = allProviders.filter((p) => {
1344
1372
  if (enabledProviders && !enabledProviders.includes(p.name)) return false;
1345
1373
  return true;
1346
1374
  });
@@ -1372,7 +1400,7 @@ async function resolveRichPacket(filePath, context, enabledProviders) {
1372
1400
  if (totalTokens + sectionTokens > budget) {
1373
1401
  break;
1374
1402
  }
1375
- const provider = ALL_PROVIDERS.find((p) => p.name === result.provider);
1403
+ const provider = allProviders.find((p) => p.name === result.provider);
1376
1404
  const label = provider?.label ?? result.provider.toUpperCase();
1377
1405
  const cacheTag = result.cached ? ", cached" : "";
1378
1406
  sections.push(`${label} (${result.provider}${cacheTag}):
@@ -1406,7 +1434,7 @@ async function warmAllProviders(projectRoot, enabledProviders) {
1406
1434
  try {
1407
1435
  const result = await withTimeout2(p.warmup(projectRoot), 5e3);
1408
1436
  if (result && result.entries.length > 0) {
1409
- const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
1437
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
1410
1438
  const store = await getStore2(projectRoot);
1411
1439
  try {
1412
1440
  store.warmCache(
@@ -2314,98 +2342,6 @@ function watchProject(projectRoot, options = {}) {
2314
2342
  import chalk from "chalk";
2315
2343
  import { existsSync as existsSync7, statSync as statSync3 } from "fs";
2316
2344
  import { join as join7, resolve as resolve6, basename as basename4 } from "path";
2317
-
2318
- // src/intercept/stats.ts
2319
- var ESTIMATED_TOKENS_PER_READ_DENY = 1200;
2320
- function summarizeHookLog(entries) {
2321
- const byEvent = {};
2322
- const byTool = {};
2323
- const byDecision = {};
2324
- let readDenyCount = 0;
2325
- let firstEntryTs = null;
2326
- let lastEntryTs = null;
2327
- for (const entry of entries) {
2328
- const event = entry.event ?? "unknown";
2329
- byEvent[event] = (byEvent[event] ?? 0) + 1;
2330
- const tool = entry.tool ?? "unknown";
2331
- byTool[tool] = (byTool[tool] ?? 0) + 1;
2332
- if (entry.decision) {
2333
- byDecision[entry.decision] = (byDecision[entry.decision] ?? 0) + 1;
2334
- }
2335
- if (event === "PreToolUse" && tool === "Read" && entry.decision === "deny") {
2336
- readDenyCount += 1;
2337
- }
2338
- const ts = entry.ts;
2339
- if (typeof ts === "string") {
2340
- if (firstEntryTs === null || ts < firstEntryTs) firstEntryTs = ts;
2341
- if (lastEntryTs === null || ts > lastEntryTs) lastEntryTs = ts;
2342
- }
2343
- }
2344
- return {
2345
- totalInvocations: entries.length,
2346
- byEvent: Object.freeze(byEvent),
2347
- byTool: Object.freeze(byTool),
2348
- byDecision: Object.freeze(byDecision),
2349
- readDenyCount,
2350
- estimatedTokensSaved: readDenyCount * ESTIMATED_TOKENS_PER_READ_DENY,
2351
- firstEntry: firstEntryTs,
2352
- lastEntry: lastEntryTs
2353
- };
2354
- }
2355
- function formatStatsSummary(summary) {
2356
- if (summary.totalInvocations === 0) {
2357
- return "engram hook stats: no log entries yet.\n\nRun engram install-hook in a project, then use Claude Code to see interceptions.";
2358
- }
2359
- const lines = [];
2360
- lines.push(`engram hook stats (${summary.totalInvocations} invocations)`);
2361
- lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2362
- if (summary.firstEntry && summary.lastEntry) {
2363
- lines.push(`Time range: ${summary.firstEntry} \u2192 ${summary.lastEntry}`);
2364
- lines.push("");
2365
- }
2366
- lines.push("By event:");
2367
- const eventEntries = Object.entries(summary.byEvent).sort(
2368
- (a, b) => b[1] - a[1]
2369
- );
2370
- for (const [event, count] of eventEntries) {
2371
- const pct = (count / summary.totalInvocations * 100).toFixed(1);
2372
- lines.push(` ${event.padEnd(18)} ${String(count).padStart(5)} (${pct}%)`);
2373
- }
2374
- lines.push("");
2375
- lines.push("By tool:");
2376
- const toolEntries = Object.entries(summary.byTool).filter(([k]) => k !== "unknown").sort((a, b) => b[1] - a[1]);
2377
- for (const [tool, count] of toolEntries) {
2378
- lines.push(` ${tool.padEnd(18)} ${String(count).padStart(5)}`);
2379
- }
2380
- if (toolEntries.length === 0) {
2381
- lines.push(" (no tool-tagged entries)");
2382
- }
2383
- lines.push("");
2384
- const decisionEntries = Object.entries(summary.byDecision);
2385
- if (decisionEntries.length > 0) {
2386
- lines.push("PreToolUse decisions:");
2387
- for (const [decision, count] of decisionEntries.sort(
2388
- (a, b) => b[1] - a[1]
2389
- )) {
2390
- lines.push(` ${decision.padEnd(18)} ${String(count).padStart(5)}`);
2391
- }
2392
- lines.push("");
2393
- }
2394
- if (summary.readDenyCount > 0) {
2395
- lines.push(
2396
- `Estimated tokens saved: ~${summary.estimatedTokensSaved.toLocaleString()}`
2397
- );
2398
- lines.push(
2399
- ` (${summary.readDenyCount} Read denies \xD7 ${ESTIMATED_TOKENS_PER_READ_DENY} tok/deny avg)`
2400
- );
2401
- } else {
2402
- lines.push("Estimated tokens saved: 0");
2403
- lines.push(" (no PreToolUse:Read denies recorded yet)");
2404
- }
2405
- return lines.join("\n");
2406
- }
2407
-
2408
- // src/dashboard.ts
2409
2345
  var AMBER = chalk.hex("#d97706");
2410
2346
  var DIM = chalk.dim;
2411
2347
  var GREEN = chalk.green;
@@ -2736,116 +2672,15 @@ function formatInstallDiff(before, after) {
2736
2672
  return lines.length > 0 ? lines.join("\n") : "(no changes)";
2737
2673
  }
2738
2674
 
2739
- // src/intercept/component-status.ts
2740
- import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
2741
- import { join as join8 } from "path";
2742
- import { tmpdir as tmpdir2, homedir } from "os";
2743
- function statusPath(projectRoot) {
2744
- return join8(projectRoot, ".engram", "component-status.json");
2745
- }
2746
- function readCachedStatus(projectRoot) {
2747
- const path2 = statusPath(projectRoot);
2748
- if (!existsSync8(path2)) return null;
2749
- try {
2750
- const raw = JSON.parse(readFileSync4(path2, "utf-8"));
2751
- if (Date.now() - raw.generatedAt > 3e4) return null;
2752
- return raw;
2753
- } catch {
2754
- return null;
2755
- }
2756
- }
2757
- function checkHttp(projectRoot) {
2758
- return existsSync8(join8(projectRoot, ".engram", "http-server.pid"));
2759
- }
2760
- function checkLsp(projectRoot) {
2761
- if (existsSync8(join8(projectRoot, ".engram", "lsp-available"))) return true;
2762
- const tmp = tmpdir2();
2763
- const candidates = [
2764
- join8(tmp, "tsserver.sock"),
2765
- join8(tmp, "typescript-language-server.sock")
2766
- ];
2767
- return candidates.some((c) => existsSync8(c));
2768
- }
2769
- function checkAst(projectRoot) {
2770
- const grammarsDir = join8(projectRoot, "node_modules", "web-tree-sitter");
2771
- return existsSync8(grammarsDir);
2772
- }
2773
- function countIdeAdapters(projectRoot) {
2774
- let count = 0;
2775
- if (existsSync8(join8(projectRoot, ".cursor", "rules", "engram-context.mdc"))) {
2776
- count += 1;
2777
- }
2778
- const continueConfig = join8(homedir(), ".continue", "config.json");
2779
- if (existsSync8(continueConfig)) {
2780
- try {
2781
- const cfg = readFileSync4(continueConfig, "utf-8");
2782
- if (cfg.includes("engram")) count += 1;
2783
- } catch {
2784
- }
2785
- }
2786
- const zedSettings = join8(homedir(), ".config", "zed", "settings.json");
2787
- if (existsSync8(zedSettings)) {
2788
- try {
2789
- const cfg = readFileSync4(zedSettings, "utf-8");
2790
- if (cfg.includes("engram")) count += 1;
2791
- } catch {
2792
- }
2793
- }
2794
- const claudeSettings = join8(projectRoot, ".claude", "settings.local.json");
2795
- if (existsSync8(claudeSettings)) {
2796
- try {
2797
- const cfg = readFileSync4(claudeSettings, "utf-8");
2798
- if (cfg.includes("engram")) count += 1;
2799
- } catch {
2800
- }
2801
- }
2802
- return count;
2803
- }
2804
- function refreshComponentStatus(projectRoot) {
2805
- const now = Date.now();
2806
- const components = [
2807
- { name: "http", available: checkHttp(projectRoot), checkedAt: now },
2808
- { name: "lsp", available: checkLsp(projectRoot), checkedAt: now },
2809
- { name: "ast", available: checkAst(projectRoot), checkedAt: now }
2810
- ];
2811
- const ideCount = countIdeAdapters(projectRoot);
2812
- const report = {
2813
- components,
2814
- ideCount,
2815
- generatedAt: now
2816
- };
2817
- try {
2818
- writeFileSync(statusPath(projectRoot), JSON.stringify(report), "utf-8");
2819
- } catch {
2820
- }
2821
- return report;
2822
- }
2823
- function getComponentStatus(projectRoot) {
2824
- const cached = readCachedStatus(projectRoot);
2825
- if (cached) return cached;
2826
- return refreshComponentStatus(projectRoot);
2827
- }
2828
- function formatHudStatus(report) {
2829
- const parts = [];
2830
- for (const c of report.components) {
2831
- const icon = c.available ? "\u2713" : "\u2717";
2832
- parts.push(`${c.name.toUpperCase()} ${icon}`);
2833
- }
2834
- if (report.ideCount > 0) {
2835
- parts.push(`${report.ideCount} IDE${report.ideCount > 1 ? "s" : ""}`);
2836
- }
2837
- return parts.join(" | ");
2838
- }
2839
-
2840
2675
  // src/intercept/memory-md.ts
2841
2676
  import {
2842
- existsSync as existsSync9,
2843
- readFileSync as readFileSync5,
2844
- writeFileSync as writeFileSync2,
2677
+ existsSync as existsSync8,
2678
+ readFileSync as readFileSync4,
2679
+ writeFileSync,
2845
2680
  renameSync,
2846
2681
  statSync as statSync4
2847
2682
  } from "fs";
2848
- import { join as join9 } from "path";
2683
+ import { join as join8 } from "path";
2849
2684
  var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
2850
2685
  var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
2851
2686
  var MAX_MEMORY_FILE_BYTES = 1e6;
@@ -2916,19 +2751,19 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
2916
2751
  if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
2917
2752
  return false;
2918
2753
  }
2919
- const memoryPath = join9(projectRoot, "MEMORY.md");
2754
+ const memoryPath = join8(projectRoot, "MEMORY.md");
2920
2755
  try {
2921
2756
  let existing = "";
2922
- if (existsSync9(memoryPath)) {
2757
+ if (existsSync8(memoryPath)) {
2923
2758
  const st = statSync4(memoryPath);
2924
2759
  if (st.size > MAX_MEMORY_FILE_BYTES) {
2925
2760
  return false;
2926
2761
  }
2927
- existing = readFileSync5(memoryPath, "utf-8");
2762
+ existing = readFileSync4(memoryPath, "utf-8");
2928
2763
  }
2929
2764
  const updated = upsertEngramSection(existing, engramSection);
2930
2765
  const tmpPath = memoryPath + ".engram-tmp";
2931
- writeFileSync2(tmpPath, updated);
2766
+ writeFileSync(tmpPath, updated);
2932
2767
  renameSync(tmpPath, memoryPath);
2933
2768
  return true;
2934
2769
  } catch {
@@ -2948,10 +2783,14 @@ program.name("engram").description(
2948
2783
  program.command("init").description("Scan codebase and build knowledge graph (zero LLM cost)").argument("[path]", "Project directory", ".").option(
2949
2784
  "--with-skills [dir]",
2950
2785
  "Also index Claude Code skills from ~/.claude/skills/ or a given path"
2951
- ).option("--from-ccs", "Import .context/index.md (CCS) into graph after init").action(async (projectPath, opts) => {
2952
- console.log(chalk2.dim("\u{1F50D} Scanning codebase..."));
2786
+ ).option("--from-ccs", "Import .context/index.md (CCS) into graph after init").option(
2787
+ "--incremental",
2788
+ "Skip unchanged files (mtime-based). Dramatically faster on re-index of large repos."
2789
+ ).action(async (projectPath, opts) => {
2790
+ console.log(chalk2.dim(opts.incremental ? "\u{1F50D} Scanning changed files..." : "\u{1F50D} Scanning codebase..."));
2953
2791
  const result = await init(projectPath, {
2954
- withSkills: opts.withSkills
2792
+ withSkills: opts.withSkills,
2793
+ incremental: opts.incremental
2955
2794
  });
2956
2795
  console.log(
2957
2796
  chalk2.green("\u{1F333} AST extraction complete") + chalk2.dim(` (${result.timeMs}ms, 0 tokens used)`)
@@ -2959,6 +2798,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2959
2798
  console.log(
2960
2799
  ` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${result.totalLines.toLocaleString()} lines)`
2961
2800
  );
2801
+ if (result.incremental && result.skippedFiles && result.skippedFiles > 0) {
2802
+ console.log(chalk2.dim(` ${result.skippedFiles} unchanged files skipped (incremental mode)`));
2803
+ }
2962
2804
  if (result.skillCount && result.skillCount > 0) {
2963
2805
  console.log(
2964
2806
  chalk2.cyan(` ${chalk2.bold(String(result.skillCount))} skills indexed`)
@@ -2977,9 +2819,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2977
2819
  console.log(chalk2.green("\n\u2705 Ready. Your AI now has persistent memory."));
2978
2820
  console.log(chalk2.dim(" Graph stored in .engram/graph.db"));
2979
2821
  const resolvedProject = pathResolve(projectPath);
2980
- const localSettings = join10(resolvedProject, ".claude", "settings.local.json");
2981
- const projectSettings = join10(resolvedProject, ".claude", "settings.json");
2982
- const hasHooks = existsSync10(localSettings) && readFileSync6(localSettings, "utf-8").includes("engram intercept") || existsSync10(projectSettings) && readFileSync6(projectSettings, "utf-8").includes("engram intercept");
2822
+ const localSettings = join9(resolvedProject, ".claude", "settings.local.json");
2823
+ const projectSettings = join9(resolvedProject, ".claude", "settings.json");
2824
+ const hasHooks = existsSync9(localSettings) && readFileSync5(localSettings, "utf-8").includes("engram intercept") || existsSync9(projectSettings) && readFileSync5(projectSettings, "utf-8").includes("engram intercept");
2983
2825
  if (!hasHooks) {
2984
2826
  console.log(
2985
2827
  chalk2.yellow("\n\u{1F4A1} Next step: ") + chalk2.white("engram install-hook") + chalk2.dim(
@@ -2993,7 +2835,7 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2993
2835
  );
2994
2836
  }
2995
2837
  if (opts.fromCcs) {
2996
- const { importCcs } = await import("./importer-LU2YFZDY.js");
2838
+ const { importCcs } = await import("./importer-V62NGZRK.js");
2997
2839
  const resolvedProjectPath = pathResolve(projectPath);
2998
2840
  const ccsResult = await importCcs(resolvedProjectPath);
2999
2841
  if (ccsResult.nodesCreated > 0) {
@@ -3035,8 +2877,8 @@ program.command("watch").description("Watch project for file changes and re-inde
3035
2877
  });
3036
2878
  program.command("dashboard").alias("hud").description("Live terminal dashboard showing hook activity and token savings").argument("[path]", "Project directory", ".").action(async (projectPath) => {
3037
2879
  const resolvedPath = pathResolve(projectPath);
3038
- const dbPath = join10(resolvedPath, ".engram", "graph.db");
3039
- if (!existsSync10(dbPath)) {
2880
+ const dbPath = join9(resolvedPath, ".engram", "graph.db");
2881
+ if (!existsSync9(dbPath)) {
3040
2882
  console.error(
3041
2883
  chalk2.red("No engram graph found at ") + chalk2.white(resolvedPath)
3042
2884
  );
@@ -3056,7 +2898,7 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
3056
2898
  let resolvedPath = pathResolve(projectPath);
3057
2899
  let found = false;
3058
2900
  for (let depth = 0; depth < 20; depth++) {
3059
- if (existsSync10(join10(resolvedPath, ".engram", "graph.db"))) {
2901
+ if (existsSync9(join9(resolvedPath, ".engram", "graph.db"))) {
3060
2902
  found = true;
3061
2903
  break;
3062
2904
  }
@@ -3068,8 +2910,8 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
3068
2910
  console.log('{"label":""}');
3069
2911
  return;
3070
2912
  }
3071
- const logPath = join10(resolvedPath, ".engram", "hook-log.jsonl");
3072
- if (!existsSync10(logPath)) {
2913
+ const logPath = join9(resolvedPath, ".engram", "hook-log.jsonl");
2914
+ if (!existsSync9(logPath)) {
3073
2915
  console.log('{"label":"\u26A1engram \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 ready"}');
3074
2916
  return;
3075
2917
  }
@@ -3223,7 +3065,7 @@ program.command("gen").description("Generate CLAUDE.md / .cursorrules section fr
3223
3065
  }
3224
3066
  );
3225
3067
  program.command("gen-mdc").description("Generate .cursor/rules/engram-context.mdc from knowledge graph").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3226
- const { generateCursorMdc } = await import("./cursor-mdc-HWVUZUZH.js");
3068
+ const { generateCursorMdc } = await import("./cursor-mdc-GJ7E5LDD.js");
3227
3069
  const result = await generateCursorMdc(opts.project);
3228
3070
  console.log(
3229
3071
  chalk2.green(
@@ -3244,7 +3086,7 @@ program.command("gen-mdc").description("Generate .cursor/rules/engram-context.md
3244
3086
  }
3245
3087
  });
3246
3088
  program.command("gen-ccs").description("Export knowledge graph as .context/index.md (CCS format)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3247
- const { exportCcs } = await import("./exporter-A3VSLS4U.js");
3089
+ const { exportCcs } = await import("./exporter-GWU2GF23.js");
3248
3090
  const result = await exportCcs(pathResolve(opts.project));
3249
3091
  console.log(
3250
3092
  chalk2.green(
@@ -3253,7 +3095,7 @@ program.command("gen-ccs").description("Export knowledge graph as .context/index
3253
3095
  );
3254
3096
  });
3255
3097
  program.command("gen-aider").description("Generate .aider-context.md from knowledge graph").option("-p, --project <path>", "Project directory", ".").option("--watch", "Regenerate on graph changes").action(async (opts) => {
3256
- const { generateAiderContext } = await import("./aider-context-TNGSXMVY.js");
3098
+ const { generateAiderContext } = await import("./aider-context-BC5R2ZTA.js");
3257
3099
  const result = await generateAiderContext(pathResolve(opts.project));
3258
3100
  console.log(
3259
3101
  chalk2.green(
@@ -3273,15 +3115,36 @@ program.command("gen-aider").description("Generate .aider-context.md from knowle
3273
3115
  });
3274
3116
  }
3275
3117
  });
3118
+ 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));
3121
+ console.log(
3122
+ chalk2.green(
3123
+ `\u2705 Generated ${result.filePath} (${result.sections} sections, ${result.nodes} nodes)`
3124
+ )
3125
+ );
3126
+ if (opts.watch) {
3127
+ watchProject(pathResolve(opts.project), {
3128
+ onReindex: async () => {
3129
+ const r = await generateWindsurfRules(opts.project);
3130
+ console.log(chalk2.dim(` \u21BB Regenerated .windsurfrules (${r.nodes} nodes)`));
3131
+ },
3132
+ onError: (err) => console.error(chalk2.red(err.message)),
3133
+ onReady: () => console.log(chalk2.dim(" Watching for changes..."))
3134
+ });
3135
+ await new Promise(() => {
3136
+ });
3137
+ }
3138
+ });
3276
3139
  function resolveSettingsPath(scope, projectPath) {
3277
3140
  const absProject = pathResolve(projectPath);
3278
3141
  switch (scope) {
3279
3142
  case "local":
3280
- return join10(absProject, ".claude", "settings.local.json");
3143
+ return join9(absProject, ".claude", "settings.local.json");
3281
3144
  case "project":
3282
- return join10(absProject, ".claude", "settings.json");
3145
+ return join9(absProject, ".claude", "settings.json");
3283
3146
  case "user":
3284
- return join10(homedir2(), ".claude", "settings.json");
3147
+ return join9(homedir(), ".claude", "settings.json");
3285
3148
  default:
3286
3149
  return null;
3287
3150
  }
@@ -3375,9 +3238,9 @@ program.command("install-hook").description("Install engram hook entries into Cl
3375
3238
  process.exit(1);
3376
3239
  }
3377
3240
  let existing = {};
3378
- if (existsSync10(settingsPath)) {
3241
+ if (existsSync9(settingsPath)) {
3379
3242
  try {
3380
- const raw = readFileSync6(settingsPath, "utf-8");
3243
+ const raw = readFileSync5(settingsPath, "utf-8");
3381
3244
  existing = raw.trim() ? JSON.parse(raw) : {};
3382
3245
  } catch (err) {
3383
3246
  console.error(
@@ -3423,13 +3286,13 @@ program.command("install-hook").description("Install engram hook entries into Cl
3423
3286
  }
3424
3287
  try {
3425
3288
  mkdirSync(dirname4(settingsPath), { recursive: true });
3426
- if (existsSync10(settingsPath)) {
3289
+ if (existsSync9(settingsPath)) {
3427
3290
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
3428
3291
  copyFileSync(settingsPath, backupPath);
3429
3292
  console.log(chalk2.dim(` Backup: ${backupPath}`));
3430
3293
  }
3431
3294
  const tmpPath = settingsPath + ".engram-tmp";
3432
- writeFileSync3(
3295
+ writeFileSync2(
3433
3296
  tmpPath,
3434
3297
  JSON.stringify(result.updated, null, 2) + "\n"
3435
3298
  );
@@ -3474,7 +3337,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3474
3337
  console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
3475
3338
  process.exit(1);
3476
3339
  }
3477
- if (!existsSync10(settingsPath)) {
3340
+ if (!existsSync9(settingsPath)) {
3478
3341
  console.log(
3479
3342
  chalk2.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
3480
3343
  );
@@ -3482,7 +3345,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3482
3345
  }
3483
3346
  let existing;
3484
3347
  try {
3485
- const raw = readFileSync6(settingsPath, "utf-8");
3348
+ const raw = readFileSync5(settingsPath, "utf-8");
3486
3349
  existing = raw.trim() ? JSON.parse(raw) : {};
3487
3350
  } catch (err) {
3488
3351
  console.error(
@@ -3502,7 +3365,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3502
3365
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
3503
3366
  copyFileSync(settingsPath, backupPath);
3504
3367
  const tmpPath = settingsPath + ".engram-tmp";
3505
- writeFileSync3(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
3368
+ writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
3506
3369
  renameSync2(tmpPath, settingsPath);
3507
3370
  if (result.removed.length > 0) {
3508
3371
  console.log(
@@ -3597,9 +3460,9 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
3597
3460
  console.error(chalk2.dim("Run 'engram init' first."));
3598
3461
  process.exit(1);
3599
3462
  }
3600
- const flagPath = join10(projectRoot, ".engram", "hook-disabled");
3463
+ const flagPath = join9(projectRoot, ".engram", "hook-disabled");
3601
3464
  try {
3602
- writeFileSync3(flagPath, (/* @__PURE__ */ new Date()).toISOString());
3465
+ writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
3603
3466
  console.log(
3604
3467
  chalk2.green(`\u2705 engram hooks disabled for ${projectRoot}`)
3605
3468
  );
@@ -3621,8 +3484,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
3621
3484
  console.error(chalk2.red(`Not an engram project: ${absProject}`));
3622
3485
  process.exit(1);
3623
3486
  }
3624
- const flagPath = join10(projectRoot, ".engram", "hook-disabled");
3625
- if (!existsSync10(flagPath)) {
3487
+ const flagPath = join9(projectRoot, ".engram", "hook-disabled");
3488
+ if (!existsSync9(flagPath)) {
3626
3489
  console.log(
3627
3490
  chalk2.yellow(`engram hooks already enabled for ${projectRoot}`)
3628
3491
  );
@@ -3664,9 +3527,9 @@ program.command("memory-sync").description(
3664
3527
  }
3665
3528
  let branch = null;
3666
3529
  try {
3667
- const headPath = join10(projectRoot, ".git", "HEAD");
3668
- if (existsSync10(headPath)) {
3669
- const content = readFileSync6(headPath, "utf-8").trim();
3530
+ const headPath = join9(projectRoot, ".git", "HEAD");
3531
+ if (existsSync9(headPath)) {
3532
+ const content = readFileSync5(headPath, "utf-8").trim();
3670
3533
  const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
3671
3534
  if (m) branch = m[1];
3672
3535
  }
@@ -3692,7 +3555,7 @@ program.command("memory-sync").description(
3692
3555
  \u{1F4DD} engram memory-sync`)
3693
3556
  );
3694
3557
  console.log(
3695
- chalk2.dim(` Target: ${join10(projectRoot, "MEMORY.md")}`)
3558
+ chalk2.dim(` Target: ${join9(projectRoot, "MEMORY.md")}`)
3696
3559
  );
3697
3560
  if (opts.dryRun) {
3698
3561
  console.log(chalk2.cyan("\n Section to write (dry-run):\n"));
@@ -3736,29 +3599,69 @@ program.command("stress-test").description("Run stress tests: memory, concurrenc
3736
3599
  if (opts.replay) args.push("--replay", opts.replay);
3737
3600
  if (opts.limit) args.push("--limit", String(opts.limit));
3738
3601
  try {
3739
- execFileSync2("npx", ["tsx", ...args], { stdio: "inherit", shell: true, cwd: join10(dirname4(fileURLToPath2(import.meta.url)), "..") });
3602
+ execFileSync2("npx", ["tsx", ...args], { stdio: "inherit", shell: true, cwd: join9(dirname4(fileURLToPath2(import.meta.url)), "..") });
3740
3603
  } catch {
3741
3604
  process.exit(1);
3742
3605
  }
3743
3606
  });
3744
3607
  program.command("server").description("Start engram HTTP REST server (binds to 127.0.0.1 only)").option("--http", "Enable HTTP server (default)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3745
- const { startHttpServer } = await import("./server-I3C74ZLB.js");
3608
+ const { startHttpServer } = await import("./server-VBRTTECZ.js");
3746
3609
  await startHttpServer(pathResolve(opts.project), parseInt(opts.port, 10));
3747
3610
  });
3611
+ 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
+ const port = parseInt(opts.port, 10);
3613
+ const url = `http://127.0.0.1:${port}/ui`;
3614
+ const projectRoot = pathResolve(opts.project);
3615
+ const { existsSync: existsSync10, readFileSync: readFileSync6 } = await import("fs");
3616
+ const pidPath = join9(projectRoot, ".engram", "http-server.pid");
3617
+ let alreadyRunning = false;
3618
+ if (existsSync10(pidPath)) {
3619
+ try {
3620
+ const pid = parseInt(readFileSync6(pidPath, "utf-8"), 10);
3621
+ process.kill(pid, 0);
3622
+ alreadyRunning = true;
3623
+ } catch {
3624
+ }
3625
+ }
3626
+ if (alreadyRunning) {
3627
+ console.log(chalk2.dim(`engram server already running \u2014 opening ${url}`));
3628
+ } else {
3629
+ console.log(chalk2.dim(`Starting engram server on ${url}...`));
3630
+ const { spawn } = await import("child_process");
3631
+ const child = spawn(
3632
+ process.argv[0],
3633
+ [process.argv[1], "server", "--port", String(port), "-p", projectRoot],
3634
+ { detached: true, stdio: "ignore" }
3635
+ );
3636
+ child.unref();
3637
+ await new Promise((r) => setTimeout(r, 500));
3638
+ }
3639
+ console.log(chalk2.green(`\u2713 Dashboard: ${url}`));
3640
+ if (opts.open !== false) {
3641
+ const { platform } = process;
3642
+ const opener = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
3643
+ try {
3644
+ const { execFile: execFile4 } = await import("child_process");
3645
+ execFile4(opener, [url], { shell: platform === "win32" }, () => {
3646
+ });
3647
+ } catch {
3648
+ }
3649
+ }
3650
+ });
3748
3651
  program.command("context-server").description("Start Zed-compatible context server (JSON-RPC over stdio)").action(async () => {
3749
3652
  const { execFileSync: execFileSync2 } = await import("child_process");
3750
3653
  try {
3751
3654
  execFileSync2("npx", ["tsx", "adapters/zed/index.ts"], {
3752
3655
  stdio: "inherit",
3753
3656
  shell: true,
3754
- cwd: join10(dirname4(fileURLToPath2(import.meta.url)), "..")
3657
+ cwd: join9(dirname4(fileURLToPath2(import.meta.url)), "..")
3755
3658
  });
3756
3659
  } catch {
3757
3660
  process.exit(1);
3758
3661
  }
3759
3662
  });
3760
3663
  program.command("tune").description("Analyze hook-log and propose provider config changes").option("-p, --project <path>", "Project directory", ".").option("--dry-run", "Show proposed changes without applying (default)").option("--apply", "Apply proposed changes to .engram/config.json").action(async (opts) => {
3761
- const { analyzeTuning, applyTuning } = await import("./tuner-2LVIEE5V.js");
3664
+ const { analyzeTuning, applyTuning } = await import("./tuner-KFNNGKG3.js");
3762
3665
  const proposal = analyzeTuning(pathResolve(opts.project));
3763
3666
  if (proposal.changes.length === 0) {
3764
3667
  console.log(
@@ -3789,8 +3692,8 @@ program.command("tune").description("Analyze hook-log and propose provider confi
3789
3692
  });
3790
3693
  var dbCmd = program.command("db").description("Database management");
3791
3694
  dbCmd.command("status").description("Show schema version and migration status").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3792
- const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
3793
- const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-5ZJWF2HD.js");
3695
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3696
+ const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-UKCO6BUU.js");
3794
3697
  const store = await getStore2(pathResolve(opts.project));
3795
3698
  try {
3796
3699
  const version = getSchemaVersion(store.db);
@@ -3806,11 +3709,11 @@ dbCmd.command("status").description("Show schema version and migration status").
3806
3709
  }
3807
3710
  });
3808
3711
  dbCmd.command("migrate").description("Run pending schema migrations").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3809
- const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
3810
- const { runMigrations } = await import("./migrate-5ZJWF2HD.js");
3712
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3713
+ const { runMigrations } = await import("./migrate-UKCO6BUU.js");
3811
3714
  const store = await getStore2(pathResolve(opts.project));
3812
3715
  try {
3813
- const dbPath = join10(pathResolve(opts.project), ".engram", "graph.db");
3716
+ const dbPath = join9(pathResolve(opts.project), ".engram", "graph.db");
3814
3717
  const result = runMigrations(
3815
3718
  store.db,
3816
3719
  dbPath
@@ -3830,4 +3733,223 @@ dbCmd.command("migrate").description("Run pending schema migrations").option("-p
3830
3733
  store.close();
3831
3734
  }
3832
3735
  });
3736
+ dbCmd.command("rollback").description("Roll back to an earlier schema version (DESTRUCTIVE \u2014 always backs up first)").option("-p, --project <path>", "Project directory", ".").option("--to <version>", "Target schema version (0 drops all tables)").option("--yes", "Skip confirmation prompt").action(async (opts) => {
3737
+ if (opts.to === void 0) {
3738
+ console.error(chalk2.red("Required: --to <version>"));
3739
+ console.log(chalk2.dim("Run 'engram db status' to see current version."));
3740
+ process.exit(1);
3741
+ }
3742
+ const target = parseInt(opts.to, 10);
3743
+ if (isNaN(target)) {
3744
+ console.error(chalk2.red(`Invalid version: ${opts.to}`));
3745
+ process.exit(1);
3746
+ }
3747
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3748
+ const { rollback, getSchemaVersion } = await import("./migrate-UKCO6BUU.js");
3749
+ const store = await getStore2(pathResolve(opts.project));
3750
+ try {
3751
+ const dbPath = join9(pathResolve(opts.project), ".engram", "graph.db");
3752
+ const current = getSchemaVersion(
3753
+ store.db
3754
+ );
3755
+ if (target === current) {
3756
+ console.log(chalk2.green(`Already at v${target}.`));
3757
+ return;
3758
+ }
3759
+ if (target > current) {
3760
+ console.error(
3761
+ chalk2.red(`Cannot roll back to v${target}: current is v${current}.`)
3762
+ );
3763
+ console.log(chalk2.dim("Use 'engram db migrate' to move forward."));
3764
+ process.exit(1);
3765
+ }
3766
+ if (!opts.yes) {
3767
+ console.log(
3768
+ chalk2.yellow(
3769
+ `\u26A0 This will roll back from v${current} \u2192 v${target}.`
3770
+ )
3771
+ );
3772
+ console.log(
3773
+ chalk2.yellow(
3774
+ ` Tables created after v${target} will be DROPPED (data loss).`
3775
+ )
3776
+ );
3777
+ console.log(chalk2.dim(` A backup will be saved to ${dbPath}.bak-v${current}`));
3778
+ console.log(chalk2.dim(` Re-run with --yes to confirm.`));
3779
+ process.exit(1);
3780
+ }
3781
+ const result = rollback(
3782
+ store.db,
3783
+ dbPath,
3784
+ target
3785
+ );
3786
+ store.save();
3787
+ console.log(
3788
+ chalk2.green(
3789
+ `\u2713 Rolled back v${result.fromVersion} \u2192 v${result.toVersion} (${result.migrationsReverted} migrations reverted)`
3790
+ )
3791
+ );
3792
+ if (result.backedUp) {
3793
+ console.log(chalk2.dim(` Backup: ${dbPath}.bak-v${result.fromVersion}`));
3794
+ }
3795
+ } finally {
3796
+ store.close();
3797
+ }
3798
+ });
3799
+ var pluginCmd = program.command("plugin").description("Manage context provider plugins");
3800
+ pluginCmd.command("list").description("List installed provider plugins").action(async () => {
3801
+ const { loadPlugins, PLUGINS_DIR, ensurePluginsDir } = await import("./plugin-loader-FCOMVOX7.js");
3802
+ ensurePluginsDir();
3803
+ const { loaded, failed } = await loadPlugins();
3804
+ if (loaded.length === 0 && failed.length === 0) {
3805
+ console.log(chalk2.dim(`No plugins installed.`));
3806
+ console.log(chalk2.dim(`Install with: engram plugin install <file.mjs>`));
3807
+ console.log(chalk2.dim(`Plugins directory: ${PLUGINS_DIR}`));
3808
+ return;
3809
+ }
3810
+ if (loaded.length > 0) {
3811
+ console.log(chalk2.bold(`Installed plugins (${loaded.length})`));
3812
+ console.log(chalk2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3813
+ for (const p of loaded) {
3814
+ const tierLabel = p.tier === 1 ? "internal" : "external";
3815
+ const descr = p.description ? ` \u2014 ${p.description}` : "";
3816
+ console.log(
3817
+ ` ${chalk2.green("\u25CF")} ${chalk2.bold(p.name)} ${chalk2.dim(`v${p.version}`)} ` + chalk2.dim(`[${tierLabel}, ${p.tokenBudget}tok budget]`)
3818
+ );
3819
+ if (descr) console.log(` ${chalk2.dim(descr.trim())}`);
3820
+ }
3821
+ }
3822
+ if (failed.length > 0) {
3823
+ console.log();
3824
+ console.log(chalk2.yellow(`Failed to load (${failed.length}):`));
3825
+ for (const f of failed) {
3826
+ console.log(` ${chalk2.red("\u2717")} ${f.file} ${chalk2.dim(`\u2014 ${f.reason}`)}`);
3827
+ }
3828
+ }
3829
+ });
3830
+ pluginCmd.command("install").description("Install a plugin by copying its .mjs file into ~/.engram/plugins/").argument("<file>", "Path to plugin .mjs file").action(async (file) => {
3831
+ const { copyFileSync: copyFileSync2, statSync: statSync5 } = await import("fs");
3832
+ const { basename: basename6 } = await import("path");
3833
+ const { PLUGINS_DIR, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-FCOMVOX7.js");
3834
+ const { pathToFileURL } = await import("url");
3835
+ const absPath = pathResolve(file);
3836
+ if (!existsSync9(absPath)) {
3837
+ console.error(chalk2.red(`File not found: ${absPath}`));
3838
+ process.exit(1);
3839
+ }
3840
+ if (!statSync5(absPath).isFile()) {
3841
+ console.error(chalk2.red(`Not a file: ${absPath}`));
3842
+ process.exit(1);
3843
+ }
3844
+ if (!absPath.endsWith(".mjs") && !absPath.endsWith(".js")) {
3845
+ console.error(chalk2.red(`Plugin must be .mjs or .js (got ${absPath})`));
3846
+ process.exit(1);
3847
+ }
3848
+ try {
3849
+ const mod = await import(pathToFileURL(absPath).href);
3850
+ const { plugin, reason } = validatePlugin(mod);
3851
+ if (!plugin) {
3852
+ console.error(chalk2.red(`Invalid plugin: ${reason}`));
3853
+ process.exit(1);
3854
+ }
3855
+ console.log(
3856
+ chalk2.dim(`Validated ${plugin.name} v${plugin.version} (tier ${plugin.tier})`)
3857
+ );
3858
+ } catch (e) {
3859
+ console.error(chalk2.red(`Failed to load plugin: ${e.message}`));
3860
+ process.exit(1);
3861
+ }
3862
+ ensurePluginsDir();
3863
+ const destName = basename6(absPath);
3864
+ const destPath = join9(PLUGINS_DIR, destName);
3865
+ copyFileSync2(absPath, destPath);
3866
+ console.log(chalk2.green(`\u2713 Installed: ${destPath}`));
3867
+ });
3868
+ pluginCmd.command("remove").description("Remove an installed plugin by filename").argument("<filename>", "Plugin filename (e.g., my-provider.mjs)").action(async (filename) => {
3869
+ const { PLUGINS_DIR } = await import("./plugin-loader-FCOMVOX7.js");
3870
+ const target = join9(PLUGINS_DIR, filename);
3871
+ if (!existsSync9(target)) {
3872
+ console.error(chalk2.red(`No such plugin: ${filename}`));
3873
+ console.log(chalk2.dim(`Plugins directory: ${PLUGINS_DIR}`));
3874
+ process.exit(1);
3875
+ }
3876
+ unlinkSync(target);
3877
+ console.log(chalk2.green(`\u2713 Removed: ${filename}`));
3878
+ });
3879
+ var cacheCmd = program.command("cache").description("Inspect and manage the context cache");
3880
+ cacheCmd.command("stats").description("Show cache hit rate, entries, and LRU sizes").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3881
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3882
+ const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3883
+ const store = await getStore2(pathResolve(opts.project));
3884
+ try {
3885
+ ContextCache.ensureTables(store);
3886
+ const cache = getContextCache();
3887
+ const s = cache.getStats(store);
3888
+ const hitRatePct = (s.hitRate * 100).toFixed(1);
3889
+ const totalOps = s.totalHits + s.totalMisses;
3890
+ console.log(chalk2.bold("Cache stats"));
3891
+ console.log(chalk2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3892
+ console.log(
3893
+ ` ${chalk2.dim("Hit rate")} ${chalk2.green(chalk2.bold(hitRatePct + "%"))} ${chalk2.dim(
3894
+ `(${s.totalHits} hits / ${totalOps} ops)`
3895
+ )}`
3896
+ );
3897
+ console.log(
3898
+ ` ${chalk2.dim("Query cache")} ${chalk2.bold(String(s.queryEntries))} entries, ${chalk2.green(
3899
+ String(s.queryHits)
3900
+ )} hits, ${chalk2.dim(String(s.queryMisses) + " miss")}`
3901
+ );
3902
+ console.log(
3903
+ ` ${chalk2.dim("Pattern cache")} ${chalk2.bold(String(s.patternEntries))} entries, ${chalk2.green(
3904
+ String(s.patternHits)
3905
+ )} hits, ${chalk2.dim(String(s.patternMisses) + " miss")}`
3906
+ );
3907
+ console.log(
3908
+ ` ${chalk2.dim("Hot files")} ${chalk2.bold(String(s.hotFileCount))} warmed`
3909
+ );
3910
+ } finally {
3911
+ store.close();
3912
+ }
3913
+ });
3914
+ cacheCmd.command("clear").description("Flush all cache layers (query, pattern, hot files)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3915
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3916
+ const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3917
+ const store = await getStore2(pathResolve(opts.project));
3918
+ try {
3919
+ ContextCache.ensureTables(store);
3920
+ const cache = getContextCache();
3921
+ const before = cache.getStats(store);
3922
+ cache.clearAll(store);
3923
+ store.save();
3924
+ console.log(
3925
+ chalk2.green(
3926
+ `\u2713 Cleared ${before.queryEntries} query entries, ${before.patternEntries} pattern entries`
3927
+ )
3928
+ );
3929
+ } finally {
3930
+ store.close();
3931
+ }
3932
+ });
3933
+ 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) => {
3934
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3935
+ const { getContextCache, ContextCache } = await import("./cache-AK6CF3BC.js");
3936
+ const store = await getStore2(pathResolve(opts.project));
3937
+ try {
3938
+ ContextCache.ensureTables(store);
3939
+ const cache = getContextCache();
3940
+ const topN = parseInt(opts.limit, 10) || 20;
3941
+ const count = cache.warmHotFiles(store, pathResolve(opts.project), topN);
3942
+ if (count === 0) {
3943
+ console.log(
3944
+ chalk2.dim(
3945
+ "No files to warm. Cache is empty \u2014 run a few Read-intercepted sessions first."
3946
+ )
3947
+ );
3948
+ } else {
3949
+ console.log(chalk2.green(`\u2713 Warmed ${count} hot file${count === 1 ? "" : "s"} into LRU`));
3950
+ }
3951
+ } finally {
3952
+ store.close();
3953
+ }
3954
+ });
3833
3955
  program.parse();