@hanna84/mcp-writing 1.11.0 → 1.11.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 +10 -0
- package/README.md +12 -0
- package/index.js +229 -5
- package/package.json +1 -1
- package/scripts/async-job-runner.mjs +47 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,21 @@ 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
|
+
#### [v1.11.1](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v1.11.0...v1.11.1)
|
|
9
|
+
|
|
10
|
+
- Implement graceful batch cancellation and complete Phase D coverage [`#59`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/59)
|
|
12
|
+
|
|
7
13
|
#### [v1.11.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
14
|
/compare/v1.10.0...v1.11.0)
|
|
9
15
|
|
|
16
|
+
> 21 April 2026
|
|
17
|
+
|
|
10
18
|
- feat: add reusable manual real-data Scrivener test runner [`#58`](https://github.com/hannasdev/mcp-writing.git
|
|
11
19
|
/pull/58)
|
|
20
|
+
- Release 1.11.0 [`5938d51`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/5938d51b277dcce2f5fcfe728ff2112d3af644e5)
|
|
12
22
|
|
|
13
23
|
#### [v1.10.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
24
|
/compare/v1.9.4...v1.10.0)
|
package/README.md
CHANGED
|
@@ -76,5 +76,17 @@ Goal: let AI propose prose edits without losing control of your draft.
|
|
|
76
76
|
|
|
77
77
|
Outcome: you get AI speed with explicit approval and recoverable history for every applied change.
|
|
78
78
|
|
|
79
|
+
### 5) Refreshing scene-character links after imports or major rewrites
|
|
80
|
+
|
|
81
|
+
Goal: rebuild scene-to-character links in a controlled way after imported prose changes or metadata drift.
|
|
82
|
+
|
|
83
|
+
1. Start with `enrich_scene_characters_batch` using the default `dry_run=true` to preview inferred links for a project, chapter, or explicit scene list.
|
|
84
|
+
2. Poll `get_async_job_status` until the batch job completes, then review `job.result.results` for changed scenes, ambiguous matches, and partial failures.
|
|
85
|
+
3. Spot-check a few affected scenes with `get_scene_prose` if the changes touch important continuity or cast-heavy chapters.
|
|
86
|
+
4. Re-run `enrich_scene_characters_batch` with `dry_run=false` once the preview looks correct.
|
|
87
|
+
5. If you want a destructive overwrite instead of additive merge behavior, use `replace_mode=replace` with `confirm_replace=true` deliberately.
|
|
88
|
+
|
|
89
|
+
Outcome: character-link maintenance becomes a preview-first batch operation instead of a one-off regex script or manual sidecar cleanup.
|
|
90
|
+
|
|
79
91
|
## License
|
|
80
92
|
AGPL-3.0-only
|
package/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { isGitAvailable, isGitRepository, initGitRepository, createSnapshot, lis
|
|
|
17
17
|
import { renderCharacterArcTemplate, renderCharacterSheetTemplate, renderPlaceSheetTemplate, slugifyEntityName } from "./world-entity-templates.js";
|
|
18
18
|
import { importScrivenerSync, validateProjectId } from "./importer.js";
|
|
19
19
|
import { mergeScrivenerProjectMetadata } from "./scrivener-direct.js";
|
|
20
|
+
import { ASYNC_PROGRESS_PREFIX } from "./async-progress.js";
|
|
20
21
|
|
|
21
22
|
const SYNC_DIR = process.env.WRITING_SYNC_DIR ?? "./sync";
|
|
22
23
|
const DB_PATH = process.env.DB_PATH ?? "./writing.db";
|
|
@@ -119,12 +120,14 @@ function toPublicJob(job, includeResult = true) {
|
|
|
119
120
|
finished_at: job.finishedAt,
|
|
120
121
|
pid: job.pid,
|
|
121
122
|
error: job.error,
|
|
123
|
+
...(job.progress ? { progress: job.progress } : {}),
|
|
122
124
|
...(includeResult ? { result: job.result } : {}),
|
|
123
125
|
};
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
function startAsyncJob({ kind, requestPayload, onComplete }) {
|
|
127
129
|
pruneAsyncJobs();
|
|
130
|
+
const progressPrefix = ASYNC_PROGRESS_PREFIX;
|
|
128
131
|
|
|
129
132
|
const id = randomUUID();
|
|
130
133
|
const tmpPrefix = path.join(os.tmpdir(), "mcp-writing-job-");
|
|
@@ -156,14 +159,38 @@ function startAsyncJob({ kind, requestPayload, onComplete }) {
|
|
|
156
159
|
requestPath,
|
|
157
160
|
resultPath,
|
|
158
161
|
result: null,
|
|
162
|
+
progress: null,
|
|
159
163
|
error: null,
|
|
160
164
|
onComplete,
|
|
161
165
|
child,
|
|
162
166
|
};
|
|
163
167
|
asyncJobs.set(id, job);
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
let stdoutBuffer = "";
|
|
170
|
+
child.stdout.on("data", (chunk) => {
|
|
171
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
172
|
+
const lines = stdoutBuffer.split("\n");
|
|
173
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
174
|
+
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
const trimmed = line.trim();
|
|
177
|
+
if (!trimmed.startsWith(progressPrefix)) continue;
|
|
178
|
+
const payload = trimmed.slice(progressPrefix.length);
|
|
179
|
+
try {
|
|
180
|
+
const progress = JSON.parse(payload);
|
|
181
|
+
if (progress && typeof progress === "object") {
|
|
182
|
+
const nextProgress = {
|
|
183
|
+
total_scenes: Number(progress.total_scenes ?? 0),
|
|
184
|
+
processed_scenes: Number(progress.processed_scenes ?? 0),
|
|
185
|
+
scenes_changed: Number(progress.scenes_changed ?? 0),
|
|
186
|
+
failed_scenes: Number(progress.failed_scenes ?? 0),
|
|
187
|
+
};
|
|
188
|
+
job.progress = nextProgress;
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// Ignore malformed progress lines; they are best-effort telemetry.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
167
194
|
});
|
|
168
195
|
child.stderr.on("data", () => {
|
|
169
196
|
// avoid crashing on stderr backpressure for noisy runs
|
|
@@ -187,12 +214,32 @@ function startAsyncJob({ kind, requestPayload, onComplete }) {
|
|
|
187
214
|
const payload = readJsonIfExists(resultPath);
|
|
188
215
|
const successful = payload?.ok === true;
|
|
189
216
|
const cancelledBySignal = signal === "SIGTERM" || signal === "SIGKILL";
|
|
217
|
+
const cancelledByPayload = payload?.cancelled === true;
|
|
190
218
|
|
|
191
219
|
job.finishedAt = new Date().toISOString();
|
|
192
220
|
job.result = payload;
|
|
193
221
|
|
|
222
|
+
const hasProgressFields = payload && (
|
|
223
|
+
payload.total_scenes !== undefined
|
|
224
|
+
|| payload.processed_scenes !== undefined
|
|
225
|
+
|| payload.scenes_changed !== undefined
|
|
226
|
+
|| payload.failed_scenes !== undefined
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (payload && payload.ok === true && hasProgressFields) {
|
|
230
|
+
job.progress = {
|
|
231
|
+
total_scenes: Number(payload.total_scenes ?? job.progress?.total_scenes ?? 0),
|
|
232
|
+
processed_scenes: Number(payload.processed_scenes ?? job.progress?.processed_scenes ?? 0),
|
|
233
|
+
scenes_changed: Number(payload.scenes_changed ?? job.progress?.scenes_changed ?? 0),
|
|
234
|
+
failed_scenes: Number(payload.failed_scenes ?? job.progress?.failed_scenes ?? 0),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
194
238
|
if (job.status === "cancelling") {
|
|
195
|
-
if (
|
|
239
|
+
if (cancelledByPayload) {
|
|
240
|
+
job.status = "cancelled";
|
|
241
|
+
job.error = "Async job cancelled after returning partial results.";
|
|
242
|
+
} else if (successful && !cancelledBySignal) {
|
|
196
243
|
// Race: cancellation was requested as work completed successfully.
|
|
197
244
|
job.status = "completed";
|
|
198
245
|
} else {
|
|
@@ -379,6 +426,63 @@ function resolveWorldEntityDir({ kind, projectId, universeId, name }) {
|
|
|
379
426
|
};
|
|
380
427
|
}
|
|
381
428
|
|
|
429
|
+
function resolveBatchTargetScenes(dbHandle, {
|
|
430
|
+
projectId,
|
|
431
|
+
sceneIds,
|
|
432
|
+
part,
|
|
433
|
+
chapter,
|
|
434
|
+
onlyStale,
|
|
435
|
+
}) {
|
|
436
|
+
const projectExists = Boolean(
|
|
437
|
+
dbHandle.prepare(`SELECT 1 FROM projects WHERE project_id = ? LIMIT 1`).get(projectId)
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (sceneIds?.length) {
|
|
441
|
+
const placeholders = sceneIds.map(() => "?").join(",");
|
|
442
|
+
const existingRows = dbHandle.prepare(
|
|
443
|
+
`SELECT scene_id FROM scenes WHERE project_id = ? AND scene_id IN (${placeholders})`
|
|
444
|
+
).all(projectId, ...sceneIds);
|
|
445
|
+
const existing = new Set(existingRows.map(row => row.scene_id));
|
|
446
|
+
const missing = sceneIds.filter(sceneId => !existing.has(sceneId));
|
|
447
|
+
if (missing.length > 0) {
|
|
448
|
+
return { ok: false, code: "NOT_FOUND", message: `Requested scene IDs were not found in project '${projectId}'.`, details: { missing_scene_ids: missing, project_id: projectId } };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const conditions = ["project_id = ?"];
|
|
453
|
+
const params = [projectId];
|
|
454
|
+
|
|
455
|
+
if (sceneIds?.length) {
|
|
456
|
+
const placeholders = sceneIds.map(() => "?").join(",");
|
|
457
|
+
conditions.push(`scene_id IN (${placeholders})`);
|
|
458
|
+
params.push(...sceneIds);
|
|
459
|
+
}
|
|
460
|
+
if (part !== undefined) {
|
|
461
|
+
conditions.push("part = ?");
|
|
462
|
+
params.push(part);
|
|
463
|
+
}
|
|
464
|
+
if (chapter !== undefined) {
|
|
465
|
+
conditions.push("chapter = ?");
|
|
466
|
+
params.push(chapter);
|
|
467
|
+
}
|
|
468
|
+
if (onlyStale) {
|
|
469
|
+
conditions.push("metadata_stale = 1");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const query = `
|
|
473
|
+
SELECT scene_id, project_id, file_path
|
|
474
|
+
FROM scenes
|
|
475
|
+
WHERE ${conditions.join(" AND ")}
|
|
476
|
+
ORDER BY part, chapter, timeline_position
|
|
477
|
+
`;
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
ok: true,
|
|
481
|
+
rows: dbHandle.prepare(query).all(...params),
|
|
482
|
+
project_exists: projectExists,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
382
486
|
function createCanonicalWorldEntity({ kind, name, notes, projectId, universeId, meta }) {
|
|
383
487
|
const prefix = kind === "character" ? "char" : "place";
|
|
384
488
|
const idKey = kind === "character" ? "character_id" : "place_id";
|
|
@@ -1062,9 +1166,129 @@ function createMcpServer() {
|
|
|
1062
1166
|
}
|
|
1063
1167
|
);
|
|
1064
1168
|
|
|
1169
|
+
s.tool(
|
|
1170
|
+
"enrich_scene_characters_batch",
|
|
1171
|
+
"Start an asynchronous batch job that infers scene character mentions and updates scene metadata links. Version 1 uses canonical character names only (no aliases). Defaults to dry_run=true.",
|
|
1172
|
+
{
|
|
1173
|
+
project_id: z.string().describe("Project ID (e.g. 'the-lamb' or 'universe-1/book-1-the-lamb')."),
|
|
1174
|
+
scene_ids: z.array(z.string()).optional().describe("Optional allowlist of scene IDs to process before other filters are applied."),
|
|
1175
|
+
part: z.number().int().optional().describe("Optional part number filter."),
|
|
1176
|
+
chapter: z.number().int().optional().describe("Optional chapter number filter."),
|
|
1177
|
+
only_stale: z.boolean().optional().describe("If true, only process scenes currently marked metadata_stale."),
|
|
1178
|
+
dry_run: z.boolean().optional().describe("If true (default), returns preview results without writing sidecars."),
|
|
1179
|
+
replace_mode: z.enum(["merge", "replace"]).optional().describe("merge (default): add inferred IDs; replace: overwrite characters with inferred IDs."),
|
|
1180
|
+
max_scenes: z.number().int().positive().optional().describe("Hard guardrail for resolved scene count (default: 200)."),
|
|
1181
|
+
include_match_details: z.boolean().optional().describe("If true, include extra match diagnostics per scene."),
|
|
1182
|
+
confirm_replace: z.boolean().optional().describe("Must be true when replace_mode=replace."),
|
|
1183
|
+
},
|
|
1184
|
+
async ({
|
|
1185
|
+
project_id,
|
|
1186
|
+
scene_ids,
|
|
1187
|
+
part,
|
|
1188
|
+
chapter,
|
|
1189
|
+
only_stale = false,
|
|
1190
|
+
dry_run = true,
|
|
1191
|
+
replace_mode = "merge",
|
|
1192
|
+
max_scenes = 200,
|
|
1193
|
+
include_match_details = false,
|
|
1194
|
+
confirm_replace = false,
|
|
1195
|
+
}) => {
|
|
1196
|
+
const projectIdCheck = validateProjectId(project_id);
|
|
1197
|
+
if (!projectIdCheck.ok) {
|
|
1198
|
+
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (replace_mode === "replace" && !confirm_replace) {
|
|
1202
|
+
return errorResponse(
|
|
1203
|
+
"VALIDATION_ERROR",
|
|
1204
|
+
"replace_mode=replace requires confirm_replace=true.",
|
|
1205
|
+
{ replace_mode, confirm_replace }
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (!dry_run && !SYNC_DIR_WRITABLE) {
|
|
1210
|
+
return errorResponse(
|
|
1211
|
+
"READ_ONLY",
|
|
1212
|
+
"Cannot run batch character enrichment in write mode: sync dir is read-only.",
|
|
1213
|
+
{ sync_dir: SYNC_DIR_ABS }
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const characterRows = db.prepare(`
|
|
1218
|
+
SELECT character_id, name
|
|
1219
|
+
FROM characters
|
|
1220
|
+
WHERE project_id = ? OR universe_id = (SELECT universe_id FROM projects WHERE project_id = ?)
|
|
1221
|
+
ORDER BY length(name) DESC
|
|
1222
|
+
`).all(project_id, project_id);
|
|
1223
|
+
|
|
1224
|
+
const targetResolution = resolveBatchTargetScenes(db, {
|
|
1225
|
+
projectId: project_id,
|
|
1226
|
+
sceneIds: scene_ids,
|
|
1227
|
+
part,
|
|
1228
|
+
chapter,
|
|
1229
|
+
onlyStale: Boolean(only_stale),
|
|
1230
|
+
});
|
|
1231
|
+
if (!targetResolution.ok) {
|
|
1232
|
+
return errorResponse(targetResolution.code, targetResolution.message, targetResolution.details);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const targetScenes = targetResolution.rows;
|
|
1236
|
+
const projectExists = targetResolution.project_exists !== false;
|
|
1237
|
+
if (targetScenes.length > max_scenes) {
|
|
1238
|
+
return errorResponse(
|
|
1239
|
+
"VALIDATION_ERROR",
|
|
1240
|
+
`Matched ${targetScenes.length} scenes, which exceeds max_scenes=${max_scenes}.`,
|
|
1241
|
+
{
|
|
1242
|
+
matched_scenes: targetScenes.length,
|
|
1243
|
+
max_scenes,
|
|
1244
|
+
project_id,
|
|
1245
|
+
}
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const job = startAsyncJob({
|
|
1250
|
+
kind: "enrich_scene_characters_batch",
|
|
1251
|
+
requestPayload: {
|
|
1252
|
+
kind: "enrich_scene_characters_batch",
|
|
1253
|
+
args: {
|
|
1254
|
+
project_id,
|
|
1255
|
+
dry_run: Boolean(dry_run),
|
|
1256
|
+
replace_mode,
|
|
1257
|
+
include_match_details: Boolean(include_match_details),
|
|
1258
|
+
project_exists: projectExists,
|
|
1259
|
+
target_scenes: targetScenes,
|
|
1260
|
+
character_rows: characterRows,
|
|
1261
|
+
},
|
|
1262
|
+
context: { sync_dir: SYNC_DIR },
|
|
1263
|
+
},
|
|
1264
|
+
onComplete: (completedJob) => {
|
|
1265
|
+
if (dry_run || completedJob.status !== "completed" || !completedJob.result?.ok) return;
|
|
1266
|
+
|
|
1267
|
+
syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
|
|
1268
|
+
|
|
1269
|
+
const changedScenes = (completedJob.result.results ?? [])
|
|
1270
|
+
.filter(row => row.status === "changed")
|
|
1271
|
+
.map(row => row.scene_id);
|
|
1272
|
+
|
|
1273
|
+
for (const sceneId of changedScenes) {
|
|
1274
|
+
db.prepare(`UPDATE scenes SET metadata_stale = 0 WHERE scene_id = ? AND project_id = ?`)
|
|
1275
|
+
.run(sceneId, project_id);
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
return jsonResponse({
|
|
1281
|
+
ok: true,
|
|
1282
|
+
async: true,
|
|
1283
|
+
job: toPublicJob(job, false),
|
|
1284
|
+
next_step: "Call get_async_job_status with job_id until status is 'completed', 'failed', or 'cancelled'.",
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1065
1289
|
s.tool(
|
|
1066
1290
|
"get_async_job_status",
|
|
1067
|
-
"Get status and result for an asynchronous job started by import_scrivener_sync_async or
|
|
1291
|
+
"Get status and result for an asynchronous job started by async tools such as import_scrivener_sync_async, merge_scrivener_project_beta_async, or enrich_scene_characters_batch.",
|
|
1068
1292
|
{
|
|
1069
1293
|
job_id: z.string().describe("Job ID returned by an async start tool."),
|
|
1070
1294
|
include_result: z.boolean().optional().describe("If true (default), includes completed result payload when available."),
|
|
@@ -1081,7 +1305,7 @@ function createMcpServer() {
|
|
|
1081
1305
|
|
|
1082
1306
|
s.tool(
|
|
1083
1307
|
"list_async_jobs",
|
|
1084
|
-
"List asynchronous
|
|
1308
|
+
"List asynchronous jobs currently known to this server.",
|
|
1085
1309
|
{
|
|
1086
1310
|
include_results: z.boolean().optional().describe("If true, includes completed result payloads."),
|
|
1087
1311
|
},
|
package/package.json
CHANGED
|
@@ -2,12 +2,24 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { importScrivenerSync } from "../importer.js";
|
|
4
4
|
import { mergeScrivenerProjectMetadata } from "../scrivener-direct.js";
|
|
5
|
+
import { runSceneCharacterBatch } from "../scene-character-batch.js";
|
|
6
|
+
import { ASYNC_PROGRESS_PREFIX } from "../async-progress.js";
|
|
7
|
+
|
|
8
|
+
const PROGRESS_PREFIX = ASYNC_PROGRESS_PREFIX;
|
|
5
9
|
|
|
6
10
|
function writeResult(resultPath, payload) {
|
|
7
11
|
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
8
12
|
fs.writeFileSync(resultPath, JSON.stringify(payload, null, 2), "utf8");
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
function writeProgress(payload) {
|
|
16
|
+
try {
|
|
17
|
+
process.stdout.write(`${PROGRESS_PREFIX}${JSON.stringify(payload)}\n`);
|
|
18
|
+
} catch {
|
|
19
|
+
// Best-effort only; never fail the job due to progress telemetry.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
function normalizeImportResult(importResult) {
|
|
12
24
|
const importPayload = {
|
|
13
25
|
source_dir: importResult.scrivenerDir,
|
|
@@ -68,7 +80,14 @@ function normalizeMergeResult(mergeResult) {
|
|
|
68
80
|
};
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
function
|
|
83
|
+
function normalizeSceneCharacterBatchResult(batchResult) {
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
...batchResult,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
72
91
|
const requestPath = process.argv[2];
|
|
73
92
|
const resultPath = process.argv[3];
|
|
74
93
|
|
|
@@ -107,11 +126,37 @@ function main() {
|
|
|
107
126
|
return;
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
if (request.kind === "enrich_scene_characters_batch") {
|
|
130
|
+
let cancellationRequested = false;
|
|
131
|
+
const handleSigterm = () => {
|
|
132
|
+
cancellationRequested = true;
|
|
133
|
+
};
|
|
134
|
+
process.on("SIGTERM", handleSigterm);
|
|
135
|
+
|
|
136
|
+
const result = await runSceneCharacterBatch({
|
|
137
|
+
syncDir,
|
|
138
|
+
args: {
|
|
139
|
+
project_id: request.args?.project_id,
|
|
140
|
+
dry_run: Boolean(request.args?.dry_run),
|
|
141
|
+
replace_mode: request.args?.replace_mode ?? "merge",
|
|
142
|
+
include_match_details: Boolean(request.args?.include_match_details),
|
|
143
|
+
project_exists: request.args?.project_exists !== false,
|
|
144
|
+
target_scenes: request.args?.target_scenes ?? [],
|
|
145
|
+
character_rows: request.args?.character_rows ?? [],
|
|
146
|
+
},
|
|
147
|
+
onProgress: progress => writeProgress({ kind: request.kind, ...progress }),
|
|
148
|
+
shouldCancel: () => cancellationRequested,
|
|
149
|
+
});
|
|
150
|
+
process.off("SIGTERM", handleSigterm);
|
|
151
|
+
writeResult(resultPath, normalizeSceneCharacterBatchResult(result));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
throw new Error(`Unsupported async job kind '${request.kind}'.`);
|
|
111
156
|
}
|
|
112
157
|
|
|
113
158
|
try {
|
|
114
|
-
main();
|
|
159
|
+
await main();
|
|
115
160
|
} catch (error) {
|
|
116
161
|
const resultPath = process.argv[3];
|
|
117
162
|
if (resultPath) {
|