@pcircle/memesh 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +6 -4
  3. package/.mcp.json +1 -1
  4. package/AGENTS.md +95 -0
  5. package/README.de.md +130 -39
  6. package/README.md +188 -43
  7. package/README.zh-TW.md +133 -41
  8. package/dashboard/dist/index.html +9 -9
  9. package/dist/cli/view-live.js +3 -3
  10. package/dist/core/analytics.d.ts +3 -3
  11. package/dist/core/analytics.d.ts.map +1 -1
  12. package/dist/core/analytics.js.map +1 -1
  13. package/dist/core/auto-tagger.d.ts.map +1 -1
  14. package/dist/core/auto-tagger.js +4 -9
  15. package/dist/core/auto-tagger.js.map +1 -1
  16. package/dist/core/briefing.d.ts +8 -0
  17. package/dist/core/briefing.d.ts.map +1 -0
  18. package/dist/core/briefing.js +91 -0
  19. package/dist/core/briefing.js.map +1 -0
  20. package/dist/core/capture-flag.d.ts +5 -0
  21. package/dist/core/capture-flag.d.ts.map +1 -0
  22. package/dist/core/capture-flag.js +10 -0
  23. package/dist/core/capture-flag.js.map +1 -0
  24. package/dist/core/config.d.ts +0 -1
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js +2 -1
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/conflict-candidates.d.ts +20 -0
  29. package/dist/core/conflict-candidates.d.ts.map +1 -0
  30. package/dist/core/conflict-candidates.js +79 -0
  31. package/dist/core/conflict-candidates.js.map +1 -0
  32. package/dist/core/conflict-judge.d.ts +47 -0
  33. package/dist/core/conflict-judge.d.ts.map +1 -0
  34. package/dist/core/conflict-judge.js +189 -0
  35. package/dist/core/conflict-judge.js.map +1 -0
  36. package/dist/core/demo.d.ts +2 -2
  37. package/dist/core/demo.d.ts.map +1 -1
  38. package/dist/core/demo.js.map +1 -1
  39. package/dist/core/digest-validator.d.ts.map +1 -1
  40. package/dist/core/digest-validator.js +3 -5
  41. package/dist/core/digest-validator.js.map +1 -1
  42. package/dist/core/doctor.d.ts +3 -0
  43. package/dist/core/doctor.d.ts.map +1 -1
  44. package/dist/core/doctor.js +207 -85
  45. package/dist/core/doctor.js.map +1 -1
  46. package/dist/core/dreamer.d.ts +20 -9
  47. package/dist/core/dreamer.d.ts.map +1 -1
  48. package/dist/core/dreamer.js +416 -58
  49. package/dist/core/dreamer.js.map +1 -1
  50. package/dist/core/embedder.d.ts +7 -5
  51. package/dist/core/embedder.d.ts.map +1 -1
  52. package/dist/core/embedder.js +38 -9
  53. package/dist/core/embedder.js.map +1 -1
  54. package/dist/core/extractor.d.ts.map +1 -1
  55. package/dist/core/extractor.js +2 -1
  56. package/dist/core/extractor.js.map +1 -1
  57. package/dist/core/failure-analyzer.d.ts.map +1 -1
  58. package/dist/core/failure-analyzer.js +7 -12
  59. package/dist/core/failure-analyzer.js.map +1 -1
  60. package/dist/core/graph.d.ts +4 -4
  61. package/dist/core/graph.d.ts.map +1 -1
  62. package/dist/core/graph.js.map +1 -1
  63. package/dist/core/install-channel.d.ts +1 -1
  64. package/dist/core/install-channel.d.ts.map +1 -1
  65. package/dist/core/install-channel.js +16 -5
  66. package/dist/core/install-channel.js.map +1 -1
  67. package/dist/core/install-hooks.d.ts +6 -0
  68. package/dist/core/install-hooks.d.ts.map +1 -1
  69. package/dist/core/install-hooks.js +0 -0
  70. package/dist/core/install-hooks.js.map +1 -1
  71. package/dist/core/json-utils.d.ts +1 -0
  72. package/dist/core/json-utils.d.ts.map +1 -1
  73. package/dist/core/json-utils.js +19 -10
  74. package/dist/core/json-utils.js.map +1 -1
  75. package/dist/core/kg-backfill.d.ts +3 -4
  76. package/dist/core/kg-backfill.d.ts.map +1 -1
  77. package/dist/core/kg-backfill.js +1 -4
  78. package/dist/core/kg-backfill.js.map +1 -1
  79. package/dist/core/lesson-engine.d.ts +1 -0
  80. package/dist/core/lesson-engine.d.ts.map +1 -1
  81. package/dist/core/lesson-engine.js +1 -0
  82. package/dist/core/lesson-engine.js.map +1 -1
  83. package/dist/core/lifecycle.d.ts +4 -4
  84. package/dist/core/lifecycle.d.ts.map +1 -1
  85. package/dist/core/lifecycle.js +15 -22
  86. package/dist/core/lifecycle.js.map +1 -1
  87. package/dist/core/llm-client.d.ts.map +1 -1
  88. package/dist/core/llm-client.js +3 -6
  89. package/dist/core/llm-client.js.map +1 -1
  90. package/dist/core/llm-telemetry.d.ts +4 -4
  91. package/dist/core/llm-telemetry.d.ts.map +1 -1
  92. package/dist/core/llm-telemetry.js +1 -1
  93. package/dist/core/llm-telemetry.js.map +1 -1
  94. package/dist/core/memory-tool.d.ts.map +1 -1
  95. package/dist/core/memory-tool.js +8 -4
  96. package/dist/core/memory-tool.js.map +1 -1
  97. package/dist/core/operations.d.ts.map +1 -1
  98. package/dist/core/operations.js +41 -17
  99. package/dist/core/operations.js.map +1 -1
  100. package/dist/core/paths.d.ts +3 -0
  101. package/dist/core/paths.d.ts.map +1 -1
  102. package/dist/core/paths.js +67 -1
  103. package/dist/core/paths.js.map +1 -1
  104. package/dist/core/patterns.d.ts +2 -2
  105. package/dist/core/patterns.d.ts.map +1 -1
  106. package/dist/core/patterns.js.map +1 -1
  107. package/dist/core/project-tags.d.ts +3 -3
  108. package/dist/core/project-tags.d.ts.map +1 -1
  109. package/dist/core/project-tags.js.map +1 -1
  110. package/dist/core/projects.d.ts +2 -2
  111. package/dist/core/projects.d.ts.map +1 -1
  112. package/dist/core/projects.js.map +1 -1
  113. package/dist/core/prompt-safety.d.ts +1 -0
  114. package/dist/core/prompt-safety.d.ts.map +1 -1
  115. package/dist/core/prompt-safety.js +7 -0
  116. package/dist/core/prompt-safety.js.map +1 -1
  117. package/dist/core/schema-export.d.ts.map +1 -1
  118. package/dist/core/schema-export.js +27 -30
  119. package/dist/core/schema-export.js.map +1 -1
  120. package/dist/core/serializer.d.ts.map +1 -1
  121. package/dist/core/serializer.js +45 -4
  122. package/dist/core/serializer.js.map +1 -1
  123. package/dist/core/setup.d.ts +29 -0
  124. package/dist/core/setup.d.ts.map +1 -0
  125. package/dist/core/setup.js +127 -0
  126. package/dist/core/setup.js.map +1 -0
  127. package/dist/core/stats.d.ts +2 -2
  128. package/dist/core/stats.d.ts.map +1 -1
  129. package/dist/core/stats.js.map +1 -1
  130. package/dist/core/task-state-store.d.ts +17 -0
  131. package/dist/core/task-state-store.d.ts.map +1 -0
  132. package/dist/core/task-state-store.js +45 -0
  133. package/dist/core/task-state-store.js.map +1 -0
  134. package/dist/core/task-state.d.ts +19 -0
  135. package/dist/core/task-state.d.ts.map +1 -0
  136. package/dist/core/task-state.js +91 -0
  137. package/dist/core/task-state.js.map +1 -0
  138. package/dist/core/time-utils.d.ts +2 -0
  139. package/dist/core/time-utils.d.ts.map +1 -0
  140. package/dist/core/time-utils.js +14 -0
  141. package/dist/core/time-utils.js.map +1 -0
  142. package/dist/core/title.d.ts +5 -0
  143. package/dist/core/title.d.ts.map +1 -0
  144. package/dist/core/title.js +14 -0
  145. package/dist/core/title.js.map +1 -0
  146. package/dist/core/transcript-extractor.d.ts +5 -6
  147. package/dist/core/transcript-extractor.d.ts.map +1 -1
  148. package/dist/core/transcript-extractor.js +4 -24
  149. package/dist/core/transcript-extractor.js.map +1 -1
  150. package/dist/core/transcript-source.d.ts.map +1 -1
  151. package/dist/core/transcript-source.js +2 -3
  152. package/dist/core/transcript-source.js.map +1 -1
  153. package/dist/core/types.d.ts +21 -7
  154. package/dist/core/types.d.ts.map +1 -1
  155. package/dist/core/types.js +2 -0
  156. package/dist/core/types.js.map +1 -1
  157. package/dist/core/work-topology.d.ts +33 -0
  158. package/dist/core/work-topology.d.ts.map +1 -0
  159. package/dist/core/work-topology.js +183 -0
  160. package/dist/core/work-topology.js.map +1 -0
  161. package/dist/db.d.ts +5 -10
  162. package/dist/db.d.ts.map +1 -1
  163. package/dist/db.js +194 -196
  164. package/dist/db.js.map +1 -1
  165. package/dist/knowledge-graph.d.ts +4 -2
  166. package/dist/knowledge-graph.d.ts.map +1 -1
  167. package/dist/knowledge-graph.js +68 -49
  168. package/dist/knowledge-graph.js.map +1 -1
  169. package/dist/mcp/server.js +2 -1
  170. package/dist/mcp/server.js.map +1 -1
  171. package/dist/skills-manifest.json +61 -36
  172. package/dist/storage/conflicts.d.ts +3 -3
  173. package/dist/storage/conflicts.d.ts.map +1 -1
  174. package/dist/storage/conflicts.js +2 -7
  175. package/dist/storage/conflicts.js.map +1 -1
  176. package/dist/storage/fts-index.d.ts +6 -4
  177. package/dist/storage/fts-index.d.ts.map +1 -1
  178. package/dist/storage/fts-index.js +16 -4
  179. package/dist/storage/fts-index.js.map +1 -1
  180. package/dist/storage/schema.d.ts +20 -0
  181. package/dist/storage/schema.d.ts.map +1 -0
  182. package/dist/storage/schema.js +274 -0
  183. package/dist/storage/schema.js.map +1 -0
  184. package/dist/storage/sqlite.d.ts +20 -0
  185. package/dist/storage/sqlite.d.ts.map +1 -0
  186. package/dist/storage/sqlite.js +64 -0
  187. package/dist/storage/sqlite.js.map +1 -0
  188. package/dist/storage/vector-index.d.ts +3 -0
  189. package/dist/storage/vector-index.d.ts.map +1 -0
  190. package/dist/storage/vector-index.js +7 -0
  191. package/dist/storage/vector-index.js.map +1 -0
  192. package/dist/transports/cli/cli.d.ts +1 -4
  193. package/dist/transports/cli/cli.d.ts.map +1 -1
  194. package/dist/transports/cli/cli.js +494 -76
  195. package/dist/transports/cli/cli.js.map +1 -1
  196. package/dist/transports/http/retired-routes.d.ts.map +1 -1
  197. package/dist/transports/http/retired-routes.js +1 -0
  198. package/dist/transports/http/retired-routes.js.map +1 -1
  199. package/dist/transports/http/server.d.ts.map +1 -1
  200. package/dist/transports/http/server.js +243 -323
  201. package/dist/transports/http/server.js.map +1 -1
  202. package/dist/transports/mcp/handlers.d.ts +48 -94
  203. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  204. package/dist/transports/mcp/handlers.js +77 -56
  205. package/dist/transports/mcp/handlers.js.map +1 -1
  206. package/dist/transports/schemas.d.ts +31 -40
  207. package/dist/transports/schemas.d.ts.map +1 -1
  208. package/dist/transports/schemas.js +31 -37
  209. package/dist/transports/schemas.js.map +1 -1
  210. package/hooks/hooks.json +0 -10
  211. package/llms-install.md +138 -0
  212. package/package.json +19 -18
  213. package/scripts/hooks/_generated/capture-flag.js +17 -0
  214. package/scripts/hooks/_generated/core-paths.js +67 -1
  215. package/scripts/hooks/_generated/fts-index.js +16 -4
  216. package/scripts/hooks/_generated/schema.js +281 -0
  217. package/scripts/hooks/_generated/sqlite.js +71 -0
  218. package/scripts/hooks/_generated/task-state.js +98 -0
  219. package/scripts/hooks/_generated/time-utils.js +21 -0
  220. package/scripts/hooks/_generated/title.js +21 -0
  221. package/scripts/hooks/_generated/work-topology.js +190 -0
  222. package/scripts/hooks/_shared.js +269 -534
  223. package/scripts/hooks/post-commit.js +55 -10
  224. package/scripts/hooks/pre-compact.js +22 -7
  225. package/scripts/hooks/pre-edit-recall.js +9 -11
  226. package/scripts/hooks/session-start.js +230 -106
  227. package/scripts/hooks/session-summary.js +176 -73
  228. package/scripts/hooks/user-prompt-intent.js +3 -2
  229. package/skills/memesh/SKILL.md +97 -77
  230. package/README.es.md +0 -470
  231. package/README.fr.md +0 -462
  232. package/README.ja.md +0 -470
  233. package/README.ko.md +0 -470
  234. package/README.pt.md +0 -462
  235. package/README.th.md +0 -463
  236. package/README.vi.md +0 -462
  237. package/README.zh-CN.md +0 -469
  238. package/dist/cli/view.d.ts +0 -3
  239. package/dist/cli/view.d.ts.map +0 -1
  240. package/dist/cli/view.js +0 -523
  241. package/dist/cli/view.js.map +0 -1
  242. package/dist/core/skill-usage-log.d.ts +0 -11
  243. package/dist/core/skill-usage-log.d.ts.map +0 -1
  244. package/dist/core/skill-usage-log.js +0 -125
  245. package/dist/core/skill-usage-log.js.map +0 -1
  246. package/dist/core/verifier.d.ts +0 -40
  247. package/dist/core/verifier.d.ts.map +0 -1
  248. package/dist/core/verifier.js +0 -206
  249. package/dist/core/verifier.js.map +0 -1
  250. package/dist/mcp/launcher.d.ts +0 -3
  251. package/dist/mcp/launcher.d.ts.map +0 -1
  252. package/dist/mcp/launcher.js +0 -37
  253. package/dist/mcp/launcher.js.map +0 -1
  254. package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
  255. package/scripts/postinstall-rebuild.mjs +0 -41
  256. package/skills/agentic-orchestration/SKILL.md +0 -399
