autoclaw 1.3.1 → 1.3.3
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 +29 -2
- package/README.zh-CN.md +30 -2
- package/dist/doctor.js +81 -0
- package/dist/index.js +142 -34
- package/dist/providers.js +12 -0
- package/dist/setup.js +61 -0
- package/dist/tools/core.js +48 -1
- package/dist/tools/email.js +3 -0
- package/dist/tools/image.js +1 -1
- package/dist/tools/notify.js +2 -1
- package/dist/tools/search.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ npm install -g autoclaw
|
|
|
78
78
|
|
|
79
79
|
## Quick Start
|
|
80
80
|
|
|
81
|
-
1. **Setup**: Run the interactive setup wizard to configure your API keys and integrations.
|
|
81
|
+
1. **Setup**: Run the interactive setup wizard to configure your API keys and integrations. The wizard runs a live connection test (failures map to the likely wrong field: 401 = key, 404 = base URL, 400 = model name) and can list the provider's models for you to pick from.
|
|
82
82
|
```bash
|
|
83
83
|
autoclaw setup
|
|
84
84
|
```
|
|
@@ -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
|
|
package/README.zh-CN.md
CHANGED
|
@@ -79,7 +79,7 @@ npm install -g autoclaw
|
|
|
79
79
|
|
|
80
80
|
## 快速上手
|
|
81
81
|
|
|
82
|
-
1. **配置**: 运行交互式设置向导以配置您的 API
|
|
82
|
+
1. **配置**: 运行交互式设置向导以配置您的 API 密钥和集成插件。向导会现场做连接测试(失败映射到最可能出错的字段:401=key 错、404=URL 错、400=模型名错),并能拉取服务商的模型列表供你直接选择。
|
|
83
83
|
```bash
|
|
84
84
|
autoclaw setup
|
|
85
85
|
```
|
|
@@ -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
|
|
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
|
@@ -6,6 +6,8 @@ import dotenv from 'dotenv';
|
|
|
6
6
|
import { Agent } from './agent.js';
|
|
7
7
|
import { parseManifest, runBatch } from './batch.js';
|
|
8
8
|
import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
|
|
9
|
+
import { fetchModelIds, normalizeBaseUrl, testConnection } from './setup.js';
|
|
10
|
+
import { collectDoctorChecks } from './doctor.js';
|
|
9
11
|
import * as fs from 'fs';
|
|
10
12
|
import * as path from 'path';
|
|
11
13
|
import * as os from 'os';
|
|
@@ -44,7 +46,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
44
46
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
47
|
// In dist/index.js, package.json is usually up one level in the root
|
|
46
48
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
47
|
-
let version = '1.3.
|
|
49
|
+
let version = '1.3.3';
|
|
48
50
|
try {
|
|
49
51
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
50
52
|
version = pkg.version;
|
|
@@ -58,9 +60,10 @@ program
|
|
|
58
60
|
.description('A lightweight AI agent CLI tool')
|
|
59
61
|
.version(version)
|
|
60
62
|
.option('-m, --model <model>', 'Model to use')
|
|
61
|
-
.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)')
|
|
62
64
|
.option('-n, --no-interactive', 'Exit after processing the initial query (Headless mode)')
|
|
63
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')
|
|
64
67
|
.option('--json', 'Emit NDJSON events on stdout (for orchestrators; use with -n)');
|
|
65
68
|
program
|
|
66
69
|
.command('setup')
|
|
@@ -87,6 +90,36 @@ program
|
|
|
87
90
|
const options = program.opts();
|
|
88
91
|
await runBatchCommand(manifest, options, cmdOptions);
|
|
89
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
|
+
});
|
|
90
123
|
program.parse(process.argv);
|
|
91
124
|
async function runSetup(options = {}) {
|
|
92
125
|
const isProject = options.project;
|
|
@@ -109,7 +142,7 @@ async function runSetup(options = {}) {
|
|
|
109
142
|
}
|
|
110
143
|
const providerAnswer = await inquirer.prompt([
|
|
111
144
|
{
|
|
112
|
-
type: '
|
|
145
|
+
type: 'select',
|
|
113
146
|
name: 'provider',
|
|
114
147
|
message: 'Select your LLM provider:',
|
|
115
148
|
choices: [
|
|
@@ -121,34 +154,101 @@ async function runSetup(options = {}) {
|
|
|
121
154
|
]);
|
|
122
155
|
const provider = providerAnswer.provider;
|
|
123
156
|
const preset = resolveProvider(provider === 'custom' ? undefined : provider);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
157
|
+
// The connection part (key / Base URL / model) is asked in a loop with a
|
|
158
|
+
// live test, so typos never make it into the saved config.
|
|
159
|
+
const askConnection = async () => {
|
|
160
|
+
const defaults = {
|
|
161
|
+
apiKey: currentConfig.apiKey,
|
|
162
|
+
baseUrl: currentConfig.baseUrl || preset?.baseUrl || 'https://api.openai.com/v1',
|
|
163
|
+
model: currentConfig.model || preset?.defaultModel || 'gpt-5.6'
|
|
164
|
+
};
|
|
165
|
+
const core = await inquirer.prompt([
|
|
166
|
+
{
|
|
167
|
+
type: 'password',
|
|
168
|
+
name: 'apiKey',
|
|
169
|
+
message: defaults.apiKey
|
|
170
|
+
? `Enter API Key (Leave empty to keep ${maskSecret(defaults.apiKey)}):`
|
|
171
|
+
: `Enter API Key${preset?.apiKeyEnv ? ` (or set ${preset.apiKeyEnv} in your environment)` : ''}:`,
|
|
172
|
+
mask: '*',
|
|
173
|
+
validate: (input) => {
|
|
174
|
+
if (input.length > 0)
|
|
175
|
+
return true;
|
|
176
|
+
if (defaults.apiKey)
|
|
177
|
+
return true;
|
|
178
|
+
return 'API Key cannot be empty.';
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
type: 'input',
|
|
183
|
+
name: 'baseUrl',
|
|
184
|
+
message: 'Enter API Base URL:',
|
|
185
|
+
default: defaults.baseUrl
|
|
138
186
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
187
|
+
]);
|
|
188
|
+
const apiKey = core.apiKey || defaults.apiKey || '';
|
|
189
|
+
const baseUrl = normalizeBaseUrl(core.baseUrl || defaults.baseUrl);
|
|
190
|
+
// Prefer the provider's own catalog over guessing model names.
|
|
191
|
+
console.log(chalk.dim('Fetching available models...'));
|
|
192
|
+
const ids = await fetchModelIds(baseUrl, apiKey);
|
|
193
|
+
let model;
|
|
194
|
+
if (ids) {
|
|
195
|
+
console.log(chalk.dim(`Found ${ids.length} models.`));
|
|
196
|
+
const picked = await inquirer.prompt([
|
|
197
|
+
{
|
|
198
|
+
type: 'select',
|
|
199
|
+
name: 'model',
|
|
200
|
+
message: 'Select default Model:',
|
|
201
|
+
choices: [{ name: '✎ Enter manually', value: '__manual__' }, ...ids.map(id => ({ name: id, value: id }))],
|
|
202
|
+
default: ids.includes(defaults.model) ? defaults.model : undefined,
|
|
203
|
+
pageSize: 12
|
|
204
|
+
}
|
|
205
|
+
]);
|
|
206
|
+
if (picked.model === '__manual__') {
|
|
207
|
+
const manual = await inquirer.prompt([
|
|
208
|
+
{ type: 'input', name: 'model', message: 'Enter default Model:', default: defaults.model }
|
|
209
|
+
]);
|
|
210
|
+
model = manual.model;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
model = picked.model;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const manual = await inquirer.prompt([
|
|
218
|
+
{ type: 'input', name: 'model', message: 'Enter default Model (catalog unavailable):', default: defaults.model }
|
|
219
|
+
]);
|
|
220
|
+
model = manual.model;
|
|
221
|
+
}
|
|
222
|
+
return { apiKey, baseUrl, model };
|
|
223
|
+
};
|
|
224
|
+
let connection = await askConnection();
|
|
225
|
+
console.log(chalk.dim('Running connection test (sends one tiny prompt — normal provider billing applies)...'));
|
|
226
|
+
let test = await testConnection(connection.baseUrl, connection.apiKey, connection.model);
|
|
227
|
+
while (!test.ok) {
|
|
228
|
+
console.log(chalk.red(`\n✗ ${test.message}`));
|
|
229
|
+
const next = await inquirer.prompt([
|
|
230
|
+
{
|
|
231
|
+
type: 'select',
|
|
232
|
+
name: 'action',
|
|
233
|
+
message: 'Connection test failed. What next?',
|
|
234
|
+
choices: [
|
|
235
|
+
{ name: 'Re-enter API key / Base URL / model', value: 'edit' },
|
|
236
|
+
{ name: 'Test again', value: 'retry' },
|
|
237
|
+
{ name: 'Save anyway (e.g. the provider is temporarily down)', value: 'save' }
|
|
238
|
+
],
|
|
239
|
+
default: 'edit'
|
|
240
|
+
}
|
|
241
|
+
]);
|
|
242
|
+
if (next.action === 'save')
|
|
243
|
+
break;
|
|
244
|
+
if (next.action === 'edit')
|
|
245
|
+
connection = await askConnection();
|
|
246
|
+
console.log(chalk.dim('Running connection test...'));
|
|
247
|
+
test = await testConnection(connection.baseUrl, connection.apiKey, connection.model);
|
|
248
|
+
}
|
|
249
|
+
if (test.ok)
|
|
250
|
+
console.log(chalk.green('✓ Connection test passed.'));
|
|
251
|
+
const answers = await inquirer.prompt([
|
|
152
252
|
{
|
|
153
253
|
type: 'confirm',
|
|
154
254
|
name: 'configureImage',
|
|
@@ -183,7 +283,7 @@ async function runSetup(options = {}) {
|
|
|
183
283
|
}
|
|
184
284
|
]);
|
|
185
285
|
// Resolve sensitive values (Keep old if empty)
|
|
186
|
-
const finalApiKey =
|
|
286
|
+
const finalApiKey = connection.apiKey || currentConfig.apiKey;
|
|
187
287
|
let imageConfig = {
|
|
188
288
|
imageApiKey: currentConfig.imageApiKey,
|
|
189
289
|
imageBaseUrl: currentConfig.imageBaseUrl,
|
|
@@ -346,8 +446,8 @@ async function runSetup(options = {}) {
|
|
|
346
446
|
}
|
|
347
447
|
const newConfig = {
|
|
348
448
|
apiKey: finalApiKey,
|
|
349
|
-
baseUrl:
|
|
350
|
-
model:
|
|
449
|
+
baseUrl: connection.baseUrl,
|
|
450
|
+
model: connection.model,
|
|
351
451
|
provider: provider === 'custom' ? undefined : provider,
|
|
352
452
|
...imageConfig,
|
|
353
453
|
...emailConfig,
|
|
@@ -360,6 +460,8 @@ async function runSetup(options = {}) {
|
|
|
360
460
|
}
|
|
361
461
|
fs.writeFileSync(targetFile, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
|
|
362
462
|
console.log(chalk.green(`\n✅ Configuration saved to ${targetFile}`));
|
|
463
|
+
console.log(chalk.dim(` provider: ${provider === 'custom' ? 'custom' : provider} | baseUrl: ${connection.baseUrl} | model: ${connection.model}`));
|
|
464
|
+
console.log(chalk.dim(` connection test: ${test.ok ? 'passed ✓' : 'skipped (saved without a passing test)'}`));
|
|
363
465
|
console.log(chalk.cyan("You can now run 'autoclaw' to start using the agent."));
|
|
364
466
|
}
|
|
365
467
|
catch (error) {
|
|
@@ -368,7 +470,9 @@ async function runSetup(options = {}) {
|
|
|
368
470
|
}
|
|
369
471
|
// Shared by `chat` and `batch`: resolve credentials, endpoints and runtime
|
|
370
472
|
// flags from CLI args > env > project config > global config > provider preset.
|
|
371
|
-
|
|
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 = {}) {
|
|
372
476
|
// 1. Load Global JSON
|
|
373
477
|
const globalConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
|
374
478
|
// 2. Load Local JSON (Project Level)
|
|
@@ -391,6 +495,7 @@ async function resolveRuntime(options) {
|
|
|
391
495
|
let model = options.model || process.env.OPENAI_MODEL || fullConfig.model || preset?.defaultModel || 'gpt-5.6';
|
|
392
496
|
// Inject Runtime Flags
|
|
393
497
|
fullConfig.autoConfirm = options.yes;
|
|
498
|
+
fullConfig.allowDangerous = !!options.allowDangerous;
|
|
394
499
|
fullConfig.jsonMode = !!options.json;
|
|
395
500
|
// Usage tracking is opt-in: not every OpenAI-compatible provider accepts
|
|
396
501
|
// stream_options.include_usage, so never force it on.
|
|
@@ -422,6 +527,9 @@ async function resolveRuntime(options) {
|
|
|
422
527
|
if (process.env.WECOM_KEYWORD)
|
|
423
528
|
fullConfig.wecomKeyword = process.env.WECOM_KEYWORD;
|
|
424
529
|
if (!apiKey) {
|
|
530
|
+
if (opts.interactive === false) {
|
|
531
|
+
return { apiKey: '', baseURL, model, fullConfig };
|
|
532
|
+
}
|
|
425
533
|
console.log(chalk.yellow("API Key not found."));
|
|
426
534
|
const { doSetup } = await inquirer.prompt([
|
|
427
535
|
{
|
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/setup.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Pure helpers behind the setup wizard: URL normalization, a live
|
|
2
|
+
// connection test with actionable error mapping, and provider model
|
|
3
|
+
// catalog fetching. Kept free of inquirer so they are unit-testable.
|
|
4
|
+
export function normalizeBaseUrl(url) {
|
|
5
|
+
let u = String(url ?? '').trim();
|
|
6
|
+
if (u && !/^https?:\/\//i.test(u))
|
|
7
|
+
u = `https://${u}`;
|
|
8
|
+
return u.replace(/\/+$/, '');
|
|
9
|
+
}
|
|
10
|
+
// Sends one tiny real prompt through the exact endpoint the user
|
|
11
|
+
// configured, and maps failures to the field that is most likely wrong.
|
|
12
|
+
export async function testConnection(baseUrl, apiKey, model) {
|
|
13
|
+
const url = `${normalizeBaseUrl(baseUrl)}/chat/completions`;
|
|
14
|
+
let resp;
|
|
15
|
+
try {
|
|
16
|
+
resp = await fetch(url, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
19
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: 'ping' }], stream: false }),
|
|
20
|
+
signal: AbortSignal.timeout(15000)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return { ok: false, kind: 'network', message: `Cannot reach ${url} (${err?.message ?? err}). Check the Base URL and your network.` };
|
|
25
|
+
}
|
|
26
|
+
if (resp.ok) {
|
|
27
|
+
return { ok: true, kind: 'ok', message: `Connection OK — ${model} responded.` };
|
|
28
|
+
}
|
|
29
|
+
const detail = (await resp.text().catch(() => '')).slice(0, 300);
|
|
30
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
31
|
+
return { ok: false, kind: 'auth', message: `API key rejected (${resp.status}). Double-check the key. ${detail}` };
|
|
32
|
+
}
|
|
33
|
+
if (resp.status === 404) {
|
|
34
|
+
return { ok: false, kind: 'not-found', message: `Endpoint not found (404). The Base URL is likely wrong. ${detail}` };
|
|
35
|
+
}
|
|
36
|
+
if (resp.status === 400) {
|
|
37
|
+
return { ok: false, kind: 'model', message: `Request rejected (400). The model name "${model}" is likely wrong for this endpoint. ${detail}` };
|
|
38
|
+
}
|
|
39
|
+
return { ok: false, kind: 'server', message: `Provider returned ${resp.status}. ${detail}` };
|
|
40
|
+
}
|
|
41
|
+
// Returns the provider's model IDs (sorted) or null when the endpoint does
|
|
42
|
+
// not offer a catalog — the wizard then falls back to manual entry.
|
|
43
|
+
export async function fetchModelIds(baseUrl, apiKey) {
|
|
44
|
+
try {
|
|
45
|
+
const resp = await fetch(`${normalizeBaseUrl(baseUrl)}/models`, {
|
|
46
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
47
|
+
signal: AbortSignal.timeout(15000)
|
|
48
|
+
});
|
|
49
|
+
if (!resp.ok)
|
|
50
|
+
return null;
|
|
51
|
+
const data = await resp.json();
|
|
52
|
+
const ids = (Array.isArray(data?.data) ? data.data : [])
|
|
53
|
+
.map((m) => m?.id)
|
|
54
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
55
|
+
ids.sort();
|
|
56
|
+
return ids.length > 0 ? ids : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/tools/core.js
CHANGED
|
@@ -5,6 +5,28 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { execShellCommand } from '../shell.js';
|
|
6
6
|
const DEFAULT_SHELL_TIMEOUT_MS = 120000;
|
|
7
7
|
const SHELL_MAX_BUFFER = 10 * 1024 * 1024;
|
|
8
|
+
// Bounded reads keep a huge file from exhausting memory or the model context;
|
|
9
|
+
// the agent-level truncation in truncate.ts applies on top of this.
|
|
10
|
+
const READ_FILE_MAX_BYTES = 1024 * 1024;
|
|
11
|
+
// Even with -y, an unattended agent must not execute clearly destructive
|
|
12
|
+
// commands — a poisoned tool result (e.g. prompt injection via a web page)
|
|
13
|
+
// would otherwise reach the shell directly. Matched commands are refused
|
|
14
|
+
// with a message the model can act on; --allow-dangerous overrides.
|
|
15
|
+
export const DANGEROUS_PATTERNS = [
|
|
16
|
+
{ re: /\brm\s+(?:-\w+\s+)*-\w*[rR]\w*[fF]\b/, label: 'rm with recursive+force' },
|
|
17
|
+
{ re: /\b(?:rd|rmdir|del|erase)\s+\/[sS]\b/, label: 'Windows recursive delete (rd/del /s)' },
|
|
18
|
+
{ re: /\bRemove-Item\b[^;\n]*-(?:Recurse[^;\n]*Force|Force[^;\n]*Recurse)/i, label: 'PowerShell Remove-Item -Recurse -Force' },
|
|
19
|
+
{ re: /\b(?:format|diskpart)\b/i, label: 'disk format / diskpart' },
|
|
20
|
+
{ re: /\bmkfs(?:\.\w+)?\b/i, label: 'mkfs' },
|
|
21
|
+
{ re: /\bdd\b[^|]*\bof=/i, label: 'dd raw write' },
|
|
22
|
+
{ re: /(?:>>?|tee)\s*\/dev\/(?:sd|nvme|hd|vd)[a-z]/i, label: 'write to block device' },
|
|
23
|
+
{ re: /\b(?:shutdown|reboot|halt|poweroff)\b/i, label: 'host power control' },
|
|
24
|
+
{ re: /\breg\s+delete\b/i, label: 'registry delete' }
|
|
25
|
+
];
|
|
26
|
+
export function matchDangerousPattern(command) {
|
|
27
|
+
const hit = DANGEROUS_PATTERNS.find(p => p.re.test(command));
|
|
28
|
+
return hit ? hit.label : null;
|
|
29
|
+
}
|
|
8
30
|
export const ShellTool = {
|
|
9
31
|
name: "Shell Execution",
|
|
10
32
|
definition: {
|
|
@@ -25,6 +47,14 @@ export const ShellTool = {
|
|
|
25
47
|
handler: async (args, config) => {
|
|
26
48
|
console.log(chalk.yellow(`\nAI wants to execute: `) + chalk.bold(args.command));
|
|
27
49
|
console.log(chalk.dim(`Reason: ${args.rationale}`));
|
|
50
|
+
// Safety gate runs first: blocked commands are refused even with --yes.
|
|
51
|
+
if (!config?.allowDangerous) {
|
|
52
|
+
const label = matchDangerousPattern(args.command);
|
|
53
|
+
if (label) {
|
|
54
|
+
console.log(chalk.red(`\n[blocked] ${label}`));
|
|
55
|
+
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.`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
28
58
|
const timeoutMs = Number(config?.shellTimeout || process.env.AUTOCLOW_SHELL_TIMEOUT || DEFAULT_SHELL_TIMEOUT_MS);
|
|
29
59
|
// Check for auto-confirm flag
|
|
30
60
|
if (!config?.autoConfirm) {
|
|
@@ -78,13 +108,30 @@ export const ReadFileTool = {
|
|
|
78
108
|
}
|
|
79
109
|
},
|
|
80
110
|
handler: async (args) => {
|
|
111
|
+
let fh;
|
|
81
112
|
try {
|
|
82
|
-
|
|
113
|
+
fh = await fs.open(args.path, 'r');
|
|
114
|
+
const buf = Buffer.alloc(READ_FILE_MAX_BYTES);
|
|
115
|
+
const { bytesRead } = await fh.read(buf, 0, READ_FILE_MAX_BYTES, 0);
|
|
116
|
+
const slice = buf.subarray(0, bytesRead);
|
|
117
|
+
// NUL bytes are the reliable tell for binary content; returning them
|
|
118
|
+
// as "utf-8" would only hand the model mojibake.
|
|
119
|
+
if (slice.includes(0)) {
|
|
120
|
+
return `Error: ${args.path} looks like a binary file (${bytesRead} bytes read). Inspect it with execute_shell_command instead (e.g. strings, xxd, file).`;
|
|
121
|
+
}
|
|
122
|
+
const content = slice.toString('utf-8');
|
|
123
|
+
if (bytesRead === READ_FILE_MAX_BYTES) {
|
|
124
|
+
const { size } = await fh.stat();
|
|
125
|
+
return `${content}\n[AutoClaw] File truncated at ${READ_FILE_MAX_BYTES} bytes (file is ${size} bytes). Use execute_shell_command to read specific ranges.`;
|
|
126
|
+
}
|
|
83
127
|
return content;
|
|
84
128
|
}
|
|
85
129
|
catch (error) {
|
|
86
130
|
return `Error reading file: ${error.message}`;
|
|
87
131
|
}
|
|
132
|
+
finally {
|
|
133
|
+
await fh?.close();
|
|
134
|
+
}
|
|
88
135
|
}
|
|
89
136
|
};
|
|
90
137
|
export const WriteFileTool = {
|
package/dist/tools/email.js
CHANGED
|
@@ -34,6 +34,9 @@ export const EmailTool = {
|
|
|
34
34
|
host: config.smtpHost,
|
|
35
35
|
port: parseInt(config.smtpPort || '587'),
|
|
36
36
|
secure: parseInt(config.smtpPort) === 465, // true for 465, false for other ports
|
|
37
|
+
connectionTimeout: 30000,
|
|
38
|
+
greetingTimeout: 30000,
|
|
39
|
+
socketTimeout: 120000,
|
|
37
40
|
auth: {
|
|
38
41
|
user: config.smtpUser,
|
|
39
42
|
pass: config.smtpPass,
|
package/dist/tools/image.js
CHANGED
|
@@ -64,7 +64,7 @@ const toolDefinition = {
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
async function downloadImage(url, destPath) {
|
|
67
|
-
const response = await fetch(url);
|
|
67
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(120000) });
|
|
68
68
|
if (!response.ok)
|
|
69
69
|
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
70
70
|
const buffer = await response.arrayBuffer();
|
package/dist/tools/notify.js
CHANGED
|
@@ -80,7 +80,8 @@ export const NotifyTool = {
|
|
|
80
80
|
const response = await fetch(webhookUrl, {
|
|
81
81
|
method: "POST",
|
|
82
82
|
headers: { "Content-Type": "application/json" },
|
|
83
|
-
body: JSON.stringify(payload)
|
|
83
|
+
body: JSON.stringify(payload),
|
|
84
|
+
signal: AbortSignal.timeout(30000)
|
|
84
85
|
});
|
|
85
86
|
const result = await response.json();
|
|
86
87
|
// Platform specific success checks
|
package/dist/tools/search.js
CHANGED