sverklo 0.16.0 → 0.17.1

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.
@@ -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: 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.",
@@ -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
- const projectPath = await resolveProjectPath(flags);
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 markdown = handleReviewDiff(indexer, {
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,156 @@ 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 export` — push the memory store out to markdown,
1400
+ // Notion, or raw JSON. Closes the "memory is a private journal"
1401
+ // gap from the v0.16 product teardown.
1402
+ const sub = args[1];
1403
+ if (sub !== "export") {
1404
+ console.log(`\nsverklo memory — export the memory store to other tools\n\n` +
1405
+ `Usage:\n sverklo memory export --format markdown|notion|json --to PATH [flags]\n\n` +
1406
+ `Flags:\n` +
1407
+ ` --format markdown|notion|json output format (required)\n` +
1408
+ ` --to PATH directory (markdown) or file (json/notion)\n` +
1409
+ ` --kind episodic|semantic|procedural filter by cognitive axis (default: all)\n` +
1410
+ ` --include-invalidated include superseded rows for the bi-temporal timeline\n` +
1411
+ ` --notion-database ID target database id (required for --format notion)\n` +
1412
+ ` -h, --help show this help\n`);
1413
+ process.exit(0);
1414
+ }
1415
+ const flags = args.slice(2);
1416
+ if (flags.includes("--help") || flags.includes("-h")) {
1417
+ console.log(`\nsverklo memory export — push memory rows to markdown / Notion / JSON\n\n` +
1418
+ `Required: --format markdown|notion|json --to PATH\n` +
1419
+ `See \`sverklo memory\` for the full flag list.\n`);
1420
+ process.exit(0);
1421
+ }
1422
+ const flagVal = (name) => {
1423
+ const idx = flags.indexOf(name);
1424
+ if (idx !== -1 && flags[idx + 1])
1425
+ return flags[idx + 1];
1426
+ const prefixed = flags.find((f) => f.startsWith(`${name}=`));
1427
+ return prefixed ? prefixed.slice(name.length + 1) : undefined;
1428
+ };
1429
+ const format = flagVal("--format");
1430
+ if (!format || !["markdown", "notion", "json"].includes(format)) {
1431
+ console.error("✗ --format markdown|notion|json is required");
1432
+ process.exit(2);
1433
+ }
1434
+ const to = flagVal("--to");
1435
+ if (!to) {
1436
+ console.error("✗ --to PATH is required");
1437
+ process.exit(2);
1438
+ }
1439
+ const kindRaw = flagVal("--kind");
1440
+ if (kindRaw && !["episodic", "semantic", "procedural"].includes(kindRaw)) {
1441
+ console.error(`✗ --kind must be episodic|semantic|procedural, got "${kindRaw}"`);
1442
+ process.exit(2);
1443
+ }
1444
+ const includeInvalidated = flags.includes("--include-invalidated");
1445
+ const notionDatabase = flagVal("--notion-database");
1446
+ if (format === "notion" && !notionDatabase) {
1447
+ console.error("✗ --notion-database ID is required for --format notion");
1448
+ process.exit(2);
1449
+ }
1450
+ // Strip value-taking flags before resolving the project path
1451
+ const valueFlags = new Set(["--format", "--to", "--kind", "--notion-database"]);
1452
+ const cleanFlags = [];
1453
+ for (let i = 0; i < flags.length; i++) {
1454
+ if (valueFlags.has(flags[i])) {
1455
+ i++;
1456
+ continue;
1457
+ }
1458
+ if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`)))
1459
+ continue;
1460
+ cleanFlags.push(flags[i]);
1461
+ }
1462
+ const projectPath = await resolveProjectPath(cleanFlags);
1463
+ const { resolve: resolvePath } = await import("node:path");
1464
+ const outPath = resolvePath(to);
1465
+ const { getProjectConfig } = await import("../src/utils/config.js");
1466
+ const { Indexer } = await import("../src/indexer/indexer.js");
1467
+ const { runMemoryExport } = await import("../src/memory/export.js");
1468
+ const config = getProjectConfig(projectPath);
1469
+ const indexer = new Indexer(config);
1470
+ await indexer.index();
1471
+ try {
1472
+ const report = runMemoryExport(indexer, {
1473
+ format,
1474
+ to: outPath,
1475
+ kind: kindRaw,
1476
+ includeInvalidated,
1477
+ notionDatabase,
1478
+ });
1479
+ console.log(`\nExported ${report.rowsExported} memories (${format}):\n` +
1480
+ report.written.map((p) => ` ${p}`).join("\n") +
1481
+ `\n\nBy category: ${Object.entries(report.byCategory).map(([k, v]) => `${k}=${v}`).join(", ") || "(none)"}\n`);
1482
+ }
1483
+ catch (err) {
1484
+ const e = err;
1485
+ console.error(`✗ export failed: ${e.message ?? String(err)}`);
1486
+ process.exit(1);
1487
+ }
1488
+ indexer.close();
1489
+ process.exit(0);
1490
+ }
1224
1491
  if (command === "digest") {
1225
1492
  // Habit-loop scaffold: 5-line summary of what changed in this project
1226
1493
  // since the user last paid attention. Designed to be cheap to render
@@ -1395,6 +1662,8 @@ Memory + offline maintenance:
1395
1662
  sverklo wakeup Print compressed project context (for system-prompt injection)
1396
1663
  sverklo wiki Generate a markdown wiki from the indexed codebase
1397
1664
  sverklo digest 5-line summary of what changed in this project (--since 7d)
1665
+ sverklo memory export Export memories to markdown / Notion / JSON
1666
+ sverklo grammars install Install tree-sitter grammars for the v0.17 opt-in parser
1398
1667
  sverklo prune Decay stale memories + consolidate similar episodic ones
1399
1668
  sverklo concept-index Label clusters with an LLM (requires Ollama)
1400
1669
  sverklo enrich-symbols Add LLM-generated purpose to top-PageRank symbols (requires Ollama)