@hanna84/mcp-writing 2.18.1 → 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 +10 -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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
3
4
|
|
|
4
5
|
const BASE_URL = "http://localhost:3000";
|
|
5
6
|
|
|
@@ -22,8 +23,8 @@ async function connect() {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async function callTool(client, name, args = {}) {
|
|
25
|
-
const result = await client
|
|
26
|
-
return result.
|
|
26
|
+
const result = await callToolParsed(client, name, args);
|
|
27
|
+
return result.text;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const results = [];
|
|
@@ -45,12 +46,9 @@ async function runScenarios() {
|
|
|
45
46
|
try {
|
|
46
47
|
const text = await callTool(client, "find_scenes", { project_id: "the-lamb" });
|
|
47
48
|
const parsed = JSON.parse(text);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (isArray) {
|
|
52
|
-
log("1a", "PASS", `Got array of ${parsed.length} scenes (backward compat)`);
|
|
53
|
-
} else if (isEnvelope) {
|
|
49
|
+
const isEnvelope = parsed && parsed.results && parsed.total_count !== undefined;
|
|
50
|
+
|
|
51
|
+
if (isEnvelope) {
|
|
54
52
|
log("1b", "PASS", `Got envelope with results: ${parsed.results.length}, total_count: ${parsed.total_count}`);
|
|
55
53
|
} else {
|
|
56
54
|
log("1c", "FAIL", `Unknown shape: ${JSON.stringify(parsed).slice(0, 100)}`);
|
|
@@ -241,17 +239,23 @@ async function runScenarios() {
|
|
|
241
239
|
const ga = JSON.parse(await callTool(client, "get_arc", { character_id: "char-mira-nystrom", page_size: 1 }));
|
|
242
240
|
const sm = JSON.parse(await callTool(client, "search_metadata", { query: "scene", page_size: 1 }));
|
|
243
241
|
const lt = JSON.parse(await callTool(client, "list_threads", { project_id: "the-lamb" }));
|
|
242
|
+
const lc = JSON.parse(await callTool(client, "list_characters", { project_id: "the-lamb" }));
|
|
243
|
+
const lp = JSON.parse(await callTool(client, "list_places", { project_id: "the-lamb" }));
|
|
244
|
+
const sr = JSON.parse(await callTool(client, "search_reference", { query: "scene" }));
|
|
244
245
|
|
|
245
246
|
const commonFields = ["results", "total_count"];
|
|
246
247
|
const fsHas = commonFields.every(f => f in fs);
|
|
247
248
|
const gaHas = commonFields.every(f => f in ga);
|
|
248
249
|
const smHas = commonFields.every(f => f in sm);
|
|
249
250
|
const ltHas = commonFields.every(f => f in lt);
|
|
251
|
+
const lcHas = commonFields.every(f => f in lc);
|
|
252
|
+
const lpHas = commonFields.every(f => f in lp);
|
|
253
|
+
const srHas = commonFields.every(f => f in sr);
|
|
250
254
|
|
|
251
|
-
if (fsHas && gaHas && smHas && ltHas) {
|
|
252
|
-
log("11", "PASS", `All tools have results + total_count envelope`);
|
|
255
|
+
if (fsHas && gaHas && smHas && ltHas && lcHas && lpHas && srHas) {
|
|
256
|
+
log("11", "PASS", `All metadata-read tools have results + total_count envelope`);
|
|
253
257
|
} else {
|
|
254
|
-
log("11", "FAIL", `Missing fields: fs=${fsHas}, ga=${gaHas}, sm=${smHas}, lt=${ltHas}`);
|
|
258
|
+
log("11", "FAIL", `Missing fields: fs=${fsHas}, ga=${gaHas}, sm=${smHas}, lt=${ltHas}, lc=${lcHas}, lp=${lpHas}, sr=${srHas}`);
|
|
255
259
|
}
|
|
256
260
|
} catch (err) {
|
|
257
261
|
log("11", "FAIL", err.message);
|
|
@@ -267,14 +271,22 @@ async function runScenarios() {
|
|
|
267
271
|
ga: JSON.parse(await callTool(client, "get_arc", { character_id: "char-mira-nystrom" })),
|
|
268
272
|
sm: JSON.parse(await callTool(client, "search_metadata", { query: "scene" })),
|
|
269
273
|
lt: JSON.parse(await callTool(client, "list_threads", { project_id: "the-lamb" })),
|
|
274
|
+
lc: JSON.parse(await callTool(client, "list_characters", { project_id: "the-lamb" })),
|
|
275
|
+
lp: JSON.parse(await callTool(client, "list_places", { project_id: "the-lamb" })),
|
|
276
|
+
sr: JSON.parse(await callTool(client, "search_reference", { query: "scene" })),
|
|
270
277
|
};
|
|
271
278
|
|
|
272
|
-
const allValid = Object.values(smoke).every(obj =>
|
|
279
|
+
const allValid = Object.values(smoke).every(obj => (
|
|
280
|
+
obj !== null
|
|
281
|
+
&& typeof obj === "object"
|
|
282
|
+
&& "results" in obj
|
|
283
|
+
&& "total_count" in obj
|
|
284
|
+
));
|
|
273
285
|
|
|
274
286
|
if (allValid) {
|
|
275
|
-
log("12", "PASS", `All
|
|
287
|
+
log("12", "PASS", `All sampled metadata-read tools returned envelope responses`);
|
|
276
288
|
} else {
|
|
277
|
-
log("12", "FAIL", `Some responses did not
|
|
289
|
+
log("12", "FAIL", `Some responses did not match envelope shape`);
|
|
278
290
|
}
|
|
279
291
|
} catch (err) {
|
|
280
292
|
log("12", "FAIL", err.message);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
3
4
|
|
|
4
5
|
const BASE_URL = "http://localhost:3000";
|
|
5
6
|
|
|
@@ -12,9 +13,10 @@ let failed = 0;
|
|
|
12
13
|
|
|
13
14
|
async function test(label, toolName, args, check) {
|
|
14
15
|
try {
|
|
15
|
-
const
|
|
16
|
-
const text =
|
|
17
|
-
const
|
|
16
|
+
const parsed = await callToolParsed(client, toolName, args);
|
|
17
|
+
const text = parsed.text;
|
|
18
|
+
const result = parsed.raw;
|
|
19
|
+
const ok = check(text, result);
|
|
18
20
|
if (ok) {
|
|
19
21
|
console.log(` ✓ ${label}`);
|
|
20
22
|
passed++;
|
|
@@ -77,7 +79,12 @@ console.log("\n── get_scene_prose ──────────────
|
|
|
77
79
|
await test(
|
|
78
80
|
"returns prose for sc-001",
|
|
79
81
|
"get_scene_prose", { scene_id: "sc-001" },
|
|
80
|
-
t
|
|
82
|
+
(t, result) => {
|
|
83
|
+
if (result.structuredContent?.warning) {
|
|
84
|
+
console.log(` warning: ${result.structuredContent.warning}`);
|
|
85
|
+
}
|
|
86
|
+
return t.includes("gangway") && t.includes("Marcus");
|
|
87
|
+
}
|
|
81
88
|
);
|
|
82
89
|
|
|
83
90
|
await test(
|
|
@@ -7,6 +7,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
|
7
7
|
import { URL as NodeURL } from "node:url";
|
|
8
8
|
import { execSync } from "node:child_process";
|
|
9
9
|
import fs from "node:fs";
|
|
10
|
+
import { callToolParsed } from "./manual/mcp-result.mjs";
|
|
10
11
|
|
|
11
12
|
const ROOT = process.cwd();
|
|
12
13
|
|
|
@@ -38,7 +39,8 @@ async function connectClient(url) {
|
|
|
38
39
|
|
|
39
40
|
async function callTool(client, name, args = {}) {
|
|
40
41
|
try {
|
|
41
|
-
|
|
42
|
+
const parsed = await callToolParsed(client, name, args);
|
|
43
|
+
return parsed.raw;
|
|
42
44
|
} catch (e) {
|
|
43
45
|
return { error: e.message };
|
|
44
46
|
}
|
|
@@ -48,12 +50,14 @@ function parseResponse(result) {
|
|
|
48
50
|
if (result.error) return { error: result.error };
|
|
49
51
|
try {
|
|
50
52
|
const text = result.content?.[0]?.text;
|
|
53
|
+
const structured = result.structuredContent;
|
|
51
54
|
if (!text) return { raw: result };
|
|
52
55
|
// Try to parse as JSON
|
|
53
56
|
try {
|
|
54
|
-
|
|
57
|
+
const parsed = JSON.parse(text);
|
|
58
|
+
return structured ? { ...parsed, _structured: structured } : parsed;
|
|
55
59
|
} catch {
|
|
56
|
-
return { text };
|
|
60
|
+
return structured ? { text, _structured: structured } : { text };
|
|
57
61
|
}
|
|
58
62
|
} catch {
|
|
59
63
|
return { raw: result };
|
|
@@ -218,7 +222,10 @@ async function runPhaseB() {
|
|
|
218
222
|
const proseRes = await callTool(client, "get_scene_prose", { scene_id: results.firstSceneId });
|
|
219
223
|
const proseData = parseResponse(proseRes);
|
|
220
224
|
results.proseExcerpt = proseData.prose?.slice(0, 200) || proseData.text?.slice(0, 200) || proseRes.content?.[0]?.text?.slice(0, 200) || "(no prose)";
|
|
225
|
+
results.proseWarning = proseData._structured?.warning || null;
|
|
226
|
+
results.proseNextStep = proseData._structured?.next_step || null;
|
|
221
227
|
console.log("get_scene_prose excerpt:", results.proseExcerpt.slice(0, 150) + "...");
|
|
228
|
+
if (results.proseWarning) console.log("get_scene_prose warning:", results.proseWarning);
|
|
222
229
|
} else {
|
|
223
230
|
results.proseExcerpt = "(no scene_id available)";
|
|
224
231
|
}
|
|
@@ -260,6 +267,7 @@ async function main() {
|
|
|
260
267
|
console.log(" Place count:", phaseB.placeCount);
|
|
261
268
|
console.log(" Airport search count:", phaseB.airportSearchCount);
|
|
262
269
|
console.log(" First scene_id:", phaseB.firstSceneId || "N/A");
|
|
270
|
+
if (phaseB.proseWarning) console.log(" Prose warning:", phaseB.proseWarning);
|
|
263
271
|
console.log(" Prose excerpt (200 chars):", phaseB.proseExcerpt?.slice(0, 200) || "N/A");
|
|
264
272
|
if (phaseB.warnings.length) console.log(" Lint warnings:", phaseB.warnings.length > 0 ? "Yes (see above)" : "None");
|
|
265
273
|
if (phaseB.errors.length) console.log(" Errors:", phaseB.errors);
|
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
3
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
4
|
import { URL as NodeURL } from "node:url";
|
|
5
|
+
import { callToolParsed } from "./manual/mcp-result.mjs";
|
|
5
6
|
|
|
6
7
|
const ROOT = process.cwd();
|
|
7
8
|
|
|
@@ -31,11 +32,11 @@ try {
|
|
|
31
32
|
const transport = new SSEClientTransport(new NodeURL(`${BASE}/sse`));
|
|
32
33
|
await client.connect(transport);
|
|
33
34
|
|
|
34
|
-
await client
|
|
35
|
+
await callToolParsed(client, "sync", {});
|
|
35
36
|
|
|
36
|
-
const scenes = await client
|
|
37
|
+
const scenes = await callToolParsed(client, "find_scenes", { project_id: "scrivener-export", page_size: 3, page: 1 });
|
|
37
38
|
console.log("=== find_scenes raw response ===");
|
|
38
|
-
console.log(JSON.stringify(scenes, null, 2));
|
|
39
|
+
console.log(JSON.stringify(scenes.raw, null, 2));
|
|
39
40
|
|
|
40
41
|
await client.close();
|
|
41
42
|
} finally {
|
package/src/sync/sync.js
CHANGED
|
@@ -919,13 +919,10 @@ function pruneMissingScenes(db, seenSceneKeys, syncDir) {
|
|
|
919
919
|
WHERE source_kind = 'scene' AND source_project_id = ? AND source_id = ?
|
|
920
920
|
`).run(row.project_id ?? "", row.scene_id);
|
|
921
921
|
db.prepare(`DELETE FROM scenes WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
db.prepare(`DELETE FROM scene_places WHERE scene_id = ?`).run(row.scene_id);
|
|
927
|
-
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ?`).run(row.scene_id);
|
|
928
|
-
}
|
|
922
|
+
db.prepare(`DELETE FROM scene_characters WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
923
|
+
db.prepare(`DELETE FROM scene_places WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
924
|
+
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
925
|
+
db.prepare(`DELETE FROM scene_threads WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
929
926
|
}
|
|
930
927
|
}
|
|
931
928
|
|
|
@@ -994,14 +991,14 @@ export function indexSceneFile(db, syncDir, file, meta, prose) {
|
|
|
994
991
|
new Date().toISOString()
|
|
995
992
|
);
|
|
996
993
|
|
|
997
|
-
db.prepare(`DELETE FROM scene_characters WHERE scene_id = ?`).run(meta.scene_id);
|
|
998
|
-
db.prepare(`DELETE FROM scene_places WHERE scene_id = ?`).run(meta.scene_id);
|
|
999
|
-
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ?`).run(meta.scene_id);
|
|
994
|
+
db.prepare(`DELETE FROM scene_characters WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
|
|
995
|
+
db.prepare(`DELETE FROM scene_places WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
|
|
996
|
+
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
|
|
1000
997
|
|
|
1001
998
|
for (const c of (meta.characters ?? [])) {
|
|
1002
999
|
// Version continuity markers (e.g. v7.3, v3.3b) are tracked as tags, not characters
|
|
1003
1000
|
if (/^v\d[\d.a-z]*$/i.test(c)) {
|
|
1004
|
-
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, tag) VALUES (?, ?)`).run(meta.scene_id, c);
|
|
1001
|
+
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(meta.scene_id, project_id, c);
|
|
1005
1002
|
continue;
|
|
1006
1003
|
}
|
|
1007
1004
|
let cid = c;
|
|
@@ -1021,23 +1018,23 @@ export function indexSceneFile(db, syncDir, file, meta, prose) {
|
|
|
1021
1018
|
}
|
|
1022
1019
|
if (row) cid = row.character_id;
|
|
1023
1020
|
}
|
|
1024
|
-
db.prepare(`INSERT OR IGNORE INTO scene_characters (scene_id, character_id) VALUES (?, ?)`).run(
|
|
1025
|
-
meta.scene_id, cid
|
|
1021
|
+
db.prepare(`INSERT OR IGNORE INTO scene_characters (scene_id, project_id, character_id) VALUES (?, ?, ?)`).run(
|
|
1022
|
+
meta.scene_id, project_id, cid
|
|
1026
1023
|
);
|
|
1027
1024
|
}
|
|
1028
1025
|
for (const p of (meta.places ?? [])) {
|
|
1029
|
-
db.prepare(`INSERT OR IGNORE INTO scene_places (scene_id, place_id) VALUES (?, ?)`).run(
|
|
1030
|
-
meta.scene_id, p
|
|
1026
|
+
db.prepare(`INSERT OR IGNORE INTO scene_places (scene_id, project_id, place_id) VALUES (?, ?, ?)`).run(
|
|
1027
|
+
meta.scene_id, project_id, p
|
|
1031
1028
|
);
|
|
1032
1029
|
}
|
|
1033
1030
|
for (const t of (meta.tags ?? [])) {
|
|
1034
|
-
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, tag) VALUES (?, ?)`).run(
|
|
1035
|
-
meta.scene_id, t
|
|
1031
|
+
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(
|
|
1032
|
+
meta.scene_id, project_id, t
|
|
1036
1033
|
);
|
|
1037
1034
|
}
|
|
1038
1035
|
for (const v of (meta.versions ?? [])) {
|
|
1039
|
-
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, tag) VALUES (?, ?)`).run(
|
|
1040
|
-
meta.scene_id, v
|
|
1036
|
+
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(
|
|
1037
|
+
meta.scene_id, project_id, v
|
|
1041
1038
|
);
|
|
1042
1039
|
}
|
|
1043
1040
|
|
package/src/tools/editing.js
CHANGED
|
@@ -25,20 +25,37 @@ export function registerEditingTools(s, {
|
|
|
25
25
|
// ---- propose_edit --------------------------------------------------------
|
|
26
26
|
s.tool(
|
|
27
27
|
"propose_edit",
|
|
28
|
-
"Generate a proposed revision for a scene. Returns a proposal_id and a diff preview. Nothing is written yet — you must call commit_edit to apply the change. This tool requires git to be available.",
|
|
28
|
+
"Generate a proposed revision for a scene. Returns a proposal_id and a diff preview. Nothing is written yet — you must call commit_edit to apply the change. This tool requires git to be available. If scene IDs are reused across projects, omitting project_id returns CONFLICT with candidate project_ids.",
|
|
29
29
|
{
|
|
30
30
|
scene_id: z.string().describe("The scene_id to revise (e.g. 'sc-011-sebastian')."),
|
|
31
|
+
project_id: z.string().optional().describe("Optional project ID to disambiguate duplicate scene IDs across projects."),
|
|
31
32
|
instruction: z.string().describe("A brief instruction for the edit (e.g. 'Tighten the opening paragraph'). Used in the git commit message."),
|
|
32
33
|
revised_prose: z.string().describe("The complete revised prose text for the scene."),
|
|
33
34
|
},
|
|
34
|
-
async ({ scene_id, instruction, revised_prose }) => {
|
|
35
|
+
async ({ scene_id, project_id, instruction, revised_prose }) => {
|
|
35
36
|
if (!GIT_ENABLED) {
|
|
36
37
|
return errorResponse("GIT_UNAVAILABLE", "Git is not available — prose editing is not supported. Ensure git is installed and the sync directory is writable.");
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
40
|
+
let scene;
|
|
41
|
+
if (project_id) {
|
|
42
|
+
scene = db.prepare(`SELECT file_path, project_id FROM scenes WHERE scene_id = ? AND project_id = ?`).get(scene_id, project_id);
|
|
43
|
+
if (!scene) {
|
|
44
|
+
return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'. Hint: call find_scenes with project_id to get valid scene IDs.`);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const scenes = db.prepare(`SELECT file_path, project_id FROM scenes WHERE scene_id = ? ORDER BY project_id`).all(scene_id);
|
|
48
|
+
if (scenes.length === 0) {
|
|
49
|
+
return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found. Hint: call find_scenes to get valid scene IDs.`);
|
|
50
|
+
}
|
|
51
|
+
if (scenes.length > 1) {
|
|
52
|
+
return errorResponse(
|
|
53
|
+
"CONFLICT",
|
|
54
|
+
`Scene ID '${scene_id}' exists in multiple projects. Provide project_id to disambiguate.`,
|
|
55
|
+
{ scene_id, project_ids: scenes.map(s => s.project_id) }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
scene = scenes[0];
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
try {
|
|
@@ -66,6 +83,7 @@ export function registerEditingTools(s, {
|
|
|
66
83
|
const proposalId = generateProposalId();
|
|
67
84
|
pendingProposals.set(proposalId, {
|
|
68
85
|
scene_id,
|
|
86
|
+
project_id: scene.project_id,
|
|
69
87
|
scene_file_path: scene.file_path,
|
|
70
88
|
instruction,
|
|
71
89
|
revised_prose,
|
|
@@ -85,12 +103,16 @@ export function registerEditingTools(s, {
|
|
|
85
103
|
return jsonResponse({
|
|
86
104
|
proposal_id: proposalId,
|
|
87
105
|
scene_id,
|
|
106
|
+
project_id: scene.project_id,
|
|
88
107
|
instruction,
|
|
89
108
|
diff_preview: diffPreview,
|
|
90
109
|
noop,
|
|
91
110
|
note: noop
|
|
92
111
|
? "This proposal matches the current scene file. Calling commit_edit will be a no-op."
|
|
93
112
|
: "Review the diff above. Call commit_edit with this proposal_id to apply the change.",
|
|
113
|
+
next_step: noop
|
|
114
|
+
? "If this was intentional, no action is required. Otherwise, adjust revised_prose and call propose_edit again."
|
|
115
|
+
: "Review diff_preview, then call commit_edit to apply or discard_edit to reject.",
|
|
94
116
|
});
|
|
95
117
|
} catch (err) {
|
|
96
118
|
if (err.code === "ENOENT") {
|
|
@@ -107,9 +129,10 @@ export function registerEditingTools(s, {
|
|
|
107
129
|
"Apply a proposed edit and commit it to git. First creates a pre-edit snapshot, then writes the revised prose and metadata back to disk. The scene metadata stale flag is cleared.",
|
|
108
130
|
{
|
|
109
131
|
scene_id: z.string().describe("The scene_id being revised."),
|
|
132
|
+
project_id: z.string().optional().describe("Optional project ID. Required when scene IDs are duplicated across projects."),
|
|
110
133
|
proposal_id: z.string().describe("The proposal_id returned by propose_edit."),
|
|
111
134
|
},
|
|
112
|
-
async ({ scene_id, proposal_id }) => {
|
|
135
|
+
async ({ scene_id, project_id, proposal_id }) => {
|
|
113
136
|
if (!GIT_ENABLED) {
|
|
114
137
|
return errorResponse("GIT_UNAVAILABLE", "Git is not available — prose editing is not supported.");
|
|
115
138
|
}
|
|
@@ -123,6 +146,27 @@ export function registerEditingTools(s, {
|
|
|
123
146
|
return errorResponse("INVALID_EDIT", `Proposal '${proposal_id}' is for scene '${proposal.scene_id}', not '${scene_id}'.`);
|
|
124
147
|
}
|
|
125
148
|
|
|
149
|
+
if (!project_id && proposal.project_id) {
|
|
150
|
+
const projectMatches = db.prepare(`SELECT project_id FROM scenes WHERE scene_id = ? ORDER BY project_id`).all(scene_id);
|
|
151
|
+
if (projectMatches.length > 1) {
|
|
152
|
+
return errorResponse(
|
|
153
|
+
"CONFLICT",
|
|
154
|
+
`Scene ID '${scene_id}' exists in multiple projects. Provide project_id to disambiguate commit_edit.`,
|
|
155
|
+
{
|
|
156
|
+
scene_id,
|
|
157
|
+
project_ids: projectMatches.map((row) => row.project_id),
|
|
158
|
+
proposal_project_id: proposal.project_id,
|
|
159
|
+
proposal_id,
|
|
160
|
+
next_step: `Retry commit_edit with project_id '${proposal.project_id}' and the same proposal_id.`,
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (project_id && proposal.project_id && proposal.project_id !== project_id) {
|
|
167
|
+
return errorResponse("INVALID_EDIT", `Proposal '${proposal_id}' is for project '${proposal.project_id}', not '${project_id}'.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
126
170
|
try {
|
|
127
171
|
const proseWriteDiagnostics = getFileWriteDiagnostics(proposal.scene_file_path);
|
|
128
172
|
if (proseWriteDiagnostics.stat_error_code === "EACCES" || proseWriteDiagnostics.stat_error_code === "EPERM") {
|
|
@@ -184,10 +228,12 @@ export function registerEditingTools(s, {
|
|
|
184
228
|
return jsonResponse({
|
|
185
229
|
ok: true,
|
|
186
230
|
scene_id,
|
|
231
|
+
project_id: proposal.project_id ?? null,
|
|
187
232
|
proposal_id,
|
|
188
233
|
snapshot_commit: null,
|
|
189
234
|
noop: true,
|
|
190
235
|
message: `Proposal for scene '${scene_id}' matches the current file. Nothing was written.`,
|
|
236
|
+
next_step: "No changes were applied. If you still need edits, call propose_edit with revised prose.",
|
|
191
237
|
});
|
|
192
238
|
}
|
|
193
239
|
|
|
@@ -204,10 +250,12 @@ export function registerEditingTools(s, {
|
|
|
204
250
|
return jsonResponse({
|
|
205
251
|
ok: true,
|
|
206
252
|
scene_id,
|
|
253
|
+
project_id: proposal.project_id ?? null,
|
|
207
254
|
proposal_id,
|
|
208
255
|
snapshot_commit: snapshot.commit_hash,
|
|
209
256
|
noop: false,
|
|
210
257
|
message: `Applied edit to scene '${scene_id}'${snapshot.commit_hash ? ` (snapshot: ${snapshot.commit_hash.substring(0, 7)})` : " (no pre-edit snapshot needed)"}`,
|
|
258
|
+
next_step: "Edit applied. Run get_scene_prose to verify prose, then continue with additional targeted edits if needed.",
|
|
211
259
|
});
|
|
212
260
|
} catch (err) {
|
|
213
261
|
if (err.code === "ENOENT") {
|
|
@@ -236,6 +284,7 @@ export function registerEditingTools(s, {
|
|
|
236
284
|
ok: true,
|
|
237
285
|
proposal_id,
|
|
238
286
|
message: `Discarded proposal '${proposal_id}' for scene '${proposal.scene_id}'.`,
|
|
287
|
+
next_step: "Proposal discarded. Call propose_edit again if you want to stage a revised alternative.",
|
|
239
288
|
});
|
|
240
289
|
}
|
|
241
290
|
);
|
|
@@ -293,15 +342,32 @@ export function registerEditingTools(s, {
|
|
|
293
342
|
"List git commit history for a scene, with timestamps and commit messages. Use this to find commit hashes for get_scene_prose historical retrieval.",
|
|
294
343
|
{
|
|
295
344
|
scene_id: z.string().describe("The scene_id to list snapshots for."),
|
|
345
|
+
project_id: z.string().optional().describe("Optional project ID to disambiguate duplicate scene IDs across projects."),
|
|
296
346
|
},
|
|
297
|
-
async ({ scene_id }) => {
|
|
347
|
+
async ({ scene_id, project_id }) => {
|
|
298
348
|
if (!GIT_ENABLED) {
|
|
299
349
|
return errorResponse("GIT_UNAVAILABLE", "Git is not available — snapshots cannot be retrieved.");
|
|
300
350
|
}
|
|
301
351
|
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
352
|
+
let scene;
|
|
353
|
+
if (project_id) {
|
|
354
|
+
scene = db.prepare(`SELECT file_path, project_id FROM scenes WHERE scene_id = ? AND project_id = ?`).get(scene_id, project_id);
|
|
355
|
+
if (!scene) {
|
|
356
|
+
return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'.`);
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
const scenes = db.prepare(`SELECT file_path, project_id FROM scenes WHERE scene_id = ? ORDER BY project_id`).all(scene_id);
|
|
360
|
+
if (scenes.length === 0) {
|
|
361
|
+
return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found.`);
|
|
362
|
+
}
|
|
363
|
+
if (scenes.length > 1) {
|
|
364
|
+
return errorResponse(
|
|
365
|
+
"CONFLICT",
|
|
366
|
+
`Scene ID '${scene_id}' exists in multiple projects. Provide project_id to disambiguate.`,
|
|
367
|
+
{ scene_id, project_ids: scenes.map((row) => row.project_id) }
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
scene = scenes[0];
|
|
305
371
|
}
|
|
306
372
|
|
|
307
373
|
try {
|
|
@@ -312,6 +378,7 @@ export function registerEditingTools(s, {
|
|
|
312
378
|
|
|
313
379
|
return jsonResponse({
|
|
314
380
|
scene_id,
|
|
381
|
+
project_id: scene.project_id ?? null,
|
|
315
382
|
snapshots: snapshots.map(s => ({
|
|
316
383
|
commit_hash: s.commit_hash,
|
|
317
384
|
short_hash: s.commit_hash.substring(0, 7),
|
package/src/tools/metadata.js
CHANGED
|
@@ -347,15 +347,15 @@ export function registerMetadataTools(s, {
|
|
|
347
347
|
`).run(thread_id, project_id, thread_name, status ?? "active");
|
|
348
348
|
|
|
349
349
|
db.prepare(`
|
|
350
|
-
INSERT INTO scene_threads (scene_id, thread_id, beat)
|
|
351
|
-
VALUES (?, ?, ?)
|
|
352
|
-
ON CONFLICT (scene_id, thread_id) DO UPDATE SET
|
|
350
|
+
INSERT INTO scene_threads (scene_id, project_id, thread_id, beat)
|
|
351
|
+
VALUES (?, ?, ?, ?)
|
|
352
|
+
ON CONFLICT (scene_id, project_id, thread_id) DO UPDATE SET
|
|
353
353
|
beat = excluded.beat
|
|
354
|
-
`).run(scene_id, thread_id, beat ?? null);
|
|
354
|
+
`).run(scene_id, project_id, thread_id, beat ?? null);
|
|
355
355
|
|
|
356
356
|
const thread = db.prepare(`SELECT * FROM threads WHERE thread_id = ?`).get(thread_id);
|
|
357
|
-
const link = db.prepare(`SELECT scene_id, thread_id, beat FROM scene_threads WHERE scene_id = ? AND thread_id = ?`)
|
|
358
|
-
.get(scene_id, thread_id);
|
|
357
|
+
const link = db.prepare(`SELECT scene_id, project_id, thread_id, beat FROM scene_threads WHERE scene_id = ? AND project_id = ? AND thread_id = ?`)
|
|
358
|
+
.get(scene_id, project_id, thread_id);
|
|
359
359
|
|
|
360
360
|
return jsonResponse({
|
|
361
361
|
ok: true,
|
|
@@ -74,7 +74,12 @@ export function registerReviewBundleTools(s, {
|
|
|
74
74
|
bundle_name,
|
|
75
75
|
format,
|
|
76
76
|
});
|
|
77
|
-
return jsonResponse(
|
|
77
|
+
return jsonResponse({
|
|
78
|
+
...plan,
|
|
79
|
+
next_step: plan.strictness_result?.can_proceed
|
|
80
|
+
? "Preview complete. Review warnings and planned_outputs, then call create_review_bundle with the same scope and output_dir."
|
|
81
|
+
: "Preview complete, but strictness blockers are present. Resolve blockers (for example stale metadata) or switch to strictness='warn' before create_review_bundle.",
|
|
82
|
+
});
|
|
78
83
|
} catch (error) {
|
|
79
84
|
if (error instanceof ReviewBundlePlanError) {
|
|
80
85
|
return errorResponse(error.code, error.message, error.details);
|
|
@@ -172,7 +177,11 @@ export function registerReviewBundleTools(s, {
|
|
|
172
177
|
return errorResponse(
|
|
173
178
|
"STRICTNESS_BLOCKED",
|
|
174
179
|
"Bundle generation blocked by strictness policy.",
|
|
175
|
-
{
|
|
180
|
+
{
|
|
181
|
+
strictness_result: plan.strictness_result,
|
|
182
|
+
warning_summary: plan.warning_summary,
|
|
183
|
+
next_step: "Resolve blockers from strictness_result (for example by running enrich_scene on stale scenes), then re-run create_review_bundle.",
|
|
184
|
+
}
|
|
176
185
|
);
|
|
177
186
|
}
|
|
178
187
|
|
|
@@ -200,6 +209,7 @@ export function registerReviewBundleTools(s, {
|
|
|
200
209
|
generated_at: artifacts.generated_at,
|
|
201
210
|
project_id: plan.resolved_scope.project_id,
|
|
202
211
|
},
|
|
212
|
+
next_step: "Bundle created. Share output_paths with reviewers, or run preview_review_bundle again to adjust scope/profile before regenerating.",
|
|
203
213
|
});
|
|
204
214
|
} catch (error) {
|
|
205
215
|
if (error instanceof ReviewBundlePlanError) {
|