autoclaw 1.3.0 → 1.3.1
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 +11 -0
- package/README.zh-CN.md +11 -0
- package/dist/agent.js +47 -19
- package/dist/batch.js +25 -5
- package/dist/index.js +36 -4
- package/dist/shell.js +171 -0
- package/dist/tools/browser.js +11 -0
- package/dist/tools/core.js +8 -11
- package/dist/tools/email.js +1 -0
- package/dist/tools/image.js +1 -0
- package/dist/tools/index.js +9 -2
- package/dist/tools/notify.js +2 -0
- package/dist/tools/prompt-optimizer.js +1 -0
- package/dist/tools/screenshot.js +11 -0
- package/dist/tools/search.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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.${
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
done++;
|
|
70
|
+
options.onResult?.(entry, result, done, total);
|
|
60
71
|
if (options.failFast && result.status !== 'completed')
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
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
|
@@ -44,7 +44,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
44
44
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
45
|
// In dist/index.js, package.json is usually up one level in the root
|
|
46
46
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
47
|
-
let version = '1.3.
|
|
47
|
+
let version = '1.3.1';
|
|
48
48
|
try {
|
|
49
49
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
50
50
|
version = pkg.version;
|
|
@@ -81,6 +81,8 @@ program
|
|
|
81
81
|
.description('Run tasks from a JSONL manifest, each in a fresh agent ({"id":"...","task":"..."})')
|
|
82
82
|
.option('-o, --output <file>', 'Per-task results as JSONL (default: <manifest base>.results.jsonl)')
|
|
83
83
|
.option('--fail-fast', 'Stop on the first failed task')
|
|
84
|
+
.option('--resume', 'Skip tasks already completed in the results file')
|
|
85
|
+
.option('-c, --concurrency <n>', 'Max tasks to run in parallel (default: 1)')
|
|
84
86
|
.action(async (manifest, cmdOptions) => {
|
|
85
87
|
const options = program.opts();
|
|
86
88
|
await runBatchCommand(manifest, options, cmdOptions);
|
|
@@ -465,10 +467,37 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
|
|
|
465
467
|
process.exit(1);
|
|
466
468
|
}
|
|
467
469
|
const outputPath = cmdOptions.output || manifestPath.replace(/\.[^.]+$/, '') + '.results.jsonl';
|
|
468
|
-
|
|
470
|
+
let resumeState;
|
|
471
|
+
if (cmdOptions.resume) {
|
|
472
|
+
const previousById = new Map();
|
|
473
|
+
const completedIds = new Set();
|
|
474
|
+
if (fs.existsSync(outputPath)) {
|
|
475
|
+
for (const line of fs.readFileSync(outputPath, 'utf-8').split('\n')) {
|
|
476
|
+
if (!line.trim())
|
|
477
|
+
continue;
|
|
478
|
+
try {
|
|
479
|
+
const r = JSON.parse(line);
|
|
480
|
+
if (r?.id) {
|
|
481
|
+
previousById.set(r.id, r);
|
|
482
|
+
if (r.status === 'completed')
|
|
483
|
+
completedIds.add(r.id);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// ignore malformed lines in the previous results file
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.log(chalk.yellow(`--resume: no previous results at ${outputPath}; starting fresh.`));
|
|
493
|
+
}
|
|
494
|
+
resumeState = { completedIds, previousById };
|
|
495
|
+
}
|
|
496
|
+
const concurrency = Math.max(1, parseInt(cmdOptions.concurrency ?? '1', 10) || 1);
|
|
497
|
+
console.log(chalk.bold.cyan(`AutoClaw Batch 🦞 ${entries.length} task(s)${concurrency > 1 ? ` (concurrency ${concurrency})` : ''}${resumeState ? ' [resume]' : ''}`));
|
|
469
498
|
console.log(chalk.dim(`Results: ${outputPath}\n`));
|
|
470
499
|
const startedAt = Date.now();
|
|
471
|
-
const { results, completed, failed } = await runBatch(entries, async (entry) => {
|
|
500
|
+
const { results, completed, failed, skipped } = await runBatch(entries, async (entry) => {
|
|
472
501
|
// A fresh Agent per task keeps contexts isolated; per-task overrides
|
|
473
502
|
// beat the globally resolved defaults.
|
|
474
503
|
const taskPreset = resolveProvider(entry.provider);
|
|
@@ -494,6 +523,8 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
|
|
|
494
523
|
};
|
|
495
524
|
}, {
|
|
496
525
|
failFast: !!cmdOptions.failFast,
|
|
526
|
+
concurrency,
|
|
527
|
+
resume: resumeState,
|
|
497
528
|
onResult: (entry, result, done, total) => {
|
|
498
529
|
const color = result.status === 'completed' ? chalk.green : chalk.red;
|
|
499
530
|
console.log(color(`[${done}/${total}] ${entry.id} -> ${result.status} (${Math.round(result.durationMs / 1000)}s)`));
|
|
@@ -506,7 +537,8 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
|
|
|
506
537
|
console.error(chalk.red(`Failed to write results to ${outputPath}: ${err.message}`));
|
|
507
538
|
process.exit(1);
|
|
508
539
|
}
|
|
509
|
-
|
|
540
|
+
const skipNote = skipped > 0 ? `, ${skipped} skipped (resume)` : '';
|
|
541
|
+
console.log(chalk.cyan(`\nBatch done: ${completed}/${results.length + skipped} completed, ${failed} failed${skipNote} in ${Math.round((Date.now() - startedAt) / 1000)}s -> ${outputPath}`));
|
|
510
542
|
process.exit(failed > 0 ? 1 : 0);
|
|
511
543
|
}
|
|
512
544
|
async function runChat(queryParts, options) {
|
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
|
+
}
|
package/dist/tools/browser.js
CHANGED
|
@@ -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: {
|
package/dist/tools/core.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
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
|
|
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;
|
|
10
8
|
export const ShellTool = {
|
|
@@ -50,16 +48,15 @@ export const ShellTool = {
|
|
|
50
48
|
console.log(chalk.gray("(Auto-confirming command execution due to --yes flag)"));
|
|
51
49
|
}
|
|
52
50
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
return stdout +
|
|
51
|
+
const r = await execShellCommand(args.command, { timeoutMs, maxBuffer: SHELL_MAX_BUFFER });
|
|
52
|
+
if (r.timedOut) {
|
|
53
|
+
return `Command timed out after ${timeoutMs}ms and was terminated.\nStdout: ${r.stdout}\nStderr: ${r.stderr}`;
|
|
54
|
+
}
|
|
55
|
+
return (r.stdout +
|
|
56
|
+
(r.truncated ? '\n[AutoClaw] Output truncated at the buffer limit.' : '') +
|
|
57
|
+
(r.stderr ? `\nStderr: ${r.stderr}` : ''));
|
|
58
58
|
}
|
|
59
59
|
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
60
|
return `Command failed: ${error.message}\nStdout: ${error.stdout}\nStderr: ${error.stderr}`;
|
|
64
61
|
}
|
|
65
62
|
}
|
package/dist/tools/email.js
CHANGED
|
@@ -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: {
|
package/dist/tools/image.js
CHANGED
|
@@ -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
|
};
|
package/dist/tools/index.js
CHANGED
|
@@ -20,8 +20,15 @@ export const toolRegistry = [
|
|
|
20
20
|
ScreenshotTool,
|
|
21
21
|
ImageTool
|
|
22
22
|
];
|
|
23
|
-
export function getToolDefinitions() {
|
|
24
|
-
return toolRegistry
|
|
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);
|
package/dist/tools/notify.js
CHANGED
|
@@ -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: {
|
package/dist/tools/screenshot.js
CHANGED
|
@@ -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: {
|
package/dist/tools/search.js
CHANGED