wave-agent-sdk 0.14.4 → 0.15.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/SKILLS.md +31 -6
- package/dist/agent.d.ts +0 -5
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +0 -15
- package/dist/constants/toolLimits.d.ts +10 -0
- package/dist/constants/toolLimits.d.ts.map +1 -0
- package/dist/constants/toolLimits.js +9 -0
- package/dist/managers/aiManager.d.ts +0 -5
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +0 -22
- package/dist/managers/hookManager.d.ts +0 -4
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +0 -25
- package/dist/managers/permissionManager.d.ts +1 -1
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +5 -5
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +3 -4
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +10 -8
- package/dist/services/hook.d.ts +0 -4
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +0 -10
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +4 -1
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +2 -45
- package/dist/tools/editTool.js +1 -1
- package/dist/tools/types.d.ts +0 -3
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +0 -1
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +1 -5
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +0 -1
- package/dist/utils/editUtils.d.ts +5 -2
- package/dist/utils/editUtils.d.ts.map +1 -1
- package/dist/utils/editUtils.js +3 -57
- package/dist/utils/markdownParser.d.ts +8 -1
- package/dist/utils/markdownParser.d.ts.map +1 -1
- package/dist/utils/markdownParser.js +64 -11
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +0 -11
- package/package.json +1 -1
- package/src/agent.ts +0 -17
- package/src/constants/toolLimits.ts +12 -0
- package/src/managers/aiManager.ts +0 -38
- package/src/managers/hookManager.ts +0 -32
- package/src/managers/permissionManager.ts +6 -6
- package/src/prompts/index.ts +3 -5
- package/src/services/aiService.ts +10 -12
- package/src/services/hook.ts +0 -15
- package/src/services/session.ts +6 -1
- package/src/tools/bashTool.ts +2 -51
- package/src/tools/editTool.ts +1 -1
- package/src/tools/types.ts +0 -3
- package/src/types/agent.ts +0 -1
- package/src/types/hooks.ts +1 -7
- package/src/utils/editUtils.ts +3 -73
- package/src/utils/markdownParser.ts +85 -11
- package/src/utils/openaiClient.ts +0 -11
|
@@ -669,7 +669,6 @@ export class HookManager {
|
|
|
669
669
|
event === "Stop" ||
|
|
670
670
|
event === "SubagentStop" ||
|
|
671
671
|
event === "WorktreeCreate" ||
|
|
672
|
-
event === "CwdChanged" ||
|
|
673
672
|
event === "SessionStart" ||
|
|
674
673
|
event === "SessionEnd"
|
|
675
674
|
) {
|
|
@@ -773,7 +772,6 @@ export class HookManager {
|
|
|
773
772
|
SubagentStop: 0,
|
|
774
773
|
PermissionRequest: 0,
|
|
775
774
|
WorktreeCreate: 0,
|
|
776
|
-
CwdChanged: 0,
|
|
777
775
|
SessionStart: 0,
|
|
778
776
|
SessionEnd: 0,
|
|
779
777
|
},
|
|
@@ -788,7 +786,6 @@ export class HookManager {
|
|
|
788
786
|
SubagentStop: 0,
|
|
789
787
|
PermissionRequest: 0,
|
|
790
788
|
WorktreeCreate: 0,
|
|
791
|
-
CwdChanged: 0,
|
|
792
789
|
SessionStart: 0,
|
|
793
790
|
SessionEnd: 0,
|
|
794
791
|
};
|
|
@@ -815,35 +812,6 @@ export class HookManager {
|
|
|
815
812
|
};
|
|
816
813
|
}
|
|
817
814
|
|
|
818
|
-
/**
|
|
819
|
-
* Execute CwdChanged hooks.
|
|
820
|
-
*/
|
|
821
|
-
async executeCwdChangedHooks(
|
|
822
|
-
oldCwd: string,
|
|
823
|
-
newCwd: string,
|
|
824
|
-
sessionId: string,
|
|
825
|
-
transcriptPath: string,
|
|
826
|
-
env: Record<string, string>,
|
|
827
|
-
): Promise<HookExecutionResult[]> {
|
|
828
|
-
const context: ExtendedHookExecutionContext = {
|
|
829
|
-
event: "CwdChanged",
|
|
830
|
-
projectDir: this.workdir,
|
|
831
|
-
timestamp: new Date(),
|
|
832
|
-
sessionId,
|
|
833
|
-
transcriptPath,
|
|
834
|
-
cwd: newCwd,
|
|
835
|
-
oldCwd,
|
|
836
|
-
newCwd,
|
|
837
|
-
env,
|
|
838
|
-
};
|
|
839
|
-
const results = await this.executeHooks("CwdChanged", context);
|
|
840
|
-
if (results.length > 0) {
|
|
841
|
-
// For CwdChanged hooks, we don't block, just log errors
|
|
842
|
-
this.processHookResults("CwdChanged", results);
|
|
843
|
-
}
|
|
844
|
-
return results;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
815
|
/**
|
|
848
816
|
* Register hooks provided by a plugin
|
|
849
817
|
*/
|
|
@@ -131,7 +131,7 @@ export class PermissionManager {
|
|
|
131
131
|
private planFilePath?: string;
|
|
132
132
|
private worktreeName?: string;
|
|
133
133
|
private mainRepoRoot?: string;
|
|
134
|
-
private
|
|
134
|
+
private workdir?: string;
|
|
135
135
|
private onConfiguredPermissionModeChange?: (mode: PermissionMode) => void;
|
|
136
136
|
private _logger?: Logger;
|
|
137
137
|
|
|
@@ -153,7 +153,7 @@ export class PermissionManager {
|
|
|
153
153
|
|
|
154
154
|
this.worktreeName = this.container.get<string>("WorktreeName");
|
|
155
155
|
this.mainRepoRoot = this.container.get<string>("MainRepoRoot");
|
|
156
|
-
this.
|
|
156
|
+
this.workdir = this.container.get<string>("Workdir");
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
@@ -277,7 +277,7 @@ export class PermissionManager {
|
|
|
277
277
|
* Update the additional directories (e.g., when configuration reloads)
|
|
278
278
|
*/
|
|
279
279
|
updateAdditionalDirectories(directories: string[]): void {
|
|
280
|
-
const workdir = this.
|
|
280
|
+
const workdir = this.workdir;
|
|
281
281
|
this.additionalDirectories = directories.map((dir) => {
|
|
282
282
|
if (workdir && !path.isAbsolute(dir)) {
|
|
283
283
|
return path.resolve(workdir, dir);
|
|
@@ -290,7 +290,7 @@ export class PermissionManager {
|
|
|
290
290
|
* Add a system-level additional directory that is persistent across configuration reloads
|
|
291
291
|
*/
|
|
292
292
|
public addSystemAdditionalDirectory(directory: string): void {
|
|
293
|
-
const workdir = this.
|
|
293
|
+
const workdir = this.workdir;
|
|
294
294
|
const resolvedPath =
|
|
295
295
|
workdir && !path.isAbsolute(directory)
|
|
296
296
|
? path.resolve(workdir, directory)
|
|
@@ -329,7 +329,7 @@ export class PermissionManager {
|
|
|
329
329
|
targetPath: string,
|
|
330
330
|
workdir?: string,
|
|
331
331
|
): { isInside: boolean; resolvedPath: string } {
|
|
332
|
-
const effectiveWorkdir = this.
|
|
332
|
+
const effectiveWorkdir = this.workdir || workdir;
|
|
333
333
|
|
|
334
334
|
// Resolve the target path relative to effectiveWorkdir if it's not absolute
|
|
335
335
|
const absolutePath =
|
|
@@ -1068,7 +1068,7 @@ export class PermissionManager {
|
|
|
1068
1068
|
* @param rule - The rule to add (e.g., "Bash(ls)")
|
|
1069
1069
|
*/
|
|
1070
1070
|
public async addPermissionRule(rule: string): Promise<void> {
|
|
1071
|
-
const workdir = this.
|
|
1071
|
+
const workdir = this.workdir;
|
|
1072
1072
|
if (!workdir) {
|
|
1073
1073
|
throw new Error("Working directory not set in PermissionManager");
|
|
1074
1074
|
}
|
package/src/prompts/index.ts
CHANGED
|
@@ -238,7 +238,6 @@ export function buildSystemPrompt(
|
|
|
238
238
|
tools: ToolPlugin[],
|
|
239
239
|
options: {
|
|
240
240
|
workdir?: string;
|
|
241
|
-
originalWorkdir?: string;
|
|
242
241
|
memory?: string;
|
|
243
242
|
language?: string;
|
|
244
243
|
isSubagent?: boolean;
|
|
@@ -276,9 +275,8 @@ export function buildSystemPrompt(
|
|
|
276
275
|
prompt += `\n\n${buildPlanModePrompt(options.planMode.planFilePath, options.planMode.planExists, options.isSubagent)}`;
|
|
277
276
|
}
|
|
278
277
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const isGitRepo = isGitRepository(workdirForPrompt);
|
|
278
|
+
if (options.workdir) {
|
|
279
|
+
const isGitRepo = isGitRepository(options.workdir);
|
|
282
280
|
const platform = os.platform();
|
|
283
281
|
const osVersion = `${os.type()} ${os.release()}`;
|
|
284
282
|
const today = new Date().toISOString().split("T")[0];
|
|
@@ -293,7 +291,7 @@ export function buildSystemPrompt(
|
|
|
293
291
|
|
|
294
292
|
Here is useful information about the environment you are running in:
|
|
295
293
|
<env>
|
|
296
|
-
Working directory: ${
|
|
294
|
+
Working directory: ${options.workdir}
|
|
297
295
|
Is directory a git repo: ${isGitRepo}
|
|
298
296
|
Platform: ${platform}
|
|
299
297
|
Shell: ${shellName}
|
|
@@ -377,10 +377,7 @@ export async function callAgent(
|
|
|
377
377
|
result.content = finalContent;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
if (
|
|
381
|
-
typeof finalReasoningContent === "string" &&
|
|
382
|
-
finalReasoningContent.length > 0
|
|
383
|
-
) {
|
|
380
|
+
if (typeof finalReasoningContent === "string") {
|
|
384
381
|
result.reasoning_content = finalReasoningContent;
|
|
385
382
|
}
|
|
386
383
|
|
|
@@ -544,6 +541,7 @@ async function processStreamingResponse(
|
|
|
544
541
|
): Promise<CallAgentResult> {
|
|
545
542
|
let accumulatedContent = "";
|
|
546
543
|
let accumulatedReasoningContent = "";
|
|
544
|
+
let hasReasoningContent = false;
|
|
547
545
|
const toolCalls: {
|
|
548
546
|
id: string;
|
|
549
547
|
type: "function";
|
|
@@ -618,13 +616,13 @@ async function processStreamingResponse(
|
|
|
618
616
|
}
|
|
619
617
|
}
|
|
620
618
|
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
reasoning_content.length > 0
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
619
|
+
if (typeof reasoning_content === "string") {
|
|
620
|
+
hasReasoningContent = true;
|
|
621
|
+
if (reasoning_content.length > 0) {
|
|
622
|
+
accumulatedReasoningContent += reasoning_content;
|
|
623
|
+
if (onReasoningUpdate) {
|
|
624
|
+
onReasoningUpdate(accumulatedReasoningContent);
|
|
625
|
+
}
|
|
628
626
|
}
|
|
629
627
|
}
|
|
630
628
|
|
|
@@ -716,7 +714,7 @@ async function processStreamingResponse(
|
|
|
716
714
|
result.content = accumulatedContent.trim();
|
|
717
715
|
}
|
|
718
716
|
|
|
719
|
-
if (
|
|
717
|
+
if (hasReasoningContent) {
|
|
720
718
|
result.reasoning_content = accumulatedReasoningContent.trim();
|
|
721
719
|
}
|
|
722
720
|
|
package/src/services/hook.ts
CHANGED
|
@@ -277,21 +277,6 @@ export async function executeCommands(
|
|
|
277
277
|
return results;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
/**
|
|
281
|
-
* Execute a CwdChanged hook
|
|
282
|
-
*/
|
|
283
|
-
export async function executeCwdChangedHooks(
|
|
284
|
-
oldCwd: string,
|
|
285
|
-
newCwd: string,
|
|
286
|
-
context: ExtendedHookExecutionContext,
|
|
287
|
-
): Promise<HookExecutionResult[]> {
|
|
288
|
-
// CwdChanged hooks are executed through HookManager.executeCwdChangedHooks()
|
|
289
|
-
void context;
|
|
290
|
-
void oldCwd;
|
|
291
|
-
void newCwd;
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
280
|
/**
|
|
296
281
|
* Validate command safety (basic checks)
|
|
297
282
|
*/
|
package/src/services/session.ts
CHANGED
|
@@ -953,7 +953,12 @@ export async function handleSessionRestoration(
|
|
|
953
953
|
// Use only JSONL format - no legacy support
|
|
954
954
|
sessionToRestore = await loadSessionFromJsonl(restoreSessionId, workdir);
|
|
955
955
|
if (!sessionToRestore) {
|
|
956
|
-
|
|
956
|
+
// Session doesn't exist on disk (e.g. new project with no messages saved yet).
|
|
957
|
+
// Gracefully fall back to starting fresh instead of throwing.
|
|
958
|
+
logger?.warn(
|
|
959
|
+
`Session ${restoreSessionId} not found on disk, starting fresh session`,
|
|
960
|
+
);
|
|
961
|
+
return;
|
|
957
962
|
}
|
|
958
963
|
} else if (continueLastSession) {
|
|
959
964
|
// Use only JSONL format - no legacy support
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -240,14 +240,7 @@ Try to maintain your current working directory throughout the session by using a
|
|
|
240
240
|
|
|
241
241
|
// Foreground execution (original behavior)
|
|
242
242
|
return new Promise((resolve) => {
|
|
243
|
-
|
|
244
|
-
const tempCwdFile = path.join(
|
|
245
|
-
os.tmpdir(),
|
|
246
|
-
`wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`,
|
|
247
|
-
);
|
|
248
|
-
const wrappedCommand = `${command} && pwd -P >| ${tempCwdFile}`;
|
|
249
|
-
|
|
250
|
-
const child: ChildProcess = spawn(wrappedCommand, {
|
|
243
|
+
const child: ChildProcess = spawn(command, {
|
|
251
244
|
shell: true,
|
|
252
245
|
stdio: "pipe",
|
|
253
246
|
cwd: context.workdir,
|
|
@@ -431,55 +424,13 @@ Try to maintain your current working directory throughout the session by using a
|
|
|
431
424
|
clearTimeout(timeoutHandle);
|
|
432
425
|
}
|
|
433
426
|
|
|
434
|
-
// Read the new CWD from the temporary file
|
|
435
|
-
let newCwd: string | undefined;
|
|
436
|
-
try {
|
|
437
|
-
if (fs.existsSync(tempCwdFile)) {
|
|
438
|
-
newCwd = fs.readFileSync(tempCwdFile, "utf8").trim();
|
|
439
|
-
// Validate the path exists before calling the callback
|
|
440
|
-
fs.accessSync(newCwd, fs.constants.F_OK);
|
|
441
|
-
}
|
|
442
|
-
} catch (fileError) {
|
|
443
|
-
logger.warn(
|
|
444
|
-
`Could not read or validate new CWD from temp file ${tempCwdFile}:`,
|
|
445
|
-
fileError,
|
|
446
|
-
);
|
|
447
|
-
newCwd = undefined;
|
|
448
|
-
} finally {
|
|
449
|
-
// Ensure temp file is cleaned up even if reading fails
|
|
450
|
-
try {
|
|
451
|
-
if (fs.existsSync(tempCwdFile)) {
|
|
452
|
-
fs.unlinkSync(tempCwdFile);
|
|
453
|
-
}
|
|
454
|
-
} catch (fileError) {
|
|
455
|
-
logger.error("Failed to clean up temp CWD file:", fileError);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// If CWD changed, call the onCwdChange callback and add notification
|
|
460
|
-
let cwdChangedNotification = "";
|
|
461
|
-
if (newCwd && newCwd !== context.workdir && context.onCwdChange) {
|
|
462
|
-
const isInSafeZone =
|
|
463
|
-
context.permissionManager?.isPathInSafeZone?.(newCwd) ?? true;
|
|
464
|
-
|
|
465
|
-
if (isInSafeZone) {
|
|
466
|
-
context.onCwdChange(newCwd);
|
|
467
|
-
} else if (context.originalWorkdir) {
|
|
468
|
-
context.onCwdChange(context.originalWorkdir);
|
|
469
|
-
cwdChangedNotification = `Shell cwd was reset to ${context.originalWorkdir}\n`;
|
|
470
|
-
} else {
|
|
471
|
-
context.onCwdChange(newCwd);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
427
|
const exitCode = code ?? 0;
|
|
476
428
|
const combinedOutput =
|
|
477
429
|
outputBuffer + (errorBuffer ? "\n" + errorBuffer : "");
|
|
478
430
|
|
|
479
431
|
// Handle large output by truncation and persistence if needed
|
|
480
432
|
const finalOutput =
|
|
481
|
-
|
|
482
|
-
(combinedOutput || `Command executed with exit code: ${exitCode}`);
|
|
433
|
+
combinedOutput || `Command executed with exit code: ${exitCode}`;
|
|
483
434
|
const content = processOutput(finalOutput);
|
|
484
435
|
|
|
485
436
|
const lines = combinedOutput.trim().split("\n");
|
package/src/tools/editTool.ts
CHANGED
package/src/tools/types.ts
CHANGED
|
@@ -58,7 +58,6 @@ export interface ToolContext {
|
|
|
58
58
|
abortSignal?: AbortSignal;
|
|
59
59
|
backgroundTaskManager?: import("../managers/backgroundTaskManager.js").BackgroundTaskManager;
|
|
60
60
|
workdir: string;
|
|
61
|
-
originalWorkdir?: string;
|
|
62
61
|
/** Permission mode for this tool execution */
|
|
63
62
|
permissionMode?: PermissionMode;
|
|
64
63
|
/** Custom permission callback */
|
|
@@ -104,6 +103,4 @@ export interface ToolContext {
|
|
|
104
103
|
};
|
|
105
104
|
/** State of files read in the current session for deduplication */
|
|
106
105
|
readFileState?: Map<string, { mtime: number; hash: string }>;
|
|
107
|
-
/** Callback to notify when the current working directory changes */
|
|
108
|
-
onCwdChange?: (newCwd: string) => void;
|
|
109
106
|
}
|
package/src/types/agent.ts
CHANGED
|
@@ -98,6 +98,5 @@ export interface AgentCallbacks
|
|
|
98
98
|
onConfiguredModelsChange?: (models: string[]) => void;
|
|
99
99
|
onLoadingChange?: (loading: boolean) => void;
|
|
100
100
|
onCommandRunningChange?: (running: boolean) => void;
|
|
101
|
-
onWorkdirChange?: (newCwd: string) => void;
|
|
102
101
|
onQueuedMessagesChange?: (messages: QueuedMessage[]) => void;
|
|
103
102
|
}
|
package/src/types/hooks.ts
CHANGED
|
@@ -21,7 +21,6 @@ export type HookEvent =
|
|
|
21
21
|
| "SubagentStop"
|
|
22
22
|
| "PermissionRequest"
|
|
23
23
|
| "WorktreeCreate"
|
|
24
|
-
| "CwdChanged"
|
|
25
24
|
| "SessionStart"
|
|
26
25
|
| "SessionEnd";
|
|
27
26
|
|
|
@@ -110,7 +109,6 @@ export function isValidHookEvent(event: string): event is HookEvent {
|
|
|
110
109
|
"SubagentStop",
|
|
111
110
|
"PermissionRequest",
|
|
112
111
|
"WorktreeCreate",
|
|
113
|
-
"CwdChanged",
|
|
114
112
|
"SessionStart",
|
|
115
113
|
"SessionEnd",
|
|
116
114
|
].includes(event);
|
|
@@ -169,7 +167,7 @@ export interface HookJsonInput {
|
|
|
169
167
|
session_id: string; // Format: "wave_session_{uuid}_{shortId}"
|
|
170
168
|
transcript_path: string; // Format: "~/.wave/sessions/session_{shortId}.json"
|
|
171
169
|
cwd: string; // Absolute path to current working directory
|
|
172
|
-
hook_event_name: HookEvent; // "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop" | "SubagentStop" | "PermissionRequest" | "WorktreeCreate" | "
|
|
170
|
+
hook_event_name: HookEvent; // "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop" | "SubagentStop" | "PermissionRequest" | "WorktreeCreate" | "SessionStart"
|
|
173
171
|
|
|
174
172
|
// Optional fields based on event type
|
|
175
173
|
tool_name?: string; // Present for PreToolUse, PostToolUse, PermissionRequest
|
|
@@ -178,8 +176,6 @@ export interface HookJsonInput {
|
|
|
178
176
|
user_prompt?: string; // Present for UserPromptSubmit only
|
|
179
177
|
subagent_type?: string; // Present when hook is executed by a subagent
|
|
180
178
|
name?: string; // Present for WorktreeCreate events
|
|
181
|
-
old_cwd?: string; // Present for CwdChanged events
|
|
182
|
-
new_cwd?: string; // Present for CwdChanged events
|
|
183
179
|
source?: SessionStartSource; // Present for SessionStart events
|
|
184
180
|
agent_type?: string; // Present for SessionStart events
|
|
185
181
|
end_source?: SessionEndSource; // Present for SessionEnd events
|
|
@@ -196,8 +192,6 @@ export interface ExtendedHookExecutionContext extends HookExecutionContext {
|
|
|
196
192
|
userPrompt?: string; // User prompt text (UserPromptSubmit only)
|
|
197
193
|
subagentType?: string; // Subagent type when hook is executed by a subagent
|
|
198
194
|
worktreeName?: string; // Worktree name (WorktreeCreate only)
|
|
199
|
-
oldCwd?: string; // Previous working directory (CwdChanged only)
|
|
200
|
-
newCwd?: string; // New working directory (CwdChanged only)
|
|
201
195
|
source?: SessionStartSource; // Session start source (SessionStart only)
|
|
202
196
|
agentType?: string; // Agent type identifier (SessionStart only)
|
|
203
197
|
endSource?: SessionEndSource; // Session end source (SessionEnd only)
|
package/src/utils/editUtils.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for file editing tools
|
|
3
3
|
*/
|
|
4
|
-
import { formatLineNumberPrefix } from "./stringUtils.js";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Escape regular expression special characters
|
|
@@ -11,77 +10,8 @@ export function escapeRegExp(string: string): string {
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
13
|
+
* Returns a generic error message when old_string is not found.
|
|
15
14
|
*/
|
|
16
|
-
export function analyzeEditMismatch(
|
|
17
|
-
|
|
18
|
-
searchString: string,
|
|
19
|
-
): string {
|
|
20
|
-
const contentLines = content.split("\n");
|
|
21
|
-
const searchLines = searchString.split("\n");
|
|
22
|
-
|
|
23
|
-
if (searchLines.length === 0 || contentLines.length === 0) {
|
|
24
|
-
return "old_string not found in file (empty search or content)";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let bestMatchIndex = -1;
|
|
28
|
-
let bestMatchScore = -1;
|
|
29
|
-
|
|
30
|
-
// Sliding window to find the best partial match
|
|
31
|
-
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
32
|
-
let currentScore = 0;
|
|
33
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
34
|
-
if (contentLines[i + j] === searchLines[j]) {
|
|
35
|
-
currentScore++;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Heuristic: prioritize matches where first or last lines match
|
|
40
|
-
if (contentLines[i] === searchLines[0]) currentScore += 0.5;
|
|
41
|
-
if (
|
|
42
|
-
contentLines[i + searchLines.length - 1] ===
|
|
43
|
-
searchLines[searchLines.length - 1]
|
|
44
|
-
)
|
|
45
|
-
currentScore += 0.5;
|
|
46
|
-
|
|
47
|
-
// Also consider trimmed matches to catch indentation issues
|
|
48
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
49
|
-
if (
|
|
50
|
-
contentLines[i + j].trim() === searchLines[j].trim() &&
|
|
51
|
-
contentLines[i + j] !== searchLines[j]
|
|
52
|
-
) {
|
|
53
|
-
currentScore += 0.1;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (currentScore > bestMatchScore) {
|
|
58
|
-
bestMatchScore = currentScore;
|
|
59
|
-
bestMatchIndex = i;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// If no decent match found (score <= 0), return generic message
|
|
64
|
-
if (bestMatchScore <= 0) {
|
|
65
|
-
return "old_string not found in file (no similar block found)";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Generate detailed report
|
|
69
|
-
const reportLines: string[] = [
|
|
70
|
-
`old_string not found in file. Best partial match found at line ${bestMatchIndex + 1}:`,
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
74
|
-
const lineNum = bestMatchIndex + j + 1;
|
|
75
|
-
const actualLine = contentLines[bestMatchIndex + j];
|
|
76
|
-
const expectedLine = searchLines[j];
|
|
77
|
-
|
|
78
|
-
if (actualLine === expectedLine) {
|
|
79
|
-
reportLines.push(`${formatLineNumberPrefix(lineNum)}${actualLine}`);
|
|
80
|
-
} else {
|
|
81
|
-
reportLines.push(`${formatLineNumberPrefix(lineNum)}- ${expectedLine}`);
|
|
82
|
-
reportLines.push(`${formatLineNumberPrefix(lineNum)}+ ${actualLine}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return reportLines.join("\n");
|
|
15
|
+
export function analyzeEditMismatch(): string {
|
|
16
|
+
return "old_string not found in file";
|
|
87
17
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { readFileSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
2
|
import { exec } from "child_process";
|
|
3
3
|
import { promisify } from "util";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
4
6
|
import type { CustomSlashCommandConfig } from "../types/index.js";
|
|
7
|
+
import {
|
|
8
|
+
SKILL_BASH_MAX_OUTPUT_CHARS,
|
|
9
|
+
PREVIEW_SIZE_BYTES,
|
|
10
|
+
} from "../constants/toolLimits.js";
|
|
5
11
|
|
|
6
12
|
const execAsync = promisify(exec);
|
|
7
13
|
|
|
@@ -138,17 +144,52 @@ export interface BashCommandResult {
|
|
|
138
144
|
exitCode: number;
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Block syntax pattern: ```! command ```
|
|
149
|
+
*/
|
|
150
|
+
const BLOCK_BASH_REGEX = /```!\s*\n?([\s\S]*?)\n?```/g;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Inline syntax pattern: !`command`
|
|
154
|
+
*/
|
|
155
|
+
const INLINE_BASH_REGEX = /!`([^`]+)`/g;
|
|
156
|
+
|
|
141
157
|
export function parseBashCommands(content: string): {
|
|
142
158
|
commands: string[];
|
|
143
159
|
processedContent: string;
|
|
144
160
|
} {
|
|
145
|
-
|
|
161
|
+
// Performance gate: skip expensive regex if no bash pattern exists
|
|
162
|
+
// Covers the common case where 93% of skills have no bash substitution
|
|
163
|
+
if (!content.includes("!`") && !content.includes("```!")) {
|
|
164
|
+
return { commands: [], processedContent: content };
|
|
165
|
+
}
|
|
166
|
+
|
|
146
167
|
const commands: string[] = [];
|
|
147
|
-
let match;
|
|
148
168
|
|
|
149
|
-
// Extract
|
|
150
|
-
|
|
151
|
-
|
|
169
|
+
// Extract block commands
|
|
170
|
+
let blockMatch;
|
|
171
|
+
const blockRegex = new RegExp(
|
|
172
|
+
BLOCK_BASH_REGEX.source,
|
|
173
|
+
BLOCK_BASH_REGEX.flags,
|
|
174
|
+
);
|
|
175
|
+
while ((blockMatch = blockRegex.exec(content)) !== null) {
|
|
176
|
+
const cmd = blockMatch[1].trim();
|
|
177
|
+
if (cmd) {
|
|
178
|
+
commands.push(cmd);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Extract inline commands
|
|
183
|
+
let inlineMatch;
|
|
184
|
+
const inlineRegex = new RegExp(
|
|
185
|
+
INLINE_BASH_REGEX.source,
|
|
186
|
+
INLINE_BASH_REGEX.flags,
|
|
187
|
+
);
|
|
188
|
+
while ((inlineMatch = inlineRegex.exec(content)) !== null) {
|
|
189
|
+
const cmd = inlineMatch[1].trim();
|
|
190
|
+
if (cmd) {
|
|
191
|
+
commands.push(cmd);
|
|
192
|
+
}
|
|
152
193
|
}
|
|
153
194
|
|
|
154
195
|
// For now, return the content as-is. The actual command execution
|
|
@@ -160,22 +201,55 @@ export function parseBashCommands(content: string): {
|
|
|
160
201
|
}
|
|
161
202
|
|
|
162
203
|
/**
|
|
163
|
-
*
|
|
204
|
+
* Truncate output if it exceeds the size limit.
|
|
205
|
+
* Writes to a temp file and returns a preview + file path if truncated.
|
|
206
|
+
*/
|
|
207
|
+
export function truncateOutput(output: string): string {
|
|
208
|
+
if (output.length <= SKILL_BASH_MAX_OUTPUT_CHARS) {
|
|
209
|
+
return output;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const preview = output.slice(0, PREVIEW_SIZE_BYTES);
|
|
213
|
+
const tempDir = join(tmpdir(), "wave-skill-bash");
|
|
214
|
+
mkdirSync(tempDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
const tempFile = join(
|
|
217
|
+
tempDir,
|
|
218
|
+
`output-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`,
|
|
219
|
+
);
|
|
220
|
+
writeFileSync(tempFile, output, "utf-8");
|
|
221
|
+
|
|
222
|
+
return `${preview}\n\n[Output truncated (${output.length} chars). Full output saved to: ${tempFile}]`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Replace bash command placeholders with their outputs.
|
|
227
|
+
* Uses function replacer to avoid $$, $&, $' corruption in shell output.
|
|
228
|
+
* Handles both inline (!`cmd`) and block (```! cmd ```) syntax.
|
|
164
229
|
*/
|
|
165
230
|
export function replaceBashCommandsWithOutput(
|
|
166
231
|
content: string,
|
|
167
232
|
results: BashCommandResult[],
|
|
168
233
|
): string {
|
|
169
|
-
const bashCommandRegex = /!`([^`]+)`/g;
|
|
170
234
|
let processedContent = content;
|
|
171
235
|
let commandIndex = 0;
|
|
172
236
|
|
|
173
|
-
|
|
237
|
+
// Replace block syntax first: ```! command ```
|
|
238
|
+
processedContent = processedContent.replace(BLOCK_BASH_REGEX, () => {
|
|
239
|
+
if (commandIndex < results.length) {
|
|
240
|
+
const result = results[commandIndex++];
|
|
241
|
+
return truncateOutput(result.output);
|
|
242
|
+
}
|
|
243
|
+
return "";
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Replace inline syntax: !`command`
|
|
247
|
+
processedContent = processedContent.replace(INLINE_BASH_REGEX, () => {
|
|
174
248
|
if (commandIndex < results.length) {
|
|
175
249
|
const result = results[commandIndex++];
|
|
176
|
-
return result.output;
|
|
250
|
+
return truncateOutput(result.output);
|
|
177
251
|
}
|
|
178
|
-
return
|
|
252
|
+
return "";
|
|
179
253
|
});
|
|
180
254
|
|
|
181
255
|
return processedContent;
|
|
@@ -177,28 +177,17 @@ export class OpenAIClient {
|
|
|
177
177
|
error.body = errorBody;
|
|
178
178
|
|
|
179
179
|
if (response.status === 429 && attempt < maxRetries) {
|
|
180
|
-
const responseHeaders: Record<string, string> = {};
|
|
181
|
-
response.headers.forEach((value, key) => {
|
|
182
|
-
responseHeaders[key] = value;
|
|
183
|
-
});
|
|
184
180
|
logger.warn("OpenAI API 429 Too Many Requests, retrying...", {
|
|
185
181
|
attempt: attempt + 1,
|
|
186
182
|
status: response.status,
|
|
187
|
-
responseHeaders,
|
|
188
183
|
});
|
|
189
184
|
lastError = error;
|
|
190
185
|
continue;
|
|
191
186
|
}
|
|
192
187
|
|
|
193
|
-
const responseHeaders: Record<string, string> = {};
|
|
194
|
-
response.headers.forEach((value, key) => {
|
|
195
|
-
responseHeaders[key] = value;
|
|
196
|
-
});
|
|
197
188
|
logger.error("OpenAI API Error:", {
|
|
198
189
|
status: response.status,
|
|
199
190
|
statusText: response.statusText,
|
|
200
|
-
requestHeaders: headers,
|
|
201
|
-
responseHeaders,
|
|
202
191
|
errorBody,
|
|
203
192
|
});
|
|
204
193
|
throw error;
|