opencode-dux 1.0.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,832 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import {
4
+ type PluginInput,
5
+ type ToolDefinition,
6
+ tool,
7
+ } from '@opencode-ai/plugin';
8
+ import { buildBinaryResultMessage, saveBinary } from './binary';
9
+ import {
10
+ buildCacheKey,
11
+ CACHE,
12
+ cacheFetchResult,
13
+ isInvalidLlmsResult,
14
+ } from './cache';
15
+ import {
16
+ DEFAULT_TIMEOUT_SECONDS,
17
+ MAX_BINARY_DOWNLOAD_BYTES,
18
+ MAX_LLMS_PROBE_TIMEOUT_MS,
19
+ MAX_RESPONSE_BYTES,
20
+ MAX_TIMEOUT_SECONDS,
21
+ WEBFETCH_DESCRIPTION,
22
+ } from './constants';
23
+ import {
24
+ buildAllowedOrigins,
25
+ buildConditionalHeaders,
26
+ buildPermissionPatterns,
27
+ decodeBody,
28
+ extractHeaderMetadata,
29
+ fetchWithUpgradeFallback,
30
+ getBinaryKind,
31
+ isBinaryContentType,
32
+ isDocsLikeUrl,
33
+ isGenericBinaryMime,
34
+ isHtmlLikeContentType,
35
+ looksLikeHtmlText,
36
+ looksLikeTextBody,
37
+ normalizeUrl,
38
+ probeLlmsText,
39
+ readBodyLimited,
40
+ runWithScopedTimeout,
41
+ } from './network';
42
+ import {
43
+ decideSecondaryModelUse,
44
+ readSecondaryModelFromConfig,
45
+ runSecondaryModelWithFallback,
46
+ } from './secondary-model';
47
+ import type { RedirectStep, SmartfetchOptions } from './types';
48
+ import {
49
+ buildLlmsRequiredMessage,
50
+ buildRedirectResultMessage,
51
+ cleanFetchedText,
52
+ detectQualitySignals,
53
+ escapeHtml,
54
+ extractFromHtml,
55
+ extractHeadingsFromMarkdown,
56
+ frontmatter,
57
+ inferCanonicalUrlFromText,
58
+ joinRenderedContent,
59
+ pickContent,
60
+ renderMessageForFormat,
61
+ trimBlankRuns,
62
+ withTruncationMarker,
63
+ wordCount,
64
+ } from './utils';
65
+
66
+ const z = tool.schema;
67
+
68
+ export function createWebfetchTool(
69
+ pluginCtx: PluginInput,
70
+ options: SmartfetchOptions = {},
71
+ ): ToolDefinition {
72
+ const binaryDir =
73
+ options.binaryDir || path.join(os.tmpdir(), 'opencode-smartfetch');
74
+
75
+ return tool({
76
+ description: WEBFETCH_DESCRIPTION,
77
+ args: {
78
+ url: z.httpUrl(),
79
+ format: z.enum(['text', 'markdown', 'html']).default('markdown'),
80
+ timeout: z
81
+ .number()
82
+ .positive()
83
+ .max(MAX_TIMEOUT_SECONDS)
84
+ .optional()
85
+ .describe('Timeout in seconds, max 120.'),
86
+ prompt: z
87
+ .string()
88
+ .optional()
89
+ .describe(
90
+ 'Optional extraction task to run on the fetched content using a cheap secondary model.',
91
+ ),
92
+ extract_main: z.boolean().default(true),
93
+ prefer_llms_txt: z.enum(['auto', 'always', 'never']).default('auto'),
94
+ include_metadata: z.boolean().default(true),
95
+ save_binary: z
96
+ .boolean()
97
+ .default(false)
98
+ .describe(
99
+ 'Save binary payload to disk when it fits within the active download limit.',
100
+ ),
101
+ },
102
+ async execute(args, ctx) {
103
+ const secondaryModels = await readSecondaryModelFromConfig(
104
+ ctx.directory || pluginCtx.directory,
105
+ );
106
+ const normalized = normalizeUrl(args.url);
107
+ const url = new URL(normalized.url);
108
+ const cacheKey = buildCacheKey(
109
+ args.url,
110
+ args.extract_main,
111
+ args.prefer_llms_txt,
112
+ args.save_binary,
113
+ );
114
+ const shouldProbeLlmsTxt =
115
+ args.prefer_llms_txt === 'always' ||
116
+ (args.prefer_llms_txt === 'auto' && isDocsLikeUrl(url));
117
+ const permissionPatterns = buildPermissionPatterns(
118
+ normalized,
119
+ shouldProbeLlmsTxt,
120
+ );
121
+ const allowedOrigins = buildAllowedOrigins(permissionPatterns);
122
+
123
+ await ctx.ask({
124
+ permission: 'webfetch',
125
+ patterns: permissionPatterns,
126
+ always: permissionPatterns,
127
+ metadata: {
128
+ url: normalized.url,
129
+ requested_url: args.url,
130
+ fallback_url: normalized.fallbackUrl,
131
+ llms_probe_enabled: shouldProbeLlmsTxt,
132
+ format: args.format,
133
+ prompt: args.prompt,
134
+ },
135
+ });
136
+
137
+ const timeoutMs = Math.min(
138
+ (args.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
139
+ MAX_TIMEOUT_SECONDS * 1000,
140
+ );
141
+ const controller = new AbortController();
142
+ const timeout = setTimeout(
143
+ () => controller.abort(new Error(`timeout after ${timeoutMs}ms`)),
144
+ timeoutMs,
145
+ );
146
+ const abortHandler = () => controller.abort(ctx.abort.reason);
147
+ ctx.abort.addEventListener('abort', abortHandler, { once: true });
148
+
149
+ try {
150
+ let fetchResult = CACHE.get(cacheKey);
151
+ if (isInvalidLlmsResult(fetchResult)) {
152
+ CACHE.delete(cacheKey);
153
+ fetchResult = undefined;
154
+ }
155
+ const cacheHit = !!fetchResult;
156
+ if (fetchResult) {
157
+ fetchResult = {
158
+ ...fetchResult,
159
+ requestedUrl: args.url,
160
+ cacheHit: true,
161
+ };
162
+ }
163
+ if (!fetchResult) {
164
+ let staleFetchResult = CACHE.get(cacheKey, {
165
+ allowStale: true,
166
+ noDeleteOnStaleGet: true,
167
+ });
168
+ if (isInvalidLlmsResult(staleFetchResult)) {
169
+ CACHE.delete(cacheKey);
170
+ staleFetchResult = undefined;
171
+ }
172
+ let llmsProbeError: string | undefined;
173
+
174
+ if (shouldProbeLlmsTxt) {
175
+ const fallbackOrigin = normalized.fallbackUrl
176
+ ? new URL(normalized.fallbackUrl).origin
177
+ : undefined;
178
+ const probeTimeoutMs = Math.max(
179
+ 1,
180
+ Math.min(MAX_LLMS_PROBE_TIMEOUT_MS, timeoutMs),
181
+ );
182
+ const llms = await runWithScopedTimeout(
183
+ controller.signal,
184
+ probeTimeoutMs,
185
+ (probeSignal) =>
186
+ probeLlmsText(url, timeoutMs, probeSignal, fallbackOrigin),
187
+ );
188
+ if (llms && 'text' in llms) {
189
+ const llmsHeaders = llms.headers || {};
190
+ const text = trimBlankRuns(llms.text);
191
+ fetchResult = {
192
+ requestedUrl: args.url,
193
+ finalUrl: llms.url,
194
+ statusCode: llms.statusCode,
195
+ contentType: llmsHeaders.contentType || 'text/plain',
196
+ charset: llmsHeaders.charset,
197
+ etag: llmsHeaders.etag,
198
+ lastModified: llmsHeaders.lastModified,
199
+ contentLength: llmsHeaders.contentLength,
200
+ filename: llmsHeaders.filename,
201
+ canonicalUrl: inferCanonicalUrlFromText(text, llms.url),
202
+ headings: extractHeadingsFromMarkdown(text),
203
+ title: undefined,
204
+ rawContent: text,
205
+ markdown: text,
206
+ text,
207
+ html: text,
208
+ extractedMain: false,
209
+ usedLlmsTxt: true,
210
+ sourceKind: 'llms_txt',
211
+ upgradedToHttps: !!llms.upgradedToHttps,
212
+ redirectChain: llms.redirectChain || [],
213
+ truncated: !!llms.truncated,
214
+ wordCount: wordCount(text),
215
+ qualitySignals: detectQualitySignals({
216
+ text,
217
+ markdown: text,
218
+ rawContent: text,
219
+ wordCount: wordCount(text),
220
+ sourceKind: 'llms_txt',
221
+ extractedMain: false,
222
+ }),
223
+ llmsProbeTruncated: !!llms.truncated,
224
+ decodedCharset: llms.decodedCharset,
225
+ decodeFallback: llms.decodeFallback,
226
+ decodeWarning: llms.decodeWarning,
227
+ cacheHit: false,
228
+ };
229
+ cacheFetchResult(
230
+ fetchResult,
231
+ args.extract_main,
232
+ args.prefer_llms_txt,
233
+ args.save_binary,
234
+ );
235
+ } else if (llms?.error) {
236
+ llmsProbeError = llms.error;
237
+ }
238
+
239
+ if (!fetchResult && args.prefer_llms_txt === 'always') {
240
+ const metadata = args.include_metadata
241
+ ? frontmatter({
242
+ requested_url: args.url,
243
+ used_llms_txt: false,
244
+ llms_probe_error: llmsProbeError,
245
+ prefer_llms_txt: args.prefer_llms_txt,
246
+ })
247
+ : '';
248
+ return joinRenderedContent(
249
+ metadata,
250
+ renderMessageForFormat(
251
+ buildLlmsRequiredMessage(args.url, llmsProbeError),
252
+ args.format,
253
+ ),
254
+ args.format,
255
+ );
256
+ }
257
+ }
258
+
259
+ if (!fetchResult) {
260
+ const { result, upgradedToHttps } = await fetchWithUpgradeFallback(
261
+ normalized,
262
+ timeoutMs,
263
+ args.format,
264
+ controller.signal,
265
+ buildConditionalHeaders(staleFetchResult),
266
+ 'GET',
267
+ allowedOrigins,
268
+ );
269
+ if ('blockedRedirect' in result) {
270
+ const metadata = args.include_metadata
271
+ ? frontmatter({
272
+ requested_url: args.url,
273
+ redirect_url: result.redirectUrl,
274
+ status_code: result.statusCode,
275
+ redirect_chain: result.redirectChain.map(
276
+ (step: RedirectStep) =>
277
+ `${step.status} ${step.from} -> ${step.to}`,
278
+ ),
279
+ upgraded_to_https: upgradedToHttps,
280
+ })
281
+ : '';
282
+ return joinRenderedContent(
283
+ metadata,
284
+ renderMessageForFormat(
285
+ buildRedirectResultMessage(
286
+ args.url,
287
+ result.redirectUrl,
288
+ result.statusCode,
289
+ ),
290
+ args.format,
291
+ ),
292
+ args.format,
293
+ );
294
+ }
295
+
296
+ const { response, finalUrl, redirectChain } = result;
297
+ if (response.status === 304 && staleFetchResult) {
298
+ fetchResult = {
299
+ ...staleFetchResult,
300
+ requestedUrl: args.url,
301
+ finalUrl,
302
+ statusCode: staleFetchResult.statusCode,
303
+ redirectChain,
304
+ llmsProbeError,
305
+ cacheRevalidated: true,
306
+ upstreamStatusCode: 304,
307
+ cacheHit: false,
308
+ };
309
+ cacheFetchResult(
310
+ fetchResult,
311
+ args.extract_main,
312
+ args.prefer_llms_txt,
313
+ args.save_binary,
314
+ );
315
+ } else {
316
+ if (!response.ok) {
317
+ throw new Error(
318
+ `Request failed with status code: ${response.status}`,
319
+ );
320
+ }
321
+ const headerMetadata = extractHeaderMetadata(
322
+ response.headers,
323
+ finalUrl,
324
+ );
325
+ const explicitBinary = isBinaryContentType(
326
+ headerMetadata.contentType || '',
327
+ );
328
+ const genericBinaryMime = isGenericBinaryMime(
329
+ headerMetadata.contentType || '',
330
+ );
331
+ const binaryDownloadLimit = args.save_binary
332
+ ? MAX_RESPONSE_BYTES
333
+ : MAX_BINARY_DOWNLOAD_BYTES;
334
+ if (
335
+ explicitBinary &&
336
+ !genericBinaryMime &&
337
+ typeof headerMetadata.contentLength === 'number' &&
338
+ headerMetadata.contentLength > binaryDownloadLimit
339
+ ) {
340
+ try {
341
+ await response.body?.cancel();
342
+ } catch {
343
+ // ignore cancel failures
344
+ }
345
+ fetchResult = {
346
+ requestedUrl: args.url,
347
+ finalUrl,
348
+ statusCode: response.status,
349
+ contentType:
350
+ headerMetadata.contentType || 'application/octet-stream',
351
+ charset: headerMetadata.charset,
352
+ etag: headerMetadata.etag,
353
+ lastModified: headerMetadata.lastModified,
354
+ contentLength: headerMetadata.contentLength,
355
+ filename: headerMetadata.filename,
356
+ canonicalUrl: finalUrl,
357
+ redirectChain,
358
+ upgradedToHttps,
359
+ truncated: false,
360
+ binary: true,
361
+ binaryKind: getBinaryKind(
362
+ headerMetadata.contentType || 'application/octet-stream',
363
+ ),
364
+ downloadLimitBytes: binaryDownloadLimit,
365
+ metadataOnly: true,
366
+ data: undefined,
367
+ llmsProbeError,
368
+ llmsProbeTruncated: false,
369
+ cacheHit: false,
370
+ };
371
+ cacheFetchResult(
372
+ fetchResult,
373
+ args.extract_main,
374
+ args.prefer_llms_txt,
375
+ args.save_binary,
376
+ );
377
+ } else {
378
+ const readLimit =
379
+ explicitBinary && !genericBinaryMime
380
+ ? binaryDownloadLimit
381
+ : MAX_RESPONSE_BYTES;
382
+ const body = await readBodyLimited(response, readLimit);
383
+ const provisionalDecoded =
384
+ !headerMetadata.contentType ||
385
+ genericBinaryMime ||
386
+ /^text\//i.test(headerMetadata.contentType)
387
+ ? decodeBody(
388
+ body.data,
389
+ headerMetadata.charset,
390
+ headerMetadata.contentType,
391
+ )
392
+ : undefined;
393
+ const looksHtmlPayload = provisionalDecoded
394
+ ? looksLikeHtmlText(provisionalDecoded.text)
395
+ : false;
396
+ const contentType = headerMetadata.contentType
397
+ ? genericBinaryMime && looksLikeTextBody(body.data)
398
+ ? looksHtmlPayload
399
+ ? 'text/html'
400
+ : 'text/plain'
401
+ : /^text\/plain(?:;|$)/i.test(headerMetadata.contentType) &&
402
+ looksHtmlPayload
403
+ ? 'text/html'
404
+ : headerMetadata.contentType
405
+ : looksLikeTextBody(body.data)
406
+ ? looksHtmlPayload
407
+ ? 'text/html'
408
+ : 'text/plain'
409
+ : 'application/octet-stream';
410
+
411
+ if (isBinaryContentType(contentType)) {
412
+ const binaryTooLarge =
413
+ body.truncated ||
414
+ (typeof headerMetadata.contentLength === 'number' &&
415
+ headerMetadata.contentLength > binaryDownloadLimit);
416
+ fetchResult = {
417
+ requestedUrl: args.url,
418
+ finalUrl,
419
+ statusCode: response.status,
420
+ contentType,
421
+ charset: headerMetadata.charset,
422
+ etag: headerMetadata.etag,
423
+ lastModified: headerMetadata.lastModified,
424
+ contentLength: headerMetadata.contentLength,
425
+ filename: headerMetadata.filename,
426
+ canonicalUrl: finalUrl,
427
+ redirectChain,
428
+ upgradedToHttps,
429
+ truncated: body.truncated,
430
+ binary: true,
431
+ binaryKind: getBinaryKind(contentType),
432
+ downloadLimitBytes: binaryDownloadLimit,
433
+ metadataOnly: binaryTooLarge,
434
+ data: binaryTooLarge ? undefined : body.data,
435
+ llmsProbeError,
436
+ llmsProbeTruncated: false,
437
+ cacheHit: false,
438
+ };
439
+ cacheFetchResult(
440
+ fetchResult,
441
+ args.extract_main,
442
+ args.prefer_llms_txt,
443
+ args.save_binary,
444
+ );
445
+ } else {
446
+ const decoded =
447
+ provisionalDecoded ||
448
+ decodeBody(body.data, headerMetadata.charset, contentType);
449
+ const rawText = decoded.text;
450
+ const extracted = isHtmlLikeContentType(contentType)
451
+ ? await extractFromHtml(
452
+ rawText,
453
+ finalUrl,
454
+ args.extract_main,
455
+ )
456
+ : {
457
+ title: undefined,
458
+ rawContent: cleanFetchedText(rawText),
459
+ html: cleanFetchedText(rawText),
460
+ text: cleanFetchedText(rawText),
461
+ markdown: cleanFetchedText(rawText),
462
+ extractedMain: false,
463
+ canonicalUrl: undefined,
464
+ headings: [],
465
+ };
466
+
467
+ fetchResult = {
468
+ requestedUrl: args.url,
469
+ finalUrl,
470
+ statusCode: response.status,
471
+ contentType,
472
+ charset: headerMetadata.charset,
473
+ etag: headerMetadata.etag,
474
+ lastModified: headerMetadata.lastModified,
475
+ contentLength: headerMetadata.contentLength,
476
+ filename: headerMetadata.filename,
477
+ canonicalUrl:
478
+ extracted.canonicalUrl ||
479
+ inferCanonicalUrlFromText(extracted.markdown, finalUrl) ||
480
+ finalUrl,
481
+ headings: extracted.headings?.length
482
+ ? extracted.headings
483
+ : extractHeadingsFromMarkdown(extracted.markdown),
484
+ title: extracted.title,
485
+ rawContent: extracted.rawContent,
486
+ markdown: extracted.markdown,
487
+ text: extracted.text,
488
+ html: extracted.html,
489
+ extractedMain: extracted.extractedMain,
490
+ usedLlmsTxt: false,
491
+ sourceKind: isHtmlLikeContentType(contentType)
492
+ ? 'html'
493
+ : 'text',
494
+ upgradedToHttps,
495
+ redirectChain,
496
+ truncated: body.truncated,
497
+ wordCount: wordCount(extracted.text),
498
+ qualitySignals: detectQualitySignals({
499
+ text: extracted.text,
500
+ markdown: extracted.markdown,
501
+ rawContent: extracted.rawContent,
502
+ wordCount: wordCount(extracted.text),
503
+ sourceKind: isHtmlLikeContentType(contentType)
504
+ ? 'html'
505
+ : 'text',
506
+ extractedMain: extracted.extractedMain,
507
+ }),
508
+ llmsProbeError,
509
+ llmsProbeTruncated: false,
510
+ decodedCharset: decoded.decodedCharset,
511
+ decodeFallback: decoded.decodeFallback,
512
+ decodeWarning: decoded.decodeWarning,
513
+ cacheHit: false,
514
+ };
515
+ cacheFetchResult(
516
+ fetchResult,
517
+ args.extract_main,
518
+ args.prefer_llms_txt,
519
+ args.save_binary,
520
+ );
521
+ }
522
+ }
523
+ }
524
+ }
525
+ }
526
+
527
+ ctx.metadata({
528
+ title:
529
+ ('binary' in fetchResult
530
+ ? fetchResult.filename
531
+ : fetchResult.title) || fetchResult.finalUrl,
532
+ metadata: {
533
+ url: fetchResult.finalUrl,
534
+ contentType: fetchResult.contentType,
535
+ truncated: fetchResult.truncated,
536
+ },
537
+ });
538
+
539
+ if ('binary' in fetchResult) {
540
+ if (fetchResult.metadataOnly || !fetchResult.data) {
541
+ const metadata = args.include_metadata
542
+ ? frontmatter({
543
+ requested_url: fetchResult.requestedUrl,
544
+ final_url: fetchResult.finalUrl,
545
+ canonical_url: fetchResult.canonicalUrl,
546
+ status_code: fetchResult.statusCode,
547
+ source_content_type: fetchResult.contentType,
548
+ charset: fetchResult.charset,
549
+ etag: fetchResult.etag,
550
+ last_modified: fetchResult.lastModified,
551
+ content_length: fetchResult.contentLength,
552
+ filename: fetchResult.filename,
553
+ binary_kind: fetchResult.binaryKind,
554
+ redirect_chain: fetchResult.redirectChain.map(
555
+ (step: RedirectStep) =>
556
+ `${step.status} ${step.from} -> ${step.to}`,
557
+ ),
558
+ upgraded_to_https: fetchResult.upgradedToHttps,
559
+ llms_probe_error: fetchResult.llmsProbeError,
560
+ cache_revalidated: fetchResult.cacheRevalidated,
561
+ cache_hit: fetchResult.cacheHit ?? cacheHit,
562
+ upstream_status_code: fetchResult.upstreamStatusCode,
563
+ truncated: fetchResult.truncated,
564
+ download_limit_bytes:
565
+ fetchResult.downloadLimitBytes ?? MAX_BINARY_DOWNLOAD_BYTES,
566
+ binary_metadata_only: true,
567
+ })
568
+ : '';
569
+ return joinRenderedContent(
570
+ metadata,
571
+ renderMessageForFormat(
572
+ buildBinaryResultMessage(fetchResult),
573
+ args.format,
574
+ ),
575
+ args.format,
576
+ );
577
+ }
578
+ if (!args.save_binary) {
579
+ const metadata = args.include_metadata
580
+ ? frontmatter({
581
+ requested_url: fetchResult.requestedUrl,
582
+ final_url: fetchResult.finalUrl,
583
+ canonical_url: fetchResult.canonicalUrl,
584
+ status_code: fetchResult.statusCode,
585
+ source_content_type: fetchResult.contentType,
586
+ charset: fetchResult.charset,
587
+ etag: fetchResult.etag,
588
+ last_modified: fetchResult.lastModified,
589
+ content_length: fetchResult.contentLength,
590
+ filename: fetchResult.filename,
591
+ binary_kind: fetchResult.binaryKind,
592
+ redirect_chain: fetchResult.redirectChain.map(
593
+ (step: RedirectStep) =>
594
+ `${step.status} ${step.from} -> ${step.to}`,
595
+ ),
596
+ upgraded_to_https: fetchResult.upgradedToHttps,
597
+ truncated: fetchResult.truncated,
598
+ save_binary: false,
599
+ cache_hit: fetchResult.cacheHit ?? cacheHit,
600
+ })
601
+ : '';
602
+ return joinRenderedContent(
603
+ metadata,
604
+ renderMessageForFormat(
605
+ `${fetchResult.binaryKind.toUpperCase()} content fetched but not saved. Re-run with save_binary=true to persist it.`,
606
+ args.format,
607
+ ),
608
+ args.format,
609
+ );
610
+ }
611
+ const savedPath = await saveBinary(
612
+ binaryDir,
613
+ fetchResult.data,
614
+ fetchResult.contentType,
615
+ fetchResult.filename,
616
+ );
617
+ const metadata = args.include_metadata
618
+ ? frontmatter({
619
+ requested_url: fetchResult.requestedUrl,
620
+ final_url: fetchResult.finalUrl,
621
+ canonical_url: fetchResult.canonicalUrl,
622
+ status_code: fetchResult.statusCode,
623
+ source_content_type: fetchResult.contentType,
624
+ charset: fetchResult.charset,
625
+ etag: fetchResult.etag,
626
+ last_modified: fetchResult.lastModified,
627
+ content_length: fetchResult.contentLength,
628
+ filename: fetchResult.filename,
629
+ binary_kind: fetchResult.binaryKind,
630
+ redirect_chain: fetchResult.redirectChain.map(
631
+ (step: RedirectStep) =>
632
+ `${step.status} ${step.from} -> ${step.to}`,
633
+ ),
634
+ upgraded_to_https: fetchResult.upgradedToHttps,
635
+ llms_probe_error: fetchResult.llmsProbeError,
636
+ cache_revalidated: fetchResult.cacheRevalidated,
637
+ cache_hit: fetchResult.cacheHit ?? cacheHit,
638
+ upstream_status_code: fetchResult.upstreamStatusCode,
639
+ truncated: fetchResult.truncated,
640
+ download_limit_bytes:
641
+ fetchResult.downloadLimitBytes ?? MAX_BINARY_DOWNLOAD_BYTES,
642
+ saved_path: savedPath,
643
+ })
644
+ : '';
645
+ return joinRenderedContent(
646
+ metadata,
647
+ renderMessageForFormat(
648
+ buildBinaryResultMessage(fetchResult, savedPath),
649
+ args.format,
650
+ ),
651
+ args.format,
652
+ );
653
+ }
654
+
655
+ const baseContent = pickContent(fetchResult, args.format);
656
+ const secondaryModelDecision = decideSecondaryModelUse(
657
+ fetchResult,
658
+ args.prompt,
659
+ secondaryModels,
660
+ );
661
+ const metadata = args.include_metadata
662
+ ? frontmatter({
663
+ requested_url: fetchResult.requestedUrl,
664
+ final_url: fetchResult.finalUrl,
665
+ canonical_url: fetchResult.canonicalUrl,
666
+ status_code: fetchResult.statusCode,
667
+ source_content_type: fetchResult.contentType,
668
+ charset: fetchResult.charset,
669
+ etag: fetchResult.etag,
670
+ last_modified: fetchResult.lastModified,
671
+ content_length: fetchResult.contentLength,
672
+ filename: fetchResult.filename,
673
+ headings: fetchResult.headings,
674
+ title: fetchResult.title,
675
+ source_kind: fetchResult.sourceKind,
676
+ used_llms_txt: fetchResult.usedLlmsTxt,
677
+ extracted_main: fetchResult.extractedMain,
678
+ redirect_chain: fetchResult.redirectChain.map(
679
+ (step: RedirectStep) =>
680
+ `${step.status} ${step.from} -> ${step.to}`,
681
+ ),
682
+ upgraded_to_https: fetchResult.upgradedToHttps,
683
+ llms_probe_error: fetchResult.llmsProbeError,
684
+ llms_probe_truncated: fetchResult.llmsProbeTruncated,
685
+ cache_revalidated: fetchResult.cacheRevalidated,
686
+ cache_hit: fetchResult.cacheHit ?? cacheHit,
687
+ upstream_status_code: fetchResult.upstreamStatusCode,
688
+ truncated: fetchResult.truncated,
689
+ word_count: fetchResult.wordCount,
690
+ quality_signals: fetchResult.qualitySignals,
691
+ decoded_charset: fetchResult.decodedCharset,
692
+ decode_fallback: fetchResult.decodeFallback,
693
+ decode_warning: fetchResult.decodeWarning,
694
+ secondary_model: undefined,
695
+ secondary_model_skipped_reason:
696
+ !secondaryModelDecision.use && args.prompt
697
+ ? secondaryModelDecision.reason
698
+ : undefined,
699
+ })
700
+ : '';
701
+
702
+ if (!secondaryModelDecision.use) {
703
+ return joinRenderedContent(metadata, baseContent, args.format);
704
+ }
705
+
706
+ if (!secondaryModels.length) {
707
+ return joinRenderedContent(metadata, baseContent, args.format);
708
+ }
709
+ let secondaryRun:
710
+ | Awaited<ReturnType<typeof runSecondaryModelWithFallback>>
711
+ | undefined;
712
+ let secondaryModelError: string | undefined;
713
+ try {
714
+ secondaryRun = await runSecondaryModelWithFallback(
715
+ pluginCtx.client,
716
+ ctx.directory || process.cwd(),
717
+ secondaryModels,
718
+ args.prompt || '',
719
+ fetchResult.markdown,
720
+ );
721
+ } catch (error: unknown) {
722
+ secondaryModelError =
723
+ error instanceof Error ? error.message : String(error);
724
+ }
725
+
726
+ if (!secondaryRun) {
727
+ const degradedMetadata = args.include_metadata
728
+ ? frontmatter({
729
+ requested_url: fetchResult.requestedUrl,
730
+ final_url: fetchResult.finalUrl,
731
+ canonical_url: fetchResult.canonicalUrl,
732
+ status_code: fetchResult.statusCode,
733
+ source_content_type: fetchResult.contentType,
734
+ charset: fetchResult.charset,
735
+ etag: fetchResult.etag,
736
+ last_modified: fetchResult.lastModified,
737
+ content_length: fetchResult.contentLength,
738
+ filename: fetchResult.filename,
739
+ headings: fetchResult.headings,
740
+ title: fetchResult.title,
741
+ source_kind: fetchResult.sourceKind,
742
+ used_llms_txt: fetchResult.usedLlmsTxt,
743
+ extracted_main: fetchResult.extractedMain,
744
+ redirect_chain: fetchResult.redirectChain.map(
745
+ (step: RedirectStep) =>
746
+ `${step.status} ${step.from} -> ${step.to}`,
747
+ ),
748
+ upgraded_to_https: fetchResult.upgradedToHttps,
749
+ llms_probe_error: fetchResult.llmsProbeError,
750
+ llms_probe_truncated: fetchResult.llmsProbeTruncated,
751
+ cache_revalidated: fetchResult.cacheRevalidated,
752
+ cache_hit: fetchResult.cacheHit ?? cacheHit,
753
+ upstream_status_code: fetchResult.upstreamStatusCode,
754
+ truncated: fetchResult.truncated,
755
+ word_count: fetchResult.wordCount,
756
+ quality_signals: fetchResult.qualitySignals,
757
+ decoded_charset: fetchResult.decodedCharset,
758
+ decode_fallback: fetchResult.decodeFallback,
759
+ decode_warning: fetchResult.decodeWarning,
760
+ secondary_model: undefined,
761
+ secondary_model_skipped_reason: 'secondary_model_failed',
762
+ secondary_model_error: secondaryModelError,
763
+ })
764
+ : '';
765
+ return joinRenderedContent(
766
+ degradedMetadata,
767
+ baseContent,
768
+ args.format,
769
+ );
770
+ }
771
+
772
+ const metadataWithSecondary = args.include_metadata
773
+ ? frontmatter({
774
+ requested_url: fetchResult.requestedUrl,
775
+ final_url: fetchResult.finalUrl,
776
+ canonical_url: fetchResult.canonicalUrl,
777
+ status_code: fetchResult.statusCode,
778
+ source_content_type: fetchResult.contentType,
779
+ charset: fetchResult.charset,
780
+ etag: fetchResult.etag,
781
+ last_modified: fetchResult.lastModified,
782
+ content_length: fetchResult.contentLength,
783
+ filename: fetchResult.filename,
784
+ headings: fetchResult.headings,
785
+ title: fetchResult.title,
786
+ source_kind: fetchResult.sourceKind,
787
+ used_llms_txt: fetchResult.usedLlmsTxt,
788
+ extracted_main: fetchResult.extractedMain,
789
+ redirect_chain: fetchResult.redirectChain.map(
790
+ (step: RedirectStep) =>
791
+ `${step.status} ${step.from} -> ${step.to}`,
792
+ ),
793
+ upgraded_to_https: fetchResult.upgradedToHttps,
794
+ llms_probe_error: fetchResult.llmsProbeError,
795
+ llms_probe_truncated: fetchResult.llmsProbeTruncated,
796
+ cache_revalidated: fetchResult.cacheRevalidated,
797
+ cache_hit: fetchResult.cacheHit ?? cacheHit,
798
+ upstream_status_code: fetchResult.upstreamStatusCode,
799
+ truncated: fetchResult.truncated,
800
+ word_count: fetchResult.wordCount,
801
+ quality_signals: fetchResult.qualitySignals,
802
+ decoded_charset: fetchResult.decodedCharset,
803
+ decode_fallback: fetchResult.decodeFallback,
804
+ decode_warning: fetchResult.decodeWarning,
805
+ secondary_model_input_truncated: secondaryRun.inputTruncated,
806
+ secondary_model_input_chars: secondaryRun.inputChars,
807
+ secondary_model_source_chars: secondaryRun.sourceChars,
808
+ secondary_model: `${secondaryRun.model.providerID}/${secondaryRun.model.modelID}`,
809
+ })
810
+ : '';
811
+ const secondaryRaw =
812
+ secondaryRun.text || 'No response from secondary model.';
813
+ const secondaryContent =
814
+ args.format === 'html'
815
+ ? withTruncationMarker(
816
+ `<pre>${escapeHtml(secondaryRaw)}</pre>`,
817
+ 'html',
818
+ false,
819
+ )
820
+ : withTruncationMarker(secondaryRaw, args.format, false);
821
+ return joinRenderedContent(
822
+ metadataWithSecondary,
823
+ secondaryContent,
824
+ args.format,
825
+ );
826
+ } finally {
827
+ clearTimeout(timeout);
828
+ ctx.abort.removeEventListener('abort', abortHandler);
829
+ }
830
+ },
831
+ });
832
+ }