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.
Files changed (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Runtime dependencies the CLI installs on demand.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Two dependencies live here: `sharp` (the image codec behind outbound image
7
+ * resizing, ~19MB unpacked) and `@vscode/ripgrep` (the search binary behind the
8
+ * Grep tool, ~5MB with its platform package). Both are too big to ship in every
9
+ * host bundle and useless to users who never send an image or run a search. Two
10
+ * of the three ways of getting the CLI can simply use npm:
11
+ *
12
+ * | how the CLI was installed | where the dependencies come from | what this does |
13
+ * | --- | --- | --- |
14
+ * | `npm i -g wave-code`, project install | plain npm dependencies (npm picks the matching platform package; sharp's libvips arrives through the platform package's own optionalDependencies) | step 1 short-circuits, nothing is downloaded |
15
+ * | desktop / VS Code / JetBrains (the host copies the CLI into `~/.wave/cli/<end>/`) | nothing ships them — the copy is three files (entry, package.json, bundle) | downloads into the shared `~/.wave/cli/node_modules` |
16
+ * | in-repo dev (`pnpm run wave`) | the repo's node_modules | step 1 short-circuits |
17
+ *
18
+ * It is deliberately CLI-side rather than per-host: the platform key is computed
19
+ * by the *same* runtime that later loads the module (a JVM-side arch guess
20
+ * cannot be), and one implementation covers the TUI, stdio (all three hosts),
21
+ * remote and daemon paths. ripgrep used to be the counter-example — each of the
22
+ * three hosts downloaded it before spawning the CLI, and the remote path shelled
23
+ * out to `npm install --prefix` — so the table replaced four implementations.
24
+ *
25
+ * ## Layout
26
+ *
27
+ * ```
28
+ * ~/.wave/cli/node_modules/ <- shared by all three hosts, survives CLI upgrades
29
+ * sharp/ semver/ detect-libc/ @img/colour/
30
+ * @img/sharp-<key>/ key = linux-x64 | linuxmusl-x64 | darwin-arm64 | win32-x64
31
+ * @img/sharp-libvips-<key>/ must stay a sibling of the above: its .node has
32
+ * rpath `$ORIGIN/../../sharp-libvips-<key>/lib`
33
+ * @vscode/ripgrep/ the wrapper (ESM, no dependencies of its own)
34
+ * @vscode/ripgrep-<platform>-<arch>/ holds bin/rg, published with the exec bit
35
+ * .wave-runtime-deps.json <- marker, written last: present means this
36
+ * batch completed
37
+ * ```
38
+ *
39
+ * Node resolves the modules from `~/.wave/cli/<end>/dist/bundle/wave.mjs` by
40
+ * walking up, which lands on this `node_modules`.
41
+ *
42
+ * ## Why detect-libc decides the platform key
43
+ *
44
+ * `linux-x64` and `linuxmusl-x64` are different downloads, and a wrong guess
45
+ * means a `.node` that refuses to dlopen. The key is therefore computed *by*
46
+ * `detect-libc` (the very module sharp consults, `dist/libvips.cjs`:
47
+ * `` `${process.platform}${isNonGlibcLinuxSync() ? familySync() : ""}-${process.arch}` ``)
48
+ * rather than by a copy of that logic. A future sharp that renames or drops a
49
+ * dependency is followed automatically; only a *renamed* dependency needs a
50
+ * code change here.
51
+ *
52
+ * A row can opt out of the libc family (`libcAwarePlatformKey: false`):
53
+ * `@vscode/ripgrep` publishes only `@vscode/ripgrep-linux-x64`, so a musl key
54
+ * would match nothing.
55
+ */
56
+ import { createHash } from "node:crypto";
57
+ import * as fs from "node:fs";
58
+ import * as os from "node:os";
59
+ import * as path from "node:path";
60
+ import { createRequire } from "node:module";
61
+ import { fileURLToPath } from "node:url";
62
+ import { maxSatisfying } from "semver";
63
+ import { logWarn, logger } from "./globalLogger.js";
64
+ import { getImageProcessor, resetImageProcessor, resolveSharp, } from "./imageProcessor.js";
65
+ import { extractNpmTarball } from "./npmTarball.js";
66
+ import { getRgPath, resetRipgrep, resolveRipgrep } from "./ripgrep.js";
67
+ /** npm registry mirror for China users (same choice as the ripgrep download). */
68
+ export const RUNTIME_DEPS_REGISTRY = "https://registry.npmmirror.com";
69
+ const MARKER_FILE = ".wave-runtime-deps.json";
70
+ const sharpDependency = {
71
+ name: "sharp",
72
+ sharedPackages(entry) {
73
+ const requests = [
74
+ { name: "sharp", range: entry.version },
75
+ ];
76
+ // The plain JS deps come from sharp's own ranges, so a sharp upgrade that
77
+ // adds or drops one is followed without a code change.
78
+ for (const [name, range] of Object.entries(entry.dependencies ?? {})) {
79
+ requests.push({ name, range });
80
+ }
81
+ return requests;
82
+ },
83
+ platformPackages(entry, platformKey) {
84
+ // Platform builds are pinned exactly in sharp's optionalDependencies —
85
+ // including the libvips version, which no other field carries.
86
+ const optional = entry.optionalDependencies ?? {};
87
+ return ["@img/sharp", "@img/sharp-libvips"].map((prefix) => {
88
+ const name = `${prefix}-${platformKey}`;
89
+ const range = optional[name];
90
+ if (!range) {
91
+ throw new Error(`sharp 不支持当前平台(${platformKey}):缺 ${name}`);
92
+ }
93
+ return { name, range };
94
+ });
95
+ },
96
+ isAvailable: () => getImageProcessor() !== undefined,
97
+ loadsFrom: (requireBase) => resolveSharp(requireFnFor(requireBase)) !== undefined,
98
+ onInstalled: () => resetImageProcessor(),
99
+ };
100
+ /**
101
+ * ripgrep row. The wrapper declares no dependencies of its own — only the
102
+ * platform packages in `optionalDependencies` — and keys them by
103
+ * `<platform>-<arch>` with no libc family.
104
+ */
105
+ const ripgrepDependency = {
106
+ name: "@vscode/ripgrep",
107
+ sharedPackages(entry) {
108
+ return [{ name: "@vscode/ripgrep", range: entry.version }];
109
+ },
110
+ platformPackages(entry, platformKey) {
111
+ const name = `@vscode/ripgrep-${platformKey}`;
112
+ const range = entry.optionalDependencies?.[name];
113
+ if (!range) {
114
+ throw new Error(`ripgrep 不支持当前平台(${platformKey}):缺 ${name}`);
115
+ }
116
+ return [{ name, range }];
117
+ },
118
+ libcAwarePlatformKey: false,
119
+ isAvailable: () => getRgPath() !== undefined,
120
+ loadsFrom: (requireBase) => resolveRipgrep(requireFnFor(requireBase)) !== undefined,
121
+ onInstalled: () => resetRipgrep(),
122
+ };
123
+ const RUNTIME_DEPENDENCIES = [
124
+ sharpDependency,
125
+ ripgrepDependency,
126
+ ];
127
+ /** `<home>/.wave/cli` — where hosts copy the CLI into and share downloads. */
128
+ function defaultCliHome() {
129
+ return path.join(os.homedir(), ".wave", "cli");
130
+ }
131
+ /**
132
+ * Whether this code runs from a CLI copy under the shared cli home.
133
+ *
134
+ * An npm-installed CLI (bundle under `<prefix>/lib/node_modules/wave-code/...`)
135
+ * must not self-install: Node would never look in `~/.wave/cli/node_modules`
136
+ * from there, and writing into a global npm prefix may lack permissions and
137
+ * gets pruned by the next `npm install`. Such a setup either already has the
138
+ * dependencies (the normal case) or degrades with an actionable warning.
139
+ */
140
+ export function isManagedCliInstall(moduleUrl, cliHome = defaultCliHome()) {
141
+ let file;
142
+ try {
143
+ file = fileURLToPath(moduleUrl);
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ const relative = path.relative(cliHome, file);
149
+ return (relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative));
150
+ }
151
+ /** Nearest `package.json` upwards from [from] that declares [dependency]. */
152
+ function findEntryPackageJson(from, dependency) {
153
+ let dir = path.dirname(from);
154
+ for (;;) {
155
+ try {
156
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
157
+ const range = parsed.dependencies?.[dependency];
158
+ if (range)
159
+ return { range };
160
+ }
161
+ catch {
162
+ // Unreadable or absent — keep walking towards the filesystem root.
163
+ }
164
+ const parent = path.dirname(dir);
165
+ if (parent === dir)
166
+ return undefined;
167
+ dir = parent;
168
+ }
169
+ }
170
+ async function downloadBuffer(url, fetchImpl) {
171
+ const res = await fetchImpl(url, { redirect: "follow" });
172
+ if (!res.ok) {
173
+ throw new Error(`下载失败(HTTP ${res.status}):${url}`);
174
+ }
175
+ return Buffer.from(await res.arrayBuffer());
176
+ }
177
+ /** sha512/base64 check against the registry's `dist.integrity`. */
178
+ export function verifyIntegrity(buffer, integrity) {
179
+ for (const part of integrity.split(/\s+/)) {
180
+ const separator = part.indexOf("-");
181
+ if (separator === -1)
182
+ continue;
183
+ const algorithm = part.slice(0, separator);
184
+ if (algorithm !== "sha512")
185
+ continue;
186
+ const actual = createHash("sha512").update(buffer).digest("base64");
187
+ if (actual !== part.slice(separator + 1)) {
188
+ throw new Error("下载内容 sha512 校验失败");
189
+ }
190
+ return;
191
+ }
192
+ throw new Error(`不支持的完整性摘要:${integrity}`);
193
+ }
194
+ /** Registry metadata for the highest version of [name] matching [range]. */
195
+ async function resolveVersion(name, range, fetchImpl) {
196
+ const res = await fetchImpl(`${RUNTIME_DEPS_REGISTRY}/${name}`, {
197
+ redirect: "follow",
198
+ });
199
+ if (!res.ok) {
200
+ throw new Error(`获取 ${name} 元数据失败(HTTP ${res.status})`);
201
+ }
202
+ const metadata = (await res.json());
203
+ const versions = metadata.versions ?? {};
204
+ const best = maxSatisfying(Object.keys(versions), range);
205
+ if (!best) {
206
+ throw new Error(`没有满足 ${range} 的 ${name} 版本`);
207
+ }
208
+ return { version: best, ...versions[best] };
209
+ }
210
+ /**
211
+ * A `require` rooted at [requireBase] — the cli home when the packages are
212
+ * already on disk, the temp dir mid-install. Used both to ask `detect-libc`
213
+ * which build to fetch and to verify an install afterwards.
214
+ */
215
+ function requireFnFor(requireBase) {
216
+ return createRequire(path.join(requireBase, "index.js"));
217
+ }
218
+ /**
219
+ * Platform key of the native build. With [libcFamily] (the default) the key
220
+ * carries the libc family — `linuxmusl-x64` vs `linux-x64` — decided by
221
+ * `detect-libc`, resolved from `[requireBase]/node_modules` and falling back to
222
+ * Node's own report when it cannot be loaded. Dependencies that publish a single
223
+ * Linux build pass `false`.
224
+ */
225
+ function runtimePlatformKey(requireBase, libcFamily = true) {
226
+ if (!libcFamily)
227
+ return `${process.platform}-${process.arch}`;
228
+ if (requireBase) {
229
+ try {
230
+ const detect = requireFnFor(requireBase)("detect-libc");
231
+ const family = detect.isNonGlibcLinuxSync?.()
232
+ ? (detect.familySync?.() ?? "")
233
+ : "";
234
+ return `${process.platform}${family}-${process.arch}`;
235
+ }
236
+ catch {
237
+ // Fall through to the report-based fallback.
238
+ }
239
+ }
240
+ let family = "";
241
+ if (process.platform === "linux") {
242
+ const report = process.report?.getReport();
243
+ if (!report?.header?.glibcVersionRuntime)
244
+ family = "musl";
245
+ }
246
+ return `${process.platform}${family}-${process.arch}`;
247
+ }
248
+ /** Write the tarball's entries under [dest], creating parent directories. */
249
+ function writeEntries(dest, entries) {
250
+ for (const entry of entries) {
251
+ const target = path.join(dest, entry.path);
252
+ fs.mkdirSync(entry.kind === "directory" ? target : path.dirname(target), {
253
+ recursive: true,
254
+ });
255
+ if (entry.kind !== "file")
256
+ continue;
257
+ fs.writeFileSync(target, entry.data);
258
+ // npm publishes executables (the ripgrep binary) as 0755; without the exec
259
+ // bit every spawn fails with EACCES. Only the exec bit is worth restoring —
260
+ // the rest is umask's business. A no-op on Windows, where there is no mode.
261
+ if (entry.mode & 0o111)
262
+ fs.chmodSync(target, entry.mode & 0o777);
263
+ }
264
+ }
265
+ /** Fetch one package and unpack it into `modulesDir/<name>`. */
266
+ async function fetchPackage(request, modulesDir, fetchImpl, known) {
267
+ const meta = known ?? (await resolveVersion(request.name, request.range, fetchImpl));
268
+ const tarball = meta.dist?.tarball;
269
+ if (!tarball) {
270
+ throw new Error(`${request.name}@${meta.version} 缺少下载地址`);
271
+ }
272
+ const buffer = await downloadBuffer(tarball, fetchImpl);
273
+ if (meta.dist?.integrity)
274
+ verifyIntegrity(buffer, meta.dist.integrity);
275
+ writeEntries(path.join(modulesDir, request.name), extractNpmTarball(buffer));
276
+ }
277
+ /**
278
+ * Install one dependency: download into a temp `node_modules` beside the real
279
+ * one, then move each package into place. A crash mid-way leaves the marker
280
+ * absent, so the next launch redoes the batch — packages already moved are
281
+ * skipped.
282
+ *
283
+ * Returns the platform key the batch was built for.
284
+ */
285
+ async function installDependency(dependency, entry, cliHome, fetchImpl) {
286
+ const installDir = path.join(cliHome, "node_modules");
287
+ fs.mkdirSync(installDir, { recursive: true });
288
+ const tempDir = path.join(cliHome, `.wave-runtime-tmp-${process.pid}-${Math.random().toString(36).slice(2)}`);
289
+ const tempModules = path.join(tempDir, "node_modules");
290
+ fs.mkdirSync(tempModules, { recursive: true });
291
+ try {
292
+ const shared = dependency.sharedPackages(entry);
293
+ // detect-libc has to land first: it decides which native build to fetch.
294
+ for (const request of shared) {
295
+ await fetchPackage(request, tempModules, fetchImpl,
296
+ // The entry package's metadata is already in hand — no second round trip.
297
+ request.name === dependency.name ? entry : undefined);
298
+ }
299
+ const platformKey = runtimePlatformKey(tempDir, dependency.libcAwarePlatformKey ?? true);
300
+ const platform = dependency.platformPackages(entry, platformKey);
301
+ for (const request of platform) {
302
+ await fetchPackage(request, tempModules, fetchImpl);
303
+ }
304
+ for (const { name } of [...shared, ...platform]) {
305
+ const target = path.join(installDir, name);
306
+ fs.mkdirSync(path.dirname(target), { recursive: true });
307
+ // Already there: either a previous run or a process racing us.
308
+ if (fs.existsSync(target))
309
+ continue;
310
+ try {
311
+ fs.renameSync(path.join(tempModules, name), target);
312
+ }
313
+ catch (error) {
314
+ const code = error.code;
315
+ const raced = code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM";
316
+ if (!raced)
317
+ throw error;
318
+ }
319
+ }
320
+ return platformKey;
321
+ }
322
+ finally {
323
+ fs.rmSync(tempDir, { recursive: true, force: true });
324
+ }
325
+ }
326
+ function readMarker(installDir) {
327
+ try {
328
+ return JSON.parse(fs.readFileSync(path.join(installDir, MARKER_FILE), "utf-8"));
329
+ }
330
+ catch {
331
+ return {};
332
+ }
333
+ }
334
+ let inFlight;
335
+ let settled;
336
+ /**
337
+ * Make sure the runtime dependencies are installed. Never throws: the long-lived
338
+ * CLI entry points await it before serving, so a first pasted image (or a first
339
+ * search) cannot race a download — the same reason the hosts used to block on
340
+ * ripgrep before spawning the CLI. A failed install is reported once and only
341
+ * degrades images and grep.
342
+ *
343
+ * The outcome is memoised for the process lifetime, failures included — one
344
+ * attempt per launch, so a broken network does not retry on every image.
345
+ */
346
+ export function ensureRuntimeDeps(options) {
347
+ if (settled)
348
+ return Promise.resolve(settled);
349
+ if (!inFlight) {
350
+ inFlight = runInstall(options).then((result) => {
351
+ settled = result;
352
+ return result;
353
+ }, (error) => {
354
+ settled = {
355
+ available: false,
356
+ reason: error instanceof Error ? error.message : String(error),
357
+ };
358
+ return settled;
359
+ });
360
+ }
361
+ return inFlight;
362
+ }
363
+ async function runInstall(options) {
364
+ const fetchImpl = options?.fetchImpl ?? fetch;
365
+ const moduleUrl = options?.moduleUrl ?? import.meta.url;
366
+ const cliHome = options?.cliHome ?? defaultCliHome();
367
+ if (RUNTIME_DEPENDENCIES.every((dependency) => dependency.isAvailable())) {
368
+ return { available: true };
369
+ }
370
+ if (!isManagedCliInstall(moduleUrl, cliHome)) {
371
+ const reason = "当前 wave CLI 不是 ~/.wave/cli 下的副本,无法自动安装运行时依赖" +
372
+ "(sharp / ripgrep);请重装 wave-code(不要使用 --omit=optional)";
373
+ logWarn(`[Wave] ${reason}`);
374
+ return { available: false, reason };
375
+ }
376
+ const installDir = path.join(cliHome, "node_modules");
377
+ // One dependency failing must not skip the others: they degrade independently
378
+ // (a broken image codec has nothing to do with grep).
379
+ const failures = [];
380
+ for (const dependency of RUNTIME_DEPENDENCIES) {
381
+ if (dependency.isAvailable())
382
+ continue;
383
+ try {
384
+ const entry = findEntryPackageJson(fileURLToPath(moduleUrl), dependency.name);
385
+ if (!entry) {
386
+ fail(failures, `找不到声明 ${dependency.name} 依赖范围的 package.json`);
387
+ continue;
388
+ }
389
+ // A batch that completed but still will not load is a platform problem
390
+ // (wrong libc, unsupported CPU). Downloading ~19MB again on every launch
391
+ // would not fix it, so report instead.
392
+ if (readMarker(installDir)[dependency.name] === entry.range) {
393
+ fail(failures, `${dependency.name} 已安装但无法加载(平台 ${runtimePlatformKey(cliHome, dependency.libcAwarePlatformKey ?? true)})`);
394
+ continue;
395
+ }
396
+ const meta = await resolveVersion(dependency.name, entry.range, fetchImpl);
397
+ const platformKey = await installDependency(dependency, meta, cliHome, fetchImpl);
398
+ // Verify through the cli home itself: the only require base that proves
399
+ // the layout works for the bundle that will load it.
400
+ if (!dependency.loadsFrom(cliHome)) {
401
+ fs.rmSync(path.join(installDir, MARKER_FILE), { force: true });
402
+ fail(failures, `${dependency.name} 安装后仍无法加载(平台 ${platformKey})`);
403
+ continue;
404
+ }
405
+ dependency.onInstalled();
406
+ fs.writeFileSync(path.join(installDir, MARKER_FILE), `${JSON.stringify({ ...readMarker(installDir), [dependency.name]: entry.range }, null, 2)}\n`);
407
+ logger.info(`[Wave] 已安装 ${dependency.name} ${meta.version}(平台 ${platformKey})到 ${installDir}`);
408
+ }
409
+ catch (error) {
410
+ fail(failures, `安装 ${dependency.name} 失败:${error instanceof Error ? error.message : String(error)}`);
411
+ }
412
+ }
413
+ if (failures.length > 0)
414
+ return { available: false, reason: failures.join(";") };
415
+ return { available: true };
416
+ }
417
+ /** Log and record one dependency's failure, without aborting the batch. */
418
+ function fail(failures, reason) {
419
+ logWarn(`[Wave] ${reason}`);
420
+ failures.push(reason);
421
+ }
422
+ /** Forget the memoised outcome, and the settled result (tests). */
423
+ export function __resetRuntimeDepsForTesting() {
424
+ inFlight = undefined;
425
+ settled = undefined;
426
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { dirname } from "path";
3
+ import { parseFrontmatterYaml, splitFrontmatter } from "./frontmatterYaml.js";
3
4
  /**
4
5
  * Parse a SKILL.md file and validate its contents
5
6
  */
@@ -22,13 +23,12 @@ export function parseSkillFile(filePath, options = {}) {
22
23
  const content = readFileSync(filePath, "utf-8");
23
24
  result.content = content;
24
25
  // Parse YAML frontmatter
25
- const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
26
- if (!frontmatterMatch) {
26
+ const { yaml } = splitFrontmatter(content);
27
+ if (yaml === null) {
27
28
  result.validationErrors.push("Missing YAML frontmatter");
28
29
  return result;
29
30
  }
30
- const yamlContent = frontmatterMatch[1];
31
- result.frontmatter = parseYamlFrontmatter(yamlContent);
31
+ result.frontmatter = parseYamlFrontmatter(yaml);
32
32
  if (!result.frontmatter.name || !result.frontmatter.description) {
33
33
  result.validationErrors.push("Missing required fields: name and description");
34
34
  return result;
@@ -88,56 +88,26 @@ export function parseSkillFile(filePath, options = {}) {
88
88
  }
89
89
  }
90
90
  /**
91
- * Simple YAML frontmatter parser for skill files
91
+ * Simple YAML frontmatter parser for skill files.
92
+ *
93
+ * The value parsing lives in `frontmatterYaml.ts` so skills, subagents, custom
94
+ * slash commands, memory rules and memory files all read frontmatter the same
95
+ * way (block scalars included — see spec `ecosystem/agent-skills` 场景 2).
96
+ * Here we only narrow the result to the fields a skill declares.
92
97
  */
93
98
  function parseYamlFrontmatter(yamlContent) {
94
- const frontmatter = { name: "", description: "" };
95
- try {
96
- const lines = yamlContent.split("\n");
97
- let currentKey = null;
98
- for (const line of lines) {
99
- const trimmed = line.trim();
100
- if (!trimmed || trimmed.startsWith("#"))
101
- continue;
102
- // Check for list item
103
- if (trimmed.startsWith("-") && currentKey) {
104
- const value = trimmed
105
- .substring(1)
106
- .trim()
107
- .replace(/^["']|["']$/g, "");
108
- if (value) {
109
- const existing = frontmatter[currentKey];
110
- if (Array.isArray(existing)) {
111
- existing.push(value);
112
- }
113
- else {
114
- frontmatter[currentKey] = [value];
115
- }
116
- }
117
- continue;
118
- }
119
- const colonIndex = trimmed.indexOf(":");
120
- if (colonIndex === -1)
121
- continue;
122
- const key = trimmed.substring(0, colonIndex).trim();
123
- const value = trimmed
124
- .substring(colonIndex + 1)
125
- .trim()
126
- .replace(/^["']|["']$/g, "");
127
- currentKey = key;
128
- if (key && value) {
129
- frontmatter[key] = value;
130
- }
131
- else if (key) {
132
- // Key with no value on the same line, might be a list
133
- frontmatter[key] = [];
134
- }
135
- }
136
- }
137
- catch {
138
- // Return empty frontmatter on parse error - validation will catch missing fields
139
- }
140
- return frontmatter;
99
+ const parsed = parseFrontmatterYaml(yamlContent);
100
+ const asString = (key) => {
101
+ const value = parsed[key];
102
+ return typeof value === "string" ? value : undefined;
103
+ };
104
+ return {
105
+ ...parsed,
106
+ // Required fields must be strings: a bare `name:` / `description:` key
107
+ // parses to a list, which validation reports as missing.
108
+ name: asString("name") ?? "",
109
+ description: asString("description") ?? "",
110
+ };
141
111
  }
142
112
  /**
143
113
  * Validate skill metadata according to requirements
@@ -2,64 +2,60 @@ import { readFileSync, readdirSync, statSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join, extname, basename } from "path";
4
4
  import { logger } from "./globalLogger.js";
5
+ import { parseFrontmatterYaml, splitFrontmatter, } from "./frontmatterYaml.js";
5
6
  import { getBuiltinSubagentsDir } from "./configPaths.js";
6
7
  /**
7
- * Parse YAML frontmatter from markdown file content
8
+ * Parse YAML frontmatter from markdown file content.
9
+ *
10
+ * Value parsing is shared with skills, custom slash commands and memory files —
11
+ * see `frontmatterYaml.ts` (spec `multi-agent/subagent` 场景 6).
8
12
  */
9
13
  function parseFrontmatter(content) {
10
- const frontmatterRegex = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n([\s\S]*)$/;
11
- const match = content.match(frontmatterRegex);
12
- if (!match) {
14
+ const { yaml, body } = splitFrontmatter(content);
15
+ if (yaml === null) {
13
16
  return { frontmatter: {}, body: content.trim() };
14
17
  }
15
- const [, yamlContent, body] = match;
16
- const frontmatter = parseYamlFrontmatter(yamlContent);
17
- return { frontmatter, body: body.trim() };
18
+ return {
19
+ frontmatter: toSubagentFrontmatter(parseFrontmatterYaml(yaml)),
20
+ body: body.trim(),
21
+ };
18
22
  }
19
23
  /**
20
- * Simple YAML frontmatter parser for subagent files
24
+ * Narrow a parsed frontmatter block to the fields a subagent declares. Values
25
+ * of an unexpected shape (e.g. a bare `description:` key, which parses to a
26
+ * list) are dropped so `validateConfiguration` reports the field as missing.
21
27
  */
22
- function parseYamlFrontmatter(yamlContent) {
28
+ function toSubagentFrontmatter(parsed) {
23
29
  const frontmatter = {};
24
- try {
25
- const lines = yamlContent.split("\n");
26
- for (const line of lines) {
27
- const trimmed = line.trim();
28
- if (!trimmed || trimmed.startsWith("#"))
29
- continue;
30
- const colonIndex = trimmed.indexOf(":");
31
- if (colonIndex === -1)
32
- continue;
33
- const key = trimmed.substring(0, colonIndex).trim();
34
- const value = trimmed
35
- .substring(colonIndex + 1)
36
- .trim()
37
- .replace(/^["']|["']$/g, "");
38
- if (key && value) {
39
- // Handle array values for tools
40
- if (key === "tools" && value) {
41
- let arrayValue = value;
42
- if (arrayValue.startsWith("[") && arrayValue.endsWith("]")) {
43
- arrayValue = arrayValue.slice(1, -1);
44
- }
45
- frontmatter[key] = arrayValue
46
- .split(",")
47
- .map((s) => s.trim())
48
- .filter(Boolean);
49
- }
50
- else {
51
- if (key === "name" || key === "description" || key === "model") {
52
- frontmatter[key] = value;
53
- }
54
- }
55
- }
30
+ for (const key of ["name", "description", "model"]) {
31
+ const value = parsed[key];
32
+ if (typeof value === "string" && value) {
33
+ frontmatter[key] = value;
56
34
  }
57
35
  }
58
- catch {
59
- // Return empty frontmatter on parse error - validation will catch missing fields
36
+ const tools = normalizeTools(parsed.tools);
37
+ if (tools) {
38
+ frontmatter.tools = tools;
60
39
  }
61
40
  return frontmatter;
62
41
  }
42
+ /**
43
+ * Normalize the frontmatter `tools` field: a comma-separated string
44
+ * (`Read, Bash`), a bracketed list (`[Read, Bash]`) or a block list.
45
+ */
46
+ function normalizeTools(value) {
47
+ if (Array.isArray(value)) {
48
+ return value.map((item) => item.trim()).filter(Boolean);
49
+ }
50
+ if (typeof value !== "string") {
51
+ return undefined;
52
+ }
53
+ const list = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
54
+ return list
55
+ .split(",")
56
+ .map((item) => item.trim())
57
+ .filter(Boolean);
58
+ }
63
59
  /**
64
60
  * Validate subagent configuration
65
61
  */