@pcircle/memesh 4.4.0 → 4.5.1

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 (198) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +1 -1
  4. package/README.de.md +32 -12
  5. package/README.es.md +13 -12
  6. package/README.fr.md +31 -11
  7. package/README.ja.md +13 -12
  8. package/README.ko.md +14 -13
  9. package/README.md +41 -16
  10. package/README.pt.md +30 -10
  11. package/README.th.md +16 -9
  12. package/README.vi.md +32 -12
  13. package/README.zh-CN.md +14 -13
  14. package/README.zh-TW.md +14 -13
  15. package/dashboard/dist/index.html +10 -10
  16. package/dist/cli/view-live.js +1 -1
  17. package/dist/cli/view.d.ts.map +1 -1
  18. package/dist/cli/view.js +2 -2
  19. package/dist/cli/view.js.map +1 -1
  20. package/dist/core/analytics.d.ts +7 -3
  21. package/dist/core/analytics.d.ts.map +1 -1
  22. package/dist/core/analytics.js +8 -8
  23. package/dist/core/analytics.js.map +1 -1
  24. package/dist/core/auto-tagger.d.ts.map +1 -1
  25. package/dist/core/auto-tagger.js.map +1 -1
  26. package/dist/core/config.d.ts +5 -3
  27. package/dist/core/config.d.ts.map +1 -1
  28. package/dist/core/config.js +30 -14
  29. package/dist/core/config.js.map +1 -1
  30. package/dist/core/demo.d.ts +2 -2
  31. package/dist/core/demo.d.ts.map +1 -1
  32. package/dist/core/demo.js.map +1 -1
  33. package/dist/core/digest-validator.d.ts.map +1 -1
  34. package/dist/core/digest-validator.js +3 -1
  35. package/dist/core/digest-validator.js.map +1 -1
  36. package/dist/core/doctor.d.ts +3 -0
  37. package/dist/core/doctor.d.ts.map +1 -1
  38. package/dist/core/doctor.js +255 -101
  39. package/dist/core/doctor.js.map +1 -1
  40. package/dist/core/dreamer.d.ts +35 -7
  41. package/dist/core/dreamer.d.ts.map +1 -1
  42. package/dist/core/dreamer.js +394 -41
  43. package/dist/core/dreamer.js.map +1 -1
  44. package/dist/core/embedder.d.ts +3 -5
  45. package/dist/core/embedder.d.ts.map +1 -1
  46. package/dist/core/embedder.js +26 -95
  47. package/dist/core/embedder.js.map +1 -1
  48. package/dist/core/extractor.d.ts.map +1 -1
  49. package/dist/core/extractor.js +2 -1
  50. package/dist/core/extractor.js.map +1 -1
  51. package/dist/core/failure-analyzer.d.ts.map +1 -1
  52. package/dist/core/failure-analyzer.js +2 -1
  53. package/dist/core/failure-analyzer.js.map +1 -1
  54. package/dist/core/graph.d.ts +4 -4
  55. package/dist/core/graph.d.ts.map +1 -1
  56. package/dist/core/graph.js.map +1 -1
  57. package/dist/core/install-hooks.d.ts +1 -0
  58. package/dist/core/install-hooks.d.ts.map +1 -1
  59. package/dist/core/install-hooks.js +0 -0
  60. package/dist/core/install-hooks.js.map +1 -1
  61. package/dist/core/kg-backfill.d.ts +4 -4
  62. package/dist/core/kg-backfill.d.ts.map +1 -1
  63. package/dist/core/kg-backfill.js +1 -1
  64. package/dist/core/kg-backfill.js.map +1 -1
  65. package/dist/core/lesson-engine.d.ts +1 -0
  66. package/dist/core/lesson-engine.d.ts.map +1 -1
  67. package/dist/core/lesson-engine.js +1 -0
  68. package/dist/core/lesson-engine.js.map +1 -1
  69. package/dist/core/lifecycle.d.ts +4 -4
  70. package/dist/core/lifecycle.d.ts.map +1 -1
  71. package/dist/core/lifecycle.js +1 -1
  72. package/dist/core/lifecycle.js.map +1 -1
  73. package/dist/core/llm-client.d.ts.map +1 -1
  74. package/dist/core/llm-client.js +3 -6
  75. package/dist/core/llm-client.js.map +1 -1
  76. package/dist/core/llm-telemetry.d.ts +4 -4
  77. package/dist/core/llm-telemetry.d.ts.map +1 -1
  78. package/dist/core/llm-telemetry.js +1 -1
  79. package/dist/core/llm-telemetry.js.map +1 -1
  80. package/dist/core/llm-validator.d.ts +1 -0
  81. package/dist/core/llm-validator.d.ts.map +1 -1
  82. package/dist/core/llm-validator.js +33 -10
  83. package/dist/core/llm-validator.js.map +1 -1
  84. package/dist/core/memory-tool.d.ts.map +1 -1
  85. package/dist/core/memory-tool.js +4 -0
  86. package/dist/core/memory-tool.js.map +1 -1
  87. package/dist/core/operations.d.ts.map +1 -1
  88. package/dist/core/operations.js +23 -8
  89. package/dist/core/operations.js.map +1 -1
  90. package/dist/core/output-language.d.ts +6 -0
  91. package/dist/core/output-language.d.ts.map +1 -0
  92. package/dist/core/output-language.js +25 -0
  93. package/dist/core/output-language.js.map +1 -0
  94. package/dist/core/paths.d.ts +3 -0
  95. package/dist/core/paths.d.ts.map +1 -1
  96. package/dist/core/paths.js +67 -1
  97. package/dist/core/paths.js.map +1 -1
  98. package/dist/core/patterns.d.ts +2 -3
  99. package/dist/core/patterns.d.ts.map +1 -1
  100. package/dist/core/patterns.js +1 -5
  101. package/dist/core/patterns.js.map +1 -1
  102. package/dist/core/project-tags.d.ts +3 -3
  103. package/dist/core/project-tags.d.ts.map +1 -1
  104. package/dist/core/project-tags.js.map +1 -1
  105. package/dist/core/projects.d.ts +2 -2
  106. package/dist/core/projects.d.ts.map +1 -1
  107. package/dist/core/projects.js.map +1 -1
  108. package/dist/core/schema-export.d.ts.map +1 -1
  109. package/dist/core/schema-export.js +0 -34
  110. package/dist/core/schema-export.js.map +1 -1
  111. package/dist/core/serializer.d.ts.map +1 -1
  112. package/dist/core/serializer.js +45 -4
  113. package/dist/core/serializer.js.map +1 -1
  114. package/dist/core/stats.d.ts +2 -2
  115. package/dist/core/stats.d.ts.map +1 -1
  116. package/dist/core/stats.js.map +1 -1
  117. package/dist/core/transcript-extractor.d.ts +88 -0
  118. package/dist/core/transcript-extractor.d.ts.map +1 -0
  119. package/dist/core/transcript-extractor.js +417 -0
  120. package/dist/core/transcript-extractor.js.map +1 -0
  121. package/dist/core/transcript-source.d.ts +21 -0
  122. package/dist/core/transcript-source.d.ts.map +1 -0
  123. package/dist/core/transcript-source.js +142 -0
  124. package/dist/core/transcript-source.js.map +1 -0
  125. package/dist/core/types.d.ts +17 -7
  126. package/dist/core/types.d.ts.map +1 -1
  127. package/dist/core/types.js +2 -0
  128. package/dist/core/types.js.map +1 -1
  129. package/dist/db.d.ts +5 -5
  130. package/dist/db.d.ts.map +1 -1
  131. package/dist/db.js +165 -17
  132. package/dist/db.js.map +1 -1
  133. package/dist/knowledge-graph.d.ts +3 -2
  134. package/dist/knowledge-graph.d.ts.map +1 -1
  135. package/dist/knowledge-graph.js +19 -10
  136. package/dist/knowledge-graph.js.map +1 -1
  137. package/dist/mcp/server.js +2 -1
  138. package/dist/mcp/server.js.map +1 -1
  139. package/dist/skills-manifest.json +28 -33
  140. package/dist/storage/conflicts.d.ts +3 -3
  141. package/dist/storage/conflicts.d.ts.map +1 -1
  142. package/dist/storage/conflicts.js.map +1 -1
  143. package/dist/storage/fts-index.d.ts +4 -4
  144. package/dist/storage/fts-index.d.ts.map +1 -1
  145. package/dist/storage/fts-index.js.map +1 -1
  146. package/dist/storage/sqlite.d.ts +20 -0
  147. package/dist/storage/sqlite.d.ts.map +1 -0
  148. package/dist/storage/sqlite.js +64 -0
  149. package/dist/storage/sqlite.js.map +1 -0
  150. package/dist/storage/vector-index.d.ts +3 -0
  151. package/dist/storage/vector-index.d.ts.map +1 -0
  152. package/dist/storage/vector-index.js +7 -0
  153. package/dist/storage/vector-index.js.map +1 -0
  154. package/dist/transports/cli/cli.d.ts.map +1 -1
  155. package/dist/transports/cli/cli.js +278 -91
  156. package/dist/transports/cli/cli.js.map +1 -1
  157. package/dist/transports/http/retired-routes.d.ts.map +1 -1
  158. package/dist/transports/http/retired-routes.js +1 -0
  159. package/dist/transports/http/retired-routes.js.map +1 -1
  160. package/dist/transports/http/server.d.ts +7 -0
  161. package/dist/transports/http/server.d.ts.map +1 -1
  162. package/dist/transports/http/server.js +165 -79
  163. package/dist/transports/http/server.js.map +1 -1
  164. package/dist/transports/mcp/handlers.d.ts +4 -96
  165. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  166. package/dist/transports/mcp/handlers.js +26 -59
  167. package/dist/transports/mcp/handlers.js.map +1 -1
  168. package/dist/transports/schemas.d.ts +10 -30
  169. package/dist/transports/schemas.d.ts.map +1 -1
  170. package/dist/transports/schemas.js +5 -29
  171. package/dist/transports/schemas.js.map +1 -1
  172. package/hooks/hooks.json +0 -10
  173. package/package.json +6 -19
  174. package/scripts/hooks/_generated/core-paths.js +67 -1
  175. package/scripts/hooks/_generated/sqlite.js +71 -0
  176. package/scripts/hooks/_shared.js +286 -195
  177. package/scripts/hooks/post-commit.js +52 -10
  178. package/scripts/hooks/pre-compact.js +10 -7
  179. package/scripts/hooks/pre-edit-recall.js +4 -8
  180. package/scripts/hooks/session-start.js +95 -47
  181. package/scripts/hooks/session-summary.js +120 -52
  182. package/scripts/hooks/user-prompt-intent.js +3 -2
  183. package/skills/memesh/SKILL.md +3 -2
  184. package/dist/core/skill-usage-log.d.ts +0 -11
  185. package/dist/core/skill-usage-log.d.ts.map +0 -1
  186. package/dist/core/skill-usage-log.js +0 -125
  187. package/dist/core/skill-usage-log.js.map +0 -1
  188. package/dist/core/verifier.d.ts +0 -40
  189. package/dist/core/verifier.d.ts.map +0 -1
  190. package/dist/core/verifier.js +0 -206
  191. package/dist/core/verifier.js.map +0 -1
  192. package/dist/mcp/launcher.d.ts +0 -3
  193. package/dist/mcp/launcher.d.ts.map +0 -1
  194. package/dist/mcp/launcher.js +0 -37
  195. package/dist/mcp/launcher.js.map +0 -1
  196. package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
  197. package/scripts/postinstall-rebuild.mjs +0 -41
  198. package/skills/agentic-orchestration/SKILL.md +0 -399
