chore(release): merge v0.7.5 pre-release into main

Brings in three issue fixes from the 0.7.5 branch:

- fix(ai): unwrap fetch error.cause for actionable network failures (#121)
- fix(ci): always source-build agent-native and bundle to napi/ root
- docs: drop op export from CLI docs and clarify pen-mcp usage (#116, #117)

Release: https://github.com/ZSeven-W/openpencil/releases/tag/v0.7.5
This commit is contained in:
Kayshen-X 2026-04-26 19:23:56 +08:00
commit 902cc6d227
59 changed files with 326 additions and 336 deletions

View file

@ -22,15 +22,19 @@ jobs:
- os: macos-latest
platform: mac-arm64
build_args: --mac --arm64
zig_target: aarch64-macos
- os: macos-latest
platform: mac-x64
build_args: --mac --x64
zig_target: x86_64-macos
- os: windows-latest
platform: win
build_args: --win
zig_target: x86_64-windows
- os: ubuntu-latest
platform: linux
build_args: --linux
zig_target: x86_64-linux
steps:
- uses: actions/checkout@v4
@ -50,8 +54,20 @@ jobs:
version: 0.15.2
- name: Install dependencies
env:
OPENPENCIL_REQUIRE_AGENT_NATIVE: '1'
ZIG_TARGET: ${{ matrix.zig_target }}
run: bun install --frozen-lockfile
- name: Verify agent-native binary
shell: bash
run: |
if [ ! -f packages/agent-native/napi/agent_napi.node ]; then
echo "::error::packages/agent-native/napi/agent_napi.node missing — electron-builder would ship without it."
exit 1
fi
ls -la packages/agent-native/napi/agent_napi.node
- name: Build web (electron target)
run: bun --bun run build
env:

View file

@ -34,6 +34,8 @@ jobs:
version: 0.15.2
- name: Install dependencies
env:
OPENPENCIL_SKIP_AGENT_NATIVE: '1'
run: bun install --frozen-lockfile
- name: Get version

View file

@ -108,7 +108,6 @@ The `op` command-line tool controls the desktop app or web server from the termi
- **Design:** `op design <dsl|@file|->` — batch design DSL operations
- **Document:** `op open`, `op save`, `op get`, `op selection`
- **Nodes:** `op insert`, `op update`, `op delete`, `op move`, `op copy`, `op replace`
- **Export:** `op export <react|html|vue|svelte|flutter|swiftui|compose|rn|css>`
- **Cross-platform:** macOS, Windows (NSIS/portable), Linux (AppImage/deb/snap/flatpak)
### CI / CD

View file

@ -129,7 +129,6 @@ The `op` command-line tool controls the desktop app or web server from the termi
- **Design:** `op design <dsl|@file|->` — batch design DSL operations
- **Document:** `op open`, `op save`, `op get`, `op selection`
- **Nodes:** `op insert`, `op update`, `op delete`, `op move`, `op copy`, `op replace`
- **Export:** `op export <react|html|vue|svelte|flutter|swiftui|compose|rn|css>`
- **Cross-platform:** macOS, Windows (NSIS/portable), Linux (AppImage/deb/snap/flatpak)
### CI / CD

View file

@ -89,7 +89,7 @@ Web-App + native Desktop-Anwendung auf macOS, Windows und Linux über Electron.
### ⌨️ CLI — `op`
Steuern Sie das Design-Tool vom Terminal aus. `op design`, `op insert`, `op export` — Batch-Design-DSL, Knotenmanipulation, Code-Export. Pipe-Eingabe von Dateien oder stdin. Funktioniert mit der Desktop-App oder dem Webserver.
Steuern Sie das Design-Tool vom Terminal aus. `op design`, `op insert` — Batch-Design-DSL, Knotenmanipulation. Pipe-Eingabe von Dateien oder stdin. Funktioniert mit der Desktop-App oder dem Webserver.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Desktop-App starten
op design @landing.txt # Batch-Design aus Datei
op insert '{"type":"RECT"}' # Knoten einfügen
op export react --out . # Nach React + Tailwind exportieren
op import:figma design.fig # Figma-Datei importieren
cat design.dsl | op design - # Pipe von stdin
```
@ -294,7 +293,7 @@ Unterstützt drei Eingabemethoden: Inline-String, `@filepath` (aus Datei lesen)
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — Terminal-Steuerung, Batch-Design-DSL, Code-Export |
| **CLI** | `op` — Terminal-Steuerung, Batch-Design-DSL |
| **KI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Laufzeit** | Bun · Vite 7 |
| **Dateiformat** | `.op` — JSON-basiert, menschenlesbar, Git-freundlich |

View file

@ -89,7 +89,7 @@ Aplicación web + escritorio nativo en macOS, Windows y Linux mediante Electron.
### ⌨️ CLI — `op`
Controla la herramienta de diseño desde la terminal. `op design`, `op insert`, `op export` — DSL de diseño por lotes, manipulación de nodos, exportación de código. Entrada por pipe desde archivos o stdin. Funciona con la app de escritorio o el servidor web.
Controla la herramienta de diseño desde la terminal. `op design`, `op insert` — DSL de diseño por lotes, manipulación de nodos. Entrada por pipe desde archivos o stdin. Funciona con la app de escritorio o el servidor web.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Iniciar la app de escritorio
op design @landing.txt # Diseño por lotes desde archivo
op insert '{"type":"RECT"}' # Insertar un nodo
op export react --out . # Exportar a React + Tailwind
op import:figma design.fig # Importar archivo de Figma
cat design.dsl | op design - # Entrada por pipe desde stdin
```
@ -294,7 +293,7 @@ Soporta tres métodos de entrada: cadena inline, `@filepath` (leer desde archivo
| **Estado** | Zustand v5 |
| **Servidor** | Nitro |
| **Escritorio** | Electron 35 |
| **CLI** | `op` — control desde terminal, DSL de diseño por lotes, exportación de código |
| **CLI** | `op` — control desde terminal, DSL de diseño por lotes |
| **IA** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Formato de archivo** | `.op` — basado en JSON, legible por humanos, compatible con Git |

View file

@ -89,7 +89,7 @@ Application web + bureau natif sur macOS, Windows et Linux via Electron. Mises
### ⌨️ CLI — `op`
Contrôlez l'outil de design depuis le terminal. `op design`, `op insert`, `op export` — DSL de design par lots, manipulation de nœuds, export de code. Entrée par pipe depuis des fichiers ou stdin. Fonctionne avec l'app de bureau ou le serveur web.
Contrôlez l'outil de design depuis le terminal. `op design`, `op insert` — DSL de design par lots, manipulation de nœuds. Entrée par pipe depuis des fichiers ou stdin. Fonctionne avec l'app de bureau ou le serveur web.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Lancer l'app de bureau
op design @landing.txt # Design par lots depuis un fichier
op insert '{"type":"RECT"}' # Insérer un nœud
op export react --out . # Exporter en React + Tailwind
op import:figma design.fig # Importer un fichier Figma
cat design.dsl | op design - # Pipe depuis stdin
```
@ -294,7 +293,7 @@ Supporte trois méthodes d'entrée : chaîne en ligne, `@filepath` (lecture depu
| **État** | Zustand v5 |
| **Serveur** | Nitro |
| **Bureau** | Electron 35 |
| **CLI** | `op` — contrôle depuis le terminal, DSL de design par lots, export de code |
| **CLI** | `op` — contrôle depuis le terminal, DSL de design par lots |
| **IA** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Format de fichier** | `.op` — basé sur JSON, lisible par l'humain, compatible Git |

View file

@ -89,7 +89,7 @@ Claude Code, Codex, Gemini, OpenCode, Kiro, या Copilot CLIs में वन
### ⌨️ CLI — `op`
अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें। `op design`, `op insert`, `op export` — बैच डिज़ाइन DSL, नोड मैनिपुलेशन, कोड एक्सपोर्ट। फ़ाइलों या stdin से पाइप करें। डेस्कटॉप ऐप या वेब सर्वर के साथ काम करता है।
अपने टर्मिनल से डिज़ाइन टूल को नियंत्रित करें। `op design`, `op insert` — बैच डिज़ाइन DSL, नोड मैनिपुलेशन। फ़ाइलों या stdin से पाइप करें। डेस्कटॉप ऐप या वेब सर्वर के साथ काम करता है।
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # डेस्कटॉप ऐप लॉन्च करें
op design @landing.txt # फ़ाइल से बैच डिज़ाइन
op insert '{"type":"RECT"}' # एक नोड डालें
op export react --out . # React + Tailwind में एक्सपोर्ट
op import:figma design.fig # Figma फ़ाइल इम्पोर्ट करें
cat design.dsl | op design - # stdin से पाइप करें
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # stdin से पाइप करें
| **स्टेट** | Zustand v5 |
| **सर्वर** | Nitro |
| **डेस्कटॉप** | Electron 35 |
| **CLI** | `op` — टर्मिनल नियंत्रण, बैच डिज़ाइन DSL, कोड एक्सपोर्ट |
| **CLI** | `op` — टर्मिनल नियंत्रण, बैच डिज़ाइन DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **रनटाइम** | Bun · Vite 7 |
| **फ़ाइल फ़ॉर्मेट** | `.op` — JSON-आधारित, मानव-पठनीय, Git-फ्रेंडली |

View file

@ -89,7 +89,7 @@ Aplikasi web + desktop native di macOS, Windows, dan Linux melalui Electron. Pem
### ⌨️ CLI — `op`
Kontrol alat desain dari terminal Anda. `op design`, `op insert`, `op export` — batch design DSL, manipulasi node, ekspor kode. Pipe dari file atau stdin. Bekerja dengan aplikasi desktop atau web server.
Kontrol alat desain dari terminal Anda. `op design`, `op insert` — batch design DSL, manipulasi node. Pipe dari file atau stdin. Bekerja dengan aplikasi desktop atau web server.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Jalankan aplikasi desktop
op design @landing.txt # Desain batch dari file
op insert '{"type":"RECT"}' # Sisipkan sebuah node
op export react --out . # Ekspor ke React + Tailwind
op import:figma design.fig # Impor file Figma
cat design.dsl | op design - # Pipe dari stdin
```
@ -294,7 +293,7 @@ Mendukung tiga metode input: string inline, `@filepath` (baca dari file), atau `
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — kontrol terminal, batch design DSL, ekspor kode |
| **CLI** | `op` — kontrol terminal, batch design DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Format file** | `.op` — berbasis JSON, mudah dibaca manusia, ramah Git |

View file

@ -89,7 +89,7 @@ Web アプリ + Electron による macOS・Windows・Linux ネイティブデス
### ⌨️ CLI — `op`
ターミナルからデザインツールを操作。`op design`、`op insert`、`op export` — バッチデザインDSL、ード操作、コードエクスポート。ファイルやstdinからのパイプ入力に対応。デスクトップアプリまたはWebサーバーと連携。
ターミナルからデザインツールを操作。`op design`、`op insert` — バッチデザインDSL、ード操作。ファイルやstdinからのパイプ入力に対応。デスクトップアプリまたはWebサーバーと連携。
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # デスクトップアプリを起動
op design @landing.txt # ファイルからバッチデザイン
op insert '{"type":"RECT"}' # ノードを挿入
op export react --out . # React + Tailwind にエクスポート
op import:figma design.fig # Figma ファイルをインポート
cat design.dsl | op design - # stdin からパイプ入力
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # stdin からパイプ入力
| **状態管理** | Zustand v5 |
| **サーバー** | Nitro |
| **デスクトップ** | Electron 35 |
| **CLI** | `op` — ターミナル制御、バッチデザインDSL、コードエクスポート |
| **CLI** | `op` — ターミナル制御、バッチデザインDSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **ランタイム** | Bun · Vite 7 |
| **ファイル形式** | `.op` — JSON ベース、人間が読みやすく、Git フレンドリー |

View file

@ -89,7 +89,7 @@ Claude Code, Codex, Gemini, OpenCode, Kiro 또는 Copilot CLI에 원클릭 설
### ⌨️ CLI — `op`
터미널에서 디자인 도구 제어. `op design`, `op insert`, `op export` — 배치 디자인 DSL, 노드 조작, 코드 내보내기. 파일이나 stdin에서 파이프 입력 지원. 데스크톱 앱 또는 웹 서버와 연동.
터미널에서 디자인 도구 제어. `op design`, `op insert` — 배치 디자인 DSL, 노드 조작. 파일이나 stdin에서 파이프 입력 지원. 데스크톱 앱 또는 웹 서버와 연동.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # 데스크톱 앱 실행
op design @landing.txt # 파일에서 배치 디자인
op insert '{"type":"RECT"}' # 노드 삽입
op export react --out . # React + Tailwind로 내보내기
op import:figma design.fig # Figma 파일 가져오기
cat design.dsl | op design - # stdin에서 파이프 입력
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # stdin에서 파이프 입력
| **상태 관리** | Zustand v5 |
| **서버** | Nitro |
| **데스크톱** | Electron 35 |
| **CLI** | `op` — 터미널 제어, 배치 디자인 DSL, 코드 내보내기 |
| **CLI** | `op` — 터미널 제어, 배치 디자인 DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **런타임** | Bun · Vite 7 |
| **파일 형식** | `.op` — JSON 기반, 사람이 읽을 수 있는, Git 친화적 |

View file

@ -96,7 +96,7 @@ Web app + native desktop on macOS, Windows, and Linux via Electron. Auto-updates
### ⌨️ CLI — `op`
Control the design tool from your terminal. `op design`, `op insert`, `op export` — batch design DSL, node manipulation, code export. Pipe in from files or stdin. Works with desktop app or web server.
Control the design tool from your terminal. `op design`, `op insert` — batch design DSL, node manipulation. Pipe in from files or stdin. Works with desktop app or web server.
</td>
</tr>
@ -275,7 +275,6 @@ npm install -g @zseven-w/openpencil
op start # Launch desktop app
op design @landing.txt # Batch design from file
op insert '{"type":"RECT"}' # Insert a node
op export react --out . # Export to React + Tailwind
op import:figma design.fig # Import Figma file
cat design.dsl | op design - # Pipe from stdin
```
@ -350,7 +349,7 @@ Supports three input methods: inline string, `@filepath` (read from file), or `-
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — terminal control, batch design DSL, code export |
| **CLI** | `op` — terminal control, batch design DSL |
| **AI** | agent-native (Zig NAPI) · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Lint** | oxlint · oxfmt |

View file

@ -89,7 +89,7 @@ App web + desktop nativo no macOS, Windows e Linux via Electron. Atualização a
### ⌨️ CLI — `op`
Controle a ferramenta de design pelo terminal. `op design`, `op insert`, `op export` — DSL de design em lote, manipulação de nós, exportação de código. Entrada por pipe de arquivos ou stdin. Funciona com o app desktop ou servidor web.
Controle a ferramenta de design pelo terminal. `op design`, `op insert` — DSL de design em lote, manipulação de nós. Entrada por pipe de arquivos ou stdin. Funciona com o app desktop ou servidor web.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Iniciar app desktop
op design @landing.txt # Design em lote a partir de arquivo
op insert '{"type":"RECT"}' # Inserir um nó
op export react --out . # Exportar para React + Tailwind
op import:figma design.fig # Importar arquivo Figma
cat design.dsl | op design - # Entrada por pipe via stdin
```
@ -294,7 +293,7 @@ Suporta três métodos de entrada: string inline, `@filepath` (ler de arquivo) o
| **Estado** | Zustand v5 |
| **Servidor** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — controle pelo terminal, DSL de design em lote, exportação de código |
| **CLI** | `op` — controle pelo terminal, DSL de design em lote |
| **IA** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Formato de arquivo** | `.op` — baseado em JSON, legível por humanos, compatível com Git |

View file

@ -89,7 +89,7 @@
### ⌨️ CLI — `op`
Управляйте инструментом дизайна из терминала. `op design`, `op insert`, `op export` — пакетный DSL дизайна, манипуляция узлами, экспорт кода. Ввод через pipe из файлов или stdin. Работает с десктопным приложением или веб-сервером.
Управляйте инструментом дизайна из терминала. `op design`, `op insert` — пакетный DSL дизайна, манипуляция узлами. Ввод через pipe из файлов или stdin. Работает с десктопным приложением или веб-сервером.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Запустить десктопное приложение
op design @landing.txt # Пакетный дизайн из файла
op insert '{"type":"RECT"}' # Вставить узел
op export react --out . # Экспорт в React + Tailwind
op import:figma design.fig # Импортировать файл Figma
cat design.dsl | op design - # Передача через stdin
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # Передача через stdin
| **Состояние** | Zustand v5 |
| **Сервер** | Nitro |
| **Десктоп** | Electron 35 |
| **CLI** | `op` — управление из терминала, пакетный DSL дизайна, экспорт кода |
| **CLI** | `op` — управление из терминала, пакетный DSL дизайна |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Среда выполнения** | Bun · Vite 7 |
| **Формат файла** | `.op` — на основе JSON, удобочитаемый, дружественный к Git |

View file

@ -89,7 +89,7 @@ Orchestrator แบ่งหน้าที่ซับซ้อนออกเ
### ⌨️ CLI — `op`
ควบคุมเครื่องมือออกแบบจาก terminal ของคุณ `op design`, `op insert`, `op export` — batch design DSL, จัดการ node, ส่งออกโค้ด Pipe จากไฟล์หรือ stdin ทำงานร่วมกับแอปเดสก์ท็อปหรือ web server
ควบคุมเครื่องมือออกแบบจาก terminal ของคุณ `op design`, `op insert` — batch design DSL, จัดการ node Pipe จากไฟล์หรือ stdin ทำงานร่วมกับแอปเดสก์ท็อปหรือ web server
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # เปิดแอปเดสก์ท็อป
op design @landing.txt # ออกแบบแบบ batch จากไฟล์
op insert '{"type":"RECT"}' # แทรก node
op export react --out . # ส่งออกเป็น React + Tailwind
op import:figma design.fig # นำเข้าไฟล์ Figma
cat design.dsl | op design - # Pipe จาก stdin
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # Pipe จาก stdin
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — ควบคุมจาก terminal, batch design DSL, ส่งออกโค้ด |
| **CLI** | `op` — ควบคุมจาก terminal, batch design DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **รูปแบบไฟล์** | `.op` — ใช้ JSON, อ่านได้โดยมนุษย์, Git-friendly |

View file

@ -89,7 +89,7 @@ Web uygulaması + Electron ile macOS, Windows ve Linux'ta yerel masaüstü. GitH
### ⌨️ CLI — `op`
Tasarım aracını terminalinizden kontrol edin. `op design`, `op insert`, `op export` — toplu tasarım DSL, düğüm manipülasyonu, kod dışa aktarımı. Dosyalardan veya stdin'den pipe ile besleyin. Masaüstü uygulama veya web sunucusuyla çalışır.
Tasarım aracını terminalinizden kontrol edin. `op design`, `op insert` — toplu tasarım DSL, düğüm manipülasyonu. Dosyalardan veya stdin'den pipe ile besleyin. Masaüstü uygulama veya web sunucusuyla çalışır.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Masaüstü uygulamayı başlat
op design @landing.txt # Dosyadan toplu tasarım
op insert '{"type":"RECT"}' # Bir düğüm ekle
op export react --out . # React + Tailwind'e dışa aktar
op import:figma design.fig # Figma dosyasını içe aktar
cat design.dsl | op design - # stdin'den pipe ile besle
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # stdin'den pipe ile besle
| **Durum Yönetimi** | Zustand v5 |
| **Sunucu** | Nitro |
| **Masaüstü** | Electron 35 |
| **CLI** | `op` — terminal kontrolü, toplu tasarım DSL, kod dışa aktarımı |
| **CLI** | `op` — terminal kontrolü, toplu tasarım DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Çalışma Ortamı** | Bun · Vite 7 |
| **Dosya Formatı** | `.op` — JSON tabanlı, insan tarafından okunabilir, Git dostu |

View file

@ -89,7 +89,7 @@ Tệp `.op` là JSON — dễ đọc, thân thiện Git, dễ so sánh khác bi
### ⌨️ CLI — `op`
Điều khiển công cụ thiết kế từ terminal của bạn. `op design`, `op insert`, `op export` — batch design DSL, thao tác node, xuất mã. Pipe từ tệp hoặc stdin. Hoạt động với ứng dụng desktop hoặc web server.
Điều khiển công cụ thiết kế từ terminal của bạn. `op design`, `op insert` — batch design DSL, thao tác node. Pipe từ tệp hoặc stdin. Hoạt động với ứng dụng desktop hoặc web server.
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # Khởi chạy ứng dụng desktop
op design @landing.txt # Thiết kế hàng loạt từ tệp
op insert '{"type":"RECT"}' # Chèn một node
op export react --out . # Xuất sang React + Tailwind
op import:figma design.fig # Nhập tệp Figma
cat design.dsl | op design - # Pipe từ stdin
```
@ -294,7 +293,7 @@ Hỗ trợ ba phương thức nhập liệu: chuỗi inline, `@filepath` (đọc
| **Trạng thái** | Zustand v5 |
| **Máy chủ** | Nitro |
| **Desktop** | Electron 35 |
| **CLI** | `op` — điều khiển từ terminal, batch design DSL, xuất mã |
| **CLI** | `op` — điều khiển từ terminal, batch design DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **Runtime** | Bun · Vite 7 |
| **Định dạng tệp** | `.op` — dựa trên JSON, dễ đọc, thân thiện với Git |

View file

@ -89,7 +89,7 @@ Web 應用程式 + 透過 Electron 在 macOS、Windows 和 Linux 上原生執行
### ⌨️ CLI — `op`
從終端機控制設計工具。`op design`、`op insert`、`op export` — 批次設計 DSL、節點操作、程式碼匯出。支援從檔案或 stdin 管道輸入。可搭配桌面應用程式或 Web 伺服器使用。
從終端機控制設計工具。`op design`、`op insert` — 批次設計 DSL、節點操作。支援從檔案或 stdin 管道輸入。可搭配桌面應用程式或 Web 伺服器使用。
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # 啟動桌面應用程式
op design @landing.txt # 從檔案批次設計
op insert '{"type":"RECT"}' # 插入節點
op export react --out . # 匯出為 React + Tailwind
op import:figma design.fig # 匯入 Figma 檔案
cat design.dsl | op design - # 從 stdin 管道輸入
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # 從 stdin 管道輸入
| **狀態管理** | Zustand v5 |
| **伺服器** | Nitro |
| **桌面端** | Electron 35 |
| **CLI** | `op` — 終端機控制、批次設計 DSL、程式碼匯出 |
| **CLI** | `op` — 終端機控制、批次設計 DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **執行環境** | Bun · Vite 7 |
| **檔案格式** | `.op` — 基於 JSON人類可讀對 Git 友好 |

View file

@ -89,7 +89,7 @@ Web 应用 + 通过 Electron 支持 macOS、Windows 和 Linux 原生桌面端。
### ⌨️ CLI — `op`
从终端控制设计工具。`op design`、`op insert`、`op export` — 批量设计 DSL、节点操作、代码导出。支持从文件或 stdin 管道输入。可搭配桌面应用或 Web 服务器使用。
从终端控制设计工具。`op design`、`op insert` — 批量设计 DSL、节点操作。支持从文件或 stdin 管道输入。可搭配桌面应用或 Web 服务器使用。
</td>
<td width="50%">
@ -220,7 +220,6 @@ npm install -g @zseven-w/openpencil
op start # 启动桌面应用
op design @landing.txt # 从文件批量设计
op insert '{"type":"RECT"}' # 插入节点
op export react --out . # 导出为 React + Tailwind
op import:figma design.fig # 导入 Figma 文件
cat design.dsl | op design - # 从 stdin 管道输入
```
@ -294,7 +293,7 @@ cat design.dsl | op design - # 从 stdin 管道输入
| **状态管理** | Zustand v5 |
| **服务器** | Nitro |
| **桌面端** | Electron 35 |
| **CLI** | `op` — 终端控制、批量设计 DSL、代码导出 |
| **CLI** | `op` — 终端控制、批量设计 DSL |
| **AI** | Vercel AI SDK v6 · Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
| **运行时** | Bun · Vite 7 |
| **文件格式** | `.op` — 基于 JSON人类可读对 Git 友好 |

View file

@ -15,7 +15,7 @@ apps/cli/
│ ├── app.ts start, stop, status
│ ├── design.ts design, design:skeleton, design:content, design:refine
│ ├── document.ts open, save, get, selection
│ ├── export.ts export (react, html, vue, svelte, flutter, swiftui, compose, rn, css)
│ ├── codegen.ts codegen:plan, codegen:submit, codegen:assemble, codegen:clean
│ ├── import.ts import:svg, import:figma
│ ├── install.ts install, uninstall (openpencil-skill for AI agents)
│ ├── layout.ts layout, find-space

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Code-Export
```bash
op export <format> [--out file]
# Formate: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variablen und Themes
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Exportacion de codigo
```bash
op export <format> [--out file]
# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variables y temas
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Export de code
```bash
op export <format> [--out file]
# Formats : react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variables et themes
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### कोड निर्यात
```bash
op export <format> [--out file]
# प्रारूप: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### वेरिएबल और थीम
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Ekspor Kode
```bash
op export <format> [--out file]
# Format: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variabel & Tema
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### コードエクスポート
```bash
op export <format> [--out file]
# フォーマット: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 変数とテーマ
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### 코드 내보내기
```bash
op export <format> [--out file]
# 형식: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 변수 및 테마
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Code Export
```bash
op export <format> [--out file]
# Formats: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variables & Themes
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Exportacao de Codigo
```bash
op export <format> [--out file]
# Formatos: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Variaveis e Temas
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Экспорт кода
```bash
op export <format> [--out file]
# Форматы: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Переменные и темы
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### การส่งออกโค้ด
```bash
op export <format> [--out file]
# รูปแบบ: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### ตัวแปรและธีม
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Kod Disari Aktarimi
```bash
op export <format> [--out dosya]
# Formatlar: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Degiskenler ve Temalar
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### Xuất mã nguồn
```bash
op export <format> [--out file]
# Định dạng: react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### Biến và giao diện
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### 程式碼匯出
```bash
op export <format> [--out file]
# 格式react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 變數與主題
```bash

View file

@ -73,13 +73,6 @@ op copy <id> [--parent P]
op replace <id> <json> [--post-process]
```
### 代码导出
```bash
op export <format> [--out file]
# 格式react, html, vue, svelte, flutter, swiftui, compose, rn, css
```
### 变量与主题
```bash

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/openpencil",
"version": "0.7.4",
"version": "0.7.5",
"description": "CLI for OpenPencil — control the design tool from your terminal",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/apps/cli",
"bugs": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/desktop",
"version": "0.7.4",
"version": "0.7.5",
"private": true,
"type": "module"
}

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/web",
"version": "0.7.4",
"version": "0.7.5",
"private": true,
"type": "module",
"dependencies": {

View file

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
buildProviderModelsURL,
formatFetchError,
normalizeBaseURL,
normalizeMemberBaseURL,
normalizeOptionalBaseURL,
@ -63,3 +64,65 @@ describe('provider-url helpers', () => {
);
});
});
describe('formatFetchError', () => {
it('unwraps undici "fetch failed" by reading error.cause', () => {
const cause = Object.assign(new Error('Client network socket disconnected'), {
code: 'ECONNRESET',
});
const err = Object.assign(new TypeError('fetch failed'), { cause });
expect(formatFetchError(err)).toBe('ECONNRESET: Client network socket disconnected');
});
it('skips the prefix when the cause message already contains the code', () => {
const cause = Object.assign(new Error('getaddrinfo ENOTFOUND api.example.com'), {
code: 'ENOTFOUND',
});
const err = Object.assign(new TypeError('fetch failed'), { cause });
expect(formatFetchError(err)).toBe('getaddrinfo ENOTFOUND api.example.com');
});
it('returns just the cause code when message is missing', () => {
const cause = Object.assign(new Error(''), { code: 'ECONNREFUSED' });
const err = Object.assign(new TypeError('fetch failed'), { cause });
expect(formatFetchError(err)).toBe('ECONNREFUSED');
});
it('returns just the cause message when no code', () => {
const cause = new Error('self-signed certificate in certificate chain');
const err = Object.assign(new TypeError('fetch failed'), { cause });
expect(formatFetchError(err)).toBe('self-signed certificate in certificate chain');
});
it('falls back to error.message when no cause', () => {
expect(formatFetchError(new Error('Provider returned 401'))).toBe('Provider returned 401');
});
it('handles non-Error inputs', () => {
expect(formatFetchError('boom')).toBe('Unknown error');
expect(formatFetchError(undefined)).toBe('Unknown error');
});
it('walks nested cause chains', () => {
const root = Object.assign(new Error('Hostname does not match certificate'), {
code: 'ERR_TLS_CERT_ALTNAME_INVALID',
});
const wrapper = Object.assign(new Error('TLS handshake failed'), { cause: root });
const outer = Object.assign(new TypeError('fetch failed'), { cause: wrapper });
expect(formatFetchError(outer)).toBe(
'ERR_TLS_CERT_ALTNAME_INVALID: Hostname does not match certificate',
);
});
it('unwraps AggregateError so per-IP attempt reasons reach the user', () => {
const a = Object.assign(new Error('connect ECONNREFUSED 127.0.0.1:8080'), {
code: 'ECONNREFUSED',
});
const b = Object.assign(new Error('connect ECONNREFUSED ::1:8080'), { code: 'ECONNREFUSED' });
const agg = Object.assign(new AggregateError([a, b], 'all attempts failed'));
const err = Object.assign(new TypeError('fetch failed'), { cause: agg });
expect(formatFetchError(err)).toBe(
'connect ECONNREFUSED 127.0.0.1:8080; connect ECONNREFUSED ::1:8080',
);
});
});

View file

@ -10,7 +10,11 @@ import {
buildSpawnClaudeCodeProcess,
getClaudeAgentDebugFilePath,
} from '../../utils/resolve-claude-agent-env';
import { normalizeOptionalBaseURL, requireOpenAICompatBaseURL } from './provider-url';
import {
formatFetchError,
normalizeOptionalBaseURL,
requireOpenAICompatBaseURL,
} from './provider-url';
// SENSITIVE_LOG_PATTERN + readDebugTail are now canonical in @zseven-w/pen-mcp.
// Re-export here to keep existing consumers (tests, other modules) working.
import { SENSITIVE_LOG_PATTERN, readDebugTail } from '@zseven-w/pen-mcp';
@ -1101,7 +1105,7 @@ function streamViaBuiltin(body: ChatBody) {
destroyProvider(builtinProvider);
}
} catch (error) {
const content = error instanceof Error ? error.message : 'Unknown error';
const content = formatFetchError(error);
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'error', content })}\n\n`),
);

View file

@ -254,17 +254,29 @@ async function fetchFromOpenverse(
}
}
const res = await fetch(url.toString(), { headers });
if (res.status === 429) {
// Rate limited — signal fallback
return null;
}
if (!res.ok) {
return null;
}
// Network failures (ConnectTimeoutError on restricted networks, DNS
// failures, etc.) need to behave like a 429: return null so the caller
// falls back to Wikimedia. Without this, fetch() throws, the throw
// bubbles up to nitro's default handler, and the user sees a 500
// instead of placeholder images.
try {
const res = await fetch(url.toString(), {
headers,
signal: AbortSignal.timeout(8000),
});
if (res.status === 429) {
// Rate limited — signal fallback
return null;
}
if (!res.ok) {
return null;
}
const data = (await res.json()) as OpenverseSearchResponse;
return (data.results ?? []).map(mapOpenverseResult);
const data = (await res.json()) as OpenverseSearchResponse;
return (data.results ?? []).map(mapOpenverseResult);
} catch {
return null;
}
}
async function fetchFromWikimedia(query: string, count: number): Promise<ImageSearchResult[]> {
@ -280,14 +292,21 @@ async function fetchFromWikimedia(query: string, count: number): Promise<ImageSe
url.searchParams.set('format', 'json');
url.searchParams.set('origin', '*');
const res = await fetch(url.toString());
if (!res.ok) return [];
// Same network-failure shielding as Openverse: an empty result is the
// documented fallback signal, returning [] keeps the endpoint at 200
// with placeholder-friendly results instead of bubbling the throw.
try {
const res = await fetch(url.toString(), { signal: AbortSignal.timeout(8000) });
if (!res.ok) return [];
const data = (await res.json()) as WikimediaQueryResponse;
const pages = data.query?.pages;
if (!pages) return [];
const data = (await res.json()) as WikimediaQueryResponse;
const pages = data.query?.pages;
if (!pages) return [];
return mapWikimediaPages(pages);
return mapWikimediaPages(pages);
} catch {
return [];
}
}
// ---------------------------------------------------------------------------

