autoclaw 1.2.0 → 1.3.0
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 +10 -0
- package/README.zh-CN.md +10 -0
- package/dist/agent.js +13 -1
- package/dist/batch.js +64 -0
- package/dist/index.js +82 -6
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -111,6 +111,16 @@ autoclaw "Deploy and report" -y -n --json
|
|
|
111
111
|
```
|
|
112
112
|
Token usage is collected only when `AUTOCLOW_INCLUDE_USAGE=1` (or `true`) is set — it is opt-in because not every OpenAI-compatible provider accepts `stream_options.include_usage`.
|
|
113
113
|
|
|
114
|
+
### Batch Mode (Swarm Worker)
|
|
115
|
+
Feed a JSONL manifest of tasks; each task runs in a fresh, isolated agent (one task's context never leaks into another) and per-task results are written as JSONL:
|
|
116
|
+
```bash
|
|
117
|
+
autoclaw batch tasks.jsonl -y # results -> tasks.results.jsonl
|
|
118
|
+
autoclaw batch tasks.jsonl -o out.jsonl --fail-fast
|
|
119
|
+
```
|
|
120
|
+
Manifest lines are `{"id": "...", "task": "..."}` — `id` is optional (defaults to `task-N`); blank lines and `#` comments are skipped. Optional per-task overrides: `maxSteps`, `model`, `provider`.
|
|
121
|
+
|
|
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
|
+
|
|
114
124
|
### Auto-Confirm (CI/CD)
|
|
115
125
|
Automatically approve all tool executions (dangerous, use with caution or in sandboxes).
|
|
116
126
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -112,6 +112,16 @@ autoclaw "执行部署并汇报" -y -n --json
|
|
|
112
112
|
```
|
|
113
113
|
Token 用量仅在设置 `AUTOCLOW_INCLUDE_USAGE=1`(或 `true`)时才收集——这是可选项,因为并非所有 OpenAI 兼容端点都接受 `stream_options.include_usage`。
|
|
114
114
|
|
|
115
|
+
### 批处理模式 (蜂群工人)
|
|
116
|
+
把 JSONL 任务清单交给 AutoClaw;每个任务在全新的隔离 Agent 中执行(任务之间上下文互不污染),结果逐行写入 JSONL 文件:
|
|
117
|
+
```bash
|
|
118
|
+
autoclaw batch tasks.jsonl -y # 结果 -> tasks.results.jsonl
|
|
119
|
+
autoclaw batch tasks.jsonl -o out.jsonl --fail-fast
|
|
120
|
+
```
|
|
121
|
+
清单每行为 `{"id": "...", "task": "..."}`——`id` 可省略(默认 `task-N`);空行和 `#` 注释行会跳过。可选的每任务覆盖项:`maxSteps`、`model`、`provider`。
|
|
122
|
+
|
|
123
|
+
单个任务失败不会中断批次(需要中断用 `--fail-fast`)。全部完成时进程退出 `0`,否则 `1`,cron 和 K8s Job 由此感知批次成败。任务输出保持人类可读——结果文件才是机器可读契约,含每个任务的 `status`、`steps`、`message`、`error`、`usage`。
|
|
124
|
+
|
|
115
125
|
### 自动确认 (CI/CD)
|
|
116
126
|
自动批准所有工具执行(危险操作,请谨慎使用或在沙箱环境下运行)。
|
|
117
127
|
```bash
|
package/dist/agent.js
CHANGED
|
@@ -9,6 +9,14 @@ import { getToolDefinitions, executeToolHandler } from './tools/index.js';
|
|
|
9
9
|
import { withRetry } from './retry.js';
|
|
10
10
|
import { truncateOutput } from './truncate.js';
|
|
11
11
|
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
|
+
}
|
|
12
20
|
export class Agent {
|
|
13
21
|
client;
|
|
14
22
|
messages;
|
|
@@ -25,6 +33,7 @@ export class Agent {
|
|
|
25
33
|
const systemInfo = `
|
|
26
34
|
System Information:
|
|
27
35
|
- OS: ${os.type()} ${os.release()} (${os.platform()})
|
|
36
|
+
- Shell: ${buildShellInfo()}
|
|
28
37
|
- Architecture: ${os.arch()}
|
|
29
38
|
- Node.js Version: ${process.version}
|
|
30
39
|
- Current Working Directory: ${process.cwd()}
|
|
@@ -32,6 +41,9 @@ System Information:
|
|
|
32
41
|
- Home Directory: ${os.homedir()}
|
|
33
42
|
- Current Date: ${new Date().toLocaleString()}
|
|
34
43
|
`;
|
|
44
|
+
const windowsShellRule = os.platform() === 'win32'
|
|
45
|
+
? `\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
|
+
: '';
|
|
35
47
|
this.messages = [
|
|
36
48
|
{
|
|
37
49
|
role: "system",
|
|
@@ -56,7 +68,7 @@ RULES OF ENGAGEMENT:
|
|
|
56
68
|
4. Container-friendly: stick to standard Unix tools available in Alpine/Debian slim images. No GUI apps, no browser-based debug tools.
|
|
57
69
|
5. For creative or complex tasks (image prompts, long-form writing, intricate scripts): call optimize_prompt first. It significantly raises output quality.
|
|
58
70
|
6. If a command fails, diagnose and try one alternative. Don't retry the same thing, don't give up on first error.
|
|
59
|
-
7. Read before write. When modifying a file, read it first. When installing a package, check if it's already there
|
|
71
|
+
7. Read before write. When modifying a file, read it first. When installing a package, check if it's already there.${windowsShellRule}
|
|
60
72
|
`
|
|
61
73
|
}
|
|
62
74
|
];
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Blank lines and lines starting with '#' are skipped. Malformed lines are
|
|
2
|
+
// kept as errored entries (in manifest order) rather than dropped, so the
|
|
3
|
+
// result file accounts for every task the user submitted.
|
|
4
|
+
export function parseManifest(raw) {
|
|
5
|
+
return raw
|
|
6
|
+
.split('\n')
|
|
7
|
+
.map((line, idx) => {
|
|
8
|
+
const lineNo = idx + 1;
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
11
|
+
return null;
|
|
12
|
+
let parsed = null;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(trimmed);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// fall through to the error entry below
|
|
18
|
+
}
|
|
19
|
+
const id = parsed && typeof parsed === 'object' && typeof parsed.id === 'string' && parsed.id.trim()
|
|
20
|
+
? parsed.id.trim()
|
|
21
|
+
: `task-${lineNo}`;
|
|
22
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.task !== 'string' || !parsed.task.trim()) {
|
|
23
|
+
return { lineNo, id, task: '', error: 'not valid JSON or missing "task" field' };
|
|
24
|
+
}
|
|
25
|
+
const entry = { lineNo, id, task: parsed.task.trim() };
|
|
26
|
+
if (typeof parsed.maxSteps === 'number' && parsed.maxSteps > 0)
|
|
27
|
+
entry.maxSteps = parsed.maxSteps;
|
|
28
|
+
if (typeof parsed.model === 'string' && parsed.model.trim())
|
|
29
|
+
entry.model = parsed.model.trim();
|
|
30
|
+
if (typeof parsed.provider === 'string' && parsed.provider.trim())
|
|
31
|
+
entry.provider = parsed.provider.trim();
|
|
32
|
+
return entry;
|
|
33
|
+
})
|
|
34
|
+
.filter((entry) => entry !== null);
|
|
35
|
+
}
|
|
36
|
+
export async function runBatch(entries, execute, options = {}) {
|
|
37
|
+
const total = entries.length;
|
|
38
|
+
const results = [];
|
|
39
|
+
let completed = 0;
|
|
40
|
+
let failed = 0;
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
let result;
|
|
43
|
+
if (entry.error) {
|
|
44
|
+
result = { id: entry.id, status: 'error', error: `Manifest line ${entry.lineNo}: ${entry.error}`, durationMs: 0 };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
try {
|
|
48
|
+
result = await execute(entry);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
result = { id: entry.id, status: 'error', error: err?.message ?? String(err), durationMs: 0 };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (result.status === 'completed')
|
|
55
|
+
completed++;
|
|
56
|
+
else
|
|
57
|
+
failed++;
|
|
58
|
+
results.push(result);
|
|
59
|
+
options.onResult?.(entry, result, results.length, total);
|
|
60
|
+
if (options.failFast && result.status !== 'completed')
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
return { results, completed, failed };
|
|
64
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import inquirer from 'inquirer';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import dotenv from 'dotenv';
|
|
6
6
|
import { Agent } from './agent.js';
|
|
7
|
+
import { parseManifest, runBatch } from './batch.js';
|
|
7
8
|
import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
|
|
8
9
|
import * as fs from 'fs';
|
|
9
10
|
import * as path from 'path';
|
|
@@ -43,7 +44,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
43
44
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
44
45
|
// In dist/index.js, package.json is usually up one level in the root
|
|
45
46
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
46
|
-
let version = '1.
|
|
47
|
+
let version = '1.3.0';
|
|
47
48
|
try {
|
|
48
49
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
49
50
|
version = pkg.version;
|
|
@@ -75,6 +76,15 @@ program
|
|
|
75
76
|
const options = program.opts();
|
|
76
77
|
await runChat(queryParts, options);
|
|
77
78
|
});
|
|
79
|
+
program
|
|
80
|
+
.command('batch <manifest>')
|
|
81
|
+
.description('Run tasks from a JSONL manifest, each in a fresh agent ({"id":"...","task":"..."})')
|
|
82
|
+
.option('-o, --output <file>', 'Per-task results as JSONL (default: <manifest base>.results.jsonl)')
|
|
83
|
+
.option('--fail-fast', 'Stop on the first failed task')
|
|
84
|
+
.action(async (manifest, cmdOptions) => {
|
|
85
|
+
const options = program.opts();
|
|
86
|
+
await runBatchCommand(manifest, options, cmdOptions);
|
|
87
|
+
});
|
|
78
88
|
program.parse(process.argv);
|
|
79
89
|
async function runSetup(options = {}) {
|
|
80
90
|
const isProject = options.project;
|
|
@@ -354,11 +364,9 @@ async function runSetup(options = {}) {
|
|
|
354
364
|
console.error(chalk.red(`Failed to write config: ${error.message}`));
|
|
355
365
|
}
|
|
356
366
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
const initialQuery = queryParts.join(' ');
|
|
367
|
+
// Shared by `chat` and `batch`: resolve credentials, endpoints and runtime
|
|
368
|
+
// flags from CLI args > env > project config > global config > provider preset.
|
|
369
|
+
async function resolveRuntime(options) {
|
|
362
370
|
// 1. Load Global JSON
|
|
363
371
|
const globalConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
|
364
372
|
// 2. Load Local JSON (Project Level)
|
|
@@ -439,6 +447,74 @@ async function runChat(queryParts, options) {
|
|
|
439
447
|
console.error(chalk.red("API Key is still missing. Exiting."));
|
|
440
448
|
process.exit(1);
|
|
441
449
|
}
|
|
450
|
+
return { apiKey, baseURL, model, fullConfig };
|
|
451
|
+
}
|
|
452
|
+
async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
|
|
453
|
+
const { apiKey, baseURL, model, fullConfig } = await resolveRuntime(globalOptions);
|
|
454
|
+
let raw;
|
|
455
|
+
try {
|
|
456
|
+
raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
console.error(chalk.red(`Cannot read manifest ${manifestPath}: ${err.message}`));
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
const entries = parseManifest(raw);
|
|
463
|
+
if (entries.length === 0) {
|
|
464
|
+
console.error(chalk.red(`Manifest ${manifestPath} contains no tasks (blank lines and '#' comments are skipped).`));
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
const outputPath = cmdOptions.output || manifestPath.replace(/\.[^.]+$/, '') + '.results.jsonl';
|
|
468
|
+
console.log(chalk.bold.cyan(`AutoClaw Batch 🦞 ${entries.length} task(s)`));
|
|
469
|
+
console.log(chalk.dim(`Results: ${outputPath}\n`));
|
|
470
|
+
const startedAt = Date.now();
|
|
471
|
+
const { results, completed, failed } = await runBatch(entries, async (entry) => {
|
|
472
|
+
// A fresh Agent per task keeps contexts isolated; per-task overrides
|
|
473
|
+
// beat the globally resolved defaults.
|
|
474
|
+
const taskPreset = resolveProvider(entry.provider);
|
|
475
|
+
const taskModel = entry.model || taskPreset?.defaultModel || model;
|
|
476
|
+
const taskBaseURL = taskPreset?.baseUrl || baseURL;
|
|
477
|
+
const taskConfig = {
|
|
478
|
+
...fullConfig,
|
|
479
|
+
// The results file is the machine-readable contract in batch mode.
|
|
480
|
+
jsonMode: false,
|
|
481
|
+
maxSteps: entry.maxSteps ?? fullConfig.maxSteps
|
|
482
|
+
};
|
|
483
|
+
const agent = new Agent(apiKey, taskBaseURL, taskModel, taskConfig);
|
|
484
|
+
const start = Date.now();
|
|
485
|
+
const runResult = await agent.chat(entry.task);
|
|
486
|
+
return {
|
|
487
|
+
id: entry.id,
|
|
488
|
+
status: runResult.status,
|
|
489
|
+
steps: runResult.steps,
|
|
490
|
+
message: runResult.message ?? null,
|
|
491
|
+
...(runResult.error ? { error: runResult.error } : {}),
|
|
492
|
+
...(runResult.usage ? { usage: runResult.usage } : {}),
|
|
493
|
+
durationMs: Date.now() - start
|
|
494
|
+
};
|
|
495
|
+
}, {
|
|
496
|
+
failFast: !!cmdOptions.failFast,
|
|
497
|
+
onResult: (entry, result, done, total) => {
|
|
498
|
+
const color = result.status === 'completed' ? chalk.green : chalk.red;
|
|
499
|
+
console.log(color(`[${done}/${total}] ${entry.id} -> ${result.status} (${Math.round(result.durationMs / 1000)}s)`));
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
fs.writeFileSync(outputPath, results.map(r => JSON.stringify(r)).join('\n') + '\n', 'utf-8');
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
console.error(chalk.red(`Failed to write results to ${outputPath}: ${err.message}`));
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
console.log(chalk.cyan(`\nBatch done: ${completed}/${results.length} completed, ${failed} failed in ${Math.round((Date.now() - startedAt) / 1000)}s -> ${outputPath}`));
|
|
510
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
511
|
+
}
|
|
512
|
+
async function runChat(queryParts, options) {
|
|
513
|
+
if (options.interactive) {
|
|
514
|
+
console.log(chalk.bold.cyan("Welcome to AutoClaw CLI 🦞"));
|
|
515
|
+
}
|
|
516
|
+
const initialQuery = queryParts.join(' ');
|
|
517
|
+
const { apiKey, baseURL, model, fullConfig } = await resolveRuntime(options);
|
|
442
518
|
const agent = new Agent(apiKey, baseURL, model, fullConfig);
|
|
443
519
|
if (options.interactive) {
|
|
444
520
|
console.log(chalk.green(`Agent initialized with model: ${model}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autoclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dev": "node --loader ts-node/esm src/index.ts",
|
|
13
13
|
"test": "vitest run",
|
|
14
14
|
"test:watch": "vitest",
|
|
15
|
+
"coverage": "vitest run --coverage",
|
|
15
16
|
"prepublishOnly": "npm run build"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"@types/jsdom": "^27.0.0",
|
|
66
67
|
"@types/node": "^25.2.1",
|
|
67
68
|
"@types/nodemailer": "^7.0.9",
|
|
69
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
68
70
|
"ts-node": "^10.9.2",
|
|
69
71
|
"typescript": "^5.9.3",
|
|
70
72
|
"vitest": "^4.1.4"
|