@astralyn/pi 0.81.1-1 → 0.81.1-2
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/CHANGELOG.md +7 -0
- package/README.md +677 -677
- package/dist/cli.js +0 -0
- package/dist/core/tools/bash-command-summary.d.ts +7 -0
- package/dist/core/tools/bash-command-summary.d.ts.map +1 -0
- package/dist/core/tools/bash-command-summary.js +261 -0
- package/dist/core/tools/bash-command-summary.js.map +1 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +101 -11
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/rpc-entry.js +0 -0
- package/docs/compaction.md +401 -401
- package/docs/containerization.md +111 -111
- package/docs/custom-provider.md +763 -763
- package/docs/extensions.md +2953 -2953
- package/docs/index.md +77 -77
- package/docs/packages.md +228 -228
- package/docs/quickstart.md +165 -165
- package/docs/rpc.md +1559 -1559
- package/docs/sdk.md +1186 -1186
- package/docs/session-format.md +436 -436
- package/docs/termux.md +127 -127
- package/docs/tui.md +942 -942
- package/examples/extensions/README.md +213 -213
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +0 -0
- package/examples/sdk/README.md +140 -140
- package/npm-shrinkwrap.json +2 -2
- package/package.json +6 -1
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return unique, ordered command names when a command is a safe multi-command
|
|
3
|
+
* chain suitable for a compact display summary. Returns undefined when the
|
|
4
|
+
* command contains unsupported shell syntax or an unrecognized command.
|
|
5
|
+
*/
|
|
6
|
+
export declare function summarizeBashCommand(command: string): string[] | undefined;
|
|
7
|
+
//# sourceMappingURL=bash-command-summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bash-command-summary.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash-command-summary.ts"],"names":[],"mappings":"AA6PA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAY1E","sourcesContent":["const SUMMARIZABLE_COMMANDS = new Set([\n\t\"cd\",\n\t\"ls\",\n\t\"find\",\n\t\"fd\",\n\t\"rg\",\n\t\"grep\",\n\t\"git\",\n\t\"gh\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"yarn\",\n\t\"bun\",\n\t\"cat\",\n\t\"head\",\n\t\"tail\",\n\t\"sed\",\n\t\"awk\",\n\t\"jq\",\n\t\"sort\",\n\t\"uniq\",\n\t\"wc\",\n\t\"mkdir\",\n\t\"cp\",\n\t\"mv\",\n\t\"rm\",\n\t\"touch\",\n\t\"curl\",\n\t\"wget\",\n\t\"make\",\n\t\"cmake\",\n\t\"cargo\",\n\t\"go\",\n\t\"docker\",\n\t\"kubectl\",\n]);\n\nconst SHELL_KEYWORDS = new Set([\n\t\"case\",\n\t\"do\",\n\t\"done\",\n\t\"elif\",\n\t\"else\",\n\t\"esac\",\n\t\"fi\",\n\t\"for\",\n\t\"function\",\n\t\"if\",\n\t\"in\",\n\t\"select\",\n\t\"then\",\n\t\"until\",\n\t\"while\",\n]);\n\ntype CommandSeparator = \"&&\" | \"||\" | \"|\" | \"|&\" | \";\" | \"\\n\";\n\nfunction splitTopLevelCommands(command: string): string[] | undefined {\n\tconst segments: string[] = [];\n\tlet current = \"\";\n\tlet quote: \"'\" | '\"' | undefined;\n\tlet escaped = false;\n\tlet sawSeparator = false;\n\tlet trailingSeparator: CommandSeparator | undefined;\n\n\tconst pushSegment = (separator: CommandSeparator): boolean => {\n\t\tconst segment = current.trim();\n\t\tif (!segment) return false;\n\t\tsegments.push(segment);\n\t\tcurrent = \"\";\n\t\tsawSeparator = true;\n\t\ttrailingSeparator = separator;\n\t\treturn true;\n\t};\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst char = command[i];\n\t\tconst next = command[i + 1];\n\n\t\tif (escaped) {\n\t\t\tif (char !== \"\\n\") current += char;\n\t\t\tescaped = false;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\\\\") {\n\t\t\tif (next === \"\\n\") {\n\t\t\t\ti++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tescaped = true;\n\t\t\tcurrent += char;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (quote) {\n\t\t\tcurrent += char;\n\t\t\tif (char === quote) quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"'\" || char === '\"') {\n\t\t\tquote = char;\n\t\t\tcurrent += char;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"`\" || (char === \"$\" && (next === \"(\" || next === \"{\"))) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Redirections, grouping, comments, and background execution need more\n\t\t// shell grammar than a display-only summary should attempt to model.\n\t\tif (char === \"<\" || char === \">\" || char === \"(\" || char === \")\" || char === \"#\") {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (char === \"&\") {\n\t\t\tif (next !== \"&\") return undefined;\n\t\t\tif (!pushSegment(\"&&\")) return undefined;\n\t\t\tcurrent = \"\";\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"|\") {\n\t\t\tif (next === \"|\") {\n\t\t\t\tif (!pushSegment(\"||\")) return undefined;\n\t\t\t\ti++;\n\t\t\t} else if (next === \"&\") {\n\t\t\t\tif (!pushSegment(\"|&\")) return undefined;\n\t\t\t\ti++;\n\t\t\t} else if (!pushSegment(\"|\")) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \";\") {\n\t\t\tif (next === \";\" || next === \"&\") return undefined;\n\t\t\tif (!pushSegment(\";\")) return undefined;\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\n\") {\n\t\t\tif (!current.trim() && trailingSeparator && trailingSeparator !== \";\" && trailingSeparator !== \"\\n\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!pushSegment(\"\\n\")) return undefined;\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrent += char;\n\t}\n\n\tif (escaped || quote) return undefined;\n\tif (current.trim()) {\n\t\tsegments.push(current.trim());\n\t} else if (trailingSeparator && trailingSeparator !== \";\" && trailingSeparator !== \"\\n\") {\n\t\treturn undefined;\n\t}\n\n\treturn sawSeparator && segments.length > 1 ? segments : undefined;\n}\n\nfunction tokenizeSimpleCommand(segment: string): string[] | undefined {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet quote: \"'\" | '\"' | undefined;\n\tlet escaped = false;\n\tlet tokenStarted = false;\n\n\tconst pushToken = () => {\n\t\tif (!tokenStarted) return;\n\t\ttokens.push(current);\n\t\tcurrent = \"\";\n\t\ttokenStarted = false;\n\t};\n\n\tfor (let i = 0; i < segment.length; i++) {\n\t\tconst char = segment[i];\n\n\t\tif (escaped) {\n\t\t\tif (char !== \"\\n\") current += char;\n\t\t\tescaped = false;\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\\\\") {\n\t\t\tescaped = true;\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (quote) {\n\t\t\tif (char === quote) {\n\t\t\t\tquote = undefined;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"'\" || char === '\"') {\n\t\t\tquote = char;\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (/\\s/.test(char)) {\n\t\t\tpushToken();\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrent += char;\n\t\ttokenStarted = true;\n\t}\n\n\tif (escaped || quote) return undefined;\n\tpushToken();\n\treturn tokens.length > 0 ? tokens : undefined;\n}\n\nfunction normalizeCommandName(token: string): string | undefined {\n\tif (!token || token.startsWith(\"$\")) return undefined;\n\tconst lastSlash = token.lastIndexOf(\"/\");\n\tif (lastSlash !== -1) {\n\t\tif (token.startsWith(\"./\") || token.startsWith(\"../\")) return undefined;\n\t\ttoken = token.slice(lastSlash + 1);\n\t}\n\n\tconst normalized = token.toLowerCase().replace(/\\.exe$/, \"\");\n\treturn SUMMARIZABLE_COMMANDS.has(normalized) ? normalized : undefined;\n}\n\nfunction extractSimpleCommandName(segment: string): string | undefined {\n\tconst tokens = tokenizeSimpleCommand(segment);\n\tif (!tokens) return undefined;\n\n\tlet index = 0;\n\twhile (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) {\n\t\tindex++;\n\t}\n\tconst command = tokens[index];\n\tif (!command || SHELL_KEYWORDS.has(command)) return undefined;\n\treturn normalizeCommandName(command);\n}\n\n/**\n * Return unique, ordered command names when a command is a safe multi-command\n * chain suitable for a compact display summary. Returns undefined when the\n * command contains unsupported shell syntax or an unrecognized command.\n */\nexport function summarizeBashCommand(command: string): string[] | undefined {\n\tconst segments = splitTopLevelCommands(command);\n\tif (!segments) return undefined;\n\n\tconst names: string[] = [];\n\tfor (const segment of segments) {\n\t\tconst name = extractSimpleCommandName(segment);\n\t\tif (!name) return undefined;\n\t\tif (!names.includes(name)) names.push(name);\n\t}\n\n\treturn names.length > 0 ? names : undefined;\n}\n"]}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const SUMMARIZABLE_COMMANDS = new Set([
|
|
2
|
+
"cd",
|
|
3
|
+
"ls",
|
|
4
|
+
"find",
|
|
5
|
+
"fd",
|
|
6
|
+
"rg",
|
|
7
|
+
"grep",
|
|
8
|
+
"git",
|
|
9
|
+
"gh",
|
|
10
|
+
"npm",
|
|
11
|
+
"pnpm",
|
|
12
|
+
"yarn",
|
|
13
|
+
"bun",
|
|
14
|
+
"cat",
|
|
15
|
+
"head",
|
|
16
|
+
"tail",
|
|
17
|
+
"sed",
|
|
18
|
+
"awk",
|
|
19
|
+
"jq",
|
|
20
|
+
"sort",
|
|
21
|
+
"uniq",
|
|
22
|
+
"wc",
|
|
23
|
+
"mkdir",
|
|
24
|
+
"cp",
|
|
25
|
+
"mv",
|
|
26
|
+
"rm",
|
|
27
|
+
"touch",
|
|
28
|
+
"curl",
|
|
29
|
+
"wget",
|
|
30
|
+
"make",
|
|
31
|
+
"cmake",
|
|
32
|
+
"cargo",
|
|
33
|
+
"go",
|
|
34
|
+
"docker",
|
|
35
|
+
"kubectl",
|
|
36
|
+
]);
|
|
37
|
+
const SHELL_KEYWORDS = new Set([
|
|
38
|
+
"case",
|
|
39
|
+
"do",
|
|
40
|
+
"done",
|
|
41
|
+
"elif",
|
|
42
|
+
"else",
|
|
43
|
+
"esac",
|
|
44
|
+
"fi",
|
|
45
|
+
"for",
|
|
46
|
+
"function",
|
|
47
|
+
"if",
|
|
48
|
+
"in",
|
|
49
|
+
"select",
|
|
50
|
+
"then",
|
|
51
|
+
"until",
|
|
52
|
+
"while",
|
|
53
|
+
]);
|
|
54
|
+
function splitTopLevelCommands(command) {
|
|
55
|
+
const segments = [];
|
|
56
|
+
let current = "";
|
|
57
|
+
let quote;
|
|
58
|
+
let escaped = false;
|
|
59
|
+
let sawSeparator = false;
|
|
60
|
+
let trailingSeparator;
|
|
61
|
+
const pushSegment = (separator) => {
|
|
62
|
+
const segment = current.trim();
|
|
63
|
+
if (!segment)
|
|
64
|
+
return false;
|
|
65
|
+
segments.push(segment);
|
|
66
|
+
current = "";
|
|
67
|
+
sawSeparator = true;
|
|
68
|
+
trailingSeparator = separator;
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
for (let i = 0; i < command.length; i++) {
|
|
72
|
+
const char = command[i];
|
|
73
|
+
const next = command[i + 1];
|
|
74
|
+
if (escaped) {
|
|
75
|
+
if (char !== "\n")
|
|
76
|
+
current += char;
|
|
77
|
+
escaped = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (char === "\\") {
|
|
81
|
+
if (next === "\n") {
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
escaped = true;
|
|
86
|
+
current += char;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (quote) {
|
|
90
|
+
current += char;
|
|
91
|
+
if (char === quote)
|
|
92
|
+
quote = undefined;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === "'" || char === '"') {
|
|
96
|
+
quote = char;
|
|
97
|
+
current += char;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "`" || (char === "$" && (next === "(" || next === "{"))) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
// Redirections, grouping, comments, and background execution need more
|
|
104
|
+
// shell grammar than a display-only summary should attempt to model.
|
|
105
|
+
if (char === "<" || char === ">" || char === "(" || char === ")" || char === "#") {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
if (char === "&") {
|
|
109
|
+
if (next !== "&")
|
|
110
|
+
return undefined;
|
|
111
|
+
if (!pushSegment("&&"))
|
|
112
|
+
return undefined;
|
|
113
|
+
current = "";
|
|
114
|
+
i++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === "|") {
|
|
118
|
+
if (next === "|") {
|
|
119
|
+
if (!pushSegment("||"))
|
|
120
|
+
return undefined;
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
else if (next === "&") {
|
|
124
|
+
if (!pushSegment("|&"))
|
|
125
|
+
return undefined;
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
else if (!pushSegment("|")) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
current = "";
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === ";") {
|
|
135
|
+
if (next === ";" || next === "&")
|
|
136
|
+
return undefined;
|
|
137
|
+
if (!pushSegment(";"))
|
|
138
|
+
return undefined;
|
|
139
|
+
current = "";
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (char === "\n") {
|
|
143
|
+
if (!current.trim() && trailingSeparator && trailingSeparator !== ";" && trailingSeparator !== "\n") {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!pushSegment("\n"))
|
|
147
|
+
return undefined;
|
|
148
|
+
current = "";
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
current += char;
|
|
152
|
+
}
|
|
153
|
+
if (escaped || quote)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (current.trim()) {
|
|
156
|
+
segments.push(current.trim());
|
|
157
|
+
}
|
|
158
|
+
else if (trailingSeparator && trailingSeparator !== ";" && trailingSeparator !== "\n") {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
return sawSeparator && segments.length > 1 ? segments : undefined;
|
|
162
|
+
}
|
|
163
|
+
function tokenizeSimpleCommand(segment) {
|
|
164
|
+
const tokens = [];
|
|
165
|
+
let current = "";
|
|
166
|
+
let quote;
|
|
167
|
+
let escaped = false;
|
|
168
|
+
let tokenStarted = false;
|
|
169
|
+
const pushToken = () => {
|
|
170
|
+
if (!tokenStarted)
|
|
171
|
+
return;
|
|
172
|
+
tokens.push(current);
|
|
173
|
+
current = "";
|
|
174
|
+
tokenStarted = false;
|
|
175
|
+
};
|
|
176
|
+
for (let i = 0; i < segment.length; i++) {
|
|
177
|
+
const char = segment[i];
|
|
178
|
+
if (escaped) {
|
|
179
|
+
if (char !== "\n")
|
|
180
|
+
current += char;
|
|
181
|
+
escaped = false;
|
|
182
|
+
tokenStarted = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (char === "\\") {
|
|
186
|
+
escaped = true;
|
|
187
|
+
tokenStarted = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (quote) {
|
|
191
|
+
if (char === quote) {
|
|
192
|
+
quote = undefined;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
current += char;
|
|
196
|
+
}
|
|
197
|
+
tokenStarted = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (char === "'" || char === '"') {
|
|
201
|
+
quote = char;
|
|
202
|
+
tokenStarted = true;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (/\s/.test(char)) {
|
|
206
|
+
pushToken();
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
current += char;
|
|
210
|
+
tokenStarted = true;
|
|
211
|
+
}
|
|
212
|
+
if (escaped || quote)
|
|
213
|
+
return undefined;
|
|
214
|
+
pushToken();
|
|
215
|
+
return tokens.length > 0 ? tokens : undefined;
|
|
216
|
+
}
|
|
217
|
+
function normalizeCommandName(token) {
|
|
218
|
+
if (!token || token.startsWith("$"))
|
|
219
|
+
return undefined;
|
|
220
|
+
const lastSlash = token.lastIndexOf("/");
|
|
221
|
+
if (lastSlash !== -1) {
|
|
222
|
+
if (token.startsWith("./") || token.startsWith("../"))
|
|
223
|
+
return undefined;
|
|
224
|
+
token = token.slice(lastSlash + 1);
|
|
225
|
+
}
|
|
226
|
+
const normalized = token.toLowerCase().replace(/\.exe$/, "");
|
|
227
|
+
return SUMMARIZABLE_COMMANDS.has(normalized) ? normalized : undefined;
|
|
228
|
+
}
|
|
229
|
+
function extractSimpleCommandName(segment) {
|
|
230
|
+
const tokens = tokenizeSimpleCommand(segment);
|
|
231
|
+
if (!tokens)
|
|
232
|
+
return undefined;
|
|
233
|
+
let index = 0;
|
|
234
|
+
while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) {
|
|
235
|
+
index++;
|
|
236
|
+
}
|
|
237
|
+
const command = tokens[index];
|
|
238
|
+
if (!command || SHELL_KEYWORDS.has(command))
|
|
239
|
+
return undefined;
|
|
240
|
+
return normalizeCommandName(command);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Return unique, ordered command names when a command is a safe multi-command
|
|
244
|
+
* chain suitable for a compact display summary. Returns undefined when the
|
|
245
|
+
* command contains unsupported shell syntax or an unrecognized command.
|
|
246
|
+
*/
|
|
247
|
+
export function summarizeBashCommand(command) {
|
|
248
|
+
const segments = splitTopLevelCommands(command);
|
|
249
|
+
if (!segments)
|
|
250
|
+
return undefined;
|
|
251
|
+
const names = [];
|
|
252
|
+
for (const segment of segments) {
|
|
253
|
+
const name = extractSimpleCommandName(segment);
|
|
254
|
+
if (!name)
|
|
255
|
+
return undefined;
|
|
256
|
+
if (!names.includes(name))
|
|
257
|
+
names.push(name);
|
|
258
|
+
}
|
|
259
|
+
return names.length > 0 ? names : undefined;
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=bash-command-summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bash-command-summary.js","sourceRoot":"","sources":["../../../src/core/tools/bash-command-summary.ts"],"names":[],"mappings":"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","sourcesContent":["const SUMMARIZABLE_COMMANDS = new Set([\n\t\"cd\",\n\t\"ls\",\n\t\"find\",\n\t\"fd\",\n\t\"rg\",\n\t\"grep\",\n\t\"git\",\n\t\"gh\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"yarn\",\n\t\"bun\",\n\t\"cat\",\n\t\"head\",\n\t\"tail\",\n\t\"sed\",\n\t\"awk\",\n\t\"jq\",\n\t\"sort\",\n\t\"uniq\",\n\t\"wc\",\n\t\"mkdir\",\n\t\"cp\",\n\t\"mv\",\n\t\"rm\",\n\t\"touch\",\n\t\"curl\",\n\t\"wget\",\n\t\"make\",\n\t\"cmake\",\n\t\"cargo\",\n\t\"go\",\n\t\"docker\",\n\t\"kubectl\",\n]);\n\nconst SHELL_KEYWORDS = new Set([\n\t\"case\",\n\t\"do\",\n\t\"done\",\n\t\"elif\",\n\t\"else\",\n\t\"esac\",\n\t\"fi\",\n\t\"for\",\n\t\"function\",\n\t\"if\",\n\t\"in\",\n\t\"select\",\n\t\"then\",\n\t\"until\",\n\t\"while\",\n]);\n\ntype CommandSeparator = \"&&\" | \"||\" | \"|\" | \"|&\" | \";\" | \"\\n\";\n\nfunction splitTopLevelCommands(command: string): string[] | undefined {\n\tconst segments: string[] = [];\n\tlet current = \"\";\n\tlet quote: \"'\" | '\"' | undefined;\n\tlet escaped = false;\n\tlet sawSeparator = false;\n\tlet trailingSeparator: CommandSeparator | undefined;\n\n\tconst pushSegment = (separator: CommandSeparator): boolean => {\n\t\tconst segment = current.trim();\n\t\tif (!segment) return false;\n\t\tsegments.push(segment);\n\t\tcurrent = \"\";\n\t\tsawSeparator = true;\n\t\ttrailingSeparator = separator;\n\t\treturn true;\n\t};\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst char = command[i];\n\t\tconst next = command[i + 1];\n\n\t\tif (escaped) {\n\t\t\tif (char !== \"\\n\") current += char;\n\t\t\tescaped = false;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\\\\") {\n\t\t\tif (next === \"\\n\") {\n\t\t\t\ti++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tescaped = true;\n\t\t\tcurrent += char;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (quote) {\n\t\t\tcurrent += char;\n\t\t\tif (char === quote) quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"'\" || char === '\"') {\n\t\t\tquote = char;\n\t\t\tcurrent += char;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"`\" || (char === \"$\" && (next === \"(\" || next === \"{\"))) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Redirections, grouping, comments, and background execution need more\n\t\t// shell grammar than a display-only summary should attempt to model.\n\t\tif (char === \"<\" || char === \">\" || char === \"(\" || char === \")\" || char === \"#\") {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (char === \"&\") {\n\t\t\tif (next !== \"&\") return undefined;\n\t\t\tif (!pushSegment(\"&&\")) return undefined;\n\t\t\tcurrent = \"\";\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"|\") {\n\t\t\tif (next === \"|\") {\n\t\t\t\tif (!pushSegment(\"||\")) return undefined;\n\t\t\t\ti++;\n\t\t\t} else if (next === \"&\") {\n\t\t\t\tif (!pushSegment(\"|&\")) return undefined;\n\t\t\t\ti++;\n\t\t\t} else if (!pushSegment(\"|\")) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \";\") {\n\t\t\tif (next === \";\" || next === \"&\") return undefined;\n\t\t\tif (!pushSegment(\";\")) return undefined;\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\n\") {\n\t\t\tif (!current.trim() && trailingSeparator && trailingSeparator !== \";\" && trailingSeparator !== \"\\n\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!pushSegment(\"\\n\")) return undefined;\n\t\t\tcurrent = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrent += char;\n\t}\n\n\tif (escaped || quote) return undefined;\n\tif (current.trim()) {\n\t\tsegments.push(current.trim());\n\t} else if (trailingSeparator && trailingSeparator !== \";\" && trailingSeparator !== \"\\n\") {\n\t\treturn undefined;\n\t}\n\n\treturn sawSeparator && segments.length > 1 ? segments : undefined;\n}\n\nfunction tokenizeSimpleCommand(segment: string): string[] | undefined {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet quote: \"'\" | '\"' | undefined;\n\tlet escaped = false;\n\tlet tokenStarted = false;\n\n\tconst pushToken = () => {\n\t\tif (!tokenStarted) return;\n\t\ttokens.push(current);\n\t\tcurrent = \"\";\n\t\ttokenStarted = false;\n\t};\n\n\tfor (let i = 0; i < segment.length; i++) {\n\t\tconst char = segment[i];\n\n\t\tif (escaped) {\n\t\t\tif (char !== \"\\n\") current += char;\n\t\t\tescaped = false;\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\\\\") {\n\t\t\tescaped = true;\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (quote) {\n\t\t\tif (char === quote) {\n\t\t\t\tquote = undefined;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"'\" || char === '\"') {\n\t\t\tquote = char;\n\t\t\ttokenStarted = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (/\\s/.test(char)) {\n\t\t\tpushToken();\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrent += char;\n\t\ttokenStarted = true;\n\t}\n\n\tif (escaped || quote) return undefined;\n\tpushToken();\n\treturn tokens.length > 0 ? tokens : undefined;\n}\n\nfunction normalizeCommandName(token: string): string | undefined {\n\tif (!token || token.startsWith(\"$\")) return undefined;\n\tconst lastSlash = token.lastIndexOf(\"/\");\n\tif (lastSlash !== -1) {\n\t\tif (token.startsWith(\"./\") || token.startsWith(\"../\")) return undefined;\n\t\ttoken = token.slice(lastSlash + 1);\n\t}\n\n\tconst normalized = token.toLowerCase().replace(/\\.exe$/, \"\");\n\treturn SUMMARIZABLE_COMMANDS.has(normalized) ? normalized : undefined;\n}\n\nfunction extractSimpleCommandName(segment: string): string | undefined {\n\tconst tokens = tokenizeSimpleCommand(segment);\n\tif (!tokens) return undefined;\n\n\tlet index = 0;\n\twhile (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) {\n\t\tindex++;\n\t}\n\tconst command = tokens[index];\n\tif (!command || SHELL_KEYWORDS.has(command)) return undefined;\n\treturn normalizeCommandName(command);\n}\n\n/**\n * Return unique, ordered command names when a command is a safe multi-command\n * chain suitable for a compact display summary. Returns undefined when the\n * command contains unsupported shell syntax or an unrecognized command.\n */\nexport function summarizeBashCommand(command: string): string[] | undefined {\n\tconst segments = splitTopLevelCommands(command);\n\tif (!segments) return undefined;\n\n\tconst names: string[] = [];\n\tfor (const segment of segments) {\n\t\tconst name = extractSimpleCommandName(segment);\n\t\tif (!name) return undefined;\n\t\tif (!names.includes(name)) names.push(name);\n\t}\n\n\treturn names.length > 0 ? names : undefined;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAG/D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAY5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAItF,OAAO,EAAoD,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAkBxG,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,CACL,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QACR,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;KACxB,KACG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CAkE1F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;AAO5E,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAKD,KAAK,eAAe,GAAG;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;CACrC,CAAC;AA8GF,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,EAAE,eAAe,CAAC,CA4KjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { constants } from \"node:fs\";\nimport { access as fsAccess } from \"node:fs/promises\";\nimport type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Container, Text, truncateToWidth } from \"@earendil-works/pi-tui\";\nimport { spawn } from \"child_process\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.ts\";\nimport { theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { waitForChildProcess } from \"../../utils/child-process.ts\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { OutputAccumulator } from \"./output-accumulator.ts\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.ts\";\n\nconst MAX_TIMEOUT_MS = 2_147_483_647;\nconst MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;\n\nfunction resolveTimeoutMs(timeout: number | undefined): number | undefined {\n\tif (timeout === undefined) return undefined;\n\tif (!Number.isFinite(timeout) || timeout <= 0) {\n\t\tthrow new Error(\"Invalid timeout: must be a finite number of seconds\");\n\t}\n\n\tconst timeoutMs = timeout * 1000;\n\tif (timeoutMs > MAX_TIMEOUT_MS) {\n\t\tthrow new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);\n\t}\n\treturn timeoutMs;\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: async (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\tconst timeoutMs = resolveTimeoutMs(timeout);\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"aborted\");\n\t\t\t}\n\t\t\tconst shellConfig = getShellConfig(options?.shellPath);\n\t\t\ttry {\n\t\t\t\tawait fsAccess(cwd, constants.F_OK);\n\t\t\t} catch {\n\t\t\t\tthrow new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`);\n\t\t\t}\n\n\t\t\tconst commandFromStdin = shellConfig.commandTransport === \"stdin\";\n\t\t\tconst child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], {\n\t\t\t\tcwd,\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\tstdio: [commandFromStdin ? \"pipe\" : \"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (commandFromStdin) {\n\t\t\t\tchild.stdin?.on(\"error\", () => {});\n\t\t\t\tchild.stdin?.end(command);\n\t\t\t}\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\ttry {\n\t\t\t\t// Set timeout if provided.\n\t\t\t\tif (timeoutMs !== undefined) {\n\t\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t\t}, timeoutMs);\n\t\t\t\t}\n\t\t\t\t// Stream stdout and stderr.\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\t\t\t\t// Handle abort signal by killing the entire process tree.\n\t\t\t\tif (signal) {\n\t\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t\t// Handle shell spawn errors and wait for the process to terminate without hanging\n\t\t\t\t// on inherited stdio handles held by detached descendants.\n\t\t\t\tconst exitCode = await waitForChildProcess(child);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"aborted\");\n\t\t\t\t}\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tthrow new Error(`timeout:${timeout}`);\n\t\t\t\t}\n\t\t\t\treturn { exitCode };\n\t\t\t} finally {\n\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\nconst BASH_UPDATE_THROTTLE_MS = 100;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashResultRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashResultRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashCall(args: { command?: string; timeout?: number } | undefined): string {\n\tconst command = str(args?.command);\n\tconst timeout = args?.timeout as number | undefined;\n\tconst timeoutSuffix = timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n\tconst commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg(\"toolOutput\", \"...\");\n\treturn theme.fg(\"toolTitle\", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tlet output = getTextOutput(result as any, showImages).trim();\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith(\"]\")) {\n\t\tconst footerStart = output.lastIndexOf(\"\\n\\n[\");\n\t\tif (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) {\n\t\t\toutput = output.slice(0, footerStart).trimEnd();\n\t\t}\n\t}\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tconst output = new OutputAccumulator({ tempFilePrefix: \"pi-bash\" });\n\t\t\tlet acceptingOutput = true;\n\t\t\tlet updateTimer: NodeJS.Timeout | undefined;\n\t\t\tlet updateDirty = false;\n\t\t\tlet lastUpdateAt = 0;\n\n\t\t\tconst emitOutputUpdate = () => {\n\t\t\t\tif (!onUpdate || !updateDirty) return;\n\t\t\t\tupdateDirty = false;\n\t\t\t\tlastUpdateAt = Date.now();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tonUpdate({\n\t\t\t\t\tcontent: [{ type: \"text\", text: snapshot.content || \"\" }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,\n\t\t\t\t\t\tfullOutputPath: snapshot.fullOutputPath,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst clearUpdateTimer = () => {\n\t\t\t\tif (updateTimer) {\n\t\t\t\t\tclearTimeout(updateTimer);\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst scheduleOutputUpdate = () => {\n\t\t\t\tif (!onUpdate) return;\n\t\t\t\tupdateDirty = true;\n\t\t\t\tconst delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);\n\t\t\t\tif (delay <= 0) {\n\t\t\t\t\tclearUpdateTimer();\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tupdateTimer ??= setTimeout(() => {\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t}, delay);\n\t\t\t};\n\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\tif (!acceptingOutput) return;\n\t\t\t\toutput.append(data);\n\t\t\t\tscheduleOutputUpdate();\n\t\t\t};\n\n\t\t\tconst finishOutput = async () => {\n\t\t\t\tacceptingOutput = false;\n\t\t\t\toutput.finish();\n\t\t\t\tclearUpdateTimer();\n\t\t\t\temitOutputUpdate();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tawait output.closeTempFile();\n\t\t\t\treturn snapshot;\n\t\t\t};\n\n\t\t\tconst formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = \"(no output)\") => {\n\t\t\t\tconst truncation = snapshot.truncation;\n\t\t\t\tlet text = snapshot.content || emptyText;\n\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = { truncation, fullOutputPath: snapshot.fullOutputPath };\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\tconst lastLineSize = formatSize(output.getLastLineBytes());\n\t\t\t\t\t\ttext += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { text, details };\n\t\t\t};\n\n\t\t\tconst appendStatus = (text: string, status: string) => `${text ? `${text}\\n\\n` : \"\"}${status}`;\n\n\t\t\ttry {\n\t\t\t\tlet exitCode: number | null;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await ops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\t\tonData: handleData,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\ttimeout,\n\t\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t\t});\n\t\t\t\t\texitCode = result.exitCode;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\t\tconst { text } = formatOutput(snapshot, \"\");\n\t\t\t\t\tif (err instanceof Error && err.message === \"aborted\") {\n\t\t\t\t\t\tthrow new Error(appendStatus(text, \"Command aborted\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\tthrow new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\tconst { text: outputText, details } = formatOutput(snapshot);\n\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\tthrow new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));\n\t\t\t\t}\n\t\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t\t} finally {\n\t\t\t\tclearUpdateTimer();\n\t\t\t}\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBashCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAG/D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAY5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAAoD,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAkBxG,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,CACL,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QACR,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;KACxB,KACG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CAkE1F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;AAO5E,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAKD,KAAK,eAAe,GAAG;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;CACrC,CAAC;AAkNF,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,EAAE,eAAe,CAAC,CA6KjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { constants } from \"node:fs\";\nimport { access as fsAccess } from \"node:fs/promises\";\nimport type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { type Component, Container, Text, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { spawn } from \"child_process\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { truncateToVisualLines } from \"../../modes/interactive/components/visual-truncate.ts\";\nimport { highlightCode, theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { waitForChildProcess } from \"../../utils/child-process.ts\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { summarizeBashCommand } from \"./bash-command-summary.ts\";\nimport { OutputAccumulator } from \"./output-accumulator.ts\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from \"./truncate.ts\";\n\nconst MAX_TIMEOUT_MS = 2_147_483_647;\nconst MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;\n\nfunction resolveTimeoutMs(timeout: number | undefined): number | undefined {\n\tif (timeout === undefined) return undefined;\n\tif (!Number.isFinite(timeout) || timeout <= 0) {\n\t\tthrow new Error(\"Invalid timeout: must be a finite number of seconds\");\n\t}\n\n\tconst timeoutMs = timeout * 1000;\n\tif (timeoutMs > MAX_TIMEOUT_MS) {\n\t\tthrow new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);\n\t}\n\treturn timeoutMs;\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\n/**\n * Pluggable operations for the bash tool.\n * Override these to delegate command execution to remote systems (for example SSH).\n */\nexport interface BashOperations {\n\t/**\n\t * Execute a command and stream output.\n\t * @param command The command to execute\n\t * @param cwd Working directory\n\t * @param options Execution options\n\t * @returns Promise resolving to exit code (null if killed)\n\t */\n\texec: (\n\t\tcommand: string,\n\t\tcwd: string,\n\t\toptions: {\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tsignal?: AbortSignal;\n\t\t\ttimeout?: number;\n\t\t\tenv?: NodeJS.ProcessEnv;\n\t\t},\n\t) => Promise<{ exitCode: number | null }>;\n}\n\n/**\n * Create bash operations using pi's built-in local shell execution backend.\n *\n * This is useful for extensions that intercept user_bash and still want pi's\n * standard local shell behavior while wrapping or rewriting commands.\n */\nexport function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {\n\treturn {\n\t\texec: async (command, cwd, { onData, signal, timeout, env }) => {\n\t\t\tconst timeoutMs = resolveTimeoutMs(timeout);\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"aborted\");\n\t\t\t}\n\t\t\tconst shellConfig = getShellConfig(options?.shellPath);\n\t\t\ttry {\n\t\t\t\tawait fsAccess(cwd, constants.F_OK);\n\t\t\t} catch {\n\t\t\t\tthrow new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`);\n\t\t\t}\n\n\t\t\tconst commandFromStdin = shellConfig.commandTransport === \"stdin\";\n\t\t\tconst child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], {\n\t\t\t\tcwd,\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\tenv: env ?? getShellEnv(),\n\t\t\t\tstdio: [commandFromStdin ? \"pipe\" : \"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (commandFromStdin) {\n\t\t\t\tchild.stdin?.on(\"error\", () => {});\n\t\t\t\tchild.stdin?.end(command);\n\t\t\t}\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\ttry {\n\t\t\t\t// Set timeout if provided.\n\t\t\t\tif (timeoutMs !== undefined) {\n\t\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t\t}, timeoutMs);\n\t\t\t\t}\n\t\t\t\t// Stream stdout and stderr.\n\t\t\t\tchild.stdout?.on(\"data\", onData);\n\t\t\t\tchild.stderr?.on(\"data\", onData);\n\t\t\t\t// Handle abort signal by killing the entire process tree.\n\t\t\t\tif (signal) {\n\t\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t\t// Handle shell spawn errors and wait for the process to terminate without hanging\n\t\t\t\t// on inherited stdio handles held by detached descendants.\n\t\t\t\tconst exitCode = await waitForChildProcess(child);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"aborted\");\n\t\t\t\t}\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tthrow new Error(`timeout:${timeout}`);\n\t\t\t\t}\n\t\t\t\treturn { exitCode };\n\t\t\t} finally {\n\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport interface BashSpawnContext {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;\n\nfunction resolveSpawnContext(command: string, cwd: string, spawnHook?: BashSpawnHook): BashSpawnContext {\n\tconst baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } };\n\treturn spawnHook ? spawnHook(baseContext) : baseContext;\n}\n\nexport interface BashToolOptions {\n\t/** Custom operations for command execution. Default: local shell */\n\toperations?: BashOperations;\n\t/** Command prefix prepended to every command (for example shell setup commands) */\n\tcommandPrefix?: string;\n\t/** Optional explicit shell path from settings */\n\tshellPath?: string;\n\t/** Hook to adjust command, cwd, or env before execution */\n\tspawnHook?: BashSpawnHook;\n}\n\nconst BASH_PREVIEW_LINES = 5;\nconst BASH_UPDATE_THROTTLE_MS = 100;\n\ntype BashRenderState = {\n\tstartedAt: number | undefined;\n\tendedAt: number | undefined;\n\tinterval: NodeJS.Timeout | undefined;\n};\n\ntype BashCachedRenderState = {\n\tcachedWidth: number | undefined;\n\tcachedLines: string[] | undefined;\n\tcachedSkipped: number | undefined;\n};\n\nclass BashCallRenderComponent implements Component {\n\tprivate command: string | null = \"\";\n\tprivate timeout: number | undefined;\n\tprivate argsComplete = false;\n\tprivate expanded = false;\n\tprivate summary: string[] | undefined;\n\tprivate cachedWidth: number | undefined;\n\tprivate cachedLines: string[] | undefined;\n\n\tupdate(args: { command?: string; timeout?: number } | undefined, argsComplete: boolean, expanded: boolean): void {\n\t\tconst command = str(args?.command);\n\t\tconst timeout = args?.timeout as number | undefined;\n\t\tif (\n\t\t\tthis.command === command &&\n\t\t\tthis.timeout === timeout &&\n\t\t\tthis.argsComplete === argsComplete &&\n\t\t\tthis.expanded === expanded\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.command = command;\n\t\tthis.timeout = timeout;\n\t\tthis.argsComplete = argsComplete;\n\t\tthis.expanded = expanded;\n\t\tthis.summary = argsComplete && command ? summarizeBashCommand(command) : undefined;\n\t\tthis.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines === undefined || this.cachedWidth !== width) {\n\t\t\tthis.cachedLines = this.renderLines(Math.max(1, width));\n\t\t\tthis.cachedWidth = width;\n\t\t}\n\t\treturn this.cachedLines;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\tprivate renderLines(width: number): string[] {\n\t\tconst fullCall = formatFullBashCall(this.command, this.timeout);\n\t\tconst fullLines = new Text(fullCall, 0, 0).render(width);\n\t\tif (this.expanded || fullLines.length <= 1) return fullLines;\n\t\tif (this.summary) return [formatBashSummary(this.summary, this.timeout, width)];\n\t\treturn [formatTruncatedBashCall(this.command, this.timeout, width)];\n\t}\n}\n\nclass BashResultRenderComponent extends Container {\n\tstate: BashCachedRenderState = {\n\t\tcachedWidth: undefined,\n\t\tcachedLines: undefined,\n\t\tcachedSkipped: undefined,\n\t};\n}\n\nfunction formatDuration(ms: number): string {\n\treturn `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatBashPrompt(): string {\n\treturn theme.fg(\"toolTitle\", theme.bold(\"$ \"));\n}\n\nfunction formatBashTimeout(timeout: number | undefined): string {\n\treturn timeout ? theme.fg(\"muted\", ` (timeout ${timeout}s)`) : \"\";\n}\n\nfunction styleBashCommand(command: string): string {\n\treturn highlightCode(command, \"bash\").join(\"\\n\");\n}\n\nfunction formatFullBashCall(command: string | null, timeout: number | undefined): string {\n\tconst commandDisplay =\n\t\tcommand === null ? invalidArgText(theme) : command ? styleBashCommand(command) : theme.fg(\"toolOutput\", \"...\");\n\treturn formatBashPrompt() + commandDisplay + formatBashTimeout(timeout);\n}\n\nfunction fitCollapsedBashCall(body: string, timeout: number | undefined, width: number): string {\n\tconst prompt = formatBashPrompt();\n\tconst timeoutSuffix = formatBashTimeout(timeout);\n\tconst ellipsis = theme.fg(\"muted\", \" …\");\n\tconst bodyWidth = Math.max(0, width - visibleWidth(prompt) - visibleWidth(ellipsis) - visibleWidth(timeoutSuffix));\n\tconst fittedBody = truncateToWidth(body, bodyWidth, \"\");\n\treturn truncateToWidth(prompt + fittedBody + ellipsis + timeoutSuffix, width, \"...\");\n}\n\nfunction formatBashSummary(commands: string[], timeout: number | undefined, width: number): string {\n\tconst separator = theme.fg(\"muted\", \", \");\n\tconst prompt = formatBashPrompt();\n\tconst timeoutSuffix = formatBashTimeout(timeout);\n\tconst ellipsis = theme.fg(\"muted\", \" …\");\n\tconst namesWidth = Math.max(0, width - visibleWidth(prompt) - visibleWidth(ellipsis) - visibleWidth(timeoutSuffix));\n\tlet names = \"\";\n\tfor (const command of commands) {\n\t\tconst styledCommand = theme.fg(\"toolOutput\", command);\n\t\tconst candidate = names ? `${names}${separator}${styledCommand}` : styledCommand;\n\t\tif (visibleWidth(candidate) > namesWidth) break;\n\t\tnames = candidate;\n\t}\n\tif (!names) {\n\t\treturn truncateToWidth(prompt + theme.fg(\"muted\", \"…\") + timeoutSuffix, width, \"...\");\n\t}\n\treturn truncateToWidth(prompt + names + ellipsis + timeoutSuffix, width, \"...\");\n}\n\nfunction formatTruncatedBashCall(command: string | null, timeout: number | undefined, width: number): string {\n\tif (command === null) return truncateToWidth(formatFullBashCall(command, timeout), width, \"...\");\n\tif (!command) return formatFullBashCall(command, timeout);\n\tconst firstNonEmptyLine =\n\t\tcommand\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.find((line) => line.trim())\n\t\t\t?.trim() ?? command.trim();\n\treturn fitCollapsedBashCall(styleBashCommand(firstNonEmptyLine), timeout, width);\n}\n\nfunction rebuildBashResultRenderComponent(\n\tcomponent: BashResultRenderComponent,\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: BashToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n\tstartedAt: number | undefined,\n\tendedAt: number | undefined,\n): void {\n\tconst state = component.state;\n\tcomponent.clear();\n\n\tlet output = getTextOutput(result as any, showImages).trim();\n\tconst truncation = result.details?.truncation;\n\tconst fullOutputPath = result.details?.fullOutputPath;\n\tif (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith(\"]\")) {\n\t\tconst footerStart = output.lastIndexOf(\"\\n\\n[\");\n\t\tif (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) {\n\t\t\toutput = output.slice(0, footerStart).trimEnd();\n\t\t}\n\t}\n\n\tif (output) {\n\t\tconst styledOutput = output\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\n\t\tif (options.expanded) {\n\t\t\tcomponent.addChild(new Text(`\\n${styledOutput}`, 0, 0));\n\t\t} else {\n\t\t\tcomponent.addChild({\n\t\t\t\trender: (width: number) => {\n\t\t\t\t\tif (state.cachedLines === undefined || state.cachedWidth !== width) {\n\t\t\t\t\t\tconst preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);\n\t\t\t\t\t\tstate.cachedLines = preview.visualLines;\n\t\t\t\t\t\tstate.cachedSkipped = preview.skippedCount;\n\t\t\t\t\t\tstate.cachedWidth = width;\n\t\t\t\t\t}\n\t\t\t\t\tif (state.cachedSkipped && state.cachedSkipped > 0) {\n\t\t\t\t\t\tconst hint =\n\t\t\t\t\t\t\ttheme.fg(\"muted\", `... (${state.cachedSkipped} earlier lines,`) +\n\t\t\t\t\t\t\t` ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t\t\t\t\treturn [\"\", truncateToWidth(hint, width, \"...\"), ...(state.cachedLines ?? [])];\n\t\t\t\t\t}\n\t\t\t\t\treturn [\"\", ...(state.cachedLines ?? [])];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => {\n\t\t\t\t\tstate.cachedWidth = undefined;\n\t\t\t\t\tstate.cachedLines = undefined;\n\t\t\t\t\tstate.cachedSkipped = undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tif (truncation?.truncated || fullOutputPath) {\n\t\tconst warnings: string[] = [];\n\t\tif (fullOutputPath) {\n\t\t\twarnings.push(`Full output: ${fullOutputPath}`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\twarnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);\n\t\t\t} else {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"warning\", `[${warnings.join(\". \")}]`)}`, 0, 0));\n\t}\n\n\tif (startedAt !== undefined) {\n\t\tconst label = options.isPartial ? \"Elapsed\" : \"Took\";\n\t\tconst endTime = endedAt ?? Date.now();\n\t\tcomponent.addChild(new Text(`\\n${theme.fg(\"muted\", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));\n\t}\n}\n\nexport function createBashToolDefinition(\n\tcwd: string,\n\toptions?: BashToolOptions,\n): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {\n\tconst ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });\n\tconst commandPrefix = options?.commandPrefix;\n\tconst spawnHook = options?.spawnHook;\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tpromptSnippet: \"Execute bash commands (ls, grep, find, etc.)\",\n\t\tparameters: bashSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst resolvedCommand = commandPrefix ? `${commandPrefix}\\n${command}` : command;\n\t\t\tconst spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);\n\t\t\tconst output = new OutputAccumulator({ tempFilePrefix: \"pi-bash\" });\n\t\t\tlet acceptingOutput = true;\n\t\t\tlet updateTimer: NodeJS.Timeout | undefined;\n\t\t\tlet updateDirty = false;\n\t\t\tlet lastUpdateAt = 0;\n\n\t\t\tconst emitOutputUpdate = () => {\n\t\t\t\tif (!onUpdate || !updateDirty) return;\n\t\t\t\tupdateDirty = false;\n\t\t\t\tlastUpdateAt = Date.now();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tonUpdate({\n\t\t\t\t\tcontent: [{ type: \"text\", text: snapshot.content || \"\" }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,\n\t\t\t\t\t\tfullOutputPath: snapshot.fullOutputPath,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst clearUpdateTimer = () => {\n\t\t\t\tif (updateTimer) {\n\t\t\t\t\tclearTimeout(updateTimer);\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst scheduleOutputUpdate = () => {\n\t\t\t\tif (!onUpdate) return;\n\t\t\t\tupdateDirty = true;\n\t\t\t\tconst delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);\n\t\t\t\tif (delay <= 0) {\n\t\t\t\t\tclearUpdateTimer();\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tupdateTimer ??= setTimeout(() => {\n\t\t\t\t\tupdateTimer = undefined;\n\t\t\t\t\temitOutputUpdate();\n\t\t\t\t}, delay);\n\t\t\t};\n\n\t\t\tif (onUpdate) {\n\t\t\t\tonUpdate({ content: [], details: undefined });\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\tif (!acceptingOutput) return;\n\t\t\t\toutput.append(data);\n\t\t\t\tscheduleOutputUpdate();\n\t\t\t};\n\n\t\t\tconst finishOutput = async () => {\n\t\t\t\tacceptingOutput = false;\n\t\t\t\toutput.finish();\n\t\t\t\tclearUpdateTimer();\n\t\t\t\temitOutputUpdate();\n\t\t\t\tconst snapshot = output.snapshot({ persistIfTruncated: true });\n\t\t\t\tawait output.closeTempFile();\n\t\t\t\treturn snapshot;\n\t\t\t};\n\n\t\t\tconst formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = \"(no output)\") => {\n\t\t\t\tconst truncation = snapshot.truncation;\n\t\t\t\tlet text = snapshot.content || emptyText;\n\t\t\t\tlet details: BashToolDetails | undefined;\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = { truncation, fullOutputPath: snapshot.fullOutputPath };\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\tconst lastLineSize = formatSize(output.getLastLineBytes());\n\t\t\t\t\t\ttext += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttext += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { text, details };\n\t\t\t};\n\n\t\t\tconst appendStatus = (text: string, status: string) => `${text ? `${text}\\n\\n` : \"\"}${status}`;\n\n\t\t\ttry {\n\t\t\t\tlet exitCode: number | null;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await ops.exec(spawnContext.command, spawnContext.cwd, {\n\t\t\t\t\t\tonData: handleData,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\ttimeout,\n\t\t\t\t\t\tenv: spawnContext.env,\n\t\t\t\t\t});\n\t\t\t\t\texitCode = result.exitCode;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\t\tconst { text } = formatOutput(snapshot, \"\");\n\t\t\t\t\tif (err instanceof Error && err.message === \"aborted\") {\n\t\t\t\t\t\tthrow new Error(appendStatus(text, \"Command aborted\"));\n\t\t\t\t\t}\n\t\t\t\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\t\t\t\tconst timeoutSecs = err.message.split(\":\")[1];\n\t\t\t\t\t\tthrow new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tconst snapshot = await finishOutput();\n\t\t\t\tconst { text: outputText, details } = formatOutput(snapshot);\n\t\t\t\tif (exitCode !== 0 && exitCode !== null) {\n\t\t\t\t\tthrow new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));\n\t\t\t\t}\n\t\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t\t} finally {\n\t\t\t\tclearUpdateTimer();\n\t\t\t}\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (context.executionStarted && state.startedAt === undefined) {\n\t\t\t\tstate.startedAt = Date.now();\n\t\t\t\tstate.endedAt = undefined;\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashCallRenderComponent | undefined) ?? new BashCallRenderComponent();\n\t\t\tcomponent.update(args, context.argsComplete, context.expanded);\n\t\t\treturn component;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst state = context.state;\n\t\t\tif (state.startedAt !== undefined && options.isPartial && !state.interval) {\n\t\t\t\tstate.interval = setInterval(() => context.invalidate(), 1000);\n\t\t\t}\n\t\t\tif (!options.isPartial || context.isError) {\n\t\t\t\tstate.endedAt ??= Date.now();\n\t\t\t\tif (state.interval) {\n\t\t\t\t\tclearInterval(state.interval);\n\t\t\t\t\tstate.interval = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();\n\t\t\trebuildBashResultRenderComponent(\n\t\t\t\tcomponent,\n\t\t\t\tresult as any,\n\t\t\t\toptions,\n\t\t\t\tcontext.showImages,\n\t\t\t\tstate.startedAt,\n\t\t\t\tstate.endedAt,\n\t\t\t);\n\t\t\tcomponent.invalidate();\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {\n\treturn wrapToolDefinition(createBashToolDefinition(cwd, options));\n}\n"]}
|
package/dist/core/tools/bash.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { access as fsAccess } from "node:fs/promises";
|
|
3
|
-
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Container, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
|
7
7
|
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
|
|
8
|
-
import { theme } from "../../modes/interactive/theme/theme.js";
|
|
8
|
+
import { highlightCode, theme } from "../../modes/interactive/theme/theme.js";
|
|
9
9
|
import { waitForChildProcess } from "../../utils/child-process.js";
|
|
10
10
|
import { getShellConfig, getShellEnv, killProcessTree, trackDetachedChildPid, untrackDetachedChildPid, } from "../../utils/shell.js";
|
|
11
|
+
import { summarizeBashCommand } from "./bash-command-summary.js";
|
|
11
12
|
import { OutputAccumulator } from "./output-accumulator.js";
|
|
12
13
|
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
|
13
14
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
@@ -117,6 +118,51 @@ function resolveSpawnContext(command, cwd, spawnHook) {
|
|
|
117
118
|
}
|
|
118
119
|
const BASH_PREVIEW_LINES = 5;
|
|
119
120
|
const BASH_UPDATE_THROTTLE_MS = 100;
|
|
121
|
+
class BashCallRenderComponent {
|
|
122
|
+
command = "";
|
|
123
|
+
timeout;
|
|
124
|
+
argsComplete = false;
|
|
125
|
+
expanded = false;
|
|
126
|
+
summary;
|
|
127
|
+
cachedWidth;
|
|
128
|
+
cachedLines;
|
|
129
|
+
update(args, argsComplete, expanded) {
|
|
130
|
+
const command = str(args?.command);
|
|
131
|
+
const timeout = args?.timeout;
|
|
132
|
+
if (this.command === command &&
|
|
133
|
+
this.timeout === timeout &&
|
|
134
|
+
this.argsComplete === argsComplete &&
|
|
135
|
+
this.expanded === expanded) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.command = command;
|
|
139
|
+
this.timeout = timeout;
|
|
140
|
+
this.argsComplete = argsComplete;
|
|
141
|
+
this.expanded = expanded;
|
|
142
|
+
this.summary = argsComplete && command ? summarizeBashCommand(command) : undefined;
|
|
143
|
+
this.invalidate();
|
|
144
|
+
}
|
|
145
|
+
render(width) {
|
|
146
|
+
if (this.cachedLines === undefined || this.cachedWidth !== width) {
|
|
147
|
+
this.cachedLines = this.renderLines(Math.max(1, width));
|
|
148
|
+
this.cachedWidth = width;
|
|
149
|
+
}
|
|
150
|
+
return this.cachedLines;
|
|
151
|
+
}
|
|
152
|
+
invalidate() {
|
|
153
|
+
this.cachedWidth = undefined;
|
|
154
|
+
this.cachedLines = undefined;
|
|
155
|
+
}
|
|
156
|
+
renderLines(width) {
|
|
157
|
+
const fullCall = formatFullBashCall(this.command, this.timeout);
|
|
158
|
+
const fullLines = new Text(fullCall, 0, 0).render(width);
|
|
159
|
+
if (this.expanded || fullLines.length <= 1)
|
|
160
|
+
return fullLines;
|
|
161
|
+
if (this.summary)
|
|
162
|
+
return [formatBashSummary(this.summary, this.timeout, width)];
|
|
163
|
+
return [formatTruncatedBashCall(this.command, this.timeout, width)];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
120
166
|
class BashResultRenderComponent extends Container {
|
|
121
167
|
state = {
|
|
122
168
|
cachedWidth: undefined,
|
|
@@ -127,12 +173,56 @@ class BashResultRenderComponent extends Container {
|
|
|
127
173
|
function formatDuration(ms) {
|
|
128
174
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
129
175
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
function formatBashPrompt() {
|
|
177
|
+
return theme.fg("toolTitle", theme.bold("$ "));
|
|
178
|
+
}
|
|
179
|
+
function formatBashTimeout(timeout) {
|
|
180
|
+
return timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
|
|
181
|
+
}
|
|
182
|
+
function styleBashCommand(command) {
|
|
183
|
+
return highlightCode(command, "bash").join("\n");
|
|
184
|
+
}
|
|
185
|
+
function formatFullBashCall(command, timeout) {
|
|
186
|
+
const commandDisplay = command === null ? invalidArgText(theme) : command ? styleBashCommand(command) : theme.fg("toolOutput", "...");
|
|
187
|
+
return formatBashPrompt() + commandDisplay + formatBashTimeout(timeout);
|
|
188
|
+
}
|
|
189
|
+
function fitCollapsedBashCall(body, timeout, width) {
|
|
190
|
+
const prompt = formatBashPrompt();
|
|
191
|
+
const timeoutSuffix = formatBashTimeout(timeout);
|
|
192
|
+
const ellipsis = theme.fg("muted", " …");
|
|
193
|
+
const bodyWidth = Math.max(0, width - visibleWidth(prompt) - visibleWidth(ellipsis) - visibleWidth(timeoutSuffix));
|
|
194
|
+
const fittedBody = truncateToWidth(body, bodyWidth, "");
|
|
195
|
+
return truncateToWidth(prompt + fittedBody + ellipsis + timeoutSuffix, width, "...");
|
|
196
|
+
}
|
|
197
|
+
function formatBashSummary(commands, timeout, width) {
|
|
198
|
+
const separator = theme.fg("muted", ", ");
|
|
199
|
+
const prompt = formatBashPrompt();
|
|
200
|
+
const timeoutSuffix = formatBashTimeout(timeout);
|
|
201
|
+
const ellipsis = theme.fg("muted", " …");
|
|
202
|
+
const namesWidth = Math.max(0, width - visibleWidth(prompt) - visibleWidth(ellipsis) - visibleWidth(timeoutSuffix));
|
|
203
|
+
let names = "";
|
|
204
|
+
for (const command of commands) {
|
|
205
|
+
const styledCommand = theme.fg("toolOutput", command);
|
|
206
|
+
const candidate = names ? `${names}${separator}${styledCommand}` : styledCommand;
|
|
207
|
+
if (visibleWidth(candidate) > namesWidth)
|
|
208
|
+
break;
|
|
209
|
+
names = candidate;
|
|
210
|
+
}
|
|
211
|
+
if (!names) {
|
|
212
|
+
return truncateToWidth(prompt + theme.fg("muted", "…") + timeoutSuffix, width, "...");
|
|
213
|
+
}
|
|
214
|
+
return truncateToWidth(prompt + names + ellipsis + timeoutSuffix, width, "...");
|
|
215
|
+
}
|
|
216
|
+
function formatTruncatedBashCall(command, timeout, width) {
|
|
217
|
+
if (command === null)
|
|
218
|
+
return truncateToWidth(formatFullBashCall(command, timeout), width, "...");
|
|
219
|
+
if (!command)
|
|
220
|
+
return formatFullBashCall(command, timeout);
|
|
221
|
+
const firstNonEmptyLine = command
|
|
222
|
+
.split(/\r?\n/)
|
|
223
|
+
.find((line) => line.trim())
|
|
224
|
+
?.trim() ?? command.trim();
|
|
225
|
+
return fitCollapsedBashCall(styleBashCommand(firstNonEmptyLine), timeout, width);
|
|
136
226
|
}
|
|
137
227
|
function rebuildBashResultRenderComponent(component, result, options, showImages, startedAt, endedAt) {
|
|
138
228
|
const state = component.state;
|
|
@@ -332,9 +422,9 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
332
422
|
state.startedAt = Date.now();
|
|
333
423
|
state.endedAt = undefined;
|
|
334
424
|
}
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
return
|
|
425
|
+
const component = context.lastComponent ?? new BashCallRenderComponent();
|
|
426
|
+
component.update(args, context.argsComplete, context.expanded);
|
|
427
|
+
return component;
|
|
338
428
|
},
|
|
339
429
|
renderResult(result, options, _theme, context) {
|
|
340
430
|
const state = context.state;
|