@pcircle/memesh 4.8.2 → 4.8.5

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 (169) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/mcp.json +7 -0
  4. package/.codex-plugin/plugin.json +6 -0
  5. package/AGENTS.md +18 -8
  6. package/README.de.md +13 -11
  7. package/README.md +25 -12
  8. package/README.zh-TW.md +25 -12
  9. package/dashboard/dist/index.html +3 -3
  10. package/dist/core/agent-message-inbox.d.ts +3 -2
  11. package/dist/core/agent-message-inbox.d.ts.map +1 -1
  12. package/dist/core/agent-message-inbox.js +30 -6
  13. package/dist/core/agent-message-inbox.js.map +1 -1
  14. package/dist/core/agent-messaging.d.ts +7 -0
  15. package/dist/core/agent-messaging.d.ts.map +1 -1
  16. package/dist/core/agent-messaging.js +51 -24
  17. package/dist/core/agent-messaging.js.map +1 -1
  18. package/dist/core/agent-router.d.ts +27 -20
  19. package/dist/core/agent-router.d.ts.map +1 -1
  20. package/dist/core/agent-router.js +114 -43
  21. package/dist/core/agent-router.js.map +1 -1
  22. package/dist/core/agent-scope-id.d.ts +11 -0
  23. package/dist/core/agent-scope-id.d.ts.map +1 -0
  24. package/dist/core/agent-scope-id.js +40 -0
  25. package/dist/core/agent-scope-id.js.map +1 -0
  26. package/dist/core/briefing.d.ts +1 -1
  27. package/dist/core/briefing.d.ts.map +1 -1
  28. package/dist/core/briefing.js +25 -7
  29. package/dist/core/briefing.js.map +1 -1
  30. package/dist/core/citation-rule.d.ts.map +1 -1
  31. package/dist/core/citation-rule.js +5 -2
  32. package/dist/core/citation-rule.js.map +1 -1
  33. package/dist/core/doctor.d.ts +9 -0
  34. package/dist/core/doctor.d.ts.map +1 -1
  35. package/dist/core/doctor.js +461 -24
  36. package/dist/core/doctor.js.map +1 -1
  37. package/dist/core/dreamer.d.ts.map +1 -1
  38. package/dist/core/dreamer.js +3 -1
  39. package/dist/core/dreamer.js.map +1 -1
  40. package/dist/core/embedder.d.ts.map +1 -1
  41. package/dist/core/embedder.js +4 -1
  42. package/dist/core/embedder.js.map +1 -1
  43. package/dist/core/install-channel.d.ts +3 -0
  44. package/dist/core/install-channel.d.ts.map +1 -1
  45. package/dist/core/install-channel.js +79 -2
  46. package/dist/core/install-channel.js.map +1 -1
  47. package/dist/core/install-hooks.d.ts.map +1 -1
  48. package/dist/core/install-hooks.js +10 -6
  49. package/dist/core/install-hooks.js.map +1 -1
  50. package/dist/core/lesson-slug.d.ts.map +1 -1
  51. package/dist/core/lesson-slug.js +6 -4
  52. package/dist/core/lesson-slug.js.map +1 -1
  53. package/dist/core/lifecycle.d.ts.map +1 -1
  54. package/dist/core/lifecycle.js +8 -5
  55. package/dist/core/lifecycle.js.map +1 -1
  56. package/dist/core/llm-client.d.ts.map +1 -1
  57. package/dist/core/llm-client.js +4 -1
  58. package/dist/core/llm-client.js.map +1 -1
  59. package/dist/core/llm-validator.d.ts.map +1 -1
  60. package/dist/core/llm-validator.js +6 -15
  61. package/dist/core/llm-validator.js.map +1 -1
  62. package/dist/core/memory-tool.d.ts.map +1 -1
  63. package/dist/core/memory-tool.js +3 -1
  64. package/dist/core/memory-tool.js.map +1 -1
  65. package/dist/core/ollama-host.d.ts +6 -0
  66. package/dist/core/ollama-host.d.ts.map +1 -0
  67. package/dist/core/ollama-host.js +62 -0
  68. package/dist/core/ollama-host.js.map +1 -0
  69. package/dist/core/operations.d.ts +1 -1
  70. package/dist/core/operations.d.ts.map +1 -1
  71. package/dist/core/operations.js +1 -1
  72. package/dist/core/operations.js.map +1 -1
  73. package/dist/core/project-tags.d.ts +2 -0
  74. package/dist/core/project-tags.d.ts.map +1 -1
  75. package/dist/core/project-tags.js +29 -1
  76. package/dist/core/project-tags.js.map +1 -1
  77. package/dist/core/schema-export.d.ts.map +1 -1
  78. package/dist/core/schema-export.js +10 -8
  79. package/dist/core/schema-export.js.map +1 -1
  80. package/dist/core/version-check.d.ts +1 -0
  81. package/dist/core/version-check.d.ts.map +1 -1
  82. package/dist/core/version-check.js +35 -0
  83. package/dist/core/version-check.js.map +1 -1
  84. package/dist/core/work-topology.d.ts +3 -0
  85. package/dist/core/work-topology.d.ts.map +1 -1
  86. package/dist/core/work-topology.js +24 -2
  87. package/dist/core/work-topology.js.map +1 -1
  88. package/dist/db.d.ts.map +1 -1
  89. package/dist/db.js +5 -2
  90. package/dist/db.js.map +1 -1
  91. package/dist/host-adapters/claude-channel.d.ts +0 -58
  92. package/dist/host-adapters/claude-channel.d.ts.map +1 -1
  93. package/dist/host-adapters/claude-channel.js +0 -242
  94. package/dist/host-adapters/claude-channel.js.map +1 -1
  95. package/dist/host-adapters/codex-cli-queue.d.ts.map +1 -1
  96. package/dist/host-adapters/codex-cli-queue.js +15 -12
  97. package/dist/host-adapters/codex-cli-queue.js.map +1 -1
  98. package/dist/host-runtime/acp.d.ts +2 -0
  99. package/dist/host-runtime/acp.d.ts.map +1 -1
  100. package/dist/host-runtime/acp.js +6 -1
  101. package/dist/host-runtime/acp.js.map +1 -1
  102. package/dist/host-runtime/claude.d.ts +2 -0
  103. package/dist/host-runtime/claude.d.ts.map +1 -1
  104. package/dist/host-runtime/claude.js +12 -13
  105. package/dist/host-runtime/claude.js.map +1 -1
  106. package/dist/host-runtime/codex-session.d.ts +3 -1
  107. package/dist/host-runtime/codex-session.d.ts.map +1 -1
  108. package/dist/host-runtime/codex-session.js +7 -5
  109. package/dist/host-runtime/codex-session.js.map +1 -1
  110. package/dist/host-runtime/codex.d.ts +2 -0
  111. package/dist/host-runtime/codex.d.ts.map +1 -1
  112. package/dist/host-runtime/codex.js +6 -1
  113. package/dist/host-runtime/codex.js.map +1 -1
  114. package/dist/host-runtime/entry.d.ts +5 -0
  115. package/dist/host-runtime/entry.d.ts.map +1 -0
  116. package/dist/host-runtime/entry.js +11 -0
  117. package/dist/host-runtime/entry.js.map +1 -0
  118. package/dist/host-runtime/router-client.d.ts +2 -0
  119. package/dist/host-runtime/router-client.d.ts.map +1 -1
  120. package/dist/host-runtime/router-client.js.map +1 -1
  121. package/dist/knowledge-graph.d.ts +0 -1
  122. package/dist/knowledge-graph.d.ts.map +1 -1
  123. package/dist/knowledge-graph.js +27 -19
  124. package/dist/knowledge-graph.js.map +1 -1
  125. package/dist/skills-manifest.json +40 -25
  126. package/dist/storage/entity-index.d.ts +4 -0
  127. package/dist/storage/entity-index.d.ts.map +1 -0
  128. package/dist/storage/entity-index.js +15 -0
  129. package/dist/storage/entity-index.js.map +1 -0
  130. package/dist/storage/fts-index.d.ts.map +1 -1
  131. package/dist/storage/fts-index.js +5 -0
  132. package/dist/storage/fts-index.js.map +1 -1
  133. package/dist/storage/graph-repairs.d.ts +11 -1
  134. package/dist/storage/graph-repairs.d.ts.map +1 -1
  135. package/dist/storage/graph-repairs.js +241 -30
  136. package/dist/storage/graph-repairs.js.map +1 -1
  137. package/dist/transports/agent-messaging.d.ts +10 -1
  138. package/dist/transports/agent-messaging.d.ts.map +1 -1
  139. package/dist/transports/agent-messaging.js +92 -9
  140. package/dist/transports/agent-messaging.js.map +1 -1
  141. package/dist/transports/cli/cli.d.ts +4 -0
  142. package/dist/transports/cli/cli.d.ts.map +1 -1
  143. package/dist/transports/cli/cli.js +88 -38
  144. package/dist/transports/cli/cli.js.map +1 -1
  145. package/dist/transports/mcp/handlers.d.ts +12 -8
  146. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  147. package/dist/transports/mcp/handlers.js +23 -11
  148. package/dist/transports/mcp/handlers.js.map +1 -1
  149. package/dist/transports/schemas.d.ts +22 -17
  150. package/dist/transports/schemas.d.ts.map +1 -1
  151. package/dist/transports/schemas.js +25 -11
  152. package/dist/transports/schemas.js.map +1 -1
  153. package/docs/platforms/README.md +2 -2
  154. package/docs/platforms/agent-messaging.md +246 -36
  155. package/hooks/hooks.json +10 -0
  156. package/llms-install.md +29 -6
  157. package/package.json +13 -5
  158. package/scripts/hooks/_generated/agent-message-inbox.js +30 -6
  159. package/scripts/hooks/_generated/citation-rule.js +5 -2
  160. package/scripts/hooks/_generated/fts-index.js +5 -0
  161. package/scripts/hooks/_generated/work-topology.js +24 -2
  162. package/scripts/hooks/_shared.js +52 -4
  163. package/scripts/hooks/decision-nudge.js +152 -0
  164. package/scripts/hooks/post-commit.js +11 -0
  165. package/scripts/hooks/pre-compact.js +12 -4
  166. package/scripts/hooks/session-start.js +61 -26
  167. package/scripts/upgrade-plugin.sh +600 -81
  168. package/skills/memesh/SKILL.md +24 -5
  169. /package/{.mcp.json → .claude-plugin/mcp.json} +0 -0