@@ -3,7 +3,7 @@ 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, isTranscriptMiningEnabled } from './config.js';
6
+ import { detectCapabilities, getConfigPath, isTranscriptMiningEnabled, readConfig } from './config.js';
7
7
  import { embedText } from './embedder.js';
8
8
  import { probeProvider } from './llm-validator.js';
9
9
  import { openDatabase, closeDatabase, getPendingReindexInfo, isDatabaseOpen } from '../db.js';
@@ -11,20 +11,17 @@ import { getUpdateCheck } from './version-check.js';
11
11
  import { getCurrentInstallChannel, getInstallChannelSupport } from './install-channel.js';
12
12
  import { getInstallRecord } from './install-id.js';
13
13
  import { getDbPath, memeshDir, getProjectName } from './paths.js';
14
+ import { detectPluginRuntime } from './install-hooks.js';
14
15
  import { lastTranscriptMineAt } from './transcript-source.js';
15
16
  import { UNSPACED_SCRIPT_GLOB_RUN3 } from '../storage/fts-index.js';
17
+ import { MemeshDatabase } from '../storage/sqlite.js';
18
+ import { AUTO_CAPTURE_TAG } from './types.js';
19
+ import { parseSqliteUtcMs } from './time-utils.js';
20
+ import { autoCaptureDecision } from './capture-flag.js';
16
21
  const EMBEDDING_PROBE_TIMEOUT_MS = 15000;
