gnosys 5.4.0 → 5.4.2

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 (45) hide show
  1. package/README.md +114 -8
  2. package/dist/cli.js +157 -63
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +202 -99
  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 +14 -0
  11. package/dist/lib/dashboard.d.ts.map +1 -1
  12. package/dist/lib/dashboard.js +200 -94
  13. package/dist/lib/dashboard.js.map +1 -1
  14. package/dist/lib/db.d.ts +87 -3
  15. package/dist/lib/db.d.ts.map +1 -1
  16. package/dist/lib/db.js +236 -17
  17. package/dist/lib/db.js.map +1 -1
  18. package/dist/lib/desktopNotify.d.ts +31 -0
  19. package/dist/lib/desktopNotify.d.ts.map +1 -0
  20. package/dist/lib/desktopNotify.js +80 -0
  21. package/dist/lib/desktopNotify.js.map +1 -0
  22. package/dist/lib/dream.d.ts +12 -0
  23. package/dist/lib/dream.d.ts.map +1 -1
  24. package/dist/lib/dream.js +82 -2
  25. package/dist/lib/dream.js.map +1 -1
  26. package/dist/lib/ingest.d.ts.map +1 -1
  27. package/dist/lib/ingest.js +24 -4
  28. package/dist/lib/ingest.js.map +1 -1
  29. package/dist/lib/lock.d.ts +5 -0
  30. package/dist/lib/lock.d.ts.map +1 -1
  31. package/dist/lib/lock.js +9 -0
  32. package/dist/lib/lock.js.map +1 -1
  33. package/dist/lib/remote.d.ts +15 -0
  34. package/dist/lib/remote.d.ts.map +1 -1
  35. package/dist/lib/remote.js +85 -0
  36. package/dist/lib/remote.js.map +1 -1
  37. package/dist/lib/setup.d.ts +15 -0
  38. package/dist/lib/setup.d.ts.map +1 -1
  39. package/dist/lib/setup.js +228 -0
  40. package/dist/lib/setup.js.map +1 -1
  41. package/dist/lib/store.d.ts +2 -0
  42. package/dist/lib/store.d.ts.map +1 -1
  43. package/dist/lib/store.js +4 -11
  44. package/dist/lib/store.js.map +1 -1
  45. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -62,6 +62,17 @@ const server = new McpServer({
62
62
  name: "gnosys",
63
63
  version: "2.0.0",
64
64
  });
65
+ /**
66
+ * v5.4.1: Format MCP errors. Detects DB corruption and replaces the raw
67
+ * "database disk image is malformed" with actionable recovery instructions.
68
+ * Use this in catch blocks instead of inlining error.message.
69
+ */
70
+ function formatMcpError(action, err) {
71
+ if (GnosysDB.isCorruptionError(err)) {
72
+ return `Error ${action}: ${err instanceof Error ? err.message : String(err)}\n\n${GnosysDB.corruptionRecoveryInstructions()}`;
73
+ }
74
+ return `Error ${action}: ${err instanceof Error ? err.message : String(err)}`;
75
+ }
65
76
  // These are initialized in main() after resolver runs
66
77
  let search = null;
67
78
  let tagRegistry = null;
@@ -480,12 +491,7 @@ server.tool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structu
480
491
  }
481
492
  catch (err) {
482
493
  return {
483
- content: [
484
- {
485
- type: "text",
486
- text: `Error adding memory: ${err instanceof Error ? err.message : String(err)}`,
487
- },
488
- ],
494
+ content: [{ type: "text", text: formatMcpError("adding memory", err) }],
489
495
  isError: true,
490
496
  };
491
497
  }
