wave-agent-sdk 1.2.0 → 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 (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -0,0 +1,192 @@
1
+ const FRONTMATTER_REGEX = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n([\s\S]*))?$/;
2
+ /**
3
+ * Split a markdown file into its leading `---` frontmatter block and the body.
4
+ * `yaml` is null when the file has no frontmatter; the body is then the whole
5
+ * content, untrimmed.
6
+ */
7
+ export function splitFrontmatter(content) {
8
+ const match = content.match(FRONTMATTER_REGEX);
9
+ if (!match) {
10
+ return { yaml: null, body: content };
11
+ }
12
+ return { yaml: match[1], body: match[2] ?? "" };
13
+ }
14
+ /**
15
+ * Parse a frontmatter block (without the `---` delimiters) into key/value
16
+ * pairs. Unparseable input yields an empty object; callers that require fields
17
+ * report the missing ones themselves.
18
+ */
19
+ export function parseFrontmatterYaml(yamlContent) {
20
+ const frontmatter = {};
21
+ try {
22
+ const lines = yamlContent.split("\n");
23
+ let currentKey = null;
24
+ for (let index = 0; index < lines.length; index++) {
25
+ const raw = lines[index].replace(/\r$/, "");
26
+ const trimmed = raw.trim();
27
+ if (!trimmed || trimmed.startsWith("#"))
28
+ continue;
29
+ const indent = raw.length - raw.trimStart().length;
30
+ // Block list item belonging to the key above it.
31
+ if (trimmed.startsWith("-") && currentKey) {
32
+ const value = stripQuotes(trimmed.substring(1).trim());
33
+ if (value) {
34
+ const existing = frontmatter[currentKey];
35
+ if (Array.isArray(existing)) {
36
+ existing.push(value);
37
+ }
38
+ else {
39
+ frontmatter[currentKey] = [value];
40
+ }
41
+ }
42
+ continue;
43
+ }
44
+ const colonIndex = trimmed.indexOf(":");
45
+ if (colonIndex === -1)
46
+ continue;
47
+ const key = trimmed.substring(0, colonIndex).trim();
48
+ if (!key)
49
+ continue;
50
+ const inlineValue = trimmed.substring(colonIndex + 1).trim();
51
+ currentKey = key;
52
+ // Block scalar: `>-`, `|`, `>+`, `|-`, ...
53
+ const blockIndicator = /^([>|])([-+]?)$/.exec(inlineValue);
54
+ if (blockIndicator) {
55
+ const block = readBlockScalar(lines, index + 1, indent, blockIndicator[1]);
56
+ // Same treatment as inline scalars: chomping indicators only decide how
57
+ // the block's line breaks are trimmed, and a metadata scalar never
58
+ // keeps leading/trailing whitespace.
59
+ frontmatter[key] = block.text.trim();
60
+ index = block.nextIndex - 1;
61
+ continue;
62
+ }
63
+ if (inlineValue) {
64
+ frontmatter[key] = stripQuotes(inlineValue);
65
+ continue;
66
+ }
67
+ // Key with no inline value: either an indented block list or an indented
68
+ // (multi-line) scalar.
69
+ const continuation = readIndentedValue(lines, index + 1, indent);
70
+ index = continuation.nextIndex - 1;
71
+ if (continuation.items) {
72
+ frontmatter[key] = continuation.items;
73
+ }
74
+ else {
75
+ frontmatter[key] = continuation.text ?? [];
76
+ }
77
+ }
78
+ }
79
+ catch {
80
+ // Return whatever was parsed so far — callers validate required fields.
81
+ }
82
+ return frontmatter;
83
+ }
84
+ function stripQuotes(value) {
85
+ return value.replace(/^["']|["']$/g, "");
86
+ }
87
+ /**
88
+ * Fold a block scalar's lines the way YAML does: `|` keeps line breaks, `>`
89
+ * folds a single break into a space and turns each blank line into one line
90
+ * break (a blank line is a paragraph break, not two).
91
+ */
92
+ function foldBlockLines(lines, indicator) {
93
+ if (indicator === "|")
94
+ return lines.join("\n");
95
+ let text = "";
96
+ for (let index = 0; index < lines.length; index++) {
97
+ const line = lines[index];
98
+ if (line === "") {
99
+ let blanks = 0;
100
+ while (lines[index] === "") {
101
+ blanks++;
102
+ index++;
103
+ }
104
+ index--;
105
+ text += "\n".repeat(blanks);
106
+ continue;
107
+ }
108
+ text += line;
109
+ if (index + 1 < lines.length && lines[index + 1] !== "") {
110
+ text += " ";
111
+ }
112
+ }
113
+ return text;
114
+ }
115
+ /**
116
+ * Read the block scalar that starts at `startIndex` (the line after its key).
117
+ * Lines belong to the block while they are blank or indented deeper than the
118
+ * key; the block's own indentation is taken from its first non-blank line.
119
+ */
120
+ function readBlockScalar(lines, startIndex, keyIndent, indicator) {
121
+ let index = startIndex;
122
+ let baseIndent = null;
123
+ const collected = [];
124
+ while (index < lines.length) {
125
+ const raw = lines[index].replace(/\r$/, "");
126
+ if (!raw.trim()) {
127
+ // Blank lines before the first content line belong to the YAML document,
128
+ // not to the scalar; afterwards they are paragraph breaks.
129
+ if (baseIndent !== null)
130
+ collected.push("");
131
+ index++;
132
+ continue;
133
+ }
134
+ const indent = raw.length - raw.trimStart().length;
135
+ if (indent <= keyIndent)
136
+ break;
137
+ if (baseIndent === null)
138
+ baseIndent = indent;
139
+ if (indent < baseIndent)
140
+ break;
141
+ collected.push(raw.slice(baseIndent));
142
+ index++;
143
+ }
144
+ // Trailing blank lines belong to the following key, not to the scalar's
145
+ // content (chomping decides how many line breaks survive).
146
+ while (collected.length > 0 && collected[collected.length - 1] === "") {
147
+ collected.pop();
148
+ }
149
+ if (collected.length === 0) {
150
+ return { text: "", nextIndex: index };
151
+ }
152
+ return {
153
+ text: foldBlockLines(collected, indicator),
154
+ nextIndex: index,
155
+ };
156
+ }
157
+ /**
158
+ * Read the value of a key whose value starts on the following lines: an
159
+ * indented block list (`- item`) or an indented multi-line plain scalar (folded
160
+ * with spaces).
161
+ */
162
+ function readIndentedValue(lines, startIndex, keyIndent) {
163
+ let index = startIndex;
164
+ const items = [];
165
+ const textLines = [];
166
+ let sawListItem = false;
167
+ while (index < lines.length) {
168
+ const raw = lines[index].replace(/\r$/, "");
169
+ if (!raw.trim())
170
+ break;
171
+ const indent = raw.length - raw.trimStart().length;
172
+ if (indent <= keyIndent)
173
+ break;
174
+ const trimmed = raw.trim();
175
+ if (trimmed.startsWith("- ")) {
176
+ sawListItem = true;
177
+ items.push(stripQuotes(trimmed.substring(1).trim()));
178
+ }
179
+ else if (sawListItem) {
180
+ break;
181
+ }
182
+ else {
183
+ textLines.push(trimmed);
184
+ }
185
+ index++;
186
+ }
187
+ if (sawListItem)
188
+ return { items, nextIndex: index };
189
+ if (textLines.length === 0)
190
+ return { nextIndex: index };
191
+ return { text: textLines.join(" "), nextIndex: index };
192
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Outbound image budget: the size we deliberately keep every image under before
3
+ * it goes into a request body, and the arithmetic / wording around it.
4
+ *
5
+ * ## Why a budget on top of the gateway bound
6
+ *
7
+ * `imageDimensions.ts` documents the gateway's *hard* bound (8192px per side):
8
+ * exceeding it fails the whole request, so it must never be crossed. This module
9
+ * is a second, tighter bound we impose on ourselves, aligned with Claude Code —
10
+ * 2000px per side and 5 MB of base64 payload ({@link OUTBOUND_IMAGE_MAX_BASE64_BYTES},
11
+ * their `API_IMAGE_MAX_BASE64_SIZE`). Images inside the budget are sent
12
+ * byte-for-byte; images outside it are re-encoded by `imageRewrite.ts` (via
13
+ * sharp) and only fall back to the "omitted" note when no codec is available.
14
+ *
15
+ * Two consequences worth keeping in mind:
16
+ *
17
+ * - The budget is ours, not the gateway's — it is deliberately conservative and
18
+ * may make a 3000px screenshot slightly softer. Raise it whenever we want
19
+ * more fidelity; nothing upstream depends on the exact value.
20
+ * - The byte budget is expressed twice, in the two units that matter: base64
21
+ * characters (what the request actually carries) and raw bytes (what an
22
+ * encoder produces). Base64 inflates by 4/3, so bytes-on-the-wire = raw * 4/3,
23
+ * i.e. raw = base64 * 3/4 — see {@link OUTBOUND_IMAGE_TARGET_RAW_BYTES}.
24
+ *
25
+ * ## Why raw bytes, not the whole data URL
26
+ *
27
+ * A data URL is `data:<mime>;base64,<payload>` — the prefix is metadata and is
28
+ * not part of the encoded image. Measuring the whole string would make the
29
+ * budget depend on the mime string's length and would reject images that are
30
+ * actually within budget; {@link rawBytesFromDataUrl} measures the payload only.
31
+ *
32
+ * This module holds no image codec and must stay dependency-free so it can be
33
+ * imported from anywhere in the SDK.
34
+ */
35
+ import { OUTBOUND_IMAGE_MAX_DIMENSION_PX } from "../constants/images.js";
36
+ import type { ImageDimensions } from "./imageDimensions.js";
37
+ /**
38
+ * Client-side per-side dimension budget (Claude Code's `IMAGE_MAX_WIDTH` /
39
+ * `IMAGE_MAX_HEIGHT`). Distinct from — and much tighter than — the gateway's
40
+ * hard bound in `imageDimensions.ts`. The value itself lives in
41
+ * `constants/images.ts` so the webview paste path can share it instead of
42
+ * keeping a copy that could drift; re-exported here because this is the module
43
+ * every outbound-image caller already imports.
44
+ */
45
+ export { OUTBOUND_IMAGE_MAX_DIMENSION_PX };
46
+ /**
47
+ * Maximum base64 payload we are willing to put in a request, counting only the
48
+ * characters after the `,` in the data URL. Aligned with Claude Code's
49
+ * `API_IMAGE_MAX_BASE64_SIZE`.
50
+ */
51
+ export declare const OUTBOUND_IMAGE_MAX_BASE64_BYTES: number;
52
+ /**
53
+ * The same budget in raw bytes: `5 MiB * 3 / 4`. Encoders produce raw bytes, so
54
+ * comparing their output against this number is equivalent to comparing the
55
+ * resulting base64 against {@link OUTBOUND_IMAGE_MAX_BASE64_BYTES} — it keeps
56
+ * the input side and the output side of the ladder on one scale.
57
+ */
58
+ export declare const OUTBOUND_IMAGE_TARGET_RAW_BYTES: number;
59
+ /**
60
+ * Raw (decoded) byte count implied by a data URL's base64 payload — the inverse
61
+ * of the 4/3 base64 inflation. Padding makes this overstate the real payload by
62
+ * at most 2 bytes, which errs toward rewriting an image slightly before it
63
+ * actually has to.
64
+ */
65
+ export declare function rawBytesFromDataUrl(dataUrl: string): number;
66
+ /**
67
+ * Whether an image must be rewritten before it can be sent: it carries more
68
+ * base64 than the byte budget, or (when the container header gave us
69
+ * dimensions) exceeds the per-side budget.
70
+ *
71
+ * Unknown dimensions never count as oversized — see `imageDimensions.ts`; a
72
+ * header we cannot parse means "unknown", not "too big".
73
+ */
74
+ export declare function exceedsOutboundBudget(dataUrl: string, dimensions?: ImageDimensions): boolean;
75
+ /**
76
+ * Note appended after a rewritten image so the model can map what it sees back
77
+ * to the original pixels — the same contract as Claude Code's
78
+ * `createImageMetadataText` (`src/utils/imageResizer.ts`). Kept verbatim in
79
+ * shape: original size, displayed size, and the factor to multiply by.
80
+ *
81
+ * `fit: "inside"` scales both sides by one factor, so a single ratio (taken
82
+ * from whichever side shrank more, to stay correct under rounding) applies.
83
+ * Callers only append it when the dimensions actually changed.
84
+ */
85
+ export declare function resizedImageNote(original: ImageDimensions, displayed: ImageDimensions): string;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Outbound image budget: the size we deliberately keep every image under before
3
+ * it goes into a request body, and the arithmetic / wording around it.
4
+ *
5
+ * ## Why a budget on top of the gateway bound
6
+ *
7
+ * `imageDimensions.ts` documents the gateway's *hard* bound (8192px per side):
8
+ * exceeding it fails the whole request, so it must never be crossed. This module
9
+ * is a second, tighter bound we impose on ourselves, aligned with Claude Code —
10
+ * 2000px per side and 5 MB of base64 payload ({@link OUTBOUND_IMAGE_MAX_BASE64_BYTES},
11
+ * their `API_IMAGE_MAX_BASE64_SIZE`). Images inside the budget are sent
12
+ * byte-for-byte; images outside it are re-encoded by `imageRewrite.ts` (via
13
+ * sharp) and only fall back to the "omitted" note when no codec is available.
14
+ *
15
+ * Two consequences worth keeping in mind:
16
+ *
17
+ * - The budget is ours, not the gateway's — it is deliberately conservative and
18
+ * may make a 3000px screenshot slightly softer. Raise it whenever we want
19
+ * more fidelity; nothing upstream depends on the exact value.
20
+ * - The byte budget is expressed twice, in the two units that matter: base64
21
+ * characters (what the request actually carries) and raw bytes (what an
22
+ * encoder produces). Base64 inflates by 4/3, so bytes-on-the-wire = raw * 4/3,
23
+ * i.e. raw = base64 * 3/4 — see {@link OUTBOUND_IMAGE_TARGET_RAW_BYTES}.
24
+ *
25
+ * ## Why raw bytes, not the whole data URL
26
+ *
27
+ * A data URL is `data:<mime>;base64,<payload>` — the prefix is metadata and is
28
+ * not part of the encoded image. Measuring the whole string would make the
29
+ * budget depend on the mime string's length and would reject images that are
30
+ * actually within budget; {@link rawBytesFromDataUrl} measures the payload only.
31
+ *
32
+ * This module holds no image codec and must stay dependency-free so it can be
33
+ * imported from anywhere in the SDK.
34
+ */
35
+ import { OUTBOUND_IMAGE_MAX_DIMENSION_PX } from "../constants/images.js";
36
+ import { exceedsMaxDimension } from "./imageDimensions.js";
37
+ /**
38
+ * Client-side per-side dimension budget (Claude Code's `IMAGE_MAX_WIDTH` /
39
+ * `IMAGE_MAX_HEIGHT`). Distinct from — and much tighter than — the gateway's
40
+ * hard bound in `imageDimensions.ts`. The value itself lives in
41
+ * `constants/images.ts` so the webview paste path can share it instead of
42
+ * keeping a copy that could drift; re-exported here because this is the module
43
+ * every outbound-image caller already imports.
44
+ */
45
+ export { OUTBOUND_IMAGE_MAX_DIMENSION_PX };
46
+ /**
47
+ * Maximum base64 payload we are willing to put in a request, counting only the
48
+ * characters after the `,` in the data URL. Aligned with Claude Code's
49
+ * `API_IMAGE_MAX_BASE64_SIZE`.
50
+ */
51
+ export const OUTBOUND_IMAGE_MAX_BASE64_BYTES = 5 * 1024 * 1024;
52
+ /**
53
+ * The same budget in raw bytes: `5 MiB * 3 / 4`. Encoders produce raw bytes, so
54
+ * comparing their output against this number is equivalent to comparing the
55
+ * resulting base64 against {@link OUTBOUND_IMAGE_MAX_BASE64_BYTES} — it keeps
56
+ * the input side and the output side of the ladder on one scale.
57
+ */
58
+ export const OUTBOUND_IMAGE_TARGET_RAW_BYTES = (OUTBOUND_IMAGE_MAX_BASE64_BYTES * 3) / 4;
59
+ /**
60
+ * Length of the base64 payload of a data URL, ignoring the `data:<mime>;base64,`
61
+ * prefix. A string without a comma is treated as bare payload.
62
+ */
63
+ function base64PayloadLength(dataUrl) {
64
+ const comma = dataUrl.indexOf(",");
65
+ return comma === -1 ? dataUrl.length : dataUrl.length - comma - 1;
66
+ }
67
+ /**
68
+ * Raw (decoded) byte count implied by a data URL's base64 payload — the inverse
69
+ * of the 4/3 base64 inflation. Padding makes this overstate the real payload by
70
+ * at most 2 bytes, which errs toward rewriting an image slightly before it
71
+ * actually has to.
72
+ */
73
+ export function rawBytesFromDataUrl(dataUrl) {
74
+ return Math.floor((base64PayloadLength(dataUrl) * 3) / 4);
75
+ }
76
+ /**
77
+ * Whether an image must be rewritten before it can be sent: it carries more
78
+ * base64 than the byte budget, or (when the container header gave us
79
+ * dimensions) exceeds the per-side budget.
80
+ *
81
+ * Unknown dimensions never count as oversized — see `imageDimensions.ts`; a
82
+ * header we cannot parse means "unknown", not "too big".
83
+ */
84
+ export function exceedsOutboundBudget(dataUrl, dimensions) {
85
+ if (rawBytesFromDataUrl(dataUrl) > OUTBOUND_IMAGE_TARGET_RAW_BYTES) {
86
+ return true;
87
+ }
88
+ if (dimensions &&
89
+ exceedsMaxDimension(dimensions, OUTBOUND_IMAGE_MAX_DIMENSION_PX)) {
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+ /**
95
+ * Note appended after a rewritten image so the model can map what it sees back
96
+ * to the original pixels — the same contract as Claude Code's
97
+ * `createImageMetadataText` (`src/utils/imageResizer.ts`). Kept verbatim in
98
+ * shape: original size, displayed size, and the factor to multiply by.
99
+ *
100
+ * `fit: "inside"` scales both sides by one factor, so a single ratio (taken
101
+ * from whichever side shrank more, to stay correct under rounding) applies.
102
+ * Callers only append it when the dimensions actually changed.
103
+ */
104
+ export function resizedImageNote(original, displayed) {
105
+ const ratio = Math.max(original.width / displayed.width, original.height / displayed.height);
106
+ return (`[Image: original ${original.width}x${original.height}, displayed at ` +
107
+ `${displayed.width}x${displayed.height}. Multiply coordinates by ` +
108
+ `${ratio.toFixed(2)} to map to the original image.]`);
109
+ }
@@ -0,0 +1,83 @@
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 declare const MAX_IMAGE_DIMENSION_PX = 8192;
63
+ export interface ImageDimensions {
64
+ width: number;
65
+ height: number;
66
+ }
67
+ /**
68
+ * Read the pixel dimensions straight out of the header. Supports the four
69
+ * formats the gateway accepts (png / jpeg / gif / webp).
70
+ *
71
+ * Returns `undefined` when the format is unknown or the header is truncated —
72
+ * callers must treat that as "unknown", never as "oversized".
73
+ */
74
+ export declare function getImageDimensions(bytes: Uint8Array): ImageDimensions | undefined;
75
+ export declare function getImageDimensionsFromDataUrl(dataUrl: string): ImageDimensions | undefined;
76
+ export declare function exceedsMaxDimension(dimensions: ImageDimensions, max?: number): boolean;
77
+ /**
78
+ * One-line, actionable note that replaces an image the gateway would reject.
79
+ * Modelled on opencode's `[N image omitted: ...]` placeholder: the model (and
80
+ * anyone reading the transcript) gets the real size, the bound, and the way
81
+ * out.
82
+ */
83
+ export declare function omittedImageNote(dimensions: ImageDimensions, sourcePath?: string): string;