@@ -3,15 +3,18 @@ import path from 'path';
3
3
  import { createHash } from 'crypto';
4
4
  import { createRequire } from 'module';
5
5
  import { pathToFileURL } from 'url';
6
- import { detectCapabilities, getConfigPath } from './config.js';
7
- import { embedText, isOnnxModelCached } from './embedder.js';
6
+ import { detectCapabilities, getConfigPath, isTranscriptMiningEnabled, readConfig } from './config.js';
7
+ import { embedText } from './embedder.js';
8
8
  import { probeProvider } from './llm-validator.js';
9
9
  import { openDatabase, closeDatabase, getPendingReindexInfo, isDatabaseOpen } from '../db.js';
10
10
  import { getUpdateCheck } from './version-check.js';
11
11
  import { getCurrentInstallChannel, getInstallChannelSupport } from './install-channel.js';
12
12
  import { getInstallRecord } from './install-id.js';
13
- import { getDbPath, memeshDir } from './paths.js';
13
+ import { getDbPath, memeshDir, getProjectName } from './paths.js';
14
+ import { lastTranscriptMineAt } from './transcript-source.js';
14
15
  import { UNSPACED_SCRIPT_GLOB_RUN3 } from '../storage/fts-index.js';
16
+ import { MemeshDatabase } from '../storage/sqlite.js';
17
+ import { AUTO_CAPTURE_TAG } from './types.js';
15
18
  const EMBEDDING_PROBE_TIMEOUT_MS = 15000;
16
19
  const EXPECTED_HOOK_TYPES = ['PreToolUse', 'SessionStart', 'PostToolUse', 'Stop', 'PreCompact'];
17
20
  const LOCALE_README_FILES = [
@@ -53,7 +56,7 @@ function inspectLocaleReadmeParity(packageRoot, existsSyncImpl, readFileSyncImpl
53
56
  englishCount = countH2Headings(readFileSyncImpl(englishPath, 'utf8'));
54
57
  }
55
58
  catch (err) {
56
- return createCheck('readme_locale_parity', 'README locale parity', 'warn', `Could not read README.md: ${err instanceof Error ? err.message : String(err)}`);
59
+ return createCheck('readme_locale_parity', 'README locale parity', 'warn', `Could not read README.md: ${err instanceof Error ? err.message : String(err)}`, undefined, { code: 'readme-parity.unreadable', params: { detail: err instanceof Error ? err.message : String(err) } });
57
60
  }
58
61
  const missing = [];
59
62
  const drift = [];
@@ -86,13 +89,13 @@ function inspectLocaleReadmeParity(packageRoot, existsSyncImpl, readFileSyncImpl
86
89
  .join(', ');
87
90
  parts.push(`H2 count drift (English=${englishCount}): ${driftDetail}`);
88
91
  }
89
- return createCheck('readme_locale_parity', 'README locale parity', 'warn', parts.join('; '), `Re-sync the listed READMEs against README.md so section structure matches (±${LOCALE_H2_TOLERANCE} H2 tolerated to absorb translation collapse).`);
92
+ return createCheck('readme_locale_parity', 'README locale parity', 'warn', parts.join('; '), `Re-sync the listed READMEs against README.md so section structure matches (±${LOCALE_H2_TOLERANCE} H2 tolerated to absorb translation collapse).`, { code: 'readme-parity.drift', params: { detail: parts.join('; '), tolerance: LOCALE_H2_TOLERANCE } });
90
93
  }
91
94
  function resolveDatabasePath() {
92
95
  return getDbPath();
93
96
  }
94
- function createCheck(id, label, status, summary, fix) {
95
- return { id, label, status, summary, fix };
97
+ function createCheck(id, label, status, summary, fix, i18n) {
98
+ return { id, label, status, summary, fix, code: i18n?.code, params: i18n?.params };
96
99
  }
97
100
  function createInfo(id, label, summary, fix) {
98
101
  return { id, label, status: 'pass', summary, fix, informational: true };
@@ -120,24 +123,32 @@ function inspectConfigFile(existsSyncImpl, readFileSyncImpl, getConfigPathImpl)
120
123
  }
121
124
  const parsed = parseJsonFile(configPath, readFileSyncImpl);
122
125
  if (!parsed.ok) {
123
- return createCheck('config', 'Config', 'fail', `Config file is invalid JSON at ${configPath}.`, `Fix or remove ${configPath}, then run \`memesh config list\` to confirm it loads cleanly.`);
126
+ return createCheck('config', 'Config', 'fail', `Config file is invalid JSON at ${configPath}.`, `Fix or remove ${configPath}, then run \`memesh config list\` to confirm it loads cleanly.`, { code: 'config.invalid-json', params: { path: configPath } });
124
127
  }
125
128
  return createCheck('config', 'Config', 'pass', `Config file is readable at ${configPath}.`);
126
129
  }
127
130
  function inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl) {
128
131
  const mcpPath = path.join(packageRoot, '.mcp.json');
129
132
  if (!existsSyncImpl(mcpPath)) {
130
- return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json is missing.', 'Restore `.mcp.json` from the package or reinstall MeMesh.');
133
+ return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json is missing.', 'Restore `.mcp.json` from the package or reinstall MeMesh.', { code: 'mcp-config.missing' });
131
134
  }
132
135
  const parsed = parseJsonFile(mcpPath, readFileSyncImpl);
133
136
  if (!parsed.ok) {
134
- return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json is not valid JSON.', `Fix ${mcpPath} so Claude Code can read the MCP server definition.`);
137
+ return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json is not valid JSON.', `Fix ${mcpPath} so Claude Code can read the MCP server definition.`, { code: 'mcp-config.invalid-json', params: { path: mcpPath } });
135
138
  }
136
139
  const server = parsed.value.mcpServers?.memesh;
137
140
  if (!server || typeof server.command !== 'string') {
138
- return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json does not define a usable `memesh` MCP server entry.', 'Reinstall MeMesh or restore the `mcpServers.memesh` entry in `.mcp.json`.');
141
+ return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json does not define a usable `memesh` MCP server entry.', 'Reinstall MeMesh or restore the `mcpServers.memesh` entry in `.mcp.json`.', { code: 'mcp-config.no-entry' });
139
142
  }
140
- return createCheck('mcp-config', 'MCP config', 'pass', '.mcp.json is present and defines the memesh MCP server.');
143
+ const args = Array.isArray(server.args) ? server.args : [];
144
+ const entry = typeof args[0] === 'string' ? args[0] : null;
145
+ if (entry) {
146
+ const resolved = path.resolve(entry.replaceAll('${CLAUDE_PLUGIN_ROOT}', packageRoot));
147
+ if (!existsSyncImpl(resolved)) {
148
+ return createCheck('mcp-config', 'MCP config', 'fail', `.mcp.json starts \`${entry}\`, and that file is not in this install — so every memesh MCP tool fails to start.`, 'Reinstall MeMesh; if you edited `.mcp.json` by hand, point it back at `${CLAUDE_PLUGIN_ROOT}/dist/mcp/server.js`.', { code: 'mcp-config.entry-missing', params: { entry, resolved } });
149
+ }
150
+ }
151
+ return createCheck('mcp-config', 'MCP config', 'pass', '.mcp.json is present, defines the memesh MCP server, and the script it starts exists.');
141
152
  }
