gnosys 5.12.0 → 5.12.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 (72) hide show
  1. package/dist/cli.js +48 -7
  2. package/dist/index.js +179 -10
  3. package/dist/lib/addCommand.js +0 -1
  4. package/dist/lib/archive.js +0 -2
  5. package/dist/lib/askCommand.js +1 -1
  6. package/dist/lib/attachCommand.d.ts +17 -0
  7. package/dist/lib/attachCommand.js +66 -0
  8. package/dist/lib/attachments.d.ts +43 -2
  9. package/dist/lib/attachments.js +81 -2
  10. package/dist/lib/chat/choose.js +2 -2
  11. package/dist/lib/clientReadOverlay.js +3 -0
  12. package/dist/lib/config.d.ts +1 -48
  13. package/dist/lib/configCommand.js +2 -2
  14. package/dist/lib/db.d.ts +16 -1
  15. package/dist/lib/db.js +216 -119
  16. package/dist/lib/dbWrite.d.ts +1 -1
  17. package/dist/lib/dearchiveCommand.js +1 -1
  18. package/dist/lib/docxExtract.js +1 -1
  19. package/dist/lib/dream.d.ts +8 -0
  20. package/dist/lib/dream.js +35 -1
  21. package/dist/lib/dreamLogCommand.js +1 -1
  22. package/dist/lib/dreamRunLog.d.ts +1 -1
  23. package/dist/lib/dreamRunLog.js +26 -4
  24. package/dist/lib/embeddings.js +0 -3
  25. package/dist/lib/exportProject.d.ts +3 -2
  26. package/dist/lib/exportProject.js +2 -1
  27. package/dist/lib/federated.js +1 -1
  28. package/dist/lib/hybridSearchCommand.js +1 -1
  29. package/dist/lib/importProject.js +2 -1
  30. package/dist/lib/llm.js +1 -1
  31. package/dist/lib/lock.d.ts +1 -1
  32. package/dist/lib/lock.js +5 -3
  33. package/dist/lib/migrate.js +0 -1
  34. package/dist/lib/multimodalIngest.js +1 -1
  35. package/dist/lib/platform.d.ts +0 -6
  36. package/dist/lib/platform.js +0 -28
  37. package/dist/lib/readCommand.js +11 -10
  38. package/dist/lib/remoteWizard.d.ts +1 -1
  39. package/dist/lib/remoteWizard.js +4 -4
  40. package/dist/lib/rulesGen.d.ts +8 -0
  41. package/dist/lib/rulesGen.js +16 -0
  42. package/dist/lib/search.d.ts +0 -2
  43. package/dist/lib/search.js +0 -7
  44. package/dist/lib/semanticSearchCommand.js +1 -1
  45. package/dist/lib/setup/sections/providers.js +56 -4
  46. package/dist/lib/setup/sections/routing.js +42 -5
  47. package/dist/lib/setup/sections/taskRoutingEditor.d.ts +1 -5
  48. package/dist/lib/setup/sections/taskRoutingEditor.js +0 -10
  49. package/dist/lib/setup/ui/header.js +0 -1
  50. package/dist/lib/setup/ui/status.d.ts +0 -1
  51. package/dist/lib/setup/ui/status.js +0 -2
  52. package/dist/lib/setup.d.ts +0 -15
  53. package/dist/lib/setup.js +13 -158
  54. package/dist/lib/staleCommand.js +2 -2
  55. package/dist/lib/syncClient.d.ts +0 -6
  56. package/dist/lib/syncClient.js +36 -14
  57. package/dist/lib/syncDoctorCommand.js +2 -2
  58. package/dist/lib/syncIngest.d.ts +11 -0
  59. package/dist/lib/syncIngest.js +24 -1
  60. package/dist/lib/syncIngestStartup.js +2 -2
  61. package/dist/lib/syncSnapshot.d.ts +2 -0
  62. package/dist/lib/syncSnapshot.js +4 -0
  63. package/dist/lib/syncStaging.d.ts +0 -2
  64. package/dist/lib/syncStaging.js +0 -2
  65. package/dist/lib/updateCommand.js +1 -1
  66. package/dist/lib/webBuildCommand.js +1 -1
  67. package/dist/lib/webIndex.js +0 -1
  68. package/dist/lib/webIngestCommand.js +1 -1
  69. package/dist/sandbox/client.js +1 -1
  70. package/dist/sandbox/manager.js +1 -14
  71. package/dist/sandbox/server.js +3 -5
  72. package/package.json +5 -2
