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.
Files changed (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -5,64 +5,22 @@ import { join } from "path";
5
5
  import { tmpdir } from "os";
6
6
  import { SKILL_BASH_MAX_OUTPUT_CHARS, PREVIEW_SIZE_BYTES, } from "../constants/toolLimits.js";
7
7
  import { logger } from "./globalLogger.js";
8
+ import { parseFrontmatterYaml, splitFrontmatter } from "./frontmatterYaml.js";
8
9
  import { resolveShellPath } from "./shellResolver.js";
9
10
  const execAsync = promisify(exec);
10
11
  /**
11
- * Parse YAML frontmatter from markdown content
12
+ * Parse YAML frontmatter from markdown content.
13
+ *
14
+ * Value parsing is shared with skills, subagents and memory files — see
15
+ * `frontmatterYaml.ts` (spec `ui/slash-commands` 场景 3,
16
+ * `core/memory-management` 场景 4/13).
12
17
  */
13
18
  export function parseFrontmatter(content) {
14
- const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
15
- const match = content.match(frontmatterRegex);
16
- if (!match) {
17
- return { content };
18
- }
19
- const [, frontmatterStr, bodyContent] = match;
20
- try {
21
- // Simple YAML parser for our use case (supports key: value and list items)
22
- const frontmatter = {};
23
- const lines = frontmatterStr.split("\n");
24
- let currentKey = null;
25
- for (const line of lines) {
26
- const trimmedLine = line.trim();
27
- if (!trimmedLine || trimmedLine.startsWith("#"))
28
- continue;
29
- // Check if it's a list item
30
- if (trimmedLine.startsWith("-") && currentKey) {
31
- let value = trimmedLine.slice(1).trim();
32
- if (value) {
33
- // Remove surrounding quotes if present
34
- if ((value.startsWith('"') && value.endsWith('"')) ||
35
- (value.startsWith("'") && value.endsWith("'"))) {
36
- value = value.slice(1, -1);
37
- }
38
- if (!Array.isArray(frontmatter[currentKey])) {
39
- frontmatter[currentKey] = [];
40
- }
41
- frontmatter[currentKey].push(value);
42
- }
43
- continue;
44
- }
45
- const colonIndex = trimmedLine.indexOf(":");
46
- if (colonIndex === -1)
47
- continue;
48
- const key = trimmedLine.slice(0, colonIndex).trim();
49
- const value = trimmedLine.slice(colonIndex + 1).trim();
50
- currentKey = key;
51
- if (value) {
52
- // Remove surrounding quotes if present
53
- const unquotedValue = (value.startsWith('"') && value.endsWith('"')) ||
54
- (value.startsWith("'") && value.endsWith("'"))
55
- ? value.slice(1, -1)
56
- : value;
57
- frontmatter[key] = unquotedValue;
58
- }
59
- }
60
- return { frontmatter, content: bodyContent };
61
- }
62
- catch {
63
- // If parsing fails, just return the content without frontmatter
19
+ const { yaml, body } = splitFrontmatter(content);
20
+ if (yaml === null) {
64
21
  return { content };
65
22
  }
23
+ return { frontmatter: parseFrontmatterYaml(yaml), content: body };
66
24
  }
67
25
  /**
68
26
  * Parse markdown file and extract config and content
@@ -0,0 +1,61 @@
1
+ /**
2
+ * MCP server usage notes, announced in the conversation instead of the system
3
+ * prompt (aligned with Claude Code's `mcp_instructions_delta` attachment).
4
+ *
5
+ * A server's `initialize.instructions` is the one channel for "you need to know
6
+ * this without calling anything" content: rate limits, preconditions, whole-server
7
+ * conventions. It cannot ride in a tool description — the catalog compresses those
8
+ * — and it must not ride in the system prompt: it appears only when a connection
9
+ * happens, and a connection that happens mid-session would rewrite the whole
10
+ * cached prefix (that is exactly the note Claude Code left on the system-prompt
11
+ * variant it replaced: "busts the prompt cache on late MCP connect").
12
+ *
13
+ * So each server's notes are announced once, as a persisted meta message appended
14
+ * at the tail. Two consequences are deliberate:
15
+ * - History is not rewritten, so a disconnection cannot take the prose back out
16
+ * of the context. It gets a "no longer applies" notice instead.
17
+ * - The announced set is derived from the history itself (a marker line), not
18
+ * from process state, so there is nothing to desync: if the marker is no longer
19
+ * there (compaction, rewind), the next turn announces the server again. A
20
+ * duplicate is better than a loss.
21
+ */
22
+ import type { Message } from "../types/messaging.js";
23
+ /**
24
+ * Per-server cap on announced notes, in characters. Borrowed from Claude Code's
25
+ * `MAX_MCP_DESCRIPTION_LENGTH` (`services/mcp/client.ts`), which caps both MCP tool
26
+ * descriptions and server instructions at this value. Counted in characters like
27
+ * CC, not CJK-aware: what MCP servers write about themselves is overwhelmingly
28
+ * English, the same assumption the catalog budget makes.
29
+ */
30
+ export declare const MCP_INSTRUCTIONS_MAX_CHARS = 2048;
31
+ /** What one announcement carries: who appeared, who went away. */
32
+ export interface McpInstructionsDelta {
33
+ added: Array<{
34
+ name: string;
35
+ instructions: string;
36
+ }>;
37
+ removed: string[];
38
+ }
39
+ /**
40
+ * Bound a server's notes to the cap, marking the cut so the model knows it is
41
+ * reading an incomplete copy rather than a short one.
42
+ */
43
+ export declare function truncateMcpInstructions(raw: string | undefined): string | undefined;
44
+ /**
45
+ * Servers whose notes the history already contains: markers are folded in message
46
+ * order, so an announced server that later went away stops counting as announced
47
+ * (and is announced again if it comes back).
48
+ *
49
+ * Only the scanner's own messages are read. A marker quoted anywhere else — a
50
+ * reply explaining the mechanism, a hook echoing one — is prose *about* a marker,
51
+ * not a statement of connection state. Counting it invents an announcement, and
52
+ * the invented server is then reported as departed as soon as it is unavailable.
53
+ */
54
+ export declare function collectAnnouncedServers(messages: Message[]): Set<string>;
55
+ /**
56
+ * Render one announcement, or null when there is nothing to say. The marker line
57
+ * leads so the scan reads state without parsing the prose, and the prose keeps
58
+ * opencode's `<mcp_instructions>` envelope so notes stay distinguishable per
59
+ * server and verbatim per line.
60
+ */
61
+ export declare function buildMcpInstructionsAnnouncement(delta: McpInstructionsDelta): string | null;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * MCP server usage notes, announced in the conversation instead of the system
3
+ * prompt (aligned with Claude Code's `mcp_instructions_delta` attachment).
4
+ *
5
+ * A server's `initialize.instructions` is the one channel for "you need to know
6
+ * this without calling anything" content: rate limits, preconditions, whole-server
7
+ * conventions. It cannot ride in a tool description — the catalog compresses those
8
+ * — and it must not ride in the system prompt: it appears only when a connection
9
+ * happens, and a connection that happens mid-session would rewrite the whole
10
+ * cached prefix (that is exactly the note Claude Code left on the system-prompt
11
+ * variant it replaced: "busts the prompt cache on late MCP connect").
12
+ *
13
+ * So each server's notes are announced once, as a persisted meta message appended
14
+ * at the tail. Two consequences are deliberate:
15
+ * - History is not rewritten, so a disconnection cannot take the prose back out
16
+ * of the context. It gets a "no longer applies" notice instead.
17
+ * - The announced set is derived from the history itself (a marker line), not
18
+ * from process state, so there is nothing to desync: if the marker is no longer
19
+ * there (compaction, rewind), the next turn announces the server again. A
20
+ * duplicate is better than a loss.
21
+ */
22
+ /**
23
+ * Per-server cap on announced notes, in characters. Borrowed from Claude Code's
24
+ * `MAX_MCP_DESCRIPTION_LENGTH` (`services/mcp/client.ts`), which caps both MCP tool
25
+ * descriptions and server instructions at this value. Counted in characters like
26
+ * CC, not CJK-aware: what MCP servers write about themselves is overwhelmingly
27
+ * English, the same assumption the catalog budget makes.
28
+ */
29
+ export const MCP_INSTRUCTIONS_MAX_CHARS = 2048;
30
+ /** Marks an announcement message so the history scan can find it. */
31
+ const MARKER_PREFIX = "<!-- mcp-instructions ";
32
+ const MARKER_SUFFIX = " -->";
33
+ /**
34
+ * Bound a server's notes to the cap, marking the cut so the model knows it is
35
+ * reading an incomplete copy rather than a short one.
36
+ */
37
+ export function truncateMcpInstructions(raw) {
38
+ if (raw === undefined || raw.length <= MCP_INSTRUCTIONS_MAX_CHARS) {
39
+ return raw;
40
+ }
41
+ return `${raw.slice(0, MCP_INSTRUCTIONS_MAX_CHARS)}… [truncated]`;
42
+ }
43
+ /**
44
+ * Servers whose notes the history already contains: markers are folded in message
45
+ * order, so an announced server that later went away stops counting as announced
46
+ * (and is announced again if it comes back).
47
+ *
48
+ * Only the scanner's own messages are read. A marker quoted anywhere else — a
49
+ * reply explaining the mechanism, a hook echoing one — is prose *about* a marker,
50
+ * not a statement of connection state. Counting it invents an announcement, and
51
+ * the invented server is then reported as departed as soon as it is unavailable.
52
+ */
53
+ export function collectAnnouncedServers(messages) {
54
+ const announced = new Set();
55
+ for (const message of messages) {
56
+ if (message.isMeta !== true)
57
+ continue;
58
+ for (const block of message.blocks) {
59
+ if (block.type !== "text")
60
+ continue;
61
+ for (const line of block.content.split("\n")) {
62
+ const delta = parseMarkerLine(line);
63
+ if (!delta)
64
+ continue;
65
+ for (const name of delta.added)
66
+ announced.add(name);
67
+ for (const name of delta.removed)
68
+ announced.delete(name);
69
+ }
70
+ }
71
+ }
72
+ return announced;
73
+ }
74
+ function parseMarkerLine(line) {
75
+ const trimmed = line.trim();
76
+ if (!trimmed.startsWith(MARKER_PREFIX) || !trimmed.endsWith(MARKER_SUFFIX)) {
77
+ return null;
78
+ }
79
+ const payload = trimmed.slice(MARKER_PREFIX.length, trimmed.length - MARKER_SUFFIX.length);
80
+ try {
81
+ const parsed = JSON.parse(payload);
82
+ return {
83
+ added: Array.isArray(parsed.added) ? parsed.added.map(String) : [],
84
+ removed: Array.isArray(parsed.removed) ? parsed.removed.map(String) : [],
85
+ };
86
+ }
87
+ catch {
88
+ // A marker we cannot read (hand-edited or corrupted history) must not take
89
+ // the turn down; the worst case is announcing that server once more.
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Render one announcement, or null when there is nothing to say. The marker line
95
+ * leads so the scan reads state without parsing the prose, and the prose keeps
96
+ * opencode's `<mcp_instructions>` envelope so notes stay distinguishable per
97
+ * server and verbatim per line.
98
+ */
99
+ export function buildMcpInstructionsAnnouncement(delta) {
100
+ if (delta.added.length === 0 && delta.removed.length === 0)
101
+ return null;
102
+ const parts = [
103
+ `${MARKER_PREFIX}${JSON.stringify({
104
+ added: delta.added.map((server) => server.name),
105
+ removed: delta.removed,
106
+ })}${MARKER_SUFFIX}`,
107
+ ];
108
+ if (delta.added.length > 0) {
109
+ parts.push("The following MCP servers have provided instructions for how to use their tools and resources:", "", renderEnvelope(delta.added));
110
+ }
111
+ if (delta.removed.length > 0) {
112
+ parts.push("The following MCP servers have disconnected. Their instructions above no longer apply:", ...delta.removed);
113
+ }
114
+ return parts.join("\n");
115
+ }
116
+ function renderEnvelope(servers) {
117
+ return [
118
+ "<mcp_instructions>",
119
+ ...servers.flatMap((server) => [
120
+ ` <server name="${server.name}">`,
121
+ ...server.instructions.split("\n").map((line) => ` ${line}`),
122
+ " </server>",
123
+ ]),
124
+ "</mcp_instructions>",
125
+ ].join("\n");
126
+ }
@@ -1,6 +1,13 @@
1
1
  import { ChatCompletionFunctionTool } from "openai/resources.js";
2
2
  import type { ToolPlugin, ToolContext } from "../tools/types.js";
3
3
  import type { McpTool, McpServerStatus } from "../types/index.js";
4
+ /**
5
+ * The flattened name an MCP tool is declared and called under.
6
+ *
7
+ * Single source of truth: `McpManager.executeMcpTool` parses names in this
8
+ * shape back into (server, tool), so every producer must agree exactly.
9
+ */
10
+ export declare function mcpToolFlatName(serverName: string, toolName: string): string;
4
11
  /**
5
12
  * Convert MCP tool to OpenAI function tool format
6
13
  */
@@ -36,12 +36,21 @@ function cleanSchema(schema) {
36
36
  }
37
37
  return newSchema;
38
38
  }
39
+ /**
40
+ * The flattened name an MCP tool is declared and called under.
41
+ *
42
+ * Single source of truth: `McpManager.executeMcpTool` parses names in this
43
+ * shape back into (server, tool), so every producer must agree exactly.
44
+ */
45
+ export function mcpToolFlatName(serverName, toolName) {
46
+ return `mcp__${serverName}__${toolName}`;
47
+ }
39
48
  /**
40
49
  * Convert MCP tool to OpenAI function tool format
41
50
  */
42
51
  export function mcpToolToOpenAITool(mcpTool, serverName) {
43
52
  const cleanInputSchema = cleanSchema(mcpTool.inputSchema);
44
- const prefixedName = `mcp__${serverName}__${mcpTool.name}`;
53
+ const prefixedName = mcpToolFlatName(serverName, mcpTool.name);
45
54
  return {
46
55
  type: "function",
47
56
  function: {
@@ -55,7 +64,7 @@ export function mcpToolToOpenAITool(mcpTool, serverName) {
55
64
  * Create a tool plugin wrapper for an MCP tool
56
65
  */
57
66
  export function createMcpToolPlugin(mcpTool, serverName, executeTool) {
58
- const prefixedName = `mcp__${serverName}__${mcpTool.name}`;
67
+ const prefixedName = mcpToolFlatName(serverName, mcpTool.name);
59
68
  return {
60
69
  name: prefixedName,
61
70
  config: mcpToolToOpenAITool(mcpTool, serverName),
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Staleness annotations for auto-memory files.
3
+ *
4
+ * A memory is a point-in-time observation, so its `file:line` citations and
5
+ * symbol names can silently rot as the code moves on. A stale claim that
6
+ * carries a citation reads as *more* authoritative, not less, so the age has to
7
+ * be stated explicitly when the file is read.
8
+ *
9
+ * Aligned with Claude Code's `memdir/memoryAge.ts`.
10
+ */
11
+ /**
12
+ * Days elapsed since mtime, floor-rounded — 0 for today, 1 for yesterday, 2+
13
+ * for older. Negative inputs (future mtime, clock skew) clamp to 0.
14
+ */
15
+ export declare function memoryAgeDays(mtimeMs: number): number;
16
+ /**
17
+ * Plain-text staleness caveat for memories more than a day old, or `""` for
18
+ * fresh ones (warning there is noise). Models are poor at date arithmetic — a
19
+ * raw ISO timestamp does not trigger staleness reasoning the way "47 days ago"
20
+ * does.
21
+ *
22
+ * Only the day count is derived from the clock, so the text is byte-stable
23
+ * within a day: re-reading the same file at the same mtime cannot change the
24
+ * bytes in the conversation history.
25
+ */
26
+ export declare function memoryFreshnessText(mtimeMs: number): string;
27
+ /**
28
+ * Per-memory staleness note wrapped in `<system-reminder>` tags, or `""` for
29
+ * memories up to a day old. For callers that do not add their own wrapper
30
+ * (the Read tool prepends this to the file content it returns).
31
+ */
32
+ export declare function memoryFreshnessNote(mtimeMs: number): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Staleness annotations for auto-memory files.
3
+ *
4
+ * A memory is a point-in-time observation, so its `file:line` citations and
5
+ * symbol names can silently rot as the code moves on. A stale claim that
6
+ * carries a citation reads as *more* authoritative, not less, so the age has to
7
+ * be stated explicitly when the file is read.
8
+ *
9
+ * Aligned with Claude Code's `memdir/memoryAge.ts`.
10
+ */
11
+ /**
12
+ * Days elapsed since mtime, floor-rounded — 0 for today, 1 for yesterday, 2+
13
+ * for older. Negative inputs (future mtime, clock skew) clamp to 0.
14
+ */
15
+ export function memoryAgeDays(mtimeMs) {
16
+ return Math.max(0, Math.floor((Date.now() - mtimeMs) / 86400000));
17
+ }
18
+ /**
19
+ * Plain-text staleness caveat for memories more than a day old, or `""` for
20
+ * fresh ones (warning there is noise). Models are poor at date arithmetic — a
21
+ * raw ISO timestamp does not trigger staleness reasoning the way "47 days ago"
22
+ * does.
23
+ *
24
+ * Only the day count is derived from the clock, so the text is byte-stable
25
+ * within a day: re-reading the same file at the same mtime cannot change the
26
+ * bytes in the conversation history.
27
+ */
28
+ export function memoryFreshnessText(mtimeMs) {
29
+ const days = memoryAgeDays(mtimeMs);
30
+ if (days <= 1)
31
+ return "";
32
+ return (`This memory is ${days} days old. ` +
33
+ `Memories are point-in-time observations, not live state — ` +
34
+ `claims about code behavior or file:line citations may be outdated. ` +
35
+ `Verify against current code before asserting as fact.`);
36
+ }
37
+ /**
38
+ * Per-memory staleness note wrapped in `<system-reminder>` tags, or `""` for
39
+ * memories up to a day old. For callers that do not add their own wrapper
40
+ * (the Read tool prepends this to the file content it returns).
41
+ */
42
+ export function memoryFreshnessNote(mtimeMs) {
43
+ const text = memoryFreshnessText(mtimeMs);
44
+ if (!text)
45
+ return "";
46
+ return `<system-reminder>${text}</system-reminder>\n`;
47
+ }
@@ -0,0 +1,20 @@
1
+ export interface EntrypointTruncation {
2
+ /** Content ready to be injected, including the warning when truncated. */
3
+ content: string;
4
+ lineCount: number;
5
+ charCount: number;
6
+ wasLineTruncated: boolean;
7
+ wasCharTruncated: boolean;
8
+ }
9
+ /**
10
+ * Truncate the auto-memory entrypoint to the line AND character caps, appending
11
+ * a warning that names which cap fired.
12
+ *
13
+ * Line-truncates first (a natural boundary), then character-truncates at the
14
+ * last newline before the cap so a line is never cut in half. The character cap
15
+ * is checked against the *original* size: long lines are the failure mode it
16
+ * targets, so measuring after the line truncation would understate the warning.
17
+ *
18
+ * Aligned with Claude Code's `truncateEntrypointContent()` in `memdir.ts`.
19
+ */
20
+ export declare function truncateEntrypointContent(raw: string): EntrypointTruncation;
@@ -0,0 +1,49 @@
1
+ import { MAX_MEMORY_ENTRYPOINT_CHARS, MAX_MEMORY_ENTRYPOINT_LINES, MEMORY_ENTRYPOINT_NAME, } from "../constants/memory.js";
2
+ /**
3
+ * Truncate the auto-memory entrypoint to the line AND character caps, appending
4
+ * a warning that names which cap fired.
5
+ *
6
+ * Line-truncates first (a natural boundary), then character-truncates at the
7
+ * last newline before the cap so a line is never cut in half. The character cap
8
+ * is checked against the *original* size: long lines are the failure mode it
9
+ * targets, so measuring after the line truncation would understate the warning.
10
+ *
11
+ * Aligned with Claude Code's `truncateEntrypointContent()` in `memdir.ts`.
12
+ */
13
+ export function truncateEntrypointContent(raw) {
14
+ const trimmed = raw.trim();
15
+ const contentLines = trimmed.split("\n");
16
+ const lineCount = contentLines.length;
17
+ const charCount = trimmed.length;
18
+ const wasLineTruncated = lineCount > MAX_MEMORY_ENTRYPOINT_LINES;
19
+ const wasCharTruncated = charCount > MAX_MEMORY_ENTRYPOINT_CHARS;
20
+ if (!wasLineTruncated && !wasCharTruncated) {
21
+ return {
22
+ content: trimmed,
23
+ lineCount,
24
+ charCount,
25
+ wasLineTruncated,
26
+ wasCharTruncated,
27
+ };
28
+ }
29
+ let truncated = wasLineTruncated
30
+ ? contentLines.slice(0, MAX_MEMORY_ENTRYPOINT_LINES).join("\n")
31
+ : trimmed;
32
+ if (truncated.length > MAX_MEMORY_ENTRYPOINT_CHARS) {
33
+ const cutAt = truncated.lastIndexOf("\n", MAX_MEMORY_ENTRYPOINT_CHARS);
34
+ truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_MEMORY_ENTRYPOINT_CHARS);
35
+ }
36
+ const reason = wasCharTruncated && !wasLineTruncated
37
+ ? `${charCount} characters (limit: ${MAX_MEMORY_ENTRYPOINT_CHARS}) — index entries are too long`
38
+ : wasLineTruncated && !wasCharTruncated
39
+ ? `${lineCount} lines (limit: ${MAX_MEMORY_ENTRYPOINT_LINES})`
40
+ : `${lineCount} lines and ${charCount} characters`;
41
+ return {
42
+ content: truncated +
43
+ `\n\n> WARNING: ${MEMORY_ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.`,
44
+ lineCount,
45
+ charCount,
46
+ wasLineTruncated,
47
+ wasCharTruncated,
48
+ };
49
+ }
@@ -0,0 +1,30 @@
1
+ import { type MemoryType } from "../constants/memory.js";
2
+ export interface MemoryHeader {
3
+ filename: string;
4
+ filePath: string;
5
+ mtimeMs: number;
6
+ description: string | null;
7
+ type: MemoryType | undefined;
8
+ }
9
+ /**
10
+ * List the topic files in the auto-memory directory with their frontmatter
11
+ * metadata, newest first.
12
+ *
13
+ * The listing is injected into the extraction fork prompt so the agent can tell
14
+ * whether a memory already exists without spending a turn on `ls`. `MEMORY.md`
15
+ * is excluded — it is the index, not a topic file.
16
+ *
17
+ * Aligned with Claude Code's `memdir/memoryScan.ts`. Two deliberate
18
+ * differences: Wave has a single memory directory (no private/team split), and
19
+ * we read the whole file instead of the first 30 lines — memory topic files are
20
+ * small, and one `readFile` plus one `stat` is simpler than a bounded read that
21
+ * has to return the mtime itself.
22
+ */
23
+ export declare function scanMemoryFiles(memoryDir: string): Promise<MemoryHeader[]>;
24
+ /**
25
+ * Format memory headers as one line per file:
26
+ * `- [type] filename (timestamp): description`. The type tag and the
27
+ * description are each omitted when absent, so a legacy file without
28
+ * frontmatter degrades to `- filename (timestamp)`.
29
+ */
30
+ export declare function formatMemoryManifest(memories: MemoryHeader[]): string;
@@ -0,0 +1,76 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { MEMORY_ENTRYPOINT_NAME, parseMemoryType, } from "../constants/memory.js";
4
+ import { parseFrontmatter } from "./markdownParser.js";
5
+ const MAX_MEMORY_FILES = 200;
6
+ /**
7
+ * List the topic files in the auto-memory directory with their frontmatter
8
+ * metadata, newest first.
9
+ *
10
+ * The listing is injected into the extraction fork prompt so the agent can tell
11
+ * whether a memory already exists without spending a turn on `ls`. `MEMORY.md`
12
+ * is excluded — it is the index, not a topic file.
13
+ *
14
+ * Aligned with Claude Code's `memdir/memoryScan.ts`. Two deliberate
15
+ * differences: Wave has a single memory directory (no private/team split), and
16
+ * we read the whole file instead of the first 30 lines — memory topic files are
17
+ * small, and one `readFile` plus one `stat` is simpler than a bounded read that
18
+ * has to return the mtime itself.
19
+ */
20
+ export async function scanMemoryFiles(memoryDir) {
21
+ let entries;
22
+ try {
23
+ entries = await fs.readdir(memoryDir, { recursive: true });
24
+ }
25
+ catch {
26
+ // The directory is created lazily; a missing one simply has no memories.
27
+ return [];
28
+ }
29
+ const headers = await Promise.all(entries
30
+ .filter((entry) => entry.endsWith(".md") &&
31
+ path.basename(entry) !== MEMORY_ENTRYPOINT_NAME)
32
+ .map(async (filename) => {
33
+ const filePath = path.join(memoryDir, filename);
34
+ try {
35
+ const [content, stats] = await Promise.all([
36
+ fs.readFile(filePath, "utf-8"),
37
+ fs.stat(filePath),
38
+ ]);
39
+ const { frontmatter } = parseFrontmatter(content);
40
+ return {
41
+ filename,
42
+ filePath,
43
+ mtimeMs: stats.mtimeMs,
44
+ description: typeof frontmatter?.description === "string"
45
+ ? frontmatter.description
46
+ : null,
47
+ type: parseMemoryType(frontmatter?.type),
48
+ };
49
+ }
50
+ catch {
51
+ // A single unreadable file must not hide the rest of the directory.
52
+ return null;
53
+ }
54
+ }));
55
+ return headers
56
+ .filter((header) => header !== null)
57
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
58
+ .slice(0, MAX_MEMORY_FILES);
59
+ }
60
+ /**
61
+ * Format memory headers as one line per file:
62
+ * `- [type] filename (timestamp): description`. The type tag and the
63
+ * description are each omitted when absent, so a legacy file without
64
+ * frontmatter degrades to `- filename (timestamp)`.
65
+ */
66
+ export function formatMemoryManifest(memories) {
67
+ return memories
68
+ .map((memory) => {
69
+ const tag = memory.type ? `[${memory.type}] ` : "";
70
+ const timestamp = new Date(memory.mtimeMs).toISOString();
71
+ return memory.description
72
+ ? `- ${tag}${memory.filename} (${timestamp}): ${memory.description}`
73
+ : `- ${tag}${memory.filename} (${timestamp})`;
74
+ })
75
+ .join("\n");
76
+ }
@@ -57,9 +57,13 @@ export interface AddErrorBlockParams {
57
57
  /**
58
58
  * Convert image file path to base64 format
59
59
  * @param imagePath Image file path
60
- * @returns base64 format image data URL
60
+ * @returns base64 format image data URL, or `undefined` when the file cannot be
61
+ * used as an image (unreadable, empty, or an extension we cannot map to a
62
+ * MIME type). Callers must skip the image instead of sending an empty or
63
+ * mislabelled payload — the model gateway rejects both with
64
+ * `HTTP 400 ... You have uploaded an unsupported image`.
61
65
  */
62
- export declare const convertImageToBase64: (imagePath: string) => string;
66
+ export declare const convertImageToBase64: (imagePath: string) => string | undefined;
63
67
  export declare const generateMessageId: () => string;
64
68
  export declare const addUserMessageToMessages: ({ messages, content, customCommandContent, images, source, id, isMeta, }: AddUserMessageParams) => Message[];
65
69
  /**
@@ -5,42 +5,53 @@ import { logger } from "./globalLogger.js";
5
5
  /**
6
6
  * Convert image file path to base64 format
7
7
  * @param imagePath Image file path
8
- * @returns base64 format image data URL
8
+ * @returns base64 format image data URL, or `undefined` when the file cannot be
9
+ * used as an image (unreadable, empty, or an extension we cannot map to a
10
+ * MIME type). Callers must skip the image instead of sending an empty or
11
+ * mislabelled payload — the model gateway rejects both with
12
+ * `HTTP 400 ... You have uploaded an unsupported image`.
9
13
  */
10
14
  export const convertImageToBase64 = (imagePath) => {
15
+ let imageBuffer;
11
16
  try {
12
- const imageBuffer = readFileSync(imagePath);
13
- const ext = extname(imagePath).toLowerCase().substring(1);
14
- // Determine MIME type based on file extension
15
- let mimeType = "image/png"; // Default
16
- switch (ext) {
17
- case "jpg":
18
- case "jpeg":
19
- mimeType = "image/jpeg";
20
- break;
21
- case "png":
22
- mimeType = "image/png";
23
- break;
24
- case "gif":
25
- mimeType = "image/gif";
26
- break;
27
- case "webp":
28
- mimeType = "image/webp";
29
- break;
30
- case "bmp":
31
- mimeType = "image/bmp";
32
- break;
33
- default:
34
- mimeType = "image/png";
35
- }
36
- const base64String = imageBuffer.toString("base64");
37
- return `data:${mimeType};base64,${base64String}`;
17
+ imageBuffer = readFileSync(imagePath);
38
18
  }
39
19
  catch (error) {
40
20
  logger.error(`Failed to convert image to base64: ${imagePath}`, error);
41
- // Return an error placeholder or throw error
42
- return `data:image/png;base64,`; // Empty base64, avoid program crash
21
+ return undefined;
22
+ }
23
+ if (imageBuffer.length === 0) {
24
+ logger.warn(`Skipping empty image file: ${imagePath}`);
25
+ return undefined;
26
+ }
27
+ const ext = extname(imagePath).toLowerCase().substring(1);
28
+ // Determine MIME type based on file extension. Unknown extensions are NOT
29
+ // relabelled as image/png (that is how svg/heic/avif bytes used to reach the
30
+ // model dressed up as PNG).
31
+ let mimeType;
32
+ switch (ext) {
33
+ case "jpg":
34
+ case "jpeg":
35
+ mimeType = "image/jpeg";
36
+ break;
37
+ case "png":
38
+ mimeType = "image/png";
39
+ break;
40
+ case "gif":
41
+ mimeType = "image/gif";
42
+ break;
43
+ case "webp":
44
+ mimeType = "image/webp";
45
+ break;
46
+ case "bmp":
47
+ mimeType = "image/bmp";
48
+ break;
49
+ default:
50
+ logger.warn(`Skipping image with unsupported extension: ${imagePath}`);
51
+ return undefined;
43
52
  }
53
+ const base64String = imageBuffer.toString("base64");
54
+ return `data:${mimeType};base64,${base64String}`;
44
55
  };
45
56
  export const generateMessageId = () => `msg-${randomUUID()}`;
46
57
  // Add user message