narrarium 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/repo.js CHANGED
@@ -7798,6 +7798,595 @@ function buildVscodeMcpConfig() {
7798
7798
  "",
7799
7799
  ].join("\n");
7800
7800
  }
7801
+ function buildVscodeTasksConfig() {
7802
+ return JSON.stringify({
7803
+ version: "2.0.0",
7804
+ tasks: [
7805
+ {
7806
+ label: "Build Manuscript (Full + Sample)",
7807
+ type: "shell",
7808
+ command: "node scripts/run-manuscript.mjs",
7809
+ group: { kind: "build", isDefault: true },
7810
+ presentation: { reveal: "always", panel: "shared" },
7811
+ problemMatcher: [],
7812
+ },
7813
+ {
7814
+ label: "Build Manuscript (Full Only)",
7815
+ type: "shell",
7816
+ command: "node scripts/run-manuscript.mjs --full-only",
7817
+ group: "build",
7818
+ presentation: { reveal: "always", panel: "shared" },
7819
+ problemMatcher: [],
7820
+ },
7821
+ {
7822
+ label: "Build Manuscript (Sample Only)",
7823
+ type: "shell",
7824
+ command: "node scripts/run-manuscript.mjs --sample-only",
7825
+ group: "build",
7826
+ presentation: { reveal: "always", panel: "shared" },
7827
+ problemMatcher: [],
7828
+ },
7829
+ {
7830
+ label: "Install Manuscript Dependencies",
7831
+ type: "shell",
7832
+ command: "pip install -r scripts/requirements-manuscript.txt",
7833
+ presentation: { reveal: "always", panel: "shared" },
7834
+ problemMatcher: [],
7835
+ },
7836
+ ],
7837
+ }, null, 2) + "\n";
7838
+ }
7839
+ function buildManuscriptConfig() {
7840
+ return [
7841
+ "# Narrarium Manuscript Export Settings",
7842
+ "# This file controls how the .docx manuscript is generated.",
7843
+ "# Edit these values and run the VS Code build task (Ctrl+Shift+B).",
7844
+ "",
7845
+ "# Number of chapters to include in the sample manuscript for editors.",
7846
+ "# Set to 0 to skip sample generation.",
7847
+ "sample_chapters: 5",
7848
+ "",
7849
+ "# Whether to show paragraph/scene titles in the manuscript.",
7850
+ "# In standard manuscript format these are typically hidden.",
7851
+ "show_paragraph_titles: false",
7852
+ "",
7853
+ "# Whether to include chapter summaries (from frontmatter) in the manuscript.",
7854
+ "show_chapter_summary: false",
7855
+ "",
7856
+ "# Font settings (standard manuscript format uses Times New Roman 12pt).",
7857
+ "font_name: Times New Roman",
7858
+ "font_size: 12",
7859
+ "",
7860
+ "# Line spacing (2.0 = double-spaced, standard for submissions).",
7861
+ "line_spacing: 2.0",
7862
+ "",
7863
+ "# Page margins in inches (1.0 is standard).",
7864
+ "margin_inches: 1.0",
7865
+ "",
7866
+ "# First-line paragraph indent in inches.",
7867
+ "paragraph_indent_inches: 0.5",
7868
+ "",
7869
+ "# Minimum consecutive blank lines to create a new paragraph (with indent).",
7870
+ "# Lines below this threshold are joined into the current paragraph.",
7871
+ "# Default 3 means: \\n\\n\\n starts a new paragraph, \\n\\n does not.",
7872
+ "paragraph_break_newlines: 3",
7873
+ "",
7874
+ "# Scene break marker between paragraphs/scenes within a chapter.",
7875
+ "scene_break: \"#\"",
7876
+ "",
7877
+ "# Output directory (relative to book root).",
7878
+ "output_dir: build",
7879
+ "",
7880
+ "# Output filenames.",
7881
+ "full_filename: manuscript.docx",
7882
+ "sample_filename: manuscript-sample.docx",
7883
+ "",
7884
+ "# Whether to include a title page.",
7885
+ "include_title_page: true",
7886
+ "",
7887
+ "# Page size: 'letter' (US 8.5x11) or 'a4'.",
7888
+ "page_size: letter",
7889
+ "",
7890
+ ].join("\n");
7891
+ }
7892
+ function buildManuscriptScript() {
7893
+ return [
7894
+ '#!/usr/bin/env python3',
7895
+ '"""',
7896
+ 'Narrarium Manuscript Builder',
7897
+ '',
7898
+ 'Generates submission-ready .docx manuscripts from a Narrarium book repository.',
7899
+ 'Follows standard manuscript format conventions:',
7900
+ ' - Times New Roman 12pt (configurable)',
7901
+ ' - Double-spaced',
7902
+ ' - 1-inch margins',
7903
+ ' - Title page with title, author, word count',
7904
+ ' - Running header: Author / Title / Page',
7905
+ ' - 0.5-inch first-line paragraph indent',
7906
+ ' - Chapter breaks on new pages',
7907
+ ' - Scene breaks marked with #',
7908
+ '',
7909
+ 'Outputs:',
7910
+ ' - Full manuscript: build/manuscript.docx',
7911
+ ' - Sample chapters: build/manuscript-sample.docx (first N chapters, default 5)',
7912
+ '',
7913
+ 'Usage:',
7914
+ ' python build_manuscript.py [--book-root <path>] [--config <path>]',
7915
+ '',
7916
+ 'Reads settings from manuscript.yaml in the book root.',
7917
+ '"""',
7918
+ '',
7919
+ 'import argparse',
7920
+ 'import re',
7921
+ 'import sys',
7922
+ 'from pathlib import Path',
7923
+ '',
7924
+ 'try:',
7925
+ ' import yaml',
7926
+ 'except ImportError:',
7927
+ ' yaml = None',
7928
+ '',
7929
+ 'try:',
7930
+ ' from docx import Document',
7931
+ ' from docx.shared import Pt, Inches, Cm',
7932
+ ' from docx.enum.text import WD_ALIGN_PARAGRAPH',
7933
+ ' from docx.oxml.ns import qn',
7934
+ ' from docx.oxml import OxmlElement',
7935
+ 'except ImportError:',
7936
+ ' print(',
7937
+ ' "ERROR: python-docx is required. Install it with:\\n"',
7938
+ ' " pip install python-docx\\n"',
7939
+ ' "Or:\\n"',
7940
+ ' " pip install -r requirements-manuscript.txt",',
7941
+ ' file=sys.stderr,',
7942
+ ' )',
7943
+ ' sys.exit(1)',
7944
+ '',
7945
+ '',
7946
+ '_FRONTMATTER_RE = re.compile(r"^---\\s*\\n(.*?)\\n---\\s*\\n?", re.DOTALL)',
7947
+ '',
7948
+ '',
7949
+ 'def strip_frontmatter(text: str) -> tuple[dict, str]:',
7950
+ ' match = _FRONTMATTER_RE.match(text)',
7951
+ ' if not match:',
7952
+ ' return {}, text',
7953
+ ' fm_raw = match.group(1)',
7954
+ ' body = text[match.end():]',
7955
+ ' fm: dict = {}',
7956
+ ' if yaml is not None:',
7957
+ ' try:',
7958
+ ' fm = yaml.safe_load(fm_raw) or {}',
7959
+ ' except Exception:',
7960
+ ' fm = {}',
7961
+ ' else:',
7962
+ ' for line in fm_raw.splitlines():',
7963
+ ' if ":" in line:',
7964
+ ' key, _, value = line.partition(":")',
7965
+ ' key = key.strip()',
7966
+ ' value = value.strip().strip(\'"\').strip("\\\'")',
7967
+ ' if value:',
7968
+ ' fm[key] = value',
7969
+ ' return fm, body',
7970
+ '',
7971
+ '',
7972
+ 'def read_book_metadata(book_root: Path) -> dict:',
7973
+ ' book_file = book_root / "book.md"',
7974
+ ' if not book_file.exists():',
7975
+ ' print(f"ERROR: {book_file} not found.", file=sys.stderr)',
7976
+ ' sys.exit(1)',
7977
+ ' fm, _ = strip_frontmatter(book_file.read_text(encoding="utf-8"))',
7978
+ ' return fm',
7979
+ '',
7980
+ '',
7981
+ 'def list_chapters(book_root: Path) -> list[dict]:',
7982
+ ' chapters_dir = book_root / "chapters"',
7983
+ ' if not chapters_dir.is_dir():',
7984
+ ' return []',
7985
+ ' chapters = []',
7986
+ ' for entry in sorted(chapters_dir.iterdir()):',
7987
+ ' if not entry.is_dir():',
7988
+ ' continue',
7989
+ ' chapter_file = entry / "chapter.md"',
7990
+ ' if not chapter_file.exists():',
7991
+ ' continue',
7992
+ ' fm, body = strip_frontmatter(chapter_file.read_text(encoding="utf-8"))',
7993
+ ' chapters.append({"slug": entry.name, "path": entry, "metadata": fm, "body": body, "number": fm.get("number", 0)})',
7994
+ ' chapters.sort(key=lambda c: c["number"])',
7995
+ ' return chapters',
7996
+ '',
7997
+ '',
7998
+ 'def read_paragraphs(chapter_path: Path) -> list[dict]:',
7999
+ ' skip_files = {"chapter.md", "writing-style.md", "notes.md", "ideas.md", "promoted.md"}',
8000
+ ' paragraphs = []',
8001
+ ' for md_file in sorted(chapter_path.glob("*.md")):',
8002
+ ' if md_file.name in skip_files:',
8003
+ ' continue',
8004
+ ' fm, body = strip_frontmatter(md_file.read_text(encoding="utf-8"))',
8005
+ ' paragraphs.append({"metadata": fm, "body": body.strip(), "number": fm.get("number", 0)})',
8006
+ ' paragraphs.sort(key=lambda p: p["number"])',
8007
+ ' return paragraphs',
8008
+ '',
8009
+ '',
8010
+ 'DEFAULT_SETTINGS = {',
8011
+ ' "sample_chapters": 5,',
8012
+ ' "show_paragraph_titles": False,',
8013
+ ' "show_chapter_summary": False,',
8014
+ ' "font_name": "Times New Roman",',
8015
+ ' "font_size": 12,',
8016
+ ' "line_spacing": 2.0,',
8017
+ ' "margin_inches": 1.0,',
8018
+ ' "paragraph_indent_inches": 0.5,',
8019
+ ' "paragraph_break_newlines": 3,',
8020
+ ' "scene_break": "#",',
8021
+ ' "output_dir": "build",',
8022
+ ' "full_filename": "manuscript.docx",',
8023
+ ' "sample_filename": "manuscript-sample.docx",',
8024
+ ' "include_title_page": True,',
8025
+ ' "page_size": "letter",',
8026
+ '}',
8027
+ '',
8028
+ '',
8029
+ 'def load_settings(book_root: Path, config_path=None) -> dict:',
8030
+ ' settings = dict(DEFAULT_SETTINGS)',
8031
+ ' candidates = []',
8032
+ ' if config_path:',
8033
+ ' candidates.append(Path(config_path))',
8034
+ ' candidates.append(book_root / "manuscript.yaml")',
8035
+ ' candidates.append(book_root / "manuscript.yml")',
8036
+ ' for candidate in candidates:',
8037
+ ' if candidate.exists():',
8038
+ ' raw = candidate.read_text(encoding="utf-8")',
8039
+ ' if yaml is not None:',
8040
+ ' user = yaml.safe_load(raw) or {}',
8041
+ ' else:',
8042
+ ' user = {}',
8043
+ ' for line in raw.splitlines():',
8044
+ ' if ":" in line and not line.strip().startswith("#"):',
8045
+ ' key, _, value = line.partition(":")',
8046
+ ' key = key.strip()',
8047
+ ' value = value.strip().strip(\'"\').strip("\\\'")',
8048
+ ' if value.lower() == "true":',
8049
+ ' value = True',
8050
+ ' elif value.lower() == "false":',
8051
+ ' value = False',
8052
+ ' else:',
8053
+ ' try:',
8054
+ ' value = int(value)',
8055
+ ' except ValueError:',
8056
+ ' try:',
8057
+ ' value = float(value)',
8058
+ ' except ValueError:',
8059
+ ' pass',
8060
+ ' if value != "":',
8061
+ ' user[key] = value',
8062
+ ' settings.update(user)',
8063
+ ' break',
8064
+ ' return settings',
8065
+ '',
8066
+ '',
8067
+ 'def count_words(text: str) -> int:',
8068
+ ' return len(text.split())',
8069
+ '',
8070
+ '',
8071
+ 'def set_page_size_and_margins(section, settings: dict):',
8072
+ ' if settings["page_size"] == "a4":',
8073
+ ' section.page_width = Cm(21.0)',
8074
+ ' section.page_height = Cm(29.7)',
8075
+ ' else:',
8076
+ ' section.page_width = Inches(8.5)',
8077
+ ' section.page_height = Inches(11)',
8078
+ ' margin = Inches(settings["margin_inches"])',
8079
+ ' section.top_margin = margin',
8080
+ ' section.bottom_margin = margin',
8081
+ ' section.left_margin = margin',
8082
+ ' section.right_margin = margin',
8083
+ '',
8084
+ '',
8085
+ 'def add_header(section, author: str, title: str, font_name: str, font_size: int):',
8086
+ ' header = section.header',
8087
+ ' header.is_linked_to_previous = False',
8088
+ ' paragraph = header.paragraphs[0] if header.paragraphs else header.add_paragraph()',
8089
+ ' paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT',
8090
+ ' short_title = title[:30].upper() if len(title) > 30 else title.upper()',
8091
+ ' author_last = author.split()[-1] if author else "AUTHOR"',
8092
+ ' run = paragraph.add_run(f"{author_last} / {short_title} / ")',
8093
+ ' run.font.name = font_name',
8094
+ ' run.font.size = Pt(font_size)',
8095
+ ' fld_char_begin = OxmlElement("w:fldChar")',
8096
+ ' fld_char_begin.set(qn("w:fldCharType"), "begin")',
8097
+ ' run_page = paragraph.add_run()',
8098
+ ' run_page._r.append(fld_char_begin)',
8099
+ ' instr_text = OxmlElement("w:instrText")',
8100
+ ' instr_text.set(qn("xml:space"), "preserve")',
8101
+ ' instr_text.text = " PAGE "',
8102
+ ' run_page2 = paragraph.add_run()',
8103
+ ' run_page2.font.name = font_name',
8104
+ ' run_page2.font.size = Pt(font_size)',
8105
+ ' run_page2._r.append(instr_text)',
8106
+ ' fld_char_end = OxmlElement("w:fldChar")',
8107
+ ' fld_char_end.set(qn("w:fldCharType"), "end")',
8108
+ ' run_page3 = paragraph.add_run()',
8109
+ ' run_page3._r.append(fld_char_end)',
8110
+ '',
8111
+ '',
8112
+ 'def configure_style(doc, settings: dict):',
8113
+ ' style = doc.styles["Normal"]',
8114
+ ' font = style.font',
8115
+ ' font.name = settings["font_name"]',
8116
+ ' font.size = Pt(settings["font_size"])',
8117
+ ' pf = style.paragraph_format',
8118
+ ' pf.line_spacing = settings["line_spacing"]',
8119
+ ' pf.space_before = Pt(0)',
8120
+ ' pf.space_after = Pt(0)',
8121
+ ' pf.first_line_indent = Inches(settings["paragraph_indent_inches"])',
8122
+ ' if "Heading 1" in doc.styles:',
8123
+ ' h1 = doc.styles["Heading 1"]',
8124
+ ' h1.font.name = settings["font_name"]',
8125
+ ' h1.font.size = Pt(settings["font_size"])',
8126
+ ' h1.font.bold = True',
8127
+ ' h1.font.color.rgb = None',
8128
+ ' h1.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER',
8129
+ ' h1.paragraph_format.space_before = Pt(72)',
8130
+ ' h1.paragraph_format.space_after = Pt(24)',
8131
+ ' h1.paragraph_format.first_line_indent = Inches(0)',
8132
+ ' h1.paragraph_format.line_spacing = settings["line_spacing"]',
8133
+ ' h1.paragraph_format.page_break_before = True',
8134
+ ' if "Heading 2" in doc.styles:',
8135
+ ' h2 = doc.styles["Heading 2"]',
8136
+ ' h2.font.name = settings["font_name"]',
8137
+ ' h2.font.size = Pt(settings["font_size"])',
8138
+ ' h2.font.bold = True',
8139
+ ' h2.font.italic = False',
8140
+ ' h2.font.color.rgb = None',
8141
+ ' h2.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT',
8142
+ ' h2.paragraph_format.space_before = Pt(12)',
8143
+ ' h2.paragraph_format.space_after = Pt(6)',
8144
+ ' h2.paragraph_format.first_line_indent = Inches(0)',
8145
+ ' h2.paragraph_format.line_spacing = settings["line_spacing"]',
8146
+ '',
8147
+ '',
8148
+ 'def add_title_page(doc, book: dict, word_count: int, settings: dict):',
8149
+ ' author = book.get("author", "")',
8150
+ ' if author:',
8151
+ ' contact = doc.add_paragraph()',
8152
+ ' contact.alignment = WD_ALIGN_PARAGRAPH.LEFT',
8153
+ ' contact.paragraph_format.first_line_indent = Inches(0)',
8154
+ ' contact.paragraph_format.space_after = Pt(0)',
8155
+ ' run = contact.add_run(author)',
8156
+ ' run.font.name = settings["font_name"]',
8157
+ ' run.font.size = Pt(settings["font_size"])',
8158
+ ' wc_para = doc.add_paragraph()',
8159
+ ' wc_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT',
8160
+ ' wc_para.paragraph_format.first_line_indent = Inches(0)',
8161
+ ' wc_para.paragraph_format.space_after = Pt(0)',
8162
+ ' rounded_wc = round(word_count / 100) * 100 if word_count > 500 else word_count',
8163
+ ' run = wc_para.add_run(f"Approx. {rounded_wc:,} words")',
8164
+ ' run.font.name = settings["font_name"]',
8165
+ ' run.font.size = Pt(settings["font_size"])',
8166
+ ' for _ in range(10):',
8167
+ ' spacer = doc.add_paragraph()',
8168
+ ' spacer.paragraph_format.first_line_indent = Inches(0)',
8169
+ ' spacer.paragraph_format.space_before = Pt(0)',
8170
+ ' spacer.paragraph_format.space_after = Pt(0)',
8171
+ ' title_para = doc.add_paragraph()',
8172
+ ' title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER',
8173
+ ' title_para.paragraph_format.first_line_indent = Inches(0)',
8174
+ ' title_para.paragraph_format.space_after = Pt(12)',
8175
+ ' run = title_para.add_run(book.get("title", "Untitled"))',
8176
+ ' run.font.name = settings["font_name"]',
8177
+ ' run.font.size = Pt(settings["font_size"])',
8178
+ ' run.bold = True',
8179
+ ' if author:',
8180
+ ' by_para = doc.add_paragraph()',
8181
+ ' by_para.alignment = WD_ALIGN_PARAGRAPH.CENTER',
8182
+ ' by_para.paragraph_format.first_line_indent = Inches(0)',
8183
+ ' run = by_para.add_run(f"by {author}")',
8184
+ ' run.font.name = settings["font_name"]',
8185
+ ' run.font.size = Pt(settings["font_size"])',
8186
+ ' genre = book.get("genre")',
8187
+ ' if genre:',
8188
+ ' genre_para = doc.add_paragraph()',
8189
+ ' genre_para.alignment = WD_ALIGN_PARAGRAPH.CENTER',
8190
+ ' genre_para.paragraph_format.first_line_indent = Inches(0)',
8191
+ ' run = genre_para.add_run(genre)',
8192
+ ' run.font.name = settings["font_name"]',
8193
+ ' run.font.size = Pt(settings["font_size"])',
8194
+ '',
8195
+ '',
8196
+ 'def add_chapter(doc, chapter: dict, settings: dict, is_first_chapter: bool = False):',
8197
+ ' chapter_meta = chapter["metadata"]',
8198
+ ' chapter_title = chapter_meta.get("title", f"Chapter {chapter_meta.get(\'number\', \'?\')}")',
8199
+ ' chapter_number = chapter_meta.get("number", "")',
8200
+ ' heading_text = f"Chapter {chapter_number}" if chapter_number else chapter_title',
8201
+ ' h = doc.add_heading(heading_text, level=1)',
8202
+ ' if is_first_chapter:',
8203
+ ' h.paragraph_format.page_break_before = True',
8204
+ ' if chapter_number and chapter_title != f"Chapter {chapter_number}":',
8205
+ ' subtitle = doc.add_paragraph()',
8206
+ ' subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER',
8207
+ ' subtitle.paragraph_format.first_line_indent = Inches(0)',
8208
+ ' subtitle.paragraph_format.space_after = Pt(24)',
8209
+ ' run = subtitle.add_run(chapter_title)',
8210
+ ' run.font.name = settings["font_name"]',
8211
+ ' run.font.size = Pt(settings["font_size"])',
8212
+ ' run.italic = True',
8213
+ ' if settings["show_chapter_summary"]:',
8214
+ ' summary = chapter_meta.get("summary", "")',
8215
+ ' if summary:',
8216
+ ' summary_para = doc.add_paragraph()',
8217
+ ' summary_para.paragraph_format.first_line_indent = Inches(0)',
8218
+ ' summary_para.paragraph_format.space_after = Pt(12)',
8219
+ ' run = summary_para.add_run(summary)',
8220
+ ' run.font.name = settings["font_name"]',
8221
+ ' run.font.size = Pt(settings["font_size"])',
8222
+ ' run.italic = True',
8223
+ ' paragraphs = read_paragraphs(chapter["path"])',
8224
+ ' for idx, para in enumerate(paragraphs):',
8225
+ ' if settings["show_paragraph_titles"]:',
8226
+ ' para_title = para["metadata"].get("title", "")',
8227
+ ' if para_title:',
8228
+ ' doc.add_heading(para_title, level=2)',
8229
+ ' if idx > 0 and not settings["show_paragraph_titles"]:',
8230
+ ' scene_break = doc.add_paragraph()',
8231
+ ' scene_break.alignment = WD_ALIGN_PARAGRAPH.CENTER',
8232
+ ' scene_break.paragraph_format.first_line_indent = Inches(0)',
8233
+ ' scene_break.paragraph_format.space_before = Pt(12)',
8234
+ ' scene_break.paragraph_format.space_after = Pt(12)',
8235
+ ' run = scene_break.add_run(settings["scene_break"])',
8236
+ ' run.font.name = settings["font_name"]',
8237
+ ' run.font.size = Pt(settings["font_size"])',
8238
+ ' body = para["body"]',
8239
+ ' if not body:',
8240
+ ' continue',
8241
+ ' # Split body into paragraphs using the configured newline threshold.',
8242
+ ' break_nl = settings.get("paragraph_break_newlines", 3)',
8243
+ ' sep = "\\n" * max(break_nl, 2)',
8244
+ ' for text_para in body.split(sep):',
8245
+ ' text_para = text_para.strip()',
8246
+ ' if not text_para:',
8247
+ ' continue',
8248
+ ' # Collapse remaining newlines (below threshold) into spaces.',
8249
+ ' text_para = re.sub(r"\\n+", " ", text_para)',
8250
+ ' doc.add_paragraph(text_para)',
8251
+ '',
8252
+ '',
8253
+ 'def build_manuscript(book_root, chapters, book_meta, settings, output_path, label="manuscript"):',
8254
+ ' doc = Document()',
8255
+ ' # Set compatibility mode to Word 2016+ (version 15) to avoid',
8256
+ ' # "older file type" warnings in modern Word.',
8257
+ ' import lxml.etree as ET',
8258
+ ' compat = doc.settings.element.makeelement(',
8259
+ ' "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}compat", {}',
8260
+ ' )',
8261
+ ' compat_setting = compat.makeelement(',
8262
+ ' "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}compatSetting",',
8263
+ ' {',
8264
+ ' "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}name": "compatibilityMode",',
8265
+ ' "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}uri": "http://schemas.microsoft.com/office/word",',
8266
+ ' "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val": "15",',
8267
+ ' },',
8268
+ ' )',
8269
+ ' compat.append(compat_setting)',
8270
+ ' doc.settings.element.append(compat)',
8271
+ ' configure_style(doc, settings)',
8272
+ ' section = doc.sections[0]',
8273
+ ' set_page_size_and_margins(section, settings)',
8274
+ ' total_words = 0',
8275
+ ' for ch in chapters:',
8276
+ ' for para in read_paragraphs(ch["path"]):',
8277
+ ' total_words += count_words(para["body"])',
8278
+ ' add_header(section, book_meta.get("author", ""), book_meta.get("title", ""), settings["font_name"], settings["font_size"])',
8279
+ ' if settings["include_title_page"]:',
8280
+ ' add_title_page(doc, book_meta, total_words, settings)',
8281
+ ' for idx, chapter in enumerate(chapters):',
8282
+ ' add_chapter(doc, chapter, settings, is_first_chapter=(idx == 0))',
8283
+ ' output_path.parent.mkdir(parents=True, exist_ok=True)',
8284
+ ' doc.save(str(output_path))',
8285
+ ' return total_words',
8286
+ '',
8287
+ '',
8288
+ 'def main():',
8289
+ ' parser = argparse.ArgumentParser(description="Build .docx manuscripts from a Narrarium book.")',
8290
+ ' parser.add_argument("--book-root", default=".", help="Path to the book root")',
8291
+ ' parser.add_argument("--config", default=None, help="Path to manuscript.yaml")',
8292
+ ' parser.add_argument("--full-only", action="store_true", help="Only generate full manuscript")',
8293
+ ' parser.add_argument("--sample-only", action="store_true", help="Only generate sample manuscript")',
8294
+ ' args = parser.parse_args()',
8295
+ ' book_root = Path(args.book_root).resolve()',
8296
+ ' settings = load_settings(book_root, args.config)',
8297
+ ' book_meta = read_book_metadata(book_root)',
8298
+ ' chapters = list_chapters(book_root)',
8299
+ ' if not chapters:',
8300
+ ' print("WARNING: No chapters found.", file=sys.stderr)',
8301
+ ' sys.exit(0)',
8302
+ ' output_dir = book_root / settings["output_dir"]',
8303
+ ' results = []',
8304
+ ' if not args.sample_only:',
8305
+ ' full_path = output_dir / settings["full_filename"]',
8306
+ ' wc = build_manuscript(book_root, chapters, book_meta, settings, full_path, "full")',
8307
+ ' results.append(("Full manuscript", full_path, len(chapters), wc))',
8308
+ ' if not args.full_only:',
8309
+ ' sample_count = int(settings["sample_chapters"])',
8310
+ ' if sample_count > 0 and len(chapters) >= sample_count:',
8311
+ ' sample_chapters = chapters[:sample_count]',
8312
+ ' sample_path = output_dir / settings["sample_filename"]',
8313
+ ' swc = build_manuscript(book_root, sample_chapters, book_meta, settings, sample_path, "sample")',
8314
+ ' results.append(("Sample manuscript", sample_path, len(sample_chapters), swc))',
8315
+ ' elif sample_count > 0:',
8316
+ ' print(f"NOTE: {len(chapters)} chapters < sample_chapters={sample_count}. Skipping sample.", file=sys.stderr)',
8317
+ ' print()',
8318
+ ' print("=" * 60)',
8319
+ ' print(" NARRARIUM MANUSCRIPT BUILD COMPLETE")',
8320
+ ' print("=" * 60)',
8321
+ ' for label, p, cc, wc in results:',
8322
+ ' print(f" {label}:")',
8323
+ ' print(f" File: {p}")',
8324
+ ' print(f" Chapters: {cc}")',
8325
+ ' print(f" Words: ~{wc:,}")',
8326
+ ' print()',
8327
+ ' print(f" Format: Standard Manuscript Format")',
8328
+ ' print(f" Font: {settings[\'font_name\']} {settings[\'font_size\']}pt")',
8329
+ ' print(f" Spacing: {settings[\'line_spacing\']}x")',
8330
+ ' print(f" Margins: {settings[\'margin_inches\']}\\"")',
8331
+ ' print("=" * 60)',
8332
+ '',
8333
+ '',
8334
+ 'if __name__ == "__main__":',
8335
+ ' main()',
8336
+ '',
8337
+ ].join("\n");
8338
+ }
8339
+ function buildManuscriptRequirements() {
8340
+ return [
8341
+ "python-docx>=1.1.0",
8342
+ "PyYAML>=6.0",
8343
+ "",
8344
+ ].join("\n");
8345
+ }
8346
+ function buildManuscriptRunner() {
8347
+ return [
8348
+ "#!/usr/bin/env node",
8349
+ "// Thin wrapper that invokes the Python manuscript builder.",
8350
+ "// If Python is not installed it prints a friendly warning and exits cleanly.",
8351
+ 'import { execFileSync } from "node:child_process";',
8352
+ 'import { existsSync } from "node:fs";',
8353
+ 'import { join, dirname } from "node:path";',
8354
+ 'import { fileURLToPath } from "node:url";',
8355
+ "",
8356
+ 'const __dirname = dirname(fileURLToPath(import.meta.url));',
8357
+ 'const script = join(__dirname, "build_manuscript.py");',
8358
+ "",
8359
+ "function findPython() {",
8360
+ ' for (const cmd of ["python3", "python"]) {',
8361
+ " try {",
8362
+ ' execFileSync(cmd, ["--version"], { stdio: "pipe" });',
8363
+ " return cmd;",
8364
+ " } catch { /* not found, try next */ }",
8365
+ " }",
8366
+ " return null;",
8367
+ "}",
8368
+ "",
8369
+ "const py = findPython();",
8370
+ "if (!py) {",
8371
+ ' console.warn("");',
8372
+ ' console.warn("[WARNING] Python is not installed or not in PATH.");',
8373
+ ' console.warn(" Manuscript export requires Python 3 and the python-docx package.");',
8374
+ ' console.warn(" Install Python from https://www.python.org/downloads/");',
8375
+ ' console.warn(" Then run: pip install -r scripts/requirements-manuscript.txt");',
8376
+ ' console.warn("");',
8377
+ " process.exit(1);",
8378
+ "}",
8379
+ "",
8380
+ "// Forward remaining CLI args to the Python script.",
8381
+ 'const args = [script, "--book-root", ".", ...process.argv.slice(2)];',
8382
+ "try {",
8383
+ ' execFileSync(py, args, { stdio: "inherit" });',
8384
+ "} catch (err) {",
8385
+ " process.exit(err.status ?? 1);",
8386
+ "}",
8387
+ "",
8388
+ ].join("\n");
8389
+ }
7801
8390
  function buildGithubCopilotInstructions() {
7802
8391
  // Same content as skillTemplate but without the YAML frontmatter block,
7803
8392
  // since .github/copilot-instructions.md must be plain markdown.
@@ -7933,6 +8522,9 @@ function getManagedBookScaffoldFiles(createSkills) {
7933
8522
  { relativePath: ".opencode/commands/resume-book.md", content: buildResumeBookCommand() },
7934
8523
  { relativePath: ".opencode/plugins/conversation-export.js", content: buildConversationExportPlugin() },
7935
8524
  { relativePath: "conversations/README.md", content: buildConversationsReadme() },
8525
+ { relativePath: "scripts/build_manuscript.py", content: buildManuscriptScript() },
8526
+ { relativePath: "scripts/requirements-manuscript.txt", content: buildManuscriptRequirements() },
8527
+ { relativePath: "scripts/run-manuscript.mjs", content: buildManuscriptRunner() },
7936
8528
  ];
7937
8529
  }
7938
8530
  // Files created once and never overwritten on upgrade.
@@ -7941,6 +8533,8 @@ function getInitOnlyBookScaffoldFiles() {
7941
8533
  return [
7942
8534
  { relativePath: "opencode.jsonc", content: buildOpencodeProjectConfig() },
7943
8535
  { relativePath: ".vscode/mcp.json", content: buildVscodeMcpConfig() },
8536
+ { relativePath: ".vscode/tasks.json", content: buildVscodeTasksConfig() },
8537
+ { relativePath: "manuscript.yaml", content: buildManuscriptConfig() },
7944
8538
  ];
7945
8539
  }
7946
8540
  function buildResumeBookCommand() {