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.
Files changed (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. 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
- // Parse all messages, skipping the metadata header line (if any)
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
- // Metadata header line: not a message, skip
105
- if (message.type === "metadata")
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
- // A file whose only line is the metadata header has no messages yet
147
- if (parsed.type === "metadata") {
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 first 200 lines of MEMORY.md.
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
  }
@@ -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 fs.writeFile(memoryFile, initialContent, "utf-8");
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 first 200 lines of MEMORY.md.
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, "MEMORY.md");
80
+ const memoryFile = path.join(memoryDir, MEMORY_ENTRYPOINT_NAME);
76
81
  try {
77
- const content = await fs.readFile(memoryFile, "utf-8");
78
- const lines = content.split("\n").slice(0, 200);
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 fs.writeFile(USER_MEMORY_FILE, initialContent, "utf-8");
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 {};