@@ -12,7 +12,7 @@ import { probeProvider } from './llm-validator.js';
12
12
  import { openDatabase, closeDatabase, getPendingReindexInfo, isDatabaseOpen, readVectorGeneration, generationRowIds, } from '../db.js';
13
13
  import { getUpdateCheck } from './version-check.js';
14
14
  import { classifyBump } from './updater.js';
15
- import { getCurrentInstallChannel, getInstallChannelSupport, detectPluginHost } from './install-channel.js';
15
+ import { getCurrentInstallChannel, getInstallChannelSupport, detectPluginHost, pluginHostConfigRoot, versionedPluginCacheRoots, PLUGIN_REFRESH_COMMANDS, } from './install-channel.js';
16
16
  import { getInstallRecord } from './install-id.js';
17
17
  import { citationRulePath, citationRuleState } from './citation-rule.js';
18
18
  import { getDbPath, getMemeshDirFromDbPath, homeDir, memeshDir, getProjectName } from './paths.js';
@@ -27,6 +27,8 @@ import { parseSqliteUtcMs } from './time-utils.js';
27
27
  import { autoCaptureDecision } from './capture-flag.js';
28
28
  import { guardFromMetadata } from './guards.js';
29
29
  import { getAgentMessageStorageReport } from './agent-message-storage.js';
30
+ import { readHostConfigFile } from '../host-runtime/config.js';
31
+ import { summariseTelemetry } from './llm-telemetry.js';
30
32
  const EMBEDDING_PROBE_TIMEOUT_MS = 15000;
31
33
  const EXPECTED_HOOK_TYPES = ['PreToolUse', 'SessionStart', 'PostToolUse', 'Stop', 'PreCompact'];
32
34
  const AGENT_MESSAGE_STORAGE_QUOTA_ENV = 'MEMESH_AGENT_MESSAGE_STORAGE_QUOTA_BYTES';
@@ -147,6 +149,15 @@ function inspectAgentMessageStorage(db, databasePath, policy) {
147
149
  return undefined;
148
150
  }
149
151
  }
152
+ function inspectCodexSessionSetup(codexPluginCacheDetected, existsSyncImpl) {
153
+ if (!codexPluginCacheDetected)
154
+ return null;
155
+ const configPath = path.join(getMemeshDirFromDbPath(), 'hosts', 'codex-session.json');
156
+ if (existsSyncImpl(configPath)) {
157
+ return createCheck('codex-session-setup', 'Codex ordinary-session notification setup', 'pass', 'A Codex plugin cache copy was detected, but this proves only that cached source exists, not that the plugin is enabled or registered. The explicit opt-in ordinary-session notification setup is present; durable inbox remains available, and MeMesh will not auto-attach.');
158
+ }
159
+ return createCheck('codex-session-setup', 'Codex ordinary-session notification setup', 'warn', 'A Codex plugin cache copy was detected, but this proves only that cached source exists, not that the plugin is enabled or registered. Durable inbox remains available, but live ordinary-session wakeup is inactive. Setup is explicit opt-in; MeMesh will not auto-attach.', 'Run `memesh agent setup codex-session --project <project> --principal <principal> --workspace <exact-workspace>`, then restart Codex.', { code: 'codex-session.config-missing' });
160
+ }
150
161
  function configuredAgentMessageStoragePolicy(explicit) {
151
162
  if (explicit !== undefined)
152
163
  return explicit;
@@ -206,28 +217,49 @@ function inspectConfigFile(existsSyncImpl, readFileSyncImpl, getConfigPathImpl,
206
217
  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 } });
207
218
  }
208
219
  }
