engramx 1.0.0 → 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 +381 -264
  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-MCNFMV5O.js → importer-V62NGZRK.js} +4 -3
  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,22 +35,23 @@ 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";
54
+ import { fileURLToPath as fileURLToPath2 } from "url";
45
55
  import { homedir } from "os";
46
56
 
47
57
  // src/intercept/safety.ts
@@ -407,8 +417,13 @@ function findGrammarWasm(lang) {
407
417
  const candidates = [];
408
418
  try {
409
419
  const here = dirname2(fileURLToPath(import.meta.url));
410
- candidates.push(join3(here, "..", "..", "node_modules", pkg, wasmName));
411
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));
412
427
  } catch {
413
428
  }
414
429
  try {
@@ -1324,7 +1339,7 @@ var lspProvider = {
1324
1339
  };
1325
1340
 
1326
1341
  // src/providers/resolver.ts
1327
- var ALL_PROVIDERS = [
1342
+ var BUILTIN_PROVIDERS = [
1328
1343
  astProvider,
1329
1344
  structureProvider,
1330
1345
  mistakesProvider,
@@ -1334,12 +1349,26 @@ var ALL_PROVIDERS = [
1334
1349
  obsidianProvider,
1335
1350
  lspProvider
1336
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;
1337
1360
  function estimateTokens(text) {
1338
1361
  return Math.ceil(text.length / 4);
1339
1362
  }
1340
1363
  async function resolveRichPacket(filePath, context, enabledProviders) {
1341
1364
  const start = Date.now();
1342
- 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) => {
1343
1372
  if (enabledProviders && !enabledProviders.includes(p.name)) return false;
1344
1373
  return true;
1345
1374
  });
@@ -1371,7 +1400,7 @@ async function resolveRichPacket(filePath, context, enabledProviders) {
1371
1400
  if (totalTokens + sectionTokens > budget) {
1372
1401
  break;
1373
1402
  }
1374
- const provider = ALL_PROVIDERS.find((p) => p.name === result.provider);
1403
+ const provider = allProviders.find((p) => p.name === result.provider);
1375
1404
  const label = provider?.label ?? result.provider.toUpperCase();
1376
1405
  const cacheTag = result.cached ? ", cached" : "";
1377
1406
  sections.push(`${label} (${result.provider}${cacheTag}):
@@ -1405,7 +1434,7 @@ async function warmAllProviders(projectRoot, enabledProviders) {
1405
1434
  try {
1406
1435
  const result = await withTimeout2(p.warmup(projectRoot), 5e3);
1407
1436
  if (result && result.entries.length > 0) {
1408
- const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
1437
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
1409
1438
  const store = await getStore2(projectRoot);
1410
1439
  try {
1411
1440
  store.warmCache(
@@ -2313,98 +2342,6 @@ function watchProject(projectRoot, options = {}) {
2313
2342
  import chalk from "chalk";
2314
2343
  import { existsSync as existsSync7, statSync as statSync3 } from "fs";
2315
2344
  import { join as join7, resolve as resolve6, basename as basename4 } from "path";
2316
-
2317
- // src/intercept/stats.ts
2318
- var ESTIMATED_TOKENS_PER_READ_DENY = 1200;
2319
- function summarizeHookLog(entries) {
2320
- const byEvent = {};
2321
- const byTool = {};
2322
- const byDecision = {};
2323
- let readDenyCount = 0;
2324
- let firstEntryTs = null;
2325
- let lastEntryTs = null;
2326
- for (const entry of entries) {
2327
- const event = entry.event ?? "unknown";
2328
- byEvent[event] = (byEvent[event] ?? 0) + 1;
2329
- const tool = entry.tool ?? "unknown";
2330
- byTool[tool] = (byTool[tool] ?? 0) + 1;
2331
- if (entry.decision) {
2332
- byDecision[entry.decision] = (byDecision[entry.decision] ?? 0) + 1;
2333
- }
2334
- if (event === "PreToolUse" && tool === "Read" && entry.decision === "deny") {
2335
- readDenyCount += 1;
2336
- }
2337
- const ts = entry.ts;
2338
- if (typeof ts === "string") {
2339
- if (firstEntryTs === null || ts < firstEntryTs) firstEntryTs = ts;
2340
- if (lastEntryTs === null || ts > lastEntryTs) lastEntryTs = ts;
2341
- }
2342
- }
2343
- return {
2344
- totalInvocations: entries.length,
2345
- byEvent: Object.freeze(byEvent),
2346
- byTool: Object.freeze(byTool),
2347
- byDecision: Object.freeze(byDecision),
2348
- readDenyCount,
2349
- estimatedTokensSaved: readDenyCount * ESTIMATED_TOKENS_PER_READ_DENY,
2350
- firstEntry: firstEntryTs,
2351
- lastEntry: lastEntryTs
2352
- };
2353
- }
2354
- function formatStatsSummary(summary) {
2355
- if (summary.totalInvocations === 0) {
2356
- return "engram hook stats: no log entries yet.\n\nRun engram install-hook in a project, then use Claude Code to see interceptions.";
2357
- }
2358
- const lines = [];
2359
- lines.push(`engram hook stats (${summary.totalInvocations} invocations)`);
2360
- 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");
2361
- if (summary.firstEntry && summary.lastEntry) {
2362
- lines.push(`Time range: ${summary.firstEntry} \u2192 ${summary.lastEntry}`);
2363
- lines.push("");
2364
- }
2365
- lines.push("By event:");
2366
- const eventEntries = Object.entries(summary.byEvent).sort(
2367
- (a, b) => b[1] - a[1]
2368
- );
2369
- for (const [event, count] of eventEntries) {
2370
- const pct = (count / summary.totalInvocations * 100).toFixed(1);
2371
- lines.push(` ${event.padEnd(18)} ${String(count).padStart(5)} (${pct}%)`);
2372
- }
2373
- lines.push("");
2374
- lines.push("By tool:");
2375
- const toolEntries = Object.entries(summary.byTool).filter(([k]) => k !== "unknown").sort((a, b) => b[1] - a[1]);
2376
- for (const [tool, count] of toolEntries) {
2377
- lines.push(` ${tool.padEnd(18)} ${String(count).padStart(5)}`);
2378
- }
2379
- if (toolEntries.length === 0) {
2380
- lines.push(" (no tool-tagged entries)");
2381
- }
2382
- lines.push("");
2383
- const decisionEntries = Object.entries(summary.byDecision);
2384
- if (decisionEntries.length > 0) {
2385
- lines.push("PreToolUse decisions:");
2386
- for (const [decision, count] of decisionEntries.sort(
2387
- (a, b) => b[1] - a[1]
2388
- )) {
2389
- lines.push(` ${decision.padEnd(18)} ${String(count).padStart(5)}`);
2390
- }
2391
- lines.push("");
2392
- }
2393
- if (summary.readDenyCount > 0) {
2394
- lines.push(
2395
- `Estimated tokens saved: ~${summary.estimatedTokensSaved.toLocaleString()}`
2396
- );
2397
- lines.push(
2398
- ` (${summary.readDenyCount} Read denies \xD7 ${ESTIMATED_TOKENS_PER_READ_DENY} tok/deny avg)`
2399
- );
2400
- } else {
2401
- lines.push("Estimated tokens saved: 0");
2402
- lines.push(" (no PreToolUse:Read denies recorded yet)");
2403
- }
2404
- return lines.join("\n");
2405
- }
2406
-
2407
- // src/dashboard.ts
2408
2345
  var AMBER = chalk.hex("#d97706");
2409
2346
  var DIM = chalk.dim;
2410
2347
  var GREEN = chalk.green;
@@ -2735,123 +2672,15 @@ function formatInstallDiff(before, after) {
2735
2672
  return lines.length > 0 ? lines.join("\n") : "(no changes)";
2736
2673
  }
2737
2674
 
2738
- // src/intercept/component-status.ts
2739
- import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
2740
- import { join as join8 } from "path";
2741
- function statusPath(projectRoot) {
2742
- return join8(projectRoot, ".engram", "component-status.json");
2743
- }
2744
- function readCachedStatus(projectRoot) {
2745
- const path2 = statusPath(projectRoot);
2746
- if (!existsSync8(path2)) return null;
2747
- try {
2748
- const raw = JSON.parse(readFileSync4(path2, "utf-8"));
2749
- if (Date.now() - raw.generatedAt > 3e4) return null;
2750
- return raw;
2751
- } catch {
2752
- return null;
2753
- }
2754
- }
2755
- function checkHttp(projectRoot) {
2756
- return existsSync8(join8(projectRoot, ".engram", "http-server.pid"));
2757
- }
2758
- function checkLsp(projectRoot) {
2759
- if (existsSync8(join8(projectRoot, ".engram", "lsp-available"))) return true;
2760
- const candidates = [
2761
- "/tmp/tsserver.sock",
2762
- "/tmp/typescript-language-server.sock"
2763
- ];
2764
- return candidates.some((c) => existsSync8(c));
2765
- }
2766
- function checkAst(projectRoot) {
2767
- const grammarsDir = join8(projectRoot, "node_modules", "web-tree-sitter");
2768
- return existsSync8(grammarsDir);
2769
- }
2770
- function countIdeAdapters(projectRoot) {
2771
- let count = 0;
2772
- if (existsSync8(join8(projectRoot, ".cursor", "rules", "engram-context.mdc"))) {
2773
- count += 1;
2774
- }
2775
- const continueConfig = join8(
2776
- process.env.HOME ?? "",
2777
- ".continue",
2778
- "config.json"
2779
- );
2780
- if (existsSync8(continueConfig)) {
2781
- try {
2782
- const cfg = readFileSync4(continueConfig, "utf-8");
2783
- if (cfg.includes("engram")) count += 1;
2784
- } catch {
2785
- }
2786
- }
2787
- const zedSettings = join8(
2788
- process.env.HOME ?? "",
2789
- ".config",
2790
- "zed",
2791
- "settings.json"
2792
- );
2793
- if (existsSync8(zedSettings)) {
2794
- try {
2795
- const cfg = readFileSync4(zedSettings, "utf-8");
2796
- if (cfg.includes("engram")) count += 1;
2797
- } catch {
2798
- }
2799
- }
2800
- const claudeSettings = join8(projectRoot, ".claude", "settings.local.json");
2801
- if (existsSync8(claudeSettings)) {
2802
- try {
2803
- const cfg = readFileSync4(claudeSettings, "utf-8");
2804
- if (cfg.includes("engram")) count += 1;
2805
- } catch {
2806
- }
2807
- }
2808
- return count;
2809
- }
2810
- function refreshComponentStatus(projectRoot) {
2811
- const now = Date.now();
2812
- const components = [
2813
- { name: "http", available: checkHttp(projectRoot), checkedAt: now },
2814
- { name: "lsp", available: checkLsp(projectRoot), checkedAt: now },
2815
- { name: "ast", available: checkAst(projectRoot), checkedAt: now }
2816
- ];
2817
- const ideCount = countIdeAdapters(projectRoot);
2818
- const report = {
2819
- components,
2820
- ideCount,
2821
- generatedAt: now
2822
- };
2823
- try {
2824
- writeFileSync(statusPath(projectRoot), JSON.stringify(report), "utf-8");
2825
- } catch {
2826
- }
2827
- return report;
2828
- }
2829
- function getComponentStatus(projectRoot) {
2830
- const cached = readCachedStatus(projectRoot);
2831
- if (cached) return cached;
2832
- return refreshComponentStatus(projectRoot);
2833
- }
2834
- function formatHudStatus(report) {
2835
- const parts = [];
2836
- for (const c of report.components) {
2837
- const icon = c.available ? "\u2713" : "\u2717";
2838
- parts.push(`${c.name.toUpperCase()} ${icon}`);
2839
- }
2840
- if (report.ideCount > 0) {
2841
- parts.push(`${report.ideCount} IDE${report.ideCount > 1 ? "s" : ""}`);
2842
- }
2843
- return parts.join(" | ");
2844
- }
2845
-
2846
2675
  // src/intercept/memory-md.ts
2847
2676
  import {
2848
- existsSync as existsSync9,
2849
- readFileSync as readFileSync5,
2850
- writeFileSync as writeFileSync2,
2677
+ existsSync as existsSync8,
2678
+ readFileSync as readFileSync4,
2679
+ writeFileSync,
2851
2680
  renameSync,
2852
2681
  statSync as statSync4
2853
2682
  } from "fs";
2854
- import { join as join9 } from "path";
2683
+ import { join as join8 } from "path";
2855
2684
  var ENGRAM_MARKER_START = "<!-- engram:structural-facts:start -->";
2856
2685
  var ENGRAM_MARKER_END = "<!-- engram:structural-facts:end -->";
2857
2686
  var MAX_MEMORY_FILE_BYTES = 1e6;
@@ -2922,19 +2751,19 @@ function writeEngramSectionToMemoryMd(projectRoot, engramSection) {
2922
2751
  if (engramSection.length > MAX_ENGRAM_SECTION_BYTES) {
2923
2752
  return false;
2924
2753
  }
2925
- const memoryPath = join9(projectRoot, "MEMORY.md");
2754
+ const memoryPath = join8(projectRoot, "MEMORY.md");
2926
2755
  try {
2927
2756
  let existing = "";
2928
- if (existsSync9(memoryPath)) {
2757
+ if (existsSync8(memoryPath)) {
2929
2758
  const st = statSync4(memoryPath);
2930
2759
  if (st.size > MAX_MEMORY_FILE_BYTES) {
2931
2760
  return false;
2932
2761
  }
2933
- existing = readFileSync5(memoryPath, "utf-8");
2762
+ existing = readFileSync4(memoryPath, "utf-8");
2934
2763
  }
2935
2764
  const updated = upsertEngramSection(existing, engramSection);
2936
2765
  const tmpPath = memoryPath + ".engram-tmp";
2937
- writeFileSync2(tmpPath, updated);
2766
+ writeFileSync(tmpPath, updated);
2938
2767
  renameSync(tmpPath, memoryPath);
2939
2768
  return true;
2940
2769
  } catch {
@@ -2954,10 +2783,14 @@ program.name("engram").description(
2954
2783
  program.command("init").description("Scan codebase and build knowledge graph (zero LLM cost)").argument("[path]", "Project directory", ".").option(
2955
2784
  "--with-skills [dir]",
2956
2785
  "Also index Claude Code skills from ~/.claude/skills/ or a given path"
2957
- ).option("--from-ccs", "Import .context/index.md (CCS) into graph after init").action(async (projectPath, opts) => {
2958
- 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..."));
2959
2791
  const result = await init(projectPath, {
2960
- withSkills: opts.withSkills
2792
+ withSkills: opts.withSkills,
2793
+ incremental: opts.incremental
2961
2794
  });
2962
2795
  console.log(
2963
2796
  chalk2.green("\u{1F333} AST extraction complete") + chalk2.dim(` (${result.timeMs}ms, 0 tokens used)`)
@@ -2965,6 +2798,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2965
2798
  console.log(
2966
2799
  ` ${chalk2.bold(String(result.nodes))} nodes, ${chalk2.bold(String(result.edges))} edges from ${chalk2.bold(String(result.fileCount))} files (${result.totalLines.toLocaleString()} lines)`
2967
2800
  );
2801
+ if (result.incremental && result.skippedFiles && result.skippedFiles > 0) {
2802
+ console.log(chalk2.dim(` ${result.skippedFiles} unchanged files skipped (incremental mode)`));
2803
+ }
2968
2804
  if (result.skillCount && result.skillCount > 0) {
2969
2805
  console.log(
2970
2806
  chalk2.cyan(` ${chalk2.bold(String(result.skillCount))} skills indexed`)
@@ -2983,9 +2819,9 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2983
2819
  console.log(chalk2.green("\n\u2705 Ready. Your AI now has persistent memory."));
2984
2820
  console.log(chalk2.dim(" Graph stored in .engram/graph.db"));
2985
2821
  const resolvedProject = pathResolve(projectPath);
2986
- const localSettings = join10(resolvedProject, ".claude", "settings.local.json");
2987
- const projectSettings = join10(resolvedProject, ".claude", "settings.json");
2988
- const hasHooks = existsSync10(localSettings) && readFileSync6(localSettings, "utf-8").includes("engram intercept") || existsSync10(projectSettings) && readFileSync6(projectSettings, "utf-8").includes("engram intercept");
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");
2989
2825
  if (!hasHooks) {
2990
2826
  console.log(
2991
2827
  chalk2.yellow("\n\u{1F4A1} Next step: ") + chalk2.white("engram install-hook") + chalk2.dim(
@@ -2999,7 +2835,7 @@ program.command("init").description("Scan codebase and build knowledge graph (ze
2999
2835
  );
3000
2836
  }
3001
2837
  if (opts.fromCcs) {
3002
- const { importCcs } = await import("./importer-MCNFMV5O.js");
2838
+ const { importCcs } = await import("./importer-V62NGZRK.js");
3003
2839
  const resolvedProjectPath = pathResolve(projectPath);
3004
2840
  const ccsResult = await importCcs(resolvedProjectPath);
3005
2841
  if (ccsResult.nodesCreated > 0) {
@@ -3041,8 +2877,8 @@ program.command("watch").description("Watch project for file changes and re-inde
3041
2877
  });
3042
2878
  program.command("dashboard").alias("hud").description("Live terminal dashboard showing hook activity and token savings").argument("[path]", "Project directory", ".").action(async (projectPath) => {
3043
2879
  const resolvedPath = pathResolve(projectPath);
3044
- const dbPath = join10(resolvedPath, ".engram", "graph.db");
3045
- if (!existsSync10(dbPath)) {
2880
+ const dbPath = join9(resolvedPath, ".engram", "graph.db");
2881
+ if (!existsSync9(dbPath)) {
3046
2882
  console.error(
3047
2883
  chalk2.red("No engram graph found at ") + chalk2.white(resolvedPath)
3048
2884
  );
@@ -3062,7 +2898,7 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
3062
2898
  let resolvedPath = pathResolve(projectPath);
3063
2899
  let found = false;
3064
2900
  for (let depth = 0; depth < 20; depth++) {
3065
- if (existsSync10(join10(resolvedPath, ".engram", "graph.db"))) {
2901
+ if (existsSync9(join9(resolvedPath, ".engram", "graph.db"))) {
3066
2902
  found = true;
3067
2903
  break;
3068
2904
  }
@@ -3074,8 +2910,8 @@ program.command("hud-label").description("Output JSON label for Claude HUD --ext
3074
2910
  console.log('{"label":""}');
3075
2911
  return;
3076
2912
  }
3077
- const logPath = join10(resolvedPath, ".engram", "hook-log.jsonl");
3078
- if (!existsSync10(logPath)) {
2913
+ const logPath = join9(resolvedPath, ".engram", "hook-log.jsonl");
2914
+ if (!existsSync9(logPath)) {
3079
2915
  console.log('{"label":"\u26A1engram \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 ready"}');
3080
2916
  return;
3081
2917
  }
@@ -3229,7 +3065,7 @@ program.command("gen").description("Generate CLAUDE.md / .cursorrules section fr
3229
3065
  }
3230
3066
  );
3231
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) => {
3232
- const { generateCursorMdc } = await import("./cursor-mdc-HWVUZUZH.js");
3068
+ const { generateCursorMdc } = await import("./cursor-mdc-GJ7E5LDD.js");
3233
3069
  const result = await generateCursorMdc(opts.project);
3234
3070
  console.log(
3235
3071
  chalk2.green(
@@ -3250,7 +3086,7 @@ program.command("gen-mdc").description("Generate .cursor/rules/engram-context.md
3250
3086
  }
3251
3087
  });
3252
3088
  program.command("gen-ccs").description("Export knowledge graph as .context/index.md (CCS format)").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3253
- const { exportCcs } = await import("./exporter-A3VSLS4U.js");
3089
+ const { exportCcs } = await import("./exporter-GWU2GF23.js");
3254
3090
  const result = await exportCcs(pathResolve(opts.project));
3255
3091
  console.log(
3256
3092
  chalk2.green(
@@ -3259,7 +3095,7 @@ program.command("gen-ccs").description("Export knowledge graph as .context/index
3259
3095
  );
3260
3096
  });
3261
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) => {
3262
- const { generateAiderContext } = await import("./aider-context-TNGSXMVY.js");
3098
+ const { generateAiderContext } = await import("./aider-context-BC5R2ZTA.js");
3263
3099
  const result = await generateAiderContext(pathResolve(opts.project));
3264
3100
  console.log(
3265
3101
  chalk2.green(
@@ -3279,15 +3115,36 @@ program.command("gen-aider").description("Generate .aider-context.md from knowle
3279
3115
  });
3280
3116
  }
3281
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
+ });
3282
3139
  function resolveSettingsPath(scope, projectPath) {
3283
3140
  const absProject = pathResolve(projectPath);
3284
3141
  switch (scope) {
3285
3142
  case "local":
3286
- return join10(absProject, ".claude", "settings.local.json");
3143
+ return join9(absProject, ".claude", "settings.local.json");
3287
3144
  case "project":
3288
- return join10(absProject, ".claude", "settings.json");
3145
+ return join9(absProject, ".claude", "settings.json");
3289
3146
  case "user":
3290
- return join10(homedir(), ".claude", "settings.json");
3147
+ return join9(homedir(), ".claude", "settings.json");
3291
3148
  default:
3292
3149
  return null;
3293
3150
  }
@@ -3381,9 +3238,9 @@ program.command("install-hook").description("Install engram hook entries into Cl
3381
3238
  process.exit(1);
3382
3239
  }
3383
3240
  let existing = {};
3384
- if (existsSync10(settingsPath)) {
3241
+ if (existsSync9(settingsPath)) {
3385
3242
  try {
3386
- const raw = readFileSync6(settingsPath, "utf-8");
3243
+ const raw = readFileSync5(settingsPath, "utf-8");
3387
3244
  existing = raw.trim() ? JSON.parse(raw) : {};
3388
3245
  } catch (err) {
3389
3246
  console.error(
@@ -3429,13 +3286,13 @@ program.command("install-hook").description("Install engram hook entries into Cl
3429
3286
  }
3430
3287
  try {
3431
3288
  mkdirSync(dirname4(settingsPath), { recursive: true });
3432
- if (existsSync10(settingsPath)) {
3289
+ if (existsSync9(settingsPath)) {
3433
3290
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
3434
3291
  copyFileSync(settingsPath, backupPath);
3435
3292
  console.log(chalk2.dim(` Backup: ${backupPath}`));
3436
3293
  }
3437
3294
  const tmpPath = settingsPath + ".engram-tmp";
3438
- writeFileSync3(
3295
+ writeFileSync2(
3439
3296
  tmpPath,
3440
3297
  JSON.stringify(result.updated, null, 2) + "\n"
3441
3298
  );
@@ -3480,7 +3337,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3480
3337
  console.error(chalk2.red(`Unknown scope: ${opts.scope}`));
3481
3338
  process.exit(1);
3482
3339
  }
3483
- if (!existsSync10(settingsPath)) {
3340
+ if (!existsSync9(settingsPath)) {
3484
3341
  console.log(
3485
3342
  chalk2.yellow(`No settings file at ${settingsPath} \u2014 nothing to remove.`)
3486
3343
  );
@@ -3488,7 +3345,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3488
3345
  }
3489
3346
  let existing;
3490
3347
  try {
3491
- const raw = readFileSync6(settingsPath, "utf-8");
3348
+ const raw = readFileSync5(settingsPath, "utf-8");
3492
3349
  existing = raw.trim() ? JSON.parse(raw) : {};
3493
3350
  } catch (err) {
3494
3351
  console.error(
@@ -3508,7 +3365,7 @@ program.command("uninstall-hook").description("Remove engram hook entries from C
3508
3365
  const backupPath = `${settingsPath}.engram-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
3509
3366
  copyFileSync(settingsPath, backupPath);
3510
3367
  const tmpPath = settingsPath + ".engram-tmp";
3511
- writeFileSync3(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
3368
+ writeFileSync2(tmpPath, JSON.stringify(result.updated, null, 2) + "\n");
3512
3369
  renameSync2(tmpPath, settingsPath);
3513
3370
  if (result.removed.length > 0) {
3514
3371
  console.log(
@@ -3603,9 +3460,9 @@ program.command("hook-disable").description("Disable engram hooks via kill switc
3603
3460
  console.error(chalk2.dim("Run 'engram init' first."));
3604
3461
  process.exit(1);
3605
3462
  }
3606
- const flagPath = join10(projectRoot, ".engram", "hook-disabled");
3463
+ const flagPath = join9(projectRoot, ".engram", "hook-disabled");
3607
3464
  try {
3608
- writeFileSync3(flagPath, (/* @__PURE__ */ new Date()).toISOString());
3465
+ writeFileSync2(flagPath, (/* @__PURE__ */ new Date()).toISOString());
3609
3466
  console.log(
3610
3467
  chalk2.green(`\u2705 engram hooks disabled for ${projectRoot}`)
3611
3468
  );
@@ -3627,8 +3484,8 @@ program.command("hook-enable").description("Re-enable engram hooks (remove kill
3627
3484
  console.error(chalk2.red(`Not an engram project: ${absProject}`));
3628
3485
  process.exit(1);
3629
3486
  }
3630
- const flagPath = join10(projectRoot, ".engram", "hook-disabled");
3631
- if (!existsSync10(flagPath)) {
3487
+ const flagPath = join9(projectRoot, ".engram", "hook-disabled");
3488
+ if (!existsSync9(flagPath)) {
3632
3489
  console.log(
3633
3490
  chalk2.yellow(`engram hooks already enabled for ${projectRoot}`)
3634
3491
  );
@@ -3670,9 +3527,9 @@ program.command("memory-sync").description(
3670
3527
  }
3671
3528
  let branch = null;
3672
3529
  try {
3673
- const headPath = join10(projectRoot, ".git", "HEAD");
3674
- if (existsSync10(headPath)) {
3675
- 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();
3676
3533
  const m = content.match(/^ref:\s+refs\/heads\/(.+)$/);
3677
3534
  if (m) branch = m[1];
3678
3535
  }
@@ -3698,7 +3555,7 @@ program.command("memory-sync").description(
3698
3555
  \u{1F4DD} engram memory-sync`)
3699
3556
  );
3700
3557
  console.log(
3701
- chalk2.dim(` Target: ${join10(projectRoot, "MEMORY.md")}`)
3558
+ chalk2.dim(` Target: ${join9(projectRoot, "MEMORY.md")}`)
3702
3559
  );
3703
3560
  if (opts.dryRun) {
3704
3561
  console.log(chalk2.cyan("\n Section to write (dry-run):\n"));
@@ -3742,28 +3599,69 @@ program.command("stress-test").description("Run stress tests: memory, concurrenc
3742
3599
  if (opts.replay) args.push("--replay", opts.replay);
3743
3600
  if (opts.limit) args.push("--limit", String(opts.limit));
3744
3601
  try {
3745
- execFileSync2("npx", ["tsx", ...args], { stdio: "inherit", cwd: join10(dirname4(import.meta.url.replace("file://", "")), "..") });
3602
+ execFileSync2("npx", ["tsx", ...args], { stdio: "inherit", shell: true, cwd: join9(dirname4(fileURLToPath2(import.meta.url)), "..") });
3746
3603
  } catch {
3747
3604
  process.exit(1);
3748
3605
  }
3749
3606
  });
3750
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) => {
3751
- const { startHttpServer } = await import("./server-I3C74ZLB.js");
3608
+ const { startHttpServer } = await import("./server-VBRTTECZ.js");
3752
3609
  await startHttpServer(pathResolve(opts.project), parseInt(opts.port, 10));
3753
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
+ });
3754
3651
  program.command("context-server").description("Start Zed-compatible context server (JSON-RPC over stdio)").action(async () => {
3755
3652
  const { execFileSync: execFileSync2 } = await import("child_process");
3756
3653
  try {
3757
3654
  execFileSync2("npx", ["tsx", "adapters/zed/index.ts"], {
3758
3655
  stdio: "inherit",
3759
- cwd: join10(dirname4(import.meta.url.replace("file://", "")), "..")
3656
+ shell: true,
3657
+ cwd: join9(dirname4(fileURLToPath2(import.meta.url)), "..")
3760
3658
  });
3761
3659
  } catch {
3762
3660
  process.exit(1);
3763
3661
  }
3764
3662
  });
3765
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) => {
3766
- const { analyzeTuning, applyTuning } = await import("./tuner-2LVIEE5V.js");
3664
+ const { analyzeTuning, applyTuning } = await import("./tuner-KFNNGKG3.js");
3767
3665
  const proposal = analyzeTuning(pathResolve(opts.project));
3768
3666
  if (proposal.changes.length === 0) {
3769
3667
  console.log(
@@ -3794,8 +3692,8 @@ program.command("tune").description("Analyze hook-log and propose provider confi
3794
3692
  });
3795
3693
  var dbCmd = program.command("db").description("Database management");
3796
3694
  dbCmd.command("status").description("Show schema version and migration status").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3797
- const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
3798
- const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-5ZJWF2HD.js");
3695
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3696
+ const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-UKCO6BUU.js");
3799
3697
  const store = await getStore2(pathResolve(opts.project));
3800
3698
  try {
3801
3699
  const version = getSchemaVersion(store.db);
@@ -3811,11 +3709,11 @@ dbCmd.command("status").description("Show schema version and migration status").
3811
3709
  }
3812
3710
  });
3813
3711
  dbCmd.command("migrate").description("Run pending schema migrations").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3814
- const { getStore: getStore2 } = await import("./core-77MHT3QV.js");
3815
- const { runMigrations } = await import("./migrate-5ZJWF2HD.js");
3712
+ const { getStore: getStore2 } = await import("./core-6IY5L6II.js");
3713
+ const { runMigrations } = await import("./migrate-UKCO6BUU.js");
3816
3714
  const store = await getStore2(pathResolve(opts.project));
3817
3715
  try {
3818
- const dbPath = join10(pathResolve(opts.project), ".engram", "graph.db");
3716
+ const dbPath = join9(pathResolve(opts.project), ".engram", "graph.db");
3819
3717
  const result = runMigrations(
3820
3718
  store.db,
3821
3719
  dbPath
@@ -3835,4 +3733,223 @@ dbCmd.command("migrate").description("Run pending schema migrations").option("-p
3835
3733
  store.close();
3836
3734
  }
3837
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
+ });
3838
3955
  program.parse();