@pcircle/memesh 4.3.0 → 4.5.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 (100) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.de.md +35 -12
  4. package/README.es.md +13 -9
  5. package/README.fr.md +33 -10
  6. package/README.ja.md +13 -9
  7. package/README.ko.md +14 -10
  8. package/README.md +18 -11
  9. package/README.pt.md +32 -9
  10. package/README.th.md +18 -8
  11. package/README.vi.md +34 -11
  12. package/README.zh-CN.md +14 -10
  13. package/README.zh-TW.md +14 -10
  14. package/dashboard/dist/index.html +10 -10
  15. package/dist/cli/view-live.js +2 -2
  16. package/dist/cli/view.js +1 -1
  17. package/dist/core/analytics.d.ts +4 -0
  18. package/dist/core/analytics.d.ts.map +1 -1
  19. package/dist/core/analytics.js +8 -8
  20. package/dist/core/analytics.js.map +1 -1
  21. package/dist/core/auto-tagger.d.ts.map +1 -1
  22. package/dist/core/auto-tagger.js.map +1 -1
  23. package/dist/core/config.d.ts +5 -2
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +28 -13
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/demo.d.ts +1 -0
  28. package/dist/core/demo.d.ts.map +1 -1
  29. package/dist/core/demo.js +24 -0
  30. package/dist/core/demo.js.map +1 -1
  31. package/dist/core/digest-validator.d.ts.map +1 -1
  32. package/dist/core/digest-validator.js +3 -1
  33. package/dist/core/digest-validator.js.map +1 -1
  34. package/dist/core/doctor.d.ts +2 -0
  35. package/dist/core/doctor.d.ts.map +1 -1
  36. package/dist/core/doctor.js +70 -60
  37. package/dist/core/doctor.js.map +1 -1
  38. package/dist/core/dreamer.d.ts +21 -1
  39. package/dist/core/dreamer.d.ts.map +1 -1
  40. package/dist/core/dreamer.js +86 -8
  41. package/dist/core/dreamer.js.map +1 -1
  42. package/dist/core/embedder.d.ts +1 -4
  43. package/dist/core/embedder.d.ts.map +1 -1
  44. package/dist/core/embedder.js +5 -92
  45. package/dist/core/embedder.js.map +1 -1
  46. package/dist/core/failure-analyzer.d.ts.map +1 -1
  47. package/dist/core/failure-analyzer.js +2 -1
  48. package/dist/core/failure-analyzer.js.map +1 -1
  49. package/dist/core/llm-client.d.ts.map +1 -1
  50. package/dist/core/llm-client.js.map +1 -1
  51. package/dist/core/llm-validator.d.ts +1 -0
  52. package/dist/core/llm-validator.d.ts.map +1 -1
  53. package/dist/core/llm-validator.js +33 -10
  54. package/dist/core/llm-validator.js.map +1 -1
  55. package/dist/core/operations.d.ts.map +1 -1
  56. package/dist/core/operations.js +10 -3
  57. package/dist/core/operations.js.map +1 -1
  58. package/dist/core/output-language.d.ts +6 -0
  59. package/dist/core/output-language.d.ts.map +1 -0
  60. package/dist/core/output-language.js +25 -0
  61. package/dist/core/output-language.js.map +1 -0
  62. package/dist/core/patterns.d.ts +0 -1
  63. package/dist/core/patterns.d.ts.map +1 -1
  64. package/dist/core/patterns.js +1 -5
  65. package/dist/core/patterns.js.map +1 -1
  66. package/dist/core/transcript-extractor.d.ts +89 -0
  67. package/dist/core/transcript-extractor.d.ts.map +1 -0
  68. package/dist/core/transcript-extractor.js +437 -0
  69. package/dist/core/transcript-extractor.js.map +1 -0
  70. package/dist/core/transcript-source.d.ts +21 -0
  71. package/dist/core/transcript-source.d.ts.map +1 -0
  72. package/dist/core/transcript-source.js +142 -0
  73. package/dist/core/transcript-source.js.map +1 -0
  74. package/dist/core/types.d.ts +4 -0
  75. package/dist/core/types.d.ts.map +1 -1
  76. package/dist/core/verifier.js +1 -1
  77. package/dist/core/verifier.js.map +1 -1
  78. package/dist/db.d.ts.map +1 -1
  79. package/dist/db.js +10 -0
  80. package/dist/db.js.map +1 -1
  81. package/dist/skills-manifest.json +11 -11
  82. package/dist/transports/cli/cli.d.ts.map +1 -1
  83. package/dist/transports/cli/cli.js +229 -65
  84. package/dist/transports/cli/cli.js.map +1 -1
  85. package/dist/transports/http/server.d.ts +7 -0
  86. package/dist/transports/http/server.d.ts.map +1 -1
  87. package/dist/transports/http/server.js +145 -50
  88. package/dist/transports/http/server.js.map +1 -1
  89. package/dist/transports/mcp/handlers.d.ts +1 -1
  90. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  91. package/dist/transports/mcp/handlers.js +3 -2
  92. package/dist/transports/mcp/handlers.js.map +1 -1
  93. package/dist/transports/schemas.d.ts.map +1 -1
  94. package/dist/transports/schemas.js.map +1 -1
  95. package/package.json +2 -11
  96. package/scripts/hooks/post-commit.js +8 -2
  97. package/scripts/hooks/pre-compact.js +9 -0
  98. package/scripts/hooks/session-start.js +1 -1
  99. package/skills/agentic-orchestration/SKILL.md +1 -1
  100. package/skills/memesh/SKILL.md +2 -0
