@specship/specship-darwin-arm64 0.28.0 → 0.30.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 (324) hide show
  1. package/bin/specship +1 -1
  2. package/lib/dist/.claude-plugin/plugin.json +15 -3
  3. package/lib/dist/activation/cheatsheet.d.ts +10 -7
  4. package/lib/dist/activation/cheatsheet.d.ts.map +1 -1
  5. package/lib/dist/activation/cheatsheet.js +21 -5
  6. package/lib/dist/activation/cheatsheet.js.map +1 -1
  7. package/lib/dist/activation/hook-output.d.ts +75 -2
  8. package/lib/dist/activation/hook-output.d.ts.map +1 -1
  9. package/lib/dist/activation/hook-output.js +114 -3
  10. package/lib/dist/activation/hook-output.js.map +1 -1
  11. package/lib/dist/activation/search-intercept.d.ts +20 -7
  12. package/lib/dist/activation/search-intercept.d.ts.map +1 -1
  13. package/lib/dist/activation/search-intercept.js +27 -5
  14. package/lib/dist/activation/search-intercept.js.map +1 -1
  15. package/lib/dist/activation/steering.d.ts +7 -4
  16. package/lib/dist/activation/steering.d.ts.map +1 -1
  17. package/lib/dist/activation/steering.js +10 -8
  18. package/lib/dist/activation/steering.js.map +1 -1
  19. package/lib/dist/agents/detect.d.ts +32 -0
  20. package/lib/dist/agents/detect.d.ts.map +1 -0
  21. package/lib/dist/agents/detect.js +54 -0
  22. package/lib/dist/agents/detect.js.map +1 -0
  23. package/lib/dist/agents/recent-project.d.ts +18 -0
  24. package/lib/dist/agents/recent-project.d.ts.map +1 -0
  25. package/lib/dist/agents/recent-project.js +92 -0
  26. package/lib/dist/agents/recent-project.js.map +1 -0
  27. package/lib/dist/analytics/canonical-tools.d.ts +23 -0
  28. package/lib/dist/analytics/canonical-tools.d.ts.map +1 -0
  29. package/lib/dist/analytics/canonical-tools.js +36 -0
  30. package/lib/dist/analytics/canonical-tools.js.map +1 -0
  31. package/lib/dist/analytics/canonical-tools.json +16 -0
  32. package/lib/dist/analytics/specship-impact.d.ts +3 -2
  33. package/lib/dist/analytics/specship-impact.d.ts.map +1 -1
  34. package/lib/dist/analytics/specship-impact.js +6 -4
  35. package/lib/dist/analytics/specship-impact.js.map +1 -1
  36. package/lib/dist/bin/specship.js +252 -170
  37. package/lib/dist/bin/specship.js.map +1 -1
  38. package/lib/dist/bin/uninstall.js +2 -2
  39. package/lib/dist/bin/uninstall.js.map +1 -1
  40. package/lib/dist/claudemd/audit.d.ts +12 -3
  41. package/lib/dist/claudemd/audit.d.ts.map +1 -1
  42. package/lib/dist/claudemd/audit.js +65 -22
  43. package/lib/dist/claudemd/audit.js.map +1 -1
  44. package/lib/dist/commands/specship/spec.md +16 -1
  45. package/lib/dist/db/migrations.d.ts +1 -1
  46. package/lib/dist/db/migrations.d.ts.map +1 -1
  47. package/lib/dist/db/migrations.js +18 -4
  48. package/lib/dist/db/migrations.js.map +1 -1
  49. package/lib/dist/db/schema.sql +8 -0
  50. package/lib/dist/db/spec-queries.d.ts +13 -1
  51. package/lib/dist/db/spec-queries.d.ts.map +1 -1
  52. package/lib/dist/db/spec-queries.js +21 -0
  53. package/lib/dist/db/spec-queries.js.map +1 -1
  54. package/lib/dist/design-md/check.d.ts +51 -0
  55. package/lib/dist/design-md/check.d.ts.map +1 -0
  56. package/lib/dist/design-md/check.js +335 -0
  57. package/lib/dist/design-md/check.js.map +1 -0
  58. package/lib/dist/design-md/css.d.ts +40 -0
  59. package/lib/dist/design-md/css.d.ts.map +1 -0
  60. package/lib/dist/design-md/css.js +224 -0
  61. package/lib/dist/design-md/css.js.map +1 -0
  62. package/lib/dist/design-md/extract.d.ts +84 -0
  63. package/lib/dist/design-md/extract.d.ts.map +1 -0
  64. package/lib/dist/design-md/extract.js +595 -0
  65. package/lib/dist/design-md/extract.js.map +1 -0
  66. package/lib/dist/design-md/maintain.d.ts +45 -0
  67. package/lib/dist/design-md/maintain.d.ts.map +1 -0
  68. package/lib/dist/design-md/maintain.js +127 -0
  69. package/lib/dist/design-md/maintain.js.map +1 -0
  70. package/lib/dist/design-md/render.d.ts +28 -0
  71. package/lib/dist/design-md/render.d.ts.map +1 -0
  72. package/lib/dist/design-md/render.js +178 -0
  73. package/lib/dist/design-md/render.js.map +1 -0
  74. package/lib/dist/design-md/upkeep.d.ts +48 -0
  75. package/lib/dist/design-md/upkeep.d.ts.map +1 -0
  76. package/lib/dist/design-md/upkeep.js +169 -0
  77. package/lib/dist/design-md/upkeep.js.map +1 -0
  78. package/lib/dist/design-md/verify.d.ts +65 -0
  79. package/lib/dist/design-md/verify.d.ts.map +1 -0
  80. package/lib/dist/design-md/verify.js +311 -0
  81. package/lib/dist/design-md/verify.js.map +1 -0
  82. package/lib/dist/enforce/changed-files.d.ts +7 -0
  83. package/lib/dist/enforce/changed-files.d.ts.map +1 -1
  84. package/lib/dist/enforce/changed-files.js +17 -0
  85. package/lib/dist/enforce/changed-files.js.map +1 -1
  86. package/lib/dist/enforce/enforce.d.ts +35 -1
  87. package/lib/dist/enforce/enforce.d.ts.map +1 -1
  88. package/lib/dist/enforce/enforce.js +66 -2
  89. package/lib/dist/enforce/enforce.js.map +1 -1
  90. package/lib/dist/extraction/index.d.ts +50 -4
  91. package/lib/dist/extraction/index.d.ts.map +1 -1
  92. package/lib/dist/extraction/index.js +105 -13
  93. package/lib/dist/extraction/index.js.map +1 -1
  94. package/lib/dist/extraction/parse-warnings.d.ts +16 -0
  95. package/lib/dist/extraction/parse-warnings.d.ts.map +1 -0
  96. package/lib/dist/extraction/parse-warnings.js +30 -0
  97. package/lib/dist/extraction/parse-warnings.js.map +1 -0
  98. package/lib/dist/extraction/tree-sitter.d.ts +27 -0
  99. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  100. package/lib/dist/extraction/tree-sitter.js +168 -1
  101. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  102. package/lib/dist/health/smoke-check.js +1 -1
  103. package/lib/dist/health/smoke-check.js.map +1 -1
  104. package/lib/dist/hooks/hooks.json +16 -1
  105. package/lib/dist/index.d.ts +63 -3
  106. package/lib/dist/index.d.ts.map +1 -1
  107. package/lib/dist/index.js +235 -9
  108. package/lib/dist/index.js.map +1 -1
  109. package/lib/dist/installer/agent-record.d.ts +80 -0
  110. package/lib/dist/installer/agent-record.d.ts.map +1 -0
  111. package/lib/dist/installer/agent-record.js +351 -0
  112. package/lib/dist/installer/agent-record.js.map +1 -0
  113. package/lib/dist/installer/auto-migrate.d.ts +56 -0
  114. package/lib/dist/installer/auto-migrate.d.ts.map +1 -0
  115. package/lib/dist/installer/auto-migrate.js +328 -0
  116. package/lib/dist/installer/auto-migrate.js.map +1 -0
  117. package/lib/dist/installer/index.d.ts +37 -8
  118. package/lib/dist/installer/index.d.ts.map +1 -1
  119. package/lib/dist/installer/index.js +156 -46
  120. package/lib/dist/installer/index.js.map +1 -1
  121. package/lib/dist/installer/install-defaults.d.ts +4 -3
  122. package/lib/dist/installer/install-defaults.d.ts.map +1 -1
  123. package/lib/dist/installer/install-defaults.js +6 -5
  124. package/lib/dist/installer/install-defaults.js.map +1 -1
  125. package/lib/dist/installer/{config-writer.d.ts → targets/claude-config-writer.d.ts} +1 -1
  126. package/lib/dist/installer/targets/claude-config-writer.d.ts.map +1 -0
  127. package/lib/dist/installer/{config-writer.js → targets/claude-config-writer.js} +7 -6
  128. package/lib/dist/installer/targets/claude-config-writer.js.map +1 -0
  129. package/lib/dist/installer/targets/claude-projects.d.ts +9 -0
  130. package/lib/dist/installer/targets/claude-projects.d.ts.map +1 -0
  131. package/lib/dist/installer/targets/claude-projects.js +151 -0
  132. package/lib/dist/installer/targets/claude-projects.js.map +1 -0
  133. package/lib/dist/installer/targets/claude.d.ts +29 -2
  134. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  135. package/lib/dist/installer/targets/claude.js +44 -20
  136. package/lib/dist/installer/targets/claude.js.map +1 -1
  137. package/lib/dist/installer/targets/feature-parity.d.ts +28 -0
  138. package/lib/dist/installer/targets/feature-parity.d.ts.map +1 -0
  139. package/lib/dist/installer/targets/feature-parity.js +32 -0
  140. package/lib/dist/installer/targets/feature-parity.js.map +1 -0
  141. package/lib/dist/installer/{gemini-commands.d.ts → targets/gemini-commands.d.ts} +7 -3
  142. package/lib/dist/installer/targets/gemini-commands.d.ts.map +1 -0
  143. package/lib/dist/installer/{gemini-commands.js → targets/gemini-commands.js} +37 -9
  144. package/lib/dist/installer/targets/gemini-commands.js.map +1 -0
  145. package/lib/dist/installer/targets/gemini-projects.d.ts +11 -0
  146. package/lib/dist/installer/targets/gemini-projects.d.ts.map +1 -0
  147. package/lib/dist/installer/targets/gemini-projects.js +84 -0
  148. package/lib/dist/installer/targets/gemini-projects.js.map +1 -0
  149. package/lib/dist/installer/targets/gemini.d.ts +94 -8
  150. package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
  151. package/lib/dist/installer/targets/gemini.js +392 -33
  152. package/lib/dist/installer/targets/gemini.js.map +1 -1
  153. package/lib/dist/installer/targets/registry.d.ts +8 -1
  154. package/lib/dist/installer/targets/registry.d.ts.map +1 -1
  155. package/lib/dist/installer/targets/registry.js +14 -5
  156. package/lib/dist/installer/targets/registry.js.map +1 -1
  157. package/lib/dist/installer/targets/shared.d.ts +44 -1
  158. package/lib/dist/installer/targets/shared.d.ts.map +1 -1
  159. package/lib/dist/installer/targets/shared.js +130 -3
  160. package/lib/dist/installer/targets/shared.js.map +1 -1
  161. package/lib/dist/installer/targets/types.d.ts +23 -1
  162. package/lib/dist/installer/targets/types.d.ts.map +1 -1
  163. package/lib/dist/jira/auto-publish.d.ts +22 -2
  164. package/lib/dist/jira/auto-publish.d.ts.map +1 -1
  165. package/lib/dist/jira/auto-publish.js +54 -9
  166. package/lib/dist/jira/auto-publish.js.map +1 -1
  167. package/lib/dist/jira/board-first.d.ts +15 -2
  168. package/lib/dist/jira/board-first.d.ts.map +1 -1
  169. package/lib/dist/jira/board-first.js +16 -0
  170. package/lib/dist/jira/board-first.js.map +1 -1
  171. package/lib/dist/jira/ci-publish.d.ts +72 -0
  172. package/lib/dist/jira/ci-publish.d.ts.map +1 -0
  173. package/lib/dist/jira/ci-publish.js +162 -0
  174. package/lib/dist/jira/ci-publish.js.map +1 -0
  175. package/lib/dist/jira/publish.d.ts +23 -0
  176. package/lib/dist/jira/publish.d.ts.map +1 -1
  177. package/lib/dist/jira/publish.js +0 -0
  178. package/lib/dist/jira/publish.js.map +1 -1
  179. package/lib/dist/jira/regression-pack.d.ts.map +1 -1
  180. package/lib/dist/jira/regression-pack.js +7 -1
  181. package/lib/dist/jira/regression-pack.js.map +1 -1
  182. package/lib/dist/jira/repo-config.d.ts.map +1 -1
  183. package/lib/dist/jira/repo-config.js +8 -0
  184. package/lib/dist/jira/repo-config.js.map +1 -1
  185. package/lib/dist/jira/types.d.ts +8 -0
  186. package/lib/dist/jira/types.d.ts.map +1 -1
  187. package/lib/dist/jira/types.js.map +1 -1
  188. package/lib/dist/knowledge/enforce.d.ts.map +1 -1
  189. package/lib/dist/knowledge/enforce.js +6 -1
  190. package/lib/dist/knowledge/enforce.js.map +1 -1
  191. package/lib/dist/mcp/jira-tools.d.ts +5 -0
  192. package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
  193. package/lib/dist/mcp/jira-tools.js +59 -4
  194. package/lib/dist/mcp/jira-tools.js.map +1 -1
  195. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  196. package/lib/dist/mcp/proxy.js +2 -1
  197. package/lib/dist/mcp/proxy.js.map +1 -1
  198. package/lib/dist/mcp/server-instructions.d.ts +16 -10
  199. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  200. package/lib/dist/mcp/server-instructions.js +69 -20
  201. package/lib/dist/mcp/server-instructions.js.map +1 -1
  202. package/lib/dist/mcp/session.d.ts +11 -0
  203. package/lib/dist/mcp/session.d.ts.map +1 -1
  204. package/lib/dist/mcp/session.js +43 -1
  205. package/lib/dist/mcp/session.js.map +1 -1
  206. package/lib/dist/mcp/tools.d.ts.map +1 -1
  207. package/lib/dist/mcp/tools.js +8 -0
  208. package/lib/dist/mcp/tools.js.map +1 -1
  209. package/lib/dist/reflect/apply.d.ts +2 -2
  210. package/lib/dist/reflect/apply.d.ts.map +1 -1
  211. package/lib/dist/reflect/apply.js +6 -5
  212. package/lib/dist/reflect/apply.js.map +1 -1
  213. package/lib/dist/reflect/miner.d.ts.map +1 -1
  214. package/lib/dist/reflect/miner.js +28 -15
  215. package/lib/dist/reflect/miner.js.map +1 -1
  216. package/lib/dist/reflect/session-outcomes.d.ts.map +1 -1
  217. package/lib/dist/reflect/session-outcomes.js +8 -3
  218. package/lib/dist/reflect/session-outcomes.js.map +1 -1
  219. package/lib/dist/reflect/targets.d.ts.map +1 -1
  220. package/lib/dist/reflect/targets.js +25 -5
  221. package/lib/dist/reflect/targets.js.map +1 -1
  222. package/lib/dist/reflect/types.d.ts +7 -0
  223. package/lib/dist/reflect/types.d.ts.map +1 -1
  224. package/lib/dist/resolution/spec-link-resolver.d.ts +14 -2
  225. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  226. package/lib/dist/resolution/spec-link-resolver.js +13 -3
  227. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  228. package/lib/dist/server/canonical-tools.js +50 -0
  229. package/lib/dist/server/cli.js +2 -2
  230. package/lib/dist/server/host-profiles.js +39 -0
  231. package/lib/dist/server/ingest/index.js +2 -1
  232. package/lib/dist/server/ingest/ingestor.js +195 -76
  233. package/lib/dist/server/ingest/project-paths.js +8 -2
  234. package/lib/dist/server/ingest/recorded-projects.js +43 -0
  235. package/lib/dist/server/ingest/specship-classify.js +7 -2
  236. package/lib/dist/server/ingest/transcripts/claude-jsonl.js +54 -0
  237. package/lib/dist/server/ingest/transcripts/claude.js +64 -0
  238. package/lib/dist/server/ingest/transcripts/gemini.js +469 -0
  239. package/lib/dist/server/ingest/transcripts/index.js +14 -0
  240. package/lib/dist/server/ingest/{parser.js → transcripts/shared.js} +3 -49
  241. package/lib/dist/server/ingest/transcripts/types.js +4 -0
  242. package/lib/dist/server/ingest/watcher.js +31 -30
  243. package/lib/dist/server/routes/claude.js +27 -10
  244. package/lib/dist/server/routes/graph.js +64 -3
  245. package/lib/dist/server/routes/mcp.js +57 -15
  246. package/lib/dist/server/routes/memory.js +54 -35
  247. package/lib/dist/server/routes/projects.js +78 -7
  248. package/lib/dist/server/routes/spec.js +125 -0
  249. package/lib/dist/server/server.js +8 -2
  250. package/lib/dist/skills/design-md/SKILL.md +172 -0
  251. package/lib/dist/skills/design-md/references/authored-sections.md +90 -0
  252. package/lib/dist/skills/design-md/references/sources.md +71 -0
  253. package/lib/dist/skills/spec-author/SKILL.md +1 -1
  254. package/lib/dist/skills/spec-author/references/gap-questions.md +1 -1
  255. package/lib/dist/spec-lint/lint.d.ts +26 -1
  256. package/lib/dist/spec-lint/lint.d.ts.map +1 -1
  257. package/lib/dist/spec-lint/lint.js +96 -7
  258. package/lib/dist/spec-lint/lint.js.map +1 -1
  259. package/lib/dist/spec-quality/postcheck.d.ts +7 -1
  260. package/lib/dist/spec-quality/postcheck.d.ts.map +1 -1
  261. package/lib/dist/spec-quality/postcheck.js +31 -0
  262. package/lib/dist/spec-quality/postcheck.js.map +1 -1
  263. package/lib/dist/sync/worktree.d.ts +48 -0
  264. package/lib/dist/sync/worktree.d.ts.map +1 -1
  265. package/lib/dist/sync/worktree.js +109 -0
  266. package/lib/dist/sync/worktree.js.map +1 -1
  267. package/lib/dist/ui/assets/bricolage-latin-C23fTOVi.woff2 +0 -0
  268. package/lib/dist/ui/assets/bricolage-latin-ext-B82pxkPf.woff2 +0 -0
  269. package/lib/dist/ui/assets/index-54vorjyq.css +1 -0
  270. package/lib/dist/ui/assets/index-BZQonCVb.js +52 -0
  271. package/lib/dist/ui/assets/space-grotesk-latin-DPT1xrvW.woff2 +0 -0
  272. package/lib/dist/ui/assets/space-grotesk-latin-ext-wJGaI8U0.woff2 +0 -0
  273. package/lib/dist/ui/index.html +2 -2
  274. package/lib/dist/update/updater.d.ts +6 -0
  275. package/lib/dist/update/updater.d.ts.map +1 -1
  276. package/lib/dist/update/updater.js +13 -1
  277. package/lib/dist/update/updater.js.map +1 -1
  278. package/lib/dist/workflows/agents/claude.d.ts +9 -0
  279. package/lib/dist/workflows/agents/claude.d.ts.map +1 -1
  280. package/lib/dist/workflows/agents/claude.js +22 -3
  281. package/lib/dist/workflows/agents/claude.js.map +1 -1
  282. package/lib/dist/workflows/agents/gemini-version.d.ts +18 -0
  283. package/lib/dist/workflows/agents/gemini-version.d.ts.map +1 -0
  284. package/lib/dist/workflows/agents/gemini-version.js +81 -0
  285. package/lib/dist/workflows/agents/gemini-version.js.map +1 -0
  286. package/lib/dist/workflows/agents/gemini.d.ts +1 -1
  287. package/lib/dist/workflows/agents/gemini.d.ts.map +1 -1
  288. package/lib/dist/workflows/agents/gemini.js +12 -2
  289. package/lib/dist/workflows/agents/gemini.js.map +1 -1
  290. package/lib/dist/workflows/agents/index.d.ts +2 -0
  291. package/lib/dist/workflows/agents/index.d.ts.map +1 -1
  292. package/lib/dist/workflows/agents/index.js +5 -0
  293. package/lib/dist/workflows/agents/index.js.map +1 -1
  294. package/lib/dist/workflows/agents/types.d.ts +13 -0
  295. package/lib/dist/workflows/agents/types.d.ts.map +1 -1
  296. package/lib/dist/workflows/agents/types.js.map +1 -1
  297. package/lib/dist/workflows/condition-evaluator.d.ts +8 -0
  298. package/lib/dist/workflows/condition-evaluator.d.ts.map +1 -1
  299. package/lib/dist/workflows/condition-evaluator.js +7 -0
  300. package/lib/dist/workflows/condition-evaluator.js.map +1 -1
  301. package/lib/dist/workflows/defaults/claude-design-implement.yaml +44 -15
  302. package/lib/dist/workflows/defaults/spec-author.yaml +9 -9
  303. package/lib/dist/workflows/defaults/spec-fix.yaml +8 -8
  304. package/lib/dist/workflows/defaults/spec-implement.yaml +54 -23
  305. package/lib/dist/workflows/defaults/spec-relink.yaml +5 -5
  306. package/lib/dist/workflows/defaults/spec-verify.yaml +3 -3
  307. package/lib/dist/workflows/executor.d.ts +7 -0
  308. package/lib/dist/workflows/executor.d.ts.map +1 -1
  309. package/lib/dist/workflows/executor.js +22 -5
  310. package/lib/dist/workflows/executor.js.map +1 -1
  311. package/lib/node_modules/.package-lock.json +1 -1
  312. package/lib/package.json +3 -2
  313. package/package.json +1 -1
  314. package/lib/dist/installer/config-writer.d.ts.map +0 -1
  315. package/lib/dist/installer/config-writer.js.map +0 -1
  316. package/lib/dist/installer/gemini-commands.d.ts.map +0 -1
  317. package/lib/dist/installer/gemini-commands.js.map +0 -1
  318. package/lib/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  319. package/lib/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  320. package/lib/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  321. package/lib/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  322. package/lib/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  323. package/lib/dist/ui/assets/index-BzuUWUAE.js +0 -50
  324. package/lib/dist/ui/assets/index-ezwX2EfG.css +0 -1