17
22
  const EXPECTED_HOOK_TYPES = ['PreToolUse', 'SessionStart', 'PostToolUse', 'Stop', 'PreCompact'];
18
23
  const LOCALE_README_FILES = [
19
24
  'README.de.md',
20
- 'README.es.md',
21
- 'README.fr.md',
22
- 'README.ja.md',
23
- 'README.ko.md',
24
- 'README.pt.md',
25
- 'README.th.md',
26
- 'README.vi.md',
27
- 'README.zh-CN.md',
28
25
  'README.zh-TW.md',
29
26
  ];
30
27
  const LOCALE_H2_TOLERANCE = 1;
@@ -92,8 +89,8 @@ function inspectLocaleReadmeParity(packageRoot, existsSyncImpl, readFileSyncImpl
92
89
  function resolveDatabasePath() {
93
90
  return getDbPath();
94
91
  }
95
- function createCheck(id, label, status, summary, fix, i18n) {
96
- return { id, label, status, summary, fix, code: i18n?.code, params: i18n?.params };
92
+ function createCheck(id, label, status, summary, fix, i18n, fixId) {
93
+ return { id, label, status, summary, fix, code: i18n?.code, params: i18n?.params, fixId };
97
94
  }
98
95
  function createInfo(id, label, summary, fix) {
99
96
  return { id, label, status: 'pass', summary, fix, informational: true };
@@ -119,11 +116,18 @@ function inspectConfigFile(existsSyncImpl, readFileSyncImpl, getConfigPathImpl)
119
116
  if (!existsSyncImpl(configPath)) {
120
117
  return createCheck('config', 'Config', 'pass', `No config file yet (${configPath}). MeMesh will run in Core mode until you configure Smart Mode.`, 'Optional: run `memesh config list` or set an LLM with `memesh config set llm.provider anthropic`.');
121
118
  }
122
- const parsed = parseJsonFile(configPath, readFileSyncImpl);
123
- if (!parsed.ok) {
124
- 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 } });
119
+ try {
120
+ const raw = readFileSyncImpl(configPath, 'utf8');
121
+ const parsed = JSON.parse(raw);
122
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
123
+ return createCheck('config', 'Config', '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 } });
124
+ }
125
+ return createCheck('config', 'Config', 'pass', `${configPath} is valid JSON and its settings are in effect.`);
126
+ }
127
+ catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ return createCheck('config', 'Config', '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 } });
125
130
  }
126
- return createCheck('config', 'Config', 'pass', `Config file is readable at ${configPath}.`);
127
131
  }
