@hanna84/mcp-writing 3.18.0 → 3.19.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 +14 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/core/filesystem-boundary.js +12 -0
- package/src/index.js +1 -0
- package/src/structure/project-backup-diagnostics.js +346 -0
- package/src/structure/project-backup-operations.js +116 -0
- package/src/structure/project-backup-refresh.js +160 -0
- package/src/structure/project-backup.js +476 -0
- package/src/tools/editing.js +55 -0
- package/src/tools/metadata.js +469 -8
- package/src/tools/search.js +34 -0
- package/src/tools/sync.js +200 -0
- package/src/workflows/workflow-catalogue.js +2 -0
package/src/tools/search.js
CHANGED
|
@@ -4,6 +4,10 @@ import matter from "gray-matter";
|
|
|
4
4
|
import { readMeta } from "../sync/sync.js";
|
|
5
5
|
import { persistSceneReferenceLink, upsertExplicitReferenceLinkRow } from "./reference-link-persistence.js";
|
|
6
6
|
import { resolveValidatedChapterFilter } from "../core/chapter-resolution.js";
|
|
7
|
+
import {
|
|
8
|
+
createToolActor,
|
|
9
|
+
refreshProjectBackupAfterMutation,
|
|
10
|
+
} from "../structure/project-backup-refresh.js";
|
|
7
11
|
|
|
8
12
|
function accumulateSuggestionScore(scoreMap, rows, sourceLabel) {
|
|
9
13
|
for (const row of rows) {
|
|
@@ -59,6 +63,7 @@ export function registerSearchTools(s, {
|
|
|
59
63
|
db,
|
|
60
64
|
SYNC_DIR,
|
|
61
65
|
SYNC_DIR_WRITABLE,
|
|
66
|
+
MCP_SERVER_VERSION = "0.0.0",
|
|
62
67
|
GIT_ENABLED,
|
|
63
68
|
errorResponse,
|
|
64
69
|
paginateRows,
|
|
@@ -1292,6 +1297,32 @@ export function registerSearchTools(s, {
|
|
|
1292
1297
|
});
|
|
1293
1298
|
}
|
|
1294
1299
|
|
|
1300
|
+
const backupResult = appliedLinks.length
|
|
1301
|
+
? refreshProjectBackupAfterMutation(db, {
|
|
1302
|
+
syncDir: SYNC_DIR,
|
|
1303
|
+
projectId: resolvedProjectId,
|
|
1304
|
+
applicationVersion: MCP_SERVER_VERSION,
|
|
1305
|
+
operation: "suggest_scene_references",
|
|
1306
|
+
actor: createToolActor("suggest_scene_references"),
|
|
1307
|
+
affected: {
|
|
1308
|
+
scenes: [scene_id],
|
|
1309
|
+
reference_docs: appliedLinks.map(link => link.target_doc_id),
|
|
1310
|
+
},
|
|
1311
|
+
summary: `Applied ${appliedLinks.length} suggested reference link(s) for scene "${scene_id}".`,
|
|
1312
|
+
before: null,
|
|
1313
|
+
after: {
|
|
1314
|
+
applied_links: appliedLinks,
|
|
1315
|
+
},
|
|
1316
|
+
metadata: {
|
|
1317
|
+
failed_count: failedLinks.length,
|
|
1318
|
+
},
|
|
1319
|
+
})
|
|
1320
|
+
: {
|
|
1321
|
+
operation_history: null,
|
|
1322
|
+
backup_refresh: null,
|
|
1323
|
+
backup_warnings: [],
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1295
1326
|
return {
|
|
1296
1327
|
content: [{
|
|
1297
1328
|
type: "text",
|
|
@@ -1305,6 +1336,9 @@ export function registerSearchTools(s, {
|
|
|
1305
1336
|
applied_links: appliedLinks,
|
|
1306
1337
|
failed_count: failedLinks.length,
|
|
1307
1338
|
failed_links: failedLinks,
|
|
1339
|
+
operation_history: backupResult.operation_history,
|
|
1340
|
+
backup_refresh: backupResult.backup_refresh,
|
|
1341
|
+
backup_warnings: backupResult.backup_warnings,
|
|
1308
1342
|
candidates: enriched,
|
|
1309
1343
|
}, null, 2),
|
|
1310
1344
|
}],
|
package/src/tools/sync.js
CHANGED
|
@@ -10,6 +10,15 @@ import {
|
|
|
10
10
|
defaultStructureExportFileName,
|
|
11
11
|
writeStructureExportFile,
|
|
12
12
|
} from "../structure/structure-export.js";
|
|
13
|
+
import {
|
|
14
|
+
buildProjectBackup,
|
|
15
|
+
writeProjectBackupFiles,
|
|
16
|
+
} from "../structure/project-backup.js";
|
|
17
|
+
import { runProjectBackupDiagnostics } from "../structure/project-backup-diagnostics.js";
|
|
18
|
+
import {
|
|
19
|
+
createToolActor,
|
|
20
|
+
refreshProjectBackupAfterMutation,
|
|
21
|
+
} from "../structure/project-backup-refresh.js";
|
|
13
22
|
import { restoreStructureFromExport } from "../structure/structure-restore.js";
|
|
14
23
|
|
|
15
24
|
export function registerSyncTools(s, {
|
|
@@ -18,6 +27,7 @@ export function registerSyncTools(s, {
|
|
|
18
27
|
SYNC_DIR_ABS,
|
|
19
28
|
SYNC_DIR_REAL,
|
|
20
29
|
SYNC_DIR_WRITABLE,
|
|
30
|
+
MCP_SERVER_VERSION = "0.0.0",
|
|
21
31
|
asyncJobs,
|
|
22
32
|
errorResponse,
|
|
23
33
|
jsonResponse,
|
|
@@ -33,6 +43,14 @@ export function registerSyncTools(s, {
|
|
|
33
43
|
deriveLoglineFromProse,
|
|
34
44
|
inferCharacterIdsFromProse,
|
|
35
45
|
}) {
|
|
46
|
+
function backupMutationFields(backupResult) {
|
|
47
|
+
return {
|
|
48
|
+
operation_history: backupResult.operation_history,
|
|
49
|
+
backup_refresh: backupResult.backup_refresh,
|
|
50
|
+
backup_warnings: backupResult.backup_warnings,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
s.tool("sync", "Re-scan the sync folder and update derived scene/character/place indexes from disk. For already managed projects, sync reports file-derived chapter or epigraph drift without adopting it as canonical structure; use explicit import, repair, or structure tools for structural changes.", {}, async () => {
|
|
37
55
|
const result = syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
|
|
38
56
|
const parts = [`Sync complete. ${result.indexed} scenes indexed. ${result.staleMarked} scenes marked stale.`];
|
|
@@ -151,6 +169,143 @@ export function registerSyncTools(s, {
|
|
|
151
169
|
}
|
|
152
170
|
);
|
|
153
171
|
|
|
172
|
+
s.tool(
|
|
173
|
+
"export_project_backup",
|
|
174
|
+
"Generate a deterministic project backup bundle from SQLite canonical state. Writes manifest.json, canonical.snapshot.json, and operations.jsonl under WRITING_SYNC_DIR for explicit review and future restore workflows; this is generated transparency only and does not mutate canonical state.",
|
|
175
|
+
{
|
|
176
|
+
project_id: z.string().describe("Project ID to back up (e.g. 'test-novel' or 'universe-1/book-1-the-lamb')."),
|
|
177
|
+
output_dir: z.string().optional().describe("Directory under WRITING_SYNC_DIR where manifest.json, canonical.snapshot.json, and operations.jsonl should be written. Defaults to project-backups/<project_id>."),
|
|
178
|
+
},
|
|
179
|
+
async ({ project_id, output_dir }) => {
|
|
180
|
+
if (!SYNC_DIR_WRITABLE) {
|
|
181
|
+
return errorResponse("READ_ONLY", "Cannot export project backup: sync dir is read-only.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const projectIdCheck = validateProjectId(project_id);
|
|
185
|
+
if (!projectIdCheck.ok) {
|
|
186
|
+
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const requestedOutputDir = output_dir
|
|
191
|
+
? (path.isAbsolute(output_dir) ? output_dir : path.join(SYNC_DIR_ABS, output_dir))
|
|
192
|
+
: path.join(SYNC_DIR_ABS, "project-backups", project_id);
|
|
193
|
+
const { resolvedOutputDir, relativeToSyncDir } = resolveOutputDirWithinSync(requestedOutputDir);
|
|
194
|
+
const relativeBase = relativeToSyncDir.split(path.sep).filter(Boolean).join("/");
|
|
195
|
+
const outputDirSegments = relativeToSyncDir
|
|
196
|
+
.split(path.sep)
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.map(segment => segment.toLowerCase());
|
|
199
|
+
if (outputDirSegments.includes("scenes")) {
|
|
200
|
+
return errorResponse(
|
|
201
|
+
"INVALID_OUTPUT_DIR",
|
|
202
|
+
"output_dir cannot be inside a scenes directory. Choose a dedicated generated backup folder under WRITING_SYNC_DIR.",
|
|
203
|
+
{ output_dir: resolvedOutputDir }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const built = buildProjectBackup(db, {
|
|
208
|
+
projectId: project_id,
|
|
209
|
+
syncDir: SYNC_DIR_ABS,
|
|
210
|
+
applicationVersion: MCP_SERVER_VERSION,
|
|
211
|
+
backupLocation: relativeBase ? `${relativeBase}/` : "./",
|
|
212
|
+
});
|
|
213
|
+
if (!built.ok) {
|
|
214
|
+
return errorResponse(built.error.code, built.error.message, built.error.details);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const written = writeProjectBackupFiles(built, { outputDir: resolvedOutputDir });
|
|
218
|
+
const relativePath = fileName => (relativeBase ? `${relativeBase}/${fileName}` : fileName);
|
|
219
|
+
|
|
220
|
+
return jsonResponse({
|
|
221
|
+
ok: true,
|
|
222
|
+
action: "exported",
|
|
223
|
+
project_id,
|
|
224
|
+
output_dir: resolvedOutputDir,
|
|
225
|
+
relative_output_dir: relativeBase,
|
|
226
|
+
files: {
|
|
227
|
+
manifest: written.manifestPath,
|
|
228
|
+
canonical_snapshot: written.snapshotPath,
|
|
229
|
+
operations: written.operationLogPath,
|
|
230
|
+
},
|
|
231
|
+
written: written.written,
|
|
232
|
+
relative_files: {
|
|
233
|
+
manifest: relativePath("manifest.json"),
|
|
234
|
+
canonical_snapshot: relativePath("canonical.snapshot.json"),
|
|
235
|
+
operations: relativePath("operations.jsonl"),
|
|
236
|
+
},
|
|
237
|
+
manifest: {
|
|
238
|
+
schema_version: built.manifest.schema_version,
|
|
239
|
+
canonical_source: built.manifest.canonical_source,
|
|
240
|
+
generated_transparency: built.manifest.generated_transparency,
|
|
241
|
+
mutation_surface: built.manifest.mutation_surface,
|
|
242
|
+
backup_location: built.manifest.backup_location,
|
|
243
|
+
restore_policy: built.manifest.restore_policy,
|
|
244
|
+
privacy: built.manifest.privacy,
|
|
245
|
+
coverage: built.manifest.coverage,
|
|
246
|
+
checksums: built.manifest.checksums,
|
|
247
|
+
},
|
|
248
|
+
next_step: "Review or commit the generated project backup bundle. Do not edit it as a mutation surface; future restore tools will treat it as explicit recovery input.",
|
|
249
|
+
});
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (
|
|
252
|
+
error &&
|
|
253
|
+
typeof error === "object" &&
|
|
254
|
+
error.name === "CoreValidationError" &&
|
|
255
|
+
typeof error.code === "string"
|
|
256
|
+
) {
|
|
257
|
+
return errorResponse(error.code, error.message ?? "Request failed.", error.details);
|
|
258
|
+
}
|
|
259
|
+
return errorResponse(
|
|
260
|
+
"EXPORT_PROJECT_BACKUP_FAILED",
|
|
261
|
+
error instanceof Error ? error.message : "Failed to export project backup."
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
s.tool(
|
|
268
|
+
"diagnose_project_backups",
|
|
269
|
+
"Run read-only diagnostics for a generated project backup bundle. Reports missing, partial, wrong-project, incompatible-schema, tampered, unreadable, and stale backups without mutating SQLite or generated files.",
|
|
270
|
+
{
|
|
271
|
+
project_id: z.string().describe("Project ID whose backup bundle should be diagnosed (e.g. 'test-novel')."),
|
|
272
|
+
backup_dir: z.string().optional().describe("Directory under WRITING_SYNC_DIR containing manifest.json and canonical.snapshot.json. Defaults to project-backups/<project_id>."),
|
|
273
|
+
},
|
|
274
|
+
async ({ project_id, backup_dir } = {}) => {
|
|
275
|
+
const projectIdCheck = validateProjectId(project_id);
|
|
276
|
+
if (!projectIdCheck.ok) {
|
|
277
|
+
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const requestedBackupDir = backup_dir
|
|
282
|
+
? (path.isAbsolute(backup_dir) ? backup_dir : path.join(SYNC_DIR_ABS, backup_dir))
|
|
283
|
+
: path.join(SYNC_DIR_ABS, "project-backups", project_id);
|
|
284
|
+
const { resolvedOutputDir } = resolveOutputDirWithinSync(requestedBackupDir);
|
|
285
|
+
|
|
286
|
+
return jsonResponse(runProjectBackupDiagnostics(db, {
|
|
287
|
+
syncDir: SYNC_DIR_ABS,
|
|
288
|
+
backupDir: resolvedOutputDir,
|
|
289
|
+
projectId: project_id,
|
|
290
|
+
applicationVersion: MCP_SERVER_VERSION,
|
|
291
|
+
}));
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (
|
|
294
|
+
error &&
|
|
295
|
+
typeof error === "object" &&
|
|
296
|
+
error.name === "CoreValidationError" &&
|
|
297
|
+
typeof error.code === "string"
|
|
298
|
+
) {
|
|
299
|
+
return errorResponse(error.code, error.message ?? "Request failed.", error.details);
|
|
300
|
+
}
|
|
301
|
+
return errorResponse(
|
|
302
|
+
"DIAGNOSE_PROJECT_BACKUPS_FAILED",
|
|
303
|
+
error instanceof Error ? error.message : "Failed to diagnose project backup."
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
|
|
154
309
|
s.tool(
|
|
155
310
|
"restore_structure_from_export",
|
|
156
311
|
"Explicitly restore canonical SQLite chapter, scene-placement, and epigraph structure from a trusted generated structure export. This is a repair workflow, never a sync side effect; it validates project identity, schema, checksum, file presence, and conflicts before applying a transaction.",
|
|
@@ -573,6 +728,28 @@ export function registerSyncTools(s, {
|
|
|
573
728
|
db.prepare(`UPDATE scenes SET metadata_stale = 0 WHERE scene_id = ? AND project_id = ?`)
|
|
574
729
|
.run(sceneId, project_id);
|
|
575
730
|
}
|
|
731
|
+
|
|
732
|
+
if (changedScenes.length > 0) {
|
|
733
|
+
const backupResult = refreshProjectBackupAfterMutation(db, {
|
|
734
|
+
syncDir: SYNC_DIR,
|
|
735
|
+
projectId: project_id,
|
|
736
|
+
applicationVersion: MCP_SERVER_VERSION,
|
|
737
|
+
operation: "enrich_scene_characters_batch",
|
|
738
|
+
actor: createToolActor("enrich_scene_characters_batch"),
|
|
739
|
+
affected: {
|
|
740
|
+
scenes: changedScenes,
|
|
741
|
+
},
|
|
742
|
+
summary: `Applied batch character enrichment to ${changedScenes.length} scene(s).`,
|
|
743
|
+
before: null,
|
|
744
|
+
after: {
|
|
745
|
+
scenes_changed: changedScenes.length,
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
completedJob.result = {
|
|
749
|
+
...completedJob.result,
|
|
750
|
+
...backupMutationFields(backupResult),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
576
753
|
},
|
|
577
754
|
});
|
|
578
755
|
|
|
@@ -735,6 +912,28 @@ export function registerSyncTools(s, {
|
|
|
735
912
|
});
|
|
736
913
|
db.prepare(`UPDATE scenes SET metadata_stale = 0 WHERE scene_id = ? AND project_id = ?`)
|
|
737
914
|
.run(scene.scene_id, scene.project_id);
|
|
915
|
+
const backupResult = refreshProjectBackupAfterMutation(db, {
|
|
916
|
+
syncDir: SYNC_DIR,
|
|
917
|
+
projectId: scene.project_id,
|
|
918
|
+
applicationVersion: MCP_SERVER_VERSION,
|
|
919
|
+
operation: "enrich_scene",
|
|
920
|
+
actor: createToolActor("enrich_scene"),
|
|
921
|
+
affected: {
|
|
922
|
+
scenes: [scene.scene_id],
|
|
923
|
+
},
|
|
924
|
+
summary: `Enriched scene "${scene.scene_id}".`,
|
|
925
|
+
before: null,
|
|
926
|
+
after: {
|
|
927
|
+
scene: {
|
|
928
|
+
scene_id: scene.scene_id,
|
|
929
|
+
project_id: scene.project_id,
|
|
930
|
+
updated_fields: {
|
|
931
|
+
logline: Boolean(inferredLogline),
|
|
932
|
+
characters: inferredCharacters.length,
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
});
|
|
738
937
|
|
|
739
938
|
return jsonResponse({
|
|
740
939
|
ok: true,
|
|
@@ -746,6 +945,7 @@ export function registerSyncTools(s, {
|
|
|
746
945
|
characters: inferredCharacters.length,
|
|
747
946
|
},
|
|
748
947
|
metadata_stale: false,
|
|
948
|
+
...backupMutationFields(backupResult),
|
|
749
949
|
});
|
|
750
950
|
} catch (err) {
|
|
751
951
|
if (err?.name === "CoreValidationError") {
|
|
@@ -89,8 +89,10 @@ export const WORKFLOW_CATALOGUE = [
|
|
|
89
89
|
{ tool: "move_scene", note: "Use when a scene should move to another canonical chapter and/or unused timeline_position; this does not move the scene source file." },
|
|
90
90
|
{ tool: "assign_scene_to_chapter", note: "Use this named structure workflow for chapter assignment or clearing instead of editing chapter fields through generic metadata updates." },
|
|
91
91
|
{ tool: "diagnose_structure", note: "Run when the assignment is part of a drift repair workflow, when folder-derived structure may disagree with the requested link, or before trusting an export for recovery." },
|
|
92
|
+
{ tool: "diagnose_project_backups", note: "Structure mutation tools normally refresh the project backup automatically; run this if backup_warnings were returned or you need to verify backup freshness before recovery work." },
|
|
92
93
|
{ tool: "restore_structure_from_export", note: "Use only as an explicit repair workflow after diagnostics show a trusted generated export can recover canonical SQLite structure." },
|
|
93
94
|
{ tool: "export_structure_snapshot", note: "Generate a deterministic SQLite-derived structure export for Git review after canonical structure changes; editing the export does not mutate structure." },
|
|
95
|
+
{ tool: "export_project_backup", note: "Generate or repair the broader deterministic project backup bundle for Git review and future recovery input; editing it does not mutate canonical state." },
|
|
94
96
|
],
|
|
95
97
|
},
|
|
96
98
|
{
|