gnosys 5.7.0 → 5.8.0

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 (67) hide show
  1. package/README.md +29 -2
  2. package/dist/cli.js +249 -117
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +167 -25
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/ask.d.ts.map +1 -1
  7. package/dist/lib/ask.js +20 -4
  8. package/dist/lib/ask.js.map +1 -1
  9. package/dist/lib/chat/SlashPalette.d.ts +34 -0
  10. package/dist/lib/chat/SlashPalette.d.ts.map +1 -0
  11. package/dist/lib/chat/SlashPalette.js +49 -0
  12. package/dist/lib/chat/SlashPalette.js.map +1 -0
  13. package/dist/lib/chat/index.d.ts.map +1 -1
  14. package/dist/lib/chat/index.js +6 -4
  15. package/dist/lib/chat/index.js.map +1 -1
  16. package/dist/lib/chat/llmTurn.d.ts.map +1 -1
  17. package/dist/lib/chat/llmTurn.js +4 -1
  18. package/dist/lib/chat/llmTurn.js.map +1 -1
  19. package/dist/lib/chat/render.d.ts.map +1 -1
  20. package/dist/lib/chat/render.js +91 -10
  21. package/dist/lib/chat/render.js.map +1 -1
  22. package/dist/lib/config.d.ts +25 -1
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +30 -0
  25. package/dist/lib/config.js.map +1 -1
  26. package/dist/lib/db.d.ts +21 -0
  27. package/dist/lib/db.d.ts.map +1 -1
  28. package/dist/lib/db.js +44 -17
  29. package/dist/lib/db.js.map +1 -1
  30. package/dist/lib/heartbeat.d.ts +31 -0
  31. package/dist/lib/heartbeat.d.ts.map +1 -0
  32. package/dist/lib/heartbeat.js +91 -0
  33. package/dist/lib/heartbeat.js.map +1 -0
  34. package/dist/lib/idFormat.d.ts +41 -0
  35. package/dist/lib/idFormat.d.ts.map +1 -0
  36. package/dist/lib/idFormat.js +66 -0
  37. package/dist/lib/idFormat.js.map +1 -0
  38. package/dist/lib/import.d.ts.map +1 -1
  39. package/dist/lib/import.js +2 -1
  40. package/dist/lib/import.js.map +1 -1
  41. package/dist/lib/ingest.d.ts +7 -1
  42. package/dist/lib/ingest.d.ts.map +1 -1
  43. package/dist/lib/ingest.js +23 -4
  44. package/dist/lib/ingest.js.map +1 -1
  45. package/dist/lib/llm.d.ts +1 -1
  46. package/dist/lib/llm.d.ts.map +1 -1
  47. package/dist/lib/llm.js.map +1 -1
  48. package/dist/lib/progress.d.ts +54 -0
  49. package/dist/lib/progress.d.ts.map +1 -0
  50. package/dist/lib/progress.js +92 -0
  51. package/dist/lib/progress.js.map +1 -0
  52. package/dist/lib/remote.d.ts +14 -1
  53. package/dist/lib/remote.d.ts.map +1 -1
  54. package/dist/lib/remote.js +75 -28
  55. package/dist/lib/remote.js.map +1 -1
  56. package/dist/lib/setup/sections/routing.d.ts.map +1 -1
  57. package/dist/lib/setup/sections/routing.js +4 -2
  58. package/dist/lib/setup/sections/routing.js.map +1 -1
  59. package/dist/lib/setup.d.ts +5 -0
  60. package/dist/lib/setup.d.ts.map +1 -1
  61. package/dist/lib/setup.js +127 -0
  62. package/dist/lib/setup.js.map +1 -1
  63. package/dist/lib/upgrade.d.ts +38 -0
  64. package/dist/lib/upgrade.d.ts.map +1 -0
  65. package/dist/lib/upgrade.js +61 -0
  66. package/dist/lib/upgrade.js.map +1 -0
  67. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  import dotenv from "dotenv";
12
12
  import path from "path";
13
13
  import { readFileSync } from "fs";
14
+ import { fileURLToPath } from "url";
14
15
  const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
