@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
@@ -1,13 +1,18 @@
1
1
  /**
2
- * JSONL → SQLite ingestor for Claude Code transcripts.
2
+ * Transcript → SQLite ingestor, one adapter per agent (REQ-AGENTSWITCH-004).
3
3
  *
4
- * Each Claude Code session lives in `~/.claude/projects/<slug>/<sessionId>.jsonl`,
5
- * where `<slug>` is the slash-escaped absolute path of the user's project.
6
- * Files are append-only (Claude Code writes one JSON line per assistant/user
7
- * turn), so we resume from the last byte offset on every pass — no re-parse
8
- * of the whole file.
4
+ * `ingestAll` walks every adapter in `./transcripts/` and ingests each of its
5
+ * files. Adapters normalise tool names to the shared set, so every row's
6
+ * `tool_name` is comparable across agents; `raw_tool_name` keeps the agent's
7
+ * own name and `host` names the agent. A document the adapter cannot parse
8
+ * is skipped and counted (`filesUnreadable`), never guessed at.
9
9
  *
10
- * Pipeline per file:
10
+ * JSON-lines adapters (Claude Code) are append-only, so we resume from the
11
+ * last byte offset on every pass — no re-parse of the whole file. Document
12
+ * adapters (Gemini CLI) rewrite the whole file, so a changed file replaces
13
+ * that session's rows.
14
+ *
15
+ * JSON-lines pipeline per file:
11
16
  * 1. Read state from `claude_ingest_state` (offset, last_size). If file
12
17
  * grew, open it and seek to offset; otherwise skip.
13
18
  * 2. Parse each line with parseLine(). Group entries by promptId.
@@ -26,15 +31,10 @@ import * as fs from 'fs';
26
31
  import * as os from 'os';
27
32
  import * as path from 'path';
28
33
  import { computeCost, resolvePricing } from './pricing.js';
29
- import { parseLine, toEpochMs, extractUserPrompt, summarizeToolInput, toolResultLength, } from './parser.js';
34
+ import { toEpochMs } from './transcripts/shared.js';
30
35
  import { classifyToolCall } from './specship-classify.js';
31
- /**
32
- * Convert Claude's slash-escaped project dir name back into a real path.
33
- * `~/.claude/projects/-Users-alice-projects-foo` → `/Users/alice/projects/foo`
34
- */
35
- export function decodeProjectSlug(slug) {
36
- return slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug;
37
- }
36
+ import { transcriptAdapters, decodeProjectSlug, listClaudeTranscriptFiles } from './transcripts/index.js';
37
+ export { decodeProjectSlug };
38
38
  /**
39
39
  * Build a predicate that tells whether a stored `project_path` belongs to the
40
40
  * primary project, given the primary's REAL filesystem path.
@@ -54,38 +54,28 @@ export function primaryProjectMatcher(primaryRealPath) {
54
54
  return (storedPath) => storedPath === primaryRealPath || storedPath === mangled;
55
55
  }
56
56
  /**
57
- * List every JSONL file inside `<claudeRoot>/<slug>/<sessionId>.jsonl`.
58
- * Returns the absolute file path + the decoded project path.
57
+ * List every Claude Code JSONL file inside `<claudeRoot>/<slug>/<sessionId>.jsonl`.
58
+ * Kept for existing callers; the Claude transcript adapter owns the walk.
59
59
  */
