gnosys 5.6.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 (53) hide show
  1. package/dist/cli.js +736 -401
  2. package/dist/cli.js.map +1 -1
  3. package/dist/lib/audit.d.ts +13 -0
  4. package/dist/lib/audit.d.ts.map +1 -1
  5. package/dist/lib/audit.js +42 -0
  6. package/dist/lib/audit.js.map +1 -1
  7. package/dist/lib/chat/llmTurn.d.ts +20 -2
  8. package/dist/lib/chat/llmTurn.d.ts.map +1 -1
  9. package/dist/lib/chat/llmTurn.js +58 -16
  10. package/dist/lib/chat/llmTurn.js.map +1 -1
  11. package/dist/lib/chat/render.d.ts.map +1 -1
  12. package/dist/lib/chat/render.js +18 -0
  13. package/dist/lib/chat/render.js.map +1 -1
  14. package/dist/lib/chat/toolFence.d.ts +54 -0
  15. package/dist/lib/chat/toolFence.d.ts.map +1 -0
  16. package/dist/lib/chat/toolFence.js +90 -0
  17. package/dist/lib/chat/toolFence.js.map +1 -0
  18. package/dist/lib/chat/tools.d.ts +48 -0
  19. package/dist/lib/chat/tools.d.ts.map +1 -0
  20. package/dist/lib/chat/tools.js +338 -0
  21. package/dist/lib/chat/tools.js.map +1 -0
  22. package/dist/lib/db.d.ts +38 -0
  23. package/dist/lib/db.d.ts.map +1 -1
  24. package/dist/lib/db.js +118 -26
  25. package/dist/lib/db.js.map +1 -1
  26. package/dist/lib/remote.d.ts +23 -0
  27. package/dist/lib/remote.d.ts.map +1 -1
  28. package/dist/lib/remote.js +88 -0
  29. package/dist/lib/remote.js.map +1 -1
  30. package/dist/lib/remoteWizard.d.ts.map +1 -1
  31. package/dist/lib/remoteWizard.js +13 -17
  32. package/dist/lib/remoteWizard.js.map +1 -1
  33. package/dist/lib/setup/sections/ides.d.ts +20 -0
  34. package/dist/lib/setup/sections/ides.d.ts.map +1 -0
  35. package/dist/lib/setup/sections/ides.js +124 -0
  36. package/dist/lib/setup/sections/ides.js.map +1 -0
  37. package/dist/lib/setup/sections/preferences.d.ts +30 -0
  38. package/dist/lib/setup/sections/preferences.d.ts.map +1 -0
  39. package/dist/lib/setup/sections/preferences.js +128 -0
  40. package/dist/lib/setup/sections/preferences.js.map +1 -0
  41. package/dist/lib/setup/sections/routing.d.ts +21 -0
  42. package/dist/lib/setup/sections/routing.d.ts.map +1 -0
  43. package/dist/lib/setup/sections/routing.js +160 -0
  44. package/dist/lib/setup/sections/routing.js.map +1 -0
  45. package/dist/lib/setup/summary.d.ts +42 -0
  46. package/dist/lib/setup/summary.d.ts.map +1 -0
  47. package/dist/lib/setup/summary.js +206 -0
  48. package/dist/lib/setup/summary.js.map +1 -0
  49. package/dist/lib/timeline.d.ts +7 -0
  50. package/dist/lib/timeline.d.ts.map +1 -1
  51. package/dist/lib/timeline.js +19 -5
  52. package/dist/lib/timeline.js.map +1 -1
  53. 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 {
@@ -543,13 +560,26 @@ program
543
560
  const setupCmd = program
544
561
  .command("setup")
545
562
  .description("Configure Gnosys — LLM provider, models, remote sync, and IDE integration");
546
- // 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.
547
566
  setupCmd
548
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")
549
569
  .action(async (opts) => {
550
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
+ }
551
581
  await runSetup({
552
- directory: process.cwd(),
582
+ directory: projectDir,
553
583
  nonInteractive: opts.nonInteractive,
554
584
  });
555
585
  });
