wave-agent-sdk 1.0.5 → 1.0.7

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 (202) hide show
  1. package/dist/agent.d.ts +34 -1
  2. package/dist/agent.js +49 -4
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +1 -1
  5. package/dist/managers/aiManager.d.ts +36 -0
  6. package/dist/managers/aiManager.js +240 -11
  7. package/dist/managers/messageManager.d.ts +2 -4
  8. package/dist/managers/messageManager.js +14 -7
  9. package/dist/managers/slashCommandManager.js +28 -0
  10. package/dist/managers/subagentManager.d.ts +0 -2
  11. package/dist/services/aiService.d.ts +2 -3
  12. package/dist/services/aiService.js +4 -18
  13. package/dist/services/configurationService.d.ts +7 -0
  14. package/dist/services/configurationService.js +10 -0
  15. package/dist/telemetry/instrumentation.d.ts +1 -1
  16. package/dist/telemetry/instrumentation.js +57 -28
  17. package/dist/tools/bashTool.js +16 -6
  18. package/dist/tools/grepTool.js +11 -3
  19. package/dist/types/core.d.ts +1 -34
  20. package/dist/types/index.d.ts +4 -0
  21. package/dist/types/messaging.d.ts +2 -0
  22. package/dist/utils/cacheControlUtils.d.ts +5 -27
  23. package/dist/utils/cacheControlUtils.js +12 -73
  24. package/dist/utils/containerSetup.js +0 -3
  25. package/dist/utils/messageOperations.d.ts +2 -1
  26. package/dist/utils/messageOperations.js +2 -1
  27. package/dist/utils/notificationXml.js +5 -0
  28. package/dist/utils/ripgrep.d.ts +4 -4
  29. package/dist/utils/ripgrep.js +4 -27
  30. package/package.json +20 -8
  31. package/bin/rg +0 -79
  32. package/dist/managers/forkedAgentManager.d.ts +0 -50
  33. package/dist/managers/forkedAgentManager.js +0 -130
  34. package/scripts/install_ripgrep.js +0 -111
  35. package/scripts/postinstall.js +0 -38
  36. package/src/agent.ts +0 -1271
  37. package/src/constants/subagents.ts +0 -4
  38. package/src/constants/toolLimits.ts +0 -15
  39. package/src/constants/tools.ts +0 -24
  40. package/src/core/plugin.ts +0 -237
  41. package/src/core/session.ts +0 -9
  42. package/src/index.ts +0 -38
  43. package/src/managers/MemoryRuleManager.ts +0 -198
  44. package/src/managers/aiManager.ts +0 -2429
  45. package/src/managers/backgroundTaskManager.ts +0 -480
  46. package/src/managers/bangManager.ts +0 -119
  47. package/src/managers/cronManager.ts +0 -401
  48. package/src/managers/foregroundTaskManager.ts +0 -30
  49. package/src/managers/forkedAgentManager.ts +0 -216
  50. package/src/managers/hookManager.ts +0 -1089
  51. package/src/managers/liveConfigManager.ts +0 -445
  52. package/src/managers/lspManager.ts +0 -470
  53. package/src/managers/mcpManager.ts +0 -1010
  54. package/src/managers/messageManager.ts +0 -1184
  55. package/src/managers/messageQueue.ts +0 -157
  56. package/src/managers/permissionManager.ts +0 -1140
  57. package/src/managers/planManager.ts +0 -112
  58. package/src/managers/pluginManager.ts +0 -330
  59. package/src/managers/pluginScopeManager.ts +0 -124
  60. package/src/managers/reversionManager.ts +0 -178
  61. package/src/managers/skillManager.ts +0 -578
  62. package/src/managers/slashCommandManager.ts +0 -564
  63. package/src/managers/subagentManager.ts +0 -915
  64. package/src/managers/toolManager.ts +0 -489
  65. package/src/managers/workflowManager.ts +0 -493
  66. package/src/prompts/autoMemory.ts +0 -33
  67. package/src/prompts/autoMemoryExtraction.ts +0 -146
  68. package/src/prompts/index.ts +0 -506
  69. package/src/prompts/planModeReminders.ts +0 -126
  70. package/src/services/GitService.ts +0 -131
  71. package/src/services/MarketplaceService.ts +0 -1052
  72. package/src/services/MemoryRuleService.ts +0 -71
  73. package/src/services/aiService.ts +0 -919
  74. package/src/services/authService.ts +0 -565
  75. package/src/services/autoMemoryService.ts +0 -280
  76. package/src/services/configurationService.ts +0 -1520
  77. package/src/services/fileWatcher.ts +0 -319
  78. package/src/services/hook.ts +0 -348
  79. package/src/services/initializationService.ts +0 -357
  80. package/src/services/interactionService.ts +0 -229
  81. package/src/services/jsonlHandler.ts +0 -295
  82. package/src/services/memory.ts +0 -234
  83. package/src/services/pluginLoader.ts +0 -252
  84. package/src/services/remoteSettingsService.ts +0 -366
  85. package/src/services/reversionService.ts +0 -122
  86. package/src/services/session.ts +0 -917
  87. package/src/services/taskManager.ts +0 -321
  88. package/src/telemetry/events.ts +0 -65
  89. package/src/telemetry/instrumentation.ts +0 -499
  90. package/src/telemetry/sessionTracing.ts +0 -348
  91. package/src/tools/agentTool.ts +0 -322
  92. package/src/tools/askUserQuestion.ts +0 -160
  93. package/src/tools/bashTool.ts +0 -621
  94. package/src/tools/buildTool.ts +0 -61
  95. package/src/tools/cronCreateTool.ts +0 -161
  96. package/src/tools/cronDeleteTool.ts +0 -51
  97. package/src/tools/cronListTool.ts +0 -47
  98. package/src/tools/editTool.ts +0 -313
  99. package/src/tools/enterPlanMode.ts +0 -124
  100. package/src/tools/enterWorktreeTool.ts +0 -190
  101. package/src/tools/exitPlanMode.ts +0 -139
  102. package/src/tools/exitWorktreeTool.ts +0 -236
  103. package/src/tools/globTool.ts +0 -167
  104. package/src/tools/grepTool.ts +0 -399
  105. package/src/tools/lspTool.ts +0 -883
  106. package/src/tools/readTool.ts +0 -426
  107. package/src/tools/skillTool.ts +0 -248
  108. package/src/tools/taskManagementTools.ts +0 -574
  109. package/src/tools/taskStopTool.ts +0 -78
  110. package/src/tools/types.ts +0 -137
  111. package/src/tools/webFetchTool.ts +0 -373
  112. package/src/tools/workflowTool.ts +0 -205
  113. package/src/tools/writeTool.ts +0 -267
  114. package/src/types/agent.ts +0 -128
  115. package/src/types/auth.ts +0 -19
  116. package/src/types/commands.ts +0 -30
  117. package/src/types/config.ts +0 -43
  118. package/src/types/configuration.ts +0 -168
  119. package/src/types/core.ts +0 -105
  120. package/src/types/cron.ts +0 -13
  121. package/src/types/environment.ts +0 -106
  122. package/src/types/fileSearch.ts +0 -4
  123. package/src/types/history.ts +0 -7
  124. package/src/types/hooks.ts +0 -256
  125. package/src/types/index.ts +0 -41
  126. package/src/types/lsp.ts +0 -98
  127. package/src/types/marketplace.ts +0 -71
  128. package/src/types/mcp.ts +0 -43
  129. package/src/types/memoryRule.ts +0 -31
  130. package/src/types/messaging.ts +0 -114
  131. package/src/types/permissions.ts +0 -80
  132. package/src/types/plugins.ts +0 -39
  133. package/src/types/processes.ts +0 -99
  134. package/src/types/reversion.ts +0 -29
  135. package/src/types/session.ts +0 -12
  136. package/src/types/skills.ts +0 -108
  137. package/src/types/tasks.ts +0 -13
  138. package/src/types/telemetry.ts +0 -98
  139. package/src/types/tools.ts +0 -44
  140. package/src/types/workflow.ts +0 -6
  141. package/src/utils/abortUtils.ts +0 -118
  142. package/src/utils/atomicWrite.ts +0 -61
  143. package/src/utils/bashParser.ts +0 -899
  144. package/src/utils/cacheControlUtils.ts +0 -426
  145. package/src/utils/commandArgumentParser.ts +0 -161
  146. package/src/utils/commandPathResolver.ts +0 -88
  147. package/src/utils/configPaths.ts +0 -220
  148. package/src/utils/configValidator.ts +0 -100
  149. package/src/utils/constants.ts +0 -39
  150. package/src/utils/container.ts +0 -92
  151. package/src/utils/containerSetup.ts +0 -371
  152. package/src/utils/convertMessagesForAPI.ts +0 -326
  153. package/src/utils/cronTasks.ts +0 -128
  154. package/src/utils/cronTasksLock.ts +0 -182
  155. package/src/utils/cronToHuman.ts +0 -99
  156. package/src/utils/customCommands.ts +0 -95
  157. package/src/utils/editUtils.ts +0 -20
  158. package/src/utils/fileFormat.ts +0 -40
  159. package/src/utils/fileSearch.ts +0 -151
  160. package/src/utils/fileUtils.ts +0 -223
  161. package/src/utils/gitUtils.ts +0 -315
  162. package/src/utils/globalLogger.ts +0 -128
  163. package/src/utils/groupMessagesByApiRound.ts +0 -121
  164. package/src/utils/hookMatcher.ts +0 -176
  165. package/src/utils/markdownParser.ts +0 -317
  166. package/src/utils/mcpUtils.ts +0 -138
  167. package/src/utils/messageOperations.ts +0 -659
  168. package/src/utils/modelCapabilities.ts +0 -30
  169. package/src/utils/nameGenerator.ts +0 -99
  170. package/src/utils/notificationXml.ts +0 -57
  171. package/src/utils/openaiClient.ts +0 -251
  172. package/src/utils/parseCronExpression.ts +0 -78
  173. package/src/utils/path.ts +0 -72
  174. package/src/utils/pathEncoder.ts +0 -400
  175. package/src/utils/pathSafety.ts +0 -40
  176. package/src/utils/promptHistory.ts +0 -168
  177. package/src/utils/ripgrep.ts +0 -35
  178. package/src/utils/shellResolver.ts +0 -190
  179. package/src/utils/skillParser.ts +0 -246
  180. package/src/utils/stringUtils.ts +0 -156
  181. package/src/utils/subagentParser.ts +0 -279
  182. package/src/utils/taskReminder.ts +0 -96
  183. package/src/utils/tokenCalculation.ts +0 -43
  184. package/src/utils/tokenEstimate.ts +0 -34
  185. package/src/utils/toolResultStorage.ts +0 -117
  186. package/src/utils/worktreeSession.ts +0 -26
  187. package/src/utils/worktreeUtils.ts +0 -786
  188. package/src/workflow/budgetTracker.ts +0 -34
  189. package/src/workflow/concurrencyLimiter.ts +0 -47
  190. package/src/workflow/journal.ts +0 -95
  191. package/src/workflow/progressReporter.ts +0 -141
  192. package/src/workflow/runState.ts +0 -65
  193. package/src/workflow/scriptRuntime.ts +0 -274
  194. package/src/workflow/structuredOutput.ts +0 -123
  195. package/src/workflow/types.ts +0 -101
  196. package/src/workflow/workflowApis.ts +0 -410
  197. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  198. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  199. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  200. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  201. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  202. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -1,151 +0,0 @@
