@selvakumaresra/specship 0.1.3 → 0.4.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 (718) hide show
  1. package/README.md +2 -2
  2. package/commands/ss-design-implement.md +84 -0
  3. package/commands/ss-design-loop.md +125 -0
  4. package/commands/{cg-drifted.md → ss-drifted.md} +2 -2
  5. package/commands/{cg-fix.md → ss-fix.md} +1 -1
  6. package/commands/{cg-implement.md → ss-implement.md} +1 -1
  7. package/commands/ss-spec-author.md +43 -0
  8. package/commands/ss-spec-review.md +48 -0
  9. package/dist/bin/node-version-check.d.ts +37 -0
  10. package/dist/bin/node-version-check.d.ts.map +1 -0
  11. package/dist/bin/node-version-check.js +79 -0
  12. package/dist/bin/node-version-check.js.map +1 -0
  13. package/dist/bin/specship.d.ts +25 -0
  14. package/dist/bin/specship.d.ts.map +1 -0
  15. package/dist/bin/specship.js +2085 -0
  16. package/dist/bin/specship.js.map +1 -0
  17. package/dist/bin/uninstall.d.ts +13 -0
  18. package/dist/bin/uninstall.d.ts.map +1 -0
  19. package/dist/bin/uninstall.js +35 -0
  20. package/dist/bin/uninstall.js.map +1 -0
  21. package/dist/context/formatter.d.ts +30 -0
  22. package/dist/context/formatter.d.ts.map +1 -0
  23. package/dist/context/formatter.js +263 -0
  24. package/dist/context/formatter.js.map +1 -0
  25. package/dist/context/index.d.ts +119 -0
  26. package/dist/context/index.d.ts.map +1 -0
  27. package/dist/context/index.js +1289 -0
  28. package/dist/context/index.js.map +1 -0
  29. package/dist/context/markers.d.ts +19 -0
  30. package/dist/context/markers.d.ts.map +1 -0
  31. package/dist/context/markers.js +22 -0
  32. package/dist/context/markers.js.map +1 -0
  33. package/dist/db/index.d.ts +103 -0
  34. package/dist/db/index.d.ts.map +1 -0
  35. package/dist/db/index.js +279 -0
  36. package/dist/db/index.js.map +1 -0
  37. package/dist/db/migrations.d.ts +44 -0
  38. package/dist/db/migrations.d.ts.map +1 -0
  39. package/dist/db/migrations.js +462 -0
  40. package/dist/db/migrations.js.map +1 -0
  41. package/dist/db/queries.d.ts +357 -0
  42. package/dist/db/queries.d.ts.map +1 -0
  43. package/dist/db/queries.js +1504 -0
  44. package/dist/db/queries.js.map +1 -0
  45. package/dist/db/schema.sql +419 -0
  46. package/dist/db/spec-queries.d.ts +101 -0
  47. package/dist/db/spec-queries.d.ts.map +1 -0
  48. package/dist/db/spec-queries.js +675 -0
  49. package/dist/db/spec-queries.js.map +1 -0
  50. package/dist/db/sqlite-adapter.d.ts +65 -0
  51. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  52. package/dist/db/sqlite-adapter.js +214 -0
  53. package/dist/db/sqlite-adapter.js.map +1 -0
  54. package/dist/designer/artifact-store.js +54 -0
  55. package/dist/designer/browser.js +141 -0
  56. package/dist/designer/cdp-ensure.js +60 -0
  57. package/dist/designer/cdp-env.js +18 -0
  58. package/dist/designer/cdp-trace.js +599 -0
  59. package/dist/designer/cross-platform.js +74 -0
  60. package/dist/designer/designer-controller.js +1413 -0
  61. package/dist/designer/file-panel.js +39 -0
  62. package/dist/designer/interstitials.js +97 -0
  63. package/dist/designer/oopif-reader.js +176 -0
  64. package/dist/designer/package-meta.js +18 -0
  65. package/dist/designer/preview-host.js +50 -0
  66. package/dist/designer/repo-root.js +31 -0
  67. package/dist/designer/run-state.js +353 -0
  68. package/dist/designer/session-store.js +59 -0
  69. package/dist/designer/ui-anchors.js +651 -0
  70. package/dist/directory.d.ts +67 -0
  71. package/dist/directory.d.ts.map +1 -0
  72. package/dist/directory.js +267 -0
  73. package/dist/directory.js.map +1 -0
  74. package/dist/errors.d.ts +136 -0
  75. package/dist/errors.d.ts.map +1 -0
  76. package/dist/errors.js +219 -0
  77. package/dist/errors.js.map +1 -0
  78. package/dist/extraction/dfm-extractor.d.ts +31 -0
  79. package/dist/extraction/dfm-extractor.d.ts.map +1 -0
  80. package/dist/extraction/dfm-extractor.js +151 -0
  81. package/dist/extraction/dfm-extractor.js.map +1 -0
  82. package/dist/extraction/generated-detection.d.ts +30 -0
  83. package/dist/extraction/generated-detection.d.ts.map +1 -0
  84. package/dist/extraction/generated-detection.js +80 -0
  85. package/dist/extraction/generated-detection.js.map +1 -0
  86. package/dist/extraction/grammars.d.ts +100 -0
  87. package/dist/extraction/grammars.d.ts.map +1 -0
  88. package/dist/extraction/grammars.js +426 -0
  89. package/dist/extraction/grammars.js.map +1 -0
  90. package/dist/extraction/index.d.ts +138 -0
  91. package/dist/extraction/index.d.ts.map +1 -0
  92. package/dist/extraction/index.js +1394 -0
  93. package/dist/extraction/index.js.map +1 -0
  94. package/dist/extraction/languages/c-cpp.d.ts +4 -0
  95. package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
  96. package/dist/extraction/languages/c-cpp.js +171 -0
  97. package/dist/extraction/languages/c-cpp.js.map +1 -0
  98. package/dist/extraction/languages/csharp.d.ts +3 -0
  99. package/dist/extraction/languages/csharp.d.ts.map +1 -0
  100. package/dist/extraction/languages/csharp.js +73 -0
  101. package/dist/extraction/languages/csharp.js.map +1 -0
  102. package/dist/extraction/languages/dart.d.ts +3 -0
  103. package/dist/extraction/languages/dart.d.ts.map +1 -0
  104. package/dist/extraction/languages/dart.js +192 -0
  105. package/dist/extraction/languages/dart.js.map +1 -0
  106. package/dist/extraction/languages/go.d.ts +3 -0
  107. package/dist/extraction/languages/go.d.ts.map +1 -0
  108. package/dist/extraction/languages/go.js +74 -0
  109. package/dist/extraction/languages/go.js.map +1 -0
  110. package/dist/extraction/languages/index.d.ts +10 -0
  111. package/dist/extraction/languages/index.d.ts.map +1 -0
  112. package/dist/extraction/languages/index.js +51 -0
  113. package/dist/extraction/languages/index.js.map +1 -0
  114. package/dist/extraction/languages/java.d.ts +3 -0
  115. package/dist/extraction/languages/java.d.ts.map +1 -0
  116. package/dist/extraction/languages/java.js +70 -0
  117. package/dist/extraction/languages/java.js.map +1 -0
  118. package/dist/extraction/languages/javascript.d.ts +3 -0
  119. package/dist/extraction/languages/javascript.d.ts.map +1 -0
  120. package/dist/extraction/languages/javascript.js +90 -0
  121. package/dist/extraction/languages/javascript.js.map +1 -0
  122. package/dist/extraction/languages/kotlin.d.ts +3 -0
  123. package/dist/extraction/languages/kotlin.d.ts.map +1 -0
  124. package/dist/extraction/languages/kotlin.js +259 -0
  125. package/dist/extraction/languages/kotlin.js.map +1 -0
  126. package/dist/extraction/languages/lua.d.ts +3 -0
  127. package/dist/extraction/languages/lua.d.ts.map +1 -0
  128. package/dist/extraction/languages/lua.js +150 -0
  129. package/dist/extraction/languages/lua.js.map +1 -0
  130. package/dist/extraction/languages/luau.d.ts +3 -0
  131. package/dist/extraction/languages/luau.d.ts.map +1 -0
  132. package/dist/extraction/languages/luau.js +37 -0
  133. package/dist/extraction/languages/luau.js.map +1 -0
  134. package/dist/extraction/languages/objc.d.ts +3 -0
  135. package/dist/extraction/languages/objc.d.ts.map +1 -0
  136. package/dist/extraction/languages/objc.js +133 -0
  137. package/dist/extraction/languages/objc.js.map +1 -0
  138. package/dist/extraction/languages/pascal.d.ts +3 -0
  139. package/dist/extraction/languages/pascal.d.ts.map +1 -0
  140. package/dist/extraction/languages/pascal.js +66 -0
  141. package/dist/extraction/languages/pascal.js.map +1 -0
  142. package/dist/extraction/languages/php.d.ts +3 -0
  143. package/dist/extraction/languages/php.d.ts.map +1 -0
  144. package/dist/extraction/languages/php.js +107 -0
  145. package/dist/extraction/languages/php.js.map +1 -0
  146. package/dist/extraction/languages/python.d.ts +3 -0
  147. package/dist/extraction/languages/python.d.ts.map +1 -0
  148. package/dist/extraction/languages/python.js +56 -0
  149. package/dist/extraction/languages/python.js.map +1 -0
  150. package/dist/extraction/languages/ruby.d.ts +3 -0
  151. package/dist/extraction/languages/ruby.d.ts.map +1 -0
  152. package/dist/extraction/languages/ruby.js +114 -0
  153. package/dist/extraction/languages/ruby.js.map +1 -0
  154. package/dist/extraction/languages/rust.d.ts +3 -0
  155. package/dist/extraction/languages/rust.d.ts.map +1 -0
  156. package/dist/extraction/languages/rust.js +109 -0
  157. package/dist/extraction/languages/rust.js.map +1 -0
  158. package/dist/extraction/languages/scala.d.ts +3 -0
  159. package/dist/extraction/languages/scala.d.ts.map +1 -0
  160. package/dist/extraction/languages/scala.js +139 -0
  161. package/dist/extraction/languages/scala.js.map +1 -0
  162. package/dist/extraction/languages/swift.d.ts +3 -0
  163. package/dist/extraction/languages/swift.d.ts.map +1 -0
  164. package/dist/extraction/languages/swift.js +91 -0
  165. package/dist/extraction/languages/swift.js.map +1 -0
  166. package/dist/extraction/languages/typescript.d.ts +3 -0
  167. package/dist/extraction/languages/typescript.d.ts.map +1 -0
  168. package/dist/extraction/languages/typescript.js +129 -0
  169. package/dist/extraction/languages/typescript.js.map +1 -0
  170. package/dist/extraction/liquid-extractor.d.ts +52 -0
  171. package/dist/extraction/liquid-extractor.d.ts.map +1 -0
  172. package/dist/extraction/liquid-extractor.js +313 -0
  173. package/dist/extraction/liquid-extractor.js.map +1 -0
  174. package/dist/extraction/mybatis-extractor.d.ts +48 -0
  175. package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
  176. package/dist/extraction/mybatis-extractor.js +198 -0
  177. package/dist/extraction/mybatis-extractor.js.map +1 -0
  178. package/dist/extraction/parse-worker.d.ts +8 -0
  179. package/dist/extraction/parse-worker.d.ts.map +1 -0
  180. package/dist/extraction/parse-worker.js +94 -0
  181. package/dist/extraction/parse-worker.js.map +1 -0
  182. package/dist/extraction/specs/markdown-spec-extractor.d.ts +59 -0
  183. package/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -0
  184. package/dist/extraction/specs/markdown-spec-extractor.js +327 -0
  185. package/dist/extraction/specs/markdown-spec-extractor.js.map +1 -0
  186. package/dist/extraction/specs/types.d.ts +39 -0
  187. package/dist/extraction/specs/types.d.ts.map +1 -0
  188. package/dist/extraction/specs/types.js +8 -0
  189. package/dist/extraction/specs/types.js.map +1 -0
  190. package/dist/extraction/svelte-extractor.d.ts +56 -0
  191. package/dist/extraction/svelte-extractor.d.ts.map +1 -0
  192. package/dist/extraction/svelte-extractor.js +272 -0
  193. package/dist/extraction/svelte-extractor.js.map +1 -0
  194. package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
  195. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
  196. package/dist/extraction/tree-sitter-helpers.js +103 -0
  197. package/dist/extraction/tree-sitter-helpers.js.map +1 -0
  198. package/dist/extraction/tree-sitter-types.d.ts +193 -0
  199. package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
  200. package/dist/extraction/tree-sitter-types.js +10 -0
  201. package/dist/extraction/tree-sitter-types.js.map +1 -0
  202. package/dist/extraction/tree-sitter.d.ts +317 -0
  203. package/dist/extraction/tree-sitter.d.ts.map +1 -0
  204. package/dist/extraction/tree-sitter.js +3092 -0
  205. package/dist/extraction/tree-sitter.js.map +1 -0
  206. package/dist/extraction/vue-extractor.d.ts +51 -0
  207. package/dist/extraction/vue-extractor.d.ts.map +1 -0
  208. package/dist/extraction/vue-extractor.js +251 -0
  209. package/dist/extraction/vue-extractor.js.map +1 -0
  210. package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
  211. package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
  212. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  213. package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  214. package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
  215. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
  216. package/dist/extraction/wasm-runtime-flags.js +106 -0
  217. package/dist/extraction/wasm-runtime-flags.js.map +1 -0
  218. package/dist/graph/index.d.ts +8 -0
  219. package/dist/graph/index.d.ts.map +1 -0
  220. package/dist/graph/index.js +13 -0
  221. package/dist/graph/index.js.map +1 -0
  222. package/dist/graph/queries.d.ts +106 -0
  223. package/dist/graph/queries.d.ts.map +1 -0
  224. package/dist/graph/queries.js +366 -0
  225. package/dist/graph/queries.js.map +1 -0
  226. package/dist/graph/traversal.d.ts +127 -0
  227. package/dist/graph/traversal.d.ts.map +1 -0
  228. package/dist/graph/traversal.js +531 -0
  229. package/dist/graph/traversal.js.map +1 -0
  230. package/dist/index.d.ts +551 -0
  231. package/dist/index.d.ts.map +1 -0
  232. package/dist/index.js +1165 -0
  233. package/dist/index.js.map +1 -0
  234. package/dist/installer/config-writer.d.ts +28 -0
  235. package/dist/installer/config-writer.d.ts.map +1 -0
  236. package/dist/installer/config-writer.js +91 -0
  237. package/dist/installer/config-writer.js.map +1 -0
  238. package/dist/installer/index.d.ts +92 -0
  239. package/dist/installer/index.d.ts.map +1 -0
  240. package/dist/installer/index.js +416 -0
  241. package/dist/installer/index.js.map +1 -0
  242. package/dist/installer/instructions-template.d.ts +35 -0
  243. package/dist/installer/instructions-template.d.ts.map +1 -0
  244. package/dist/installer/instructions-template.js +51 -0
  245. package/dist/installer/instructions-template.js.map +1 -0
  246. package/dist/installer/targets/claude.d.ts +117 -0
  247. package/dist/installer/targets/claude.d.ts.map +1 -0
  248. package/dist/installer/targets/claude.js +736 -0
  249. package/dist/installer/targets/claude.js.map +1 -0
  250. package/dist/installer/targets/registry.d.ts +19 -0
  251. package/dist/installer/targets/registry.d.ts.map +1 -0
  252. package/dist/installer/targets/registry.js +31 -0
  253. package/dist/installer/targets/registry.js.map +1 -0
  254. package/dist/installer/targets/shared.d.ts +76 -0
  255. package/dist/installer/targets/shared.d.ts.map +1 -0
  256. package/dist/installer/targets/shared.js +256 -0
  257. package/dist/installer/targets/shared.js.map +1 -0
  258. package/dist/installer/targets/types.d.ts +84 -0
  259. package/dist/installer/targets/types.d.ts.map +1 -0
  260. package/dist/installer/targets/types.js +12 -0
  261. package/dist/installer/targets/types.js.map +1 -0
  262. package/dist/isolation/worktree.d.ts +65 -0
  263. package/dist/isolation/worktree.d.ts.map +1 -0
  264. package/dist/isolation/worktree.js +231 -0
  265. package/dist/isolation/worktree.js.map +1 -0
  266. package/dist/mcp/daemon-paths.d.ts +46 -0
  267. package/dist/mcp/daemon-paths.d.ts.map +1 -0
  268. package/dist/mcp/daemon-paths.js +125 -0
  269. package/dist/mcp/daemon-paths.js.map +1 -0
  270. package/dist/mcp/daemon.d.ts +161 -0
  271. package/dist/mcp/daemon.d.ts.map +1 -0
  272. package/dist/mcp/daemon.js +403 -0
  273. package/dist/mcp/daemon.js.map +1 -0
  274. package/dist/mcp/designer-tools.d.ts +33 -0
  275. package/dist/mcp/designer-tools.d.ts.map +1 -0
  276. package/dist/mcp/designer-tools.js +313 -0
  277. package/dist/mcp/designer-tools.js.map +1 -0
  278. package/dist/mcp/engine.d.ts +105 -0
  279. package/dist/mcp/engine.d.ts.map +1 -0
  280. package/dist/mcp/engine.js +270 -0
  281. package/dist/mcp/engine.js.map +1 -0
  282. package/dist/mcp/index.d.ts +112 -0
  283. package/dist/mcp/index.d.ts.map +1 -0
  284. package/dist/mcp/index.js +477 -0
  285. package/dist/mcp/index.js.map +1 -0
  286. package/dist/mcp/proxy.d.ts +81 -0
  287. package/dist/mcp/proxy.d.ts.map +1 -0
  288. package/dist/mcp/proxy.js +510 -0
  289. package/dist/mcp/proxy.js.map +1 -0
  290. package/dist/mcp/server-instructions.d.ts +18 -0
  291. package/dist/mcp/server-instructions.d.ts.map +1 -0
  292. package/dist/mcp/server-instructions.js +77 -0
  293. package/dist/mcp/server-instructions.js.map +1 -0
  294. package/dist/mcp/session.d.ts +77 -0
  295. package/dist/mcp/session.d.ts.map +1 -0
  296. package/dist/mcp/session.js +294 -0
  297. package/dist/mcp/session.js.map +1 -0
  298. package/dist/mcp/spec-tools.d.ts +39 -0
  299. package/dist/mcp/spec-tools.d.ts.map +1 -0
  300. package/dist/mcp/spec-tools.js +326 -0
  301. package/dist/mcp/spec-tools.js.map +1 -0
  302. package/dist/mcp/tools.d.ts +404 -0
  303. package/dist/mcp/tools.d.ts.map +1 -0
  304. package/dist/mcp/tools.js +3087 -0
  305. package/dist/mcp/tools.js.map +1 -0
  306. package/dist/mcp/transport.d.ts +188 -0
  307. package/dist/mcp/transport.d.ts.map +1 -0
  308. package/dist/mcp/transport.js +343 -0
  309. package/dist/mcp/transport.js.map +1 -0
  310. package/dist/mcp/version.d.ts +19 -0
  311. package/dist/mcp/version.d.ts.map +1 -0
  312. package/dist/mcp/version.js +71 -0
  313. package/dist/mcp/version.js.map +1 -0
  314. package/dist/resolution/callback-synthesizer.d.ts +10 -0
  315. package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
  316. package/dist/resolution/callback-synthesizer.js +1300 -0
  317. package/dist/resolution/callback-synthesizer.js.map +1 -0
  318. package/dist/resolution/frameworks/cargo-workspace.d.ts +18 -0
  319. package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -0
  320. package/dist/resolution/frameworks/cargo-workspace.js +225 -0
  321. package/dist/resolution/frameworks/cargo-workspace.js.map +1 -0
  322. package/dist/resolution/frameworks/csharp.d.ts +8 -0
  323. package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
  324. package/dist/resolution/frameworks/csharp.js +241 -0
  325. package/dist/resolution/frameworks/csharp.js.map +1 -0
  326. package/dist/resolution/frameworks/drupal.d.ts +51 -0
  327. package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
  328. package/dist/resolution/frameworks/drupal.js +367 -0
  329. package/dist/resolution/frameworks/drupal.js.map +1 -0
  330. package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
  331. package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
  332. package/dist/resolution/frameworks/expo-modules.js +143 -0
  333. package/dist/resolution/frameworks/expo-modules.js.map +1 -0
  334. package/dist/resolution/frameworks/express.d.ts +8 -0
  335. package/dist/resolution/frameworks/express.d.ts.map +1 -0
  336. package/dist/resolution/frameworks/express.js +308 -0
  337. package/dist/resolution/frameworks/express.js.map +1 -0
  338. package/dist/resolution/frameworks/fabric.d.ts +3 -0
  339. package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
  340. package/dist/resolution/frameworks/fabric.js +354 -0
  341. package/dist/resolution/frameworks/fabric.js.map +1 -0
  342. package/dist/resolution/frameworks/go.d.ts +8 -0
  343. package/dist/resolution/frameworks/go.d.ts.map +1 -0
  344. package/dist/resolution/frameworks/go.js +161 -0
  345. package/dist/resolution/frameworks/go.js.map +1 -0
  346. package/dist/resolution/frameworks/index.d.ts +48 -0
  347. package/dist/resolution/frameworks/index.d.ts.map +1 -0
  348. package/dist/resolution/frameworks/index.js +161 -0
  349. package/dist/resolution/frameworks/index.js.map +1 -0
  350. package/dist/resolution/frameworks/java.d.ts +8 -0
  351. package/dist/resolution/frameworks/java.d.ts.map +1 -0
  352. package/dist/resolution/frameworks/java.js +504 -0
  353. package/dist/resolution/frameworks/java.js.map +1 -0
  354. package/dist/resolution/frameworks/laravel.d.ts +13 -0
  355. package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
  356. package/dist/resolution/frameworks/laravel.js +257 -0
  357. package/dist/resolution/frameworks/laravel.js.map +1 -0
  358. package/dist/resolution/frameworks/nestjs.d.ts +26 -0
  359. package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
  360. package/dist/resolution/frameworks/nestjs.js +698 -0
  361. package/dist/resolution/frameworks/nestjs.js.map +1 -0
  362. package/dist/resolution/frameworks/play.d.ts +19 -0
  363. package/dist/resolution/frameworks/play.d.ts.map +1 -0
  364. package/dist/resolution/frameworks/play.js +111 -0
  365. package/dist/resolution/frameworks/play.js.map +1 -0
  366. package/dist/resolution/frameworks/python.d.ts +10 -0
  367. package/dist/resolution/frameworks/python.d.ts.map +1 -0
  368. package/dist/resolution/frameworks/python.js +396 -0
  369. package/dist/resolution/frameworks/python.js.map +1 -0
  370. package/dist/resolution/frameworks/react-native.d.ts +3 -0
  371. package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
  372. package/dist/resolution/frameworks/react-native.js +360 -0
  373. package/dist/resolution/frameworks/react-native.js.map +1 -0
  374. package/dist/resolution/frameworks/react.d.ts +8 -0
  375. package/dist/resolution/frameworks/react.d.ts.map +1 -0
  376. package/dist/resolution/frameworks/react.js +365 -0
  377. package/dist/resolution/frameworks/react.js.map +1 -0
  378. package/dist/resolution/frameworks/ruby.d.ts +8 -0
  379. package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
  380. package/dist/resolution/frameworks/ruby.js +302 -0
  381. package/dist/resolution/frameworks/ruby.js.map +1 -0
  382. package/dist/resolution/frameworks/rust.d.ts +8 -0
  383. package/dist/resolution/frameworks/rust.d.ts.map +1 -0
  384. package/dist/resolution/frameworks/rust.js +304 -0
  385. package/dist/resolution/frameworks/rust.js.map +1 -0
  386. package/dist/resolution/frameworks/svelte.d.ts +9 -0
  387. package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
  388. package/dist/resolution/frameworks/svelte.js +249 -0
  389. package/dist/resolution/frameworks/svelte.js.map +1 -0
  390. package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
  391. package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
  392. package/dist/resolution/frameworks/swift-objc.js +252 -0
  393. package/dist/resolution/frameworks/swift-objc.js.map +1 -0
  394. package/dist/resolution/frameworks/swift.d.ts +10 -0
  395. package/dist/resolution/frameworks/swift.d.ts.map +1 -0
  396. package/dist/resolution/frameworks/swift.js +400 -0
  397. package/dist/resolution/frameworks/swift.js.map +1 -0
  398. package/dist/resolution/frameworks/vue.d.ts +9 -0
  399. package/dist/resolution/frameworks/vue.d.ts.map +1 -0
  400. package/dist/resolution/frameworks/vue.js +306 -0
  401. package/dist/resolution/frameworks/vue.js.map +1 -0
  402. package/dist/resolution/go-module.d.ts +26 -0
  403. package/dist/resolution/go-module.d.ts.map +1 -0
  404. package/dist/resolution/go-module.js +78 -0
  405. package/dist/resolution/go-module.js.map +1 -0
  406. package/dist/resolution/import-resolver.d.ts +68 -0
  407. package/dist/resolution/import-resolver.d.ts.map +1 -0
  408. package/dist/resolution/import-resolver.js +1275 -0
  409. package/dist/resolution/import-resolver.js.map +1 -0
  410. package/dist/resolution/index.d.ts +117 -0
  411. package/dist/resolution/index.d.ts.map +1 -0
  412. package/dist/resolution/index.js +895 -0
  413. package/dist/resolution/index.js.map +1 -0
  414. package/dist/resolution/lru-cache.d.ts +24 -0
  415. package/dist/resolution/lru-cache.d.ts.map +1 -0
  416. package/dist/resolution/lru-cache.js +62 -0
  417. package/dist/resolution/lru-cache.js.map +1 -0
  418. package/dist/resolution/name-matcher.d.ts +32 -0
  419. package/dist/resolution/name-matcher.d.ts.map +1 -0
  420. package/dist/resolution/name-matcher.js +596 -0
  421. package/dist/resolution/name-matcher.js.map +1 -0
  422. package/dist/resolution/path-aliases.d.ts +68 -0
  423. package/dist/resolution/path-aliases.d.ts.map +1 -0
  424. package/dist/resolution/path-aliases.js +238 -0
  425. package/dist/resolution/path-aliases.js.map +1 -0
  426. package/dist/resolution/spec-link-resolver.d.ts +103 -0
  427. package/dist/resolution/spec-link-resolver.d.ts.map +1 -0
  428. package/dist/resolution/spec-link-resolver.js +259 -0
  429. package/dist/resolution/spec-link-resolver.js.map +1 -0
  430. package/dist/resolution/strip-comments.d.ts +27 -0
  431. package/dist/resolution/strip-comments.d.ts.map +1 -0
  432. package/dist/resolution/strip-comments.js +441 -0
  433. package/dist/resolution/strip-comments.js.map +1 -0
  434. package/dist/resolution/swift-objc-bridge.d.ts +134 -0
  435. package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
  436. package/dist/resolution/swift-objc-bridge.js +256 -0
  437. package/dist/resolution/swift-objc-bridge.js.map +1 -0
  438. package/dist/resolution/types.d.ts +216 -0
  439. package/dist/resolution/types.d.ts.map +1 -0
  440. package/dist/resolution/types.js +8 -0
  441. package/dist/resolution/types.js.map +1 -0
  442. package/dist/resolution/workspace-packages.d.ts +48 -0
  443. package/dist/resolution/workspace-packages.d.ts.map +1 -0
  444. package/dist/resolution/workspace-packages.js +208 -0
  445. package/dist/resolution/workspace-packages.js.map +1 -0
  446. package/dist/search/query-parser.d.ts +57 -0
  447. package/dist/search/query-parser.d.ts.map +1 -0
  448. package/dist/search/query-parser.js +177 -0
  449. package/dist/search/query-parser.js.map +1 -0
  450. package/dist/search/query-utils.d.ts +71 -0
  451. package/dist/search/query-utils.d.ts.map +1 -0
  452. package/dist/search/query-utils.js +380 -0
  453. package/dist/search/query-utils.js.map +1 -0
  454. package/dist/server/cli.js +152 -0
  455. package/dist/server/index.js +12 -0
  456. package/dist/server/ingest/index.js +18 -0
  457. package/dist/server/ingest/ingestor.js +506 -0
  458. package/dist/server/ingest/parser.js +104 -0
  459. package/dist/server/ingest/pricing.js +78 -0
  460. package/dist/server/ingest/types.js +9 -0
  461. package/dist/server/ingest/watcher.js +77 -0
  462. package/dist/server/package.json +3 -0
  463. package/dist/server/project-registry.js +101 -0
  464. package/dist/server/routes/claude.js +868 -0
  465. package/dist/server/routes/graph.js +211 -0
  466. package/dist/server/routes/memory.js +272 -0
  467. package/dist/server/routes/projects.js +197 -0
  468. package/dist/server/routes/spec.js +265 -0
  469. package/dist/server/routes/status.js +112 -0
  470. package/dist/server/routes/workflow.js +212 -0
  471. package/dist/server/server.js +206 -0
  472. package/dist/server/static-handler.js +87 -0
  473. package/dist/sync/git-hooks.d.ts +45 -0
  474. package/dist/sync/git-hooks.d.ts.map +1 -0
  475. package/dist/sync/git-hooks.js +225 -0
  476. package/dist/sync/git-hooks.js.map +1 -0
  477. package/dist/sync/index.d.ts +19 -0
  478. package/dist/sync/index.d.ts.map +1 -0
  479. package/dist/sync/index.js +35 -0
  480. package/dist/sync/index.js.map +1 -0
  481. package/dist/sync/watch-policy.d.ts +48 -0
  482. package/dist/sync/watch-policy.d.ts.map +1 -0
  483. package/dist/sync/watch-policy.js +124 -0
  484. package/dist/sync/watch-policy.js.map +1 -0
  485. package/dist/sync/watcher.d.ts +283 -0
  486. package/dist/sync/watcher.d.ts.map +1 -0
  487. package/dist/sync/watcher.js +606 -0
  488. package/dist/sync/watcher.js.map +1 -0
  489. package/dist/sync/worktree.d.ts +54 -0
  490. package/dist/sync/worktree.d.ts.map +1 -0
  491. package/dist/sync/worktree.js +137 -0
  492. package/dist/sync/worktree.js.map +1 -0
  493. package/dist/types.d.ts +623 -0
  494. package/dist/types.d.ts.map +1 -0
  495. package/dist/types.js +108 -0
  496. package/dist/types.js.map +1 -0
  497. package/dist/ui/glyphs.d.ts +42 -0
  498. package/dist/ui/glyphs.d.ts.map +1 -0
  499. package/dist/ui/glyphs.js +78 -0
  500. package/dist/ui/glyphs.js.map +1 -0
  501. package/dist/ui/shimmer-progress.d.ts +11 -0
  502. package/dist/ui/shimmer-progress.d.ts.map +1 -0
  503. package/dist/ui/shimmer-progress.js +90 -0
  504. package/dist/ui/shimmer-progress.js.map +1 -0
  505. package/dist/ui/shimmer-worker.d.ts +2 -0
  506. package/dist/ui/shimmer-worker.d.ts.map +1 -0
  507. package/dist/ui/shimmer-worker.js +118 -0
  508. package/dist/ui/shimmer-worker.js.map +1 -0
  509. package/dist/ui/types.d.ts +17 -0
  510. package/dist/ui/types.d.ts.map +1 -0
  511. package/dist/ui/types.js +3 -0
  512. package/dist/ui/types.js.map +1 -0
  513. package/dist/utils.d.ts +205 -0
  514. package/dist/utils.d.ts.map +1 -0
  515. package/dist/utils.js +549 -0
  516. package/dist/utils.js.map +1 -0
  517. package/dist/web/chunk-2AJCHB7P.js +1 -0
  518. package/dist/web/chunk-2CPLUFCH.js +2 -0
  519. package/dist/web/chunk-2GBEK2GM.js +1 -0
  520. package/dist/web/chunk-2I7L37NS.js +1 -0
  521. package/dist/web/chunk-2NAWAJB5.js +1 -0
  522. package/dist/web/chunk-2OJBIPE4.js +1 -0
  523. package/dist/web/chunk-2YUJNZ2Y.js +6 -0
  524. package/dist/web/chunk-3E2WB6D5.js +1 -0
  525. package/dist/web/chunk-3EBFYSCH.js +2 -0
  526. package/dist/web/chunk-3QCQ4BXS.js +1 -0
  527. package/dist/web/chunk-42XVAQ6I.js +1 -0
  528. package/dist/web/chunk-45QHGCB4.js +17 -0
  529. package/dist/web/chunk-4IMMPEYM.js +1 -0
  530. package/dist/web/chunk-4TJQJPCZ.js +1 -0
  531. package/dist/web/chunk-4WZIHTPC.js +1 -0
  532. package/dist/web/chunk-4YVSYOSD.js +1 -0
  533. package/dist/web/chunk-5BQIOYKW.js +1 -0
  534. package/dist/web/chunk-5HGWHUJA.js +1 -0
  535. package/dist/web/chunk-5Y244R4G.js +1 -0
  536. package/dist/web/chunk-6RRDPT5Z.js +1 -0
  537. package/dist/web/chunk-6VKB2ZWM.js +1 -0
  538. package/dist/web/chunk-7DMFVTU4.js +1 -0
  539. package/dist/web/chunk-7RNS77UP.js +1 -0
  540. package/dist/web/chunk-7SMPKVEP.js +1 -0
  541. package/dist/web/chunk-A5R3MJMO.js +1 -0
  542. package/dist/web/chunk-ASZ77FMZ.js +1 -0
  543. package/dist/web/chunk-AZJVTPLU.js +1 -0
  544. package/dist/web/chunk-B3YPFY6A.js +1 -0
  545. package/dist/web/chunk-BLBRMCN2.js +1 -0
  546. package/dist/web/chunk-BMIAXD2V.js +2 -0
  547. package/dist/web/chunk-BUXWEHIY.js +1 -0
  548. package/dist/web/chunk-BYZFQSM6.js +1 -0
  549. package/dist/web/chunk-D5OCNEJA.js +2 -0
  550. package/dist/web/chunk-DLQPZWSI.css +1 -0
  551. package/dist/web/chunk-DTRN7FZR.js +1 -0
  552. package/dist/web/chunk-DYRFLPJA.js +1 -0
  553. package/dist/web/chunk-E3J3CXR5.js +1 -0
  554. package/dist/web/chunk-E44X4RH2.js +1 -0
  555. package/dist/web/chunk-E73OX2P7.js +1 -0
  556. package/dist/web/chunk-EAXRKDLV.js +1 -0
  557. package/dist/web/chunk-EBKKDHYI.js +1 -0
  558. package/dist/web/chunk-EE7V7Q5P.js +1 -0
  559. package/dist/web/chunk-EKY2FUHU.js +1 -0
  560. package/dist/web/chunk-EMGMOEVR.js +1 -0
  561. package/dist/web/chunk-EP6XOPXH.js +1 -0
  562. package/dist/web/chunk-ESGDLJOJ.js +1 -0
  563. package/dist/web/chunk-ETJG7NCY.js +1 -0
  564. package/dist/web/chunk-EUUEFEDI.js +1 -0
  565. package/dist/web/chunk-FGNZDHTL.js +11 -0
  566. package/dist/web/chunk-FHZHD2ZG.js +1 -0
  567. package/dist/web/chunk-FIJW2UNJ.js +1 -0
  568. package/dist/web/chunk-FMV5PXRC.js +5 -0
  569. package/dist/web/chunk-G7VZT5KB.js +3 -0
  570. package/dist/web/chunk-GR72OOCN.js +1 -0
  571. package/dist/web/chunk-GRZYXPSO.js +7 -0
  572. package/dist/web/chunk-GWPVKJIY.js +1 -0
  573. package/dist/web/chunk-GYGPS3AN.js +1 -0
  574. package/dist/web/chunk-H7AF7YS4.js +1 -0
  575. package/dist/web/chunk-HDZDQILN.js +1 -0
  576. package/dist/web/chunk-HMK6UO6N.js +1 -0
  577. package/dist/web/chunk-HZA6NEAB.js +1 -0
  578. package/dist/web/chunk-IHEE5NYJ.js +1 -0
  579. package/dist/web/chunk-ISNEBICW.js +1 -0
  580. package/dist/web/chunk-J2GZVLHH.js +1 -0
  581. package/dist/web/chunk-JTFXTIPE.js +903 -0
  582. package/dist/web/chunk-L37MTFSG.js +3 -0
  583. package/dist/web/chunk-LB6JPLX2.js +1 -0
  584. package/dist/web/chunk-LNSVDHCI.js +1 -0
  585. package/dist/web/chunk-LVGIY3SO.js +1 -0
  586. package/dist/web/chunk-LXLHIHEN.js +1 -0
  587. package/dist/web/chunk-MC4DFIHG.js +1 -0
  588. package/dist/web/chunk-MVOMVPYB.js +1 -0
  589. package/dist/web/chunk-N6SS4G6S.js +1 -0
  590. package/dist/web/chunk-NTBJG6SJ.js +1 -0
  591. package/dist/web/chunk-NUDB3Q2Y.js +3 -0
  592. package/dist/web/chunk-NZEZCT65.js +1 -0
  593. package/dist/web/chunk-OXEF5E3E.js +1 -0
  594. package/dist/web/chunk-PDN6QYGJ.js +4 -0
  595. package/dist/web/chunk-PGGJPDJG.js +1 -0
  596. package/dist/web/chunk-PUYSJNJR.js +1 -0
  597. package/dist/web/chunk-Q2RVFS45.js +1 -0
  598. package/dist/web/chunk-Q7L6LLAK.js +1 -0
  599. package/dist/web/chunk-QCMKJIWY.js +1 -0
  600. package/dist/web/chunk-QH6CF3M3.js +1 -0
  601. package/dist/web/chunk-QQ5LD7PI.js +1 -0
  602. package/dist/web/chunk-QR6L3KAC.js +1 -0
  603. package/dist/web/chunk-R2DLK4HO.js +1 -0
  604. package/dist/web/chunk-R5W2MDZN.js +1 -0
  605. package/dist/web/chunk-RD6TVPOT.js +1 -0
  606. package/dist/web/chunk-RKY4EJYJ.js +1 -0
  607. package/dist/web/chunk-RONYWVY7.js +1 -0
  608. package/dist/web/chunk-RXKXYF2C.js +1 -0
  609. package/dist/web/chunk-SBWU7JFC.js +1 -0
  610. package/dist/web/chunk-SEXBRGYK.js +1 -0
  611. package/dist/web/chunk-SHPTC4RL.js +1 -0
  612. package/dist/web/chunk-SUZYBYDW.js +1 -0
  613. package/dist/web/chunk-SWKJRNYY.js +1 -0
  614. package/dist/web/chunk-T66XVKGB.js +1 -0
  615. package/dist/web/chunk-T7AZ65JP.js +1 -0
  616. package/dist/web/chunk-TCZDVOHD.js +1 -0
  617. package/dist/web/chunk-TPTITA3V.js +1 -0
  618. package/dist/web/chunk-TR335633.js +1 -0
  619. package/dist/web/chunk-UBOZGQNK.js +1 -0
  620. package/dist/web/chunk-UR5KDXPX.js +1 -0
  621. package/dist/web/chunk-UR6O2GEH.js +1 -0
  622. package/dist/web/chunk-UTNMGWTP.js +1 -0
  623. package/dist/web/chunk-UYC52MBC.js +1 -0
  624. package/dist/web/chunk-VECWMHJP.js +1 -0
  625. package/dist/web/chunk-VUACT35R.js +3 -0
  626. package/dist/web/chunk-VZI7H4SZ.js +1 -0
  627. package/dist/web/chunk-WAI2JMZP.js +1 -0
  628. package/dist/web/chunk-WB6YHOD4.js +1 -0
  629. package/dist/web/chunk-WBT64AWV.js +1 -0
  630. package/dist/web/chunk-WCKHQIYN.js +1 -0
  631. package/dist/web/chunk-WDU3WICG.js +1 -0
  632. package/dist/web/chunk-WFXJIXZE.js +4 -0
  633. package/dist/web/chunk-WLIMNDS3.js +1 -0
  634. package/dist/web/chunk-WTGYRH3Z.js +298 -0
  635. package/dist/web/chunk-WXTCVDTP.js +1 -0
  636. package/dist/web/chunk-X2HTISHL.js +1 -0
  637. package/dist/web/chunk-XCDHWLVH.js +1 -0
  638. package/dist/web/chunk-Y3H6FFUZ.js +1 -0
  639. package/dist/web/chunk-Y4F5ULGJ.js +1 -0
  640. package/dist/web/chunk-YAMRN47K.js +2 -0
  641. package/dist/web/chunk-YEGKAAEE.js +1 -0
  642. package/dist/web/chunk-YM2KU57F.js +1 -0
  643. package/dist/web/chunk-YRERBP6T.js +1 -0
  644. package/dist/web/chunk-ZLV4VCDG.js +3 -0
  645. package/dist/web/chunk-ZTVI5KFF.js +1 -0
  646. package/dist/web/favicon-16.png +0 -0
  647. package/dist/web/favicon-180.png +0 -0
  648. package/dist/web/favicon-32.png +0 -0
  649. package/dist/web/favicon-512.png +0 -0
  650. package/dist/web/favicon-small.svg +15 -0
  651. package/dist/web/favicon.ico +0 -0
  652. package/dist/web/favicon.svg +20 -0
  653. package/dist/web/index.html +145 -0
  654. package/dist/web/main-ESADRXN2.css +1 -0
  655. package/dist/web/main-R53HA54V.js +1 -0
  656. package/dist/web/media/codicon-LN6W7LCM.ttf +0 -0
  657. package/dist/web/styles-KSOPUVDA.css +1 -0
  658. package/dist/web/sw.js +69 -0
  659. package/dist/workflows/condition-evaluator.d.ts +75 -0
  660. package/dist/workflows/condition-evaluator.d.ts.map +1 -0
  661. package/dist/workflows/condition-evaluator.js +282 -0
  662. package/dist/workflows/condition-evaluator.js.map +1 -0
  663. package/dist/workflows/defaults/claude-design-implement.yaml +336 -0
  664. package/dist/workflows/defaults/index.d.ts +26 -0
  665. package/dist/workflows/defaults/index.d.ts.map +1 -0
  666. package/dist/workflows/defaults/index.js +94 -0
  667. package/dist/workflows/defaults/index.js.map +1 -0
  668. package/dist/workflows/defaults/spec-author.yaml +214 -0
  669. package/dist/workflows/defaults/spec-fix.yaml +110 -0
  670. package/dist/workflows/defaults/spec-implement.yaml +150 -0
  671. package/dist/workflows/defaults/spec-relink.yaml +81 -0
  672. package/dist/workflows/defaults/spec-verify.yaml +51 -0
  673. package/dist/workflows/discovery.d.ts +46 -0
  674. package/dist/workflows/discovery.d.ts.map +1 -0
  675. package/dist/workflows/discovery.js +193 -0
  676. package/dist/workflows/discovery.js.map +1 -0
  677. package/dist/workflows/executor.d.ts +83 -0
  678. package/dist/workflows/executor.d.ts.map +1 -0
  679. package/dist/workflows/executor.js +624 -0
  680. package/dist/workflows/executor.js.map +1 -0
  681. package/dist/workflows/runners/approval.d.ts +18 -0
  682. package/dist/workflows/runners/approval.d.ts.map +1 -0
  683. package/dist/workflows/runners/approval.js +34 -0
  684. package/dist/workflows/runners/approval.js.map +1 -0
  685. package/dist/workflows/runners/bash.d.ts +13 -0
  686. package/dist/workflows/runners/bash.d.ts.map +1 -0
  687. package/dist/workflows/runners/bash.js +143 -0
  688. package/dist/workflows/runners/bash.js.map +1 -0
  689. package/dist/workflows/runners/cancel.d.ts +10 -0
  690. package/dist/workflows/runners/cancel.d.ts.map +1 -0
  691. package/dist/workflows/runners/cancel.js +19 -0
  692. package/dist/workflows/runners/cancel.js.map +1 -0
  693. package/dist/workflows/runners/prompt.d.ts +28 -0
  694. package/dist/workflows/runners/prompt.d.ts.map +1 -0
  695. package/dist/workflows/runners/prompt.js +212 -0
  696. package/dist/workflows/runners/prompt.js.map +1 -0
  697. package/dist/workflows/runners/script.d.ts +17 -0
  698. package/dist/workflows/runners/script.d.ts.map +1 -0
  699. package/dist/workflows/runners/script.js +155 -0
  700. package/dist/workflows/runners/script.js.map +1 -0
  701. package/dist/workflows/runners/types.d.ts +51 -0
  702. package/dist/workflows/runners/types.d.ts.map +1 -0
  703. package/dist/workflows/runners/types.js +13 -0
  704. package/dist/workflows/runners/types.js.map +1 -0
  705. package/dist/workflows/schemas/workflow.d.ts +166 -0
  706. package/dist/workflows/schemas/workflow.d.ts.map +1 -0
  707. package/dist/workflows/schemas/workflow.js +437 -0
  708. package/dist/workflows/schemas/workflow.js.map +1 -0
  709. package/hooks/hooks.json +11 -0
  710. package/package.json +7 -3
  711. package/scripts/offline-install.sh +19 -6
  712. package/selectors.json +41 -0
  713. /package/commands/{cg-explore.md → ss-explore.md} +0 -0
  714. /package/commands/{cg-impact.md → ss-impact.md} +0 -0
  715. /package/commands/{cg-relink.md → ss-relink.md} +0 -0
  716. /package/commands/{cg-spec.md → ss-spec.md} +0 -0
  717. /package/commands/{cg-sync.md → ss-sync.md} +0 -0
  718. /package/commands/{cg-trace.md → ss-trace.md} +0 -0
