wave-agent-sdk 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +58 -4
- package/dist/agent.js +91 -19
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/skills/settings.js +1 -12
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +53 -13
- package/dist/core/plugin.js +134 -26
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -1
- package/dist/managers/aiManager.js +190 -21
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/hookManager.d.ts +13 -0
- package/dist/managers/hookManager.js +31 -4
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +103 -8
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +45 -10
- package/dist/managers/mcpManager.js +103 -1
- package/dist/managers/messageManager.d.ts +48 -5
- package/dist/managers/messageManager.js +107 -21
- package/dist/managers/permissionManager.d.ts +40 -0
- package/dist/managers/permissionManager.js +63 -8
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +19 -0
- package/dist/managers/skillManager.js +44 -0
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +35 -3
- package/dist/managers/subagentManager.d.ts +8 -0
- package/dist/managers/subagentManager.js +20 -0
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +318 -119
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +60 -9
- package/dist/services/configurationService.js +129 -54
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +19 -15
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +84 -0
- package/dist/services/jsonlHandler.js +209 -14
- package/dist/services/memory.d.ts +3 -1
- package/dist/services/memory.js +13 -9
- package/dist/services/officialMarketplaceMirror.js +3 -2
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +16 -2
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +144 -3
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/stdio/index.d.ts +3 -1
- package/dist/stdio/index.js +3 -1
- package/dist/stdio/notificationRouter.js +1 -0
- package/dist/stdio/stdioAgent.d.ts +14 -7
- package/dist/stdio/stdioAgent.js +19 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +8 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +20 -5
- package/dist/types/configuration.d.ts +39 -1
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +39 -0
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +15 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +43 -0
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +33 -0
- package/dist/utils/fileUtils.js +81 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -2
- package/dist/utils/messageOperations.js +40 -29
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +39 -43
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/package.json +10 -7
|
@@ -4,14 +4,21 @@ import { marked } from "marked";
|
|
|
4
4
|
import { ARTIFACT_TOOL_NAME } from "../constants/tools.js";
|
|
5
5
|
import { authService, createAuthAwareFetch } from "../services/authService.js";
|
|
6
6
|
import { logger } from "../utils/globalLogger.js";
|
|
7
|
-
import { recordArtifact, getArtifactByFilePath, getRecordedVersion, recordVersion, } from "../services/artifactSession.js";
|
|
7
|
+
import { recordArtifact, getArtifactByFilePath, getRecordedVersion, recordVersion, markArtifactReadApproved, isArtifactReadApproved, } from "../services/artifactSession.js";
|
|
8
|
+
import { extractArtifactSlug, fetchFrameMeta, readArtifactContent, persistArtifactContent, summarizeArtifactAsReader, } from "../services/artifactContent.js";
|
|
9
|
+
import { formatSize } from "../services/contentSummarizer.js";
|
|
8
10
|
// --- Limits ---
|
|
9
11
|
const DEPLOY_TIMEOUT_MS = 30000;
|
|
10
12
|
const PROBE_TIMEOUT_MS = 15000;
|
|
11
13
|
/** Server-side content limit — POSTs above this get a 413. */
|
|
12
14
|
const ARTIFACT_MAX_CONTENT_BYTES = 16 * 1024 * 1024; // 16MB
|
|
13
15
|
const LABEL_MAX_LENGTH = 60;
|
|
16
|
+
/** Server-side title limit — POSTs above this get a 400. */
|
|
17
|
+
const TITLE_MAX_LENGTH = 1000;
|
|
14
18
|
const DEFAULT_FAVICON = "📄";
|
|
19
|
+
/** The tool's two actions; publishing is the default when `action` is omitted. */
|
|
20
|
+
const PUBLISH_ACTION = "publish";
|
|
21
|
+
const READ_ACTION = "read";
|
|
15
22
|
function isValidFavicon(favicon) {
|
|
16
23
|
if (!favicon || favicon.trim().length === 0)
|
|
17
24
|
return false;
|
|
@@ -27,48 +34,375 @@ function isValidFavicon(favicon) {
|
|
|
27
34
|
return /\p{Emoji}/u.test(cp);
|
|
28
35
|
});
|
|
29
36
|
}
|
|
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
37
|
function escapeHtml(s) {
|
|
42
38
|
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
43
39
|
}
|
|
44
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The file's basename without extension — CC's last-resort title. HTML publishes
|
|
42
|
+
* always send a non-empty `title` this way, which is what closes the title chain;
|
|
43
|
+
* Markdown publishes use it as their filename identity (see the publish path).
|
|
44
|
+
*/
|
|
45
|
+
function basenameTitle(filePath) {
|
|
46
|
+
const base = path.basename(filePath, path.extname(filePath)).trim();
|
|
47
|
+
return base || undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Render Markdown to a complete HTML document (client-side md→HTML). The injected
|
|
51
|
+
* `<title>` carries the file's **filename identity** — CC keeps Markdown pages on
|
|
52
|
+
* their file name, so `title` never enters the tag.
|
|
53
|
+
*/
|
|
45
54
|
function renderMarkdown(md, title) {
|
|
46
55
|
const body = marked.parse(md, { async: false });
|
|
47
56
|
const titleTag = title ? `<title>${escapeHtml(title)}</title>` : "";
|
|
48
57
|
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
58
|
}
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
function denyResult(permissionResult) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
content: "",
|
|
63
|
+
error: `${ARTIFACT_TOOL_NAME} operation denied by user, reason: ${permissionResult.message || "No reason provided"}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// --- Read action ---
|
|
67
|
+
/**
|
|
68
|
+
* Read an artifact back. Owned artifacts return raw HTML (inline CSS/JS intact)
|
|
69
|
+
* so the page can be edited or debugged; artifacts shared by someone else go
|
|
70
|
+
* through the fast model and only the summary reaches the conversation.
|
|
71
|
+
*/
|
|
72
|
+
async function readArtifact(args, context) {
|
|
73
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
74
|
+
if (!url) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
content: "",
|
|
78
|
+
error: `${ARTIFACT_TOOL_NAME}: missing required parameter "url" for a read`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const slug = extractArtifactSlug(url);
|
|
82
|
+
if (!slug) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
content: "",
|
|
86
|
+
error: `${ARTIFACT_TOOL_NAME}: url must point to an artifact page ({host}/code/artifact/{slug})`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
90
|
+
if (!authService.getSSOToken()) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
content: "",
|
|
94
|
+
error: `${ARTIFACT_TOOL_NAME}: not authenticated. Run /login to connect your account before reading artifacts.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const readResult = await readArtifactContent(slug, {
|
|
98
|
+
url,
|
|
99
|
+
abortSignal: context.abortSignal,
|
|
100
|
+
});
|
|
101
|
+
if (readResult.kind === "error") {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
content: "",
|
|
105
|
+
error: `${ARTIFACT_TOOL_NAME}: ${readResult.error}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const artifact = readResult.artifact;
|
|
109
|
+
const sessionId = context.sessionId || "";
|
|
110
|
+
if (sessionId && artifact.version) {
|
|
111
|
+
// Reading counts as seeing the latest version, so a follow-up republish of
|
|
112
|
+
// this artifact is not flagged as stale.
|
|
113
|
+
recordVersion(sessionId, slug, artifact.version);
|
|
114
|
+
}
|
|
115
|
+
if (artifact.ownership === "reader") {
|
|
116
|
+
// Someone else's page: the content is third-party, so ask once per session
|
|
117
|
+
// before it reaches the model, and never hand back the full text.
|
|
118
|
+
if (context.permissionManager && !isArtifactReadApproved(sessionId, slug)) {
|
|
119
|
+
const permissionContext = context.permissionManager.createContext(ARTIFACT_TOOL_NAME, context.permissionMode || "default", context.canUseToolCallback, {
|
|
120
|
+
action: READ_ACTION,
|
|
121
|
+
url,
|
|
122
|
+
...(prompt ? { prompt } : {}),
|
|
123
|
+
}, context.toolCallId);
|
|
124
|
+
permissionContext.warning =
|
|
125
|
+
"读取他人分享的 artifact:其内容将以摘要形式进入对话上下文。";
|
|
126
|
+
// Session-scoped approval: a standing rule would silently allow any
|
|
127
|
+
// other page later.
|
|
128
|
+
permissionContext.hidePersistentOption = true;
|
|
129
|
+
const permissionResult = await context.permissionManager.checkPermission(permissionContext);
|
|
130
|
+
if (permissionResult.behavior === "deny") {
|
|
131
|
+
return denyResult(permissionResult);
|
|
132
|
+
}
|
|
133
|
+
if (sessionId)
|
|
134
|
+
markArtifactReadApproved(sessionId, slug);
|
|
135
|
+
}
|
|
136
|
+
return summarizeArtifactAsReader(url, prompt, artifact.html, context);
|
|
137
|
+
}
|
|
138
|
+
const lines = [`Artifact: ${url}`];
|
|
139
|
+
if (artifact.version)
|
|
140
|
+
lines.push(`Version: ${artifact.version}`);
|
|
141
|
+
const persisted = persistArtifactContent(artifact.html);
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
content: `${lines.join("\n")}\n\n${persisted ?? artifact.html}`,
|
|
145
|
+
shortResult: `Read artifact ${slug}${artifact.version ? ` (v${artifact.version})` : ""} — ${formatSize(artifact.bytes)} of HTML`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// --- Publish action ---
|
|
149
|
+
async function publishArtifact(args, context) {
|
|
150
|
+
const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
|
|
151
|
+
if (!filePath) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
content: "",
|
|
155
|
+
error: `${ARTIFACT_TOOL_NAME}: missing required parameter "file_path"`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const faviconRaw = typeof args.favicon === "string" ? args.favicon.trim() : "";
|
|
159
|
+
const favicon = faviconRaw || DEFAULT_FAVICON;
|
|
160
|
+
if (!isValidFavicon(favicon)) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
content: "",
|
|
164
|
+
error: `${ARTIFACT_TOOL_NAME}: favicon must be 1-2 emoji characters (e.g. "📄" or "🔖"), no text, URLs, or HTML markup`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// `label` is the short name for THIS publish (CC), not a title fallback — it
|
|
168
|
+
// only feeds the version list, so it is never auto-filled.
|
|
169
|
+
const labelRaw = typeof args.label === "string" ? args.label.trim() : "";
|
|
170
|
+
const label = labelRaw || undefined;
|
|
171
|
+
if (label !== undefined && label.length > LABEL_MAX_LENGTH) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
content: "",
|
|
175
|
+
error: `${ARTIFACT_TOOL_NAME}: label must be at most ${LABEL_MAX_LENGTH} characters (got ${label.length})`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// `title` is the artifact title, HTML publishes only (CC). It is rejected at
|
|
179
|
+
// the same length the server enforces, so an over-long title fails here instead
|
|
180
|
+
// of costing a round trip (the request would come back a 400 either way).
|
|
181
|
+
const titleRaw = typeof args.title === "string" ? args.title.trim() : "";
|
|
182
|
+
if (titleRaw.length > TITLE_MAX_LENGTH) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
content: "",
|
|
186
|
+
error: `${ARTIFACT_TOOL_NAME}: title must be at most ${TITLE_MAX_LENGTH} characters (got ${titleRaw.length})`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const explicitTitle = titleRaw || undefined;
|
|
190
|
+
const force = args.force === true;
|
|
191
|
+
const urlRaw = typeof args.url === "string" ? args.url.trim() : "";
|
|
192
|
+
const url = urlRaw || undefined;
|
|
193
|
+
let slug;
|
|
194
|
+
if (url) {
|
|
195
|
+
slug = extractArtifactSlug(url);
|
|
196
|
+
if (!slug) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
content: "",
|
|
200
|
+
error: `${ARTIFACT_TOOL_NAME}: url must point to an artifact page ({host}/code/artifact/{slug})`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Resolve and read the file (relative to the workdir).
|
|
205
|
+
const absolutePath = path.resolve(context.workdir, filePath);
|
|
206
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
207
|
+
if (ext !== ".html" && ext !== ".md") {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
content: "",
|
|
211
|
+
error: `${ARTIFACT_TOOL_NAME}: only .html and .md files can be published as artifacts (got "${ext || "no extension"}")`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
let fileContent;
|
|
215
|
+
try {
|
|
216
|
+
fileContent = readFileSync(absolutePath, "utf-8");
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
content: "",
|
|
222
|
+
error: `${ARTIFACT_TOOL_NAME}: file not found or unreadable: ${filePath}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// CC's title chain is <title> tag (wins, server-side) → `title` parameter →
|
|
226
|
+
// file basename, and the client is the one that supplies that last resort — so
|
|
227
|
+
// an .html publish always sends a non-empty `title`. Markdown keeps its file
|
|
228
|
+
// name identity instead: the tag injected below carries it, and the `title`
|
|
229
|
+
// parameter does not apply.
|
|
230
|
+
const fileTitle = basenameTitle(filePath);
|
|
231
|
+
const title = ext === ".html" ? (explicitTitle ?? fileTitle) : undefined;
|
|
232
|
+
const content = ext === ".md" ? renderMarkdown(fileContent, fileTitle) : fileContent;
|
|
233
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
234
|
+
if (contentBytes > ARTIFACT_MAX_CONTENT_BYTES) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
content: "",
|
|
238
|
+
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.`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (!authService.getSSOToken()) {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
content: "",
|
|
245
|
+
error: `${ARTIFACT_TOOL_NAME}: not authenticated. Run /login to connect your account before publishing artifacts.`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const sessionId = context.sessionId || "";
|
|
249
|
+
const signal = context.abortSignal
|
|
250
|
+
? AbortSignal.any([
|
|
251
|
+
context.abortSignal,
|
|
252
|
+
AbortSignal.timeout(DEPLOY_TIMEOUT_MS),
|
|
253
|
+
])
|
|
254
|
+
: AbortSignal.timeout(DEPLOY_TIMEOUT_MS);
|
|
255
|
+
// Redeploy: probe current metadata for baseVersion, shared-live detection,
|
|
256
|
+
// and the stale-version guard.
|
|
257
|
+
let serverVersion;
|
|
258
|
+
let sharedLive = false;
|
|
259
|
+
if (url && slug) {
|
|
260
|
+
const metaResult = await fetchFrameMeta(slug, {
|
|
261
|
+
abortSignal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
262
|
+
});
|
|
263
|
+
if (metaResult.kind === "error") {
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
content: "",
|
|
267
|
+
error: `${ARTIFACT_TOOL_NAME}: artifact not found at ${url}. It may have been deleted or the URL is invalid.`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const meta = metaResult.meta;
|
|
271
|
+
serverVersion = meta.version;
|
|
272
|
+
sharedLive = !!(meta.perm && meta.perm.mode !== "owner" && !meta.shared);
|
|
273
|
+
const recorded = getRecordedVersion(sessionId, slug);
|
|
274
|
+
if (recorded !== undefined && recorded !== meta.version && !force) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
content: "",
|
|
278
|
+
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.`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Permission check: first publish and shared-live redeploys require
|
|
283
|
+
// confirmation; republishing a file/artifact this session already
|
|
284
|
+
// confirmed once auto-allows (matching Claude Code's behavior).
|
|
285
|
+
const publishedThisSession = !!getArtifactByFilePath(sessionId, filePath) ||
|
|
286
|
+
(slug ? getRecordedVersion(sessionId, slug) !== undefined : false);
|
|
287
|
+
const needsConfirm = !publishedThisSession || sharedLive;
|
|
288
|
+
if (context.permissionManager && needsConfirm) {
|
|
289
|
+
const permissionContext = context.permissionManager.createContext(ARTIFACT_TOOL_NAME, context.permissionMode || "default", context.canUseToolCallback, {
|
|
290
|
+
file_path: filePath,
|
|
291
|
+
...(favicon !== DEFAULT_FAVICON ? { favicon } : {}),
|
|
292
|
+
...(label !== undefined ? { label } : {}),
|
|
293
|
+
...(url !== undefined ? { url } : {}),
|
|
294
|
+
...(force ? { force: true } : {}),
|
|
295
|
+
}, context.toolCallId);
|
|
296
|
+
if (sharedLive) {
|
|
297
|
+
permissionContext.warning =
|
|
298
|
+
"此 artifact 处于 shared-live 状态(共享且实时更新),重新部署后所有访问者都会立即看到新内容。";
|
|
299
|
+
permissionContext.hidePersistentOption = true;
|
|
300
|
+
}
|
|
301
|
+
const permissionResult = await context.permissionManager.checkPermission(permissionContext);
|
|
302
|
+
if (permissionResult.behavior === "deny") {
|
|
303
|
+
return denyResult(permissionResult);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Deploy.
|
|
52
307
|
const serverUrl = authService.getServerUrl();
|
|
53
308
|
const authFetch = createAuthAwareFetch(globalThis.fetch);
|
|
309
|
+
const body = {
|
|
310
|
+
content,
|
|
311
|
+
favicon,
|
|
312
|
+
...(title !== undefined ? { title } : {}),
|
|
313
|
+
...(label !== undefined ? { label } : {}),
|
|
314
|
+
...(url !== undefined ? { url } : {}),
|
|
315
|
+
...(serverVersion !== undefined ? { baseVersion: serverVersion } : {}),
|
|
316
|
+
...(force ? { force: true } : {}),
|
|
317
|
+
};
|
|
318
|
+
let res;
|
|
54
319
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
status: res.status,
|
|
62
|
-
statusText: res.statusText,
|
|
63
|
-
});
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
return (await res.json());
|
|
320
|
+
res = await authFetch(`${serverUrl}/api/frame/deploy/direct`, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: { "Content-Type": "application/json" },
|
|
323
|
+
body: JSON.stringify(body),
|
|
324
|
+
signal,
|
|
325
|
+
});
|
|
67
326
|
}
|
|
68
327
|
catch (err) {
|
|
69
|
-
logger?.warn("Artifact
|
|
70
|
-
return
|
|
328
|
+
logger?.warn("Artifact deploy request failed", { error: String(err) });
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
content: "",
|
|
332
|
+
error: `${ARTIFACT_TOOL_NAME}: failed to reach the server: ${err instanceof Error ? err.message : String(err)}`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const data = (await res.json().catch(() => ({})));
|
|
336
|
+
if (res.status === 201) {
|
|
337
|
+
const deploy = data;
|
|
338
|
+
recordArtifact(sessionId, filePath, {
|
|
339
|
+
url: deploy.url,
|
|
340
|
+
slug: deploy.slug,
|
|
341
|
+
version: deploy.version,
|
|
342
|
+
});
|
|
343
|
+
const lines = [`Artifact published: ${deploy.url}`];
|
|
344
|
+
if (deploy.path)
|
|
345
|
+
lines.push(`Path: ${deploy.path}`);
|
|
346
|
+
if (deploy.title)
|
|
347
|
+
lines.push(`Title: ${deploy.title}`);
|
|
348
|
+
lines.push(`Version: ${deploy.version}`);
|
|
349
|
+
return {
|
|
350
|
+
success: true,
|
|
351
|
+
content: lines.join("\n"),
|
|
352
|
+
shortResult: `Published ${filePath} → ${deploy.url}`,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (res.status === 409) {
|
|
356
|
+
const live = typeof data.live === "string" ? data.live : undefined;
|
|
357
|
+
if (live && slug) {
|
|
358
|
+
recordVersion(sessionId, slug, live);
|
|
359
|
+
}
|
|
360
|
+
const serverMessage = typeof data.message === "string"
|
|
361
|
+
? data.message
|
|
362
|
+
: typeof data.error === "string"
|
|
363
|
+
? data.error
|
|
364
|
+
: "the artifact has been updated by someone else";
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
content: "",
|
|
368
|
+
error: `${ARTIFACT_TOOL_NAME}: conflict detected — ${serverMessage}${live ? ` (live version: ${live})` : ""}. Pass "force": true to overwrite the live version.`,
|
|
369
|
+
};
|
|
71
370
|
}
|
|
371
|
+
if (res.status === 413) {
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
content: "",
|
|
375
|
+
error: `${ARTIFACT_TOOL_NAME}: the published content is too large (server limit is 16MB). Reduce the file or split it up.`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (res.status === 400) {
|
|
379
|
+
const serverMessage = typeof data.message === "string"
|
|
380
|
+
? data.message
|
|
381
|
+
: typeof data.error === "string"
|
|
382
|
+
? data.error
|
|
383
|
+
: "the server rejected the content";
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
content: "",
|
|
387
|
+
error: `${ARTIFACT_TOOL_NAME}: the server rejected the publish — ${serverMessage}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (res.status === 401 || res.status === 403) {
|
|
391
|
+
return {
|
|
392
|
+
success: false,
|
|
393
|
+
content: "",
|
|
394
|
+
error: `${ARTIFACT_TOOL_NAME}: authentication failed (HTTP ${res.status}). Run /login again and retry.`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
logger?.warn("Artifact deploy unexpected status", {
|
|
398
|
+
status: res.status,
|
|
399
|
+
statusText: res.statusText,
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
success: false,
|
|
403
|
+
content: "",
|
|
404
|
+
error: `${ARTIFACT_TOOL_NAME}: server returned HTTP ${res.status} ${res.statusText}`,
|
|
405
|
+
};
|
|
72
406
|
}
|
|
73
407
|
export const artifactTool = {
|
|
74
408
|
name: ARTIFACT_TOOL_NAME,
|
|
@@ -77,281 +411,80 @@ export const artifactTool = {
|
|
|
77
411
|
type: "function",
|
|
78
412
|
function: {
|
|
79
413
|
name: ARTIFACT_TOOL_NAME,
|
|
80
|
-
description: "Publish local HTML or Markdown files as shareable web pages (artifacts). " +
|
|
81
|
-
"
|
|
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
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
414
|
+
description: "Publish local HTML or Markdown files as shareable web pages (artifacts), or read a published artifact back. " +
|
|
415
|
+
`\`action: "publish"\` (the default when omitted) publishes a file: each publish returns a private URL you can share; ` +
|
|
416
|
+
"the page is only accessible to you unless you change its sharing. Only .html and .md files are supported — inline content " +
|
|
417
|
+
"is not accepted. Markdown files are rendered to HTML automatically. Pass `url` (an existing artifact URL) to redeploy that " +
|
|
418
|
+
"artifact, or omit it to republish a file already published in this session. Use `force` to overwrite an artifact that has " +
|
|
419
|
+
`been updated by someone else. \`action: "read"\` returns the current content of an artifact you pass as \`url\`: artifacts you ` +
|
|
420
|
+
"own come back as raw HTML with inline CSS/JS intact (use it to keep editing, style-check, or debug a published page), while " +
|
|
421
|
+
"artifacts shared by someone else come back as an isolated summary steered by the optional `prompt`.",
|
|
87
422
|
parameters: {
|
|
88
423
|
type: "object",
|
|
89
424
|
properties: {
|
|
425
|
+
action: {
|
|
426
|
+
type: "string",
|
|
427
|
+
enum: [PUBLISH_ACTION, READ_ACTION],
|
|
428
|
+
description: 'The action to perform: "publish" (default) publishes a local file, "read" returns an artifact\'s current content.',
|
|
429
|
+
},
|
|
90
430
|
file_path: {
|
|
91
431
|
type: "string",
|
|
92
|
-
description:
|
|
432
|
+
description: 'Path to the .html or .md file to publish (relative to the working directory). The file must exist. Required for action "publish".',
|
|
93
433
|
},
|
|
94
434
|
favicon: {
|
|
95
435
|
type: "string",
|
|
96
|
-
description: "1-2 emoji characters shown as the page favicon (no text, URLs, or HTML). Defaults to 📄.",
|
|
436
|
+
description: "1-2 emoji characters shown as the page favicon (no text, URLs, or HTML). Defaults to 📄. Only used when publishing.",
|
|
437
|
+
},
|
|
438
|
+
title: {
|
|
439
|
+
type: "string",
|
|
440
|
+
description: "Title for the artifact — the name shown in the browser tab and gallery. Only used when publishing an .html file. Prefer a <title> tag at the top of the HTML itself: this parameter fills in only when the file lacks one in the first 8KB, and never overrides the tag. When neither is present the file name is used. Markdown pages keep their filename identity instead.",
|
|
97
441
|
},
|
|
98
442
|
label: {
|
|
99
443
|
type: "string",
|
|
100
|
-
description: `
|
|
444
|
+
description: `A short name for this publish, max ${LABEL_MAX_LENGTH} characters (e.g. "Draft to legal"). Optional — a few words, not a description. Only used when publishing.`,
|
|
101
445
|
},
|
|
102
446
|
url: {
|
|
103
447
|
type: "string",
|
|
104
|
-
description: "
|
|
448
|
+
description: "Artifact URL, e.g. https://host/code/artifact/abc123. When publishing: the existing artifact to redeploy (omit when republishing a file already published earlier in this session). When reading: the artifact to read (required).",
|
|
105
449
|
},
|
|
106
450
|
force: {
|
|
107
451
|
type: "boolean",
|
|
108
|
-
description: "Set true to overwrite an artifact that was updated since this session last saw it (stale version or conflict).",
|
|
452
|
+
description: "Set true to overwrite an artifact that was updated since this session last saw it (stale version or conflict). Only used when publishing.",
|
|
453
|
+
},
|
|
454
|
+
prompt: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: 'Optional steering for action "read" when the artifact is shared by someone else — e.g. "how is the layout structured?" or "what does it render?". Ignored for artifacts you own.',
|
|
109
457
|
},
|
|
110
458
|
},
|
|
111
|
-
required: [
|
|
459
|
+
required: [],
|
|
112
460
|
},
|
|
113
461
|
},
|
|
114
462
|
},
|
|
463
|
+
// Value only, never the tool name: the collapsed row renders
|
|
464
|
+
// "<tool name> <compactParams>", so wrapping the value in "Artifact(...)"
|
|
465
|
+
// printed the name twice.
|
|
115
466
|
formatCompactParams: (params) => {
|
|
116
|
-
const filePath = typeof params.file_path === "string" ? params.file_path : "";
|
|
117
467
|
const url = typeof params.url === "string" ? params.url : "";
|
|
118
|
-
|
|
468
|
+
if (params.action === READ_ACTION) {
|
|
469
|
+
return `read ${url}`;
|
|
470
|
+
}
|
|
471
|
+
const filePath = typeof params.file_path === "string" ? params.file_path : "";
|
|
472
|
+
return `${filePath}${url ? ` → ${url}` : ""}`;
|
|
119
473
|
},
|
|
120
474
|
execute: async (args, context) => {
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
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") {
|
|
475
|
+
const actionRaw = typeof args.action === "string" ? args.action.trim() : "";
|
|
476
|
+
if (actionRaw &&
|
|
477
|
+
actionRaw !== PUBLISH_ACTION &&
|
|
478
|
+
actionRaw !== READ_ACTION) {
|
|
165
479
|
return {
|
|
166
480
|
success: false,
|
|
167
481
|
content: "",
|
|
168
|
-
error: `${ARTIFACT_TOOL_NAME}:
|
|
482
|
+
error: `${ARTIFACT_TOOL_NAME}: action must be "publish" or "read" (got "${actionRaw}")`,
|
|
169
483
|
};
|
|
170
484
|
}
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
};
|
|
485
|
+
if (actionRaw === READ_ACTION) {
|
|
486
|
+
return readArtifact(args, context);
|
|
181
487
|
}
|
|
182
|
-
|
|
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
|
-
};
|
|
488
|
+
return publishArtifact(args, context);
|
|
356
489
|
},
|
|
357
490
|
};
|