@steipete/summarize 0.7.1 → 0.8.1

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 (151) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +53 -2
  3. package/dist/cli.js +3 -0
  4. package/dist/esm/cache.js +353 -0
  5. package/dist/esm/cache.js.map +1 -0
  6. package/dist/esm/config.js +78 -1
  7. package/dist/esm/config.js.map +1 -1
  8. package/dist/esm/content/asset.js +11 -17
  9. package/dist/esm/content/asset.js.map +1 -1
  10. package/dist/esm/daemon/auto-mode.js +8 -0
  11. package/dist/esm/daemon/auto-mode.js.map +1 -0
  12. package/dist/esm/daemon/cli.js +284 -0
  13. package/dist/esm/daemon/cli.js.map +1 -0
  14. package/dist/esm/daemon/config.js +82 -0
  15. package/dist/esm/daemon/config.js.map +1 -0
  16. package/dist/esm/daemon/constants.js +8 -0
  17. package/dist/esm/daemon/constants.js.map +1 -0
  18. package/dist/esm/daemon/env-merge.js +4 -0
  19. package/dist/esm/daemon/env-merge.js.map +1 -0
  20. package/dist/esm/daemon/env-snapshot.js +43 -0
  21. package/dist/esm/daemon/env-snapshot.js.map +1 -0
  22. package/dist/esm/daemon/flow-context.js +265 -0
  23. package/dist/esm/daemon/flow-context.js.map +1 -0
  24. package/dist/esm/daemon/launchd.js +149 -0
  25. package/dist/esm/daemon/launchd.js.map +1 -0
  26. package/dist/esm/daemon/meta.js +35 -0
  27. package/dist/esm/daemon/meta.js.map +1 -0
  28. package/dist/esm/daemon/models.js +175 -0
  29. package/dist/esm/daemon/models.js.map +1 -0
  30. package/dist/esm/daemon/request-settings.js +91 -0
  31. package/dist/esm/daemon/request-settings.js.map +1 -0
  32. package/dist/esm/daemon/schtasks.js +108 -0
  33. package/dist/esm/daemon/schtasks.js.map +1 -0
  34. package/dist/esm/daemon/server.js +399 -0
  35. package/dist/esm/daemon/server.js.map +1 -0
  36. package/dist/esm/daemon/summarize-progress.js +57 -0
  37. package/dist/esm/daemon/summarize-progress.js.map +1 -0
  38. package/dist/esm/daemon/summarize.js +263 -0
  39. package/dist/esm/daemon/summarize.js.map +1 -0
  40. package/dist/esm/daemon/systemd.js +117 -0
  41. package/dist/esm/daemon/systemd.js.map +1 -0
  42. package/dist/esm/flags.js +3 -1
  43. package/dist/esm/flags.js.map +1 -1
  44. package/dist/esm/llm/generate-text.js +445 -154
  45. package/dist/esm/llm/generate-text.js.map +1 -1
  46. package/dist/esm/llm/html-to-markdown.js +4 -1
  47. package/dist/esm/llm/html-to-markdown.js.map +1 -1
  48. package/dist/esm/llm/prompt.js +14 -0
  49. package/dist/esm/llm/prompt.js.map +1 -0
  50. package/dist/esm/llm/transcript-to-markdown.js +57 -0
  51. package/dist/esm/llm/transcript-to-markdown.js.map +1 -0
  52. package/dist/esm/model-spec.js +2 -2
  53. package/dist/esm/model-spec.js.map +1 -1
  54. package/dist/esm/run/attachments.js +10 -42
  55. package/dist/esm/run/attachments.js.map +1 -1
  56. package/dist/esm/run/cache-state.js +48 -0
  57. package/dist/esm/run/cache-state.js.map +1 -0
  58. package/dist/esm/run/cli-preflight.js +15 -1
  59. package/dist/esm/run/cli-preflight.js.map +1 -1
  60. package/dist/esm/run/cookies/twitter.js +224 -0
  61. package/dist/esm/run/cookies/twitter.js.map +1 -0
  62. package/dist/esm/run/fetch-with-timeout.js +1 -1
  63. package/dist/esm/run/fetch-with-timeout.js.map +1 -1
  64. package/dist/esm/run/finish-line.js +46 -17
  65. package/dist/esm/run/finish-line.js.map +1 -1
  66. package/dist/esm/run/flows/asset/input.js +2 -4
  67. package/dist/esm/run/flows/asset/input.js.map +1 -1
  68. package/dist/esm/run/flows/asset/preprocess.js +52 -72
  69. package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
  70. package/dist/esm/run/flows/asset/summary.js +127 -47
  71. package/dist/esm/run/flows/asset/summary.js.map +1 -1
  72. package/dist/esm/run/flows/url/extract.js +6 -1
  73. package/dist/esm/run/flows/url/extract.js.map +1 -1
  74. package/dist/esm/run/flows/url/flow.js +166 -85
  75. package/dist/esm/run/flows/url/flow.js.map +1 -1
  76. package/dist/esm/run/flows/url/markdown.js +88 -46
  77. package/dist/esm/run/flows/url/markdown.js.map +1 -1
  78. package/dist/esm/run/flows/url/summary.js +263 -185
  79. package/dist/esm/run/flows/url/summary.js.map +1 -1
  80. package/dist/esm/run/help.js +33 -2
  81. package/dist/esm/run/help.js.map +1 -1
  82. package/dist/esm/run/run-env.js +36 -2
  83. package/dist/esm/run/run-env.js.map +1 -1
  84. package/dist/esm/run/runner.js +362 -227
  85. package/dist/esm/run/runner.js.map +1 -1
  86. package/dist/esm/run/summary-engine.js +21 -6
  87. package/dist/esm/run/summary-engine.js.map +1 -1
  88. package/dist/esm/run/summary-llm.js +4 -1
  89. package/dist/esm/run/summary-llm.js.map +1 -1
  90. package/dist/esm/tty/format.js +9 -0
  91. package/dist/esm/tty/format.js.map +1 -1
  92. package/dist/esm/version.js +1 -1
  93. package/dist/types/cache.d.ts +70 -0
  94. package/dist/types/config.d.ts +46 -0
  95. package/dist/types/content/asset.d.ts +4 -3
  96. package/dist/types/daemon/auto-mode.d.ts +8 -0
  97. package/dist/types/daemon/cli.d.ts +9 -0
  98. package/dist/types/daemon/config.d.ts +19 -0
  99. package/dist/types/daemon/constants.d.ts +7 -0
  100. package/dist/types/daemon/env-merge.d.ts +5 -0
  101. package/dist/types/daemon/env-snapshot.d.ts +4 -0
  102. package/dist/types/daemon/flow-context.d.ts +28 -0
  103. package/dist/types/daemon/launchd.d.ts +29 -0
  104. package/dist/types/daemon/meta.d.ts +12 -0
  105. package/dist/types/daemon/models.d.ts +27 -0
  106. package/dist/types/daemon/request-settings.d.ts +27 -0
  107. package/dist/types/daemon/schtasks.d.ts +16 -0
  108. package/dist/types/daemon/server.d.ts +12 -0
  109. package/dist/types/daemon/summarize-progress.d.ts +2 -0
  110. package/dist/types/daemon/summarize.d.ts +59 -0
  111. package/dist/types/daemon/systemd.d.ts +16 -0
  112. package/dist/types/flags.d.ts +1 -1
  113. package/dist/types/llm/generate-text.d.ts +11 -5
  114. package/dist/types/llm/html-to-markdown.d.ts +4 -1
  115. package/dist/types/llm/prompt.d.ts +9 -0
  116. package/dist/types/llm/transcript-to-markdown.d.ts +34 -0
  117. package/dist/types/run/attachments.d.ts +4 -10
  118. package/dist/types/run/cache-state.d.ts +12 -0
  119. package/dist/types/run/cli-preflight.d.ts +1 -0
  120. package/dist/types/run/cookies/twitter.d.ts +17 -0
  121. package/dist/types/run/finish-line.d.ts +31 -1
  122. package/dist/types/run/flows/asset/preprocess.d.ts +5 -2
  123. package/dist/types/run/flows/asset/summary.d.ts +11 -0
  124. package/dist/types/run/flows/url/markdown.d.ts +3 -0
  125. package/dist/types/run/flows/url/summary.d.ts +6 -3
  126. package/dist/types/run/flows/url/types.d.ts +52 -18
  127. package/dist/types/run/help.d.ts +1 -0
  128. package/dist/types/run/run-env.d.ts +6 -0
  129. package/dist/types/run/summary-engine.d.ts +8 -2
  130. package/dist/types/run/summary-llm.d.ts +6 -3
  131. package/dist/types/tty/format.d.ts +1 -0
  132. package/dist/types/version.d.ts +1 -1
  133. package/docs/README.md +5 -0
  134. package/docs/cache.md +72 -0
  135. package/docs/chrome-extension.md +180 -0
  136. package/docs/cli.md +6 -0
  137. package/docs/config.md +65 -1
  138. package/docs/extract-only.md +6 -0
  139. package/docs/firecrawl.md +6 -0
  140. package/docs/language.md +6 -0
  141. package/docs/llm.md +20 -0
  142. package/docs/manual-tests.md +6 -0
  143. package/docs/model-auto.md +6 -0
  144. package/docs/openai.md +6 -0
  145. package/docs/site/index.html +11 -1
  146. package/docs/smoketest.md +6 -0
  147. package/docs/website.md +6 -0
  148. package/docs/youtube.md +9 -2
  149. package/package.json +7 -10
  150. package/dist/cli.cjs +0 -80566
  151. package/dist/cli.cjs.map +0 -7