209
- function inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl) {
210
- const mcpPath = path.join(packageRoot, '.mcp.json');
220
+ const MCP_PLACEHOLDER = '${CLAUDE_PLUGIN_ROOT}';
221
+ function declaredMcpManifest(packageRoot, readFileSyncImpl) {
222
+ const parsed = parseJsonFile(path.join(packageRoot, '.claude-plugin', 'plugin.json'), readFileSyncImpl);
223
+ if (!parsed.ok)
224
+ return null;
225
+ const declared = parsed.value.mcpServers;
226
+ if (typeof declared !== 'string' || !declared.startsWith('./'))
227
+ return null;
228
+ return declared.slice(2);
229
+ }
230
+ function inspectMcpConfig(packageRoot, installChannel, existsSyncImpl, readFileSyncImpl, env) {
231
+ const relativeManifest = declaredMcpManifest(packageRoot, readFileSyncImpl);
232
+ if (relativeManifest === null) {
233
+ return createCheck('mcp-config', 'MCP config', 'fail', '.claude-plugin/plugin.json declares no `mcpServers` path, so Claude Code has no MeMesh MCP server to start.', 'Reinstall MeMesh so the plugin manifest and the MCP manifest it names are both restored.', { code: 'mcp-config.missing' });
234
+ }
235
+ const label = relativeManifest;
236
+ const mcpPath = path.join(packageRoot, relativeManifest);
211
237
  if (!existsSyncImpl(mcpPath)) {
212
- return createCheck('mcp-config', 'MCP config', 'fail', '.mcp.json is missing.', 'Restore `.mcp.json` from the package or reinstall MeMesh.', { code: 'mcp-config.missing' });
238
+ return createCheck('mcp-config', 'MCP config', 'fail', `${label} is missing.`, 'Reinstall MeMesh so the plugin manifest and the MCP manifest it names are both restored.', { code: 'mcp-config.missing' });
213
239
  }
214
240
  const parsed = parseJsonFile(mcpPath, readFileSyncImpl);
215
241
  if (!parsed.ok) {
216
- 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 } });
242
+ return createCheck('mcp-config', 'MCP config', 'fail', `${label} is not valid JSON.`, `Fix ${mcpPath} so Claude Code can read the MCP server definition.`, { code: 'mcp-config.invalid-json', params: { path: mcpPath } });
217
243
  }
218
244
  const server = parsed.value.mcpServers?.memesh;
219
245
  if (!server || typeof server.command !== 'string') {
220
- 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' });
246
+ return createCheck('mcp-config', 'MCP config', 'fail', `${label} does not define a usable \`memesh\` MCP server entry.`, `Reinstall MeMesh or restore the \`mcpServers.memesh\` entry in \`${label}\`.`, { code: 'mcp-config.no-entry' });
221
247
  }
222
248
  const args = Array.isArray(server.args) ? server.args : [];
223
249
  const entry = typeof args[0] === 'string' ? args[0] : null;
224
250
  if (entry) {
225
- const resolved = path.resolve(entry.replaceAll('${CLAUDE_PLUGIN_ROOT}', packageRoot));
251
+ const pluginRoot = installChannel === 'plugin-marketplace' ? packageRoot : (env.CLAUDE_PLUGIN_ROOT || null);
252
+ if (entry.includes(MCP_PLACEHOLDER) && pluginRoot === null) {
253
+ return createCheck('mcp-config', 'MCP config', 'warn', `${label} starts \`${entry}\`. NOT VERIFIED: \`${MCP_PLACEHOLDER}\` is substituted by the Claude Code plugin runtime, and this is a ${installChannel} install with CLAUDE_PLUGIN_ROOT unset — so the file it names was not checked.`, `Verify it the way the plugin runtime would: CLAUDE_PLUGIN_ROOT=${packageRoot} memesh doctor`, { code: 'mcp-config.placeholder-unresolved', params: { entry, channel: installChannel } });
254
+ }
255
+ const resolved = pluginRoot === null
256
+ ? path.resolve(packageRoot, entry)
257
+ : path.resolve(entry.replaceAll(MCP_PLACEHOLDER, pluginRoot));
226
258
  if (!existsSyncImpl(resolved)) {
227
- 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 } });
259
+ return createCheck('mcp-config', 'MCP config', 'fail', `${label} starts \`${entry}\`, and that file is not in this install — so every memesh MCP tool fails to start.`, `Reinstall MeMesh; if you edited \`${label}\` by hand, point it back at \`${MCP_PLACEHOLDER}/dist/mcp/server.js\`.`, { code: 'mcp-config.entry-missing', params: { entry, resolved } });
228
260
  }
229
261
  }
230
- return createCheck('mcp-config', 'MCP config', 'pass', '.mcp.json is present, defines the memesh MCP server, and the script it starts exists.');
262
+ return createCheck('mcp-config', 'MCP config', 'pass', `${label} is present, defines the memesh MCP server, and the script it starts exists.`);
231
263
  }
232
264
  function extractHookScriptPaths(hooksConfig, packageRoot) {
233
265
  const hooks = hooksConfig.hooks;
@@ -235,8 +267,12 @@ function extractHookScriptPaths(hooksConfig, packageRoot) {
235
267
  return [];
236
268
  const scripts = new Set();
237
269
  for (const entries of Object.values(hooks)) {
238
- for (const entry of entries ?? []) {
239
- for (const hook of entry.hooks ?? []) {
270
+ if (!Array.isArray(entries))
271
+ continue;
272
+ for (const entry of entries) {
273
+ if (!Array.isArray(entry.hooks))
274
+ continue;
275
+ for (const hook of entry.hooks) {
240
276
  if (typeof hook.command !== 'string')
241
277
  continue;
242
278
  const command = hook.command.replace('${CLAUDE_PLUGIN_ROOT}/', '');
@@ -259,7 +295,10 @@ function inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncI
259
295
  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 } }),
260
296
  ];
261
297
  }
262
- const hookTypes = Object.keys(parsed.value.hooks ?? {});
298
+ const parsedHooks = parsed.value.hooks;
299
+ const hookTypes = parsedHooks && typeof parsedHooks === 'object' && !Array.isArray(parsedHooks)
300
+ ? Object.keys(parsedHooks)
301
+ : [];
263
302
  const missingTypes = EXPECTED_HOOK_TYPES.filter((type) => !hookTypes.includes(type));
264
303
  const configCheck = missingTypes.length > 0
265
304
  ? 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(', ') } })
@@ -602,7 +641,32 @@ function inspectNativeBinding(packageRoot, _existsSyncImpl, probeImpl = defaultN
602
641
  }
603
642
  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 } });
604
643
  }
605
- function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
644
+ function readVersionFromInstalledBinary(binaryPath, existsSyncImpl, readFileSyncImpl, realpathSyncImpl = fs.realpathSync) {
645
+ let resolved;
646
+ try {
647
+ resolved = realpathSyncImpl(binaryPath);
648
+ }
649
+ catch {
650
+ resolved = binaryPath;
651
+ }
652
+ let dir = path.dirname(resolved);
653
+ for (let depth = 0; depth < 8; depth += 1) {
654
+ const pkgPath = path.join(dir, 'package.json');
655
+ if (existsSyncImpl(pkgPath)) {
656
+ const parsed = parseJsonFile(pkgPath, readFileSyncImpl);
657
+ if (!parsed.ok)
658
+ return null;
659
+ const { name, version } = parsed.value;
660
+ return name === '@pcircle/memesh' && typeof version === 'string' ? version : null;
661
+ }
662
+ const parent = path.dirname(dir);
663
+ if (parent === dir)
664
+ return null;
665
+ dir = parent;
666
+ }
667
+ return null;
668
+ }
669
+ function inspectShellCli(installChannel, packageRoot, packageVersion, resolveShellMemeshImpl, existsSyncImpl, readFileSyncImpl) {
606
670
  const shellPath = resolveShellMemeshImpl();
607
671
  const isSameAsCurrent = shellPath ? path.resolve(shellPath).startsWith(path.resolve(packageRoot)) : false;
608
672
  const hasDistinctShellCli = !!shellPath && !isSameAsCurrent;
@@ -612,7 +676,25 @@ function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
612
676
  : 'Running from npm-global install — shell access available in this terminal.');