@@ -3,14 +3,15 @@ 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 } 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';
15
16
  const EMBEDDING_PROBE_TIMEOUT_MS = 15000;
16
17
  const EXPECTED_HOOK_TYPES = ['PreToolUse', 'SessionStart', 'PostToolUse', 'Stop', 'PreCompact'];
@@ -53,7 +54,7 @@ function inspectLocaleReadmeParity(packageRoot, existsSyncImpl, readFileSyncImpl
53
54
  englishCount = countH2Headings(readFileSyncImpl(englishPath, 'utf8'));
54
55
  }
55
56
  catch (err) {
56
- return createCheck('readme_locale_parity', 'README locale parity', 'warn', `Could not read README.md: ${err instanceof Error ? err.message : String(err)}`);
57
+ 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
58
  }
58
59
  const missing = [];
59
60
  const drift = [];
@@ -86,13 +87,13 @@ function inspectLocaleReadmeParity(packageRoot, existsSyncImpl, readFileSyncImpl
86
87
  .join(', ');
87
88
  parts.push(`H2 count drift (English=${englishCount}): ${driftDetail}`);
88
89
  }
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).`);
90
+ 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
91
  }
91
92
  function resolveDatabasePath() {
92
93
  return getDbPath();
93
94
  }
94
- function createCheck(id, label, status, summary, fix) {
95
- return { id, label, status, summary, fix };
95
+ function createCheck(id, label, status, summary, fix, i18n) {
96
+ return { id, label, status, summary, fix, code: i18n?.code, params: i18n?.params };
96
97
  }
97
98
  function createInfo(id, label, summary, fix) {
98
99
  return { id, label, status: 'pass', summary, fix, informational: true };
@@ -120,22 +121,22 @@ function inspectConfigFile(existsSyncImpl, readFileSyncImpl, getConfigPathImpl)
120
121
  }
121
122
  const parsed = parseJsonFile(configPath, readFileSyncImpl);
122
123
  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.`);
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 } });
124
125
  }
125
126
  return createCheck('config', 'Config', 'pass', `Config file is readable at ${configPath}.`);
126
127
  }
127
128
  function inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl) {
128
129
  const mcpPath = path.join(packageRoot, '.mcp.json');
129
130
  if (!existsSyncImpl(mcpPath)) {
130
- return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json is missing.', 'Restore `.mcp.json` from the package or reinstall MeMesh.');
131
+ 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
132
  }
132
133
  const parsed = parseJsonFile(mcpPath, readFileSyncImpl);
133
134
  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.`);
135
+ 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
136
  }
136
137
  const server = parsed.value.mcpServers?.memesh;
137
138
  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`.');
139
+ 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
140
  }
140
141
  return createCheck('mcp-config', 'MCP config', 'pass', '.mcp.json is present and defines the memesh MCP server.');
141
142
  }
@@ -160,32 +161,32 @@ function inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncI
160
161
  const hooksPath = path.join(packageRoot, 'hooks', 'hooks.json');
161
162
  if (!existsSyncImpl(hooksPath)) {
162
163
  return [
163
- createCheck('hooks-config', 'Hooks config', 'fail', 'hooks/hooks.json is missing.', 'Restore `hooks/hooks.json` from the package or reinstall MeMesh.'),
164
+ 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
165
  ];
165
166
  }
166
167
  const parsed = parseJsonFile(hooksPath, readFileSyncImpl);