128
132
  function inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl) {
129
133
  const mcpPath = path.join(packageRoot, '.mcp.json');
@@ -138,7 +142,15 @@ function inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl) {
138
142
  if (!server || typeof server.command !== 'string') {
139
143
  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' });
140
144
  }
141
- return createCheck('mcp-config', 'MCP config', 'pass', '.mcp.json is present and defines the memesh MCP server.');
145
+ const args = Array.isArray(server.args) ? server.args : [];
146
+ const entry = typeof args[0] === 'string' ? args[0] : null;
147
+ if (entry) {
148
+ const resolved = path.resolve(entry.replaceAll('${CLAUDE_PLUGIN_ROOT}', packageRoot));
149
+ if (!existsSyncImpl(resolved)) {
150
+ 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 } });
151
+ }
152
+ }
153
+ return createCheck('mcp-config', 'MCP config', 'pass', '.mcp.json is present, defines the memesh MCP server, and the script it starts exists.');
142
154
  }
143
155
  function extractHookScriptPaths(hooksConfig, packageRoot) {
144
156
  const hooks = hooksConfig.hooks;
@@ -206,16 +218,16 @@ function inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncI
206
218
  createCheck('hook-scripts', 'Hook scripts', 'pass', `All ${scriptPaths.length} hook scripts are present${platform === 'win32' ? '' : ' and executable'}.`),
207
219
  ];
208
220
  }
209
- function inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir, packageRoot) {
221
+ function inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir, installChannel, installedPluginsPath) {
210
222
  const markerPath = path.join(memeshDir, 'install-hooks.json');
211
223
  if (!existsSyncImpl(markerPath)) {
212
- if (packageRoot) {
213
- const pluginManifest = path.join(packageRoot, '.claude-plugin', 'plugin.json');
214
- if (existsSyncImpl(pluginManifest)) {
215
- 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.');
216
- }
224
+ if (installChannel === 'plugin-marketplace') {
225
+ 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.');
217
226
  }
218
- 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' });
227
+ if (detectPluginRuntime(installedPluginsPath)) {
228
+ return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'pass', 'Wired via the Claude Code plugin runtime (found in installed_plugins.json). This copy is not the one doing the capturing — the plugin manages the hooks.');
229
+ }
230
+ 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' }, 'install-hooks');
219
231
  }
220
232
  const parsed = parseJsonFile(markerPath, readFileSyncImpl);
221
233
  if (!parsed.ok) {
@@ -232,58 +244,147 @@ function inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir, packageR
232
244
  if (!settingsParsed.ok) {
233
245
  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) } });
234
246
  }
247
+ const CAPTURE_EVENTS = new Set(['Stop', 'PostToolUse', 'PreCompact']);
235
248
  const hooks = settingsParsed.value.hooks;
