autoclaw 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/README.zh-CN.md +31 -1
- package/dist/agent.js +58 -6
- package/dist/batch.js +2 -0
- package/dist/doctor.js +81 -0
- package/dist/index.js +45 -6
- package/dist/providers.js +12 -0
- package/dist/tools/core.js +54 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,29 @@ Unattempted tasks are simply absent from the results file, so `--fail-fast` foll
|
|
|
130
130
|
|
|
131
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
132
|
|
|
133
|
+
### Recipes
|
|
134
|
+
|
|
135
|
+
Daily ops sweep on Linux (crontab):
|
|
136
|
+
```cron
|
|
137
|
+
0 9 * * * autoclaw batch /opt/ops/daily.jsonl -y -n --resume >> /var/log/autoclaw.log 2>&1
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Scheduled sweep on Windows (Task Scheduler):
|
|
141
|
+
```bash
|
|
142
|
+
schtasks /create /tn "AutoClaw Daily" /tr "autoclaw batch C:\ops\daily.jsonl -y -n" /sc daily /st 09:00
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Pipeline inside one manifest — each task writes files the next task reads:
|
|
146
|
+
```jsonl
|
|
147
|
+
{"id": "sweep", "task": "检查磁盘与关键服务状态,报告写入 report/sweep.md"}
|
|
148
|
+
{"id": "notify", "task": "读取 report/sweep.md,用三句话总结后推送到飞书"}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Diagnostics on a fresh machine or in CI:
|
|
152
|
+
```bash
|
|
153
|
+
autoclaw doctor # exit 0 = ready; exit 1 = what's missing is printed
|
|
154
|
+
```
|
|
155
|
+
|
|
133
156
|
### Auto-Confirm (CI/CD)
|
|
134
157
|
Automatically approve all tool executions (dangerous, use with caution or in sandboxes).
|
|
135
158
|
```bash
|
|
@@ -141,14 +164,18 @@ autoclaw "Refactor src/index.ts to use ES modules" -y
|
|
|
141
164
|
- `-P, --provider <name>`: Use a provider preset (see [Providers](#providers)).
|
|
142
165
|
- `-n, --no-interactive`: Exit after processing the initial query (Headless mode).
|
|
143
166
|
- `-y, --yes`: Auto-confirm all tool executions (e.g., shell commands).
|
|
167
|
+
- `--allow-dangerous`: Let `-y` run clearly destructive commands (rm -rf, format, shutdown, ...) that the built-in safety gate would block.
|
|
144
168
|
- `--json`: Emit NDJSON events on stdout (for orchestrators; use with `-n`).
|
|
145
169
|
|
|
170
|
+
### Diagnostics
|
|
171
|
+
`autoclaw doctor` checks everything headlessly and prints ✓/✗ per item: config files, resolved provider/baseUrl/model, API key, a live connection test, resolved shell, registered tools, and playwright browser status. Exit `0` = ready, `1` = a critical item failed (the failing item is printed). Ideal for CI or a fresh machine.
|
|
172
|
+
|
|
146
173
|
### Providers
|
|
147
174
|
AutoClaw works with any OpenAI-compatible endpoint. Built-in presets fill in the base URL and a default model for you:
|
|
148
175
|
```bash
|
|
149
176
|
autoclaw -P deepseek "Check disk usage and save a report" -y -n
|
|
150
177
|
```
|
|
151
|
-
Available presets: `openai`, `deepseek`, `moonshot` (Kimi), `dashscope` (Qwen), `zhipu` (GLM), `openrouter`, `ollama` (local). You can still override the model with `-m` or config. When `OPENAI_API_KEY` is not set, the API key is read from the provider's own env var (e.g. `DEEPSEEK_API_KEY`, `MOONSHOT_API_KEY`, `DASHSCOPE_API_KEY`, `ZHIPU_API_KEY`, `OPENROUTER_API_KEY`).
|
|
178
|
+
Available presets: `openai`, `deepseek`, `moonshot` (Kimi), `dashscope` (Qwen), `zhipu` (GLM), `ark` (Volcano Ark), `siliconflow`, `openrouter`, `ollama` (local). You can still override the model with `-m` or config. When `OPENAI_API_KEY` is not set, the API key is read from the provider's own env var (e.g. `DEEPSEEK_API_KEY`, `MOONSHOT_API_KEY`, `DASHSCOPE_API_KEY`, `ZHIPU_API_KEY`, `ARK_API_KEY`, `SILICONFLOW_API_KEY`, `OPENROUTER_API_KEY`).
|
|
152
179
|
|
|
153
180
|
## Configuration
|
|
154
181
|
|
|
@@ -167,6 +194,7 @@ AutoClaw uses a hierarchical configuration system.
|
|
|
167
194
|
- `model`: Default model to use.
|
|
168
195
|
- `maxSteps`: Max LLM turns per task before the agent stops (default: `25`).
|
|
169
196
|
- `shellTimeout`: Shell command timeout in milliseconds (default: `120000`).
|
|
197
|
+
- `taskTimeoutMs`: Whole-task wall-clock timeout in milliseconds (off by default; aborts in-flight API calls and stops with `timeout` status).
|
|
170
198
|
- `shell`: Force a shell for `execute_shell_command` (`bash`, `powershell`, `cmd`, `sh`; default: auto-detect — Git Bash > PowerShell > cmd on Windows).
|
|
171
199
|
- `tavilyApiKey`: API Key for Tavily Web Search.
|
|
172
200
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP Email settings.
|
|
@@ -187,6 +215,7 @@ Create a file at `.autoclaw/setting.json`:
|
|
|
187
215
|
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: main LLM settings.
|
|
188
216
|
- `AUTOCLOW_PROVIDER`: provider preset used when `-P` is not passed.
|
|
189
217
|
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: reliability limits (max LLM turns per task; shell timeout in ms).
|
|
218
|
+
- `AUTOCLOW_TASK_TIMEOUT_MS`: whole-task wall-clock timeout in ms.
|
|
190
219
|
- `AUTOCLOW_SHELL`: force the shell for shell commands (`bash`, `powershell`, `cmd`, `sh`).
|
|
191
220
|
- `AUTOCLOW_INCLUDE_USAGE`: set to `1`/`true` to request token usage from the API (opt-in).
|
|
192
221
|
- `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.
|
package/README.zh-CN.md
CHANGED
|
@@ -131,6 +131,29 @@ autoclaw batch big.jsonl -y -c 4 # 最多 4 个任务并行
|
|
|
131
131
|
|
|
132
132
|
AutoClaw 同时会自动给提示词瘦身:可选工具(网页搜索、邮件、群通知、图像生成)只在凭据配置后才会注册进工具定义;长循环中较早的工具结果会被替换为短摘要。
|
|
133
133
|
|
|
134
|
+
### 实战配方
|
|
135
|
+
|
|
136
|
+
Linux 定时巡检(crontab):
|
|
137
|
+
```cron
|
|
138
|
+
0 9 * * * autoclaw batch /opt/ops/daily.jsonl -y -n --resume >> /var/log/autoclaw.log 2>&1
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Windows 计划任务:
|
|
142
|
+
```bash
|
|
143
|
+
schtasks /create /tn "AutoClaw Daily" /tr "autoclaw batch C:\ops\daily.jsonl -y -n" /sc daily /st 09:00
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
一个清单内的流水线——前一个任务写文件,后一个任务读:
|
|
147
|
+
```jsonl
|
|
148
|
+
{"id": "sweep", "task": "检查磁盘与关键服务状态,报告写入 report/sweep.md"}
|
|
149
|
+
{"id": "notify", "task": "读取 report/sweep.md,用三句话总结后推送到飞书"}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
新机器或 CI 环境自检:
|
|
153
|
+
```bash
|
|
154
|
+
autoclaw doctor # 退出 0 = 就绪;退出 1 = 打印缺失项
|
|
155
|
+
```
|
|
156
|
+
|
|
134
157
|
### 自动确认 (CI/CD)
|
|
135
158
|
自动批准所有工具执行(危险操作,请谨慎使用或在沙箱环境下运行)。
|
|
136
159
|
```bash
|
|
@@ -142,6 +165,11 @@ autoclaw "将 src/index.ts 重构为使用 ES 模块" -y
|
|
|
142
165
|
- `-P, --provider <name>`: 使用 provider 预设 (见 [Provider 预设](#provider-预设))。
|
|
143
166
|
- `-n, --no-interactive`: 处理完初始查询后退出 (无头模式)。
|
|
144
167
|
- `-y, --yes`: 自动确认所有工具执行 (例如 Shell 命令)。
|
|
168
|
+
- `--allow-dangerous`: 允许 `-y` 直接执行内置安全闸拦截的明显破坏性命令 (rm -rf、format、shutdown 等)。
|
|
169
|
+
- `--json`: 在 stdout 输出 NDJSON 事件流 (供编排器使用,配合 `-n`)。
|
|
170
|
+
|
|
171
|
+
### 诊断
|
|
172
|
+
`autoclaw doctor` 无头完成全面自检,逐项打印 ✓/✗:配置文件、解析出的 provider/baseUrl/model、API key、真实连接测试、解析出的 shell、已注册工具、playwright 浏览器状态。退出 `0` = 就绪,`1` = 有关键项失败(会打印是哪项)。适合 CI 或新机器。
|
|
145
173
|
- `--json`: 在 stdout 输出 NDJSON 事件流 (供编排器使用,配合 `-n`)。
|
|
146
174
|
|
|
147
175
|
### Provider 预设
|
|
@@ -149,7 +177,7 @@ AutoClaw 可对接任意 OpenAI 兼容端点。内置预设可自动填好 Base
|
|
|
149
177
|
```bash
|
|
150
178
|
autoclaw -P deepseek "检查磁盘使用情况并保存报告" -y -n
|
|
151
179
|
```
|
|
152
|
-
可用预设:`openai`、`deepseek`、`moonshot` (Kimi)、`dashscope` (Qwen)、`zhipu` (GLM)、`openrouter`、`ollama` (本地)。模型仍可用 `-m` 或配置覆盖。未设置 `OPENAI_API_KEY` 时,会自动读取各家自己的环境变量 (如 `DEEPSEEK_API_KEY`、`MOONSHOT_API_KEY`、`DASHSCOPE_API_KEY`、`ZHIPU_API_KEY`、`OPENROUTER_API_KEY`)。
|
|
180
|
+
可用预设:`openai`、`deepseek`、`moonshot` (Kimi)、`dashscope` (Qwen)、`zhipu` (GLM)、`ark` (火山方舟)、`siliconflow` (硅基流动)、`openrouter`、`ollama` (本地)。模型仍可用 `-m` 或配置覆盖。未设置 `OPENAI_API_KEY` 时,会自动读取各家自己的环境变量 (如 `DEEPSEEK_API_KEY`、`MOONSHOT_API_KEY`、`DASHSCOPE_API_KEY`、`ZHIPU_API_KEY`、`ARK_API_KEY`、`SILICONFLOW_API_KEY`、`OPENROUTER_API_KEY`)。
|
|
153
181
|
|
|
154
182
|
## 配置
|
|
155
183
|
|
|
@@ -168,6 +196,7 @@ AutoClaw 使用层级配置系统。
|
|
|
168
196
|
- `model`: 默认使用的模型。
|
|
169
197
|
- `maxSteps`: 单任务最大 LLM 轮数,超出后自动停止 (默认: `25`)。
|
|
170
198
|
- `shellTimeout`: Shell 命令超时时间(毫秒)(默认: `120000`)。
|
|
199
|
+
- `taskTimeoutMs`: 单任务整体墙钟超时(毫秒,默认关闭;会中断进行中的 API 调用并以 `timeout` 状态停止)。
|
|
171
200
|
- `shell`: 强制 `execute_shell_command` 使用的 shell (`bash`、`powershell`、`cmd`、`sh`;默认自动检测——Windows 上优先 Git Bash > PowerShell > cmd)。
|
|
172
201
|
- `tavilyApiKey`: Tavily 网页搜索的 API 密钥。
|
|
173
202
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP 邮件设置。
|
|
@@ -188,6 +217,7 @@ AutoClaw 使用层级配置系统。
|
|
|
188
217
|
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: 主模型设置。
|
|
189
218
|
- `AUTOCLOW_PROVIDER`: 未传 `-P` 时使用的 provider 预设。
|
|
190
219
|
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: 稳定性限制(单任务最大轮数;Shell 超时毫秒数)。
|
|
220
|
+
- `AUTOCLOW_TASK_TIMEOUT_MS`: 单任务整体墙钟超时(毫秒)。
|
|
191
221
|
- `AUTOCLOW_SHELL`: 强制 shell 命令使用的 shell (`bash`、`powershell`、`cmd`、`sh`)。
|
|
192
222
|
- `AUTOCLOW_INCLUDE_USAGE`: 设为 `1`/`true` 时向 API 请求 token 用量(可选开启)。
|
|
193
223
|
- `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: 工具凭据,可作为 setup 的替代方式。
|
package/dist/agent.js
CHANGED
|
@@ -104,6 +104,13 @@ RULES OF ENGAGEMENT:
|
|
|
104
104
|
async chat(userInput) {
|
|
105
105
|
this.messages.push({ role: "user", content: userInput });
|
|
106
106
|
const maxSteps = Number(this.config?.maxSteps || process.env.AUTOCLOW_MAX_STEPS || DEFAULT_MAX_STEPS);
|
|
107
|
+
const taskTimeoutMs = Number(this.config?.taskTimeoutMs || process.env.AUTOCLOW_TASK_TIMEOUT_MS || 0);
|
|
108
|
+
const deadline = taskTimeoutMs > 0 ? Date.now() + taskTimeoutMs : Number.POSITIVE_INFINITY;
|
|
109
|
+
const abortController = new AbortController();
|
|
110
|
+
const abortTimer = taskTimeoutMs > 0
|
|
111
|
+
? setTimeout(() => abortController.abort(new Error(`task wall-clock timeout after ${taskTimeoutMs}ms`)), taskTimeoutMs)
|
|
112
|
+
: null;
|
|
113
|
+
const startedAt = Date.now();
|
|
107
114
|
let active = true;
|
|
108
115
|
let step = 0;
|
|
109
116
|
let status = 'completed';
|
|
@@ -120,6 +127,13 @@ RULES OF ENGAGEMENT:
|
|
|
120
127
|
}
|
|
121
128
|
break;
|
|
122
129
|
}
|
|
130
|
+
if (Date.now() > deadline) {
|
|
131
|
+
status = 'timeout';
|
|
132
|
+
if (!this.jsonMode) {
|
|
133
|
+
console.log(chalk.yellow(`\n[TaskTimeout] Wall-clock limit of ${taskTimeoutMs}ms reached; stopping.`));
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
123
137
|
this.trimOldToolResults();
|
|
124
138
|
step++;
|
|
125
139
|
const spinner = this.jsonMode
|
|
@@ -133,13 +147,13 @@ RULES OF ENGAGEMENT:
|
|
|
133
147
|
stream = await withRetry(async () => this.client.chat.completions.create({
|
|
134
148
|
model: this.model,
|
|
135
149
|
messages: this.messages,
|
|
136
|
-
tools: getToolDefinitions(),
|
|
150
|
+
tools: getToolDefinitions(this.config),
|
|
137
151
|
tool_choice: "auto",
|
|
138
152
|
stream: true,
|
|
139
153
|
// Not every OpenAI-compatible provider accepts stream_options;
|
|
140
154
|
// usage tracking is therefore opt-in only.
|
|
141
155
|
...(this.config?.includeUsage ? { stream_options: { include_usage: true } } : {})
|
|
142
|
-
}), {
|
|
156
|
+
}, { signal: abortController.signal }), {
|
|
143
157
|
onRetry: (err, nextAttempt, delayMs) => {
|
|
144
158
|
spinner.text = `API error (${err.message}); retrying in ${Math.round(delayMs / 1000)}s (attempt ${nextAttempt})...`;
|
|
145
159
|
}
|
|
@@ -149,8 +163,14 @@ RULES OF ENGAGEMENT:
|
|
|
149
163
|
spinner.fail('Error during processing');
|
|
150
164
|
if (!this.jsonMode)
|
|
151
165
|
console.error(chalk.red(error.message));
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
if (abortController.signal.aborted) {
|
|
167
|
+
status = 'timeout';
|
|
168
|
+
errorMessage = `task wall-clock timeout after ${taskTimeoutMs}ms`;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
status = 'error';
|
|
172
|
+
errorMessage = error.message;
|
|
173
|
+
}
|
|
154
174
|
active = false;
|
|
155
175
|
break;
|
|
156
176
|
}
|
|
@@ -231,8 +251,14 @@ RULES OF ENGAGEMENT:
|
|
|
231
251
|
spinner.fail('Error during processing');
|
|
232
252
|
if (!this.jsonMode)
|
|
233
253
|
console.error(chalk.red(error.message));
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
if (abortController.signal.aborted) {
|
|
255
|
+
status = 'timeout';
|
|
256
|
+
errorMessage = `task wall-clock timeout after ${taskTimeoutMs}ms`;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
status = 'error';
|
|
260
|
+
errorMessage = error.message;
|
|
261
|
+
}
|
|
236
262
|
active = false;
|
|
237
263
|
break;
|
|
238
264
|
}
|
|
@@ -351,6 +377,8 @@ RULES OF ENGAGEMENT:
|
|
|
351
377
|
this.emitEvent({ event: 'usage', step, ...totalUsage });
|
|
352
378
|
}
|
|
353
379
|
}
|
|
380
|
+
if (abortTimer)
|
|
381
|
+
clearTimeout(abortTimer);
|
|
354
382
|
const result = {
|
|
355
383
|
status,
|
|
356
384
|
steps: step,
|
|
@@ -358,9 +386,33 @@ RULES OF ENGAGEMENT:
|
|
|
358
386
|
...(errorMessage ? { error: errorMessage } : {}),
|
|
359
387
|
...(sawUsage ? { usage: totalUsage } : {})
|
|
360
388
|
};
|
|
389
|
+
this.appendRunLog(userInput, result, startedAt);
|
|
361
390
|
this.emitEvent({ event: 'run_end', ...result });
|
|
362
391
|
return result;
|
|
363
392
|
}
|
|
393
|
+
// Best-effort local run history: ~/.autoclaw/logs/runs.jsonl, one line
|
|
394
|
+
// per run, for post-hoc debugging of unattended batches. Logging must
|
|
395
|
+
// never fail a run.
|
|
396
|
+
appendRunLog(userInput, result, startedAt) {
|
|
397
|
+
try {
|
|
398
|
+
const dir = path.join(os.homedir(), '.autoclaw', 'logs');
|
|
399
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
400
|
+
const line = JSON.stringify({
|
|
401
|
+
time: new Date().toISOString(),
|
|
402
|
+
model: this.model,
|
|
403
|
+
task: String(userInput).slice(0, 200),
|
|
404
|
+
status: result.status,
|
|
405
|
+
steps: result.steps,
|
|
406
|
+
...(result.error ? { error: result.error.slice(0, 300) } : {}),
|
|
407
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
408
|
+
durationMs: Date.now() - startedAt
|
|
409
|
+
});
|
|
410
|
+
fs.appendFileSync(path.join(dir, 'runs.jsonl'), line + '\n');
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// ignore
|
|
414
|
+
}
|
|
415
|
+
}
|
|
364
416
|
// Every turn resends the full history, so early large tool results
|
|
365
417
|
// dominate context growth. Keep the most recent results intact and bound
|
|
366
418
|
// older ones to a short excerpt (full output stays on disk via /view when
|
package/dist/batch.js
CHANGED
|
@@ -25,6 +25,8 @@ export function parseManifest(raw) {
|
|
|
25
25
|
const entry = { lineNo, id, task: parsed.task.trim() };
|
|
26
26
|
if (typeof parsed.maxSteps === 'number' && parsed.maxSteps > 0)
|
|
27
27
|
entry.maxSteps = parsed.maxSteps;
|
|
28
|
+
if (typeof parsed.taskTimeoutMs === 'number' && parsed.taskTimeoutMs > 0)
|
|
29
|
+
entry.taskTimeoutMs = parsed.taskTimeoutMs;
|
|
28
30
|
if (typeof parsed.model === 'string' && parsed.model.trim())
|
|
29
31
|
entry.model = parsed.model.trim();
|
|
30
32
|
if (typeof parsed.provider === 'string' && parsed.provider.trim())
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { testConnection } from './setup.js';
|
|
4
|
+
import { getBashPath, resolveShellType } from './shell.js';
|
|
5
|
+
import { getToolDefinitions, listUnavailableTools } from './tools/index.js';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
function playwrightBrowserStatus() {
|
|
8
|
+
try {
|
|
9
|
+
const { chromium } = require('playwright');
|
|
10
|
+
const exe = chromium.executablePath();
|
|
11
|
+
if (exe && fs.existsSync(exe)) {
|
|
12
|
+
return { ok: true, detail: exe };
|
|
13
|
+
}
|
|
14
|
+
return { ok: false, detail: 'playwright installed but no browser downloaded (run: npx playwright install chromium)' };
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return { ok: false, detail: `playwright not available (${String(err?.message ?? err).split('\n')[0]})` };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function maskKey(key) {
|
|
21
|
+
if (!key)
|
|
22
|
+
return 'missing';
|
|
23
|
+
if (key.length < 8)
|
|
24
|
+
return '***';
|
|
25
|
+
return `${key.slice(0, 6)}***${key.slice(-4)}`;
|
|
26
|
+
}
|
|
27
|
+
// Headless self-diagnosis: every check reports ok/critical/detail, so the
|
|
28
|
+
// CLI can render ✓/✗ and derive an exit code without any interaction.
|
|
29
|
+
export async function collectDoctorChecks(cfg) {
|
|
30
|
+
const checks = [];
|
|
31
|
+
checks.push({
|
|
32
|
+
name: 'Global config',
|
|
33
|
+
ok: cfg.globalExists,
|
|
34
|
+
critical: true,
|
|
35
|
+
detail: cfg.globalExists ? cfg.globalFile : `${cfg.globalFile} (missing — run: autoclaw setup)`
|
|
36
|
+
});
|
|
37
|
+
checks.push({
|
|
38
|
+
name: 'Project config',
|
|
39
|
+
ok: true,
|
|
40
|
+
critical: false,
|
|
41
|
+
detail: cfg.projectExists ? cfg.projectFile : '(none)'
|
|
42
|
+
});
|
|
43
|
+
checks.push({
|
|
44
|
+
name: 'API key',
|
|
45
|
+
ok: !!cfg.apiKey,
|
|
46
|
+
critical: true,
|
|
47
|
+
detail: maskKey(cfg.apiKey)
|
|
48
|
+
});
|
|
49
|
+
checks.push({
|
|
50
|
+
name: 'Endpoint',
|
|
51
|
+
ok: !!cfg.baseUrl,
|
|
52
|
+
critical: true,
|
|
53
|
+
detail: `${cfg.providerLabel} | ${cfg.baseUrl || '?'} | model: ${cfg.model}`
|
|
54
|
+
});
|
|
55
|
+
if (cfg.apiKey && cfg.baseUrl && cfg.model) {
|
|
56
|
+
const t = await testConnection(cfg.baseUrl, cfg.apiKey, cfg.model);
|
|
57
|
+
checks.push({ name: 'Connection', ok: t.ok, critical: true, detail: t.message });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
checks.push({ name: 'Connection', ok: false, critical: true, detail: 'skipped (key / baseUrl / model missing)' });
|
|
61
|
+
}
|
|
62
|
+
const shellType = resolveShellType(cfg.toolConfig);
|
|
63
|
+
const bashPath = shellType === 'bash' ? getBashPath() : null;
|
|
64
|
+
checks.push({
|
|
65
|
+
name: 'Shell',
|
|
66
|
+
ok: true,
|
|
67
|
+
critical: false,
|
|
68
|
+
detail: bashPath ? `${shellType} (${bashPath})` : shellType
|
|
69
|
+
});
|
|
70
|
+
const registered = getToolDefinitions(cfg.toolConfig).length;
|
|
71
|
+
const missing = listUnavailableTools(cfg.toolConfig);
|
|
72
|
+
checks.push({
|
|
73
|
+
name: 'Tools',
|
|
74
|
+
ok: true,
|
|
75
|
+
critical: false,
|
|
76
|
+
detail: `${registered} registered${missing.length ? ` (not configured: ${missing.join(', ')})` : ''}`
|
|
77
|
+
});
|
|
78
|
+
const pw = playwrightBrowserStatus();
|
|
79
|
+
checks.push({ name: 'Playwright browsers', ok: pw.ok, critical: false, detail: pw.detail });
|
|
80
|
+
return checks;
|
|
81
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Agent } from './agent.js';
|
|
|
7
7
|
import { parseManifest, runBatch } from './batch.js';
|
|
8
8
|
import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
|
|
9
9
|
import { fetchModelIds, normalizeBaseUrl, testConnection } from './setup.js';
|
|
10
|
+
import { collectDoctorChecks } from './doctor.js';
|
|
10
11
|
import * as fs from 'fs';
|
|
11
12
|
import * as path from 'path';
|
|
12
13
|
import * as os from 'os';
|
|
@@ -45,7 +46,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
45
46
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
47
|
// In dist/index.js, package.json is usually up one level in the root
|
|
47
48
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
48
|
-
let version = '1.3.
|
|
49
|
+
let version = '1.3.4';
|
|
49
50
|
try {
|
|
50
51
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
51
52
|
version = pkg.version;
|
|
@@ -59,9 +60,10 @@ program
|
|
|
59
60
|
.description('A lightweight AI agent CLI tool')
|
|
60
61
|
.version(version)
|
|
61
62
|
.option('-m, --model <model>', 'Model to use')
|
|
62
|
-
.option('-P, --provider <name>', 'Use a provider preset (openai, deepseek, moonshot, dashscope, zhipu, openrouter, ollama)')
|
|
63
|
+
.option('-P, --provider <name>', 'Use a provider preset (openai, deepseek, moonshot, dashscope, zhipu, ark, siliconflow, openrouter, ollama)')
|
|
63
64
|
.option('-n, --no-interactive', 'Exit after processing the initial query (Headless mode)')
|
|
64
65
|
.option('-y, --yes', 'Auto-confirm all tool executions (e.g., shell commands)')
|
|
66
|
+
.option('--allow-dangerous', 'Let -y run clearly destructive commands (rm -rf, format, shutdown, ...) without the safety block')
|
|
65
67
|
.option('--json', 'Emit NDJSON events on stdout (for orchestrators; use with -n)');
|
|
66
68
|
program
|
|
67
69
|
.command('setup')
|
|
@@ -88,6 +90,36 @@ program
|
|
|
88
90
|
const options = program.opts();
|
|
89
91
|
await runBatchCommand(manifest, options, cmdOptions);
|
|
90
92
|
});
|
|
93
|
+
program
|
|
94
|
+
.command('doctor')
|
|
95
|
+
.description('Diagnose configuration and environment (headless)')
|
|
96
|
+
.action(async () => {
|
|
97
|
+
const options = program.opts();
|
|
98
|
+
const { apiKey, baseURL, model, fullConfig } = await resolveRuntime(options, { interactive: false });
|
|
99
|
+
const providerName = options.provider || process.env.AUTOCLOW_PROVIDER || fullConfig.provider;
|
|
100
|
+
console.log(chalk.bold.cyan('AutoClaw Doctor 🦞\n'));
|
|
101
|
+
const checks = await collectDoctorChecks({
|
|
102
|
+
apiKey,
|
|
103
|
+
baseUrl: baseURL,
|
|
104
|
+
model,
|
|
105
|
+
providerLabel: providerName || 'custom',
|
|
106
|
+
globalFile: GLOBAL_CONFIG_FILE,
|
|
107
|
+
projectFile: LOCAL_CONFIG_FILE,
|
|
108
|
+
globalExists: fs.existsSync(GLOBAL_CONFIG_FILE),
|
|
109
|
+
projectExists: fs.existsSync(LOCAL_CONFIG_FILE),
|
|
110
|
+
toolConfig: fullConfig
|
|
111
|
+
});
|
|
112
|
+
for (const c of checks) {
|
|
113
|
+
const mark = c.ok ? chalk.green('✓') : c.critical ? chalk.red('✗') : chalk.yellow('!');
|
|
114
|
+
console.log(`${mark} ${c.name.padEnd(20)} ${chalk.dim(c.detail)}`);
|
|
115
|
+
}
|
|
116
|
+
const failed = checks.filter(c => c.critical && !c.ok);
|
|
117
|
+
if (failed.length === 0)
|
|
118
|
+
console.log(chalk.green('\nAll critical checks passed.'));
|
|
119
|
+
else
|
|
120
|
+
console.log(chalk.red(`\n${failed.length} critical check(s) failed.`));
|
|
121
|
+
process.exit(failed.length === 0 ? 0 : 1);
|
|
122
|
+
});
|
|
91
123
|
program.parse(process.argv);
|
|
92
124
|
async function runSetup(options = {}) {
|
|
93
125
|
const isProject = options.project;
|
|
@@ -438,7 +470,9 @@ async function runSetup(options = {}) {
|
|
|
438
470
|
}
|
|
439
471
|
// Shared by `chat` and `batch`: resolve credentials, endpoints and runtime
|
|
440
472
|
// flags from CLI args > env > project config > global config > provider preset.
|
|
441
|
-
|
|
473
|
+
// With interactive: false (doctor), a missing key resolves to '' instead of
|
|
474
|
+
// prompting, so the caller can report it.
|
|
475
|
+
async function resolveRuntime(options, opts = {}) {
|
|
442
476
|
// 1. Load Global JSON
|
|
443
477
|
const globalConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
|
444
478
|
// 2. Load Local JSON (Project Level)
|
|
@@ -461,6 +495,7 @@ async function resolveRuntime(options) {
|
|
|
461
495
|
let model = options.model || process.env.OPENAI_MODEL || fullConfig.model || preset?.defaultModel || 'gpt-5.6';
|
|
462
496
|
// Inject Runtime Flags
|
|
463
497
|
fullConfig.autoConfirm = options.yes;
|
|
498
|
+
fullConfig.allowDangerous = !!options.allowDangerous;
|
|
464
499
|
fullConfig.jsonMode = !!options.json;
|
|
465
500
|
// Usage tracking is opt-in: not every OpenAI-compatible provider accepts
|
|
466
501
|
// stream_options.include_usage, so never force it on.
|
|
@@ -492,6 +527,9 @@ async function resolveRuntime(options) {
|
|
|
492
527
|
if (process.env.WECOM_KEYWORD)
|
|
493
528
|
fullConfig.wecomKeyword = process.env.WECOM_KEYWORD;
|
|
494
529
|
if (!apiKey) {
|
|
530
|
+
if (opts.interactive === false) {
|
|
531
|
+
return { apiKey: '', baseURL, model, fullConfig };
|
|
532
|
+
}
|
|
495
533
|
console.log(chalk.yellow("API Key not found."));
|
|
496
534
|
const { doSetup } = await inquirer.prompt([
|
|
497
535
|
{
|
|
@@ -577,7 +615,8 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
|
|
|
577
615
|
...fullConfig,
|
|
578
616
|
// The results file is the machine-readable contract in batch mode.
|
|
579
617
|
jsonMode: false,
|
|
580
|
-
maxSteps: entry.maxSteps ?? fullConfig.maxSteps
|
|
618
|
+
maxSteps: entry.maxSteps ?? fullConfig.maxSteps,
|
|
619
|
+
taskTimeoutMs: entry.taskTimeoutMs ?? fullConfig.taskTimeoutMs
|
|
581
620
|
};
|
|
582
621
|
const agent = new Agent(apiKey, taskBaseURL, taskModel, taskConfig);
|
|
583
622
|
const start = Date.now();
|
|
@@ -629,9 +668,9 @@ async function runChat(queryParts, options) {
|
|
|
629
668
|
}
|
|
630
669
|
const result = await agent.chat(initialQuery);
|
|
631
670
|
// Headless mode exit — the exit code is the orchestrator-facing outcome:
|
|
632
|
-
// 0 completed, 1 hard failure, 2 step cap
|
|
671
|
+
// 0 completed, 1 hard failure, 2 unfinished (step cap or wall-clock timeout).
|
|
633
672
|
if (!options.interactive) {
|
|
634
|
-
process.exit(result.status === 'completed' ? 0 : result.status === 'max_steps' ? 2 : 1);
|
|
673
|
+
process.exit(result.status === 'completed' ? 0 : result.status === 'max_steps' || result.status === 'timeout' ? 2 : 1);
|
|
635
674
|
}
|
|
636
675
|
}
|
|
637
676
|
// Main chat loop
|
package/dist/providers.js
CHANGED
|
@@ -29,6 +29,18 @@ export const PROVIDER_PRESETS = {
|
|
|
29
29
|
defaultModel: 'glm-5',
|
|
30
30
|
apiKeyEnv: 'ZHIPU_API_KEY'
|
|
31
31
|
},
|
|
32
|
+
ark: {
|
|
33
|
+
label: 'Volcano Ark (Doubao/DeepSeek)',
|
|
34
|
+
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
35
|
+
defaultModel: 'deepseek-v4-flash',
|
|
36
|
+
apiKeyEnv: 'ARK_API_KEY'
|
|
37
|
+
},
|
|
38
|
+
siliconflow: {
|
|
39
|
+
label: 'SiliconFlow',
|
|
40
|
+
baseUrl: 'https://api.siliconflow.cn/v1',
|
|
41
|
+
defaultModel: 'deepseek-ai/DeepSeek-V4',
|
|
42
|
+
apiKeyEnv: 'SILICONFLOW_API_KEY'
|
|
43
|
+
},
|
|
32
44
|
openrouter: {
|
|
33
45
|
label: 'OpenRouter',
|
|
34
46
|
baseUrl: 'https://openrouter.ai/api/v1',
|
package/dist/tools/core.js
CHANGED
|
@@ -3,11 +3,49 @@ import * as path from 'path';
|
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { execShellCommand } from '../shell.js';
|
|
6
|
+
import * as os from 'os';
|
|
6
7
|
const DEFAULT_SHELL_TIMEOUT_MS = 120000;
|
|
7
8
|
const SHELL_MAX_BUFFER = 10 * 1024 * 1024;
|
|
8
9
|
// Bounded reads keep a huge file from exhausting memory or the model context;
|
|
9
10
|
// the agent-level truncation in truncate.ts applies on top of this.
|
|
10
11
|
const READ_FILE_MAX_BYTES = 1024 * 1024;
|
|
12
|
+
// Even with -y, an unattended agent must not execute clearly destructive
|
|
13
|
+
// commands — a poisoned tool result (e.g. prompt injection via a web page)
|
|
14
|
+
// would otherwise reach the shell directly. Matched commands are refused
|
|
15
|
+
// with a message the model can act on; --allow-dangerous overrides.
|
|
16
|
+
export const DANGEROUS_PATTERNS = [
|
|
17
|
+
{ re: /\brm\s+(?:-\w+\s+)*-\w*[rR]\w*[fF]\b/, label: 'rm with recursive+force' },
|
|
18
|
+
{ re: /\b(?:rd|rmdir|del|erase)\s+\/[sS]\b/, label: 'Windows recursive delete (rd/del /s)' },
|
|
19
|
+
{ re: /\bRemove-Item\b[^;\n]*-(?:Recurse[^;\n]*Force|Force[^;\n]*Recurse)/i, label: 'PowerShell Remove-Item -Recurse -Force' },
|
|
20
|
+
{ re: /\b(?:format|diskpart)\b/i, label: 'disk format / diskpart' },
|
|
21
|
+
{ re: /\bmkfs(?:\.\w+)?\b/i, label: 'mkfs' },
|
|
22
|
+
{ re: /\bdd\b[^|]*\bof=/i, label: 'dd raw write' },
|
|
23
|
+
{ re: /(?:>>?|tee)\s*\/dev\/(?:sd|nvme|hd|vd)[a-z]/i, label: 'write to block device' },
|
|
24
|
+
{ re: /\b(?:shutdown|reboot|halt|poweroff)\b/i, label: 'host power control' },
|
|
25
|
+
{ re: /\breg\s+delete\b/i, label: 'registry delete' }
|
|
26
|
+
];
|
|
27
|
+
export function matchDangerousPattern(command) {
|
|
28
|
+
const hit = DANGEROUS_PATTERNS.find(p => p.re.test(command));
|
|
29
|
+
return hit ? hit.label : null;
|
|
30
|
+
}
|
|
31
|
+
// An unattended agent reads web pages and repos; a prompt injection must not
|
|
32
|
+
// end up reading AutoClaw's own credential stores (or classic .env files)
|
|
33
|
+
// and exfiltrating them. Blocked for read_file/write_file unless
|
|
34
|
+
// --allow-dangerous. execute_shell_command is not pattern-restricted here.
|
|
35
|
+
export function isSensitivePath(p) {
|
|
36
|
+
const norm = path.normalize(String(p)).toLowerCase();
|
|
37
|
+
const protectedFiles = [
|
|
38
|
+
path.join(os.homedir(), '.autoclaw', 'setting.json').toLowerCase(),
|
|
39
|
+
path.join(os.homedir(), '.autoclaw', '.env').toLowerCase(),
|
|
40
|
+
path.join(process.cwd(), '.autoclaw', 'setting.json').toLowerCase()
|
|
41
|
+
];
|
|
42
|
+
if (protectedFiles.includes(norm))
|
|
43
|
+
return true;
|
|
44
|
+
return path.basename(norm).startsWith('.env');
|
|
45
|
+
}
|
|
46
|
+
function sensitivePathBlock(p, verb) {
|
|
47
|
+
return `Error: ${p} is blocked by AutoClaw safety policy (credential/secret store). It was NOT ${verb === 'read' ? 'read' : 'written'}. If this task genuinely requires it, restart AutoClaw with --allow-dangerous.`;
|
|
48
|
+
}
|
|
11
49
|
export const ShellTool = {
|
|
12
50
|
name: "Shell Execution",
|
|
13
51
|
definition: {
|
|
@@ -28,6 +66,14 @@ export const ShellTool = {
|
|
|
28
66
|
handler: async (args, config) => {
|
|
29
67
|
console.log(chalk.yellow(`\nAI wants to execute: `) + chalk.bold(args.command));
|
|
30
68
|
console.log(chalk.dim(`Reason: ${args.rationale}`));
|
|
69
|
+
// Safety gate runs first: blocked commands are refused even with --yes.
|
|
70
|
+
if (!config?.allowDangerous) {
|
|
71
|
+
const label = matchDangerousPattern(args.command);
|
|
72
|
+
if (label) {
|
|
73
|
+
console.log(chalk.red(`\n[blocked] ${label}`));
|
|
74
|
+
return `Error: command blocked by AutoClaw safety policy (matched: ${label}). It was NOT executed. If this task genuinely requires it, the user must restart AutoClaw with --allow-dangerous; otherwise find a safer way to achieve the same goal.`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
31
77
|
const timeoutMs = Number(config?.shellTimeout || process.env.AUTOCLOW_SHELL_TIMEOUT || DEFAULT_SHELL_TIMEOUT_MS);
|
|
32
78
|
// Check for auto-confirm flag
|
|
33
79
|
if (!config?.autoConfirm) {
|
|
@@ -80,7 +126,10 @@ export const ReadFileTool = {
|
|
|
80
126
|
}
|
|
81
127
|
}
|
|
82
128
|
},
|
|
83
|
-
handler: async (args) => {
|
|
129
|
+
handler: async (args, config) => {
|
|
130
|
+
if (!config?.allowDangerous && isSensitivePath(args.path)) {
|
|
131
|
+
return sensitivePathBlock(args.path, 'read');
|
|
132
|
+
}
|
|
84
133
|
let fh;
|
|
85
134
|
try {
|
|
86
135
|
fh = await fs.open(args.path, 'r');
|
|
@@ -124,7 +173,10 @@ export const WriteFileTool = {
|
|
|
124
173
|
}
|
|
125
174
|
}
|
|
126
175
|
},
|
|
127
|
-
handler: async (args) => {
|
|
176
|
+
handler: async (args, config) => {
|
|
177
|
+
if (!config?.allowDangerous && isSensitivePath(args.path)) {
|
|
178
|
+
return sensitivePathBlock(args.path, 'write');
|
|
179
|
+
}
|
|
128
180
|
try {
|
|
129
181
|
await fs.mkdir(path.dirname(args.path), { recursive: true });
|
|
130
182
|
await fs.writeFile(args.path, args.content, 'utf-8');
|