@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.
Files changed (292) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/index.d.ts.map +1 -1
  3. package/dist/agent/index.js +2 -5
  4. package/dist/commands/history/types.d.ts +3 -2
  5. package/dist/commands/history/types.d.ts.map +1 -1
  6. package/dist/commands/init-project/utils.d.ts +0 -1
  7. package/dist/commands/init-project/utils.d.ts.map +1 -1
  8. package/dist/commands/init-project/utils.js +1 -12
  9. package/dist/commands/manager.d.ts.map +1 -1
  10. package/dist/commands/manager.js +6 -19
  11. package/dist/commands/resources/index.d.ts.map +1 -1
  12. package/dist/commands/resources/index.js +4 -1
  13. package/dist/commands/session/index.d.ts.map +1 -1
  14. package/dist/commands/session/index.js +6 -0
  15. package/dist/commands/session/types.d.ts +1 -0
  16. package/dist/commands/session/types.d.ts.map +1 -1
  17. package/dist/commands/share/html-renderer.d.ts.map +1 -1
  18. package/dist/commands/share/html-renderer.js +54 -48
  19. package/dist/commands/tools/index.d.ts +3 -0
  20. package/dist/commands/tools/index.d.ts.map +1 -0
  21. package/dist/commands/tools/index.js +191 -0
  22. package/dist/commands/tools/templates.d.ts +6 -0
  23. package/dist/commands/tools/templates.d.ts.map +1 -0
  24. package/dist/commands/tools/templates.js +97 -0
  25. package/dist/config/index.d.ts +5 -2
  26. package/dist/config/index.d.ts.map +1 -1
  27. package/dist/config/index.js +42 -6
  28. package/dist/index.d.ts +14 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +98 -68
  31. package/dist/models/anthropic-provider.d.ts +1 -1
  32. package/dist/models/deepseek-provider.d.ts +3 -3
  33. package/dist/models/deepseek-provider.js +17 -17
  34. package/dist/models/google-provider.d.ts +2 -4
  35. package/dist/models/google-provider.d.ts.map +1 -1
  36. package/dist/models/google-provider.js +2 -17
  37. package/dist/models/groq-provider.d.ts +2 -4
  38. package/dist/models/groq-provider.d.ts.map +1 -1
  39. package/dist/models/groq-provider.js +3 -21
  40. package/dist/models/manager.d.ts +1 -10
  41. package/dist/models/manager.d.ts.map +1 -1
  42. package/dist/models/manager.js +2 -2
  43. package/dist/models/opencode-go-provider.d.ts +11 -1
  44. package/dist/models/opencode-go-provider.d.ts.map +1 -1
  45. package/dist/models/opencode-go-provider.js +130 -0
  46. package/dist/models/opencode-zen-provider.d.ts +3 -3
  47. package/dist/models/opencode-zen-provider.d.ts.map +1 -1
  48. package/dist/models/opencode-zen-provider.js +26 -32
  49. package/dist/models/openrouter-provider.d.ts +4 -15
  50. package/dist/models/openrouter-provider.d.ts.map +1 -1
  51. package/dist/models/openrouter-provider.js +26 -169
  52. package/dist/models/providers.d.ts +1 -1
  53. package/dist/models/providers.d.ts.map +1 -1
  54. package/dist/models/xai-provider.d.ts +1 -2
  55. package/dist/models/xai-provider.d.ts.map +1 -1
  56. package/dist/models/xai-provider.js +0 -13
  57. package/dist/prompts/manager.d.ts.map +1 -1
  58. package/dist/prompts/manager.js +5 -1
  59. package/dist/prompts/system-prompt.d.ts +1 -2
  60. package/dist/prompts/system-prompt.d.ts.map +1 -1
  61. package/dist/prompts/system-prompt.js +26 -15
  62. package/dist/repl/index.d.ts +28 -13
  63. package/dist/repl/index.d.ts.map +1 -1
  64. package/dist/repl/index.js +242 -305
  65. package/dist/skills/activated-tracker.d.ts +11 -0
  66. package/dist/skills/activated-tracker.d.ts.map +1 -0
  67. package/dist/skills/activated-tracker.js +16 -0
  68. package/dist/skills/index.d.ts +12 -2
  69. package/dist/skills/index.d.ts.map +1 -1
  70. package/dist/skills/index.js +127 -71
  71. package/dist/terminal/control.d.ts +1 -21
  72. package/dist/terminal/control.d.ts.map +1 -1
  73. package/dist/terminal/control.js +1 -32
  74. package/dist/terminal/formatting.d.ts +0 -33
  75. package/dist/terminal/formatting.d.ts.map +1 -1
  76. package/dist/terminal/formatting.js +0 -91
  77. package/dist/terminal/highlight/theme.d.ts +0 -37
  78. package/dist/terminal/highlight/theme.d.ts.map +1 -1
  79. package/dist/terminal/highlight/theme.js +1 -79
  80. package/dist/terminal/keys.d.ts +0 -97
  81. package/dist/terminal/keys.d.ts.map +1 -1
  82. package/dist/terminal/keys.js +0 -194
  83. package/dist/terminal/select-prompt.d.ts.map +1 -1
  84. package/dist/terminal/select-prompt.js +65 -51
  85. package/dist/terminal/style.d.ts +1 -1
  86. package/dist/terminal/style.d.ts.map +1 -1
  87. package/dist/terminal/supports-color.d.ts.map +1 -1
  88. package/dist/terminal/supports-color.js +38 -20
  89. package/dist/terminal/supports-hyperlinks.d.ts +3 -0
  90. package/dist/terminal/supports-hyperlinks.d.ts.map +1 -1
  91. package/dist/terminal/supports-hyperlinks.js +72 -31
  92. package/dist/terminal/table/layout-manager.d.ts +0 -20
  93. package/dist/terminal/table/layout-manager.d.ts.map +1 -1
  94. package/dist/terminal/table/layout-manager.js +68 -44
  95. package/dist/terminal/table/utils.d.ts +0 -1
  96. package/dist/terminal/table/utils.d.ts.map +1 -1
  97. package/dist/terminal/table/utils.js +2 -4
  98. package/dist/tools/apply-patch.d.ts +5 -3
  99. package/dist/tools/apply-patch.d.ts.map +1 -1
  100. package/dist/tools/apply-patch.js +154 -123
  101. package/dist/tools/bash.d.ts +4 -4
  102. package/dist/tools/bash.d.ts.map +1 -1
  103. package/dist/tools/bash.js +27 -13
  104. package/dist/tools/dynamic-tool-loader.d.ts +17 -3
  105. package/dist/tools/dynamic-tool-loader.d.ts.map +1 -1
  106. package/dist/tools/dynamic-tool-loader.js +360 -81
  107. package/dist/tools/edit-file.d.ts +2 -2
  108. package/dist/tools/index.d.ts +27 -151
  109. package/dist/tools/index.d.ts.map +1 -1
  110. package/dist/tools/index.js +16 -21
  111. package/dist/tools/read-file.d.ts +8 -8
  112. package/dist/tools/read-file.d.ts.map +1 -1
  113. package/dist/tools/read-file.js +32 -13
  114. package/dist/tools/save-file.d.ts +4 -4
  115. package/dist/tools/skill.d.ts +2 -1
  116. package/dist/tools/skill.d.ts.map +1 -1
  117. package/dist/tools/skill.js +64 -14
  118. package/dist/tools/types.d.ts +8 -2
  119. package/dist/tools/types.d.ts.map +1 -1
  120. package/dist/tools/web-fetch.d.ts +8 -12
  121. package/dist/tools/web-fetch.d.ts.map +1 -1
  122. package/dist/tools/web-fetch.js +86 -44
  123. package/dist/tools/web-search.d.ts +4 -8
  124. package/dist/tools/web-search.d.ts.map +1 -1
  125. package/dist/tools/web-search.js +41 -33
  126. package/dist/tui/autocomplete/utils.d.ts +0 -15
  127. package/dist/tui/autocomplete/utils.d.ts.map +1 -1
  128. package/dist/tui/autocomplete/utils.js +0 -85
  129. package/dist/tui/autocomplete.d.ts +1 -1
  130. package/dist/tui/autocomplete.d.ts.map +1 -1
  131. package/dist/tui/autocomplete.js +1 -1
  132. package/dist/tui/components/editor.d.ts +12 -0
  133. package/dist/tui/components/editor.d.ts.map +1 -1
  134. package/dist/tui/components/editor.js +181 -195
  135. package/dist/tui/components/footer.d.ts +0 -2
  136. package/dist/tui/components/footer.d.ts.map +1 -1
  137. package/dist/tui/components/footer.js +1 -17
  138. package/dist/tui/components/input.d.ts +4 -0
  139. package/dist/tui/components/input.d.ts.map +1 -1
  140. package/dist/tui/components/input.js +33 -31
  141. package/dist/tui/components/markdown.d.ts +30 -0
  142. package/dist/tui/components/markdown.d.ts.map +1 -1
  143. package/dist/tui/components/markdown.js +274 -242
  144. package/dist/tui/components/modal.d.ts.map +1 -1
  145. package/dist/tui/components/modal.js +11 -11
  146. package/dist/tui/components/select-list.d.ts +5 -0
  147. package/dist/tui/components/select-list.d.ts.map +1 -1
  148. package/dist/tui/components/select-list.js +69 -98
  149. package/dist/tui/components/table.d.ts +6 -0
  150. package/dist/tui/components/table.d.ts.map +1 -1
  151. package/dist/tui/components/table.js +61 -52
  152. package/dist/tui/index.d.ts +1 -8
  153. package/dist/tui/index.d.ts.map +1 -1
  154. package/dist/tui/index.js +0 -8
  155. package/dist/tui/tui.d.ts +9 -0
  156. package/dist/tui/tui.d.ts.map +1 -1
  157. package/dist/tui/tui.js +78 -48
  158. package/dist/tui/utils.d.ts +7 -0
  159. package/dist/tui/utils.d.ts.map +1 -1
  160. package/dist/tui/utils.js +117 -53
  161. package/dist/utils/bash.d.ts.map +1 -1
  162. package/dist/utils/bash.js +121 -104
  163. package/dist/utils/binary-output.d.ts +32 -0
  164. package/dist/utils/binary-output.d.ts.map +1 -0
  165. package/dist/utils/binary-output.js +127 -0
  166. package/dist/utils/command-protection.d.ts +3 -2
  167. package/dist/utils/command-protection.d.ts.map +1 -1
  168. package/dist/utils/command-protection.js +121 -29
  169. package/dist/utils/filesystem/operations.d.ts +0 -15
  170. package/dist/utils/filesystem/operations.d.ts.map +1 -1
  171. package/dist/utils/filesystem/operations.js +1 -38
  172. package/dist/utils/filesystem/security.d.ts.map +1 -1
  173. package/dist/utils/filesystem/security.js +22 -15
  174. package/dist/utils/filetype-detection.d.ts +0 -1
  175. package/dist/utils/filetype-detection.d.ts.map +1 -1
  176. package/dist/utils/filetype-detection.js +0 -12
  177. package/dist/utils/formatting.d.ts +0 -30
  178. package/dist/utils/formatting.d.ts.map +1 -1
  179. package/dist/utils/formatting.js +0 -44
  180. package/dist/utils/git.d.ts +4 -6
  181. package/dist/utils/git.d.ts.map +1 -1
  182. package/dist/utils/git.js +36 -46
  183. package/dist/utils/ignore.d.ts +1 -1
  184. package/dist/utils/ignore.d.ts.map +1 -1
  185. package/dist/utils/ignore.js +1 -1
  186. package/dist/utils/parsing.d.ts +1 -1
  187. package/dist/utils/parsing.d.ts.map +1 -1
  188. package/dist/utils/process.d.ts.map +1 -1
  189. package/dist/utils/process.js +77 -79
  190. package/dist/utils/yaml.d.ts +0 -1
  191. package/dist/utils/yaml.d.ts.map +1 -1
  192. package/dist/utils/yaml.js +80 -89
  193. package/dist/utils/zod.d.ts +0 -3
  194. package/dist/utils/zod.d.ts.map +1 -1
  195. package/dist/utils/zod.js +0 -7
  196. package/package.json +32 -29
  197. package/dist/agent/sub-agent.d.ts +0 -23
  198. package/dist/agent/sub-agent.d.ts.map +0 -1
  199. package/dist/agent/sub-agent.js +0 -109
  200. package/dist/commands/add-directory/index.d.ts +0 -3
  201. package/dist/commands/add-directory/index.d.ts.map +0 -1
  202. package/dist/commands/add-directory/index.js +0 -50
  203. package/dist/commands/add-directory/utils.d.ts +0 -3
  204. package/dist/commands/add-directory/utils.d.ts.map +0 -1
  205. package/dist/commands/add-directory/utils.js +0 -15
  206. package/dist/commands/clear/index.d.ts +0 -3
  207. package/dist/commands/clear/index.d.ts.map +0 -1
  208. package/dist/commands/clear/index.js +0 -13
  209. package/dist/commands/generate-rules/index.d.ts +0 -3
  210. package/dist/commands/generate-rules/index.d.ts.map +0 -1
  211. package/dist/commands/generate-rules/index.js +0 -206
  212. package/dist/commands/generate-rules/service.d.ts +0 -22
  213. package/dist/commands/generate-rules/service.d.ts.map +0 -1
  214. package/dist/commands/generate-rules/service.js +0 -103
  215. package/dist/commands/generate-rules/utils.d.ts +0 -5
  216. package/dist/commands/generate-rules/utils.d.ts.map +0 -1
  217. package/dist/commands/generate-rules/utils.js +0 -25
  218. package/dist/commands/handoff/index.d.ts +0 -3
  219. package/dist/commands/handoff/index.d.ts.map +0 -1
  220. package/dist/commands/handoff/index.js +0 -97
  221. package/dist/commands/handoff/utils.d.ts +0 -4
  222. package/dist/commands/handoff/utils.d.ts.map +0 -1
  223. package/dist/commands/handoff/utils.js +0 -123
  224. package/dist/commands/list-directories/index.d.ts +0 -3
  225. package/dist/commands/list-directories/index.d.ts.map +0 -1
  226. package/dist/commands/list-directories/index.js +0 -35
  227. package/dist/commands/pickup/index.d.ts +0 -3
  228. package/dist/commands/pickup/index.d.ts.map +0 -1
  229. package/dist/commands/pickup/index.js +0 -141
  230. package/dist/commands/pickup/types.d.ts +0 -6
  231. package/dist/commands/pickup/types.d.ts.map +0 -1
  232. package/dist/commands/pickup/types.js +0 -1
  233. package/dist/commands/pickup/utils.d.ts +0 -7
  234. package/dist/commands/pickup/utils.d.ts.map +0 -1
  235. package/dist/commands/pickup/utils.js +0 -56
  236. package/dist/commands/remove-directory/index.d.ts +0 -3
  237. package/dist/commands/remove-directory/index.d.ts.map +0 -1
  238. package/dist/commands/remove-directory/index.js +0 -55
  239. package/dist/commands/review/index.d.ts +0 -3
  240. package/dist/commands/review/index.d.ts.map +0 -1
  241. package/dist/commands/review/index.js +0 -12
  242. package/dist/commands/review/review-panel.d.ts +0 -3
  243. package/dist/commands/review/review-panel.d.ts.map +0 -1
  244. package/dist/commands/review/review-panel.js +0 -186
  245. package/dist/commands/review/utils.d.ts +0 -18
  246. package/dist/commands/review/utils.d.ts.map +0 -1
  247. package/dist/commands/review/utils.js +0 -146
  248. package/dist/commands/shell/index.d.ts +0 -3
  249. package/dist/commands/shell/index.d.ts.map +0 -1
  250. package/dist/commands/shell/index.js +0 -96
  251. package/dist/modes/manager.d.ts +0 -24
  252. package/dist/modes/manager.d.ts.map +0 -1
  253. package/dist/modes/manager.js +0 -77
  254. package/dist/modes/prompts.d.ts +0 -2
  255. package/dist/modes/prompts.d.ts.map +0 -1
  256. package/dist/modes/prompts.js +0 -142
  257. package/dist/subagents/index.d.ts +0 -16
  258. package/dist/subagents/index.d.ts.map +0 -1
  259. package/dist/subagents/index.js +0 -231
  260. package/dist/terminal/index.d.ts +0 -9
  261. package/dist/terminal/index.d.ts.map +0 -1
  262. package/dist/terminal/index.js +0 -8
  263. package/dist/tools/agent.d.ts +0 -27
  264. package/dist/tools/agent.d.ts.map +0 -1
  265. package/dist/tools/agent.js +0 -81
  266. package/dist/tools/directory-tree.d.ts +0 -28
  267. package/dist/tools/directory-tree.d.ts.map +0 -1
  268. package/dist/tools/directory-tree.js +0 -152
  269. package/dist/tools/glob.d.ts +0 -35
  270. package/dist/tools/glob.d.ts.map +0 -1
  271. package/dist/tools/glob.js +0 -159
  272. package/dist/tools/grep.d.ts +0 -100
  273. package/dist/tools/grep.d.ts.map +0 -1
  274. package/dist/tools/grep.js +0 -601
  275. package/dist/tools/ls.d.ts +0 -26
  276. package/dist/tools/ls.d.ts.map +0 -1
  277. package/dist/tools/ls.js +0 -82
  278. package/dist/tui/components/header.d.ts +0 -21
  279. package/dist/tui/components/header.d.ts.map +0 -1
  280. package/dist/tui/components/header.js +0 -63
  281. package/dist/utils/bash/parse.d.ts +0 -19
  282. package/dist/utils/bash/parse.d.ts.map +0 -1
  283. package/dist/utils/bash/parse.js +0 -223
  284. package/dist/utils/bash/quote.d.ts +0 -6
  285. package/dist/utils/bash/quote.d.ts.map +0 -1
  286. package/dist/utils/bash/quote.js +0 -23
  287. package/dist/utils/generators.d.ts +0 -3
  288. package/dist/utils/generators.d.ts.map +0 -1
  289. package/dist/utils/generators.js +0 -25
  290. package/dist/utils/glob.d.ts +0 -52
  291. package/dist/utils/glob.d.ts.map +0 -1
  292. 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
