gnosys 5.7.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 CHANGED
@@ -80,7 +80,7 @@ gnosys recall "database selection"
80
80
 
81
81
  > **Postinstall hook:** After `npm install -g gnosys`, a postinstall script automatically runs `gnosys setup` if no configuration is detected, so first-time users are guided through provider and IDE setup immediately.
82
82
 
83
- > **Multi-machine?** Set `GNOSYS_GLOBAL` to a cloud-synced folder (iCloud Drive, Dropbox, OneDrive) and both machines share the same brain. After updating, run `gnosys upgrade` — it re-syncs all projects, regenerates agent rules, and warns other machines to upgrade too. See the [User Guide — Installation & Setup](https://gnosys.ai/guide.html#guide-installation) for the full walkthrough, memory scopes, and multi-machine setup.
83
+ > **Multi-machine?** Set `GNOSYS_GLOBAL` to a cloud-synced folder (iCloud Drive, Dropbox, OneDrive) and both machines share the same brain. To update: run `gnosys upgrade` — it installs the latest gnosys, signals any running MCP servers to restart cleanly, and prompts to run `gnosys setup sync-projects` afterwards (re-syncs all projects, regenerates agent rules, warns other machines to upgrade). See the [User Guide — Installation & Setup](https://gnosys.ai/guide.html#guide-installation) for the full walkthrough, memory scopes, and multi-machine setup.
84
84
 
85
85
  ### Agent / Helper Library
86
86
 
@@ -331,7 +331,7 @@ Dream Mode is the idle-time consolidation engine — confidence decay, summary g
331
331
  - Run `gnosys setup dream` on the machine you want to host dream cycles.
332
332
  - The wizard validates your provider/model with a live API probe before saving.
333
333
  - Other machines stay quiet — they see the designation in the central DB and skip the scheduler.
334
- - `gnosys dream log` shows recent runs; `gnosys dashboard` has a `DREAM HEALTH` section with last-run timestamp, designated machine, and consecutive-failure counter.
334
+ - `gnosys dream log` shows recent runs; `gnosys status --system` has a `DREAM HEALTH` section with last-run timestamp, designated machine, and consecutive-failure counter.
335
335
  - If the designated machine's LLM provider becomes unreachable, you'll see warnings at three layers: in audit log entries (`dream_provider_unreachable`), as stderr at MCP startup, and as a desktop notification after 3 consecutive failures.
336
336
 
337
337
  ### Removed in v5.4.2
@@ -345,6 +345,19 @@ The following commands were removed in favor of the canonical `gnosys setup <thi
345
345
 
346
346
  `gnosys remote push|pull|sync|status` remain unchanged — only `configure` moved.
347
347
 
348
+ ### Removed / renamed in v5.7.1
349
+
350
+ | Removed / renamed | Use instead |
351
+ |---|---|
352
+ | `gnosys dashboard` | `gnosys status --system` |
353
+ | `gnosys portfolio` | `gnosys status --projects` (or `--web` for HTML) |
354
+ | `gnosys upgrade` (old behaviour) | `gnosys setup sync-projects` |
355
+ | `gnosys status --global` | `gnosys status --projects` (alias kept) |
356
+
357
+ `gnosys upgrade` is now `npm install -g gnosys@latest` + a marker that
358
+ tells running MCP servers to exit-and-respawn against the new global
359
+ binary. Run it on each machine.
360
+
348
361
  ---
349
362
 
350
363
  ### Manual config (if you prefer)
package/dist/cli.js CHANGED
@@ -225,6 +225,7 @@ program
225
225
  .option("--federated", "Use federated discovery with tier boosting (project > user > global)")
226
226
  .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated for multiple)")
227
227
  .option("-d, --directory <dir>", "Project directory for context")
228
+ .option("--id-format <format>", "ID display format: short | long | raw (default: short)", "short")
228
229
  .action(async (query, opts) => {
229
230
  // Federated discover path
230
231
  if (opts.federated || opts.scope) {
@@ -279,11 +280,16 @@ program
279
280
  });
280
281
  return;
281
282
  }
283
+ const { formatMemoryId, buildProjectNameLookup, parseIdFormat } = await import("./lib/idFormat.js");
284
+ const idFormat = parseIdFormat(opts.idFormat);
285
+ const projectNames = buildProjectNameLookup(centralDb);
282
286
  outputResult(!!opts.json, { query, count: results.length, results }, () => {
283
287
  console.log(`Found ${results.length} relevant memories for "${query}":\n`);
284
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);
285
291
  console.log(` ${r.title}`);
286
- console.log(` id: ${r.id}`);
292
+ console.log(` id: ${displayId}`);
287
293
  if (r.relevance)
288
294
  console.log(` Relevance: ${r.relevance}`);
289
295
  console.log();
@@ -391,6 +397,7 @@ program
391
397
  .option("-t, --tag <tag>", "Filter by tag")
392
398
  .option("-s, --store <store>", "Filter by store layer (project|user|global)")
393
399
  .option("--json", "Output as JSON")
400
+ .option("--id-format <format>", "ID display format: short | long | raw (default: short)", "short")
394
401
  .action(async (opts) => {
395
402
  let centralDb = null;
396
403
  try {
@@ -424,6 +431,9 @@ program
424
431
  }
425
432
  });
426
433
  }
434
+ const { formatMemoryId, buildProjectNameLookup, parseIdFormat } = await import("./lib/idFormat.js");
435
+ const idFormat = parseIdFormat(opts.idFormat);
436
+ const projectNames = buildProjectNameLookup(centralDb);
427
437
  outputResult(!!opts.json, {
428
438
  count: memories.length,
429
439
  memories: memories.map((m) => ({
@@ -433,12 +443,15 @@ program
433
443
  status: m.status,
434
444
  scope: m.scope,
435
445
  confidence: m.confidence,
446
+ project: m.project_id ? projectNames.get(m.project_id) || null : null,
436
447
  })),
437
448
  }, () => {
438
449
  console.log(`${memories.length} memories:\n`);
439
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);
440
453
  console.log(` [${m.scope}] [${m.status}] ${m.title}`);
441
- console.log(` id: ${m.id} | category: ${m.category} | confidence: ${m.confidence}`);
454
+ console.log(` id: ${displayId} | category: ${m.category} | confidence: ${m.confidence}`);
442
455
  console.log();
443
456
  }
444
457
  });
@@ -654,8 +667,9 @@ setupRemoteCmd
654
667
  return;
655
668
  }
656
669
  const { RemoteSync, formatStatus } = await import("./lib/remote.js");
670
+ const { withHeartbeat } = await import("./lib/heartbeat.js");
657
671
  const sync = new RemoteSync(centralDb, remotePath);
658
- const status = await sync.getStatus();
672
+ const status = await withHeartbeat("Checking remote sync status", () => sync.getStatus());
659
673
  sync.closeRemote();
660
674
  if (opts.json) {
661
675
  console.log(JSON.stringify(status, null, 2));
@@ -685,6 +699,7 @@ setupRemoteCmd
685
699
  .command("push")
686
700
  .description("Push local changes to remote")
687
701
  .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
702
+ .option("--verbose", "Stream per-memory progress to stderr")
688
703
  .action(async (opts) => {
689
704
  let centralDb = null;
690
705
  try {
@@ -699,8 +714,18 @@ setupRemoteCmd
699
714
  process.exit(1);
700
715
  }
701
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);
702
720
  const sync = new RemoteSync(centralDb, remotePath);
703
- const result = await sync.push({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
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);
704
729
  sync.closeRemote();
705
730
  const projParts = (result.projectsPushed || 0) > 0 ? ` | Projects pushed: ${result.projectsPushed}` : "";
706
731
  const auditParts = (result.auditPushed || 0) > 0 ? ` | Audit pushed: ${result.auditPushed}` : "";
@@ -728,6 +753,7 @@ setupRemoteCmd
728
753
  .command("pull")
729
754
  .description("Pull remote changes to local")
730
755
  .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
756
+ .option("--verbose", "Stream per-memory progress to stderr")
731
757
  .action(async (opts) => {
732
758
  let centralDb = null;
733
759
  try {
@@ -742,8 +768,17 @@ setupRemoteCmd
742
768
  process.exit(1);
743
769
  }
744
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);
745
774
  const sync = new RemoteSync(centralDb, remotePath);
746
- const result = await sync.pull({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
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);
747
782
  sync.closeRemote();
748
783
  const projParts = (result.projectsPulled || 0) > 0 ? ` | Projects pulled: ${result.projectsPulled}` : "";
749
784
  const auditParts = (result.auditPulled || 0) > 0 ? ` | Audit pulled: ${result.auditPulled}` : "";
@@ -767,6 +802,7 @@ setupRemoteCmd
767
802
  .description("Two-way sync: push local changes then pull remote changes")
768
803
  .option("--auto", "Run silently for cron/LaunchAgent (skip-and-flag for conflicts)")
769
804
  .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
805
+ .option("--verbose", "Stream per-memory progress to stderr")
770
806
  .action(async (opts) => {
771
807
  let centralDb = null;
772
808
  try {
@@ -783,11 +819,20 @@ setupRemoteCmd
783
819
  process.exit(opts.auto ? 0 : 1);
784
820
  }
785
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);
786
825
  const sync = new RemoteSync(centralDb, remotePath);
787
- const result = await sync.sync({
826
+ const runSync = () => sync.sync({
788
827
  auto: opts.auto,
789
828
  strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
829
+ onProgress: progress.noop ? undefined : progress.emit.bind(progress),
790
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);
791
836
  sync.closeRemote();
792
837
  if (!opts.auto || result.conflicts.length > 0 || result.errors.length > 0) {
793
838
  const pp = result.projectsPushed || 0;
@@ -3007,41 +3052,9 @@ program
3007
3052
  console.log("");
3008
3053
  console.log(formatGraphStats(stats));
3009
3054
  });
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.
3013
- program
3014
- .command("dashboard")
3015
- .description("(alias) Show system health — equivalent to 'gnosys status --system'")
3016
- .option("--json", "Output as JSON instead of pretty table")
3017
- .action(async (opts) => {
3018
- const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");
3019
- const resolver = await getResolver();
3020
- const stores = resolver.getStores();
3021
- if (stores.length === 0) {
3022
- console.error("No Gnosys stores found. Run gnosys init first.");
3023
- process.exit(1);
3024
- }
3025
- const cfg = await loadConfig(stores[0].path);
3026
- // v5.1: Use central DB for dashboard stats
3027
- let dashDb;
3028
- try {
3029
- const db = GnosysDB.openCentral();
3030
- if (db.isAvailable() && db.isMigrated()) {
3031
- dashDb = db;
3032
- }
3033
- }
3034
- catch {
3035
- // Central DB not available — legacy dashboard only
3036
- }
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));
3043
- }
3044
- });
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.
3045
3058
  // ─── gnosys maintain ─────────────────────────────────────────────────────
3046
3059
  program
3047
3060
  .command("maintain")
@@ -3125,12 +3138,19 @@ program
3125
3138
  }
3126
3139
  });
3127
3140
  // NOTE: gnosys migrate is defined below (near the end) with --to-central support
3128
- // ─── gnosys upgrade ─────────────────────────────────────────────────────
3129
- program
3130
- .command("upgrade")
3131
- .description("Re-initialize all registered projects after a Gnosys version upgrade. Updates agent rules, project registry, stamps the central DB, and regenerates the portfolio dashboard.")
3132
- .option("--skip-dashboard", "Skip regenerating the portfolio dashboard")
3133
- .action(async (opts) => {
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) {
3134
3154
  const currentVersion = pkg.version;
3135
3155
  console.log(`Gnosys v${currentVersion} — upgrading registered projects...\n`);
3136
3156
  // 1. Read registered projects from file registry AND central DB
@@ -3349,6 +3369,76 @@ program
3349
3369
  console.log(`\n Could not regenerate portfolio dashboard`);
3350
3370
  }
3351
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({});
3352
3442
  });
3353
3443
  // ─── gnosys doctor ──────────────────────────────────────────────────────
3354
3444
  program
@@ -4858,80 +4948,70 @@ program
4858
4948
  centralDb?.close();
4859
4949
  }
4860
4950
  });
4861
- // ─── gnosys portfolio ───────────────────────────────────────────────────
4951
+ // `gnosys portfolio` was removed in v5.7.1.
4952
+ // Use `gnosys status --projects` (formerly --global) for the projects
4953
+ // overview, or `gnosys status --web` for the HTML dashboard, or
4954
+ // `gnosys status --projects --output file.html` to write to disk.
4955
+ // ─── gnosys status ──────────────────────────────────────────────────────
4956
+ // v5.7.1 (#11): the catch-all status command. Section flags select what to
4957
+ // show; output flags control format. Default (no flag) is the current
4958
+ // project. `dashboard` and `portfolio` were removed in v5.7.1 — their
4959
+ // content lives under `--system` and `--projects` respectively.
4862
4960
  program
4863
- .command("portfolio")
4864
- .description("Portfolio dashboard all projects with status, roadmap, and recent activity")
4865
- .option("-o, --output <file>", "Write dashboard to a file (auto-detects format from extension)")
4866
- .option("--html", "Output as HTML dashboard")
4961
+ .command("status")
4962
+ .description("Show status. Sections: --projects (all projects) · --remote (sync) · --system (memory/LLM health) · default: current project. Output: --web · --json. Note: 'gnosys dashboard' and 'gnosys portfolio' were removed in v5.7.1 — use 'gnosys status --system' and 'gnosys status --projects' instead.")
4963
+ .option("-d, --directory <dir>", "Project directory (auto-detects if omitted)")
4964
+ .option("-p, --project <id>", "Project ID")
4965
+ .option("-g, --global", "(deprecated alias for --projects)")
4966
+ .option("--projects", "Show all projects portfolio (replaces the old 'gnosys portfolio')")
4967
+ .option("-r, --remote", "Show remote sync status (alias for 'gnosys setup remote status')")
4968
+ .option("-w, --web", "Open the HTML dashboard in the browser")
4969
+ .option("-s, --system", "Show system health (memory count, LLM connectivity, embeddings, archive)")
4867
4970
  .option("--json", "Output as JSON")
4868
4971
  .action(async (opts) => {
4869
- let centralDb = null;
4870
- try {
4871
- centralDb = GnosysDB.openCentral();
4872
- if (!centralDb.isAvailable()) {
4873
- console.error("Central DB not available.");
4874
- process.exit(1);
4875
- }
4876
- const { generatePortfolio, formatPortfolioMarkdown } = await import("./lib/portfolio.js");
4877
- const report = generatePortfolio(centralDb);
4878
- // Detect format from output extension if not explicitly set
4879
- const useHtml = opts.html || (opts.output?.endsWith(".html") ?? false);
4880
- const useJson = opts.json || (opts.output?.endsWith(".json") ?? false);
4881
- if (useJson) {
4882
- const json = JSON.stringify(report, null, 2);
4883
- if (opts.output) {
4884
- const { writeFileSync } = await import("fs");
4885
- writeFileSync(opts.output, json, "utf-8");
4886
- 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);
4887
4983
  }
4888
- else {
4889
- console.log(json);
4984
+ const remotePath = remoteCentralDb.getMeta("remote_path");
4985
+ if (!remotePath) {
4986
+ if (opts.json) {
4987
+ console.log(JSON.stringify({ configured: false, message: "Remote not configured. Run 'gnosys setup remote'." }, null, 2));
4988
+ }
4989
+ else {
4990
+ console.log("Remote sync: not configured. Run 'gnosys setup remote' to set up multi-machine sync.");
4991
+ }
4992
+ return;
4890
4993
  }
4891
- return;
4892
- }
4893
- if (useHtml) {
4894
- const { generatePortfolioHtml } = await import("./lib/portfolioHtml.js");
4895
- const html = generatePortfolioHtml(report, opts.output);
4896
- if (opts.output) {
4897
- const { writeFileSync } = await import("fs");
4898
- writeFileSync(opts.output, html, "utf-8");
4899
- 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));
4900
5001
  }
4901
5002
  else {
4902
- console.log(html);
5003
+ console.log(formatStatus(status));
4903
5004
  }
4904
5005
  return;
4905
5006
  }
4906
- const markdown = formatPortfolioMarkdown(report);
4907
- if (opts.output) {
4908
- const { writeFileSync } = await import("fs");
4909
- writeFileSync(opts.output, markdown, "utf-8");
4910
- 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);
4911
5010
  }
4912
- else {
4913
- console.log(markdown);
5011
+ finally {
5012
+ remoteCentralDb?.close();
4914
5013
  }
4915
5014
  }
4916
- catch (err) {
4917
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
4918
- process.exit(1);
4919
- }
4920
- finally {
4921
- centralDb?.close();
4922
- }
4923
- });
4924
- // ─── gnosys status ──────────────────────────────────────────────────────
4925
- program
4926
- .command("status")
4927
- .description("Show project status (--global: all projects, --web: HTML dashboard, --system: memory/LLM health)")
4928
- .option("-d, --directory <dir>", "Project directory (auto-detects if omitted)")
4929
- .option("-p, --project <id>", "Project ID")
4930
- .option("-g, --global", "Show all projects")
4931
- .option("-w, --web", "Open the HTML dashboard in the browser")
4932
- .option("-s, --system", "Show system health (memory count, LLM connectivity, embeddings, archive)")
4933
- .option("--json", "Output as JSON")
4934
- .action(async (opts) => {
4935
5015
  // --system delegates to the dashboard formatter (formerly `gnosys dashboard`).
4936
5016
  if (opts.system) {
4937
5017
  const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");