@hanna84/mcp-writing 2.12.4 → 2.12.6

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/async-jobs.js +1 -218
  3. package/async-progress.js +1 -1
  4. package/helpers.js +2 -2
  5. package/importer.js +1 -448
  6. package/index.js +1 -501
  7. package/metadata-lint.js +1 -468
  8. package/package.json +32 -2
  9. package/runtime-diagnostics.js +1 -97
  10. package/scene-character-batch.js +1 -246
  11. package/scene-character-normalization.js +1 -199
  12. package/scripts/async-job-runner.mjs +3 -3
  13. package/scripts/generate-tool-docs.mjs +21 -3
  14. package/scripts/import.js +1 -1
  15. package/scripts/lint-metadata.mjs +1 -1
  16. package/scripts/manual-scrivener-realtest.mjs +3 -3
  17. package/scripts/merge-scrivx.js +2 -2
  18. package/scripts/new-world-entity.js +2 -2
  19. package/scripts/normalize-scene-characters.mjs +3 -3
  20. package/scripts/profile-review-bundles.mjs +1 -1
  21. package/scrivener-direct.js +1 -843
  22. package/src/index.js +502 -0
  23. package/src/runtime/async-jobs.js +218 -0
  24. package/src/runtime/async-progress.js +1 -0
  25. package/src/runtime/runtime-diagnostics.js +97 -0
  26. package/src/sync/importer.js +448 -0
  27. package/src/sync/metadata-lint.js +468 -0
  28. package/src/sync/scene-character-batch.js +246 -0
  29. package/src/sync/scene-character-normalization.js +199 -0
  30. package/src/sync/scrivener-direct.js +843 -0
  31. package/src/sync/sync.js +755 -0
  32. package/src/world/world-entity-templates.js +116 -0
  33. package/sync.js +1 -755
  34. package/tools/editing.js +1 -1
  35. package/tools/metadata.js +2 -2
  36. package/tools/review-bundles.js +1 -1
  37. package/tools/styleguide.js +1 -1
  38. package/tools/sync.js +2 -2
  39. package/world-entity-templates.js +1 -116
package/CHANGELOG.md CHANGED
@@ -4,11 +4,31 @@ 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
+ #### [v2.12.6](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v2.12.5...v2.12.6)
9
+
10
+ - refactor: move sync and world modules under src domains [`#120`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/120)
12
+
13
+ #### [v2.12.5](https://github.com/hannasdev/mcp-writing.git
14
+ /compare/v2.12.4...v2.12.5)
15
+
16
+ > 28 April 2026
17
+
18
+ - refactor: begin root-structure reorganization (phase 1 runtime) [`#119`](https://github.com/hannasdev/mcp-writing.git
19
+ /pull/119)
20
+ - Release 2.12.5 [`acfdbf8`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/acfdbf87454bd8037549cd1ba34e3f4f2d6704ca)
22
+
7
23
  #### [v2.12.4](https://github.com/hannasdev/mcp-writing.git
8
24
  /compare/v2.12.3...v2.12.4)
9
25
 
26
+ > 28 April 2026
27
+
10
28
  - fix: detect no-op commit_edit proposals [`#117`](https://github.com/hannasdev/mcp-writing.git
11
29
  /pull/117)
30
+ - Release 2.12.4 [`83a62b1`](https://github.com/hannasdev/mcp-writing.git
31
+ /commit/83a62b1e03233412eb136e501f3e8e03c51f506f)
12
32
 
13
33
  #### [v2.12.3](https://github.com/hannasdev/mcp-writing.git
14
34
  /compare/v2.12.2...v2.12.3)
