@pcircle/memesh 4.8.5 → 4.9.3

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 (232) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/mcp.json +6 -4
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/AGENTS.md +60 -14
  6. package/README.de.md +83 -556
  7. package/README.md +83 -582
  8. package/README.zh-TW.md +84 -573
  9. package/dashboard/dist/index.html +10 -10
  10. package/dist/cli/view-live.d.ts.map +1 -1
  11. package/dist/cli/view-live.js +154 -384
  12. package/dist/cli/view-live.js.map +1 -1
  13. package/dist/core/agent-router.d.ts +11 -10
  14. package/dist/core/agent-router.d.ts.map +1 -1
  15. package/dist/core/agent-router.js +29 -14
  16. package/dist/core/agent-router.js.map +1 -1
  17. package/dist/core/agent-scope-id.d.ts.map +1 -1
  18. package/dist/core/agent-scope-id.js.map +1 -1
  19. package/dist/core/analytics.d.ts.map +1 -1
  20. package/dist/core/analytics.js.map +1 -1
  21. package/dist/core/config.d.ts +4 -40
  22. package/dist/core/config.d.ts.map +1 -1
  23. package/dist/core/config.js +75 -141
  24. package/dist/core/config.js.map +1 -1
  25. package/dist/core/demo.d.ts.map +1 -1
  26. package/dist/core/demo.js +6 -6
  27. package/dist/core/demo.js.map +1 -1
  28. package/dist/core/doctor-fixes.d.ts +19 -0
  29. package/dist/core/doctor-fixes.d.ts.map +1 -0
  30. package/dist/core/doctor-fixes.js +104 -0
  31. package/dist/core/doctor-fixes.js.map +1 -0
  32. package/dist/core/doctor.d.ts +2 -7
  33. package/dist/core/doctor.d.ts.map +1 -1
  34. package/dist/core/doctor.js +57 -229
  35. package/dist/core/doctor.js.map +1 -1
  36. package/dist/core/dreamer.d.ts +32 -47
  37. package/dist/core/dreamer.d.ts.map +1 -1
  38. package/dist/core/dreamer.js +211 -703
  39. package/dist/core/dreamer.js.map +1 -1
  40. package/dist/core/install-channel.d.ts.map +1 -1
  41. package/dist/core/install-channel.js +4 -47
  42. package/dist/core/install-channel.js.map +1 -1
  43. package/dist/core/install-id.d.ts.map +1 -1
  44. package/dist/core/install-id.js.map +1 -1
  45. package/dist/core/kg-backfill.d.ts.map +1 -1
  46. package/dist/core/kg-backfill.js.map +1 -1
  47. package/dist/core/lesson-engine.d.ts +0 -5
  48. package/dist/core/lesson-engine.d.ts.map +1 -1
  49. package/dist/core/lesson-engine.js +0 -25
  50. package/dist/core/lesson-engine.js.map +1 -1
  51. package/dist/core/lifecycle.d.ts.map +1 -1
  52. package/dist/core/lifecycle.js +54 -48
  53. package/dist/core/lifecycle.js.map +1 -1
  54. package/dist/core/memory-tool.d.ts.map +1 -1
  55. package/dist/core/memory-tool.js +17 -17
  56. package/dist/core/memory-tool.js.map +1 -1
  57. package/dist/core/operations.d.ts +2 -26
  58. package/dist/core/operations.d.ts.map +1 -1
  59. package/dist/core/operations.js +11 -246
  60. package/dist/core/operations.js.map +1 -1
  61. package/dist/core/paths.d.ts +4 -1
  62. package/dist/core/paths.d.ts.map +1 -1
  63. package/dist/core/paths.js +88 -14
  64. package/dist/core/paths.js.map +1 -1
  65. package/dist/core/product-improvements.js +2 -2
  66. package/dist/core/product-improvements.js.map +1 -1
  67. package/dist/core/schema-export.d.ts.map +1 -1
  68. package/dist/core/schema-export.js +11 -1
  69. package/dist/core/schema-export.js.map +1 -1
  70. package/dist/core/semver.d.ts +7 -0
  71. package/dist/core/semver.d.ts.map +1 -0
  72. package/dist/core/semver.js +49 -0
  73. package/dist/core/semver.js.map +1 -0
  74. package/dist/core/serializer.d.ts.map +1 -1
  75. package/dist/core/serializer.js +69 -57
  76. package/dist/core/serializer.js.map +1 -1
  77. package/dist/core/signal-scorer.d.ts.map +1 -1
  78. package/dist/core/signal-scorer.js.map +1 -1
  79. package/dist/core/transcript-extractor.d.ts +1 -85
  80. package/dist/core/transcript-extractor.d.ts.map +1 -1
  81. package/dist/core/transcript-extractor.js +5 -364
  82. package/dist/core/transcript-extractor.js.map +1 -1
  83. package/dist/core/transcript-source.d.ts +22 -6
  84. package/dist/core/transcript-source.d.ts.map +1 -1
  85. package/dist/core/transcript-source.js +108 -69
  86. package/dist/core/transcript-source.js.map +1 -1
  87. package/dist/core/types.d.ts +1 -17
  88. package/dist/core/types.d.ts.map +1 -1
  89. package/dist/core/version-check.d.ts.map +1 -1
  90. package/dist/core/version-check.js +11 -1
  91. package/dist/core/version-check.js.map +1 -1
  92. package/dist/db.d.ts +0 -34
  93. package/dist/db.d.ts.map +1 -1
  94. package/dist/db.js +3 -287
  95. package/dist/db.js.map +1 -1
  96. package/dist/host-runtime/acp.d.ts.map +1 -1
  97. package/dist/host-runtime/acp.js +2 -2
  98. package/dist/host-runtime/acp.js.map +1 -1
  99. package/dist/host-runtime/claude.d.ts.map +1 -1
  100. package/dist/host-runtime/claude.js +9 -7
  101. package/dist/host-runtime/claude.js.map +1 -1
  102. package/dist/host-runtime/codex-session.d.ts +9 -1
  103. package/dist/host-runtime/codex-session.d.ts.map +1 -1
  104. package/dist/host-runtime/codex-session.js +474 -29
  105. package/dist/host-runtime/codex-session.js.map +1 -1
  106. package/dist/host-runtime/codex.d.ts.map +1 -1
  107. package/dist/host-runtime/codex.js +2 -2
  108. package/dist/host-runtime/codex.js.map +1 -1
  109. package/dist/host-runtime/config.d.ts +1 -0
  110. package/dist/host-runtime/config.d.ts.map +1 -1
  111. package/dist/host-runtime/config.js +4 -0
  112. package/dist/host-runtime/config.js.map +1 -1
  113. package/dist/host-runtime/router-client.d.ts.map +1 -1
  114. package/dist/host-runtime/router-client.js +62 -15
  115. package/dist/host-runtime/router-client.js.map +1 -1
  116. package/dist/host-runtime/router.js +2 -2
  117. package/dist/host-runtime/router.js.map +1 -1
  118. package/dist/knowledge-graph.d.ts.map +1 -1
  119. package/dist/knowledge-graph.js +84 -55
  120. package/dist/knowledge-graph.js.map +1 -1
  121. package/dist/mcp/THIRD_PARTY_NOTICES.txt +217 -0
  122. package/dist/mcp/server.js +30714 -38
  123. package/dist/mcp/server.js.map +6 -1
  124. package/dist/skills-manifest.json +24 -24
  125. package/dist/storage/entity-index.d.ts +0 -1
  126. package/dist/storage/entity-index.d.ts.map +1 -1
  127. package/dist/storage/entity-index.js +0 -7
  128. package/dist/storage/entity-index.js.map +1 -1
  129. package/dist/storage/fts-index.d.ts.map +1 -1
  130. package/dist/storage/fts-index.js +6 -7
  131. package/dist/storage/fts-index.js.map +1 -1
  132. package/dist/storage/graph-repairs.d.ts +0 -5
  133. package/dist/storage/graph-repairs.d.ts.map +1 -1
  134. package/dist/storage/graph-repairs.js +4 -91
  135. package/dist/storage/graph-repairs.js.map +1 -1
  136. package/dist/storage/schema.d.ts +1 -1
  137. package/dist/storage/schema.d.ts.map +1 -1
  138. package/dist/storage/schema.js +1 -2
  139. package/dist/storage/schema.js.map +1 -1
  140. package/dist/storage/sqlite.d.ts +0 -1
  141. package/dist/storage/sqlite.d.ts.map +1 -1
  142. package/dist/storage/sqlite.js.map +1 -1
  143. package/dist/transports/agent-messaging.d.ts +4 -0
  144. package/dist/transports/agent-messaging.d.ts.map +1 -1
  145. package/dist/transports/agent-messaging.js +16 -8
  146. package/dist/transports/agent-messaging.js.map +1 -1
  147. package/dist/transports/cli/cli.d.ts.map +1 -1
  148. package/dist/transports/cli/cli.js +60751 -2408
  149. package/dist/transports/cli/cli.js.map +6 -1
  150. package/dist/transports/http/retired-routes.js +1 -1
  151. package/dist/transports/http/retired-routes.js.map +1 -1
  152. package/dist/transports/http/server.d.ts.map +1 -1
  153. package/dist/transports/http/server.js +45 -233
  154. package/dist/transports/http/server.js.map +1 -1
  155. package/dist/transports/mcp/handlers.d.ts +130 -4
  156. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  157. package/dist/transports/mcp/handlers.js +56 -7
  158. package/dist/transports/mcp/handlers.js.map +1 -1
  159. package/dist/transports/schemas.d.ts +52 -0
  160. package/dist/transports/schemas.d.ts.map +1 -1
  161. package/dist/transports/schemas.js +39 -0
  162. package/dist/transports/schemas.js.map +1 -1
  163. package/docs/platforms/README.md +5 -4
  164. package/docs/platforms/agent-messaging.md +134 -61
  165. package/hooks/hooks.json +13 -2
  166. package/llms-install.md +62 -30
  167. package/package.json +4 -5
  168. package/scripts/hooks/_generated/core-paths.js +88 -14
  169. package/scripts/hooks/_generated/fts-index.js +6 -7
  170. package/scripts/hooks/_generated/schema.js +1 -2
  171. package/scripts/hooks/_shared.js +164 -21
  172. package/scripts/hooks/session-start.js +98 -10
  173. package/scripts/hooks/session-summary.js +34 -376
  174. package/scripts/hooks/user-prompt-intent.js +71 -11
  175. package/skills/memesh/SKILL.md +19 -14
  176. package/skills/memesh-review/SKILL.md +7 -6
  177. package/dist/core/auto-tagger.d.ts +0 -10
  178. package/dist/core/auto-tagger.d.ts.map +0 -1
  179. package/dist/core/auto-tagger.js +0 -63
  180. package/dist/core/auto-tagger.js.map +0 -1
  181. package/dist/core/conflict-candidates.d.ts +0 -20
  182. package/dist/core/conflict-candidates.d.ts.map +0 -1
  183. package/dist/core/conflict-candidates.js +0 -71
  184. package/dist/core/conflict-candidates.js.map +0 -1
  185. package/dist/core/conflict-judge.d.ts +0 -58
  186. package/dist/core/conflict-judge.d.ts.map +0 -1
  187. package/dist/core/conflict-judge.js +0 -189
  188. package/dist/core/conflict-judge.js.map +0 -1
  189. package/dist/core/digest-validator.d.ts +0 -18
  190. package/dist/core/digest-validator.d.ts.map +0 -1
  191. package/dist/core/digest-validator.js +0 -85
  192. package/dist/core/digest-validator.js.map +0 -1
  193. package/dist/core/embedder.d.ts +0 -20
  194. package/dist/core/embedder.d.ts.map +0 -1
  195. package/dist/core/embedder.js +0 -245
  196. package/dist/core/embedder.js.map +0 -1
  197. package/dist/core/failure-analyzer.d.ts +0 -19
  198. package/dist/core/failure-analyzer.d.ts.map +0 -1
  199. package/dist/core/failure-analyzer.js +0 -83
  200. package/dist/core/failure-analyzer.js.map +0 -1
  201. package/dist/core/json-utils.d.ts +0 -3
  202. package/dist/core/json-utils.d.ts.map +0 -1
  203. package/dist/core/json-utils.js +0 -46
  204. package/dist/core/json-utils.js.map +0 -1
  205. package/dist/core/llm-client.d.ts +0 -22
  206. package/dist/core/llm-client.d.ts.map +0 -1
  207. package/dist/core/llm-client.js +0 -203
  208. package/dist/core/llm-client.js.map +0 -1
  209. package/dist/core/llm-telemetry.d.ts +0 -47
  210. package/dist/core/llm-telemetry.d.ts.map +0 -1
  211. package/dist/core/llm-telemetry.js +0 -117
  212. package/dist/core/llm-telemetry.js.map +0 -1
  213. package/dist/core/llm-validator.d.ts +0 -20
  214. package/dist/core/llm-validator.d.ts.map +0 -1
  215. package/dist/core/llm-validator.js +0 -231
  216. package/dist/core/llm-validator.js.map +0 -1
  217. package/dist/core/ollama-host.d.ts +0 -6
  218. package/dist/core/ollama-host.d.ts.map +0 -1
  219. package/dist/core/ollama-host.js +0 -62
  220. package/dist/core/ollama-host.js.map +0 -1
  221. package/dist/core/output-language.d.ts +0 -6
  222. package/dist/core/output-language.d.ts.map +0 -1
  223. package/dist/core/output-language.js +0 -25
  224. package/dist/core/output-language.js.map +0 -1
  225. package/dist/core/prompt-safety.d.ts +0 -4
  226. package/dist/core/prompt-safety.d.ts.map +0 -1
  227. package/dist/core/prompt-safety.js +0 -20
  228. package/dist/core/prompt-safety.js.map +0 -1
  229. package/dist/storage/vector-index.d.ts +0 -3
  230. package/dist/storage/vector-index.d.ts.map +0 -1
  231. package/dist/storage/vector-index.js +0 -13
  232. package/dist/storage/vector-index.js.map +0 -1
