@steipete/summarize 0.9.0 → 0.11.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 (398) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +1 -1
  3. package/README.md +391 -183
  4. package/dist/cli.js +1 -1
  5. package/dist/esm/cache.js +134 -64
  6. package/dist/esm/cache.js.map +1 -1
  7. package/dist/esm/cli-main.js +27 -27
  8. package/dist/esm/cli-main.js.map +1 -1
  9. package/dist/esm/cli.js +2 -2
  10. package/dist/esm/cli.js.map +1 -1
  11. package/dist/esm/config.js +396 -126
  12. package/dist/esm/config.js.map +1 -1
  13. package/dist/esm/content/asset.js +53 -50
  14. package/dist/esm/content/asset.js.map +1 -1
  15. package/dist/esm/content/index.js +1 -1
  16. package/dist/esm/content/index.js.map +1 -1
  17. package/dist/esm/costs.js +1 -1
  18. package/dist/esm/costs.js.map +1 -1
  19. package/dist/esm/daemon/agent.js +548 -0
  20. package/dist/esm/daemon/agent.js.map +1 -0
  21. package/dist/esm/daemon/auto-mode.js +3 -3
  22. package/dist/esm/daemon/auto-mode.js.map +1 -1
  23. package/dist/esm/daemon/chat.js +88 -178
  24. package/dist/esm/daemon/chat.js.map +1 -1
  25. package/dist/esm/daemon/cli-entrypoint.js +72 -0
  26. package/dist/esm/daemon/cli-entrypoint.js.map +1 -0
  27. package/dist/esm/daemon/cli.js +91 -83
  28. package/dist/esm/daemon/cli.js.map +1 -1
  29. package/dist/esm/daemon/config.js +15 -15
  30. package/dist/esm/daemon/config.js.map +1 -1
  31. package/dist/esm/daemon/constants.js +6 -6
  32. package/dist/esm/daemon/constants.js.map +1 -1
  33. package/dist/esm/daemon/env-merge.js.map +1 -1
  34. package/dist/esm/daemon/env-snapshot.js +36 -28
  35. package/dist/esm/daemon/env-snapshot.js.map +1 -1
  36. package/dist/esm/daemon/flow-context.js +86 -32
  37. package/dist/esm/daemon/flow-context.js.map +1 -1
  38. package/dist/esm/daemon/launchd.js +119 -47
  39. package/dist/esm/daemon/launchd.js.map +1 -1
  40. package/dist/esm/daemon/meta.js +5 -5
  41. package/dist/esm/daemon/meta.js.map +1 -1
  42. package/dist/esm/daemon/models.js +54 -31
  43. package/dist/esm/daemon/models.js.map +1 -1
  44. package/dist/esm/daemon/process-registry.js +206 -0
  45. package/dist/esm/daemon/process-registry.js.map +1 -0
  46. package/dist/esm/daemon/schtasks.js +96 -32
  47. package/dist/esm/daemon/schtasks.js.map +1 -1
  48. package/dist/esm/daemon/server.js +832 -158
  49. package/dist/esm/daemon/server.js.map +1 -1
  50. package/dist/esm/daemon/summarize-progress.js +11 -11
  51. package/dist/esm/daemon/summarize-progress.js.map +1 -1
  52. package/dist/esm/daemon/summarize.js +61 -32
  53. package/dist/esm/daemon/summarize.js.map +1 -1
  54. package/dist/esm/daemon/systemd.js +96 -35
  55. package/dist/esm/daemon/systemd.js.map +1 -1
  56. package/dist/esm/firecrawl.js +12 -12
  57. package/dist/esm/firecrawl.js.map +1 -1
  58. package/dist/esm/flags.js +55 -31
  59. package/dist/esm/flags.js.map +1 -1
  60. package/dist/esm/index.js +3 -3
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/language.js +1 -1
  63. package/dist/esm/language.js.map +1 -1
  64. package/dist/esm/llm/cli.js +128 -64
  65. package/dist/esm/llm/cli.js.map +1 -1
  66. package/dist/esm/llm/errors.js +1 -1
  67. package/dist/esm/llm/errors.js.map +1 -1
  68. package/dist/esm/llm/generate-text.js +107 -98
  69. package/dist/esm/llm/generate-text.js.map +1 -1
  70. package/dist/esm/llm/google-models.js +17 -17
  71. package/dist/esm/llm/google-models.js.map +1 -1
  72. package/dist/esm/llm/html-to-markdown.js +3 -3
  73. package/dist/esm/llm/html-to-markdown.js.map +1 -1
  74. package/dist/esm/llm/model-id.js +38 -16
  75. package/dist/esm/llm/model-id.js.map +1 -1
  76. package/dist/esm/llm/prompt.js +5 -5
  77. package/dist/esm/llm/prompt.js.map +1 -1
  78. package/dist/esm/llm/providers/anthropic.js +33 -33
  79. package/dist/esm/llm/providers/anthropic.js.map +1 -1
  80. package/dist/esm/llm/providers/google.js +19 -19
  81. package/dist/esm/llm/providers/google.js.map +1 -1
  82. package/dist/esm/llm/providers/models.js +30 -30
  83. package/dist/esm/llm/providers/models.js.map +1 -1
  84. package/dist/esm/llm/providers/openai.js +36 -35
  85. package/dist/esm/llm/providers/openai.js.map +1 -1
  86. package/dist/esm/llm/providers/shared.js +8 -8
  87. package/dist/esm/llm/providers/shared.js.map +1 -1
  88. package/dist/esm/llm/transcript-to-markdown.js +9 -5
  89. package/dist/esm/llm/transcript-to-markdown.js.map +1 -1
  90. package/dist/esm/llm/usage.js +18 -18
  91. package/dist/esm/llm/usage.js.map +1 -1
  92. package/dist/esm/logging/daemon.js +21 -21
  93. package/dist/esm/logging/daemon.js.map +1 -1
  94. package/dist/esm/logging/ring-file.js +5 -5
  95. package/dist/esm/logging/ring-file.js.map +1 -1
  96. package/dist/esm/markitdown.js +21 -19
  97. package/dist/esm/markitdown.js.map +1 -1
  98. package/dist/esm/media-cache.js +251 -0
  99. package/dist/esm/media-cache.js.map +1 -0
  100. package/dist/esm/model-auto.js +175 -106
  101. package/dist/esm/model-auto.js.map +1 -1
  102. package/dist/esm/model-spec.js +52 -42
  103. package/dist/esm/model-spec.js.map +1 -1
  104. package/dist/esm/pricing/litellm.js +4 -4
  105. package/dist/esm/pricing/litellm.js.map +1 -1
  106. package/dist/esm/processes.js +2 -0
  107. package/dist/esm/processes.js.map +1 -0
  108. package/dist/esm/prompts/index.js +1 -1
  109. package/dist/esm/prompts/index.js.map +1 -1
  110. package/dist/esm/refresh-free.js +81 -81
  111. package/dist/esm/refresh-free.js.map +1 -1
  112. package/dist/esm/run/attachments.js +47 -44
  113. package/dist/esm/run/attachments.js.map +1 -1
  114. package/dist/esm/run/bird.js +125 -12
  115. package/dist/esm/run/bird.js.map +1 -1
  116. package/dist/esm/run/cache-state.js +7 -7
  117. package/dist/esm/run/cache-state.js.map +1 -1
  118. package/dist/esm/run/cli-fallback-state.js +45 -0
  119. package/dist/esm/run/cli-fallback-state.js.map +1 -0
  120. package/dist/esm/run/cli-preflight.js +40 -22
  121. package/dist/esm/run/cli-preflight.js.map +1 -1
  122. package/dist/esm/run/constants.js +12 -12
  123. package/dist/esm/run/constants.js.map +1 -1
  124. package/dist/esm/run/cookies/twitter.js +47 -47
  125. package/dist/esm/run/cookies/twitter.js.map +1 -1
  126. package/dist/esm/run/env.js +21 -15
  127. package/dist/esm/run/env.js.map +1 -1
  128. package/dist/esm/run/fetch-with-timeout.js +4 -4
  129. package/dist/esm/run/fetch-with-timeout.js.map +1 -1
  130. package/dist/esm/run/finish-line.js +78 -71
  131. package/dist/esm/run/finish-line.js.map +1 -1
  132. package/dist/esm/run/flows/asset/extract.js +70 -0
  133. package/dist/esm/run/flows/asset/extract.js.map +1 -0
  134. package/dist/esm/run/flows/asset/input.js +202 -37
  135. package/dist/esm/run/flows/asset/input.js.map +1 -1
  136. package/dist/esm/run/flows/asset/media-policy.js +3 -0
  137. package/dist/esm/run/flows/asset/media-policy.js.map +1 -0
  138. package/dist/esm/run/flows/asset/media.js +233 -0
  139. package/dist/esm/run/flows/asset/media.js.map +1 -0
  140. package/dist/esm/run/flows/asset/output.js +98 -0
  141. package/dist/esm/run/flows/asset/output.js.map +1 -0
  142. package/dist/esm/run/flows/asset/preprocess.js +79 -44
  143. package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
  144. package/dist/esm/run/flows/asset/summary.js +306 -89
  145. package/dist/esm/run/flows/asset/summary.js.map +1 -1
  146. package/dist/esm/run/flows/url/extract.js +31 -31
  147. package/dist/esm/run/flows/url/extract.js.map +1 -1
  148. package/dist/esm/run/flows/url/flow.js +388 -82
  149. package/dist/esm/run/flows/url/flow.js.map +1 -1
  150. package/dist/esm/run/flows/url/markdown.js +61 -56
  151. package/dist/esm/run/flows/url/markdown.js.map +1 -1
  152. package/dist/esm/run/flows/url/slides-output.js +487 -0
  153. package/dist/esm/run/flows/url/slides-output.js.map +1 -0
  154. package/dist/esm/run/flows/url/slides-text.js +628 -0
  155. package/dist/esm/run/flows/url/slides-text.js.map +1 -0
  156. package/dist/esm/run/flows/url/summary.js +493 -152
  157. package/dist/esm/run/flows/url/summary.js.map +1 -1
  158. package/dist/esm/run/format.js +10 -10
  159. package/dist/esm/run/format.js.map +1 -1
  160. package/dist/esm/run/help.js +179 -84
  161. package/dist/esm/run/help.js.map +1 -1
  162. package/dist/esm/run/logging.js +20 -12
  163. package/dist/esm/run/logging.js.map +1 -1
  164. package/dist/esm/run/markdown.js +12 -12
  165. package/dist/esm/run/markdown.js.map +1 -1
  166. package/dist/esm/run/media-cache-state.js +33 -0
  167. package/dist/esm/run/media-cache-state.js.map +1 -0
  168. package/dist/esm/run/model-attempts.js.map +1 -1
  169. package/dist/esm/run/openrouter.js +11 -11
  170. package/dist/esm/run/openrouter.js.map +1 -1
  171. package/dist/esm/run/progress.js +19 -1
  172. package/dist/esm/run/progress.js.map +1 -1
  173. package/dist/esm/run/run-config.js +16 -16
  174. package/dist/esm/run/run-config.js.map +1 -1
  175. package/dist/esm/run/run-context.js +2 -2
  176. package/dist/esm/run/run-context.js.map +1 -1
  177. package/dist/esm/run/run-env.js +55 -54
  178. package/dist/esm/run/run-env.js.map +1 -1
  179. package/dist/esm/run/run-input.js +3 -3
  180. package/dist/esm/run/run-input.js.map +1 -1
  181. package/dist/esm/run/run-metrics.js +16 -16
  182. package/dist/esm/run/run-metrics.js.map +1 -1
  183. package/dist/esm/run/run-models.js +28 -23
  184. package/dist/esm/run/run-models.js.map +1 -1
  185. package/dist/esm/run/run-output.js +3 -3
  186. package/dist/esm/run/run-output.js.map +1 -1
  187. package/dist/esm/run/run-settings.js +108 -21
  188. package/dist/esm/run/run-settings.js.map +1 -1
  189. package/dist/esm/run/run-stream.js +4 -4
  190. package/dist/esm/run/run-stream.js.map +1 -1
  191. package/dist/esm/run/runner.js +327 -100
  192. package/dist/esm/run/runner.js.map +1 -1
  193. package/dist/esm/run/slides-cli.js +226 -0
  194. package/dist/esm/run/slides-cli.js.map +1 -0
  195. package/dist/esm/run/slides-render.js +163 -0
  196. package/dist/esm/run/slides-render.js.map +1 -0
  197. package/dist/esm/run/stdin-temp-file.js +77 -0
  198. package/dist/esm/run/stdin-temp-file.js.map +1 -0
  199. package/dist/esm/run/stream-output.js +17 -10
  200. package/dist/esm/run/stream-output.js.map +1 -1
  201. package/dist/esm/run/streaming.js +16 -16
  202. package/dist/esm/run/streaming.js.map +1 -1
  203. package/dist/esm/run/summary-engine.js +89 -57
  204. package/dist/esm/run/summary-engine.js.map +1 -1
  205. package/dist/esm/run/summary-llm.js +3 -3
  206. package/dist/esm/run/summary-llm.js.map +1 -1
  207. package/dist/esm/run/terminal.js +4 -4
  208. package/dist/esm/run/terminal.js.map +1 -1
  209. package/dist/esm/run/tips.js +2 -2
  210. package/dist/esm/run/tips.js.map +1 -1
  211. package/dist/esm/run/transcriber-cli.js +148 -0
  212. package/dist/esm/run/transcriber-cli.js.map +1 -0
  213. package/dist/esm/run.js +1 -1
  214. package/dist/esm/run.js.map +1 -1
  215. package/dist/esm/shared/contracts.js +1 -1
  216. package/dist/esm/shared/contracts.js.map +1 -1
  217. package/dist/esm/shared/sse-events.js +16 -12
  218. package/dist/esm/shared/sse-events.js.map +1 -1
  219. package/dist/esm/shared/streaming-merge.js +3 -3
  220. package/dist/esm/shared/streaming-merge.js.map +1 -1
  221. package/dist/esm/slides/extract.js +1951 -0
  222. package/dist/esm/slides/extract.js.map +1 -0
  223. package/dist/esm/slides/index.js +4 -0
  224. package/dist/esm/slides/index.js.map +1 -0
  225. package/dist/esm/slides/settings.js +73 -0
  226. package/dist/esm/slides/settings.js.map +1 -0
  227. package/dist/esm/slides/store.js +111 -0
  228. package/dist/esm/slides/store.js.map +1 -0
  229. package/dist/esm/slides/types.js +2 -0
  230. package/dist/esm/slides/types.js.map +1 -0
  231. package/dist/esm/tty/format.js +13 -13
  232. package/dist/esm/tty/format.js.map +1 -1
  233. package/dist/esm/tty/osc-progress.js +22 -2
  234. package/dist/esm/tty/osc-progress.js.map +1 -1
  235. package/dist/esm/tty/progress/fetch-html.js +20 -16
  236. package/dist/esm/tty/progress/fetch-html.js.map +1 -1
  237. package/dist/esm/tty/progress/transcript.js +127 -68
  238. package/dist/esm/tty/progress/transcript.js.map +1 -1
  239. package/dist/esm/tty/spinner.js +21 -10
  240. package/dist/esm/tty/spinner.js.map +1 -1
  241. package/dist/esm/tty/theme.js +189 -0
  242. package/dist/esm/tty/theme.js.map +1 -0
  243. package/dist/esm/tty/website-progress.js +38 -34
  244. package/dist/esm/tty/website-progress.js.map +1 -1
  245. package/dist/esm/version.js +29 -29
  246. package/dist/esm/version.js.map +1 -1
  247. package/dist/types/cache.d.ts +19 -7
  248. package/dist/types/config.d.ts +71 -6
  249. package/dist/types/content/asset.d.ts +8 -6
  250. package/dist/types/content/index.d.ts +1 -1
  251. package/dist/types/costs.d.ts +3 -3
  252. package/dist/types/daemon/agent.d.ts +25 -0
  253. package/dist/types/daemon/auto-mode.d.ts +3 -3
  254. package/dist/types/daemon/chat.d.ts +10 -18
  255. package/dist/types/daemon/cli-entrypoint.d.ts +2 -0
  256. package/dist/types/daemon/config.d.ts +2 -2
  257. package/dist/types/daemon/env-merge.d.ts +1 -1
  258. package/dist/types/daemon/env-snapshot.d.ts +1 -1
  259. package/dist/types/daemon/flow-context.d.ts +24 -4
  260. package/dist/types/daemon/launchd.d.ts +12 -0
  261. package/dist/types/daemon/models.d.ts +6 -2
  262. package/dist/types/daemon/process-registry.d.ts +73 -0
  263. package/dist/types/daemon/schtasks.d.ts +4 -0
  264. package/dist/types/daemon/server.d.ts +2 -2
  265. package/dist/types/daemon/summarize-progress.d.ts +1 -1
  266. package/dist/types/daemon/summarize.d.ts +38 -7
  267. package/dist/types/daemon/systemd.d.ts +4 -0
  268. package/dist/types/firecrawl.d.ts +1 -1
  269. package/dist/types/flags.d.ts +12 -11
  270. package/dist/types/index.d.ts +4 -4
  271. package/dist/types/language.d.ts +1 -1
  272. package/dist/types/llm/attachments.d.ts +1 -1
  273. package/dist/types/llm/cli.d.ts +3 -3
  274. package/dist/types/llm/generate-text.d.ts +7 -7
  275. package/dist/types/llm/html-to-markdown.d.ts +3 -3
  276. package/dist/types/llm/model-id.d.ts +1 -1
  277. package/dist/types/llm/prompt.d.ts +2 -2
  278. package/dist/types/llm/providers/anthropic.d.ts +3 -3
  279. package/dist/types/llm/providers/google.d.ts +3 -3
  280. package/dist/types/llm/providers/models.d.ts +2 -2
  281. package/dist/types/llm/providers/openai.d.ts +4 -4
  282. package/dist/types/llm/providers/shared.d.ts +2 -2
  283. package/dist/types/llm/transcript-to-markdown.d.ts +4 -2
  284. package/dist/types/llm/usage.d.ts +1 -1
  285. package/dist/types/logging/daemon.d.ts +4 -4
  286. package/dist/types/markitdown.d.ts +1 -1
  287. package/dist/types/media-cache.d.ts +22 -0
  288. package/dist/types/model-auto.d.ts +14 -4
  289. package/dist/types/model-spec.d.ts +10 -10
  290. package/dist/types/pricing/litellm.d.ts +1 -1
  291. package/dist/types/processes.d.ts +1 -0
  292. package/dist/types/prompts/index.d.ts +1 -1
  293. package/dist/types/run/attachments.d.ts +7 -7
  294. package/dist/types/run/bird.d.ts +7 -0
  295. package/dist/types/run/cache-state.d.ts +2 -2
  296. package/dist/types/run/cli-fallback-state.d.ts +6 -0
  297. package/dist/types/run/constants.d.ts +1 -1
  298. package/dist/types/run/cookies/twitter.d.ts +1 -1
  299. package/dist/types/run/env.d.ts +1 -1
  300. package/dist/types/run/finish-line.d.ts +7 -6
  301. package/dist/types/run/flows/asset/extract.d.ts +18 -0
  302. package/dist/types/run/flows/asset/input.d.ts +19 -3
  303. package/dist/types/run/flows/asset/media-policy.d.ts +2 -0
  304. package/dist/types/run/flows/asset/media.d.ts +21 -0
  305. package/dist/types/run/flows/asset/output.d.ts +42 -0
  306. package/dist/types/run/flows/asset/preprocess.d.ts +23 -17
  307. package/dist/types/run/flows/asset/summary.d.ts +24 -16
  308. package/dist/types/run/flows/url/extract.d.ts +3 -2
  309. package/dist/types/run/flows/url/flow.d.ts +1 -1
  310. package/dist/types/run/flows/url/markdown.d.ts +6 -6
  311. package/dist/types/run/flows/url/slides-output.d.ts +66 -0
  312. package/dist/types/run/flows/url/slides-text.d.ts +87 -0
  313. package/dist/types/run/flows/url/summary.d.ts +18 -10
  314. package/dist/types/run/flows/url/types.d.ts +52 -21
  315. package/dist/types/run/format.d.ts +3 -3
  316. package/dist/types/run/help.d.ts +4 -1
  317. package/dist/types/run/logging.d.ts +3 -2
  318. package/dist/types/run/media-cache-state.d.ts +7 -0
  319. package/dist/types/run/model-attempts.d.ts +1 -1
  320. package/dist/types/run/progress.d.ts +2 -1
  321. package/dist/types/run/run-config.d.ts +4 -4
  322. package/dist/types/run/run-context.d.ts +3 -1
  323. package/dist/types/run/run-env.d.ts +3 -1
  324. package/dist/types/run/run-input.d.ts +2 -2
  325. package/dist/types/run/run-metrics.d.ts +3 -3
  326. package/dist/types/run/run-models.d.ts +3 -2
  327. package/dist/types/run/run-output.d.ts +1 -1
  328. package/dist/types/run/run-settings.d.ts +20 -5
  329. package/dist/types/run/run-stream.d.ts +2 -2
  330. package/dist/types/run/runner.d.ts +3 -2
  331. package/dist/types/run/slides-cli.d.ts +9 -0
  332. package/dist/types/run/slides-render.d.ts +30 -0
  333. package/dist/types/run/stdin-temp-file.d.ts +9 -0
  334. package/dist/types/run/stream-output.d.ts +3 -2
  335. package/dist/types/run/streaming.d.ts +4 -4
  336. package/dist/types/run/summary-engine.d.ts +22 -12
  337. package/dist/types/run/summary-llm.d.ts +5 -5
  338. package/dist/types/run/transcriber-cli.d.ts +8 -0
  339. package/dist/types/run/types.d.ts +4 -4
  340. package/dist/types/run.d.ts +1 -1
  341. package/dist/types/shared/contracts.d.ts +2 -2
  342. package/dist/types/shared/sse-events.d.ts +26 -6
  343. package/dist/types/slides/extract.d.ts +43 -0
  344. package/dist/types/slides/index.d.ts +5 -0
  345. package/dist/types/slides/settings.d.ts +20 -0
  346. package/dist/types/slides/store.d.ts +15 -0
  347. package/dist/types/slides/types.d.ts +40 -0
  348. package/dist/types/tty/osc-progress.d.ts +5 -5
  349. package/dist/types/tty/progress/fetch-html.d.ts +5 -3
  350. package/dist/types/tty/progress/transcript.d.ts +5 -3
  351. package/dist/types/tty/spinner.d.ts +3 -1
  352. package/dist/types/tty/theme.d.ts +44 -0
  353. package/dist/types/tty/website-progress.d.ts +5 -3
  354. package/dist/types/version.d.ts +1 -1
  355. package/docs/README.md +1 -1
  356. package/docs/_config.yml +26 -0
  357. package/docs/_layouts/default.html +60 -0
  358. package/docs/agent.md +367 -0
  359. package/docs/assets/site.css +748 -0
  360. package/docs/assets/site.js +72 -0
  361. package/docs/assets/summarize-cli.png +0 -0
  362. package/docs/assets/summarize-extension.png +0 -0
  363. package/docs/assets/youtube-slides.png +0 -0
  364. package/docs/cache.md +29 -3
  365. package/docs/chrome-extension.md +72 -16
  366. package/docs/cli.md +59 -13
  367. package/docs/config.md +109 -12
  368. package/docs/extract-only.md +10 -0
  369. package/docs/index.html +224 -0
  370. package/docs/index.md +25 -0
  371. package/docs/llm.md +18 -5
  372. package/docs/manual-tests.md +2 -0
  373. package/docs/media.md +6 -2
  374. package/docs/model-auto.md +3 -2
  375. package/docs/nvidia-onnx-transcription.md +55 -0
  376. package/docs/openai.md +1 -1
  377. package/docs/releasing.md +3 -0
  378. package/docs/site/404.html +4 -1
  379. package/docs/site/assets/site.css +399 -228
  380. package/docs/site/assets/site.js +46 -46
  381. package/docs/site/assets/summarize-cli.png +0 -0
  382. package/docs/site/assets/summarize-extension.png +0 -0
  383. package/docs/site/docs/chrome-extension.html +101 -0
  384. package/docs/site/docs/config.html +30 -8
  385. package/docs/site/docs/extract-only.html +17 -4
  386. package/docs/site/docs/firecrawl.html +13 -3
  387. package/docs/site/docs/index.html +40 -6
  388. package/docs/site/docs/llm.html +20 -5
  389. package/docs/site/docs/openai.html +19 -5
  390. package/docs/site/docs/website.html +30 -9
  391. package/docs/site/docs/youtube.html +13 -3
  392. package/docs/site/index.html +168 -85
  393. package/docs/slides.md +82 -0
  394. package/docs/smoketest.md +29 -20
  395. package/docs/timestamps.md +124 -0
  396. package/docs/website.md +13 -0
  397. package/docs/youtube.md +20 -0
  398. package/package.json +57 -48