142
153
  function extractHookScriptPaths(hooksConfig, packageRoot) {
143
154
  const hooks = hooksConfig.hooks;
@@ -160,32 +171,32 @@ function inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncI
160
171
  const hooksPath = path.join(packageRoot, 'hooks', 'hooks.json');
161
172
  if (!existsSyncImpl(hooksPath)) {
162
173
  return [
163
- createCheck('hooks-config', 'Hooks config', 'fail', 'hooks/hooks.json is missing.', 'Restore `hooks/hooks.json` from the package or reinstall MeMesh.'),
174
+ createCheck('hooks-config', 'Hooks config', 'fail', 'hooks/hooks.json is missing.', 'Restore `hooks/hooks.json` from the package or reinstall MeMesh.', { code: 'hooks-config.missing' }),
164
175
  ];
165
176
  }
166
177
  const parsed = parseJsonFile(hooksPath, readFileSyncImpl);
167
178
  if (!parsed.ok) {
168
179
  return [
169
- createCheck('hooks-config', 'Hooks config', 'fail', 'hooks/hooks.json is not valid JSON.', `Fix ${hooksPath} so Claude Code can load the hook definitions.`),
180
+ createCheck('hooks-config', 'Hooks config', 'fail', 'hooks/hooks.json is not valid JSON.', `Fix ${hooksPath} so Claude Code can load the hook definitions.`, { code: 'hooks-config.invalid-json', params: { path: hooksPath } }),
170
181
  ];
171
182
  }
172
183
  const hookTypes = Object.keys(parsed.value.hooks ?? {});
173
184
  const missingTypes = EXPECTED_HOOK_TYPES.filter((type) => !hookTypes.includes(type));
174
185
  const configCheck = missingTypes.length > 0
175
- ? createCheck('hooks-config', 'Hooks config', 'fail', `hooks/hooks.json is missing expected hook types: ${missingTypes.join(', ')}.`, 'Restore the shipped hook configuration or reinstall MeMesh.')
186
+ ? createCheck('hooks-config', 'Hooks config', 'fail', `hooks/hooks.json is missing expected hook types: ${missingTypes.join(', ')}.`, 'Restore the shipped hook configuration or reinstall MeMesh.', { code: 'hooks-config.missing-types', params: { types: missingTypes.join(', ') } })
176
187
  : createCheck('hooks-config', 'Hooks config', 'pass', `hooks/hooks.json is present with ${hookTypes.length} hook types configured.`);
177
188
  const scriptPaths = extractHookScriptPaths(parsed.value, packageRoot);
178
189
  if (scriptPaths.length === 0) {
179
190
  return [
180
191
  configCheck,
181
- createCheck('hook-scripts', 'Hook scripts', 'fail', 'hooks/hooks.json parsed, but yields zero hook script commands — hooks can never fire.', 'Restore the shipped hook configuration or reinstall MeMesh.'),
192
+ createCheck('hook-scripts', 'Hook scripts', 'fail', 'hooks/hooks.json parsed, but yields zero hook script commands — hooks can never fire.', 'Restore the shipped hook configuration or reinstall MeMesh.', { code: 'hook-scripts.none' }),
182
193
  ];
183
194
  }
184
195
  const missingScripts = scriptPaths.filter((scriptPath) => !existsSyncImpl(scriptPath));
185
196
  if (missingScripts.length > 0) {
186
197
  return [
187
198
  configCheck,
188
- createCheck('hook-scripts', 'Hook scripts', 'fail', `Missing hook scripts: ${missingScripts.map((entry) => path.relative(packageRoot, entry)).join(', ')}.`, 'Restore the missing files from the package or reinstall MeMesh.'),
199
+ createCheck('hook-scripts', 'Hook scripts', 'fail', `Missing hook scripts: ${missingScripts.map((entry) => path.relative(packageRoot, entry)).join(', ')}.`, 'Restore the missing files from the package or reinstall MeMesh.', { code: 'hook-scripts.missing', params: { files: missingScripts.map((entry) => path.relative(packageRoot, entry)).join(', ') } }),
189
200
  ];
190
201
  }
191
202
  if (platform !== 'win32') {
@@ -196,7 +207,7 @@ function inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncI
196
207
  if (nonExecutable.length > 0) {
197
208
  return [
198
209
  configCheck,
199
- createCheck('hook-scripts', 'Hook scripts', 'fail', `Hook scripts are not executable: ${nonExecutable.map((entry) => path.relative(packageRoot, entry)).join(', ')}.`, 'Run `npm run build` from the repo checkout or `chmod +x scripts/hooks/*.js` for a local repair.'),
210
+ createCheck('hook-scripts', 'Hook scripts', 'fail', `Hook scripts are not executable: ${nonExecutable.map((entry) => path.relative(packageRoot, entry)).join(', ')}.`, 'Run `npm run build` from the repo checkout or `chmod +x scripts/hooks/*.js` for a local repair.', { code: 'hook-scripts.not-executable', params: { files: nonExecutable.map((entry) => path.relative(packageRoot, entry)).join(', ') } }),
200
211
  ];
201
212
  }
202
213
  }
@@ -205,84 +216,170 @@ function inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncI
205
216
  createCheck('hook-scripts', 'Hook scripts', 'pass', `All ${scriptPaths.length} hook scripts are present${platform === 'win32' ? '' : ' and executable'}.`),
206
217
  ];
207
218
  }
208
- function inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir, packageRoot) {
219
+ function inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir, installChannel) {
209
220
  const markerPath = path.join(memeshDir, 'install-hooks.json');
210
221
  if (!existsSyncImpl(markerPath)) {
211
- if (packageRoot) {
212
- const pluginManifest = path.join(packageRoot, '.claude-plugin', 'plugin.json');
213
- if (existsSyncImpl(pluginManifest)) {
214
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'pass', 'Wired via Claude Code plugin runtime (.claude-plugin/plugin.json present). The install-hooks marker is not used on this install path.');
215
- }
222
+ if (installChannel === 'plugin-marketplace') {
223
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'pass', 'Wired via the Claude Code plugin runtime (this is a plugin-marketplace install). The install-hooks marker is not used on this install path.');
216
224
  }
217
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'warn', 'No install-hooks marker found. memesh\'s session-summary, pre-edit-recall, and other hooks may not be firing for Claude Code sessions the auto-capture / lesson-generation flow is silent without them.', 'Run `memesh install-hooks` to wire memesh into ~/.claude/settings.json (one-time setup). Then `memesh doctor` to confirm.');
225
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'warn', 'memesh is not connected to Claude Code yet, so nothing gets remembered automatically from your sessions.', 'Run `memesh install-hooks` once to connect it, then `memesh doctor` to confirm.', { code: 'hook-wiring.no-marker' });
218
226
  }
219
227
  const parsed = parseJsonFile(markerPath, readFileSyncImpl);
220
228
  if (!parsed.ok) {
221
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'warn', `install-hooks marker at ${markerPath} is unreadable.`, 'Re-run `memesh install-hooks` to refresh the marker.');
229
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'warn', `install-hooks marker at ${markerPath} is unreadable.`, 'Re-run `memesh install-hooks` to refresh the marker.', { code: 'hook-wiring.marker-unreadable', params: { path: markerPath } });
222
230
  }
223
231
  const marker = parsed.value;
224
232
  if (typeof marker.settings_path !== 'string') {
225
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'warn', 'install-hooks marker is malformed (missing settings_path).', 'Re-run `memesh install-hooks`.');
233
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'warn', 'install-hooks marker is malformed (missing settings_path).', 'Re-run `memesh install-hooks`.', { code: 'hook-wiring.marker-malformed' });
226
234
  }
227
235
  if (!existsSyncImpl(marker.settings_path)) {
228
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `Marker recorded settings at ${marker.settings_path} but the file no longer exists. Hooks are not wired.`, 'Re-run `memesh install-hooks`.');
236
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `Marker recorded settings at ${marker.settings_path} but the file no longer exists. Hooks are not wired.`, 'Re-run `memesh install-hooks`.', { code: 'hook-wiring.settings-missing', params: { path: String(marker.settings_path) } });
229
237
  }
230
238
  const settingsParsed = parseJsonFile(marker.settings_path, readFileSyncImpl);
231
239
  if (!settingsParsed.ok) {
232
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `${marker.settings_path} is no longer valid JSON.`, 'Restore from your ~/.claude backups or re-create with `memesh install-hooks`.');
240
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `${marker.settings_path} is no longer valid JSON.`, 'Restore from your ~/.claude backups or re-create with `memesh install-hooks`.', { code: 'hook-wiring.settings-invalid', params: { path: String(marker.settings_path) } });
233
241
  }