@@ -4,26 +4,9 @@
4
4
  // Extracts knowledge from completed Claude Code sessions
5
5
  // and stores as session-insight entities in MeMesh.
6
6
  //
7
- // THIS HOOK STAYS, and that was an open question rather than an assumption.
8
- // `dream run --from-transcripts` reads the same sessions from their raw JSONL
9
- // and does not depend on this hook having fired, so the obvious next step was
10
- // to retire the hook as redundant. Measured first, on a real graph
11
- // (2026-08-09, 214 active entities, ollama nomic-embed-text, L2 over the same
12
- // `name + observations` text the runtime embeds):
13
- //
14
- // every transcript-mined memory -> its nearest hook-captured entity
15
- // min 0.784 p25 0.821 p50 0.865 max 0.946
16
- // within 0.55: 0 of 47. within 0.70: 0 of 47.
17
- //
18
- // Nothing the transcript miner produced came within 0.78 of anything this hook
19
- // recorded. They are not two views of the same material: this hook records what
20
- // HAPPENED (files touched, commands run, commits), the miner extracts what was
21
- // DECIDED and what was LEARNED. Retiring either one loses a whole category.
22
-
23
7
  import { createRequire } from 'module';
24
8
  import { basename, join } from 'path';
25
- import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
26
- import { spawn } from 'child_process';
9
+ import { existsSync, readFileSync } from 'fs';
27
10
  import { pathToFileURL } from 'url';