613
677
  }
614
678
  if (hasDistinctShellCli) {
615
- return createCheck('shell-cli', 'Shell CLI on PATH', 'pass', `\`memesh\` resolves to ${shellPath} (separate from this install at ${packageRoot}). Both paths coexist and share the same DB.`);
679
+ const shellVersion = readVersionFromInstalledBinary(shellPath, existsSyncImpl, readFileSyncImpl);
680
+ const shellIsBehind = shellVersion ? classifyBump(shellVersion, packageVersion) : null;
681
+ const thisIsBehind = shellVersion ? classifyBump(packageVersion, shellVersion) : null;
682
+ if (shellIsBehind) {
683
+ return createCheck('shell-cli', 'Shell CLI on PATH', 'warn', `\`memesh\` resolves to ${shellPath} (separate from this install at ${packageRoot}), and it is running ${shellVersion} — behind this install's ${packageVersion}. Both share the same DB, but an agent using this install and a human typing \`memesh\` in a terminal are running different code.`, 'Run `npm install -g @pcircle/memesh@latest` to bring the shell CLI up to date — a separate global install is never updated automatically by the plugin marketplace.');
684
+ }
685
+ if (thisIsBehind) {
686
+ const pluginHost = installChannel === 'plugin-marketplace' ? detectPluginHost(packageRoot) : null;
687
+ const fix = installChannel !== 'plugin-marketplace'
688
+ ? `Update this install (a ${installChannel}) to ${shellVersion} or newer via its own channel — see \`memesh status\`.`
689
+ : pluginHost
690
+ ? `Run \`${PLUGIN_REFRESH_COMMANDS[pluginHost]}\` to bring this plugin copy to ${shellVersion} (or newer).`
691
+ : `Bring this plugin copy to ${shellVersion} (or newer) with your host's refresh command — `
692
+ + `Claude Code: \`${PLUGIN_REFRESH_COMMANDS['claude-code']}\`; `
693
+ + `Codex: \`${PLUGIN_REFRESH_COMMANDS.codex}\`.`;
694
+ return createCheck('shell-cli', 'Shell CLI on PATH', 'warn', `\`memesh\` resolves to ${shellPath} (separate from this install at ${packageRoot}), and it is running ${shellVersion} — ahead of this install's ${packageVersion}.`, fix);
695
+ }
696
+ return createCheck('shell-cli', 'Shell CLI on PATH', 'pass', `\`memesh\` resolves to ${shellPath} (separate from this install at ${packageRoot}). Both paths coexist and share the same DB`
697
+ + (shellVersion ? `, both on ${packageVersion}.` : ' — could not read the shell copy\'s own version to compare.'));
616
698
  }
617
699
  if (installChannel === 'plugin-marketplace') {
618
700
  const host = detectPluginHost(packageRoot) === 'codex' ? 'Codex CLI' : 'Claude Code';
@@ -623,6 +705,282 @@ function inspectShellCli(installChannel, packageRoot, resolveShellMemeshImpl) {
623
705
  ? `\`memesh\` resolves to ${shellPath}.`
624
706
  : `No shell-PATH \`memesh\` detected. If you want terminal access, run \`npm install -g @pcircle/memesh\` (this install is a ${installChannel}, so the check is informational only).`);
625
707
  }