242
+ const CAPTURE_EVENTS = new Set(['Stop', 'PostToolUse', 'PreCompact']);
234
243
  const hooks = settingsParsed.value.hooks;
235
244
  let hasMemeshHook = false;
245
+ let hasCaptureHook = false;
246
+ let missingScript = null;
236
247
  if (hooks && typeof hooks === 'object') {
237
- for (const entries of Object.values(hooks)) {
248
+ for (const [event, entries] of Object.entries(hooks)) {
238
249
  if (!Array.isArray(entries))
239
250
  continue;
240
251
  for (const entry of entries) {
241
252
  const cmds = entry.hooks;
242
253
  if (!Array.isArray(cmds))
243
254
  continue;
244
- if (cmds.some((c) => c._memesh === true)) {
255
+ for (const c of cmds) {
256
+ const cmd = c;
257
+ if (cmd._memesh !== true)
258
+ continue;
245
259
  hasMemeshHook = true;
246
- break;
260
+ if (CAPTURE_EVENTS.has(event))
261
+ hasCaptureHook = true;
262
+ if (missingScript === null && typeof cmd.command === 'string'
263
+ && path.isAbsolute(cmd.command) && !existsSyncImpl(cmd.command)) {
264
+ missingScript = cmd.command;
265
+ }
247
266
  }
248
267
  }
249
- if (hasMemeshHook)
250
- break;
251
268
  }
252
269
  }
270
+ if (missingScript !== null) {
271
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `A wired memesh hook points at ${missingScript}, which no longer exists — the agent invokes a missing file on every matching event. This is usually residue from an upgrade that retired the hook.`, 'Run `memesh install-hooks` to re-wire (it now removes retired entries), then restart your agent.', { code: 'hook-wiring.script-missing', params: { path: missingScript } });
272
+ }
253
273
  if (!hasMemeshHook) {
254
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `Marker recorded a memesh install at ${marker.settings_path}, but no _memesh:true hook entries are present anymore. Settings drifted (manual edit?) or memesh was uninstalled out-of-band.`, 'Re-run `memesh install-hooks` to re-wire.');
274
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `Marker recorded a memesh install at ${marker.settings_path}, but no _memesh:true hook entries are present anymore. Settings drifted (manual edit?) or memesh was uninstalled out-of-band.`, 'Re-run `memesh install-hooks` to re-wire.', { code: 'hook-wiring.entries-removed', params: { path: String(marker.settings_path) } });
255
275
  }
256
276
  if (typeof marker.plugin_root === 'string' && !existsSyncImpl(marker.plugin_root)) {
257
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `Hook commands point at ${marker.plugin_root}, which no longer exists (likely after an npm-global path change).`, 'Re-run `memesh install-hooks` to refresh paths.');
277
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'fail', `Hook commands point at ${marker.plugin_root}, which no longer exists (likely after an npm-global path change).`, 'Re-run `memesh install-hooks` to refresh paths.', { code: 'hook-wiring.root-moved', params: { path: String(marker.plugin_root) } });
258
278
  }
259
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'pass', `Wired in ${marker.settings_path} (scope: ${marker.scope ?? 'user'}, version: ${marker.version ?? 'unknown'}).`);
279
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'pass', `Wired in ${marker.settings_path} (scope: ${marker.scope ?? 'user'}, version: ${marker.version ?? 'unknown'}).`, undefined, { params: { captureWired: hasCaptureHook ? 1 : 0 } });
260
280
  }
