@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.
- package/CHANGELOG.md +20 -0
- package/async-jobs.js +1 -218
- package/async-progress.js +1 -1
- package/helpers.js +2 -2
- package/importer.js +1 -448
- package/index.js +1 -501
- package/metadata-lint.js +1 -468
- package/package.json +32 -2
- package/runtime-diagnostics.js +1 -97
- package/scene-character-batch.js +1 -246
- package/scene-character-normalization.js +1 -199
- package/scripts/async-job-runner.mjs +3 -3
- package/scripts/generate-tool-docs.mjs +21 -3
- package/scripts/import.js +1 -1
- package/scripts/lint-metadata.mjs +1 -1
- package/scripts/manual-scrivener-realtest.mjs +3 -3
- package/scripts/merge-scrivx.js +2 -2
- package/scripts/new-world-entity.js +2 -2
- package/scripts/normalize-scene-characters.mjs +3 -3
- package/scripts/profile-review-bundles.mjs +1 -1
- package/scrivener-direct.js +1 -843
- package/src/index.js +502 -0
- package/src/runtime/async-jobs.js +218 -0
- package/src/runtime/async-progress.js +1 -0
- package/src/runtime/runtime-diagnostics.js +97 -0
- package/src/sync/importer.js +448 -0
- package/src/sync/metadata-lint.js +468 -0
- package/src/sync/scene-character-batch.js +246 -0
- package/src/sync/scene-character-normalization.js +199 -0
- package/src/sync/scrivener-direct.js +843 -0
- package/src/sync/sync.js +755 -0
- package/src/world/world-entity-templates.js +116 -0
- package/sync.js +1 -755
- package/tools/editing.js +1 -1
- package/tools/metadata.js +2 -2
- package/tools/review-bundles.js +1 -1
- package/tools/styleguide.js +1 -1
- package/tools/sync.js +2 -2
- package/world-entity-templates.js +1 -116
package/scene-character-batch.js
CHANGED
|
@@ -1,246 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import matter from "gray-matter";
|
|
3
|
-
import { buildCharacterNormalizationContext, escapeRegex, resolveCharacterReference } from "./scene-character-normalization.js";
|
|
4
|
-
import { normalizeSceneMetaForPath, readMeta, writeMeta } from "./sync.js";
|
|
5
|
-
|
|
6
|
-
function normalizeCharacterRows(rows) {
|
|
7
|
-
return buildCharacterNormalizationContext(rows);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function inferCharactersFromProse(prose, characterRows) {
|
|
11
|
-
const { clean, tokenMap } = characterRows;
|
|
12
|
-
const inferred = new Set();
|
|
13
|
-
const full_name_matches = new Set();
|
|
14
|
-
const ambiguous_tokens = [];
|
|
15
|
-
|
|
16
|
-
for (const row of clean) {
|
|
17
|
-
if (row.full_name_regex?.test(prose)) {
|
|
18
|
-
inferred.add(row.character_id);
|
|
19
|
-
full_name_matches.add(row.character_id);
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Precision-first v1 policy: multi-token names require a full phrase match.
|
|
24
|
-
if (row.phrase_tokens.length !== 1) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
for (const token of row.informative_tokens) {
|
|
29
|
-
const tokenRegex = new RegExp(`\\b${escapeRegex(token)}\\b`, "i");
|
|
30
|
-
if (!tokenRegex.test(prose)) continue;
|
|
31
|
-
|
|
32
|
-
const tokenIds = tokenMap.get(token) ?? [];
|
|
33
|
-
if (tokenIds.length === 1) {
|
|
34
|
-
inferred.add(row.character_id);
|
|
35
|
-
} else if (!ambiguous_tokens.includes(token)) {
|
|
36
|
-
ambiguous_tokens.push(token);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
for (const [token, tokenIds] of tokenMap.entries()) {
|
|
42
|
-
if (tokenIds.length < 2) continue;
|
|
43
|
-
const tokenRegex = new RegExp(`\\b${escapeRegex(token)}\\b`, "i");
|
|
44
|
-
if (tokenRegex.test(prose) && !ambiguous_tokens.includes(token)) {
|
|
45
|
-
ambiguous_tokens.push(token);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
inferred_characters: [...inferred],
|
|
51
|
-
full_name_matches: [...full_name_matches],
|
|
52
|
-
ambiguous_tokens,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function resolveCharacterEntry(entry, characterRows) {
|
|
57
|
-
return resolveCharacterReference(entry, characterRows);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function pruneLessSpecificCharacters(characterIds, fullNameMatches, characterRows) {
|
|
61
|
-
const kept = new Set(characterIds);
|
|
62
|
-
|
|
63
|
-
for (const candidateId of [...kept]) {
|
|
64
|
-
const candidate = characterRows.byId.get(candidateId);
|
|
65
|
-
if (!candidate || candidate.informative_tokens.length < 2) continue;
|
|
66
|
-
|
|
67
|
-
for (const dominantId of fullNameMatches) {
|
|
68
|
-
if (candidateId === dominantId) continue;
|
|
69
|
-
const dominant = characterRows.byId.get(dominantId);
|
|
70
|
-
if (!dominant) continue;
|
|
71
|
-
if (candidate.informative_tokens.length >= dominant.informative_tokens.length) continue;
|
|
72
|
-
|
|
73
|
-
if (candidate.informative_tokens.every(token => dominant.informative_tokens.includes(token))) {
|
|
74
|
-
kept.delete(candidateId);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return [...kept];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function nextTurn() {
|
|
84
|
-
return new Promise(resolve => setImmediate(resolve));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getInterSceneDelayMs() {
|
|
88
|
-
const raw = Number(process.env.MCP_WRITING_SCENE_CHARACTER_BATCH_DELAY_MS ?? 0);
|
|
89
|
-
return Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function delay(ms) {
|
|
93
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function runSceneCharacterBatch({ syncDir, args, onProgress, shouldCancel }) {
|
|
97
|
-
const {
|
|
98
|
-
project_id,
|
|
99
|
-
dry_run = true,
|
|
100
|
-
replace_mode = "merge",
|
|
101
|
-
include_match_details = false,
|
|
102
|
-
project_exists = true,
|
|
103
|
-
target_scenes = [],
|
|
104
|
-
character_rows = [],
|
|
105
|
-
} = args;
|
|
106
|
-
|
|
107
|
-
const targetScenes = Array.isArray(target_scenes) ? target_scenes : [];
|
|
108
|
-
const characterRows = Array.isArray(character_rows) ? character_rows : [];
|
|
109
|
-
const normalizedCharacterRows = normalizeCharacterRows(characterRows);
|
|
110
|
-
|
|
111
|
-
const results = [];
|
|
112
|
-
let processed_scenes = 0;
|
|
113
|
-
let scenes_changed = 0;
|
|
114
|
-
let failed_scenes = 0;
|
|
115
|
-
let links_added = 0;
|
|
116
|
-
let links_removed = 0;
|
|
117
|
-
const interSceneDelayMs = getInterSceneDelayMs();
|
|
118
|
-
|
|
119
|
-
const emitProgress = () => {
|
|
120
|
-
if (typeof onProgress !== "function") return;
|
|
121
|
-
onProgress({
|
|
122
|
-
total_scenes: targetScenes.length,
|
|
123
|
-
processed_scenes,
|
|
124
|
-
scenes_changed,
|
|
125
|
-
failed_scenes,
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
emitProgress();
|
|
130
|
-
|
|
131
|
-
for (const scene of targetScenes) {
|
|
132
|
-
await nextTurn();
|
|
133
|
-
|
|
134
|
-
if (typeof shouldCancel === "function" && shouldCancel()) {
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const raw = fs.readFileSync(scene.file_path, "utf8");
|
|
140
|
-
const { content: prose } = matter(raw);
|
|
141
|
-
const { meta } = readMeta(scene.file_path, syncDir, { writable: !dry_run });
|
|
142
|
-
|
|
143
|
-
const before_characters = [...new Set((meta.characters ?? []).map(String).filter(Boolean))];
|
|
144
|
-
const normalized_before_characters = [...new Set(
|
|
145
|
-
before_characters
|
|
146
|
-
.map(character => resolveCharacterEntry(character, normalizedCharacterRows))
|
|
147
|
-
.filter(Boolean)
|
|
148
|
-
)];
|
|
149
|
-
const inference = inferCharactersFromProse(prose, normalizedCharacterRows);
|
|
150
|
-
const inferred_characters = inference.inferred_characters;
|
|
151
|
-
|
|
152
|
-
const afterSet = new Set(normalized_before_characters);
|
|
153
|
-
if (replace_mode === "replace") {
|
|
154
|
-
afterSet.clear();
|
|
155
|
-
}
|
|
156
|
-
for (const characterId of inferred_characters) {
|
|
157
|
-
afterSet.add(characterId);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const after_characters = pruneLessSpecificCharacters(
|
|
161
|
-
[...afterSet],
|
|
162
|
-
inference.full_name_matches,
|
|
163
|
-
normalizedCharacterRows
|
|
164
|
-
);
|
|
165
|
-
const beforeSet = new Set(before_characters);
|
|
166
|
-
const added = after_characters.filter(id => !beforeSet.has(id));
|
|
167
|
-
const afterSetLookup = new Set(after_characters);
|
|
168
|
-
const removed = before_characters.filter(id => !afterSetLookup.has(id));
|
|
169
|
-
const changed = added.length > 0 || removed.length > 0;
|
|
170
|
-
|
|
171
|
-
if (!dry_run && changed) {
|
|
172
|
-
const updatedMeta = normalizeSceneMetaForPath(syncDir, scene.file_path, {
|
|
173
|
-
...meta,
|
|
174
|
-
characters: after_characters,
|
|
175
|
-
}).meta;
|
|
176
|
-
|
|
177
|
-
writeMeta(scene.file_path, updatedMeta);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
scenes_changed += changed ? 1 : 0;
|
|
181
|
-
links_added += added.length;
|
|
182
|
-
links_removed += removed.length;
|
|
183
|
-
|
|
184
|
-
const hasInferredMatches = inferred_characters.length > 0;
|
|
185
|
-
const sceneStatus = changed
|
|
186
|
-
? "changed"
|
|
187
|
-
: (!hasInferredMatches && inference.ambiguous_tokens.length > 0 ? "skipped_ambiguous" : "unchanged");
|
|
188
|
-
|
|
189
|
-
results.push({
|
|
190
|
-
scene_id: scene.scene_id,
|
|
191
|
-
file_path: scene.file_path,
|
|
192
|
-
before_characters,
|
|
193
|
-
inferred_characters,
|
|
194
|
-
after_characters,
|
|
195
|
-
added,
|
|
196
|
-
removed,
|
|
197
|
-
changed,
|
|
198
|
-
status: sceneStatus,
|
|
199
|
-
...(include_match_details ? { match_details: { ambiguous_tokens: inference.ambiguous_tokens } } : {}),
|
|
200
|
-
});
|
|
201
|
-
} catch (error) {
|
|
202
|
-
failed_scenes += 1;
|
|
203
|
-
results.push({
|
|
204
|
-
scene_id: scene.scene_id,
|
|
205
|
-
file_path: scene.file_path,
|
|
206
|
-
before_characters: [],
|
|
207
|
-
inferred_characters: [],
|
|
208
|
-
after_characters: [],
|
|
209
|
-
added: [],
|
|
210
|
-
removed: [],
|
|
211
|
-
changed: false,
|
|
212
|
-
status: "failed",
|
|
213
|
-
error: error instanceof Error ? error.message : String(error),
|
|
214
|
-
});
|
|
215
|
-
} finally {
|
|
216
|
-
processed_scenes += 1;
|
|
217
|
-
emitProgress();
|
|
218
|
-
if (interSceneDelayMs > 0) {
|
|
219
|
-
await delay(interSceneDelayMs);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const warnings = [];
|
|
225
|
-
if (failed_scenes > 0) {
|
|
226
|
-
warnings.push("PARTIAL_SUCCESS: one or more scenes failed to process.");
|
|
227
|
-
}
|
|
228
|
-
if (!project_exists && targetScenes.length === 0) {
|
|
229
|
-
warnings.push(`PROJECT_NOT_FOUND_WARNING: project '${project_id}' was not found; nothing to process.`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
ok: true,
|
|
234
|
-
cancelled: Boolean(typeof shouldCancel === "function" && shouldCancel() && processed_scenes < targetScenes.length),
|
|
235
|
-
project_id,
|
|
236
|
-
dry_run: Boolean(dry_run),
|
|
237
|
-
total_scenes: targetScenes.length,
|
|
238
|
-
processed_scenes,
|
|
239
|
-
scenes_changed,
|
|
240
|
-
failed_scenes,
|
|
241
|
-
links_added,
|
|
242
|
-
links_removed,
|
|
243
|
-
results,
|
|
244
|
-
...(warnings.length > 0 ? { warning: warnings.join(" ") } : {}),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
1
|
+
export * from "./src/sync/scene-character-batch.js";
|
|
@@ -1,199 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
"the",
|
|
3
|
-
"and",
|
|
4
|
-
"for",
|
|
5
|
-
"with",
|
|
6
|
-
"from",
|
|
7
|
-
"into",
|
|
8
|
-
"onto",
|
|
9
|
-
"over",
|
|
10
|
-
"under",
|
|
11
|
-
"after",
|
|
12
|
-
"before",
|
|
13
|
-
"about",
|
|
14
|
-
"around",
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
export function escapeRegex(text) {
|
|
18
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isDistinctiveToken(token) {
|
|
22
|
-
const normalized = String(token ?? "").trim().toLowerCase();
|
|
23
|
-
return Boolean(normalized) && normalized.length >= 3 && !NON_DISTINCTIVE_TOKENS.has(normalized);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalizeRawCharacterValues(values) {
|
|
27
|
-
const raw = Array.isArray(values) ? values : [];
|
|
28
|
-
const seen = new Set();
|
|
29
|
-
const normalized = [];
|
|
30
|
-
|
|
31
|
-
for (const value of raw) {
|
|
32
|
-
const text = String(value ?? "").trim();
|
|
33
|
-
if (!text || seen.has(text)) continue;
|
|
34
|
-
seen.add(text);
|
|
35
|
-
normalized.push(text);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return normalized;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function tokenizeValue(value) {
|
|
42
|
-
return [...new Set(String(value ?? "").toLowerCase().split(/\s+/).filter(isDistinctiveToken))];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function buildCharacterNormalizationContext(rows) {
|
|
46
|
-
const clean = (Array.isArray(rows) ? rows : [])
|
|
47
|
-
.filter(row => row?.character_id && row?.name)
|
|
48
|
-
.map(row => {
|
|
49
|
-
const character_id = String(row.character_id).trim();
|
|
50
|
-
const name = String(row.name).trim();
|
|
51
|
-
const phrase_tokens = name.toLowerCase().split(/\s+/).filter(Boolean);
|
|
52
|
-
const tokens = [...new Set(phrase_tokens)];
|
|
53
|
-
return {
|
|
54
|
-
character_id,
|
|
55
|
-
name,
|
|
56
|
-
phrase_tokens,
|
|
57
|
-
tokens,
|
|
58
|
-
informative_tokens: tokens.filter(isDistinctiveToken),
|
|
59
|
-
full_name_regex: phrase_tokens.length > 1
|
|
60
|
-
? new RegExp(`\\b${phrase_tokens.map(escapeRegex).join("\\s+")}\\b`, "i")
|
|
61
|
-
: null,
|
|
62
|
-
};
|
|
63
|
-
})
|
|
64
|
-
.filter(row => row.character_id.length > 0 && row.name.length > 0);
|
|
65
|
-
|
|
66
|
-
const byId = new Map();
|
|
67
|
-
const nameMap = new Map();
|
|
68
|
-
const tokenMap = new Map();
|
|
69
|
-
for (const row of clean) {
|
|
70
|
-
byId.set(row.character_id, row);
|
|
71
|
-
const normalizedName = row.name.toLowerCase();
|
|
72
|
-
const ids = nameMap.get(normalizedName) ?? [];
|
|
73
|
-
ids.push(row.character_id);
|
|
74
|
-
nameMap.set(normalizedName, ids);
|
|
75
|
-
|
|
76
|
-
for (const token of row.informative_tokens) {
|
|
77
|
-
const tokenIds = tokenMap.get(token) ?? [];
|
|
78
|
-
tokenIds.push(row.character_id);
|
|
79
|
-
tokenMap.set(token, tokenIds);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { clean, byId, nameMap, tokenMap };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function resolveCharacterReference(value, context) {
|
|
87
|
-
const text = String(value ?? "").trim();
|
|
88
|
-
if (!text) return null;
|
|
89
|
-
|
|
90
|
-
if (context.byId.has(text)) {
|
|
91
|
-
return text;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const exactNameIds = context.nameMap.get(text.toLowerCase());
|
|
95
|
-
if (exactNameIds?.length === 1) {
|
|
96
|
-
return exactNameIds[0];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const words = text.toLowerCase().split(/\s+/).filter(isDistinctiveToken);
|
|
100
|
-
if (words.length === 0) {
|
|
101
|
-
return text;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const matches = context.clean.filter(row =>
|
|
105
|
-
words.every(word => row.informative_tokens.includes(word))
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
if (matches.length === 1) {
|
|
109
|
-
return matches[0].character_id;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return text;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isProperSubset(subsetTokens, supersetTokens) {
|
|
116
|
-
if (subsetTokens.length < 2 || subsetTokens.length >= supersetTokens.length) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
return subsetTokens.every(token => supersetTokens.includes(token));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function hasMoreSpecificNonCanonicalSource(candidate, sourceInfo) {
|
|
123
|
-
if (!sourceInfo || sourceInfo.hadCanonicalSource || sourceInfo.nonCanonicalTokens.length === 0) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return sourceInfo.nonCanonicalTokens.some(tokens => isProperSubset(candidate.informative_tokens, tokens));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function pruneLessSpecificCanonicalIds(values, context, sourceMap) {
|
|
131
|
-
return values.filter((value, idx) => {
|
|
132
|
-
const row = context.byId.get(value);
|
|
133
|
-
if (!row || row.informative_tokens.length === 0) {
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const rowSource = sourceMap.get(value);
|
|
138
|
-
if (!rowSource?.hadCanonicalSource) {
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
for (let i = 0; i < values.length; i++) {
|
|
143
|
-
if (i === idx) continue;
|
|
144
|
-
|
|
145
|
-
const otherId = values[i];
|
|
146
|
-
const other = context.byId.get(otherId);
|
|
147
|
-
if (!other || other.informative_tokens.length === 0) continue;
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
isProperSubset(row.informative_tokens, other.informative_tokens)
|
|
151
|
-
&& hasMoreSpecificNonCanonicalSource(row, sourceMap.get(otherId))
|
|
152
|
-
) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return true;
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function normalizeSceneCharacters(values, context) {
|
|
162
|
-
const before = normalizeRawCharacterValues(values);
|
|
163
|
-
const resolved = [];
|
|
164
|
-
const seen = new Set();
|
|
165
|
-
const sourceMap = new Map();
|
|
166
|
-
|
|
167
|
-
for (const value of before) {
|
|
168
|
-
const normalized = resolveCharacterReference(value, context);
|
|
169
|
-
if (!normalized) continue;
|
|
170
|
-
|
|
171
|
-
const source = sourceMap.get(normalized) ?? {
|
|
172
|
-
hadCanonicalSource: false,
|
|
173
|
-
nonCanonicalTokens: [],
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
if (context.byId.has(value)) {
|
|
177
|
-
source.hadCanonicalSource = true;
|
|
178
|
-
} else {
|
|
179
|
-
source.nonCanonicalTokens.push(tokenizeValue(value));
|
|
180
|
-
}
|
|
181
|
-
sourceMap.set(normalized, source);
|
|
182
|
-
|
|
183
|
-
if (seen.has(normalized)) continue;
|
|
184
|
-
seen.add(normalized);
|
|
185
|
-
resolved.push(normalized);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const after = pruneLessSpecificCanonicalIds(resolved, context, sourceMap);
|
|
189
|
-
const beforeSet = new Set(before);
|
|
190
|
-
const afterSet = new Set(after);
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
before,
|
|
194
|
-
after,
|
|
195
|
-
changed: before.length !== after.length || before.some((value, idx) => after[idx] !== value),
|
|
196
|
-
added: after.filter(value => !beforeSet.has(value)),
|
|
197
|
-
removed: before.filter(value => !afterSet.has(value)),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
1
|
+
export * from "./src/sync/scene-character-normalization.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { importScrivenerSync } from "../importer.js";
|
|
4
|
-
import { mergeScrivenerProjectMetadata } from "../scrivener-direct.js";
|
|
5
|
-
import { runSceneCharacterBatch } from "../scene-character-batch.js";
|
|
3
|
+
import { importScrivenerSync } from "../src/sync/importer.js";
|
|
4
|
+
import { mergeScrivenerProjectMetadata } from "../src/sync/scrivener-direct.js";
|
|
5
|
+
import { runSceneCharacterBatch } from "../src/sync/scene-character-batch.js";
|
|
6
6
|
import { ASYNC_PROGRESS_PREFIX } from "../async-progress.js";
|
|
7
7
|
|
|
8
8
|
const PROGRESS_PREFIX = ASYNC_PROGRESS_PREFIX;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Generates docs/tools.md from tool definitions in
|
|
3
|
+
* Generates docs/tools.md from tool definitions in the runtime entrypoint
|
|
4
|
+
* (src/index.js when present, otherwise index.js) and tools/*.js.
|
|
4
5
|
*
|
|
5
6
|
* Run: node scripts/generate-tool-docs.mjs
|
|
6
7
|
* or: npm run docs
|
|
@@ -29,7 +30,17 @@ try {
|
|
|
29
30
|
|
|
30
31
|
// Inline each register*Tools(s, ...) call with the module source so that
|
|
31
32
|
// s.tool() blocks appear in registration order (matching createMcpServer()).
|
|
32
|
-
|
|
33
|
+
const ENTRYPOINT_PATH = path.join(ROOT, 'src', 'index.js');
|
|
34
|
+
const LEGACY_ENTRYPOINT_PATH = path.join(ROOT, 'index.js');
|
|
35
|
+
const sourcePath = (() => {
|
|
36
|
+
try {
|
|
37
|
+
readFileSync(ENTRYPOINT_PATH, 'utf8');
|
|
38
|
+
return ENTRYPOINT_PATH;
|
|
39
|
+
} catch {
|
|
40
|
+
return LEGACY_ENTRYPOINT_PATH;
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
let source = readFileSync(sourcePath, 'utf8');
|
|
33
44
|
for (const [fnName, content] of toolModuleMap) {
|
|
34
45
|
source = source.replace(new RegExp(`[ \\t]*${fnName}\\s*\\([\\s\\S]*?\\);`), content);
|
|
35
46
|
}
|
|
@@ -129,6 +140,12 @@ function extractConstantValues(src) {
|
|
|
129
140
|
values.set(match[1], Number.parseInt(match[2], 10));
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
for (const match of src.matchAll(/const\s+(\w+)\s*=\s*parsePositiveIntEnv\(\s*process\.env\.\w+\s*,\s*(\d+)\s*\);/g)) {
|
|
144
|
+
if (!values.has(match[1])) {
|
|
145
|
+
values.set(match[1], Number.parseInt(match[2], 10));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
for (const match of src.matchAll(/const\s+(\w+)\s*=\s*process\.env\.\w+\s*\?\?\s*(["'`])([\s\S]*?)\2;/g)) {
|
|
133
150
|
if (!values.has(match[1])) {
|
|
134
151
|
values.set(match[1], match[3]);
|
|
@@ -416,10 +433,11 @@ function anchor(name) {
|
|
|
416
433
|
}
|
|
417
434
|
|
|
418
435
|
function generateMarkdown(tools) {
|
|
436
|
+
const sourceLabel = path.relative(ROOT, sourcePath).replace(/\\/g, '/');
|
|
419
437
|
const lines = [
|
|
420
438
|
'# Tool Reference',
|
|
421
439
|
'',
|
|
422
|
-
|
|
440
|
+
`> Auto-generated from \`${sourceLabel}\`.`,
|
|
423
441
|
'> Do not edit manually — run `npm run docs` to regenerate.',
|
|
424
442
|
'',
|
|
425
443
|
'## Tools',
|
package/scripts/import.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import { importScrivenerSync, validateProjectId } from "../importer.js";
|
|
10
|
+
import { importScrivenerSync, validateProjectId } from "../src/sync/importer.js";
|
|
11
11
|
|
|
12
12
|
function printUsage() {
|
|
13
13
|
console.log("Usage: node scripts/import.js <scrivener-sync-dir> <mcp-sync-dir> [--project <id>] [--dry-run]");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { lintMetadataInSyncDir } from "../metadata-lint.js";
|
|
3
|
+
import { lintMetadataInSyncDir } from "../src/sync/metadata-lint.js";
|
|
4
4
|
|
|
5
5
|
function parseArgs(argv) {
|
|
6
6
|
const args = { syncDir: process.env.WRITING_SYNC_DIR ?? "./sync" };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import { importScrivenerSync, validateProjectId } from "../importer.js";
|
|
5
|
-
import { mergeScrivenerProjectMetadata } from "../scrivener-direct.js";
|
|
4
|
+
import { importScrivenerSync, validateProjectId } from "../src/sync/importer.js";
|
|
5
|
+
import { mergeScrivenerProjectMetadata } from "../src/sync/scrivener-direct.js";
|
|
6
6
|
|
|
7
7
|
function usage() {
|
|
8
8
|
return [
|
|
@@ -259,4 +259,4 @@ try {
|
|
|
259
259
|
console.error(err instanceof Error ? err.message : String(err));
|
|
260
260
|
console.error(usage());
|
|
261
261
|
process.exit(1);
|
|
262
|
-
}
|
|
262
|
+
}
|
package/scripts/merge-scrivx.js
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import path from "node:path";
|
|
30
|
-
import { validateProjectId } from "../importer.js";
|
|
31
|
-
import { mergeScrivenerProjectMetadata } from "../scrivener-direct.js";
|
|
30
|
+
import { validateProjectId } from "../src/sync/importer.js";
|
|
31
|
+
import { mergeScrivenerProjectMetadata } from "../src/sync/scrivener-direct.js";
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
// Args
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
renderCharacterSheetTemplate,
|
|
7
7
|
renderPlaceSheetTemplate,
|
|
8
8
|
slugifyEntityName,
|
|
9
|
-
} from "../world-entity-templates.js";
|
|
9
|
+
} from "../src/world/world-entity-templates.js";
|
|
10
10
|
|
|
11
11
|
function parseArgs(argv) {
|
|
12
12
|
const args = argv.slice(2);
|
|
@@ -157,4 +157,4 @@ function main() {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
main();
|
|
160
|
+
main();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { openDb } from "../db.js";
|
|
4
|
-
import { buildCharacterNormalizationContext, normalizeSceneCharacters } from "../scene-character-normalization.js";
|
|
5
|
-
import { normalizeSceneMetaForPath, readMeta, syncAll, writeMeta } from "../sync.js";
|
|
4
|
+
import { buildCharacterNormalizationContext, normalizeSceneCharacters } from "../src/sync/scene-character-normalization.js";
|
|
5
|
+
import { normalizeSceneMetaForPath, readMeta, syncAll, writeMeta } from "../src/sync/sync.js";
|
|
6
6
|
|
|
7
7
|
function readRequiredValue(argv, index, option) {
|
|
8
8
|
const value = argv[index + 1];
|
|
@@ -222,4 +222,4 @@ function main() {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
main();
|
|
225
|
+
main();
|
|
@@ -9,7 +9,7 @@ import fs from "node:fs";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import { openDb } from "../db.js";
|
|
12
|
-
import { syncAll } from "../sync.js";
|
|
12
|
+
import { syncAll } from "../src/sync/sync.js";
|
|
13
13
|
import { isGitRepository, getHeadCommitHash } from "../git.js";
|
|
14
14
|
import {
|
|
15
15
|
buildReviewBundlePlan,
|