wave-agent-sdk 1.0.7 → 1.0.9
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/builtin/skills/artifact/SKILL.md +14 -0
- package/builtin/skills/settings/ENV.md +2 -1
- package/builtin/skills/settings/MODELS.md +3 -0
- package/builtin/skills/settings/SKILL.md +7 -0
- package/builtin/skills/settings/SUBAGENTS.md +1 -1
- package/builtin/subagents/vision.md +18 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/managers/aiManager.js +46 -4
- package/dist/managers/backgroundTaskManager.js +52 -4
- package/dist/managers/messageManager.js +8 -2
- package/dist/managers/permissionManager.d.ts +5 -0
- package/dist/managers/permissionManager.js +18 -2
- package/dist/managers/pluginManager.d.ts +9 -0
- package/dist/managers/pluginManager.js +20 -0
- package/dist/managers/skillManager.d.ts +13 -0
- package/dist/managers/skillManager.js +30 -0
- package/dist/managers/subagentManager.d.ts +12 -0
- package/dist/managers/subagentManager.js +32 -2
- package/dist/managers/toolManager.d.ts +8 -0
- package/dist/managers/toolManager.js +18 -0
- package/dist/services/aiService.d.ts +1 -1
- package/dist/services/aiService.js +3 -2
- package/dist/services/artifactAvailability.d.ts +9 -0
- package/dist/services/artifactAvailability.js +34 -0
- package/dist/services/artifactSession.d.ts +27 -0
- package/dist/services/artifactSession.js +52 -0
- package/dist/services/configurationService.d.ts +2 -1
- package/dist/services/configurationService.js +16 -1
- package/dist/services/initializationService.js +5 -0
- package/dist/services/remoteSettingsService.js +2 -0
- package/dist/tools/agentTool.js +2 -1
- package/dist/tools/artifactTool.d.ts +2 -0
- package/dist/tools/artifactTool.js +357 -0
- package/dist/tools/bashTool.js +24 -13
- package/dist/tools/editTool.js +7 -2
- package/dist/tools/types.d.ts +3 -1
- package/dist/tools/webFetchTool.js +141 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/configuration.d.ts +2 -0
- package/dist/types/messaging.d.ts +3 -1
- package/dist/types/permissions.d.ts +3 -1
- package/dist/types/permissions.js +2 -1
- package/dist/utils/bashParser.d.ts +14 -0
- package/dist/utils/bashParser.js +45 -1
- package/dist/utils/containerSetup.js +8 -0
- package/dist/utils/convertMessagesForAPI.js +51 -4
- package/dist/utils/encoding.d.ts +28 -0
- package/dist/utils/encoding.js +99 -0
- package/dist/utils/messageOperations.d.ts +4 -2
- package/dist/utils/messageOperations.js +23 -7
- package/dist/utils/subagentParser.d.ts +5 -2
- package/dist/utils/subagentParser.js +14 -4
- package/package.json +2 -1
package/dist/tools/bashTool.js
CHANGED
|
@@ -6,6 +6,7 @@ import { logger } from "../utils/globalLogger.js";
|
|
|
6
6
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
7
7
|
import { toPosixPath } from "../utils/path.js";
|
|
8
8
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
9
|
+
import { WindowsStreamDecoder } from "../utils/encoding.js";
|
|
9
10
|
import { processToolResult } from "../utils/toolResultStorage.js";
|
|
10
11
|
import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
|
|
11
12
|
import { BASH_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, READ_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, } from "../constants/tools.js";
|
|
@@ -219,17 +220,14 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
219
220
|
const { id: taskId } = backgroundTaskManager.startShell(command, undefined, context.workdir);
|
|
220
221
|
const task = backgroundTaskManager.getTask(taskId);
|
|
221
222
|
const outputPath = task?.outputPath;
|
|
222
|
-
const backgroundMsg =
|
|
223
|
-
`Command
|
|
224
|
-
`
|
|
225
|
-
outputPath
|
|
226
|
-
? `output_file: ${outputPath}`
|
|
227
|
-
: `Use ${READ_TOOL_NAME} tool with task_id="${taskId}" to read the output.`,
|
|
228
|
-
].join("\n");
|
|
223
|
+
const backgroundMsg = outputPath
|
|
224
|
+
? `Command running in background with ID: ${taskId}. Output is being written to: ${outputPath}`
|
|
225
|
+
: `Command running in background with ID: ${taskId}. Use ${READ_TOOL_NAME} tool with task_id="${taskId}" to read the output.`;
|
|
229
226
|
return {
|
|
230
227
|
success: true,
|
|
231
228
|
content: recoveryNotice + backgroundMsg,
|
|
232
229
|
shortResult: `Background process ${taskId} started${outputPath ? ` → ${outputPath}` : ""}`,
|
|
230
|
+
backgroundTaskId: taskId,
|
|
233
231
|
};
|
|
234
232
|
}
|
|
235
233
|
// Foreground execution (original behavior)
|
|
@@ -253,6 +251,10 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
253
251
|
let isAborted = false;
|
|
254
252
|
let isBackgrounded = false;
|
|
255
253
|
let isFinished = false;
|
|
254
|
+
// On Windows, native tools (taskkill, powershell, ...) write GBK (cp936)
|
|
255
|
+
// instead of UTF-8; decode their byte streams accordingly (issue #1753).
|
|
256
|
+
const stdoutDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
257
|
+
const stderrDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
256
258
|
// Best-effort cleanup of the temp CWD file — used by abort/error/exit paths
|
|
257
259
|
const cleanupTempFile = () => {
|
|
258
260
|
try {
|
|
@@ -303,9 +305,10 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
303
305
|
const outputPath = task?.outputPath;
|
|
304
306
|
resolve({
|
|
305
307
|
success: true,
|
|
306
|
-
content: `Command
|
|
308
|
+
content: `Command was manually backgrounded by user with ID: ${taskId}.${outputPath ? ` Output is being written to: ${outputPath}` : ""}`,
|
|
307
309
|
shortResult: `Process ${taskId} backgrounded`,
|
|
308
|
-
|
|
310
|
+
backgroundedByUser: true,
|
|
311
|
+
backgroundTaskId: taskId,
|
|
309
312
|
});
|
|
310
313
|
}
|
|
311
314
|
else {
|
|
@@ -337,8 +340,10 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
337
340
|
logger.info(`[Bash] Command timed out after ${timeout}ms, auto-backgrounded as ${taskId}`);
|
|
338
341
|
resolve({
|
|
339
342
|
success: true,
|
|
340
|
-
content: `Command
|
|
341
|
-
shortResult: `Process ${taskId} auto-backgrounded
|
|
343
|
+
content: `Command exceeded the timeout (${timeout / 1000}s) and was moved to the background with ID: ${taskId}. It is still running — you will be notified when it completes.${outputPath ? ` Output is being written to: ${outputPath}` : ""}`,
|
|
344
|
+
shortResult: `Process ${taskId} auto-backgrounded`,
|
|
345
|
+
assistantAutoBackgrounded: true,
|
|
346
|
+
backgroundTaskId: taskId,
|
|
342
347
|
});
|
|
343
348
|
}
|
|
344
349
|
else {
|
|
@@ -416,14 +421,14 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
416
421
|
}
|
|
417
422
|
child.stdout?.on("data", (data) => {
|
|
418
423
|
if (!isAborted && !isBackgrounded && !runInBackground) {
|
|
419
|
-
const chunk = stripAnsiColors(data.toString());
|
|
424
|
+
const chunk = stripAnsiColors(stdoutDecoder ? stdoutDecoder.push(data) : data.toString());
|
|
420
425
|
outputBuffer += chunk;
|
|
421
426
|
updateRealtimeResults();
|
|
422
427
|
}
|
|
423
428
|
});
|
|
424
429
|
child.stderr?.on("data", (data) => {
|
|
425
430
|
if (!isAborted && !isBackgrounded && !runInBackground) {
|
|
426
|
-
const chunk = stripAnsiColors(data.toString());
|
|
431
|
+
const chunk = stripAnsiColors(stderrDecoder ? stderrDecoder.push(data) : data.toString());
|
|
427
432
|
errorBuffer += chunk;
|
|
428
433
|
updateRealtimeResults();
|
|
429
434
|
}
|
|
@@ -467,6 +472,12 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
467
472
|
}
|
|
468
473
|
}
|
|
469
474
|
const exitCode = code ?? 0;
|
|
475
|
+
// Decode any bytes still held at stream end (e.g. a trailing UTF-8
|
|
476
|
+
// character split across the last chunk).
|
|
477
|
+
if (stdoutDecoder)
|
|
478
|
+
outputBuffer += stdoutDecoder.flush();
|
|
479
|
+
if (stderrDecoder)
|
|
480
|
+
errorBuffer += stderrDecoder.flush();
|
|
470
481
|
const combinedOutput = outputBuffer + (errorBuffer ? "\n" + errorBuffer : "");
|
|
471
482
|
// Prepend CWD change message to output if present
|
|
472
483
|
const finalOutput = recoveryNotice +
|
package/dist/tools/editTool.js
CHANGED
|
@@ -168,7 +168,11 @@ Usage:
|
|
|
168
168
|
if (replaceAll) {
|
|
169
169
|
// Replace all matches
|
|
170
170
|
const regex = new RegExp(escapeRegExp(matchedOldString), "g");
|
|
171
|
-
|
|
171
|
+
// Function replacer (claude-code's applyEditToFile approach): newString
|
|
172
|
+
// is inserted literally, never parsed as a $ replacement template. A
|
|
173
|
+
// string replacer would expand $& to the matched text, $$ to a single
|
|
174
|
+
// $, and `$` could even truncate and duplicate the file (issue #1752).
|
|
175
|
+
newContent = normalizedContent.replace(regex, () => newString);
|
|
172
176
|
replacementCount = (normalizedContent.match(regex) || []).length;
|
|
173
177
|
}
|
|
174
178
|
else {
|
|
@@ -181,7 +185,8 @@ Usage:
|
|
|
181
185
|
error: `old_string appears ${matches} times in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all=true to change every instance.`,
|
|
182
186
|
};
|
|
183
187
|
}
|
|
184
|
-
|
|
188
|
+
// Function replacer: see note above — $ in newString stays literal
|
|
189
|
+
newContent = normalizedContent.replace(matchedOldString, () => newString);
|
|
185
190
|
replacementCount = 1;
|
|
186
191
|
}
|
|
187
192
|
// Permission check after validation but before real operation
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -37,7 +37,9 @@ export interface ToolResult {
|
|
|
37
37
|
data: string;
|
|
38
38
|
mediaType?: string;
|
|
39
39
|
}>;
|
|
40
|
-
|
|
40
|
+
backgroundTaskId?: string;
|
|
41
|
+
backgroundedByUser?: boolean;
|
|
42
|
+
assistantAutoBackgrounded?: boolean;
|
|
41
43
|
metadata?: Record<string, unknown>;
|
|
42
44
|
}
|
|
43
45
|
export interface ToolContext {
|
|
@@ -2,12 +2,18 @@ import TurndownService from "turndown";
|
|
|
2
2
|
import { LRUCache } from "lru-cache";
|
|
3
3
|
import { WEB_FETCH_TOOL_NAME } from "../constants/tools.js";
|
|
4
4
|
import { logger } from "../utils/globalLogger.js";
|
|
5
|
+
import { isArtifactEnabled } from "../services/artifactAvailability.js";
|
|
6
|
+
import { authService, createAuthAwareFetch } from "../services/authService.js";
|
|
7
|
+
import { recordVersion } from "../services/artifactSession.js";
|
|
8
|
+
import { buildPersistedOutputMessage, generatePreview, persistToolResult, } from "../utils/toolResultStorage.js";
|
|
5
9
|
// --- Security Limits ---
|
|
6
10
|
const MAX_HTTP_CONTENT_LENGTH = 10 * 1024 * 1024; // 10MB
|
|
7
11
|
const FETCH_TIMEOUT_MS = 60000; // 60s
|
|
8
12
|
const MAX_REDIRECTS = 10;
|
|
9
13
|
const MAX_MARKDOWN_LENGTH = 100000;
|
|
10
14
|
const USER_AGENT = "Wave-User (+https://github.com/netease-lcap/wave-agent)";
|
|
15
|
+
/** Artifact HTML beyond this size is persisted to a temp file (path + head preview). */
|
|
16
|
+
const ARTIFACT_PREVIEW_BYTES = 2 * 1024; // ~2KB
|
|
11
17
|
// --- Cache (LRU with 15min TTL, 50MB max) ---
|
|
12
18
|
const CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
13
19
|
const CACHE_MAX_BYTES = 50 * 1024 * 1024; // 50MB
|
|
@@ -73,6 +79,124 @@ function isPermittedRedirect(originalUrl, redirectUrl) {
|
|
|
73
79
|
return false;
|
|
74
80
|
}
|
|
75
81
|
}
|
|
82
|
+
// --- Artifact read channel ---
|
|
83
|
+
/** Extract the artifact slug from a `{host}/code/artifact/{slug}` URL. */
|
|
84
|
+
function extractArtifactSlug(url) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = new URL(url);
|
|
87
|
+
const match = parsed.pathname.match(/^\/code\/artifact\/([^/]+)\/?$/);
|
|
88
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetch artifact metadata (`via=model_read`) then the HTML content (Bearer).
|
|
96
|
+
* Returns markdown (HTML converted via turndown) on success.
|
|
97
|
+
*/
|
|
98
|
+
async function readArtifact(url, slug, abortSignal) {
|
|
99
|
+
const serverUrl = authService.getServerUrl();
|
|
100
|
+
const authFetch = createAuthAwareFetch(globalThis.fetch);
|
|
101
|
+
const signal = abortSignal
|
|
102
|
+
? AbortSignal.any([abortSignal, AbortSignal.timeout(FETCH_TIMEOUT_MS)])
|
|
103
|
+
: AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
104
|
+
let meta;
|
|
105
|
+
try {
|
|
106
|
+
const metaRes = await authFetch(`${serverUrl}/api/frame/${encodeURIComponent(slug)}?via=model_read`, { method: "GET", signal });
|
|
107
|
+
if (metaRes.status === 404) {
|
|
108
|
+
return {
|
|
109
|
+
kind: "error",
|
|
110
|
+
error: `Artifact not found: ${url} (it may have been deleted)`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!metaRes.ok) {
|
|
114
|
+
return {
|
|
115
|
+
kind: "error",
|
|
116
|
+
error: `Failed to fetch artifact metadata: ${metaRes.status} ${metaRes.statusText}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
meta = (await metaRes.json());
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger?.warn("Artifact read metadata failed", {
|
|
123
|
+
slug,
|
|
124
|
+
error: String(error),
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
kind: "error",
|
|
128
|
+
error: `Failed to fetch artifact metadata: ${error instanceof Error ? error.message : String(error)}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const contentUrl = typeof meta.contentUrl === "string" ? meta.contentUrl : "";
|
|
132
|
+
const version = typeof meta.version === "string" ? meta.version : "";
|
|
133
|
+
if (!contentUrl) {
|
|
134
|
+
return {
|
|
135
|
+
kind: "error",
|
|
136
|
+
error: "Artifact metadata did not include a contentUrl",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
let html;
|
|
140
|
+
try {
|
|
141
|
+
const contentRes = await authFetch(new URL(contentUrl, serverUrl).toString(), { method: "GET", signal });
|
|
142
|
+
if (!contentRes.ok) {
|
|
143
|
+
return {
|
|
144
|
+
kind: "error",
|
|
145
|
+
error: `Failed to fetch artifact content: ${contentRes.status} ${contentRes.statusText}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
html = await contentRes.text();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
logger?.warn("Artifact read content failed", {
|
|
152
|
+
slug,
|
|
153
|
+
error: String(error),
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
kind: "error",
|
|
157
|
+
error: `Failed to fetch artifact content: ${error instanceof Error ? error.message : String(error)}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const turndownService = new TurndownService();
|
|
161
|
+
const markdown = turndownService.turndown(html);
|
|
162
|
+
return {
|
|
163
|
+
kind: "ok",
|
|
164
|
+
slug,
|
|
165
|
+
version,
|
|
166
|
+
markdown,
|
|
167
|
+
bytes: new TextEncoder().encode(markdown).length,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Process an artifact read result: persist content >~2KB to a temp file
|
|
172
|
+
* (return file path + head preview), run the prompt, attach artifactRead metadata.
|
|
173
|
+
*/
|
|
174
|
+
async function processArtifactRead(url, prompt, markdown, slug, version, context) {
|
|
175
|
+
const bytes = new TextEncoder().encode(markdown).length;
|
|
176
|
+
let aiInput = markdown;
|
|
177
|
+
let persistedMessage = "";
|
|
178
|
+
if (bytes > ARTIFACT_PREVIEW_BYTES) {
|
|
179
|
+
const filePath = persistToolResult(markdown, "artifact");
|
|
180
|
+
if (filePath) {
|
|
181
|
+
persistedMessage = buildPersistedOutputMessage(markdown.length, filePath, generatePreview(markdown));
|
|
182
|
+
aiInput = persistedMessage;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
aiInput =
|
|
186
|
+
markdown.substring(0, MAX_MARKDOWN_LENGTH) +
|
|
187
|
+
"\n\n... (content truncated, failed to persist full output)";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const result = await processWithAI(url, prompt, aiInput, 200, "OK", context, bytes);
|
|
191
|
+
result.metadata = { artifactRead: { slug, ver: version } };
|
|
192
|
+
if (persistedMessage) {
|
|
193
|
+
// Append the persisted-output message so the model can Read the full content.
|
|
194
|
+
result.content = result.content
|
|
195
|
+
? result.content + "\n\n" + persistedMessage
|
|
196
|
+
: persistedMessage;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
76
200
|
// --- Tool ---
|
|
77
201
|
export const webFetchTool = {
|
|
78
202
|
name: WEB_FETCH_TOOL_NAME,
|
|
@@ -141,6 +265,23 @@ Usage notes:
|
|
|
141
265
|
error: validation.error,
|
|
142
266
|
};
|
|
143
267
|
}
|
|
268
|
+
// Artifact URL interception: {host}/code/artifact/{slug} goes through the
|
|
269
|
+
// dedicated read channel (via=model_read + Bearer) instead of a public fetch.
|
|
270
|
+
// Only when the Artifact tool is enabled (spec 6.4: disabled = no interception).
|
|
271
|
+
if (isArtifactEnabled(context.workdir)) {
|
|
272
|
+
const artifactSlug = extractArtifactSlug(url);
|
|
273
|
+
if (artifactSlug) {
|
|
274
|
+
const readResult = await readArtifact(url, artifactSlug, context.abortSignal);
|
|
275
|
+
if (readResult.kind === "error") {
|
|
276
|
+
return { success: false, content: "", error: readResult.error };
|
|
277
|
+
}
|
|
278
|
+
if (context.sessionId && readResult.version) {
|
|
279
|
+
// Keep the stale-version guard in sync with what the model has seen.
|
|
280
|
+
recordVersion(context.sessionId, artifactSlug, readResult.version);
|
|
281
|
+
}
|
|
282
|
+
return processArtifactRead(url, prompt, readResult.markdown, artifactSlug, readResult.version, context);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
144
285
|
try {
|
|
145
286
|
const cached = cache.get(url);
|
|
146
287
|
if (cached) {
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface AgentOptions {
|
|
|
25
25
|
fetch?: ClientOptions["fetch"];
|
|
26
26
|
model?: string;
|
|
27
27
|
fastModel?: string;
|
|
28
|
+
/** Vision-capable model used by the builtin vision subagent (resolved from WAVE_VISION_MODEL env var). */
|
|
29
|
+
visionModel?: string;
|
|
28
30
|
maxInputTokens?: number;
|
|
29
31
|
maxTokens?: number;
|
|
30
32
|
/** Preferred language for agent communication */
|
package/dist/types/config.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface ModelCapabilities {
|
|
|
22
22
|
export interface ModelConfig {
|
|
23
23
|
model?: string;
|
|
24
24
|
fastModel?: string;
|
|
25
|
+
/** Vision-capable model for image recognition subagents (resolved from WAVE_VISION_MODEL env var). */
|
|
26
|
+
visionModel?: string;
|
|
25
27
|
maxTokens?: number;
|
|
26
28
|
permissionMode?: PermissionMode;
|
|
27
29
|
capabilities?: ModelCapabilities;
|
|
@@ -55,6 +55,8 @@ export interface WaveConfiguration {
|
|
|
55
55
|
/** Base ref for new worktrees: "fresh" (origin/<default-branch>, default) | "head" (local HEAD) */
|
|
56
56
|
baseRef?: "fresh" | "head";
|
|
57
57
|
};
|
|
58
|
+
/** Whether the Artifact tool is enabled. Unset follows the code default constant (ARTIFACT_DEFAULT_ENABLED). */
|
|
59
|
+
enableArtifact?: boolean;
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
60
62
|
* Legacy alias for backward compatibility - will be deprecated
|
|
@@ -52,7 +52,9 @@ export interface ToolBlock {
|
|
|
52
52
|
error?: string | Error;
|
|
53
53
|
compactParams?: string;
|
|
54
54
|
parametersChunk?: string;
|
|
55
|
-
|
|
55
|
+
backgroundTaskId?: string;
|
|
56
|
+
backgroundedByUser?: boolean;
|
|
57
|
+
assistantAutoBackgrounded?: boolean;
|
|
56
58
|
timestamp?: number;
|
|
57
59
|
}
|
|
58
60
|
export interface ImageBlock {
|
|
@@ -36,9 +36,11 @@ export interface ToolPermissionContext {
|
|
|
36
36
|
toolCallId?: string;
|
|
37
37
|
/** The content of the plan being exited from */
|
|
38
38
|
planContent?: string;
|
|
39
|
+
/** Optional warning line to surface in the confirmation UI (e.g. shared-live redeploy impact) */
|
|
40
|
+
warning?: string;
|
|
39
41
|
}
|
|
40
42
|
/** List of tools that require permission checks in default mode */
|
|
41
|
-
export declare const RESTRICTED_TOOLS: readonly ["Edit", "Bash", "Write", "EnterPlanMode", "ExitPlanMode", "AskUserQuestion"];
|
|
43
|
+
export declare const RESTRICTED_TOOLS: readonly ["Edit", "Bash", "Write", "EnterPlanMode", "ExitPlanMode", "AskUserQuestion", "Artifact"];
|
|
42
44
|
/** Type for restricted tool names */
|
|
43
45
|
export type RestrictedTool = (typeof RESTRICTED_TOOLS)[number];
|
|
44
46
|
export declare const OPERATION_CANCELLED_BY_USER = "Operation cancelled by user";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Permission system types for Wave Agent SDK
|
|
3
3
|
* Dependencies: None
|
|
4
4
|
*/
|
|
5
|
-
import { EDIT_TOOL_NAME, BASH_TOOL_NAME, WRITE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "../constants/tools.js";
|
|
5
|
+
import { EDIT_TOOL_NAME, BASH_TOOL_NAME, WRITE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, ARTIFACT_TOOL_NAME, } from "../constants/tools.js";
|
|
6
6
|
/** List of tools that require permission checks in default mode */
|
|
7
7
|
export const RESTRICTED_TOOLS = [
|
|
8
8
|
EDIT_TOOL_NAME,
|
|
@@ -11,5 +11,6 @@ export const RESTRICTED_TOOLS = [
|
|
|
11
11
|
ENTER_PLAN_MODE_TOOL_NAME,
|
|
12
12
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
13
13
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
14
|
+
ARTIFACT_TOOL_NAME,
|
|
14
15
|
];
|
|
15
16
|
export const OPERATION_CANCELLED_BY_USER = "Operation cancelled by user";
|
|
@@ -46,6 +46,12 @@ export interface ToolRule {
|
|
|
46
46
|
depth: number;
|
|
47
47
|
scopeFlags?: string[];
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Global scope flags for git that only change the target repository or
|
|
51
|
+
* configuration, not the subcommand being run. Shared by TOOL_RULES (smart
|
|
52
|
+
* prefix extraction) and stripGitScopePrefix (rule matching).
|
|
53
|
+
*/
|
|
54
|
+
export declare const GIT_SCOPE_FLAGS: string[];
|
|
49
55
|
export declare const TOOL_RULES: Record<string, ToolRule>;
|
|
50
56
|
/**
|
|
51
57
|
* Registry of dangerous subcommands for specific tools.
|
|
@@ -71,6 +77,14 @@ export declare function hasProcessSubstitution(command: string): boolean;
|
|
|
71
77
|
* sed -i modifies files in place and must NOT be auto-allowed. FR-019.5.
|
|
72
78
|
*/
|
|
73
79
|
export declare function hasSedInPlace(command: string): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Removes leading git global scope flags (e.g. `git -C <path>`, `git -c <key>=<value>`,
|
|
82
|
+
* `git --git-dir <path>`) from a command string, so that `git -C /tmp/foo status` is
|
|
83
|
+
* classified the same as `git status`. Only the leading sequence before the git
|
|
84
|
+
* subcommand is stripped; the remainder is re-joined with single spaces.
|
|
85
|
+
* Returns the input unchanged when nothing is stripped.
|
|
86
|
+
*/
|
|
87
|
+
export declare function stripGitScopePrefix(command: string): string;
|
|
74
88
|
/**
|
|
75
89
|
* Extracts a "smart prefix" from a bash command based on common developer tools.
|
|
76
90
|
* Returns null if the command is blacklisted or cannot be safely prefix-matched.
|
package/dist/utils/bashParser.js
CHANGED
|
@@ -500,6 +500,18 @@ export const READ_ONLY_COMMANDS = [
|
|
|
500
500
|
"bc",
|
|
501
501
|
"sleep",
|
|
502
502
|
];
|
|
503
|
+
/**
|
|
504
|
+
* Global scope flags for git that only change the target repository or
|
|
505
|
+
* configuration, not the subcommand being run. Shared by TOOL_RULES (smart
|
|
506
|
+
* prefix extraction) and stripGitScopePrefix (rule matching).
|
|
507
|
+
*/
|
|
508
|
+
export const GIT_SCOPE_FLAGS = [
|
|
509
|
+
"-C",
|
|
510
|
+
"-c",
|
|
511
|
+
"--directory",
|
|
512
|
+
"--work-tree",
|
|
513
|
+
"--git-dir",
|
|
514
|
+
];
|
|
503
515
|
export const TOOL_RULES = {
|
|
504
516
|
// Node/JS
|
|
505
517
|
npm: { depth: 2, scopeFlags: ["--prefix", "-C", "--registry"] },
|
|
@@ -517,7 +529,7 @@ export const TOOL_RULES = {
|
|
|
517
529
|
// Git
|
|
518
530
|
git: {
|
|
519
531
|
depth: 2,
|
|
520
|
-
scopeFlags:
|
|
532
|
+
scopeFlags: GIT_SCOPE_FLAGS,
|
|
521
533
|
},
|
|
522
534
|
// Python
|
|
523
535
|
python: { depth: 2 },
|
|
@@ -670,6 +682,38 @@ export function hasSedInPlace(command) {
|
|
|
670
682
|
return false;
|
|
671
683
|
return tokens.some((token) => /^-i(\..*)?$/.test(token));
|
|
672
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Removes leading git global scope flags (e.g. `git -C <path>`, `git -c <key>=<value>`,
|
|
687
|
+
* `git --git-dir <path>`) from a command string, so that `git -C /tmp/foo status` is
|
|
688
|
+
* classified the same as `git status`. Only the leading sequence before the git
|
|
689
|
+
* subcommand is stripped; the remainder is re-joined with single spaces.
|
|
690
|
+
* Returns the input unchanged when nothing is stripped.
|
|
691
|
+
*/
|
|
692
|
+
export function stripGitScopePrefix(command) {
|
|
693
|
+
const trimmed = command.trim();
|
|
694
|
+
if (!/^git(?:\s|$)/.test(trimmed))
|
|
695
|
+
return command;
|
|
696
|
+
const tokens = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
697
|
+
if (tokens.length === 0 || tokens[0] !== "git")
|
|
698
|
+
return command;
|
|
699
|
+
let i = 1;
|
|
700
|
+
while (i < tokens.length) {
|
|
701
|
+
const token = tokens[i];
|
|
702
|
+
const eqIndex = token.indexOf("=");
|
|
703
|
+
const flag = eqIndex > 0 ? token.slice(0, eqIndex) : token;
|
|
704
|
+
if (!GIT_SCOPE_FLAGS.includes(flag))
|
|
705
|
+
break;
|
|
706
|
+
if (eqIndex > 0) {
|
|
707
|
+
i++; // --flag=value form carries its own value
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
i += 2; // skip the flag and its argument
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (i === 1)
|
|
714
|
+
return command;
|
|
715
|
+
return ["git", ...tokens.slice(i)].join(" ");
|
|
716
|
+
}
|
|
673
717
|
/**
|
|
674
718
|
* Extracts a "smart prefix" from a bash command based on common developer tools.
|
|
675
719
|
* Returns null if the command is blacklisted or cannot be safely prefix-matched.
|
|
@@ -219,6 +219,14 @@ export function setupAgentContainer(setupOptions) {
|
|
|
219
219
|
onReload: () => {
|
|
220
220
|
const models = configurationService.getConfiguredModels();
|
|
221
221
|
callbacks.onConfiguredModelsChange?.(models);
|
|
222
|
+
// Re-evaluate feature-gated tools (e.g. Artifact behind
|
|
223
|
+
// enableArtifact) so toggling the flag applies without a restart.
|
|
224
|
+
toolManager.reloadFeatureGatedTools();
|
|
225
|
+
// Same gate for the builtin /artifact skill: refresh emits "refreshed"
|
|
226
|
+
// so slash-command registration follows enableArtifact.
|
|
227
|
+
void skillManager.reloadFeatureGatedSkills().catch((error) => {
|
|
228
|
+
logger.error("Failed to reload feature-gated skills:", error);
|
|
229
|
+
});
|
|
222
230
|
},
|
|
223
231
|
});
|
|
224
232
|
container.register("LiveConfigManager", liveConfigManager);
|
|
@@ -158,14 +158,38 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
158
158
|
tool_calls = undefined;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
// Construct assistant message - only add if there is meaningful content
|
|
161
|
+
// Construct assistant message - only add if there is meaningful content,
|
|
162
|
+
// tool calls, or reasoning content. Reasoning-only messages (a truncated
|
|
163
|
+
// turn that produced nothing but thinking) must be preserved so the next
|
|
164
|
+
// round can continue from the previous reasoning instead of starting
|
|
165
|
+
// over (aligned with Claude Code's thinking trajectory preservation).
|
|
162
166
|
const hasContent = content && content.trim().length > 0;
|
|
163
167
|
const hasToolCalls = tool_calls && tool_calls.length > 0;
|
|
164
|
-
|
|
168
|
+
const hasReasoning = reasoning_content && reasoning_content.trim().length > 0;
|
|
169
|
+
if (hasContent || hasToolCalls || hasReasoning) {
|
|
170
|
+
// OpenAI-compatible upstreams reject assistant messages where neither
|
|
171
|
+
// content nor tool_calls is set — a reasoning_content-only message
|
|
172
|
+
// gets stripped and returns 400 "content or tool_calls must be set"
|
|
173
|
+
// (this is fine for Claude Code, whose thinking blocks ARE content).
|
|
174
|
+
// When a turn produced only thinking (interrupted/truncated
|
|
175
|
+
// mid-reasoning), the thinking stays on its native reasoning_content
|
|
176
|
+
// field (the channel reasoning models natively continue from), and
|
|
177
|
+
// content carries an explanatory note so the request stays valid and
|
|
178
|
+
// the model knows the thinking was cut off and auto-preserved.
|
|
179
|
+
// Tool calls are meaningful content of their own — a reasoning +
|
|
180
|
+
// tool-call turn is not truncated, so no note is injected.
|
|
181
|
+
const fallbackContent = hasContent
|
|
182
|
+
? content
|
|
183
|
+
: hasReasoning && !hasToolCalls
|
|
184
|
+
? "[Note: The reasoning above was cut off before completion. It was auto-preserved from an interrupted or truncated turn — continue from where it left off, or disregard it if no longer relevant.]"
|
|
185
|
+
: undefined;
|
|
165
186
|
const assistantMessage = {
|
|
166
187
|
role: "assistant",
|
|
167
|
-
content:
|
|
188
|
+
content: fallbackContent,
|
|
168
189
|
tool_calls,
|
|
190
|
+
// Sent whenever reasoning exists: alongside real text it is the
|
|
191
|
+
// turn's own thinking; in the fallback case it carries the
|
|
192
|
+
// preserved thinking itself (content only holds the note).
|
|
169
193
|
...(reasoning_content ? { reasoning_content } : {}),
|
|
170
194
|
...(message.additionalFields ? { ...message.additionalFields } : {}),
|
|
171
195
|
};
|
|
@@ -196,12 +220,25 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
196
220
|
type: "text",
|
|
197
221
|
text: "[User shared an image, but the current model does not support image recognition]",
|
|
198
222
|
});
|
|
223
|
+
// Append source path metadata for local-file images so the main
|
|
224
|
+
// model can delegate recognition to a vision subagent (which
|
|
225
|
+
// reads the path with the Read tool). Inline dataURLs are
|
|
226
|
+
// skipped — only persisted paths are delegatable.
|
|
227
|
+
block.imageUrls.forEach((imageUrl) => {
|
|
228
|
+
if (!imageUrl.startsWith("data:image/")) {
|
|
229
|
+
contentParts.push({
|
|
230
|
+
type: "text",
|
|
231
|
+
text: `[Image source: ${imageUrl}]`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
199
235
|
}
|
|
200
236
|
else {
|
|
201
237
|
block.imageUrls.forEach((imageUrl) => {
|
|
202
238
|
// Check if it's already base64, convert if not
|
|
239
|
+
const isDataUrl = imageUrl.startsWith("data:image/");
|
|
203
240
|
let finalImageUrl = imageUrl;
|
|
204
|
-
if (!
|
|
241
|
+
if (!isDataUrl) {
|
|
205
242
|
// If it's a file path, it needs to be converted to base64
|
|
206
243
|
try {
|
|
207
244
|
finalImageUrl = convertImageToBase64(imageUrl);
|
|
@@ -219,6 +256,16 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
219
256
|
detail: "auto",
|
|
220
257
|
},
|
|
221
258
|
});
|
|
259
|
+
// Aligned with Claude Code: when the image comes from a local
|
|
260
|
+
// file (not an inline dataURL), append its source path as text
|
|
261
|
+
// metadata so the model can reference the file with tools
|
|
262
|
+
// (e.g. a vision-model subagent reading the image).
|
|
263
|
+
if (!isDataUrl) {
|
|
264
|
+
contentParts.push({
|
|
265
|
+
type: "text",
|
|
266
|
+
text: `[Image source: ${imageUrl}]`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
222
269
|
});
|
|
223
270
|
}
|
|
224
271
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming byte-to-text decoder for bash tool output on Windows.
|
|
3
|
+
*
|
|
4
|
+
* Git Bash (MSYS) tools emit UTF-8, but native Windows programs (taskkill,
|
|
5
|
+
* powershell, ping, ...) write the system OEM code page — GBK (cp936) on
|
|
6
|
+
* zh-CN systems. Node's default `data.toString()` decodes every stream as
|
|
7
|
+
* UTF-8, so GBK bytes turn into U+FFFD mojibake (issue #1753).
|
|
8
|
+
*
|
|
9
|
+
* Strategy (per-chunk buffering, decide-once):
|
|
10
|
+
* - Accumulate raw bytes; try strict UTF-8 (`fatal: true`) over everything
|
|
11
|
+
* buffered so far. If it decodes cleanly, emit the text.
|
|
12
|
+
* - If strict UTF-8 fails, hold back up to 3 trailing bytes (a UTF-8
|
|
13
|
+
* character split across chunk boundaries) and retry on the next chunk.
|
|
14
|
+
* - Once even the head is not valid UTF-8, the stream is GBK: re-decode all
|
|
15
|
+
* buffered bytes with GBK and switch to GBK for every subsequent chunk.
|
|
16
|
+
* - `flush()` decodes any leftover buffered bytes (leniently) at stream end.
|
|
17
|
+
*/
|
|
18
|
+
export declare class WindowsStreamDecoder {
|
|
19
|
+
private pending;
|
|
20
|
+
private gbkDecoder;
|
|
21
|
+
/** Longest possible incomplete UTF-8 tail at a chunk boundary is 3 bytes. */
|
|
22
|
+
private static readonly MAX_UTF8_TRAIL_BYTES;
|
|
23
|
+
push(data: Buffer): string;
|
|
24
|
+
/** Decode any bytes still held at stream end (lenient UTF-8, else GBK). */
|
|
25
|
+
flush(): string;
|
|
26
|
+
}
|
|
27
|
+
/** Decode a single complete byte sequence (used for non-streaming reads). */
|
|
28
|
+
export declare function decodeBytes(buf: Buffer): string;
|