plotcoder-board 0.1.26 → 0.1.28
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 +3 -3
- package/package.json +1 -1
- package/scripts/plotcoder-mcp-server.mjs +57 -22
- package/src/board/fountain.js +1 -0
- package/src/board/readWall.d.ts +1 -1
- package/src/board/readWall.js +24 -4
package/README.md
CHANGED
|
@@ -51,9 +51,9 @@ An agent should call the tools, never fake mouse drags. The skill in `.cursor/sk
|
|
|
51
51
|
A **blind run** is a fresh agent given the on-ramp and a treatment and nothing
|
|
52
52
|
else, asked to build a wall and to keep a log of everything that made the job
|
|
53
53
|
harder than it should have been. The friction log is the product; the wall is
|
|
54
|
-
just what produces it.
|
|
54
|
+
just what produces it. Eighteen rounds have been run, the first three through a
|
|
55
55
|
repo checkout and the rest through the account door; every finding from the
|
|
56
|
-
first
|
|
56
|
+
first seventeen is fixed or decided, and round seventeen, the first to start from
|
|
57
57
|
a page of notes instead of a treatment, found the wall has no way to hold a
|
|
58
58
|
maybe — the open card, R59, proposed and mocked from it. [`blind-runs/`](blind-runs/) holds the rules that keep a round honest,
|
|
59
59
|
the table of rounds, and the next round's prompt with the test account filled in.
|
|
@@ -100,4 +100,4 @@ next round into a test of `claim_account` instead of the door it meant to test.
|
|
|
100
100
|
|
|
101
101
|
## Status
|
|
102
102
|
|
|
103
|
-
Version 0.1.
|
|
103
|
+
Version 0.1.28. A project of boards; sign in with your email and a password from the PlotCoder mark and your projects follow you to every device, share one with another writer by email and write it together live, or stay signed out and work on this device as before. Pages sit beside the wall: a scene's text lives on its card, measures it, paginates to the industry's rules, prints, goes out and comes in as Fountain or Final Draft, and goes out as Markdown or plain text for a collaborator in Google Docs. It installs as a progressive web app and opens offline; plotcoder.com serves over HTTPS. The wall, beats, card length, groups, arrows, pan and zoom, save and open, and the agent surface are in use.
|
package/package.json
CHANGED
|
@@ -1010,6 +1010,13 @@ function cueReport(state, text) {
|
|
|
1010
1010
|
return ` Cues: ${parts.join("; ")}.`;
|
|
1011
1011
|
}
|
|
1012
1012
|
|
|
1013
|
+
/** A write on an open card does not close it: the words stay until the writer clears them, and the reply says so (round eighteen, entry 31). */
|
|
1014
|
+
function stillOpen(notes) {
|
|
1015
|
+
const open = (notes ?? []).filter((note) => (note.open ?? "").trim());
|
|
1016
|
+
if (!open.length) return "";
|
|
1017
|
+
return ` ${open.length === 1 ? `"${open[0].headline}" is still open (${open[0].open})` : `${open.length} of them are still open`}: the words stay until set_open "" clears them, and the wall asks nothing else of ${open.length === 1 ? "it" : "them"} until then.`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1013
1020
|
/** What kinds of number a runtime folds together: measured from text, set by the writer, or the default page (round fifteen, entry 39). */
|
|
1014
1021
|
function runtimeKinds(state) {
|
|
1015
1022
|
const measured = state.notes.filter((note) => isMeasured(note));
|
|
@@ -1303,7 +1310,7 @@ server.registerTool(
|
|
|
1303
1310
|
"Add a card (post-it) to the board. A card is one scene: a headline plus the change it causes. Provide both headline and change. Optionally set color, x/y position, rank ('beat' for one of the major turns — a beat is a whole card, the scene where the turn happens), pages (how long it runs; leave it out and the card is taken to be about a page), plants (true if this scene sets something up that must pay off later), location (where it happens, as the writer would say it — 'the piano shop', not 'INT. PIANO SHOP'), and characters (who is in the scene, by name; a name not in the cast yet is added to it — name an unnamed person by their role, 'Dana's mother', rather than leaving them off). The reply names the card's id.",
|
|
1304
1311
|
inputSchema: {
|
|
1305
1312
|
headline: z.string().min(1),
|
|
1306
|
-
change: z.string().
|
|
1313
|
+
change: z.string().optional().describe("What is different when the scene ends. Required, unless the card is born open — then it may wait, and the reading will not ask for it while the words stand."),
|
|
1307
1314
|
color: colorSchema.optional(),
|
|
1308
1315
|
rank: rankSchema.optional(),
|
|
1309
1316
|
pages: pagesSchema.optional(),
|
|
@@ -1312,7 +1319,7 @@ server.registerTool(
|
|
|
1312
1319
|
before: z.string().optional().describe("Or before this card (id or headline)."),
|
|
1313
1320
|
location: z.string().optional(),
|
|
1314
1321
|
when: z.string().optional().describe('When the scene happens, as the writer says it — "night", "day four, dawn" — printed after the place on the scene heading.'),
|
|
1315
|
-
open: z.string().optional().describe("The writer's words for what is not decided about this card
|
|
1322
|
+
open: z.string().optional().describe("The writer's words for what is not decided about this card — \"whether Tom knows\" — so the card is born open: the reading lists it and asks nothing else of it until the words are cleared."),
|
|
1316
1323
|
characters: z.array(z.string().min(1)).optional(),
|
|
1317
1324
|
x: z.number().optional(),
|
|
1318
1325
|
y: z.number().optional(),
|
|
@@ -1326,17 +1333,21 @@ server.registerTool(
|
|
|
1326
1333
|
// The card, anyone new in its cast, and the casting land as one change, so
|
|
1327
1334
|
// one ⌘Z on the wall takes back the whole call and not just the cast.
|
|
1328
1335
|
if (args.after && args.before) return ok("Say where: after one card, or before one, not both.");
|
|
1336
|
+
// A card needs its change line — unless it is born open (round eighteen, entry 13): the writer had no consequence yet.
|
|
1337
|
+
if (!(args.change ?? "").trim() && !(args.open ?? "").trim()) return ok("A card needs its change line: what is different when the scene ends. If the writer has not decided it, pass open with their words and the change line waits.");
|
|
1329
1338
|
// The card beside which the new scene goes (round sixteen, entry 36): by id or headline, on this board.
|
|
1330
1339
|
const besideKey = (args.after ?? args.before ?? "").trim();
|
|
1331
1340
|
const beside = besideKey ? state.notes.find((note) => note.id === besideKey) ?? state.notes.find((note) => note.headline.trim().toLowerCase() === besideKey.toLowerCase()) ?? null : null;
|
|
1332
1341
|
if (besideKey && !beside) return ok(`No card with id or headline "${besideKey}" on this board. Call list_board.`);
|
|
1333
1342
|
const wallHasFollows = state.arrows.some((arrow) => arrow.kind !== "setup");
|
|
1334
1343
|
let joinedGroup = null;
|
|
1344
|
+
let removedArrows = 0;
|
|
1345
|
+
let drawnArrows = 0;
|
|
1335
1346
|
const { value: result, live, state: after } = await commitAll(`create_note "${args.headline}"`, (step, current) => {
|
|
1336
1347
|
let made = step({
|
|
1337
1348
|
type: "create_note",
|
|
1338
1349
|
headline: args.headline,
|
|
1339
|
-
change: args.change,
|
|
1350
|
+
change: (args.change ?? "").trim() || undefined,
|
|
1340
1351
|
// One colour unless the agent chooses: a wall an agent builds in one go
|
|
1341
1352
|
// would otherwise stripe through the cycle, and a writer reads a pattern
|
|
1342
1353
|
// into it (round four, finding 17). The wall's own new-card button keeps
|
|
@@ -1370,7 +1381,14 @@ server.registerTool(
|
|
|
1370
1381
|
}
|
|
1371
1382
|
// Wired into the story where the writer said, in the same frame.
|
|
1372
1383
|
if (beside && made?.id && wallHasFollows) {
|
|
1373
|
-
|
|
1384
|
+
// Count the rewiring, so the reply can say it as move_scene does (round eighteen, entry 36).
|
|
1385
|
+
const run = (command) => {
|
|
1386
|
+
const done = step(command);
|
|
1387
|
+
if (done.changed && command.type === "delete_arrow") removedArrows += 1;
|
|
1388
|
+
if (done.changed && command.type === "create_arrow") drawnArrows += 1;
|
|
1389
|
+
return done;
|
|
1390
|
+
};
|
|
1391
|
+
joinedGroup = landBeside(run, current, made.id, beside, Boolean(args.after));
|
|
1374
1392
|
step({ type: "apply_poses", poses: organizePoses(current(), {}) });
|
|
1375
1393
|
made = current().notes.find((note) => note.id === made.id) ?? made;
|
|
1376
1394
|
}
|
|
@@ -1389,7 +1407,7 @@ server.registerTool(
|
|
|
1389
1407
|
// Where it landed matters only until the tidy, so the reply says the rule once and never the coordinates (round fourteen, entry 11).
|
|
1390
1408
|
const placed = beside
|
|
1391
1409
|
? wallHasFollows
|
|
1392
|
-
? ` Wired ${args.after ? "after" : "before"} "${beside.headline}" in the story${joinedGroup ? `, in "${joinedGroup}"` : ""}, and the wall tidied.`
|
|
1410
|
+
? ` Wired ${args.after ? "after" : "before"} "${beside.headline}" in the story (${removedArrows} follows arrow${removedArrows === 1 ? "" : "s"} removed, ${drawnArrows} drawn)${joinedGroup ? `, in "${joinedGroup}"` : ""}, and the wall tidied.`
|
|
1393
1411
|
: ` The wall has no follows arrows, so "${args.after ? "after" : "before"}" has no story to land in: it sits after the last card; create_arrow the sequence, then move_scene.`
|
|
1394
1412
|
: args.x === undefined && args.y === undefined ? ` Placed after the last card in story order.${once("placed", " organize lays the wall out along the arrows.")}` : "";
|
|
1395
1413
|
// Under a lock a new scene has a letter, not a number: say it, since the board is the only other place to learn it (round fourteen, entry 44).
|
|
@@ -1519,6 +1537,12 @@ server.registerTool(
|
|
|
1519
1537
|
});
|
|
1520
1538
|
// No blank lines: ok() splits prose from payload on the first one.
|
|
1521
1539
|
const written = state.notes.filter((note) => isMeasured(note)).length;
|
|
1540
|
+
// Whose number the runtime is (round eighteen, entries 45, 47, 50): once a
|
|
1541
|
+
// scene is written the total is part measure, part guess, part default,
|
|
1542
|
+
// and the reading says which, the same way list_board does.
|
|
1543
|
+
const whose = runtimeKinds(state).replace(/^; /, "");
|
|
1544
|
+
// A beat's own pages are in no run (entry 48); say how many pages that is.
|
|
1545
|
+
const beatEighths = reading.beats.reduce((sum, beat) => sum + noteEighths(state.notes.find((note) => note.id === beat.id) ?? {}), 0);
|
|
1522
1546
|
const lines = [
|
|
1523
1547
|
`PlotCoder wall (${door(live, base)})`,
|
|
1524
1548
|
...(state.lock ? [`numbers: locked since ${String(state.lock.at).slice(0, 10)}; read_pages shows each scene's number`] : []),
|
|
@@ -1526,8 +1550,8 @@ server.registerTool(
|
|
|
1526
1550
|
`logline: ${state.logline ? `"${state.logline}"` : "(none yet)"}`,
|
|
1527
1551
|
"the cast and the places are list_board's, not the reading's",
|
|
1528
1552
|
state.targetEighths === DEFAULT_TARGET_EIGHTHS
|
|
1529
|
-
? `runtime: about ${formatPages(boardEighths(state))} pages; no target set (set_target)`
|
|
1530
|
-
: `runtime: about ${formatPages(boardEighths(state))} pages of a ${formatPages(state.targetEighths)}-page target — ${boardEighths(state) > state.targetEighths ? `${formatPages(boardEighths(state) - state.targetEighths)} over` : boardEighths(state) < state.targetEighths ? `${formatPages(state.targetEighths - boardEighths(state))} under` : "on it"}
|
|
1553
|
+
? `runtime: about ${formatPages(boardEighths(state))} pages (${whose || "no cards"}); no target set (set_target)`
|
|
1554
|
+
: `runtime: about ${formatPages(boardEighths(state))} pages of a ${formatPages(state.targetEighths)}-page target — ${boardEighths(state) > state.targetEighths ? `${formatPages(boardEighths(state) - state.targetEighths)} over` : boardEighths(state) < state.targetEighths ? `${formatPages(state.targetEighths - boardEighths(state))} under` : "on it"} (${whose || "no cards"}; page_count is the script so far)`,
|
|
1531
1555
|
`groups: ${
|
|
1532
1556
|
state.groups.length
|
|
1533
1557
|
? state.groups
|
|
@@ -1547,7 +1571,7 @@ server.registerTool(
|
|
|
1547
1571
|
? reading.beats.map((beat) => `"${beat.headline}"`).join(", ")
|
|
1548
1572
|
: "(none marked)"
|
|
1549
1573
|
}`,
|
|
1550
|
-
`runs between beats (the scenes between two turns; a beat's own pages are in no run${written < state.notes.length ? "; pages are estimates" : ""}):`,
|
|
1574
|
+
`runs between beats (the scenes between two turns; a beat's own pages are in no run${reading.beats.length ? ` — the ${reading.beats.length} beat${reading.beats.length === 1 ? "" : "s"} hold${reading.beats.length === 1 ? "s" : ""} about ${formatPages(beatEighths)} pages between them` : ""}${written < state.notes.length ? "; pages are estimates" : ""}):`,
|
|
1551
1575
|
...(runs.length ? runs.map((line) => ` - ${line}`) : [" (none)"]),
|
|
1552
1576
|
`setups and payoffs${written < state.notes.length ? " (distances in estimated pages)" : ""}:`,
|
|
1553
1577
|
...(reading.setups.length
|
|
@@ -1556,7 +1580,7 @@ server.registerTool(
|
|
|
1556
1580
|
...reading.later.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" is folded and pays off later, on "${boardById(projectForRead, item.boardId)?.name ?? item.boardId}"${item.noteId ? `, at ${episodeLabel(projectForRead, boardsNow, item.boardId, item.noteId)} "${boardsNow[item.boardId]?.notes?.find((note) => note.id === item.noteId)?.headline ?? item.noteId}"` : " — no scene there claims it yet"}`),
|
|
1557
1581
|
...reading.paidBy.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" pays off "${item.fromHeadline}" from "${item.fromBoardName}" (${episodeLabel(projectForRead, boardsNow, item.fromBoardId, item.fromNoteId)}), one board earlier`),
|
|
1558
1582
|
...(reading.open.length
|
|
1559
|
-
? ["open, by the writer's word (listed, not asked about while the words stand; set_open with \"\" closes):", ...reading.open.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" — ${item.words}`)]
|
|
1583
|
+
? ["open, by the writer's word (listed, not asked about while the words stand; set_open with \"\" closes):", ...reading.open.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" — ${item.words}${item.hides.length ? ` (would be asked, closed: ${item.hides.map((kind) => CHECK_WORDS[kind] ?? kind).join("; ")})` : ""}`)]
|
|
1560
1584
|
: []),
|
|
1561
1585
|
"questions the wall raises:",
|
|
1562
1586
|
...(reading.findings.length
|
|
@@ -1583,6 +1607,9 @@ server.registerTool(
|
|
|
1583
1607
|
}
|
|
1584
1608
|
if (kind === "unplaced" && !state.notes.some((note) => (note.location ?? "").trim())) return "no card without a place (not asked: no card placed yet)";
|
|
1585
1609
|
if (kind === "sequence" && state.groups.length === 0) return "no group too long for one sequence (not asked: no groups)";
|
|
1610
|
+
// A kind clean only because an open card is not asked says so (round eighteen, entry 18).
|
|
1611
|
+
const hiddenBy = reading.open.filter((item) => item.hides.includes(kind)).length;
|
|
1612
|
+
if (hiddenBy) return `${CHECK_WORDS[kind]} (except ${hiddenBy} open card${hiddenBy === 1 ? "" : "s"}, not asked)`;
|
|
1586
1613
|
return CHECK_WORDS[kind];
|
|
1587
1614
|
}).join("; ") || "(nothing — every check found something)"}`,
|
|
1588
1615
|
];
|
|
@@ -1869,7 +1896,7 @@ server.registerTool(
|
|
|
1869
1896
|
? ` It joined "${joinedGroup}", the group it landed in, so the tidy keeps it with the act.`
|
|
1870
1897
|
: group ? ` It is still in "${group.title || "an untitled group"}"; a frame does not follow a move, so say if the act or sequence should change.` : "";
|
|
1871
1898
|
return ok(
|
|
1872
|
-
`Moved "${card.headline}" to ${args.after ? "after" : "before"} "${target.headline}": ${removed} follows arrow(s) removed, ${drawn} drawn, setup arrows untouched, the wall tidied along them${where(live)}. Story order now: ${order.map((note, index) => `${index + 1}. ${note.headline}`).join(", ")}.${groupLine}${lockedNow} One undo takes the whole move back.`,
|
|
1899
|
+
`Moved "${card.headline}" to ${args.after ? "after" : "before"} "${target.headline}": ${removed} follows arrow(s) removed, ${drawn} drawn${final.arrows.some((arrow) => arrow.kind === "setup") ? ", setup arrows untouched" : ""}, the wall tidied along them${where(live)}. Story order now: ${order.map((note, index) => `${index + 1}. ${note.headline}`).join(", ")}.${groupLine}${lockedNow} One undo takes the whole move back.`,
|
|
1873
1900
|
{ order: order.map((note) => note.id) },
|
|
1874
1901
|
);
|
|
1875
1902
|
},
|
|
@@ -1990,11 +2017,12 @@ server.registerTool(
|
|
|
1990
2017
|
const short = state.targetEighths > 0 && boardEighths(state) * 2 < state.targetEighths;
|
|
1991
2018
|
const lines = [
|
|
1992
2019
|
`"${chosen.name}" beside this wall's ${beats} beat${beats === 1 ? "" : "s"}, of ${formatPages(state.targetEighths)} pages (the story so far runs to p. ${comparison.soFar}${allMeasured ? ", measured" : ", an estimate: unsized cards read as a page each"}); a match is the nearest of the wall's beats within ${MATCH_PAGES} pages, one to one and in order:`,
|
|
2020
|
+
// On a wall under half its target the pairing is arithmetic; say so before the rows, not after them (round eighteen, entry 52).
|
|
2021
|
+
...(short ? [`the wall runs to less than half its target, so its beats sit early and the ${MATCH_PAGES}-page window pairs them with the structure's first beats by arithmetic; the pairing says more once the cards are sized or written, and whether a turn is missing is the writer's call, not this reading's`] : []),
|
|
1993
2022
|
...describeComparison(comparison).map((line) => ` - ${line}`),
|
|
1994
2023
|
comparison.unmatched.length
|
|
1995
2024
|
? `beats of the wall no beat of the structure answers: ${comparison.unmatched.map((beat) => `"${beat.headline}" (p. ${beat.page})`).join(", ")}`
|
|
1996
2025
|
: "every beat of the wall answers one of the structure's",
|
|
1997
|
-
...(short ? [`the wall runs to less than half its target, so its beats sit early and the ${MATCH_PAGES}-page window pairs them with the structure's first beats by arithmetic; the pairing says more once the cards are sized or written, and whether a turn is missing is the writer's call, not this reading's`] : []),
|
|
1998
2026
|
beats === 0 ? "No card on this board is marked as a beat (set_rank), so there is nothing to compare; apply_template lays the structure's beats to fill." : "Nothing moved and nothing was made: this is a reading. apply_template lays the beats as cards when the writer wants them.",
|
|
1999
2027
|
];
|
|
2000
2028
|
return ok(lines.join("\n"), { structure: { id: chosen.id, name: chosen.name }, ...comparison });
|
|
@@ -2122,7 +2150,13 @@ server.registerTool(
|
|
|
2122
2150
|
}
|
|
2123
2151
|
const printed = sceneLineCount(args.text);
|
|
2124
2152
|
return ok(
|
|
2125
|
-
`Wrote "${result.headline}": ${printed} line(s) as they print (headings, blank lines and wrapped dialogue counted), measured at ${formatPages(noteEighths(result))} of a 55-line page, rounded to the nearest eighth and never below one eighth${where(live)}.${revisionMark(state, result.id)}${once("heading-from-place", " The heading comes from the card's place and when, so the text starts with the action.")} While the text stands the wall reads the measure, not the estimate${
|
|
2153
|
+
`Wrote "${result.headline}": ${printed} line(s) as they print (headings, blank lines and wrapped dialogue counted), measured at ${formatPages(noteEighths(result))} of a 55-line page, rounded to the nearest eighth and never below one eighth${where(live)}.${revisionMark(state, result.id)}${once("heading-from-place", " The heading comes from the card's place and when, so the text starts with the action.")} While the text stands the wall reads the measure, not the estimate${(() => {
|
|
2154
|
+
// How far the measure sits from what the card was read as before (round eighteen, entry 43): the writer's estimate, or the page an unsized card is read as.
|
|
2155
|
+
const before = result.lengthEighths !== null ? result.lengthEighths : 8;
|
|
2156
|
+
const label = result.lengthEighths !== null ? `the writer's ${formatPages(result.lengthEighths)} pages` : "the page an unsized card is read as";
|
|
2157
|
+
const moved = noteEighths(result) - before;
|
|
2158
|
+
return ` (${label}${moved ? `, so the runtime moved ${formatPages(Math.abs(moved))} ${moved < 0 ? "down" : "up"}` : ""})`;
|
|
2159
|
+
})()}; the estimate is kept for when the text goes, and set_length changes it.${cueReport(state, result.text)}`,
|
|
2126
2160
|
{ ...result, eighths: noteEighths(result), measured: true, printedLines: printed },
|
|
2127
2161
|
);
|
|
2128
2162
|
},
|
|
@@ -2187,7 +2221,7 @@ server.registerTool(
|
|
|
2187
2221
|
if (/^\.(?!\.)/.test(line) && index < ids.length) {
|
|
2188
2222
|
const note = state.notes.find((item) => item.id === ids[index]);
|
|
2189
2223
|
index += 1;
|
|
2190
|
-
const standIn = note && !(note.location ?? "").trim() ? " · no place: the headline stands in for the heading" : "";
|
|
2224
|
+
const standIn = note && !(note.location ?? "").trim() ? `${note.open ? " · open card" : ""} · no place: the headline stands in for the heading${note.open ? ", not a place" : ""}` : "";
|
|
2191
2225
|
const numbered = note && pageNumbers?.get(note.id) ? ` · locked no. ${pageNumbers.get(note.id)}` : "";
|
|
2192
2226
|
const mark = note ? marks.get(note.id) : null;
|
|
2193
2227
|
const sourceLines = (note?.text ?? "").split("\n");
|
|
@@ -2356,7 +2390,7 @@ server.registerTool(
|
|
|
2356
2390
|
const note = order.length > 0 && unwritten === order.length
|
|
2357
2391
|
? [`None of the ${order.length} scenes is written: every scene sets its change line as action, marked [Unwritten], a few lines each — so this is the wall as pages, not a script; the runtime estimate from the cards is about ${formatPages(boardEighths(state))} of ${formatPages(state.targetEighths)} pages.`]
|
|
2358
2392
|
: unwritten
|
|
2359
|
-
? [`${unwritten} of ${order.length} scenes are unwritten and set their change line as action, marked [Unwritten], a few lines each — so this is the script so far, not the runtime: the
|
|
2393
|
+
? [`${unwritten} of ${order.length} scenes are unwritten and set their change line as action, marked [Unwritten], a few lines each — so this is the script so far, not the runtime: the runtime from the cards is about ${formatPages(boardEighths(state))} pages, ${order.length - unwritten} of ${order.length} measured and the rest estimated.`]
|
|
2360
2394
|
: [];
|
|
2361
2395
|
return ok([...note, `pages: ${result.pageCount} of ${Math.round(state.targetEighths / 8)}`, `scene numbers here are ${state.lock ? "the locked numbers" : "story order (not locked)"}`, ...lines].join("\n"), result.scenes);
|
|
2362
2396
|
},
|
|
@@ -2663,8 +2697,8 @@ server.registerTool(
|
|
|
2663
2697
|
const when = result[0]?.when ?? "";
|
|
2664
2698
|
return ok(
|
|
2665
2699
|
when
|
|
2666
|
-
? `${result.length} card(s) now happen ${/^(at|on|in|by|the)\b/i.test(when) ? "" : "at "}"${when}"${where(live)}. The heading prints as ${sceneHeading(result[0]).slice(1)}
|
|
2667
|
-
: `${result.length} card(s) no longer say when they happen${where(live)}
|
|
2700
|
+
? `${result.length} card(s) now happen ${/^(at|on|in|by|the)\b/i.test(when) ? "" : "at "}"${when}"${where(live)}. The heading prints as ${sceneHeading(result[0]).slice(1)}.${stillOpen(result)}`
|
|
2701
|
+
: `${result.length} card(s) no longer say when they happen${where(live)}.${stillOpen(result)}`,
|
|
2668
2702
|
result,
|
|
2669
2703
|
);
|
|
2670
2704
|
},
|
|
@@ -2678,7 +2712,7 @@ server.registerTool(
|
|
|
2678
2712
|
{
|
|
2679
2713
|
title: "Leave a card open",
|
|
2680
2714
|
description:
|
|
2681
|
-
"Mark one or more cards open, with the writer's words for what is not decided — \"
|
|
2715
|
+
"Mark one or more cards open, with the writer's words for what is not decided — \"whether Tom knows\", \"who sent the letter\". An open card is listed by read_wall under its own head, with what it would be asked if closed, and asked nothing of itself while the words stand: not its place, its change line, its arrows, its cast or its fold; a question about the run it sits in — beats back to back, a sag — is still asked, because that is about the story around it. It is still counted, in the order, and a page. open \"\" closes the card and its questions come back on their own. Only on the writer's word — a card is open because they said so, never because you could not decide; where the notes have two versions, ask, and if they say leave it, this is how.",
|
|
2682
2716
|
inputSchema: { ids: z.array(z.string()).min(1), open: z.string() },
|
|
2683
2717
|
},
|
|
2684
2718
|
async (args) => {
|
|
@@ -2906,7 +2940,8 @@ server.registerTool(
|
|
|
2906
2940
|
return { meta, on };
|
|
2907
2941
|
});
|
|
2908
2942
|
const total = parts.reduce((sum, part) => sum + part.on.length, 0);
|
|
2909
|
-
|
|
2943
|
+
// An open card reads as open on a person's page too (round eighteen, entry 53).
|
|
2944
|
+
const where_ = (note) => [note.location ? `at ${note.location}` : "", note.when ? note.when : "", note.rank === "beat" ? "beat" : "", note.open ? `open: ${note.open}` : ""].filter(Boolean).join(" · ");
|
|
2910
2945
|
// A read opens with the door it came through, like every reading (round sixteen, entry 28); one scene a line (29).
|
|
2911
2946
|
const lines = [
|
|
2912
2947
|
`PlotCoder cast (${door(live, base)})`,
|
|
@@ -2993,7 +3028,7 @@ server.registerTool(
|
|
|
2993
3028
|
: [];
|
|
2994
3029
|
const warn = near.length ? ` The wall also has ${near.map((other) => `"${other}"`).join(", ")} — the same place spelled twice, or two places? Each distinct phrase counts as one place.` : "";
|
|
2995
3030
|
return ok(
|
|
2996
|
-
`${result.length} card(s) now ${place ? `at ${place}` : "nowhere"}${where(live)}.${warn}`,
|
|
3031
|
+
`${result.length} card(s) now ${place ? `at ${place}` : "nowhere"}${where(live)}.${warn}${stillOpen(result)}`,
|
|
2997
3032
|
result,
|
|
2998
3033
|
);
|
|
2999
3034
|
},
|
|
@@ -3065,7 +3100,7 @@ server.registerTool(
|
|
|
3065
3100
|
(id) => state.characters.find((character) => character.id === id)?.name ?? id,
|
|
3066
3101
|
);
|
|
3067
3102
|
return ok(
|
|
3068
|
-
`${result.length} card(s) now cast ${names.length ? names.join(", ") : "nobody"}: ${result.map((note) => `"${note.headline}"`).join(", ")}${added.length ? ` (added to the cast: ${added.join(", ")})` : ""}${where(live)}
|
|
3103
|
+
`${result.length} card(s) now cast ${names.length ? names.join(", ") : "nobody"}: ${result.map((note) => `"${note.headline}"`).join(", ")}${added.length ? ` (added to the cast: ${added.join(", ")})` : ""}${where(live)}.${stillOpen(result)}`,
|
|
3069
3104
|
result,
|
|
3070
3105
|
);
|
|
3071
3106
|
},
|
|
@@ -3291,7 +3326,7 @@ server.registerTool(
|
|
|
3291
3326
|
{
|
|
3292
3327
|
title: "Set the project's premise",
|
|
3293
3328
|
description:
|
|
3294
|
-
"Set the project's premise: the line above every board's logline, held by the project whatever its board count — what a series is about, or what is true before a film starts ('the
|
|
3329
|
+
"Set the project's premise: the line above every board's logline, held by the project whatever its board count — what a series is about, or what is true before a film starts ('the winter the shop closes'). An empty string clears it. Boards keep their own loglines.",
|
|
3295
3330
|
inputSchema: { premise: z.string() },
|
|
3296
3331
|
},
|
|
3297
3332
|
async (args) => {
|
|
@@ -3615,7 +3650,7 @@ server.registerTool(
|
|
|
3615
3650
|
{
|
|
3616
3651
|
title: "Save the project as a file",
|
|
3617
3652
|
description:
|
|
3618
|
-
"The project the server is working, as the file Save project writes and Open project takes: the record, every board with its cards, the reminders and the writer's structures. Pass path to write it (a .json) — an absolute path, since a relative one resolves from the server
|
|
3653
|
+
"The project the server is working, as the file Save project writes and Open project takes: the record, every board with its cards, the reminders and the writer's structures. Pass path to write it (a .json) — an absolute path, since a relative one resolves from the folder the server was started in, not yours; without a path, the reply's JSON is the file. Pictures and takes on the account are not in the file. Works through every door.",
|
|
3619
3654
|
inputSchema: { path: z.string().optional() },
|
|
3620
3655
|
},
|
|
3621
3656
|
async (args) => {
|
package/src/board/fountain.js
CHANGED
|
@@ -123,6 +123,7 @@ export function toFountain(state, options = {}) {
|
|
|
123
123
|
const cast = (note.characterIds ?? []).map((id) => nameOf.get(id)).filter(Boolean);
|
|
124
124
|
if (cast.length) marks.push(`with ${cast.join(", ")}`);
|
|
125
125
|
if (note.plants) marks.push("plants something to pay off later");
|
|
126
|
+
if (note.open) marks.push(`open: ${note.open}`);
|
|
126
127
|
if (revisionMarksFor(marks, note)) marks.push(`changed in the ${state.revision.color} revision`);
|
|
127
128
|
if (marks.length) {
|
|
128
129
|
body.push(`[[${marks.join(" · ")}]]`);
|
package/src/board/readWall.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export type WallReading = {
|
|
|
68
68
|
/** Folded cards that pay off on another board of the project (R50): the card, the board, and the scene there that claims it (R58) or null while the board is a promise. */
|
|
69
69
|
later: { id: string; boardId: string; noteId: string | null }[];
|
|
70
70
|
/** Open cards (R59): the writer's words for what is not decided, in story order; not asked about while they stand. */
|
|
71
|
-
open: Array<{ id: string; words: string }>;
|
|
71
|
+
open: Array<{ id: string; words: string; hides: FindingKind[] }>;
|
|
72
72
|
/** Cards here that pay off a fold of another board (R58), composed by the door from the project. */
|
|
73
73
|
paidBy: Array<{ id: string; fromBoardId: string; fromBoardName: string; fromNoteId: string; fromHeadline: string; fromColor: string }>;
|
|
74
74
|
/** The questions the wall asks now. A left one (R53) is not here while its words hold. */
|
package/src/board/readWall.js
CHANGED
|
@@ -262,6 +262,10 @@ export function readWall(state, options = {}) {
|
|
|
262
262
|
// whose turns come thick at the end would otherwise ask the same sentence
|
|
263
263
|
// six times (round four, finding 19).
|
|
264
264
|
let chain = [];
|
|
265
|
+
// On a wall where most runs hold nothing, the turns are back to back by
|
|
266
|
+
// construction, and the question stands until scenes go in; say so rather
|
|
267
|
+
// than ask it as if it were a choice (round eighteen, entry 29).
|
|
268
|
+
const thin = between.length >= 2 && between.filter((run) => run.cards === 0).length * 2 > between.length ? " Most runs hold nothing yet, so this stands until scenes go in." : "";
|
|
265
269
|
const askChain = () => {
|
|
266
270
|
if (chain.length === 0) return;
|
|
267
271
|
const ids = [chain[0].from, ...chain.map((run) => run.to)];
|
|
@@ -271,8 +275,8 @@ export function readWall(state, options = {}) {
|
|
|
271
275
|
ids,
|
|
272
276
|
text:
|
|
273
277
|
ids.length === 2
|
|
274
|
-
? `Nothing runs between ${names[0]} and ${names[1]}: two turns back to back. Are they one beat, or is a scene missing
|
|
275
|
-
: `Nothing runs between ${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}: ${countWord(ids.length)} turns back to back. Are some of them one beat, or are scenes missing between them
|
|
278
|
+
? `Nothing runs between ${names[0]} and ${names[1]}: two turns back to back. Are they one beat, or is a scene missing?${thin}`
|
|
279
|
+
: `Nothing runs between ${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}: ${countWord(ids.length)} turns back to back. Are some of them one beat, or are scenes missing between them?${thin}`,
|
|
276
280
|
});
|
|
277
281
|
chain = [];
|
|
278
282
|
};
|
|
@@ -330,7 +334,7 @@ export function readWall(state, options = {}) {
|
|
|
330
334
|
findings.push({
|
|
331
335
|
kind: "unlinked",
|
|
332
336
|
ids: loose.map((note) => note.id),
|
|
333
|
-
text: `${loose.length === 1 ? "One card has" : `${loose.length} cards have`} no arrow in or out: ${list(loose)}. What
|
|
337
|
+
text: `${loose.length === 1 ? "One card has" : `${loose.length} cards have`} no arrow in or out: ${list(loose)}. What comes before ${loose.length === 1 ? "it" : "them"} in the story, and what after?`,
|
|
334
338
|
});
|
|
335
339
|
}
|
|
336
340
|
}
|
|
@@ -524,12 +528,28 @@ export function readWall(state, options = {}) {
|
|
|
524
528
|
payoffs,
|
|
525
529
|
later,
|
|
526
530
|
paidBy: paidHere,
|
|
527
|
-
open:
|
|
531
|
+
open: describeOpen(state, options, order, openIds),
|
|
528
532
|
findings: asked,
|
|
529
533
|
left,
|
|
530
534
|
};
|
|
531
535
|
}
|
|
532
536
|
|
|
537
|
+
/**
|
|
538
|
+
* The open cards, each with the kinds of question it would be asked if it were
|
|
539
|
+
* closed (round eighteen, entry 27): the reading of the same wall with the
|
|
540
|
+
* words cleared, read once more, so the writer can see what the words hide.
|
|
541
|
+
*/
|
|
542
|
+
function describeOpen(state, options, order, openIds) {
|
|
543
|
+
if (!openIds.size) return [];
|
|
544
|
+
const hides = new Map();
|
|
545
|
+
if (!options.closedReading) {
|
|
546
|
+
const closed = { ...state, notes: state.notes.map((note) => (openIds.has(note.id) ? { ...note, open: "" } : note)) };
|
|
547
|
+
const again = readWall(closed, { ...options, closedReading: true });
|
|
548
|
+
for (const finding of again.findings) for (const id of finding.ids) if (openIds.has(id)) (hides.get(id) ?? hides.set(id, new Set()).get(id)).add(finding.kind);
|
|
549
|
+
}
|
|
550
|
+
return order.filter((note) => openIds.has(note.id)).map((note) => ({ id: note.id, words: note.open.trim(), hides: [...(hides.get(note.id) ?? [])] }));
|
|
551
|
+
}
|
|
552
|
+
|
|
533
553
|
function sameList(a, b) {
|
|
534
554
|
return a.length === b.length && a.every((id, index) => id === b[index]);
|
|
535
555
|
}
|