package/dist/cli.js CHANGED
@@ -9,16 +9,16 @@ import fs from "fs/promises";
9
9
  import os from "os";
10
10
  import { fileURLToPath } from "url";
11
11
  import dotenv from "dotenv";
12
- import { readFileSync, existsSync } from "fs";
12
+ import { readFileSync, existsSync, } from "fs";
13
13
  // v5.8.0 (#4): only the lightweight modules are imported at top-level.
14
14
  // Anything that pulls @huggingface/transformers, mammoth/pdf-parse/turndown,
15
15
  // large file-walking machinery, or otherwise costs >100ms to load gets
16
16
  // `await import(...)` inside its own action handler. This keeps
17
17
  // `gnosys --help` and other lightweight commands fast.
18
18
  import { GnosysResolver } from "./lib/resolver.js";
19
- import { loadConfig, generateConfigTemplate } from "./lib/config.js";
19
+ import { loadConfig, generateConfigTemplate, } from "./lib/config.js";
20
20
  import { GnosysDB } from "./lib/db.js";
21
- import { createProjectIdentity, findProjectIdentity } from "./lib/projectIdentity.js";
21
+ import { createProjectIdentity, findProjectIdentity, } from "./lib/projectIdentity.js";
22
22
  // Lazy-loaded inside action handlers (each ~200ms-2.5s on cold cache):
23
23
  // - ./lib/embeddings.js (@huggingface/transformers — 80MB)
24
24
  // - ./lib/hybridSearch.js (depends on embeddings)
@@ -468,9 +468,9 @@ setupCmd
468
468
  }
469
469
  });
470
470
  // v5.4.2 removal: `gnosys models` (top-level shortcut) was removed in favor
471
- // of the canonical `gnosys setup models` form. The implementation function
472
- // runModelsCommand() in setup.ts is no longer wired but kept for now in case
473
- // we need to revive a top-level shortcut later.
471
+ // of the canonical `gnosys setup models` form. The unwired implementation
472
+ // (runModelsCommand in setup.ts) was deleted in v5.12.1 after eight minor
473
+ // versions without revival recover from git history if ever needed.
474
474
  // ─── gnosys init ─────────────────────────────────────────────────────────
475
475
  program
476
476
  .command("init")
@@ -685,6 +685,24 @@ program
685
685
  const { runIngestCommand } = await import("./lib/ingestCommand.js");
686
686
  await runIngestCommand(getResolver, fileOrGlob, opts);
687
687
  });
688
+ // ─── gnosys attach <file> --memory <id> ──────────────────────────────────
689
+ program
690
+ .command("attach <file>")
691
+ .description("Attach a small binary file (logo, diagram, screenshot) inline to a memory. Travels machine-to-machine over normal sync. Limit ~10MB — use 'gnosys ingest' for large media.")
692
+ .requiredOption("--memory <id>", "Memory ID to attach the file to")
693
+ .action(async (file, opts) => {
694
+ const { runAttachCommand } = await import("./lib/attachCommand.js");
695
+ await runAttachCommand(file, opts);
696
+ });
697
+ // ─── gnosys get-attachment <id> ───────────────────────────────────────────
698
+ program
699
+ .command("get-attachment <memoryId>")
700
+ .description("Retrieve the binary attachment stored on a memory. Writes to --out, or prints base64 to stdout.")
701
+ .option("--out <path>", "Write the attachment to this file path instead of printing base64")
702
+ .action(async (memoryId, opts) => {
703
+ const { runGetAttachmentCommand } = await import("./lib/attachCommand.js");
704
+ await runGetAttachmentCommand(memoryId, opts);
705
+ });
688
706
  // ─── gnosys tags-add ────────────────────────────────────────────────────
689
707
  program
690
708
  .command("tags-add")
@@ -970,7 +988,22 @@ program
970
988
  .description("Remove dead and temp-dir entries from the project registry")
971
989
  .option("--yes", "Non-interactive, remove without prompting")
972
990
  .option("--dry-run", "Show what would be removed without writing")
