pindeck 0.0.0-stage → 2.8.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emanuël A. P. Habets
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/README.md CHANGED
@@ -1,3 +1,322 @@
1
- # Temporary Holding Version
1
+ # PinDeck
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Pin it. Say it. Done.** PinDeck is a local editor for HTML slide decks (reveal.js): the slides on
4
+ the left, the slide in the middle, and a chat with Claude Code on the right. You click elements to
5
+ pin them, write what to change, and Claude edits the HTML and CSS. The slide updates in place.
6
+
7
+ ![PinDeck: the slide list, slide 10 of the example deck with two numbered pins, and the chat with Claude's answer to an earlier request and a new one being typed](docs/pindeck.png)
8
+
9
+ *The example deck in PinDeck. Claude has lined up the three numbers from the first request and
10
+ checked the layout; the next request, still being typed, refers to the new pins 1 and 2.*
11
+
12
+ ## What it does
13
+
14
+ * **Point instead of describe.** Pins name the exact element, word or table cell; "make 2 match 1"
15
+ is enough. Claude also gets the measured layout of the slide, so it sees overlaps and overflow
16
+ without screenshots.
17
+ * **Edit by hand, too.** Move, resize and nudge elements with snapping, edit text in place (math stays
18
+ TeX), write speaker notes, add, hide, move and delete slides. One undo history covers your edits and
19
+ Claude's.
20
+ * **Stay on brand.** `brand.css` is locked: Claude edits `index.html` and `theme.css` only, and checks
21
+ its own work with PinDeck's layout command.
22
+ * **Bring material in.** Attach figures, audio and PDFs to a message, or import slides from PowerPoint
23
+ into the deck's own layouts.
24
+ * **Get it out.** Present from the browser, or export a handout or presentation PDF.
25
+ * **Local and small.** One Node process on 127.0.0.1, no npm dependencies, your normal Claude login.
26
+
27
+ ## Quick start
28
+
29
+ ```bash
30
+ npx pindeck path/to/deck # from npm
31
+
32
+ git clone https://github.com/ehabets/PinDeck.git && cd PinDeck
33
+ node server.mjs ocean-example # or from source, with the example deck in this repository
34
+ ```
35
+
36
+ Then open <http://127.0.0.1:5180/>. Requirements: Node 20 or later and the `claude` CLI, signed in
37
+ with your normal Claude account. Chrome (or Chromium, Edge, Brave) is optional and is used for
38
+ screenshots and PDF export; PowerPoint or Keynote (macOS) for pictures in the PowerPoint import.
39
+
40
+ Options: `--port 5180`, `--model <name>` (for this run only; otherwise the model in
41
+ `~/.pindeck/config.json`), `--help`. The deck folder defaults to the current folder and must contain
42
+ `index.html`. Runtime files (chat history, screenshots, backups) are kept in `~/.pindeck/` (change with
43
+ `PINDECK_HOME`), not in the deck. `ocean-example/` is a complete example deck to start from.
44
+
45
+ ## Preparing a deck
46
+
47
+ PinDeck works with reveal.js decks whose slides are `<section>` elements inside `.slides`. Two
48
+ optional `<meta>` tags in the deck's `<head>` tell it about the design:
49
+
50
+ ```html
51
+ <meta name="pindeck:footer-top" content="652"> <!-- y of the footer rule: content must stay above it -->
52
+ <meta name="pindeck:locked" content=".slide-foot, .logo"> <!-- brand elements that cannot be selected or moved -->
53
+ ```
54
+
55
+ Hidden slides use reveal.js's `data-visibility="hidden"`. If the deck numbers its slides itself (as
56
+ the example's `deck.js` does for the footer), count only slides without that attribute, or the
57
+ numbers will jump where a slide is hidden.
58
+
59
+ Without them PinDeck runs no footer check and locks only `.slide-foot` and `.logo`. A file called
60
+ `brand.css` in the deck folder is locked for Claude. See `ocean-example/` for a complete deck.
61
+
62
+ ## Making your own template
63
+
64
+ PinDeck has no template generator; a template is simply a deck folder. Start from a copy of the example:
65
+
66
+ ```bash
67
+ cp -R ocean-example my-template
68
+ ```
69
+
70
+ 1. **Brand in `brand.css`:** palette, backgrounds, footer and logo positions. PinDeck locks this file
71
+ for Claude, so edit it by hand (or with Claude Code outside PinDeck). Replace the logos in
72
+ `assets/` too.
73
+ 2. **Everything else in `theme.css`:** type sizes, spacing, layouts and components. This can be done
74
+ in PinDeck with Claude.
75
+ 3. **`index.html`:** the footer text in the `data-` attributes of `<div class="reveal">`, and the
76
+ `pindeck:footer-top` and `pindeck:locked` meta tags if the footer or the brand elements move.
77
+ `deck.js` holds the math macros, the footer markup and the audio player.
78
+ 4. **`README.md` and `CLAUDE.md`:** Claude follows both, so describe the new classes and rules there.
79
+ The example's `CLAUDE.md` calls `../cli.mjs` and `../server.mjs`; fix these paths if the template
80
+ does not sit next to PinDeck.
81
+
82
+ **New slide** and the PowerPoint import model new slides on the deck's own: keep a slide with class
83
+ `title-slide`, one with `section-slide`, and a content slide with `.slide-head` (`h2`, optionally a
84
+ `.kicker` or `.sub`) and `.slide-body`. Imported tables take the class of the deck's first table,
85
+ and imported audio copies the first `.demo-row` (with a `data-target`). Without a title or section
86
+ slide, those PowerPoint slides are imported as content slides; without a `.demo-row`, audio gets
87
+ plain players.
88
+
89
+ ## Configuration
90
+
91
+ `~/.pindeck/config.json` (or `$PINDECK_HOME/config.json`) is created on first start:
92
+
93
+ ```json
94
+ {
95
+ "model": "sonnet",
96
+ "effort": "medium"
97
+ }
98
+ ```
99
+
100
+ `model` is `opus` or `sonnet`; `effort` is `low`, `medium`, `high`, `xhigh` or `max`. A choice in the
101
+ chat header's menu is written to this file, so it always shows what PinDeck uses. Edit it while
102
+ PinDeck is stopped, or simply use the menu.
103
+
104
+ ## Keeping the ids out of git
105
+
106
+ PinDeck adds `data-eid` attributes to the deck it opens. If the deck is in git, run once per clone:
107
+
108
+ ```bash
109
+ node cli.mjs git-setup path/to/deck
110
+ ```
111
+
112
+ This adds a line like `path/to/deck/*.html filter=pindeck-ids` to `.gitattributes` (commit it) and
113
+ defines the filter in the clone's git config: git then strips the ids whenever it reads the deck's
114
+ HTML. The ids stay in the file on disk, where PinDeck and Claude need them, but commits and diffs
115
+ never contain them, and a change that only adds ids does not show in `git status`. Your own edits
116
+ show and commit as usual. Clones without the filter configured simply see the file as it is.
117
+
118
+ ## Working with it
119
+
120
+ | Action | How |
121
+ |---|---|
122
+ | Select, move, resize | Select mode (`V`, the default): click an element to select it, drag to move, pull a handle to resize. Elements snap to the slide edges and centre, the footer rule and other elements (pink guides); hold `Alt` to turn snapping off, `Shift` to move along one axis. Images keep their aspect ratio (`Shift` toggles). Arrow keys nudge by 1 px, `Shift`+arrows by 10 px. `Delete` removes the selected element after a confirmation; a picture goes with its caption, and files nothing else in the deck uses can be deleted with it. `⌘Z` brings both back. |
123
+ | Edit text | Double-click the text (or select it and press `Enter`). The element shows its source, so math appears as TeX, e.g. `\(\mu=0.5\)`. `Enter` or a click elsewhere saves, `Esc` cancels, `Shift+Enter` adds a line break, `⌘B` / `⌘I` make text bold or italic. In a bullet, `Enter` splits it at the cursor and continues in a new bullet below; `Enter` on an empty new bullet removes it and stops. Pasted text arrives as plain text. Table cells are edited one at a time. |
124
+ | Undo, redo | `⌘Z` / `⇧⌘Z` or the toolbar arrows. One history for your hand edits and Claude's changes. |
125
+ | Pin elements | Press `P` (or the Pin button), click elements; each click gets a number. Click a marker to remove it, `Esc` to stop. |
126
+ | Ask Claude | Write in the sidebar and refer to pins by number: "1: align with 2, make 3 smaller". The selected element is sent too, so "make this smaller" works. `Enter` sends, `Shift+Enter` adds a new line. |
127
+ | Visual questions | **+** in the chat, then **Screenshot**: the current slide with its pin markers is attached when you send. |
128
+ | Review | Each answer ends with the changed files, a **Diff** button and an **Undo** button (most recent change). |
129
+ | Stop | **Stop** interrupts Claude; the conversation continues. **New chat** starts over. |
130
+ | New slide | **+ New slide** above the slide list: a **content slide** (title and bullets), a **section divider**, or a **duplicate** of the current slide. It is added after the current slide, modelled on the deck's own slides, and its title opens for typing. |
131
+ | Import from PowerPoint | **+ New slide** → **Import from PowerPoint…**, then choose a `.pptx` file and tick the slides you want (hidden PowerPoint slides start unticked). They are added after the current slide in this deck's own layouts, and Claude can improve the complex ones; see [PowerPoint import](#powerpoint-import). `⌘Z` removes them again, with their figures. |
132
+ | Speaker notes | The **Notes** field under the slide shows the current slide's notes (the speaker view, `S`, shows them when presenting). Type to change them; they are saved when you click elsewhere or press `⌘Enter`, and `Esc` puts back what was there. A slide without notes gets them; emptying the field removes them. Notes with formatting other than line breaks are edited as HTML. Drag the line above the notes to give them more or less room (double-click it for the default size); collapse them with their header. |
133
+ | Move a slide | Drag it up or down in the slide list; an orange line shows where it will go. It moves with its notes and the comment above it, and `⌘Z` puts it back. Slides inside vertical stacks cannot be moved yet. |
134
+ | Hide a slide | The eye on a slide in the list, or `H`. A hidden slide stays in the deck and in PinDeck (greyed out, with a banner) but is skipped when presenting and in PDFs; **Export ▾** can include hidden slides. Show it the same way. |
135
+ | Model and effort | Click **Claude ▾** in the chat header: Opus or Sonnet, and the effort (how long Claude thinks: low to max). The conversation continues after a change. The choice is saved in `~/.pindeck/config.json` (default: Sonnet, medium). |
136
+ | Attach figures and audio | **+** in the chat, then **File…**, drag and drop onto the chat or a slide, or paste an image. Files are saved in `<deck>/figures/` straight away; audio also gets a waveform SVG. Then ask Claude, e.g. "put this figure on a new slide after 4". Removing an attachment before sending deletes the file again. |
137
+ | Attach a PDF | The same way: a figure or table to take over, or a paper or report with more details. PDFs stay outside the deck (in `~/.pindeck`); Claude reads them, rebuilds tables as HTML, and renders a figure (a page or a cropped part of one) into `<deck>/figures/` as PNG with `cli.mjs pdf-figure` (macOS). E.g. "put Fig. 3 of this paper on slide 7" or "add the results of Table 2 to slide 9". |
138
+ | Export PDF | **Export ▾** in the toolbar. **Handout PDF**: one page per slide, build steps shown complete. **Presentation PDF**: one page per click, so a slide with two build steps gives three pages. Files go to `<deck>/export/`. Needs Chrome or Chromium. |
139
+ | Navigate | Slide list, `←` `→`, or the toolbar. **Present** opens the deck in a new tab. |
140
+
141
+ You can also edit `index.html` or `theme.css` in any editor; the slide reloads, and Claude is told
142
+ the files changed. CSS changes are swapped in without a reload.
143
+
144
+ ## PowerPoint import
145
+
146
+ The import keeps what can be carried over exactly and makes pictures of the rest:
147
+
148
+ * **Text becomes HTML** in the deck's layouts: the title (and the subtitle, as a `.sub` line or a
149
+ kicker, as the deck does it), bullets with their levels, numbered lists, bold, italics, coloured words (as `.hl`),
150
+ superscripts, web and mail links and line breaks, other text as paragraphs, references at the bottom of a slide
151
+ as `.refs`, and the speaker notes. Side notes and labels clearly smaller than the body text stay smaller.
152
+ * **Tables become HTML tables** with the class of the deck's own tables, merged header cells, header rows
153
+ and a caption from the text above them. Cells merged down in the body become a label on the group's
154
+ first row with empty cells below it, and each later group's first row gets `class="group"`. A table with shapes drawn over it (e.g. boxes marking rows)
155
+ is kept as a picture instead.
156
+ * **Build steps:** shapes and paragraphs that appear (or disappear) on a click become reveal.js
157
+ fragments in the same order, also when several things appear on one click. A picture made of several
158
+ shapes appears on a click only if all of its shapes do.
159
+ * **Audio** goes into the deck's own audio player (a copy of its first `.demo-row`, with a waveform made
160
+ from the sound), labelled with the nearest text on the slide; without such a player, plain players.
161
+ **Video** keeps its place, with its poster frame.
162
+ The first slide is filled into the deck's title slide (title, subtitle, authors, event, institute)
163
+ and PowerPoint section headers into its section divider.
164
+ * **Pictures are copied** to `figures/` (`<file>-s04-1.png`, ...).
165
+ * **Everything drawn** (diagrams built from shapes and arrows, charts, pictures with labels on
166
+ them, EMF graphics) becomes a picture cut from the slide as PowerPoint renders it. For this PinDeck
167
+ asks PowerPoint (or else Keynote) to save the presentation as a PDF; macOS asks once whether PinDeck
168
+ may control the app. The same PDF gives the thumbnails in the import dialog.
169
+ * **Layout:** blocks that sit side by side become `.cols` with the slide's proportions, others are
170
+ stacked top to bottom; figures and, if needed, the text are scaled so that the slide fits.
171
+ * **Equations** (PowerPoint's equation editor) become TeX for the deck's KaTeX: `\( \)` in the text,
172
+ `\[ \]` on their own line; fractions, scripts, roots, sums and integrals, functions and limits,
173
+ accents, braces, matrices and the bold, italic and double-struck math letters are converted.
174
+ * **Hidden slides** stay hidden (`data-visibility="hidden"`).
175
+
176
+ **Hand-off to Claude.** Slides the import can only approximate are tagged *complex* in the dialog:
177
+ diagrams of many shapes or tables with shapes over them that became pictures, pictures placed inside
178
+ text (often equations as images), build steps on parts of a diagram, slides without a title, and
179
+ text that had to shrink to fit. With **Then ask Claude to improve the complex slides** ticked, Claude
180
+ gets these slides right after the import, each with a picture of the original slide and the texts of
181
+ its diagrams, and rebuilds diagrams as SVG, equations as TeX and tables as HTML in the deck's style.
182
+ The request shows in the chat like your own messages, and its changes can be undone as usual.
183
+
184
+ Without PowerPoint or Keynote (or on other systems than macOS) only the text and the pictures are imported.
185
+
186
+ ## How it works
187
+
188
+ * **Element ids.** On start, the server gives every slide and every visible element a readable
189
+ `data-eid` attribute (`summary`, `summary/callout`, `summary/li-2`). Only the attribute is
190
+ inserted; the rest of the file stays as it was. A backup of the file before the first change is
191
+ kept in `~/.pindeck/decks/<deck>/backups/`. Ids survive edits, so pins, layout reports and Claude all name
192
+ the same element.
193
+ * **Hand edits.** A move is stored as the CSS `translate` property and a resize as `width`/`height`,
194
+ inline on the element in `index.html`. `translate` moves an element without changing the layout
195
+ around it, for elements in the flow and absolutely positioned ones alike. Only that one style
196
+ attribute is rewritten; the rest of the file stays as it was. Claude gets a note about each hand
197
+ edit. Brand-locked elements (footer, logos) cannot be selected.
198
+ * **Text edits.** While you type, the element holds its source HTML from `index.html`, not the
199
+ rendered slide, so math stays TeX and nothing that KaTeX or the deck script generates ends up in
200
+ the file. On save only that element's content is replaced. Browser editing artefacts are cleaned
201
+ up: `<b>`/`<i>` become `<strong>`/`<em>`, styled spans from formatting are removed, trailing
202
+ `<br>`s dropped.
203
+ * **Pins.** A pin records the element id, its box in slide coordinates (1280 x 720), the click
204
+ point, the clicked word and, in tables, the cell. Pins on the footer or logos are marked as
205
+ locked.
206
+ * **Layout instead of screenshots.** With each message the editor measures the current slide and
207
+ every pinned slide in your browser: position and size of every element, font sizes, and issues
208
+ (content below the footer rule, outside the slide, overlaps, clipped text). Claude gets this as
209
+ text. To check its own work it runs `node ../cli.mjs layout <slide>...` (`--issues` for the
210
+ issues only). Screenshots are
211
+ only taken when you tick the box or Claude needs a visual judgement.
212
+ * **One Claude Code process.** The server starts `claude -p` in stream-json mode once and keeps it
213
+ running, so there is no start-up delay per message. It runs in the deck folder under your Claude
214
+ login, with `--append-system-prompt` from `lib/system-prompt.md` and the deck's `CLAUDE.md`.
215
+ * **Permissions.** Claude may read files, edit `index.html` and `theme.css`, and run the editor's
216
+ CLI. Nothing else, and in particular no edits to `brand.css`. Claude Code loads no settings files
217
+ and no MCP servers, so no allow rule in your own, the deck's or a parent folder's settings widens this. If `brand.css` changes during a
218
+ turn anyway, the server restores it and says so in the chat.
219
+ * **Undo.** The server keeps a copy of the editable files from before each change (a Claude turn
220
+ or a hand edit). Undo and redo walk that history and tell Claude on the next message.
221
+
222
+ ## Files
223
+
224
+ | File | Purpose |
225
+ |---|---|
226
+ | `server.mjs` | Local server (127.0.0.1 only, random token per run): UI, deck files, events, Claude session, undo, CLI endpoints. |
227
+ | `cli.mjs` | `layout`, `shot`, `slides`, `pdf handout` / `pdf presentation` and `pdf-figure` commands for Claude (and for you), and `git-setup`. |
228
+ | `public/` | The editor page, and `bridge.js`, which is injected into the deck in edit mode (never saved to it). |
229
+ | `lib/pptx.mjs`, `lib/zip.mjs`, `lib/xml.mjs` | Reading `.pptx` files (no dependencies): slides, text, shapes and their positions, notes. |
230
+ | `lib/pptx-import.mjs` | Turns PowerPoint slides into slides of the deck: figures, tables, build steps, audio, layout, HTML in the deck's style. |
231
+ | `lib/waveform.mjs` | Waveform SVGs for imported audio. |
232
+ | `lib/omml.mjs` | PowerPoint equations (Office Math) to TeX. |
233
+ | `lib/pptx-render.mjs`, `lib/pdf-render.jxa.js` | Slide pictures: PowerPoint or Keynote saves a PDF, macOS PDFKit renders pages and regions of it. |
234
+ | `lib/pdf.mjs` | PDF export: reveal.js print layout in headless Chrome, then print to PDF. |
235
+ | `lib/eids.mjs` | Adds the `data-eid` attributes by editing the HTML as text. |
236
+ | `lib/claude.mjs` | The long-running Claude Code process. |
237
+ | `lib/system-prompt.md` | What Claude is told about the editor. |
238
+ | `CONTRIBUTING.md` | How to report bugs and contribute. |
239
+ | `docs/` | Images for this README. |
240
+ | `LICENSE` | MIT. |
241
+
242
+ ## Current Limitations
243
+
244
+ * Text inside inline SVG diagrams cannot be edited in place; pin it and ask Claude.
245
+ * Parts of an inline SVG move with the whole SVG; inline text (bold, spans) moves with its block.
246
+ * No indenting or outdenting of bullets with `Tab` yet, and `Backspace` at the start of a bullet does not merge it with the one above; ask Claude for those.
247
+ * Moves are offsets (`translate`), so the space an element came from stays reserved. Ask Claude to turn hand edits into proper CSS when a layout should really change.
248
+ * The undo history lives on the server and starts fresh when PinDeck restarts.
249
+
250
+ ## Versions
251
+
252
+ * **2.8.1**: fixes from a review. Speaker notes that fail to load leave the field empty and locked, instead
253
+ of showing the previous slide's notes and saving your edits over them. A save that fails because the
254
+ server cannot be reached is reported, and the edits after it (drags, text, bullets, notes) are still
255
+ saved instead of being dropped without a word. Stop no longer ends a turn started after it. Slides in
256
+ vertical stacks keep their order and ids, and a slide moved to the end goes after a final stack.
257
+ A first automated test suite (`npm test`).
258
+ * **2.8**: a code review: safer, sturdier, and a PowerPoint import that keeps more. The server answers
259
+ only to `127.0.0.1` and `localhost`, so another website cannot read the editor's token (DNS
260
+ rebinding). A missing `claude` command ends the turn with a message instead of leaving the chat
261
+ working forever, and a replaced Claude Code process can no longer end the next turn. Esc in the import
262
+ dialog no longer repeats the last import, and an import never overwrites edits made while it runs. The
263
+ PowerPoint import keeps animations that play by themselves off the clicks, wraps shapes that come and
264
+ go in fragments of their own, skips hidden shapes and `javascript:` links, reads packages with
265
+ absolute part names, takes only the first line of a subtitle placeholder as the subtitle, keeps
266
+ equations in titles and notes, writes numbered lists as `<ol>`, keeps the text of coloured boxes
267
+ without PowerPoint or Keynote, and makes valid TeX of escaped text, equation arrays, odd delimiters
268
+ and primes after superscripts. Damaged WAV or XML parts no longer stop an import. The toolbar wraps
269
+ instead of overlapping when the slide area is narrow.
270
+ * **2.7**: attach PDFs to chat messages (a figure, a table, a paper or report). They stay outside the deck; Claude reads
271
+ them, rebuilds tables as HTML and renders figures into `figures/` with `cli.mjs pdf-figure` (a page or a cropped part, macOS).
272
+ * **2.6.1**: tinted table rows are one rectangle per row instead of one per cell, so PDF viewers show no hairline seams between the cells.
273
+ * **2.6**: booktabs-style tables in both example decks (rules at top, under the header and at the bottom,
274
+ short rules under grouped headings, numbers right-aligned); the import writes row groups without `rowspan`.
275
+ * **2.5**: a notes field under the slide for the speaker notes, saved as hand edits with undo.
276
+ * **2.4.1**: the layout check judges a plain container by what it draws (its children), not by its box, so
277
+ a body stretched down to the references no longer counts as an overlap; a real overlap is reported once,
278
+ for the outermost pair.
279
+ * **2.4**: move slides by dragging them in the slide list; undo puts them back.
280
+ * **2.3**: lessons from a PowerPoint hand-off. Claude Code runs without settings files or MCP servers
281
+ (the deck's `CLAUDE.md` goes into the system prompt instead), so stray allow rules no longer let it run
282
+ scripts; `layout` takes several slides and `--issues`; the chat no longer labels a refused script as a
283
+ layout check; shared SVG arrowheads live outside the slides, where hidden slides cannot hide them.
284
+ * **2.2**: PowerPoint import with equations as TeX for KaTeX, and the hand-off to Claude: complex slides are
285
+ tagged in the import dialog and, if wanted, improved by Claude right after the import, with a picture of
286
+ each original slide.
287
+ * **2.1**: PowerPoint import with build steps (fragments in click order), HTML tables in the deck's table
288
+ style, audio in the deck's player with a waveform, video with its poster, coloured words as `.hl`.
289
+ * **2.0**: import slides from PowerPoint (`.pptx`): text as HTML in the deck's layouts, pictures copied,
290
+ diagrams, tables and charts as pictures cut from PowerPoint's rendering; thumbnails in the import
291
+ dialog; undo removes the imported slides and their figures.
292
+ * **1.12.1**: the PinDeck pin as the browser tab icon (SVG, with PNG versions for Safari).
293
+ * **1.12**: double-clicking an equation opens its text with the equation's TeX selected, also for
294
+ equations on their own line and for parts KaTeX draws as SVG (roots, wide accents, arrows).
295
+ * **1.11.3**: a calmer interface: no slide count, no Hide/Show button in the header (eye and `H`
296
+ remain), no messages or chat entries for hiding and showing, shorter export descriptions.
297
+ * **1.11.2**: the header shows the deck's name instead of the slide title; the browser tab is named after the deck.
298
+ * **1.11.1**: the Hide and Show icons sit at the same height.
299
+ * **1.11**: hide and show slides (eye in the slide list, toolbar button, `H`); hidden slides are skipped
300
+ when presenting and in PDFs unless the export includes them.
301
+ * **1.10**: model and effort without "Default" entries (default Sonnet, medium), set in `~/.pindeck/config.json`.
302
+ * **1.9**: `node cli.mjs git-setup` keeps PinDeck's ids out of git with a clean filter; id-only changes no longer show in `git status`.
303
+ * **1.8**: + New slide (content slide, section divider, duplicate); model (Opus, Sonnet) and effort menu in the chat header; a + menu in the chat for files and screenshots.
304
+ * **1.7**: `Delete` removes the selected element after a confirmation, a picture with its caption, and optionally the files nothing else uses; undo restores elements and files. Claude reads attached images directly instead of trying to convert them; refused commands are explained in the chat.
305
+ * **1.6**: attach figures and audio to chat messages (file picker, drag and drop, paste); audio gets a waveform SVG; files dropped on a slide carry their position.
306
+ * **1.5**: PDF export (handout and presentation with build steps on separate pages).
307
+ * **1.4**: decks describe their footer and locked brand elements with `<meta name="pindeck:...">` tags; the layout check ignores invisible elements. New public example deck: `ocean-example/`.
308
+ * **1.3**: `Enter` in a bullet starts a new bullet (split at the cursor); empty new bullets are removed again.
309
+ * **1.2**: text editing on the slide (double-click or Enter), with math as TeX, table cells, bold and italic, plain-text paste; text edits are part of the undo history and reported to Claude.
310
+ * **1.1**: select, drag, resize and nudge with snapping guides; one undo/redo history for hand edits and Claude's changes; the selected element is sent to Claude.
311
+ * **1.0**: pins, chat with Claude Code, layout reports, element ids, diff and undo.
312
+
313
+ ## Contributing
314
+
315
+ Bug reports, ideas, example decks and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md)
316
+ explains how to report a bug, set up PinDeck for development, test a change by hand, and what a pull
317
+ request should contain. In short: no npm dependencies, deck files are edited only where they must
318
+ change, Claude's permissions stay narrow, and the README describes what users see.
319
+
320
+ ## License
321
+
322
+ MIT, see [LICENSE](LICENSE).
package/cli.mjs ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /* PinDeck command line: Claude's self-checks (need the PinDeck server running) and git setup.
3
+
4
+ node cli.mjs layout <slide>... measured layout and issues of one or more slides
5
+ (add --issues for the issues only: layout 9 10 11 --issues)
6
+ node cli.mjs shot <slide>... screenshots with pin markers; prints the PNG paths
7
+ node cli.mjs slides slide numbers and ids
8
+ node cli.mjs pdf handout PDF with one page per slide (build steps in their final state)
9
+ node cli.mjs pdf presentation PDF with one page per build step
10
+ (add "all" to include hidden slides: pdf handout all)
11
+ node cli.mjs pdf-figure <pdf> <page> [x,y,w,h] [figures/name.png] [--width 1600]
12
+ a page of an attached PDF, or a box of it (fractions of the page
13
+ from the top left), as PNG: without a name in the screenshots
14
+ folder to look at, with one saved into the deck
15
+
16
+ node cli.mjs git-setup [deck] keep PinDeck's data-eid ids out of git (no server needed)
17
+
18
+ <slide> is a slide id (data-eid), a slide number (1-based) or "current". */
19
+
20
+ import fs from "node:fs";
21
+ import os from "node:os";
22
+ import path from "node:path";
23
+ import { execFileSync } from "node:child_process";
24
+
25
+ const [cmd, ...rest] = process.argv.slice(2);
26
+ const arg = rest[0];
27
+
28
+ /* git-setup: a git "clean" filter strips PinDeck's data-eid attributes whenever git reads the deck's
29
+ HTML, so ids never reach a commit and id-only changes do not show in `git status`. The ids stay in
30
+ the file on disk, where PinDeck and Claude need them. The filter is plain sed, so it does not depend
31
+ on where PinDeck is installed; git config is per clone, so run this once in each clone. */
32
+ if (cmd === "git-setup") {
33
+ const FILTER = `sed 's/ data-eid="[^"]*"//g'`;
34
+ const deck = path.resolve(arg || ".");
35
+ const git = (...a) => execFileSync("git", ["-C", deck, ...a], { encoding: "utf8" }).trim();
36
+ let root;
37
+ try { root = git("rev-parse", "--show-toplevel"); }
38
+ catch { console.error(`${deck} is not inside a git repository.`); process.exit(1); }
39
+ git("config", "filter.pindeck-ids.clean", FILTER);
40
+ const rel = path.relative(root, deck).split(path.sep).join("/");
41
+ const pattern = `${rel ? rel + "/" : ""}*.html`;
42
+ const attrFile = path.join(root, ".gitattributes");
43
+ const attrs = fs.existsSync(attrFile) ? fs.readFileSync(attrFile, "utf8") : "";
44
+ const line = `${pattern} filter=pindeck-ids`;
45
+ const added = !attrs.split("\n").some((l) => l.trim() === line);
46
+ if (added) fs.writeFileSync(attrFile, attrs + (attrs && !attrs.endsWith("\n") ? "\n" : "") + line + "\n");
47
+ console.log(`Git filter "pindeck-ids" configured in ${root} (local git config).`);
48
+ console.log(added ? `Added "${line}" to .gitattributes; commit that file.` : `.gitattributes already has "${line}".`);
49
+ process.exit(0);
50
+ }
51
+ const issuesOnly = cmd === "layout" && rest.includes("--issues");
52
+ const slides = rest.filter((a) => a !== "--issues");
53
+ if (!["layout", "shot", "slides", "pdf", "pdf-figure"].includes(cmd)) {
54
+ console.error("Usage: node cli.mjs layout|shot <slide>... [--issues] | node cli.mjs slides | node cli.mjs pdf handout|presentation | " +
55
+ "node cli.mjs pdf-figure <pdf> <page> [x,y,w,h] [figures/name.png] [--width px] | node cli.mjs git-setup [deck]");
56
+ process.exit(2);
57
+ }
58
+ /* pdf-figure: the box and the output name are told apart by their form, so their order does not matter */
59
+ function figureQuery(args) {
60
+ const w = args.indexOf("--width"), width = w >= 0 ? args.splice(w, 2)[1] : "";
61
+ const [file = "", page = "1", ...more] = args;
62
+ return { file: file && path.resolve(file), page, width,
63
+ box: more.find((a) => /^[\d.]+(,[\d.]+){3}$/.test(a)) || "", out: more.find((a) => /\.png$/i.test(a)) || "" };
64
+ }
65
+
66
+ let url = process.env.DECK_EDITOR_URL, token = process.env.DECK_EDITOR_TOKEN;
67
+ if (!url || !token) {
68
+ try {
69
+ const home = process.env.PINDECK_HOME || path.join(os.homedir(), ".pindeck");
70
+ const s = JSON.parse(fs.readFileSync(path.join(home, "server.json"), "utf8"));
71
+ url = s.url; token = s.token;
72
+ } catch {}
73
+ }
74
+ if (!url) { console.error("The PinDeck server is not running (start it with: pindeck <deck-folder>)."); process.exit(1); }
75
+
76
+ /* layout and shot take several slides, so Claude needs no shell loop or pipe (which PinDeck refuses) */
77
+ const queries = cmd === "pdf-figure" ? [figureQuery([...rest])]
78
+ : cmd === "pdf" ? [{ slide: rest[0] || "handout", ...(rest[1] === "all" ? { all: "1" } : {}) }]
79
+ : cmd === "slides" ? [{}] : (slides.length ? slides : ["current"]).map((slide) => ({ slide }));
80
+ let ok = true;
81
+ for (const q of queries) {
82
+ let res, out;
83
+ try {
84
+ res = await fetch(`${url}/api/${cmd}?` + new URLSearchParams(q), { headers: { "x-editor-token": token } });
85
+ out = await res.text();
86
+ } catch {
87
+ console.error("Cannot reach the PinDeck server at " + url + ". Is it running?");
88
+ process.exit(1);
89
+ }
90
+ if (res.ok && issuesOnly) {
91
+ const lines = out.split("\n"), i = lines.findIndex((l) => l.startsWith("Issues"));
92
+ out = [lines[0], ...lines.slice(i)].join("\n");
93
+ }
94
+ (res.ok ? console.log : console.error)(out);
95
+ ok &&= res.ok;
96
+ }
97
+ process.exit(ok ? 0 : 1);
package/lib/claude.mjs ADDED
@@ -0,0 +1,70 @@
1
+ /* A long-running Claude Code process in stream-json mode.
2
+
3
+ One process serves the whole chat: messages are written to stdin as JSON lines,
4
+ events come back on stdout. It runs under the user's normal Claude login. */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import readline from "node:readline";
8
+
9
+ export class ClaudeSession {
10
+ constructor({ cwd, args, env, onEvent, onExit, onStderr }) {
11
+ Object.assign(this, { cwd, args, env, onEvent, onExit, onStderr });
12
+ this.proc = null;
13
+ this.sessionId = null;
14
+ this.reqId = 0;
15
+ }
16
+
17
+ get running() { return !!this.proc; }
18
+
19
+ start(resumeId) {
20
+ const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose",
21
+ ...(typeof this.args === "function" ? this.args() : this.args), ...(resumeId ? ["--resume", resumeId] : [])];
22
+ const proc = spawn(process.env.CLAUDE_BIN || "claude", args, {
23
+ cwd: this.cwd, env: { ...process.env, ...this.env }, stdio: ["pipe", "pipe", "pipe"],
24
+ });
25
+ this.proc = proc;
26
+ readline.createInterface({ input: proc.stdout }).on("line", (line) => {
27
+ if (this.proc !== proc && this.proc) return; // a stopped process that is still talking
28
+ if (!line.trim()) return;
29
+ let ev;
30
+ try { ev = JSON.parse(line); } catch { return; }
31
+ if (ev.session_id) this.sessionId = ev.session_id;
32
+ this.onEvent(ev);
33
+ });
34
+ proc.stderr.on("data", (d) => this.onStderr?.(String(d)));
35
+ proc.stdin.on("error", () => {}); // writing to a process that has died: its exit is reported below
36
+ // a process that could not start (no `claude` on the PATH) reports an error and never exits
37
+ let ended = false;
38
+ const end = (code, signal) => {
39
+ if (ended) return;
40
+ ended = true;
41
+ // stale: a newer process has been started since (model change, new chat); its turn is not ours to end
42
+ const stale = this.proc !== proc && this.proc !== null;
43
+ if (this.proc === proc) this.proc = null;
44
+ this.onExit(code, signal, { stale });
45
+ };
46
+ proc.on("error", (err) => { this.onStderr?.(String(err)); if (proc.pid === undefined) end(null, null); });
47
+ proc.on("exit", end);
48
+ }
49
+
50
+ send(content) {
51
+ this.#write({ type: "user", message: { role: "user", content } });
52
+ }
53
+
54
+ interrupt() {
55
+ this.#write({ type: "control_request", request_id: `int-${++this.reqId}`, request: { subtype: "interrupt" } });
56
+ }
57
+
58
+ stop() {
59
+ if (!this.proc) return;
60
+ const p = this.proc;
61
+ this.proc = null;
62
+ p.stdin.end();
63
+ setTimeout(() => p.kill("SIGTERM"), 1500);
64
+ }
65
+
66
+ #write(obj) {
67
+ if (!this.proc) throw new Error("Claude Code is not running");
68
+ this.proc.stdin.write(JSON.stringify(obj) + "\n");
69
+ }
70
+ }
package/lib/diff.mjs ADDED
@@ -0,0 +1,46 @@
1
+ /* Line diff (LCS) with unified-style hunks, good enough for slide-sized files. */
2
+
3
+ export function diffLines(a, b) {
4
+ const A = a.split("\n"), B = b.split("\n");
5
+ // trim common prefix/suffix so the LCS table stays small
6
+ let pre = 0;
7
+ while (pre < A.length && pre < B.length && A[pre] === B[pre]) pre++;
8
+ let suf = 0;
9
+ while (suf < A.length - pre && suf < B.length - pre && A[A.length - 1 - suf] === B[B.length - 1 - suf]) suf++;
10
+ const a2 = A.slice(pre, A.length - suf), b2 = B.slice(pre, B.length - suf);
11
+ const n = a2.length, m = b2.length;
12
+ const L = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
13
+ for (let i = n - 1; i >= 0; i--)
14
+ for (let j = m - 1; j >= 0; j--)
15
+ L[i][j] = a2[i] === b2[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
16
+ const ops = [];
17
+ for (let k = 0; k < pre; k++) ops.push([" ", A[k], k + 1, k + 1]);
18
+ let i = 0, j = 0;
19
+ while (i < n || j < m) {
20
+ if (i < n && j < m && a2[i] === b2[j]) { ops.push([" ", a2[i], pre + i + 1, pre + j + 1]); i++; j++; }
21
+ else if (i < n && (j === m || L[i + 1][j] >= L[i][j + 1])) { ops.push(["-", a2[i], pre + i + 1, null]); i++; }
22
+ else { ops.push(["+", b2[j], null, pre + j + 1]); j++; }
23
+ }
24
+ for (let k = 0; k < suf; k++) ops.push([" ", A[A.length - suf + k], A.length - suf + k + 1, B.length - suf + k + 1]);
25
+ return ops;
26
+ }
27
+
28
+ export function unifiedDiff(a, b, file, context = 2) {
29
+ const ops = diffLines(a, b);
30
+ let added = 0, removed = 0;
31
+ const changed = ops.map((o, k) => (o[0] !== " " ? k : -1)).filter((k) => k >= 0);
32
+ if (!changed.length) return { text: "", added, removed };
33
+ const keep = new Set();
34
+ for (const k of changed) for (let d = -context; d <= context; d++) keep.add(k + d);
35
+ const lines = [`--- ${file}`, `+++ ${file}`];
36
+ let last = -2;
37
+ ops.forEach((o, k) => {
38
+ if (!keep.has(k)) return;
39
+ if (k !== last + 1) lines.push(`@@ line ${o[2] ?? o[3]} @@`);
40
+ lines.push(o[0] + o[1]);
41
+ if (o[0] === "+") added++;
42
+ if (o[0] === "-") removed++;
43
+ last = k;
44
+ });
45
+ return { text: lines.join("\n"), added, removed };
46
+ }