@steipete/summarize 0.8.2 → 0.10.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 (284) hide show
  1. package/CHANGELOG.md +114 -1
  2. package/LICENSE +1 -1
  3. package/README.md +309 -182
  4. package/dist/cli.js +1 -1
  5. package/dist/esm/cache.js +72 -4
  6. package/dist/esm/cache.js.map +1 -1
  7. package/dist/esm/config.js +197 -1
  8. package/dist/esm/config.js.map +1 -1
  9. package/dist/esm/content/asset.js +75 -2
  10. package/dist/esm/content/asset.js.map +1 -1
  11. package/dist/esm/daemon/agent.js +547 -0
  12. package/dist/esm/daemon/agent.js.map +1 -0
  13. package/dist/esm/daemon/chat.js +97 -0
  14. package/dist/esm/daemon/chat.js.map +1 -0
  15. package/dist/esm/daemon/cli.js +105 -10
  16. package/dist/esm/daemon/cli.js.map +1 -1
  17. package/dist/esm/daemon/env-snapshot.js +3 -0
  18. package/dist/esm/daemon/env-snapshot.js.map +1 -1
  19. package/dist/esm/daemon/flow-context.js +53 -28
  20. package/dist/esm/daemon/flow-context.js.map +1 -1
  21. package/dist/esm/daemon/launchd.js +27 -0
  22. package/dist/esm/daemon/launchd.js.map +1 -1
  23. package/dist/esm/daemon/process-registry.js +206 -0
  24. package/dist/esm/daemon/process-registry.js.map +1 -0
  25. package/dist/esm/daemon/schtasks.js +64 -0
  26. package/dist/esm/daemon/schtasks.js.map +1 -1
  27. package/dist/esm/daemon/server.js +1034 -52
  28. package/dist/esm/daemon/server.js.map +1 -1
  29. package/dist/esm/daemon/summarize.js +66 -18
  30. package/dist/esm/daemon/summarize.js.map +1 -1
  31. package/dist/esm/daemon/systemd.js +61 -0
  32. package/dist/esm/daemon/systemd.js.map +1 -1
  33. package/dist/esm/flags.js +24 -0
  34. package/dist/esm/flags.js.map +1 -1
  35. package/dist/esm/llm/attachments.js +2 -0
  36. package/dist/esm/llm/attachments.js.map +1 -0
  37. package/dist/esm/llm/errors.js +6 -0
  38. package/dist/esm/llm/errors.js.map +1 -0
  39. package/dist/esm/llm/generate-text.js +206 -356
  40. package/dist/esm/llm/generate-text.js.map +1 -1
  41. package/dist/esm/llm/html-to-markdown.js +1 -2
  42. package/dist/esm/llm/html-to-markdown.js.map +1 -1
  43. package/dist/esm/llm/prompt.js.map +1 -1
  44. package/dist/esm/llm/providers/anthropic.js +126 -0
  45. package/dist/esm/llm/providers/anthropic.js.map +1 -0
  46. package/dist/esm/llm/providers/google.js +78 -0
  47. package/dist/esm/llm/providers/google.js.map +1 -0
  48. package/dist/esm/llm/providers/models.js +111 -0
  49. package/dist/esm/llm/providers/models.js.map +1 -0
  50. package/dist/esm/llm/providers/openai.js +150 -0
  51. package/dist/esm/llm/providers/openai.js.map +1 -0
  52. package/dist/esm/llm/providers/shared.js +48 -0
  53. package/dist/esm/llm/providers/shared.js.map +1 -0
  54. package/dist/esm/llm/providers/types.js +2 -0
  55. package/dist/esm/llm/providers/types.js.map +1 -0
  56. package/dist/esm/llm/transcript-to-markdown.js +1 -2
  57. package/dist/esm/llm/transcript-to-markdown.js.map +1 -1
  58. package/dist/esm/llm/types.js +2 -0
  59. package/dist/esm/llm/types.js.map +1 -0
  60. package/dist/esm/llm/usage.js +69 -0
  61. package/dist/esm/llm/usage.js.map +1 -0
  62. package/dist/esm/logging/daemon.js +124 -0
  63. package/dist/esm/logging/daemon.js.map +1 -0
  64. package/dist/esm/logging/ring-file.js +66 -0
  65. package/dist/esm/logging/ring-file.js.map +1 -0
  66. package/dist/esm/media-cache.js +251 -0
  67. package/dist/esm/media-cache.js.map +1 -0
  68. package/dist/esm/model-auto.js +103 -5
  69. package/dist/esm/model-auto.js.map +1 -1
  70. package/dist/esm/processes.js +2 -0
  71. package/dist/esm/processes.js.map +1 -0
  72. package/dist/esm/refresh-free.js +3 -3
  73. package/dist/esm/refresh-free.js.map +1 -1
  74. package/dist/esm/run/attachments.js +8 -4
  75. package/dist/esm/run/attachments.js.map +1 -1
  76. package/dist/esm/run/bird.js +118 -5
  77. package/dist/esm/run/bird.js.map +1 -1
  78. package/dist/esm/run/cache-state.js +3 -2
  79. package/dist/esm/run/cache-state.js.map +1 -1
  80. package/dist/esm/run/cli-preflight.js +19 -1
  81. package/dist/esm/run/cli-preflight.js.map +1 -1
  82. package/dist/esm/run/constants.js +0 -7
  83. package/dist/esm/run/constants.js.map +1 -1
  84. package/dist/esm/run/finish-line.js +58 -11
  85. package/dist/esm/run/finish-line.js.map +1 -1
  86. package/dist/esm/run/flows/asset/extract.js +70 -0
  87. package/dist/esm/run/flows/asset/extract.js.map +1 -0
  88. package/dist/esm/run/flows/asset/input.js +209 -25
  89. package/dist/esm/run/flows/asset/input.js.map +1 -1
  90. package/dist/esm/run/flows/asset/media-policy.js +3 -0
  91. package/dist/esm/run/flows/asset/media-policy.js.map +1 -0
  92. package/dist/esm/run/flows/asset/media.js +224 -0
  93. package/dist/esm/run/flows/asset/media.js.map +1 -0
  94. package/dist/esm/run/flows/asset/output.js +98 -0
  95. package/dist/esm/run/flows/asset/output.js.map +1 -0
  96. package/dist/esm/run/flows/asset/preprocess.js +92 -16
  97. package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
  98. package/dist/esm/run/flows/asset/summary.js +165 -11
  99. package/dist/esm/run/flows/asset/summary.js.map +1 -1
  100. package/dist/esm/run/flows/url/extract.js +6 -6
  101. package/dist/esm/run/flows/url/extract.js.map +1 -1
  102. package/dist/esm/run/flows/url/flow.js +338 -36
  103. package/dist/esm/run/flows/url/flow.js.map +1 -1
  104. package/dist/esm/run/flows/url/markdown.js +6 -1
  105. package/dist/esm/run/flows/url/markdown.js.map +1 -1
  106. package/dist/esm/run/flows/url/slides-output.js +485 -0
  107. package/dist/esm/run/flows/url/slides-output.js.map +1 -0
  108. package/dist/esm/run/flows/url/slides-text.js +628 -0
  109. package/dist/esm/run/flows/url/slides-text.js.map +1 -0
  110. package/dist/esm/run/flows/url/summary.js +358 -83
  111. package/dist/esm/run/flows/url/summary.js.map +1 -1
  112. package/dist/esm/run/help.js +94 -5
  113. package/dist/esm/run/help.js.map +1 -1
  114. package/dist/esm/run/logging.js +12 -4
  115. package/dist/esm/run/logging.js.map +1 -1
  116. package/dist/esm/run/media-cache-state.js +33 -0
  117. package/dist/esm/run/media-cache-state.js.map +1 -0
  118. package/dist/esm/run/progress.js +19 -1
  119. package/dist/esm/run/progress.js.map +1 -1
  120. package/dist/esm/run/run-context.js +19 -0
  121. package/dist/esm/run/run-context.js.map +1 -0
  122. package/dist/esm/run/run-output.js +1 -1
  123. package/dist/esm/run/run-output.js.map +1 -1
  124. package/dist/esm/run/run-settings.js +182 -0
  125. package/dist/esm/run/run-settings.js.map +1 -0
  126. package/dist/esm/run/runner.js +225 -32
  127. package/dist/esm/run/runner.js.map +1 -1
  128. package/dist/esm/run/slides-cli.js +225 -0
  129. package/dist/esm/run/slides-cli.js.map +1 -0
  130. package/dist/esm/run/slides-render.js +163 -0
  131. package/dist/esm/run/slides-render.js.map +1 -0
  132. package/dist/esm/run/stream-output.js +63 -0
  133. package/dist/esm/run/stream-output.js.map +1 -0
  134. package/dist/esm/run/streaming.js +16 -43
  135. package/dist/esm/run/streaming.js.map +1 -1
  136. package/dist/esm/run/summary-engine.js +59 -41
  137. package/dist/esm/run/summary-engine.js.map +1 -1
  138. package/dist/esm/run/transcriber-cli.js +148 -0
  139. package/dist/esm/run/transcriber-cli.js.map +1 -0
  140. package/dist/esm/shared/sse-events.js +26 -0
  141. package/dist/esm/shared/sse-events.js.map +1 -0
  142. package/dist/esm/shared/streaming-merge.js +44 -0
  143. package/dist/esm/shared/streaming-merge.js.map +1 -0
  144. package/dist/esm/slides/extract.js +1942 -0
  145. package/dist/esm/slides/extract.js.map +1 -0
  146. package/dist/esm/slides/index.js +4 -0
  147. package/dist/esm/slides/index.js.map +1 -0
  148. package/dist/esm/slides/settings.js +73 -0
  149. package/dist/esm/slides/settings.js.map +1 -0
  150. package/dist/esm/slides/store.js +111 -0
  151. package/dist/esm/slides/store.js.map +1 -0
  152. package/dist/esm/slides/types.js +2 -0
  153. package/dist/esm/slides/types.js.map +1 -0
  154. package/dist/esm/tty/osc-progress.js +21 -1
  155. package/dist/esm/tty/osc-progress.js.map +1 -1
  156. package/dist/esm/tty/progress/fetch-html.js +8 -4
  157. package/dist/esm/tty/progress/fetch-html.js.map +1 -1
  158. package/dist/esm/tty/progress/transcript.js +82 -31
  159. package/dist/esm/tty/progress/transcript.js.map +1 -1
  160. package/dist/esm/tty/spinner.js +2 -2
  161. package/dist/esm/tty/spinner.js.map +1 -1
  162. package/dist/esm/tty/theme.js +189 -0
  163. package/dist/esm/tty/theme.js.map +1 -0
  164. package/dist/esm/tty/website-progress.js +17 -13
  165. package/dist/esm/tty/website-progress.js.map +1 -1
  166. package/dist/esm/version.js +1 -1
  167. package/dist/esm/version.js.map +1 -1
  168. package/dist/types/cache.d.ts +14 -2
  169. package/dist/types/config.d.ts +34 -0
  170. package/dist/types/daemon/agent.d.ts +25 -0
  171. package/dist/types/daemon/chat.d.ts +27 -0
  172. package/dist/types/daemon/env-snapshot.d.ts +1 -1
  173. package/dist/types/daemon/flow-context.d.ts +24 -3
  174. package/dist/types/daemon/launchd.d.ts +4 -0
  175. package/dist/types/daemon/process-registry.d.ts +73 -0
  176. package/dist/types/daemon/schtasks.d.ts +4 -0
  177. package/dist/types/daemon/server.d.ts +7 -1
  178. package/dist/types/daemon/summarize.d.ts +47 -5
  179. package/dist/types/daemon/systemd.d.ts +4 -0
  180. package/dist/types/flags.d.ts +1 -0
  181. package/dist/types/llm/attachments.d.ts +6 -0
  182. package/dist/types/llm/errors.d.ts +1 -0
  183. package/dist/types/llm/generate-text.d.ts +29 -13
  184. package/dist/types/llm/prompt.d.ts +7 -2
  185. package/dist/types/llm/providers/anthropic.d.ts +30 -0
  186. package/dist/types/llm/providers/google.d.ts +29 -0
  187. package/dist/types/llm/providers/models.d.ts +27 -0
  188. package/dist/types/llm/providers/openai.d.ts +38 -0
  189. package/dist/types/llm/providers/shared.d.ts +14 -0
  190. package/dist/types/llm/providers/types.d.ts +6 -0
  191. package/dist/types/llm/types.d.ts +5 -0
  192. package/dist/types/llm/usage.d.ts +5 -0
  193. package/dist/types/logging/daemon.d.ts +26 -0
  194. package/dist/types/logging/ring-file.d.ts +10 -0
  195. package/dist/types/media-cache.d.ts +22 -0
  196. package/dist/types/model-auto.d.ts +1 -0
  197. package/dist/types/processes.d.ts +1 -0
  198. package/dist/types/run/attachments.d.ts +9 -6
  199. package/dist/types/run/bird.d.ts +7 -0
  200. package/dist/types/run/constants.d.ts +0 -2
  201. package/dist/types/run/finish-line.d.ts +59 -1
  202. package/dist/types/run/flows/asset/extract.d.ts +18 -0
  203. package/dist/types/run/flows/asset/input.d.ts +12 -2
  204. package/dist/types/run/flows/asset/media-policy.d.ts +2 -0
  205. package/dist/types/run/flows/asset/media.d.ts +21 -0
  206. package/dist/types/run/flows/asset/output.d.ts +42 -0
  207. package/dist/types/run/flows/asset/preprocess.d.ts +22 -2
  208. package/dist/types/run/flows/asset/summary.d.ts +6 -0
  209. package/dist/types/run/flows/url/extract.d.ts +2 -1
  210. package/dist/types/run/flows/url/slides-output.d.ts +66 -0
  211. package/dist/types/run/flows/url/slides-text.d.ts +87 -0
  212. package/dist/types/run/flows/url/summary.d.ts +11 -3
  213. package/dist/types/run/flows/url/types.d.ts +29 -2
  214. package/dist/types/run/help.d.ts +3 -0
  215. package/dist/types/run/logging.d.ts +3 -2
  216. package/dist/types/run/media-cache-state.d.ts +7 -0
  217. package/dist/types/run/progress.d.ts +2 -1
  218. package/dist/types/run/run-context.d.ts +44 -0
  219. package/dist/types/run/run-settings.d.ts +62 -0
  220. package/dist/types/run/slides-cli.d.ts +9 -0
  221. package/dist/types/run/slides-render.d.ts +30 -0
  222. package/dist/types/run/stream-output.d.ts +12 -0
  223. package/dist/types/run/streaming.d.ts +10 -4
  224. package/dist/types/run/summary-engine.d.ts +15 -3
  225. package/dist/types/run/summary-llm.d.ts +2 -2
  226. package/dist/types/run/transcriber-cli.d.ts +8 -0
  227. package/dist/types/shared/sse-events.d.ts +64 -0
  228. package/dist/types/shared/streaming-merge.d.ts +4 -0
  229. package/dist/types/slides/extract.d.ts +42 -0
  230. package/dist/types/slides/index.d.ts +5 -0
  231. package/dist/types/slides/settings.d.ts +20 -0
  232. package/dist/types/slides/store.d.ts +15 -0
  233. package/dist/types/slides/types.d.ts +40 -0
  234. package/dist/types/tty/osc-progress.d.ts +2 -2
  235. package/dist/types/tty/progress/fetch-html.d.ts +3 -1
  236. package/dist/types/tty/progress/transcript.d.ts +3 -1
  237. package/dist/types/tty/spinner.d.ts +3 -1
  238. package/dist/types/tty/theme.d.ts +44 -0
  239. package/dist/types/tty/website-progress.d.ts +3 -1
  240. package/dist/types/version.d.ts +1 -1
  241. package/docs/README.md +13 -8
  242. package/docs/_config.yml +26 -0
  243. package/docs/_layouts/default.html +60 -0
  244. package/docs/agent.md +333 -0
  245. package/docs/assets/site.css +748 -0
  246. package/docs/assets/site.js +72 -0
  247. package/docs/assets/summarize-cli.png +0 -0
  248. package/docs/assets/summarize-extension.png +0 -0
  249. package/docs/assets/youtube-slides.png +0 -0
  250. package/docs/cache.md +29 -3
  251. package/docs/chrome-extension.md +85 -7
  252. package/docs/config.md +74 -2
  253. package/docs/extract-only.md +10 -2
  254. package/docs/index.html +205 -0
  255. package/docs/index.md +25 -0
  256. package/docs/language.md +1 -1
  257. package/docs/llm.md +17 -1
  258. package/docs/manual-tests.md +2 -0
  259. package/docs/media.md +37 -0
  260. package/docs/model-auto.md +2 -1
  261. package/docs/nvidia-onnx-transcription.md +55 -0
  262. package/docs/openai.md +5 -0
  263. package/docs/releasing.md +26 -0
  264. package/docs/site/assets/site.css +399 -228
  265. package/docs/site/assets/summarize-cli.png +0 -0
  266. package/docs/site/assets/summarize-extension.png +0 -0
  267. package/docs/site/docs/chrome-extension.html +89 -0
  268. package/docs/site/docs/config.html +1 -0
  269. package/docs/site/docs/extract-only.html +1 -0
  270. package/docs/site/docs/firecrawl.html +1 -0
  271. package/docs/site/docs/index.html +5 -0
  272. package/docs/site/docs/llm.html +1 -0
  273. package/docs/site/docs/openai.html +1 -0
  274. package/docs/site/docs/website.html +1 -0
  275. package/docs/site/docs/youtube.html +1 -0
  276. package/docs/site/index.html +148 -84
  277. package/docs/slides.md +74 -0
  278. package/docs/timestamps.md +103 -0
  279. package/docs/website.md +13 -0
  280. package/docs/youtube.md +16 -0
  281. package/package.json +22 -18
  282. package/dist/esm/daemon/request-settings.js +0 -91
  283. package/dist/esm/daemon/request-settings.js.map +0 -1
  284. package/dist/types/daemon/request-settings.d.ts +0 -27
