@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,19 +1,138 @@
1
- import { countTokens } from 'gpt-tokenizer';
2
- import { render as renderMarkdownAnsi } from 'markdansi';
3
- import { buildLanguageKey, buildLengthKey, buildPromptHash, buildSummaryCacheKey, hashString, normalizeContentForHash, } from '../../../cache.js';
4
- import { formatOutputLanguageForJson } from '../../../language.js';
5
- import { parseGatewayStyleModelId } from '../../../llm/model-id.js';
6
- import { buildAutoModelAttempts } from '../../../model-auto.js';
7
- import { buildLinkSummaryPrompt } from '../../../prompts/index.js';
8
- import { parseCliUserModelId } from '../../env.js';
9
- import { buildExtractFinishLabel, buildLengthPartsForFinishLine, writeFinishLine, } from '../../finish-line.js';
10
- import { writeVerbose } from '../../logging.js';
11
- import { prepareMarkdownForTerminal } from '../../markdown.js';
12
- import { runModelAttempts } from '../../model-attempts.js';
13
- import { buildOpenRouterNoAllowedProvidersMessage } from '../../openrouter.js';
14
- import { isRichTty, markdownRenderWidth, supportsColor } from '../../terminal.js';
15
- export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOverride, lengthInstruction, languageInstruction, }) {
16
- const isYouTube = extracted.siteName === 'YouTube';
1
+ import { isTwitterStatusUrl, isYouTubeUrl } from "@steipete/summarize-core/content/url";
2
+ import { countTokens } from "gpt-tokenizer";
3
+ import { render as renderMarkdownAnsi } from "markdansi";
4
+ import { buildLanguageKey, buildLengthKey, buildPromptHash, buildSummaryCacheKey, hashString, normalizeContentForHash, } from "../../../cache.js";
5
+ import { formatOutputLanguageForJson } from "../../../language.js";
6
+ import { parseGatewayStyleModelId } from "../../../llm/model-id.js";
7
+ import { buildAutoModelAttempts } from "../../../model-auto.js";
8
+ import { buildLinkSummaryPrompt, SUMMARY_LENGTH_TARGET_CHARACTERS, SUMMARY_SYSTEM_PROMPT, } from "../../../prompts/index.js";
9
+ import { readLastSuccessfulCliProvider, writeLastSuccessfulCliProvider, } from "../../cli-fallback-state.js";
10
+ import { parseCliUserModelId } from "../../env.js";
11
+ import { buildExtractFinishLabel, buildLengthPartsForFinishLine, writeFinishLine, } from "../../finish-line.js";
12
+ import { resolveTargetCharacters } from "../../format.js";
13
+ import { writeVerbose } from "../../logging.js";
14
+ import { prepareMarkdownForTerminal } from "../../markdown.js";
15
+ import { runModelAttempts } from "../../model-attempts.js";
16
+ import { buildOpenRouterNoAllowedProvidersMessage } from "../../openrouter.js";
17
+ import { isRichTty, markdownRenderWidth, supportsColor } from "../../terminal.js";
18
+ import { coerceSummaryWithSlides, interleaveSlidesIntoTranscript, normalizeSummarySlideHeadings, } from "./slides-text.js";
19
+ const MAX_SLIDE_TRANSCRIPT_CHARS_BY_PRESET = {
20
+ short: 2500,
21
+ medium: 5000,
22
+ long: 9000,
23
+ xl: 15000,
24
+ xxl: 24000,
25
+ };
26
+ const SLIDE_TRANSCRIPT_DEFAULT_EDGE_SECONDS = 30;
27
+ const SLIDE_TRANSCRIPT_LEEWAY_SECONDS = 10;
28
+ function parseTimestampSeconds(value) {
29
+ const parts = value.split(":").map((item) => Number(item));
30
+ if (parts.some((item) => !Number.isFinite(item)))
31
+ return null;
32
+ if (parts.length === 2) {
33
+ const [minutes, seconds] = parts;
34
+ return minutes * 60 + seconds;
35
+ }
36
+ if (parts.length === 3) {
37
+ const [hours, minutes, seconds] = parts;
38
+ return hours * 3600 + minutes * 60 + seconds;
39
+ }
40
+ return null;
41
+ }
42
+ function parseTranscriptTimedText(input) {
43
+ if (!input)
44
+ return [];
45
+ const segments = [];
46
+ for (const line of input.split("\n")) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed.startsWith("["))
49
+ continue;
50
+ const match = trimmed.match(/^\[(\d{1,2}:\d{2}(?::\d{2})?)\]\s*(.*)$/);
51
+ if (!match)
52
+ continue;
53
+ const seconds = parseTimestampSeconds(match[1]);
54
+ if (seconds == null)
55
+ continue;
56
+ const text = (match[2] ?? "").trim();
57
+ if (!text)
58
+ continue;
59
+ segments.push({ startSeconds: seconds, text });
60
+ }
61
+ segments.sort((a, b) => a.startSeconds - b.startSeconds);
62
+ return segments;
63
+ }
64
+ function formatTimestamp(seconds) {
65
+ const clamped = Math.max(0, Math.floor(seconds));
66
+ const hours = Math.floor(clamped / 3600);
67
+ const minutes = Math.floor((clamped % 3600) / 60);
68
+ const secs = clamped % 60;
69
+ const mm = String(minutes).padStart(2, "0");
70
+ const ss = String(secs).padStart(2, "0");
71
+ if (hours <= 0)
72
+ return `${minutes}:${ss}`;
73
+ const hh = String(hours).padStart(2, "0");
74
+ return `${hh}:${mm}:${ss}`;
75
+ }
76
+ function truncateTranscript(value, limit) {
77
+ if (value.length <= limit)
78
+ return value;
79
+ const truncated = value.slice(0, limit).trimEnd();
80
+ const clean = truncated.replace(/\s+\S*$/, "").trim();
81
+ const result = clean.length > 0 ? clean : truncated.trim();
82
+ return result.length > 0 ? `${result}…` : "";
83
+ }
84
+ function buildSlidesPromptText({ slides, transcriptTimedText, preset, }) {
85
+ if (!slides || slides.slides.length === 0)
86
+ return null;
87
+ const segments = parseTranscriptTimedText(transcriptTimedText);
88
+ const slidesWithTimestamps = slides.slides
89
+ .filter((slide) => Number.isFinite(slide.timestamp))
90
+ .map((slide) => ({ index: slide.index, timestamp: Math.max(0, Math.floor(slide.timestamp)) }))
91
+ .sort((a, b) => a.timestamp - b.timestamp);
92
+ if (slidesWithTimestamps.length === 0)
93
+ return null;
94
+ const totalBudget = Number(MAX_SLIDE_TRANSCRIPT_CHARS_BY_PRESET[preset]);
95
+ const perSlideBudget = Math.max(120, Math.floor(totalBudget / Math.max(1, slidesWithTimestamps.length)));
96
+ let remaining = totalBudget;
97
+ const blocks = [];
98
+ for (let i = 0; i < slidesWithTimestamps.length; i += 1) {
99
+ const slide = slidesWithTimestamps[i];
100
+ if (!slide)
101
+ continue;
102
+ const prev = slidesWithTimestamps[i - 1];
103
+ const next = slidesWithTimestamps[i + 1];
104
+ const startBase = prev ? Math.floor((prev.timestamp + slide.timestamp) / 2) : slide.timestamp;
105
+ const endBase = next ? Math.ceil((slide.timestamp + next.timestamp) / 2) : slide.timestamp;
106
+ const start = Math.max(0, (prev ? startBase : slide.timestamp - SLIDE_TRANSCRIPT_DEFAULT_EDGE_SECONDS) -
107
+ SLIDE_TRANSCRIPT_LEEWAY_SECONDS);
108
+ const end = (next ? endBase : slide.timestamp + SLIDE_TRANSCRIPT_DEFAULT_EDGE_SECONDS) +
109
+ SLIDE_TRANSCRIPT_LEEWAY_SECONDS;
110
+ const excerptParts = [];
111
+ for (const segment of segments) {
112
+ if (segment.startSeconds < start)
113
+ continue;
114
+ if (segment.startSeconds > end)
115
+ break;
116
+ excerptParts.push(segment.text);
117
+ }
118
+ const excerptRaw = excerptParts.join(" ").trim().replace(/\s+/g, " ");
119
+ const excerptBudget = remaining > 0 ? Math.min(perSlideBudget, remaining) : 0;
120
+ const excerpt = excerptRaw && excerptBudget > 0 ? truncateTranscript(excerptRaw, excerptBudget) : "";
121
+ const label = `[slide:${slide.index}] [${formatTimestamp(start)}–${formatTimestamp(end)}]`;
122
+ const block = excerpt ? `${label}\n${excerpt}` : label;
123
+ blocks.push(block);
124
+ remaining = Math.max(0, remaining - block.length);
125
+ }
126
+ return blocks.length > 0 ? blocks.join("\n\n") : null;
127
+ }
128
+ export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOverride, lengthInstruction, languageInstruction, slides, }) {
129
+ const isYouTube = extracted.siteName === "YouTube";
130
+ const preset = lengthArg.kind === "preset" ? lengthArg.preset : "medium";
131
+ const slidesText = buildSlidesPromptText({
132
+ slides,
133
+ transcriptTimedText: extracted.transcriptTimedText,
134
+ preset,
135
+ });
17
136
  return buildLinkSummaryPrompt({
18
137
  url: extracted.url,
19
138
  title: extracted.title,
@@ -22,8 +141,10 @@ export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOve
22
141
  content: extracted.content,
23
142
  truncated: extracted.truncated,
24
143
  hasTranscript: isYouTube ||
25
- (extracted.transcriptSource !== null && extracted.transcriptSource !== 'unavailable'),
26
- summaryLength: lengthArg.kind === 'preset' ? lengthArg.preset : { maxCharacters: lengthArg.maxCharacters },
144
+ (extracted.transcriptSource !== null && extracted.transcriptSource !== "unavailable"),
145
+ hasTranscriptTimestamps: Boolean(extracted.transcriptTimedText),
146
+ slides: slidesText ? { count: slides?.slides.length ?? 0, text: slidesText } : null,
147
+ summaryLength: lengthArg.kind === "preset" ? lengthArg.preset : { maxCharacters: lengthArg.maxCharacters },
27
148
  outputLanguage,
28
149
  shares: [],
29
150
  promptOverride: promptOverride ?? null,
@@ -31,6 +152,97 @@ export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOve
31
152
  languageInstruction: languageInstruction ?? null,
32
153
  });
33
154
  }
