wave-agent-sdk 1.0.4 → 1.0.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.
Files changed (194) hide show
  1. package/dist/agent.d.ts +23 -1
  2. package/dist/agent.js +34 -4
  3. package/dist/managers/aiManager.d.ts +36 -0
  4. package/dist/managers/aiManager.js +240 -8
  5. package/dist/managers/messageManager.d.ts +1 -3
  6. package/dist/managers/messageManager.js +19 -9
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/services/session.js +2 -9
  10. package/dist/telemetry/instrumentation.d.ts +1 -1
  11. package/dist/telemetry/instrumentation.js +57 -28
  12. package/dist/tools/bashTool.js +16 -6
  13. package/dist/tools/grepTool.js +11 -3
  14. package/dist/types/index.d.ts +3 -0
  15. package/dist/types/messaging.d.ts +2 -0
  16. package/dist/utils/containerSetup.js +0 -3
  17. package/dist/utils/messageOperations.d.ts +10 -1
  18. package/dist/utils/messageOperations.js +19 -1
  19. package/dist/utils/notificationXml.js +5 -0
  20. package/dist/utils/ripgrep.d.ts +4 -4
  21. package/dist/utils/ripgrep.js +4 -27
  22. package/package.json +20 -8
  23. package/bin/rg +0 -79
  24. package/dist/managers/forkedAgentManager.d.ts +0 -50
  25. package/dist/managers/forkedAgentManager.js +0 -130
  26. package/scripts/install_ripgrep.js +0 -111
  27. package/scripts/postinstall.js +0 -38
  28. package/src/agent.ts +0 -1271
  29. package/src/constants/subagents.ts +0 -4
  30. package/src/constants/toolLimits.ts +0 -15
  31. package/src/constants/tools.ts +0 -24
  32. package/src/core/plugin.ts +0 -237
  33. package/src/core/session.ts +0 -9
  34. package/src/index.ts +0 -38
  35. package/src/managers/MemoryRuleManager.ts +0 -198
  36. package/src/managers/aiManager.ts +0 -2429
  37. package/src/managers/backgroundTaskManager.ts +0 -480
  38. package/src/managers/bangManager.ts +0 -119
  39. package/src/managers/cronManager.ts +0 -401
  40. package/src/managers/foregroundTaskManager.ts +0 -30
  41. package/src/managers/forkedAgentManager.ts +0 -216
  42. package/src/managers/hookManager.ts +0 -1089
  43. package/src/managers/liveConfigManager.ts +0 -445
  44. package/src/managers/lspManager.ts +0 -470
  45. package/src/managers/mcpManager.ts +0 -1010
  46. package/src/managers/messageManager.ts +0 -1180
  47. package/src/managers/messageQueue.ts +0 -157
  48. package/src/managers/permissionManager.ts +0 -1140
  49. package/src/managers/planManager.ts +0 -112
  50. package/src/managers/pluginManager.ts +0 -330
  51. package/src/managers/pluginScopeManager.ts +0 -124
  52. package/src/managers/reversionManager.ts +0 -178
  53. package/src/managers/skillManager.ts +0 -578
  54. package/src/managers/slashCommandManager.ts +0 -564
  55. package/src/managers/subagentManager.ts +0 -915
  56. package/src/managers/toolManager.ts +0 -489
  57. package/src/managers/workflowManager.ts +0 -493
  58. package/src/prompts/autoMemory.ts +0 -33
  59. package/src/prompts/autoMemoryExtraction.ts +0 -146
  60. package/src/prompts/index.ts +0 -506
  61. package/src/prompts/planModeReminders.ts +0 -126
  62. package/src/services/GitService.ts +0 -131
  63. package/src/services/MarketplaceService.ts +0 -1052
  64. package/src/services/MemoryRuleService.ts +0 -71
  65. package/src/services/aiService.ts +0 -919
  66. package/src/services/authService.ts +0 -565
  67. package/src/services/autoMemoryService.ts +0 -280
  68. package/src/services/configurationService.ts +0 -1520
  69. package/src/services/fileWatcher.ts +0 -319
  70. package/src/services/hook.ts +0 -348
  71. package/src/services/initializationService.ts +0 -357
  72. package/src/services/interactionService.ts +0 -229
  73. package/src/services/jsonlHandler.ts +0 -295
  74. package/src/services/memory.ts +0 -234
  75. package/src/services/pluginLoader.ts +0 -252
  76. package/src/services/remoteSettingsService.ts +0 -366
  77. package/src/services/reversionService.ts +0 -122
  78. package/src/services/session.ts +0 -922
  79. package/src/services/taskManager.ts +0 -321
  80. package/src/telemetry/events.ts +0 -65
  81. package/src/telemetry/instrumentation.ts +0 -499
  82. package/src/telemetry/sessionTracing.ts +0 -348
  83. package/src/tools/agentTool.ts +0 -322
  84. package/src/tools/askUserQuestion.ts +0 -160
  85. package/src/tools/bashTool.ts +0 -621
  86. package/src/tools/buildTool.ts +0 -61
  87. package/src/tools/cronCreateTool.ts +0 -161
  88. package/src/tools/cronDeleteTool.ts +0 -51
  89. package/src/tools/cronListTool.ts +0 -47
  90. package/src/tools/editTool.ts +0 -313
  91. package/src/tools/enterPlanMode.ts +0 -124
  92. package/src/tools/enterWorktreeTool.ts +0 -190
  93. package/src/tools/exitPlanMode.ts +0 -139
  94. package/src/tools/exitWorktreeTool.ts +0 -236
  95. package/src/tools/globTool.ts +0 -167
  96. package/src/tools/grepTool.ts +0 -399
  97. package/src/tools/lspTool.ts +0 -883
  98. package/src/tools/readTool.ts +0 -426
  99. package/src/tools/skillTool.ts +0 -248
  100. package/src/tools/taskManagementTools.ts +0 -574
  101. package/src/tools/taskStopTool.ts +0 -78
  102. package/src/tools/types.ts +0 -137
  103. package/src/tools/webFetchTool.ts +0 -373
  104. package/src/tools/workflowTool.ts +0 -205
  105. package/src/tools/writeTool.ts +0 -267
  106. package/src/types/agent.ts +0 -128
  107. package/src/types/auth.ts +0 -19
  108. package/src/types/commands.ts +0 -30
  109. package/src/types/config.ts +0 -43
  110. package/src/types/configuration.ts +0 -168
  111. package/src/types/core.ts +0 -105
  112. package/src/types/cron.ts +0 -13
  113. package/src/types/environment.ts +0 -106
  114. package/src/types/fileSearch.ts +0 -4
  115. package/src/types/history.ts +0 -7
  116. package/src/types/hooks.ts +0 -256
  117. package/src/types/index.ts +0 -41
  118. package/src/types/lsp.ts +0 -98
  119. package/src/types/marketplace.ts +0 -71
  120. package/src/types/mcp.ts +0 -43
  121. package/src/types/memoryRule.ts +0 -31
  122. package/src/types/messaging.ts +0 -114
  123. package/src/types/permissions.ts +0 -80
  124. package/src/types/plugins.ts +0 -39
  125. package/src/types/processes.ts +0 -99
  126. package/src/types/reversion.ts +0 -29
  127. package/src/types/session.ts +0 -12
  128. package/src/types/skills.ts +0 -108
  129. package/src/types/tasks.ts +0 -13
  130. package/src/types/telemetry.ts +0 -98
  131. package/src/types/tools.ts +0 -44
  132. package/src/types/workflow.ts +0 -6
  133. package/src/utils/abortUtils.ts +0 -118
  134. package/src/utils/atomicWrite.ts +0 -61
  135. package/src/utils/bashParser.ts +0 -899
  136. package/src/utils/cacheControlUtils.ts +0 -426
  137. package/src/utils/commandArgumentParser.ts +0 -161
  138. package/src/utils/commandPathResolver.ts +0 -88
  139. package/src/utils/configPaths.ts +0 -220
  140. package/src/utils/configValidator.ts +0 -100
  141. package/src/utils/constants.ts +0 -39
  142. package/src/utils/container.ts +0 -92
  143. package/src/utils/containerSetup.ts +0 -371
  144. package/src/utils/convertMessagesForAPI.ts +0 -326
  145. package/src/utils/cronTasks.ts +0 -128
  146. package/src/utils/cronTasksLock.ts +0 -182
  147. package/src/utils/cronToHuman.ts +0 -99
  148. package/src/utils/customCommands.ts +0 -95
  149. package/src/utils/editUtils.ts +0 -20
  150. package/src/utils/fileFormat.ts +0 -40
  151. package/src/utils/fileSearch.ts +0 -151
  152. package/src/utils/fileUtils.ts +0 -223
  153. package/src/utils/gitUtils.ts +0 -315
  154. package/src/utils/globalLogger.ts +0 -128
  155. package/src/utils/groupMessagesByApiRound.ts +0 -121
  156. package/src/utils/hookMatcher.ts +0 -176
  157. package/src/utils/markdownParser.ts +0 -317
  158. package/src/utils/mcpUtils.ts +0 -138
  159. package/src/utils/messageOperations.ts +0 -641
  160. package/src/utils/modelCapabilities.ts +0 -30
  161. package/src/utils/nameGenerator.ts +0 -99
  162. package/src/utils/notificationXml.ts +0 -57
  163. package/src/utils/openaiClient.ts +0 -251
  164. package/src/utils/parseCronExpression.ts +0 -78
  165. package/src/utils/path.ts +0 -72
  166. package/src/utils/pathEncoder.ts +0 -400
  167. package/src/utils/pathSafety.ts +0 -40
  168. package/src/utils/promptHistory.ts +0 -168
  169. package/src/utils/ripgrep.ts +0 -35
  170. package/src/utils/shellResolver.ts +0 -190
  171. package/src/utils/skillParser.ts +0 -246
  172. package/src/utils/stringUtils.ts +0 -156
  173. package/src/utils/subagentParser.ts +0 -279
  174. package/src/utils/taskReminder.ts +0 -96
  175. package/src/utils/tokenCalculation.ts +0 -43
  176. package/src/utils/tokenEstimate.ts +0 -34
  177. package/src/utils/toolResultStorage.ts +0 -117
  178. package/src/utils/worktreeSession.ts +0 -26
  179. package/src/utils/worktreeUtils.ts +0 -786
  180. package/src/workflow/budgetTracker.ts +0 -34
  181. package/src/workflow/concurrencyLimiter.ts +0 -47
  182. package/src/workflow/journal.ts +0 -95
  183. package/src/workflow/progressReporter.ts +0 -141
  184. package/src/workflow/runState.ts +0 -65
  185. package/src/workflow/scriptRuntime.ts +0 -274
  186. package/src/workflow/structuredOutput.ts +0 -123
  187. package/src/workflow/types.ts +0 -101
  188. package/src/workflow/workflowApis.ts +0 -410
  189. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  190. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  191. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  192. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  193. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  194. 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
- }