plotcoder-board 0.1.37 → 0.1.38

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 CHANGED
@@ -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.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.
110
+ Version 0.1.38. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plotcoder-board",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -835,7 +835,7 @@ function changeNote() {
835
835
  const parts = [];
836
836
  if (change.gone.length || change.came.length) {
837
837
  parts.push(
838
- `the wall now asks ${change.asks} question${change.asks === 1 ? "" : "s"}${change.gone.length ? ` (gone: ${change.gone.map((finding) => `[${finding.kind}] ${finding.text}`).join(" ")})` : ""}${change.came.length ? ` (new: ${change.came.map((finding) => `[${finding.kind}] ${finding.text}`).join(" ")})` : ""}`,
838
+ `the wall now asks ${change.asks} question${change.asks === 1 ? "" : "s"}${change.gone.length ? ` (gone: ${change.gone.map((finding) => `[${finding.kind}] ${finding.text.replace(/\.$/, "")}`).join(" ")})` : ""}${change.came.length ? ` (new: ${change.came.map((finding) => `[${finding.kind}] ${finding.text.replace(/\.$/, "")}`).join(" ")})` : ""}`,
839
839
  );
840
840
  }
841
841
  if (change.leftAfter !== change.leftBefore) parts.push(`left, for now: ${change.leftAfter} (was ${change.leftBefore})`);
