diff --git a/docs/development/guides/bundled-skills.md b/docs/development/guides/bundled-skills.md index 5f900755..599465cb 100644 --- a/docs/development/guides/bundled-skills.md +++ b/docs/development/guides/bundled-skills.md @@ -6,12 +6,12 @@ sidebar_position: 4 # Bundled Skills -HybridClaw currently ships with 33 bundled skills. A few notable categories: +HybridClaw currently ships with 34 bundled skills. A few notable categories: - office workflows: `pdf`, `xlsx`, `docx`, `pptx`, `office-workflows` - planning and engineering: `project-manager`, `feature-planning`, `code-review`, `code-simplification` -- visual explainers and animation: `manim-video` +- visual explainers and animation: `manim-video`, `excalidraw` - platform integrations: `github-pr-workflow`, `notion`, `trello`, `stripe`, `wordpress`, `google-workspace`, `discord` - knowledge workflows: `llm-wiki`, `obsidian`, `zettelkasten` diff --git a/docs/index.html b/docs/index.html index a48815e6..96745ab1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1431,7 +1431,7 @@

Proactive Scheduler Jobs

Skills Engine

-

HybridClaw ships with 33 bundled skills, including the manim-video animation workflow, category-aware catalog and admin authoring flows, and OpenClaw/CLAUDE-compatible SKILL.md discovery with prompt embedding modes (always/summary/hidden), eligibility checks, and trust-aware security scanning. Community imports support packaged official/<skill> skills, skills-sh, clawhub, lobehub, claude-marketplace, well-known, and explicit GitHub repo/path sources via hybridclaw skill import or /skill import.

+

HybridClaw ships with 34 bundled skills, including the manim-video animation workflow and the excalidraw diagramming workflow, category-aware catalog and admin authoring flows, and OpenClaw/CLAUDE-compatible SKILL.md discovery with prompt embedding modes (always/summary/hidden), eligibility checks, and trust-aware security scanning. Community imports support packaged official/<skill> skills, skills-sh, clawhub, lobehub, claude-marketplace, well-known, and explicit GitHub repo/path sources via hybridclaw skill import or /skill import.

Agent Packaging

