gnosys 5.3.3 → 5.4.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.
Files changed (56) hide show
  1. package/README.md +80 -8
  2. package/dist/cli.js +225 -46
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +136 -97
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/config.d.ts +0 -5
  7. package/dist/lib/config.d.ts.map +1 -1
  8. package/dist/lib/config.js +71 -9
  9. package/dist/lib/config.js.map +1 -1
  10. package/dist/lib/dashboard.d.ts.map +1 -1
  11. package/dist/lib/dashboard.js +137 -94
  12. package/dist/lib/dashboard.js.map +1 -1
  13. package/dist/lib/db.d.ts +54 -6
  14. package/dist/lib/db.d.ts.map +1 -1
  15. package/dist/lib/db.js +157 -25
  16. package/dist/lib/db.js.map +1 -1
  17. package/dist/lib/ingest.d.ts.map +1 -1
  18. package/dist/lib/ingest.js +24 -4
  19. package/dist/lib/ingest.js.map +1 -1
  20. package/dist/lib/lock.d.ts +5 -0
  21. package/dist/lib/lock.d.ts.map +1 -1
  22. package/dist/lib/lock.js +9 -0
  23. package/dist/lib/lock.js.map +1 -1
  24. package/dist/lib/modelValidation.d.ts +22 -0
  25. package/dist/lib/modelValidation.d.ts.map +1 -0
  26. package/dist/lib/modelValidation.js +157 -0
  27. package/dist/lib/modelValidation.js.map +1 -0
  28. package/dist/lib/paths.d.ts +32 -0
  29. package/dist/lib/paths.d.ts.map +1 -0
  30. package/dist/lib/paths.js +44 -0
  31. package/dist/lib/paths.js.map +1 -0
  32. package/dist/lib/remote.d.ts +15 -0
  33. package/dist/lib/remote.d.ts.map +1 -1
  34. package/dist/lib/remote.js +85 -0
  35. package/dist/lib/remote.js.map +1 -1
  36. package/dist/lib/remoteWizard.d.ts +2 -1
  37. package/dist/lib/remoteWizard.d.ts.map +1 -1
  38. package/dist/lib/remoteWizard.js +6 -3
  39. package/dist/lib/remoteWizard.js.map +1 -1
  40. package/dist/lib/setup.d.ts +25 -0
  41. package/dist/lib/setup.d.ts.map +1 -1
  42. package/dist/lib/setup.js +459 -25
  43. package/dist/lib/setup.js.map +1 -1
  44. package/dist/lib/store.d.ts +2 -0
  45. package/dist/lib/store.d.ts.map +1 -1
  46. package/dist/lib/store.js +4 -11
  47. package/dist/lib/store.js.map +1 -1
  48. package/dist/postinstall.js +2 -2
  49. package/dist/postinstall.js.map +1 -1
  50. package/dist/sandbox/helper-template.d.ts.map +1 -1
  51. package/dist/sandbox/helper-template.js +8 -2
  52. package/dist/sandbox/helper-template.js.map +1 -1
  53. package/dist/sandbox/server.d.ts.map +1 -1
  54. package/dist/sandbox/server.js +2 -2
  55. package/dist/sandbox/server.js.map +1 -1
  56. package/package.json +2 -1
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  Gnosys is **sandbox-first**: a persistent background process holds the database connection while agents import a tiny helper library and call memory operations like normal code — no MCP schemas, no round-trips, near-zero context cost. The central brain at `~/.gnosys/gnosys.db` unifies all projects, user preferences, and global knowledge. Federated search ranks results across scopes with tier boosting and recency awareness. The **Web Knowledge Base** turns any website into a searchable knowledge base for serverless chatbots — pre-computed JSON index, zero runtime dependencies. **Multimodal ingestion** handles PDFs, images, audio, and video. **Portfolio Dashboard** gives a bird's-eye view of all projects. Process tracing builds call chains from source code. Dream Mode consolidates knowledge during idle time. One-command export regenerates a full Obsidian vault.
26
26
 
