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.
- package/dist/cli.js +48 -7
- package/dist/index.js +179 -10
- package/dist/lib/addCommand.js +0 -1
- package/dist/lib/archive.js +0 -2
- package/dist/lib/askCommand.js +1 -1
- package/dist/lib/attachCommand.d.ts +17 -0
- package/dist/lib/attachCommand.js +66 -0
- package/dist/lib/attachments.d.ts +43 -2
- package/dist/lib/attachments.js +81 -2
- package/dist/lib/chat/choose.js +2 -2
- package/dist/lib/clientReadOverlay.js +3 -0
- package/dist/lib/config.d.ts +1 -48
- package/dist/lib/configCommand.js +2 -2
- package/dist/lib/db.d.ts +16 -1
- package/dist/lib/db.js +216 -119
- package/dist/lib/dbWrite.d.ts +1 -1
- package/dist/lib/dearchiveCommand.js +1 -1
- package/dist/lib/docxExtract.js +1 -1
- package/dist/lib/dream.d.ts +8 -0
- package/dist/lib/dream.js +35 -1
- package/dist/lib/dreamLogCommand.js +1 -1
- package/dist/lib/dreamRunLog.d.ts +1 -1
- package/dist/lib/dreamRunLog.js +26 -4
- package/dist/lib/embeddings.js +0 -3
- package/dist/lib/exportProject.d.ts +3 -2
- package/dist/lib/exportProject.js +2 -1
- package/dist/lib/federated.js +1 -1
- package/dist/lib/hybridSearchCommand.js +1 -1
- package/dist/lib/importProject.js +2 -1
- package/dist/lib/llm.js +1 -1
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/lock.js +5 -3
- package/dist/lib/migrate.js +0 -1
- package/dist/lib/multimodalIngest.js +1 -1
- package/dist/lib/platform.d.ts +0 -6
- package/dist/lib/platform.js +0 -28
- package/dist/lib/readCommand.js +11 -10
- package/dist/lib/remoteWizard.d.ts +1 -1
- package/dist/lib/remoteWizard.js +4 -4
- package/dist/lib/rulesGen.d.ts +8 -0
- package/dist/lib/rulesGen.js +16 -0
- package/dist/lib/search.d.ts +0 -2
- package/dist/lib/search.js +0 -7
- package/dist/lib/semanticSearchCommand.js +1 -1
- package/dist/lib/setup/sections/providers.js +56 -4
- package/dist/lib/setup/sections/routing.js +42 -5
- package/dist/lib/setup/sections/taskRoutingEditor.d.ts +1 -5
- package/dist/lib/setup/sections/taskRoutingEditor.js +0 -10
- package/dist/lib/setup/ui/header.js +0 -1
- package/dist/lib/setup/ui/status.d.ts +0 -1
- package/dist/lib/setup/ui/status.js +0 -2
- package/dist/lib/setup.d.ts +0 -15
- package/dist/lib/setup.js +13 -158
- package/dist/lib/staleCommand.js +2 -2
- package/dist/lib/syncClient.d.ts +0 -6
- package/dist/lib/syncClient.js +36 -14
- package/dist/lib/syncDoctorCommand.js +2 -2
- package/dist/lib/syncIngest.d.ts +11 -0
- package/dist/lib/syncIngest.js +24 -1
- package/dist/lib/syncIngestStartup.js +2 -2
- package/dist/lib/syncSnapshot.d.ts +2 -0
- package/dist/lib/syncSnapshot.js +4 -0
- package/dist/lib/syncStaging.d.ts +0 -2
- package/dist/lib/syncStaging.js +0 -2
- package/dist/lib/updateCommand.js +1 -1
- package/dist/lib/webBuildCommand.js +1 -1
- package/dist/lib/webIndex.js +0 -1
- package/dist/lib/webIngestCommand.js +1 -1
- package/dist/sandbox/client.js +1 -1
- package/dist/sandbox/manager.js +1 -14
- package/dist/sandbox/server.js +3 -5
- 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
|
|
472
|
-
// runModelsCommand
|
|
473
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/dist/lib/addCommand.js
CHANGED
|
@@ -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;
|
package/dist/lib/archive.js
CHANGED
|
@@ -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;
|
package/dist/lib/askCommand.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
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;
|