diff --git a/skills/excalidraw/SKILL.md b/skills/excalidraw/SKILL.md new file mode 100644 index 00000000..2c7901dc --- /dev/null +++ b/skills/excalidraw/SKILL.md @@ -0,0 +1,146 @@ +--- +name: excalidraw +description: Create and revise editable `.excalidraw` diagrams as Excalidraw JSON for architecture diagrams, flowcharts, sequence diagrams, concept maps, and other hand-drawn explainers. +user-invocable: true +disable-model-invocation: false +metadata: + hybridclaw: + category: publishing + short_description: "Editable Excalidraw diagrams and share links." + tags: + - excalidraw + - diagrams + - flowcharts + - architecture + - visualization + related_skills: + - manim-video + - write-blog-post +--- +# Excalidraw + +Use this skill when the user wants an editable diagram, not just a rendered image. + +Typical requests: + +- architecture or system diagrams +- flowcharts and process maps +- sequence diagrams +- concept maps and explainers +- hand-drawn style visuals that should stay editable in Excalidraw + +Excalidraw files are plain JSON. The default deliverable is a `*.excalidraw` file in the workspace. The user can drag that file into [excalidraw.com](https://excalidraw.com) to view, edit, or export it. + +## Default Workflow + +1. Plan the diagram before writing JSON: title, nodes, connectors, groups, and rough canvas size. +2. Write a valid Excalidraw `elements` array. +3. Wrap the array in the standard file envelope. +4. Save the result as `*.excalidraw`. +5. If the user wants a shareable browser link, run: + +```bash +node skills/excalidraw/scripts/upload.mjs diagram.excalidraw +``` + +The upload helper encrypts the diagram client-side and prints the Excalidraw share URL. + +## File Envelope + +Use this shape unless you are editing an existing file and need to preserve more fields: + +```json +{ + "type": "excalidraw", + "version": 2, + "source": "hybridclaw", + "elements": [], + "appState": { + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} +``` + +When editing an existing `.excalidraw` file, preserve `appState`, `files`, and any other existing top-level keys unless the user asked for a deliberate reset. + +## Rules + +- Use Excalidraw JSON, not SVG or HTML, unless the user explicitly asked for another format. +- For labeled shapes or arrows, create a separate `text` element and bind it with `containerId` plus the container's `boundElements`. +- Do **not** invent a `"label"` property on rectangles, diamonds, ellipses, or arrows. Excalidraw ignores it. +- Place a bound text element immediately after its container in the `elements` array. +- Use readable sizes: `fontSize` 16+ for normal labels, 20+ for titles, and at least `120x60` for labeled boxes. +- Leave about `20-30px` of space between major elements. +- Prefer short stable ids such as `api`, `text-api`, `arrow-api-db`. +- Avoid emoji and decorative Unicode. Stick to plain text that Excalidraw renders reliably. +- Default to a white background with dark text unless the user explicitly asks for dark mode. +- For arrows, `points` are offsets relative to the arrow's `x` and `y`. + +## Core Patterns + +### Labeled Rectangle + +```json +[ + { + "type": "rectangle", + "id": "api", + "x": 120, + "y": 120, + "width": 220, + "height": 80, + "roundness": { "type": 3 }, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "boundElements": [{ "id": "text-api", "type": "text" }] + }, + { + "type": "text", + "id": "text-api", + "x": 130, + "y": 145, + "width": 200, + "height": 24, + "text": "API Service", + "fontSize": 20, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "api", + "originalText": "API Service", + "autoResize": true + } +] +``` + +### Arrow Between Shapes + +```json +{ + "type": "arrow", + "id": "arrow-api-db", + "x": 340, + "y": 160, + "width": 180, + "height": 0, + "points": [[0, 0], [180, 0]], + "endArrowhead": "arrow", + "startBinding": { "elementId": "api", "fixedPoint": [1, 0.5] }, + "endBinding": { "elementId": "db", "fixedPoint": [0, 0.5] } +} +``` + +## Reference Files + +- For palette and contrast guidance, read [references/colors.md](references/colors.md). +- For copy-pasteable diagram patterns, read [references/examples.md](references/examples.md). +- For dark-background diagrams, read [references/dark-mode.md](references/dark-mode.md). + +## Anti-Patterns + +- Do not cram many tiny nodes into one canvas when two simpler diagrams would read better. +- Do not put all shapes first and all text last; that usually breaks layering and bindings. +- Do not guess at Excalidraw-only properties you have not already seen in a working example. +- Do not replace an editable diagram request with a static PNG export unless the user asked for the export. diff --git a/skills/excalidraw/agents/openai.yaml b/skills/excalidraw/agents/openai.yaml new file mode 100644 index 00000000..7dcafd54 --- /dev/null +++ b/skills/excalidraw/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Excalidraw" + short_description: "Editable Excalidraw diagrams and share links." + default_prompt: "Use $excalidraw to create an editable Excalidraw diagram for this system or process and save the .excalidraw file in the workspace." diff --git a/skills/excalidraw/references/colors.md b/skills/excalidraw/references/colors.md new file mode 100644 index 00000000..4d47d777 --- /dev/null +++ b/skills/excalidraw/references/colors.md @@ -0,0 +1,45 @@ +# Excalidraw Palette + +Use a small, consistent palette. Excalidraw diagrams look better when color meaning stays stable across the canvas. + +## Fill Colors + +| Use | Fill | Hex | +|-----|------|-----| +| Primary nodes | Light Blue | `#a5d8ff` | +| Outputs / success | Light Green | `#b2f2bb` | +| Warnings / external systems | Light Orange | `#ffd8a8` | +| Processing / orchestration | Light Purple | `#d0bfff` | +| Errors / critical states | Light Red | `#ffc9c9` | +| Notes / decisions | Light Yellow | `#fff3bf` | +| Storage / data | Light Teal | `#c3fae8` | + +## Stroke And Accent Colors + +| Use | Stroke | Hex | +|-----|--------|-----| +| Default outline / text | Dark Gray | `#1e1e1e` | +| Blue accent | Blue | `#4a9eed` | +| Green accent | Green | `#22c55e` | +| Orange accent | Amber | `#f59e0b` | +| Red accent | Red | `#ef4444` | +| Purple accent | Purple | `#8b5cf6` | + +## Background Zones + +For layered diagrams, use large low-opacity rectangles behind content: + +| Layer | Color | Hex | +|-------|-------|-----| +| UI / frontend | Blue zone | `#dbe4ff` | +| Logic / agent layer | Purple zone | `#e5dbff` | +| Data / tools layer | Green zone | `#d3f9d8` | + +Use `opacity: 30-35` for those background zones so they do not overpower the main nodes. + +## Contrast Rules + +- On white backgrounds, prefer `#1e1e1e` for text. +- Secondary text on white should stay at or darker than `#757575`. +- Do not use pale gray text on white. +- On light fills, keep text dark; do not match the fill color with bright text. diff --git a/skills/excalidraw/references/dark-mode.md b/skills/excalidraw/references/dark-mode.md new file mode 100644 index 00000000..fa918d80 --- /dev/null +++ b/skills/excalidraw/references/dark-mode.md @@ -0,0 +1,37 @@ +# Dark Mode + +For dark diagrams, add a large background rectangle as the first element in the array: + +```json +{ + "type": "rectangle", + "id": "dark-bg", + "x": -4000, + "y": -3000, + "width": 10000, + "height": 7500, + "backgroundColor": "#1e1e2e", + "fillStyle": "solid", + "strokeColor": "transparent", + "strokeWidth": 0 +} +``` + +## Text On Dark Backgrounds + +- Primary text: `#e5e5e5` +- Secondary text: `#a0a0a0` +- Do not use the default `#1e1e1e` text color on a dark background. + +## Useful Dark Fills + +| Use | Fill | Hex | +|-----|------|-----| +| Primary nodes | Dark Blue | `#1e3a5f` | +| Success / output | Dark Green | `#1a4d2e` | +| Processing | Dark Purple | `#2d1b69` | +| Warning | Dark Orange | `#5c3d1a` | +| Error | Dark Red | `#5c1a1a` | +| Storage | Dark Teal | `#1a4d4d` | + +Keep bright stroke colors for arrows and borders so the diagram still reads clearly. diff --git a/skills/excalidraw/references/examples.md b/skills/excalidraw/references/examples.md new file mode 100644 index 00000000..9e9e1591 --- /dev/null +++ b/skills/excalidraw/references/examples.md @@ -0,0 +1,266 @@ +# Examples + +These are complete `elements` arrays. Wrap them in the `.excalidraw` envelope from `SKILL.md` before saving. + +## Example 1: Simple Flow + +```json +[ + { + "type": "text", + "id": "title", + "x": 270, + "y": 40, + "text": "Simple Flow", + "fontSize": 28, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "originalText": "Simple Flow", + "autoResize": true + }, + { + "type": "rectangle", + "id": "start", + "x": 100, + "y": 130, + "width": 200, + "height": 90, + "roundness": { "type": 3 }, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "boundElements": [{ "id": "text-start", "type": "text" }] + }, + { + "type": "text", + "id": "text-start", + "x": 110, + "y": 160, + "width": 180, + "height": 24, + "text": "Start", + "fontSize": 20, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "start", + "originalText": "Start", + "autoResize": true + }, + { + "type": "rectangle", + "id": "finish", + "x": 430, + "y": 130, + "width": 200, + "height": 90, + "roundness": { "type": 3 }, + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "boundElements": [{ "id": "text-finish", "type": "text" }] + }, + { + "type": "text", + "id": "text-finish", + "x": 440, + "y": 160, + "width": 180, + "height": 24, + "text": "Finish", + "fontSize": 20, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "finish", + "originalText": "Finish", + "autoResize": true + }, + { + "type": "arrow", + "id": "arrow-start-finish", + "x": 300, + "y": 175, + "width": 130, + "height": 0, + "points": [[0, 0], [130, 0]], + "endArrowhead": "arrow", + "startBinding": { "elementId": "start", "fixedPoint": [1, 0.5] }, + "endBinding": { "elementId": "finish", "fixedPoint": [0, 0.5] } + } +] +``` + +## Example 2: Client -> API -> Database + +```json +[ + { + "type": "text", + "id": "title", + "x": 210, + "y": 30, + "text": "Service Overview", + "fontSize": 28, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "originalText": "Service Overview", + "autoResize": true + }, + { + "type": "rectangle", + "id": "client", + "x": 70, + "y": 150, + "width": 180, + "height": 80, + "roundness": { "type": 3 }, + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "boundElements": [{ "id": "text-client", "type": "text" }] + }, + { + "type": "text", + "id": "text-client", + "x": 80, + "y": 178, + "width": 160, + "height": 24, + "text": "Web Client", + "fontSize": 20, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "client", + "originalText": "Web Client", + "autoResize": true + }, + { + "type": "rectangle", + "id": "api", + "x": 330, + "y": 150, + "width": 200, + "height": 80, + "roundness": { "type": 3 }, + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "boundElements": [{ "id": "text-api", "type": "text" }] + }, + { + "type": "text", + "id": "text-api", + "x": 340, + "y": 178, + "width": 180, + "height": 24, + "text": "API Service", + "fontSize": 20, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "api", + "originalText": "API Service", + "autoResize": true + }, + { + "type": "rectangle", + "id": "db", + "x": 620, + "y": 150, + "width": 190, + "height": 80, + "roundness": { "type": 3 }, + "backgroundColor": "#c3fae8", + "fillStyle": "solid", + "boundElements": [{ "id": "text-db", "type": "text" }] + }, + { + "type": "text", + "id": "text-db", + "x": 630, + "y": 178, + "width": 170, + "height": 24, + "text": "Database", + "fontSize": 20, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "db", + "originalText": "Database", + "autoResize": true + }, + { + "type": "arrow", + "id": "arrow-client-api", + "x": 250, + "y": 190, + "width": 80, + "height": 0, + "points": [[0, 0], [80, 0]], + "endArrowhead": "arrow", + "boundElements": [{ "id": "text-http", "type": "text" }], + "startBinding": { "elementId": "client", "fixedPoint": [1, 0.5] }, + "endBinding": { "elementId": "api", "fixedPoint": [0, 0.5] } + }, + { + "type": "text", + "id": "text-http", + "x": 270, + "y": 162, + "width": 40, + "height": 20, + "text": "HTTP", + "fontSize": 16, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "arrow-client-api", + "originalText": "HTTP", + "autoResize": true + }, + { + "type": "arrow", + "id": "arrow-api-db", + "x": 530, + "y": 190, + "width": 90, + "height": 0, + "points": [[0, 0], [90, 0]], + "endArrowhead": "arrow", + "boundElements": [{ "id": "text-sql", "type": "text" }], + "startBinding": { "elementId": "api", "fixedPoint": [1, 0.5] }, + "endBinding": { "elementId": "db", "fixedPoint": [0, 0.5] } + }, + { + "type": "text", + "id": "text-sql", + "x": 555, + "y": 162, + "width": 40, + "height": 20, + "text": "SQL", + "fontSize": 16, + "fontFamily": 1, + "strokeColor": "#1e1e1e", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "arrow-api-db", + "originalText": "SQL", + "autoResize": true + } +] +``` + +## Common Mistakes + +- Do not use `"label"` on shapes or arrows. +- Do not forget `containerId` on bound text. +- Do not forget `boundElements` on the container. +- Do not use tiny labels or cramped node spacing. +- Do not put bound text far away from the shape it belongs to in the array. diff --git a/skills/excalidraw/scripts/upload.mjs b/skills/excalidraw/scripts/upload.mjs new file mode 100644 index 00000000..f61a412a --- /dev/null +++ b/skills/excalidraw/scripts/upload.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import { createCipheriv, randomBytes } from 'node:crypto'; +import { deflateSync } from 'node:zlib'; + +const UPLOAD_URL = 'https://json.excalidraw.com/api/v2/post/'; + +function concatBuffers(...buffers) { + const parts = []; + const version = Buffer.alloc(4); + version.writeUInt32BE(1, 0); + parts.push(version); + + for (const buffer of buffers) { + const length = Buffer.alloc(4); + length.writeUInt32BE(buffer.length, 0); + parts.push(length, buffer); + } + + return Buffer.concat(parts); +} + +function validateDocument(document) { + if (!document || typeof document !== 'object' || Array.isArray(document)) { + throw new Error('File must contain a JSON object.'); + } + + if (!Array.isArray(document.elements)) { + throw new Error('File must contain an "elements" array.'); + } +} + +async function uploadExcalidrawJson(excalidrawJson) { + const fileMetadata = Buffer.from(JSON.stringify({}), 'utf8'); + const dataBytes = Buffer.from(excalidrawJson, 'utf8'); + const innerPayload = concatBuffers(fileMetadata, dataBytes); + const compressed = deflateSync(innerPayload); + + const key = randomBytes(16); + const iv = randomBytes(12); + const cipher = createCipheriv('aes-128-gcm', key, iv); + const ciphertext = Buffer.concat([ + cipher.update(compressed), + cipher.final(), + cipher.getAuthTag(), + ]); + + const encodingMetadata = Buffer.from( + JSON.stringify({ + version: 2, + compression: 'pako@1', + encryption: 'AES-GCM', + }), + 'utf8', + ); + const payload = concatBuffers(encodingMetadata, iv, ciphertext); + + const response = await fetch(UPLOAD_URL, { + method: 'POST', + body: payload, + }); + + if (!response.ok) { + throw new Error(`Upload failed with HTTP ${response.status}`); + } + + const result = await response.json(); + if (!result?.id) { + throw new Error(`Upload returned no file id: ${JSON.stringify(result)}`); + } + + return `https://excalidraw.com/#json=${result.id},${key.toString('base64url')}`; +} + +async function main() { + const filePath = process.argv[2]; + if (!filePath) { + throw new Error('Usage: node skills/excalidraw/scripts/upload.mjs '); + } + + const content = fs.readFileSync(filePath, 'utf8'); + const document = JSON.parse(content); + validateDocument(document); + + const url = await uploadExcalidrawJson(content); + process.stdout.write(`${url}\n`); +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`Error: ${message}\n`); + process.exitCode = 1; +});