@@ -1,6 +1,8 @@
1
1
  import { countTokens } from 'gpt-tokenizer';
2
2
  import { render as renderMarkdownAnsi } from 'markdansi';
3
+ import { buildLanguageKey, buildLengthKey, buildPromptHash, buildSummaryCacheKey, hashString, normalizeContentForHash, } from '../../../cache.js';
3
4
  import { formatOutputLanguageForJson } from '../../../language.js';
5
+ import { parseGatewayStyleModelId } from '../../../llm/model-id.js';
4
6
  import { buildAutoModelAttempts } from '../../../model-auto.js';
5
7
  import { buildLinkSummaryPrompt } from '../../../prompts/index.js';
6
8
  import { parseCliUserModelId } from '../../env.js';
@@ -10,7 +12,7 @@ import { prepareMarkdownForTerminal } from '../../markdown.js';
10
12
  import { runModelAttempts } from '../../model-attempts.js';
11
13
  import { buildOpenRouterNoAllowedProvidersMessage } from '../../openrouter.js';
12
14
  import { isRichTty, markdownRenderWidth, supportsColor } from '../../terminal.js';
13
- export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, }) {
15
+ export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOverride, lengthInstruction, languageInstruction, }) {
14
16
  const isYouTube = extracted.siteName === 'YouTube';
15
17
  return buildLinkSummaryPrompt({
16
18
  url: extracted.url,
@@ -18,12 +20,15 @@ export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, }) {
18
20
  siteName: extracted.siteName,
19
21
  description: extracted.description,
20
22
  content: extracted.content,
21
- truncated: false,
23
+ truncated: extracted.truncated,
22
24
  hasTranscript: isYouTube ||
23
25
  (extracted.transcriptSource !== null && extracted.transcriptSource !== 'unavailable'),
24
26
  summaryLength: lengthArg.kind === 'preset' ? lengthArg.preset : { maxCharacters: lengthArg.maxCharacters },
25
27
  outputLanguage,
26
28
  shares: [],
29
+ promptOverride: promptOverride ?? null,
30
+ lengthInstruction: lengthInstruction ?? null,
31
+ languageInstruction: languageInstruction ?? null,
27
32
  });
