wave-agent-sdk 1.1.5 → 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 +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- 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 +54 -10
- package/dist/core/plugin.js +137 -23
- 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 +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- 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 +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- 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/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- 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 +323 -102
- 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 +92 -9
- package/dist/services/configurationService.js +246 -64
- 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 +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -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/exitPlanMode.js +10 -2
- 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 +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- 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 +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- 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 +44 -0
- package/dist/utils/fileUtils.js +118 -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 -20
- package/dist/utils/messageOperations.js +40 -91
- 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 +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* JSONL file operations service
|
|
3
3
|
* Handles reading and writing JSONL (JSON Lines) session files for improved performance
|
|
4
4
|
*/
|
|
5
|
-
import { appendFile, readFile, writeFile, stat, mkdir } from "fs/promises";
|
|
5
|
+
import { appendFile, readFile, writeFile, stat, mkdir, rename, unlink, } from "fs/promises";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
6
7
|
import { dirname } from "path";
|
|
7
|
-
import { getLastLine, readFirstNLines } from "../utils/fileUtils.js";
|
|
8
|
+
import { forEachLine, getLastLine, readFirstNLines, readTailLines, } from "../utils/fileUtils.js";
|
|
9
|
+
import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
|
|
10
|
+
import { isReservedEntry } from "./sessionEntries.js";
|
|
11
|
+
/**
|
|
12
|
+
* Flush threshold for `truncateSession`'s output buffer. Only bounds the
|
|
13
|
+
* rewrite's memory — the file it produces is identical regardless of value.
|
|
14
|
+
*/
|
|
15
|
+
const TRUNCATE_FLUSH_BYTES = 64 * 1024;
|
|
8
16
|
/**
|
|
9
17
|
* JSONL handler class for message persistence operations
|
|
10
18
|
*/
|
|
@@ -42,6 +50,49 @@ export class JsonlHandler {
|
|
|
42
50
|
async appendMessage(filePath, message) {
|
|
43
51
|
return this.appendMessages(filePath, [message]);
|
|
44
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Append a user-set conversation title (`{"type":"custom-title",...}`).
|
|
55
|
+
*
|
|
56
|
+
* Appending — never rewriting — keeps the file's append-only contract intact
|
|
57
|
+
* and leaves the metadata header and every message untouched. The entry has
|
|
58
|
+
* no `timestamp`, so it can never be mistaken for a message by readers that
|
|
59
|
+
* filter on timestamps.
|
|
60
|
+
*/
|
|
61
|
+
async appendCustomTitle(filePath, customTitle, sessionId) {
|
|
62
|
+
const entry = {
|
|
63
|
+
type: "custom-title",
|
|
64
|
+
customTitle,
|
|
65
|
+
...(sessionId ? { sessionId } : {}),
|
|
66
|
+
};
|
|
67
|
+
await this.ensureDirectory(dirname(filePath));
|
|
68
|
+
await appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read the session's user-set title from the tail window, if present.
|
|
72
|
+
*
|
|
73
|
+
* Scans the tail and keeps the newest entry, so a rename always wins over an
|
|
74
|
+
* earlier one. Only the tail is read: this is the cheap path used by session
|
|
75
|
+
* listing and by `reAppendSessionMetadata`'s external-writer refresh, and the
|
|
76
|
+
* write side re-appends the entry at EOF at its sparse flush points. An entry
|
|
77
|
+
* that later messages have already pushed out of the window is invisible
|
|
78
|
+
* here — that is what `readMessagesAndCustomTitle` recovers on resume.
|
|
79
|
+
*/
|
|
80
|
+
async readCustomTitle(filePath) {
|
|
81
|
+
for (const line of (await readTailLines(filePath)).reverse()) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(line);
|
|
84
|
+
if (parsed.type !== "custom-title")
|
|
85
|
+
continue;
|
|
86
|
+
return typeof parsed.customTitle === "string" && parsed.customTitle
|
|
87
|
+
? parsed.customTitle
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Partial line at the tail window's boundary — keep looking.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
45
96
|
/**
|
|
46
97
|
* Append multiple messages to JSONL file
|
|
47
98
|
*/
|
|
@@ -86,37 +137,76 @@ export class JsonlHandler {
|
|
|
86
137
|
* Read all messages from JSONL file (simplified - no metadata handling)
|
|
87
138
|
*/
|
|
88
139
|
async read(filePath) {
|
|
140
|
+
return (await this.readMessagesAndCustomTitle(filePath)).messages;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Read the whole file once, returning every message plus the newest
|
|
144
|
+
* `custom-title` entry.
|
|
145
|
+
*
|
|
146
|
+
* `readCustomTitle` only scans the tail window, so a title that later
|
|
147
|
+
* messages pushed out of it is invisible there. The whole file still holds
|
|
148
|
+
* it, and the resume path collects it here: that recovered value is what a
|
|
149
|
+
* later `reAppendSessionMetadata` writes back at EOF, which is what makes an
|
|
150
|
+
* evicted title self-heal instead of being lost for good.
|
|
151
|
+
*
|
|
152
|
+
* @param filePath - Path to the session JSONL file
|
|
153
|
+
* @returns The messages (reserved entries filtered out) and the newest
|
|
154
|
+
* non-empty custom title, when the file carries one
|
|
155
|
+
*/
|
|
156
|
+
async readMessagesAndCustomTitle(filePath) {
|
|
89
157
|
try {
|
|
90
158
|
const content = await readFile(filePath, "utf8");
|
|
159
|
+
// append() always terminates a written batch with "\n" (see append),
|
|
160
|
+
// so a file that does not end with a newline means the process was
|
|
161
|
+
// interrupted mid-write, leaving a partial trailing line behind.
|
|
162
|
+
const endsWithNewline = content.length === 0 || content.endsWith("\n");
|
|
91
163
|
const lines = content
|
|
92
164
|
.split(/\r?\n/)
|
|
93
165
|
.map((line) => line.trim())
|
|
94
166
|
.filter((line) => line.length > 0);
|
|
95
167
|
if (lines.length === 0) {
|
|
96
|
-
return [];
|
|
168
|
+
return { messages: [] };
|
|
97
169
|
}
|
|
98
170
|
const allMessages = [];
|
|
99
|
-
|
|
171
|
+
let customTitle;
|
|
172
|
+
// Parse every line: real messages are kept, reserved entries (the
|
|
173
|
+
// metadata header, and custom titles) are read for their own fields.
|
|
100
174
|
for (let i = 0; i < lines.length; i++) {
|
|
101
175
|
const line = lines[i];
|
|
102
176
|
try {
|
|
103
177
|
const message = JSON.parse(line);
|
|
104
|
-
|
|
105
|
-
|
|
178
|
+
if (message.type === "custom-title") {
|
|
179
|
+
// Last one wins: a later rename supersedes an earlier one.
|
|
180
|
+
if (typeof message.customTitle === "string" &&
|
|
181
|
+
message.customTitle) {
|
|
182
|
+
customTitle = message.customTitle;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// Any other reserved entry (metadata header): not a message
|
|
187
|
+
if (isReservedEntry(message.type))
|
|
106
188
|
continue;
|
|
107
189
|
if (message.timestamp)
|
|
108
190
|
allMessages.push(message);
|
|
109
191
|
}
|
|
110
192
|
catch (error) {
|
|
193
|
+
// A parse failure on the final line of a file that lacks a trailing
|
|
194
|
+
// newline is the residue of an interrupted append (complete batches
|
|
195
|
+
// always end with "\n"): drop the partial line and keep every
|
|
196
|
+
// message that was fully written. Corruption on any earlier line is
|
|
197
|
+
// genuine corruption and still fails.
|
|
198
|
+
if (!endsWithNewline && i === lines.length - 1) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
111
201
|
// Throw error for invalid JSON lines with line number
|
|
112
202
|
throw new Error(`Invalid JSON at line ${i + 1}: ${error}`);
|
|
113
203
|
}
|
|
114
204
|
}
|
|
115
|
-
return allMessages;
|
|
205
|
+
return { messages: allMessages, customTitle };
|
|
116
206
|
}
|
|
117
207
|
catch (error) {
|
|
118
208
|
if (error.code === "ENOENT") {
|
|
119
|
-
return [];
|
|
209
|
+
return { messages: [] };
|
|
120
210
|
}
|
|
121
211
|
throw new Error(`Failed to read JSONL file "${filePath}": ${error}`);
|
|
122
212
|
}
|
|
@@ -143,20 +233,63 @@ export class JsonlHandler {
|
|
|
143
233
|
}
|
|
144
234
|
try {
|
|
145
235
|
const parsed = JSON.parse(lastLine);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return null;
|
|
236
|
+
if (!isReservedEntry(parsed.type)) {
|
|
237
|
+
return parsed;
|
|
149
238
|
}
|
|
150
|
-
return parsed;
|
|
151
239
|
}
|
|
152
240
|
catch (error) {
|
|
153
241
|
throw new Error(`Invalid JSON in last line of "${filePath}": ${error}`);
|
|
154
242
|
}
|
|
243
|
+
// The file ends with a reserved entry — a `custom-title` appended by a
|
|
244
|
+
// rename, or the metadata header of a session with no messages yet. Both
|
|
245
|
+
// are non-messages, so walk the tail window back to the newest message.
|
|
246
|
+
for (const line of (await readTailLines(filePath)).reverse()) {
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse(line);
|
|
249
|
+
if (isReservedEntry(parsed.type))
|
|
250
|
+
continue;
|
|
251
|
+
return parsed;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Partial line at the tail window's boundary — keep looking.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
155
258
|
}
|
|
156
259
|
catch (error) {
|
|
157
260
|
throw new Error(`Failed to get last message from "${filePath}": ${error}`);
|
|
158
261
|
}
|
|
159
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Latest context-usage total for a session file.
|
|
265
|
+
*
|
|
266
|
+
* Sessions accumulate usage-less messages at the end — SessionStart hook
|
|
267
|
+
* meta messages are appended (and persisted) on every resume — so the last
|
|
268
|
+
* line alone cannot answer "how much context did this conversation use".
|
|
269
|
+
* Scan backwards over the file's tail window and take the newest message
|
|
270
|
+
* that carries usage.
|
|
271
|
+
*
|
|
272
|
+
* @param filePath - Path to the session JSONL file
|
|
273
|
+
* @returns total_tokens of the newest usage-bearing message, or 0 when the
|
|
274
|
+
* tail holds none (empty/unreadable file, no completed request yet)
|
|
275
|
+
*/
|
|
276
|
+
async getLatestTotalTokens(filePath) {
|
|
277
|
+
const messages = [];
|
|
278
|
+
for (const line of await readTailLines(filePath)) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(line);
|
|
281
|
+
// Reserved entry (metadata header, custom-title): not a message
|
|
282
|
+
if (isReservedEntry(parsed.type))
|
|
283
|
+
continue;
|
|
284
|
+
messages.push(parsed);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Partial line at the tail window's boundary — drop it.
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return extractLatestTotalTokens(messages);
|
|
292
|
+
}
|
|
160
293
|
/**
|
|
161
294
|
* Read the creation-time metadata from the session file's header line.
|
|
162
295
|
*
|
|
@@ -186,6 +319,111 @@ export class JsonlHandler {
|
|
|
186
319
|
}
|
|
187
320
|
return null;
|
|
188
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Rewrite a session file keeping only its first `keepMessageCount` messages.
|
|
324
|
+
*
|
|
325
|
+
* This is the one operation that removes history from a session file
|
|
326
|
+
* (`/rewind`); every other write appends. It is a filter over the file's
|
|
327
|
+
* original lines rather than a re-serialization of parsed messages:
|
|
328
|
+
*
|
|
329
|
+
* - Reserved entries (`metadata` header, `custom-title`, and any type added
|
|
330
|
+
* to `RESERVED_ENTRY_TYPES` later) are carried over byte for byte, wherever
|
|
331
|
+
* they sit in the file. Rebuilding the file from `Message[]` used to lose
|
|
332
|
+
* them — `read()` filters reserved entries out, so a rewrite could not see
|
|
333
|
+
* the header or the title — which silently degraded the session to the
|
|
334
|
+
* "legacy file without a header" shape: `createdAt` fell back to
|
|
335
|
+
* `new Date()`, `gitBranch` vanished, and the title could never be read
|
|
336
|
+
* back again.
|
|
337
|
+
* - Lines that are neither reserved nor a message (no `timestamp`) are also
|
|
338
|
+
* carried over, so an entry type written by a newer version survives a
|
|
339
|
+
* rewind performed by an older one.
|
|
340
|
+
* - Only message lines past `keepMessageCount` are dropped.
|
|
341
|
+
*
|
|
342
|
+
* The result is written to a temp file and renamed into place, so an
|
|
343
|
+
* interrupted rewrite leaves the original file untouched.
|
|
344
|
+
*
|
|
345
|
+
* The "is this a message line" test deliberately mirrors `read()`
|
|
346
|
+
* (`!isReservedEntry(type) && timestamp`) and the partial-trailing-line
|
|
347
|
+
* tolerance mirrors its interrupted-append handling, so the caller's message
|
|
348
|
+
* count and this file's line order stay in step.
|
|
349
|
+
*
|
|
350
|
+
* @param filePath - Session JSONL path (must already exist)
|
|
351
|
+
* @param keepMessageCount - Number of leading messages to keep
|
|
352
|
+
*/
|
|
353
|
+
async truncateSession(filePath, keepMessageCount) {
|
|
354
|
+
const tempPath = `${filePath}.tmp.${process.pid}.${randomUUID()}`;
|
|
355
|
+
try {
|
|
356
|
+
// Materialize the temp file up front so an empty result still renames.
|
|
357
|
+
await writeFile(tempPath, "", "utf8");
|
|
358
|
+
let keptMessages = 0;
|
|
359
|
+
let pendingBrokenLine = false;
|
|
360
|
+
let buffered = [];
|
|
361
|
+
let bufferedLength = 0;
|
|
362
|
+
const flush = async () => {
|
|
363
|
+
if (buffered.length === 0) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await appendFile(tempPath, buffered.join(""), "utf8");
|
|
367
|
+
buffered = [];
|
|
368
|
+
bufferedLength = 0;
|
|
369
|
+
};
|
|
370
|
+
const keepLine = async (line) => {
|
|
371
|
+
const chunk = `${line}\n`;
|
|
372
|
+
buffered.push(chunk);
|
|
373
|
+
bufferedLength += chunk.length;
|
|
374
|
+
if (bufferedLength >= TRUNCATE_FLUSH_BYTES) {
|
|
375
|
+
await flush();
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const handleLine = async (raw) => {
|
|
379
|
+
const line = raw.trim();
|
|
380
|
+
if (line.length === 0) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
let parsed;
|
|
384
|
+
try {
|
|
385
|
+
parsed = JSON.parse(line);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
if (pendingBrokenLine) {
|
|
389
|
+
throw new Error(`Invalid JSON in session file "${filePath}": ${error}`);
|
|
390
|
+
}
|
|
391
|
+
pendingBrokenLine = true;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// A parseable line after a broken one proves the broken line was not
|
|
395
|
+
// the file's last, so it is corruption rather than a partial write.
|
|
396
|
+
if (pendingBrokenLine) {
|
|
397
|
+
throw new Error(`Invalid JSON in session file "${filePath}": truncated trailing line`);
|
|
398
|
+
}
|
|
399
|
+
if (isReservedEntry(parsed.type) || !parsed.timestamp) {
|
|
400
|
+
await keepLine(line);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (keptMessages < keepMessageCount) {
|
|
404
|
+
keptMessages++;
|
|
405
|
+
await keepLine(line);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const { endsWithNewline } = await forEachLine(filePath, handleLine);
|
|
409
|
+
// A broken line is only acceptable as the last line of a file that lacks
|
|
410
|
+
// a trailing newline — the residue of an interrupted append.
|
|
411
|
+
if (pendingBrokenLine && endsWithNewline) {
|
|
412
|
+
throw new Error(`Invalid JSON in session file "${filePath}"`);
|
|
413
|
+
}
|
|
414
|
+
await flush();
|
|
415
|
+
await rename(tempPath, filePath);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
try {
|
|
419
|
+
await unlink(tempPath);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// Cleanup is best effort — the original error is more important.
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
189
427
|
/**
|
|
190
428
|
* Validate messages before writing
|
|
191
429
|
*/
|
|
@@ -19,11 +19,27 @@ export declare class MemoryService {
|
|
|
19
19
|
*/
|
|
20
20
|
ensureAutoMemoryDirectory(workdir: string): Promise<void>;
|
|
21
21
|
/**
|
|
22
|
-
* Get the
|
|
22
|
+
* Get the auto-memory entrypoint (`MEMORY.md`), bounded by both the line and
|
|
23
|
+
* the character cap. Truncation appends a warning naming the cap that fired,
|
|
24
|
+
* so the model can tell the index it is reading is incomplete.
|
|
23
25
|
*/
|
|
24
26
|
getAutoMemoryContent(workdir: string): Promise<string>;
|
|
25
27
|
ensureUserMemoryFile(): Promise<void>;
|
|
26
28
|
getUserMemoryContent(): Promise<string>;
|
|
27
29
|
readMemoryFile(workdir: string): Promise<string>;
|
|
28
30
|
getCombinedMemoryContent(workdir: string): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Persist the user-level memory file (~/.wave/AGENTS.md), creating it if
|
|
33
|
+
* missing. Clears the user-memory cache so subsequent reads see the new
|
|
34
|
+
* content (the default-template fallback to ~/.claude/AGENTS.md is also
|
|
35
|
+
* bypassed once the user writes real content).
|
|
36
|
+
*/
|
|
37
|
+
writeUserMemoryContent(content: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Persist the project-level memory file (<workdir>/AGENTS.md), creating it
|
|
40
|
+
* if missing. Reads fall back to CLAUDE.md but writes always target
|
|
41
|
+
* AGENTS.md (the canonical project memory file). Clears the project-memory
|
|
42
|
+
* cache so subsequent reads see the new content.
|
|
43
|
+
*/
|
|
44
|
+
writeProjectMemoryContent(workdir: string, content: string): Promise<void>;
|
|
29
45
|
}
|
package/dist/services/memory.js
CHANGED
|
@@ -3,8 +3,11 @@ import * as path from "node:path";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { USER_MEMORY_FILE, DATA_DIRECTORY } from "../utils/constants.js";
|
|
5
5
|
import { logger } from "../utils/globalLogger.js";
|
|
6
|
+
import { atomicWriteFile } from "../utils/atomicWrite.js";
|
|
6
7
|
import { getGitCommonDir } from "../utils/gitUtils.js";
|
|
7
8
|
import { pathEncoder } from "../utils/pathEncoder.js";
|
|
9
|
+
import { MEMORY_ENTRYPOINT_NAME } from "../constants/memory.js";
|
|
10
|
+
import { truncateEntrypointContent } from "../utils/memoryEntrypoint.js";
|
|
8
11
|
export class MemoryService {
|
|
9
12
|
constructor(container) {
|
|
10
13
|
this.container = container;
|
|
@@ -51,7 +54,7 @@ export class MemoryService {
|
|
|
51
54
|
catch (error) {
|
|
52
55
|
if (error.code === "ENOENT") {
|
|
53
56
|
const initialContent = "# Project Memory\n\nThis file serves as an index for the project's auto-memory. Wave uses this to track knowledge across sessions.\n\n";
|
|
54
|
-
await
|
|
57
|
+
await atomicWriteFile(memoryFile, initialContent);
|
|
55
58
|
logger.debug(`Created auto-memory file: ${memoryFile}`);
|
|
56
59
|
}
|
|
57
60
|
else {
|
|
@@ -65,18 +68,19 @@ export class MemoryService {
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
68
|
-
* Get the
|
|
71
|
+
* Get the auto-memory entrypoint (`MEMORY.md`), bounded by both the line and
|
|
72
|
+
* the character cap. Truncation appends a warning naming the cap that fired,
|
|
73
|
+
* so the model can tell the index it is reading is incomplete.
|
|
69
74
|
*/
|
|
70
75
|
async getAutoMemoryContent(workdir) {
|
|
71
76
|
if (this._cachedAutoMemoryContent !== null) {
|
|
72
77
|
return this._cachedAutoMemoryContent;
|
|
73
78
|
}
|
|
74
79
|
const memoryDir = this.getAutoMemoryDirectory(workdir);
|
|
75
|
-
const memoryFile = path.join(memoryDir,
|
|
80
|
+
const memoryFile = path.join(memoryDir, MEMORY_ENTRYPOINT_NAME);
|
|
76
81
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
this._cachedAutoMemoryContent = lines.join("\n");
|
|
82
|
+
const raw = await fs.readFile(memoryFile, "utf-8");
|
|
83
|
+
this._cachedAutoMemoryContent = truncateEntrypointContent(raw).content;
|
|
80
84
|
return this._cachedAutoMemoryContent;
|
|
81
85
|
}
|
|
82
86
|
catch (error) {
|
|
@@ -103,7 +107,7 @@ export class MemoryService {
|
|
|
103
107
|
userMemoryFile: USER_MEMORY_FILE,
|
|
104
108
|
});
|
|
105
109
|
const initialContent = "# User Memory\n\nThis is the user-level memory file, recording important information and context across projects.\n\n";
|
|
106
|
-
await
|
|
110
|
+
await atomicWriteFile(USER_MEMORY_FILE, initialContent);
|
|
107
111
|
logger.debug(`Created user memory file: ${USER_MEMORY_FILE}`);
|
|
108
112
|
}
|
|
109
113
|
else {
|
|
@@ -215,4 +219,37 @@ export class MemoryService {
|
|
|
215
219
|
this._cachedCombinedMemory = combined;
|
|
216
220
|
return combined;
|
|
217
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Persist the user-level memory file (~/.wave/AGENTS.md), creating it if
|
|
224
|
+
* missing. Clears the user-memory cache so subsequent reads see the new
|
|
225
|
+
* content (the default-template fallback to ~/.claude/AGENTS.md is also
|
|
226
|
+
* bypassed once the user writes real content).
|
|
227
|
+
*/
|
|
228
|
+
async writeUserMemoryContent(content) {
|
|
229
|
+
await this.ensureUserMemoryFile();
|
|
230
|
+
await atomicWriteFile(USER_MEMORY_FILE, content);
|
|
231
|
+
this._cachedUserMemory = null;
|
|
232
|
+
this._cachedCombinedMemory = null;
|
|
233
|
+
logger.debug("User memory content written", {
|
|
234
|
+
userMemoryFile: USER_MEMORY_FILE,
|
|
235
|
+
contentLength: content.length,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Persist the project-level memory file (<workdir>/AGENTS.md), creating it
|
|
240
|
+
* if missing. Reads fall back to CLAUDE.md but writes always target
|
|
241
|
+
* AGENTS.md (the canonical project memory file). Clears the project-memory
|
|
242
|
+
* cache so subsequent reads see the new content.
|
|
243
|
+
*/
|
|
244
|
+
async writeProjectMemoryContent(workdir, content) {
|
|
245
|
+
const memoryFilePath = path.join(workdir, "AGENTS.md");
|
|
246
|
+
await fs.mkdir(workdir, { recursive: true });
|
|
247
|
+
await atomicWriteFile(memoryFilePath, content);
|
|
248
|
+
this._cachedProjectMemory = null;
|
|
249
|
+
this._cachedCombinedMemory = null;
|
|
250
|
+
logger.debug("Project memory content written", {
|
|
251
|
+
memoryFilePath,
|
|
252
|
+
contentLength: content.length,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
218
255
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin official marketplace zip-snapshot mirror fetch.
|
|
3
|
+
*
|
|
4
|
+
* The official marketplace (wave-plugins-official, BUILTIN in MarketplaceService)
|
|
5
|
+
* is hosted on GitHub, which is poorly reachable from CN networks. Instead of a
|
|
6
|
+
* dumb-HTTP git endpoint we mirror the "content-addressed zip + latest pointer +
|
|
7
|
+
* local sentinel" model Claude Code uses for its official marketplace
|
|
8
|
+
* (officialMarketplaceGcs.ts, inc-5046):
|
|
9
|
+
*
|
|
10
|
+
* 1. GET `{base}/latest` (10s timeout) → content-addressed sha
|
|
11
|
+
* 2. Compare against the local sentinel `.wave-market-sha` at the market root
|
|
12
|
+
* — equal ⇒ no-op (idempotent, one ~40B request per run)
|
|
13
|
+
* 3. GET `{base}/{sha}.zip` (60s timeout) → extract into a `.staging` dir
|
|
14
|
+
* (path-traversal/absolute-path rejection, size limits, exec-bit restore
|
|
15
|
+
* from zip external attrs)
|
|
16
|
+
* 4. Atomic swap: rm old market dir + rename staging → market dir, write the
|
|
17
|
+
* sentinel back
|
|
18
|
+
*
|
|
19
|
+
* Any failure returns null; the caller decides whether to fall through to the
|
|
20
|
+
* existing git path (guarded by a kill switch, mirroring Claude's
|
|
21
|
+
* tengu_plugin_official_mkt_git_fallback flag).
|
|
22
|
+
*/
|
|
23
|
+
/** Sentinel file name stored at the market root holding the last fetched sha. */
|
|
24
|
+
export declare const OFFICIAL_MARKET_SENTINEL_FILE = ".wave-market-sha";
|
|
25
|
+
/** Env var override for the mirror base URL (highest precedence). */
|
|
26
|
+
export declare const OFFICIAL_MARKET_MIRROR_BASE_URL_ENV = "WAVE_OFFICIAL_MARKET_MIRROR_BASE_URL";
|
|
27
|
+
/**
|
|
28
|
+
* Kill switch for the git fallback after a mirror failure (default: allowed).
|
|
29
|
+
* Mirrors Claude Code's `tengu_plugin_official_mkt_git_fallback` flag. When
|
|
30
|
+
* false and the mirror fails, the builtin marketplace update is skipped
|
|
31
|
+
* (retried on next run) instead of hitting GitHub.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ALLOW_OFFICIAL_MARKET_GIT_FALLBACK = true;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the mirror base URL: env var wins, otherwise the built-in constant.
|
|
36
|
+
* Returns null when neither is configured (mirror channel disabled).
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveOfficialMarketplaceMirrorBaseUrl(): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Rejects zip entry paths that would escape the extraction root: absolute
|
|
41
|
+
* paths (POSIX or Windows drive/UNC), `..` segments, and null bytes. Paths
|
|
42
|
+
* use either `/` or `\` as separator. Returns the normalized relative path on
|
|
43
|
+
* success or null for entries that must be skipped (empty/directory-only is
|
|
44
|
+
* handled by the caller via the trailing `/`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function sanitizeZipEntryPath(entryPath: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Fetches the official marketplace from the zip-snapshot mirror and extracts
|
|
49
|
+
* it to installLocation. Idempotent — compares the local `.wave-market-sha`
|
|
50
|
+
* sentinel before downloading.
|
|
51
|
+
*
|
|
52
|
+
* @param installLocation where to extract (must be inside marketplacesCacheDir)
|
|
53
|
+
* @param marketplacesCacheDir the marketplace cache root (defense-in-depth:
|
|
54
|
+
* this function `rm`s installLocation during the atomic swap)
|
|
55
|
+
* @param baseUrl optional explicit base URL (tests); defaults to env/constant
|
|
56
|
+
* @returns the fetched sha on success (including no-op), null on any failure
|
|
57
|
+
* (unconfigured mirror, network, 404, corrupt zip, unsafe entries). Caller
|
|
58
|
+
* decides whether to fall through to git.
|
|
59
|
+
*/
|
|
60
|
+
export declare function fetchOfficialMarketplaceFromMirror(installLocation: string, marketplacesCacheDir: string, baseUrl?: string): Promise<string | null>;
|
|
61
|
+
interface ZipValidationState {
|
|
62
|
+
fileCount: number;
|
|
63
|
+
totalUncompressed: number;
|
|
64
|
+
compressedSize: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validates one zip entry (called from the fflate filter). Throws to abort the
|
|
68
|
+
* whole extraction when an entry is unsafe or the archive exceeds limits.
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateZipEntry(name: string, originalSize: number, state: ZipValidationState): void;
|
|
71
|
+
/**
|
|
72
|
+
* Parse Unix file modes from a zip's central directory.
|
|
73
|
+
*
|
|
74
|
+
* fflate's `unzipSync` returns only `Record<string, Uint8Array>` — it does not
|
|
75
|
+
* surface the external file attributes stored in the central directory, so
|
|
76
|
+
* executable bits would be lost (everything extracts as 0644). Returns
|
|
77
|
+
* `name → mode` for entries created on a Unix host (`versionMadeBy` high byte
|
|
78
|
+
* === 3); other entries are omitted and extract with default mode.
|
|
79
|
+
*
|
|
80
|
+
* Format per PKZIP APPNOTE.TXT §4.3.12 (central directory) and §4.3.16 (EOCD).
|
|
81
|
+
* ZIP64 is not handled — returns `{}` on archives >4GB or >65535 entries,
|
|
82
|
+
* which is fine for marketplace zips.
|
|
83
|
+
*/
|
|
84
|
+
export declare function parseZipModes(data: Uint8Array): Record<string, number>;
|
|
85
|
+
export {};
|