plotcoder-board 0.1.36 → 0.1.37
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 +2 -2
- package/package.json +1 -1
- package/scripts/plotcoder-mcp-server.mjs +13 -6
- package/src/board/agents.js +1 -1
- package/src/board/fountain.js +11 -2
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ An agent should call the tools, never fake mouse drags. The skill in `.cursor/sk
|
|
|
53
53
|
A **blind run** is a fresh agent given the on-ramp and a treatment and nothing
|
|
54
54
|
else, asked to build a wall and to keep a log of everything that made the job
|
|
55
55
|
harder than it should have been. The friction log is the product; the wall is
|
|
56
|
-
just what produces it. Twenty rounds have been run, the first three through a
|
|
56
|
+
just what produces it. Twenty-one rounds have been run, the first three through a
|
|
57
57
|
repo checkout and the rest through the account door; every finding from the
|
|
58
58
|
first seventeen is fixed or decided. Round seventeen, the first to start from
|
|
59
59
|
a page of notes instead of a treatment, found the wall has no way to hold a
|
|
@@ -107,4 +107,4 @@ next round into a test of `claim_account` instead of the door it meant to test.
|
|
|
107
107
|
|
|
108
108
|
## Status
|
|
109
109
|
|
|
110
|
-
Version 0.1.
|
|
110
|
+
Version 0.1.37. 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
|
@@ -982,7 +982,7 @@ const CHECK_WORDS = {
|
|
|
982
982
|
sag: "no run out of proportion",
|
|
983
983
|
empty: "no beats back to back",
|
|
984
984
|
unwritten: "no card without a headline or change line",
|
|
985
|
-
unlinked: "no card without
|
|
985
|
+
unlinked: "no card without a follows arrow (a setup arrow is a claim, not a place in the story)",
|
|
986
986
|
duplicate: "no two headlines alike",
|
|
987
987
|
sequence: "no group too long for one sequence (act groups are not asked)",
|
|
988
988
|
uncast: "nobody in the cast on no card of the project",
|
|
@@ -1345,11 +1345,13 @@ server.registerTool(
|
|
|
1345
1345
|
rank: rankSchema.optional(),
|
|
1346
1346
|
pages: pagesSchema.optional(),
|
|
1347
1347
|
plants: z.boolean().optional(),
|
|
1348
|
-
plantsWhat: z.string().optional(),
|
|
1348
|
+
plantsWhat: z.string().optional().describe("What the folded corner plants, in the writer's words; naming it folds the card."),
|
|
1349
1349
|
after: z.string().optional().describe("Wire the new scene into the story after this card (id or headline): one call, one number under a lock. On a wall with no follows arrows yet this draws the first, so a wall can be built in order from its second card."),
|
|
1350
1350
|
before: z.string().optional().describe("Or before this card (id or headline)."),
|
|
1351
1351
|
location: z.string().optional(),
|
|
1352
1352
|
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.'),
|
|
1353
|
+
locationOpen: z.string().optional().describe("The writer's words for why the place is not decided (R61's edge): the card is born with its place open, listed and not asked where, while its other questions stand."),
|
|
1354
|
+
whenOpen: z.string().optional().describe("The writer's words for why the when is not decided: the card is born with its when open, listed and not asked."),
|
|
1353
1355
|
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."),
|
|
1354
1356
|
characters: z.array(z.string().min(1)).optional(),
|
|
1355
1357
|
x: z.number().optional(),
|
|
@@ -1389,6 +1391,8 @@ server.registerTool(
|
|
|
1389
1391
|
plants: args.plants,
|
|
1390
1392
|
plantsWhat: args.plantsWhat,
|
|
1391
1393
|
location: args.location,
|
|
1394
|
+
locationOpen: args.locationOpen,
|
|
1395
|
+
whenOpen: args.whenOpen,
|
|
1392
1396
|
when: args.when,
|
|
1393
1397
|
open: args.open,
|
|
1394
1398
|
x: landing.x,
|
|
@@ -1433,8 +1437,8 @@ server.registerTool(
|
|
|
1433
1437
|
result?.lengthEighths === null ? "about a page (unsized: the writer's guess until set_length)" : `${formatPages(noteEighths(result))} ${formatPages(noteEighths(result)) === "1" ? "page" : "pages"}`,
|
|
1434
1438
|
result?.color ? `${result.color} paper${args.color ? "" : once("paper", " (pass color to choose)")}` : null,
|
|
1435
1439
|
result?.plants ? `corner folded${result.plantsWhat ? ` — plants ${result.plantsWhat}` : ""}` : null,
|
|
1436
|
-
result?.location ? `at ${result.location}` : `no place yet${once("place", " (location here, or set_location)")}`,
|
|
1437
|
-
result?.when ? `when: ${result.when}` : null,
|
|
1440
|
+
result?.location ? `at ${result.location}` : result?.locationOpen ? `place open: "${result.locationOpen}" (listed, not asked where)` : `no place yet${once("place", " (location here, or set_location)")}`,
|
|
1441
|
+
result?.when ? `when: ${result.when}` : result?.whenOpen ? `when open: "${result.whenOpen}" (listed, not asked)` : null,
|
|
1438
1442
|
result?.open ? `open: "${result.open}" (listed, not asked about)` : null,
|
|
1439
1443
|
].filter(Boolean).join(", ");
|
|
1440
1444
|
// Where it landed matters only until the tidy, so the reply says the rule once and never the coordinates (round fourteen, entry 11).
|
|
@@ -1669,7 +1673,10 @@ server.registerTool(
|
|
|
1669
1673
|
if (kind === "sequence" && state.groups.length === 0) return "no group too long for one sequence (not asked: no groups)";
|
|
1670
1674
|
// A kind clean only because an open card is not asked says so (round eighteen, entry 18).
|
|
1671
1675
|
const hiddenBy = reading.open.filter((item) => item.hides.includes(kind)).length;
|
|
1672
|
-
|
|
1676
|
+
// A place left open is not an open card (round twenty-one, entry 26): say which.
|
|
1677
|
+
const placesOpen = kind === "unplaced" ? reading.openFields.filter((field) => field.field === "location").length : 0;
|
|
1678
|
+
const except = [hiddenBy ? `${hiddenBy} open card${hiddenBy === 1 ? "" : "s"}` : "", placesOpen ? `${placesOpen} with ${placesOpen === 1 ? "its" : "their"} place open` : ""].filter(Boolean).join(" and ");
|
|
1679
|
+
if (except) return `${CHECK_WORDS[kind]} (except ${except}, not asked)`;
|
|
1673
1680
|
return CHECK_WORDS[kind];
|
|
1674
1681
|
}).join("; ") || "(nothing — every check found something)"}`,
|
|
1675
1682
|
];
|
|
@@ -2286,7 +2293,7 @@ server.registerTool(
|
|
|
2286
2293
|
if (/^\.(?!\.)/.test(line) && index < ids.length) {
|
|
2287
2294
|
const note = state.notes.find((item) => item.id === ids[index]);
|
|
2288
2295
|
index += 1;
|
|
2289
|
-
const standIn = note && !(note.location ?? "").trim() ? `${note.open ? " · open card" : ""} · no place: the headline stands in for the heading${note.open ? ", not a place" : ""}` : "";
|
|
2296
|
+
const standIn = note && !(note.location ?? "").trim() ? ((note.locationOpen ?? "").trim() ? ` · place open: the writer's words head the scene, marked, not a place` : `${note.open ? " · open card" : ""} · no place: the headline stands in for the heading${note.open ? ", not a place" : ""}`) : "";
|
|
2290
2297
|
const numbered = note && pageNumbers?.get(note.id) ? ` · locked no. ${pageNumbers.get(note.id)}` : "";
|
|
2291
2298
|
const mark = note ? marks.get(note.id) : null;
|
|
2292
2299
|
const sourceLines = (note?.text ?? "").split("\n");
|
package/src/board/agents.js
CHANGED
|
@@ -45,7 +45,7 @@ export const AGENTS = {
|
|
|
45
45
|
firstNote: "Make these five before anything else; none depends on another, so any order is fine. list_words and list_workflows are the app's and read no project, and their first line says so; every other reading opens with the door it came through — the project it read and how many the account holds — and a write's tail says where it landed; list_projects lists the projects. read_wall, list_reminders and list_board are about the wall you will work, so after open_project or open_board make those three again, and after new_project read the wall once it holds cards. An emptied account has nothing to read: go straight to new_project: a name is enough; pages sets the target and board names the first board when the writer has them, and set_target and rename_board set them later. On an account with no project yet, read_wall has nothing to read and says so, and list_reminders gives the house principles every project starts with. No server in front of you, and no shell to take the shell door? Nothing gets you in from inside the session: say so, and ask the person to wire the server and start a new session.",
|
|
46
46
|
first: [
|
|
47
47
|
{ tool: "list_words", why: "the room's words, the app's meaning." },
|
|
48
|
-
{ tool: "read_wall", why: "the reading: the beats, the runs, the setups, and what the wall asks. The records — every card, the cast, the places, the rows — are list_board's. A fresh folder holds a sample wall (Maya, Tom, the letter) and the reading says so only when it is the sample; it is not the writer's." },
|
|
48
|
+
{ tool: "read_wall", why: "the reading: the beats, the runs, the setups, and what the wall asks. The records — every card, the cast, the places, the rows — are list_board's. A fresh folder holds a sample wall (Maya, Tom, the letter) and the reading says so only when it is the sample; it is not the writer's. An account can hold a wall from earlier work that is neither the sample nor what the writer means to work now: every reading names the project it read and list_projects names who is on it; ask the writer before touching it, and new_project starts theirs beside it." },
|
|
49
49
|
{ tool: "list_workflows", why: "what a writer can ask you for." },
|
|
50
50
|
{ tool: "list_reminders", why: "the house principles the app starts with, and the writer's own; read them before you change anything. Reminders live on the project and go with it." },
|
|
51
51
|
{ tool: "list_board", why: "the records — every card, the cast, the places, the rows — with their ids, which the reading does not carry and every write needs." },
|
package/src/board/fountain.js
CHANGED
|
@@ -24,10 +24,16 @@ function upper(text) {
|
|
|
24
24
|
* A forced scene heading: the place, or the headline when the card has none,
|
|
25
25
|
* and the card's when after a dash — THE PIER AT FENIT - NIGHT (R55).
|
|
26
26
|
*/
|
|
27
|
+
/** The heading's mark for a place the writer has left open (R61's edge): the words print, and never read back as a place. */
|
|
28
|
+
export const OPEN_PLACE_HEADING = "PLACE NOT DECIDED:";
|
|
29
|
+
|
|
27
30
|
export function sceneHeading(note) {
|
|
28
31
|
const place = typeof note.location === "string" ? note.location.trim() : "";
|
|
32
|
+
const placeOpen = typeof note.locationOpen === "string" ? note.locationOpen.trim() : "";
|
|
29
33
|
const when = typeof note.when === "string" ? note.when.trim() : "";
|
|
30
|
-
|
|
34
|
+
// No place but the writer's words for why: the words head the scene, marked,
|
|
35
|
+
// so a reader never takes them for a place and the headline is not a slugline.
|
|
36
|
+
const words = place || (placeOpen ? `${OPEN_PLACE_HEADING} ${placeOpen}` : "") || note.headline || "UNTITLED";
|
|
31
37
|
return `.${upper(words)}${when ? ` - ${upper(when)}` : ""}`;
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -266,6 +272,8 @@ export function mergeFountain(state, parsed) {
|
|
|
266
272
|
const headline = scene.synopsis || titleCase(scene.heading);
|
|
267
273
|
const isPlace = scene.forced && Boolean(scene.synopsis);
|
|
268
274
|
const parts = splitHeading(scene.heading);
|
|
275
|
+
// A heading that says the place is not decided comes back as an open place, not a place named that.
|
|
276
|
+
const openPlace = parts.place.toUpperCase().startsWith(OPEN_PLACE_HEADING) ? parts.place.slice(OPEN_PLACE_HEADING.length).trim().toLowerCase() : "";
|
|
269
277
|
const id = `scene-${Math.random().toString(36).slice(2, 8)}`;
|
|
270
278
|
// A marked body is an unwritten scene: its words are the change line, not a page.
|
|
271
279
|
const body = unmark(scene.text);
|
|
@@ -274,7 +282,8 @@ export function mergeFountain(state, parsed) {
|
|
|
274
282
|
id,
|
|
275
283
|
headline,
|
|
276
284
|
change: body.marked ? body.text || "What changes?" : scene.text ? firstSentence(scene.text) : "What changes?",
|
|
277
|
-
location: isPlace ? titleCase(parts.place) : "",
|
|
285
|
+
location: isPlace && !openPlace ? titleCase(parts.place) : "",
|
|
286
|
+
locationOpen: isPlace && openPlace ? openPlace : "",
|
|
278
287
|
when: isPlace ? parts.when.toLowerCase() : "",
|
|
279
288
|
text: body.marked ? "" : scene.text,
|
|
280
289
|
x: anchor ? anchor.x + 40 : 140,
|