wave-agent-sdk 1.0.8 → 1.0.9
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/builtin/skills/artifact/SKILL.md +14 -0
- package/builtin/skills/settings/ENV.md +1 -1
- package/builtin/skills/settings/SKILL.md +7 -0
- package/builtin/skills/settings/SUBAGENTS.md +1 -1
- package/dist/managers/backgroundTaskManager.js +52 -4
- package/dist/managers/skillManager.d.ts +13 -0
- package/dist/managers/skillManager.js +30 -0
- 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 +7 -2
- package/dist/utils/containerSetup.js +8 -0
- package/dist/utils/encoding.d.ts +28 -0
- package/dist/utils/encoding.js +99 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: artifact
|
|
3
|
+
description: Publish a local HTML or Markdown file as a shareable web page
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Artifact: Publish a File as a Shareable Web Page
|
|
8
|
+
|
|
9
|
+
Publish a local `.html` or `.md` file as a default-private, shareable web page.
|
|
10
|
+
|
|
11
|
+
- If a file path was provided ($ARGUMENTS / $1), use it directly as the `file_path`.
|
|
12
|
+
- Otherwise, infer which file to publish from the conversation context; if it is not clear, ask the user which file to publish.
|
|
13
|
+
|
|
14
|
+
Call the `Artifact` tool with the resolved `file_path` (and `favicon` if relevant), then report the resulting URL to the user.
|
|
@@ -23,7 +23,7 @@ Wave uses several environment variables to control its core functionality. Varia
|
|
|
23
23
|
| :--- | :--- | :--- |
|
|
24
24
|
| `WAVE_API_KEY` | API key for the AI gateway. | - |
|
|
25
25
|
| `WAVE_BASE_URL` | Base URL for the AI gateway. | - |
|
|
26
|
-
| `WAVE_SERVER_URL` | Server URL for SSO authentication.
|
|
26
|
+
| `WAVE_SERVER_URL` | Server URL for SSO authentication. Resolution order: `options.serverUrl` → `process.env.WAVE_SERVER_URL` → default. Unlike other `WAVE_*` vars, a settings.json `env` value is also mirrored to `process.env` so process-level singletons (AuthService) see it without a per-session snapshot. | `https://codechat.codewave.163.com` |
|
|
27
27
|
| `WAVE_CUSTOM_HEADERS` | Custom HTTP headers for the AI gateway. Newline-separated `Key: Value` pairs (e.g., `"X-Foo: bar\nAuthorization: Bearer xxx"`). | - |
|
|
28
28
|
| `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
|
|
29
29
|
| `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
|
|
@@ -104,6 +104,13 @@ For detailed guidance on creating plugins and marketplaces, see [PLUGINS.md](${W
|
|
|
104
104
|
- `language`: Preferred language for agent communication (e.g., `"en"`, `"zh"`).
|
|
105
105
|
- `autoMemoryEnabled`: Enable or disable auto-memory (default: `true`).
|
|
106
106
|
- `autoMemoryFrequency`: Frequency of auto-memory extraction turns (default: `1`).
|
|
107
|
+
- `enableArtifact`: Enable the Artifact tool, which publishes local `.html`/`.md` files as shareable (default-private) web pages. Defaults to `false` while the frame backend is not live; set to `true` to register the tool and enable WebFetch interception for artifact URLs. Toggling it hot-reloads the tool registry.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"enableArtifact": true
|
|
112
|
+
}
|
|
113
|
+
```
|
|
107
114
|
|
|
108
115
|
## How to use this skill
|
|
109
116
|
|
|
@@ -35,7 +35,7 @@ You are a specialized subagent for a specific task. Your goal is to:
|
|
|
35
35
|
- `name`: (Required) Unique identifier.
|
|
36
36
|
- `description`: (Required) Explains the subagent's expertise and when to use it.
|
|
37
37
|
- `tools`: (Optional) List of tools the subagent can use.
|
|
38
|
-
- `model`: (Optional) Overrides the default model for this subagent.
|
|
38
|
+
- `model`: (Optional) Overrides the default model for this subagent. The special values `fastModel` and `visionModel` resolve to the `WAVE_FAST_MODEL` / `WAVE_VISION_MODEL` env vars respectively. Built-in subagents declaring `model: visionModel` (e.g. the built-in `vision` agent) are only registered when `WAVE_VISION_MODEL` is set; for user-defined subagents the value simply resolves to the configured vision model.
|
|
39
39
|
|
|
40
40
|
## Subagent Locations
|
|
41
41
|
|
|
@@ -3,6 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
6
|
+
import { WindowsStreamDecoder } from "../utils/encoding.js";
|
|
6
7
|
import { logger } from "../utils/globalLogger.js";
|
|
7
8
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
8
9
|
export class BackgroundTaskManager {
|
|
@@ -116,8 +117,12 @@ export class BackgroundTaskManager {
|
|
|
116
117
|
}
|
|
117
118
|
}, timeout);
|
|
118
119
|
}
|
|
120
|
+
// On Windows, native tools (taskkill, powershell, ...) write GBK (cp936)
|
|
121
|
+
// instead of UTF-8; decode their byte streams accordingly (issue #1753).
|
|
122
|
+
const stdoutDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
123
|
+
const stderrDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
119
124
|
const onStdout = (data) => {
|
|
120
|
-
const stripped = stripAnsiColors(data.toString());
|
|
125
|
+
const stripped = stripAnsiColors(stdoutDecoder ? stdoutDecoder.push(data) : data.toString());
|
|
121
126
|
shell.stdout += stripped;
|
|
122
127
|
if (logStream.writable) {
|
|
123
128
|
logStream.write(stripped);
|
|
@@ -125,7 +130,7 @@ export class BackgroundTaskManager {
|
|
|
125
130
|
this.notifyTasksChange();
|
|
126
131
|
};
|
|
127
132
|
const onStderr = (data) => {
|
|
128
|
-
const stripped = stripAnsiColors(data.toString());
|
|
133
|
+
const stripped = stripAnsiColors(stderrDecoder ? stderrDecoder.push(data) : data.toString());
|
|
129
134
|
shell.stderr += stripped;
|
|
130
135
|
if (logStream.writable) {
|
|
131
136
|
logStream.write(stripped);
|
|
@@ -136,6 +141,26 @@ export class BackgroundTaskManager {
|
|
|
136
141
|
if (timeoutHandle) {
|
|
137
142
|
clearTimeout(timeoutHandle);
|
|
138
143
|
}
|
|
144
|
+
// Decode any bytes still held at stream end (e.g. a trailing UTF-8
|
|
145
|
+
// character split across the last chunk).
|
|
146
|
+
if (stdoutDecoder) {
|
|
147
|
+
const rest = stdoutDecoder.flush();
|
|
148
|
+
if (rest) {
|
|
149
|
+
shell.stdout += rest;
|
|
150
|
+
if (logStream.writable) {
|
|
151
|
+
logStream.write(rest);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (stderrDecoder) {
|
|
156
|
+
const rest = stderrDecoder.flush();
|
|
157
|
+
if (rest) {
|
|
158
|
+
shell.stderr += rest;
|
|
159
|
+
if (logStream.writable) {
|
|
160
|
+
logStream.write(rest);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
139
164
|
if (logStream.writable) {
|
|
140
165
|
logStream.end();
|
|
141
166
|
}
|
|
@@ -256,8 +281,12 @@ export class BackgroundTaskManager {
|
|
|
256
281
|
};
|
|
257
282
|
this.tasks.set(id, shell);
|
|
258
283
|
this.notifyTasksChange();
|
|
284
|
+
// On Windows, native tools write GBK (cp936) instead of UTF-8; decode
|
|
285
|
+
// their byte streams accordingly (issue #1753).
|
|
286
|
+
const stdoutDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
287
|
+
const stderrDecoder = process.platform === "win32" ? new WindowsStreamDecoder() : null;
|
|
259
288
|
child.stdout?.on("data", (data) => {
|
|
260
|
-
const stripped = stripAnsiColors(data.toString());
|
|
289
|
+
const stripped = stripAnsiColors(stdoutDecoder ? stdoutDecoder.push(data) : data.toString());
|
|
261
290
|
shell.stdout += stripped;
|
|
262
291
|
if (logStream.writable) {
|
|
263
292
|
logStream.write(stripped);
|
|
@@ -265,7 +294,7 @@ export class BackgroundTaskManager {
|
|
|
265
294
|
this.notifyTasksChange();
|
|
266
295
|
});
|
|
267
296
|
child.stderr?.on("data", (data) => {
|
|
268
|
-
const stripped = stripAnsiColors(data.toString());
|
|
297
|
+
const stripped = stripAnsiColors(stderrDecoder ? stderrDecoder.push(data) : data.toString());
|
|
269
298
|
shell.stderr += stripped;
|
|
270
299
|
if (logStream.writable) {
|
|
271
300
|
logStream.write(stripped);
|
|
@@ -273,6 +302,25 @@ export class BackgroundTaskManager {
|
|
|
273
302
|
this.notifyTasksChange();
|
|
274
303
|
});
|
|
275
304
|
child.on("exit", (code) => {
|
|
305
|
+
// Decode any bytes still held at stream end
|
|
306
|
+
if (stdoutDecoder) {
|
|
307
|
+
const rest = stdoutDecoder.flush();
|
|
308
|
+
if (rest) {
|
|
309
|
+
shell.stdout += rest;
|
|
310
|
+
if (logStream.writable) {
|
|
311
|
+
logStream.write(rest);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (stderrDecoder) {
|
|
316
|
+
const rest = stderrDecoder.flush();
|
|
317
|
+
if (rest) {
|
|
318
|
+
shell.stderr += rest;
|
|
319
|
+
if (logStream.writable) {
|
|
320
|
+
logStream.write(rest);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
276
324
|
if (logStream.writable) {
|
|
277
325
|
logStream.end();
|
|
278
326
|
}
|
|
@@ -66,6 +66,19 @@ export declare class SkillManager extends EventEmitter {
|
|
|
66
66
|
* Find all directories that could contain skills
|
|
67
67
|
*/
|
|
68
68
|
private findSkillDirectories;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a discovered skill is a feature-gated builtin skill whose gate
|
|
71
|
+
* is currently off. Mirrors ToolManager's gate for the Artifact tool so
|
|
72
|
+
* the /artifact skill command stays in sync with enableArtifact.
|
|
73
|
+
*/
|
|
74
|
+
private isSkillGatedOff;
|
|
75
|
+
/**
|
|
76
|
+
* Re-evaluate feature-gated builtin skills after a live configuration
|
|
77
|
+
* reload (same hook as ToolManager.reloadFeatureGatedTools). Re-scans
|
|
78
|
+
* skill directories and emits "refreshed" so slash-command registration
|
|
79
|
+
* follows the gate.
|
|
80
|
+
*/
|
|
81
|
+
reloadFeatureGatedSkills(): Promise<void>;
|
|
69
82
|
/**
|
|
70
83
|
* Execute a skill by name
|
|
71
84
|
*/
|
|
@@ -7,6 +7,7 @@ import { parseSkillFile, formatSkillError } from "../utils/skillParser.js";
|
|
|
7
7
|
import { substituteCommandParameters } from "../utils/commandArgumentParser.js";
|
|
8
8
|
import { parseBashCommands, replaceBashCommandsWithOutput, executeBashCommands, } from "../utils/markdownParser.js";
|
|
9
9
|
import { getBuiltinSkillsDir } from "../utils/configPaths.js";
|
|
10
|
+
import { isArtifactEnabled } from "../services/artifactAvailability.js";
|
|
10
11
|
import { logger } from "../utils/globalLogger.js";
|
|
11
12
|
/**
|
|
12
13
|
* Manages skill discovery and loading
|
|
@@ -247,6 +248,12 @@ export class SkillManager extends EventEmitter {
|
|
|
247
248
|
...parsed.skillMetadata,
|
|
248
249
|
type: collection.type,
|
|
249
250
|
};
|
|
251
|
+
// Feature-gated builtin skills (e.g. artifact behind
|
|
252
|
+
// enableArtifact) are skipped while the gate is off, mirroring
|
|
253
|
+
// ToolManager's registration gate for the Artifact tool.
|
|
254
|
+
if (this.isSkillGatedOff(skillMetadata)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
250
257
|
// Create full skill object with content
|
|
251
258
|
const skill = {
|
|
252
259
|
...skillMetadata,
|
|
@@ -304,6 +311,29 @@ export class SkillManager extends EventEmitter {
|
|
|
304
311
|
}
|
|
305
312
|
return directories;
|
|
306
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Whether a discovered skill is a feature-gated builtin skill whose gate
|
|
316
|
+
* is currently off. Mirrors ToolManager's gate for the Artifact tool so
|
|
317
|
+
* the /artifact skill command stays in sync with enableArtifact.
|
|
318
|
+
*/
|
|
319
|
+
isSkillGatedOff(metadata) {
|
|
320
|
+
if (metadata.type !== "builtin") {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (metadata.name === "artifact") {
|
|
324
|
+
return !isArtifactEnabled(this.workdir);
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Re-evaluate feature-gated builtin skills after a live configuration
|
|
330
|
+
* reload (same hook as ToolManager.reloadFeatureGatedTools). Re-scans
|
|
331
|
+
* skill directories and emits "refreshed" so slash-command registration
|
|
332
|
+
* follows the gate.
|
|
333
|
+
*/
|
|
334
|
+
async reloadFeatureGatedSkills() {
|
|
335
|
+
await this.refreshSkills();
|
|
336
|
+
}
|
|
307
337
|
/**
|
|
308
338
|
* Execute a skill by name
|
|
309
339
|
*/
|
|
@@ -53,6 +53,14 @@ declare class ToolManager {
|
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
55
|
initializeBuiltInTools(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Re-evaluate feature-gated built-in tools after a live configuration
|
|
58
|
+
* reload. Currently gates the Artifact tool on settings.json
|
|
59
|
+
* `enableArtifact`. Safe to call multiple times: gated tools are removed
|
|
60
|
+
* from the registry first, then initializeBuiltInTools() re-registers them
|
|
61
|
+
* only if still enabled (so toggling the flag off actually unregisters).
|
|
62
|
+
*/
|
|
63
|
+
reloadFeatureGatedTools(): void;
|
|
56
64
|
/**
|
|
57
65
|
* Check if a tool should be enabled based on tools configuration and permission rules
|
|
58
66
|
*/
|
|
@@ -114,6 +114,17 @@ class ToolManager {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Re-evaluate feature-gated built-in tools after a live configuration
|
|
119
|
+
* reload. Currently gates the Artifact tool on settings.json
|
|
120
|
+
* `enableArtifact`. Safe to call multiple times: gated tools are removed
|
|
121
|
+
* from the registry first, then initializeBuiltInTools() re-registers them
|
|
122
|
+
* only if still enabled (so toggling the flag off actually unregisters).
|
|
123
|
+
*/
|
|
124
|
+
reloadFeatureGatedTools() {
|
|
125
|
+
this.toolsRegistry.delete(artifactTool.name);
|
|
126
|
+
this.initializeBuiltInTools();
|
|
127
|
+
}
|
|
117
128
|
/**
|
|
118
129
|
* Check if a tool should be enabled based on tools configuration and permission rules
|
|
119
130
|
*/
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export declare const ARTIFACT_DEFAULT_ENABLED = false;
|
|
3
3
|
/**
|
|
4
4
|
* Whether the Artifact tool should be registered / usable for the given workdir.
|
|
5
|
-
*
|
|
5
|
+
* Resolution order: remote managed settings (`enableArtifact` from
|
|
6
|
+
* `GET /api/wave/settings`, admin override) → explicit `enableArtifact` in local
|
|
7
|
+
* merged settings → code default.
|
|
6
8
|
*/
|
|
7
9
|
export declare function isArtifactEnabled(workdir?: string): boolean;
|
|
@@ -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
|
|
@@ -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);
|
|
@@ -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
|
+
}
|