261
- function inspectHookActivity(openDatabaseImpl, closeDatabaseImpl, existsSyncImpl = fs.existsSync, statSyncImpl = fs.statSync) {
281
+ function inspectHookActivity(openDatabaseImpl, closeDatabaseImpl, existsSyncImpl = fs.existsSync, statSyncImpl = fs.statSync, wiringPresent = true) {
282
+ const TITLE = 'Hook activity';
283
+ const captureOff = autoCaptureOffSource();
284
+ if (captureOff === 'config') {
285
+ return createCheck('hook-activity', TITLE, 'pass', 'Automatic capture is turned off (config autoCapture: false) — deliberately, so hook silence is expected. Re-enable it to resume capturing sessions.', undefined, { code: 'hook-activity.disabled' });
286
+ }
287
+ if (captureOff === 'env') {
288
+ return createCheck('hook-activity', TITLE, 'pass', 'Automatic capture is turned off by MEMESH_AUTO_CAPTURE=false in this shell\'s environment. Doctor can only see its own environment — if your agent runs without this variable, capture there is unaffected.', undefined, { code: 'hook-activity.disabled-env' });
289
+ }
262
290
  let db = null;
263
291
  try {
264
292
  db = openDatabaseImpl();
265
- const row = db.prepare(`SELECT COUNT(*) as c FROM entities
266
- WHERE type IN ('session-insight', 'session-summary', 'commit', 'lesson_learned')
267
- AND created_at > datetime('now', '-24 hours')`).get();
268
- const count = row?.c ?? 0;
269
- if (count === 0) {
270
- const markerPath = path.join(memeshDir(), 'install-hooks.json');
271
- if (existsSyncImpl(markerPath)) {
272
- try {
273
- const ageMs = Date.now() - statSyncImpl(markerPath).mtimeMs;
274
- if (ageMs < 24 * 60 * 60 * 1000) {
275
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'pass', 'Hooks wired recently — no captureable sessions yet (this is normal for a fresh install).');
276
- }
293
+ const hookRunsTablePresent = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'hook_runs'").get();
294
+ const rows = hookRunsTablePresent
295
+ ? db.prepare(`SELECT hook, last_run_at FROM hook_runs`).all()
296
+ : [];
297
+ const captured = db.prepare(`SELECT COUNT(DISTINCT e.id) as c FROM entities e
298
+ JOIN tags t ON t.entity_id = e.id
299
+ WHERE t.tag = ?
300
+ AND e.created_at > datetime('now', '-24 hours')`).get(AUTO_CAPTURE_TAG)?.c ?? 0;
301
+ const since = db.prepare("SELECT value FROM memesh_metadata WHERE key = 'hook_runs_since'").get()?.value;
302
+ const measuringHours = since !== undefined ? hoursSince(since) : null;
303
+ const SKEW_TOLERANCE_H = 5 / 60;
304
+ const KNOWN_HOOKS = new Set(['session-summary', 'post-commit', 'pre-compact']);
305
+ const ages = new Map();
306
+ for (const r of rows) {
307
+ if (!KNOWN_HOOKS.has(r.hook))
308
+ continue;
309
+ const age = hoursSince(r.last_run_at);
310
+ ages.set(r.hook, age !== null && age >= -SKEW_TOLERANCE_H ? Math.max(age, 0) : null);
311
+ }
312
+ if (ages.size > 0) {
313
+ const capturedTail = captured > 0
314
+ ? `${captured} memor${captured === 1 ? 'y' : 'ies'} captured in the last 24h.`
315
+ : 'Nothing was worth saving in that time, which is normal — the loop still ran.';
316
+ const staleUnknown = (hook) => createCheck('hook-activity', TITLE, 'fail', `The ${hook} hook's last-run timestamp cannot be read (corrupt, or stamped by a machine with a wrong clock). ` +
317
+ 'Capture health is unknown, which is not the same as healthy.', 'End one work session, then re-run `memesh doctor` — a fresh run overwrites the bad timestamp.', { code: 'hook-activity.stale-unknown', params: { hook } });
318
+ const stale = (hook, age, status) => createCheck('hook-activity', TITLE, status, `The ${hook} hook last ran ${formatHoursAgo(Math.ceil(age))}. ` +
319
+ 'If you have worked since then, capture has stopped and nothing from those sessions was saved.', 'Restart your agent, then end one session and re-run `memesh doctor`. If it does not recover, run `memesh install-hooks`.', { code: 'hook-activity.stale', params: { hook, hours: Math.ceil(age) } });
320
+ const ssAge = ages.has('session-summary') ? ages.get('session-summary') : undefined;
321
+ if (ssAge === null)
322
+ return staleUnknown('session-summary');
323
+ if (ssAge !== undefined) {
324
+ if (ssAge <= 24) {
325
+ return createCheck('hook-activity', TITLE, 'pass', `The session-summary hook last ran ${formatHoursAgo(ssAge)} — auto-capture is alive. ${capturedTail}`);
326
+ }
327
+ if (ssAge <= 72)
328
+ return stale('session-summary', ssAge, 'warn');
329
+ const ccWrites = db.prepare(`SELECT COUNT(*) as c FROM entities
330
+ WHERE created_at > datetime('now', '-72 hours')
331
+ AND json_valid(metadata)
332
+ AND json_extract(metadata, '$.provenance.source_host') = 'claude-code'`).get()?.c ?? 0;
333
+ if (ccWrites > 0)
334
+ return stale('session-summary', ssAge, 'fail');
335
+ return createCheck('hook-activity', TITLE, 'warn', `The session-summary hook last ran ${formatHoursAgo(Math.ceil(ssAge))}, and no Claude Code writes landed in the last 3 days either. If this machine has moved to another agent (Codex / Gemini), this is expected; if you still use Claude Code here, capture has stopped.`, 'If you still use Claude Code on this machine: restart it, end one session, and re-run `memesh doctor`. If it does not recover, run `memesh install-hooks`.', { code: 'hook-activity.stale-unconfirmed', params: { hook: 'session-summary', hours: Math.ceil(ssAge) } });
336
+ }
337
+ const known = [...ages.entries()].filter((e) => e[1] !== null);
338
+ if (known.length === 0) {
339
+ const [hook] = [...ages.keys()].sort();
340
+ return staleUnknown(hook);
341
+ }
342
+ const [freshestHook, freshestAge] = known.sort((a, b) => a[1] - b[1])[0];
343
+ if (freshestAge <= 24) {
344
+ if (measuringHours !== null && measuringHours > 72) {
345
+ return createCheck('hook-activity', TITLE, 'warn', `The ${freshestHook} hook is stamping (last ran ${formatHoursAgo(freshestAge)}), but the session-summary hook has never run once in the ${Math.round(measuringHours)} hours since tracking began — session capture may be broken while commit capture hides it.`, 'End one work session and re-run `memesh doctor`. If session-summary still has not run, run `memesh install-hooks` and restart your agent.', { code: 'hook-activity.stop-silent', params: { hook: freshestHook, hours: Math.round(measuringHours) } });
277
346
  }
278
- catch { }
347
+ return createCheck('hook-activity', TITLE, 'pass', `The ${freshestHook} hook last ran ${formatHoursAgo(freshestAge)} — auto-capture is alive. ` +
348
+ `(session-summary has not stamped yet — expected until a session with real work ends.) ${capturedTail}`);
279
349
  }
280
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'warn', 'No memesh-attributed entities (session-insight, session-summary, commit, lesson_learned) in the past 24 hours. Hooks may be wired but not firing — likely a Claude Code restart is needed, or the agentic-loop guard is filtering all sessions.', 'Open a Claude Code session that uses ≥3 tools and ends naturally (not user_interrupt), or commit something. Then run `memesh doctor` again.');
350
+ return stale(freshestHook, freshestAge, 'warn');
351
+ }
352
+ if (since !== undefined && (measuringHours === null || measuringHours < 0)) {
353
+ return createCheck('hook-activity', TITLE, 'pass', 'The hook-run tracking marker is unreadable — it will be re-stamped automatically the next time the database is opened for writing (any session, commit, or memesh command does it). Tracking restarts then.');
281
354
  }
282
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'pass', `${count} memesh-attributed entit${count === 1 ? 'y' : 'ies'} captured in the past 24h — auto-capture loop is alive.`);
355
+ if (measuringHours === null || measuringHours < 24) {
356
+ return createCheck('hook-activity', TITLE, 'pass', 'Hook-run tracking has only just started on this database — the first work session will fill it in.');
357
+ }
358
+ const markerPath = path.join(memeshDir(), 'install-hooks.json');
359
+ if (existsSyncImpl(markerPath)) {
360
+ try {
361
+ if (Date.now() - statSyncImpl(markerPath).mtimeMs < 24 * 60 * 60 * 1000) {
362
+ return createCheck('hook-activity', TITLE, 'pass', 'Hooks were wired in the last day and no session has ended yet — normal for a fresh install.');
363
+ }
364
+ }
365
+ catch { }
366
+ }
367
+ const legacyCaptured = db.prepare(`SELECT COUNT(DISTINCT e.id) as c FROM entities e
368
+ JOIN tags t ON t.entity_id = e.id
369
+ WHERE t.tag = ?
370
+ AND e.created_at > ?`).get(AUTO_CAPTURE_TAG, since)?.c ?? 0;
371
+ if (legacyCaptured > 0) {
372
+ return createCheck('hook-activity', TITLE, 'warn', `No hook has stamped the heartbeat, but ${legacyCaptured} auto-capture memor${legacyCaptured === 1 ? 'y' : 'ies'} landed since tracking began — hooks from a version before heartbeat tracking are probably still running.`, 'Update the memesh hooks to the current version (plugin installs: `/plugin update memesh`; npm installs: `memesh install-hooks`), then restart your agent.', { code: 'hook-activity.never-ran-legacy', params: { captured: legacyCaptured } });
373
+ }
374
+ if (!wiringPresent) {
375
+ return createCheck('hook-activity', TITLE, 'warn', 'No capture hook has ever run — and no capture hook (Stop / PostToolUse / PreCompact) is confirmed wired on this machine, so there is nothing to run.', 'If you want automatic capture, run `memesh install-hooks`. If this install is MCP-only (Codex / Gemini), this is expected and safe to ignore.', { code: 'hook-activity.not-wired' });
376
+ }
377
+ return createCheck('hook-activity', TITLE, 'fail', `No capture hook has run since tracking began ${formatHoursAgo(measuringHours)}. ` +
378
+ 'Hook wiring is in place, so they should be executing and are not — nothing is being remembered.', 'Run `memesh doctor` after ending one work session. If this still says no hook has run, run `memesh install-hooks` and restart your agent.', { code: 'hook-activity.never-ran', params: { hours: Math.round(measuringHours) } });
283
379
  }
284
380
  catch (err) {
285
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'warn', `Could not query the database: ${err instanceof Error ? err.message : String(err)}`);
381
+ const detail = err instanceof Error ? err.message : String(err);
382
+ return createCheck('hook-activity', TITLE, 'fail', `Could not read hook activity from the database: ${detail}. Capture health is unknown, which is not the same as healthy.`, 'Run `memesh doctor --verbose` for the full error, and check that ~/.memesh is readable and not out of disk.', { code: 'hook-activity.query-failed', params: { detail } });
286
383
  }
287
384
  finally {
288
385
  try {
@@ -292,6 +389,44 @@ function inspectHookActivity(openDatabaseImpl, closeDatabaseImpl, existsSyncImpl
292
389
  catch { }
293
390
  }
294
391
  }
392
+ function autoCaptureOffSource() {
393
+ const envVal = process.env.MEMESH_AUTO_CAPTURE;
394
+ if (envVal === 'false')
395
+ return 'env';
396
+ if (envVal === 'true')
397
+ return null;
398
+ try {
399
+ return readConfig().autoCapture === false ? 'config' : null;
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ export function hoursSince(sqliteTimestamp) {
406
+ const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/.exec(sqliteTimestamp ?? '');
407
+ if (!m)
408
+ return null;
409
+ const then = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
410
+ if (!Number.isFinite(then))
411
+ return null;
412
+ const d = new Date(then);
413
+ if (d.getUTCFullYear() !== +m[1] || d.getUTCMonth() !== +m[2] - 1 || d.getUTCDate() !== +m[3]
414
+ || d.getUTCHours() !== +m[4] || d.getUTCMinutes() !== +m[5] || d.getUTCSeconds() !== +m[6])
415
+ return null;
416
+ return (Date.now() - then) / (60 * 60 * 1000);
417
+ }
418
+ function formatHoursAgo(hours) {
419
+ if (hours === null || !Number.isFinite(hours) || hours < 0)
420
+ return 'at an unknown time';
421
+ if (hours < 1)
422
+ return 'less than an hour ago';
423
+ if (hours < 48) {
424
+ const h = Math.round(hours);
425
+ return `${h} hour${h === 1 ? '' : 's'} ago`;
426
+ }
427
+ const d = Math.round(hours / 24);
428
+ return `${d} day${d === 1 ? '' : 's'} ago`;
429
+ }
295
430
  function defaultResolveShellMemesh() {
296
431
  try {
297
432
  const cmd = process.platform === 'win32' ? 'where' : 'which';
@@ -305,12 +440,23 @@ function defaultResolveShellMemesh() {
305
440
  return null;
306
441
  }
307
442
  }
443
+ const RUNTIME_TOO_OLD = 'memesh:node-sqlite-too-old';
308
444
  function defaultNativeBindingProbe(packageRoot) {
309
445
  try {
310
- const localRequire = createRequire(pathToFileURL(path.join(packageRoot, 'package.json')).href);
311
- const Database = localRequire('better-sqlite3');
312
- const probe = new Database(':memory:');
313
- probe.close();
446
+ const probe = new MemeshDatabase(':memory:', { allowExtension: true });
447
+ try {
448
+ if (typeof probe.enableLoadExtension !== 'function') {
449
+ return { ok: false, message: `${RUNTIME_TOO_OLD}: node:sqlite in ${process.version} has no enableLoadExtension` };
450
+ }
451
+ probe.enableLoadExtension(true);
452
+ const localRequire = createRequire(pathToFileURL(path.join(packageRoot, 'package.json')).href);
453
+ const sqliteVec = localRequire('sqlite-vec');
454
+ sqliteVec.load(probe);
455
+ probe.prepare('SELECT vec_version()').get();
456
+ }
457
+ finally {
458
+ probe.close();
459
+ }
314
460
  return { ok: true };
315
461
  }
316
462
  catch (err) {
@@ -356,7 +502,7 @@ export function inspectNodeRuntime(packageRoot, existsSyncImpl, readFileSyncImpl
356
502
  }
357
503
  if (!ok) {
358
504
  return createCheck('node-runtime', 'Node runtime', 'fail', `${facts} This package requires Node ${declared}, so this runtime is BELOW the ` +
359
- `supported floor. Native modules and hooks may fail in ways that look unrelated.`, `Upgrade Node to ${declared.replace(/^>=\s*/, '')} or newer, then run \`memesh doctor\` again.`);
505
+ `supported floor. Native modules and hooks may fail in ways that look unrelated.`, `Upgrade Node to ${declared.replace(/^>=\s*/, '')} or newer, then run \`memesh doctor\` again.`, { code: 'node-runtime.too-old', params: { detail: facts, required: declared.replace(/^>=\s*/, '') } });
360
506
  }
361
507
  return createCheck('node-runtime', 'Node runtime', 'pass', `${facts} Meets the required range ${declared}.`);
362
508
  }
@@ -372,20 +518,16 @@ export function hasBuiltInSqlite() {
372
518
  function inspectNativeBinding(packageRoot, _existsSyncImpl, probeImpl = defaultNativeBindingProbe) {
373
519
  const result = probeImpl(packageRoot);
374
520
  if (result.ok) {
375
- return createCheck('native-binding', 'Native SQLite binding', 'pass', 'better-sqlite3 native binding loads cleanly (Database probe succeeded).');
521
+ return createCheck('native-binding', 'SQLite and vector search', 'pass', 'node:sqlite opened a database and sqlite-vec loaded (probe succeeded).');
522
+ }
523
+ if (result.message.startsWith(RUNTIME_TOO_OLD)) {
524
+ return createCheck('native-binding', 'SQLite and vector search', 'fail', `The node:sqlite in this Node (${process.version}) is too old for memesh — it cannot load the vector-search extension. The complete version arrived in Node 22.13.`, 'Upgrade Node to 22.13 or newer, then re-run `memesh doctor`.', { code: 'native-binding.node-too-old', params: { version: process.version } });
376
525
  }
377
- const isMissingBinding = /bindings file|locate the bindings/i.test(result.message);
378
526
  const isMissingPackage = /MODULE_NOT_FOUND|Cannot find module/i.test(result.message);
379
527
  if (isMissingPackage) {
380
- return createCheck('native-binding', 'Native SQLite binding', 'fail', 'better-sqlite3 is not installed (Node could not resolve the module from any '
381
- + 'parent node_modules). Memesh hooks and database operations will not work.', `Run: npm install (in the directory that depends on @pcircle/memesh)`);
382
- }
383
- if (isMissingBinding) {
384
- return createCheck('native-binding', 'Native SQLite binding', 'fail', 'better-sqlite3 is installed but the native binding (.node file) is missing. '
385
- + 'Hooks will silently skip-and-exit, and auto-capture will NOT write any entities. '
386
- + 'This is the plugin-marketplace silent-dropout class of bug.', `Run: cd "${packageRoot}" && npm rebuild better-sqlite3 (or "npm install --omit=dev" for a clean reinstall)`);
528
+ return createCheck('native-binding', 'SQLite and vector search', 'warn', 'sqlite-vec is not installed, so memesh cannot search by meaning. Memories are still saved, and still found by keyword.', 'Run: npm install (in the directory that depends on @pcircle/memesh)', { code: 'native-binding.not-installed' });
387
529
  }
388
- return createCheck('native-binding', 'Native SQLite binding', 'fail', `better-sqlite3 failed to load: ${result.message}`, `Run: cd "${packageRoot}" && npm rebuild better-sqlite3`);
530
+ return createCheck('native-binding', 'SQLite and vector search', 'warn', `sqlite-vec could not be loaded: ${result.message}. Memories are still saved and found by keyword; only search by meaning is off.`, `Run: cd "${packageRoot}" && npm install --omit=dev`, { code: 'native-binding.load-failed', params: { detail: result.message, root: packageRoot } });
389
531
  }
390
532
  function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
391
533
  const shellPath = resolveShellMemeshImpl();
@@ -401,7 +543,7 @@ function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
401
543
  }
402
544
  if (installChannel === 'plugin-marketplace') {
403
545
  return createCheck('shell-cli', 'Shell CLI on PATH', 'warn', 'Plugin is installed but `memesh` is not on the shell PATH. Typing `memesh` in a regular terminal will report `command not found`. '
404
- + 'Claude Code MCP / hooks / `/memesh` skill still work — this only affects standalone shell usage and other MCP clients (Cursor, Cline, etc.).', 'Run `npm install -g @pcircle/memesh` to add the shell CLI. Both paths coexist; they share the same `~/.memesh/knowledge-graph.db`.');
546
+ + 'Claude Code MCP / hooks / `/memesh` skill still work — this only affects standalone shell usage and other MCP clients (Cursor, Cline, etc.).', 'Run `npm install -g @pcircle/memesh` to add the shell CLI. Both paths coexist; they share the same `~/.memesh/knowledge-graph.db`.', { code: 'shell-cli.not-on-path' });
405
547
  }
406
548
  return createCheck('shell-cli', 'Shell CLI on PATH', 'pass', shellPath
407
549
  ? `\`memesh\` resolves to ${shellPath}.`
@@ -410,14 +552,14 @@ function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
410
552
  function inspectDashboardArtifact(packageRoot, existsSyncImpl) {
411
553
  const dashboardPath = path.join(packageRoot, 'dashboard', 'dist', 'index.html');
412
554
  if (!existsSyncImpl(dashboardPath)) {
413
- return createCheck('dashboard', 'Dashboard artifact', 'fail', 'dashboard/dist/index.html is missing.', 'Build the dashboard with `cd dashboard && npm install && npm run build`, then run `npm run build` at the repo root if needed.');
555
+ return createCheck('dashboard', 'Dashboard artifact', 'fail', 'dashboard/dist/index.html is missing.', 'Build the dashboard with `cd dashboard && npm install && npm run build`, then run `npm run build` at the repo root if needed.', { code: 'dashboard.missing' });
414
556
  }
415
557
  return createCheck('dashboard', 'Dashboard artifact', 'pass', 'dashboard/dist/index.html is present.');
416
558
  }
417
559
  async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSupport) {
418
560
  const update = await getUpdateCheckImpl(packageVersion, { preferFresh: false });
419
561
  if (!update) {
420
- return createCheck('update-status', 'Update status', 'warn', 'No successful cached npm update check is available yet.', 'Run `memesh status` once while online to populate update status.');
562
+ return createCheck('update-status', 'Update status', 'warn', 'memesh has not been able to check for newer versions yet, so it cannot tell you whether an update exists.', 'Run `memesh status` once while connected to the internet — that stores the answer and this notice goes away.', { code: 'update-status.no-cache' });
421
563
  }
422
564
  if (update.currentVersionDeprecated && update.deprecationMessage) {
423
565
  const target = update.latestVersion && update.latestVersion !== packageVersion
@@ -439,10 +581,10 @@ async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSu
439
581
  else {
440
582
  fix = `Upgrade via your install method (see \`memesh status\`).`;
441
583
  }
442
- return createCheck('update-status', 'Update status', 'fail', `Installed version ${packageVersion} is DEPRECATED by maintainers: ${update.deprecationMessage}`, fix);
584
+ return createCheck('update-status', 'Update status', 'fail', `Installed version ${packageVersion} is DEPRECATED by maintainers: ${update.deprecationMessage}`, fix, { code: 'update-status.deprecated', params: { version: packageVersion, detail: update.deprecationMessage ?? '' } });
443
585
  }
444
586
  if (update.freshness === 'unavailable') {
445
- return createCheck('update-status', 'Update status', 'warn', 'No successful cached npm update check is available yet.', 'Run `memesh status` once while online to populate update status.');
587
+ return createCheck('update-status', 'Update status', 'warn', 'memesh has not been able to check for newer versions yet, so it cannot tell you whether an update exists.', 'Run `memesh status` once while connected to the internet — that stores the answer and this notice goes away.', { code: 'update-status.no-cache' });
446
588
  }
447
589
  if (update.checkSucceeded && update.lastError) {
448
590
  const hasUpdate = Boolean(update.updateAvailable && update.latestVersion);
@@ -465,11 +607,14 @@ async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSu
465
607
  upgradeHint = '';
466
608
  }
467
609
  const fix = `Run \`memesh status\` while online to retry the deprecation lookup${upgradeHint}.`;
468
- return createCheck('update-status', 'Update status', 'warn', summary, fix);
610
+ return createCheck('update-status', 'Update status', 'warn', summary, fix, {
611
+ code: 'update-status.deprecation-unknown',
612
+ params: { version: packageVersion, detail: update.lastError ?? '' },
613
+ });
469
614
  }
470
615
  if (update.updateAvailable && update.latestVersion) {
471
616
  if (packageVersion < update.latestVersion) {
472
- return createCheck('update-status', 'Update status', 'warn', `Update available: ${update.latestVersion} (current: ${packageVersion})`, `Run 'memesh update' to upgrade`);
617
+ return createCheck('update-status', 'Update status', 'warn', `Update available: ${update.latestVersion} (current: ${packageVersion})`, `Run 'memesh update' to upgrade`, { code: 'update-status.update-available', params: { latest: update.latestVersion, current: packageVersion } });
473
618
  }
474
619
  else {
475
620
  return createCheck('update-status', 'Update status', 'pass', `Running pre-release version (${packageVersion}), npm latest is ${update.latestVersion}`);
@@ -477,35 +622,37 @@ async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSu
477
622
  }
478
623
  return createCheck('update-status', 'Update status', update.freshness === 'stale' ? 'warn' : 'pass', `Version ${packageVersion} is current${update.freshness === 'stale' ? ', but cached update data is stale.' : '.'}`, update.freshness === 'stale'
479
624
  ? 'Run `memesh status` while online to refresh cached update metadata.'
625
+ : undefined, update.freshness === 'stale'
626
+ ? { code: 'update-status.stale', params: { version: packageVersion } }
480
627
  : undefined);
481
628
  }
482
629
  async function inspectHttpProbe(httpBaseUrl, fetchImpl) {
483
630
  try {
484
631
  const response = await fetchImpl(`${httpBaseUrl.replace(/\/$/, '')}/v1/health`);
485
632
  if (!response.ok) {
486
- return createCheck('http-probe', 'HTTP probe', 'warn', `HTTP server responded with ${response.status} at ${httpBaseUrl}.`, 'Run `memesh serve` and check the logs, then retry `memesh doctor --probe-http`.');
633
+ return createCheck('http-probe', 'HTTP probe', 'warn', `HTTP server responded with ${response.status} at ${httpBaseUrl}.`, 'Run `memesh serve` and check the logs, then retry `memesh doctor --probe-http`.', { code: 'http-probe.bad-status', params: { status: response.status, url: httpBaseUrl } });
487
634
  }
488
635
  return createCheck('http-probe', 'HTTP probe', 'pass', `HTTP server is reachable at ${httpBaseUrl}.`);
489
636
  }
490
637
  catch {
491
- return createCheck('http-probe', 'HTTP probe', 'warn', `No running HTTP server detected at ${httpBaseUrl}.`, 'Start the local server with `memesh serve` if you want dashboard and HTTP API verification.');
638
+ return createCheck('http-probe', 'HTTP probe', 'warn', `No running HTTP server detected at ${httpBaseUrl}.`, 'Start the local server with `memesh serve` if you want dashboard and HTTP API verification.', { code: 'http-probe.no-server', params: { url: httpBaseUrl } });
492
639
  }
493
640
  }
494
641
  function verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl) {
495
642
  const manifestPath = path.join(packageRoot, 'dist', 'skills-manifest.json');
496
643
  if (!existsSyncImpl(manifestPath)) {
497
- return createCheck('skills-manifest', 'Skills + hooks integrity', 'warn', 'No skills-manifest.json found. This is normal for source checkouts — packaged installs ship the manifest.', 'Run `npm run build` to regenerate, or reinstall via `npm install -g @pcircle/memesh`.');
644
+ return createCheck('skills-manifest', 'Skills + hooks integrity', 'warn', 'No skills-manifest.json found. This is normal for source checkouts — packaged installs ship the manifest.', 'Run `npm run build` to regenerate, or reinstall via `npm install -g @pcircle/memesh`.', { code: 'skills-manifest.missing-dev' });
498
645
  }
499
646
  let manifest;
500
647
  try {
501
648
  manifest = JSON.parse(readFileSyncImpl(manifestPath, 'utf8'));
502
649
  }
503
650
  catch (err) {
504
- return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', `skills-manifest.json is unreadable (${err instanceof Error ? err.message : 'parse error'}).`, 'Reinstall the package: `npm install -g @pcircle/memesh`. If the problem persists open an issue.');
651
+ return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', `skills-manifest.json is unreadable (${err instanceof Error ? err.message : 'parse error'}).`, 'Reinstall the package: `npm install -g @pcircle/memesh`. If the problem persists open an issue.', { code: 'skills-manifest.unreadable', params: { detail: err instanceof Error ? err.message : 'parse error' } });
505
652
  }
506
653
  const entries = manifest.entries ?? [];
507
654
  if (entries.length === 0) {
508
- return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', 'skills-manifest.json contains zero entries.', 'Reinstall the package: `npm install -g @pcircle/memesh`.');
655
+ return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', 'skills-manifest.json contains zero entries.', 'Reinstall the package: `npm install -g @pcircle/memesh`.', { code: 'skills-manifest.empty' });
509
656
  }
510
657
  const mismatches = [];
511
658
  const missing = [];
@@ -534,7 +681,7 @@ function verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl) {
534
681
  missing.length > 0 ? `${missing.length} missing: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ` (+${missing.length - 3} more)` : ''}` : null,
535
682
  mismatches.length > 0 ? `${mismatches.length} tampered: ${mismatches.slice(0, 3).join(', ')}${mismatches.length > 3 ? ` (+${mismatches.length - 3} more)` : ''}` : null,
536
683
  ].filter(Boolean).join('; ');
537
- return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', `Manifest verification failed: ${detail}.`, 'Reinstall the package: `npm install -g @pcircle/memesh`. If the problem reproduces on a fresh install, open a security issue at https://github.com/PCIRCLE-AI/memesh-llm-memory/security.');
684
+ return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', `Manifest verification failed: ${detail}.`, 'Reinstall the package: `npm install -g @pcircle/memesh`. If the problem reproduces on a fresh install, open a security issue at https://github.com/PCIRCLE-AI/memesh-llm-memory/security.', { code: 'skills-manifest.verify-failed', params: { detail } });
538
685
  }
539
686
  async function inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyncImpl) {
540
687
  const configPath = getConfigPathImpl();
@@ -545,13 +692,13 @@ async function inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyn
545
692
  const raw = readFileSyncImpl(configPath, 'utf8');
546
693
  const parsed = JSON.parse(raw);
547
694
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
548
- return createCheck('config_parse', 'Config parses', 'fail', `${configPath} parsed but is not a JSON object — every setting is being ignored.`, `Fix or remove ${configPath}, then re-run memesh doctor.`);
695
+ return createCheck('config_parse', 'Config parses', 'fail', `${configPath} parsed but is not a JSON object — every setting is being ignored.`, `Fix or remove ${configPath}, then re-run memesh doctor.`, { code: 'config-parse.not-object', params: { path: configPath } });
549
696
  }
550
697
  return createCheck('config_parse', 'Config parses', 'pass', `${configPath} is valid JSON and its settings are in effect.`);
551
698
  }
