wave-agent-sdk 1.0.8 → 1.0.10
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/builtin/index.d.ts +1 -0
- package/dist/builtin/index.js +20 -0
- package/dist/builtin/plugins.d.ts +1 -0
- package/dist/builtin/plugins.js +225 -0
- package/dist/builtin/skills/artifact.d.ts +1 -0
- package/dist/builtin/skills/artifact.js +18 -0
- package/dist/builtin/skills/code-review.d.ts +1 -0
- package/{builtin/skills/code-review/SKILL.md → dist/builtin/skills/code-review.js} +23 -19
- package/dist/builtin/skills/deep-research.d.ts +1 -0
- package/{builtin/skills/deep-research/SKILL.md → dist/builtin/skills/deep-research.js} +18 -14
- package/dist/builtin/skills/init.d.ts +1 -0
- package/{builtin/skills/init/SKILL.md → dist/builtin/skills/init.js} +6 -3
- package/dist/builtin/skills/loop.d.ts +1 -0
- package/dist/builtin/skills/loop.js +83 -0
- package/dist/builtin/skills/settings.d.ts +1 -0
- package/dist/builtin/skills/settings.js +1224 -0
- package/dist/builtin/skills/simplify.d.ts +1 -0
- package/{builtin/skills/simplify/SKILL.md → dist/builtin/skills/simplify.js} +7 -3
- package/dist/builtin/subagents.d.ts +1 -0
- package/dist/builtin/subagents.js +164 -0
- package/dist/managers/aiManager.js +18 -1
- package/dist/managers/backgroundTaskManager.js +52 -4
- package/dist/managers/mcpManager.js +1 -1
- package/dist/managers/messageManager.d.ts +6 -0
- package/dist/managers/messageManager.js +33 -0
- package/dist/managers/skillManager.d.ts +13 -0
- package/dist/managers/skillManager.js +30 -0
- package/dist/managers/subagentManager.d.ts +8 -0
- package/dist/managers/subagentManager.js +57 -2
- package/dist/managers/toolManager.d.ts +8 -0
- package/dist/managers/toolManager.js +11 -0
- package/dist/services/artifactAvailability.d.ts +3 -1
- package/dist/services/artifactAvailability.js +9 -1
- package/dist/services/remoteSettingsService.js +2 -0
- package/dist/tools/bashTool.js +13 -2
- package/dist/tools/editTool.js +8 -2
- package/dist/tools/readTool.js +6 -3
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/writeTool.js +1 -0
- package/dist/types/messaging.d.ts +1 -0
- package/dist/utils/builtinEmbed.d.ts +21 -0
- package/dist/utils/builtinEmbed.js +53 -0
- package/dist/utils/configPaths.d.ts +0 -1
- package/dist/utils/configPaths.js +5 -26
- package/dist/utils/containerSetup.js +8 -0
- package/dist/utils/convertMessagesForAPI.js +21 -2
- package/dist/utils/encoding.d.ts +28 -0
- package/dist/utils/encoding.js +99 -0
- package/dist/utils/messageOperations.d.ts +1 -0
- package/dist/utils/toolImagePersistence.d.ts +25 -0
- package/dist/utils/toolImagePersistence.js +56 -0
- package/package.json +1 -3
- package/builtin/plugins/sdd/.wave-plugin/plugin.json +0 -8
- package/builtin/plugins/sdd/hooks/hooks.json +0 -14
- package/builtin/plugins/sdd/scripts/session-start.js +0 -24
- package/builtin/plugins/sdd/scripts/spec-count.js +0 -77
- package/builtin/plugins/sdd/skills/specify/SKILL.md +0 -47
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +0 -47
- package/builtin/skills/loop/SKILL.md +0 -79
- package/builtin/skills/settings/ENV.md +0 -78
- package/builtin/skills/settings/HOOKS.md +0 -227
- package/builtin/skills/settings/MCP.md +0 -137
- package/builtin/skills/settings/MEMORY.md +0 -76
- package/builtin/skills/settings/MODELS.md +0 -119
- package/builtin/skills/settings/PERMISSIONS.md +0 -88
- package/builtin/skills/settings/PLUGINS.md +0 -171
- package/builtin/skills/settings/SKILL.md +0 -125
- package/builtin/skills/settings/SKILLS.md +0 -107
- package/builtin/skills/settings/SUBAGENTS.md +0 -77
- package/builtin/subagents/bash.md +0 -19
- package/builtin/subagents/explore.md +0 -43
- package/builtin/subagents/general-purpose.md +0 -20
- package/builtin/subagents/plan.md +0 -56
- package/builtin/subagents/vision.md +0 -18
|
@@ -9,13 +9,21 @@
|
|
|
9
9
|
* "unset follows feature availability" semantics).
|
|
10
10
|
*/
|
|
11
11
|
import { loadMergedWaveConfig } from "./configurationService.js";
|
|
12
|
+
import { getRemoteSettingsSync } from "./remoteSettingsService.js";
|
|
12
13
|
/** Code default for Artifact availability. Flip to true after the frame backend goes live. */
|
|
13
14
|
export const ARTIFACT_DEFAULT_ENABLED = false;
|
|
14
15
|
/**
|
|
15
16
|
* Whether the Artifact tool should be registered / usable for the given workdir.
|
|
16
|
-
*
|
|
17
|
+
* Resolution order: remote managed settings (`enableArtifact` from
|
|
18
|
+
* `GET /api/wave/settings`, admin override) → explicit `enableArtifact` in local
|
|
19
|
+
* merged settings → code default.
|
|
17
20
|
*/
|
|
18
21
|
export function isArtifactEnabled(workdir) {
|
|
22
|
+
// Remote managed settings win (same last-write-wins semantics as `model`).
|
|
23
|
+
const remote = getRemoteSettingsSync();
|
|
24
|
+
if (remote?.enableArtifact !== undefined) {
|
|
25
|
+
return remote.enableArtifact;
|
|
26
|
+
}
|
|
19
27
|
if (workdir) {
|
|
20
28
|
const config = loadMergedWaveConfig(workdir);
|
|
21
29
|
if (config?.enableArtifact !== undefined) {
|
|
@@ -307,6 +307,8 @@ export function mergeRemoteSettings(localMerged, remote) {
|
|
|
307
307
|
result.marketplaces = remote.marketplaces;
|
|
308
308
|
if (remote.enabledPlugins !== undefined)
|
|
309
309
|
result.enabledPlugins = remote.enabledPlugins;
|
|
310
|
+
if (remote.enableArtifact !== undefined)
|
|
311
|
+
result.enableArtifact = remote.enableArtifact;
|
|
310
312
|
return result;
|
|
311
313
|
}
|
|
312
314
|
/**
|
package/dist/tools/bashTool.js
CHANGED
|
@@ -6,6 +6,7 @@ import { logger } from "../utils/globalLogger.js";
|
|
|
6
6
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
7
7
|
import { toPosixPath } from "../utils/path.js";
|
|
8
8
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
9
|
+
import { WindowsStreamDecoder } from "../utils/encoding.js";
|
|
9
10
|
import { processToolResult } from "../utils/toolResultStorage.js";
|
|
10
11
|
import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
|
|
11
12
|
import { BASH_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, READ_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, } from "../constants/tools.js";
|
|
@@ -250,6 +251,10 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
250
251
|
let isAborted = false;
|
|
251
252
|
let isBackgrounded = false;
|
|
252
253
|
let isFinished = false;
|
|
254
|
+
// On Windows, native tools (taskkill, powershell, ...) write GBK (cp936)
|
|
255
|
+
// instead of UTF-8; decode their byte streams accordingly (issue #1753).
|
|
256
|
+
const stdoutDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
257
|
+
const stderrDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
253
258
|
// Best-effort cleanup of the temp CWD file — used by abort/error/exit paths
|
|
254
259
|
const cleanupTempFile = () => {
|
|
255
260
|
try {
|
|
@@ -416,14 +421,14 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
416
421
|
}
|
|
417
422
|
child.stdout?.on("data", (data) => {
|
|
418
423
|
if (!isAborted && !isBackgrounded && !runInBackground) {
|
|
419
|
-
const chunk = stripAnsiColors(data.toString());
|
|
424
|
+
const chunk = stripAnsiColors(stdoutDecoder ? stdoutDecoder.push(data) : data.toString());
|
|
420
425
|
outputBuffer += chunk;
|
|
421
426
|
updateRealtimeResults();
|
|
422
427
|
}
|
|
423
428
|
});
|
|
424
429
|
child.stderr?.on("data", (data) => {
|
|
425
430
|
if (!isAborted && !isBackgrounded && !runInBackground) {
|
|
426
|
-
const chunk = stripAnsiColors(data.toString());
|
|
431
|
+
const chunk = stripAnsiColors(stderrDecoder ? stderrDecoder.push(data) : data.toString());
|
|
427
432
|
errorBuffer += chunk;
|
|
428
433
|
updateRealtimeResults();
|
|
429
434
|
}
|
|
@@ -467,6 +472,12 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
467
472
|
}
|
|
468
473
|
}
|
|
469
474
|
const exitCode = code ?? 0;
|
|
475
|
+
// Decode any bytes still held at stream end (e.g. a trailing UTF-8
|
|
476
|
+
// character split across the last chunk).
|
|
477
|
+
if (stdoutDecoder)
|
|
478
|
+
outputBuffer += stdoutDecoder.flush();
|
|
479
|
+
if (stderrDecoder)
|
|
480
|
+
errorBuffer += stderrDecoder.flush();
|
|
470
481
|
const combinedOutput = outputBuffer + (errorBuffer ? "\n" + errorBuffer : "");
|
|
471
482
|
// Prepend CWD change message to output if present
|
|
472
483
|
const finalOutput = recoveryNotice +
|
package/dist/tools/editTool.js
CHANGED
|
@@ -168,7 +168,11 @@ Usage:
|
|
|
168
168
|
if (replaceAll) {
|
|
169
169
|
// Replace all matches
|
|
170
170
|
const regex = new RegExp(escapeRegExp(matchedOldString), "g");
|
|
171
|
-
|
|
171
|
+
// Function replacer (claude-code's applyEditToFile approach): newString
|
|
172
|
+
// is inserted literally, never parsed as a $ replacement template. A
|
|
173
|
+
// string replacer would expand $& to the matched text, $$ to a single
|
|
174
|
+
// $, and `$` could even truncate and duplicate the file (issue #1752).
|
|
175
|
+
newContent = normalizedContent.replace(regex, () => newString);
|
|
172
176
|
replacementCount = (normalizedContent.match(regex) || []).length;
|
|
173
177
|
}
|
|
174
178
|
else {
|
|
@@ -181,7 +185,8 @@ Usage:
|
|
|
181
185
|
error: `old_string appears ${matches} times in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all=true to change every instance.`,
|
|
182
186
|
};
|
|
183
187
|
}
|
|
184
|
-
|
|
188
|
+
// Function replacer: see note above — $ in newString stays literal
|
|
189
|
+
newContent = normalizedContent.replace(matchedOldString, () => newString);
|
|
185
190
|
replacementCount = 1;
|
|
186
191
|
}
|
|
187
192
|
// Permission check after validation but before real operation
|
|
@@ -238,6 +243,7 @@ Usage:
|
|
|
238
243
|
context.readFileState.set(resolvedPath, {
|
|
239
244
|
mtime: newStats.mtime.getTime(),
|
|
240
245
|
hash,
|
|
246
|
+
source: "edit",
|
|
241
247
|
offset: undefined,
|
|
242
248
|
limit: undefined,
|
|
243
249
|
});
|
package/dist/tools/readTool.js
CHANGED
|
@@ -208,12 +208,14 @@ Usage:
|
|
|
208
208
|
: resolvePath(filePath, context.workdir);
|
|
209
209
|
const stats = await stat(actualFilePath);
|
|
210
210
|
// Deduplication: only dedup if the exact same range was read before
|
|
211
|
+
// (aligned with Claude Code — full reads dedup too). Entries written by
|
|
212
|
+
// Edit/Write have source "edit"/"write" and never dedup, so a file that
|
|
213
|
+
// was written before its first Read is still returned in full.
|
|
211
214
|
if (context.readFileState) {
|
|
212
215
|
const state = context.readFileState.get(actualFilePath);
|
|
213
|
-
// Only dedup entries from a prior Read with explicit offset (not Edit/Write entries)
|
|
214
216
|
if (state &&
|
|
215
|
-
state.
|
|
216
|
-
state.
|
|
217
|
+
state.source === "read" &&
|
|
218
|
+
state.mtime === stats.mtime.getTime()) {
|
|
217
219
|
const rangeMatch = state.offset === offset && state.limit === limit;
|
|
218
220
|
if (rangeMatch) {
|
|
219
221
|
return {
|
|
@@ -249,6 +251,7 @@ Usage:
|
|
|
249
251
|
context.readFileState.set(actualFilePath, {
|
|
250
252
|
mtime: stats.mtime.getTime(),
|
|
251
253
|
hash,
|
|
254
|
+
source: "read",
|
|
252
255
|
offset, // undefined for full reads
|
|
253
256
|
limit, // undefined for full reads
|
|
254
257
|
});
|
package/dist/tools/types.d.ts
CHANGED
package/dist/tools/writeTool.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin content embedding.
|
|
3
|
+
*
|
|
4
|
+
* Builtin skills/subagents/plugins ship as TS source (src/builtin/*.ts) and are
|
|
5
|
+
* compiled into the bundle. At runtime they are lazily materialized to a
|
|
6
|
+
* content-hashed cache directory under the OS temp dir, so consumers that
|
|
7
|
+
* readdirSync real directories keep working unchanged — including inside
|
|
8
|
+
* esbuild bundles that never ship a `builtin/` dir on disk.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Content-hash key: sha256 of the sorted `rel\0content` entries, truncated.
|
|
12
|
+
* Any change to builtin content yields a new key, auto-invalidating stale caches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getBuiltinCacheKey(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Materialize builtin content to `{tmpdir}/wave-builtin/{contentHash}/` and
|
|
17
|
+
* return the directory. Memoized per process; idempotent across processes via
|
|
18
|
+
* the `.wave-builtin-complete` marker written last (a torn write leaves no
|
|
19
|
+
* marker, so the next run rewrites).
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureBuiltinMaterialized(): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin content embedding.
|
|
3
|
+
*
|
|
4
|
+
* Builtin skills/subagents/plugins ship as TS source (src/builtin/*.ts) and are
|
|
5
|
+
* compiled into the bundle. At runtime they are lazily materialized to a
|
|
6
|
+
* content-hashed cache directory under the OS temp dir, so consumers that
|
|
7
|
+
* readdirSync real directories keep working unchanged — including inside
|
|
8
|
+
* esbuild bundles that never ship a `builtin/` dir on disk.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { BUILTIN_CONTENT } from "../builtin/index.js";
|
|
15
|
+
const BUILTIN_CACHE_ROOT = "wave-builtin";
|
|
16
|
+
const BUILTIN_CACHE_COMPLETE = ".wave-builtin-complete";
|
|
17
|
+
/**
|
|
18
|
+
* Content-hash key: sha256 of the sorted `rel\0content` entries, truncated.
|
|
19
|
+
* Any change to builtin content yields a new key, auto-invalidating stale caches.
|
|
20
|
+
*/
|
|
21
|
+
export function getBuiltinCacheKey() {
|
|
22
|
+
const entries = Object.keys(BUILTIN_CONTENT)
|
|
23
|
+
.sort()
|
|
24
|
+
.map((rel) => `${rel}\0${BUILTIN_CONTENT[rel]}`);
|
|
25
|
+
return createHash("sha256")
|
|
26
|
+
.update(entries.join("\0"))
|
|
27
|
+
.digest("hex")
|
|
28
|
+
.slice(0, 16);
|
|
29
|
+
}
|
|
30
|
+
let _builtinDir;
|
|
31
|
+
/**
|
|
32
|
+
* Materialize builtin content to `{tmpdir}/wave-builtin/{contentHash}/` and
|
|
33
|
+
* return the directory. Memoized per process; idempotent across processes via
|
|
34
|
+
* the `.wave-builtin-complete` marker written last (a torn write leaves no
|
|
35
|
+
* marker, so the next run rewrites).
|
|
36
|
+
*/
|
|
37
|
+
export function ensureBuiltinMaterialized() {
|
|
38
|
+
if (_builtinDir)
|
|
39
|
+
return _builtinDir;
|
|
40
|
+
const dir = join(tmpdir(), BUILTIN_CACHE_ROOT, getBuiltinCacheKey());
|
|
41
|
+
const marker = join(dir, BUILTIN_CACHE_COMPLETE);
|
|
42
|
+
if (!existsSync(marker)) {
|
|
43
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
|
+
for (const [rel, content] of Object.entries(BUILTIN_CONTENT)) {
|
|
45
|
+
const filePath = join(dir, rel);
|
|
46
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
47
|
+
writeFileSync(filePath, content);
|
|
48
|
+
}
|
|
49
|
+
writeFileSync(marker, "");
|
|
50
|
+
}
|
|
51
|
+
_builtinDir = dir;
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - Local configs: {workdir}/.wave/settings.local.json > {workdir}/.wave/settings.json
|
|
10
10
|
* - Project configs override user configs (existing behavior)
|
|
11
11
|
*/
|
|
12
|
-
export declare function getPackageRoot(): string;
|
|
13
12
|
/**
|
|
14
13
|
* Get the builtin skills directory path
|
|
15
14
|
*/
|
|
@@ -9,48 +9,27 @@
|
|
|
9
9
|
* - Local configs: {workdir}/.wave/settings.local.json > {workdir}/.wave/settings.json
|
|
10
10
|
* - Project configs override user configs (existing behavior)
|
|
11
11
|
*/
|
|
12
|
-
import { join
|
|
12
|
+
import { join } from "path";
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
import { existsSync } from "fs";
|
|
15
|
-
import {
|
|
16
|
-
import { findUpSync } from "find-up";
|
|
17
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
-
const __dirname = dirname(__filename);
|
|
19
|
-
/**
|
|
20
|
-
* Resolve the package root directory by finding the nearest package.json.
|
|
21
|
-
* Works correctly even when bundled (e.g. esbuild into a VS Code extension),
|
|
22
|
-
* as long as vendor/ and builtin/ remain alongside package.json on disk.
|
|
23
|
-
*/
|
|
24
|
-
let _packageRoot;
|
|
25
|
-
export function getPackageRoot() {
|
|
26
|
-
if (_packageRoot)
|
|
27
|
-
return _packageRoot;
|
|
28
|
-
const pkgPath = findUpSync("package.json", { cwd: __dirname });
|
|
29
|
-
if (pkgPath) {
|
|
30
|
-
_packageRoot = dirname(pkgPath);
|
|
31
|
-
return _packageRoot;
|
|
32
|
-
}
|
|
33
|
-
// Fallback: relative to this file (works during development)
|
|
34
|
-
_packageRoot = join(__dirname, "..", "..");
|
|
35
|
-
return _packageRoot;
|
|
36
|
-
}
|
|
15
|
+
import { ensureBuiltinMaterialized } from "./builtinEmbed.js";
|
|
37
16
|
/**
|
|
38
17
|
* Get the builtin skills directory path
|
|
39
18
|
*/
|
|
40
19
|
export function getBuiltinSkillsDir() {
|
|
41
|
-
return join(
|
|
20
|
+
return join(ensureBuiltinMaterialized(), "skills");
|
|
42
21
|
}
|
|
43
22
|
/**
|
|
44
23
|
* Get the builtin subagents directory path
|
|
45
24
|
*/
|
|
46
25
|
export function getBuiltinSubagentsDir() {
|
|
47
|
-
return join(
|
|
26
|
+
return join(ensureBuiltinMaterialized(), "subagents");
|
|
48
27
|
}
|
|
49
28
|
/**
|
|
50
29
|
* Get the builtin plugins directory path
|
|
51
30
|
*/
|
|
52
31
|
export function getBuiltinPluginsDir() {
|
|
53
|
-
return join(
|
|
32
|
+
return join(ensureBuiltinMaterialized(), "plugins");
|
|
54
33
|
}
|
|
55
34
|
/**
|
|
56
35
|
* Get the user-specific configuration file path (legacy function)
|
|
@@ -219,6 +219,14 @@ export function setupAgentContainer(setupOptions) {
|
|
|
219
219
|
onReload: () => {
|
|
220
220
|
const models = configurationService.getConfiguredModels();
|
|
221
221
|
callbacks.onConfiguredModelsChange?.(models);
|
|
222
|
+
// Re-evaluate feature-gated tools (e.g. Artifact behind
|
|
223
|
+
// enableArtifact) so toggling the flag applies without a restart.
|
|
224
|
+
toolManager.reloadFeatureGatedTools();
|
|
225
|
+
// Same gate for the builtin /artifact skill: refresh emits "refreshed"
|
|
226
|
+
// so slash-command registration follows enableArtifact.
|
|
227
|
+
void skillManager.reloadFeatureGatedSkills().catch((error) => {
|
|
228
|
+
logger.error("Failed to reload feature-gated skills:", error);
|
|
229
|
+
});
|
|
222
230
|
},
|
|
223
231
|
});
|
|
224
232
|
container.register("LiveConfigManager", liveConfigManager);
|
|
@@ -102,10 +102,29 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
105
|
-
// Non-vision model: replace images with a text placeholder
|
|
105
|
+
// Non-vision model: replace images with a text placeholder and
|
|
106
|
+
// append [Image source: <path>] metadata for MCP images that
|
|
107
|
+
// were persisted to temp files, so the main model can delegate
|
|
108
|
+
// recognition to a vision subagent (which reads the path with
|
|
109
|
+
// the Read tool). Images without a persisted path (e.g. legacy
|
|
110
|
+
// history) fall back to the placeholder only.
|
|
111
|
+
const contentParts = [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: "[Tool returned an image, but the current model does not support image recognition]",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
toolBlock.images.forEach((image) => {
|
|
118
|
+
if (image.path) {
|
|
119
|
+
contentParts.push({
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `[Image source: ${image.path}]`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
106
125
|
imageUserMessages.push({
|
|
107
126
|
role: "user",
|
|
108
|
-
content:
|
|
127
|
+
content: contentParts,
|
|
109
128
|
});
|
|
110
129
|
}
|
|
111
130
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming byte-to-text decoder for bash tool output on Windows.
|
|
3
|
+
*
|
|
4
|
+
* Git Bash (MSYS) tools emit UTF-8, but native Windows programs (taskkill,
|
|
5
|
+
* powershell, ping, ...) write the system OEM code page — GBK (cp936) on
|
|
6
|
+
* zh-CN systems. Node's default `data.toString()` decodes every stream as
|
|
7
|
+
* UTF-8, so GBK bytes turn into U+FFFD mojibake (issue #1753).
|
|
8
|
+
*
|
|
9
|
+
* Strategy (per-chunk buffering, decide-once):
|
|
10
|
+
* - Accumulate raw bytes; try strict UTF-8 (`fatal: true`) over everything
|
|
11
|
+
* buffered so far. If it decodes cleanly, emit the text.
|
|
12
|
+
* - If strict UTF-8 fails, hold back up to 3 trailing bytes (a UTF-8
|
|
13
|
+
* character split across chunk boundaries) and retry on the next chunk.
|
|
14
|
+
* - Once even the head is not valid UTF-8, the stream is GBK: re-decode all
|
|
15
|
+
* buffered bytes with GBK and switch to GBK for every subsequent chunk.
|
|
16
|
+
* - `flush()` decodes any leftover buffered bytes (leniently) at stream end.
|
|
17
|
+
*/
|
|
18
|
+
export declare class WindowsStreamDecoder {
|
|
19
|
+
private pending;
|
|
20
|
+
private gbkDecoder;
|
|
21
|
+
/** Longest possible incomplete UTF-8 tail at a chunk boundary is 3 bytes. */
|
|
22
|
+
private static readonly MAX_UTF8_TRAIL_BYTES;
|
|
23
|
+
push(data: Buffer): string;
|
|
24
|
+
/** Decode any bytes still held at stream end (lenient UTF-8, else GBK). */
|
|
25
|
+
flush(): string;
|
|
26
|
+
}
|
|
27
|
+
/** Decode a single complete byte sequence (used for non-streaming reads). */
|
|
28
|
+
export declare function decodeBytes(buf: Buffer): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming byte-to-text decoder for bash tool output on Windows.
|
|
3
|
+
*
|
|
4
|
+
* Git Bash (MSYS) tools emit UTF-8, but native Windows programs (taskkill,
|
|
5
|
+
* powershell, ping, ...) write the system OEM code page — GBK (cp936) on
|
|
6
|
+
* zh-CN systems. Node's default `data.toString()` decodes every stream as
|
|
7
|
+
* UTF-8, so GBK bytes turn into U+FFFD mojibake (issue #1753).
|
|
8
|
+
*
|
|
9
|
+
* Strategy (per-chunk buffering, decide-once):
|
|
10
|
+
* - Accumulate raw bytes; try strict UTF-8 (`fatal: true`) over everything
|
|
11
|
+
* buffered so far. If it decodes cleanly, emit the text.
|
|
12
|
+
* - If strict UTF-8 fails, hold back up to 3 trailing bytes (a UTF-8
|
|
13
|
+
* character split across chunk boundaries) and retry on the next chunk.
|
|
14
|
+
* - Once even the head is not valid UTF-8, the stream is GBK: re-decode all
|
|
15
|
+
* buffered bytes with GBK and switch to GBK for every subsequent chunk.
|
|
16
|
+
* - `flush()` decodes any leftover buffered bytes (leniently) at stream end.
|
|
17
|
+
*/
|
|
18
|
+
export class WindowsStreamDecoder {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.pending = Buffer.alloc(0);
|
|
21
|
+
this.gbkDecoder = null;
|
|
22
|
+
}
|
|
23
|
+
push(data) {
|
|
24
|
+
// Already determined GBK: decode each chunk as it arrives.
|
|
25
|
+
if (this.gbkDecoder) {
|
|
26
|
+
return this.gbkDecoder.decode(data);
|
|
27
|
+
}
|
|
28
|
+
this.pending = Buffer.concat([this.pending, data]);
|
|
29
|
+
try {
|
|
30
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(this.pending);
|
|
31
|
+
this.pending = Buffer.alloc(0);
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Strict UTF-8 failed. The trailing bytes may be a UTF-8 sequence split
|
|
36
|
+
// across chunks: hold only the trailing run of non-ASCII bytes (max 3,
|
|
37
|
+
// the longest incomplete UTF-8 tail). Everything before it is already
|
|
38
|
+
// decodable and is emitted immediately.
|
|
39
|
+
const n = this.pending.length;
|
|
40
|
+
let keep = 0;
|
|
41
|
+
while (keep < WindowsStreamDecoder.MAX_UTF8_TRAIL_BYTES &&
|
|
42
|
+
keep < n &&
|
|
43
|
+
this.pending[n - 1 - keep] >= 0x80) {
|
|
44
|
+
keep++;
|
|
45
|
+
}
|
|
46
|
+
const head = this.pending.subarray(0, n - keep);
|
|
47
|
+
if (head.length > 0) {
|
|
48
|
+
try {
|
|
49
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(head);
|
|
50
|
+
this.pending = Buffer.from(this.pending.subarray(n - keep));
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Head is not valid UTF-8 → the whole stream is GBK. Re-decode
|
|
55
|
+
// everything accumulated so far and commit to GBK.
|
|
56
|
+
this.gbkDecoder = new TextDecoder("gbk");
|
|
57
|
+
const text = this.gbkDecoder.decode(this.pending);
|
|
58
|
+
this.pending = Buffer.alloc(0);
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Only the trailing bytes are suspect: hold them for the next chunk.
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Decode any bytes still held at stream end (lenient UTF-8, else GBK). */
|
|
67
|
+
flush() {
|
|
68
|
+
if (this.pending.length === 0)
|
|
69
|
+
return "";
|
|
70
|
+
const rest = this.pending;
|
|
71
|
+
this.pending = Buffer.alloc(0);
|
|
72
|
+
if (this.gbkDecoder) {
|
|
73
|
+
return this.gbkDecoder.decode(rest);
|
|
74
|
+
}
|
|
75
|
+
const utf8 = rest.toString("utf-8");
|
|
76
|
+
if (!utf8.includes("\uFFFD"))
|
|
77
|
+
return utf8;
|
|
78
|
+
try {
|
|
79
|
+
return new TextDecoder("gbk").decode(rest);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return utf8;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Longest possible incomplete UTF-8 tail at a chunk boundary is 3 bytes. */
|
|
87
|
+
WindowsStreamDecoder.MAX_UTF8_TRAIL_BYTES = 3;
|
|
88
|
+
/** Decode a single complete byte sequence (used for non-streaming reads). */
|
|
89
|
+
export function decodeBytes(buf) {
|
|
90
|
+
const utf8 = buf.toString("utf-8");
|
|
91
|
+
if (!utf8.includes("\uFFFD"))
|
|
92
|
+
return utf8;
|
|
93
|
+
try {
|
|
94
|
+
return new TextDecoder("gbk").decode(buf);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return utf8;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for MCP tool-returned images.
|
|
3
|
+
*
|
|
4
|
+
* MCP tools can return image content blocks as in-memory base64. When the
|
|
5
|
+
* agent's model does not support vision, the image is written to a temp file
|
|
6
|
+
* so convertMessagesForAPI can attach `[Image source: <path>]` metadata and
|
|
7
|
+
* the main model can delegate recognition to the vision subagent (which reads
|
|
8
|
+
* the path with the Read tool). Vision-capable models keep the inline base64
|
|
9
|
+
* and never write to disk.
|
|
10
|
+
*/
|
|
11
|
+
export interface PersistedToolImage {
|
|
12
|
+
data: string;
|
|
13
|
+
mediaType?: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Persist base64 images to temp files under /tmp/wave-mcp-images/.
|
|
18
|
+
* Uses the OS tmpdir for simplicity and automatic OS cleanup (same convention
|
|
19
|
+
* as /tmp/wave-tool-results/). Returns each image unchanged (no path) when the
|
|
20
|
+
* write fails, so the caller degrades to the placeholder-only behavior.
|
|
21
|
+
*/
|
|
22
|
+
export declare function persistToolImages(images: Array<{
|
|
23
|
+
data: string;
|
|
24
|
+
mediaType?: string;
|
|
25
|
+
}>): PersistedToolImage[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for MCP tool-returned images.
|
|
3
|
+
*
|
|
4
|
+
* MCP tools can return image content blocks as in-memory base64. When the
|
|
5
|
+
* agent's model does not support vision, the image is written to a temp file
|
|
6
|
+
* so convertMessagesForAPI can attach `[Image source: <path>]` metadata and
|
|
7
|
+
* the main model can delegate recognition to the vision subagent (which reads
|
|
8
|
+
* the path with the Read tool). Vision-capable models keep the inline base64
|
|
9
|
+
* and never write to disk.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { logger } from "./globalLogger.js";
|
|
15
|
+
const MCP_IMAGES_DIR = path.join(os.tmpdir(), "wave-mcp-images");
|
|
16
|
+
/** Map MCP mimeType to a file extension. Unknown types fall back to .png. */
|
|
17
|
+
const MIME_TO_EXT = {
|
|
18
|
+
"image/png": ".png",
|
|
19
|
+
"image/jpeg": ".jpg",
|
|
20
|
+
"image/webp": ".webp",
|
|
21
|
+
"image/gif": ".gif",
|
|
22
|
+
"image/bmp": ".bmp",
|
|
23
|
+
"image/svg+xml": ".svg",
|
|
24
|
+
"image/avif": ".avif",
|
|
25
|
+
"image/x-icon": ".ico",
|
|
26
|
+
"image/tiff": ".tiff",
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Persist base64 images to temp files under /tmp/wave-mcp-images/.
|
|
30
|
+
* Uses the OS tmpdir for simplicity and automatic OS cleanup (same convention
|
|
31
|
+
* as /tmp/wave-tool-results/). Returns each image unchanged (no path) when the
|
|
32
|
+
* write fails, so the caller degrades to the placeholder-only behavior.
|
|
33
|
+
*/
|
|
34
|
+
export function persistToolImages(images) {
|
|
35
|
+
return images.map((image) => {
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(MCP_IMAGES_DIR, { recursive: true });
|
|
38
|
+
// Strip a data: URL prefix if present (MCP spec carries raw base64, but
|
|
39
|
+
// be defensive about dataURL-formatted data).
|
|
40
|
+
let data = image.data;
|
|
41
|
+
if (data.startsWith("data:")) {
|
|
42
|
+
const commaIndex = data.indexOf(",");
|
|
43
|
+
data = commaIndex >= 0 ? data.slice(commaIndex + 1) : data;
|
|
44
|
+
}
|
|
45
|
+
const ext = MIME_TO_EXT[image.mediaType || ""] || ".png";
|
|
46
|
+
const id = `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
47
|
+
const filePath = path.join(MCP_IMAGES_DIR, `mcp-image_${id}${ext}`);
|
|
48
|
+
fs.writeFileSync(filePath, Buffer.from(data, "base64"));
|
|
49
|
+
return { ...image, path: filePath };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger?.error("Failed to persist MCP tool image:", error);
|
|
53
|
+
return image;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-agent-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "SDK for building AI-powered development tools and agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
|
-
"builtin",
|
|
40
39
|
"README.md"
|
|
41
40
|
],
|
|
42
41
|
"dependencies": {
|
|
@@ -51,7 +50,6 @@
|
|
|
51
50
|
"@vscode/ripgrep": "^1.18.0",
|
|
52
51
|
"chokidar": "^4.0.3",
|
|
53
52
|
"cron-parser": "^5.5.0",
|
|
54
|
-
"find-up": "^8.0.0",
|
|
55
53
|
"fuzzysort": "^3.1.0",
|
|
56
54
|
"glob": "^13.0.0",
|
|
57
55
|
"lru-cache": "^11.3.5",
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SessionStart hook for the sdd built-in plugin.
|
|
3
|
-
// Emits the spec-first workflow guidance as additionalContext (JSON form),
|
|
4
|
-
// resolving the absolute path to the plugin's spec-count validator so the
|
|
5
|
-
// agent can run it from its bash tool (which does not carry WAVE_PLUGIN_ROOT).
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
|
|
8
|
-
const root =
|
|
9
|
-
process.env.WAVE_PLUGIN_ROOT ||
|
|
10
|
-
path.dirname(new URL("..", import.meta.url).pathname);
|
|
11
|
-
const specCount = `node ${JSON.stringify(path.join(root, "scripts", "spec-count.js"))}`;
|
|
12
|
-
|
|
13
|
-
const guidance = [
|
|
14
|
-
"Spec-First Workflow(规格优先工作流):",
|
|
15
|
-
"- 需求增加或变更时,优先更新 spec:先更新对应规格说明(新增用户故事、验收场景),待用户确认 spec 后再实现代码。spec 是功能设计的权威来源,不是 changelog。",
|
|
16
|
-
"- 边界模糊时也先写 spec 草稿请用户确认,不要直接改代码。",
|
|
17
|
-
"- 规格编写技能(specify)由 AI 自动触发:对话中涉及新需求或需求变更时主动创建或更新规格文件,不需要用户手动调用(不出现在斜杠命令列表中)。",
|
|
18
|
-
`- 新增或修改 spec 后运行校验:${specCount}(自动检测 docs/specs/,否则 specs/,否则退出)。`,
|
|
19
|
-
].join("\n");
|
|
20
|
-
|
|
21
|
-
// JSON form → parsed as hookSpecificOutput.additionalContext by the hook manager.
|
|
22
|
-
console.log(
|
|
23
|
-
JSON.stringify({ hookSpecificOutput: { additionalContext: guidance } }),
|
|
24
|
-
);
|