wave-agent-sdk 0.18.6 → 0.19.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/builtin/skills/settings/ENV.md +0 -1
- package/builtin/skills/settings/HOOKS.md +10 -0
- package/builtin/skills/settings/MODELS.md +37 -0
- package/builtin/skills/settings/SKILLS.md +6 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +31 -8
- package/dist/managers/cronManager.d.ts.map +1 -1
- package/dist/managers/cronManager.js +2 -0
- package/dist/managers/liveConfigManager.d.ts.map +1 -1
- package/dist/managers/liveConfigManager.js +0 -9
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +13 -3
- package/dist/managers/messageManager.d.ts +4 -6
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +27 -81
- package/dist/managers/skillManager.d.ts +1 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +11 -2
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +2 -0
- package/dist/prompts/index.d.ts +3 -2
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +5 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +13 -9
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +24 -9
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +6 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +1 -1
- package/dist/services/interactionService.js +1 -1
- package/dist/services/reversionService.d.ts +5 -1
- package/dist/services/reversionService.d.ts.map +1 -1
- package/dist/services/reversionService.js +50 -22
- package/dist/services/session.d.ts +4 -13
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +59 -215
- package/dist/telemetry/sessionTracing.d.ts +31 -20
- package/dist/telemetry/sessionTracing.d.ts.map +1 -1
- package/dist/telemetry/sessionTracing.js +192 -62
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +16 -1
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +6 -7
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +0 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +16 -1
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/utils/atomicWrite.d.ts +15 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +59 -0
- package/dist/utils/cacheControlUtils.d.ts +3 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +4 -35
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +5 -1
- package/dist/utils/convertMessagesForAPI.d.ts +9 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +74 -55
- package/dist/utils/fileUtils.d.ts +0 -6
- package/dist/utils/fileUtils.d.ts.map +1 -1
- package/dist/utils/fileUtils.js +0 -43
- package/dist/utils/gitUtils.d.ts +11 -0
- package/dist/utils/gitUtils.d.ts.map +1 -1
- package/dist/utils/gitUtils.js +51 -0
- package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -1
- package/dist/utils/groupMessagesByApiRound.js +7 -6
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +2 -1
- package/dist/utils/modelCapabilities.d.ts +22 -0
- package/dist/utils/modelCapabilities.d.ts.map +1 -0
- package/dist/utils/modelCapabilities.js +24 -0
- package/dist/utils/skillParser.d.ts.map +1 -1
- package/dist/utils/skillParser.js +3 -1
- package/dist/utils/tokenEstimate.d.ts +24 -0
- package/dist/utils/tokenEstimate.d.ts.map +1 -0
- package/dist/utils/tokenEstimate.js +30 -0
- package/dist/utils/worktreeUtils.d.ts.map +1 -1
- package/dist/utils/worktreeUtils.js +3 -1
- package/package.json +1 -1
- package/src/agent.ts +11 -5
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +36 -7
- package/src/managers/cronManager.ts +2 -0
- package/src/managers/liveConfigManager.ts +0 -12
- package/src/managers/mcpManager.ts +16 -3
- package/src/managers/messageManager.ts +32 -109
- package/src/managers/skillManager.ts +16 -2
- package/src/managers/toolManager.ts +2 -0
- package/src/prompts/index.ts +6 -1
- package/src/services/aiService.ts +12 -5
- package/src/services/configurationService.ts +26 -24
- package/src/services/hook.ts +7 -0
- package/src/services/initializationService.ts +1 -3
- package/src/services/interactionService.ts +1 -1
- package/src/services/reversionService.ts +50 -27
- package/src/services/session.ts +63 -262
- package/src/telemetry/sessionTracing.ts +225 -73
- package/src/tools/bashTool.ts +23 -1
- package/src/tools/readTool.ts +6 -12
- package/src/types/config.ts +8 -0
- package/src/types/hooks.ts +1 -0
- package/src/types/messaging.ts +0 -1
- package/src/types/skills.ts +1 -0
- package/src/types/telemetry.ts +19 -1
- package/src/utils/atomicWrite.ts +61 -0
- package/src/utils/cacheControlUtils.ts +5 -38
- package/src/utils/containerSetup.ts +5 -1
- package/src/utils/convertMessagesForAPI.ts +87 -57
- package/src/utils/fileUtils.ts +0 -46
- package/src/utils/gitUtils.ts +54 -0
- package/src/utils/groupMessagesByApiRound.ts +7 -6
- package/src/utils/mcpUtils.ts +2 -1
- package/src/utils/modelCapabilities.ts +30 -0
- package/src/utils/skillParser.ts +3 -1
- package/src/utils/tokenEstimate.ts +34 -0
- package/src/utils/worktreeUtils.ts +8 -1
|
@@ -13,6 +13,8 @@ import type {
|
|
|
13
13
|
CompletionUsage,
|
|
14
14
|
} from "openai/resources";
|
|
15
15
|
import { logger } from "./globalLogger.js";
|
|
16
|
+
import { supportsPromptCaching } from "./modelCapabilities.js";
|
|
17
|
+
import type { ModelCapabilities } from "../types/config.js";
|
|
16
18
|
|
|
17
19
|
// ============================================================================
|
|
18
20
|
// Core Types
|
|
@@ -73,41 +75,6 @@ export interface ClaudeUsage extends CompletionUsage {
|
|
|
73
75
|
// Utility Functions (Basic Structure - to be implemented)
|
|
74
76
|
// ============================================================================
|
|
75
77
|
|
|
76
|
-
/**
|
|
77
|
-
* Determines if a model supports prompt caching
|
|
78
|
-
* @param modelName - Model identifier
|
|
79
|
-
* @returns True if model name matches the cache pattern (default: contains 'claude')
|
|
80
|
-
*/
|
|
81
|
-
export function supportsPromptCaching(modelName: string): boolean {
|
|
82
|
-
// Handle null, undefined, and non-string inputs
|
|
83
|
-
if (!modelName || typeof modelName !== "string") {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Handle empty strings and whitespace-only strings
|
|
88
|
-
const trimmed = modelName.trim();
|
|
89
|
-
if (trimmed.length === 0) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const cachePattern = process.env.WAVE_PROMPT_CACHE_REGEX || "claude";
|
|
94
|
-
try {
|
|
95
|
-
const regex = new RegExp(cachePattern, "i");
|
|
96
|
-
return regex.test(trimmed);
|
|
97
|
-
} catch {
|
|
98
|
-
// If regex is invalid, fall back to simple includes check with default
|
|
99
|
-
return trimmed.toLowerCase().includes("claude");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Determines if a model supports cache control
|
|
105
|
-
* @param modelName - Model identifier
|
|
106
|
-
* @returns True if model name contains 'claude' (case-insensitive)
|
|
107
|
-
* @deprecated Use supportsPromptCaching instead
|
|
108
|
-
*/
|
|
109
|
-
export const isClaudeModel = supportsPromptCaching;
|
|
110
|
-
|
|
111
78
|
/**
|
|
112
79
|
* Validates cache control structure
|
|
113
80
|
* @param control - Object to validate
|
|
@@ -254,12 +221,12 @@ export function countContentBlocks(
|
|
|
254
221
|
* Tools are marked separately via addCacheControlToLastTool (called by aiService).
|
|
255
222
|
*
|
|
256
223
|
* @param messages - Original OpenAI message array
|
|
257
|
-
* @param
|
|
224
|
+
* @param capabilities - Declarative model capabilities for cache detection
|
|
258
225
|
* @returns Messages with cache control markers applied
|
|
259
226
|
*/
|
|
260
227
|
export function transformMessagesForExplicitCache(
|
|
261
228
|
messages: ChatCompletionMessageParam[],
|
|
262
|
-
|
|
229
|
+
capabilities?: ModelCapabilities,
|
|
263
230
|
): ChatCompletionMessageParam[] {
|
|
264
231
|
// Validate inputs
|
|
265
232
|
if (!messages || !Array.isArray(messages)) {
|
|
@@ -274,7 +241,7 @@ export function transformMessagesForExplicitCache(
|
|
|
274
241
|
}
|
|
275
242
|
|
|
276
243
|
// Only apply cache control for models that support prompt caching
|
|
277
|
-
if (!supportsPromptCaching(
|
|
244
|
+
if (!supportsPromptCaching(capabilities)) {
|
|
278
245
|
return messages;
|
|
279
246
|
}
|
|
280
247
|
|
|
@@ -205,7 +205,11 @@ export function setupAgentContainer(
|
|
|
205
205
|
|
|
206
206
|
const rootSessionId = messageManager.getRootSessionId();
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
const reversionService = new ReversionService(rootSessionId);
|
|
209
|
+
container.register("ReversionService", reversionService);
|
|
210
|
+
reversionService.cleanupOldSessions(30).catch((error) => {
|
|
211
|
+
logger.error("Failed to cleanup old file history:", error);
|
|
212
|
+
});
|
|
209
213
|
const reversionManager = new ReversionManager(container);
|
|
210
214
|
container.register("ReversionManager", reversionManager);
|
|
211
215
|
|
|
@@ -39,15 +39,26 @@ function safeToolArguments(args: string): string {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Options for converting messages to API format.
|
|
44
|
+
*/
|
|
45
|
+
export interface ConvertMessagesOptions {
|
|
46
|
+
/** When false, images are stripped and replaced with text placeholders. */
|
|
47
|
+
supportsVision?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* Convert message format to API call format, stopping when a compacted message is encountered.
|
|
44
52
|
* Messages with no meaningful content or tool calls are filtered out.
|
|
45
53
|
* @param messages Message list
|
|
54
|
+
* @param options Optional conversion options (e.g. supportsVision)
|
|
46
55
|
* @returns Converted API message format list
|
|
47
56
|
*/
|
|
48
57
|
export function convertMessagesForAPI(
|
|
49
58
|
messages: Message[],
|
|
59
|
+
options?: ConvertMessagesOptions,
|
|
50
60
|
): ChatCompletionMessageParam[] {
|
|
61
|
+
const supportsVision = options?.supportsVision !== false;
|
|
51
62
|
const recentMessages: ChatCompletionMessageParam[] = [];
|
|
52
63
|
|
|
53
64
|
const startIndex = messages.length - 1;
|
|
@@ -86,53 +97,64 @@ export function convertMessagesForAPI(
|
|
|
86
97
|
const completedToolIds = new Set<string>(); // Record completed tool IDs
|
|
87
98
|
|
|
88
99
|
if (toolBlocks.length > 0) {
|
|
100
|
+
// Collect image user messages to place after all tool messages
|
|
101
|
+
const imageUserMessages: ChatCompletionMessageParam[] = [];
|
|
102
|
+
|
|
89
103
|
toolBlocks.forEach((toolBlock) => {
|
|
90
104
|
// Only add completed tool blocks (i.e., stage is 'end')
|
|
91
105
|
if (toolBlock.id && toolBlock.stage === "end") {
|
|
92
106
|
completedToolIds.add(toolBlock.id);
|
|
93
107
|
|
|
94
|
-
//
|
|
108
|
+
// Always add a proper tool message to satisfy tool_call_id pairing
|
|
109
|
+
recentMessages.unshift({
|
|
110
|
+
tool_call_id: toolBlock.id,
|
|
111
|
+
role: "tool",
|
|
112
|
+
content: stripAnsiColors(toolBlock.result || ""),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// If there are images, also prepare a user message with image content
|
|
95
116
|
if (toolBlock.images && toolBlock.images.length > 0) {
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
if (supportsVision) {
|
|
118
|
+
const contentParts: ChatCompletionContentPart[] = [];
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
text: toolResultText,
|
|
104
|
-
});
|
|
120
|
+
toolBlock.images.forEach((image) => {
|
|
121
|
+
const imageUrl = image.data.startsWith("data:")
|
|
122
|
+
? image.data
|
|
123
|
+
: `data:${image.mediaType || "image/png"};base64,${image.data}`;
|
|
105
124
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
type: "image_url",
|
|
114
|
-
image_url: {
|
|
115
|
-
url: imageUrl,
|
|
116
|
-
detail: "auto",
|
|
117
|
-
},
|
|
125
|
+
contentParts.push({
|
|
126
|
+
type: "image_url",
|
|
127
|
+
image_url: {
|
|
128
|
+
url: imageUrl,
|
|
129
|
+
detail: "auto",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
118
132
|
});
|
|
119
|
-
});
|
|
120
133
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
134
|
+
imageUserMessages.push({
|
|
135
|
+
role: "user",
|
|
136
|
+
content: contentParts,
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
// Non-vision model: replace images with a text placeholder
|
|
140
|
+
imageUserMessages.push({
|
|
141
|
+
role: "user",
|
|
142
|
+
content:
|
|
143
|
+
"[Tool returned an image, but the current model does not support image recognition]",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
});
|
|
149
|
+
|
|
150
|
+
// Insert image user messages after all tool messages but before the
|
|
151
|
+
// assistant message (which will be unshifted next). Since tool messages
|
|
152
|
+
// were unshifted to the front, we splice images right after them.
|
|
153
|
+
if (imageUserMessages.length > 0) {
|
|
154
|
+
// Tool messages are at indices 0..N-1, existing messages start at N
|
|
155
|
+
const toolMsgCount = completedToolIds.size;
|
|
156
|
+
recentMessages.splice(toolMsgCount, 0, ...imageUserMessages);
|
|
157
|
+
}
|
|
136
158
|
}
|
|
137
159
|
|
|
138
160
|
// Construct the content of the assistant message
|
|
@@ -227,32 +249,40 @@ export function convertMessagesForAPI(
|
|
|
227
249
|
block.imageUrls &&
|
|
228
250
|
block.imageUrls.length > 0
|
|
229
251
|
) {
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
252
|
+
if (!supportsVision) {
|
|
253
|
+
// Non-vision model: replace images with a text placeholder
|
|
254
|
+
contentParts.push({
|
|
255
|
+
type: "text",
|
|
256
|
+
text: "[User shared an image, but the current model does not support image recognition]",
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
block.imageUrls.forEach((imageUrl: string) => {
|
|
260
|
+
// Check if it's already base64, convert if not
|
|
261
|
+
let finalImageUrl = imageUrl;
|
|
262
|
+
if (!imageUrl.startsWith("data:image/")) {
|
|
263
|
+
// If it's a file path, it needs to be converted to base64
|
|
264
|
+
try {
|
|
265
|
+
finalImageUrl = convertImageToBase64(imageUrl);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
logger.error(
|
|
268
|
+
"Failed to convert image path to base64:",
|
|
269
|
+
imageUrl,
|
|
270
|
+
error,
|
|
271
|
+
);
|
|
272
|
+
// Skip this image, do not add to content
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
245
275
|
}
|
|
246
|
-
}
|
|
247
276
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
contentParts.push({
|
|
278
|
+
type: "image_url",
|
|
279
|
+
image_url: {
|
|
280
|
+
url: finalImageUrl,
|
|
281
|
+
detail: "auto",
|
|
282
|
+
},
|
|
283
|
+
});
|
|
254
284
|
});
|
|
255
|
-
}
|
|
285
|
+
}
|
|
256
286
|
}
|
|
257
287
|
|
|
258
288
|
// If there is a tool block in user message, add its result
|
package/src/utils/fileUtils.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
4
|
import { glob } from "glob";
|
|
7
5
|
|
|
8
6
|
/**
|
|
@@ -156,50 +154,6 @@ export async function getLastLine(
|
|
|
156
154
|
}
|
|
157
155
|
}
|
|
158
156
|
|
|
159
|
-
/**
|
|
160
|
-
* Ensures that a pattern is present in the global git ignore file.
|
|
161
|
-
*
|
|
162
|
-
* @param {string} pattern - The pattern to add to global git ignore.
|
|
163
|
-
*/
|
|
164
|
-
export async function ensureGlobalGitIgnore(pattern: string): Promise<void> {
|
|
165
|
-
try {
|
|
166
|
-
let globalIgnorePath: string;
|
|
167
|
-
try {
|
|
168
|
-
globalIgnorePath = execSync("git config --get core.excludesfile", {
|
|
169
|
-
encoding: "utf8",
|
|
170
|
-
}).trim();
|
|
171
|
-
} catch {
|
|
172
|
-
// If not set, use default paths
|
|
173
|
-
const xdgConfigHome =
|
|
174
|
-
process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config");
|
|
175
|
-
globalIgnorePath = path.join(xdgConfigHome, "git", "ignore");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!globalIgnorePath) return;
|
|
179
|
-
|
|
180
|
-
// Ensure directory exists
|
|
181
|
-
await fs.mkdir(path.dirname(globalIgnorePath), { recursive: true });
|
|
182
|
-
|
|
183
|
-
let content = "";
|
|
184
|
-
try {
|
|
185
|
-
content = await fs.readFile(globalIgnorePath, "utf8");
|
|
186
|
-
} catch {
|
|
187
|
-
// File doesn't exist
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const lines = content.split("\n").map((line) => line.trim());
|
|
191
|
-
if (!lines.includes(pattern)) {
|
|
192
|
-
const newContent =
|
|
193
|
-
content.endsWith("\n") || content === ""
|
|
194
|
-
? `${content}${pattern}\n`
|
|
195
|
-
: `${content}\n${pattern}\n`;
|
|
196
|
-
await fs.writeFile(globalIgnorePath, newContent, "utf8");
|
|
197
|
-
}
|
|
198
|
-
} catch {
|
|
199
|
-
// Ignore errors
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
157
|
/**
|
|
204
158
|
* Simple Levenshtein distance implementation
|
|
205
159
|
*/
|
package/src/utils/gitUtils.ts
CHANGED
|
@@ -222,6 +222,60 @@ export function getDefaultRemoteBranch(cwd: string): string {
|
|
|
222
222
|
return "main";
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Module-level set tracking git dirs already processed in this process,
|
|
227
|
+
* to avoid redundant I/O.
|
|
228
|
+
*/
|
|
229
|
+
const excludedGitDirs = new Set<string>();
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Ensure that Wave runtime files are excluded from git status by writing
|
|
233
|
+
* patterns to `.git/info/exclude` (per-repo, not global).
|
|
234
|
+
*
|
|
235
|
+
* Idempotent: if the marker `# wave-runtime` is already present in the
|
|
236
|
+
* exclude file, it skips writing. A module-level Set provides additional
|
|
237
|
+
* per-process dedup to avoid redundant file reads.
|
|
238
|
+
*
|
|
239
|
+
* @param cwd Working directory to start searching from
|
|
240
|
+
*/
|
|
241
|
+
export function ensureWaveRuntimeFilesExcluded(cwd: string): void {
|
|
242
|
+
try {
|
|
243
|
+
const gitDir = resolveGitDir(cwd);
|
|
244
|
+
if (!gitDir) return;
|
|
245
|
+
|
|
246
|
+
if (excludedGitDirs.has(gitDir)) return;
|
|
247
|
+
|
|
248
|
+
const excludePath = path.join(gitDir, "info", "exclude");
|
|
249
|
+
let content = "";
|
|
250
|
+
try {
|
|
251
|
+
content = fsSync.readFileSync(excludePath, "utf8");
|
|
252
|
+
} catch {
|
|
253
|
+
// File doesn't exist yet
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const marker = "# wave-runtime";
|
|
257
|
+
if (content.includes(marker)) {
|
|
258
|
+
excludedGitDirs.add(gitDir);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const block = [
|
|
263
|
+
marker,
|
|
264
|
+
"**/.wave/scheduled_tasks.lock",
|
|
265
|
+
"**/.wave/scheduled_tasks.json",
|
|
266
|
+
"**/.wave/worktrees/",
|
|
267
|
+
"**/.wave/settings.local.json",
|
|
268
|
+
"",
|
|
269
|
+
].join("\n");
|
|
270
|
+
|
|
271
|
+
fsSync.mkdirSync(path.join(gitDir, "info"), { recursive: true });
|
|
272
|
+
fsSync.appendFileSync(excludePath, block);
|
|
273
|
+
excludedGitDirs.add(gitDir);
|
|
274
|
+
} catch {
|
|
275
|
+
// Best-effort: ignore all errors
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
225
279
|
/**
|
|
226
280
|
* Check if there are uncommitted changes in the working directory
|
|
227
281
|
* @param cwd Working directory
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from "../types/index.js";
|
|
2
|
+
import { estimateTokens as estimateStrTokens } from "./tokenEstimate.js";
|
|
2
3
|
|
|
3
4
|
export interface ApiRound {
|
|
4
5
|
messages: Message[];
|
|
@@ -95,26 +96,26 @@ export function getLastApiRounds(
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
/**
|
|
98
|
-
*
|
|
99
|
+
* Estimate token count from message blocks using CJK-aware estimation.
|
|
99
100
|
*/
|
|
100
101
|
function estimateTokens(messages: Message[]): number {
|
|
101
|
-
let
|
|
102
|
+
let combined = "";
|
|
102
103
|
for (const msg of messages) {
|
|
103
104
|
for (const block of msg.blocks) {
|
|
104
105
|
if ("content" in block && typeof block.content === "string") {
|
|
105
|
-
|
|
106
|
+
combined += block.content;
|
|
106
107
|
}
|
|
107
108
|
if (
|
|
108
109
|
block.type === "tool" &&
|
|
109
110
|
block.parameters &&
|
|
110
111
|
typeof block.parameters === "string"
|
|
111
112
|
) {
|
|
112
|
-
|
|
113
|
+
combined += block.parameters;
|
|
113
114
|
}
|
|
114
115
|
if (block.type === "tool" && block.result) {
|
|
115
|
-
|
|
116
|
+
combined += block.result;
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
|
-
return
|
|
120
|
+
return estimateStrTokens(combined);
|
|
120
121
|
}
|
package/src/utils/mcpUtils.ts
CHANGED
|
@@ -132,6 +132,7 @@ export function findToolServer(
|
|
|
132
132
|
): McpServerStatus | undefined {
|
|
133
133
|
return servers.find(
|
|
134
134
|
(s) =>
|
|
135
|
-
s.status === "connected"
|
|
135
|
+
(s.status === "connected" || s.status === "reconnecting") &&
|
|
136
|
+
s.tools?.some((t) => t.name === toolName),
|
|
136
137
|
);
|
|
137
138
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Capability Detection
|
|
3
|
+
*
|
|
4
|
+
* Pure declarative capability accessors. Capabilities are configured per-model
|
|
5
|
+
* via the `capabilities` field in `ModelConfig` (settings.json), not via regex
|
|
6
|
+
* or environment variables.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ModelCapabilities } from "../types/config.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Determines if a model supports prompt caching.
|
|
13
|
+
* @param capabilities - Declarative model capabilities
|
|
14
|
+
* @returns True if promptCaching is explicitly enabled (default: false)
|
|
15
|
+
*/
|
|
16
|
+
export function supportsPromptCaching(
|
|
17
|
+
capabilities?: ModelCapabilities,
|
|
18
|
+
): boolean {
|
|
19
|
+
return capabilities?.promptCaching ?? false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Determines if a model supports image/vision recognition.
|
|
24
|
+
* Opt-out semantics: by default all models are assumed to support vision.
|
|
25
|
+
* @param capabilities - Declarative model capabilities
|
|
26
|
+
* @returns True unless vision is explicitly disabled (default: true)
|
|
27
|
+
*/
|
|
28
|
+
export function supportsVision(capabilities?: ModelCapabilities): boolean {
|
|
29
|
+
return capabilities?.vision ?? true;
|
|
30
|
+
}
|
package/src/utils/skillParser.ts
CHANGED
|
@@ -57,7 +57,9 @@ export function parseSkillFile(
|
|
|
57
57
|
skillPath.includes("/.wave/skills") ||
|
|
58
58
|
skillPath.includes("\\.wave\\skills") ||
|
|
59
59
|
skillPath.includes("/.claude/skills") ||
|
|
60
|
-
skillPath.includes("\\.claude\\skills")
|
|
60
|
+
skillPath.includes("\\.claude\\skills") ||
|
|
61
|
+
skillPath.includes("/.agents/skills") ||
|
|
62
|
+
skillPath.includes("\\.agents\\skills")
|
|
61
63
|
? "project"
|
|
62
64
|
: "personal";
|
|
63
65
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CJK-aware token estimation without external dependencies.
|
|
3
|
+
*
|
|
4
|
+
* The naive `length / 4` heuristic under-estimates CJK text by ~4x because
|
|
5
|
+
* each Chinese/Japanese/Korean character is typically 1-2 tokens, not 0.25.
|
|
6
|
+
* This function separates CJK characters from other text and applies
|
|
7
|
+
* different ratios:
|
|
8
|
+
*
|
|
9
|
+
* - CJK characters: 1 char ≈ 1 token
|
|
10
|
+
* - Other characters: 4 chars ≈ 1 token (2 for JSON/JSONL/JSONC)
|
|
11
|
+
*
|
|
12
|
+
* The estimate intentionally leans high for CJK (safe direction for limit
|
|
13
|
+
* checks — better to reject than to let oversized content through).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// CJK Unified Ideographs + Extension A + Hiragana + Katakana + Hangul Syllables
|
|
17
|
+
const CJK_REGEX =
|
|
18
|
+
/[\u4e00-\u9fff\u3400-\u4dbf\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Estimate token count for a string, with CJK-awareness.
|
|
22
|
+
*
|
|
23
|
+
* @param content - The text content to estimate
|
|
24
|
+
* @param ext - File extension (without dot), e.g. "ts", "json". JSON/JSONL/JSONC
|
|
25
|
+
* files use a tighter ratio (2 chars/token) for non-CJK text.
|
|
26
|
+
* @returns Estimated token count
|
|
27
|
+
*/
|
|
28
|
+
export function estimateTokens(content: string, ext?: string): number {
|
|
29
|
+
const cjkCount = (content.match(CJK_REGEX) || []).length;
|
|
30
|
+
const otherCount = content.length - cjkCount;
|
|
31
|
+
const bytesPerToken =
|
|
32
|
+
ext === "json" || ext === "jsonl" || ext === "jsonc" ? 2 : 4;
|
|
33
|
+
return Math.ceil(cjkCount + otherCount / bytesPerToken);
|
|
34
|
+
}
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getGitMainRepoRoot,
|
|
11
|
+
getDefaultRemoteBranch,
|
|
12
|
+
ensureWaveRuntimeFilesExcluded,
|
|
13
|
+
} from "./gitUtils.js";
|
|
10
14
|
import { logger } from "./globalLogger.js";
|
|
11
15
|
|
|
12
16
|
export interface WorktreeInfo {
|
|
@@ -105,6 +109,9 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
|
|
|
105
109
|
const branchName = `worktree-${name}`;
|
|
106
110
|
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
107
111
|
|
|
112
|
+
// Ensure Wave runtime files are git-excluded in this repo
|
|
113
|
+
ensureWaveRuntimeFilesExcluded(cwd);
|
|
114
|
+
|
|
108
115
|
// Ensure parent directory exists
|
|
109
116
|
const parentDir = path.dirname(worktreePath);
|
|
110
117
|
if (!fs.existsSync(parentDir)) {
|