991
+ .option("--rules [dir]", "Remove the GNOSYS block from agent rules files (CLAUDE.md, .cursor, .codex) in the given directory (default: cwd)")
973
992
  .action(async (opts) => {
993
+ // v5.12.1: uninstall counterpart of `gnosys setup ides` rules generation.
994
+ if (opts.rules !== undefined) {
995
+ const { removeRulesFromProject } = await import("./lib/rulesGen.js");
996
+ const dir = typeof opts.rules === "string" ? path.resolve(opts.rules) : process.cwd();
997
+ const cleaned = await removeRulesFromProject(dir);
998
+ if (cleaned.length === 0) {
999
+ console.log(`No GNOSYS rules blocks found in ${dir}`);
1000
+ }
1001
+ else {
1002
+ for (const rel of cleaned)
1003
+ console.log(`Removed GNOSYS block: ${rel}`);
1004
+ }
1005
+ return;
1006
+ }
974
1007
  const { cleanupRegistry } = await import("./lib/cleanup.js");
975
1008
  const result = await cleanupRegistry({
976
1009
  interactive: !opts.yes && !opts.dryRun,
@@ -1852,4 +1885,12 @@ if (!isTestEnv()) {
1852
1885
  // non-critical — don't block CLI startup
1853
1886
  }
1854
1887
  }
1855
- program.parse();
1888
+ // v5.12.x observability: all 100+ command actions are async — with bare
1889
+ // program.parse() a thrown action error surfaced as a raw Node
1890
+ // UnhandledPromiseRejection (full engine stack, no gnosys framing).
1891
+ // parseAsync routes every action failure through one clean exit path.
1892
+ program.parseAsync().catch(async (err) => {
1893
+ const { logError } = await import("./lib/log.js");
1894
+ logError(err, { module: "cli" });
1895
+ process.exitCode = 1;
1896
+ });
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  // MCP stdio JSON protocol. parse() is a pure function with no side effects.
11
11
  import dotenv from "dotenv";
12
12
  import path from "path";
13
+ import { AsyncLocalStorage } from "async_hooks";
13
14
  import { readFileSync, realpathSync } from "fs";
14
15
  import { fileURLToPath } from "url";
15
16
  const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
@@ -49,13 +50,14 @@ import { groupByPeriod, computeStats } from "./lib/timeline.js";
49
50
  import { buildLinkGraph, getBacklinks, getOutgoingLinks, formatGraphSummary } from "./lib/wikilinks.js";
50
51
  import { loadConfig, DEFAULT_CONFIG } from "./lib/config.js";
51
52
  import { getLLMProvider } from "./lib/llm.js";
52
- import { recall, formatRecall } from "./lib/recall.js";
53
+ import { recall, formatRecall, } from "./lib/recall.js";
53
54
  import { initAudit, readAuditLog, formatAuditTimeline } from "./lib/audit.js";
55
+ import { logError } from "./lib/log.js";
54
56
  import { GnosysDB } from "./lib/db.js";
55
57
  import { syncMemoryToDb, syncUpdateToDb, syncDearchiveToDb, syncReinforcementToDb, auditToDb } from "./lib/dbWrite.js";
56
- import { createProjectIdentity, readProjectIdentity } from "./lib/projectIdentity.js";
58
+ import { createProjectIdentity, readProjectIdentity, } from "./lib/projectIdentity.js";
57
59
  import { setPreference, getPreference, getAllPreferences, deletePreference, KNOWN_PREFERENCE_KEYS, suggestPreferenceKey } from "./lib/preferences.js";
58
- import { syncRules, generateRulesBlock } from "./lib/rulesGen.js";
60
+ import { syncRules, generateRulesBlock, } from "./lib/rulesGen.js";
59
61
  import { federatedSearch, detectAmbiguity, generateBriefing, generateAllBriefings, getWorkingSet, formatWorkingSet, detectCurrentProject } from "./lib/federated.js";
60
62
  import { generatePortfolio, formatPortfolioCompact, formatPortfolioMarkdown, generateStatusPrompt } from "./lib/portfolio.js";
61
63
  import { applyPendingOverlay, mergeOverlayDiscoverResults, mergeOverlaySearchResults, pendingAddToDbMemory, } from "./lib/clientReadOverlay.js";
@@ -71,9 +73,47 @@ const server = new McpServer({
71
73
  version: "2.0.0",
72
74
  });
73
75
  const _registrations = [];
76
+ // v5.12.1 reliability: every tool handler resolves a ToolContext whose
77
+ // clientRead (v13 sync) may own a DB handle. Historically only 8 of 52
78
+ // handlers released it, leaking handles on every early return. Enforce
79
+ // release centrally: resolveToolContext() registers each context in the
80
+ // per-call AsyncLocalStorage store, and withContextRelease() (wrapped around
81
+ // every tool handler at registration) releases them when the handler settles
82
+ // — every return path, every throw, every future tool, no per-handler code.
83
+ const activeToolContexts = new AsyncLocalStorage();
84
+ function withContextRelease(handler, toolName) {
85
+ return (...hargs) => {
86
+ const opened = [];
87
+ return activeToolContexts.run(opened, async () => {
88
+ try {
89
+ return await handler(...hargs);
90
+ }
91
+ catch (err) {
92
+ // v5.12.x observability: last-resort error envelope. 19 of 52 tools
93
+ // had no catch at all — a throw reached the SDK as a raw JSON-RPC
94
+ // error with no corruption-recovery guidance. Per-tool catches with
95
+ // more specific messages still take precedence; this only sees what
96
+ // they let through. Logged to stderr (stdout is JSON-RPC).
97
+ logError(err, { module: "mcp", op: toolName });
98
+ return {
99
+ content: [{ type: "text", text: formatMcpError(`in ${toolName}`, err) }],
100
+ isError: true,
101
+ };
102
+ }
103
+ finally {
104
+ for (const c of opened)
105
+ releaseClientReadFromContext(c);
106
+ }
107
+ });
108
+ };
109
+ }
74
110
  // Typed to the McpServer methods so call-site generic inference (Zod schema →
75
111
  // handler arg types) is preserved; the body just collects a replay thunk.
76
112
  const regTool = ((...args) => {
113
+ const last = args.length - 1;
114
+ if (typeof args[last] === "function") {
115
+ args[last] = withContextRelease(args[last], typeof args[0] === "string" ? args[0] : "tool");
116
+ }
77
117
  _registrations.push((s) => s.tool(...args));
78
118
  });
79
119
  const regPrompt = ((...args) => {
@@ -160,9 +200,25 @@ function applyClientReadToCentralDb(localDb) {
160
200
  const clientRead = openClientReadContext(localDb, masterPath, mc.machineId);
161
201
  return { centralDb: clientRead.db, clientRead };
162
202
  }
203
+ /** Idempotent: safe to call from both a handler's own finally and the central
204
+ * withContextRelease wrapper. */
163
205
  function releaseClientReadFromContext(ctx) {
164
- if (ctx.clientRead)
206
+ if (ctx.clientRead) {
165
207
  closeClientReadContext(ctx.clientRead);
208
+ ctx.clientRead = null;
209
+ }
210
+ // v5.12.x perf/leak: projectRoot-scoped contexts open their own
211
+ // GnosysSearch (SQLite handle to <store>/.config/search.db) per call.
212
+ if (ctx.ownsSearch && ctx.search) {
213
+ try {
214
+ ctx.search.close();
215
+ }
216
+ catch {
217
+ // already closed / never opened — fine
218
+ }
219
+ ctx.search = null;
220
+ ctx.ownsSearch = false;
221
+ }
166
222
  }
167
223
  async function resolveToolContext(projectRoot) {
168
224
  if (!projectRoot) {
@@ -176,7 +232,7 @@ async function resolveToolContext(projectRoot) {
176
232
  projectId = identity?.projectId || null;
177
233
  }
178
234
  const applied = applyClientReadToCentralDb(centralDb);
179
- return {
235
+ const ctx = {
180
236
  resolver,
181
237
  store: writeTarget?.store || null,
182
238
  storePath: writeTarget?.store.getStorePath() || "",
@@ -187,6 +243,8 @@ async function resolveToolContext(projectRoot) {
187
243
  projectId,
188
244
  clientRead: applied.clientRead,
189
245
  };
246
+ activeToolContexts.getStore()?.push(ctx);
247
+ return ctx;
190
248
  }
191
249
  // Scoped context — resolve for this specific project
192
250
  const scopedResolver = await GnosysResolver.resolveForProject(projectRoot);
@@ -215,7 +273,7 @@ async function resolveToolContext(projectRoot) {
215
273
  // gnosys.db in the project's .gnosys/ directory.
216
274
  }
217
275
  const applied = applyClientReadToCentralDb(centralDb);
218
- return {
276
+ const ctx = {
219
277
  resolver: scopedResolver,
220
278
  store: scopedWriteTarget?.store || null,
221
279
  storePath: scopedStorePath,
@@ -225,7 +283,10 @@ async function resolveToolContext(projectRoot) {
225
283
  centralDb: applied.centralDb,
226
284
  projectId,
227
285
  clientRead: applied.clientRead,
286
+ ownsSearch: scopedSearch !== null,
228
287
  };
288
+ activeToolContexts.getStore()?.push(ctx);
289
+ return ctx;
229
290
  }
230
291
  /**
231
292
  * v5.7.1 (#13): Resolve scope + projectId for a memory write.
@@ -1632,7 +1693,7 @@ regTool("gnosys_import", "Bulk import structured data (CSV, JSON, JSONL) into Gn
1632
1693
  const effectiveMode = mode || "structured";
1633
1694
  try {
1634
1695
  // v5.9.1 (#100): import.js pulls mammoth + pdf-parse + turndown.
1635
- const { performImport, formatImportSummary, estimateDuration } = await import("./lib/import.js");
1696
+ const { performImport, formatImportSummary } = await import("./lib/import.js");
1636
1697
  const result = await performImport(writeTarget.store, ingestion, {
1637
1698
  format: format,
1638
1699
  data,
@@ -1657,7 +1718,6 @@ regTool("gnosys_import", "Bulk import structured data (CSV, JSON, JSONL) into Gn
1657
1718
  // Smart threshold guidance
1658
1719
  if (effectiveMode === "llm" &&
1659
1720
  result.totalProcessed > 100) {
1660
- const estimate = estimateDuration(result.totalProcessed, "llm", concurrency || 5);
1661
1721
  response += `\n\n💡 Tip: For large LLM imports, the CLI offers progress tracking and resume:\n gnosys import ${data.length < 100 ? data : "<file>"} --format ${format} --mode llm --skip-existing`;
1662
1722
  }
1663
1723
  return { content: [{ type: "text", text: response }] };
@@ -1967,7 +2027,7 @@ regTool("gnosys_dream", "Run a Dream Mode cycle — idle-time consolidation that
1967
2027
  }, async (params) => {
1968
2028
  try {
1969
2029
  const ctx = await resolveToolContext(params.projectRoot);
1970
- if (!ctx.centralDb || !ctx.centralDb.isAvailable() || !ctx.centralDb.isMigrated()) {
2030
+ if (!ctx.centralDb?.isAvailable() || !ctx.centralDb.isMigrated()) {
1971
2031
  return {
1972
2032
  content: [
1973
2033
  {
@@ -2047,7 +2107,7 @@ regTool("gnosys_export", "Export gnosys.db to Obsidian-compatible vault — atom
2047
2107
  }, async (params) => {
2048
2108
  try {
2049
2109
  const ctx = await resolveToolContext(params.projectRoot);
2050
- if (!ctx.centralDb || !ctx.centralDb.isAvailable() || !ctx.centralDb.isMigrated()) {
2110
+ if (!ctx.centralDb?.isAvailable() || !ctx.centralDb.isMigrated()) {
2051
2111
  return {
2052
2112
  content: [
2053
2113
  {
@@ -2720,6 +2780,106 @@ regTool("gnosys_remote_resolve", "Resolve a sync conflict by choosing which vers
2720
2780
  localDb.close();
2721
2781
  }
2722
2782
  });
2783
+ // ─── Tool: gnosys_attach ────────────────────────────────────────────────
2784
+ regTool("gnosys_attach", "Attach a small binary file (logo, diagram, screenshot, small PDF) directly to a memory. The bytes are stored inline in the memory row, so the attachment travels machine-to-machine over the normal sync and works with a remote/dockerized server (no shared filesystem). Limit ~10MB — use gnosys_ingest_file for large media.", {
2785
+ memoryId: z.string().describe("Memory ID to attach the file to (e.g., 'deci-052')"),
2786
+ filePath: z.string().describe("Absolute path to the file to attach"),
2787
+ projectRoot: projectRootParam,
2788
+ }, async ({ memoryId, filePath, projectRoot }) => {
2789
+ const ctx = await resolveToolContext(projectRoot);
2790
+ if (!ctx.centralDb?.isAvailable()) {
2791
+ return {
2792
+ content: [{ type: "text", text: "Database not available. Cannot attach file." }],
2793
+ isError: true,
2794
+ };
2795
+ }
2796
+ try {
2797
+ const { attachFileToMemory } = await import("./lib/attachments.js");
2798
+ const result = await attachFileToMemory(ctx.centralDb, memoryId, filePath);
2799
+ auditToDb(ctx.centralDb, "write", memoryId, {
2800
+ tool: "gnosys_attach",
2801
+ name: result.name,
2802
+ mime: result.mime,
2803
+ sizeBytes: result.sizeBytes,
2804
+ unchanged: result.unchanged,
2805
+ });
2806
+ const sizeKb = (result.sizeBytes / 1024).toFixed(1);
2807
+ const verb = result.unchanged ? "already attached (no change)" : "attached";
2808
+ return {
2809
+ content: [
2810
+ {
2811
+ type: "text",
2812
+ text: `File ${verb}: ${result.name} (${result.mime}, ${sizeKb} KB)\nMemory: ${memoryId}`,
2813
+ },
2814
+ ],
2815
+ };
2816
+ }
2817
+ catch (err) {
2818
+ return {
2819
+ content: [{ type: "text", text: formatMcpError("attaching file", err) }],
2820
+ isError: true,
2821
+ };
2822
+ }
2823
+ });
2824
+ // ─── Tool: gnosys_get_attachment ────────────────────────────────────────
2825
+ regTool("gnosys_get_attachment", "Retrieve the binary attachment stored on a memory. By default returns the bytes (base64, plus an inline image when the attachment is an image). Pass outputPath to write the file to disk instead.", {
2826
+ memoryId: z.string().describe("Memory ID that holds the attachment"),
2827
+ outputPath: z.string().optional().describe("If provided, write the attachment to this absolute path instead of returning bytes"),
2828
+ projectRoot: projectRootParam,
2829
+ }, async ({ memoryId, outputPath, projectRoot }) => {
2830
+ const ctx = await resolveToolContext(projectRoot);
2831
+ if (!ctx.centralDb?.isAvailable()) {
2832
+ return {
2833
+ content: [{ type: "text", text: "Database not available." }],
2834
+ isError: true,
2835
+ };
2836
+ }
2837
+ try {
2838
+ const { getMemoryAttachment } = await import("./lib/attachments.js");
2839
+ const att = getMemoryAttachment(ctx.centralDb, memoryId);
2840
+ if (!att) {
2841
+ return {
2842
+ content: [{ type: "text", text: `No attachment found on memory: ${memoryId}` }],
2843
+ isError: true,
2844
+ };
2845
+ }
2846
+ if (outputPath) {
2847
+ const { writeFile } = await import("fs/promises");
2848
+ await writeFile(outputPath, att.data);
2849
+ return {
2850
+ content: [
2851
+ {
2852
+ type: "text",
2853
+ text: `Wrote ${att.name} (${att.mime}, ${att.data.length} bytes) to ${outputPath}`,
2854
+ },
2855
+ ],
2856
+ };
2857
+ }
2858
+ const base64 = att.data.toString("base64");
2859
+ if (att.mime.startsWith("image/")) {
2860
+ return {
2861
+ content: [
2862
+ { type: "image", data: base64, mimeType: att.mime },
2863
+ { type: "text", text: `${att.name} (${att.mime}, ${att.data.length} bytes)` },
2864
+ ],
2865
+ };
2866
+ }
2867
+ return {
2868
+ content: [
2869
+ {
2870
+ type: "text",
2871
+ text: `${att.name} (${att.mime}, ${att.data.length} bytes)\n\nbase64:\n${base64}`,
2872
+ },
2873
+ ],
2874
+ };
2875
+ }
2876
+ catch (err) {
2877
+ return {
2878
+ content: [{ type: "text", text: formatMcpError("reading attachment", err) }],
2879
+ isError: true,
2880
+ };
2881
+ }
2882
+ });
2723
2883
  // ─── Tool: gnosys_update_status ─────────────────────────────────────────
2724
2884
  regTool("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.", {
2725
2885
  projectRoot: z.string().optional().describe("Project root for auto-detection"),
@@ -3080,6 +3240,15 @@ async function initHeavyDeps() {
3080
3240
  // ─── Start the server ────────────────────────────────────────────────────
3081
3241
  /** Start the MCP server (stdio or http). Called by `gnosys serve` and when invoked as `gnosys-mcp`. */
3082
3242
  export async function startMcpServer() {
3243
+ // v5.12.1 reliability: an escaped async error must not kill the server.
3244
+ // Log to stderr (stdout is JSON-RPC in stdio mode) and keep serving —
3245
+ // all persistent state is transactional SQLite, so survivors are safe.
3246
+ process.on("unhandledRejection", (reason) => {
3247
+ console.error(`Gnosys MCP: unhandled rejection — ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`);
3248
+ });
3249
+ process.on("uncaughtException", (err) => {
3250
+ console.error(`Gnosys MCP: uncaught exception — ${err.stack || err.message}`);
3251
+ });
3083
3252
  // v5.7.1 (#15): start the upgrade-marker watcher BEFORE anything else.
3084
3253
  // If `gnosys upgrade` was run on this machine while the MCP was idle,
3085
3254
  // pick that up immediately instead of serving stale tool handlers.
@@ -54,7 +54,6 @@ export async function runAddCommand(getResolver, input, opts, resolveProjectId)
54
54
  centralDb = GnosysDB.openCentral();
55
55
  const projectId = await resolveProjectId();
56
56
  const id = centralDb.getNextId(result.category, projectId || undefined);
57
- const today = new Date().toISOString().split("T")[0];
58
57
  const now = new Date().toISOString();
59
58
  const content = `# ${result.title}\n\n${result.content}`;
60
59
  const tags = result.tags;
@@ -10,7 +10,6 @@
10
10
  * then dearchives used memories back to active
11
11
  */
12
12
  // Dynamic import — gracefully handles missing native module
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
13
  let Database = null;
15
14
  try {
16
15
  Database = (await import("better-sqlite3")).default;
@@ -26,7 +25,6 @@ import { enableWAL } from "./lock.js";
26
25
  import { auditLog } from "./audit.js";
27
26
  // ─── Archive Manager ────────────────────────────────────────────────────
28
27
  export class GnosysArchive {
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
28
  db = null;
31
29
  storePath;
32
30
  available = false;
@@ -90,7 +90,7 @@ export async function runAskCommand(getResolver, question, opts) {
90
90
  const useStream = opts.stream !== false && !opts.json;
91
91
  try {
92
92
  const result = await ask.ask(question, {
93
- limit: parseInt(opts.limit),
93
+ limit: parseInt(opts.limit, 10),
94
94
  mode,
95
95
  stream: useStream,
96
96
  additionalContext: federatedContext,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * CLI handlers for inline DB-blob attachments (v5.12).
3
+ *
4
+ * gnosys attach <file> --memory <id> store bytes inline on a memory
5
+ * gnosys get-attachment <id> [--out path] retrieve the stored bytes
6
+ *
7
+ * Inline attachments live in the memory row, so they ride the normal sync
8
+ * rail to other machines and a remote/dockerized server.
9
+ */
10
+ export interface AttachCommandOptions {
11
+ memory: string;
12
+ }
13
+ export declare function runAttachCommand(filePath: string, opts: AttachCommandOptions): Promise<void>;
14
+ export interface GetAttachmentCommandOptions {
15
+ out?: string;
16
+ }
17
+ export declare function runGetAttachmentCommand(memoryId: string, opts: GetAttachmentCommandOptions): Promise<void>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * CLI handlers for inline DB-blob attachments (v5.12).
3
+ *
4
+ * gnosys attach <file> --memory <id> store bytes inline on a memory
5
+ * gnosys get-attachment <id> [--out path] retrieve the stored bytes
6
+ *
7
+ * Inline attachments live in the memory row, so they ride the normal sync
8
+ * rail to other machines and a remote/dockerized server.
9
+ */
10
+ import { GnosysDB } from "./db.js";
11
+ export async function runAttachCommand(filePath, opts) {
12
+ const db = GnosysDB.openCentral();
13
+ try {
14
+ if (!db.isAvailable()) {
15
+ console.error("Database not available.");
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+ const { attachFileToMemory } = await import("./attachments.js");
20
+ const result = await attachFileToMemory(db, opts.memory, filePath);
21
+ const sizeKb = (result.sizeBytes / 1024).toFixed(1);
22
+ const verb = result.unchanged ? "Already attached (no change)" : "Attached";
23
+ console.log(`${verb}: ${result.name} (${result.mime}, ${sizeKb} KB) → ${opts.memory}`);
24
+ }
25
+ catch (err) {
26
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
27
+ process.exitCode = 1;
28
+ }
29
+ finally {
30
+ db.close();
31
+ }
32
+ }
33
+ export async function runGetAttachmentCommand(memoryId, opts) {
34
+ const db = GnosysDB.openCentral();
35
+ try {
36
+ if (!db.isAvailable()) {
37
+ console.error("Database not available.");
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ const { getMemoryAttachment } = await import("./attachments.js");
42
+ const att = getMemoryAttachment(db, memoryId);
43
+ if (!att) {
44
+ console.error(`No attachment found on memory: ${memoryId}`);
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ if (opts.out) {
49
+ const { writeFile } = await import("fs/promises");
50
+ await writeFile(opts.out, att.data);
51
+ console.log(`Wrote ${att.name} (${att.mime}, ${att.data.length} bytes) to ${opts.out}`);
52
+ }
53
+ else {
54
+ // No output path: print metadata + base64 so it can be piped/redirected.
55
+ console.error(`${att.name} (${att.mime}, ${att.data.length} bytes)`);
56
+ console.log(att.data.toString("base64"));
57
+ }
58
+ }
59
+ catch (err) {
60
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
61
+ process.exitCode = 1;
62
+ }
63
+ finally {
64
+ db.close();
65
+ }
66
+ }
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Gnosys Attachments — File attachment management for multimodal ingestion.
3
3
  *
4
- * Stores binary files in .gnosys/attachments/<uuid>.<ext> with a JSON manifest
5
- * at .gnosys/attachments/attachments.json for tracking metadata and memory links.
4
+ * Two storage modes:
5
+ * - Filesystem (legacy, large media): bytes copied to .gnosys/attachments/<uuid>.<ext>
6
+ * with a JSON manifest. Does NOT travel between machines.
7
+ * - Inline DB blob (v5.12, small assets): bytes stored in the memory row's
8
+ * attachment_data column. Travels over the same row-copy sync rail as
9
+ * embeddings, so it works single-machine, multi-machine, and with a future
10
+ * dockerized MCP without any shared volume.
6
11
  */
12
+ import type { GnosysDB } from "./db.js";
7
13
  export interface AttachmentRecord {
8
14
  uuid: string;
9
15
  originalName: string;
@@ -14,6 +20,8 @@ export interface AttachmentRecord {
14
20
  createdAt: string;
15
21
  memoryIds: string[];
16
22
  }
23
+ /** Infer a MIME type from a file path's extension. */
24
+ export declare function inferMimeType(filePath: string): string;
17
25
  /**
18
26
  * Initialize the attachments directory and manifest in a store.
19
27
  * Safe to call multiple times — creates only if missing.
@@ -40,3 +48,36 @@ export declare function getAttachmentPath(storePath: string, uuid: string, ext:
40
48
  * tracks which memories reference it.
41
49
  */
42
50
  export declare function linkMemoryToAttachment(storePath: string, uuid: string, memoryId: string): Promise<void>;
51
+ /**
52
+ * Maximum size for an inline DB-blob attachment. Larger files should use the
53
+ * filesystem path (`gnosys ingest`) so the synced central DB stays lean.
54
+ */
55
+ export declare const MAX_INLINE_ATTACHMENT_BYTES: number;
56
+ export interface InlineAttachment {
57
+ /** Raw file bytes. */
58
+ data: Buffer;
59
+ /** MIME type, e.g. "image/svg+xml". */
60
+ mime: string;
61
+ /** Original filename, e.g. "prospero-logo.svg". */
62
+ name: string;
63
+ /** Size in bytes. */
64
+ sizeBytes: number;
65
+ }
66
+ export interface AttachToMemoryResult {
67
+ memoryId: string;
68
+ name: string;
69
+ mime: string;
70
+ sizeBytes: number;
71
+ /** True when the file was identical to what was already attached (no write). */
72
+ unchanged: boolean;
73
+ }
74
+ /**
75
+ * Read a file and store its bytes inline on a memory row (attachment_data).
76
+ * Enforces the size cap and skips the write if the same bytes are already
77
+ * attached (content-hash dedup). Bumps `modified` so remote sync picks it up.
78
+ */
79
+ export declare function attachFileToMemory(db: GnosysDB, memoryId: string, filePath: string): Promise<AttachToMemoryResult>;
80
+ /** Return the inline attachment stored on a memory row, or null if none. */
81
+ export declare function getMemoryAttachment(db: GnosysDB, memoryId: string): InlineAttachment | null;
82
+ /** Remove an inline attachment from a memory row (keeps the memory itself). */
83
+ export declare function detachFromMemory(db: GnosysDB, memoryId: string): boolean;