236
249
  let hasMemeshHook = false;
250
+ let hasCaptureHook = false;
251
+ let missingScript = null;
237
252
  if (hooks && typeof hooks === 'object') {
238
- for (const entries of Object.values(hooks)) {
253
+ for (const [event, entries] of Object.entries(hooks)) {
239
254
  if (!Array.isArray(entries))
240
255
  continue;
241
256
  for (const entry of entries) {
242
257
  const cmds = entry.hooks;
243
258
  if (!Array.isArray(cmds))
244
259
  continue;
245
- if (cmds.some((c) => c._memesh === true)) {
260
+ for (const c of cmds) {
261
+ const cmd = c;
262
+ if (cmd._memesh !== true)
263
+ continue;
246
264
  hasMemeshHook = true;
247
- break;
265
+ if (CAPTURE_EVENTS.has(event))
266
+ hasCaptureHook = true;
267
+ if (missingScript === null && typeof cmd.command === 'string'
268
+ && path.isAbsolute(cmd.command) && !existsSyncImpl(cmd.command)) {
269
+ missingScript = cmd.command;
270
+ }
248
271
  }
249
272
  }
250
- if (hasMemeshHook)
251
- break;
252
273
  }
253
274
  }
275
+ if (missingScript !== null) {
276
+ 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 } });
277
+ }
254
278
  if (!hasMemeshHook) {
255
279
  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) } });
256
280
  }
257
281
  if (typeof marker.plugin_root === 'string' && !existsSyncImpl(marker.plugin_root)) {
258
282
  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) } });
259
283
  }
260
- return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'pass', `Wired in ${marker.settings_path} (scope: ${marker.scope ?? 'user'}, version: ${marker.version ?? 'unknown'}).`);
284
+ 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 } });
261
285
  }
