pindeck 3.2.0 → 3.3.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/README.md +10 -4
- package/front.mjs +1 -0
- package/package.json +1 -1
- package/server.mjs +25 -16
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PinDeck
|
|
2
2
|
|
|
3
|
-
> **macOS and Linux · Version 3** (`3.
|
|
3
|
+
> **macOS and Linux · Version 3** (`3.3.0`). On Linux, PinDeck runs as a headless server that you use
|
|
4
4
|
> from your Mac's browser through an SSH tunnel (see [Running on a Linux server](#running-on-a-linux-server)).
|
|
5
5
|
> Windows is not supported. Coming from PinDeck 2: version 3 keeps its settings and chats in
|
|
6
6
|
> `~/.pindeck-v3`, apart from 2.x (see [Models, providers and configuration](#models-providers-and-configuration)).
|
|
@@ -492,8 +492,8 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
|
|
|
492
492
|
from `lib/system-prompt.md` and the deck's `CLAUDE.md` and `AGENTS.md`. Codex model and effort changes
|
|
493
493
|
reuse the active thread writer and apply to the next turn. Claude changes and provider switches prepare
|
|
494
494
|
a replacement process before retiring the old one. Events from replaced sessions are ignored.
|
|
495
|
-
* **Permissions.** The assistant may read
|
|
496
|
-
editor's CLI. Nothing else: no other commands, and no edits to `brand.css`, `vendor/`, `.git/` or, for a
|
|
495
|
+
* **Permissions.** The assistant may read the deck folder, its screenshots, attached PDFs and asset roots,
|
|
496
|
+
edit and create files in the deck folder, and run the editor's CLI. Nothing else: no other commands, and no edits to `brand.css`, `vendor/`, `.git/` or, for a
|
|
497
497
|
named deck file, the other decks in its folder. Claude Code loads no settings files and no MCP servers,
|
|
498
498
|
so no allow rule in your own, the deck's or a parent folder's settings widens this. Codex has no shell
|
|
499
499
|
and no file access of its own: it reads and edits through PinDeck's own tools, which apply the same
|
|
@@ -553,12 +553,18 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
|
|
|
553
553
|
* One writer at a time: PinDeck is not made for editing the same deck from two editors or tools at once.
|
|
554
554
|
* A deck's own scripts run sandboxed in the editor and in Present: they cannot use `localStorage`, cookies
|
|
555
555
|
or the editor page. A deck that needs them for its slides to work should catch the error.
|
|
556
|
-
* Decks are `.html` files; `.htm` files are not listed or opened as decks.
|
|
557
556
|
* Linux: pictures of PDF pages and PowerPoint slides, and waveforms of non-WAV audio, need
|
|
558
557
|
Poppler, LibreOffice and ffmpeg on the server (see [Running on a Linux server](#running-on-a-linux-server)).
|
|
559
558
|
|
|
560
559
|
## Versions
|
|
561
560
|
|
|
561
|
+
* **3.3.0**: fixes from a code audit.
|
|
562
|
+
- After a PowerPoint import, refining the slides no longer deletes pictures that the deck still uses
|
|
563
|
+
(when the turn was stopped, failed or only changed `theme.css`); Undo brings removed pictures back.
|
|
564
|
+
- Long texts with non-ASCII characters (large notes, tables, SVG) no longer get broken characters.
|
|
565
|
+
- Claude reads only the deck, its screenshots, attached PDFs and asset roots, as Codex already did, and
|
|
566
|
+
cannot create a `.git` file in the deck folder.
|
|
567
|
+
|
|
562
568
|
* **3.2.0**: PinDeck 3, the first stable release of version 3: everything from the 3.0, 3.1 and 3.2 betas
|
|
563
569
|
below. Compared with 2.11:
|
|
564
570
|
- OpenAI Codex as a second assistant next to Claude Code, with the models and efforts each backend
|
package/front.mjs
CHANGED
|
@@ -243,6 +243,7 @@ function follow(st, tab, key) {
|
|
|
243
243
|
}
|
|
244
244
|
async function readJson(req) {
|
|
245
245
|
let s = "";
|
|
246
|
+
req.setEncoding("utf8");
|
|
246
247
|
for await (const c of req) { s += c; if (s.length > 10000) throw new Error("body too large"); }
|
|
247
248
|
return s ? JSON.parse(s) : {};
|
|
248
249
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pindeck",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "PinDeck: an editor for HTML slide decks on macOS, and on Linux through an SSH tunnel. Pin elements, describe the change, and Claude Code or OpenAI Codex edits the slides.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"slides",
|
package/server.mjs
CHANGED
|
@@ -243,6 +243,7 @@ function staleEdit(res, b) {
|
|
|
243
243
|
const text = (res, code, s) => { res.writeHead(code, { "content-type": "text/plain; charset=utf-8" }); res.end(s); };
|
|
244
244
|
async function body(req) {
|
|
245
245
|
let s = "";
|
|
246
|
+
req.setEncoding("utf8");
|
|
246
247
|
for await (const c of req) { s += c; if (s.length > 5e6) throw new Error("body too large"); }
|
|
247
248
|
return s ? JSON.parse(s) : {};
|
|
248
249
|
}
|
|
@@ -576,7 +577,7 @@ function slidesInstructions() {
|
|
|
576
577
|
win over allow rules; a locked file changed anyway is restored after the turn (lock 2 in finishTurn). */
|
|
577
578
|
function lockedPaths() {
|
|
578
579
|
const { files, folders } = otherDecks();
|
|
579
|
-
return [...LOCKED, "vendor/**", ".git/**", ...files, ...folders.map((f) => `${f}/**`)];
|
|
580
|
+
return [...LOCKED, "vendor/**", ".git", ".git/**", ...files, ...folders.map((f) => `${f}/**`)];
|
|
580
581
|
}
|
|
581
582
|
/* The other decks in this deck's folder: its other .html files, and the subfolders that hold a deck of their
|
|
582
583
|
own (as the deck menu lists them). Neither the assistant nor this deck's undo touches them. */
|
|
@@ -612,10 +613,9 @@ function othersWorkedSince(start) {
|
|
|
612
613
|
for (const f of files) {
|
|
613
614
|
try {
|
|
614
615
|
const o = JSON.parse(fs.readFileSync(path.join(TURNS_DIR, f), "utf8"));
|
|
615
|
-
|
|
616
|
-
try { process.kill(o.pid, 0); } catch (e) { alive = e.code === "EPERM"; }
|
|
616
|
+
const running = alive(o.pid);
|
|
617
617
|
// a turn still open in a server that is gone ended when that server did, at the latest
|
|
618
|
-
if (o.turns.some((x) => (x.end ?? (
|
|
618
|
+
if (o.turns.some((x) => (x.end ?? (running ? now : x.start)) >= start)) names.push(o.deck);
|
|
619
619
|
} catch {}
|
|
620
620
|
}
|
|
621
621
|
return names;
|
|
@@ -671,7 +671,7 @@ function makeSession(choice) {
|
|
|
671
671
|
args: () => ["--append-system-prompt", systemPrompt + slidesInstructions() + deckInstructions(),
|
|
672
672
|
"--setting-sources", "", "--strict-mcp-config", "--safe-mode", "--disable-slash-commands", // no settings files, no MCP servers or connectors
|
|
673
673
|
"--permission-mode", "default", "--tools", "Read,Glob,Grep,Edit,Write,Bash",
|
|
674
|
-
"--allowedTools",
|
|
674
|
+
"--allowedTools", ...readRules(), "Edit(./**)", "Write(./**)",
|
|
675
675
|
...["layout", "shot", "slides", "pdf", "pdf-figure"].map(command => `Bash(${CLI_CMD} ${command}:*)`),
|
|
676
676
|
"--disallowedTools", ...lockedPaths().flatMap((f) => [`Edit(./${f})`, `Write(./${f})`]),
|
|
677
677
|
...(choice.model ? ["--model", choice.model] : []), ...(choice.effort ? ["--effort", choice.effort] : [])],
|
|
@@ -679,6 +679,13 @@ function makeSession(choice) {
|
|
|
679
679
|
agent.initialEvent = () => initial;
|
|
680
680
|
return agent;
|
|
681
681
|
}
|
|
682
|
+
/* Claude reads what Codex's tools may read (agentTools' readDirs): the deck, screenshots, attached PDFs and
|
|
683
|
+
asset roots, each by its path and its real path ("//" starts an absolute path in a rule) */
|
|
684
|
+
function readRules() {
|
|
685
|
+
const dirs = [SHOT_DIR, PDF_DIR, ...ASSET_ROOTS].flatMap((d) => { try { return [d, fs.realpathSync(d)]; } catch { return [d]; } });
|
|
686
|
+
const globs = ["./**", ...new Set(dirs.map((d) => `/${d}/**`))];
|
|
687
|
+
return ["Read", "Glob", "Grep"].flatMap((tool) => globs.map((g) => `${tool}(${g})`));
|
|
688
|
+
}
|
|
682
689
|
const providerAvailable = (provider) => detectCapabilities()[provider];
|
|
683
690
|
/* Every backend process that is stopped is waited for on exit: the discovery helpers, the session a switch
|
|
684
691
|
replaced, the current one. */
|
|
@@ -892,7 +899,7 @@ function handleEvent(ev) {
|
|
|
892
899
|
function startTurn(promptText, userEntry) {
|
|
893
900
|
// every deck file as it is now: the assistant may change any of them (not brand.css or vendor/), and undo
|
|
894
901
|
// takes back all it changed
|
|
895
|
-
state.turn = { id: userEntry.id, before: snapshot([...EDITABLE, ...LOCKED]), gitConfig: readDeck(".git/config"), scan: scanDeck(), touched: new Set(),
|
|
902
|
+
state.turn = { id: userEntry.id, before: snapshot([...EDITABLE, ...LOCKED]), gitConfig: readDeck(".git/config"), hadGit: fs.existsSync(path.join(DECK_DIR, ".git")), scan: scanDeck(), touched: new Set(),
|
|
896
903
|
time: { start: Date.now(), end: null } };
|
|
897
904
|
logTurnTime(state.turn.time);
|
|
898
905
|
state.busy = true;
|
|
@@ -930,6 +937,12 @@ function finishTurn({ error, cost, duration, denials = [] }) {
|
|
|
930
937
|
restored.push(".git/config");
|
|
931
938
|
} catch (e) { console.error(e.message); }
|
|
932
939
|
}
|
|
940
|
+
// a .git file ("gitdir: elsewhere") points git at another config, which the check above does not see
|
|
941
|
+
const dotGit = path.join(DECK_DIR, ".git");
|
|
942
|
+
if (!turn.hadGit && fs.lstatSync(dotGit, { throwIfNoEntry: false })?.isDirectory() === false) {
|
|
943
|
+
fs.rmSync(dotGit, { force: true });
|
|
944
|
+
restored.push(".git");
|
|
945
|
+
}
|
|
933
946
|
toolEntries.clear();
|
|
934
947
|
const eid = runEidPass();
|
|
935
948
|
const found = scanChanges(turn.scan, scanDeck());
|
|
@@ -952,13 +965,14 @@ function finishTurn({ error, cost, duration, denials = [] }) {
|
|
|
952
965
|
const deletedFiles = [];
|
|
953
966
|
for (const rel of turn.importedFiles || []) {
|
|
954
967
|
const abs = path.join(DECK_DIR, rel);
|
|
955
|
-
if (!fs.existsSync(abs) || EDITABLE.some((f) => (
|
|
968
|
+
if (!fs.existsSync(abs) || EDITABLE.some((f) => (readDeck(f) || "").includes(rel))) continue;
|
|
956
969
|
deletedFiles.push({ rel, data: fs.readFileSync(abs) });
|
|
957
970
|
fs.rmSync(abs, { force: true });
|
|
958
971
|
}
|
|
959
972
|
const t = { id: turn.id, kind: "agent", provider: settings.provider, conversation: state.conversation, before, after, files, deletedFiles, undone: false };
|
|
960
|
-
|
|
961
|
-
|
|
973
|
+
const kept = files.length || deletedFiles.length;
|
|
974
|
+
if (kept) { state.turns.push(t); state.redo = []; }
|
|
975
|
+
addEntry({ kind: "turn", turnId: kept ? t.id : null, files, error, cost, duration,
|
|
962
976
|
denials, restored, newIds: eid.assigned.length });
|
|
963
977
|
if (deletedFiles.length) addEntry({ kind: "notice", text: `Removed ${deletedFiles.map((f) => f.rel).join(", ")}: replaced, and no longer used.` });
|
|
964
978
|
if (notOurs.length) addEntry({ kind: "notice", text: `${notOurs.join(", ")} changed during this turn while the assistant of ` +
|
|
@@ -1243,11 +1257,7 @@ function undoConflicts(t, direction) {
|
|
|
1243
1257
|
const go = direction === "undo" ? t.addedFiles || [] : t.deletedFiles || [];
|
|
1244
1258
|
// a file to bring back or remove through a symlink out of the deck: not done (deckPath), so not begun
|
|
1245
1259
|
for (const { rel } of [...comeBack, ...go]) { try { deckPath(rel); } catch { files.push(rel); } }
|
|
1246
|
-
for (const { rel, data } of comeBack) {
|
|
1247
|
-
const abs = path.join(DECK_DIR, rel);
|
|
1248
|
-
if (fs.existsSync(abs) && !sameContent(abs, data)) files.push(rel);
|
|
1249
|
-
}
|
|
1250
|
-
for (const { rel, data } of go) {
|
|
1260
|
+
for (const { rel, data } of [...comeBack, ...go]) {
|
|
1251
1261
|
const abs = path.join(DECK_DIR, rel);
|
|
1252
1262
|
if (fs.existsSync(abs) && !sameContent(abs, data)) files.push(rel);
|
|
1253
1263
|
}
|
|
@@ -1330,8 +1340,7 @@ async function applyImport(imp, numbers, afterEid, refine = false) {
|
|
|
1330
1340
|
const afterIdx = found < 0 ? slides.length - 1 : found; // no such slide (any more): at the end
|
|
1331
1341
|
const after = slides[afterIdx];
|
|
1332
1342
|
const css = (readDeck("theme.css") || "") + (readDeck("brand.css") || "");
|
|
1333
|
-
const prefix = path.basename(imp.name, path.extname(imp.name)).
|
|
1334
|
-
.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").split("-").slice(0, 3).join("-") || "import";
|
|
1343
|
+
const prefix = slug(path.basename(imp.name, path.extname(imp.name))).split("-").slice(0, 3).join("-");
|
|
1335
1344
|
const r = await buildImport({ model: imp.model, numbers, pdf, workDir: imp.dir, deckDir: DECK_DIR, deckHtml: html0, css, after: afterIdx, prefix });
|
|
1336
1345
|
// building takes a while (pictures are cut from the slides): a change made meanwhile must not be overwritten
|
|
1337
1346
|
if (EDITABLE.some((f) => readDeck(f) !== before[f])) throw new Error("The deck changed during the import. Import again.");
|