wave-agent-sdk 0.16.9 → 0.16.12

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 (85) hide show
  1. package/dist/agent.d.ts +5 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +18 -0
  4. package/dist/constants/toolLimits.d.ts +2 -0
  5. package/dist/constants/toolLimits.d.ts.map +1 -1
  6. package/dist/constants/toolLimits.js +2 -0
  7. package/dist/managers/aiManager.d.ts +5 -0
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +21 -0
  10. package/dist/managers/hookManager.d.ts +6 -3
  11. package/dist/managers/hookManager.d.ts.map +1 -1
  12. package/dist/managers/hookManager.js +36 -13
  13. package/dist/managers/mcpManager.d.ts +4 -28
  14. package/dist/managers/mcpManager.d.ts.map +1 -1
  15. package/dist/managers/mcpManager.js +10 -127
  16. package/dist/services/authService.d.ts +33 -1
  17. package/dist/services/authService.d.ts.map +1 -1
  18. package/dist/services/authService.js +212 -11
  19. package/dist/services/configurationService.d.ts +1 -0
  20. package/dist/services/configurationService.d.ts.map +1 -1
  21. package/dist/services/configurationService.js +48 -6
  22. package/dist/services/hook.d.ts +4 -0
  23. package/dist/services/hook.d.ts.map +1 -1
  24. package/dist/services/hook.js +10 -0
  25. package/dist/services/initializationService.d.ts.map +1 -1
  26. package/dist/services/initializationService.js +11 -0
  27. package/dist/services/interactionService.d.ts.map +1 -1
  28. package/dist/services/interactionService.js +0 -12
  29. package/dist/services/remoteSettingsService.d.ts +21 -0
  30. package/dist/services/remoteSettingsService.d.ts.map +1 -0
  31. package/dist/services/remoteSettingsService.js +280 -0
  32. package/dist/tools/bashTool.d.ts.map +1 -1
  33. package/dist/tools/bashTool.js +58 -32
  34. package/dist/tools/types.d.ts +4 -0
  35. package/dist/tools/types.d.ts.map +1 -1
  36. package/dist/types/agent.d.ts +7 -0
  37. package/dist/types/agent.d.ts.map +1 -1
  38. package/dist/types/auth.d.ts +12 -0
  39. package/dist/types/auth.d.ts.map +1 -1
  40. package/dist/types/configuration.d.ts +20 -0
  41. package/dist/types/configuration.d.ts.map +1 -1
  42. package/dist/types/hooks.d.ts +5 -1
  43. package/dist/types/hooks.d.ts.map +1 -1
  44. package/dist/types/hooks.js +1 -0
  45. package/dist/types/mcp.d.ts +1 -1
  46. package/dist/types/mcp.d.ts.map +1 -1
  47. package/dist/utils/containerSetup.d.ts.map +1 -1
  48. package/dist/utils/containerSetup.js +9 -8
  49. package/dist/utils/gitUtils.d.ts +18 -1
  50. package/dist/utils/gitUtils.d.ts.map +1 -1
  51. package/dist/utils/gitUtils.js +120 -49
  52. package/dist/utils/mcpUtils.d.ts.map +1 -1
  53. package/dist/utils/mcpUtils.js +6 -1
  54. package/dist/utils/openaiClient.d.ts.map +1 -1
  55. package/dist/utils/openaiClient.js +4 -2
  56. package/dist/utils/toolResultStorage.d.ts +46 -0
  57. package/dist/utils/toolResultStorage.d.ts.map +1 -0
  58. package/dist/utils/toolResultStorage.js +90 -0
  59. package/dist/utils/worktreeUtils.d.ts.map +1 -1
  60. package/dist/utils/worktreeUtils.js +58 -0
  61. package/package.json +3 -3
  62. package/src/agent.ts +20 -0
  63. package/src/constants/toolLimits.ts +3 -0
  64. package/src/managers/aiManager.ts +37 -0
  65. package/src/managers/hookManager.ts +42 -17
  66. package/src/managers/mcpManager.ts +10 -178
  67. package/src/services/authService.ts +243 -16
  68. package/src/services/configurationService.ts +58 -6
  69. package/src/services/hook.ts +15 -0
  70. package/src/services/initializationService.ts +13 -0
  71. package/src/services/interactionService.ts +0 -18
  72. package/src/services/remoteSettingsService.ts +315 -0
  73. package/src/tools/bashTool.ts +70 -38
  74. package/src/tools/types.ts +4 -0
  75. package/src/types/agent.ts +7 -0
  76. package/src/types/auth.ts +10 -0
  77. package/src/types/configuration.ts +23 -0
  78. package/src/types/hooks.ts +7 -1
  79. package/src/types/mcp.ts +1 -1
  80. package/src/utils/containerSetup.ts +8 -8
  81. package/src/utils/gitUtils.ts +123 -48
  82. package/src/utils/mcpUtils.ts +12 -1
  83. package/src/utils/openaiClient.ts +5 -2
  84. package/src/utils/toolResultStorage.ts +117 -0
  85. package/src/utils/worktreeUtils.ts +63 -0
