autoclaw 1.3.5 → 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.
Files changed (33) hide show
  1. package/README.md +56 -3
  2. package/README.zh-CN.md +56 -3
  3. package/dist/agent.js +13 -1
  4. package/dist/index.js +75 -1
  5. package/dist/skills.js +276 -0
  6. package/dist/tools/index.js +4 -0
  7. package/dist/tools/render-image.js +135 -0
  8. package/dist/tools/render-pdf.js +111 -0
  9. package/dist/tools/takumi-fonts.js +61 -0
  10. package/dist/zip.js +149 -0
  11. package/package.json +5 -2
  12. package/skills/code2media/SKILL.md +82 -0
  13. package/skills/code2media/references/syntax-guide.md +63 -0
  14. package/skills/code2media/scripts/package.json +10 -0
  15. package/skills/code2media/scripts/render.mjs +177 -0
  16. package/skills/code2media/templates/animation.html +19 -0
  17. package/skills/code2media/templates/badge.html +6 -0
  18. package/skills/code2media/templates/certificate.html +9 -0
  19. package/skills/code2media/templates/metrics-card.html +25 -0
  20. package/skills/code2media/templates/weekly-report.html +86 -0
  21. package/skills/invoice-maker/SKILL.md +65 -0
  22. package/skills/invoice-maker/references/syntax-guide.md +63 -0
  23. package/skills/invoice-maker/scripts/package.json +10 -0
  24. package/skills/invoice-maker/scripts/render.mjs +177 -0
  25. package/skills/invoice-maker/templates/invoice.html +26 -0
  26. package/skills/invoice-maker/templates/quote.html +68 -0
  27. package/skills/poster-maker/SKILL.md +62 -0
  28. package/skills/poster-maker/references/syntax-guide.md +63 -0
  29. package/skills/poster-maker/scripts/package.json +10 -0
  30. package/skills/poster-maker/scripts/render.mjs +177 -0
  31. package/skills/poster-maker/templates/cover.html +13 -0
  32. package/skills/poster-maker/templates/og-card.html +17 -0
  33. package/skills/poster-maker/templates/social-post.html +14 -0
package/README.md CHANGED
@@ -40,6 +40,8 @@ Unlike "screen-seeing" agents (such as OpenClaw) that rely on visual interpretat
40
40
  - 🌐 **Web Search**: Integrated with Tavily for real-time information retrieval.
41
41
  - 🌍 **Web Reading & Screenshots**: Extract article content and capture page screenshots (requires `npx playwright install chromium`).
42
42
  - 🎨 **Image Generation**: DALL-E compatible image generation via any OpenAI-compatible images API.
43
+ - 🖼️ **Deterministic Image Rendering** (`render_image`): HTML + Tailwind templates rendered into PNG/JPEG/WebP/SVG, plus animations (animated WebP/GIF/APNG from CSS `@keyframes`). Fully offline, no browser, milliseconds per render — for OG cards, banners, badges and data cards where exact text and layout matter.
44
+ - 📄 **PDF Rendering** (`render_pdf`): HTML templates rendered into paged PDFs with selectable text, repeating headers/footers and page counters. Fully offline, no browser — for invoices, reports and certificates.
43
45
  - 🕒 **Time Accuracy**: Built-in tool to get precise system date and time for correct temporal context.
44
46
  - 📧 **Communication**: Send emails and push notifications to chat groups automatically.
45
47
 
@@ -50,6 +52,7 @@ Unlike "screen-seeing" agents (such as OpenClaw) that rely on visual interpretat
50
52
  - **UI**: Inquirer (interactivity), Chalk (styling), Ora (spinners)
51
53
  - **AI**: OpenAI SDK (any OpenAI-compatible endpoint: DeepSeek, Kimi, Qwen, GLM, Ollama, …)
52
54
  - **Web tools**: Playwright (headless Chromium for `read_website` / `take_screenshot`)
55
+ - **Rendering**: Takumi (Rust engine via native binding — powers `render_image` / `render_pdf`, no browser)
53
56
 
54
57
  ## Installation
55
58
 
@@ -132,6 +135,22 @@ Unattempted tasks are simply absent from the results file, so `--fail-fast` foll
132
135
 
133
136
  AutoClaw also keeps its own prompt lean: optional tools (web search, email, group notifications, image generation) only register once their credentials are configured, and in long loops older tool results in the model context are replaced by short excerpts.
134
137
 
138
+ ### Skills (Portable Capability Packages)
139
+ AutoClaw runs `SKILL.md` skill packages — the same format used by the WorkBuddy skill store, so one package runs both inside AutoClaw and on other platforms. The system prompt only carries a one-line manifest per skill; when a task matches, the agent reads that skill's `SKILL.md` and follows it with the normal file and shell tools. There is no privileged runtime: skill scripts pass through the same destructive-command gate, sandbox and step caps as any command.
140
+
141
+ Scopes (later shadows earlier on name collision): built-in `skills/` (ships with the npm package) → `~/.autoclaw/skills/` → `.autoclaw/skills/`.
142
+
143
+ ```bash
144
+ autoclaw skill list # show discovered skills with scope and version
145
+ autoclaw skill install <zip|dir|https-url> # install into ~/.autoclaw/skills/ (zip-slip protected)
146
+ autoclaw skill remove <name> # remove a user-installed skill (built-ins are protected)
147
+ autoclaw skill pack <dir> # zip a skill dir (skills/<name>/ root) for store upload
148
+ ```
149
+
150
+ Install accepts any SKILL.md-compatible package: a local directory, a local zip, or an https download URL. It tolerates third-party layout variance (SKILL.md at the zip root, a plain folder, or a `skills/<name>/` wrapper, macOS `__MACOSX`/`.DS_Store` junk) and always installs under the skill's frontmatter `name`, so discovery and the manifest stay consistent.
151
+
152
+ Three built-in skills, layered: [`code2media`](skills/code2media/SKILL.md) (Code to Media) is the universal rendering engine — a standalone Node script turning any HTML into images/SVG/paged PDFs/animations; [`poster-maker`](skills/poster-maker/SKILL.md) and [`invoice-maker`](skills/invoice-maker/SKILL.md) are independently optimized scenario skills carrying platform size specs, document layout conventions and quality checklists. The same zips publish to any SKILL.md-compatible store. Skills compose with batch mode: one manifest line like `{"id":"inv-042","task":"用 invoice-maker 技能根据 orders-042.json 生成发票 invoices/042.pdf"}` drives an isolated swarm worker through the same skill.
153
+
135
154
  ### Recipes
136
155
 
137
156
  Daily ops sweep on Linux (crontab):
@@ -206,6 +225,7 @@ AutoClaw uses a hierarchical configuration system.
206
225
  - `shellTimeout`: Shell command timeout in milliseconds (default: `120000`).
207
226
  - `taskTimeoutMs`: Whole-task wall-clock timeout in milliseconds (off by default; aborts in-flight API calls and stops with `timeout` status).
208
227
  - `sandbox`: Confine shell commands (`read-only`, `workspace-write`, `danger-full-access`; default: `danger-full-access`).
228
+ - `skillsEnabled`: Set `false` to disable the skill system (default: `true`).
209
229
  - `shell`: Force a shell for `execute_shell_command` (`bash`, `powershell`, `cmd`, `sh`; default: auto-detect — Git Bash > PowerShell > cmd on Windows).
210
230
  - `tavilyApiKey`: API Key for Tavily Web Search.