@@ -19,6 +19,22 @@
19
19
  */
20
20
  import { writeSseHead } from './sse.js';
21
21
  import { decodeProjectSlug, getLastIngestStats } from '../ingest/index.js';
22
+ import { CLAUDE_PROFILE, HOST_PROFILES } from '../host-profiles.js';
23
+ import { CANONICAL_TOOLS } from '../canonical-tools.js';
24
+ // Tool names in claude_tool_calls are the SHARED names every transcript
25
+ // adapter normalises to (REQ-AGENTSWITCH-004), so these queries cover every
26
+ // agent's sessions. Values come from our own canonical-tools.json, never from
27
+ // input, so quoting them into the SQL is safe.
28
+ const sqlString = (v) => `'${v.replace(/'/g, "''")}'`;
29
+ const FILE_TOUCH_TOOLS = [CANONICAL_TOOLS.read, CANONICAL_TOOLS.edit, CANONICAL_TOOLS.write, CANONICAL_TOOLS.notebookEdit];
30
+ const FILE_TOUCH_TOOLS_SQL = FILE_TOUCH_TOOLS.map(sqlString).join(',');
31
+ const FILE_TOUCH_TOOLS_SQL_WITH_MULTI = [...FILE_TOUCH_TOOLS, CANONICAL_TOOLS.multiEdit].map(sqlString).join(',');
32
+ const READ_TOOL_SQL = sqlString(CANONICAL_TOOLS.read);
33
+ /** `<configDir>/settings.json` of the agent a session came from (Claude Code when unknown). */
34
+ function settingsFileFor(host) {
35
+ const profile = HOST_PROFILES.find((p) => p.id === host) ?? CLAUDE_PROFILE;
36
+ return `${profile.configDir}/settings.json`;
37
+ }
22
38
  /**
23
39
  * Normalize a `?project=` filter value to the form stored in
24
40
  * claude_sessions.project_path. The Sessions page (and any other UI
@@ -361,7 +377,7 @@ export async function registerClaudeRoutes(app) {
361
377
  ORDER BY inner_tc.ts DESC LIMIT 1) as lastOp
362
378
  FROM claude_tool_calls AS tc
363
379
  WHERE tc.session_id = ?
364
- AND tc.tool_name IN ('Read','Edit','Write','NotebookEdit','MultiEdit')
380
+ AND tc.tool_name IN (${FILE_TOUCH_TOOLS_SQL_WITH_MULTI})
365
381
  AND tc.input_summary != ''
366
382
  GROUP BY tc.input_summary
367
383
  ORDER BY ops DESC
@@ -437,7 +453,7 @@ export async function registerClaudeRoutes(app) {
437
453
  const files = db.prepare(`
438
454
  SELECT input_summary as path, COUNT(*) as calls, SUM(result_length) as resultBytes
439
455
  FROM claude_tool_calls
440
- WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''${inProject()}
456
+ WHERE ts >= ? AND tool_name IN (${FILE_TOUCH_TOOLS_SQL}) AND input_summary != ''${inProject()}
441
457
  GROUP BY input_summary
442
458
  ORDER BY calls DESC
443
459
  LIMIT 100
@@ -450,7 +466,7 @@ export async function registerClaudeRoutes(app) {
450
466
  const trendRows = db.prepare(`
451
467
  SELECT input_summary as path, CAST((ts - ?) / ? AS INTEGER) as bucket, COUNT(*) as calls
452
468
  FROM claude_tool_calls
453
- WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''${inProject()}
469
+ WHERE ts >= ? AND tool_name IN (${FILE_TOUCH_TOOLS_SQL}) AND input_summary != ''${inProject()}
454
470
  GROUP BY input_summary, bucket
455
471
  `).all(...withProject(trend7Start, dayMs, trend7Start));
456
472
  const trendByPath = new Map();
@@ -518,7 +534,7 @@ export async function registerClaudeRoutes(app) {
518
534
  MAX(t.ts) as lastTs, MIN(t.ts) as firstTs
519
535
  FROM claude_tool_calls t
520
536
  LEFT JOIN claude_sessions s ON s.id = t.session_id
521
- WHERE t.input_summary = ? AND t.ts >= ? AND t.tool_name IN ('Read','Edit','Write','NotebookEdit')
537
+ WHERE t.input_summary = ? AND t.ts >= ? AND t.tool_name IN (${FILE_TOUCH_TOOLS_SQL})
522
538
  GROUP BY t.session_id
523
539
  ORDER BY calls DESC
524
540
  LIMIT 50
@@ -526,7 +542,7 @@ export async function registerClaudeRoutes(app) {
526
542
  const byTool = db.prepare(`
527
543
  SELECT tool_name as name, COUNT(*) as calls, COALESCE(SUM(result_length), 0) as bytes
528
544
  FROM claude_tool_calls
529
- WHERE input_summary = ? AND ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit')
545
+ WHERE input_summary = ? AND ts >= ? AND tool_name IN (${FILE_TOUCH_TOOLS_SQL})
530
546
  GROUP BY tool_name
531
547
  ORDER BY calls DESC
532
548
  `).all(path, since);
@@ -930,7 +946,7 @@ export async function registerClaudeRoutes(app) {
930
946
  const wastefulReads = db.prepare(`
931
947
  SELECT session_id, input_summary as file, COUNT(*) as n
932
948
  FROM claude_tool_calls
933
- WHERE tool_name = 'Read' AND input_summary != '' AND ${inProject}
949
+ WHERE tool_name = ${READ_TOOL_SQL} AND input_summary != '' AND ${inProject}
934
950
  GROUP BY session_id, input_summary
935
951
  HAVING n >= 10
936
952
  ORDER BY n DESC
@@ -970,13 +986,13 @@ export async function registerClaudeRoutes(app) {
970
986
  title: `${r.tool_name} returned ${Math.round(r.result_length / 1000)}k chars (~${Math.round(estTokens / 1000)}k tokens est.) — try a structural query`,
971
987
  why: 'Tools that dump raw content into context are the dominant cost driver. A structural query returns just what the agent needs.',
972
988
  evidence: { session: r.session_id, detail: `${r.tool_name}(${r.input_summary.slice(0, 100)}) → ${r.result_length} chars (~${estTokens} tokens est.)` },
973
- fix: r.tool_name === 'Bash' ? 'specship_search "<symbol>" instead of Bash(grep)' : 'specship_explore "<symbol or question>"',
989
+ fix: r.tool_name === CANONICAL_TOOLS.bash ? 'specship_search "<symbol>" instead of Bash(grep)' : 'specship_explore "<symbol or question>"',
974
990
  saving: `~$${((estTokens / 1_000_000) * 15).toFixed(2)} est. on this call`,
975
991
  });
976
992
  }
977
993
  // Rule 3: "cache miss rate" — sessions with > 10 prompts and cache_read_rate < 0.3.
978
994
  const lowCache = db.prepare(`
979
- SELECT s.id, s.total_cache_read_tokens as cr, s.total_input_tokens as ti, s.total_cache_creation_tokens as cw, s.prompt_count, s.last_model
995
+ SELECT s.id, s.total_cache_read_tokens as cr, s.total_input_tokens as ti, s.total_cache_creation_tokens as cw, s.prompt_count, s.last_model, s.host
980
996
  FROM claude_sessions s
981
997
  WHERE s.prompt_count >= 10
982
998
  AND (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens) > 0
@@ -996,7 +1012,8 @@ export async function registerClaudeRoutes(app) {
996
1012
  title: `Cache read rate is ${Math.round(rate * 100)}% on a ${r.prompt_count}-prompt session`,
997
1013
  why: 'When the prompt prefix changes every turn, the 1h cache gets invalidated. Pinning a stable system-prompt prefix lets the cache absorb most of your input.',
998
1014
  evidence: { session: r.id, detail: `cache_read=${(r.cr / 1_000_000).toFixed(2)}M / total=${(total / 1_000_000).toFixed(2)}M` },
999
- fix: 'Pin a stable system-prompt prefix in .claude/settings.json',
1015
+ // The session's own agent's settings file (REQ-AGENTSWITCH-004).
1016
+ fix: `Pin a stable system-prompt prefix in ${settingsFileFor(r.host)}`,
1000
1017
  // Not measurable per session without a model price table — state it
1001
1018
  // qualitatively rather than render a placeholder (REQ-AUDFIX-006.A1).
1002
1019
  saving: 'cached input bills at a fraction of fresh input',
@@ -1008,7 +1025,7 @@ export async function registerClaudeRoutes(app) {
1008
1025
  // mixed-model workflow (fresh context per step + external verification).
1009
1026
  const flounder = db.prepare(`
1010
1027
  SELECT s.id, s.last_model,
1011
- (SELECT COUNT(*) FROM claude_tool_calls t WHERE t.session_id = s.id AND t.tool_name = 'Read') AS reads
1028
+ (SELECT COUNT(*) FROM claude_tool_calls t WHERE t.session_id = s.id AND t.tool_name = ${READ_TOOL_SQL}) AS reads
1012
1029
  FROM claude_sessions s
1013
1030
  WHERE s.project_path IN (?, ?)
1014
1031
  AND (LOWER(COALESCE(s.last_model,'')) LIKE '%haiku%' OR LOWER(COALESCE(s.last_model,'')) LIKE '%sonnet%')
@@ -10,6 +10,30 @@
10
10
  * The MCP layer's `specship_*` tools are markdown-formatted for agents;
11
11
  * here we return raw JSON so the UI can render visually.
12
12
  */
