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.
- package/dist/agent.d.ts +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +19 -9
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/services/session.js +2 -9
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +10 -1
- package/dist/utils/messageOperations.js +19 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1180
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -922
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -641
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
package/src/utils/fileSearch.ts
DELETED
|
@@ -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
|
-
};
|
package/src/utils/fileUtils.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/gitUtils.ts
DELETED
|
@@ -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
|
-
}
|