@travisennis/acai 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mentions.js CHANGED
@@ -78,7 +78,7 @@ async function processFileCommand(context) {
78
78
  async function processShellCommand(command) {
79
79
  try {
80
80
  const { stdout, stderr, code } = await executeCommand(command, {
81
- shell: true,
81
+ shell: false,
82
82
  });
83
83
  if (code === 0) {
84
84
  return stdout;
package/dist/prompts.js CHANGED
@@ -85,10 +85,12 @@ function toolUsage() {
85
85
  - Outline multi-step tasks before execution
86
86
 
87
87
  ### Bash Commands (\`${BashTool.name}\`)
88
- - Execute bash commands within project directory only
89
- - Always specify absolute paths to avoid errors
90
- - You have access to the Github CLI
91
- - Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
88
+ - Shell is disabled. Do NOT use: |, >, >>, <, <<, ;, &&, ||, &, \`...\`, $().
89
+ - Run a single allowed command with quoted args; no command substitution or chaining.
90
+ - Compose multi-step flows via multiple tool calls or program flags (rg, jq, grep options, etc.).
91
+ - For large gh/git messages, prefer --message-file instead of inlining big strings.
92
+ - Commands execute only within the project directory; always use absolute paths.
93
+ - Avoid interactive commands; prefer non-interactive flags (e.g., npm init -y).
92
94
 
93
95
  ### Code Interpreter (\`${CodeInterpreterTool.name}\`)
94
96
  - Executes JavaScript code in a separate Node.js process using Node's Permission Model
@@ -116,7 +116,7 @@ export const createBashTool = ({ baseDir, sendData, tokenCounter, terminal, auto
116
116
  let autoAcceptCommands = autoAcceptAll;
117
117
  return {
118
118
  [BashTool.name]: tool({
119
- description: `Execute bash commands and return their output. Limited to a whitelist of safe commands: ${ALLOWED_COMMANDS.join(", ")}. Commands will only execute within the project directory for security. Always specify absolute paths to avoid errors.`,
119
+ description: `Execute commands without a shell. Restrictions: no pipes (|), redirects (>, >>, <, <<), chaining (&&, ||, ;), background (&), or command substitution (\`...\`, $()). Pass a single allowed base command with quoted args. Compose by running multiple tool calls or using program flags (e.g., rg, jq). Commands execute only within the project directory. Always use absolute paths. Allowed commands: ${ALLOWED_COMMANDS.join(", ")}.`,
120
120
  inputSchema: z.object({
121
121
  command: z.string().describe("Full CLI command to execute."),
122
122
  cwd: z
@@ -214,7 +214,7 @@ export const createBashTool = ({ baseDir, sendData, tokenCounter, terminal, auto
214
214
  const result = await executeCommand(command, {
215
215
  cwd: safeCwd,
216
216
  timeout: safeTimeout,
217
- shell: true,
217
+ shell: false,
218
218
  throwOnError: false,
219
219
  });
220
220
  if (result.signal === "SIGTERM") {
@@ -7,6 +7,7 @@ export declare const createCodeInterpreterTool: ({ sendData, }: Readonly<{
7
7
  }>) => {
8
8
  codeInterpreter: import("ai").Tool<{
9
9
  code: string;
10
+ type: "Typescript" | "JavaScript" | null;
10
11
  timeoutSeconds: number | null;
11
12
  }, string>;
12
13
  };
@@ -8,7 +8,12 @@ import { z } from "zod";
8
8
  export const CodeInterpreterTool = {
9
9
  name: "codeInterpreter",
10
10
  };
11
- const toolDescription = `Executes JavaScript code in a separate Node.js process using Node's Permission Model. By default, the child process has no permissions except read/write within the current working directory. The tool returns stdout, stderr, and exitCode. Use console.log/console.error to produce output.
11
+ const toolDescription = `Executes JavaScript or Typescript code in a separate Node.js process using Node's Permission Model.
12
+
13
+ ⚠️ **IMPORTANT TYPE SELECTION**:
14
+ - Use "JavaScript" for plain JavaScript code (no TypeScript syntax)
15
+ - Use "Typescript" for code containing interfaces, type annotations, generics, etc.
16
+ - Code type is automatically validated before execution
12
17
 
13
18
  ⚠️ **IMPORTANT**: This tool uses ES Modules (ESM) only.
14
19
  - Use \`import\` statements, NOT \`require()\`
@@ -18,12 +23,85 @@ const toolDescription = `Executes JavaScript code in a separate Node.js process
18
23
  These scripts are run in the \`${process.cwd}/.acai-ci-tmp\`. Keep this in mind if you intend to import or reference files from this project in your script.
19
24
 
20
25
  Timeout defaults to 5 seconds and can be extended up to 60 seconds.`;
26
+ /**
27
+ * Detects if code contains TypeScript syntax patterns
28
+ */
29
+ function containsTypeScriptSyntax(code) {
30
+ // Common TypeScript patterns that don't exist in JavaScript
31
+ const tsPatterns = [
32
+ // Type annotations
33
+ /:\s*[a-zA-Z]\w*(?:\s*\[])?\s*(?=[,;=)])/g, // Type annotations after variables/parameters (including any, string[], etc.)
34
+ /:\s*\{[^}]*\}\s*(?=[,;=)])/g, // Object type annotations
35
+ /:\s*\([^)]*\)\s*=>/g, // Function type annotations
36
+ // Type declarations
37
+ /^\s*interface\s+\w+/gm, // Interface declarations
38
+ /^\s*type\s+\w+\s*=/gm, // Type aliases
39
+ /^\s*enum\s+\w+/gm, // Enum declarations
40
+ // Generic types
41
+ /<\s*[A-Z]\w*(?:\s*,\s*[A-Z]\w*)*\s*>/g, // Generic type parameters
42
+ /\w+\s*<\s*[^<>]+?\s*>/g, // Generic type usage
43
+ // TypeScript-specific keywords (in specific contexts)
44
+ /\b(?:implements|extends\s+[A-Z]\w*|readonly|private|protected|public)\b/g,
45
+ // Utility types
46
+ /\b(?:Partial|Required|Pick|Omit|Record|Exclude|Extract)\b/g,
47
+ ];
48
+ return tsPatterns.some((pattern) => pattern.test(code));
49
+ }
50
+ /**
51
+ * Validates that code content matches the specified type
52
+ */
53
+ function validateCodeTypeMatch(code, specifiedType) {
54
+ if (!code?.trim()) {
55
+ return "No code provided";
56
+ }
57
+ const detectedType = containsTypeScriptSyntax(code)
58
+ ? "Typescript"
59
+ : "JavaScript";
60
+ const expectedType = specifiedType ?? "JavaScript";
61
+ // If TypeScript syntax detected but JavaScript specified
62
+ if (detectedType === "Typescript" && expectedType === "JavaScript") {
63
+ return `Code contains TypeScript syntax but is specified as JavaScript. Please either:
64
+ 1. Change type to "Typescript", or
65
+ 2. Remove TypeScript syntax (interfaces, type annotations, generics, etc.)
66
+
67
+ Detected TypeScript patterns: ${getTypeScriptPatternsFound(code).join(", ")}`;
68
+ }
69
+ // If no TypeScript syntax but TypeScript specified (warning, not error)
70
+ if (detectedType === "JavaScript" && expectedType === "Typescript") {
71
+ // This is not an error, just potentially unnecessary
72
+ return null;
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Identifies specific TypeScript patterns found in code
78
+ */
79
+ function getTypeScriptPatternsFound(code) {
80
+ const patterns = [];
81
+ if (/\binterface\s+\w+/.test(code))
82
+ patterns.push("interface");
83
+ if (/\btype\s+\w+\s*=/.test(code))
84
+ patterns.push("type alias");
85
+ if (/:\s*[a-zA-Z]\w*(?:\s*\[])?\s*(?=[,;=)])/g.test(code))
86
+ patterns.push("type annotations");
87
+ if (/<\s*[A-Z]\w*\s*>/.test(code))
88
+ patterns.push("generics");
89
+ if (/\benum\s+\w+/.test(code))
90
+ patterns.push("enum");
91
+ return patterns;
92
+ }
21
93
  export const createCodeInterpreterTool = ({ sendData, }) => {
22
94
  return {
23
95
  [CodeInterpreterTool.name]: tool({
24
96
  description: toolDescription,
25
97
  inputSchema: z.object({
26
- code: z.string().describe("JavaScript code to be executed."),
98
+ code: z
99
+ .string()
100
+ .describe("The JavaScript or Typescript code to be executed."),
101
+ type: z
102
+ .enum(["JavaScript", "Typescript"])
103
+ .nullable()
104
+ .describe("The type of code. Either Javascript or Typescript."),
27
105
  timeoutSeconds: z
28
106
  .number()
29
107
  .int()
@@ -32,21 +110,32 @@ export const createCodeInterpreterTool = ({ sendData, }) => {
32
110
  .nullable()
33
111
  .describe("Execution timeout in seconds (1-60). Default 5."),
34
112
  }),
35
- execute: async ({ code, timeoutSeconds }, { toolCallId }) => {
113
+ execute: async ({ code, type, timeoutSeconds }, { toolCallId }) => {
36
114
  const workingDirectory = process.cwd();
37
115
  try {
116
+ // Pre-execution validation
117
+ const validationError = validateCodeTypeMatch(code, type);
118
+ if (validationError) {
119
+ sendData?.({
120
+ event: "tool-error",
121
+ id: toolCallId,
122
+ data: validationError,
123
+ });
124
+ return validationError;
125
+ }
38
126
  sendData?.({
39
127
  event: "tool-init",
40
128
  id: toolCallId,
41
129
  data: "Initializing code interpreter environment",
42
130
  });
131
+ const scriptType = (type ?? "JavaScript").toLowerCase();
43
132
  sendData?.({
44
133
  event: "tool-update",
45
134
  id: toolCallId,
46
135
  data: {
47
136
  primary: "Executing...",
48
137
  secondary: [
49
- `${"`".repeat(3)} javascript\n${code.slice(0, 500)}${"`".repeat(3)}`,
138
+ `${"`".repeat(3)} ${scriptType}}\n${code.slice(0, 500)}${"`".repeat(3)}`,
50
139
  ],
51
140
  },
52
141
  });
@@ -56,7 +145,8 @@ export const createCodeInterpreterTool = ({ sendData, }) => {
56
145
  const timeoutMs = Math.min(Math.max((timeoutSeconds ?? 5) * 1000, 1000), 60000);
57
146
  const tmpBase = join(workingDirectory, ".acai-ci-tmp");
58
147
  await mkdir(tmpBase, { recursive: true });
59
- const scriptPath = join(tmpBase, `temp_script_${Date.now()}_${randomUUID()}.mjs`);
148
+ const ext = type === "JavaScript" ? ".mjs" : ".ts";
149
+ const scriptPath = join(tmpBase, `temp_script_${Date.now()}_${randomUUID()}${ext}`);
60
150
  await writeFile(scriptPath, code, { encoding: "utf8" });
61
151
  const args = [
62
152
  "--permission",
@@ -8,5 +8,4 @@ export declare class CommandValidation {
8
8
  isValid: boolean;
9
9
  error?: string;
10
10
  };
11
- private splitOnPipes;
12
11
  }
@@ -3,12 +3,14 @@ export class CommandValidation {
3
3
  dangerousPatterns;
4
4
  constructor(allowedCommands) {
5
5
  this.allowedCommands = allowedCommands;
6
- // Only block truly dangerous patterns, not useful shell operations
6
+ // Block shell operators and substitutions outright
7
7
  this.dangerousPatterns = [
8
8
  /`/, // backticks (command substitution)
9
9
  /\$\(/, // $() command substitution
10
- /&&\s*rm\s+-rf/, // dangerous rm chains
11
- /;\s*rm\s+-rf/, // dangerous rm chains
10
+ /\|/, // pipes
11
+ />|>>|<|<</, // redirects
12
+ /;|&&|\|\||&/, // chaining and backgrounding
13
+ /[\r\n]/, // newlines
12
14
  ];
13
15
  }
14
16
  isCommandAllowed(command) {
@@ -16,98 +18,28 @@ export class CommandValidation {
16
18
  return this.allowedCommands.includes(baseCommand);
17
19
  }
18
20
  hasDangerousPatterns(command) {
19
- // Remove all quoted segments first
20
- const stripped = command
21
- .replace(/'([^'\\]|\\.)*'/g, "")
22
- .replace(/"([^"\\]|\\.)*"/g, "");
23
- // Check for dangerous patterns only in unquoted portions
24
- return this.dangerousPatterns.some((re) => re.test(stripped));
21
+ // Do not strip quotes; reject if any dangerous pattern appears anywhere
22
+ return this.dangerousPatterns.some((re) => re.test(command));
25
23
  }
26
24
  isValid(command) {
27
25
  if (!command.trim()) {
28
26
  return { isValid: false, error: "Command cannot be empty" };
29
27
  }
30
- // First check for dangerous patterns
31
28
  if (this.hasDangerousPatterns(command)) {
32
29
  return {
33
30
  isValid: false,
34
- error: "Command contains dangerous patterns (command substitution or unsafe rm chains)",
31
+ error: "Pipes, redirects, command substitution, chaining, and newlines are disabled for security.",
35
32
  };
36
33
  }
37
- // Process command while preserving quoted strings to extract sub-commands
38
- const subCommands = [];
39
- let currentSegment = "";
40
- let inSingleQuote = false;
41
- let inDoubleQuote = false;
42
- for (let i = 0; i < command.length; i++) {
43
- const char = command[i];
44
- // Handle quote states
45
- if (char === "'" && !inDoubleQuote)
46
- inSingleQuote = !inSingleQuote;
47
- if (char === '"' && !inSingleQuote)
48
- inDoubleQuote = !inDoubleQuote;
49
- // Split on command separators only when not in quotes
50
- // Note: We allow pipes (|) and redirects (>, <) but split on command separators
51
- if (!inSingleQuote && !inDoubleQuote && (char === "&" || char === ";")) {
52
- if (currentSegment.trim()) {
53
- subCommands.push(currentSegment.trim());
54
- currentSegment = "";
55
- }
56
- // Skip the operator and any subsequent same operators (like &&)
57
- while (i + 1 < command.length &&
58
- ["&", ";"].includes(command[i + 1] ?? "")) {
59
- i++;
60
- }
61
- }
62
- else {
63
- currentSegment += char;
64
- }
65
- }
66
- // Add the last segment
67
- if (currentSegment.trim()) {
68
- subCommands.push(currentSegment.trim());
69
- }
70
- // Validate all sub-commands (but be smart about pipes)
71
- for (const subCmd of subCommands) {
72
- // For piped commands, validate each part of the pipe
73
- const pipeParts = this.splitOnPipes(subCmd);
74
- for (const part of pipeParts) {
75
- const trimmedPart = part.trim();
76
- if (trimmedPart && !this.isCommandAllowed(trimmedPart)) {
77
- const baseCmd = trimmedPart.split(" ")[0] || "";
78
- return {
79
- isValid: false,
80
- error: `Command '${baseCmd}' is not allowed. Allowed commands: ${this.allowedCommands.join(", ")}`,
81
- };
82
- }
83
- }
34
+ // No pipes to split now; validate the single command only
35
+ const trimmed = command.trim();
36
+ if (!this.isCommandAllowed(trimmed)) {
37
+ const baseCmd = trimmed.split(" ")[0] || "";
38
+ return {
39
+ isValid: false,
40
+ error: `Command '${baseCmd}' is not allowed. Allowed commands: ${this.allowedCommands.join(", ")}`,
41
+ };
84
42
  }
85
43
  return { isValid: true };
86
44
  }
87
- splitOnPipes(command) {
88
- const parts = [];
89
- let current = "";
90
- let inSingleQuote = false;
91
- let inDoubleQuote = false;
92
- for (let i = 0; i < command.length; i++) {
93
- const char = command[i];
94
- if (char === "'" && !inDoubleQuote)
95
- inSingleQuote = !inSingleQuote;
96
- if (char === '"' && !inSingleQuote)
97
- inDoubleQuote = !inDoubleQuote;
98
- if (char === "|" && !inSingleQuote && !inDoubleQuote) {
99
- if (current.trim()) {
100
- parts.push(current.trim());
101
- current = "";
102
- }
103
- }
104
- else {
105
- current += char;
106
- }
107
- }
108
- if (current.trim()) {
109
- parts.push(current.trim());
110
- }
111
- return parts;
112
- }
113
45
  }
@@ -35,6 +35,7 @@ export declare function initTools({ terminal, tokenCounter, events, autoAcceptAl
35
35
  }, string>;
36
36
  readonly codeInterpreter: import("ai").Tool<{
37
37
  code: string;
38
+ type: "Typescript" | "JavaScript" | null;
38
39
  timeoutSeconds: number | null;
39
40
  }, string>;
40
41
  readonly deleteFile: import("ai").Tool<{
@@ -98,6 +99,7 @@ export declare function initCliTools({ tokenCounter, }: {
98
99
  }, string>;
99
100
  readonly codeInterpreter: import("ai").Tool<{
100
101
  code: string;
102
+ type: "Typescript" | "JavaScript" | null;
101
103
  timeoutSeconds: number | null;
102
104
  }, string>;
103
105
  readonly deleteFile: import("ai").Tool<{
@@ -24,6 +24,14 @@ export interface ExecuteResult {
24
24
  /** The signal that terminated the process, if any */
25
25
  signal?: NodeJS.Signals;
26
26
  }
27
+ export type ParseResult = {
28
+ ok: true;
29
+ argv: [string, ...string[]];
30
+ } | {
31
+ ok: false;
32
+ error: string;
33
+ };
34
+ export declare function parseArgv(input: string): ParseResult;
27
35
  /**
28
36
  * Executes a command and returns the result, providing unified error handling
29
37
  *
@@ -3,6 +3,81 @@ import { isUndefined } from "@travisennis/stdlib/typeguards";
3
3
  const MS_IN_SECOND = 1000;
4
4
  const SECONDS_IN_MINUTE = 60;
5
5
  const DEFAULT_TIMEOUT = 2 * SECONDS_IN_MINUTE * MS_IN_SECOND;
6
+ // Quote/escape-aware argv tokenizer that forbids command substitution
7
+ export function parseArgv(input) {
8
+ const argv = [];
9
+ let buf = "";
10
+ let i = 0;
11
+ const n = input.length;
12
+ let inSingle = false;
13
+ let inDouble = false;
14
+ while (i < n) {
15
+ const ch = input[i] ?? "";
16
+ // Reject shell-only constructs early
17
+ if (ch === "`")
18
+ return { ok: false, error: "Backticks are not allowed" };
19
+ if (ch === "$" && i + 1 < n && input[i + 1] === "(") {
20
+ return { ok: false, error: "Command substitution $() is not allowed" };
21
+ }
22
+ if (!inSingle && !inDouble && /\s/.test(ch)) {
23
+ if (buf.length > 0) {
24
+ argv.push(buf);
25
+ buf = "";
26
+ }
27
+ i += 1;
28
+ continue;
29
+ }
30
+ if (!inDouble && ch === "'" && !inSingle) {
31
+ inSingle = true;
32
+ i += 1;
33
+ continue;
34
+ }
35
+ if (inSingle && ch === "'") {
36
+ inSingle = false;
37
+ i += 1;
38
+ continue;
39
+ }
40
+ if (!inSingle && ch === '"' && !inDouble) {
41
+ inDouble = true;
42
+ i += 1;
43
+ continue;
44
+ }
45
+ if (inDouble && ch === '"') {
46
+ inDouble = false;
47
+ i += 1;
48
+ continue;
49
+ }
50
+ if (!inSingle && ch === "\\") {
51
+ i += 1;
52
+ if (i >= n)
53
+ return { ok: false, error: "Dangling escape" };
54
+ const next = input[i] ?? "";
55
+ // Inside double quotes, only escape " and \\ reliably
56
+ if (inDouble && next !== '"' && next !== "\\") {
57
+ // Keep backslash literally for safety
58
+ buf += `\\${next}`;
59
+ }
60
+ else {
61
+ buf += next;
62
+ }
63
+ i += 1;
64
+ continue;
65
+ }
66
+ buf += ch;
67
+ i += 1;
68
+ }
69
+ if (inSingle || inDouble)
70
+ return { ok: false, error: "Unterminated quote" };
71
+ if (buf.length > 0)
72
+ argv.push(buf);
73
+ if (argv.length === 0)
74
+ return { ok: false, error: "Empty command" };
75
+ const first = argv[0];
76
+ if (typeof first !== "string" || first.trim() === "") {
77
+ return { ok: false, error: "Missing command" };
78
+ }
79
+ return { ok: true, argv: argv };
80
+ }
6
81
  /**
7
82
  * Executes a command and returns the result, providing unified error handling
8
83
  *
@@ -19,9 +94,18 @@ export function executeCommand(command, options) {
19
94
  [cmd, ...args] = command;
20
95
  }
21
96
  else {
22
- const parts = command.split(" ");
23
- cmd = parts[0] ?? "";
24
- args = parts.slice(1);
97
+ const parsed = parseArgv(command);
98
+ if (!parsed.ok) {
99
+ const result = {
100
+ stdout: "",
101
+ stderr: parsed.error,
102
+ code: 1,
103
+ };
104
+ return throwOnError
105
+ ? Promise.reject(new Error(parsed.error))
106
+ : Promise.resolve(result);
107
+ }
108
+ [cmd, ...args] = parsed.argv;
25
109
  }
26
110
  if (isUndefined(cmd) || cmd.trim() === "") {
27
111
  const result = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travisennis/acai",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "An AI assistant for developing software.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,26 +44,26 @@
44
44
  "typecheck:staged": "tsc --noEmit --pretty -p tsconfig.json"
45
45
  },
46
46
  "dependencies": {
47
- "@ai-sdk/anthropic": "^2.0.6",
48
- "@ai-sdk/deepseek": "^1.0.11",
49
- "@ai-sdk/google": "^2.0.8",
50
- "@ai-sdk/openai": "^2.0.20",
51
- "@ai-sdk/openai-compatible": "^1.0.11",
47
+ "@ai-sdk/anthropic": "^2.0.9",
48
+ "@ai-sdk/deepseek": "^1.0.13",
49
+ "@ai-sdk/google": "^2.0.11",
50
+ "@ai-sdk/openai": "^2.0.23",
51
+ "@ai-sdk/openai-compatible": "^1.0.13",
52
52
  "@crosscopy/clipboard": "^0.2.8",
53
53
  "@inquirer/prompts": "^7.8.4",
54
54
  "@travisennis/stdlib": "^0.0.14",
55
- "ai": "^5.0.23",
55
+ "ai": "^5.0.28",
56
56
  "chalk": "^5.6.0",
57
57
  "cheerio": "^1.1.2",
58
58
  "cli-highlight": "^2.1.11",
59
59
  "cli-table3": "^0.6.5",
60
60
  "diff": "^8.0.2",
61
61
  "duck-duck-scrape": "^2.2.7",
62
- "exa-js": "^1.9.2",
62
+ "exa-js": "^1.9.3",
63
63
  "globby": "^14.1.0",
64
64
  "ignore": "^7.0.5",
65
65
  "log-update": "^6.1.0",
66
- "marked": "16.2.0",
66
+ "marked": "16.2.1",
67
67
  "ora": "^8.2.0",
68
68
  "p-throttle": "^8.0.0",
69
69
  "pino": "^9.9.0",