@@ -1982,7 +1982,6 @@ server.registerTool(
1982
1982
  const poses = organizePoses(state, { onlyIds: args.noteIds });
1983
1983
  if (poses.length === 0) return ok("Nothing to organize: no cards in scope.");
1984
1984
  const { changed, live } = await commit({ type: "apply_poses", poses });
1985
- if (!changed) return ok("Nothing moved.");
1986
1985
  const rows = new Set(poses.map((pose) => pose.y)).size;
1987
1986
  const beats = state.notes.filter(
1988
1987
  (note) => note.rank === "beat" && poses.some((pose) => pose.id === note.id),
@@ -2004,6 +2003,8 @@ server.registerTool(
2004
2003
  const shape = beats
2005
2004
  ? `${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(", ")}` : ""}`
2006
2005
  : `${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`;
2006
+ // Nothing moved says what already stands (round fifteen, entry 20): after a rank change the agent asked whether a beat row still held.
2007
+ if (!changed) return ok(`Nothing moved: the ${poses.length} card(s) already lie along the arrows in ${shape}.`, poses);
2007
2008
  return ok(`Organized ${poses.length} card(s) along the arrows into ${shape}${where(live)}.`, poses);
2008
2009
  },
2009
2010
  );
@@ -3512,6 +3513,53 @@ function describeBoards(project, boards, changedAt = null) {
3512
3513
  .join("\n");
3513
3514
  }
3514
3515
 
3516
+ /**
3517
+ * The project as a whole (round fifteen, entry 14; the handover's first
3518
+ * call): every board's questions in one reading, with what each board
3519
+ * leaves open, the folds that pay off on another board, and the project's
3520
+ * length. Each board is read as read_wall reads it — the same checks, the
3521
+ * same cross-board context — so this never says something read_wall would
3522
+ * not; it only says it for every board at once.
3523
+ */
3524
+ server.registerTool(
3525
+ "read_project",
3526
+ {
3527
+ title: "Read the project",
3528
+ description:
3529
+ "Every board's reading in one call: for each board in the writer's order, its logline, its runtime, the questions it asks and what it leaves open by the writer's word — the same checks read_wall runs, board by board — then the folds that pay off on another board and the project's length. The records — cards, ids, cast, places — are list_board's, board by board; open_board and read_wall for one board in full.",
3530
+ inputSchema: {},
3531
+ },
3532
+ async () => {
3533
+ const { project, boards, live, base } = await readProject();
3534
+ const states = new Map(project.boards.map((meta) => [meta.id, isBoardState(boards[meta.id]) ? normalizeState(boards[meta.id]) : emptyState()]));
3535
+ const boardsNow = Object.fromEntries(states);
3536
+ const lines = [
3537
+ `PlotCoder project "${project.name}"${project.nameOpen ? ` — its name is open, by the writer's word: "${project.nameOpen}"` : ""} (${door(live, base)})`,
3538
+ `premise: ${project.premiseOpen ? `open, by the writer's word — "${project.premiseOpen}"` : project.premise ? `"${project.premise}"` : "(not set)"}`,
3539
+ ];
3540
+ let asked = 0;
3541
+ for (const [index, meta] of project.boards.entries()) {
3542
+ const state = states.get(meta.id);
3543
+ const reading = readWall(state, { elsewhere: Object.keys(castElsewhere(project, boardsNow, meta.id)), laterBoards: laterBoards(project, boardsNow), paidBy: landingsOn(project, boardsNow, meta.id).paid });
3544
+ asked += reading.findings.length;
3545
+ const open = reading.open.length + reading.openFields.length + (meta.nameOpen ? 1 : 0);
3546
+ const headline = (id) => `"${state.notes.find((note) => note.id === id)?.headline ?? id}"`;
3547
+ lines.push(
3548
+ `${index + 1}. "${meta.name}"${meta.nameOpen ? ` (name open: "${meta.nameOpen}")` : ""}${meta.id === project.activeBoardId ? " (open)" : ""} — ${state.notes.length} card${state.notes.length === 1 ? "" : "s"}, ${reading.beats.length} beat${reading.beats.length === 1 ? "" : "s"}, about ${formatPages(boardEighths(state))} of ${formatPages(state.targetEighths)} pages; logline: ${state.loglineOpen ? `open — "${state.loglineOpen}"` : state.logline ? `"${state.logline}"` : "(none yet)"}`,
3549
+ ...(reading.findings.length ? reading.findings.map((finding) => ` - [${finding.kind}] ${finding.text}`) : [` (asks nothing${reading.left.length ? `; ${reading.left.length} left by the writer` : ""}${state.notes.length ? "" : ": no cards yet"})`]),
3550
+ ...(open ? [` open by the writer's word: ${[reading.open.length ? `${reading.open.length} card${reading.open.length === 1 ? "" : "s"}` : "", reading.openFields.length ? `${reading.openFields.length} field${reading.openFields.length === 1 ? "" : "s"}` : "", meta.nameOpen ? "the board's name" : ""].filter(Boolean).join(", ")} — read_wall there lists them`] : []),
3551
+ ...reading.threads.filter((thread) => thread.startOpen || thread.endOpen).map((thread) => ` thread "${thread.name}" — ${thread.startOpen ? "starts nowhere yet" : ""}${thread.startOpen && thread.endOpen ? ", " : ""}${thread.endOpen ? "ends nowhere yet" : ""}`),
3552
+ ...reading.later.map((item) => ` "${headline(item.id)}" is folded and pays off later, on "${boardById(project, item.boardId)?.name ?? item.boardId}"${item.noteId ? `, at "${boardsNow[item.boardId]?.notes?.find((note) => note.id === item.noteId)?.headline ?? item.noteId}"` : ", no scene there claimed yet"}`),
3553
+ );
3554
+ }
3555
+ const pages = [...states.values()].reduce((sum, state) => sum + boardEighths(state), 0);
3556
+ const target = [...states.values()].reduce((sum, state) => sum + state.targetEighths, 0);
3557
+ const cards = [...states.values()].reduce((sum, state) => sum + state.notes.length, 0);
3558
+ lines.push(`the whole project: ${cards} card${cards === 1 ? "" : "s"}, about ${formatPages(pages)} of ${formatPages(target)} pages across ${project.boards.length} board${project.boards.length === 1 ? "" : "s"}; ${asked} question${asked === 1 ? "" : "s"} in all (each board's runtime is an estimate unless every scene is written)`);
3559
+ return ok(lines.join("\n"), { project: { id: project.id, name: project.name }, boards: project.boards.map((meta) => meta.id), asked });
3560
+ },
3561
+ );
3562
+
3515
3563
  server.registerTool(
3516
3564
  "list_boards",
3517
3565
  {