28
11
  import {
29
12
  AUTO_CAPTURE_TAG,
@@ -32,7 +15,7 @@ import {
32
15
  extractCitedMemoryIds,
33
16
  getMemeshDirFromDbPath,
34
17
  getProjectName,
35
- importFromPluginRoot,
18
+ findAutoUpdateConsent,
36
19
  isAutoCaptureEnabled,
37
20
  openHookDb,
38
21
  readUpdateCheckCache,
@@ -47,25 +30,18 @@ import {
47
30
 
48
31
  const require = createRequire(import.meta.url);
49
32
 
50
- // Pre-load dist/core/install-channel.js for auto-update channel detection.
51
- // Same pattern as session-start.js: async ESM import at process init,
52
- // falls back to null if dist is missing (source checkout pre-build).
53
- let _installChannelMod = null;
33
+ let installChannel = null;
54
34
  try {
55
- const _pluginRootForInit = resolvePluginRoot(import.meta.url);
56
- const _modPath = join(_pluginRootForInit, 'dist/core/install-channel.js');
57
- if (existsSync(_modPath)) {
58
- _installChannelMod = await import(pathToFileURL(_modPath).href);
35
+ const pluginRoot = resolvePluginRoot(import.meta.url);
36
+ const modulePath = join(pluginRoot, 'dist/core/install-channel.js');
37
+ if (existsSync(modulePath)) {
38
+ installChannel = await import(pathToFileURL(modulePath).href);
59
39
  }
60
- } catch { /* best-effort */ }
40
+ } catch {
41
+ // Best-effort: source checkouts may not have built dist output yet.
42
+ }
61
43
 
62
- /**
63
- * Run auto-update at Stop hook: reads cache, evaluates policy, and dispatches
64
- * the detached updater runner if warranted. Runs after all session work
65
- * completes, avoiding the TOCTOU race where install would overwrite dist/
66
- * mid-session.
67
- */
68
- async function runAutoUpdateAtStop() {
44
+ async function runAutoUpdateAtStop(sessionId) {
69
45
  try {
70
46
  const pluginRoot = resolvePluginRoot(import.meta.url);
71
47
  const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
@@ -75,11 +51,16 @@ async function runAutoUpdateAtStop() {
75
51
  const cache = readUpdateCheckCache(installedVersion);
76
52
  const policy = resolveAutoUpdatePolicy(process.env);
77
53
  const decision = decideAutoUpdateHook(installedVersion, cache, policy);
78
- if (decision.run) {
79
- await spawnAutoUpdate(decision.latest, _installChannelMod);
54
+ let channel = 'unknown';
55
+ try { channel = installChannel?.getCurrentInstallChannel({ packageRoot: pluginRoot }) ?? 'unknown'; } catch { /* best-effort */ }
56
+ const consent = decision.run
57
+ ? findAutoUpdateConsent(sessionId, installedVersion, decision.latest, channel)
58
+ : null;
59
+ if (decision.run && consent?.decision === 'approved') {
60
+ await spawnAutoUpdate(decision.latest, installChannel);
80
61
  }
81
62
  } catch {
82
- // Best-effort never crash the hook.
63
+ // Best-effort: update failures must never break session capture.
83
64
  }
84
65
  }
85
66
 
@@ -183,13 +164,9 @@ function parseTranscript(transcriptPath) {
183
164
  const text = typeof block.content === 'string'
184
165
  ? block.content
185
166
  : JSON.stringify(block.content);
186
- // Same reason as the bash branch, and one more: this array is
187
- // ALSO the payload `analyzeFailure` sends to the configured LLM
188
- // provider. A failed request that echoes its own Authorization
189
- // header — the ordinary shape of an auth error — would be stored
190
- // and then transmitted off the machine. Redacted once here, at
191
- // the point the text enters the process, so every downstream use
192
- // inherits it.
167
+ // Same reason as the bash branch: a failed request may echo its
168
+ // own Authorization header. Redact once, where text enters the
169
+ // process, so every downstream use inherits it.
193
170
  errorsEncountered.push(redactSecrets(text).slice(0, 200));
194
171
  }
195
172
  }
@@ -235,6 +212,7 @@ let input = '';
235
212
  process.stdin.setEncoding('utf8');
236
213
  process.stdin.on('data', (chunk) => { input += chunk; });
237
214
  process.stdin.on('end', async () => {
215
+ let sessionId = 'unknown';
238
216
  try {
239
217
  if (!input.trim()) return exit0();
240
218
 
@@ -255,7 +233,7 @@ process.stdin.on('end', async () => {
255
233
  return exit0();
256
234
  }
257
235
 
258
- const sessionId = inputData.session_id || 'unknown';
236
+ sessionId = inputData.session_id || 'unknown';
259
237
  const transcriptPath = inputData.transcript_path;
260
238
 
261
239
  // `cwd` decides the project tag, and the project tag decides which
@@ -338,33 +316,12 @@ process.stdin.on('end', async () => {
338
316
  // healthy exit, so it MUST stamp (see stampHookRunOnly).
339
317
  if (toolCallCount < 3) { stampHookRunOnly(process.env, 'session-summary'); return exit0(); }
340
318
 
341
- // Hoisted to outer-try scope so the LLM failure-analysis block
342
- // below (which runs AFTER db.close()) can reference it. Earlier
343
- // version defined projectName inside the inner try-finally and the
344
- // LLM path threw `projectName is not defined` silently — caught by
345
- // the LLM try/catch but logged to stderr. Result: lesson_learned
346
- // creation never actually happened in production.
347
319
  const projectName = getProjectName(cwd);
348
320
 
349
321
  // Open DB via shared helper — applies SCHEMA_SQL + status migration.
350
322
  // { fts: true } guarantees the entities_fts table exists so captureEntity()
351
323
  // can keep it in sync — session-insight memories must be FTS-recallable.
352
324
  //
353
- // sqlite-vec is NOT loaded here, and used to be. The comment said it was
354
- // needed "for embedding-aware recall-effectiveness tracking" — but this
355
- // hook runs exactly two statements, `PRAGMA table_info(entities)` and
356
- // `SELECT id FROM entities WHERE name = ?`, and `captureEntity` in
357
- // _shared.js touches no vectors either. Nothing here has ever used the
358
- // extension.
359
- //
360
- // It was not free. sqlite-vec ships its engine as a per-platform file
361
- // through optionalDependencies, and on a platform it does not publish the
362
- // load threw — past the `require` guard, which never fired because the JS
363
- // wrapper resolves fine and the throw happens later inside
364
- // `sqliteVec.load()`. Measured with the platform binary hidden: the whole
365
- // Stop capture vanished (0 entities against a control run's 1) and the
366
- // user got a `Require stack:` dump on stderr. An extension nobody calls
367
- // was silently costing every session on those platforms its memory.
368
325
  const { db } = openHookDb(process.env, { fts: true });
369
326
  let writeFailed = false;
370
327
  try {
@@ -389,6 +346,10 @@ process.stdin.on('end', async () => {
389
346
  ).get(`session-${sessionId}-files`, `session-${sessionId}-fixes`, `session-${sessionId}-summary`);
390
347
  if (alreadyCaptured) {
391
348
  recordHookRun(db, 'session-summary');
349
+ // A duplicate capture is still a completed Stop lifecycle. Update
350
+ // consent is session-scoped and must not be skipped merely because
351
+ // the same transcript was observed twice (a common host retry).
352
+ await runAutoUpdateAtStop(sessionId);
392
353
  return exit0();
393
354
  }
394
355
 
@@ -628,69 +589,18 @@ process.stdin.on('end', async () => {
628
589
  db.close();
629
590
  }
630
591
 
631
- // ── LLM-powered failure analysis (Level 1 only) ──────────────────────
632
- // Runs AFTER the hook's own DB is closed.
633
- // Uses the core module's DB singleton (openDatabase/closeDatabase).
634
- // Wrapped in its own try/catch — never blocks rule-based extraction.
635
- if (errorsEncountered.length > 0 && filesEdited.length > 0) {
636
- try {
637
- // F5: derive pluginRoot strictly from this file's location.
638
- // See `resolvePluginRoot` for the full reasoning.
639
- const pluginRoot = resolvePluginRoot(import.meta.url);
640
- const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
641
- const config = configMod.readConfig();
642
-
643
- if (config.llm) {
644
- const { openDatabase, closeDatabase } = await importFromPluginRoot(pluginRoot, 'dist/db.js');
645
- const { analyzeFailure } = await importFromPluginRoot(pluginRoot, 'dist/core/failure-analyzer.js');
646
- const { createLesson } = await importFromPluginRoot(pluginRoot, 'dist/core/lesson-engine.js');
647
-
648
- openDatabase();
649
- try {
650
- // Pass cross-provider failover chain so a stale Anthropic key
651
- // doesn't silently disable Stop-hook lesson generation.
652
- const lesson = await analyzeFailure(errorsEncountered, filesEdited, config.llm, { fallbacks: config.llmFallbacks });
653
- if (lesson) {
654
- createLesson(lesson, projectName);
655
- }
656
- } finally {
657
- closeDatabase();
658
- }
659
- }
660
- } catch (llmErr) {
661
- // LLM analysis failed — rule-based extraction already captured the session.
662
- // Log to stderr so config issues (e.g. invalid API key) are visible.
663
- try { process.stderr.write(`[memesh] LLM failure analysis skipped: ${llmErr?.message || llmErr}\n`); } catch {}
664
- }
665
- }
666
-
667
- // Auto-trigger dream — solves the "Insights tab is empty for users
668
- // who don't know `memesh dream run` exists" problem. Throttled to
669
- // once per project per 24h, gated by minimum activity threshold.
670
- // Background-detached spawn so the hook exits immediately even if
671
- // the LLM call takes 30-60s. See `maybeTriggerDream` for the gate
672
- // logic and dream-history.json schema.
673
- try {
674
- const pluginRoot = resolvePluginRoot(import.meta.url);
675
- const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
676
- const config = configMod.readConfig();
677
- maybeTriggerDream(projectName, config, pluginRoot);
678
- } catch (dreamErr) {
679
- try { process.stderr.write(`[memesh] dream auto-trigger skipped: ${dreamErr?.message || dreamErr}\n`); } catch {}
680
- }
681
592
  } catch (err) {
682
593
  // Never crash Claude Code — leave a trace for debugging.
683
594
  //
684
- // Every error is traced now. There used to be a suppression branch for a
685
- // `skip-session-capture:` sentinel, thrown when sqlite-vec was missing
686
- // an extension this hook never used. The thrower is gone, so the branch
687
- // could only ever hide a real error from here on.
595
+ // Every error is traced. A retired suppression sentinel once hid setup
596
+ // failures from this hook; with that branch gone, real capture errors stay
597
+ // visible without crashing the host session.
688
598
  try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
689
599
  }
690
600
 
691
- // Dispatch auto-update if policy + cache permit. Runs after all session work
692
- // so the runner cannot overwrite dist/ while peer hooks are reading it.
693
- await runAutoUpdateAtStop();
601
+ // Update only after all session work so installed files cannot change while
602
+ // this hook is still reading them.
603
+ await runAutoUpdateAtStop(sessionId);
694
604
 
695
605
  // Emit NOTHING on success — not `{"suppressOutput": true}`.
696
606
  //
@@ -711,173 +621,6 @@ process.stdin.on('end', async () => {
711
621
  function exit0() {
712
622
  process.exit(0);
713
623
  }
714
-
715
- // =============================================================================
716
- // Dream auto-trigger (Phase 2 / Phase 3 background runner)
717
- // =============================================================================
718
- //
719
- // Without an automated trigger, `memesh dream` only runs when the user
720
- // types it into a terminal — and most users never read the docs that
721
- // far. Result: Insights tab stays empty and the KG accumulates 89.7%
722
- // orphan rate (the maintainer's own observation on this DB).
723
- //
724
- // This trigger fires at the END of every Stop hook (after rule-based
725
- // session capture + optional LLM failure analysis). It:
726
- // 1. Loads ~/.memesh/dream-history.json — a per-project record of
727
- // the last dream run timestamp + outcome.
728
- // 2. Throttles: skip if < THROTTLE_HOURS since the project's last
729
- // run, even if the previous run produced zero proposals.
730
- // 3. Activity gate: skip if the project has < MIN_EPISODIC episodic
731
- // entities to draw from in the last WINDOW_DAYS days.
732
- // 4. LLM gate: skip if no LLM provider configured (Phase 2 needs
733
- // Smart Mode — Phase 3 patterns same).
734
- // 5. Spawns `node <pluginRoot>/dist/transports/cli/cli.js dream run
735
- // --project <name> --max-llm-calls 2 --window-days 14` as a
736
- // detached child process. Stdout/stderr go to a per-project log
737
- // under ~/.memesh/dream-runs/ so the user can `tail -f` to see
738
- // progress without the hook blocking.
739
- // 6. Records the run start in dream-history.json BEFORE spawning so
740
- // a long-running spawn doesn't get re-triggered on the next
741
- // Stop within the same window.
742
- //
743
- // The spawned process inherits config from disk, including the
744
- // `llmFallbacks` chain wired in commit 883abd4d, so a primary
745
- // outage falls through to Ollama automatically.
746
-
747
- const DREAM_THROTTLE_HOURS = 24;
748
- const DREAM_MIN_EPISODIC = 10;
749
- const DREAM_WINDOW_DAYS = 14;
750
- const DREAM_MAX_LLM_CALLS = 2;
751
- const DREAM_HISTORY_BASENAME = 'dream-history.json';
752
- const DREAM_LOG_DIRNAME = 'dream-runs';
753
-
754
- const DREAM_EPISODIC_TYPES = [
755
- 'commit',
756
- 'session_keypoint',
757
- 'session-insight',
758
- 'workflow_checkpoint',
759
- 'weekly-summary',
760
- 'weekly_summary',
761
- ];
762
-
763
- // Debug-trace gate. Set MEMESH_DREAM_TRIGGER_DEBUG=1 to emit a stderr
764
- // breadcrumb at every gate decision in maybeTriggerDream. Contract:
765
- // [memesh dream-trigger] <stage>=<value>...
766
- // Matches one line per stage (resolve, llm-gate, throttle-gate,
767
- // activity-gate, history-write, spawn). Stable across releases — if
768
- // you rename a stage, update the matching test fixture too.
769
- function dreamTrigTrace(stage, fields) {
770
- if (process.env.MEMESH_DREAM_TRIGGER_DEBUG !== '1') return;
771
- try {
772
- const parts = Object.entries(fields || {}).map(([k, v]) => {
773
- if (v === undefined) return `${k}=<undef>`;
774
- if (v === null) return `${k}=<null>`;
775
- return `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`;
776
- });
777
- process.stderr.write(`[memesh dream-trigger] ${stage} ${parts.join(' ')}\n`);
778
- } catch {}
779
- }
780
-
781
- function dreamHistoryPath() {
782
- // Test isolation: tests set MEMESH_DB_PATH to a tmp file, expecting
783
- // sibling state (history, logs) to land beside it. Without honouring
784
- // that, real-home `~/.memesh/dream-history.json` would be polluted
785
- // by every test run. Same precedence as memesh's other state files —
786
- // see `getMemeshDirFromDbPath` in `_shared.js`.
787
- //
788
- // Windows note: on Windows under execFileSync, env vars propagate as
789
- // plain strings (no canonicalisation). If a caller passed MEMESH_DIR
790
- // with mixed separators (e.g. `C:/Users/.../tmp/.memesh`), join() will
791
- // happily mix `/` and `\`. Node fs accepts either — the only divergence
792
- // is in pure string comparisons, which we don't do here. The trace
793
- // below shows the resolved value verbatim so a Windows diagnosis run
794
- // can confirm what actually arrived.
795
- // The helper IS the precedence (MEMESH_DB_PATH > MEMESH_DIR > home).
796
- // A hand-rolled version here inverted it (MEMESH_DIR won over
797
- // MEMESH_DB_PATH), so with both set, dream history landed in a different
798
- // directory than every sibling state file — plus a dead home-fallback
799
- // branch, since the helper always returns a string.
800
- const dir = getMemeshDirFromDbPath();
801
- dreamTrigTrace('resolve', {
802
- src: process.env.MEMESH_DB_PATH ? 'db-path' : (process.env.MEMESH_DIR ? 'env' : 'home'),
803
- MEMESH_DIR: process.env.MEMESH_DIR,
804
- MEMESH_DB_PATH: process.env.MEMESH_DB_PATH,
805
- dir,
806
- platform: process.platform,
807
- });
808
- return { dir, historyFile: join(dir, DREAM_HISTORY_BASENAME), logDir: join(dir, DREAM_LOG_DIRNAME) };
809
- }
810
-
811
- function readDreamHistory() {
812
- try {
813
- const { historyFile } = dreamHistoryPath();
814
- if (!existsSync(historyFile)) return {};
815
- const raw = readFileSync(historyFile, 'utf8');
816
- const parsed = JSON.parse(raw);
817
- return (parsed && typeof parsed === 'object') ? parsed : {};
818
- } catch { return {}; }
819
- }
820
-
821
- function writeDreamHistory(history) {
822
- try {
823
- const { dir, historyFile } = dreamHistoryPath();
824
- mkdirSync(dir, { recursive: true });
825
- writeFileSync(historyFile, JSON.stringify(history, null, 2));
826
- dreamTrigTrace('history-write', { historyFile, ok: true, projects: Object.keys(history) });
827
- } catch (err) {
828
- dreamTrigTrace('history-write', { ok: false, err: err?.message || String(err) });
829
- try { process.stderr.write(`[memesh dream-history write] ${err?.message || err}\n`); } catch {}
830
- }
831
- }
832
-
833
- /**
834
- * Count episodic entities for the given project over the configured
835
- * window. Only issues a SELECT, but the handle is openHookDb's ordinary
836
- * read-write one (it runs the schema/migration chain) — there is no
837
- * read-only variant on the hook side, and this call site must not stamp
838
- * the heartbeat (recordHookRun is per-hook-exit, never per-open).
839
- */
840
- function countEpisodicEntities(projectName) {
841
- let handle;
842
- try {
843
- handle = openHookDb();
844
- // { db, dbPath } — don't call db.prepare() on the wrapper itself (an
845
- // earlier version did, and silently skipped the gate, defeating the
846
- // trigger's whole purpose).
847
- const db = handle.db;
848
- const since = new Date(Date.now() - DREAM_WINDOW_DAYS * 86400000).toISOString();
849
- const types = DREAM_EPISODIC_TYPES.map(() => '?').join(',');
850
- // Project-membership is determined by the `project:<name>` tag only.
851
- // The previous implementation also OR'd on `e.name LIKE 'project-%'`
852
- // as a fallback for legacy entities, but that branch over-counts when
853
- // two projects share a name prefix (e.g. counting `memesh` would
854
- // sweep in `memesh-cloud-keypoint-*` entities). Post-v3.0 episodic
855
- // entities all carry the project tag — the auto-tagger writes it on
856
- // every session_keypoint / commit / session_insight / session_lesson
857
- // / failure_pattern / decision_anchor — so the tag-only path is the
858
- // accurate signal. See v4.2.1 CHANGELOG known-limitations note.
859
- const sql = `SELECT COUNT(DISTINCT e.id) AS n
860
- FROM entities e
861
- INNER JOIN tags t ON t.entity_id = e.id AND t.tag = ?
862
- WHERE e.status = 'active'
863
- AND e.type IN (${types})
864
- AND e.created_at >= ?`;
865
- const projectTag = `project:${projectName}`;
866
- const row = db.prepare(sql).get(projectTag, ...DREAM_EPISODIC_TYPES, since);
867
- return row?.n ?? 0;
868
- } catch (err) {
869
- try { process.stderr.write(`[memesh dream-trigger count] ${err?.message || err}\n`); } catch {}
870
- return 0;
871
- } finally {
872
- try { handle?.db?.close(); } catch {}
873
- }
874
- }
875
-
876
- /**
877
- * Decide whether to fire dream for `projectName` and, if so, spawn
878
- * the detached background runner. Pure side effect — no return value
879
- * used by callers.
880
- */
881
624
  /**
882
625
  * Attachment record types Claude Code uses to persist a hook's own output
883
626
  * into the transcript. Anything memesh injected reaches the transcript
@@ -927,88 +670,3 @@ export function stripHookEchoes(rawTranscript) {
927
670
  }
928
671
  return kept.join('\n');
929
672
  }
930
-
931
- export function maybeTriggerDream(projectName, config, pluginRoot) {
932
- dreamTrigTrace('enter', { projectName, hasLlm: Boolean(config?.llm) });
933
- if (!projectName || projectName === 'unknown') {
934
- dreamTrigTrace('exit', { reason: 'no-project-name', projectName });
935
- return;
936
- }
937
-
938
- // Phase 2 & 3 both need a configured LLM. Without it the dreamer's
939
- // first action is to push `{reason: 'no LLM configured'}` into
940
- // skipped[] and exit, which would still bump our throttle clock
941
- // for no value — so gate here.
942
- if (!config?.llm) {
943
- dreamTrigTrace('exit', { reason: 'llm-gate-fail' });
944
- return;
945
- }
946
- dreamTrigTrace('llm-gate', { ok: true });
947
-
948
- const history = readDreamHistory();
949
- const last = history[projectName];
950
- if (last?.last_run_iso) {
951
- const ageMs = Date.now() - new Date(last.last_run_iso).getTime();
952
- if (Number.isFinite(ageMs) && ageMs < DREAM_THROTTLE_HOURS * 3600 * 1000) {
953
- dreamTrigTrace('exit', { reason: 'throttle-gate-fail', ageMs, last_run_iso: last.last_run_iso });
954
- return; // throttled
955
- }
956
- }
957
- dreamTrigTrace('throttle-gate', { ok: true, prior_last_run_iso: last?.last_run_iso });
958
-
959
- const episodicCount = countEpisodicEntities(projectName);
960
- if (episodicCount < DREAM_MIN_EPISODIC) {
961
- dreamTrigTrace('exit', { reason: 'activity-gate-fail', episodicCount, threshold: DREAM_MIN_EPISODIC });
962
- return;
963
- }
964
- dreamTrigTrace('activity-gate', { ok: true, episodicCount });
965
-
966
- // Activity gate passed — record start BEFORE spawning so we don't
967
- // re-trigger on any subsequent Stop within the window even if the
968
- // child takes 30-60s.
969
- history[projectName] = {
970
- last_run_iso: new Date().toISOString(),
971
- last_episodic_count: episodicCount,
972
- last_window_days: DREAM_WINDOW_DAYS,
973
- };
974
- writeDreamHistory(history);
975
-
976
- // Spawn detached so the hook exits immediately. Stdio routes to a
977
- // per-project log file so the user can inspect dream output without
978
- // the hook blocking on the LLM call.
979
- try {
980
- const { dir, logDir } = dreamHistoryPath();
981
- mkdirSync(logDir, { recursive: true });
982
- const safeProj = projectName.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64);
983
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
984
- const logFile = join(logDir, `${safeProj}-${ts}.log`);
985
-
986
- // Append a header so a tail -f shows context for the run.
987
- appendFileSync(logFile, `[memesh dream] ${new Date().toISOString()} project=${projectName} episodic_count=${episodicCount} window=${DREAM_WINDOW_DAYS}d max_llm_calls=${DREAM_MAX_LLM_CALLS}\n`);
988
-
989
- const cliPath = join(pluginRoot, 'dist/transports/cli/cli.js');
990
- if (!existsSync(cliPath)) {
991
- appendFileSync(logFile, `[memesh dream] cli.js missing at ${cliPath}, skipping\n`);
992
- return;
993
- }
994
-
995
- const args = [
996
- cliPath, 'dream', 'run',
997
- '--project', projectName,
998
- '--max-llm-calls', String(DREAM_MAX_LLM_CALLS),
999
- '--window-days', String(DREAM_WINDOW_DAYS),
1000
- ];
1001
-
1002
- const logFd = require('fs').openSync(logFile, 'a');
1003
- const child = spawn(process.execPath, args, {
1004
- detached: true,
1005
- stdio: ['ignore', logFd, logFd],
1006
- env: { ...process.env, MEMESH_DIR: dir },
1007
- });
1008
- child.unref();
1009
- dreamTrigTrace('spawn', { ok: true, pid: child.pid, logFile, MEMESH_DIR: dir });
1010
- } catch (err) {
1011
- dreamTrigTrace('spawn', { ok: false, err: err?.message || String(err) });
1012
- try { process.stderr.write(`[memesh dream-trigger spawn] ${err?.message || err}\n`); } catch {}
1013
- }
1014
- }
@@ -19,7 +19,62 @@
19
19
  // Gated by `autoCapture` flag (same as other memesh write hooks).
20
20
 
21
21
  import { pathToFileURL } from 'url';
22
- import { isAutoCaptureEnabled } from './_shared.js';
22
+ import {
23
+ findAutoUpdateConsent,
24
+ isAutoCaptureEnabled,
25
+ parseAutoUpdateConsent,
26
+ readUpdateCheckCache,
27
+ resolvePluginRoot,
28
+ writeAutoUpdateConsent,
29
+ } from './_shared.js';
30
+ import { join } from 'path';
31
+ import { existsSync, readFileSync } from 'fs';
32
+
33
+ let installChannelMod = null;
34
+ try {
35
+ const pluginRoot = resolvePluginRoot(import.meta.url);
36
+ const modulePath = join(pluginRoot, 'dist/core/install-channel.js');
37
+ if (existsSync(modulePath)) installChannelMod = await import(pathToFileURL(modulePath).href);
38
+ } catch {
39
+ // Source checkouts without a build remain non-blocking and cannot self-update.
40
+ }
41
+
42
+ function currentInstallChannel() {
43
+ try {
44
+ return installChannelMod?.getCurrentInstallChannel({
45
+ packageRoot: resolvePluginRoot(import.meta.url),
46
+ }) ?? 'unknown';
47
+ } catch {
48
+ return 'unknown';
49
+ }
50
+ }
51
+
52
+ function currentInstalledVersion() {
53
+ try {
54
+ const pkg = JSON.parse(readFileSync(join(resolvePluginRoot(import.meta.url), 'package.json'), 'utf8'));
55
+ return typeof pkg.version === 'string' ? pkg.version : null;
56
+ } catch { return null; }
57
+ }
58
+
59
+ function recordUpdateConsent(sessionId, prompt) {
60
+ const current = currentInstalledVersion();
61
+ if (!current || !sessionId) return null;
62
+ const channel = currentInstallChannel();
63
+ // Only npm-global has a hook-owned installer. Other channels receive an
64
+ // actionable notice at SessionStart and must not turn an "Upgrade" word
65
+ // into a misleading approval marker for a different installation path.
66
+ if (channel !== 'npm-global') return null;
67
+ const cache = readUpdateCheckCache(current);
68
+ const latest = cache?.latestVersion;
69
+ if (typeof latest !== 'string' || !latest) return null;
70
+ const pending = findAutoUpdateConsent(sessionId, current, latest, channel);
71
+ if (!pending || !['pending'].includes(pending.decision)) return null;
72
+ const decision = parseAutoUpdateConsent(prompt);
73
+ if (!decision) return null;
74
+ return writeAutoUpdateConsent(
75
+ sessionId, current, latest, pending.channel ?? 'unknown', decision,
76
+ ) ? decision : null;
77
+ }
23
78
 
24
79
  // Patterns compiled at module load — invalid regex MUST fail loudly. Do
25
80
  // NOT move into a try block "for safety": a regex compile error is a
@@ -121,8 +176,6 @@ if (isMainModule) {
121
176
  process.stdin.on('data', (chunk) => { input += chunk; });
122
177
  process.stdin.on('end', () => {
123
178
  try {
124
- if (!isAutoCaptureEnabled(process.env)) return process.exit(0);
125
-
126
179
  // Distinguish empty stdin (legitimate degenerate event) from malformed
127
180
  // input (protocol drift). Both stay non-blocking, but only malformed
128
181
  // input is logged — empty is normal, garbage indicates a real bug.
@@ -142,14 +195,21 @@ if (isMainModule) {
142
195
  // we accept either name to survive a similar rename. If both are absent
143
196
  // or non-string, detectRememberIntent's type guard returns false safely.
144
197
  const prompt = data.prompt ?? data.user_prompt ?? '';
145
- if (!detectRememberIntent(prompt)) return process.exit(0);
146
-
147
- const out = {
148
- hookSpecificOutput: {
149
- hookEventName: 'UserPromptSubmit',
150
- additionalContext: buildHint(),
151
- },
152
- };
198
+ const updateDecision = recordUpdateConsent(data.session_id, prompt);
199
+ const rememberIntent = detectRememberIntent(prompt);
200
+ if (!rememberIntent && !updateDecision) return process.exit(0);
201
+ // Update consent is a user-authorized control decision, not memory
202
+ // capture; it must still be recorded when auto-capture is disabled.
203
+ if (!isAutoCaptureEnabled(process.env) && !updateDecision) return process.exit(0);
204
+
205
+ const contexts = [];
206
+ if (updateDecision) {
207
+ contexts.push(updateDecision === 'approved'
208
+ ? 'The user explicitly approved the MeMesh upgrade. The Stop hook may now update the consented installation.'
209
+ : 'The user declined the MeMesh upgrade for this session. Do not install it or ask again in this session.');
210
+ }
211
+ if (rememberIntent) contexts.push(buildHint());
212
+ const out = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: contexts.join('\n\n') } };
153
213
  process.stdout.write(JSON.stringify(out));
154
214
  process.exit(0);
155
215
  } catch (err) {