sverklo 0.16.0 → 0.17.2
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/README.md +38 -1
- package/dist/bin/sverklo.js +438 -5
- package/dist/bin/sverklo.js.map +1 -1
- package/dist/src/indexer/grammars-install.d.ts +28 -0
- package/dist/src/indexer/grammars-install.js +105 -0
- package/dist/src/indexer/grammars-install.js.map +1 -0
- package/dist/src/indexer/parser-tree-sitter.d.ts +8 -0
- package/dist/src/indexer/parser-tree-sitter.js +240 -0
- package/dist/src/indexer/parser-tree-sitter.js.map +1 -0
- package/dist/src/indexer/parser.d.ts +8 -0
- package/dist/src/indexer/parser.js +29 -0
- package/dist/src/indexer/parser.js.map +1 -1
- package/dist/src/init.js +1 -0
- package/dist/src/init.js.map +1 -1
- package/dist/src/memory/export.d.ts +49 -0
- package/dist/src/memory/export.js +254 -0
- package/dist/src/memory/export.js.map +1 -0
- package/dist/src/search/investigate.d.ts +13 -1
- package/dist/src/search/investigate.js +136 -0
- package/dist/src/search/investigate.js.map +1 -1
- package/dist/src/server/tools/recall.js +37 -7
- package/dist/src/server/tools/recall.js.map +1 -1
- package/dist/src/server/tools/remember.d.ts +5 -0
- package/dist/src/server/tools/remember.js +56 -3
- package/dist/src/server/tools/remember.js.map +1 -1
- package/dist/src/server/tools/review-format.d.ts +35 -0
- package/dist/src/server/tools/review-format.js +123 -0
- package/dist/src/server/tools/review-format.js.map +1 -0
- package/dist/src/storage/memory-store.js +4 -0
- package/dist/src/storage/memory-store.js.map +1 -1
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/workspace/memory.d.ts +43 -0
- package/dist/src/workspace/memory.js +98 -0
- package/dist/src/workspace/memory.js.map +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
<img src="./docs/logo.svg" alt="sverklo" width="280" height="79"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **The only code-intel MCP with a published benchmark and reproducible eval harness.**
|
|
6
6
|
> Local-first MCP server that gives Claude Code, Cursor, Windsurf, and Zed a real symbol graph, a blast-radius lens, and a git-pinned memory — so the agent stops guessing. MIT. Zero config. Your code never leaves the machine.
|
|
7
|
+
>
|
|
8
|
+
> [Paper (Zenodo, CC BY 4.0)](https://doi.org/10.5281/zenodo.19802051) · [bench:primitives](./benchmark/) — sverklo cuts agent context by **65 % vs grep** (255 vs 731 tokens per task, n=60) · [bench:swe](https://sverklo.com/blog/bench-swe-first-results/) — 38/65 perfect recall on 5 OSS repos, including the runs we lose.
|
|
7
9
|
|
|
8
10
|
[](https://www.npmjs.com/package/sverklo)
|
|
9
11
|
[](https://www.npmjs.com/package/sverklo)
|
|
10
12
|
[](LICENSE)
|
|
11
13
|
[](https://sverklo.com/report)
|
|
14
|
+
[](https://doi.org/10.5281/zenodo.19802051)
|
|
15
|
+
|
|
16
|
+

|
|
12
17
|
|
|
13
18
|

|
|
14
19
|
|
|
@@ -39,6 +44,18 @@ That's it. `sverklo init` auto-detects your installed AI coding agent (Claude Co
|
|
|
39
44
|
|
|
40
45
|
---
|
|
41
46
|
|
|
47
|
+
## What's new in 0.17
|
|
48
|
+
|
|
49
|
+
- **`npm run bench:swe`** — third-party-reproducible cross-repo eval. Clones 5 OSS repos (express, nestjs, vite, prisma, fastapi), runs 65 grounded questions, prints aggregated recall. PRs that add questions are welcome.
|
|
50
|
+
- **Tree-sitter parser opt-in.** `sverklo grammars install` (~3.5 MB across 6 languages) + `SVERKLO_PARSER=tree-sitter` and the indexer routes through real ASTs for TypeScript/TSX/JavaScript/Python/Go/Rust. Silent regex fallback when grammars aren't installed. v0.18 plan to flip the default lives in [docs/parser-parity.md](./docs/parser-parity.md).
|
|
51
|
+
- **Workspace shared memory.** `sverklo workspace memory <name> add/list/search` plus `sverklo_remember scope:"workspace"` from the agent — write a decision once, query it from every other repo in the workspace. `sverklo_recall` blends workspace results under project ones with a `[ws]` badge.
|
|
52
|
+
- **`sverklo memory export`** — markdown / Notion / JSON. Migrate your team's decision log to wherever it actually lives.
|
|
53
|
+
- **PR-bot inline review.** `sverklo review --format github-review-json` + the action's new `inline-comments: true` default posts per-line review comments via `pulls.createReview`, alongside the existing sticky summary.
|
|
54
|
+
- **VS Code extension scaffold** at [`extensions/vscode/`](./extensions/vscode/) with a pre-built `sverklo-vscode-0.1.0.vsix`. Inline caller-count decorations on every function header (`⟵ 47 callers`). Marketplace publish workflow ships dormant; install with `code --install-extension extensions/vscode/sverklo-vscode-0.1.0.vsix` today.
|
|
55
|
+
- **`sverklo digest [--since 7d]`** — 5-line summary of audit-grade trend, new vs stale memories, and high-PageRank files touched. Wire into a shell-hook on `cd` for a daily sverklo check-in.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
42
59
|
## Grep vs Sverklo — the same question, side by side
|
|
43
60
|
|
|
44
61
|
Every one of these is a query a real engineer asked a real AI assistant last week. Grep gives you lines. Sverklo gives you a ranked answer.
|
|
@@ -349,6 +366,26 @@ The open-core line: **Pro adds new things, never gates current things.** Anythin
|
|
|
349
366
|
- [Issues](https://github.com/sverklo/sverklo/issues)
|
|
350
367
|
- [First Run Guide](FIRST_RUN.md)
|
|
351
368
|
- [Benchmarks](BENCHMARKS.md)
|
|
369
|
+
- [Paper (Zenodo, CC BY 4.0)](https://doi.org/10.5281/zenodo.19802051)
|
|
370
|
+
|
|
371
|
+
## Citing Sverklo
|
|
372
|
+
|
|
373
|
+
If you use Sverklo or its benchmarks (`bench:primitives`, `bench:swe`) in research, please cite:
|
|
374
|
+
|
|
375
|
+
> Groshin, N. (2026). *Sverklo: A Local-First Code Intelligence MCP Server and a Cross-Repository Software Engineering Benchmark*. Zenodo. https://doi.org/10.5281/zenodo.19802051
|
|
376
|
+
|
|
377
|
+
BibTeX:
|
|
378
|
+
|
|
379
|
+
```bibtex
|
|
380
|
+
@misc{groshin2026sverklo,
|
|
381
|
+
author = {Groshin, Nikita},
|
|
382
|
+
title = {{Sverklo}: A Local-First Code Intelligence {MCP} Server and a Cross-Repository Software Engineering Benchmark},
|
|
383
|
+
year = {2026},
|
|
384
|
+
publisher = {Zenodo},
|
|
385
|
+
doi = {10.5281/zenodo.19802051},
|
|
386
|
+
url = {https://doi.org/10.5281/zenodo.19802051}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
352
389
|
|
|
353
390
|
## License
|
|
354
391
|
|
package/dist/bin/sverklo.js
CHANGED
|
@@ -51,6 +51,8 @@ if (command && command !== "--help" && command !== "-h") {
|
|
|
51
51
|
dashboard: "Alias for `sverklo ui`.",
|
|
52
52
|
wakeup: "Print compressed project context (for system-prompt injection in non-MCP clients).",
|
|
53
53
|
digest: "5-line summary of what changed in this project. Flags: --since 7d, --format markdown|plain.",
|
|
54
|
+
memory: "Manage the memory store. Subcommands: show, edit, export.",
|
|
55
|
+
grammars: "Manage tree-sitter grammars for the SVERKLO_PARSER=tree-sitter opt-in path. Subcommands: install.",
|
|
54
56
|
"audit-prompt": "Print a ready-to-paste codebase-audit prompt (hybrid agent workflow).",
|
|
55
57
|
"review-prompt": "Print a ready-to-paste PR/MR-review prompt (hybrid agent workflow).",
|
|
56
58
|
bench: "Run reproducible benchmarks on gin/nestjs/react.",
|
|
@@ -64,7 +66,7 @@ if (command && command !== "--help" && command !== "-h") {
|
|
|
64
66
|
prune: "", // prune already prints its own --help inside the block
|
|
65
67
|
};
|
|
66
68
|
// Pass-throughs: subcommands that handle --help themselves.
|
|
67
|
-
const SELF_HANDLES_HELP = new Set(["prune"]);
|
|
69
|
+
const SELF_HANDLES_HELP = new Set(["prune", "memory"]);
|
|
68
70
|
if (!SELF_HANDLES_HELP.has(command)) {
|
|
69
71
|
const blurb = HELP_BLURBS[command];
|
|
70
72
|
if (blurb) {
|
|
@@ -327,6 +329,88 @@ if (command === "workspace") {
|
|
|
327
329
|
console.log(` · ${r.alias || ""} ${r.path}`);
|
|
328
330
|
process.exit(0);
|
|
329
331
|
}
|
|
332
|
+
if (sub === "memory") {
|
|
333
|
+
// sverklo workspace memory <name> <list|add|search|forget> [...]
|
|
334
|
+
//
|
|
335
|
+
// Per-workspace shared memory store at
|
|
336
|
+
// ~/.sverklo/workspaces/<name>/memories.db. CLI ships in v0.17;
|
|
337
|
+
// sverklo_remember scope:workspace is the v0.18 follow-up.
|
|
338
|
+
const name = args[2];
|
|
339
|
+
const op = args[3];
|
|
340
|
+
if (!name || !op) {
|
|
341
|
+
console.error("Usage: sverklo workspace memory <name> <list|add|search|forget> [args]");
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
const { openWorkspaceMemory, addWorkspaceMemory, searchWorkspaceMemory, workspaceMemoryExists, } = await import("../src/workspace/memory.js");
|
|
345
|
+
if (op === "add") {
|
|
346
|
+
const content = args.slice(4).join(" ");
|
|
347
|
+
if (!content) {
|
|
348
|
+
console.error('Usage: sverklo workspace memory <name> add "memory text"');
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
const ws = openWorkspaceMemory(name);
|
|
352
|
+
const id = addWorkspaceMemory(ws, { content });
|
|
353
|
+
console.log(`Saved workspace memory #${id} → ${ws.dbPath}`);
|
|
354
|
+
ws.close();
|
|
355
|
+
process.exit(0);
|
|
356
|
+
}
|
|
357
|
+
if (op === "list") {
|
|
358
|
+
if (!workspaceMemoryExists(name)) {
|
|
359
|
+
console.log(`No memories yet for workspace "${name}". Add one with:`);
|
|
360
|
+
console.log(` sverklo workspace memory ${name} add "your decision here"`);
|
|
361
|
+
process.exit(0);
|
|
362
|
+
}
|
|
363
|
+
const ws = openWorkspaceMemory(name);
|
|
364
|
+
const rows = ws.memoryStore.getAll(50);
|
|
365
|
+
console.log(`\nWorkspace "${name}" memories (${rows.length}):\n`);
|
|
366
|
+
for (const m of rows) {
|
|
367
|
+
const age = new Date(m.created_at).toISOString().slice(0, 10);
|
|
368
|
+
console.log(` #${m.id} [${m.category}/${m.kind}] ${age}`);
|
|
369
|
+
console.log(` ${m.content.replace(/\n/g, " ").slice(0, 100)}${m.content.length > 100 ? "…" : ""}`);
|
|
370
|
+
}
|
|
371
|
+
ws.close();
|
|
372
|
+
process.exit(0);
|
|
373
|
+
}
|
|
374
|
+
if (op === "search") {
|
|
375
|
+
const query = args.slice(4).join(" ");
|
|
376
|
+
if (!query) {
|
|
377
|
+
console.error('Usage: sverklo workspace memory <name> search "query"');
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
if (!workspaceMemoryExists(name)) {
|
|
381
|
+
console.log(`No memories for workspace "${name}".`);
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
const ws = openWorkspaceMemory(name);
|
|
385
|
+
const rows = searchWorkspaceMemory(ws, query, 20);
|
|
386
|
+
console.log(`\n${rows.length} match${rows.length === 1 ? "" : "es"}:\n`);
|
|
387
|
+
for (const m of rows) {
|
|
388
|
+
console.log(` #${m.id} [${m.category}/${m.kind}]`);
|
|
389
|
+
console.log(` ${m.content.replace(/\n/g, " ").slice(0, 200)}${m.content.length > 200 ? "…" : ""}`);
|
|
390
|
+
}
|
|
391
|
+
ws.close();
|
|
392
|
+
process.exit(0);
|
|
393
|
+
}
|
|
394
|
+
if (op === "forget") {
|
|
395
|
+
const idStr = args[4];
|
|
396
|
+
if (!idStr) {
|
|
397
|
+
console.error("Usage: sverklo workspace memory <name> forget <id>");
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const id = parseInt(idStr, 10);
|
|
401
|
+
if (!Number.isFinite(id)) {
|
|
402
|
+
console.error(`✗ "${idStr}" is not a valid id`);
|
|
403
|
+
process.exit(2);
|
|
404
|
+
}
|
|
405
|
+
const ws = openWorkspaceMemory(name);
|
|
406
|
+
const ok = ws.memoryStore.delete(id);
|
|
407
|
+
ws.close();
|
|
408
|
+
console.log(ok ? `Forgot memory #${id}.` : `Memory #${id} not found.`);
|
|
409
|
+
process.exit(ok ? 0 : 1);
|
|
410
|
+
}
|
|
411
|
+
console.error(`Unknown workspace memory op: ${op}`);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
330
414
|
console.log(`
|
|
331
415
|
sverklo workspace — manage multi-repo workspaces
|
|
332
416
|
|
|
@@ -621,7 +705,20 @@ if (command === "review") {
|
|
|
621
705
|
(process.env.GITHUB_BASE_REF
|
|
622
706
|
? `origin/${process.env.GITHUB_BASE_REF}..HEAD`
|
|
623
707
|
: "main..HEAD");
|
|
624
|
-
|
|
708
|
+
// Strip value-taking flags so `--format github-review-json` doesn't
|
|
709
|
+
// leave "github-review-json" looking like a positional path.
|
|
710
|
+
const valueFlags = new Set(["--ref", "--format", "--max-files", "--fail-on"]);
|
|
711
|
+
const cleanFlags = [];
|
|
712
|
+
for (let i = 0; i < flags.length; i++) {
|
|
713
|
+
if (valueFlags.has(flags[i])) {
|
|
714
|
+
i++;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`)))
|
|
718
|
+
continue;
|
|
719
|
+
cleanFlags.push(flags[i]);
|
|
720
|
+
}
|
|
721
|
+
const projectPath = await resolveProjectPath(cleanFlags);
|
|
625
722
|
// Ensure model is available
|
|
626
723
|
const { existsSync: modelExists } = await import("node:fs");
|
|
627
724
|
const { join: joinPath } = await import("node:path");
|
|
@@ -636,17 +733,34 @@ if (command === "review") {
|
|
|
636
733
|
const { getProjectConfig } = await import("../src/utils/config.js");
|
|
637
734
|
const { Indexer } = await import("../src/indexer/indexer.js");
|
|
638
735
|
const { handleReviewDiff } = await import("../src/server/tools/review-diff.js");
|
|
736
|
+
const { buildReviewJson } = await import("../src/server/tools/review-format.js");
|
|
639
737
|
const config = getProjectConfig(projectPath);
|
|
640
738
|
const indexer = new Indexer(config);
|
|
641
739
|
await indexer.index();
|
|
642
|
-
const
|
|
740
|
+
const reviewArgs = {
|
|
643
741
|
ref: effectiveRef,
|
|
644
742
|
max_files: maxFiles,
|
|
645
743
|
token_budget: 8000,
|
|
646
|
-
}
|
|
744
|
+
};
|
|
745
|
+
// Run early so we can both decide the threshold AND emit the format.
|
|
746
|
+
// For github-review-json we want the structured payload; for the
|
|
747
|
+
// other two formats we just need the markdown.
|
|
748
|
+
let markdown = "";
|
|
749
|
+
let structured = null;
|
|
750
|
+
if (format === "github-review-json") {
|
|
751
|
+
structured = buildReviewJson(indexer, reviewArgs);
|
|
752
|
+
if ("error" in structured) {
|
|
753
|
+
process.stderr.write(`✗ ${structured.error}\n`);
|
|
754
|
+
indexer.close();
|
|
755
|
+
process.exit(2);
|
|
756
|
+
}
|
|
757
|
+
markdown = structured.summary;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
markdown = handleReviewDiff(indexer, reviewArgs);
|
|
761
|
+
}
|
|
647
762
|
indexer.close();
|
|
648
763
|
if (format === "json") {
|
|
649
|
-
// Wrap the markdown in a JSON envelope with parsed risk level
|
|
650
764
|
const riskLevels = ["low", "medium", "high", "critical"];
|
|
651
765
|
let maxRisk = "low";
|
|
652
766
|
for (const level of riskLevels) {
|
|
@@ -660,6 +774,9 @@ if (command === "review") {
|
|
|
660
774
|
};
|
|
661
775
|
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
662
776
|
}
|
|
777
|
+
else if (format === "github-review-json") {
|
|
778
|
+
process.stdout.write(JSON.stringify(structured, null, 2) + "\n");
|
|
779
|
+
}
|
|
663
780
|
else {
|
|
664
781
|
process.stdout.write(markdown + "\n");
|
|
665
782
|
}
|
|
@@ -1221,6 +1338,320 @@ if (command === "concept-index") {
|
|
|
1221
1338
|
indexer.close();
|
|
1222
1339
|
process.exit(r.failed > 0 && r.labeled === 0 ? 1 : 0);
|
|
1223
1340
|
}
|
|
1341
|
+
if (command === "grammars") {
|
|
1342
|
+
// Installs / refreshes the WASM grammars used by SVERKLO_PARSER=tree-sitter
|
|
1343
|
+
// into ~/.sverklo/grammars/. v0.17 opt-in path; the regex parser
|
|
1344
|
+
// works without grammars, so this is purely additive.
|
|
1345
|
+
const sub = args[1];
|
|
1346
|
+
if (sub !== "install" && sub !== "list") {
|
|
1347
|
+
console.log(`\nsverklo grammars — manage tree-sitter grammars for the SVERKLO_PARSER=tree-sitter opt-in parser\n\n` +
|
|
1348
|
+
`Usage:\n` +
|
|
1349
|
+
` sverklo grammars install [--lang typescript,python,go] [--force]\n` +
|
|
1350
|
+
` sverklo grammars list\n\n` +
|
|
1351
|
+
`Languages supported: typescript, tsx, javascript, python, go, rust\n` +
|
|
1352
|
+
`Grammars land in ~/.sverklo/grammars/. Total ~6 MB across all six.\n`);
|
|
1353
|
+
process.exit(0);
|
|
1354
|
+
}
|
|
1355
|
+
const flags = args.slice(2);
|
|
1356
|
+
const langArg = flags.find((f, i) => flags[i - 1] === "--lang") ?? "";
|
|
1357
|
+
const langs = langArg
|
|
1358
|
+
? langArg.split(",").map((s) => s.trim())
|
|
1359
|
+
: undefined;
|
|
1360
|
+
const force = flags.includes("--force");
|
|
1361
|
+
const { installGrammars, grammarsDir, GRAMMARS } = await import("../src/indexer/grammars-install.js");
|
|
1362
|
+
if (sub === "list") {
|
|
1363
|
+
console.log(`\nGrammars dir: ${grammarsDir()}\n`);
|
|
1364
|
+
const { existsSync, statSync } = await import("node:fs");
|
|
1365
|
+
const { join: jp } = await import("node:path");
|
|
1366
|
+
for (const g of GRAMMARS) {
|
|
1367
|
+
const p = jp(grammarsDir(), g.wasm);
|
|
1368
|
+
if (existsSync(p)) {
|
|
1369
|
+
console.log(` ✓ ${g.lang.padEnd(12)}${(statSync(p).size / 1024).toFixed(0)} KB`);
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
console.log(` ✗ ${g.lang.padEnd(12)}not installed`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
console.log(`\nRun \`sverklo grammars install\` to fetch missing grammars.\n`);
|
|
1376
|
+
process.exit(0);
|
|
1377
|
+
}
|
|
1378
|
+
console.log(`\nInstalling tree-sitter grammars into ${grammarsDir()}\n`);
|
|
1379
|
+
const results = await installGrammars({
|
|
1380
|
+
langs,
|
|
1381
|
+
force,
|
|
1382
|
+
onProgress: (m) => console.log(m),
|
|
1383
|
+
});
|
|
1384
|
+
const fresh = results.filter((r) => r.status === "fresh").length;
|
|
1385
|
+
const cached = results.filter((r) => r.status === "cached").length;
|
|
1386
|
+
const errors = results.filter((r) => r.status === "error");
|
|
1387
|
+
console.log(`\nDone: ${fresh} downloaded, ${cached} cached, ${errors.length} failed.`);
|
|
1388
|
+
if (errors.length > 0) {
|
|
1389
|
+
for (const e of errors)
|
|
1390
|
+
console.log(` ✗ ${e.lang}: ${e.error}`);
|
|
1391
|
+
console.log(`\nRetry with \`sverklo grammars install --force\` after checking your network.\n`);
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
console.log(`\nNext: SVERKLO_PARSER=tree-sitter sverklo audit . to use them.\n`);
|
|
1395
|
+
}
|
|
1396
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
1397
|
+
}
|
|
1398
|
+
if (command === "memory") {
|
|
1399
|
+
// `sverklo memory` subcommands:
|
|
1400
|
+
// export — push memory rows to markdown/Notion/JSON files
|
|
1401
|
+
// show — print memory rows as markdown to stdout
|
|
1402
|
+
// edit — open memory rows in $EDITOR; round-trip content edits back
|
|
1403
|
+
// to SQLite. Never deletes by omission.
|
|
1404
|
+
const sub = args[1];
|
|
1405
|
+
if (sub !== "export" && sub !== "show" && sub !== "edit") {
|
|
1406
|
+
console.log(`\nsverklo memory — read, edit, and export the memory store\n\n` +
|
|
1407
|
+
`Subcommands:\n` +
|
|
1408
|
+
` show print all memories as markdown to stdout\n` +
|
|
1409
|
+
` edit open memories in $EDITOR; round-trip text edits back\n` +
|
|
1410
|
+
` export write per-category .md files / JSON / push to Notion\n\n` +
|
|
1411
|
+
`Usage:\n` +
|
|
1412
|
+
` sverklo memory show [--include-invalidated]\n` +
|
|
1413
|
+
` sverklo memory edit [--editor PATH]\n` +
|
|
1414
|
+
` sverklo memory export --format markdown|notion|json --to PATH [flags]\n\n` +
|
|
1415
|
+
`Run \`sverklo memory <subcommand> --help\` for the full flag list.\n`);
|
|
1416
|
+
process.exit(0);
|
|
1417
|
+
}
|
|
1418
|
+
if (sub === "show") {
|
|
1419
|
+
// Render every active memory as markdown to stdout. AI Edge's
|
|
1420
|
+
// "open Memory.md and read it" workflow, but driven by SQLite —
|
|
1421
|
+
// bi-temporal history, git provenance, no manual upkeep.
|
|
1422
|
+
const flags = args.slice(2);
|
|
1423
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
1424
|
+
console.log(`\nsverklo memory show — print memories as markdown to stdout\n\n` +
|
|
1425
|
+
`Flags:\n` +
|
|
1426
|
+
` --include-invalidated include superseded rows (full bi-temporal timeline)\n` +
|
|
1427
|
+
` --kind episodic|semantic|procedural filter by cognitive axis\n`);
|
|
1428
|
+
process.exit(0);
|
|
1429
|
+
}
|
|
1430
|
+
const includeInvalidated = flags.includes("--include-invalidated");
|
|
1431
|
+
const kindFlag = (() => {
|
|
1432
|
+
const idx = flags.indexOf("--kind");
|
|
1433
|
+
if (idx !== -1 && flags[idx + 1])
|
|
1434
|
+
return flags[idx + 1];
|
|
1435
|
+
const prefixed = flags.find((f) => f.startsWith("--kind="));
|
|
1436
|
+
return prefixed ? prefixed.slice("--kind=".length) : undefined;
|
|
1437
|
+
})();
|
|
1438
|
+
if (kindFlag && !["episodic", "semantic", "procedural"].includes(kindFlag)) {
|
|
1439
|
+
console.error(`✗ --kind must be episodic|semantic|procedural, got "${kindFlag}"`);
|
|
1440
|
+
process.exit(2);
|
|
1441
|
+
}
|
|
1442
|
+
const valueFlags = new Set(["--kind"]);
|
|
1443
|
+
const cleanFlags = [];
|
|
1444
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1445
|
+
if (valueFlags.has(flags[i])) {
|
|
1446
|
+
i++;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`)))
|
|
1450
|
+
continue;
|
|
1451
|
+
cleanFlags.push(flags[i]);
|
|
1452
|
+
}
|
|
1453
|
+
const projectPath = await resolveProjectPath(cleanFlags);
|
|
1454
|
+
const { getProjectConfig } = await import("../src/utils/config.js");
|
|
1455
|
+
const { Indexer } = await import("../src/indexer/indexer.js");
|
|
1456
|
+
const { renderMarkdownCombined } = await import("../src/memory/export.js");
|
|
1457
|
+
const config = getProjectConfig(projectPath);
|
|
1458
|
+
const indexer = new Indexer(config);
|
|
1459
|
+
await indexer.index();
|
|
1460
|
+
const rows = includeInvalidated
|
|
1461
|
+
? indexer.memoryStore.getTimeline(10_000)
|
|
1462
|
+
: indexer.memoryStore.getAll(10_000);
|
|
1463
|
+
const filtered = kindFlag ? rows.filter((m) => m.kind === kindFlag) : rows;
|
|
1464
|
+
process.stdout.write(renderMarkdownCombined(filtered));
|
|
1465
|
+
indexer.close();
|
|
1466
|
+
process.exit(0);
|
|
1467
|
+
}
|
|
1468
|
+
if (sub === "edit") {
|
|
1469
|
+
// Render memories to a temp markdown file, open in $EDITOR, parse
|
|
1470
|
+
// changed content back into SQLite. Strict safety policy: omission
|
|
1471
|
+
// never deletes (use `sverklo memory demote` for that); a parse
|
|
1472
|
+
// error aborts without writing.
|
|
1473
|
+
const flags = args.slice(2);
|
|
1474
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
1475
|
+
console.log(`\nsverklo memory edit — open memories in $EDITOR; round-trip text edits\n\n` +
|
|
1476
|
+
`Flags:\n` +
|
|
1477
|
+
` --editor PATH editor to invoke (default: $EDITOR or vi)\n\n` +
|
|
1478
|
+
`Safety:\n` +
|
|
1479
|
+
` - Removing a memory's heading from the file does NOT delete it.\n` +
|
|
1480
|
+
` Use \`sverklo memory demote <id>\` (planned) or \`sverklo_demote\`\n` +
|
|
1481
|
+
` from MCP for explicit deletion.\n` +
|
|
1482
|
+
` - Adding a new memory by hand is not supported here. Use\n` +
|
|
1483
|
+
` \`sverklo_remember\` from MCP or call the API directly.\n` +
|
|
1484
|
+
` - If the parser can't make sense of your edits, the change\n` +
|
|
1485
|
+
` is rejected and your SQLite store is left untouched.\n`);
|
|
1486
|
+
process.exit(0);
|
|
1487
|
+
}
|
|
1488
|
+
const editorOverride = (() => {
|
|
1489
|
+
const idx = flags.indexOf("--editor");
|
|
1490
|
+
if (idx !== -1 && flags[idx + 1])
|
|
1491
|
+
return flags[idx + 1];
|
|
1492
|
+
const prefixed = flags.find((f) => f.startsWith("--editor="));
|
|
1493
|
+
return prefixed ? prefixed.slice("--editor=".length) : undefined;
|
|
1494
|
+
})();
|
|
1495
|
+
const editor = editorOverride ?? process.env.EDITOR ?? "vi";
|
|
1496
|
+
const valueFlags = new Set(["--editor"]);
|
|
1497
|
+
const cleanFlags = [];
|
|
1498
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1499
|
+
if (valueFlags.has(flags[i])) {
|
|
1500
|
+
i++;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`)))
|
|
1504
|
+
continue;
|
|
1505
|
+
cleanFlags.push(flags[i]);
|
|
1506
|
+
}
|
|
1507
|
+
const projectPath = await resolveProjectPath(cleanFlags);
|
|
1508
|
+
const { getProjectConfig } = await import("../src/utils/config.js");
|
|
1509
|
+
const { Indexer } = await import("../src/indexer/indexer.js");
|
|
1510
|
+
const { renderMarkdownCombined, parseMarkdownEdits } = await import("../src/memory/export.js");
|
|
1511
|
+
const { writeFileSync, readFileSync, mkdtempSync } = await import("node:fs");
|
|
1512
|
+
const { tmpdir } = await import("node:os");
|
|
1513
|
+
const { join: joinPath } = await import("node:path");
|
|
1514
|
+
const { spawnSync: spawn } = await import("node:child_process");
|
|
1515
|
+
const config = getProjectConfig(projectPath);
|
|
1516
|
+
const indexer = new Indexer(config);
|
|
1517
|
+
await indexer.index();
|
|
1518
|
+
const rows = indexer.memoryStore.getAll(10_000);
|
|
1519
|
+
if (rows.length === 0) {
|
|
1520
|
+
console.log("(no memories to edit)");
|
|
1521
|
+
indexer.close();
|
|
1522
|
+
process.exit(0);
|
|
1523
|
+
}
|
|
1524
|
+
const dir = mkdtempSync(joinPath(tmpdir(), "sverklo-memory-edit-"));
|
|
1525
|
+
const filePath = joinPath(dir, "memory.md");
|
|
1526
|
+
const original = renderMarkdownCombined(rows);
|
|
1527
|
+
writeFileSync(filePath, original, "utf-8");
|
|
1528
|
+
console.log(`Opening ${filePath} in ${editor}...`);
|
|
1529
|
+
const result = spawn(editor, [filePath], { stdio: "inherit" });
|
|
1530
|
+
if (result.status !== 0) {
|
|
1531
|
+
console.error(`✗ editor exited with status ${result.status}; no changes applied`);
|
|
1532
|
+
indexer.close();
|
|
1533
|
+
process.exit(1);
|
|
1534
|
+
}
|
|
1535
|
+
const after = readFileSync(filePath, "utf-8");
|
|
1536
|
+
if (after === original) {
|
|
1537
|
+
console.log("(no edits — exiting)");
|
|
1538
|
+
indexer.close();
|
|
1539
|
+
process.exit(0);
|
|
1540
|
+
}
|
|
1541
|
+
const parsed = parseMarkdownEdits(after);
|
|
1542
|
+
if (parsed === null) {
|
|
1543
|
+
console.error("✗ couldn't parse the edited file (heading structure broken). No changes applied.");
|
|
1544
|
+
console.error(` Your edits are preserved at ${filePath} in case you want to recover them.`);
|
|
1545
|
+
indexer.close();
|
|
1546
|
+
process.exit(1);
|
|
1547
|
+
}
|
|
1548
|
+
// Compute the diff: ids whose content changed.
|
|
1549
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
1550
|
+
const updates = [];
|
|
1551
|
+
for (const edit of parsed) {
|
|
1552
|
+
const original = byId.get(edit.id);
|
|
1553
|
+
if (!original)
|
|
1554
|
+
continue; // edit references an unknown id — skip silently
|
|
1555
|
+
if (original.content === edit.content)
|
|
1556
|
+
continue;
|
|
1557
|
+
updates.push({ id: edit.id, content: edit.content, before: original.content });
|
|
1558
|
+
}
|
|
1559
|
+
if (updates.length === 0) {
|
|
1560
|
+
console.log("(content unchanged — only metadata or formatting edits, ignoring)");
|
|
1561
|
+
indexer.close();
|
|
1562
|
+
process.exit(0);
|
|
1563
|
+
}
|
|
1564
|
+
for (const u of updates) {
|
|
1565
|
+
indexer.memoryStore.update(u.id, u.content);
|
|
1566
|
+
}
|
|
1567
|
+
console.log(`✓ updated ${updates.length} memor${updates.length === 1 ? "y" : "ies"}:`);
|
|
1568
|
+
for (const u of updates) {
|
|
1569
|
+
const beforeSnip = u.before.slice(0, 60).replace(/\n/g, " ");
|
|
1570
|
+
const afterSnip = u.content.slice(0, 60).replace(/\n/g, " ");
|
|
1571
|
+
console.log(` #${u.id}: "${beforeSnip}${u.before.length > 60 ? "..." : ""}"`);
|
|
1572
|
+
console.log(` → "${afterSnip}${u.content.length > 60 ? "..." : ""}"`);
|
|
1573
|
+
}
|
|
1574
|
+
console.log(`\nNote: omitted memories are preserved. Use \`sverklo_demote <id>\` from MCP\n` +
|
|
1575
|
+
`to explicitly archive a memory.`);
|
|
1576
|
+
indexer.close();
|
|
1577
|
+
process.exit(0);
|
|
1578
|
+
}
|
|
1579
|
+
const flags = args.slice(2);
|
|
1580
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
1581
|
+
console.log(`\nsverklo memory export — push memory rows to markdown / Notion / JSON\n\n` +
|
|
1582
|
+
`Required: --format markdown|notion|json --to PATH\n` +
|
|
1583
|
+
`See \`sverklo memory\` for the full flag list.\n`);
|
|
1584
|
+
process.exit(0);
|
|
1585
|
+
}
|
|
1586
|
+
const flagVal = (name) => {
|
|
1587
|
+
const idx = flags.indexOf(name);
|
|
1588
|
+
if (idx !== -1 && flags[idx + 1])
|
|
1589
|
+
return flags[idx + 1];
|
|
1590
|
+
const prefixed = flags.find((f) => f.startsWith(`${name}=`));
|
|
1591
|
+
return prefixed ? prefixed.slice(name.length + 1) : undefined;
|
|
1592
|
+
};
|
|
1593
|
+
const format = flagVal("--format");
|
|
1594
|
+
if (!format || !["markdown", "notion", "json"].includes(format)) {
|
|
1595
|
+
console.error("✗ --format markdown|notion|json is required");
|
|
1596
|
+
process.exit(2);
|
|
1597
|
+
}
|
|
1598
|
+
const to = flagVal("--to");
|
|
1599
|
+
if (!to) {
|
|
1600
|
+
console.error("✗ --to PATH is required");
|
|
1601
|
+
process.exit(2);
|
|
1602
|
+
}
|
|
1603
|
+
const kindRaw = flagVal("--kind");
|
|
1604
|
+
if (kindRaw && !["episodic", "semantic", "procedural"].includes(kindRaw)) {
|
|
1605
|
+
console.error(`✗ --kind must be episodic|semantic|procedural, got "${kindRaw}"`);
|
|
1606
|
+
process.exit(2);
|
|
1607
|
+
}
|
|
1608
|
+
const includeInvalidated = flags.includes("--include-invalidated");
|
|
1609
|
+
const notionDatabase = flagVal("--notion-database");
|
|
1610
|
+
if (format === "notion" && !notionDatabase) {
|
|
1611
|
+
console.error("✗ --notion-database ID is required for --format notion");
|
|
1612
|
+
process.exit(2);
|
|
1613
|
+
}
|
|
1614
|
+
// Strip value-taking flags before resolving the project path
|
|
1615
|
+
const valueFlags = new Set(["--format", "--to", "--kind", "--notion-database"]);
|
|
1616
|
+
const cleanFlags = [];
|
|
1617
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1618
|
+
if (valueFlags.has(flags[i])) {
|
|
1619
|
+
i++;
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`)))
|
|
1623
|
+
continue;
|
|
1624
|
+
cleanFlags.push(flags[i]);
|
|
1625
|
+
}
|
|
1626
|
+
const projectPath = await resolveProjectPath(cleanFlags);
|
|
1627
|
+
const { resolve: resolvePath } = await import("node:path");
|
|
1628
|
+
const outPath = resolvePath(to);
|
|
1629
|
+
const { getProjectConfig } = await import("../src/utils/config.js");
|
|
1630
|
+
const { Indexer } = await import("../src/indexer/indexer.js");
|
|
1631
|
+
const { runMemoryExport } = await import("../src/memory/export.js");
|
|
1632
|
+
const config = getProjectConfig(projectPath);
|
|
1633
|
+
const indexer = new Indexer(config);
|
|
1634
|
+
await indexer.index();
|
|
1635
|
+
try {
|
|
1636
|
+
const report = runMemoryExport(indexer, {
|
|
1637
|
+
format,
|
|
1638
|
+
to: outPath,
|
|
1639
|
+
kind: kindRaw,
|
|
1640
|
+
includeInvalidated,
|
|
1641
|
+
notionDatabase,
|
|
1642
|
+
});
|
|
1643
|
+
console.log(`\nExported ${report.rowsExported} memories (${format}):\n` +
|
|
1644
|
+
report.written.map((p) => ` ${p}`).join("\n") +
|
|
1645
|
+
`\n\nBy category: ${Object.entries(report.byCategory).map(([k, v]) => `${k}=${v}`).join(", ") || "(none)"}\n`);
|
|
1646
|
+
}
|
|
1647
|
+
catch (err) {
|
|
1648
|
+
const e = err;
|
|
1649
|
+
console.error(`✗ export failed: ${e.message ?? String(err)}`);
|
|
1650
|
+
process.exit(1);
|
|
1651
|
+
}
|
|
1652
|
+
indexer.close();
|
|
1653
|
+
process.exit(0);
|
|
1654
|
+
}
|
|
1224
1655
|
if (command === "digest") {
|
|
1225
1656
|
// Habit-loop scaffold: 5-line summary of what changed in this project
|
|
1226
1657
|
// since the user last paid attention. Designed to be cheap to render
|
|
@@ -1395,6 +1826,8 @@ Memory + offline maintenance:
|
|
|
1395
1826
|
sverklo wakeup Print compressed project context (for system-prompt injection)
|
|
1396
1827
|
sverklo wiki Generate a markdown wiki from the indexed codebase
|
|
1397
1828
|
sverklo digest 5-line summary of what changed in this project (--since 7d)
|
|
1829
|
+
sverklo memory export Export memories to markdown / Notion / JSON
|
|
1830
|
+
sverklo grammars install Install tree-sitter grammars for the v0.17 opt-in parser
|
|
1398
1831
|
sverklo prune Decay stale memories + consolidate similar episodic ones
|
|
1399
1832
|
sverklo concept-index Label clusters with an LLM (requires Ollama)
|
|
1400
1833
|
sverklo enrich-symbols Add LLM-generated purpose to top-PageRank symbols (requires Ollama)
|