13
+ /**
14
+ * Longest source the split view returns for one symbol. `getCode` already
15
+ * slices the symbol's own startLine..endLine; this caps a huge class body so a
16
+ * single match can't turn into a whole-file dump. File nodes get no code at all.
17
+ */
18
+ export const MAX_CODE_LINES = 400;
19
+ /** One symbol's verbatim source, capped to its own body (null when unavailable). */
20
+ async function symbolCode(cg, node) {
21
+ if (node.kind === 'file')
22
+ return { code: null, codeTruncated: false };
23
+ let code = null;
24
+ try {
25
+ code = await cg.getCode(node.id);
26
+ }
27
+ catch {
28
+ code = null;
29
+ }
30
+ if (code == null)
31
+ return { code: null, codeTruncated: false };
32
+ const lines = code.split('\n');
33
+ if (lines.length <= MAX_CODE_LINES)
34
+ return { code, codeTruncated: false };
35
+ return { code: lines.slice(0, MAX_CODE_LINES).join('\n'), codeTruncated: true };
36
+ }
13
37
  /** Returns the active SpecShip or sends a 409. */
14
38
  async function resolveCg(app, req, reply) {
15
39
  const cg = await app.activeCg(req);
@@ -89,14 +113,51 @@ export async function registerGraphRoutes(app) {
89
113
  }
90
114
  if (matches.length === 0)
91
115
  return reply.code(404).send({ error: 'not found' });
92
- const enriched = matches.map((n) => {
116
+ const withCode = req.query.includeCode === '1' || req.query.includeCode === 'true';
117
+ const enriched = await Promise.all(matches.map(async (n) => {
93
118
  const callers = cg.getCallers(n.id).map((e) => e.node);
94
119
  const callees = cg.getCallees(n.id).map((e) => e.node);
95
120
  const links = cg.getSpecQueries().getLinksByNode(n.id);
96
- return { ...n, callers, callees, linkedSpecs: links };
97
- });
121
+ const base = { ...n, callers, callees, linkedSpecs: links };
122
+ return withCode ? { ...base, ...(await symbolCode(cg, n)) } : base;
123
+ }));
98
124
  return { matches: enriched };
99
125
  });
