pindeck 3.0.0-beta.1 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # PinDeck
2
2
 
3
- > **macOS-only · Version 3 beta preview** (`3.0.0-beta.1`). The beta uses npm’s `beta` channel; stable releases use `latest`. See availability and installation below.
3
+ > **macOS, and Linux as a beta · Version 3 beta preview** (`3.0.0-beta.2`). On Linux, PinDeck runs as a
4
+ > headless server that you use from your Mac's browser through an SSH tunnel (see
5
+ > [Running on a Linux server](#running-on-a-linux-server-beta)). Windows is not supported. The beta is on
6
+ > npm’s `beta` channel and the [`v3-beta`](https://github.com/ehabets/PinDeck/tree/v3-beta) branch; stable
7
+ > releases (`npx pindeck`) stay on 2.x. See installation below.
4
8
 
5
- **Pin it. Say it. Done.** PinDeck is a local macOS editor for HTML slide decks (reveal.js): the slides on
9
+ **Pin it. Say it. Done.** PinDeck is a local editor for HTML slide decks (reveal.js): the slides on
6
10
  the left, the slide in the middle, and a chat with Claude Code or OpenAI Codex on the right. You click elements to
7
11
  pin them, write what to change, and the selected assistant edits the HTML and CSS. The slide updates in place.
8
12
 
@@ -19,8 +23,8 @@ checked the layout; the next request, still being typed, refers to the new pins
19
23
  * **Edit by hand, too.** Move, resize and nudge elements with snapping, edit text in place (math stays
20
24
  TeX), write speaker notes, add, hide, move and delete slides. One undo history covers your edits and
21
25
  the assistant's.
22
- * **Stay on brand.** `brand.css` is locked: the assistant edits `index.html` and `theme.css` only, and checks
23
- its own work with PinDeck's layout command.
26
+ * **Stay on brand.** `brand.css` is locked: the assistant edits the deck, its styles and other files of the
27
+ deck folder, never `brand.css` or `vendor/`, and checks its own work with PinDeck's layout command.
24
28
  * **Bring material in.** Attach figures, audio and PDFs to a message, or import slides from PowerPoint
25
29
  into the deck's own layouts.
26
30
  * **Get it out.** Present from the browser, or export a handout or presentation PDF.
@@ -28,17 +32,20 @@ checked the layout; the next request, still being typed, refers to the new pins
28
32
 
29
33
  ## Quick start
30
34
 
31
- **Requires macOS.** Windows and Linux are not supported in v3.
35
+ Runs on macOS, and on Linux as a beta. The npm package declares `"os": ["darwin", "linux"]`, so normal
36
+ npm installation rejects other operating systems. Overriding that check does not make those platforms
37
+ supported. Running the server from a cloned repository on another OS shows a local support notice
38
+ instead of opening the deck or starting a backend.
32
39
 
33
- Install the public beta once `3.0.0-beta.1` has been published:
40
+ Install the beta from npm:
34
41
 
35
42
  ```bash
36
43
  npx pindeck@beta /path/to/deck
37
44
  # Or select this exact beta:
38
- npx pindeck@3.0.0-beta.1 /path/to/deck
45
+ npx pindeck@3.0.0-beta.2 /path/to/deck
39
46
  ```
40
47
 
41
- For the source preview, once the `v3-beta` branch is public:
48
+ Or run it from the `v3-beta` branch, which includes the example deck:
42
49
 
43
50
  ```bash
44
51
  git clone --branch v3-beta --single-branch https://github.com/ehabets/PinDeck.git PinDeck-v3
@@ -46,16 +53,19 @@ cd PinDeck-v3
46
53
  node server.mjs ocean-example
47
54
  ```
48
55
 
49
- **Availability:** this checkout contains the prepared beta; its branch and npm package have not yet
50
- been published. Until then, run `node server.mjs ocean-example` from your local v3 checkout.
56
+ Use `npx pindeck@latest /path/to/deck` for the stable release; the beta channel does not replace it.
57
+ Releases, with notes for each beta, are on [GitHub](https://github.com/ehabets/PinDeck/releases).
51
58
 
52
- Use `npx pindeck@latest /path/to/deck` for the stable release. The beta channel does not replace it.
53
- The example deck is included in the repository; npm users supply their own deck folder.
54
- Then open <http://127.0.0.1:5180/>.
59
+ Then open the login link PinDeck prints (`PinDeck: http://127.0.0.1:5180/login?code=...`). It works
60
+ once and logs this browser in; for a new one, run `pindeck login` (or `node server.mjs login`).
55
61
 
56
- Options: `--port 5180`, `--provider claude|codex`, `--model <name>` (for this run only; otherwise the model in
57
- `~/.pindeck-v3/config.json`), `--help`. The deck folder defaults to the current folder and must contain
58
- `index.html`. Runtime files (chat history, screenshots, backups) are kept in `~/.pindeck-v3/` (change with
62
+ The deck is a file (`pindeck path/to/talk.html`) or a folder, which opens its `index.html`; without
63
+ one, the current folder. Several decks can share a folder, each with its own chat and undo history;
64
+ they share the folder's `theme.css` and `brand.css`, and the assistant edits only the deck that is open.
65
+ PinDeck adds element ids to the deck file when it opens it (see below), so try it on a copy of a deck
66
+ first. Options: `--port 5180`, `--provider claude|codex`, `--model <name>` (for this run only; otherwise the
67
+ model in `~/.pindeck-v3/config.json`), `--asset-root <folder>` (see [Files outside the deck folder](#files-outside-the-deck-folder)),
68
+ `--help`. Runtime files (chat history, screenshots, backups) are kept in `~/.pindeck-v3/` (change with
59
69
  `PINDECK_HOME`), not in the deck. `ocean-example/` is a complete example deck to start from.
60
70
 
61
71
  ## Compatibility and requirements
@@ -65,17 +75,19 @@ Options: `--port 5180`, `--provider claude|codex`, `--model <name>` (for this ru
65
75
  The corresponding executable must be on `PATH`; `CLAUDE_BIN` and `CODEX_BIN` can override its path.
66
76
  Only the selected backend is required for editing. Model and effort availability comes from that
67
77
  backend and account, and may differ from the provider’s website or desktop app.
68
- * **Platforms:** macOS only. Windows and Linux are not supported in v3. The npm package declares
69
- `"os": ["darwin"]`, so normal npm installation rejects other operating systems. This check can be
70
- overridden; bypassing it does not make those platforms supported. Source launches on unsupported
71
- platforms show a local support page without opening the deck or starting a backend. Native Windows support is a
72
- possible v4 feature, not part of this beta.
73
- * **Screenshots and PDF export:** require a locally installed Chrome, Chromium, Edge or Brave.
74
- * **PDF input through Codex:** page-text extraction and figure rendering use macOS PDFKit and
75
- `osascript`; this bridge is macOS-only. Claude uses its native PDF-reading capabilities, while
76
- PinDeck’s PDF figure-rendering helper still requires macOS.
78
+ * **Platforms:** macOS, and Linux as a beta (see [Running on a Linux server](#running-on-a-linux-server-beta)).
79
+ Windows is not supported. The npm package declares `"os": ["darwin", "linux"]`, so normal npm
80
+ installation rejects other operating systems. This check can be overridden; bypassing it does not make
81
+ those platforms supported. Source launches on unsupported platforms show a local support page without
82
+ opening the deck or starting a backend. Native Windows support is a possible v4 feature, not part of this beta.
83
+ * **Screenshots and PDF export:** require Chrome, Chromium, Edge or Brave where the server runs.
84
+ * **PDF input through Codex:** pages are rendered with macOS PDFKit, or Poppler on Linux. Page-text
85
+ extraction uses `osascript` and is macOS-only; on Linux Codex reads the rendered page image. Claude uses
86
+ its native PDF-reading capabilities.
77
87
  * **PowerPoint import:** basic `.pptx` parsing uses Node.js; rendered slide pictures require
78
- PowerPoint or Keynote on macOS.
88
+ PowerPoint or Keynote on macOS, or LibreOffice and Poppler on Linux.
89
+ * **Linux tools:** Poppler, LibreOffice and ffmpeg, each optional; features that need a missing one are
90
+ switched off with the reason shown in the editor.
79
91
 
80
92
  The initial live editing checks used Codex CLI **0.144.1** and Claude Code **2.1.289**.
81
93
  Further live checks on macOS passed with Codex CLI **0.162.1** and Claude Code **2.1.301**:
@@ -120,10 +132,24 @@ cp -R ocean-example my-template
120
132
  3. **`index.html`:** the footer text in the `data-` attributes of `<div class="reveal">`, and the
121
133
  `pindeck:footer-top` and `pindeck:locked` meta tags if the footer or the brand elements move.
122
134
  `deck.js` holds the math macros, the footer markup and the audio player.
123
- 4. **`README.md` and `CLAUDE.md`:** describe the new classes and rules there. PinDeck includes
124
- the deck’s `CLAUDE.md` in both backends’ instructions; the assistant can read `README.md` for details.
125
- The example's `CLAUDE.md` calls `../cli.mjs` and `../server.mjs`; fix these paths if the template
126
- does not sit next to PinDeck.
135
+ 4. **`README.md`, `CLAUDE.md` and `AGENTS.md`:** both backends follow the deck's `CLAUDE.md` and
136
+ `AGENTS.md`, so describe the new classes and rules there; the assistant can read `README.md` for details.
137
+ Refer to PinDeck's commands as "the PinDeck CLI command given in the system prompt", as the
138
+ example's `CLAUDE.md` does; PinDeck tells the assistant the right path wherever the deck is. An older
139
+ `node ../cli.mjs` in a deck's `CLAUDE.md` is replaced with that command.
140
+
141
+ **Instructions for the assistant.** PinDeck gives both backends the deck folder's own `CLAUDE.md` and
142
+ `AGENTS.md`. It does not load such files from parent folders, as Claude Code and Codex do in a terminal:
143
+ those are usually written for coding (run the tests, commit), which PinDeck's assistant cannot do; they
144
+ would change with where the deck happens to sit; and on a shared server a parent folder may be writable
145
+ by others. To share rules between decks, say so in the deck's own file, in one of two ways:
146
+
147
+ - **Import:** `Follow @../shared/STYLE.md for wording.` includes that file's text. Paths are relative to
148
+ the file they are in (or absolute, or `~/...`); imported files may import others, up to 5 levels,
149
+ each file once, 200 KB in all. Only text files are imported; an e-mail address, an `@name` that is
150
+ not a file, and anything in code are left as written.
151
+ - **Pointer:** `The style rules are in ../shared/STYLE.md; read them first.` The assistant reads the file
152
+ when it needs it.
127
153
 
128
154
  **New slide** and the PowerPoint import model new slides on the deck's own: keep a slide with class
129
155
  `title-slide`, one with `section-slide`, and a content slide with `.slide-head` (`h2`, optionally a
@@ -200,6 +226,72 @@ Attached PDFs are handled page by page using PDFKit on macOS: extracted text plu
200
226
  PDF reading/rendering through this bridge is currently macOS-only. Audio is embedded by path and is
201
227
  not transcribed. Read operations have a 12 MB limit; long text can be read in chunks.
202
228
 
229
+ ### Files outside the deck folder
230
+
231
+ A deck can use files from a folder around it, such as a style sheet or figures shared by several
232
+ talks: start PinDeck with `--asset-root <folder>` (repeatable), or list the folders in config.json as
233
+ `"assetRoots": ["~/talks"]`. When the folder contains the deck, PinDeck serves the deck from inside
234
+ it, so relative paths such as `../shared/style.css` work, in the editor, in screenshots and in PDF
235
+ exports. Claude may read those files but does not edit them.
236
+
237
+ Symlinks: PinDeck serves a file only if, with symlinks followed, it lies inside the deck folder or an
238
+ asset root. A symlink in the deck that leads anywhere else is refused (403). To use a symlinked
239
+ library folder, add the folder it points to as an asset root; it does not have to contain the deck.
240
+
241
+ ## Running on a Linux server (beta)
242
+
243
+ PinDeck can run on a Linux machine where the deck lives, with the editor in your Mac's browser. The
244
+ deck, PinDeck, the backend (Claude Code or Codex) and the deck's git clone all stay on the server; the browser only shows the
245
+ editor. The server listens on 127.0.0.1 there, as on a Mac, and an SSH tunnel brings that port to your Mac.
246
+
247
+ On the server you need Node 20 or later (22 or 24 recommended), git if the deck is in a repository, and
248
+ the `claude` or `codex` CLI installed and logged in under your account there, if you want the chat. Without one,
249
+ editing by hand still works. Then, in one SSH session on the server:
250
+
251
+ ```bash
252
+ node server.mjs path/to/deck --port 5180
253
+ ```
254
+
255
+ and on your Mac, in another terminal (`myserver` is your usual SSH host):
256
+
257
+ ```bash
258
+ ssh -N -T -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -L 127.0.0.1:5180:127.0.0.1:5180 myserver
259
+ ```
260
+
261
+ Open the login link PinDeck printed on the server in the Mac's browser. The local port may differ from
262
+ the server's: with `-L 127.0.0.1:8080:127.0.0.1:5180`, change 5180 in the link to 8080. PinDeck accepts any
263
+ port, as long as the address is `127.0.0.1` or `localhost`. If the tunnel drops, a message you have already sent keeps running on the
264
+ server. Restart the tunnel and the editor reconnects and shows the files as they are then; it never sends a
265
+ message twice. If PinDeck restarts, the editor asks you to log in again: run `node server.mjs login
266
+ path/to/deck` on the server for a new link.
267
+
268
+ Other users of a shared server cannot open the editor or read the deck through PinDeck: the token that
269
+ protects it is only in your browser's cookie and in PinDeck's own files in `~/.pindeck` (readable only by
270
+ you), never in a page or on a command line.
271
+
272
+ What works on Linux in this beta: editing by hand, speaker notes, slides, pins, the chat with Claude or Codex,
273
+ Undo/Redo, live reload when files change on the server, layout checks, attaching figures, audio and
274
+ PDFs, `git-setup` and the PowerPoint import. Some features use programs on the server, and are on when
275
+ they are installed (on Debian or Ubuntu: `sudo apt install poppler-utils libreoffice-impress ffmpeg`):
276
+
277
+ | Feature | Needs |
278
+ |---|---|
279
+ | Screenshots, layout checks without an open editor, PDF export | Chrome or Chromium (`PINDECK_LINUX_CHROME=0` turns these off) |
280
+ | Pictures of PDF pages (`pdf-figure`) | Poppler (`pdftoppm`, `pdfinfo`) |
281
+ | Pictures of PowerPoint slides in the import (thumbnails, diagrams) | LibreOffice (`soffice`) and Poppler |
282
+ | Waveforms of audio other than WAV in the import | ffmpeg |
283
+
284
+ Without them the editor says what is missing (hover over "Server: Linux (beta)"), and the assistant is told
285
+ which commands are not available. LibreOffice may draw some slides a little differently from PowerPoint;
286
+ the pictures are references and figures, the slides themselves are rebuilt from the file. Chromium from snap may not be
287
+ allowed to use hidden folders in your home folder such as `~/.pindeck-v3`; if screenshots fail with it, set
288
+ `PINDECK_HOME` to a folder that is not hidden, or install Chrome or Chromium from a package.
289
+
290
+ Run one PinDeck per deck. While PinDeck or the assistant is working on a deck, do not edit it with other tools
291
+ or switch git branches at the same time. Stop first, or wait until the assistant has finished. PinDeck notices
292
+ changes made by other programs and reloads, and it refuses Undo, Redo or an edit that would overwrite
293
+ them. Its checks cannot rule out every race with a program that writes at the same moment.
294
+
203
295
  ## Keeping the ids out of git
204
296
 
205
297
  PinDeck adds `data-eid` attributes to the deck it opens. If the deck is in git, run once per clone:
@@ -231,9 +323,9 @@ show and commit as usual. Clones without the filter configured simply see the fi
231
323
  | 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. |
232
324
  | 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. |
233
325
  | 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. |
234
- | Model and effort | Choose a discovered Claude or OpenAI model in the chat header, then use the separate reasoning-effort selector. Provider changes offer an optional handover; same-provider changes continue the conversation. |
326
+ | Model and effort | Choose a discovered Claude or OpenAI model in the chat header, then use the separate reasoning-effort selector. Provider changes offer an optional handover; same-provider changes continue the conversation. The choice is saved in `~/.pindeck-v3/config.json`. |
235
327
  | 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 the assistant, e.g. "put this figure on a new slide after 4". Removing an attachment before sending deletes the file again. |
236
- | 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-v3`); the assistant 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". |
328
+ | 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-v3`); the assistant 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, or Linux with Poppler). E.g. "put Fig. 3 of this paper on slide 7" or "add the results of Table 2 to slide 9". |
237
329
  | 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. |
238
330
  | Navigate | Slide list, `←` `→`, or the toolbar. **Present** opens the deck in a new tab. |
239
331
 
@@ -305,34 +397,50 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
305
397
  * **Layout instead of screenshots.** With each message the editor measures the current slide and
306
398
  every pinned slide in your browser: position and size of every element, font sizes, and issues
307
399
  (content below the footer rule, outside the slide, overlaps, clipped text). The assistant gets this as
308
- text. To check its own work it runs `node ../cli.mjs layout <slide>...` (`--issues` for the
400
+ text. To check its own work it runs the PinDeck CLI's `layout <slide>...` (`--issues` for the
309
401
  issues only). Screenshots are
310
402
  only taken when you tick the box or the assistant needs a visual judgement.
311
- * **One selected backend.** Claude runs as a long-lived stream-json process; Codex runs as a local
312
- app-server over stdio. Codex model and effort changes reuse the active thread writer and apply
313
- to the next turn. Claude changes and provider switches prepare a replacement process before
314
- retiring the old one. Events from replaced sessions are ignored. Provider-specific tool instructions share the same pin/layout context.
315
- * **Permissions.** The selected backend edits the two slide files through the restrictions described
316
- above; it does not receive unrestricted workspace write access.
317
- * **Undo.** The server keeps a copy of the editable files from before each change (an AI turn
318
- or a hand edit). Undo and redo walk that history and tell the assistant on the next message.
403
+ * **One selected backend.** Claude runs as a long-lived `claude -p` stream-json process; Codex runs as a
404
+ local app-server over stdio. Both run in the deck folder under your own login, with the instructions
405
+ from `lib/system-prompt.md` and the deck's `CLAUDE.md` and `AGENTS.md`. Codex model and effort changes
406
+ reuse the active thread writer and apply to the next turn. Claude changes and provider switches prepare
407
+ a replacement process before retiring the old one. Events from replaced sessions are ignored.
408
+ * **Permissions.** The assistant may read files, edit and create files in the deck folder, and run the
409
+ editor's CLI. Nothing else: no other commands, and no edits to `brand.css`, `vendor/`, `.git/` or, for a
410
+ named deck file, the other decks in its folder. Claude Code loads no settings files and no MCP servers,
411
+ so no allow rule in your own, the deck's or a parent folder's settings widens this. Codex has no shell
412
+ and no file access of its own: it reads and edits through PinDeck's own tools, which apply the same
413
+ rules and refuse symlinks out of the deck. If `brand.css` changes during a turn anyway, the server
414
+ restores it and says so in the chat.
415
+ * **Undo.** The server keeps a copy of every file a change touched, from before and after it (an AI
416
+ turn, which may change, create or delete several files of the deck, or a hand edit). Files over 2 MB
417
+ are not copied: the chat says when a turn changed one, and Undo leaves it as it is. Hidden files,
418
+ `vendor/` and `export/` are not part of undo. Undo and redo walk that history and tell the assistant on
419
+ the next message. They first check that the files are still as that change left them; if something
420
+ else changed them since, they do nothing and say which file differs, rather than overwrite that work.
421
+ * **Saving.** Every file PinDeck writes into the deck is written to a temporary file next to it and then
422
+ renamed into place, so other programs never see half a file. Each change from the editor carries the
423
+ revision of the files it was made on; if the files have changed since, the change is refused, the
424
+ slide reloads, and edited text can be copied from the message to type in again.
319
425
 
320
426
  ## Files
321
427
 
322
428
  | File | Purpose |
323
429
  |---|---|
324
- | `server.mjs` | Local server (127.0.0.1 only, random token per run): UI, deck files, events, backend session, undo, CLI endpoints. |
430
+ | `server.mjs` | Local server (127.0.0.1 only, random token per run, one-time login links): UI, deck files, events, backend session, undo, CLI endpoints, `login`. |
325
431
  | `cli.mjs` | `layout`, `shot`, `slides`, `pdf handout` / `pdf presentation` and `pdf-figure` commands for the backends (and for you), and `git-setup`. |
326
432
  | `public/` | The editor page, and `bridge.js`, which is injected into the deck in edit mode (never saved to it). |
327
433
  | `lib/pptx.mjs`, `lib/zip.mjs`, `lib/xml.mjs` | Reading `.pptx` files (no dependencies): slides, text, shapes and their positions, notes. |
328
434
  | `lib/pptx-import.mjs` | Turns PowerPoint slides into slides of the deck: figures, tables, build steps, audio, layout, HTML in the deck's style. |
329
435
  | `lib/waveform.mjs` | Waveform SVGs for imported audio. |
330
436
  | `lib/omml.mjs` | PowerPoint equations (Office Math) to TeX. |
331
- | `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. |
437
+ | `lib/pptx-render.mjs`, `lib/pdf-render.jxa.js` | Slide and PDF pictures: PowerPoint or Keynote (macOS) or LibreOffice (Linux) saves a PDF; macOS PDFKit or Poppler renders pages and regions of it. |
332
438
  | `lib/pdf.mjs` | PDF export: reveal.js print layout in headless Chrome, then print to PDF. |
333
439
  | `lib/eids.mjs` | Adds the `data-eid` attributes by editing the HTML as text. |
334
440
  | `lib/claude.mjs`, `lib/codex.mjs`, `lib/agent-process.mjs` | Backend adapters and JSON-line transport. |
335
441
  | `lib/models.mjs`, `lib/agent-tools.mjs` | Model/settings normalization, handover, and restricted Codex tools. |
442
+ | `lib/capabilities.mjs` | What this server can do on this system (macOS, Linux), for the editor, the API, the CLI and the assistant. |
443
+ | `lib/deck-files.mjs` | Atomic writes and the revision of the deck's files. |
336
444
  | `lib/system-prompt.md` | What the assistant is told about the editor. |
337
445
  | `CONTRIBUTING.md` | How to report bugs and contribute. |
338
446
  | `docs/` | Images for this README. |
@@ -340,17 +448,82 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
340
448
 
341
449
  ## Current Limitations
342
450
 
451
+ * reveal.js decks only. A deck made with another framework (Marp, Slidev, impress.js, remark, Spectacle,
452
+ Shower, deck.js, bespoke.js) is recognised and refused with its name before PinDeck changes anything.
343
453
  * Text inside inline SVG diagrams cannot be edited in place; pin it and ask the assistant.
344
454
  * Parts of an inline SVG move with the whole SVG; inline text (bold, spans) moves with its block.
345
455
  * 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 the assistant for those.
346
456
  * Moves are offsets (`translate`), so the space an element came from stays reserved. Ask the assistant to turn hand edits into proper CSS when a layout should really change.
347
457
  * The undo history lives on the server and starts fresh when PinDeck restarts.
458
+ * One writer at a time: PinDeck is not made for editing the same deck from two editors or tools at once.
459
+ * Linux (beta): pictures of PDF pages and PowerPoint slides, and waveforms of non-WAV audio, need
460
+ Poppler, LibreOffice and ffmpeg on the server (see [Running on a Linux server](#running-on-a-linux-server-beta)).
348
461
 
349
462
  ## Versions
350
463
 
464
+ * **3.0.0-beta.2**: everything from 2.9 and 2.10, with Codex brought up to it.
465
+ - Security: the fix from 2.9. The editor page no longer contains the token, and the deck needs it
466
+ too, so other users of the same machine cannot read either. PinDeck prints a one-time login link at
467
+ start (`pindeck login` prints a new one). Update from beta.1.
468
+ - Linux as a beta, safe saving, Undo/Redo that refuses to overwrite outside changes, decks with their
469
+ own file names, `--asset-root`, `@imports` in the deck's `CLAUDE.md` and `AGENTS.md`, and the other
470
+ 2.9 and 2.10 changes (see below).
471
+ - Both assistants may edit and create other files of the deck, never `brand.css`, `vendor/`, `.git/`
472
+ or other decks in the folder; one Undo takes back a whole turn. Codex creates files with a new
473
+ `pindeck_write` tool and edits through PinDeck's safe saving.
474
+ - Claude may run only PinDeck's own commands (layout, shot, slides, pdf, pdf-figure), each named.
475
+ - Starting with `--provider codex` and no saved model now picks Codex's default model, also while
476
+ the model lists are still loading.
477
+ - Stopping PinDeck waits for every backend process, including those that list models.
478
+
351
479
  * **3.0.0-beta.1**: Claude and Codex adapters, discovered model/effort menu, explicit provider
352
480
  switching with optional handover, curated model lists, a separate effort selector with per-model
353
- preferences, restricted Codex tools, and separate v3 runtime state. Not published.
481
+ preferences, restricted Codex tools, and separate v3 runtime state. On npm as `pindeck@beta`.
482
+
483
+ * **2.10.0-beta.1** (2.10 beta): decks with their own file names and shared files, and Claude working
484
+ on the whole deck.
485
+ - A deck file by name: `pindeck path/to/talk.html` (a folder still opens its `index.html`). Several
486
+ decks can share a folder, each with its own chat and undo history.
487
+ - Claude may edit and create other files of the deck (scripts, style sheets, notes), never
488
+ `brand.css`, `vendor/` or other decks in the folder. One Undo takes back everything a turn changed,
489
+ created or deleted; files over 2 MB are not copied, and the chat says so.
490
+ - Files outside the deck folder: `--asset-root <folder>` (or `"assetRoots"` in config.json) serves
491
+ the deck from inside that folder, so paths such as `../shared/style.css` work in the editor,
492
+ screenshots and PDF exports. A symlink that leads out of the deck and its asset roots is refused.
493
+ - Shared instructions: the deck's `CLAUDE.md` and `AGENTS.md` may import other files with `@path`.
494
+ `CLAUDE.md` files in parent folders are still not loaded, on purpose (see the README).
495
+ - Decks made with other slide frameworks (Marp, Slidev, impress.js, remark and others) are
496
+ recognised and refused with their name, before PinDeck changes anything.
497
+ - Linux: pictures of PDF pages with Poppler, the PowerPoint import with slide pictures from
498
+ LibreOffice, and waveforms of non-WAV audio with ffmpeg, each when installed.
499
+
500
+ * **2.9**: Linux as a beta, and safer editing.
501
+ - Linux (beta): PinDeck runs as a headless server, used from a Mac's browser through an SSH tunnel,
502
+ with Claude Code running on the server. The tunnel may use another local port than the server.
503
+ Screenshots, layout checks without an open editor and PDF export work when Chrome or Chromium is
504
+ found (`PINDECK_LINUX_CHROME=0` turns them off). Features a system cannot provide are switched off,
505
+ with the reason shown in the editor. Changes made to the deck by other programs are seen every
506
+ time, also after a file was replaced by a rename.
507
+ - Security: the editor page no longer contains the token, and the deck needs it too, so other users
508
+ of the same machine cannot read either. PinDeck prints a one-time login link at start
509
+ (`pindeck login` prints a new one), which keeps the token in an HttpOnly cookie named after the
510
+ server's port; headless Chrome logs in the same way, so the token is never on a command line. After
511
+ a restart, log in again with a new link.
512
+ - Saving is atomic; edits made on files that changed meanwhile are refused, and Undo/Redo no longer
513
+ overwrite changes made outside PinDeck.
514
+ - Audio and video play in Safari: deck files are sent with their length and byte ranges, and ogg,
515
+ opus, m4a, aac, flac and mov get their media types.
516
+ - Claude gets the right CLI command for decks outside the PinDeck folder; the example's `CLAUDE.md`
517
+ no longer names a path. The deck's `AGENTS.md` reaches Claude along with its `CLAUDE.md`.
518
+ - macOS: pictures of PDF pages (`pdf-figure`) and of PowerPoint slides have a white page instead of a
519
+ transparent one, so dark text no longer disappears on a dark slide.
520
+ - Screenshots and PDF exports are no longer now and then reported as failed although Chrome wrote them.
521
+ - `git-setup` works for decks behind symlinks or with spaces in their path. The tests run on Linux
522
+ and macOS in CI, including a real Chrome.
523
+
524
+ * **2.8.2**: declares macOS-only support in npm metadata and the README to prevent accidental
525
+ installation on unsupported operating systems. Source launches on other platforms show a support
526
+ notice without opening the deck.
354
527
 
355
528
  * **2.8.1**: fixes from a review. Speaker notes that fail to load leave the field empty and locked, instead
356
529
  of showing the previous slide's notes and saving your edits over them. A save that fails because the
package/cli.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  from the top left), as PNG: without a name in the screenshots
14
14
  folder to look at, with one saved into the deck
15
15
 
16
- node cli.mjs git-setup [deck] keep PinDeck's data-eid ids out of git (no server needed)
16
+ node cli.mjs git-setup [deck] keep PinDeck's data-eid ids out of git (no server needed; a folder or deck file)
17
17
 
18
18
  <slide> is a slide id (data-eid), a slide number (1-based) or "current". */
19
19
 
@@ -31,14 +31,17 @@ const arg = rest[0];
31
31
  on where PinDeck is installed; git config is per clone, so run this once in each clone. */
32
32
  if (cmd === "git-setup") {
33
33
  const FILTER = `sed 's/ data-eid="[^"]*"//g'`;
34
- const deck = path.resolve(arg || ".");
34
+ let deck = path.resolve(arg || ".");
35
+ try { if (!fs.statSync(deck).isDirectory()) deck = path.dirname(deck); } catch {} // a deck file: its folder
36
+ try { deck = fs.realpathSync(deck); } catch {} // git reports the real path; a symlinked path would not match
35
37
  const git = (...a) => execFileSync("git", ["-C", deck, ...a], { encoding: "utf8" }).trim();
36
38
  let root;
37
39
  try { root = git("rev-parse", "--show-toplevel"); }
38
40
  catch { console.error(`${deck} is not inside a git repository.`); process.exit(1); }
39
41
  git("config", "filter.pindeck-ids.clean", FILTER);
40
42
  const rel = path.relative(root, deck).split(path.sep).join("/");
41
- const pattern = `${rel ? rel + "/" : ""}*.html`;
43
+ // .gitattributes patterns end at whitespace: spaces in folder names are written as [[:space:]]
44
+ const pattern = `${rel ? rel.replace(/\s/g, "[[:space:]]") + "/" : ""}*.html`;
42
45
  const attrFile = path.join(root, ".gitattributes");
43
46
  const attrs = fs.existsSync(attrFile) ? fs.readFileSync(attrFile, "utf8") : "";
44
47
  const line = `${pattern} filter=pindeck-ids`;
@@ -6,6 +6,7 @@ export class AgentProcess {
6
6
  Object.assign(this, { onMessage, onExit });
7
7
  this.pending = new Map(); this.seq = 0; this.closed = false; this.stderr = '';
8
8
  this.proc = spawn(binary, args, { cwd, env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'] });
9
+ this.exited = new Promise(resolve => this.proc.once('close', resolve).once('error', () => this.proc.pid === undefined && resolve()));
9
10
  readline.createInterface({ input: this.proc.stdout }).on('line', line => {
10
11
  if (this.closed) return;
11
12
  let m; try { m = JSON.parse(line); } catch { return; }
@@ -39,11 +40,14 @@ export class AgentProcess {
39
40
  for (const p of this.pending.values()) { clearTimeout(p.timer); p.reject(error); }
40
41
  this.pending.clear(); this.onExit(error);
41
42
  }
43
+ /* End of input first (the CLI finishes and exits), SIGTERM after 1.5 s, SIGKILL after 4 s.
44
+ Resolves once the process has exited, so shutdown can wait for it. */
42
45
  stop() {
43
46
  this.end(new Error('Agent stopped.'));
44
- this.proc.stdin.destroy(); this.proc.kill('SIGTERM');
45
47
  const proc = this.proc;
46
- const timer = setTimeout(() => { if (proc.exitCode === null && proc.signalCode === null) proc.kill('SIGKILL'); }, 1500);
47
- timer.unref();
48
+ if (proc.exitCode !== null || proc.signalCode !== null || proc.pid === undefined) return Promise.resolve();
49
+ proc.stdin.end();
50
+ const term = setTimeout(() => proc.kill('SIGTERM'), 1500), kill = setTimeout(() => proc.kill('SIGKILL'), 4000);
51
+ return this.exited.finally(() => { clearTimeout(term); clearTimeout(kill); });
48
52
  }
49
53
  }
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
+ import { writeAtomic } from './deck-files.mjs';
6
7
  const exec = promisify(execFile);
7
8
  const text = value => ({ type: 'inputText', text: String(value) });
8
9
  const schema = properties => ({ type: 'object', properties, additionalProperties: false });
@@ -10,22 +11,35 @@ const str = { type: 'string' };
10
11
  export const renderedImagePath = result => result.match(/^(.+\.png) \(\d+ × \d+ px\)/m)?.[1];
11
12
  export const deckTools = [
12
13
  { type: 'function', name: 'pindeck_read', description: 'Read a deck text file, attached image, screenshot, or PDF page. For PDFs supply a 1-based page; returns page text and its rendered image. For text use offset to read beyond the first 120000 characters.', inputSchema: { ...schema({ path: str, page: { type: 'integer', minimum: 1 }, offset: { type: 'integer', minimum: 0 } }), required: ['path'] } },
13
- { type: 'function', name: 'pindeck_edit', description: 'Replace one exact unique text occurrence in index.html or theme.css. Read the file first. Only these two files are editable; brand.css is locked.', inputSchema: { ...schema({ path: str, oldText: str, newText: str }), required: ['path', 'oldText', 'newText'] } },
14
+ { type: 'function', name: 'pindeck_edit', description: 'Replace one exact unique text occurrence in a file of the deck folder: the deck file, theme.css or another of its files. Read the file first. brand.css, vendor/, .git/ and other decks in the folder are locked.', inputSchema: { ...schema({ path: str, oldText: str, newText: str }), required: ['path', 'oldText', 'newText'] } },
15
+ { type: 'function', name: 'pindeck_write', description: 'Create a new file in the deck folder (missing folders are created), or replace all of an existing one. Prefer pindeck_edit for changes to existing files. The same files are locked as for pindeck_edit.', inputSchema: { ...schema({ path: str, content: str }), required: ['path', 'content'] } },
14
16
  { type: 'function', name: 'pindeck_command', description: 'Run a PinDeck command: layout, shot, slides, pdf, or pdf-figure. Pass individual arguments in an array, never a shell command. Use pindeck_read on returned image paths.', inputSchema: { ...schema({ command: { enum: ['layout', 'shot', 'slides', 'pdf', 'pdf-figure'] }, args: { type: 'array', items: str, maxItems: 100 } }), required: ['command', 'args'] } },
15
17
  ];
16
- export function createDeckTools({ deckDir, readDirs = [], cliFile, env, pdfTextFile }) {
18
+ /* Codex may read the deck, screenshots, attached PDFs and asset roots, and write any file of the deck folder
19
+ except the locked ones: the same rules as Claude's --allowedTools / --disallowedTools. locked: patterns
20
+ such as brand.css or vendor/** (or a function returning them, as the other decks in the folder change);
21
+ env: an object or a function (the server's address is known only once it listens). */
22
+ export function createDeckTools({ deckDir, readDirs = [], cliFile, env, pdfTextFile, locked = ['brand.css', 'vendor/**', '.git/**'] }) {
17
23
  const deck = fs.realpathSync(deckDir);
18
- const roots = [deck, ...readDirs.map(d => fs.realpathSync(d))];
24
+ // looked up when used: the state folders (screenshots, attachments) are created once the server listens
25
+ const roots = () => [deck, ...readDirs.flatMap(d => { try { return [fs.realpathSync(d)]; } catch { return []; } })];
19
26
  const inside = (root, file) => file === root || file.startsWith(root + path.sep);
27
+ const envNow = () => (typeof env === 'function' ? env() : env);
28
+ const isLocked = rel => (typeof locked === 'function' ? locked() : locked)
29
+ .some(p => p.endsWith('/**') ? rel === p.slice(0, -3) || rel.startsWith(p.slice(0, -2)) : rel === p);
20
30
  function checked(file, write = false) {
21
31
  if (typeof file !== 'string' || !file || file.includes('\0')) throw new Error('Invalid file path.');
22
32
  const full = path.resolve(deck, file);
23
- if (write && !['index.html', 'theme.css'].some(n => full === path.join(deck, n))) throw new Error('Only index.html and theme.css can be edited.');
33
+ if (write) {
34
+ const rel = path.relative(deck, full).split(path.sep).join('/');
35
+ if (!rel || !inside(deck, full)) throw new Error('Only files inside the deck folder can be edited.');
36
+ if (isLocked(rel)) throw new Error(`${rel} is locked: it cannot be edited in PinDeck.`);
37
+ }
24
38
  // Check every component, including missing-file parents, to reject symlink escapes.
25
39
  let existing = full;
26
40
  while (!fs.existsSync(existing)) { const parent = path.dirname(existing); if (parent === existing) throw new Error('Missing file.'); existing = parent; }
27
41
  const real = fs.realpathSync(existing);
28
- if (!roots.some(root => inside(root, real)) || (write && real !== existing)) throw new Error('File is outside the permitted directories or is a symlink.');
42
+ if (!(write ? [deck] : roots()).some(root => inside(root, real)) || (write && real !== existing)) throw new Error('File is outside the permitted directories or is a symlink.');
29
43
  if (write && fs.existsSync(full) && (fs.lstatSync(full).isSymbolicLink() || fs.statSync(full).nlink !== 1)) throw new Error('Linked files cannot be edited.');
30
44
  return full;
31
45
  }
@@ -41,7 +55,7 @@ export function createDeckTools({ deckDir, readDirs = [], cliFile, env, pdfTextF
41
55
  if (!inside(figures, full) || (fs.existsSync(full) && (fs.lstatSync(full).isSymbolicLink() || fs.statSync(full).nlink !== 1))) throw new Error('Linked or invalid output file.');
42
56
  }
43
57
  }
44
- const { stdout } = await exec(process.execPath, [cliFile, name, ...args], { cwd: deck, env: { ...process.env, ...env }, signal, timeout: 120000, maxBuffer: 2 * 1024 * 1024 });
58
+ const { stdout } = await exec(process.execPath, [cliFile, name, ...args], { cwd: deck, env: { ...process.env, ...envNow() }, signal, timeout: 120000, maxBuffer: 2 * 1024 * 1024 });
45
59
  return stdout;
46
60
  }
47
61
  async function read(file, page, offset, signal) {
@@ -63,7 +77,7 @@ export function createDeckTools({ deckDir, readDirs = [], cliFile, env, pdfTextF
63
77
  const data = fs.readFileSync(full);
64
78
  const mime = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif' }[ext];
65
79
  if (mime) return [{ type: 'inputImage', imageUrl: `data:${mime};base64,${data.toString('base64')}` }];
66
- if (!['.html', '.css', '.md', '.txt', '.svg', '.json', '.js', '.mjs', '.csv'].includes(ext)) throw new Error('Read supports text, PNG/JPEG/WebP/GIF images, and rendered PDF pages. Audio is embedded by path, not transcribed.');
80
+ if (!['.html', '.htm', '.css', '.md', '.txt', '.svg', '.json', '.js', '.mjs', '.csv'].includes(ext)) throw new Error('Read supports text, PNG/JPEG/WebP/GIF images, and rendered PDF pages. Audio is embedded by path, not transcribed.');
67
81
  const start = Number.isInteger(offset) && offset >= 0 ? offset : 0;
68
82
  return [text(data.toString('utf8').slice(start, start + 120000))];
69
83
  }
@@ -77,8 +91,17 @@ export function createDeckTools({ deckDir, readDirs = [], cliFile, env, pdfTextF
77
91
  const old = fs.readFileSync(full, 'utf8');
78
92
  if (old.indexOf(args.oldText) < 0 || old.indexOf(args.oldText) !== old.lastIndexOf(args.oldText)) throw new Error('oldText must match exactly once. Re-read the file.');
79
93
  signal?.throwIfAborted();
80
- fs.writeFileSync(full, old.replace(args.oldText, () => args.newText));
81
- return [text('Edited ' + path.basename(full))];
94
+ writeAtomic(full, old.replace(args.oldText, () => args.newText));
95
+ return [text('Edited ' + path.relative(deck, full))];
96
+ }
97
+ if (name === 'pindeck_write') {
98
+ const full = checked(args.path, true);
99
+ if (typeof args.content !== 'string' || args.content.length > 2e6) throw new Error('Supply the file content as text (at most 2 MB).');
100
+ if (fs.existsSync(full) && !fs.statSync(full).isFile()) throw new Error('That path is a folder, not a file.');
101
+ signal?.throwIfAborted();
102
+ fs.mkdirSync(path.dirname(full), { recursive: true });
103
+ writeAtomic(full, args.content);
104
+ return [text('Wrote ' + path.relative(deck, full))];
82
105
  }
83
106
  throw new Error('Unknown PinDeck tool.');
84
107
  };