552
699
  catch (err) {
553
700
  const msg = err instanceof Error ? err.message : String(err);
554
- return createCheck('config_parse', 'Config parses', 'fail', `${configPath} could not be read or parsed (${msg}). Every setting in it — LLM provider, fallbacks, embedder — is being silently ignored right now.`, `Fix the JSON or remove the file to fall back to defaults: mv ${configPath} ${configPath}.bak`);
701
+ return createCheck('config_parse', 'Config parses', 'fail', `${configPath} could not be read or parsed (${msg}). Every setting in it — LLM provider, fallbacks, embedder — is being silently ignored right now.`, `Fix the JSON or remove the file to fall back to defaults: mv ${configPath} ${configPath}.bak`, { code: 'config-parse.unreadable', params: { path: configPath, detail: msg } });
555
702
  }
556
703
  }
557
704
  async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextImpl) {
@@ -559,13 +706,7 @@ async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextI
559
706
  return createInfo('embeddings_probe', 'Embeddings work', 'No neural embedder configured — recall runs on FTS5 keyword search alone. That is a supported mode, not a fault.');
560
707
  }
561
708
  if (!probeCapabilities) {
562
- const isLocal = capabilities.embeddings === 'onnx';
563
- if (!isLocal) {
564
- return createInfo('embeddings_probe', 'Embeddings work', `NOT VERIFIED. Config names "${capabilities.embeddings}", but generating a test embedding is a network call (billed on hosted providers) so it was not made — a revoked key or an unreachable host would look identical to a healthy setup here.`, 'Run: memesh doctor --probe (generates one test embedding to confirm)');
565
- }
566
- if (!isOnnxModelCached()) {
567
- return createInfo('embeddings_probe', 'Embeddings work', `NOT VERIFIED. Config names "onnx" but the model is not in the local cache yet, and probing would download ~90 MB — which a diagnostic command must not do on its own.`, 'Run: memesh doctor --probe (downloads the model once, then verifies)');
568
- }
709
+ return createInfo('embeddings_probe', 'Embeddings work', `NOT VERIFIED. Config names "${capabilities.embeddings}", but generating a test embedding is a network call (billed on hosted providers) so it was not made — a revoked key or an unreachable host would look identical to a healthy setup here.`, 'Run: memesh doctor --probe (generates one test embedding to confirm)');
569
710
  }
