wave-agent-sdk 1.0.7 → 1.0.8
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/settings/ENV.md +1 -0
- package/builtin/skills/settings/MODELS.md +3 -0
- 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/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/subagentManager.d.ts +12 -0
- package/dist/managers/subagentManager.js +32 -2
- package/dist/managers/toolManager.js +7 -0
- package/dist/services/aiService.d.ts +1 -1
- package/dist/services/aiService.js +3 -2
- package/dist/services/artifactAvailability.d.ts +7 -0
- package/dist/services/artifactAvailability.js +26 -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/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 +11 -11
- 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/convertMessagesForAPI.js +51 -4
- 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
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
import { ARTIFACT_TOOL_NAME } from "../constants/tools.js";
|
|
5
|
+
import { authService, createAuthAwareFetch } from "../services/authService.js";
|
|
6
|
+
import { logger } from "../utils/globalLogger.js";
|
|
7
|
+
import { recordArtifact, getArtifactByFilePath, getRecordedVersion, recordVersion, } from "../services/artifactSession.js";
|
|
8
|
+
// --- Limits ---
|
|
9
|
+
const DEPLOY_TIMEOUT_MS = 30000;
|
|
10
|
+
const PROBE_TIMEOUT_MS = 15000;
|
|
11
|
+
/** Server-side content limit — POSTs above this get a 413. */
|
|
12
|
+
const ARTIFACT_MAX_CONTENT_BYTES = 16 * 1024 * 1024; // 16MB
|
|
13
|
+
const LABEL_MAX_LENGTH = 60;
|
|
14
|
+
const DEFAULT_FAVICON = "📄";
|
|
15
|
+
function isValidFavicon(favicon) {
|
|
16
|
+
if (!favicon || favicon.trim().length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
// Count code points excluding variation selectors (👨👩👧 counts as 3 and is
|
|
19
|
+
// rejected — only simple 1-2 emoji are accepted per the server contract).
|
|
20
|
+
const codePoints = [...favicon].filter((cp) => cp !== "\uFE0F");
|
|
21
|
+
if (codePoints.length < 1 || codePoints.length > 2)
|
|
22
|
+
return false;
|
|
23
|
+
return codePoints.every((cp) => {
|
|
24
|
+
// \p{Emoji} also matches ASCII digits/letters — plain text is not an emoji.
|
|
25
|
+
if (/[A-Za-z0-9]/.test(cp))
|
|
26
|
+
return false;
|
|
27
|
+
return /\p{Emoji}/u.test(cp);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Extract the artifact slug from a `{host}/code/artifact/{slug}` URL. */
|
|
31
|
+
function extractSlugFromUrl(url) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(url);
|
|
34
|
+
const match = parsed.pathname.match(/^\/code\/artifact\/([^/]+)\/?$/);
|
|
35
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function escapeHtml(s) {
|
|
42
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
43
|
+
}
|
|
44
|
+
/** Render Markdown to a complete HTML document (client-side md→HTML). */
|
|
45
|
+
function renderMarkdown(md, title) {
|
|
46
|
+
const body = marked.parse(md, { async: false });
|
|
47
|
+
const titleTag = title ? `<title>${escapeHtml(title)}</title>` : "";
|
|
48
|
+
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${titleTag}</head><body>${body}</body></html>`;
|
|
49
|
+
}
|
|
50
|
+
/** Probe an artifact's current metadata (`via=model_read` marks model access). */
|
|
51
|
+
async function probeFrame(slug, signal) {
|
|
52
|
+
const serverUrl = authService.getServerUrl();
|
|
53
|
+
const authFetch = createAuthAwareFetch(globalThis.fetch);
|
|
54
|
+
try {
|
|
55
|
+
const res = await authFetch(`${serverUrl}/api/frame/${encodeURIComponent(slug)}?via=model_read`, { method: "GET", signal });
|
|
56
|
+
if (res.status === 404)
|
|
57
|
+
return null;
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
logger?.warn("Artifact probe failed", {
|
|
60
|
+
slug,
|
|
61
|
+
status: res.status,
|
|
62
|
+
statusText: res.statusText,
|
|
63
|
+
});
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return (await res.json());
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
logger?.warn("Artifact probe error", { slug, error: String(err) });
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const artifactTool = {
|
|
74
|
+
name: ARTIFACT_TOOL_NAME,
|
|
75
|
+
isConcurrencySafe: false,
|
|
76
|
+
config: {
|
|
77
|
+
type: "function",
|
|
78
|
+
function: {
|
|
79
|
+
name: ARTIFACT_TOOL_NAME,
|
|
80
|
+
description: "Publish local HTML or Markdown files as shareable web pages (artifacts). " +
|
|
81
|
+
"Each publish returns a private URL you can share; the page is only accessible to you " +
|
|
82
|
+
"unless you change its sharing. Only .html and .md files are supported — inline content " +
|
|
83
|
+
"is not accepted. Markdown files are rendered to HTML automatically. " +
|
|
84
|
+
"Pass `url` (an existing artifact URL) to redeploy that artifact, " +
|
|
85
|
+
"or omit it to republish a file already published in this session. " +
|
|
86
|
+
"Use `force` to overwrite an artifact that has been updated by someone else.",
|
|
87
|
+
parameters: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
file_path: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Path to the .html or .md file to publish (relative to the working directory). The file must exist.",
|
|
93
|
+
},
|
|
94
|
+
favicon: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "1-2 emoji characters shown as the page favicon (no text, URLs, or HTML). Defaults to 📄.",
|
|
97
|
+
},
|
|
98
|
+
label: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: `Optional short label for the artifact (max ${LABEL_MAX_LENGTH} characters).`,
|
|
101
|
+
},
|
|
102
|
+
url: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Existing artifact URL to redeploy, e.g. https://host/code/artifact/abc123. Omit when republishing a file already published earlier in this session.",
|
|
105
|
+
},
|
|
106
|
+
force: {
|
|
107
|
+
type: "boolean",
|
|
108
|
+
description: "Set true to overwrite an artifact that was updated since this session last saw it (stale version or conflict).",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: ["file_path"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
formatCompactParams: (params) => {
|
|
116
|
+
const filePath = typeof params.file_path === "string" ? params.file_path : "";
|
|
117
|
+
const url = typeof params.url === "string" ? params.url : "";
|
|
118
|
+
return `${ARTIFACT_TOOL_NAME}(${filePath}${url ? ` → ${url}` : ""})`;
|
|
119
|
+
},
|
|
120
|
+
execute: async (args, context) => {
|
|
121
|
+
const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
|
|
122
|
+
if (!filePath) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
content: "",
|
|
126
|
+
error: `${ARTIFACT_TOOL_NAME}: missing required parameter "file_path"`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const faviconRaw = typeof args.favicon === "string" ? args.favicon.trim() : "";
|
|
130
|
+
const favicon = faviconRaw || DEFAULT_FAVICON;
|
|
131
|
+
if (!isValidFavicon(favicon)) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
content: "",
|
|
135
|
+
error: `${ARTIFACT_TOOL_NAME}: favicon must be 1-2 emoji characters (e.g. "📄" or "🔖"), no text, URLs, or HTML markup`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const labelRaw = typeof args.label === "string" ? args.label.trim() : "";
|
|
139
|
+
const label = labelRaw || undefined;
|
|
140
|
+
if (label !== undefined && label.length > LABEL_MAX_LENGTH) {
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
content: "",
|
|
144
|
+
error: `${ARTIFACT_TOOL_NAME}: label must be at most ${LABEL_MAX_LENGTH} characters (got ${label.length})`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const force = args.force === true;
|
|
148
|
+
const urlRaw = typeof args.url === "string" ? args.url.trim() : "";
|
|
149
|
+
const url = urlRaw || undefined;
|
|
150
|
+
let slug;
|
|
151
|
+
if (url) {
|
|
152
|
+
slug = extractSlugFromUrl(url);
|
|
153
|
+
if (!slug) {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
content: "",
|
|
157
|
+
error: `${ARTIFACT_TOOL_NAME}: url must point to an artifact page ({host}/code/artifact/{slug})`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Resolve and read the file (relative to the workdir).
|
|
162
|
+
const absolutePath = path.resolve(context.workdir, filePath);
|
|
163
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
164
|
+
if (ext !== ".html" && ext !== ".md") {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
content: "",
|
|
168
|
+
error: `${ARTIFACT_TOOL_NAME}: only .html and .md files can be published as artifacts (got "${ext || "no extension"}")`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
let fileContent;
|
|
172
|
+
try {
|
|
173
|
+
fileContent = readFileSync(absolutePath, "utf-8");
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
content: "",
|
|
179
|
+
error: `${ARTIFACT_TOOL_NAME}: file not found or unreadable: ${filePath}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const content = ext === ".md" ? renderMarkdown(fileContent, label) : fileContent;
|
|
183
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
184
|
+
if (contentBytes > ARTIFACT_MAX_CONTENT_BYTES) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
content: "",
|
|
188
|
+
error: `${ARTIFACT_TOOL_NAME}: content exceeds the ${Math.floor(ARTIFACT_MAX_CONTENT_BYTES / 1024 / 1024)}MB server limit (${(contentBytes / 1024 / 1024).toFixed(1)}MB). Reduce the file or split it up.`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (!authService.getSSOToken()) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
content: "",
|
|
195
|
+
error: `${ARTIFACT_TOOL_NAME}: not authenticated. Run /login to connect your account before publishing artifacts.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const sessionId = context.sessionId || "";
|
|
199
|
+
const signal = context.abortSignal
|
|
200
|
+
? AbortSignal.any([
|
|
201
|
+
context.abortSignal,
|
|
202
|
+
AbortSignal.timeout(DEPLOY_TIMEOUT_MS),
|
|
203
|
+
])
|
|
204
|
+
: AbortSignal.timeout(DEPLOY_TIMEOUT_MS);
|
|
205
|
+
// Redeploy: probe current metadata for baseVersion, shared-live detection,
|
|
206
|
+
// and the stale-version guard.
|
|
207
|
+
let serverVersion;
|
|
208
|
+
let sharedLive = false;
|
|
209
|
+
if (url && slug) {
|
|
210
|
+
const meta = await probeFrame(slug, AbortSignal.timeout(PROBE_TIMEOUT_MS));
|
|
211
|
+
if (!meta) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
content: "",
|
|
215
|
+
error: `${ARTIFACT_TOOL_NAME}: artifact not found at ${url}. It may have been deleted or the URL is invalid.`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
serverVersion = meta.version;
|
|
219
|
+
sharedLive = !!(meta.perm && meta.perm.mode !== "owner" && !meta.shared);
|
|
220
|
+
const recorded = getRecordedVersion(sessionId, slug);
|
|
221
|
+
if (recorded !== undefined && recorded !== meta.version && !force) {
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
content: "",
|
|
225
|
+
error: `${ARTIFACT_TOOL_NAME}: stale version — this artifact has been updated to version ${meta.version} since this session last saw version ${recorded}. Pass "force": true to overwrite it anyway.`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Permission check: first publish and shared-live redeploys require
|
|
230
|
+
// confirmation; republishing a file/artifact this session already
|
|
231
|
+
// confirmed once auto-allows (matching Claude Code's behavior).
|
|
232
|
+
const publishedThisSession = !!getArtifactByFilePath(sessionId, filePath) ||
|
|
233
|
+
(slug ? getRecordedVersion(sessionId, slug) !== undefined : false);
|
|
234
|
+
const needsConfirm = !publishedThisSession || sharedLive;
|
|
235
|
+
if (context.permissionManager && needsConfirm) {
|
|
236
|
+
const permissionContext = context.permissionManager.createContext(ARTIFACT_TOOL_NAME, context.permissionMode || "default", context.canUseToolCallback, {
|
|
237
|
+
file_path: filePath,
|
|
238
|
+
...(favicon !== DEFAULT_FAVICON ? { favicon } : {}),
|
|
239
|
+
...(label !== undefined ? { label } : {}),
|
|
240
|
+
...(url !== undefined ? { url } : {}),
|
|
241
|
+
...(force ? { force: true } : {}),
|
|
242
|
+
}, context.toolCallId);
|
|
243
|
+
if (sharedLive) {
|
|
244
|
+
permissionContext.warning =
|
|
245
|
+
"此 artifact 处于 shared-live 状态(共享且实时更新),重新部署后所有访问者都会立即看到新内容。";
|
|
246
|
+
permissionContext.hidePersistentOption = true;
|
|
247
|
+
}
|
|
248
|
+
const permissionResult = await context.permissionManager.checkPermission(permissionContext);
|
|
249
|
+
if (permissionResult.behavior === "deny") {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
content: "",
|
|
253
|
+
error: `${ARTIFACT_TOOL_NAME} operation denied by user, reason: ${permissionResult.message || "No reason provided"}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Deploy.
|
|
258
|
+
const serverUrl = authService.getServerUrl();
|
|
259
|
+
const authFetch = createAuthAwareFetch(globalThis.fetch);
|
|
260
|
+
const body = {
|
|
261
|
+
content,
|
|
262
|
+
favicon,
|
|
263
|
+
...(label !== undefined ? { label } : {}),
|
|
264
|
+
...(url !== undefined ? { url } : {}),
|
|
265
|
+
...(serverVersion !== undefined ? { baseVersion: serverVersion } : {}),
|
|
266
|
+
...(force ? { force: true } : {}),
|
|
267
|
+
};
|
|
268
|
+
let res;
|
|
269
|
+
try {
|
|
270
|
+
res = await authFetch(`${serverUrl}/api/frame/deploy/direct`, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "Content-Type": "application/json" },
|
|
273
|
+
body: JSON.stringify(body),
|
|
274
|
+
signal,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
logger?.warn("Artifact deploy request failed", { error: String(err) });
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
content: "",
|
|
282
|
+
error: `${ARTIFACT_TOOL_NAME}: failed to reach the server: ${err instanceof Error ? err.message : String(err)}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const data = (await res.json().catch(() => ({})));
|
|
286
|
+
if (res.status === 201) {
|
|
287
|
+
const deploy = data;
|
|
288
|
+
recordArtifact(sessionId, filePath, {
|
|
289
|
+
url: deploy.url,
|
|
290
|
+
slug: deploy.slug,
|
|
291
|
+
version: deploy.version,
|
|
292
|
+
});
|
|
293
|
+
const lines = [`Artifact published: ${deploy.url}`];
|
|
294
|
+
if (deploy.path)
|
|
295
|
+
lines.push(`Path: ${deploy.path}`);
|
|
296
|
+
if (deploy.title)
|
|
297
|
+
lines.push(`Title: ${deploy.title}`);
|
|
298
|
+
lines.push(`Version: ${deploy.version}`);
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
content: lines.join("\n"),
|
|
302
|
+
shortResult: `Published ${filePath} → ${deploy.url}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (res.status === 409) {
|
|
306
|
+
const live = typeof data.live === "string" ? data.live : undefined;
|
|
307
|
+
if (live && slug) {
|
|
308
|
+
recordVersion(sessionId, slug, live);
|
|
309
|
+
}
|
|
310
|
+
const serverMessage = typeof data.message === "string"
|
|
311
|
+
? data.message
|
|
312
|
+
: typeof data.error === "string"
|
|
313
|
+
? data.error
|
|
314
|
+
: "the artifact has been updated by someone else";
|
|
315
|
+
return {
|
|
316
|
+
success: false,
|
|
317
|
+
content: "",
|
|
318
|
+
error: `${ARTIFACT_TOOL_NAME}: conflict detected — ${serverMessage}${live ? ` (live version: ${live})` : ""}. Pass "force": true to overwrite the live version.`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (res.status === 413) {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
content: "",
|
|
325
|
+
error: `${ARTIFACT_TOOL_NAME}: the published content is too large (server limit is 16MB). Reduce the file or split it up.`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (res.status === 400) {
|
|
329
|
+
const serverMessage = typeof data.message === "string"
|
|
330
|
+
? data.message
|
|
331
|
+
: typeof data.error === "string"
|
|
332
|
+
? data.error
|
|
333
|
+
: "the server rejected the content";
|
|
334
|
+
return {
|
|
335
|
+
success: false,
|
|
336
|
+
content: "",
|
|
337
|
+
error: `${ARTIFACT_TOOL_NAME}: the server rejected the publish — ${serverMessage}`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (res.status === 401 || res.status === 403) {
|
|
341
|
+
return {
|
|
342
|
+
success: false,
|
|
343
|
+
content: "",
|
|
344
|
+
error: `${ARTIFACT_TOOL_NAME}: authentication failed (HTTP ${res.status}). Run /login again and retry.`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
logger?.warn("Artifact deploy unexpected status", {
|
|
348
|
+
status: res.status,
|
|
349
|
+
statusText: res.statusText,
|
|
350
|
+
});
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
content: "",
|
|
354
|
+
error: `${ARTIFACT_TOOL_NAME}: server returned HTTP ${res.status} ${res.statusText}`,
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
};
|
package/dist/tools/bashTool.js
CHANGED
|
@@ -219,17 +219,14 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
219
219
|
const { id: taskId } = backgroundTaskManager.startShell(command, undefined, context.workdir);
|
|
220
220
|
const task = backgroundTaskManager.getTask(taskId);
|
|
221
221
|
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");
|
|
222
|
+
const backgroundMsg = outputPath
|
|
223
|
+
? `Command running in background with ID: ${taskId}. Output is being written to: ${outputPath}`
|
|
224
|
+
: `Command running in background with ID: ${taskId}. Use ${READ_TOOL_NAME} tool with task_id="${taskId}" to read the output.`;
|
|
229
225
|
return {
|
|
230
226
|
success: true,
|
|
231
227
|
content: recoveryNotice + backgroundMsg,
|
|
232
228
|
shortResult: `Background process ${taskId} started${outputPath ? ` → ${outputPath}` : ""}`,
|
|
229
|
+
backgroundTaskId: taskId,
|
|
233
230
|
};
|
|
234
231
|
}
|
|
235
232
|
// Foreground execution (original behavior)
|
|
@@ -303,9 +300,10 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
303
300
|
const outputPath = task?.outputPath;
|
|
304
301
|
resolve({
|
|
305
302
|
success: true,
|
|
306
|
-
content: `Command
|
|
303
|
+
content: `Command was manually backgrounded by user with ID: ${taskId}.${outputPath ? ` Output is being written to: ${outputPath}` : ""}`,
|
|
307
304
|
shortResult: `Process ${taskId} backgrounded`,
|
|
308
|
-
|
|
305
|
+
backgroundedByUser: true,
|
|
306
|
+
backgroundTaskId: taskId,
|
|
309
307
|
});
|
|
310
308
|
}
|
|
311
309
|
else {
|
|
@@ -337,8 +335,10 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
337
335
|
logger.info(`[Bash] Command timed out after ${timeout}ms, auto-backgrounded as ${taskId}`);
|
|
338
336
|
resolve({
|
|
339
337
|
success: true,
|
|
340
|
-
content: `Command
|
|
341
|
-
shortResult: `Process ${taskId} auto-backgrounded
|
|
338
|
+
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}` : ""}`,
|
|
339
|
+
shortResult: `Process ${taskId} auto-backgrounded`,
|
|
340
|
+
assistantAutoBackgrounded: true,
|
|
341
|
+
backgroundTaskId: taskId,
|
|
342
342
|
});
|
|
343
343
|
}
|
|
344
344
|
else {
|
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";
|