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.
- package/README.md +56 -3
- package/README.zh-CN.md +56 -3
- package/dist/agent.js +13 -1
- package/dist/index.js +75 -1
- package/dist/skills.js +276 -0
- package/dist/tools/index.js +4 -0
- 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.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.
|
|
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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|