wave-agent-sdk 0.0.10 → 0.0.11
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.map +1 -1
- package/dist/agent.js +25 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/permissionManager.d.ts +9 -0
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +166 -6
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +66 -27
- package/dist/types/permissions.d.ts +4 -0
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/utils/bashParser.d.ts +24 -0
- package/dist/utils/bashParser.d.ts.map +1 -0
- package/dist/utils/bashParser.js +413 -0
- package/dist/utils/pathSafety.d.ts +10 -0
- package/dist/utils/pathSafety.d.ts.map +1 -0
- package/dist/utils/pathSafety.js +23 -0
- package/package.json +5 -2
- package/src/agent.ts +28 -13
- package/src/index.ts +1 -0
- package/src/managers/permissionManager.ts +210 -6
- package/src/tools/bashTool.ts +72 -32
- package/src/types/permissions.ts +4 -0
- package/src/utils/bashParser.ts +444 -0
- package/src/utils/pathSafety.ts +26 -0
- package/dist/utils/largeOutputHandler.d.ts +0 -15
- package/dist/utils/largeOutputHandler.d.ts.map +0 -1
- package/dist/utils/largeOutputHandler.js +0 -40
- package/dist/utils/tokenEstimator.d.ts +0 -39
- package/dist/utils/tokenEstimator.d.ts.map +0 -1
- package/dist/utils/tokenEstimator.js +0 -55
- package/src/utils/largeOutputHandler.ts +0 -55
- package/src/utils/tokenEstimator.ts +0 -68
|
@@ -0,0 +1,444 @@
|
|
|
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 !== ">") opLen = 1;
|
|
62
|
+
|
|
63
|
+
if (opLen > 0) {
|
|
64
|
+
// Check if preceded by an odd number of backslashes
|
|
65
|
+
let backslashCount = 0;
|
|
66
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
67
|
+
if (command[j] === "\\") backslashCount++;
|
|
68
|
+
else break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ALSO check if preceded by an escaped operator character (e.g., \&&)
|
|
72
|
+
let precededByEscapedOp = false;
|
|
73
|
+
if (i > 0 && /[&|;]/.test(command[i - 1])) {
|
|
74
|
+
let bsCount = 0;
|
|
75
|
+
for (let j = i - 2; j >= 0; j--) {
|
|
76
|
+
if (command[j] === "\\") bsCount++;
|
|
77
|
+
else break;
|
|
78
|
+
}
|
|
79
|
+
if (bsCount % 2 !== 0) precededByEscapedOp = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (backslashCount % 2 === 0 && !precededByEscapedOp) {
|
|
83
|
+
splitPositions.push(i, i + opLen);
|
|
84
|
+
i += opLen - 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let lastPos = 0;
|
|
90
|
+
const parts: string[] = [];
|
|
91
|
+
for (let i = 0; i < splitPositions.length; i += 2) {
|
|
92
|
+
const start = splitPositions[i];
|
|
93
|
+
const end = splitPositions[i + 1];
|
|
94
|
+
const part = command.substring(lastPos, start).trim();
|
|
95
|
+
if (part) parts.push(part);
|
|
96
|
+
lastPos = end;
|
|
97
|
+
}
|
|
98
|
+
const lastPart = command.substring(lastPos).trim();
|
|
99
|
+
if (lastPart) parts.push(lastPart);
|
|
100
|
+
|
|
101
|
+
const finalResult: string[] = [];
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
const stripped = stripRedirections(stripEnvVars(part));
|
|
104
|
+
if (stripped.startsWith("(") && stripped.endsWith(")")) {
|
|
105
|
+
const inner = stripped.substring(1, stripped.length - 1).trim();
|
|
106
|
+
if (inner) {
|
|
107
|
+
finalResult.push(...splitBashCommand(inner));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
finalResult.push(part);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return finalResult;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Removes inline environment variable assignments (e.g., VAR=val cmd -> cmd).
|
|
119
|
+
*/
|
|
120
|
+
export function stripEnvVars(command: string): string {
|
|
121
|
+
let result = command.trim();
|
|
122
|
+
while (true) {
|
|
123
|
+
const match = result.match(/^([a-zA-Z_][a-zA-Z0-9_]*)=/);
|
|
124
|
+
if (!match) break;
|
|
125
|
+
|
|
126
|
+
const varNameEnd = match[0].length;
|
|
127
|
+
let valueEnd = varNameEnd;
|
|
128
|
+
|
|
129
|
+
if (result[varNameEnd] === "'") {
|
|
130
|
+
valueEnd = result.indexOf("'", varNameEnd + 1);
|
|
131
|
+
if (valueEnd === -1) break;
|
|
132
|
+
valueEnd++;
|
|
133
|
+
} else if (result[varNameEnd] === '"') {
|
|
134
|
+
let escaped = false;
|
|
135
|
+
let found = false;
|
|
136
|
+
for (let i = varNameEnd + 1; i < result.length; i++) {
|
|
137
|
+
if (escaped) {
|
|
138
|
+
escaped = false;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (result[i] === "\\") {
|
|
142
|
+
escaped = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (result[i] === '"') {
|
|
146
|
+
valueEnd = i + 1;
|
|
147
|
+
found = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!found) break;
|
|
152
|
+
} else {
|
|
153
|
+
const spaceIndex = result.search(/\s/);
|
|
154
|
+
if (spaceIndex === -1) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
valueEnd = spaceIndex;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
result = result.substring(valueEnd).trim();
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Removes redirections (e.g., echo "data" > output.txt -> echo "data").
|
|
167
|
+
*/
|
|
168
|
+
export function stripRedirections(command: string): string {
|
|
169
|
+
let result = "";
|
|
170
|
+
let inSingleQuote = false;
|
|
171
|
+
let inDoubleQuote = false;
|
|
172
|
+
let escaped = false;
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < command.length; i++) {
|
|
175
|
+
const char = command[i];
|
|
176
|
+
|
|
177
|
+
if (escaped) {
|
|
178
|
+
result += char;
|
|
179
|
+
escaped = false;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (char === "\\") {
|
|
184
|
+
result += char;
|
|
185
|
+
escaped = true;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (char === "'" && !inDoubleQuote) {
|
|
190
|
+
inSingleQuote = !inSingleQuote;
|
|
191
|
+
result += char;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (char === '"' && !inSingleQuote) {
|
|
196
|
+
inDoubleQuote = !inDoubleQuote;
|
|
197
|
+
result += char;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
202
|
+
result += char;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle whitespace outside quotes: collapse multiple spaces into one
|
|
207
|
+
if (/\s/.test(char)) {
|
|
208
|
+
if (result.length > 0 && !/\s/.test(result[result.length - 1])) {
|
|
209
|
+
result += " ";
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check for redirection
|
|
215
|
+
if (char === ">" || char === "<") {
|
|
216
|
+
// Check if preceded by a digit or & (for 2> or &>)
|
|
217
|
+
if (result.length > 0 && /[0-9&]/.test(result[result.length - 1])) {
|
|
218
|
+
// Ensure it's at the start of a word or preceded by whitespace
|
|
219
|
+
if (result.length === 1 || /\s/.test(result[result.length - 2])) {
|
|
220
|
+
// Remove the digit/& from result
|
|
221
|
+
result = result.substring(0, result.length - 1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let end = i + 1;
|
|
226
|
+
if (command[end] === char) {
|
|
227
|
+
end++;
|
|
228
|
+
if (char === "<" && command[end] === "-") {
|
|
229
|
+
end++;
|
|
230
|
+
}
|
|
231
|
+
} else if (
|
|
232
|
+
command[end] === "&" ||
|
|
233
|
+
(char === ">" && command[end] === "|")
|
|
234
|
+
) {
|
|
235
|
+
end++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Skip whitespace after operator
|
|
239
|
+
while (end < command.length && /\s/.test(command[end])) {
|
|
240
|
+
end++;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Skip the following word (the target of redirection)
|
|
244
|
+
let wordEscaped = false;
|
|
245
|
+
let wordInSingleQuote = false;
|
|
246
|
+
let wordInDoubleQuote = false;
|
|
247
|
+
while (end < command.length) {
|
|
248
|
+
const c = command[end];
|
|
249
|
+
if (wordEscaped) {
|
|
250
|
+
wordEscaped = false;
|
|
251
|
+
end++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (c === "\\") {
|
|
255
|
+
wordEscaped = true;
|
|
256
|
+
end++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (c === "'" && !wordInDoubleQuote) {
|
|
260
|
+
wordInSingleQuote = !wordInSingleQuote;
|
|
261
|
+
end++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (c === '"' && !wordInSingleQuote) {
|
|
265
|
+
wordInDoubleQuote = !wordInDoubleQuote;
|
|
266
|
+
end++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (!wordInSingleQuote && !wordInDoubleQuote && /\s/.test(c)) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
end++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
i = end - 1;
|
|
276
|
+
// After stripping a redirection, ensure there's a space if we're not at the end
|
|
277
|
+
if (result.length > 0 && !/\s/.test(result[result.length - 1])) {
|
|
278
|
+
result += " ";
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
result += char;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return result.trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Blacklist of dangerous commands that should not be safely prefix-matched
|
|
291
|
+
* and should not have persistent permissions.
|
|
292
|
+
*/
|
|
293
|
+
export const DANGEROUS_COMMANDS = [
|
|
294
|
+
"rm",
|
|
295
|
+
"mv",
|
|
296
|
+
"chmod",
|
|
297
|
+
"chown",
|
|
298
|
+
"sh",
|
|
299
|
+
"bash",
|
|
300
|
+
"sudo",
|
|
301
|
+
"dd",
|
|
302
|
+
"apt",
|
|
303
|
+
"apt-get",
|
|
304
|
+
"yum",
|
|
305
|
+
"dnf",
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Extracts a "smart prefix" from a bash command based on common developer tools.
|
|
310
|
+
* Returns null if the command is blacklisted or cannot be safely prefix-matched.
|
|
311
|
+
*/
|
|
312
|
+
export function getSmartPrefix(command: string): string | null {
|
|
313
|
+
const parts = splitBashCommand(command);
|
|
314
|
+
if (parts.length === 0) return null;
|
|
315
|
+
|
|
316
|
+
// For now, we only support prefix matching for single commands or the first command in a chain
|
|
317
|
+
// to keep it simple and safe.
|
|
318
|
+
const firstCommand = parts[0];
|
|
319
|
+
let stripped = stripRedirections(stripEnvVars(firstCommand));
|
|
320
|
+
|
|
321
|
+
// Handle sudo
|
|
322
|
+
if (stripped.startsWith("sudo ")) {
|
|
323
|
+
stripped = stripped.substring(5).trim();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const tokens = stripped.split(/\s+/);
|
|
327
|
+
if (tokens.length === 0) return null;
|
|
328
|
+
|
|
329
|
+
const exe = tokens[0];
|
|
330
|
+
const sub = tokens[1];
|
|
331
|
+
|
|
332
|
+
// Blacklist - Hard blacklist for dangerous commands
|
|
333
|
+
if (DANGEROUS_COMMANDS.includes(exe)) return null;
|
|
334
|
+
|
|
335
|
+
// Node/JS
|
|
336
|
+
if (["npm", "pnpm", "yarn", "deno", "bun"].includes(exe)) {
|
|
337
|
+
if (
|
|
338
|
+
[
|
|
339
|
+
"install",
|
|
340
|
+
"i",
|
|
341
|
+
"add",
|
|
342
|
+
"remove",
|
|
343
|
+
"test",
|
|
344
|
+
"t",
|
|
345
|
+
"build",
|
|
346
|
+
"start",
|
|
347
|
+
"dev",
|
|
348
|
+
].includes(sub)
|
|
349
|
+
) {
|
|
350
|
+
return `${exe} ${sub}`;
|
|
351
|
+
}
|
|
352
|
+
if (sub === "run" && tokens[2]) {
|
|
353
|
+
return `${exe} run ${tokens[2]}`;
|
|
354
|
+
}
|
|
355
|
+
return exe;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Git
|
|
359
|
+
if (exe === "git") {
|
|
360
|
+
if (
|
|
361
|
+
[
|
|
362
|
+
"commit",
|
|
363
|
+
"push",
|
|
364
|
+
"pull",
|
|
365
|
+
"checkout",
|
|
366
|
+
"add",
|
|
367
|
+
"status",
|
|
368
|
+
"diff",
|
|
369
|
+
"branch",
|
|
370
|
+
"merge",
|
|
371
|
+
"rebase",
|
|
372
|
+
"log",
|
|
373
|
+
"fetch",
|
|
374
|
+
"remote",
|
|
375
|
+
"stash",
|
|
376
|
+
].includes(sub)
|
|
377
|
+
) {
|
|
378
|
+
return `${exe} ${sub}`;
|
|
379
|
+
}
|
|
380
|
+
return exe;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Python
|
|
384
|
+
if (["python", "python3", "pip", "pip3", "poetry", "conda"].includes(exe)) {
|
|
385
|
+
if (exe === "python" || exe === "python3") {
|
|
386
|
+
if (sub === "-m" && tokens[2] === "pip" && tokens[3] === "install") {
|
|
387
|
+
return `${exe} -m pip install`;
|
|
388
|
+
}
|
|
389
|
+
return exe;
|
|
390
|
+
}
|
|
391
|
+
if (["install", "add", "remove", "test", "run"].includes(sub)) {
|
|
392
|
+
return `${exe} ${sub}`;
|
|
393
|
+
}
|
|
394
|
+
return exe;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Java
|
|
398
|
+
if (["mvn", "gradle"].includes(exe)) {
|
|
399
|
+
if (sub && !sub.startsWith("-")) {
|
|
400
|
+
return `${exe} ${sub}`;
|
|
401
|
+
}
|
|
402
|
+
return exe;
|
|
403
|
+
}
|
|
404
|
+
if (exe === "java") {
|
|
405
|
+
if (sub === "-jar") return "java -jar";
|
|
406
|
+
return "java";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Rust & Go
|
|
410
|
+
if (exe === "cargo") {
|
|
411
|
+
if (["build", "test", "run", "add", "check"].includes(sub)) {
|
|
412
|
+
return `${exe} ${sub}`;
|
|
413
|
+
}
|
|
414
|
+
return exe;
|
|
415
|
+
}
|
|
416
|
+
if (exe === "go") {
|
|
417
|
+
if (["build", "test", "run", "get", "mod"].includes(sub)) {
|
|
418
|
+
return `${exe} ${sub}`;
|
|
419
|
+
}
|
|
420
|
+
return exe;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Containers & Infrastructure
|
|
424
|
+
if (exe === "docker" || exe === "docker-compose") {
|
|
425
|
+
if (["run", "build", "ps", "exec", "up", "down"].includes(sub)) {
|
|
426
|
+
return `${exe} ${sub}`;
|
|
427
|
+
}
|
|
428
|
+
return exe;
|
|
429
|
+
}
|
|
430
|
+
if (exe === "kubectl") {
|
|
431
|
+
if (["get", "describe", "apply", "logs"].includes(sub)) {
|
|
432
|
+
return `${exe} ${sub}`;
|
|
433
|
+
}
|
|
434
|
+
return exe;
|
|
435
|
+
}
|
|
436
|
+
if (exe === "terraform") {
|
|
437
|
+
if (["plan", "apply", "destroy", "init"].includes(sub)) {
|
|
438
|
+
return `${exe} ${sub}`;
|
|
439
|
+
}
|
|
440
|
+
return exe;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a target path is inside a parent directory.
|
|
6
|
+
* Resolves symlinks and handles absolute paths.
|
|
7
|
+
* Returns true if target is the same as parent or is a subdirectory of parent.
|
|
8
|
+
* @param target The path to check
|
|
9
|
+
* @param parent The parent directory path
|
|
10
|
+
* @returns boolean
|
|
11
|
+
*/
|
|
12
|
+
export function isPathInside(target: string, parent: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
const absoluteTarget = path.resolve(target);
|
|
15
|
+
const absoluteParent = path.resolve(parent);
|
|
16
|
+
|
|
17
|
+
const realTarget = fs.realpathSync(absoluteTarget);
|
|
18
|
+
const realParent = fs.realpathSync(absoluteParent);
|
|
19
|
+
|
|
20
|
+
const relative = path.relative(realParent, realTarget);
|
|
21
|
+
|
|
22
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare const LARGE_OUTPUT_TOKEN_THRESHOLD = 20000;
|
|
2
|
-
/**
|
|
3
|
-
* Handle large command output by writing to temporary file when token threshold is exceeded
|
|
4
|
-
*
|
|
5
|
-
* Uses token-based threshold (20k tokens) to determine when output should be written to temp file.
|
|
6
|
-
* This provides accurate estimation of actual token cost for LLM processing.
|
|
7
|
-
*
|
|
8
|
-
* @param output - The command output string
|
|
9
|
-
* @returns Object containing processed content and optional file path
|
|
10
|
-
*/
|
|
11
|
-
export declare function handleLargeOutput(output: string): Promise<{
|
|
12
|
-
content: string;
|
|
13
|
-
filePath?: string;
|
|
14
|
-
}>;
|
|
15
|
-
//# sourceMappingURL=largeOutputHandler.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"largeOutputHandler.d.ts","sourceRoot":"","sources":["../../src/utils/largeOutputHandler.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,4BAA4B,QAAQ,CAAC;AAElD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,CA8BD"}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { writeFile } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { logger } from "./globalLogger.js";
|
|
5
|
-
import { estimateTokenCount, getTokenUsageDescription, } from "./tokenEstimator.js";
|
|
6
|
-
// Token threshold for writing output to temp file (20k tokens)
|
|
7
|
-
export const LARGE_OUTPUT_TOKEN_THRESHOLD = 20000;
|
|
8
|
-
/**
|
|
9
|
-
* Handle large command output by writing to temporary file when token threshold is exceeded
|
|
10
|
-
*
|
|
11
|
-
* Uses token-based threshold (20k tokens) to determine when output should be written to temp file.
|
|
12
|
-
* This provides accurate estimation of actual token cost for LLM processing.
|
|
13
|
-
*
|
|
14
|
-
* @param output - The command output string
|
|
15
|
-
* @returns Object containing processed content and optional file path
|
|
16
|
-
*/
|
|
17
|
-
export async function handleLargeOutput(output) {
|
|
18
|
-
const estimatedTokens = estimateTokenCount(output);
|
|
19
|
-
// Check token threshold
|
|
20
|
-
if (estimatedTokens <= LARGE_OUTPUT_TOKEN_THRESHOLD) {
|
|
21
|
-
return { content: output };
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
// Create temp file for large output
|
|
25
|
-
const tempFileName = `bash-output-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.txt`;
|
|
26
|
-
const tempFilePath = join(tmpdir(), tempFileName);
|
|
27
|
-
await writeFile(tempFilePath, output, "utf8");
|
|
28
|
-
const sizeKB = Math.round(output.length / 1024);
|
|
29
|
-
const tokenDescription = getTokenUsageDescription(output, LARGE_OUTPUT_TOKEN_THRESHOLD);
|
|
30
|
-
return {
|
|
31
|
-
content: `Large output (${sizeKB} KB, ${tokenDescription}) written to temporary file. Use the Read tool to access the full content.`,
|
|
32
|
-
filePath: tempFilePath,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
logger.warn(`Failed to write large output to temp file: ${error}`);
|
|
37
|
-
// Fallback to direct output if temp file creation fails
|
|
38
|
-
return { content: output };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple token estimation utility for text content
|
|
3
|
-
*
|
|
4
|
-
* This provides a fast approximation of token count without requiring
|
|
5
|
-
* actual tokenization, which would be expensive for large content.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Estimate the number of tokens in a text string
|
|
9
|
-
*
|
|
10
|
-
* Uses a simple heuristic based on character count and common patterns:
|
|
11
|
-
* - Average token length varies by language and content type
|
|
12
|
-
* - English text: ~4-5 characters per token
|
|
13
|
-
* - Code/structured text: ~3-4 characters per token
|
|
14
|
-
* - Numbers/symbols: ~2-3 characters per token
|
|
15
|
-
*
|
|
16
|
-
* This function uses a conservative estimate of 4 characters per token
|
|
17
|
-
* which works well for mixed content (text + code + symbols).
|
|
18
|
-
*
|
|
19
|
-
* @param text - The text to estimate tokens for
|
|
20
|
-
* @returns Estimated number of tokens
|
|
21
|
-
*/
|
|
22
|
-
export declare function estimateTokenCount(text: string): number;
|
|
23
|
-
/**
|
|
24
|
-
* Check if estimated token count exceeds a threshold
|
|
25
|
-
*
|
|
26
|
-
* @param text - The text to check
|
|
27
|
-
* @param threshold - Token threshold (default: 20,000)
|
|
28
|
-
* @returns True if estimated tokens exceed threshold
|
|
29
|
-
*/
|
|
30
|
-
export declare function exceedsTokenThreshold(text: string, threshold?: number): boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Get a human-readable description of estimated token usage
|
|
33
|
-
*
|
|
34
|
-
* @param text - The text to analyze
|
|
35
|
-
* @param threshold - Token threshold for comparison
|
|
36
|
-
* @returns Description string with token count and threshold info
|
|
37
|
-
*/
|
|
38
|
-
export declare function getTokenUsageDescription(text: string, threshold?: number): string;
|
|
39
|
-
//# sourceMappingURL=tokenEstimator.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tokenEstimator.d.ts","sourceRoot":"","sources":["../../src/utils/tokenEstimator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcvD;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAAc,GACxB,OAAO,CAET;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAAc,GACxB,MAAM,CAKR"}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple token estimation utility for text content
|
|
3
|
-
*
|
|
4
|
-
* This provides a fast approximation of token count without requiring
|
|
5
|
-
* actual tokenization, which would be expensive for large content.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Estimate the number of tokens in a text string
|
|
9
|
-
*
|
|
10
|
-
* Uses a simple heuristic based on character count and common patterns:
|
|
11
|
-
* - Average token length varies by language and content type
|
|
12
|
-
* - English text: ~4-5 characters per token
|
|
13
|
-
* - Code/structured text: ~3-4 characters per token
|
|
14
|
-
* - Numbers/symbols: ~2-3 characters per token
|
|
15
|
-
*
|
|
16
|
-
* This function uses a conservative estimate of 4 characters per token
|
|
17
|
-
* which works well for mixed content (text + code + symbols).
|
|
18
|
-
*
|
|
19
|
-
* @param text - The text to estimate tokens for
|
|
20
|
-
* @returns Estimated number of tokens
|
|
21
|
-
*/
|
|
22
|
-
export function estimateTokenCount(text) {
|
|
23
|
-
if (!text || text.length === 0) {
|
|
24
|
-
return 0;
|
|
25
|
-
}
|
|
26
|
-
// Base estimation: 4 characters per token (conservative)
|
|
27
|
-
const baseEstimate = Math.ceil(text.length / 4);
|
|
28
|
-
// Adjust for whitespace (spaces don't contribute much to token count)
|
|
29
|
-
const whitespaceCount = (text.match(/\s/g) || []).length;
|
|
30
|
-
const adjustedEstimate = Math.ceil((text.length - whitespaceCount * 0.5) / 4);
|
|
31
|
-
// Use the more conservative (higher) estimate
|
|
32
|
-
return Math.max(baseEstimate, adjustedEstimate);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Check if estimated token count exceeds a threshold
|
|
36
|
-
*
|
|
37
|
-
* @param text - The text to check
|
|
38
|
-
* @param threshold - Token threshold (default: 20,000)
|
|
39
|
-
* @returns True if estimated tokens exceed threshold
|
|
40
|
-
*/
|
|
41
|
-
export function exceedsTokenThreshold(text, threshold = 20000) {
|
|
42
|
-
return estimateTokenCount(text) > threshold;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Get a human-readable description of estimated token usage
|
|
46
|
-
*
|
|
47
|
-
* @param text - The text to analyze
|
|
48
|
-
* @param threshold - Token threshold for comparison
|
|
49
|
-
* @returns Description string with token count and threshold info
|
|
50
|
-
*/
|
|
51
|
-
export function getTokenUsageDescription(text, threshold = 20000) {
|
|
52
|
-
const estimatedTokens = estimateTokenCount(text);
|
|
53
|
-
const exceedsThreshold = estimatedTokens > threshold;
|
|
54
|
-
return `${estimatedTokens.toLocaleString()} tokens (${exceedsThreshold ? "exceeds" : "within"} ${threshold.toLocaleString()} limit)`;
|
|
55
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { writeFile } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { logger } from "./globalLogger.js";
|
|
5
|
-
import {
|
|
6
|
-
estimateTokenCount,
|
|
7
|
-
getTokenUsageDescription,
|
|
8
|
-
} from "./tokenEstimator.js";
|
|
9
|
-
|
|
10
|
-
// Token threshold for writing output to temp file (20k tokens)
|
|
11
|
-
export const LARGE_OUTPUT_TOKEN_THRESHOLD = 20000;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Handle large command output by writing to temporary file when token threshold is exceeded
|
|
15
|
-
*
|
|
16
|
-
* Uses token-based threshold (20k tokens) to determine when output should be written to temp file.
|
|
17
|
-
* This provides accurate estimation of actual token cost for LLM processing.
|
|
18
|
-
*
|
|
19
|
-
* @param output - The command output string
|
|
20
|
-
* @returns Object containing processed content and optional file path
|
|
21
|
-
*/
|
|
22
|
-
export async function handleLargeOutput(output: string): Promise<{
|
|
23
|
-
content: string;
|
|
24
|
-
filePath?: string;
|
|
25
|
-
}> {
|
|
26
|
-
const estimatedTokens = estimateTokenCount(output);
|
|
27
|
-
|
|
28
|
-
// Check token threshold
|
|
29
|
-
if (estimatedTokens <= LARGE_OUTPUT_TOKEN_THRESHOLD) {
|
|
30
|
-
return { content: output };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
// Create temp file for large output
|
|
35
|
-
const tempFileName = `bash-output-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.txt`;
|
|
36
|
-
const tempFilePath = join(tmpdir(), tempFileName);
|
|
37
|
-
|
|
38
|
-
await writeFile(tempFilePath, output, "utf8");
|
|
39
|
-
|
|
40
|
-
const sizeKB = Math.round(output.length / 1024);
|
|
41
|
-
const tokenDescription = getTokenUsageDescription(
|
|
42
|
-
output,
|
|
43
|
-
LARGE_OUTPUT_TOKEN_THRESHOLD,
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
content: `Large output (${sizeKB} KB, ${tokenDescription}) written to temporary file. Use the Read tool to access the full content.`,
|
|
48
|
-
filePath: tempFilePath,
|
|
49
|
-
};
|
|
50
|
-
} catch (error) {
|
|
51
|
-
logger.warn(`Failed to write large output to temp file: ${error}`);
|
|
52
|
-
// Fallback to direct output if temp file creation fails
|
|
53
|
-
return { content: output };
|
|
54
|
-
}
|
|
55
|
-
}
|