27
- It also runs as a CLI and a complete MCP server that drops straight into Cursor, Claude Desktop, Claude Code, Cowork, Codex, or any MCP client.
27
+ It also runs as a CLI and a complete MCP server that drops straight into Cursor, Claude Desktop (Chat / Cowork / Code), Claude Code, Codex, Gemini CLI, Antigravity, or any MCP client.
28
28
 
29
29
  No vector DBs. No black boxes. No external services. Just SQLite and optional Obsidian export — the way knowledge should be.
30
30
 
@@ -53,7 +53,7 @@ Gnosys takes a different approach: the central brain is a single SQLite database
53
53
  - **Bulk import** — CSV, JSON, JSONL. Import entire datasets in seconds.
54
54
  - **Obsidian-native** — `gnosys export` generates a full vault with YAML frontmatter, `[[wikilinks]]`, summaries, and graph data.
55
55
  - **Multi-machine sync (v5.3.0)** — share your `gnosys.db` across machines via NAS or shared drive. Local cache for speed, remote source of truth for consistency. Built-in conflict detection with skip-and-flag resolution. Run `gnosys remote configure` to set up.
56
- - **MCP-compatible** — also runs as a full MCP server that drops into Cursor, Claude Desktop, Claude Code, Cowork, Codex, or any MCP client.
56
+ - **MCP-compatible** — also runs as a full MCP server that drops into Cursor, Claude Desktop (Chat / Cowork / Code), Claude Code, Codex, Gemini CLI, Antigravity, or any MCP client.
57
57
  - **Zero infrastructure** — no external databases, no Docker (unless you want it), no cloud services. Just `npm install`.
58
58
 
