@hanna84/mcp-writing 3.17.2 → 3.18.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.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/core/helpers.js +9 -8
- package/src/sync/metadata-lint.js +2 -0
- package/src/tools/search.js +9 -5
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.18.1](https://github.com/hannasdev/mcp-writing/compare/v3.18.0...v3.18.1)
|
|
8
|
+
|
|
9
|
+
- docs(product): activate database backup initiative [`#218`](https://github.com/hannasdev/mcp-writing/pull/218)
|
|
10
|
+
|
|
11
|
+
#### [v3.18.0](https://github.com/hannasdev/mcp-writing/compare/v3.17.2...v3.18.0)
|
|
12
|
+
|
|
13
|
+
> 23 May 2026
|
|
14
|
+
|
|
15
|
+
- feat: complete chapter structure alignment [`#217`](https://github.com/hannasdev/mcp-writing/pull/217)
|
|
16
|
+
- Release 3.18.0 [`c421c1b`](https://github.com/hannasdev/mcp-writing/commit/c421c1b4fa18a7c36f471b6d42a97f324a3b5f50)
|
|
17
|
+
|
|
7
18
|
#### [v3.17.2](https://github.com/hannasdev/mcp-writing/compare/v3.17.1...v3.17.2)
|
|
8
19
|
|
|
20
|
+
> 23 May 2026
|
|
21
|
+
|
|
9
22
|
- docs: complete Docker deployment readiness [`#216`](https://github.com/hannasdev/mcp-writing/pull/216)
|
|
23
|
+
- Release 3.17.2 [`e2fb946`](https://github.com/hannasdev/mcp-writing/commit/e2fb946bdbae6ca4c8216edfc578d94ceda7fabf)
|
|
10
24
|
|
|
11
25
|
#### [v3.17.1](https://github.com/hannasdev/mcp-writing/compare/v3.17.0...v3.17.1)
|
|
12
26
|
|
package/README.md
CHANGED
|
@@ -30,8 +30,8 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
|
|
|
30
30
|
- **Core platform complete:** Metadata-first analysis, sidecar-backed metadata maintenance, AI-assisted prose editing with confirmation + git history, review bundles, and Scrivener Direct extraction are all implemented.
|
|
31
31
|
- **Recently completed:** Docker, CI, and Deployment Workflow made Docker a supported way to build, run, smoke-test, and deploy Writing MCP.
|
|
32
32
|
- **Previous milestone:** Filesystem Boundary Hardening centralized local file mutation through application-aware helpers and lint guardrails.
|
|
33
|
-
- **Active development:**
|
|
34
|
-
- **Deferred backlog:** OpenClaw integration, client-agnostic setup,
|
|
33
|
+
- **Active development:** Database Backup and Recovery is defining deterministic Git-reviewable backup artifacts for SQLite-canonical project state.
|
|
34
|
+
- **Deferred backlog:** OpenClaw integration, client-agnostic setup, divisions, and embeddings search.
|
|
35
35
|
- **Ideas and open questions:** tracked separately so future exploration does not distort the active roadmap.
|
|
36
36
|
|
|
37
37
|
## Who it is for
|
package/package.json
CHANGED
package/src/core/helpers.js
CHANGED
|
@@ -125,7 +125,7 @@ export function resolveBatchTargetScenes(dbHandle, {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
const conditions = ["project_id = ?"];
|
|
128
|
+
const conditions = ["s.project_id = ?"];
|
|
129
129
|
const params = [projectId];
|
|
130
130
|
const resolvedChapterFilter = (chapter !== undefined || chapterId !== undefined)
|
|
131
131
|
? resolveValidatedChapterFilter(dbHandle, { projectId, chapterNumber: chapter, chapterId })
|
|
@@ -146,26 +146,27 @@ export function resolveBatchTargetScenes(dbHandle, {
|
|
|
146
146
|
|
|
147
147
|
if (sceneIds?.length) {
|
|
148
148
|
const placeholders = sceneIds.map(() => "?").join(",");
|
|
149
|
-
conditions.push(`scene_id IN (${placeholders})`);
|
|
149
|
+
conditions.push(`s.scene_id IN (${placeholders})`);
|
|
150
150
|
params.push(...sceneIds);
|
|
151
151
|
}
|
|
152
152
|
if (part !== undefined) {
|
|
153
|
-
conditions.push("part = ?");
|
|
153
|
+
conditions.push("s.part = ?");
|
|
154
154
|
params.push(part);
|
|
155
155
|
}
|
|
156
156
|
if (resolvedChapterFilter.chapter) {
|
|
157
|
-
conditions.push("chapter_id = ?");
|
|
157
|
+
conditions.push("s.chapter_id = ?");
|
|
158
158
|
params.push(resolvedChapterFilter.chapter.chapter_id);
|
|
159
159
|
}
|
|
160
160
|
if (onlyStale) {
|
|
161
|
-
conditions.push("metadata_stale = 1");
|
|
161
|
+
conditions.push("s.metadata_stale = 1");
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
const query = `
|
|
165
|
-
SELECT scene_id, project_id, file_path
|
|
166
|
-
FROM scenes
|
|
165
|
+
SELECT s.scene_id, s.project_id, s.file_path
|
|
166
|
+
FROM scenes s
|
|
167
|
+
LEFT JOIN chapters c ON c.project_id = s.project_id AND c.chapter_id = s.chapter_id
|
|
167
168
|
WHERE ${conditions.join(" AND ")}
|
|
168
|
-
ORDER BY part, chapter, timeline_position, scene_id
|
|
169
|
+
ORDER BY s.part, COALESCE(c.sort_index, s.chapter), s.timeline_position, s.scene_id
|
|
169
170
|
`;
|
|
170
171
|
|
|
171
172
|
return {
|
|
@@ -30,6 +30,8 @@ const sceneSchema = z.object({
|
|
|
30
30
|
external_source: z.string().min(1).optional(),
|
|
31
31
|
external_id: z.string().min(1).optional(),
|
|
32
32
|
title: z.string().min(1).optional(),
|
|
33
|
+
chapter_id: z.string().min(1).nullable().optional(),
|
|
34
|
+
scene_role: z.enum(["prologue", "epilogue"]).nullable().optional(),
|
|
33
35
|
part: z.number().int().positive().optional(),
|
|
34
36
|
chapter: z.number().int().positive().optional(),
|
|
35
37
|
chapter_title: z.string().min(1).optional(),
|
package/src/tools/search.js
CHANGED
|
@@ -112,6 +112,7 @@ export function registerSearchTools(s, {
|
|
|
112
112
|
s.save_the_cat_beat, s.timeline_position, s.story_time,
|
|
113
113
|
s.word_count, s.metadata_stale
|
|
114
114
|
FROM scenes s
|
|
115
|
+
LEFT JOIN chapters c ON c.project_id = s.project_id AND c.chapter_id = s.chapter_id
|
|
115
116
|
`;
|
|
116
117
|
const joins = [];
|
|
117
118
|
const conditions = [];
|
|
@@ -139,7 +140,7 @@ export function registerSearchTools(s, {
|
|
|
139
140
|
|
|
140
141
|
if (joins.length) query += " " + joins.join(" ");
|
|
141
142
|
if (conditions.length) query += " WHERE " + conditions.join(" AND ");
|
|
142
|
-
query += " ORDER BY s.part, s.chapter, s.timeline_position, s.scene_id";
|
|
143
|
+
query += " ORDER BY s.part, COALESCE(c.sort_index, s.chapter), s.timeline_position, s.scene_id";
|
|
143
144
|
|
|
144
145
|
const rows = db.prepare(query).all(...params);
|
|
145
146
|
if (rows.length === 0) {
|
|
@@ -424,7 +425,7 @@ export function registerSearchTools(s, {
|
|
|
424
425
|
// ---- get_arc -------------------------------------------------------------
|
|
425
426
|
s.tool(
|
|
426
427
|
"get_arc",
|
|
427
|
-
"Get every scene a character appears in, ordered by part
|
|
428
|
+
"Get every scene a character appears in, ordered by part, canonical chapter order, and scene position. Returns scene metadata only — no prose. Use this as the primary structural entry point when the question is about a character's progression through the manuscript. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Use list_characters only when you need help finding a character_id. Response shape note: always returns a structured envelope (`results`, `total_count`, with pagination fields when paging is active).",
|
|
428
429
|
{
|
|
429
430
|
character_id: z.string().describe("The character_id to trace (e.g. 'char-mira-nystrom'). Use list_characters to find valid IDs."),
|
|
430
431
|
project_id: z.string().optional().describe("Limit to a specific project (e.g. 'the-lamb')."),
|
|
@@ -438,11 +439,12 @@ export function registerSearchTools(s, {
|
|
|
438
439
|
s.save_the_cat_beat, s.timeline_position, s.story_time, s.pov, s.metadata_stale
|
|
439
440
|
FROM scenes s
|
|
440
441
|
JOIN scene_characters sc ON sc.scene_id = s.scene_id AND sc.project_id = s.project_id
|
|
442
|
+
LEFT JOIN chapters c ON c.project_id = s.project_id AND c.chapter_id = s.chapter_id
|
|
441
443
|
WHERE sc.character_id = ?
|
|
442
444
|
`;
|
|
443
445
|
const params = [character_id];
|
|
444
446
|
if (project_id) { query += ` AND s.project_id = ?`; params.push(project_id); }
|
|
445
|
-
query += ` ORDER BY s.part, s.chapter, s.timeline_position`;
|
|
447
|
+
query += ` ORDER BY s.part, COALESCE(c.sort_index, s.chapter), s.timeline_position, s.scene_id`;
|
|
446
448
|
|
|
447
449
|
const rows = db.prepare(query).all(...params);
|
|
448
450
|
if (rows.length === 0) {
|
|
@@ -1000,7 +1002,8 @@ export function registerSearchTools(s, {
|
|
|
1000
1002
|
st.beat AS thread_beat, s.timeline_position, s.story_time, s.metadata_stale
|
|
1001
1003
|
FROM scenes s
|
|
1002
1004
|
JOIN scene_threads st ON st.scene_id = s.scene_id AND st.project_id = s.project_id AND st.thread_id = ?
|
|
1003
|
-
|
|
1005
|
+
LEFT JOIN chapters c ON c.project_id = s.project_id AND c.chapter_id = s.chapter_id
|
|
1006
|
+
ORDER BY s.part, COALESCE(c.sort_index, s.chapter), s.timeline_position, s.scene_id
|
|
1004
1007
|
`).all(thread_id);
|
|
1005
1008
|
const staleCount = rows.filter(r => r.metadata_stale).length;
|
|
1006
1009
|
const warning = staleCount > 0 ? `${staleCount} scene(s) have stale metadata.` : undefined;
|
|
@@ -1047,11 +1050,12 @@ export function registerSearchTools(s, {
|
|
|
1047
1050
|
FROM character_relationships r
|
|
1048
1051
|
LEFT JOIN scenes s ON s.scene_id = r.scene_id
|
|
1049
1052
|
AND (r.scene_id IS NULL OR s.project_id = COALESCE(?, s.project_id))
|
|
1053
|
+
LEFT JOIN chapters c ON c.project_id = s.project_id AND c.chapter_id = s.chapter_id
|
|
1050
1054
|
WHERE r.from_character = ? AND r.to_character = ?
|
|
1051
1055
|
`;
|
|
1052
1056
|
const params = [project_id ?? null, from_character, to_character];
|
|
1053
1057
|
if (project_id) { query += ` AND (s.project_id = ? OR r.scene_id IS NULL)`; params.push(project_id); }
|
|
1054
|
-
query += ` ORDER BY s.part, s.chapter, s.timeline_position, s.scene_id`;
|
|
1058
|
+
query += ` ORDER BY s.part, COALESCE(c.sort_index, s.chapter), s.timeline_position, s.scene_id`;
|
|
1055
1059
|
|
|
1056
1060
|
const rows = db.prepare(query).all(...params);
|
|
1057
1061
|
if (rows.length === 0) {
|