wave-agent-sdk 1.0.6 → 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/agent.d.ts +11 -0
- package/dist/agent.js +15 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/managers/aiManager.js +46 -7
- package/dist/managers/messageManager.d.ts +1 -1
- package/dist/managers/messageManager.js +9 -3
- 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 +3 -4
- package/dist/services/aiService.js +7 -20
- 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 +9 -1
- package/dist/services/configurationService.js +26 -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/core.d.ts +1 -34
- package/dist/types/index.d.ts +1 -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/cacheControlUtils.d.ts +5 -27
- package/dist/utils/cacheControlUtils.js +12 -73
- 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,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped state shared between the Artifact tool and the WebFetch tool.
|
|
3
|
+
*
|
|
4
|
+
* - file_path → artifact record: lets a model republish a file it already
|
|
5
|
+
* published this session without passing `url` again, and enables the
|
|
6
|
+
* same-session auto-allow (the publish was already confirmed once).
|
|
7
|
+
* - slug → latest known version: powers the stale-version guard
|
|
8
|
+
* (local knowledge older than the live version blocks a redeploy
|
|
9
|
+
* unless `force` is set) and lets WebFetch record versions it observed.
|
|
10
|
+
*
|
|
11
|
+
* State is keyed by sessionId so parallel sessions never observe each other.
|
|
12
|
+
*/
|
|
13
|
+
const sessionArtifacts = new Map();
|
|
14
|
+
const sessionSlugVersions = new Map();
|
|
15
|
+
function fileMapFor(sessionId) {
|
|
16
|
+
let map = sessionArtifacts.get(sessionId);
|
|
17
|
+
if (!map) {
|
|
18
|
+
map = new Map();
|
|
19
|
+
sessionArtifacts.set(sessionId, map);
|
|
20
|
+
}
|
|
21
|
+
return map;
|
|
22
|
+
}
|
|
23
|
+
function slugMapFor(sessionId) {
|
|
24
|
+
let map = sessionSlugVersions.get(sessionId);
|
|
25
|
+
if (!map) {
|
|
26
|
+
map = new Map();
|
|
27
|
+
sessionSlugVersions.set(sessionId, map);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
}
|
|
31
|
+
/** Record a successful publish so same-session republishes auto-allow. */
|
|
32
|
+
export function recordArtifact(sessionId, filePath, record) {
|
|
33
|
+
fileMapFor(sessionId).set(filePath, record);
|
|
34
|
+
slugMapFor(sessionId).set(record.slug, record.version);
|
|
35
|
+
}
|
|
36
|
+
/** Look up an artifact previously published in this session by file path. */
|
|
37
|
+
export function getArtifactByFilePath(sessionId, filePath) {
|
|
38
|
+
return sessionArtifacts.get(sessionId)?.get(filePath);
|
|
39
|
+
}
|
|
40
|
+
/** Latest version of a slug this session has observed (publish, conflict, or read). */
|
|
41
|
+
export function getRecordedVersion(sessionId, slug) {
|
|
42
|
+
return sessionSlugVersions.get(sessionId)?.get(slug);
|
|
43
|
+
}
|
|
44
|
+
/** Record an observed version (from a publish, a 409 conflict, or a WebFetch read). */
|
|
45
|
+
export function recordVersion(sessionId, slug, version) {
|
|
46
|
+
slugMapFor(sessionId).set(slug, version);
|
|
47
|
+
}
|
|
48
|
+
/** Clear all session-scoped artifact state (used when a session ends). */
|
|
49
|
+
export function clearArtifactSession(sessionId) {
|
|
50
|
+
sessionArtifacts.delete(sessionId);
|
|
51
|
+
sessionSlugVersions.delete(sessionId);
|
|
52
|
+
}
|
|
@@ -81,9 +81,10 @@ export declare class ConfigurationService {
|
|
|
81
81
|
* @param fastModel - Fast model override (optional)
|
|
82
82
|
* @param maxTokens - Max output tokens override (optional)
|
|
83
83
|
* @param permissionMode - Permission mode override (optional)
|
|
84
|
+
* @param visionModel - Vision model override (optional)
|
|
84
85
|
* @returns Resolved model configuration with defaults
|
|
85
86
|
*/
|
|
86
|
-
resolveModelConfig(model?: string, fastModel?: string, maxTokens?: number, permissionMode?: PermissionMode): ModelConfig;
|
|
87
|
+
resolveModelConfig(model?: string, fastModel?: string, maxTokens?: number, permissionMode?: PermissionMode, visionModel?: string): ModelConfig;
|
|
87
88
|
/**
|
|
88
89
|
* Resolves token limit with fallbacks
|
|
89
90
|
* Resolution priority: override > options > env (from settings.json) > process.env > default
|
|
@@ -196,6 +197,13 @@ export declare function mergeEnvironmentConfig(userEnv: Record<string, string> |
|
|
|
196
197
|
* Supports both hooks and environment variables with proper validation
|
|
197
198
|
*/
|
|
198
199
|
export declare function loadWaveConfigFromFile(filePath: string): WaveConfiguration | null;
|
|
200
|
+
/**
|
|
201
|
+
* Load the user-level (~/.wave/settings.json) `env` block only — no workdir
|
|
202
|
+
* dependency. Used at daemon startup to apply WAVE_SERVER_URL before any agent
|
|
203
|
+
* initializes: auth queries (getAuthStatus) can run before the first agent,
|
|
204
|
+
* and AuthService falls back to the default URL otherwise.
|
|
205
|
+
*/
|
|
206
|
+
export declare function loadUserConfigEnv(): Record<string, string>;
|
|
199
207
|
/**
|
|
200
208
|
* Load and merge Wave configuration from both user and project sources
|
|
201
209
|
* Project configuration takes precedence over user configuration
|
|
@@ -459,9 +459,10 @@ export class ConfigurationService {
|
|
|
459
459
|
* @param fastModel - Fast model override (optional)
|
|
460
460
|
* @param maxTokens - Max output tokens override (optional)
|
|
461
461
|
* @param permissionMode - Permission mode override (optional)
|
|
462
|
+
* @param visionModel - Vision model override (optional)
|
|
462
463
|
* @returns Resolved model configuration with defaults
|
|
463
464
|
*/
|
|
464
|
-
resolveModelConfig(model, fastModel, maxTokens, permissionMode) {
|
|
465
|
+
resolveModelConfig(model, fastModel, maxTokens, permissionMode, visionModel) {
|
|
465
466
|
// Resolve agent model: override > options > currentConfiguration (settings.json model, possibly remote-merged) > process.env
|
|
466
467
|
// Priority: user's explicit model field > admin's env.WAVE_MODEL default.
|
|
467
468
|
// If admin wants hard enforcement, they set the `model` scalar field (overwrites local in mergeRemoteSettings).
|
|
@@ -473,11 +474,16 @@ export class ConfigurationService {
|
|
|
473
474
|
const resolvedFastModel = fastModel ||
|
|
474
475
|
this.options.fastModel ||
|
|
475
476
|
(this.envSnapshot.WAVE_FAST_MODEL ?? process.env.WAVE_FAST_MODEL);
|
|
477
|
+
// Resolve vision model: override > options > process.env (includes settings.json env)
|
|
478
|
+
const resolvedVisionModel = visionModel ||
|
|
479
|
+
this.options.visionModel ||
|
|
480
|
+
(this.envSnapshot.WAVE_VISION_MODEL ?? process.env.WAVE_VISION_MODEL);
|
|
476
481
|
// Resolve max output tokens
|
|
477
482
|
const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
|
|
478
483
|
const baseConfig = {
|
|
479
484
|
model: resolvedAgentModel,
|
|
480
485
|
fastModel: resolvedFastModel,
|
|
486
|
+
visionModel: resolvedVisionModel,
|
|
481
487
|
maxTokens: resolvedMaxTokens,
|
|
482
488
|
permissionMode: permissionMode ?? this.options.permissionMode,
|
|
483
489
|
};
|
|
@@ -1062,9 +1068,13 @@ export function loadWaveConfigFromFile(filePath) {
|
|
|
1062
1068
|
autoMemoryEnabled: config.autoMemoryEnabled !== undefined
|
|
1063
1069
|
? config.autoMemoryEnabled
|
|
1064
1070
|
: undefined,
|
|
1071
|
+
autoMemoryFrequency: config.autoMemoryFrequency !== undefined
|
|
1072
|
+
? config.autoMemoryFrequency
|
|
1073
|
+
: undefined,
|
|
1065
1074
|
models: config.models || undefined,
|
|
1066
1075
|
marketplaces: config.marketplaces || undefined,
|
|
1067
1076
|
worktree: config.worktree || undefined,
|
|
1077
|
+
enableArtifact: config.enableArtifact !== undefined ? config.enableArtifact : undefined,
|
|
1068
1078
|
};
|
|
1069
1079
|
}
|
|
1070
1080
|
catch (error) {
|
|
@@ -1079,6 +1089,16 @@ export function loadWaveConfigFromFile(filePath) {
|
|
|
1079
1089
|
throw error;
|
|
1080
1090
|
}
|
|
1081
1091
|
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Load the user-level (~/.wave/settings.json) `env` block only — no workdir
|
|
1094
|
+
* dependency. Used at daemon startup to apply WAVE_SERVER_URL before any agent
|
|
1095
|
+
* initializes: auth queries (getAuthStatus) can run before the first agent,
|
|
1096
|
+
* and AuthService falls back to the default URL otherwise.
|
|
1097
|
+
*/
|
|
1098
|
+
export function loadUserConfigEnv() {
|
|
1099
|
+
const config = loadWaveConfigFromFile(getUserConfigPaths()[0]);
|
|
1100
|
+
return config?.env ?? {};
|
|
1101
|
+
}
|
|
1082
1102
|
/**
|
|
1083
1103
|
* Load and merge Wave configuration from both user and project sources
|
|
1084
1104
|
* Project configuration takes precedence over user configuration
|
|
@@ -1199,6 +1219,10 @@ export function loadMergedWaveConfig(workdir) {
|
|
|
1199
1219
|
if (config.worktree !== undefined) {
|
|
1200
1220
|
mergedConfig.worktree = config.worktree;
|
|
1201
1221
|
}
|
|
1222
|
+
// Merge enableArtifact (last one wins)
|
|
1223
|
+
if (config.enableArtifact !== undefined) {
|
|
1224
|
+
mergedConfig.enableArtifact = config.enableArtifact;
|
|
1225
|
+
}
|
|
1202
1226
|
// Merge models
|
|
1203
1227
|
if (config.models) {
|
|
1204
1228
|
if (!mergedConfig.models)
|
|
@@ -1237,5 +1261,6 @@ export function loadMergedWaveConfig(workdir) {
|
|
|
1237
1261
|
? mergedConfig.models
|
|
1238
1262
|
: undefined,
|
|
1239
1263
|
worktree: mergedConfig.worktree,
|
|
1264
|
+
enableArtifact: mergedConfig.enableArtifact,
|
|
1240
1265
|
};
|
|
1241
1266
|
}
|
|
@@ -64,6 +64,11 @@ export class InitializationService {
|
|
|
64
64
|
const phaseStart = performance.now();
|
|
65
65
|
// Load hooks configuration using ConfigurationService
|
|
66
66
|
const configResult = await configurationService.loadMergedConfiguration(workdir);
|
|
67
|
+
// Rebuild subagent configurations now that settings.json env is loaded
|
|
68
|
+
// into the snapshot: conditional builtin subagents (model: visionModel
|
|
69
|
+
// → WAVE_VISION_MODEL) can only register once the env is available.
|
|
70
|
+
// Plugin agents are preserved by refreshConfigurations.
|
|
71
|
+
await subagentManager.refreshConfigurations();
|
|
67
72
|
hookManager.loadConfigurationFromWaveConfig(configResult.configuration);
|
|
68
73
|
// Update plugin manager with enabled plugins configuration
|
|
69
74
|
if (configResult.configuration?.enabledPlugins) {
|
package/dist/tools/agentTool.js
CHANGED
|
@@ -172,7 +172,8 @@ When using the Agent tool, you must specify a subagent_type parameter to select
|
|
|
172
172
|
success: true,
|
|
173
173
|
content: `Agent backgrounded with ID: ${taskId}.${outputPath ? ` Real-time output: ${outputPath}` : ""}`,
|
|
174
174
|
shortResult: "Agent backgrounded",
|
|
175
|
-
|
|
175
|
+
backgroundedByUser: true,
|
|
176
|
+
backgroundTaskId: taskId,
|
|
176
177
|
});
|
|
177
178
|
},
|
|
178
179
|
});
|
|
@@ -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 {
|