211
231
  - `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP Email settings.
@@ -250,6 +270,39 @@ Configure webhooks to receive alerts or reports in your team chat apps.
250
270
  Built-in utility to provide the agent with the current system time, ensuring accurate handling of relative time requests.
251
271
  - **Usage**: "What's the date today?" or "Remind me to check the logs next Monday."
252
272
 
273
+ ### Deterministic Rendering (Takumi)
274
+ `render_image` turns HTML templates into precise images — PNG, JPEG, WebP or vector SVG — offline with no browser or AI model involved. `render_pdf` turns HTML templates into paged PDFs with selectable text, repeating header/footer bands, and `<span class="pageNumber">` / `<span class="totalPages">` counters. Templates are styled with inline CSS, `<style>` blocks, or Tailwind v4 utilities via the `tw` attribute (`<div tw="w-full h-full bg-blue-500">`); plain `class` attributes only match regular CSS selectors. Both tools auto-detect common system fonts (CJK/emoji included); register specific font files via `font_paths`.
275
+
276
+ Typical workflows — describe the job in natural language and the agent writes the templates itself:
277
+
278
+ ```bash
279
+ # Blog SEO: one OG share image per post
280
+ autoclaw "Read the title and summary of every .md file in content/posts/ and render an OG share image (1200x630) for each into public/og/" -y -n
281
+
282
+ # Finance / e-commerce: invoice PDFs from an orders export, then email them out
283
+ autoclaw "Read orders.csv, render a PDF invoice for each order into invoices/ (A4, page-number footer), then email every invoice to the customer address in its row" -y
284
+
285
+ # HR / training: personalized completion certificates for an attendee list
286
+ autoclaw "Read attendees.json and render a completion certificate (1414x1000) for each attendee into certs/, numbered from AC-2026-0001" -y -n
287
+
288
+ # Ops reporting under cron/CI: deterministic output — same input produces the same PDF
289
+ autoclaw "Aggregate this week's nginx access log into a one-page A4 PDF report with a metrics table and save it as report.pdf" -y -n
290
+ ```
291
+
292
+ Swarm scale via batch mode — each task renders in its own isolated agent:
293
+
294
+ ```bash
295
+ cat > render-jobs.jsonl <<'EOF'
296
+ {"id": "og-001", "task": "Render an OG share image for post-001.md into public/og/001.png"}
297
+ {"id": "og-002", "task": "Render an OG share image for post-002.md into public/og/002.png"}
298
+ EOF
299
+ autoclaw batch render-jobs.jsonl -y -c 4
300
+ ```
301
+
302
+ Tool choice: use `render_image` / `render_pdf` when exact text, layout and branding matter (cards, banners, badges, documents); use `generate_image` for artistic or photographic imagery. Emoji in templates are fetched from the Twemoji CDN by default, so fully offline environments should keep templates text-only.
303
+
304
+ Runnable examples with committed previews: [examples/render](examples/render/README.md) (OG cards, social posters, KPI cards, weekly-report PDFs, SVG badges, certificates, animations, multi-page invoices — plus a real agent one-shot run under `agent-run/`). The same capability ships as a portable [WorkBuddy skill](skills/code2media/SKILL.md) (`code2media-skill.zip`) that renders HTML → image/SVG/PDF/animation via a standalone Node script on any machine with Node >= 20.19.
305
+
253
306
  ## Docker Support
254
307
 
255
308
  ### Build & Run
@@ -260,10 +313,10 @@ docker run --rm -v "$PWD":/workspace -w /workspace -e OPENAI_API_KEY=sk-... auto
260
313
  ```
261
314
  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.
262
315
 
263
- ### Chinese Font Issues in Screenshots
264
- 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.
316
+ ### Chinese Font Issues in Screenshots and Rendered Output
317
+ 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. The same affects `render_image` / `render_pdf` output containing CJK text.
265
318
 
266
- **Solution:** Install CJK (Chinese/Japanese/Korean) and Emoji fonts in your container.
319
+ **Solution:** Install CJK (Chinese/Japanese/Korean) and Emoji fonts in your container. The render tools auto-detect the same font paths, so installing these packages fixes both screenshots and rendered images/PDFs.
267
320
 
268
321
  **For Debian/Ubuntu:**
