wave-agent-sdk 0.18.4 → 0.18.6
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 +1 -1
- package/builtin/skills/settings/MEMORY.md +76 -0
- package/builtin/skills/settings/SKILL.md +4 -4
- package/dist/managers/aiManager.js +80 -82
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +22 -15
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +17 -10
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +5 -4
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/gitUtils.js +6 -6
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +7 -25
- package/dist/utils/path.d.ts +7 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +9 -0
- package/dist/utils/worktreeUtils.d.ts.map +1 -1
- package/dist/utils/worktreeUtils.js +19 -13
- package/package.json +1 -1
- package/scripts/install_ripgrep.js +1 -21
- package/src/managers/aiManager.ts +97 -97
- package/src/managers/mcpManager.ts +26 -15
- package/src/tools/bashTool.ts +18 -9
- package/src/tools/editTool.ts +5 -7
- package/src/utils/constants.ts +1 -1
- package/src/utils/gitUtils.ts +6 -6
- package/src/utils/openaiClient.ts +7 -24
- package/src/utils/path.ts +10 -0
- package/src/utils/worktreeUtils.ts +46 -28
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
- package/builtin/skills/settings/MEMORY_RULES.md +0 -60
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Git worktree creation and removal utilities for the SDK.
|
|
3
3
|
* Used by EnterWorktree and ExitWorktree tools.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import { getGitMainRepoRoot, getDefaultRemoteBranch } from "./gitUtils.js";
|
|
@@ -61,7 +61,7 @@ export function generateWorktreeName() {
|
|
|
61
61
|
* Get the current HEAD commit SHA.
|
|
62
62
|
*/
|
|
63
63
|
export function getHeadCommit(cwd) {
|
|
64
|
-
return
|
|
64
|
+
return execFileSync("git", ["-C", cwd, "rev-parse", "HEAD"], {
|
|
65
65
|
encoding: "utf8",
|
|
66
66
|
stdio: ["ignore", "pipe", "ignore"],
|
|
67
67
|
}).trim();
|
|
@@ -97,7 +97,7 @@ export function createWorktree(name, cwd) {
|
|
|
97
97
|
}
|
|
98
98
|
try {
|
|
99
99
|
// Create worktree and branch
|
|
100
|
-
|
|
100
|
+
execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, baseBranch], {
|
|
101
101
|
cwd: repoRoot,
|
|
102
102
|
stdio: ["ignore", "pipe", "pipe"],
|
|
103
103
|
env: {
|
|
@@ -120,7 +120,7 @@ export function createWorktree(name, cwd) {
|
|
|
120
120
|
if (stderr.includes("already exists")) {
|
|
121
121
|
// Branch exists but worktree doesn't — attach to existing branch
|
|
122
122
|
try {
|
|
123
|
-
|
|
123
|
+
execFileSync("git", ["worktree", "add", worktreePath, branchName], {
|
|
124
124
|
cwd: repoRoot,
|
|
125
125
|
stdio: ["ignore", "pipe", "pipe"],
|
|
126
126
|
env: {
|
|
@@ -147,7 +147,7 @@ export function createWorktree(name, cwd) {
|
|
|
147
147
|
// Base branch not fetched yet — try fetching then retrying
|
|
148
148
|
const branchNameOnly = baseBranch.split("/").pop();
|
|
149
149
|
try {
|
|
150
|
-
|
|
150
|
+
execFileSync("git", ["fetch", "origin", branchNameOnly], {
|
|
151
151
|
cwd: repoRoot,
|
|
152
152
|
stdio: ["ignore", "pipe", "pipe"],
|
|
153
153
|
env: {
|
|
@@ -156,7 +156,7 @@ export function createWorktree(name, cwd) {
|
|
|
156
156
|
GIT_ASKPASS: "",
|
|
157
157
|
},
|
|
158
158
|
});
|
|
159
|
-
|
|
159
|
+
execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, baseBranch], {
|
|
160
160
|
cwd: repoRoot,
|
|
161
161
|
stdio: ["ignore", "pipe", "pipe"],
|
|
162
162
|
env: {
|
|
@@ -177,7 +177,7 @@ export function createWorktree(name, cwd) {
|
|
|
177
177
|
catch {
|
|
178
178
|
// Fetch or retry failed — fall back to HEAD
|
|
179
179
|
try {
|
|
180
|
-
|
|
180
|
+
execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, "HEAD"], {
|
|
181
181
|
cwd: repoRoot,
|
|
182
182
|
stdio: ["ignore", "pipe", "pipe"],
|
|
183
183
|
env: {
|
|
@@ -212,7 +212,7 @@ export function removeWorktree(info) {
|
|
|
212
212
|
// Get current branch in worktree before removing
|
|
213
213
|
let currentBranch;
|
|
214
214
|
try {
|
|
215
|
-
currentBranch =
|
|
215
|
+
currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
216
216
|
cwd: info.path,
|
|
217
217
|
encoding: "utf8",
|
|
218
218
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -222,13 +222,13 @@ export function removeWorktree(info) {
|
|
|
222
222
|
// Ignore errors
|
|
223
223
|
}
|
|
224
224
|
// Remove worktree
|
|
225
|
-
|
|
225
|
+
execFileSync("git", ["worktree", "remove", "--force", info.path], {
|
|
226
226
|
cwd: repoRoot,
|
|
227
227
|
stdio: ["ignore", "pipe", "pipe"],
|
|
228
228
|
});
|
|
229
229
|
// Delete worktree branch
|
|
230
230
|
try {
|
|
231
|
-
|
|
231
|
+
execFileSync("git", ["branch", "-D", info.branch], {
|
|
232
232
|
cwd: repoRoot,
|
|
233
233
|
stdio: ["ignore", "pipe", "pipe"],
|
|
234
234
|
});
|
|
@@ -246,7 +246,7 @@ export function removeWorktree(info) {
|
|
|
246
246
|
currentBranch !== "main" &&
|
|
247
247
|
currentBranch !== "master") {
|
|
248
248
|
try {
|
|
249
|
-
|
|
249
|
+
execFileSync("git", ["branch", "-D", currentBranch], {
|
|
250
250
|
cwd: repoRoot,
|
|
251
251
|
stdio: ["ignore", "pipe", "pipe"],
|
|
252
252
|
});
|
|
@@ -271,7 +271,7 @@ export function removeWorktree(info) {
|
|
|
271
271
|
*/
|
|
272
272
|
export function countWorktreeChanges(worktreePath, originalHeadCommit) {
|
|
273
273
|
try {
|
|
274
|
-
const statusOutput =
|
|
274
|
+
const statusOutput = execFileSync("git", ["-C", worktreePath, "status", "--porcelain"], {
|
|
275
275
|
encoding: "utf8",
|
|
276
276
|
stdio: ["ignore", "pipe", "ignore"],
|
|
277
277
|
});
|
|
@@ -281,7 +281,13 @@ export function countWorktreeChanges(worktreePath, originalHeadCommit) {
|
|
|
281
281
|
if (!originalHeadCommit) {
|
|
282
282
|
return null;
|
|
283
283
|
}
|
|
284
|
-
const revListOutput =
|
|
284
|
+
const revListOutput = execFileSync("git", [
|
|
285
|
+
"-C",
|
|
286
|
+
worktreePath,
|
|
287
|
+
"rev-list",
|
|
288
|
+
"--count",
|
|
289
|
+
`${originalHeadCommit}..HEAD`,
|
|
290
|
+
], {
|
|
285
291
|
encoding: "utf8",
|
|
286
292
|
stdio: ["ignore", "pipe", "ignore"],
|
|
287
293
|
});
|
package/package.json
CHANGED
|
@@ -9,18 +9,6 @@ const __dirname = path.dirname(__filename);
|
|
|
9
9
|
const MANIFEST_PATH = path.resolve(__dirname, "../bin/rg");
|
|
10
10
|
const VENDOR_DIR = path.resolve(__dirname, "../vendor/ripgrep");
|
|
11
11
|
|
|
12
|
-
function getCurrentPlatform() {
|
|
13
|
-
const platform = process.platform;
|
|
14
|
-
const arch = process.arch;
|
|
15
|
-
if (platform === "darwin")
|
|
16
|
-
return `macos-${arch === "arm64" ? "aarch64" : "x86_64"}`;
|
|
17
|
-
if (platform === "linux")
|
|
18
|
-
return `linux-${arch === "arm64" ? "aarch64" : "x86_64"}`;
|
|
19
|
-
if (platform === "win32")
|
|
20
|
-
return `windows-${arch === "arm64" ? "aarch64" : "x86_64"}`;
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
12
|
async function main() {
|
|
25
13
|
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
26
14
|
console.error(`Manifest not found: ${MANIFEST_PATH}`);
|
|
@@ -30,15 +18,7 @@ async function main() {
|
|
|
30
18
|
const manifestContent = fs.readFileSync(MANIFEST_PATH, "utf-8");
|
|
31
19
|
const jsonContent = manifestContent.replace(/^#!.*\n/, "");
|
|
32
20
|
const manifest = JSON.parse(jsonContent);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
// In CI, only download for the current platform
|
|
36
|
-
const isCI = process.env.CI === "true";
|
|
37
|
-
const currentPlatform = isCI ? getCurrentPlatform() : null;
|
|
38
|
-
const platforms =
|
|
39
|
-
currentPlatform && currentPlatform in allPlatforms
|
|
40
|
-
? { [currentPlatform]: allPlatforms[currentPlatform] }
|
|
41
|
-
: allPlatforms;
|
|
21
|
+
const platforms = manifest.platforms;
|
|
42
22
|
|
|
43
23
|
if (!fs.existsSync(VENDOR_DIR)) {
|
|
44
24
|
fs.mkdirSync(VENDOR_DIR, { recursive: true });
|
|
@@ -1271,7 +1271,103 @@ export class AIManager {
|
|
|
1271
1271
|
// Set loading to false first
|
|
1272
1272
|
this.setIsLoading(false);
|
|
1273
1273
|
|
|
1274
|
-
//
|
|
1274
|
+
// Clear temporary rules
|
|
1275
|
+
this.permissionManager?.clearTemporaryRules();
|
|
1276
|
+
|
|
1277
|
+
// Clear abort controllers
|
|
1278
|
+
this.abortController = null;
|
|
1279
|
+
this.toolAbortController = null;
|
|
1280
|
+
|
|
1281
|
+
// Execute Stop/SubagentStop hooks only if the operation was not aborted
|
|
1282
|
+
const isCurrentlyAborted =
|
|
1283
|
+
abortController.signal.aborted || toolAbortController.signal.aborted;
|
|
1284
|
+
|
|
1285
|
+
if (!isCurrentlyAborted) {
|
|
1286
|
+
// Record committed snapshots to message history for the final turn
|
|
1287
|
+
if (this.reversionManager) {
|
|
1288
|
+
const snapshots =
|
|
1289
|
+
this.reversionManager.getAndClearCommittedSnapshots();
|
|
1290
|
+
if (snapshots.length > 0) {
|
|
1291
|
+
this.messageManager.addFileHistoryBlock(snapshots);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Goal evaluation — supersedes Stop hooks when active
|
|
1296
|
+
const goalManager = this.container.has("GoalManager")
|
|
1297
|
+
? this.container.get<import("./goalManager.js").GoalManager>(
|
|
1298
|
+
"GoalManager",
|
|
1299
|
+
)
|
|
1300
|
+
: undefined;
|
|
1301
|
+
|
|
1302
|
+
let goalContinuing = false;
|
|
1303
|
+
|
|
1304
|
+
if (goalManager?.isGoalActive() && !this.subagentType) {
|
|
1305
|
+
// 1. Increment turn count and check circuit breakers
|
|
1306
|
+
goalManager.incrementTurnCount();
|
|
1307
|
+
const circuitBreaker = goalManager.checkCircuitBreakers();
|
|
1308
|
+
|
|
1309
|
+
if (circuitBreaker) {
|
|
1310
|
+
goalManager.clearGoal();
|
|
1311
|
+
logger?.info(`[Goal] ${circuitBreaker}`);
|
|
1312
|
+
this.messageManager.addUserMessage({
|
|
1313
|
+
content: `<system-reminder>${circuitBreaker}</system-reminder>`,
|
|
1314
|
+
isMeta: true,
|
|
1315
|
+
});
|
|
1316
|
+
// Fall through to normal Stop hooks on the final turn
|
|
1317
|
+
} else {
|
|
1318
|
+
// 2. Evaluate goal
|
|
1319
|
+
const evaluation = await goalManager.evaluateGoal(
|
|
1320
|
+
abortController.signal,
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
if (evaluation.isMet) {
|
|
1324
|
+
goalManager.clearGoal();
|
|
1325
|
+
logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
|
|
1326
|
+
this.messageManager.addUserMessage({
|
|
1327
|
+
content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
|
|
1328
|
+
isMeta: true,
|
|
1329
|
+
});
|
|
1330
|
+
// Fall through to normal Stop hooks on the final turn
|
|
1331
|
+
} else {
|
|
1332
|
+
const goal = goalManager.getGoal()!;
|
|
1333
|
+
goal.lastReason = evaluation.reason;
|
|
1334
|
+
logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
|
|
1335
|
+
this.messageManager.addUserMessage({
|
|
1336
|
+
content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
|
|
1337
|
+
isMeta: true,
|
|
1338
|
+
});
|
|
1339
|
+
// Keep loading state active to prevent UI flicker
|
|
1340
|
+
this.setIsLoading(true);
|
|
1341
|
+
goalContinuing = true;
|
|
1342
|
+
// Restart outer loop to continue goal pursuit
|
|
1343
|
+
shouldRestart = true;
|
|
1344
|
+
turnOffset = 0;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Skip Stop hooks when goal evaluator is continuing the conversation
|
|
1350
|
+
if (goalContinuing) {
|
|
1351
|
+
// Goal evaluator supersedes Stop hooks
|
|
1352
|
+
} else {
|
|
1353
|
+
const shouldContinue = await this.executeStopHooks();
|
|
1354
|
+
|
|
1355
|
+
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
1356
|
+
// restart the AI conversation cycle
|
|
1357
|
+
if (shouldContinue) {
|
|
1358
|
+
logger?.info(
|
|
1359
|
+
`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
// Restart the conversation to let AI fix the issues
|
|
1363
|
+
shouldRestart = true;
|
|
1364
|
+
turnOffset = 0;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Inject pending notifications from background tasks (after Stop hooks,
|
|
1370
|
+
// aligned with Claude Code which fires Stop hooks unconditionally)
|
|
1275
1371
|
const notificationQueue = this.container.has("NotificationQueue")
|
|
1276
1372
|
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
1277
1373
|
: undefined;
|
|
@@ -1292,102 +1388,6 @@ export class AIManager {
|
|
|
1292
1388
|
// Restart outer loop to process the notifications
|
|
1293
1389
|
shouldRestart = true;
|
|
1294
1390
|
turnOffset = 0;
|
|
1295
|
-
} else {
|
|
1296
|
-
// Clear temporary rules
|
|
1297
|
-
this.permissionManager?.clearTemporaryRules();
|
|
1298
|
-
|
|
1299
|
-
// Clear abort controllers
|
|
1300
|
-
this.abortController = null;
|
|
1301
|
-
this.toolAbortController = null;
|
|
1302
|
-
|
|
1303
|
-
// Execute Stop/SubagentStop hooks only if the operation was not aborted
|
|
1304
|
-
const isCurrentlyAborted =
|
|
1305
|
-
abortController.signal.aborted ||
|
|
1306
|
-
toolAbortController.signal.aborted;
|
|
1307
|
-
|
|
1308
|
-
if (!isCurrentlyAborted) {
|
|
1309
|
-
// Record committed snapshots to message history for the final turn
|
|
1310
|
-
if (this.reversionManager) {
|
|
1311
|
-
const snapshots =
|
|
1312
|
-
this.reversionManager.getAndClearCommittedSnapshots();
|
|
1313
|
-
if (snapshots.length > 0) {
|
|
1314
|
-
this.messageManager.addFileHistoryBlock(snapshots);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Goal evaluation — supersedes Stop hooks when active
|
|
1319
|
-
const goalManager = this.container.has("GoalManager")
|
|
1320
|
-
? this.container.get<import("./goalManager.js").GoalManager>(
|
|
1321
|
-
"GoalManager",
|
|
1322
|
-
)
|
|
1323
|
-
: undefined;
|
|
1324
|
-
|
|
1325
|
-
let goalContinuing = false;
|
|
1326
|
-
|
|
1327
|
-
if (goalManager?.isGoalActive() && !this.subagentType) {
|
|
1328
|
-
// 1. Increment turn count and check circuit breakers
|
|
1329
|
-
goalManager.incrementTurnCount();
|
|
1330
|
-
const circuitBreaker = goalManager.checkCircuitBreakers();
|
|
1331
|
-
|
|
1332
|
-
if (circuitBreaker) {
|
|
1333
|
-
goalManager.clearGoal();
|
|
1334
|
-
logger?.info(`[Goal] ${circuitBreaker}`);
|
|
1335
|
-
this.messageManager.addUserMessage({
|
|
1336
|
-
content: `<system-reminder>${circuitBreaker}</system-reminder>`,
|
|
1337
|
-
isMeta: true,
|
|
1338
|
-
});
|
|
1339
|
-
// Fall through to normal Stop hooks on the final turn
|
|
1340
|
-
} else {
|
|
1341
|
-
// 2. Evaluate goal
|
|
1342
|
-
const evaluation = await goalManager.evaluateGoal(
|
|
1343
|
-
abortController.signal,
|
|
1344
|
-
);
|
|
1345
|
-
|
|
1346
|
-
if (evaluation.isMet) {
|
|
1347
|
-
goalManager.clearGoal();
|
|
1348
|
-
logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
|
|
1349
|
-
this.messageManager.addUserMessage({
|
|
1350
|
-
content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
|
|
1351
|
-
isMeta: true,
|
|
1352
|
-
});
|
|
1353
|
-
// Fall through to normal Stop hooks on the final turn
|
|
1354
|
-
} else {
|
|
1355
|
-
const goal = goalManager.getGoal()!;
|
|
1356
|
-
goal.lastReason = evaluation.reason;
|
|
1357
|
-
logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
|
|
1358
|
-
this.messageManager.addUserMessage({
|
|
1359
|
-
content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
|
|
1360
|
-
isMeta: true,
|
|
1361
|
-
});
|
|
1362
|
-
// Keep loading state active to prevent UI flicker
|
|
1363
|
-
this.setIsLoading(true);
|
|
1364
|
-
goalContinuing = true;
|
|
1365
|
-
// Restart outer loop to continue goal pursuit
|
|
1366
|
-
shouldRestart = true;
|
|
1367
|
-
turnOffset = 0;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// Skip Stop hooks when goal evaluator is continuing the conversation
|
|
1373
|
-
if (goalContinuing) {
|
|
1374
|
-
// Goal evaluator supersedes Stop hooks
|
|
1375
|
-
} else {
|
|
1376
|
-
const shouldContinue = await this.executeStopHooks();
|
|
1377
|
-
|
|
1378
|
-
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
1379
|
-
// restart the AI conversation cycle
|
|
1380
|
-
if (shouldContinue) {
|
|
1381
|
-
logger?.info(
|
|
1382
|
-
`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
|
|
1383
|
-
);
|
|
1384
|
-
|
|
1385
|
-
// Restart the conversation to let AI fix the issues
|
|
1386
|
-
shouldRestart = true;
|
|
1387
|
-
turnOffset = 0;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
1391
|
}
|
|
1392
1392
|
}
|
|
1393
1393
|
|
|
@@ -484,29 +484,40 @@ export class McpManager {
|
|
|
484
484
|
stderr: "pipe", // Pipe stderr to capture it
|
|
485
485
|
});
|
|
486
486
|
|
|
487
|
-
//
|
|
487
|
+
// Buffer stderr silently (MCP servers use stderr for all logging;
|
|
488
|
+
// stdout is reserved for JSON-RPC). Only dump it on connection
|
|
489
|
+
// success (once) or failure — not line-by-line in real time.
|
|
490
|
+
// Aligns with Claude Code's approach to avoid noise from [INFO],
|
|
491
|
+
// [DEBUG], Warning: lines that are not actual errors.
|
|
488
492
|
const stderr = (transport as StdioClientTransport).stderr;
|
|
493
|
+
let stderrOutput = "";
|
|
489
494
|
if (stderr) {
|
|
490
|
-
let buffer = "";
|
|
491
495
|
stderr.on("data", (chunk: Buffer) => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
buffer = lines.pop() || "";
|
|
495
|
-
for (const line of lines) {
|
|
496
|
-
if (line.trim()) {
|
|
497
|
-
logger?.error(`[MCP Server ${name}] ${line}`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
stderr.on("end", () => {
|
|
502
|
-
if (buffer.trim()) {
|
|
503
|
-
logger?.error(`[MCP Server ${name}] ${buffer}`);
|
|
496
|
+
if (stderrOutput.length < 64 * 1024 * 1024) {
|
|
497
|
+
stderrOutput += chunk.toString();
|
|
504
498
|
}
|
|
505
499
|
});
|
|
506
500
|
}
|
|
507
501
|
|
|
508
502
|
client = createClient();
|
|
509
|
-
|
|
503
|
+
try {
|
|
504
|
+
await client.connect(transport);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (stderrOutput.trim()) {
|
|
507
|
+
logger?.error(
|
|
508
|
+
`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Dump accumulated stderr once after successful connection
|
|
515
|
+
if (stderrOutput.trim()) {
|
|
516
|
+
logger?.debug(
|
|
517
|
+
`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
|
|
518
|
+
);
|
|
519
|
+
stderrOutput = "";
|
|
520
|
+
}
|
|
510
521
|
|
|
511
522
|
const toolsResponse = await client.listTools();
|
|
512
523
|
tools =
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
import { logger } from "../utils/globalLogger.js";
|
|
6
6
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
7
|
+
import { toPosixPath } from "../utils/path.js";
|
|
7
8
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
8
9
|
import { processToolResult } from "../utils/toolResultStorage.js";
|
|
9
10
|
import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
|
|
@@ -246,7 +247,8 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
246
247
|
os.tmpdir(),
|
|
247
248
|
`wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`,
|
|
248
249
|
);
|
|
249
|
-
const
|
|
250
|
+
const tempCwdFileForBash = toPosixPath(tempCwdFile);
|
|
251
|
+
const wrappedCommand = `${command} && pwd -P >| ${tempCwdFileForBash}`;
|
|
250
252
|
|
|
251
253
|
const child: ChildProcess = spawn(wrappedCommand, {
|
|
252
254
|
shell: shellPath || true,
|
|
@@ -264,6 +266,17 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
264
266
|
let isBackgrounded = false;
|
|
265
267
|
let isFinished = false;
|
|
266
268
|
|
|
269
|
+
// Best-effort cleanup of the temp CWD file — used by abort/error/exit paths
|
|
270
|
+
const cleanupTempFile = () => {
|
|
271
|
+
try {
|
|
272
|
+
if (fs.existsSync(tempCwdFile)) {
|
|
273
|
+
fs.unlinkSync(tempCwdFile);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// ignore — best-effort cleanup
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
267
280
|
const updateRealtimeResults = () => {
|
|
268
281
|
if (isAborted || isBackgrounded || isFinished) return;
|
|
269
282
|
|
|
@@ -420,6 +433,8 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
420
433
|
}
|
|
421
434
|
}
|
|
422
435
|
|
|
436
|
+
cleanupTempFile();
|
|
437
|
+
|
|
423
438
|
const processedOutput = processToolResult(
|
|
424
439
|
outputBuffer + (errorBuffer ? "\n" + errorBuffer : ""),
|
|
425
440
|
BASH_MAX_OUTPUT_CHARS,
|
|
@@ -491,14 +506,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
491
506
|
);
|
|
492
507
|
newCwd = undefined;
|
|
493
508
|
} finally {
|
|
494
|
-
|
|
495
|
-
try {
|
|
496
|
-
if (fs.existsSync(tempCwdFile)) {
|
|
497
|
-
fs.unlinkSync(tempCwdFile);
|
|
498
|
-
}
|
|
499
|
-
} catch (fileError) {
|
|
500
|
-
logger.error("Failed to clean up temp CWD file:", fileError);
|
|
501
|
-
}
|
|
509
|
+
cleanupTempFile();
|
|
502
510
|
}
|
|
503
511
|
|
|
504
512
|
// If CWD changed, call the onCwdChange callback and add notification
|
|
@@ -560,6 +568,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
560
568
|
if (timeoutHandle) {
|
|
561
569
|
clearTimeout(timeoutHandle);
|
|
562
570
|
}
|
|
571
|
+
cleanupTempFile();
|
|
563
572
|
resolve({
|
|
564
573
|
success: false,
|
|
565
574
|
content: "",
|
package/src/tools/editTool.ts
CHANGED
|
@@ -112,11 +112,11 @@ Usage:
|
|
|
112
112
|
// Trigger conditional rule loading for this file
|
|
113
113
|
context.messageManager?.triggerFileRead(filePath);
|
|
114
114
|
|
|
115
|
-
// Enforce read-before-edit: the file must have been read first
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
) {
|
|
115
|
+
// Enforce read-before-edit: the file must have been read or written first.
|
|
116
|
+
// readFileState is populated by Read, Write, and Edit tools — single source
|
|
117
|
+
// of truth, aligned with Claude Code's readFileState approach.
|
|
118
|
+
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
119
|
+
if (!context.readFileState?.has(resolvedPath)) {
|
|
120
120
|
return {
|
|
121
121
|
success: false,
|
|
122
122
|
content: "",
|
|
@@ -125,8 +125,6 @@ Usage:
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
try {
|
|
128
|
-
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
129
|
-
|
|
130
128
|
// Read file content
|
|
131
129
|
let originalContent: string;
|
|
132
130
|
try {
|
package/src/utils/constants.ts
CHANGED
|
@@ -30,4 +30,4 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
|
|
|
30
30
|
* AI related constants
|
|
31
31
|
*/
|
|
32
32
|
export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 200000; // Default token limit
|
|
33
|
-
export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS =
|
|
33
|
+
export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 32000; // Default output token limit (aligned with Claude Code)
|
package/src/utils/gitUtils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Check if a directory is a git repository
|
|
@@ -31,7 +31,7 @@ export function isGitRepository(dirPath: string): string {
|
|
|
31
31
|
*/
|
|
32
32
|
export function getGitRepoRoot(cwd: string): string {
|
|
33
33
|
try {
|
|
34
|
-
return
|
|
34
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
35
35
|
cwd,
|
|
36
36
|
encoding: "utf8",
|
|
37
37
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -48,7 +48,7 @@ export function getGitRepoRoot(cwd: string): string {
|
|
|
48
48
|
*/
|
|
49
49
|
export function getGitCommonDir(cwd: string): string {
|
|
50
50
|
try {
|
|
51
|
-
const commonDir =
|
|
51
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
52
52
|
cwd,
|
|
53
53
|
encoding: "utf8",
|
|
54
54
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -66,7 +66,7 @@ export function getGitCommonDir(cwd: string): string {
|
|
|
66
66
|
*/
|
|
67
67
|
export function getGitMainRepoRoot(cwd: string): string {
|
|
68
68
|
try {
|
|
69
|
-
const output =
|
|
69
|
+
const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
70
70
|
cwd,
|
|
71
71
|
encoding: "utf8",
|
|
72
72
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -229,7 +229,7 @@ export function getDefaultRemoteBranch(cwd: string): string {
|
|
|
229
229
|
*/
|
|
230
230
|
export function hasUncommittedChanges(cwd: string): boolean {
|
|
231
231
|
try {
|
|
232
|
-
const status =
|
|
232
|
+
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
233
233
|
cwd,
|
|
234
234
|
encoding: "utf8",
|
|
235
235
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -249,7 +249,7 @@ export function hasUncommittedChanges(cwd: string): boolean {
|
|
|
249
249
|
export function hasNewCommits(cwd: string, baseBranch?: string): boolean {
|
|
250
250
|
try {
|
|
251
251
|
const range = baseBranch ? `${baseBranch}..HEAD` : "@{u}..HEAD";
|
|
252
|
-
const log =
|
|
252
|
+
const log = execFileSync("git", ["log", range, "--oneline"], {
|
|
253
253
|
cwd,
|
|
254
254
|
encoding: "utf8",
|
|
255
255
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -133,6 +133,7 @@ export class OpenAIClient {
|
|
|
133
133
|
}
|
|
134
134
|
if (attempt < MAX_RETRIES) {
|
|
135
135
|
logger.warn("OpenAI API network error, retrying...", {
|
|
136
|
+
model: params.model,
|
|
136
137
|
attempt: attempt + 1,
|
|
137
138
|
error: e,
|
|
138
139
|
});
|
|
@@ -166,37 +167,18 @@ export class OpenAIClient {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
let
|
|
170
|
+
let responseText = "";
|
|
170
171
|
try {
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
errorBody = JSON.parse(text);
|
|
174
|
-
} catch (e) {
|
|
175
|
-
logger.error("Failed to parse error response body as JSON", {
|
|
176
|
-
error: e,
|
|
177
|
-
text,
|
|
178
|
-
});
|
|
179
|
-
errorBody = text;
|
|
180
|
-
}
|
|
172
|
+
responseText = await response.text();
|
|
181
173
|
} catch (e) {
|
|
182
174
|
logger.error("Failed to read error response text", { error: e });
|
|
183
|
-
errorBody = {};
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
const error = new Error(
|
|
187
|
-
|
|
188
|
-
errorBody !== null &&
|
|
189
|
-
"error" in errorBody &&
|
|
190
|
-
typeof (errorBody as { error: unknown }).error === "object" &&
|
|
191
|
-
(errorBody as { error: object }).error !== null &&
|
|
192
|
-
"message" in (errorBody as { error: { message: unknown } }).error
|
|
193
|
-
? String((errorBody as { error: { message: string } }).error.message)
|
|
194
|
-
: typeof errorBody === "string"
|
|
195
|
-
? errorBody
|
|
196
|
-
: response.statusText,
|
|
178
|
+
`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}: ${responseText}`,
|
|
197
179
|
) as Error & { status?: number; body?: unknown };
|
|
198
180
|
error.status = response.status;
|
|
199
|
-
error.body =
|
|
181
|
+
error.body = responseText;
|
|
200
182
|
|
|
201
183
|
const retryableStatus =
|
|
202
184
|
response.status === 429 ||
|
|
@@ -204,6 +186,7 @@ export class OpenAIClient {
|
|
|
204
186
|
if (retryableStatus && attempt < MAX_RETRIES) {
|
|
205
187
|
lastRetryAfter = response.headers.get("retry-after");
|
|
206
188
|
logger.warn("OpenAI API error, retrying...", {
|
|
189
|
+
model: params.model,
|
|
207
190
|
attempt: attempt + 1,
|
|
208
191
|
status: response.status,
|
|
209
192
|
retryAfter: lastRetryAfter,
|
|
@@ -215,7 +198,7 @@ export class OpenAIClient {
|
|
|
215
198
|
logger.error("OpenAI API Error:", {
|
|
216
199
|
status: response.status,
|
|
217
200
|
statusText: response.statusText,
|
|
218
|
-
|
|
201
|
+
body: responseText,
|
|
219
202
|
});
|
|
220
203
|
throw error;
|
|
221
204
|
}
|
package/src/utils/path.ts
CHANGED
|
@@ -54,3 +54,13 @@ export function getDisplayPath(filePath: string, workdir: string): string {
|
|
|
54
54
|
}
|
|
55
55
|
return filePath;
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert backslashes to forward slashes for shell compatibility on Windows.
|
|
60
|
+
* On Windows, os.tmpdir() returns paths with backslashes (e.g., C:\Users\foo\Temp),
|
|
61
|
+
* which are treated as escape characters when interpolated into shell command strings.
|
|
62
|
+
* Returns the path as-is on non-Windows platforms.
|
|
63
|
+
*/
|
|
64
|
+
export function toPosixPath(p: string): string {
|
|
65
|
+
return process.platform === "win32" ? p.replace(/\\/g, "/") : p;
|
|
66
|
+
}
|