gnosys 5.5.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +44 -0
  2. package/dist/cli.js +959 -438
  3. package/dist/cli.js.map +1 -1
  4. package/dist/lib/audit.d.ts +13 -0
  5. package/dist/lib/audit.d.ts.map +1 -1
  6. package/dist/lib/audit.js +42 -0
  7. package/dist/lib/audit.js.map +1 -1
  8. package/dist/lib/chat/choose.d.ts +75 -0
  9. package/dist/lib/chat/choose.d.ts.map +1 -0
  10. package/dist/lib/chat/choose.js +146 -0
  11. package/dist/lib/chat/choose.js.map +1 -0
  12. package/dist/lib/chat/commands.d.ts +96 -0
  13. package/dist/lib/chat/commands.d.ts.map +1 -0
  14. package/dist/lib/chat/commands.js +367 -0
  15. package/dist/lib/chat/commands.js.map +1 -0
  16. package/dist/lib/chat/focus.d.ts +70 -0
  17. package/dist/lib/chat/focus.d.ts.map +1 -0
  18. package/dist/lib/chat/focus.js +120 -0
  19. package/dist/lib/chat/focus.js.map +1 -0
  20. package/dist/lib/chat/index.d.ts +32 -0
  21. package/dist/lib/chat/index.d.ts.map +1 -0
  22. package/dist/lib/chat/index.js +151 -0
  23. package/dist/lib/chat/index.js.map +1 -0
  24. package/dist/lib/chat/intent.d.ts +100 -0
  25. package/dist/lib/chat/intent.d.ts.map +1 -0
  26. package/dist/lib/chat/intent.js +192 -0
  27. package/dist/lib/chat/intent.js.map +1 -0
  28. package/dist/lib/chat/llmTurn.d.ts +55 -0
  29. package/dist/lib/chat/llmTurn.d.ts.map +1 -0
  30. package/dist/lib/chat/llmTurn.js +103 -0
  31. package/dist/lib/chat/llmTurn.js.map +1 -0
  32. package/dist/lib/chat/recall.d.ts +58 -0
  33. package/dist/lib/chat/recall.d.ts.map +1 -0
  34. package/dist/lib/chat/recall.js +109 -0
  35. package/dist/lib/chat/recall.js.map +1 -0
  36. package/dist/lib/chat/render.d.ts +30 -0
  37. package/dist/lib/chat/render.d.ts.map +1 -0
  38. package/dist/lib/chat/render.js +755 -0
  39. package/dist/lib/chat/render.js.map +1 -0
  40. package/dist/lib/chat/session.d.ts +121 -0
  41. package/dist/lib/chat/session.d.ts.map +1 -0
  42. package/dist/lib/chat/session.js +148 -0
  43. package/dist/lib/chat/session.js.map +1 -0
  44. package/dist/lib/chat/toolFence.d.ts +54 -0
  45. package/dist/lib/chat/toolFence.d.ts.map +1 -0
  46. package/dist/lib/chat/toolFence.js +90 -0
  47. package/dist/lib/chat/toolFence.js.map +1 -0
  48. package/dist/lib/chat/tools.d.ts +48 -0
  49. package/dist/lib/chat/tools.d.ts.map +1 -0
  50. package/dist/lib/chat/tools.js +338 -0
  51. package/dist/lib/chat/tools.js.map +1 -0
  52. package/dist/lib/chat/types.d.ts +42 -0
  53. package/dist/lib/chat/types.d.ts.map +1 -0
  54. package/dist/lib/chat/types.js +6 -0
  55. package/dist/lib/chat/types.js.map +1 -0
  56. package/dist/lib/chat/write.d.ts +66 -0
  57. package/dist/lib/chat/write.d.ts.map +1 -0
  58. package/dist/lib/chat/write.js +203 -0
  59. package/dist/lib/chat/write.js.map +1 -0
  60. package/dist/lib/db.d.ts +41 -1
  61. package/dist/lib/db.d.ts.map +1 -1
  62. package/dist/lib/db.js +136 -28
  63. package/dist/lib/db.js.map +1 -1
  64. package/dist/lib/exportProject.d.ts +51 -0
  65. package/dist/lib/exportProject.d.ts.map +1 -0
  66. package/dist/lib/exportProject.js +72 -0
  67. package/dist/lib/exportProject.js.map +1 -0
  68. package/dist/lib/importProject.d.ts +35 -0
  69. package/dist/lib/importProject.d.ts.map +1 -0
  70. package/dist/lib/importProject.js +135 -0
  71. package/dist/lib/importProject.js.map +1 -0
  72. package/dist/lib/remote.d.ts +23 -0
  73. package/dist/lib/remote.d.ts.map +1 -1
  74. package/dist/lib/remote.js +88 -0
  75. package/dist/lib/remote.js.map +1 -1
  76. package/dist/lib/remoteWizard.d.ts.map +1 -1
  77. package/dist/lib/remoteWizard.js +13 -17
  78. package/dist/lib/remoteWizard.js.map +1 -1
  79. package/dist/lib/setup/sections/ides.d.ts +20 -0
  80. package/dist/lib/setup/sections/ides.d.ts.map +1 -0
  81. package/dist/lib/setup/sections/ides.js +124 -0
  82. package/dist/lib/setup/sections/ides.js.map +1 -0
  83. package/dist/lib/setup/sections/preferences.d.ts +30 -0
  84. package/dist/lib/setup/sections/preferences.d.ts.map +1 -0
  85. package/dist/lib/setup/sections/preferences.js +128 -0
  86. package/dist/lib/setup/sections/preferences.js.map +1 -0
  87. package/dist/lib/setup/sections/routing.d.ts +21 -0
  88. package/dist/lib/setup/sections/routing.d.ts.map +1 -0
  89. package/dist/lib/setup/sections/routing.js +160 -0
  90. package/dist/lib/setup/sections/routing.js.map +1 -0
  91. package/dist/lib/setup/summary.d.ts +42 -0
  92. package/dist/lib/setup/summary.d.ts.map +1 -0
  93. package/dist/lib/setup/summary.js +206 -0
  94. package/dist/lib/setup/summary.js.map +1 -0
  95. package/dist/lib/timeline.d.ts +7 -0
  96. package/dist/lib/timeline.d.ts.map +1 -1
  97. package/dist/lib/timeline.js +19 -5
  98. package/dist/lib/timeline.js.map +1 -1
  99. package/package.json +7 -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";
@@ -108,6 +108,32 @@ function maybePrintUpgradeNudge() {
108
108
  }
109
109
  }
110
110
  maybePrintUpgradeNudge();