167
168
  if (!parsed.ok) {
168
169
  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.`),
170
+ 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
171
  ];
171
172
  }
172
173
  const hookTypes = Object.keys(parsed.value.hooks ?? {});
173
174
  const missingTypes = EXPECTED_HOOK_TYPES.filter((type) => !hookTypes.includes(type));
174
175
  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.')
176
+ ? 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
177
  : createCheck('hooks-config', 'Hooks config', 'pass', `hooks/hooks.json is present with ${hookTypes.length} hook types configured.`);
177
178
  const scriptPaths = extractHookScriptPaths(parsed.value, packageRoot);
178
179
  if (scriptPaths.length === 0) {
179
180
  return [
180
181
  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.'),
182
+ 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
183
  ];
183
184
  }
184
185
  const missingScripts = scriptPaths.filter((scriptPath) => !existsSyncImpl(scriptPath));
185
186
  if (missingScripts.length > 0) {
186
187
  return [
187
188
  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.'),
189
+ 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
190
  ];
190
191
  }
191
192
  if (platform !== 'win32') {
@@ -196,7 +197,7 @@ function inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncI
196
197
  if (nonExecutable.length > 0) {
197
198
  return [
198
199
  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.'),
200
+ 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
201
  ];
201
202
  }
202
203
  }
@@ -214,22 +215,22 @@ function inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir, packageR
214
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.');
215
216
  }
216
217
  }
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.');
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' });
218
219
  }
219
220
  const parsed = parseJsonFile(markerPath, readFileSyncImpl);
220
221
  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.');
222
+ 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
223
  }
223
224
  const marker = parsed.value;
224
225
  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`.');
226
+ 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
227
  }
227
228
  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`.');
229
+ 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
230
  }
230
231
  const settingsParsed = parseJsonFile(marker.settings_path, readFileSyncImpl);
231
232
  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`.');
233
+ 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
234
  }
234
235
  const hooks = settingsParsed.value.hooks;
235
236
  let hasMemeshHook = false;
@@ -251,10 +252,10 @@ function inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir, packageR
251
252
  }
252
253
  }
253
254
  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.');
255
+ 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
256
  }
256
257
  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.');
258
+ 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
259
  }
259
260
  return createCheck('hook-wiring', 'Hooks wired into Claude Code', 'pass', `Wired in ${marker.settings_path} (scope: ${marker.scope ?? 'user'}, version: ${marker.version ?? 'unknown'}).`);
260
261
  }
@@ -277,12 +278,12 @@ function inspectHookActivity(openDatabaseImpl, closeDatabaseImpl, existsSyncImpl
277
278
  }
278
279
  catch { }
279
280
  }
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.');
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' });
281
282
  }
282
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.`);
283
284
  }
284
285
  catch (err) {
285
- return createCheck('hook-activity', 'Hook activity (last 24h)', 'warn', `Could not query the database: ${err instanceof Error ? err.message : String(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) } });
286
287
  }
287
288
  finally {
288
289
  try {
@@ -356,7 +357,7 @@ export function inspectNodeRuntime(packageRoot, existsSyncImpl, readFileSyncImpl
356
357
  }
357
358
  if (!ok) {
358
359
  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.`);
360
+ `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
361
  }
361
362
  return createCheck('node-runtime', 'Node runtime', 'pass', `${facts} Meets the required range ${declared}.`);
362
363
  }
@@ -378,14 +379,14 @@ function inspectNativeBinding(packageRoot, _existsSyncImpl, probeImpl = defaultN
378
379
  const isMissingPackage = /MODULE_NOT_FOUND|Cannot find module/i.test(result.message);
379
380
  if (isMissingPackage) {
380
381
  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
+ + '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' });
382
383
  }
383
384
  if (isMissingBinding) {
384
385
  return createCheck('native-binding', 'Native SQLite binding', 'fail', 'better-sqlite3 is installed but the native binding (.node file) is missing. '
385
386
  + '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)`);
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 } });
387
388
  }
388
- return createCheck('native-binding', 'Native SQLite binding', 'fail', `better-sqlite3 failed to load: ${result.message}`, `Run: cd "${packageRoot}" && npm rebuild better-sqlite3`);
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 } });
389
390
  }
390
391
  function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
391
392
  const shellPath = resolveShellMemeshImpl();
@@ -401,7 +402,7 @@ function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
401
402
  }