@@ -1,40 +1,81 @@
1
- import { randomUUID } from 'node:crypto';
2
- import http from 'node:http';
3
- import { Writable } from 'node:stream';
4
- import { loadSummarizeConfig } from '../config.js';
5
- import { createDaemonLogger } from '../logging/daemon.js';
6
- import { refreshFree } from '../refresh-free.js';
7
- import { createCacheStateFromConfig, refreshCacheStoreIfMissing } from '../run/cache-state.js';
8
- import { formatModelLabelForDisplay } from '../run/finish-line.js';
9
- import { resolveRunOverrides } from '../run/run-settings.js';
10
- import { encodeSseEvent } from '../shared/sse-events.js';
11
- import { resolvePackageVersion } from '../version.js';
12
- import { resolveAutoDaemonMode } from './auto-mode.js';
13
- import { streamChatResponse } from './chat.js';
14
- import { DAEMON_HOST, DAEMON_PORT_DEFAULT } from './constants.js';
15
- import { buildModelPickerOptions } from './models.js';
16
- import { extractContentForUrl, streamSummaryForUrl, streamSummaryForVisiblePage, } from './summarize.js';
1
+ import { randomUUID } from "node:crypto";
2
+ import { createReadStream, promises as fs } from "node:fs";
3
+ import http from "node:http";
4
+ import path from "node:path";
5
+ import { Writable } from "node:stream";
6
+ import { loadSummarizeConfig } from "../config.js";
7
+ import { createDaemonLogger } from "../logging/daemon.js";
8
+ import { runWithProcessContext, setProcessObserver } from "../processes.js";
9
+ import { refreshFree } from "../refresh-free.js";
10
+ import { createCacheStateFromConfig, refreshCacheStoreIfMissing } from "../run/cache-state.js";
11
+ import { resolveExecutableInPath } from "../run/env.js";
12
+ import { formatModelLabelForDisplay } from "../run/finish-line.js";
13
+ 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
+ import { resolvePackageVersion } from "../version.js";
18
+ import { completeAgentResponse, streamAgentResponse } from "./agent.js";
19
+ import { resolveAutoDaemonMode } from "./auto-mode.js";
20
+ import { DAEMON_HOST, DAEMON_PORT_DEFAULT } from "./constants.js";
21
+ 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";
17
25
  function json(res, status, payload, headers) {
18
26
  const body = `${JSON.stringify(payload)}\n`;
19
27
  res.writeHead(status, {
20
- 'content-type': 'application/json; charset=utf-8',
21
- 'content-length': Buffer.byteLength(body).toString(),
28
+ "content-type": "application/json; charset=utf-8",
29
+ "content-length": Buffer.byteLength(body).toString(),
22
30
  ...headers,
23
31
  });
24
32
  res.end(body);
25
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
+ }
26
67
  function text(res, status, body, headers) {
27
- const out = body.endsWith('\n') ? body : `${body}\n`;
68
+ const out = body.endsWith("\n") ? body : `${body}\n`;
28
69
  res.writeHead(status, {
29
- 'content-type': 'text/plain; charset=utf-8',
30
- 'content-length': Buffer.byteLength(out).toString(),
70
+ "content-type": "text/plain; charset=utf-8",
71
+ "content-length": Buffer.byteLength(out).toString(),
31
72
  ...headers,
32
73
  });
33
74
  res.end(out);
34
75
  }