111
+ /**
112
+ * v5.6.0 back-compat shim: rewrite `gnosys export --to <dir>` →
113
+ * `gnosys export vault --to <dir>` before commander parses argv. The v5.6.0
114
+ * restructure made `export` a parent command with `vault` and `project`
115
+ * subcommands; without this shim, the bare `--to` form prints usage instead
116
+ * of running the vault export.
117
+ *
118
+ * Pattern: argv[2]==="export" AND argv[3] is not a known subcommand AND any
119
+ * of the v5.5.x flags appear (`--to`, `--all`, `--overwrite`, etc.).
120
+ */
121
+ function rewriteLegacyExport() {
122
+ if (process.argv[2] !== "export")
123
+ return;
124
+ const next = process.argv[3];
125
+ if (next === "vault" || next === "project" || next === "--help" || next === "-h")
126
+ return;
127
+ // Any v5.5.x-style flag → assume legacy vault invocation
128
+ const looksLegacy = process.argv.slice(3).some((a) => a === "--to" || a.startsWith("--to=") ||
129
+ a === "--all" || a === "--overwrite" ||
130
+ a === "--no-summaries" || a === "--no-reviews" || a === "--no-graph" ||
131
+ a === "--json");
132
+ if (looksLegacy) {
133
+ process.argv.splice(3, 0, "vault");
134
+ }
135
+ }
136
+ rewriteLegacyExport();
111
137
  async function getResolver() {
112
138
  const resolver = new GnosysResolver();
113
139
  await resolver.resolve();
@@ -137,6 +163,23 @@ program
137
163
  .name("gnosys")
138
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.")
139
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
+ `)
140
183
  .hook("preAction", async () => {
141
184
  // Check if central DB was upgraded to a newer version on another machine
142
185
  try {
@@ -517,13 +560,26 @@ program
517
560
  const setupCmd = program
518
561
  .command("setup")
519
562
  .description("Configure Gnosys — LLM provider, models, remote sync, and IDE integration");
520
- // Bare `gnosys setup` runs the full interactive wizard
563
+ // Bare `gnosys setup` when config exists, opens the summary-first menu
564
+ // so the user can edit one section without re-running the whole wizard.
565
+ // First-time setup or `--full` runs the linear 5-step flow.
521
566
  setupCmd
522
567
  .option("--non-interactive", "Skip prompts, use defaults (for CI/scripting)")
568
+ .option("--full", "Run the linear 5-step wizard even when a config exists")
523
569
  .action(async (opts) => {
524
570
  const { runSetup } = await import("./lib/setup.js");
571
+ const projectDir = process.cwd();
572
+ // Detect existing config — if present and the user didn't pass --full,
573
+ // route to the summary-first menu.
574
+ const configPath = path.join(os.homedir(), ".gnosys", "gnosys.json");
575
+ const hasConfig = existsSync(configPath);
576
+ if (hasConfig && !opts.full && !opts.nonInteractive) {
577
+ const { runSummaryWizard } = await import("./lib/setup/summary.js");
578
+ await runSummaryWizard({ directory: projectDir });
579
+ return;
580
+ }
525
581
  await runSetup({
526
- directory: process.cwd(),
582
+ directory: projectDir,
527
583
  nonInteractive: opts.nonInteractive,
528
584
  });
529
585
  });
@@ -543,14 +599,17 @@ setupCmd
543
599
  validate: opts.validate,
544
600
  });
545
601
  });
546
- // `gnosys setup remote` — configure remote sync (alias for `gnosys remote configure`)
547
- setupCmd
602
+ // ─── gnosys setup remote (parent + subcommands) ────────────────────────
603
+ // v5.7.0: the standalone `gnosys remote` parent was dropped; everything
604
+ // (configure, status, push, pull, sync, resolve) lives here now.
605
+ const setupRemoteCmd = setupCmd
548
606
  .command("remote")
549
- .description("Configure multi-machine sync (NAS/shared drive)")
607
+ .description("Multi-machine sync configure, sync, and resolve conflicts");
608
+ // Bare `gnosys setup remote` — configure wizard (back-compat with v5.6.x)
609
+ setupRemoteCmd
550
610
  .option("--path <path>", "Set remote path directly (non-interactive)")
551
611
  .action(async (opts) => {
552
612
  const { GnosysDB } = await import("./lib/db.js");
553
- // Sync configuration needs explicit local DB access (not auto-routed remote).
554
613
  const db = GnosysDB.openLocal();
555
614
  if (!db.isAvailable()) {
556
615
  console.error("Central DB not available.");
@@ -571,6 +630,233 @@ setupCmd
571
630
  db.close();
572
631
  }
573
632
  });
633
+ setupRemoteCmd
634
+ .command("status")
635
+ .description("Show remote sync status: pending changes, conflicts, last sync")
636
+ .option("--json", "Output as JSON")
637
+ .action(async (opts) => {
638
+ let centralDb = null;
639
+ try {
640
+ centralDb = GnosysDB.openLocal();
641
+ if (!centralDb.isAvailable()) {
642
+ console.error("Central DB not available.");
643
+ process.exit(1);
644
+ }
645
+ const remotePath = centralDb.getMeta("remote_path");
646
+ if (!remotePath) {
647
+ if (opts.json) {
648
+ console.log(JSON.stringify({ configured: false, message: "Remote not configured. Run 'gnosys setup remote'." }, null, 2));
649
+ }
650
+ else {
651
+ console.log("Remote sync: not configured.");
652
+ console.log("Run 'gnosys setup remote' to set up multi-machine sync.");
653
+ }
654
+ return;
655
+ }
656
+ const { RemoteSync, formatStatus } = await import("./lib/remote.js");
657
+ const sync = new RemoteSync(centralDb, remotePath);
658
+ const status = await sync.getStatus();
659
+ sync.closeRemote();
660
+ if (opts.json) {
661
+ console.log(JSON.stringify(status, null, 2));
662
+ }
663
+ else {
664
+ console.log(formatStatus(status));
665
+ if (status.conflicts.length > 0) {
666
+ console.log("\nConflicts:");
667
+ for (const c of status.conflicts) {
668
+ console.log(` ${c.memoryId}: ${c.title}`);
669
+ console.log(` local: ${c.localModified}`);
670
+ console.log(` remote: ${c.remoteModified}`);
671
+ }
672
+ console.log("\nResolve with: gnosys setup remote resolve <memory-id> --keep <local|remote>");
673
+ }
674
+ }
675
+ }
676
+ catch (err) {
677
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
678
+ process.exit(1);
679
+ }
680
+ finally {
681
+ centralDb?.close();
682
+ }
683
+ });
684
+ setupRemoteCmd
685
+ .command("push")
686
+ .description("Push local changes to remote")
687
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
688
+ .action(async (opts) => {
689
+ let centralDb = null;
690
+ try {
691
+ centralDb = GnosysDB.openLocal();
692
+ if (!centralDb.isAvailable()) {
693
+ console.error("Central DB not available.");
694
+ process.exit(1);
695
+ }
696
+ const remotePath = centralDb.getMeta("remote_path");
697
+ if (!remotePath) {
698
+ console.error("Remote not configured.");
699
+ process.exit(1);
700
+ }
701
+ const { RemoteSync } = await import("./lib/remote.js");
702
+ const sync = new RemoteSync(centralDb, remotePath);
703
+ const result = await sync.push({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
704
+ sync.closeRemote();
705
+ const projParts = (result.projectsPushed || 0) > 0 ? ` | Projects pushed: ${result.projectsPushed}` : "";
706
+ const auditParts = (result.auditPushed || 0) > 0 ? ` | Audit pushed: ${result.auditPushed}` : "";
707
+ console.log(`Pushed: ${result.pushed} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}${auditParts}`);
708
+ if (result.errors.length > 0) {
709
+ console.log("\nErrors:");
710
+ for (const e of result.errors)
711
+ console.log(` ${e}`);
712
+ }
713
+ if (result.conflicts.length > 0) {
714
+ console.log("\nConflicts flagged (run 'gnosys setup remote status' for details):");
715
+ for (const c of result.conflicts)
716
+ console.log(` ${c.memoryId} — ${c.title}`);
717
+ }
718
+ }
719
+ catch (err) {
720
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
721
+ process.exit(1);
722
+ }
723
+ finally {
724
+ centralDb?.close();
725
+ }
726
+ });
727
+ setupRemoteCmd
728
+ .command("pull")
729
+ .description("Pull remote changes to local")
730
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
731
+ .action(async (opts) => {
732
+ let centralDb = null;
733
+ try {
734
+ centralDb = GnosysDB.openLocal();
735
+ if (!centralDb.isAvailable()) {
736
+ console.error("Central DB not available.");
737
+ process.exit(1);
738
+ }
739
+ const remotePath = centralDb.getMeta("remote_path");
740
+ if (!remotePath) {
741
+ console.error("Remote not configured.");
742
+ process.exit(1);
743
+ }
744
+ const { RemoteSync } = await import("./lib/remote.js");
745
+ const sync = new RemoteSync(centralDb, remotePath);
746
+ const result = await sync.pull({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
747
+ sync.closeRemote();
748
+ const projParts = (result.projectsPulled || 0) > 0 ? ` | Projects pulled: ${result.projectsPulled}` : "";
749
+ const auditParts = (result.auditPulled || 0) > 0 ? ` | Audit pulled: ${result.auditPulled}` : "";
750
+ console.log(`Pulled: ${result.pulled} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}${auditParts}`);
751
+ if (result.errors.length > 0) {
752
+ console.log("\nErrors:");
753
+ for (const e of result.errors)
754
+ console.log(` ${e}`);
755
+ }
756
+ }
757
+ catch (err) {
758
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
759
+ process.exit(1);
760
+ }
761
+ finally {
762
+ centralDb?.close();
763
+ }
764
+ });
765
+ setupRemoteCmd
766
+ .command("sync")
767
+ .description("Two-way sync: push local changes then pull remote changes")
768
+ .option("--auto", "Run silently for cron/LaunchAgent (skip-and-flag for conflicts)")
769
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
770
+ .action(async (opts) => {
771
+ let centralDb = null;
772
+ try {
773
+ centralDb = GnosysDB.openLocal();
774
+ if (!centralDb.isAvailable()) {
775
+ if (!opts.auto)
776
+ console.error("Central DB not available.");
777
+ process.exit(1);
778
+ }
779
+ const remotePath = centralDb.getMeta("remote_path");
780
+ if (!remotePath) {
781
+ if (!opts.auto)
782
+ console.error("Remote not configured.");
783
+ process.exit(opts.auto ? 0 : 1);
784
+ }
785
+ const { RemoteSync } = await import("./lib/remote.js");
786
+ const sync = new RemoteSync(centralDb, remotePath);
787
+ const result = await sync.sync({
788
+ auto: opts.auto,
789
+ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
790
+ });
791
+ sync.closeRemote();
792
+ if (!opts.auto || result.conflicts.length > 0 || result.errors.length > 0) {
793
+ const pp = result.projectsPushed || 0;
794
+ const pl = result.projectsPulled || 0;
795
+ const ap = result.auditPushed || 0;
796
+ const al = result.auditPulled || 0;
797
+ const projParts = (pp + pl) > 0 ? ` | Projects: ↑${pp}/↓${pl}` : "";
798
+ const auditParts = (ap + al) > 0 ? ` | Audit: ↑${ap}/↓${al}` : "";
799
+ console.log(`Pushed: ${result.pushed} | Pulled: ${result.pulled} | Conflicts: ${result.conflicts.length}${projParts}${auditParts}`);
800
+ if (result.errors.length > 0) {
801
+ console.log("\nErrors:");
802
+ for (const e of result.errors)
803
+ console.log(` ${e}`);
804
+ }
805
+ if (result.conflicts.length > 0) {
806
+ console.log("\nConflicts need resolution (run 'gnosys setup remote status' for details).");
807
+ }
808
+ }
809
+ }
810
+ catch (err) {
811
+ if (!opts.auto)
812
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
813
+ process.exit(1);
814
+ }
815
+ finally {
816
+ centralDb?.close();
817
+ }
818
+ });
819
+ setupRemoteCmd
820
+ .command("resolve <memoryId>")
821
+ .description("Resolve a sync conflict by choosing local, remote, or merged content")
822
+ .option("--keep <choice>", "Choice: local | remote", "local")
823
+ .action(async (memoryId, opts) => {
824
+ let centralDb = null;
825
+ try {
826
+ centralDb = GnosysDB.openLocal();
827
+ if (!centralDb.isAvailable()) {
828
+ console.error("Central DB not available.");
829
+ process.exit(1);
830
+ }
831
+ const remotePath = centralDb.getMeta("remote_path");
832
+ if (!remotePath) {
833
+ console.error("Remote not configured.");
834
+ process.exit(1);
835
+ }
836
+ if (opts.keep !== "local" && opts.keep !== "remote") {
837
+ console.error(`--keep must be 'local' or 'remote' (got: ${opts.keep})`);
838
+ process.exit(1);
839
+ }
840
+ const { RemoteSync } = await import("./lib/remote.js");
841
+ const sync = new RemoteSync(centralDb, remotePath);
842
+ const result = await sync.resolve(memoryId, opts.keep);
843
+ sync.closeRemote();
844
+ if (result.ok) {
845
+ console.log(`Resolved ${memoryId}: kept ${opts.keep} version.`);
846
+ }
847
+ else {
848
+ console.error(`Failed to resolve: ${result.error}`);
849
+ process.exit(1);
850
+ }
851
+ }
852
+ catch (err) {
853
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
854
+ process.exit(1);
855
+ }
856
+ finally {
857
+ centralDb?.close();
858
+ }
859
+ });
574
860
  // `gnosys setup dream` — configure dream mode (designation, provider, schedule)
575
861
  setupCmd
576
862
  .command("dream")
@@ -579,6 +865,51 @@ setupCmd
579
865
  const { runDreamSetup } = await import("./lib/setup.js");
580
866
  await runDreamSetup({ directory: process.cwd() });
581
867
  });
868
+ // `gnosys setup ides` — configure IDE / MCP integrations standalone
869
+ setupCmd
870
+ .command("ides")
871
+ .description("Configure IDE integrations (Claude Code/Desktop, Cursor, Codex, Gemini CLI, Antigravity)")
872
+ .action(async () => {
873
+ const readline = await import("readline/promises");
874
+ const { runIdesSetup } = await import("./lib/setup/sections/ides.js");
875
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
876
+ try {
877
+ await runIdesSetup({ rl, directory: process.cwd() });
878
+ }
879
+ finally {
880
+ rl.close();
881
+ }
882
+ });
883
+ // `gnosys setup routing` — task-routing wizard standalone
884
+ setupCmd
885
+ .command("routing")
886
+ .description("Configure per-task LLM routing (structuring, synthesis, vision, transcription, dream)")
887
+ .action(async () => {
888
+ const readline = await import("readline/promises");
889
+ const { runRoutingSetup } = await import("./lib/setup/sections/routing.js");
890
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
891
+ try {
892
+ await runRoutingSetup({ rl, directory: process.cwd() });
893
+ }
894
+ finally {
895
+ rl.close();
896
+ }
897
+ });
898
+ // `gnosys setup preferences` — review user-scope preferences
899
+ setupCmd
900
+ .command("preferences")
901
+ .description("Review and clean up user-scope preferences (incl. legacy imports)")
902
+ .action(async () => {
903
+ const readline = await import("readline/promises");
904
+ const { runPreferencesReview } = await import("./lib/setup/sections/preferences.js");
905
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
906
+ try {
907
+ await runPreferencesReview(rl);
908
+ }
909
+ finally {
910
+ rl.close();
911
+ }
912
+ });
582
913
  // v5.4.2 removal: `gnosys models` (top-level shortcut) was removed in favor
583
914
  // of the canonical `gnosys setup models` form. The implementation function
584
915
  // runModelsCommand() in setup.ts is no longer wired but kept for now in case
@@ -1218,21 +1549,60 @@ program
1218
1549
  centralDb?.close();
1219
1550
  }
1220
1551
  });
1221
- // ─── gnosys ingest <file> ─────────────────────────────────────────────────
1552
+ // ─── gnosys chat (TUI) ───────────────────────────────────────────────────
1222
1553
  program
1223
- .command("ingest <fileOrGlob>")
1224
- .description("Ingest a file (PDF, DOCX, TXT, MD) into Gnosys memory. Extracts text, splits into chunks, and creates atomic memories.")
1225
- .option("--mode <mode>", "Ingestion mode: llm or structured", "llm")
1226
- .option("-s, --store <store>", "Target store: project, personal, global")
1227
- .option("-a, --author <author>", "Author", "human")
1228
- .option("--authority <authority>", "Authority level", "imported")
1229
- .option("--dry-run", "Preview what would be created without writing")
1230
- .option("--list-attachments", "List all stored attachments")
1231
- .option("-d, --directory <dir>", "Project directory")
1232
- .action(async (fileOrGlob, opts) => {
1233
- // List attachments mode
1234
- if (opts.listAttachments) {
1235
- const { listAttachments } = await import("./lib/attachments.js");
1554
+ .command("chat")
1555
+ .description("Interactive memory-aware terminal chat (TUI)")
1556
+ .option("--resume <sessionId>", "Resume an existing chat session")
1557
+ .option("--list", "List recent chat sessions and exit")
1558
+ .option("--search <query>", "Full-text search across session logs")
1559
+ .option("--provider <name>", "Override LLM provider (anthropic, openai, groq, ollama, …)")
1560
+ .option("--model <name>", "Override LLM model name")
1561
+ .option("--limit <n>", "Limit for --list / --search (default 20)", "20")
1562
+ .action(async (opts) => {
1563
+ const limit = parseInt(opts.limit, 10) || 20;
1564
+ const chat = await import("./lib/chat/index.js");
1565
+ if (opts.list) {
1566
+ chat.printSessionList(limit);
1567
+ return;
1568
+ }
1569
+ if (opts.search) {
1570
+ chat.printSearchResults(opts.search, limit);
1571
+ return;
1572
+ }
1573
+ // Interactive chat
1574
+ const resolver = await getResolver();
1575
+ const stores = resolver.getStores();
1576
+ const storePath = stores[0]?.path ?? process.cwd();
1577
+ let cliConfig;
1578
+ try {
1579
+ cliConfig = await loadConfig(storePath);
1580
+ }
1581
+ catch {
1582
+ cliConfig = (await import("./lib/config.js")).DEFAULT_CONFIG;
1583
+ }
1584
+ await chat.startChat({
1585
+ config: cliConfig,
1586
+ resume: opts.resume,
1587
+ providerName: opts.provider,
1588
+ modelName: opts.model,
1589
+ });
1590
+ });
1591
+ // ─── gnosys ingest <file> ─────────────────────────────────────────────────
1592
+ program
1593
+ .command("ingest <fileOrGlob>")
1594
+ .description("Ingest a file (PDF, DOCX, TXT, MD) into Gnosys memory. Extracts text, splits into chunks, and creates atomic memories.")
1595
+ .option("--mode <mode>", "Ingestion mode: llm or structured", "llm")
1596
+ .option("-s, --store <store>", "Target store: project, personal, global")
1597
+ .option("-a, --author <author>", "Author", "human")
1598
+ .option("--authority <authority>", "Authority level", "imported")
1599
+ .option("--dry-run", "Preview what would be created without writing")
1600
+ .option("--list-attachments", "List all stored attachments")
1601
+ .option("-d, --directory <dir>", "Project directory")
1602
+ .action(async (fileOrGlob, opts) => {
1603
+ // List attachments mode
1604
+ if (opts.listAttachments) {
1605
+ const { listAttachments } = await import("./lib/attachments.js");
1236
1606
  const resolver = await getResolver();
1237
1607
  const writeTarget = resolver.getWriteTarget(opts.store || undefined);
1238
1608
  if (!writeTarget) {
@@ -1616,34 +1986,51 @@ program
1616
1986
  .command("timeline")
1617
1987
  .description("Show when memories were created and modified over time")
1618
1988
  .option("-p, --period <period>", "Group by: day, week, month (default), year", "month")
1989
+ .option("--project <id>", "Filter to a specific project ID (default: all projects)")
1990
+ .option("--limit-titles <n>", "Show titles inline when an entry has <= N memories (default 5)", "5")
1619
1991
  .action(async (opts) => {
1620
- const resolver = await getResolver();
1621
- const allMemories = await resolver.getAllMemories();
1622
- if (allMemories.length === 0) {
1623
- console.log("No memories found.");
1624
- return;
1992
+ const { groupDbByPeriod } = await import("./lib/timeline.js");
1993
+ const centralDb = GnosysDB.openCentral();
1994
+ if (!centralDb.isAvailable()) {
1995
+ console.error("Central DB unavailable.");
1996
+ process.exit(1);
1625
1997
  }
1626
- const entries = groupByPeriod(allMemories, opts.period);
1627
- console.log(`Knowledge Timeline (by ${opts.period}):\n`);
1628
- for (const entry of entries) {
1629
- const parts = [];
1630
- if (entry.created > 0)
1631
- parts.push(`${entry.created} created`);
1632
- if (entry.modified > 0)
1633
- parts.push(`${entry.modified} modified`);
1634
- console.log(` ${entry.period}: ${parts.join(", ")}`);
1635
- if (entry.titles.length > 0 && entry.titles.length <= 5) {
1636
- for (const t of entry.titles) {
1637
- console.log(` + ${t}`);
1998
+ try {
1999
+ const memories = opts.project
2000
+ ? centralDb.getMemoriesByProject(opts.project)
2001
+ : centralDb.getActiveMemories();
2002
+ if (memories.length === 0) {
2003
+ console.log("No memories found.");
2004
+ return;
2005
+ }
2006
+ const entries = groupDbByPeriod(memories, opts.period);
2007
+ const titleLimit = Math.max(0, parseInt(opts.limitTitles, 10) || 5);
2008
+ console.log(`Knowledge Timeline (by ${opts.period}, ${memories.length} memories):\n`);
2009
+ for (const entry of entries) {
2010
+ const parts = [];
2011
+ if (entry.created > 0)
2012
+ parts.push(`${entry.created} created`);
2013
+ if (entry.modified > 0)
2014
+ parts.push(`${entry.modified} modified`);
2015
+ console.log(` ${entry.period}: ${parts.join(", ")}`);
2016
+ if (entry.titles.length > 0 && entry.titles.length <= titleLimit) {
2017
+ for (const t of entry.titles) {
2018
+ console.log(` + ${t}`);
2019
+ }
1638
2020
  }
1639
2021
  }
1640
2022
  }
2023
+ finally {
2024
+ centralDb.close();
2025
+ }
1641
2026
  });
1642
2027
  // ─── gnosys stats ───────────────────────────────────────────────────────
1643
2028
  program
1644
2029
  .command("stats")
1645
- .description("Show summary statistics for the memory store")
2030
+ .description("Show summary statistics for the memory store. Use --by-project for a per-project breakdown across the central DB.")
1646
2031
  .option("--json", "Output as JSON")
2032
+ .option("--by-project", "Show a per-project breakdown table instead of single-store stats")
2033
+ .option("--all", "Include all projects (don't filter to current project)")
1647
2034
  .action(async (opts) => {
1648
2035
  let centralDb = null;
1649
2036
  try {
@@ -1652,8 +2039,66 @@ program
1652
2039
  console.error("Central DB not available. Run 'gnosys init' first.");
1653
2040
  process.exit(1);
1654
2041
  }
2042
+ // v5.7.0: --by-project shows a per-project breakdown across the entire
2043
+ // central DB (memories, archived, never reinforced, etc.) as a table.
2044
+ if (opts.byProject) {
2045
+ const projects = centralDb.getAllProjects();
2046
+ const all = centralDb.getAllMemories();
2047
+ const rows = projects.map((p) => {
2048
+ const ms = all.filter((m) => m.project_id === p.id);
2049
+ const active = ms.filter((m) => m.tier === "active" && m.status === "active").length;
2050
+ const archived = ms.filter((m) => m.tier === "archive").length;
2051
+ const reinforced = ms.reduce((sum, m) => sum + (m.reinforcement_count ?? 0), 0);
2052
+ const lastTouch = ms.reduce((m, x) => (x.modified > m ? x.modified : m), "0");
2053
+ return { name: p.name, id: p.id, active, archived, reinforced, lastTouch };
2054
+ });
2055
+ // User/global memories (no project_id)
2056
+ const userScope = all.filter((m) => !m.project_id && m.scope === "user");
2057
+ const globalScope = all.filter((m) => !m.project_id && m.scope === "global");
2058
+ if (userScope.length > 0) {
2059
+ rows.push({
2060
+ name: "(user)",
2061
+ id: "—",
2062
+ active: userScope.filter((m) => m.tier === "active" && m.status === "active").length,
2063
+ archived: userScope.filter((m) => m.tier === "archive").length,
2064
+ reinforced: userScope.reduce((sum, m) => sum + (m.reinforcement_count ?? 0), 0),
2065
+ lastTouch: userScope.reduce((m, x) => (x.modified > m ? x.modified : m), "0"),
2066
+ });
2067
+ }
2068
+ if (globalScope.length > 0) {
2069
+ rows.push({
2070
+ name: "(global)",
2071
+ id: "—",
2072
+ active: globalScope.filter((m) => m.tier === "active" && m.status === "active").length,
2073
+ archived: globalScope.filter((m) => m.tier === "archive").length,
2074
+ reinforced: globalScope.reduce((sum, m) => sum + (m.reinforcement_count ?? 0), 0),
2075
+ lastTouch: globalScope.reduce((m, x) => (x.modified > m ? x.modified : m), "0"),
2076
+ });
2077
+ }
2078
+ rows.sort((a, b) => b.active - a.active);
2079
+ if (opts.json) {
2080
+ console.log(JSON.stringify({ rows }, null, 2));
2081
+ return;
2082
+ }
2083
+ const nameW = Math.max(8, ...rows.map((r) => r.name.length));
2084
+ const idW = 12;
2085
+ console.log("");
2086
+ console.log(` ${"PROJECT".padEnd(nameW)} ${"ID".padEnd(idW)} ${"ACTIVE".padStart(7)} ${"ARCHIVED".padStart(8)} ${"REINF".padStart(6)} LAST MODIFIED`);
2087
+ console.log(` ${"-".repeat(nameW + idW + 7 + 8 + 6 + 19 + 10)}`);
2088
+ for (const r of rows) {
2089
+ const last = r.lastTouch === "0" ? "—" : r.lastTouch.slice(0, 19);
2090
+ const idShort = r.id === "—" ? "—" : r.id.slice(0, idW);
2091
+ 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}`);
2092
+ }
2093
+ const totalActive = rows.reduce((s, r) => s + r.active, 0);
2094
+ console.log(` ${"-".repeat(nameW + idW + 7 + 8 + 6 + 19 + 10)}`);
2095
+ console.log(` ${"TOTAL".padEnd(nameW)} ${" ".repeat(idW)} ${String(totalActive).padStart(7)}`);
2096
+ console.log("");
2097
+ return;
2098
+ }
2099
+ // Default behavior: scoped stats (current project + user/global, OR --all)
1655
2100
  const projIdentity = await findProjectIdentity(process.cwd());
