autoclaw 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,7 +78,7 @@ npm install -g autoclaw
78
78
 
79
79
  ## Quick Start
80
80
 
81
- 1. **Setup**: Run the interactive setup wizard to configure your API keys and integrations.
81
+ 1. **Setup**: Run the interactive setup wizard to configure your API keys and integrations. The wizard runs a live connection test (failures map to the likely wrong field: 401 = key, 404 = base URL, 400 = model name) and can list the provider's models for you to pick from.
82
82
  ```bash
83
83
  autoclaw setup
84
84
  ```
@@ -121,6 +121,15 @@ Manifest lines are `{"id": "...", "task": "..."}` — `id` is optional (defaults
121
121
 
122
122
  One failing task does not stop the batch (use `--fail-fast` for that). The process exits `0` when every task completed, `1` otherwise, so cron and K8s Jobs can detect bad batches. Task output stays human-readable on stdout — the results file is the machine-readable contract, with `status`, `steps`, `message`, `error` and `usage` per task.
123
123
 
124
+ Long batches can stop and pick up where they left off, and can use local parallelism:
125
+ ```bash
126
+ autoclaw batch big.jsonl -y --resume # skip tasks already completed in the results file
127
+ autoclaw batch big.jsonl -y -c 4 # run up to 4 tasks in parallel
128
+ ```
129
+ Unattempted tasks are simply absent from the results file, so `--fail-fast` followed by `--resume` is a natural retry loop.
130
+
131
+ AutoClaw also keeps its own prompt lean: optional tools (web search, email, group notifications, image generation) only register once their credentials are configured, and in long loops older tool results in the model context are replaced by short excerpts.
132
+
124
133
  ### Auto-Confirm (CI/CD)
125
134
  Automatically approve all tool executions (dangerous, use with caution or in sandboxes).
126
135
  ```bash
@@ -158,6 +167,7 @@ AutoClaw uses a hierarchical configuration system.
158
167
  - `model`: Default model to use.
159
168
  - `maxSteps`: Max LLM turns per task before the agent stops (default: `25`).
160
169
  - `shellTimeout`: Shell command timeout in milliseconds (default: `120000`).
170
+ - `shell`: Force a shell for `execute_shell_command` (`bash`, `powershell`, `cmd`, `sh`; default: auto-detect — Git Bash > PowerShell > cmd on Windows).
161
171
  - `tavilyApiKey`: API Key for Tavily Web Search.
162
172
  - `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP Email settings.
163
173
  - `feishuWebhook`, `dingtalkWebhook`, `wecomWebhook`: Notification webhooks.
@@ -177,6 +187,7 @@ Create a file at `.autoclaw/setting.json`:
177
187
  - `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: main LLM settings.
178
188
  - `AUTOCLOW_PROVIDER`: provider preset used when `-P` is not passed.
179
189
  - `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: reliability limits (max LLM turns per task; shell timeout in ms).
190
+ - `AUTOCLOW_SHELL`: force the shell for shell commands (`bash`, `powershell`, `cmd`, `sh`).
180
191
  - `AUTOCLOW_INCLUDE_USAGE`: set to `1`/`true` to request token usage from the API (opt-in).
181
192
  - `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: tool credentials as an alternative to setup.
182
193
 
package/README.zh-CN.md CHANGED
@@ -79,7 +79,7 @@ npm install -g autoclaw
79
79
 
80
80
  ## 快速上手
81
81
 
82
- 1. **配置**: 运行交互式设置向导以配置您的 API 密钥和集成插件。
82
+ 1. **配置**: 运行交互式设置向导以配置您的 API 密钥和集成插件。向导会现场做连接测试(失败映射到最可能出错的字段:401=key 错、404=URL 错、400=模型名错),并能拉取服务商的模型列表供你直接选择。
83
83
  ```bash
84
84
  autoclaw setup
85
85
  ```
@@ -122,6 +122,15 @@ autoclaw batch tasks.jsonl -o out.jsonl --fail-fast
122
122
 
123
123
  单个任务失败不会中断批次(需要中断用 `--fail-fast`)。全部完成时进程退出 `0`,否则 `1`,cron 和 K8s Job 由此感知批次成败。任务输出保持人类可读——结果文件才是机器可读契约,含每个任务的 `status`、`steps`、`message`、`error`、`usage`。
124
124
 
125
+ 长批次可以中途停、从断点继续,也可以本地并行:
126
+ ```bash
127
+ autoclaw batch big.jsonl -y --resume # 跳过结果文件中已完成的任务
128
+ autoclaw batch big.jsonl -y -c 4 # 最多 4 个任务并行
129
+ ```
130
+ 未执行的任务不会出现在结果文件里,所以 `--fail-fast` 之后接 `--resume` 就是天然的重试循环。
131
+
132
+ AutoClaw 同时会自动给提示词瘦身:可选工具(网页搜索、邮件、群通知、图像生成)只在凭据配置后才会注册进工具定义;长循环中较早的工具结果会被替换为短摘要。
133
+
125
134
  ### 自动确认 (CI/CD)
126
135
  自动批准所有工具执行(危险操作,请谨慎使用或在沙箱环境下运行)。
127
136
  ```bash
@@ -159,6 +168,7 @@ AutoClaw 使用层级配置系统。
159
168
  - `model`: 默认使用的模型。
160
169
  - `maxSteps`: 单任务最大 LLM 轮数,超出后自动停止 (默认: `25`)。
161
170
  - `shellTimeout`: Shell 命令超时时间(毫秒)(默认: `120000`)。
171
+ - `shell`: 强制 `execute_shell_command` 使用的 shell (`bash`、`powershell`、`cmd`、`sh`;默认自动检测——Windows 上优先 Git Bash > PowerShell > cmd)。
162
172
  - `tavilyApiKey`: Tavily 网页搜索的 API 密钥。
163
173
  - `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP 邮件设置。
164
174
  - `feishuWebhook`, `dingtalkWebhook`, `wecomWebhook`: 通知钩子地址。
@@ -178,6 +188,7 @@ AutoClaw 使用层级配置系统。
178
188
  - `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: 主模型设置。
179
189
  - `AUTOCLOW_PROVIDER`: 未传 `-P` 时使用的 provider 预设。
180
190
  - `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: 稳定性限制(单任务最大轮数;Shell 超时毫秒数)。
191
+ - `AUTOCLOW_SHELL`: 强制 shell 命令使用的 shell (`bash`、`powershell`、`cmd`、`sh`)。
181
192
  - `AUTOCLOW_INCLUDE_USAGE`: 设为 `1`/`true` 时向 API 请求 token 用量(可选开启)。
182
193
  - `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: 工具凭据,可作为 setup 的替代方式。
183
194
 
package/dist/agent.js CHANGED
@@ -5,18 +5,12 @@ import * as fs from 'fs';
5
5
  import * as os from 'os';
6
6
  import * as path from 'path';
7
7
  import * as util from 'util';
8
- import { getToolDefinitions, executeToolHandler } from './tools/index.js';
8
+ import { getToolDefinitions, executeToolHandler, listUnavailableTools } from './tools/index.js';
9
9
  import { withRetry } from './retry.js';
10
10
  import { truncateOutput } from './truncate.js';
11
+ import { buildShellInfo, resolveShellType } from './shell.js';
11
12
  const DEFAULT_MAX_STEPS = 25;
12
- // The shell behind child_process.exec differs by platform; telling the model
13
- // up front avoids trial-and-error turns (cmd.exe rejects ; and $()).
14
- export function buildShellInfo(platform = os.platform()) {
15
- if (platform === 'win32') {
16
- return 'cmd.exe (Windows). Chain commands with && only, there is no $() command substitution, mkdir has no -p flag, and native tool output may be GBK-garbled. For system queries prefer: powershell -Command "..."';
17
- }
18
- return 'POSIX shell (sh). Standard Unix tools apply.';
19
- }
13
+ const TOOL_RESULT_TRIM_MARKER = 'older tool output trimmed';
20
14
  export class Agent {
21
15
  client;
22
16
  messages;
@@ -30,10 +24,11 @@ export class Agent {
30
24
  });
31
25
  this.model = model;
32
26
  this.config = config;
27
+ const shellType = resolveShellType(config);
33
28
  const systemInfo = `
34
29
  System Information:
35
30
  - OS: ${os.type()} ${os.release()} (${os.platform()})
36
- - Shell: ${buildShellInfo()}
31
+ - Shell: ${buildShellInfo(shellType)}
37
32
  - Architecture: ${os.arch()}
38
33
  - Node.js Version: ${process.version}
39
34
  - Current Working Directory: ${process.cwd()}
@@ -41,9 +36,27 @@ System Information:
41
36
  - Home Directory: ${os.homedir()}
42
37
  - Current Date: ${new Date().toLocaleString()}
43
38
  `;
44
- const windowsShellRule = os.platform() === 'win32'
39
+ const shellRule = shellType === 'cmd'
45
40
  ? `\n8. Mind the shell noted above: on Windows it is cmd.exe, not bash — && only, no \$(...) substitution, no mkdir -p, GBK output possible; prefer powershell -Command "..." for system queries.`
46
- : '';
41
+ : shellType === 'powershell'
42
+ ? `\n8. The shell is Windows PowerShell: use PowerShell syntax — \$( ) works, but && does not in Windows PowerShell 5; use ; or separate tool calls instead.`
43
+ : '';
44
+ // Every turn resends every tool definition, so unconfigured capabilities
45
+ // are dropped from both the tool array and this capability list.
46
+ const unavailable = listUnavailableTools(config);
47
+ const has = (tool) => !unavailable.includes(tool);
48
+ const capabilities = [
49
+ '- Shell: execute_shell_command — run scripts, install packages, manage processes, interact with the OS',
50
+ '- Files: read_file / write_file — inspect logs, generate configs, produce reports',
51
+ has('web_search') ? '- Web: web_search — real-time information lookup' : null,
52
+ has('read_website') ? '- Web: read_website — extract article content from a URL' : null,
53
+ has('take_screenshot') ? '- Web: take_screenshot — capture page visuals' : null,
54
+ has('send_email') ? '- Communication: send_email — SMTP email delivery' : null,
55
+ has('send_notification') ? '- Communication: send_notification — push to Feishu/DingTalk/WeCom' : null,
56
+ has('generate_image') ? '- Creation: generate_image — AI image generation (DALL-E compatible)' : null,
57
+ has('optimize_prompt') ? '- Creation: optimize_prompt — refine raw prompts for creative/complex tasks (recommended before creative work)' : null,
58
+ '- Utility: get_current_datetime — accurate system time for temporal reasoning'
59
+ ].filter((line) => line !== null).join('\n');
47
60
  this.messages = [
48
61
  {
49
62
  role: "system",
@@ -54,12 +67,7 @@ You may be running on a developer workstation, a headless server, inside a Docke
54
67
  ${systemInfo}
55
68
 
56
69
  WHAT YOU CAN DO:
57
- - Shell: execute_shell_command — run scripts, install packages, manage processes, interact with the OS
58
- - Files: read_file / write_file — inspect logs, generate configs, produce reports
59
- - Web: web_search — real-time information lookup; read_website — extract article content; take_screenshot — capture page visuals
60
- - Communication: send_email — SMTP email delivery; send_notification — push to Feishu/DingTalk/WeCom
61
- - Creation: generate_image — AI image generation (DALL-E compatible); optimize_prompt — refine raw prompts for creative/complex tasks
62
- - Utility: get_current_datetime — accurate system time for temporal reasoning
70
+ ${capabilities}
63
71
 
64
72
  RULES OF ENGAGEMENT:
65
73
  1. One shot, not one chat. Produce working results, not conversation. Be terse.
@@ -68,7 +76,7 @@ RULES OF ENGAGEMENT:
68
76
  4. Container-friendly: stick to standard Unix tools available in Alpine/Debian slim images. No GUI apps, no browser-based debug tools.
69
77
  5. For creative or complex tasks (image prompts, long-form writing, intricate scripts): call optimize_prompt first. It significantly raises output quality.
70
78
  6. If a command fails, diagnose and try one alternative. Don't retry the same thing, don't give up on first error.
71
- 7. Read before write. When modifying a file, read it first. When installing a package, check if it's already there.${windowsShellRule}
79
+ 7. Read before write. When modifying a file, read it first. When installing a package, check if it's already there.${shellRule}
72
80
  `
73
81
  }
74
82
  ];
@@ -112,6 +120,7 @@ RULES OF ENGAGEMENT:
112
120
  }
113
121
  break;
114
122
  }
123
+ this.trimOldToolResults();
115
124
  step++;
116
125
  const spinner = this.jsonMode
117
126
  ? { stop() { }, fail() { }, text: '' }
@@ -352,6 +361,25 @@ RULES OF ENGAGEMENT:
352
361
  this.emitEvent({ event: 'run_end', ...result });
353
362
  return result;
354
363
  }
364
+ // Every turn resends the full history, so early large tool results
365
+ // dominate context growth. Keep the most recent results intact and bound
366
+ // older ones to a short excerpt (full output stays on disk via /view when
367
+ // it was large enough to be saved).
368
+ trimOldToolResults() {
369
+ const toolIndexes = [];
370
+ this.messages.forEach((m, i) => {
371
+ if (m.role === 'tool')
372
+ toolIndexes.push(i);
373
+ });
374
+ const cutoff = toolIndexes.length - 3;
375
+ for (let k = 0; k < cutoff; k++) {
376
+ const msg = this.messages[toolIndexes[k]];
377
+ if (typeof msg.content === 'string' && msg.content.length > 512 && !msg.content.includes(TOOL_RESULT_TRIM_MARKER)) {
378
+ const original = msg.content.length;
379
+ msg.content = `${msg.content.slice(0, 256)}\n[${TOOL_RESULT_TRIM_MARKER}: ${original} bytes total; re-run the tool if you need the full output again]`;
380
+ }
381
+ }
382
+ }
355
383
  async saveOutput(functionName, toolResult) {
356
384
  const outputDir = path.join(os.homedir(), '.autoclaw', 'output');
357
385
  if (!fs.existsSync(outputDir)) {
package/dist/batch.js CHANGED
@@ -38,11 +38,21 @@ export async function runBatch(entries, execute, options = {}) {
38
38
  const results = [];
39
39
  let completed = 0;
40
40
  let failed = 0;
41
- for (const entry of entries) {
41
+ let skipped = 0;
42
+ let stopped = false;
43
+ let done = 0;
44
+ const runEntry = async (i) => {
45
+ const entry = entries[i];
42
46
  let result;
43
47
  if (entry.error) {
44
48
  result = { id: entry.id, status: 'error', error: `Manifest line ${entry.lineNo}: ${entry.error}`, durationMs: 0 };
45
49
  }
50
+ else if (options.resume?.completedIds.has(entry.id)) {
51
+ result = options.resume.previousById.get(entry.id) ?? {
52
+ id: entry.id, status: 'error', error: 'missing previous result', durationMs: 0
53
+ };
54
+ skipped++;
55
+ }
46
56
  else {
47
57
  try {
48
58
  result = await execute(entry);
@@ -56,9 +66,19 @@ export async function runBatch(entries, execute, options = {}) {
56
66
  else
57
67
  failed++;
58
68
  results.push(result);
59
- options.onResult?.(entry, result, results.length, total);
69
+ done++;
70
+ options.onResult?.(entry, result, done, total);
60
71
  if (options.failFast && result.status !== 'completed')
61
- break;
62
- }
63
- return { results, completed, failed };
72
+ stopped = true;
73
+ };
74
+ const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, total || 1));
75
+ let next = 0;
76
+ const worker = async () => {
77
+ while (next < total && !stopped) {
78
+ const i = next++;
79
+ await runEntry(i);
80
+ }
81
+ };
82
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
83
+ return { results, completed, failed, skipped };
64
84
  }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import dotenv from 'dotenv';
6
6
  import { Agent } from './agent.js';
7
7
  import { parseManifest, runBatch } from './batch.js';
8
8
  import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
9
+ import { fetchModelIds, normalizeBaseUrl, testConnection } from './setup.js';
9
10
  import * as fs from 'fs';
10
11
  import * as path from 'path';
11
12
  import * as os from 'os';
@@ -44,7 +45,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
44
45
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
46
  // In dist/index.js, package.json is usually up one level in the root
46
47
  const pkgPath = path.join(__dirname, '..', 'package.json');
47
- let version = '1.3.0';
48
+ let version = '1.3.2';
48
49
  try {
49
50
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
50
51
  version = pkg.version;
@@ -81,6 +82,8 @@ program
81
82
  .description('Run tasks from a JSONL manifest, each in a fresh agent ({"id":"...","task":"..."})')
82
83
  .option('-o, --output <file>', 'Per-task results as JSONL (default: <manifest base>.results.jsonl)')
83
84
  .option('--fail-fast', 'Stop on the first failed task')
85
+ .option('--resume', 'Skip tasks already completed in the results file')
86
+ .option('-c, --concurrency <n>', 'Max tasks to run in parallel (default: 1)')
84
87
  .action(async (manifest, cmdOptions) => {
85
88
  const options = program.opts();
86
89
  await runBatchCommand(manifest, options, cmdOptions);
@@ -107,7 +110,7 @@ async function runSetup(options = {}) {
107
110
  }
108
111
  const providerAnswer = await inquirer.prompt([
109
112
  {
110
- type: 'list',
113
+ type: 'select',
111
114
  name: 'provider',
112
115
  message: 'Select your LLM provider:',
113
116
  choices: [
@@ -119,34 +122,101 @@ async function runSetup(options = {}) {
119
122
  ]);
120
123
  const provider = providerAnswer.provider;
121
124
  const preset = resolveProvider(provider === 'custom' ? undefined : provider);
122
- const answers = await inquirer.prompt([
123
- {
124
- type: 'password',
125
- name: 'apiKey',
126
- message: currentConfig.apiKey
127
- ? `Enter OpenAI API Key (Leave empty to keep ${maskSecret(currentConfig.apiKey)}):`
128
- : 'Enter OpenAI API Key:',
129
- mask: '*',
130
- validate: (input) => {
131
- if (input.length > 0)
132
- return true;
133
- if (currentConfig.apiKey)
134
- return true;
135
- return 'API Key cannot be empty.';
125
+ // The connection part (key / Base URL / model) is asked in a loop with a
126
+ // live test, so typos never make it into the saved config.
127
+ const askConnection = async () => {
128
+ const defaults = {
129
+ apiKey: currentConfig.apiKey,
130
+ baseUrl: currentConfig.baseUrl || preset?.baseUrl || 'https://api.openai.com/v1',
131
+ model: currentConfig.model || preset?.defaultModel || 'gpt-5.6'
132
+ };
133
+ const core = await inquirer.prompt([
134
+ {
135
+ type: 'password',
136
+ name: 'apiKey',
137
+ message: defaults.apiKey
138
+ ? `Enter API Key (Leave empty to keep ${maskSecret(defaults.apiKey)}):`
139
+ : `Enter API Key${preset?.apiKeyEnv ? ` (or set ${preset.apiKeyEnv} in your environment)` : ''}:`,
140
+ mask: '*',
141
+ validate: (input) => {
142
+ if (input.length > 0)
143
+ return true;
144
+ if (defaults.apiKey)
145
+ return true;
146
+ return 'API Key cannot be empty.';
147
+ }
148
+ },
149
+ {
150
+ type: 'input',
151
+ name: 'baseUrl',
152
+ message: 'Enter API Base URL:',
153
+ default: defaults.baseUrl
136
154
  }
137
- },
138
- {
139
- type: 'input',
140
- name: 'baseUrl',
141
- message: 'Enter API Base URL:',
142
- default: currentConfig.baseUrl || preset?.baseUrl || 'https://api.openai.com/v1'
143
- },
144
- {
145
- type: 'input',
146
- name: 'model',
147
- message: 'Enter default Model:',
148
- default: currentConfig.model || preset?.defaultModel || 'gpt-5.6'
149
- },
155
+ ]);
156
+ const apiKey = core.apiKey || defaults.apiKey || '';
157
+ const baseUrl = normalizeBaseUrl(core.baseUrl || defaults.baseUrl);
158
+ // Prefer the provider's own catalog over guessing model names.
159
+ console.log(chalk.dim('Fetching available models...'));
160
+ const ids = await fetchModelIds(baseUrl, apiKey);
161
+ let model;
162
+ if (ids) {
163
+ console.log(chalk.dim(`Found ${ids.length} models.`));
164
+ const picked = await inquirer.prompt([
165
+ {
166
+ type: 'select',
167
+ name: 'model',
168
+ message: 'Select default Model:',
169
+ choices: [{ name: '✎ Enter manually', value: '__manual__' }, ...ids.map(id => ({ name: id, value: id }))],
170
+ default: ids.includes(defaults.model) ? defaults.model : undefined,
171
+ pageSize: 12
172
+ }
173
+ ]);
174
+ if (picked.model === '__manual__') {
175
+ const manual = await inquirer.prompt([
176
+ { type: 'input', name: 'model', message: 'Enter default Model:', default: defaults.model }
177
+ ]);
178
+ model = manual.model;
179
+ }
180
+ else {
181
+ model = picked.model;
182
+ }
183
+ }
184
+ else {
185
+ const manual = await inquirer.prompt([
186
+ { type: 'input', name: 'model', message: 'Enter default Model (catalog unavailable):', default: defaults.model }
187
+ ]);
188
+ model = manual.model;
189
+ }
190
+ return { apiKey, baseUrl, model };
191
+ };
192
+ let connection = await askConnection();
193
+ console.log(chalk.dim('Running connection test (sends one tiny prompt — normal provider billing applies)...'));
194
+ let test = await testConnection(connection.baseUrl, connection.apiKey, connection.model);
195
+ while (!test.ok) {
196
+ console.log(chalk.red(`\n✗ ${test.message}`));
197
+ const next = await inquirer.prompt([
198
+ {
199
+ type: 'select',
200
+ name: 'action',
201
+ message: 'Connection test failed. What next?',
202
+ choices: [
203
+ { name: 'Re-enter API key / Base URL / model', value: 'edit' },
204
+ { name: 'Test again', value: 'retry' },
205
+ { name: 'Save anyway (e.g. the provider is temporarily down)', value: 'save' }
206
+ ],
207
+ default: 'edit'
208
+ }
209
+ ]);
210
+ if (next.action === 'save')
211
+ break;
212
+ if (next.action === 'edit')
213
+ connection = await askConnection();
214
+ console.log(chalk.dim('Running connection test...'));
215
+ test = await testConnection(connection.baseUrl, connection.apiKey, connection.model);
216
+ }
217
+ if (test.ok)
218
+ console.log(chalk.green('✓ Connection test passed.'));
219
+ const answers = await inquirer.prompt([
150
220
  {
151
221
  type: 'confirm',
152
222
  name: 'configureImage',
@@ -181,7 +251,7 @@ async function runSetup(options = {}) {
181
251
  }
182
252
  ]);
183
253
  // Resolve sensitive values (Keep old if empty)
184
- const finalApiKey = answers.apiKey || currentConfig.apiKey;
254
+ const finalApiKey = connection.apiKey || currentConfig.apiKey;
185
255
  let imageConfig = {
186
256
  imageApiKey: currentConfig.imageApiKey,
187
257
  imageBaseUrl: currentConfig.imageBaseUrl,
@@ -344,8 +414,8 @@ async function runSetup(options = {}) {
344
414
  }
345
415
  const newConfig = {
346
416
  apiKey: finalApiKey,
347
- baseUrl: answers.baseUrl,
348
- model: answers.model,
417
+ baseUrl: connection.baseUrl,
418
+ model: connection.model,
349
419
  provider: provider === 'custom' ? undefined : provider,
350
420
  ...imageConfig,
351
421
  ...emailConfig,
@@ -358,6 +428,8 @@ async function runSetup(options = {}) {
358
428
  }
359
429
  fs.writeFileSync(targetFile, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
360
430
  console.log(chalk.green(`\n✅ Configuration saved to ${targetFile}`));
431
+ console.log(chalk.dim(` provider: ${provider === 'custom' ? 'custom' : provider} | baseUrl: ${connection.baseUrl} | model: ${connection.model}`));
432
+ console.log(chalk.dim(` connection test: ${test.ok ? 'passed ✓' : 'skipped (saved without a passing test)'}`));
361
433
  console.log(chalk.cyan("You can now run 'autoclaw' to start using the agent."));
362
434
  }
363
435
  catch (error) {
@@ -465,10 +537,37 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
465
537
  process.exit(1);
466
538
  }
467
539
  const outputPath = cmdOptions.output || manifestPath.replace(/\.[^.]+$/, '') + '.results.jsonl';
468
- console.log(chalk.bold.cyan(`AutoClaw Batch 🦞 ${entries.length} task(s)`));
540
+ let resumeState;
541
+ if (cmdOptions.resume) {
542
+ const previousById = new Map();
543
+ const completedIds = new Set();
544
+ if (fs.existsSync(outputPath)) {
545
+ for (const line of fs.readFileSync(outputPath, 'utf-8').split('\n')) {
546
+ if (!line.trim())
547
+ continue;
548
+ try {
549
+ const r = JSON.parse(line);
550
+ if (r?.id) {
551
+ previousById.set(r.id, r);
552
+ if (r.status === 'completed')
553
+ completedIds.add(r.id);
554
+ }
555
+ }
556
+ catch {
557
+ // ignore malformed lines in the previous results file
558
+ }
559
+ }
560
+ }
561
+ else {
562
+ console.log(chalk.yellow(`--resume: no previous results at ${outputPath}; starting fresh.`));
563
+ }
564
+ resumeState = { completedIds, previousById };
565
+ }
566
+ const concurrency = Math.max(1, parseInt(cmdOptions.concurrency ?? '1', 10) || 1);
567
+ console.log(chalk.bold.cyan(`AutoClaw Batch 🦞 ${entries.length} task(s)${concurrency > 1 ? ` (concurrency ${concurrency})` : ''}${resumeState ? ' [resume]' : ''}`));
469
568
  console.log(chalk.dim(`Results: ${outputPath}\n`));
470
569
  const startedAt = Date.now();
471
- const { results, completed, failed } = await runBatch(entries, async (entry) => {
570
+ const { results, completed, failed, skipped } = await runBatch(entries, async (entry) => {
472
571
  // A fresh Agent per task keeps contexts isolated; per-task overrides
473
572
  // beat the globally resolved defaults.
474
573
  const taskPreset = resolveProvider(entry.provider);
@@ -494,6 +593,8 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
494
593
  };
495
594
  }, {
496
595
  failFast: !!cmdOptions.failFast,
596
+ concurrency,
597
+ resume: resumeState,
497
598
  onResult: (entry, result, done, total) => {
498
599
  const color = result.status === 'completed' ? chalk.green : chalk.red;
499
600
  console.log(color(`[${done}/${total}] ${entry.id} -> ${result.status} (${Math.round(result.durationMs / 1000)}s)`));
@@ -506,7 +607,8 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
506
607
  console.error(chalk.red(`Failed to write results to ${outputPath}: ${err.message}`));
507
608
  process.exit(1);
508
609
  }
509
- console.log(chalk.cyan(`\nBatch done: ${completed}/${results.length} completed, ${failed} failed in ${Math.round((Date.now() - startedAt) / 1000)}s -> ${outputPath}`));
610
+ const skipNote = skipped > 0 ? `, ${skipped} skipped (resume)` : '';
611
+ console.log(chalk.cyan(`\nBatch done: ${completed}/${results.length + skipped} completed, ${failed} failed${skipNote} in ${Math.round((Date.now() - startedAt) / 1000)}s -> ${outputPath}`));
510
612
  process.exit(failed > 0 ? 1 : 0);
511
613
  }
512
614
  async function runChat(queryParts, options) {
package/dist/setup.js ADDED
@@ -0,0 +1,61 @@
1
+ // Pure helpers behind the setup wizard: URL normalization, a live
2
+ // connection test with actionable error mapping, and provider model
3
+ // catalog fetching. Kept free of inquirer so they are unit-testable.
4
+ export function normalizeBaseUrl(url) {
5
+ let u = String(url ?? '').trim();
6
+ if (u && !/^https?:\/\//i.test(u))
7
+ u = `https://${u}`;
8
+ return u.replace(/\/+$/, '');
9
+ }
10
+ // Sends one tiny real prompt through the exact endpoint the user
11
+ // configured, and maps failures to the field that is most likely wrong.
12
+ export async function testConnection(baseUrl, apiKey, model) {
13
+ const url = `${normalizeBaseUrl(baseUrl)}/chat/completions`;
14
+ let resp;
15
+ try {
16
+ resp = await fetch(url, {
17
+ method: 'POST',
18
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
19
+ body: JSON.stringify({ model, messages: [{ role: 'user', content: 'ping' }], stream: false }),
20
+ signal: AbortSignal.timeout(15000)
21
+ });
22
+ }
23
+ catch (err) {
24
+ return { ok: false, kind: 'network', message: `Cannot reach ${url} (${err?.message ?? err}). Check the Base URL and your network.` };
25
+ }
26
+ if (resp.ok) {
27
+ return { ok: true, kind: 'ok', message: `Connection OK — ${model} responded.` };
28
+ }
29
+ const detail = (await resp.text().catch(() => '')).slice(0, 300);
30
+ if (resp.status === 401 || resp.status === 403) {
31
+ return { ok: false, kind: 'auth', message: `API key rejected (${resp.status}). Double-check the key. ${detail}` };
32
+ }
33
+ if (resp.status === 404) {
34
+ return { ok: false, kind: 'not-found', message: `Endpoint not found (404). The Base URL is likely wrong. ${detail}` };
35
+ }
36
+ if (resp.status === 400) {
37
+ return { ok: false, kind: 'model', message: `Request rejected (400). The model name "${model}" is likely wrong for this endpoint. ${detail}` };
38
+ }
39
+ return { ok: false, kind: 'server', message: `Provider returned ${resp.status}. ${detail}` };
40
+ }
41
+ // Returns the provider's model IDs (sorted) or null when the endpoint does
42
+ // not offer a catalog — the wizard then falls back to manual entry.
43
+ export async function fetchModelIds(baseUrl, apiKey) {
44
+ try {
45
+ const resp = await fetch(`${normalizeBaseUrl(baseUrl)}/models`, {
46
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
47
+ signal: AbortSignal.timeout(15000)
48
+ });
49
+ if (!resp.ok)
50
+ return null;
51
+ const data = await resp.json();
52
+ const ids = (Array.isArray(data?.data) ? data.data : [])
53
+ .map((m) => m?.id)
54
+ .filter((id) => typeof id === 'string' && id.length > 0);
55
+ ids.sort();
56
+ return ids.length > 0 ? ids : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
package/dist/shell.js ADDED
@@ -0,0 +1,171 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ const GIT_BASH_CANDIDATES = [
4
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
5
+ 'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
6
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe'
7
+ ];
8
+ let cachedType = null;
9
+ let cachedBashPath;
10
+ function findBashPath() {
11
+ if (process.platform === 'win32') {
12
+ for (const candidate of GIT_BASH_CANDIDATES) {
13
+ if (fs.existsSync(candidate))
14
+ return candidate;
15
+ }
16
+ try {
17
+ // 'where' may report WSL's System32\bash.exe, which has a different
18
+ // filesystem view — exclude it.
19
+ const out = execSync('where bash.exe', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
20
+ const hit = out.split(/\r?\n/).map(l => l.trim()).find(l => l && !/System32/i.test(l));
21
+ return hit ?? null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ return fs.existsSync('/bin/bash') ? '/bin/bash' : null;
28
+ }
29
+ export function getBashPath() {
30
+ if (cachedBashPath === undefined)
31
+ cachedBashPath = findBashPath();
32
+ return cachedBashPath;
33
+ }
34
+ export function resolveShellType(config) {
35
+ const requested = String(config?.shell || process.env.AUTOCLOW_SHELL || 'auto').toLowerCase();
36
+ if (requested === 'bash' || requested === 'powershell' || requested === 'cmd' || requested === 'sh') {
37
+ if (requested !== 'bash' || getBashPath())
38
+ return requested;
39
+ }
40
+ if (!cachedType) {
41
+ cachedType = process.platform === 'win32' ? (getBashPath() ? 'bash' : 'powershell') : 'sh';
42
+ }
43
+ return cachedType;
44
+ }
45
+ // Windows tools emit GBK on CN-locale systems while Unix tools emit UTF-8;
46
+ // strict UTF-8 decode with GBK fallback covers both.
47
+ export function smartDecode(buf) {
48
+ try {
49
+ return new TextDecoder('utf-8', { fatal: true }).decode(buf);
50
+ }
51
+ catch {
52
+ try {
53
+ return new TextDecoder('gbk').decode(buf);
54
+ }
55
+ catch {
56
+ return buf.toString('utf8');
57
+ }
58
+ }
59
+ }
60
+ export function execShellCommand(command, opts) {
61
+ const type = resolveShellType();
62
+ let file;
63
+ let args;
64
+ if (type === 'bash') {
65
+ // -l sources the profile so Git Bash's /usr/bin lands on PATH and Unix
66
+ // tools (ls, grep, ...) actually resolve.
67
+ file = getBashPath();
68
+ args = ['-l', '-c', command];
69
+ }
70
+ else if (type === 'powershell') {
71
+ file = 'powershell.exe';
72
+ args = ['-NoProfile', '-Command', command];
73
+ }
74
+ else if (type === 'cmd') {
75
+ file = process.env.ComSpec || 'cmd.exe';
76
+ args = ['/d', '/s', '/c', command];
77
+ }
78
+ else {
79
+ file = '/bin/sh';
80
+ args = ['-c', command];
81
+ }
82
+ return new Promise(resolve => {
83
+ const stdout = [];
84
+ const stderr = [];
85
+ let total = 0;
86
+ let timedOut = false;
87
+ let truncated = false;
88
+ let settled = false;
89
+ const posix = process.platform !== 'win32';
90
+ const child = spawn(file, args, { windowsHide: true, detached: posix });
91
+ const finish = () => {
92
+ if (settled)
93
+ return;
94
+ settled = true;
95
+ clearTimeout(timer);
96
+ resolve({
97
+ stdout: smartDecode(Buffer.concat(stdout)),
98
+ stderr: smartDecode(Buffer.concat(stderr)),
99
+ timedOut,
100
+ truncated
101
+ });
102
+ };
103
+ const killTree = () => {
104
+ if (!child.pid)
105
+ return;
106
+ if (posix) {
107
+ try {
108
+ process.kill(-child.pid, 'SIGKILL');
109
+ }
110
+ catch {
111
+ child.kill('SIGKILL');
112
+ }
113
+ }
114
+ else {
115
+ // Terminating the shell alone would orphan grandchildren, whose
116
+ // inherited stdio pipes keep 'close' pending — kill the whole tree.
117
+ spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
118
+ }
119
+ };
120
+ const timer = setTimeout(() => {
121
+ timedOut = true;
122
+ killTree();
123
+ }, Math.max(1, opts.timeoutMs));
124
+ const collect = (buf, chunk) => {
125
+ if (total >= opts.maxBuffer)
126
+ return;
127
+ total += chunk.length;
128
+ if (total > opts.maxBuffer) {
129
+ buf.push(chunk.subarray(0, chunk.length - (total - opts.maxBuffer)));
130
+ truncated = true;
131
+ killTree();
132
+ }
133
+ else {
134
+ buf.push(chunk);
135
+ }
136
+ };
137
+ child.stdout.on('data', c => collect(stdout, c));
138
+ child.stderr.on('data', c => collect(stderr, c));
139
+ child.on('error', err => {
140
+ stderr.push(Buffer.from(`\n[shell] failed to start ${type}: ${err.message}`));
141
+ finish();
142
+ });
143
+ child.on('exit', () => {
144
+ // The direct child is gone. Killed shells leave grandchildren holding
145
+ // stdio pipes, so don't wait for 'close' after a kill — return what we
146
+ // collected. On natural exit give the streams a short grace period
147
+ // before 'close' confirms the full flush.
148
+ if (timedOut || truncated) {
149
+ finish();
150
+ }
151
+ else {
152
+ const grace = setTimeout(finish, 1000);
153
+ if (typeof grace.unref === 'function')
154
+ grace.unref();
155
+ }
156
+ });
157
+ child.on('close', finish);
158
+ });
159
+ }
160
+ export function buildShellInfo(type) {
161
+ switch (type) {
162
+ case 'bash':
163
+ return 'Bash (POSIX). Standard Unix tools, pipes and $() substitution work.';
164
+ case 'powershell':
165
+ return 'Windows PowerShell. Use PowerShell syntax: $() works, but && does not on Windows PowerShell 5 — use ; or separate calls. Prefer cmdlets like Get-ChildItem.';
166
+ case 'cmd':
167
+ return 'cmd.exe (Windows). Chain commands with && only, there is no $() command substitution, mkdir has no -p flag, and native tool output may be GBK-garbled. For system queries prefer: powershell -Command "..."';
168
+ default:
169
+ return 'POSIX shell (sh). Standard Unix tools apply.';
170
+ }
171
+ }
@@ -1,9 +1,20 @@
1
1
  import { chromium } from 'playwright';
2
2
  import { Readability } from '@mozilla/readability';
3
3
  import { JSDOM } from 'jsdom';
4
+ import { createRequire } from 'module';
5
+ const require = createRequire(import.meta.url);
4
6
  export const BrowserTool = {
5
7
  name: "Web Browser",
6
8
  configKeys: [],
9
+ isAvailable: () => {
10
+ try {
11
+ require.resolve('playwright');
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ },
7
18
  definition: {
8
19
  type: "function",
9
20
  function: {
@@ -1,12 +1,13 @@
1
- import { exec } from 'child_process';
2
1
  import * as fs from 'fs/promises';
3
2
  import * as path from 'path';
4
3
  import inquirer from 'inquirer';
5
4
  import chalk from 'chalk';
6
- import util from 'util';
7
- const execAsync = util.promisify(exec);
5
+ import { execShellCommand } from '../shell.js';
8
6
  const DEFAULT_SHELL_TIMEOUT_MS = 120000;
9
7
  const SHELL_MAX_BUFFER = 10 * 1024 * 1024;
8
+ // Bounded reads keep a huge file from exhausting memory or the model context;
9
+ // the agent-level truncation in truncate.ts applies on top of this.
10
+ const READ_FILE_MAX_BYTES = 1024 * 1024;
10
11
  export const ShellTool = {
11
12
  name: "Shell Execution",
12
13
  definition: {
@@ -50,16 +51,15 @@ export const ShellTool = {
50
51
  console.log(chalk.gray("(Auto-confirming command execution due to --yes flag)"));
51
52
  }
52
53
  try {
53
- const { stdout, stderr } = await execAsync(args.command, {
54
- timeout: timeoutMs,
55
- maxBuffer: SHELL_MAX_BUFFER
56
- });
57
- return stdout + (stderr ? `\nStderr: ${stderr}` : '');
54
+ const r = await execShellCommand(args.command, { timeoutMs, maxBuffer: SHELL_MAX_BUFFER });
55
+ if (r.timedOut) {
56
+ return `Command timed out after ${timeoutMs}ms and was terminated.\nStdout: ${r.stdout}\nStderr: ${r.stderr}`;
57
+ }
58
+ return (r.stdout +
59
+ (r.truncated ? '\n[AutoClaw] Output truncated at the buffer limit.' : '') +
60
+ (r.stderr ? `\nStderr: ${r.stderr}` : ''));
58
61
  }
59
62
  catch (error) {
60
- if (error.killed === true || error.signal) {
61
- return `Command timed out after ${timeoutMs}ms and was terminated.\nStdout: ${error.stdout ?? ''}\nStderr: ${error.stderr ?? ''}`;
62
- }
63
63
  return `Command failed: ${error.message}\nStdout: ${error.stdout}\nStderr: ${error.stderr}`;
64
64
  }
65
65
  }
@@ -81,13 +81,30 @@ export const ReadFileTool = {
81
81
  }
82
82
  },
83
83
  handler: async (args) => {
84
+ let fh;
84
85
  try {
85
- const content = await fs.readFile(args.path, 'utf-8');
86
+ fh = await fs.open(args.path, 'r');
87
+ const buf = Buffer.alloc(READ_FILE_MAX_BYTES);
88
+ const { bytesRead } = await fh.read(buf, 0, READ_FILE_MAX_BYTES, 0);
89
+ const slice = buf.subarray(0, bytesRead);
90
+ // NUL bytes are the reliable tell for binary content; returning them
91
+ // as "utf-8" would only hand the model mojibake.
92
+ if (slice.includes(0)) {
93
+ return `Error: ${args.path} looks like a binary file (${bytesRead} bytes read). Inspect it with execute_shell_command instead (e.g. strings, xxd, file).`;
94
+ }
95
+ const content = slice.toString('utf-8');
96
+ if (bytesRead === READ_FILE_MAX_BYTES) {
97
+ const { size } = await fh.stat();
98
+ return `${content}\n[AutoClaw] File truncated at ${READ_FILE_MAX_BYTES} bytes (file is ${size} bytes). Use execute_shell_command to read specific ranges.`;
99
+ }
86
100
  return content;
87
101
  }
88
102
  catch (error) {
89
103
  return `Error reading file: ${error.message}`;
90
104
  }
105
+ finally {
106
+ await fh?.close();
107
+ }
91
108
  }
92
109
  };
93
110
  export const WriteFileTool = {
@@ -2,6 +2,7 @@ import nodemailer from 'nodemailer';
2
2
  export const EmailTool = {
3
3
  name: "Email Service",
4
4
  configKeys: ["smtpHost", "smtpPort", "smtpUser", "smtpPass", "smtpFrom"],
5
+ isAvailable: (config) => !!(config?.smtpHost && config?.smtpUser && config?.smtpPass),
5
6
  definition: {
6
7
  type: "function",
7
8
  function: {
@@ -33,6 +34,9 @@ export const EmailTool = {
33
34
  host: config.smtpHost,
34
35
  port: parseInt(config.smtpPort || '587'),
35
36
  secure: parseInt(config.smtpPort) === 465, // true for 465, false for other ports
37
+ connectionTimeout: 30000,
38
+ greetingTimeout: 30000,
39
+ socketTimeout: 120000,
36
40
  auth: {
37
41
  user: config.smtpUser,
38
42
  pass: config.smtpPass,
@@ -64,7 +64,7 @@ const toolDefinition = {
64
64
  }
65
65
  };
66
66
  async function downloadImage(url, destPath) {
67
- const response = await fetch(url);
67
+ const response = await fetch(url, { signal: AbortSignal.timeout(120000) });
68
68
  if (!response.ok)
69
69
  throw new Error(`Failed to download image: ${response.statusText}`);
70
70
  const buffer = await response.arrayBuffer();
@@ -251,6 +251,7 @@ const handler = async (args, config) => {
251
251
  };
252
252
  export const ImageTool = {
253
253
  name: "Image Generation",
254
+ isAvailable: (config) => !!(config?.imageApiKey || config?.apiKey || process.env.OPENAI_API_KEY),
254
255
  definition: toolDefinition,
255
256
  handler: handler
256
257
  };
@@ -20,8 +20,15 @@ export const toolRegistry = [
20
20
  ScreenshotTool,
21
21
  ImageTool
22
22
  ];
23
- export function getToolDefinitions() {
24
- return toolRegistry.map(t => t.definition);
23
+ export function getToolDefinitions(config) {
24
+ return toolRegistry
25
+ .filter(t => !t.isAvailable || t.isAvailable(config ?? {}))
26
+ .map(t => t.definition);
27
+ }
28
+ export function listUnavailableTools(config) {
29
+ return toolRegistry
30
+ .filter(t => t.isAvailable && !t.isAvailable(config ?? {}))
31
+ .map(t => t.definition.function.name);
25
32
  }
26
33
  export async function executeToolHandler(name, args, fullConfig) {
27
34
  const tool = toolRegistry.find(t => t.definition.function.name === name);
@@ -5,6 +5,8 @@ export const NotifyTool = {
5
5
  "dingtalkWebhook", "dingtalkKeyword",
6
6
  "wecomWebhook", "wecomKeyword"
7
7
  ],
8
+ isAvailable: (config) => !!(config?.feishuWebhook || config?.dingtalkWebhook || config?.wecomWebhook ||
9
+ process.env.FEISHU_WEBHOOK || process.env.DINGTALK_WEBHOOK || process.env.WECOM_WEBHOOK),
8
10
  definition: {
9
11
  type: "function",
10
12
  function: {
@@ -78,7 +80,8 @@ export const NotifyTool = {
78
80
  const response = await fetch(webhookUrl, {
79
81
  method: "POST",
80
82
  headers: { "Content-Type": "application/json" },
81
- body: JSON.stringify(payload)
83
+ body: JSON.stringify(payload),
84
+ signal: AbortSignal.timeout(30000)
82
85
  });
83
86
  const result = await response.json();
84
87
  // Platform specific success checks
@@ -1,6 +1,7 @@
1
1
  import OpenAI from 'openai';
2
2
  export const PromptOptimizerTool = {
3
3
  name: "Prompt Optimizer",
4
+ isAvailable: (config) => !!(config?.apiKey || process.env.OPENAI_API_KEY),
4
5
  definition: {
5
6
  type: "function",
6
7
  function: {
@@ -1,7 +1,9 @@
1
1
  import { chromium } from 'playwright';
2
+ import { createRequire } from 'module';
2
3
  import * as fs from 'fs';
3
4
  import * as os from 'os';
4
5
  import * as child_process from 'child_process';
6
+ const require = createRequire(import.meta.url);
5
7
  // Helper to check for common CJK and Emoji font paths on Linux
6
8
  const checkLinuxFonts = () => {
7
9
  if (os.platform() !== 'linux')
@@ -77,6 +79,15 @@ const installFonts = (missing) => {
77
79
  export const ScreenshotTool = {
78
80
  name: "Screenshot Tool",
79
81
  configKeys: [],
82
+ isAvailable: () => {
83
+ try {
84
+ require.resolve('playwright');
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ },
80
91
  definition: {
81
92
  type: "function",
82
93
  function: {
@@ -1,6 +1,7 @@
1
1
  export const SearchTool = {
2
2
  name: "Web Search (Tavily)",
3
3
  configKeys: ["tavilyApiKey"],
4
+ isAvailable: (config) => !!(config?.tavilyApiKey || process.env.TAVILY_API_KEY),
4
5
  definition: {
5
6
  type: "function",
6
7
  function: {
@@ -41,7 +42,8 @@ export const SearchTool = {
41
42
  include_answer: true,
42
43
  include_images: false,
43
44
  max_results: 5
44
- })
45
+ }),
46
+ signal: AbortSignal.timeout(30000)
45
47
  });
46
48
  if (!response.ok) {
47
49
  const errText = await response.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autoclaw",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {