autoclaw 1.1.1 → 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 +56 -6
- package/README.zh-CN.md +57 -7
- package/dist/agent.js +234 -87
- package/dist/batch.js +64 -0
- package/dist/index.js +129 -20
- package/dist/providers.js +51 -0
- package/dist/retry.js +42 -0
- package/dist/tools/core.js +15 -1
- package/dist/tools/prompt-optimizer.js +1 -1
- package/dist/truncate.js +32 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -30,11 +30,14 @@ Unlike "screen-seeing" agents (such as OpenClaw) that rely on visual interpretat
|
|
|
30
30
|
|
|
31
31
|
## Features
|
|
32
32
|
|
|
33
|
-
- 📜 **Headless Execution**: No
|
|
33
|
+
- 📜 **Headless Execution**: No GUI required — pure terminal efficiency. Core operation is shell + file I/O; the optional web tools run in headless Chromium.
|
|
34
34
|
- 🤖 **Non-Interactive Mode**: Intelligent flag handling (`-y`, `--no-interactive`) for zero-touch automation.
|
|
35
35
|
- 📂 **Universal Control**: From simple file I/O to complex system administration.
|
|
36
|
-
-
|
|
36
|
+
- 🛡️ **Runaway Protection**: Max-step cap per task, API retries with exponential backoff, shell command timeouts, and tool output truncation to keep the model context bounded.
|
|
37
|
+
- 🧠 **Context Aware**: Provides accurate OS, system and time context so relative dates ("today", "next Monday") are handled correctly.
|
|
37
38
|
- 🌐 **Web Search**: Integrated with Tavily for real-time information retrieval.
|
|
39
|
+
- 🌍 **Web Reading & Screenshots**: Extract article content and capture page screenshots (requires `npx playwright install chromium`).
|
|
40
|
+
- 🎨 **Image Generation**: DALL-E compatible image generation via any OpenAI-compatible images API.
|
|
38
41
|
- 🕒 **Time Accuracy**: Built-in tool to get precise system date and time for correct temporal context.
|
|
39
42
|
- 📧 **Communication**: Send emails and push notifications to chat groups automatically.
|
|
40
43
|
|
|
@@ -43,7 +46,8 @@ Unlike "screen-seeing" agents (such as OpenClaw) that rely on visual interpretat
|
|
|
43
46
|
- **Language**: TypeScript
|
|
44
47
|
- **Framework**: Commander.js
|
|
45
48
|
- **UI**: Inquirer (interactivity), Chalk (styling), Ora (spinners)
|
|
46
|
-
- **AI**: OpenAI SDK (
|
|
49
|
+
- **AI**: OpenAI SDK (any OpenAI-compatible endpoint: DeepSeek, Kimi, Qwen, GLM, Ollama, …)
|
|
50
|
+
- **Web tools**: Playwright (headless Chromium for `read_website` / `take_screenshot`)
|
|
47
51
|
|
|
48
52
|
## Installation
|
|
49
53
|
|
|
@@ -91,12 +95,31 @@ Simply run `autoclaw` to enter the chat loop.
|
|
|
91
95
|
autoclaw
|
|
92
96
|
> List all TypeScript files in the src folder.
|
|
93
97
|
```
|
|
98
|
+
Interactive commands: `exit` / `quit` to leave, and `/view` to open the full output of the last tool result in a pager — tool output longer than 20 lines is folded on screen and saved to `~/.autoclaw/output/`.
|
|
94
99
|
|
|
95
100
|
### Headless Mode (One-Shot)
|
|
96
101
|
Run a single command and exit.
|
|
97
102
|
```bash
|
|
98
103
|
autoclaw "Check disk usage and save the report to usage.txt" --no-interactive
|
|
99
104
|
```
|
|
105
|
+
The exit code reports the outcome for orchestrators: `0` completed, `1` hard failure (e.g. API error), `2` step cap reached (task unfinished).
|
|
106
|
+
|
|
107
|
+
### Machine-Readable Output (--json)
|
|
108
|
+
Add `--json` to print one JSON event per line on stdout (run_start, tool_call, tool_result, usage, run_end); human output moves to stderr, including anything tools print themselves.
|
|
109
|
+
```bash
|
|
110
|
+
autoclaw "Deploy and report" -y -n --json
|
|
111
|
+
```
|
|
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
|
+
|
|
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.
|
|
100
123
|
|
|
101
124
|
### Auto-Confirm (CI/CD)
|
|
102
125
|
Automatically approve all tool executions (dangerous, use with caution or in sandboxes).
|
|
@@ -105,24 +128,36 @@ autoclaw "Refactor src/index.ts to use ES modules" -y
|
|
|
105
128
|
```
|
|
106
129
|
|
|
107
130
|
### CLI Options
|
|
108
|
-
- `-m, --model <model>`: Specify the LLM model (default: `gpt-
|
|
131
|
+
- `-m, --model <model>`: Specify the LLM model (default: `gpt-5.6`).
|
|
132
|
+
- `-P, --provider <name>`: Use a provider preset (see [Providers](#providers)).
|
|
109
133
|
- `-n, --no-interactive`: Exit after processing the initial query (Headless mode).
|
|
110
134
|
- `-y, --yes`: Auto-confirm all tool executions (e.g., shell commands).
|
|
135
|
+
- `--json`: Emit NDJSON events on stdout (for orchestrators; use with `-n`).
|
|
136
|
+
|
|
137
|
+
### Providers
|
|
138
|
+
AutoClaw works with any OpenAI-compatible endpoint. Built-in presets fill in the base URL and a default model for you:
|
|
139
|
+
```bash
|
|
140
|
+
autoclaw -P deepseek "Check disk usage and save a report" -y -n
|
|
141
|
+
```
|
|
142
|
+
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`).
|
|
111
143
|
|
|
112
144
|
## Configuration
|
|
113
145
|
|
|
114
146
|
AutoClaw uses a hierarchical configuration system.
|
|
115
147
|
|
|
116
148
|
**Priority Order (Highest to Lowest):**
|
|
117
|
-
1. **CLI Arguments**: (e.g., `-m gpt-
|
|
149
|
+
1. **CLI Arguments**: (e.g., `-m gpt-5.6`)
|
|
118
150
|
2. **Environment Variables**: (`OPENAI_API_KEY`, `.env` file)
|
|
119
151
|
3. **Project Config**: (`./.autoclaw/setting.json` in current directory)
|
|
120
152
|
4. **Global Config**: (`~/.autoclaw/setting.json`)
|
|
121
153
|
|
|
122
154
|
### Supported Configuration Keys (JSON)
|
|
155
|
+
- `provider`: Provider preset name (e.g. `deepseek`).
|
|
123
156
|
- `apiKey`: Your OpenAI API Key.
|
|
124
157
|
- `baseUrl`: Custom Base URL (e.g., for DeepSeek or LocalLLM).
|
|
125
158
|
- `model`: Default model to use.
|
|
159
|
+
- `maxSteps`: Max LLM turns per task before the agent stops (default: `25`).
|
|
160
|
+
- `shellTimeout`: Shell command timeout in milliseconds (default: `120000`).
|
|
126
161
|
- `tavilyApiKey`: API Key for Tavily Web Search.
|
|
127
162
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP Email settings.
|
|
128
163
|
- `feishuWebhook`, `dingtalkWebhook`, `wecomWebhook`: Notification webhooks.
|
|
@@ -131,13 +166,20 @@ AutoClaw uses a hierarchical configuration system.
|
|
|
131
166
|
Create a file at `.autoclaw/setting.json`:
|
|
132
167
|
```json
|
|
133
168
|
{
|
|
134
|
-
"model": "gpt-
|
|
169
|
+
"model": "gpt-5.6",
|
|
135
170
|
"baseUrl": "https://api.deepseek.com/v1"
|
|
136
171
|
}
|
|
137
172
|
```
|
|
138
173
|
|
|
139
174
|
> **⚠️ Security Warning**: If you store your `apiKey` or secrets in `.autoclaw/setting.json`, make sure to add `.autoclaw/` to your `.gitignore` file to prevent leaking secrets!
|
|
140
175
|
|
|
176
|
+
### Environment Variables
|
|
177
|
+
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: main LLM settings.
|
|
178
|
+
- `AUTOCLOW_PROVIDER`: provider preset used when `-P` is not passed.
|
|
179
|
+
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: reliability limits (max LLM turns per task; shell timeout in ms).
|
|
180
|
+
- `AUTOCLOW_INCLUDE_USAGE`: set to `1`/`true` to request token usage from the API (opt-in).
|
|
181
|
+
- `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: tool credentials as an alternative to setup.
|
|
182
|
+
|
|
141
183
|
## Integrations
|
|
142
184
|
|
|
143
185
|
### Web Search (Tavily)
|
|
@@ -158,6 +200,14 @@ Built-in utility to provide the agent with the current system time, ensuring acc
|
|
|
158
200
|
|
|
159
201
|
## Docker Support
|
|
160
202
|
|
|
203
|
+
### Build & Run
|
|
204
|
+
The repository ships a multi-stage `Dockerfile` (node:22-alpine, browser downloads skipped to keep the image slim). The container runs headless one-shot tasks against the mounted directory:
|
|
205
|
+
```bash
|
|
206
|
+
docker build -t autoclaw .
|
|
207
|
+
docker run --rm -v "$PWD":/workspace -w /workspace -e OPENAI_API_KEY=sk-... autoclaw "Check disk usage and save a report" -y -n
|
|
208
|
+
```
|
|
209
|
+
Note: browser-based tools (`read_website` / `take_screenshot`) are not functional in the default image since browsers are not bundled — they return a friendly install hint instead.
|
|
210
|
+
|
|
161
211
|
### Chinese Font Issues in Screenshots
|
|
162
212
|
When running AutoClaw inside a Docker container (especially Alpine or Debian Slim), screenshots of Chinese websites may display text as square boxes ("tofu") due to missing fonts. Emojis (e.g., 🔥) may also appear as squares.
|
|
163
213
|
|
package/README.zh-CN.md
CHANGED
|
@@ -31,11 +31,14 @@ AutoClaw 是一款针对 **“无界面系统” (Headless Systems)** 的高稳
|
|
|
31
31
|
|
|
32
32
|
## 特性
|
|
33
33
|
|
|
34
|
-
- 📜 **无头执行 (Headless Execution)**:
|
|
34
|
+
- 📜 **无头执行 (Headless Execution)**: 无需图形界面,纯终端运行。核心流程仅依赖 Shell 与文件操作;可选的网页工具在无头 Chromium 中运行。
|
|
35
35
|
- 🤖 **非交互模式**: 支持自动化标志(`-y`, `--no-interactive`),完美适配零干预的自动化流程。
|
|
36
|
-
- 📂 **全方位控制 (Universal Control)**: 从基础的文件 I/O
|
|
37
|
-
-
|
|
36
|
+
- 📂 **全方位控制 (Universal Control)**: 从基础的文件 I/O 到复杂的系统管理。
|
|
37
|
+
- 🛡️ **防失控保护**: 单任务步数上限、API 指数退避重试、Shell 命令超时、工具输出截断,保证模型上下文不被撑爆。
|
|
38
|
+
- 🧠 **上下文感知 (Context Aware)**: 提供精确的操作系统与时间上下文,正确处理"今天"、"下周一"等相对时间。
|
|
38
39
|
- 🌐 **网页搜索**: 集成 Tavily,支持实时信息检索。
|
|
40
|
+
- 🌍 **网页阅读与截图**: 提取文章正文、截取页面图片(需先执行 `npx playwright install chromium`)。
|
|
41
|
+
- 🎨 **图像生成**: 通过任意 OpenAI 兼容的图像接口生成图片(兼容 DALL-E)。
|
|
39
42
|
- 🕒 **时间精准**: 内置工具获取精确系统日期和时间,确保正确的时间上下文。
|
|
40
43
|
- 📧 **通讯能力**: 自动发送电子邮件并将通知推送至聊天群组。
|
|
41
44
|
|
|
@@ -44,7 +47,8 @@ AutoClaw 是一款针对 **“无界面系统” (Headless Systems)** 的高稳
|
|
|
44
47
|
- **语言**: TypeScript
|
|
45
48
|
- **框架**: Commander.js
|
|
46
49
|
- **UI**: Inquirer (交互), Chalk (样式), Ora (加载动画)
|
|
47
|
-
- **AI**: OpenAI SDK
|
|
50
|
+
- **AI**: OpenAI SDK(任意 OpenAI 兼容端点:DeepSeek、Kimi、Qwen、GLM、Ollama 等)
|
|
51
|
+
- **网页工具**: Playwright(无头 Chromium,用于 `read_website` / `take_screenshot`)
|
|
48
52
|
|
|
49
53
|
## 安装
|
|
50
54
|
|
|
@@ -92,12 +96,31 @@ npm install -g autoclaw
|
|
|
92
96
|
autoclaw
|
|
93
97
|
> 列出 src 文件夹中所有的 TypeScript 文件。
|
|
94
98
|
```
|
|
99
|
+
交互命令:`exit` / `quit` 退出;`/view` 用分页器查看上一次工具的完整输出——超过 20 行的工具输出会在屏幕上折叠,并保存到 `~/.autoclaw/output/`。
|
|
95
100
|
|
|
96
101
|
### 无头模式 (一次性任务)
|
|
97
102
|
执行单个指令后立即退出。
|
|
98
103
|
```bash
|
|
99
104
|
autoclaw "检查磁盘使用情况并将报告保存到 usage.txt" --no-interactive
|
|
100
105
|
```
|
|
106
|
+
退出码向编排器报告结果:`0` 完成,`1` 硬性失败(如 API 错误),`2` 触发步数上限(任务未完成)。
|
|
107
|
+
|
|
108
|
+
### 机器可读输出 (--json)
|
|
109
|
+
加 `--json` 后,stdout 每行输出一个 JSON 事件(run_start、tool_call、tool_result、usage、run_end);人类可读输出(包括工具自己打印的内容)全部转到 stderr。
|
|
110
|
+
```bash
|
|
111
|
+
autoclaw "执行部署并汇报" -y -n --json
|
|
112
|
+
```
|
|
113
|
+
Token 用量仅在设置 `AUTOCLOW_INCLUDE_USAGE=1`(或 `true`)时才收集——这是可选项,因为并非所有 OpenAI 兼容端点都接受 `stream_options.include_usage`。
|
|
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`。
|
|
101
124
|
|
|
102
125
|
### 自动确认 (CI/CD)
|
|
103
126
|
自动批准所有工具执行(危险操作,请谨慎使用或在沙箱环境下运行)。
|
|
@@ -106,24 +129,36 @@ autoclaw "将 src/index.ts 重构为使用 ES 模块" -y
|
|
|
106
129
|
```
|
|
107
130
|
|
|
108
131
|
### CLI 选项
|
|
109
|
-
- `-m, --model <model>`: 指定 LLM 模型 (默认: `gpt-
|
|
132
|
+
- `-m, --model <model>`: 指定 LLM 模型 (默认: `gpt-5.6`)。
|
|
133
|
+
- `-P, --provider <name>`: 使用 provider 预设 (见 [Provider 预设](#provider-预设))。
|
|
110
134
|
- `-n, --no-interactive`: 处理完初始查询后退出 (无头模式)。
|
|
111
135
|
- `-y, --yes`: 自动确认所有工具执行 (例如 Shell 命令)。
|
|
136
|
+
- `--json`: 在 stdout 输出 NDJSON 事件流 (供编排器使用,配合 `-n`)。
|
|
137
|
+
|
|
138
|
+
### Provider 预设
|
|
139
|
+
AutoClaw 可对接任意 OpenAI 兼容端点。内置预设可自动填好 Base URL 和默认模型:
|
|
140
|
+
```bash
|
|
141
|
+
autoclaw -P deepseek "检查磁盘使用情况并保存报告" -y -n
|
|
142
|
+
```
|
|
143
|
+
可用预设:`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`)。
|
|
112
144
|
|
|
113
145
|
## 配置
|
|
114
146
|
|
|
115
147
|
AutoClaw 使用层级配置系统。
|
|
116
148
|
|
|
117
149
|
**优先级排序 (从高到低):**
|
|
118
|
-
1. **CLI 参数**: (例如 `-m gpt-
|
|
150
|
+
1. **CLI 参数**: (例如 `-m gpt-5.6`)
|
|
119
151
|
2. **环境变量**: (`OPENAI_API_KEY`, `.env` 文件)
|
|
120
152
|
3. **项目配置**: (当前目录下的 `./.autoclaw/setting.json`)
|
|
121
153
|
4. **全局配置**: (`~/.autoclaw/setting.json`)
|
|
122
154
|
|
|
123
155
|
### 支持的配置键 (JSON)
|
|
156
|
+
- `provider`: Provider 预设名 (如 `deepseek`)。
|
|
124
157
|
- `apiKey`: 您的 OpenAI API 密钥。
|
|
125
158
|
- `baseUrl`: 自定义 API 基础地址 (例如 DeepSeek 或本地 LLM)。
|
|
126
159
|
- `model`: 默认使用的模型。
|
|
160
|
+
- `maxSteps`: 单任务最大 LLM 轮数,超出后自动停止 (默认: `25`)。
|
|
161
|
+
- `shellTimeout`: Shell 命令超时时间(毫秒)(默认: `120000`)。
|
|
127
162
|
- `tavilyApiKey`: Tavily 网页搜索的 API 密钥。
|
|
128
163
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP 邮件设置。
|
|
129
164
|
- `feishuWebhook`, `dingtalkWebhook`, `wecomWebhook`: 通知钩子地址。
|
|
@@ -132,13 +167,20 @@ AutoClaw 使用层级配置系统。
|
|
|
132
167
|
在 `.autoclaw/setting.json` 创建文件:
|
|
133
168
|
```json
|
|
134
169
|
{
|
|
135
|
-
"model": "gpt-
|
|
170
|
+
"model": "gpt-5.6",
|
|
136
171
|
"baseUrl": "https://api.deepseek.com/v1"
|
|
137
172
|
}
|
|
138
173
|
```
|
|
139
174
|
|
|
140
175
|
> **⚠️ 安全警告**: 如果您在 `.autoclaw/setting.json` 中存储了 `apiKey` 或机密信息,请务必将 `.autoclaw/` 添加到您的 `.gitignore` 文件中,以防泄露!
|
|
141
176
|
|
|
177
|
+
### 环境变量
|
|
178
|
+
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: 主模型设置。
|
|
179
|
+
- `AUTOCLOW_PROVIDER`: 未传 `-P` 时使用的 provider 预设。
|
|
180
|
+
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: 稳定性限制(单任务最大轮数;Shell 超时毫秒数)。
|
|
181
|
+
- `AUTOCLOW_INCLUDE_USAGE`: 设为 `1`/`true` 时向 API 请求 token 用量(可选开启)。
|
|
182
|
+
- `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: 工具凭据,可作为 setup 的替代方式。
|
|
183
|
+
|
|
142
184
|
## 集成功能
|
|
143
185
|
|
|
144
186
|
### 网页搜索 (Tavily)
|
|
@@ -159,6 +201,14 @@ AutoClaw 使用层级配置系统。
|
|
|
159
201
|
|
|
160
202
|
## Docker 支持
|
|
161
203
|
|
|
204
|
+
### 构建与运行
|
|
205
|
+
仓库自带多阶段构建的 `Dockerfile`(node:22-alpine,跳过浏览器下载保持镜像苗条)。容器内直接运行无头一次性任务,配合目录挂载操作当前目录:
|
|
206
|
+
```bash
|
|
207
|
+
docker build -t autoclaw .
|
|
208
|
+
docker run --rm -v "$PWD":/workspace -w /workspace -e OPENAI_API_KEY=sk-... autoclaw "检查磁盘使用情况并保存报告" -y -n
|
|
209
|
+
```
|
|
210
|
+
注意:默认镜像中未内置浏览器,基于浏览器的工具(`read_website` / `take_screenshot`)不可用——它们会返回友好的安装提示,而不是报错崩溃。
|
|
211
|
+
|
|
162
212
|
### 截图中的中文显示问题
|
|
163
213
|
在 Docker 容器(尤其是 Alpine 或 Debian Slim)中运行时,网页截图中的中文可能会显示为方块("豆腐块")。表情符号(如 🔥)也可能显示为方块。
|
|
164
214
|
|
package/dist/agent.js
CHANGED
|
@@ -4,14 +4,26 @@ import ora from 'ora';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
+
import * as util from 'util';
|
|
7
8
|
import { getToolDefinitions, executeToolHandler } from './tools/index.js';
|
|
9
|
+
import { withRetry } from './retry.js';
|
|
10
|
+
import { truncateOutput } from './truncate.js';
|
|
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
|
+
}
|
|
8
20
|
export class Agent {
|
|
9
21
|
client;
|
|
10
22
|
messages;
|
|
11
23
|
model;
|
|
12
24
|
config;
|
|
13
25
|
lastOutputFile = null;
|
|
14
|
-
constructor(apiKey, baseURL, model = 'gpt-
|
|
26
|
+
constructor(apiKey, baseURL, model = 'gpt-5.6', config = {}) {
|
|
15
27
|
this.client = new OpenAI({
|
|
16
28
|
apiKey: apiKey,
|
|
17
29
|
baseURL: baseURL
|
|
@@ -21,6 +33,7 @@ export class Agent {
|
|
|
21
33
|
const systemInfo = `
|
|
22
34
|
System Information:
|
|
23
35
|
- OS: ${os.type()} ${os.release()} (${os.platform()})
|
|
36
|
+
- Shell: ${buildShellInfo()}
|
|
24
37
|
- Architecture: ${os.arch()}
|
|
25
38
|
- Node.js Version: ${process.version}
|
|
26
39
|
- Current Working Directory: ${process.cwd()}
|
|
@@ -28,6 +41,9 @@ System Information:
|
|
|
28
41
|
- Home Directory: ${os.homedir()}
|
|
29
42
|
- Current Date: ${new Date().toLocaleString()}
|
|
30
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
|
+
: '';
|
|
31
47
|
this.messages = [
|
|
32
48
|
{
|
|
33
49
|
role: "system",
|
|
@@ -48,59 +64,133 @@ WHAT YOU CAN DO:
|
|
|
48
64
|
RULES OF ENGAGEMENT:
|
|
49
65
|
1. One shot, not one chat. Produce working results, not conversation. Be terse.
|
|
50
66
|
2. Use the right tool for the job. Shell for system ops. Files for content. Web tools for external info.
|
|
51
|
-
3. Always pass non-interactive flags: --yes for npx, -y for apt/apk, -f for rm, etc. Assume no human is watching.
|
|
67
|
+
3. Always pass non-interactive flags: --yes for npx, -y for apt/apk, -f for rm, etc. Assume no human is watching. Set GIT_TERMINAL_PROMPT=0 for git commands that may need credentials so they fail fast instead of hanging.
|
|
52
68
|
4. Container-friendly: stick to standard Unix tools available in Alpine/Debian slim images. No GUI apps, no browser-based debug tools.
|
|
53
69
|
5. For creative or complex tasks (image prompts, long-form writing, intricate scripts): call optimize_prompt first. It significantly raises output quality.
|
|
54
70
|
6. If a command fails, diagnose and try one alternative. Don't retry the same thing, don't give up on first error.
|
|
55
|
-
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}
|
|
56
72
|
`
|
|
57
73
|
}
|
|
58
74
|
];
|
|
59
75
|
}
|
|
76
|
+
get jsonMode() {
|
|
77
|
+
return !!this.config?.jsonMode;
|
|
78
|
+
}
|
|
79
|
+
emitEvent(event) {
|
|
80
|
+
if (this.jsonMode)
|
|
81
|
+
console.log(JSON.stringify(event));
|
|
82
|
+
}
|
|
83
|
+
// Tool handlers print progress via console; in JSON mode stdout is an NDJSON
|
|
84
|
+
// contract, so route those prints to stderr for the duration of the call.
|
|
85
|
+
async runToolQuietly(run) {
|
|
86
|
+
const original = [console.log, console.info, console.warn, console.error];
|
|
87
|
+
const toStderr = (...args) => process.stderr.write(util.format(...args) + '\n');
|
|
88
|
+
[console.log, console.info, console.warn, console.error] = [toStderr, toStderr, toStderr, toStderr];
|
|
89
|
+
try {
|
|
90
|
+
return await run();
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
[console.log, console.info, console.warn, console.error] = original;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
60
96
|
async chat(userInput) {
|
|
61
97
|
this.messages.push({ role: "user", content: userInput });
|
|
98
|
+
const maxSteps = Number(this.config?.maxSteps || process.env.AUTOCLOW_MAX_STEPS || DEFAULT_MAX_STEPS);
|
|
62
99
|
let active = true;
|
|
100
|
+
let step = 0;
|
|
101
|
+
let status = 'completed';
|
|
102
|
+
let errorMessage;
|
|
103
|
+
let lastContent = null;
|
|
104
|
+
const totalUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
105
|
+
let sawUsage = false;
|
|
106
|
+
this.emitEvent({ event: 'run_start', model: this.model, task: userInput });
|
|
63
107
|
while (active) {
|
|
64
|
-
|
|
108
|
+
if (step >= maxSteps) {
|
|
109
|
+
status = 'max_steps';
|
|
110
|
+
if (!this.jsonMode) {
|
|
111
|
+
console.log(chalk.yellow(`\n[MaxSteps] Reached the ${maxSteps}-turn limit; stopping to avoid a runaway loop.`));
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
step++;
|
|
116
|
+
const spinner = this.jsonMode
|
|
117
|
+
? { stop() { }, fail() { }, text: '' }
|
|
118
|
+
: ora('Thinking...').start();
|
|
119
|
+
let stream;
|
|
65
120
|
try {
|
|
66
|
-
|
|
121
|
+
// Retries cover request setup and the header phase; a failure after
|
|
122
|
+
// the stream started yielding chunks is not retried, because partial
|
|
123
|
+
// output may already have been printed.
|
|
124
|
+
stream = await withRetry(async () => this.client.chat.completions.create({
|
|
67
125
|
model: this.model,
|
|
68
126
|
messages: this.messages,
|
|
69
127
|
tools: getToolDefinitions(),
|
|
70
128
|
tool_choice: "auto",
|
|
71
|
-
stream: true
|
|
129
|
+
stream: true,
|
|
130
|
+
// Not every OpenAI-compatible provider accepts stream_options;
|
|
131
|
+
// usage tracking is therefore opt-in only.
|
|
132
|
+
...(this.config?.includeUsage ? { stream_options: { include_usage: true } } : {})
|
|
133
|
+
}), {
|
|
134
|
+
onRetry: (err, nextAttempt, delayMs) => {
|
|
135
|
+
spinner.text = `API error (${err.message}); retrying in ${Math.round(delayMs / 1000)}s (attempt ${nextAttempt})...`;
|
|
136
|
+
}
|
|
72
137
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
spinner.fail('Error during processing');
|
|
141
|
+
if (!this.jsonMode)
|
|
142
|
+
console.error(chalk.red(error.message));
|
|
143
|
+
status = 'error';
|
|
144
|
+
errorMessage = error.message;
|
|
145
|
+
active = false;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
let content = '';
|
|
149
|
+
let reasoningContent = '';
|
|
150
|
+
let toolCalls = [];
|
|
151
|
+
let contentStarted = false;
|
|
152
|
+
let reasoningStarted = false;
|
|
153
|
+
const toolNamesSeen = new Set();
|
|
154
|
+
try {
|
|
79
155
|
for await (const chunk of stream) {
|
|
156
|
+
if (chunk.usage) {
|
|
157
|
+
sawUsage = true;
|
|
158
|
+
totalUsage.prompt_tokens += chunk.usage.prompt_tokens ?? 0;
|
|
159
|
+
totalUsage.completion_tokens += chunk.usage.completion_tokens ?? 0;
|
|
160
|
+
totalUsage.total_tokens += chunk.usage.total_tokens ?? 0;
|
|
161
|
+
}
|
|
80
162
|
const delta = chunk.choices[0]?.delta;
|
|
81
163
|
// Handle reasoning/thinking content (e.g., DeepSeek)
|
|
82
164
|
if (delta?.reasoning_content) {
|
|
83
165
|
if (!reasoningStarted) {
|
|
84
166
|
spinner.stop();
|
|
85
|
-
|
|
167
|
+
if (!this.jsonMode) {
|
|
168
|
+
process.stdout.write(chalk.dim('\n[Thinking] '));
|
|
169
|
+
}
|
|
86
170
|
reasoningStarted = true;
|
|
87
171
|
}
|
|
88
|
-
|
|
172
|
+
if (!this.jsonMode) {
|
|
173
|
+
process.stdout.write(chalk.dim(delta.reasoning_content));
|
|
174
|
+
}
|
|
89
175
|
reasoningContent += delta.reasoning_content;
|
|
90
176
|
}
|
|
91
177
|
// Handle regular content
|
|
92
178
|
if (delta?.content) {
|
|
93
179
|
if (!contentStarted) {
|
|
94
180
|
spinner.stop();
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
181
|
+
if (!this.jsonMode) {
|
|
182
|
+
if (reasoningStarted)
|
|
183
|
+
process.stdout.write('\n');
|
|
184
|
+
process.stdout.write(chalk.blue("AutoClaw: "));
|
|
185
|
+
}
|
|
98
186
|
contentStarted = true;
|
|
99
187
|
}
|
|
100
|
-
|
|
188
|
+
if (!this.jsonMode) {
|
|
189
|
+
process.stdout.write(delta.content);
|
|
190
|
+
}
|
|
101
191
|
content += delta.content;
|
|
102
192
|
}
|
|
103
|
-
// Handle tool calls
|
|
193
|
+
// Handle tool calls
|
|
104
194
|
if (delta?.tool_calls) {
|
|
105
195
|
for (const tc of delta.tool_calls) {
|
|
106
196
|
const idx = tc.index;
|
|
@@ -113,45 +203,76 @@ RULES OF ENGAGEMENT:
|
|
|
113
203
|
toolCalls[idx].function.name += tc.function.name;
|
|
114
204
|
if (tc.function?.arguments)
|
|
115
205
|
toolCalls[idx].function.arguments += tc.function.arguments;
|
|
116
|
-
// Show tool name as soon as it's complete
|
|
117
206
|
if (tc.function?.name && !toolNamesSeen.has(idx)) {
|
|
118
207
|
toolNamesSeen.add(idx);
|
|
119
208
|
spinner.stop();
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
209
|
+
if (!this.jsonMode) {
|
|
210
|
+
if (contentStarted)
|
|
211
|
+
process.stdout.write('\n');
|
|
212
|
+
if (reasoningStarted && !contentStarted)
|
|
213
|
+
process.stdout.write('\n');
|
|
214
|
+
process.stdout.write(chalk.cyan(`[Calling] ${tc.function.name}\n`));
|
|
215
|
+
}
|
|
125
216
|
}
|
|
126
217
|
}
|
|
127
218
|
}
|
|
128
219
|
}
|
|
129
|
-
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
spinner.fail('Error during processing');
|
|
223
|
+
if (!this.jsonMode)
|
|
224
|
+
console.error(chalk.red(error.message));
|
|
225
|
+
status = 'error';
|
|
226
|
+
errorMessage = error.message;
|
|
227
|
+
active = false;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
if (!this.jsonMode) {
|
|
231
|
+
if (reasoningStarted)
|
|
130
232
|
console.log(); // newline after reasoning
|
|
131
|
-
|
|
132
|
-
if (contentStarted) {
|
|
233
|
+
if (contentStarted)
|
|
133
234
|
console.log(); // newline after streamed content
|
|
134
|
-
|
|
135
|
-
if (!reasoningStarted && !contentStarted) {
|
|
235
|
+
if (!reasoningStarted && !contentStarted)
|
|
136
236
|
spinner.stop();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
237
|
+
}
|
|
238
|
+
// Build the full message for history
|
|
239
|
+
const message = { role: "assistant" };
|
|
240
|
+
if (content)
|
|
241
|
+
message.content = content;
|
|
242
|
+
if (reasoningContent)
|
|
243
|
+
message.reasoning_content = reasoningContent;
|
|
244
|
+
if (toolCalls.length > 0) {
|
|
245
|
+
message.tool_calls = toolCalls;
|
|
246
|
+
message.content = message.content || null;
|
|
247
|
+
}
|
|
248
|
+
this.messages.push(message);
|
|
249
|
+
if (content)
|
|
250
|
+
lastContent = content;
|
|
251
|
+
if (toolCalls.length > 0) {
|
|
252
|
+
for (const toolCall of toolCalls) {
|
|
253
|
+
if (toolCall.type !== 'function')
|
|
254
|
+
continue;
|
|
255
|
+
const functionName = toolCall.function.name;
|
|
256
|
+
let functionArgs;
|
|
257
|
+
try {
|
|
258
|
+
functionArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
259
|
+
}
|
|
260
|
+
catch (parseError) {
|
|
261
|
+
// Feed the failure back so the model can correct itself next turn
|
|
262
|
+
if (!this.jsonMode) {
|
|
263
|
+
console.log(chalk.red(`\n[Tool] ${functionName} — malformed arguments (not valid JSON)`));
|
|
264
|
+
}
|
|
265
|
+
this.messages.push({
|
|
266
|
+
role: "tool",
|
|
267
|
+
tool_call_id: toolCall.id,
|
|
268
|
+
content: `Error: arguments for ${functionName} were not valid JSON (${parseError.message}). Re-issue the tool call with well-formed JSON arguments.`
|
|
269
|
+
});
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (this.jsonMode) {
|
|
273
|
+
this.emitEvent({ event: 'tool_call', step, tool: functionName, args: functionArgs });
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
155
276
|
// Display tool call info
|
|
156
277
|
console.log(chalk.cyan(`\n[Tool] ${functionName}`));
|
|
157
278
|
const argsStr = JSON.stringify(functionArgs, null, 2);
|
|
@@ -163,56 +284,82 @@ RULES OF ENGAGEMENT:
|
|
|
163
284
|
else {
|
|
164
285
|
console.log(chalk.dim(argsStr));
|
|
165
286
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
287
|
+
}
|
|
288
|
+
let toolResult;
|
|
289
|
+
try {
|
|
290
|
+
const run = () => executeToolHandler(functionName, functionArgs, this.config);
|
|
291
|
+
toolResult = this.jsonMode ? await this.runToolQuietly(run) : await run();
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
toolResult = `Error: ${err.message}`;
|
|
295
|
+
}
|
|
296
|
+
// Bound what goes back into the model context; the full output is
|
|
297
|
+
// kept on disk for /view.
|
|
298
|
+
const MAX_PREVIEW_LINES = 20;
|
|
299
|
+
const truncation = truncateOutput(toolResult);
|
|
300
|
+
const boundedResult = truncation.content;
|
|
301
|
+
const resultLines = boundedResult.split('\n');
|
|
302
|
+
let outputFile = null;
|
|
303
|
+
if (resultLines.length > MAX_PREVIEW_LINES || truncation.truncated) {
|
|
304
|
+
outputFile = await this.saveOutput(functionName, toolResult);
|
|
305
|
+
this.lastOutputFile = outputFile;
|
|
306
|
+
if (!this.jsonMode) {
|
|
307
|
+
console.log(chalk.green(`[Result]`));
|
|
182
308
|
console.log(resultLines.slice(0, MAX_PREVIEW_LINES).join('\n'));
|
|
183
309
|
const remaining = resultLines.length - MAX_PREVIEW_LINES;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const outputDir = path.join(os.homedir(), '.autoclaw', 'output');
|
|
187
|
-
if (!fs.existsSync(outputDir)) {
|
|
188
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
310
|
+
if (remaining > 0) {
|
|
311
|
+
console.log(chalk.dim(`\n ... ${remaining} more lines (${resultLines.length} lines total)`));
|
|
189
312
|
}
|
|
190
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
191
|
-
const outputFile = path.join(outputDir, `${functionName}_${ts}.txt`);
|
|
192
|
-
fs.writeFileSync(outputFile, toolResult, 'utf-8');
|
|
193
|
-
this.lastOutputFile = outputFile;
|
|
194
313
|
console.log(chalk.dim(` Type '/view' to see full output`));
|
|
195
314
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
315
|
+
}
|
|
316
|
+
else if (!this.jsonMode) {
|
|
317
|
+
console.log(chalk.green(`[Result]`));
|
|
318
|
+
console.log(boundedResult);
|
|
319
|
+
this.lastOutputFile = null;
|
|
320
|
+
}
|
|
321
|
+
if (this.jsonMode) {
|
|
322
|
+
this.emitEvent({
|
|
323
|
+
event: 'tool_result',
|
|
324
|
+
step,
|
|
325
|
+
tool: functionName,
|
|
326
|
+
truncated: truncation.truncated,
|
|
327
|
+
bytes: truncation.totalBytes,
|
|
328
|
+
...(outputFile ? { output_file: outputFile } : {})
|
|
204
329
|
});
|
|
205
330
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
331
|
+
this.messages.push({
|
|
332
|
+
role: "tool",
|
|
333
|
+
tool_call_id: toolCall.id,
|
|
334
|
+
content: boundedResult
|
|
335
|
+
});
|
|
209
336
|
}
|
|
210
337
|
}
|
|
211
|
-
|
|
212
|
-
spinner.fail('Error during processing');
|
|
213
|
-
console.error(chalk.red(error.message));
|
|
338
|
+
else {
|
|
214
339
|
active = false;
|
|
215
340
|
}
|
|
341
|
+
if (sawUsage) {
|
|
342
|
+
this.emitEvent({ event: 'usage', step, ...totalUsage });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const result = {
|
|
346
|
+
status,
|
|
347
|
+
steps: step,
|
|
348
|
+
message: lastContent,
|
|
349
|
+
...(errorMessage ? { error: errorMessage } : {}),
|
|
350
|
+
...(sawUsage ? { usage: totalUsage } : {})
|
|
351
|
+
};
|
|
352
|
+
this.emitEvent({ event: 'run_end', ...result });
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
async saveOutput(functionName, toolResult) {
|
|
356
|
+
const outputDir = path.join(os.homedir(), '.autoclaw', 'output');
|
|
357
|
+
if (!fs.existsSync(outputDir)) {
|
|
358
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
216
359
|
}
|
|
360
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
361
|
+
const outputFile = path.join(outputDir, `${functionName}_${ts}.txt`);
|
|
362
|
+
fs.writeFileSync(outputFile, toolResult, 'utf-8');
|
|
363
|
+
return outputFile;
|
|
217
364
|
}
|
|
218
365
|
}
|
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,8 @@ 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';
|
|
8
|
+
import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
|
|
7
9
|
import * as fs from 'fs';
|
|
8
10
|
import * as path from 'path';
|
|
9
11
|
import * as os from 'os';
|
|
@@ -42,7 +44,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
42
44
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
43
45
|
// In dist/index.js, package.json is usually up one level in the root
|
|
44
46
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
45
|
-
let version = '1.0
|
|
47
|
+
let version = '1.3.0';
|
|
46
48
|
try {
|
|
47
49
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
48
50
|
version = pkg.version;
|
|
@@ -56,8 +58,10 @@ program
|
|
|
56
58
|
.description('A lightweight AI agent CLI tool')
|
|
57
59
|
.version(version)
|
|
58
60
|
.option('-m, --model <model>', 'Model to use')
|
|
61
|
+
.option('-P, --provider <name>', 'Use a provider preset (openai, deepseek, moonshot, dashscope, zhipu, openrouter, ollama)')
|
|
59
62
|
.option('-n, --no-interactive', 'Exit after processing the initial query (Headless mode)')
|
|
60
|
-
.option('-y, --yes', 'Auto-confirm all tool executions (e.g., shell commands)')
|
|
63
|
+
.option('-y, --yes', 'Auto-confirm all tool executions (e.g., shell commands)')
|
|
64
|
+
.option('--json', 'Emit NDJSON events on stdout (for orchestrators; use with -n)');
|
|
61
65
|
program
|
|
62
66
|
.command('setup')
|
|
63
67
|
.description('Run the interactive setup wizard to configure API keys')
|
|
@@ -72,6 +76,15 @@ program
|
|
|
72
76
|
const options = program.opts();
|
|
73
77
|
await runChat(queryParts, options);
|
|
74
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
|
+
});
|
|
75
88
|
program.parse(process.argv);
|
|
76
89
|
async function runSetup(options = {}) {
|
|
77
90
|
const isProject = options.project;
|
|
@@ -92,6 +105,20 @@ async function runSetup(options = {}) {
|
|
|
92
105
|
return '******';
|
|
93
106
|
return `${secret.slice(0, 3)}...${secret.slice(-4)}`;
|
|
94
107
|
}
|
|
108
|
+
const providerAnswer = await inquirer.prompt([
|
|
109
|
+
{
|
|
110
|
+
type: 'list',
|
|
111
|
+
name: 'provider',
|
|
112
|
+
message: 'Select your LLM provider:',
|
|
113
|
+
choices: [
|
|
114
|
+
...providerNames().map((name) => ({ name: `${PROVIDER_PRESETS[name].label} (${name})`, value: name })),
|
|
115
|
+
{ name: 'Custom OpenAI-compatible endpoint', value: 'custom' }
|
|
116
|
+
],
|
|
117
|
+
default: currentConfig.provider || 'openai'
|
|
118
|
+
}
|
|
119
|
+
]);
|
|
120
|
+
const provider = providerAnswer.provider;
|
|
121
|
+
const preset = resolveProvider(provider === 'custom' ? undefined : provider);
|
|
95
122
|
const answers = await inquirer.prompt([
|
|
96
123
|
{
|
|
97
124
|
type: 'password',
|
|
@@ -112,13 +139,13 @@ async function runSetup(options = {}) {
|
|
|
112
139
|
type: 'input',
|
|
113
140
|
name: 'baseUrl',
|
|
114
141
|
message: 'Enter API Base URL:',
|
|
115
|
-
default: currentConfig.baseUrl || 'https://api.openai.com/v1'
|
|
142
|
+
default: currentConfig.baseUrl || preset?.baseUrl || 'https://api.openai.com/v1'
|
|
116
143
|
},
|
|
117
144
|
{
|
|
118
145
|
type: 'input',
|
|
119
146
|
name: 'model',
|
|
120
147
|
message: 'Enter default Model:',
|
|
121
|
-
default: currentConfig.model || 'gpt-
|
|
148
|
+
default: currentConfig.model || preset?.defaultModel || 'gpt-5.6'
|
|
122
149
|
},
|
|
123
150
|
{
|
|
124
151
|
type: 'confirm',
|
|
@@ -319,6 +346,7 @@ async function runSetup(options = {}) {
|
|
|
319
346
|
apiKey: finalApiKey,
|
|
320
347
|
baseUrl: answers.baseUrl,
|
|
321
348
|
model: answers.model,
|
|
349
|
+
provider: provider === 'custom' ? undefined : provider,
|
|
322
350
|
...imageConfig,
|
|
323
351
|
...emailConfig,
|
|
324
352
|
...searchConfig,
|
|
@@ -336,11 +364,9 @@ async function runSetup(options = {}) {
|
|
|
336
364
|
console.error(chalk.red(`Failed to write config: ${error.message}`));
|
|
337
365
|
}
|
|
338
366
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
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) {
|
|
344
370
|
// 1. Load Global JSON
|
|
345
371
|
const globalConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
|
346
372
|
// 2. Load Local JSON (Project Level)
|
|
@@ -351,18 +377,31 @@ async function runChat(queryParts, options) {
|
|
|
351
377
|
// 3. Merge Configs for Tool Usage
|
|
352
378
|
// Priority: Local > Global
|
|
353
379
|
const fullConfig = { ...globalConfig, ...localConfig };
|
|
354
|
-
// 4. Resolve
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
380
|
+
// 4. Resolve Provider Preset (CLI > Env > Config)
|
|
381
|
+
const providerName = options.provider || process.env.AUTOCLOW_PROVIDER || fullConfig.provider;
|
|
382
|
+
const preset = resolveProvider(providerName);
|
|
383
|
+
if (providerName && !preset) {
|
|
384
|
+
console.log(chalk.yellow(`Unknown provider '${providerName}'. Known providers: ${providerNames().join(', ')}`));
|
|
385
|
+
}
|
|
386
|
+
// 5. Resolve Env Vars (CLI > Env > Config > Provider preset)
|
|
387
|
+
let apiKey = process.env.OPENAI_API_KEY || fullConfig.apiKey || (preset?.apiKeyEnv ? process.env[preset.apiKeyEnv] : undefined);
|
|
388
|
+
let baseURL = process.env.OPENAI_BASE_URL || fullConfig.baseUrl || preset?.baseUrl;
|
|
389
|
+
let model = options.model || process.env.OPENAI_MODEL || fullConfig.model || preset?.defaultModel || 'gpt-5.6';
|
|
358
390
|
// Inject Runtime Flags
|
|
359
391
|
fullConfig.autoConfirm = options.yes;
|
|
392
|
+
fullConfig.jsonMode = !!options.json;
|
|
393
|
+
// Usage tracking is opt-in: not every OpenAI-compatible provider accepts
|
|
394
|
+
// stream_options.include_usage, so never force it on.
|
|
395
|
+
fullConfig.includeUsage =
|
|
396
|
+
!!fullConfig.includeUsage ||
|
|
397
|
+
process.env.AUTOCLOW_INCLUDE_USAGE === '1' ||
|
|
398
|
+
process.env.AUTOCLOW_INCLUDE_USAGE === 'true';
|
|
360
399
|
// Inject Env vars
|
|
361
400
|
if (process.env.SMTP_HOST)
|
|
362
401
|
fullConfig.smtpHost = process.env.SMTP_HOST;
|
|
363
402
|
if (process.env.SMTP_PORT)
|
|
364
403
|
fullConfig.smtpPort = process.env.SMTP_PORT;
|
|
365
|
-
if (process.env.
|
|
404
|
+
if (process.env.SMTP_USER)
|
|
366
405
|
fullConfig.smtpUser = process.env.SMTP_USER;
|
|
367
406
|
if (process.env.SMTP_PASS)
|
|
368
407
|
fullConfig.smtpPass = process.env.SMTP_PASS;
|
|
@@ -393,9 +432,10 @@ async function runChat(queryParts, options) {
|
|
|
393
432
|
if (doSetup) {
|
|
394
433
|
await runSetup();
|
|
395
434
|
const newConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
435
|
+
const setupPreset = resolveProvider(newConfig.provider);
|
|
436
|
+
apiKey = newConfig.apiKey || (setupPreset?.apiKeyEnv ? process.env[setupPreset.apiKeyEnv] : undefined);
|
|
437
|
+
baseURL = newConfig.baseUrl || setupPreset?.baseUrl;
|
|
438
|
+
model = options.model || newConfig.model || setupPreset?.defaultModel || 'gpt-5.6';
|
|
399
439
|
Object.assign(fullConfig, newConfig);
|
|
400
440
|
}
|
|
401
441
|
else {
|
|
@@ -407,6 +447,74 @@ async function runChat(queryParts, options) {
|
|
|
407
447
|
console.error(chalk.red("API Key is still missing. Exiting."));
|
|
408
448
|
process.exit(1);
|
|
409
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);
|
|
410
518
|
const agent = new Agent(apiKey, baseURL, model, fullConfig);
|
|
411
519
|
if (options.interactive) {
|
|
412
520
|
console.log(chalk.green(`Agent initialized with model: ${model}`));
|
|
@@ -417,10 +525,11 @@ async function runChat(queryParts, options) {
|
|
|
417
525
|
if (options.interactive) {
|
|
418
526
|
console.log(chalk.blue("\nProcessing initial request: ") + chalk.bold(initialQuery));
|
|
419
527
|
}
|
|
420
|
-
await agent.chat(initialQuery);
|
|
421
|
-
// Headless mode exit
|
|
528
|
+
const result = await agent.chat(initialQuery);
|
|
529
|
+
// Headless mode exit — the exit code is the orchestrator-facing outcome:
|
|
530
|
+
// 0 completed, 1 hard failure, 2 step cap reached (task unfinished).
|
|
422
531
|
if (!options.interactive) {
|
|
423
|
-
process.exit(0);
|
|
532
|
+
process.exit(result.status === 'completed' ? 0 : result.status === 'max_steps' ? 2 : 1);
|
|
424
533
|
}
|
|
425
534
|
}
|
|
426
535
|
// Main chat loop
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const PROVIDER_PRESETS = {
|
|
2
|
+
openai: {
|
|
3
|
+
label: 'OpenAI',
|
|
4
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
5
|
+
defaultModel: 'gpt-5.6',
|
|
6
|
+
apiKeyEnv: 'OPENAI_API_KEY'
|
|
7
|
+
},
|
|
8
|
+
deepseek: {
|
|
9
|
+
label: 'DeepSeek',
|
|
10
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
11
|
+
defaultModel: 'deepseek-v4-pro',
|
|
12
|
+
apiKeyEnv: 'DEEPSEEK_API_KEY'
|
|
13
|
+
},
|
|
14
|
+
moonshot: {
|
|
15
|
+
label: 'Moonshot (Kimi)',
|
|
16
|
+
baseUrl: 'https://api.moonshot.cn/v1',
|
|
17
|
+
defaultModel: 'kimi-k3',
|
|
18
|
+
apiKeyEnv: 'MOONSHOT_API_KEY'
|
|
19
|
+
},
|
|
20
|
+
dashscope: {
|
|
21
|
+
label: 'Alibaba DashScope (Qwen)',
|
|
22
|
+
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
23
|
+
defaultModel: 'qwen3.8-max',
|
|
24
|
+
apiKeyEnv: 'DASHSCOPE_API_KEY'
|
|
25
|
+
},
|
|
26
|
+
zhipu: {
|
|
27
|
+
label: 'Zhipu (GLM)',
|
|
28
|
+
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
29
|
+
defaultModel: 'glm-5',
|
|
30
|
+
apiKeyEnv: 'ZHIPU_API_KEY'
|
|
31
|
+
},
|
|
32
|
+
openrouter: {
|
|
33
|
+
label: 'OpenRouter',
|
|
34
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
35
|
+
defaultModel: 'openai/gpt-5.6',
|
|
36
|
+
apiKeyEnv: 'OPENROUTER_API_KEY'
|
|
37
|
+
},
|
|
38
|
+
ollama: {
|
|
39
|
+
label: 'Ollama (local)',
|
|
40
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
41
|
+
defaultModel: 'qwen3'
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export function resolveProvider(name) {
|
|
45
|
+
if (!name)
|
|
46
|
+
return undefined;
|
|
47
|
+
return PROVIDER_PRESETS[name.toLowerCase()];
|
|
48
|
+
}
|
|
49
|
+
export function providerNames() {
|
|
50
|
+
return Object.keys(PROVIDER_PRESETS);
|
|
51
|
+
}
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const DEFAULT_MAX_ATTEMPTS = 3;
|
|
2
|
+
export const DEFAULT_BASE_DELAY_MS = 1000;
|
|
3
|
+
export function sleep(ms) {
|
|
4
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
5
|
+
}
|
|
6
|
+
export function isRetryableError(err) {
|
|
7
|
+
if (!err)
|
|
8
|
+
return false;
|
|
9
|
+
const status = err.status ?? err.statusCode;
|
|
10
|
+
if (typeof status === 'number') {
|
|
11
|
+
return status === 429 || status === 408 || status >= 500;
|
|
12
|
+
}
|
|
13
|
+
const match = /status code (\d{3})/.exec(String(err.message ?? ''));
|
|
14
|
+
if (match) {
|
|
15
|
+
const code = Number(match[1]);
|
|
16
|
+
return code === 429 || code === 408 || code >= 500;
|
|
17
|
+
}
|
|
18
|
+
const transientCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EAI_AGAIN', 'ECONNABORTED', 'EPIPE', 'ENOTFOUND'];
|
|
19
|
+
if (typeof err.code === 'string' && transientCodes.includes(err.code))
|
|
20
|
+
return true;
|
|
21
|
+
return /fetch failed|network|socket hang up|connection error|terminated/i.test(String(err.message ?? ''));
|
|
22
|
+
}
|
|
23
|
+
export async function withRetry(fn, options = {}) {
|
|
24
|
+
const attempts = options.attempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
25
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
26
|
+
const sleepFn = options.sleepFn ?? sleep;
|
|
27
|
+
let lastErr;
|
|
28
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
lastErr = err;
|
|
34
|
+
if (attempt >= attempts || !isRetryableError(err))
|
|
35
|
+
throw err;
|
|
36
|
+
const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
|
|
37
|
+
options.onRetry?.(err, attempt + 1, delayMs);
|
|
38
|
+
await sleepFn(delayMs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw lastErr;
|
|
42
|
+
}
|
package/dist/tools/core.js
CHANGED
|
@@ -5,6 +5,8 @@ import inquirer from 'inquirer';
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import util from 'util';
|
|
7
7
|
const execAsync = util.promisify(exec);
|
|
8
|
+
const DEFAULT_SHELL_TIMEOUT_MS = 120000;
|
|
9
|
+
const SHELL_MAX_BUFFER = 10 * 1024 * 1024;
|
|
8
10
|
export const ShellTool = {
|
|
9
11
|
name: "Shell Execution",
|
|
10
12
|
definition: {
|
|
@@ -25,8 +27,14 @@ export const ShellTool = {
|
|
|
25
27
|
handler: async (args, config) => {
|
|
26
28
|
console.log(chalk.yellow(`\nAI wants to execute: `) + chalk.bold(args.command));
|
|
27
29
|
console.log(chalk.dim(`Reason: ${args.rationale}`));
|
|
30
|
+
const timeoutMs = Number(config?.shellTimeout || process.env.AUTOCLOW_SHELL_TIMEOUT || DEFAULT_SHELL_TIMEOUT_MS);
|
|
28
31
|
// Check for auto-confirm flag
|
|
29
32
|
if (!config?.autoConfirm) {
|
|
33
|
+
if (!process.stdin.isTTY) {
|
|
34
|
+
// No human can answer the confirmation prompt in this environment;
|
|
35
|
+
// denying beats hanging on a dead prompt or auto-running unasked.
|
|
36
|
+
return "Denied: this shell command requires user confirmation, but no interactive terminal is attached. Re-run with --yes (or -y) to allow unattended execution.";
|
|
37
|
+
}
|
|
30
38
|
const { confirm } = await inquirer.prompt([
|
|
31
39
|
{
|
|
32
40
|
type: 'confirm',
|
|
@@ -42,10 +50,16 @@ export const ShellTool = {
|
|
|
42
50
|
console.log(chalk.gray("(Auto-confirming command execution due to --yes flag)"));
|
|
43
51
|
}
|
|
44
52
|
try {
|
|
45
|
-
const { stdout, stderr } = await execAsync(args.command
|
|
53
|
+
const { stdout, stderr } = await execAsync(args.command, {
|
|
54
|
+
timeout: timeoutMs,
|
|
55
|
+
maxBuffer: SHELL_MAX_BUFFER
|
|
56
|
+
});
|
|
46
57
|
return stdout + (stderr ? `\nStderr: ${stderr}` : '');
|
|
47
58
|
}
|
|
48
59
|
catch (error) {
|
|
60
|
+
if (error.killed === true || error.signal) {
|
|
61
|
+
return `Command timed out after ${timeoutMs}ms and was terminated.\nStdout: ${error.stdout ?? ''}\nStderr: ${error.stderr ?? ''}`;
|
|
62
|
+
}
|
|
49
63
|
return `Command failed: ${error.message}\nStdout: ${error.stdout}\nStderr: ${error.stderr}`;
|
|
50
64
|
}
|
|
51
65
|
}
|
|
@@ -33,7 +33,7 @@ export const PromptOptimizerTool = {
|
|
|
33
33
|
const contextMsg = args.context ? `Context: ${args.context}` : "Context: General AI Assistant interaction.";
|
|
34
34
|
try {
|
|
35
35
|
const completion = await client.chat.completions.create({
|
|
36
|
-
model: config.model || 'gpt-
|
|
36
|
+
model: config.model || 'gpt-5.6',
|
|
37
37
|
messages: [
|
|
38
38
|
{
|
|
39
39
|
role: "system",
|
package/dist/truncate.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Tool outputs are fed back into the model context, so they must be bounded.
|
|
2
|
+
// Two independent limits apply — whichever is hit first wins:
|
|
3
|
+
// - line limit (default 2000 lines)
|
|
4
|
+
// - byte limit (default 50KB, counted as UTF-8 bytes)
|
|
5
|
+
export const DEFAULT_MAX_LINES = 2000;
|
|
6
|
+
export const DEFAULT_MAX_BYTES = 50 * 1024;
|
|
7
|
+
export function truncateOutput(content, options = {}) {
|
|
8
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
9
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
10
|
+
const totalBytes = Buffer.byteLength(content, 'utf8');
|
|
11
|
+
const lines = content.length === 0 ? [] : content.split('\n');
|
|
12
|
+
const totalLines = lines.length;
|
|
13
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
14
|
+
return { content, truncated: false, totalLines, totalBytes, emittedLines: totalLines, emittedBytes: totalBytes };
|
|
15
|
+
}
|
|
16
|
+
let emittedLines = 0;
|
|
17
|
+
let emittedBytes = 0;
|
|
18
|
+
const kept = [];
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
if (kept.length + 1 > maxLines)
|
|
21
|
+
break;
|
|
22
|
+
const lineBytes = Buffer.byteLength(line, 'utf8') + (kept.length < lines.length - 1 ? 1 : 0);
|
|
23
|
+
if (emittedBytes + lineBytes > maxBytes)
|
|
24
|
+
break;
|
|
25
|
+
kept.push(line);
|
|
26
|
+
emittedLines++;
|
|
27
|
+
emittedBytes += lineBytes;
|
|
28
|
+
}
|
|
29
|
+
const notice = `[Truncated: showing ${emittedLines} of ${totalLines} lines, ${emittedBytes} of ${totalBytes} bytes]`;
|
|
30
|
+
const out = kept.length > 0 ? kept.join('\n') + '\n' + notice : notice;
|
|
31
|
+
return { content: out, truncated: true, totalLines, totalBytes, emittedLines, emittedBytes };
|
|
32
|
+
}
|
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": {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node dist/index.js",
|
|
12
12
|
"dev": "node --loader ts-node/esm src/index.ts",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"coverage": "vitest run --coverage",
|
|
14
16
|
"prepublishOnly": "npm run build"
|
|
15
17
|
},
|
|
16
18
|
"files": [
|
|
@@ -64,7 +66,9 @@
|
|
|
64
66
|
"@types/jsdom": "^27.0.0",
|
|
65
67
|
"@types/node": "^25.2.1",
|
|
66
68
|
"@types/nodemailer": "^7.0.9",
|
|
69
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
67
70
|
"ts-node": "^10.9.2",
|
|
68
|
-
"typescript": "^5.9.3"
|
|
71
|
+
"typescript": "^5.9.3",
|
|
72
|
+
"vitest": "^4.1.4"
|
|
69
73
|
}
|
|
70
74
|
}
|