@steipete/summarize 0.11.0 → 0.12.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 (361) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +63 -17
  3. package/dist/cli.js +1 -1
  4. package/dist/esm/cache-keys.js +75 -0
  5. package/dist/esm/cache-keys.js.map +1 -0
  6. package/dist/esm/cache-slides-cleanup.js +47 -0
  7. package/dist/esm/cache-slides-cleanup.js.map +1 -0
  8. package/dist/esm/cache.js +14 -91
  9. package/dist/esm/cache.js.map +1 -1
  10. package/dist/esm/config/env.js +49 -0
  11. package/dist/esm/config/env.js.map +1 -0
  12. package/dist/esm/config/model.js +193 -0
  13. package/dist/esm/config/model.js.map +1 -0
  14. package/dist/esm/config/parse-helpers.js +50 -0
  15. package/dist/esm/config/parse-helpers.js.map +1 -0
  16. package/dist/esm/config/read.js +83 -0
  17. package/dist/esm/config/read.js.map +1 -0
  18. package/dist/esm/config/sections.js +438 -0
  19. package/dist/esm/config/sections.js.map +1 -0
  20. package/dist/esm/config/types.js +2 -0
  21. package/dist/esm/config/types.js.map +1 -0
  22. package/dist/esm/config.js +24 -807
  23. package/dist/esm/config.js.map +1 -1
  24. package/dist/esm/content/asset.js +2 -2
  25. package/dist/esm/content/asset.js.map +1 -1
  26. package/dist/esm/daemon/agent-model.js +235 -0
  27. package/dist/esm/daemon/agent-model.js.map +1 -0
  28. package/dist/esm/daemon/agent-request.js +87 -0
  29. package/dist/esm/daemon/agent-request.js.map +1 -0
  30. package/dist/esm/daemon/agent.js +42 -243
  31. package/dist/esm/daemon/agent.js.map +1 -1
  32. package/dist/esm/daemon/chat.js +69 -8
  33. package/dist/esm/daemon/chat.js.map +1 -1
  34. package/dist/esm/daemon/cli.js +21 -4
  35. package/dist/esm/daemon/cli.js.map +1 -1
  36. package/dist/esm/daemon/config.js +65 -9
  37. package/dist/esm/daemon/config.js.map +1 -1
  38. package/dist/esm/daemon/env-snapshot.js +4 -0
  39. package/dist/esm/daemon/env-snapshot.js.map +1 -1
  40. package/dist/esm/daemon/flow-context.js +8 -1
  41. package/dist/esm/daemon/flow-context.js.map +1 -1
  42. package/dist/esm/daemon/models.js +16 -0
  43. package/dist/esm/daemon/models.js.map +1 -1
  44. package/dist/esm/daemon/process-registry.js.map +1 -1
  45. package/dist/esm/daemon/server-admin-routes.js +134 -0
  46. package/dist/esm/daemon/server-admin-routes.js.map +1 -0
  47. package/dist/esm/daemon/server-agent-route.js +104 -0
  48. package/dist/esm/daemon/server-agent-route.js.map +1 -0
  49. package/dist/esm/daemon/server-http.js +89 -0
  50. package/dist/esm/daemon/server-http.js.map +1 -0
  51. package/dist/esm/daemon/server-session-routes.js +209 -0
  52. package/dist/esm/daemon/server-session-routes.js.map +1 -0
  53. package/dist/esm/daemon/server-session.js +118 -0
  54. package/dist/esm/daemon/server-session.js.map +1 -0
  55. package/dist/esm/daemon/server-sse.js +28 -0
  56. package/dist/esm/daemon/server-sse.js.map +1 -0
  57. package/dist/esm/daemon/server-summarize-execution.js +357 -0
  58. package/dist/esm/daemon/server-summarize-execution.js.map +1 -0
  59. package/dist/esm/daemon/server-summarize-request.js +119 -0
  60. package/dist/esm/daemon/server-summarize-request.js.map +1 -0
  61. package/dist/esm/daemon/server.js +72 -1121
  62. package/dist/esm/daemon/server.js.map +1 -1
  63. package/dist/esm/daemon/summarize-progress.js +1 -1
  64. package/dist/esm/daemon/summarize-progress.js.map +1 -1
  65. package/dist/esm/daemon/summarize.js.map +1 -1
  66. package/dist/esm/llm/cli-exec.js +75 -0
  67. package/dist/esm/llm/cli-exec.js.map +1 -0
  68. package/dist/esm/llm/cli-provider-output.js +191 -0
  69. package/dist/esm/llm/cli-provider-output.js.map +1 -0
  70. package/dist/esm/llm/cli.js +3 -212
  71. package/dist/esm/llm/cli.js.map +1 -1
  72. package/dist/esm/llm/generate-text-document.js +109 -0
  73. package/dist/esm/llm/generate-text-document.js.map +1 -0
  74. package/dist/esm/llm/generate-text-shared.js +102 -0
  75. package/dist/esm/llm/generate-text-shared.js.map +1 -0
  76. package/dist/esm/llm/generate-text-stream.js +258 -0
  77. package/dist/esm/llm/generate-text-stream.js.map +1 -0
  78. package/dist/esm/llm/generate-text.js +145 -480
  79. package/dist/esm/llm/generate-text.js.map +1 -1
  80. package/dist/esm/llm/model-id.js +21 -20
  81. package/dist/esm/llm/model-id.js.map +1 -1
  82. package/dist/esm/llm/provider-capabilities.js +2 -0
  83. package/dist/esm/llm/provider-capabilities.js.map +1 -0
  84. package/dist/esm/llm/provider-profile.js +142 -0
  85. package/dist/esm/llm/provider-profile.js.map +1 -0
  86. package/dist/esm/llm/providers/google.js +42 -5
  87. package/dist/esm/llm/providers/google.js.map +1 -1
  88. package/dist/esm/llm/providers/models.js +13 -0
  89. package/dist/esm/llm/providers/models.js.map +1 -1
  90. package/dist/esm/llm/providers/openai.js.map +1 -1
  91. package/dist/esm/llm/transcript-to-markdown.js.map +1 -1
  92. package/dist/esm/model-auto-cli.js +89 -0
  93. package/dist/esm/model-auto-cli.js.map +1 -0
  94. package/dist/esm/model-auto-rules.js +86 -0
  95. package/dist/esm/model-auto-rules.js.map +1 -0
  96. package/dist/esm/model-auto.js +10 -245
  97. package/dist/esm/model-auto.js.map +1 -1
  98. package/dist/esm/model-spec.js +23 -17
  99. package/dist/esm/model-spec.js.map +1 -1
  100. package/dist/esm/refresh-free.js +1 -1
  101. package/dist/esm/refresh-free.js.map +1 -1
  102. package/dist/esm/run/attachments.js +1 -1
  103. package/dist/esm/run/attachments.js.map +1 -1
  104. package/dist/esm/run/bird/exec.js +23 -0
  105. package/dist/esm/run/bird/exec.js.map +1 -0
  106. package/dist/esm/run/bird/media.js +171 -0
  107. package/dist/esm/run/bird/media.js.map +1 -0
  108. package/dist/esm/run/bird/parse.js +82 -0
  109. package/dist/esm/run/bird/parse.js.map +1 -0
  110. package/dist/esm/run/bird/types.js +2 -0
  111. package/dist/esm/run/bird/types.js.map +1 -0
  112. package/dist/esm/run/bird.js +86 -144
  113. package/dist/esm/run/bird.js.map +1 -1
  114. package/dist/esm/run/cache-state.js.map +1 -1
  115. package/dist/esm/run/constants.js +2 -1
  116. package/dist/esm/run/constants.js.map +1 -1
  117. package/dist/esm/run/env.js +3 -0
  118. package/dist/esm/run/env.js.map +1 -1
  119. package/dist/esm/run/finish-line-labels.js +76 -0
  120. package/dist/esm/run/finish-line-labels.js.map +1 -0
  121. package/dist/esm/run/finish-line-lengths.js +96 -0
  122. package/dist/esm/run/finish-line-lengths.js.map +1 -0
  123. package/dist/esm/run/finish-line.js +3 -169
  124. package/dist/esm/run/finish-line.js.map +1 -1
  125. package/dist/esm/run/flows/asset/extract.js.map +1 -1
  126. package/dist/esm/run/flows/asset/input.js +1 -1
  127. package/dist/esm/run/flows/asset/input.js.map +1 -1
  128. package/dist/esm/run/flows/asset/media.js +19 -10
  129. package/dist/esm/run/flows/asset/media.js.map +1 -1
  130. package/dist/esm/run/flows/asset/output.js.map +1 -1
  131. package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
  132. package/dist/esm/run/flows/asset/summary-attempts.js +109 -0
  133. package/dist/esm/run/flows/asset/summary-attempts.js.map +1 -0
  134. package/dist/esm/run/flows/asset/summary.js +19 -107
  135. package/dist/esm/run/flows/asset/summary.js.map +1 -1
  136. package/dist/esm/run/flows/url/extract.js +7 -4
  137. package/dist/esm/run/flows/url/extract.js.map +1 -1
  138. package/dist/esm/run/flows/url/flow-progress.js +119 -0
  139. package/dist/esm/run/flows/url/flow-progress.js.map +1 -0
  140. package/dist/esm/run/flows/url/flow.js +22 -93
  141. package/dist/esm/run/flows/url/flow.js.map +1 -1
  142. package/dist/esm/run/flows/url/markdown.js +21 -3
  143. package/dist/esm/run/flows/url/markdown.js.map +1 -1
  144. package/dist/esm/run/flows/url/progress-status.js +56 -0
  145. package/dist/esm/run/flows/url/progress-status.js.map +1 -0
  146. package/dist/esm/run/flows/url/slides-output-render.js +78 -0
  147. package/dist/esm/run/flows/url/slides-output-render.js.map +1 -0
  148. package/dist/esm/run/flows/url/slides-output-state.js +86 -0
  149. package/dist/esm/run/flows/url/slides-output-state.js.map +1 -0
  150. package/dist/esm/run/flows/url/slides-output-stream.js +271 -0
  151. package/dist/esm/run/flows/url/slides-output-stream.js.map +1 -0
  152. package/dist/esm/run/flows/url/slides-output.js +29 -422
  153. package/dist/esm/run/flows/url/slides-output.js.map +1 -1
  154. package/dist/esm/run/flows/url/slides-text-markdown.js +431 -0
  155. package/dist/esm/run/flows/url/slides-text-markdown.js.map +1 -0
  156. package/dist/esm/run/flows/url/slides-text-transcript.js +199 -0
  157. package/dist/esm/run/flows/url/slides-text-transcript.js.map +1 -0
  158. package/dist/esm/run/flows/url/slides-text-types.js +2 -0
  159. package/dist/esm/run/flows/url/slides-text-types.js.map +1 -0
  160. package/dist/esm/run/flows/url/slides-text.js +2 -627
  161. package/dist/esm/run/flows/url/slides-text.js.map +1 -1
  162. package/dist/esm/run/flows/url/summary-finish.js +34 -0
  163. package/dist/esm/run/flows/url/summary-finish.js.map +1 -0
  164. package/dist/esm/run/flows/url/summary-json.js +32 -0
  165. package/dist/esm/run/flows/url/summary-json.js.map +1 -0
  166. package/dist/esm/run/flows/url/summary-prompt.js +147 -0
  167. package/dist/esm/run/flows/url/summary-prompt.js.map +1 -0
  168. package/dist/esm/run/flows/url/summary-resolution.js +320 -0
  169. package/dist/esm/run/flows/url/summary-resolution.js.map +1 -0
  170. package/dist/esm/run/flows/url/summary-timestamps.js +136 -0
  171. package/dist/esm/run/flows/url/summary-timestamps.js.map +1 -0
  172. package/dist/esm/run/flows/url/summary.js +49 -543
  173. package/dist/esm/run/flows/url/summary.js.map +1 -1
  174. package/dist/esm/run/help.js +9 -3
  175. package/dist/esm/run/help.js.map +1 -1
  176. package/dist/esm/run/markdown-transforms.js +89 -0
  177. package/dist/esm/run/markdown-transforms.js.map +1 -0
  178. package/dist/esm/run/markdown.js +1 -96
  179. package/dist/esm/run/markdown.js.map +1 -1
  180. package/dist/esm/run/run-env.js +28 -7
  181. package/dist/esm/run/run-env.js.map +1 -1
  182. package/dist/esm/run/run-settings-parse.js +73 -0
  183. package/dist/esm/run/run-settings-parse.js.map +1 -0
  184. package/dist/esm/run/run-settings.js +1 -72
  185. package/dist/esm/run/run-settings.js.map +1 -1
  186. package/dist/esm/run/runner-contexts.js +116 -0
  187. package/dist/esm/run/runner-contexts.js.map +1 -0
  188. package/dist/esm/run/runner-execution.js +62 -0
  189. package/dist/esm/run/runner-execution.js.map +1 -0
  190. package/dist/esm/run/runner-flags.js +97 -0
  191. package/dist/esm/run/runner-flags.js.map +1 -0
  192. package/dist/esm/run/runner-setup.js +109 -0
  193. package/dist/esm/run/runner-setup.js.map +1 -0
  194. package/dist/esm/run/runner-slides.js +38 -0
  195. package/dist/esm/run/runner-slides.js.map +1 -0
  196. package/dist/esm/run/runner.js +99 -390
  197. package/dist/esm/run/runner.js.map +1 -1
  198. package/dist/esm/run/slides-render.js +5 -2
  199. package/dist/esm/run/slides-render.js.map +1 -1
  200. package/dist/esm/run/stdin-temp-file.js +1 -1
  201. package/dist/esm/run/stdin-temp-file.js.map +1 -1
  202. package/dist/esm/run/streaming.js +1 -0
  203. package/dist/esm/run/streaming.js.map +1 -1
  204. package/dist/esm/run/summary-engine.js +26 -10
  205. package/dist/esm/run/summary-engine.js.map +1 -1
  206. package/dist/esm/run/summary-llm.js +2 -1
  207. package/dist/esm/run/summary-llm.js.map +1 -1
  208. package/dist/esm/run/terminal.js +4 -1
  209. package/dist/esm/run/terminal.js.map +1 -1
  210. package/dist/esm/run/transcriber-cli.js +1 -1
  211. package/dist/esm/run/transcriber-cli.js.map +1 -1
  212. package/dist/esm/slides/download.js +242 -0
  213. package/dist/esm/slides/download.js.map +1 -0
  214. package/dist/esm/slides/extract-finalize.js +98 -0
  215. package/dist/esm/slides/extract-finalize.js.map +1 -0
  216. package/dist/esm/slides/extract.js +64 -1621
  217. package/dist/esm/slides/extract.js.map +1 -1
  218. package/dist/esm/slides/frame-extraction.js +372 -0
  219. package/dist/esm/slides/frame-extraction.js.map +1 -0
  220. package/dist/esm/slides/ingest.js +167 -0
  221. package/dist/esm/slides/ingest.js.map +1 -0
  222. package/dist/esm/slides/ocr.js +91 -0
  223. package/dist/esm/slides/ocr.js.map +1 -0
  224. package/dist/esm/slides/process.js +218 -0
  225. package/dist/esm/slides/process.js.map +1 -0
  226. package/dist/esm/slides/scene-detection.js +387 -0
  227. package/dist/esm/slides/scene-detection.js.map +1 -0
  228. package/dist/esm/slides/source-id.js +42 -0
  229. package/dist/esm/slides/source-id.js.map +1 -0
  230. package/dist/esm/tty/progress/fetch-html.js.map +1 -1
  231. package/dist/esm/tty/progress/transcript.js +21 -8
  232. package/dist/esm/tty/progress/transcript.js.map +1 -1
  233. package/dist/esm/tty/spinner.js +8 -2
  234. package/dist/esm/tty/spinner.js.map +1 -1
  235. package/dist/esm/tty/website-progress.js +5 -3
  236. package/dist/esm/tty/website-progress.js.map +1 -1
  237. package/dist/esm/version.js +1 -1
  238. package/dist/types/cache-keys.d.ts +44 -0
  239. package/dist/types/cache-slides-cleanup.d.ts +1 -0
  240. package/dist/types/cache.d.ts +1 -9
  241. package/dist/types/config/env.d.ts +6 -0
  242. package/dist/types/config/model.d.ts +3 -0
  243. package/dist/types/config/parse-helpers.d.ts +7 -0
  244. package/dist/types/config/read.d.ts +2 -0
  245. package/dist/types/config/sections.d.ts +33 -0
  246. package/dist/types/config/types.d.ts +230 -0
  247. package/dist/types/config.d.ts +3 -209
  248. package/dist/types/costs.d.ts +1 -1
  249. package/dist/types/daemon/agent-model.d.ts +40 -0
  250. package/dist/types/daemon/agent-request.d.ts +14 -0
  251. package/dist/types/daemon/chat.d.ts +3 -1
  252. package/dist/types/daemon/config.d.ts +13 -2
  253. package/dist/types/daemon/env-snapshot.d.ts +1 -1
  254. package/dist/types/daemon/flow-context.d.ts +1 -1
  255. package/dist/types/daemon/models.d.ts +1 -0
  256. package/dist/types/daemon/server-admin-routes.d.ts +22 -0
  257. package/dist/types/daemon/server-agent-route.d.ts +9 -0
  258. package/dist/types/daemon/server-http.d.ts +10 -0
  259. package/dist/types/daemon/server-session-routes.d.ts +11 -0
  260. package/dist/types/daemon/server-session.d.ts +52 -0
  261. package/dist/types/daemon/server-sse.d.ts +12 -0
  262. package/dist/types/daemon/server-summarize-execution.d.ts +70 -0
  263. package/dist/types/daemon/server-summarize-request.d.ts +36 -0
  264. package/dist/types/daemon/server.d.ts +3 -4
  265. package/dist/types/daemon/summarize.d.ts +1 -1
  266. package/dist/types/llm/cli-exec.d.ts +13 -0
  267. package/dist/types/llm/cli-provider-output.d.ts +16 -0
  268. package/dist/types/llm/generate-text-document.d.ts +34 -0
  269. package/dist/types/llm/generate-text-shared.d.ts +25 -0
  270. package/dist/types/llm/generate-text-stream.d.ts +26 -0
  271. package/dist/types/llm/generate-text.d.ts +6 -26
  272. package/dist/types/llm/html-to-markdown.d.ts +1 -1
  273. package/dist/types/llm/model-id.d.ts +1 -1
  274. package/dist/types/llm/provider-capabilities.d.ts +2 -0
  275. package/dist/types/llm/provider-profile.d.ts +31 -0
  276. package/dist/types/llm/providers/google.d.ts +6 -0
  277. package/dist/types/llm/providers/models.d.ts +5 -0
  278. package/dist/types/llm/transcript-to-markdown.d.ts +1 -1
  279. package/dist/types/model-auto-cli.d.ts +15 -0
  280. package/dist/types/model-auto-rules.d.ts +7 -0
  281. package/dist/types/model-auto.d.ts +5 -7
  282. package/dist/types/model-spec.d.ts +2 -2
  283. package/dist/types/run/attachments.d.ts +2 -2
  284. package/dist/types/run/bird/exec.d.ts +1 -0
  285. package/dist/types/run/bird/media.d.ts +3 -0
  286. package/dist/types/run/bird/parse.d.ts +3 -0
  287. package/dist/types/run/bird/types.d.ts +18 -0
  288. package/dist/types/run/bird.d.ts +12 -17
  289. package/dist/types/run/cache-state.d.ts +1 -1
  290. package/dist/types/run/constants.d.ts +2 -1
  291. package/dist/types/run/env.d.ts +1 -0
  292. package/dist/types/run/finish-line-labels.d.ts +29 -0
  293. package/dist/types/run/finish-line-lengths.d.ts +23 -0
  294. package/dist/types/run/finish-line.d.ts +2 -52
  295. package/dist/types/run/flows/asset/extract.d.ts +1 -1
  296. package/dist/types/run/flows/asset/input.d.ts +1 -1
  297. package/dist/types/run/flows/asset/preprocess.d.ts +1 -1
  298. package/dist/types/run/flows/asset/summary-attempts.d.ts +24 -0
  299. package/dist/types/run/flows/asset/summary.d.ts +6 -2
  300. package/dist/types/run/flows/url/flow-progress.d.ts +41 -0
  301. package/dist/types/run/flows/url/markdown.d.ts +2 -2
  302. package/dist/types/run/flows/url/progress-status.d.ts +16 -0
  303. package/dist/types/run/flows/url/slides-output-render.d.ts +43 -0
  304. package/dist/types/run/flows/url/slides-output-state.d.ts +21 -0
  305. package/dist/types/run/flows/url/slides-output-stream.d.ts +18 -0
  306. package/dist/types/run/flows/url/slides-output.d.ts +2 -17
  307. package/dist/types/run/flows/url/slides-text-markdown.d.ts +46 -0
  308. package/dist/types/run/flows/url/slides-text-transcript.d.ts +36 -0
  309. package/dist/types/run/flows/url/slides-text-types.d.ts +8 -0
  310. package/dist/types/run/flows/url/slides-text.d.ts +3 -87
  311. package/dist/types/run/flows/url/summary-finish.d.ts +16 -0
  312. package/dist/types/run/flows/url/summary-json.d.ts +51 -0
  313. package/dist/types/run/flows/url/summary-prompt.d.ts +22 -0
  314. package/dist/types/run/flows/url/summary-resolution.d.ts +31 -0
  315. package/dist/types/run/flows/url/summary-timestamps.d.ts +11 -0
  316. package/dist/types/run/flows/url/types.d.ts +4 -0
  317. package/dist/types/run/markdown-transforms.d.ts +3 -0
  318. package/dist/types/run/run-context.d.ts +4 -0
  319. package/dist/types/run/run-env.d.ts +4 -0
  320. package/dist/types/run/run-settings-parse.d.ts +5 -0
  321. package/dist/types/run/runner-contexts.d.ts +62 -0
  322. package/dist/types/run/runner-execution.d.ts +57 -0
  323. package/dist/types/run/runner-flags.d.ts +41 -0
  324. package/dist/types/run/runner-setup.d.ts +21 -0
  325. package/dist/types/run/runner-slides.d.ts +8 -0
  326. package/dist/types/run/streaming.d.ts +1 -1
  327. package/dist/types/run/summary-engine.d.ts +8 -4
  328. package/dist/types/run/summary-llm.d.ts +4 -3
  329. package/dist/types/run/terminal.d.ts +2 -0
  330. package/dist/types/run/types.d.ts +2 -2
  331. package/dist/types/slides/download.d.ts +29 -0
  332. package/dist/types/slides/extract-finalize.d.ts +57 -0
  333. package/dist/types/slides/extract.d.ts +1 -7
  334. package/dist/types/slides/frame-extraction.d.ts +38 -0
  335. package/dist/types/slides/ingest.d.ts +47 -0
  336. package/dist/types/slides/ocr.d.ts +5 -0
  337. package/dist/types/slides/process.d.ts +22 -0
  338. package/dist/types/slides/scene-detection.d.ts +75 -0
  339. package/dist/types/slides/source-id.d.ts +2 -0
  340. package/dist/types/version.d.ts +1 -1
  341. package/docs/_config.yml +1 -0
  342. package/docs/agent.md +3 -2
  343. package/docs/assets/site.css +134 -2
  344. package/docs/cache.md +2 -1
  345. package/docs/chrome-extension.md +12 -4
  346. package/docs/cli.md +2 -2
  347. package/docs/config.md +11 -4
  348. package/docs/index.html +5 -0
  349. package/docs/llm.md +5 -2
  350. package/docs/manual-tests.md +3 -0
  351. package/docs/media.md +3 -1
  352. package/docs/model-auto.md +2 -2
  353. package/docs/model-provider-resolution.md +57 -0
  354. package/docs/site/index.html +5 -0
  355. package/docs/slides-rendering-flow.md +46 -0
  356. package/docs/slides.md +5 -5
  357. package/docs/smoketest.md +1 -1
  358. package/docs/transcript-provider-flow.md +66 -0
  359. package/docs/website.md +1 -0
  360. package/docs/youtube.md +4 -2
  361. package/package.json +34 -41