402
403
  if (installChannel === 'plugin-marketplace') {
403
404
  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`.');
405
+ + '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
406
  }
406
407
  return createCheck('shell-cli', 'Shell CLI on PATH', 'pass', shellPath
407
408
  ? `\`memesh\` resolves to ${shellPath}.`
@@ -410,14 +411,14 @@ function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
410
411
  function inspectDashboardArtifact(packageRoot, existsSyncImpl) {
411
412
  const dashboardPath = path.join(packageRoot, 'dashboard', 'dist', 'index.html');
412
413
  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.');
414
+ 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
415
  }
415
416
  return createCheck('dashboard', 'Dashboard artifact', 'pass', 'dashboard/dist/index.html is present.');
416
417
  }
417
418
  async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSupport) {
418
419
  const update = await getUpdateCheckImpl(packageVersion, { preferFresh: false });
419
420
  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.');
421
+ 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
422
  }
422
423
  if (update.currentVersionDeprecated && update.deprecationMessage) {
423
424
  const target = update.latestVersion && update.latestVersion !== packageVersion
@@ -439,10 +440,10 @@ async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSu
439
440
  else {
440
441
  fix = `Upgrade via your install method (see \`memesh status\`).`;
441
442
  }
442
- return createCheck('update-status', 'Update status', 'fail', `Installed version ${packageVersion} is DEPRECATED by maintainers: ${update.deprecationMessage}`, fix);
443
+ 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
444
  }
444
445
  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.');
446
+ 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
447
  }
447
448
  if (update.checkSucceeded && update.lastError) {
448
449
  const hasUpdate = Boolean(update.updateAvailable && update.latestVersion);
@@ -465,11 +466,14 @@ async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSu
465
466
  upgradeHint = '';
466
467
  }
467
468
  const fix = `Run \`memesh status\` while online to retry the deprecation lookup${upgradeHint}.`;
468
- return createCheck('update-status', 'Update status', 'warn', summary, fix);
469
+ return createCheck('update-status', 'Update status', 'warn', summary, fix, {
470
+ code: 'update-status.deprecation-unknown',
471
+ params: { version: packageVersion, detail: update.lastError ?? '' },
472
+ });
469
473
  }
470
474
  if (update.updateAvailable && update.latestVersion) {
471
475
  if (packageVersion < update.latestVersion) {
472
- return createCheck('update-status', 'Update status', 'warn', `Update available: ${update.latestVersion} (current: ${packageVersion})`, `Run 'memesh update' to upgrade`);
476
+ 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
477
  }
474
478
  else {
475
479
  return createCheck('update-status', 'Update status', 'pass', `Running pre-release version (${packageVersion}), npm latest is ${update.latestVersion}`);
@@ -477,35 +481,37 @@ async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSu
477
481
  }
478
482
  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
483
  ? 'Run `memesh status` while online to refresh cached update metadata.'
484
+ : undefined, update.freshness === 'stale'
485
+ ? { code: 'update-status.stale', params: { version: packageVersion } }
480
486
  : undefined);
481
487
  }
482
488
  async function inspectHttpProbe(httpBaseUrl, fetchImpl) {
483
489
  try {
484
490
  const response = await fetchImpl(`${httpBaseUrl.replace(/\/$/, '')}/v1/health`);
485
491
  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`.');
492
+ 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
493
  }
488
494
  return createCheck('http-probe', 'HTTP probe', 'pass', `HTTP server is reachable at ${httpBaseUrl}.`);
489
495
  }
490
496
  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.');
497
+ 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
498
  }
493
499
  }
494
500
  function verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl) {
495
501
  const manifestPath = path.join(packageRoot, 'dist', 'skills-manifest.json');
496
502
  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`.');
503
+ 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
504
  }
499
505
  let manifest;
500
506
  try {
501
507
  manifest = JSON.parse(readFileSyncImpl(manifestPath, 'utf8'));
502
508
  }
503
509
  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.');
510
+ 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
511
  }
506
512
  const entries = manifest.entries ?? [];
507
513
  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`.');
514
+ 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
515
  }
510
516
  const mismatches = [];
511
517
  const missing = [];
@@ -534,7 +540,7 @@ function verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl) {
534
540
  missing.length > 0 ? `${missing.length} missing: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ` (+${missing.length - 3} more)` : ''}` : null,
535
541
  mismatches.length > 0 ? `${mismatches.length} tampered: ${mismatches.slice(0, 3).join(', ')}${mismatches.length > 3 ? ` (+${mismatches.length - 3} more)` : ''}` : null,
536
542
  ].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.');
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 } });
538
544
  }
539
545
  async function inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyncImpl) {
540
546
  const configPath = getConfigPathImpl();
@@ -545,13 +551,13 @@ async function inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyn
545
551
  const raw = readFileSyncImpl(configPath, 'utf8');
546
552
  const parsed = JSON.parse(raw);
547
553
  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.`);
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 } });
549
555
  }