@@ -1,9 +1,12 @@
1
- import { buildExtractCacheKey } from '../../../cache.js';
1
+ import * as urlUtils from '@steipete/summarize-core/content/url';
2
+ import { buildExtractCacheKey, buildSlidesCacheKey } from '../../../cache.js';
2
3
  import { loadRemoteAsset } from '../../../content/asset.js';
3
4
  import { createLinkPreviewClient, } from '../../../content/index.js';
4
5
  import { createFirecrawlScraper } from '../../../firecrawl.js';
6
+ import { extractSlidesForSource, resolveSlideSource, validateSlidesCache, } from '../../../slides/index.js';
5
7
  import { createOscProgressController } from '../../../tty/osc-progress.js';
6
8
  import { startSpinner } from '../../../tty/spinner.js';
9
+ import { createThemeRenderer, resolveThemeNameFromSources, resolveTrueColor, } from '../../../tty/theme.js';
7
10
  import { createWebsiteProgress } from '../../../tty/website-progress.js';
8
11
  import { assertAssetMediaTypeSupported } from '../../attachments.js';
9
12
  import { readTweetWithBird } from '../../bird.js';
@@ -14,22 +17,28 @@ import { estimateWhisperTranscriptionCostUsd, formatOptionalNumber, formatOption
14
17
  import { writeVerbose } from '../../logging.js';
15
18
  import { deriveExtractionUi, fetchLinkContentWithBirdTip, logExtractionDiagnostics, } from './extract.js';
16
19
  import { createMarkdownConverters } from './markdown.js';
20
+ import { createSlidesTerminalOutput } from './slides-output.js';
17
21
  import { buildUrlPrompt, outputExtractedUrl, summarizeExtractedUrl } from './summary.js';
18
22
  export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
19
23
  if (!url) {
20
24
  throw new Error('Only HTTP and HTTPS URLs can be summarized');
21
25
  }
22
26
  const { io, flags, model, cache: cacheState, hooks } = ctx;
27
+ const theme = createThemeRenderer({
28
+ themeName: resolveThemeNameFromSources({ env: io.envForRun.SUMMARIZE_THEME }),
29
+ enabled: flags.verboseColor,
30
+ trueColor: resolveTrueColor(io.envForRun),
31
+ });
23
32
  const markdown = createMarkdownConverters(ctx, { isYoutubeUrl });
24
33
  if (flags.firecrawlMode === 'always' && !model.apiStatus.firecrawlConfigured) {
25
34
  throw new Error('--firecrawl always requires FIRECRAWL_API_KEY');
26
35
  }
27
36
  writeVerbose(io.stderr, flags.verbose, `config url=${url} timeoutMs=${flags.timeoutMs} youtube=${flags.youtubeMode} firecrawl=${flags.firecrawlMode} length=${flags.lengthArg.kind === 'preset'
28
37
  ? flags.lengthArg.preset
29
- : `${flags.lengthArg.maxCharacters} chars`} maxOutputTokens=${formatOptionalNumber(flags.maxOutputTokensArg)} retries=${flags.retries} json=${flags.json} extract=${flags.extractMode} format=${flags.format} preprocess=${flags.preprocessMode} markdownMode=${flags.markdownMode} model=${model.requestedModelLabel} videoMode=${flags.videoMode} stream=${flags.streamingEnabled ? 'on' : 'off'} plain=${flags.plain}`, flags.verboseColor);
30
- writeVerbose(io.stderr, flags.verbose, `configFile path=${formatOptionalString(flags.configPath)} model=${formatOptionalString(flags.configModelLabel)}`, flags.verboseColor);
31
- writeVerbose(io.stderr, flags.verbose, `env xaiKey=${Boolean(model.apiStatus.xaiApiKey)} openaiKey=${Boolean(model.apiStatus.apiKey)} zaiKey=${Boolean(model.apiStatus.zaiApiKey)} googleKey=${model.apiStatus.googleConfigured} anthropicKey=${model.apiStatus.anthropicConfigured} openrouterKey=${model.apiStatus.openrouterConfigured} apifyToken=${Boolean(model.apiStatus.apifyToken)} firecrawlKey=${model.apiStatus.firecrawlConfigured}`, flags.verboseColor);
32
- writeVerbose(io.stderr, flags.verbose, `markdown htmlRequested=${markdown.markdownRequested} transcriptRequested=${markdown.transcriptMarkdownRequested} provider=${markdown.markdownProvider}`, flags.verboseColor);
38
+ : `${flags.lengthArg.maxCharacters} chars`} maxOutputTokens=${formatOptionalNumber(flags.maxOutputTokensArg)} retries=${flags.retries} json=${flags.json} extract=${flags.extractMode} format=${flags.format} preprocess=${flags.preprocessMode} markdownMode=${flags.markdownMode} model=${model.requestedModelLabel} videoMode=${flags.videoMode} timestamps=${flags.transcriptTimestamps ? 'on' : 'off'} stream=${flags.streamingEnabled ? 'on' : 'off'} plain=${flags.plain}`, flags.verboseColor, io.envForRun);
39
+ writeVerbose(io.stderr, flags.verbose, `configFile path=${formatOptionalString(flags.configPath)} model=${formatOptionalString(flags.configModelLabel)}`, flags.verboseColor, io.envForRun);
40
+ writeVerbose(io.stderr, flags.verbose, `env xaiKey=${Boolean(model.apiStatus.xaiApiKey)} openaiKey=${Boolean(model.apiStatus.apiKey)} zaiKey=${Boolean(model.apiStatus.zaiApiKey)} googleKey=${model.apiStatus.googleConfigured} anthropicKey=${model.apiStatus.anthropicConfigured} openrouterKey=${model.apiStatus.openrouterConfigured} apifyToken=${Boolean(model.apiStatus.apifyToken)} firecrawlKey=${model.apiStatus.firecrawlConfigured}`, flags.verboseColor, io.envForRun);
41
+ writeVerbose(io.stderr, flags.verbose, `markdown htmlRequested=${markdown.markdownRequested} transcriptRequested=${markdown.transcriptMarkdownRequested} provider=${markdown.markdownProvider}`, flags.verboseColor, io.envForRun);
33
42
  const firecrawlApiKey = model.apiStatus.firecrawlApiKey;
34
43
  const scrapeWithFirecrawl = model.apiStatus.firecrawlConfigured && flags.firecrawlMode !== 'off' && firecrawlApiKey
35
44
  ? createFirecrawlScraper({
@@ -40,7 +49,7 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
40
49
  const readTweetWithBirdClient = hasBirdCli(io.env)
41
50
  ? ({ url, timeoutMs }) => readTweetWithBird({ url, timeoutMs, env: io.env })
42
51
  : null;
43
- writeVerbose(io.stderr, flags.verbose, 'extract start', flags.verboseColor);
52
+ writeVerbose(io.stderr, flags.verbose, 'extract start', flags.verboseColor, io.envForRun);
44
53
  const oscProgress = createOscProgressController({
45
54
  label: 'Fetching website',
46
55
  env: io.env,
@@ -49,18 +58,65 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
49
58
  });
50
59
  oscProgress.setIndeterminate('Fetching website');
51
60
  const spinner = startSpinner({
52
- text: 'Fetching website (connecting)…',
61
+ text: `${theme.label('Fetching website')}${theme.dim(' (connecting)…')}`,
53
62
  enabled: flags.progressEnabled,
54
63
  stream: io.stderr,
64
+ color: theme.palette.spinner,
55
65
  });
66
+ const styleLabel = (text) => theme.label(text);
67
+ const styleDim = (text) => theme.dim(text);
68
+ const renderStatus = (label, detail = '…') => `${styleLabel(label)}${styleDim(detail)}`;
69
+ const renderStatusWithMeta = (label, meta, suffix = '…') => `${styleLabel(label)} ${meta}${styleDim(suffix)}`;
70
+ const renderStatusFromText = (text) => {
71
+ const match = text.match(/^([^:]+):(.*)$/);
72
+ if (!match)
73
+ return styleLabel(text);
74
+ return `${styleLabel(match[1])}${styleDim(`:${match[2]}`)}`;
75
+ };
76
+ const handleSignal = () => {
77
+ try {
78
+ spinner.stopAndClear();
79
+ }
80
+ catch {
81
+ // ignore
82
+ }
83
+ oscProgress.clear();
84
+ };
85
+ const handleSigint = () => {
86
+ handleSignal();
87
+ process.exit(130);
88
+ };
89
+ const handleSigterm = () => {
90
+ handleSignal();
91
+ process.exit(143);
92
+ };
93
+ if (flags.progressEnabled) {
94
+ process.once('SIGINT', handleSigint);
95
+ process.once('SIGTERM', handleSigterm);
96
+ }
97
+ if (!hooks.onSlidesProgress && flags.progressEnabled) {
98
+ hooks.onSlidesProgress = (text) => {
99
+ const match = text.match(/(\d{1,3})%/);
100
+ const percent = match ? Number(match[1]) : null;
101
+ spinner.setText(renderStatusFromText(text));
102
+ if (Number.isFinite(percent) && percent !== null) {
103
+ oscProgress.setPercent('Slides', Math.max(0, Math.min(100, percent)));
104
+ }
105
+ else {
106
+ oscProgress.setIndeterminate('Slides');
107
+ }
108
+ };
109
+ }
56
110
  const websiteProgress = createWebsiteProgress({
57
111
  enabled: flags.progressEnabled,
58
112
  spinner,
59
113
  oscProgress,
114
+ theme,
60
115
  });
61
116
  const cacheStore = cacheState.mode === 'default' ? cacheState.store : null;
62
117
  const transcriptCache = cacheStore ? cacheStore.transcriptCache : null;
63
118
  const client = createLinkPreviewClient({
119
+ env: io.envForRun,
64
120
  apifyApiToken: model.apiStatus.apifyToken,
65
121
  ytDlpPath: model.apiStatus.ytDlpPath,
66
122
  falApiKey: model.apiStatus.falApiKey,
@@ -78,6 +134,7 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
78
134
  },
79
135
  fetch: io.fetch,
80
136
  transcriptCache,
137
+ mediaCache: ctx.mediaCache ?? null,
81
138
  onProgress: websiteProgress || hooks.onLinkPreviewProgress
82
139
  ? (event) => {
83
140
  websiteProgress?.onProgress(event);
@@ -94,10 +151,11 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
94
151
  spinner.stopAndClear();
95
152
  oscProgress.clear();
96
153
  };
97
- const clearProgressLine = () => {
98
- stopProgress();
154
+ const pauseProgressLine = () => {
155
+ spinner.pause();
156
+ return () => spinner.resume();
99
157
  };
100
- hooks.setClearProgressBeforeStdout(clearProgressLine);
158
+ hooks.setClearProgressBeforeStdout(pauseProgressLine);
101
159
  try {
102
160
  const buildFetchOptions = () => ({
103
161
  timeoutMs: flags.timeoutMs,
@@ -105,49 +163,266 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
105
163
  ? flags.maxExtractCharacters
106
164
  : undefined,
107
165
  youtubeTranscript: flags.youtubeMode,
166
+ mediaTranscript: flags.videoMode === 'transcript' ? 'prefer' : 'auto',
167
+ transcriptTimestamps: flags.transcriptTimestamps,
108
168
  firecrawl: flags.firecrawlMode,
109
169
  format: markdown.markdownRequested ? 'markdown' : 'text',
110
170
  markdownMode: markdown.markdownRequested ? markdown.effectiveMarkdownMode : undefined,
111
171
  cacheMode: cacheState.mode,
112
172
  });
113
- const fetchWithCache = async (targetUrl) => {
173
+ const fetchWithCache = async (targetUrl, { bypassExtractCache = false, } = {}) => {
114
174
  const options = buildFetchOptions();
115
175
  const cacheKey = cacheStore && cacheState.mode === 'default'
116
176
  ? buildExtractCacheKey({
117
177
  url: targetUrl,
118
178
  options: {
119
179
  youtubeTranscript: options.youtubeTranscript,
180
+ mediaTranscript: options.mediaTranscript,
120
181
  firecrawl: options.firecrawl,
121
182
  format: options.format,
122
183
  markdownMode: options.markdownMode ?? null,
184
+ transcriptTimestamps: options.transcriptTimestamps ?? false,
123
185
  ...(typeof options.maxCharacters === 'number'
124
186
  ? { maxCharacters: options.maxCharacters }
125
187
  : {}),
126
188
  },
127
189
  })
128
190
  : null;
129
- if (cacheKey && cacheStore) {
191
+ if (!bypassExtractCache && cacheKey && cacheStore) {
130
192
  const cached = cacheStore.getJson('extract', cacheKey);
131
193
  if (cached) {
132
- writeVerbose(io.stderr, flags.verbose, 'cache hit extract', flags.verboseColor);
194
+ writeVerbose(io.stderr, flags.verbose, 'cache hit extract', flags.verboseColor, io.envForRun);
133
195
  return cached;
134
196
  }
135
- writeVerbose(io.stderr, flags.verbose, 'cache miss extract', flags.verboseColor);
197
+ writeVerbose(io.stderr, flags.verbose, 'cache miss extract', flags.verboseColor, io.envForRun);
136
198
  }
137
- const extracted = await fetchLinkContentWithBirdTip({
138
- client,
139
- url: targetUrl,
140
- options,
141
- env: io.env,
142
- });
143
- if (cacheKey && cacheStore) {
144
- cacheStore.setJson('extract', cacheKey, extracted, cacheState.ttlMs);
145
- writeVerbose(io.stderr, flags.verbose, 'cache write extract', flags.verboseColor);
199
+ try {
200
+ const extracted = await fetchLinkContentWithBirdTip({
201
+ client,
202
+ url: targetUrl,
203
+ options,
204
+ env: io.env,
205
+ });
206
+ if (cacheKey && cacheStore) {
207
+ cacheStore.setJson('extract', cacheKey, extracted, cacheState.ttlMs);
208
+ writeVerbose(io.stderr, flags.verbose, 'cache write extract', flags.verboseColor, io.envForRun);
209
+ }
210
+ return extracted;
211
+ }
212
+ catch (err) {
213
+ const preferUrlMode = typeof urlUtils.shouldPreferUrlMode === 'function'
214
+ ? urlUtils.shouldPreferUrlMode(targetUrl)
215
+ : false;
216
+ const isTwitter = urlUtils.isTwitterStatusUrl?.(targetUrl) ?? false;
217
+ if (!preferUrlMode || isTwitter)
218
+ throw err;
219
+ // Fallback: skip HTML fetch and proceed with URL-only extraction (YouTube/direct media).
220
+ writeVerbose(io.stderr, flags.verbose, `extract fallback url-only (${err.message ?? String(err)})`, flags.verboseColor, io.envForRun);
221
+ return {
222
+ content: '',
223
+ title: null,
224
+ description: null,
225
+ url: targetUrl,
226
+ siteName: null,
227
+ wordCount: 0,
228
+ totalCharacters: 0,
229
+ truncated: false,
230
+ mediaDurationSeconds: null,
231
+ video: null,
232
+ isVideoOnly: true,
233
+ transcriptSource: null,
234
+ transcriptCharacters: null,
235
+ transcriptWordCount: null,
236
+ transcriptLines: null,
237
+ transcriptMetadata: null,
238
+ transcriptSegments: null,
239
+ transcriptTimedText: null,
240
+ transcriptionProvider: null,
241
+ diagnostics: {
242
+ strategy: 'html',
243
+ firecrawl: {
244
+ attempted: false,
245
+ used: false,
246
+ cacheMode: cacheState.mode,
247
+ cacheStatus: 'bypassed',
248
+ notes: 'skipped (url-only fallback)',
249
+ },
250
+ markdown: {
251
+ requested: false,
252
+ used: false,
253
+ provider: null,
254
+ notes: 'skipped (url fallback)',
255
+ },
256
+ transcript: {
257
+ cacheMode: cacheState.mode,
258
+ cacheStatus: 'unknown',
259
+ textProvided: false,
260
+ provider: null,
261
+ attemptedProviders: [],
262
+ },
263
+ },
264
+ };
146
265
  }
147
- return extracted;
148
266
  };
149
267
  let extracted = await fetchWithCache(url);
268
+ if (flags.slides && !resolveSlideSource({ url, extracted })) {
269
+ const isTwitter = urlUtils.isTwitterStatusUrl?.(url) ?? false;
270
+ if (isTwitter) {
271
+ const refreshed = await fetchWithCache(url, { bypassExtractCache: true });
272
+ if (resolveSlideSource({ url, extracted: refreshed })) {
273
+ writeVerbose(io.stderr, flags.verbose, 'extract refresh for slides', flags.verboseColor, io.envForRun);
274
+ extracted = refreshed;
275
+ }
276
+ }
277
+ }
150
278
  let extractionUi = deriveExtractionUi(extracted);
279
+ let slidesExtracted = null;
280
+ let slidesDone = false;
281
+ let slidesTimelineResolved = false;
282
+ let resolveSlidesTimeline = null;
283
+ const slidesTimelinePromise = flags.slides
284
+ ? new Promise((resolve) => {
285
+ resolveSlidesTimeline = resolve;
286
+ })
287
+ : null;
288
+ const resolveTimeline = (value) => {
289
+ if (slidesTimelineResolved)
290
+ return;
291
+ slidesTimelineResolved = true;
292
+ resolveSlidesTimeline?.(value);
293
+ };
294
+ const slidesOutputEnabled = Boolean(flags.slides) && flags.slidesOutput !== false && !flags.json && !flags.extractMode;
295
+ const slidesOutput = createSlidesTerminalOutput({
296
+ io,
297
+ flags: { plain: flags.plain, lengthArg: flags.lengthArg, slidesDebug: flags.slidesDebug },
298
+ extracted,
299
+ slides: null,
300
+ enabled: slidesOutputEnabled,
301
+ outputMode: 'delta',
302
+ clearProgressForStdout: hooks.clearProgressForStdout,
303
+ restoreProgressAfterStdout: hooks.restoreProgressAfterStdout ?? null,
304
+ onProgressText: flags.progressEnabled
305
+ ? (text) => spinner.setText(renderStatusFromText(text))
306
+ : null,
307
+ });
308
+ if (slidesOutput) {
309
+ const existingSlidesExtracted = hooks.onSlidesExtracted;
310
+ const existingSlidesDone = hooks.onSlidesDone;
311
+ const existingSlideChunk = hooks.onSlideChunk;
312
+ hooks.onSlidesExtracted = (value) => {
313
+ existingSlidesExtracted?.(value);
314
+ slidesOutput.onSlidesExtracted(value);
315
+ };
316
+ hooks.onSlidesDone = (result) => {
317
+ existingSlidesDone?.(result);
318
+ slidesOutput.onSlidesDone(result);
319
+ };
320
+ hooks.onSlideChunk = (chunk) => {
321
+ existingSlideChunk?.(chunk);
322
+ slidesOutput.onSlideChunk(chunk);
323
+ };
324
+ }
325
+ const markSlidesDone = (result) => {
326
+ if (slidesDone)
327
+ return;
328
+ slidesDone = true;
329
+ hooks.onSlidesDone?.(result);
330
+ };
331
+ const runSlidesExtraction = async () => {
332
+ if (!flags.slides)
333
+ return null;
334
+ if (slidesExtracted) {
335
+ if (!slidesDone)
336
+ markSlidesDone({ ok: true });
337
+ return slidesExtracted;
338
+ }
339
+ let errorMessage = null;
340
+ try {
341
+ const source = resolveSlideSource({ url, extracted });
342
+ if (!source) {
343
+ throw new Error('Slides are only supported for YouTube or direct video URLs.');
344
+ }
345
+ const slidesCacheKey = cacheStore && cacheState.mode === 'default'
346
+ ? buildSlidesCacheKey({ url: source.url, settings: flags.slides })
347
+ : null;
348
+ if (slidesCacheKey && cacheStore) {
349
+ const cached = cacheStore.getJson('slides', slidesCacheKey);
350
+ const validated = cached
351
+ ? await validateSlidesCache({ cached, source, settings: flags.slides })
352
+ : null;
353
+ if (validated) {
354
+ writeVerbose(io.stderr, flags.verbose, 'cache hit slides', flags.verboseColor, io.envForRun);
355
+ slidesExtracted = validated;
356
+ resolveTimeline(validated);
357
+ ctx.hooks.onSlidesExtracted?.(slidesExtracted);
358
+ ctx.hooks.onSlidesProgress?.('Slides: cached 100%');
359
+ return slidesExtracted;
360
+ }
361
+ writeVerbose(io.stderr, flags.verbose, 'cache miss slides', flags.verboseColor, io.envForRun);
362
+ }
363
+ if (flags.progressEnabled) {
364
+ spinner.setText(renderStatus('Extracting slides'));
365
+ oscProgress.setIndeterminate('Extracting slides');
366
+ }
367
+ // Prefer indeterminate progress until we get real percentage updates from the slide pipeline.
368
+ ctx.hooks.onSlidesProgress?.('Slides: extracting');
369
+ const onSlidesLog = (message) => {
370
+ writeVerbose(io.stderr, flags.verbose, `slides ${message}`, flags.verboseColor, io.envForRun);
371
+ };
372
+ slidesExtracted = await extractSlidesForSource({
373
+ source,
374
+ settings: flags.slides,
375
+ noCache: cacheState.mode === 'bypass',
376
+ mediaCache: ctx.mediaCache,
377
+ env: io.env,
378
+ timeoutMs: flags.timeoutMs,
379
+ ytDlpPath: model.apiStatus.ytDlpPath,
380
+ ffmpegPath: null,
381
+ tesseractPath: null,
382
+ hooks: {
383
+ onSlideChunk: (chunk) => ctx.hooks.onSlideChunk?.(chunk),
384
+ onSlidesTimeline: (timeline) => {
385
+ resolveTimeline(timeline);
386
+ ctx.hooks.onSlidesExtracted?.(timeline);
387
+ },
388
+ onSlidesProgress: ctx.hooks.onSlidesProgress ?? undefined,
389
+ onSlidesLog,
390
+ },
391
+ });
392
+ if (slidesExtracted) {
393
+ ctx.hooks.onSlidesExtracted?.(slidesExtracted);
394
+ ctx.hooks.onSlidesProgress?.(`Slides: done (${slidesExtracted.slides.length.toString()} slides) 100%`);
395
+ if (slidesCacheKey && cacheStore) {
396
+ cacheStore.setJson('slides', slidesCacheKey, slidesExtracted, cacheState.ttlMs);
397
+ writeVerbose(io.stderr, flags.verbose, 'cache write slides', flags.verboseColor, io.envForRun);
398
+ }
399
+ }
400
+ if (flags.progressEnabled) {
401
+ updateSummaryProgress();
402
+ }
403
+ return slidesExtracted;
404
+ }
405
+ catch (error) {
406
+ errorMessage = error instanceof Error ? error.message : String(error);
407
+ throw error;
408
+ }
409
+ finally {
410
+ if (!slidesTimelineResolved) {
411
+ resolveTimeline(slidesExtracted ?? null);
412
+ }
413
+ if (!slidesDone) {
414
+ markSlidesDone(errorMessage ? { ok: false, error: errorMessage } : { ok: true });
415
+ }
416
+ }
417
+ };
418
+ const formatSummaryProgress = (modelId) => {
419
+ const dim = (value) => theme.dim(value);
420
+ const accent = (value) => theme.accent(value);
421
+ const sentLabel = `${dim('sent ')}${extractionUi.contentSizeLabel}${extractionUi.viaSourceLabel}`;
422
+ const modelLabel = modelId ? `${dim('model: ')}${accent(modelId)}` : '';
423
+ const meta = modelLabel ? `${sentLabel}${dim(', ')}${modelLabel}` : sentLabel;
424
+ return `${styleLabel('Summarizing')} ${dim('(')}${meta}${dim(')')}${dim('…')}`;
425
+ };
151
426
  const updateSummaryProgress = () => {
152
427
  if (!flags.progressEnabled)
153
428
  return;
@@ -156,8 +431,8 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
156
431
  oscProgress.setIndeterminate('Summarizing');
157
432
  }
158
433
  spinner.setText(flags.extractMode
159
- ? `Extracted (${extractionUi.contentSizeLabel}${extractionUi.viaSourceLabel})`
160
- : `Summarizing (sent ${extractionUi.contentSizeLabel}${extractionUi.viaSourceLabel})…`);
434
+ ? `${styleLabel('Extracted')}${styleDim(` (${extractionUi.contentSizeLabel}${extractionUi.viaSourceLabel})`)}`
435
+ : formatSummaryProgress());
161
436
  };
162
437
  updateSummaryProgress();
163
438
  logExtractionDiagnostics({
@@ -165,10 +440,11 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
165
440
  stderr: io.stderr,
166
441
  verbose: flags.verbose,
167
442
  verboseColor: flags.verboseColor,
443
+ env: io.envForRun,
168
444
  });
169
445
  const transcriptCacheStatus = extracted.diagnostics?.transcript?.cacheStatus;
170
446
  if (transcriptCacheStatus && transcriptCacheStatus !== 'unknown') {
171
- writeVerbose(io.stderr, flags.verbose, `cache ${transcriptCacheStatus} transcript`, flags.verboseColor);
447
+ writeVerbose(io.stderr, flags.verbose, `cache ${transcriptCacheStatus} transcript`, flags.verboseColor, io.envForRun);
172
448
  }
173
449
  if (flags.extractMode &&
174
450
  markdown.markdownRequested &&
@@ -180,15 +456,16 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
180
456
  }
181
457
  if (!isYoutubeUrl && extracted.isVideoOnly && extracted.video) {
182
458
  if (extracted.video.kind === 'youtube') {
183
- writeVerbose(io.stderr, flags.verbose, `video-only page detected; switching to YouTube URL ${extracted.video.url}`, flags.verboseColor);
459
+ writeVerbose(io.stderr, flags.verbose, `video-only page detected; switching to YouTube URL ${extracted.video.url}`, flags.verboseColor, io.envForRun);
184
460
  if (flags.progressEnabled) {
185
- spinner.setText('Video-only page: fetching YouTube transcript…');
461
+ spinner.setText(renderStatus('Video-only page', ': fetching YouTube transcript…'));
186
462
  }
187
463
  extracted = await fetchWithCache(extracted.video.url);
188
464
  extractionUi = deriveExtractionUi(extracted);
189
465
  updateSummaryProgress();
190
466
  }
191
467
  else if (extracted.video.kind === 'direct') {
468
+ const directVideoSlides = await runSlidesExtraction();
192
469
  const wantsVideoUnderstanding = flags.videoMode === 'understand' || flags.videoMode === 'auto';
193
470
  // Direct video URLs require a model that can consume video attachments (currently Gemini).
194
471
  const canVideoUnderstand = wantsVideoUnderstanding &&
@@ -199,7 +476,7 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
199
476
  if (canVideoUnderstand) {
200
477
  hooks.onExtracted?.(extracted);
201
478
  if (flags.progressEnabled)
202
- spinner.setText('Downloading video');
479
+ spinner.setText(renderStatus('Downloading video'));
203
480
  const loadedVideo = await loadRemoteAsset({
204
481
  url: extracted.video.url,
205
482
  fetchImpl: io.fetch,
@@ -208,7 +485,7 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
208
485
  assertAssetMediaTypeSupported({ attachment: loadedVideo.attachment, sizeLabel: null });
209
486
  let chosenModel = null;
210
487
  if (flags.progressEnabled)
211
- spinner.setText('Summarizing video');
488
+ spinner.setText(renderStatus('Summarizing video'));
212
489
  await hooks.summarizeAsset({
213
490
  sourceKind: 'asset-url',
214
491
  sourceLabel: loadedVideo.sourceLabel,
@@ -216,19 +493,35 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
216
493
  onModelChosen: (modelId) => {
217
494
  chosenModel = modelId;
218
495
  hooks.onModelChosen?.(modelId);
219
- if (flags.progressEnabled)
220
- spinner.setText(`Summarizing video (model: ${modelId})…`);
496
+ if (flags.progressEnabled) {
497
+ const meta = `${styleDim('(')}${styleDim('model: ')}${theme.accent(modelId)}${styleDim(')')}`;
498
+ spinner.setText(renderStatusWithMeta('Summarizing video', meta));
499
+ }
221
500
  },
222
501
  });
502
+ const slideCount = directVideoSlides ? directVideoSlides.slides.length : null;
223
503
  hooks.writeViaFooter([
224
504
  ...extractionUi.footerParts,
225
505
  ...(chosenModel ? [`model ${chosenModel}`] : []),
506
+ ...(slideCount != null ? [`slides ${slideCount}`] : []),
226
507
  ]);
227
508
  return;
228
509
  }
229
510
  }
230
511
  }
512
+ // Start slides in parallel; wait for real timing data before prompting.
513
+ if (flags.slides) {
514
+ void runSlidesExtraction().catch((error) => {
515
+ const message = error instanceof Error ? error.message : String(error);
516
+ ctx.hooks.onSlidesProgress?.(`Slides: failed (${message})`);
517
+ writeVerbose(io.stderr, flags.verbose, `slides failed: ${message}`, flags.verboseColor, io.envForRun);
518
+ });
519
+ }
231
520
  hooks.onExtracted?.(extracted);
521
+ let slidesForPrompt = null;
522
+ if (slidesTimelinePromise) {
523
+ slidesForPrompt = await slidesTimelinePromise;
524
+ }
232
525
  const prompt = buildUrlPrompt({
233
526
  extracted,
234
527
  outputLanguage: flags.outputLanguage,
@@ -236,6 +529,7 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
236
529
  promptOverride: flags.promptOverride ?? null,
237
530
  lengthInstruction: flags.lengthInstruction ?? null,
238
531
  languageInstruction: flags.languageInstruction ?? null,
532
+ slides: slidesForPrompt ?? slidesExtracted ?? null,
239
533
  });
240
534
  // Whisper transcription costs need to be folded into the finish line totals.
241
535
  const transcriptionCostUsd = estimateWhisperTranscriptionCostUsd({
@@ -251,7 +545,7 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
251
545
  let extractedForOutput = extracted;
252
546
  if (markdown.transcriptMarkdownRequested && markdown.convertTranscriptToMarkdown) {
253
547
  if (flags.progressEnabled) {
254
- spinner.setText('Converting transcript to markdown');
548
+ spinner.setText(renderStatus('Converting transcript to markdown'));
255
549
  }
256
550
  const markdownContent = await markdown.convertTranscriptToMarkdown({
257
551
  title: extracted.title,
@@ -283,6 +577,8 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
283
577
  prompt,
284
578
  effectiveMarkdownMode: markdown.effectiveMarkdownMode,
285
579
  transcriptionCostLabel,
580
+ slides: slidesExtracted ?? slidesForPrompt ?? null,
581
+ slidesOutput,
286
582
  });
287
583
  return;
288
584
  }
@@ -290,7 +586,7 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
290
586
  hooks.onModelChosen?.(modelId);
291
587
  if (!flags.progressEnabled)
292
588
  return;
293
- spinner.setText(`Summarizing (sent ${extractionUi.contentSizeLabel}${extractionUi.viaSourceLabel}, model: ${modelId})…`);
589
+ spinner.setText(formatSummaryProgress(modelId));
294
590
  };
295
591
  await summarizeExtractedUrl({
296
592
  ctx,
@@ -301,10 +597,16 @@ export async function runUrlFlow({ ctx, url, isYoutubeUrl, }) {
301
597
  effectiveMarkdownMode: markdown.effectiveMarkdownMode,
302
598
  transcriptionCostLabel,
303
599
  onModelChosen,
600
+ slides: slidesExtracted ?? slidesForPrompt ?? null,
601
+ slidesOutput,
304
602
  });
305
603
  }
306
604
  finally {
307
- hooks.clearProgressIfCurrent(clearProgressLine);
605
+ if (flags.progressEnabled) {
606
+ process.off('SIGINT', handleSigint);
607
+ process.off('SIGTERM', handleSigterm);
608
+ }
609
+ hooks.clearProgressIfCurrent(pauseProgressLine);
308
610
  stopProgress();
309
611
  }
310
612
  }