@@ -1,140 +1,27 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { createReadStream, promises as fs } from "node:fs";
3
2
  import http from "node:http";
4
3
  import path from "node:path";
5
4
  import { Writable } from "node:stream";
6
5
  import { loadSummarizeConfig } from "../config.js";
7
6
  import { createDaemonLogger } from "../logging/daemon.js";
8
- import { runWithProcessContext, setProcessObserver } from "../processes.js";
7
+ import { setProcessObserver } from "../processes.js";
9
8
  import { refreshFree } from "../refresh-free.js";
10
9
  import { createCacheStateFromConfig, refreshCacheStoreIfMissing } from "../run/cache-state.js";
11
10
  import { resolveExecutableInPath } from "../run/env.js";
12
- import { formatModelLabelForDisplay } from "../run/finish-line.js";
13
11
  import { createMediaCacheFromConfig } from "../run/media-cache-state.js";
14
- import { resolveRunOverrides } from "../run/run-settings.js";
15
- import { encodeSseEvent } from "../shared/sse-events.js";
16
- import { resolveSlideImagePath, resolveSlideSettings } from "../slides/index.js";
17
12
  import { resolvePackageVersion } from "../version.js";
18
- import { completeAgentResponse, streamAgentResponse } from "./agent.js";
19
- import { resolveAutoDaemonMode } from "./auto-mode.js";
13
+ import { daemonConfigTokens } from "./config.js";
20
14
  import { DAEMON_HOST, DAEMON_PORT_DEFAULT } from "./constants.js";