@@ -569,14 +599,17 @@ setupCmd
569
599
  validate: opts.validate,
570
600
  });
571
601
  });
572
- // `gnosys setup remote` — configure remote sync (alias for `gnosys remote configure`)
573
- 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
574
606
  .command("remote")
575
- .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
576
610
  .option("--path <path>", "Set remote path directly (non-interactive)")
577
611
  .action(async (opts) => {
578
612
  const { GnosysDB } = await import("./lib/db.js");
579
- // Sync configuration needs explicit local DB access (not auto-routed remote).
580
613
  const db = GnosysDB.openLocal();
581
614
  if (!db.isAvailable()) {
582
615
  console.error("Central DB not available.");
@@ -597,6 +630,233 @@ setupCmd
597
630
  db.close();
598
631
  }
599
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
+ });
600
860
  // `gnosys setup dream` — configure dream mode (designation, provider, schedule)
601
861
  setupCmd
602
862
  .command("dream")
@@ -605,6 +865,51 @@ setupCmd
605
865
  const { runDreamSetup } = await import("./lib/setup.js");
606
866
  await runDreamSetup({ directory: process.cwd() });
607
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
+ });
608
913
  // v5.4.2 removal: `gnosys models` (top-level shortcut) was removed in favor
609
914
  // of the canonical `gnosys setup models` form. The implementation function
610
915
  // runModelsCommand() in setup.ts is no longer wired but kept for now in case
@@ -1681,34 +1986,51 @@ program
1681
1986
  .command("timeline")
1682
1987
  .description("Show when memories were created and modified over time")
1683
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")
1684
1991
  .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;
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);
1690
1997
  }
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}`);
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
+ }
1703
2020
  }
1704
2021
  }
1705
2022
  }
2023
+ finally {
2024
+ centralDb.close();
2025
+ }
1706
2026
  });
1707
2027
  // ─── gnosys stats ───────────────────────────────────────────────────────
1708
2028
  program
1709
2029
  .command("stats")
1710
- .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.")
1711
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)")
1712
2034
  .action(async (opts) => {
1713
2035
  let centralDb = null;
1714
2036
  try {
@@ -1717,8 +2039,66 @@ program
1717
2039
  console.error("Central DB not available. Run 'gnosys init' first.");
1718
2040
  process.exit(1);
1719
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)
1720
2100
  const projIdentity = await findProjectIdentity(process.cwd());
1721
- const projectId = projIdentity?.identity.projectId || null;
2101
+ const projectId = !opts.all && projIdentity?.identity.projectId || null;
1722
2102
  let dbMemories = centralDb.getActiveMemories();
1723
2103
  if (projectId) {
1724
2104
  dbMemories = dbMemories.filter((m) => m.project_id === projectId || m.scope === "user" || m.scope === "global");
@@ -1821,7 +2201,7 @@ program
1821
2201
  // ─── gnosys graph ───────────────────────────────────────────────────────
1822
2202
  program
1823
2203
  .command("graph")
1824
- .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.")
1825
2205
  .action(async () => {
1826
2206
  // v5.4.1: Query the central DB directly. Previously this used the
1827
2207
  // filesystem resolver, which returns nothing in v5.x DB-only mode
@@ -2091,7 +2471,7 @@ importCmd
2091
2471
  // ─── gnosys reindex ──────────────────────────────────────────────────────
2092
2472
  program
2093
2473
  .command("reindex")
2094
- .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.")
2095
2475
  .action(async () => {
2096
2476
  const resolver = await getResolver();
2097
2477
  const stores = resolver.getStores();
@@ -2628,9 +3008,11 @@ program
2628
3008
  console.log(formatGraphStats(stats));
2629
3009
  });
2630
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.
2631
3013
  program
2632
3014
  .command("dashboard")
2633
- .description("Show system dashboard: memory count, health, graph stats, LLM status")
3015
+ .description("(alias) Show system health equivalent to 'gnosys status --system'")
2634
3016
  .option("--json", "Output as JSON instead of pretty table")
2635
3017
  .action(async (opts) => {
2636
3018
  const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");
@@ -2645,335 +3027,104 @@ program
2645
3027
  let dashDb;
2646
3028
  try {
2647
3029
  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
- });
2663
- // ─── gnosys maintain ─────────────────────────────────────────────────────
2664
- program
2665
- .command("maintain")
2666
- .description("Run vault maintenance: detect duplicates, apply confidence decay, consolidate similar memories")
2667
- .option("--dry-run", "Show what would change without modifying anything")
2668
- .option("--auto-apply", "Automatically apply all changes (no prompts)")
2669
- .action(async (opts) => {
2670
- const { GnosysMaintenanceEngine, formatMaintenanceReport } = await import("./lib/maintenance.js");
2671
- const resolver = await getResolver();
2672
- const stores = resolver.getStores();
2673
- if (stores.length === 0) {
2674
- console.error("No Gnosys stores found. Run gnosys init first.");
2675
- process.exit(1);
2676
- }
2677
- const cfg = await loadConfig(stores[0].path);
2678
- const engine = new GnosysMaintenanceEngine(resolver, cfg);
2679
- const report = await engine.maintain({
2680
- dryRun: opts.dryRun,
2681
- autoApply: opts.autoApply,
2682
- onLog: (level, message) => {
2683
- if (level === "warn") {
2684
- console.error(`⚠ ${message}`);
2685
- }
2686
- else if (level === "action") {
2687
- console.log(`→ ${message}`);
2688
- }
2689
- 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}`);
3030
+ if (db.isAvailable() && db.isMigrated()) {
3031
+ dashDb = db;
2871
3032
  }
