@travisennis/acai 0.0.11 → 0.0.13
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/README.md +5 -5
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +2 -5
- package/dist/commands/history/types.d.ts +3 -2
- package/dist/commands/history/types.d.ts.map +1 -1
- package/dist/commands/init-project/utils.d.ts +0 -1
- package/dist/commands/init-project/utils.d.ts.map +1 -1
- package/dist/commands/init-project/utils.js +1 -12
- package/dist/commands/manager.d.ts.map +1 -1
- package/dist/commands/manager.js +6 -19
- package/dist/commands/resources/index.d.ts.map +1 -1
- package/dist/commands/resources/index.js +4 -1
- package/dist/commands/session/index.d.ts.map +1 -1
- package/dist/commands/session/index.js +6 -0
- package/dist/commands/session/types.d.ts +1 -0
- package/dist/commands/session/types.d.ts.map +1 -1
- package/dist/commands/share/html-renderer.d.ts.map +1 -1
- package/dist/commands/share/html-renderer.js +54 -48
- package/dist/commands/tools/index.d.ts +3 -0
- package/dist/commands/tools/index.d.ts.map +1 -0
- package/dist/commands/tools/index.js +191 -0
- package/dist/commands/tools/templates.d.ts +6 -0
- package/dist/commands/tools/templates.d.ts.map +1 -0
- package/dist/commands/tools/templates.js +97 -0
- package/dist/config/index.d.ts +5 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +42 -6
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -68
- package/dist/models/anthropic-provider.d.ts +1 -1
- package/dist/models/deepseek-provider.d.ts +3 -3
- package/dist/models/deepseek-provider.js +17 -17
- package/dist/models/google-provider.d.ts +2 -4
- package/dist/models/google-provider.d.ts.map +1 -1
- package/dist/models/google-provider.js +2 -17
- package/dist/models/groq-provider.d.ts +2 -4
- package/dist/models/groq-provider.d.ts.map +1 -1
- package/dist/models/groq-provider.js +3 -21
- package/dist/models/manager.d.ts +1 -10
- package/dist/models/manager.d.ts.map +1 -1
- package/dist/models/manager.js +2 -2
- package/dist/models/opencode-go-provider.d.ts +11 -1
- package/dist/models/opencode-go-provider.d.ts.map +1 -1
- package/dist/models/opencode-go-provider.js +130 -0
- package/dist/models/opencode-zen-provider.d.ts +3 -3
- package/dist/models/opencode-zen-provider.d.ts.map +1 -1
- package/dist/models/opencode-zen-provider.js +26 -32
- package/dist/models/openrouter-provider.d.ts +4 -15
- package/dist/models/openrouter-provider.d.ts.map +1 -1
- package/dist/models/openrouter-provider.js +26 -169
- package/dist/models/providers.d.ts +1 -1
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/xai-provider.d.ts +1 -2
- package/dist/models/xai-provider.d.ts.map +1 -1
- package/dist/models/xai-provider.js +0 -13
- package/dist/prompts/manager.d.ts.map +1 -1
- package/dist/prompts/manager.js +5 -1
- package/dist/prompts/system-prompt.d.ts +1 -2
- package/dist/prompts/system-prompt.d.ts.map +1 -1
- package/dist/prompts/system-prompt.js +26 -15
- package/dist/repl/index.d.ts +28 -13
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +242 -305
- package/dist/skills/activated-tracker.d.ts +11 -0
- package/dist/skills/activated-tracker.d.ts.map +1 -0
- package/dist/skills/activated-tracker.js +16 -0
- package/dist/skills/index.d.ts +12 -2
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +127 -71
- package/dist/terminal/control.d.ts +1 -21
- package/dist/terminal/control.d.ts.map +1 -1
- package/dist/terminal/control.js +1 -32
- package/dist/terminal/formatting.d.ts +0 -33
- package/dist/terminal/formatting.d.ts.map +1 -1
- package/dist/terminal/formatting.js +0 -91
- package/dist/terminal/highlight/theme.d.ts +0 -37
- package/dist/terminal/highlight/theme.d.ts.map +1 -1
- package/dist/terminal/highlight/theme.js +1 -79
- package/dist/terminal/keys.d.ts +0 -97
- package/dist/terminal/keys.d.ts.map +1 -1
- package/dist/terminal/keys.js +0 -194
- package/dist/terminal/select-prompt.d.ts.map +1 -1
- package/dist/terminal/select-prompt.js +65 -51
- package/dist/terminal/style.d.ts +1 -1
- package/dist/terminal/style.d.ts.map +1 -1
- package/dist/terminal/supports-color.d.ts.map +1 -1
- package/dist/terminal/supports-color.js +38 -20
- package/dist/terminal/supports-hyperlinks.d.ts +3 -0
- package/dist/terminal/supports-hyperlinks.d.ts.map +1 -1
- package/dist/terminal/supports-hyperlinks.js +72 -31
- package/dist/terminal/table/layout-manager.d.ts +0 -20
- package/dist/terminal/table/layout-manager.d.ts.map +1 -1
- package/dist/terminal/table/layout-manager.js +68 -44
- package/dist/terminal/table/utils.d.ts +0 -1
- package/dist/terminal/table/utils.d.ts.map +1 -1
- package/dist/terminal/table/utils.js +2 -4
- package/dist/tools/apply-patch.d.ts +5 -3
- package/dist/tools/apply-patch.d.ts.map +1 -1
- package/dist/tools/apply-patch.js +154 -123
- package/dist/tools/bash.d.ts +4 -4
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +27 -13
- package/dist/tools/dynamic-tool-loader.d.ts +17 -3
- package/dist/tools/dynamic-tool-loader.d.ts.map +1 -1
- package/dist/tools/dynamic-tool-loader.js +360 -81
- package/dist/tools/edit-file.d.ts +2 -2
- package/dist/tools/index.d.ts +27 -151
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +16 -21
- package/dist/tools/read-file.d.ts +8 -8
- package/dist/tools/read-file.d.ts.map +1 -1
- package/dist/tools/read-file.js +32 -13
- package/dist/tools/save-file.d.ts +4 -4
- package/dist/tools/skill.d.ts +2 -1
- package/dist/tools/skill.d.ts.map +1 -1
- package/dist/tools/skill.js +64 -14
- package/dist/tools/types.d.ts +8 -2
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/web-fetch.d.ts +8 -12
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +86 -44
- package/dist/tools/web-search.d.ts +4 -8
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +41 -33
- package/dist/tui/autocomplete/utils.d.ts +0 -15
- package/dist/tui/autocomplete/utils.d.ts.map +1 -1
- package/dist/tui/autocomplete/utils.js +0 -85
- package/dist/tui/autocomplete.d.ts +1 -1
- package/dist/tui/autocomplete.d.ts.map +1 -1
- package/dist/tui/autocomplete.js +1 -1
- package/dist/tui/components/editor.d.ts +12 -0
- package/dist/tui/components/editor.d.ts.map +1 -1
- package/dist/tui/components/editor.js +181 -195
- package/dist/tui/components/footer.d.ts +0 -2
- package/dist/tui/components/footer.d.ts.map +1 -1
- package/dist/tui/components/footer.js +1 -17
- package/dist/tui/components/input.d.ts +4 -0
- package/dist/tui/components/input.d.ts.map +1 -1
- package/dist/tui/components/input.js +33 -31
- package/dist/tui/components/markdown.d.ts +30 -0
- package/dist/tui/components/markdown.d.ts.map +1 -1
- package/dist/tui/components/markdown.js +274 -242
- package/dist/tui/components/modal.d.ts.map +1 -1
- package/dist/tui/components/modal.js +11 -11
- package/dist/tui/components/select-list.d.ts +5 -0
- package/dist/tui/components/select-list.d.ts.map +1 -1
- package/dist/tui/components/select-list.js +69 -98
- package/dist/tui/components/table.d.ts +6 -0
- package/dist/tui/components/table.d.ts.map +1 -1
- package/dist/tui/components/table.js +61 -52
- package/dist/tui/index.d.ts +1 -8
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +0 -8
- package/dist/tui/tui.d.ts +9 -0
- package/dist/tui/tui.d.ts.map +1 -1
- package/dist/tui/tui.js +78 -48
- package/dist/tui/utils.d.ts +7 -0
- package/dist/tui/utils.d.ts.map +1 -1
- package/dist/tui/utils.js +117 -53
- package/dist/utils/bash.d.ts.map +1 -1
- package/dist/utils/bash.js +121 -104
- package/dist/utils/binary-output.d.ts +32 -0
- package/dist/utils/binary-output.d.ts.map +1 -0
- package/dist/utils/binary-output.js +127 -0
- package/dist/utils/command-protection.d.ts +3 -2
- package/dist/utils/command-protection.d.ts.map +1 -1
- package/dist/utils/command-protection.js +121 -29
- package/dist/utils/filesystem/operations.d.ts +0 -15
- package/dist/utils/filesystem/operations.d.ts.map +1 -1
- package/dist/utils/filesystem/operations.js +1 -38
- package/dist/utils/filesystem/security.d.ts.map +1 -1
- package/dist/utils/filesystem/security.js +22 -15
- package/dist/utils/filetype-detection.d.ts +0 -1
- package/dist/utils/filetype-detection.d.ts.map +1 -1
- package/dist/utils/filetype-detection.js +0 -12
- package/dist/utils/formatting.d.ts +0 -30
- package/dist/utils/formatting.d.ts.map +1 -1
- package/dist/utils/formatting.js +0 -44
- package/dist/utils/git.d.ts +4 -6
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +36 -46
- package/dist/utils/ignore.d.ts +1 -1
- package/dist/utils/ignore.d.ts.map +1 -1
- package/dist/utils/ignore.js +1 -1
- package/dist/utils/parsing.d.ts +1 -1
- package/dist/utils/parsing.d.ts.map +1 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +77 -79
- package/dist/utils/yaml.d.ts +0 -1
- package/dist/utils/yaml.d.ts.map +1 -1
- package/dist/utils/yaml.js +80 -89
- package/dist/utils/zod.d.ts +0 -3
- package/dist/utils/zod.d.ts.map +1 -1
- package/dist/utils/zod.js +0 -7
- package/package.json +32 -29
- package/dist/agent/sub-agent.d.ts +0 -23
- package/dist/agent/sub-agent.d.ts.map +0 -1
- package/dist/agent/sub-agent.js +0 -109
- package/dist/commands/add-directory/index.d.ts +0 -3
- package/dist/commands/add-directory/index.d.ts.map +0 -1
- package/dist/commands/add-directory/index.js +0 -50
- package/dist/commands/add-directory/utils.d.ts +0 -3
- package/dist/commands/add-directory/utils.d.ts.map +0 -1
- package/dist/commands/add-directory/utils.js +0 -15
- package/dist/commands/clear/index.d.ts +0 -3
- package/dist/commands/clear/index.d.ts.map +0 -1
- package/dist/commands/clear/index.js +0 -13
- package/dist/commands/generate-rules/index.d.ts +0 -3
- package/dist/commands/generate-rules/index.d.ts.map +0 -1
- package/dist/commands/generate-rules/index.js +0 -206
- package/dist/commands/generate-rules/service.d.ts +0 -22
- package/dist/commands/generate-rules/service.d.ts.map +0 -1
- package/dist/commands/generate-rules/service.js +0 -103
- package/dist/commands/generate-rules/utils.d.ts +0 -5
- package/dist/commands/generate-rules/utils.d.ts.map +0 -1
- package/dist/commands/generate-rules/utils.js +0 -25
- package/dist/commands/handoff/index.d.ts +0 -3
- package/dist/commands/handoff/index.d.ts.map +0 -1
- package/dist/commands/handoff/index.js +0 -97
- package/dist/commands/handoff/utils.d.ts +0 -4
- package/dist/commands/handoff/utils.d.ts.map +0 -1
- package/dist/commands/handoff/utils.js +0 -123
- package/dist/commands/list-directories/index.d.ts +0 -3
- package/dist/commands/list-directories/index.d.ts.map +0 -1
- package/dist/commands/list-directories/index.js +0 -35
- package/dist/commands/pickup/index.d.ts +0 -3
- package/dist/commands/pickup/index.d.ts.map +0 -1
- package/dist/commands/pickup/index.js +0 -141
- package/dist/commands/pickup/types.d.ts +0 -6
- package/dist/commands/pickup/types.d.ts.map +0 -1
- package/dist/commands/pickup/types.js +0 -1
- package/dist/commands/pickup/utils.d.ts +0 -7
- package/dist/commands/pickup/utils.d.ts.map +0 -1
- package/dist/commands/pickup/utils.js +0 -56
- package/dist/commands/remove-directory/index.d.ts +0 -3
- package/dist/commands/remove-directory/index.d.ts.map +0 -1
- package/dist/commands/remove-directory/index.js +0 -55
- package/dist/commands/review/index.d.ts +0 -3
- package/dist/commands/review/index.d.ts.map +0 -1
- package/dist/commands/review/index.js +0 -12
- package/dist/commands/review/review-panel.d.ts +0 -3
- package/dist/commands/review/review-panel.d.ts.map +0 -1
- package/dist/commands/review/review-panel.js +0 -186
- package/dist/commands/review/utils.d.ts +0 -18
- package/dist/commands/review/utils.d.ts.map +0 -1
- package/dist/commands/review/utils.js +0 -146
- package/dist/commands/shell/index.d.ts +0 -3
- package/dist/commands/shell/index.d.ts.map +0 -1
- package/dist/commands/shell/index.js +0 -96
- package/dist/modes/manager.d.ts +0 -24
- package/dist/modes/manager.d.ts.map +0 -1
- package/dist/modes/manager.js +0 -77
- package/dist/modes/prompts.d.ts +0 -2
- package/dist/modes/prompts.d.ts.map +0 -1
- package/dist/modes/prompts.js +0 -142
- package/dist/subagents/index.d.ts +0 -16
- package/dist/subagents/index.d.ts.map +0 -1
- package/dist/subagents/index.js +0 -231
- package/dist/terminal/index.d.ts +0 -9
- package/dist/terminal/index.d.ts.map +0 -1
- package/dist/terminal/index.js +0 -8
- package/dist/tools/agent.d.ts +0 -27
- package/dist/tools/agent.d.ts.map +0 -1
- package/dist/tools/agent.js +0 -81
- package/dist/tools/directory-tree.d.ts +0 -28
- package/dist/tools/directory-tree.d.ts.map +0 -1
- package/dist/tools/directory-tree.js +0 -152
- package/dist/tools/glob.d.ts +0 -35
- package/dist/tools/glob.d.ts.map +0 -1
- package/dist/tools/glob.js +0 -159
- package/dist/tools/grep.d.ts +0 -100
- package/dist/tools/grep.d.ts.map +0 -1
- package/dist/tools/grep.js +0 -601
- package/dist/tools/ls.d.ts +0 -26
- package/dist/tools/ls.d.ts.map +0 -1
- package/dist/tools/ls.js +0 -82
- package/dist/tui/components/header.d.ts +0 -21
- package/dist/tui/components/header.d.ts.map +0 -1
- package/dist/tui/components/header.js +0 -63
- package/dist/utils/bash/parse.d.ts +0 -19
- package/dist/utils/bash/parse.d.ts.map +0 -1
- package/dist/utils/bash/parse.js +0 -223
- package/dist/utils/bash/quote.d.ts +0 -6
- package/dist/utils/bash/quote.d.ts.map +0 -1
- package/dist/utils/bash/quote.js +0 -23
- package/dist/utils/generators.d.ts +0 -3
- package/dist/utils/generators.d.ts.map +0 -1
- package/dist/utils/generators.js +0 -25
- package/dist/utils/glob.d.ts +0 -52
- package/dist/utils/glob.d.ts.map +0 -1
- package/dist/utils/glob.js +0 -376
package/dist/tui/utils.js
CHANGED
|
@@ -123,6 +123,20 @@ function truncateSimple(text, targetWidth, maxWidth) {
|
|
|
123
123
|
visibleSoFar: state.visibleSoFar,
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Find the end of the current text segment (until tab or ANSI code).
|
|
128
|
+
*/
|
|
129
|
+
function findSegmentEnd(text, start) {
|
|
130
|
+
let end = start;
|
|
131
|
+
while (end < text.length && text[end] !== "\t") {
|
|
132
|
+
const nextAnsi = extractAnsiCode(text, end);
|
|
133
|
+
if (nextAnsi) {
|
|
134
|
+
return end;
|
|
135
|
+
}
|
|
136
|
+
end++;
|
|
137
|
+
}
|
|
138
|
+
return end;
|
|
139
|
+
}
|
|
126
140
|
function truncateComplex(text, targetWidth, maxWidth) {
|
|
127
141
|
const state = createInitialState();
|
|
128
142
|
let i = 0;
|
|
@@ -140,14 +154,7 @@ function truncateComplex(text, targetWidth, maxWidth) {
|
|
|
140
154
|
continue;
|
|
141
155
|
}
|
|
142
156
|
// Find end of current text segment (until tab or ANSI)
|
|
143
|
-
|
|
144
|
-
while (end < text.length && text[end] !== "\t") {
|
|
145
|
-
const nextAnsi = extractAnsiCode(text, end);
|
|
146
|
-
if (nextAnsi) {
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
end++;
|
|
150
|
-
}
|
|
157
|
+
const end = findSegmentEnd(text, i);
|
|
151
158
|
// Process graphemes in this segment
|
|
152
159
|
for (const { segment } of getSegmenter().segment(text.slice(i, end))) {
|
|
153
160
|
if (processGraphemeSegment(state, segment, targetWidth, maxWidth)) {
|
|
@@ -192,12 +199,24 @@ export function truncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
|
|
|
192
199
|
}
|
|
193
200
|
// Fast path: pure ASCII printable
|
|
194
201
|
if (isPrintableAscii(text)) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
return truncateAscii(text, maxWidth, ellipsis, ellipsisWidth, pad);
|
|
203
|
+
}
|
|
204
|
+
return truncateNonAscii(text, maxWidth, ellipsis, ellipsisWidth, pad);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Truncate ASCII-only text with ellipsis.
|
|
208
|
+
*/
|
|
209
|
+
function truncateAscii(text, maxWidth, ellipsis, ellipsisWidth, pad) {
|
|
210
|
+
if (text.length <= maxWidth) {
|
|
211
|
+
return pad ? text + " ".repeat(maxWidth - text.length) : text;
|
|
200
212
|
}
|
|
213
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
214
|
+
return finalizeTruncatedResult(text.slice(0, targetWidth), targetWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Truncate text that may contain ANSI codes, tabs, or Unicode wide characters.
|
|
218
|
+
*/
|
|
219
|
+
function truncateNonAscii(text, maxWidth, ellipsis, ellipsisWidth, pad) {
|
|
201
220
|
const targetWidth = maxWidth - ellipsisWidth;
|
|
202
221
|
const hasAnsi = text.includes("\x1b");
|
|
203
222
|
const hasTabs = text.includes("\t");
|
|
@@ -221,31 +240,71 @@ export function truncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
|
|
|
221
240
|
return finalizeTruncatedResult(truncatedResult.result, truncatedResult.keptWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
222
241
|
}
|
|
223
242
|
/**
|
|
224
|
-
* Truncate
|
|
243
|
+
* Truncate text that contains only graphemes (no ANSI, no tabs) by
|
|
244
|
+
* iterating grapheme clusters up to maxWidth.
|
|
225
245
|
*/
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
246
|
+
function truncateGraphemesOnly(text, maxWidth) {
|
|
247
|
+
let result = "";
|
|
248
|
+
let width = 0;
|
|
249
|
+
for (const { segment } of getSegmenter().segment(text)) {
|
|
250
|
+
const w = graphemeWidth(segment);
|
|
251
|
+
if (width + w > maxWidth) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
result += segment;
|
|
255
|
+
width += w;
|
|
229
256
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
257
|
+
return { text: result, width };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Process a contiguous text segment (no tabs, no ANSI codes) within the
|
|
261
|
+
* larger ANSI/tab-aware truncation loop. Returns the consumed portion and
|
|
262
|
+
* updated state.
|
|
263
|
+
*/
|
|
264
|
+
function processTextSegment(text, start, maxWidth, currentWidth, pendingAnsi) {
|
|
265
|
+
// Find end of current text segment (until tab or ANSI code)
|
|
266
|
+
let end = start;
|
|
267
|
+
while (end < text.length && text[end] !== "\t") {
|
|
268
|
+
const nextAnsi = extractAnsiCode(text, end);
|
|
269
|
+
if (nextAnsi) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
end++;
|
|
233
273
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
274
|
+
let result = "";
|
|
275
|
+
let width = currentWidth;
|
|
276
|
+
let pending = pendingAnsi;
|
|
277
|
+
for (const { segment } of getSegmenter().segment(text.slice(start, end))) {
|
|
278
|
+
const w = graphemeWidth(segment);
|
|
279
|
+
if (width + w > maxWidth) {
|
|
280
|
+
return {
|
|
281
|
+
text: result,
|
|
282
|
+
width,
|
|
283
|
+
pendingAnsi: pending,
|
|
284
|
+
nextIndex: end,
|
|
285
|
+
exhausted: true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (pending) {
|
|
289
|
+
result += pending;
|
|
290
|
+
pending = "";
|
|
246
291
|
}
|
|
247
|
-
|
|
292
|
+
result += segment;
|
|
293
|
+
width += w;
|
|
248
294
|
}
|
|
295
|
+
return {
|
|
296
|
+
text: result,
|
|
297
|
+
width,
|
|
298
|
+
pendingAnsi: pending,
|
|
299
|
+
nextIndex: end,
|
|
300
|
+
exhausted: false,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Truncate text that contains ANSI codes and/or tab characters,
|
|
305
|
+
* correctly measuring visible width.
|
|
306
|
+
*/
|
|
307
|
+
function truncateWithAnsiAndTabs(text, maxWidth) {
|
|
249
308
|
let result = "";
|
|
250
309
|
let width = 0;
|
|
251
310
|
let i = 0;
|
|
@@ -270,30 +329,35 @@ function truncateFragmentToWidth(text, maxWidth) {
|
|
|
270
329
|
i++;
|
|
271
330
|
continue;
|
|
272
331
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
for (const { segment } of getSegmenter().segment(text.slice(i, end))) {
|
|
282
|
-
const w = graphemeWidth(segment);
|
|
283
|
-
if (width + w > maxWidth) {
|
|
284
|
-
return { text: result, width };
|
|
285
|
-
}
|
|
286
|
-
if (pendingAnsi) {
|
|
287
|
-
result += pendingAnsi;
|
|
288
|
-
pendingAnsi = "";
|
|
289
|
-
}
|
|
290
|
-
result += segment;
|
|
291
|
-
width += w;
|
|
332
|
+
const segmentResult = processTextSegment(text, i, maxWidth, width, pendingAnsi);
|
|
333
|
+
result += segmentResult.text;
|
|
334
|
+
width = segmentResult.width;
|
|
335
|
+
pendingAnsi = segmentResult.pendingAnsi;
|
|
336
|
+
i = segmentResult.nextIndex;
|
|
337
|
+
if (segmentResult.exhausted) {
|
|
338
|
+
break;
|
|
292
339
|
}
|
|
293
|
-
i = end;
|
|
294
340
|
}
|
|
295
341
|
return { text: result, width };
|
|
296
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Truncate a single fragment to a maximum width.
|
|
345
|
+
*/
|
|
346
|
+
export function truncateFragmentToWidth(text, maxWidth) {
|
|
347
|
+
if (maxWidth <= 0 || text.length === 0) {
|
|
348
|
+
return { text: "", width: 0 };
|
|
349
|
+
}
|
|
350
|
+
if (isPrintableAscii(text)) {
|
|
351
|
+
const clipped = text.slice(0, maxWidth);
|
|
352
|
+
return { text: clipped, width: clipped.length };
|
|
353
|
+
}
|
|
354
|
+
const hasAnsi = text.includes("\x1b");
|
|
355
|
+
const hasTabs = text.includes("\t");
|
|
356
|
+
if (!hasAnsi && !hasTabs) {
|
|
357
|
+
return truncateGraphemesOnly(text, maxWidth);
|
|
358
|
+
}
|
|
359
|
+
return truncateWithAnsiAndTabs(text, maxWidth);
|
|
360
|
+
}
|
|
297
361
|
/**
|
|
298
362
|
* Finalize a truncated result with proper ANSI reset codes.
|
|
299
363
|
* Only adds reset codes if the prefix contains ANSI sequences.
|
package/dist/utils/bash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../source/utils/bash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../source/utils/bash.ts"],"names":[],"mappings":"AAkNA,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBtC;AAiFD,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,YAAY,MAAM,EAClB,cAAc,MAAM,EAAE,KACrB,MAaF,CAAC;AA8EF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,OAetD,CAAC"}
|
package/dist/utils/bash.js
CHANGED
|
@@ -81,6 +81,33 @@ function isFullyQuoted(token) {
|
|
|
81
81
|
const last = token[token.length - 1];
|
|
82
82
|
return (first === '"' && last === '"') || (first === "'" && last === "'");
|
|
83
83
|
}
|
|
84
|
+
// Process a single git message flag token and update skip mask
|
|
85
|
+
// Returns true if the token was recognized as a message flag
|
|
86
|
+
function processGitMessageToken(tokens, i, t, skip) {
|
|
87
|
+
// --message=<msg> - skip this entire token
|
|
88
|
+
if (t.startsWith("--message=")) {
|
|
89
|
+
skip[i] = true;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// -m<msg> (attached message) - skip this entire token
|
|
93
|
+
if (/^-m.+/.test(t)) {
|
|
94
|
+
skip[i] = true;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// -m or --message consumes next token
|
|
98
|
+
if (t === "-m" || t === "--message") {
|
|
99
|
+
if (i + 1 < tokens.length)
|
|
100
|
+
skip[i + 1] = true;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
// Combined short opts containing m, e.g. -am, -avm
|
|
104
|
+
if (/^-[^-]+$/.test(t) && t.includes("m")) {
|
|
105
|
+
if (i + 1 < tokens.length)
|
|
106
|
+
skip[i + 1] = true;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
84
111
|
// Compute which tokens should be skipped from path validation
|
|
85
112
|
function computeSkipTokenMask(tokens) {
|
|
86
113
|
const skip = new Array(tokens.length).fill(false);
|
|
@@ -88,41 +115,52 @@ function computeSkipTokenMask(tokens) {
|
|
|
88
115
|
const sub = stripQuotes(tokens[1] ?? "");
|
|
89
116
|
// Commands where -m/--message is a string argument (not a path)
|
|
90
117
|
const gitMessageSubs = new Set(["commit", "merge", "tag", "revert", "notes"]);
|
|
91
|
-
if (bin
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
continue;
|
|
101
|
-
// --message=<msg> - skip this entire token
|
|
102
|
-
if (t.startsWith("--message=")) {
|
|
103
|
-
skip[i] = true;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
// -m<msg> (attached message) - skip this entire token
|
|
107
|
-
if (/^-m.+/.test(t)) {
|
|
108
|
-
skip[i] = true;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// -m or --message consumes next token
|
|
112
|
-
if (t === "-m" || t === "--message") {
|
|
113
|
-
if (i + 1 < tokens.length)
|
|
114
|
-
skip[i + 1] = true;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
// Combined short opts containing m, e.g. -am, -avm
|
|
118
|
-
if (/^-[^-]+$/.test(t) && t.includes("m")) {
|
|
119
|
-
if (i + 1 < tokens.length)
|
|
120
|
-
skip[i + 1] = true;
|
|
121
|
-
}
|
|
118
|
+
if (bin !== "git" || !gitMessageSubs.has(sub)) {
|
|
119
|
+
return skip;
|
|
120
|
+
}
|
|
121
|
+
let seenDoubleDash = false;
|
|
122
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
123
|
+
const t = stripQuotes(tokens[i] ?? "");
|
|
124
|
+
if (t === "--") {
|
|
125
|
+
seenDoubleDash = true;
|
|
126
|
+
continue;
|
|
122
127
|
}
|
|
128
|
+
if (seenDoubleDash)
|
|
129
|
+
continue;
|
|
130
|
+
processGitMessageToken(tokens, i, t, skip);
|
|
123
131
|
}
|
|
124
132
|
return skip;
|
|
125
133
|
}
|
|
134
|
+
function validateToken(token, allowedDirs, cwd) {
|
|
135
|
+
// Skip fully quoted tokens - they're string arguments, not paths
|
|
136
|
+
// (paths are typically unquoted or only quoted to handle spaces)
|
|
137
|
+
if (isFullyQuoted(token) && token.includes("\n")) {
|
|
138
|
+
return { isValid: true };
|
|
139
|
+
}
|
|
140
|
+
// Remove quotes for path checking
|
|
141
|
+
const cleanToken = stripQuotes(token);
|
|
142
|
+
// Skip if it's clearly not a path
|
|
143
|
+
if (cleanToken.startsWith("-") ||
|
|
144
|
+
cleanToken.includes("://") ||
|
|
145
|
+
(!cleanToken.includes("/") && cleanToken !== "~")) {
|
|
146
|
+
return { isValid: true };
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
// Expand ~ to home directory for proper validation
|
|
150
|
+
const expandedToken = cleanToken.startsWith("~/") || cleanToken === "~"
|
|
151
|
+
? path.join(os.homedir(), cleanToken.slice(1))
|
|
152
|
+
: cleanToken;
|
|
153
|
+
const resolvedPath = path.resolve(cwd, expandedToken);
|
|
154
|
+
if (!isPathWithinAllowedDirs(resolvedPath, allowedDirs)) {
|
|
155
|
+
return {
|
|
156
|
+
isValid: false,
|
|
157
|
+
error: `Path '${cleanToken}' resolves outside the allowed directories (${resolvedPath}). All paths must be within ${allowedDirs.join(", ")}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (_e) { }
|
|
162
|
+
return { isValid: true };
|
|
163
|
+
}
|
|
126
164
|
// Validate path arguments to ensure they're within the project
|
|
127
165
|
export function validatePaths(command, allowedDirs, cwd) {
|
|
128
166
|
const tokens = tokenizeShellWords(command);
|
|
@@ -135,75 +173,52 @@ export function validatePaths(command, allowedDirs, cwd) {
|
|
|
135
173
|
// Skip tokens marked by command-aware logic
|
|
136
174
|
if (skip[i])
|
|
137
175
|
continue;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
// Remove quotes for path checking
|
|
144
|
-
const cleanToken = stripQuotes(token);
|
|
145
|
-
// Skip if it's clearly not a path
|
|
146
|
-
if (cleanToken.startsWith("-") ||
|
|
147
|
-
cleanToken.includes("://") ||
|
|
148
|
-
(!cleanToken.includes("/") && cleanToken !== "~")) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
// Expand ~ to home directory for proper validation
|
|
153
|
-
const expandedToken = cleanToken.startsWith("~/") || cleanToken === "~"
|
|
154
|
-
? path.join(os.homedir(), cleanToken.slice(1))
|
|
155
|
-
: cleanToken;
|
|
156
|
-
const resolvedPath = path.resolve(cwd, expandedToken);
|
|
157
|
-
if (!isPathWithinAllowedDirs(resolvedPath, allowedDirs)) {
|
|
158
|
-
return {
|
|
159
|
-
isValid: false,
|
|
160
|
-
error: `Path '${cleanToken}' resolves outside the allowed directories (${resolvedPath}). All paths must be within ${allowedDirs.join(", ")}`,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch (_e) { }
|
|
176
|
+
const result = validateToken(token, allowedDirs, cwd);
|
|
177
|
+
if (!result.isValid)
|
|
178
|
+
return result;
|
|
165
179
|
}
|
|
166
180
|
return { isValid: true };
|
|
167
181
|
}
|
|
168
|
-
|
|
169
|
-
// Determine which directory to use as the base for resolving relative paths
|
|
170
|
-
// If allowedDirs is provided and non-empty, use the first allowed directory
|
|
171
|
-
// Otherwise use the workingDir parameter (backward compatibility)
|
|
182
|
+
function resolveBaseDir(allowedDirs, workingDir) {
|
|
172
183
|
const baseDirForResolution = allowedDirs && allowedDirs.length > 0 ? allowedDirs[0] : workingDir;
|
|
173
184
|
const projectRootAbs = path.resolve(baseDirForResolution);
|
|
174
|
-
let projectRoot = projectRootAbs;
|
|
175
185
|
try {
|
|
176
|
-
|
|
186
|
+
return fs.realpathSync(projectRootAbs);
|
|
177
187
|
}
|
|
178
188
|
catch {
|
|
179
|
-
|
|
189
|
+
return projectRootAbs;
|
|
180
190
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
}
|
|
192
|
+
function resolveRawCwd(cwdInput, projectRoot) {
|
|
193
|
+
if (typeof cwdInput === "string" && cwdInput.trim() !== "") {
|
|
194
|
+
const trimmed = cwdInput.trim();
|
|
195
|
+
return path.isAbsolute(trimmed)
|
|
196
|
+
? trimmed
|
|
197
|
+
: path.resolve(projectRoot, trimmed);
|
|
198
|
+
}
|
|
199
|
+
return projectRoot;
|
|
200
|
+
}
|
|
201
|
+
function resolveRealTarget(raw) {
|
|
186
202
|
try {
|
|
187
|
-
|
|
203
|
+
return fs.realpathSync(raw);
|
|
188
204
|
}
|
|
189
205
|
catch {
|
|
190
|
-
|
|
206
|
+
return raw;
|
|
191
207
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
208
|
+
}
|
|
209
|
+
function assertWithinAllowedDirs(target, allowedDirs, cwdInput) {
|
|
210
|
+
if (!isPathWithinAllowedDirs(target, allowedDirs)) {
|
|
211
|
+
throw new Error(`Working directory must be within the allowed directories: ${allowedDirs.join(", ")}. Received: ${cwdInput ?? "<default>"} -> ${target}`);
|
|
197
212
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
213
|
+
}
|
|
214
|
+
function assertWithinProjectRoot(target, projectRoot, cwdInput) {
|
|
215
|
+
const rel = path.relative(projectRoot, target);
|
|
216
|
+
const inside = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
217
|
+
if (!inside) {
|
|
218
|
+
throw new Error(`Working directory must be within the project directory: ${projectRoot}. Received: ${cwdInput ?? "<default>"} -> ${target}`);
|
|
205
219
|
}
|
|
206
|
-
|
|
220
|
+
}
|
|
221
|
+
function assertDirectoryExists(target) {
|
|
207
222
|
let stats;
|
|
208
223
|
try {
|
|
209
224
|
stats = fs.statSync(target);
|
|
@@ -211,13 +226,26 @@ export const resolveCwd = (cwdInput, workingDir, allowedDirs) => {
|
|
|
211
226
|
catch (err) {
|
|
212
227
|
const error = err;
|
|
213
228
|
if (error.code === "ENOENT") {
|
|
214
|
-
throw new Error(`Working directory does not exist: ${target}
|
|
229
|
+
throw new Error(`Working directory does not exist: ${target}`);
|
|
215
230
|
}
|
|
216
231
|
throw error;
|
|
217
232
|
}
|
|
218
233
|
if (!stats.isDirectory()) {
|
|
219
234
|
throw new Error(`Working directory is not a directory: ${target}`);
|
|
220
235
|
}
|
|
236
|
+
return stats;
|
|
237
|
+
}
|
|
238
|
+
export const resolveCwd = (cwdInput, workingDir, allowedDirs) => {
|
|
239
|
+
const projectRoot = resolveBaseDir(allowedDirs, workingDir);
|
|
240
|
+
const raw = resolveRawCwd(cwdInput, projectRoot);
|
|
241
|
+
const target = resolveRealTarget(raw);
|
|
242
|
+
if (allowedDirs && allowedDirs.length > 0) {
|
|
243
|
+
assertWithinAllowedDirs(target, allowedDirs, cwdInput);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
assertWithinProjectRoot(target, projectRoot, cwdInput);
|
|
247
|
+
}
|
|
248
|
+
assertDirectoryExists(target);
|
|
221
249
|
return target;
|
|
222
250
|
};
|
|
223
251
|
const mutatingBinaries = new Set([
|
|
@@ -278,27 +306,16 @@ function isSegmentMutating(seg) {
|
|
|
278
306
|
const bin = tokens[0];
|
|
279
307
|
if (!bin)
|
|
280
308
|
return false;
|
|
281
|
-
if (tokens.some((t) => actionMutating.has(t)))
|
|
309
|
+
if (tokens.some((t) => actionMutating.has(t)))
|
|
282
310
|
return true;
|
|
283
|
-
|
|
284
|
-
if (bin === "sed" && tokens.some((t) => /^-i/.test(t))) {
|
|
311
|
+
if (bin === "sed" && tokens.some((t) => /^-i/.test(t)))
|
|
285
312
|
return true;
|
|
286
|
-
|
|
287
|
-
|
|
313
|
+
if (mutatingBinaries.has(bin))
|
|
314
|
+
return true;
|
|
315
|
+
if (bin === "git" && tokens[1] && gitMutating.has(tokens[1]))
|
|
316
|
+
return true;
|
|
317
|
+
if (packageManagers.has(bin) && tokens[1] && npmMutating.has(tokens[1]))
|
|
288
318
|
return true;
|
|
289
|
-
}
|
|
290
|
-
if (bin === "git" && tokens.length > 1) {
|
|
291
|
-
const sub = tokens[1];
|
|
292
|
-
if (typeof sub === "string" && gitMutating.has(sub)) {
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (packageManagers.has(bin) && tokens.length > 1) {
|
|
297
|
-
const sub = tokens[1];
|
|
298
|
-
if (typeof sub === "string" && npmMutating.has(sub)) {
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
319
|
return false;
|
|
303
320
|
}
|
|
304
321
|
export const isMutatingCommand = (rawCommand) => {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary output handling for Bash tool
|
|
3
|
+
*
|
|
4
|
+
* Detects binary output from commands and saves it to temp files
|
|
5
|
+
* with helpful metadata for the user.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check if output appears to be binary data
|
|
9
|
+
* Binary is detected by:
|
|
10
|
+
* - Null bytes (most reliable indicator)
|
|
11
|
+
* - High ratio of non-printable characters
|
|
12
|
+
*/
|
|
13
|
+
export declare function isBinaryOutput(output: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Result of saving binary output
|
|
16
|
+
*/
|
|
17
|
+
export interface BinarySaveResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
path?: string;
|
|
20
|
+
size?: number;
|
|
21
|
+
mimeType?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Save binary output to a temp file and detect its MIME type
|
|
26
|
+
*/
|
|
27
|
+
export declare function saveBinaryOutput(output: string): BinarySaveResult;
|
|
28
|
+
/**
|
|
29
|
+
* Format a user-friendly message for binary output
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatBinaryMessage(result: BinarySaveResult): string;
|
|
32
|
+
//# sourceMappingURL=binary-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary-output.d.ts","sourceRoot":"","sources":["../../source/utils/binary-output.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CA4BtD;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CA4CjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAqBpE"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary output handling for Bash tool
|
|
3
|
+
*
|
|
4
|
+
* Detects binary output from commands and saves it to temp files
|
|
5
|
+
* with helpful metadata for the user.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
/**
|
|
12
|
+
* Threshold for checking binary content (check first N bytes)
|
|
13
|
+
*/
|
|
14
|
+
const BINARY_CHECK_BYTES = 8192;
|
|
15
|
+
/**
|
|
16
|
+
* Check if output appears to be binary data
|
|
17
|
+
* Binary is detected by:
|
|
18
|
+
* - Null bytes (most reliable indicator)
|
|
19
|
+
* - High ratio of non-printable characters
|
|
20
|
+
*/
|
|
21
|
+
export function isBinaryOutput(output) {
|
|
22
|
+
if (output.length === 0) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// Check first N bytes for null bytes (strongest binary indicator)
|
|
26
|
+
const checkLength = Math.min(output.length, BINARY_CHECK_BYTES);
|
|
27
|
+
const sample = output.slice(0, checkLength);
|
|
28
|
+
// Null byte is definitive binary indicator
|
|
29
|
+
if (sample.includes("\0")) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// Count non-printable characters (excluding common whitespace)
|
|
33
|
+
let nonPrintable = 0;
|
|
34
|
+
for (let i = 0; i < sample.length; i++) {
|
|
35
|
+
const code = sample.charCodeAt(i);
|
|
36
|
+
// Allow printable ASCII (32-126), newlines (10), tabs (9), and carriage returns (13)
|
|
37
|
+
// Also allow extended ASCII/UTF-8 (127+)
|
|
38
|
+
if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
|
|
39
|
+
nonPrintable++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// If more than 30% non-printable in the sample, treat as binary
|
|
43
|
+
const ratio = nonPrintable / sample.length;
|
|
44
|
+
return ratio > 0.3;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Save binary output to a temp file and detect its MIME type
|
|
48
|
+
*/
|
|
49
|
+
export function saveBinaryOutput(output) {
|
|
50
|
+
try {
|
|
51
|
+
// Generate unique filename
|
|
52
|
+
const id = randomBytes(8).toString("hex");
|
|
53
|
+
const filePath = `/tmp/acai/bash_binary_${id}`;
|
|
54
|
+
// Ensure directory exists
|
|
55
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
56
|
+
// Convert string back to buffer for accurate binary writing
|
|
57
|
+
// Note: Some data loss may have occurred during UTF-8 decoding
|
|
58
|
+
const buffer = Buffer.from(output, "utf8");
|
|
59
|
+
writeFileSync(filePath, buffer);
|
|
60
|
+
const size = buffer.length;
|
|
61
|
+
// Detect MIME type using `file` command
|
|
62
|
+
let mimeType = "application/octet-stream";
|
|
63
|
+
try {
|
|
64
|
+
const fileOutput = execSync(`file --mime-type -b "${filePath}"`, {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: 5000,
|
|
67
|
+
}).trim();
|
|
68
|
+
if (fileOutput && fileOutput !== "cannot open") {
|
|
69
|
+
mimeType = fileOutput;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// `file` command not available or failed, use default
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
path: filePath,
|
|
78
|
+
size,
|
|
79
|
+
mimeType,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: errorMessage,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Format a user-friendly message for binary output
|
|
92
|
+
*/
|
|
93
|
+
export function formatBinaryMessage(result) {
|
|
94
|
+
if (!result.success) {
|
|
95
|
+
return `⚠️ Binary output detected but could not be saved: ${result.error ?? "Unknown error"}`;
|
|
96
|
+
}
|
|
97
|
+
const sizeStr = formatBytes(result.size ?? 0);
|
|
98
|
+
const lines = [
|
|
99
|
+
"📦 Binary output detected",
|
|
100
|
+
"",
|
|
101
|
+
`**Size:** ${sizeStr}`,
|
|
102
|
+
`**Type:** ${result.mimeType}`,
|
|
103
|
+
`**Saved to:** \`${result.path}\``,
|
|
104
|
+
"",
|
|
105
|
+
"**To inspect this file, you can use:**",
|
|
106
|
+
" • `file <path>` - Detect file type",
|
|
107
|
+
" • `xxd <path>` - Hex dump",
|
|
108
|
+
" • `hexdump -C <path>` - Hex dump with ASCII",
|
|
109
|
+
" • `head -c 100 <path> | xxd` - Preview first 100 bytes",
|
|
110
|
+
];
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Format bytes as human-readable string
|
|
115
|
+
*/
|
|
116
|
+
function formatBytes(bytes) {
|
|
117
|
+
if (bytes === 0)
|
|
118
|
+
return "0 bytes";
|
|
119
|
+
const units = ["bytes", "KB", "MB", "GB"];
|
|
120
|
+
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
121
|
+
const size = bytes / 1024 ** unitIndex;
|
|
122
|
+
// Show decimal for KB and up, whole number for bytes
|
|
123
|
+
if (unitIndex === 0) {
|
|
124
|
+
return `${bytes} bytes`;
|
|
125
|
+
}
|
|
126
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
127
|
+
}
|