155
+ function shouldBypassShortContentSummary({ extracted, lengthArg, forceSummary, maxOutputTokensArg, json, }) {
156
+ if (forceSummary)
157
+ return false;
158
+ if (!extracted.content || extracted.content.length === 0)
159
+ return false;
160
+ const targetCharacters = resolveTargetCharacters(lengthArg, SUMMARY_LENGTH_TARGET_CHARACTERS);
161
+ if (!Number.isFinite(targetCharacters) || targetCharacters <= 0)
162
+ return false;
163
+ if (extracted.content.length > targetCharacters)
164
+ return false;
165
+ if (!json && typeof maxOutputTokensArg === "number") {
166
+ const tokenCount = countTokens(extracted.content);
167
+ if (tokenCount > maxOutputTokensArg)
168
+ return false;
169
+ }
170
+ return true;
171
+ }
172
+ async function outputSummaryFromExtractedContent({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, slides, footerLabel, verboseMessage, }) {
173
+ const { io, flags, model, hooks } = ctx;
174
+ hooks.clearProgressForStdout();
175
+ const finishModel = pickModelForFinishLine(model.llmCalls, null);
176
+ if (flags.json) {
177
+ const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
178
+ const payload = {
179
+ input: {
180
+ kind: "url",
181
+ url,
182
+ timeoutMs: flags.timeoutMs,
183
+ youtube: flags.youtubeMode,
184
+ firecrawl: flags.firecrawlMode,
185
+ format: flags.format,
186
+ markdown: effectiveMarkdownMode,
187
+ timestamps: flags.transcriptTimestamps,
188
+ length: flags.lengthArg.kind === "preset"
189
+ ? { kind: "preset", preset: flags.lengthArg.preset }
190
+ : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
191
+ maxOutputTokens: flags.maxOutputTokensArg,
192
+ model: model.requestedModelLabel,
193
+ language: formatOutputLanguageForJson(flags.outputLanguage),
194
+ },
195
+ env: {
196
+ hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
197
+ hasOpenAIKey: Boolean(model.apiStatus.apiKey),
198
+ hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
199
+ hasApifyToken: Boolean(model.apiStatus.apifyToken),
200
+ hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
201
+ hasGoogleKey: model.apiStatus.googleConfigured,
202
+ hasAnthropicKey: model.apiStatus.anthropicConfigured,
203
+ },
204
+ extracted,
205
+ slides,
206
+ prompt,
207
+ llm: null,
208
+ metrics: flags.metricsEnabled ? finishReport : null,
209
+ summary: extracted.content,
210
+ };
211
+ io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
212
+ if (flags.metricsEnabled && finishReport) {
213
+ const costUsd = await hooks.estimateCostUsd();
214
+ hooks.clearProgressForStdout();
215
+ writeFinishLine({
216
+ stderr: io.stderr,
217
+ env: io.envForRun,
218
+ elapsedMs: Date.now() - flags.runStartedAtMs,
219
+ label: extractionUi.finishSourceLabel,
220
+ model: finishModel,
221
+ report: finishReport,
222
+ costUsd,
223
+ detailed: flags.metricsDetailed,
224
+ extraParts: buildFinishExtras({
225
+ extracted,
226
+ metricsDetailed: flags.metricsDetailed,
227
+ transcriptionCostLabel,
228
+ }),
229
+ color: flags.verboseColor,
230
+ });
231
+ }
232
+ return;
233
+ }
234
+ io.stdout.write(`${extracted.content}\n`);
235
+ hooks.restoreProgressAfterStdout?.();
236
+ if (extractionUi.footerParts.length > 0) {
237
+ const footer = footerLabel
238
+ ? [...extractionUi.footerParts, footerLabel]
239
+ : extractionUi.footerParts;
240
+ hooks.writeViaFooter(footer);
241
+ }
242
+ if (verboseMessage && flags.verbose) {
243
+ writeVerbose(io.stderr, flags.verbose, verboseMessage, flags.verboseColor, io.envForRun);
244
+ }
245
+ }
34
246
  const buildFinishExtras = ({ extracted, metricsDetailed, transcriptionCostLabel, }) => {
35
247
  const parts = [
36
248
  ...(buildLengthPartsForFinishLine(extracted, metricsDetailed) ?? []),
@@ -47,45 +259,46 @@ const pickModelForFinishLine = (llmCalls, fallback) => {
47
259
  }
48
260
  return null;
49
261
  };
50
- return (findLastModel('summary') ??
51
- findLastModel('markdown') ??
262
+ return (findLastModel("summary") ??
263
+ findLastModel("markdown") ??
52
264
  (llmCalls.length > 0 ? (llmCalls[llmCalls.length - 1]?.model ?? null) : null) ??
53
265
  fallback);
54
266
  };
55
267
  const buildModelMetaFromAttempt = (attempt) => {
56
- if (attempt.transport === 'cli') {
57
- return { provider: 'cli', canonical: attempt.userModelId };
268
+ if (attempt.transport === "cli") {
269
+ return { provider: "cli", canonical: attempt.userModelId };
58
270
  }
59
271
  const parsed = parseGatewayStyleModelId(attempt.llmModelId ?? attempt.userModelId);
60
- const canonical = attempt.userModelId.toLowerCase().startsWith('openrouter/')
272
+ const canonical = attempt.userModelId.toLowerCase().startsWith("openrouter/")
61
273
  ? attempt.userModelId
62
274
  : parsed.canonical;
63
275
  return { provider: parsed.provider, canonical };
64
276
  };
65
- export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, }) {
277
+ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, slides, slidesOutput, }) {
66
278
  const { io, flags, model, hooks } = ctx;
67
279
  hooks.clearProgressForStdout();
68
280
  const finishLabel = buildExtractFinishLabel({
69
281
  extracted: { diagnostics: extracted.diagnostics },
70
282
  format: flags.format,
71
283
  markdownMode: effectiveMarkdownMode,
72
- hasMarkdownLlmCall: model.llmCalls.some((call) => call.purpose === 'markdown'),
284
+ hasMarkdownLlmCall: model.llmCalls.some((call) => call.purpose === "markdown"),
73
285
  });
74
286
  const finishModel = pickModelForFinishLine(model.llmCalls, null);
75
287
  if (flags.json) {
76
288
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
77
289
  const payload = {
78
290
  input: {
79
- kind: 'url',
291
+ kind: "url",
80
292
  url,
81
293
  timeoutMs: flags.timeoutMs,
82
294
  youtube: flags.youtubeMode,
83
295
  firecrawl: flags.firecrawlMode,
84
296
  format: flags.format,
85
297
  markdown: effectiveMarkdownMode,
86
- length: flags.lengthArg.kind === 'preset'
87
- ? { kind: 'preset', preset: flags.lengthArg.preset }
88
- : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
298
+ timestamps: flags.transcriptTimestamps,
299
+ length: flags.lengthArg.kind === "preset"
300
+ ? { kind: "preset", preset: flags.lengthArg.preset }
301
+ : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
89
302
  maxOutputTokens: flags.maxOutputTokensArg,
90
303
  model: model.requestedModelLabel,
91
304
  language: formatOutputLanguageForJson(flags.outputLanguage),
@@ -100,16 +313,20 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
100
313
  hasAnthropicKey: model.apiStatus.anthropicConfigured,
101
314
  },
102
315
  extracted,
316
+ slides,
103
317
  prompt,
104
318
  llm: null,
105
319
  metrics: flags.metricsEnabled ? finishReport : null,
106
320
  summary: null,
107
321
  };
108
322
  io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
323
+ hooks.restoreProgressAfterStdout?.();
324
+ hooks.restoreProgressAfterStdout?.();
109
325
  if (flags.metricsEnabled && finishReport) {
110
326
  const costUsd = await hooks.estimateCostUsd();
111
327
  writeFinishLine({
112
328
  stderr: io.stderr,
329
+ env: io.envForRun,
113
330
  elapsedMs: Date.now() - flags.runStartedAtMs,
114
331
  label: finishLabel,
115
332
  model: finishModel,
@@ -126,29 +343,81 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
126
343
  }
127
344
  return;
128
345
  }
129
- const renderedExtract = flags.format === 'markdown' && !flags.plain && isRichTty(io.stdout)
130
- ? renderMarkdownAnsi(prepareMarkdownForTerminal(extracted.content), {
346
+ const extractCandidate = flags.transcriptTimestamps &&
347
+ extracted.transcriptTimedText &&
348
+ extracted.transcriptSource &&
349
+ extracted.content.toLowerCase().startsWith("transcript:")
350
+ ? `Transcript:\n${extracted.transcriptTimedText}`
351
+ : extracted.content;
352
+ const slideTags = slides?.slides && slides.slides.length > 0
353
+ ? slides.slides.map((slide) => `[slide:${slide.index}]`).join("\n")
354
+ : "";
355
+ if (slidesOutput && slides?.slides && slides.slides.length > 0) {
356
+ const transcriptText = extracted.transcriptTimedText
357
+ ? `Transcript:\n${extracted.transcriptTimedText}`
358
+ : null;
359
+ const interleaved = transcriptText
360
+ ? interleaveSlidesIntoTranscript({
361
+ transcriptTimedText: transcriptText,
362
+ slides: slides.slides.map((slide) => ({
363
+ index: slide.index,
364
+ timestamp: slide.timestamp,
365
+ })),
366
+ })
367
+ : `${extractCandidate.trimEnd()}\n\n${slideTags}`;
368
+ await slidesOutput.renderFromText(interleaved);
369
+ hooks.restoreProgressAfterStdout?.();
370
+ const slideFooter = slides ? [`slides ${slides.slides.length}`] : [];
371
+ hooks.writeViaFooter([...extractionUi.footerParts, ...slideFooter]);
372
+ const report = flags.shouldComputeReport ? await hooks.buildReport() : null;
373
+ if (flags.metricsEnabled && report) {
374
+ const costUsd = await hooks.estimateCostUsd();
375
+ writeFinishLine({
376
+ stderr: io.stderr,
377
+ env: io.envForRun,
378
+ elapsedMs: Date.now() - flags.runStartedAtMs,
379
+ label: finishLabel,
380
+ model: finishModel,
381
+ report,
382
+ costUsd,
383
+ detailed: flags.metricsDetailed,
384
+ extraParts: buildFinishExtras({
385
+ extracted,
386
+ metricsDetailed: flags.metricsDetailed,
387
+ transcriptionCostLabel,
388
+ }),
389
+ color: flags.verboseColor,
390
+ });
391
+ }
392
+ return;
393
+ }
394
+ const renderedExtract = flags.format === "markdown" && !flags.plain && isRichTty(io.stdout)
395
+ ? renderMarkdownAnsi(prepareMarkdownForTerminal(extractCandidate), {
131
396
  width: markdownRenderWidth(io.stdout, io.env),
132
397
  wrap: true,
133
398
  color: supportsColor(io.stdout, io.envForRun),
134
399
  hyperlinks: true,
135
400
  })
136
- : extracted.content;
137
- if (flags.format === 'markdown' && !flags.plain && isRichTty(io.stdout)) {
138
- io.stdout.write(`\n${renderedExtract.replace(/^\n+/, '')}`);
401
+ : extractCandidate;
402
+ if (flags.format === "markdown" && !flags.plain && isRichTty(io.stdout)) {
403
+ io.stdout.write(`\n${renderedExtract.replace(/^\n+/, "")}`);
139
404
  }
140
405
  else {
141
406
  io.stdout.write(renderedExtract);
142
407
  }
143
- if (!renderedExtract.endsWith('\n')) {
144
- io.stdout.write('\n');
408
+ if (!renderedExtract.endsWith("\n")) {
409
+ io.stdout.write("\n");
145
410
  }
146
- hooks.writeViaFooter(extractionUi.footerParts);
411
+ hooks.restoreProgressAfterStdout?.();
412
+ const slideFooter = slides ? [`slides ${slides.slides.length}`] : [];
413
+ hooks.writeViaFooter([...extractionUi.footerParts, ...slideFooter]);
147
414
  const report = flags.shouldComputeReport ? await hooks.buildReport() : null;
148
415
  if (flags.metricsEnabled && report) {
149
416
  const costUsd = await hooks.estimateCostUsd();
417
+ hooks.clearProgressForStdout();
150
418
  writeFinishLine({
151
419
  stderr: io.stderr,
420
+ env: io.envForRun,
152
421
  elapsedMs: Date.now() - flags.runStartedAtMs,
153
422
  label: finishLabel,
154
423
  model: finishModel,
@@ -164,11 +433,14 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
164
433
  });
165
434
  }
166
435
  }
167
- export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, onModelChosen, }) {
436
+ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, onModelChosen, slides, slidesOutput, }) {
168
437
  const { io, flags, model, cache: cacheState, hooks } = ctx;
169
- const promptPayload = { userText: prompt };
438
+ const lastSuccessfulCliProvider = model.isFallbackModel
439
+ ? await readLastSuccessfulCliProvider(io.envForRun)
440
+ : null;
441
+ const promptPayload = { system: SUMMARY_SYSTEM_PROMPT, userText: prompt };
170
442
  const promptTokens = countTokens(promptPayload.userText);
171
- const kindForAuto = extracted.siteName === 'YouTube' ? 'youtube' : 'website';
443
+ const kindForAuto = extracted.siteName === "YouTube" ? "youtube" : "website";
172
444
  const attempts = await (async () => {
173
445
  if (model.isFallbackModel) {
174
446
  const catalog = await model.getLiteLlmCatalog();
@@ -182,14 +454,17 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
182
454
  catalog,
183
455
  openrouterProvidersFromEnv: null,
184
456
  cliAvailability: model.cliAvailability,
457
+ isImplicitAutoSelection: model.isImplicitAutoSelection,
458
+ allowAutoCliFallback: model.allowAutoCliFallback,
459
+ lastSuccessfulCliProvider,
185
460
  });
186
461
  if (flags.verbose) {
187
462
  for (const attempt of list.slice(0, 8)) {
188
- writeVerbose(io.stderr, flags.verbose, `auto candidate ${attempt.debug}`, flags.verboseColor);
463
+ writeVerbose(io.stderr, flags.verbose, `auto candidate ${attempt.debug}`, flags.verboseColor, io.envForRun);
189
464
  }
190
465
  }
191
466
  return list.map((attempt) => {
192
- if (attempt.transport !== 'cli')
467
+ if (attempt.transport !== "cli")
193
468
  return model.summaryEngine.applyZaiOverrides(attempt);
194
469
  const parsed = parseCliUserModelId(attempt.userModelId);
195
470
  return { ...attempt, cliProvider: parsed.provider, cliModel: parsed.model };
@@ -197,12 +472,12 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
197
472
  }
198
473
  /* v8 ignore next */
199
474
  if (!model.fixedModelSpec) {
200
- throw new Error('Internal error: missing fixed model spec');
475
+ throw new Error("Internal error: missing fixed model spec");
201
476
  }
202
- if (model.fixedModelSpec.transport === 'cli') {
477
+ if (model.fixedModelSpec.transport === "cli") {
203
478
  return [
204
479
  {
205
- transport: 'cli',
480
+ transport: "cli",
206
481
  userModelId: model.fixedModelSpec.userModelId,
207
482
  llmModelId: null,
208
483
  cliProvider: model.fixedModelSpec.cliProvider,
@@ -213,7 +488,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
213
488
  },
214
489
  ];
215
490
  }
216
- const openaiOverrides = model.fixedModelSpec.requiredEnv === 'Z_AI_API_KEY'
491
+ const openaiOverrides = model.fixedModelSpec.requiredEnv === "Z_AI_API_KEY"
217
492
  ? {
218
493
  openaiApiKeyOverride: model.apiStatus.zaiApiKey,
219
494
  openaiBaseUrlOverride: model.apiStatus.zaiBaseUrl,
@@ -222,7 +497,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
222
497
  : {};
223
498
  return [
224
499
  {
225
- transport: model.fixedModelSpec.transport === 'openrouter' ? 'openrouter' : 'native',
500
+ transport: model.fixedModelSpec.transport === "openrouter" ? "openrouter" : "native",
226
501
  userModelId: model.fixedModelSpec.userModelId,
227
502
  llmModelId: model.fixedModelSpec.llmModelId,
228
503
  openrouterProviders: model.fixedModelSpec.openrouterProviders,
@@ -232,45 +507,119 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
232
507
  },
233
508
  ];
234
509
  })();
235
- const cacheStore = cacheState.mode === 'default' ? cacheState.store : null;
510
+ const cacheStore = cacheState.mode === "default" && !flags.summaryCacheBypass ? cacheState.store : null;
236
511
  const contentHash = cacheStore ? hashString(normalizeContentForHash(extracted.content)) : null;
237
512
  const promptHash = cacheStore ? buildPromptHash(prompt) : null;
238
513
  const lengthKey = buildLengthKey(flags.lengthArg);
239
514
  const languageKey = buildLanguageKey(flags.outputLanguage);
515
+ const autoSelectionCacheModel = model.isFallbackModel
516
+ ? `selection:${model.requestedModelInput.toLowerCase()}`
517
+ : null;
240
518
  let summaryResult = null;
241
519
  let usedAttempt = null;
242
520
  let summaryFromCache = false;
243
521
  let cacheChecked = false;
522
+ const isTweet = extracted.siteName?.toLowerCase() === "x" || isTwitterStatusUrl(extracted.url);
523
+ const isYouTube = extracted.siteName === "YouTube" || isYouTubeUrl(url);
524
+ const hasMedia = Boolean(extracted.video) ||
525
+ (extracted.transcriptSource != null && extracted.transcriptSource !== "unavailable") ||
526
+ (typeof extracted.mediaDurationSeconds === "number" && extracted.mediaDurationSeconds > 0) ||
527
+ extracted.isVideoOnly === true;
528
+ const autoBypass = ctx.model.isFallbackModel && !ctx.model.isNamedModelSelection;
529
+ const canBypassShortContent = (autoBypass || isTweet) &&
530
+ !flags.slides &&
531
+ !hasMedia &&
532
+ flags.streamMode !== "on" &&
533
+ !isYouTube &&
534
+ shouldBypassShortContentSummary({
535
+ extracted,
536
+ lengthArg: flags.lengthArg,
537
+ forceSummary: flags.forceSummary,
538
+ maxOutputTokensArg: flags.maxOutputTokensArg,
539
+ json: flags.json,
540
+ });
541
+ if (canBypassShortContent) {
542
+ await outputSummaryFromExtractedContent({
543
+ ctx,
544
+ url,
545
+ extracted,
546
+ extractionUi,
547
+ prompt,
548
+ effectiveMarkdownMode,
549
+ transcriptionCostLabel,
550
+ slides,
551
+ footerLabel: "short content",
552
+ verboseMessage: "short content: skipping summary",
553
+ });
554
+ return;
555
+ }
244
556
  if (cacheStore && contentHash && promptHash) {
245
557
  cacheChecked = true;
246
- for (const attempt of attempts) {
247
- if (!model.summaryEngine.envHasKeyFor(attempt.requiredEnv))
248
- continue;
558
+ if (autoSelectionCacheModel) {
249
559
  const key = buildSummaryCacheKey({
250
560
  contentHash,
251
561
  promptHash,
252
- model: attempt.userModelId,
562
+ model: autoSelectionCacheModel,
253
563
  lengthKey,
254
564
  languageKey,
255
565
  });
256
- const cached = cacheStore.getText('summary', key);
257
- if (!cached)
258
- continue;
259
- writeVerbose(io.stderr, flags.verbose, 'cache hit summary', flags.verboseColor);
260
- onModelChosen?.(attempt.userModelId);
261
- summaryResult = {
262
- summary: cached,
263
- summaryAlreadyPrinted: false,
264
- modelMeta: buildModelMetaFromAttempt(attempt),
265
- maxOutputTokensForCall: null,
266
- };
267
- usedAttempt = attempt;
268
- summaryFromCache = true;
269
- break;
566
+ const cached = cacheStore.getJson("summary", key);
567
+ const cachedSummary = cached && typeof cached.summary === "string" ? cached.summary.trim() : null;
568
+ const cachedModelId = cached && typeof cached.model === "string" ? cached.model.trim() : null;
569
+ if (cachedSummary) {
570
+ const cachedAttempt = cachedModelId
571
+ ? (attempts.find((attempt) => attempt.userModelId === cachedModelId) ?? null)
572
+ : null;
573
+ const fallbackAttempt = attempts.find((attempt) => model.summaryEngine.envHasKeyFor(attempt.requiredEnv)) ??
574
+ attempts[0] ??
575
+ null;
576
+ const matchedAttempt = cachedAttempt && model.summaryEngine.envHasKeyFor(cachedAttempt.requiredEnv)
577
+ ? cachedAttempt
578
+ : fallbackAttempt;
579
+ if (matchedAttempt) {
580
+ writeVerbose(io.stderr, flags.verbose, "cache hit summary (auto selection)", flags.verboseColor, io.envForRun);
581
+ onModelChosen?.(cachedModelId || matchedAttempt.userModelId);
582
+ summaryResult = {
583
+ summary: cachedSummary,
584
+ summaryAlreadyPrinted: false,
585
+ modelMeta: buildModelMetaFromAttempt(matchedAttempt),
586
+ maxOutputTokensForCall: null,
587
+ };
588
+ usedAttempt = matchedAttempt;
589
+ summaryFromCache = true;
590
+ }
591
+ }
592
+ }
593
+ if (!summaryFromCache) {
594
+ for (const attempt of attempts) {
595
+ if (!model.summaryEngine.envHasKeyFor(attempt.requiredEnv))
596
+ continue;
597
+ const key = buildSummaryCacheKey({
598
+ contentHash,
599
+ promptHash,
600
+ model: attempt.userModelId,
601
+ lengthKey,
602
+ languageKey,
603
+ });
604
+ const cached = cacheStore.getText("summary", key);
605
+ if (!cached)
606
+ continue;
607
+ writeVerbose(io.stderr, flags.verbose, "cache hit summary", flags.verboseColor, io.envForRun);
608
+ onModelChosen?.(attempt.userModelId);
609
+ summaryResult = {
610
+ summary: cached,
611
+ summaryAlreadyPrinted: false,
612
+ modelMeta: buildModelMetaFromAttempt(attempt),
613
+ maxOutputTokensForCall: null,
614
+ };
615
+ usedAttempt = attempt;
616
+ summaryFromCache = true;
617
+ break;
618
+ }
270
619
  }
271
620
  }
272
621
  if (cacheChecked && !summaryFromCache) {
273
- writeVerbose(io.stderr, flags.verbose, 'cache miss summary', flags.verboseColor);
622
+ writeVerbose(io.stderr, flags.verbose, "cache miss summary", flags.verboseColor, io.envForRun);
274
623
  }
275
624
  ctx.hooks.onSummaryCached?.(summaryFromCache);
276
625
  let lastError = null;
@@ -284,10 +633,10 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
284
633
  envHasKeyFor: model.summaryEngine.envHasKeyFor,
285
634
  formatMissingModelError: model.summaryEngine.formatMissingModelError,
286
635
  onAutoSkip: (attempt) => {
287
- writeVerbose(io.stderr, flags.verbose, `auto skip ${attempt.userModelId}: missing ${attempt.requiredEnv}`, flags.verboseColor);
636
+ writeVerbose(io.stderr, flags.verbose, `auto skip ${attempt.userModelId}: missing ${attempt.requiredEnv}`, flags.verboseColor, io.envForRun);
288
637
  },
289
638
  onAutoFailure: (attempt, error) => {
290
- writeVerbose(io.stderr, flags.verbose, `auto failed ${attempt.userModelId}: ${error instanceof Error ? error.message : String(error)}`, flags.verboseColor);
639
+ writeVerbose(io.stderr, flags.verbose, `auto failed ${attempt.userModelId}: ${error instanceof Error ? error.message : String(error)}`, flags.verboseColor, io.envForRun);
291
640
  },
292
641
  onFixedModelError: (_attempt, error) => {
293
642
  throw error;
@@ -297,6 +646,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
297
646
  prompt: promptPayload,
298
647
  allowStreaming: flags.streamingEnabled,
299
648
  onModelChosen: onModelChosen ?? null,
649
+ streamHandler: slidesOutput?.streamHandler ?? null,
300
650
  }),
301
651
  });
302
652
  summaryResult = attemptOutcome.result;
@@ -315,7 +665,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
315
665
  };
316
666
  if (model.isNamedModelSelection) {
317
667
  if (lastError === null && missingRequiredEnvs.size > 0) {
318
- throw new Error(withFreeTip(`Missing ${Array.from(missingRequiredEnvs).sort().join(', ')} for --model ${model.requestedModelInput}.`));
668
+ throw new Error(withFreeTip(`Missing ${Array.from(missingRequiredEnvs).sort().join(", ")} for --model ${model.requestedModelInput}.`));
319
669
  }
320
670
  if (lastError instanceof Error) {
321
671
  if (sawOpenRouterNoAllowedProviders) {
@@ -330,97 +680,68 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
330
680
  }
331
681
  throw new Error(withFreeTip(`No model available for --model ${model.requestedModelInput}`));
332
682
  }
333
- hooks.clearProgressForStdout();
334
- if (flags.json) {
335
- const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
336
- const finishModel = pickModelForFinishLine(model.llmCalls, null);
337
- const payload = {
338
- input: {
339
- kind: 'url',
340
- url,
341
- timeoutMs: flags.timeoutMs,
342
- youtube: flags.youtubeMode,
343
- firecrawl: flags.firecrawlMode,
344
- format: flags.format,
345
- markdown: effectiveMarkdownMode,
346
- length: flags.lengthArg.kind === 'preset'
347
- ? { kind: 'preset', preset: flags.lengthArg.preset }
348
- : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
349
- maxOutputTokens: flags.maxOutputTokensArg,
350
- model: model.requestedModelLabel,
351
- language: formatOutputLanguageForJson(flags.outputLanguage),
352
- },
353
- env: {
354
- hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
355
- hasOpenAIKey: Boolean(model.apiStatus.apiKey),
356
- hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
357
- hasApifyToken: Boolean(model.apiStatus.apifyToken),
358
- hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
359
- hasGoogleKey: model.apiStatus.googleConfigured,
360
- hasAnthropicKey: model.apiStatus.anthropicConfigured,
361
- },
362
- extracted,
363
- prompt,
364
- llm: null,
365
- metrics: flags.metricsEnabled ? finishReport : null,
366
- summary: extracted.content,
367
- };
368
- io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
369
- if (flags.metricsEnabled && finishReport) {
370
- const costUsd = await hooks.estimateCostUsd();
371
- writeFinishLine({
372
- stderr: io.stderr,
373
- elapsedMs: Date.now() - flags.runStartedAtMs,
374
- label: extractionUi.finishSourceLabel,
375
- model: finishModel,
376
- report: finishReport,
377
- costUsd,
378
- detailed: flags.metricsDetailed,
379
- extraParts: buildFinishExtras({
380
- extracted,
381
- metricsDetailed: flags.metricsDetailed,
382
- transcriptionCostLabel,
383
- }),
384
- color: flags.verboseColor,
385
- });
386
- }
387
- return;
388
- }
389
- io.stdout.write(`${extracted.content}\n`);
390
- if (extractionUi.footerParts.length > 0) {
391
- hooks.writeViaFooter([...extractionUi.footerParts, 'no model']);
392
- }
393
- if (lastError instanceof Error && flags.verbose) {
394
- writeVerbose(io.stderr, flags.verbose, `auto failed all models: ${lastError.message}`, flags.verboseColor);
395
- }
683
+ await outputSummaryFromExtractedContent({
684
+ ctx,
685
+ url,
686
+ extracted,
687
+ extractionUi,
688
+ prompt,
689
+ effectiveMarkdownMode,
690
+ transcriptionCostLabel,
691
+ slides,
692
+ footerLabel: "no model",
693
+ verboseMessage: lastError instanceof Error ? `auto failed all models: ${lastError.message}` : null,
694
+ });
396
695
  return;
397
696
  }
398
697
  if (!summaryFromCache && cacheStore && contentHash && promptHash) {
399
- const key = buildSummaryCacheKey({
698
+ const perModelKey = buildSummaryCacheKey({
400
699
  contentHash,
401
700
  promptHash,
402
701
  model: usedAttempt.userModelId,
403
702
  lengthKey,
404
703
  languageKey,
405
704
  });
406
- cacheStore.setText('summary', key, summaryResult.summary, cacheState.ttlMs);
407
- writeVerbose(io.stderr, flags.verbose, 'cache write summary', flags.verboseColor);
705
+ cacheStore.setText("summary", perModelKey, summaryResult.summary, cacheState.ttlMs);
706
+ writeVerbose(io.stderr, flags.verbose, "cache write summary", flags.verboseColor, io.envForRun);
707
+ if (autoSelectionCacheModel) {
708
+ const selectionKey = buildSummaryCacheKey({
709
+ contentHash,
710
+ promptHash,
711
+ model: autoSelectionCacheModel,
712
+ lengthKey,
713
+ languageKey,
714
+ });
715
+ cacheStore.setJson("summary", selectionKey, { summary: summaryResult.summary, model: usedAttempt.userModelId }, cacheState.ttlMs);
716
+ writeVerbose(io.stderr, flags.verbose, "cache write summary (auto selection)", flags.verboseColor, io.envForRun);
717
+ }
718
+ }
719
+ if (!summaryFromCache &&
720
+ model.isFallbackModel &&
721
+ usedAttempt.transport === "cli" &&
722
+ usedAttempt.cliProvider) {
723
+ await writeLastSuccessfulCliProvider({
724
+ env: io.envForRun,
725
+ provider: usedAttempt.cliProvider,
726
+ });
408
727
  }
409
728
  const { summary, summaryAlreadyPrinted, modelMeta, maxOutputTokensForCall } = summaryResult;
729
+ const normalizedSummary = slides && slides.slides.length > 0 ? normalizeSummarySlideHeadings(summary) : summary;
410
730
  if (flags.json) {
411
731
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
412
732
  const payload = {
413
733
  input: {
414
- kind: 'url',
734
+ kind: "url",
415
735
  url,
416
736
  timeoutMs: flags.timeoutMs,
417
737
  youtube: flags.youtubeMode,
418
738
  firecrawl: flags.firecrawlMode,
419
739
  format: flags.format,
420
740
  markdown: effectiveMarkdownMode,
421
- length: flags.lengthArg.kind === 'preset'
422
- ? { kind: 'preset', preset: flags.lengthArg.preset }
423
- : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
741
+ timestamps: flags.transcriptTimestamps,
742
+ length: flags.lengthArg.kind === "preset"
743
+ ? { kind: "preset", preset: flags.lengthArg.preset }
744
+ : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
424
745
  maxOutputTokens: flags.maxOutputTokensArg,
425
746
  model: model.requestedModelLabel,
426
747
  language: formatOutputLanguageForJson(flags.outputLanguage),
@@ -435,23 +756,25 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
435
756
  hasAnthropicKey: model.apiStatus.anthropicConfigured,
436
757
  },
437
758
  extracted,
759
+ slides,
438
760
  prompt,
439
761
  llm: {
440
762
  provider: modelMeta.provider,
441
763
  model: usedAttempt.userModelId,
442
764
  maxCompletionTokens: maxOutputTokensForCall,
443
- strategy: 'single',
765
+ strategy: "single",
444
766
  },
445
767
  metrics: flags.metricsEnabled ? finishReport : null,
446
- summary,
768
+ summary: normalizedSummary,
447
769
  };
448
770
  io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
449
771
  if (flags.metricsEnabled && finishReport) {
450
772
  const costUsd = await hooks.estimateCostUsd();
451
773
  writeFinishLine({
452
774
  stderr: io.stderr,
775
+ env: io.envForRun,
453
776
  elapsedMs: Date.now() - flags.runStartedAtMs,
454
- elapsedLabel: summaryFromCache ? 'Cached' : null,
777
+ elapsedLabel: summaryFromCache ? "Cached" : null,
455
778
  label: extractionUi.finishSourceLabel,
456
779
  model: usedAttempt.userModelId,
457
780
  report: finishReport,
@@ -467,35 +790,53 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
467
790
  }
468
791
  return;
469
792
  }
470
- if (!summaryAlreadyPrinted) {
793
+ if (slidesOutput) {
794
+ if (!summaryAlreadyPrinted) {
795
+ const summaryForSlides = slides && slides.slides.length > 0
796
+ ? coerceSummaryWithSlides({
797
+ markdown: normalizedSummary,
798
+ slides: slides.slides.map((slide) => ({
799
+ index: slide.index,
800
+ timestamp: slide.timestamp,
801
+ })),
802
+ transcriptTimedText: extracted.transcriptTimedText ?? null,
803
+ lengthArg: flags.lengthArg,
804
+ })
805
+ : normalizedSummary;
806
+ await slidesOutput.renderFromText(summaryForSlides);
807
+ }
808
+ }
809
+ else if (!summaryAlreadyPrinted) {
471
810
  hooks.clearProgressForStdout();
472
811
  const rendered = !flags.plain && isRichTty(io.stdout)
473
- ? renderMarkdownAnsi(prepareMarkdownForTerminal(summary), {
812
+ ? renderMarkdownAnsi(prepareMarkdownForTerminal(normalizedSummary), {
474
813
  width: markdownRenderWidth(io.stdout, io.env),
475
814
  wrap: true,
476
815
  color: supportsColor(io.stdout, io.envForRun),
477
816
  hyperlinks: true,
478
817
  })
479
- : summary;
818
+ : normalizedSummary;
480
819
  if (!flags.plain && isRichTty(io.stdout)) {
481
- io.stdout.write(`\n${rendered.replace(/^\n+/, '')}`);
820
+ io.stdout.write(`\n${rendered.replace(/^\n+/, "")}`);
482
821
  }
483
822
  else {
484
823
  if (isRichTty(io.stdout))
485
- io.stdout.write('\n');
486
- io.stdout.write(rendered.replace(/^\n+/, ''));
824
+ io.stdout.write("\n");
825
+ io.stdout.write(rendered.replace(/^\n+/, ""));
487
826
  }
488
- if (!rendered.endsWith('\n')) {
489
- io.stdout.write('\n');
827
+ if (!rendered.endsWith("\n")) {
828
+ io.stdout.write("\n");
490
829
  }
830
+ hooks.restoreProgressAfterStdout?.();
491
831
  }
492
832
  const report = flags.shouldComputeReport ? await hooks.buildReport() : null;
493
833
  if (flags.metricsEnabled && report) {
494
834
  const costUsd = await hooks.estimateCostUsd();
495
835
  writeFinishLine({
496
836
  stderr: io.stderr,
837
+ env: io.envForRun,
497
838
  elapsedMs: Date.now() - flags.runStartedAtMs,
498
- elapsedLabel: summaryFromCache ? 'Cached' : null,
839
+ elapsedLabel: summaryFromCache ? "Cached" : null,
499
840
  label: extractionUi.finishSourceLabel,
500
841
  model: modelMeta.canonical,
501
842
  report,