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
package/dist/tools/bashTool.js
CHANGED
|
@@ -11,6 +11,7 @@ import { processToolResult } from "../utils/toolResultStorage.js";
|
|
|
11
11
|
import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
|
|
12
12
|
import { BASH_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, READ_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, } from "../constants/tools.js";
|
|
13
13
|
const BASH_DEFAULT_TIMEOUT_MS = 120000;
|
|
14
|
+
const BASH_MAX_TIMEOUT_MS = 600000;
|
|
14
15
|
// After the shell exits, its last stdout/stderr chunks may still be in flight:
|
|
15
16
|
// Node emits the child's 'exit' event as soon as the process terminates, then
|
|
16
17
|
// delivers the remaining pipe data on a later event-loop turn and finally
|
|
@@ -64,7 +65,7 @@ export const bashTool = {
|
|
|
64
65
|
},
|
|
65
66
|
timeout: {
|
|
66
67
|
type: "number",
|
|
67
|
-
description:
|
|
68
|
+
description: `Optional timeout in milliseconds (max ${BASH_MAX_TIMEOUT_MS})`,
|
|
68
69
|
},
|
|
69
70
|
description: {
|
|
70
71
|
type: "string",
|
|
@@ -102,7 +103,7 @@ Before executing the command, please follow these steps:
|
|
|
102
103
|
|
|
103
104
|
Usage notes:
|
|
104
105
|
- The command argument is required.
|
|
105
|
-
- You
|
|
106
|
+
- You may specify an optional timeout in milliseconds (up to ${BASH_MAX_TIMEOUT_MS}ms / ${BASH_MAX_TIMEOUT_MS / 60000} minutes). By default, your command will timeout after ${BASH_DEFAULT_TIMEOUT_MS}ms (${BASH_DEFAULT_TIMEOUT_MS / 60000} minutes).
|
|
106
107
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
|
107
108
|
- If the output exceeds ${BASH_MAX_OUTPUT_CHARS.toLocaleString()} characters, output will be truncated and the full output will be persisted to a file you can read with the Read tool.
|
|
108
109
|
- You can use the \`run_in_background\` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the ${READ_TOOL_NAME} tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.
|
|
@@ -179,13 +180,14 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
179
180
|
: "No suitable shell found. Please ensure bash or zsh is installed, or set WAVE_SHELL environment variable.",
|
|
180
181
|
};
|
|
181
182
|
}
|
|
182
|
-
// Validate timeout
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
// Validate the timeout's type only. An upper bound is deliberately not
|
|
184
|
+
// enforced (matching Claude Code, which leaves the type check to its zod
|
|
185
|
+
// schema and applies whatever value the model sends).
|
|
186
|
+
if (timeout !== undefined && (typeof timeout !== "number" || timeout < 0)) {
|
|
185
187
|
return {
|
|
186
188
|
success: false,
|
|
187
189
|
content: "",
|
|
188
|
-
error: "Timeout must be a number
|
|
190
|
+
error: "Timeout must be a non-negative number of milliseconds",
|
|
189
191
|
};
|
|
190
192
|
}
|
|
191
193
|
// Permission check after validation but before real operation
|
package/dist/tools/editTool.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readFile,
|
|
1
|
+
import { readFile, stat } from "fs/promises";
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { logger } from "../utils/globalLogger.js";
|
|
4
|
+
import { atomicWriteFile } from "../utils/atomicWrite.js";
|
|
4
5
|
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
5
6
|
import { escapeRegExp, analyzeEditMismatch } from "../utils/editUtils.js";
|
|
6
7
|
import { EDIT_TOOL_NAME, READ_TOOL_NAME } from "../constants/tools.js";
|
|
@@ -221,9 +222,10 @@ Usage:
|
|
|
221
222
|
if (context.reversionManager && context.messageId) {
|
|
222
223
|
snapshotId = await context.reversionManager.recordSnapshot(context.messageId, resolvedPath, "modify");
|
|
223
224
|
}
|
|
224
|
-
// Write file
|
|
225
|
+
// Write file (atomic: temp file + rename, so concurrent readers never
|
|
226
|
+
// observe a truncated file). Staleness/OCC was checked above.
|
|
225
227
|
try {
|
|
226
|
-
await
|
|
228
|
+
await atomicWriteFile(resolvedPath, newContent);
|
|
227
229
|
// Commit snapshot on success
|
|
228
230
|
if (context.reversionManager && snapshotId) {
|
|
229
231
|
await context.reversionManager.commitSnapshot(snapshotId);
|
|
@@ -244,6 +246,7 @@ Usage:
|
|
|
244
246
|
mtime: newStats.mtime.getTime(),
|
|
245
247
|
hash,
|
|
246
248
|
source: "edit",
|
|
249
|
+
content: newContent,
|
|
247
250
|
offset: undefined,
|
|
248
251
|
limit: undefined,
|
|
249
252
|
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { EXEC_TOOL_NAME } from "../constants/tools.js";
|
|
2
|
+
import { EXEC_RESERVED_NAMESPACE } from "../exec/constants.js";
|
|
3
|
+
import { buildExecPool } from "../exec/catalog.js";
|
|
4
|
+
import { runExecScript } from "../exec/execRuntime.js";
|
|
5
|
+
/**
|
|
6
|
+
* The `search` bullet of the sandbox API blurb. It names the entry point and stops
|
|
7
|
+
* there: the call form is taught by the catalog announcement, and only while the
|
|
8
|
+
* catalog is actually truncated. Printing it here would advertise a search on every
|
|
9
|
+
* turn, complete catalog or not — the tool has no idea how many tools exist.
|
|
10
|
+
*
|
|
11
|
+
* The path is derived from the reserved namespace, the same constant the sandbox
|
|
12
|
+
* builds its `tools` object from, so the two cannot drift.
|
|
13
|
+
*/
|
|
14
|
+
const SEARCH_ENTRY = `- \`tools[${JSON.stringify(EXEC_RESERVED_NAMESPACE)}].search(...)\` — ` +
|
|
15
|
+
"search the whole pool from inside the script.";
|
|
16
|
+
/**
|
|
17
|
+
* Model-visible API description.
|
|
18
|
+
*
|
|
19
|
+
* Static on purpose, and not merely "no tunable limits in it": it must be identical
|
|
20
|
+
* for any tool pool. `tools[]` sits in the cached prefix, so a description that
|
|
21
|
+
* mentioned the MCP servers (or their tools) would rewrite that prefix every time a
|
|
22
|
+
* server connected or dropped. The catalog is a tail announcement instead — see
|
|
23
|
+
* `exec/catalogAnnouncement.ts`.
|
|
24
|
+
*/
|
|
25
|
+
const EXEC_DESCRIPTION = `Run a JavaScript script in a sandbox where every MCP tool of this session is exposed as a function, so a whole sequence of MCP calls can be composed in a single turn instead of one model round-trip per call.
|
|
26
|
+
|
|
27
|
+
Sandbox API:
|
|
28
|
+
- \`await tools.<name>(args)\` — call an MCP tool, passing that tool's own arguments object directly. Resolves to the tool's output: its \`structuredContent\` object when it returned one, otherwise its text, otherwise \`null\`. The catalog gives each tool's return type.
|
|
29
|
+
${SEARCH_ENTRY}
|
|
30
|
+
- \`console.log(...)\` — collected and returned alongside the result. Use it to inspect intermediate values.
|
|
31
|
+
- \`return <value>\` — the returned value is JSON-serialized and given back to you.
|
|
32
|
+
|
|
33
|
+
It is very helpful if you write a clear, concise description of what this script does in 5-10 words.
|
|
34
|
+
|
|
35
|
+
Which MCP tools are reachable is announced in the conversation as the catalog changes. The script has no filesystem, no network, no \`import\`, and no \`eval\`/\`new Function\`. It stops when it exceeds its time or tool-call budget. Every nested MCP call goes through the normal permission check, so it can still be denied — a denied call rejects with the reason.`;
|
|
36
|
+
/**
|
|
37
|
+
* How many of the most recent nested calls the result summary lists, mirroring
|
|
38
|
+
* how the `Agent` tool lists the subagent tools it just ran.
|
|
39
|
+
*/
|
|
40
|
+
const RECENT_CALLS_SHOWN = 2;
|
|
41
|
+
/**
|
|
42
|
+
* The collapsed result row: the call count, then the most recent calls by name.
|
|
43
|
+
*
|
|
44
|
+
* Names only — MCP tools have no compact-params summary of their own, and a
|
|
45
|
+
* flat MCP call's collapsed row shows just the name. No "Exec" prefix either:
|
|
46
|
+
* the row this text sits in already prints the tool name.
|
|
47
|
+
*
|
|
48
|
+
* Derived from the calls the sandbox actually issued, so it can be rendered
|
|
49
|
+
* mid-run as well as at the end.
|
|
50
|
+
*/
|
|
51
|
+
function formatSummary(calls) {
|
|
52
|
+
const count = calls.length;
|
|
53
|
+
const lines = [
|
|
54
|
+
`${count > RECENT_CALLS_SHOWN ? "... " : ""}${count} tool call${count === 1 ? "" : "s"}`,
|
|
55
|
+
];
|
|
56
|
+
for (const name of calls.slice(-RECENT_CALLS_SHOWN)) {
|
|
57
|
+
lines.push(name);
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
function formatRun(result, calls) {
|
|
62
|
+
const lines = [];
|
|
63
|
+
if (result.logs.length > 0) {
|
|
64
|
+
lines.push(result.logs.join("\n"));
|
|
65
|
+
}
|
|
66
|
+
if (result.ok) {
|
|
67
|
+
if (result.value !== undefined && result.value !== "undefined") {
|
|
68
|
+
lines.push(result.value);
|
|
69
|
+
}
|
|
70
|
+
else if (lines.length === 0) {
|
|
71
|
+
lines.push("Exec finished without a return value or console output.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
lines.push(result.error ?? "Exec script failed");
|
|
76
|
+
}
|
|
77
|
+
const content = lines.join("\n");
|
|
78
|
+
return {
|
|
79
|
+
success: result.ok,
|
|
80
|
+
content,
|
|
81
|
+
...(result.ok ? {} : { error: result.error }),
|
|
82
|
+
shortResult: result.ok ? formatSummary(calls) : "failed",
|
|
83
|
+
...(result.images.length > 0 ? { images: result.images } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export const execTool = {
|
|
87
|
+
name: EXEC_TOOL_NAME,
|
|
88
|
+
// Nested calls reach arbitrary MCP tools, which are conservatively non-safe.
|
|
89
|
+
isConcurrencySafe: false,
|
|
90
|
+
config: {
|
|
91
|
+
type: "function",
|
|
92
|
+
function: {
|
|
93
|
+
name: EXEC_TOOL_NAME,
|
|
94
|
+
description: EXEC_DESCRIPTION,
|
|
95
|
+
parameters: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
code: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "JavaScript to run in the sandbox. It may `await` tool calls and `return` a value.",
|
|
101
|
+
},
|
|
102
|
+
description: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Clear, concise description of what this script does in 5-10 words.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: ["code"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
prompt: () => EXEC_DESCRIPTION,
|
|
112
|
+
/**
|
|
113
|
+
* The parameter slot of the collapsed row, shared by the TUI and the desktop
|
|
114
|
+
* UI: the model's own one-line summary of the script, taken verbatim — no
|
|
115
|
+
* prefix (the row already prints `Exec`), no rewrite, no truncation.
|
|
116
|
+
*
|
|
117
|
+
* `Bash` can fall back to the command string; a script has no equivalent, and
|
|
118
|
+
* the first line of a multi-line blob usually says nothing, so an absent or
|
|
119
|
+
* empty description leaves the slot blank rather than inventing text. What the
|
|
120
|
+
* script actually did is the result slot's job (`formatSummary`).
|
|
121
|
+
*
|
|
122
|
+
* Deliberately reads nothing but the argument: no code scan, no pool, no
|
|
123
|
+
* catalog, so the preview cannot claim a call the script never makes.
|
|
124
|
+
*/
|
|
125
|
+
formatCompactParams: (params) => {
|
|
126
|
+
const description = params.description;
|
|
127
|
+
return typeof description === "string" ? description : "";
|
|
128
|
+
},
|
|
129
|
+
execute: async (args, context) => {
|
|
130
|
+
const code = typeof args.code === "string" ? args.code : "";
|
|
131
|
+
if (code.trim().length === 0) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
content: "",
|
|
135
|
+
error: `${EXEC_TOOL_NAME}: missing required parameter "code"`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const mcpManager = context.mcpManager;
|
|
139
|
+
if (!mcpManager) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
content: "",
|
|
143
|
+
error: `${EXEC_TOOL_NAME}: MCP manager is not available in this session`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Recomputed here rather than reused from the declaration-time catalog: a
|
|
147
|
+
// server may have connected or dropped since. Both are derived from the same
|
|
148
|
+
// `buildExecPool`, so the sandbox can never reach a tool the agent could not
|
|
149
|
+
// already call directly.
|
|
150
|
+
const pool = buildExecPool(mcpManager, context.permissionManager);
|
|
151
|
+
const calls = [];
|
|
152
|
+
const result = await runExecScript({
|
|
153
|
+
code,
|
|
154
|
+
pool,
|
|
155
|
+
context,
|
|
156
|
+
// Report each call as it is issued, so the collapsed row shows what the
|
|
157
|
+
// script is doing while it runs — the same live update the Agent tool does.
|
|
158
|
+
onToolCall: (name) => {
|
|
159
|
+
calls.push(name);
|
|
160
|
+
context.onShortResultUpdate?.(formatSummary(calls));
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
return formatRun(result, calls);
|
|
164
|
+
},
|
|
165
|
+
};
|
package/dist/tools/grepTool.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { getRgPath } from "../utils/ripgrep.js";
|
|
3
|
+
import { ensureRuntimeDeps } from "../utils/runtimeDeps.js";
|
|
3
4
|
import { getDisplayPath } from "../utils/path.js";
|
|
4
5
|
import { logger } from "../utils/globalLogger.js";
|
|
5
6
|
import { GREP_TOOL_NAME, BASH_TOOL_NAME, AGENT_TOOL_NAME, } from "../constants/tools.js";
|
|
@@ -122,7 +123,12 @@ export const grepTool = {
|
|
|
122
123
|
error: "pattern parameter is required and must be a string",
|
|
123
124
|
};
|
|
124
125
|
}
|
|
126
|
+
// Resolved per call, not at import time: the CLI installs this dependency
|
|
127
|
+
// itself, possibly after the module graph has been evaluated.
|
|
128
|
+
const rgPath = getRgPath();
|
|
125
129
|
if (!rgPath) {
|
|
130
|
+
// Cover the entry points that do not await the installer at startup.
|
|
131
|
+
void ensureRuntimeDeps();
|
|
126
132
|
return {
|
|
127
133
|
success: false,
|
|
128
134
|
content: "",
|
package/dist/tools/readTool.js
CHANGED
|
@@ -7,6 +7,8 @@ import { formatLineNumberPrefix } from "../utils/stringUtils.js";
|
|
|
7
7
|
import { estimateTokens } from "../utils/tokenEstimate.js";
|
|
8
8
|
import { isBinaryDocument, getBinaryDocumentError, } from "../utils/fileFormat.js";
|
|
9
9
|
import { convertImageToBase64 } from "../utils/messageOperations.js";
|
|
10
|
+
import { isPathInside } from "../utils/pathSafety.js";
|
|
11
|
+
import { memoryFreshnessNote } from "../utils/memoryAge.js";
|
|
10
12
|
import { READ_TOOL_NAME } from "../constants/tools.js";
|
|
11
13
|
/**
|
|
12
14
|
* Supported image file extensions
|
|
@@ -88,6 +90,15 @@ async function processImageFile(filePath, context) {
|
|
|
88
90
|
}
|
|
89
91
|
// Convert image to base64
|
|
90
92
|
const imageDataUrl = convertImageToBase64(actualFilePath);
|
|
93
|
+
if (!imageDataUrl) {
|
|
94
|
+
// Unreadable, empty or unknown-format file: fail instead of handing the
|
|
95
|
+
// model an empty image payload.
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
content: "",
|
|
99
|
+
error: `Failed to process image: ${getDisplayPath(filePath, context.workdir)} is empty or not a readable image file`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
91
102
|
const mimeType = getImageMimeType(actualFilePath);
|
|
92
103
|
// Extract base64 data from data URL (remove data:image/type;base64, prefix)
|
|
93
104
|
const base64Data = imageDataUrl.split(",")[1] || "";
|
|
@@ -254,8 +265,17 @@ Usage:
|
|
|
254
265
|
source: "read",
|
|
255
266
|
offset, // undefined for full reads
|
|
256
267
|
limit, // undefined for full reads
|
|
268
|
+
// Retain the raw content only for full reads: it is the diff baseline
|
|
269
|
+
// for the external-change notification, and partial entries are
|
|
270
|
+
// excluded from that comparison anyway.
|
|
271
|
+
...(offset === undefined && limit === undefined
|
|
272
|
+
? { content: fileContent }
|
|
273
|
+
: {}),
|
|
257
274
|
});
|
|
258
275
|
}
|
|
276
|
+
// A successful read may pull in the memory files of its ancestor
|
|
277
|
+
// directories (see `collectNestedMemoryFiles`); only Read triggers this.
|
|
278
|
+
context.messageManager?.triggerNestedMemory(actualFilePath);
|
|
259
279
|
// Check if file is empty
|
|
260
280
|
if (fileContent.length === 0) {
|
|
261
281
|
logger.warn(`File ${filePath} exists but has empty contents`);
|
|
@@ -315,8 +335,16 @@ Usage:
|
|
|
315
335
|
},
|
|
316
336
|
};
|
|
317
337
|
}
|
|
318
|
-
// Add file information header
|
|
319
|
-
|
|
338
|
+
// Add file information header. A file inside the auto-memory directory
|
|
339
|
+
// gets a staleness note in front of it: memory is a point-in-time
|
|
340
|
+
// observation, and a stale `file:line` citation reads as authoritative.
|
|
341
|
+
// `autoMemoryDir` is unset when auto-memory is off, so the note can never
|
|
342
|
+
// fire for a disabled feature.
|
|
343
|
+
const memoryNote = context.autoMemoryDir &&
|
|
344
|
+
isPathInside(actualFilePath, context.autoMemoryDir)
|
|
345
|
+
? memoryFreshnessNote(stats.mtime.getTime())
|
|
346
|
+
: "";
|
|
347
|
+
let content = `${memoryNote}File: ${filePath}\n`;
|
|
320
348
|
if (startLine > 1 || endLine < totalLines) {
|
|
321
349
|
content += `Lines ${startLine}-${endLine} of ${totalLines}\n`;
|
|
322
350
|
}
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -5,6 +5,32 @@ import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
|
5
5
|
import type { PermissionMode, PermissionCallback } from "../types/permissions.js";
|
|
6
6
|
import type { SubagentConfiguration } from "../utils/subagentParser.js";
|
|
7
7
|
import type { SkillMetadata } from "../types/skills.js";
|
|
8
|
+
/**
|
|
9
|
+
* Read state recorded for a file by the Read/Write/Edit tools. Used for
|
|
10
|
+
* read-before-write enforcement, staleness detection, Read dedup, and the
|
|
11
|
+
* external-change notification (diffing `content` against the disk).
|
|
12
|
+
*/
|
|
13
|
+
export interface ReadFileStateEntry {
|
|
14
|
+
/** File mtime (ms) observed when this entry was recorded. */
|
|
15
|
+
mtime: number;
|
|
16
|
+
/** sha256 of the content observed when this entry was recorded. */
|
|
17
|
+
hash: string;
|
|
18
|
+
/**
|
|
19
|
+
* Which tool recorded the entry. Read dedups only entries from Read;
|
|
20
|
+
* "changed" is recorded when an external change was observed and reported.
|
|
21
|
+
*/
|
|
22
|
+
source: "read" | "edit" | "write" | "changed";
|
|
23
|
+
/**
|
|
24
|
+
* Content observed when this entry was recorded (full reads and writes
|
|
25
|
+
* only). Serves as the diff baseline for the external-change notification.
|
|
26
|
+
*/
|
|
27
|
+
content?: string;
|
|
28
|
+
/** Line offset the entry was recorded at; undefined = full read. */
|
|
29
|
+
offset?: number;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Per-session read state keyed by resolved absolute path. */
|
|
33
|
+
export type ReadFileState = Map<string, ReadFileStateEntry>;
|
|
8
34
|
export interface ToolPlugin {
|
|
9
35
|
name: string;
|
|
10
36
|
config: ChatCompletionFunctionTool;
|
|
@@ -46,7 +72,7 @@ export interface ToolContext {
|
|
|
46
72
|
abortSignal?: AbortSignal;
|
|
47
73
|
backgroundTaskManager?: import("../managers/backgroundTaskManager.js").BackgroundTaskManager;
|
|
48
74
|
workdir: string;
|
|
49
|
-
/** Tool manager instance for
|
|
75
|
+
/** Tool manager instance, for tools that need to change session tool state */
|
|
50
76
|
toolManager?: import("../managers/toolManager.js").ToolManager;
|
|
51
77
|
/** Permission mode for this tool execution */
|
|
52
78
|
permissionMode?: PermissionMode;
|
|
@@ -92,13 +118,7 @@ export interface ToolContext {
|
|
|
92
118
|
maxTokens: number;
|
|
93
119
|
};
|
|
94
120
|
/** State of files read in the current session for deduplication */
|
|
95
|
-
readFileState?:
|
|
96
|
-
mtime: number;
|
|
97
|
-
hash: string;
|
|
98
|
-
source: "read" | "edit" | "write";
|
|
99
|
-
offset?: number;
|
|
100
|
-
limit?: number;
|
|
101
|
-
}>;
|
|
121
|
+
readFileState?: ReadFileState;
|
|
102
122
|
/** Hook manager instance for executing hooks */
|
|
103
123
|
hookManager?: import("../managers/hookManager.js").HookManager;
|
|
104
124
|
/** Callback to notify when the current working directory changes */
|
|
@@ -114,4 +134,10 @@ export interface ToolContext {
|
|
|
114
134
|
* the subprocess without polluting other sessions in one stdio process.
|
|
115
135
|
*/
|
|
116
136
|
sessionEnv?: Record<string, string>;
|
|
137
|
+
/**
|
|
138
|
+
* Auto-memory directory for this session, or undefined when auto-memory is
|
|
139
|
+
* off. Read uses it to prepend a staleness note to files inside it — leaving
|
|
140
|
+
* it unset is what keeps that note off when the feature is disabled.
|
|
141
|
+
*/
|
|
142
|
+
autoMemoryDir?: string;
|
|
117
143
|
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import TurndownService from "turndown";
|
|
2
1
|
import { LRUCache } from "lru-cache";
|
|
3
2
|
import { WEB_FETCH_TOOL_NAME } from "../constants/tools.js";
|
|
4
3
|
import { logger } from "../utils/globalLogger.js";
|
|
5
4
|
import { isArtifactEnabled } from "../services/artifactAvailability.js";
|
|
6
|
-
import { authService, createAuthAwareFetch } from "../services/authService.js";
|
|
7
5
|
import { recordVersion } from "../services/artifactSession.js";
|
|
8
|
-
import {
|
|
6
|
+
import { extractArtifactSlug, htmlToMarkdown, readArtifactContent, summarizeArtifactAsReader, } from "../services/artifactContent.js";
|
|
7
|
+
import { formatSize, processContentWithAI, } from "../services/contentSummarizer.js";
|
|
9
8
|
// --- Security Limits ---
|
|
10
9
|
const MAX_HTTP_CONTENT_LENGTH = 10 * 1024 * 1024; // 10MB
|
|
11
10
|
const FETCH_TIMEOUT_MS = 60000; // 60s
|
|
12
11
|
const MAX_REDIRECTS = 10;
|
|
13
12
|
const MAX_MARKDOWN_LENGTH = 100000;
|
|
14
13
|
const USER_AGENT = "Wave-User (+https://github.com/netease-lcap/wave-agent)";
|
|
15
|
-
/** Artifact HTML beyond this size is persisted to a temp file (path + head preview). */
|
|
16
|
-
const ARTIFACT_PREVIEW_BYTES = 2 * 1024; // ~2KB
|
|
17
14
|
// --- Cache (LRU with 15min TTL, 50MB max) ---
|
|
18
15
|
const CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
19
16
|
const CACHE_MAX_BYTES = 50 * 1024 * 1024; // 50MB
|
|
@@ -22,14 +19,6 @@ const cache = new LRUCache({
|
|
|
22
19
|
maxSize: CACHE_MAX_BYTES,
|
|
23
20
|
sizeCalculation: (entry) => entry.bytes,
|
|
24
21
|
});
|
|
25
|
-
// --- Helpers ---
|
|
26
|
-
function formatSize(bytes) {
|
|
27
|
-
if (bytes < 1024)
|
|
28
|
-
return `${bytes}B`;
|
|
29
|
-
if (bytes < 1024 * 1024)
|
|
30
|
-
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
31
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
32
|
-
}
|
|
33
22
|
function validateURL(url) {
|
|
34
23
|
if (url.length > 2000) {
|
|
35
24
|
return {
|
|
@@ -79,124 +68,6 @@ function isPermittedRedirect(originalUrl, redirectUrl) {
|
|
|
79
68
|
return false;
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
|
-
// --- Artifact read channel ---
|
|
83
|
-
/** Extract the artifact slug from a `{host}/code/artifact/{slug}` URL. */
|
|
84
|
-
function extractArtifactSlug(url) {
|
|
85
|
-
try {
|
|
86
|
-
const parsed = new URL(url);
|
|
87
|
-
const match = parsed.pathname.match(/^\/code\/artifact\/([^/]+)\/?$/);
|
|
88
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Fetch artifact metadata (`via=model_read`) then the HTML content (Bearer).
|
|
96
|
-
* Returns markdown (HTML converted via turndown) on success.
|
|
97
|
-
*/
|
|
98
|
-
async function readArtifact(url, slug, abortSignal) {
|
|
99
|
-
const serverUrl = authService.getServerUrl();
|
|
100
|
-
const authFetch = createAuthAwareFetch(globalThis.fetch);
|
|
101
|
-
const signal = abortSignal
|
|
102
|
-
? AbortSignal.any([abortSignal, AbortSignal.timeout(FETCH_TIMEOUT_MS)])
|
|
103
|
-
: AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
104
|
-
let meta;
|
|
105
|
-
try {
|
|
106
|
-
const metaRes = await authFetch(`${serverUrl}/api/frame/${encodeURIComponent(slug)}?via=model_read`, { method: "GET", signal });
|
|
107
|
-
if (metaRes.status === 404) {
|
|
108
|
-
return {
|
|
109
|
-
kind: "error",
|
|
110
|
-
error: `Artifact not found: ${url} (it may have been deleted)`,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
if (!metaRes.ok) {
|
|
114
|
-
return {
|
|
115
|
-
kind: "error",
|
|
116
|
-
error: `Failed to fetch artifact metadata: ${metaRes.status} ${metaRes.statusText}`,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
meta = (await metaRes.json());
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
logger?.warn("Artifact read metadata failed", {
|
|
123
|
-
slug,
|
|
124
|
-
error: String(error),
|
|
125
|
-
});
|
|
126
|
-
return {
|
|
127
|
-
kind: "error",
|
|
128
|
-
error: `Failed to fetch artifact metadata: ${error instanceof Error ? error.message : String(error)}`,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
const contentUrl = typeof meta.contentUrl === "string" ? meta.contentUrl : "";
|
|
132
|
-
const version = typeof meta.version === "string" ? meta.version : "";
|
|
133
|
-
if (!contentUrl) {
|
|
134
|
-
return {
|
|
135
|
-
kind: "error",
|
|
136
|
-
error: "Artifact metadata did not include a contentUrl",
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
let html;
|
|
140
|
-
try {
|
|
141
|
-
const contentRes = await authFetch(new URL(contentUrl, serverUrl).toString(), { method: "GET", signal });
|
|
142
|
-
if (!contentRes.ok) {
|
|
143
|
-
return {
|
|
144
|
-
kind: "error",
|
|
145
|
-
error: `Failed to fetch artifact content: ${contentRes.status} ${contentRes.statusText}`,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
html = await contentRes.text();
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
logger?.warn("Artifact read content failed", {
|
|
152
|
-
slug,
|
|
153
|
-
error: String(error),
|
|
154
|
-
});
|
|
155
|
-
return {
|
|
156
|
-
kind: "error",
|
|
157
|
-
error: `Failed to fetch artifact content: ${error instanceof Error ? error.message : String(error)}`,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
const turndownService = new TurndownService();
|
|
161
|
-
const markdown = turndownService.turndown(html);
|
|
162
|
-
return {
|
|
163
|
-
kind: "ok",
|
|
164
|
-
slug,
|
|
165
|
-
version,
|
|
166
|
-
markdown,
|
|
167
|
-
bytes: new TextEncoder().encode(markdown).length,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Process an artifact read result: persist content >~2KB to a temp file
|
|
172
|
-
* (return file path + head preview), run the prompt, attach artifactRead metadata.
|
|
173
|
-
*/
|
|
174
|
-
async function processArtifactRead(url, prompt, markdown, slug, version, context) {
|
|
175
|
-
const bytes = new TextEncoder().encode(markdown).length;
|
|
176
|
-
let aiInput = markdown;
|
|
177
|
-
let persistedMessage = "";
|
|
178
|
-
if (bytes > ARTIFACT_PREVIEW_BYTES) {
|
|
179
|
-
const filePath = persistToolResult(markdown, "artifact");
|
|
180
|
-
if (filePath) {
|
|
181
|
-
persistedMessage = buildPersistedOutputMessage(markdown.length, filePath, generatePreview(markdown));
|
|
182
|
-
aiInput = persistedMessage;
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
aiInput =
|
|
186
|
-
markdown.substring(0, MAX_MARKDOWN_LENGTH) +
|
|
187
|
-
"\n\n... (content truncated, failed to persist full output)";
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const result = await processWithAI(url, prompt, aiInput, 200, "OK", context, bytes);
|
|
191
|
-
result.metadata = { artifactRead: { slug, ver: version } };
|
|
192
|
-
if (persistedMessage) {
|
|
193
|
-
// Append the persisted-output message so the model can Read the full content.
|
|
194
|
-
result.content = result.content
|
|
195
|
-
? result.content + "\n\n" + persistedMessage
|
|
196
|
-
: persistedMessage;
|
|
197
|
-
}
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
71
|
// --- Tool ---
|
|
201
72
|
export const webFetchTool = {
|
|
202
73
|
name: WEB_FETCH_TOOL_NAME,
|
|
@@ -271,22 +142,28 @@ Usage notes:
|
|
|
271
142
|
if (isArtifactEnabled(context.workdir)) {
|
|
272
143
|
const artifactSlug = extractArtifactSlug(url);
|
|
273
144
|
if (artifactSlug) {
|
|
274
|
-
const readResult = await
|
|
145
|
+
const readResult = await readArtifactContent(artifactSlug, {
|
|
146
|
+
url,
|
|
147
|
+
abortSignal: context.abortSignal,
|
|
148
|
+
});
|
|
275
149
|
if (readResult.kind === "error") {
|
|
276
150
|
return { success: false, content: "", error: readResult.error };
|
|
277
151
|
}
|
|
278
|
-
|
|
152
|
+
const { slug, version, html } = readResult.artifact;
|
|
153
|
+
if (context.sessionId && version) {
|
|
279
154
|
// Keep the stale-version guard in sync with what the model has seen.
|
|
280
|
-
recordVersion(context.sessionId,
|
|
155
|
+
recordVersion(context.sessionId, slug, version);
|
|
281
156
|
}
|
|
282
|
-
|
|
157
|
+
const result = await summarizeArtifactAsReader(url, prompt, html, context);
|
|
158
|
+
result.metadata = { artifactRead: { slug, ver: version } };
|
|
159
|
+
return result;
|
|
283
160
|
}
|
|
284
161
|
}
|
|
285
162
|
try {
|
|
286
163
|
const cached = cache.get(url);
|
|
287
164
|
if (cached) {
|
|
288
165
|
const markdown = cached.content;
|
|
289
|
-
return
|
|
166
|
+
return processContentWithAI(url, prompt, markdown, cached.code, cached.codeText, context);
|
|
290
167
|
}
|
|
291
168
|
// Fetch with redirect following
|
|
292
169
|
const result = await fetchWithRedirects(url, context.abortSignal);
|
|
@@ -323,8 +200,7 @@ Usage notes:
|
|
|
323
200
|
};
|
|
324
201
|
}
|
|
325
202
|
const html = await response.text();
|
|
326
|
-
|
|
327
|
-
let markdown = turndownService.turndown(html);
|
|
203
|
+
let markdown = htmlToMarkdown(html);
|
|
328
204
|
const markdownBytes = new TextEncoder().encode(markdown).length;
|
|
329
205
|
// Truncate if too large
|
|
330
206
|
if (markdown.length > MAX_MARKDOWN_LENGTH) {
|
|
@@ -340,7 +216,7 @@ Usage notes:
|
|
|
340
216
|
content: markdown,
|
|
341
217
|
contentType: response.headers.get("content-type") || "",
|
|
342
218
|
});
|
|
343
|
-
return
|
|
219
|
+
return processContentWithAI(finalUrl, prompt, markdown, response.status, response.statusText, context, markdownBytes);
|
|
344
220
|
}
|
|
345
221
|
catch (error) {
|
|
346
222
|
logger.error(`WebFetch failed for ${url}:`, error);
|
|
@@ -398,30 +274,3 @@ async function fetchWithRedirects(initialUrl, abortSignal, redirectCount = 0) {
|
|
|
398
274
|
}
|
|
399
275
|
return { kind: "response", response, finalUrl: initialUrl };
|
|
400
276
|
}
|
|
401
|
-
// --- AI Processing ---
|
|
402
|
-
async function processWithAI(url, prompt, markdown, statusCode, statusText, context, contentSize) {
|
|
403
|
-
if (!context.aiManager || !context.aiService) {
|
|
404
|
-
return {
|
|
405
|
-
success: false,
|
|
406
|
-
content: markdown,
|
|
407
|
-
error: "AI Manager or AI Service not available for processing content",
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
const modelConfig = context.aiManager.getModelConfig();
|
|
411
|
-
const fastModel = modelConfig.fastModel;
|
|
412
|
-
const aiResponse = await context.aiService.processWebContent({
|
|
413
|
-
gatewayConfig: context.aiManager.getGatewayConfig(),
|
|
414
|
-
modelConfig: modelConfig,
|
|
415
|
-
content: markdown,
|
|
416
|
-
prompt: prompt,
|
|
417
|
-
model: fastModel,
|
|
418
|
-
abortSignal: context.abortSignal,
|
|
419
|
-
});
|
|
420
|
-
const sizeStr = contentSize !== undefined ? formatSize(contentSize) : "unknown size";
|
|
421
|
-
const statusStr = `${statusCode} ${statusText}`.trim();
|
|
422
|
-
return {
|
|
423
|
-
success: true,
|
|
424
|
-
content: aiResponse.content || "",
|
|
425
|
-
shortResult: `Received ${sizeStr} (${statusStr}) from ${url}`,
|
|
426
|
-
};
|
|
427
|
-
}
|