708
+ function defaultMarketplaceHeadSha(host) {
709
+ if (host === 'codex') {
710
+ const codexHome = pluginHostConfigRoot('codex');
711
+ return readCodexInstallRevision(path.join(codexHome, '.tmp', 'marketplaces', 'pcircle-memesh'), fs.readFileSync);
712
+ }
713
+ const dir = path.join(pluginHostConfigRoot('claude-code'), 'plugins', 'marketplaces', 'pcircle-memesh');
714
+ try {
715
+ const out = execFileSync('git', ['-C', dir, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
716
+ return /^[0-9a-f]{40}$/.test(out) ? out : null;
717
+ }
718
+ catch {
719
+ return null;
720
+ }
721
+ }
722
+ function readCodexInstallRevision(root, readFileSyncImpl) {
723
+ const parsed = parseJsonFile(path.join(root, '.codex-marketplace-install.json'), readFileSyncImpl);
724
+ if (!parsed.ok)
725
+ return null;
726
+ const rev = parsed.value.revision;
727
+ return typeof rev === 'string' && /^[0-9a-f]{40}$/.test(rev) ? rev : null;
728
+ }
729
+ function pluginCacheUnverifiable(host, missing) {
730
+ const hostLabel = host === 'codex' ? 'Codex' : 'Claude Code';
731
+ const command = PLUGIN_REFRESH_COMMANDS[host];
732
+ return createCheck('plugin-cache', `Plugin cache source record is current (${hostLabel})`, 'warn', `Could not tell whether the plugin cache matches the marketplace: ${missing}. The version alone cannot answer this — two builds can carry the same version.`, `Run \`${command}\` — it re-stages the cache from the marketplace and records the commit, after which this check can answer.`, { code: 'plugin-cache.unverifiable', params: { host: hostLabel, command } });
733
+ }
734
+ function readClaudePluginEntries(registryPath, readFileSyncImpl, existsSyncImpl) {
735
+ if (!existsSyncImpl(registryPath)) {
736
+ return { exists: false, readable: false, defined: false, malformed: false, entries: [] };
737
+ }
738
+ const parsed = parseJsonFile(registryPath, readFileSyncImpl);
739
+ if (!parsed.ok)
740
+ return { exists: true, readable: false, defined: false, malformed: false, entries: [] };
741
+ const raw = parsed.value?.plugins?.['memesh@pcircle-memesh'];
742
+ const entries = Array.isArray(raw)
743
+ ? raw.filter((entry) => !!entry && typeof entry === 'object')
744
+ : [];
745
+ return {
746
+ exists: true,
747
+ readable: true,
748
+ defined: raw !== undefined,
749
+ malformed: raw !== undefined && (!Array.isArray(raw) || entries.length !== raw.length),
750
+ entries,
751
+ };
752
+ }
753
+ function defaultPluginCacheDiscovery(readFileSyncImpl, existsSyncImpl) {
754
+ const discovered = [];
755
+ const claudeConfigRoot = pluginHostConfigRoot('claude-code');
756
+ const claudeRegistry = path.join(claudeConfigRoot, 'plugins', 'installed_plugins.json');
757
+ const claudeCacheRoot = path.join(claudeConfigRoot, 'plugins', 'cache', 'pcircle-memesh', 'memesh');
758
+ const registry = readClaudePluginEntries(claudeRegistry, readFileSyncImpl, existsSyncImpl);
759
+ const entries = registry.entries;
760
+ if (registry.malformed) {
761
+ discovered.push({
762
+ host: 'claude-code',
763
+ packageRoot: claudeCacheRoot,
764
+ installedPluginsPath: claudeRegistry,
765
+ unverifiableReason: 'installed_plugins.json has a malformed memesh entry',
766
+ });
767
+ }
768
+ else if (entries.length > 0) {
769
+ const cachesByRoot = new Map();
770
+ for (const entry of entries) {
771
+ const recordedInstallPath = typeof entry.installPath === 'string' && entry.installPath.length > 0
772
+ ? entry.installPath
773
+ : null;
774
+ const installPath = recordedInstallPath ?? claudeCacheRoot;
775
+ const cache = {
776
+ host: 'claude-code', packageRoot: installPath, installedPluginsPath: claudeRegistry,
777
+ ...(!recordedInstallPath ? { unverifiableReason: 'an installed_plugins.json entry has no usable installPath' }
778
+ : !existsSyncImpl(installPath) ? { unverifiableReason: `the recorded plugin cache does not exist at ${installPath}` } : {}),
779
+ };
780
+ const rootKey = path.resolve(installPath);
781
+ const existing = cachesByRoot.get(rootKey);
782
+ if (!existing)
783
+ cachesByRoot.set(rootKey, cache);
784
+ else if (!existing.unverifiableReason && cache.unverifiableReason) {
785
+ existing.unverifiableReason = cache.unverifiableReason;
786
+ }
787
+ }
788
+ discovered.push(...cachesByRoot.values());
789
+ }
790
+ else if (registry.defined) {
791
+ const cachedRoots = versionedPluginCacheRoots(claudeCacheRoot);
792
+ if (cachedRoots.length > 0) {
793
+ discovered.push({
794
+ host: 'claude-code',
795
+ packageRoot: cachedRoots[cachedRoots.length - 1],
796
+ installedPluginsPath: claudeRegistry,
797
+ unverifiableReason: 'installed_plugins.json records no active memesh install while a versioned plugin cache still exists',
798
+ });
799
+ }
800
+ }
801
+ else if (!registry.readable) {
802
+ const cachedRoots = versionedPluginCacheRoots(claudeCacheRoot);
803
+ const cachedRoot = cachedRoots[cachedRoots.length - 1];
804
+ if (cachedRoot) {
805
+ discovered.push({
806
+ host: 'claude-code', packageRoot: cachedRoot, installedPluginsPath: claudeRegistry,
807
+ unverifiableReason: registry.exists
808
+ ? 'installed_plugins.json could not be read or parsed'
809
+ : 'installed_plugins.json does not exist while a versioned plugin cache still exists',
810
+ });
811
+ }
812
+ }
813
+ const codexHome = pluginHostConfigRoot('codex');
814
+ const codexCacheRoot = path.join(codexHome, 'plugins', 'cache', 'pcircle-memesh', 'memesh');
815
+ const codexRoots = versionedPluginCacheRoots(codexCacheRoot);
816
+ if (codexRoots.length === 1) {
817
+ discovered.push({ host: 'codex', packageRoot: codexRoots[0] });
818
+ }
819
+ else if (codexRoots.length > 1) {
820
+ discovered.push({
821
+ host: 'codex',
822
+ packageRoot: codexRoots[codexRoots.length - 1],
823
+ unverifiableReason: `several versioned Codex plugin cache directories exist under ${codexCacheRoot}`,
824
+ });
825
+ }
826
+ return discovered;
827
+ }
828
+ function inspectPluginCacheCurrency(installChannel, pluginHost, packageRoot, installedPluginsPath, readFileSyncImpl, existsSyncImpl, marketplaceHeadShaImpl) {
829
+ if (installChannel !== 'plugin-marketplace')
830
+ return null;
831
+ const host = pluginHost === 'codex' ? 'codex' : 'claude-code';
832
+ const hostLabel = host === 'codex' ? 'Codex' : 'Claude Code';
833
+ const command = PLUGIN_REFRESH_COMMANDS[host];
834
+ let installedSha;
835
+ let installedMissing;
836
+ if (host === 'codex') {
837
+ installedSha = readCodexInstallRevision(packageRoot, readFileSyncImpl);
838
+ installedMissing = 'the plugin cache carries no readable .codex-marketplace-install.json revision';
839
+ }
840
+ else {
841
+ const registryPath = installedPluginsPath ?? path.join(pluginHostConfigRoot('claude-code'), 'plugins', 'installed_plugins.json');
842
+ const registry = readClaudePluginEntries(registryPath, readFileSyncImpl, existsSyncImpl);
843
+ const entries = registry.entries;
844
+ const here = path.resolve(packageRoot);
845
+ const matching = entries.filter(e => typeof e?.installPath === 'string' && path.resolve(e.installPath) === here);
846
+ const soleEntry = entries.length === 1 ? entries[0] : undefined;
847
+ const soleEntryHasPath = typeof soleEntry?.installPath === 'string' && soleEntry.installPath.length > 0;
848
+ const entry = matching.length === 1 ? matching[0] : undefined;
849
+ installedSha = !registry.malformed
850
+ && typeof entry?.gitCommitSha === 'string'
851
+ && /^[0-9a-f]{40}$/.test(entry.gitCommitSha)
852
+ ? entry.gitCommitSha
853
+ : null;
854
+ if (!registry.exists) {
855
+ installedMissing = `installed_plugins.json does not exist at ${registryPath}`;
856
+ }
857
+ else if (!registry.readable) {
858
+ installedMissing = 'installed_plugins.json could not be read or parsed';
859
+ }
860
+ else if (registry.malformed) {
861
+ installedMissing = 'installed_plugins.json has a malformed memesh entry';
862
+ }
863
+ else if (!registry.defined) {
864
+ installedMissing = 'installed_plugins.json has no memesh@pcircle-memesh entry';
865
+ }
866
+ else if (entries.length === 0) {
867
+ installedMissing = 'installed_plugins.json records no active memesh install for this cache';
868
+ }
869
+ else if (matching.length > 1) {
870
+ installedMissing = `installed_plugins.json lists ${matching.length} memesh entries for this install (${packageRoot})`;
871
+ }
872
+ else if (matching.length === 0 && soleEntryHasPath) {
873
+ installedMissing = `the only memesh entry in installed_plugins.json names another install, not this one (${packageRoot})`;
874
+ }
875
+ else if (matching.length === 0 && soleEntry) {
876
+ installedMissing = 'the only memesh entry in installed_plugins.json has no usable installPath to identify this cache';
877
+ }
878
+ else if (entries.length > 1 && matching.length === 0) {
879
+ installedMissing = `installed_plugins.json lists ${entries.length} memesh entries and none of them is this install (${packageRoot})`;
880
+ }
881
+ else {
882
+ installedMissing = 'installed_plugins.json does not record the commit this plugin was installed from';
883
+ }
884
+ }
885
+ const marketplaceSha = marketplaceHeadShaImpl(host);
886
+ if (!installedSha || !marketplaceSha) {
887
+ const missing = !installedSha ? installedMissing : `the ${hostLabel} marketplace snapshot has no readable commit`;
888
+ return pluginCacheUnverifiable(host, missing);
889
+ }
890
+ if (installedSha === marketplaceSha) {
891
+ return createCheck('plugin-cache', `Plugin cache source record is current (${hostLabel})`, 'pass', `The plugin cache records marketplace commit ${marketplaceSha.slice(0, 8)}, which matches the current marketplace snapshot.`);
892
+ }
893
+ return createCheck('plugin-cache', `Plugin cache source record is current (${hostLabel})`, 'warn', `The plugin cache records commit ${installedSha.slice(0, 8)}, but the marketplace has moved to ${marketplaceSha.slice(0, 8)} under the same version — ${hostLabel} does not normally refresh a cache whose version did not change, so refresh the cache before relying on the newer marketplace code.`, `Run \`${command}\` to refresh the cache in place, then restart ${hostLabel}.`, { code: 'plugin-cache.stale', params: { installed: installedSha.slice(0, 8), marketplace: marketplaceSha.slice(0, 8), host: hostLabel, command } });
894
+ }
895
+ function annotateNpmGlobalPluginCacheVersion(check, discoveredPackageRoot, hostLabel, runningVersion) {
896
+ const cacheRoot = path.dirname(discoveredPackageRoot);
897
+ const discoveredVersion = path.basename(discoveredPackageRoot);
898
+ let amended = check;
899
+ if (discoveredVersion !== runningVersion && classifyBump(runningVersion, discoveredVersion)) {
900
+ const skewNote = `This npm-global install is on ${runningVersion}; the ${hostLabel} plugin cache is on ${discoveredVersion}. `
901
+ + 'The plugin marketplace\'s own auto-updater only ever refreshes its plugin copy — it cannot and will not update this separate npm-global install.';
902
+ const skewFix = `Run \`memesh update\` to bring this npm-global install to ${discoveredVersion} (or newer) — it does not update itself automatically.`;
903
+ amended = {
904
+ ...amended,
905
+ status: amended.status === 'pass' ? 'warn' : amended.status,
906
+ summary: `${amended.summary} ${skewNote}`,
907
+ fix: amended.fix ? `${amended.fix} Also: ${skewFix}` : skewFix,
908
+ code: undefined,
909
+ params: undefined,
910
+ };
911
+ }
912
+ const cachedVersions = versionedPluginCacheRoots(cacheRoot);
913
+ if (cachedVersions.length > 2) {
914
+ amended = {
915
+ ...amended,
916
+ summary: `${amended.summary} ${cachedVersions.length} versioned copies of the ${hostLabel} plugin are cached under ${cacheRoot}; old ones are never removed automatically. `
917
+ + `Delete ones you no longer need once no ${hostLabel} process is using them, e.g. \`rm -rf "${cachedVersions[0]}"\`.`,
918
+ };
919
+ }
920
+ return amended;
921
+ }
922
+ function inspectClaudeChannelRegistration(existsSyncImpl, readFileSyncImpl) {
923
+ const configPath = path.join(homeDir(), '.claude.json');
924
+ let parsed = null;
925
+ if (existsSyncImpl(configPath)) {
926
+ try {
927
+ const value = JSON.parse(readFileSyncImpl(configPath, 'utf8'));
928
+ if (!value || typeof value !== 'object' || Array.isArray(value))
929
+ throw new Error('not an object');
930
+ parsed = value;
931
+ }
932
+ catch {
933
+ return createCheck('claude-channel', 'Claude Channel registration', 'warn', 'The canonical Claude user config could not be read as JSON, so the memesh-channel registration is malformed or unverifiable.', 'Repair the owner-controlled Claude config with `claude mcp remove memesh-channel` and re-run `memesh agent setup claude` to obtain a fresh registration command.');
934
+ }
935
+ }
936
+ const servers = parsed?.mcpServers;
937
+ const server = servers && typeof servers === 'object' && !Array.isArray(servers)
938
+ ? servers['memesh-channel']
939
+ : undefined;
940
+ if (server === undefined) {
941
+ return createCheck('claude-channel', 'Claude Channel registration', 'warn', 'No user-scoped memesh-channel registration was found. Durable MCP/inbox messaging can still work, but live Claude Channel notification is inactive. The upstream research-preview channel remains opt-in.', 'If you want the opt-in channel, run `memesh agent setup claude` and then register the printed user-scoped MCP command.');
942
+ }
943
+ const record = server && typeof server === 'object' && !Array.isArray(server)
944
+ ? server
945
+ : null;
946
+ const command = record?.command;
947
+ const rawArgs = record?.args;
948
+ const args = Array.isArray(rawArgs) ? rawArgs : null;
949
+ const configIndex = args?.indexOf('--config') ?? -1;
950
+ const target = args && configIndex >= 0 && typeof args[configIndex + 1] === 'string'
951
+ ? args[configIndex + 1]
952
+ : null;
953
+ let targetConfigValid = false;
954
+ if (target) {
955
+ try {
956
+ const config = readHostConfigFile(target);
957
+ const required = ['router_socket', 'token_file', 'project', 'principal_id'];
958
+ targetConfigValid = config.server_name === 'memesh-channel'
959
+ && required.every((key) => {
960
+ const value = config[key];
961
+ return typeof value === 'string'
962
+ && value.length > 0
963
+ && Buffer.byteLength(value) <= 4096;
964
+ });
965
+ }
966
+ catch {
967
+ targetConfigValid = false;
968
+ }
969
+ }
970
+ const coherent = command === 'memesh-host-claude'
971
+ && args !== null
972
+ && args.length === 2
973
+ && configIndex === 0
974
+ && target !== null
975
+ && targetConfigValid;
976
+ if (!coherent) {
977
+ const reason = command !== 'memesh-host-claude' || args === null || configIndex !== 0 || target === null
978
+ ? 'the command or --config declaration is malformed'
979
+ : 'the declared owner config target is missing, insecure, malformed, or incomplete';
980
+ return createCheck('claude-channel', 'Claude Channel registration', 'warn', `The user-scoped memesh-channel registration is present but ${reason}. Live Claude Channel notification is not established.`, 'Remove and re-register the owner-controlled memesh-channel entry with `memesh agent setup claude`; keep its generated config file owner-private.');
981
+ }
982
+ return createInfo('claude-channel', 'Claude Channel registration', 'The user-scoped memesh-channel registration and owner-private config target are coherent (CONFIGURED). Development-channel admission and agent surfacing are not verified; durable MCP/inbox messaging remains a separate path.');
983
+ }
626
984
  function inspectDashboardArtifact(packageRoot, existsSyncImpl) {
627
985
  const dashboardPath = path.join(packageRoot, 'dashboard', 'dist', 'index.html');
628
986
  if (!existsSyncImpl(dashboardPath)) {
@@ -711,7 +1069,13 @@ async function inspectUpdateStatus(packageVersion, getUpdateCheckImpl, installSu
711
1069
  return createCheck('update-status', 'Update status', 'pass', `Running pre-release version (${packageVersion}), npm latest is ${update.latestVersion}`);
712
1070
  }
713
1071
  }
714
- 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'
1072
+ const checkedHoursAgo = update.lastSuccessfulCheckAt === null
1073
+ ? null
1074
+ : (Date.now() - Date.parse(update.lastSuccessfulCheckAt)) / 3600_000;
1075
+ const checkedAgo = formatHoursAgo(checkedHoursAgo);
1076
+ return createCheck('update-status', 'Update status', update.freshness === 'stale' ? 'warn' : 'pass', update.freshness === 'stale'
1077
+ ? `As of the last check (${checkedAgo}), ${packageVersion} was the latest version — that check is more than 24h old, so a newer release may exist since. Doctor never makes a live registry call itself.`
1078
+ : `As of the last check (${checkedAgo}), ${packageVersion} was the latest version.`, update.freshness === 'stale'
715
1079
  ? 'Run `memesh status` while online to refresh cached update metadata.'
716
1080
  : undefined, update.freshness === 'stale'
717
1081
  ? { code: 'update-status.stale', params: { version: packageVersion } }
@@ -742,7 +1106,7 @@ function verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl, ins
742
1106
  catch (err) {
743
1107
  return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', `skills-manifest.json is unreadable (${err instanceof Error ? err.message : 'parse error'}).`, `Reinstall the package: ${reinstall} If the problem persists open an issue.`, { code: 'skills-manifest.unreadable', params: { detail: err instanceof Error ? err.message : 'parse error' } });
744
1108
  }
745
- const entries = manifest.entries ?? [];
1109
+ const entries = Array.isArray(manifest.entries) ? manifest.entries : [];
746
1110
  if (entries.length === 0) {
747
1111
  return createCheck('skills-manifest', 'Skills + hooks integrity', 'fail', 'skills-manifest.json contains zero entries.', `Reinstall the package: ${reinstall}`, { code: 'skills-manifest.empty' });
748
1112
  }
@@ -806,7 +1170,7 @@ function probeInstalledMessageCapability(packageRoot) {
806
1170
  const tool = (await client.listTools()).tools.find((candidate) => candidate.name === 'message');
807
1171
  assert.ok(tool, 'installed MCP did not advertise message');
808
1172
  const actions = tool.inputSchema?.properties?.action?.enum;
809
- assert.deepEqual(actions, ['send', 'poll', 'fetch', 'intake', 'ack', 'disposition', 'activation', 'receipts']);
1173
+ assert.deepEqual(actions, ['send', 'poll', 'discover', 'fetch', 'intake', 'ack', 'disposition', 'activation', 'receipts']);
810
1174
  } finally { await client.close(); }
811
1175
  `], {
812
1176
  cwd: packageRoot,
@@ -825,11 +1189,11 @@ function probeInstalledMessageCapability(packageRoot) {
825
1189
  }
826
1190
  function inspectMessageCapability(packageRoot, enabled, probe) {
827
1191
  if (!enabled) {
828
- return createInfo('message-capability', 'Message adapter imports', 'Not verified (opt-in). Set MEMESH_DOCTOR_PROBE_MESSAGE_CAPABILITY=1 to start this installed MCP and verify its eight-action message schema plus bundled host-adapter imports. This does not check a live router socket or host registration.');
1192
+ return createInfo('message-capability', 'Message adapter imports', 'Not verified (opt-in). Set MEMESH_DOCTOR_PROBE_MESSAGE_CAPABILITY=1 to start this installed MCP and verify its nine-action message schema plus bundled host-adapter imports. This does not check a live router socket or host registration.');
829
1193
  }
830
1194
  const result = probe(packageRoot);
831
1195
  if (result.ok)
832
- return createCheck('message-capability', 'Message adapter imports', 'pass', 'This installed MCP advertised the eight-action message schema and all bundled host adapters imported successfully. No live router socket, host registration, or host acceptance was verified.');
1196
+ return createCheck('message-capability', 'Message adapter imports', 'pass', 'This installed MCP advertised the nine-action message schema and all bundled host adapters imported successfully. No live router socket, host registration, or host acceptance was verified.');
833
1197
  return createCheck('message-capability', 'Message adapter imports', 'fail', `Installed message capability probe failed: ${result.message}.`, 'Reinstall or rebuild this package, then retry with MEMESH_DOCTOR_PROBE_MESSAGE_CAPABILITY=1.', { code: 'message-capability.probe-failed', params: { detail: result.message } });
834
1198
  }
835
1199
  async function defaultMessageRouterStatusProbe() {
@@ -909,6 +1273,33 @@ async function inspectEmbeddingProbe(capabilities, probeCapabilities, embedTextI
909
1273
  clearTimeout(timer);
910
1274
  }
911
1275
  }
1276
+ const LLM_TELEMETRY_HEALTH_WINDOW_DAYS = 7;
1277
+ const LLM_TELEMETRY_HEALTH_MIN_CALLS = 3;
1278
+ function inspectLlmTelemetryHealth(db, windowDays = LLM_TELEMETRY_HEALTH_WINDOW_DAYS, minCalls = LLM_TELEMETRY_HEALTH_MIN_CALLS) {
1279
+ let summaries;
1280
+ try {
1281
+ summaries = summariseTelemetry(windowDays, db);
1282
+ }
1283
+ catch {
1284
+ return undefined;
1285
+ }
1286
+ if (summaries.length === 0)
1287
+ return undefined;
1288
+ const broken = summaries
1289
+ .filter((s) => s.total_calls >= minCalls && s.successes === 0)
1290
+ .sort((a, b) => b.total_calls - a.total_calls);
1291
+ if (broken.length > 0) {
1292
+ const detail = broken.map((s) => `${s.flow} (${s.total_calls} call${s.total_calls === 1 ? '' : 's'}, 0 succeeded)`).join(', ');
1293
+ return createCheck('llm_telemetry_health', 'AI feature health', 'warn', `${broken.length} AI-backed feature${broken.length === 1 ? '' : 's'} failed every call in the last `
1294
+ + `${windowDays} days: ${detail}. Those features are silently doing nothing.`, 'Run `memesh telemetry` for the full per-flow detail, then check the model/provider configured for '
1295
+ + 'the failing flow. `memesh doctor --probe` confirms whether it answers a live call.', { code: 'llm-telemetry.silent-failure', params: { count: broken.length, detail, windowDays } });
1296
+ }
1297
+ const totalCalls = summaries.reduce((n, s) => n + s.total_calls, 0);
1298
+ const totalSuccesses = summaries.reduce((n, s) => n + s.successes, 0);
1299
+ const rate = totalCalls > 0 ? Math.round((totalSuccesses / totalCalls) * 100) : 100;
1300
+ return createInfo('llm_telemetry_health', 'AI feature health', `${summaries.length} AI-backed flow${summaries.length === 1 ? '' : 's'} made ${totalCalls} call(s) in the `
1301
+ + `last ${windowDays} days; ${rate}% succeeded.`);
1302
+ }
912
1303
  async function inspectLlmProbe(capabilities, probeCapabilities, probeProviderImpl) {
913
1304
  const llm = capabilities.llm;
914
1305
  if (!llm) {
@@ -938,7 +1329,7 @@ function summarizeOverallStatus(checks) {
938
1329
  return 'PASS';
939
1330
  }
940
1331
  export async function runDoctor(options) {
941
- 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, agentMessageStoragePolicy, nativeBindingProbeImpl, resolveShellMemeshImpl = defaultResolveShellMemesh, probeMessageCapability = process.env.MEMESH_DOCTOR_PROBE_MESSAGE_CAPABILITY === '1', messageCapabilityProbeImpl = probeInstalledMessageCapability, probeMessageRouterStatus = process.env.MEMESH_DOCTOR_PROBE_MESSAGE_ROUTER === '1', messageRouterStatusProbeImpl = defaultMessageRouterStatusProbe, } = options;
1332
+ const { packageRoot, packageVersion, probeHttp = false, probeCapabilities = false, embedTextImpl = embedText, probeProviderImpl = probeProvider, httpBaseUrl = 'http://127.0.0.1:3737', platform = process.platform, envImpl = process.env, openDatabaseImpl = openDatabase, closeDatabaseImpl = closeDatabase, isDatabaseOpenImpl = isDatabaseOpen, detectCapabilitiesImpl = detectCapabilities, getConfigPathImpl = getConfigPath, getUpdateCheckImpl = getUpdateCheck, getCurrentInstallChannelImpl = getCurrentInstallChannel, installedPluginsPathImpl, marketplaceHeadShaImpl = defaultMarketplaceHeadSha, pluginCacheDiscoveryImpl, getInstallChannelSupportImpl = getInstallChannelSupport, existsSyncImpl = fs.existsSync, readFileSyncImpl = fs.readFileSync, statSyncImpl = fs.statSync, fetchImpl = fetch, agentMessageStoragePolicy, nativeBindingProbeImpl, resolveShellMemeshImpl = defaultResolveShellMemesh, probeMessageCapability = process.env.MEMESH_DOCTOR_PROBE_MESSAGE_CAPABILITY === '1', messageCapabilityProbeImpl = probeInstalledMessageCapability, probeMessageRouterStatus = process.env.MEMESH_DOCTOR_PROBE_MESSAGE_ROUTER === '1', messageRouterStatusProbeImpl = defaultMessageRouterStatusProbe, } = options;
942
1333
  const wasDbOpenBeforeUs = isDatabaseOpenImpl();
943
1334
  const safeCloseDatabaseImpl = wasDbOpenBeforeUs
944
1335
  ? () => undefined
@@ -993,7 +1384,14 @@ export async function runDoctor(options) {
993
1384
  ? 'The vector index could not be read, so how much of your memory semantic recall can see is unknown'
994
1385
  : `${missingVectors} memor${missingVectors === 1 ? 'y has' : 'ies have'} no search vector, `
995
1386
  + 'so semantic recall cannot find them (keyword search still works)';
996
- dbChecks.push(createCheck('vector_index', 'Vector Index', 'warn', owed, `Run 'memesh reindex' to fix. This will restore full search functionality.`, { code: 'vector-index.stale', params: { missing: missingVectors ?? -1 } }));
1387
+ const noEmbedderConfigured = detectCapabilitiesImpl().embeddings === 'tfidf';
1388
+ const vectorIndexFix = noEmbedderConfigured
1389
+ ? `No embedder is configured, so reindex has nothing to embed with — run 'memesh config set embedder.provider ollama' (or 'openai') first, then 'memesh reindex'.`
1390
+ : `Run 'memesh reindex' to fix. This will restore full search functionality.`;
1391
+ dbChecks.push(createCheck('vector_index', 'Vector Index', 'warn', owed, vectorIndexFix, {
1392
+ code: noEmbedderConfigured ? 'vector-index.stale-no-embedder' : 'vector-index.stale',
1393
+ params: { missing: missingVectors ?? -1 },
1394
+ }));
997
1395
  }
998
1396
  const generation = readVectorGeneration();
999
1397
  if (generation.state !== 'none') {
@@ -1074,6 +1472,9 @@ export async function runDoctor(options) {
1074
1472
  dbChecks.push(createInfo('citation_compliance', 'Memory citation rate', `${cited} of ${citationTotal} session(s) with injected memories cited at least one (${rate}%).`));
1075
1473
  }
1076
1474
  }
1475
+ const llmTelemetryHealth = inspectLlmTelemetryHealth(db);
1476
+ if (llmTelemetryHealth)
1477
+ dbChecks.push(llmTelemetryHealth);
1077
1478
  }
1078
1479
  catch (err) {
1079
1480
  const message = err instanceof Error ? err.message : 'unknown database error';
@@ -1141,17 +1542,53 @@ export async function runDoctor(options) {
1141
1542
  }
1142
1543
  }
1143
1544
  checks.push(inspectConfigFile(existsSyncImpl, readFileSyncImpl, getConfigPathImpl, detectCapabilitiesImpl().llm));
1144
- checks.push(inspectMcpConfig(packageRoot, existsSyncImpl, readFileSyncImpl));
1545
+ checks.push(inspectMcpConfig(packageRoot, install, existsSyncImpl, readFileSyncImpl, envImpl));
1145
1546
  checks.push(...inspectHooksConfig(packageRoot, platform, existsSyncImpl, readFileSyncImpl, statSyncImpl));
1146
- const wiring = inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir(), install, installedPluginsPathImpl, detectPluginHost(packageRoot));
1547
+ const pluginHost = detectPluginHost(packageRoot);
1548
+ let codexPluginCacheDetected = pluginHost === 'codex';
1549
+ let claudePluginCacheDetected = pluginHost === 'claude-code';
1550
+ const wiring = inspectHookWiring(existsSyncImpl, readFileSyncImpl, memeshDir(), install, installedPluginsPathImpl, pluginHost);
1551
+ const pluginCache = inspectPluginCacheCurrency(install, pluginHost, packageRoot, installedPluginsPathImpl, readFileSyncImpl, existsSyncImpl, marketplaceHeadShaImpl);
1552
+ if (pluginCache)
1553
+ checks.push(pluginCache);
1554
+ if (install === 'npm-global') {
1555
+ const discoveredCounts = new Map();
1556
+ const discoveredPluginCaches = pluginCacheDiscoveryImpl
1557
+ ? pluginCacheDiscoveryImpl()
1558
+ : defaultPluginCacheDiscovery(readFileSyncImpl, existsSyncImpl);
1559
+ for (const discovered of discoveredPluginCaches) {
1560
+ if (discovered.host === 'codex')
1561
+ codexPluginCacheDetected = true;
1562
+ if (discovered.host === 'claude-code')
1563
+ claudePluginCacheDetected = true;
1564
+ let check = discovered.unverifiableReason
1565
+ ? pluginCacheUnverifiable(discovered.host, discovered.unverifiableReason)
1566
+ : inspectPluginCacheCurrency('plugin-marketplace', discovered.host, discovered.packageRoot, discovered.installedPluginsPath, readFileSyncImpl, existsSyncImpl, marketplaceHeadShaImpl);
1567
+ if (check && !discovered.unverifiableReason) {
1568
+ check = annotateNpmGlobalPluginCacheVersion(check, discovered.packageRoot, discovered.host === 'codex' ? 'Codex' : 'Claude Code', packageVersion);
1569
+ }
1570
+ if (check) {
1571
+ const hostName = discovered.host;
1572
+ const index = (discoveredCounts.get(discovered.host) ?? 0) + 1;
1573
+ discoveredCounts.set(discovered.host, index);
1574
+ checks.push({ ...check, id: `plugin-cache-${hostName}${index === 1 ? '' : `-${index}`}` });
1575
+ }
1576
+ }
1577
+ }
1147
1578
  checks.push(wiring);
1579
+ if (claudePluginCacheDetected) {
1580
+ checks.push(inspectClaudeChannelRegistration(existsSyncImpl, readFileSyncImpl));
1581
+ }
1582
+ const codexSessionSetup = inspectCodexSessionSetup(codexPluginCacheDetected, existsSyncImpl);
1583
+ if (codexSessionSetup)
1584
+ checks.push(codexSessionSetup);
1148
1585
  const captureWired = wiring.status === 'pass'
1149
1586
  && (wiring.params === undefined || wiring.params.captureWired === 1);
1150
1587
  checks.push(inspectHookActivity(openDatabaseImpl, safeCloseDatabaseImpl, existsSyncImpl, statSyncImpl, captureWired));
1151
1588
  checks.push(inspectDashboardArtifact(packageRoot, existsSyncImpl));
1152
1589
  checks.push(inspectNodeRuntime(packageRoot, existsSyncImpl, readFileSyncImpl));
1153
1590
  checks.push(inspectNativeBinding(packageRoot, existsSyncImpl, nativeBindingProbeImpl));
1154
- checks.push(inspectShellCli(install, packageRoot, resolveShellMemeshImpl));
1591
+ checks.push(inspectShellCli(install, packageRoot, packageVersion, resolveShellMemeshImpl, existsSyncImpl, readFileSyncImpl));
1155
1592
  checks.push(verifySkillsManifest(packageRoot, existsSyncImpl, readFileSyncImpl, installSupport));
1156
1593
  checks.push(inspectMessageCapability(packageRoot, probeMessageCapability, messageCapabilityProbeImpl));
1157
1594
  checks.push(await inspectMessageRouterStatus(probeMessageRouterStatus, messageRouterStatusProbeImpl));