drafter-mcp 0.25.0 → 0.26.0

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.
Files changed (2) hide show
  1. package/drafter-mcp.js +469 -99
  2. package/package.json +1 -1
package/drafter-mcp.js CHANGED
@@ -23233,7 +23233,7 @@ function toError(value) {
23233
23233
  return value instanceof Error ? value : new Error(String(value));
23234
23234
  }
23235
23235
  // package.json
23236
- var version = "0.25.0";
23236
+ var version = "0.26.0";
23237
23237
  // ../fountain/src/inline.ts
23238
23238
  const MARKERS$1 = /[*_\\]/;
23239
23239
  const isSpace$10 = (code) => code === void 0 || /\s/.test(String.fromCharCode(code));
@@ -25892,7 +25892,7 @@ function columns(s) {
25892
25892
  return n;
25893
25893
  }
25894
25894
  const joined = (runs) => runs.map((run) => run.text).join("");
25895
- const isBlank$1 = (line) => joined(line.runs).trim() === "" && (!line.right || joined(line.right.runs).trim() === "");
25895
+ const isBlank$2 = (line) => joined(line.runs).trim() === "" && (!line.right || joined(line.right.runs).trim() === "");
25896
25896
  function colFor(frame, length) {
25897
25897
  if (frame.align === "center") return frame.indent + Math.floor((frame.width - length) / 2);
25898
25898
  if (frame.align === "right") return frame.indent + frame.width - length;
@@ -26072,7 +26072,7 @@ function titlePage(tp, template, height, diagnostics) {
26072
26072
  page: {
26073
26073
  number: 0,
26074
26074
  lines: rows.map((row, k) => {
26075
- if (row && !isBlank$1(row)) last = k;
26075
+ if (row && !isBlank$2(row)) last = k;
26076
26076
  return row ?? blankLine(tp.span.from);
26077
26077
  }),
26078
26078
  sceneNumbersLeft: [],
@@ -26293,7 +26293,7 @@ var Items = class {
26293
26293
  lines,
26294
26294
  spaceBefore: p.style.spaceBefore,
26295
26295
  spaceAfter: p.style.spaceAfter ?? 0,
26296
- keepWithNext: p.style.keepWithNext === true || lines.every(isBlank$1),
26296
+ keepWithNext: p.style.keepWithNext === true || lines.every(isBlank$2),
26297
26297
  keepTogether: p.style.keepTogether === true
26298
26298
  };
26299
26299
  }
@@ -26324,7 +26324,7 @@ var Items = class {
26324
26324
  if (!p) continue;
26325
26325
  const kind = this.#blocks[m].kind;
26326
26326
  for (const line of this.#lines(m, p)) {
26327
- const tag = isBlank$1(line) ? "gap" : kind === "parenthetical" ? "paren" : "text";
26327
+ const tag = isBlank$2(line) ? "gap" : kind === "parenthetical" ? "paren" : "text";
26328
26328
  body.push(line);
26329
26329
  tags.push(tag);
26330
26330
  spoken.push(spoken.at(-1) + (tag === "text" ? 1 : 0));
@@ -26453,7 +26453,7 @@ var Filler = class {
26453
26453
  */
26454
26454
  #canStart(item, room, space) {
26455
26455
  if (item.type === "block") {
26456
- const lead = Math.max(0, item.lines.findIndex((line) => !isBlank$1(line)));
26456
+ const lead = Math.max(0, item.lines.findIndex((line) => !isBlank$2(line)));
26457
26457
  return space + (item.keepTogether ? item.lines.length : Math.min(item.lines.length, lead + this.template.keepWithNextLines)) <= room;
26458
26458
  }
26459
26459
  if (item.type === "dual") return space + item.rows.length <= room;
@@ -26593,7 +26593,7 @@ var Pager = class {
26593
26593
  /** Adds a line, starting a new page when this one is full. A page never starts blank. */
26594
26594
  put(line) {
26595
26595
  if (this.#lines.length >= this.height) this.newPage();
26596
- const blank = isBlank$1(line);
26596
+ const blank = isBlank$2(line);
26597
26597
  if (blank && this.#lines.length === 0) return;
26598
26598
  const at = {
26599
26599
  page: this.pages.length + 1,
@@ -30174,12 +30174,44 @@ async function imported(run) {
30174
30174
  return got;
30175
30175
  }
30176
30176
  /**
30177
+ * The most paragraphs (text between blank lines) a file may have to be read: a long feature
30178
+ * script has a few thousand. Past this, a file's parse can outgrow a reader's heap
30179
+ * (READER_HEAP_MB), and in list_scripts or search_scripts the reader stopping would fail the whole
30180
+ * folder; so the file is refused alone, as too long (red/blue audit, 2026-11-03: a 3 MB file of
30181
+ * 540,000 one-line scenes and actions stopped every listing, where 360,000 still listed).
30182
+ */
30183
+ const MAX_PARAGRAPHS = 3e5;
30184
+ /** Whether `text` has more than `max` paragraphs (lines ending in LF, CR, or CRLF), in one pass. */
30185
+ function moreParagraphsThan(text, max) {
30186
+ let paragraphs = 0;
30187
+ let blank = true;
30188
+ let lineBlank = true;
30189
+ for (let i = 0; i < text.length; i++) {
30190
+ const code = text.charCodeAt(i);
30191
+ if (code === 10 || code === 13) {
30192
+ if (code === 13 && text.charCodeAt(i + 1) === 10) i++;
30193
+ blank = lineBlank;
30194
+ lineBlank = true;
30195
+ } else if (code !== 32 && code !== 9 && lineBlank) {
30196
+ lineBlank = false;
30197
+ if (blank && ++paragraphs > max) return true;
30198
+ blank = false;
30199
+ }
30200
+ }
30201
+ return false;
30202
+ }
30203
+ /**
30177
30204
  * The script inside `file`, named `name` (its shown name, which is all a refusal says), judged in
30178
30205
  * Open's order. Throws NotRead when its bytes couldn't be read, NotOpened in Open's words for a
30179
30206
  * file Open turns away, and Refused (the reader's own phrase) for text Open won't read: over 5 MB,
30180
- * not UTF-8, more than text.
30207
+ * not UTF-8, more than text, or more than MAX_PARAGRAPHS paragraphs.
30181
30208
  */
30182
30209
  async function readInside(file, name) {
30210
+ const inside = await readWhole(file, name);
30211
+ if (moreParagraphsThan(inside.text, 3e5)) throw new Refused(`it has more than ${MAX_PARAGRAPHS.toLocaleString("en-US")} paragraphs`);
30212
+ return inside;
30213
+ }
30214
+ async function readWhole(file, name) {
30183
30215
  const kind = importKindOf(name);
30184
30216
  const read = {
30185
30217
  noFollow: true,
@@ -30894,12 +30926,13 @@ function signatureLines(candidates, used = () => false) {
30894
30926
  }
30895
30927
  // ../story-map/src/mailto.ts
30896
30928
  /** What a contact may never hold: spaces, controls, and anything that ends or reshapes a URL. */
30897
- const REFUSED = /[\s?&"<>:\\/\u0000-\u001f\u007f]/;
30929
+ const REFUSED = /[\s?&"<>:\\/,;%\u0000-\u001f\u007f]/;
30898
30930
  /**
30899
30931
  * A `mailto:` link for a contact the page may link (design §10.8): one `@`, neither first nor
30900
30932
  * last; a dot in the part after it, with something on both sides of it; and no space, control
30901
- * character, `?` or `&` (which would add headers or a body to the mail), quote, angle bracket,
30902
- * colon, or slash (which could end the attribute or make it another kind of link). Else null, and
30933
+ * character, `?` or `&` (which would add headers or a body to the mail), comma, semicolon, or
30934
+ * `%` (which would add another address, plainly or encoded: red/blue audit, 2026-11-03), quote,
30935
+ * angle bracket, colon, or slash (which could end the attribute or make it another kind of link). Else null, and
30903
30936
  * the page shows the contact as text.
30904
30937
  */
30905
30938
  function mailtoOf(contact) {
@@ -30916,18 +30949,18 @@ const MOST_HOOK = 200;
30916
30949
  /** The planned length's bounds, in pages. */
30917
30950
  const FEWEST_PAGES = 1;
30918
30951
  const MOST_PAGES = 999;
30919
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
30952
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
30920
30953
  /** The `storyMap` part of a parsed metadata block (`doc.meta`), read strictly. */
30921
30954
  function storyMapMeta(meta) {
30922
30955
  const hooks = Object.create(null);
30923
- const part = isRecord(meta) ? meta.storyMap : void 0;
30924
- if (!isRecord(part)) return {
30956
+ const part = isRecord$1(meta) ? meta.storyMap : void 0;
30957
+ if (!isRecord$1(part)) return {
30925
30958
  plannedPages: null,
30926
30959
  hooks
30927
30960
  };
30928
30961
  const pages = part.plannedPages;
30929
30962
  const plannedPages = typeof pages === "number" && Number.isInteger(pages) && pages >= FEWEST_PAGES && pages <= MOST_PAGES ? pages : null;
30930
- if (isRecord(part.hooks)) {
30963
+ if (isRecord$1(part.hooks)) {
30931
30964
  for (const [key, hook] of Object.entries(part.hooks)) if (typeof hook === "string" && hook.length <= MOST_HOOK) hooks[key] = hook;
30932
30965
  }
30933
30966
  return {
@@ -31492,6 +31525,66 @@ function shares(amounts) {
31492
31525
  }
31493
31526
  return whole;
31494
31527
  }
31528
+ // ../markdown/src/meta.ts
31529
+ const OPENER = "<!-- drafter ";
31530
+ const CLOSER = " -->";
31531
+ function isSpace$6(code) {
31532
+ return code === 32 || code === 9 || code === 10 || code === 13;
31533
+ }
31534
+ /**
31535
+ * The document's metadata block: its last line (trailing spaces and blank lines aside), starting
31536
+ * at the line's start (so not indented code) as `<!-- drafter `, ending ` -->`, and holding a JSON
31537
+ * object. Anything else, a comment with words after it, or one whose JSON doesn't read as an
31538
+ * object, is the writer's own text. One backwards scan, then one JSON parse: linear.
31539
+ */
31540
+ function findMarkdownMeta(text) {
31541
+ let end = text.length;
31542
+ while (end > 0 && isSpace$6(text.charCodeAt(end - 1))) end--;
31543
+ const from = text.lastIndexOf("\n", end - 1) + 1;
31544
+ if (end - from < 17) return null;
31545
+ if (!text.startsWith(OPENER, from) || !text.startsWith(CLOSER, end - 4)) return null;
31546
+ let meta;
31547
+ try {
31548
+ meta = JSON.parse(text.slice(from + 13, end - 4));
31549
+ } catch {
31550
+ return null;
31551
+ }
31552
+ if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return null;
31553
+ return {
31554
+ from,
31555
+ to: end,
31556
+ meta
31557
+ };
31558
+ }
31559
+ /**
31560
+ * The document's text and its metadata. The body is the text without the block, the line break
31561
+ * after it, and the one blank line before it that `setMarkdownMeta` wrote, so a body written with
31562
+ * `setMarkdownMeta` comes back exactly. No block: the whole text, and `meta: null`.
31563
+ */
31564
+ function readMarkdownMeta(text) {
31565
+ const block = findMarkdownMeta(text);
31566
+ if (!block) return {
31567
+ body: text,
31568
+ meta: null
31569
+ };
31570
+ const before = text.slice(0, block.from);
31571
+ const nl = text.startsWith("\r\n", block.to) ? "\r\n" : text.startsWith("\n", block.to) ? "\n" : before.endsWith("\r\n\r\n") ? "\r\n" : "\n";
31572
+ const gap = nl + nl;
31573
+ return {
31574
+ body: before.endsWith(gap) ? before.slice(0, -gap.length) : before,
31575
+ meta: block.meta
31576
+ };
31577
+ }
31578
+ /**
31579
+ * The document as prose: its body (`readMarkdownMeta`), the text without its trailing metadata
31580
+ * block, the line breaks after it, and the blank line `setMarkdownMeta` wrote before it. The block
31581
+ * is Drafter's, not the writer's: the Preview, Print, and every export draw the prose, the word
31582
+ * counts count it, and the cut finder and Compare compare it. The prose is the text's start, so an
31583
+ * offset into it is the same offset into the text. No block: the whole text.
31584
+ */
31585
+ function markdownProse(text) {
31586
+ return readMarkdownMeta(text).body;
31587
+ }
31495
31588
  // ../markdown/src/words.ts
31496
31589
  /**
31497
31590
  * Whether a UTF-16 code unit is one that `/\s/` matches: the ASCII spaces (tab, line feed,
@@ -31500,7 +31593,7 @@ function shares(amounts) {
31500
31593
  * medium mathematical space, the ideographic space, and the byte-order mark. A comparison per
31501
31594
  * character, where a regular expression per character cost more than the reparse on a long script.
31502
31595
  */
31503
- function isSpace$6(code) {
31596
+ function isSpace$5(code) {
31504
31597
  if (code <= 32) return code === 32 || code >= 9 && code <= 13;
31505
31598
  if (code < 160) return false;
31506
31599
  return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
@@ -31510,7 +31603,7 @@ function wordsIn(text) {
31510
31603
  let count = 0;
31511
31604
  let inWord = false;
31512
31605
  for (let i = 0; i < text.length; i++) {
31513
- const space = isSpace$6(text.charCodeAt(i));
31606
+ const space = isSpace$5(text.charCodeAt(i));
31514
31607
  if (!space && !inWord) count++;
31515
31608
  inWord = !space;
31516
31609
  }
@@ -31547,7 +31640,7 @@ const SMALL = /* @__PURE__ */ new Set([
31547
31640
  const cased = (c) => c.toLowerCase() !== c.toUpperCase();
31548
31641
  const isUpper$1 = (c) => cased(c) && c === c.toUpperCase();
31549
31642
  const isLower = (c) => cased(c) && c === c.toLowerCase();
31550
- const isSpace$5 = (c) => c !== void 0 && c !== "" && c.trim() === "";
31643
+ const isSpace$4 = (c) => c !== void 0 && c !== "" && c.trim() === "";
31551
31644
  /** One word in title case: its first letter, and the first after a hyphen, in capitals. */
31552
31645
  function titleWord(word, first) {
31553
31646
  const chars = Array.from(word);
@@ -31570,7 +31663,7 @@ function titleWord(word, first) {
31570
31663
  function titleCase(text) {
31571
31664
  let first = true;
31572
31665
  return text.split(/(\s+)/).map((part) => {
31573
- if (part === "" || isSpace$5(part[0])) return part;
31666
+ if (part === "" || isSpace$4(part[0])) return part;
31574
31667
  const word = titleWord(part, first);
31575
31668
  first = false;
31576
31669
  return word;
@@ -31598,7 +31691,7 @@ function withoutNumber(text) {
31598
31691
  function slashParts(part) {
31599
31692
  const out = [];
31600
31693
  let from = 0;
31601
- for (let i = 0; i < part.length; i++) if (part[i] === "/" && (isSpace$5(part[i - 1]) || isSpace$5(part[i + 1]))) {
31694
+ for (let i = 0; i < part.length; i++) if (part[i] === "/" && (isSpace$4(part[i - 1]) || isSpace$4(part[i + 1]))) {
31602
31695
  out.push(part.slice(from, i));
31603
31696
  from = i + 1;
31604
31697
  }
@@ -31614,7 +31707,7 @@ function placeWords(location) {
31614
31707
  const text = withoutNumber(location);
31615
31708
  const parts = [];
31616
31709
  let from = 0;
31617
- for (let i = 1; i < text.length - 1; i++) if (DASHES$1.has(text[i]) && isSpace$5(text[i - 1]) && isSpace$5(text[i + 1])) {
31710
+ for (let i = 1; i < text.length - 1; i++) if (DASHES$1.has(text[i]) && isSpace$4(text[i - 1]) && isSpace$4(text[i + 1])) {
31618
31711
  parts.push(text.slice(from, i));
31619
31712
  from = i + 1;
31620
31713
  }
@@ -31629,7 +31722,7 @@ const followsOn = (key) => key.startsWith("LATER ") || key.endsWith(" LATER");
31629
31722
  */
31630
31723
  function followOn(location) {
31631
31724
  const text = location.trimEnd();
31632
- for (let i = text.length - 2; i > 0; i--) if (DASHES$1.has(text[i]) && isSpace$5(text[i - 1]) && isSpace$5(text[i + 1])) {
31725
+ for (let i = text.length - 2; i > 0; i--) if (DASHES$1.has(text[i]) && isSpace$4(text[i - 1]) && isSpace$4(text[i + 1])) {
31633
31726
  const part = text.slice(i + 1).trim();
31634
31727
  if (!followsOn(part.toUpperCase().split(/\s+/).join(" "))) break;
31635
31728
  return {
@@ -31795,7 +31888,7 @@ const TRAILING = /* @__PURE__ */ new Set([
31795
31888
  ]);
31796
31889
  /** A comma or dashes just after a name (or its age), and the spaces after them: anchored, linear. */
31797
31890
  const LEAD = /^[,\-–—―][\s,\-–—―]*/u;
31798
- const isSpace$4 = (c) => c !== void 0 && c.trim() === "";
31891
+ const isSpace$3 = (c) => c !== void 0 && c.trim() === "";
31799
31892
  const isLetter = (c) => c !== void 0 && /\p{L}/u.test(c);
31800
31893
  const isStop = (c) => c === "." || c === "!" || c === "?";
31801
31894
  /** Whether the period at `i` closes an abbreviation or an initial (`Dr.`, `J.`). */
@@ -31818,7 +31911,7 @@ function sentenceEnd(text, from = 0, limit = text.length) {
31818
31911
  let j = i + 1;
31819
31912
  while (j < text.length && isStop(text[j])) j++;
31820
31913
  while (j < text.length && CLOSERS.has(text[j])) j++;
31821
- if (j < text.length && !isSpace$4(text[j])) {
31914
+ if (j < text.length && !isSpace$3(text[j])) {
31822
31915
  i = j - 1;
31823
31916
  continue;
31824
31917
  }
@@ -31943,7 +32036,7 @@ function nameCase(text, map, names, ages = true) {
31943
32036
  at += 2;
31944
32037
  }
31945
32038
  if (!ages) {
31946
- const age = ageAt(text, isSpace$4(text[at]) ? at + 1 : at);
32039
+ const age = ageAt(text, isSpace$3(text[at]) ? at + 1 : at);
31947
32040
  if (age) at = age.end;
31948
32041
  }
31949
32042
  }
@@ -31964,7 +32057,7 @@ function withoutMarks(text) {
31964
32057
  return text.replace(SCREEN_MARKS, (mark, at) => {
31965
32058
  if (mark.startsWith("(") || !mark.endsWith(".")) return GONE;
31966
32059
  let next = at + mark.length;
31967
- while (isSpace$4(text[next])) next++;
32060
+ while (isSpace$3(text[next])) next++;
31968
32061
  const c = text[next];
31969
32062
  return c === void 0 || c !== c.toLowerCase() && c === c.toUpperCase() ? STOP_KEPT : GONE;
31970
32063
  }).replace(/\s+/g, " ").replace(/ ?\uE001(?=[,.;:!?])/g, "").replace(/\uE001/g, "").replace(/ ?\uE000/g, ".").replace(/\s+/g, " ").trim();
@@ -32022,7 +32115,7 @@ function pastCapitals(text, at) {
32022
32115
  let i = at;
32023
32116
  for (let n = 0; n < NAME_WORDS; n++) {
32024
32117
  let j = i;
32025
- while (isSpace$4(text[j])) j++;
32118
+ while (isSpace$3(text[j])) j++;
32026
32119
  let k = j;
32027
32120
  while (k < text.length && /[\p{L}\p{M}'’.-]/u.test(text[k])) k++;
32028
32121
  const word = text.slice(j, k).replace(/\.$/, "");
@@ -32039,11 +32132,11 @@ function pastCapitals(text, at) {
32039
32132
  */
32040
32133
  function introducedAt(text, at) {
32041
32134
  let j = pastCapitals(text, at);
32042
- while (isSpace$4(text[j])) j++;
32135
+ while (isSpace$3(text[j])) j++;
32043
32136
  if (ageAt(text, j)) return true;
32044
32137
  const aside = asideEnd(text, j);
32045
32138
  if (aside >= 0) j = aside;
32046
- while (isSpace$4(text[j])) j++;
32139
+ while (isSpace$3(text[j])) j++;
32047
32140
  const c = text[j];
32048
32141
  return c === "," || c === "-" || c !== void 0 && DASHES.has(c);
32049
32142
  }
@@ -32054,7 +32147,7 @@ function introducedAt(text, at) {
32054
32147
  function hookFrom(text, at, map, names) {
32055
32148
  let i = pastCapitals(text, at);
32056
32149
  let j = i;
32057
- while (isSpace$4(text[j])) j++;
32150
+ while (isSpace$3(text[j])) j++;
32058
32151
  const aged = ageAt(text, j);
32059
32152
  const aside = aged ? aged.end : asideEnd(text, j);
32060
32153
  if (aside >= 0) i = aside;
@@ -32065,7 +32158,7 @@ function hookFrom(text, at, map, names) {
32065
32158
  rest = rest.slice(lead.length);
32066
32159
  if (lead !== "") for (let k = 0; k < rest.length; k++) {
32067
32160
  const c = rest[k];
32068
- const hyphen = c === "-" && (rest[k + 1] === "-" || isSpace$4(rest[k - 1]) && isSpace$4(rest[k + 1]));
32161
+ const hyphen = c === "-" && (rest[k + 1] === "-" || isSpace$3(rest[k - 1]) && isSpace$3(rest[k + 1]));
32069
32162
  if (c === "," || DASHES.has(c) || hyphen) {
32070
32163
  rest = rest.slice(0, k);
32071
32164
  break;
@@ -33388,7 +33481,7 @@ function groupRows(rows, oldAnchors, newAnchors) {
33388
33481
  });
33389
33482
  return groups;
33390
33483
  }
33391
- const FENCE$1 = /^ {0,3}(`{3,}|~{3,})/;
33484
+ const FENCE$2 = /^ {0,3}(`{3,}|~{3,})/;
33392
33485
  const HEADING = /^ {0,3}(#{1,3})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
33393
33486
  /**
33394
33487
  * The lines of a Markdown text that start a heading of level 1 to 3, with its text, skipping fenced
@@ -33398,7 +33491,7 @@ function markdownAnchors(text) {
33398
33491
  const anchors = [];
33399
33492
  let fence = null;
33400
33493
  splitLines(text).lines.forEach((line, i) => {
33401
- const open = FENCE$1.exec(line);
33494
+ const open = FENCE$2.exec(line);
33402
33495
  if (fence !== null) {
33403
33496
  if (open && open[1][0] === fence[0] && open[1].length >= fence.length && !line.slice(open[0].length).trim()) fence = null;
33404
33497
  return;
@@ -33417,39 +33510,8 @@ function markdownAnchors(text) {
33417
33510
  });
33418
33511
  return anchors;
33419
33512
  }
33420
- // ../markdown/src/meta.ts
33421
- const OPENER = "<!-- drafter ";
33422
- const CLOSER = " -->";
33423
- function isSpace$3(code) {
33424
- return code === 32 || code === 9 || code === 10 || code === 13;
33425
- }
33426
- /**
33427
- * The document's metadata block: its last line (trailing spaces and blank lines aside), starting
33428
- * at the line's start (so not indented code) as `<!-- drafter `, ending ` -->`, and holding a JSON
33429
- * object. Anything else, a comment with words after it, or one whose JSON doesn't read as an
33430
- * object, is the writer's own text. One backwards scan, then one JSON parse: linear.
33431
- */
33432
- function findMarkdownMeta(text) {
33433
- let end = text.length;
33434
- while (end > 0 && isSpace$3(text.charCodeAt(end - 1))) end--;
33435
- const from = text.lastIndexOf("\n", end - 1) + 1;
33436
- if (end - from < 17) return null;
33437
- if (!text.startsWith(OPENER, from) || !text.startsWith(CLOSER, end - 4)) return null;
33438
- let meta;
33439
- try {
33440
- meta = JSON.parse(text.slice(from + 13, end - 4));
33441
- } catch {
33442
- return null;
33443
- }
33444
- if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return null;
33445
- return {
33446
- from,
33447
- to: end,
33448
- meta
33449
- };
33450
- }
33451
33513
  // ../markdown/src/title.ts
33452
- const isBlank = (c) => c === " " || c === " ";
33514
+ const isBlank$1 = (c) => c === " " || c === " ";
33453
33515
  /**
33454
33516
  * An ATX heading's parts, read by hand: up to three spaces, 1 to 6 `#` (`prefix`), then a space or
33455
33517
  * a tab or the line's end, and the text, without the blanks around it or a closing run of `#`s (one
@@ -33463,23 +33525,23 @@ function atx(line) {
33463
33525
  if (hashes < 1 || hashes > 6) return void 0;
33464
33526
  const prefix = line.slice(0, i + hashes);
33465
33527
  i += hashes;
33466
- if (i < line.length && !isBlank(line[i])) return void 0;
33528
+ if (i < line.length && !isBlank$1(line[i])) return void 0;
33467
33529
  let start = i;
33468
- while (start < line.length && isBlank(line[start])) start++;
33530
+ while (start < line.length && isBlank$1(line[start])) start++;
33469
33531
  let end = line.length;
33470
- while (end > start && isBlank(line[end - 1])) end--;
33532
+ while (end > start && isBlank$1(line[end - 1])) end--;
33471
33533
  let close = end;
33472
33534
  while (close > start && line[close - 1] === "#") close--;
33473
- if (close < end && (close === start || isBlank(line[close - 1]))) {
33535
+ if (close < end && (close === start || isBlank$1(line[close - 1]))) {
33474
33536
  end = close;
33475
- while (end > start && isBlank(line[end - 1])) end--;
33537
+ while (end > start && isBlank$1(line[end - 1])) end--;
33476
33538
  }
33477
33539
  return {
33478
33540
  prefix,
33479
33541
  text: line.slice(start, end)
33480
33542
  };
33481
33543
  }
33482
- const FENCE = /^ {0,3}(`{3,}|~{3,})/;
33544
+ const FENCE$1 = /^ {0,3}(`{3,}|~{3,})/;
33483
33545
  const SETEXT = /^ {0,3}(?:=+|-+)[ \t]*$/;
33484
33546
  /**
33485
33547
  * The label of a footnote definition at a line's start, after any indentation, quote marks, and
@@ -33488,15 +33550,15 @@ const SETEXT = /^ {0,3}(?:=+|-+)[ \t]*$/;
33488
33550
  function definitionLabel(line) {
33489
33551
  let i = 0;
33490
33552
  for (;;) {
33491
- while (isBlank(line[i]) || line[i] === ">") i++;
33553
+ while (isBlank$1(line[i]) || line[i] === ">") i++;
33492
33554
  const c = line[i];
33493
- if ((c === "-" || c === "*" || c === "+") && isBlank(line[i + 1])) {
33555
+ if ((c === "-" || c === "*" || c === "+") && isBlank$1(line[i + 1])) {
33494
33556
  i++;
33495
33557
  continue;
33496
33558
  }
33497
33559
  let j = i;
33498
33560
  while (j - i < 9 && (line[j] ?? "") >= "0" && (line[j] ?? "") <= "9") j++;
33499
- if (j > i && (line[j] === "." || line[j] === ")") && isBlank(line[j + 1])) {
33561
+ if (j > i && (line[j] === "." || line[j] === ")") && isBlank$1(line[j + 1])) {
33500
33562
  i = j + 1;
33501
33563
  continue;
33502
33564
  }
@@ -33519,7 +33581,7 @@ function definedLabels(source) {
33519
33581
  if (end < 0) end = source.length;
33520
33582
  const line = source.slice(start, end);
33521
33583
  start = end + 1;
33522
- const opens = FENCE.exec(line);
33584
+ const opens = FENCE$1.exec(line);
33523
33585
  if (fence !== void 0) {
33524
33586
  if (opens?.[1]?.startsWith(fence.charAt(0)) && opens[1].length >= fence.length) fence = void 0;
33525
33587
  continue;
@@ -33636,7 +33698,7 @@ function squeeze(text) {
33636
33698
  for (let i = 0; i <= text.length; i++) {
33637
33699
  const c = text[i];
33638
33700
  const mark = c === "*" || c === "_" || c === "`";
33639
- const blank = c !== void 0 && !mark && isSpace$6(text.charCodeAt(i));
33701
+ const blank = c !== void 0 && !mark && isSpace$5(text.charCodeAt(i));
33640
33702
  if (c !== void 0 && !mark && !blank) {
33641
33703
  if (from < 0) {
33642
33704
  if (space && parts.length > 0) parts.push(" ");
@@ -33675,7 +33737,7 @@ function markdownTitle(source) {
33675
33737
  if (end < 0) end = source.length;
33676
33738
  const line = source.slice(start, end);
33677
33739
  start = end + 1;
33678
- const opens = FENCE.exec(line);
33740
+ const opens = FENCE$1.exec(line);
33679
33741
  if (fence !== void 0) {
33680
33742
  if (opens?.[1]?.startsWith(fence.charAt(0)) && opens[1].length >= fence.length) fence = void 0;
33681
33743
  before = "";
@@ -33697,6 +33759,212 @@ function markdownTitle(source) {
33697
33759
  before = line.trim() === "" || /^ {4}/.test(line) ? "" : line;
33698
33760
  }
33699
33761
  }
33762
+ // ../novel/src/structure.ts
33763
+ const RANK = {
33764
+ part: 0,
33765
+ chapter: 1,
33766
+ section: 2
33767
+ };
33768
+ const FENCE = /^ {0,3}(`{3,}|~{3,})/;
33769
+ const ATX = /^ {0,3}(#{1,6})(?:[ \t]([^\n]*))?$/;
33770
+ const BREAK = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
33771
+ const SETEXT_1 = /^ {0,3}=+[ \t]*$/;
33772
+ const SETEXT_2 = /^ {0,3}-+[ \t]*$/;
33773
+ const NOT_PARAGRAPH_START = /^(?: {4}|\t|<!--)/;
33774
+ const CONTAINER_START = /^ {0,3}(?:>|[-*+](?:[ \t]|$)|\d{1,9}[.)](?:[ \t]|$))/;
33775
+ const PART = /^part(?:\s|$)/i;
33776
+ function isBlank(char) {
33777
+ return char === " " || char === " " || char === "\r";
33778
+ }
33779
+ /** An ATX heading's text: trimmed, without a closing run of `#` (which a space or tab must precede). */
33780
+ function atxTitle(rest) {
33781
+ let a = 0;
33782
+ let b = rest.length;
33783
+ while (a < b && isBlank(rest[a])) a++;
33784
+ while (b > a && isBlank(rest[b - 1])) b--;
33785
+ let c = b;
33786
+ while (c > a && rest[c - 1] === "#") c--;
33787
+ if (c < b && (c === a || isBlank(rest[c - 1]))) {
33788
+ b = c;
33789
+ while (b > a && isBlank(rest[b - 1])) b--;
33790
+ }
33791
+ return rest.slice(a, b);
33792
+ }
33793
+ function slug(title) {
33794
+ return title.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
33795
+ }
33796
+ /** Reads a novel's structure in one pass. The metadata comment, and what it holds, is not read. */
33797
+ function novelStructure(text) {
33798
+ const end = findMarkdownMeta(text)?.from ?? text.length;
33799
+ const roots = [];
33800
+ const seen = /* @__PURE__ */ new Map();
33801
+ const stack = [];
33802
+ let firstHeading = -1;
33803
+ let fence = null;
33804
+ let paraStart = -1;
33805
+ let paraText = "";
33806
+ let container = false;
33807
+ const open = (wanted, title, from, to) => {
33808
+ if (firstHeading < 0) firstHeading = from;
33809
+ const top = stack.length > 0 ? stack[stack.length - 1].node.kind : null;
33810
+ const kind = wanted === "section" && (top === null || top === "part") ? "chapter" : wanted;
33811
+ while (stack.length > 0 && RANK[stack[stack.length - 1].node.kind] >= RANK[kind]) {
33812
+ const done = stack.pop();
33813
+ done.node.span.to = from;
33814
+ }
33815
+ const siblings = stack.length > 0 ? stack[stack.length - 1].node.children : roots;
33816
+ const key = `${kind}:${slug(title)}`;
33817
+ const n = seen.get(key) ?? 0;
33818
+ seen.set(key, n + 1);
33819
+ const node = {
33820
+ id: `${key}:${n}`,
33821
+ kind,
33822
+ title,
33823
+ heading: {
33824
+ from,
33825
+ to
33826
+ },
33827
+ span: {
33828
+ from,
33829
+ to: end
33830
+ },
33831
+ words: 0,
33832
+ ownWords: 0,
33833
+ children: []
33834
+ };
33835
+ siblings.push(node);
33836
+ stack.push({ node });
33837
+ };
33838
+ let pos = 0;
33839
+ while (pos < end) {
33840
+ let eol = text.indexOf("\n", pos);
33841
+ if (eol < 0 || eol > end) eol = end;
33842
+ const next = eol < end ? eol + 1 : end;
33843
+ let lineEnd = eol;
33844
+ if (lineEnd > pos && text.charCodeAt(lineEnd - 1) === 13) lineEnd--;
33845
+ const line = text.slice(pos, lineEnd);
33846
+ const fenceMatch = FENCE.exec(line);
33847
+ if (fence) {
33848
+ if (fenceMatch && fenceMatch[1][0] === fence.char && fenceMatch[1].length >= fence.size && line.trim() === fenceMatch[1]) fence = null;
33849
+ paraStart = -1;
33850
+ } else if (fenceMatch && !(fenceMatch[1][0] === "`" && line.includes("`", fenceMatch[0].length))) {
33851
+ fence = {
33852
+ char: fenceMatch[1][0],
33853
+ size: fenceMatch[1].length
33854
+ };
33855
+ paraStart = -1;
33856
+ } else if (line.trim() === "") {
33857
+ paraStart = -1;
33858
+ container = false;
33859
+ } else {
33860
+ const atx = ATX.exec(line);
33861
+ if (atx) {
33862
+ paraStart = -1;
33863
+ const level = atx[1].length;
33864
+ if (level <= 2) {
33865
+ const title = atxTitle(atx[2] ?? "");
33866
+ open(level === 2 ? "section" : PART.test(title) ? "part" : "chapter", title, pos, lineEnd);
33867
+ }
33868
+ } else if (paraStart >= 0 && SETEXT_1.test(line)) {
33869
+ open(PART.test(paraText) ? "part" : "chapter", paraText, paraStart, lineEnd);
33870
+ paraStart = -1;
33871
+ } else if (paraStart >= 0 && SETEXT_2.test(line)) {
33872
+ open("section", paraText, paraStart, lineEnd);
33873
+ paraStart = -1;
33874
+ } else if (BREAK.test(line)) {
33875
+ paraStart = -1;
33876
+ if (stack.length > 0) open("section", "", pos, lineEnd);
33877
+ } else if (CONTAINER_START.test(line)) {
33878
+ paraStart = -1;
33879
+ container = true;
33880
+ } else if (container) {} else if (paraStart >= 0) paraText += ` ${line.trim()}`;
33881
+ else if (!NOT_PARAGRAPH_START.test(line)) {
33882
+ paraStart = pos;
33883
+ paraText = line.trim();
33884
+ }
33885
+ }
33886
+ pos = next;
33887
+ }
33888
+ const count = (node) => {
33889
+ const own = node.children.length > 0 ? node.children[0].heading.from : node.span.to;
33890
+ node.ownWords = wordsIn(node.title) + wordsIn(text.slice(node.heading.to, own));
33891
+ let words = node.ownWords;
33892
+ for (const child of node.children) {
33893
+ count(child);
33894
+ words += child.words;
33895
+ }
33896
+ node.words = words;
33897
+ };
33898
+ let words = 0;
33899
+ for (const node of roots) {
33900
+ count(node);
33901
+ words += node.words;
33902
+ }
33903
+ const stop = firstHeading < 0 ? end : firstHeading;
33904
+ const preamble = stop > 0 ? {
33905
+ from: 0,
33906
+ to: stop
33907
+ } : null;
33908
+ if (preamble) words += wordsIn(text.slice(0, stop));
33909
+ return {
33910
+ preamble,
33911
+ parts: roots.some((n) => n.kind === "part"),
33912
+ nodes: roots,
33913
+ words
33914
+ };
33915
+ }
33916
+ // ../novel/src/facts.ts
33917
+ const STATUSES = [
33918
+ "todo",
33919
+ "draft",
33920
+ "revised",
33921
+ "done"
33922
+ ];
33923
+ /** The longest synopsis kept, in UTF-16 code units: a longer one is cut there when read or written. */
33924
+ const MAX_SYNOPSIS = 2e3;
33925
+ const MAX_TARGET = 1e7;
33926
+ function isRecord(value) {
33927
+ return value !== null && typeof value === "object" && !Array.isArray(value);
33928
+ }
33929
+ function target(value) {
33930
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_TARGET ? value : void 0;
33931
+ }
33932
+ function newNodes() {
33933
+ return Object.create(null);
33934
+ }
33935
+ function readOne(raw) {
33936
+ if (!isRecord(raw)) return null;
33937
+ const facts = {};
33938
+ const status = raw.status;
33939
+ if (typeof status === "string" && STATUSES.includes(status)) facts.status = status;
33940
+ if (typeof raw.synopsis === "string" && raw.synopsis !== "") {
33941
+ let s = raw.synopsis.slice(0, MAX_SYNOPSIS);
33942
+ const last = s.charCodeAt(s.length - 1);
33943
+ if (last >= 55296 && last <= 56319) s = s.slice(0, -1);
33944
+ facts.synopsis = s;
33945
+ }
33946
+ const t = target(raw.target);
33947
+ if (t !== void 0) facts.target = t;
33948
+ return Object.keys(facts).length > 0 ? facts : null;
33949
+ }
33950
+ /** The facts kept in a document's metadata comment; empty when there are none or they are unreadable. */
33951
+ function readFacts(text) {
33952
+ const out = { nodes: newNodes() };
33953
+ let novel;
33954
+ try {
33955
+ novel = readMarkdownMeta(text).meta?.novel;
33956
+ } catch {
33957
+ return out;
33958
+ }
33959
+ if (!isRecord(novel)) return out;
33960
+ const bookTarget = target(novel.bookTarget);
33961
+ if (bookTarget !== void 0) out.bookTarget = bookTarget;
33962
+ if (isRecord(novel.nodes)) for (const id of Object.keys(novel.nodes)) {
33963
+ const facts = readOne(novel.nodes[id]);
33964
+ if (facts) out.nodes[id] = facts;
33965
+ }
33966
+ return out;
33967
+ }
33700
33968
  // src/formats.ts
33701
33969
  const NO_BYTES = /* @__PURE__ */ new Uint8Array();
33702
33970
  /**
@@ -33713,6 +33981,45 @@ function formatOf(name) {
33713
33981
  return importKindOf(name) ?? reopenFormatOf(name, NO_BYTES);
33714
33982
  }
33715
33983
  // src/load.ts
33984
+ /** Whether the text's metadata comment names the novel template (Drafter's own file kind). */
33985
+ function isNovelText(text) {
33986
+ try {
33987
+ return readMarkdownMeta(text).meta?.template === "novel";
33988
+ } catch {
33989
+ return false;
33990
+ }
33991
+ }
33992
+ /**
33993
+ * The title of a unit with none of its own. An untitled section is the one a scene break starts,
33994
+ * which the app calls "Scene break"; a part or chapter with an empty heading is named by its kind.
33995
+ */
33996
+ const untitled = (kind) => kind === "section" ? "Scene break" : `(untitled ${kind})`;
33997
+ function flatten(nodes, into) {
33998
+ for (const node of nodes) {
33999
+ into.push(node);
34000
+ flatten(node.children, into);
34001
+ }
34002
+ return into;
34003
+ }
34004
+ /**
34005
+ * A novel's units as comparison anchors (their heading lines from 0, as `markdownAnchors` gives
34006
+ * for plain Markdown), so drafts and compares number them as `read_scene` does.
34007
+ */
34008
+ function novelAnchors(text) {
34009
+ const { starts } = splitLines(text);
34010
+ let line = 0;
34011
+ return novelNodes(text).map((node) => {
34012
+ while (line + 1 < starts.length && starts[line + 1] <= node.heading.from) line++;
34013
+ return {
34014
+ line,
34015
+ title: node.title === "" ? untitled(node.kind) : node.title
34016
+ };
34017
+ });
34018
+ }
34019
+ /** A novel's parts, chapters, and sections in document order: the units every tool numbers. */
34020
+ function novelNodes(text) {
34021
+ return flatten(novelStructure(text).nodes, []);
34022
+ }
33716
34023
  const NOT_READ = "Drafter for Claude doesn’t read that kind of file. It reads Fountain, plain text, Markdown, Final Draft, Highland, and Fade In files, and Drafter’s HTML and EPUB exports.";
33717
34024
  const SCREENPLAY$2 = builtins["us-screenplay"];
33718
34025
  let parses = 0;
@@ -33807,9 +34114,33 @@ function loadScript(file, format, content, text, importNotes, wordsPerMinute) {
33807
34114
  importNotes
33808
34115
  };
33809
34116
  }
34117
+ function loadNovel(file, format, content, text, end) {
34118
+ const nodes = novelNodes(text);
34119
+ const facts = readFacts(text).nodes;
34120
+ return {
34121
+ kind: "markdown",
34122
+ file,
34123
+ format,
34124
+ content,
34125
+ text,
34126
+ title: markdownTitle(text) ?? null,
34127
+ sections: nodes.map((node, i) => ({
34128
+ title: node.title === "" ? untitled(node.kind) : node.title,
34129
+ from: node.heading.from,
34130
+ to: Math.min(end, nodes[i + 1]?.heading.from ?? end)
34131
+ })),
34132
+ novel: nodes.map((node) => ({
34133
+ kind: node.kind,
34134
+ words: node.words,
34135
+ ownWords: node.ownWords,
34136
+ ...facts[node.id]
34137
+ }))
34138
+ };
34139
+ }
33810
34140
  function loadMarkdown(file, format, content, text) {
33811
34141
  const meta = findMarkdownMeta(text);
33812
34142
  const end = meta ? meta.from : text.length;
34143
+ if (isNovelText(text)) return loadNovel(file, format, content, text, end);
33813
34144
  const { starts } = splitLines(text);
33814
34145
  const anchors = markdownAnchors(text).filter((anchor) => (starts[anchor.line] ?? 0) < end);
33815
34146
  const sections = anchors.map((anchor, i) => {
@@ -33827,7 +34158,8 @@ function loadMarkdown(file, format, content, text) {
33827
34158
  content,
33828
34159
  text,
33829
34160
  title: markdownTitle(text) ?? null,
33830
- sections
34161
+ sections,
34162
+ novel: null
33831
34163
  };
33832
34164
  }
33833
34165
  /** The file a tool's path names, read and parsed (or the cached parse, while it is unchanged). */
@@ -34199,10 +34531,14 @@ function visibleMarkdownIn(text) {
34199
34531
  }
34200
34532
  // ../history/src/print.ts
34201
34533
  const SCREENPLAY$1 = builtins["us-screenplay"];
34202
- /** The template `doc` names in its settings, else the screenplay. */
34534
+ /**
34535
+ * The template `doc` names in its settings, else the screenplay. Only a built-in's own name: a
34536
+ * script naming `constructor` or `__proto__` got the object's prototype, and Cuts and History
34537
+ * stopped reading it (red/blue audit, 2026-11-03).
34538
+ */
34203
34539
  function templateOf(doc) {
34204
34540
  const id = doc.meta?.template;
34205
- return (typeof id === "string" ? builtins[id] : void 0) ?? SCREENPLAY$1;
34541
+ return typeof id === "string" && Object.hasOwn(builtins, id) ? builtins[id] : SCREENPLAY$1;
34206
34542
  }
34207
34543
  /**
34208
34544
  * A heading folded for matching: upper case, the locked number (`#12A#`) dropped, every dash
@@ -35942,7 +36278,24 @@ const overviewOutput = object({
35942
36278
  sections: array(object({
35943
36279
  n: number$1().int(),
35944
36280
  title: string(),
35945
- words: number$1().int()
36281
+ /** A novel's: the whole passage, its sections and chapters included. */
36282
+ words: number$1().int(),
36283
+ /** A novel's: the words before its first section or chapter. */
36284
+ own_words: number$1().int().optional(),
36285
+ kind: _enum([
36286
+ "part",
36287
+ "chapter",
36288
+ "section"
36289
+ ]).optional(),
36290
+ status: _enum([
36291
+ "todo",
36292
+ "draft",
36293
+ "revised",
36294
+ "done"
36295
+ ]).optional(),
36296
+ synopsis: string().optional(),
36297
+ synopsis_cut: literal(true).optional(),
36298
+ target: number$1().int().optional()
35946
36299
  })).optional(),
35947
36300
  words: number$1().int().optional(),
35948
36301
  left_out: leftOut,
@@ -36004,12 +36357,26 @@ function bounded(items, share, more) {
36004
36357
  const sectionWords = (text) => wordsIn(text.replace(/^ {0,3}#{1,3}(?=[ \t]|$)/, "").replace(/[ \t]+#+[ \t]*(?=\n|$)/, ""));
36005
36358
  function markdownOverview(loaded, args, ctx, include) {
36006
36359
  const { text, sections } = loaded;
36007
- const items = sections.map((section, i) => ({
36008
- n: i + 1,
36009
- title: clipText(section.title, TEXT_LIMITS.heading).text,
36010
- words: sectionWords(visibleMarkdown(text, section.from, section.to))
36011
- }));
36012
- const words = wordsIn(visibleMarkdown(text, 0, sections[0]?.from ?? text.length)) + items.reduce((sum, s) => sum + s.words, 0);
36360
+ const shown = visibleMarkdownIn(text);
36361
+ const own = sections.map((section) => sectionWords(shown(section.from, section.to)));
36362
+ const items = sections.map((section, i) => {
36363
+ const unit = loaded.novel?.[i];
36364
+ const synopsis = unit?.synopsis === void 0 ? void 0 : clipText(unit.synopsis, TEXT_LIMITS.synopsis);
36365
+ return {
36366
+ n: i + 1,
36367
+ title: clipText(section.title, TEXT_LIMITS.heading).text,
36368
+ words: unit ? unit.words : own[i],
36369
+ ...unit ? {
36370
+ kind: unit.kind,
36371
+ own_words: unit.ownWords,
36372
+ ...unit.status === void 0 ? {} : { status: unit.status },
36373
+ ...synopsis ? { synopsis: synopsis.text } : {},
36374
+ ...synopsis?.cut ? { synopsis_cut: true } : {},
36375
+ ...unit.target === void 0 ? {} : { target: unit.target }
36376
+ } : {}
36377
+ };
36378
+ });
36379
+ const words = wordsIn(shown(0, sections[0]?.from ?? text.length)) + items.reduce((sum, _s, i) => sum + own[i], 0);
36013
36380
  const now = cursorBase("script_overview", loaded, {
36014
36381
  script: args.script,
36015
36382
  include
@@ -36173,7 +36540,7 @@ async function overview(args, ctx) {
36173
36540
  const scriptOverview = {
36174
36541
  name: "script_overview",
36175
36542
  title: "Script overview",
36176
- description: "The shape of one script: its title page (never the contact), template, pages, and running time; its acts with their synopses; every scene with its heading, place, time of day, act, page, length, synopsis, and who is in it; every character’s speeches, words, share of the talking, scenes, long absences, and who they are most often with; its places; what is still to be written; and a count of problems. For a Markdown document, its title and sections. Notes and the boneyard are left out unless include asks for them. Pass `next` back as `cursor` for more scenes. A field ending `_cut` means its text was too long and only its start is shown; one ending `_more` counts what didn’t fit.",
36543
+ description: "The shape of one script: its title page (never the contact), template, pages, and running time; its acts with their synopses; every scene with its heading, place, time of day, act, page, length, synopsis, and who is in it; every character’s speeches, words, share of the talking, scenes, long absences, and who they are most often with; its places; what is still to be written; and a count of problems. For a Markdown document, its title and sections; for a novel, its parts, chapters, and sections, each with its words and the status, synopsis, and word target the writer keeps for it. Notes and the boneyard are left out unless include asks for them. Pass `next` back as `cursor` for more scenes. A field ending `_cut` means its text was too long and only its start is shown; one ending `_more` counts what didn’t fit.",
36177
36544
  input: overviewInput,
36178
36545
  output: overviewOutput,
36179
36546
  run: overview
@@ -36609,7 +36976,8 @@ const listed = object({
36609
36976
  kind: _enum([
36610
36977
  "screenplay",
36611
36978
  "stage play",
36612
- "markdown"
36979
+ "markdown",
36980
+ "novel"
36613
36981
  ]).nullable(),
36614
36982
  title: string().nullable(),
36615
36983
  /** The title was longer than TEXT_LIMITS.heading, and is its start. */
@@ -36667,11 +37035,12 @@ function scriptSummary(content, text) {
36667
37035
  };
36668
37036
  }
36669
37037
  function markdownSummary(content, text) {
37038
+ const novel = isNovelText(text);
36670
37039
  return {
36671
37040
  content,
36672
- kind: "markdown",
37041
+ kind: novel ? "novel" : "markdown",
36673
37042
  title: markdownTitle(text) ?? null,
36674
- scenes: markdownAnchors(text).length
37043
+ scenes: novel ? novelNodes(text).length : markdownAnchors(text).length
36675
37044
  };
36676
37045
  }
36677
37046
  const tooBig$1 = () => new Refused("it is too big");
@@ -36875,7 +37244,7 @@ async function describe(file, ctx, spend) {
36875
37244
  const listScripts = {
36876
37245
  name: "list_scripts",
36877
37246
  title: "List scripts",
36878
- description: "Lists the scripts, plays, and documents in the writer’s folders (or one folder, given as a path that starts with a folder’s name): each file’s path, kind, title, scene count, date, size, and how many drafts and cuts Drafter keeps beside it, with a manifest: what Drafter found inside each file (`content`), whether that matches its name (`matches_name`), and why Drafter won’t read it (`refused`). Pass `next` back as `cursor` for the rest.",
37247
+ description: "Lists the scripts, plays, and documents in the writer’s folders (or one folder, given as a path that starts with a folder’s name): each file’s path, kind (screenplay, stage play, novel, or markdown), title, scene count, date, size, and how many drafts and cuts Drafter keeps beside it, with a manifest: what Drafter found inside each file (`content`), whether that matches its name (`matches_name`), and why Drafter won’t read it (`refused`). Pass `next` back as `cursor` for the rest.",
36879
37248
  input,
36880
37249
  output,
36881
37250
  run
@@ -37955,11 +38324,11 @@ const draftsOutput = object({
37955
38324
  });
37956
38325
  const draftSummaries = new LoadCache(999);
37957
38326
  /** A draft's pages, scenes, and words: from its text, never returned. */
37958
- function figures(text, kind) {
38327
+ function figures(text, kind, novel = false) {
37959
38328
  if (kind === "markdown") return {
37960
38329
  pages: null,
37961
- scenes: markdownAnchors(text).length,
37962
- words: wordsIn(text)
38330
+ scenes: (novel ? novelAnchors(text) : markdownAnchors(text)).length,
38331
+ words: wordsIn(markdownProse(text))
37963
38332
  };
37964
38333
  const doc = parse(text);
37965
38334
  const template = templateFor(doc);
@@ -37972,7 +38341,7 @@ function figures(text, kind) {
37972
38341
  }
37973
38342
  /** Whether a draft's summary is kept already (so reading it again costs nothing). */
37974
38343
  const summarised = (file) => draftSummaries.get(file.real, file.size, file.mtimeMs) !== void 0;
37975
- async function summaryOf(file, kind) {
38344
+ async function summaryOf(file, kind, novel = false) {
37976
38345
  const cached = draftSummaries.get(file.real, file.size, file.mtimeMs);
37977
38346
  if (cached !== void 0) return cached;
37978
38347
  const read = await readDraft(file, kind);
@@ -37993,7 +38362,7 @@ async function summaryOf(file, kind) {
37993
38362
  scenes: null,
37994
38363
  words: null,
37995
38364
  figures: false
37996
- } : figures(read.text, kind),
38365
+ } : figures(read.text, kind, novel),
37997
38366
  matches: read.matches
37998
38367
  } };
37999
38368
  }
@@ -38003,6 +38372,7 @@ async function summaryOf(file, kind) {
38003
38372
  async function listDrafts(args, ctx) {
38004
38373
  const loaded = await load(args.script, ctx);
38005
38374
  const kind = kindOf(loaded);
38375
+ const isNovel = loaded.kind === "markdown" && loaded.novel !== null;
38006
38376
  const files = await draftFiles(loaded, ctx);
38007
38377
  const now = cursorBase("list_drafts", loaded, {
38008
38378
  script: args.script,
@@ -38033,7 +38403,7 @@ async function listDrafts(args, ctx) {
38033
38403
  const size = fresh ? checked.size : 0;
38034
38404
  const parse = fresh && checked.size <= 5242880 ? checked.size : 0;
38035
38405
  if (examined > 0 && (read + size > PAGE_READ_BYTES || parsed > 0 && parsed + parse > PAGE_PARSE_BYTES)) break;
38036
- const summary = checked ? await summaryOf(checked, kind) : null;
38406
+ const summary = checked ? await summaryOf(checked, kind, isNovel) : null;
38037
38407
  read += size;
38038
38408
  parsed += parse;
38039
38409
  if (!summary) {
@@ -38231,7 +38601,7 @@ async function compareDrafts(args, ctx) {
38231
38601
  left.notes += shown.leftOut.notes;
38232
38602
  left.boneyard += shown.leftOut.boneyard;
38233
38603
  }
38234
- const anchorsOf = kind === "markdown" ? markdownAnchors : sceneAnchors;
38604
+ const anchorsOf = kind !== "markdown" ? sceneAnchors : loaded.kind === "markdown" && loaded.novel !== null ? novelAnchors : markdownAnchors;
38235
38605
  const oldAnchors = anchorsOf(shownOld.text);
38236
38606
  const newAnchors = anchorsOf(shownNew.text);
38237
38607
  const { rows } = diffRows(shownOld.text, shownNew.text, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafter-mcp",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Drafter for Claude: a local, read-only MCP server that lets an AI app read the scripts in the folders the writer names.",
5
5
  "license": "MIT",
6
6
  "type": "module",