262
- function inspectHookActivity(openDatabaseImpl, closeDatabaseImpl, existsSyncImpl = fs.existsSync, statSyncImpl = fs.statSync) {
286
+ function inspectHookActivity(openDatabaseImpl, closeDatabaseImpl, existsSyncImpl = fs.existsSync, statSyncImpl = fs.statSync, wiringPresent = true) {
287
+ const TITLE = 'Hook activity';
288
+ const captureOff = autoCaptureOffSource();
289
+ if (captureOff === 'config') {
290
+ 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' });
291
+ }
292
+ if (captureOff === 'env') {
293
+ 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' });
294
+ }
263
295
  let db = null;
264
296
  try {
265
297
  db = openDatabaseImpl();
266
- const row = db.prepare(`SELECT COUNT(*) as c FROM entities
267
- WHERE type IN ('session-insight', 'session-summary', 'commit', 'lesson_learned')
268
- AND created_at > datetime('now', '-24 hours')`).get();
269
- const count = row?.c ?? 0;
270
- if (count === 0) {
271
- const markerPath = path.join(memeshDir(), 'install-hooks.json');
272
- if (existsSyncImpl(markerPath)) {
273
- try {
274
- const ageMs = Date.now() - statSyncImpl(markerPath).mtimeMs;
275
- if (ageMs < 24 * 60 * 60 * 1000) {
276
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'pass', 'Hooks wired recently — no captureable sessions yet (this is normal for a fresh install).');
277
- }
298
+ const hookRunsTablePresent = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'hook_runs'").get();
299
+ const rows = hookRunsTablePresent
300
+ ? db.prepare(`SELECT hook, last_run_at FROM hook_runs`).all()
301
+ : [];
302
+ const captured = db.prepare(`SELECT COUNT(DISTINCT e.id) as c FROM entities e
303
+ JOIN tags t ON t.entity_id = e.id
304
+ WHERE t.tag = ?
305
+ AND e.created_at > datetime('now', '-24 hours')`).get(AUTO_CAPTURE_TAG)?.c ?? 0;
306
+ const since = db.prepare("SELECT value FROM memesh_metadata WHERE key = 'hook_runs_since'").get()?.value;
307
+ const measuringHours = since !== undefined ? hoursSince(since) : null;
308
+ const SKEW_TOLERANCE_H = 5 / 60;
309
+ const KNOWN_HOOKS = new Set(['session-summary', 'post-commit', 'pre-compact']);
310
+ const ages = new Map();
311
+ for (const r of rows) {
312
+ if (!KNOWN_HOOKS.has(r.hook))
313
+ continue;
314
+ const age = hoursSince(r.last_run_at);
315
+ ages.set(r.hook, age !== null && age >= -SKEW_TOLERANCE_H ? Math.max(age, 0) : null);
316
+ }
317
+ if (ages.size > 0) {
318
+ const capturedTail = captured > 0
319
+ ? `${captured} memor${captured === 1 ? 'y' : 'ies'} captured in the last 24h.`
320
+ : 'Nothing was worth saving in that time, which is normal — the loop still ran.';
321
+ 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). ` +
322
+ '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 } });
323
+ const stale = (hook, age, status) => createCheck('hook-activity', TITLE, status, `The ${hook} hook last ran ${formatHoursAgo(Math.ceil(age))}. ` +
324
+ '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) } });
325
+ const ssAge = ages.has('session-summary') ? ages.get('session-summary') : undefined;
326
+ if (ssAge === null)
327
+ return staleUnknown('session-summary');
328
+ if (ssAge !== undefined) {
329
+ if (ssAge <= 24) {
330
+ return createCheck('hook-activity', TITLE, 'pass', `The session-summary hook last ran ${formatHoursAgo(ssAge)} — auto-capture is alive. ${capturedTail}`);
331
+ }
332
+ if (ssAge <= 72)
333
+ return stale('session-summary', ssAge, 'warn');
334
+ const ccWrites = db.prepare(`SELECT COUNT(*) as c FROM entities
335
+ WHERE created_at > datetime('now', '-72 hours')
336
+ AND json_valid(metadata)
337
+ AND json_extract(metadata, '$.provenance.source_host') = 'claude-code'`).get()?.c ?? 0;
338
+ if (ccWrites > 0)
339
+ return stale('session-summary', ssAge, 'fail');
340
+ 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) } });
341
+ }
342
+ const known = [...ages.entries()].filter((e) => e[1] !== null);
343
+ if (known.length === 0) {
344
+ const [hook] = [...ages.keys()].sort();
345
+ return staleUnknown(hook);
346
+ }
347
+ const [freshestHook, freshestAge] = known.sort((a, b) => a[1] - b[1])[0];
348
+ if (freshestAge <= 24) {
349
+ if (measuringHours !== null && measuringHours > 72) {
350
+ 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) } });
351
+ }
352
+ return createCheck('hook-activity', TITLE, 'pass', `The ${freshestHook} hook last ran ${formatHoursAgo(freshestAge)} — auto-capture is alive. ` +
353
+ `(session-summary has not stamped yet — expected until a session with real work ends.) ${capturedTail}`);
354
+ }
355
+ return stale(freshestHook, freshestAge, 'warn');
356
+ }
357
+ if (since !== undefined && (measuringHours === null || measuringHours < 0)) {
358
+ 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.');
359
+ }
360
+ if (measuringHours === null || measuringHours < 24) {
361
+ return createCheck('hook-activity', TITLE, 'pass', 'Hook-run tracking has only just started on this database — the first work session will fill it in.');
362
+ }
363
+ const markerPath = path.join(memeshDir(), 'install-hooks.json');
364
+ if (existsSyncImpl(markerPath)) {
365
+ try {
366
+ if (Date.now() - statSyncImpl(markerPath).mtimeMs < 24 * 60 * 60 * 1000) {
367
+ return createCheck('hook-activity', TITLE, 'pass', 'Hooks were wired in the last day and no session has ended yet — normal for a fresh install.');
278
368
  }
279
- catch { }
280
369
  }
281
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'warn', 'memesh has not saved anything automatically in the last 24 hours. Everything is set up — it just has not seen a work session to remember yet. If you HAVE been working all day, Claude Code may need a restart to pick the connection up.', 'Do a normal Claude Code work session (or make a git commit), then check again with `memesh doctor`.', { code: 'hook-activity.quiet' });
370
+ catch { }
282
371
  }
283
- 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.`);
372
+ const legacyCaptured = db.prepare(`SELECT COUNT(DISTINCT e.id) as c FROM entities e
373
+ JOIN tags t ON t.entity_id = e.id
374
+ WHERE t.tag = ?
375
+ AND e.created_at > ?`).get(AUTO_CAPTURE_TAG, since)?.c ?? 0;
376
+ if (legacyCaptured > 0) {
377
+ 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 } });
378
+ }
379
+ if (!wiringPresent) {
380
+ 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' });
381
+ }
382
+ return createCheck('hook-activity', TITLE, 'fail', `No capture hook has run since tracking began ${formatHoursAgo(measuringHours)}. ` +
383
+ '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) } });
284
384
  }
285
385
  catch (err) {
286
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'warn', `Could not query the database: ${err instanceof Error ? err.message : String(err)}`, undefined, { code: 'hook-activity.query-failed', params: { detail: err instanceof Error ? err.message : String(err) } });
386
+ const detail = err instanceof Error ? err.message : String(err);
387
+ 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 } });
287
388
  }