28
33
  }
29
34
  const buildFinishExtras = ({ extracted, metricsDetailed, transcriptionCostLabel, }) => {
@@ -47,315 +52,386 @@ const pickModelForFinishLine = (llmCalls, fallback) => {
47
52
  (llmCalls.length > 0 ? (llmCalls[llmCalls.length - 1]?.model ?? null) : null) ??
48
53
  fallback);
49
54
  };
55
+ const buildModelMetaFromAttempt = (attempt) => {
56
+ if (attempt.transport === 'cli') {
57
+ return { provider: 'cli', canonical: attempt.userModelId };
58
+ }
59
+ const parsed = parseGatewayStyleModelId(attempt.llmModelId ?? attempt.userModelId);
60
+ const canonical = attempt.userModelId.toLowerCase().startsWith('openrouter/')
61
+ ? attempt.userModelId
62
+ : parsed.canonical;
63
+ return { provider: parsed.provider, canonical };
64
+ };
50
65
  export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, }) {
51
- ctx.clearProgressForStdout();
66
+ const { io, flags, model, hooks } = ctx;
67
+ hooks.clearProgressForStdout();
52
68
  const finishLabel = buildExtractFinishLabel({
53
69
  extracted: { diagnostics: extracted.diagnostics },
54
- format: ctx.format,
70
+ format: flags.format,
55
71
  markdownMode: effectiveMarkdownMode,
56
- hasMarkdownLlmCall: ctx.llmCalls.some((call) => call.purpose === 'markdown'),
72
+ hasMarkdownLlmCall: model.llmCalls.some((call) => call.purpose === 'markdown'),
57
73
  });
58
- const finishModel = pickModelForFinishLine(ctx.llmCalls, null);
59
- if (ctx.json) {
60
- const finishReport = ctx.shouldComputeReport ? await ctx.buildReport() : null;
74
+ const finishModel = pickModelForFinishLine(model.llmCalls, null);
75
+ if (flags.json) {
76
+ const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
61
77
  const payload = {
62
78
  input: {
63
79
  kind: 'url',
64
80
  url,
65
- timeoutMs: ctx.timeoutMs,
66
- youtube: ctx.youtubeMode,
67
- firecrawl: ctx.firecrawlMode,
68
- format: ctx.format,
81
+ timeoutMs: flags.timeoutMs,
82
+ youtube: flags.youtubeMode,
83
+ firecrawl: flags.firecrawlMode,
84
+ format: flags.format,
69
85
  markdown: effectiveMarkdownMode,
70
- length: ctx.lengthArg.kind === 'preset'
71
- ? { kind: 'preset', preset: ctx.lengthArg.preset }
72
- : { kind: 'chars', maxCharacters: ctx.lengthArg.maxCharacters },
73
- maxOutputTokens: ctx.maxOutputTokensArg,
74
- model: ctx.requestedModelLabel,
75
- language: formatOutputLanguageForJson(ctx.outputLanguage),
86
+ length: flags.lengthArg.kind === 'preset'
87
+ ? { kind: 'preset', preset: flags.lengthArg.preset }
88
+ : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
89
+ maxOutputTokens: flags.maxOutputTokensArg,
90
+ model: model.requestedModelLabel,
91
+ language: formatOutputLanguageForJson(flags.outputLanguage),
76
92
  },
77
93
  env: {
78
- hasXaiKey: Boolean(ctx.apiStatus.xaiApiKey),
79
- hasOpenAIKey: Boolean(ctx.apiStatus.apiKey),
80
- hasOpenRouterKey: Boolean(ctx.apiStatus.openrouterApiKey),
81
- hasApifyToken: Boolean(ctx.apiStatus.apifyToken),
82
- hasFirecrawlKey: ctx.apiStatus.firecrawlConfigured,
83
- hasGoogleKey: ctx.apiStatus.googleConfigured,
84
- hasAnthropicKey: ctx.apiStatus.anthropicConfigured,
94
+ hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
95
+ hasOpenAIKey: Boolean(model.apiStatus.apiKey),
96
+ hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
97
+ hasApifyToken: Boolean(model.apiStatus.apifyToken),
98
+ hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
99
+ hasGoogleKey: model.apiStatus.googleConfigured,
100
+ hasAnthropicKey: model.apiStatus.anthropicConfigured,
85
101
  },
86
102
  extracted,
87
103
  prompt,
88
104
  llm: null,
89
- metrics: ctx.metricsEnabled ? finishReport : null,
105
+ metrics: flags.metricsEnabled ? finishReport : null,
90
106
  summary: null,
91
107
  };
92
- ctx.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
93
- if (ctx.metricsEnabled && finishReport) {
94
- const costUsd = await ctx.estimateCostUsd();
108
+ io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
109
+ if (flags.metricsEnabled && finishReport) {
110
+ const costUsd = await hooks.estimateCostUsd();
95
111
  writeFinishLine({
96
- stderr: ctx.stderr,
97
- elapsedMs: Date.now() - ctx.runStartedAtMs,
112
+ stderr: io.stderr,
113
+ elapsedMs: Date.now() - flags.runStartedAtMs,
98
114
  label: finishLabel,
99
115
  model: finishModel,
100
116
  report: finishReport,
101
117
  costUsd,
102
- detailed: ctx.metricsDetailed,
118
+ detailed: flags.metricsDetailed,
103
119
  extraParts: buildFinishExtras({
104
120
  extracted,
105
- metricsDetailed: ctx.metricsDetailed,
121
+ metricsDetailed: flags.metricsDetailed,
106
122
  transcriptionCostLabel,
107
123
  }),
108
- color: ctx.verboseColor,
124
+ color: flags.verboseColor,
109
125
  });
110
126
  }
111
127
  return;
112
128
  }
113
- const renderedExtract = ctx.format === 'markdown' && !ctx.plain && isRichTty(ctx.stdout)
129
+ const renderedExtract = flags.format === 'markdown' && !flags.plain && isRichTty(io.stdout)
114
130
  ? renderMarkdownAnsi(prepareMarkdownForTerminal(extracted.content), {
115
- width: markdownRenderWidth(ctx.stdout, ctx.env),
131
+ width: markdownRenderWidth(io.stdout, io.env),
116
132
  wrap: true,
117
- color: supportsColor(ctx.stdout, ctx.envForRun),
133
+ color: supportsColor(io.stdout, io.envForRun),
118
134
  hyperlinks: true,
119
135
  })
120
136
  : extracted.content;
121
- if (ctx.format === 'markdown' && !ctx.plain && isRichTty(ctx.stdout)) {
122
- ctx.stdout.write(`\n${renderedExtract.replace(/^\n+/, '')}`);
137
+ if (flags.format === 'markdown' && !flags.plain && isRichTty(io.stdout)) {
138
+ io.stdout.write(`\n${renderedExtract.replace(/^\n+/, '')}`);
123
139
  }
124
140
  else {
125
- ctx.stdout.write(renderedExtract);
141
+ io.stdout.write(renderedExtract);
126
142
  }
127
143
  if (!renderedExtract.endsWith('\n')) {
128
- ctx.stdout.write('\n');
144
+ io.stdout.write('\n');
129
145
  }
130
- ctx.writeViaFooter(extractionUi.footerParts);
131
- const report = ctx.shouldComputeReport ? await ctx.buildReport() : null;
132
- if (ctx.metricsEnabled && report) {
133
- const costUsd = await ctx.estimateCostUsd();
146
+ hooks.writeViaFooter(extractionUi.footerParts);
147
+ const report = flags.shouldComputeReport ? await hooks.buildReport() : null;
148
+ if (flags.metricsEnabled && report) {
149
+ const costUsd = await hooks.estimateCostUsd();
134
150
  writeFinishLine({
135
- stderr: ctx.stderr,
136
- elapsedMs: Date.now() - ctx.runStartedAtMs,
151
+ stderr: io.stderr,
152
+ elapsedMs: Date.now() - flags.runStartedAtMs,
137
153
  label: finishLabel,
138
154
  model: finishModel,
139
155
  report,
140
156
  costUsd,
141
- detailed: ctx.metricsDetailed,
157
+ detailed: flags.metricsDetailed,
142
158
  extraParts: buildFinishExtras({
143
159
  extracted,
144
- metricsDetailed: ctx.metricsDetailed,
160
+ metricsDetailed: flags.metricsDetailed,
145
161
  transcriptionCostLabel,
146
162
  }),
147
- color: ctx.verboseColor,
163
+ color: flags.verboseColor,
148
164
  });
149
165
  }
150
166
  }
151
167
  export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, onModelChosen, }) {
168
+ const { io, flags, model, cache: cacheState, hooks } = ctx;
152
169
  const promptTokens = countTokens(prompt);
153
170
  const kindForAuto = extracted.siteName === 'YouTube' ? 'youtube' : 'website';
154
171
  const attempts = await (async () => {
155
- if (ctx.isFallbackModel) {
156
- const catalog = await ctx.getLiteLlmCatalog();
172
+ if (model.isFallbackModel) {
173
+ const catalog = await model.getLiteLlmCatalog();
157
174
  const list = buildAutoModelAttempts({
158
175
  kind: kindForAuto,
159
176
  promptTokens,
160
- desiredOutputTokens: ctx.desiredOutputTokens,
177
+ desiredOutputTokens: model.desiredOutputTokens,
161
178
  requiresVideoUnderstanding: false,
162
- env: ctx.envForAuto,
163
- config: ctx.configForModelSelection,
179
+ env: model.envForAuto,
180
+ config: model.configForModelSelection,
164
181
  catalog,
165
182
  openrouterProvidersFromEnv: null,
166
- cliAvailability: ctx.cliAvailability,
183
+ cliAvailability: model.cliAvailability,
167
184
  });
168
- if (ctx.verbose) {
185
+ if (flags.verbose) {
169
186
  for (const attempt of list.slice(0, 8)) {
170
- writeVerbose(ctx.stderr, ctx.verbose, `auto candidate ${attempt.debug}`, ctx.verboseColor);
187
+ writeVerbose(io.stderr, flags.verbose, `auto candidate ${attempt.debug}`, flags.verboseColor);
171
188
  }
172
189
  }
173
190
  return list.map((attempt) => {
174
191
  if (attempt.transport !== 'cli')
175
- return ctx.summaryEngine.applyZaiOverrides(attempt);
192
+ return model.summaryEngine.applyZaiOverrides(attempt);
176
193
  const parsed = parseCliUserModelId(attempt.userModelId);
177
194
  return { ...attempt, cliProvider: parsed.provider, cliModel: parsed.model };
178
195
  });
179
196
  }
180
197
  /* v8 ignore next */
181
- if (!ctx.fixedModelSpec) {
198
+ if (!model.fixedModelSpec) {
182
199
  throw new Error('Internal error: missing fixed model spec');
183
200
  }
184
- if (ctx.fixedModelSpec.transport === 'cli') {
201
+ if (model.fixedModelSpec.transport === 'cli') {
185
202
  return [
186
203
  {
187
204
  transport: 'cli',
188
- userModelId: ctx.fixedModelSpec.userModelId,
205
+ userModelId: model.fixedModelSpec.userModelId,
189
206
  llmModelId: null,
190
- cliProvider: ctx.fixedModelSpec.cliProvider,
191
- cliModel: ctx.fixedModelSpec.cliModel,
207
+ cliProvider: model.fixedModelSpec.cliProvider,
208
+ cliModel: model.fixedModelSpec.cliModel,
192
209
  openrouterProviders: null,
193
210
  forceOpenRouter: false,
194
- requiredEnv: ctx.fixedModelSpec.requiredEnv,
211
+ requiredEnv: model.fixedModelSpec.requiredEnv,
195
212
  },
196
213
  ];
197
214
  }
198
- const openaiOverrides = ctx.fixedModelSpec.requiredEnv === 'Z_AI_API_KEY'
215
+ const openaiOverrides = model.fixedModelSpec.requiredEnv === 'Z_AI_API_KEY'
199
216
  ? {
200
- openaiApiKeyOverride: ctx.apiStatus.zaiApiKey,
201
- openaiBaseUrlOverride: ctx.apiStatus.zaiBaseUrl,
217
+ openaiApiKeyOverride: model.apiStatus.zaiApiKey,
218
+ openaiBaseUrlOverride: model.apiStatus.zaiBaseUrl,
202
219
  forceChatCompletions: true,
203
220
  }
204
221
  : {};
205
222
  return [
206
223
  {
207
- transport: ctx.fixedModelSpec.transport === 'openrouter' ? 'openrouter' : 'native',
208
- userModelId: ctx.fixedModelSpec.userModelId,
209
- llmModelId: ctx.fixedModelSpec.llmModelId,
210
- openrouterProviders: ctx.fixedModelSpec.openrouterProviders,
211
- forceOpenRouter: ctx.fixedModelSpec.forceOpenRouter,
212
- requiredEnv: ctx.fixedModelSpec.requiredEnv,
224
+ transport: model.fixedModelSpec.transport === 'openrouter' ? 'openrouter' : 'native',
225
+ userModelId: model.fixedModelSpec.userModelId,
226
+ llmModelId: model.fixedModelSpec.llmModelId,
227
+ openrouterProviders: model.fixedModelSpec.openrouterProviders,
228
+ forceOpenRouter: model.fixedModelSpec.forceOpenRouter,
229
+ requiredEnv: model.fixedModelSpec.requiredEnv,
213
230
  ...openaiOverrides,
214
231
  },
215
232
  ];
216
233
  })();
217
- const attemptOutcome = await runModelAttempts({
218
- attempts,
219
- isFallbackModel: ctx.isFallbackModel,
220
- isNamedModelSelection: ctx.isNamedModelSelection,
221
- envHasKeyFor: ctx.summaryEngine.envHasKeyFor,
222
- formatMissingModelError: ctx.summaryEngine.formatMissingModelError,
223
- onAutoSkip: (attempt) => {
224
- writeVerbose(ctx.stderr, ctx.verbose, `auto skip ${attempt.userModelId}: missing ${attempt.requiredEnv}`, ctx.verboseColor);
225
- },
226
- onAutoFailure: (attempt, error) => {
227
- writeVerbose(ctx.stderr, ctx.verbose, `auto failed ${attempt.userModelId}: ${error instanceof Error ? error.message : String(error)}`, ctx.verboseColor);
228
- },
229
- onFixedModelError: (_attempt, error) => {
230
- throw error;
231
- },
232
- runAttempt: (attempt) => ctx.summaryEngine.runSummaryAttempt({
233
- attempt,
234
- prompt,
235
- allowStreaming: ctx.streamingEnabled,
236
- onModelChosen: onModelChosen ?? null,
237
- }),
238
- });
239
- const summaryResult = attemptOutcome.result;
240
- const usedAttempt = attemptOutcome.usedAttempt;
241
- const { lastError, missingRequiredEnvs, sawOpenRouterNoAllowedProviders } = attemptOutcome;
234
+ const cacheStore = cacheState.mode === 'default' ? cacheState.store : null;
235
+ const contentHash = cacheStore ? hashString(normalizeContentForHash(extracted.content)) : null;
236
+ const promptHash = cacheStore ? buildPromptHash(prompt) : null;
237
+ const lengthKey = buildLengthKey(flags.lengthArg);
238
+ const languageKey = buildLanguageKey(flags.outputLanguage);
239
+ let summaryResult = null;
240
+ let usedAttempt = null;
241
+ let summaryFromCache = false;
242
+ let cacheChecked = false;
243
+ if (cacheStore && contentHash && promptHash) {
244
+ cacheChecked = true;
245
+ for (const attempt of attempts) {
246
+ if (!model.summaryEngine.envHasKeyFor(attempt.requiredEnv))
247
+ continue;
248
+ const key = buildSummaryCacheKey({
249
+ contentHash,
250
+ promptHash,
251
+ model: attempt.userModelId,
252
+ lengthKey,
253
+ languageKey,
254
+ });
255
+ const cached = cacheStore.getText('summary', key);
256
+ if (!cached)
257
+ continue;
258
+ writeVerbose(io.stderr, flags.verbose, 'cache hit summary', flags.verboseColor);
259
+ onModelChosen?.(attempt.userModelId);
260
+ summaryResult = {
261
+ summary: cached,
262
+ summaryAlreadyPrinted: false,
263
+ modelMeta: buildModelMetaFromAttempt(attempt),
264
+ maxOutputTokensForCall: null,
265
+ };
266
+ usedAttempt = attempt;
267
+ summaryFromCache = true;
268
+ break;
269
+ }
270
+ }
271
+ if (cacheChecked && !summaryFromCache) {
272
+ writeVerbose(io.stderr, flags.verbose, 'cache miss summary', flags.verboseColor);
273
+ }
274
+ ctx.hooks.onSummaryCached?.(summaryFromCache);
275
+ let lastError = null;
276
+ let missingRequiredEnvs = new Set();
277
+ let sawOpenRouterNoAllowedProviders = false;
278
+ if (!summaryResult || !usedAttempt) {
279
+ const attemptOutcome = await runModelAttempts({
280
+ attempts,
281
+ isFallbackModel: model.isFallbackModel,
282
+ isNamedModelSelection: model.isNamedModelSelection,
283
+ envHasKeyFor: model.summaryEngine.envHasKeyFor,
284
+ formatMissingModelError: model.summaryEngine.formatMissingModelError,
285
+ onAutoSkip: (attempt) => {
286
+ writeVerbose(io.stderr, flags.verbose, `auto skip ${attempt.userModelId}: missing ${attempt.requiredEnv}`, flags.verboseColor);
287
+ },
288
+ onAutoFailure: (attempt, error) => {
289
+ writeVerbose(io.stderr, flags.verbose, `auto failed ${attempt.userModelId}: ${error instanceof Error ? error.message : String(error)}`, flags.verboseColor);
290
+ },
291
+ onFixedModelError: (_attempt, error) => {
292
+ throw error;
293
+ },
294
+ runAttempt: (attempt) => model.summaryEngine.runSummaryAttempt({
295
+ attempt,
296
+ prompt,
297
+ allowStreaming: flags.streamingEnabled,
298
+ onModelChosen: onModelChosen ?? null,
299
+ }),
300
+ });
301
+ summaryResult = attemptOutcome.result;
302
+ usedAttempt = attemptOutcome.usedAttempt;
303
+ lastError = attemptOutcome.lastError;
304
+ missingRequiredEnvs = attemptOutcome.missingRequiredEnvs;
305
+ sawOpenRouterNoAllowedProviders = attemptOutcome.sawOpenRouterNoAllowedProviders;
306
+ }
242
307
  if (!summaryResult || !usedAttempt) {
243
308
  // Auto mode: surface raw extracted content when no model can run.
244
309
  const withFreeTip = (message) => {
245
- if (!ctx.isNamedModelSelection || !ctx.wantsFreeNamedModel)
310
+ if (!model.isNamedModelSelection || !model.wantsFreeNamedModel)
246
311
  return message;
247
312
  return (`${message}\n` +
248
313
  `Tip: run "summarize refresh-free" to refresh the free model candidates (writes ~/.summarize/config.json).`);
249
314
  };
250
- if (ctx.isNamedModelSelection) {
315
+ if (model.isNamedModelSelection) {
251
316
  if (lastError === null && missingRequiredEnvs.size > 0) {
252
- throw new Error(withFreeTip(`Missing ${Array.from(missingRequiredEnvs).sort().join(', ')} for --model ${ctx.requestedModelInput}.`));
317
+ throw new Error(withFreeTip(`Missing ${Array.from(missingRequiredEnvs).sort().join(', ')} for --model ${model.requestedModelInput}.`));
253
318
  }
254
319
  if (lastError instanceof Error) {
255
320
  if (sawOpenRouterNoAllowedProviders) {
256
321
  const message = await buildOpenRouterNoAllowedProvidersMessage({
257
322
  attempts,
258
- fetchImpl: ctx.trackedFetch,
259
- timeoutMs: ctx.timeoutMs,
323
+ fetchImpl: io.fetch,
324
+ timeoutMs: flags.timeoutMs,
260
325
  });
261
326
  throw new Error(withFreeTip(message), { cause: lastError });
262
327
  }
263
328
  throw new Error(withFreeTip(lastError.message), { cause: lastError });
264
329
  }
265
- throw new Error(withFreeTip(`No model available for --model ${ctx.requestedModelInput}`));
330
+ throw new Error(withFreeTip(`No model available for --model ${model.requestedModelInput}`));
266
331
  }
267
- ctx.clearProgressForStdout();
268
- if (ctx.json) {
269
- const finishReport = ctx.shouldComputeReport ? await ctx.buildReport() : null;
270
- const finishModel = pickModelForFinishLine(ctx.llmCalls, null);
332
+ hooks.clearProgressForStdout();
333
+ if (flags.json) {
334
+ const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
335
+ const finishModel = pickModelForFinishLine(model.llmCalls, null);
271
336
  const payload = {
272
337
  input: {
273
338
  kind: 'url',
274
339
  url,
275
- timeoutMs: ctx.timeoutMs,
276
- youtube: ctx.youtubeMode,
277
- firecrawl: ctx.firecrawlMode,
278
- format: ctx.format,
340
+ timeoutMs: flags.timeoutMs,
341
+ youtube: flags.youtubeMode,
342
+ firecrawl: flags.firecrawlMode,
343
+ format: flags.format,
279
344
  markdown: effectiveMarkdownMode,
280
- length: ctx.lengthArg.kind === 'preset'
281
- ? { kind: 'preset', preset: ctx.lengthArg.preset }
282
- : { kind: 'chars', maxCharacters: ctx.lengthArg.maxCharacters },
283
- maxOutputTokens: ctx.maxOutputTokensArg,
284
- model: ctx.requestedModelLabel,
285
- language: formatOutputLanguageForJson(ctx.outputLanguage),
345
+ length: flags.lengthArg.kind === 'preset'
346
+ ? { kind: 'preset', preset: flags.lengthArg.preset }
347
+ : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
348
+ maxOutputTokens: flags.maxOutputTokensArg,
349
+ model: model.requestedModelLabel,
350
+ language: formatOutputLanguageForJson(flags.outputLanguage),
286
351
  },
287
352
  env: {
288
- hasXaiKey: Boolean(ctx.apiStatus.xaiApiKey),
289
- hasOpenAIKey: Boolean(ctx.apiStatus.apiKey),
290
- hasOpenRouterKey: Boolean(ctx.apiStatus.openrouterApiKey),
291
- hasApifyToken: Boolean(ctx.apiStatus.apifyToken),
292
- hasFirecrawlKey: ctx.apiStatus.firecrawlConfigured,
293
- hasGoogleKey: ctx.apiStatus.googleConfigured,
294
- hasAnthropicKey: ctx.apiStatus.anthropicConfigured,
353
+ hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
354
+ hasOpenAIKey: Boolean(model.apiStatus.apiKey),
355
+ hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
356
+ hasApifyToken: Boolean(model.apiStatus.apifyToken),
357
+ hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
358
+ hasGoogleKey: model.apiStatus.googleConfigured,
359
+ hasAnthropicKey: model.apiStatus.anthropicConfigured,
295
360
  },
296
361
  extracted,
297
362
  prompt,
298
363
  llm: null,
299
- metrics: ctx.metricsEnabled ? finishReport : null,
364
+ metrics: flags.metricsEnabled ? finishReport : null,
300
365
  summary: extracted.content,
301
366
  };
302
- ctx.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
303
- if (ctx.metricsEnabled && finishReport) {
304
- const costUsd = await ctx.estimateCostUsd();
367
+ io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
368
+ if (flags.metricsEnabled && finishReport) {
369
+ const costUsd = await hooks.estimateCostUsd();
305
370
  writeFinishLine({
306
- stderr: ctx.stderr,
307
- elapsedMs: Date.now() - ctx.runStartedAtMs,
371
+ stderr: io.stderr,
372
+ elapsedMs: Date.now() - flags.runStartedAtMs,
308
373
  label: extractionUi.finishSourceLabel,
309
374
  model: finishModel,
310
375
  report: finishReport,
311
376
  costUsd,
312
- detailed: ctx.metricsDetailed,
377
+ detailed: flags.metricsDetailed,
313
378
  extraParts: buildFinishExtras({
314
379
  extracted,
315
- metricsDetailed: ctx.metricsDetailed,
380
+ metricsDetailed: flags.metricsDetailed,
316
381
  transcriptionCostLabel,
317
382
  }),
318
- color: ctx.verboseColor,
383
+ color: flags.verboseColor,
319
384
  });
320
385
  }
321
386
  return;
322
387
  }
323
- ctx.stdout.write(`${extracted.content}\n`);
388
+ io.stdout.write(`${extracted.content}\n`);
324
389
  if (extractionUi.footerParts.length > 0) {
325
- ctx.writeViaFooter([...extractionUi.footerParts, 'no model']);
390
+ hooks.writeViaFooter([...extractionUi.footerParts, 'no model']);
326
391
  }
327
- if (lastError instanceof Error && ctx.verbose) {
328
- writeVerbose(ctx.stderr, ctx.verbose, `auto failed all models: ${lastError.message}`, ctx.verboseColor);
392
+ if (lastError instanceof Error && flags.verbose) {
393
+ writeVerbose(io.stderr, flags.verbose, `auto failed all models: ${lastError.message}`, flags.verboseColor);
329
394
  }
330
395
  return;
331
396
  }
397
+ if (!summaryFromCache && cacheStore && contentHash && promptHash) {
398
+ const key = buildSummaryCacheKey({
399
+ contentHash,
400
+ promptHash,
401
+ model: usedAttempt.userModelId,
402
+ lengthKey,
403
+ languageKey,
404
+ });
405
+ cacheStore.setText('summary', key, summaryResult.summary, cacheState.ttlMs);
406
+ writeVerbose(io.stderr, flags.verbose, 'cache write summary', flags.verboseColor);
407
+ }
332
408
  const { summary, summaryAlreadyPrinted, modelMeta, maxOutputTokensForCall } = summaryResult;
333
- if (ctx.json) {
334
- const finishReport = ctx.shouldComputeReport ? await ctx.buildReport() : null;
409
+ if (flags.json) {
410
+ const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
335
411
  const payload = {
336
412
  input: {
337
413
  kind: 'url',
338
414
  url,
339
- timeoutMs: ctx.timeoutMs,
340
- youtube: ctx.youtubeMode,
341
- firecrawl: ctx.firecrawlMode,
342
- format: ctx.format,
415
+ timeoutMs: flags.timeoutMs,
416
+ youtube: flags.youtubeMode,
417
+ firecrawl: flags.firecrawlMode,
418
+ format: flags.format,
343
419
  markdown: effectiveMarkdownMode,
344
- length: ctx.lengthArg.kind === 'preset'
345
- ? { kind: 'preset', preset: ctx.lengthArg.preset }
346
- : { kind: 'chars', maxCharacters: ctx.lengthArg.maxCharacters },
347
- maxOutputTokens: ctx.maxOutputTokensArg,
348
- model: ctx.requestedModelLabel,
349
- language: formatOutputLanguageForJson(ctx.outputLanguage),
420
+ length: flags.lengthArg.kind === 'preset'
421
+ ? { kind: 'preset', preset: flags.lengthArg.preset }
422
+ : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
423
+ maxOutputTokens: flags.maxOutputTokensArg,
424
+ model: model.requestedModelLabel,
425
+ language: formatOutputLanguageForJson(flags.outputLanguage),
350
426
  },
351
427
  env: {
352
- hasXaiKey: Boolean(ctx.apiStatus.xaiApiKey),
353
- hasOpenAIKey: Boolean(ctx.apiStatus.apiKey),
354
- hasOpenRouterKey: Boolean(ctx.apiStatus.openrouterApiKey),
355
- hasApifyToken: Boolean(ctx.apiStatus.apifyToken),
356
- hasFirecrawlKey: ctx.apiStatus.firecrawlConfigured,
357
- hasGoogleKey: ctx.apiStatus.googleConfigured,
358
- hasAnthropicKey: ctx.apiStatus.anthropicConfigured,
428
+ hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
429
+ hasOpenAIKey: Boolean(model.apiStatus.apiKey),
430
+ hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
431
+ hasApifyToken: Boolean(model.apiStatus.apifyToken),
432
+ hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
433
+ hasGoogleKey: model.apiStatus.googleConfigured,
434
+ hasAnthropicKey: model.apiStatus.anthropicConfigured,
359
435
  },
360
436
  extracted,
361
437
  prompt,
@@ -365,69 +441,71 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
365
441
  maxCompletionTokens: maxOutputTokensForCall,
366
442
  strategy: 'single',
367
443
  },
368
- metrics: ctx.metricsEnabled ? finishReport : null,
444
+ metrics: flags.metricsEnabled ? finishReport : null,
369
445
  summary,
370
446
  };
371
- ctx.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
372
- if (ctx.metricsEnabled && finishReport) {
373
- const costUsd = await ctx.estimateCostUsd();
447
+ io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
448
+ if (flags.metricsEnabled && finishReport) {
449
+ const costUsd = await hooks.estimateCostUsd();
374
450
  writeFinishLine({
375
- stderr: ctx.stderr,
376
- elapsedMs: Date.now() - ctx.runStartedAtMs,
451
+ stderr: io.stderr,
452
+ elapsedMs: Date.now() - flags.runStartedAtMs,
453
+ elapsedLabel: summaryFromCache ? 'Cached' : null,
377
454
  label: extractionUi.finishSourceLabel,
378
455
  model: usedAttempt.userModelId,
379
456
  report: finishReport,
380
457
  costUsd,
381
- detailed: ctx.metricsDetailed,
458
+ detailed: flags.metricsDetailed,
382
459
  extraParts: buildFinishExtras({
383
460
  extracted,
384
- metricsDetailed: ctx.metricsDetailed,
461
+ metricsDetailed: flags.metricsDetailed,
385
462
  transcriptionCostLabel,
386
463
  }),
387
- color: ctx.verboseColor,
464
+ color: flags.verboseColor,
388
465
  });
389
466
  }
390
467
  return;
391
468
  }
392
469
  if (!summaryAlreadyPrinted) {
393
- ctx.clearProgressForStdout();
394
- const rendered = !ctx.plain && isRichTty(ctx.stdout)
470
+ hooks.clearProgressForStdout();
471
+ const rendered = !flags.plain && isRichTty(io.stdout)
395
472
  ? renderMarkdownAnsi(prepareMarkdownForTerminal(summary), {
396
- width: markdownRenderWidth(ctx.stdout, ctx.env),
473
+ width: markdownRenderWidth(io.stdout, io.env),
397
474
  wrap: true,
398
- color: supportsColor(ctx.stdout, ctx.envForRun),
475
+ color: supportsColor(io.stdout, io.envForRun),
399
476
  hyperlinks: true,
400
477
  })
401
478
  : summary;
402
- if (!ctx.plain && isRichTty(ctx.stdout)) {
403
- ctx.stdout.write(`\n${rendered.replace(/^\n+/, '')}`);
479
+ if (!flags.plain && isRichTty(io.stdout)) {
480
+ io.stdout.write(`\n${rendered.replace(/^\n+/, '')}`);
404
481
  }
405
482
  else {
406
- if (isRichTty(ctx.stdout))
407
- ctx.stdout.write('\n');
408
- ctx.stdout.write(rendered.replace(/^\n+/, ''));
483
+ if (isRichTty(io.stdout))
484
+ io.stdout.write('\n');
485
+ io.stdout.write(rendered.replace(/^\n+/, ''));
409
486
  }
410
487
  if (!rendered.endsWith('\n')) {
411
- ctx.stdout.write('\n');
488
+ io.stdout.write('\n');
412
489
  }
413
490
  }
414
- const report = ctx.shouldComputeReport ? await ctx.buildReport() : null;
415
- if (ctx.metricsEnabled && report) {
416
- const costUsd = await ctx.estimateCostUsd();
491
+ const report = flags.shouldComputeReport ? await hooks.buildReport() : null;
492
+ if (flags.metricsEnabled && report) {
493
+ const costUsd = await hooks.estimateCostUsd();
417
494
  writeFinishLine({
418
- stderr: ctx.stderr,
419
- elapsedMs: Date.now() - ctx.runStartedAtMs,
495
+ stderr: io.stderr,
496
+ elapsedMs: Date.now() - flags.runStartedAtMs,
497
+ elapsedLabel: summaryFromCache ? 'Cached' : null,
420
498
  label: extractionUi.finishSourceLabel,
421
499
  model: modelMeta.canonical,
422
500
  report,
423
501
  costUsd,
424
- detailed: ctx.metricsDetailed,
502
+ detailed: flags.metricsDetailed,
425
503
  extraParts: buildFinishExtras({
426
504
  extracted,
427
- metricsDetailed: ctx.metricsDetailed,
505
+ metricsDetailed: flags.metricsDetailed,
428
506
  transcriptionCostLabel,
429
507
  }),
430
- color: ctx.verboseColor,
508
+ color: flags.verboseColor,
431
509
  });
432
510
  }
433
511
  }