View file

@ -1,5 +1,5 @@
import { defineEventHandler, readBody } from 'h3';
import { buildProviderModelsURL, normalizeOptionalBaseURL } from './provider-url';
import { buildProviderModelsURL, formatFetchError, normalizeOptionalBaseURL } from './provider-url';
interface ProviderModelsBody {
baseURL: string;
@ -63,7 +63,6 @@ export default defineEventHandler(async (event) => {
return { models };
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return { models: [], error: message };
return { models: [], error: formatFetchError(err) };
}
});

View file

@ -63,3 +63,44 @@ export function normalizeMemberBaseURL(
export function buildProviderModelsURL(baseURL: string): string {
return `${normalizeOpenAICompatBaseURL(baseURL) ?? normalizeBaseURL(baseURL)}/models`;
}
/**
* Node's `fetch` (undici) collapses every network-level failure DNS, TLS,
* refused connection, timeout into a single opaque `TypeError: fetch failed`.
* The real reason lives on `error.cause` as a SystemError with `code`/`syscall`/
* `hostname`. Surface that so users can act on it ("ENOTFOUND api.foo.com",
* "self-signed certificate", "ECONNREFUSED 127.0.0.1:443") instead of staring
* at "fetch failed".
*
* Walks the `cause` chain (some failures wrap multiple times) and unwraps
* AggregateError (undici emits one when DNS returns multiple A records and
* every connect attempt fails) so each leaf reason makes it to the user.
*/
export function formatFetchError(error: unknown): string {
const reasons = collectFetchErrorReasons(error);
if (reasons.length === 0) {
return error instanceof Error ? error.message || 'Unknown error' : 'Unknown error';
}
return Array.from(new Set(reasons)).join('; ');
}
function collectFetchErrorReasons(error: unknown, depth = 0): string[] {
if (depth > 5 || !(error instanceof Error)) return [];
const aggregate = (error as { errors?: unknown }).errors;
if (Array.isArray(aggregate) && aggregate.length > 0) {
return aggregate.flatMap((sub) => collectFetchErrorReasons(sub, depth + 1));
}
const cause = (error as { cause?: unknown }).cause;
if (cause instanceof Error) {
return collectFetchErrorReasons(cause, depth + 1);
}
const code = (error as { code?: string }).code;
const message = error.message?.trim();
if (code && message && !message.includes(code)) return [`${code}: ${message}`];
if (message) return [message];
if (code) return [code];
return [];
}

View file

@ -48,7 +48,8 @@ export const BUILTIN_MODEL_LISTS: Partial<
{ id: 'doubao-seed-2.0-pro', name: 'Doubao Seed 2.0 Pro' },
{ id: 'doubao-seed-2.0-lite', name: 'Doubao Seed 2.0 Lite' },
{ id: 'glm-4.7', name: 'GLM-4.7' },
{ id: 'deepseek-v3.2', name: 'DeepSeek V3.2' },
{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro' },
{ id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash' },
{ id: 'kimi-k2.5', name: 'Kimi K2.5' },
{ id: 'minimax-m2.5', name: 'MiniMax M2.5' },
],

View file

@ -50,7 +50,7 @@ export const BUILTIN_PROVIDER_PRESETS: Record<BuiltinProviderPreset, BuiltinPres
altBaseURL: 'https://api.deepseek.com/anthropic',
altType: 'anthropic',
placeholder: 'sk-...',
modelPlaceholder: 'deepseek-chat',
modelPlaceholder: 'deepseek-v4-pro',
},
gemini: {
label: 'Google Gemini',

View file

@ -44,7 +44,42 @@ const MODEL_PROFILES: ModelProfile[] = [
},
{ match: 'gemini-pro', tier: 'standard', thinkingMode: 'disabled', label: 'Gemini Pro' },
{ match: /^gemini-2/, tier: 'standard', thinkingMode: 'disabled', label: 'Gemini 2' },
{ match: 'deepseek', tier: 'standard', thinkingMode: 'disabled', label: 'DeepSeek' },
// DeepSeek v4 series — v4-pro and v4-flash default to thinking enabled;
// the API toggles it via `{"thinking":{"type":"disabled"}}`. Mark
// these as disabled so the app keeps its fast/non-thinking default —
// server reasoning paths that wire DeepSeek's toggle will honor it.
// The Zig openai-compat path doesn't emit the toggle yet, so calls
// through that path still see provider-default thinking until the
// parameter is wired there.
{
match: 'deepseek-v4-pro',
tier: 'full',
thinkingMode: 'disabled',
// Until the Zig openai-compat path actually sends
// `thinking:{type:disabled}`, v4-pro keeps reasoning enabled on
// every request and reasoning tokens explode on long planning
// prompts. Double the timeout windows so orchestrator planning
// doesn't fall back on the first big request. Drop this back to 1
// once the toggle is wired.
timeoutMultiplier: 2,
label: 'DeepSeek V4 Pro',
},
{
match: 'deepseek-v4-flash',
tier: 'standard',
thinkingMode: 'disabled',
label: 'DeepSeek V4 Flash',
},
// Legacy aliases — exact match only so future deepseek-* variants
// (e.g. a hypothetical deepseek-r2 with native reasoning) don't
// inherit a forced disabled thinkingMode. These two sunset 2026-07-24
// and DeepSeek auto-routes them to v4-flash today.
{
match: /^deepseek-(chat|reasoner)$/,
tier: 'standard',
thinkingMode: 'disabled',
label: 'DeepSeek (legacy)',
},
// Basic tier — disable thinking, use simplified prompt
{ match: 'claude-haiku', tier: 'basic', thinkingMode: 'disabled', label: 'Claude Haiku' },

View file

@ -1,6 +1,6 @@
{
"name": "openpencil",
"version": "0.7.4",
"version": "0.7.5",
"private": true,
"description": "The world's first open-source AI-native vector design tool and the first to feature concurrent Agent Teams. Design-as-Code. Turn prompts into UI directly on the live canvas. A modern alternative to Pencil.",
"author": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-acp",
"version": "0.7.4",
"version": "0.7.5",
"description": "ACP (Agent Client Protocol) client for OpenPencil — connect to external ACP agents",
"files": [
"src"

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-ai-skills",
"version": "0.7.4",
"version": "0.7.5",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-ai-skills",
"bugs": {
"url": "https://github.com/ZSeven-W/openpencil/issues"

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-core",
"version": "0.7.4",
"version": "0.7.5",
"description": "Core document operations, tree utils, variables, layout engine for OpenPencil",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-core",
"bugs": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-engine",
"version": "0.7.4",
"version": "0.7.5",
"description": "Headless design engine for OpenPencil — zero framework dependencies",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-engine",
"bugs": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-figma",
"version": "0.7.4",
"version": "0.7.5",
"description": "Figma .fig file parser and converter for OpenPencil",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-figma",
"bugs": {

View file

@ -2,13 +2,7 @@
[MCP](https://modelcontextprotocol.io/) server for [OpenPencil](https://github.com/ZSeven-W/openpencil) — enables Claude, GPT, Gemini, and other LLMs to read, create, and modify designs through a standard tool protocol.
## Install
```bash
npm install @zseven-w/pen-mcp
# or
bun add @zseven-w/pen-mcp
```
> **Note:** `pen-mcp` is shipped as part of the OpenPencil app (desktop + web) and is **not a standalone CLI**. The published package ships TypeScript source against workspace-only dependencies and has no `bin` entry, so `npx @zseven-w/pen-mcp` does not work. Run the server from the OpenPencil monorepo or connect external clients to the HTTP endpoint exposed by a running OpenPencil instance.
## Overview
@ -22,28 +16,26 @@ Three workflows are supported:
| **Layered** | `design_skeleton``design_content` × N → `design_refine` | Full-page designs with high fidelity |
| **CRUD** | `batch_get``update_node` / `delete_node` | Reading & modifying existing content |
## Quick Start
## Running the MCP Server
The server supports both **stdio** and **streamable HTTP** transports. The default HTTP endpoint is `http://localhost:3100/mcp`.
### From the monorepo (development)
```bash
# Run as stdio MCP server (for Claude Desktop, Cursor, etc.)
npx @zseven-w/pen-mcp
# Or connect to a running OpenPencil instance
op mcp:dev
git clone https://github.com/ZSeven-W/openpencil.git
cd openpencil && bun install
bun run mcp:dev # starts stdio + HTTP on port 3100
# flags: --http (HTTP only), --stdio (stdio only), --port <n>
```
### Claude Desktop Configuration
### Built-in to the OpenPencil app
```json
{
"mcpServers": {
"openpencil": {
"command": "npx",
"args": ["@zseven-w/pen-mcp"]
}
}
}
```
Launching the desktop or web app automatically starts the MCP server in the background. External MCP clients should connect over HTTP to the running instance — no separate install required.
### Connecting an MCP client
Most MCP-aware clients (Claude Desktop, Cursor, Continue, etc.) accept an HTTP URL pointing at a running server. Point them at `http://localhost:3100/mcp` while the OpenPencil app or `bun run mcp:dev` is running.
## Tools

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-mcp",
"version": "0.7.4",
"version": "0.7.5",
"description": "MCP server, document manager, and tools for OpenPencil",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-mcp",
"bugs": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-react",
"version": "0.7.4",
"version": "0.7.5",
"description": "React UI SDK for OpenPencil — hooks, components, and state bridges for pen-engine",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-react",
"bugs": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-renderer",
"version": "0.7.4",
"version": "0.7.5",
"description": "Standalone CanvasKit/Skia renderer for OpenPencil (.op) design files",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-renderer",
"bugs": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-sdk",
"version": "0.7.4",
"version": "0.7.5",
"description": "OpenPencil SDK — parse, manipulate, and generate code from .op design files",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-sdk",
"bugs": {

View file

@ -1,6 +1,6 @@
{
"name": "@zseven-w/pen-types",
"version": "0.7.4",
"version": "0.7.5",
"description": "Type definitions for OpenPencil document model",
"homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-types",
"bugs": {

View file

@ -1,165 +1,105 @@
#!/usr/bin/env node
// Ensures the Zig NAPI addon binary exists.
// Provisions the Zig NAPI addon binary by building it from source.
//
// Strategy (fastest to slowest):
// 1. Already built / bundled — use it.
// 2. Download prebuilt from the ZSeven-W/agent release whose tag points at
// the submodule's currently checked-out commit. This means CI and local
// installs never need Zig installed, as long as whoever bumped the
// submodule also tagged + published a matching release.
// 3. Build from source with local Zig (slow but authoritative).
// We always build from source on the host so the resulting `agent_napi.node`
// matches the runner's platform/arch. Earlier revisions also tried to download
// a prebuilt from a sibling release repo, but that path was racy: when the
// prebuilt was missing for the current submodule SHA the build fell through
// to source compilation, deposited the binary at `zig-out/napi/...`, and
// electron-builder (which only ships `packages/agent-native/napi/`) silently
// shipped without the addon — every chat request then died at the dynamic
// `@zseven-w/agent-native` import.
//
// Failing all of those is non-fatal — the postinstall wrapper swallows exit
// codes so `bun install` never breaks. Tests / runtime will surface a clear
// "could not locate agent_napi.node" error instead, with instructions.
// Build prerequisite: Zig 0.15+ on PATH. CI workflows install it via
// `mlugg/setup-zig`; local devs install once via their package manager.
//
// Set OPENPENCIL_REQUIRE_AGENT_NATIVE=1 to fail the install when the build
// can't run (electron CI uses this to surface missing prerequisites early).
//
// Set OPENPENCIL_SKIP_AGENT_NATIVE=1 to no-op the script entirely. Useful for
// workflows (npm publish, lint-only CI) that never load the addon at runtime
// and would otherwise pay for a Zig build on every install.
//
// Set ZIG_TARGET to cross-compile for a non-host triple (e.g. on a macOS arm64
// runner build for x86_64-macos with `ZIG_TARGET=x86_64-macos`). Without it
// the build follows the host arch — fine for native runs, wrong when the
// runner doesn't match the artifact you intend to ship.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const REPO = 'ZSeven-W/agent';
const NAPI_DIR = path.join(__dirname, '..', 'packages', 'agent-native', 'napi');
const AGENT_DIR = path.join(__dirname, '..', 'packages', 'agent-native');
const NAPI_DIR = path.join(AGENT_DIR, 'napi');
const ZIG_OUT = path.join(AGENT_DIR, 'zig-out', 'napi', 'agent_napi.node');
const BUNDLED = path.join(NAPI_DIR, 'agent_napi.node');
const STRICT = process.env.OPENPENCIL_REQUIRE_AGENT_NATIVE === '1';
function log(msg) {
console.log(`[agent-native] ${msg}`);
}
function assetNameForHost() {
const p = process.platform; // 'darwin' | 'linux' | 'win32'
const a = process.arch; // 'arm64' | 'x64'
const os = p === 'darwin' ? 'macos' : p === 'win32' ? 'windows' : 'linux';
return `agent_napi-${os}-${a}.node`;
function fail(msg) {
log(msg);
return STRICT ? 1 : 0;
}
function readSubmoduleSha() {
try {
return execSync('git rev-parse HEAD', {
cwd: AGENT_DIR,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
return null;
}
}
function ghJson(url) {
const headers = ['-H', 'Accept: application/vnd.github+json'];
if (process.env.GITHUB_TOKEN) {
headers.push('-H', `Authorization: Bearer ${process.env.GITHUB_TOKEN}`);
}
const raw = execSync(`curl -sLf ${headers.join(' ')} "${url}"`, { encoding: 'utf8' });
return JSON.parse(raw);
}
function findMatchingRelease(submoduleSha) {
if (!submoduleSha) return null;
// Walk tags (paginated) until we find one whose commit SHA matches the
// submodule pointer. Tags are listed newest first, so for a freshly bumped
// submodule we almost always hit on the first page.
for (let page = 1; page <= 3; page += 1) {
let tags;
try {
tags = ghJson(`https://api.github.com/repos/${REPO}/tags?per_page=30&page=${page}`);
} catch {
return null;
}
if (!Array.isArray(tags) || tags.length === 0) return null;
for (const t of tags) {
if (t?.commit?.sha === submoduleSha) return t.name;
}
}
return null;
}
function downloadPrebuilt(tagName) {
let release;
try {
release = ghJson(`https://api.github.com/repos/${REPO}/releases/tags/${tagName}`);
} catch (err) {
log(`No release for tag ${tagName}: ${err.message}`);
return false;
}
const assetName = assetNameForHost();
const asset = release.assets?.find((a) => a.name === assetName);
if (!asset) {
log(
`Release ${tagName} has no asset ${assetName} (built: ${(release.assets ?? []).map((a) => a.name).join(', ') || 'none'}).`,
);
return false;
}
log(`Downloading ${assetName} from release ${tagName}`);
try {
fs.mkdirSync(NAPI_DIR, { recursive: true });
execSync(`curl -sLf -o "${BUNDLED}" "${asset.browser_download_url}"`, { stdio: 'inherit' });
} catch (err) {
log(`Download failed: ${err.message}`);
return false;
}
return fs.existsSync(BUNDLED);
function bundleBinary() {
fs.mkdirSync(NAPI_DIR, { recursive: true });
fs.copyFileSync(ZIG_OUT, BUNDLED);
log(`Bundled binary at ${BUNDLED}.`);
}
function buildFromSource() {
try {
execSync('zig version', { stdio: 'ignore' });
} catch {
log('Zig not installed; cannot build from source.');
return false;
return fail(
'Zig not installed (need 0.15+). Skipping. Install Zig and re-run `bun run agent:build`.',
);
}
log('Building NAPI addon from source (zig build napi)…');
const target = process.env.ZIG_TARGET?.trim();
const targetFlag = target ? ` -Dtarget=${target}` : '';
log(`Building NAPI addon (zig build napi -Doptimize=ReleaseFast${targetFlag})…`);
try {
execSync('zig build napi -Doptimize=ReleaseFast', {
execSync(`zig build napi -Doptimize=ReleaseFast${targetFlag}`, {
cwd: AGENT_DIR,
stdio: 'inherit',
});
} catch (err) {
log(`Source build failed: ${err.message}`);
return false;
return fail(`Zig build failed: ${err.message}`);
}
return fs.existsSync(ZIG_OUT);
if (!fs.existsSync(ZIG_OUT)) {
return fail(`Zig build produced no output at ${ZIG_OUT}.`);
}
bundleBinary();
return 0;
}
function main() {
// 1. Already have it?
if (fs.existsSync(ZIG_OUT) || fs.existsSync(BUNDLED)) {
log('Binary already present, skipping.');
if (process.env.OPENPENCIL_SKIP_AGENT_NATIVE === '1') {
log('OPENPENCIL_SKIP_AGENT_NATIVE=1, skipping native binary provisioning.');
return 0;
}
// Submodule initialized?
if (!fs.existsSync(path.join(NAPI_DIR, 'package.json'))) {
log('Submodule not initialized; run `git submodule update --init`. Skipping.');
return fail('Submodule not initialized; run `git submodule update --init`. Skipping.');
}
// Fast path: binary already in place. Make sure both lookup locations are
// populated so electron-builder (`napi/`) and the runtime loader (which
// checks `zig-out/` first) both find it without re-running the build.
if (fs.existsSync(BUNDLED)) {
log('Binary already present, skipping rebuild.');
return 0;
}
if (fs.existsSync(ZIG_OUT)) {
log('Binary already built; copying into napi/ for electron-builder.');
bundleBinary();
return 0;
}
// 2. Prebuilt release matching submodule SHA?
const sha = readSubmoduleSha();
if (sha) {
const tag = findMatchingRelease(sha);
if (tag) {
if (downloadPrebuilt(tag)) {
log(`Prebuilt ready at ${BUNDLED}.`);
return 0;
}
} else {
log(`No release tag matches submodule ${sha.slice(0, 7)}.`);
}
}
// 3. Source build fallback.
if (buildFromSource()) {
log(`Built at ${ZIG_OUT}.`);
return 0;
}
log('Could not provision agent_napi.node. Tests / runtime will fail loudly until resolved.');
log(
'Options: bump + tag the agent submodule, or install Zig 0.15+ and run `bun run agent:build`.',
);
return 0; // Non-fatal: let the wrapper keep install green, real error surfaces at test time.
return buildFromSource();
}
process.exit(main());