@hanna84/mcp-writing 1.5.1 → 1.5.2

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 (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/importer.js +78 -10
  3. package/package.json +1 -1
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.5.2](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v1.5.1...v1.5.2)
9
+
10
+ - fix: avoid nested scenes/projects paths on import [`#44`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/44)
12
+
7
13
  #### [v1.5.1](https://github.com/hannasdev/mcp-writing.git
8
14
  /compare/v1.5.0...v1.5.1)
9
15
 
16
+ > 19 April 2026
17
+
10
18
  - chore: switch license from MIT to AGPL v3 [`#43`](https://github.com/hannasdev/mcp-writing.git
11
19
  /pull/43)
20
+ - Release 1.5.1 [`73b233f`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/73b233f2f210d435386f1235aa15ebc3c945542b)
12
22
 
13
23
  #### [v1.5.0](https://github.com/hannasdev/mcp-writing.git
14
24
  /compare/v1.4.8...v1.5.0)
package/importer.js CHANGED
@@ -129,6 +129,57 @@ function removeIfExists(filePath) {
129
129
  if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
130
130
  }
131
131
 
132
+ function resolveSyncRootFromPrefix(prefix, syncDirAbs) {
133
+ const parsedRoot = path.parse(syncDirAbs).root;
134
+
135
+ if (!prefix) {
136
+ return parsedRoot ? path.resolve(parsedRoot) : path.resolve(syncDirAbs);
137
+ }
138
+
139
+ // On Windows, a regex prefix like "C:" would resolve relative to cwd on drive C.
140
+ // Use the true drive root instead (e.g., "C:\\").
141
+ if (/^[a-zA-Z]:$/.test(prefix)) {
142
+ return parsedRoot || `${prefix}${path.sep}`;
143
+ }
144
+
145
+ return path.resolve(prefix);
146
+ }
147
+
148
+ function detectScopedSyncDir(syncDirAbs) {
149
+ const normalized = syncDirAbs.split(path.sep).join("/");
150
+
151
+ const universeMatch = normalized.match(/^(.*)\/universes\/([^/]+)\/([^/]+)(?:\/scenes)?$/);
152
+ if (universeMatch) {
153
+ const prefix = universeMatch[1];
154
+ const universeId = universeMatch[2];
155
+ const projectSlug = universeMatch[3];
156
+ const syncRoot = resolveSyncRootFromPrefix(prefix, syncDirAbs);
157
+ const projectRoot = path.join(syncRoot, "universes", universeId, projectSlug);
158
+ return {
159
+ projectId: `${universeId}/${projectSlug}`,
160
+ scope: "universe",
161
+ syncRoot,
162
+ projectRoot,
163
+ };
164
+ }
165
+
166
+ const projectMatch = normalized.match(/^(.*)\/projects\/([^/]+)(?:\/scenes)?$/);
167
+ if (projectMatch) {
168
+ const prefix = projectMatch[1];
169
+ const projectSlug = projectMatch[2];
170
+ const syncRoot = resolveSyncRootFromPrefix(prefix, syncDirAbs);
171
+ const projectRoot = path.join(syncRoot, "projects", projectSlug);
172
+ return {
173
+ projectId: projectSlug,
174
+ scope: "project",
175
+ syncRoot,
176
+ projectRoot,
177
+ };
178
+ }
179
+
180
+ return null;
181
+ }
182
+
132
183
  export function importScrivenerSync({
133
184
  scrivenerDir,
134
185
  mcpSyncDir,
@@ -138,31 +189,48 @@ export function importScrivenerSync({
138
189
  }) {
139
190
  const scrivenerDirAbs = path.resolve(scrivenerDir);
140
191
  const mcpSyncDirAbs = path.resolve(mcpSyncDir);
192
+ const scopedSyncDir = detectScopedSyncDir(mcpSyncDirAbs);
193
+ const fallbackProjectId = path.basename(mcpSyncDirAbs).replace(/[^a-z0-9-]/gi, "-").toLowerCase();
141
194
  const resolvedProjectId = projectId
142
195
  ? projectId
143
- : path.basename(mcpSyncDirAbs).replace(/[^a-z0-9-]/gi, "-").toLowerCase();
196
+ : scopedSyncDir?.projectId ?? fallbackProjectId;
144
197
 
145
198
  const projectIdCheck = validateProjectId(resolvedProjectId);
146
199
  if (!projectIdCheck.ok) {
147
200
  throw new Error(`Invalid project_id '${resolvedProjectId}': ${projectIdCheck.reason}`);
148
201
  }
149
202
 
203
+ if (scopedSyncDir && projectId && projectId !== scopedSyncDir.projectId) {
204
+ throw new Error(
205
+ `project_id '${projectId}' does not match WRITING_SYNC_DIR scope '${scopedSyncDir.projectId}'. `
206
+ + "Set WRITING_SYNC_DIR to the sync root or use the matching project_id."
207
+ );
208
+ }
209
+
150
210
  if (!fs.existsSync(scrivenerDirAbs)) {
151
211
  throw new Error(`Scrivener sync dir not found: ${scrivenerDirAbs}`);
152
212
  }
153
213
 
154
- // Route universe/project IDs to universes/<universe>/<project>/scenes,
155
- // matching the convention used by inferProjectAndUniverse in sync.js.
156
- const segments = resolvedProjectId.split("/");
157
214
  let scenesDir;
158
215
  let scenesBoundaryRoot;
159
- if (segments.length === 2) {
160
- const [universeId, projectSlug] = segments;
161
- scenesBoundaryRoot = path.join(mcpSyncDirAbs, "universes");
162
- scenesDir = path.resolve(scenesBoundaryRoot, universeId, projectSlug, "scenes");
216
+ if (scopedSyncDir) {
217
+ scenesBoundaryRoot = path.join(
218
+ scopedSyncDir.syncRoot,
219
+ scopedSyncDir.scope === "universe" ? "universes" : "projects"
220
+ );
221
+ scenesDir = path.join(scopedSyncDir.projectRoot, "scenes");
163
222
  } else {
164
- scenesBoundaryRoot = path.join(mcpSyncDirAbs, "projects");
165
- scenesDir = path.resolve(scenesBoundaryRoot, resolvedProjectId, "scenes");
223
+ // Route universe/project IDs to universes/<universe>/<project>/scenes,
224
+ // matching the convention used by inferProjectAndUniverse in sync.js.
225
+ const segments = resolvedProjectId.split("/");
226
+ if (segments.length === 2) {
227
+ const [universeId, projectSlug] = segments;
228
+ scenesBoundaryRoot = path.join(mcpSyncDirAbs, "universes");
229
+ scenesDir = path.resolve(scenesBoundaryRoot, universeId, projectSlug, "scenes");
230
+ } else {
231
+ scenesBoundaryRoot = path.join(mcpSyncDirAbs, "projects");
232
+ scenesDir = path.resolve(scenesBoundaryRoot, resolvedProjectId, "scenes");
233
+ }
166
234
  }
167
235
 
168
236
  const relFromBoundary = path.relative(scenesBoundaryRoot, scenesDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "type": "module",
6
6
  "main": "index.js",