@travisennis/acai 0.0.11 → 0.0.12

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 (110) hide show
  1. package/README.md +2 -3
  2. package/dist/commands/init-project/utils.d.ts.map +1 -1
  3. package/dist/commands/init-project/utils.js +0 -11
  4. package/dist/commands/manager.d.ts.map +1 -1
  5. package/dist/commands/manager.js +6 -1
  6. package/dist/commands/resources/index.d.ts.map +1 -1
  7. package/dist/commands/resources/index.js +4 -1
  8. package/dist/commands/session/index.d.ts.map +1 -1
  9. package/dist/commands/session/index.js +6 -0
  10. package/dist/commands/session/types.d.ts +1 -0
  11. package/dist/commands/session/types.d.ts.map +1 -1
  12. package/dist/commands/tools/index.d.ts +3 -0
  13. package/dist/commands/tools/index.d.ts.map +1 -0
  14. package/dist/commands/tools/index.js +190 -0
  15. package/dist/commands/tools/templates.d.ts +6 -0
  16. package/dist/commands/tools/templates.d.ts.map +1 -0
  17. package/dist/commands/tools/templates.js +97 -0
  18. package/dist/config/index.d.ts +5 -0
  19. package/dist/config/index.d.ts.map +1 -1
  20. package/dist/config/index.js +41 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -3
  23. package/dist/models/anthropic-provider.d.ts +1 -1
  24. package/dist/models/deepseek-provider.d.ts +3 -3
  25. package/dist/models/deepseek-provider.js +17 -17
  26. package/dist/models/google-provider.d.ts +2 -4
  27. package/dist/models/google-provider.d.ts.map +1 -1
  28. package/dist/models/google-provider.js +2 -17
  29. package/dist/models/groq-provider.d.ts +2 -4
  30. package/dist/models/groq-provider.d.ts.map +1 -1
  31. package/dist/models/groq-provider.js +3 -21
  32. package/dist/models/opencode-go-provider.d.ts +11 -1
  33. package/dist/models/opencode-go-provider.d.ts.map +1 -1
  34. package/dist/models/opencode-go-provider.js +136 -0
  35. package/dist/models/opencode-zen-provider.d.ts +3 -3
  36. package/dist/models/opencode-zen-provider.d.ts.map +1 -1
  37. package/dist/models/opencode-zen-provider.js +26 -32
  38. package/dist/models/openrouter-provider.d.ts +4 -15
  39. package/dist/models/openrouter-provider.d.ts.map +1 -1
  40. package/dist/models/openrouter-provider.js +26 -169
  41. package/dist/models/providers.d.ts +1 -1
  42. package/dist/models/providers.d.ts.map +1 -1
  43. package/dist/models/xai-provider.d.ts +1 -2
  44. package/dist/models/xai-provider.d.ts.map +1 -1
  45. package/dist/models/xai-provider.js +0 -13
  46. package/dist/prompts/manager.d.ts.map +1 -1
  47. package/dist/prompts/manager.js +5 -1
  48. package/dist/prompts/system-prompt.d.ts +1 -0
  49. package/dist/prompts/system-prompt.d.ts.map +1 -1
  50. package/dist/prompts/system-prompt.js +20 -5
  51. package/dist/repl/index.d.ts +1 -2
  52. package/dist/repl/index.d.ts.map +1 -1
  53. package/dist/repl/index.js +5 -52
  54. package/dist/skills/activated-tracker.d.ts +11 -0
  55. package/dist/skills/activated-tracker.d.ts.map +1 -0
  56. package/dist/skills/activated-tracker.js +16 -0
  57. package/dist/skills/index.d.ts +1 -1
  58. package/dist/skills/index.d.ts.map +1 -1
  59. package/dist/skills/index.js +7 -1
  60. package/dist/tools/bash.d.ts +4 -4
  61. package/dist/tools/bash.d.ts.map +1 -1
  62. package/dist/tools/bash.js +17 -6
  63. package/dist/tools/directory-tree.d.ts +4 -4
  64. package/dist/tools/directory-tree.d.ts.map +1 -1
  65. package/dist/tools/directory-tree.js +2 -0
  66. package/dist/tools/dynamic-tool-loader.d.ts +11 -2
  67. package/dist/tools/dynamic-tool-loader.d.ts.map +1 -1
  68. package/dist/tools/dynamic-tool-loader.js +299 -39
  69. package/dist/tools/edit-file.d.ts +2 -2
  70. package/dist/tools/glob.d.ts +16 -16
  71. package/dist/tools/glob.d.ts.map +1 -1
  72. package/dist/tools/glob.js +9 -1
  73. package/dist/tools/grep.d.ts +14 -14
  74. package/dist/tools/grep.d.ts.map +1 -1
  75. package/dist/tools/grep.js +7 -0
  76. package/dist/tools/index.d.ts +42 -36
  77. package/dist/tools/index.d.ts.map +1 -1
  78. package/dist/tools/index.js +16 -1
  79. package/dist/tools/ls.d.ts +2 -2
  80. package/dist/tools/ls.d.ts.map +1 -1
  81. package/dist/tools/ls.js +1 -0
  82. package/dist/tools/read-file.d.ts +8 -8
  83. package/dist/tools/save-file.d.ts +4 -4
  84. package/dist/tools/skill.d.ts +2 -1
  85. package/dist/tools/skill.d.ts.map +1 -1
  86. package/dist/tools/skill.js +55 -12
  87. package/dist/tools/types.d.ts +8 -2
  88. package/dist/tools/types.d.ts.map +1 -1
  89. package/dist/tools/web-fetch.d.ts +6 -6
  90. package/dist/tools/web-fetch.d.ts.map +1 -1
  91. package/dist/tools/web-fetch.js +27 -8
  92. package/dist/tools/web-search.d.ts +4 -4
  93. package/dist/tools/web-search.js +1 -1
  94. package/dist/tui/components/footer.d.ts +0 -2
  95. package/dist/tui/components/footer.d.ts.map +1 -1
  96. package/dist/tui/components/footer.js +1 -17
  97. package/dist/utils/binary-output.d.ts +32 -0
  98. package/dist/utils/binary-output.d.ts.map +1 -0
  99. package/dist/utils/binary-output.js +127 -0
  100. package/dist/utils/command-protection.d.ts.map +1 -1
  101. package/dist/utils/command-protection.js +92 -9
  102. package/dist/utils/parsing.d.ts +1 -1
  103. package/dist/utils/parsing.d.ts.map +1 -1
  104. package/package.json +27 -25
  105. package/dist/modes/manager.d.ts +0 -24
  106. package/dist/modes/manager.d.ts.map +0 -1
  107. package/dist/modes/manager.js +0 -77
  108. package/dist/modes/prompts.d.ts +0 -2
  109. package/dist/modes/prompts.d.ts.map +0 -1
  110. package/dist/modes/prompts.js +0 -142
