gnosys 5.6.0 → 5.7.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.
Files changed (72) hide show
  1. package/README.md +15 -2
  2. package/dist/cli.js +879 -464
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +96 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/audit.d.ts +13 -0
  7. package/dist/lib/audit.d.ts.map +1 -1
  8. package/dist/lib/audit.js +42 -0
  9. package/dist/lib/audit.js.map +1 -1
  10. package/dist/lib/chat/llmTurn.d.ts +20 -2
  11. package/dist/lib/chat/llmTurn.d.ts.map +1 -1
  12. package/dist/lib/chat/llmTurn.js +58 -16
  13. package/dist/lib/chat/llmTurn.js.map +1 -1
  14. package/dist/lib/chat/render.d.ts.map +1 -1
  15. package/dist/lib/chat/render.js +18 -0
  16. package/dist/lib/chat/render.js.map +1 -1
  17. package/dist/lib/chat/toolFence.d.ts +54 -0
  18. package/dist/lib/chat/toolFence.d.ts.map +1 -0
  19. package/dist/lib/chat/toolFence.js +90 -0
  20. package/dist/lib/chat/toolFence.js.map +1 -0
  21. package/dist/lib/chat/tools.d.ts +48 -0
  22. package/dist/lib/chat/tools.d.ts.map +1 -0
  23. package/dist/lib/chat/tools.js +338 -0
  24. package/dist/lib/chat/tools.js.map +1 -0
  25. package/dist/lib/db.d.ts +58 -0
  26. package/dist/lib/db.d.ts.map +1 -1
  27. package/dist/lib/db.js +154 -38
  28. package/dist/lib/db.js.map +1 -1
  29. package/dist/lib/heartbeat.d.ts +31 -0
  30. package/dist/lib/heartbeat.d.ts.map +1 -0
  31. package/dist/lib/heartbeat.js +91 -0
  32. package/dist/lib/heartbeat.js.map +1 -0
  33. package/dist/lib/idFormat.d.ts +41 -0
  34. package/dist/lib/idFormat.d.ts.map +1 -0
  35. package/dist/lib/idFormat.js +66 -0
  36. package/dist/lib/idFormat.js.map +1 -0
  37. package/dist/lib/progress.d.ts +54 -0
  38. package/dist/lib/progress.d.ts.map +1 -0
  39. package/dist/lib/progress.js +92 -0
  40. package/dist/lib/progress.js.map +1 -0
  41. package/dist/lib/remote.d.ts +37 -1
  42. package/dist/lib/remote.d.ts.map +1 -1
  43. package/dist/lib/remote.js +163 -28
  44. package/dist/lib/remote.js.map +1 -1
  45. package/dist/lib/remoteWizard.d.ts.map +1 -1
  46. package/dist/lib/remoteWizard.js +13 -17
  47. package/dist/lib/remoteWizard.js.map +1 -1
  48. package/dist/lib/setup/sections/ides.d.ts +20 -0
  49. package/dist/lib/setup/sections/ides.d.ts.map +1 -0
  50. package/dist/lib/setup/sections/ides.js +124 -0
  51. package/dist/lib/setup/sections/ides.js.map +1 -0
  52. package/dist/lib/setup/sections/preferences.d.ts +30 -0
  53. package/dist/lib/setup/sections/preferences.d.ts.map +1 -0
  54. package/dist/lib/setup/sections/preferences.js +128 -0
  55. package/dist/lib/setup/sections/preferences.js.map +1 -0
  56. package/dist/lib/setup/sections/routing.d.ts +21 -0
  57. package/dist/lib/setup/sections/routing.d.ts.map +1 -0
  58. package/dist/lib/setup/sections/routing.js +160 -0
  59. package/dist/lib/setup/sections/routing.js.map +1 -0
  60. package/dist/lib/setup/summary.d.ts +42 -0
  61. package/dist/lib/setup/summary.d.ts.map +1 -0
  62. package/dist/lib/setup/summary.js +206 -0
  63. package/dist/lib/setup/summary.js.map +1 -0
  64. package/dist/lib/timeline.d.ts +7 -0
  65. package/dist/lib/timeline.d.ts.map +1 -1
  66. package/dist/lib/timeline.js +19 -5
  67. package/dist/lib/timeline.js.map +1 -1
  68. package/dist/lib/upgrade.d.ts +38 -0
  69. package/dist/lib/upgrade.d.ts.map +1 -0
  70. package/dist/lib/upgrade.js +61 -0
  71. package/dist/lib/upgrade.js.map +1 -0
  72. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { GnosysTagRegistry } from "./lib/tags.js";
17
17
  import { GnosysIngestion } from "./lib/ingest.js";
18
18
  import { applyLens } from "./lib/lensing.js";
19
19
  import { getFileHistory, rollbackToCommit, hasGitHistory, getFileDiff } from "./lib/history.js";
20
- import { groupByPeriod, computeStats } from "./lib/timeline.js";
20
+ import { computeStats } from "./lib/timeline.js";
21
21
  import { buildLinkGraph, getBacklinks, getOutgoingLinks, formatGraphSummary } from "./lib/wikilinks.js";
22
22
  import { bootstrap, discoverFiles } from "./lib/bootstrap.js";
23
23
  import { performImport, formatImportSummary } from "./lib/import.js";
@@ -163,6 +163,23 @@ program
163
163
  .name("gnosys")
164
164
  .description("Gnosys — Persistent memory for AI agents. Sandbox-first runtime, central SQLite brain, federated search, reflection API, process tracing, preferences, Dream Mode, Obsidian export. Also runs as a full MCP server.")
165
165
  .version(pkg.version)
166
+ .addHelpText("after", `
167
+ Commands by group (alphabetical within group):
168
+ Setup & status: setup · status · doctor · check · upgrade
169
+ Memory ops: add · add-structured · update · read · reinforce · ingest
170
+ bootstrap · import · export
171
+ Search: discover · search · hybrid-search · semantic-search · ask · recall
172
+ fsearch · briefing · lens
173
+ Project mgmt: init · projects · list · stats · timeline · graph · tags · tags-add
174
+ stale · history · rollback · audit · links
175
+ Chat (TUI): chat
176
+ Maintenance: maintain · reindex · reindex-graph · dearchive · dream · backup · restore · prune
177
+ Multi-machine: setup remote (configure | status | push | pull | sync | resolve)
178
+ Agent runtime: serve · sandbox · helper · pref · sync · update-status · working-set
179
+ Legacy / advanced: dashboard · migrate · migrate-db · stores · config
180
+
181
+ Run 'gnosys <command> --help' for command-specific help.
182
+ `)
166
183
  .hook("preAction", async () => {
167
184
  // Check if central DB was upgraded to a newer version on another machine
168
185
  try {
@@ -208,6 +225,7 @@ program
208
225
  .option("--federated", "Use federated discovery with tier boosting (project > user > global)")
209
226
  .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated for multiple)")
210
227
  .option("-d, --directory <dir>", "Project directory for context")
228
+ .option("--id-format <format>", "ID display format: short | long | raw (default: short)", "short")
211
229
  .action(async (query, opts) => {
212
230
  // Federated discover path
213
231
  if (opts.federated || opts.scope) {
@@ -262,11 +280,16 @@ program
262
280
  });
263
281
  return;
264
282
  }
283
+ const { formatMemoryId, buildProjectNameLookup, parseIdFormat } = await import("./lib/idFormat.js");
284
+ const idFormat = parseIdFormat(opts.idFormat);
285
+ const projectNames = buildProjectNameLookup(centralDb);
265
286
  outputResult(!!opts.json, { query, count: results.length, results }, () => {
266
287
  console.log(`Found ${results.length} relevant memories for "${query}":\n`);
267
288
  for (const r of results) {
289
+ const projectName = r.project_id ? projectNames.get(r.project_id) || null : null;
290
+ const displayId = formatMemoryId(r.id, projectName, idFormat);
268
291
  console.log(` ${r.title}`);
269
- console.log(` id: ${r.id}`);
292
+ console.log(` id: ${displayId}`);
270
293
  if (r.relevance)
271
294
  console.log(` Relevance: ${r.relevance}`);
272
295
  console.log();
@@ -374,6 +397,7 @@ program
374
397
  .option("-t, --tag <tag>", "Filter by tag")
375
398
  .option("-s, --store <store>", "Filter by store layer (project|user|global)")
376
399
  .option("--json", "Output as JSON")
400
+ .option("--id-format <format>", "ID display format: short | long | raw (default: short)", "short")
377
401
  .action(async (opts) => {
378
402
  let centralDb = null;
379
403
  try {
@@ -407,6 +431,9 @@ program
407
431
  }
408
432
  });
409
433
  }
434
+ const { formatMemoryId, buildProjectNameLookup, parseIdFormat } = await import("./lib/idFormat.js");
435
+ const idFormat = parseIdFormat(opts.idFormat);
436
+ const projectNames = buildProjectNameLookup(centralDb);
410
437
  outputResult(!!opts.json, {
411
438
  count: memories.length,
412
439
  memories: memories.map((m) => ({
@@ -416,12 +443,15 @@ program
416
443
  status: m.status,
417
444
  scope: m.scope,
418
445
  confidence: m.confidence,
446
+ project: m.project_id ? projectNames.get(m.project_id) || null : null,
419
447
  })),
420
448
  }, () => {
421
449
  console.log(`${memories.length} memories:\n`);
422
450
  for (const m of memories) {
451
+ const projectName = m.project_id ? projectNames.get(m.project_id) || null : null;
452
+ const displayId = formatMemoryId(m.id, projectName, idFormat);
423
453
  console.log(` [${m.scope}] [${m.status}] ${m.title}`);
424
- console.log(` id: ${m.id} | category: ${m.category} | confidence: ${m.confidence}`);
454
+ console.log(` id: ${displayId} | category: ${m.category} | confidence: ${m.confidence}`);
425
455
  console.log();
426
456
  }
427
457
  });
@@ -543,13 +573,26 @@ program
543
573
  const setupCmd = program
544
574
  .command("setup")
545
575
  .description("Configure Gnosys — LLM provider, models, remote sync, and IDE integration");
546
- // Bare `gnosys setup` runs the full interactive wizard
576
+ // Bare `gnosys setup` when config exists, opens the summary-first menu
577
+ // so the user can edit one section without re-running the whole wizard.
578
+ // First-time setup or `--full` runs the linear 5-step flow.
547
579
  setupCmd
548
580
  .option("--non-interactive", "Skip prompts, use defaults (for CI/scripting)")
581
+ .option("--full", "Run the linear 5-step wizard even when a config exists")
549
582
  .action(async (opts) => {
550
583
  const { runSetup } = await import("./lib/setup.js");
584
+ const projectDir = process.cwd();
585
+ // Detect existing config — if present and the user didn't pass --full,
586
+ // route to the summary-first menu.
587
+ const configPath = path.join(os.homedir(), ".gnosys", "gnosys.json");
588
+ const hasConfig = existsSync(configPath);
589
+ if (hasConfig && !opts.full && !opts.nonInteractive) {
590
+ const { runSummaryWizard } = await import("./lib/setup/summary.js");
591
+ await runSummaryWizard({ directory: projectDir });
592
+ return;
593
+ }
551
594
  await runSetup({
552
- directory: process.cwd(),
595
+ directory: projectDir,
553
596
  nonInteractive: opts.nonInteractive,
554
597
  });
555
598
  });
@@ -569,14 +612,17 @@ setupCmd
569
612
  validate: opts.validate,
570
613
  });
571
614
  });
