lattice-orchestrator 0.7.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 (440) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +58 -0
  3. package/config/logrotate.conf +15 -0
  4. package/dist/cli-parser.d.ts +11 -0
  5. package/dist/cli-parser.d.ts.map +1 -0
  6. package/dist/cli-parser.js +48 -0
  7. package/dist/cli-parser.js.map +1 -0
  8. package/dist/lattice-server.d.ts +70 -0
  9. package/dist/lattice-server.d.ts.map +1 -0
  10. package/dist/lattice-server.js +969 -0
  11. package/dist/lattice-server.js.map +1 -0
  12. package/dist/mcp-server/index.d.ts +3 -0
  13. package/dist/mcp-server/index.d.ts.map +1 -0
  14. package/dist/mcp-server/index.js +190 -0
  15. package/dist/mcp-server/index.js.map +1 -0
  16. package/dist/mcp-server/lattice-tools.d.ts +15 -0
  17. package/dist/mcp-server/lattice-tools.d.ts.map +1 -0
  18. package/dist/mcp-server/lattice-tools.js +366 -0
  19. package/dist/mcp-server/lattice-tools.js.map +1 -0
  20. package/dist/middleware/cors-setup.d.ts +7 -0
  21. package/dist/middleware/cors-setup.d.ts.map +1 -0
  22. package/dist/middleware/cors-setup.js +8 -0
  23. package/dist/middleware/cors-setup.js.map +1 -0
  24. package/dist/middleware/error-handler.d.ts +4 -0
  25. package/dist/middleware/error-handler.d.ts.map +1 -0
  26. package/dist/middleware/error-handler.js +27 -0
  27. package/dist/middleware/error-handler.js.map +1 -0
  28. package/dist/middleware/query-parser.d.ts +11 -0
  29. package/dist/middleware/query-parser.d.ts.map +1 -0
  30. package/dist/middleware/query-parser.js +68 -0
  31. package/dist/middleware/query-parser.js.map +1 -0
  32. package/dist/middleware/request-logger.d.ts +4 -0
  33. package/dist/middleware/request-logger.d.ts.map +1 -0
  34. package/dist/middleware/request-logger.js +6 -0
  35. package/dist/middleware/request-logger.js.map +1 -0
  36. package/dist/process-daemon/index.d.ts +14 -0
  37. package/dist/process-daemon/index.d.ts.map +1 -0
  38. package/dist/process-daemon/index.js +51 -0
  39. package/dist/process-daemon/index.js.map +1 -0
  40. package/dist/process-daemon/process-daemon.d.ts +101 -0
  41. package/dist/process-daemon/process-daemon.d.ts.map +1 -0
  42. package/dist/process-daemon/process-daemon.js +846 -0
  43. package/dist/process-daemon/process-daemon.js.map +1 -0
  44. package/dist/process-daemon/process-manager-client.d.ts +123 -0
  45. package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
  46. package/dist/process-daemon/process-manager-client.js +329 -0
  47. package/dist/process-daemon/process-manager-client.js.map +1 -0
  48. package/dist/process-daemon/process-manager-interface.d.ts +61 -0
  49. package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
  50. package/dist/process-daemon/process-manager-interface.js +8 -0
  51. package/dist/process-daemon/process-manager-interface.js.map +1 -0
  52. package/dist/process-daemon/test-daemon.d.ts +12 -0
  53. package/dist/process-daemon/test-daemon.d.ts.map +1 -0
  54. package/dist/process-daemon/test-daemon.js +81 -0
  55. package/dist/process-daemon/test-daemon.js.map +1 -0
  56. package/dist/process-daemon/types.d.ts +97 -0
  57. package/dist/process-daemon/types.d.ts.map +1 -0
  58. package/dist/process-daemon/types.js +8 -0
  59. package/dist/process-daemon/types.js.map +1 -0
  60. package/dist/routes/analysis.routes.d.ts +13 -0
  61. package/dist/routes/analysis.routes.d.ts.map +1 -0
  62. package/dist/routes/analysis.routes.js +520 -0
  63. package/dist/routes/analysis.routes.js.map +1 -0
  64. package/dist/routes/config.routes.d.ts +4 -0
  65. package/dist/routes/config.routes.d.ts.map +1 -0
  66. package/dist/routes/config.routes.js +27 -0
  67. package/dist/routes/config.routes.js.map +1 -0
  68. package/dist/routes/conversation.routes.d.ts +43 -0
  69. package/dist/routes/conversation.routes.d.ts.map +1 -0
  70. package/dist/routes/conversation.routes.js +79 -0
  71. package/dist/routes/conversation.routes.js.map +1 -0
  72. package/dist/routes/filesystem.routes.d.ts +4 -0
  73. package/dist/routes/filesystem.routes.d.ts.map +1 -0
  74. package/dist/routes/filesystem.routes.js +86 -0
  75. package/dist/routes/filesystem.routes.js.map +1 -0
  76. package/dist/routes/insights.routes.d.ts +17 -0
  77. package/dist/routes/insights.routes.d.ts.map +1 -0
  78. package/dist/routes/insights.routes.js +633 -0
  79. package/dist/routes/insights.routes.js.map +1 -0
  80. package/dist/routes/lattice.routes.d.ts +10 -0
  81. package/dist/routes/lattice.routes.d.ts.map +1 -0
  82. package/dist/routes/lattice.routes.js +123 -0
  83. package/dist/routes/lattice.routes.js.map +1 -0
  84. package/dist/routes/license.routes.d.ts +3 -0
  85. package/dist/routes/license.routes.d.ts.map +1 -0
  86. package/dist/routes/license.routes.js +95 -0
  87. package/dist/routes/license.routes.js.map +1 -0
  88. package/dist/routes/log.routes.d.ts +3 -0
  89. package/dist/routes/log.routes.d.ts.map +1 -0
  90. package/dist/routes/log.routes.js +184 -0
  91. package/dist/routes/log.routes.js.map +1 -0
  92. package/dist/routes/pending-question.routes.d.ts +9 -0
  93. package/dist/routes/pending-question.routes.d.ts.map +1 -0
  94. package/dist/routes/pending-question.routes.js +162 -0
  95. package/dist/routes/pending-question.routes.js.map +1 -0
  96. package/dist/routes/permission.routes.d.ts +18 -0
  97. package/dist/routes/permission.routes.d.ts.map +1 -0
  98. package/dist/routes/permission.routes.js +370 -0
  99. package/dist/routes/permission.routes.js.map +1 -0
  100. package/dist/routes/process-events.routes.d.ts +9 -0
  101. package/dist/routes/process-events.routes.d.ts.map +1 -0
  102. package/dist/routes/process-events.routes.js +141 -0
  103. package/dist/routes/process-events.routes.js.map +1 -0
  104. package/dist/routes/prototype.routes.d.ts +9 -0
  105. package/dist/routes/prototype.routes.d.ts.map +1 -0
  106. package/dist/routes/prototype.routes.js +757 -0
  107. package/dist/routes/prototype.routes.js.map +1 -0
  108. package/dist/routes/question.routes.d.ts +8 -0
  109. package/dist/routes/question.routes.d.ts.map +1 -0
  110. package/dist/routes/question.routes.js +83 -0
  111. package/dist/routes/question.routes.js.map +1 -0
  112. package/dist/routes/session-control.routes.d.ts +29 -0
  113. package/dist/routes/session-control.routes.d.ts.map +1 -0
  114. package/dist/routes/session-control.routes.js +455 -0
  115. package/dist/routes/session-control.routes.js.map +1 -0
  116. package/dist/routes/session-lifecycle.routes.d.ts +21 -0
  117. package/dist/routes/session-lifecycle.routes.d.ts.map +1 -0
  118. package/dist/routes/session-lifecycle.routes.js +256 -0
  119. package/dist/routes/session-lifecycle.routes.js.map +1 -0
  120. package/dist/routes/session-query.routes.d.ts +25 -0
  121. package/dist/routes/session-query.routes.d.ts.map +1 -0
  122. package/dist/routes/session-query.routes.js +363 -0
  123. package/dist/routes/session-query.routes.js.map +1 -0
  124. package/dist/routes/session-stream.routes.d.ts +21 -0
  125. package/dist/routes/session-stream.routes.d.ts.map +1 -0
  126. package/dist/routes/session-stream.routes.js +235 -0
  127. package/dist/routes/session-stream.routes.js.map +1 -0
  128. package/dist/routes/streaming.routes.d.ts +4 -0
  129. package/dist/routes/streaming.routes.d.ts.map +1 -0
  130. package/dist/routes/streaming.routes.js +33 -0
  131. package/dist/routes/streaming.routes.js.map +1 -0
  132. package/dist/routes/system.routes.d.ts +7 -0
  133. package/dist/routes/system.routes.d.ts.map +1 -0
  134. package/dist/routes/system.routes.js +214 -0
  135. package/dist/routes/system.routes.js.map +1 -0
  136. package/dist/routes/walkthrough.routes.d.ts +19 -0
  137. package/dist/routes/walkthrough.routes.d.ts.map +1 -0
  138. package/dist/routes/walkthrough.routes.js +688 -0
  139. package/dist/routes/walkthrough.routes.js.map +1 -0
  140. package/dist/routes/working-directories.routes.d.ts +4 -0
  141. package/dist/routes/working-directories.routes.d.ts.map +1 -0
  142. package/dist/routes/working-directories.routes.js +25 -0
  143. package/dist/routes/working-directories.routes.js.map +1 -0
  144. package/dist/server.d.ts +3 -0
  145. package/dist/server.d.ts.map +1 -0
  146. package/dist/server.js +34 -0
  147. package/dist/server.js.map +1 -0
  148. package/dist/services/ToolMetricsService.d.ts +53 -0
  149. package/dist/services/ToolMetricsService.d.ts.map +1 -0
  150. package/dist/services/ToolMetricsService.js +230 -0
  151. package/dist/services/ToolMetricsService.js.map +1 -0
  152. package/dist/services/claude-router-service.d.ts +19 -0
  153. package/dist/services/claude-router-service.d.ts.map +1 -0
  154. package/dist/services/claude-router-service.js +160 -0
  155. package/dist/services/claude-router-service.js.map +1 -0
  156. package/dist/services/commands-service.d.ts +20 -0
  157. package/dist/services/commands-service.d.ts.map +1 -0
  158. package/dist/services/commands-service.js +115 -0
  159. package/dist/services/commands-service.js.map +1 -0
  160. package/dist/services/connection-debug-logger.d.ts +85 -0
  161. package/dist/services/connection-debug-logger.d.ts.map +1 -0
  162. package/dist/services/connection-debug-logger.js +221 -0
  163. package/dist/services/connection-debug-logger.js.map +1 -0
  164. package/dist/services/debug-log.d.ts +6 -0
  165. package/dist/services/debug-log.d.ts.map +1 -0
  166. package/dist/services/debug-log.js +27 -0
  167. package/dist/services/debug-log.js.map +1 -0
  168. package/dist/services/gemini-service.d.ts +35 -0
  169. package/dist/services/gemini-service.d.ts.map +1 -0
  170. package/dist/services/gemini-service.js +256 -0
  171. package/dist/services/gemini-service.js.map +1 -0
  172. package/dist/services/infrastructure/config-service.d.ts +79 -0
  173. package/dist/services/infrastructure/config-service.d.ts.map +1 -0
  174. package/dist/services/infrastructure/config-service.js +431 -0
  175. package/dist/services/infrastructure/config-service.js.map +1 -0
  176. package/dist/services/infrastructure/cost-tracker.d.ts +112 -0
  177. package/dist/services/infrastructure/cost-tracker.d.ts.map +1 -0
  178. package/dist/services/infrastructure/cost-tracker.js +423 -0
  179. package/dist/services/infrastructure/cost-tracker.js.map +1 -0
  180. package/dist/services/infrastructure/file-system-service.d.ts +61 -0
  181. package/dist/services/infrastructure/file-system-service.d.ts.map +1 -0
  182. package/dist/services/infrastructure/file-system-service.js +348 -0
  183. package/dist/services/infrastructure/file-system-service.js.map +1 -0
  184. package/dist/services/infrastructure/log-formatter.d.ts +5 -0
  185. package/dist/services/infrastructure/log-formatter.d.ts.map +1 -0
  186. package/dist/services/infrastructure/log-formatter.js +77 -0
  187. package/dist/services/infrastructure/log-formatter.js.map +1 -0
  188. package/dist/services/infrastructure/log-stream-buffer.d.ts +11 -0
  189. package/dist/services/infrastructure/log-stream-buffer.d.ts.map +1 -0
  190. package/dist/services/infrastructure/log-stream-buffer.js +36 -0
  191. package/dist/services/infrastructure/log-stream-buffer.js.map +1 -0
  192. package/dist/services/infrastructure/logger.d.ts +71 -0
  193. package/dist/services/infrastructure/logger.d.ts.map +1 -0
  194. package/dist/services/infrastructure/logger.js +215 -0
  195. package/dist/services/infrastructure/logger.js.map +1 -0
  196. package/dist/services/infrastructure/service-registry.d.ts +86 -0
  197. package/dist/services/infrastructure/service-registry.d.ts.map +1 -0
  198. package/dist/services/infrastructure/service-registry.js +162 -0
  199. package/dist/services/infrastructure/service-registry.js.map +1 -0
  200. package/dist/services/infrastructure/stream-manager.d.ts +87 -0
  201. package/dist/services/infrastructure/stream-manager.d.ts.map +1 -0
  202. package/dist/services/infrastructure/stream-manager.js +436 -0
  203. package/dist/services/infrastructure/stream-manager.js.map +1 -0
  204. package/dist/services/infrastructure/timing.d.ts +72 -0
  205. package/dist/services/infrastructure/timing.d.ts.map +1 -0
  206. package/dist/services/infrastructure/timing.js +115 -0
  207. package/dist/services/infrastructure/timing.js.map +1 -0
  208. package/dist/services/insights/anthropic-service.d.ts +224 -0
  209. package/dist/services/insights/anthropic-service.d.ts.map +1 -0
  210. package/dist/services/insights/anthropic-service.js +1062 -0
  211. package/dist/services/insights/anthropic-service.js.map +1 -0
  212. package/dist/services/insights/insight-audit-repository.d.ts +119 -0
  213. package/dist/services/insights/insight-audit-repository.d.ts.map +1 -0
  214. package/dist/services/insights/insight-audit-repository.js +242 -0
  215. package/dist/services/insights/insight-audit-repository.js.map +1 -0
  216. package/dist/services/insights/insight-queue.d.ts +99 -0
  217. package/dist/services/insights/insight-queue.d.ts.map +1 -0
  218. package/dist/services/insights/insight-queue.js +277 -0
  219. package/dist/services/insights/insight-queue.js.map +1 -0
  220. package/dist/services/insights/insights-computer.d.ts +132 -0
  221. package/dist/services/insights/insights-computer.d.ts.map +1 -0
  222. package/dist/services/insights/insights-computer.js +936 -0
  223. package/dist/services/insights/insights-computer.js.map +1 -0
  224. package/dist/services/insights/insights-coordinator.d.ts +165 -0
  225. package/dist/services/insights/insights-coordinator.d.ts.map +1 -0
  226. package/dist/services/insights/insights-coordinator.js +1678 -0
  227. package/dist/services/insights/insights-coordinator.js.map +1 -0
  228. package/dist/services/insights/insights-event-log.d.ts +196 -0
  229. package/dist/services/insights/insights-event-log.d.ts.map +1 -0
  230. package/dist/services/insights/insights-event-log.js +319 -0
  231. package/dist/services/insights/insights-event-log.js.map +1 -0
  232. package/dist/services/lattice-service.d.ts +77 -0
  233. package/dist/services/lattice-service.d.ts.map +1 -0
  234. package/dist/services/lattice-service.js +195 -0
  235. package/dist/services/lattice-service.js.map +1 -0
  236. package/dist/services/license-service.d.ts +69 -0
  237. package/dist/services/license-service.d.ts.map +1 -0
  238. package/dist/services/license-service.js +330 -0
  239. package/dist/services/license-service.js.map +1 -0
  240. package/dist/services/mcp-config-generator.d.ts +32 -0
  241. package/dist/services/mcp-config-generator.d.ts.map +1 -0
  242. package/dist/services/mcp-config-generator.js +126 -0
  243. package/dist/services/mcp-config-generator.js.map +1 -0
  244. package/dist/services/message-filter.d.ts +22 -0
  245. package/dist/services/message-filter.d.ts.map +1 -0
  246. package/dist/services/message-filter.js +57 -0
  247. package/dist/services/message-filter.js.map +1 -0
  248. package/dist/services/notification-service.d.ts +45 -0
  249. package/dist/services/notification-service.d.ts.map +1 -0
  250. package/dist/services/notification-service.js +184 -0
  251. package/dist/services/notification-service.js.map +1 -0
  252. package/dist/services/pending-question-service.d.ts +97 -0
  253. package/dist/services/pending-question-service.d.ts.map +1 -0
  254. package/dist/services/pending-question-service.js +223 -0
  255. package/dist/services/pending-question-service.js.map +1 -0
  256. package/dist/services/permission-event-log.d.ts +136 -0
  257. package/dist/services/permission-event-log.d.ts.map +1 -0
  258. package/dist/services/permission-event-log.js +252 -0
  259. package/dist/services/permission-event-log.js.map +1 -0
  260. package/dist/services/permission-pattern-matcher.d.ts +82 -0
  261. package/dist/services/permission-pattern-matcher.d.ts.map +1 -0
  262. package/dist/services/permission-pattern-matcher.js +294 -0
  263. package/dist/services/permission-pattern-matcher.js.map +1 -0
  264. package/dist/services/permission-tracker.d.ts +67 -0
  265. package/dist/services/permission-tracker.d.ts.map +1 -0
  266. package/dist/services/permission-tracker.js +162 -0
  267. package/dist/services/permission-tracker.js.map +1 -0
  268. package/dist/services/process/claude-process-manager.d.ts +142 -0
  269. package/dist/services/process/claude-process-manager.d.ts.map +1 -0
  270. package/dist/services/process/claude-process-manager.js +1218 -0
  271. package/dist/services/process/claude-process-manager.js.map +1 -0
  272. package/dist/services/process/conversation-status-manager.d.ts +110 -0
  273. package/dist/services/process/conversation-status-manager.d.ts.map +1 -0
  274. package/dist/services/process/conversation-status-manager.js +349 -0
  275. package/dist/services/process/conversation-status-manager.js.map +1 -0
  276. package/dist/services/process/json-lines-parser.d.ts +19 -0
  277. package/dist/services/process/json-lines-parser.d.ts.map +1 -0
  278. package/dist/services/process/json-lines-parser.js +59 -0
  279. package/dist/services/process/json-lines-parser.js.map +1 -0
  280. package/dist/services/process/process-event-log.d.ts +263 -0
  281. package/dist/services/process/process-event-log.d.ts.map +1 -0
  282. package/dist/services/process/process-event-log.js +509 -0
  283. package/dist/services/process/process-event-log.js.map +1 -0
  284. package/dist/services/process/process-manager-factory.d.ts +109 -0
  285. package/dist/services/process/process-manager-factory.d.ts.map +1 -0
  286. package/dist/services/process/process-manager-factory.js +338 -0
  287. package/dist/services/process/process-manager-factory.js.map +1 -0
  288. package/dist/services/question-tracker.d.ts +51 -0
  289. package/dist/services/question-tracker.d.ts.map +1 -0
  290. package/dist/services/question-tracker.js +111 -0
  291. package/dist/services/question-tracker.js.map +1 -0
  292. package/dist/services/sessions/claude-history-reader.d.ts +139 -0
  293. package/dist/services/sessions/claude-history-reader.d.ts.map +1 -0
  294. package/dist/services/sessions/claude-history-reader.js +864 -0
  295. package/dist/services/sessions/claude-history-reader.js.map +1 -0
  296. package/dist/services/sessions/conversation-cache.d.ts +98 -0
  297. package/dist/services/sessions/conversation-cache.d.ts.map +1 -0
  298. package/dist/services/sessions/conversation-cache.js +329 -0
  299. package/dist/services/sessions/conversation-cache.js.map +1 -0
  300. package/dist/services/sessions/session-activity-watcher.d.ts +67 -0
  301. package/dist/services/sessions/session-activity-watcher.d.ts.map +1 -0
  302. package/dist/services/sessions/session-activity-watcher.js +236 -0
  303. package/dist/services/sessions/session-activity-watcher.js.map +1 -0
  304. package/dist/services/sessions/session-analysis-service.d.ts +72 -0
  305. package/dist/services/sessions/session-analysis-service.d.ts.map +1 -0
  306. package/dist/services/sessions/session-analysis-service.js +373 -0
  307. package/dist/services/sessions/session-analysis-service.js.map +1 -0
  308. package/dist/services/sessions/session-branch-service.d.ts +76 -0
  309. package/dist/services/sessions/session-branch-service.d.ts.map +1 -0
  310. package/dist/services/sessions/session-branch-service.js +355 -0
  311. package/dist/services/sessions/session-branch-service.js.map +1 -0
  312. package/dist/services/sessions/session-info-service.d.ts +455 -0
  313. package/dist/services/sessions/session-info-service.d.ts.map +1 -0
  314. package/dist/services/sessions/session-info-service.js +1640 -0
  315. package/dist/services/sessions/session-info-service.js.map +1 -0
  316. package/dist/services/sessions/session-marks-repository.d.ts +78 -0
  317. package/dist/services/sessions/session-marks-repository.d.ts.map +1 -0
  318. package/dist/services/sessions/session-marks-repository.js +263 -0
  319. package/dist/services/sessions/session-marks-repository.js.map +1 -0
  320. package/dist/services/sessions/session-marks-service.d.ts +137 -0
  321. package/dist/services/sessions/session-marks-service.d.ts.map +1 -0
  322. package/dist/services/sessions/session-marks-service.js +562 -0
  323. package/dist/services/sessions/session-marks-service.js.map +1 -0
  324. package/dist/services/sessions/session-review-service.d.ts +98 -0
  325. package/dist/services/sessions/session-review-service.d.ts.map +1 -0
  326. package/dist/services/sessions/session-review-service.js +629 -0
  327. package/dist/services/sessions/session-review-service.js.map +1 -0
  328. package/dist/services/sessions/turn-capture-service.d.ts +83 -0
  329. package/dist/services/sessions/turn-capture-service.d.ts.map +1 -0
  330. package/dist/services/sessions/turn-capture-service.js +477 -0
  331. package/dist/services/sessions/turn-capture-service.js.map +1 -0
  332. package/dist/services/sessions/turn-repository.d.ts +48 -0
  333. package/dist/services/sessions/turn-repository.d.ts.map +1 -0
  334. package/dist/services/sessions/turn-repository.js +103 -0
  335. package/dist/services/sessions/turn-repository.js.map +1 -0
  336. package/dist/services/walkthrough-service.d.ts +226 -0
  337. package/dist/services/walkthrough-service.d.ts.map +1 -0
  338. package/dist/services/walkthrough-service.js +1112 -0
  339. package/dist/services/walkthrough-service.js.map +1 -0
  340. package/dist/services/walkthrough-skill-prompt.d.ts +34 -0
  341. package/dist/services/walkthrough-skill-prompt.d.ts.map +1 -0
  342. package/dist/services/walkthrough-skill-prompt.js +313 -0
  343. package/dist/services/walkthrough-skill-prompt.js.map +1 -0
  344. package/dist/services/web-push-service.d.ts +48 -0
  345. package/dist/services/web-push-service.d.ts.map +1 -0
  346. package/dist/services/web-push-service.js +186 -0
  347. package/dist/services/web-push-service.js.map +1 -0
  348. package/dist/services/working-directories-service.d.ts +19 -0
  349. package/dist/services/working-directories-service.d.ts.map +1 -0
  350. package/dist/services/working-directories-service.js +103 -0
  351. package/dist/services/working-directories-service.js.map +1 -0
  352. package/dist/types/config.d.ts +122 -0
  353. package/dist/types/config.d.ts.map +1 -0
  354. package/dist/types/config.js +21 -0
  355. package/dist/types/config.js.map +1 -0
  356. package/dist/types/express.d.ts +5 -0
  357. package/dist/types/express.d.ts.map +1 -0
  358. package/dist/types/express.js +2 -0
  359. package/dist/types/express.js.map +1 -0
  360. package/dist/types/index.d.ts +400 -0
  361. package/dist/types/index.d.ts.map +1 -0
  362. package/dist/types/index.js +41 -0
  363. package/dist/types/index.js.map +1 -0
  364. package/dist/types/insights.d.ts +176 -0
  365. package/dist/types/insights.d.ts.map +1 -0
  366. package/dist/types/insights.js +23 -0
  367. package/dist/types/insights.js.map +1 -0
  368. package/dist/types/license.d.ts +70 -0
  369. package/dist/types/license.d.ts.map +1 -0
  370. package/dist/types/license.js +5 -0
  371. package/dist/types/license.js.map +1 -0
  372. package/dist/types/router-config.d.ts +13 -0
  373. package/dist/types/router-config.d.ts.map +1 -0
  374. package/dist/types/router-config.js +2 -0
  375. package/dist/types/router-config.js.map +1 -0
  376. package/dist/utils/constants.d.ts +26 -0
  377. package/dist/utils/constants.d.ts.map +1 -0
  378. package/dist/utils/constants.js +28 -0
  379. package/dist/utils/constants.js.map +1 -0
  380. package/dist/utils/machine-id.d.ts +7 -0
  381. package/dist/utils/machine-id.d.ts.map +1 -0
  382. package/dist/utils/machine-id.js +76 -0
  383. package/dist/utils/machine-id.js.map +1 -0
  384. package/dist/utils/server-startup.d.ts +11 -0
  385. package/dist/utils/server-startup.d.ts.map +1 -0
  386. package/dist/utils/server-startup.js +9 -0
  387. package/dist/utils/server-startup.js.map +1 -0
  388. package/dist/utils/update-check.d.ts +13 -0
  389. package/dist/utils/update-check.d.ts.map +1 -0
  390. package/dist/utils/update-check.js +90 -0
  391. package/dist/utils/update-check.js.map +1 -0
  392. package/dist/web/assets/ArchivedCardPrototype-S9ifiasa.js +5 -0
  393. package/dist/web/assets/BannerGallery-B__rJV6F.js +1 -0
  394. package/dist/web/assets/BannerPrototype-DBKP9Uiu.js +52 -0
  395. package/dist/web/assets/CodeHikeExperiment-B8jjWAFy.js +15 -0
  396. package/dist/web/assets/ContextTooltipVariations-DzklAFam.js +1 -0
  397. package/dist/web/assets/FontShowcasePrototype-KIMEWeP2.js +13 -0
  398. package/dist/web/assets/GeometricGallery-DddlWhHK.js +1 -0
  399. package/dist/web/assets/HistoryWalkthroughPrototype-DeniRRdw.js +18 -0
  400. package/dist/web/assets/InlineWalkthroughPrototype-Csd5r_Hk.js +1 -0
  401. package/dist/web/assets/MarkButtonPrototype-CxhxE0RP.js +1 -0
  402. package/dist/web/assets/MenuStylesPrototype-D7neA6YM.js +1 -0
  403. package/dist/web/assets/MomentCardVariations-2GT7GyFn.js +1 -0
  404. package/dist/web/assets/MomentHeaderVariations-DhGEw4XC.js +1 -0
  405. package/dist/web/assets/NarrativeWalkthroughDemo-B5C566fu.js +389 -0
  406. package/dist/web/assets/OutcomeVariations-BrZfsVcs.js +1 -0
  407. package/dist/web/assets/PermissionPatternPickerPrototype-CBOhe2Me.js +1 -0
  408. package/dist/web/assets/PermissionPrototype-BcF-a5an.js +1 -0
  409. package/dist/web/assets/PipelineGallery-BJhyM0rx.js +1 -0
  410. package/dist/web/assets/ScopeHeaderPrototype-GD1HNfaO.js +1 -0
  411. package/dist/web/assets/ScopeHeaderStylesPrototype-aa4uNJJ1.js +1 -0
  412. package/dist/web/assets/ScrollycodingPrototype-CKW1bf72.js +70 -0
  413. package/dist/web/assets/SectionHeaderVariations-DM8vUwfj.js +1 -0
  414. package/dist/web/assets/SemanticGallery-CtQEo7St.js +1 -0
  415. package/dist/web/assets/SessionCardPrototype-CgHCIMHe.js +1 -0
  416. package/dist/web/assets/SessionSidebarVariations-DMQL3Azj.js +3 -0
  417. package/dist/web/assets/SessionStartPrototype-Cwsv01Rr.js +1 -0
  418. package/dist/web/assets/SmartMenuPrototype-Br37Qbs_.js +1 -0
  419. package/dist/web/assets/StyleGallery-rZgrploB.js +1 -0
  420. package/dist/web/assets/TimelineCardPrototype-lzPc5mhe.js +19 -0
  421. package/dist/web/assets/ToolbarPrototype-Dm4BNZra.js +1 -0
  422. package/dist/web/assets/TooltipExperiment-Dy8QzTIP.js +13 -0
  423. package/dist/web/assets/WalkthroughCTAPrototype-uHoovujd.js +1 -0
  424. package/dist/web/assets/WalkthroughHeaderVariations-Do7Di1g1.js +1 -0
  425. package/dist/web/assets/WalkthroughShowcase-sGmRoPoM.js +112 -0
  426. package/dist/web/assets/arrow-right-D46Nx1mC.js +1 -0
  427. package/dist/web/assets/brain-BXIZKtOZ.js +1 -0
  428. package/dist/web/assets/grid-3x3-Cb81B62m.js +1 -0
  429. package/dist/web/assets/main-B1fyog77.js +321 -0
  430. package/dist/web/assets/main-C2PK2Klg.css +1 -0
  431. package/dist/web/assets/semantic-variations-Bd-W7ea2.js +1 -0
  432. package/dist/web/assets/target-Cf92wDTW.js +1 -0
  433. package/dist/web/favicon.png +0 -0
  434. package/dist/web/favicon.svg +22 -0
  435. package/dist/web/icon-192x192.png +0 -0
  436. package/dist/web/icon-512x512.png +0 -0
  437. package/dist/web/index.html +45 -0
  438. package/dist/web/manifest.json +61 -0
  439. package/package.json +192 -0
  440. package/scripts/postinstall.js +60 -0
