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