2872
3033
  }
2873
- catch (err) {
2874
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2875
- process.exit(1);
3034
+ catch {
3035
+ // Central DB not available legacy dashboard only
2876
3036
  }
2877
- finally {
2878
- 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));
2879
3043
  }
2880
3044
  });
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")
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)")
2886
3051
  .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}`);
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);
3058
+ }
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}`);
2917
3067
  }
2918
- if (result.conflicts.length > 0) {
2919
- console.log("\nConflicts need resolution (run 'gnosys remote status' for details).");
3068
+ else if (level === "action") {
3069
+ console.log(`→ ${message}`);
2920
3070
  }
2921
- }
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.");
3095
+ process.exit(1);
2922
3096
  }
2923
- catch (err) {
2924
- if (!opts.auto)
2925
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
3097
+ const writeTarget = resolver.getWriteTarget();
3098
+ if (!writeTarget) {
3099
+ console.error("No writable store found.");
2926
3100
  process.exit(1);
2927
3101
  }
2928
- finally {
2929
- centralDb?.close();
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);
2930
3106
  }
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
- }
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;
2964
3112
  }
2965
- catch (err) {
2966
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2967
- process.exit(1);
3113
+ console.log(`Found ${results.length} archived memories matching "${query}":\n`);
3114
+ for (const r of results) {
3115
+ console.log(` • ${r.title} (${r.id})`);
2968
3116
  }
2969
- finally {
2970
- centralDb?.close();
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}`);
2971
3125
  }
2972
3126
  });
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.
3127
+ // NOTE: gnosys migrate is defined below (near the end) with --to-central support
2977
3128
  // ─── gnosys upgrade ─────────────────────────────────────────────────────
2978
3129
  program
2979
3130
  .command("upgrade")
@@ -3203,7 +3354,8 @@ program
3203
3354
  program
3204
3355
  .command("doctor")
3205
3356
  .description("Check system health: stores, LLM connectivity, embeddings, archive")
3206
- .action(async () => {
3357
+ .option("--fix", "Offer interactive cleanup of legacy artifacts (e.g. per-store gnosys.db)")
3358
+ .action(async (opts) => {
3207
3359
  const resolver = await getResolver();
3208
3360
  const stores = resolver.getStores();
3209
3361
  console.log("Gnosys Doctor");
@@ -3214,9 +3366,36 @@ program
3214
3366
  const localDbExists = await fs.stat(localDbPath).then(() => true).catch(() => false);
3215
3367
  if (localDbExists) {
3216
3368
  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}"`);
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
+ }
3220
3399
  console.log("");