126
+ /**
127
+ * GET /api/graph/split-search?q= — the Graph split view's one search box
128
+ * (REQ-DASHSIMPLE-005): spec matches (an exact requirement id first, then
129
+ * the spec full-text index) and code-symbol matches, as two groups.
130
+ */
131
+ app.get('/api/graph/split-search', async (req, reply) => {
132
+ const cg = await resolveCg(app, req, reply);
133
+ if (!cg)
134
+ return;
135
+ const q = (req.query.q ?? '').trim();
136
+ const limit = Math.min(Math.max(parseInt(req.query.limit ?? '8', 10) || 8, 1), 50);
137
+ if (!q)
138
+ return { specs: [], symbols: [] };
139
+ const sq = cg.getSpecQueries();
140
+ const specs = [];
141
+ const seen = new Set();
142
+ const exact = sq.getSpecById(q) ?? sq.getSpecById(q.toUpperCase());
143
+ if (exact) {
144
+ seen.add(exact.id);
145
+ specs.push({ id: exact.id, title: exact.title, kind: exact.kind, snippet: '' });
146
+ }
147
+ for (const hit of sq.searchSpecs(q, limit)) {
148
+ if (seen.has(hit.spec.id))
149
+ continue;
150
+ seen.add(hit.spec.id);
151
+ specs.push({ id: hit.spec.id, title: hit.spec.title, kind: hit.spec.kind, snippet: hit.snippet });
152
+ }
153
+ // Spec nodes live in the code graph too; the specs group already covers them.
154
+ const symbols = cg.searchNodes(q, { limit: limit * 2 })
155
+ .map((r) => r.node)
156
+ .filter((n) => n.kind !== 'spec')
157
+ .slice(0, limit)
158
+ .map((n) => ({ id: n.id, name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine }));
159
+ return { specs: specs.slice(0, limit), symbols };
160
+ });
100
161
  app.post('/api/graph/explore', async (req, reply) => {
101
162
  const cg = await resolveCg(app, req, reply);
102
163
  if (!cg)
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * MCP servers routes — inventory, live status, and control (REQ-DESKTOP-026).
3
3
  *
4
- * Three real sources, merged (this fork is Claude Code only, so Claude
5
- * Code's config surfaces ARE the inventory — no other clients are read):
4
+ * Three real sources, merged (each supported agent's config surfaces are
5
+ * the inventory; per-agent file names come from `host-profiles.ts`,
6
+ * REQ-AGENTSWITCH-001 — Gemini CLI's user and project `settings.json`
7
+ * entries are listed read-only under `gemini:<name>` keys):
6
8
  * 1. Claude Code's own config — `~/.claude.json` (global `mcpServers` +
7
9
  * `projects[<root>].mcpServers`) and `<projectRoot>/.mcp.json`, the
8
10
  * same files `specship install` writes. Disabled servers come from
@@ -30,12 +32,15 @@ import { spawn } from 'node:child_process';
30
32
  import * as fs from 'node:fs';
31
33
  import * as os from 'node:os';
32
34
  import * as path from 'node:path';
35
+ import { CLAUDE_PROFILE, GEMINI_PROFILE } from '../host-profiles.js';
33
36
  function getDb(cg) {
34
37
  const anyCg = cg;
35
38
  if (anyCg.db?.getDb)
36
39
  return anyCg.db.getDb();
37
40
  return anyCg.queries?.db;
38
41
  }
42
+ /** Inventory key prefix for a Gemini CLI entry, so it never collides with a Claude Code one of the same name. */
43
+ export const GEMINI_KEY_PREFIX = GEMINI_PROFILE.id + ':';
39
44
  /** Config-write failures the routes translate to HTTP statuses. */
40
45
  export class McpConfigError extends Error {
41
46
  statusCode;
@@ -61,7 +66,7 @@ function readJson(p) {
61
66
  export function configuredServers(paths) {
62
67
  const out = new Map();
63
68
  const claudeJson = readJson(paths.claudeJsonPath);
64
- const add = (name, cfg) => out.set(name, { name, ...cfg });
69
+ const add = (name, cfg) => out.set(name, { name, host: CLAUDE_PROFILE.id, ...cfg });
65
70
  for (const [name, entry] of Object.entries((claudeJson?.['mcpServers'] ?? {}))) {
66
71
  add(name, { scope: 'user', entry, configFile: paths.claudeJsonPath, disabled: false, container: 'user' });
67
72
  }
@@ -77,12 +82,26 @@ export function configuredServers(paths) {
77
82
  add(name, { scope: 'project', entry, configFile: paths.claudeJsonPath, disabled: true, container: 'project-local' });
78
83
  }
79
84
  const disabledMcpjson = new Set((proj?.['disabledMcpjsonServers'] ?? []));
80
- const mcpJsonPath = path.join(paths.projectRoot, '.mcp.json');
85
+ const mcpJsonPath = path.join(paths.projectRoot, CLAUDE_PROFILE.projectMcpFile);
81
86
  const mcpJson = readJson(mcpJsonPath);
82
87
  for (const [name, entry] of Object.entries((mcpJson?.['mcpServers'] ?? {}))) {
83
88
  add(name, { scope: 'project', entry, configFile: mcpJsonPath, disabled: disabledMcpjson.has(name), container: 'mcpjson' });
84
89
  }
85
90
  }
91
+ if (paths.geminiSettingsPath) {
92
+ // Gemini CLI: user settings, then the project's settings (project wins).
93
+ const files = [[paths.geminiSettingsPath, 'user']];
94
+ if (paths.projectRoot)
95
+ files.push([path.join(paths.projectRoot, GEMINI_PROFILE.projectMcpFile), 'project']);
96
+ for (const [file, scope] of files) {
97
+ const settings = readJson(file);
98
+ for (const [name, entry] of Object.entries((settings?.['mcpServers'] ?? {}))) {
99
+ out.set(GEMINI_KEY_PREFIX + name, {
100
+ name, scope, entry, configFile: file, disabled: false, container: 'gemini', host: GEMINI_PROFILE.id,
101
+ });
102
+ }
103
+ }
104
+ }
86
105
  return out;
87
106
  }
88
107
  /**
@@ -281,6 +300,9 @@ export function setServerEnabled(paths, name, enabled) {
281
300
  throw new McpConfigError(`unknown MCP server "${name}"`, 404);
282
301
  if (cfg.disabled === !enabled)
283
302
  return cfg;
303
+ if (cfg.container === 'gemini') {
304
+ throw new McpConfigError(`${GEMINI_PROFILE.displayName} servers are edited in ${cfg.configFile}`, 400);
305
+ }
284
306
  if (cfg.container === 'mcpjson') {
285
307
  editJsonFile(paths.claudeJsonPath, (data) => {
286
308
  const projects = (data['projects'] = data['projects'] ?? {});
@@ -335,16 +357,16 @@ export function addServer(paths, input) {
335
357
  if (input.scope === 'project') {
336
358
  if (!paths.projectRoot)
337
359
  throw new McpConfigError('no project open — cannot write .mcp.json', 400);
338
- const mcpJsonPath = path.join(paths.projectRoot, '.mcp.json');
360
+ const mcpJsonPath = path.join(paths.projectRoot, CLAUDE_PROFILE.projectMcpFile);
339
361
  editJsonFile(mcpJsonPath, (data) => {
340
362
  data['mcpServers'] = { ...(data['mcpServers'] ?? {}), [name]: entry };
341
363
  });
342
- return { name, scope: 'project', entry, configFile: mcpJsonPath, disabled: false, container: 'mcpjson' };
364
+ return { name, scope: 'project', entry, configFile: mcpJsonPath, disabled: false, container: 'mcpjson', host: CLAUDE_PROFILE.id };
343
365
  }
344
366
  editJsonFile(paths.claudeJsonPath, (data) => {
345
367
  data['mcpServers'] = { ...(data['mcpServers'] ?? {}), [name]: entry };
346
368
  });
347
- return { name, scope: 'user', entry, configFile: paths.claudeJsonPath, disabled: false, container: 'user' };
369
+ return { name, scope: 'user', entry, configFile: paths.claudeJsonPath, disabled: false, container: 'user', host: CLAUDE_PROFILE.id };
348
370
  }
349
371
  /** Aggregate claude_tool_calls into per-server usage + the latest example call. */
350
372
  function collectUsage(cg) {
@@ -354,16 +376,16 @@ function collectUsage(cg) {
354
376
  try {
355
377
  const db = getDb(cg);
356
378
  usage = db.prepare(`
357
- SELECT tool_name, COUNT(*) as calls,
379
+ SELECT host, tool_name, COUNT(*) as calls,
358
380
  COALESCE(SUM(result_length), 0) as bytes,
359
381
  MAX(ts) as last
360
382
  FROM claude_tool_calls
361
383
  WHERE tool_name LIKE 'mcp__%'
362
- GROUP BY tool_name
384
+ GROUP BY host, tool_name
363
385
  `).all();
364
386
  // Latest verbatim inputs (schema v7) — first row per server wins below.
365
387
  examples = db.prepare(`
366
- SELECT tool_name, input_json
388
+ SELECT host, tool_name, input_json
367
389
  FROM claude_tool_calls
368
390
  WHERE tool_name LIKE 'mcp__%' AND input_json IS NOT NULL
369
391
  ORDER BY ts DESC
@@ -372,11 +394,17 @@ function collectUsage(cg) {
372
394
  }
373
395
  catch { /* analytics tables absent — config-only response */ }
374
396
  const parse = (t) => t.match(/^mcp__(.+?)__(.+)$/);
397
+ // Transcript adapters store every agent's MCP calls under the shared
398
+ // `mcp__<server>__<tool>` name (REQ-AGENTSWITCH-004), so the bare server
399
+ // name is the match key; a Gemini CLI call is credited to the Gemini entry
400
+ // (`gemini:<server>`) that inventory lists for it.
401
+ const keyFor = mcpUsageKey;
375
402
  for (const u of usage) {
376
403
  const m = parse(u.tool_name);
377
404
  if (!m)
378
405
  continue;
379
- const [, server, tool] = m;
406
+ const [, bare, tool] = m;
407
+ const server = keyFor(u.host, bare);
380
408
  if (!byServer.has(server))
381
409
  byServer.set(server, { calls: 0, bytes: 0, last: 0, tools: new Map(), example: null });
382
410
  const s = byServer.get(server);
@@ -395,7 +423,8 @@ function collectUsage(cg) {
395
423
  const m = parse(row.tool_name);
396
424
  if (!m)
397
425
  continue;
398
- const [, server, tool] = m;
426
+ const [, bare, tool] = m;
427
+ const server = keyFor(row.host, bare);
399
428
  const s = byServer.get(server);
400
429
  if (!s || s.example)
401
430
  continue;
@@ -417,8 +446,9 @@ function collectUsage(cg) {
417
446
  }
418
447
  export async function registerMcpRoutes(app) {
419
448
  const paths = () => ({
420
- claudeJsonPath: path.join(os.homedir(), '.claude.json'),
449
+ claudeJsonPath: path.join(os.homedir(), CLAUDE_PROFILE.userConfigFile),
421
450
  projectRoot: app.primaryCg?.getProjectRoot?.() ?? null,
451
+ geminiSettingsPath: path.join(os.homedir(), GEMINI_PROFILE.userConfigFile),
422
452
  });
423
453
  function requirePrimary(reply) {
424
454
  if (!app.primaryCg) {
@@ -447,7 +477,8 @@ export async function registerMcpRoutes(app) {
447
477
  const command = entry.url ?? [entry.command, ...(entry.args ?? [])].filter(Boolean).join(' ');
448
478
  const state = deriveState(cfg, use?.last ? { lastMs: use.last } : null, probes.get(name) ?? null);
449
479
  return {
450
- id: name, name,
480
+ id: name, name: cfg?.name ?? name,
481
+ host: cfg?.host ?? null,
451
482
  scope: cfg?.scope ?? 'unknown',
452
483
  state, transport, command,
453
484
  configFile: cfg?.configFile ?? null,
@@ -498,10 +529,21 @@ export async function registerMcpRoutes(app) {
498
529
  }
499
530
  });
500
531
  }
501
- /** True when transcripts contain any mcp__<name>__* call. */
532
+ /**
533
+ * The inventory key a recorded MCP call is credited to: a Gemini CLI call to
534
+ * server `x` belongs to the `gemini:x` entry, any other agent's to `x`
535
+ * (REQ-AGENTSWITCH-004 — calls carry the shared name plus their host).
536
+ */
537
+ export function mcpUsageKey(host, server) {
538
+ return host === GEMINI_PROFILE.id ? GEMINI_KEY_PREFIX + server : server;
539
+ }
540
+ /** True when transcripts contain any mcp__<name>__* call (a `gemini:` key matches Gemini CLI calls by bare server name). */
502
541
  function hasUsage(cg, name) {
503
542
  try {
504
543
  const db = getDb(cg);
544
+ if (name.startsWith(GEMINI_KEY_PREFIX)) {
545
+ return db.prepare(`SELECT 1 FROM claude_tool_calls WHERE host = ? AND tool_name LIKE 'mcp__' || ? || '__%' LIMIT 1`).all(GEMINI_PROFILE.id, name.slice(GEMINI_KEY_PREFIX.length)).length > 0;
546
+ }
505
547
  const rows = db.prepare(`SELECT 1 FROM claude_tool_calls WHERE tool_name LIKE 'mcp__' || ? || '__%' LIMIT 1`).all(name);
506
548
  return rows.length > 0;
507
549
  }
@@ -1,7 +1,9 @@
1
1
  /**
2
- * GET /api/memory — CLAUDE.md hierarchy + ~/.claude/memory notes.
2
+ * GET /api/memory — every installed agent's memory-file hierarchy + notes.
3
3
  *
4
- * Walks the standard Claude Code memory locations and returns a flattened
4
+ * Walks each agent's standard memory locations (from `host-profiles.ts`,
5
+ * REQ-AGENTSWITCH-001 — Claude Code's CLAUDE.md tree, Gemini CLI's GEMINI.md
6
+ * tree; each file is tagged with its `host`) and returns a flattened
5
7
  * list of MemoryFile records the desktop UI can render directly. Resolves
6
8
  * `@path` imports from each instruction file.
7
9
  *
@@ -17,11 +19,7 @@ import { promises as fs } from 'node:fs';
17
19
  import path from 'node:path';
18
20
  import os from 'node:os';
19
21
  import { decodeProjectSlug } from '../ingest/ingestor.js';
20
- const ENTERPRISE_PATHS = [
21
- '/Library/Application Support/ClaudeCode/CLAUDE.md',
22
- '/etc/ClaudeCode/CLAUDE.md',
23
- process.env['ProgramData'] ? path.join(process.env['ProgramData'], 'ClaudeCode', 'CLAUDE.md') : '',
24
- ].filter(Boolean);
22
+ import { HOST_PROFILES } from '../host-profiles.js';
25
23
  function estimateTokens(body) {
26
24
  // Rough heuristic: ~4 chars per token. Cheap; the UI tolerates approximation.
27
25
  return Math.max(1, Math.round(body.length / 4));
@@ -86,48 +84,56 @@ function makeFile(opts) {
86
84
  body: opts.body,
87
85
  readOnly: opts.readOnly,
88
86
  imports: opts.imports?.length ? opts.imports : undefined,
87
+ host: opts.host,
89
88
  };
90
89
  }
91
- async function collectEnterprise() {
90
+ /** Id suffix for a non-default host, so both agents' files stay distinct. */
91
+ function idSuffix(profile) {
92
+ return profile === HOST_PROFILES[0] ? '' : '-' + profile.id;
93
+ }
94
+ async function collectEnterprise(profile) {
92
95
  const out = [];
93
- for (const p of ENTERPRISE_PATHS) {
96
+ for (const p of profile.enterpriseMemoryPaths()) {
94
97
  const file = await tryRead(p);
95
98
  if (file) {
96
99
  out.push(makeFile({
97
- id: 'm-ent',
100
+ id: 'm-ent' + idSuffix(profile),
98
101
  level: 'enterprise',
99
102
  type: 'instruction',
100
- name: 'CLAUDE.md',
103
+ name: profile.memoryFile,
101
104
  scope: 'managed',
102
105
  absolutePath: p,
103
106
  body: file.body,
104
107
  modifiedMs: file.modified,
105
108
  readOnly: true,
109
+ host: profile.id,
106
110
  }));
107
111
  break;
108
112
  }
109
113
  }
110
114
  return out;
111
115
  }
112
- async function collectUser() {
113
- const p = path.join(os.homedir(), '.claude', 'CLAUDE.md');
116
+ async function collectUser(profile) {
117
+ const p = path.join(os.homedir(), profile.configDir, profile.memoryFile);
114
118
  const file = await tryRead(p);
115
119
  if (!file)
116
120
  return [];
117
121
  return [makeFile({
118
- id: 'm-user',
122
+ id: 'm-user' + idSuffix(profile),
119
123
  level: 'user',
120
124
  type: 'instruction',
121
- name: 'CLAUDE.md',
122
- scope: '~/.claude',
125
+ name: profile.memoryFile,
126
+ scope: '~/' + profile.configDir,
123
127
  absolutePath: p,
124
128
  body: file.body,
125
129
  modifiedMs: file.modified,
130
+ host: profile.id,
126
131
  })];
127
132
  }
128
- async function collectProject(projectRoot) {
133
+ async function collectProject(projectRoot, profile) {
129
134
  const out = [];
130
- const proj = path.join(projectRoot, 'CLAUDE.md');
135
+ const suffix = idSuffix(profile);
136
+ const proj = path.join(projectRoot, profile.memoryFile);
131
137
  const projFile = await tryRead(proj);
132
138
  const importMap = new Map();
133
139
  if (projFile) {
@@ -137,7 +143,7 @@ async function collectProject(projectRoot) {
137
143
  const abs = path.isAbsolute(imp) ? imp : path.join(projectRoot, imp);
138
144
  const sub = await tryRead(abs);
139
145
  if (sub) {
140
- const id = 'm-imp-' + Buffer.from(imp).toString('base64').replace(/=/g, '').slice(0, 10);
146
+ const id = 'm-imp-' + Buffer.from(imp).toString('base64').replace(/=/g, '').slice(0, 10) + suffix;
141
147
  importMap.set(imp, makeFile({
142
148
  id,
143
149
  level: 'import',
@@ -147,25 +153,27 @@ async function collectProject(projectRoot) {
147
153
  absolutePath: abs,
148
154
  body: sub.body,
149
155
  modifiedMs: sub.modified,
156
+ host: profile.id,
150
157
  }));
151
158
  resolvedIds.push(id);
152
159
  }
153
160
  }
154
161
  out.push(makeFile({
155
- id: 'm-proj',
162
+ id: 'm-proj' + suffix,
156
163
  level: 'project',
157
164
  type: 'instruction',
158
- name: 'CLAUDE.md',
165
+ name: profile.memoryFile,
159
166
  scope: 'project root',
160
167
  absolutePath: proj,
161
168
  body: projFile.body,
162
169
  modifiedMs: projFile.modified,
163
170
  imports: resolvedIds,
171
+ host: profile.id,
164
172
  }));
165
173
  for (const im of importMap.values())
166
174
  out.push(im);
167
175
  }
168
- // Scan one level of subdirectories for nested CLAUDE.md files. Keeps the
176
+ // Scan one level of subdirectories for nested memory files. Keeps the
169
177
  // walk cheap — going deeper would need ignoring node_modules etc.
170
178
  try {
171
179
  const entries = await fs.readdir(projectRoot, { withFileTypes: true });
@@ -174,18 +182,19 @@ async function collectProject(projectRoot) {
174
182
  continue;
175
183
  if (ent.name.startsWith('.') || ent.name === 'node_modules' || ent.name === 'dist')
176
184
  continue;
177
- const subPath = path.join(projectRoot, ent.name, 'CLAUDE.md');
185
+ const subPath = path.join(projectRoot, ent.name, profile.memoryFile);
178
186
  const sub = await tryRead(subPath);
179
187
  if (sub) {
180
188
  out.push(makeFile({
181
- id: 'm-sub-' + ent.name,
189
+ id: 'm-sub-' + ent.name + suffix,
182
190
  level: 'subdir',
183
191
  type: 'instruction',
184
- name: 'CLAUDE.md',
192
+ name: profile.memoryFile,
185
193
  scope: ent.name,
186
194
  absolutePath: subPath,
187
195
  body: sub.body,
188
196
  modifiedMs: sub.modified,
197
+ host: profile.id,
189
198
  }));
190
199
  }
191
200
  }
@@ -195,9 +204,11 @@ async function collectProject(projectRoot) {
195
204
  }
196
205
  return out;
197
206
  }
198
- async function collectNotes() {
199
- const dir = path.join(os.homedir(), '.claude', 'memory');
207
+ async function collectNotes(profile) {
200
208
  const out = [];
209
+ if (!profile.notesDir)
210
+ return out;
211
+ const dir = path.join(os.homedir(), profile.notesDir);
201
212
  let entries;
202
213
  try {
203
214
  entries = await fs.readdir(dir, { withFileTypes: true });
@@ -217,7 +228,7 @@ async function collectNotes() {
217
228
  .filter((t) => typeof t === 'string')
218
229
  .slice(0, 6);
219
230
  out.push(makeFile({
220
- id: 'n-' + ent.name.replace(/\.md$/, ''),
231
+ id: 'n-' + ent.name.replace(/\.md$/, '') + idSuffix(profile),
221
232
  level: 'note',
222
233
  type: 'note',
223
234
  name: ent.name,
@@ -225,6 +236,7 @@ async function collectNotes() {
225
236
  absolutePath: abs,
226
237
  body: file.body,
227
238
  modifiedMs: file.modified,
239
+ host: profile.id,
228
240
  }));
229
241
  if (tags.length) {
230
242
  const last = out[out.length - 1];
@@ -234,6 +246,19 @@ async function collectNotes() {
234
246
  }
235
247
  return out;
236
248
  }
249
+ /**
250
+ * Every agent's memory files, grouped tier by tier: enterprise, user, project,
251
+ * notes — each tier lists Claude Code's files, then Gemini CLI's.
252
+ */
253
+ export async function collectMemoryFiles(projectRoot) {
254
+ const tiers = await Promise.all([
255
+ Promise.all(HOST_PROFILES.map((p) => collectEnterprise(p))),
256
+ Promise.all(HOST_PROFILES.map((p) => collectUser(p))),
257
+ projectRoot ? Promise.all(HOST_PROFILES.map((p) => collectProject(projectRoot, p))) : Promise.resolve([]),
258
+ Promise.all(HOST_PROFILES.map((p) => collectNotes(p))),
259
+ ]);
260
+ return tiers.flat(2);
261
+ }
237
262
  export async function registerMemoryRoutes(app) {
238
263
  app.get('/api/memory', async (req) => {
239
264
  // Memory has three global tiers (enterprise / user / notes) and one
@@ -251,13 +276,7 @@ export async function registerMemoryRoutes(app) {
251
276
  projectRoot = req.query.projectPath;
252
277
  else if (app.primaryCg?.getProjectRoot)
253
278
  projectRoot = app.primaryCg.getProjectRoot();
254
- const [enterprise, user, project, notes] = await Promise.all([
255
- collectEnterprise(),
256
- collectUser(),
257
- projectRoot ? collectProject(projectRoot) : Promise.resolve([]),
258
- collectNotes(),
259
- ]);
260
- const files = [...enterprise, ...user, ...project, ...notes];
279
+ const files = await collectMemoryFiles(projectRoot);
261
280
  const resp = {
262
281
  totalTokens: files.reduce((a, b) => a + b.tokens, 0),
263
282
  instructionCount: files.filter((f) => f.type === 'instruction').length,