15
16
  try {
16
17
  const envFile = readFileSync(path.join(home, ".config", "gnosys", ".env"), "utf8");
@@ -23,6 +24,19 @@ try {
23
24
  catch {
24
25
  // .env file not found — that's fine, env vars may be set elsewhere
25
26
  }
27
+ // v5.7.1 (#15): read our running version so the upgrade-marker watcher
28
+ // can detect when a newer global install lands and exit for client respawn.
29
+ const __filenameMcp = fileURLToPath(import.meta.url);
30
+ const __dirnameMcp = path.dirname(__filenameMcp);
31
+ const RUNNING_VERSION = (() => {
32
+ try {
33
+ const raw = readFileSync(path.resolve(__dirnameMcp, "..", "package.json"), "utf8");
34
+ return JSON.parse(raw).version || "0.0.0";
35
+ }
36
+ catch {
37
+ return "0.0.0";
38
+ }
39
+ })();
26
40
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
41
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
28
42
  import { z } from "zod";
@@ -51,7 +65,7 @@ import { GnosysDreamEngine, DreamScheduler, formatDreamReport } from "./lib/drea
51
65
  import { GnosysExporter, formatExportReport } from "./lib/export.js";
52
66
  import { createProjectIdentity, readProjectIdentity } from "./lib/projectIdentity.js";
53
67
  import { setPreference, getPreference, getAllPreferences, deletePreference } from "./lib/preferences.js";
54
- import { syncRules } from "./lib/rulesGen.js";
68
+ import { syncRules, generateRulesBlock } from "./lib/rulesGen.js";
55
69
  import { federatedSearch, detectAmbiguity, generateBriefing, generateAllBriefings, getWorkingSet, formatWorkingSet, detectCurrentProject } from "./lib/federated.js";
56
70
  import { generatePortfolio, formatPortfolioCompact, formatPortfolioMarkdown, generateStatusPrompt } from "./lib/portfolio.js";
57
71
  // Initialize resolver (discovers all layered stores)
@@ -73,6 +87,38 @@ function formatMcpError(action, err) {
73
87
  }
74
88
  return `Error ${action}: ${err instanceof Error ? err.message : String(err)}`;
75
89
  }
90
+ /**
91
+ * v5.7.1 (#15): poll `~/.gnosys/last-upgrade-at` and exit when a newer
92
+ * version has been installed. The MCP client (Claude Code, Cursor, etc.)
93
+ * sees the clean exit and respawns the process, which then runs the new
94
+ * global binary.
95
+ *
96
+ * The 10s cadence is a deliberate trade-off: latency is small, the FS
97
+ * check is one stat (~µs), and an upgrade that landed mid-session gets
98
+ * picked up before the next agent turn in almost every case.
99
+ */
100
+ function startUpgradeMarkerWatcher() {
101
+ const check = async () => {
102
+ try {
103
+ const { shouldRestartMcp, readUpgradeMarker } = await import("./lib/upgrade.js");
104
+ if (shouldRestartMcp(RUNNING_VERSION)) {
105
+ const marker = readUpgradeMarker();
106
+ const newVersion = marker?.version || "newer";
107
+ console.error(`gnosys MCP: upgrading from v${RUNNING_VERSION} → v${newVersion} — restarting`);
108
+ // Clean exit so the MCP host respawns us against the upgraded binary.
109
+ process.exit(0);
110
+ }
111
+ }
112
+ catch {
113
+ // Stat failed — non-critical. Try again on the next tick.
114
+ }
115
+ };
116
+ // Immediate boot-time check, then poll.
117
+ void check();
118
+ const timer = setInterval(() => void check(), 10_000);
119
+ // Don't block process exit on this timer.
120
+ timer.unref();
121
+ }
76
122
  // These are initialized in main() after resolver runs
77
123
  let search = null;
78
124
  let tagRegistry = null;
@@ -150,6 +196,38 @@ async function resolveToolContext(projectRoot) {
150
196
  projectId,
151
197
  };
152
198
  }
199
+ /**
200
+ * v5.7.1 (#13): Resolve scope + projectId for a memory write.
201
+ *
202
+ * Previously every write was hard-coded to scope="project" with whatever
203
+ * `ctx.projectId` happened to resolve to — including null. That produced
204
+ * "project-scope, no project" orphans visible cross-project.
205
+ *
206
+ * Now: derive scope from the explicit `store` argument and refuse to
207
+ * create a project-scoped memory when no project identity is reachable,
208
+ * telling the caller exactly how to fix it.
209
+ */
210
+ function resolveWriteScope(ctx, targetStore) {
211
+ const scope = targetStore || "project";
212
+ if (scope === "global" || scope === "personal") {
213
+ return { ok: true, scope, projectId: null };
214
+ }
215
+ if (!ctx.projectId) {
216
+ return {
217
+ ok: false,
218
+ error: [
219
+ "Cannot write a project-scoped memory: no project identity is reachable.",
220
+ "",
221
+ "Pick one:",
222
+ " • Pass projectRoot=<path-to-project> to anchor this memory to that project",
223
+ " • Pass store='global' to write to shared org knowledge",
224
+ " • Pass store='personal' for cross-project personal notes",
225
+ " • Run gnosys_init in the target directory to register it as a project",
226
+ ].join("\n"),
227
+ };
228
+ }
229
+ return { ok: true, scope: "project", projectId: ctx.projectId };
230
+ }
153
231
  // ─── Tool: gnosys_discover ──────────────────────────────────────────────
