autoclaw 1.3.4 → 1.3.6
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 +356 -291
- package/README.zh-CN.md +69 -4
- package/dist/agent.js +47 -2
- package/dist/index.js +75 -1
- package/dist/sandbox.js +94 -0
- package/dist/shell.js +55 -32
- package/dist/skills.js +276 -0
- package/dist/tools/background.js +165 -0
- package/dist/tools/core.js +17 -1
- package/dist/tools/index.js +9 -1
- package/dist/tools/render-image.js +135 -0
- package/dist/tools/render-pdf.js +111 -0
- package/dist/tools/takumi-fonts.js +61 -0
- package/dist/zip.js +149 -0
- package/package.json +5 -2
- package/skills/code2media/SKILL.md +82 -0
- package/skills/code2media/references/syntax-guide.md +63 -0
- package/skills/code2media/scripts/package.json +10 -0
- package/skills/code2media/scripts/render.mjs +177 -0
- package/skills/code2media/templates/animation.html +19 -0
- package/skills/code2media/templates/badge.html +6 -0
- package/skills/code2media/templates/certificate.html +9 -0
- package/skills/code2media/templates/metrics-card.html +25 -0
- package/skills/code2media/templates/weekly-report.html +86 -0
- package/skills/invoice-maker/SKILL.md +65 -0
- package/skills/invoice-maker/references/syntax-guide.md +63 -0
- package/skills/invoice-maker/scripts/package.json +10 -0
- package/skills/invoice-maker/scripts/render.mjs +177 -0
- package/skills/invoice-maker/templates/invoice.html +26 -0
- package/skills/invoice-maker/templates/quote.html +68 -0
- package/skills/poster-maker/SKILL.md +62 -0
- package/skills/poster-maker/references/syntax-guide.md +63 -0
- package/skills/poster-maker/scripts/package.json +10 -0
- package/skills/poster-maker/scripts/render.mjs +177 -0
- package/skills/poster-maker/templates/cover.html +13 -0
- package/skills/poster-maker/templates/og-card.html +17 -0
- package/skills/poster-maker/templates/social-post.html +14 -0
package/README.zh-CN.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/autoclaw)
|
|
5
5
|
[](https://github.com/tsingliuwin/autoclaw)
|
|
6
6
|
[](https://github.com/tsingliuwin/autoclaw/blob/main/LICENSE)
|
|
7
|
+
[](./SAFETY.md)
|
|
7
8
|
[](http://makeapullrequest.com)
|
|
8
9
|
|
|
9
10
|
**稳定、高工程化、易规模化:专为无界面系统设计的高效自动化 Agent 框架。**
|
|
@@ -34,11 +35,14 @@ AutoClaw 是一款针对 **“无界面系统” (Headless Systems)** 的高稳
|
|
|
34
35
|
- 📜 **无头执行 (Headless Execution)**: 无需图形界面,纯终端运行。核心流程仅依赖 Shell 与文件操作;可选的网页工具在无头 Chromium 中运行。
|
|
35
36
|
- 🤖 **非交互模式**: 支持自动化标志(`-y`, `--no-interactive`),完美适配零干预的自动化流程。
|
|
36
37
|
- 📂 **全方位控制 (Universal Control)**: 从基础的文件 I/O 到复杂的系统管理。
|
|
37
|
-
-
|
|
38
|
+
- 🖥️ **后台进程**: 后台启动长期命令(开发服务器、watcher)并轮询输出,不阻塞任务运行。
|
|
39
|
+
- 🛡️ **安全护栏**: 破坏性命令闸、凭据文件防线、步数上限、墙钟超时与 API 重试——为没有人看着的机器而设计。
|
|
38
40
|
- 🧠 **上下文感知 (Context Aware)**: 提供精确的操作系统与时间上下文,正确处理"今天"、"下周一"等相对时间。
|
|
39
41
|
- 🌐 **网页搜索**: 集成 Tavily,支持实时信息检索。
|
|
40
42
|
- 🌍 **网页阅读与截图**: 提取文章正文、截取页面图片(需先执行 `npx playwright install chromium`)。
|
|
41
43
|
- 🎨 **图像生成**: 通过任意 OpenAI 兼容的图像接口生成图片(兼容 DALL-E)。
|
|
44
|
+
- 🖼️ **确定性图像渲染** (`render_image`): HTML + Tailwind 模板直接渲染为 PNG/JPEG/WebP/SVG,并支持动图(由 CSS `@keyframes` 采样的动画 WebP/GIF/APNG)。完全离线、无浏览器、毫秒级渲染——适合 OG 分享卡、横幅、徽章、数据卡片等对文字与排版精度有要求的场景。
|
|
45
|
+
- 📄 **PDF 渲染** (`render_pdf`): HTML 模板直接渲染为分页 PDF,文字可选中、页眉页脚逐页重复、支持页码计数。完全离线、无浏览器——适合发票、报表、证书等结构化文档。
|
|
42
46
|
- 🕒 **时间精准**: 内置工具获取精确系统日期和时间,确保正确的时间上下文。
|
|
43
47
|
- 📧 **通讯能力**: 自动发送电子邮件并将通知推送至聊天群组。
|
|
44
48
|
|
|
@@ -49,6 +53,7 @@ AutoClaw 是一款针对 **“无界面系统” (Headless Systems)** 的高稳
|
|
|
49
53
|
- **UI**: Inquirer (交互), Chalk (样式), Ora (加载动画)
|
|
50
54
|
- **AI**: OpenAI SDK(任意 OpenAI 兼容端点:DeepSeek、Kimi、Qwen、GLM、Ollama 等)
|
|
51
55
|
- **网页工具**: Playwright(无头 Chromium,用于 `read_website` / `take_screenshot`)
|
|
56
|
+
- **渲染引擎**: Takumi(Rust 引擎,经原生绑定驱动 `render_image` / `render_pdf`,无需浏览器)
|
|
52
57
|
|
|
53
58
|
## 安装
|
|
54
59
|
|
|
@@ -131,6 +136,22 @@ autoclaw batch big.jsonl -y -c 4 # 最多 4 个任务并行
|
|
|
131
136
|
|
|
132
137
|
AutoClaw 同时会自动给提示词瘦身:可选工具(网页搜索、邮件、群通知、图像生成)只在凭据配置后才会注册进工具定义;长循环中较早的工具结果会被替换为短摘要。
|
|
133
138
|
|
|
139
|
+
### 技能(可移植能力包)
|
|
140
|
+
AutoClaw 支持 `SKILL.md` 技能包——与 WorkBuddy 技能商店相同的格式,一份技能包既能在 AutoClaw 内运行,也能发布到其它平台。system prompt 里每个技能只占一行清单;任务匹配时 agent 才去读取该技能的 `SKILL.md` 并照做,全程走普通的文件与 shell 工具。技能没有特权运行时:脚本同样经过破坏性命令闸、沙箱与步数上限。
|
|
141
|
+
|
|
142
|
+
作用域(同名后者遮蔽前者):内置 `skills/`(随 npm 包发布)→ `~/.autoclaw/skills/` → `.autoclaw/skills/`。
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
autoclaw skill list # 列出发现的技能(含作用域与版本)
|
|
146
|
+
autoclaw skill install <zip|目录|https地址> # 安装到 ~/.autoclaw/skills/(含 zip-slip 防护)
|
|
147
|
+
autoclaw skill remove <name> # 移除用户级技能(内置技能受保护)
|
|
148
|
+
autoclaw skill pack <目录> # 打包为商店上传 zip(zip 根为 skills/<name>/)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
安装兼容任意 SKILL.md 格式的第三方包:本地目录、本地 zip 或 https 下载地址均可。对第三方布局差异做了容错(SKILL.md 位于 zip 根部、普通文件夹、`skills/<name>/` 包装、macOS 的 `__MACOSX`/`.DS_Store` 垃圾文件),并且始终按技能 frontmatter 的 `name` 安装,保证发现与清单的一致性。
|
|
152
|
+
|
|
153
|
+
内置三个技能,分层协作:[`code2media`](skills/code2media/SKILL.md)(代码转多媒体)是通用渲染引擎——独立 Node 脚本把任意 HTML 变成图片/SVG/分页 PDF/动图;[`poster-maker`](skills/poster-maker/SKILL.md)(海报生成器)与 [`invoice-maker`](skills/invoice-maker/SKILL.md)(发票生成器)是独立优化的场景技能,各自沉淀了平台尺寸表、票据版式规范与质量清单。同一个 zip 可直接发布到任何兼容 SKILL.md 的商店。技能与 batch 模式天然组合:清单里一行 `{"id":"cert-042","task":"用 invoice-maker 技能根据 orders-042.json 生成发票 invoices/042.pdf"}` 就能让一个隔离的 swarm worker 跑同一个技能。
|
|
154
|
+
|
|
134
155
|
### 实战配方
|
|
135
156
|
|
|
136
157
|
Linux 定时巡检(crontab):
|
|
@@ -170,6 +191,14 @@ autoclaw "将 src/index.ts 重构为使用 ES 模块" -y
|
|
|
170
191
|
|
|
171
192
|
### 诊断
|
|
172
193
|
`autoclaw doctor` 无头完成全面自检,逐项打印 ✓/✗:配置文件、解析出的 provider/baseUrl/model、API key、真实连接测试、解析出的 shell、已注册工具、playwright 浏览器状态。退出 `0` = 就绪,`1` = 有关键项失败(会打印是哪项)。适合 CI 或新机器。
|
|
194
|
+
|
|
195
|
+
### 沙箱
|
|
196
|
+
命令执行可通过 `config.sandbox` / `AUTOCLOW_SANDBOX` 加以约束(词汇借鉴自 DeepSeek Harness):
|
|
197
|
+
- `danger-full-access`(默认):命令不受约束地运行。
|
|
198
|
+
- `workspace-write`:命令只能在当前工作目录和 `/tmp` 内写入。
|
|
199
|
+
- `read-only`:命令无法在任何位置写入。
|
|
200
|
+
|
|
201
|
+
后端:Linux 使用 bubblewrap(`apt install bubblewrap`),macOS 使用 `sandbox-exec`。**Windows 暂无后端**——非默认模式会"失败关闭"(直接拒绝命令并给出明确错误),而不是假装约束;在 Windows 上请暂用 `danger-full-access`。此词汇不约束读取与网络。
|
|
173
202
|
- `--json`: 在 stdout 输出 NDJSON 事件流 (供编排器使用,配合 `-n`)。
|
|
174
203
|
|
|
175
204
|
### Provider 预设
|
|
@@ -197,6 +226,8 @@ AutoClaw 使用层级配置系统。
|
|
|
197
226
|
- `maxSteps`: 单任务最大 LLM 轮数,超出后自动停止 (默认: `25`)。
|
|
198
227
|
- `shellTimeout`: Shell 命令超时时间(毫秒)(默认: `120000`)。
|
|
199
228
|
- `taskTimeoutMs`: 单任务整体墙钟超时(毫秒,默认关闭;会中断进行中的 API 调用并以 `timeout` 状态停止)。
|
|
229
|
+
- `sandbox`: 约束 shell 命令(`read-only`、`workspace-write`、`danger-full-access`;默认 `danger-full-access`)。
|
|
230
|
+
- `skillsEnabled`: 设为 `false` 关闭技能系统(默认开启)。
|
|
200
231
|
- `shell`: 强制 `execute_shell_command` 使用的 shell (`bash`、`powershell`、`cmd`、`sh`;默认自动检测——Windows 上优先 Git Bash > PowerShell > cmd)。
|
|
201
232
|
- `tavilyApiKey`: Tavily 网页搜索的 API 密钥。
|
|
202
233
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP 邮件设置。
|
|
@@ -218,6 +249,7 @@ AutoClaw 使用层级配置系统。
|
|
|
218
249
|
- `AUTOCLOW_PROVIDER`: 未传 `-P` 时使用的 provider 预设。
|
|
219
250
|
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: 稳定性限制(单任务最大轮数;Shell 超时毫秒数)。
|
|
220
251
|
- `AUTOCLOW_TASK_TIMEOUT_MS`: 单任务整体墙钟超时(毫秒)。
|
|
252
|
+
- `AUTOCLOW_SANDBOX`: 约束 shell 命令(`read-only`、`workspace-write`、`danger-full-access`)。
|
|
221
253
|
- `AUTOCLOW_SHELL`: 强制 shell 命令使用的 shell (`bash`、`powershell`、`cmd`、`sh`)。
|
|
222
254
|
- `AUTOCLOW_INCLUDE_USAGE`: 设为 `1`/`true` 时向 API 请求 token 用量(可选开启)。
|
|
223
255
|
- `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: 工具凭据,可作为 setup 的替代方式。
|
|
@@ -240,6 +272,39 @@ AutoClaw 使用层级配置系统。
|
|
|
240
272
|
内置工具为 Agent 提供当前系统时间,确保准确处理相对时间请求。
|
|
241
273
|
- **示例**: "今天是几号?" 或 "提醒我下周一检查日志。"
|
|
242
274
|
|
|
275
|
+
### 确定性渲染 (Takumi)
|
|
276
|
+
`render_image` 将 HTML 模板渲染为精确的图像——PNG、JPEG、WebP 或矢量 SVG——全程离线,不依赖浏览器或 AI 模型。`render_pdf` 将 HTML 模板渲染为分页 PDF,文字可选中,页眉/页脚逐页重复,并支持 `<span class="pageNumber">` / `<span class="totalPages">` 页码计数。模板样式支持内联 CSS、`<style>` 块,或通过 `tw` 属性使用 Tailwind v4 工具类(`<div tw="w-full h-full bg-blue-500">`);普通 `class` 属性仅匹配常规 CSS 选择器。两个工具都会自动探测常见系统字体(含中日韩与 Emoji);也可通过 `font_paths` 注册指定字体文件。
|
|
277
|
+
|
|
278
|
+
典型工作流——用自然语言描述任务,模板由 agent 自己编写:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# 博客 SEO:为每篇文章生成 OG 分享图
|
|
282
|
+
autoclaw "读取 content/posts/ 下每篇 .md 的标题和摘要,为每篇文章渲染一张 OG 分享图(1200x630)到 public/og/" -y -n
|
|
283
|
+
|
|
284
|
+
# 财务/电商:从订单表批量生成 PDF 发票并邮件发送
|
|
285
|
+
autoclaw "读取 orders.csv,为每个订单生成 PDF 发票保存到 invoices/(A4,页脚带页码),然后把每张发票邮件发送给该行记录的客户邮箱" -y
|
|
286
|
+
|
|
287
|
+
# 培训/HR:为学员名单批量生成结业证书
|
|
288
|
+
autoclaw "读取 attendees.json,为每位学员渲染一张结业证书(1414x1000)保存到 certs/,编号从 AC-2026-0001 起" -y -n
|
|
289
|
+
|
|
290
|
+
# cron/CI 定时报告:输出确定——相同输入得到完全相同的 PDF,可做校验
|
|
291
|
+
autoclaw "汇总本周 nginx 访问日志,生成一页 A4 的 PDF 流量周报(含指标表格),保存为 report.pdf" -y -n
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
集群规模用 batch 模式——每个任务在隔立的 agent 中渲染:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
cat > render-jobs.jsonl <<'EOF'
|
|
298
|
+
{"id": "og-001", "task": "为 post-001.md 渲染 OG 分享图到 public/og/001.png"}
|
|
299
|
+
{"id": "og-002", "task": "为 post-002.md 渲染 OG 分享图到 public/og/002.png"}
|
|
300
|
+
EOF
|
|
301
|
+
autoclaw batch render-jobs.jsonl -y -c 4
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
选型提示:需要精确文字、排版与品牌一致性(卡片、横幅、徽章、文档)时用 `render_image` / `render_pdf`;艺术创作、照片类图像用 `generate_image`。模板中的 Emoji 默认从 Twemoji CDN 在线获取,完全离线的环境请让模板保持纯文本。
|
|
305
|
+
|
|
306
|
+
可运行案例与渲染效果预览:[examples/render](examples/render/README.zh-CN.md)(OG 分享卡、社媒海报、KPI 指标卡、周报 PDF、SVG 徽章、证书、动图、多页采购订单——`agent-run/` 下还有一次真实 agent 无头运行的产物)。同一能力也打包成了可移植的 [WorkBuddy 技能](skills/code2media/SKILL.md)(`code2media-skill.zip`):一个独立 Node 脚本,任何装有 Node >= 20.19 的机器都能把 HTML 渲染成图片/SVG/PDF/动图。
|
|
307
|
+
|
|
243
308
|
## Docker 支持
|
|
244
309
|
|
|
245
310
|
### 构建与运行
|
|
@@ -250,10 +315,10 @@ docker run --rm -v "$PWD":/workspace -w /workspace -e OPENAI_API_KEY=sk-... auto
|
|
|
250
315
|
```
|
|
251
316
|
注意:默认镜像中未内置浏览器,基于浏览器的工具(`read_website` / `take_screenshot`)不可用——它们会返回友好的安装提示,而不是报错崩溃。
|
|
252
317
|
|
|
253
|
-
###
|
|
254
|
-
在 Docker 容器(尤其是 Alpine 或 Debian Slim)中运行时,网页截图中的中文可能会显示为方块("豆腐块")。表情符号(如
|
|
318
|
+
### 截图与渲染输出中的中文显示问题
|
|
319
|
+
在 Docker 容器(尤其是 Alpine 或 Debian Slim)中运行时,网页截图中的中文可能会显示为方块("豆腐块")。表情符号(如 🔥)也可能显示为方块。`render_image` / `render_pdf` 输出中包含中日韩文字时同样受影响。
|
|
255
320
|
|
|
256
|
-
**解决方案:** 在容器中安装 CJK(中日韩)和 Emoji
|
|
321
|
+
**解决方案:** 在容器中安装 CJK(中日韩)和 Emoji 字体。渲染工具会自动探测同一批字体路径,因此安装这些字体包可以同时修复截图与渲染输出的中文显示。
|
|
257
322
|
|
|
258
323
|
**Debian/Ubuntu:**
|
|
259
324
|
```bash
|
package/dist/agent.js
CHANGED
|
@@ -9,8 +9,24 @@ import { getToolDefinitions, executeToolHandler, listUnavailableTools } from './
|
|
|
9
9
|
import { withRetry } from './retry.js';
|
|
10
10
|
import { truncateOutput } from './truncate.js';
|
|
11
11
|
import { buildShellInfo, resolveShellType } from './shell.js';
|
|
12
|
+
import { buildSkillsManifest } from './skills.js';
|
|
12
13
|
const DEFAULT_MAX_STEPS = 25;
|
|
13
14
|
const TOOL_RESULT_TRIM_MARKER = 'older tool output trimmed';
|
|
15
|
+
// Canonical JSON: sorted object keys, so equivalent arguments from
|
|
16
|
+
// different model turns map to the same repeat signature.
|
|
17
|
+
function canonicalArgs(args) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(args, (_key, value) => {
|
|
20
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
21
|
+
return Object.keys(value).sort().reduce((acc, k) => { acc[k] = value[k]; return acc; }, {});
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return String(args);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
export class Agent {
|
|
15
31
|
client;
|
|
16
32
|
messages;
|
|
@@ -51,12 +67,24 @@ System Information:
|
|
|
51
67
|
has('web_search') ? '- Web: web_search — real-time information lookup' : null,
|
|
52
68
|
has('read_website') ? '- Web: read_website — extract article content from a URL' : null,
|
|
53
69
|
has('take_screenshot') ? '- Web: take_screenshot — capture page visuals' : null,
|
|
70
|
+
has('start_background_process') ? '- Processes: start_background_process / check_background_process / stop_background_process — run long-lived commands (servers, watchers) in the background and poll their output' : null,
|
|
54
71
|
has('send_email') ? '- Communication: send_email — SMTP email delivery' : null,
|
|
55
72
|
has('send_notification') ? '- Communication: send_notification — push to Feishu/DingTalk/WeCom' : null,
|
|
56
73
|
has('generate_image') ? '- Creation: generate_image — AI image generation (DALL-E compatible)' : null,
|
|
57
74
|
has('optimize_prompt') ? '- Creation: optimize_prompt — refine raw prompts for creative/complex tasks (recommended before creative work)' : null,
|
|
58
75
|
'- Utility: get_current_datetime — accurate system time for temporal reasoning'
|
|
59
76
|
].filter((line) => line !== null).join('\n');
|
|
77
|
+
// Skills ride along as one-line manifest entries; the body is only read
|
|
78
|
+
// (via read_file) when a task actually matches. Never break startup on
|
|
79
|
+
// a malformed skill directory.
|
|
80
|
+
let skillsManifest = null;
|
|
81
|
+
try {
|
|
82
|
+
skillsManifest = buildSkillsManifest(config);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
skillsManifest = null;
|
|
86
|
+
}
|
|
87
|
+
const skillsBlock = skillsManifest ? `\n${skillsManifest}\n` : '';
|
|
60
88
|
this.messages = [
|
|
61
89
|
{
|
|
62
90
|
role: "system",
|
|
@@ -68,7 +96,7 @@ ${systemInfo}
|
|
|
68
96
|
|
|
69
97
|
WHAT YOU CAN DO:
|
|
70
98
|
${capabilities}
|
|
71
|
-
|
|
99
|
+
${skillsBlock}
|
|
72
100
|
RULES OF ENGAGEMENT:
|
|
73
101
|
1. One shot, not one chat. Produce working results, not conversation. Be terse.
|
|
74
102
|
2. Use the right tool for the job. Shell for system ops. Files for content. Web tools for external info.
|
|
@@ -116,6 +144,8 @@ RULES OF ENGAGEMENT:
|
|
|
116
144
|
let status = 'completed';
|
|
117
145
|
let errorMessage;
|
|
118
146
|
let lastContent = null;
|
|
147
|
+
let lastToolSignature = null;
|
|
148
|
+
let consecutiveRepeats = 0;
|
|
119
149
|
const totalUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
120
150
|
let sawUsage = false;
|
|
121
151
|
this.emitEvent({ event: 'run_start', model: this.model, task: userInput });
|
|
@@ -334,6 +364,21 @@ RULES OF ENGAGEMENT:
|
|
|
334
364
|
const truncation = truncateOutput(toolResult);
|
|
335
365
|
const boundedResult = truncation.content;
|
|
336
366
|
const resultLines = boundedResult.split('\n');
|
|
367
|
+
// Loop hygiene (idea from dsh repeat-tool-reminder): identical
|
|
368
|
+
// consecutive calls get an escalating reminder so the model changes
|
|
369
|
+
// approach early instead of burning turns until the step cap.
|
|
370
|
+
const signature = `${functionName}:${canonicalArgs(functionArgs)}`;
|
|
371
|
+
if (signature === lastToolSignature)
|
|
372
|
+
consecutiveRepeats++;
|
|
373
|
+
else {
|
|
374
|
+
consecutiveRepeats = 1;
|
|
375
|
+
lastToolSignature = signature;
|
|
376
|
+
}
|
|
377
|
+
const repeatSuffix = consecutiveRepeats < 3
|
|
378
|
+
? ''
|
|
379
|
+
: consecutiveRepeats < 5
|
|
380
|
+
? `\n[AutoClaw] Note: this is the ${consecutiveRepeats}th identical ${functionName} call in a row. If it keeps returning the same result, change approach instead of repeating it.`
|
|
381
|
+
: `\n[AutoClaw] You have now made the identical ${functionName} call ${consecutiveRepeats} times in a row (arguments: ${JSON.stringify(functionArgs).slice(0, 120)}). This exact call keeps returning the same result. Stop retrying it: change approach, fix the underlying problem, or finish the task with what you already have.`;
|
|
337
382
|
let outputFile = null;
|
|
338
383
|
if (resultLines.length > MAX_PREVIEW_LINES || truncation.truncated) {
|
|
339
384
|
outputFile = await this.saveOutput(functionName, toolResult);
|
|
@@ -366,7 +411,7 @@ RULES OF ENGAGEMENT:
|
|
|
366
411
|
this.messages.push({
|
|
367
412
|
role: "tool",
|
|
368
413
|
tool_call_id: toolCall.id,
|
|
369
|
-
content: boundedResult
|
|
414
|
+
content: boundedResult + repeatSuffix
|
|
370
415
|
});
|
|
371
416
|
}
|
|
372
417
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { parseManifest, runBatch } from './batch.js';
|
|
|
8
8
|
import { PROVIDER_PRESETS, providerNames, resolveProvider } from './providers.js';
|
|
9
9
|
import { fetchModelIds, normalizeBaseUrl, testConnection } from './setup.js';
|
|
10
10
|
import { collectDoctorChecks } from './doctor.js';
|
|
11
|
+
import { defaultSkillScopes, discoverSkills, installSkill, installSkillFromUrl, packSkill, removeSkill } from './skills.js';
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import * as path from 'path';
|
|
13
14
|
import * as os from 'os';
|
|
@@ -46,7 +47,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
46
47
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
47
48
|
// In dist/index.js, package.json is usually up one level in the root
|
|
48
49
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
49
|
-
let version = '1.3.
|
|
50
|
+
let version = '1.3.6';
|
|
50
51
|
try {
|
|
51
52
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
52
53
|
version = pkg.version;
|
|
@@ -90,6 +91,79 @@ program
|
|
|
90
91
|
const options = program.opts();
|
|
91
92
|
await runBatchCommand(manifest, options, cmdOptions);
|
|
92
93
|
});
|
|
94
|
+
const skillCmd = program
|
|
95
|
+
.command('skill')
|
|
96
|
+
.description('Manage skill packages (SKILL.md format, WorkBuddy-store compatible)');
|
|
97
|
+
skillCmd
|
|
98
|
+
.command('list')
|
|
99
|
+
.description('List discovered skills across builtin / user / project scopes')
|
|
100
|
+
.action(() => {
|
|
101
|
+
const { skills, warnings } = discoverSkills(defaultSkillScopes());
|
|
102
|
+
for (const w of warnings)
|
|
103
|
+
console.log(chalk.dim(` ! ${w}`));
|
|
104
|
+
if (skills.length === 0) {
|
|
105
|
+
console.log('No skills found. Install one with: autoclaw skill install <zip-or-dir>');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(chalk.bold.cyan(`Skills (${skills.length}):\n`));
|
|
109
|
+
for (const s of skills) {
|
|
110
|
+
const desc = s.descriptionZh || s.descriptionEn || s.description;
|
|
111
|
+
console.log(` ${chalk.bold(s.name)}${s.version ? chalk.dim(` v${s.version}`) : ''} ${chalk.dim(`[${s.source}]`)}`);
|
|
112
|
+
console.log(` ${desc.replace(/\s+/g, ' ').slice(0, 100)}${desc.length > 100 ? '…' : ''}`);
|
|
113
|
+
console.log(` ${chalk.dim(s.dir)}`);
|
|
114
|
+
}
|
|
115
|
+
console.log(chalk.dim('\nSkills run inside chat/batch automatically: the agent sees a skill list and reads a matched SKILL.md on demand.'));
|
|
116
|
+
});
|
|
117
|
+
skillCmd
|
|
118
|
+
.command('install <target>')
|
|
119
|
+
.description('Install a skill from an https .zip URL, a local .zip, or a directory (into ~/.autoclaw/skills/)')
|
|
120
|
+
.action(async (target) => {
|
|
121
|
+
try {
|
|
122
|
+
const result = /^https:\/\//.test(target)
|
|
123
|
+
? await installSkillFromUrl(target)
|
|
124
|
+
: installSkill(target);
|
|
125
|
+
console.log(chalk.green(`Installed skill '${result.name}' (${result.files} files) -> ${result.dir}`));
|
|
126
|
+
console.log(chalk.dim('Verify with: autoclaw skill list'));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error(chalk.red(`Error: ${err?.message || err}`));
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
skillCmd
|
|
134
|
+
.command('remove <name>')
|
|
135
|
+
.description('Remove a user-installed skill (~/.autoclaw/skills/)')
|
|
136
|
+
.action((name) => {
|
|
137
|
+
const result = removeSkill(name);
|
|
138
|
+
if (result === 'removed')
|
|
139
|
+
console.log(chalk.green(`Removed skill '${name}'.`));
|
|
140
|
+
else if (result === 'builtin') {
|
|
141
|
+
console.error(chalk.red(`'${name}' is a built-in skill and cannot be removed.`));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
else if (result === 'project') {
|
|
145
|
+
console.error(chalk.red(`'${name}' lives in .autoclaw/skills/ — delete it manually.`));
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.error(chalk.red(`Skill '${name}' not found.`));
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
skillCmd
|
|
154
|
+
.command('pack <dir>')
|
|
155
|
+
.description('Package a skill directory into a store-upload zip (skills/<name>/ at zip root)')
|
|
156
|
+
.option('-o, --output <file>', 'Output zip path (default: <name>-skill.zip in cwd)')
|
|
157
|
+
.action((dir, cmdOptions) => {
|
|
158
|
+
try {
|
|
159
|
+
const result = packSkill(dir, cmdOptions.output);
|
|
160
|
+
console.log(chalk.green(`Packed '${result.name}' (${result.fileCount} files) -> ${result.zipPath}`));
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.error(chalk.red(`Error: ${err?.message || err}`));
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
93
167
|
program
|
|
94
168
|
.command('doctor')
|
|
95
169
|
.description('Diagnose configuration and environment (headless)')
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { shellInvocation } from './shell.js';
|
|
4
|
+
export function resolveSandboxMode(config) {
|
|
5
|
+
const requested = String(config?.sandbox || process.env.AUTOCLOW_SANDBOX || 'danger-full-access').toLowerCase();
|
|
6
|
+
if (requested === 'read-only' || requested === 'workspace-write' || requested === 'danger-full-access') {
|
|
7
|
+
return requested;
|
|
8
|
+
}
|
|
9
|
+
return 'danger-full-access';
|
|
10
|
+
}
|
|
11
|
+
export function sandboxBackend(platform = process.platform) {
|
|
12
|
+
if (platform === 'linux') {
|
|
13
|
+
const available = hasExecutable('bwrap');
|
|
14
|
+
return {
|
|
15
|
+
backend: 'bwrap',
|
|
16
|
+
available,
|
|
17
|
+
detail: available
|
|
18
|
+
? 'bubblewrap found'
|
|
19
|
+
: 'bubblewrap (bwrap) not found — install it (e.g. apt install bubblewrap) or set sandbox to danger-full-access'
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (platform === 'darwin') {
|
|
23
|
+
const available = fs.existsSync('/usr/bin/sandbox-exec');
|
|
24
|
+
return {
|
|
25
|
+
backend: 'sandbox-exec',
|
|
26
|
+
available,
|
|
27
|
+
detail: available ? 'sandbox-exec found' : 'sandbox-exec missing (unexpected on macOS)'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
backend: 'none',
|
|
32
|
+
available: false,
|
|
33
|
+
detail: 'no sandbox backend on this platform yet (Windows restricted-token rung is planned). Set sandbox to danger-full-access to run unrestricted.'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function hasExecutable(name) {
|
|
37
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
38
|
+
try {
|
|
39
|
+
if (fs.existsSync(path.join(dir, name)))
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// skip unreadable PATH entries
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Pure shape builder: wraps the platform shell invocation so the command can
|
|
49
|
+
// only write where the mode allows. danger-full-access returns the command
|
|
50
|
+
// unchanged; read-only/workspace-write require the platform backend, which
|
|
51
|
+
// the caller checks with sandboxBackend() first (fail-closed otherwise).
|
|
52
|
+
export function sandboxedInvocation(command, mode, workspace, platform = process.platform) {
|
|
53
|
+
const base = shellInvocation(command);
|
|
54
|
+
if (mode === 'danger-full-access')
|
|
55
|
+
return base;
|
|
56
|
+
if (platform === 'linux') {
|
|
57
|
+
if (mode === 'read-only') {
|
|
58
|
+
return {
|
|
59
|
+
file: 'bwrap',
|
|
60
|
+
args: ['--ro-bind', '/', '/', '--dev-bind', '/dev', '/dev', '--proc', '/proc', '--', base.file, ...base.args]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
file: 'bwrap',
|
|
65
|
+
args: [
|
|
66
|
+
'--ro-bind', '/', '/',
|
|
67
|
+
'--bind', workspace, workspace,
|
|
68
|
+
'--dev-bind', '/dev', '/dev',
|
|
69
|
+
'--proc', '/proc',
|
|
70
|
+
'--tmpfs', '/tmp',
|
|
71
|
+
'--', base.file, ...base.args
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (platform === 'darwin') {
|
|
76
|
+
const ws = workspace.replace(/"/g, '\\"');
|
|
77
|
+
if (mode === 'read-only') {
|
|
78
|
+
return {
|
|
79
|
+
file: '/usr/bin/sandbox-exec',
|
|
80
|
+
args: ['-p', '(version 1)(allow default)(deny file-write*)', base.file, ...base.args]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
file: '/usr/bin/sandbox-exec',
|
|
85
|
+
args: [
|
|
86
|
+
'-p',
|
|
87
|
+
`(version 1)(allow default)(deny file-write*)(allow file-write* (subpath "${ws}") (subpath "/private/tmp") (regex #"^/private/var/folders/"))`,
|
|
88
|
+
base.file,
|
|
89
|
+
...base.args
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { blocked: true, detail: sandboxBackend(platform).detail };
|
|
94
|
+
}
|
package/dist/shell.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, execSync } from 'child_process';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
3
4
|
const GIT_BASH_CANDIDATES = [
|
|
4
5
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
5
6
|
'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
|
@@ -7,6 +8,7 @@ const GIT_BASH_CANDIDATES = [
|
|
|
7
8
|
];
|
|
8
9
|
let cachedType = null;
|
|
9
10
|
let cachedBashPath;
|
|
11
|
+
let cachedBashEnv = null;
|
|
10
12
|
function findBashPath() {
|
|
11
13
|
if (process.platform === 'win32') {
|
|
12
14
|
for (const candidate of GIT_BASH_CANDIDATES) {
|
|
@@ -31,6 +33,26 @@ export function getBashPath() {
|
|
|
31
33
|
cachedBashPath = findBashPath();
|
|
32
34
|
return cachedBashPath;
|
|
33
35
|
}
|
|
36
|
+
// A login shell (-l) would source the profile to put Git's /usr/bin on
|
|
37
|
+
// PATH, but that costs ~450ms per command. Prepending the directories to
|
|
38
|
+
// the child PATH achieves the same tool resolution at spawn speed.
|
|
39
|
+
export function spawnEnv() {
|
|
40
|
+
if (process.platform !== 'win32')
|
|
41
|
+
return process.env;
|
|
42
|
+
if (cachedBashEnv)
|
|
43
|
+
return cachedBashEnv;
|
|
44
|
+
const env = { ...process.env };
|
|
45
|
+
const bp = getBashPath();
|
|
46
|
+
if (bp) {
|
|
47
|
+
const usrBin = path.dirname(bp);
|
|
48
|
+
const gitRoot = path.resolve(usrBin, '..', '..');
|
|
49
|
+
const extra = [usrBin, path.join(gitRoot, 'mingw64', 'bin'), path.join(gitRoot, 'cmd')]
|
|
50
|
+
.filter(p => fs.existsSync(p));
|
|
51
|
+
env.PATH = `${extra.join(path.delimiter)}${path.delimiter}${env.PATH ?? ''}`;
|
|
52
|
+
}
|
|
53
|
+
cachedBashEnv = env;
|
|
54
|
+
return env;
|
|
55
|
+
}
|
|
34
56
|
export function resolveShellType(config) {
|
|
35
57
|
const requested = String(config?.shell || process.env.AUTOCLOW_SHELL || 'auto').toLowerCase();
|
|
36
58
|
if (requested === 'bash' || requested === 'powershell' || requested === 'cmd' || requested === 'sh') {
|
|
@@ -57,28 +79,42 @@ export function smartDecode(buf) {
|
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
81
|
}
|
|
60
|
-
export function
|
|
82
|
+
export function shellInvocation(command) {
|
|
61
83
|
const type = resolveShellType();
|
|
62
|
-
let file;
|
|
63
|
-
let args;
|
|
64
84
|
if (type === 'bash') {
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
file
|
|
68
|
-
args = ['-l', '-c', command];
|
|
85
|
+
// --noprofile/--norc: profile loading costs ~450ms per command; PATH is
|
|
86
|
+
// handled by spawnEnv() instead.
|
|
87
|
+
return { file: getBashPath(), args: ['--noprofile', '--norc', '-c', command] };
|
|
69
88
|
}
|
|
70
|
-
|
|
71
|
-
file
|
|
72
|
-
args = ['-NoProfile', '-Command', command];
|
|
89
|
+
if (type === 'powershell') {
|
|
90
|
+
return { file: 'powershell.exe', args: ['-NoProfile', '-Command', command] };
|
|
73
91
|
}
|
|
74
|
-
|
|
75
|
-
file
|
|
76
|
-
args = ['/d', '/s', '/c', command];
|
|
92
|
+
if (type === 'cmd') {
|
|
93
|
+
return { file: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command] };
|
|
77
94
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
return { file: '/bin/sh', args: ['-c', command] };
|
|
96
|
+
}
|
|
97
|
+
export function killProcessTree(pid) {
|
|
98
|
+
if (process.platform !== 'win32') {
|
|
99
|
+
// The negative pid targets the detached process group.
|
|
100
|
+
try {
|
|
101
|
+
process.kill(-pid, 'SIGKILL');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, 'SIGKILL');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// already gone
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
81
112
|
}
|
|
113
|
+
spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
114
|
+
}
|
|
115
|
+
export function execShellCommand(command, opts) {
|
|
116
|
+
const type = resolveShellType();
|
|
117
|
+
const { file, args } = opts.sandboxed ?? shellInvocation(command);
|
|
82
118
|
return new Promise(resolve => {
|
|
83
119
|
const stdout = [];
|
|
84
120
|
const stderr = [];
|
|
@@ -87,7 +123,7 @@ export function execShellCommand(command, opts) {
|
|
|
87
123
|
let truncated = false;
|
|
88
124
|
let settled = false;
|
|
89
125
|
const posix = process.platform !== 'win32';
|
|
90
|
-
const child = spawn(file, args, { windowsHide: true, detached: posix });
|
|
126
|
+
const child = spawn(file, args, { windowsHide: true, detached: posix, env: spawnEnv() });
|
|
91
127
|
const finish = () => {
|
|
92
128
|
if (settled)
|
|
93
129
|
return;
|
|
@@ -101,21 +137,8 @@ export function execShellCommand(command, opts) {
|
|
|
101
137
|
});
|
|
102
138
|
};
|
|
103
139
|
const killTree = () => {
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
if (posix) {
|
|
107
|
-
try {
|
|
108
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
child.kill('SIGKILL');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
// Terminating the shell alone would orphan grandchildren, whose
|
|
116
|
-
// inherited stdio pipes keep 'close' pending — kill the whole tree.
|
|
117
|
-
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
118
|
-
}
|
|
140
|
+
if (child.pid)
|
|
141
|
+
killProcessTree(child.pid);
|
|
119
142
|
};
|
|
120
143
|
const timer = setTimeout(() => {
|
|
121
144
|
timedOut = true;
|