60
60
  export function listTranscriptFiles(claudeRoot) {
61
+ return listClaudeTranscriptFiles(claudeRoot);
62
+ }
63
+ /**
64
+ * The adapters an ingest pass runs, each with its root. Named roots win and
65
+ * restrict the pass to those adapters (see `IngestOptions.roots`).
66
+ */
67
+ export function resolveIngestRoots(options = {}) {
68
+ const named = { ...(options.roots ?? {}) };
69
+ if (options.claudeRoot)
70
+ named['claude'] = options.claudeRoot;
71
+ const anyNamed = Object.values(named).some((r) => typeof r === 'string' && r.length > 0);
61
72
  const out = [];
62
- let projects;
63
- try {
64
- projects = fs.readdirSync(claudeRoot, { withFileTypes: true });
65
- }
66
- catch {
67
- return out;
68
- }
69
- for (const p of projects) {
70
- if (!p.isDirectory())
71
- continue;
72
- const dir = path.join(claudeRoot, p.name);
73
- let files;
74
- try {
75
- files = fs.readdirSync(dir, { withFileTypes: true });
76
- }
77
- catch {
78
- continue;
79
- }
80
- for (const f of files) {
81
- if (f.isFile() && f.name.toLowerCase().endsWith('.jsonl')) {
82
- out.push({
83
- filePath: path.join(dir, f.name),
84
- projectPath: decodeProjectSlug(p.name),
85
- projectSlug: p.name,
86
- });
87
- }
88
- }
73
+ for (const adapter of options.adapters ?? transcriptAdapters()) {
74
+ const root = named[adapter.id];
75
+ if (root)
76
+ out.push({ adapter, root });
77
+ else if (!anyNamed)
78
+ out.push({ adapter, root: adapter.defaultRoot(os.homedir()) });
89
79
  }
90
80
  return out;
91
81
  }
@@ -146,16 +136,16 @@ function readTail(filePath, fromOffset) {
146
136
  }
147
137
  }
148
138
  /**
149
- * Walk every transcript file under claudeRoot, ingest any new bytes, and
150
- * return aggregate stats. Synchronous: the caller controls cadence.
139
+ * Walk every adapter's transcript files, ingest anything new, and return
140
+ * aggregate stats. Synchronous: the caller controls cadence.
151
141
  */
