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 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.2.0';
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
- async function runChat(queryParts, options) {
358
- if (options.interactive) {
359
- console.log(chalk.bold.cyan("Welcome to AutoClaw CLI 🦞"));
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.2.0",
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"