570
711
  let timer;
571
712
  try {
@@ -576,13 +717,13 @@ async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextI
576
717
  }),
577
718
  ]);
578
719
  if (!vector || vector.length === 0) {
579
- return createCheck('embeddings_probe', 'Embeddings work', 'warn', `Config selects "${capabilities.embeddings}" but generating a test embedding returned nothing. Semantic recall is degraded to FTS5-only; keyword search still works.`, 'Run: memesh doctor --probe for detail, or check network access to the embedding provider.');
720
+ return createCheck('embeddings_probe', 'Embeddings work', 'warn', `Config selects "${capabilities.embeddings}" but generating a test embedding returned nothing. Semantic recall is degraded to FTS5-only; keyword search still works.`, 'Run: memesh doctor --probe for detail, or check network access to the embedding provider.', { code: 'embeddings.empty', params: { provider: String(capabilities.embeddings) } });
580
721
  }
581
722
  return createCheck('embeddings_probe', 'Embeddings work', 'pass', `Generated a ${vector.length}-dim test embedding via "${capabilities.embeddings}".`);
582
723
  }
583
724
  catch (err) {
584
725
  const msg = err instanceof Error ? err.message : String(err);
585
- return createCheck('embeddings_probe', 'Embeddings work', 'warn', `Config selects "${capabilities.embeddings}" but the embedder threw (${msg}). Semantic recall is degraded to FTS5-only.`, 'Check the embedding provider is reachable, or set: memesh config set embedder.provider onnx');
726
+ return createCheck('embeddings_probe', 'Embeddings work', 'warn', `Config selects "${capabilities.embeddings}" but the embedder threw (${msg}). Semantic recall is degraded to FTS5-only.`, 'Check the embedding provider is reachable (e.g. run `ollama serve`), or remove embedder config to use keyword-only search.', { code: 'embeddings.threw', params: { provider: String(capabilities.embeddings), detail: msg } });
586
727
  }
