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.
- package/dist/agent.d.ts +58 -4
- package/dist/agent.js +91 -19
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/skills/settings.js +1 -12
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +53 -13
- package/dist/core/plugin.js +134 -26
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -1
- package/dist/managers/aiManager.js +190 -21
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/hookManager.d.ts +13 -0
- package/dist/managers/hookManager.js +31 -4
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +103 -8
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +45 -10
- package/dist/managers/mcpManager.js +103 -1
- package/dist/managers/messageManager.d.ts +48 -5
- package/dist/managers/messageManager.js +107 -21
- package/dist/managers/permissionManager.d.ts +40 -0
- package/dist/managers/permissionManager.js +63 -8
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +19 -0
- package/dist/managers/skillManager.js +44 -0
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +35 -3
- package/dist/managers/subagentManager.d.ts +8 -0
- package/dist/managers/subagentManager.js +20 -0
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +318 -119
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +60 -9
- package/dist/services/configurationService.js +129 -54
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +19 -15
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +84 -0
- package/dist/services/jsonlHandler.js +209 -14
- package/dist/services/memory.d.ts +3 -1
- package/dist/services/memory.js +13 -9
- package/dist/services/officialMarketplaceMirror.js +3 -2
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +16 -2
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +144 -3
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/stdio/index.d.ts +3 -1
- package/dist/stdio/index.js +3 -1
- package/dist/stdio/notificationRouter.js +1 -0
- package/dist/stdio/stdioAgent.d.ts +14 -7
- package/dist/stdio/stdioAgent.js +19 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +8 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +20 -5
- package/dist/types/configuration.d.ts +39 -1
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +39 -0
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +15 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +43 -0
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +33 -0
- package/dist/utils/fileUtils.js +81 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -2
- package/dist/utils/messageOperations.js +40 -29
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +39 -43
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- 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;
|