slides-grab 1.0.0
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/AGENTS.md +80 -0
- package/LICENSE +21 -0
- package/PROGRESS.md +39 -0
- package/README.md +120 -0
- package/SETUP.md +51 -0
- package/bin/ppt-agent.js +204 -0
- package/convert.cjs +184 -0
- package/package.json +51 -0
- package/prd.json +135 -0
- package/prd.md +104 -0
- package/scripts/editor-server.js +779 -0
- package/scripts/html2pdf.js +217 -0
- package/scripts/validate-slides.js +416 -0
- package/skills/ppt-design-skill/SKILL.md +38 -0
- package/skills/ppt-plan-skill/SKILL.md +37 -0
- package/skills/ppt-pptx-skill/SKILL.md +37 -0
- package/skills/ppt-presentation-skill/SKILL.md +57 -0
- package/src/editor/codex-edit.js +213 -0
- package/src/editor/editor.html +1733 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +110 -0
- package/src/editor/js/editor-dom.js +55 -0
- package/src/editor/js/editor-init.js +284 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +264 -0
- package/src/editor/js/editor-send.js +157 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +32 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/screenshot.js +73 -0
- package/src/resolve.js +159 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +41 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
- package/themes/corporate.css +8 -0
- package/themes/executive.css +10 -0
- package/themes/modern-dark.css +9 -0
- package/themes/sage.css +9 -0
- package/themes/warm.css +8 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
slides-grab is an agent-first PPT framework. AI agents write HTML slides directly, and a 3-stage pipeline (Planning → Design → Conversion) produces high-quality PPTX/PDF output.
|
|
6
|
+
|
|
7
|
+
**Core philosophy: HTML is the design language.** Agents write HTML/CSS directly. The framework provides templates, validation, and conversion tools — not abstractions over HTML.
|
|
8
|
+
|
|
9
|
+
## Key Paths
|
|
10
|
+
|
|
11
|
+
| Path | Purpose |
|
|
12
|
+
|------|---------|
|
|
13
|
+
| `.claude/skills/plan-skill/SKILL.md` | Planning skill (outline generation supervisor) |
|
|
14
|
+
| `.claude/skills/design-skill/SKILL.md` | Design skill (HTML slide generation rules, ~963 lines) |
|
|
15
|
+
| `.claude/skills/pptx-skill/SKILL.md` | Conversion skill (HTML→PPTX) |
|
|
16
|
+
| `skills/ppt-plan-skill/SKILL.md` | Codex plan skill (outline workflow) |
|
|
17
|
+
| `skills/ppt-design-skill/SKILL.md` | Codex design skill (HTML slides + viewer loop) |
|
|
18
|
+
| `skills/ppt-pptx-skill/SKILL.md` | Codex conversion skill (HTML→PPTX/PDF) |
|
|
19
|
+
| `.claude/skills/pptx-skill/scripts/html2pptx.js` | Main HTML→PPTX converter (Playwright + PptxGenJS) |
|
|
20
|
+
| `.claude/skills/pptx-skill/scripts/thumbnail.py` | Thumbnail grid generator |
|
|
21
|
+
| `.claude/skills/pptx-skill/ooxml/` | OOXML pack/unpack/validate scripts |
|
|
22
|
+
| `scripts/build-viewer.js` | Builds viewer.html from slide-*.html files in selected `--slides-dir` |
|
|
23
|
+
| `slides/` (default) or custom `--slides-dir` path | Generated HTML slide workspace |
|
|
24
|
+
| `slide-outline.md` | Presentation outline (generated by organizer-agent) |
|
|
25
|
+
| `package.json` | Node.js dependencies (playwright, sharp, pptxgenjs) |
|
|
26
|
+
| `convert.cjs` | Legacy HTML→PPTX converter |
|
|
27
|
+
|
|
28
|
+
## Slide Specifications
|
|
29
|
+
|
|
30
|
+
- **Size**: 720pt × 405pt (16:9)
|
|
31
|
+
- **Font**: Pretendard (CDN: `https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css`)
|
|
32
|
+
- **Text tags**: Only `<p>`, `<h1>`-`<h6>`, `<ul>`, `<ol>`, `<li>` — never put text directly in `<div>` or `<span>`
|
|
33
|
+
- **Colors in CSS**: Use `#` prefix (`#FF0000`)
|
|
34
|
+
- **Colors in PptxGenJS**: No `#` prefix (`FF0000`)
|
|
35
|
+
- **No CSS gradients**: Rasterize to PNG with Sharp instead
|
|
36
|
+
|
|
37
|
+
## Agent System
|
|
38
|
+
|
|
39
|
+
| Agent | Role | Model | Tools |
|
|
40
|
+
|-------|------|-------|-------|
|
|
41
|
+
| `organizer-agent` | Structure presentation outline | Claude Sonnet | Read, Write, Edit |
|
|
42
|
+
| `research-agent` | Web research and data gathering | Claude Sonnet | WebSearch, WebFetch, Read, Grep, Glob |
|
|
43
|
+
|
|
44
|
+
## Skill System
|
|
45
|
+
|
|
46
|
+
| Skill | Role | Stage |
|
|
47
|
+
|-------|------|-------|
|
|
48
|
+
| `plan-skill` | Supervisor: delegates to agents, manages user feedback loop | Stage 1 |
|
|
49
|
+
| `design-skill` | Defines HTML generation rules, templates, palettes | Stage 2 |
|
|
50
|
+
| `pptx-skill` | HTML→PPTX conversion pipeline | Stage 3 |
|
|
51
|
+
| `ppt-plan-skill` | Codex Stage 1 outline planning | Stage 1 |
|
|
52
|
+
| `ppt-design-skill` | Codex Stage 2 slide design/review | Stage 2 |
|
|
53
|
+
| `ppt-pptx-skill` | Codex Stage 3 conversion | Stage 3 |
|
|
54
|
+
|
|
55
|
+
## Dependencies
|
|
56
|
+
|
|
57
|
+
### Node.js (package.json)
|
|
58
|
+
- `pptxgenjs` ^3.12.0 — PowerPoint generation
|
|
59
|
+
- `playwright` ^1.40.0 — Browser automation (Chromium)
|
|
60
|
+
- `sharp` ^0.33.0 — Image processing
|
|
61
|
+
|
|
62
|
+
### Python
|
|
63
|
+
- `markitdown` — Markdown conversion
|
|
64
|
+
- `defusedxml` — XML parsing
|
|
65
|
+
- `pillow` — Image processing
|
|
66
|
+
|
|
67
|
+
### System (optional)
|
|
68
|
+
- `LibreOffice` (soffice) — PPTX thumbnail generation
|
|
69
|
+
- `Poppler` (pdftoppm) — PDF to image conversion
|
|
70
|
+
|
|
71
|
+
## Rules for Agents
|
|
72
|
+
|
|
73
|
+
1. **Do not modify SKILL.md philosophy sections** — design principles, rules, and constraints must be preserved when editing SKILL.md files.
|
|
74
|
+
2. **All new scripts go in `scripts/`** unless they are skill-specific (then `skills/<name>/scripts/`).
|
|
75
|
+
3. **Use ES modules** — `import/export` syntax, not `require()`. Exception: `.cjs` files for CommonJS compat.
|
|
76
|
+
4. **JavaScript, not TypeScript** — Use JSDoc for type hints if needed.
|
|
77
|
+
5. **Test scripts by running them** — Use `node scripts/<name>.js` to verify they work.
|
|
78
|
+
6. **Playwright browser reuse** — When processing multiple slides, keep the browser instance open and reuse it across slides for performance.
|
|
79
|
+
7. **Environment variables for API keys** — `GEMINI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`. Never hardcode keys.
|
|
80
|
+
8. **CSS-safe HTML** — All text in semantic tags (`<p>`, `<h1>`-`<h6>`, `<ul>`, `<ol>`), backgrounds only on `<div>` elements.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeffrey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PROGRESS.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## US-001 — PASS (2026-02-27T14:20:45Z)
|
|
2
|
+
- Title: 템플릿 시스템 외부화
|
|
3
|
+
- Attempts: 1
|
|
4
|
+
|
|
5
|
+
## US-002 — PASS (2026-02-27T14:27:39Z)
|
|
6
|
+
- Title: Playwright 기반 구조화 검증 스크립트
|
|
7
|
+
- Attempts: 1
|
|
8
|
+
|
|
9
|
+
## US-003 — PASS (2026-02-27T14:33:38Z)
|
|
10
|
+
- Title: VLM Provider 표준 인터페이스 (Vercel AI SDK)
|
|
11
|
+
- Attempts: 1
|
|
12
|
+
|
|
13
|
+
## US-004 — PASS (2026-02-27T14:44:20Z)
|
|
14
|
+
- Title: 차트/다이어그램/이미지 HTML 라이브러리 지원
|
|
15
|
+
- Attempts: 1
|
|
16
|
+
## US-004 — PASS (2026-02-27T14:46:17Z)
|
|
17
|
+
- Title: 차트/다이어그램/이미지 HTML 라이브러리 지원
|
|
18
|
+
- Attempts: 1
|
|
19
|
+
|
|
20
|
+
## US-005 — PASS (2026-02-27T14:55:18Z)
|
|
21
|
+
- Title: VLM 기반 슬라이드 자동 포맷 검증
|
|
22
|
+
- Attempts: 1
|
|
23
|
+
|
|
24
|
+
## US-006 — PASS (2026-02-27T15:01:33Z)
|
|
25
|
+
- Title: PDF 출력 지원
|
|
26
|
+
- Attempts: 1
|
|
27
|
+
|
|
28
|
+
## US-007 — PASS (2026-02-27T15:04:54Z)
|
|
29
|
+
- Title: VLM 기반 사용자 스타일 추출
|
|
30
|
+
- Attempts: 1
|
|
31
|
+
|
|
32
|
+
## US-007 — PASS (2026-02-27T15:10:48Z)
|
|
33
|
+
- Title: VLM 기반 사용자 스타일 추출
|
|
34
|
+
- Attempts: 1
|
|
35
|
+
|
|
36
|
+
## US-008 — PASS (2026-02-27T15:17:36Z)
|
|
37
|
+
- Title: 프로젝트 패키징 기반 구조
|
|
38
|
+
- Attempts: 1
|
|
39
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<h1 align="center">slides-grab</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">Select context for agents directly from AI-generated HTML slides</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
How? Just drag an area in the slides and ask the agent to edit it.<br>
|
|
7
|
+
Simple things like text, size, or bold can still be edited manually, just like in the 2024 era.
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
The whole slides are HTML & CSS, the programming langauge (which is not) that outperformed by AI agents.<br>
|
|
12
|
+
So the slides are beautiful, easily editable by AI agents, and can be converted to pdf and pptx.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
The editor is pure javascript file. You can easily add up new features like adding new coding agents, changing designs, etc.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://github.com/vkehfdl1/slides-grab/releases/download/v0.0.1-demo/demo.mp4">
|
|
21
|
+
<img src="docs/assets/demo.gif" alt="slides-grab demo" width="720">
|
|
22
|
+
</a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
Paste one of these into your coding agent:
|
|
30
|
+
|
|
31
|
+
**Claude Code:**
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Read https://raw.githubusercontent.com/vkehfdl1/slides-grab/main/docs/prompts/setup-claude.md and follow every step.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Codex:**
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Read https://raw.githubusercontent.com/vkehfdl1/slides-grab/main/docs/prompts/setup-codex.md and follow every step.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or clone manually:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/vkehfdl1/slides-grab.git && cd slides-grab
|
|
47
|
+
npm ci && npx playwright install chromium
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> Requires **Node.js >= 18**.
|
|
51
|
+
|
|
52
|
+
## Why This Project?
|
|
53
|
+
|
|
54
|
+
There are many AI tools that generate slide HTML. Almost none let you **visually point at what you want changed** and iterate in-place. slides-grab fills that gap:
|
|
55
|
+
|
|
56
|
+
- **Plan** — Agent creates a structured slide outline from your topic/files
|
|
57
|
+
- **Design** — Agent generates each slide as a self-contained HTML file
|
|
58
|
+
- **Edit** — Browser-based editor with bbox selection, direct text editing, and agent-powered rewrites
|
|
59
|
+
- **Export** — One command to PPTX or PDF
|
|
60
|
+
|
|
61
|
+
## CLI Commands
|
|
62
|
+
|
|
63
|
+
All commands support `--slides-dir <path>` (default: `slides`).
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
slides-grab edit # Launch visual slide editor
|
|
67
|
+
slides-grab build-viewer # Build single-file viewer.html
|
|
68
|
+
slides-grab validate # Validate slide HTML (Playwright-based)
|
|
69
|
+
slides-grab convert # Export to PPTX
|
|
70
|
+
slides-grab pdf # Export to PDF
|
|
71
|
+
slides-grab list-templates # Show available slide templates
|
|
72
|
+
slides-grab list-themes # Show available color themes
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Multi-Deck Workflow
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
slides-grab edit --slides-dir decks/my-deck
|
|
79
|
+
slides-grab validate --slides-dir decks/my-deck
|
|
80
|
+
slides-grab pdf --slides-dir decks/my-deck --output decks/my-deck.pdf
|
|
81
|
+
slides-grab convert --slides-dir decks/my-deck --output decks/my-deck.pptx
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Installation Guides
|
|
85
|
+
|
|
86
|
+
- [Claude Code setup](docs/prompts/setup-claude.md)
|
|
87
|
+
- [Codex setup](docs/prompts/setup-codex.md)
|
|
88
|
+
- [Claude detailed guide](docs/installation/claude.md)
|
|
89
|
+
- [Codex detailed guide](docs/installation/codex.md)
|
|
90
|
+
|
|
91
|
+
## npm Package
|
|
92
|
+
|
|
93
|
+
Also available as an npm package for standalone CLI usage:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm install slides-grab
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Project Structure
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
bin/ CLI entry point
|
|
103
|
+
src/editor/ Visual editor (HTML + JS client modules)
|
|
104
|
+
scripts/ Build, validate, convert, editor server
|
|
105
|
+
templates/ Slide HTML templates (cover, content, chart, ...)
|
|
106
|
+
themes/ Color themes (modern-dark, executive, sage, ...)
|
|
107
|
+
.claude/skills/ Claude Code skill definitions
|
|
108
|
+
skills/ Codex skill definitions
|
|
109
|
+
docs/ Installation & usage guides
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
[MIT](LICENSE)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## Acknowledgment
|
|
118
|
+
|
|
119
|
+
This project is built based on the [ppt_team_agent](https://github.com/uxjoseph/ppt_team_agent) by Builder Josh. Huge thanks to him!
|
|
120
|
+
|
package/SETUP.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Setup
|
|
2
|
+
|
|
3
|
+
Copy-paste installation guide for `slides-grab`.
|
|
4
|
+
|
|
5
|
+
## 1) Clone the Repository
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/vkehfdl1/slides-grab.git && cd slides-grab
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2) Install Dependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm ci && npx playwright install chromium
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Platform-specific: install Node.js first if needed
|
|
18
|
+
|
|
19
|
+
macOS (Homebrew):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
brew update && brew install node git
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Ubuntu (apt):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
sudo apt-get update && sudo apt-get install -y curl git && curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Windows (winget, PowerShell):
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
winget install -e --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements; winget install -e --id Git.Git --accept-package-agreements --accept-source-agreements
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 3) Verify CLI
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm exec -- slides-grab --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 4) For Codex Users
|
|
44
|
+
|
|
45
|
+
Install Codex skills after cloning:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm exec -- slides-grab install-codex-skills --force
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then restart Codex.
|
package/bin/ppt-agent.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageRoot = resolve(__dirname, '..');
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf-8'));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run a Node.js script from the package, with CWD set to the user's directory.
|
|
15
|
+
* Scripts resolve slide paths via --slides-dir and templates/themes via src/resolve.js.
|
|
16
|
+
*/
|
|
17
|
+
function runNodeScript(relativePath, args = []) {
|
|
18
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
19
|
+
const scriptPath = resolve(packageRoot, relativePath);
|
|
20
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
PPT_AGENT_PACKAGE_ROOT: packageRoot,
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
child.on('error', rejectPromise);
|
|
30
|
+
child.on('close', (code, signal) => {
|
|
31
|
+
if (signal) {
|
|
32
|
+
rejectPromise(new Error(`Command terminated by signal ${signal}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
resolvePromise(code ?? 0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runCommand(relativePath, args = []) {
|
|
41
|
+
try {
|
|
42
|
+
const code = await runNodeScript(relativePath, args);
|
|
43
|
+
if (code !== 0) {
|
|
44
|
+
process.exitCode = code;
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`[slides-grab] ${error.message}`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const program = new Command();
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.name('slides-grab')
|
|
56
|
+
.description('Agent-first PPT framework CLI')
|
|
57
|
+
.version(packageJson.version);
|
|
58
|
+
|
|
59
|
+
// --- Core workflow commands ---
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('build-viewer')
|
|
63
|
+
.description('Build viewer.html from slide HTML files')
|
|
64
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
65
|
+
.action(async (options = {}) => {
|
|
66
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
67
|
+
await runCommand('scripts/build-viewer.js', args);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command('validate')
|
|
72
|
+
.description('Run structured validation on slide HTML files (Playwright-based)')
|
|
73
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
74
|
+
.action(async (options = {}) => {
|
|
75
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
76
|
+
await runCommand('scripts/validate-slides.js', args);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('convert')
|
|
81
|
+
.description('Convert slide HTML files to PPTX')
|
|
82
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
83
|
+
.option('--output <path>', 'Output PPTX file')
|
|
84
|
+
.action(async (options = {}) => {
|
|
85
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
86
|
+
if (options.output) {
|
|
87
|
+
args.push('--output', String(options.output));
|
|
88
|
+
}
|
|
89
|
+
await runCommand('convert.cjs', args);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
program
|
|
93
|
+
.command('pdf')
|
|
94
|
+
.description('Convert slide HTML files to PDF')
|
|
95
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
96
|
+
.option('--output <path>', 'Output PDF file')
|
|
97
|
+
.action(async (options = {}) => {
|
|
98
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
99
|
+
if (options.output) {
|
|
100
|
+
args.push('--output', String(options.output));
|
|
101
|
+
}
|
|
102
|
+
await runCommand('scripts/html2pdf.js', args);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command('edit')
|
|
107
|
+
.description('Start interactive slide editor with Codex image-based edit flow')
|
|
108
|
+
.option('--port <number>', 'Server port')
|
|
109
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
110
|
+
.action(async (options = {}) => {
|
|
111
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
112
|
+
if (options.port) {
|
|
113
|
+
args.push('--port', String(options.port));
|
|
114
|
+
}
|
|
115
|
+
await runCommand('scripts/editor-server.js', args);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command('install-codex-skills')
|
|
120
|
+
.description('Install project Codex skills into $CODEX_HOME/skills (default: ~/.codex/skills)')
|
|
121
|
+
.option('--force', 'Overwrite existing skill directories')
|
|
122
|
+
.option('--dry-run', 'Preview what would be installed')
|
|
123
|
+
.action(async (options = {}) => {
|
|
124
|
+
const args = [];
|
|
125
|
+
if (options.force) args.push('--force');
|
|
126
|
+
if (options.dryRun) args.push('--dry-run');
|
|
127
|
+
await runCommand('scripts/install-codex-skills.js', args);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// --- Template/theme discovery commands ---
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('list-templates')
|
|
134
|
+
.description('List all available slide templates (local overrides + package built-ins)')
|
|
135
|
+
.action(async () => {
|
|
136
|
+
const { listTemplates } = await import('../src/resolve.js');
|
|
137
|
+
const templates = listTemplates();
|
|
138
|
+
if (templates.length === 0) {
|
|
139
|
+
console.log('No templates found.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log('Available templates:\n');
|
|
143
|
+
for (const t of templates) {
|
|
144
|
+
const tag = t.source === 'local' ? '(local)' : '(built-in)';
|
|
145
|
+
console.log(` ${t.name.padEnd(20)} ${tag}`);
|
|
146
|
+
}
|
|
147
|
+
console.log(`\nTotal: ${templates.length} templates`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
program
|
|
151
|
+
.command('list-themes')
|
|
152
|
+
.description('List all available color themes (local overrides + package built-ins)')
|
|
153
|
+
.action(async () => {
|
|
154
|
+
const { listThemes } = await import('../src/resolve.js');
|
|
155
|
+
const themes = listThemes();
|
|
156
|
+
if (themes.length === 0) {
|
|
157
|
+
console.log('No themes found.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log('Available themes:\n');
|
|
161
|
+
for (const t of themes) {
|
|
162
|
+
const tag = t.source === 'local' ? '(local)' : '(built-in)';
|
|
163
|
+
console.log(` ${t.name.padEnd(20)} ${tag}`);
|
|
164
|
+
}
|
|
165
|
+
console.log(`\nTotal: ${themes.length} themes`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
program
|
|
169
|
+
.command('show-template')
|
|
170
|
+
.description('Print the contents of a template file')
|
|
171
|
+
.argument('<name>', 'Template name (e.g. "cover", "content", "chart")')
|
|
172
|
+
.action(async (name) => {
|
|
173
|
+
const { resolveTemplate } = await import('../src/resolve.js');
|
|
174
|
+
const result = resolveTemplate(name);
|
|
175
|
+
if (!result) {
|
|
176
|
+
console.error(`Template "${name}" not found.`);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const content = readFileSync(result.path, 'utf-8');
|
|
181
|
+
console.log(`# Template: ${name} (${result.source})`);
|
|
182
|
+
console.log(`# Path: ${result.path}\n`);
|
|
183
|
+
console.log(content);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
program
|
|
187
|
+
.command('show-theme')
|
|
188
|
+
.description('Print the contents of a theme file')
|
|
189
|
+
.argument('<name>', 'Theme name (e.g. "modern-dark", "executive")')
|
|
190
|
+
.action(async (name) => {
|
|
191
|
+
const { resolveTheme } = await import('../src/resolve.js');
|
|
192
|
+
const result = resolveTheme(name);
|
|
193
|
+
if (!result) {
|
|
194
|
+
console.error(`Theme "${name}" not found.`);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const content = readFileSync(result.path, 'utf-8');
|
|
199
|
+
console.log(`/* Theme: ${name} (${result.source}) */`);
|
|
200
|
+
console.log(`/* Path: ${result.path} */\n`);
|
|
201
|
+
console.log(content);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await program.parseAsync(process.argv);
|
package/convert.cjs
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const PptxGenJS = require('pptxgenjs');
|
|
2
|
+
const { chromium } = require('playwright');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const sharp = require('sharp');
|
|
6
|
+
|
|
7
|
+
// Inline a simplified version that uses Playwright Chromium (not Chrome)
|
|
8
|
+
const PT_PER_PX = 0.75;
|
|
9
|
+
const PX_PER_IN = 96;
|
|
10
|
+
const EMU_PER_IN = 914400;
|
|
11
|
+
const DEFAULT_SLIDES_DIR = 'slides';
|
|
12
|
+
const DEFAULT_OUTPUT = 'output.pptx';
|
|
13
|
+
|
|
14
|
+
function printUsage() {
|
|
15
|
+
process.stdout.write(
|
|
16
|
+
[
|
|
17
|
+
'Usage: node convert.cjs [options]',
|
|
18
|
+
'',
|
|
19
|
+
'Options:',
|
|
20
|
+
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
21
|
+
` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
|
|
22
|
+
' -h, --help Show this help message',
|
|
23
|
+
].join('\n'),
|
|
24
|
+
);
|
|
25
|
+
process.stdout.write('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readOptionValue(args, index, optionName) {
|
|
29
|
+
const next = args[index + 1];
|
|
30
|
+
if (!next || next.startsWith('-')) {
|
|
31
|
+
throw new Error(`Missing value for ${optionName}.`);
|
|
32
|
+
}
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(args) {
|
|
37
|
+
const options = {
|
|
38
|
+
slidesDir: DEFAULT_SLIDES_DIR,
|
|
39
|
+
output: DEFAULT_OUTPUT,
|
|
40
|
+
help: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
if (arg === '-h' || arg === '--help') {
|
|
46
|
+
options.help = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (arg === '--slides-dir') {
|
|
51
|
+
options.slidesDir = readOptionValue(args, i, '--slides-dir');
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (arg.startsWith('--slides-dir=')) {
|
|
57
|
+
options.slidesDir = arg.slice('--slides-dir='.length);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (arg === '--output') {
|
|
62
|
+
options.output = readOptionValue(args, i, '--output');
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (arg.startsWith('--output=')) {
|
|
68
|
+
options.output = arg.slice('--output='.length);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
|
|
76
|
+
throw new Error('--slides-dir must be a non-empty string.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof options.output !== 'string' || options.output.trim() === '') {
|
|
80
|
+
throw new Error('--output must be a non-empty string.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
options.slidesDir = options.slidesDir.trim();
|
|
84
|
+
options.output = options.output.trim();
|
|
85
|
+
return options;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function convertSlide(htmlFile, pres, browser) {
|
|
89
|
+
const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
|
|
90
|
+
|
|
91
|
+
const page = await browser.newPage();
|
|
92
|
+
await page.goto(`file://${filePath}`);
|
|
93
|
+
|
|
94
|
+
const bodyDimensions = await page.evaluate(() => {
|
|
95
|
+
const body = document.body;
|
|
96
|
+
const style = window.getComputedStyle(body);
|
|
97
|
+
return {
|
|
98
|
+
width: parseFloat(style.width),
|
|
99
|
+
height: parseFloat(style.height),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await page.setViewportSize({
|
|
104
|
+
width: Math.round(bodyDimensions.width),
|
|
105
|
+
height: Math.round(bodyDimensions.height)
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Take screenshot and add as full-slide image
|
|
109
|
+
const screenshot = await page.screenshot({ type: 'png' });
|
|
110
|
+
await page.close();
|
|
111
|
+
|
|
112
|
+
// Resize to exact slide dimensions (13.33" x 7.5" at 150 DPI)
|
|
113
|
+
const targetWidth = Math.round(13.33 * 150);
|
|
114
|
+
const targetHeight = Math.round(7.5 * 150);
|
|
115
|
+
|
|
116
|
+
const resized = await sharp(screenshot)
|
|
117
|
+
.resize(targetWidth, targetHeight, { fit: 'fill' })
|
|
118
|
+
.png()
|
|
119
|
+
.toBuffer();
|
|
120
|
+
|
|
121
|
+
const tmpPath = path.join(process.env.TMPDIR || '/tmp', `slide-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
|
|
122
|
+
fs.writeFileSync(tmpPath, resized);
|
|
123
|
+
|
|
124
|
+
const slide = pres.addSlide();
|
|
125
|
+
slide.addImage({
|
|
126
|
+
path: tmpPath,
|
|
127
|
+
x: 0,
|
|
128
|
+
y: 0,
|
|
129
|
+
w: '100%',
|
|
130
|
+
h: '100%'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return tmpPath;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function main() {
|
|
137
|
+
const options = parseArgs(process.argv.slice(2));
|
|
138
|
+
if (options.help) {
|
|
139
|
+
printUsage();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const pres = new PptxGenJS();
|
|
144
|
+
pres.layout = 'LAYOUT_WIDE'; // 16:9
|
|
145
|
+
|
|
146
|
+
const slidesDir = path.resolve(process.cwd(), options.slidesDir);
|
|
147
|
+
const files = fs.readdirSync(slidesDir)
|
|
148
|
+
.filter(f => f.endsWith('.html'))
|
|
149
|
+
.sort();
|
|
150
|
+
|
|
151
|
+
console.log(`Converting ${files.length} slides...`);
|
|
152
|
+
|
|
153
|
+
// Launch Chromium (not Chrome)
|
|
154
|
+
const browser = await chromium.launch();
|
|
155
|
+
const tmpFiles = [];
|
|
156
|
+
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
const filePath = path.join(slidesDir, file);
|
|
159
|
+
console.log(` Processing: ${file}`);
|
|
160
|
+
try {
|
|
161
|
+
const tmpPath = await convertSlide(filePath, pres, browser);
|
|
162
|
+
tmpFiles.push(tmpPath);
|
|
163
|
+
console.log(` ✓ ${file} done`);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(` ✗ ${file} error: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await browser.close();
|
|
170
|
+
|
|
171
|
+
const outputFile = path.resolve(process.cwd(), options.output);
|
|
172
|
+
await pres.writeFile({ fileName: outputFile });
|
|
173
|
+
console.log(`\nSaved: ${outputFile}`);
|
|
174
|
+
|
|
175
|
+
// Cleanup tmp files
|
|
176
|
+
for (const f of tmpFiles) {
|
|
177
|
+
try { fs.unlinkSync(f); } catch {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch(err => {
|
|
182
|
+
console.error('Fatal error:', err);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|