@@ -0,0 +1,868 @@
1
+ /**
2
+ * Claude Code analytics routes.
3
+ *
4
+ * All queries hit specship's SQLite directly. The ingest worker
5
+ * (`@selvakumaresra/specship-ingest`) writes to claude_* tables; this layer
6
+ * just rolls up.
7
+ *
8
+ * Endpoints:
9
+ * GET /api/claude/projects — every indexed Claude project
10
+ * GET /api/claude/sessions?project=&limit= — sessions list
11
+ * GET /api/claude/session/:id — session detail (prompts + tools)
12
+ * GET /api/claude/heatmap?range= — file/tool/subagent heatmaps
13
+ * GET /api/claude/costs?range= — cost rollup, timeseries, per-model
14
+ * GET /api/claude/compare — per-project cost comparison
15
+ * GET /api/claude/tips — rule-based tips engine output
16
+ * POST /api/claude/ingest — force a one-shot ingest pass
17
+ */
18
+ import { decodeProjectSlug } from '../ingest/index.js';
19
+ /**
20
+ * Normalize a `?project=` filter value to the form stored in
21
+ * claude_sessions.project_path. The Sessions page (and any other UI
22
+ * surface that uses ProjectsService.projectQuery()) sends the directory
23
+ * SLUG that names the Claude Code transcript dir (e.g.
24
+ * `-Users-foo-projects-bar`), but the ingestor writes the DECODED path
25
+ * (e.g. `/Users/foo/projects/bar`) into project_path because that's the
26
+ * value the agent actually identifies the project by. Comparing slug
27
+ * against path always fails and the list comes back empty even though
28
+ * the rows are there — fix by decoding the slug form here.
29
+ *
30
+ * A path passed in directly (doesn't start with `-`) round-trips
31
+ * unchanged, so curl-by-path and the old behavior both keep working.
32
+ */
33
+ function normalizeProjectFilter(input) {
34
+ return decodeProjectSlug(input);
35
+ }
36
+ const RANGE_WINDOW_MS = {
37
+ today: 24 * 60 * 60 * 1000,
38
+ week: 7 * 24 * 60 * 60 * 1000,
39
+ month: 30 * 24 * 60 * 60 * 1000,
40
+ all: Number.MAX_SAFE_INTEGER,
41
+ };
42
+ function rangeKey(input) {
43
+ if (input === 'today' || input === 'week' || input === 'month' || input === 'all')
44
+ return input;
45
+ return 'week';
46
+ }
47
+ function rangeStart(key) {
48
+ if (key === 'all')
49
+ return 0;
50
+ return Date.now() - RANGE_WINDOW_MS[key];
51
+ }
52
+ /**
53
+ * Bounds of the period immediately BEFORE the current range window — used for
54
+ * week-over-week (wowDelta) comparisons. For 'all' there's no prior period, so
55
+ * both bounds collapse to 0 and callers should treat the delta as 0.
56
+ */
57
+ function priorWindow(key) {
58
+ if (key === 'all')
59
+ return { start: 0, end: 0 };
60
+ const w = RANGE_WINDOW_MS[key];
61
+ const end = Date.now() - w;
62
+ return { start: end - w, end };
63
+ }
64
+ /**
65
+ * Get the internal SQLite handle off the DatabaseConnection so we can run
66
+ * Claude-specific aggregate queries directly. SpecShip exposes this via
67
+ * `getDb()`-style accessors. Falls back to digging via the queries property.
68
+ */
69
+ function getDb(cg) {
70
+ // SpecShip exposes the underlying DB via its DatabaseConnection. Look it
71
+ // up via the private field as a fallback — works because it's the same
72
+ // shape regardless of which adapter (better-sqlite3 / node:sqlite) is active.
73
+ const anyCg = cg;
74
+ if (anyCg.db?.getDb)
75
+ return anyCg.db.getDb();
76
+ if (anyCg.queries?.db)
77
+ return anyCg.queries.db;
78
+ throw new Error('specship DB handle not accessible from server context');
79
+ }
80
+ export async function registerClaudeRoutes(app) {
81
+ /**
82
+ * Analytics routes share one SQLite — the boot-time "primary" project's
83
+ * specship.db hosts the cross-project claude_* tables. Without a primary
84
+ * the JSONL ingest has nowhere to write and there's nothing to query, so
85
+ * every analytics handler asks here first.
86
+ */
87
+ function requirePrimary(reply) {
88
+ if (!app.primaryCg) {
89
+ reply.code(409).send({ error: 'analytics unavailable: no primary project configured', code: 'no_primary' });
90
+ return null;
91
+ }
92
+ return app.primaryCg;
93
+ }
94
+ app.get('/api/claude/projects', async (_req, reply) => {
95
+ const cg = requirePrimary(reply);
96
+ if (!cg)
97
+ return;
98
+ const db = getDb(cg);
99
+ const rows = db.prepare(`
100
+ SELECT p.path, p.name, p.first_seen, p.last_seen,
101
+ COUNT(s.id) as sessions,
102
+ COALESCE(SUM(s.total_cost_usd), 0) as cost,
103
+ COALESCE(SUM(s.total_cache_read_tokens), 0) as cacheRead,
104
+ COALESCE(SUM(s.total_cache_creation_tokens + s.total_input_tokens), 0) as totalInput,
105
+ COALESCE(SUM(s.prompt_count), 0) as prompts
106
+ FROM claude_projects p
107
+ LEFT JOIN claude_sessions s ON s.project_path = p.path
108
+ GROUP BY p.path
109
+ ORDER BY cost DESC
110
+ `).all();
111
+ return { projects: rows };
112
+ });
113
+ app.get('/api/claude/sessions', async (req, reply) => {
114
+ const cg = requirePrimary(reply);
115
+ if (!cg)
116
+ return;
117
+ const db = getDb(cg);
118
+ const limit = Math.min(parseInt(req.query.limit ?? '100', 10) || 100, 500);
119
+ const since = rangeStart(rangeKey(req.query.range));
120
+ const params = [since];
121
+ let whereProject = '';
122
+ if (req.query.project) {
123
+ whereProject = ' AND project_path = ?';
124
+ params.push(normalizeProjectFilter(req.query.project));
125
+ }
126
+ params.push(limit);
127
+ const sessions = db.prepare(`
128
+ SELECT * FROM claude_sessions
129
+ WHERE started_at >= ?${whereProject}
130
+ ORDER BY started_at DESC
131
+ LIMIT ?
132
+ `).all(...params);
133
+ return { sessions };
134
+ });
135
+ app.get('/api/claude/session/:id', async (req, reply) => {
136
+ const cg = requirePrimary(reply);
137
+ if (!cg)
138
+ return;
139
+ const db = getDb(cg);
140
+ const session = db.prepare('SELECT * FROM claude_sessions WHERE id = ?').get(req.params.id);
141
+ if (!session)
142
+ return reply.code(404).send({ error: 'session not found' });
143
+ const prompts = db.prepare(`
144
+ SELECT * FROM claude_prompts WHERE session_id = ? ORDER BY ts ASC
145
+ `).all(req.params.id);
146
+ const toolCalls = db.prepare(`
147
+ SELECT * FROM claude_tool_calls WHERE session_id = ? ORDER BY ts ASC
148
+ `).all(req.params.id);
149
+ // Per-prompt wall-clock duration: the gap from this prompt to the next one
150
+ // (last prompt runs to the session's end). Lets the UI show "how long did
151
+ // this turn take" without a separate per-event timeline.
152
+ const endedAt = session.ended_at ?? null;
153
+ prompts.forEach((p, i) => {
154
+ const next = prompts[i + 1];
155
+ const end = next ? next.ts : (endedAt ?? p.ts);
156
+ p.durationMs = Math.max(0, end - p.ts);
157
+ });
158
+ return { session, prompts, toolCalls };
159
+ });
160
+ /**
161
+ * SSE event stream for a single session — pushes new prompts and tool
162
+ * calls as the JSONL ingest watcher lands them, so the dashboard's
163
+ * Session Detail page can update without polling. Mirrors the shape used
164
+ * by /api/workflows/runs/:id/events.
165
+ *
166
+ * Polling cadence inside the loop is 500 ms — fast enough that the
167
+ * end-to-end "user hit Enter in Claude Code → prompt visible in
168
+ * dashboard" latency stays well under one second (300 ms watcher
169
+ * debounce + ≤50 ms ingest + ≤500 ms poll). Heartbeat every 15 s
170
+ * keeps idle connections alive past any proxy or browser tab-throttle
171
+ * cutoff.
172
+ *
173
+ * The client (LiveSessionTail in session-detail.ts) doesn't merge events
174
+ * incrementally — it just calls resource.refetch() on every event since
175
+ * the detail endpoint is local + cheap. Server-side, that means we only
176
+ * need to push enough info for the client to know "something changed"
177
+ * (id + ts), not the full row payloads. We send the full row anyway so
178
+ * a future client could merge incrementally without an API change.
179
+ */
180
+ app.get('/api/claude/session/:id/events', async (req, reply) => {
181
+ const cg = requirePrimary(reply);
182
+ if (!cg)
183
+ return;
184
+ const db = getDb(cg);
185
+ const sessionId = req.params.id;
186
+ // Confirm the session exists before opening the stream — saves a
187
+ // doomed connection from polling forever against a typo.
188
+ const sessionRow = db.prepare('SELECT id FROM claude_sessions WHERE id = ?').get(sessionId);
189
+ if (!sessionRow) {
190
+ return reply.code(404).send({ error: 'session not found' });
191
+ }
192
+ // Resume points — clients can pick up after a disconnect without
193
+ // re-receiving every prompt. Defaults to "now" so an opening client
194
+ // only sees future events.
195
+ let lastPromptTs = parseInt(req.query.sincePromptTs ?? '0', 10);
196
+ let lastToolTs = parseInt(req.query.sinceToolTs ?? '0', 10);
197
+ if (!lastPromptTs || Number.isNaN(lastPromptTs)) {
198
+ const row = db.prepare('SELECT MAX(ts) as m FROM claude_prompts WHERE session_id = ?').get(sessionId);
199
+ lastPromptTs = row?.m ?? 0;
200
+ }
201
+ if (!lastToolTs || Number.isNaN(lastToolTs)) {
202
+ const row = db.prepare('SELECT MAX(ts) as m FROM claude_tool_calls WHERE session_id = ?').get(sessionId);
203
+ lastToolTs = row?.m ?? 0;
204
+ }
205
+ reply.raw.setHeader('Content-Type', 'text/event-stream');
206
+ reply.raw.setHeader('Cache-Control', 'no-cache');
207
+ reply.raw.setHeader('Connection', 'keep-alive');
208
+ reply.raw.setHeader('X-Accel-Buffering', 'no'); // nginx-friendly
209
+ reply.raw.flushHeaders();
210
+ // Initial snapshot — gives the client a clean baseline (so it knows
211
+ // SSE is wired up even before any new event fires) and the cursor
212
+ // positions it should resume from on reconnect.
213
+ reply.raw.write(`event: snapshot\ndata: ${JSON.stringify({ sessionId, lastPromptTs, lastToolTs })}\n\n`);
214
+ let closed = false;
215
+ req.raw.on('close', () => { closed = true; });
216
+ const newPromptsStmt = db.prepare('SELECT * FROM claude_prompts WHERE session_id = ? AND ts > ? ORDER BY ts ASC');
217
+ const newToolsStmt = db.prepare('SELECT * FROM claude_tool_calls WHERE session_id = ? AND ts > ? ORDER BY ts ASC');
218
+ let lastHeartbeat = Date.now();
219
+ const tick = () => {
220
+ if (closed)
221
+ return;
222
+ try {
223
+ const freshPrompts = newPromptsStmt.all(sessionId, lastPromptTs);
224
+ for (const p of freshPrompts) {
225
+ reply.raw.write(`event: prompt_added\ndata: ${JSON.stringify(p)}\n\n`);
226
+ if (p.ts > lastPromptTs)
227
+ lastPromptTs = p.ts;
228
+ }
229
+ const freshTools = newToolsStmt.all(sessionId, lastToolTs);
230
+ for (const t of freshTools) {
231
+ reply.raw.write(`event: tool_call_added\ndata: ${JSON.stringify(t)}\n\n`);
232
+ if (t.ts > lastToolTs)
233
+ lastToolTs = t.ts;
234
+ }
235
+ // Heartbeat every 15 s — keeps proxies / browser tab throttles
236
+ // from killing an otherwise-idle connection.
237
+ const now = Date.now();
238
+ if (now - lastHeartbeat >= 15_000) {
239
+ reply.raw.write(`: keepalive ${now}\n\n`);
240
+ lastHeartbeat = now;
241
+ }
242
+ }
243
+ catch (err) {
244
+ // Surface DB errors to the client as a named event and end the
245
+ // stream — the client's onError handler flips to polling.
246
+ reply.raw.write(`event: stream_error\ndata: ${JSON.stringify({ message: err instanceof Error ? err.message : String(err) })}\n\n`);
247
+ reply.raw.end();
248
+ closed = true;
249
+ return;
250
+ }
251
+ setTimeout(tick, 500);
252
+ };
253
+ void tick();
254
+ });
255
+ /**
256
+ * Session-level aggregation for the Session Detail "Session summary" panel.
257
+ * Three things callers can't cheaply derive client-side from the per-prompt
258
+ * list: tool usage rolled up by name, files touched across all turns, and
259
+ * slash-command / skill invocation counts (which require either a regex
260
+ * over every prompt's text or json_extract over every Skill tool input).
261
+ * Computed once per session-detail page load (and after refresh) — cheap
262
+ * enough to recompute on every call, no caching.
263
+ */
264
+ app.get('/api/claude/session/:id/summary', async (req, reply) => {
265
+ const cg = requirePrimary(reply);
266
+ if (!cg)
267
+ return;
268
+ const db = getDb(cg);
269
+ const sessionId = req.params.id;
270
+ const session = db.prepare('SELECT id, started_at, ended_at FROM claude_sessions WHERE id = ?').get(sessionId);
271
+ if (!session)
272
+ return reply.code(404).send({ error: 'session not found' });
273
+ // Tools used in this session, rolled up by name. Mirrors the heatmap
274
+ // tool query but scoped to one session, so the panel can answer "what
275
+ // did Claude DO in this session" at a glance.
276
+ const byTool = db.prepare(`
277
+ SELECT tool_name as name, COUNT(*) as calls, COALESCE(SUM(result_length), 0) as totalBytes
278
+ FROM claude_tool_calls
279
+ WHERE session_id = ?
280
+ GROUP BY tool_name
281
+ ORDER BY calls DESC
282
+ `).all(sessionId);
283
+ // Models used. Most sessions stay on one model but sidechains can fan
284
+ // out to Haiku, so this surfaces multi-model spend that the session-level
285
+ // last_model column hides.
286
+ const byModel = db.prepare(`
287
+ SELECT model, COUNT(*) as prompts, COALESCE(SUM(cost_usd), 0) as cost
288
+ FROM claude_prompts
289
+ WHERE session_id = ? AND model IS NOT NULL
290
+ GROUP BY model
291
+ ORDER BY cost DESC
292
+ `).all(sessionId);
293
+ // Files touched via Read/Edit/Write/NotebookEdit. input_summary IS the
294
+ // file path for those tools (set in the ingestor's parser). Group by
295
+ // path, count ops, also record which tool last touched it so the UI
296
+ // can show a "last op" hint (Read vs Edit).
297
+ const filesTouched = db.prepare(`
298
+ SELECT input_summary as path, COUNT(*) as ops,
299
+ (SELECT tool_name FROM claude_tool_calls AS inner_tc
300
+ WHERE inner_tc.session_id = tc.session_id
301
+ AND inner_tc.input_summary = tc.input_summary
302
+ ORDER BY inner_tc.ts DESC LIMIT 1) as lastOp
303
+ FROM claude_tool_calls AS tc
304
+ WHERE tc.session_id = ?
305
+ AND tc.tool_name IN ('Read','Edit','Write','NotebookEdit','MultiEdit')
306
+ AND tc.input_summary != ''
307
+ GROUP BY tc.input_summary
308
+ ORDER BY ops DESC
309
+ LIMIT 30
310
+ `).all(sessionId);
311
+ // Skills invoked via the Skill tool. The ingestor stashes the
312
+ // JSON-serialized input under input_summary, so json_extract pulls the
313
+ // skill name straight out without needing the v7 input_json column to
314
+ // be backfilled.
315
+ const skills = db.prepare(`
316
+ SELECT
317
+ COALESCE(json_extract(input_summary, '$.skill'), json_extract(input_summary, '$.skill_name'), 'unknown') as name,
318
+ COUNT(*) as count
319
+ FROM claude_tool_calls
320
+ WHERE session_id = ? AND tool_name = 'Skill'
321
+ GROUP BY name
322
+ ORDER BY count DESC
323
+ `).all(sessionId);
324
+ // Slash commands invoked. Claude Code wraps each one in a
325
+ // <command-name>/foo</command-name> tag in the user-prompt text.
326
+ // Pull every prompt that has that tag and regex it client-side here
327
+ // (SQLite has no built-in regex). Cheap — most sessions have <50.
328
+ const taggedPrompts = db.prepare(`
329
+ SELECT text FROM claude_prompts WHERE session_id = ? AND text LIKE '%<command-name>%'
330
+ `).all(sessionId);
331
+ const cmdCounts = new Map();
332
+ const cmdRe = /<command-name>([^<]+)<\/command-name>/g;
333
+ for (const { text } of taggedPrompts) {
334
+ if (!text)
335
+ continue;
336
+ for (const m of text.matchAll(cmdRe)) {
337
+ const raw = m[1]?.trim();
338
+ if (!raw)
339
+ continue;
340
+ cmdCounts.set(raw, (cmdCounts.get(raw) ?? 0) + 1);
341
+ }
342
+ }
343
+ const slashCommands = Array.from(cmdCounts.entries())
344
+ .map(([name, count]) => ({ name, count }))
345
+ .sort((a, b) => b.count - a.count);
346
+ const durationMs = session.started_at && session.ended_at
347
+ ? Math.max(0, session.ended_at - session.started_at)
348
+ : 0;
349
+ return {
350
+ sessionId,
351
+ byTool,
352
+ byModel,
353
+ slashCommands,
354
+ skills,
355
+ filesTouched,
356
+ durationMs,
357
+ };
358
+ });
359
+ app.get('/api/claude/heatmap', async (req, reply) => {
360
+ const cg = requirePrimary(reply);
361
+ if (!cg)
362
+ return;
363
+ const db = getDb(cg);
364
+ const since = rangeStart(rangeKey(req.query.range));
365
+ // Files heatmap — input_summary doubles as the file path for Read/Edit/Write.
366
+ const files = db.prepare(`
367
+ SELECT input_summary as path, COUNT(*) as calls, SUM(result_length) as resultBytes
368
+ FROM claude_tool_calls
369
+ WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''
370
+ GROUP BY input_summary
371
+ ORDER BY calls DESC
372
+ LIMIT 100
373
+ `).all(since);
374
+ // Per-file 7-day call trend — one sparkline (oldest→newest) per file cell.
375
+ // Always a fixed 7-day window, independent of `range`, so the trend reads
376
+ // consistently regardless of the heatmap's selected window.
377
+ const dayMs = 24 * 60 * 60 * 1000;
378
+ const trend7Start = Math.floor(Date.now() / dayMs) * dayMs - 6 * dayMs;
379
+ const trendRows = db.prepare(`
380
+ SELECT input_summary as path, CAST((ts - ?) / ? AS INTEGER) as bucket, COUNT(*) as calls
381
+ FROM claude_tool_calls
382
+ WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''
383
+ GROUP BY input_summary, bucket
384
+ `).all(trend7Start, dayMs, trend7Start);
385
+ const trendByPath = new Map();
386
+ for (const r of trendRows) {
387
+ if (r.bucket < 0 || r.bucket > 6)
388
+ continue;
389
+ const arr = trendByPath.get(r.path) ?? [0, 0, 0, 0, 0, 0, 0];
390
+ arr[r.bucket] = r.calls;
391
+ trendByPath.set(r.path, arr);
392
+ }
393
+ for (const f of files) {
394
+ f.trend = trendByPath.get(f.path) ?? [0, 0, 0, 0, 0, 0, 0];
395
+ }
396
+ // Tools heatmap.
397
+ const tools = db.prepare(`
398
+ SELECT tool_name as name, COUNT(*) as calls, SUM(result_length) as resultBytes
399
+ FROM claude_tool_calls
400
+ WHERE ts >= ?
401
+ GROUP BY tool_name
402
+ ORDER BY calls DESC
403
+ `).all(since);
404
+ // Subagent attribution (is_sidechain at the prompt level — main vs. sidechain rollup).
405
+ const subagents = db.prepare(`
406
+ SELECT
407
+ CASE WHEN p.is_sidechain = 1 THEN 'subagent' ELSE 'main' END as type,
408
+ COUNT(*) as prompts,
409
+ SUM(p.input_tokens + p.output_tokens + p.cache_creation_tokens + p.cache_read_tokens) as tokens,
410
+ SUM(p.cost_usd) as cost
411
+ FROM claude_prompts p
412
+ WHERE p.ts >= ?
413
+ GROUP BY type
414
+ `).all(since);
415
+ // Subagent breakdown by name — Task tool calls grouped by subagent_type.
416
+ // input_summary is the JSON-serialized tool input; json_extract pulls
417
+ // out subagent_type (defaults to 'general-purpose' when unset).
418
+ const subagentByName = db.prepare(`
419
+ SELECT
420
+ COALESCE(NULLIF(json_extract(input_summary, '$.subagent_type'), ''), 'general-purpose') as name,
421
+ COUNT(*) as calls,
422
+ MIN(ts) as firstSeen,
423
+ MAX(ts) as lastSeen
424
+ FROM claude_tool_calls
425
+ WHERE ts >= ? AND tool_name = 'Task'
426
+ GROUP BY name
427
+ ORDER BY calls DESC
428
+ `).all(since);
429
+ return { files, tools, subagents, subagentByName };
430
+ });
431
+ /**
432
+ * Drill-down: which sessions touched a given file (via Read/Edit/Write).
433
+ * Used by the heatmap page when the user clicks a file cell.
434
+ */
435
+ app.get('/api/claude/heatmap/file', async (req, reply) => {
436
+ const path = req.query.path;
437
+ if (!path)
438
+ return reply.code(400).send({ error: 'path required' });
439
+ const cg = requirePrimary(reply);
440
+ if (!cg)
441
+ return;
442
+ const db = getDb(cg);
443
+ const since = rangeStart(rangeKey(req.query.range));
444
+ const sessions = db.prepare(`
445
+ SELECT t.session_id, s.last_model, s.project_path,
446
+ COUNT(*) as calls, COALESCE(SUM(t.result_length), 0) as bytes,
447
+ MAX(t.ts) as lastTs, MIN(t.ts) as firstTs
448
+ FROM claude_tool_calls t
449
+ LEFT JOIN claude_sessions s ON s.id = t.session_id
450
+ WHERE t.input_summary = ? AND t.ts >= ? AND t.tool_name IN ('Read','Edit','Write','NotebookEdit')
451
+ GROUP BY t.session_id
452
+ ORDER BY calls DESC
453
+ LIMIT 50
454
+ `).all(path, since);
455
+ const byTool = db.prepare(`
456
+ SELECT tool_name as name, COUNT(*) as calls, COALESCE(SUM(result_length), 0) as bytes
457
+ FROM claude_tool_calls
458
+ WHERE input_summary = ? AND ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit')
459
+ GROUP BY tool_name
460
+ ORDER BY calls DESC
461
+ `).all(path, since);
462
+ return { path, sessions, byTool };
463
+ });
464
+ /**
465
+ * Drill-down: the top distinct inputs for a given tool (file paths for
466
+ * Read/Edit/Write, patterns for Grep/Glob, commands for Bash).
467
+ */
468
+ app.get('/api/claude/heatmap/tool', async (req, reply) => {
469
+ const name = req.query.name;
470
+ if (!name)
471
+ return reply.code(400).send({ error: 'name required' });
472
+ const cg = requirePrimary(reply);
473
+ if (!cg)
474
+ return;
475
+ const db = getDb(cg);
476
+ const since = rangeStart(rangeKey(req.query.range));
477
+ const totals = db.prepare(`
478
+ SELECT COUNT(*) as calls, COALESCE(SUM(result_length), 0) as bytes,
479
+ COUNT(DISTINCT session_id) as sessions
480
+ FROM claude_tool_calls
481
+ WHERE tool_name = ? AND ts >= ?
482
+ `).get(name, since);
483
+ const inputs = db.prepare(`
484
+ SELECT
485
+ CASE WHEN length(input_summary) > 120 THEN substr(input_summary, 1, 120) || '…'
486
+ ELSE input_summary END as input,
487
+ COUNT(*) as calls,
488
+ COALESCE(SUM(result_length), 0) as bytes,
489
+ MAX(ts) as lastTs
490
+ FROM claude_tool_calls
491
+ WHERE tool_name = ? AND ts >= ? AND input_summary != ''
492
+ GROUP BY input_summary
493
+ ORDER BY calls DESC
494
+ LIMIT 30
495
+ `).all(name, since);
496
+ const recentSessions = db.prepare(`
497
+ SELECT t.session_id, s.last_model, s.project_path, COUNT(*) as calls, MAX(t.ts) as lastTs
498
+ FROM claude_tool_calls t
499
+ LEFT JOIN claude_sessions s ON s.id = t.session_id
500
+ WHERE t.tool_name = ? AND t.ts >= ?
501
+ GROUP BY t.session_id
502
+ ORDER BY lastTs DESC
503
+ LIMIT 20
504
+ `).all(name, since);
505
+ return { tool: name, totals, inputs, recentSessions };
506
+ });
507
+ /**
508
+ * Drill-down: invocations of a specific subagent (by subagent_type name).
509
+ */
510
+ app.get('/api/claude/heatmap/subagent', async (req, reply) => {
511
+ const type = req.query.type;
512
+ if (!type)
513
+ return reply.code(400).send({ error: 'type required' });
514
+ const cg = requirePrimary(reply);
515
+ if (!cg)
516
+ return;
517
+ const db = getDb(cg);
518
+ const since = rangeStart(rangeKey(req.query.range));
519
+ const totals = db.prepare(`
520
+ SELECT COUNT(*) as calls, COUNT(DISTINCT session_id) as sessions
521
+ FROM claude_tool_calls
522
+ WHERE tool_name = 'Task' AND ts >= ?
523
+ AND COALESCE(NULLIF(json_extract(input_summary, '$.subagent_type'), ''), 'general-purpose') = ?
524
+ `).get(since, type);
525
+ const invocations = db.prepare(`
526
+ SELECT
527
+ t.session_id,
528
+ t.ts,
529
+ COALESCE(json_extract(t.input_summary, '$.description'), '') as description,
530
+ COALESCE(json_extract(t.input_summary, '$.prompt'), '') as prompt,
531
+ s.last_model
532
+ FROM claude_tool_calls t
533
+ LEFT JOIN claude_sessions s ON s.id = t.session_id
534
+ WHERE t.tool_name = 'Task' AND t.ts >= ?
535
+ AND COALESCE(NULLIF(json_extract(t.input_summary, '$.subagent_type'), ''), 'general-purpose') = ?
536
+ ORDER BY t.ts DESC
537
+ LIMIT 50
538
+ `).all(since, type);
539
+ return { subagent: type, totals, invocations };
540
+ });
541
+ app.get('/api/claude/cache', async (req, reply) => {
542
+ const cg = requirePrimary(reply);
543
+ if (!cg)
544
+ return;
545
+ const db = getDb(cg);
546
+ const since = rangeStart(rangeKey(req.query.range));
547
+ // Aggregate cache totals from claude_sessions for sessions in window.
548
+ const agg = db.prepare(`
549
+ SELECT
550
+ COALESCE(SUM(total_input_tokens), 0) as inp,
551
+ COALESCE(SUM(total_output_tokens), 0) as out,
552
+ COALESCE(SUM(total_cache_creation_tokens), 0) as cw,
553
+ COALESCE(SUM(total_cache_read_tokens), 0) as cr,
554
+ COALESCE(SUM(total_cost_usd), 0) as cost
555
+ FROM claude_sessions
556
+ WHERE started_at >= ?
557
+ `).get(since);
558
+ const total = (agg.inp ?? 0) + (agg.cw ?? 0) + (agg.cr ?? 0);
559
+ const readRate = total > 0 ? agg.cr / total : 0;
560
+ // Dollars saved estimate: cache_read tokens billed at ~10% of input;
561
+ // savings vs charging them at input rate ≈ 0.9 × (cr / 1M) × inputRate.
562
+ // Use Opus 4-7 input ($15/M) as a generous upper bound — the UI shows
563
+ // this as an approximation.
564
+ const dollarsSaved = ((agg.cr ?? 0) / 1_000_000) * 15 * 0.9;
565
+ // Week-over-week reuse delta: current read-rate minus the prior equal-length
566
+ // window's read-rate. Fractional (e.g. 0.06 → "+6%" in the UI). Zero for
567
+ // 'all' (no prior period) or when there's no prior-window data to compare.
568
+ const prior = priorWindow(rangeKey(req.query.range));
569
+ let wowDelta = 0;
570
+ if (prior.end > prior.start) {
571
+ const pAgg = db.prepare(`
572
+ SELECT
573
+ COALESCE(SUM(total_input_tokens), 0) as inp,
574
+ COALESCE(SUM(total_cache_creation_tokens), 0) as cw,
575
+ COALESCE(SUM(total_cache_read_tokens), 0) as cr
576
+ FROM claude_sessions
577
+ WHERE started_at >= ? AND started_at < ?
578
+ `).get(prior.start, prior.end);
579
+ const pTotal = (pAgg.inp ?? 0) + (pAgg.cw ?? 0) + (pAgg.cr ?? 0);
580
+ if (pTotal > 0)
581
+ wowDelta = readRate - pAgg.cr / pTotal;
582
+ }
583
+ return {
584
+ readRate,
585
+ creationTokens: agg.cw ?? 0,
586
+ readTokens: agg.cr ?? 0,
587
+ inputTokens: agg.inp ?? 0,
588
+ outputTokens: agg.out ?? 0,
589
+ totalCost: agg.cost ?? 0,
590
+ dollarsSaved,
591
+ wowDelta,
592
+ };
593
+ });
594
+ app.get('/api/claude/costs', async (req, reply) => {
595
+ const cg = requirePrimary(reply);
596
+ if (!cg)
597
+ return;
598
+ const db = getDb(cg);
599
+ const since = rangeStart(rangeKey(req.query.range));
600
+ const total = db.prepare(`SELECT SUM(total_cost_usd) as t FROM claude_sessions WHERE started_at >= ?`).get(since);
601
+ // Week-over-week spend delta: fractional change in total cost vs the prior
602
+ // equal-length window (e.g. -0.08 → "-8%"). Zero for 'all' or no prior data.
603
+ const prior = priorWindow(rangeKey(req.query.range));
604
+ let wowDelta = 0;
605
+ if (prior.end > prior.start) {
606
+ const pTotal = db.prepare(`SELECT SUM(total_cost_usd) as t FROM claude_sessions WHERE started_at >= ? AND started_at < ?`).get(prior.start, prior.end);
607
+ const pt = pTotal.t ?? 0;
608
+ if (pt > 0)
609
+ wowDelta = ((total.t ?? 0) - pt) / pt;
610
+ }
611
+ // Top prompts by cost.
612
+ const topPrompts = db.prepare(`
613
+ SELECT id, session_id, substr(text, 1, 200) as text, model, cost_usd,
614
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ts
615
+ FROM claude_prompts
616
+ WHERE ts >= ? AND cost_usd > 0
617
+ ORDER BY cost_usd DESC
618
+ LIMIT 50
619
+ `).all(since);
620
+ // Daily timeseries: bucket by 24h windows.
621
+ const days = 30;
622
+ const dayMs = 24 * 60 * 60 * 1000;
623
+ const dayBoundary = Math.floor(Date.now() / dayMs) * dayMs - (days - 1) * dayMs;
624
+ const series = db.prepare(`
625
+ SELECT
626
+ CAST((ts - ?) / ? AS INTEGER) as bucket,
627
+ SUM(cost_usd) as cost,
628
+ COUNT(*) as prompts
629
+ FROM claude_prompts
630
+ WHERE ts >= ?
631
+ GROUP BY bucket
632
+ ORDER BY bucket ASC
633
+ `).all(dayBoundary, dayMs, dayBoundary);
634
+ // Densify with zeros for missing days.
635
+ const dense = [];
636
+ for (let i = 0; i < days; i++) {
637
+ const found = series.find((s) => s.bucket === i);
638
+ dense.push({ day: days - 1 - i, cost: found?.cost ?? 0, prompts: found?.prompts ?? 0 });
639
+ }
640
+ // By model.
641
+ const byModel = db.prepare(`
642
+ SELECT model, COUNT(*) as prompts, SUM(cost_usd) as cost
643
+ FROM claude_prompts
644
+ WHERE ts >= ? AND model IS NOT NULL
645
+ GROUP BY model
646
+ ORDER BY cost DESC
647
+ `).all(since);
648
+ return { total: total.t ?? 0, topPrompts, series: dense, byModel, wowDelta };
649
+ });
650
+ /**
651
+ * GET /api/claude/stats?range= — the four dashboard stat tiles, each with a
652
+ * value, a fractional week-over-week delta (vs the prior equal-length window)
653
+ * and a 7-day sparkline series (oldest→newest). Drift is a live graph metric
654
+ * with no history, so it returns value-only.
655
+ */
656
+ app.get('/api/claude/stats', async (req, reply) => {
657
+ const cg = requirePrimary(reply);
658
+ if (!cg)
659
+ return;
660
+ const db = getDb(cg);
661
+ const rkey = rangeKey(req.query.range);
662
+ const since = rangeStart(rkey);
663
+ const prior = priorWindow(rkey);
664
+ const dayMs = 24 * 60 * 60 * 1000;
665
+ const d7Start = Math.floor(Date.now() / dayMs) * dayMs - 6 * dayMs;
666
+ const dense7 = (rows) => {
667
+ const a = [0, 0, 0, 0, 0, 0, 0];
668
+ for (const r of rows)
669
+ if (r.bucket >= 0 && r.bucket <= 6)
670
+ a[r.bucket] = r.v;
671
+ return a;
672
+ };
673
+ const countSince = (sql, ...p) => db.prepare(sql).get(...p).c ?? 0;
674
+ // --- Tool calls ---
675
+ const tcTotal = countSince(`SELECT COUNT(*) c FROM claude_tool_calls WHERE ts >= ?`, since);
676
+ const tcPrior = prior.end > prior.start
677
+ ? countSince(`SELECT COUNT(*) c FROM claude_tool_calls WHERE ts >= ? AND ts < ?`, prior.start, prior.end)
678
+ : 0;
679
+ const tcSeries = db.prepare(`SELECT CAST((ts - ?) / ? AS INTEGER) as bucket, COUNT(*) as v FROM claude_tool_calls WHERE ts >= ? GROUP BY bucket`).all(d7Start, dayMs, d7Start);
680
+ const toolCalls = { value: tcTotal, delta: tcPrior > 0 ? (tcTotal - tcPrior) / tcPrior : 0, series: dense7(tcSeries) };
681
+ // --- Subagent spend share (by cost) ---
682
+ const subPctOf = (start, end) => {
683
+ const where = end == null ? `ts >= ?` : `ts >= ? AND ts < ?`;
684
+ const args = end == null ? [start] : [start, end];
685
+ const rows = db.prepare(`SELECT is_sidechain as side, COALESCE(SUM(cost_usd), 0) as cost FROM claude_prompts WHERE ${where} GROUP BY is_sidechain`).all(...args);
686
+ const sub = rows.find((r) => r.side === 1)?.cost ?? 0;
687
+ const tot = rows.reduce((a, r) => a + (r.cost ?? 0), 0);
688
+ return tot > 0 ? sub / tot : 0;
689
+ };
690
+ const subPct = subPctOf(since, null);
691
+ const subPctPrior = prior.end > prior.start ? subPctOf(prior.start, prior.end) : 0;
692
+ const saDaily = db.prepare(`
693
+ SELECT CAST((ts - ?) / ? AS INTEGER) as bucket,
694
+ SUM(CASE WHEN is_sidechain = 1 THEN cost_usd ELSE 0 END) as sub,
695
+ SUM(cost_usd) as tot
696
+ FROM claude_prompts WHERE ts >= ? GROUP BY bucket
697
+ `).all(d7Start, dayMs, d7Start);
698
+ const saSeries = [0, 0, 0, 0, 0, 0, 0];
699
+ for (const r of saDaily)
700
+ if (r.bucket >= 0 && r.bucket <= 6)
701
+ saSeries[r.bucket] = r.tot > 0 ? Math.round((r.sub / r.tot) * 100) : 0;
702
+ const subagentPct = { value: Math.round(subPct * 100), delta: subPct - subPctPrior, series: saSeries };
703
+ // --- Last session cost (delta vs previous session, spark of recent sessions) ---
704
+ const recent = db.prepare(`SELECT total_cost_usd as cost FROM claude_sessions ORDER BY started_at DESC LIMIT 10`).all();
705
+ const lastCost = recent[0]?.cost ?? 0;
706
+ const prevCost = recent[1]?.cost ?? 0;
707
+ const lastSessionCost = {
708
+ value: lastCost,
709
+ delta: prevCost > 0 ? (lastCost - prevCost) / prevCost : 0,
710
+ series: recent.map((r) => r.cost ?? 0).reverse(),
711
+ };
712
+ // --- Drift (live graph count, no time series) ---
713
+ const driftCount = cg.getSpecQueries().getLinksByState(['drifted', 'broken', 'orphaned']).length;
714
+ const drift = { value: driftCount, delta: 0, series: [] };
715
+ return { lastSessionCost, toolCalls, subagentPct, drift };
716
+ });
717
+ app.get('/api/claude/compare', async (_req, reply) => {
718
+ const cg = requirePrimary(reply);
719
+ if (!cg)
720
+ return;
721
+ const db = getDb(cg);
722
+ const rows = db.prepare(`
723
+ SELECT
724
+ p.path, p.name,
725
+ COUNT(s.id) as sessions,
726
+ COALESCE(SUM(s.total_cost_usd), 0) as cost,
727
+ COALESCE(AVG(s.total_cost_usd), 0) as avgCost,
728
+ COALESCE(SUM(s.prompt_count), 0) as prompts,
729
+ CASE WHEN SUM(s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens) > 0
730
+ THEN CAST(SUM(s.total_cache_read_tokens) AS REAL) / SUM(s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens)
731
+ ELSE 0 END as cacheHit
732
+ FROM claude_projects p
733
+ LEFT JOIN claude_sessions s ON s.project_path = p.path
734
+ GROUP BY p.path
735
+ ORDER BY cost DESC
736
+ `).all();
737
+ // Per-model cost split per project (drives the stacked bars).
738
+ const modelRows = db.prepare(`
739
+ SELECT s.project_path as path, p.model as model, COALESCE(SUM(p.cost_usd), 0) as cost
740
+ FROM claude_prompts p
741
+ JOIN claude_sessions s ON s.id = p.session_id
742
+ WHERE p.model IS NOT NULL
743
+ GROUP BY s.project_path, p.model
744
+ `).all();
745
+ const byModelByPath = new Map();
746
+ for (const r of modelRows) {
747
+ const arr = byModelByPath.get(r.path) ?? [];
748
+ arr.push({ model: r.model, cost: r.cost });
749
+ byModelByPath.set(r.path, arr);
750
+ }
751
+ // Top tools per project (top 4 by call count).
752
+ const toolRows = db.prepare(`
753
+ SELECT s.project_path as path, t.tool_name as name, COUNT(*) as calls
754
+ FROM claude_tool_calls t
755
+ JOIN claude_sessions s ON s.id = t.session_id
756
+ GROUP BY s.project_path, t.tool_name
757
+ `).all();
758
+ const toolsByPath = new Map();
759
+ for (const r of toolRows) {
760
+ const arr = toolsByPath.get(r.path) ?? [];
761
+ arr.push({ name: r.name, calls: r.calls });
762
+ toolsByPath.set(r.path, arr);
763
+ }
764
+ const projects = rows.map((p) => ({
765
+ ...p,
766
+ byModel: (byModelByPath.get(p.path) ?? []).sort((a, b) => b.cost - a.cost),
767
+ topTools: (toolsByPath.get(p.path) ?? [])
768
+ .sort((a, b) => b.calls - a.calls)
769
+ .slice(0, 4)
770
+ .map((t) => t.name),
771
+ }));
772
+ return { projects };
773
+ });
774
+ /**
775
+ * Rule-based tips engine. Each rule is a SQL query that finds a wasteful
776
+ * pattern in the user's recent transcripts; the result is shaped into a
777
+ * tip card matching the design system's voice.
778
+ */
779
+ app.get('/api/claude/tips', async (_req, reply) => {
780
+ const cg = requirePrimary(reply);
781
+ if (!cg)
782
+ return;
783
+ const db = getDb(cg);
784
+ const tips = [];
785
+ // Rule 1: "you read X N times" — same file path Read more than 10 times in
786
+ // a single session.
787
+ const wastefulReads = db.prepare(`
788
+ SELECT session_id, input_summary as file, COUNT(*) as n
789
+ FROM claude_tool_calls
790
+ WHERE tool_name = 'Read' AND input_summary != ''
791
+ GROUP BY session_id, input_summary
792
+ HAVING n >= 10
793
+ ORDER BY n DESC
794
+ LIMIT 5
795
+ `).all();
796
+ for (const r of wastefulReads) {
797
+ tips.push({
798
+ id: 'wasteful_reads:' + r.session_id + ':' + r.file,
799
+ severity: 'error',
800
+ icon: 'wrench',
801
+ title: `You read ${r.file.split('/').pop()} ${r.n}× in one session — specship_explore covers it`,
802
+ why: 'Re-reading the same file burns input tokens every turn. A single structural query returns callers, callees, and linked specs at once.',
803
+ evidence: { session: r.session_id, detail: `Read(${r.file}) × ${r.n}` },
804
+ fix: `specship_explore --symbol ${r.file.replace(/\.\w+$/, '').split('/').pop()}`,
805
+ saving: '≈$0.10/read avoided',
806
+ });
807
+ }
808
+ // Rule 2: "tool returned X tokens" — any single tool call with result_length > 50000.
809
+ const heavyResults = db.prepare(`
810
+ SELECT id, session_id, tool_name, input_summary, result_length
811
+ FROM claude_tool_calls
812
+ WHERE result_length > 50000
813
+ ORDER BY result_length DESC
814
+ LIMIT 5
815
+ `).all();
816
+ for (const r of heavyResults) {
817
+ tips.push({
818
+ id: 'heavy_result:' + r.id,
819
+ severity: 'error',
820
+ icon: 'flame',
821
+ title: `${r.tool_name} returned ${Math.round(r.result_length / 1000)}k tokens — try a structural query`,
822
+ why: 'Tools that dump raw content into context are the dominant cost driver. A structural query returns just what the agent needs.',
823
+ evidence: { session: r.session_id, detail: `${r.tool_name}(${r.input_summary.slice(0, 100)}) → ${r.result_length} tokens` },
824
+ fix: r.tool_name === 'Bash' ? 'specship_search instead of Bash(grep)' : 'specship_explore on the symbol',
825
+ saving: `~$${((r.result_length / 1_000_000) * 15).toFixed(2)} on this call`,
826
+ });
827
+ }
828
+ // Rule 3: "cache miss rate" — sessions with > 10 prompts and cache_read_rate < 0.3.
829
+ const lowCache = db.prepare(`
830
+ 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
831
+ FROM claude_sessions s
832
+ WHERE s.prompt_count >= 10
833
+ AND (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens) > 0
834
+ ORDER BY (CAST(s.total_cache_read_tokens AS REAL) / (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens)) ASC
835
+ LIMIT 3
836
+ `).all();
837
+ for (const r of lowCache) {
838
+ const total = r.ti + r.cw + r.cr;
839
+ const rate = total > 0 ? r.cr / total : 0;
840
+ if (rate >= 0.3)
841
+ continue;
842
+ tips.push({
843
+ id: 'low_cache:' + r.id,
844
+ severity: 'warn',
845
+ icon: 'database',
846
+ title: `Cache read rate is ${Math.round(rate * 100)}% on a ${r.prompt_count}-prompt session`,
847
+ 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.',
848
+ evidence: { session: r.id, detail: `cache_read=${(r.cr / 1_000_000).toFixed(2)}M / total=${(total / 1_000_000).toFixed(2)}M` },
849
+ fix: 'Pin a stable system-prompt prefix in .claude/settings.json',
850
+ saving: '~$X.XX / session (model-dependent)',
851
+ });
852
+ }
853
+ // Sort: errors before warns before info, then within bucket by saving heuristic.
854
+ const order = { error: 0, warn: 1, info: 2 };
855
+ tips.sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9));
856
+ return { tips };
857
+ });
858
+ /**
859
+ * Force a one-shot ingest pass. Useful for "Refresh" button in the UI.
860
+ */
861
+ app.post('/api/claude/ingest', async () => {
862
+ const watcher = app.watcher;
863
+ if (!watcher)
864
+ return { ok: false, error: 'watcher not running' };
865
+ const stats = watcher.ingestNow();
866
+ return { ok: true, stats };
867
+ });
868
+ }