3221
3400
  }
3222
3401
  }
@@ -3328,31 +3507,110 @@ program
3328
3507
  catch {
3329
3508
  console.log(" Index: not initialized (run gnosys reindex to build)");
3330
3509
  }
3331
- // 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).
3332
3513
  console.log("");
3333
3514
  console.log("Maintenance Health:");
3334
3515
  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}`);
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();
3343
3557
  }
3344
3558
  catch (err) {
3345
3559
  console.log(` Error: ${err instanceof Error ? err.message : String(err)}`);
3346
3560
  }
3347
3561
  }
3348
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
+ }
3349
3607
  // ─── gnosys check ─────────────────────────────────────────────────────────
3350
3608
  program
3351
3609
  .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)")
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)")
3354
3612
  .action(async (opts) => {
3355
- const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
3613
+ const projectDir = process.cwd();
3356
3614
  const storePath = path.join(projectDir, ".gnosys");
3357
3615
  const globalStorePath = getGnosysHome();
3358
3616
  // Load config: try project-level first, fall back to global ~/.gnosys/
@@ -3397,6 +3655,13 @@ program
3397
3655
  description: "Q&A answers (gnosys ask)",
3398
3656
  resolve: () => resolveTaskModel(cfg, "synthesis"),
3399
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
+ },
3400
3665
  {
3401
3666
  name: "vision",
3402
3667
  description: "images, PDFs",
@@ -3419,7 +3684,15 @@ program
3419
3684
  let passed = 0;
3420
3685
  let failed = 0;
3421
3686
  let skipped = 0;
3422
- 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) {
3423
3696
  const { provider, model } = task.resolve();
3424
3697
  const label = `${task.name.padEnd(16)} ${DIM}${provider} / ${model}${RESET}`;
3425
3698
  const desc = `${DIM}(${task.description})${RESET}`;
@@ -3735,7 +4008,7 @@ exportCmd
3735
4008
  // ─── gnosys serve ────────────────────────────────────────────────────────
3736
4009
  program
3737
4010
  .command("serve")
3738
- .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.")
3739
4012
  .option("--with-maintenance", "Run maintenance every 6 hours in background")
3740
4013
  .action(async (opts) => {
3741
4014
  if (opts.withMaintenance) {
@@ -3890,31 +4163,33 @@ program
3890
4163
  // ─── gnosys audit ────────────────────────────────────────────────────────
3891
4164
  program
3892
4165
  .command("audit")
3893
- .description("View the structured audit trail of memory operations")
4166
+ .description("View the structured audit trail of memory operations from the central DB")
3894
4167
  .option("--days <n>", "Show entries from the last N days", "7")
3895
- .option("--operation <op>", "Filter by operation type (read, write, recall, etc.)")
4168
+ .option("--operation <op>", "Filter by operation type (read, write, recall, dream_*, etc.)")
3896
4169
  .option("--limit <n>", "Max entries to show")
3897
4170
  .option("--json", "Output raw JSON instead of formatted timeline")
3898
4171
  .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.");
4172
+ const { readAuditFromDb, formatAuditTimeline } = await import("./lib/audit.js");
4173
+ const centralDb = GnosysDB.openCentral();
4174
+ if (!centralDb.isAvailable()) {
4175
+ console.error("Central DB unavailable.");
3904
4176
  process.exit(1);
3905
4177
  }
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));
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
+ }
3915
4190
  }
3916
- else {
3917
- console.log(formatAuditTimeline(entries));
4191
+ finally {
4192
+ centralDb.close();
3918
4193
  }
3919
4194
  });
3920
4195
  // ─── gnosys backup ──────────────────────────────────────────────────────
@@ -4116,8 +4391,10 @@ program
4116
4391
  .command("projects")
4117
4392
  .description("List registered projects from the central DB")
4118
4393
  .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")
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)")
4121
4398
  .action(async (opts) => {
4122
4399
  let centralDb = null;
4123
4400
  try {
@@ -4128,8 +4405,37 @@ program
4128
4405
  }
4129
4406
  const allProjects = centralDb.getAllProjects();
4130
4407
  if (opts.prune) {
4131
- // 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.
4132
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
+ }
4133
4439
  for (const p of deadProjects) {
4134
4440
  centralDb.deleteProject(p.id);
4135
4441
  }
@@ -4138,19 +4444,7 @@ program
4138
4444
  remaining: allProjects.length - deadProjects.length,
4139
4445
  deletedProjects: deadProjects.map((p) => ({ id: p.id, name: p.name, directory: p.working_directory })),
4140
4446
  }, () => {
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
- }
4447
+ console.log(`✓ Pruned ${deadProjects.length} project(s). ${allProjects.length - deadProjects.length} remain.`);
4154
4448
  });
4155
4449
  return;
4156
4450
  }
@@ -4212,7 +4506,7 @@ program
4212
4506
  // ─── gnosys pref ─────────────────────────────────────────────────────────
4213
4507
  const prefCmd = program
4214
4508
  .command("pref")
4215
- .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`.");
4216
4510
  prefCmd