@@ -510,51 +516,59 @@ server.tool("gnosys_add_structured", "Add a memory with structured input (no LLM
510
516
  confidence: z.number().min(0).max(1).optional(),
511
517
  projectRoot: projectRootParam,
512
518
  }, async ({ title, category, tags, relevance, content, store: targetStore, author, authority, confidence, projectRoot }) => {
513
- const ctx = await resolveToolContext(projectRoot);
514
- const writeTarget = ctx.resolver.getWriteTarget(targetStore || undefined);
515
- if (!writeTarget) {
519
+ try {
520
+ const ctx = await resolveToolContext(projectRoot);
521
+ const writeTarget = ctx.resolver.getWriteTarget(targetStore || undefined);
522
+ if (!writeTarget) {
523
+ return {
524
+ content: [{ type: "text", text: "No writable store found." }],
525
+ isError: true,
526
+ };
527
+ }
528
+ if (!ctx.centralDb?.isAvailable()) {
529
+ return {
530
+ content: [{ type: "text", text: "Database not available. Cannot write memory." }],
531
+ isError: true,
532
+ };
533
+ }
534
+ const id = ctx.centralDb.getNextId(category, ctx.projectId ?? undefined);
535
+ const today = new Date().toISOString().split("T")[0];
536
+ const frontmatter = {
537
+ id,
538
+ title,
539
+ category,
540
+ tags: tags,
541
+ relevance: relevance || "",
542
+ author: author || "ai",
543
+ authority: authority || "observed",
544
+ confidence: confidence || 0.8,
545
+ created: today,
546
+ modified: today,
547
+ last_reviewed: today,
548
+ status: "active",
549
+ supersedes: null,
550
+ };
551
+ const fullContent = `# ${title}\n\n${content}`;
552
+ // Write to DB only (SQLite is sole source of truth)
553
+ syncMemoryToDb(ctx.centralDb, frontmatter, fullContent, undefined, ctx.projectId, "project");
554
+ auditToDb(ctx.centralDb, "write", id, { tool: "gnosys_add_structured", category });
555
+ if (ctx.search)
556
+ await reindexAllStores();
516
557
  return {
517
- content: [{ type: "text", text: "No writable store found." }],
518
- isError: true,
558
+ content: [
559
+ {
560
+ type: "text",
561
+ text: `Memory added to [${writeTarget.label}]: **${title}**\nID: ${id}`,
562
+ },
563
+ ],
519
564
  };
520
565
  }
521
- if (!ctx.centralDb?.isAvailable()) {
566
+ catch (err) {
522
567
  return {
523
- content: [{ type: "text", text: "Database not available. Cannot write memory." }],
568
+ content: [{ type: "text", text: formatMcpError("adding structured memory", err) }],
524
569
  isError: true,
525
570
  };
526
571
  }
527
- const id = ctx.centralDb.getNextId(category, ctx.projectId ?? undefined);
528
- const today = new Date().toISOString().split("T")[0];
529
- const frontmatter = {
530
- id,
531
- title,
532
- category,
533
- tags: tags,
534
- relevance: relevance || "",
535
- author: author || "ai",
536
- authority: authority || "observed",
537
- confidence: confidence || 0.8,
538
- created: today,
539
- modified: today,
540
- last_reviewed: today,
541
- status: "active",
542
- supersedes: null,
543
- };
544
- const fullContent = `# ${title}\n\n${content}`;
545
- // Write to DB only (SQLite is sole source of truth)
546
- syncMemoryToDb(ctx.centralDb, frontmatter, fullContent, undefined, ctx.projectId, "project");
547
- auditToDb(ctx.centralDb, "write", id, { tool: "gnosys_add_structured", category });
548
- if (ctx.search)
549
- await reindexAllStores();
550
- return {
551
- content: [
552
- {
553
- type: "text",
554
- text: `Memory added to [${writeTarget.label}]: **${title}**\nID: ${id}`,
555
- },
556
- ],
557
- };
558
572
  });
559
573
  // ─── Tool: gnosys_tags ───────────────────────────────────────────────────
560
574
  server.tool("gnosys_tags", "List all tags in the registry, grouped by category.", { projectRoot: projectRootParam }, async ({ projectRoot }) => {
@@ -2282,81 +2296,106 @@ server.tool("gnosys_portfolio", "Portfolio dashboard — shows all registered pr
2282
2296
  });
2283
2297
  // ─── Remote sync tools (v5.3.0) ─────────────────────────────────────────
2284
2298
  server.tool("gnosys_remote_status", "Check the status of remote sync (multi-machine). Returns pending pushes, pulls, conflicts, and reachability. Agents should surface this to the user when there are pending changes or conflicts.", {}, async () => {
2285
- if (!centralDb?.isAvailable()) {
2286
- return { content: [{ type: "text", text: "Central DB not available." }], isError: true };
2287
- }
2288
- const remotePath = centralDb.getMeta("remote_path");
2289
- if (!remotePath) {
2299
+ // Sync operations need explicit local DB access (not auto-routed remote).
2300
+ const localDb = GnosysDB.openLocal();
2301
+ try {
2302
+ if (!localDb.isAvailable()) {
2303
+ return { content: [{ type: "text", text: "Local DB not available." }], isError: true };
2304
+ }
2305
+ const remotePath = localDb.getMeta("remote_path");
2306
+ if (!remotePath) {
2307
+ return {
2308
+ content: [{
2309
+ type: "text",
2310
+ text: JSON.stringify({ configured: false, message: "Remote sync not configured." }, null, 2),
2311
+ }],
2312
+ };
2313
+ }
2314
+ const { RemoteSync } = await import("./lib/remote.js");
2315
+ const sync = new RemoteSync(localDb, remotePath);
2316
+ const status = await sync.getStatus();
2317
+ sync.closeRemote();
2290
2318
  return {
2291
- content: [{
2292
- type: "text",
2293
- text: JSON.stringify({ configured: false, message: "Remote sync not configured." }, null, 2),
2294
- }],
2319
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
2295
2320
  };
2296
2321
  }
2297
- const { RemoteSync } = await import("./lib/remote.js");
2298
- const sync = new RemoteSync(centralDb, remotePath);
2299
- const status = await sync.getStatus();
2300
- sync.closeRemote();
2301
- return {
2302
- content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
2303
- };
2322
+ finally {
2323
+ localDb.close();
2324
+ }
2304
2325
  });
2305
2326
  server.tool("gnosys_remote_push", "Push local memory changes to the remote (NAS) database. Uses skip-and-flag for conflicts by default. Call this when the user has approved pushing local changes.", {
2306
2327
  newerWins: z.boolean().optional().describe("Auto-resolve conflicts by taking the newer version"),
2307
2328
  }, async ({ newerWins }) => {
2308
- if (!centralDb?.isAvailable()) {
2309
- return { content: [{ type: "text", text: "Central DB not available." }], isError: true };
2329
+ const localDb = GnosysDB.openLocal();
2330
+ try {
2331
+ if (!localDb.isAvailable()) {
2332
+ return { content: [{ type: "text", text: "Local DB not available." }], isError: true };
2333
+ }
2334
+ const remotePath = localDb.getMeta("remote_path");
2335
+ if (!remotePath) {
2336
+ return { content: [{ type: "text", text: "Remote not configured. Run 'gnosys remote configure'." }], isError: true };
2337
+ }
2338
+ const { RemoteSync } = await import("./lib/remote.js");
2339
+ const sync = new RemoteSync(localDb, remotePath);
2340
+ const result = await sync.push({ strategy: newerWins ? "newer-wins" : "skip-and-flag" });
2341
+ sync.closeRemote();
2342
+ return {
2343
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2344
+ };
2310
2345
  }
2311
- const remotePath = centralDb.getMeta("remote_path");
2312
- if (!remotePath) {
2313
- return { content: [{ type: "text", text: "Remote not configured. Run 'gnosys remote configure'." }], isError: true };
2346
+ finally {
2347
+ localDb.close();
2314
2348
  }
2315
- const { RemoteSync } = await import("./lib/remote.js");
2316
- const sync = new RemoteSync(centralDb, remotePath);
2317
- const result = await sync.push({ strategy: newerWins ? "newer-wins" : "skip-and-flag" });
2318
- sync.closeRemote();
2319
- return {
2320
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2321
- };
2322
2349
  });
2323
2350
  server.tool("gnosys_remote_pull", "Pull remote memory changes to the local database. Uses skip-and-flag for conflicts by default. Call this when the user wants the latest from the remote.", {
2324
2351
  newerWins: z.boolean().optional().describe("Auto-resolve conflicts by taking the newer version"),
2325
2352
  }, async ({ newerWins }) => {
2326
- if (!centralDb?.isAvailable()) {
2327
- return { content: [{ type: "text", text: "Central DB not available." }], isError: true };
2353
+ const localDb = GnosysDB.openLocal();
2354
+ try {
2355
+ if (!localDb.isAvailable()) {
2356
+ return { content: [{ type: "text", text: "Local DB not available." }], isError: true };
2357
+ }
2358
+ const remotePath = localDb.getMeta("remote_path");
2359
+ if (!remotePath) {
2360
+ return { content: [{ type: "text", text: "Remote not configured." }], isError: true };
2361
+ }
2362
+ const { RemoteSync } = await import("./lib/remote.js");
2363
+ const sync = new RemoteSync(localDb, remotePath);
2364
+ const result = await sync.pull({ strategy: newerWins ? "newer-wins" : "skip-and-flag" });
2365
+ sync.closeRemote();
2366
+ return {
2367
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2368
+ };
2328
2369
  }
2329
- const remotePath = centralDb.getMeta("remote_path");
2330
- if (!remotePath) {
2331
- return { content: [{ type: "text", text: "Remote not configured." }], isError: true };
2370
+ finally {
2371
+ localDb.close();
2332
2372
  }
2333
- const { RemoteSync } = await import("./lib/remote.js");
2334
- const sync = new RemoteSync(centralDb, remotePath);
2335
- const result = await sync.pull({ strategy: newerWins ? "newer-wins" : "skip-and-flag" });
2336
- sync.closeRemote();
2337
- return {
2338
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2339
- };
2340
2373
  });
2341
2374
  server.tool("gnosys_remote_resolve", "Resolve a sync conflict by choosing which version to keep. Use after gnosys_remote_status reveals conflicts. The agent should present the local and remote versions to the user and call this with their choice.", {
2342
2375
  memoryId: z.string().describe("Memory ID with the conflict"),
2343
2376
  choice: z.enum(["local", "remote"]).describe("Which version to keep"),
2344
2377
  }, async ({ memoryId, choice }) => {
2345
- if (!centralDb?.isAvailable()) {
2346
- return { content: [{ type: "text", text: "Central DB not available." }], isError: true };
2347
- }
2348
- const remotePath = centralDb.getMeta("remote_path");
2349
- if (!remotePath) {
2350
- return { content: [{ type: "text", text: "Remote not configured." }], isError: true };
2378
+ const localDb = GnosysDB.openLocal();
2379
+ try {
2380
+ if (!localDb.isAvailable()) {
2381
+ return { content: [{ type: "text", text: "Local DB not available." }], isError: true };
2382
+ }
2383
+ const remotePath = localDb.getMeta("remote_path");
2384
+ if (!remotePath) {
2385
+ return { content: [{ type: "text", text: "Remote not configured." }], isError: true };
2386
+ }
2387
+ const { RemoteSync } = await import("./lib/remote.js");
2388
+ const sync = new RemoteSync(localDb, remotePath);
2389
+ const result = await sync.resolve(memoryId, choice);
2390
+ sync.closeRemote();
2391
+ if (result.ok) {
2392
+ return { content: [{ type: "text", text: `Resolved ${memoryId}: kept ${choice} version.` }] };
2393
+ }
2394
+ return { content: [{ type: "text", text: `Failed to resolve: ${result.error}` }], isError: true };
2351
2395
  }
2352
- const { RemoteSync } = await import("./lib/remote.js");
2353
- const sync = new RemoteSync(centralDb, remotePath);
2354
- const result = await sync.resolve(memoryId, choice);
2355
- sync.closeRemote();
2356
- if (result.ok) {
2357
- return { content: [{ type: "text", text: `Resolved ${memoryId}: kept ${choice} version.` }] };
2396
+ finally {
2397
+ localDb.close();
2358
2398
  }
2359
- return { content: [{ type: "text", text: `Failed to resolve: ${result.error}` }], isError: true };
2360
2399
  });
2361
2400
  // ─── Tool: gnosys_update_status ─────────────────────────────────────────
2362
2401
  server.tool("gnosys_update_status", "Get the prompt/template for writing a dashboard-compatible status memory for this project. Returns instructions for creating a landscape memory with the correct heading format so the portfolio dashboard can parse it. Run this, then follow the instructions to analyze and write the status.", {
@@ -2659,14 +2698,78 @@ async function main() {
2659
2698
  console.error(`Hybrid search: ${embCount > 0 ? `ready (${embCount} embeddings)` : "available (run gnosys_reindex to build embeddings)"}`);
2660
2699
  console.error(`Ask engine: ${askEngine.isLLMAvailable ? `ready (${askEngine.providerName}/${askEngine.modelName})` : "disabled (configure LLM provider)"}`);
2661
2700
  // v2.0: Initialize Dream Mode (idle-time consolidation)
2701
+ // v5.4.2: Designation gate inside DreamScheduler.start() — only the
2702
+ // machine designated via `gnosys setup dream` arms the timer. Other
2703
+ // machines no-op silently. Layer 3 startup probe surfaces a stderr
2704
+ // warning if this machine is designated but the dream provider is
2705
+ // unreachable.
2662
2706
  if (gnosysDb && config.dream?.enabled) {
2663
2707
  const dreamEngine = new GnosysDreamEngine(gnosysDb, config, config.dream);
2664
2708
  dreamScheduler = new DreamScheduler(dreamEngine, config.dream);
2709
+ // Layer 3: probe the dream provider if this machine is the dream node.
2710
+ // Done before scheduler.start() so users see the warning immediately
2711
+ // alongside other startup output.
2712
+ try {
2713
+ const designated = gnosysDb.getDreamMachineId();
2714
+ const localId = gnosysDb.getMeta("machine_id");
2715
+ if (designated && designated === localId) {
2716
+ // Quick reachability probe — 5s timeout to avoid blocking startup.
2717
+ const dreamProvider = config.dream.provider || "ollama";
2718
+ const dreamModel = config.dream.model || "(default)";
2719
+ const { validateModel } = await import("./lib/modelValidation.js");
2720
+ // Resolve API key from env or keychain (mirroring resolveApiKey precedence).
2721
+ const envVarName = `GNOSYS_${dreamProvider.toUpperCase()}_KEY`;
2722
+ let apiKey = process.env[envVarName] || "";
2723
+ if (!apiKey && process.platform === "darwin" && dreamProvider !== "ollama" && dreamProvider !== "lmstudio") {
2724
+ try {
2725
+ const { execSync } = await import("child_process");
2726
+ apiKey = execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w 2>/dev/null`, {
2727
+ stdio: "pipe", encoding: "utf-8", timeout: 2000,
2728
+ }).trim();
2729
+ }
2730
+ catch {
2731
+ // No key in keychain
2732
+ }
2733
+ }
2734
+ // Skip the network probe if there's clearly no key configured for a
2735
+ // remote provider — surface the obvious config gap instead.
2736
+ if (!apiKey && dreamProvider !== "ollama" && dreamProvider !== "lmstudio") {
2737
+ process.stderr.write(`gnosys: dream provider '${dreamProvider}/${dreamModel}' has no API key configured.\n` +
2738
+ ` This machine is designated to dream, but the LLM cannot be called.\n` +
2739
+ ` Run 'gnosys setup dream' to reconfigure.\n`);
2740
+ }
2741
+ else {
2742
+ const result = await Promise.race([
2743
+ validateModel(dreamProvider, dreamModel, apiKey),
2744
+ new Promise((resolve) => setTimeout(() => resolve({ ok: false, error: "probe timeout (5s)" }), 5000)),
2745
+ ]);
2746
+ if (!result.ok) {
2747
+ process.stderr.write(`gnosys: dream provider '${dreamProvider}/${dreamModel}' is unreachable at startup.\n` +
2748
+ ` This machine is designated to dream, but the LLM cannot be called.\n` +
2749
+ ` Error: ${("error" in result && result.error) || "unknown"}\n` +
2750
+ ` Run 'gnosys setup dream' to reconfigure.\n`);
2751
+ }
2752
+ }
2753
+ }
2754
+ }
2755
+ catch {
2756
+ // Probe failed — non-fatal. Continue with scheduler start.
2757
+ }
2665
2758
  dreamScheduler.start();
2666
- console.error(`Dream Mode: enabled (idle ${config.dream.idleMinutes}min, max ${config.dream.maxRuntimeMinutes}min)`);
2759
+ const designated = gnosysDb.getDreamMachineId();
2760
+ const localId = gnosysDb.getMeta("machine_id");
2761
+ if (!designated) {
2762
+ console.error(`Dream Mode: enabled but no machine designated. Run 'gnosys setup dream' on the machine you want to host dreams.`);
2763
+ }
2764
+ else if (designated !== localId) {
2765
+ console.error(`Dream Mode: enabled — designated to '${designated}'. This machine (${localId || "?"}) will not dream.`);
2766
+ }
2767
+ else {
2768
+ console.error(`Dream Mode: enabled on this machine (idle ${config.dream.idleMinutes}min, max ${config.dream.maxRuntimeMinutes}min)`);
2769
+ }
2667
2770
  }
2668
2771
  else {
2669
- console.error(`Dream Mode: disabled (enable in gnosys.json: dream.enabled = true)`);
2772
+ console.error(`Dream Mode: disabled (run 'gnosys setup dream' to configure)`);
2670
2773
  }
2671
2774
  }
2672
2775
  const transport = new StdioServerTransport();