@hanna84/mcp-writing 2.18.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +53 -0
- package/package.json +1 -1
- package/src/core/db.js +214 -11
- package/src/index.js +15 -2
- package/src/review-bundles/review-bundles-planner.js +8 -7
- package/src/review-bundles/review-bundles-renderer.js +1 -1
- package/src/scripts/manual/README.md +27 -0
- package/src/scripts/manual/mcp-result.mjs +27 -0
- package/src/scripts/manual/run_create_review_bundle.js +14 -13
- package/src/scripts/manual/run_mcp_and_review.js +3 -5
- package/src/scripts/manual/run_mcp_test.js +15 -15
- package/src/scripts/manual/test-scenarios.mjs +26 -14
- package/src/scripts/manual/test.mjs +11 -4
- package/src/scripts/manual-validation.mjs +11 -3
- package/src/scripts/mcp-debug-client.mjs +4 -3
- package/src/sync/sync.js +16 -19
- package/src/tools/editing.js +77 -10
- package/src/tools/metadata.js +6 -6
- package/src/tools/review-bundles.js +12 -2
- package/src/tools/search.js +140 -38
- package/src/tools/sync.js +3 -0
- package/src/workflows/workflow-catalogue.js +82 -52
package/src/tools/search.js
CHANGED
|
@@ -70,7 +70,7 @@ export function registerSearchTools(s, {
|
|
|
70
70
|
// ---- find_scenes ---------------------------------------------------------
|
|
71
71
|
s.tool(
|
|
72
72
|
"find_scenes",
|
|
73
|
-
"Find scenes by filtering on character, Save the Cat beat, tags, part, chapter, or POV. Returns ordered scene metadata only — no prose. All filters are optional and combinable. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Warns if any matching scenes have stale metadata.",
|
|
73
|
+
"Find scenes by filtering on character, Save the Cat beat, tags, part, chapter, or POV. Returns ordered scene metadata only — no prose. All filters are optional and combinable. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Warns if any matching scenes have stale metadata. Response shape note: always returns a structured envelope (`results`, `total_count`, with pagination fields when paging is active).",
|
|
74
74
|
{
|
|
75
75
|
project_id: z.string().optional().describe("Project ID (e.g. 'the-lamb'). Use to scope results to one project."),
|
|
76
76
|
character: z.string().optional().describe("A character_id (e.g. 'char-mira-nystrom'). Returns only scenes that character appears in. Use list_characters first to find valid IDs."),
|
|
@@ -95,11 +95,11 @@ export function registerSearchTools(s, {
|
|
|
95
95
|
const params = [];
|
|
96
96
|
|
|
97
97
|
if (character) {
|
|
98
|
-
joins.push(`JOIN scene_characters sc ON sc.scene_id = s.scene_id AND sc.character_id = ?`);
|
|
98
|
+
joins.push(`JOIN scene_characters sc ON sc.scene_id = s.scene_id AND sc.project_id = s.project_id AND sc.character_id = ?`);
|
|
99
99
|
params.push(character);
|
|
100
100
|
}
|
|
101
101
|
if (tag) {
|
|
102
|
-
joins.push(`JOIN scene_tags st ON st.scene_id = s.scene_id AND st.tag = ?`);
|
|
102
|
+
joins.push(`JOIN scene_tags st ON st.scene_id = s.scene_id AND st.project_id = s.project_id AND st.tag = ?`);
|
|
103
103
|
params.push(tag);
|
|
104
104
|
}
|
|
105
105
|
if (project_id) { conditions.push(`s.project_id = ?`); params.push(project_id); }
|
|
@@ -133,8 +133,18 @@ export function registerSearchTools(s, {
|
|
|
133
133
|
results: paged.rows,
|
|
134
134
|
...paged.meta,
|
|
135
135
|
warning,
|
|
136
|
+
next_step: staleCount > 0
|
|
137
|
+
? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
|
|
138
|
+
: undefined,
|
|
136
139
|
}
|
|
137
|
-
:
|
|
140
|
+
: {
|
|
141
|
+
results: rows,
|
|
142
|
+
total_count: rows.length,
|
|
143
|
+
warning,
|
|
144
|
+
next_step: staleCount > 0
|
|
145
|
+
? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
|
|
146
|
+
: undefined,
|
|
147
|
+
};
|
|
138
148
|
|
|
139
149
|
return {
|
|
140
150
|
content: [{
|
|
@@ -148,15 +158,36 @@ export function registerSearchTools(s, {
|
|
|
148
158
|
// ---- get_scene_prose -----------------------------------------------------
|
|
149
159
|
s.tool(
|
|
150
160
|
"get_scene_prose",
|
|
151
|
-
"Load the full prose text of a single scene. Use this for close reading, continuity checks, or when you need the actual writing. For overview or filtering, use find_scenes instead — it is much cheaper. Optionally retrieve a past version from git history.",
|
|
161
|
+
"Load the full prose text of a single scene. Use this for close reading, continuity checks, or when you need the actual writing. For overview or filtering, use find_scenes instead — it is much cheaper. Optionally retrieve a past version from git history. If scene IDs are reused across projects, omitting project_id returns CONFLICT with candidate project_ids.",
|
|
152
162
|
{
|
|
153
163
|
scene_id: z.string().describe("The scene_id to retrieve (e.g. 'sc-001-prologue'). Get this from find_scenes or get_arc."),
|
|
164
|
+
project_id: z.string().optional().describe("Optional project ID to disambiguate duplicate scene IDs across projects."),
|
|
154
165
|
commit: z.string().optional().describe("Optional git commit hash to retrieve a past version. Use list_snapshots to find valid hashes. If omitted, returns the current prose."),
|
|
155
166
|
},
|
|
156
|
-
async ({ scene_id, commit }) => {
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
167
|
+
async ({ scene_id, project_id, commit }) => {
|
|
168
|
+
let scene;
|
|
169
|
+
if (project_id) {
|
|
170
|
+
scene = db.prepare(`SELECT file_path, metadata_stale, project_id FROM scenes WHERE scene_id = ? AND project_id = ?`).get(scene_id, project_id);
|
|
171
|
+
if (!scene) {
|
|
172
|
+
return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'. Run sync() if you just added it.`, {
|
|
173
|
+
next_step: "Run sync() to refresh the index, then call find_scenes with project_id to locate the scene by current metadata.",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
const scenes = db.prepare(`SELECT file_path, metadata_stale, project_id FROM scenes WHERE scene_id = ? ORDER BY project_id`).all(scene_id);
|
|
178
|
+
if (scenes.length === 0) {
|
|
179
|
+
return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found. Run sync() if you just added it.`, {
|
|
180
|
+
next_step: "Run sync() to refresh the index, then call find_scenes to locate the scene by current metadata.",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (scenes.length > 1) {
|
|
184
|
+
return errorResponse(
|
|
185
|
+
"CONFLICT",
|
|
186
|
+
`Scene ID '${scene_id}' exists in multiple projects. Provide project_id to disambiguate.`,
|
|
187
|
+
{ scene_id, project_ids: scenes.map(s => s.project_id) }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
scene = scenes[0];
|
|
160
191
|
}
|
|
161
192
|
try {
|
|
162
193
|
let rawContent;
|
|
@@ -170,10 +201,18 @@ export function registerSearchTools(s, {
|
|
|
170
201
|
|
|
171
202
|
const { content: prose } = matter(rawContent);
|
|
172
203
|
const versionNote = commit ? `\n\n(Retrieved from commit: ${commit})` : "";
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: prose.trim() + versionNote,
|
|
208
|
+
}],
|
|
209
|
+
structuredContent: scene.metadata_stale && !commit
|
|
210
|
+
? {
|
|
211
|
+
warning: "Metadata for this scene may be stale — prose has changed since last enrichment.",
|
|
212
|
+
next_step: `Run enrich_scene with scene_id ${scene_id} and project_id ${scene.project_id} after this read to recover metadata parity for this scene.`,
|
|
213
|
+
}
|
|
214
|
+
: undefined,
|
|
215
|
+
};
|
|
177
216
|
} catch (err) {
|
|
178
217
|
if (err.code === "ENOENT") {
|
|
179
218
|
return errorResponse(
|
|
@@ -221,17 +260,24 @@ export function registerSearchTools(s, {
|
|
|
221
260
|
}
|
|
222
261
|
}
|
|
223
262
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
:
|
|
227
|
-
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text: parts.join("\n\n---\n\n") }],
|
|
265
|
+
structuredContent: {
|
|
266
|
+
...(truncated
|
|
267
|
+
? { warning: `Chapter has ${allScenes.length} scenes — only the first ${MAX_CHAPTER_SCENES} were loaded. Set MAX_CHAPTER_SCENES to increase this limit.` }
|
|
268
|
+
: {}),
|
|
269
|
+
next_step: truncated
|
|
270
|
+
? "Narrow with find_scenes and inspect key scenes individually with get_scene_prose before expanding chapter scope."
|
|
271
|
+
: "If you only need a subset, switch to find_scenes + get_scene_prose for tighter context control.",
|
|
272
|
+
},
|
|
273
|
+
};
|
|
228
274
|
}
|
|
229
275
|
);
|
|
230
276
|
|
|
231
277
|
// ---- get_arc -------------------------------------------------------------
|
|
232
278
|
s.tool(
|
|
233
279
|
"get_arc",
|
|
234
|
-
"Get every scene a character appears in, ordered by part/chapter/position. Returns scene metadata only — no prose. Use this
|
|
280
|
+
"Get every scene a character appears in, ordered by part/chapter/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).",
|
|
235
281
|
{
|
|
236
282
|
character_id: z.string().describe("The character_id to trace (e.g. 'char-mira-nystrom'). Use list_characters to find valid IDs."),
|
|
237
283
|
project_id: z.string().optional().describe("Limit to a specific project (e.g. 'the-lamb')."),
|
|
@@ -244,7 +290,7 @@ export function registerSearchTools(s, {
|
|
|
244
290
|
s.scene_change, s.causality, s.stakes, s.scene_functions,
|
|
245
291
|
s.save_the_cat_beat, s.timeline_position, s.story_time, s.pov, s.metadata_stale
|
|
246
292
|
FROM scenes s
|
|
247
|
-
JOIN scene_characters sc ON sc.scene_id = s.scene_id
|
|
293
|
+
JOIN scene_characters sc ON sc.scene_id = s.scene_id AND sc.project_id = s.project_id
|
|
248
294
|
WHERE sc.character_id = ?
|
|
249
295
|
`;
|
|
250
296
|
const params = [character_id];
|
|
@@ -272,8 +318,18 @@ export function registerSearchTools(s, {
|
|
|
272
318
|
results: paged.rows,
|
|
273
319
|
...paged.meta,
|
|
274
320
|
warning,
|
|
321
|
+
next_step: staleCount > 0
|
|
322
|
+
? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
|
|
323
|
+
: undefined,
|
|
275
324
|
}
|
|
276
|
-
:
|
|
325
|
+
: {
|
|
326
|
+
results: rows,
|
|
327
|
+
total_count: rows.length,
|
|
328
|
+
warning,
|
|
329
|
+
next_step: staleCount > 0
|
|
330
|
+
? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
|
|
331
|
+
: undefined,
|
|
332
|
+
};
|
|
277
333
|
|
|
278
334
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
279
335
|
}
|
|
@@ -282,7 +338,7 @@ export function registerSearchTools(s, {
|
|
|
282
338
|
// ---- list_characters -----------------------------------------------------
|
|
283
339
|
s.tool(
|
|
284
340
|
"list_characters",
|
|
285
|
-
"List
|
|
341
|
+
"List indexed characters with their character_id, name, role, and arc_summary. Use this mainly as a lookup and disambiguation helper when you need to find a character_id for a broader reasoning task. Response shape note: returns a structured envelope (`results`, `total_count`).",
|
|
286
342
|
{
|
|
287
343
|
project_id: z.string().optional().describe("Limit to a specific project (e.g. 'the-lamb')."),
|
|
288
344
|
universe_id: z.string().optional().describe("Limit to a specific universe (if using cross-project world-building)."),
|
|
@@ -300,21 +356,31 @@ export function registerSearchTools(s, {
|
|
|
300
356
|
if (rows.length === 0) {
|
|
301
357
|
return errorResponse("NO_RESULTS", "No characters found.");
|
|
302
358
|
}
|
|
303
|
-
return {
|
|
359
|
+
return {
|
|
360
|
+
content: [{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: JSON.stringify({
|
|
363
|
+
results: rows,
|
|
364
|
+
total_count: rows.length,
|
|
365
|
+
}, null, 2),
|
|
366
|
+
}],
|
|
367
|
+
};
|
|
304
368
|
}
|
|
305
369
|
);
|
|
306
370
|
|
|
307
371
|
// ---- get_character_sheet -------------------------------------------------
|
|
308
372
|
s.tool(
|
|
309
373
|
"get_character_sheet",
|
|
310
|
-
"Get full character details: role, arc_summary, traits, the canonical sheet content, and any adjacent support notes when the character uses a folder-based layout. Use
|
|
374
|
+
"Get full character details: role, arc_summary, traits, the canonical sheet content, and any adjacent support notes when the character uses a folder-based layout. Use this when the reasoning task needs the character's canonical profile rather than only their scene progression.",
|
|
311
375
|
{
|
|
312
376
|
character_id: z.string().describe("The character_id to look up (e.g. 'char-sebastian'). Use list_characters to find valid IDs."),
|
|
313
377
|
},
|
|
314
378
|
async ({ character_id }) => {
|
|
315
379
|
const character = db.prepare(`SELECT * FROM characters WHERE character_id = ?`).get(character_id);
|
|
316
380
|
if (!character) {
|
|
317
|
-
return errorResponse("NOT_FOUND", `Character '${character_id}' not found
|
|
381
|
+
return errorResponse("NOT_FOUND", `Character '${character_id}' not found.`, {
|
|
382
|
+
next_step: "Call list_characters to find valid character_id values, then retry get_character_sheet or get_arc.",
|
|
383
|
+
});
|
|
318
384
|
}
|
|
319
385
|
|
|
320
386
|
const traits = db.prepare(`SELECT trait FROM character_traits WHERE character_id = ?`)
|
|
@@ -336,6 +402,7 @@ export function registerSearchTools(s, {
|
|
|
336
402
|
traits,
|
|
337
403
|
notes: notes || undefined,
|
|
338
404
|
supporting_notes: supportingNotes.length ? supportingNotes : undefined,
|
|
405
|
+
next_step: "Use get_arc with this character_id to trace scene-level progression, then open specific scenes with get_scene_prose when prose evidence is needed.",
|
|
339
406
|
};
|
|
340
407
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
341
408
|
}
|
|
@@ -344,7 +411,7 @@ export function registerSearchTools(s, {
|
|
|
344
411
|
// ---- list_places ---------------------------------------------------------
|
|
345
412
|
s.tool(
|
|
346
413
|
"list_places",
|
|
347
|
-
"List
|
|
414
|
+
"List indexed places with their place_id and name. Use this mainly as a lookup and disambiguation helper when place context becomes relevant to the current reasoning task. Response shape note: returns a structured envelope (`results`, `total_count`).",
|
|
348
415
|
{
|
|
349
416
|
project_id: z.string().optional().describe("Limit to a specific project (e.g. 'the-lamb')."),
|
|
350
417
|
universe_id: z.string().optional().describe("Limit to a specific universe."),
|
|
@@ -362,21 +429,31 @@ export function registerSearchTools(s, {
|
|
|
362
429
|
if (rows.length === 0) {
|
|
363
430
|
return errorResponse("NO_RESULTS", "No places found.");
|
|
364
431
|
}
|
|
365
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
content: [{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: JSON.stringify({
|
|
436
|
+
results: rows,
|
|
437
|
+
total_count: rows.length,
|
|
438
|
+
}, null, 2),
|
|
439
|
+
}],
|
|
440
|
+
};
|
|
366
441
|
}
|
|
367
442
|
);
|
|
368
443
|
|
|
369
444
|
// ---- get_place_sheet -----------------------------------------------------
|
|
370
445
|
s.tool(
|
|
371
446
|
"get_place_sheet",
|
|
372
|
-
"Get full place details: associated_characters, tags, the canonical sheet content, and any adjacent support notes when the place uses a folder-based layout. Use
|
|
447
|
+
"Get full place details: associated_characters, tags, the canonical sheet content, and any adjacent support notes when the place uses a folder-based layout. Use this when the current scene or question makes the place itself materially relevant.",
|
|
373
448
|
{
|
|
374
449
|
place_id: z.string().describe("The place_id to look up (e.g. 'place-harbor-district'). Use list_places to find valid IDs."),
|
|
375
450
|
},
|
|
376
451
|
async ({ place_id }) => {
|
|
377
452
|
const place = db.prepare(`SELECT * FROM places WHERE place_id = ?`).get(place_id);
|
|
378
453
|
if (!place) {
|
|
379
|
-
return errorResponse("NOT_FOUND", `Place '${place_id}' not found
|
|
454
|
+
return errorResponse("NOT_FOUND", `Place '${place_id}' not found.`, {
|
|
455
|
+
next_step: "Call list_places to find valid place_id values, then retry get_place_sheet.",
|
|
456
|
+
});
|
|
380
457
|
}
|
|
381
458
|
|
|
382
459
|
let notes = "";
|
|
@@ -403,6 +480,7 @@ export function registerSearchTools(s, {
|
|
|
403
480
|
tags: tags.length ? tags : undefined,
|
|
404
481
|
notes: notes || undefined,
|
|
405
482
|
supporting_notes: supportingNotes.length ? supportingNotes : undefined,
|
|
483
|
+
next_step: "Use find_scenes with related filters to locate scenes where this place matters, then open targeted scenes with get_scene_prose when prose context is needed.",
|
|
406
484
|
};
|
|
407
485
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
408
486
|
}
|
|
@@ -445,7 +523,15 @@ export function registerSearchTools(s, {
|
|
|
445
523
|
ORDER BY rank
|
|
446
524
|
`).all(query);
|
|
447
525
|
|
|
448
|
-
return {
|
|
526
|
+
return {
|
|
527
|
+
content: [{
|
|
528
|
+
type: "text",
|
|
529
|
+
text: JSON.stringify({
|
|
530
|
+
results: rows,
|
|
531
|
+
total_count: rows.length,
|
|
532
|
+
}, null, 2),
|
|
533
|
+
}],
|
|
534
|
+
};
|
|
449
535
|
}
|
|
450
536
|
|
|
451
537
|
const safePageSize = Math.max(1, page_size ?? DEFAULT_METADATA_PAGE_SIZE);
|
|
@@ -480,7 +566,7 @@ export function registerSearchTools(s, {
|
|
|
480
566
|
// ---- search_reference ----------------------------------------------------
|
|
481
567
|
s.tool(
|
|
482
568
|
"search_reference",
|
|
483
|
-
"Full-text search across indexed reference document titles, summaries, and tags. Use this to discover world-building notes, continuity references, research docs, and other reference material without loading full file contents.",
|
|
569
|
+
"Full-text search across indexed reference document titles, summaries, and tags. Use this to discover world-building notes, continuity references, research docs, and other reference material without loading full file contents. Response shape note: returns a structured envelope (`results`, `total_count`).",
|
|
484
570
|
{
|
|
485
571
|
query: z.string().describe("Search terms (e.g. 'vampirism' or 'blood replacement'). FTS5 syntax supported."),
|
|
486
572
|
type: z.string().optional().describe("Optional reference type filter (for example: 'world', 'continuity', 'research', 'style')."),
|
|
@@ -534,7 +620,15 @@ export function registerSearchTools(s, {
|
|
|
534
620
|
return errorResponse("NO_RESULTS", "No reference documents matched the provided filters.");
|
|
535
621
|
}
|
|
536
622
|
|
|
537
|
-
return {
|
|
623
|
+
return {
|
|
624
|
+
content: [{
|
|
625
|
+
type: "text",
|
|
626
|
+
text: JSON.stringify({
|
|
627
|
+
results: rows,
|
|
628
|
+
total_count: rows.length,
|
|
629
|
+
}, null, 2),
|
|
630
|
+
}],
|
|
631
|
+
};
|
|
538
632
|
}
|
|
539
633
|
);
|
|
540
634
|
|
|
@@ -695,7 +789,7 @@ export function registerSearchTools(s, {
|
|
|
695
789
|
// ---- list_threads --------------------------------------------------------
|
|
696
790
|
s.tool(
|
|
697
791
|
"list_threads",
|
|
698
|
-
"List
|
|
792
|
+
"List subplot/storyline threads for a project. Returns a structured JSON envelope with results and total_count. Use this mainly as a lookup and disambiguation helper before deeper thread reasoning with get_thread_arc. Supports pagination via page/page_size.",
|
|
699
793
|
{
|
|
700
794
|
project_id: z.string().describe("Project ID."),
|
|
701
795
|
page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
|
|
@@ -709,11 +803,13 @@ export function registerSearchTools(s, {
|
|
|
709
803
|
project_id,
|
|
710
804
|
results: paged.rows,
|
|
711
805
|
...paged.meta,
|
|
806
|
+
next_step: "Use a thread_id from results with get_thread_arc to inspect storyline progression across scenes.",
|
|
712
807
|
}
|
|
713
808
|
: {
|
|
714
809
|
project_id,
|
|
715
810
|
results: rows,
|
|
716
811
|
total_count: rows.length,
|
|
812
|
+
next_step: "Use a thread_id from results with get_thread_arc to inspect storyline progression across scenes.",
|
|
717
813
|
};
|
|
718
814
|
|
|
719
815
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
@@ -723,7 +819,7 @@ export function registerSearchTools(s, {
|
|
|
723
819
|
// ---- get_thread_arc ------------------------------------------------------
|
|
724
820
|
s.tool(
|
|
725
821
|
"get_thread_arc",
|
|
726
|
-
"Get ordered scene metadata for all scenes belonging to a thread, including the per-thread beat. Returns a structured JSON envelope with thread metadata, results, and total_count. Use
|
|
822
|
+
"Get ordered scene metadata for all scenes belonging to a thread, including the per-thread beat. Returns a structured JSON envelope with thread metadata, results, and total_count. Use this when the question is about subplot movement, continuity, or recurring storyline structure across scenes. Supports pagination via page/page_size.",
|
|
727
823
|
{
|
|
728
824
|
thread_id: z.string().describe("Thread ID."),
|
|
729
825
|
page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
|
|
@@ -739,7 +835,7 @@ export function registerSearchTools(s, {
|
|
|
739
835
|
SELECT s.scene_id, s.project_id, s.part, s.chapter, s.chapter_title, s.title, s.logline,
|
|
740
836
|
st.beat AS thread_beat, s.timeline_position, s.story_time, s.metadata_stale
|
|
741
837
|
FROM scenes s
|
|
742
|
-
JOIN scene_threads st ON st.scene_id = s.scene_id AND st.thread_id = ?
|
|
838
|
+
JOIN scene_threads st ON st.scene_id = s.scene_id AND st.project_id = s.project_id AND st.thread_id = ?
|
|
743
839
|
ORDER BY s.part, s.chapter, s.timeline_position
|
|
744
840
|
`).all(thread_id);
|
|
745
841
|
const staleCount = rows.filter(r => r.metadata_stale).length;
|
|
@@ -752,12 +848,18 @@ export function registerSearchTools(s, {
|
|
|
752
848
|
results: paged.rows,
|
|
753
849
|
...paged.meta,
|
|
754
850
|
warning,
|
|
851
|
+
next_step: staleCount > 0
|
|
852
|
+
? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
|
|
853
|
+
: undefined,
|
|
755
854
|
}
|
|
756
855
|
: {
|
|
757
856
|
thread,
|
|
758
857
|
results: rows,
|
|
759
858
|
total_count: rows.length,
|
|
760
859
|
warning,
|
|
860
|
+
next_step: staleCount > 0
|
|
861
|
+
? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
|
|
862
|
+
: undefined,
|
|
761
863
|
};
|
|
762
864
|
|
|
763
865
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
@@ -855,12 +957,12 @@ export function registerSearchTools(s, {
|
|
|
855
957
|
if (!loadedSceneEntitiesFromMetadata) {
|
|
856
958
|
// Fallback for scenes without readable indexed file paths.
|
|
857
959
|
characterIds = db.prepare(`
|
|
858
|
-
SELECT character_id FROM scene_characters WHERE scene_id = ?
|
|
859
|
-
`).all(scene_id).map((row) => row.character_id);
|
|
960
|
+
SELECT character_id FROM scene_characters WHERE scene_id = ? AND project_id = ?
|
|
961
|
+
`).all(scene_id, resolvedProjectId).map((row) => row.character_id);
|
|
860
962
|
|
|
861
963
|
placeIds = db.prepare(`
|
|
862
|
-
SELECT place_id FROM scene_places WHERE scene_id = ?
|
|
863
|
-
`).all(scene_id).map((row) => row.place_id);
|
|
964
|
+
SELECT place_id FROM scene_places WHERE scene_id = ? AND project_id = ?
|
|
965
|
+
`).all(scene_id, resolvedProjectId).map((row) => row.place_id);
|
|
864
966
|
}
|
|
865
967
|
|
|
866
968
|
// Get explicit scene → reference links already present
|
package/src/tools/sync.js
CHANGED
|
@@ -28,6 +28,9 @@ export function registerSyncTools(s, {
|
|
|
28
28
|
s.tool("sync", "Re-scan the sync folder and update the scene/character/place index from disk. Call this after making edits in Scrivener or updating sidecar files outside the MCP.", {}, async () => {
|
|
29
29
|
const result = syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
|
|
30
30
|
const parts = [`Sync complete. ${result.indexed} scenes indexed. ${result.staleMarked} scenes marked stale.`];
|
|
31
|
+
if (result.staleMarked > 0) {
|
|
32
|
+
parts.push(`Next step: when you touch one of those scenes, run enrich_scene(scene_id, project_id) to recover metadata parity incrementally.`);
|
|
33
|
+
}
|
|
31
34
|
if (result.sidecarsMigrated) parts.push(`${result.sidecarsMigrated} sidecar(s) auto-generated from frontmatter.`);
|
|
32
35
|
if (result.skipped) {
|
|
33
36
|
parts.push(`${result.skipped} file(s) skipped (no scene_id).`);
|
|
@@ -1,86 +1,116 @@
|
|
|
1
1
|
export const WORKFLOW_CATALOGUE = [
|
|
2
2
|
{
|
|
3
|
-
id: "
|
|
4
|
-
label: "
|
|
5
|
-
use_when: "
|
|
3
|
+
id: "question_driven_discovery",
|
|
4
|
+
label: "Find scenes for a manuscript question",
|
|
5
|
+
use_when: "Start here for most sessions: when the user has a manuscript question, you need to narrow scope, or you are not yet sure which scene matters.",
|
|
6
6
|
steps: [
|
|
7
|
-
{ tool: "
|
|
8
|
-
{ tool: "
|
|
7
|
+
{ tool: "find_scenes", note: "Use structured metadata filters first when the question already suggests characters, beats, tags, parts, chapters, or POV." },
|
|
8
|
+
{ tool: "search_metadata", note: "Use this when the question is thematic, fuzzy, or keyword-driven rather than cleanly filterable." },
|
|
9
|
+
{ tool: "get_scene_prose", note: "Escalate to prose only after likely scenes have been identified and metadata is no longer enough." },
|
|
10
|
+
{ tool: "flag_scene", note: "Use only when the current task naturally leads to recording a follow-up note for later editorial attention." },
|
|
9
11
|
],
|
|
10
12
|
},
|
|
11
13
|
{
|
|
12
|
-
id: "
|
|
13
|
-
label: "
|
|
14
|
-
use_when: "
|
|
14
|
+
id: "targeted_scene_reading",
|
|
15
|
+
label: "Inspect prose for a likely scene",
|
|
16
|
+
use_when: "Use this after metadata has narrowed the space and the user needs details, nuance, continuity, tone, pacing, or other evidence only prose can confirm.",
|
|
15
17
|
steps: [
|
|
16
|
-
{ tool: "
|
|
17
|
-
{ tool: "
|
|
18
|
-
{ tool: "
|
|
19
|
-
{ tool: "
|
|
18
|
+
{ tool: "find_scenes", note: "Use metadata discovery first if the target scene is not already known." },
|
|
19
|
+
{ tool: "get_scene_prose", note: "Load the specific scene that matters once you have a likely target." },
|
|
20
|
+
{ tool: "get_chapter_prose", note: "Escalate only when the question cannot be answered scene-by-scene and chapter-wide prose context is truly required." },
|
|
21
|
+
{ tool: "list_scene_references", note: "Use when linked references are relevant to understanding the scene in context." },
|
|
20
22
|
],
|
|
21
23
|
},
|
|
22
24
|
{
|
|
23
|
-
id: "
|
|
24
|
-
label: "
|
|
25
|
-
use_when: "
|
|
25
|
+
id: "safe_scene_revision",
|
|
26
|
+
label: "Revise a scene safely",
|
|
27
|
+
use_when: "Use when the next meaningful step is changing prose rather than continuing discovery or inspection.",
|
|
26
28
|
steps: [
|
|
27
|
-
{ tool: "
|
|
28
|
-
{ tool: "
|
|
29
|
-
{ tool: "
|
|
29
|
+
{ tool: "find_scenes", note: "Identify the target scene if the user has not already narrowed to a specific scene_id." },
|
|
30
|
+
{ tool: "get_scene_prose", note: "Read the current prose before proposing a revision." },
|
|
31
|
+
{ tool: "propose_edit", note: "Stage a revision and review the diff preview with the user before writing anything." },
|
|
32
|
+
{ tool: "commit_edit", note: "Apply the revision only after explicit user approval." },
|
|
33
|
+
{ tool: "discard_edit", note: "Use when the proposed change should not be applied." },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "character_understanding",
|
|
38
|
+
label: "Understand a character in context",
|
|
39
|
+
use_when: "Use when the user wants to understand a character's path through the manuscript or needs the character's canonical profile in support of a story question.",
|
|
40
|
+
steps: [
|
|
41
|
+
{ tool: "get_arc", note: "Use as the primary structural entry point when the question is about the character's progression across scenes." },
|
|
42
|
+
{ tool: "get_character_sheet", note: "Use when you need the canonical character profile, traits, notes, or supporting material." },
|
|
43
|
+
{ tool: "list_characters", note: "Use only as a helper to find or disambiguate character_id values." },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "place_understanding",
|
|
48
|
+
label: "Understand a place in context",
|
|
49
|
+
use_when: "Use when the current scene involves a place that matters, or when the user is asking directly about a location's role in the manuscript.",
|
|
50
|
+
steps: [
|
|
51
|
+
{ tool: "get_place_sheet", note: "Use when the place itself is part of the reasoning task and you need its canonical profile or notes." },
|
|
52
|
+
{ tool: "find_scenes", note: "Use to locate scenes where the place is likely to matter through tags, chapter context, or related story structure." },
|
|
53
|
+
{ tool: "list_places", note: "Use only as a helper to find or disambiguate place_id values." },
|
|
30
54
|
],
|
|
31
55
|
},
|
|
32
56
|
{
|
|
33
|
-
id: "
|
|
34
|
-
label: "
|
|
35
|
-
use_when: "
|
|
57
|
+
id: "thread_understanding",
|
|
58
|
+
label: "Understand a thread or arc in context",
|
|
59
|
+
use_when: "Use when the question is about progression, continuity, subplot movement, or recurring storyline structure across scenes.",
|
|
36
60
|
steps: [
|
|
37
|
-
{ tool: "
|
|
38
|
-
{ tool: "
|
|
39
|
-
{ tool: "
|
|
40
|
-
{ tool: "search_metadata", note: "Full-text search across scene metadata fields." },
|
|
61
|
+
{ tool: "get_thread_arc", note: "Use when the storyline or subplot is already identified and you need its ordered scene progression." },
|
|
62
|
+
{ tool: "list_threads", note: "Use only as a helper to find or disambiguate thread_id values." },
|
|
63
|
+
{ tool: "get_arc", note: "Use when the thread question is really a character-progression question and character context is the better structural entry point." },
|
|
41
64
|
],
|
|
42
65
|
},
|
|
43
66
|
{
|
|
44
|
-
id: "
|
|
45
|
-
label: "
|
|
46
|
-
use_when: "
|
|
67
|
+
id: "parity_recovery",
|
|
68
|
+
label: "Recover metadata parity",
|
|
69
|
+
use_when: "Use when new material has been added, sync/import reveals metadata gaps, or normal work touches scenes or documents with weak, stale, or missing metadata support.",
|
|
47
70
|
steps: [
|
|
48
|
-
{ tool: "
|
|
49
|
-
{ tool: "
|
|
50
|
-
{ tool: "
|
|
51
|
-
{ tool: "
|
|
52
|
-
{ tool: "discard_edit", note: "Reject the revision if the user does not approve." },
|
|
71
|
+
{ tool: "sync", note: "Refresh the index and use the result as the main signal that material has changed or parity may need attention." },
|
|
72
|
+
{ tool: "enrich_scene", note: "Use for lightweight opportunistic recovery when the current task is already touching a specific low-parity scene." },
|
|
73
|
+
{ tool: "enrich_scene_characters_batch", note: "Use when recovery scope is broad enough to justify focused catch-up work; prefer dry_run first." },
|
|
74
|
+
{ tool: "suggest_scene_references", note: "Use when low parity is specifically about missing scene-to-reference relationships." },
|
|
53
75
|
],
|
|
54
76
|
},
|
|
55
77
|
{
|
|
56
|
-
id: "
|
|
57
|
-
label: "
|
|
58
|
-
use_when: "
|
|
78
|
+
id: "review_preparation",
|
|
79
|
+
label: "Prepare material for human review",
|
|
80
|
+
use_when: "Use when the task has shifted from reasoning or revising into packaging material for editors, collaborators, or beta readers.",
|
|
59
81
|
steps: [
|
|
60
|
-
{ tool: "
|
|
61
|
-
{ tool: "
|
|
62
|
-
{ tool: "create_character_sheet", note: "Create a new character. Requires exactly one of project_id or universe_id." },
|
|
63
|
-
{ tool: "update_character_sheet", note: "Edit character metadata." },
|
|
82
|
+
{ tool: "preview_review_bundle", note: "Check scope, warnings, and planned outputs before generating anything." },
|
|
83
|
+
{ tool: "create_review_bundle", note: "Generate the review artifact once scope and warnings have been reviewed." },
|
|
64
84
|
],
|
|
65
85
|
},
|
|
66
86
|
{
|
|
67
|
-
id: "
|
|
68
|
-
label: "
|
|
69
|
-
use_when: "
|
|
87
|
+
id: "first_time_setup",
|
|
88
|
+
label: "Connect and verify a project",
|
|
89
|
+
use_when: "Use when connecting to a project for the first time or when runtime configuration needs to be verified before normal workflows begin.",
|
|
70
90
|
steps: [
|
|
71
|
-
{ tool: "
|
|
72
|
-
{ tool: "
|
|
73
|
-
{ tool: "create_place_sheet", note: "Create a new place. Requires exactly one of project_id or universe_id." },
|
|
74
|
-
{ tool: "update_place_sheet", note: "Edit place metadata." },
|
|
91
|
+
{ tool: "get_runtime_config", note: "Verify sync dir, writability, and git availability." },
|
|
92
|
+
{ tool: "sync", note: "Index scenes from disk so the main manuscript workflows can operate." },
|
|
75
93
|
],
|
|
76
94
|
},
|
|
77
95
|
{
|
|
78
|
-
id: "
|
|
79
|
-
label: "
|
|
80
|
-
use_when: "
|
|
96
|
+
id: "styleguide_setup_new",
|
|
97
|
+
label: "Set up a prose styleguide",
|
|
98
|
+
use_when: "Use only when styleguide configuration is intentionally part of the task and no suitable config exists yet.",
|
|
81
99
|
steps: [
|
|
82
|
-
{ tool: "
|
|
83
|
-
{ tool: "
|
|
100
|
+
{ tool: "describe_workflows", note: "Check context.scene_count; use that value as max_scenes in the next call." },
|
|
101
|
+
{ tool: "bootstrap_prose_styleguide_config", note: "Detect dominant conventions. Confirm suggestions with the user before applying." },
|
|
102
|
+
{ tool: "setup_prose_styleguide_config", note: "Only if ALL context.styleguide_exists fields are false — a config at any scope is sufficient. Create at project_root scope (requires project_id and language e.g. 'english_us'), or sync_root if no project_id is known." },
|
|
103
|
+
{ tool: "update_prose_styleguide_config", note: "Apply the fields accepted from bootstrap suggestions." },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "styleguide_drift_check",
|
|
108
|
+
label: "Check styleguide drift",
|
|
109
|
+
use_when: "Use only when styleguide conformance is intentionally the task and a styleguide config already exists.",
|
|
110
|
+
steps: [
|
|
111
|
+
{ tool: "get_prose_styleguide_config", note: "Confirm the currently resolved config." },
|
|
112
|
+
{ tool: "check_prose_styleguide_drift", note: "Detect non-conforming scenes. Pass project_id from context.project_id and set max_scenes from context.scene_count." },
|
|
113
|
+
{ tool: "update_prose_styleguide_config", note: "If drift found and user approves, update config or note the outliers." },
|
|
84
114
|
],
|
|
85
115
|
},
|
|
86
116
|
{
|