4217
4511
  .command("set <key> <value>")
4218
4512
  .description("Set a user preference. Key should be kebab-case (e.g. 'commit-convention').")
@@ -4471,13 +4765,13 @@ program
4471
4765
  });
4472
4766
  // ─── gnosys briefing ─────────────────────────────────────────────────────
4473
4767
  program
4474
- .command("briefing")
4768
+ .command("briefing [projectNameOrId]")
4475
4769
  .description("Generate project briefing — memory state summary, categories, recent activity, top tags")
4476
4770
  .option("-p, --project <id>", "Project ID (auto-detects if omitted)")
4477
4771
  .option("-a, --all", "Generate briefings for all projects")
4478
4772
  .option("-d, --directory <dir>", "Project directory for auto-detection")
4479
4773
  .option("--json", "Output as JSON")
4480
- .action(async (opts) => {
4774
+ .action(async (projectNameOrId, opts) => {
4481
4775
  let centralDb = null;
4482
4776
  try {
4483
4777
  centralDb = GnosysDB.openCentral();
@@ -4503,7 +4797,26 @@ program
4503
4797
  }
4504
4798
  return;
4505
4799
  }
4506
- 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
+ }
4507
4820
  if (!pid)
4508
4821
  pid = await detectCurrentProject(centralDb, opts.directory || undefined);
4509
4822
  if (!pid) {
@@ -4611,13 +4924,35 @@ program
4611
4924
  // ─── gnosys status ──────────────────────────────────────────────────────
4612
4925
  program
4613
4926
  .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.")
4927
+ .description("Show project status (--global: all projects, --web: HTML dashboard, --system: memory/LLM health)")
4615
4928
  .option("-d, --directory <dir>", "Project directory (auto-detects if omitted)")
4616
4929
  .option("-p, --project <id>", "Project ID")
4617
4930
  .option("-g, --global", "Show all projects")
4618
4931
  .option("-w, --web", "Open the HTML dashboard in the browser")
4932
+ .option("-s, --system", "Show system health (memory count, LLM connectivity, embeddings, archive)")
4619
4933
  .option("--json", "Output as JSON")
4620
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
+ }
4621
4956
  let centralDb = null;
4622
4957
  try {
4623
4958
  centralDb = GnosysDB.openCentral();
@@ -4840,7 +5175,7 @@ program
4840
5175
  // ─── gnosys sandbox start|stop|status ─────────────────────────────────────
4841
5176
  const sandboxCmd = program
4842
5177
  .command("sandbox")
4843
- .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.");
4844
5179
  sandboxCmd
4845
5180
  .command("start")
4846
5181
  .description("Start the Gnosys sandbox background process")
@@ -4923,7 +5258,7 @@ sandboxCmd
4923
5258
  // ─── gnosys helper generate ───────────────────────────────────────────────
4924
5259
  const helperCmd = program
4925
5260
  .command("helper")
4926
- .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`.");
4927
5262
  helperCmd
4928
5263
  .command("generate")
4929
5264
  .description("Generate a gnosys-helper.ts file in the current directory (or specified directory)")