@specship/specship-darwin-arm64 0.29.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 (289) 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 +215 -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 +9 -0
  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/enforce/changed-files.d.ts +7 -0
  55. package/lib/dist/enforce/changed-files.d.ts.map +1 -1
  56. package/lib/dist/enforce/changed-files.js +17 -0
  57. package/lib/dist/enforce/changed-files.js.map +1 -1
  58. package/lib/dist/enforce/enforce.d.ts +35 -1
  59. package/lib/dist/enforce/enforce.d.ts.map +1 -1
  60. package/lib/dist/enforce/enforce.js +66 -2
  61. package/lib/dist/enforce/enforce.js.map +1 -1
  62. package/lib/dist/extraction/index.d.ts +50 -4
  63. package/lib/dist/extraction/index.d.ts.map +1 -1
  64. package/lib/dist/extraction/index.js +105 -13
  65. package/lib/dist/extraction/index.js.map +1 -1
  66. package/lib/dist/extraction/parse-warnings.d.ts +16 -0
  67. package/lib/dist/extraction/parse-warnings.d.ts.map +1 -0
  68. package/lib/dist/extraction/parse-warnings.js +30 -0
  69. package/lib/dist/extraction/parse-warnings.js.map +1 -0
  70. package/lib/dist/extraction/tree-sitter.d.ts +27 -0
  71. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  72. package/lib/dist/extraction/tree-sitter.js +168 -1
  73. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  74. package/lib/dist/health/smoke-check.js +1 -1
  75. package/lib/dist/health/smoke-check.js.map +1 -1
  76. package/lib/dist/hooks/hooks.json +16 -1
  77. package/lib/dist/index.d.ts +63 -3
  78. package/lib/dist/index.d.ts.map +1 -1
  79. package/lib/dist/index.js +235 -9
  80. package/lib/dist/index.js.map +1 -1
  81. package/lib/dist/installer/agent-record.d.ts +80 -0
  82. package/lib/dist/installer/agent-record.d.ts.map +1 -0
  83. package/lib/dist/installer/agent-record.js +351 -0
  84. package/lib/dist/installer/agent-record.js.map +1 -0
  85. package/lib/dist/installer/auto-migrate.d.ts +56 -0
  86. package/lib/dist/installer/auto-migrate.d.ts.map +1 -0
  87. package/lib/dist/installer/auto-migrate.js +328 -0
  88. package/lib/dist/installer/auto-migrate.js.map +1 -0
  89. package/lib/dist/installer/index.d.ts +37 -8
  90. package/lib/dist/installer/index.d.ts.map +1 -1
  91. package/lib/dist/installer/index.js +156 -46
  92. package/lib/dist/installer/index.js.map +1 -1
  93. package/lib/dist/installer/install-defaults.d.ts +4 -3
  94. package/lib/dist/installer/install-defaults.d.ts.map +1 -1
  95. package/lib/dist/installer/install-defaults.js +6 -5
  96. package/lib/dist/installer/install-defaults.js.map +1 -1
  97. package/lib/dist/installer/{config-writer.d.ts → targets/claude-config-writer.d.ts} +1 -1
  98. package/lib/dist/installer/targets/claude-config-writer.d.ts.map +1 -0
  99. package/lib/dist/installer/{config-writer.js → targets/claude-config-writer.js} +7 -6
  100. package/lib/dist/installer/targets/claude-config-writer.js.map +1 -0
  101. package/lib/dist/installer/targets/claude-projects.d.ts +9 -0
  102. package/lib/dist/installer/targets/claude-projects.d.ts.map +1 -0
  103. package/lib/dist/installer/targets/claude-projects.js +151 -0
  104. package/lib/dist/installer/targets/claude-projects.js.map +1 -0
  105. package/lib/dist/installer/targets/claude.d.ts +28 -1
  106. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  107. package/lib/dist/installer/targets/claude.js +39 -20
  108. package/lib/dist/installer/targets/claude.js.map +1 -1
  109. package/lib/dist/installer/targets/feature-parity.d.ts +28 -0
  110. package/lib/dist/installer/targets/feature-parity.d.ts.map +1 -0
  111. package/lib/dist/installer/targets/feature-parity.js +32 -0
  112. package/lib/dist/installer/targets/feature-parity.js.map +1 -0
  113. package/lib/dist/installer/{gemini-commands.d.ts → targets/gemini-commands.d.ts} +7 -3
  114. package/lib/dist/installer/targets/gemini-commands.d.ts.map +1 -0
  115. package/lib/dist/installer/{gemini-commands.js → targets/gemini-commands.js} +35 -14
  116. package/lib/dist/installer/targets/gemini-commands.js.map +1 -0
  117. package/lib/dist/installer/targets/gemini-projects.d.ts +11 -0
  118. package/lib/dist/installer/targets/gemini-projects.d.ts.map +1 -0
  119. package/lib/dist/installer/targets/gemini-projects.js +84 -0
  120. package/lib/dist/installer/targets/gemini-projects.js.map +1 -0
  121. package/lib/dist/installer/targets/gemini.d.ts +94 -8
  122. package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
  123. package/lib/dist/installer/targets/gemini.js +392 -33
  124. package/lib/dist/installer/targets/gemini.js.map +1 -1
  125. package/lib/dist/installer/targets/registry.d.ts +8 -1
  126. package/lib/dist/installer/targets/registry.d.ts.map +1 -1
  127. package/lib/dist/installer/targets/registry.js +14 -5
  128. package/lib/dist/installer/targets/registry.js.map +1 -1
  129. package/lib/dist/installer/targets/shared.d.ts +44 -1
  130. package/lib/dist/installer/targets/shared.d.ts.map +1 -1
  131. package/lib/dist/installer/targets/shared.js +130 -3
  132. package/lib/dist/installer/targets/shared.js.map +1 -1
  133. package/lib/dist/installer/targets/types.d.ts +23 -1
  134. package/lib/dist/installer/targets/types.d.ts.map +1 -1
  135. package/lib/dist/jira/auto-publish.d.ts +22 -2
  136. package/lib/dist/jira/auto-publish.d.ts.map +1 -1
  137. package/lib/dist/jira/auto-publish.js +54 -9
  138. package/lib/dist/jira/auto-publish.js.map +1 -1
  139. package/lib/dist/jira/board-first.d.ts +15 -2
  140. package/lib/dist/jira/board-first.d.ts.map +1 -1
  141. package/lib/dist/jira/board-first.js +16 -0
  142. package/lib/dist/jira/board-first.js.map +1 -1
  143. package/lib/dist/jira/ci-publish.d.ts +72 -0
  144. package/lib/dist/jira/ci-publish.d.ts.map +1 -0
  145. package/lib/dist/jira/ci-publish.js +162 -0
  146. package/lib/dist/jira/ci-publish.js.map +1 -0
  147. package/lib/dist/jira/publish.d.ts +23 -0
  148. package/lib/dist/jira/publish.d.ts.map +1 -1
  149. package/lib/dist/jira/publish.js +0 -0
  150. package/lib/dist/jira/publish.js.map +1 -1
  151. package/lib/dist/jira/regression-pack.d.ts.map +1 -1
  152. package/lib/dist/jira/regression-pack.js +7 -1
  153. package/lib/dist/jira/regression-pack.js.map +1 -1
  154. package/lib/dist/jira/repo-config.d.ts.map +1 -1
  155. package/lib/dist/jira/repo-config.js +8 -0
  156. package/lib/dist/jira/repo-config.js.map +1 -1
  157. package/lib/dist/jira/types.d.ts +8 -0
  158. package/lib/dist/jira/types.d.ts.map +1 -1
  159. package/lib/dist/jira/types.js.map +1 -1
  160. package/lib/dist/knowledge/enforce.d.ts.map +1 -1
  161. package/lib/dist/knowledge/enforce.js +6 -1
  162. package/lib/dist/knowledge/enforce.js.map +1 -1
  163. package/lib/dist/mcp/jira-tools.d.ts +5 -0
  164. package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
  165. package/lib/dist/mcp/jira-tools.js +59 -4
  166. package/lib/dist/mcp/jira-tools.js.map +1 -1
  167. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  168. package/lib/dist/mcp/proxy.js +2 -1
  169. package/lib/dist/mcp/proxy.js.map +1 -1
  170. package/lib/dist/mcp/server-instructions.d.ts +16 -10
  171. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  172. package/lib/dist/mcp/server-instructions.js +69 -20
  173. package/lib/dist/mcp/server-instructions.js.map +1 -1
  174. package/lib/dist/mcp/session.d.ts +11 -0
  175. package/lib/dist/mcp/session.d.ts.map +1 -1
  176. package/lib/dist/mcp/session.js +43 -1
  177. package/lib/dist/mcp/session.js.map +1 -1
  178. package/lib/dist/mcp/tools.d.ts.map +1 -1
  179. package/lib/dist/mcp/tools.js +8 -0
  180. package/lib/dist/mcp/tools.js.map +1 -1
  181. package/lib/dist/reflect/apply.d.ts +2 -2
  182. package/lib/dist/reflect/apply.d.ts.map +1 -1
  183. package/lib/dist/reflect/apply.js +6 -5
  184. package/lib/dist/reflect/apply.js.map +1 -1
  185. package/lib/dist/reflect/miner.d.ts.map +1 -1
  186. package/lib/dist/reflect/miner.js +28 -15
  187. package/lib/dist/reflect/miner.js.map +1 -1
  188. package/lib/dist/reflect/session-outcomes.d.ts.map +1 -1
  189. package/lib/dist/reflect/session-outcomes.js +8 -3
  190. package/lib/dist/reflect/session-outcomes.js.map +1 -1
  191. package/lib/dist/reflect/targets.d.ts.map +1 -1
  192. package/lib/dist/reflect/targets.js +25 -5
  193. package/lib/dist/reflect/targets.js.map +1 -1
  194. package/lib/dist/reflect/types.d.ts +7 -0
  195. package/lib/dist/reflect/types.d.ts.map +1 -1
  196. package/lib/dist/resolution/spec-link-resolver.d.ts +14 -2
  197. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  198. package/lib/dist/resolution/spec-link-resolver.js +13 -3
  199. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  200. package/lib/dist/server/canonical-tools.js +50 -0
  201. package/lib/dist/server/cli.js +2 -2
  202. package/lib/dist/server/host-profiles.js +39 -0
  203. package/lib/dist/server/ingest/index.js +2 -1
  204. package/lib/dist/server/ingest/ingestor.js +195 -76
  205. package/lib/dist/server/ingest/project-paths.js +8 -2
  206. package/lib/dist/server/ingest/recorded-projects.js +43 -0
  207. package/lib/dist/server/ingest/specship-classify.js +7 -2
  208. package/lib/dist/server/ingest/transcripts/claude-jsonl.js +54 -0
  209. package/lib/dist/server/ingest/transcripts/claude.js +64 -0
  210. package/lib/dist/server/ingest/transcripts/gemini.js +469 -0
  211. package/lib/dist/server/ingest/transcripts/index.js +14 -0
  212. package/lib/dist/server/ingest/{parser.js → transcripts/shared.js} +3 -49
  213. package/lib/dist/server/ingest/transcripts/types.js +4 -0
  214. package/lib/dist/server/ingest/watcher.js +31 -30
  215. package/lib/dist/server/routes/claude.js +27 -10
  216. package/lib/dist/server/routes/graph.js +64 -3
  217. package/lib/dist/server/routes/mcp.js +57 -15
  218. package/lib/dist/server/routes/memory.js +54 -35
  219. package/lib/dist/server/routes/projects.js +78 -7
  220. package/lib/dist/server/routes/spec.js +125 -0
  221. package/lib/dist/server/server.js +8 -2
  222. package/lib/dist/skills/spec-author/SKILL.md +1 -1
  223. package/lib/dist/skills/spec-author/references/gap-questions.md +1 -1
  224. package/lib/dist/spec-lint/lint.d.ts +26 -1
  225. package/lib/dist/spec-lint/lint.d.ts.map +1 -1
  226. package/lib/dist/spec-lint/lint.js +96 -7
  227. package/lib/dist/spec-lint/lint.js.map +1 -1
  228. package/lib/dist/sync/worktree.d.ts +48 -0
  229. package/lib/dist/sync/worktree.d.ts.map +1 -1
  230. package/lib/dist/sync/worktree.js +109 -0
  231. package/lib/dist/sync/worktree.js.map +1 -1
  232. package/lib/dist/ui/assets/bricolage-latin-C23fTOVi.woff2 +0 -0
  233. package/lib/dist/ui/assets/bricolage-latin-ext-B82pxkPf.woff2 +0 -0
  234. package/lib/dist/ui/assets/index-54vorjyq.css +1 -0
  235. package/lib/dist/ui/assets/index-BZQonCVb.js +52 -0
  236. package/lib/dist/ui/assets/space-grotesk-latin-DPT1xrvW.woff2 +0 -0
  237. package/lib/dist/ui/assets/space-grotesk-latin-ext-wJGaI8U0.woff2 +0 -0
  238. package/lib/dist/ui/index.html +2 -2
  239. package/lib/dist/update/updater.d.ts +6 -0
  240. package/lib/dist/update/updater.d.ts.map +1 -1
  241. package/lib/dist/update/updater.js +13 -1
  242. package/lib/dist/update/updater.js.map +1 -1
  243. package/lib/dist/workflows/agents/claude.d.ts +9 -0
  244. package/lib/dist/workflows/agents/claude.d.ts.map +1 -1
  245. package/lib/dist/workflows/agents/claude.js +22 -3
  246. package/lib/dist/workflows/agents/claude.js.map +1 -1
  247. package/lib/dist/workflows/agents/gemini-version.d.ts +18 -0
  248. package/lib/dist/workflows/agents/gemini-version.d.ts.map +1 -0
  249. package/lib/dist/workflows/agents/gemini-version.js +81 -0
  250. package/lib/dist/workflows/agents/gemini-version.js.map +1 -0
  251. package/lib/dist/workflows/agents/gemini.d.ts +1 -1
  252. package/lib/dist/workflows/agents/gemini.d.ts.map +1 -1
  253. package/lib/dist/workflows/agents/gemini.js +12 -2
  254. package/lib/dist/workflows/agents/gemini.js.map +1 -1
  255. package/lib/dist/workflows/agents/index.d.ts +2 -0
  256. package/lib/dist/workflows/agents/index.d.ts.map +1 -1
  257. package/lib/dist/workflows/agents/index.js +5 -0
  258. package/lib/dist/workflows/agents/index.js.map +1 -1
  259. package/lib/dist/workflows/agents/types.d.ts +13 -0
  260. package/lib/dist/workflows/agents/types.d.ts.map +1 -1
  261. package/lib/dist/workflows/agents/types.js.map +1 -1
  262. package/lib/dist/workflows/condition-evaluator.d.ts +8 -0
  263. package/lib/dist/workflows/condition-evaluator.d.ts.map +1 -1
  264. package/lib/dist/workflows/condition-evaluator.js +7 -0
  265. package/lib/dist/workflows/condition-evaluator.js.map +1 -1
  266. package/lib/dist/workflows/defaults/claude-design-implement.yaml +11 -11
  267. package/lib/dist/workflows/defaults/spec-author.yaml +9 -9
  268. package/lib/dist/workflows/defaults/spec-fix.yaml +8 -8
  269. package/lib/dist/workflows/defaults/spec-implement.yaml +38 -23
  270. package/lib/dist/workflows/defaults/spec-relink.yaml +5 -5
  271. package/lib/dist/workflows/defaults/spec-verify.yaml +3 -3
  272. package/lib/dist/workflows/executor.d.ts +7 -0
  273. package/lib/dist/workflows/executor.d.ts.map +1 -1
  274. package/lib/dist/workflows/executor.js +22 -5
  275. package/lib/dist/workflows/executor.js.map +1 -1
  276. package/lib/node_modules/.package-lock.json +1 -1
  277. package/lib/package.json +3 -2
  278. package/package.json +1 -1
  279. package/lib/dist/installer/config-writer.d.ts.map +0 -1
  280. package/lib/dist/installer/config-writer.js.map +0 -1
  281. package/lib/dist/installer/gemini-commands.d.ts.map +0 -1
  282. package/lib/dist/installer/gemini-commands.js.map +0 -1
  283. package/lib/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  284. package/lib/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  285. package/lib/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  286. package/lib/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  287. package/lib/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  288. package/lib/dist/ui/assets/index-BzuUWUAE.js +0 -50
  289. package/lib/dist/ui/assets/index-ezwX2EfG.css +0 -1
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Gemini CLI transcript adapter (REQ-AGENTSWITCH-004, DOM-SPECSHIP-007).
3
+ *
4
+ * Verified against @google/gemini-cli-core 0.28.0
5
+ * (services/chatRecordingService.js, core/geminiChat.js, config/storage.js):
6
+ *
7
+ * - a session is ONE pretty-printed JSON document at
8
+ * `~/.gemini/tmp/<project_hash>/chats/session-<timestamp>-<id8>.json`,
9
+ * rewritten whole on every update — never JSON lines;
10
+ * - `<project_hash>` is sha256(project root) in hex, so a project is
11
+ * recognised by hashing the roots the caller knows; otherwise the path is
12
+ * kept as `gemini:<hash>` (never guessed);
13
+ * - `messages[]` carry `type` ('user' | 'gemini' | 'info' | …), `content`
14
+ * (string or parts), and on gemini messages `toolCalls[]`
15
+ * (`{ id, name, args, result: responseParts | null, timestamp }`),
16
+ * `thoughts[]` (`{ subject, description }`), `tokens`
17
+ * (`{ input, output, cached, thoughts, … }`) and `model`;
18
+ * - an MCP tool is recorded under its bare name (`specship_explore`); the
19
+ * qualified `<server>__<tool>` form appears only on a name collision.
20
+ *
21
+ * Gemini CLI 0.33.0 changed both (REQ-AGENTSWITCH-018; verified against
22
+ * 0.67.0 services/chatRecordingService and config/storage):
23
+ *
24
+ * - a session is JSON Lines (`session-*.jsonl`): a metadata record
25
+ * (`sessionId`, `projectHash`, `startTime`, …, optionally `messages`),
26
+ * then message records (keyed by `id`, the last one for an id wins),
27
+ * `{ $set }` metadata updates (a `$set.messages` replaces the list),
28
+ * `{ $patch }` records (`content` and tool-call `result`s for an id,
29
+ * `updates[]`, `removeIds[]`, `orderIds[]`) and `{ $rewindTo }`;
30
+ * - the folder is a short project name from Gemini's project registry, so
31
+ * the project is resolved from the header's `projectHash` (still
32
+ * sha256 of the root);
33
+ * - MCP tools are named `mcp_<server>_<tool>`;
34
+ * - tool responses and Gemini's `<session_context>` / `<hook_context>`
35
+ * turns are also `user` messages (they open no prompt), and a prompt's
36
+ * own text is in `displayContent` when Gemini added context to it.
37
+ *
38
+ * Tool names are mapped onto the shared set; a document that does not parse,
39
+ * or has no `messages` array, is unreadable — `parseFile` returns null and
40
+ * the ingestor counts it.
41
+ */
42
+ import * as crypto from 'crypto';
43
+ import * as fs from 'fs';
44
+ import * as path from 'path';
45
+ import { GEMINI_PROFILE } from '../../host-profiles.js';
46
+ import { CANONICAL_TOOLS, specshipServerName, canonicalMcpToolName } from '../../canonical-tools.js';
47
+ import { summarizeToolInput, toEpochMs } from './shared.js';
48
+ import { normalizeToolName, } from './types.js';
49
+ /** Gemini CLI 0.28.0 built-in tool names → shared names. */
50
+ export const GEMINI_TOOL_NAMES = Object.freeze({
51
+ read_file: CANONICAL_TOOLS.read,
52
+ read_many_files: CANONICAL_TOOLS.read,
53
+ run_shell_command: CANONICAL_TOOLS.bash,
54
+ grep_search: CANONICAL_TOOLS.grep,
55
+ glob: CANONICAL_TOOLS.glob,
56
+ write_file: CANONICAL_TOOLS.write,
57
+ replace: CANONICAL_TOOLS.edit,
58
+ });
59
+ /** Bare SpecShip tools all start with this (tools/mcp-tool.js records the server's own tool name). */
60
+ const bareSpecshipPrefix = `${specshipServerName}_`;
61
+ /** The collision-qualified form Gemini used before 0.33.0: `<server>__<tool>`. */
62
+ const QUALIFIED_RE = /^([A-Za-z0-9_-]+?)__(.+)$/;
63
+ /** The form Gemini uses from 0.33.0: `mcp_<server>_<tool>` (server names carry no `_`). */
64
+ const MCP_PREFIXED_RE = /^mcp_([^_]+)_(.+)$/;
65
+ /** Map one Gemini tool name onto the shared set. */
66
+ export function normalizeGeminiToolName(raw) {
67
+ const builtin = normalizeToolName(GEMINI_TOOL_NAMES, raw);
68
+ if (builtin !== raw)
69
+ return builtin;
70
+ const p = raw.match(MCP_PREFIXED_RE);
71
+ if (p)
72
+ return canonicalMcpToolName(p[1], p[2]);
73
+ const q = raw.match(QUALIFIED_RE);
74
+ if (q)
75
+ return canonicalMcpToolName(q[1], q[2]);
76
+ if (raw.startsWith(bareSpecshipPrefix))
77
+ return canonicalMcpToolName(specshipServerName, raw);
78
+ return raw;
79
+ }
80
+ /** Re-key Gemini args onto the shape the shared summariser reads. */
81
+ function summarize(raw, name, args) {
82
+ if (!args || typeof args !== 'object')
83
+ return '';
84
+ if (raw === 'read_many_files') {
85
+ const inc = args['include'] ?? args['paths'];
86
+ if (Array.isArray(inc))
87
+ return inc.filter((x) => typeof x === 'string').join(', ').slice(0, 400);
88
+ }
89
+ const file = args['file_path'] ?? args['absolute_path'] ?? args['path'];
90
+ const keyed = typeof file === 'string' && args['file_path'] === undefined ? { ...args, file_path: file } : args;
91
+ return summarizeToolInput(name, keyed);
92
+ }
93
+ /** Text carried by a Gemini content / result value (string, part, or part list). */
94
+ function textLength(v) {
95
+ if (v == null)
96
+ return 0;
97
+ if (typeof v === 'string')
98
+ return v.length;
99
+ if (Array.isArray(v))
100
+ return v.reduce((n, p) => n + textLength(p), 0);
101
+ if (typeof v === 'object') {
102
+ const o = v;
103
+ if (typeof o['text'] === 'string')
104
+ return o['text'].length;
105
+ const fr = o['functionResponse'];
106
+ if (fr && typeof fr === 'object') {
107
+ const out = fr.response?.['output'];
108
+ if (typeof out === 'string')
109
+ return out.length;
110
+ try {
111
+ return JSON.stringify(fr.response ?? null).length;
112
+ }
113
+ catch {
114
+ return 0;
115
+ }
116
+ }
117
+ }
118
+ return 0;
119
+ }
120
+ function contentText(v) {
121
+ if (typeof v === 'string')
122
+ return v;
123
+ if (Array.isArray(v)) {
124
+ return v.map((p) => (p && typeof p === 'object' && typeof p.text === 'string' ? p.text : ''))
125
+ .filter(Boolean).join('\n').trim();
126
+ }
127
+ if (v && typeof v === 'object' && typeof v.text === 'string')
128
+ return v.text;
129
+ return '';
130
+ }
131
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
132
+ const isRec = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
133
+ const strProp = (o, k) => (typeof o[k] === 'string' ? o[k] : undefined);
134
+ /**
135
+ * Fold a JSON Lines session (Gemini CLI >= 0.33.0) into the single-document
136
+ * shape, applying records in order the way Gemini's own loader does.
137
+ * Malformed lines are skipped; null when no metadata record is found.
138
+ */
139
+ export function foldGeminiJsonl(text) {
140
+ let meta = null;
141
+ const messages = new Map();
142
+ const setMessages = (list) => {
143
+ if (!Array.isArray(list))
144
+ return;
145
+ for (const m of list)
146
+ if (isRec(m) && strProp(m, 'id'))
147
+ messages.set(m['id'], m);
148
+ };
149
+ const patchOne = (patch) => {
150
+ const existing = messages.get(patch['id']);
151
+ if (!existing)
152
+ return;
153
+ if (patch['content'] !== undefined)
154
+ existing['content'] = patch['content'];
155
+ const calls = existing['toolCalls'];
156
+ if (Array.isArray(patch['toolCalls']) && Array.isArray(calls)) {
157
+ for (const tp of patch['toolCalls']) {
158
+ if (!isRec(tp) || !('result' in tp))
159
+ continue;
160
+ const tc = calls.find((c) => isRec(c) && c['id'] === tp['id']);
161
+ if (isRec(tc))
162
+ tc['result'] = tp['result'];
163
+ }
164
+ }
165
+ };
166
+ for (const line of text.split('\n')) {
167
+ if (!line.trim())
168
+ continue;
169
+ let r;
170
+ try {
171
+ r = JSON.parse(line);
172
+ }
173
+ catch {
174
+ continue;
175
+ }
176
+ if (!isRec(r))
177
+ continue;
178
+ const rewind = strProp(r, '$rewindTo');
179
+ if (rewind !== undefined) {
180
+ let found = false;
181
+ for (const id of [...messages.keys()]) {
182
+ if (id === rewind)
183
+ found = true;
184
+ if (found)
185
+ messages.delete(id);
186
+ }
187
+ if (!found)
188
+ messages.clear();
189
+ }
190
+ else if (isRec(r['$patch'])) {
191
+ const p = r['$patch'];
192
+ if (strProp(p, 'id'))
193
+ patchOne(p);
194
+ if (Array.isArray(p['updates']))
195
+ for (const u of p['updates'])
196
+ if (isRec(u) && strProp(u, 'id'))
197
+ patchOne(u);
198
+ if (Array.isArray(p['removeIds']))
199
+ for (const id of p['removeIds'])
200
+ if (typeof id === 'string')
201
+ messages.delete(id);
202
+ if (Array.isArray(p['orderIds'])) {
203
+ const ordered = p['orderIds'].filter((id) => typeof id === 'string' && messages.has(id));
204
+ const keep = new Set(ordered);
205
+ const entries = [...messages.entries()];
206
+ messages.clear();
207
+ for (const [id, m] of entries)
208
+ if (!keep.has(id))
209
+ messages.set(id, m);
210
+ for (const id of ordered)
211
+ messages.set(id, entries.find(([k]) => k === id)[1]);
212
+ }
213
+ }
214
+ else if (strProp(r, 'id')) {
215
+ messages.set(r['id'], r);
216
+ }
217
+ else if (isRec(r['$set'])) {
218
+ const set = r['$set'];
219
+ if (Array.isArray(set['messages'])) {
220
+ messages.clear();
221
+ setMessages(set['messages']);
222
+ }
223
+ meta = { ...(meta ?? {}), ...set };
224
+ }
225
+ else if (strProp(r, 'sessionId') && strProp(r, 'projectHash')) {
226
+ meta = { ...(meta ?? {}), ...r };
227
+ setMessages(r['messages']);
228
+ }
229
+ }
230
+ if (!meta)
231
+ return null;
232
+ return { ...meta, messages: [...messages.values()] };
233
+ }
234
+ /** Parse one Gemini session (a JSON document, or JSON Lines). Null when it cannot be read. */
235
+ export function parseGeminiSession(text) {
236
+ let doc;
237
+ try {
238
+ doc = JSON.parse(text);
239
+ }
240
+ catch {
241
+ doc = null;
242
+ }
243
+ // A JSON Lines session — or one holding only its header line, which parses whole.
244
+ if (!isRec(doc) || !Array.isArray(doc['messages']))
245
+ doc = foldGeminiJsonl(text);
246
+ if (!doc || typeof doc !== 'object')
247
+ return null;
248
+ const d = doc;
249
+ if (typeof d.sessionId !== 'string' || !d.sessionId || !Array.isArray(d.messages))
250
+ return null;
251
+ const sessionId = d.sessionId;
252
+ const startedAt = typeof d.startTime === 'string' ? toEpochMs(d.startTime) : 0;
253
+ const prompts = [];
254
+ let current = null;
255
+ let lastModel = null;
256
+ let lastTs = startedAt;
257
+ const openPrompt = (id, text, ts) => {
258
+ const p = {
259
+ id, text, ts, model: null,
260
+ inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0,
261
+ assistantText: null, thinkingText: null, toolCalls: [],
262
+ };
263
+ prompts.push(p);
264
+ return p;
265
+ };
266
+ d.messages.forEach((m, i) => {
267
+ if (!m || typeof m !== 'object')
268
+ return;
269
+ const msg = m;
270
+ const ts = typeof msg['timestamp'] === 'string' ? toEpochMs(msg['timestamp']) : lastTs;
271
+ lastTs = Math.max(lastTs, ts);
272
+ const id = typeof msg['id'] === 'string' && msg['id'] ? msg['id'] : `${sessionId}:${i}`;
273
+ if (msg['type'] === 'user') {
274
+ // From 0.33.0 Gemini also records tool responses (no text parts) and
275
+ // its own `<session_context>` / `<hook_context>` turns as user
276
+ // messages; like Gemini's own resume logic, those start no prompt.
277
+ const said = contentText(msg['content']);
278
+ if (!said || /^<(session|hook)_context>/.test(said))
279
+ return;
280
+ current = openPrompt(id, contentText(msg['displayContent']) || said, ts);
281
+ return;
282
+ }
283
+ if (msg['type'] !== 'gemini')
284
+ return;
285
+ // A gemini turn before any user message still belongs to a prompt row.
286
+ const prompt = current ?? (current = openPrompt(`${sessionId}:prompt`, null, ts));
287
+ const model = typeof msg['model'] === 'string' && msg['model'] ? msg['model'] : null;
288
+ if (model) {
289
+ prompt.model = model;
290
+ lastModel = model;
291
+ }
292
+ const tokens = (msg['tokens'] ?? null);
293
+ if (tokens && typeof tokens === 'object') {
294
+ const cached = num(tokens['cached']);
295
+ prompt.inputTokens += Math.max(0, num(tokens['input']) - cached);
296
+ prompt.cacheReadTokens += cached;
297
+ prompt.outputTokens += num(tokens['output']) + num(tokens['thoughts']);
298
+ }
299
+ const said = contentText(msg['content']);
300
+ if (said)
301
+ prompt.assistantText = (prompt.assistantText ?? '') + said + '\n\n';
302
+ const thoughts = msg['thoughts'];
303
+ if (Array.isArray(thoughts)) {
304
+ const t = thoughts
305
+ .map((th) => (th && typeof th === 'object'
306
+ ? [th.subject, th.description]
307
+ .filter((x) => typeof x === 'string' && x.length > 0).join(': ')
308
+ : ''))
309
+ .filter(Boolean).join('\n');
310
+ if (t)
311
+ prompt.thinkingText = (prompt.thinkingText ?? '') + t + '\n\n';
312
+ }
313
+ const calls = msg['toolCalls'];
314
+ if (!Array.isArray(calls))
315
+ return;
316
+ calls.forEach((c, j) => {
317
+ if (!c || typeof c !== 'object')
318
+ return;
319
+ const call = c;
320
+ const rawName = typeof call['name'] === 'string' ? call['name'] : '';
321
+ if (!rawName)
322
+ return;
323
+ const name = normalizeGeminiToolName(rawName);
324
+ const args = call['args'] && typeof call['args'] === 'object' ? call['args'] : undefined;
325
+ let inputJson = null;
326
+ try {
327
+ inputJson = JSON.stringify(call['args'] ?? null);
328
+ }
329
+ catch {
330
+ inputJson = null;
331
+ }
332
+ const tc = {
333
+ id: typeof call['id'] === 'string' && call['id'] ? call['id'] : `${id}:${j}`,
334
+ rawName,
335
+ name,
336
+ summary: summarize(rawName, name, args),
337
+ inputJson,
338
+ resultLength: textLength(call['result']),
339
+ ts: typeof call['timestamp'] === 'string' ? toEpochMs(call['timestamp']) : ts,
340
+ };
341
+ prompt.toolCalls.push(tc);
342
+ });
343
+ });
344
+ const endedAt = typeof d.lastUpdated === 'string' ? Math.max(toEpochMs(d.lastUpdated), lastTs) : lastTs;
345
+ return { sessionId, startedAt: startedAt || (prompts[0]?.ts ?? endedAt), endedAt, model: lastModel, prompts };
346
+ }
347
+ /** sha256(root) in hex — Gemini's project hash (config/storage.js getFilePathHash). */
348
+ export function geminiProjectHash(projectRoot) {
349
+ return crypto.createHash('sha256').update(projectRoot).digest('hex');
350
+ }
351
+ const isSessionFile = (name) => /\.jsonl?$/i.test(name);
352
+ /** The `projectHash` in a JSON Lines session's header, read from its first line. */
353
+ function headerProjectHash(file) {
354
+ let fd;
355
+ try {
356
+ fd = fs.openSync(file, 'r');
357
+ const buf = Buffer.alloc(64 * 1024);
358
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
359
+ const first = buf.toString('utf-8', 0, n).split('\n', 1)[0] ?? '';
360
+ const rec = JSON.parse(first);
361
+ return isRec(rec) && typeof rec['projectHash'] === 'string' ? rec['projectHash'] : null;
362
+ }
363
+ catch {
364
+ return null;
365
+ }
366
+ finally {
367
+ if (fd !== undefined)
368
+ fs.closeSync(fd);
369
+ }
370
+ }
371
+ /**
372
+ * Every `<root>/<project>/chats/*.json[l]`. The project is resolved when a
373
+ * known root hashes to the folder name (before 0.33.0) or to the session
374
+ * header's `projectHash` (0.33.0 on); otherwise it is kept as `gemini:<id>`.
375
+ */
376
+ export function listGeminiTranscriptFiles(root, ctx = {}) {
377
+ const byHash = new Map();
378
+ for (const p of ctx.projectPaths ?? [])
379
+ byHash.set(geminiProjectHash(p), p);
380
+ const out = [];
381
+ let hashes;
382
+ try {
383
+ hashes = fs.readdirSync(root, { withFileTypes: true });
384
+ }
385
+ catch {
386
+ return out;
387
+ }
388
+ for (const h of hashes) {
389
+ if (!h.isDirectory())
390
+ continue;
391
+ const chats = path.join(root, h.name, 'chats');
392
+ let files;
393
+ try {
394
+ files = fs.readdirSync(chats, { withFileTypes: true });
395
+ }
396
+ catch {
397
+ continue;
398
+ }
399
+ const byDir = byHash.get(h.name);
400
+ for (const f of files) {
401
+ if (!f.isFile() || !isSessionFile(f.name))
402
+ continue;
403
+ const filePath = path.join(chats, f.name);
404
+ let projectPath = byDir;
405
+ if (!projectPath && f.name.toLowerCase().endsWith('.jsonl')) {
406
+ const hash = headerProjectHash(filePath);
407
+ if (hash)
408
+ projectPath = byHash.get(hash);
409
+ }
410
+ out.push({ filePath, projectPath: projectPath ?? `${GEMINI_PROFILE.id}:${h.name}` });
411
+ }
412
+ }
413
+ return out;
414
+ }
415
+ /**
416
+ * Projects in Gemini CLI's registry (`~/.gemini/projects.json`, 0.33.0+:
417
+ * `{ projects: { <root>: <short id> } }`), with the session count and newest
418
+ * session under `tmp/<short id>/chats` (REQ-AGENTSWITCH-019.A1).
419
+ */
420
+ export function listGeminiProjects(home) {
421
+ const dir = path.join(home, GEMINI_PROFILE.configDir);
422
+ let projects;
423
+ try {
424
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'projects.json'), 'utf-8'));
425
+ if (!isRec(parsed) || !isRec(parsed['projects']))
426
+ return [];
427
+ projects = parsed['projects'];
428
+ }
429
+ catch {
430
+ return [];
431
+ }
432
+ const out = [];
433
+ for (const [root, id] of Object.entries(projects)) {
434
+ if (!path.isAbsolute(root))
435
+ continue;
436
+ let sessionCount = 0;
437
+ let lastModifiedMs = 0;
438
+ if (typeof id === 'string' && id && !/[\\/]/.test(id)) {
439
+ const chats = path.join(dir, 'tmp', id, 'chats');
440
+ try {
441
+ for (const f of fs.readdirSync(chats, { withFileTypes: true })) {
442
+ if (!f.isFile() || !isSessionFile(f.name))
443
+ continue;
444
+ sessionCount++;
445
+ try {
446
+ lastModifiedMs = Math.max(lastModifiedMs, fs.statSync(path.join(chats, f.name)).mtimeMs);
447
+ }
448
+ catch { /* gone */ }
449
+ }
450
+ }
451
+ catch { /* no sessions yet */ }
452
+ }
453
+ out.push({ path: root, sessionCount, lastModifiedMs });
454
+ }
455
+ return out;
456
+ }
457
+ export const geminiTranscriptAdapter = {
458
+ id: GEMINI_PROFILE.id,
459
+ format: 'document',
460
+ defaultRoot: (home) => path.join(home, GEMINI_PROFILE.configDir, 'tmp'),
461
+ listFiles: listGeminiTranscriptFiles,
462
+ listProjects: listGeminiProjects,
463
+ watches: (fileName) => {
464
+ const parts = fileName.split(/[\\/]/);
465
+ return parts.length >= 2 && parts[parts.length - 2] === 'chats' && isSessionFile(fileName);
466
+ },
467
+ normalizeToolName: normalizeGeminiToolName,
468
+ parseFile: parseGeminiSession,
469
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Transcript adapter registry (REQ-AGENTSWITCH-004) — one adapter per agent,
3
+ * Claude Code first (the default host). Mirrors `src/workflows/agents/index.ts`.
4
+ */
5
+ import { claudeTranscriptAdapter } from './claude.js';
6
+ import { geminiTranscriptAdapter } from './gemini.js';
7
+ const ADAPTERS = [claudeTranscriptAdapter, geminiTranscriptAdapter];
8
+ /** Every transcript adapter. */
9
+ export function transcriptAdapters() {
10
+ return ADAPTERS;
11
+ }
12
+ export { claudeTranscriptAdapter, decodeProjectSlug, listClaudeTranscriptFiles } from './claude.js';
13
+ export { geminiTranscriptAdapter, normalizeGeminiToolName, parseGeminiSession, geminiProjectHash, GEMINI_TOOL_NAMES } from './gemini.js';
14
+ export { normalizeToolName } from './types.js';
@@ -1,24 +1,8 @@
1
1
  /**
2
- * JSONL line parser — converts one line of a Claude Code transcript into a
3
- * structured ClaudeRawEntry, or null for unparseable / unrecognised lines.
4
- *
5
- * Tolerant: a malformed line never throws; the worker logs and skips it.
2
+ * Agent-neutral transcript helpers shared by every transcript adapter
3
+ * (REQ-AGENTSWITCH-017): timestamp normalisation and the one-line tool-input
4
+ * summary the heatmap, tips and reflect read.
6
5
  */
7
- export function parseLine(line) {
8
- const trimmed = line.trim();
9
- if (!trimmed)
10
- return null;
11
- try {
12
- const obj = JSON.parse(trimmed);
13
- if (!obj || typeof obj !== 'object' || typeof obj.type !== 'string') {
14
- return null;
15
- }
16
- return obj;
17
- }
18
- catch {
19
- return null;
20
- }
21
- }
22
6
  /** Convert a timestamp field (ISO string or epoch number) to epoch ms. */
23
7
  export function toEpochMs(ts) {
24
8
  if (ts == null)
@@ -30,24 +14,6 @@ export function toEpochMs(ts) {
30
14
  const parsed = Date.parse(ts);
31
15
  return Number.isFinite(parsed) ? parsed : Date.now();
32
16
  }
33
- /** Extract the user-visible prompt text from a user-entry message. */
34
- export function extractUserPrompt(entry) {
35
- // Some user entries store text at the top level; most use message.content.
36
- if (typeof entry.text === 'string' && entry.text.length > 0)
37
- return entry.text;
38
- const content = entry.message?.content;
39
- if (typeof content === 'string')
40
- return content;
41
- if (Array.isArray(content)) {
42
- return content
43
- .filter((b) => b && typeof b === 'object')
44
- .map((b) => (b.type === 'text' && typeof b.text === 'string' ? b.text : ''))
45
- .filter((t) => t.length > 0)
46
- .join('\n')
47
- .trim();
48
- }
49
- return '';
50
- }
51
17
  /**
52
18
  * For a tool_use block, build a short, format-aware input summary:
53
19
  * - Read/Edit/Write → file path
@@ -90,15 +56,3 @@ export function summarizeToolInput(name, input) {
90
56
  return '';
91
57
  }
92
58
  }
93
- /** Sum the character length of a tool_result content block. */
94
- export function toolResultLength(block) {
95
- if (!block)
96
- return 0;
97
- const c = block.content;
98
- if (typeof c === 'string')
99
- return c.length;
100
- if (Array.isArray(c)) {
101
- return c.reduce((sum, item) => sum + (typeof item?.text === 'string' ? item.text.length : 0), 0);
102
- }
103
- return 0;
104
- }
@@ -0,0 +1,4 @@
1
+ /** Look `raw` up in an adapter's name map; an unmapped name passes through. */
2
+ export function normalizeToolName(map, raw) {
3
+ return Object.prototype.hasOwnProperty.call(map, raw) ? map[raw] : raw;
4
+ }
@@ -1,23 +1,23 @@
1
1
  /**
2
- * Filesystem watcher that re-runs the ingestor on JSONL changes.
2
+ * Filesystem watcher that re-runs the ingestor on transcript changes, one
3
+ * watched root per transcript adapter (REQ-AGENTSWITCH-004).
3
4
  *
4
5
  * Strategy:
5
6
  * - One initial full pass on start (catches everything written while the
6
7
  * watcher wasn't running — same offset state, no duplicates).
7
- * - fs.watch on the ~/.claude/projects/ tree, debounced 300ms — Claude
8
- * Code writes one line at a time during a session, so we don't want to
9
- * re-ingest on every keystroke.
8
+ * - fs.watch on each adapter's transcript root, debounced 300ms — agents
9
+ * write a session incrementally, so we don't want to re-ingest on every
10
+ * keystroke.
10
11
  * - On change, re-run ingestAll. The state table makes this cheap: only
11
12
  * new bytes are read + parsed.
12
13
  */
13
14
  import * as fs from 'fs';
14
- import * as os from 'os';
15
- import * as path from 'path';
16
- import { ingestAll } from './ingestor.js';
15
+ import { ingestAll, resolveIngestRoots } from './ingestor.js';
17
16
  export function startWatcher(db, options = {}) {
18
- const claudeRoot = options.claudeRoot ?? path.join(os.homedir(), '.claude', 'projects');
19
17
  const debounceMs = options.debounceMs ?? 300;
20
18
  const verbose = options.verbose ?? false;
19
+ // One root per transcript adapter (REQ-AGENTSWITCH-004).
20
+ const targets = resolveIngestRoots(options);
21
21
  let pending = null;
22
22
  const triggerSoon = () => {
23
23
  if (pending)
@@ -25,7 +25,7 @@ export function startWatcher(db, options = {}) {
25
25
  pending = setTimeout(() => {
26
26
  pending = null;
27
27
  try {
28
- ingestAll(db, { ...options, claudeRoot });
28
+ ingestAll(db, options);
29
29
  }
30
30
  catch (err) {
31
31
  if (verbose) {
@@ -39,24 +39,25 @@ export function startWatcher(db, options = {}) {
39
39
  if (options.initialIngest !== false) {
40
40
  triggerSoon();
41
41
  }
42
- // fs.watch on the root, recursive. On macOS + Linux this gives us per-file
43
- // events; on Windows the recursive flag is supported as of recent Node.
44
- let watcher = null;
45
- try {
46
- watcher = fs.watch(claudeRoot, { recursive: true, persistent: true }, (eventType, filename) => {
47
- if (!filename)
48
- return;
49
- const f = filename.toString();
50
- if (!f.toLowerCase().endsWith('.jsonl'))
51
- return;
52
- triggerSoon();
53
- });
54
- }
55
- catch (err) {
56
- // Directory doesn't exist (user hasn't run Claude Code) — just no-op.
57
- if (verbose) {
58
- // eslint-disable-next-line no-console
59
- console.error('[ingest watcher] could not watch', claudeRoot, ':', err instanceof Error ? err.message : String(err));
42
+ // fs.watch each adapter's root, recursive. On macOS + Linux this gives us
43
+ // per-file events; on Windows the recursive flag is supported as of recent Node.
44
+ const watchers = [];
45
+ for (const { adapter, root } of targets) {
46
+ try {
47
+ watchers.push(fs.watch(root, { recursive: true, persistent: true }, (_eventType, filename) => {
48
+ if (!filename)
49
+ return;
50
+ if (!adapter.watches(filename.toString()))
51
+ return;
52
+ triggerSoon();
53
+ }));
54
+ }
55
+ catch (err) {
56
+ // Directory doesn't exist (the agent was never used here) — just no-op.
57
+ if (verbose) {
58
+ // eslint-disable-next-line no-console
59
+ console.error('[ingest watcher] could not watch', root, ':', err instanceof Error ? err.message : String(err));
60
+ }
60
61
  }
61
62
  }
62
63
  return {
@@ -65,13 +66,13 @@ export function startWatcher(db, options = {}) {
65
66
  clearTimeout(pending);
66
67
  pending = null;
67
68
  }
68
- if (watcher) {
69
+ for (const w of watchers) {
69
70
  try {
70
- watcher.close();
71
+ w.close();
71
72
  }
72
73
  catch { /* ignore */ }
73
74
  }
74
75
  },
75
- ingestNow: () => ingestAll(db, { ...options, claudeRoot }),
76
+ ingestNow: () => ingestAll(db, options),
76
77
  };
77
78
  }