1656
- const projectId = projIdentity?.identity.projectId || null;
2101
+ const projectId = !opts.all && projIdentity?.identity.projectId || null;
1657
2102
  let dbMemories = centralDb.getActiveMemories();
1658
2103
  if (projectId) {
1659
2104
  dbMemories = dbMemories.filter((m) => m.project_id === projectId || m.scope === "user" || m.scope === "global");
@@ -1756,7 +2201,7 @@ program
1756
2201
  // ─── gnosys graph ───────────────────────────────────────────────────────
1757
2202
  program
1758
2203
  .command("graph")
1759
- .description("Show the full cross-reference graph across all memories")
2204
+ .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.")
1760
2205
  .action(async () => {
1761
2206
  // v5.4.1: Query the central DB directly. Previously this used the
1762
2207
  // filesystem resolver, which returns nothing in v5.x DB-only mode
@@ -1876,12 +2321,13 @@ program
1876
2321
  }
1877
2322
  }
1878
2323
  });
1879
- // ─── gnosys import <file> ────────────────────────────────────────────────
1880
- program
1881
- .command("import <fileOrUrl>")
1882
- .description("Bulk import structured data (CSV, JSON, JSONL) into Gnosys memories")
1883
- .requiredOption("--format <format>", "Data format: csv, json, jsonl")
1884
- .requiredOption("--mapping <json>", 'Field mapping as JSON: \'{"source_field":"gnosys_field"}\'. Valid targets: title, category, content, tags, relevance')
2324
+ // ─── gnosys import (parent + subcommands) ───────────────────────────────
2325
+ const importCmd = program
2326
+ .command("import [fileOrUrl]")
2327
+ .enablePositionalOptions()
2328
+ .description("Import data into Gnosys (bulk CSV/JSON/JSONL — see also: 'gnosys import project <bundle>')")
2329
+ .option("--format <format>", "Data format: csv, json, jsonl (required for bulk import)")
2330
+ .option("--mapping <json>", 'Field mapping as JSON: \'{"source_field":"gnosys_field"}\'. Valid targets: title, category, content, tags, relevance')
1885
2331
  .option("--mode <mode>", "Processing mode: llm or structured", "structured")