152
142
  export function ingestAll(db, options = {}) {
153
143
  const start = Date.now();
154
- const claudeRoot = options.claudeRoot ?? path.join(os.homedir(), '.claude', 'projects');
155
144
  const verbose = options.verbose ?? false;
156
145
  const stats = {
157
146
  filesScanned: 0,
158
147
  filesSkipped: 0,
148
+ filesUnreadable: 0,
159
149
  bytesIngested: 0,
160
150
  linesParsed: 0,
161
151
  linesSkipped: 0,
@@ -165,12 +155,24 @@ export function ingestAll(db, options = {}) {
165
155
  durationMs: 0,
166
156
  };
167
157
  const pricing = loadPricing(db);
168
- const files = listTranscriptFiles(claudeRoot);
169
- for (const f of files) {
158
+ const files = [];
159
+ for (const { adapter, root } of resolveIngestRoots(options)) {
160
+ for (const file of adapter.listFiles(root, { projectPaths: options.projectPaths }))
161
+ files.push({ adapter, file });
162
+ }
163
+ for (const { adapter, file: f } of files) {
170
164
  stats.filesScanned++;
171
165
  try {
172
- const wasIngested = ingestFile(db, f.filePath, f.projectPath, pricing, options);
173
- if (!wasIngested.modified) {
166
+ const wasIngested = ingestFile(db, adapter, f, pricing, options);
167
+ if (wasIngested === 'unreadable') {
168
+ // Skipped and counted, never guessed at (REQ-AGENTSWITCH-004.A3).
169
+ stats.filesUnreadable++;
170
+ if (verbose) {
171
+ // eslint-disable-next-line no-console
172
+ console.error('[ingest] unreadable transcript skipped:', f.filePath);
173
+ }
174
+ }
175
+ else if (!wasIngested.modified) {
174
176
  stats.filesSkipped++;
175
177
  }
176
178
  else {
@@ -202,7 +204,28 @@ let lastIngestStats = null;
202
204
  export function getLastIngestStats() {
203
205
  return lastIngestStats;
204
206
  }
205
- function ingestFile(db, filePath, projectPath, pricing, options) {
207
+ /**
208
+ * Ingest one transcript through its adapter. Returns `'unreadable'` when a
209
+ * document adapter cannot parse the file — nothing is written for it.
210
+ */
211
+ export function ingestFile(db, adapter, file, pricing, options) {
212
+ return adapter.format === 'document'
213
+ ? ingestDocumentFile(db, adapter, file, pricing, options)
214
+ : ingestJsonlFile(db, adapter, file, pricing, options);
215
+ }
216
+ /** Resolve the graph once per file; best-effort, never aborts ingest. */
217
+ function resolveGraphFor(options, projectPath) {
218
+ if (!options.resolveGraph)
219
+ return null;
220
+ try {
221
+ return options.resolveGraph(projectPath);
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
227
+ function ingestJsonlFile(db, adapter, file, pricing, options) {
228
+ const { filePath, projectPath } = file;
206
229
  // Load state.
207
230
  const stateRow = db
208
231
  .prepare('SELECT last_offset, file_size, session_id FROM claude_ingest_state WHERE file_path = ?')
@@ -257,18 +280,10 @@ function ingestFile(db, filePath, projectPath, pricing, options) {
257
280
  // on every tool call. Returns null when no resolver is configured. The
258
281
  // resolver can throw on a locked/corrupt index; that estimate is best-effort
259
282
  // decoration and must NEVER abort transcript ingest, so degrade to null.
260
- let graph = null;
261
- if (options.resolveGraph) {
262
- try {
263
- graph = options.resolveGraph(projectPath);
264
- }
265
- catch {
266
- graph = null;
267
- }
268
- }
283
+ const graph = resolveGraphFor(options, projectPath);
269
284
  // Per-file ingest in a transaction.
270
285
  const txn = db.transaction(() => {
271
- return processLines(db, filePath, projectPath, completeLines, pricing, graph);
286
+ return processLines(db, adapter, filePath, projectPath, completeLines, pricing, graph);
272
287
  });
273
288
  const result = txn();
274
289
  // Persist new offset.
@@ -294,16 +309,121 @@ function ingestFile(db, filePath, projectPath, pricing, options) {
294
309
  toolCalls: result.toolCalls,
295
310
  };
296
311
  }
312
+ const INSERT_TOOL_CALL_SQL = `
313
+ INSERT INTO claude_tool_calls (prompt_id, session_id, assistant_uuid, tool_use_id, tool_name, input_summary, input_json, result_length, ts, is_specship, displaced_files, resolution, host, raw_tool_name)
314
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
315
+ `;
316
+ /**
317
+ * Ingest one whole-document transcript (Gemini CLI). The file is rewritten
318
+ * whole on every update, so a changed file replaces that session's prompt
319
+ * and tool-call rows in one transaction; an unchanged one (same size, not
320
+ * modified since the last pass) is skipped. An unparseable document writes
321
+ * nothing and records no state, so it is retried — and counted — every pass.
322
+ */
323
+ function ingestDocumentFile(db, adapter, file, pricing, options) {
324
+ const { filePath, projectPath } = file;
325
+ const stat = fs.statSync(filePath);
326
+ const stateRow = db
327
+ .prepare('SELECT last_ingested_at, file_size FROM claude_ingest_state WHERE file_path = ?')
328
+ .get(filePath);
329
+ // mtimeMs carries sub-millisecond precision; last_ingested_at is whole ms.
330
+ if (stateRow && stateRow.file_size === stat.size && stateRow.last_ingested_at >= Math.floor(stat.mtimeMs)) {
331
+ return { modified: false, bytes: 0, lines: 0, linesSkipped: 0, prompts: 0, toolCalls: 0 };
332
+ }
333
+ const text = fs.readFileSync(filePath, 'utf-8');
334
+ const session = adapter.parseFile(text);
335
+ if (!session)
336
+ return 'unreadable';
337
+ const now = Date.now();
338
+ db.prepare(`
339
+ INSERT INTO claude_projects (path, name, first_seen, last_seen)
340
+ VALUES (?, ?, ?, ?)
341
+ ON CONFLICT(path) DO UPDATE SET last_seen = excluded.last_seen
342
+ `).run(projectPath, projectPath, now, now);
343
+ const graph = resolveGraphFor(options, projectPath);
344
+ const txn = db.transaction(() => {
345
+ const sid = session.sessionId;
346
+ const before = {
347
+ prompts: db.prepare('SELECT COUNT(*) AS n FROM claude_prompts WHERE session_id = ?').get(sid).n,
348
+ toolCalls: db.prepare('SELECT COUNT(*) AS n FROM claude_tool_calls WHERE session_id = ?').get(sid).n,
349
+ };
350
+ db.prepare('DELETE FROM claude_tool_calls WHERE session_id = ?').run(sid);
351
+ db.prepare('DELETE FROM claude_prompts WHERE session_id = ?').run(sid);
352
+ let ti = 0, to = 0, tcw = 0, tcr = 0, cost = 0, toolCalls = 0;
353
+ const promptCosts = session.prompts.map((p) => computeCost({
354
+ input_tokens: p.inputTokens,
355
+ output_tokens: p.outputTokens,
356
+ cache_creation_input_tokens: p.cacheCreationTokens,
357
+ cache_read_input_tokens: p.cacheReadTokens,
358
+ }, resolvePricing(p.model ?? session.model ?? undefined, pricing)));
359
+ session.prompts.forEach((p, i) => {
360
+ ti += p.inputTokens;
361
+ to += p.outputTokens;
362
+ tcw += p.cacheCreationTokens;
363
+ tcr += p.cacheReadTokens;
364
+ cost += promptCosts[i];
365
+ });
366
+ db.prepare(`
367
+ INSERT INTO claude_sessions (id, project_path, source_file, started_at, ended_at, prompt_count,
368
+ total_input_tokens, total_output_tokens, total_cache_creation_tokens, total_cache_read_tokens,
369
+ total_cost_usd, last_model, host)
370
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
371
+ ON CONFLICT(id) DO UPDATE SET
372
+ ended_at = excluded.ended_at,
373
+ prompt_count = excluded.prompt_count,
374
+ total_input_tokens = excluded.total_input_tokens,
375
+ total_output_tokens = excluded.total_output_tokens,
376
+ total_cache_creation_tokens = excluded.total_cache_creation_tokens,
377
+ total_cache_read_tokens = excluded.total_cache_read_tokens,
378
+ total_cost_usd = excluded.total_cost_usd,
379
+ last_model = COALESCE(excluded.last_model, claude_sessions.last_model),
380
+ host = excluded.host
381
+ `).run(sid, projectPath, filePath, session.startedAt, session.endedAt, session.prompts.length, ti, to, tcw, tcr, cost, session.model, adapter.id);
382
+ const insPrompt = db.prepare(`
383
+ INSERT INTO claude_prompts (id, session_id, text, ts, leaf_uuid, model, input_tokens, output_tokens,
384
+ cache_creation_tokens, cache_read_tokens, cost_usd, is_sidechain, assistant_text, thinking_text)
385
+ VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, 0, ?, ?)
386
+ `);
387
+ const insToolCall = db.prepare(INSERT_TOOL_CALL_SQL);
388
+ session.prompts.forEach((p, i) => {
389
+ insPrompt.run(p.id, sid, p.text, p.ts, p.model, p.inputTokens, p.outputTokens, p.cacheCreationTokens, p.cacheReadTokens, promptCosts[i], p.assistantText, p.thinkingText);
390
+ for (const tc of p.toolCalls) {
391
+ const cls = classifyToolCall({ toolName: tc.name, inputJson: tc.inputJson, resultLength: tc.resultLength }, graph);
392
+ insToolCall.run(p.id, sid, p.id, tc.id, tc.name, tc.summary, tc.inputJson, tc.resultLength, tc.ts, cls.isSpecship, cls.displacedFiles, cls.resolution, adapter.id, tc.rawName);
393
+ toolCalls++;
394
+ }
395
+ });
396
+ return {
397
+ prompts: Math.max(0, session.prompts.length - before.prompts),
398
+ toolCalls: Math.max(0, toolCalls - before.toolCalls),
399
+ };
400
+ });
401
+ const result = txn();
402
+ db.prepare(`
403
+ INSERT INTO claude_ingest_state (file_path, last_offset, last_ingested_at, file_size, session_id)
404
+ VALUES (?, ?, ?, ?, ?)
405
+ ON CONFLICT(file_path) DO UPDATE SET
406
+ last_offset = excluded.last_offset,
407
+ last_ingested_at = excluded.last_ingested_at,
408
+ file_size = excluded.file_size,
409
+ session_id = excluded.session_id
410
+ `).run(filePath, stat.size, now, stat.size, session.sessionId);
411
+ if (options.verbose) {
412
+ // eslint-disable-next-line no-console
413
+ console.error(`[ingest] ${path.basename(filePath)} (${adapter.id}): ${session.prompts.length} prompts`);
414
+ }
415
+ return { modified: true, bytes: stat.size, lines: 0, linesSkipped: 0, prompts: result.prompts, toolCalls: result.toolCalls };
416
+ }
297
417
  /**
298
418
  * Single-pass over the entries in this file batch. We need to relate
299
419
  * tool_use blocks (in assistant entries) to their tool_result blocks (in
300
420
  * the next user entry) so result_length is captured. Maintain a map of
301
421
  * pending tool_use_id → { promptId, name, summary, ts } as we walk.
302
422
  */
303
- function processLines(db, filePath, projectPath, completeLines, pricing, graph) {
423
+ function processLines(db, adapter, filePath, projectPath, completeLines, pricing, graph) {
304
424
  const insSession = db.prepare(`
305
- INSERT INTO claude_sessions (id, project_path, source_file, started_at, ended_at, prompt_count, last_model)
306
- VALUES (?, ?, ?, ?, ?, 0, ?)
425
+ INSERT INTO claude_sessions (id, project_path, source_file, started_at, ended_at, prompt_count, last_model, host)
426
+ VALUES (?, ?, ?, ?, ?, 0, ?, '${adapter.id}')
307
427
  ON CONFLICT(id) DO UPDATE SET
308
428
  ended_at = excluded.ended_at,
309
429
  last_model = COALESCE(excluded.last_model, claude_sessions.last_model)
@@ -330,10 +450,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
330
450
  cache_read_tokens = excluded.cache_read_tokens,
331
451
  cost_usd = excluded.cost_usd
332
452
  `);
333
- const insToolCall = db.prepare(`
334
- INSERT INTO claude_tool_calls (prompt_id, session_id, assistant_uuid, tool_use_id, tool_name, input_summary, input_json, result_length, ts, is_specship, displaced_files, resolution)
335
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
336
- `);
453
+ const insToolCall = db.prepare(INSERT_TOOL_CALL_SQL);
337
454
  /**
338
455
  * Append the assistant's text + thinking blocks from one assistant turn
339
456
  * onto the prompt row. A single user prompt can span multiple assistant
@@ -388,7 +505,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
388
505
  };
389
506
  const pendingTools = new Map();
390
507
  for (const raw of completeLines) {
391
- const entry = parseLine(raw);
508
+ const entry = adapter.parseLine(raw);
392
509
  if (!entry) {
393
510
  // Blank lines aren't events; a non-empty line the parser couldn't
394
511
  // classify is a coverage gap and must be counted (REQ-DASHINT-008).
@@ -404,7 +521,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
404
521
  if (entry.type === 'user') {
405
522
  // Bookkeep session row first.
406
523
  insSession.run(sessionId, projectPath, filePath, ts, ts, null);
407
- const text = extractUserPrompt(entry);
524
+ const text = adapter.extractUserPrompt(entry);
408
525
  const promptId = entry.promptId ?? entry.uuid ?? null;
409
526
  const isSidechain = entry.isSidechain ? 1 : 0;
410
527
  if (promptId) {
@@ -435,11 +552,11 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
435
552
  if (Array.isArray(content)) {
436
553
  for (const block of content) {
437
554
  if (block && block.type === 'tool_result' && block.tool_use_id) {
438
- const len = toolResultLength(block);
555
+ const len = adapter.toolResultLength(block);
439
556
  const pending = pendingTools.get(block.tool_use_id);
440
557
  if (pending) {
441
558
  const cls = classifyToolCall({ toolName: pending.toolName, inputJson: pending.inputJson, resultLength: len }, graph);
442
- insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, block.tool_use_id, pending.toolName, pending.summary, pending.inputJson, len, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution);
559
+ insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, block.tool_use_id, pending.toolName, pending.summary, pending.inputJson, len, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution, adapter.id, pending.rawToolName);
443
560
  toolCallsInserted++;
444
561
  pendingTools.delete(block.tool_use_id);
445
562
  }
@@ -506,12 +623,14 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
506
623
  catch {
507
624
  inputJson = null;
508
625
  }
626
+ const toolName = adapter.normalizeToolName(block.name);
509
627
  pendingTools.set(block.id, {
510
628
  promptId,
511
629
  sessionId,
512
630
  assistantUuid,
513
- toolName: block.name,
514
- summary: summarizeToolInput(block.name, block.input),
631
+ toolName,
632
+ rawToolName: block.name,
633
+ summary: adapter.summarizeToolInput(toolName, block.input),
515
634
  inputJson,
516
635
  ts,
517
636
  });
@@ -546,7 +665,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
546
665
  for (const [toolUseId, pending] of pendingTools) {
547
666
  // result_length=0 for pending/unmatched tools → classifyToolCall returns 'n/a' for specship.
548
667
  const cls = classifyToolCall({ toolName: pending.toolName, inputJson: pending.inputJson, resultLength: 0 }, graph);
549
- insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, toolUseId, pending.toolName, pending.summary, pending.inputJson, 0, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution);
668
+ insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, toolUseId, pending.toolName, pending.summary, pending.inputJson, 0, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution, adapter.id, pending.rawToolName);
550
669
  toolCallsInserted++;
551
670
  }
552
671
  return {
@@ -14,6 +14,8 @@
14
14
  * one of its ancestors) whose encoding equals the slug is the real path.
15
15
  * 3. The legacy lossy decode, preserving behavior for hyphen-free paths.
16
16
  */
17
+ import { CLAUDE_PROFILE } from '../host-profiles.js';
18
+ import { recordedProjects } from './recorded-projects.js';
17
19
  import * as fs from 'fs';
18
20
  import * as os from 'os';
19
21
  import * as path from 'path';
@@ -128,13 +130,17 @@ function sniffCwd(claudeRoot, slug) {
128
130
  * mtime change (REQ-SLUGRES-001).
129
131
  */
130
132
  export function createSlugResolver(opts = {}) {
131
- const claudeJsonPath = opts.claudeJsonPath ?? path.join(os.homedir(), '.claude.json');
132
- const claudeRoot = opts.claudeRoot ?? path.join(os.homedir(), '.claude', 'projects');
133
+ const claudeJsonPath = opts.claudeJsonPath ?? path.join(os.homedir(), CLAUDE_PROFILE.userConfigFile);
134
+ const claudeRoot = opts.claudeRoot ?? path.join(os.homedir(), CLAUDE_PROFILE.configDir, 'projects');
133
135
  const sniffed = new Map();
134
136
  return (slug) => {
135
137
  const fromIndex = claudeJsonIndex(claudeJsonPath).get(slug);
136
138
  if (fromIndex)
137
139
  return fromIndex;
140
+ // A project SpecShip is installed in, by any agent (REQ-AGENTSWITCH-011).
141
+ const recorded = recordedProjects(opts.recordPath).find((p) => encodeProjectSlug(p.path) === slug);
142
+ if (recorded)
143
+ return recorded.path;
138
144
  const cached = sniffed.get(slug);
139
145
  if (cached)
140
146
  return cached;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Projects SpecShip is installed in, from the installed-agents record
3
+ * (REQ-AGENTSWITCH-006) — the agent-neutral source of project paths
4
+ * (REQ-AGENTSWITCH-011). An agent whose session folders cannot be mapped back
5
+ * to a path (Gemini CLI names them by a one-way hash of it) still has its
6
+ * projects found here. SpecShip's own file, read directly: the server never
7
+ * imports the CLI package.
8
+ */
9
+ import * as fs from 'node:fs';
10
+ import * as os from 'node:os';
11
+ import * as path from 'node:path';
12
+ /** `$SPECSHIP_INSTALL_DIR` or `~/.specship` — the same rule the CLI uses. */
13
+ export function installedAgentsRecordPath(env = process.env, home = os.homedir()) {
14
+ return path.join(env.SPECSHIP_INSTALL_DIR || path.join(home, '.specship'), 'installed-agents.json');
15
+ }
16
+ /** Every project-scoped entry in the record, one row per project. Never throws. */
17
+ export function recordedProjects(recordPath = installedAgentsRecordPath()) {
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(fs.readFileSync(recordPath, 'utf-8'));
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ const entries = parsed?.entries;
26
+ if (!Array.isArray(entries))
27
+ return [];
28
+ const byPath = new Map();
29
+ for (const e of entries) {
30
+ if (!e || typeof e !== 'object')
31
+ continue;
32
+ const { scope, projectRoot, agent, recordedAt } = e;
33
+ if (scope !== 'local' || typeof projectRoot !== 'string' || !projectRoot)
34
+ continue;
35
+ const at = typeof recordedAt === 'string' ? Date.parse(recordedAt) || 0 : 0;
36
+ const row = byPath.get(projectRoot) ?? { path: projectRoot, agents: [], recordedAtMs: 0 };
37
+ if (typeof agent === 'string' && !row.agents.includes(agent))
38
+ row.agents.push(agent);
39
+ row.recordedAtMs = Math.max(row.recordedAtMs, at);
40
+ byPath.set(projectRoot, row);
41
+ }
42
+ return [...byPath.values()];
43
+ }
@@ -10,11 +10,16 @@
10
10
  * pure (no I/O, no deps), so duplication is cheap; `__tests__/specship-impact-parity.test.ts`
11
11
  * asserts the two copies stay behaviourally identical.
12
12
  */
13
+ import { specshipToolPrefix } from '../canonical-tools.js';
13
14
  // ---------------------------------------------------------------------------
14
15
  // Constants
15
16
  // ---------------------------------------------------------------------------
16
- /** Prefix shared by every MCP tool routed through the specship server. */
17
- const MCP_SPECSHIP_PREFIX = 'mcp__specship__';
17
+ /**
18
+ * Prefix shared by every (normalised) MCP tool routed through the specship
19
+ * server. Tool names reaching this module are already in the shared set —
20
+ * each transcript adapter normalises its agent's names first (REQ-AGENTSWITCH-004).
21
+ */
22
+ const MCP_SPECSHIP_PREFIX = specshipToolPrefix;
18
23
  /**
19
24
  * Strip-prefix base names of SpecShip tools that return code-graph source.
20
25
  * These are the tools a SpecShip call can displace a native Read/Grep with.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Claude transcript line format (REQ-AGENTSWITCH-017): converts one line of a
3
+ * Claude Code JSONL transcript into a
4
+ * structured ClaudeRawEntry, or null for unparseable / unrecognised lines.
5
+ *
6
+ * Tolerant: a malformed line never throws; the worker logs and skips it.
7
+ */
8
+ export function parseLine(line) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed)
11
+ return null;
12
+ try {
13
+ const obj = JSON.parse(trimmed);
14
+ if (!obj || typeof obj !== 'object' || typeof obj.type !== 'string') {
15
+ return null;
16
+ }
17
+ return obj;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /** Extract the user-visible prompt text from a user-entry message. */
24
+ export function extractUserPrompt(entry) {
25
+ // Some user entries store text at the top level; most use message.content.
26
+ if (typeof entry.text === 'string' && entry.text.length > 0)
27
+ return entry.text;
28
+ const content = entry.message?.content;
29
+ if (typeof content === 'string')
30
+ return content;
31
+ if (Array.isArray(content)) {
32
+ return content
33
+ .filter((b) => b && typeof b === 'object')
34
+ .map((b) => (b.type === 'text' && typeof b.text === 'string' ? b.text : ''))
35
+ .filter((t) => t.length > 0)
36
+ .join('\n')
37
+ .trim();
38
+ }
39
+ return '';
40
+ }
41
+ /** Sum the character length of a tool_result content block. */
42
+ export function toolResultLength(block) {
43
+ if (!block)
44
+ return 0;
45
+ const c = block.content;
46
+ if (typeof c === 'string')
47
+ return c.length;
48
+ if (Array.isArray(c)) {
49
+ return c.reduce((sum, item) => sum + (typeof item?.text === 'string' ? item.text.length : 0), 0);
50
+ }
51
+ return 0;
52
+ }
53
+ // The agent-neutral helpers live in ./shared; re-exported for this adapter's callers.
54
+ export { toEpochMs, summarizeToolInput } from './shared.js';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Claude Code transcript adapter (REQ-AGENTSWITCH-004).
3
+ *
4
+ * Sessions live in `~/.claude/projects/<slug>/<sessionId>.jsonl`, where
5
+ * `<slug>` is the slash-escaped absolute project path. Files are append-only
6
+ * JSON lines, so the ingestor resumes from the last byte offset.
7
+ *
8
+ * Claude Code's tool names ARE the shared set (`Read`, `Bash`,
9
+ * `mcp__<server>__<tool>` …), so normalisation is the identity — which is
10
+ * what keeps existing rows and counts unchanged (REQ-AGENTSWITCH-004.A2).
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { CLAUDE_PROFILE } from '../../host-profiles.js';
15
+ import { extractUserPrompt, parseLine, toolResultLength } from './claude-jsonl.js';
16
+ import { summarizeToolInput } from './shared.js';
17
+ /**
18
+ * Convert Claude's slash-escaped project dir name back into a real path.
19
+ * `-Users-alice-projects-foo` → `/Users/alice/projects/foo`
20
+ */
21
+ export function decodeProjectSlug(slug) {
22
+ return slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug;
23
+ }
24
+ /** Every `<root>/<slug>/<sessionId>.jsonl`, with the decoded project path. */
25
+ export function listClaudeTranscriptFiles(root) {
26
+ const out = [];
27
+ let projects;
28
+ try {
29
+ projects = fs.readdirSync(root, { withFileTypes: true });
30
+ }
31
+ catch {
32
+ return out;
33
+ }
34
+ for (const p of projects) {
35
+ if (!p.isDirectory())
36
+ continue;
37
+ const dir = path.join(root, p.name);
38
+ let files;
39
+ try {
40
+ files = fs.readdirSync(dir, { withFileTypes: true });
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ for (const f of files) {
46
+ if (f.isFile() && f.name.toLowerCase().endsWith('.jsonl')) {
47
+ out.push({ filePath: path.join(dir, f.name), projectPath: decodeProjectSlug(p.name), projectSlug: p.name });
48
+ }
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ export const claudeTranscriptAdapter = {
54
+ id: CLAUDE_PROFILE.id,
55
+ format: 'jsonl',
56
+ defaultRoot: (home) => path.join(home, CLAUDE_PROFILE.configDir, 'projects'),
57
+ listFiles: (root) => listClaudeTranscriptFiles(root),
58
+ watches: (fileName) => fileName.toLowerCase().endsWith('.jsonl'),
59
+ normalizeToolName: (raw) => raw,
60
+ parseLine,
61
+ summarizeToolInput,
62
+ extractUserPrompt,
63
+ toolResultLength,
64
+ };