572
- // `gnosys setup remote` — configure remote sync (alias for `gnosys remote configure`)
573
- setupCmd
615
+ // ─── gnosys setup remote (parent + subcommands) ────────────────────────
616
+ // v5.7.0: the standalone `gnosys remote` parent was dropped; everything
617
+ // (configure, status, push, pull, sync, resolve) lives here now.
618
+ const setupRemoteCmd = setupCmd
574
619
  .command("remote")
575
- .description("Configure multi-machine sync (NAS/shared drive)")
620
+ .description("Multi-machine sync configure, sync, and resolve conflicts");
621
+ // Bare `gnosys setup remote` — configure wizard (back-compat with v5.6.x)
622
+ setupRemoteCmd
576
623
  .option("--path <path>", "Set remote path directly (non-interactive)")
577
624
  .action(async (opts) => {
578
625
  const { GnosysDB } = await import("./lib/db.js");
579
- // Sync configuration needs explicit local DB access (not auto-routed remote).
580
626
  const db = GnosysDB.openLocal();
581
627
  if (!db.isAvailable()) {
582
628
  console.error("Central DB not available.");
@@ -597,6 +643,265 @@ setupCmd
597
643
  db.close();
598
644
  }
599
645
  });
646
+ setupRemoteCmd
647
+ .command("status")
648
+ .description("Show remote sync status: pending changes, conflicts, last sync")
649
+ .option("--json", "Output as JSON")
650
+ .action(async (opts) => {
651
+ let centralDb = null;
652
+ try {
653
+ centralDb = GnosysDB.openLocal();
654
+ if (!centralDb.isAvailable()) {
655
+ console.error("Central DB not available.");
656
+ process.exit(1);
657
+ }
658
+ const remotePath = centralDb.getMeta("remote_path");
659
+ if (!remotePath) {
660
+ if (opts.json) {
661
+ console.log(JSON.stringify({ configured: false, message: "Remote not configured. Run 'gnosys setup remote'." }, null, 2));
662
+ }
663
+ else {
664
+ console.log("Remote sync: not configured.");
665
+ console.log("Run 'gnosys setup remote' to set up multi-machine sync.");
666
+ }
667
+ return;
668
+ }
669
+ const { RemoteSync, formatStatus } = await import("./lib/remote.js");
670
+ const { withHeartbeat } = await import("./lib/heartbeat.js");
671
+ const sync = new RemoteSync(centralDb, remotePath);
672
+ const status = await withHeartbeat("Checking remote sync status", () => sync.getStatus());
673
+ sync.closeRemote();
674
+ if (opts.json) {
675
+ console.log(JSON.stringify(status, null, 2));
676
+ }
677
+ else {
678
+ console.log(formatStatus(status));
679
+ if (status.conflicts.length > 0) {
680
+ console.log("\nConflicts:");
681
+ for (const c of status.conflicts) {
682
+ console.log(` ${c.memoryId}: ${c.title}`);
683
+ console.log(` local: ${c.localModified}`);
684
+ console.log(` remote: ${c.remoteModified}`);
685
+ }
686
+ console.log("\nResolve with: gnosys setup remote resolve <memory-id> --keep <local|remote>");
687
+ }
688
+ }
689
+ }
690
+ catch (err) {
691
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
692
+ process.exit(1);
693
+ }
694
+ finally {
695
+ centralDb?.close();
696
+ }
697
+ });
698
+ setupRemoteCmd
699
+ .command("push")
700
+ .description("Push local changes to remote")
701
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
702
+ .option("--verbose", "Stream per-memory progress to stderr")
703
+ .action(async (opts) => {
704
+ let centralDb = null;
705
+ try {
706
+ centralDb = GnosysDB.openLocal();
707
+ if (!centralDb.isAvailable()) {
708
+ console.error("Central DB not available.");
709
+ process.exit(1);
710
+ }
711
+ const remotePath = centralDb.getMeta("remote_path");
712
+ if (!remotePath) {
713
+ console.error("Remote not configured.");
714
+ process.exit(1);
715
+ }
716
+ const { RemoteSync } = await import("./lib/remote.js");
717
+ const { withHeartbeat } = await import("./lib/heartbeat.js");
718
+ const { createProgress } = await import("./lib/progress.js");
719
+ const progress = createProgress(!!opts.verbose);
720
+ const sync = new RemoteSync(centralDb, remotePath);
721
+ // Suppress heartbeat when verbose is on (progress already streams).
722
+ const runPush = () => sync.push({
723
+ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
724
+ onProgress: progress.noop ? undefined : progress.emit.bind(progress),
725
+ });
726
+ const result = opts.verbose
727
+ ? await runPush()
728
+ : await withHeartbeat("Pushing to remote", runPush);
729
+ sync.closeRemote();
730
+ const projParts = (result.projectsPushed || 0) > 0 ? ` | Projects pushed: ${result.projectsPushed}` : "";
731
+ const auditParts = (result.auditPushed || 0) > 0 ? ` | Audit pushed: ${result.auditPushed}` : "";
732
+ console.log(`Pushed: ${result.pushed} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}${auditParts}`);
733
+ if (result.errors.length > 0) {
734
+ console.log("\nErrors:");
735
+ for (const e of result.errors)
736
+ console.log(` ${e}`);
737
+ }
738
+ if (result.conflicts.length > 0) {
739
+ console.log("\nConflicts flagged (run 'gnosys setup remote status' for details):");
740
+ for (const c of result.conflicts)
741
+ console.log(` ${c.memoryId} — ${c.title}`);
742
+ }
743
+ }
744
+ catch (err) {
745
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
746
+ process.exit(1);
747
+ }
748
+ finally {
749
+ centralDb?.close();
750
+ }
751
+ });
752
+ setupRemoteCmd
753
+ .command("pull")
754
+ .description("Pull remote changes to local")
755
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
756
+ .option("--verbose", "Stream per-memory progress to stderr")
757
+ .action(async (opts) => {
758
+ let centralDb = null;
759
+ try {
760
+ centralDb = GnosysDB.openLocal();
761
+ if (!centralDb.isAvailable()) {
762
+ console.error("Central DB not available.");
763
+ process.exit(1);
764
+ }
765
+ const remotePath = centralDb.getMeta("remote_path");
766
+ if (!remotePath) {
767
+ console.error("Remote not configured.");
768
+ process.exit(1);
769
+ }
770
+ const { RemoteSync } = await import("./lib/remote.js");
771
+ const { withHeartbeat } = await import("./lib/heartbeat.js");
772
+ const { createProgress } = await import("./lib/progress.js");
773
+ const progress = createProgress(!!opts.verbose);
774
+ const sync = new RemoteSync(centralDb, remotePath);
775
+ const runPull = () => sync.pull({
776
+ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
777
+ onProgress: progress.noop ? undefined : progress.emit.bind(progress),
778
+ });
779
+ const result = opts.verbose
780
+ ? await runPull()
781
+ : await withHeartbeat("Pulling from remote", runPull);
782
+ sync.closeRemote();
783
+ const projParts = (result.projectsPulled || 0) > 0 ? ` | Projects pulled: ${result.projectsPulled}` : "";
784
+ const auditParts = (result.auditPulled || 0) > 0 ? ` | Audit pulled: ${result.auditPulled}` : "";
785
+ console.log(`Pulled: ${result.pulled} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}${auditParts}`);
786
+ if (result.errors.length > 0) {
787
+ console.log("\nErrors:");
788
+ for (const e of result.errors)
789
+ console.log(` ${e}`);
790
+ }
791
+ }
792
+ catch (err) {
793
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
794
+ process.exit(1);
795
+ }
796
+ finally {
797
+ centralDb?.close();
798
+ }
799
+ });
800
+ setupRemoteCmd
801
+ .command("sync")
802
+ .description("Two-way sync: push local changes then pull remote changes")
803
+ .option("--auto", "Run silently for cron/LaunchAgent (skip-and-flag for conflicts)")
804
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
805
+ .option("--verbose", "Stream per-memory progress to stderr")
806
+ .action(async (opts) => {
807
+ let centralDb = null;
808
+ try {
809
+ centralDb = GnosysDB.openLocal();
810
+ if (!centralDb.isAvailable()) {
811
+ if (!opts.auto)
812
+ console.error("Central DB not available.");
813
+ process.exit(1);
814
+ }
815
+ const remotePath = centralDb.getMeta("remote_path");
816
+ if (!remotePath) {
817
+ if (!opts.auto)
818
+ console.error("Remote not configured.");
819
+ process.exit(opts.auto ? 0 : 1);
820
+ }
821
+ const { RemoteSync } = await import("./lib/remote.js");
822
+ const { withHeartbeat } = await import("./lib/heartbeat.js");
823
+ const { createProgress } = await import("./lib/progress.js");
824
+ const progress = createProgress(!!opts.verbose);
825
+ const sync = new RemoteSync(centralDb, remotePath);
826
+ const runSync = () => sync.sync({
827
+ auto: opts.auto,
828
+ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
829
+ onProgress: progress.noop ? undefined : progress.emit.bind(progress),
830
+ });
831
+ // Auto mode + verbose mode both bypass the heartbeat. Auto mode is
832
+ // for non-interactive runs (no spinner). Verbose streams its own output.
833
+ const result = opts.auto || opts.verbose
834
+ ? await runSync()
835
+ : await withHeartbeat("Syncing with remote", runSync);
836
+ sync.closeRemote();
837
+ if (!opts.auto || result.conflicts.length > 0 || result.errors.length > 0) {
838
+ const pp = result.projectsPushed || 0;
839
+ const pl = result.projectsPulled || 0;
840
+ const ap = result.auditPushed || 0;
841
+ const al = result.auditPulled || 0;
842
+ const projParts = (pp + pl) > 0 ? ` | Projects: ↑${pp}/↓${pl}` : "";
843
+ const auditParts = (ap + al) > 0 ? ` | Audit: ↑${ap}/↓${al}` : "";
844
+ console.log(`Pushed: ${result.pushed} | Pulled: ${result.pulled} | Conflicts: ${result.conflicts.length}${projParts}${auditParts}`);
845
+ if (result.errors.length > 0) {
846
+ console.log("\nErrors:");
847
+ for (const e of result.errors)
848
+ console.log(` ${e}`);
849
+ }
850
+ if (result.conflicts.length > 0) {
851
+ console.log("\nConflicts need resolution (run 'gnosys setup remote status' for details).");
852
+ }
853
+ }
854
+ }
855
+ catch (err) {
856
+ if (!opts.auto)
857
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
858
+ process.exit(1);
859
+ }
860
+ finally {
861
+ centralDb?.close();
862
+ }
863
+ });
864
+ setupRemoteCmd
865
+ .command("resolve <memoryId>")
866
+ .description("Resolve a sync conflict by choosing local, remote, or merged content")
867
+ .option("--keep <choice>", "Choice: local | remote", "local")
868
+ .action(async (memoryId, opts) => {
869
+ let centralDb = null;
870
+ try {
871
+ centralDb = GnosysDB.openLocal();
872
+ if (!centralDb.isAvailable()) {
873
+ console.error("Central DB not available.");
874
+ process.exit(1);
875
+ }
876
+ const remotePath = centralDb.getMeta("remote_path");
877
+ if (!remotePath) {
878
+ console.error("Remote not configured.");
879
+ process.exit(1);
880
+ }
881
+ if (opts.keep !== "local" && opts.keep !== "remote") {
882
+ console.error(`--keep must be 'local' or 'remote' (got: ${opts.keep})`);
883
+ process.exit(1);
884
+ }
885
+ const { RemoteSync } = await import("./lib/remote.js");
886
+ const sync = new RemoteSync(centralDb, remotePath);
887
+ const result = await sync.resolve(memoryId, opts.keep);
888
+ sync.closeRemote();
889
+ if (result.ok) {
890
+ console.log(`Resolved ${memoryId}: kept ${opts.keep} version.`);
891
+ }
892
+ else {
893
+ console.error(`Failed to resolve: ${result.error}`);
894
+ process.exit(1);
895
+ }
896
+ }
897
+ catch (err) {
898
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
899
+ process.exit(1);
900
+ }
901
+ finally {
902
+ centralDb?.close();
903
+ }
904
+ });
600
905
  // `gnosys setup dream` — configure dream mode (designation, provider, schedule)