35
76
  function resolveOriginHeader(req) {
36
77
  const origin = req.headers.origin;
37
- if (typeof origin !== 'string')
78
+ if (typeof origin !== "string")
38
79
  return null;
39
80
  if (!origin.trim())
40
81
  return null;
@@ -44,20 +85,20 @@ function corsHeaders(origin) {
44
85
  if (!origin)
45
86
  return {};
46
87
  return {
47
- 'access-control-allow-origin': origin,
48
- 'access-control-allow-credentials': 'true',
49
- 'access-control-allow-headers': 'authorization, content-type',
50
- 'access-control-allow-methods': 'GET,POST,OPTIONS',
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",
51
92
  // Chrome Private Network Access (PNA): allow requests to localhost from secure contexts.
52
93
  // Without this, extensions often fail with a generic "Failed to fetch".
53
- 'access-control-allow-private-network': 'true',
54
- 'access-control-max-age': '600',
55
- vary: 'Origin',
94
+ "access-control-allow-private-network": "true",
95
+ "access-control-max-age": "600",
96
+ vary: "Origin",
56
97
  };
57
98
  }
58
99
  function readBearerToken(req) {
59
100
  const header = req.headers.authorization;
60
- if (typeof header !== 'string')
101
+ if (typeof header !== "string")
61
102
  return null;
62
103
  const m = header.match(/^Bearer\s+(.+)\s*$/i);
63
104
  return m?.[1]?.trim() || null;
@@ -72,28 +113,40 @@ async function readJsonBody(req, maxBytes) {
72
113
  throw new Error(`Body too large (>${maxBytes} bytes)`);
73
114
  chunks.push(buf);
74
115
  }
75
- const text = Buffer.concat(chunks).toString('utf8');
116
+ const text = Buffer.concat(chunks).toString("utf8");
76
117
  return JSON.parse(text);
77
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
+ }
78
131
  function parseDiagnostics(raw) {
79
- if (!raw || typeof raw !== 'object') {
132
+ if (!raw || typeof raw !== "object") {
80
133
  return { includeContent: false };
81
134
  }
82
135
  const obj = raw;
83
136
  return { includeContent: Boolean(obj.includeContent) };
84
137
  }
85
138
  function createLineWriter(onLine) {
86
- let buffer = '';
139
+ let buffer = "";
87
140
  return new Writable({
88
141
  write(chunk, _encoding, callback) {
89
142
  buffer += chunk.toString();
90
- let index = buffer.indexOf('\n');
143
+ let index = buffer.indexOf("\n");
91
144
  while (index >= 0) {
92
145
  const line = buffer.slice(0, index).trimEnd();
93
146
  buffer = buffer.slice(index + 1);
94
147
  if (line.trim().length > 0)
95
148
  onLine(line);
96
- index = buffer.indexOf('\n');
149
+ index = buffer.indexOf("\n");
97
150
  }
98
151
  callback();
99
152
  },
@@ -101,7 +154,7 @@ function createLineWriter(onLine) {
101
154
  const line = buffer.trim();
102
155
  if (line)
103
156
  onLine(line);
104
- buffer = '';
157
+ buffer = "";
105
158
  callback();
106
159
  },
107
160
  });
@@ -114,11 +167,21 @@ function createSession() {
114
167
  bufferBytes: 0,
115
168
  done: false,
116
169
  clients: new Set(),
170
+ slidesBuffer: [],
171
+ slidesBufferBytes: 0,
172
+ slidesClients: new Set(),
173
+ slidesDone: false,
174
+ slidesRequested: false,
175
+ slidesLastStatus: null,
117
176
  lastMeta: { model: null, modelLabel: null, inputSummary: null, summaryFromCache: null },
177
+ slides: null,
118
178
  };
119
179
  }
120
180
  const MAX_SESSION_BUFFER_EVENTS = 2000;
121
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;
122
185
  function pushToSession(session, evt, onSessionEvent) {
123
186
  const encoded = encodeSseEvent(evt);
124
187
  for (const res of session.clients) {
@@ -135,10 +198,33 @@ function pushToSession(session, evt, onSessionEvent) {
135
198
  break;
136
199
  session.bufferBytes -= removed.bytes;
137
200
  }
138
- if (evt.event === 'done' || evt.event === 'error') {
201
+ if (evt.event === "done" || evt.event === "error") {
139
202
  session.done = true;
140
203
  }
141
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
+ }
142
228
  function emitMeta(session, patch, onSessionEvent) {
143
229
  const next = { ...session.lastMeta, ...patch };
144
230
  if (next.model === session.lastMeta.model &&
@@ -148,13 +234,92 @@ function emitMeta(session, patch, onSessionEvent) {
148
234
  return;
149
235
  }
150
236
  session.lastMeta = next;
151
- pushToSession(session, { event: 'meta', data: next }, onSessionEvent);
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
+ function resolveToolPath(binary, env, explicitEnvKey) {
297
+ const explicit = explicitEnvKey && typeof env[explicitEnvKey] === "string" ? env[explicitEnvKey]?.trim() : "";
298
+ if (explicit)
299
+ return resolveExecutableInPath(explicit, env);
300
+ return resolveExecutableInPath(binary, env);
152
301
  }
153
302
  function endSession(session) {
154
303
  for (const res of session.clients) {
155
304
  res.end();
156
305
  }
157
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();
158
323
  }
159
324
  export function buildHealthPayload(importMetaUrl) {
160
325
  return { ok: true, pid: process.pid, version: resolvePackageVersion(importMetaUrl) };
@@ -162,12 +327,21 @@ export function buildHealthPayload(importMetaUrl) {
162
327
  export async function runDaemonServer({ env, fetchImpl, config, port = config.port ?? DAEMON_PORT_DEFAULT, signal, onListening, onSessionEvent, }) {
163
328
  const { config: summarizeConfig } = loadSummarizeConfig({ env });
164
329
  const daemonLogger = createDaemonLogger({ env, config: summarizeConfig });
330
+ const daemonLogPaths = resolveDaemonLogPaths(env);
331
+ const daemonLogFile = daemonLogger.config?.file ?? path.join(daemonLogPaths.logDir, "daemon.jsonl");
165
332
  const cacheState = await createCacheStateFromConfig({
166
333
  envForRun: env,
167
334
  config: summarizeConfig,
168
335
  noCacheFlag: false,
169
- transcriptNamespace: 'yt:auto',
336
+ transcriptNamespace: "yt:auto",
170
337
  });
338
+ const mediaCache = await createMediaCacheFromConfig({
339
+ envForRun: env,
340
+ config: summarizeConfig,
341
+ noMediaCacheFlag: false,
342
+ });
343
+ const processRegistry = new ProcessRegistry();
344
+ setProcessObserver(processRegistry.createObserver());
171
345
  const sessions = new Map();
172
346
  const refreshSessions = new Map();
173
347
  let activeRefreshSessionId = null;
@@ -175,28 +349,98 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
175
349
  void (async () => {
176
350
  const origin = resolveOriginHeader(req);
177
351
  const cors = corsHeaders(origin);
178
- if (req.method === 'OPTIONS') {
352
+ if (req.method === "OPTIONS") {
179
353
  res.writeHead(204, cors);
180
354
  res.end();
181
355
  return;
182
356
  }
183
- const url = new URL(req.url ?? '/', `http://${DAEMON_HOST}:${port}`);
357
+ const url = new URL(req.url ?? "/", `http://${DAEMON_HOST}:${port}`);
184
358
  const pathname = url.pathname;
185
- if (req.method === 'GET' && pathname === '/health') {
359
+ if (req.method === "GET" && pathname === "/health") {
186
360
  json(res, 200, buildHealthPayload(import.meta.url), cors);
187
361
  return;
188
362
  }
189
363
  const token = readBearerToken(req);
190
364
  const authed = token && token === config.token;
191
- if (pathname.startsWith('/v1/') && !authed) {
192
- json(res, 401, { ok: false, error: 'unauthorized' }, cors);
365
+ if (pathname.startsWith("/v1/") && !authed) {
366
+ json(res, 401, { ok: false, error: "unauthorized" }, cors);
193
367
  return;
194
368
  }
195
- if (req.method === 'GET' && pathname === '/v1/ping') {
369
+ if (req.method === "GET" && pathname === "/v1/ping") {
196
370
  json(res, 200, { ok: true }, cors);
197
371
  return;
198
372
  }
199
- if (req.method === 'GET' && pathname === '/v1/models') {
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") {
200
444
  const result = await buildModelPickerOptions({
201
445
  env,
202
446
  envForRun: env,
@@ -206,7 +450,21 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
206
450
  json(res, 200, result, cors);
207
451
  return;
208
452
  }
209
- if (req.method === 'POST' && pathname === '/v1/refresh-free') {
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);
465
+ return;
466
+ }
467
+ if (req.method === "POST" && pathname === "/v1/refresh-free") {
210
468
  if (activeRefreshSessionId) {
211
469
  json(res, 200, { ok: true, id: activeRefreshSessionId, running: true }, cors);
212
470
  return;
@@ -217,19 +475,19 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
217
475
  json(res, 200, { ok: true, id: session.id }, cors);
218
476
  void (async () => {
219
477
  const pushStatus = (text) => {
220
- pushToSession(session, { event: 'status', data: { text } }, onSessionEvent);
478
+ pushToSession(session, { event: "status", data: { text } }, onSessionEvent);
221
479
  };
222
480
  try {
223
- pushStatus('Refresh free: starting…');
481
+ pushStatus("Refresh free: starting…");
224
482
  const stdout = createLineWriter(pushStatus);
225
483
  const stderr = createLineWriter(pushStatus);
226
484
  await refreshFree({ env, fetchImpl, stdout, stderr });
227
- pushToSession(session, { event: 'done', data: {} }, onSessionEvent);
485
+ pushToSession(session, { event: "done", data: {} }, onSessionEvent);
228
486
  }
229
487
  catch (error) {
230
488
  const message = error instanceof Error ? error.message : String(error);
231
- pushToSession(session, { event: 'error', data: { message } }, onSessionEvent);
232
- console.error('[summarize-daemon] refresh-free failed', error);
489
+ pushToSession(session, { event: "error", data: { message } }, onSessionEvent);
490
+ console.error("[summarize-daemon] refresh-free failed", error);
233
491
  }
234
492
  finally {
235
493
  if (activeRefreshSessionId === session.id) {
@@ -243,8 +501,8 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
243
501
  })();
244
502
  return;
245
503
  }
246
- if (req.method === 'POST' && pathname === '/v1/summarize') {
247
- await refreshCacheStoreIfMissing({ cacheState, transcriptNamespace: 'yt:auto' });
504
+ if (req.method === "POST" && pathname === "/v1/summarize") {
505
+ await refreshCacheStoreIfMissing({ cacheState, transcriptNamespace: "yt:auto" });
248
506
  let body;
249
507
  try {
250
508
  body = await readJsonBody(req, 2_000_000);
@@ -254,60 +512,90 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
254
512
  json(res, 400, { ok: false, error: message }, cors);
255
513
  return;
256
514
  }
257
- if (!body || typeof body !== 'object') {
258
- json(res, 400, { ok: false, error: 'invalid json' }, cors);
515
+ if (!body || typeof body !== "object") {
516
+ json(res, 400, { ok: false, error: "invalid json" }, cors);
259
517
  return;
260
518
  }
261
519
  const obj = body;
262
- const pageUrl = typeof obj.url === 'string' ? obj.url.trim() : '';
263
- const title = typeof obj.title === 'string' ? obj.title.trim() : null;
264
- const textContent = typeof obj.text === 'string' ? obj.text : '';
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 : "";
265
523
  const truncated = Boolean(obj.truncated);
266
- const modelOverride = typeof obj.model === 'string' ? obj.model.trim() : null;
267
- const lengthRaw = typeof obj.length === 'string' ? obj.length.trim() : '';
268
- const languageRaw = typeof obj.language === 'string' ? obj.language.trim() : '';
269
- const promptRaw = typeof obj.prompt === 'string' ? obj.prompt : '';
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 : "";
270
528
  const promptOverride = promptRaw.trim() || null;
271
529
  const noCache = Boolean(obj.noCache);
272
530
  const extractOnly = Boolean(obj.extractOnly);
273
- const modeRaw = typeof obj.mode === 'string' ? obj.mode.trim().toLowerCase() : '';
274
- const mode = modeRaw === 'url' ? 'url' : modeRaw === 'page' ? 'page' : 'auto';
275
- const maxCharacters = typeof obj.maxCharacters === 'number' && Number.isFinite(obj.maxCharacters)
276
- ? obj.maxCharacters
277
- : null;
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";
278
541
  const overrides = resolveRunOverrides({
279
542
  firecrawl: obj.firecrawl,
280
543
  markdownMode: obj.markdownMode,
281
544
  preprocess: obj.preprocess,
282
545
  youtube: obj.youtube,
283
546
  videoMode: obj.videoMode,
547
+ timestamps: obj.timestamps,
548
+ forceSummary: obj.forceSummary,
284
549
  timeout: obj.timeout,
285
550
  retries: obj.retries,
286
551
  maxOutputTokens: obj.maxOutputTokens,
552
+ autoCliFallback: obj.autoCliFallback,
553
+ autoCliOrder: obj.autoCliOrder,
554
+ magicCliAuto: obj.magicCliAuto,
555
+ magicCliOrder: obj.magicCliOrder,
287
556
  });
557
+ const slidesSettings = resolveSlidesSettings({ env, request: obj });
288
558
  const diagnostics = parseDiagnostics(obj.diagnostics);
289
559
  const includeContentLog = daemonLogger.enabled && diagnostics.includeContent;
290
560
  const hasText = Boolean(textContent.trim());
291
561
  if (!pageUrl || !/^https?:\/\//i.test(pageUrl)) {
292
- json(res, 400, { ok: false, error: 'missing url' }, cors);
562
+ json(res, 400, { ok: false, error: "missing url" }, cors);
293
563
  return;
294
564
  }
295
565
  if (extractOnly) {
296
- if (mode === 'page') {
297
- json(res, 400, { ok: false, error: 'extractOnly requires mode=url' }, cors);
566
+ if (mode === "page") {
567
+ json(res, 400, { ok: false, error: "extractOnly requires mode=url" }, cors);
298
568
  return;
299
569
  }
300
570
  try {
301
571
  const requestCache = noCache
302
- ? { ...cacheState, mode: 'bypass', store: null }
572
+ ? { ...cacheState, mode: "bypass", store: null }
303
573
  : cacheState;
304
- const extracted = await extractContentForUrl({
574
+ const runId = randomUUID();
575
+ const { extracted, slides } = await runWithProcessContext({ runId, source: "extract" }, async () => extractContentForUrl({
305
576
  env,
306
577
  fetchImpl,
307
578
  input: { url: pageUrl, title, maxCharacters },
308
579
  cache: requestCache,
580
+ mediaCache,
309
581
  overrides,
310
- });
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;
311
599
  json(res, 200, {
312
600
  ok: true,
313
601
  extracted: {
@@ -321,10 +609,13 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
321
609
  transcriptCharacters: extracted.transcriptCharacters ?? null,
322
610
  transcriptWordCount: extracted.transcriptWordCount ?? null,
323
611
  transcriptLines: extracted.transcriptLines ?? null,
612
+ transcriptSegments: extracted.transcriptSegments ?? null,
613
+ transcriptTimedText: extracted.transcriptTimedText ?? null,
324
614
  transcriptionProvider: extracted.transcriptionProvider ?? null,
325
615
  mediaDurationSeconds: extracted.mediaDurationSeconds ?? null,
326
616
  diagnostics: extracted.diagnostics,
327
617
  },
618
+ ...(slidesPayload ? { slides: slidesPayload } : {}),
328
619
  }, cors);
329
620
  }
330
621
  catch (error) {
@@ -333,19 +624,20 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
333
624
  }
334
625
  return;
335
626
  }
336
- if (mode === 'page' && !hasText) {
337
- json(res, 400, { ok: false, error: 'missing text' }, cors);
627
+ if (mode === "page" && !hasText) {
628
+ json(res, 400, { ok: false, error: "missing text" }, cors);
338
629
  return;
339
630
  }
340
631
  const session = createSession();
632
+ session.slidesRequested = Boolean(slidesSettings);
341
633
  sessions.set(session.id, session);
342
- const requestLogger = daemonLogger.getSubLogger('daemon.summarize', {
634
+ const requestLogger = daemonLogger.getSubLogger("daemon.summarize", {
343
635
  requestId: session.id,
344
636
  });
345
637
  const logStartedAt = Date.now();
346
638
  let logSummaryFromCache = false;
347
639
  let logInputSummary = null;
348
- let logSummaryText = '';
640
+ let logSummaryText = "";
349
641
  let logExtracted = null;
350
642
  const logInput = includeContentLog
351
643
  ? {
@@ -355,8 +647,19 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
355
647
  truncated: hasText ? truncated : null,
356
648
  }
357
649
  : null;
650
+ const logSlidesSettings = includeContentLog && slidesSettings
651
+ ? {
652
+ enabled: slidesSettings.enabled,
653
+ ocr: slidesSettings.ocr,
654
+ outputDir: slidesSettings.outputDir,
655
+ sceneThreshold: slidesSettings.sceneThreshold,
656
+ autoTuneThreshold: slidesSettings.autoTuneThreshold,
657
+ maxSlides: slidesSettings.maxSlides,
658
+ minDurationSeconds: slidesSettings.minDurationSeconds,
659
+ }
660
+ : null;
358
661
  requestLogger?.info({
359
- event: 'summarize.request',
662
+ event: "summarize.request",
360
663
  url: pageUrl,
361
664
  mode,
362
665
  hasText,
@@ -365,9 +668,23 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
365
668
  language: languageRaw,
366
669
  model: modelOverride,
367
670
  includeContent: includeContentLog,
671
+ slides: Boolean(slidesSettings),
672
+ ...(logSlidesSettings ? { slidesSettings: logSlidesSettings } : {}),
673
+ ...(includeContentLog ? { diagnostics } : {}),
368
674
  });
369
675
  json(res, 200, { ok: true, id: session.id }, cors);
370
- void (async () => {
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
+ };
371
688
  try {
372
689
  let emittedOutput = false;
373
690
  const sink = {
@@ -376,7 +693,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
376
693
  if (includeContentLog) {
377
694
  logSummaryText += chunk;
378
695
  }
379
- pushToSession(session, { event: 'chunk', data: { text: chunk } }, onSessionEvent);
696
+ pushToSession(session, { event: "chunk", data: { text: chunk } }, onSessionEvent);
380
697
  },
381
698
  onModelChosen: (modelId) => {
382
699
  if (session.lastMeta.model === modelId)
@@ -391,27 +708,40 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
391
708
  const clean = text.trim();
392
709
  if (!clean)
393
710
  return;
394
- pushToSession(session, { event: 'status', data: { text: clean } }, onSessionEvent);
711
+ pushToSession(session, { event: "status", data: { text: clean } }, onSessionEvent);
395
712
  },
396
713
  writeMeta: (data) => {
397
- if (typeof data.inputSummary === 'string') {
714
+ if (typeof data.inputSummary === "string") {
398
715
  logInputSummary = data.inputSummary;
399
716
  }
400
- if (typeof data.summaryFromCache === 'boolean') {
717
+ if (typeof data.summaryFromCache === "boolean") {
401
718
  logSummaryFromCache = data.summaryFromCache;
402
719
  }
403
720
  emitMeta(session, {
404
- inputSummary: typeof data.inputSummary === 'string' ? data.inputSummary : null,
405
- summaryFromCache: typeof data.summaryFromCache === 'boolean' ? data.summaryFromCache : null,
721
+ inputSummary: typeof data.inputSummary === "string" ? data.inputSummary : null,
722
+ summaryFromCache: typeof data.summaryFromCache === "boolean" ? data.summaryFromCache : null,
406
723
  }, onSessionEvent);
407
724
  },
408
725
  };
409
- const normalizedModelOverride = modelOverride && modelOverride.toLowerCase() !== 'auto' ? modelOverride : null;
726
+ const normalizedModelOverride = modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null;
410
727
  const requestCache = noCache
411
- ? { ...cacheState, mode: 'bypass', store: null }
728
+ ? { ...cacheState, mode: "bypass", store: null }
412
729
  : cacheState;
730
+ let liveSlides = null;
413
731
  const runWithMode = async (resolved) => {
414
- return resolved === 'url'
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"
415
745
  ? await streamSummaryForUrl({
416
746
  env,
417
747
  fetchImpl,
@@ -419,17 +749,123 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
419
749
  promptOverride,
420
750
  lengthRaw,
421
751
  languageRaw,
752
+ format,
422
753
  input: { url: pageUrl, title, maxCharacters },
423
754
  sink,
424
755
  cache: requestCache,
756
+ mediaCache,
425
757
  overrides,
426
- hooks: includeContentLog
427
- ? {
428
- onExtracted: (content) => {
429
- logExtracted = content;
430
- },
431
- }
432
- : null,
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
+ },
433
869
  })
434
870
  : await streamSummaryForVisiblePage({
435
871
  env,
@@ -438,14 +874,16 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
438
874
  promptOverride,
439
875
  lengthRaw,
440
876
  languageRaw,
877
+ format,
441
878
  input: { url: pageUrl, title, text: textContent, truncated },
442
879
  sink,
443
880
  cache: requestCache,
881
+ mediaCache,
444
882
  overrides,
445
883
  });
446
884
  };
447
885
  const result = await (async () => {
448
- if (mode !== 'auto')
886
+ if (mode !== "auto")
449
887
  return runWithMode(mode);
450
888
  const { primary, fallback } = resolveAutoDaemonMode({ url: pageUrl, hasText });
451
889
  try {
@@ -454,7 +892,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
454
892
  catch (error) {
455
893
  if (!fallback || emittedOutput)
456
894
  throw error;
457
- sink.writeStatus?.('Primary failed. Trying fallback…');
895
+ sink.writeStatus?.("Primary failed. Trying fallback…");
458
896
  try {
459
897
  return await runWithMode(fallback);
460
898
  }
@@ -471,16 +909,30 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
471
909
  modelLabel: formatModelLabelForDisplay(result.usedModel),
472
910
  }, onSessionEvent);
473
911
  }
474
- pushToSession(session, { event: 'metrics', data: result.metrics }, onSessionEvent);
475
- pushToSession(session, { event: 'done', data: {} }, onSessionEvent);
912
+ pushToSession(session, { event: "metrics", data: result.metrics }, onSessionEvent);
913
+ pushToSession(session, { event: "done", data: {} }, onSessionEvent);
476
914
  requestLogger?.info({
477
- event: 'summarize.done',
915
+ event: "summarize.done",
478
916
  url: pageUrl,
479
917
  mode,
480
918
  model: result.usedModel,
481
919
  elapsedMs: Date.now() - logStartedAt,
482
920
  summaryFromCache: logSummaryFromCache,
483
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
+ : {}),
484
936
  ...(includeContentLog && !logSummaryFromCache
485
937
  ? {
486
938
  input: logInput,
@@ -492,16 +944,33 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
492
944
  }
493
945
  catch (error) {
494
946
  const message = error instanceof Error ? error.message : String(error);
495
- pushToSession(session, { event: 'error', data: { message } }, onSessionEvent);
947
+ pushToSession(session, { event: "error", data: { message } }, onSessionEvent);
948
+ if (session.slidesRequested && !session.slidesDone) {
949
+ emitSlidesDone(session, { ok: false, error: message }, onSessionEvent);
950
+ }
496
951
  // Preserve full stack trace in daemon logs for debugging.
497
- console.error('[summarize-daemon] summarize failed', error);
952
+ console.error("[summarize-daemon] summarize failed", error);
498
953
  requestLogger?.error({
499
- event: 'summarize.error',
954
+ event: "summarize.error",
500
955
  url: pageUrl,
501
956
  mode,
502
957
  elapsedMs: Date.now() - logStartedAt,
503
958
  summaryFromCache: logSummaryFromCache,
504
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
+ : {}),
505
974
  error: {
506
975
  message,
507
976
  stack: error instanceof Error ? error.stack : null,
@@ -516,73 +985,230 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
516
985
  });
517
986
  }
518
987
  finally {
519
- setTimeout(() => {
520
- sessions.delete(session.id);
521
- endSession(session);
522
- }, 60_000).unref();
988
+ scheduleSessionCleanup({ session, sessions });
523
989
  }
524
- })();
990
+ });
525
991
  return;
526
992
  }
527
- if (req.method === 'POST' && pathname === '/v1/chat') {
528
- await refreshCacheStoreIfMissing({ cacheState, transcriptNamespace: 'yt:auto' });
993
+ if (req.method === "POST" && pathname === "/v1/agent") {
529
994
  let body;
530
995
  try {
531
- body = await readJsonBody(req, 2_000_000);
996
+ body = await readJsonBody(req, 4_000_000);
532
997
  }
533
998
  catch (error) {
534
999
  const message = error instanceof Error ? error.message : String(error);
535
1000
  json(res, 400, { ok: false, error: message }, cors);
536
1001
  return;
537
1002
  }
538
- if (!body || typeof body !== 'object') {
539
- json(res, 400, { ok: false, error: 'invalid json' }, cors);
1003
+ if (!body || typeof body !== "object") {
1004
+ json(res, 400, { ok: false, error: "invalid json" }, cors);
540
1005
  return;
541
1006
  }
542
1007
  const obj = body;
543
- const pageUrl = typeof obj.url === 'string' ? obj.url.trim() : '';
544
- const pageTitle = typeof obj.title === 'string' ? obj.title.trim() : null;
545
- const pageContent = typeof obj.pageContent === 'string' ? obj.pageContent : '';
546
- const messages = Array.isArray(obj.messages) ? obj.messages : [];
547
- const modelOverride = typeof obj.model === 'string' ? obj.model.trim() : null;
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);
548
1017
  if (!pageUrl) {
549
- json(res, 400, { ok: false, error: 'missing url' }, cors);
1018
+ json(res, 400, { ok: false, error: "missing url" }, cors);
550
1019
  return;
551
1020
  }
552
- const session = createSession();
553
- sessions.set(session.id, session);
554
- json(res, 200, { ok: true, id: session.id }, cors);
555
- void (async () => {
1021
+ const runId = `agent-${randomUUID()}`;
1022
+ const wantsJson = wantsJsonResponse(req, url);
1023
+ if (wantsJson) {
556
1024
  try {
557
- await streamChatResponse({
1025
+ const assistant = await runWithProcessContext({ runId, source: "agent" }, async () => completeAgentResponse({
558
1026
  env,
559
- fetchImpl,
560
- session,
561
1027
  pageUrl,
562
1028
  pageTitle,
563
1029
  pageContent,
564
- messages: messages,
565
- modelOverride: modelOverride && modelOverride.toLowerCase() !== 'auto' ? modelOverride : null,
566
- pushToSession: (evt) => pushToSession(session, evt),
567
- emitMeta: (patch) => emitMeta(session, patch),
568
- });
1030
+ messages,
1031
+ modelOverride: modelOverride && modelOverride.toLowerCase() !== "auto" ? modelOverride : null,
1032
+ tools,
1033
+ automationEnabled,
1034
+ }));
1035
+ json(res, 200, { ok: true, assistant }, cors);
569
1036
  }
570
1037
  catch (error) {
571
1038
  const message = error instanceof Error ? error.message : String(error);
572
- pushToSession(session, { event: 'error', data: { message } });
573
- console.error('[summarize-daemon] chat failed', error);
1039
+ console.error("[summarize-daemon] agent failed", error);
1040
+ json(res, 500, { ok: false, error: message }, cors);
574
1041
  }
575
- finally {
576
- setTimeout(() => {
577
- sessions.delete(session.id);
578
- endSession(session);
579
- }, 60_000).unref();
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
+ }
580
1156
  }
581
- })();
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
+ }
582
1208
  return;
583
1209
  }
584
1210
  const eventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/events$/);
585
- if (req.method === 'GET' && eventsMatch) {
1211
+ if (req.method === "GET" && eventsMatch) {
586
1212
  const id = eventsMatch[1];
587
1213
  if (!id) {
588
1214
  json(res, 404, { ok: false }, cors);
@@ -590,14 +1216,14 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
590
1216
  }
591
1217
  const session = sessions.get(id);
592
1218
  if (!session) {
593
- json(res, 404, { ok: false, error: 'not found' }, cors);
1219
+ json(res, 404, { ok: false, error: "not found" }, cors);
594
1220
  return;
595
1221
  }
596
1222
  res.writeHead(200, {
597
1223
  ...cors,
598
- 'content-type': 'text/event-stream; charset=utf-8',
599
- 'cache-control': 'no-cache, no-transform',
600
- connection: 'keep-alive',
1224
+ "content-type": "text/event-stream; charset=utf-8",
1225
+ "cache-control": "no-cache, no-transform",
1226
+ connection: "keep-alive",
601
1227
  });
602
1228
  session.clients.add(res);
603
1229
  for (const entry of session.buffer) {
@@ -612,14 +1238,62 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
612
1238
  res.write(`: keepalive ${Date.now()}\n\n`);
613
1239
  }, 15_000);
614
1240
  keepalive.unref();
615
- res.on('close', () => {
1241
+ res.on("close", () => {
616
1242
  clearInterval(keepalive);
617
1243
  session.clients.delete(res);
618
1244
  });
619
1245
  return;
620
1246
  }
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
+ });
1293
+ return;
1294
+ }
621
1295
  const refreshEventsMatch = pathname.match(/^\/v1\/refresh-free\/([^/]+)\/events$/);
622
- if (req.method === 'GET' && refreshEventsMatch) {
1296
+ if (req.method === "GET" && refreshEventsMatch) {
623
1297
  const id = refreshEventsMatch[1];
624
1298
  if (!id) {
625
1299
  json(res, 404, { ok: false }, cors);
@@ -627,14 +1301,14 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
627
1301
  }
628
1302
  const session = refreshSessions.get(id);
629
1303
  if (!session) {
630
- json(res, 404, { ok: false, error: 'not found' }, cors);
1304
+ json(res, 404, { ok: false, error: "not found" }, cors);
631
1305
  return;
632
1306
  }
633
1307
  res.writeHead(200, {
634
1308
  ...cors,
635
- 'content-type': 'text/event-stream; charset=utf-8',
636
- 'cache-control': 'no-cache, no-transform',
637
- connection: 'keep-alive',
1309
+ "content-type": "text/event-stream; charset=utf-8",
1310
+ "cache-control": "no-cache, no-transform",
1311
+ connection: "keep-alive",
638
1312
  });
639
1313
  session.clients.add(res);
640
1314
  for (const entry of session.buffer) {
@@ -649,13 +1323,13 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
649
1323
  res.write(`: keepalive ${Date.now()}\n\n`);
650
1324
  }, 15_000);
651
1325
  keepalive.unref();
652
- res.on('close', () => {
1326
+ res.on("close", () => {
653
1327
  clearInterval(keepalive);
654
1328
  session.clients.delete(res);
655
1329
  });
656
1330
  return;
657
1331
  }
658
- text(res, 404, 'Not found', cors);
1332
+ text(res, 404, "Not found", cors);
659
1333
  })().catch((error) => {
660
1334
  const origin = resolveOriginHeader(req);
661
1335
  const cors = corsHeaders(origin);
@@ -674,10 +1348,10 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
674
1348
  });
675
1349
  try {
676
1350
  await new Promise((resolve, reject) => {
677
- server.once('error', reject);
1351
+ server.once("error", reject);
678
1352
  server.listen(port, DAEMON_HOST, () => {
679
1353
  const address = server.address();
680
- const actualPort = address && typeof address === 'object' && typeof address.port === 'number'
1354
+ const actualPort = address && typeof address === "object" && typeof address.port === "number"
681
1355
  ? address.port
682
1356
  : port;
683
1357
  onListening?.(actualPort);
@@ -692,14 +1366,14 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
692
1366
  resolved = true;
693
1367
  server.close(() => resolve());
694
1368
  };
695
- process.once('SIGTERM', onStop);
696
- process.once('SIGINT', onStop);
1369
+ process.once("SIGTERM", onStop);
1370
+ process.once("SIGINT", onStop);
697
1371
  if (signal) {
698
1372
  if (signal.aborted) {
699
1373
  onStop();
700
1374
  }
701
1375
  else {
702
- signal.addEventListener('abort', onStop, { once: true });
1376
+ signal.addEventListener("abort", onStop, { once: true });
703
1377
  }
704
1378
  }
705
1379
  });