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.
- package/dist/cli.js +736 -401
- package/dist/cli.js.map +1 -1
- package/dist/lib/audit.d.ts +13 -0
- package/dist/lib/audit.d.ts.map +1 -1
- package/dist/lib/audit.js +42 -0
- package/dist/lib/audit.js.map +1 -1
- package/dist/lib/chat/llmTurn.d.ts +20 -2
- package/dist/lib/chat/llmTurn.d.ts.map +1 -1
- package/dist/lib/chat/llmTurn.js +58 -16
- package/dist/lib/chat/llmTurn.js.map +1 -1
- package/dist/lib/chat/render.d.ts.map +1 -1
- package/dist/lib/chat/render.js +18 -0
- package/dist/lib/chat/render.js.map +1 -1
- package/dist/lib/chat/toolFence.d.ts +54 -0
- package/dist/lib/chat/toolFence.d.ts.map +1 -0
- package/dist/lib/chat/toolFence.js +90 -0
- package/dist/lib/chat/toolFence.js.map +1 -0
- package/dist/lib/chat/tools.d.ts +48 -0
- package/dist/lib/chat/tools.d.ts.map +1 -0
- package/dist/lib/chat/tools.js +338 -0
- package/dist/lib/chat/tools.js.map +1 -0
- package/dist/lib/db.d.ts +38 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +118 -26
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/remote.d.ts +23 -0
- package/dist/lib/remote.d.ts.map +1 -1
- package/dist/lib/remote.js +88 -0
- package/dist/lib/remote.js.map +1 -1
- package/dist/lib/remoteWizard.d.ts.map +1 -1
- package/dist/lib/remoteWizard.js +13 -17
- package/dist/lib/remoteWizard.js.map +1 -1
- package/dist/lib/setup/sections/ides.d.ts +20 -0
- package/dist/lib/setup/sections/ides.d.ts.map +1 -0
- package/dist/lib/setup/sections/ides.js +124 -0
- package/dist/lib/setup/sections/ides.js.map +1 -0
- package/dist/lib/setup/sections/preferences.d.ts +30 -0
- package/dist/lib/setup/sections/preferences.d.ts.map +1 -0
- package/dist/lib/setup/sections/preferences.js +128 -0
- package/dist/lib/setup/sections/preferences.js.map +1 -0
- package/dist/lib/setup/sections/routing.d.ts +21 -0
- package/dist/lib/setup/sections/routing.d.ts.map +1 -0
- package/dist/lib/setup/sections/routing.js +160 -0
- package/dist/lib/setup/sections/routing.js.map +1 -0
- package/dist/lib/setup/summary.d.ts +42 -0
- package/dist/lib/setup/summary.d.ts.map +1 -0
- package/dist/lib/setup/summary.js +206 -0
- package/dist/lib/setup/summary.js.map +1 -0
- package/dist/lib/timeline.d.ts +7 -0
- package/dist/lib/timeline.d.ts.map +1 -1
- package/dist/lib/timeline.js +19 -5
- package/dist/lib/timeline.js.map +1 -1
- 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 {
|
|
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`
|
|
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:
|
|
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
|
-
//
|
|
573
|
-
|
|
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("
|
|
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
|
|
1686
|
-
const
|
|
1687
|
-
if (
|
|
1688
|
-
console.
|
|
1689
|
-
|
|
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
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
if (
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2874
|
-
|
|
2875
|
-
process.exit(1);
|
|
3034
|
+
catch {
|
|
3035
|
+
// Central DB not available — legacy dashboard only
|
|
2876
3036
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
-
|
|
2882
|
-
|
|
2883
|
-
.
|
|
2884
|
-
.
|
|
2885
|
-
.option("--
|
|
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
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
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 (
|
|
2919
|
-
console.log(
|
|
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
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3097
|
+
const writeTarget = resolver.getWriteTarget();
|
|
3098
|
+
if (!writeTarget) {
|
|
3099
|
+
console.error("No writable store found.");
|
|
2926
3100
|
process.exit(1);
|
|
2927
3101
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
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
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
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
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
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
|
-
|
|
2970
|
-
|
|
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
|
-
//
|
|
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
|
-
.
|
|
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
|
|
3219
|
-
console.log(`
|
|
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
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|
|
3353
|
-
.option("-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
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
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
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
|
-
|
|
3917
|
-
|
|
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
|
|
4120
|
-
.option("--prune", "Delete registry entries whose directory no longer exists
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
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("
|
|
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)")
|