package/async-jobs.js CHANGED
@@ -1,218 +1 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { spawn } from "node:child_process";
5
- import { randomUUID } from "node:crypto";
6
- import { ASYNC_PROGRESS_PREFIX } from "./async-progress.js";
7
- import { checkpointJobCreate, checkpointJobFinish, pruneJobCheckpoints } from "./db.js";
8
-
9
- export function readJsonIfExists(filePath) {
10
- if (!filePath || !fs.existsSync(filePath)) return null;
11
- try {
12
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
13
- } catch {
14
- return null;
15
- }
16
- }
17
-
18
- export function createAsyncJobManager({ db, asyncJobs, ttlMs, runnerDir }) {
19
- function pruneAsyncJobs() {
20
- const now = Date.now();
21
- let anyPruned = false;
22
- for (const [id, job] of asyncJobs.entries()) {
23
- if (!job.finishedAt) continue;
24
- if (now - Date.parse(job.finishedAt) > ttlMs) {
25
- try {
26
- if (job.tmpDir && fs.existsSync(job.tmpDir)) {
27
- fs.rmSync(job.tmpDir, { recursive: true, force: true });
28
- } else {
29
- if (job.requestPath && fs.existsSync(job.requestPath)) fs.unlinkSync(job.requestPath);
30
- if (job.resultPath && fs.existsSync(job.resultPath)) fs.unlinkSync(job.resultPath);
31
- }
32
- } catch {
33
- // best effort cleanup
34
- }
35
- asyncJobs.delete(id);
36
- anyPruned = true;
37
- }
38
- }
39
- if (anyPruned) {
40
- try { pruneJobCheckpoints(db, ttlMs); } catch { /* best effort */ }
41
- }
42
- }
43
-
44
- function toPublicJob(job, includeResult = true) {
45
- return {
46
- job_id: job.id,
47
- kind: job.kind,
48
- status: job.status,
49
- created_at: job.createdAt,
50
- started_at: job.startedAt,
51
- finished_at: job.finishedAt,
52
- pid: job.pid,
53
- error: job.error,
54
- ...(job.progress ? { progress: job.progress } : {}),
55
- ...(includeResult ? { result: job.result } : {}),
56
- };
57
- }
58
-
59
- function startAsyncJob({ kind, requestPayload, onComplete }) {
60
- pruneAsyncJobs();
61
-
62
- const id = randomUUID();
63
- const tmpPrefix = path.join(os.tmpdir(), "mcp-writing-job-");
64
- const tmpDir = fs.mkdtempSync(tmpPrefix);
65
- const requestPath = path.join(tmpDir, `${id}.request.json`);
66
- const resultPath = path.join(tmpDir, `${id}.result.json`);
67
-
68
- fs.writeFileSync(requestPath, JSON.stringify(requestPayload, null, 2), "utf8");
69
-
70
- const runnerPath = path.join(runnerDir, "scripts", "async-job-runner.mjs");
71
- const child = spawn(
72
- process.execPath,
73
- ["--experimental-sqlite", runnerPath, requestPath, resultPath],
74
- {
75
- env: process.env,
76
- stdio: ["ignore", "pipe", "pipe"],
77
- }
78
- );
79
-
80
- const job = {
81
- id,
82
- kind,
83
- status: "running",
84
- createdAt: new Date().toISOString(),
85
- startedAt: new Date().toISOString(),
86
- finishedAt: null,
87
- pid: child.pid,
88
- tmpDir,
89
- requestPath,
90
- resultPath,
91
- result: null,
92
- progress: null,
93
- error: null,
94
- onComplete,
95
- child,
96
- };
97
- asyncJobs.set(id, job);
98
- try {
99
- checkpointJobCreate(db, job);
100
- } catch (err) {
101
- process.stderr.write(`[mcp-writing] WARNING: failed to checkpoint job ${id}: ${err.message}\n`);
102
- }
103
-
104
- let stdoutBuffer = "";
105
- child.stdout.on("data", (chunk) => {
106
- stdoutBuffer += chunk.toString("utf8");
107
- const lines = stdoutBuffer.split("\n");
108
- stdoutBuffer = lines.pop() ?? "";
109
-
110
- for (const line of lines) {
111
- const trimmed = line.trim();
112
- if (!trimmed.startsWith(ASYNC_PROGRESS_PREFIX)) continue;
113
- const payload = trimmed.slice(ASYNC_PROGRESS_PREFIX.length);
114
- try {
115
- const progress = JSON.parse(payload);
116
- if (progress && typeof progress === "object") {
117
- const nextProgress = {
118
- total_scenes: Number(progress.total_scenes ?? 0),
119
- processed_scenes: Number(progress.processed_scenes ?? 0),
120
- scenes_changed: Number(progress.scenes_changed ?? 0),
121
- failed_scenes: Number(progress.failed_scenes ?? 0),
122
- };
123
- job.progress = nextProgress;
124
- }
125
- } catch {
126
- // Ignore malformed progress lines; they are best-effort telemetry.
127
- }
128
- }
129
- });
130
- child.stderr.on("data", () => {
131
- // avoid crashing on stderr backpressure for noisy runs
132
- });
133
-
134
- child.on("error", (error) => {
135
- if (job.status === "cancelling") {
136
- job.status = "cancelled";
137
- job.error = error.message;
138
- job.finishedAt = new Date().toISOString();
139
- try { checkpointJobFinish(db, job); } catch { /* best effort */ }
140
- pruneAsyncJobs();
141
- return;
142
- }
143
- job.status = "failed";
144
- job.error = error.message;
145
- job.finishedAt = new Date().toISOString();
146
- try { checkpointJobFinish(db, job); } catch { /* best effort */ }
147
- pruneAsyncJobs();
148
- });
149
-
150
- child.on("exit", (code, signal) => {
151
- const payload = readJsonIfExists(resultPath);
152
- const successful = payload?.ok === true;
153
- const cancelledBySignal = signal === "SIGTERM" || signal === "SIGKILL";
154
- const cancelledByPayload = payload?.cancelled === true;
155
-
156
- job.finishedAt = new Date().toISOString();
157
- job.result = payload;
158
-
159
- const hasProgressFields = payload && (
160
- payload.total_scenes !== undefined
161
- || payload.processed_scenes !== undefined
162
- || payload.scenes_changed !== undefined
163
- || payload.failed_scenes !== undefined
164
- );
165
-
166
- if (payload && payload.ok === true && hasProgressFields) {
167
- job.progress = {
168
- total_scenes: Number(payload.total_scenes ?? job.progress?.total_scenes ?? 0),
169
- processed_scenes: Number(payload.processed_scenes ?? job.progress?.processed_scenes ?? 0),
170
- scenes_changed: Number(payload.scenes_changed ?? job.progress?.scenes_changed ?? 0),
171
- failed_scenes: Number(payload.failed_scenes ?? job.progress?.failed_scenes ?? 0),
172
- };
173
- }
174
-
175
- if (job.status === "cancelling") {
176
- if (cancelledByPayload) {
177
- job.status = "cancelled";
178
- job.error = "Async job cancelled after returning partial results.";
179
- } else if (successful && !cancelledBySignal) {
180
- // Race: cancellation was requested as work completed successfully.
181
- job.status = "completed";
182
- } else {
183
- job.status = "cancelled";
184
- job.error = cancelledBySignal
185
- ? `Async job cancelled by signal ${signal}.`
186
- : payload?.error?.message ?? payload?.error ?? "Async job cancelled.";
187
- try { checkpointJobFinish(db, job); } catch { /* best effort */ }
188
- pruneAsyncJobs();
189
- return;
190
- }
191
- } else {
192
- job.status = successful ? "completed" : "failed";
193
- if (!successful) {
194
- job.error = payload?.error?.message
195
- ?? payload?.error
196
- ?? (signal
197
- ? `Async job exited due to signal ${signal}.`
198
- : `Async job exited with code ${code}.`);
199
- }
200
- }
201
-
202
- if (job.status === "completed" && typeof job.onComplete === "function") {
203
- try {
204
- job.onComplete(job);
205
- } catch (error) {
206
- job.status = "failed";
207
- job.error = error instanceof Error ? error.message : String(error);
208
- }
209
- }
210
- try { checkpointJobFinish(db, job); } catch { /* best effort */ }
211
- pruneAsyncJobs();
212
- });
213
-
214
- return job;
215
- }
216
-
217
- return { pruneAsyncJobs, toPublicJob, startAsyncJob };
218
- }
1
+ export * from "./src/runtime/async-jobs.js";
package/async-progress.js CHANGED
@@ -1 +1 @@
1
- export const ASYNC_PROGRESS_PREFIX = "__MCP_ASYNC_PROGRESS__ ";
1
+ export * from "./src/runtime/async-progress.js";
package/helpers.js CHANGED
@@ -2,13 +2,13 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import matter from "gray-matter";
4
4
  import yaml from "js-yaml";
5
- import { sidecarPath, syncAll } from "./sync.js";
5
+ import { sidecarPath, syncAll } from "./src/sync/sync.js";
6
6
  import {
7
7
  slugifyEntityName,
8
8
  renderCharacterSheetTemplate,
9
9
  renderPlaceSheetTemplate,
10
10
  renderCharacterArcTemplate,
11
- } from "./world-entity-templates.js";
11
+ } from "./src/world/world-entity-templates.js";
12
12
  import { ReviewBundlePlanError } from "./review-bundles.js";
13
13
 
14
14
  export function deriveLoglineFromProse(prose) {