269
322
  ```bash
package/README.zh-CN.md CHANGED
@@ -41,6 +41,8 @@ AutoClaw 是一款针对 **“无界面系统” (Headless Systems)** 的高稳
41
41
  - 🌐 **网页搜索**: 集成 Tavily,支持实时信息检索。
42
42
  - 🌍 **网页阅读与截图**: 提取文章正文、截取页面图片(需先执行 `npx playwright install chromium`)。
43
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,文字可选中、页眉页脚逐页重复、支持页码计数。完全离线、无浏览器——适合发票、报表、证书等结构化文档。
44
46
  - 🕒 **时间精准**: 内置工具获取精确系统日期和时间,确保正确的时间上下文。
45
47
  - 📧 **通讯能力**: 自动发送电子邮件并将通知推送至聊天群组。
46
48
 
@@ -51,6 +53,7 @@ AutoClaw 是一款针对 **“无界面系统” (Headless Systems)** 的高稳
51
53
  - **UI**: Inquirer (交互), Chalk (样式), Ora (加载动画)
52
54
  - **AI**: OpenAI SDK(任意 OpenAI 兼容端点:DeepSeek、Kimi、Qwen、GLM、Ollama 等)
53
55
  - **网页工具**: Playwright(无头 Chromium,用于 `read_website` / `take_screenshot`)
56
+ - **渲染引擎**: Takumi(Rust 引擎,经原生绑定驱动 `render_image` / `render_pdf`,无需浏览器)
54
57
 
55
58
  ## 安装
56
59
 
@@ -133,6 +136,22 @@ autoclaw batch big.jsonl -y -c 4 # 最多 4 个任务并行
133
136
 
134
137
  AutoClaw 同时会自动给提示词瘦身:可选工具(网页搜索、邮件、群通知、图像生成)只在凭据配置后才会注册进工具定义;长循环中较早的工具结果会被替换为短摘要。
135
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
+
136
155
  ### 实战配方
137
156
 
138
157
  Linux 定时巡检(crontab):
@@ -208,6 +227,7 @@ AutoClaw 使用层级配置系统。
208
227
  - `shellTimeout`: Shell 命令超时时间(毫秒)(靘莤: `120000`)。
209
228
  - `taskTimeoutMs`: 单任务整体墙钟超时(毫秒,默认关闭;会中断进行中的 API 调用并以 `timeout` 状态停止)。
210
229
  - `sandbox`: 约束 shell 命令(`read-only`、`workspace-write`、`danger-full-access`;默认 `danger-full-access`)。
230
+ - `skillsEnabled`: 设为 `false` 关闭技能系统(默认开启)。
211
231
  - `shell`: 基刜 `execute_shell_command` 使用的 shell (`bash`、`powershell`、`cmd`、`sh`;默认自动检测——Windows 上优先 Git Bash > PowerShell > cmd)。
212
232
  - `tavilyApiKey`: Tavily 网页搜索的 API 密钥。
213
233
  - `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP 邮件设置。
@@ -252,6 +272,39 @@ AutoClaw 使用层级配置系统。
252
272
  内置工具为 Agent 提供当前系统时间,确保准确处理相对时间请求。
253
273
  - **示例**: "今天是几号?" 或 "提醒我下周一检查日志。"
254
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
+
255
308
  ## Docker 支持
256
309
 
257
310
  ### 构建与运行
@@ -262,10 +315,10 @@ docker run --rm -v "$PWD":/workspace -w /workspace -e OPENAI_API_KEY=sk-... auto
262
315
  ```
263
316
  注意:默认镜像中未内置浏览器,基于浏览器的工具(`read_website` / `take_screenshot`)不可用——它们会返回友好的安装提示,而不是报错崩溃。
264
317
 
265
- ### 截图中的中文显示问题
266
- 在 Docker 容器(尤其是 Alpine 或 Debian Slim)中运行时,网页截图中的中文可能会显示为方块("豆腐块")。表情符号(如 🔥)也可能显示为方块。
318
+ ### 截图与渲染输出中的中文显示问题
319
+ 在 Docker 容器(尤其是 Alpine 或 Debian Slim)中运行时,网页截图中的中文可能会显示为方块("豆腐块")。表情符号(如 🔥)也可能显示为方块。`render_image` / `render_pdf` 输出中包含中日韩文字时同样受影响。
267
320
 
268
- **解决方案:** 在容器中安装 CJK(中日韩)和 Emoji 字体。
321
+ **解决方案:** 在容器中安装 CJK(中日韩)和 Emoji 字体。渲染工具会自动探测同一批字体路径,因此安装这些字体包可以同时修复截图与渲染输出的中文显示。
269
322
 
270
323
  **Debian/Ubuntu:**
271
324
  ```bash