@@ -0,0 +1,1112 @@
1
+ /**
2
+ * WalkthroughService - Generates AI-narrated walkthroughs from session data
3
+ *
4
+ * Extracts Edit/Write tool calls from a session and uses Claude to create
5
+ * a narrative explanation of the changes with Code Hike syntax highlighting.
6
+ */
7
+ import Anthropic from '@anthropic-ai/sdk';
8
+ import { highlight } from 'codehike/code';
9
+ import * as Diff from 'diff';
10
+ import { SessionInfoService } from './sessions/session-info-service.js';
11
+ import { ConfigService } from './infrastructure/config-service.js';
12
+ import { createLogger } from './infrastructure/logger.js';
13
+ const logger = createLogger('WalkthroughService');
14
+ /**
15
+ * Validate a generated walkthrough for quality and completeness.
16
+ * Returns errors (must fix) and warnings (should consider).
17
+ */
18
+ export function validateWalkthrough(walkthrough) {
19
+ const errors = [];
20
+ const warnings = [];
21
+ // Type guard
22
+ if (!walkthrough || typeof walkthrough !== 'object') {
23
+ return { passed: false, errors: ['Walkthrough is not a valid object'], warnings: [] };
24
+ }
25
+ const wt = walkthrough;
26
+ // Required top-level fields
27
+ if (!wt.title || typeof wt.title !== 'string') {
28
+ errors.push('Missing or invalid title');
29
+ }
30
+ if (!wt.summary || typeof wt.summary !== 'string') {
31
+ errors.push('Missing or invalid summary');
32
+ }
33
+ if (!wt.phases || !Array.isArray(wt.phases)) {
34
+ errors.push('Missing or invalid phases array');
35
+ return { passed: false, errors, warnings };
36
+ }
37
+ // Count code blocks and collect files with code
38
+ const filesWithCode = new Set();
39
+ let totalCodeBlocks = 0;
40
+ let codeBlocksMissingLinkedPhrases = 0;
41
+ let codeBlocksMissingContext = 0;
42
+ for (const phase of wt.phases) {
43
+ if (!phase.name || typeof phase.name !== 'string') {
44
+ errors.push('Phase missing name');
45
+ continue;
46
+ }
47
+ if (!phase.keyMoments || !Array.isArray(phase.keyMoments)) {
48
+ warnings.push(`Phase "${phase.name}" has no keyMoments`);
49
+ continue;
50
+ }
51
+ for (const moment of phase.keyMoments) {
52
+ if (!moment.code || !Array.isArray(moment.code))
53
+ continue;
54
+ for (const code of moment.code) {
55
+ totalCodeBlocks++;
56
+ const file = code.file;
57
+ if (file)
58
+ filesWithCode.add(file);
59
+ // Check required fields
60
+ if (!code.linkedPhrases || !Array.isArray(code.linkedPhrases) || code.linkedPhrases.length === 0) {
61
+ codeBlocksMissingLinkedPhrases++;
62
+ }
63
+ if (!code.contextBefore && !code.contextAfter) {
64
+ codeBlocksMissingContext++;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ // Minimum code block requirement
70
+ if (totalCodeBlocks < 5) {
71
+ errors.push(`Only ${totalCodeBlocks} code blocks - need at least 5 to show meaningful changes`);
72
+ }
73
+ // Check linkedPhrases coverage
74
+ if (codeBlocksMissingLinkedPhrases > 0) {
75
+ const pct = Math.round((codeBlocksMissingLinkedPhrases / totalCodeBlocks) * 100);
76
+ if (pct > 50) {
77
+ errors.push(`${codeBlocksMissingLinkedPhrases}/${totalCodeBlocks} code blocks (${pct}%) missing linkedPhrases`);
78
+ }
79
+ else if (pct > 20) {
80
+ warnings.push(`${codeBlocksMissingLinkedPhrases}/${totalCodeBlocks} code blocks missing linkedPhrases`);
81
+ }
82
+ }
83
+ // Check context coverage
84
+ if (codeBlocksMissingContext > 0) {
85
+ const pct = Math.round((codeBlocksMissingContext / totalCodeBlocks) * 100);
86
+ if (pct > 50) {
87
+ warnings.push(`${codeBlocksMissingContext}/${totalCodeBlocks} code blocks missing context`);
88
+ }
89
+ }
90
+ // Check outcome section
91
+ const outcome = wt.outcome;
92
+ if (!outcome) {
93
+ errors.push('Missing outcome section');
94
+ }
95
+ else {
96
+ const filesChanged = outcome.filesChanged;
97
+ if (!filesChanged || !Array.isArray(filesChanged) || filesChanged.length === 0) {
98
+ errors.push('outcome.filesChanged is empty or missing');
99
+ }
100
+ else {
101
+ // Check that files in filesChanged have code blocks
102
+ const missingCodeFiles = [];
103
+ for (const file of filesChanged) {
104
+ // Check both full path and just filename
105
+ const hasCode = filesWithCode.has(file) ||
106
+ Array.from(filesWithCode).some(f => f.endsWith(file) || file.endsWith(f.split('/').pop() || ''));
107
+ if (!hasCode) {
108
+ missingCodeFiles.push(file);
109
+ }
110
+ }
111
+ if (missingCodeFiles.length > 0) {
112
+ const pct = Math.round((missingCodeFiles.length / filesChanged.length) * 100);
113
+ if (pct > 30) {
114
+ warnings.push(`${missingCodeFiles.length}/${filesChanged.length} files in filesChanged have no code blocks: ${missingCodeFiles.slice(0, 3).join(', ')}${missingCodeFiles.length > 3 ? '...' : ''}`);
115
+ }
116
+ }
117
+ }
118
+ // Check that linesAdded/linesRemoved are reasonable (not obviously fake)
119
+ const linesAdded = outcome.linesAdded;
120
+ const linesRemoved = outcome.linesRemoved;
121
+ if (linesAdded !== undefined && linesRemoved !== undefined) {
122
+ // If claimed stats are very round numbers, flag as potentially fake
123
+ if (linesAdded % 100 === 0 && linesRemoved % 100 === 0 && linesAdded > 500) {
124
+ warnings.push(`Line stats (${linesAdded}/${linesRemoved}) are suspiciously round - verify accuracy`);
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ passed: errors.length === 0,
130
+ errors,
131
+ warnings,
132
+ };
133
+ }
134
+ /**
135
+ * Fix linkedPhrase line numbers by finding where phrases actually appear in code.
136
+ * LLMs often produce off-by-one errors when counting lines.
137
+ * This function scans the code and corrects the line numbers.
138
+ */
139
+ export function fixLinkedPhraseLines(walkthrough) {
140
+ if (!walkthrough || typeof walkthrough !== 'object')
141
+ return walkthrough;
142
+ const wt = walkthrough;
143
+ if (!wt.phases || !Array.isArray(wt.phases))
144
+ return walkthrough;
145
+ for (const phase of wt.phases) {
146
+ if (!phase.keyMoments || !Array.isArray(phase.keyMoments))
147
+ continue;
148
+ for (const moment of phase.keyMoments) {
149
+ if (!moment.code || !Array.isArray(moment.code))
150
+ continue;
151
+ for (const code of moment.code) {
152
+ const linkedPhrases = code.linkedPhrases;
153
+ if (!linkedPhrases || !Array.isArray(linkedPhrases))
154
+ continue;
155
+ // Get the code content - prefer 'after', fall back to content from Write
156
+ const codeContent = (code.after || code.content || '');
157
+ if (!codeContent)
158
+ continue;
159
+ const codeLines = codeContent.split('\n');
160
+ for (const lp of linkedPhrases) {
161
+ const phrase = lp.phrase;
162
+ if (!phrase)
163
+ continue;
164
+ // Find which line(s) actually contain this phrase
165
+ const matchingLines = [];
166
+ for (let i = 0; i < codeLines.length; i++) {
167
+ if (codeLines[i].includes(phrase)) {
168
+ matchingLines.push(i + 1); // 1-indexed
169
+ }
170
+ }
171
+ if (matchingLines.length > 0) {
172
+ // Update the lines spec
173
+ if (matchingLines.length === 1) {
174
+ lp.lines = String(matchingLines[0]);
175
+ }
176
+ else {
177
+ // If phrase spans multiple lines or appears multiple times, use range
178
+ const min = Math.min(...matchingLines);
179
+ const max = Math.max(...matchingLines);
180
+ lp.lines = min === max ? String(min) : `${min}:${max}`;
181
+ }
182
+ }
183
+ // If phrase not found, leave original lines (might be in contextBefore/After)
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return walkthrough;
189
+ }
190
+ // ============================================================================
191
+ // SCHEMA & PROMPTS
192
+ // ============================================================================
193
+ const WALKTHROUGH_SCHEMA = `
194
+ {
195
+ "title": "string - A concise title for this set of changes",
196
+ "summary": "string - 1-2 sentence overview of what was accomplished",
197
+
198
+ "context": {
199
+ "problem": "string - What was the user trying to achieve? What was broken, missing, or needed improvement?",
200
+ "priorState": "string - How did things work before? Describe the user experience, code flow, or system behavior.",
201
+ "approach": "string - High-level summary of the solution approach before diving into details"
202
+ },
203
+
204
+ "orientation": {
205
+ "narrative": "string - A brief tour of the relevant parts of the codebase BEFORE any changes. Help the reader understand where we're working.",
206
+ "keyFiles": [
207
+ {
208
+ "file": "string - relative file path",
209
+ "role": "string - what this file does in the system"
210
+ }
211
+ ]
212
+ },
213
+
214
+ "sections": [
215
+ {
216
+ "heading": "string - Conceptual heading (not a filename)",
217
+ "narrative": "string - Explanation of this part of the changes. Write in first person as Claude. Use phrases that reference specific code elements.",
218
+ "linkedPhrases": [
219
+ {
220
+ "phrase": "string - exact text from narrative to make interactive (underlined, hoverable)",
221
+ "focusLines": "string - lines in the 'after' code to highlight on hover, e.g. '3:5' or '7'",
222
+ "explanation": "string (optional) - tooltip to show on hover"
223
+ }
224
+ ],
225
+ "file": "string - relative file path",
226
+ "language": "string - programming language (tsx, typescript, python, etc.)",
227
+ "before": {
228
+ "description": "string - Brief description of what the code looked like before",
229
+ "code": "string - The code BEFORE the change (use // !focus and // !mark annotations)"
230
+ },
231
+ "after": {
232
+ "description": "string - Brief description of what changed",
233
+ "code": "string - The code AFTER the change (use // !focus and // !mark annotations)"
234
+ }
235
+ }
236
+ ],
237
+
238
+ "systemFlow": {
239
+ "description": "string - How the pieces work together after the changes",
240
+ "components": ["string - list of key components/files involved and their roles"]
241
+ }
242
+ }
243
+ `;
244
+ // Language mapping for Code Hike
245
+ const LANG_MAP = {
246
+ tsx: 'tsx',
247
+ typescript: 'typescript',
248
+ ts: 'typescript',
249
+ javascript: 'javascript',
250
+ js: 'javascript',
251
+ jsx: 'jsx',
252
+ python: 'python',
253
+ py: 'python',
254
+ bash: 'bash',
255
+ sh: 'bash',
256
+ shell: 'bash',
257
+ json: 'json',
258
+ css: 'css',
259
+ html: 'html',
260
+ markdown: 'markdown',
261
+ md: 'markdown',
262
+ sql: 'sql',
263
+ yaml: 'yaml',
264
+ yml: 'yaml',
265
+ };
266
+ // ============================================================================
267
+ // SERVICE
268
+ // ============================================================================
269
+ export class WalkthroughService {
270
+ historyReader;
271
+ client = null;
272
+ constructor(historyReader) {
273
+ this.historyReader = historyReader;
274
+ }
275
+ /**
276
+ * Pre-compute all file change data for a session.
277
+ * This data is passed to the skill prompt so the model doesn't need to query the DB.
278
+ * @param sessionId - The session to precompute data for
279
+ * @param scope - Optional scope with editIndices and writeIndices to filter changes
280
+ */
281
+ async precomputeWalkthroughData(sessionId, scope) {
282
+ logger.info('Pre-computing walkthrough data', { sessionId, hasScope: !!scope });
283
+ const sessionInfo = SessionInfoService.getInstance();
284
+ // Get file changes from database (returns camelCase props)
285
+ let fileChanges = sessionInfo.getFileChanges(sessionId);
286
+ // If scope provided, filter to only the specified indices
287
+ if (scope?.editIndices || scope?.writeIndices) {
288
+ const editIndices = new Set(scope.editIndices || []);
289
+ const writeIndices = new Set(scope.writeIndices || []);
290
+ // File changes are ordered by ID, which corresponds to the indices
291
+ // We need to track edit and write indices separately
292
+ let editIdx = 0;
293
+ let writeIdx = 0;
294
+ fileChanges = fileChanges.filter(change => {
295
+ if (change.toolName === 'Edit') {
296
+ const include = editIndices.has(editIdx);
297
+ editIdx++;
298
+ return include;
299
+ }
300
+ else {
301
+ const include = writeIndices.has(writeIdx);
302
+ writeIdx++;
303
+ return include;
304
+ }
305
+ });
306
+ logger.info('Filtered file changes by scope', {
307
+ originalCount: sessionInfo.getFileChanges(sessionId).length,
308
+ filteredCount: fileChanges.length,
309
+ editIndicesCount: editIndices.size,
310
+ writeIndicesCount: writeIndices.size,
311
+ });
312
+ }
313
+ // Get turn summaries from database (returns snake_case props)
314
+ const turns = await sessionInfo.getTurns(sessionId);
315
+ // Get session insights
316
+ const insights = await sessionInfo.getInsights(sessionId);
317
+ // Group changes by file
318
+ const fileMap = new Map();
319
+ let totalLinesAdded = 0;
320
+ let totalLinesRemoved = 0;
321
+ for (const change of fileChanges) {
322
+ const relativePath = change.filePath
323
+ .replace(/^\/home\/jason\/(claudia|cui-custom)\//, '')
324
+ .replace(/^\/home\/jason\//, '~/');
325
+ if (!fileMap.has(change.filePath)) {
326
+ fileMap.set(change.filePath, {
327
+ path: change.filePath,
328
+ relativePath,
329
+ turnNumbers: [],
330
+ totalEdits: 0,
331
+ totalWrites: 0,
332
+ changes: [],
333
+ });
334
+ }
335
+ const fileData = fileMap.get(change.filePath);
336
+ // Track turn numbers
337
+ if (!fileData.turnNumbers.includes(change.turnNumber)) {
338
+ fileData.turnNumbers.push(change.turnNumber);
339
+ }
340
+ // Count by tool type
341
+ if (change.toolName === 'Edit') {
342
+ fileData.totalEdits++;
343
+ }
344
+ else {
345
+ fileData.totalWrites++;
346
+ }
347
+ // Add change data
348
+ fileData.changes.push({
349
+ turn: change.turnNumber,
350
+ toolName: change.toolName,
351
+ oldString: change.oldString,
352
+ newString: change.newString,
353
+ });
354
+ // Compute line stats from actual changes
355
+ const newLines = (change.newString.match(/\n/g) || []).length + 1;
356
+ const oldLines = change.oldString ? (change.oldString.match(/\n/g) || []).length + 1 : 0;
357
+ if (change.toolName === 'Write') {
358
+ // New file - all lines are added
359
+ totalLinesAdded += newLines;
360
+ }
361
+ else {
362
+ // Edit - compute diff
363
+ totalLinesAdded += Math.max(0, newLines - oldLines);
364
+ totalLinesRemoved += Math.max(0, oldLines - newLines);
365
+ }
366
+ }
367
+ // Convert map to array and sort by first turn number
368
+ const files = Array.from(fileMap.values()).sort((a, b) => {
369
+ const aFirst = Math.min(...a.turnNumbers);
370
+ const bFirst = Math.min(...b.turnNumbers);
371
+ return aFirst - bFirst;
372
+ });
373
+ // Format turn summaries (TurnRecord uses snake_case)
374
+ const turnSummaries = turns.map((t) => ({
375
+ number: t.turn_number,
376
+ headline: t.headline,
377
+ tag: t.tag || 'implement',
378
+ toolCount: t.tool_count || 0,
379
+ }));
380
+ // Compute total turns (max turn number from changes or turns)
381
+ const maxTurnFromChanges = fileChanges.length > 0
382
+ ? Math.max(...fileChanges.map(c => c.turnNumber))
383
+ : 0;
384
+ const maxTurnFromTurns = turns.length > 0
385
+ ? Math.max(...turns.map((t) => t.turn_number))
386
+ : 0;
387
+ const totalTurns = Math.max(maxTurnFromChanges, maxTurnFromTurns);
388
+ logger.info('Pre-computed walkthrough data', {
389
+ sessionId,
390
+ totalFiles: files.length,
391
+ totalChanges: fileChanges.length,
392
+ totalTurns,
393
+ linesAdded: totalLinesAdded,
394
+ linesRemoved: totalLinesRemoved,
395
+ });
396
+ return {
397
+ sessionId,
398
+ totalTurns,
399
+ totalFiles: files.length,
400
+ linesAdded: totalLinesAdded,
401
+ linesRemoved: totalLinesRemoved,
402
+ files,
403
+ turns: turnSummaries,
404
+ purpose: insights?.purpose || undefined,
405
+ description: insights?.description || undefined,
406
+ };
407
+ }
408
+ getClient() {
409
+ if (!this.client) {
410
+ const config = ConfigService.getInstance().getConfig();
411
+ const apiKey = config.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY;
412
+ if (!apiKey) {
413
+ throw new Error('No Anthropic API key configured');
414
+ }
415
+ this.client = new Anthropic({ apiKey });
416
+ }
417
+ return this.client;
418
+ }
419
+ /**
420
+ * Generate a walkthrough for a session, optionally filtered to specific indices
421
+ * @param sessionId - The session to generate walkthrough for
422
+ * @param scope - Optional scope with editIndices and writeIndices to filter changes
423
+ */
424
+ async generateWalkthrough(sessionId, scope) {
425
+ logger.info('Generating walkthrough', { sessionId, hasScope: !!scope });
426
+ // Extract session data
427
+ let data = await this.extractWalkthroughData(sessionId);
428
+ if (data.edits.length === 0 && data.writes.length === 0) {
429
+ throw new Error('No code changes found in session');
430
+ }
431
+ // If scope indices provided, filter the data
432
+ if (scope?.editIndices || scope?.writeIndices) {
433
+ const editIndices = scope.editIndices || [];
434
+ const writeIndices = scope.writeIndices || [];
435
+ logger.info('Filtering to scope', {
436
+ editCount: editIndices.length,
437
+ writeCount: writeIndices.length,
438
+ totalEdits: data.edits.length,
439
+ totalWrites: data.writes.length,
440
+ });
441
+ // Filter edits and writes to only those in this scope
442
+ data = {
443
+ ...data,
444
+ edits: editIndices.map(i => data.edits[i]).filter(Boolean),
445
+ writes: writeIndices.map(i => data.writes[i]).filter(Boolean),
446
+ };
447
+ }
448
+ logger.info('Extracted session data', {
449
+ sessionId,
450
+ edits: data.edits.length,
451
+ writes: data.writes.length,
452
+ });
453
+ // Generate walkthrough with Claude
454
+ const walkthrough = await this.callClaude(data);
455
+ // Add syntax highlighting
456
+ const highlighted = await this.addHighlighting(walkthrough);
457
+ logger.info('Walkthrough generated', { sessionId, sections: highlighted.sections.length });
458
+ return highlighted;
459
+ }
460
+ /**
461
+ * Analyze session changes and suggest conceptual groupings for walkthroughs.
462
+ * Uses Haiku for fast analysis of what distinct features/tasks were worked on.
463
+ * @param sessionId - The session to analyze
464
+ * @param previousGroupings - Optional previous groupings to maintain stability
465
+ */
466
+ async analyzeScope(sessionId, previousGroupings) {
467
+ logger.info('Analyzing walkthrough scope', { sessionId, hasPreviousGroupings: !!previousGroupings });
468
+ const data = await this.extractWalkthroughData(sessionId);
469
+ if (data.edits.length === 0 && data.writes.length === 0) {
470
+ throw new Error('No code changes found in session');
471
+ }
472
+ // Build a summary of changes for Haiku to analyze
473
+ const changesSummary = this.buildChangesSummaryForScoping(data);
474
+ // Build stability context if we have previous groupings
475
+ let stabilityContext = '';
476
+ if (previousGroupings && previousGroupings.length > 0) {
477
+ stabilityContext = `
478
+ IMPORTANT: This session was previously analyzed with these groupings:
479
+ ${JSON.stringify(previousGroupings, null, 2)}
480
+
481
+ STABILITY RULES:
482
+ - Keep existing group IDs and titles when the same work is still present
483
+ - Only rename a group if its scope has fundamentally changed
484
+ - New changes (higher indices) should either extend existing groups OR form new groups
485
+ - Prefer extending existing groups over creating new ones when work is related
486
+ - If a previous group's changes are still present, keep that group
487
+
488
+ `;
489
+ }
490
+ const client = this.getClient();
491
+ const response = await client.messages.create({
492
+ model: 'claude-haiku-4-5-20251001',
493
+ max_tokens: 2048,
494
+ messages: [{
495
+ role: 'user',
496
+ content: `Analyze these code changes from a coding session and identify distinct conceptual groupings.
497
+
498
+ The user's original request was:
499
+ "${data.userRequest.slice(0, 500)}"
500
+
501
+ Here are the changes made, with reasoning before each:
502
+
503
+ ${changesSummary}
504
+
505
+ ---
506
+ ${stabilityContext}
507
+ Identify 1-4 conceptual groupings based on WHAT was being accomplished (not just which files were touched).
508
+ Consider the reasoning text - it explains intent.
509
+
510
+ For each grouping, specify which edit/write indices belong to it (0-indexed).
511
+
512
+ Output JSON:
513
+ {
514
+ "groupings": [
515
+ {
516
+ "id": "unique-slug",
517
+ "title": "Short title (3-6 words)",
518
+ "description": "One sentence explaining what this group of changes accomplishes",
519
+ "editIndices": [0, 1, 5],
520
+ "writeIndices": [0]
521
+ }
522
+ ]
523
+ }
524
+
525
+ If all changes are part of one cohesive feature, return just one grouping with all indices.
526
+ Output only valid JSON.`
527
+ }],
528
+ });
529
+ const content = response.content[0];
530
+ if (content.type !== 'text') {
531
+ throw new Error('Unexpected response type from scope analysis');
532
+ }
533
+ // Parse response
534
+ let jsonText = content.text.trim();
535
+ const jsonMatch = jsonText.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
536
+ if (jsonMatch) {
537
+ jsonText = jsonMatch[1];
538
+ }
539
+ let parsed;
540
+ try {
541
+ parsed = JSON.parse(jsonText);
542
+ }
543
+ catch {
544
+ logger.error('Failed to parse scope analysis', { response: content.text.slice(0, 500) });
545
+ // Fallback: single group with everything
546
+ parsed = {
547
+ groupings: [{
548
+ id: 'all-changes',
549
+ title: 'All Changes',
550
+ description: 'All changes in this session',
551
+ editIndices: data.edits.map((_, i) => i),
552
+ writeIndices: data.writes.map((_, i) => i),
553
+ }]
554
+ };
555
+ }
556
+ // Build scope options with file counts and turn ranges
557
+ const options = parsed.groupings.map(g => {
558
+ const files = new Set();
559
+ const turnNumbers = [];
560
+ g.editIndices.forEach(i => {
561
+ if (data.edits[i]) {
562
+ files.add(data.edits[i].file_path);
563
+ turnNumbers.push(data.edits[i].turnNumber);
564
+ }
565
+ });
566
+ g.writeIndices.forEach(i => {
567
+ if (data.writes[i]) {
568
+ files.add(data.writes[i].file_path);
569
+ turnNumbers.push(data.writes[i].turnNumber);
570
+ }
571
+ });
572
+ // Compute turn range (min/max turn numbers in this group)
573
+ const minTurn = turnNumbers.length > 0 ? Math.min(...turnNumbers) : 1;
574
+ const maxTurn = turnNumbers.length > 0 ? Math.max(...turnNumbers) : 1;
575
+ return {
576
+ id: g.id,
577
+ title: g.title,
578
+ description: g.description,
579
+ editIndices: g.editIndices,
580
+ writeIndices: g.writeIndices,
581
+ fileCount: files.size,
582
+ changeCount: g.editIndices.length + g.writeIndices.length,
583
+ turnRange: [minTurn, maxTurn],
584
+ };
585
+ });
586
+ // Compute total turn count from all edits/writes
587
+ const allTurnNumbers = [
588
+ ...data.edits.map(e => e.turnNumber),
589
+ ...data.writes.map(w => w.turnNumber),
590
+ ];
591
+ const totalTurns = allTurnNumbers.length > 0 ? Math.max(...allTurnNumbers) : 0;
592
+ // Compute all unique files for full session scope
593
+ const allFiles = new Set();
594
+ data.edits.forEach(e => allFiles.add(e.file_path));
595
+ data.writes.forEach(w => allFiles.add(w.file_path));
596
+ // Prepend "Complete Session" option if there are multiple groupings
597
+ // (If Haiku already returned a single "all changes" group, don't duplicate)
598
+ const hasMultipleGroupings = options.length > 1;
599
+ const fullSessionOption = {
600
+ id: 'complete-session',
601
+ title: 'Complete Session',
602
+ description: 'Walk through everything done in this session from start to finish',
603
+ editIndices: data.edits.map((_, i) => i),
604
+ writeIndices: data.writes.map((_, i) => i),
605
+ fileCount: allFiles.size,
606
+ changeCount: data.edits.length + data.writes.length,
607
+ turnRange: [1, totalTurns],
608
+ };
609
+ const finalOptions = hasMultipleGroupings ? [fullSessionOption, ...options] : options;
610
+ logger.info('Scope analysis complete', {
611
+ sessionId,
612
+ optionCount: finalOptions.length,
613
+ totalTurns,
614
+ hasFullSessionOption: hasMultipleGroupings,
615
+ options: finalOptions.map(o => ({ title: o.title, turnRange: o.turnRange }))
616
+ });
617
+ return {
618
+ sessionId,
619
+ totalEdits: data.edits.length,
620
+ totalWrites: data.writes.length,
621
+ totalTurns,
622
+ userRequest: data.userRequest.slice(0, 200),
623
+ options: finalOptions,
624
+ };
625
+ }
626
+ /**
627
+ * Build a concise summary of changes for scope analysis
628
+ */
629
+ buildChangesSummaryForScoping(data) {
630
+ const lines = [];
631
+ data.edits.forEach((edit, i) => {
632
+ const shortPath = edit.file_path.replace(/^\/home\/jason\/(claudia|cui-custom)\//, '');
633
+ const reasoningSnippet = edit.reasoning.slice(0, 150).replace(/\n/g, ' ');
634
+ lines.push(`Edit ${i}: ${shortPath}`);
635
+ lines.push(` Reasoning: ${reasoningSnippet}...`);
636
+ lines.push(` Changed: ${edit.old_string.slice(0, 50).replace(/\n/g, '\\n')}... → ${edit.new_string.slice(0, 50).replace(/\n/g, '\\n')}...`);
637
+ lines.push('');
638
+ });
639
+ data.writes.forEach((write, i) => {
640
+ const shortPath = write.file_path.replace(/^\/home\/jason\/(claudia|cui-custom)\//, '');
641
+ const reasoningSnippet = write.reasoning.slice(0, 150).replace(/\n/g, ' ');
642
+ lines.push(`Write ${i}: ${shortPath} (new file)`);
643
+ lines.push(` Reasoning: ${reasoningSnippet}...`);
644
+ lines.push('');
645
+ });
646
+ return lines.join('\n');
647
+ }
648
+ /**
649
+ * Check if a user message contains actual human text (not just tool_result).
650
+ */
651
+ hasHumanText(content) {
652
+ if (typeof content === 'string' && content.trim()) {
653
+ return true;
654
+ }
655
+ if (Array.isArray(content)) {
656
+ return content.some(block => typeof block === 'object' && block !== null && block.type === 'text' && 'text' in block);
657
+ }
658
+ return false;
659
+ }
660
+ /**
661
+ * Extract Edit/Write calls from session messages, including turn numbers.
662
+ */
663
+ async extractWalkthroughData(sessionId) {
664
+ const details = await this.historyReader.fetchConversationDirect(sessionId);
665
+ if (!details || !details.messages) {
666
+ throw new Error(`Session not found: ${sessionId}`);
667
+ }
668
+ const messages = details.messages;
669
+ // Extract user request (first user message with human text)
670
+ let userRequest = '';
671
+ for (const msg of messages) {
672
+ if (msg.type === 'user' && this.hasHumanText(msg.message.content)) {
673
+ const content = msg.message.content;
674
+ if (typeof content === 'string') {
675
+ userRequest = content;
676
+ break;
677
+ }
678
+ else if (Array.isArray(content)) {
679
+ const textBlock = content.find((b) => b.type === 'text');
680
+ if (textBlock?.text) {
681
+ userRequest = textBlock.text;
682
+ break;
683
+ }
684
+ }
685
+ }
686
+ }
687
+ // Extract Edit/Write calls with reasoning and turn numbers
688
+ // A turn starts when we see a user message with actual human text
689
+ const edits = [];
690
+ const writes = [];
691
+ let currentReasoning = '';
692
+ let currentTurn = 0;
693
+ for (const msg of messages) {
694
+ // Detect turn boundaries - user message with human text starts a new turn
695
+ if (msg.type === 'user' && this.hasHumanText(msg.message.content)) {
696
+ currentTurn++;
697
+ }
698
+ if (msg.type === 'assistant') {
699
+ const content = msg.message.content;
700
+ if (Array.isArray(content)) {
701
+ // Capture reasoning from text blocks
702
+ const textBlocks = content.filter((b) => b.type === 'text');
703
+ if (textBlocks.length > 0) {
704
+ currentReasoning = textBlocks.map((b) => b.text || '').join('\n');
705
+ }
706
+ // Extract tool calls
707
+ for (const block of content) {
708
+ if (block.type === 'tool_use') {
709
+ if (block.name === 'Edit' && block.input) {
710
+ edits.push({
711
+ file_path: block.input.file_path,
712
+ old_string: block.input.old_string,
713
+ new_string: block.input.new_string,
714
+ reasoning: currentReasoning,
715
+ turnNumber: currentTurn,
716
+ });
717
+ }
718
+ else if (block.name === 'Write' && block.input) {
719
+ writes.push({
720
+ file_path: block.input.file_path,
721
+ content: block.input.content,
722
+ reasoning: currentReasoning,
723
+ turnNumber: currentTurn,
724
+ });
725
+ }
726
+ }
727
+ }
728
+ }
729
+ }
730
+ }
731
+ return { sessionId, userRequest, edits, writes };
732
+ }
733
+ /**
734
+ * Build prompt and call Claude to generate walkthrough
735
+ * Uses structured outputs beta for guaranteed JSON schema compliance
736
+ */
737
+ async callClaude(data) {
738
+ const prompt = this.buildPrompt(data);
739
+ const client = this.getClient();
740
+ // Use structured outputs beta for guaranteed JSON schema compliance
741
+ const response = await client.beta.messages.create({
742
+ model: 'claude-sonnet-4-5-20250929',
743
+ max_tokens: 16384,
744
+ betas: ['structured-outputs-2025-11-13'],
745
+ messages: [{ role: 'user', content: prompt }],
746
+ output_format: {
747
+ type: 'json_schema',
748
+ schema: {
749
+ type: 'object',
750
+ properties: {
751
+ title: { type: 'string' },
752
+ summary: { type: 'string' },
753
+ context: {
754
+ type: 'object',
755
+ properties: {
756
+ problem: { type: 'string' },
757
+ priorState: { type: 'string' },
758
+ approach: { type: 'string' },
759
+ },
760
+ required: ['problem', 'priorState', 'approach'],
761
+ additionalProperties: false,
762
+ },
763
+ orientation: {
764
+ type: 'object',
765
+ properties: {
766
+ narrative: { type: 'string' },
767
+ keyFiles: {
768
+ type: 'array',
769
+ items: {
770
+ type: 'object',
771
+ properties: {
772
+ file: { type: 'string' },
773
+ role: { type: 'string' },
774
+ },
775
+ required: ['file', 'role'],
776
+ additionalProperties: false,
777
+ },
778
+ },
779
+ },
780
+ required: ['narrative'],
781
+ additionalProperties: false,
782
+ },
783
+ sections: {
784
+ type: 'array',
785
+ items: {
786
+ type: 'object',
787
+ properties: {
788
+ heading: { type: 'string' },
789
+ narrative: { type: 'string' },
790
+ linkedPhrases: {
791
+ type: 'array',
792
+ items: {
793
+ type: 'object',
794
+ properties: {
795
+ phrase: { type: 'string' },
796
+ focusLines: { type: 'string' },
797
+ explanation: { type: 'string' },
798
+ },
799
+ required: ['phrase'],
800
+ additionalProperties: false,
801
+ },
802
+ },
803
+ file: { type: 'string' },
804
+ language: { type: 'string' },
805
+ before: {
806
+ type: 'object',
807
+ properties: {
808
+ description: { type: 'string' },
809
+ code: { type: 'string' },
810
+ },
811
+ required: ['description', 'code'],
812
+ additionalProperties: false,
813
+ },
814
+ after: {
815
+ type: 'object',
816
+ properties: {
817
+ description: { type: 'string' },
818
+ code: { type: 'string' },
819
+ },
820
+ required: ['description', 'code'],
821
+ additionalProperties: false,
822
+ },
823
+ },
824
+ required: ['heading', 'narrative'],
825
+ additionalProperties: false,
826
+ },
827
+ },
828
+ systemFlow: {
829
+ type: 'object',
830
+ properties: {
831
+ description: { type: 'string' },
832
+ components: {
833
+ type: 'array',
834
+ items: { type: 'string' },
835
+ },
836
+ },
837
+ required: ['description', 'components'],
838
+ additionalProperties: false,
839
+ },
840
+ },
841
+ required: ['title', 'summary', 'context', 'sections'],
842
+ additionalProperties: false,
843
+ },
844
+ },
845
+ });
846
+ const content = response.content[0];
847
+ if (content.type !== 'text') {
848
+ throw new Error('Unexpected response type');
849
+ }
850
+ // With structured outputs, the response is guaranteed valid JSON
851
+ return JSON.parse(content.text);
852
+ }
853
+ buildPrompt(data) {
854
+ // Group edits by file
855
+ const editsByFile = new Map();
856
+ for (const edit of data.edits) {
857
+ const existing = editsByFile.get(edit.file_path) || [];
858
+ existing.push(edit);
859
+ editsByFile.set(edit.file_path, existing);
860
+ }
861
+ let changesDescription = '';
862
+ for (const [filePath, fileEdits] of editsByFile) {
863
+ const shortPath = filePath.replace(/^\/home\/jason\/(claudia|cui-custom)\//, '');
864
+ changesDescription += `\n## ${shortPath}\n\n`;
865
+ for (let i = 0; i < fileEdits.length; i++) {
866
+ const edit = fileEdits[i];
867
+ changesDescription += `### Edit ${i + 1}\n`;
868
+ changesDescription += `**My reasoning at the time:**\n${edit.reasoning.slice(0, 500)}\n\n`;
869
+ changesDescription += `**Changed from:**\n\`\`\`\n${edit.old_string.slice(0, 400)}\n\`\`\`\n\n`;
870
+ changesDescription += `**Changed to:**\n\`\`\`\n${edit.new_string.slice(0, 400)}\n\`\`\`\n\n`;
871
+ }
872
+ }
873
+ for (const write of data.writes) {
874
+ const shortPath = write.file_path.replace(/^\/home\/jason\/(claudia|cui-custom)\//, '');
875
+ changesDescription += `\n## ${shortPath} (new file)\n\n`;
876
+ changesDescription += `**My reasoning:**\n${write.reasoning.slice(0, 500)}\n\n`;
877
+ changesDescription += `**Content:**\n\`\`\`\n${write.content.slice(0, 600)}\n\`\`\`\n\n`;
878
+ }
879
+ return `You are creating an interactive walkthrough of code changes from a coding session. The walkthrough will show a narrative explanation with before/after code comparisons and diffs.
880
+
881
+ The user's original request was:
882
+ "${data.userRequest.slice(0, 1000)}"
883
+
884
+ Here are all the changes you made, with your reasoning at the time:
885
+
886
+ ${changesDescription}
887
+
888
+ ---
889
+
890
+ Create a narrative-driven walkthrough with BEFORE/AFTER code comparisons:
891
+
892
+ 1. **Context** - Problem, prior state, approach (1-2 sentences each, set the scene)
893
+
894
+ 2. **Orientation** - Brief tour of relevant files (just file paths and roles, no code snippets)
895
+
896
+ 3. **Sections** - Each section explains ONE conceptual change with:
897
+ - A heading (conceptual, not just a filename)
898
+ - A narrative explaining what changed and WHY (first person, past tense)
899
+ - linkedPhrases: 2-4 phrases from the narrative that become interactive (underlined, hoverable)
900
+ - before: the code BEFORE the change
901
+ - after: the code AFTER the change (viewer will compute diff automatically)
902
+
903
+ INTERACTIVE LINKED PHRASES:
904
+ Pick key terms from your narrative that reference specific code. When the user hovers these underlined phrases, the corresponding lines in the "after" code will be highlighted. Include a brief tooltip explanation.
905
+
906
+ EXAMPLE for narrative "I wrapped the component in a SelectionProvider and made each step use Selectable":
907
+ linkedPhrases: [
908
+ { "phrase": "SelectionProvider", "focusLines": "3:4", "explanation": "Creates shared selection context for all children" },
909
+ { "phrase": "Selectable", "focusLines": "8:12", "explanation": "Makes this element respond to hover and scroll selection" }
910
+ ]
911
+
912
+ CODE HIKE ANNOTATIONS (use in before/after code):
913
+ - // !focus(1:3) - dims other lines, highlights these (great for drawing attention!)
914
+ - // !mark[/text/] - highlights specific tokens inline
915
+ - // !tooltip[/text/] Explanation - shows tooltip on hover over text in code
916
+
917
+ Output JSON matching this schema:
918
+ ${WALKTHROUGH_SCHEMA}
919
+
920
+ CRITICAL Guidelines:
921
+ - Each section needs BOTH before AND after code - show the transformation
922
+ - linkedPhrases must use EXACT text from the narrative (case-sensitive match)
923
+ - focusLines reference the AFTER code using 1-indexed line numbers
924
+ - Write in PAST TENSE - this is retrospective ("I added...", "This enabled...")
925
+ - Keep sections focused - 3-6 sections is ideal
926
+ - Code should be 10-40 lines, enough context to understand the change
927
+ - Don't include unrelated code in before/after - focus on what changed
928
+
929
+ IMPORTANT: Output ONLY the JSON object. No markdown, no prose, no explanation. Start with { end with }.`;
930
+ }
931
+ /**
932
+ * Add Code Hike syntax highlighting to all code blocks
933
+ */
934
+ async addHighlighting(walkthrough) {
935
+ const theme = 'github-dark';
936
+ const highlightCode = async (code, lang) => {
937
+ const normalizedLang = LANG_MAP[lang.toLowerCase()] || lang || 'text';
938
+ try {
939
+ return await highlight({ value: code, lang: normalizedLang, meta: '' }, theme);
940
+ }
941
+ catch (err) {
942
+ logger.warn('Failed to highlight code', { lang: normalizedLang, error: err });
943
+ return null;
944
+ }
945
+ };
946
+ const inferLang = (file) => {
947
+ if (!file)
948
+ return 'typescript';
949
+ const ext = file.split('.').pop()?.toLowerCase() || '';
950
+ return LANG_MAP[ext] || ext || 'typescript';
951
+ };
952
+ // Highlight orientation keyFiles
953
+ if (walkthrough.orientation?.keyFiles) {
954
+ for (const keyFile of walkthrough.orientation.keyFiles) {
955
+ if (keyFile.relevantCode) {
956
+ const lang = inferLang(keyFile.file);
957
+ const highlighted = await highlightCode(keyFile.relevantCode, lang);
958
+ if (highlighted) {
959
+ keyFile.highlighted = highlighted;
960
+ }
961
+ }
962
+ }
963
+ }
964
+ // Highlight sections and compute diffs
965
+ for (const section of walkthrough.sections) {
966
+ const sectionLang = section.language || section.codeBlocks?.[0]?.language || 'typescript';
967
+ // New scrollycoding format: highlight section.code
968
+ if (section.code) {
969
+ const lang = section.language || inferLang(section.file);
970
+ const highlighted = await highlightCode(section.code, lang);
971
+ if (highlighted) {
972
+ section.codeHighlighted = highlighted;
973
+ }
974
+ }
975
+ // Legacy format: before/after
976
+ if (section.before?.code) {
977
+ const highlighted = await highlightCode(section.before.code, sectionLang);
978
+ if (highlighted) {
979
+ section.before.highlighted = highlighted;
980
+ }
981
+ }
982
+ if (section.after?.code) {
983
+ const highlighted = await highlightCode(section.after.code, sectionLang);
984
+ if (highlighted) {
985
+ section.after.highlighted = highlighted;
986
+ }
987
+ }
988
+ // Compute diff if we have before and after code
989
+ if (section.before?.code && section.after?.code) {
990
+ const filename = section.codeBlocks?.[0]?.file;
991
+ section.diff = this.computeDiff(section.before.code, section.after.code, filename);
992
+ }
993
+ if (section.codeBlocks) {
994
+ for (const block of section.codeBlocks) {
995
+ if (block.snippet) {
996
+ const highlighted = await highlightCode(block.snippet, block.language || 'typescript');
997
+ if (highlighted) {
998
+ block.highlighted = highlighted;
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+ return walkthrough;
1005
+ }
1006
+ /**
1007
+ * Compute unified diff from old and new strings
1008
+ */
1009
+ computeDiff(oldStr, newStr, filename) {
1010
+ const changes = Diff.diffLines(oldStr, newStr);
1011
+ const lines = [];
1012
+ let oldLineNum = 1;
1013
+ let newLineNum = 1;
1014
+ let added = 0;
1015
+ let removed = 0;
1016
+ for (const change of changes) {
1017
+ const changeLines = change.value.replace(/\n$/, '').split('\n');
1018
+ for (const line of changeLines) {
1019
+ if (change.added) {
1020
+ lines.push({
1021
+ type: 'added',
1022
+ content: line,
1023
+ lineNumber: { new: newLineNum },
1024
+ });
1025
+ newLineNum++;
1026
+ added++;
1027
+ }
1028
+ else if (change.removed) {
1029
+ lines.push({
1030
+ type: 'removed',
1031
+ content: line,
1032
+ lineNumber: { old: oldLineNum },
1033
+ });
1034
+ oldLineNum++;
1035
+ removed++;
1036
+ }
1037
+ else {
1038
+ lines.push({
1039
+ type: 'context',
1040
+ content: line,
1041
+ lineNumber: { old: oldLineNum, new: newLineNum },
1042
+ });
1043
+ oldLineNum++;
1044
+ newLineNum++;
1045
+ }
1046
+ }
1047
+ }
1048
+ // Simplify to show only hunks with changes (context around changes)
1049
+ const contextLines = 3;
1050
+ const hunks = [];
1051
+ let currentHunk = null;
1052
+ let lastChangeIndex = -100;
1053
+ for (let i = 0; i < lines.length; i++) {
1054
+ const line = lines[i];
1055
+ const isChange = line.type !== 'context';
1056
+ if (isChange) {
1057
+ // Start new hunk if needed
1058
+ if (!currentHunk || i - lastChangeIndex > contextLines * 2) {
1059
+ // Add trailing context to previous hunk
1060
+ if (currentHunk) {
1061
+ const trailingStart = lastChangeIndex + 1;
1062
+ const trailingEnd = Math.min(trailingStart + contextLines, i);
1063
+ for (let j = trailingStart; j < trailingEnd; j++) {
1064
+ if (lines[j].type === 'context') {
1065
+ currentHunk.lines.push(lines[j]);
1066
+ }
1067
+ }
1068
+ hunks.push(currentHunk);
1069
+ }
1070
+ // Start new hunk with leading context
1071
+ currentHunk = {
1072
+ header: `@@ changes @@`,
1073
+ lines: [],
1074
+ };
1075
+ const leadingStart = Math.max(0, i - contextLines);
1076
+ for (let j = leadingStart; j < i; j++) {
1077
+ if (lines[j].type === 'context') {
1078
+ currentHunk.lines.push(lines[j]);
1079
+ }
1080
+ }
1081
+ }
1082
+ currentHunk.lines.push(line);
1083
+ lastChangeIndex = i;
1084
+ }
1085
+ }
1086
+ // Finish last hunk
1087
+ if (currentHunk && currentHunk.lines.length > 0) {
1088
+ const trailingStart = lastChangeIndex + 1;
1089
+ const trailingEnd = Math.min(trailingStart + contextLines, lines.length);
1090
+ for (let j = trailingStart; j < trailingEnd; j++) {
1091
+ if (lines[j].type === 'context') {
1092
+ currentHunk.lines.push(lines[j]);
1093
+ }
1094
+ }
1095
+ hunks.push(currentHunk);
1096
+ }
1097
+ // If no hunks (no changes), return empty
1098
+ if (hunks.length === 0) {
1099
+ return {
1100
+ hunks: [],
1101
+ stats: { added: 0, removed: 0 },
1102
+ };
1103
+ }
1104
+ return {
1105
+ oldFile: filename,
1106
+ newFile: filename,
1107
+ hunks,
1108
+ stats: { added, removed },
1109
+ };
1110
+ }
1111
+ }
1112
+ //# sourceMappingURL=walkthrough-service.js.map