@hanna84/mcp-writing 3.5.2 → 3.5.4
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/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/review-bundles/review-bundles-renderer.js +75 -19
- package/src/sync/importer.js +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v3.5.4](https://github.com/hannasdev/mcp-writing/compare/v3.5.3...v3.5.4)
|
|
8
|
+
|
|
9
|
+
- chore(skills): add reusable post-merge cleanup skill [`#186`](https://github.com/hannasdev/mcp-writing/pull/186)
|
|
10
|
+
|
|
11
|
+
#### [v3.5.3](https://github.com/hannasdev/mcp-writing/compare/v3.5.2...v3.5.3)
|
|
12
|
+
|
|
13
|
+
> 8 May 2026
|
|
14
|
+
|
|
15
|
+
- fix(review-bundles): keep epigraph prose without epigraph titles in beta exports [`#185`](https://github.com/hannasdev/mcp-writing/pull/185)
|
|
16
|
+
- Release 3.5.3 [`819bb0d`](https://github.com/hannasdev/mcp-writing/commit/819bb0da74dc984b6b4c0791be599174406682fa)
|
|
17
|
+
|
|
7
18
|
#### [v3.5.2](https://github.com/hannasdev/mcp-writing/compare/v3.5.1...v3.5.2)
|
|
8
19
|
|
|
20
|
+
> 8 May 2026
|
|
21
|
+
|
|
9
22
|
- Improve beta prose flow and centered scene dateline [`#184`](https://github.com/hannasdev/mcp-writing/pull/184)
|
|
23
|
+
- Release 3.5.2 [`5fb224a`](https://github.com/hannasdev/mcp-writing/commit/5fb224af5a66111c9b8d3d32e17acbb908fa5f97)
|
|
10
24
|
|
|
11
25
|
#### [v3.5.1](https://github.com/hannasdev/mcp-writing/compare/v3.5.0...v3.5.1)
|
|
12
26
|
|
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
|
|
|
47
47
|
| [docs/docker.md](docs/docker.md) | Docker Compose, OpenClaw integration, SSH hardening |
|
|
48
48
|
| [docs/data-ownership.md](docs/data-ownership.md) | Which tools write which files, import safety rules |
|
|
49
49
|
| [docs/tools.md](docs/tools.md) | Full tool reference — auto-generated from source |
|
|
50
|
+
| [docs/agents/README.md](docs/agents/README.md) | Index of agent-focused guidance, examples, and boot files |
|
|
51
|
+
| [docs/agents/use-cases.md](docs/agents/use-cases.md) | Agent-oriented transcripts and prompt/tooling examples |
|
|
50
52
|
| [docs/development.md](docs/development.md) | Running locally, tests, environment variables, troubleshooting |
|
|
51
53
|
|
|
52
54
|
## Breaking changes
|
package/package.json
CHANGED
|
@@ -57,7 +57,7 @@ function renderBetaFeedbackFormMarkdown({ projectId, recipientName, generatedAt
|
|
|
57
57
|
].join("\n") + "\n";
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function
|
|
60
|
+
function loadBundleSceneRowsWithTags(dbHandle, projectId, sceneIds) {
|
|
61
61
|
if (!Array.isArray(sceneIds) || sceneIds.length === 0) return [];
|
|
62
62
|
const rows = [];
|
|
63
63
|
// 900 is safely below SQLite's per-query bound of 999 host parameters
|
|
@@ -74,6 +74,7 @@ function loadBundleSceneRows(dbHandle, projectId, sceneIds) {
|
|
|
74
74
|
title,
|
|
75
75
|
part,
|
|
76
76
|
chapter,
|
|
77
|
+
chapter_title,
|
|
77
78
|
timeline_position,
|
|
78
79
|
logline,
|
|
79
80
|
pov,
|
|
@@ -85,12 +86,36 @@ function loadBundleSceneRows(dbHandle, projectId, sceneIds) {
|
|
|
85
86
|
rows.push(...chunkRows);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
const rowMap = new Map(rows.map(row => [row.scene_id, row]));
|
|
89
89
|
const orderedRows = [];
|
|
90
90
|
const missingSceneIds = [];
|
|
91
91
|
|
|
92
|
+
// Load tags in batches to avoid N+1 queries for large bundles.
|
|
93
|
+
const tagsBySceneId = new Map(rows.map(row => [row.scene_id, []]));
|
|
94
|
+
const tagChunkSize = 900;
|
|
95
|
+
for (let offset = 0; offset < rows.length; offset += tagChunkSize) {
|
|
96
|
+
const chunk = rows.slice(offset, offset + tagChunkSize);
|
|
97
|
+
const chunkSceneIds = chunk.map(row => row.scene_id);
|
|
98
|
+
if (chunkSceneIds.length === 0) continue;
|
|
99
|
+
const placeholders = chunkSceneIds.map(() => "?").join(",");
|
|
100
|
+
const tagRows = dbHandle.prepare(`
|
|
101
|
+
SELECT scene_id, tag
|
|
102
|
+
FROM scene_tags
|
|
103
|
+
WHERE project_id = ? AND scene_id IN (${placeholders})
|
|
104
|
+
`).all(projectId, ...chunkSceneIds);
|
|
105
|
+
for (const tagRow of tagRows) {
|
|
106
|
+
const tags = tagsBySceneId.get(tagRow.scene_id);
|
|
107
|
+
if (tags) tags.push(tagRow.tag);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rowsWithTags = rows.map(row => ({
|
|
112
|
+
...row,
|
|
113
|
+
tags: tagsBySceneId.get(row.scene_id) ?? [],
|
|
114
|
+
}));
|
|
115
|
+
const rowMapWithTags = new Map(rowsWithTags.map(row => [row.scene_id, row]));
|
|
116
|
+
|
|
92
117
|
for (const sceneId of sceneIds) {
|
|
93
|
-
const row =
|
|
118
|
+
const row = rowMapWithTags.get(sceneId);
|
|
94
119
|
if (row) {
|
|
95
120
|
orderedRows.push(row);
|
|
96
121
|
} else {
|
|
@@ -355,14 +380,27 @@ function renderSceneBlock(scene, options) {
|
|
|
355
380
|
includeSceneIds,
|
|
356
381
|
includeMetadataSidebar,
|
|
357
382
|
includeParagraphAnchors,
|
|
383
|
+
showChapterHeading,
|
|
358
384
|
} = options;
|
|
359
385
|
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
? `## ${escapeMarkdown(title)} (${escapeMarkdown(scene.scene_id)})`
|
|
363
|
-
: `## ${escapeMarkdown(title)}`;
|
|
386
|
+
const isBetaProfile = profile === "beta_reader_personalized";
|
|
387
|
+
const isEpigraph = isBetaProfile && scene.tags?.includes("epigraph");
|
|
364
388
|
|
|
365
|
-
const parts = [
|
|
389
|
+
const parts = [];
|
|
390
|
+
|
|
391
|
+
// Render chapter heading only when the caller detects a chapter transition.
|
|
392
|
+
if (isBetaProfile && scene.chapter_title && showChapterHeading) {
|
|
393
|
+
parts.push(`## ${escapeMarkdown(scene.chapter_title)}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Only render heading if not an epigraph
|
|
397
|
+
if (!isEpigraph) {
|
|
398
|
+
const title = scene.title || scene.scene_id;
|
|
399
|
+
const sceneHeading = includeSceneIds
|
|
400
|
+
? `## ${escapeMarkdown(title)} (${escapeMarkdown(scene.scene_id)})`
|
|
401
|
+
: `## ${escapeMarkdown(title)}`;
|
|
402
|
+
parts.push(sceneHeading);
|
|
403
|
+
}
|
|
366
404
|
|
|
367
405
|
if (profile === "outline_discussion") {
|
|
368
406
|
const summaryParts = [];
|
|
@@ -478,7 +516,7 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
478
516
|
const syncDir = syncDirOpt ?? process.env.WRITING_SYNC_DIR ?? null;
|
|
479
517
|
|
|
480
518
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
481
|
-
const rows =
|
|
519
|
+
const rows = loadBundleSceneRowsWithTags(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
482
520
|
const sections = [];
|
|
483
521
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
484
522
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
@@ -507,7 +545,8 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
507
545
|
);
|
|
508
546
|
}
|
|
509
547
|
|
|
510
|
-
for (
|
|
548
|
+
for (let sceneIndex = 0; sceneIndex < rows.length; sceneIndex += 1) {
|
|
549
|
+
const scene = rows[sceneIndex];
|
|
511
550
|
let prose = "";
|
|
512
551
|
if (profile === "editor_detailed" || profile === "beta_reader_personalized") {
|
|
513
552
|
const resolved = readProse(scene.file_path, { syncDir });
|
|
@@ -525,11 +564,16 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
525
564
|
prose = resolved;
|
|
526
565
|
}
|
|
527
566
|
const withProse = { ...scene, prose };
|
|
567
|
+
const prevScene = sceneIndex > 0 ? rows[sceneIndex - 1] : null;
|
|
568
|
+
const showChapterHeading = isBetaProfile
|
|
569
|
+
&& Boolean(scene.chapter_title)
|
|
570
|
+
&& (!prevScene || prevScene.chapter !== scene.chapter);
|
|
528
571
|
sections.push(renderSceneBlock(withProse, {
|
|
529
572
|
profile,
|
|
530
573
|
includeSceneIds,
|
|
531
574
|
includeMetadataSidebar,
|
|
532
575
|
includeParagraphAnchors,
|
|
576
|
+
showChapterHeading,
|
|
533
577
|
}));
|
|
534
578
|
}
|
|
535
579
|
|
|
@@ -558,7 +602,7 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
558
602
|
const metaFont = italicFont;
|
|
559
603
|
|
|
560
604
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
561
|
-
const rows =
|
|
605
|
+
const rows = loadBundleSceneRowsWithTags(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
562
606
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
563
607
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
564
608
|
const footerRecipientDisplayName = sanitizeFooterRecipientDisplayName(recipientDisplayName);
|
|
@@ -674,19 +718,31 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
674
718
|
doc.moveDown();
|
|
675
719
|
}
|
|
676
720
|
|
|
677
|
-
for (
|
|
721
|
+
for (let sceneIndex = 0; sceneIndex < rows.length; sceneIndex += 1) {
|
|
722
|
+
const scene = rows[sceneIndex];
|
|
723
|
+
const prevScene = sceneIndex > 0 ? rows[sceneIndex - 1] : null;
|
|
678
724
|
if (isBetaProfile) {
|
|
679
725
|
// Give chapter titles generous vertical breathing room for a
|
|
680
726
|
// print-like opening feel before prose begins.
|
|
681
727
|
doc.moveDown(2.0);
|
|
682
728
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
729
|
+
if (isBetaProfile && scene.chapter_title && (!prevScene || prevScene.chapter !== scene.chapter)) {
|
|
730
|
+
doc.fontSize(16).font(coverHeadingFont);
|
|
731
|
+
doc.text(scene.chapter_title, { align: "center" });
|
|
732
|
+
doc.moveDown(1.0);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Skip title rendering for epigraphs in beta profile
|
|
736
|
+
const isEpigraph = isBetaProfile && scene.tags?.includes("epigraph");
|
|
737
|
+
if (!isEpigraph) {
|
|
738
|
+
doc.fontSize(isBetaProfile ? 13 : 14).font(sceneHeadingFont);
|
|
739
|
+
let heading = scene.title || scene.scene_id;
|
|
740
|
+
if (includeSceneIds) {
|
|
741
|
+
heading += ` [${scene.scene_id}]`;
|
|
742
|
+
}
|
|
743
|
+
doc.text(heading, { align: isBetaProfile ? "center" : "left" });
|
|
744
|
+
doc.moveDown(isBetaProfile ? 1.6 : 0.2);
|
|
687
745
|
}
|
|
688
|
-
doc.text(heading, { align: isBetaProfile ? "center" : "left" });
|
|
689
|
-
doc.moveDown(isBetaProfile ? 1.6 : 0.2);
|
|
690
746
|
|
|
691
747
|
const metaParts = [];
|
|
692
748
|
if (profile !== "beta_reader_personalized") {
|
|
@@ -758,7 +814,7 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
758
814
|
// Add page break between scenes only for prose-including profiles where
|
|
759
815
|
// clear scene separation matters. For outline_discussion, let content flow.
|
|
760
816
|
const includesProse = profile === "editor_detailed" || profile === "beta_reader_personalized";
|
|
761
|
-
if (includesProse &&
|
|
817
|
+
if (includesProse && sceneIndex < rows.length - 1) {
|
|
762
818
|
doc.addPage();
|
|
763
819
|
}
|
|
764
820
|
}
|
package/src/sync/importer.js
CHANGED
|
@@ -358,12 +358,7 @@ export function importScrivenerSync({
|
|
|
358
358
|
continue;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
logger(` SKIP (epigraph) ${filename}`);
|
|
363
|
-
skipped++;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
|
|
361
|
+
const isEpigraphScene = isEpigraph(rawTitle);
|
|
367
362
|
const title = cleanTitle(rawTitle);
|
|
368
363
|
const existingScene = existingScenes.get(String(binderId)) ?? null;
|
|
369
364
|
const sceneId = existingScene?.meta?.scene_id ?? makeSceneId(binderId, title);
|
|
@@ -375,6 +370,10 @@ export function importScrivenerSync({
|
|
|
375
370
|
const destFile = path.join(targetDir, `${seq.toString().padStart(3, "0")} ${rawTitle} [${binderId}].${ext}`);
|
|
376
371
|
const sidecar = destFile.replace(/\.(txt|md)$/, ".meta.yaml");
|
|
377
372
|
|
|
373
|
+
const existingTags = Array.isArray(existingScene?.meta?.tags)
|
|
374
|
+
? existingScene.meta.tags
|
|
375
|
+
: [];
|
|
376
|
+
|
|
378
377
|
const meta = {
|
|
379
378
|
...(existingScene?.meta ?? {}),
|
|
380
379
|
scene_id: sceneId,
|
|
@@ -383,6 +382,7 @@ export function importScrivenerSync({
|
|
|
383
382
|
title,
|
|
384
383
|
timeline_position: seq,
|
|
385
384
|
...(beatCarry ? { save_the_cat_beat: beatCarry } : {}),
|
|
385
|
+
...(isEpigraphScene ? { tags: [...existingTags, "epigraph"].filter((v, i, a) => a.indexOf(v) === i) } : {}),
|
|
386
386
|
};
|
|
387
387
|
|
|
388
388
|
if (!beatCarry && existingScene?.meta && Object.hasOwn(existingScene.meta, "save_the_cat_beat")) {
|
|
@@ -424,7 +424,7 @@ export function importScrivenerSync({
|
|
|
424
424
|
logger("");
|
|
425
425
|
logger(`${"-".repeat(50)}`);
|
|
426
426
|
logger(`Created: ${created} sidecars${dryRun ? " (dry run)" : ""}`);
|
|
427
|
-
logger(`Skipped: ${skipped} (empty /
|
|
427
|
+
logger(`Skipped: ${skipped} (empty / pattern)`);
|
|
428
428
|
if (existing) logger(`Existing: ${existing} already had sidecars`);
|
|
429
429
|
logger(`Beat markers seen: ${beatMarkersSeen}`);
|
|
430
430
|
|