21
15
  import { resolveDaemonLogPaths } from "./launchd.js";
22
- import { buildModelPickerOptions } from "./models.js";
23
- import { buildProcessListResult, buildProcessLogsResult, ProcessRegistry, } from "./process-registry.js";
24
- import { extractContentForUrl, streamSummaryForUrl, streamSummaryForVisiblePage, } from "./summarize.js";
25
- function json(res, status, payload, headers) {
26
- const body = `${JSON.stringify(payload)}\n`;
27
- res.writeHead(status, {
28
- "content-type": "application/json; charset=utf-8",
29
- "content-length": Buffer.byteLength(body).toString(),
30
- ...headers,
31
- });
32
- res.end(body);
33
- }
34
- function clampNumber(value, min, max) {
35
- if (!Number.isFinite(value))
36
- return min;
37
- return Math.max(min, Math.min(max, value));
38
- }
39
- async function readLogTail({ filePath, maxBytes, maxLines, }) {
40
- const stat = await fs.stat(filePath);
41
- const size = stat.size;
42
- const readBytes = Math.max(0, Math.min(size, maxBytes));
43
- const handle = await fs.open(filePath, "r");
44
- try {
45
- const buffer = Buffer.alloc(readBytes);
46
- const start = Math.max(0, size - readBytes);
47
- await handle.read(buffer, 0, readBytes, start);
48
- let text = buffer.toString("utf8");
49
- let truncated = size > readBytes;
50
- if (truncated) {
51
- const firstNewline = text.indexOf("\n");
52
- if (firstNewline !== -1) {
53
- text = text.slice(firstNewline + 1);
54
- }
55
- }
56
- let lines = text.split(/\r?\n/).filter((line) => line.length > 0);
57
- if (lines.length > maxLines) {
58
- lines = lines.slice(lines.length - maxLines);
59
- truncated = true;
60
- }
61
- return { lines, truncated, bytesRead: readBytes };
62
- }
63
- finally {
64
- await handle.close();
65
- }
66
- }
67
- function text(res, status, body, headers) {
68
- const out = body.endsWith("\n") ? body : `${body}\n`;
69
- res.writeHead(status, {
70
- "content-type": "text/plain; charset=utf-8",
71
- "content-length": Buffer.byteLength(out).toString(),
72
- ...headers,
73
- });
74
- res.end(out);
75
- }
76
- function resolveOriginHeader(req) {
77
- const origin = req.headers.origin;
78
- if (typeof origin !== "string")
79
- return null;
80
- if (!origin.trim())
81
- return null;
82
- return origin;
83
- }
84
- function corsHeaders(origin) {
85
- if (!origin)
86
- return {};
87
- return {
88
- "access-control-allow-origin": origin,
89
- "access-control-allow-credentials": "true",
90
- "access-control-allow-headers": "authorization, content-type",
91
- "access-control-allow-methods": "GET,POST,OPTIONS",
92
- // Chrome Private Network Access (PNA): allow requests to localhost from secure contexts.
93
- // Without this, extensions often fail with a generic "Failed to fetch".
94
- "access-control-allow-private-network": "true",
95
- "access-control-max-age": "600",
96
- vary: "Origin",
97
- };
98
- }
99
- function readBearerToken(req) {
100
- const header = req.headers.authorization;
101
- if (typeof header !== "string")
102
- return null;
103
- const m = header.match(/^Bearer\s+(.+)\s*$/i);
104
- return m?.[1]?.trim() || null;
105
- }
106
- async function readJsonBody(req, maxBytes) {
107
- const chunks = [];
108
- let total = 0;
109
- for await (const chunk of req) {
110
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
111
- total += buf.byteLength;
112
- if (total > maxBytes)
113
- throw new Error(`Body too large (>${maxBytes} bytes)`);
114
- chunks.push(buf);
115
- }
116
- const text = Buffer.concat(chunks).toString("utf8");
117
- return JSON.parse(text);
118
- }
119
- function wantsJsonResponse(req, url) {
120
- const format = url.searchParams.get("format");
121
- if (format && format.toLowerCase() === "json")
122
- return true;
123
- const accept = req.headers.accept;
124
- if (typeof accept !== "string")
125
- return false;
126
- const lower = accept.toLowerCase();
127
- if (lower.includes("text/event-stream"))
128
- return false;
129
- return lower.includes("application/json");
130
- }
131
- function parseDiagnostics(raw) {
132
- if (!raw || typeof raw !== "object") {
133
- return { includeContent: false };
134
- }
135
- const obj = raw;
136
- return { includeContent: Boolean(obj.includeContent) };
137
- }
16
+ import { ProcessRegistry } from "./process-registry.js";
17
+ import { handleAdminRoutes } from "./server-admin-routes.js";
18
+ import { handleAgentRoute } from "./server-agent-route.js";
19
+ import { json, readBearerToken, readCorsHeaders, text, } from "./server-http.js";
20
+ import { handleSessionRoutes } from "./server-session-routes.js";
21
+ import { createSession, endSession, pushToSession, } from "./server-session.js";
22
+ import { executeSummarizeSession, handleExtractOnlySummarizeRequest, toExtractOnlySlidesPayload, } from "./server-summarize-execution.js";
23
+ import { parseSummarizeRequest } from "./server-summarize-request.js";
24
+ export { corsHeaders, isTrustedOrigin } from "./server-http.js";
138
25
  function createLineWriter(onLine) {
139
26
  let buffer = "";
140
27
  return new Writable({
@@ -159,168 +46,12 @@ function createLineWriter(onLine) {
159
46
  },
160
47
  });
161
48
  }
