plotcoder-board 0.1.12 → 0.1.13
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
|
@@ -807,10 +807,12 @@ function summarize(state) {
|
|
|
807
807
|
const cast = note.characterIds.map((id) => nameOf.get(id) ?? id);
|
|
808
808
|
const who = cast.length ? `, cast: ${cast.join(", ")}` : "";
|
|
809
809
|
const plant = note.plants ? (note.payoffBoardId ? ", plants → pays off later" : ", plants") : "";
|
|
810
|
+
const snap = state.revision?.snapshot?.[note.id];
|
|
811
|
+
const revised = snap && (snap.headline !== note.headline || snap.change !== note.change || (snap.text ?? "") !== (note.text ?? "") || (snap.location ?? "") !== (note.location ?? "")) ? `, changed in ${state.revision.color}` : "";
|
|
810
812
|
const place = note.location ? `, at: ${note.location}` : "";
|
|
811
813
|
const count = formatPages(noteEighths(note));
|
|
812
|
-
const pages = `${count} ${count === "1" ? "page" : "pages"}
|
|
813
|
-
return ` - ${note.id} [${note.rank ?? "scene"}, ${pages}${who}${place}${plant}] — "${note.headline}" (${note.color}) at ${Math.round(note.x)},${Math.round(note.y)}`;
|
|
814
|
+
const pages = isMeasured(note) ? `${count} ${count === "1" ? "page" : "pages"}, written` : note.lengthEighths === null ? "about a page, unsized" : `${count} ${count === "1" ? "page" : "pages"}`;
|
|
815
|
+
return ` - ${note.id} [${note.rank ?? "scene"}, ${pages}${who}${place}${plant}${revised}] — "${note.headline}" (${note.color}) at ${Math.round(note.x)},${Math.round(note.y)}`;
|
|
814
816
|
})
|
|
815
817
|
.join("\n");
|
|
816
818
|
const cast = state.characters
|
|
@@ -936,7 +938,7 @@ server.registerTool(
|
|
|
936
938
|
: "board";
|
|
937
939
|
return ok(
|
|
938
940
|
`PlotCoder ${which} (${door(live, base)})\n${summarize(state)}`,
|
|
939
|
-
{ ...state, notes: state.notes.map((note) => ({ ...note, eighths: noteEighths(note), measured: isMeasured(note) })) },
|
|
941
|
+
{ ...state, revision: state.revision ? { name: state.revision.name, color: state.revision.color, since: state.revision.since } : null, notes: state.notes.map((note) => ({ ...note, eighths: noteEighths(note), measured: isMeasured(note) })) },
|
|
940
942
|
);
|
|
941
943
|
},
|
|
942
944
|
);
|
|
@@ -1117,7 +1119,7 @@ server.registerTool(
|
|
|
1117
1119
|
},
|
|
1118
1120
|
},
|
|
1119
1121
|
async (args) => {
|
|
1120
|
-
const { result } = await commit({
|
|
1122
|
+
const { state, result, live } = await commit({
|
|
1121
1123
|
type: "update_note",
|
|
1122
1124
|
id: args.id,
|
|
1123
1125
|
headline: args.headline,
|
|
@@ -1125,7 +1127,8 @@ server.registerTool(
|
|
|
1125
1127
|
location: args.location,
|
|
1126
1128
|
});
|
|
1127
1129
|
if (result === undefined) return ok(`No card with id ${args.id}.`);
|
|
1128
|
-
|
|
1130
|
+
const mark = state.revision && state.revision.snapshot?.[result.id] && (state.revision.snapshot[result.id].headline !== result.headline || state.revision.snapshot[result.id].change !== result.change) ? ` Marked changed in the ${state.revision.color} revision "${state.revision.name}".` : "";
|
|
1131
|
+
return ok(`Updated "${result.headline}"${args.change !== undefined ? `: change line "${result.change}"` : ""}${where(live)}.${mark}`, result);
|
|
1129
1132
|
},
|
|
1130
1133
|
);
|
|
1131
1134
|
|
|
@@ -1189,8 +1192,9 @@ server.registerTool(
|
|
|
1189
1192
|
inputSchema: {},
|
|
1190
1193
|
},
|
|
1191
1194
|
async () => {
|
|
1192
|
-
const { state, live, base } = await readBoard();
|
|
1195
|
+
const { state, live, base, boardId: readBoardId } = await readBoard();
|
|
1193
1196
|
const { project: projectForRead } = await readProject();
|
|
1197
|
+
const readBoardMeta = boardById(projectForRead, readBoardId ?? projectForRead.activeBoardId);
|
|
1194
1198
|
const reading = readWall(state);
|
|
1195
1199
|
const runs = describeRuns(reading, state).map((line, index) => {
|
|
1196
1200
|
const ids = reading.runs[index]?.ids ?? [];
|
|
@@ -1200,6 +1204,8 @@ server.registerTool(
|
|
|
1200
1204
|
const written = state.notes.filter((note) => isMeasured(note)).length;
|
|
1201
1205
|
const lines = [
|
|
1202
1206
|
`PlotCoder wall (${door(live, base)})`,
|
|
1207
|
+
...(state.lock ? [`numbers: locked since ${String(state.lock.at).slice(0, 10)}; read_pages shows each scene's number`] : []),
|
|
1208
|
+
`board: "${readBoardMeta?.name ?? "?"}"${projectForRead.boards.length > 1 ? ` — ${projectForRead.boards.length} boards in "${projectForRead.name}"; open_board reads another` : ""}`,
|
|
1203
1209
|
`logline: ${state.logline ? `"${state.logline}"` : "(none yet)"}`,
|
|
1204
1210
|
"the cast and the places are list_board's, not the reading's",
|
|
1205
1211
|
state.targetEighths === DEFAULT_TARGET_EIGHTHS
|
|
@@ -1234,12 +1240,17 @@ server.registerTool(
|
|
|
1234
1240
|
? reading.findings.map((finding) => ` - [${finding.kind}] ${finding.text}${finding.ids.length ? ` (ids: ${finding.ids.join(", ")})` : ""}`)
|
|
1235
1241
|
: [" (none that this reading can see)"]),
|
|
1236
1242
|
`checks: ${CHECKS.length} run — ${(() => {
|
|
1237
|
-
const asked = reading.findings
|
|
1243
|
+
const asked = reading.findings;
|
|
1238
1244
|
if (asked.length === 0) return "asking nothing";
|
|
1239
1245
|
const counts = new Map();
|
|
1240
1246
|
for (const finding of asked) counts.set(finding.kind, (counts.get(finding.kind) ?? 0) + 1);
|
|
1241
1247
|
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(", ")}`;
|
|
1242
|
-
})()}; checked and clean: ${CHECKS.filter((kind) => !reading.findings.some((finding) => finding.kind === kind)).map((kind) =>
|
|
1248
|
+
})()}; checked and clean: ${CHECKS.filter((kind) => !reading.findings.some((finding) => finding.kind === kind)).map((kind) => {
|
|
1249
|
+
if (kind === "unlinked" && state.arrows.length === 0) return "no card without an arrow (not asked: no arrows yet)";
|
|
1250
|
+
if (kind === "unplaced" && !state.notes.some((note) => (note.location ?? "").trim())) return "no card without a place (not asked: no card placed yet)";
|
|
1251
|
+
if (kind === "sequence" && state.groups.length === 0) return "no group too long for one sequence (not asked: no groups)";
|
|
1252
|
+
return CHECK_WORDS[kind];
|
|
1253
|
+
}).join("; ") || "(nothing — every check found something)"}`,
|
|
1243
1254
|
];
|
|
1244
1255
|
if (isSampleWall(state)) lines.unshift(SAMPLE_NOTE);
|
|
1245
1256
|
return ok(lines.join("\n"), { ...reading, sample: isSampleWall(state) });
|
|
@@ -1311,7 +1322,7 @@ server.registerTool(
|
|
|
1311
1322
|
const group = final.groups.find((item) => item.noteIds.includes(card.id));
|
|
1312
1323
|
const groupLine = 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.` : "";
|
|
1313
1324
|
return ok(
|
|
1314
|
-
`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(", ")}.${groupLine} One undo takes the whole move back.`,
|
|
1325
|
+
`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} One undo takes the whole move back.`,
|
|
1315
1326
|
{ order: order.map((note) => note.id) },
|
|
1316
1327
|
);
|
|
1317
1328
|
},
|
|
@@ -1399,10 +1410,9 @@ server.registerTool(
|
|
|
1399
1410
|
const { project } = await readProject();
|
|
1400
1411
|
const own = project.structures ?? [];
|
|
1401
1412
|
const lines = [
|
|
1402
|
-
`built in: ${TEMPLATES.length}`,
|
|
1403
|
-
...TEMPLATES.map((template) => ` - ${template.id} — "${template.name}" (${template.beats.length} beats)`),
|
|
1404
1413
|
`the writer's own: ${own.length}`,
|
|
1405
|
-
...own.map((structure) => ` - ${structure.id} — "${structure.name}" (${structure.beats.length} beats: ${structure.beats.map((beat) => beat.name).join(", ")})`),
|
|
1414
|
+
...own.map((structure) => ` - ${structure.id} — "${structure.name}" (${structure.beats.length} beats: ${structure.beats.map((beat) => `${beat.name} at ${Math.round(beat.at * 100)}%`).join(", ")})`),
|
|
1415
|
+
`built in: ${TEMPLATES.length} — ${TEMPLATES.map((template) => `${template.id} "${template.name}" (${template.beats.length} beats)`).join(", ")}; each beat's name, prompt and place in the story are in the JSON`,
|
|
1406
1416
|
];
|
|
1407
1417
|
return ok(lines.join("\n"), { builtIn: TEMPLATES.map((template) => ({ id: template.id, name: template.name, beats: template.beats })), own });
|
|
1408
1418
|
},
|
|
@@ -1424,7 +1434,7 @@ server.registerTool(
|
|
|
1424
1434
|
const { project, boards, rev, base, live } = await readProject();
|
|
1425
1435
|
const { project: next, structure } = addStructure(project, args.name, beats);
|
|
1426
1436
|
await writeProject(next, boards, rev, base);
|
|
1427
|
-
return ok(`Saved "${structure.name}" with ${beats.length} beats${where(live)}: ${beats.map((beat) => beat.name).join(", ")}.`, structure);
|
|
1437
|
+
return ok(`Saved "${structure.name}" with ${beats.length} beats${where(live)}: ${beats.map((beat) => `${beat.name} at ${Math.round(beat.at * 100)}%`).join(", ")}. Each beat's prompt is its change line here; apply_template lays the same turns on another board, the next episode's, as beat cards to fill.`, structure);
|
|
1428
1438
|
},
|
|
1429
1439
|
);
|
|
1430
1440
|
|
|
@@ -1487,7 +1497,7 @@ server.registerTool(
|
|
|
1487
1497
|
}
|
|
1488
1498
|
const printed = sceneLineCount(args.text);
|
|
1489
1499
|
return ok(
|
|
1490
|
-
`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${where(live)}. The heading comes from the card's place, so the text starts with the action. While the text stands the card is measured, not estimated;
|
|
1500
|
+
`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${where(live)}. The heading comes from the card's place, so the text starts with the action. While the text stands the card is measured, not estimated; every reading uses the measure (eighths here), and the estimate underneath (lengthEighths) is untouched.`,
|
|
1491
1501
|
{ ...result, eighths: noteEighths(result), measured: true, printedLines: printed },
|
|
1492
1502
|
);
|
|
1493
1503
|
},
|
|
@@ -1508,6 +1518,7 @@ server.registerTool(
|
|
|
1508
1518
|
const text = toFountain(state, { title: board?.name, premise: project.premise || undefined });
|
|
1509
1519
|
const parsed = fromFountain(text);
|
|
1510
1520
|
const ids = mergeFountain(state, parsed).matched.map((item) => item.id);
|
|
1521
|
+
const pageNumbers = state.lock ? sceneNumbers(readingOrder(state.notes), state.lock) : null;
|
|
1511
1522
|
const lines = [];
|
|
1512
1523
|
let index = 0;
|
|
1513
1524
|
for (const line of text.split("\n")) {
|
|
@@ -1515,7 +1526,8 @@ server.registerTool(
|
|
|
1515
1526
|
const note = state.notes.find((item) => item.id === ids[index]);
|
|
1516
1527
|
index += 1;
|
|
1517
1528
|
const standIn = note && !(note.location ?? "").trim() ? " · no place: the headline stands in for the heading" : "";
|
|
1518
|
-
|
|
1529
|
+
const numbered = note && pageNumbers?.get(note.id) ? ` · locked no. ${pageNumbers.get(note.id)}` : "";
|
|
1530
|
+
lines.push(`${line} [[id: ${note?.id ?? "?"} · ${note && isMeasured(note) ? "measured" : "estimated"} ${formatPages(note ? noteEighths(note) : 0)}pp${standIn}${numbered}]]`);
|
|
1519
1531
|
} else {
|
|
1520
1532
|
lines.push(line);
|
|
1521
1533
|
}
|
|
@@ -1593,7 +1605,7 @@ server.registerTool(
|
|
|
1593
1605
|
const { state } = await readBoard();
|
|
1594
1606
|
const { project } = await readProject();
|
|
1595
1607
|
const board = project.boards.find((item) => item.id === project.activeBoardId);
|
|
1596
|
-
const brief = segmentBrief(state, args.ids, { title: board?.name });
|
|
1608
|
+
const brief = segmentBrief(state, args.ids, { title: board?.name, boards: project.boards });
|
|
1597
1609
|
if (!brief) return ok(`No cards with ids ${args.ids.join(", ")}. Call list_board.`);
|
|
1598
1610
|
return ok(brief);
|
|
1599
1611
|
},
|
|
@@ -1672,7 +1684,7 @@ server.registerTool(
|
|
|
1672
1684
|
const note = unwritten
|
|
1673
1685
|
? [`${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.`]
|
|
1674
1686
|
: [];
|
|
1675
|
-
return ok([`pages: ${result.pageCount} of ${Math.round(state.targetEighths / 8)}`, ...note, ...lines].join("\n"), result.scenes);
|
|
1687
|
+
return ok([`pages: ${result.pageCount} of ${Math.round(state.targetEighths / 8)}`, `scene numbers here are ${state.lock ? "the locked numbers" : "wall order (not locked)"}`, ...note, ...lines].join("\n"), result.scenes);
|
|
1676
1688
|
},
|
|
1677
1689
|
);
|
|
1678
1690
|
|
|
@@ -1697,8 +1709,10 @@ server.registerTool(
|
|
|
1697
1709
|
"unlock_numbers",
|
|
1698
1710
|
{ title: "Unlock the scene numbers", description: "Numbers follow the wall's order again.", inputSchema: {} },
|
|
1699
1711
|
async () => {
|
|
1712
|
+
const { state: before } = await readBoard();
|
|
1713
|
+
|
|
1700
1714
|
const { changed, live } = await commit({ type: "unlock_numbers" });
|
|
1701
|
-
return ok(changed ? `Unlocked${where(live)}.` : "The numbers were not locked.");
|
|
1715
|
+
return ok(changed ? `Unlocked ${Object.keys(before.lock?.numbers ?? {}).length} scene number(s)${where(live)}; scenes number by wall order again.` : "The numbers were not locked.");
|
|
1702
1716
|
},
|
|
1703
1717
|
);
|
|
1704
1718
|
|
|
@@ -1708,11 +1722,13 @@ server.registerTool(
|
|
|
1708
1722
|
title: "Start a revision",
|
|
1709
1723
|
description:
|
|
1710
1724
|
`Name a revision and give it one of the industry's colours (${REVISION_COLORS.join(", ")}). Every card is snapshotted; from then on a changed line prints in the colour with a star in the margin, and a changed card wears the colour on the wall.`,
|
|
1711
|
-
inputSchema: { name: z.string().
|
|
1725
|
+
inputSchema: { name: z.string().optional(), color: z.string().optional() },
|
|
1712
1726
|
},
|
|
1713
1727
|
async (args) => {
|
|
1714
|
-
const
|
|
1715
|
-
if (!
|
|
1728
|
+
const revisionName = (args.name ?? "").trim() || (args.color ? `${args.color.charAt(0).toUpperCase()}${args.color.slice(1)}` : "");
|
|
1729
|
+
if (!revisionName) return ok("No revision started: give it a name, or a colour to name it after.");
|
|
1730
|
+
const { changed, result, live } = await commit({ type: "start_revision", name: revisionName, color: args.color });
|
|
1731
|
+
if (!changed) return ok("No revision started: one is already in progress; end_revision first.");
|
|
1716
1732
|
return ok(`Started the ${result.color} revision "${result.name}"${where(live)}.`, { name: result.name, color: result.color, since: result.since });
|
|
1717
1733
|
},
|
|
1718
1734
|
);
|
|
@@ -1721,8 +1737,10 @@ server.registerTool(
|
|
|
1721
1737
|
"end_revision",
|
|
1722
1738
|
{ title: "End the revision", description: "The marks come off; the snapshot is dropped.", inputSchema: {} },
|
|
1723
1739
|
async () => {
|
|
1740
|
+
const { state: before } = await readBoard();
|
|
1741
|
+
|
|
1724
1742
|
const { changed, live } = await commit({ type: "end_revision" });
|
|
1725
|
-
return ok(changed ? `Revision ended${where(live)}.` : "No revision in progress.");
|
|
1743
|
+
return ok(changed ? `Revision "${before.revision?.name ?? ""}" (${before.revision?.color ?? ""}) ended${where(live)}: its marks come off and its snapshot is dropped.` : "No revision in progress.");
|
|
1726
1744
|
},
|
|
1727
1745
|
);
|
|
1728
1746
|
|
|
@@ -1740,7 +1758,7 @@ server.registerTool(
|
|
|
1740
1758
|
const { state } = await readBoard();
|
|
1741
1759
|
const { project } = await readProject();
|
|
1742
1760
|
const board = project.boards.find((item) => item.id === project.activeBoardId);
|
|
1743
|
-
const brief = segmentBrief(state, args.ids, { title: board?.name });
|
|
1761
|
+
const brief = segmentBrief(state, args.ids, { title: board?.name, boards: project.boards });
|
|
1744
1762
|
if (!brief) return ok(`No cards with ids ${args.ids.join(", ")}. Call list_board.`);
|
|
1745
1763
|
const provider = env.PLOTCODER_VIDEO_PROVIDER;
|
|
1746
1764
|
if (!provider) {
|
|
@@ -1900,8 +1918,10 @@ server.registerTool(
|
|
|
1900
1918
|
const { boardId } = await readBoard();
|
|
1901
1919
|
const live = await writeBoard(last.before, rev, base, boardId);
|
|
1902
1920
|
const orderLine = /^(move_scene|organize)/.test(last.what) ? ` Story order now: ${readingOrder(last.before.notes).map((note, index) => `${index + 1}. ${note.headline}`).join(", ")}.` : "";
|
|
1921
|
+
const cardsDiff = last.before.notes.length - state.notes.length;
|
|
1922
|
+
const countLine = cardsDiff > 0 ? ` ${cardsDiff} card(s) back.` : cardsDiff < 0 ? ` ${-cardsDiff} card(s) gone.` : "";
|
|
1903
1923
|
const more = trail.length >= TRAIL_CAP ? `${trail.length} more of mine can be undone — the most I keep, so the oldest have gone` : `${trail.length} more of mine can be undone`;
|
|
1904
|
-
return ok(`Undid ${last.what}${where(live)}.${orderLine} ${more}.`, last.before);
|
|
1924
|
+
return ok(`Undid ${last.what}${where(live)}.${orderLine}${countLine} ${more}. list_board has the board.`, { undid: last.what, notes: last.before.notes.length, arrows: last.before.arrows.length, groups: last.before.groups.length });
|
|
1905
1925
|
},
|
|
1906
1926
|
);
|
|
1907
1927
|
|
|
@@ -1924,7 +1944,7 @@ server.registerTool(
|
|
|
1924
1944
|
const after = normalizeState(JSON.parse(last.after));
|
|
1925
1945
|
const live = await writeBoard(after, rev, base, boardId);
|
|
1926
1946
|
trail.push(last);
|
|
1927
|
-
return ok(`Redid ${last.what}${where(live)}. ${undone.length} more can be redone.`, after);
|
|
1947
|
+
return ok(`Redid ${last.what}${where(live)}. ${undone.length} more can be redone. list_board has the board.`, { redid: last.what, notes: after.notes.length, arrows: after.arrows.length, groups: after.groups.length });
|
|
1928
1948
|
},
|
|
1929
1949
|
);
|
|
1930
1950
|
|
|
@@ -1933,7 +1953,7 @@ server.registerTool(
|
|
|
1933
1953
|
{
|
|
1934
1954
|
title: "Fold the corner",
|
|
1935
1955
|
description:
|
|
1936
|
-
`Fold the corner of cards — mark them as planting something — or unfold them. ${wordSentence("corner")} The setup arrow is create_arrow with kind 'setup'. A fold that pays off in a later episode: pass later, another board of the project by name, id or number
|
|
1956
|
+
`Fold the corner of cards — mark them as planting something — or unfold them. ${wordSentence("corner")} The setup arrow is create_arrow with kind 'setup'. A fold that pays off in a later episode: pass later, another board of the project by name, id or number — a board that exists; new_board makes one — and the wall stops asking where it comes back, listing the card under the reading's 'later' instead of 'payoffs'; later '' forgets it. Folding never moves a card.`,
|
|
1937
1957
|
inputSchema: {
|
|
1938
1958
|
ids: z.array(z.string()).min(1),
|
|
1939
1959
|
plants: z.boolean(),
|
|
@@ -1963,7 +1983,7 @@ server.registerTool(
|
|
|
1963
1983
|
}
|
|
1964
1984
|
} else {
|
|
1965
1985
|
const target = findBoard(project, args.later);
|
|
1966
|
-
if (!target) return ok(`No board
|
|
1986
|
+
if (!target) return ok(`No board called "${args.later}" yet. A fold pays off later on a board of the project: new_board "${args.later}" makes it (empty), open_board back to this one, then set_plant again with later.`);
|
|
1967
1987
|
if (target.id === (current ?? project.activeBoardId)) return ok(`"${target.name}" is this board. A payoff on the same board is a setup arrow: create_arrow from the fold to the scene, kind 'setup'.`);
|
|
1968
1988
|
const named = await commit({ type: "set_payoff_board", ids: args.ids, boardId: target.id });
|
|
1969
1989
|
if (named.changed) {
|
|
@@ -2280,6 +2300,7 @@ server.registerTool(
|
|
|
2280
2300
|
inputSchema: { from: z.string(), to: z.string(), kind: arrowKindSchema.optional() },
|
|
2281
2301
|
},
|
|
2282
2302
|
async (args) => {
|
|
2303
|
+
const { boardId } = await readBoard();
|
|
2283
2304
|
const { state, changed, result, live } = await commit({
|
|
2284
2305
|
type: "create_arrow",
|
|
2285
2306
|
from: args.from,
|
|
@@ -2290,13 +2311,19 @@ server.registerTool(
|
|
|
2290
2311
|
// Say which of the three reasons it was. "Something went wrong" makes an
|
|
2291
2312
|
// agent retry the same call; naming the cause makes it fix the input.
|
|
2292
2313
|
const onBoard = (id) => state.notes.some((note) => note.id === id);
|
|
2314
|
+
const { project: wholeProject, boards: allBoards } = await readProject();
|
|
2315
|
+
const elsewhere = (id) => wholeProject.boards.find((meta) => meta.id !== (boardId ?? wholeProject.activeBoardId) && isBoardState(allBoards[meta.id]) && allBoards[meta.id].notes.some((note) => note.id === id));
|
|
2316
|
+
const missing = (id) => {
|
|
2317
|
+
const other = elsewhere(id);
|
|
2318
|
+
return other ? `card ${id} is on another board, "${other.name}" — an arrow stays on one board; a fold that pays off there is set_plant with later: "${other.name}"` : `there is no card with id ${id}`;
|
|
2319
|
+
};
|
|
2293
2320
|
const why =
|
|
2294
2321
|
args.from === args.to
|
|
2295
2322
|
? "a card cannot point at itself"
|
|
2296
2323
|
: !onBoard(args.from)
|
|
2297
|
-
?
|
|
2324
|
+
? missing(args.from)
|
|
2298
2325
|
: !onBoard(args.to)
|
|
2299
|
-
?
|
|
2326
|
+
? missing(args.to)
|
|
2300
2327
|
: "that arrow already exists";
|
|
2301
2328
|
return ok(`No arrow drawn: ${why}. Call list_board to check.`);
|
|
2302
2329
|
}
|
|
@@ -2444,8 +2471,8 @@ server.registerTool(
|
|
|
2444
2471
|
const own = list.filter((item) => !item.builtIn).length;
|
|
2445
2472
|
return ok(
|
|
2446
2473
|
[
|
|
2447
|
-
`reminders on "${project.name}" (${door(live, base)}): ${list.length} — ${list.length - own} the house principles the app starts with (built in), ${own} the writer's own${own === 0 ? "; add_reminder adds one the writer asks to keep" : ""}`,
|
|
2448
|
-
...list.map((item) => ` - ${item.id}${item.builtIn ? " (built in)" : ""} — ${item.title}: ${item.body}`),
|
|
2474
|
+
`reminders on "${project.name}" (${door(live, base)}): ${list.length} — ${list.length - own} the house principles the app starts with (built in), ${own} the writer's own${own === 0 ? "; add_reminder adds one the writer asks to keep" : ""}. Reminders live on the project and go with it`,
|
|
2475
|
+
...list.map((item) => ` - ${item.id}${item.builtIn ? " (built in)" : ""} — ${item.body.replace(/\.$/, "").startsWith(item.title.replace(/\.$/, "")) ? item.body : `${item.title}: ${item.body}`}`),
|
|
2449
2476
|
].join("\n"),
|
|
2450
2477
|
list,
|
|
2451
2478
|
);
|
|
@@ -2584,12 +2611,13 @@ server.registerTool(
|
|
|
2584
2611
|
title: "Start a project",
|
|
2585
2612
|
description:
|
|
2586
2613
|
"Through the account door: start a new project of the writer's with this name — one empty board, nothing on it — and work it from now on. The writer sees it under Projects on every device.",
|
|
2587
|
-
inputSchema: { name: z.string().min(1), pages: pagesSchema.optional(), minutes: z.number().positive().optional() },
|
|
2614
|
+
inputSchema: { name: z.string().min(1), board: z.string().optional(), pages: pagesSchema.optional(), minutes: z.number().positive().optional() },
|
|
2588
2615
|
},
|
|
2589
2616
|
async (args) => {
|
|
2590
2617
|
const account = await findAccount();
|
|
2591
2618
|
if (!account) return shut("No account door: there is one project here, the open one. Set PLOTCODER_EMAIL and PLOTCODER_PASSWORD to start another on the writer's account.");
|
|
2592
|
-
|
|
2619
|
+
let record = renameProject(emptyProject(), args.name.trim());
|
|
2620
|
+
if (args.board?.trim()) record = renameBoard(record, record.activeBoardId, args.board.trim());
|
|
2593
2621
|
const inserted = await account.client.from("projects").insert({ id: record.id, record, reminders: null, rev: 1 });
|
|
2594
2622
|
if (inserted.error) return ok(`Could not start the project: ${inserted.error.message}`);
|
|
2595
2623
|
const target = args.pages ?? args.minutes;
|
|
@@ -2645,7 +2673,7 @@ server.registerTool(
|
|
|
2645
2673
|
for (const row of owned) plans.push(await deletionPlan(row));
|
|
2646
2674
|
const survive = shared.length ? ` ${shared.length} project(s) shared with the writer by others stay: ${shared.map((row) => `"${row.record.name}"`).join(", ")}.` : "";
|
|
2647
2675
|
if (plans.length === 0) return ok(`The account holds nothing of the writer's own to delete.${survive}`);
|
|
2648
|
-
if (!args.confirm) return ok(`Emptying the account deletes ${plans.length} project(s) of the writer's own: ${plans.map(describePlan).join("; ")}. Cannot be undone. Ask the writer; export_project each first if they might want them back
|
|
2676
|
+
if (!args.confirm) return ok(`Emptying the account deletes ${plans.length} project(s) of the writer's own: ${plans.map(describePlan).join("; ")}. Cannot be undone. Ask the writer, or if they have already said so, pass confirm: true now; export_project each first if they might want them back.${survive}`, plans);
|
|
2649
2677
|
for (const plan of plans) await deleteProjectRows(plan);
|
|
2650
2678
|
const next = await workWhatIsLeft(plans.map((plan) => plan.id));
|
|
2651
2679
|
return ok(`Emptied the account as ${account.email}: deleted ${plans.map(describePlan).join("; ")}.${survive}${next}`, plans);
|
|
@@ -2708,7 +2736,9 @@ server.registerTool(
|
|
|
2708
2736
|
const { project, boards, reminders } = await readProject();
|
|
2709
2737
|
const file = toProjectFile({ project, boards, reminders: reminders ?? null });
|
|
2710
2738
|
const cards = countCards(boards);
|
|
2711
|
-
const
|
|
2739
|
+
const ownReminders = (reminders ?? []).filter((item) => !item.builtIn).length;
|
|
2740
|
+
const builtInReminders = (reminders ?? []).length - ownReminders;
|
|
2741
|
+
const what = `"${project.name}": ${project.boards.length} board(s) — ${project.boards.map((meta) => `"${meta.name}" (${isBoardState(boards[meta.id]) ? boards[meta.id].notes.length : 0} cards)`).join(", ")} — ${cards} card(s) in all${reminders?.length ? `, ${reminders.length} reminder(s) (${builtInReminders} built in, ${ownReminders} the writer's own)` : ", the six built-in reminders come with every project and no reminders of the writer's own (none to write)"}${project.structures?.length ? `, ${project.structures.length} structure(s)` : ", no structures of the writer's own (none to write)"}. Pictures and takes on the account are not in the file`;
|
|
2712
2742
|
if (args.path) {
|
|
2713
2743
|
fs.mkdirSync(path.dirname(path.resolve(args.path)), { recursive: true });
|
|
2714
2744
|
fs.writeFileSync(args.path, JSON.stringify(file, null, 2));
|
|
@@ -2808,7 +2838,7 @@ server.registerTool(
|
|
|
2808
2838
|
const fresh = { ...emptyState(), ...(target ? { targetEighths: target } : {}) };
|
|
2809
2839
|
const { live } = await openBoardEverywhere(next, { ...boards, [board.id]: fresh }, rev, base, board.id);
|
|
2810
2840
|
return ok(
|
|
2811
|
-
`Added "${board.name}" (${board.id}) and opened it${where(live)}. It is empty. The logline is the story's question when the writer has one — leave it empty rather than invent it — and the cards come next.${next.name === "Untitled project" ? " The project is still \"Untitled project\": rename_project names it." : ""}${next.boards.length === 2 && isSampleWall(isBoardState(boards[next.boards[0].id]) ? normalizeState(boards[next.boards[0].id]) : emptyState()) ? " The sample stays as Board 1; delete_board drops it." : ""}`,
|
|
2841
|
+
`Added "${board.name}" (${board.id}) and opened it${where(live)}: every card call lands there now, and the writer's open wall switched with it; open_board "${project.boards.findIndex((item) => item.id === project.activeBoardId) + 1}" comes back. It is empty. The logline is the story's question when the writer has one — leave it empty rather than invent it — and the cards come next.${next.name === "Untitled project" ? " The project is still \"Untitled project\": rename_project names it." : ""}${next.boards.length === 2 && isSampleWall(isBoardState(boards[next.boards[0].id]) ? normalizeState(boards[next.boards[0].id]) : emptyState()) ? " The sample stays as Board 1; delete_board drops it." : ""}`,
|
|
2812
2842
|
board,
|
|
2813
2843
|
);
|
|
2814
2844
|
},
|
package/src/board/agents.js
CHANGED
|
@@ -42,12 +42,12 @@ 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: "
|
|
45
|
+
firstNote: "Make these four before anything else; none depends on another, so any order is fine. They 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
|
-
{ tool: "read_wall", why: "
|
|
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 — are list_board's. A fresh folder holds a sample wall (Maya, Tom, the letter) and says so; it is not the writer's." },
|
|
49
49
|
{ tool: "list_workflows", why: "what a writer can ask you for." },
|
|
50
|
-
{ tool: "list_reminders", why: "the house principles the app starts with, and the writer's own; read them before you change anything." },
|
|
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
|
],
|
|
52
52
|
rules: [
|
|
53
53
|
"Questions, not fixes, until the writer says.",
|
|
@@ -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
|
|
67
|
+
const lines = ["# PlotCoder — for agents", "", AGENTS.lead, "", `The guide: ${AGENTS.guide}. Read it once, before your first call if you can; it is the whole and this page is its first page, and where the two differ, the guide wins. Then Call these first, below. The doors between are for wiring a server in; skip them when the tools are already in front of you.`, "", "## 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
|
@@ -304,7 +304,10 @@ export function readWall(state) {
|
|
|
304
304
|
.map((arrow) => arrow.to)
|
|
305
305
|
.sort((a, b) => (wallIndex.get(a) ?? Infinity) - (wallIndex.get(b) ?? Infinity));
|
|
306
306
|
// Every payoff, in wall order: a card can plant two things (the ledger
|
|
307
|
-
// pays off at the cash and again at the initials), and both count.
|
|
307
|
+
// pays off at the cash and again at the initials), and both count. A
|
|
308
|
+
// fold that pays off only on another board is not here at all — it is
|
|
309
|
+
// under `later` — so the two never disagree about one card.
|
|
310
|
+
if (!heads.length && note.payoffBoardId) continue;
|
|
308
311
|
payoffs[note.id] = heads;
|
|
309
312
|
}
|
|
310
313
|
// A fold that pays off on another board (R50) is not unpaid: it is listed
|
package/src/board/reducer.js
CHANGED
|
@@ -809,7 +809,8 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
809
809
|
id: newId(),
|
|
810
810
|
headline: item.name,
|
|
811
811
|
change: item.prompt,
|
|
812
|
-
|
|
812
|
+
// One colour: paper means nothing to the app, and a structure is not a pattern (round eleven).
|
|
813
|
+
color: "yellow",
|
|
813
814
|
x: left + (index % 5) * (NOTE_WIDTH + 28),
|
|
814
815
|
y: top + Math.floor(index / 5) * (NOTE_HEIGHT + 40),
|
|
815
816
|
rotate: ((index % 5) - 2) * 0.8,
|
package/src/board/workflows.d.ts
CHANGED
|
@@ -22,5 +22,5 @@ export declare function workflowById(id: string): Workflow | null;
|
|
|
22
22
|
export declare function segmentBrief(
|
|
23
23
|
state: BoardState,
|
|
24
24
|
ids: string[],
|
|
25
|
-
options?: { title?: string },
|
|
25
|
+
options?: { title?: string; boards?: { id: string; name: string }[] },
|
|
26
26
|
): string | null;
|
package/src/board/workflows.js
CHANGED
|
@@ -91,6 +91,7 @@ function personLine(character) {
|
|
|
91
91
|
if (character.voice) lines.push(`voice: ${character.voice}`);
|
|
92
92
|
if (character.wants) lines.push(`wants: ${character.wants}`);
|
|
93
93
|
if (character.needs) lines.push(`needs: ${character.needs}`);
|
|
94
|
+
if (character.notes) lines.push(`notes: ${character.notes}`);
|
|
94
95
|
return `${character.name}${lines.length ? ` — ${lines.join("; ")}` : " — (no page yet)"}`;
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -120,7 +121,12 @@ export function segmentBrief(state, ids, options = {}) {
|
|
|
120
121
|
lines.push("");
|
|
121
122
|
lines.push(`SCENE: ${note.headline}${note.location ? ` — at ${note.location}` : ""}`);
|
|
122
123
|
lines.push(`WHAT CHANGES: ${note.change}`);
|
|
123
|
-
if (note.plants)
|
|
124
|
+
if (note.plants) {
|
|
125
|
+
const heads = state.arrows.filter((arrow) => arrow.kind === "setup" && arrow.from === note.id).map((arrow) => byId.get(arrow.to)?.headline).filter(Boolean);
|
|
126
|
+
const later = note.payoffBoardId ? (options.boards ?? []).find((board) => board.id === note.payoffBoardId)?.name ?? "a later board" : null;
|
|
127
|
+
const where = heads.length ? `pays off at ${heads.map((headline) => `"${headline}"`).join(" and ")}` : later ? `pays off later, on "${later}"` : "pays off later, nowhere yet";
|
|
128
|
+
lines.push(`PLANTS: something here ${where}; keep it visible.`);
|
|
129
|
+
}
|
|
124
130
|
if (note.text && note.text.trim()) {
|
|
125
131
|
lines.push("SCRIPT:");
|
|
126
132
|
lines.push(note.text.trim());
|
|
@@ -129,6 +135,6 @@ export function segmentBrief(state, ids, options = {}) {
|
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
lines.push("");
|
|
132
|
-
lines.push(`AFTER: ${notes.at(-1).change}`);
|
|
138
|
+
lines.push(`AFTER: ${notes.at(-1).change}${notes.length === 1 ? " (the change line, until the scene is written)" : ""}`);
|
|
133
139
|
return lines.join("\n");
|
|
134
140
|
}
|