288
389
  finally {
289
390
  try {
@@ -293,6 +394,34 @@ function inspectHookActivity(openDatabaseImpl, closeDatabaseImpl, existsSyncImpl
293
394
  catch { }
294
395
  }
295
396
  }
397
+ function autoCaptureOffSource() {
398
+ let configAutoCapture;
399
+ try {
400
+ configAutoCapture = readConfig().autoCapture;
401
+ }
402
+ catch {
403
+ configAutoCapture = undefined;
404
+ }
405
+ return autoCaptureDecision(process.env.MEMESH_AUTO_CAPTURE, configAutoCapture).offSource;
406
+ }
407
+ export function hoursSince(sqliteTimestamp) {
408
+ const then = parseSqliteUtcMs(sqliteTimestamp);
409
+ if (then === null)
410
+ return null;
411
+ return (Date.now() - then) / (60 * 60 * 1000);
412
+ }
413
+ function formatHoursAgo(hours) {
414
+ if (hours === null || !Number.isFinite(hours) || hours < 0)
415
+ return 'at an unknown time';
416
+ if (hours < 1)
417
+ return 'less than an hour ago';
418
+ if (hours < 48) {
419
+ const h = Math.round(hours);
420
+ return `${h} hour${h === 1 ? '' : 's'} ago`;
421
+ }
422
+ const d = Math.round(hours / 24);
423
+ return `${d} day${d === 1 ? '' : 's'} ago`;
424
+ }
296
425
  function defaultResolveShellMemesh() {
297
426
  try {
298
427
  const cmd = process.platform === 'win32' ? 'where' : 'which';
@@ -306,12 +435,23 @@ function defaultResolveShellMemesh() {
306
435
  return null;
307
436
  }
308
437
  }
438
+ const RUNTIME_TOO_OLD = 'memesh:node-sqlite-too-old';
309
439
  function defaultNativeBindingProbe(packageRoot) {
310
440
  try {
311
- const localRequire = createRequire(pathToFileURL(path.join(packageRoot, 'package.json')).href);
312
- const Database = localRequire('better-sqlite3');
313
- const probe = new Database(':memory:');
314
- probe.close();
441
+ const probe = new MemeshDatabase(':memory:', { allowExtension: true });
442
+ try {
443
+ if (typeof probe.enableLoadExtension !== 'function') {
444
+ return { ok: false, message: `${RUNTIME_TOO_OLD}: node:sqlite in ${process.version} has no enableLoadExtension` };
445
+ }
446
+ probe.enableLoadExtension(true);
447
+ const localRequire = createRequire(pathToFileURL(path.join(packageRoot, 'package.json')).href);
448
+ const sqliteVec = localRequire('sqlite-vec');
449
+ sqliteVec.load(probe);
450
+ probe.prepare('SELECT vec_version()').get();
451
+ }
452
+ finally {
453
+ probe.close();
454
+ }
315
455
  return { ok: true };
316
456
  }
317
457
  catch (err) {
@@ -373,20 +513,16 @@ export function hasBuiltInSqlite() {
373
513
  function inspectNativeBinding(packageRoot, _existsSyncImpl, probeImpl = defaultNativeBindingProbe) {
374
514
  const result = probeImpl(packageRoot);
375
515
  if (result.ok) {
376
- return createCheck('native-binding', 'Native SQLite binding', 'pass', 'better-sqlite3 native binding loads cleanly (Database probe succeeded).');
516
+ return createCheck('native-binding', 'SQLite and vector search', 'pass', 'node:sqlite opened a database and sqlite-vec loaded (probe succeeded).');
517
+ }
518
+ if (result.message.startsWith(RUNTIME_TOO_OLD)) {
519
+ 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 } });
377
520
  }
378
- const isMissingBinding = /bindings file|locate the bindings/i.test(result.message);
379
521
  const isMissingPackage = /MODULE_NOT_FOUND|Cannot find module/i.test(result.message);
380
522
  if (isMissingPackage) {
381
- return createCheck('native-binding', 'Native SQLite binding', 'fail', 'better-sqlite3 is not installed (Node could not resolve the module from any '
382
- + 'parent node_modules). Memesh hooks and database operations will not work.', `Run: npm install (in the directory that depends on @pcircle/memesh)`, { code: 'native-binding.not-installed' });
523
+ 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' });
383
524
  }
384
- if (isMissingBinding) {
385
- return createCheck('native-binding', 'Native SQLite binding', 'fail', 'better-sqlite3 is installed but the native binding (.node file) is missing. '
386
- + 'Hooks will silently skip-and-exit, and auto-capture will NOT write any entities. '
387
- + '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)`, { code: 'native-binding.missing', params: { root: packageRoot } });
388
- }
389
- return createCheck('native-binding', 'Native SQLite binding', 'fail', `better-sqlite3 failed to load: ${result.message}`, `Run: cd "${packageRoot}" && npm rebuild better-sqlite3`, { code: 'native-binding.load-failed', params: { detail: result.message, root: packageRoot } });
525
+ 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 } });
390
526
  }
391
527
  function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
392
528
  const shellPath = resolveShellMemeshImpl();
@@ -540,25 +676,7 @@ function verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl) {
540
676
  missing.length > 0 ? `${missing.length} missing: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ` (+${missing.length - 3} more)` : ''}` : null,
541
677
  mismatches.length > 0 ? `${mismatches.length} tampered: ${mismatches.slice(0, 3).join(', ')}${mismatches.length > 3 ? ` (+${mismatches.length - 3} more)` : ''}` : null,
542
678
  ].filter(Boolean).join('; ');
543
- 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 } });
544
- }
545
- async function inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyncImpl) {
546
- const configPath = getConfigPathImpl();
547
- if (!existsSyncImpl(configPath)) {
548
- return createCheck('config_parse', 'Config parses', 'pass', 'No config file yet — defaults apply. This is normal for a fresh install.');
549
- }
550
- try {
551
- const raw = readFileSyncImpl(configPath, 'utf8');
552
- const parsed = JSON.parse(raw);
553
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
554
- 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 } });
555
- }
556
- return createCheck('config_parse', 'Config parses', 'pass', `${configPath} is valid JSON and its settings are in effect.`);
557
- }
558
- catch (err) {
559
- const msg = err instanceof Error ? err.message : String(err);
560
- 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 } });
561
- }
679
+ 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/security.', { code: 'skills-manifest.verify-failed', params: { detail } });
562
680
  }