162
- function createSession() {
163
- return {
164
- id: randomUUID(),
165
- createdAtMs: Date.now(),
166
- buffer: [],
167
- bufferBytes: 0,
168
- done: false,
169
- clients: new Set(),
170
- slidesBuffer: [],
171
- slidesBufferBytes: 0,
172
- slidesClients: new Set(),
173
- slidesDone: false,
174
- slidesRequested: false,
175
- slidesLastStatus: null,
176
- lastMeta: { model: null, modelLabel: null, inputSummary: null, summaryFromCache: null },
177
- slides: null,
178
- };
179
- }
180
- const MAX_SESSION_BUFFER_EVENTS = 2000;
181
- const MAX_SESSION_BUFFER_BYTES = 512 * 1024;
182
- const MAX_SLIDES_BUFFER_EVENTS = 600;
183
- const MAX_SLIDES_BUFFER_BYTES = 256 * 1024;
184
- const MAX_SESSION_LIFETIME_MS = 30 * 60_000;
185
- function pushToSession(session, evt, onSessionEvent) {
186
- const encoded = encodeSseEvent(evt);
187
- for (const res of session.clients) {
188
- res.write(encoded);
189
- }
190
- onSessionEvent?.(evt, session.id);
191
- const bytes = Buffer.byteLength(encoded);
192
- session.buffer.push({ event: evt, bytes });
193
- session.bufferBytes += bytes;
194
- while (session.buffer.length > MAX_SESSION_BUFFER_EVENTS ||
195
- session.bufferBytes > MAX_SESSION_BUFFER_BYTES) {
196
- const removed = session.buffer.shift();
197
- if (!removed)
198
- break;
199
- session.bufferBytes -= removed.bytes;
200
- }
201
- if (evt.event === "done" || evt.event === "error") {
202
- session.done = true;
203
- }
204
- }
205
- function pushSlidesToSession(session, evt, onSessionEvent) {
206
- const encoded = encodeSseEvent(evt);
207
- for (const res of session.slidesClients) {
208
- res.write(encoded);
209
- }
210
- onSessionEvent?.(evt, session.id);
211
- const bytes = Buffer.byteLength(encoded);
212
- session.slidesBuffer.push({ event: evt, bytes });
213
- session.slidesBufferBytes += bytes;
214
- while (session.slidesBuffer.length > MAX_SLIDES_BUFFER_EVENTS ||
215
- session.slidesBufferBytes > MAX_SLIDES_BUFFER_BYTES) {
216
- const removed = session.slidesBuffer.shift();
217
- if (!removed)
218
- break;
219
- session.slidesBufferBytes -= removed.bytes;
220
- }
221
- if (evt.event === "done" || evt.event === "error") {
222
- session.slidesDone = true;
223
- }
224
- if (evt.event === "status") {
225
- session.slidesLastStatus = evt.data.text;
226
- }
227
- }
228
- function emitMeta(session, patch, onSessionEvent) {
229
- const next = { ...session.lastMeta, ...patch };
230
- if (next.model === session.lastMeta.model &&
231
- next.modelLabel === session.lastMeta.modelLabel &&
232
- next.inputSummary === session.lastMeta.inputSummary &&
233
- next.summaryFromCache === session.lastMeta.summaryFromCache) {
234
- return;
235
- }
236
- session.lastMeta = next;
237
- pushToSession(session, { event: "meta", data: next }, onSessionEvent);
238
- }
239
- function emitSlides(session, data, onSessionEvent) {
240
- pushToSession(session, { event: "slides", data }, onSessionEvent);
241
- pushSlidesToSession(session, { event: "slides", data }, onSessionEvent);
242
- }
243
- function emitSlidesStatus(session, text, onSessionEvent) {
244
- const trimmed = text.trim();
245
- if (!trimmed)
246
- return;
247
- pushSlidesToSession(session, { event: "status", data: { text: trimmed } }, onSessionEvent);
248
- }
249
- function emitSlidesDone(session, result, onSessionEvent) {
250
- if (!result.ok) {
251
- const message = result.error?.trim() || "Slides failed.";
252
- pushSlidesToSession(session, { event: "error", data: { message } }, onSessionEvent);
253
- }
254
- pushSlidesToSession(session, { event: "done", data: {} }, onSessionEvent);
255
- }
256
- function resolveHomeDir(env) {
257
- const home = env.HOME?.trim() || env.USERPROFILE?.trim();
258
- if (!home)
259
- return process.cwd();
260
- return home;
261
- }
262
- function resolveSlidesSettings({ env, request, }) {
263
- const slidesValue = request.slides;
264
- const tesseractAvailable = resolveToolPath("tesseract", env, "TESSERACT_PATH") !== null;
265
- const slidesOcrValue = tesseractAvailable ? request.slidesOcr : false;
266
- return resolveSlideSettings({
267
- slides: slidesValue,
268
- slidesOcr: slidesOcrValue,
269
- slidesDir: request.slidesDir ?? ".summarize/slides",
270
- slidesSceneThreshold: request.slidesSceneThreshold,
271
- slidesSceneThresholdExplicit: typeof request.slidesSceneThreshold !== "undefined",
272
- slidesMax: request.slidesMax,
273
- slidesMinDuration: request.slidesMinDuration,
274
- cwd: resolveHomeDir(env),
275
- });
276
- }
277
- function buildSlidesPayload({ slides, port, }) {
278
- // Use a stable URL that survives session GC, so images don't break while scrolling.
279
- const baseUrl = `http://127.0.0.1:${port}/v1/slides/${slides.sourceId}`;
280
- return {
281
- sourceUrl: slides.sourceUrl,
282
- sourceId: slides.sourceId,
283
- sourceKind: slides.sourceKind,
284
- ocrAvailable: slides.ocrAvailable,
285
- slides: slides.slides.map((slide) => ({
286
- index: slide.index,
287
- timestamp: slide.timestamp,
288
- imageUrl: `${baseUrl}/${slide.index}${typeof slide.imageVersion === "number" && slide.imageVersion > 0
289
- ? `?v=${slide.imageVersion}`
290
- : ""}`,
291
- ocrText: slide.ocrText ?? null,
292
- ocrConfidence: slide.ocrConfidence ?? null,
293
- })),
294
- };
295
- }
296
49
  function resolveToolPath(binary, env, explicitEnvKey) {
297
50
  const explicit = explicitEnvKey && typeof env[explicitEnvKey] === "string" ? env[explicitEnvKey]?.trim() : "";
298
51
  if (explicit)
299
52
  return resolveExecutableInPath(explicit, env);
300
53
  return resolveExecutableInPath(binary, env);
301
54
  }
302
- function endSession(session) {
303
- for (const res of session.clients) {
304
- res.end();
305
- }
306
- session.clients.clear();
307
- for (const res of session.slidesClients) {
308
- res.end();
309
- }
310
- session.slidesClients.clear();
311
- }
312
- function scheduleSessionCleanup({ session, sessions, delayMs = 60_000, }) {
313
- setTimeout(() => {
314
- const ageMs = Date.now() - session.createdAtMs;
315
- const slidesPending = session.slidesRequested && !session.slidesDone;
316
- if (!slidesPending || ageMs > MAX_SESSION_LIFETIME_MS) {
317
- sessions.delete(session.id);
318
- endSession(session);
319
- return;
320
- }
321
- scheduleSessionCleanup({ session, sessions, delayMs });
322
- }, delayMs).unref();
323
- }
324
55
  export function buildHealthPayload(importMetaUrl) {
325
56
  return { ok: true, pid: process.pid, version: resolvePackageVersion(importMetaUrl) };
326
57
  }
