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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { imageFileCacheKey, planOutboundImage } from "./imageRewrite.js";
|
|
1
2
|
import { convertImageToBase64 } from "./messageOperations.js";
|
|
2
3
|
import { taskNotificationToXml } from "./notificationXml.js";
|
|
3
4
|
import { recoverTruncatedJson, stripAnsiColors } from "./stringUtils.js";
|
|
@@ -35,11 +36,17 @@ function safeToolArguments(args) {
|
|
|
35
36
|
/**
|
|
36
37
|
* Convert message format to API call format, stopping when a compacted message is encountered.
|
|
37
38
|
* Messages with no meaningful content or tool calls are filtered out.
|
|
39
|
+
*
|
|
40
|
+
* Async because every outbound image is routed through `planOutboundImage`,
|
|
41
|
+
* which may have to re-encode an oversized image with the optional `sharp`
|
|
42
|
+
* codec (`utils/imageRewrite.ts`). Images that are already inside the budget —
|
|
43
|
+
* the common case — take a synchronous fast path and come out byte-identical.
|
|
44
|
+
*
|
|
38
45
|
* @param messages Message list
|
|
39
46
|
* @param options Optional conversion options (e.g. supportsVision)
|
|
40
47
|
* @returns Converted API message format list
|
|
41
48
|
*/
|
|
42
|
-
export function convertMessagesForAPI(messages, options) {
|
|
49
|
+
export async function convertMessagesForAPI(messages, options) {
|
|
43
50
|
const supportsVision = options?.supportsVision !== false;
|
|
44
51
|
const recentMessages = [];
|
|
45
52
|
const startIndex = messages.length - 1;
|
|
@@ -70,7 +77,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
70
77
|
if (toolBlocks.length > 0) {
|
|
71
78
|
// Collect image user messages to place after all tool messages
|
|
72
79
|
const imageUserMessages = [];
|
|
73
|
-
|
|
80
|
+
for (const toolBlock of toolBlocks) {
|
|
74
81
|
// Only add completed tool blocks (i.e., stage is 'end')
|
|
75
82
|
if (toolBlock.id && toolBlock.stage === "end") {
|
|
76
83
|
completedToolIds.add(toolBlock.id);
|
|
@@ -84,18 +91,34 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
84
91
|
if (toolBlock.images && toolBlock.images.length > 0) {
|
|
85
92
|
if (supportsVision) {
|
|
86
93
|
const contentParts = [];
|
|
87
|
-
toolBlock.images
|
|
94
|
+
for (const image of toolBlock.images) {
|
|
88
95
|
const imageUrl = image.data.startsWith("data:")
|
|
89
96
|
? image.data
|
|
90
97
|
: `data:${image.mediaType || "image/png"};base64,${image.data}`;
|
|
98
|
+
// Tool-produced images (Read screenshots, MCP results, Exec
|
|
99
|
+
// output) go through the same outbound budget as attachments:
|
|
100
|
+
// an image the gateway would reject is shrunk, or omitted with
|
|
101
|
+
// an actionable note, instead of failing the whole request.
|
|
102
|
+
const plan = await planOutboundImage({
|
|
103
|
+
dataUrl: imageUrl,
|
|
104
|
+
cacheKey: imageUrl,
|
|
105
|
+
sourcePath: image.path,
|
|
106
|
+
});
|
|
107
|
+
if (plan.kind === "omit") {
|
|
108
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
91
111
|
contentParts.push({
|
|
92
112
|
type: "image_url",
|
|
93
113
|
image_url: {
|
|
94
|
-
url:
|
|
114
|
+
url: plan.dataUrl,
|
|
95
115
|
detail: "auto",
|
|
96
116
|
},
|
|
97
117
|
});
|
|
98
|
-
|
|
118
|
+
if (plan.note) {
|
|
119
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
99
122
|
imageUserMessages.push({
|
|
100
123
|
role: "user",
|
|
101
124
|
content: contentParts,
|
|
@@ -129,7 +152,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
129
152
|
}
|
|
130
153
|
}
|
|
131
154
|
}
|
|
132
|
-
}
|
|
155
|
+
}
|
|
133
156
|
// Insert image user messages after all tool messages but before the
|
|
134
157
|
// assistant message (which will be unshifted next). Since tool messages
|
|
135
158
|
// were unshifted to the front, we splice images right after them.
|
|
@@ -218,7 +241,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
218
241
|
else if (message.role === "user") {
|
|
219
242
|
// User messages converted to standard format
|
|
220
243
|
const contentParts = [];
|
|
221
|
-
message.blocks
|
|
244
|
+
for (const block of message.blocks) {
|
|
222
245
|
// Add text content - only if it has meaningful content
|
|
223
246
|
if (block.type === "text" &&
|
|
224
247
|
block.content &&
|
|
@@ -253,28 +276,55 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
253
276
|
});
|
|
254
277
|
}
|
|
255
278
|
else {
|
|
256
|
-
block.imageUrls
|
|
279
|
+
for (const imageUrl of block.imageUrls) {
|
|
257
280
|
// Check if it's already base64, convert if not
|
|
258
281
|
const isDataUrl = imageUrl.startsWith("data:image/");
|
|
259
282
|
let finalImageUrl = imageUrl;
|
|
260
283
|
if (!isDataUrl) {
|
|
261
|
-
// If it's a file path, it needs to be converted to base64
|
|
284
|
+
// If it's a file path, it needs to be converted to base64.
|
|
285
|
+
// Unreadable/empty/unknown-format files come back as
|
|
286
|
+
// undefined: skip the image rather than send an empty payload.
|
|
287
|
+
let converted;
|
|
262
288
|
try {
|
|
263
|
-
|
|
289
|
+
converted = convertImageToBase64(imageUrl);
|
|
264
290
|
}
|
|
265
291
|
catch (error) {
|
|
266
292
|
logger.error("Failed to convert image path to base64:", imageUrl, error);
|
|
267
293
|
// Skip this image, do not add to content
|
|
268
|
-
|
|
294
|
+
continue;
|
|
269
295
|
}
|
|
296
|
+
if (!converted) {
|
|
297
|
+
logger.warn("Skipping unusable image file:", imageUrl);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
finalImageUrl = converted;
|
|
301
|
+
}
|
|
302
|
+
// Outbound budget: an image the vision gateway would reject, or
|
|
303
|
+
// one over our own size budget, is shrunk here (or replaced by an
|
|
304
|
+
// actionable note when it cannot be). See utils/imageRewrite.ts.
|
|
305
|
+
const plan = await planOutboundImage({
|
|
306
|
+
dataUrl: finalImageUrl,
|
|
307
|
+
cacheKey: isDataUrl
|
|
308
|
+
? finalImageUrl
|
|
309
|
+
: imageFileCacheKey(imageUrl),
|
|
310
|
+
sourcePath: isDataUrl ? undefined : imageUrl,
|
|
311
|
+
});
|
|
312
|
+
if (plan.kind === "omit") {
|
|
313
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
314
|
+
continue;
|
|
270
315
|
}
|
|
271
316
|
contentParts.push({
|
|
272
317
|
type: "image_url",
|
|
273
318
|
image_url: {
|
|
274
|
-
url:
|
|
319
|
+
url: plan.dataUrl,
|
|
275
320
|
detail: "auto",
|
|
276
321
|
},
|
|
277
322
|
});
|
|
323
|
+
// Tell the model when the image it sees is not the full-size
|
|
324
|
+
// original, so it can map coordinates back.
|
|
325
|
+
if (plan.note) {
|
|
326
|
+
contentParts.push({ type: "text", text: plan.note });
|
|
327
|
+
}
|
|
278
328
|
// Aligned with Claude Code: when the image comes from a local
|
|
279
329
|
// file (not an inline dataURL), append its source path as text
|
|
280
330
|
// metadata so the model can reference the file with tools
|
|
@@ -285,7 +335,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
285
335
|
text: `[Image source: ${imageUrl}]`,
|
|
286
336
|
});
|
|
287
337
|
}
|
|
288
|
-
}
|
|
338
|
+
}
|
|
289
339
|
}
|
|
290
340
|
}
|
|
291
341
|
// If there is a tool block in user message, add its result
|
|
@@ -302,7 +352,7 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
302
352
|
text: `A background agent completed a task:\n${taskNotificationToXml(block)}`,
|
|
303
353
|
});
|
|
304
354
|
}
|
|
305
|
-
}
|
|
355
|
+
}
|
|
306
356
|
// Only add user message if there is meaningful content
|
|
307
357
|
if (contentParts.length > 0) {
|
|
308
358
|
// Filter out empty text parts
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReadFileState } from "../tools/types.js";
|
|
2
|
+
/** Max number of changed files reported in a single reminder. */
|
|
3
|
+
export declare const CHANGED_FILES_MAX_FILES = 10;
|
|
4
|
+
/** Max bytes of the changed-lines snippet rendered for a single file. */
|
|
5
|
+
export declare const CHANGED_FILES_SNIPPET_MAX_BYTES = 2048;
|
|
6
|
+
/** Max bytes of the whole reminder (all reported files combined). */
|
|
7
|
+
export declare const CHANGED_FILES_TOTAL_MAX_BYTES = 8192;
|
|
8
|
+
/**
|
|
9
|
+
* Build a compact "what changed" snippet from the new file content: trim the
|
|
10
|
+
* common prefix/suffix, then render the differing region with line numbers.
|
|
11
|
+
* A deletion-only region renders as a removal note. Truncates at a line
|
|
12
|
+
* boundary to `maxBytes`. Returns "" when the contents are identical.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildChangedSnippet(oldContent: string, newContent: string, maxBytes?: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Detect files that changed on disk since they were read and return a reminder
|
|
17
|
+
* body for the agent, or null when nothing changed. Refreshes the read state
|
|
18
|
+
* for every detected change (see the module comment).
|
|
19
|
+
*/
|
|
20
|
+
export declare function getChangedFilesReminder(readFileState: ReadFileState | undefined): Promise<string | null>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External file-change detection for the main agent loop (aligned with Claude
|
|
3
|
+
* Code's `changed_files` attachment).
|
|
4
|
+
*
|
|
5
|
+
* For every file recorded in the session read state by a *full* read/write,
|
|
6
|
+
* this compares the on-disk mtime against the mtime recorded at read time and,
|
|
7
|
+
* when the file is newer AND the content actually differs, produces a short
|
|
8
|
+
* changed-lines summary. The read state entry is then refreshed to the new
|
|
9
|
+
* mtime/hash/content so that (a) the same change is reported only once, and
|
|
10
|
+
* (b) a subsequent Edit/Write passes the staleness check — the agent has just
|
|
11
|
+
* been told the new content.
|
|
12
|
+
*
|
|
13
|
+
* The writers this surfaces are the ones other than the main agent: the
|
|
14
|
+
* auto-memory extraction fork, an external editor, another session. The main
|
|
15
|
+
* agent's own Write/Edit refresh the read state on write, so they never show up
|
|
16
|
+
* here.
|
|
17
|
+
*/
|
|
18
|
+
import { readFile, stat } from "node:fs/promises";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
/** Max number of changed files reported in a single reminder. */
|
|
21
|
+
export const CHANGED_FILES_MAX_FILES = 10;
|
|
22
|
+
/** Max bytes of the changed-lines snippet rendered for a single file. */
|
|
23
|
+
export const CHANGED_FILES_SNIPPET_MAX_BYTES = 2048;
|
|
24
|
+
/** Max bytes of the whole reminder (all reported files combined). */
|
|
25
|
+
export const CHANGED_FILES_TOTAL_MAX_BYTES = 8192;
|
|
26
|
+
function sha256(text) {
|
|
27
|
+
return createHash("sha256").update(text).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
/** UTF-8 byte length — CJK content is ~3 bytes/char, so `.length` would under-count. */
|
|
30
|
+
function byteLength(text) {
|
|
31
|
+
return Buffer.byteLength(text, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a compact "what changed" snippet from the new file content: trim the
|
|
35
|
+
* common prefix/suffix, then render the differing region with line numbers.
|
|
36
|
+
* A deletion-only region renders as a removal note. Truncates at a line
|
|
37
|
+
* boundary to `maxBytes`. Returns "" when the contents are identical.
|
|
38
|
+
*/
|
|
39
|
+
export function buildChangedSnippet(oldContent, newContent, maxBytes = CHANGED_FILES_SNIPPET_MAX_BYTES) {
|
|
40
|
+
const oldLines = oldContent.replace(/\r\n/g, "\n").split("\n");
|
|
41
|
+
const newLines = newContent.replace(/\r\n/g, "\n").split("\n");
|
|
42
|
+
const shared = Math.min(oldLines.length, newLines.length);
|
|
43
|
+
let prefix = 0;
|
|
44
|
+
while (prefix < shared && oldLines[prefix] === newLines[prefix])
|
|
45
|
+
prefix++;
|
|
46
|
+
let suffix = 0;
|
|
47
|
+
const maxSuffix = shared - prefix;
|
|
48
|
+
while (suffix < maxSuffix &&
|
|
49
|
+
oldLines[oldLines.length - 1 - suffix] ===
|
|
50
|
+
newLines[newLines.length - 1 - suffix]) {
|
|
51
|
+
suffix++;
|
|
52
|
+
}
|
|
53
|
+
const changedOld = oldLines.slice(prefix, oldLines.length - suffix);
|
|
54
|
+
const changedNew = newLines.slice(prefix, newLines.length - suffix);
|
|
55
|
+
if (changedOld.length === 0 && changedNew.length === 0)
|
|
56
|
+
return "";
|
|
57
|
+
if (changedNew.length === 0) {
|
|
58
|
+
const plural = changedOld.length === 1 ? "line" : "lines";
|
|
59
|
+
return `(${changedOld.length} ${plural} removed at line ${prefix + 1})`;
|
|
60
|
+
}
|
|
61
|
+
const rendered = [];
|
|
62
|
+
let used = 0;
|
|
63
|
+
let truncated = 0;
|
|
64
|
+
for (let i = 0; i < changedNew.length; i++) {
|
|
65
|
+
const line = `${String(prefix + i + 1).padStart(4, " ")} | ${changedNew[i]}`;
|
|
66
|
+
const cost = byteLength(line) + 1; // + trailing newline
|
|
67
|
+
if (used + cost > maxBytes) {
|
|
68
|
+
truncated = changedNew.length - i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
rendered.push(line);
|
|
72
|
+
used += cost;
|
|
73
|
+
}
|
|
74
|
+
let snippet = rendered.join("\n");
|
|
75
|
+
if (truncated > 0) {
|
|
76
|
+
snippet += `\n... [${truncated} lines truncated] ...`;
|
|
77
|
+
}
|
|
78
|
+
return snippet;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Detect files that changed on disk since they were read and return a reminder
|
|
82
|
+
* body for the agent, or null when nothing changed. Refreshes the read state
|
|
83
|
+
* for every detected change (see the module comment).
|
|
84
|
+
*/
|
|
85
|
+
export async function getChangedFilesReminder(readFileState) {
|
|
86
|
+
if (!readFileState || readFileState.size === 0)
|
|
87
|
+
return null;
|
|
88
|
+
const blocks = [];
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
for (const [filePath, state] of readFileState) {
|
|
91
|
+
if (blocks.length >= CHANGED_FILES_MAX_FILES)
|
|
92
|
+
break;
|
|
93
|
+
// Partial reads cache only a slice — no reliable diff baseline.
|
|
94
|
+
if (state.offset !== undefined || state.limit !== undefined)
|
|
95
|
+
continue;
|
|
96
|
+
let mtimeMs;
|
|
97
|
+
let newContent;
|
|
98
|
+
try {
|
|
99
|
+
const stats = await stat(filePath);
|
|
100
|
+
mtimeMs = stats.mtime.getTime();
|
|
101
|
+
// Not newer than the recorded mtime → untouched, or touched by the main
|
|
102
|
+
// agent's own Write/Edit (which refreshes the entry to the post-write mtime).
|
|
103
|
+
if (mtimeMs <= state.mtime)
|
|
104
|
+
continue;
|
|
105
|
+
newContent = await readFile(filePath, "utf-8");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Deleted or transiently unreadable — leave the entry alone and move on.
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Refresh the baseline so the same observation is not re-processed on every
|
|
112
|
+
// turn and a subsequent Edit passes the staleness check (the agent has just
|
|
113
|
+
// been told the new content).
|
|
114
|
+
const refresh = () => {
|
|
115
|
+
readFileState.set(filePath, {
|
|
116
|
+
mtime: mtimeMs,
|
|
117
|
+
hash: sha256(newContent),
|
|
118
|
+
source: "changed",
|
|
119
|
+
content: newContent,
|
|
120
|
+
offset: undefined,
|
|
121
|
+
limit: undefined,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const oldContent = state.content;
|
|
125
|
+
// No baseline, or the file was touched but its content is unchanged
|
|
126
|
+
// (git checkout / editor round-trip save / cloud sync / antivirus).
|
|
127
|
+
if (oldContent === undefined || oldContent === newContent) {
|
|
128
|
+
refresh();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const snippet = buildChangedSnippet(oldContent, newContent);
|
|
132
|
+
if (!snippet) {
|
|
133
|
+
refresh();
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const block = `${filePath}\n${snippet}`;
|
|
137
|
+
if (totalBytes + byteLength(block) > CHANGED_FILES_TOTAL_MAX_BYTES) {
|
|
138
|
+
// Leave the entry un-refreshed so the change is reported on a later turn
|
|
139
|
+
// rather than silently dropped.
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
refresh();
|
|
143
|
+
blocks.push(block);
|
|
144
|
+
totalBytes += byteLength(block);
|
|
145
|
+
}
|
|
146
|
+
if (blocks.length === 0)
|
|
147
|
+
return null;
|
|
148
|
+
return [
|
|
149
|
+
"The following files were modified on disk since you last read them — by another process such as a background agent, an external editor, or another session. Your earlier read of these files is out of date; re-read a file before editing it if the changed lines below are not enough.",
|
|
150
|
+
"",
|
|
151
|
+
blocks.join("\n\n"),
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
package/dist/utils/fileSearch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { getRgPath } from "./ripgrep.js";
|
|
3
3
|
import fuzzysort from "fuzzysort";
|
|
4
4
|
import { logger } from "./globalLogger.js";
|
|
5
5
|
const EXCLUDED_FILES = [".git", ".DS_Store"];
|
|
@@ -7,12 +7,13 @@ const EXCLUDED_FILES = [".git", ".DS_Store"];
|
|
|
7
7
|
* Execute ripgrep to get all file paths
|
|
8
8
|
*/
|
|
9
9
|
async function getAllFiles(workingDirectory) {
|
|
10
|
-
|
|
10
|
+
const rgBinary = getRgPath();
|
|
11
|
+
if (!rgBinary) {
|
|
11
12
|
throw new Error("ripgrep is not available");
|
|
12
13
|
}
|
|
13
14
|
const rgArgs = ["--files", "--color=never", "--hidden"];
|
|
14
15
|
return new Promise((resolve, reject) => {
|
|
15
|
-
const child = spawn(
|
|
16
|
+
const child = spawn(rgBinary, rgArgs, {
|
|
16
17
|
cwd: workingDirectory,
|
|
17
18
|
stdio: ["ignore", "pipe", "pipe"],
|
|
18
19
|
});
|
|
@@ -13,6 +13,25 @@ export declare function readFirstLine(filePath: string): Promise<string>;
|
|
|
13
13
|
* @return {Promise<string[]>} - Array of non-empty lines (up to maxLines).
|
|
14
14
|
*/
|
|
15
15
|
export declare function readFirstNLines(filePath: string, maxLines: number): Promise<string[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Streams a file line by line without holding it in memory.
|
|
18
|
+
*
|
|
19
|
+
* Splits on `\n` and hands each segment to `onLine`; a trailing `\r` is left
|
|
20
|
+
* for the callback's own `trim()`. When the file does not end with a newline,
|
|
21
|
+
* the trailing partial segment is delivered too, so callers can apply the same
|
|
22
|
+
* "interrupted append" tolerance `JsonlHandler.read()` uses — its
|
|
23
|
+
* `endsWithNewline` flag is exactly the return value here.
|
|
24
|
+
*
|
|
25
|
+
* Memory stays at one line: chunks are consumed as they arrive and are not
|
|
26
|
+
* accumulated.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} filePath - The path to the file.
|
|
29
|
+
* @param {(line: string) => void | Promise<void>} onLine - Called per segment, in file order.
|
|
30
|
+
* @return {Promise<{ endsWithNewline: boolean }>} - Whether the file ended with a newline (empty files count as true).
|
|
31
|
+
*/
|
|
32
|
+
export declare function forEachLine(filePath: string, onLine: (line: string) => void | Promise<void>): Promise<{
|
|
33
|
+
endsWithNewline: boolean;
|
|
34
|
+
}>;
|
|
16
35
|
/**
|
|
17
36
|
* Reads a file from the end and returns the last non-empty line.
|
|
18
37
|
*
|
|
@@ -37,6 +56,20 @@ export declare function getLastLine(filePath: string, minLength?: number): Promi
|
|
|
37
56
|
* @return {Promise<string[]>} - Trailing non-empty lines, or [] if unreadable.
|
|
38
57
|
*/
|
|
39
58
|
export declare function readTailLines(filePath: string, maxBytes?: number): Promise<string[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Synchronously read up to `maxBytes` from the end of a file and return the
|
|
61
|
+
* raw tail text (blank lines preserved). When the window starts mid-line the
|
|
62
|
+
* partial first line is dropped, so the result holds complete lines only.
|
|
63
|
+
*
|
|
64
|
+
* Synchronous on purpose: the caller (`BackgroundTaskManager.getOutput`) sits
|
|
65
|
+
* on the synchronous `getBackgroundTaskOutput` path that hosts and the CLI
|
|
66
|
+
* already consume. The window is capped, so the blocking read stays small.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} filePath - The path to the file.
|
|
69
|
+
* @param {number} maxBytes - Size of the tail window (default 64KB).
|
|
70
|
+
* @return {string} - Tail text, or "" when the file is empty or unreadable.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readTailTextSync(filePath: string, maxBytes?: number): string;
|
|
40
73
|
/**
|
|
41
74
|
* Suggests similar paths if a file is not found.
|
|
42
75
|
*/
|
package/dist/utils/fileUtils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import fsSync from "node:fs";
|
|
2
3
|
import { createReadStream } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { glob } from "glob";
|
|
@@ -68,6 +69,44 @@ export async function readFirstNLines(filePath, maxLines) {
|
|
|
68
69
|
fileStream.destroy();
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Streams a file line by line without holding it in memory.
|
|
74
|
+
*
|
|
75
|
+
* Splits on `\n` and hands each segment to `onLine`; a trailing `\r` is left
|
|
76
|
+
* for the callback's own `trim()`. When the file does not end with a newline,
|
|
77
|
+
* the trailing partial segment is delivered too, so callers can apply the same
|
|
78
|
+
* "interrupted append" tolerance `JsonlHandler.read()` uses — its
|
|
79
|
+
* `endsWithNewline` flag is exactly the return value here.
|
|
80
|
+
*
|
|
81
|
+
* Memory stays at one line: chunks are consumed as they arrive and are not
|
|
82
|
+
* accumulated.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} filePath - The path to the file.
|
|
85
|
+
* @param {(line: string) => void | Promise<void>} onLine - Called per segment, in file order.
|
|
86
|
+
* @return {Promise<{ endsWithNewline: boolean }>} - Whether the file ended with a newline (empty files count as true).
|
|
87
|
+
*/
|
|
88
|
+
export async function forEachLine(filePath, onLine) {
|
|
89
|
+
const fileStream = createReadStream(filePath, { encoding: "utf8" });
|
|
90
|
+
try {
|
|
91
|
+
let carry = "";
|
|
92
|
+
for await (const chunk of fileStream) {
|
|
93
|
+
carry += chunk;
|
|
94
|
+
let newlineIndex = carry.indexOf("\n");
|
|
95
|
+
while (newlineIndex !== -1) {
|
|
96
|
+
await onLine(carry.slice(0, newlineIndex));
|
|
97
|
+
carry = carry.slice(newlineIndex + 1);
|
|
98
|
+
newlineIndex = carry.indexOf("\n");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (carry.length > 0) {
|
|
102
|
+
await onLine(carry);
|
|
103
|
+
}
|
|
104
|
+
return { endsWithNewline: carry.length === 0 };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
fileStream.destroy();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
71
110
|
/**
|
|
72
111
|
* Reads a file from the end and returns the last non-empty line.
|
|
73
112
|
*
|
|
@@ -173,6 +212,48 @@ export async function readTailLines(filePath, maxBytes = 64 * 1024) {
|
|
|
173
212
|
}
|
|
174
213
|
}
|
|
175
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Synchronously read up to `maxBytes` from the end of a file and return the
|
|
217
|
+
* raw tail text (blank lines preserved). When the window starts mid-line the
|
|
218
|
+
* partial first line is dropped, so the result holds complete lines only.
|
|
219
|
+
*
|
|
220
|
+
* Synchronous on purpose: the caller (`BackgroundTaskManager.getOutput`) sits
|
|
221
|
+
* on the synchronous `getBackgroundTaskOutput` path that hosts and the CLI
|
|
222
|
+
* already consume. The window is capped, so the blocking read stays small.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} filePath - The path to the file.
|
|
225
|
+
* @param {number} maxBytes - Size of the tail window (default 64KB).
|
|
226
|
+
* @return {string} - Tail text, or "" when the file is empty or unreadable.
|
|
227
|
+
*/
|
|
228
|
+
export function readTailTextSync(filePath, maxBytes = 64 * 1024) {
|
|
229
|
+
let fd;
|
|
230
|
+
try {
|
|
231
|
+
const fileSize = fsSync.statSync(filePath).size;
|
|
232
|
+
if (fileSize === 0)
|
|
233
|
+
return "";
|
|
234
|
+
const readSize = Math.min(maxBytes, fileSize);
|
|
235
|
+
const start = fileSize - readSize;
|
|
236
|
+
const buffer = Buffer.alloc(readSize);
|
|
237
|
+
fd = fsSync.openSync(filePath, "r");
|
|
238
|
+
const bytesRead = fsSync.readSync(fd, buffer, 0, readSize, start);
|
|
239
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
240
|
+
if (start === 0)
|
|
241
|
+
return text;
|
|
242
|
+
// The window began mid-line, so its first line is partial (and may start
|
|
243
|
+
// with a byte-truncated multibyte character): drop it.
|
|
244
|
+
const firstNewline = text.indexOf("\n");
|
|
245
|
+
return firstNewline === -1 ? "" : text.slice(firstNewline + 1);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Missing file, permissions, race with deletion: no tail to show.
|
|
249
|
+
return "";
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
if (fd !== undefined) {
|
|
253
|
+
fsSync.closeSync(fd);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
176
257
|
/**
|
|
177
258
|
* Simple Levenshtein distance implementation
|
|
178
259
|
*/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared YAML frontmatter reader for wave's markdown artifacts: skills
|
|
3
|
+
* (SKILL.md), subagents, custom slash commands, memory rules and memory files.
|
|
4
|
+
*
|
|
5
|
+
* Wave ships no YAML dependency, so this is a hand-rolled parser for the subset
|
|
6
|
+
* those files actually use: `key: value`, block lists (`key:` + indented
|
|
7
|
+
* `- item`), block scalars (`key: >-` / `key: |` and their chomping variants)
|
|
8
|
+
* and indented multi-line plain scalars. Block scalars matter in practice — a
|
|
9
|
+
* folded `description: >-` used to be read as the literal string ">-", which
|
|
10
|
+
* silently threw away the whole "when to use this" text (see spec
|
|
11
|
+
* `ecosystem/agent-skills` 场景 2, `multi-agent/subagent` 场景 6,
|
|
12
|
+
* `ui/slash-commands` 场景 3, `core/memory-management` 场景 4/13).
|
|
13
|
+
*
|
|
14
|
+
* Values are strings or string arrays only — no boolean/number coercion — so
|
|
15
|
+
* callers comparing against `"true"` or `parseInt`-ing the value keep working.
|
|
16
|
+
*/
|
|
17
|
+
export type FrontmatterValue = string | string[];
|
|
18
|
+
export type ParsedFrontmatter = Record<string, FrontmatterValue>;
|
|
19
|
+
/**
|
|
20
|
+
* Split a markdown file into its leading `---` frontmatter block and the body.
|
|
21
|
+
* `yaml` is null when the file has no frontmatter; the body is then the whole
|
|
22
|
+
* content, untrimmed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function splitFrontmatter(content: string): {
|
|
25
|
+
yaml: string | null;
|
|
26
|
+
body: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Parse a frontmatter block (without the `---` delimiters) into key/value
|
|
30
|
+
* pairs. Unparseable input yields an empty object; callers that require fields
|
|
31
|
+
* report the missing ones themselves.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseFrontmatterYaml(yamlContent: string): ParsedFrontmatter;
|