drafter-mcp 0.25.1 → 0.26.1

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 +462 -93
  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.1";
23236
+ var version = "0.26.1";
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,
@@ -30949,18 +30949,18 @@ const MOST_HOOK = 200;
30949
30949
  /** The planned length's bounds, in pages. */
30950
30950
  const FEWEST_PAGES = 1;
30951
30951
  const MOST_PAGES = 999;
30952
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
30952
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
30953
30953
  /** The `storyMap` part of a parsed metadata block (`doc.meta`), read strictly. */
30954
30954
  function storyMapMeta(meta) {
30955
30955
  const hooks = Object.create(null);
30956
- const part = isRecord(meta) ? meta.storyMap : void 0;
30957
- if (!isRecord(part)) return {
30956
+ const part = isRecord$1(meta) ? meta.storyMap : void 0;
30957
+ if (!isRecord$1(part)) return {
30958
30958
  plannedPages: null,
30959
30959
  hooks
30960
30960
  };
30961
30961
  const pages = part.plannedPages;
30962
30962
  const plannedPages = typeof pages === "number" && Number.isInteger(pages) && pages >= FEWEST_PAGES && pages <= MOST_PAGES ? pages : null;
30963
- if (isRecord(part.hooks)) {
30963
+ if (isRecord$1(part.hooks)) {
30964
30964
  for (const [key, hook] of Object.entries(part.hooks)) if (typeof hook === "string" && hook.length <= MOST_HOOK) hooks[key] = hook;
30965
30965
  }
30966
30966
  return {
@@ -31525,6 +31525,66 @@ function shares(amounts) {
31525
31525
  }
31526
31526
  return whole;
31527
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
+ }
31528
31588
  // ../markdown/src/words.ts
31529
31589
  /**
31530
31590
  * Whether a UTF-16 code unit is one that `/\s/` matches: the ASCII spaces (tab, line feed,
@@ -31533,7 +31593,7 @@ function shares(amounts) {
31533
31593
  * medium mathematical space, the ideographic space, and the byte-order mark. A comparison per
31534
31594
  * character, where a regular expression per character cost more than the reparse on a long script.
31535
31595
  */
31536
- function isSpace$6(code) {
31596
+ function isSpace$5(code) {
31537
31597
  if (code <= 32) return code === 32 || code >= 9 && code <= 13;
31538
31598
  if (code < 160) return false;
31539
31599
  return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
@@ -31543,7 +31603,7 @@ function wordsIn(text) {
31543
31603
  let count = 0;
31544
31604
  let inWord = false;
31545
31605
  for (let i = 0; i < text.length; i++) {
31546
- const space = isSpace$6(text.charCodeAt(i));
31606
+ const space = isSpace$5(text.charCodeAt(i));
31547
31607
  if (!space && !inWord) count++;
31548
31608
  inWord = !space;
31549
31609
  }
@@ -31580,7 +31640,7 @@ const SMALL = /* @__PURE__ */ new Set([
31580
31640
  const cased = (c) => c.toLowerCase() !== c.toUpperCase();
31581
31641
  const isUpper$1 = (c) => cased(c) && c === c.toUpperCase();
31582
31642
  const isLower = (c) => cased(c) && c === c.toLowerCase();
31583
- const isSpace$5 = (c) => c !== void 0 && c !== "" && c.trim() === "";
31643
+ const isSpace$4 = (c) => c !== void 0 && c !== "" && c.trim() === "";
31584
31644
  /** One word in title case: its first letter, and the first after a hyphen, in capitals. */
31585
31645
  function titleWord(word, first) {
31586
31646
  const chars = Array.from(word);
@@ -31603,7 +31663,7 @@ function titleWord(word, first) {
31603
31663
  function titleCase(text) {
31604
31664
  let first = true;
31605
31665
  return text.split(/(\s+)/).map((part) => {
31606
- if (part === "" || isSpace$5(part[0])) return part;
31666
+ if (part === "" || isSpace$4(part[0])) return part;
31607
31667
  const word = titleWord(part, first);
31608
31668
  first = false;
31609
31669
  return word;
@@ -31631,7 +31691,7 @@ function withoutNumber(text) {
31631
31691
  function slashParts(part) {
31632
31692
  const out = [];
31633
31693
  let from = 0;
31634
- 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]))) {
31635
31695
  out.push(part.slice(from, i));
31636
31696
  from = i + 1;
31637
31697
  }
@@ -31647,7 +31707,7 @@ function placeWords(location) {
31647
31707
  const text = withoutNumber(location);
31648
31708
  const parts = [];
31649
31709
  let from = 0;
31650
- 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])) {
31651
31711
  parts.push(text.slice(from, i));
31652
31712
  from = i + 1;
31653
31713
  }
@@ -31662,7 +31722,7 @@ const followsOn = (key) => key.startsWith("LATER ") || key.endsWith(" LATER");
31662
31722
  */
31663
31723
  function followOn(location) {
31664
31724
  const text = location.trimEnd();
31665
- 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])) {
31666
31726
  const part = text.slice(i + 1).trim();
31667
31727
  if (!followsOn(part.toUpperCase().split(/\s+/).join(" "))) break;
31668
31728
  return {
@@ -31828,7 +31888,7 @@ const TRAILING = /* @__PURE__ */ new Set([
31828
31888
  ]);
31829
31889
  /** A comma or dashes just after a name (or its age), and the spaces after them: anchored, linear. */
31830
31890
  const LEAD = /^[,\-–—―][\s,\-–—―]*/u;
31831
- const isSpace$4 = (c) => c !== void 0 && c.trim() === "";
31891
+ const isSpace$3 = (c) => c !== void 0 && c.trim() === "";
31832
31892
  const isLetter = (c) => c !== void 0 && /\p{L}/u.test(c);
31833
31893
  const isStop = (c) => c === "." || c === "!" || c === "?";
31834
31894
  /** Whether the period at `i` closes an abbreviation or an initial (`Dr.`, `J.`). */
@@ -31851,7 +31911,7 @@ function sentenceEnd(text, from = 0, limit = text.length) {
31851
31911
  let j = i + 1;
31852
31912
  while (j < text.length && isStop(text[j])) j++;
31853
31913
  while (j < text.length && CLOSERS.has(text[j])) j++;
31854
- if (j < text.length && !isSpace$4(text[j])) {
31914
+ if (j < text.length && !isSpace$3(text[j])) {
31855
31915
  i = j - 1;
31856
31916
  continue;
31857
31917
  }
@@ -31976,7 +32036,7 @@ function nameCase(text, map, names, ages = true) {
31976
32036
  at += 2;
31977
32037
  }
31978
32038
  if (!ages) {
31979
- const age = ageAt(text, isSpace$4(text[at]) ? at + 1 : at);
32039
+ const age = ageAt(text, isSpace$3(text[at]) ? at + 1 : at);
31980
32040
  if (age) at = age.end;
31981
32041
  }
31982
32042
  }
@@ -31997,7 +32057,7 @@ function withoutMarks(text) {
31997
32057
  return text.replace(SCREEN_MARKS, (mark, at) => {
31998
32058
  if (mark.startsWith("(") || !mark.endsWith(".")) return GONE;
31999
32059
  let next = at + mark.length;
32000
- while (isSpace$4(text[next])) next++;
32060
+ while (isSpace$3(text[next])) next++;
32001
32061
  const c = text[next];
32002
32062
  return c === void 0 || c !== c.toLowerCase() && c === c.toUpperCase() ? STOP_KEPT : GONE;
32003
32063
  }).replace(/\s+/g, " ").replace(/ ?\uE001(?=[,.;:!?])/g, "").replace(/\uE001/g, "").replace(/ ?\uE000/g, ".").replace(/\s+/g, " ").trim();
@@ -32055,7 +32115,7 @@ function pastCapitals(text, at) {
32055
32115
  let i = at;
32056
32116
  for (let n = 0; n < NAME_WORDS; n++) {
32057
32117
  let j = i;
32058
- while (isSpace$4(text[j])) j++;
32118
+ while (isSpace$3(text[j])) j++;
32059
32119
  let k = j;
32060
32120
  while (k < text.length && /[\p{L}\p{M}'’.-]/u.test(text[k])) k++;
32061
32121
  const word = text.slice(j, k).replace(/\.$/, "");
@@ -32072,11 +32132,11 @@ function pastCapitals(text, at) {
32072
32132
  */
32073
32133
  function introducedAt(text, at) {
32074
32134
  let j = pastCapitals(text, at);
32075
- while (isSpace$4(text[j])) j++;
32135
+ while (isSpace$3(text[j])) j++;
32076
32136
  if (ageAt(text, j)) return true;
32077
32137
  const aside = asideEnd(text, j);
32078
32138
  if (aside >= 0) j = aside;
32079
- while (isSpace$4(text[j])) j++;
32139
+ while (isSpace$3(text[j])) j++;
32080
32140
  const c = text[j];
32081
32141
  return c === "," || c === "-" || c !== void 0 && DASHES.has(c);
32082
32142
  }
@@ -32087,7 +32147,7 @@ function introducedAt(text, at) {
32087
32147
  function hookFrom(text, at, map, names) {
32088
32148
  let i = pastCapitals(text, at);
32089
32149
  let j = i;
32090
- while (isSpace$4(text[j])) j++;
32150
+ while (isSpace$3(text[j])) j++;
32091
32151
  const aged = ageAt(text, j);
32092
32152
  const aside = aged ? aged.end : asideEnd(text, j);
32093
32153
  if (aside >= 0) i = aside;
@@ -32098,7 +32158,7 @@ function hookFrom(text, at, map, names) {
32098
32158
  rest = rest.slice(lead.length);
32099
32159
  if (lead !== "") for (let k = 0; k < rest.length; k++) {
32100
32160
  const c = rest[k];
32101
- 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]));
32102
32162
  if (c === "," || DASHES.has(c) || hyphen) {
32103
32163
  rest = rest.slice(0, k);
32104
32164
  break;
@@ -33421,7 +33481,7 @@ function groupRows(rows, oldAnchors, newAnchors) {
33421
33481
  });
33422
33482
  return groups;
33423
33483
  }
33424
- const FENCE$1 = /^ {0,3}(`{3,}|~{3,})/;
33484
+ const FENCE$2 = /^ {0,3}(`{3,}|~{3,})/;
33425
33485
  const HEADING = /^ {0,3}(#{1,3})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
33426
33486
  /**
33427
33487
  * The lines of a Markdown text that start a heading of level 1 to 3, with its text, skipping fenced
@@ -33431,7 +33491,7 @@ function markdownAnchors(text) {
33431
33491
  const anchors = [];
33432
33492
  let fence = null;
33433
33493
  splitLines(text).lines.forEach((line, i) => {
33434
- const open = FENCE$1.exec(line);
33494
+ const open = FENCE$2.exec(line);
33435
33495
  if (fence !== null) {
33436
33496
  if (open && open[1][0] === fence[0] && open[1].length >= fence.length && !line.slice(open[0].length).trim()) fence = null;
33437
33497
  return;
@@ -33450,39 +33510,8 @@ function markdownAnchors(text) {
33450
33510
  });
33451
33511
  return anchors;
33452
33512
  }
33453
- // ../markdown/src/meta.ts
33454
- const OPENER = "<!-- drafter ";
33455
- const CLOSER = " -->";
33456
- function isSpace$3(code) {
33457
- return code === 32 || code === 9 || code === 10 || code === 13;
33458
- }
33459
- /**
33460
- * The document's metadata block: its last line (trailing spaces and blank lines aside), starting
33461
- * at the line's start (so not indented code) as `<!-- drafter `, ending ` -->`, and holding a JSON
33462
- * object. Anything else, a comment with words after it, or one whose JSON doesn't read as an
33463
- * object, is the writer's own text. One backwards scan, then one JSON parse: linear.
33464
- */
33465
- function findMarkdownMeta(text) {
33466
- let end = text.length;
33467
- while (end > 0 && isSpace$3(text.charCodeAt(end - 1))) end--;
33468
- const from = text.lastIndexOf("\n", end - 1) + 1;
33469
- if (end - from < 17) return null;
33470
- if (!text.startsWith(OPENER, from) || !text.startsWith(CLOSER, end - 4)) return null;
33471
- let meta;
33472
- try {
33473
- meta = JSON.parse(text.slice(from + 13, end - 4));
33474
- } catch {
33475
- return null;
33476
- }
33477
- if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return null;
33478
- return {
33479
- from,
33480
- to: end,
33481
- meta
33482
- };
33483
- }
33484
33513
  // ../markdown/src/title.ts
33485
- const isBlank = (c) => c === " " || c === " ";
33514
+ const isBlank$1 = (c) => c === " " || c === " ";
33486
33515
  /**
33487
33516
  * An ATX heading's parts, read by hand: up to three spaces, 1 to 6 `#` (`prefix`), then a space or
33488
33517
  * a tab or the line's end, and the text, without the blanks around it or a closing run of `#`s (one
@@ -33496,23 +33525,23 @@ function atx(line) {
33496
33525
  if (hashes < 1 || hashes > 6) return void 0;
33497
33526
  const prefix = line.slice(0, i + hashes);
33498
33527
  i += hashes;
33499
- if (i < line.length && !isBlank(line[i])) return void 0;
33528
+ if (i < line.length && !isBlank$1(line[i])) return void 0;
33500
33529
  let start = i;
33501
- while (start < line.length && isBlank(line[start])) start++;
33530
+ while (start < line.length && isBlank$1(line[start])) start++;
33502
33531
  let end = line.length;
33503
- while (end > start && isBlank(line[end - 1])) end--;
33532
+ while (end > start && isBlank$1(line[end - 1])) end--;
33504
33533
  let close = end;
33505
33534
  while (close > start && line[close - 1] === "#") close--;
33506
- if (close < end && (close === start || isBlank(line[close - 1]))) {
33535
+ if (close < end && (close === start || isBlank$1(line[close - 1]))) {
33507
33536
  end = close;
33508
- while (end > start && isBlank(line[end - 1])) end--;
33537
+ while (end > start && isBlank$1(line[end - 1])) end--;
33509
33538
  }
33510
33539
  return {
33511
33540
  prefix,
33512
33541
  text: line.slice(start, end)
33513
33542
  };
33514
33543
  }
33515
- const FENCE = /^ {0,3}(`{3,}|~{3,})/;
33544
+ const FENCE$1 = /^ {0,3}(`{3,}|~{3,})/;
33516
33545
  const SETEXT = /^ {0,3}(?:=+|-+)[ \t]*$/;
33517
33546
  /**
33518
33547
  * The label of a footnote definition at a line's start, after any indentation, quote marks, and
@@ -33521,15 +33550,15 @@ const SETEXT = /^ {0,3}(?:=+|-+)[ \t]*$/;
33521
33550
  function definitionLabel(line) {
33522
33551
  let i = 0;
33523
33552
  for (;;) {
33524
- while (isBlank(line[i]) || line[i] === ">") i++;
33553
+ while (isBlank$1(line[i]) || line[i] === ">") i++;
33525
33554
  const c = line[i];
33526
- if ((c === "-" || c === "*" || c === "+") && isBlank(line[i + 1])) {
33555
+ if ((c === "-" || c === "*" || c === "+") && isBlank$1(line[i + 1])) {
33527
33556
  i++;
33528
33557
  continue;
33529
33558
  }
33530
33559
  let j = i;
33531
33560
  while (j - i < 9 && (line[j] ?? "") >= "0" && (line[j] ?? "") <= "9") j++;
33532
- if (j > i && (line[j] === "." || line[j] === ")") && isBlank(line[j + 1])) {
33561
+ if (j > i && (line[j] === "." || line[j] === ")") && isBlank$1(line[j + 1])) {
33533
33562
  i = j + 1;
33534
33563
  continue;
33535
33564
  }
@@ -33552,7 +33581,7 @@ function definedLabels(source) {
33552
33581
  if (end < 0) end = source.length;
33553
33582
  const line = source.slice(start, end);
33554
33583
  start = end + 1;
33555
- const opens = FENCE.exec(line);
33584
+ const opens = FENCE$1.exec(line);
33556
33585
  if (fence !== void 0) {
33557
33586
  if (opens?.[1]?.startsWith(fence.charAt(0)) && opens[1].length >= fence.length) fence = void 0;
33558
33587
  continue;
@@ -33669,7 +33698,7 @@ function squeeze(text) {
33669
33698
  for (let i = 0; i <= text.length; i++) {
33670
33699
  const c = text[i];
33671
33700
  const mark = c === "*" || c === "_" || c === "`";
33672
- const blank = c !== void 0 && !mark && isSpace$6(text.charCodeAt(i));
33701
+ const blank = c !== void 0 && !mark && isSpace$5(text.charCodeAt(i));
33673
33702
  if (c !== void 0 && !mark && !blank) {
33674
33703
  if (from < 0) {
33675
33704
  if (space && parts.length > 0) parts.push(" ");
@@ -33708,7 +33737,7 @@ function markdownTitle(source) {
33708
33737
  if (end < 0) end = source.length;
33709
33738
  const line = source.slice(start, end);
33710
33739
  start = end + 1;
33711
- const opens = FENCE.exec(line);
33740
+ const opens = FENCE$1.exec(line);
33712
33741
  if (fence !== void 0) {
33713
33742
  if (opens?.[1]?.startsWith(fence.charAt(0)) && opens[1].length >= fence.length) fence = void 0;
33714
33743
  before = "";
@@ -33730,6 +33759,248 @@ function markdownTitle(source) {
33730
33759
  before = line.trim() === "" || /^ {4}/.test(line) ? "" : line;
33731
33760
  }
33732
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
+ /** The text cut to `max` code units, never leaving half of a surrogate pair at the cut. */
33933
+ function cut(value, max) {
33934
+ let s = value.slice(0, max);
33935
+ const last = s.charCodeAt(s.length - 1);
33936
+ if (last >= 55296 && last <= 56319) s = s.slice(0, -1);
33937
+ return s;
33938
+ }
33939
+ /** A manuscript field or contact line cut to `MAX_DETAIL`, as the book keeps it. */
33940
+ function cutDetail(value) {
33941
+ return cut(value, 300);
33942
+ }
33943
+ const DETAIL_KEYS = [
33944
+ "title",
33945
+ "byline",
33946
+ "legalName",
33947
+ "surname",
33948
+ "shortTitle"
33949
+ ];
33950
+ function manuscript(value) {
33951
+ if (!isRecord(value)) return void 0;
33952
+ const out = {};
33953
+ for (const key of DETAIL_KEYS) {
33954
+ const v = value[key];
33955
+ if (typeof v === "string" && v.trim() !== "") out[key] = cutDetail(v);
33956
+ }
33957
+ if (Array.isArray(value.contact)) {
33958
+ const lines = [];
33959
+ for (const item of value.contact) {
33960
+ if (typeof item !== "string") continue;
33961
+ for (const line of item.split(/\r\n|\r|\n/)) {
33962
+ const t = cutDetail(line.trim());
33963
+ if (t !== "") lines.push(t);
33964
+ }
33965
+ }
33966
+ if (lines.length > 0) out.contact = lines.slice(0, 10);
33967
+ }
33968
+ if (value.font === "times" || value.font === "courier") out.font = value.font;
33969
+ return Object.keys(out).length > 0 ? out : void 0;
33970
+ }
33971
+ function newNodes() {
33972
+ return Object.create(null);
33973
+ }
33974
+ function readOne(raw) {
33975
+ if (!isRecord(raw)) return null;
33976
+ const facts = {};
33977
+ const status = raw.status;
33978
+ if (typeof status === "string" && STATUSES.includes(status)) facts.status = status;
33979
+ if (typeof raw.synopsis === "string" && raw.synopsis !== "") facts.synopsis = cut(raw.synopsis, MAX_SYNOPSIS);
33980
+ const t = target(raw.target);
33981
+ if (t !== void 0) facts.target = t;
33982
+ return Object.keys(facts).length > 0 ? facts : null;
33983
+ }
33984
+ /** The facts kept in a document's metadata comment; empty when there are none or they are unreadable. */
33985
+ function readFacts(text) {
33986
+ const out = { nodes: newNodes() };
33987
+ let novel;
33988
+ try {
33989
+ novel = readMarkdownMeta(text).meta?.novel;
33990
+ } catch {
33991
+ return out;
33992
+ }
33993
+ if (!isRecord(novel)) return out;
33994
+ const bookTarget = target(novel.bookTarget);
33995
+ if (bookTarget !== void 0) out.bookTarget = bookTarget;
33996
+ const details = manuscript(novel.manuscript);
33997
+ if (details) out.manuscript = details;
33998
+ if (isRecord(novel.nodes)) for (const id of Object.keys(novel.nodes)) {
33999
+ const facts = readOne(novel.nodes[id]);
34000
+ if (facts) out.nodes[id] = facts;
34001
+ }
34002
+ return out;
34003
+ }
33733
34004
  // src/formats.ts
33734
34005
  const NO_BYTES = /* @__PURE__ */ new Uint8Array();
33735
34006
  /**
@@ -33746,6 +34017,45 @@ function formatOf(name) {
33746
34017
  return importKindOf(name) ?? reopenFormatOf(name, NO_BYTES);
33747
34018
  }
33748
34019
  // src/load.ts
34020
+ /** Whether the text's metadata comment names the novel template (Drafter's own file kind). */
34021
+ function isNovelText(text) {
34022
+ try {
34023
+ return readMarkdownMeta(text).meta?.template === "novel";
34024
+ } catch {
34025
+ return false;
34026
+ }
34027
+ }
34028
+ /**
34029
+ * The title of a unit with none of its own. An untitled section is the one a scene break starts,
34030
+ * which the app calls "Scene break"; a part or chapter with an empty heading is named by its kind.
34031
+ */
34032
+ const untitled = (kind) => kind === "section" ? "Scene break" : `(untitled ${kind})`;
34033
+ function flatten(nodes, into) {
34034
+ for (const node of nodes) {
34035
+ into.push(node);
34036
+ flatten(node.children, into);
34037
+ }
34038
+ return into;
34039
+ }
34040
+ /**
34041
+ * A novel's units as comparison anchors (their heading lines from 0, as `markdownAnchors` gives
34042
+ * for plain Markdown), so drafts and compares number them as `read_scene` does.
34043
+ */
34044
+ function novelAnchors(text) {
34045
+ const { starts } = splitLines(text);
34046
+ let line = 0;
34047
+ return novelNodes(text).map((node) => {
34048
+ while (line + 1 < starts.length && starts[line + 1] <= node.heading.from) line++;
34049
+ return {
34050
+ line,
34051
+ title: node.title === "" ? untitled(node.kind) : node.title
34052
+ };
34053
+ });
34054
+ }
34055
+ /** A novel's parts, chapters, and sections in document order: the units every tool numbers. */
34056
+ function novelNodes(text) {
34057
+ return flatten(novelStructure(text).nodes, []);
34058
+ }
33749
34059
  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.";
33750
34060
  const SCREENPLAY$2 = builtins["us-screenplay"];
33751
34061
  let parses = 0;
@@ -33840,9 +34150,33 @@ function loadScript(file, format, content, text, importNotes, wordsPerMinute) {
33840
34150
  importNotes
33841
34151
  };
33842
34152
  }
34153
+ function loadNovel(file, format, content, text, end) {
34154
+ const nodes = novelNodes(text);
34155
+ const facts = readFacts(text).nodes;
34156
+ return {
34157
+ kind: "markdown",
34158
+ file,
34159
+ format,
34160
+ content,
34161
+ text,
34162
+ title: markdownTitle(text) ?? null,
34163
+ sections: nodes.map((node, i) => ({
34164
+ title: node.title === "" ? untitled(node.kind) : node.title,
34165
+ from: node.heading.from,
34166
+ to: Math.min(end, nodes[i + 1]?.heading.from ?? end)
34167
+ })),
34168
+ novel: nodes.map((node) => ({
34169
+ kind: node.kind,
34170
+ words: node.words,
34171
+ ownWords: node.ownWords,
34172
+ ...facts[node.id]
34173
+ }))
34174
+ };
34175
+ }
33843
34176
  function loadMarkdown(file, format, content, text) {
33844
34177
  const meta = findMarkdownMeta(text);
33845
34178
  const end = meta ? meta.from : text.length;
34179
+ if (isNovelText(text)) return loadNovel(file, format, content, text, end);
33846
34180
  const { starts } = splitLines(text);
33847
34181
  const anchors = markdownAnchors(text).filter((anchor) => (starts[anchor.line] ?? 0) < end);
33848
34182
  const sections = anchors.map((anchor, i) => {
@@ -33860,7 +34194,8 @@ function loadMarkdown(file, format, content, text) {
33860
34194
  content,
33861
34195
  text,
33862
34196
  title: markdownTitle(text) ?? null,
33863
- sections
34197
+ sections,
34198
+ novel: null
33864
34199
  };
33865
34200
  }
33866
34201
  /** The file a tool's path names, read and parsed (or the cached parse, while it is unchanged). */
@@ -35979,7 +36314,24 @@ const overviewOutput = object({
35979
36314
  sections: array(object({
35980
36315
  n: number$1().int(),
35981
36316
  title: string(),
35982
- words: number$1().int()
36317
+ /** A novel's: the whole passage, its sections and chapters included. */
36318
+ words: number$1().int(),
36319
+ /** A novel's: the words before its first section or chapter. */
36320
+ own_words: number$1().int().optional(),
36321
+ kind: _enum([
36322
+ "part",
36323
+ "chapter",
36324
+ "section"
36325
+ ]).optional(),
36326
+ status: _enum([
36327
+ "todo",
36328
+ "draft",
36329
+ "revised",
36330
+ "done"
36331
+ ]).optional(),
36332
+ synopsis: string().optional(),
36333
+ synopsis_cut: literal(true).optional(),
36334
+ target: number$1().int().optional()
35983
36335
  })).optional(),
35984
36336
  words: number$1().int().optional(),
35985
36337
  left_out: leftOut,
@@ -36041,12 +36393,26 @@ function bounded(items, share, more) {
36041
36393
  const sectionWords = (text) => wordsIn(text.replace(/^ {0,3}#{1,3}(?=[ \t]|$)/, "").replace(/[ \t]+#+[ \t]*(?=\n|$)/, ""));
36042
36394
  function markdownOverview(loaded, args, ctx, include) {
36043
36395
  const { text, sections } = loaded;
36044
- const items = sections.map((section, i) => ({
36045
- n: i + 1,
36046
- title: clipText(section.title, TEXT_LIMITS.heading).text,
36047
- words: sectionWords(visibleMarkdown(text, section.from, section.to))
36048
- }));
36049
- const words = wordsIn(visibleMarkdown(text, 0, sections[0]?.from ?? text.length)) + items.reduce((sum, s) => sum + s.words, 0);
36396
+ const shown = visibleMarkdownIn(text);
36397
+ const own = sections.map((section) => sectionWords(shown(section.from, section.to)));
36398
+ const items = sections.map((section, i) => {
36399
+ const unit = loaded.novel?.[i];
36400
+ const synopsis = unit?.synopsis === void 0 ? void 0 : clipText(unit.synopsis, TEXT_LIMITS.synopsis);
36401
+ return {
36402
+ n: i + 1,
36403
+ title: clipText(section.title, TEXT_LIMITS.heading).text,
36404
+ words: unit ? unit.words : own[i],
36405
+ ...unit ? {
36406
+ kind: unit.kind,
36407
+ own_words: unit.ownWords,
36408
+ ...unit.status === void 0 ? {} : { status: unit.status },
36409
+ ...synopsis ? { synopsis: synopsis.text } : {},
36410
+ ...synopsis?.cut ? { synopsis_cut: true } : {},
36411
+ ...unit.target === void 0 ? {} : { target: unit.target }
36412
+ } : {}
36413
+ };
36414
+ });
36415
+ const words = wordsIn(shown(0, sections[0]?.from ?? text.length)) + items.reduce((sum, _s, i) => sum + own[i], 0);
36050
36416
  const now = cursorBase("script_overview", loaded, {
36051
36417
  script: args.script,
36052
36418
  include
@@ -36210,7 +36576,7 @@ async function overview(args, ctx) {
36210
36576
  const scriptOverview = {
36211
36577
  name: "script_overview",
36212
36578
  title: "Script overview",
36213
- 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.",
36579
+ 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.",
36214
36580
  input: overviewInput,
36215
36581
  output: overviewOutput,
36216
36582
  run: overview
@@ -36646,7 +37012,8 @@ const listed = object({
36646
37012
  kind: _enum([
36647
37013
  "screenplay",
36648
37014
  "stage play",
36649
- "markdown"
37015
+ "markdown",
37016
+ "novel"
36650
37017
  ]).nullable(),
36651
37018
  title: string().nullable(),
36652
37019
  /** The title was longer than TEXT_LIMITS.heading, and is its start. */
@@ -36704,11 +37071,12 @@ function scriptSummary(content, text) {
36704
37071
  };
36705
37072
  }
36706
37073
  function markdownSummary(content, text) {
37074
+ const novel = isNovelText(text);
36707
37075
  return {
36708
37076
  content,
36709
- kind: "markdown",
37077
+ kind: novel ? "novel" : "markdown",
36710
37078
  title: markdownTitle(text) ?? null,
36711
- scenes: markdownAnchors(text).length
37079
+ scenes: novel ? novelNodes(text).length : markdownAnchors(text).length
36712
37080
  };
36713
37081
  }
36714
37082
  const tooBig$1 = () => new Refused("it is too big");
@@ -36912,7 +37280,7 @@ async function describe(file, ctx, spend) {
36912
37280
  const listScripts = {
36913
37281
  name: "list_scripts",
36914
37282
  title: "List scripts",
36915
- 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.",
37283
+ 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.",
36916
37284
  input,
36917
37285
  output,
36918
37286
  run
@@ -37992,11 +38360,11 @@ const draftsOutput = object({
37992
38360
  });
37993
38361
  const draftSummaries = new LoadCache(999);
37994
38362
  /** A draft's pages, scenes, and words: from its text, never returned. */
37995
- function figures(text, kind) {
38363
+ function figures(text, kind, novel = false) {
37996
38364
  if (kind === "markdown") return {
37997
38365
  pages: null,
37998
- scenes: markdownAnchors(text).length,
37999
- words: wordsIn(text)
38366
+ scenes: (novel ? novelAnchors(text) : markdownAnchors(text)).length,
38367
+ words: wordsIn(markdownProse(text))
38000
38368
  };
38001
38369
  const doc = parse(text);
38002
38370
  const template = templateFor(doc);
@@ -38009,7 +38377,7 @@ function figures(text, kind) {
38009
38377
  }
38010
38378
  /** Whether a draft's summary is kept already (so reading it again costs nothing). */
38011
38379
  const summarised = (file) => draftSummaries.get(file.real, file.size, file.mtimeMs) !== void 0;
38012
- async function summaryOf(file, kind) {
38380
+ async function summaryOf(file, kind, novel = false) {
38013
38381
  const cached = draftSummaries.get(file.real, file.size, file.mtimeMs);
38014
38382
  if (cached !== void 0) return cached;
38015
38383
  const read = await readDraft(file, kind);
@@ -38030,7 +38398,7 @@ async function summaryOf(file, kind) {
38030
38398
  scenes: null,
38031
38399
  words: null,
38032
38400
  figures: false
38033
- } : figures(read.text, kind),
38401
+ } : figures(read.text, kind, novel),
38034
38402
  matches: read.matches
38035
38403
  } };
38036
38404
  }
@@ -38040,6 +38408,7 @@ async function summaryOf(file, kind) {
38040
38408
  async function listDrafts(args, ctx) {
38041
38409
  const loaded = await load(args.script, ctx);
38042
38410
  const kind = kindOf(loaded);
38411
+ const isNovel = loaded.kind === "markdown" && loaded.novel !== null;
38043
38412
  const files = await draftFiles(loaded, ctx);
38044
38413
  const now = cursorBase("list_drafts", loaded, {
38045
38414
  script: args.script,
@@ -38070,7 +38439,7 @@ async function listDrafts(args, ctx) {
38070
38439
  const size = fresh ? checked.size : 0;
38071
38440
  const parse = fresh && checked.size <= 5242880 ? checked.size : 0;
38072
38441
  if (examined > 0 && (read + size > PAGE_READ_BYTES || parsed > 0 && parsed + parse > PAGE_PARSE_BYTES)) break;
38073
- const summary = checked ? await summaryOf(checked, kind) : null;
38442
+ const summary = checked ? await summaryOf(checked, kind, isNovel) : null;
38074
38443
  read += size;
38075
38444
  parsed += parse;
38076
38445
  if (!summary) {
@@ -38268,7 +38637,7 @@ async function compareDrafts(args, ctx) {
38268
38637
  left.notes += shown.leftOut.notes;
38269
38638
  left.boneyard += shown.leftOut.boneyard;
38270
38639
  }
38271
- const anchorsOf = kind === "markdown" ? markdownAnchors : sceneAnchors;
38640
+ const anchorsOf = kind !== "markdown" ? sceneAnchors : loaded.kind === "markdown" && loaded.novel !== null ? novelAnchors : markdownAnchors;
38272
38641
  const oldAnchors = anchorsOf(shownOld.text);
38273
38642
  const newAnchors = anchorsOf(shownNew.text);
38274
38643
  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.1",
3
+ "version": "0.26.1",
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",