- let end = i;
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
- if (text.length <= maxWidth) {
196
- return pad ? text + " ".repeat(maxWidth - text.length) : text;
197
- }
198
- const targetWidth = maxWidth - ellipsisWidth;
199
- return finalizeTruncatedResult(text.slice(0, targetWidth), targetWidth, ellipsis, ellipsisWidth, maxWidth, pad);
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 a single fragment to a maximum width.
243
+ * Truncate text that contains only graphemes (no ANSI, no tabs) by
244
+ * iterating grapheme clusters up to maxWidth.
225
245
  */
226
- function truncateFragmentToWidth(text, maxWidth) {
227
- if (maxWidth <= 0 || text.length === 0) {
228
- return { text: "", width: 0 };
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
- if (isPrintableAscii(text)) {
231
- const clipped = text.slice(0, maxWidth);
232
- return { text: clipped, width: clipped.length };
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
- const hasAnsi = text.includes("\x1b");
235
- const hasTabs = text.includes("\t");
236
- if (!hasAnsi && !hasTabs) {
237
- let result = "";
238
- let width = 0;
239
- for (const { segment } of getSegmenter().segment(text)) {
240
- const w = graphemeWidth(segment);
241
- if (width + w > maxWidth) {
242
- break;
243
- }
244
- result += segment;
245
- width += w;
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
- return { text: result, width };
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
- let end = i;
274
- while (end < text.length && text[end] !== "\t") {
275
- const nextAnsi = extractAnsiCode(text, end);
276
- if (nextAnsi) {
277
- break;
278
- }
279
- end++;
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.
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../source/utils/bash.ts"],"names":[],"mappings":"AAuJA,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,CAiDtC;AAED,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,YAAY,MAAM,EAClB,cAAc,MAAM,EAAE,KACrB,MAkEF,CAAC;AA6FF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,OAetD,CAAC"}
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"}
@@ -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 === "git" && gitMessageSubs.has(sub)) {
92
- let seenDoubleDash = false;
93
- for (let i = 2; i < tokens.length; i++) {
94
- const t = stripQuotes(tokens[i] ?? "");
95
- if (t === "--") {
96
- seenDoubleDash = true;
97
- continue;
98
- }
99
- if (seenDoubleDash)
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
- // Skip fully quoted tokens - they're string arguments, not paths
139
- // (paths are typically unquoted or only quoted to handle spaces)
140
- if (isFullyQuoted(token) && token.includes("\n")) {
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
- export const resolveCwd = (cwdInput, workingDir, allowedDirs) => {
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
- projectRoot = fs.realpathSync(projectRootAbs);
186
+ return fs.realpathSync(projectRootAbs);
177
187
  }
178
188
  catch {
179
- // Fallback to resolved path
189
+ return projectRootAbs;
180
190
  }
181
- const raw = typeof cwdInput === "string" && cwdInput.trim() !== ""
182
- ? cwdInput.trim()
183
- : projectRoot;
184
- const abs = path.isAbsolute(raw) ? raw : path.resolve(projectRoot, raw);
185
- let target = abs;
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
- target = fs.realpathSync(abs);
203
+ return fs.realpathSync(raw);
188
204
  }
189
205
  catch {
190
- // If the path doesn't exist entirely, validate intended path
206
+ return raw;
191
207
  }
192
- // Check if within allowed directories if provided, otherwise check project root
193
- if (allowedDirs && allowedDirs.length > 0) {
194
- if (!isPathWithinAllowedDirs(target, allowedDirs)) {
195
- throw new Error(`Working directory must be within the allowed directories: ${allowedDirs.join(", ")}. Received: ${cwdInput ?? "<default>"} -> ${target}`);
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
- else {
199
- // Fallback to original behavior: check within project root
200
- const rel = path.relative(projectRoot, target);
201
- const inside = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
202
- if (!inside) {
203
- throw new Error(`Working directory must be within the project directory: ${projectRoot}. Received: ${cwdInput ?? "<default>"} -> ${target}`);
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
- // Check existence and that it's a directory
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} (from ${cwdInput ?? "<default>"})`);
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
- if (mutatingBinaries.has(bin)) {
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
+ }