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.
- package/CHANGELOG.md +118 -0
- package/README.md +67 -7
- package/dist/{aider-context-TNGSXMVY.js → aider-context-BC5R2ZTA.js} +1 -1
- package/dist/cache-AK6CF3BC.js +10 -0
- package/dist/chunk-22INHMKB.js +31 -0
- package/dist/chunk-533LR4I7.js +220 -0
- package/dist/{chunk-QOG4K427.js → chunk-C6GBUOAL.js} +1 -1
- package/dist/chunk-CIQQ5Y3S.js +338 -0
- package/dist/chunk-KL6NSPVA.js +59 -0
- package/dist/{chunk-SBHGK5WA.js → chunk-PEH54LYC.js} +85 -3
- package/dist/{chunk-6SFMVYUN.js → chunk-SJT7VS2G.js} +127 -23
- package/dist/cli.js +381 -264
- package/dist/{core-77MHT3QV.js → core-6IY5L6II.js} +2 -2
- package/dist/{cursor-mdc-HWVUZUZH.js → cursor-mdc-GJ7E5LDD.js} +1 -1
- package/dist/{exporter-A3VSLS4U.js → exporter-GWU2GF23.js} +1 -1
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/tree-sitter-rust.wasm +0 -0
- package/dist/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/{importer-MCNFMV5O.js → importer-V62NGZRK.js} +4 -3
- package/dist/index.js +3 -3
- package/dist/{migrate-5ZJWF2HD.js → migrate-UKCO6BUU.js} +3 -1
- package/dist/plugin-loader-FCOMVOX7.js +100 -0
- package/dist/serve.js +2 -2
- package/dist/server-VBRTTECZ.js +1363 -0
- package/dist/{tuner-2LVIEE5V.js → tuner-KFNNGKG3.js} +4 -2
- package/dist/windsurf-rules-C7SVDHBL.js +59 -0
- package/package.json +4 -3
- package/dist/chunk-CEAANHHX.js +0 -88
- 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-
|
|
15
|
+
} from "./chunk-KL6NSPVA.js";
|
|
7
16
|
import {
|
|
8
17
|
autogen,
|
|
9
18
|
install,
|
|
10
19
|
status,
|
|
11
20
|
uninstall
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
30
|
-
import "./chunk-
|
|
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
|
|
37
|
-
readFileSync as
|
|
38
|
-
writeFileSync as
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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-
|
|
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
|
|
2849
|
-
readFileSync as
|
|
2850
|
-
writeFileSync
|
|
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
|
|
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 =
|
|
2754
|
+
const memoryPath = join8(projectRoot, "MEMORY.md");
|
|
2926
2755
|
try {
|
|
2927
2756
|
let existing = "";
|
|
2928
|
-
if (
|
|
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 =
|
|
2762
|
+
existing = readFileSync4(memoryPath, "utf-8");
|
|
2934
2763
|
}
|
|
2935
2764
|
const updated = upsertEngramSection(existing, engramSection);
|
|
2936
2765
|
const tmpPath = memoryPath + ".engram-tmp";
|
|
2937
|
-
|
|
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").
|
|
2958
|
-
|
|
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 =
|
|
2987
|
-
const projectSettings =
|
|
2988
|
-
const hasHooks =
|
|
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-
|
|
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 =
|
|
3045
|
-
if (!
|
|
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 (
|
|
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 =
|
|
3078
|
-
if (!
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
3143
|
+
return join9(absProject, ".claude", "settings.local.json");
|
|
3287
3144
|
case "project":
|
|
3288
|
-
return
|
|
3145
|
+
return join9(absProject, ".claude", "settings.json");
|
|
3289
3146
|
case "user":
|
|
3290
|
-
return
|
|
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 (
|
|
3241
|
+
if (existsSync9(settingsPath)) {
|
|
3385
3242
|
try {
|
|
3386
|
-
const raw =
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
3463
|
+
const flagPath = join9(projectRoot, ".engram", "hook-disabled");
|
|
3607
3464
|
try {
|
|
3608
|
-
|
|
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 =
|
|
3631
|
-
if (!
|
|
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 =
|
|
3674
|
-
if (
|
|
3675
|
-
const content =
|
|
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: ${
|
|
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:
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
3798
|
-
const { CURRENT_SCHEMA_VERSION, getSchemaVersion } = await import("./migrate-
|
|
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-
|
|
3815
|
-
const { runMigrations } = await import("./migrate-
|
|
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 =
|
|
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();
|