1
- import { spawn } from "child_process";
2
- import { rgPath } from "./ripgrep.js";
3
- import fuzzysort from "fuzzysort";
4
- import type { FileItem } from "../types/fileSearch.js";
5
- import { logger } from "./globalLogger.js";
6
-
7
- const EXCLUDED_FILES = [".git", ".DS_Store"];
8
-
9
- /**
10
- * Execute ripgrep to get all file paths
11
- */
12
- async function getAllFiles(workingDirectory: string): Promise<string[]> {
13
- if (!rgPath) {
14
- throw new Error("ripgrep is not available");
15
- }
16
-
17
- const rgArgs = ["--files", "--color=never", "--hidden"];
18
-
19
- return new Promise((resolve, reject) => {
20
- const child = spawn(rgPath, rgArgs, {
21
- cwd: workingDirectory,
22
- stdio: ["ignore", "pipe", "pipe"],
23
- });
24
-
25
- let stdout = "";
26
- let stderr = "";
27
-
28
- child.stdout?.on("data", (data) => {
29
- stdout += data.toString();
30
- });
31
-
32
- child.stderr?.on("data", (data) => {
33
- stderr += data.toString();
34
- });
35
-
36
- child.on("close", (code) => {
37
- // Exit 2 = some files were unreadable (e.g. device-name files like
38
- // "nul" on Windows); stdout still holds usable partial results.
39
- // Spawn failures surface via the 'error' listener instead.
40
- if (code !== 0 && code !== 1) {
41
- logger.warn(
42
- `ripgrep exited with code ${code}, keeping partial results: ${stderr.trim()}`,
43
- );
44
- }
45
- const files = stdout
46
- .trim()
47
- .split("\n")
48
- .filter((f) => {
49
- if (f.length === 0) return false;
50
- const parts = f.split(/[/\\]/);
51
- return !parts.some((part) => EXCLUDED_FILES.includes(part));
52
- })
53
- .map((f) => f.replace(/\\/g, "/")); // Normalize to forward slashes
54
- resolve(files);
55
- });
56
-
57
- child.on("error", (err) => {
58
- reject(err);
59
- });
60
- });
61
- }
62
-
63
- /**
64
- * Derive directory paths from file paths
65
- */
66
- function deriveDirectories(files: string[]): string[] {
67
- const dirs = new Set<string>();
68
- for (const file of files) {
69
- const parts = file.split("/");
70
- // Add all parent directories
71
- for (let i = 1; i < parts.length; i++) {
72
- const dir = parts.slice(0, i).join("/") + "/";
73
- dirs.add(dir);
74
- }
75
- }
76
- return Array.from(dirs);
77
- }
78
-
79
- /**
80
- * Search files and directories using fuzzy matching
81
- */
82
- export const searchFiles = async (
83
- query: string,
84
- options?: {
85
- maxResults?: number;
86
- workingDirectory?: string;
87
- },
88
- ): Promise<FileItem[]> => {
89
- const { maxResults = 100, workingDirectory = process.cwd() } = options || {};
90
-
91
- try {
92
- const files = await getAllFiles(workingDirectory);
93
-
94
- if (!query.trim()) {
95
- // When query is empty, show some common file types and top-level directories
96
- const commonExtensions = new Set([
97
- "ts",
98
- "tsx",
99
- "js",
100
- "jsx",
101
- "json",
102
- "py",
103
- "java",
104
- ]);
105
- const results: FileItem[] = [];
106
- const seenDirs = new Set<string>();
107
-
108
- for (const file of files) {
109
- const parts = file.split("/");
110
- if (parts.length > 1) {
111
- const topDir = parts[0] + "/";
112
- if (!seenDirs.has(topDir)) {
113
- seenDirs.add(topDir);
114
- results.push({ path: topDir, type: "directory" });
115
- }
116
- }
117
-
118
- const ext = file.split(".").pop();
119
- if (ext && commonExtensions.has(ext)) {
120
- results.push({ path: file, type: "file" });
121
- }
122
- }
123
-
124
- // Sort: directories first, then files, then alphabetically
125
- return results
126
- .sort((a, b) => {
127
- if (a.type === "directory" && b.type === "file") return -1;
128
- if (a.type === "file" && b.type === "directory") return 1;
129
- return a.path.localeCompare(b.path);
130
- })
131
- .slice(0, maxResults);
132
- }
133
-
134
- const directories = deriveDirectories(files);
135
- const allItems: FileItem[] = [
136
- ...files.map((f) => ({ path: f, type: "file" as const })),
137
- ...directories.map((d) => ({ path: d, type: "directory" as const })),
138
- ];
139
-
140
- const fuzzyResults = fuzzysort.go(query, allItems, {
141
- key: "path",
142
- limit: maxResults,
143
- threshold: 0,
144
- });
145
-
146
- return fuzzyResults.map((res) => res.obj);
147
- } catch (error) {
148
- logger.error("Fuzzy search error:", error);
149
- return [];
150
- }
151
- };
@@ -1,223 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import { createReadStream } from "node:fs";
3
- import path from "node:path";
4
- import { glob } from "glob";
5
-
6
- /**
7
- * Reads the first line of a file efficiently using Node.js readline.
8
- *
9
- * @param {string} filePath - The path to the file.
10
- * @return {Promise<string>} - The first non-empty line of the file, or an empty string otherwise.
11
- */
12
- export async function readFirstLine(filePath: string): Promise<string> {
13
- const { createInterface } = await import("node:readline");
14
-
15
- const fileStream = createReadStream(filePath);
16
- const rl = createInterface({
17
- input: fileStream,
18
- crlfDelay: Infinity, // Handle \r\n properly
19
- });
20
-
21
- try {
22
- for await (const line of rl) {
23
- const trimmedLine = line.trim();
24
- if (trimmedLine.length > 0) {
25
- return trimmedLine;
26
- }
27
- }
28
- return "";
29
- } catch {
30
- // If reading fails (e.g., file doesn't exist), return empty string
31
- return "";
32
- } finally {
33
- rl.close();
34
- fileStream.destroy();
35
- }
36
- }
37
-
38
- /**
39
- * Reads up to N lines from a file using Node.js readline.
40
- *
41
- * @param {string} filePath - The path to the file.
42
- * @param {number} maxLines - Maximum number of non-empty lines to read.
43
- * @return {Promise<string[]>} - Array of non-empty lines (up to maxLines).
44
- */
45
- export async function readFirstNLines(
46
- filePath: string,
47
- maxLines: number,
48
- ): Promise<string[]> {
49
- const { createInterface } = await import("node:readline");
50
-
51
- const fileStream = createReadStream(filePath);
52
- const rl = createInterface({
53
- input: fileStream,
54
- crlfDelay: Infinity,
55
- });
56
-
57
- const lines: string[] = [];
58
-
59
- try {
60
- for await (const line of rl) {
61
- const trimmedLine = line.trim();
62
- if (trimmedLine.length > 0) {
63
- lines.push(trimmedLine);
64
- if (lines.length >= maxLines) {
65
- break;
66
- }
67
- }
68
- }
69
- return lines;
70
- } catch {
71
- return lines;
72
- } finally {
73
- rl.close();
74
- fileStream.destroy();
75
- }
76
- }
77
-
78
- /**
79
- * Reads a file from the end and returns the last non-empty line.
80
- *
81
- * This version supports files that end with:
82
- * - "\n" (Unix-style, including modern macOS)
83
- * - "\r\n" (Windows-style)
84
- * - "\r" (older Mac-style, HL7, etc.)
85
- *
86
- * @param {string} filePath - The path to the file.
87
- * @param {number} [minLength=1] - Minimum length for the returned line.
88
- * @return {Promise<string>} - The last non-empty line of the file, or an empty string if no non-empty lines found.
89
- */
90
- export async function getLastLine(
91
- filePath: string,
92
- minLength = 1,
93
- ): Promise<string> {
94
- let fileHandle;
95
- try {
96
- const stats = await fs.stat(filePath);
97
- const fileSize = stats.size;
98
-
99
- if (fileSize === 0) return "";
100
-
101
- fileHandle = await fs.open(filePath, "r");
102
- const bufferSize = 8 * 1024; // 8KB buffer is usually enough for the last line
103
- const buffer = Buffer.alloc(bufferSize);
104
-
105
- let lineEnd: number | null = null;
106
- let lineStart: number | null = null;
107
- let currentPosition = fileSize;
108
-
109
- while (currentPosition > 0 && lineStart === null) {
110
- const readSize = Math.min(bufferSize, currentPosition);
111
- currentPosition -= readSize;
112
-
113
- const { bytesRead } = await fileHandle.read(
114
- buffer,
115
- 0,
116
- readSize,
117
- currentPosition,
118
- );
119
-
120
- for (let i = bytesRead - 1; i >= 0; i--) {
121
- const charCode = buffer[i];
122
- if (lineEnd === null) {
123
- // Still looking for the end of the last non-empty line (skip trailing newlines and whitespace)
124
- if (charCode > 32) {
125
- lineEnd = currentPosition + i + 1;
126
- }
127
- } else {
128
- // Looking for the start of the line (the newline before it)
129
- if (charCode === 10 || charCode === 13) {
130
- lineStart = currentPosition + i + 1;
131
- break;
132
- }
133
- }
134
- }
135
- }
136
-
137
- if (lineEnd === null) return "";
138
- if (lineStart === null) lineStart = 0;
139
-
140
- const length = lineEnd - lineStart;
141
- if (length < minLength) return "";
142
-
143
- const resultBuffer = Buffer.alloc(length);
144
- await fileHandle.read(resultBuffer, 0, length, lineStart);
145
- const result = resultBuffer.toString("utf8").trim();
146
- return result.length >= minLength ? result : "";
147
- } catch {
148
- // If reading fails (e.g., file doesn't exist), return empty string
149
- return "";
150
- } finally {
151
- if (fileHandle) {
152
- await fileHandle.close();
153
- }
154
- }
155
- }
156
-
157
- /**
158
- * Simple Levenshtein distance implementation
159
- */
160
- function levenshtein(a: string, b: string): number {
161
- const matrix = Array.from({ length: a.length + 1 }, () =>
162
- Array.from({ length: b.length + 1 }, () => 0),
163
- );
164
-
165
- for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
166
- for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
167
-
168
- for (let i = 1; i <= a.length; i++) {
169
- for (let j = 1; j <= b.length; j++) {
170
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
171
- matrix[i][j] = Math.min(
172
- matrix[i - 1][j] + 1,
173
- matrix[i][j - 1] + 1,
174
- matrix[i - 1][j - 1] + cost,
175
- );
176
- }
177
- }
178
-
179
- return matrix[a.length][b.length];
180
- }
181
-
182
- /**
183
- * Suggests similar paths if a file is not found.
184
- */
185
- export async function suggestPathUnderCwd(
186
- targetPath: string,
187
- workdir: string,
188
- ): Promise<string[]> {
189
- try {
190
- const allFiles = await glob("**/*", {
191
- cwd: workdir,
192
- nodir: true,
193
- dot: true,
194
- ignore: ["**/.git/**", "**/node_modules/**"],
195
- });
196
-
197
- const targetBasename = path.basename(targetPath);
198
- const suggestions = allFiles
199
- .map((file) => ({
200
- path: file,
201
- distance: levenshtein(targetBasename, path.basename(file)),
202
- }))
203
- .filter((item) => item.distance <= 3) // Threshold for similarity
204
- .sort((a, b) => a.distance - b.distance)
205
- .slice(0, 5)
206
- .map((item) => item.path);
207
-
208
- return suggestions;
209
- } catch {
210
- return [];
211
- }
212
- }
213
-
214
- /**
215
- * Uses fuzzy matching to find the intended file.
216
- */
217
- export async function findSimilarFile(
218
- targetPath: string,
219
- workdir: string,
220
- ): Promise<string | null> {
221
- const suggestions = await suggestPathUnderCwd(targetPath, workdir);
222
- return suggestions.length > 0 ? suggestions[0] : null;
223
- }
@@ -1,315 +0,0 @@
1
- import * as path from "node:path";
2
- import * as fsSync from "node:fs";
3
- import { execFileSync } from "node:child_process";
4
-
5
- /**
6
- * Check if a directory is a git repository
7
- * @param dirPath Directory path
8
- * @returns "Yes" if it's a git repository, "No" otherwise
9
- */
10
- export function isGitRepository(dirPath: string): string {
11
- try {
12
- // Check if .git directory exists in current directory or any parent directory
13
- let currentPath = path.resolve(dirPath);
14
- while (currentPath !== path.dirname(currentPath)) {
15
- const gitPath = path.join(currentPath, ".git");
16
- if (fsSync.existsSync(gitPath)) {
17
- return "Yes";
18
- }
19
- currentPath = path.dirname(currentPath);
20
- }
21
- return "No";
22
- } catch {
23
- return "No";
24
- }
25
- }
26
-
27
- /**
28
- * Get the root directory of the git repository
29
- * @param cwd Working directory
30
- * @returns Repository root path
31
- */
32
- export function getGitRepoRoot(cwd: string): string {
33
- try {
34
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
35
- cwd,
36
- encoding: "utf8",
37
- stdio: ["ignore", "pipe", "ignore"],
38
- }).trim();
39
- } catch {
40
- return cwd;
41
- }
42
- }
43
-
44
- /**
45
- * Get the common directory of the git repository (handles worktrees)
46
- * @param cwd Working directory
47
- * @returns Repository common directory path
48
- */
49
- export function getGitCommonDir(cwd: string): string {
50
- try {
51
- const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
52
- cwd,
53
- encoding: "utf8",
54
- stdio: ["ignore", "pipe", "ignore"],
55
- }).trim();
56
- return path.resolve(cwd, commonDir);
57
- } catch {
58
- return getGitRepoRoot(cwd);
59
- }
60
- }
61
-
62
- /**
63
- * Get the main repository root directory (the first worktree in the list)
64
- * @param cwd Working directory
65
- * @returns Main repository root path
66
- */
67
- export function getGitMainRepoRoot(cwd: string): string {
68
- try {
69
- const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
70
- cwd,
71
- encoding: "utf8",
72
- stdio: ["ignore", "pipe", "ignore"],
73
- }).trim();
74
- const lines = output.split("\n");
75
- if (lines.length > 0 && lines[0].startsWith("worktree ")) {
76
- return lines[0].substring("worktree ".length).trim();
77
- }
78
- return getGitRepoRoot(cwd);
79
- } catch {
80
- return getGitRepoRoot(cwd);
81
- }
82
- }
83
-
84
- /**
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
91
- */
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);
124
- }
125
- return null;
126
- }
127
-
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 {
137
- try {
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;
144
- } catch {
145
- return null;
146
- }
147
- }
148
-
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
162
- try {
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
- }
176
- } catch {
177
- // packed-refs doesn't exist or can't be read
178
- }
179
- return false;
180
- }
181
-
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";
199
- }
200
-
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";
219
- }
220
-
221
- // 4. Hardcoded fallback
222
- return "main";
223
- }
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
-
279
- /**
280
- * Check if there are uncommitted changes in the working directory
281
- * @param cwd Working directory
282
- * @returns True if there are uncommitted changes
283
- */
284
- export function hasUncommittedChanges(cwd: string): boolean {
285
- try {
286
- const status = execFileSync("git", ["status", "--porcelain"], {
287
- cwd,
288
- encoding: "utf8",
289
- stdio: ["ignore", "pipe", "ignore"],
290
- }).trim();
291
- return status.length > 0;
292
- } catch {
293
- return false;
294
- }
295
- }
296
-
297
- /**
298
- * Check if there are new commits in the current branch that are not in the base branch
299
- * @param cwd Working directory
300
- * @param baseBranch Base branch name (e.g., origin/main)
301
- * @returns True if there are new commits
302
- */
303
- export function hasNewCommits(cwd: string, baseBranch?: string): boolean {
304
- try {
305
- const range = baseBranch ? `${baseBranch}..HEAD` : "@{u}..HEAD";
306
- const log = execFileSync("git", ["log", range, "--oneline"], {
307
- cwd,
308
- encoding: "utf8",
309
- stdio: ["ignore", "pipe", "ignore"],
310
- }).trim();
311
- return log.length > 0;
312
- } catch {
313
- return false;
314
- }
315
- }