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
|
@@ -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
|
|
26
|
-
if (
|
|
26
|
+
const { yaml } = splitFrontmatter(content);
|
|
27
|
+
if (yaml === null) {
|
|
27
28
|
result.validationErrors.push("Missing YAML frontmatter");
|
|
28
29
|
return result;
|
|
29
30
|
}
|
|
30
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
if (!match) {
|
|
14
|
+
const { yaml, body } = splitFrontmatter(content);
|
|
15
|
+
if (yaml === null) {
|
|
13
16
|
return { frontmatter: {}, body: content.trim() };
|
|
14
17
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
return {
|
|
19
|
+
frontmatter: toSubagentFrontmatter(parseFrontmatterYaml(yaml)),
|
|
20
|
+
body: body.trim(),
|
|
21
|
+
};
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
|
-
*
|
|
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
|
|
28
|
+
function toSubagentFrontmatter(parsed) {
|
|
23
29
|
const frontmatter = {};
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
*/
|