59
59
  > For the complete CLI reference and detailed guides, see the **[User Guide](https://gnosys.ai/guide.html)**.
@@ -235,9 +235,47 @@ This improves your site's visibility in AI-powered search results and enables LL
235
235
 
236
236
  ## MCP Server Setup
237
237
 
238
- ### Claude Desktop
238
+ The fastest way to wire gnosys into any supported client is to run `gnosys init <ide>` from the project directory you want memory-enabled. Examples:
239
239
 
240
- Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
240
+ ```bash
241
+ gnosys init claude-desktop # Claude Desktop (covers Chat, Cowork, and Code)
242
+ gnosys init claude # Claude Code CLI
243
+ gnosys init cursor # Cursor
244
+ gnosys init codex # Codex
245
+ gnosys init gemini-cli # Gemini CLI
246
+ gnosys init antigravity # Google Antigravity
247
+ ```
248
+
249
+ This does two things at once:
250
+ 1. Wires gnosys into the IDE's MCP config (idempotent — safe to re-run).
251
+ 2. Initializes the current directory as a gnosys project (creates `.gnosys/gnosys.json`, registers it in the central DB) so your memories can be scoped to it.
252
+
253
+ ### One-time vs. per-project
254
+
255
+ The IDE wiring writes to a **user-level** config file, so it only needs to happen **once**. Re-running it in another project just re-merges the same `mcpServers.gnosys` entry — harmless.
256
+
257
+ The project registration is **per-directory**: every codebase you want to be memory-aware of needs its own `gnosys init`. From then on, agents pass `projectRoot: "/path/to/project"` to gnosys MCP tools to scope memory to that codebase.
258
+
259
+ ```bash
260
+ # Once, anywhere — wires Claude Desktop's MCP config:
261
+ gnosys init claude-desktop
262
+
263
+ # Once per project — registers the codebase in the central DB:
264
+ cd /path/to/project-a && gnosys init
265
+ cd /path/to/project-b && gnosys init
266
+ ```
267
+
268
+ > **Cowork users:** Cowork sessions don't have a working directory like a CLI does. The agent in Cowork uses whichever `projectRoot` it's told to use (typically auto-detected from open files or set via the system prompt). The "every working directory" question doesn't apply to Cowork itself — only to the projects you want memory-enabled. Run `gnosys init claude-desktop` once globally; run `gnosys init` per project.
269
+
270
+ ---
271
+
272
+ ### Manual config (if you prefer)
273
+
274
+ If you'd rather edit configs by hand, here's where each client looks for MCP servers.
275
+
276
+ #### Claude Desktop (Chat, Cowork, and Code share the same config)
277
+
278
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%\Claude\claude_desktop_config.json` (Windows), or `~/.config/Claude/claude_desktop_config.json` (Linux):
241
279
 
242
280
  ```json
243
281
  {
@@ -250,9 +288,11 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
250
288
  }
251
289
  ```
252
290
 
253
- ### Cursor
291
+ Restart Claude Desktop after editing.
292
+
293
+ #### Cursor
254
294
 
255
- Add to `.cursor/mcp.json`:
295
+ Add to `.cursor/mcp.json` in your project:
256
296
 
257
297
  ```json
258
298
  {
@@ -265,13 +305,13 @@ Add to `.cursor/mcp.json`:
265
305
  }
266
306
  ```
267
307
 
268
- ### Claude Code
308
+ #### Claude Code
269
309
 
270
310
  ```bash
271
311
  claude mcp add gnosys gnosys serve
272
312
  ```
273
313
 
274
- ### Codex
314
+ #### Codex
275
315
 
276
316
  Add to `.codex/config.toml`:
277
317
 
@@ -281,6 +321,38 @@ type = "local"
281
321
  command = ["gnosys", "serve"]
282
322
  ```
283
323
 
324
+ #### Gemini CLI
325
+
326
+ Add to `~/.gemini/settings.json` (preserves any existing settings):
327
+
328
+ ```json
329
+ {
330
+ "mcpServers": {
331
+ "gnosys": {
332
+ "command": "gnosys",
333
+ "args": ["serve"]
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
339
+ #### Antigravity
340
+
341
+ Add to `~/.gemini/antigravity/mcp_config.json`:
342
+
343
+ ```json
344
+ {
345
+ "mcpServers": {
346
+ "gnosys": {
347
+ "command": "gnosys",
348
+ "args": ["serve"]
349
+ }
350
+ }
351
+ }
352
+ ```
353
+
354
+ Antigravity reloads MCP servers automatically when you save the file.
355
+
284
356
  > **Note:** API keys are configured via `gnosys setup` (macOS Keychain, environment variable, or `~/.config/gnosys/.env`). See [LLM Provider Setup](https://gnosys.ai/guide.html#guide-llm-provider-setup) in the User Guide.
285
357
 
286
358
  ---
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { fileURLToPath } from "url";
11
11
  import dotenv from "dotenv";
12
12
  import { readFileSync, existsSync, copyFileSync } from "fs";
13
13
  import { GnosysResolver } from "./lib/resolver.js";
14
+ import { getGnosysHome } from "./lib/paths.js";
14
15
  import { GnosysSearch } from "./lib/search.js";
15
16
  import { GnosysTagRegistry } from "./lib/tags.js";
16
17
  import { GnosysIngestion } from "./lib/ingest.js";
@@ -469,10 +470,12 @@ program
469
470
  }
470
471
  }
471
472
  });
472
- // ─── gnosys setup ───────────────────────────────────────────────────────
473
- program
473
+ // ─── gnosys setup (parent command) ──────────────────────────────────────
474
+ const setupCmd = program
474
475
  .command("setup")
475
- .description("Interactive setup wizard configure LLM provider, API key, model, and IDE integration in one step")
476
+ .description("Configure Gnosys — LLM provider, models, remote sync, and IDE integration");
477
+ // Bare `gnosys setup` runs the full interactive wizard
478
+ setupCmd
476
479
  .option("--non-interactive", "Skip prompts, use defaults (for CI/scripting)")
477
480
  .action(async (opts) => {
478
481
  const { runSetup } = await import("./lib/setup.js");
@@ -481,10 +484,65 @@ program
481
484
  nonInteractive: opts.nonInteractive,
482
485
  });
483
486
  });
487
+ // `gnosys setup models` — just configure LLM provider/model/key
488
+ setupCmd
489
+ .command("models")
490
+ .description("Update LLM provider and model configuration")
491
+ .option("-p, --provider <name>", "Set provider directly (anthropic, openai, xai, groq, mistral, ollama, lmstudio, custom)")
492
+ .option("-m, --model <name>", "Set model name directly")
493
+ .option("--no-validate", "Skip the test API call")
494
+ .action(async (opts) => {
495
+ const { runModelsSetup } = await import("./lib/setup.js");
496
+ await runModelsSetup({
497
+ directory: process.cwd(),
498
+ provider: opts.provider,
499
+ model: opts.model,
500
+ validate: opts.validate,
501
+ });
502
+ });
503
+ // `gnosys setup remote` — configure remote sync (alias for `gnosys remote configure`)
504
+ setupCmd
505
+ .command("remote")
506
+ .description("Configure multi-machine sync (alias for 'gnosys remote configure')")
507
+ .option("--path <path>", "Set remote path directly (non-interactive)")
508
+ .action(async (opts) => {
509
+ const { GnosysDB } = await import("./lib/db.js");
510
+ // Sync configuration needs explicit local DB access (not auto-routed remote).
511
+ const db = GnosysDB.openLocal();
512
+ if (!db.isAvailable()) {
513
+ console.error("Central DB not available.");
514
+ db.close();
515
+ process.exit(1);
516
+ }
517
+ try {
518
+ if (opts.path) {
519
+ const { configureFromPath } = await import("./lib/remoteWizard.js");
520
+ await configureFromPath(db, opts.path);
521
+ }
522
+ else {
523
+ const { runConfigureWizard } = await import("./lib/remoteWizard.js");
524
+ await runConfigureWizard(db);
525
+ }
526
+ }
527
+ finally {
528
+ db.close();
529
+ }
530
+ });
531
+ // ─── gnosys models (top-level shortcut) ─────────────────────────────────
532
+ program
533
+ .command("models")
534
+ .description("Quick model operations — list available, refresh cache, or set the default")
535
+ .option("--list", "List available models for the current provider")
536
+ .option("--refresh", "Refresh model list from OpenRouter (clears the cache)")
537
+ .option("--set <model>", "Set the default model for the current provider")
538
+ .action(async (opts) => {
539
+ const { runModelsCommand } = await import("./lib/setup.js");
540
+ await runModelsCommand(opts);
541
+ });
484
542
  // ─── gnosys init ─────────────────────────────────────────────────────────
485
543
  program
486
544
  .command("init [ide]")
487
- .description("Initialize Gnosys in the current directory. Optionally specify IDE: cursor, claude, or codex to force IDE setup.")
545
+ .description("Initialize Gnosys in the current directory. Optionally specify IDE: cursor, claude, claude-desktop, codex, gemini-cli, or antigravity to force IDE setup.")
488
546
  .option("-d, --directory <dir>", "Target directory (default: cwd)")
489
547
  .option("-n, --name <name>", "Project name (default: directory basename)")
490
548
  .action(async (ide, opts) => {
@@ -591,13 +649,15 @@ program
591
649
  }
592
650
  // If a specific IDE was requested, force-create its config
593
651
  if (ide) {
594
- const validIdes = ["cursor", "claude", "codex"];
652
+ const validIdes = ["cursor", "claude", "claude-desktop", "codex", "gemini-cli", "antigravity"];
595
653
  const normalizedIde = ide.toLowerCase();
596
654
  if (!validIdes.includes(normalizedIde)) {
597
655
  console.log(`\nUnknown IDE: "${ide}". Valid options: ${validIdes.join(", ")}`);
598
656
  }
599
657
  else {
600
658
  const { configureCursor, configureClaudeCode, configureCodex } = await import("./lib/projectIdentity.js");
659
+ // Cursor/Claude/Codex have IDE-specific session hooks. Gemini CLI and
660
+ // Antigravity don't yet, so we skip the hook step for them.
601
661
  let result;
602
662
  switch (normalizedIde) {
603
663
  case "cursor":
@@ -615,28 +675,30 @@ program
615
675
  console.log(` ${result.details}`);
616
676
  console.log(` File: ${result.filePath}`);
617
677
  }
618
- // Also set up MCP config for the IDE
678
+ // Set up MCP config for the IDE
619
679
  const { setupIDE } = await import("./lib/setup.js");
620
680
  const mcp = await setupIDE(normalizedIde, targetDir);
621
681
  if (mcp.success) {
622
682
  console.log(` MCP: ${mcp.message}`);
623
683
  }
624
- // Update agentRulesTarget in gnosys.json
625
- const identityPath = path.join(storePath, "gnosys.json");
626
- try {
627
- const identityContent = await fs.readFile(identityPath, "utf-8");
628
- const identity = JSON.parse(identityContent);
629
- const targetMap = {
630
- cursor: ".cursor/rules/gnosys.mdc",
631
- claude: "CLAUDE.md",
632
- codex: "CODEX.md",
633
- };
634
- identity.agentRulesTarget = targetMap[normalizedIde] || null;
635
- await fs.writeFile(identityPath, JSON.stringify(identity, null, 2) + "\n", "utf-8");
636
- console.log(` Config: agentRulesTarget ${identity.agentRulesTarget}`);
637
- }
638
- catch {
639
- // Non-critical
684
+ // Update agentRulesTarget in gnosys.json (only for IDEs with rules files)
685
+ const targetMap = {
686
+ cursor: ".cursor/rules/gnosys.mdc",
687
+ claude: "CLAUDE.md",
688
+ codex: "CODEX.md",
689
+ };
690
+ if (targetMap[normalizedIde]) {
691
+ const identityPath = path.join(storePath, "gnosys.json");
692
+ try {
693
+ const identityContent = await fs.readFile(identityPath, "utf-8");
694
+ const identity = JSON.parse(identityContent);
695
+ identity.agentRulesTarget = targetMap[normalizedIde];
696
+ await fs.writeFile(identityPath, JSON.stringify(identity, null, 2) + "\n", "utf-8");
697
+ console.log(` Config: agentRulesTarget → ${identity.agentRulesTarget}`);
698
+ }
699
+ catch {
700
+ // Non-critical
701
+ }
640
702
  }
641
703
  }
642
704
  }
@@ -1652,14 +1714,60 @@ program
1652
1714
  .command("graph")
1653
1715
  .description("Show the full cross-reference graph across all memories")
1654
1716
  .action(async () => {
1655
- const resolver = await getResolver();
1656
- const allMemories = await resolver.getAllMemories();
1657
- if (allMemories.length === 0) {
1658
- console.log("No memories found.");
1659
- return;
1717
+ // v5.4.1: Query the central DB directly. Previously this used the
1718
+ // filesystem resolver, which returns nothing in v5.x DB-only mode
1719
+ // because memories no longer live as markdown files.
1720
+ let centralDb = null;
1721
+ try {
1722
+ centralDb = GnosysDB.openCentral();
1723
+ if (!centralDb.isAvailable()) {
1724
+ console.error("Central DB not available.");
1725
+ process.exit(1);
1726
+ }
1727
+ const dbMemories = centralDb.getAllMemories();
1728
+ if (dbMemories.length === 0) {
1729
+ console.log("No memories found.");
1730
+ return;
1731
+ }
1732
+ // Adapt DbMemory → legacy Memory shape that buildLinkGraph expects.
1733
+ // The graph builder only reads id, title, content, and synthesises
1734
+ // a filesystem-style path for display.
1735
+ const adapted = dbMemories.map((m) => {
1736
+ let parsedTags = [];
1737
+ try {
1738
+ parsedTags = JSON.parse(m.tags);
1739
+ }
1740
+ catch {
1741
+ parsedTags = [];
1742
+ }
1743
+ const relativePath = `${m.category}/${m.id}.md`;
1744
+ return {
1745
+ frontmatter: {
1746
+ id: m.id,
1747
+ title: m.title,
1748
+ category: m.category,
1749
+ tags: parsedTags,
1750
+ relevance: m.relevance,
1751
+ author: m.author,
1752
+ authority: m.authority,
1753
+ confidence: m.confidence,
1754
+ created: m.created,
1755
+ modified: m.modified,
1756
+ last_reviewed: m.modified,
1757
+ status: m.status,
1758
+ supersedes: m.supersedes,
1759
+ },
1760
+ content: m.content,
1761
+ filePath: relativePath,
1762
+ relativePath,
1763
+ };
1764
+ });
1765
+ const graph = buildLinkGraph(adapted);
1766
+ console.log(formatGraphSummary(graph));
1767
+ }
1768
+ finally {
1769
+ centralDb?.close();
1660
1770
  }
1661
- const graph = buildLinkGraph(allMemories);
1662
- console.log(formatGraphSummary(graph));
1663
1771
  });
1664
1772
  // ─── gnosys bootstrap <sourceDir> ────────────────────────────────────────
1665
1773
  program
@@ -2480,7 +2588,8 @@ remoteCmd
2480
2588
  .action(async (opts) => {
2481
2589
  let centralDb = null;
2482
2590
  try {
2483
- centralDb = GnosysDB.openCentral();
2591
+ // Sync operations need explicit local DB access (not auto-routed remote).
2592
+ centralDb = GnosysDB.openLocal();
2484
2593
  if (!centralDb.isAvailable()) {
2485
2594
  console.error("Central DB not available.");
2486
2595
  process.exit(1);
@@ -2531,7 +2640,7 @@ remoteCmd
2531
2640
  .action(async (opts) => {
2532
2641
  let centralDb = null;
2533
2642
  try {
2534
- centralDb = GnosysDB.openCentral();
2643
+ centralDb = GnosysDB.openLocal();
2535
2644
  if (!centralDb.isAvailable()) {
2536
2645
  console.error("Central DB not available.");
2537
2646
  process.exit(1);
@@ -2545,7 +2654,8 @@ remoteCmd
2545
2654
  const sync = new RemoteSync(centralDb, remotePath);
2546
2655
  const result = await sync.push({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2547
2656
  sync.closeRemote();
2548
- console.log(`Pushed: ${result.pushed} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}`);
2657
+ const projParts = (result.projectsPushed || 0) > 0 ? ` | Projects pushed: ${result.projectsPushed}` : "";
2658
+ console.log(`Pushed: ${result.pushed} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}`);
2549
2659
  if (result.errors.length > 0) {
2550
2660
  console.log("\nErrors:");
2551
2661
  for (const e of result.errors)
@@ -2572,7 +2682,7 @@ remoteCmd
2572
2682
  .action(async (opts) => {
2573
2683
  let centralDb = null;
2574
2684
  try {
2575
- centralDb = GnosysDB.openCentral();
2685
+ centralDb = GnosysDB.openLocal();
2576
2686
  if (!centralDb.isAvailable()) {
2577
2687
  console.error("Central DB not available.");
2578
2688
  process.exit(1);
@@ -2586,7 +2696,8 @@ remoteCmd
2586
2696
  const sync = new RemoteSync(centralDb, remotePath);
2587
2697
  const result = await sync.pull({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2588
2698
  sync.closeRemote();
2589
- console.log(`Pulled: ${result.pulled} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}`);
2699
+ const projParts = (result.projectsPulled || 0) > 0 ? ` | Projects pulled: ${result.projectsPulled}` : "";
2700
+ console.log(`Pulled: ${result.pulled} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}${projParts}`);
2590
2701
  if (result.errors.length > 0) {
2591
2702
  console.log("\nErrors:");
2592
2703
  for (const e of result.errors)
@@ -2609,7 +2720,7 @@ remoteCmd
2609
2720
  .action(async (opts) => {
2610
2721
  let centralDb = null;
2611
2722
  try {
2612
- centralDb = GnosysDB.openCentral();
2723
+ centralDb = GnosysDB.openLocal();
2613
2724
  if (!centralDb.isAvailable()) {
2614
2725
  if (!opts.auto)
2615
2726
  console.error("Central DB not available.");
@@ -2629,7 +2740,10 @@ remoteCmd
2629
2740
  });
2630
2741
  sync.closeRemote();
2631
2742
  if (!opts.auto || result.conflicts.length > 0 || result.errors.length > 0) {
2632
- console.log(`Pushed: ${result.pushed} | Pulled: ${result.pulled} | Conflicts: ${result.conflicts.length}`);
2743
+ const pp = result.projectsPushed || 0;
2744
+ const pl = result.projectsPulled || 0;
2745
+ const projParts = (pp + pl) > 0 ? ` | Projects: ↑${pp}/↓${pl}` : "";
2746
+ console.log(`Pushed: ${result.pushed} | Pulled: ${result.pulled} | Conflicts: ${result.conflicts.length}${projParts}`);
2633
2747
  if (result.errors.length > 0) {
2634
2748
  console.log("\nErrors:");
2635
2749
  for (const e of result.errors)
@@ -2656,7 +2770,7 @@ remoteCmd
2656
2770
  .action(async (memoryId, opts) => {
2657
2771
  let centralDb = null;
2658
2772
  try {
2659
- centralDb = GnosysDB.openCentral();
2773
+ centralDb = GnosysDB.openLocal();
2660
2774
  if (!centralDb.isAvailable()) {
2661
2775
  console.error("Central DB not available.");
2662
2776
  process.exit(1);
@@ -2698,7 +2812,7 @@ remoteCmd
2698
2812
  .action(async (opts) => {
2699
2813
  let centralDb = null;
2700
2814
  try {
2701
- centralDb = GnosysDB.openCentral();
2815
+ centralDb = GnosysDB.openLocal();
2702
2816
  if (!centralDb.isAvailable()) {
2703
2817
  console.error("Central DB not available.");
2704
2818
  process.exit(1);
@@ -3103,7 +3217,7 @@ program
3103
3217
  .action(async (opts) => {
3104
3218
  const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
3105
3219
  const storePath = path.join(projectDir, ".gnosys");
3106
- const globalStorePath = path.join(os.homedir(), ".gnosys");
3220
+ const globalStorePath = getGnosysHome();
3107
3221
  // Load config: try project-level first, fall back to global ~/.gnosys/
3108
3222
  let cfg;
3109
3223
  let configSource;
@@ -3685,10 +3799,22 @@ program
3685
3799
  console.log(` Central DB: ${GnosysDB.getCentralDbPath()}`);
3686
3800
  });
3687
3801
  // ─── gnosys projects ────────────────────────────────────────────────────
3802
+ /**
3803
+ * Returns true if a project's working directory no longer exists on disk.
3804
+ * Used by `gnosys projects` to filter dead entries by default and by
3805
+ * `gnosys projects --prune` to delete them. We deliberately do NOT pattern-
3806
+ * match on tmp paths — active test fixtures live in /var/folders/ and
3807
+ * /tmp/ and we want them visible while they're in use.
3808
+ */
3809
+ function isDeadProjectDir(dir) {
3810
+ return !existsSync(dir);
3811
+ }
3688
3812
  program
3689
3813
  .command("projects")
3690
- .description("List all registered projects in the central DB")
3814
+ .description("List registered projects from the central DB")
3691
3815
  .option("--json", "Output as JSON")
3816
+ .option("--all", "Include dead projects (deleted directories, /tmp/ paths)")
3817
+ .option("--prune", "Delete registry entries whose directory no longer exists or is a tmp path")
3692
3818
  .action(async (opts) => {
3693
3819
  let centralDb = null;
3694
3820
  try {
@@ -3697,18 +3823,71 @@ program
3697
3823
  console.error("Central DB not available (better-sqlite3 missing).");
3698
3824
  process.exit(1);
3699
3825
  }
3700
- const projects = centralDb.getAllProjects();
3701
- if (projects.length === 0) {
3702
- console.log("No projects registered. Run 'gnosys init' in a project directory.");
3826
+ const allProjects = centralDb.getAllProjects();
3827
+ if (opts.prune) {
3828
+ // Find and delete dead projects
3829
+ const deadProjects = allProjects.filter((p) => isDeadProjectDir(p.working_directory));
3830
+ for (const p of deadProjects) {
3831
+ centralDb.deleteProject(p.id);
3832
+ }
3833
+ outputResult(!!opts.json, {
3834
+ deleted: deadProjects.length,
3835
+ remaining: allProjects.length - deadProjects.length,
3836
+ deletedProjects: deadProjects.map((p) => ({ id: p.id, name: p.name, directory: p.working_directory })),
3837
+ }, () => {
3838
+ if (deadProjects.length === 0) {
3839
+ console.log("No dead projects to prune.");
3840
+ }
3841
+ else {
3842
+ const DIM = "\x1b[2m";
3843
+ const RESET = "\x1b[0m";
3844
+ console.log(`Pruned ${deadProjects.length} dead project(s):\n`);
3845
+ for (const p of deadProjects) {
3846
+ console.log(` ${p.name} ${DIM}${p.working_directory}${RESET}`);
3847
+ }
3848
+ console.log();
3849
+ console.log(`${allProjects.length - deadProjects.length} project(s) remain.`);
3850
+ }
3851
+ });
3852
+ return;
3853
+ }
3854
+ // Normal listing — filter dead projects by default
3855
+ const visibleProjects = opts.all
3856
+ ? allProjects
3857
+ : allProjects.filter((p) => !isDeadProjectDir(p.working_directory));
3858
+ if (visibleProjects.length === 0) {
3859
+ const deadCount = allProjects.length;
3860
+ outputResult(!!opts.json, {
3861
+ count: 0,
3862
+ totalRegistered: deadCount,
3863
+ deadCount,
3864
+ projects: [],
3865
+ }, () => {
3866
+ if (deadCount === 0) {
3867
+ console.log("No projects registered. Run 'gnosys init' in a project directory.");
3868
+ }
3869
+ else {
3870
+ console.log(`No live projects (${deadCount} dead — run 'gnosys projects --all' to see them or 'gnosys projects --prune' to remove them).`);
3871
+ }
3872
+ });
3703
3873
  centralDb.close();
3704
3874
  return;
3705
3875
  }
3706
- const projectData = projects.map((p) => ({
3876
+ const projectData = visibleProjects.map((p) => ({
3707
3877
  ...p,
3708
3878
  memoryCount: centralDb.getMemoriesByProject(p.id).length,
3709
3879
  }));
3710
- outputResult(!!opts.json, { count: projects.length, projects: projectData }, () => {
3711
- console.log(`${projects.length} registered project(s):\n`);
3880
+ const deadCount = allProjects.length - visibleProjects.length;
3881
+ outputResult(!!opts.json, {
3882
+ count: visibleProjects.length,
3883
+ totalRegistered: allProjects.length,
3884
+ deadCount,
3885
+ projects: projectData,
3886
+ }, () => {
3887
+ const header = deadCount > 0 && !opts.all
3888
+ ? `${visibleProjects.length} live project(s) (${deadCount} dead hidden — use --all or --prune):\n`
3889
+ : `${visibleProjects.length} registered project(s):\n`;
3890
+ console.log(header);
3712
3891
  for (const p of projectData) {
3713
3892
  console.log(` ${p.name}`);
3714
3893
  console.log(` ID: ${p.id}`);