@@ -347,8 +78,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
347
78
  let activeRefreshSessionId = null;
348
79
  const server = http.createServer((req, res) => {
349
80
  void (async () => {
350
- const origin = resolveOriginHeader(req);
351
- const cors = corsHeaders(origin);
81
+ const cors = readCorsHeaders(req);
352
82
  if (req.method === "OPTIONS") {
353
83
  res.writeHead(204, cors);
354
84
  res.end();
@@ -361,107 +91,26 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
361
91
  return;
362
92
  }
363
93
  const token = readBearerToken(req);
364
- const authed = token && token === config.token;
94
+ const authed = token ? daemonConfigTokens(config).includes(token) : false;
365
95
  if (pathname.startsWith("/v1/") && !authed) {
366
96
  json(res, 401, { ok: false, error: "unauthorized" }, cors);
367
97
  return;
368
98
  }
369
- if (req.method === "GET" && pathname === "/v1/ping") {
370
- json(res, 200, { ok: true }, cors);
371
- return;
372
- }
373
- if (req.method === "GET" && pathname === "/v1/logs") {
374
- const source = url.searchParams.get("source")?.trim() || "daemon";
375
- const tailParam = url.searchParams.get("tail")?.trim() || "";
376
- const tail = clampNumber(Number(tailParam || "800"), 50, 5000);
377
- const maxBytes = clampNumber(Number(url.searchParams.get("maxBytes") ?? "262144"), 16_384, 2_000_000);
378
- const sources = {
379
- daemon: {
380
- filePath: daemonLogFile,
381
- format: daemonLogger.config?.format ?? "json",
382
- enabled: daemonLogger.enabled,
383
- },
384
- stdout: { filePath: daemonLogPaths.stdoutPath, format: "text" },
385
- stderr: { filePath: daemonLogPaths.stderrPath, format: "text" },
386
- };
387
- const selected = sources[source];
388
- if (!selected) {
389
- json(res, 400, { ok: false, error: `Unknown log source "${source}".` }, cors);
390
- return;
391
- }
392
- const stat = await fs.stat(selected.filePath).catch(() => null);
393
- if (!stat?.isFile()) {
394
- const disabledNote = source === "daemon" && selected.enabled === false
395
- ? "Daemon logging is disabled (no log file)."
396
- : "Log file not found.";
397
- json(res, 404, { ok: false, error: disabledNote }, cors);
398
- return;
399
- }
400
- const { lines, truncated, bytesRead } = await readLogTail({
401
- filePath: selected.filePath,
402
- maxBytes,
403
- maxLines: tail,
404
- });
405
- const warning = source === "daemon" && selected.enabled === false
406
- ? "Daemon logging disabled; showing existing file only."
407
- : null;
408
- json(res, 200, {
409
- ok: true,
410
- source,
411
- format: selected.format,
412
- lines,
413
- truncated,
414
- bytesRead,
415
- sizeBytes: stat.size,
416
- mtimeMs: stat.mtimeMs,
417
- ...(warning ? { warning } : {}),
418
- }, cors);
419
- return;
420
- }
421
- const processLogsMatch = pathname.match(/^\/v1\/processes\/([^/]+)\/logs$/);
422
- if (req.method === "GET" && processLogsMatch) {
423
- const id = processLogsMatch[1];
424
- const tail = clampNumber(Number(url.searchParams.get("tail") ?? "200"), 20, 1000);
425
- const streamRaw = (url.searchParams.get("stream") ?? "merged").toLowerCase();
426
- const stream = streamRaw === "stdout" || streamRaw === "stderr" ? streamRaw : "merged";
427
- const result = buildProcessLogsResult(processRegistry, id, { tail, stream });
428
- if (!result) {
429
- json(res, 404, { ok: false, error: "not found" }, cors);
430
- return;
431
- }
432
- json(res, 200, result, cors);
433
- return;
434
- }
435
- if (req.method === "GET" && pathname === "/v1/processes") {
436
- const includeCompleted = (url.searchParams.get("includeCompleted") ?? "").toLowerCase() === "true" ||
437
- url.searchParams.get("includeCompleted") === "1";
438
- const limit = clampNumber(Number(url.searchParams.get("limit") ?? "80"), 10, 200);
439
- const result = buildProcessListResult(processRegistry, { includeCompleted, limit });
440
- json(res, 200, result, cors);
441
- return;
442
- }
443
- if (req.method === "GET" && pathname === "/v1/models") {
444
- const result = await buildModelPickerOptions({
445
- env,
446
- envForRun: env,
447
- configForCli: summarizeConfig,
448
- fetchImpl,
449
- });
450
- json(res, 200, result, cors);
451
- return;
452
- }
453
- if (req.method === "GET" && pathname === "/v1/tools") {
454
- const ytDlpPath = resolveToolPath("yt-dlp", env, "YT_DLP_PATH");
455
- const ffmpegPath = resolveToolPath("ffmpeg", env, "FFMPEG_PATH");
456
- const tesseractPath = resolveToolPath("tesseract", env, "TESSERACT_PATH");
457
- json(res, 200, {
458
- ok: true,
459
- tools: {
460
- ytDlp: { available: Boolean(ytDlpPath), path: ytDlpPath },
461
- ffmpeg: { available: Boolean(ffmpegPath), path: ffmpegPath },
462
- tesseract: { available: Boolean(tesseractPath), path: tesseractPath },
463
- },
464
- }, cors);
99
+ if (await handleAdminRoutes({
100
+ req,
101
+ res,
102
+ url,
103
+ pathname,
104
+ cors,
105
+ env,
106
+ fetchImpl,
107
+ summarizeConfig,
108
+ daemonLogger,
109
+ daemonLogFile,
110
+ daemonLogPaths,
111
+ processRegistry,
112
+ resolveToolPath,
113
+ })) {
465
114
  return;
466
115
  }
467
116
  if (req.method === "POST" && pathname === "/v1/refresh-free") {
@@ -469,7 +118,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
469
118
  json(res, 200, { ok: true, id: activeRefreshSessionId, running: true }, cors);
470
119
  return;
471
120
  }
472
- const session = createSession();
121
+ const session = createSession(() => randomUUID());
473
122
  refreshSessions.set(session.id, session);
474
123
  activeRefreshSessionId = session.id;
475
124
  json(res, 200, { ok: true, id: session.id }, cors);
@@ -503,99 +152,28 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
503
152
  }
504
153
  if (req.method === "POST" && pathname === "/v1/summarize") {
505
154
  await refreshCacheStoreIfMissing({ cacheState, transcriptNamespace: "yt:auto" });
506
- let body;
507
- try {
508
- body = await readJsonBody(req, 2_000_000);
509
- }
510
- catch (error) {
511
- const message = error instanceof Error ? error.message : String(error);
512
- json(res, 400, { ok: false, error: message }, cors);
513
- return;
514
- }
515
- if (!body || typeof body !== "object") {
516
- json(res, 400, { ok: false, error: "invalid json" }, cors);
517
- return;
518
- }
519
- const obj = body;
520
- const pageUrl = typeof obj.url === "string" ? obj.url.trim() : "";
521
- const title = typeof obj.title === "string" ? obj.title.trim() : null;
522
- const textContent = typeof obj.text === "string" ? obj.text : "";
523
- const truncated = Boolean(obj.truncated);
524
- const modelOverride = typeof obj.model === "string" ? obj.model.trim() : null;
525
- const lengthRaw = typeof obj.length === "string" ? obj.length.trim() : "";
526
- const languageRaw = typeof obj.language === "string" ? obj.language.trim() : "";
527
- const promptRaw = typeof obj.prompt === "string" ? obj.prompt : "";
528
- const promptOverride = promptRaw.trim() || null;
529
- const noCache = Boolean(obj.noCache);
530
- const extractOnly = Boolean(obj.extractOnly);
531
- const modeRaw = typeof obj.mode === "string" ? obj.mode.trim().toLowerCase() : "";
532
- const mode = modeRaw === "url" ? "url" : modeRaw === "page" ? "page" : "auto";
533
- const maxCharactersCandidate = typeof obj.maxExtractCharacters === "number" && Number.isFinite(obj.maxExtractCharacters)
534
- ? obj.maxExtractCharacters
535
- : typeof obj.maxCharacters === "number" && Number.isFinite(obj.maxCharacters)
536
- ? obj.maxCharacters
537
- : null;
538
- const maxCharacters = maxCharactersCandidate && maxCharactersCandidate > 0 ? maxCharactersCandidate : null;
539
- const formatRaw = typeof obj.format === "string" ? obj.format.trim().toLowerCase() : "";
540
- const format = formatRaw === "markdown" || formatRaw === "md" ? "markdown" : "text";
541
- const overrides = resolveRunOverrides({
542
- firecrawl: obj.firecrawl,
543
- markdownMode: obj.markdownMode,
544
- preprocess: obj.preprocess,
545
- youtube: obj.youtube,
546
- videoMode: obj.videoMode,
547
- timestamps: obj.timestamps,
548
- forceSummary: obj.forceSummary,
549
- timeout: obj.timeout,
550
- retries: obj.retries,
551
- maxOutputTokens: obj.maxOutputTokens,
552
- autoCliFallback: obj.autoCliFallback,
553
- autoCliOrder: obj.autoCliOrder,
554
- magicCliAuto: obj.magicCliAuto,
555
- magicCliOrder: obj.magicCliOrder,
155
+ const request = await parseSummarizeRequest({
156
+ req,
157
+ res,
158
+ cors,
159
+ env,
160
+ resolveToolPath,
556
161
  });
557
- const slidesSettings = resolveSlidesSettings({ env, request: obj });
558
- const diagnostics = parseDiagnostics(obj.diagnostics);
559
- const includeContentLog = daemonLogger.enabled && diagnostics.includeContent;
560
- const hasText = Boolean(textContent.trim());
561
- if (!pageUrl || !/^https?:\/\//i.test(pageUrl)) {
562
- json(res, 400, { ok: false, error: "missing url" }, cors);
162
+ if (!request) {
563
163
  return;
564
164
  }
165
+ const { pageUrl, title, textContent, truncated, modelOverride, lengthRaw, languageRaw, promptOverride, noCache, extractOnly, mode, maxCharacters, format, overrides, slidesSettings, diagnostics, hasText, } = request;
166
+ const includeContentLog = daemonLogger.enabled && diagnostics.includeContent;
565
167
  if (extractOnly) {
566
- if (mode === "page") {
567
- json(res, 400, { ok: false, error: "extractOnly requires mode=url" }, cors);
568
- return;
569
- }
570
168
  try {
571
- const requestCache = noCache
572
- ? { ...cacheState, mode: "bypass", store: null }
573
- : cacheState;
574
- const runId = randomUUID();
575
- const { extracted, slides } = await runWithProcessContext({ runId, source: "extract" }, async () => extractContentForUrl({
169
+ const { extracted, slides } = await handleExtractOnlySummarizeRequest({
170
+ request,
576
171
  env,
577
172
  fetchImpl,
578
- input: { url: pageUrl, title, maxCharacters },
579
- cache: requestCache,
173
+ cacheState,
580
174
  mediaCache,
581
- overrides,
582
- format,
583
- slides: slidesSettings,
584
- }));
585
- const slidesPayload = slides && slides.slides.length > 0
586
- ? {
587
- sourceUrl: slides.sourceUrl,
588
- sourceId: slides.sourceId,
589
- sourceKind: slides.sourceKind,
590
- ocrAvailable: slides.ocrAvailable,
591
- slides: slides.slides.map((slide) => ({
592
- index: slide.index,
593
- timestamp: slide.timestamp,
594
- ocrText: slide.ocrText ?? null,
595
- ocrConfidence: slide.ocrConfidence ?? null,
596
- })),
597
- }
598
- : null;
175
+ });
176
+ const slidesPayload = toExtractOnlySlidesPayload(slides);
599
177
  json(res, 200, {
600
178
  ok: true,
601
179
  extracted: {
@@ -624,11 +202,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
624
202
  }
625
203
  return;
626
204
  }
627
- if (mode === "page" && !hasText) {
628
- json(res, 400, { ok: false, error: "missing text" }, cors);
629
- return;
630
- }
631
- const session = createSession();
205
+ const session = createSession(() => randomUUID());
632
206
  session.slidesRequested = Boolean(slidesSettings);
633
207
  sessions.set(session.id, session);
634
208
  const requestLogger = daemonLogger.getSubLogger("daemon.summarize", {
@@ -673,666 +247,43 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
673
247
  ...(includeContentLog ? { diagnostics } : {}),
674
248
  });
675
249
  json(res, 200, { ok: true, id: session.id }, cors);
676
- void runWithProcessContext({ runId: session.id, source: "summarize" }, async () => {
677
- const slideLogState = {
678
- startedAt: null,
679
- requested: Boolean(slidesSettings),
680
- cacheHit: false,
681
- lastStatus: null,
682
- statusCount: 0,
683
- elapsedMs: null,
684
- slidesCount: null,
685
- ocrAvailable: null,
686
- warnings: [],
687
- };
688
- try {
689
- let emittedOutput = false;
690
- const sink = {
691
- writeChunk: (chunk) => {
692
- emittedOutput = true;
693
- if (includeContentLog) {
694
- logSummaryText += chunk;
695
- }
696
- pushToSession(session, { event: "chunk", data: { text: chunk } }, onSessionEvent);
697
- },
698
- onModelChosen: (modelId) => {
699
- if (session.lastMeta.model === modelId)
700
- return;
701
- emittedOutput = true;
702
- emitMeta(session, {
703
- model: modelId,
704
- modelLabel: formatModelLabelForDisplay(modelId),
705
- }, onSessionEvent);
706
- },
707
- writeStatus: (text) => {
708
- const clean = text.trim();
709
- if (!clean)
710
- return;
711
- pushToSession(session, { event: "status", data: { text: clean } }, onSessionEvent);
712
- },
713
- writeMeta: (data) => {
714
- if (typeof data.inputSummary === "string") {
715
- logInputSummary = data.inputSummary;
716
- }
717
- if (typeof data.summaryFromCache === "boolean") {
718
- logSummaryFromCache = data.summaryFromCache;
719
- }
720
- emitMeta(session, {
721
- inputSummary: typeof data.inputSummary === "string" ? data.inputSummary : null,
722
- summaryFromCache: typeof data.summaryFromCache === "boolean" ? data.summaryFromCache : null,
723
- }, onSessionEvent);
724
- },
725
- };
726
- const normalizedModelOverride = modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null;
727
- const requestCache = noCache
728
- ? { ...cacheState, mode: "bypass", store: null }
729
- : cacheState;
730
- let liveSlides = null;
731
- const runWithMode = async (resolved) => {
732
- if (resolved === "url" && slideLogState.requested) {
733
- slideLogState.startedAt = Date.now();
734
- console.log(`[summarize-daemon] slides: start url=${pageUrl} (session=${session.id})`);
735
- if (includeContentLog) {
736
- requestLogger?.info({
737
- event: "slides.start",
738
- url: pageUrl,
739
- sessionId: session.id,
740
- ...(logSlidesSettings ? { settings: logSlidesSettings } : {}),
741
- });
742
- }
743
- }
744
- return resolved === "url"
745
- ? await streamSummaryForUrl({
746
- env,
747
- fetchImpl,
748
- modelOverride: normalizedModelOverride,
749
- promptOverride,
750
- lengthRaw,
751
- languageRaw,
752
- format,
753
- input: { url: pageUrl, title, maxCharacters },
754
- sink,
755
- cache: requestCache,
756
- mediaCache,
757
- overrides,
758
- slides: slidesSettings,
759
- hooks: {
760
- ...(includeContentLog
761
- ? {
762
- onExtracted: (content) => {
763
- logExtracted = content;
764
- },
765
- }
766
- : {}),
767
- onSlidesExtracted: (slides) => {
768
- session.slides = slides;
769
- slideLogState.slidesCount = slides.slides.length;
770
- slideLogState.ocrAvailable = slides.ocrAvailable;
771
- slideLogState.warnings = slides.warnings;
772
- if (slideLogState.startedAt) {
773
- slideLogState.elapsedMs = Date.now() - slideLogState.startedAt;
774
- }
775
- if (slideLogState.startedAt) {
776
- const elapsedMs = Date.now() - slideLogState.startedAt;
777
- console.log(`[summarize-daemon] slides: done count=${slides.slides.length} ocr=${slides.ocrAvailable} elapsedMs=${elapsedMs} warnings=${slides.warnings.join("; ")}`);
778
- }
779
- if (includeContentLog) {
780
- requestLogger?.info({
781
- event: "slides.done",
782
- url: pageUrl,
783
- sessionId: session.id,
784
- slidesCount: slides.slides.length,
785
- ocrAvailable: slides.ocrAvailable,
786
- elapsedMs: slideLogState.elapsedMs,
787
- cacheHit: slideLogState.cacheHit,
788
- warnings: slides.warnings,
789
- });
790
- }
791
- emitSlides(session, buildSlidesPayload({
792
- slides,
793
- port,
794
- }), onSessionEvent);
795
- },
796
- onSlidesDone: (result) => {
797
- emitSlidesDone(session, result, onSessionEvent);
798
- },
799
- onSlidesProgress: (text) => {
800
- const clean = typeof text === "string" ? text.trim() : "";
801
- if (!clean)
802
- return;
803
- slideLogState.lastStatus = clean;
804
- slideLogState.statusCount += 1;
805
- if (clean.toLowerCase().includes("cached")) {
806
- slideLogState.cacheHit = true;
807
- }
808
- const progressMatch = clean.match(/(\d+)%/);
809
- const progress = progressMatch ? Number(progressMatch[1]) : null;
810
- if (includeContentLog) {
811
- requestLogger?.info({
812
- event: "slides.status",
813
- url: pageUrl,
814
- sessionId: session.id,
815
- status: clean,
816
- ...(progress !== null ? { progress } : {}),
817
- });
818
- }
819
- emitSlidesStatus(session, clean, onSessionEvent);
820
- },
821
- onSlideChunk: (chunk) => {
822
- const { slide, meta } = chunk;
823
- if (slide == null ||
824
- !meta?.slidesDir ||
825
- !meta.sourceUrl ||
826
- !meta.sourceId ||
827
- !meta.sourceKind) {
828
- return;
829
- }
830
- const nextSlides = liveSlides ?? {
831
- sourceUrl: meta.sourceUrl,
832
- sourceKind: meta.sourceKind,
833
- sourceId: meta.sourceId,
834
- slidesDir: meta.slidesDir,
835
- sceneThreshold: 0,
836
- autoTuneThreshold: false,
837
- autoTune: {
838
- enabled: false,
839
- chosenThreshold: 0,
840
- confidence: 0,
841
- strategy: "none",
842
- },
843
- maxSlides: 0,
844
- minSlideDuration: 0,
845
- ocrRequested: meta.ocrAvailable,
846
- ocrAvailable: meta.ocrAvailable,
847
- slides: [],
848
- warnings: [],
849
- };
850
- liveSlides = nextSlides;
851
- const existingIndex = nextSlides.slides.findIndex((item) => item.index === slide.index);
852
- if (existingIndex >= 0) {
853
- nextSlides.slides[existingIndex] = {
854
- ...nextSlides.slides[existingIndex],
855
- ...slide,
856
- };
857
- }
858
- else {
859
- nextSlides.slides.push(slide);
860
- }
861
- nextSlides.slides.sort((a, b) => a.index - b.index);
862
- session.slides = nextSlides;
863
- emitSlides(session, buildSlidesPayload({
864
- slides: nextSlides,
865
- port,
866
- }), onSessionEvent);
867
- },
868
- },
869
- })
870
- : await streamSummaryForVisiblePage({
871
- env,
872
- fetchImpl,
873
- modelOverride: normalizedModelOverride,
874
- promptOverride,
875
- lengthRaw,
876
- languageRaw,
877
- format,
878
- input: { url: pageUrl, title, text: textContent, truncated },
879
- sink,
880
- cache: requestCache,
881
- mediaCache,
882
- overrides,
883
- });
884
- };
885
- const result = await (async () => {
886
- if (mode !== "auto")
887
- return runWithMode(mode);
888
- const { primary, fallback } = resolveAutoDaemonMode({ url: pageUrl, hasText });
889
- try {
890
- return await runWithMode(primary);
891
- }
892
- catch (error) {
893
- if (!fallback || emittedOutput)
894
- throw error;
895
- sink.writeStatus?.("Primary failed. Trying fallback…");
896
- try {
897
- return await runWithMode(fallback);
898
- }
899
- catch (fallbackError) {
900
- const primaryMessage = error instanceof Error ? error.message : String(error);
901
- const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
902
- throw new Error(`Auto mode failed.\nPrimary (${primary}): ${primaryMessage}\nFallback (${fallback}): ${fallbackMessage}`);
903
- }
904
- }
905
- })();
906
- if (!session.lastMeta.model) {
907
- emitMeta(session, {
908
- model: result.usedModel,
909
- modelLabel: formatModelLabelForDisplay(result.usedModel),
910
- }, onSessionEvent);
911
- }
912
- pushToSession(session, { event: "metrics", data: result.metrics }, onSessionEvent);
913
- pushToSession(session, { event: "done", data: {} }, onSessionEvent);
914
- requestLogger?.info({
915
- event: "summarize.done",
916
- url: pageUrl,
917
- mode,
918
- model: result.usedModel,
919
- elapsedMs: Date.now() - logStartedAt,
920
- summaryFromCache: logSummaryFromCache,
921
- inputSummary: logInputSummary,
922
- ...(includeContentLog && slideLogState.requested
923
- ? {
924
- slides: {
925
- requested: true,
926
- cacheHit: slideLogState.cacheHit,
927
- lastStatus: slideLogState.lastStatus,
928
- statusCount: slideLogState.statusCount,
929
- elapsedMs: slideLogState.elapsedMs,
930
- slidesCount: slideLogState.slidesCount,
931
- ocrAvailable: slideLogState.ocrAvailable,
932
- warnings: slideLogState.warnings,
933
- },
934
- }
935
- : {}),
936
- ...(includeContentLog && !logSummaryFromCache
937
- ? {
938
- input: logInput,
939
- extracted: logExtracted,
940
- summary: logSummaryText,
941
- }
942
- : {}),
943
- });
944
- }
945
- catch (error) {
946
- const message = error instanceof Error ? error.message : String(error);
947
- pushToSession(session, { event: "error", data: { message } }, onSessionEvent);
948
- if (session.slidesRequested && !session.slidesDone) {
949
- emitSlidesDone(session, { ok: false, error: message }, onSessionEvent);
950
- }
951
- // Preserve full stack trace in daemon logs for debugging.
952
- console.error("[summarize-daemon] summarize failed", error);
953
- requestLogger?.error({
954
- event: "summarize.error",
955
- url: pageUrl,
956
- mode,
957
- elapsedMs: Date.now() - logStartedAt,
958
- summaryFromCache: logSummaryFromCache,
959
- inputSummary: logInputSummary,
960
- ...(includeContentLog && slideLogState.requested
961
- ? {
962
- slides: {
963
- requested: true,
964
- cacheHit: slideLogState.cacheHit,
965
- lastStatus: slideLogState.lastStatus,
966
- statusCount: slideLogState.statusCount,
967
- elapsedMs: slideLogState.elapsedMs,
968
- slidesCount: slideLogState.slidesCount,
969
- ocrAvailable: slideLogState.ocrAvailable,
970
- warnings: slideLogState.warnings,
971
- },
972
- }
973
- : {}),
974
- error: {
975
- message,
976
- stack: error instanceof Error ? error.stack : null,
977
- },
978
- ...(includeContentLog && !logSummaryFromCache
979
- ? {
980
- input: logInput,
981
- extracted: logExtracted,
982
- summary: logSummaryText || null,
983
- }
984
- : {}),
985
- });
986
- }
987
- finally {
988
- scheduleSessionCleanup({ session, sessions });
989
- }
990
- });
991
- return;
992
- }
993
- if (req.method === "POST" && pathname === "/v1/agent") {
994
- let body;
995
- try {
996
- body = await readJsonBody(req, 4_000_000);
997
- }
998
- catch (error) {
999
- const message = error instanceof Error ? error.message : String(error);
1000
- json(res, 400, { ok: false, error: message }, cors);
1001
- return;
1002
- }
1003
- if (!body || typeof body !== "object") {
1004
- json(res, 400, { ok: false, error: "invalid json" }, cors);
1005
- return;
1006
- }
1007
- const obj = body;
1008
- const pageUrl = typeof obj.url === "string" ? obj.url.trim() : "";
1009
- const pageTitle = typeof obj.title === "string" ? obj.title.trim() : null;
1010
- const pageContent = typeof obj.pageContent === "string" ? obj.pageContent : "";
1011
- const messages = obj.messages;
1012
- const modelOverride = typeof obj.model === "string" ? obj.model.trim() : null;
1013
- const tools = Array.isArray(obj.tools)
1014
- ? obj.tools.filter((tool) => typeof tool === "string")
1015
- : [];
1016
- const automationEnabled = Boolean(obj.automationEnabled);
1017
- if (!pageUrl) {
1018
- json(res, 400, { ok: false, error: "missing url" }, cors);
1019
- return;
1020
- }
1021
- const runId = `agent-${randomUUID()}`;
1022
- const wantsJson = wantsJsonResponse(req, url);
1023
- if (wantsJson) {
1024
- try {
1025
- const assistant = await runWithProcessContext({ runId, source: "agent" }, async () => completeAgentResponse({
1026
- env,
1027
- pageUrl,
1028
- pageTitle,
1029
- pageContent,
1030
- messages,
1031
- modelOverride: modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null,
1032
- tools,
1033
- automationEnabled,
1034
- }));
1035
- json(res, 200, { ok: true, assistant }, cors);
1036
- }
1037
- catch (error) {
1038
- const message = error instanceof Error ? error.message : String(error);
1039
- console.error("[summarize-daemon] agent failed", error);
1040
- json(res, 500, { ok: false, error: message }, cors);
1041
- }
1042
- return;
1043
- }
1044
- res.writeHead(200, {
1045
- "content-type": "text/event-stream; charset=utf-8",
1046
- "cache-control": "no-cache",
1047
- connection: "keep-alive",
1048
- "x-accel-buffering": "no",
1049
- ...cors,
1050
- });
1051
- const controller = new AbortController();
1052
- const abort = () => controller.abort();
1053
- req.on("close", abort);
1054
- res.on("close", abort);
1055
- const writeEvent = (event) => {
1056
- if (res.writableEnded)
1057
- return;
1058
- res.write(encodeSseEvent(event));
1059
- };
1060
- try {
1061
- await runWithProcessContext({ runId, source: "agent" }, async () => streamAgentResponse({
1062
- env,
1063
- pageUrl,
1064
- pageTitle,
1065
- pageContent,
1066
- messages,
1067
- modelOverride: modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null,
1068
- tools,
1069
- automationEnabled,
1070
- onChunk: (text) => writeEvent({ event: "chunk", data: { text } }),
1071
- onAssistant: (assistant) => writeEvent({ event: "assistant", data: assistant }),
1072
- signal: controller.signal,
1073
- }));
1074
- writeEvent({ event: "done", data: {} });
1075
- res.end();
1076
- }
1077
- catch (error) {
1078
- if (controller.signal.aborted)
1079
- return;
1080
- const message = error instanceof Error ? error.message : String(error);
1081
- console.error("[summarize-daemon] agent failed", error);
1082
- writeEvent({ event: "error", data: { message } });
1083
- writeEvent({ event: "done", data: {} });
1084
- res.end();
1085
- }
1086
- return;
1087
- }
1088
- const slidesMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides$/);
1089
- if (req.method === "GET" && slidesMatch) {
1090
- const id = slidesMatch[1];
1091
- const session = id ? sessions.get(id) : null;
1092
- if (!session || !session.slides) {
1093
- json(res, 200, { ok: false, error: "not found" }, cors);
1094
- return;
1095
- }
1096
- json(res, 200, { ok: true, slides: buildSlidesPayload({ slides: session.slides, port }) }, cors);
1097
- return;
1098
- }
1099
- const slideImageMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides\/(\d+)$/);
1100
- if (req.method === "GET" && slideImageMatch) {
1101
- const id = slideImageMatch[1];
1102
- const index = Number(slideImageMatch[2]);
1103
- const session = id ? sessions.get(id) : null;
1104
- if (!session || !session.slides || !Number.isFinite(index)) {
1105
- json(res, 404, { ok: false, error: "not found" }, cors);
1106
- return;
1107
- }
1108
- const slide = session.slides.slides.find((item) => item.index === index);
1109
- if (!slide) {
1110
- json(res, 404, { ok: false, error: "not found" }, cors);
1111
- return;
1112
- }
1113
- try {
1114
- const stat = await fs.stat(slide.imagePath);
1115
- res.writeHead(200, {
1116
- "content-type": "image/png",
1117
- "content-length": stat.size.toString(),
1118
- "cache-control": "no-cache",
1119
- ...cors,
1120
- });
1121
- const stream = createReadStream(slide.imagePath);
1122
- stream.pipe(res);
1123
- stream.on("error", () => res.end());
1124
- }
1125
- catch {
1126
- json(res, 404, { ok: false, error: "not found" }, cors);
1127
- }
1128
- return;
1129
- }
1130
- const stableSlideImageMatch = pathname.match(/^\/v1\/slides\/([^/]+)\/(\d+)$/);
1131
- if (req.method === "GET" && stableSlideImageMatch) {
1132
- const sourceId = stableSlideImageMatch[1];
1133
- const index = Number(stableSlideImageMatch[2]);
1134
- if (!sourceId || !Number.isFinite(index) || index <= 0) {
1135
- json(res, 404, { ok: false, error: "not found" }, cors);
1136
- return;
1137
- }
1138
- const slidesRoot = path.resolve(resolveHomeDir(env), ".summarize", "slides");
1139
- const slidesDir = path.join(slidesRoot, sourceId);
1140
- const payloadPath = path.join(slidesDir, "slides.json");
1141
- const resolveFromDisk = async () => {
1142
- const raw = await fs.readFile(payloadPath, "utf8").catch(() => null);
1143
- if (raw) {
1144
- try {
1145
- const parsed = JSON.parse(raw);
1146
- const slide = parsed?.slides?.find?.((item) => item?.index === index);
1147
- if (slide?.imagePath) {
1148
- const resolved = resolveSlideImagePath(slidesDir, slide.imagePath);
1149
- if (resolved)
1150
- return resolved;
1151
- }
1152
- }
1153
- catch {
1154
- // fall through
1155
- }
1156
- }
1157
- const prefix = `slide_${String(index).padStart(4, "0")}`;
1158
- const entries = await fs.readdir(slidesDir).catch(() => null);
1159
- if (!entries)
1160
- return null;
1161
- const candidates = entries
1162
- .filter((name) => name.startsWith(prefix) && name.endsWith(".png"))
1163
- .map((name) => path.join(slidesDir, name));
1164
- if (candidates.length === 0)
1165
- return null;
1166
- let best = null;
1167
- for (const filePath of candidates) {
1168
- const stat = await fs.stat(filePath).catch(() => null);
1169
- if (!stat?.isFile())
1170
- continue;
1171
- const mtimeMs = stat.mtimeMs;
1172
- if (!best || mtimeMs > best.mtimeMs)
1173
- best = { filePath, mtimeMs };
1174
- }
1175
- return best?.filePath ?? null;
1176
- };
1177
- const filePath = await resolveFromDisk();
1178
- if (!filePath) {
1179
- // Return a tiny transparent PNG (placeholder) instead of 404 to avoid broken-image icons
1180
- // while extraction is still running.
1181
- const placeholder = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3kq0cAAAAASUVORK5CYII=", "base64");
1182
- res.writeHead(200, {
1183
- "content-type": "image/png",
1184
- "content-length": placeholder.length.toString(),
1185
- "cache-control": "no-store",
1186
- "x-summarize-slide-ready": "0",
1187
- ...cors,
1188
- });
1189
- res.end(placeholder);
1190
- return;
1191
- }
1192
- try {
1193
- const stat = await fs.stat(filePath);
1194
- res.writeHead(200, {
1195
- "content-type": "image/png",
1196
- "content-length": stat.size.toString(),
1197
- "cache-control": "no-store",
1198
- "x-summarize-slide-ready": "1",
1199
- ...cors,
1200
- });
1201
- const stream = createReadStream(filePath);
1202
- stream.pipe(res);
1203
- stream.on("error", () => res.end());
1204
- }
1205
- catch {
1206
- json(res, 404, { ok: false, error: "not found" }, cors);
1207
- }
1208
- return;
1209
- }
1210
- const eventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/events$/);
1211
- if (req.method === "GET" && eventsMatch) {
1212
- const id = eventsMatch[1];
1213
- if (!id) {
1214
- json(res, 404, { ok: false }, cors);
1215
- return;
1216
- }
1217
- const session = sessions.get(id);
1218
- if (!session) {
1219
- json(res, 404, { ok: false, error: "not found" }, cors);
1220
- return;
1221
- }
1222
- res.writeHead(200, {
1223
- ...cors,
1224
- "content-type": "text/event-stream; charset=utf-8",
1225
- "cache-control": "no-cache, no-transform",
1226
- connection: "keep-alive",
1227
- });
1228
- session.clients.add(res);
1229
- for (const entry of session.buffer) {
1230
- res.write(encodeSseEvent(entry.event));
1231
- }
1232
- if (session.done) {
1233
- res.end();
1234
- session.clients.delete(res);
1235
- return;
1236
- }
1237
- const keepalive = setInterval(() => {
1238
- res.write(`: keepalive ${Date.now()}\n\n`);
1239
- }, 15_000);
1240
- keepalive.unref();
1241
- res.on("close", () => {
1242
- clearInterval(keepalive);
1243
- session.clients.delete(res);
250
+ void executeSummarizeSession({
251
+ session,
252
+ request,
253
+ env,
254
+ fetchImpl,
255
+ cacheState,
256
+ mediaCache,
257
+ port,
258
+ onSessionEvent,
259
+ requestLogger,
260
+ includeContentLog,
261
+ logStartedAt,
262
+ logInput,
263
+ logSlidesSettings,
264
+ sessions,
265
+ refreshSessions,
1244
266
  });
1245
267
  return;
1246
268
  }
1247
- const slidesEventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides\/events$/);
1248
- if (req.method === "GET" && slidesEventsMatch) {
1249
- const id = slidesEventsMatch[1];
1250
- if (!id) {
1251
- json(res, 404, { ok: false }, cors);
1252
- return;
1253
- }
1254
- const session = sessions.get(id);
1255
- if (!session || !session.slidesRequested) {
1256
- json(res, 404, { ok: false, error: "not found" }, cors);
1257
- return;
1258
- }
1259
- res.writeHead(200, {
1260
- ...cors,
1261
- "content-type": "text/event-stream; charset=utf-8",
1262
- "cache-control": "no-cache, no-transform",
1263
- connection: "keep-alive",
1264
- });
1265
- session.slidesClients.add(res);
1266
- for (const entry of session.slidesBuffer) {
1267
- res.write(encodeSseEvent(entry.event));
1268
- }
1269
- const hasSlidesEvent = session.slidesBuffer.some((entry) => entry.event.event === "slides");
1270
- if (!hasSlidesEvent && session.slides) {
1271
- res.write(encodeSseEvent({
1272
- event: "slides",
1273
- data: buildSlidesPayload({ slides: session.slides, port }),
1274
- }));
1275
- }
1276
- const hasStatusEvent = session.slidesBuffer.some((entry) => entry.event.event === "status");
1277
- if (!hasStatusEvent && session.slidesLastStatus) {
1278
- res.write(encodeSseEvent({ event: "status", data: { text: session.slidesLastStatus } }));
1279
- }
1280
- if (session.slidesDone) {
1281
- res.end();
1282
- session.slidesClients.delete(res);
1283
- return;
1284
- }
1285
- const keepalive = setInterval(() => {
1286
- res.write(`: keepalive ${Date.now()}\n\n`);
1287
- }, 15_000);
1288
- keepalive.unref();
1289
- res.on("close", () => {
1290
- clearInterval(keepalive);
1291
- session.slidesClients.delete(res);
1292
- });
269
+ if (await handleAgentRoute({ req, res, url, cors, env, createRunId: randomUUID })) {
1293
270
  return;
1294
271
  }
1295
- const refreshEventsMatch = pathname.match(/^\/v1\/refresh-free\/([^/]+)\/events$/);
1296
- if (req.method === "GET" && refreshEventsMatch) {
1297
- const id = refreshEventsMatch[1];
1298
- if (!id) {
1299
- json(res, 404, { ok: false }, cors);
1300
- return;
1301
- }
1302
- const session = refreshSessions.get(id);
1303
- if (!session) {
1304
- json(res, 404, { ok: false, error: "not found" }, cors);
1305
- return;
1306
- }
1307
- res.writeHead(200, {
1308
- ...cors,
1309
- "content-type": "text/event-stream; charset=utf-8",
1310
- "cache-control": "no-cache, no-transform",
1311
- connection: "keep-alive",
1312
- });
1313
- session.clients.add(res);
1314
- for (const entry of session.buffer) {
1315
- res.write(encodeSseEvent(entry.event));
1316
- }
1317
- if (session.done) {
1318
- res.end();
1319
- session.clients.delete(res);
1320
- return;
1321
- }
1322
- const keepalive = setInterval(() => {
1323
- res.write(`: keepalive ${Date.now()}\n\n`);
1324
- }, 15_000);
1325
- keepalive.unref();
1326
- res.on("close", () => {
1327
- clearInterval(keepalive);
1328
- session.clients.delete(res);
1329
- });
272
+ if (await handleSessionRoutes({
273
+ req,
274
+ res,
275
+ pathname,
276
+ cors,
277
+ env,
278
+ port,
279
+ sessions,
280
+ refreshSessions,
281
+ })) {
1330
282
  return;
1331
283
  }
1332
284
  text(res, 404, "Not found", cors);
1333
285
  })().catch((error) => {
1334
- const origin = resolveOriginHeader(req);
1335
- const cors = corsHeaders(origin);
286
+ const cors = readCorsHeaders(req);
1336
287
  const message = error instanceof Error ? error.message : String(error);
1337
288
  if (!res.headersSent) {
1338
289
  json(res, 500, { ok: false, error: message }, cors);