563
681
  async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextImpl) {
564
682
  if (capabilities.embeddings === 'tfidf') {
@@ -617,7 +735,7 @@ function summarizeOverallStatus(checks) {
617
735
  return 'PASS';
618
736
  }
619
737
  export async function runDoctor(options) {
620
- const { packageRoot, packageVersion, probeHttp = false, probeCapabilities = false, embedTextImpl = embedText, probeProviderImpl = probeProvider, httpBaseUrl = 'http://127.0.0.1:3737', platform = process.platform, openDatabaseImpl = openDatabase, closeDatabaseImpl = closeDatabase, isDatabaseOpenImpl = isDatabaseOpen, detectCapabilitiesImpl = detectCapabilities, getConfigPathImpl = getConfigPath, getUpdateCheckImpl = getUpdateCheck, getCurrentInstallChannelImpl = getCurrentInstallChannel, getInstallChannelSupportImpl = getInstallChannelSupport, existsSyncImpl = fs.existsSync, readFileSyncImpl = fs.readFileSync, statSyncImpl = fs.statSync, fetchImpl = fetch, nativeBindingProbeImpl, resolveShellMemeshImpl = defaultResolveShellMemesh, } = options;
738
+ const { packageRoot, packageVersion, probeHttp = false, probeCapabilities = false, embedTextImpl = embedText, probeProviderImpl = probeProvider, httpBaseUrl = 'http://127.0.0.1:3737', platform = process.platform, openDatabaseImpl = openDatabase, closeDatabaseImpl = closeDatabase, isDatabaseOpenImpl = isDatabaseOpen, detectCapabilitiesImpl = detectCapabilities, getConfigPathImpl = getConfigPath, getUpdateCheckImpl = getUpdateCheck, getCurrentInstallChannelImpl = getCurrentInstallChannel, installedPluginsPathImpl, getInstallChannelSupportImpl = getInstallChannelSupport, existsSyncImpl = fs.existsSync, readFileSyncImpl = fs.readFileSync, statSyncImpl = fs.statSync, fetchImpl = fetch, nativeBindingProbeImpl, resolveShellMemeshImpl = defaultResolveShellMemesh, } = options;
621
739
  const wasDbOpenBeforeUs = isDatabaseOpenImpl();
622
740
  const safeCloseDatabaseImpl = wasDbOpenBeforeUs
623
741
  ? () => undefined
@@ -647,7 +765,7 @@ export async function runDoctor(options) {
647
765
  dbChecks.push(createCheck('fts_segmentation', 'Keyword index segmentation', 'warn', `The keyword index holds ${unsegmented.c} unsegmented term(s), so some memories are only ` +
648
766
  `findable by their exact full text. This happens when an older build wrote to a database ` +
649
767
  `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.`, { code: 'fts.unsegmented', params: { count: unsegmented.c } }));
768
+ `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 } }, 'fts-rebuild'));
651
769
  }
652
770
  }
653
771
  const pendingReindex = getPendingReindexInfo();
@@ -659,6 +777,7 @@ export async function runDoctor(options) {
659
777
  const message = err instanceof Error ? err.message : 'unknown database error';
660
778
  let diagnosis;
661
779
  let fix;
780
+ let fixId;
662
781
  if (existsSyncImpl(databasePath)) {
663
782
  try {
664
783
  const stat = statSyncImpl(databasePath);
@@ -667,6 +786,7 @@ export async function runDoctor(options) {
667
786
  if (!canRead || !canWrite) {
668
787
  diagnosis = `Database file exists but has insufficient permissions (${(stat.mode & 0o777).toString(8)})`;
669
788
  fix = `Fix permissions: chmod 600 "${databasePath}"`;
789
+ fixId = 'chmod-db';
670
790
  }
671
791
  else if (stat.size === 0) {
672
792
  diagnosis = 'Database file is empty (0 bytes) — likely corrupted';
@@ -708,7 +828,7 @@ export async function runDoctor(options) {
708
828
  }
709
829
  }
710
830
  dbChecks.length = 0;
711
- dbChecks.push(createCheck('database', 'Database', 'fail', diagnosis, fix, { code: 'database.broken', params: { detail: diagnosis } }));
831
+ dbChecks.push(createCheck('database', 'Database', 'fail', diagnosis, fix, { code: 'database.broken', params: { detail: diagnosis } }, fixId));
712
832
  }
713
833
  finally {
714
834
  checks.push(...dbChecks);
@@ -721,8 +841,11 @@ export async function runDoctor(options) {
721
841
  checks.push(inspectConfigFile(existsSyncImpl, readFileSyncImpl, getConfigPathImpl));
722
842
  checks.push(inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl));
723
843
  checks.push(...inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncImpl, statSyncImpl));
724
- checks.push(inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir(), packageRoot));
725
- checks.push(inspectHookActivity(openDatabaseImpl, safeCloseDatabaseImpl, existsSyncImpl, statSyncImpl));
844
+ const wiring = inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir(), install, installedPluginsPathImpl);
845
+ checks.push(wiring);
846
+ const captureWired = wiring.status === 'pass'
847
+ && (wiring.params === undefined || wiring.params.captureWired === 1);
848
+ checks.push(inspectHookActivity(openDatabaseImpl, safeCloseDatabaseImpl, existsSyncImpl, statSyncImpl, captureWired));
726
849
  checks.push(inspectDashboardArtifact(packageRoot, existsSyncImpl));
727
850
  checks.push(inspectNodeRuntime(packageRoot, existsSyncImpl, readFileSyncImpl));
728
851
  checks.push(inspectNativeBinding(packageRoot, existsSyncImpl, nativeBindingProbeImpl));
@@ -740,7 +863,6 @@ export async function runDoctor(options) {
740
863
  : `last mined ${((Date.now() - last) / 3600_000).toFixed(1)}h ago`;
741
864
  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\`.`));
742
865
  }
743
- checks.push(await inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyncImpl));
744
866
  checks.push(await inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextImpl));
745
867
  checks.push(await inspectLlmProbe(capabilities, probeCapabilities, probeProviderImpl));
746
868
  checks.push(await inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSupport));