plotcoder-board 0.1.6 → 0.1.8
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 +1 -1
- package/package.json +1 -1
- package/scripts/plotcoder-mcp-server.mjs +126 -17
- package/src/board/agents.js +3 -3
- package/src/board/readWall.js +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Every board verb goes through one command kernel, `src/board/reducer.js`, and th
|
|
|
41
41
|
|
|
42
42
|
- **The wall.** Tap the words to type, drag the paper to move. Lasso to select, then Group. Drag a card's handle onto another card for an arrow. ⌘Z takes back any change, whichever door made it.
|
|
43
43
|
- **`window.plotcoder`** on the page, for a console or a CDP session.
|
|
44
|
-
- **The MCP server**, published to npm as `plotcoder-board`: `npx -y plotcoder-board@latest` is the server, `npx -y plotcoder-board@latest call <tool> '{json}'` one call from a shell, `npx -y plotcoder-board@latest serve` the hosted door on a port (a `Dockerfile` is here too). Inside the repo it is `scripts/plotcoder-mcp.mjs`, wired for Cursor in `.cursor/mcp.json` and for Claude Code in `.mcp.json` (run `npm ci` once first). A version tag (`v0.2.0`) publishes it, with `NPM_TOKEN` in the repo's secrets. Seventy-
|
|
44
|
+
- **The MCP server**, published to npm as `plotcoder-board`: `npx -y plotcoder-board@latest` is the server, `npx -y plotcoder-board@latest call <tool> '{json}'` one call from a shell, `npx -y plotcoder-board@latest serve` the hosted door on a port (a `Dockerfile` is here too). Inside the repo it is `scripts/plotcoder-mcp.mjs`, wired for Cursor in `.cursor/mcp.json` and for Claude Code in `.mcp.json` (run `npm ci` once first). A version tag (`v0.2.0`) publishes it, with `NPM_TOKEN` in the repo's secrets. Seventy-two tools: reading (`list_board`, `read_wall`, `read_pages`, `read_character`, `page_count`, `list_words`, `list_workflows`, `segment_brief`); the card, cast, place, group and arrow verbs, `set_logline`, `set_target`, `set_rank`, `set_plant`, `write_scene`, `move_scene`, `organize`, `apply_template` with `list_structures`, `save_structure`, `remove_structure`; `undo` and `redo`; the project's `list_boards`, `open_board`, `new_board`, `rename_board`, `delete_board`, `set_premise`, `rename_project` and the reminders; Fountain and Final Draft both ways; the production half (`lock_numbers`, `unlock_numbers`, `start_revision`, `end_revision`); the project as a file both ways (`export_project`, `import_project`); and, through the account door, `list_projects`, `open_project`, `new_project`, `delete_project`, `empty_account`, `delete_account`, `add_picture`, `add_take`, `list_takes`, `list_files`, `remove_file`, `build_segment`. If the dev app is open, a tool call lands on the wall within a second; if not, it edits the board file and the wall catches up on the next load.
|
|
45
45
|
- **The account door.** With `PLOTCODER_EMAIL` and `PLOTCODER_PASSWORD` in the agent's environment — the writer's own — and no dev app answering, the same server works the writer's project on the account directly, and every change lands live on every open wall. `PLOTCODER_PROJECT` picks a project by name or id. No account yet? `claim_account` makes one with the writer's email and a password they chose. The on-ramp — the doors, what to call first, the rules — is in the app behind *Are you an agent? Start here* and served at [plotcoder.com/llms.txt](https://plotcoder.com/llms.txt), both from `src/board/agents.js`.
|
|
46
46
|
|
|
47
47
|
An agent should call the tools, never fake mouse drags. The skill in `.cursor/skills/plotcoder-board/SKILL.md` says how; `.claude/skills/plotcoder-board` is a symlink to the same file.
|
package/package.json
CHANGED
|
@@ -697,11 +697,30 @@ function describeCommand(command) {
|
|
|
697
697
|
return `create_note "${command.headline ?? ""}"`;
|
|
698
698
|
case "recolor_notes":
|
|
699
699
|
return "recolor_note";
|
|
700
|
+
case "apply_poses":
|
|
701
|
+
return "organize";
|
|
700
702
|
default:
|
|
701
703
|
return command.type;
|
|
702
704
|
}
|
|
703
705
|
}
|
|
704
706
|
|
|
707
|
+
/**
|
|
708
|
+
* A board as one canonical string, so "has the board changed since my call?"
|
|
709
|
+
* compares boards and not the JSON a store happened to write: the account
|
|
710
|
+
* stores JSON in its own key order, and a byte comparison refused every undo
|
|
711
|
+
* but the first (round ten, finding 29).
|
|
712
|
+
*/
|
|
713
|
+
function canon(state) {
|
|
714
|
+
const sorted = (value) => {
|
|
715
|
+
if (Array.isArray(value)) return value.map(sorted);
|
|
716
|
+
if (value && typeof value === "object") {
|
|
717
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sorted(value[key])]));
|
|
718
|
+
}
|
|
719
|
+
return value;
|
|
720
|
+
};
|
|
721
|
+
return JSON.stringify(sorted(normalizeState(state)));
|
|
722
|
+
}
|
|
723
|
+
|
|
705
724
|
async function commit(command) {
|
|
706
725
|
const { state, rev, base, boardId } = await readBoard();
|
|
707
726
|
const { state: next, changed, result } = applyCommand(state, command);
|
|
@@ -711,7 +730,7 @@ async function commit(command) {
|
|
|
711
730
|
if (!changed) return { state: next, changed, result, live: base !== null };
|
|
712
731
|
|
|
713
732
|
const live = await writeBoard(next, rev, base, boardId);
|
|
714
|
-
trail.push({ before: state, after:
|
|
733
|
+
trail.push({ before: state, after: canon(next), what: describeCommand(command) });
|
|
715
734
|
if (trail.length > TRAIL_CAP) trail.shift();
|
|
716
735
|
undone.length = 0;
|
|
717
736
|
return { state: next, changed, result, live };
|
|
@@ -1142,7 +1161,7 @@ server.registerTool(
|
|
|
1142
1161
|
{
|
|
1143
1162
|
title: "Read the wall",
|
|
1144
1163
|
description:
|
|
1145
|
-
"Read the board back: the beats in wall order (rows top to bottom, cards left to right), the pages of scenes between consecutive beats with the cards in each, every setup with the distance to its payoff, and the questions the wall raises — no beat marked yet; a run out of proportion with the others; beats back to back with nothing between them (a chain of them is one question); a card with a placeholder headline or no change line; a card no arrow touches; two headlines that read like the same scene; a group too long to be one sequence; a person in the cast on no card; a person gone for more than a third of the story and ten pages; a payoff before its setup on the wall; a folded card no setup arrow pays off; cards that say no place once any card has one. These are questions, not fixes: put them to the writer and do not act on them unasked. It says nothing about how many beats there should be, and neither should you.",
|
|
1164
|
+
"Read the board back: the beats in wall order (rows top to bottom, cards left to right), the pages of scenes between consecutive beats with the cards in each, every setup with the distance to its payoff, and the questions the wall raises — no beat marked yet; a run out of proportion with the others; beats back to back with nothing between them (a chain of them is one question); a card with a placeholder headline or no change line; a card no arrow touches; two headlines that read like the same scene; a group too long to be one sequence; a person in the cast on no card; a person gone for more than a third of the story and ten pages; a payoff before its setup on the wall; a folded card no setup arrow pays off; cards that say no place once any card has one. These are questions, not fixes: put them to the writer and do not act on them unasked. It says nothing about how many beats there should be, and neither should you. The prose carries every id; the JSON after it is the same reading for a program, and PLOTCODER_JSON=0 in the server's environment drops it.",
|
|
1146
1165
|
inputSchema: {},
|
|
1147
1166
|
},
|
|
1148
1167
|
async () => {
|
|
@@ -1157,7 +1176,21 @@ server.registerTool(
|
|
|
1157
1176
|
const lines = [
|
|
1158
1177
|
`PlotCoder wall (${door(live, base)})`,
|
|
1159
1178
|
`logline: ${state.logline ? `"${state.logline}"` : "(none yet)"}`,
|
|
1160
|
-
|
|
1179
|
+
state.targetEighths === DEFAULT_TARGET_EIGHTHS
|
|
1180
|
+
? `runtime: about ${formatPages(boardEighths(state))} pages; no target set (set_target)`
|
|
1181
|
+
: `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"}`,
|
|
1182
|
+
`groups: ${
|
|
1183
|
+
state.groups.length
|
|
1184
|
+
? state.groups
|
|
1185
|
+
.map((group) => {
|
|
1186
|
+
const members = state.notes.filter((note) => group.noteIds.includes(note.id));
|
|
1187
|
+
const act = /^act\b/i.test((group.title ?? "").trim());
|
|
1188
|
+
return `"${group.title || "(untitled)"}" — ${members.length} card(s), about ${formatPages(members.reduce((sum, note) => sum + noteEighths(note), 0))} pages${act ? ", read as an act, so its length is not questioned" : ", read as a sequence"}`;
|
|
1189
|
+
})
|
|
1190
|
+
.join("; ")
|
|
1191
|
+
: "(none)"
|
|
1192
|
+
}`,
|
|
1193
|
+
`pages: ${written === 0 ? "all estimates — no scene is written yet, so every card is the writer's guess" : written === state.notes.length ? "measured — every scene is written" : `estimates — ${written} of ${state.notes.length} cards are written${written <= 5 ? ` (${state.notes.filter((note) => isMeasured(note)).map((note) => `"${note.headline}"`).join(", ")})` : ""}, the rest are guesses`}`,
|
|
1161
1194
|
`beats in wall order: ${
|
|
1162
1195
|
reading.beats.length
|
|
1163
1196
|
? reading.beats.map((beat) => `"${beat.headline}"`).join(", ")
|
|
@@ -1180,12 +1213,81 @@ server.registerTool(
|
|
|
1180
1213
|
},
|
|
1181
1214
|
);
|
|
1182
1215
|
|
|
1216
|
+
server.registerTool(
|
|
1217
|
+
"move_scene",
|
|
1218
|
+
{
|
|
1219
|
+
title: "Move a scene in the story",
|
|
1220
|
+
description:
|
|
1221
|
+
"Move a card to another place in the story order — after one card, or before one — by rewiring its follows arrows and tidying the wall along them, as one step that undo takes back whole. The story order is the follows arrows: the card leaves its place (what pointed at it now points at what it pointed at) and lands between the target and what followed it. A person does this by dragging in the outline. Needs a wall with follows arrows; on a wall without any, create_arrow the sequence first, or move_note by position.",
|
|
1222
|
+
inputSchema: { id: z.string(), after: z.string().optional(), before: z.string().optional() },
|
|
1223
|
+
},
|
|
1224
|
+
async (args) => {
|
|
1225
|
+
if (!args.after === !args.before) return ok("Say where: after one card's id, or before one, not both.");
|
|
1226
|
+
const { state } = await readBoard();
|
|
1227
|
+
const find = (id) => state.notes.find((note) => note.id === id);
|
|
1228
|
+
const card = find(args.id);
|
|
1229
|
+
const target = find(args.after ?? args.before);
|
|
1230
|
+
if (!card) return ok(`No card with id ${args.id}. Call list_board.`);
|
|
1231
|
+
if (!target) return ok(`No card with id ${args.after ?? args.before}. Call list_board.`);
|
|
1232
|
+
if (card.id === target.id) return ok("A card cannot be moved next to itself.");
|
|
1233
|
+
const isFollows = (arrow) => arrow.kind !== "setup";
|
|
1234
|
+
if (!state.arrows.some(isFollows)) {
|
|
1235
|
+
return ok("The wall has no follows arrows, so there is no story order to move within: create_arrow the sequence first, or move_note the card by position.");
|
|
1236
|
+
}
|
|
1237
|
+
const trailBefore = trail.length;
|
|
1238
|
+
let removed = 0;
|
|
1239
|
+
let drawn = 0;
|
|
1240
|
+
let live = false;
|
|
1241
|
+
const step = async (command) => {
|
|
1242
|
+
const done = await commit(command);
|
|
1243
|
+
if (done.changed) {
|
|
1244
|
+
live = done.live;
|
|
1245
|
+
if (command.type === "delete_arrow") removed += 1;
|
|
1246
|
+
if (command.type === "create_arrow") drawn += 1;
|
|
1247
|
+
}
|
|
1248
|
+
return done;
|
|
1249
|
+
};
|
|
1250
|
+
// Leave: what pointed at the card points at what the card pointed at.
|
|
1251
|
+
const ins = state.arrows.filter((arrow) => isFollows(arrow) && arrow.to === card.id);
|
|
1252
|
+
const outs = state.arrows.filter((arrow) => isFollows(arrow) && arrow.from === card.id);
|
|
1253
|
+
for (const arrow of [...ins, ...outs]) await step({ type: "delete_arrow", id: arrow.id });
|
|
1254
|
+
for (const before of ins) for (const after of outs) if (before.from !== after.to) await step({ type: "create_arrow", from: before.from, to: after.to, kind: "follows" });
|
|
1255
|
+
// Land: between the target and what followed it (or what led to it).
|
|
1256
|
+
const { state: mid } = await readBoard();
|
|
1257
|
+
if (args.after) {
|
|
1258
|
+
for (const arrow of mid.arrows.filter((item) => isFollows(item) && item.from === target.id && item.to !== card.id)) {
|
|
1259
|
+
await step({ type: "delete_arrow", id: arrow.id });
|
|
1260
|
+
await step({ type: "create_arrow", from: card.id, to: arrow.to, kind: "follows" });
|
|
1261
|
+
}
|
|
1262
|
+
await step({ type: "create_arrow", from: target.id, to: card.id, kind: "follows" });
|
|
1263
|
+
} else {
|
|
1264
|
+
for (const arrow of mid.arrows.filter((item) => isFollows(item) && item.to === target.id && item.from !== card.id)) {
|
|
1265
|
+
await step({ type: "delete_arrow", id: arrow.id });
|
|
1266
|
+
await step({ type: "create_arrow", from: arrow.from, to: card.id, kind: "follows" });
|
|
1267
|
+
}
|
|
1268
|
+
await step({ type: "create_arrow", from: card.id, to: target.id, kind: "follows" });
|
|
1269
|
+
}
|
|
1270
|
+
const { state: linked } = await readBoard();
|
|
1271
|
+
const tidied = await step({ type: "apply_poses", poses: organizePoses(linked, {}) });
|
|
1272
|
+
const final = tidied.state;
|
|
1273
|
+
// One step for undo: the whole move, not its dozen arrows.
|
|
1274
|
+
trail.splice(trailBefore);
|
|
1275
|
+
trail.push({ before: state, after: canon(final), what: `move_scene "${card.headline}"` });
|
|
1276
|
+
undone.length = 0;
|
|
1277
|
+
const order = readingOrder(final.notes);
|
|
1278
|
+
return ok(
|
|
1279
|
+
`Moved "${card.headline}" to ${args.after ? "after" : "before"} "${target.headline}": ${removed} arrow(s) removed, ${drawn} drawn, the wall tidied along them${where(live)}. Story order now: ${order.map((note, index) => `${index + 1}. ${note.headline}`).join(", ")}. One undo takes the whole move back.`,
|
|
1280
|
+
{ order: order.map((note) => note.id) },
|
|
1281
|
+
);
|
|
1282
|
+
},
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1183
1285
|
server.registerTool(
|
|
1184
1286
|
"organize",
|
|
1185
1287
|
{
|
|
1186
1288
|
title: "Organize the wall",
|
|
1187
1289
|
description:
|
|
1188
|
-
"Tidy the wall along the arrows. Cards are ordered by their 'follows' arrows (a card comes after everything that points at it), then by reading order. With beats on the wall, each beat starts a row and the scenes that follow it fill the row to its right, wrapping under themselves when a run is long; with no beats yet, rows wrap five cards wide.
|
|
1290
|
+
"Tidy the wall along the arrows. Cards are ordered by their 'follows' arrows (a card comes after everything that points at it), then by reading order. With beats on the wall, each beat starts a row and the scenes that follow it fill the row to its right, wrapping under themselves when a run is long; with no beats yet, rows wrap five cards wide. A group's cards keep their rows, so a group that spans beats spans rows. Pass noteIds to tidy only those cards, from their own top-left. Undoable from the wall.",
|
|
1189
1291
|
inputSchema: { noteIds: z.array(z.string()).min(2).optional() },
|
|
1190
1292
|
},
|
|
1191
1293
|
async (args) => {
|
|
@@ -1204,14 +1306,16 @@ server.registerTool(
|
|
|
1204
1306
|
for (const pose of poses) byRow.set(pose.y, [...(byRow.get(pose.y) ?? []), pose]);
|
|
1205
1307
|
const wrappedUnder = [];
|
|
1206
1308
|
let currentBeat = null;
|
|
1309
|
+
let opening = 0;
|
|
1207
1310
|
for (const y of [...byRow.keys()].sort((a, b) => a - b)) {
|
|
1208
|
-
const
|
|
1209
|
-
const note = state.notes.find((item) => item.id ===
|
|
1311
|
+
const row = byRow.get(y).sort((a, b) => a.x - b.x);
|
|
1312
|
+
const note = state.notes.find((item) => item.id === row[0].id);
|
|
1210
1313
|
if (note?.rank === "beat") currentBeat = note;
|
|
1211
|
-
else if (currentBeat
|
|
1314
|
+
else if (!currentBeat) opening += row.length;
|
|
1315
|
+
else if (note && !wrappedUnder.some((item) => item.beat === currentBeat)) wrappedUnder.push({ beat: currentBeat, first: note });
|
|
1212
1316
|
}
|
|
1213
1317
|
const shape = beats
|
|
1214
|
-
? `${beats} row(s), one per beat${wrappedUnder.length ? `; ${wrappedUnder.map((item) => `the row of "${item.beat.headline}" wraps under from "${item.first.headline}"`).join(", ")}` : ""}`
|
|
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(", ")}` : ""}`
|
|
1215
1319
|
: `${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`;
|
|
1216
1320
|
return ok(`Organized ${poses.length} card(s) along the arrows into ${shape}${where(live)}.`, poses);
|
|
1217
1321
|
},
|
|
@@ -1346,8 +1450,9 @@ server.registerTool(
|
|
|
1346
1450
|
if (!result) return ok(`No card with id ${args.id}. Call list_board.`);
|
|
1347
1451
|
return ok(`Nothing changed: "${result.headline}" already reads that way.`);
|
|
1348
1452
|
}
|
|
1453
|
+
const lines = args.text.split("\n").filter((line) => line.trim()).length;
|
|
1349
1454
|
return ok(
|
|
1350
|
-
`Wrote "${result.headline}": ${formatPages(noteEighths(result))} page(
|
|
1455
|
+
`Wrote "${result.headline}": ${lines} line(s), measured at ${formatPages(noteEighths(result))} of a page (a page is 55 lines of Courier 12; a fraction is rounded up to an eighth)${where(live)}. While the text stands the card is measured, not estimated; its estimate is untouched underneath.`,
|
|
1351
1456
|
result,
|
|
1352
1457
|
);
|
|
1353
1458
|
},
|
|
@@ -1527,7 +1632,7 @@ server.registerTool(
|
|
|
1527
1632
|
);
|
|
1528
1633
|
}
|
|
1529
1634
|
const note = unwritten
|
|
1530
|
-
? [`${unwritten} of ${order.length} scenes are unwritten and count as one line each here
|
|
1635
|
+
? [`${unwritten} of ${order.length} scenes are unwritten and count as one line each here, so this is the script so far, not the runtime: the estimate from the cards is about ${formatPages(boardEighths(state))} pages, the number to use until the scenes are written.`]
|
|
1531
1636
|
: [];
|
|
1532
1637
|
return ok([`pages: ${result.pageCount} of ${Math.round(state.targetEighths / 8)}`, ...note, ...lines].join("\n"), result.scenes);
|
|
1533
1638
|
},
|
|
@@ -1747,7 +1852,7 @@ server.registerTool(
|
|
|
1747
1852
|
const last = trail[trail.length - 1];
|
|
1748
1853
|
if (!last) return ok(oneCall() ? "Nothing to undo here: through plotcoder-call every call is a fresh server, so undo works only from an MCP session. The writer can take any change back from the wall with ⌘Z." : "Nothing of mine to undo in this session.");
|
|
1749
1854
|
const { state, rev, base } = await readBoard();
|
|
1750
|
-
if (
|
|
1855
|
+
if (canon(state) !== last.after) {
|
|
1751
1856
|
return ok(
|
|
1752
1857
|
`Not undone: the board has changed since my ${last.what}. Undoing now would trample that. Ask the person to undo from the wall if they want it back.`,
|
|
1753
1858
|
);
|
|
@@ -1775,11 +1880,11 @@ server.registerTool(
|
|
|
1775
1880
|
const last = undone[undone.length - 1];
|
|
1776
1881
|
if (!last) return ok("Nothing of mine to redo.");
|
|
1777
1882
|
const { state, rev, base, boardId } = await readBoard();
|
|
1778
|
-
if (
|
|
1883
|
+
if (canon(state) !== canon(last.before)) {
|
|
1779
1884
|
return ok(`Not redone: the board has changed since I undid my ${last.what}. Redoing now would trample that.`);
|
|
1780
1885
|
}
|
|
1781
1886
|
undone.pop();
|
|
1782
|
-
const after = JSON.parse(last.after);
|
|
1887
|
+
const after = normalizeState(JSON.parse(last.after));
|
|
1783
1888
|
const live = await writeBoard(after, rev, base, boardId);
|
|
1784
1889
|
trail.push(last);
|
|
1785
1890
|
return ok(`Redid ${last.what}${where(live)}. ${undone.length} more can be redone.`, after);
|
|
@@ -1855,7 +1960,8 @@ server.registerTool(
|
|
|
1855
1960
|
? ok("Not renamed: that is already the name.")
|
|
1856
1961
|
: ok(`No character with id ${args.id}. Call list_board for the cast.`);
|
|
1857
1962
|
}
|
|
1858
|
-
|
|
1963
|
+
const followed = state.notes.filter((note) => note.characterIds.includes(result.id)).length;
|
|
1964
|
+
return ok(`Renamed to "${result.name}" (${result.id})${where(live)}; the name changed on ${followed} card${followed === 1 ? "" : "s"}.`, result);
|
|
1859
1965
|
},
|
|
1860
1966
|
);
|
|
1861
1967
|
|
|
@@ -2522,7 +2628,7 @@ server.registerTool(
|
|
|
2522
2628
|
{
|
|
2523
2629
|
title: "Save the project as a file",
|
|
2524
2630
|
description:
|
|
2525
|
-
"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); 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.",
|
|
2631
|
+
"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's own folder, 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.",
|
|
2526
2632
|
inputSchema: { path: z.string().optional() },
|
|
2527
2633
|
},
|
|
2528
2634
|
async (args) => {
|
|
@@ -2534,7 +2640,7 @@ server.registerTool(
|
|
|
2534
2640
|
if (args.path) {
|
|
2535
2641
|
fs.mkdirSync(path.dirname(path.resolve(args.path)), { recursive: true });
|
|
2536
2642
|
fs.writeFileSync(args.path, JSON.stringify(file, null, 2));
|
|
2537
|
-
return ok(`Saved ${what}. Written to ${args.path}: Open project in the app takes it, import_project brings it onto an account.`, { path: args.path, boards: project.boards.length, cards });
|
|
2643
|
+
return ok(`Saved ${what}. Written to ${path.resolve(args.path)}: Open project in the app takes it, import_project brings it onto an account.`, { path: path.resolve(args.path), boards: project.boards.length, cards });
|
|
2538
2644
|
}
|
|
2539
2645
|
return ok(`The project as a file — ${what}. The JSON below is the file; write it to a .json for Open project or import_project.`, file);
|
|
2540
2646
|
},
|
|
@@ -2688,9 +2794,12 @@ server.registerTool(
|
|
|
2688
2794
|
inputSchema: { id: z.string() },
|
|
2689
2795
|
},
|
|
2690
2796
|
async (args) => {
|
|
2797
|
+
const { state: before } = await readBoard();
|
|
2798
|
+
const arrow = before.arrows.find((item) => item.id === args.id);
|
|
2691
2799
|
const { changed, live } = await commit({ type: "delete_arrow", id: args.id });
|
|
2692
2800
|
if (!changed) return ok(`No arrow with id ${args.id}. Call list_board for the real ids.`);
|
|
2693
|
-
|
|
2801
|
+
const name = (id) => `"${before.notes.find((note) => note.id === id)?.headline ?? id}"`;
|
|
2802
|
+
return ok(`Deleted the ${arrow?.kind ?? "follows"} arrow ${name(arrow?.from)} → ${name(arrow?.to)}${where(live)}. Any arrow the other way is untouched.`);
|
|
2694
2803
|
},
|
|
2695
2804
|
);
|
|
2696
2805
|
|
package/src/board/agents.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
export const AGENTS = {
|
|
9
9
|
lead:
|
|
10
|
-
"A storyline wall. Cards are scenes, beats are the big turns, arrows say what follows or pays off what. An agent driven by a person has every tool a person here has; the person directs, the agent operates. Call the tools; never fake a mouse.",
|
|
10
|
+
"A storyline wall. Cards are scenes, beats are the big turns, arrows say what follows or pays off what. An agent driven by a person has every tool a person here has; the person directs, the agent operates. Call the tools; never fake a mouse. Already have the plotcoder-board tools in front of you? Skip the doors and go to Call these first.",
|
|
11
11
|
doors: [
|
|
12
12
|
{
|
|
13
13
|
id: "mcp",
|
|
@@ -42,7 +42,7 @@ export const AGENTS = {
|
|
|
42
42
|
text: "Skip this when the account is the wall. Without an account, a wall is a folder: any folder, empty is fine — choose one that will outlive your session, never a scratch one. The app run from that folder shows the wall, and the server writes it there (PLOTCODER_ROOT, or the folder it is run from). A fresh folder holds the sample; new_board for the writer's wall, then rename_project. No app running? export_fountain is the wall in order, as text.",
|
|
43
43
|
},
|
|
44
44
|
],
|
|
45
|
-
firstNote: "These four are about the wall you will work, so after open_project or
|
|
45
|
+
firstNote: "These four are about the wall you will work, so after open_project, open_board, new_project or empty_account, read_wall again. 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; new_project, then the four again. 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
48
|
{ tool: "read_wall", why: "what is here, and what it asks. A fresh folder holds a sample wall (Maya, Tom, the letter) and says so; it is not the writer's." },
|
|
@@ -64,7 +64,7 @@ export const AGENTS = {
|
|
|
64
64
|
|
|
65
65
|
/** The on-ramp as one text: the file at /llms.txt, and what an agent reads. */
|
|
66
66
|
export function agentsAsText() {
|
|
67
|
-
const lines = ["# PlotCoder — for agents", "", AGENTS.lead, "", `The guide, read once before anything: ${AGENTS.guide}.`, "", "## Doors"];
|
|
67
|
+
const lines = ["# PlotCoder — for agents", "", AGENTS.lead, "", `The guide, read once before anything: ${AGENTS.guide}. The guide is the whole and this page is its first page; where the two differ, the guide wins.`, "", "## Doors"];
|
|
68
68
|
for (const door of AGENTS.doors) {
|
|
69
69
|
lines.push(`- ${door.name}: ${door.text}`);
|
|
70
70
|
if (door.code) lines.push("", "```", door.code, "```", "");
|
package/src/board/readWall.js
CHANGED
|
@@ -176,7 +176,7 @@ export function readWall(state) {
|
|
|
176
176
|
findings.push({
|
|
177
177
|
kind: "sag",
|
|
178
178
|
ids: [longest.from, longest.to],
|
|
179
|
-
text: `About ${pages(longest.eighths)} pages run between "${headline(longest.from)}" and "${headline(longest.to)}"; the
|
|
179
|
+
text: `About ${pages(longest.eighths)} pages run between "${headline(longest.from)}" and "${headline(longest.to)}"; the median run here is about ${pages(typical)} (a beat's own pages are in no run). Is something sagging there, or is it one long set piece?`,
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
}
|