package/dist/agent.js CHANGED
@@ -9,6 +9,7 @@ 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';
14
15
  // Canonical JSON: sorted object keys, so equivalent arguments from
@@ -73,6 +74,17 @@ System Information:
73
74
  has('optimize_prompt') ? '- Creation: optimize_prompt — refine raw prompts for creative/complex tasks (recommended before creative work)' : null,
74
75
  '- Utility: get_current_datetime — accurate system time for temporal reasoning'
75
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` : '';
76
88
  this.messages = [
77
89
  {
78
90
  role: "system",
@@ -84,7 +96,7 @@ ${systemInfo}
84
96
 
85
97
  WHAT YOU CAN DO:
86
98
  ${capabilities}
87
-
99
+ ${skillsBlock}
88
100
  RULES OF ENGAGEMENT:
89
101
  1. One shot, not one chat. Produce working results, not conversation. Be terse.
90
102
  2. Use the right tool for the job. Shell for system ops. Files for content. Web tools for external info.
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.5';
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/skills.js ADDED
@@ -0,0 +1,276 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { createZip, readZip } from './zip.js';
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ // Built-in skills ship with the package (dist/../skills); user + project
8
+ // scopes shadow built-ins on name collisions (project > user > builtin).
9
+ export function builtinSkillsDir() {
10
+ return path.resolve(__dirname, '..', 'skills');
11
+ }
12
+ export function defaultSkillScopes() {
13
+ return [
14
+ { dir: builtinSkillsDir(), source: 'builtin' },
15
+ { dir: path.join(os.homedir(), '.autoclaw', 'skills'), source: 'user' },
16
+ { dir: path.resolve(process.cwd(), '.autoclaw', 'skills'), source: 'project' },
17
+ ];
18
+ }
19
+ export function parseSkillMd(raw) {
20
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
21
+ if (!match)
22
+ return null;
23
+ const frontmatter = {};
24
+ for (const line of match[1].split(/\r?\n/)) {
25
+ if (!line.trim() || line.trim().startsWith('#'))
26
+ continue;
27
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
28
+ if (!kv)
29
+ continue;
30
+ let value = kv[2].trim();
31
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
32
+ value = value.slice(1, -1);
33
+ }
34
+ frontmatter[kv[1]] = value;
35
+ }
36
+ return { frontmatter, body: raw.slice(match[0].length) };
37
+ }
38
+ function toMeta(frontmatter, dir, source) {
39
+ const bool = (v) => v === 'true' ? true : v === 'false' ? false : undefined;
40
+ return {
41
+ name: frontmatter.name || path.basename(dir),
42
+ displayName: frontmatter.display_name,
43
+ description: frontmatter.description || frontmatter.description_zh || frontmatter.description_en || '',
44
+ descriptionZh: frontmatter.description_zh,
45
+ descriptionEn: frontmatter.description_en,
46
+ version: frontmatter.version,
47
+ author: frontmatter.author,
48
+ category: frontmatter.category,
49
+ disableModelInvocation: bool(frontmatter['disable-model-invocation']),
50
+ userInvocable: bool(frontmatter['user-invocable']),
51
+ source,
52
+ dir,
53
+ skillMdPath: path.join(dir, 'SKILL.md'),
54
+ };
55
+ }
56
+ // Later scopes win on name collisions. Dirs starting with '.' are skipped.
57
+ export function discoverSkills(scopes) {
58
+ const warnings = [];
59
+ const byName = new Map();
60
+ for (const scope of scopes) {
61
+ let entries = [];
62
+ try {
63
+ entries = fs.readdirSync(scope.dir, { withFileTypes: true })
64
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
65
+ .map(e => e.name);
66
+ }
67
+ catch { /* scope dir missing — fine */
68
+ continue;
69
+ }
70
+ for (const entry of entries) {
71
+ const dir = path.join(scope.dir, entry);
72
+ const skillMdPath = path.join(dir, 'SKILL.md');
73
+ let raw;
74
+ try {
75
+ raw = fs.readFileSync(skillMdPath, 'utf-8');
76
+ }
77
+ catch {
78
+ warnings.push(`skipped ${dir}: no SKILL.md`);
79
+ continue;
80
+ }
81
+ try {
82
+ const parsed = parseSkillMd(raw);
83
+ if (!parsed) {
84
+ warnings.push(`skipped ${skillMdPath}: missing YAML frontmatter`);
85
+ continue;
86
+ }
87
+ const meta = toMeta(parsed.frontmatter, dir, scope.source);
88
+ if (!meta.description) {
89
+ warnings.push(`skipped ${skillMdPath}: empty description`);
90
+ continue;
91
+ }
92
+ byName.set(meta.name, meta);
93
+ }
94
+ catch (err) {
95
+ warnings.push(`skipped ${skillMdPath}: ${err?.message || err}`);
96
+ }
97
+ }
98
+ }
99
+ return { skills: [...byName.values()], warnings };
100
+ }
101
+ // ---- system-prompt manifest (progressive disclosure: one line per skill) ----
102
+ function truncate(text, max) {
103
+ const flat = text.replace(/\s+/g, ' ').trim();
104
+ return flat.length > max ? flat.slice(0, max - 1) + '…' : flat;
105
+ }
106
+ export function buildSkillsManifest(config, scopes) {
107
+ if (config?.skillsEnabled === false)
108
+ return null;
109
+ const { skills } = discoverSkills(scopes || defaultSkillScopes());
110
+ const visible = skills.filter(s => s.disableModelInvocation !== true);
111
+ if (visible.length === 0)
112
+ return null;
113
+ const lines = visible.map(s => `- ${s.name}${s.version ? ` (v${s.version})` : ''}: ${truncate(s.description, 160)} [read ${s.skillMdPath}]`);
114
+ return [
115
+ 'INSTALLED SKILL PACKAGES (procedural capabilities bundling instructions, scripts and templates).',
116
+ 'When a task matches a skill, first read its SKILL.md and follow it — skills run through your normal file and shell tools, no special API:',
117
+ ...lines,
118
+ ].join('\n');
119
+ }
120
+ // ---- install / remove ----
121
+ const INSTALL_SKIP = new Set(['node_modules', '.git', '__MACOSX']);
122
+ function isJunkPath(rel) {
123
+ return rel.split('/').some(s => INSTALL_SKIP.has(s)) || rel.split('/').pop() === '.DS_Store';
124
+ }
125
+ // Skill directory names come from untrusted frontmatter, so restrict to
126
+ // letters (any script, e.g. Chinese), digits, dot, underscore, dash — no
127
+ // separators, no leading dot (discovery skips dot-dirs), bounded length.
128
+ function isSafeSkillName(name) {
129
+ return name.length <= 64 && /^[\p{L}\p{N}][\p{L}\p{N}._-]*$/u.test(name);
130
+ }
131
+ function copyTree(src, dest) {
132
+ let count = 0;
133
+ fs.mkdirSync(dest, { recursive: true });
134
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
135
+ if (INSTALL_SKIP.has(entry.name) || entry.name === '.DS_Store')
136
+ continue;
137
+ const s = path.join(src, entry.name);
138
+ const d = path.join(dest, entry.name);
139
+ if (entry.isDirectory())
140
+ count += copyTree(s, d);
141
+ else if (entry.isFile()) {
142
+ fs.copyFileSync(s, d);
143
+ count++;
144
+ }
145
+ }
146
+ return count;
147
+ }
148
+ export function userSkillsDir() {
149
+ return path.join(os.homedir(), '.autoclaw', 'skills');
150
+ }
151
+ // Install from a skill directory, a zip package, or an https URL into the
152
+ // user scope (~/.autoclaw/skills/). The installed directory is named after
153
+ // the skill's frontmatter `name` (fallback: the folder/zip stem), so the
154
+ // path always matches what discovery and the manifest expect.
155
+ export function installSkill(target, opts) {
156
+ const userDir = opts?.userDir || userSkillsDir();
157
+ const stat = fs.existsSync(target) ? fs.statSync(target) : null;
158
+ if (!stat)
159
+ throw new Error(`target not found: ${target}`);
160
+ if (stat.isDirectory()) {
161
+ const skillMdPath = path.join(target, 'SKILL.md');
162
+ if (!fs.existsSync(skillMdPath))
163
+ throw new Error(`${target} is not a skill (no SKILL.md)`);
164
+ const parsed = parseSkillMd(fs.readFileSync(skillMdPath, 'utf-8'));
165
+ const name = pickSafeName(parsed?.frontmatter.name, path.basename(path.resolve(target)));
166
+ const dest = path.join(userDir, name);
167
+ const files = copyTree(target, dest);
168
+ return { name, dir: dest, files };
169
+ }
170
+ // zip package: locate the SKILL.md entry, rebase its dir to userDir/<name>.
171
+ // Works with third-party layouts: SKILL.md at the zip root, a plain folder,
172
+ // or a skills/<name>/ wrapper, independently of frontmatter-vs-folder naming.
173
+ const buf = fs.readFileSync(target);
174
+ const entries = readZip(buf).map(e => ({ ...e, path: e.path.replace(/\\/g, '/') }));
175
+ const candidates = entries
176
+ .filter(e => /(^|\/)SKILL\.md$/.test(e.path))
177
+ .sort((a, b) => a.path.split('/').length - b.path.split('/').length);
178
+ if (candidates.length === 0)
179
+ throw new Error('zip contains no SKILL.md — not a skill package');
180
+ const pick = candidates[0];
181
+ const rootDir = pick.path.split('/').slice(0, -1).join('/');
182
+ const parsed = parseSkillMd(pick.data.toString('utf-8'));
183
+ const fallback = rootDir ? rootDir.split('/').filter(Boolean).pop() : path.basename(target).replace(/\.zip$/i, '');
184
+ const name = pickSafeName(parsed?.frontmatter.name, fallback);
185
+ const prefix = rootDir ? rootDir + '/' : '';
186
+ const dest = path.join(userDir, name);
187
+ const destResolved = path.resolve(dest) + path.sep;
188
+ const wanted = entries
189
+ .filter(e => prefix ? e.path.startsWith(prefix) : true)
190
+ .map(e => ({ rel: prefix ? e.path.slice(prefix.length) : e.path, data: e.data }))
191
+ .filter(e => e.rel !== '' && !e.rel.endsWith('/') && !isJunkPath(e.rel));
192
+ // Validate every target path before writing anything: a hostile entry must
193
+ // reject the whole archive, not half-install it.
194
+ const targets = wanted.map(e => {
195
+ const destFile = path.resolve(dest, ...e.rel.split('/'));
196
+ if (!destFile.startsWith(destResolved))
197
+ throw new Error(`unsafe zip entry: ${e.rel}`);
198
+ return { destFile, data: e.data };
199
+ });
200
+ for (const t of targets) {
201
+ fs.mkdirSync(path.dirname(t.destFile), { recursive: true });
202
+ fs.writeFileSync(t.destFile, t.data);
203
+ }
204
+ return { name, dir: dest, files: targets.length };
205
+ }
206
+ function pickSafeName(primary, fallback) {
207
+ const candidate = primary && isSafeSkillName(primary) ? primary : fallback;
208
+ if (!isSafeSkillName(candidate)) {
209
+ throw new Error(`unsafe skill name: ${JSON.stringify(primary || fallback)}`);
210
+ }
211
+ return candidate;
212
+ }
213
+ // Download an https zip package to a temp file and install it.
214
+ export async function installSkillFromUrl(url, opts) {
215
+ if (!/^https:\/\//.test(url))
216
+ throw new Error('only https URLs are supported');
217
+ const res = await fetch(url, { signal: AbortSignal.timeout(120000) });
218
+ if (!res.ok)
219
+ throw new Error(`download failed: HTTP ${res.status} for ${url}`);
220
+ const buf = Buffer.from(await res.arrayBuffer());
221
+ if (buf.length > 100 * 1024 * 1024)
222
+ throw new Error('skill package exceeds 100 MB limit');
223
+ const tmp = path.join(os.tmpdir(), `autoclaw-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`);
224
+ fs.writeFileSync(tmp, buf);
225
+ try {
226
+ return installSkill(tmp, opts);
227
+ }
228
+ finally {
229
+ fs.rmSync(tmp, { force: true });
230
+ }
231
+ }
232
+ export function removeSkill(name, opts) {
233
+ const scopes = opts?.scopes || defaultSkillScopes();
234
+ const { skills } = discoverSkills(scopes);
235
+ const skill = skills.find(s => s.name === name);
236
+ if (!skill)
237
+ return 'not-found';
238
+ if (skill.source === 'builtin')
239
+ return 'builtin';
240
+ if (skill.source === 'project')
241
+ return 'project';
242
+ const userDir = path.resolve(opts?.userDir || userSkillsDir());
243
+ const dir = path.resolve(skill.dir);
244
+ if (!dir.startsWith(userDir + path.sep))
245
+ return 'not-found'; // never delete outside the user scope
246
+ fs.rmSync(dir, { recursive: true, force: true });
247
+ return 'removed';
248
+ }
249
+ // ---- pack (store-upload artifact: zip with skills/<name>/ at its root) ----
250
+ export function packSkill(dir, outPath) {
251
+ const abs = path.resolve(dir);
252
+ const skillMdPath = path.join(abs, 'SKILL.md');
253
+ if (!fs.existsSync(skillMdPath))
254
+ throw new Error(`${abs} is not a skill (no SKILL.md)`);
255
+ const parsed = parseSkillMd(fs.readFileSync(skillMdPath, 'utf-8'));
256
+ const name = parsed?.frontmatter.name || path.basename(abs);
257
+ if (!/^[A-Za-z0-9._-]+$/.test(name))
258
+ throw new Error(`unsafe skill name: ${name}`);
259
+ const files = [];
260
+ const walk = (current, rel) => {
261
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
262
+ if (INSTALL_SKIP.has(entry.name) || entry.name === '.DS_Store')
263
+ continue;
264
+ const child = path.join(current, entry.name);
265
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
266
+ if (entry.isDirectory())
267
+ walk(child, childRel);
268
+ else if (entry.isFile())
269
+ files.push({ path: `skills/${name}/${childRel}`, data: fs.readFileSync(child) });
270
+ }
271
+ };
272
+ walk(abs, '');
273
+ const zipPath = outPath || path.resolve(process.cwd(), `${name}-skill.zip`);
274
+ fs.writeFileSync(zipPath, createZip(files));
275
+ return { zipPath, fileCount: files.length, name };
276
+ }
@@ -6,6 +6,8 @@ import { BrowserTool } from './browser.js';
6
6
  import { ScreenshotTool } from './screenshot.js';
7
7
  import { ImageTool } from './image.js';
8
8
  import { PromptOptimizerTool } from './prompt-optimizer.js';
9
+ import { RenderImageTool } from './render-image.js';
10
+ import { RenderPdfTool } from './render-pdf.js';
9
11
  import { CheckBackgroundProcessTool, StartBackgroundProcessTool, StopBackgroundProcessTool } from './background.js';
10
12
  // Central Registry of all available tools
11
13
  export const toolRegistry = [
@@ -20,6 +22,8 @@ export const toolRegistry = [
20
22
  BrowserTool,
21
23
  ScreenshotTool,
22
24
  ImageTool,
25
+ RenderImageTool,
26
+ RenderPdfTool,
23
27
  StartBackgroundProcessTool,
24
28
  CheckBackgroundProcessTool,
25
29
  StopBackgroundProcessTool