587
728
  finally {
588
729
  clearTimeout(timer);
@@ -601,11 +742,11 @@ async function inspectLlmProbe(capabilities, probeCapabilities, probeProviderImp
601
742
  if (result.valid) {
602
743
  return createCheck('llm_probe', 'LLM reachable', 'pass', `${llm.provider} answered a live probe.`);
603
744
  }
604
- return createCheck('llm_probe', 'LLM reachable', 'fail', `${llm.provider} is configured but did not answer: ${result.error ?? 'unknown error'}. Every LLM-backed feature is silently doing nothing.`, 'Check the API key / host, then re-run: memesh doctor --probe');
745
+ return createCheck('llm_probe', 'LLM reachable', 'fail', `${llm.provider} is configured but did not answer: ${result.error ?? 'unknown error'}. Every LLM-backed feature is silently doing nothing.`, 'Check the API key / host, then re-run: memesh doctor --probe', { code: 'llm.unreachable', params: { provider: llm.provider, detail: result.error ?? 'unknown error' } });
605
746
  }
606
747
  catch (err) {
607
748
  const msg = err instanceof Error ? err.message : String(err);
608
- return createCheck('llm_probe', 'LLM reachable', 'fail', `${llm.provider} probe threw: ${msg}. Every LLM-backed feature is silently doing nothing.`, 'Check the API key / host, then re-run: memesh doctor --probe');
749
+ return createCheck('llm_probe', 'LLM reachable', 'fail', `${llm.provider} probe threw: ${msg}. Every LLM-backed feature is silently doing nothing.`, 'Check the API key / host, then re-run: memesh doctor --probe', { code: 'llm.threw', params: { provider: llm.provider, detail: msg } });
609
750
  }
610
751
  }
611
752
  function summarizeOverallStatus(checks) {
@@ -627,7 +768,7 @@ export async function runDoctor(options) {
627
768
  const installSupport = getInstallChannelSupportImpl(install);
628
769
  checks.push(createCheck('install-channel', 'Install method', install === 'unknown' ? 'warn' : 'pass', `Install method detected: ${installSupport.label}.`, install === 'unknown'
629
770
  ? 'If this is a source checkout, run MeMesh from the repo root. If this is a packaged install, reinstall with `npm install -g @pcircle/memesh`.'
630
- : undefined));
771
+ : undefined, install === 'unknown' ? { code: 'install-channel.unknown' } : undefined));
631
772
  const databasePath = resolveDatabasePath();
632
773
  const dbChecks = [];
633
774
  try {
@@ -647,12 +788,12 @@ export async function runDoctor(options) {
647
788
  dbChecks.push(createCheck('fts_segmentation', 'Keyword index segmentation', 'warn', `The keyword index holds ${unsegmented.c} unsegmented term(s), so some memories are only ` +
648
789
  `findable by their exact full text. This happens when an older build wrote to a database ` +
649
790
  `that a newer one had already migrated — the version marker only moves forward, so the ` +
650
- `automatic rebuild cannot notice. Re-run doctor after the rebuild: this count should be 0.`, `Run 'memesh reindex --fts' to rebuild the keyword index.`));
791
+ `automatic rebuild cannot notice. Re-run doctor after the rebuild: this count should be 0.`, `Run 'memesh reindex --fts' to rebuild the keyword index.`, { code: 'fts.unsegmented', params: { count: unsegmented.c } }));
651
792
  }
652
793
  }
653
794
  const pendingReindex = getPendingReindexInfo();
654
795
  if (pendingReindex) {
655
- dbChecks.push(createCheck('vector_index', 'Vector Index', 'warn', `Search index needs rebuilding (embedding configuration changed)`, `Run 'memesh reindex' to fix. This will restore full search functionality.`));
796
+ dbChecks.push(createCheck('vector_index', 'Vector Index', 'warn', `Search index needs rebuilding (embedding configuration changed)`, `Run 'memesh reindex' to fix. This will restore full search functionality.`, { code: 'vector-index.stale' }));
656
797
  }
657
798
  }
658
799
  catch (err) {
@@ -708,7 +849,7 @@ export async function runDoctor(options) {
708
849
  }
709
850
  }
710
851
  dbChecks.length = 0;
711
- dbChecks.push(createCheck('database', 'Database', 'fail', diagnosis, fix));
852
+ dbChecks.push(createCheck('database', 'Database', 'fail', diagnosis, fix, { code: 'database.broken', params: { detail: diagnosis } }));
712
853
  }
713
854
  finally {
714
855
  checks.push(...dbChecks);
@@ -721,8 +862,11 @@ export async function runDoctor(options) {
721
862
  checks.push(inspectConfigFile(existsSyncImpl, readFileSyncImpl, getConfigPathImpl));
722
863
  checks.push(inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl));
723
864
  checks.push(...inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncImpl, statSyncImpl));
724
- checks.push(inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir(), packageRoot));
725
- checks.push(inspectHookActivity(openDatabaseImpl, safeCloseDatabaseImpl, existsSyncImpl, statSyncImpl));
865
+ const wiring = inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir(), install);
866
+ checks.push(wiring);
867
+ const captureWired = wiring.status === 'pass'
868
+ && (wiring.params === undefined || wiring.params.captureWired === 1);
869
+ checks.push(inspectHookActivity(openDatabaseImpl, safeCloseDatabaseImpl, existsSyncImpl, statSyncImpl, captureWired));
726
870
  checks.push(inspectDashboardArtifact(packageRoot, existsSyncImpl));
727
871
  checks.push(inspectNodeRuntime(packageRoot, existsSyncImpl, readFileSyncImpl));
728
872
  checks.push(inspectNativeBinding(packageRoot, existsSyncImpl, nativeBindingProbeImpl));
@@ -730,6 +874,16 @@ export async function runDoctor(options) {
730
874
  checks.push(verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl));
731
875
  const capabilities = detectCapabilitiesImpl();
732
876
  checks.push(createInfo('capabilities', 'Capabilities (configured)', `Search level ${capabilities.searchLevel} (${capabilities.searchLevel === 1 ? 'Smart Mode' : 'Core'}); embeddings: ${capabilities.embeddings}; LLM: ${capabilities.llm ? `${capabilities.llm.provider} (${capabilities.llm.model ?? 'default'})` : 'not configured'}. Configured values only — see the probe rows below for what actually works.`));
877
+ if (!isTranscriptMiningEnabled()) {
878
+ checks.push(createInfo('transcript-mining', 'Scheduled transcript mining', 'Off (opt-in). memesh can mine this project\'s Claude Code session transcripts for decisions and lessons and STAGE them for your review. Turn it on with `memesh config set transcriptMining true`, then have a scheduler (cron/launchd) run `memesh dream run --from-transcripts --if-due` — it self-throttles and stages only, so nothing enters your graph without `dream accept`.'));
879
+ }
880
+ else {
881
+ const last = lastTranscriptMineAt(getProjectName(process.cwd()));
882
+ const when = last === null
883
+ ? 'not yet run for this project'
884
+ : `last mined ${((Date.now() - last) / 3600_000).toFixed(1)}h ago`;
885
+ checks.push(createInfo('transcript-mining', 'Scheduled transcript mining', `On for this project — ${when}. Have a scheduler run \`memesh dream run --from-transcripts --if-due\`; it mines when due (default every 24h) and stages proposals. Review the queue with \`memesh dream list\`.`));
886
+ }
733
887
  checks.push(await inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyncImpl));
734
888
  checks.push(await inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextImpl));
735
889
  checks.push(await inspectLlmProbe(capabilities, probeCapabilities, probeProviderImpl));