154
232
  server.tool("gnosys_discover", "Discover relevant memories by describing what you're working on. Searches relevance keyword clouds across all stores. Returns lightweight metadata (title, path, relevance keywords) — NO file contents. Use gnosys_read to load specific memories you need. Call this FIRST when starting a task to find what Gnosys knows.", {
155
233
  query: z
@@ -435,14 +513,21 @@ server.tool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structu
435
513
  };
436
514
  }
437
515
  try {
438
- const result = await ingestion.ingest(input);
516
+ // v5.8.0 (#8): pass per-call config so the LLM provider resolves against
517
+ // the merged project+global config, not the MCP's boot-time config.
518
+ const result = await ingestion.ingest(input, ctx.config);
439
519
  if (!ctx.centralDb?.isAvailable()) {
440
520
  return {
441
521
  content: [{ type: "text", text: "Database not available. Cannot write memory." }],
442
522
  isError: true,
443
523
  };
444
524
  }
445
- const id = ctx.centralDb.getNextId(result.category, ctx.projectId ?? undefined);
525
+ // v5.7.1 (#13): determine scope/projectId before writing
526
+ const scopeResult = resolveWriteScope(ctx, targetStore);
527
+ if (!scopeResult.ok) {
528
+ return { content: [{ type: "text", text: scopeResult.error }], isError: true };
529
+ }
530
+ const id = ctx.centralDb.getNextId(result.category, scopeResult.projectId ?? undefined);
446
531
  const today = new Date().toISOString().split("T")[0];
447
532
  const frontmatter = {
448
533
  id,
@@ -461,7 +546,7 @@ server.tool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structu
461
546
  };
462
547
  const content = `# ${result.title}\n\n${result.content}`;
463
548
  // Write to DB only (SQLite is sole source of truth)
464
- syncMemoryToDb(ctx.centralDb, frontmatter, content, undefined, ctx.projectId, "project");
549
+ syncMemoryToDb(ctx.centralDb, frontmatter, content, undefined, scopeResult.projectId, scopeResult.scope);
465
550
  auditToDb(ctx.centralDb, "write", id, { tool: "gnosys_add", category: result.category });
466
551
  // Rebuild search index across all stores
467
552
  if (ctx.search) {
@@ -531,7 +616,12 @@ server.tool("gnosys_add_structured", "Add a memory with structured input (no LLM
531
616
  isError: true,
532
617
  };
533
618
  }
534
- const id = ctx.centralDb.getNextId(category, ctx.projectId ?? undefined);
619
+ // v5.7.1 (#13): determine scope/projectId before writing
620
+ const scopeResult = resolveWriteScope(ctx, targetStore);
621
+ if (!scopeResult.ok) {
622
+ return { content: [{ type: "text", text: scopeResult.error }], isError: true };
623
+ }
624
+ const id = ctx.centralDb.getNextId(category, scopeResult.projectId ?? undefined);
535
625
  const today = new Date().toISOString().split("T")[0];
536
626
  const frontmatter = {
537
627
  id,
@@ -550,7 +640,7 @@ server.tool("gnosys_add_structured", "Add a memory with structured input (no LLM
550
640
  };
551
641
  const fullContent = `# ${title}\n\n${content}`;
552
642
  // Write to DB only (SQLite is sole source of truth)
553
- syncMemoryToDb(ctx.centralDb, frontmatter, fullContent, undefined, ctx.projectId, "project");
643
+ syncMemoryToDb(ctx.centralDb, frontmatter, fullContent, undefined, scopeResult.projectId, scopeResult.scope);
554
644
  auditToDb(ctx.centralDb, "write", id, { tool: "gnosys_add_structured", category });
555
645
  if (ctx.search)
556
646
  await reindexAllStores();
@@ -961,15 +1051,15 @@ server.tool("gnosys_commit_context", "Pre-compaction memory sweep. Call this bef
961
1051
  projectRoot: projectRootParam,
962
1052
  }, async ({ context, dry_run, projectRoot }) => {
963
1053
  const ctx = await resolveToolContext(projectRoot);
964
- // Note: ingestion is module-level since it's heavy
965
- if (!ingestion || !ingestion.isLLMAvailable) {
1054
+ // v5.8.0 (#8): no early-gate on the module-level `ingestion` here.
1055
+ // It's bound to the MCP's boot-time config — which may not have an LLM
1056
+ // block on the directory we launched in. The real LLM resolution
1057
+ // happens against `ctx.config` (merged project+global) below; if that
1058
+ // can't find a provider, getLLMProvider() surfaces a provider-specific
1059
+ // error message.
1060
+ if (!ingestion) {
966
1061
  return {
967
- content: [
968
- {
969
- type: "text",
970
- text: "Commit context requires an LLM. Configure a provider in gnosys.json or set ANTHROPIC_API_KEY.",
971
- },
972
- ],
1062
+ content: [{ type: "text", text: "Ingestion module not initialized." }],
973
1063
  isError: true,
974
1064
  };
975
1065
  }
@@ -1059,7 +1149,7 @@ Output ONLY the JSON array, no markdown fences.`,
1059
1149
  results.push(`❌ FAILED: "${candidate.summary}": Database not available`);
1060
1150
  continue;
1061
1151
  }
1062
- const result = await ingestion.ingest(candidate.summary);
1152
+ const result = await ingestion.ingest(candidate.summary, ctx.config);
1063
1153
  const id = ctx.centralDb.getNextId(result.category, ctx.projectId ?? undefined);
1064
1154
  const today = new Date().toISOString().split("T")[0];
1065
1155
  const frontmatter = {
@@ -2050,7 +2140,11 @@ server.tool("gnosys_preference_set", "Set a user preference. Preferences are sto
2050
2140
  return {
2051
2141
  content: [{
2052
2142
  type: "text",
2053
- text: `Preference set: **${pref.title}**\n Key: ${pref.key}\n Value: ${pref.value}\n\nRun \`gnosys_sync\` to regenerate agent rules files with this preference.`,
2143
+ // v5.8.0 (#9): no longer suggest running gnosys_sync. The
2144
+ // SessionStart hook (gnosys recall) already injects preferences
2145
+ // into the next session's context — no need to rewrite tracked
2146
+ // files like CLAUDE.md.
2147
+ text: `Preference set: **${pref.title}**\n Key: ${pref.key}\n Value: ${pref.value}`,
2054
2148
  }],
2055
2149
  };
2056
2150
  }