@@ -82,69 +82,144 @@ export function getGitMainRepoRoot(cwd: string): string {
82
82
  }
83
83
 
84
84
  /**
85
- * Get the default remote branch (e.g., origin/main)
86
- * @param cwd Working directory
87
- * @returns Default remote branch name
85
+ * Resolve the git directory for a repository by walking up from `cwd`.
86
+ * Handles both normal repos (.git is a directory) and worktrees
87
+ * (.git is a file pointing to the main repo's worktree git dir).
88
+ * For worktrees, reads the `commondir` file to find the common git dir.
89
+ * @param cwd Working directory to start searching from
90
+ * @returns Absolute path to the git directory, or null if not found
88
91
  */
89
- export function getDefaultRemoteBranch(cwd: string): string {
90
- // 1. Try git symbolic-ref refs/remotes/origin/HEAD
91
- try {
92
- const head = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
93
- cwd,
94
- encoding: "utf8",
95
- stdio: ["ignore", "pipe", "ignore"],
96
- }).trim();
97
- return head.replace("refs/remotes/", "");
98
- } catch {
99
- // Ignore error and proceed to next step
92
+ export function resolveGitDir(cwd: string): string | null {
93
+ let currentPath = path.resolve(cwd);
94
+ while (currentPath !== path.dirname(currentPath)) {
95
+ const gitPath = path.join(currentPath, ".git");
96
+ if (fsSync.existsSync(gitPath)) {
97
+ const stat = fsSync.statSync(gitPath);
98
+ if (stat.isDirectory()) {
99
+ // Normal repo: .git is a directory
100
+ return gitPath;
101
+ }
102
+ // Worktree: .git is a file containing "gitdir: <path>"
103
+ try {
104
+ const content = fsSync.readFileSync(gitPath, "utf8").trim();
105
+ const prefix = "gitdir: ";
106
+ if (content.startsWith(prefix)) {
107
+ const worktreeGitDir = content.substring(prefix.length);
108
+ // Read commondir to find the common git dir
109
+ const commondirPath = path.join(worktreeGitDir, "commondir");
110
+ if (fsSync.existsSync(commondirPath)) {
111
+ const commondirRel = fsSync
112
+ .readFileSync(commondirPath, "utf8")
113
+ .trim();
114
+ return path.resolve(worktreeGitDir, commondirRel);
115
+ }
116
+ // Fallback: resolve ../.. from the worktree git dir
117
+ return path.resolve(worktreeGitDir, "..", "..");
118
+ }
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ currentPath = path.dirname(currentPath);
100
124
  }
125
+ return null;
126
+ }
101
127
 
102
- // 2. Check if origin/main exists
128
+ /**
129
+ * Read a symbolic ref file in the git directory.
130
+ * If the file content starts with "ref: ", returns the target ref path.
131
+ * Symbolic refs are never packed in packed-refs, so only loose refs are checked.
132
+ * @param gitDir Absolute path to the git directory
133
+ * @param refPath Relative path to the ref file (e.g., "refs/remotes/origin/HEAD")
134
+ * @returns The symbolic ref target, or null if not a symbolic ref or on error
135
+ */
136
+ function readSymref(gitDir: string, refPath: string): string | null {
103
137
  try {
104
- execSync("git rev-parse --verify origin/main", {
105
- cwd,
106
- stdio: "ignore",
107
- });
108
- return "origin/main";
138
+ const fullPath = path.join(gitDir, refPath);
139
+ const content = fsSync.readFileSync(fullPath, "utf8").trim();
140
+ if (content.startsWith("ref: ")) {
141
+ return content.substring("ref: ".length);
142
+ }
143
+ return null;
109
144
  } catch {
110
- // Ignore error and proceed to next step
145
+ return null;
111
146
  }
147
+ }
112
148
 
113
- // 3. Check if origin/master exists
149
+ /**
150
+ * Check if a git ref exists, checking both loose refs and packed-refs.
151
+ * @param gitDir Absolute path to the git directory
152
+ * @param refPath Relative path to the ref (e.g., "refs/remotes/origin/main")
153
+ * @returns True if the ref exists, false otherwise
154
+ */
155
+ function refExists(gitDir: string, refPath: string): boolean {
156
+ // 1. Check loose ref
157
+ const loosePath = path.join(gitDir, refPath);
158
+ if (fsSync.existsSync(loosePath)) {
159
+ return true;
160
+ }
161
+ // 2. Check packed-refs
114
162
  try {
115
- execSync("git rev-parse --verify origin/master", {
116
- cwd,
117
- stdio: "ignore",
118
- });
119
- return "origin/master";
163
+ const packedRefsPath = path.join(gitDir, "packed-refs");
164
+ const content = fsSync.readFileSync(packedRefsPath, "utf8");
165
+ for (const line of content.split("\n")) {
166
+ // Skip comments and peeled lines
167
+ if (line.startsWith("#") || line.startsWith("^")) {
168
+ continue;
169
+ }
170
+ // Format: <sha> <refPath>
171
+ const parts = line.split(" ");
172
+ if (parts.length >= 2 && parts[1] === refPath) {
173
+ return true;
174
+ }
175
+ }
120
176
  } catch {
121
- // Ignore error and proceed to next step
177
+ // packed-refs doesn't exist or can't be read
122
178
  }
179
+ return false;
180
+ }
123
181
 
124
- // 4. Try to get the current branch's upstream
125
- try {
126
- return execSync("git rev-parse --abbrev-ref --symbolic-full-name @{u}", {
127
- cwd,
128
- encoding: "utf8",
129
- stdio: ["ignore", "pipe", "ignore"],
130
- }).trim();
131
- } catch {
132
- // Ignore error and proceed to next step
182
+ /**
183
+ * Get the default remote branch (e.g., origin/main) using filesystem reads.
184
+ * No subprocess calls matches Claude Code's approach.
185
+ *
186
+ * Priority:
187
+ * 1. Read refs/remotes/origin/HEAD symref → extract branch name (verify it exists)
188
+ * 2. Check if refs/remotes/origin/main ref exists
189
+ * 3. Check if refs/remotes/origin/master ref exists
190
+ * 4. Hardcoded "main" fallback
191
+ *
192
+ * @param cwd Working directory
193
+ * @returns Default remote branch name
194
+ */
195
+ export function getDefaultRemoteBranch(cwd: string): string {
196
+ const gitDir = resolveGitDir(cwd);
197
+ if (!gitDir) {
198
+ return "main";
133
199
  }
134
200
 
135
- // 5. Try to get the current branch name
136
- try {
137
- return execSync("git rev-parse --abbrev-ref HEAD", {
138
- cwd,
139
- encoding: "utf8",
140
- stdio: ["ignore", "pipe", "ignore"],
141
- }).trim();
142
- } catch {
143
- // Ignore error and proceed to next step
201
+ // 1. Try reading origin/HEAD symref
202
+ const symref = readSymref(gitDir, "refs/remotes/origin/HEAD");
203
+ if (symref) {
204
+ const branch = symref.replace("refs/remotes/", "");
205
+ // Verify the resolved branch actually exists (origin/HEAD can be stale)
206
+ if (refExists(gitDir, symref)) {
207
+ return branch;
208
+ }
209
+ }
210
+
211
+ // 2. Check if origin/main exists
212
+ if (refExists(gitDir, "refs/remotes/origin/main")) {
213
+ return "origin/main";
214
+ }
215
+
216
+ // 3. Check if origin/master exists
217
+ if (refExists(gitDir, "refs/remotes/origin/master")) {
218
+ return "origin/master";
144
219
  }
145
220
 
146
- // 6. Fallback to origin/main
147
- return "origin/main";
221
+ // 4. Hardcoded fallback
222
+ return "main";
148
223
  }
149
224
 
150
225
  /**
@@ -1,6 +1,8 @@
1
1
  import { ChatCompletionFunctionTool } from "openai/resources.js";
2
2
  import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
3
3
  import type { McpTool, McpServerStatus } from "../types/index.js";
4
+ import { processToolResult } from "./toolResultStorage.js";
5
+ import { DEFAULT_MAX_RESULT_SIZE_CHARS } from "../constants/toolLimits.js";
4
6
 
5
7
  /**
6
8
  * Recursively clean schema to remove unsupported fields
@@ -97,9 +99,18 @@ export function createMcpToolPlugin(
97
99
  ): Promise<ToolResult> {
98
100
  try {
99
101
  const result = await executeTool(prefixedName, args, context);
102
+
103
+ // Process content for size limits — only text content, not images
104
+ const processedContent = processToolResult(
105
+ result.content || `Executed ${mcpTool.name}`,
106
+ DEFAULT_MAX_RESULT_SIZE_CHARS,
107
+ `mcp_${serverName}_${mcpTool.name}`,
108
+ );
109
+
100
110
  return {
101
111
  success: true,
102
- content: result.content || `Executed ${mcpTool.name}`,
112
+ content: processedContent,
113
+ images: result.images,
103
114
  };
104
115
  } catch (error) {
105
116
  return {
@@ -176,8 +176,11 @@ export class OpenAIClient {
176
176
  error.status = response.status;
177
177
  error.body = errorBody;
178
178
 
179
- if (response.status === 429 && attempt < maxRetries) {
180
- logger.warn("OpenAI API 429 Too Many Requests, retrying...", {
179
+ const retryableStatus =
180
+ response.status === 429 ||
181
+ (response.status >= 500 && response.status !== 501);
182
+ if (retryableStatus && attempt < maxRetries) {
183
+ logger.warn("OpenAI API error, retrying...", {
181
184
  attempt: attempt + 1,
182
185
  status: response.status,
183
186
  });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Shared tool result persistence and truncation logic.
3
+ *
4
+ * When a tool result exceeds a size threshold, the full content is saved to a
5
+ * file in /tmp/wave-tool-results/ and the model receives a <persisted-output>
6
+ * preview with the file path so it can use the Read tool to access the full output.
7
+ */
8
+
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import * as os from "os";
12
+ import {
13
+ DEFAULT_MAX_RESULT_SIZE_CHARS,
14
+ PREVIEW_SIZE_BYTES,
15
+ } from "../constants/toolLimits.js";
16
+ import { logger } from "./globalLogger.js";
17
+
18
+ const TOOL_RESULTS_DIR = path.join(os.tmpdir(), "wave-tool-results");
19
+
20
+ /**
21
+ * Get (and create if needed) the tool-results directory.
22
+ * Uses /tmp/wave-tool-results/ for simplicity and automatic OS cleanup.
23
+ */
24
+ export function getToolResultsDir(): string {
25
+ fs.mkdirSync(TOOL_RESULTS_DIR, { recursive: true });
26
+ return TOOL_RESULTS_DIR;
27
+ }
28
+
29
+ /**
30
+ * Persist full tool output to a file in the tool-results directory.
31
+ * Returns the file path on success, or undefined on failure.
32
+ */
33
+ export function persistToolResult(
34
+ content: string,
35
+ prefix: string = "tool",
36
+ ): string | undefined {
37
+ try {
38
+ const dir = getToolResultsDir();
39
+ const id = `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
40
+ const filePath = path.join(dir, `${id}.txt`);
41
+ fs.writeFileSync(filePath, content, "utf8");
42
+ return filePath;
43
+ } catch (error) {
44
+ logger?.error("Failed to persist tool result:", error);
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Generate a preview from content: first `previewSize` characters with ellipsis.
51
+ */
52
+ export function generatePreview(
53
+ content: string,
54
+ previewSize: number = PREVIEW_SIZE_BYTES,
55
+ ): string {
56
+ if (content.length <= previewSize) return content;
57
+ return content.substring(0, previewSize) + "\n...";
58
+ }
59
+
60
+ /**
61
+ * Build the <persisted-output> wrapper message that the model sees.
62
+ *
63
+ * Example output:
64
+ * <persisted-output>
65
+ * Output too large (150,000 characters). Full output saved to: /tmp/wave-tool-results/mcp_server_tool_12345.txt
66
+ * Preview (first 2,048 characters):
67
+ * {preview content}
68
+ * ...
69
+ * </persisted-output>
70
+ */
71
+ export function buildPersistedOutputMessage(
72
+ totalChars: number,
73
+ filePath: string,
74
+ preview: string,
75
+ ): string {
76
+ return [
77
+ "<persisted-output>",
78
+ `Output too large (${totalChars.toLocaleString()} characters). Full output saved to: ${filePath}`,
79
+ `Preview (first ${PREVIEW_SIZE_BYTES.toLocaleString()} characters):`,
80
+ preview,
81
+ "</persisted-output>",
82
+ ].join("\n");
83
+ }
84
+
85
+ /**
86
+ * Process tool result: if content exceeds maxChars, persist to file and return
87
+ * truncated content with <persisted-output> wrapper. Otherwise return unchanged.
88
+ *
89
+ * This is the main entry point for both MCP and bash tools.
90
+ *
91
+ * @param content - The tool result content
92
+ * @param maxChars - Maximum characters before persistence kicks in (defaults to DEFAULT_MAX_RESULT_SIZE_CHARS)
93
+ * @param prefix - File name prefix for the persisted file (e.g. "bash", "mcp")
94
+ * @returns The content to send to the model (either original or persisted-output wrapper)
95
+ */
96
+ export function processToolResult(
97
+ content: string,
98
+ maxChars: number = DEFAULT_MAX_RESULT_SIZE_CHARS,
99
+ prefix: string = "tool",
100
+ ): string {
101
+ if (content.length <= maxChars) {
102
+ return content;
103
+ }
104
+
105
+ const filePath = persistToolResult(content, prefix);
106
+
107
+ if (filePath) {
108
+ const preview = generatePreview(content);
109
+ return buildPersistedOutputMessage(content.length, filePath, preview);
110
+ }
111
+
112
+ // Fallback: truncation only (persistence failed)
113
+ return (
114
+ content.substring(0, maxChars) +
115
+ "\n\n... (output truncated, failed to persist full output)"
116
+ );
117
+ }
@@ -174,6 +174,69 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
174
174
  );
175
175
  }
176
176
  }
177
+ if (
178
+ stderr.includes("not a valid object name") ||
179
+ stderr.includes("unknown revision")
180
+ ) {
181
+ // Base branch not fetched yet — try fetching then retrying
182
+ const branchNameOnly = baseBranch.split("/").pop()!;
183
+ try {
184
+ execSync(`git fetch origin ${branchNameOnly}`, {
185
+ cwd: repoRoot,
186
+ stdio: ["ignore", "pipe", "pipe"],
187
+ env: {
188
+ ...process.env,
189
+ GIT_TERMINAL_PROMPT: "0",
190
+ GIT_ASKPASS: "",
191
+ },
192
+ });
193
+ execSync(
194
+ `git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`,
195
+ {
196
+ cwd: repoRoot,
197
+ stdio: ["ignore", "pipe", "pipe"],
198
+ env: {
199
+ ...process.env,
200
+ GIT_TERMINAL_PROMPT: "0",
201
+ GIT_ASKPASS: "",
202
+ },
203
+ },
204
+ );
205
+ return {
206
+ name,
207
+ path: worktreePath,
208
+ branch: branchName,
209
+ repoRoot,
210
+ isNew: true,
211
+ originalHeadCommit,
212
+ };
213
+ } catch {
214
+ // Fetch or retry failed — fall back to HEAD
215
+ try {
216
+ execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
217
+ cwd: repoRoot,
218
+ stdio: ["ignore", "pipe", "pipe"],
219
+ env: {
220
+ ...process.env,
221
+ GIT_TERMINAL_PROMPT: "0",
222
+ GIT_ASKPASS: "",
223
+ },
224
+ });
225
+ return {
226
+ name,
227
+ path: worktreePath,
228
+ branch: branchName,
229
+ repoRoot,
230
+ isNew: true,
231
+ originalHeadCommit,
232
+ };
233
+ } catch {
234
+ throw new Error(
235
+ `Failed to create worktree: ${(error as Error).message}\n${stderr}`,
236
+ );
237
+ }
238
+ }
239
+ }
177
240
  throw new Error(
178
241
  `Failed to create worktree: ${(error as Error).message}\n${stderr}`,
179
242
  );