wave-agent-sdk 1.1.5 → 1.3.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 (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Image dimensions probed from the container header, plus the gateway's
3
+ * per-side dimension limit.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * The model gateway (codechat → vision model) rejects any image whose width
8
+ * **or** height exceeds `MAX_IMAGE_DIMENSION_PX`, answering with a misleading
9
+ * generic message:
10
+ *
11
+ * HTTP 400 upstream_error:
12
+ * .messages[N].image[0]: You have uploaded an unsupported image. Please
13
+ * make sure your image is valid and has one of the following formats:
14
+ * webp, png, jpeg, and gif.
15
+ *
16
+ * Live probes against `https://codechat.codewave.163.com/api/v1/chat/completions`
17
+ * (model `deepseek-flash`, single user message, `stream: true`, 2026-09-20)
18
+ * pinned the trigger down to that single dimension — everything else is
19
+ * irrelevant:
20
+ *
21
+ * | image | px | per-side | bytes | result |
22
+ * |------------------|----------|----------|----------|--------|
23
+ * | 8192x1500 | 12.3 MP | 8192 | 2.5 MB | 200 |
24
+ * | 8193x1500 | 12.3 MP | 8193 | 2.5 MB | **400** |
25
+ * | 2250x8192 | 18.4 MP | 8192 | 4.3 MB | 200 |
26
+ * | 1500x8193 | 12.3 MP | 8193 | 3.5 MB | **400** |
27
+ * | 2250x9500 | 21.4 MP | 9500 | 4.8 MB | **400** |
28
+ * | 2250x15474 | 34.8 MP | 15474 | 6.0 MB | **400** |
29
+ * | 7000x4000 | 28.0 MP | 7000 | 5.3 MB | 200 |
30
+ * | 11000x1500 | 16.5 MP | 11000 | 2.9 MB | **400** |
31
+ * | 1500x1500 noise | 2.3 MP | 1500 | 6.8 MB (9.0 MB base64) | 200 |
32
+ * | 2250x12000 JPEG | 27.0 MP | 12000 | 2.1 MB | **400** |
33
+ * | 2250x12000 WebP | 27.0 MP | 12000 | 1.3 MB | **400** |
34
+ *
35
+ * So: pixel count does **not** matter (28 MP passes, 16.5 MP fails), byte
36
+ * size / request body size does **not** matter (a 6.8 MB PNG passes, a 1.3 MB
37
+ * WebP fails), and the container format does **not** matter (PNG/JPEG/WebP at
38
+ * the same dimensions all fail). The bound is a hard integer comparison on
39
+ * each side: exactly 8192 passes, 8193 fails.
40
+ *
41
+ * One caveat to keep in mind when re-testing: the probes above used
42
+ * `stream: true`, which is the path wave itself uses (`createParams.stream`).
43
+ * The identical body sent with `stream: false` returned 200 — the two gateway
44
+ * paths do not behave the same, so a non-streaming probe reproduces nothing.
45
+ *
46
+ * ## Why the client has to handle it
47
+ *
48
+ * Neither wave nor the proxy rejects oversized images: the request reaches the
49
+ * model and the whole turn fails with that 400. Outbound images come from
50
+ * paths that cannot all be downsampled (e.g. the model itself reading a long
51
+ * screenshot with the Read tool), so this module is the *detection* half:
52
+ * callers skip the image and tell the model what to do instead.
53
+ *
54
+ * The paste path in `packages/webview/src/utils/imageValidation.ts` no longer
55
+ * mirrors this bound. It targets the tighter outbound budget instead
56
+ * (`OUTBOUND_IMAGE_MAX_DIMENSION_PX`, shared through `constants/images.ts`) and
57
+ * *downsamples* an oversized paste rather than skipping it, so a pasted image
58
+ * is already compliant by the time it is sent. This module remains the
59
+ * detection half for the images nobody can re-encode — most of all the ones the
60
+ * model itself reads with the Read tool.
61
+ */
62
+ export const MAX_IMAGE_DIMENSION_PX = 8192;
63
+ /** JPEG frame headers (SOF0-SOF15) — the only segments carrying pixel size. */
64
+ const SOF_MARKERS = new Set([
65
+ 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
66
+ ]);
67
+ function u16be(bytes, offset) {
68
+ return (bytes[offset] << 8) | bytes[offset + 1];
69
+ }
70
+ function u16le(bytes, offset) {
71
+ return bytes[offset] | (bytes[offset + 1] << 8);
72
+ }
73
+ function u24le(bytes, offset) {
74
+ return bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16);
75
+ }
76
+ function startsWith(bytes, signature, offset = 0) {
77
+ if (bytes.length < offset + signature.length)
78
+ return false;
79
+ return signature.every((byte, i) => bytes[offset + i] === byte);
80
+ }
81
+ function pngDimensions(bytes) {
82
+ // 8-byte signature, then the IHDR chunk: length(4) type(4) width(4) height(4)
83
+ if (bytes.length < 24)
84
+ return undefined;
85
+ if (!startsWith(bytes, [0x49, 0x48, 0x44, 0x52], 12))
86
+ return undefined; // "IHDR"
87
+ const width = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
88
+ const height = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
89
+ if (width <= 0 || height <= 0)
90
+ return undefined;
91
+ return { width, height };
92
+ }
93
+ /** Walk the JPEG marker segments until the frame header (SOFn). */
94
+ function jpegDimensions(bytes) {
95
+ let offset = 2; // skip SOI
96
+ while (offset + 3 < bytes.length) {
97
+ if (bytes[offset] !== 0xff) {
98
+ offset++;
99
+ continue;
100
+ }
101
+ const marker = bytes[offset + 1];
102
+ // Padding fill bytes and standalone markers carry no length.
103
+ if (marker === 0xff) {
104
+ offset++;
105
+ continue;
106
+ }
107
+ if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9)) {
108
+ offset += 2;
109
+ continue;
110
+ }
111
+ const length = u16be(bytes, offset + 2);
112
+ if (length < 2)
113
+ return undefined;
114
+ if (SOF_MARKERS.has(marker)) {
115
+ if (offset + 9 >= bytes.length)
116
+ return undefined;
117
+ const height = u16be(bytes, offset + 5);
118
+ const width = u16be(bytes, offset + 7);
119
+ if (width <= 0 || height <= 0)
120
+ return undefined;
121
+ return { width, height };
122
+ }
123
+ offset += 2 + length;
124
+ }
125
+ return undefined;
126
+ }
127
+ function webpDimensions(bytes) {
128
+ // "RIFF" <size> "WEBP" <fourcc> <chunk size> <chunk payload...>
129
+ if (bytes.length < 20)
130
+ return undefined;
131
+ const fourcc = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
132
+ if (fourcc === "VP8X") {
133
+ // Extended: 24-bit little-endian (value - 1) for each side.
134
+ if (bytes.length < 30)
135
+ return undefined;
136
+ return { width: u24le(bytes, 24) + 1, height: u24le(bytes, 27) + 1 };
137
+ }
138
+ if (fourcc === "VP8L") {
139
+ if (bytes.length < 25)
140
+ return undefined;
141
+ if (bytes[20] !== 0x2f)
142
+ return undefined;
143
+ // 14 bits width-1, then 14 bits height-1, little-endian bit packing.
144
+ const bits = bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24);
145
+ return {
146
+ width: (bits & 0x3fff) + 1,
147
+ height: ((bits >> 14) & 0x3fff) + 1,
148
+ };
149
+ }
150
+ if (fourcc === "VP8 ") {
151
+ if (bytes.length < 30)
152
+ return undefined;
153
+ // Lossy: 3-byte frame tag, then the 0x9d012a start code.
154
+ if (!startsWith(bytes, [0x9d, 0x01, 0x2a], 23)) {
155
+ return undefined;
156
+ }
157
+ return {
158
+ width: u16le(bytes, 26) & 0x3fff,
159
+ height: u16le(bytes, 28) & 0x3fff,
160
+ };
161
+ }
162
+ return undefined;
163
+ }
164
+ function gifDimensions(bytes) {
165
+ // "GIF87a"/"GIF89a", then the logical screen descriptor.
166
+ if (bytes.length < 10)
167
+ return undefined;
168
+ const width = u16le(bytes, 6);
169
+ const height = u16le(bytes, 8);
170
+ if (width <= 0 || height <= 0)
171
+ return undefined;
172
+ return { width, height };
173
+ }
174
+ /**
175
+ * Read the pixel dimensions straight out of the header. Supports the four
176
+ * formats the gateway accepts (png / jpeg / gif / webp).
177
+ *
178
+ * Returns `undefined` when the format is unknown or the header is truncated —
179
+ * callers must treat that as "unknown", never as "oversized".
180
+ */
181
+ export function getImageDimensions(bytes) {
182
+ if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
183
+ return pngDimensions(bytes);
184
+ }
185
+ if (startsWith(bytes, [0xff, 0xd8, 0xff])) {
186
+ return jpegDimensions(bytes);
187
+ }
188
+ if (startsWith(bytes, [0x47, 0x49, 0x46, 0x38])) {
189
+ return gifDimensions(bytes);
190
+ }
191
+ if (startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) &&
192
+ startsWith(bytes, [0x57, 0x45, 0x42, 0x50], 8)) {
193
+ return webpDimensions(bytes);
194
+ }
195
+ return undefined;
196
+ }
197
+ /**
198
+ * Enough base64 to cover a header of any of the supported formats: PNG/WebP/GIF
199
+ * live in the first ~30 bytes, JPEG's SOF segment follows the app segments —
200
+ * a few KB at most (large EXIF/ICC blocks occasionally reach ~64 KB).
201
+ */
202
+ const DATA_URL_PREFIX_B64_CHARS = Math.ceil((96 * 1024 * 4) / 3);
203
+ export function getImageDimensionsFromDataUrl(dataUrl) {
204
+ const comma = dataUrl.indexOf(",");
205
+ if (comma === -1 || !dataUrl.startsWith("data:image/"))
206
+ return undefined;
207
+ const prefix = dataUrl.slice(comma + 1, comma + 1 + DATA_URL_PREFIX_B64_CHARS);
208
+ if (prefix.length === 0)
209
+ return undefined;
210
+ try {
211
+ return getImageDimensions(Buffer.from(prefix, "base64"));
212
+ }
213
+ catch {
214
+ return undefined;
215
+ }
216
+ }
217
+ export function exceedsMaxDimension(dimensions, max = MAX_IMAGE_DIMENSION_PX) {
218
+ return dimensions.width > max || dimensions.height > max;
219
+ }
220
+ /**
221
+ * One-line, actionable note that replaces an image the gateway would reject.
222
+ * Modelled on opencode's `[N image omitted: ...]` placeholder: the model (and
223
+ * anyone reading the transcript) gets the real size, the bound, and the way
224
+ * out.
225
+ */
226
+ export function omittedImageNote(dimensions, sourcePath) {
227
+ const where = sourcePath ? ` Path: ${sourcePath}` : "";
228
+ return (`[Image omitted: ${dimensions.width}x${dimensions.height} exceeds the ` +
229
+ `${MAX_IMAGE_DIMENSION_PX}px per-side limit of the vision model gateway, ` +
230
+ `which would reject the whole request. Crop it, split it into smaller ` +
231
+ `images, or downscale it before sending again.${where}]`);
232
+ }
@@ -0,0 +1,66 @@
1
+ export interface SharpMetadata {
2
+ width?: number;
3
+ height?: number;
4
+ /** Container format, e.g. `png` / `jpeg` / `webp` / `gif` / `svg`. */
5
+ format?: string;
6
+ hasAlpha?: boolean;
7
+ }
8
+ /**
9
+ * The slice of sharp's chainable API this codebase uses. Structural typing keeps
10
+ * the SDK compiling without sharp's own types (and without a dependency on a
11
+ * package that may not be installed).
12
+ */
13
+ export interface SharpImage {
14
+ metadata(): Promise<SharpMetadata>;
15
+ resize(width: number, height: number, options?: {
16
+ fit?: "inside";
17
+ withoutEnlargement?: boolean;
18
+ }): SharpImage;
19
+ png(options?: {
20
+ compressionLevel?: number;
21
+ palette?: boolean;
22
+ }): SharpImage;
23
+ webp(options?: {
24
+ quality?: number;
25
+ }): SharpImage;
26
+ jpeg(options?: {
27
+ quality?: number;
28
+ }): SharpImage;
29
+ flatten(options?: {
30
+ background?: {
31
+ r: number;
32
+ g: number;
33
+ b: number;
34
+ };
35
+ }): SharpImage;
36
+ toBuffer(): Promise<Buffer>;
37
+ }
38
+ export interface SharpFactory {
39
+ (input: Buffer): SharpImage;
40
+ /** Populated at require time; `vips` doubles as a "the native side loaded" check. */
41
+ versions?: {
42
+ vips?: string;
43
+ };
44
+ }
45
+ /**
46
+ * Require sharp and check it is usable. Never throws: an absent package, a
47
+ * missing platform build, a failed `dlopen` all come back as `undefined`, with
48
+ * the underlying error logged once so an operator can tell the three apart.
49
+ *
50
+ * `requireFn` is injectable so tests can exercise every outcome without the real
51
+ * module.
52
+ */
53
+ export declare function resolveSharp(requireFn: NodeRequire): SharpFactory | undefined;
54
+ /**
55
+ * The image codec, or `undefined` when this process cannot resize images.
56
+ * Resolved once on first call — callers on a hot path pay a single boolean
57
+ * check after that.
58
+ */
59
+ export declare function getImageProcessor(): SharpFactory | undefined;
60
+ /**
61
+ * Forget the memoised result so the next {@link getImageProcessor} resolves
62
+ * again. Called by the runtime-dependency installer once sharp is on disk —
63
+ * without it a process that asked for a codec *before* the download finished
64
+ * would keep degrading for its whole lifetime. Also used by tests.
65
+ */
66
+ export declare function resetImageProcessor(): void;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Lazy access to the optional `sharp` image codec.
3
+ *
4
+ * ## Why lazy, and why a runtime require
5
+ *
6
+ * `sharp` must never be pulled in by a plain `import`:
7
+ *
8
+ * - It is a native module — its wrapper does `require("../src/build/Release/
9
+ * sharp-<platform>.node")` *relative to its own directory* and falls back to
10
+ * `@img/sharp-<platform>/sharp.node`. Bundlers therefore cannot inline it, and
11
+ * a top-level import would drag a `.node` file into every host bundle.
12
+ * - It is optional. Host processes (the desktop main process, the IDE extension
13
+ * hosts) load the SDK barrel without a platform build present; evaluating
14
+ * `require("sharp")` there would throw during module evaluation and take the
15
+ * host down. Same rule as `utils/ripgrep.ts` — both optional runtime
16
+ * dependencies are resolved lazily and memoised, so a missing one only costs
17
+ * the tool that needs it (an image without a codec, grep without rg).
18
+ *
19
+ * So resolution happens on first use, through `createRequire(import.meta.url)`
20
+ * — a *runtime* require the host-bundle scanner does not match (it looks for
21
+ * literal `require("<bare specifier>")` calls), keeping the CLI bundle free of
22
+ * both the module and a load-time dependency on it.
23
+ *
24
+ * ## Failure memory
25
+ *
26
+ * The result (including "unavailable") is memoised for the life of the process,
27
+ * so a missing codec costs one failed resolution rather than one per image.
28
+ * Because a first-use failure is often just "the installer has not finished
29
+ * yet", the installer calls {@link resetImageProcessor} after it drops files on
30
+ * disk so the next turn can pick them up.
31
+ */
32
+ import { createRequire } from "node:module";
33
+ import { logger } from "./globalLogger.js";
34
+ let cached;
35
+ let resolutionAttempted = false;
36
+ /**
37
+ * Require sharp and check it is usable. Never throws: an absent package, a
38
+ * missing platform build, a failed `dlopen` all come back as `undefined`, with
39
+ * the underlying error logged once so an operator can tell the three apart.
40
+ *
41
+ * `requireFn` is injectable so tests can exercise every outcome without the real
42
+ * module.
43
+ */
44
+ export function resolveSharp(requireFn) {
45
+ try {
46
+ const loaded = requireFn("sharp");
47
+ if (typeof loaded !== "function") {
48
+ logger.warn("sharp resolved to a non-callable value; image resizing off");
49
+ return undefined;
50
+ }
51
+ const factory = loaded;
52
+ if (typeof factory.versions?.vips !== "string") {
53
+ logger.warn("sharp loaded without a libvips version; the native build is unusable");
54
+ return undefined;
55
+ }
56
+ return factory;
57
+ }
58
+ catch (error) {
59
+ logger.warn("sharp is unavailable, images will not be resized:", error);
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * The image codec, or `undefined` when this process cannot resize images.
65
+ * Resolved once on first call — callers on a hot path pay a single boolean
66
+ * check after that.
67
+ */
68
+ export function getImageProcessor() {
69
+ if (!resolutionAttempted) {
70
+ resolutionAttempted = true;
71
+ cached = resolveSharp(createRequire(import.meta.url));
72
+ }
73
+ return cached;
74
+ }
75
+ /**
76
+ * Forget the memoised result so the next {@link getImageProcessor} resolves
77
+ * again. Called by the runtime-dependency installer once sharp is on disk —
78
+ * without it a process that asked for a codec *before* the download finished
79
+ * would keep degrading for its whole lifetime. Also used by tests.
80
+ */
81
+ export function resetImageProcessor() {
82
+ cached = undefined;
83
+ resolutionAttempted = false;
84
+ }
@@ -0,0 +1,29 @@
1
+ export type OutboundImagePlan = {
2
+ kind: "send";
3
+ dataUrl: string;
4
+ note?: string;
5
+ } | {
6
+ kind: "omit";
7
+ note: string;
8
+ };
9
+ /** Drop cached rewrites (tests, and any future "the codec just appeared" reset). */
10
+ export declare function __resetImageRewriteCacheForTesting(): void;
11
+ /**
12
+ * Cache key for an image that came from a local file: path plus size plus
13
+ * mtime. Cheaper than hashing the bytes, and a rewritten file (different size or
14
+ * mtime) gets a fresh entry instead of reusing a stale rewrite.
15
+ */
16
+ export declare function imageFileCacheKey(path: string): string;
17
+ export interface OutboundImageInput {
18
+ /** Data URL of the image (already base64, whatever its original source). */
19
+ dataUrl: string;
20
+ /** Cache identity: the data URL itself for inline images, path+size+mtime for files. */
21
+ cacheKey: string;
22
+ /** Local file the image came from, when it has one (used in notes). */
23
+ sourcePath?: string;
24
+ }
25
+ /**
26
+ * Decide what to send for one image. Never throws and never rejects — see the
27
+ * module comment.
28
+ */
29
+ export declare function planOutboundImage(input: OutboundImageInput): Promise<OutboundImagePlan>;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Outbound image rewriting: decide whether an image can go into a request body
3
+ * as it is, or must be re-encoded first.
4
+ *
5
+ * ## Where this sits
6
+ *
7
+ * `convertMessagesForAPI` is the single choke point every image passes through
8
+ * on its way out (user attachments, tool results, Read-tool screenshots). This
9
+ * module answers one question per image: send it (untouched, or rewritten) or
10
+ * omit it with an explanatory note. The caller only wires the answer into the
11
+ * message.
12
+ *
13
+ * ## The ladder
14
+ *
15
+ * Only images outside the budget (`imageBudget.ts`) touch the codec, which keeps
16
+ * the common case — the model reads a normal-sized screenshot — free of any
17
+ * native work and byte-identical.
18
+ *
19
+ * An over-budget image is re-encoded by walking two nested loops: the source is
20
+ * first clamped to the dimension budget, then downscaled in steps (1 → 0.75 →
21
+ * 0.5 → 0.25) and, at each step, encoded at several qualities. The first output
22
+ * inside the raw-byte budget wins. Transparency drives the encoder order: alpha
23
+ * sources try PNG/WebP first and only fall back to JPEG after flattening onto
24
+ * white — a naive JPEG would turn transparent pixels black. JPEG sources skip
25
+ * PNG entirely (re-encoding a photo as PNG would inflate it).
26
+ *
27
+ * An image that is only over the *byte* budget keeps its dimensions: scale 1
28
+ * with the clamp being a no-op means "compress at full resolution", which is
29
+ * what Claude Code does too, and the model's coordinates stay valid.
30
+ *
31
+ * A **fresh `sharp(buffer)` instance per rung** is mandatory, not stylistic:
32
+ * sharp is chainable and a reused instance keeps returning the previous rung's
33
+ * bytes (documented sharp behaviour, and the reason Claude Code's implementation
34
+ * does the same).
35
+ *
36
+ * The output mime always comes from the rung that produced the bytes, never from
37
+ * a hardcoded string — mislabelling a container is exactly the 400 root cause
38
+ * fixed in `#2273`.
39
+ *
40
+ * ## Omission, not failure
41
+ *
42
+ * `planOutboundImage` never rejects and never throws: a turn must keep going.
43
+ * Outcomes are `send` (possibly with a size note the model can use to map
44
+ * coordinates back) or `omit` (with a note stating the real size and what to do
45
+ * instead).
46
+ */
47
+ import { statSync } from "node:fs";
48
+ import { LRUCache } from "lru-cache";
49
+ import { logger } from "./globalLogger.js";
50
+ import { OUTBOUND_IMAGE_MAX_DIMENSION_PX, OUTBOUND_IMAGE_TARGET_RAW_BYTES, exceedsOutboundBudget, resizedImageNote, } from "./imageBudget.js";
51
+ import { exceedsMaxDimension, getImageDimensions, getImageDimensionsFromDataUrl, omittedImageNote, } from "./imageDimensions.js";
52
+ import { getImageProcessor } from "./imageProcessor.js";
53
+ import { ensureRuntimeDeps } from "./runtimeDeps.js";
54
+ /**
55
+ * Downscale steps tried in order. The floor keeps a pathological aspect ratio
56
+ * (e.g. 20000x40) from collapsing to a zero-pixel edge.
57
+ */
58
+ const SCALES = [1, 0.75, 0.5, 0.25];
59
+ const MIN_DIMENSION_PX = 64;
60
+ const JPEG_QUALITIES = [80, 60, 40, 20];
61
+ const WEBP_QUALITIES = [80, 60];
62
+ const PNG_COMPRESSION_LEVEL = 9;
63
+ const OPAQUE_BACKGROUND = { r: 255, g: 255, b: 255 };
64
+ const JPEG_RUNGS = JPEG_QUALITIES.map((quality) => ({
65
+ mime: "image/jpeg",
66
+ encode: (image) => image.jpeg({ quality }),
67
+ }));
68
+ /** Lossless first; `palette` narrows the colour depth when full PNG is too big. */
69
+ const PNG_RUNGS = [
70
+ {
71
+ mime: "image/png",
72
+ encode: (image) => image.png({ compressionLevel: PNG_COMPRESSION_LEVEL }),
73
+ },
74
+ {
75
+ mime: "image/png",
76
+ encode: (image) => image.png({ compressionLevel: PNG_COMPRESSION_LEVEL, palette: true }),
77
+ },
78
+ ];
79
+ const WEBP_RUNGS = WEBP_QUALITIES.map((quality) => ({
80
+ mime: "image/webp",
81
+ encode: (image) => image.webp({ quality }),
82
+ }));
83
+ /** Transparency survives the whole ladder; JPEG is a last resort, flattened white. */
84
+ const ALPHA_LADDER = [
85
+ ...PNG_RUNGS,
86
+ ...WEBP_RUNGS,
87
+ {
88
+ mime: "image/jpeg",
89
+ encode: (image) => image
90
+ .flatten({ background: OPAQUE_BACKGROUND })
91
+ .jpeg({ quality: JPEG_QUALITIES[0] }),
92
+ },
93
+ ];
94
+ const OPAQUE_LADDER = [...PNG_RUNGS, ...WEBP_RUNGS, ...JPEG_RUNGS];
95
+ function ladderFor(format, hasAlpha) {
96
+ // A photo is never improved by a lossless PNG pass; go straight to JPEG.
97
+ if (format === "jpeg")
98
+ return JPEG_RUNGS;
99
+ if (hasAlpha)
100
+ return ALPHA_LADDER;
101
+ return OPAQUE_LADDER;
102
+ }
103
+ function toDataUrl(mime, bytes) {
104
+ return `data:${mime};base64,${bytes.toString("base64")}`;
105
+ }
106
+ /** Base64 payload of a data URL, without the `data:<mime>;base64,` prefix. */
107
+ function dataUrlBody(dataUrl) {
108
+ const comma = dataUrl.indexOf(",");
109
+ return comma === -1 ? dataUrl : dataUrl.slice(comma + 1);
110
+ }
111
+ /**
112
+ * Shrink both sides into the dimension budget, preserving aspect ratio. `fit:
113
+ * "inside"` re-derives the ratio from the real image, so clamping each side
114
+ * independently is correct (3000x2000 → 2000x1333, 1000x4000 → 500x2000).
115
+ */
116
+ function clampToBudget(dimensions) {
117
+ return {
118
+ width: Math.min(dimensions.width, OUTBOUND_IMAGE_MAX_DIMENSION_PX),
119
+ height: Math.min(dimensions.height, OUTBOUND_IMAGE_MAX_DIMENSION_PX),
120
+ };
121
+ }
122
+ /**
123
+ * Walk the ladder until something fits the byte budget. Throws when the codec
124
+ * itself fails (missing platform build, undecodable bytes) — the caller treats
125
+ * that as "no codec".
126
+ */
127
+ async function runLadder(sharp, source, knownDimensions) {
128
+ const metadata = await sharp(source).metadata();
129
+ const width = metadata.width ?? knownDimensions?.width ?? 0;
130
+ const height = metadata.height ?? knownDimensions?.height ?? 0;
131
+ if (width <= 0 || height <= 0)
132
+ return { status: "unknown-dimensions" };
133
+ const withinBudget = clampToBudget({ width, height });
134
+ const rungs = ladderFor(metadata.format, metadata.hasAlpha === true);
135
+ for (const scale of SCALES) {
136
+ const targetWidth = Math.max(MIN_DIMENSION_PX, Math.floor(withinBudget.width * scale));
137
+ const targetHeight = Math.max(MIN_DIMENSION_PX, Math.floor(withinBudget.height * scale));
138
+ for (const rung of rungs) {
139
+ const encoded = await rung
140
+ .encode(sharp(source).resize(targetWidth, targetHeight, {
141
+ fit: "inside",
142
+ withoutEnlargement: true,
143
+ }))
144
+ .toBuffer();
145
+ if (encoded.length <= OUTBOUND_IMAGE_TARGET_RAW_BYTES) {
146
+ return {
147
+ status: "fits",
148
+ dataUrl: toDataUrl(rung.mime, encoded),
149
+ dimensions: getImageDimensions(encoded),
150
+ };
151
+ }
152
+ }
153
+ }
154
+ return { status: "exhausted", sourceDimensions: { width, height } };
155
+ }
156
+ /**
157
+ * Results keyed by the caller-supplied cache key, capped at a handful of
158
+ * entries: every turn rebuilds the request body from the whole history, so
159
+ * without a cache each turn would re-encode the same oversized screenshots.
160
+ */
161
+ const rewritten = new LRUCache({ max: 8 });
162
+ /** Drop cached rewrites (tests, and any future "the codec just appeared" reset). */
163
+ export function __resetImageRewriteCacheForTesting() {
164
+ rewritten.clear();
165
+ }
166
+ /**
167
+ * Cache key for an image that came from a local file: path plus size plus
168
+ * mtime. Cheaper than hashing the bytes, and a rewritten file (different size or
169
+ * mtime) gets a fresh entry instead of reusing a stale rewrite.
170
+ */
171
+ export function imageFileCacheKey(path) {
172
+ try {
173
+ const stat = statSync(path);
174
+ return `${path}:${stat.size}:${stat.mtimeMs}`;
175
+ }
176
+ catch {
177
+ return path;
178
+ }
179
+ }
180
+ /**
181
+ * The degradation path used whenever we could not even try to shrink the image
182
+ * (no codec, codec failure, unknown size): an image the gateway would reject
183
+ * anyway is omitted with an actionable note; anything the gateway can still take
184
+ * is sent untouched, oversized bytes included — we have no way to shrink it, and
185
+ * refusing it would break a turn that used to work.
186
+ */
187
+ function degrade(dataUrl, dimensions, sourcePath, reason) {
188
+ if (dimensions && exceedsMaxDimension(dimensions)) {
189
+ logger.warn(`Omitting oversized image (${reason}):`, dimensions, sourcePath);
190
+ return { kind: "omit", note: omittedImageNote(dimensions, sourcePath) };
191
+ }
192
+ logger.warn(`Sending image without rewriting (${reason}):`, sourcePath);
193
+ return { kind: "send", dataUrl };
194
+ }
195
+ /**
196
+ * Decide what to send for one image. Never throws and never rejects — see the
197
+ * module comment.
198
+ */
199
+ export async function planOutboundImage(input) {
200
+ const { dataUrl, cacheKey, sourcePath } = input;
201
+ const dimensions = getImageDimensionsFromDataUrl(dataUrl);
202
+ if (!exceedsOutboundBudget(dataUrl, dimensions)) {
203
+ return { kind: "send", dataUrl };
204
+ }
205
+ const cached = rewritten.get(cacheKey);
206
+ if (cached)
207
+ return cached;
208
+ const plan = await rewrite(dataUrl, dimensions, sourcePath);
209
+ rewritten.set(cacheKey, plan);
210
+ return plan;
211
+ }
212
+ async function rewrite(dataUrl, dimensions, sourcePath) {
213
+ const sharp = getImageProcessor();
214
+ if (!sharp) {
215
+ // Kick a download so a later turn has the codec; this turn still degrades
216
+ // rather than blocking the request on ~8MB.
217
+ void ensureRuntimeDeps();
218
+ return degrade(dataUrl, dimensions, sourcePath, "no image codec available");
219
+ }
220
+ let outcome;
221
+ try {
222
+ outcome = await runLadder(sharp, Buffer.from(dataUrlBody(dataUrl), "base64"), dimensions);
223
+ }
224
+ catch (error) {
225
+ logger.warn("Image rewrite failed:", sourcePath, error);
226
+ return degrade(dataUrl, dimensions, sourcePath, "the image codec failed");
227
+ }
228
+ if (outcome.status === "unknown-dimensions") {
229
+ return degrade(dataUrl, dimensions, sourcePath, "its dimensions could not be determined");
230
+ }
231
+ if (outcome.status === "exhausted") {
232
+ // The codec ran out of rungs without fitting the budget (extreme aspect
233
+ // ratios). The spec routes this to the same note as an unshrinkable
234
+ // oversized image rather than sending something we know is over budget.
235
+ logger.warn("Omitting image that cannot fit the outbound budget:", outcome.sourceDimensions, sourcePath);
236
+ return {
237
+ kind: "omit",
238
+ note: omittedImageNote(outcome.sourceDimensions, sourcePath),
239
+ };
240
+ }
241
+ const resizedTo = outcome.dimensions;
242
+ const wasResized = dimensions !== undefined &&
243
+ resizedTo !== undefined &&
244
+ (dimensions.width !== resizedTo.width ||
245
+ dimensions.height !== resizedTo.height);
246
+ return {
247
+ kind: "send",
248
+ dataUrl: outcome.dataUrl,
249
+ ...(wasResized ? { note: resizedImageNote(dimensions, resizedTo) } : {}),
250
+ };
251
+ }
@@ -4,7 +4,11 @@ interface ParsedMarkdownFile {
4
4
  config?: CustomSlashCommandConfig;
5
5
  }
6
6
  /**
7
- * Parse YAML frontmatter from markdown content
7
+ * Parse YAML frontmatter from markdown content.
8
+ *
9
+ * Value parsing is shared with skills, subagents and memory files — see
10
+ * `frontmatterYaml.ts` (spec `ui/slash-commands` 场景 3,
11
+ * `core/memory-management` 场景 4/13).
8
12
  */
9
13
  export declare function parseFrontmatter(content: string): {
10
14
  frontmatter?: Record<string, unknown>;