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/bashParser.ts
DELETED
|
@@ -1,899 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Splits a complex bash command into individual simple commands by shell operators (&&, ||, ;, |, &).
|
|
3
|
-
* Correctly handles quotes, escaped characters, and subshells.
|
|
4
|
-
*/
|
|
5
|
-
export function splitBashCommand(command: string): string[] {
|
|
6
|
-
let inSingleQuote = false;
|
|
7
|
-
let inDoubleQuote = false;
|
|
8
|
-
let escaped = false;
|
|
9
|
-
let parenLevel = 0;
|
|
10
|
-
const splitPositions: number[] = [];
|
|
11
|
-
|
|
12
|
-
for (let i = 0; i < command.length; i++) {
|
|
13
|
-
const char = command[i];
|
|
14
|
-
const nextChar = command[i + 1];
|
|
15
|
-
|
|
16
|
-
if (escaped) {
|
|
17
|
-
escaped = false;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (char === "\\") {
|
|
22
|
-
escaped = true;
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (char === "'" && !inDoubleQuote) {
|
|
27
|
-
inSingleQuote = !inSingleQuote;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (char === '"' && !inSingleQuote) {
|
|
32
|
-
inDoubleQuote = !inDoubleQuote;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (char === "(") {
|
|
41
|
-
parenLevel++;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (char === ")") {
|
|
46
|
-
parenLevel--;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (parenLevel > 0) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Check for operators
|
|
55
|
-
let opLen = 0;
|
|
56
|
-
if (char === "&" && nextChar === "&") opLen = 2;
|
|
57
|
-
else if (char === "|" && nextChar === "|") opLen = 2;
|
|
58
|
-
else if (char === "|" && nextChar === "&") opLen = 2;
|
|
59
|
-
else if (char === ";") opLen = 1;
|
|
60
|
-
else if (char === "|") opLen = 1;
|
|
61
|
-
else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
|
|
62
|
-
opLen = 1;
|
|
63
|
-
|
|
64
|
-
if (opLen > 0) {
|
|
65
|
-
// Check if preceded by an odd number of backslashes
|
|
66
|
-
let backslashCount = 0;
|
|
67
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
68
|
-
if (command[j] === "\\") backslashCount++;
|
|
69
|
-
else break;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ALSO check if preceded by an escaped operator character (e.g., \&&)
|
|
73
|
-
let precededByEscapedOp = false;
|
|
74
|
-
if (i > 0 && /[&|;]/.test(command[i - 1])) {
|
|
75
|
-
let bsCount = 0;
|
|
76
|
-
for (let j = i - 2; j >= 0; j--) {
|
|
77
|
-
if (command[j] === "\\") bsCount++;
|
|
78
|
-
else break;
|
|
79
|
-
}
|
|
80
|
-
if (bsCount % 2 !== 0) precededByEscapedOp = true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (backslashCount % 2 === 0 && !precededByEscapedOp) {
|
|
84
|
-
splitPositions.push(i, i + opLen);
|
|
85
|
-
i += opLen - 1;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let lastPos = 0;
|
|
91
|
-
const parts: string[] = [];
|
|
92
|
-
for (let i = 0; i < splitPositions.length; i += 2) {
|
|
93
|
-
const start = splitPositions[i];
|
|
94
|
-
const end = splitPositions[i + 1];
|
|
95
|
-
const part = command.substring(lastPos, start).trim();
|
|
96
|
-
if (part) parts.push(part);
|
|
97
|
-
lastPos = end;
|
|
98
|
-
}
|
|
99
|
-
const lastPart = command.substring(lastPos).trim();
|
|
100
|
-
if (lastPart) parts.push(lastPart);
|
|
101
|
-
|
|
102
|
-
const finalResult: string[] = [];
|
|
103
|
-
for (const part of parts) {
|
|
104
|
-
const envStripped = stripEnvVars(part);
|
|
105
|
-
const stripped = stripRedirections(envStripped);
|
|
106
|
-
if (
|
|
107
|
-
stripped.startsWith("(") &&
|
|
108
|
-
stripped.endsWith(")") &&
|
|
109
|
-
stripped === envStripped
|
|
110
|
-
) {
|
|
111
|
-
const inner = stripped.substring(1, stripped.length - 1).trim();
|
|
112
|
-
if (inner) {
|
|
113
|
-
finalResult.push(...splitBashCommand(inner));
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
finalResult.push(part);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return finalResult;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Removes inline environment variable assignments (e.g., VAR=val cmd -> cmd).
|
|
125
|
-
*/
|
|
126
|
-
export function stripEnvVars(command: string): string {
|
|
127
|
-
let result = command.trim();
|
|
128
|
-
while (true) {
|
|
129
|
-
const match = result.match(/^([a-zA-Z_][a-zA-Z0-9_]*)=/);
|
|
130
|
-
if (!match) break;
|
|
131
|
-
|
|
132
|
-
const varNameEnd = match[0].length;
|
|
133
|
-
let valueEnd = varNameEnd;
|
|
134
|
-
|
|
135
|
-
if (result[varNameEnd] === "'") {
|
|
136
|
-
valueEnd = result.indexOf("'", varNameEnd + 1);
|
|
137
|
-
if (valueEnd === -1) break;
|
|
138
|
-
valueEnd++;
|
|
139
|
-
} else if (result[varNameEnd] === '"') {
|
|
140
|
-
let escaped = false;
|
|
141
|
-
let found = false;
|
|
142
|
-
for (let i = varNameEnd + 1; i < result.length; i++) {
|
|
143
|
-
if (escaped) {
|
|
144
|
-
escaped = false;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (result[i] === "\\") {
|
|
148
|
-
escaped = true;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (result[i] === '"') {
|
|
152
|
-
valueEnd = i + 1;
|
|
153
|
-
found = true;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (!found) break;
|
|
158
|
-
} else {
|
|
159
|
-
const spaceIndex = result.search(/\s/);
|
|
160
|
-
if (spaceIndex === -1) {
|
|
161
|
-
return "";
|
|
162
|
-
}
|
|
163
|
-
valueEnd = spaceIndex;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
result = result.substring(valueEnd).trim();
|
|
167
|
-
}
|
|
168
|
-
return result;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Removes redirections (e.g., echo "data" > output.txt -> echo "data").
|
|
173
|
-
*/
|
|
174
|
-
export function stripRedirections(command: string): string {
|
|
175
|
-
let result = "";
|
|
176
|
-
let inSingleQuote = false;
|
|
177
|
-
let inDoubleQuote = false;
|
|
178
|
-
let escaped = false;
|
|
179
|
-
|
|
180
|
-
for (let i = 0; i < command.length; i++) {
|
|
181
|
-
const char = command[i];
|
|
182
|
-
|
|
183
|
-
if (escaped) {
|
|
184
|
-
result += char;
|
|
185
|
-
escaped = false;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (char === "\\") {
|
|
190
|
-
result += char;
|
|
191
|
-
escaped = true;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (char === "'" && !inDoubleQuote) {
|
|
196
|
-
inSingleQuote = !inSingleQuote;
|
|
197
|
-
result += char;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (char === '"' && !inSingleQuote) {
|
|
202
|
-
inDoubleQuote = !inDoubleQuote;
|
|
203
|
-
result += char;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
208
|
-
result += char;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Handle whitespace outside quotes: collapse multiple spaces into one
|
|
213
|
-
if (/\s/.test(char)) {
|
|
214
|
-
if (result.length > 0 && !/\s/.test(result[result.length - 1])) {
|
|
215
|
-
result += " ";
|
|
216
|
-
}
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check for redirection
|
|
221
|
-
if (char === ">" || char === "<") {
|
|
222
|
-
// Check if preceded by a digit or & (for 2> or &>)
|
|
223
|
-
if (result.length > 0 && /[0-9&]/.test(result[result.length - 1])) {
|
|
224
|
-
// Ensure it's at the start of a word or preceded by whitespace
|
|
225
|
-
if (result.length === 1 || /\s/.test(result[result.length - 2])) {
|
|
226
|
-
// Remove the digit/& from result
|
|
227
|
-
result = result.substring(0, result.length - 1);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
let end = i + 1;
|
|
232
|
-
if (command[end] === char) {
|
|
233
|
-
end++;
|
|
234
|
-
if (char === "<" && command[end] === "-") {
|
|
235
|
-
end++;
|
|
236
|
-
}
|
|
237
|
-
} else if (
|
|
238
|
-
command[end] === "&" ||
|
|
239
|
-
(char === ">" && command[end] === "|")
|
|
240
|
-
) {
|
|
241
|
-
end++;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Skip whitespace after operator
|
|
245
|
-
while (end < command.length && /\s/.test(command[end])) {
|
|
246
|
-
end++;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Skip the following word (the target of redirection)
|
|
250
|
-
let wordEscaped = false;
|
|
251
|
-
let wordInSingleQuote = false;
|
|
252
|
-
let wordInDoubleQuote = false;
|
|
253
|
-
while (end < command.length) {
|
|
254
|
-
const c = command[end];
|
|
255
|
-
if (wordEscaped) {
|
|
256
|
-
wordEscaped = false;
|
|
257
|
-
end++;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (c === "\\") {
|
|
261
|
-
wordEscaped = true;
|
|
262
|
-
end++;
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
if (c === "'" && !wordInDoubleQuote) {
|
|
266
|
-
wordInSingleQuote = !wordInSingleQuote;
|
|
267
|
-
end++;
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
if (c === '"' && !wordInSingleQuote) {
|
|
271
|
-
wordInDoubleQuote = !wordInDoubleQuote;
|
|
272
|
-
end++;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (!wordInSingleQuote && !wordInDoubleQuote && /\s/.test(c)) {
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
end++;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
i = end - 1;
|
|
282
|
-
// After stripping a redirection, ensure there's a space if we're not at the end
|
|
283
|
-
if (result.length > 0 && !/\s/.test(result[result.length - 1])) {
|
|
284
|
-
result += " ";
|
|
285
|
-
}
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
result += char;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return result.trim();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Checks if a bash command contains any write redirections (>, >>, &>, 2>, >|).
|
|
297
|
-
*/
|
|
298
|
-
export function hasWriteRedirections(command: string): boolean {
|
|
299
|
-
let inSingleQuote = false;
|
|
300
|
-
let inDoubleQuote = false;
|
|
301
|
-
let escaped = false;
|
|
302
|
-
|
|
303
|
-
for (let i = 0; i < command.length; i++) {
|
|
304
|
-
const char = command[i];
|
|
305
|
-
|
|
306
|
-
if (escaped) {
|
|
307
|
-
escaped = false;
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (char === "\\") {
|
|
312
|
-
escaped = true;
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (char === "'" && !inDoubleQuote) {
|
|
317
|
-
inSingleQuote = !inSingleQuote;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (char === '"' && !inSingleQuote) {
|
|
322
|
-
inDoubleQuote = !inDoubleQuote;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (char === ">") {
|
|
331
|
-
// Check if this is a redirection to /dev/null
|
|
332
|
-
let j = i + 1;
|
|
333
|
-
// Handle >> or >|
|
|
334
|
-
if (j < command.length && (command[j] === ">" || command[j] === "|")) {
|
|
335
|
-
j++;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Skip whitespace after operator
|
|
339
|
-
while (j < command.length && /\s/.test(command[j])) {
|
|
340
|
-
j++;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Extract the target word, handling quotes and escapes
|
|
344
|
-
let target = "";
|
|
345
|
-
let targetEscaped = false;
|
|
346
|
-
let targetInSingleQuote = false;
|
|
347
|
-
let targetInDoubleQuote = false;
|
|
348
|
-
let k = j;
|
|
349
|
-
while (k < command.length) {
|
|
350
|
-
const c = command[k];
|
|
351
|
-
if (targetEscaped) {
|
|
352
|
-
targetEscaped = false;
|
|
353
|
-
target += c;
|
|
354
|
-
k++;
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (c === "\\") {
|
|
358
|
-
targetEscaped = true;
|
|
359
|
-
k++;
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (c === "'" && !targetInDoubleQuote) {
|
|
363
|
-
targetInSingleQuote = !targetInSingleQuote;
|
|
364
|
-
k++;
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
if (c === '"' && !targetInSingleQuote) {
|
|
368
|
-
targetInDoubleQuote = !targetInDoubleQuote;
|
|
369
|
-
k++;
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
if (!targetInSingleQuote && !targetInDoubleQuote && /\s/.test(c)) {
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
target += c;
|
|
376
|
-
k++;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// If the target is exactly /dev/null, we ignore this redirection
|
|
380
|
-
if (target === "/dev/null") {
|
|
381
|
-
i = k - 1; // Move the main loop index to the end of the target
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Ignore file descriptor redirections like 2>&1, >&2, etc.
|
|
386
|
-
if (target.startsWith("&") && /^\d+$/.test(target.substring(1))) {
|
|
387
|
-
i = k - 1;
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return true;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
400
|
-
*/
|
|
401
|
-
export function hasHeredoc(command: string): boolean {
|
|
402
|
-
let inSingleQuote = false;
|
|
403
|
-
let inDoubleQuote = false;
|
|
404
|
-
let escaped = false;
|
|
405
|
-
|
|
406
|
-
for (let i = 0; i < command.length; i++) {
|
|
407
|
-
const char = command[i];
|
|
408
|
-
|
|
409
|
-
if (escaped) {
|
|
410
|
-
escaped = false;
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (char === "\\") {
|
|
415
|
-
escaped = true;
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (char === "'" && !inDoubleQuote) {
|
|
420
|
-
inSingleQuote = !inSingleQuote;
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (char === '"' && !inSingleQuote) {
|
|
425
|
-
inDoubleQuote = !inDoubleQuote;
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (char === "<" && command[i + 1] === "<") {
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
|
|
443
|
-
*/
|
|
444
|
-
export function isBashHeredocWrite(command: string): boolean {
|
|
445
|
-
return hasHeredoc(command) && hasWriteRedirections(command);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
450
|
-
* and should not have persistent permissions.
|
|
451
|
-
*/
|
|
452
|
-
export const DANGEROUS_COMMANDS = [
|
|
453
|
-
"rm",
|
|
454
|
-
"mv",
|
|
455
|
-
"chmod",
|
|
456
|
-
"chown",
|
|
457
|
-
"sh",
|
|
458
|
-
"bash",
|
|
459
|
-
"zsh",
|
|
460
|
-
"fish",
|
|
461
|
-
"pwsh",
|
|
462
|
-
"cmd.exe",
|
|
463
|
-
"powershell.exe",
|
|
464
|
-
"sudo",
|
|
465
|
-
"dd",
|
|
466
|
-
"apt",
|
|
467
|
-
"apt-get",
|
|
468
|
-
"yum",
|
|
469
|
-
"dnf",
|
|
470
|
-
"ssh",
|
|
471
|
-
"scp",
|
|
472
|
-
"sftp",
|
|
473
|
-
"ftp",
|
|
474
|
-
"telnet",
|
|
475
|
-
"nc",
|
|
476
|
-
"netcat",
|
|
477
|
-
];
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Read-only command set: commands that only read/transform data and write to stdout.
|
|
481
|
-
* When a command in this set is used without write redirections, command substitution,
|
|
482
|
-
* or dangerous flags (e.g. sed -i), it is auto-allowed without a confirmation dialog.
|
|
483
|
-
* Aligned with Claude Code's SEMANTIC_READ_ONLY_COMMANDS, excluding interactive pagers
|
|
484
|
-
* (less, more, man, info), command executors (xargs), and infinite-output generators (yes).
|
|
485
|
-
* FR-019.2 through FR-019.7 in tool-permission-system.md.
|
|
486
|
-
*/
|
|
487
|
-
export const READ_ONLY_COMMANDS = [
|
|
488
|
-
"ls",
|
|
489
|
-
"cat",
|
|
490
|
-
"head",
|
|
491
|
-
"tail",
|
|
492
|
-
"wc",
|
|
493
|
-
"sort",
|
|
494
|
-
"uniq",
|
|
495
|
-
"grep",
|
|
496
|
-
"egrep",
|
|
497
|
-
"fgrep",
|
|
498
|
-
"rg",
|
|
499
|
-
"find",
|
|
500
|
-
"which",
|
|
501
|
-
"whereis",
|
|
502
|
-
"file",
|
|
503
|
-
"stat",
|
|
504
|
-
"du",
|
|
505
|
-
"df",
|
|
506
|
-
"free",
|
|
507
|
-
"uptime",
|
|
508
|
-
"uname",
|
|
509
|
-
"hostname",
|
|
510
|
-
"whoami",
|
|
511
|
-
"id",
|
|
512
|
-
"groups",
|
|
513
|
-
"env",
|
|
514
|
-
"printenv",
|
|
515
|
-
"echo",
|
|
516
|
-
"printf",
|
|
517
|
-
"date",
|
|
518
|
-
"true",
|
|
519
|
-
"false",
|
|
520
|
-
"pwd",
|
|
521
|
-
"tree",
|
|
522
|
-
"diff",
|
|
523
|
-
"cmp",
|
|
524
|
-
"md5sum",
|
|
525
|
-
"sha256sum",
|
|
526
|
-
"sha1sum",
|
|
527
|
-
"xxd",
|
|
528
|
-
"od",
|
|
529
|
-
"hexdump",
|
|
530
|
-
"strings",
|
|
531
|
-
"readlink",
|
|
532
|
-
"realpath",
|
|
533
|
-
"basename",
|
|
534
|
-
"dirname",
|
|
535
|
-
"seq",
|
|
536
|
-
"column",
|
|
537
|
-
"jq",
|
|
538
|
-
"yq",
|
|
539
|
-
"cut",
|
|
540
|
-
"paste",
|
|
541
|
-
"tr",
|
|
542
|
-
"awk",
|
|
543
|
-
"sed",
|
|
544
|
-
"test",
|
|
545
|
-
"expr",
|
|
546
|
-
"bc",
|
|
547
|
-
"sleep",
|
|
548
|
-
];
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Registry of commands and their expected subcommand depth for smart prefix extraction.
|
|
552
|
-
* For example, 'git: 2' means 'git commit' is a valid prefix, but 'git' alone is not.
|
|
553
|
-
* Multi-word keys can be used for more specific rules.
|
|
554
|
-
*/
|
|
555
|
-
export interface ToolRule {
|
|
556
|
-
depth: number;
|
|
557
|
-
scopeFlags?: string[];
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
export const TOOL_RULES: Record<string, ToolRule> = {
|
|
561
|
-
// Node/JS
|
|
562
|
-
npm: { depth: 2, scopeFlags: ["--prefix", "-C", "--registry"] },
|
|
563
|
-
"npm run": { depth: 3, scopeFlags: ["--prefix", "-C", "--registry"] },
|
|
564
|
-
pnpm: { depth: 2, scopeFlags: ["-C", "--dir", "-F", "--filter"] },
|
|
565
|
-
"pnpm run": { depth: 3, scopeFlags: ["-C", "--dir", "-F", "--filter"] },
|
|
566
|
-
yarn: { depth: 2, scopeFlags: ["workspace", "--cwd"] },
|
|
567
|
-
"yarn run": { depth: 3, scopeFlags: ["workspace", "--cwd"] },
|
|
568
|
-
"yarn workspace": { depth: 4, scopeFlags: ["--cwd"] },
|
|
569
|
-
bun: { depth: 2 },
|
|
570
|
-
"bun run": { depth: 3 },
|
|
571
|
-
deno: { depth: 2 },
|
|
572
|
-
"deno run": { depth: 3 },
|
|
573
|
-
"deno task": { depth: 3 },
|
|
574
|
-
|
|
575
|
-
// Git
|
|
576
|
-
git: {
|
|
577
|
-
depth: 2,
|
|
578
|
-
scopeFlags: ["-C", "-c", "--directory", "--work-tree", "--git-dir"],
|
|
579
|
-
},
|
|
580
|
-
|
|
581
|
-
// Python
|
|
582
|
-
python: { depth: 2 },
|
|
583
|
-
python3: { depth: 2 },
|
|
584
|
-
"python -m": { depth: 2 },
|
|
585
|
-
"python3 -m": { depth: 2 },
|
|
586
|
-
"python -m pip install": { depth: 3 },
|
|
587
|
-
"python3 -m pip install": { depth: 3 },
|
|
588
|
-
pip: { depth: 2 },
|
|
589
|
-
pip3: { depth: 2 },
|
|
590
|
-
poetry: { depth: 2 },
|
|
591
|
-
conda: { depth: 2 },
|
|
592
|
-
|
|
593
|
-
// Java
|
|
594
|
-
mvn: { depth: 2 },
|
|
595
|
-
gradle: { depth: 2 },
|
|
596
|
-
java: { depth: 1 },
|
|
597
|
-
"java -jar": { depth: 1 },
|
|
598
|
-
|
|
599
|
-
// Rust & Go
|
|
600
|
-
cargo: { depth: 2 },
|
|
601
|
-
go: { depth: 2 },
|
|
602
|
-
|
|
603
|
-
// Containers & Infrastructure
|
|
604
|
-
docker: { depth: 2 },
|
|
605
|
-
"docker-compose": { depth: 2 },
|
|
606
|
-
kubectl: { depth: 2 },
|
|
607
|
-
terraform: { depth: 2 },
|
|
608
|
-
gcloud: { depth: 2 },
|
|
609
|
-
"gcloud compute": { depth: 4 },
|
|
610
|
-
"gcloud container": { depth: 4 },
|
|
611
|
-
aws: { depth: 2 },
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Registry of dangerous subcommands for specific tools.
|
|
616
|
-
*/
|
|
617
|
-
export const DANGEROUS_SUBCOMMANDS: Record<string, string[]> = {
|
|
618
|
-
docker: ["rm", "rmi", "system", "volume", "network", "image", "container"],
|
|
619
|
-
git: ["reset", "clean"],
|
|
620
|
-
npm: ["uninstall", "un", "remove", "rm"],
|
|
621
|
-
pnpm: ["uninstall", "un", "remove", "rm"],
|
|
622
|
-
yarn: ["remove"],
|
|
623
|
-
deno: ["uninstall"],
|
|
624
|
-
bun: ["remove", "rm"],
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Heuristic to determine if a flag takes an argument.
|
|
629
|
-
* If nextArg doesn't start with '-' and isn't a known subcommand, assume it's a flag value.
|
|
630
|
-
*/
|
|
631
|
-
function flagTakesArg(flag: string, nextArg: string | undefined): boolean {
|
|
632
|
-
if (!nextArg) return false;
|
|
633
|
-
if (nextArg.startsWith("-")) return false;
|
|
634
|
-
// If it's a common subcommand, it's probably not a flag argument
|
|
635
|
-
const commonSubcommands = [
|
|
636
|
-
"install",
|
|
637
|
-
"add",
|
|
638
|
-
"remove",
|
|
639
|
-
"run",
|
|
640
|
-
"test",
|
|
641
|
-
"build",
|
|
642
|
-
"status",
|
|
643
|
-
"diff",
|
|
644
|
-
"commit",
|
|
645
|
-
"push",
|
|
646
|
-
"pull",
|
|
647
|
-
"checkout",
|
|
648
|
-
"log",
|
|
649
|
-
"fetch",
|
|
650
|
-
"merge",
|
|
651
|
-
"rebase",
|
|
652
|
-
];
|
|
653
|
-
if (commonSubcommands.includes(nextArg)) return false;
|
|
654
|
-
return true;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Detects if an argument is a file path or URL.
|
|
659
|
-
*/
|
|
660
|
-
function shouldStopAtArg(arg: string): boolean {
|
|
661
|
-
if (!arg) return false;
|
|
662
|
-
// URLs
|
|
663
|
-
if (/^(https?|ftp|ssh|git):\/\//.test(arg)) return true;
|
|
664
|
-
// File paths (starts with /, ./, ../, or ~/)
|
|
665
|
-
if (
|
|
666
|
-
arg.startsWith("/") ||
|
|
667
|
-
arg.startsWith("./") ||
|
|
668
|
-
arg.startsWith("../") ||
|
|
669
|
-
arg.startsWith("~/")
|
|
670
|
-
)
|
|
671
|
-
return true;
|
|
672
|
-
// Common file extensions (but not scoped packages or common subcommands)
|
|
673
|
-
if (
|
|
674
|
-
/\.(ts|js|py|sh|md|txt|json|yml|yaml|html|css|go|rs|java|cpp|c|h|php|rb|pl|sql)$/.test(
|
|
675
|
-
arg,
|
|
676
|
-
) &&
|
|
677
|
-
!arg.includes("@") &&
|
|
678
|
-
!arg.includes("/")
|
|
679
|
-
)
|
|
680
|
-
return true;
|
|
681
|
-
return false;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Checks if a find command is dangerous (e.g., contains -exec, -delete, etc.).
|
|
686
|
-
*/
|
|
687
|
-
export function isDangerousFind(command: string): boolean {
|
|
688
|
-
const stripped = stripRedirections(stripEnvVars(command));
|
|
689
|
-
const tokens = stripped.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
690
|
-
if (tokens.length === 0 || tokens[0] !== "find") return false;
|
|
691
|
-
|
|
692
|
-
const dangerousFlags = [
|
|
693
|
-
"-exec",
|
|
694
|
-
"-execdir",
|
|
695
|
-
"-ok",
|
|
696
|
-
"-okdir",
|
|
697
|
-
"-delete",
|
|
698
|
-
"-fprint",
|
|
699
|
-
"-fprint0",
|
|
700
|
-
"-fprintf",
|
|
701
|
-
];
|
|
702
|
-
return tokens.some((token) => {
|
|
703
|
-
const unquoted = token.replace(/^(['"])(.*)\1$/, "$2");
|
|
704
|
-
return dangerousFlags.includes(unquoted);
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Detects command substitution $(...) or backticks `...` in a command string.
|
|
710
|
-
* Commands with substitution are never auto-allowed because the substituted
|
|
711
|
-
* command may be dangerous (e.g. cat $(rm x)). FR-019.6.
|
|
712
|
-
*/
|
|
713
|
-
export function hasCommandSubstitution(command: string): boolean {
|
|
714
|
-
// Remove quoted strings first so $() or backticks inside quotes don't trigger
|
|
715
|
-
const stripped = command
|
|
716
|
-
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
717
|
-
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
718
|
-
return /\$\([^)]*\)/.test(stripped) || /`[^`]*`/.test(stripped);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Detects process substitution <(...) or >(...) in a command string.
|
|
723
|
-
* These can execute side effects and are never auto-allowed. FR-019.6.
|
|
724
|
-
*/
|
|
725
|
-
export function hasProcessSubstitution(command: string): boolean {
|
|
726
|
-
const stripped = command
|
|
727
|
-
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
728
|
-
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
729
|
-
return /[<>]\([^)]*\)/.test(stripped);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Detects sed in-place edit flag (-i, with optional backup suffix like -i.bak).
|
|
734
|
-
* sed -i modifies files in place and must NOT be auto-allowed. FR-019.5.
|
|
735
|
-
*/
|
|
736
|
-
export function hasSedInPlace(command: string): boolean {
|
|
737
|
-
const stripped = stripRedirections(stripEnvVars(command));
|
|
738
|
-
const tokens = stripped.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
739
|
-
if (tokens.length === 0 || tokens[0] !== "sed") return false;
|
|
740
|
-
return tokens.some((token) => /^-i(\..*)?$/.test(token));
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* Extracts a "smart prefix" from a bash command based on common developer tools.
|
|
745
|
-
* Returns null if the command is blacklisted or cannot be safely prefix-matched.
|
|
746
|
-
*/
|
|
747
|
-
export function getSmartPrefix(command: string): string | null {
|
|
748
|
-
const parts = splitBashCommand(command);
|
|
749
|
-
if (parts.length === 0) return null;
|
|
750
|
-
|
|
751
|
-
// For now, we only support prefix matching for single commands or the first command in a chain
|
|
752
|
-
// to keep it simple and safe.
|
|
753
|
-
const firstCommand = parts[0];
|
|
754
|
-
|
|
755
|
-
const stripped = stripRedirections(stripEnvVars(firstCommand));
|
|
756
|
-
const tokens = stripped.split(/\s+/);
|
|
757
|
-
if (tokens.length === 0) return null;
|
|
758
|
-
|
|
759
|
-
const prefixParts: string[] = [];
|
|
760
|
-
let i = 0;
|
|
761
|
-
|
|
762
|
-
// Handle prefix tools like sudo
|
|
763
|
-
const prefixTools = ["sudo", "time", "stdbuf", "timeout"];
|
|
764
|
-
while (i < tokens.length && prefixTools.includes(tokens[i])) {
|
|
765
|
-
prefixParts.push(tokens[i]);
|
|
766
|
-
i++;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (i >= tokens.length) return null;
|
|
770
|
-
|
|
771
|
-
const exe = tokens[i];
|
|
772
|
-
// Blacklist - Hard blacklist for dangerous commands
|
|
773
|
-
if (DANGEROUS_COMMANDS.includes(exe)) return null;
|
|
774
|
-
|
|
775
|
-
// Find the longest matching rule in TOOL_RULES
|
|
776
|
-
let bestRuleKey = "";
|
|
777
|
-
let rule: ToolRule | undefined;
|
|
778
|
-
|
|
779
|
-
for (const [key, r] of Object.entries(TOOL_RULES)) {
|
|
780
|
-
const keyTokens = key.split(/\s+/);
|
|
781
|
-
let match = true;
|
|
782
|
-
for (let j = 0; j < keyTokens.length; j++) {
|
|
783
|
-
if (tokens[i + j] !== keyTokens[j]) {
|
|
784
|
-
match = false;
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
if (match && key.length > bestRuleKey.length) {
|
|
789
|
-
bestRuleKey = key;
|
|
790
|
-
rule = r;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// If no rule found, we don't suggest a prefix
|
|
795
|
-
if (!rule) return null;
|
|
796
|
-
|
|
797
|
-
const depth = rule.depth;
|
|
798
|
-
const scopeFlags = rule.scopeFlags || [];
|
|
799
|
-
let currentDepth = 0;
|
|
800
|
-
|
|
801
|
-
// Safety check: only allow safe subcommands for git
|
|
802
|
-
const safeGitSubcommands = [
|
|
803
|
-
"commit",
|
|
804
|
-
"push",
|
|
805
|
-
"pull",
|
|
806
|
-
"checkout",
|
|
807
|
-
"add",
|
|
808
|
-
"status",
|
|
809
|
-
"diff",
|
|
810
|
-
"branch",
|
|
811
|
-
"merge",
|
|
812
|
-
"rebase",
|
|
813
|
-
"log",
|
|
814
|
-
"fetch",
|
|
815
|
-
"remote",
|
|
816
|
-
"stash",
|
|
817
|
-
];
|
|
818
|
-
|
|
819
|
-
const destructiveGitFlags = [
|
|
820
|
-
"-d",
|
|
821
|
-
"-D",
|
|
822
|
-
"--delete",
|
|
823
|
-
"--hard",
|
|
824
|
-
"--force",
|
|
825
|
-
"-f",
|
|
826
|
-
];
|
|
827
|
-
|
|
828
|
-
// Global safety check: scan ALL tokens for dangerous flags/subcommands
|
|
829
|
-
for (let j = i; j < tokens.length; j++) {
|
|
830
|
-
const token = tokens[j];
|
|
831
|
-
if (token.startsWith("-")) {
|
|
832
|
-
if (exe === "git" && destructiveGitFlags.includes(token)) return null;
|
|
833
|
-
} else {
|
|
834
|
-
if (DANGEROUS_SUBCOMMANDS[exe]?.includes(token)) return null;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Include all tokens from the best matching rule
|
|
839
|
-
const ruleTokens = bestRuleKey.split(/\s+/);
|
|
840
|
-
for (let j = 0; j < ruleTokens.length; j++) {
|
|
841
|
-
const token = tokens[i];
|
|
842
|
-
if (!token) break;
|
|
843
|
-
|
|
844
|
-
if (token.startsWith("-")) {
|
|
845
|
-
if (exe === "git" && destructiveGitFlags.includes(token)) return null;
|
|
846
|
-
} else {
|
|
847
|
-
if (DANGEROUS_SUBCOMMANDS[exe]?.includes(token)) return null;
|
|
848
|
-
if (
|
|
849
|
-
exe === "git" &&
|
|
850
|
-
currentDepth > 0 &&
|
|
851
|
-
!safeGitSubcommands.includes(token)
|
|
852
|
-
) {
|
|
853
|
-
return null;
|
|
854
|
-
}
|
|
855
|
-
currentDepth++;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
prefixParts.push(token);
|
|
859
|
-
i++;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// Continue until we reach the required depth
|
|
863
|
-
while (i < tokens.length && currentDepth < depth) {
|
|
864
|
-
const token = tokens[i];
|
|
865
|
-
|
|
866
|
-
if (token.startsWith("-")) {
|
|
867
|
-
// Safety checks for flags
|
|
868
|
-
if (exe === "git" && destructiveGitFlags.includes(token)) return null;
|
|
869
|
-
|
|
870
|
-
prefixParts.push(token);
|
|
871
|
-
if (scopeFlags.includes(token) || flagTakesArg(token, tokens[i + 1])) {
|
|
872
|
-
if (i + 1 < tokens.length) {
|
|
873
|
-
prefixParts.push(tokens[++i]);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
} else {
|
|
877
|
-
// Safety checks for subcommands
|
|
878
|
-
if (DANGEROUS_SUBCOMMANDS[exe]?.includes(token)) return null;
|
|
879
|
-
if (
|
|
880
|
-
exe === "git" &&
|
|
881
|
-
currentDepth > 0 &&
|
|
882
|
-
!safeGitSubcommands.includes(token)
|
|
883
|
-
) {
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Stop at data/paths
|
|
888
|
-
if (shouldStopAtArg(token) && currentDepth > 0) break;
|
|
889
|
-
|
|
890
|
-
prefixParts.push(token);
|
|
891
|
-
currentDepth++;
|
|
892
|
-
}
|
|
893
|
-
i++;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (currentDepth < depth) return null;
|
|
897
|
-
|
|
898
|
-
return prefixParts.join(" ");
|
|
899
|
-
}
|