550
556
  return createCheck('config_parse', 'Config parses', 'pass', `${configPath} is valid JSON and its settings are in effect.`);
551
557
  }
552
558
  catch (err) {
553
559
  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`);
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 } });
555
561
  }
556
562
  }
557
563
  async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextImpl) {
@@ -559,13 +565,7 @@ async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextI
559
565
  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
566
  }
561
567
  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
- }
568
+ 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
569
  }
570
570
  let timer;
571
571
  try {
@@ -576,13 +576,13 @@ async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextI
576
576
  }),
577
577
  ]);
578
578
  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.');
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.', { code: 'embeddings.empty', params: { provider: String(capabilities.embeddings) } });
580
580
  }
581
581
  return createCheck('embeddings_probe', 'Embeddings work', 'pass', `Generated a ${vector.length}-dim test embedding via "${capabilities.embeddings}".`);
582
582
  }
583
583
  catch (err) {
584
584
  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');
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 (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
586
  }
587
587
  finally {
588
588
  clearTimeout(timer);
@@ -601,11 +601,11 @@ async function inspectLlmProbe(capabilities, probeCapabilities, probeProviderImp
601
601
  if (result.valid) {
602
602
  return createCheck('llm_probe', 'LLM reachable', 'pass', `${llm.provider} answered a live probe.`);
603
603
  }
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');
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', { code: 'llm.unreachable', params: { provider: llm.provider, detail: result.error ?? 'unknown error' } });
605
605
  }
606
606
  catch (err) {
607
607
  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');
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', { code: 'llm.threw', params: { provider: llm.provider, detail: msg } });
609
609
  }
610
610
  }
611
611
  function summarizeOverallStatus(checks) {
@@ -627,7 +627,7 @@ export async function runDoctor(options) {
627
627
  const installSupport = getInstallChannelSupportImpl(install);
628
628
  checks.push(createCheck('install-channel', 'Install method', install === 'unknown' ? 'warn' : 'pass', `Install method detected: ${installSupport.label}.`, install === 'unknown'
629
629
  ? '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));
630
+ : undefined, install === 'unknown' ? { code: 'install-channel.unknown' } : undefined));
631
631
  const databasePath = resolveDatabasePath();
632
632
  const dbChecks = [];
633
633
  try {
@@ -647,12 +647,12 @@ export async function runDoctor(options) {
647
647
  dbChecks.push(createCheck('fts_segmentation', 'Keyword index segmentation', 'warn', `The keyword index holds ${unsegmented.c} unsegmented term(s), so some memories are only ` +
648
648
  `findable by their exact full text. This happens when an older build wrote to a database ` +
649
649
  `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.`));
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 } }));
651
651
  }
652
652
  }
653
653
  const pendingReindex = getPendingReindexInfo();
654
654
  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.`));
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.`, { code: 'vector-index.stale' }));
656
656
  }
657
657
  }
658
658
  catch (err) {
@@ -708,7 +708,7 @@ export async function runDoctor(options) {
708
708
  }
709
709
  }
710
710
  dbChecks.length = 0;
711
- dbChecks.push(createCheck('database', 'Database', 'fail', diagnosis, fix));
711
+ dbChecks.push(createCheck('database', 'Database', 'fail', diagnosis, fix, { code: 'database.broken', params: { detail: diagnosis } }));
712
712
  }
713
713
  finally {
714
714
  checks.push(...dbChecks);
@@ -730,6 +730,16 @@ export async function runDoctor(options) {
730
730
  checks.push(verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl));
731
731
  const capabilities = detectCapabilitiesImpl();
732
732
  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.`));
733
+ if (!isTranscriptMiningEnabled()) {
734
+ 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`.'));
735
+ }
736
+ else {
737
+ const last = lastTranscriptMineAt(getProjectName(process.cwd()));
738
+ const when = last === null
739
+ ? 'not yet run for this project'
740
+ : `last mined ${((Date.now() - last) / 3600_000).toFixed(1)}h ago`;
741
+ 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
+ }
733
743
  checks.push(await inspectConfigParse(getConfigPathImpl, existsSyncImpl, readFileSyncImpl));
734
744
  checks.push(await inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextImpl));
735
745
  checks.push(await inspectLlmProbe(capabilities, probeCapabilities, probeProviderImpl));