autoclaw 1.1.1 → 1.2.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 +46 -6
- package/README.zh-CN.md +47 -7
- package/dist/agent.js +221 -86
- package/dist/index.js +48 -15
- 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 +5 -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,21 @@ 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`.
|
|
100
113
|
|
|
101
114
|
### Auto-Confirm (CI/CD)
|
|
102
115
|
Automatically approve all tool executions (dangerous, use with caution or in sandboxes).
|
|
@@ -105,24 +118,36 @@ autoclaw "Refactor src/index.ts to use ES modules" -y
|
|
|
105
118
|
```
|
|
106
119
|
|
|
107
120
|
### CLI Options
|
|
108
|
-
- `-m, --model <model>`: Specify the LLM model (default: `gpt-
|
|
121
|
+
- `-m, --model <model>`: Specify the LLM model (default: `gpt-5.6`).
|
|
122
|
+
- `-P, --provider <name>`: Use a provider preset (see [Providers](#providers)).
|
|
109
123
|
- `-n, --no-interactive`: Exit after processing the initial query (Headless mode).
|
|
110
124
|
- `-y, --yes`: Auto-confirm all tool executions (e.g., shell commands).
|
|
125
|
+
- `--json`: Emit NDJSON events on stdout (for orchestrators; use with `-n`).
|
|
126
|
+
|
|
127
|
+
### Providers
|
|
128
|
+
AutoClaw works with any OpenAI-compatible endpoint. Built-in presets fill in the base URL and a default model for you:
|
|
129
|
+
```bash
|
|
130
|
+
autoclaw -P deepseek "Check disk usage and save a report" -y -n
|
|
131
|
+
```
|
|
132
|
+
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
133
|
|
|
112
134
|
## Configuration
|
|
113
135
|
|
|
114
136
|
AutoClaw uses a hierarchical configuration system.
|
|
115
137
|
|
|
116
138
|
**Priority Order (Highest to Lowest):**
|
|
117
|
-
1. **CLI Arguments**: (e.g., `-m gpt-
|
|
139
|
+
1. **CLI Arguments**: (e.g., `-m gpt-5.6`)
|
|
118
140
|
2. **Environment Variables**: (`OPENAI_API_KEY`, `.env` file)
|
|
119
141
|
3. **Project Config**: (`./.autoclaw/setting.json` in current directory)
|
|
120
142
|
4. **Global Config**: (`~/.autoclaw/setting.json`)
|
|
121
143
|
|
|
122
144
|
### Supported Configuration Keys (JSON)
|
|
145
|
+
- `provider`: Provider preset name (e.g. `deepseek`).
|
|
123
146
|
- `apiKey`: Your OpenAI API Key.
|
|
124
147
|
- `baseUrl`: Custom Base URL (e.g., for DeepSeek or LocalLLM).
|
|
125
148
|
- `model`: Default model to use.
|
|
149
|
+
- `maxSteps`: Max LLM turns per task before the agent stops (default: `25`).
|
|
150
|
+
- `shellTimeout`: Shell command timeout in milliseconds (default: `120000`).
|
|
126
151
|
- `tavilyApiKey`: API Key for Tavily Web Search.
|
|
127
152
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP Email settings.
|
|
128
153
|
- `feishuWebhook`, `dingtalkWebhook`, `wecomWebhook`: Notification webhooks.
|
|
@@ -131,13 +156,20 @@ AutoClaw uses a hierarchical configuration system.
|
|
|
131
156
|
Create a file at `.autoclaw/setting.json`:
|
|
132
157
|
```json
|
|
133
158
|
{
|
|
134
|
-
"model": "gpt-
|
|
159
|
+
"model": "gpt-5.6",
|
|
135
160
|
"baseUrl": "https://api.deepseek.com/v1"
|
|
136
161
|
}
|
|
137
162
|
```
|
|
138
163
|
|
|
139
164
|
> **⚠️ 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
165
|
|
|
166
|
+
### Environment Variables
|
|
167
|
+
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: main LLM settings.
|
|
168
|
+
- `AUTOCLOW_PROVIDER`: provider preset used when `-P` is not passed.
|
|
169
|
+
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: reliability limits (max LLM turns per task; shell timeout in ms).
|
|
170
|
+
- `AUTOCLOW_INCLUDE_USAGE`: set to `1`/`true` to request token usage from the API (opt-in).
|
|
171
|
+
- `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.
|
|
172
|
+
|
|
141
173
|
## Integrations
|
|
142
174
|
|
|
143
175
|
### Web Search (Tavily)
|
|
@@ -158,6 +190,14 @@ Built-in utility to provide the agent with the current system time, ensuring acc
|
|
|
158
190
|
|
|
159
191
|
## Docker Support
|
|
160
192
|
|
|
193
|
+
### Build & Run
|
|
194
|
+
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:
|
|
195
|
+
```bash
|
|
196
|
+
docker build -t autoclaw .
|
|
197
|
+
docker run --rm -v "$PWD":/workspace -w /workspace -e OPENAI_API_KEY=sk-... autoclaw "Check disk usage and save a report" -y -n
|
|
198
|
+
```
|
|
199
|
+
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.
|
|
200
|
+
|
|
161
201
|
### Chinese Font Issues in Screenshots
|
|
162
202
|
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
203
|
|
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,21 @@ 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`。
|
|
101
114
|
|
|
102
115
|
### 自动确认 (CI/CD)
|
|
103
116
|
自动批准所有工具执行(危险操作,请谨慎使用或在沙箱环境下运行)。
|
|
@@ -106,24 +119,36 @@ autoclaw "将 src/index.ts 重构为使用 ES 模块" -y
|
|
|
106
119
|
```
|
|
107
120
|
|
|
108
121
|
### CLI 选项
|
|
109
|
-
- `-m, --model <model>`: 指定 LLM 模型 (默认: `gpt-
|
|
122
|
+
- `-m, --model <model>`: 指定 LLM 模型 (默认: `gpt-5.6`)。
|
|
123
|
+
- `-P, --provider <name>`: 使用 provider 预设 (见 [Provider 预设](#provider-预设))。
|
|
110
124
|
- `-n, --no-interactive`: 处理完初始查询后退出 (无头模式)。
|
|
111
125
|
- `-y, --yes`: 自动确认所有工具执行 (例如 Shell 命令)。
|
|
126
|
+
- `--json`: 在 stdout 输出 NDJSON 事件流 (供编排器使用,配合 `-n`)。
|
|
127
|
+
|
|
128
|
+
### Provider 预设
|
|
129
|
+
AutoClaw 可对接任意 OpenAI 兼容端点。内置预设可自动填好 Base URL 和默认模型:
|
|
130
|
+
```bash
|
|
131
|
+
autoclaw -P deepseek "检查磁盘使用情况并保存报告" -y -n
|
|
132
|
+
```
|
|
133
|
+
可用预设:`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
134
|
|
|
113
135
|
## 配置
|
|
114
136
|
|
|
115
137
|
AutoClaw 使用层级配置系统。
|
|
116
138
|
|
|
117
139
|
**优先级排序 (从高到低):**
|
|
118
|
-
1. **CLI 参数**: (例如 `-m gpt-
|
|
140
|
+
1. **CLI 参数**: (例如 `-m gpt-5.6`)
|
|
119
141
|
2. **环境变量**: (`OPENAI_API_KEY`, `.env` 文件)
|
|
120
142
|
3. **项目配置**: (当前目录下的 `./.autoclaw/setting.json`)
|
|
121
143
|
4. **全局配置**: (`~/.autoclaw/setting.json`)
|
|
122
144
|
|
|
123
145
|
### 支持的配置键 (JSON)
|
|
146
|
+
- `provider`: Provider 预设名 (如 `deepseek`)。
|
|
124
147
|
- `apiKey`: 您的 OpenAI API 密钥。
|
|
125
148
|
- `baseUrl`: 自定义 API 基础地址 (例如 DeepSeek 或本地 LLM)。
|
|
126
149
|
- `model`: 默认使用的模型。
|
|
150
|
+
- `maxSteps`: 单任务最大 LLM 轮数,超出后自动停止 (默认: `25`)。
|
|
151
|
+
- `shellTimeout`: Shell 命令超时时间(毫秒)(默认: `120000`)。
|
|
127
152
|
- `tavilyApiKey`: Tavily 网页搜索的 API 密钥。
|
|
128
153
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP 邮件设置。
|
|
129
154
|
- `feishuWebhook`, `dingtalkWebhook`, `wecomWebhook`: 通知钩子地址。
|
|
@@ -132,13 +157,20 @@ AutoClaw 使用层级配置系统。
|
|
|
132
157
|
在 `.autoclaw/setting.json` 创建文件:
|
|
133
158
|
```json
|
|
134
159
|
{
|
|
135
|
-
"model": "gpt-
|
|
160
|
+
"model": "gpt-5.6",
|
|
136
161
|
"baseUrl": "https://api.deepseek.com/v1"
|
|
137
162
|
}
|
|
138
163
|
```
|
|
139
164
|
|
|
140
165
|
> **⚠️ 安全警告**: 如果您在 `.autoclaw/setting.json` 中存储了 `apiKey` 或机密信息,请务必将 `.autoclaw/` 添加到您的 `.gitignore` 文件中,以防泄露!
|
|
141
166
|
|
|
167
|
+
### 环境变量
|
|
168
|
+
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: 主模型设置。
|
|
169
|
+
- `AUTOCLOW_PROVIDER`: 未传 `-P` 时使用的 provider 预设。
|
|
170
|
+
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: 稳定性限制(单任务最大轮数;Shell 超时毫秒数)。
|
|
171
|
+
- `AUTOCLOW_INCLUDE_USAGE`: 设为 `1`/`true` 时向 API 请求 token 用量(可选开启)。
|
|
172
|
+
- `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: 工具凭据,可作为 setup 的替代方式。
|
|
173
|
+
|
|
142
174
|
## 集成功能
|
|
143
175
|
|
|
144
176
|
### 网页搜索 (Tavily)
|
|
@@ -159,6 +191,14 @@ AutoClaw 使用层级配置系统。
|
|
|
159
191
|
|
|
160
192
|
## Docker 支持
|
|
161
193
|
|
|
194
|
+
### 构建与运行
|
|
195
|
+
仓库自带多阶段构建的 `Dockerfile`(node:22-alpine,跳过浏览器下载保持镜像苗条)。容器内直接运行无头一次性任务,配合目录挂载操作当前目录:
|
|
196
|
+
```bash
|
|
197
|
+
docker build -t autoclaw .
|
|
198
|
+
docker run --rm -v "$PWD":/workspace -w /workspace -e OPENAI_API_KEY=sk-... autoclaw "检查磁盘使用情况并保存报告" -y -n
|
|
199
|
+
```
|
|
200
|
+
注意:默认镜像中未内置浏览器,基于浏览器的工具(`read_website` / `take_screenshot`)不可用——它们会返回友好的安装提示,而不是报错崩溃。
|
|
201
|
+
|
|
162
202
|
### 截图中的中文显示问题
|
|
163
203
|
在 Docker 容器(尤其是 Alpine 或 Debian Slim)中运行时,网页截图中的中文可能会显示为方块("豆腐块")。表情符号(如 🔥)也可能显示为方块。
|
|
164
204
|
|
package/dist/agent.js
CHANGED
|
@@ -4,14 +4,18 @@ 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;
|
|
8
12
|
export class Agent {
|
|
9
13
|
client;
|
|
10
14
|
messages;
|
|
11
15
|
model;
|
|
12
16
|
config;
|
|
13
17
|
lastOutputFile = null;
|
|
14
|
-
constructor(apiKey, baseURL, model = 'gpt-
|
|
18
|
+
constructor(apiKey, baseURL, model = 'gpt-5.6', config = {}) {
|
|
15
19
|
this.client = new OpenAI({
|
|
16
20
|
apiKey: apiKey,
|
|
17
21
|
baseURL: baseURL
|
|
@@ -48,7 +52,7 @@ WHAT YOU CAN DO:
|
|
|
48
52
|
RULES OF ENGAGEMENT:
|
|
49
53
|
1. One shot, not one chat. Produce working results, not conversation. Be terse.
|
|
50
54
|
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.
|
|
55
|
+
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
56
|
4. Container-friendly: stick to standard Unix tools available in Alpine/Debian slim images. No GUI apps, no browser-based debug tools.
|
|
53
57
|
5. For creative or complex tasks (image prompts, long-form writing, intricate scripts): call optimize_prompt first. It significantly raises output quality.
|
|
54
58
|
6. If a command fails, diagnose and try one alternative. Don't retry the same thing, don't give up on first error.
|
|
@@ -57,50 +61,124 @@ RULES OF ENGAGEMENT:
|
|
|
57
61
|
}
|
|
58
62
|
];
|
|
59
63
|
}
|
|
64
|
+
get jsonMode() {
|
|
65
|
+
return !!this.config?.jsonMode;
|
|
66
|
+
}
|
|
67
|
+
emitEvent(event) {
|
|
68
|
+
if (this.jsonMode)
|
|
69
|
+
console.log(JSON.stringify(event));
|
|
70
|
+
}
|
|
71
|
+
// Tool handlers print progress via console; in JSON mode stdout is an NDJSON
|
|
72
|
+
// contract, so route those prints to stderr for the duration of the call.
|
|
73
|
+
async runToolQuietly(run) {
|
|
74
|
+
const original = [console.log, console.info, console.warn, console.error];
|
|
75
|
+
const toStderr = (...args) => process.stderr.write(util.format(...args) + '\n');
|
|
76
|
+
[console.log, console.info, console.warn, console.error] = [toStderr, toStderr, toStderr, toStderr];
|
|
77
|
+
try {
|
|
78
|
+
return await run();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
[console.log, console.info, console.warn, console.error] = original;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
60
84
|
async chat(userInput) {
|
|
61
85
|
this.messages.push({ role: "user", content: userInput });
|
|
86
|
+
const maxSteps = Number(this.config?.maxSteps || process.env.AUTOCLOW_MAX_STEPS || DEFAULT_MAX_STEPS);
|
|
62
87
|
let active = true;
|
|
88
|
+
let step = 0;
|
|
89
|
+
let status = 'completed';
|
|
90
|
+
let errorMessage;
|
|
91
|
+
let lastContent = null;
|
|
92
|
+
const totalUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
93
|
+
let sawUsage = false;
|
|
94
|
+
this.emitEvent({ event: 'run_start', model: this.model, task: userInput });
|
|
63
95
|
while (active) {
|
|
64
|
-
|
|
96
|
+
if (step >= maxSteps) {
|
|
97
|
+
status = 'max_steps';
|
|
98
|
+
if (!this.jsonMode) {
|
|
99
|
+
console.log(chalk.yellow(`\n[MaxSteps] Reached the ${maxSteps}-turn limit; stopping to avoid a runaway loop.`));
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
step++;
|
|
104
|
+
const spinner = this.jsonMode
|
|
105
|
+
? { stop() { }, fail() { }, text: '' }
|
|
106
|
+
: ora('Thinking...').start();
|
|
107
|
+
let stream;
|
|
65
108
|
try {
|
|
66
|
-
|
|
109
|
+
// Retries cover request setup and the header phase; a failure after
|
|
110
|
+
// the stream started yielding chunks is not retried, because partial
|
|
111
|
+
// output may already have been printed.
|
|
112
|
+
stream = await withRetry(async () => this.client.chat.completions.create({
|
|
67
113
|
model: this.model,
|
|
68
114
|
messages: this.messages,
|
|
69
115
|
tools: getToolDefinitions(),
|
|
70
116
|
tool_choice: "auto",
|
|
71
|
-
stream: true
|
|
117
|
+
stream: true,
|
|
118
|
+
// Not every OpenAI-compatible provider accepts stream_options;
|
|
119
|
+
// usage tracking is therefore opt-in only.
|
|
120
|
+
...(this.config?.includeUsage ? { stream_options: { include_usage: true } } : {})
|
|
121
|
+
}), {
|
|
122
|
+
onRetry: (err, nextAttempt, delayMs) => {
|
|
123
|
+
spinner.text = `API error (${err.message}); retrying in ${Math.round(delayMs / 1000)}s (attempt ${nextAttempt})...`;
|
|
124
|
+
}
|
|
72
125
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
spinner.fail('Error during processing');
|
|
129
|
+
if (!this.jsonMode)
|
|
130
|
+
console.error(chalk.red(error.message));
|
|
131
|
+
status = 'error';
|
|
132
|
+
errorMessage = error.message;
|
|
133
|
+
active = false;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
let content = '';
|
|
137
|
+
let reasoningContent = '';
|
|
138
|
+
let toolCalls = [];
|
|
139
|
+
let contentStarted = false;
|
|
140
|
+
let reasoningStarted = false;
|
|
141
|
+
const toolNamesSeen = new Set();
|
|
142
|
+
try {
|
|
79
143
|
for await (const chunk of stream) {
|
|
144
|
+
if (chunk.usage) {
|
|
145
|
+
sawUsage = true;
|
|
146
|
+
totalUsage.prompt_tokens += chunk.usage.prompt_tokens ?? 0;
|
|
147
|
+
totalUsage.completion_tokens += chunk.usage.completion_tokens ?? 0;
|
|
148
|
+
totalUsage.total_tokens += chunk.usage.total_tokens ?? 0;
|
|
149
|
+
}
|
|
80
150
|
const delta = chunk.choices[0]?.delta;
|
|
81
151
|
// Handle reasoning/thinking content (e.g., DeepSeek)
|
|
82
152
|
if (delta?.reasoning_content) {
|
|
83
153
|
if (!reasoningStarted) {
|
|
84
154
|
spinner.stop();
|
|
85
|
-
|
|
155
|
+
if (!this.jsonMode) {
|
|
156
|
+
process.stdout.write(chalk.dim('\n[Thinking] '));
|
|
157
|
+
}
|
|
86
158
|
reasoningStarted = true;
|
|
87
159
|
}
|
|
88
|
-
|
|
160
|
+
if (!this.jsonMode) {
|
|
161
|
+
process.stdout.write(chalk.dim(delta.reasoning_content));
|
|
162
|
+
}
|
|
89
163
|
reasoningContent += delta.reasoning_content;
|
|
90
164
|
}
|
|
91
165
|
// Handle regular content
|
|
92
166
|
if (delta?.content) {
|
|
93
167
|
if (!contentStarted) {
|
|
94
168
|
spinner.stop();
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
169
|
+
if (!this.jsonMode) {
|
|
170
|
+
if (reasoningStarted)
|
|
171
|
+
process.stdout.write('\n');
|
|
172
|
+
process.stdout.write(chalk.blue("AutoClaw: "));
|
|
173
|
+
}
|
|
98
174
|
contentStarted = true;
|
|
99
175
|
}
|
|
100
|
-
|
|
176
|
+
if (!this.jsonMode) {
|
|
177
|
+
process.stdout.write(delta.content);
|
|
178
|
+
}
|
|
101
179
|
content += delta.content;
|
|
102
180
|
}
|
|
103
|
-
// Handle tool calls
|
|
181
|
+
// Handle tool calls
|
|
104
182
|
if (delta?.tool_calls) {
|
|
105
183
|
for (const tc of delta.tool_calls) {
|
|
106
184
|
const idx = tc.index;
|
|
@@ -113,45 +191,76 @@ RULES OF ENGAGEMENT:
|
|
|
113
191
|
toolCalls[idx].function.name += tc.function.name;
|
|
114
192
|
if (tc.function?.arguments)
|
|
115
193
|
toolCalls[idx].function.arguments += tc.function.arguments;
|
|
116
|
-
// Show tool name as soon as it's complete
|
|
117
194
|
if (tc.function?.name && !toolNamesSeen.has(idx)) {
|
|
118
195
|
toolNamesSeen.add(idx);
|
|
119
196
|
spinner.stop();
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
197
|
+
if (!this.jsonMode) {
|
|
198
|
+
if (contentStarted)
|
|
199
|
+
process.stdout.write('\n');
|
|
200
|
+
if (reasoningStarted && !contentStarted)
|
|
201
|
+
process.stdout.write('\n');
|
|
202
|
+
process.stdout.write(chalk.cyan(`[Calling] ${tc.function.name}\n`));
|
|
203
|
+
}
|
|
125
204
|
}
|
|
126
205
|
}
|
|
127
206
|
}
|
|
128
207
|
}
|
|
129
|
-
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
spinner.fail('Error during processing');
|
|
211
|
+
if (!this.jsonMode)
|
|
212
|
+
console.error(chalk.red(error.message));
|
|
213
|
+
status = 'error';
|
|
214
|
+
errorMessage = error.message;
|
|
215
|
+
active = false;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (!this.jsonMode) {
|
|
219
|
+
if (reasoningStarted)
|
|
130
220
|
console.log(); // newline after reasoning
|
|
131
|
-
|
|
132
|
-
if (contentStarted) {
|
|
221
|
+
if (contentStarted)
|
|
133
222
|
console.log(); // newline after streamed content
|
|
134
|
-
|
|
135
|
-
if (!reasoningStarted && !contentStarted) {
|
|
223
|
+
if (!reasoningStarted && !contentStarted)
|
|
136
224
|
spinner.stop();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
225
|
+
}
|
|
226
|
+
// Build the full message for history
|
|
227
|
+
const message = { role: "assistant" };
|
|
228
|
+
if (content)
|
|
229
|
+
message.content = content;
|
|
230
|
+
if (reasoningContent)
|
|
231
|
+
message.reasoning_content = reasoningContent;
|
|
232
|
+
if (toolCalls.length > 0) {
|
|
233
|
+
message.tool_calls = toolCalls;
|
|
234
|
+
message.content = message.content || null;
|
|
235
|
+
}
|
|
236
|
+
this.messages.push(message);
|
|
237
|
+
if (content)
|
|
238
|
+
lastContent = content;
|
|
239
|
+
if (toolCalls.length > 0) {
|
|
240
|
+
for (const toolCall of toolCalls) {
|
|
241
|
+
if (toolCall.type !== 'function')
|
|
242
|
+
continue;
|
|
243
|
+
const functionName = toolCall.function.name;
|
|
244
|
+
let functionArgs;
|
|
245
|
+
try {
|
|
246
|
+
functionArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
247
|
+
}
|
|
248
|
+
catch (parseError) {
|
|
249
|
+
// Feed the failure back so the model can correct itself next turn
|
|
250
|
+
if (!this.jsonMode) {
|
|
251
|
+
console.log(chalk.red(`\n[Tool] ${functionName} — malformed arguments (not valid JSON)`));
|
|
252
|
+
}
|
|
253
|
+
this.messages.push({
|
|
254
|
+
role: "tool",
|
|
255
|
+
tool_call_id: toolCall.id,
|
|
256
|
+
content: `Error: arguments for ${functionName} were not valid JSON (${parseError.message}). Re-issue the tool call with well-formed JSON arguments.`
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (this.jsonMode) {
|
|
261
|
+
this.emitEvent({ event: 'tool_call', step, tool: functionName, args: functionArgs });
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
155
264
|
// Display tool call info
|
|
156
265
|
console.log(chalk.cyan(`\n[Tool] ${functionName}`));
|
|
157
266
|
const argsStr = JSON.stringify(functionArgs, null, 2);
|
|
@@ -163,56 +272,82 @@ RULES OF ENGAGEMENT:
|
|
|
163
272
|
else {
|
|
164
273
|
console.log(chalk.dim(argsStr));
|
|
165
274
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
275
|
+
}
|
|
276
|
+
let toolResult;
|
|
277
|
+
try {
|
|
278
|
+
const run = () => executeToolHandler(functionName, functionArgs, this.config);
|
|
279
|
+
toolResult = this.jsonMode ? await this.runToolQuietly(run) : await run();
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
toolResult = `Error: ${err.message}`;
|
|
283
|
+
}
|
|
284
|
+
// Bound what goes back into the model context; the full output is
|
|
285
|
+
// kept on disk for /view.
|
|
286
|
+
const MAX_PREVIEW_LINES = 20;
|
|
287
|
+
const truncation = truncateOutput(toolResult);
|
|
288
|
+
const boundedResult = truncation.content;
|
|
289
|
+
const resultLines = boundedResult.split('\n');
|
|
290
|
+
let outputFile = null;
|
|
291
|
+
if (resultLines.length > MAX_PREVIEW_LINES || truncation.truncated) {
|
|
292
|
+
outputFile = await this.saveOutput(functionName, toolResult);
|
|
293
|
+
this.lastOutputFile = outputFile;
|
|
294
|
+
if (!this.jsonMode) {
|
|
295
|
+
console.log(chalk.green(`[Result]`));
|
|
182
296
|
console.log(resultLines.slice(0, MAX_PREVIEW_LINES).join('\n'));
|
|
183
297
|
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 });
|
|
298
|
+
if (remaining > 0) {
|
|
299
|
+
console.log(chalk.dim(`\n ... ${remaining} more lines (${resultLines.length} lines total)`));
|
|
189
300
|
}
|
|
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
301
|
console.log(chalk.dim(` Type '/view' to see full output`));
|
|
195
302
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
303
|
+
}
|
|
304
|
+
else if (!this.jsonMode) {
|
|
305
|
+
console.log(chalk.green(`[Result]`));
|
|
306
|
+
console.log(boundedResult);
|
|
307
|
+
this.lastOutputFile = null;
|
|
308
|
+
}
|
|
309
|
+
if (this.jsonMode) {
|
|
310
|
+
this.emitEvent({
|
|
311
|
+
event: 'tool_result',
|
|
312
|
+
step,
|
|
313
|
+
tool: functionName,
|
|
314
|
+
truncated: truncation.truncated,
|
|
315
|
+
bytes: truncation.totalBytes,
|
|
316
|
+
...(outputFile ? { output_file: outputFile } : {})
|
|
204
317
|
});
|
|
205
318
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
319
|
+
this.messages.push({
|
|
320
|
+
role: "tool",
|
|
321
|
+
tool_call_id: toolCall.id,
|
|
322
|
+
content: boundedResult
|
|
323
|
+
});
|
|
209
324
|
}
|
|
210
325
|
}
|
|
211
|
-
|
|
212
|
-
spinner.fail('Error during processing');
|
|
213
|
-
console.error(chalk.red(error.message));
|
|
326
|
+
else {
|
|
214
327
|
active = false;
|
|
215
328
|
}
|
|
329
|
+
if (sawUsage) {
|
|
330
|
+
this.emitEvent({ event: 'usage', step, ...totalUsage });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const result = {
|
|
334
|
+
status,
|
|
335
|
+
steps: step,
|
|
336
|
+
message: lastContent,
|
|
337
|
+
...(errorMessage ? { error: errorMessage } : {}),
|
|
338
|
+
...(sawUsage ? { usage: totalUsage } : {})
|
|
339
|
+
};
|
|
340
|
+
this.emitEvent({ event: 'run_end', ...result });
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
async saveOutput(functionName, toolResult) {
|
|
344
|
+
const outputDir = path.join(os.homedir(), '.autoclaw', 'output');
|
|
345
|
+
if (!fs.existsSync(outputDir)) {
|
|
346
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
216
347
|
}
|
|
348
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
349
|
+
const outputFile = path.join(outputDir, `${functionName}_${ts}.txt`);
|
|
350
|
+
fs.writeFileSync(outputFile, toolResult, 'utf-8');
|
|
351
|
+
return outputFile;
|
|
217
352
|
}
|
|
218
353
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import inquirer from 'inquirer';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import dotenv from 'dotenv';
|
|
6
6
|
import { Agent } from './agent.js';
|
|
7
|
+
import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
|
|
7
8
|
import * as fs from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import * as os from 'os';
|
|
@@ -42,7 +43,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
42
43
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
43
44
|
// In dist/index.js, package.json is usually up one level in the root
|
|
44
45
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
45
|
-
let version = '1.0
|
|
46
|
+
let version = '1.2.0';
|
|
46
47
|
try {
|
|
47
48
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
48
49
|
version = pkg.version;
|
|
@@ -56,8 +57,10 @@ program
|
|
|
56
57
|
.description('A lightweight AI agent CLI tool')
|
|
57
58
|
.version(version)
|
|
58
59
|
.option('-m, --model <model>', 'Model to use')
|
|
60
|
+
.option('-P, --provider <name>', 'Use a provider preset (openai, deepseek, moonshot, dashscope, zhipu, openrouter, ollama)')
|
|
59
61
|
.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)')
|
|
62
|
+
.option('-y, --yes', 'Auto-confirm all tool executions (e.g., shell commands)')
|
|
63
|
+
.option('--json', 'Emit NDJSON events on stdout (for orchestrators; use with -n)');
|
|
61
64
|
program
|
|
62
65
|
.command('setup')
|
|
63
66
|
.description('Run the interactive setup wizard to configure API keys')
|
|
@@ -92,6 +95,20 @@ async function runSetup(options = {}) {
|
|
|
92
95
|
return '******';
|
|
93
96
|
return `${secret.slice(0, 3)}...${secret.slice(-4)}`;
|
|
94
97
|
}
|
|
98
|
+
const providerAnswer = await inquirer.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: 'list',
|
|
101
|
+
name: 'provider',
|
|
102
|
+
message: 'Select your LLM provider:',
|
|
103
|
+
choices: [
|
|
104
|
+
...providerNames().map((name) => ({ name: `${PROVIDER_PRESETS[name].label} (${name})`, value: name })),
|
|
105
|
+
{ name: 'Custom OpenAI-compatible endpoint', value: 'custom' }
|
|
106
|
+
],
|
|
107
|
+
default: currentConfig.provider || 'openai'
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
const provider = providerAnswer.provider;
|
|
111
|
+
const preset = resolveProvider(provider === 'custom' ? undefined : provider);
|
|
95
112
|
const answers = await inquirer.prompt([
|
|
96
113
|
{
|
|
97
114
|
type: 'password',
|
|
@@ -112,13 +129,13 @@ async function runSetup(options = {}) {
|
|
|
112
129
|
type: 'input',
|
|
113
130
|
name: 'baseUrl',
|
|
114
131
|
message: 'Enter API Base URL:',
|
|
115
|
-
default: currentConfig.baseUrl || 'https://api.openai.com/v1'
|
|
132
|
+
default: currentConfig.baseUrl || preset?.baseUrl || 'https://api.openai.com/v1'
|
|
116
133
|
},
|
|
117
134
|
{
|
|
118
135
|
type: 'input',
|
|
119
136
|
name: 'model',
|
|
120
137
|
message: 'Enter default Model:',
|
|
121
|
-
default: currentConfig.model || 'gpt-
|
|
138
|
+
default: currentConfig.model || preset?.defaultModel || 'gpt-5.6'
|
|
122
139
|
},
|
|
123
140
|
{
|
|
124
141
|
type: 'confirm',
|
|
@@ -319,6 +336,7 @@ async function runSetup(options = {}) {
|
|
|
319
336
|
apiKey: finalApiKey,
|
|
320
337
|
baseUrl: answers.baseUrl,
|
|
321
338
|
model: answers.model,
|
|
339
|
+
provider: provider === 'custom' ? undefined : provider,
|
|
322
340
|
...imageConfig,
|
|
323
341
|
...emailConfig,
|
|
324
342
|
...searchConfig,
|
|
@@ -351,18 +369,31 @@ async function runChat(queryParts, options) {
|
|
|
351
369
|
// 3. Merge Configs for Tool Usage
|
|
352
370
|
// Priority: Local > Global
|
|
353
371
|
const fullConfig = { ...globalConfig, ...localConfig };
|
|
354
|
-
// 4. Resolve
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
372
|
+
// 4. Resolve Provider Preset (CLI > Env > Config)
|
|
373
|
+
const providerName = options.provider || process.env.AUTOCLOW_PROVIDER || fullConfig.provider;
|
|
374
|
+
const preset = resolveProvider(providerName);
|
|
375
|
+
if (providerName && !preset) {
|
|
376
|
+
console.log(chalk.yellow(`Unknown provider '${providerName}'. Known providers: ${providerNames().join(', ')}`));
|
|
377
|
+
}
|
|
378
|
+
// 5. Resolve Env Vars (CLI > Env > Config > Provider preset)
|
|
379
|
+
let apiKey = process.env.OPENAI_API_KEY || fullConfig.apiKey || (preset?.apiKeyEnv ? process.env[preset.apiKeyEnv] : undefined);
|
|
380
|
+
let baseURL = process.env.OPENAI_BASE_URL || fullConfig.baseUrl || preset?.baseUrl;
|
|
381
|
+
let model = options.model || process.env.OPENAI_MODEL || fullConfig.model || preset?.defaultModel || 'gpt-5.6';
|
|
358
382
|
// Inject Runtime Flags
|
|
359
383
|
fullConfig.autoConfirm = options.yes;
|
|
384
|
+
fullConfig.jsonMode = !!options.json;
|
|
385
|
+
// Usage tracking is opt-in: not every OpenAI-compatible provider accepts
|
|
386
|
+
// stream_options.include_usage, so never force it on.
|
|
387
|
+
fullConfig.includeUsage =
|
|
388
|
+
!!fullConfig.includeUsage ||
|
|
389
|
+
process.env.AUTOCLOW_INCLUDE_USAGE === '1' ||
|
|
390
|
+
process.env.AUTOCLOW_INCLUDE_USAGE === 'true';
|
|
360
391
|
// Inject Env vars
|
|
361
392
|
if (process.env.SMTP_HOST)
|
|
362
393
|
fullConfig.smtpHost = process.env.SMTP_HOST;
|
|
363
394
|
if (process.env.SMTP_PORT)
|
|
364
395
|
fullConfig.smtpPort = process.env.SMTP_PORT;
|
|
365
|
-
if (process.env.
|
|
396
|
+
if (process.env.SMTP_USER)
|
|
366
397
|
fullConfig.smtpUser = process.env.SMTP_USER;
|
|
367
398
|
if (process.env.SMTP_PASS)
|
|
368
399
|
fullConfig.smtpPass = process.env.SMTP_PASS;
|
|
@@ -393,9 +424,10 @@ async function runChat(queryParts, options) {
|
|
|
393
424
|
if (doSetup) {
|
|
394
425
|
await runSetup();
|
|
395
426
|
const newConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
427
|
+
const setupPreset = resolveProvider(newConfig.provider);
|
|
428
|
+
apiKey = newConfig.apiKey || (setupPreset?.apiKeyEnv ? process.env[setupPreset.apiKeyEnv] : undefined);
|
|
429
|
+
baseURL = newConfig.baseUrl || setupPreset?.baseUrl;
|
|
430
|
+
model = options.model || newConfig.model || setupPreset?.defaultModel || 'gpt-5.6';
|
|
399
431
|
Object.assign(fullConfig, newConfig);
|
|
400
432
|
}
|
|
401
433
|
else {
|
|
@@ -417,10 +449,11 @@ async function runChat(queryParts, options) {
|
|
|
417
449
|
if (options.interactive) {
|
|
418
450
|
console.log(chalk.blue("\nProcessing initial request: ") + chalk.bold(initialQuery));
|
|
419
451
|
}
|
|
420
|
-
await agent.chat(initialQuery);
|
|
421
|
-
// Headless mode exit
|
|
452
|
+
const result = await agent.chat(initialQuery);
|
|
453
|
+
// Headless mode exit — the exit code is the orchestrator-facing outcome:
|
|
454
|
+
// 0 completed, 1 hard failure, 2 step cap reached (task unfinished).
|
|
422
455
|
if (!options.interactive) {
|
|
423
|
-
process.exit(0);
|
|
456
|
+
process.exit(result.status === 'completed' ? 0 : result.status === 'max_steps' ? 2 : 1);
|
|
424
457
|
}
|
|
425
458
|
}
|
|
426
459
|
// 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.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
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",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"@types/node": "^25.2.1",
|
|
66
67
|
"@types/nodemailer": "^7.0.9",
|
|
67
68
|
"ts-node": "^10.9.2",
|
|
68
|
-
"typescript": "^5.9.3"
|
|
69
|
+
"typescript": "^5.9.3",
|
|
70
|
+
"vitest": "^4.1.4"
|
|
69
71
|
}
|
|
70
72
|
}
|