@@ -2123,14 +2217,31 @@ server.tool("gnosys_preference_delete", "Delete a user preference by key.", {
2123
2217
  return {
2124
2218
  content: [{
2125
2219
  type: "text",
2126
- text: `Preference "${key}" deleted. Run \`gnosys_sync\` to update agent rules files.`,
2220
+ // v5.8.0 (#9): drop the gnosys_sync suggestion SessionStart
2221
+ // hook picks up the change next time without rewriting tracked files.
2222
+ text: `Preference "${key}" deleted.`,
2127
2223
  }],
2128
2224
  };
2129
2225
  });
2130
2226
  // ─── Tool: gnosys_sync ──────────────────────────────────────────────────
2131
- server.tool("gnosys_sync", "Regenerate agent rules file from current user preferences and project conventions. Injects a GNOSYS:START/GNOSYS:END block into the detected agent rules file (CLAUDE.md, .cursor/rules/gnosys.mdc). User content outside the block is preserved.", {
2227
+ //
2228
+ // v5.8.0 (#9): inert-by-default. Previously this tool always wrote a
2229
+ // GNOSYS:START/GNOSYS:END block into the project's CLAUDE.md (a tracked
2230
+ // file in most repos), producing phantom git diffs every time an agent
2231
+ // helpfully called it after a preference change. Now the default is
2232
+ // "return the rules block as text" — the agent can use that as in-context
2233
+ // guidance without touching disk. To actually rewrite the rules file
2234
+ // (the v5.7.0 behaviour), pass `commit_to_disk: true` explicitly.
2235
+ //
2236
+ // Routine in-session context flows through the SessionStart hook
2237
+ // (`gnosys recall`), not through this tool.
2238
+ server.tool("gnosys_sync", "Get the current user preferences + project conventions formatted as a GNOSYS:START/GNOSYS:END block. By default returns the block as text only (no disk write). Pass commit_to_disk=true to write it into the detected agent rules file (CLAUDE.md, .cursor/rules/gnosys.mdc) — only do this if the user has explicitly asked to refresh the rules file. Routine session context is already injected via the SessionStart hook (`gnosys recall`); do NOT call this tool after every preference change.", {
2132
2239
  projectRoot: projectRootParam,
2133
- }, async ({ projectRoot }) => {
2240
+ commit_to_disk: z
2241
+ .boolean()
2242
+ .optional()
2243
+ .describe("If true, write the block into the detected agent rules file on disk. Default: false (text only). Note: rules files like CLAUDE.md and .cursor/rules/*.mdc are typically tracked in git, so writing creates a diff."),
2244
+ }, async ({ projectRoot, commit_to_disk }) => {
2134
2245
  if (!centralDb?.isAvailable()) {
2135
2246
  return {
2136
2247
  content: [{ type: "text", text: "Central DB not available. Cannot sync rules." }],
@@ -2155,12 +2266,36 @@ server.tool("gnosys_sync", "Regenerate agent rules file from current user prefer
2155
2266
  isError: true,
2156
2267
  };
2157
2268
  }
2269
+ const preferences = getAllPreferences(centralDb);
2270
+ let projectConventions = [];
2271
+ if (identity.projectId) {
2272
+ const projectMems = centralDb.getMemoriesByProject(identity.projectId);
2273
+ projectConventions = projectMems.filter((m) => (m.category === "decisions" || m.category === "conventions") && m.status === "active");
2274
+ }
2275
+ const block = generateRulesBlock(preferences, projectConventions);
2276
+ // Default path: return the block as text, no disk write.
2277
+ if (!commit_to_disk) {
2278
+ return {
2279
+ content: [
2280
+ {
2281
+ type: "text",
2282
+ text: `Rules block (preview, not written to disk):\n\n${block}\n\n` +
2283
+ `Summary: ${preferences.length} preference(s), ${projectConventions.length} project convention(s).\n` +
2284
+ `To write this into ${identity.agentRulesTarget || "the agent rules file"}, call gnosys_sync again with commit_to_disk=true.\n` +
2285
+ `(SessionStart hook 'gnosys recall' already provides routine session context — usually no disk write is needed.)`,
2286
+ },
2287
+ ],
2288
+ };
2289
+ }
2290
+ // Opt-in disk write path.
2158
2291
  if (!identity.agentRulesTarget) {
2159
2292
  return {
2160
- content: [{
2293
+ content: [
2294
+ {
2161
2295
  type: "text",
2162
- text: "No agent rules target detected (no .cursor/ or CLAUDE.md found). Create one of these first, then re-run gnosys_init to detect it.",
2163
- }],
2296
+ text: "commit_to_disk=true requested, but no agent rules target detected (no .cursor/ or CLAUDE.md found). Create one first, then re-run gnosys_init to detect it.",
2297
+ },
2298
+ ],
2164
2299
  };
2165
2300
  }
2166
2301
  const result = await syncRules(centralDb, projectDir, identity.agentRulesTarget, identity.projectId);
@@ -2172,10 +2307,13 @@ server.tool("gnosys_sync", "Regenerate agent rules file from current user prefer
2172
2307
  }
2173
2308
  const action = result.created ? "Created" : "Updated";
2174
2309
  return {
2175
- content: [{
2310
+ content: [
2311
+ {
2176
2312
  type: "text",
2177
- text: `${action} rules file: ${result.filePath}\n\n Preferences injected: ${result.prefCount}\n Project conventions: ${result.conventionCount}\n\nContent is inside <!-- GNOSYS:START --> / <!-- GNOSYS:END --> markers.\nUser content outside these markers is preserved.`,
2178
- }],
2313
+ text: `${action} rules file: ${result.filePath}\n\n Preferences injected: ${result.prefCount}\n Project conventions: ${result.conventionCount}\n\n` +
2314
+ `⚠ This wrote to a file that may be tracked in git — expect a diff. Routine session context flows through the SessionStart hook; only call gnosys_sync with commit_to_disk=true when the user explicitly asks to refresh the rules file.`,
2315
+ },
2316
+ ],
2179
2317
  };
2180
2318
  });
2181
2319
  // ─── Tool: gnosys_federated_search ───────────────────────────────────────
@@ -2644,6 +2782,10 @@ This marks the conversation checkpoint so the next /gnosys-memorize only process
2644
2782
  });
2645
2783
  // ─── Start the server ────────────────────────────────────────────────────
2646
2784
  async function main() {
2785
+ // v5.7.1 (#15): start the upgrade-marker watcher BEFORE anything else.
2786
+ // If `gnosys upgrade` was run on this machine while the MCP was idle,
2787
+ // pick that up immediately instead of serving stale tool handlers.
2788
+ startUpgradeMarkerWatcher();
2647
2789
  // v3.0: Initialize central DB at ~/.gnosys/gnosys.db
2648
2790
  try {
2649
2791
  centralDb = GnosysDB.openCentral();