601
906
  setupCmd
602
907
  .command("dream")
@@ -605,6 +910,51 @@ setupCmd
605
910
  const { runDreamSetup } = await import("./lib/setup.js");
606
911
  await runDreamSetup({ directory: process.cwd() });
607
912
  });
913
+ // `gnosys setup ides` — configure IDE / MCP integrations standalone
914
+ setupCmd
915
+ .command("ides")
916
+ .description("Configure IDE integrations (Claude Code/Desktop, Cursor, Codex, Gemini CLI, Antigravity)")
917
+ .action(async () => {
918
+ const readline = await import("readline/promises");
919
+ const { runIdesSetup } = await import("./lib/setup/sections/ides.js");
920
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
921
+ try {
922
+ await runIdesSetup({ rl, directory: process.cwd() });
923
+ }
924
+ finally {
925
+ rl.close();
926
+ }
927
+ });
928
+ // `gnosys setup routing` — task-routing wizard standalone
929
+ setupCmd
930
+ .command("routing")
931
+ .description("Configure per-task LLM routing (structuring, synthesis, vision, transcription, dream)")
932
+ .action(async () => {
933
+ const readline = await import("readline/promises");
934
+ const { runRoutingSetup } = await import("./lib/setup/sections/routing.js");
935
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
936
+ try {
937
+ await runRoutingSetup({ rl, directory: process.cwd() });
938
+ }
939
+ finally {
940
+ rl.close();
941
+ }
942
+ });
943
+ // `gnosys setup preferences` — review user-scope preferences
944
+ setupCmd
945
+ .command("preferences")
946
+ .description("Review and clean up user-scope preferences (incl. legacy imports)")
947
+ .action(async () => {
948
+ const readline = await import("readline/promises");
949
+ const { runPreferencesReview } = await import("./lib/setup/sections/preferences.js");
950
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
951
+ try {
952
+ await runPreferencesReview(rl);
953
+ }
954
+ finally {
955
+ rl.close();
956
+ }
957
+ });
608
958
  // v5.4.2 removal: `gnosys models` (top-level shortcut) was removed in favor
609
959
  // of the canonical `gnosys setup models` form. The implementation function
610
960
  // runModelsCommand() in setup.ts is no longer wired but kept for now in case
@@ -1681,34 +2031,51 @@ program
1681
2031
  .command("timeline")
1682
2032
  .description("Show when memories were created and modified over time")
1683
2033
  .option("-p, --period <period>", "Group by: day, week, month (default), year", "month")
