wave-agent-sdk 1.1.5 → 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.
Files changed (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
43
39
  }
44
- /** Render Markdown to a complete HTML document (client-side md→HTML). */
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
- /** Probe an artifact's current metadata (`via=model_read` marks model access). */
51
- async function probeFrame(slug, signal) {
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
- 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());
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 probe error", { slug, error: String(err) });
70
- return null;
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
- "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.",
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: "Path to the .html or .md file to publish (relative to the working directory). The file must exist.",
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: `Optional short label for the artifact (max ${LABEL_MAX_LENGTH} characters).`,
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: "Existing artifact URL to redeploy, e.g. https://host/code/artifact/abc123. Omit when republishing a file already published earlier in this session.",
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: ["file_path"],
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
- return `${ARTIFACT_TOOL_NAME}(${filePath}${url ? ` → ${url}` : ""})`;
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 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") {
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}: only .html and .md files can be published as artifacts (got "${ext || "no extension"}")`,
482
+ error: `${ARTIFACT_TOOL_NAME}: action must be "publish" or "read" (got "${actionRaw}")`,
169
483
  };
170
484
  }
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
- };
485
+ if (actionRaw === READ_ACTION) {
486
+ return readArtifact(args, context);
181
487
  }
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
- };
488
+ return publishArtifact(args, context);
356
489
  },
357
490
  };