plotcoder-board 0.1.8 → 0.1.10
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/package.json
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
formatPages,
|
|
30
30
|
isBoardState,
|
|
31
31
|
isMeasured,
|
|
32
|
+
NOTE_HEIGHT,
|
|
33
|
+
NOTE_WIDTH,
|
|
32
34
|
DEFAULT_TARGET_EIGHTHS,
|
|
33
35
|
normalizeState,
|
|
34
36
|
noteEighths,
|
|
@@ -49,7 +51,7 @@ import { segmentBrief, WORKFLOWS } from "../src/board/workflows.js";
|
|
|
49
51
|
import { DEFAULT_REMINDERS, titleFromBody } from "../src/board/reminders.js";
|
|
50
52
|
import crypto from "node:crypto";
|
|
51
53
|
import { describeRuns, describeSetups, readWall } from "../src/board/readWall.js";
|
|
52
|
-
import { organizePoses } from "../src/board/organize.js";
|
|
54
|
+
import { GAP, ROW_WIDTH, organizePoses } from "../src/board/organize.js";
|
|
53
55
|
import {
|
|
54
56
|
addBoard,
|
|
55
57
|
addStructure,
|
|
@@ -737,7 +739,16 @@ async function commit(command) {
|
|
|
737
739
|
}
|
|
738
740
|
|
|
739
741
|
/** Said once per session: that cards stack until organize (round seven, finding 11). */
|
|
740
|
-
|
|
742
|
+
/** Where a new card lands when the agent gives no position: after the last card in reading order, wrapping five wide, so cards never stack (round eleven, finding 14). */
|
|
743
|
+
function nextPlace(state) {
|
|
744
|
+
const order = readingOrder(state.notes);
|
|
745
|
+
const last = order[order.length - 1];
|
|
746
|
+
if (!last) return { x: 140, y: 140 };
|
|
747
|
+
const originX = Math.min(...state.notes.map((note) => note.x));
|
|
748
|
+
const x = last.x + NOTE_WIDTH + GAP;
|
|
749
|
+
if (x + NOTE_WIDTH > originX + ROW_WIDTH) return { x: originX, y: last.y + NOTE_HEIGHT + GAP };
|
|
750
|
+
return { x, y: last.y };
|
|
751
|
+
}
|
|
741
752
|
|
|
742
753
|
/** Which door a read came through, for the head of a reply: the account as whom, the open app, or the file at which path. */
|
|
743
754
|
function door(live, base = null) {
|
|
@@ -857,7 +868,7 @@ function summarize(state) {
|
|
|
857
868
|
`notes: ${state.notes.length}, groups: ${state.groups.length}, arrows: ${state.arrows.length}, cast: ${state.characters.length}`,
|
|
858
869
|
"cast:",
|
|
859
870
|
cast || " (no one yet — add_character to start the roster)",
|
|
860
|
-
"places (each
|
|
871
|
+
"places (each phrase is its own place, and the app relates none of them — if two are one place, set_location them the same):",
|
|
861
872
|
places || " (no card says where it happens yet)",
|
|
862
873
|
"cards:",
|
|
863
874
|
notes || " (no cards)",
|
|
@@ -884,15 +895,23 @@ const server = new McpServer({ name: "plotcoder-board", version: "0.1.0" });
|
|
|
884
895
|
|
|
885
896
|
// A door's answer is a reply, not an error: a shut account door, or an
|
|
886
897
|
// account with no project yet, says so in words from every tool alike.
|
|
898
|
+
// Tool calls run one at a time. Every tool reads the board, decides, and
|
|
899
|
+
// writes it back; two calls interleaving at those awaits would each read the
|
|
900
|
+
// same board and the second would write over the first — thirteen parallel
|
|
901
|
+
// create_note calls naming Nessa made thirteen Nessas in prospect (round
|
|
902
|
+
// eleven, finding 22). One lane, in the order the calls arrive.
|
|
887
903
|
const registerTool = server.registerTool.bind(server);
|
|
904
|
+
let lane = Promise.resolve();
|
|
888
905
|
server.registerTool = (name, config, handler) =>
|
|
889
|
-
registerTool(name, config,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
906
|
+
registerTool(name, config, (...args) => {
|
|
907
|
+
const turn = lane.then(() =>
|
|
908
|
+
handler(...args).catch((error) => {
|
|
909
|
+
if (error instanceof DoorReply) return ok(error.message);
|
|
910
|
+
throw error;
|
|
911
|
+
}),
|
|
912
|
+
);
|
|
913
|
+
lane = turn.catch(() => undefined);
|
|
914
|
+
return turn;
|
|
896
915
|
});
|
|
897
916
|
|
|
898
917
|
server.registerTool(
|
|
@@ -1028,6 +1047,7 @@ server.registerTool(
|
|
|
1028
1047
|
},
|
|
1029
1048
|
},
|
|
1030
1049
|
async (args) => {
|
|
1050
|
+
const landing = args.x === undefined && args.y === undefined ? nextPlace((await readBoard()).state) : { x: args.x, y: args.y };
|
|
1031
1051
|
let { result, live } = await commit({
|
|
1032
1052
|
type: "create_note",
|
|
1033
1053
|
headline: args.headline,
|
|
@@ -1041,8 +1061,8 @@ server.registerTool(
|
|
|
1041
1061
|
lengthEighths: args.pages === undefined ? undefined : toEighths(args.pages),
|
|
1042
1062
|
plants: args.plants,
|
|
1043
1063
|
location: args.location,
|
|
1044
|
-
x:
|
|
1045
|
-
y:
|
|
1064
|
+
x: landing.x,
|
|
1065
|
+
y: landing.y,
|
|
1046
1066
|
});
|
|
1047
1067
|
let castLine = "";
|
|
1048
1068
|
if (args.characters && args.characters.length && result?.id) {
|
|
@@ -1074,8 +1094,7 @@ server.registerTool(
|
|
|
1074
1094
|
result?.plants ? "corner folded" : null,
|
|
1075
1095
|
result?.location ? `at ${result.location}` : "no place yet (location here, or set_location)",
|
|
1076
1096
|
].filter(Boolean).join(", ");
|
|
1077
|
-
const placed = args.x === undefined && args.y === undefined
|
|
1078
|
-
if (placed) saidStack = true;
|
|
1097
|
+
const placed = args.x === undefined && args.y === undefined ? ` Placed after the last card in reading order (${Math.round(landing.x)},${Math.round(landing.y)}); organize lays the wall out along the arrows.` : "";
|
|
1079
1098
|
return ok(`Created card ${result?.id ?? ""}: ${landed}${where(live)}.${castLine}${placed}`, result);
|
|
1080
1099
|
},
|
|
1081
1100
|
);
|
|
@@ -1176,6 +1195,7 @@ server.registerTool(
|
|
|
1176
1195
|
const lines = [
|
|
1177
1196
|
`PlotCoder wall (${door(live, base)})`,
|
|
1178
1197
|
`logline: ${state.logline ? `"${state.logline}"` : "(none yet)"}`,
|
|
1198
|
+
"the cast and the places are list_board's, not the reading's",
|
|
1179
1199
|
state.targetEighths === DEFAULT_TARGET_EIGHTHS
|
|
1180
1200
|
? `runtime: about ${formatPages(boardEighths(state))} pages; no target set (set_target)`
|
|
1181
1201
|
: `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"}`,
|
|
@@ -1206,7 +1226,13 @@ server.registerTool(
|
|
|
1206
1226
|
...(reading.findings.length
|
|
1207
1227
|
? reading.findings.map((finding) => ` - [${finding.kind}] ${finding.text}${finding.ids.length ? ` (ids: ${finding.ids.join(", ")})` : ""}`)
|
|
1208
1228
|
: [" (none that this reading can see)"]),
|
|
1209
|
-
`checks: ${CHECKS.length} run —
|
|
1229
|
+
`checks: ${CHECKS.length} run — ${(() => {
|
|
1230
|
+
const asked = reading.findings.filter((finding) => CHECKS.includes(finding.kind));
|
|
1231
|
+
if (asked.length === 0) return "asking nothing";
|
|
1232
|
+
const counts = new Map();
|
|
1233
|
+
for (const finding of asked) counts.set(finding.kind, (counts.get(finding.kind) ?? 0) + 1);
|
|
1234
|
+
return `asking ${asked.length} question${asked.length === 1 ? "" : "s"} of ${counts.size} kind${counts.size === 1 ? "" : "s"}: ${[...counts.entries()].map(([kind, n]) => (n > 1 ? `${kind} ×${n}` : kind)).join(", ")}`;
|
|
1235
|
+
})()}; checked and clean: ${CHECKS.filter((kind) => !reading.findings.some((finding) => finding.kind === kind)).map((kind) => CHECK_WORDS[kind]).join("; ") || "(nothing — every check found something)"}`,
|
|
1210
1236
|
];
|
|
1211
1237
|
if (isSampleWall(state)) lines.unshift(SAMPLE_NOTE);
|
|
1212
1238
|
return ok(lines.join("\n"), { ...reading, sample: isSampleWall(state) });
|
|
@@ -1315,7 +1341,7 @@ server.registerTool(
|
|
|
1315
1341
|
else if (note && !wrappedUnder.some((item) => item.beat === currentBeat)) wrappedUnder.push({ beat: currentBeat, first: note });
|
|
1316
1342
|
}
|
|
1317
1343
|
const shape = beats
|
|
1318
|
-
? `${opening ? `an opening row of ${opening} card(s) before the first beat, then ` : ""}${beats} row(s), one per beat${wrappedUnder.length ? `; ${wrappedUnder.map((item) => `the row of "${item.beat.headline}" wraps under from "${item.first.headline}"`).join(", ")}` : ""}`
|
|
1344
|
+
? `${opening ? `an opening row of ${opening} card(s) before the first beat, then ` : ""}${beats} row(s), one per beat${wrappedUnder.length ? `; ${wrappedUnder.map((item) => `the row of "${item.beat.headline}" wraps under from "${item.first.headline}", indented under the row's first scene, never under the beat`).join(", ")}` : ""}`
|
|
1319
1345
|
: `${rows} row(s) five cards wide — no beats yet, so nothing sets the rows; set_rank the turns and organize again for a row per beat`;
|
|
1320
1346
|
return ok(`Organized ${poses.length} card(s) along the arrows into ${shape}${where(live)}.`, poses);
|
|
1321
1347
|
},
|
|
@@ -1537,7 +1563,9 @@ server.registerTool(
|
|
|
1537
1563
|
"The workflows — the app's own, the same on every project; no project was read.\n" +
|
|
1538
1564
|
WORKFLOWS.map(
|
|
1539
1565
|
(workflow) =>
|
|
1540
|
-
`- ${workflow.id} — ${workflow.name}\n ask: "${workflow.ask}"\n tools: ${workflow.tools.join(", ")}\n keep: ${workflow.then}
|
|
1566
|
+
`- ${workflow.id} — ${workflow.name}\n ask: "${workflow.ask}"\n tools: ${workflow.tools.join(", ")}\n keep: ${workflow.then}${
|
|
1567
|
+
workflow.needs ? `\n the treatment should say (ask the writer for what it leaves open; invent none of it):\n${workflow.needs.map((need) => ` - ${need.ask} → ${need.tool}`).join("\n")}` : ""
|
|
1568
|
+
}`,
|
|
1541
1569
|
).join("\n"),
|
|
1542
1570
|
WORKFLOWS,
|
|
1543
1571
|
),
|
|
@@ -2021,7 +2049,7 @@ server.registerTool(
|
|
|
2021
2049
|
if (!result) return ok(`No character with id ${person.id}. Call list_board for the cast.`);
|
|
2022
2050
|
return ok(`Nothing changed on ${result.name}'s page: those lines already read that way.`, result);
|
|
2023
2051
|
}
|
|
2024
|
-
const trim = (text) => (text.length >
|
|
2052
|
+
const trim = (text) => (text.length > 400 ? `${text.slice(0, 140)}… (${text.length} characters in all, every one landed)` : text);
|
|
2025
2053
|
const lines = Object.keys(patch).map((field) => {
|
|
2026
2054
|
const had = (person[field] ?? "").trim();
|
|
2027
2055
|
const now = (result[field] ?? "").trim();
|
|
@@ -2528,7 +2556,8 @@ server.registerTool(
|
|
|
2528
2556
|
workingProject(record.id, record.name, (account.projectCount ?? 0) + 1);
|
|
2529
2557
|
joinPresence(record.id);
|
|
2530
2558
|
const targetLine = target === undefined ? ` Its target is ${formatPages(state.targetEighths)} pages, the default for a feature; set_target for a pilot or a half-hour, or pass pages or minutes here.` : ` Its target is ${formatPages(state.targetEighths)} pages.`;
|
|
2531
|
-
|
|
2559
|
+
const first = record.boards[0];
|
|
2560
|
+
return ok(`Started "${record.name}" (${record.id}) with its first board "${first.name}" (${first.id}), and working it now, as ${account.email}.${targetLine}${oneCallHint(record)}`, { id: record.id, name: record.name, boardId: first.id, boardName: first.name, targetEighths: state.targetEighths });
|
|
2532
2561
|
},
|
|
2533
2562
|
);
|
|
2534
2563
|
|
package/src/board/agents.js
CHANGED
|
@@ -51,7 +51,7 @@ export const AGENTS = {
|
|
|
51
51
|
],
|
|
52
52
|
rules: [
|
|
53
53
|
"Questions, not fixes, until the writer says.",
|
|
54
|
-
"No opinions about how many beats there should be.",
|
|
54
|
+
"No opinions about how many beats there should be. Marking the turns a treatment plainly makes is reading it, not an opinion: mark them, say which, and let the writer strike or add.",
|
|
55
55
|
"Page counts are estimates.",
|
|
56
56
|
"Ask before delete_board, delete_project, empty_account, delete_account, unlock_numbers, remove_file, an import_project that replaces, or claim_account — the writer gives the email and the password; never invent one. export_project first, when something might be wanted back.",
|
|
57
57
|
"Do not invent people or a logline. What the treatment states — an age, a job, a bad knee — is not invented: it goes in the person's notes. An unnamed person is named by their role — Dana's mother, the dispatcher — which is a name until the writer gives one. A scene is one place and one stretch of time; a new place or time is a new card. A beat is a whole card; a setup arrow lands on the scene's card, so a payoff never needs a card of its own. Acts are groups titled Act one, Act two, when the treatment has them; the wall never asks whether an act is a sequence. Paper colour means nothing to the app. Under target is a fact to report plainly, like over; neither is a verdict.",
|
package/src/board/workflows.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type Workflow = {
|
|
|
11
11
|
tools: string[];
|
|
12
12
|
/** The rule the agent keeps while doing it. */
|
|
13
13
|
then: string;
|
|
14
|
+
/** What the writer's material should say for the workflow to need no questions back, and the tool each answer lands in (R49). */
|
|
15
|
+
needs?: { ask: string; tool: string }[];
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
export declare const WORKFLOWS: readonly Workflow[];
|
package/src/board/workflows.js
CHANGED
|
@@ -17,6 +17,24 @@ export const WORKFLOWS = [
|
|
|
17
17
|
ask: "Here is a treatment. Break it into a wall: one card per scene with a headline and what changes, the cast on each card, the places, and the major turns marked as beats.",
|
|
18
18
|
tools: ["list_words", "read_wall", "list_reminders", "new_project", "new_board", "rename_project", "set_target", "create_note", "list_board", "add_character", "cast", "update_character", "set_location", "set_rank", "set_length", "set_plant", "create_arrow", "create_group", "organize"],
|
|
19
19
|
then: "Start where the wall will live: on the account, new_project names it; in a folder, new_board for the writer's wall, then rename_project. Read the wall (read_wall) and say what it asks. A treatment is cards, one create_note each, with characters, location, rank and plants on the call; import_fountain is the door for pages, not a treatment — a scene's text measures its card.",
|
|
20
|
+
// What a treatment should say (R49): eleven blind runs asked the writer
|
|
21
|
+
// the same questions at the end of every build. Each is a fact the wall
|
|
22
|
+
// needs and the treatment could carry, and the tool it lands in. A writer
|
|
23
|
+
// who answers them in the treatment gets a wall with no questions back;
|
|
24
|
+
// an agent asks the ones the treatment leaves open, and invents none.
|
|
25
|
+
needs: [
|
|
26
|
+
{ ask: "How long is it: an hour, a half-hour, a feature, or a page count?", tool: "set_target" },
|
|
27
|
+
{ ask: "The central question in one sentence, and the series premise above it if there is one.", tool: "set_logline, set_premise" },
|
|
28
|
+
{ ask: "Which scenes are the turns — the beats — or 'propose them and I will strike'.", tool: "set_rank" },
|
|
29
|
+
{ ask: "The acts, if it has them, and where each break falls.", tool: "create_group" },
|
|
30
|
+
{ ask: "Where each scene happens, in the writer's words; a scene that moves through one location is one place.", tool: "set_location" },
|
|
31
|
+
{ ask: "When a scene happens, where it matters — that night, the fourth of October.", tool: "the headline, for now" },
|
|
32
|
+
{ ask: "Who is in each scene, with the name to use — a full name, or the role name for someone unnamed — and who is only spoken of, which goes in notes.", tool: "add_character, cast, update_character" },
|
|
33
|
+
{ ask: "What is planted, and where it pays off — including 'later in the series', so the fold is deliberate.", tool: "set_plant, create_arrow" },
|
|
34
|
+
{ ask: "Any scene already known to run long or short; a duration in the story is not a length.", tool: "set_length" },
|
|
35
|
+
{ ask: "What the project and the board are called.", tool: "rename_project, rename_board" },
|
|
36
|
+
{ ask: "What not to invent: looks and voices are the writer's until they say.", tool: "update_character, later" },
|
|
37
|
+
],
|
|
20
38
|
},
|
|
21
39
|
{
|
|
22
40
|
id: "read-and-raise",
|