2034
+ .option("--project <id>", "Filter to a specific project ID (default: all projects)")
2035
+ .option("--limit-titles <n>", "Show titles inline when an entry has <= N memories (default 5)", "5")
1684
2036
  .action(async (opts) => {
1685
- const resolver = await getResolver();
1686
- const allMemories = await resolver.getAllMemories();
1687
- if (allMemories.length === 0) {
1688
- console.log("No memories found.");
1689
- return;
2037
+ const { groupDbByPeriod } = await import("./lib/timeline.js");
2038
+ const centralDb = GnosysDB.openCentral();
2039
+ if (!centralDb.isAvailable()) {
2040
+ console.error("Central DB unavailable.");
2041
+ process.exit(1);
1690
2042
  }
1691
- const entries = groupByPeriod(allMemories, opts.period);
1692
- console.log(`Knowledge Timeline (by ${opts.period}):\n`);
1693
- for (const entry of entries) {
1694
- const parts = [];
1695
- if (entry.created > 0)
1696
- parts.push(`${entry.created} created`);
1697
- if (entry.modified > 0)
1698
- parts.push(`${entry.modified} modified`);
1699
- console.log(` ${entry.period}: ${parts.join(", ")}`);
1700
- if (entry.titles.length > 0 && entry.titles.length <= 5) {
1701
- for (const t of entry.titles) {
1702
- console.log(` + ${t}`);
2043
+ try {
2044
+ const memories = opts.project
2045
+ ? centralDb.getMemoriesByProject(opts.project)
2046
+ : centralDb.getActiveMemories();
2047
+ if (memories.length === 0) {
2048
+ console.log("No memories found.");
2049
+ return;
2050
+ }
2051
+ const entries = groupDbByPeriod(memories, opts.period);
2052
+ const titleLimit = Math.max(0, parseInt(opts.limitTitles, 10) || 5);
2053
+ console.log(`Knowledge Timeline (by ${opts.period}, ${memories.length} memories):\n`);
2054
+ for (const entry of entries) {
2055
+ const parts = [];
2056
+ if (entry.created > 0)
2057
+ parts.push(`${entry.created} created`);
2058
+ if (entry.modified > 0)
2059
+ parts.push(`${entry.modified} modified`);
2060
+ console.log(` ${entry.period}: ${parts.join(", ")}`);
2061
+ if (entry.titles.length > 0 && entry.titles.length <= titleLimit) {
2062
+ for (const t of entry.titles) {
2063
+ console.log(` + ${t}`);
2064
+ }
1703
2065
  }
1704
2066
  }
1705
2067
  }
2068
+ finally {
2069
+ centralDb.close();
2070
+ }
1706
2071
  });
1707
2072
  // ─── gnosys stats ───────────────────────────────────────────────────────
1708
2073
  program
1709
2074
  .command("stats")
1710
- .description("Show summary statistics for the memory store")
2075
+ .description("Show summary statistics for the memory store. Use --by-project for a per-project breakdown across the central DB.")
1711
2076
  .option("--json", "Output as JSON")
2077
+ .option("--by-project", "Show a per-project breakdown table instead of single-store stats")
2078
+ .option("--all", "Include all projects (don't filter to current project)")
1712
2079
  .action(async (opts) => {
1713
2080
  let centralDb = null;
1714
2081
  try {
@@ -1717,8 +2084,66 @@ program
1717
2084
  console.error("Central DB not available. Run 'gnosys init' first.");
1718
2085
  process.exit(1);
1719
2086
  }
2087
+ // v5.7.0: --by-project shows a per-project breakdown across the entire
2088
+ // central DB (memories, archived, never reinforced, etc.) as a table.
2089
+ if (opts.byProject) {
2090
+ const projects = centralDb.getAllProjects();
2091
+ const all = centralDb.getAllMemories();
2092
+ const rows = projects.map((p) => {
2093
+ const ms = all.filter((m) => m.project_id === p.id);
2094
+ const active = ms.filter((m) => m.tier === "active" && m.status === "active").length;
2095
+ const archived = ms.filter((m) => m.tier === "archive").length;
2096
+ const reinforced = ms.reduce((sum, m) => sum + (m.reinforcement_count ?? 0), 0);
2097
+ const lastTouch = ms.reduce((m, x) => (x.modified > m ? x.modified : m), "0");
2098
+ return { name: p.name, id: p.id, active, archived, reinforced, lastTouch };
2099
+ });
2100
+ // User/global memories (no project_id)
2101
+ const userScope = all.filter((m) => !m.project_id && m.scope === "user");
2102
+ const globalScope = all.filter((m) => !m.project_id && m.scope === "global");
2103
+ if (userScope.length > 0) {
2104
+ rows.push({
2105
+ name: "(user)",
2106
+ id: "—",
2107
+ active: userScope.filter((m) => m.tier === "active" && m.status === "active").length,
2108
+ archived: userScope.filter((m) => m.tier === "archive").length,
2109
+ reinforced: userScope.reduce((sum, m) => sum + (m.reinforcement_count ?? 0), 0),
2110
+ lastTouch: userScope.reduce((m, x) => (x.modified > m ? x.modified : m), "0"),
2111
+ });
2112
+ }
2113
+ if (globalScope.length > 0) {
2114
+ rows.push({
2115
+ name: "(global)",
2116
+ id: "—",
2117
+ active: globalScope.filter((m) => m.tier === "active" && m.status === "active").length,
2118
+ archived: globalScope.filter((m) => m.tier === "archive").length,
2119
+ reinforced: globalScope.reduce((sum, m) => sum + (m.reinforcement_count ?? 0), 0),
2120
+ lastTouch: globalScope.reduce((m, x) => (x.modified > m ? x.modified : m), "0"),
2121
+ });
2122
+ }
2123
+ rows.sort((a, b) => b.active - a.active);
2124
+ if (opts.json) {
2125
+ console.log(JSON.stringify({ rows }, null, 2));
2126
+ return;
2127
+ }
2128
+ const nameW = Math.max(8, ...rows.map((r) => r.name.length));
2129
+ const idW = 12;
2130
+ console.log("");
2131
+ console.log(` ${"PROJECT".padEnd(nameW)} ${"ID".padEnd(idW)} ${"ACTIVE".padStart(7)} ${"ARCHIVED".padStart(8)} ${"REINF".padStart(6)} LAST MODIFIED`);
2132
+ console.log(` ${"-".repeat(nameW + idW + 7 + 8 + 6 + 19 + 10)}`);
2133
+ for (const r of rows) {
2134
+ const last = r.lastTouch === "0" ? "—" : r.lastTouch.slice(0, 19);
2135
+ const idShort = r.id === "—" ? "—" : r.id.slice(0, idW);
2136
+ console.log(` ${r.name.padEnd(nameW)} ${idShort.padEnd(idW)} ${String(r.active).padStart(7)} ${String(r.archived).padStart(8)} ${String(r.reinforced).padStart(6)} ${last}`);
2137
+ }
2138
+ const totalActive = rows.reduce((s, r) => s + r.active, 0);
2139
+ console.log(` ${"-".repeat(nameW + idW + 7 + 8 + 6 + 19 + 10)}`);
2140
+ console.log(` ${"TOTAL".padEnd(nameW)} ${" ".repeat(idW)} ${String(totalActive).padStart(7)}`);
2141
+ console.log("");
2142
+ return;
2143
+ }
2144
+ // Default behavior: scoped stats (current project + user/global, OR --all)
1720
2145
  const projIdentity = await findProjectIdentity(process.cwd());
1721
- const projectId = projIdentity?.identity.projectId || null;
2146
+ const projectId = !opts.all && projIdentity?.identity.projectId || null;
1722
2147
  let dbMemories = centralDb.getActiveMemories();
1723
2148
  if (projectId) {
1724
2149
  dbMemories = dbMemories.filter((m) => m.project_id === projectId || m.scope === "user" || m.scope === "global");
@@ -1821,7 +2246,7 @@ program
1821
2246
  // ─── gnosys graph ───────────────────────────────────────────────────────
1822
2247
  program
1823
2248
  .command("graph")
1824
- .description("Show the full cross-reference graph across all memories")
2249
+ .description("Show the [[wikilink]] cross-reference graph between memories. Empty until you start using [[Title]] in memory content — then this shows which memories reference each other.")
1825
2250
  .action(async () => {
1826
2251
  // v5.4.1: Query the central DB directly. Previously this used the
1827
2252
  // filesystem resolver, which returns nothing in v5.x DB-only mode
@@ -2091,7 +2516,7 @@ importCmd
2091
2516
  // ─── gnosys reindex ──────────────────────────────────────────────────────
2092
2517
  program
2093
2518
  .command("reindex")
2094
- .description("Rebuild all semantic embeddings from every memory file. Downloads the model (~80 MB) on first run.")
2519
+ .description("Rebuild semantic embeddings for every memory in the central DB. Run after bulk imports, schema changes, or if hybrid search starts returning poor matches. Downloads the all-MiniLM-L6-v2 model (~80 MB) on first run.")
2095
2520
  .action(async () => {
2096
2521
  const resolver = await getResolver();
2097
2522
  const stores = resolver.getStores();
@@ -2627,39 +3052,9 @@ program
2627
3052
  console.log("");
2628
3053
  console.log(formatGraphStats(stats));
2629
3054
  });
2630
- // ─── gnosys dashboard ───────────────────────────────────────────────────
2631
- program
2632
- .command("dashboard")
2633
- .description("Show system dashboard: memory count, health, graph stats, LLM status")
2634
- .option("--json", "Output as JSON instead of pretty table")
2635
- .action(async (opts) => {
2636
- const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");
2637
- const resolver = await getResolver();
2638
- const stores = resolver.getStores();
2639
- if (stores.length === 0) {
2640
- console.error("No Gnosys stores found. Run gnosys init first.");
2641
- process.exit(1);
2642
- }
2643
- const cfg = await loadConfig(stores[0].path);
2644
- // v5.1: Use central DB for dashboard stats
2645
- let dashDb;
2646
- try {
2647
- const db = GnosysDB.openCentral();
2648
- if (db.isAvailable() && db.isMigrated()) {
2649
- dashDb = db;
2650
- }
2651
- }
2652
- catch {
2653
- // Central DB not available — legacy dashboard only
2654
- }
2655
- const data = await collectDashboardData(resolver, cfg, pkg.version, dashDb);
2656
- if (opts.json) {
2657
- console.log(formatDashboardJSON(data));
2658
- }
2659
- else {
2660
- console.log(formatDashboard(data));
2661
- }
2662
- });
3055
+ // `gnosys dashboard` was removed in v5.7.1.
3056
+ // Use `gnosys status --system` instead. Hard removal — commander will emit
3057
+ // the standard "unknown command" error.
2663
3058
  // ─── gnosys maintain ─────────────────────────────────────────────────────
2664
3059
  program
2665
3060
  .command("maintain")
@@ -2687,299 +3082,75 @@ program
2687
3082
  console.log(`→ ${message}`);
2688
3083
  }
2689
3084
  else {
2690
- console.log(message);
2691
- }
2692
- },
2693
- onProgress: (step, current, total) => {
2694
- process.stdout.write(`\r[${current}/${total}] ${step}...`);
2695
- if (current === total)
2696
- process.stdout.write("\n");
2697
- },
2698
- });
2699
- console.log("");
2700
- console.log(formatMaintenanceReport(report));
2701
- });
2702
- // ─── gnosys dearchive ───────────────────────────────────────────────────
2703
- program
2704
- .command("dearchive <query>")
2705
- .description("Force-dearchive memories matching a query from archive.db back to active")
2706
- .option("--limit <n>", "Max memories to dearchive", "5")
2707
- .action(async (query, opts) => {
2708
- const { GnosysArchive } = await import("./lib/archive.js");
2709
- const resolver = await getResolver();
2710
- const stores = resolver.getStores();
2711
- if (stores.length === 0) {
2712
- console.error("No Gnosys stores found. Run gnosys init first.");
2713
- process.exit(1);
2714
- }
2715
- const writeTarget = resolver.getWriteTarget();
2716
- if (!writeTarget) {
2717
- console.error("No writable store found.");
2718
- process.exit(1);
2719
- }
2720
- const archive = new GnosysArchive(writeTarget.path);
2721
- if (!archive.isAvailable()) {
2722
- console.error("Archive not available. Is better-sqlite3 installed?");
2723
- process.exit(1);
2724
- }
2725
- const results = archive.searchArchive(query, parseInt(opts.limit));
2726
- if (results.length === 0) {
2727
- console.log(`No archived memories found matching "${query}".`);
2728
- archive.close();
2729
- return;
2730
- }
2731
- console.log(`Found ${results.length} archived memories matching "${query}":\n`);
2732
- for (const r of results) {
2733
- console.log(` • ${r.title} (${r.id})`);
2734
- }
2735
- console.log("");
2736
- // Dearchive all found
2737
- const ids = results.map((r) => r.id);
2738
- const restored = await archive.dearchiveBatch(ids, writeTarget.store);
2739
- archive.close();
2740
- console.log(`Dearchived ${restored.length} memories back to active:`);
2741
- for (const rp of restored) {
2742
- console.log(` → ${rp}`);
2743
- }
2744
- });
2745
- // NOTE: gnosys migrate is defined below (near the end) with --to-central support
2746
- // ─── gnosys remote (multi-machine sync) ────────────────────────────────
2747
- const remoteCmd = program
2748
- .command("remote")
2749
- .description("Multi-machine sync — share gnosys.db across machines via NAS or shared drive");
2750
- remoteCmd
2751
- .command("status")
2752
- .description("Show remote sync status: pending changes, conflicts, last sync")
2753
- .option("--json", "Output as JSON")
2754
- .action(async (opts) => {
2755
- let centralDb = null;
2756
- try {
2757
- // Sync operations need explicit local DB access (not auto-routed remote).
2758
- centralDb = GnosysDB.openLocal();
2759
- if (!centralDb.isAvailable()) {
2760
- console.error("Central DB not available.");
2761
- process.exit(1);
2762
- }
2763
- const remotePath = centralDb.getMeta("remote_path");
2764
- if (!remotePath) {
2765
- if (opts.json) {
2766
- console.log(JSON.stringify({ configured: false, message: "Remote not configured. Run 'gnosys remote configure'." }, null, 2));
2767
- }
2768
- else {
2769
- console.log("Remote sync: not configured.");
2770
- console.log("Run 'gnosys remote configure' to set up multi-machine sync.");
2771
- }
2772
- return;
2773
- }
2774
- const { RemoteSync, formatStatus } = await import("./lib/remote.js");
2775
- const sync = new RemoteSync(centralDb, remotePath);
2776
- const status = await sync.getStatus();
2777
- sync.closeRemote();
2778
- if (opts.json) {
2779
- console.log(JSON.stringify(status, null, 2));
2780
- }
2781
- else {
2782
- console.log(formatStatus(status));
2783
- if (status.conflicts.length > 0) {
2784
- console.log("\nConflicts:");
2785
- for (const c of status.conflicts) {
2786
- console.log(` ${c.memoryId}: ${c.title}`);
2787
- console.log(` local: ${c.localModified}`);
2788
- console.log(` remote: ${c.remoteModified}`);
2789
- }
2790
- console.log("\nResolve with: gnosys remote resolve <memory-id> --keep <local|remote>");
2791
- }
2792
- }
2793
- }
2794
- catch (err) {
2795
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2796
- process.exit(1);
2797
- }
2798
- finally {
2799
- centralDb?.close();
2800
- }
2801
- });
2802
- remoteCmd
2803
- .command("push")
2804
- .description("Push local changes to remote")
2805
- .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2806
- .action(async (opts) => {
2807
- let centralDb = null;
2808
- try {
2809
- centralDb = GnosysDB.openLocal();
2810
- if (!centralDb.isAvailable()) {
2811
- console.error("Central DB not available.");
2812
- process.exit(1);
2813
- }
2814
- const remotePath = centralDb.getMeta("remote_path");
2815
- if (!remotePath) {
2816
- console.error("Remote not configured.");
2817
- process.exit(1);
2818
- }
2819
- const { RemoteSync } = await import("./lib/remote.js");
2820
- const sync = new RemoteSync(centralDb, remotePath);
2821
- const result = await sync.push({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2822
- sync.closeRemote();
2823
- const projParts = (result.projectsPushed || 0) > 0 ? ` | Projects pushed: ${result.projectsPushed}` : "";
2824
- console.log(`Pushed: ${result.pushed} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}`);
2825
- if (result.errors.length > 0) {
2826
- console.log("\nErrors:");
2827
- for (const e of result.errors)
2828
- console.log(` ${e}`);
2829
- }
2830
- if (result.conflicts.length > 0) {
2831
- console.log("\nConflicts flagged (run 'gnosys remote status' for details):");
2832
- for (const c of result.conflicts)
2833
- console.log(` ${c.memoryId} — ${c.title}`);
2834
- }
2835
- }
2836
- catch (err) {
2837
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2838
- process.exit(1);
2839
- }
2840
- finally {
2841
- centralDb?.close();
2842
- }
2843
- });
2844
- remoteCmd
2845
- .command("pull")
2846
- .description("Pull remote changes to local")
2847
- .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2848
- .action(async (opts) => {
2849
- let centralDb = null;
2850
- try {
2851
- centralDb = GnosysDB.openLocal();
2852
- if (!centralDb.isAvailable()) {
2853
- console.error("Central DB not available.");
2854
- process.exit(1);
2855
- }
2856
- const remotePath = centralDb.getMeta("remote_path");
2857
- if (!remotePath) {
2858
- console.error("Remote not configured.");
2859
- process.exit(1);
2860
- }
2861
- const { RemoteSync } = await import("./lib/remote.js");
2862
- const sync = new RemoteSync(centralDb, remotePath);
2863
- const result = await sync.pull({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2864
- sync.closeRemote();
2865
- const projParts = (result.projectsPulled || 0) > 0 ? ` | Projects pulled: ${result.projectsPulled}` : "";
2866
- console.log(`Pulled: ${result.pulled} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}`);
2867
- if (result.errors.length > 0) {
2868
- console.log("\nErrors:");
2869
- for (const e of result.errors)
2870
- console.log(` ${e}`);
2871
- }
2872
- }
2873
- catch (err) {
2874
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2875
- process.exit(1);
2876
- }
2877
- finally {
2878
- centralDb?.close();
2879
- }
2880
- });
2881
- remoteCmd
2882
- .command("sync")
2883
- .description("Two-way sync: push local changes then pull remote changes")
2884
- .option("--auto", "Run silently for cron/LaunchAgent (skip-and-flag for conflicts)")
2885
- .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2886
- .action(async (opts) => {
2887
- let centralDb = null;
2888
- try {
2889
- centralDb = GnosysDB.openLocal();
2890
- if (!centralDb.isAvailable()) {
2891
- if (!opts.auto)
2892
- console.error("Central DB not available.");
2893
- process.exit(1);
2894
- }
2895
- const remotePath = centralDb.getMeta("remote_path");
2896
- if (!remotePath) {
2897
- if (!opts.auto)
2898
- console.error("Remote not configured.");
2899
- process.exit(opts.auto ? 0 : 1);
2900
- }
2901
- const { RemoteSync } = await import("./lib/remote.js");
2902
- const sync = new RemoteSync(centralDb, remotePath);
2903
- const result = await sync.sync({
2904
- auto: opts.auto,
2905
- strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
2906
- });
2907
- sync.closeRemote();
2908
- if (!opts.auto || result.conflicts.length > 0 || result.errors.length > 0) {
2909
- const pp = result.projectsPushed || 0;
2910
- const pl = result.projectsPulled || 0;
2911
- const projParts = (pp + pl) > 0 ? ` | Projects: ↑${pp}/↓${pl}` : "";
2912
- console.log(`Pushed: ${result.pushed} | Pulled: ${result.pulled} | Conflicts: ${result.conflicts.length}${projParts}`);
2913
- if (result.errors.length > 0) {
2914
- console.log("\nErrors:");
2915
- for (const e of result.errors)
2916
- console.log(` ${e}`);
2917
- }
2918
- if (result.conflicts.length > 0) {
2919
- console.log("\nConflicts need resolution (run 'gnosys remote status' for details).");
3085
+ console.log(message);
2920
3086
  }
2921
- }
3087
+ },
3088
+ onProgress: (step, current, total) => {
3089
+ process.stdout.write(`\r[${current}/${total}] ${step}...`);
3090
+ if (current === total)
3091
+ process.stdout.write("\n");
3092
+ },
3093
+ });
3094
+ console.log("");
3095
+ console.log(formatMaintenanceReport(report));
3096
+ });
3097
+ // ─── gnosys dearchive ───────────────────────────────────────────────────
3098
+ program
3099
+ .command("dearchive <query>")
3100
+ .description("Force-dearchive memories matching a query from archive.db back to active")
3101
+ .option("--limit <n>", "Max memories to dearchive", "5")
3102
+ .action(async (query, opts) => {
3103
+ const { GnosysArchive } = await import("./lib/archive.js");
3104
+ const resolver = await getResolver();
3105
+ const stores = resolver.getStores();
3106
+ if (stores.length === 0) {
3107
+ console.error("No Gnosys stores found. Run gnosys init first.");
3108
+ process.exit(1);
2922
3109
  }
2923
- catch (err) {
2924
- if (!opts.auto)
2925
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
3110
+ const writeTarget = resolver.getWriteTarget();
3111
+ if (!writeTarget) {
3112
+ console.error("No writable store found.");
2926
3113
  process.exit(1);
2927
3114
  }
2928
- finally {
2929
- centralDb?.close();
3115
+ const archive = new GnosysArchive(writeTarget.path);
3116
+ if (!archive.isAvailable()) {
3117
+ console.error("Archive not available. Is better-sqlite3 installed?");
3118
+ process.exit(1);
2930
3119
  }
2931
- });
2932
- remoteCmd
2933
- .command("resolve <memoryId>")
2934
- .description("Resolve a sync conflict by choosing local, remote, or merged content")
2935
- .option("--keep <choice>", "Choice: local | remote", "local")
2936
- .action(async (memoryId, opts) => {
2937
- let centralDb = null;
2938
- try {
2939
- centralDb = GnosysDB.openLocal();
2940
- if (!centralDb.isAvailable()) {
2941
- console.error("Central DB not available.");
2942
- process.exit(1);
2943
- }
2944
- const remotePath = centralDb.getMeta("remote_path");
2945
- if (!remotePath) {
2946
- console.error("Remote not configured.");
2947
- process.exit(1);
2948
- }
2949
- if (opts.keep !== "local" && opts.keep !== "remote") {
2950
- console.error(`--keep must be 'local' or 'remote' (got: ${opts.keep})`);
2951
- process.exit(1);
2952
- }
2953
- const { RemoteSync } = await import("./lib/remote.js");
2954
- const sync = new RemoteSync(centralDb, remotePath);
2955
- const result = await sync.resolve(memoryId, opts.keep);
2956
- sync.closeRemote();
2957
- if (result.ok) {
2958
- console.log(`Resolved ${memoryId}: kept ${opts.keep} version.`);
2959
- }
2960
- else {
2961
- console.error(`Failed to resolve: ${result.error}`);
2962
- process.exit(1);
2963
- }
3120
+ const results = archive.searchArchive(query, parseInt(opts.limit));
3121
+ if (results.length === 0) {
3122
+ console.log(`No archived memories found matching "${query}".`);
3123
+ archive.close();
3124
+ return;
2964
3125
  }
2965
- catch (err) {
2966
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2967
- process.exit(1);
3126
+ console.log(`Found ${results.length} archived memories matching "${query}":\n`);
3127
+ for (const r of results) {
3128
+ console.log(` • ${r.title} (${r.id})`);
2968
3129
  }
2969
- finally {
2970
- centralDb?.close();
3130
+ console.log("");
3131
+ // Dearchive all found
3132
+ const ids = results.map((r) => r.id);
3133
+ const restored = await archive.dearchiveBatch(ids, writeTarget.store);
3134
+ archive.close();
3135
+ console.log(`Dearchived ${restored.length} memories back to active:`);
3136
+ for (const rp of restored) {
3137
+ console.log(` → ${rp}`);
2971
3138
  }
2972
3139
  });
2973
- // v5.4.2 removal: `gnosys remote configure` was removed in favor of the
2974
- // canonical `gnosys setup remote` form (which calls the same wizard
2975
- // helpers from lib/remoteWizard.ts). Sync operations like push/pull/sync
2976
- // remain under the `remote` parent.
2977
- // ─── gnosys upgrade ─────────────────────────────────────────────────────
2978
- program
2979
- .command("upgrade")
2980
- .description("Re-initialize all registered projects after a Gnosys version upgrade. Updates agent rules, project registry, stamps the central DB, and regenerates the portfolio dashboard.")
2981
- .option("--skip-dashboard", "Skip regenerating the portfolio dashboard")
2982
- .action(async (opts) => {
3140
+ // NOTE: gnosys migrate is defined below (near the end) with --to-central support
3141
+ // ─── gnosys upgrade + gnosys setup sync-projects ──────────────────────
3142
+ //
3143
+ // v5.7.1 (#15) split this command:
3144
+ //
3145
+ // gnosys upgrade — upgrade the gnosys CLI/MCP itself
3146
+ // (npm install + restart signal to MCPs)
3147
+ // gnosys setup sync-projects what the old `gnosys upgrade` used to do
3148
+ // (re-init project identities, agent rules,
3149
+ // central DB stamp, portfolio dashboard)
3150
+ //
3151
+ // The body of the legacy command is preserved verbatim below as
3152
+ // `syncProjectsAction`, called from the new `setup sync-projects` command.
3153
+ async function syncProjectsAction(opts) {
2983
3154
  const currentVersion = pkg.version;
2984
3155
  console.log(`Gnosys v${currentVersion} — upgrading registered projects...\n`);
2985
3156
  // 1. Read registered projects from file registry AND central DB
@@ -3198,12 +3369,83 @@ program
3198
3369
  console.log(`\n Could not regenerate portfolio dashboard`);
3199
3370
  }
3200
3371
  }
3372
+ }
3373
+ // `gnosys setup sync-projects` — re-init project identities + agent rules.
3374
+ // (This is what `gnosys upgrade` used to do; renamed in v5.7.1.)
3375
+ setupCmd
3376
+ .command("sync-projects")
3377
+ .description("Re-initialize all registered projects after upgrading gnosys: refresh agent rules, project registry, central DB stamp, and portfolio dashboard.")
3378
+ .option("--skip-dashboard", "Skip regenerating the portfolio dashboard")
3379
+ .action(syncProjectsAction);
3380
+ // `gnosys upgrade` — upgrade the gnosys CLI/MCP itself, then prompt the
3381
+ // user to run sync-projects. Writes ~/.gnosys/last-upgrade-at so running
3382
+ // MCP servers exit cleanly and the host respawns them against the new
3383
+ // global binary (see src/lib/upgrade.ts).
3384
+ program
3385
+ .command("upgrade")
3386
+ .description("Upgrade gnosys itself (npm install -g gnosys@latest) and signal running MCP servers to restart. After upgrading, suggests running 'gnosys setup sync-projects'.")
3387
+ .option("--yes", "Skip the post-upgrade sync-projects prompt and exit")
3388
+ .option("--no-sync", "Don't suggest running sync-projects afterward")
3389
+ .action(async (opts) => {
3390
+ const currentVersion = pkg.version;
3391
+ console.log(`Gnosys CLI: currently v${currentVersion}`);
3392
+ console.log(`Running: npm install -g gnosys@latest ...`);
3393
+ const { execSync } = await import("child_process");
3394
+ try {
3395
+ execSync("npm install -g gnosys@latest", { stdio: "inherit" });
3396
+ }
3397
+ catch (err) {
3398
+ console.error(`\nUpgrade failed: ${err instanceof Error ? err.message : err}`);
3399
+ console.error(`Try running 'npm install -g gnosys@latest' manually.`);
3400
+ process.exit(1);
3401
+ }
3402
+ // Read the newly-installed version (best-effort — we may still be the
3403
+ // old binary in-process; this is purely informational).
3404
+ let newVersion = "(see npm output)";
3405
+ try {
3406
+ const out = execSync("npm ls -g gnosys --depth=0 --json", { encoding: "utf8" });
3407
+ const parsed = JSON.parse(out);
3408
+ newVersion = parsed?.dependencies?.gnosys?.version || newVersion;
3409
+ }
3410
+ catch {
3411
+ // Best-effort lookup only.
3412
+ }
3413
+ // Write the marker so any running MCP servers exit and respawn.
3414
+ const { writeUpgradeMarker } = await import("./lib/upgrade.js");
3415
+ try {
3416
+ writeUpgradeMarker(typeof newVersion === "string" && newVersion !== "(see npm output)"
3417
+ ? newVersion
3418
+ : currentVersion);
3419
+ console.log(`\n✓ Upgrade marker written: ~/.gnosys/last-upgrade-at`);
3420
+ console.log(` Any running MCP servers will detect this within 10s and restart cleanly.`);
3421
+ console.log(` (Your MCP client — Claude Code, Cursor, VS Code — will auto-respawn.)`);
3422
+ }
3423
+ catch (err) {
3424
+ console.error(`\nCould not write upgrade marker: ${err instanceof Error ? err.message : err}`);
3425
+ console.error(`Running MCP servers will need to be restarted manually.`);
3426
+ }
3427
+ if (opts.sync === false || opts.yes) {
3428
+ console.log(`\nDone. Run 'gnosys setup sync-projects' when you're ready to refresh registered projects.`);
3429
+ return;
3430
+ }
3431
+ // Prompt for sync-projects.
3432
+ const readline = await import("readline");
3433
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3434
+ const answer = await new Promise((resolve) => rl.question(`\nRun 'gnosys setup sync-projects' now to refresh registered projects? [Y/n] `, resolve));
3435
+ rl.close();
3436
+ if (answer.trim().toLowerCase() === "n" || answer.trim().toLowerCase() === "no") {
3437
+ console.log(`Done. You can run 'gnosys setup sync-projects' later.`);
3438
+ return;
3439
+ }
3440
+ console.log(``);
3441
+ await syncProjectsAction({});
3201
3442
  });
3202
3443
  // ─── gnosys doctor ──────────────────────────────────────────────────────
3203
3444
  program
3204
3445
  .command("doctor")
3205
3446
  .description("Check system health: stores, LLM connectivity, embeddings, archive")
3206
- .action(async () => {
3447
+ .option("--fix", "Offer interactive cleanup of legacy artifacts (e.g. per-store gnosys.db)")
3448
+ .action(async (opts) => {
3207
3449
  const resolver = await getResolver();
3208
3450
  const stores = resolver.getStores();
3209
3451
  console.log("Gnosys Doctor");
@@ -3214,9 +3456,36 @@ program
3214
3456
  const localDbExists = await fs.stat(localDbPath).then(() => true).catch(() => false);
3215
3457
  if (localDbExists) {
3216
3458
  console.log("Local Store (gnosys.db):");
3217
- console.log(" ⚠ Local gnosys.db found — this is a legacy artifact.");
3218
- console.log(" All memories should be in the central DB (~/.gnosys/gnosys.db).");
3219
- console.log(` Safe to remove: rm "${localDbPath}"`);
3459
+ console.log(" ⚠ Local gnosys.db found — this is a legacy artifact (pre-v2.0 file-based store).");
3460
+ console.log(" All memories live in the central DB now (~/.gnosys/gnosys.db).");
3461
+ console.log(` Path: ${localDbPath}`);
3462
+ if (opts.fix) {
3463
+ // Interactive cleanup — verify the local DB is safe to delete
3464
+ // (no rows that aren't already in the central DB) before prompting.
3465
+ const safe = await isLegacyStoreSafeToRemove(localDbPath);
3466
+ if (!safe.ok) {
3467
+ console.log(` ✗ NOT safe to auto-remove: ${safe.reason}`);
3468
+ console.log(` Inspect manually with: sqlite3 ${localDbPath} "SELECT COUNT(*) FROM memories;"`);
3469
+ }
3470
+ else {
3471
+ const readline = await import("readline/promises");
3472
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3473
+ const answer = await rl.question(` Remove "${localDbPath}"? [y/N] `);
3474
+ rl.close();
3475
+ if (answer.trim().toLowerCase() === "y") {
3476
+ await fs.unlink(localDbPath).catch(() => undefined);
3477
+ await fs.unlink(localDbPath + "-wal").catch(() => undefined);
3478
+ await fs.unlink(localDbPath + "-shm").catch(() => undefined);
3479
+ console.log(" ✓ Removed.");
3480
+ }
3481
+ else {
3482
+ console.log(" Skipped.");
3483
+ }
3484
+ }
3485
+ }
3486
+ else {
3487
+ console.log(" Run 'gnosys doctor --fix' to remove safely (after verifying it's empty).");
3488
+ }
3220
3489
  console.log("");
3221
3490
  }
3222
3491
  }
@@ -3328,31 +3597,110 @@ program
3328
3597
  catch {
3329
3598
  console.log(" Index: not initialized (run gnosys reindex to build)");
3330
3599
  }
3331
- // Maintenance health
3600
+ // Maintenance health — v5.7.0: queries the central DB directly
3601
+ // (the prior version used GnosysMaintenanceEngine which only sees the
3602
+ // legacy file-based stores, which are empty post-DB-only).
3332
3603
  console.log("");
3333
3604
  console.log("Maintenance Health:");
3334
3605
  try {
3335
- const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
3336
- const engine = new GnosysMaintenanceEngine(resolver, cfg);
3337
- const health = await engine.getHealthReport();
3338
- console.log(` Active memories: ${health.totalActive}`);
3339
- console.log(` Stale (confidence < 0.3): ${health.staleCount}`);
3340
- console.log(` Average confidence: ${health.avgConfidence.toFixed(3)} (decayed: ${health.avgDecayedConfidence.toFixed(3)})`);
3341
- console.log(` Never reinforced: ${health.neverReinforced}`);
3342
- console.log(` Total reinforcements: ${health.totalReinforcements}`);
3606
+ const db2 = GnosysDB.openCentral();
3607
+ if (db2.isAvailable() && db2.isMigrated()) {
3608
+ const memories = db2.getActiveMemories();
3609
+ const now = Date.now();
3610
+ const DECAY_LAMBDA = 0.005;
3611
+ const STALE_THRESHOLD = 0.3;
3612
+ let sumConfidence = 0;
3613
+ let sumDecayed = 0;
3614
+ let staleCount = 0;
3615
+ let neverReinforced = 0;
3616
+ let totalReinforcements = 0;
3617
+ for (const m of memories) {
3618
+ const baseConfidence = m.confidence ?? 0.8;
3619
+ const lastIso = m.last_reinforced || m.modified || m.created;
3620
+ const lastTs = lastIso ? new Date(lastIso).getTime() : NaN;
3621
+ // Some legacy memories have non-ISO dates that don't parse; treat
3622
+ // them as "today" rather than NaN-corrupting the average.
3623
+ const daysSince = Number.isFinite(lastTs)
3624
+ ? Math.max(0, Math.floor((now - lastTs) / (1000 * 60 * 60 * 24)))
3625
+ : 0;
3626
+ const decayed = baseConfidence * Math.exp(-DECAY_LAMBDA * daysSince);
3627
+ sumConfidence += baseConfidence;
3628
+ sumDecayed += decayed;
3629
+ if (decayed < STALE_THRESHOLD)
3630
+ staleCount++;
3631
+ const rc = m.reinforcement_count ?? 0;
3632
+ if (rc === 0)
3633
+ neverReinforced++;
3634
+ totalReinforcements += rc;
3635
+ }
3636
+ const n = Math.max(1, memories.length);
3637
+ console.log(` Active memories: ${memories.length}`);
3638
+ console.log(` Stale (decayed confidence < ${STALE_THRESHOLD}): ${staleCount}`);
3639
+ console.log(` Average confidence: ${(sumConfidence / n).toFixed(3)} (decayed: ${(sumDecayed / n).toFixed(3)})`);
3640
+ console.log(` Never reinforced: ${neverReinforced}`);
3641
+ console.log(` Total reinforcements: ${totalReinforcements}`);
3642
+ }
3643
+ else {
3644
+ console.log(" — central DB not available");
3645
+ }
3646
+ db2.close();
3343
3647
  }
3344
3648
  catch (err) {
3345
3649
  console.log(` Error: ${err instanceof Error ? err.message : String(err)}`);
3346
3650
  }
3347
3651
  }
3348
3652
  });
3653
+ /**
3654
+ * Check whether a legacy per-store gnosys.db is safe to remove.
3655
+ * Safe = the file is empty OR every memory in it is already represented
3656
+ * in the central DB (matching ID present centrally). This is conservative:
3657
+ * we don't compare hashes or content, just IDs. The legacy DB existed
3658
+ * pre-v2.0; its memories should have all migrated to central DB long ago.
3659
+ */
3660
+ async function isLegacyStoreSafeToRemove(localDbPath) {
3661
+ try {
3662
+ const Database = (await import("better-sqlite3")).default;
3663
+ const localDb = new Database(localDbPath, { readonly: true });
3664
+ let localIds = [];
3665
+ try {
3666
+ const rows = localDb.prepare("SELECT id FROM memories").all();
3667
+ localIds = rows.map((r) => r.id);
3668
+ }
3669
+ catch {
3670
+ // Table doesn't exist — file is effectively empty
3671
+ localDb.close();
3672
+ return { ok: true };
3673
+ }
3674
+ localDb.close();
3675
+ if (localIds.length === 0)
3676
+ return { ok: true };
3677
+ const centralDb = GnosysDB.openCentral();
3678
+ if (!centralDb.isAvailable()) {
3679
+ centralDb.close();
3680
+ return { ok: false, reason: "central DB unavailable — cannot verify migration" };
3681
+ }
3682
+ let missing = 0;
3683
+ for (const id of localIds) {
3684
+ if (!centralDb.getMemory(id))
3685
+ missing++;
3686
+ }
3687
+ centralDb.close();
3688
+ if (missing > 0) {
3689
+ return { ok: false, reason: `${missing} of ${localIds.length} local memories not found in central DB` };
3690
+ }
3691
+ return { ok: true };
3692
+ }
3693
+ catch (err) {
3694
+ return { ok: false, reason: `inspection failed: ${err instanceof Error ? err.message : String(err)}` };
3695
+ }
3696
+ }
3349
3697
  // ─── gnosys check ─────────────────────────────────────────────────────────
3350
3698
  program
3351
3699
  .command("check")
3352
- .description("Test LLM connectivity for all 5 task configurations (structuring, synthesis, vision, transcription, dream)")
3353
- .option("-d, --directory <dir>", "Project directory (default: cwd)")
3700
+ .description("Test LLM connectivity for each configured task (structuring, synthesis, chat, vision, transcription, dream)")
3701
+ .option("-t, --task <name>", "Test only one task (structuring | synthesis | chat | vision | transcription | dream)")
3354
3702
  .action(async (opts) => {
3355
- const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
3703
+ const projectDir = process.cwd();
3356
3704
  const storePath = path.join(projectDir, ".gnosys");
3357
3705
  const globalStorePath = getGnosysHome();
3358
3706
  // Load config: try project-level first, fall back to global ~/.gnosys/
@@ -3397,6 +3745,13 @@ program
3397
3745
  description: "Q&A answers (gnosys ask)",
3398
3746
  resolve: () => resolveTaskModel(cfg, "synthesis"),
3399
3747
  },
3748
+ {
3749
+ name: "chat",
3750
+ description: "interactive chat (gnosys chat)",
3751
+ // Chat reuses the synthesis task's model — surface it under its own name
3752
+ // so users can see exactly what their TUI will use.
3753
+ resolve: () => resolveTaskModel(cfg, "synthesis"),
3754
+ },
3400
3755
  {
3401
3756
  name: "vision",
3402
3757
  description: "images, PDFs",
@@ -3419,7 +3774,15 @@ program
3419
3774
  let passed = 0;
3420
3775
  let failed = 0;
3421
3776
  let skipped = 0;
3422
- for (const task of tasks) {
3777
+ // Filter to a single task if --task was given.
3778
+ const filteredTasks = opts.task
3779
+ ? tasks.filter((t) => t.name === opts.task)
3780
+ : tasks;
3781
+ if (opts.task && filteredTasks.length === 0) {
3782
+ console.error(`Unknown task: ${opts.task}. Pick one of: ${tasks.map((t) => t.name).join(", ")}`);
3783
+ process.exit(1);
3784
+ }
3785
+ for (const task of filteredTasks) {
3423
3786
  const { provider, model } = task.resolve();
3424
3787
  const label = `${task.name.padEnd(16)} ${DIM}${provider} / ${model}${RESET}`;
3425
3788
  const desc = `${DIM}(${task.description})${RESET}`;
@@ -3735,7 +4098,7 @@ exportCmd
3735
4098
  // ─── gnosys serve ────────────────────────────────────────────────────────
3736
4099
  program
3737
4100
  .command("serve")
3738
- .description("Start the MCP server (stdio mode)")
4101
+ .description("Start the MCP server (stdio mode). Used by IDE integrations — Claude Code/Desktop, Cursor, Codex, etc. spawn this command in the background to talk to gnosys via the Model Context Protocol. You don't normally invoke this yourself; `gnosys init <ide>` wires it into the IDE config.")
3739
4102
  .option("--with-maintenance", "Run maintenance every 6 hours in background")
3740
4103
  .action(async (opts) => {
3741
4104
  if (opts.withMaintenance) {
@@ -3890,31 +4253,33 @@ program
3890
4253
  // ─── gnosys audit ────────────────────────────────────────────────────────
3891
4254
  program
3892
4255
  .command("audit")
3893
- .description("View the structured audit trail of memory operations")
4256
+ .description("View the structured audit trail of memory operations from the central DB")
3894
4257
  .option("--days <n>", "Show entries from the last N days", "7")
3895
- .option("--operation <op>", "Filter by operation type (read, write, recall, etc.)")
4258
+ .option("--operation <op>", "Filter by operation type (read, write, recall, dream_*, etc.)")
3896
4259
  .option("--limit <n>", "Max entries to show")
3897
4260
  .option("--json", "Output raw JSON instead of formatted timeline")
3898
4261
  .action(async (opts) => {
3899
- const resolver = new GnosysResolver();
3900
- await resolver.resolve();
3901
- const stores = resolver.getStores();
3902
- if (stores.length === 0) {
3903
- console.error("No Gnosys stores found. Run 'gnosys init' first.");
4262
+ const { readAuditFromDb, formatAuditTimeline } = await import("./lib/audit.js");
4263
+ const centralDb = GnosysDB.openCentral();
4264
+ if (!centralDb.isAvailable()) {
4265
+ console.error("Central DB unavailable.");
3904
4266
  process.exit(1);
3905
4267
  }
3906
- const { readAuditLog, formatAuditTimeline } = await import("./lib/audit.js");
3907
- const storePath = stores[0].path;
3908
- const entries = readAuditLog(storePath, {
3909
- days: parseInt(opts.days, 10),
3910
- operation: opts.operation,
3911
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
3912
- });
3913
- if (opts.json) {
3914
- console.log(JSON.stringify(entries, null, 2));
4268
+ try {
4269
+ const entries = readAuditFromDb(centralDb, {
4270
+ days: parseInt(opts.days, 10),
4271
+ operation: opts.operation,
4272
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
4273
+ });
4274
+ if (opts.json) {
4275
+ console.log(JSON.stringify(entries, null, 2));
4276
+ }
4277
+ else {
4278
+ console.log(formatAuditTimeline(entries));
4279
+ }
3915
4280
  }
3916
- else {
3917
- console.log(formatAuditTimeline(entries));
4281
+ finally {
4282
+ centralDb.close();
3918
4283
  }
3919
4284
  });
3920
4285
  // ─── gnosys backup ──────────────────────────────────────────────────────
@@ -4116,8 +4481,10 @@ program
4116
4481
  .command("projects")
4117
4482
  .description("List registered projects from the central DB")
4118
4483
  .option("--json", "Output as JSON")
4119
- .option("--all", "Include dead projects (deleted directories, /tmp/ paths)")
4120
- .option("--prune", "Delete registry entries whose directory no longer exists or is a tmp path")
4484
+ .option("--all", "Include dead projects (deleted directories)")
4485
+ .option("--prune", "Delete registry entries whose directory no longer exists (interactive by default)")
4486
+ .option("--dry-run", "With --prune: list what would be deleted, don't actually delete")
4487
+ .option("--yes", "With --prune: skip the confirmation prompt (scripting/automation)")
4121
4488
  .action(async (opts) => {
4122
4489
  let centralDb = null;
4123
4490
  try {
@@ -4128,8 +4495,37 @@ program
4128
4495
  }
4129
4496
  const allProjects = centralDb.getAllProjects();
4130
4497
  if (opts.prune) {
4131
- // Find and delete dead projects
4498
+ // Find dead projects first — never just delete without showing
4499
+ // them. v5.7.0 adds confirmation by default; --yes skips for
4500
+ // scripted use; --dry-run shows the list without deleting.
4132
4501
  const deadProjects = allProjects.filter((p) => isDeadProjectDir(p.working_directory));
4502
+ if (deadProjects.length === 0) {
4503
+ console.log("No dead projects to prune.");
4504
+ return;
4505
+ }
4506
+ const DIM = "\x1b[2m";
4507
+ const RESET = "\x1b[0m";
4508
+ // Always show what would be removed first.
4509
+ console.log(`Found ${deadProjects.length} dead project(s):\n`);
4510
+ for (const p of deadProjects) {
4511
+ const memCount = centralDb.getMemoriesByProject(p.id, true).length;
4512
+ console.log(` ${p.name} ${DIM}${p.working_directory}${RESET} (${memCount} memorie(s))`);
4513
+ }
4514
+ console.log();
4515
+ if (opts.dryRun) {
4516
+ console.log("[dry-run] No changes made. Re-run without --dry-run to delete.");
4517
+ return;
4518
+ }
4519
+ if (!opts.yes) {
4520
+ const readline = await import("readline/promises");
4521
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4522
+ const answer = (await rl.question(`Delete these ${deadProjects.length} project registry entries? [y/N] `)).trim().toLowerCase();
4523
+ rl.close();
4524
+ if (answer !== "y" && answer !== "yes") {
4525
+ console.log("Cancelled.");
4526
+ return;
4527
+ }
4528
+ }
4133
4529
  for (const p of deadProjects) {
4134
4530
  centralDb.deleteProject(p.id);
4135
4531
  }
@@ -4138,19 +4534,7 @@ program
4138
4534
  remaining: allProjects.length - deadProjects.length,
4139
4535
  deletedProjects: deadProjects.map((p) => ({ id: p.id, name: p.name, directory: p.working_directory })),
4140
4536
  }, () => {
4141
- if (deadProjects.length === 0) {
4142
- console.log("No dead projects to prune.");
4143
- }
4144
- else {
4145
- const DIM = "\x1b[2m";
4146
- const RESET = "\x1b[0m";
4147
- console.log(`Pruned ${deadProjects.length} dead project(s):\n`);
4148
- for (const p of deadProjects) {
4149
- console.log(` ${p.name} ${DIM}${p.working_directory}${RESET}`);
4150
- }
4151
- console.log();
4152
- console.log(`${allProjects.length - deadProjects.length} project(s) remain.`);
4153
- }
4537
+ console.log(`✓ Pruned ${deadProjects.length} project(s). ${allProjects.length - deadProjects.length} remain.`);
4154
4538
  });
4155
4539
  return;
4156
4540
  }
@@ -4212,7 +4596,7 @@ program
4212
4596
  // ─── gnosys pref ─────────────────────────────────────────────────────────
4213
4597
  const prefCmd = program
4214
4598
  .command("pref")
4215
- .description("Manage user preferences (stored in central DB, scope='user')");
4599
+ .description("User preferences — small key-value memories scoped to you (not a project), surfaced into every agent's context. Use for cross-project conventions like 'prefer simple solutions' or 'no emoji in UI'. Subcommands: set, get, delete. Review/clean up with `gnosys setup preferences`.");
4216
4600
  prefCmd
4217
4601
  .command("set <key> <value>")
4218
4602
  .description("Set a user preference. Key should be kebab-case (e.g. 'commit-convention').")
@@ -4471,13 +4855,13 @@ program
4471
4855
  });
4472
4856
  // ─── gnosys briefing ─────────────────────────────────────────────────────
4473
4857
  program
4474
- .command("briefing")
4858
+ .command("briefing [projectNameOrId]")
4475
4859
  .description("Generate project briefing — memory state summary, categories, recent activity, top tags")
4476
4860
  .option("-p, --project <id>", "Project ID (auto-detects if omitted)")
4477
4861
  .option("-a, --all", "Generate briefings for all projects")
4478
4862
  .option("-d, --directory <dir>", "Project directory for auto-detection")
4479
4863
  .option("--json", "Output as JSON")
4480
- .action(async (opts) => {
4864
+ .action(async (projectNameOrId, opts) => {
4481
4865
  let centralDb = null;
4482
4866
  try {
4483
4867
  centralDb = GnosysDB.openCentral();
@@ -4503,7 +4887,26 @@ program
4503
4887
  }
4504
4888
  return;
4505
4889
  }
4506
- let pid = opts.project || null;
4890
+ // v5.7.0: accept project name as positional argument in addition to --project <id>.
4891
+ // Resolution order: positional name → --project flag → cwd auto-detect.
4892
+ let pid = opts.project ?? null;
4893
+ if (!pid && projectNameOrId) {
4894
+ // Try as exact ID first, then by name lookup.
4895
+ const byId = centralDb.getProject(projectNameOrId);
4896
+ if (byId) {
4897
+ pid = byId.id;
4898
+ }
4899
+ else {
4900
+ const all = centralDb.getAllProjects();
4901
+ const byName = all.find((p) => p.name === projectNameOrId);
4902
+ if (byName)
4903
+ pid = byName.id;
4904
+ }
4905
+ if (!pid) {
4906
+ console.error(`Project not found: "${projectNameOrId}". Run 'gnosys projects' to list registered projects.`);
4907
+ process.exit(1);
4908
+ }
4909
+ }
4507
4910
  if (!pid)
4508
4911
  pid = await detectCurrentProject(centralDb, opts.directory || undefined);
4509
4912
  if (!pid) {
@@ -4545,79 +4948,91 @@ program
4545
4948
  centralDb?.close();
4546
4949
  }
4547
4950
  });
4548
- // ─── gnosys portfolio ───────────────────────────────────────────────────
4951
+ // `gnosys portfolio` was removed in v5.7.1.
4952
+ // Use `gnosys status --projects` (formerly --global) for the projects
4953
+ // overview, or `gnosys status --web` for the HTML dashboard, or
4954
+ // `gnosys status --projects --output file.html` to write to disk.
4955
+ // ─── gnosys status ──────────────────────────────────────────────────────
4956
+ // v5.7.1 (#11): the catch-all status command. Section flags select what to
4957
+ // show; output flags control format. Default (no flag) is the current
4958
+ // project. `dashboard` and `portfolio` were removed in v5.7.1 — their
4959
+ // content lives under `--system` and `--projects` respectively.
4549
4960
  program
4550
- .command("portfolio")
4551
- .description("Portfolio dashboard all projects with status, roadmap, and recent activity")
4552
- .option("-o, --output <file>", "Write dashboard to a file (auto-detects format from extension)")
4553
- .option("--html", "Output as HTML dashboard")
4961
+ .command("status")
4962
+ .description("Show status. Sections: --projects (all projects) · --remote (sync) · --system (memory/LLM health) · default: current project. Output: --web · --json. Note: 'gnosys dashboard' and 'gnosys portfolio' were removed in v5.7.1 — use 'gnosys status --system' and 'gnosys status --projects' instead.")
4963
+ .option("-d, --directory <dir>", "Project directory (auto-detects if omitted)")
4964
+ .option("-p, --project <id>", "Project ID")
4965
+ .option("-g, --global", "(deprecated alias for --projects)")
4966
+ .option("--projects", "Show all projects portfolio (replaces the old 'gnosys portfolio')")
4967
+ .option("-r, --remote", "Show remote sync status (alias for 'gnosys setup remote status')")
4968
+ .option("-w, --web", "Open the HTML dashboard in the browser")
4969
+ .option("-s, --system", "Show system health (memory count, LLM connectivity, embeddings, archive)")
4554
4970
  .option("--json", "Output as JSON")
4555
4971
  .action(async (opts) => {
4556
- let centralDb = null;
4557
- try {
4558
- centralDb = GnosysDB.openCentral();
4559
- if (!centralDb.isAvailable()) {
4560
- console.error("Central DB not available.");
4561
- process.exit(1);
4562
- }
4563
- const { generatePortfolio, formatPortfolioMarkdown } = await import("./lib/portfolio.js");
4564
- const report = generatePortfolio(centralDb);
4565
- // Detect format from output extension if not explicitly set
4566
- const useHtml = opts.html || (opts.output?.endsWith(".html") ?? false);
4567
- const useJson = opts.json || (opts.output?.endsWith(".json") ?? false);
4568
- if (useJson) {
4569
- const json = JSON.stringify(report, null, 2);
4570
- if (opts.output) {
4571
- const { writeFileSync } = await import("fs");
4572
- writeFileSync(opts.output, json, "utf-8");
4573
- console.log(`Portfolio written to ${opts.output}`);
4972
+ // v5.7.1: --projects supersedes --global (kept as alias).
4973
+ if (opts.projects)
4974
+ opts.global = true;
4975
+ // v5.7.1: --remote — dispatch to RemoteSync.getStatus()
4976
+ if (opts.remote) {
4977
+ let remoteCentralDb = null;
4978
+ try {
4979
+ remoteCentralDb = GnosysDB.openLocal();
4980
+ if (!remoteCentralDb.isAvailable()) {
4981
+ console.error("Central DB not available.");
4982
+ process.exit(1);
4574
4983
  }
4575
- else {
4576
- console.log(json);
4984
+ const remotePath = remoteCentralDb.getMeta("remote_path");
4985
+ if (!remotePath) {
4986
+ if (opts.json) {
4987
+ console.log(JSON.stringify({ configured: false, message: "Remote not configured. Run 'gnosys setup remote'." }, null, 2));
4988
+ }
4989
+ else {
4990
+ console.log("Remote sync: not configured. Run 'gnosys setup remote' to set up multi-machine sync.");
4991
+ }
4992
+ return;
4577
4993
  }
4578
- return;
4579
- }
4580
- if (useHtml) {
4581
- const { generatePortfolioHtml } = await import("./lib/portfolioHtml.js");
4582
- const html = generatePortfolioHtml(report, opts.output);
4583
- if (opts.output) {
4584
- const { writeFileSync } = await import("fs");
4585
- writeFileSync(opts.output, html, "utf-8");
4586
- console.log(`Portfolio dashboard written to ${opts.output}`);
4994
+ const { RemoteSync, formatStatus } = await import("./lib/remote.js");
4995
+ const { withHeartbeat } = await import("./lib/heartbeat.js");
4996
+ const sync = new RemoteSync(remoteCentralDb, remotePath);
4997
+ const status = await withHeartbeat("Checking remote sync status", () => sync.getStatus());
4998
+ sync.closeRemote();
4999
+ if (opts.json) {
5000
+ console.log(JSON.stringify(status, null, 2));
4587
5001
  }
4588
5002
  else {
4589
- console.log(html);
5003
+ console.log(formatStatus(status));
4590
5004
  }
4591
5005
  return;
4592
5006
  }
4593
- const markdown = formatPortfolioMarkdown(report);
4594
- if (opts.output) {
4595
- const { writeFileSync } = await import("fs");
4596
- writeFileSync(opts.output, markdown, "utf-8");
4597
- console.log(`Portfolio dashboard written to ${opts.output}`);
5007
+ catch (err) {
5008
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
5009
+ process.exit(1);
4598
5010
  }
4599
- else {
4600
- console.log(markdown);
5011
+ finally {
5012
+ remoteCentralDb?.close();
4601
5013
  }
4602
5014
  }
4603
- catch (err) {
4604
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
4605
- process.exit(1);
4606
- }
4607
- finally {
4608
- centralDb?.close();
5015
+ // --system delegates to the dashboard formatter (formerly `gnosys dashboard`).
5016
+ if (opts.system) {
5017
+ const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");
5018
+ const resolver = await getResolver();
5019
+ const stores = resolver.getStores();
5020
+ if (stores.length === 0) {
5021
+ console.error("No Gnosys stores found. Run gnosys init first.");
5022
+ process.exit(1);
5023
+ }
5024
+ const cfg = await loadConfig(stores[0].path);
5025
+ let dashDb;
5026
+ try {
5027
+ const db = GnosysDB.openCentral();
5028
+ if (db.isAvailable() && db.isMigrated())
5029
+ dashDb = db;
5030
+ }
5031
+ catch { /* non-fatal */ }
5032
+ const data = await collectDashboardData(resolver, cfg, pkg.version, dashDb);
5033
+ console.log(opts.json ? formatDashboardJSON(data) : formatDashboard(data));
5034
+ return;
4609
5035
  }
4610
- });
4611
- // ─── gnosys status ──────────────────────────────────────────────────────
4612
- program
4613
- .command("status")
4614
- .description("Show project status. From a project dir: shows that project. With --global: shows all projects. With --web: opens the HTML dashboard.")
4615
- .option("-d, --directory <dir>", "Project directory (auto-detects if omitted)")
4616
- .option("-p, --project <id>", "Project ID")
4617
- .option("-g, --global", "Show all projects")
4618
- .option("-w, --web", "Open the HTML dashboard in the browser")
4619
- .option("--json", "Output as JSON")
4620
- .action(async (opts) => {
4621
5036
  let centralDb = null;
4622
5037
  try {
4623
5038
  centralDb = GnosysDB.openCentral();
@@ -4840,7 +5255,7 @@ program
4840
5255
  // ─── gnosys sandbox start|stop|status ─────────────────────────────────────
4841
5256
  const sandboxCmd = program
4842
5257
  .command("sandbox")
4843
- .description("Manage the Gnosys sandbox background process");
5258
+ .description("Manage the Gnosys sandbox — a long-lived background process that holds the SQLite handle so agents can call gnosys.add()/recall() through a tiny helper library instead of paying the MCP roundtrip on every call. Lower latency, lower context cost. Most users don't need this; it's for high-throughput agent workflows.");
4844
5259
  sandboxCmd
4845
5260
  .command("start")
4846
5261
  .description("Start the Gnosys sandbox background process")
@@ -4923,7 +5338,7 @@ sandboxCmd
4923
5338
  // ─── gnosys helper generate ───────────────────────────────────────────────
4924
5339
  const helperCmd = program
4925
5340
  .command("helper")
4926
- .description("Manage the Gnosys helper library for agent integration");
5341
+ .description("Generate a tiny TypeScript helper library that agents import to talk to the gnosys sandbox directly. Pairs with `gnosys sandbox start` — agents call gnosys.add()/recall() like normal code instead of issuing MCP tool calls. Run `gnosys helper generate` in your agent's project to drop in `gnosys-helper.ts`.");
4927
5342
  helperCmd
4928
5343
  .command("generate")
4929
5344
  .description("Generate a gnosys-helper.ts file in the current directory (or specified directory)")