1886
2332
  .option("--limit <n>", "Max records to import", parseInt)
1887
2333
  .option("--offset <n>", "Skip first N records", parseInt)
@@ -1892,6 +2338,17 @@ program
1892
2338
  .option("--dry-run", "Preview without writing")
1893
2339
  .option("--store <store>", "Target store: project, personal, global", "project")
1894
2340
  .action(async (fileOrUrl, opts) => {
2341
+ if (!fileOrUrl) {
2342
+ console.error("Usage:");
2343
+ console.error(" gnosys import <file> --format csv|json|jsonl --mapping '{...}' (bulk)");
2344
+ console.error(" gnosys import project <bundle.json.gz> (project bundle)");
2345
+ process.exit(1);
2346
+ }
2347
+ if (!opts.format || !opts.mapping) {
2348
+ console.error("Error: --format and --mapping are required for bulk imports.");
2349
+ console.error(" For project bundles, use 'gnosys import project <bundle>'.");
2350
+ process.exit(1);
2351
+ }
1895
2352
  // Parse mapping JSON
1896
2353
  let mapping;
1897
2354
  try {
@@ -1966,10 +2423,55 @@ program
1966
2423
  process.exit(1);
1967
2424
  }
1968
2425
  });
2426
+ // `gnosys import project <bundle>` — restore a portable .json.gz bundle
2427
+ importCmd
2428
+ .command("project <bundlePath>")
2429
+ .description("Import a project bundle (.json.gz) created by 'gnosys export project'")
2430
+ .option("--strategy <strategy>", "Conflict handling: merge (default), replace, new-id", "merge")
2431
+ .option("--working-directory <dir>", "Override the bundle's working_directory (e.g. when restoring on a different machine)")
2432
+ .option("--json", "Output the result as JSON")
2433
+ .action(async (bundlePath, opts) => {
2434
+ const validStrategies = ["merge", "replace", "new-id"];
2435
+ if (!validStrategies.includes(opts.strategy)) {
2436
+ console.error(`Invalid strategy: ${opts.strategy}. Use one of: ${validStrategies.join(", ")}`);
2437
+ process.exit(1);
2438
+ }
2439
+ const { GnosysDB: DbClass } = await import("./lib/db.js");
2440
+ const { importProject } = await import("./lib/importProject.js");
2441
+ const centralDb = DbClass.openCentral();
2442
+ if (!centralDb.isAvailable()) {
2443
+ console.error("Central DB unavailable.");
2444
+ process.exit(1);
2445
+ }
2446
+ try {
2447
+ const result = importProject(centralDb, {
2448
+ bundlePath: path.resolve(bundlePath),
2449
+ strategy: opts.strategy,
2450
+ workingDirectoryOverride: opts.workingDirectory,
2451
+ });
2452
+ if (opts.json) {
2453
+ console.log(JSON.stringify(result, null, 2));
2454
+ }
2455
+ else {
2456
+ console.log(`Imported project ${result.projectName} (${result.projectId})`);
2457
+ console.log(` Strategy: ${result.strategy}`);
2458
+ console.log(` Memories: ${result.memoriesInserted} inserted, ${result.memoriesSkipped} skipped, ${result.memoriesReplaced} replaced`);
2459
+ console.log(` Relationships: ${result.relationshipsInserted}`);
2460
+ console.log(` Audit entries: ${result.auditEntriesInserted}`);
2461
+ }
2462
+ }
2463
+ catch (err) {
2464
+ console.error(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
2465
+ process.exit(1);
2466
+ }
2467
+ finally {
2468
+ centralDb.close();
2469
+ }
2470
+ });
1969
2471
  // ─── gnosys reindex ──────────────────────────────────────────────────────
1970
2472
  program
1971
2473
  .command("reindex")
1972
- .description("Rebuild all semantic embeddings from every memory file. Downloads the model (~80 MB) on first run.")
2474
+ .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.")
1973
2475
  .action(async () => {
1974
2476
  const resolver = await getResolver();
1975
2477
  const stores = resolver.getStores();
@@ -2506,9 +3008,11 @@ program
2506
3008
  console.log(formatGraphStats(stats));
2507
3009
  });
2508
3010
  // ─── gnosys dashboard ───────────────────────────────────────────────────
3011
+ // v5.7.0: kept as a thin alias of `gnosys status --system`. Will be removed
3012
+ // in a future release; use `gnosys status` instead.
2509
3013
  program
2510
3014
  .command("dashboard")
2511
- .description("Show system dashboard: memory count, health, graph stats, LLM status")
3015
+ .description("(alias) Show system health equivalent to 'gnosys status --system'")
2512
3016
  .option("--json", "Output as JSON instead of pretty table")
2513
3017
  .action(async (opts) => {
2514
3018
  const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");
@@ -2524,334 +3028,103 @@ program
2524
3028
  try {
2525
3029
  const db = GnosysDB.openCentral();
2526
3030
  if (db.isAvailable() && db.isMigrated()) {
2527
- dashDb = db;
2528
- }
2529
- }
2530
- catch {
2531
- // Central DB not available — legacy dashboard only
2532
- }
2533
- const data = await collectDashboardData(resolver, cfg, pkg.version, dashDb);
2534
- if (opts.json) {
2535
- console.log(formatDashboardJSON(data));
2536
- }
2537
- else {
2538
- console.log(formatDashboard(data));
2539
- }
2540
- });
2541
- // ─── gnosys maintain ─────────────────────────────────────────────────────
2542
- program
2543
- .command("maintain")
2544
- .description("Run vault maintenance: detect duplicates, apply confidence decay, consolidate similar memories")
2545
- .option("--dry-run", "Show what would change without modifying anything")
2546
- .option("--auto-apply", "Automatically apply all changes (no prompts)")
2547
- .action(async (opts) => {
2548
- const { GnosysMaintenanceEngine, formatMaintenanceReport } = await import("./lib/maintenance.js");
2549
- const resolver = await getResolver();
2550
- const stores = resolver.getStores();
2551
- if (stores.length === 0) {
2552
- console.error("No Gnosys stores found. Run gnosys init first.");
2553
- process.exit(1);
2554
- }
2555
- const cfg = await loadConfig(stores[0].path);
2556
- const engine = new GnosysMaintenanceEngine(resolver, cfg);
2557
- const report = await engine.maintain({
2558
- dryRun: opts.dryRun,
2559
- autoApply: opts.autoApply,
2560
- onLog: (level, message) => {
2561
- if (level === "warn") {
2562
- console.error(`⚠ ${message}`);
2563
- }
2564
- else if (level === "action") {
2565
- console.log(`→ ${message}`);
2566
- }
2567
- else {
2568
- console.log(message);
2569
- }
2570
- },
2571
- onProgress: (step, current, total) => {
2572
- process.stdout.write(`\r[${current}/${total}] ${step}...`);
2573
- if (current === total)
2574
- process.stdout.write("\n");
2575
- },
2576
- });
2577
- console.log("");
2578
- console.log(formatMaintenanceReport(report));
2579
- });
2580
- // ─── gnosys dearchive ───────────────────────────────────────────────────
2581
- program
2582
- .command("dearchive <query>")
2583
- .description("Force-dearchive memories matching a query from archive.db back to active")
2584
- .option("--limit <n>", "Max memories to dearchive", "5")
2585
- .action(async (query, opts) => {
2586
- const { GnosysArchive } = await import("./lib/archive.js");
2587
- const resolver = await getResolver();
2588
- const stores = resolver.getStores();
2589
- if (stores.length === 0) {
2590
- console.error("No Gnosys stores found. Run gnosys init first.");
2591
- process.exit(1);
2592
- }
2593
- const writeTarget = resolver.getWriteTarget();
2594
- if (!writeTarget) {
2595
- console.error("No writable store found.");
2596
- process.exit(1);
2597
- }
2598
- const archive = new GnosysArchive(writeTarget.path);
2599
- if (!archive.isAvailable()) {
2600
- console.error("Archive not available. Is better-sqlite3 installed?");
2601
- process.exit(1);
2602
- }
2603
- const results = archive.searchArchive(query, parseInt(opts.limit));
2604
- if (results.length === 0) {
2605
- console.log(`No archived memories found matching "${query}".`);
2606
- archive.close();
2607
- return;
2608
- }
2609
- console.log(`Found ${results.length} archived memories matching "${query}":\n`);
2610
- for (const r of results) {
2611
- console.log(` • ${r.title} (${r.id})`);
2612
- }
2613
- console.log("");
2614
- // Dearchive all found
2615
- const ids = results.map((r) => r.id);
2616
- const restored = await archive.dearchiveBatch(ids, writeTarget.store);
2617
- archive.close();
2618
- console.log(`Dearchived ${restored.length} memories back to active:`);
2619
- for (const rp of restored) {
2620
- console.log(` → ${rp}`);
2621
- }
2622
- });
2623
- // NOTE: gnosys migrate is defined below (near the end) with --to-central support
2624
- // ─── gnosys remote (multi-machine sync) ────────────────────────────────
2625
- const remoteCmd = program
2626
- .command("remote")
2627
- .description("Multi-machine sync — share gnosys.db across machines via NAS or shared drive");
2628
- remoteCmd
2629
- .command("status")
2630
- .description("Show remote sync status: pending changes, conflicts, last sync")
2631
- .option("--json", "Output as JSON")
2632
- .action(async (opts) => {
2633
- let centralDb = null;
2634
- try {
2635
- // Sync operations need explicit local DB access (not auto-routed remote).
2636
- centralDb = GnosysDB.openLocal();
2637
- if (!centralDb.isAvailable()) {
2638
- console.error("Central DB not available.");
2639
- process.exit(1);
2640
- }
2641
- const remotePath = centralDb.getMeta("remote_path");
2642
- if (!remotePath) {
2643
- if (opts.json) {
2644
- console.log(JSON.stringify({ configured: false, message: "Remote not configured. Run 'gnosys remote configure'." }, null, 2));
2645
- }
2646
- else {
2647
- console.log("Remote sync: not configured.");
2648
- console.log("Run 'gnosys remote configure' to set up multi-machine sync.");
2649
- }
2650
- return;
2651
- }
2652
- const { RemoteSync, formatStatus } = await import("./lib/remote.js");
2653
- const sync = new RemoteSync(centralDb, remotePath);
2654
- const status = await sync.getStatus();
2655
- sync.closeRemote();
2656
- if (opts.json) {
2657
- console.log(JSON.stringify(status, null, 2));
2658
- }
2659
- else {
2660
- console.log(formatStatus(status));
2661
- if (status.conflicts.length > 0) {
2662
- console.log("\nConflicts:");
2663
- for (const c of status.conflicts) {
2664
- console.log(` ${c.memoryId}: ${c.title}`);
2665
- console.log(` local: ${c.localModified}`);
2666
- console.log(` remote: ${c.remoteModified}`);
2667
- }
2668
- console.log("\nResolve with: gnosys remote resolve <memory-id> --keep <local|remote>");
2669
- }
2670
- }
2671
- }
2672
- catch (err) {
2673
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2674
- process.exit(1);
2675
- }
2676
- finally {
2677
- centralDb?.close();
2678
- }
2679
- });
2680
- remoteCmd
2681
- .command("push")
2682
- .description("Push local changes to remote")
2683
- .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2684
- .action(async (opts) => {
2685
- let centralDb = null;
2686
- try {
2687
- centralDb = GnosysDB.openLocal();
2688
- if (!centralDb.isAvailable()) {
2689
- console.error("Central DB not available.");
2690
- process.exit(1);
2691
- }
2692
- const remotePath = centralDb.getMeta("remote_path");
2693
- if (!remotePath) {
2694
- console.error("Remote not configured.");
2695
- process.exit(1);
2696
- }
2697
- const { RemoteSync } = await import("./lib/remote.js");
2698
- const sync = new RemoteSync(centralDb, remotePath);
2699
- const result = await sync.push({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2700
- sync.closeRemote();
2701
- const projParts = (result.projectsPushed || 0) > 0 ? ` | Projects pushed: ${result.projectsPushed}` : "";
2702
- console.log(`Pushed: ${result.pushed} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}`);
2703
- if (result.errors.length > 0) {
2704
- console.log("\nErrors:");
2705
- for (const e of result.errors)
2706
- console.log(` ${e}`);
2707
- }
2708
- if (result.conflicts.length > 0) {
2709
- console.log("\nConflicts flagged (run 'gnosys remote status' for details):");
2710
- for (const c of result.conflicts)
2711
- console.log(` ${c.memoryId} — ${c.title}`);
2712
- }
2713
- }
2714
- catch (err) {
2715
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2716
- process.exit(1);
2717
- }
2718
- finally {
2719
- centralDb?.close();
2720
- }
2721
- });
2722
- remoteCmd
2723
- .command("pull")
2724
- .description("Pull remote changes to local")
2725
- .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2726
- .action(async (opts) => {
2727
- let centralDb = null;
2728
- try {
2729
- centralDb = GnosysDB.openLocal();
2730
- if (!centralDb.isAvailable()) {
2731
- console.error("Central DB not available.");
2732
- process.exit(1);
2733
- }
2734
- const remotePath = centralDb.getMeta("remote_path");
2735
- if (!remotePath) {
2736
- console.error("Remote not configured.");
2737
- process.exit(1);
2738
- }
2739
- const { RemoteSync } = await import("./lib/remote.js");
2740
- const sync = new RemoteSync(centralDb, remotePath);
2741
- const result = await sync.pull({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2742
- sync.closeRemote();
2743
- const projParts = (result.projectsPulled || 0) > 0 ? ` | Projects pulled: ${result.projectsPulled}` : "";
2744
- console.log(`Pulled: ${result.pulled} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}`);
2745
- if (result.errors.length > 0) {
2746
- console.log("\nErrors:");
2747
- for (const e of result.errors)
2748
- console.log(` ${e}`);
2749
- }
2750
- }
2751
- catch (err) {
2752
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2753
- process.exit(1);
2754
- }
2755
- finally {
2756
- centralDb?.close();
2757
- }
2758
- });
2759
- remoteCmd
2760
- .command("sync")
2761
- .description("Two-way sync: push local changes then pull remote changes")
2762
- .option("--auto", "Run silently for cron/LaunchAgent (skip-and-flag for conflicts)")
2763
- .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2764
- .action(async (opts) => {
2765
- let centralDb = null;
2766
- try {
2767
- centralDb = GnosysDB.openLocal();
2768
- if (!centralDb.isAvailable()) {
2769
- if (!opts.auto)
2770
- console.error("Central DB not available.");
2771
- process.exit(1);
2772
- }
2773
- const remotePath = centralDb.getMeta("remote_path");
2774
- if (!remotePath) {
2775
- if (!opts.auto)
2776
- console.error("Remote not configured.");
2777
- process.exit(opts.auto ? 0 : 1);
2778
- }
2779
- const { RemoteSync } = await import("./lib/remote.js");
2780
- const sync = new RemoteSync(centralDb, remotePath);
2781
- const result = await sync.sync({
2782
- auto: opts.auto,
2783
- strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
2784
- });
2785
- sync.closeRemote();
2786
- if (!opts.auto || result.conflicts.length > 0 || result.errors.length > 0) {
2787
- const pp = result.projectsPushed || 0;
2788
- const pl = result.projectsPulled || 0;
2789
- const projParts = (pp + pl) > 0 ? ` | Projects: ↑${pp}/↓${pl}` : "";
2790
- console.log(`Pushed: ${result.pushed} | Pulled: ${result.pulled} | Conflicts: ${result.conflicts.length}${projParts}`);
2791
- if (result.errors.length > 0) {
2792
- console.log("\nErrors:");
2793
- for (const e of result.errors)
2794
- console.log(` ${e}`);
2795
- }
2796
- if (result.conflicts.length > 0) {
2797
- console.log("\nConflicts need resolution (run 'gnosys remote status' for details).");
2798
- }
3031
+ dashDb = db;
2799
3032
  }
2800
3033
  }
2801
- catch (err) {
2802
- if (!opts.auto)
2803
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2804
- process.exit(1);
3034
+ catch {
3035
+ // Central DB not available — legacy dashboard only
2805
3036
  }
2806
- finally {
2807
- centralDb?.close();
3037
+ const data = await collectDashboardData(resolver, cfg, pkg.version, dashDb);
3038
+ if (opts.json) {
3039
+ console.log(formatDashboardJSON(data));
3040
+ }
3041
+ else {
3042
+ console.log(formatDashboard(data));
2808
3043
  }
2809
3044
  });
2810
- remoteCmd
2811
- .command("resolve <memoryId>")
2812
- .description("Resolve a sync conflict by choosing local, remote, or merged content")
2813
- .option("--keep <choice>", "Choice: local | remote", "local")
2814
- .action(async (memoryId, opts) => {
2815
- let centralDb = null;
2816
- try {
2817
- centralDb = GnosysDB.openLocal();
2818
- if (!centralDb.isAvailable()) {
2819
- console.error("Central DB not available.");
2820
- process.exit(1);
2821
- }
2822
- const remotePath = centralDb.getMeta("remote_path");
2823
- if (!remotePath) {
2824
- console.error("Remote not configured.");
2825
- process.exit(1);
2826
- }
2827
- if (opts.keep !== "local" && opts.keep !== "remote") {
2828
- console.error(`--keep must be 'local' or 'remote' (got: ${opts.keep})`);
2829
- process.exit(1);
2830
- }
2831
- const { RemoteSync } = await import("./lib/remote.js");
2832
- const sync = new RemoteSync(centralDb, remotePath);
2833
- const result = await sync.resolve(memoryId, opts.keep);
2834
- sync.closeRemote();
2835
- if (result.ok) {
2836
- console.log(`Resolved ${memoryId}: kept ${opts.keep} version.`);
2837
- }
2838
- else {
2839
- console.error(`Failed to resolve: ${result.error}`);
2840
- process.exit(1);
2841
- }
3045
+ // ─── gnosys maintain ─────────────────────────────────────────────────────
3046
+ program
3047
+ .command("maintain")
3048
+ .description("Run vault maintenance: detect duplicates, apply confidence decay, consolidate similar memories")
3049
+ .option("--dry-run", "Show what would change without modifying anything")
3050
+ .option("--auto-apply", "Automatically apply all changes (no prompts)")
3051
+ .action(async (opts) => {
3052
+ const { GnosysMaintenanceEngine, formatMaintenanceReport } = await import("./lib/maintenance.js");
3053
+ const resolver = await getResolver();
3054
+ const stores = resolver.getStores();
3055
+ if (stores.length === 0) {
3056
+ console.error("No Gnosys stores found. Run gnosys init first.");
3057
+ process.exit(1);
2842
3058
  }
2843
- catch (err) {
2844
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
3059
+ const cfg = await loadConfig(stores[0].path);
3060
+ const engine = new GnosysMaintenanceEngine(resolver, cfg);
3061
+ const report = await engine.maintain({
3062
+ dryRun: opts.dryRun,
3063
+ autoApply: opts.autoApply,
3064
+ onLog: (level, message) => {
3065
+ if (level === "warn") {
3066
+ console.error(`⚠ ${message}`);
3067
+ }
3068
+ else if (level === "action") {
3069
+ console.log(`→ ${message}`);
3070
+ }
3071
+ else {
3072
+ console.log(message);
3073
+ }
3074
+ },
3075
+ onProgress: (step, current, total) => {
3076
+ process.stdout.write(`\r[${current}/${total}] ${step}...`);
3077
+ if (current === total)
3078
+ process.stdout.write("\n");
3079
+ },
3080
+ });
3081
+ console.log("");
3082
+ console.log(formatMaintenanceReport(report));
3083
+ });
3084
+ // ─── gnosys dearchive ───────────────────────────────────────────────────
3085
+ program
3086
+ .command("dearchive <query>")
3087
+ .description("Force-dearchive memories matching a query from archive.db back to active")
3088
+ .option("--limit <n>", "Max memories to dearchive", "5")
3089
+ .action(async (query, opts) => {
3090
+ const { GnosysArchive } = await import("./lib/archive.js");
3091
+ const resolver = await getResolver();
3092
+ const stores = resolver.getStores();
3093
+ if (stores.length === 0) {
3094
+ console.error("No Gnosys stores found. Run gnosys init first.");
2845
3095
  process.exit(1);
2846
3096
  }
2847
- finally {
2848
- centralDb?.close();
3097
+ const writeTarget = resolver.getWriteTarget();
3098
+ if (!writeTarget) {
3099
+ console.error("No writable store found.");
3100
+ process.exit(1);
3101
+ }
3102
+ const archive = new GnosysArchive(writeTarget.path);
3103
+ if (!archive.isAvailable()) {
3104
+ console.error("Archive not available. Is better-sqlite3 installed?");
3105
+ process.exit(1);
3106
+ }
3107
+ const results = archive.searchArchive(query, parseInt(opts.limit));
3108
+ if (results.length === 0) {
3109
+ console.log(`No archived memories found matching "${query}".`);
3110
+ archive.close();
3111
+ return;
3112
+ }
3113
+ console.log(`Found ${results.length} archived memories matching "${query}":\n`);
3114
+ for (const r of results) {
3115
+ console.log(` • ${r.title} (${r.id})`);
3116
+ }
3117
+ console.log("");
3118
+ // Dearchive all found
3119
+ const ids = results.map((r) => r.id);
3120
+ const restored = await archive.dearchiveBatch(ids, writeTarget.store);
3121
+ archive.close();
3122
+ console.log(`Dearchived ${restored.length} memories back to active:`);
3123
+ for (const rp of restored) {
3124
+ console.log(` → ${rp}`);
2849
3125
  }
2850
3126
  });
2851
- // v5.4.2 removal: `gnosys remote configure` was removed in favor of the
2852
- // canonical `gnosys setup remote` form (which calls the same wizard
2853
- // helpers from lib/remoteWizard.ts). Sync operations like push/pull/sync
2854
- // remain under the `remote` parent.
3127
+ // NOTE: gnosys migrate is defined below (near the end) with --to-central support
2855
3128
  // ─── gnosys upgrade ─────────────────────────────────────────────────────
2856
3129
  program
2857
3130
  .command("upgrade")
@@ -3081,7 +3354,8 @@ program
3081
3354
  program
3082
3355
  .command("doctor")
3083
3356
  .description("Check system health: stores, LLM connectivity, embeddings, archive")
3084
- .action(async () => {
3357
+ .option("--fix", "Offer interactive cleanup of legacy artifacts (e.g. per-store gnosys.db)")
3358
+ .action(async (opts) => {
3085
3359
  const resolver = await getResolver();
3086
3360
  const stores = resolver.getStores();
3087
3361
  console.log("Gnosys Doctor");
@@ -3092,9 +3366,36 @@ program
3092
3366
  const localDbExists = await fs.stat(localDbPath).then(() => true).catch(() => false);
3093
3367
  if (localDbExists) {
3094
3368
  console.log("Local Store (gnosys.db):");
3095
- console.log(" ⚠ Local gnosys.db found — this is a legacy artifact.");
3096
- console.log(" All memories should be in the central DB (~/.gnosys/gnosys.db).");
3097
- console.log(` Safe to remove: rm "${localDbPath}"`);
3369
+ console.log(" ⚠ Local gnosys.db found — this is a legacy artifact (pre-v2.0 file-based store).");
3370
+ console.log(" All memories live in the central DB now (~/.gnosys/gnosys.db).");
3371
+ console.log(` Path: ${localDbPath}`);
3372
+ if (opts.fix) {
3373
+ // Interactive cleanup — verify the local DB is safe to delete
3374
+ // (no rows that aren't already in the central DB) before prompting.
3375
+ const safe = await isLegacyStoreSafeToRemove(localDbPath);
3376
+ if (!safe.ok) {
3377
+ console.log(` ✗ NOT safe to auto-remove: ${safe.reason}`);
3378
+ console.log(` Inspect manually with: sqlite3 ${localDbPath} "SELECT COUNT(*) FROM memories;"`);
3379
+ }
3380
+ else {
3381
+ const readline = await import("readline/promises");
3382
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3383
+ const answer = await rl.question(` Remove "${localDbPath}"? [y/N] `);
3384
+ rl.close();
3385
+ if (answer.trim().toLowerCase() === "y") {
3386
+ await fs.unlink(localDbPath).catch(() => undefined);
3387
+ await fs.unlink(localDbPath + "-wal").catch(() => undefined);
3388
+ await fs.unlink(localDbPath + "-shm").catch(() => undefined);
3389
+ console.log(" ✓ Removed.");
3390
+ }
3391
+ else {
3392
+ console.log(" Skipped.");
3393
+ }
3394
+ }
3395
+ }
3396
+ else {
3397
+ console.log(" Run 'gnosys doctor --fix' to remove safely (after verifying it's empty).");
3398
+ }
3098
3399
  console.log("");
3099
3400
  }
3100
3401
  }
@@ -3206,31 +3507,110 @@ program
3206
3507
  catch {
3207
3508
  console.log(" Index: not initialized (run gnosys reindex to build)");
3208
3509
  }
3209
- // Maintenance health
3510
+ // Maintenance health — v5.7.0: queries the central DB directly
3511
+ // (the prior version used GnosysMaintenanceEngine which only sees the
3512
+ // legacy file-based stores, which are empty post-DB-only).
3210
3513
  console.log("");
3211
3514
  console.log("Maintenance Health:");
3212
3515
  try {
3213
- const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
3214
- const engine = new GnosysMaintenanceEngine(resolver, cfg);
3215
- const health = await engine.getHealthReport();
3216
- console.log(` Active memories: ${health.totalActive}`);
3217
- console.log(` Stale (confidence < 0.3): ${health.staleCount}`);
3218
- console.log(` Average confidence: ${health.avgConfidence.toFixed(3)} (decayed: ${health.avgDecayedConfidence.toFixed(3)})`);
3219
- console.log(` Never reinforced: ${health.neverReinforced}`);
3220
- console.log(` Total reinforcements: ${health.totalReinforcements}`);
3516
+ const db2 = GnosysDB.openCentral();
3517
+ if (db2.isAvailable() && db2.isMigrated()) {
3518
+ const memories = db2.getActiveMemories();
3519
+ const now = Date.now();
3520
+ const DECAY_LAMBDA = 0.005;
3521
+ const STALE_THRESHOLD = 0.3;
3522
+ let sumConfidence = 0;
3523
+ let sumDecayed = 0;
3524
+ let staleCount = 0;
3525
+ let neverReinforced = 0;
3526
+ let totalReinforcements = 0;
3527
+ for (const m of memories) {
3528
+ const baseConfidence = m.confidence ?? 0.8;
3529
+ const lastIso = m.last_reinforced || m.modified || m.created;
3530
+ const lastTs = lastIso ? new Date(lastIso).getTime() : NaN;
3531
+ // Some legacy memories have non-ISO dates that don't parse; treat
3532
+ // them as "today" rather than NaN-corrupting the average.
3533
+ const daysSince = Number.isFinite(lastTs)
3534
+ ? Math.max(0, Math.floor((now - lastTs) / (1000 * 60 * 60 * 24)))
3535
+ : 0;
3536
+ const decayed = baseConfidence * Math.exp(-DECAY_LAMBDA * daysSince);
3537
+ sumConfidence += baseConfidence;
3538
+ sumDecayed += decayed;
3539
+ if (decayed < STALE_THRESHOLD)
3540
+ staleCount++;
3541
+ const rc = m.reinforcement_count ?? 0;
3542
+ if (rc === 0)
3543
+ neverReinforced++;
3544
+ totalReinforcements += rc;
3545
+ }
3546
+ const n = Math.max(1, memories.length);
3547
+ console.log(` Active memories: ${memories.length}`);
3548
+ console.log(` Stale (decayed confidence < ${STALE_THRESHOLD}): ${staleCount}`);
3549
+ console.log(` Average confidence: ${(sumConfidence / n).toFixed(3)} (decayed: ${(sumDecayed / n).toFixed(3)})`);
3550
+ console.log(` Never reinforced: ${neverReinforced}`);
3551
+ console.log(` Total reinforcements: ${totalReinforcements}`);
3552
+ }
3553
+ else {
3554
+ console.log(" — central DB not available");
3555
+ }
3556
+ db2.close();
3221
3557
  }
3222
3558
  catch (err) {
3223
3559
  console.log(` Error: ${err instanceof Error ? err.message : String(err)}`);
3224
3560
  }
3225
3561
  }
3226
3562
  });
3563
+ /**
3564
+ * Check whether a legacy per-store gnosys.db is safe to remove.
3565
+ * Safe = the file is empty OR every memory in it is already represented
3566
+ * in the central DB (matching ID present centrally). This is conservative:
3567
+ * we don't compare hashes or content, just IDs. The legacy DB existed
3568
+ * pre-v2.0; its memories should have all migrated to central DB long ago.
3569
+ */
3570
+ async function isLegacyStoreSafeToRemove(localDbPath) {
3571
+ try {
3572
+ const Database = (await import("better-sqlite3")).default;
3573
+ const localDb = new Database(localDbPath, { readonly: true });
3574
+ let localIds = [];
3575
+ try {
3576
+ const rows = localDb.prepare("SELECT id FROM memories").all();
3577
+ localIds = rows.map((r) => r.id);
3578
+ }
3579
+ catch {
3580
+ // Table doesn't exist — file is effectively empty
3581
+ localDb.close();
3582
+ return { ok: true };
3583
+ }
3584
+ localDb.close();
3585
+ if (localIds.length === 0)
3586
+ return { ok: true };
3587
+ const centralDb = GnosysDB.openCentral();
3588
+ if (!centralDb.isAvailable()) {
3589
+ centralDb.close();
3590
+ return { ok: false, reason: "central DB unavailable — cannot verify migration" };
3591
+ }
3592
+ let missing = 0;
3593
+ for (const id of localIds) {
3594
+ if (!centralDb.getMemory(id))
3595
+ missing++;
3596
+ }
3597
+ centralDb.close();
3598
+ if (missing > 0) {
3599
+ return { ok: false, reason: `${missing} of ${localIds.length} local memories not found in central DB` };
3600
+ }
3601
+ return { ok: true };
3602
+ }
3603
+ catch (err) {
3604
+ return { ok: false, reason: `inspection failed: ${err instanceof Error ? err.message : String(err)}` };
3605
+ }
3606
+ }
3227
3607
  // ─── gnosys check ─────────────────────────────────────────────────────────
3228
3608
  program
3229
3609
  .command("check")
3230
- .description("Test LLM connectivity for all 5 task configurations (structuring, synthesis, vision, transcription, dream)")
3231
- .option("-d, --directory <dir>", "Project directory (default: cwd)")
3610
+ .description("Test LLM connectivity for each configured task (structuring, synthesis, chat, vision, transcription, dream)")
3611
+ .option("-t, --task <name>", "Test only one task (structuring | synthesis | chat | vision | transcription | dream)")
3232
3612
  .action(async (opts) => {
3233
- const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
3613
+ const projectDir = process.cwd();
3234
3614
  const storePath = path.join(projectDir, ".gnosys");
3235
3615
  const globalStorePath = getGnosysHome();
3236
3616
  // Load config: try project-level first, fall back to global ~/.gnosys/
@@ -3275,6 +3655,13 @@ program
3275
3655
  description: "Q&A answers (gnosys ask)",
3276
3656
  resolve: () => resolveTaskModel(cfg, "synthesis"),
3277
3657
  },
3658
+ {
3659
+ name: "chat",
3660
+ description: "interactive chat (gnosys chat)",
3661
+ // Chat reuses the synthesis task's model — surface it under its own name
3662
+ // so users can see exactly what their TUI will use.
3663
+ resolve: () => resolveTaskModel(cfg, "synthesis"),
3664
+ },
3278
3665
  {
3279
3666
  name: "vision",
3280
3667
  description: "images, PDFs",
@@ -3297,7 +3684,15 @@ program
3297
3684
  let passed = 0;
3298
3685
  let failed = 0;
3299
3686
  let skipped = 0;
3300
- for (const task of tasks) {
3687
+ // Filter to a single task if --task was given.
3688
+ const filteredTasks = opts.task
3689
+ ? tasks.filter((t) => t.name === opts.task)
3690
+ : tasks;
3691
+ if (opts.task && filteredTasks.length === 0) {
3692
+ console.error(`Unknown task: ${opts.task}. Pick one of: ${tasks.map((t) => t.name).join(", ")}`);
3693
+ process.exit(1);
3694
+ }
3695
+ for (const task of filteredTasks) {
3301
3696
  const { provider, model } = task.resolve();
3302
3697
  const label = `${task.name.padEnd(16)} ${DIM}${provider} / ${model}${RESET}`;
3303
3698
  const desc = `${DIM}(${task.description})${RESET}`;
@@ -3495,18 +3890,7 @@ dreamCmd
3495
3890
  centralDb?.close();
3496
3891
  }
3497
3892
  });
3498
- // ─── gnosys export ───────────────────────────────────────────────────────
3499
- program
3500
- .command("export")
3501
- .description("Export gnosys.db to Obsidian-compatible vault (one-way)")
3502
- .requiredOption("--to <dir>", "Target directory for export")
3503
- .option("--all", "Export all memories (active + archived)")
3504
- .option("--overwrite", "Overwrite existing files")
3505
- .option("--no-summaries", "Skip category summaries")
3506
- .option("--no-reviews", "Skip review suggestions")
3507
- .option("--no-graph", "Skip relationship graph")
3508
- .option("--json", "Output raw JSON report")
3509
- .action(async (opts) => {
3893
+ async function runVaultExport(opts) {
3510
3894
  const resolver = new GnosysResolver();
3511
3895
  await resolver.resolve();
3512
3896
  const stores = resolver.getStores();
@@ -3545,11 +3929,86 @@ program
3545
3929
  console.log(formatExportReport(report));
3546
3930
  }
3547
3931
  db.close();
3932
+ }
3933
+ const exportCmd = program
3934
+ .command("export")
3935
+ .description("Export memory to a vault (markdown) or a project bundle (.json.gz)")
3936
+ .enablePositionalOptions();
3937
+ // Bare `gnosys export` shows the canonical subcommand forms. Back-compat for
3938
+ // the v5.5.x form `gnosys export --to <dir>` is handled in a pre-parse shim
3939
+ // at the top of the file (rewrites argv to insert "vault" before "--to").
3940
+ exportCmd.action(() => {
3941
+ console.error("Usage: gnosys export vault --to <dir> # Obsidian vault export");
3942
+ console.error(" gnosys export project [id] --to <bundle> # portable .json.gz bundle");
3943
+ process.exit(1);
3944
+ });
3945
+ // `gnosys export vault` — explicit alias for the default behavior
3946
+ exportCmd
3947
+ .command("vault")
3948
+ .description("Export gnosys.db to an Obsidian-compatible vault (one-way)")
3949
+ .requiredOption("--to <dir>", "Target directory for export")
3950
+ .option("--all", "Export all memories (active + archived)")
3951
+ .option("--overwrite", "Overwrite existing files")
3952
+ .option("--no-summaries", "Skip category summaries")
3953
+ .option("--no-reviews", "Skip review suggestions")
3954
+ .option("--no-graph", "Skip relationship graph")
3955
+ .option("--json", "Output raw JSON report")
3956
+ .action(runVaultExport);
3957
+ // `gnosys export project [id]` — bundle a single project for portability
3958
+ exportCmd
3959
+ .command("project [projectId]")
3960
+ .description("Export a single project to a portable .json.gz bundle (round-trips with 'gnosys import project')")
3961
+ .requiredOption("--to <file>", "Output bundle file path (e.g. ./gnosys-public.gnosys.json.gz)")
3962
+ .option("--include-archived", "Include archived and superseded memories (default: active only)")
3963
+ .option("--no-audit", "Skip the audit log")
3964
+ .option("--json", "Output the result as JSON")
3965
+ .action(async (projectIdArg, opts) => {
3966
+ const { GnosysDB: DbClass } = await import("./lib/db.js");
3967
+ const { exportProject } = await import("./lib/exportProject.js");
3968
+ const centralDb = DbClass.openCentral();
3969
+ if (!centralDb.isAvailable()) {
3970
+ console.error("Central DB unavailable.");
3971
+ process.exit(1);
3972
+ }
3973
+ let projectId = projectIdArg;
3974
+ if (!projectId) {
3975
+ // Auto-detect from cwd
3976
+ const proj = centralDb.getProjectByDirectory(process.cwd());
3977
+ if (!proj) {
3978
+ console.error("No project ID given and current directory is not a registered project.");
3979
+ console.error("Usage: gnosys export project <projectId> --to <file>");
3980
+ process.exit(1);
3981
+ }
3982
+ projectId = proj.id;
3983
+ }
3984
+ try {
3985
+ const result = exportProject(centralDb, {
3986
+ projectId,
3987
+ outputPath: path.resolve(opts.to),
3988
+ includeArchived: !!opts.includeArchived,
3989
+ includeAudit: opts.audit !== false,
3990
+ });
3991
+ if (opts.json) {
3992
+ console.log(JSON.stringify(result, null, 2));
3993
+ }
3994
+ else {
3995
+ const ratio = (result.compressedBytes / result.uncompressedBytes * 100).toFixed(1);
3996
+ console.log(`Exported project ${projectId}`);
3997
+ console.log(` Memories: ${result.memoryCount}`);
3998
+ console.log(` Relationships: ${result.relationshipCount}`);
3999
+ console.log(` Audit entries: ${result.auditEntryCount}`);
4000
+ console.log(` Bundle: ${result.outputPath}`);
4001
+ console.log(` Size: ${(result.compressedBytes / 1024).toFixed(1)} KB compressed (${ratio}% of ${(result.uncompressedBytes / 1024).toFixed(1)} KB)`);
4002
+ }
4003
+ }
4004
+ finally {
4005
+ centralDb.close();
4006
+ }
3548
4007
  });
3549
4008
  // ─── gnosys serve ────────────────────────────────────────────────────────
3550
4009
  program
3551
4010
  .command("serve")
3552
- .description("Start the MCP server (stdio mode)")
4011
+ .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.")
3553
4012
  .option("--with-maintenance", "Run maintenance every 6 hours in background")
3554
4013
  .action(async (opts) => {
3555
4014
  if (opts.withMaintenance) {
@@ -3704,31 +4163,33 @@ program
3704
4163
  // ─── gnosys audit ────────────────────────────────────────────────────────
3705
4164
  program
3706
4165
  .command("audit")
3707
- .description("View the structured audit trail of memory operations")
4166
+ .description("View the structured audit trail of memory operations from the central DB")
3708
4167
  .option("--days <n>", "Show entries from the last N days", "7")
3709
- .option("--operation <op>", "Filter by operation type (read, write, recall, etc.)")
4168
+ .option("--operation <op>", "Filter by operation type (read, write, recall, dream_*, etc.)")
3710
4169
  .option("--limit <n>", "Max entries to show")
3711
4170
  .option("--json", "Output raw JSON instead of formatted timeline")
3712
4171
  .action(async (opts) => {
3713
- const resolver = new GnosysResolver();
3714
- await resolver.resolve();
3715
- const stores = resolver.getStores();
3716
- if (stores.length === 0) {
3717
- console.error("No Gnosys stores found. Run 'gnosys init' first.");
4172
+ const { readAuditFromDb, formatAuditTimeline } = await import("./lib/audit.js");
4173
+ const centralDb = GnosysDB.openCentral();
4174
+ if (!centralDb.isAvailable()) {
4175
+ console.error("Central DB unavailable.");
3718
4176
  process.exit(1);
3719
4177
  }
3720
- const { readAuditLog, formatAuditTimeline } = await import("./lib/audit.js");
3721
- const storePath = stores[0].path;
3722
- const entries = readAuditLog(storePath, {
3723
- days: parseInt(opts.days, 10),
3724
- operation: opts.operation,
3725
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
3726
- });
3727
- if (opts.json) {
3728
- console.log(JSON.stringify(entries, null, 2));
4178
+ try {
4179
+ const entries = readAuditFromDb(centralDb, {
4180
+ days: parseInt(opts.days, 10),
4181
+ operation: opts.operation,
4182
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
4183
+ });
4184
+ if (opts.json) {
4185
+ console.log(JSON.stringify(entries, null, 2));
4186
+ }
4187
+ else {
4188
+ console.log(formatAuditTimeline(entries));
4189
+ }
3729
4190
  }
3730
- else {
3731
- console.log(formatAuditTimeline(entries));
4191
+ finally {
4192
+ centralDb.close();
3732
4193
  }
3733
4194
  });
3734
4195
  // ─── gnosys backup ──────────────────────────────────────────────────────
@@ -3930,8 +4391,10 @@ program
3930
4391
  .command("projects")
3931
4392
  .description("List registered projects from the central DB")
3932
4393
  .option("--json", "Output as JSON")
3933
- .option("--all", "Include dead projects (deleted directories, /tmp/ paths)")
3934
- .option("--prune", "Delete registry entries whose directory no longer exists or is a tmp path")
4394
+ .option("--all", "Include dead projects (deleted directories)")
4395
+ .option("--prune", "Delete registry entries whose directory no longer exists (interactive by default)")
4396
+ .option("--dry-run", "With --prune: list what would be deleted, don't actually delete")
4397
+ .option("--yes", "With --prune: skip the confirmation prompt (scripting/automation)")
3935
4398
  .action(async (opts) => {
3936
4399
  let centralDb = null;
3937
4400
  try {
@@ -3942,8 +4405,37 @@ program
3942
4405
  }
3943
4406
  const allProjects = centralDb.getAllProjects();
3944
4407
  if (opts.prune) {
3945
- // Find and delete dead projects
4408
+ // Find dead projects first — never just delete without showing
4409
+ // them. v5.7.0 adds confirmation by default; --yes skips for
4410
+ // scripted use; --dry-run shows the list without deleting.
3946
4411
  const deadProjects = allProjects.filter((p) => isDeadProjectDir(p.working_directory));
4412
+ if (deadProjects.length === 0) {
4413
+ console.log("No dead projects to prune.");
4414
+ return;
4415
+ }
4416
+ const DIM = "\x1b[2m";
4417
+ const RESET = "\x1b[0m";
4418
+ // Always show what would be removed first.
4419
+ console.log(`Found ${deadProjects.length} dead project(s):\n`);
4420
+ for (const p of deadProjects) {
4421
+ const memCount = centralDb.getMemoriesByProject(p.id, true).length;
4422
+ console.log(` ${p.name} ${DIM}${p.working_directory}${RESET} (${memCount} memorie(s))`);
4423
+ }
4424
+ console.log();
4425
+ if (opts.dryRun) {
4426
+ console.log("[dry-run] No changes made. Re-run without --dry-run to delete.");
4427
+ return;
4428
+ }
4429
+ if (!opts.yes) {
4430
+ const readline = await import("readline/promises");
4431
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4432
+ const answer = (await rl.question(`Delete these ${deadProjects.length} project registry entries? [y/N] `)).trim().toLowerCase();
4433
+ rl.close();
4434
+ if (answer !== "y" && answer !== "yes") {
4435
+ console.log("Cancelled.");
4436
+ return;
4437
+ }
4438
+ }
3947
4439
  for (const p of deadProjects) {
3948
4440
  centralDb.deleteProject(p.id);
3949
4441
  }
@@ -3952,19 +4444,7 @@ program
3952
4444
  remaining: allProjects.length - deadProjects.length,
3953
4445
  deletedProjects: deadProjects.map((p) => ({ id: p.id, name: p.name, directory: p.working_directory })),
3954
4446
  }, () => {
3955
- if (deadProjects.length === 0) {
3956
- console.log("No dead projects to prune.");
3957
- }
3958
- else {
3959
- const DIM = "\x1b[2m";
3960
- const RESET = "\x1b[0m";
3961
- console.log(`Pruned ${deadProjects.length} dead project(s):\n`);
3962
- for (const p of deadProjects) {
3963
- console.log(` ${p.name} ${DIM}${p.working_directory}${RESET}`);
3964
- }
3965
- console.log();
3966
- console.log(`${allProjects.length - deadProjects.length} project(s) remain.`);
3967
- }
4447
+ console.log(`✓ Pruned ${deadProjects.length} project(s). ${allProjects.length - deadProjects.length} remain.`);
3968
4448
  });
3969
4449
  return;
3970
4450
  }
@@ -4026,7 +4506,7 @@ program
4026
4506
  // ─── gnosys pref ─────────────────────────────────────────────────────────
4027
4507
  const prefCmd = program
4028
4508
  .command("pref")
4029
- .description("Manage user preferences (stored in central DB, scope='user')");
4509
+ .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`.");
4030
4510
  prefCmd
4031
4511
  .command("set <key> <value>")
4032
4512
  .description("Set a user preference. Key should be kebab-case (e.g. 'commit-convention').")
@@ -4285,13 +4765,13 @@ program
4285
4765
  });
4286
4766
  // ─── gnosys briefing ─────────────────────────────────────────────────────
4287
4767
  program
4288
- .command("briefing")
4768
+ .command("briefing [projectNameOrId]")
4289
4769
  .description("Generate project briefing — memory state summary, categories, recent activity, top tags")
4290
4770
  .option("-p, --project <id>", "Project ID (auto-detects if omitted)")
4291
4771
  .option("-a, --all", "Generate briefings for all projects")
4292
4772
  .option("-d, --directory <dir>", "Project directory for auto-detection")
4293
4773
  .option("--json", "Output as JSON")
4294
- .action(async (opts) => {
4774
+ .action(async (projectNameOrId, opts) => {
4295
4775
  let centralDb = null;
4296
4776
  try {
4297
4777
  centralDb = GnosysDB.openCentral();
@@ -4317,7 +4797,26 @@ program
4317
4797
  }
4318
4798
  return;
4319
4799
  }
4320
- let pid = opts.project || null;
4800
+ // v5.7.0: accept project name as positional argument in addition to --project <id>.
4801
+ // Resolution order: positional name → --project flag → cwd auto-detect.
4802
+ let pid = opts.project ?? null;
4803
+ if (!pid && projectNameOrId) {
4804
+ // Try as exact ID first, then by name lookup.
4805
+ const byId = centralDb.getProject(projectNameOrId);
4806
+ if (byId) {
4807
+ pid = byId.id;
4808
+ }
4809
+ else {
4810
+ const all = centralDb.getAllProjects();
4811
+ const byName = all.find((p) => p.name === projectNameOrId);
4812
+ if (byName)
4813
+ pid = byName.id;
4814
+ }
4815
+ if (!pid) {
4816
+ console.error(`Project not found: "${projectNameOrId}". Run 'gnosys projects' to list registered projects.`);
4817
+ process.exit(1);
4818
+ }
4819
+ }
4321
4820
  if (!pid)
4322
4821
  pid = await detectCurrentProject(centralDb, opts.directory || undefined);
4323
4822
  if (!pid) {
@@ -4425,13 +4924,35 @@ program
4425
4924
  // ─── gnosys status ──────────────────────────────────────────────────────
4426
4925
  program
4427
4926
  .command("status")
4428
- .description("Show project status. From a project dir: shows that project. With --global: shows all projects. With --web: opens the HTML dashboard.")
4927
+ .description("Show project status (--global: all projects, --web: HTML dashboard, --system: memory/LLM health)")
4429
4928
  .option("-d, --directory <dir>", "Project directory (auto-detects if omitted)")
4430
4929
  .option("-p, --project <id>", "Project ID")
4431
4930
  .option("-g, --global", "Show all projects")
4432
4931
  .option("-w, --web", "Open the HTML dashboard in the browser")
4932
+ .option("-s, --system", "Show system health (memory count, LLM connectivity, embeddings, archive)")
4433
4933
  .option("--json", "Output as JSON")
4434
4934
  .action(async (opts) => {
4935
+ // --system delegates to the dashboard formatter (formerly `gnosys dashboard`).
4936
+ if (opts.system) {
4937
+ const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");
4938
+ const resolver = await getResolver();
4939
+ const stores = resolver.getStores();
4940
+ if (stores.length === 0) {
4941
+ console.error("No Gnosys stores found. Run gnosys init first.");
4942
+ process.exit(1);
4943
+ }
4944
+ const cfg = await loadConfig(stores[0].path);
4945
+ let dashDb;
4946
+ try {
4947
+ const db = GnosysDB.openCentral();
4948
+ if (db.isAvailable() && db.isMigrated())
4949
+ dashDb = db;
4950
+ }
4951
+ catch { /* non-fatal */ }
4952
+ const data = await collectDashboardData(resolver, cfg, pkg.version, dashDb);
4953
+ console.log(opts.json ? formatDashboardJSON(data) : formatDashboard(data));
4954
+ return;
4955
+ }
4435
4956
  let centralDb = null;
4436
4957
  try {
4437
4958
  centralDb = GnosysDB.openCentral();
@@ -4654,7 +5175,7 @@ program
4654
5175
  // ─── gnosys sandbox start|stop|status ─────────────────────────────────────
4655
5176
  const sandboxCmd = program
4656
5177
  .command("sandbox")
4657
- .description("Manage the Gnosys sandbox background process");
5178
+ .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.");
4658
5179
  sandboxCmd
4659
5180
  .command("start")
4660
5181
  .description("Start the Gnosys sandbox background process")
@@ -4737,7 +5258,7 @@ sandboxCmd
4737
5258
  // ─── gnosys helper generate ───────────────────────────────────────────────
4738
5259
  const helperCmd = program
4739
5260
  .command("helper")
4740
- .description("Manage the Gnosys helper library for agent integration");
5261
+ .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`.");
4741
5262
  helperCmd
4742
5263
  .command("generate")
4743
5264
  .description("Generate a gnosys-helper.ts file in the current directory (or specified directory)")