@@ -1,3 +1,6 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { load } from "cheerio";
2
5
  import { z } from "zod";
3
6
  import style from "../terminal/style.js";
@@ -9,6 +12,7 @@ const DEFAULT_TIMEOUT = 30000; // 30 seconds
9
12
  const MAX_REDIRECTS = 5;
10
13
  const MAX_URL_LENGTH = 2048;
11
14
  const JINA_API_BASE = "https://r.jina.ai";
15
+ const MAX_INLINE_SIZE = 50_000; // bytes; content exceeding this is written to a temp file
12
16
  /**
13
17
  * Input schema for the web fetch tool
14
18
  */
@@ -400,6 +404,7 @@ export async function executeWebFetch(options, executionOptions) {
400
404
  const errorMessage = error instanceof Error ? error.message : String(error);
401
405
  throw new Error(`Web fetch failed: ${errorMessage}`);
402
406
  }
407
+ let content;
403
408
  switch (output) {
404
409
  case "json": {
405
410
  const jsonOutput = {
@@ -413,18 +418,32 @@ export async function executeWebFetch(options, executionOptions) {
413
418
  if (headers && result.headers) {
414
419
  jsonOutput["headers"] = result["headers"];
415
420
  }
416
- return JSON.stringify(jsonOutput, null, 2);
421
+ content = JSON.stringify(jsonOutput, null, 2);
422
+ break;
417
423
  }
418
424
  case "html":
419
- return result.content;
425
+ content = result.content;
426
+ break;
420
427
  case "markdown":
421
- if (result.contentType.includes("text/html")) {
422
- return htmlToMarkdown(result.content);
423
- }
424
- return result.content;
428
+ content = result.contentType.includes("text/html")
429
+ ? htmlToMarkdown(result.content)
430
+ : result.content;
431
+ break;
425
432
  default:
426
- return result.content;
433
+ content = result.content;
434
+ break;
435
+ }
436
+ const contentSize = Buffer.byteLength(content, "utf-8");
437
+ if (contentSize > MAX_INLINE_SIZE) {
438
+ const tmpFile = join(tmpdir(), `acai-webfetch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`);
439
+ writeFileSync(tmpFile, content, "utf-8");
440
+ return [
441
+ `The fetched content from ${url} is ${contentSize} bytes which exceeds the ${MAX_INLINE_SIZE} byte inline limit.`,
442
+ `The full content has been saved to: ${tmpFile}`,
443
+ "To use this content, read the file in parts using the Read tool or search it with the Grep tool.",
444
+ ].join("\n");
427
445
  }
446
+ return content;
428
447
  }
429
448
  /**
430
449
  * Create the web fetch tool
@@ -437,7 +456,7 @@ export const createWebFetchTool = async () => {
437
456
  inputSchema,
438
457
  },
439
458
  display({ url }) {
440
- return `🌐 ${style.cyan(url)}`;
459
+ return `${style.cyan(url)}`;
441
460
  },
442
461
  async execute(options, executionOptions) {
443
462
  return executeWebFetch(options, executionOptions);
@@ -8,8 +8,8 @@ export declare const WebSearchTool: {
8
8
  */
9
9
  declare const inputSchema: z.ZodObject<{
10
10
  query: z.ZodString;
11
- numResults: z.ZodOptional<z.ZodPipe<z.ZodTransform<string | null, unknown>, z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
12
- timeout: z.ZodOptional<z.ZodPipe<z.ZodTransform<string | null, unknown>, z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
11
+ numResults: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
12
+ timeout: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
13
13
  provider: z.ZodOptional<z.ZodEnum<{
14
14
  exa: "exa";
15
15
  duckduckgo: "duckduckgo";
@@ -28,8 +28,8 @@ export declare const createWebSearchTool: () => Promise<{
28
28
  description: string;
29
29
  inputSchema: z.ZodObject<{
30
30
  query: z.ZodString;
31
- numResults: z.ZodOptional<z.ZodPipe<z.ZodTransform<string | null, unknown>, z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
32
- timeout: z.ZodOptional<z.ZodPipe<z.ZodTransform<string | null, unknown>, z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
31
+ numResults: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
32
+ timeout: z.ZodOptional<z.ZodPreprocess<z.ZodNullable<z.ZodCoercedNumber<unknown>>>>;
33
33
  provider: z.ZodOptional<z.ZodEnum<{
34
34
  exa: "exa";
35
35
  duckduckgo: "duckduckgo";
@@ -217,7 +217,7 @@ export const createWebSearchTool = async () => {
217
217
  inputSchema,
218
218
  },
219
219
  display({ query }) {
220
- return `🔍 ${style.cyan(query)}`;
220
+ return `${style.cyan(query)}`;
221
221
  },
222
222
  async execute(options, executionOptions) {
223
223
  return executeWebSearch(options, executionOptions);
@@ -8,7 +8,6 @@ type State = {
8
8
  currentContextWindow: number;
9
9
  contextWindow: number;
10
10
  agentState?: AgentState;
11
- currentMode?: string;
12
11
  };
13
12
  export declare class FooterComponent implements Component {
14
13
  private modelManager;
@@ -16,7 +15,6 @@ export declare class FooterComponent implements Component {
16
15
  private state;
17
16
  private progressBar;
18
17
  private agentState?;
19
- private currentMode;
20
18
  constructor(modelManager: ModelManager, tokenTracker: TokenTracker | undefined, state: State);
21
19
  setState(state: State): void;
22
20
  resetState(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../source/tui/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,WAAW,CAAC;AAGzD,KAAK,KAAK,GAAG;IACX,aAAa,EAAE,iBAAiB,CAAC;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AA2CF,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,WAAW,CAAY;gBAE7B,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,KAAK,EAAE,KAAK;IAad,QAAQ,CAAC,KAAK,EAAE,KAAK;IAYrB,UAAU;IAIV,gBAAgB,IAAI,iBAAiB;IAIrC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CAiFhC"}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../source/tui/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,WAAW,CAAC;AAGzD,KAAK,KAAK,GAAG;IACX,aAAa,EAAE,iBAAiB,CAAC;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,CAAC;AA2CF,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,UAAU,CAAC,CAAa;gBAE9B,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,KAAK,EAAE,KAAK;IAad,QAAQ,CAAC,KAAK,EAAE,KAAK;IASrB,UAAU;IAIV,gBAAgB,IAAI,iBAAiB;IAIrC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CAkEhC"}
@@ -41,7 +41,6 @@ export class FooterComponent {
41
41
  state;
42
42
  progressBar;
43
43
  agentState;
44
- currentMode = "Normal";
45
44
  constructor(modelManager, tokenTracker, state) {
46
45
  this.modelManager = modelManager;
47
46
  this.tokenTracker = tokenTracker;
@@ -53,9 +52,6 @@ export class FooterComponent {
53
52
  if (state.agentState) {
54
53
  this.agentState = state.agentState;
55
54
  }
56
- if (state.currentMode !== undefined) {
57
- this.currentMode = state.currentMode;
58
- }
59
55
  this.state = state;
60
56
  this.progressBar.setCurrent(state.currentContextWindow);
61
57
  this.progressBar.setTotal(state.contextWindow);
@@ -72,20 +68,8 @@ export class FooterComponent {
72
68
  const [pathLine, gitLine] = formatProjectStatus(this.state.projectStatus);
73
69
  const padding = Math.max(0, width - visibleWidth(pathLine) - modelInfo.length);
74
70
  results.push(pathLine + " ".repeat(padding) + style.dim(modelInfo));
75
- const modeDisplay = this.currentMode !== "Normal"
76
- ? style.magenta(`[${this.currentMode}]`)
77
- : "";
78
71
  if (gitLine) {
79
- if (modeDisplay) {
80
- const gitPadding = Math.max(0, width - visibleWidth(gitLine) - visibleWidth(modeDisplay));
81
- results.push(`${gitLine}${" ".repeat(gitPadding)}${modeDisplay}`);
82
- }
83
- else {
84
- results.push(gitLine);
85
- }
86
- }
87
- else if (modeDisplay) {
88
- results.push(modeDisplay);
72
+ results.push(gitLine);
89
73
  }
90
74
  // Line 3: Total session usage from token tracker (accumulated across all turns)
91
75
  if (this.tokenTracker) {
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"command-protection.d.ts","sourceRoot":"","sources":["../../source/utils/command-protection.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,KAAK,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;AAE3E;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CA4B7E;AA8UD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,oBAAoB,GAC3B,MAAM,CAQR"}
1
+ {"version":3,"file":"command-protection.d.ts","sourceRoot":"","sources":["../../source/utils/command-protection.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,KAAK,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;AA0E3E;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CA4B7E;AA8WD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,oBAAoB,GAC3B,MAAM,CAQR"}
@@ -2,6 +2,66 @@
2
2
  * Command Protection Module
3
3
  * Detects and blocks destructive commands that could cause data loss
4
4
  */
5
+ /**
6
+ * Check if a git subcommand appears as an actual command (not inside quoted strings).
7
+ * This helps avoid false positives when destructive command text appears in
8
+ * commit messages or other quoted strings.
9
+ */
10
+ function isActualGitCommand(command, subcommand) {
11
+ const pattern = new RegExp(`\\bgit\\s+${subcommand}\\b`, "gi");
12
+ // Find all matches of git <subcommand>
13
+ const matches = [];
14
+ let match = pattern.exec(command);
15
+ while (match !== null) {
16
+ matches.push({ index: match.index, text: match[0] });
17
+ match = pattern.exec(command);
18
+ }
19
+ if (matches.length === 0)
20
+ return false;
21
+ // For each match, check if it's inside quotes
22
+ for (const { index } of matches) {
23
+ if (!isInsideQuotes(command, index)) {
24
+ // Also check if it's at the start or after command separators
25
+ const beforeMatch = command.slice(0, index).trim();
26
+ if (beforeMatch === "" ||
27
+ beforeMatch.endsWith("&&") ||
28
+ beforeMatch.endsWith("||") ||
29
+ beforeMatch.endsWith(";") ||
30
+ beforeMatch.endsWith("|") ||
31
+ beforeMatch.endsWith("\n")) {
32
+ return true;
33
+ }
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+ /**
39
+ * Check if a position in a string is inside quotes (single or double).
40
+ * Handles escaped quotes.
41
+ */
42
+ function isInsideQuotes(command, position) {
43
+ let inSingleQuote = false;
44
+ let inDoubleQuote = false;
45
+ let escaped = false;
46
+ for (let i = 0; i < position; i++) {
47
+ const char = command[i];
48
+ if (escaped) {
49
+ escaped = false;
50
+ continue;
51
+ }
52
+ if (char === "\\") {
53
+ escaped = true;
54
+ continue;
55
+ }
56
+ if (char === '"' && !inSingleQuote) {
57
+ inDoubleQuote = !inDoubleQuote;
58
+ }
59
+ else if (char === "'" && !inDoubleQuote) {
60
+ inSingleQuote = !inSingleQuote;
61
+ }
62
+ }
63
+ return inSingleQuote || inDoubleQuote;
64
+ }
5
65
  /**
6
66
  * Detects if a command is destructive and should be blocked
7
67
  * @param command - The full command string to check
@@ -91,15 +151,19 @@ function detectDestructiveGitCommands(command) {
91
151
  };
92
152
  }
93
153
  // Block git branch -D (force delete without merge check)
94
- if (lowerCommand.match(/git\s+branch\s+-[a-z]/)) {
95
- // Check if it's an uppercase letter (force delete)
96
- const match = lowerCommand.match(/git\s+branch\s+-[a-z]/);
97
- if (match && match[0].slice(-1) !== match[0].slice(-1).toLowerCase()) {
98
- // It's uppercase, block it
99
- }
100
- else if (match) {
101
- const upperMatch = command.match(/git\s+branch\s+-[A-Z]/);
102
- if (upperMatch) {
154
+ // Check for uppercase flag on original command (lowercase -d is safe)
155
+ // Match 'git' case-insensitively, but preserve case for the flag
156
+ // Only match if git branch appears as an actual command (not inside quoted strings)
157
+ const branchMatch = lowerCommand.match(/git\s+branch\s+-([a-z])/);
158
+ if (branchMatch) {
159
+ // Check if the flag in the original command is uppercase
160
+ const flagInOriginal = command.match(/git\s+branch\s+-([A-Za-z])/i);
161
+ if (flagInOriginal &&
162
+ flagInOriginal[1] === flagInOriginal[1].toUpperCase()) {
163
+ // Verify this is an actual git branch command, not text inside quotes
164
+ // Check if git branch appears at start or after command separators
165
+ const isActualCommand = isActualGitCommand(command, "branch");
166
+ if (isActualCommand) {
103
167
  return {
104
168
  blocked: true,
105
169
  reason: "git branch -D force-deletes branches without checking if they're merged",
@@ -216,8 +280,27 @@ function detectDangerousInlineScripts(command) {
216
280
  }
217
281
  /**
218
282
  * Detect dangerous patterns in heredocs and here-strings
283
+ * Only blocks heredocs that are explicitly executed by a scripting language,
284
+ * not heredocs used as data (e.g., commit messages, config files).
219
285
  */
220
286
  function detectDangerousHeredocs(command) {
287
+ // Check if heredoc is being executed by a scripting language
288
+ // Patterns that indicate execution:
289
+ // 1. bash <<EOF, sh <<EOF, python <<EOF, etc. (language reads from heredoc)
290
+ // 2. cat <<EOF | bash, <<EOF | python, etc. (heredoc piped to language)
291
+ const executionPatterns = [
292
+ // Shell languages reading heredoc directly
293
+ /\b(bash|sh|zsh|dash|ksh)\s*<<-?\s*['"]?\w+/i,
294
+ // Scripting languages reading heredoc directly
295
+ /\b(python\d?|ruby|perl|node)\s*<<-?\s*['"]?\w+/i,
296
+ // Heredoc piped to shell
297
+ /<<-?\s*['"]?\w+['"]?\s*\|\s*(bash|sh|zsh|dash|ksh)\b/i,
298
+ ];
299
+ const isExecutableHeredoc = executionPatterns.some((pattern) => pattern.test(command));
300
+ // Only scan heredoc content if it's being executed by a scripting language
301
+ if (!isExecutableHeredoc) {
302
+ return { blocked: false };
303
+ }
221
304
  // Match heredoc patterns: <<EOF ... EOF
222
305
  const heredocPattern = /<<-?\s*['"]?(\w+)['"]?\s*([\s\S]*?)\n\1\b/gi;
223
306
  let match = null;
@@ -1,3 +1,3 @@
1
1
  import { z } from "zod";
2
- export declare function jsonParser<T extends z.ZodTypeAny>(input: T): z.ZodPipe<z.ZodTransform<any, unknown>, T>;
2
+ export declare function jsonParser<T extends z.ZodTypeAny>(input: T): z.ZodPreprocess<T>;
3
3
  //# sourceMappingURL=parsing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parsing.d.ts","sourceRoot":"","sources":["../../source/utils/parsing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBtC,wBAAgB,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,8CAE1D"}
1
+ {"version":3,"file":"parsing.d.ts","sourceRoot":"","sources":["../../source/utils/parsing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBtC,wBAAgB,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,sBAE1D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travisennis/acai",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "An AI assistant for developing software.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "lint:ast-grep:fix": "sg scan --update-all",
34
34
  "lint:biome": "biome lint",
35
35
  "lint:biome:fix": "biome lint --unsafe --write",
36
- "lint:fix": "(npm run lint:ast-grep || true) && npm run lint:biome:fix",
36
+ "lint:fix": "npm run lint:ast-grep && npm run lint:biome:fix",
37
37
  "lint:staged": "biome lint --error-on-warnings --no-errors-on-unmatched --staged",
38
38
  "format:staged": "biome check --staged --formatter-enabled=true --linter-enabled=false --no-errors-on-unmatched",
39
39
  "prepack": "npm run clean && npm run build",
@@ -49,6 +49,7 @@
49
49
  "oxlint:single": "oxlint --type-aware --ignore-path .gitignore -A all -c .oxlintrc.json",
50
50
  "knip": "npx knip",
51
51
  "knip:prod": "npx knip --production",
52
+ "setup": "node scripts/setup.ts",
52
53
  "update": "npx npm-check-updates --interactive --format group",
53
54
  "cpd": "npx jscpd ./source",
54
55
  "typecheck": "tsc --noEmit --pretty -p tsconfig.json",
@@ -57,45 +58,46 @@
57
58
  "lint:length": "find source -name '*.ts' | xargs -I{} awk 'END{if(NR>500)print FILENAME\": \"NR\" lines\"}' {}"
58
59
  },
59
60
  "dependencies": {
60
- "@ai-sdk/anthropic": "^3.0.63",
61
- "@ai-sdk/deepseek": "^2.0.26",
62
- "@ai-sdk/devtools": "^0.0.15",
63
- "@ai-sdk/google": "^3.0.52",
64
- "@ai-sdk/groq": "^3.0.31",
65
- "@ai-sdk/open-responses": "^1.0.8",
66
- "@ai-sdk/openai": "^3.0.47",
67
- "@ai-sdk/openai-compatible": "^2.0.37",
61
+ "@ai-sdk/alibaba": "^1.0.21",
62
+ "@ai-sdk/anthropic": "^3.0.74",
63
+ "@ai-sdk/deepseek": "^2.0.32",
64
+ "@ai-sdk/devtools": "^0.0.17",
65
+ "@ai-sdk/google": "^3.0.67",
66
+ "@ai-sdk/groq": "^3.0.38",
67
+ "@ai-sdk/open-responses": "^1.0.14",
68
+ "@ai-sdk/openai": "^3.0.58",
69
+ "@ai-sdk/openai-compatible": "^2.0.45",
68
70
  "@crosscopy/clipboard": "^0.3.6",
69
71
  "@travisennis/stdlib": "^0.0.14",
70
- "ai": "^6.0.134",
72
+ "ai": "^6.0.174",
71
73
  "cheerio": "^1.2.0",
72
- "diff": "^8.0.3",
74
+ "diff": "^9.0.0",
73
75
  "fast-glob": "^3.3.3",
74
76
  "fdir": "^6.5.0",
75
77
  "highlight.js": "^11.11.1",
76
- "marked": "17.0.5",
78
+ "marked": "18.0.3",
77
79
  "p-throttle": "^8.1.0",
78
- "parse5": "^8.0.0",
79
- "parse5-htmlparser2-tree-adapter": "^8.0.0",
80
+ "parse5": "^8.0.1",
81
+ "parse5-htmlparser2-tree-adapter": "^8.0.1",
80
82
  "pino": "^10.3.1",
81
83
  "pino-pretty": "^13.1.3",
82
84
  "pino-roll": "^4.0.0",
83
85
  "tiktoken": "^1.0.22",
84
- "zod": "^4.3.6"
86
+ "zod": "^4.4.2"
85
87
  },
86
88
  "devDependencies": {
87
- "@ai-sdk/provider": "^3.0.8",
88
- "@ast-grep/napi": "^0.42.0",
89
- "@biomejs/biome": "2.4.8",
90
- "@commitlint/config-conventional": "^20.5.0",
91
- "@types/node": "^25.5.0",
89
+ "@ai-sdk/provider": "^3.0.10",
90
+ "@ast-grep/napi": "^0.42.1",
91
+ "@biomejs/biome": "2.4.14",
92
+ "@commitlint/config-conventional": "^20.5.3",
93
+ "@types/node": "^25.6.0",
92
94
  "c8": "^11.0.0",
93
- "commitlint": "^20.5.0",
95
+ "commitlint": "^20.5.3",
94
96
  "domhandler": "^6.0.1",
95
97
  "husky": "^9.1.7",
96
- "oxlint": "^1.56.0",
97
- "oxlint-tsgolint": "^0.17.1",
98
- "typescript": "^5.9.3"
98
+ "oxlint": "^1.62.0",
99
+ "oxlint-tsgolint": "^0.22.1",
100
+ "typescript": "^6.0.3"
99
101
  },
100
102
  "engines": {
101
103
  "node": ">=20"
@@ -1,24 +0,0 @@
1
- import type { UserModelMessage } from "ai";
2
- type Mode = "normal" | "planning" | "research";
3
- export declare class ModeManager {
4
- private currentMode;
5
- private firstMessageInMode;
6
- getCurrentMode(): Mode;
7
- getDisplayName(): string;
8
- cycleMode(): void;
9
- getInitialPrompt(): string;
10
- getReminderPrompt(): string;
11
- isNormal(): boolean;
12
- isFirstMessage(): boolean;
13
- markFirstMessageSent(): void;
14
- getReminderMessage(): UserModelMessage | undefined;
15
- reset(): void;
16
- toJson(): {
17
- mode: Mode;
18
- };
19
- fromJson(data: {
20
- mode?: string;
21
- }): void;
22
- }
23
- export {};
24
- //# sourceMappingURL=manager.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../source/modes/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAI3C,KAAK,IAAI,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAmC/C,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,kBAAkB,CAAQ;IAElC,cAAc,IAAI,IAAI;IAItB,cAAc,IAAI,MAAM;IAIxB,SAAS,IAAI,IAAI;IAOjB,gBAAgB,IAAI,MAAM;IAI1B,iBAAiB,IAAI,MAAM;IAI3B,QAAQ,IAAI,OAAO;IAInB,cAAc,IAAI,OAAO;IAIzB,oBAAoB,IAAI,IAAI;IAI5B,kBAAkB,IAAI,gBAAgB,GAAG,SAAS;IAWlD,KAAK,IAAI,IAAI;IAKb,MAAM,IAAI;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE;IAIxB,QAAQ,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAMxC"}