@hanna84/mcp-writing 1.1.1 → 1.3.7
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 +68 -0
- package/README.md +248 -12
- package/git.js +190 -0
- package/index.js +560 -9
- package/metadata-lint.js +113 -2
- package/package.json +7 -2
- package/scripts/import.js +7 -141
- package/scripts/new-world-entity.js +160 -0
- package/sync.js +41 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.7](https://github.com/hannasdev/mcp-writing/compare/v1.3.6...v1.3.7) (2026-04-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **ci:** remove npm whoami diagnostic step ([65435e9](https://github.com/hannasdev/mcp-writing/commit/65435e9c8cfa829ceb6937904250c46afc45b54c))
|
|
9
|
+
|
|
10
|
+
## [1.3.6](https://github.com/hannasdev/mcp-writing/compare/v1.3.5...v1.3.6) (2026-04-18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **ci:** restore npm trusted publishing auth config ([f043368](https://github.com/hannasdev/mcp-writing/commit/f0433681e984e34d1c38725f2d85dc3d14031626))
|
|
16
|
+
|
|
17
|
+
## [1.3.5](https://github.com/hannasdev/mcp-writing/compare/v1.3.4...v1.3.5) (2026-04-18)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **ci:** force OIDC-only npm publish auth ([c9f1216](https://github.com/hannasdev/mcp-writing/commit/c9f1216f2d3aa49fcc2aad5e6c4032962ad843d1))
|
|
23
|
+
|
|
24
|
+
## [1.3.4](https://github.com/hannasdev/mcp-writing/compare/v1.3.3...v1.3.4) (2026-04-18)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **ci:** use tag trigger for npm publish (matches n8n pattern) ([#18](https://github.com/hannasdev/mcp-writing/issues/18)) ([a3c5992](https://github.com/hannasdev/mcp-writing/commit/a3c5992e8cb560aa79b554389d41457fcf3c0cfd))
|
|
30
|
+
|
|
31
|
+
## [1.3.3](https://github.com/hannasdev/mcp-writing/compare/v1.3.2...v1.3.3) (2026-04-18)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
* **ci:** use tag trigger for npm publish (matches n8n pattern) ([6841de7](https://github.com/hannasdev/mcp-writing/commit/6841de78a90c3b4c121761d28a09b02f9d360a96))
|
|
37
|
+
|
|
38
|
+
## [1.3.2](https://github.com/hannasdev/mcp-writing/compare/v1.3.1...v1.3.2) (2026-04-18)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Bug Fixes
|
|
42
|
+
|
|
43
|
+
* **ci:** publish from main release commits for npm trusted publishing ([135ab25](https://github.com/hannasdev/mcp-writing/commit/135ab254fb91ee6427a79cdecdc5b9bf55cfb4d1))
|
|
44
|
+
|
|
45
|
+
## [1.3.1](https://github.com/hannasdev/mcp-writing/compare/v1.3.0...v1.3.1) (2026-04-18)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Bug Fixes
|
|
49
|
+
|
|
50
|
+
* **ci:** set npm environment for trusted publishing ([b75fb08](https://github.com/hannasdev/mcp-writing/commit/b75fb0897dbbbd9df2d136cbb78a06218f0c0b50))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Miscellaneous Chores
|
|
54
|
+
|
|
55
|
+
* generate integration fixtures at runtime and remove test-sync ([#14](https://github.com/hannasdev/mcp-writing/issues/14)) ([0ff5024](https://github.com/hannasdev/mcp-writing/commit/0ff5024f0c3724b099035a6f76fbe4c517870652))
|
|
56
|
+
|
|
57
|
+
## [1.3.0](https://github.com/hannasdev/mcp-writing/compare/v1.2.0...v1.3.0) (2026-04-17)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Features
|
|
61
|
+
|
|
62
|
+
* formalize non-draft content and stable Scrivener imports ([#12](https://github.com/hannasdev/mcp-writing/issues/12)) ([70b4beb](https://github.com/hannasdev/mcp-writing/commit/70b4bebd0d3cd823ffe3a9251f59f9a0c0e5f12f))
|
|
63
|
+
|
|
64
|
+
## [1.2.0](https://github.com/hannasdev/mcp-writing/compare/v1.1.1...v1.2.0) (2026-04-17)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Features
|
|
68
|
+
|
|
69
|
+
* Phase 3 git-backed prose editing, runtime config tool, startup path logging ([f30a7c8](https://github.com/hannasdev/mcp-writing/commit/f30a7c8b5e5aaf070b0b6cfaec38479141c5f60b))
|
|
70
|
+
|
|
3
71
|
## [1.1.1](https://github.com/hannasdev/mcp-writing/compare/v1.1.0...v1.1.1) (2026-04-16)
|
|
4
72
|
|
|
5
73
|
|
package/README.md
CHANGED
|
@@ -9,16 +9,36 @@ Designed to work with [OpenClaw](https://github.com/openclaw/openclaw) but compa
|
|
|
9
9
|
Instead of feeding an entire manuscript to an AI and hoping it fits in the context window, `mcp-writing` builds a structured index from your scene files. The AI queries that index first — finding relevant characters, beats, and loglines — then loads only the specific prose it needs.
|
|
10
10
|
|
|
11
11
|
**Phase 1:** Read-only analysis. Ask questions about your project.
|
|
12
|
-
**Phase 2
|
|
13
|
-
**Phase 3:** AI-assisted prose editing with confirmation and version history.
|
|
12
|
+
**Phase 2:** Metadata write-back. Answers stay accurate as the manuscript evolves.
|
|
13
|
+
**Phase 3 (current):** AI-assisted prose editing with confirmation and version history.
|
|
14
|
+
|
|
15
|
+
## Who it is for
|
|
16
|
+
|
|
17
|
+
- Novelists and writing teams working on long manuscripts with many scenes, characters, and continuity constraints.
|
|
18
|
+
- AI-assisted editing workflows where you want targeted context retrieval instead of full-manuscript prompting.
|
|
19
|
+
- Projects that need traceable, reversible edits with metadata that stays synchronized as drafts evolve.
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- **Node.js 22.6.0 or later** (required for SQLite support via `--experimental-sqlite` flag)
|
|
24
|
+
- **npm 8.0.0 or later**
|
|
25
|
+
- **Git** (for edit snapshots and version history)
|
|
26
|
+
|
|
27
|
+
Verify your setup:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
node --version # should be v22.6.0 or later
|
|
31
|
+
npm --version # should be 8.0.0 or later
|
|
32
|
+
git --version # should be installed
|
|
33
|
+
```
|
|
14
34
|
|
|
15
35
|
## Quick start with Scrivener
|
|
16
36
|
|
|
17
|
-
If you write in [Scrivener](https://www.literatureandlatte.com/scrivener), you can seed `mcp-writing` from a Scrivener external-sync export
|
|
37
|
+
If you write in [Scrivener](https://www.literatureandlatte.com/scrivener), you can seed `mcp-writing` from a Scrivener external-sync export for scene prose, then curate non-draft content directly into the target folder structure.
|
|
18
38
|
|
|
19
39
|
### 1. Export from Scrivener
|
|
20
40
|
|
|
21
|
-
In Scrivener: **File → Sync → With External Folder**. Set the format to **plain text** (`.txt`) and pick an output folder, for example `~/my-novel-txt/`.
|
|
41
|
+
In Scrivener: **File → Sync → With External Folder**. Set the format to **plain text** (`.txt`) and pick an output folder, for example `~/my-novel-txt/`. `mcp-writing` imports the `Draft/` folder automatically.
|
|
22
42
|
|
|
23
43
|
### 2. Import into mcp-writing
|
|
24
44
|
|
|
@@ -27,11 +47,11 @@ node scripts/import.js ~/my-novel-txt /path/to/sync-dir --project my-novel
|
|
|
27
47
|
```
|
|
28
48
|
|
|
29
49
|
The importer:
|
|
50
|
+
|
|
30
51
|
- Converts `Draft/` files to scene sidecars (`.meta.yaml`) with auto-generated `scene_id`, `title`, `part`, `chapter`, and `save_the_cat_beat` fields derived from the filename/structure.
|
|
31
|
-
- Routes `Notes/` files into `world/characters/` or `world/places/` based on section grouping.
|
|
32
52
|
- Skips beat-marker files (`-Setup-`, `-Catalyst-`, etc.), chapter-intro files, epigraphs, and trashed files.
|
|
33
53
|
|
|
34
|
-
|
|
54
|
+
Non-draft content is not inferred from `Notes/`. Put it directly into the target sync dir using the `world/` folder conventions described below.
|
|
35
55
|
|
|
36
56
|
### 3. Start the server
|
|
37
57
|
|
|
@@ -39,6 +59,14 @@ The importer:
|
|
|
39
59
|
WRITING_SYNC_DIR=/path/to/sync-dir DB_PATH=./writing.db npm start
|
|
40
60
|
```
|
|
41
61
|
|
|
62
|
+
You should see:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Listening on port 3000
|
|
66
|
+
Sync dir: /path/to/sync-dir
|
|
67
|
+
Database: ./writing.db
|
|
68
|
+
```
|
|
69
|
+
|
|
42
70
|
Then call the `sync` tool once to index everything.
|
|
43
71
|
|
|
44
72
|
### 4. Lint your metadata (optional)
|
|
@@ -80,13 +108,15 @@ Alternatively, metadata can live in a sidecar file named `<scene-file>.meta.yaml
|
|
|
80
108
|
|
|
81
109
|
### Project structure
|
|
82
110
|
|
|
83
|
-
```
|
|
111
|
+
```bash
|
|
84
112
|
/sync-root/
|
|
85
113
|
/universes/
|
|
86
114
|
/my-series/
|
|
87
115
|
/world/
|
|
88
|
-
characters/elena.md
|
|
89
|
-
|
|
116
|
+
characters/elena/sheet.md
|
|
117
|
+
characters/elena/arc.md
|
|
118
|
+
places/harbor-district/sheet.md
|
|
119
|
+
reference/vampire-biology.md
|
|
90
120
|
/book-1/
|
|
91
121
|
/part-1/chapter-1/scene-001.md
|
|
92
122
|
/projects/
|
|
@@ -94,11 +124,107 @@ Alternatively, metadata can live in a sidecar file named `<scene-file>.meta.yaml
|
|
|
94
124
|
/world/
|
|
95
125
|
characters/
|
|
96
126
|
places/
|
|
127
|
+
reference/
|
|
97
128
|
/part-1/chapter-1/scene-001.md
|
|
98
129
|
```
|
|
99
130
|
|
|
131
|
+
Character/place folders use one canonical sheet file for entity indexing:
|
|
132
|
+
|
|
133
|
+
- `world/characters/<slug>/sheet.md` or `sheet.txt`
|
|
134
|
+
- `world/places/<slug>/sheet.md` or `sheet.txt`
|
|
135
|
+
|
|
136
|
+
Additional files in the same folder are treated as support notes, not separate entities.
|
|
137
|
+
|
|
100
138
|
Universe-level characters and places are shared across all books in that universe. Standalone projects are fully isolated.
|
|
101
139
|
|
|
140
|
+
### Scaffolding templates
|
|
141
|
+
|
|
142
|
+
Yes, a template is helpful here. It keeps the canonical metadata fields consistent, lowers the friction of adding a new character or place, and reduces the chance that a file is created in the right folder but missing the fields needed for indexing.
|
|
143
|
+
|
|
144
|
+
Use the scaffold script to create a canonical character or place folder:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
npm run new:entity -- --sync-dir /path/to/sync-root --kind character --scope universe --universe my-series --name "Mira Nystrom"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
npm run new:entity -- --sync-dir /path/to/sync-root --kind place --scope project --project my-series/book-1 --name "University Hospital"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This creates:
|
|
155
|
+
|
|
156
|
+
- `world/characters/<slug>/sheet.md` plus `arc.md` for character arcs
|
|
157
|
+
- `world/places/<slug>/sheet.md` for places
|
|
158
|
+
- `sheet.meta.yaml` with the required entity ID and starter fields
|
|
159
|
+
|
|
160
|
+
Generated Markdown follows one formatting contract so scaffolded files are predictable to edit:
|
|
161
|
+
|
|
162
|
+
- the first line is a top-level title (`# Name`)
|
|
163
|
+
- every heading is followed by a blank line
|
|
164
|
+
- every generated `.md` file ends with a trailing blank line
|
|
165
|
+
|
|
166
|
+
Use `--dry-run` to preview the path without writing files.
|
|
167
|
+
|
|
168
|
+
You can also create canonical sheets directly through the MCP server with `create_character_sheet` and `create_place_sheet`, then move your existing raw notes into the generated folders.
|
|
169
|
+
|
|
170
|
+
Recommended workflow:
|
|
171
|
+
|
|
172
|
+
1. Scaffold the entity folder and canonical sheet.
|
|
173
|
+
2. Fill in the metadata fields you care about first.
|
|
174
|
+
3. For characters, fill in `arc.md` as a separate arc-analysis document.
|
|
175
|
+
4. Add any other nearby notes like `relationships.md` or `history.md` as needed.
|
|
176
|
+
5. Run `sync` to index the new entity.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Real-world usage scenarios
|
|
181
|
+
|
|
182
|
+
The tool list is useful as reference. These example workflows show how people actually use `mcp-writing` while drafting and revising.
|
|
183
|
+
|
|
184
|
+
### 1) Continuity pass before sending chapters to beta readers
|
|
185
|
+
|
|
186
|
+
Goal: catch inconsistencies before sharing pages.
|
|
187
|
+
|
|
188
|
+
1. Run `sync` after your latest writing session.
|
|
189
|
+
2. Ask `find_scenes` for scenes involving a specific character or tag (for example, all scenes tagged `injury` or `promise`).
|
|
190
|
+
3. Use `get_arc` to review that character's ordered progression across the manuscript.
|
|
191
|
+
4. Load only the suspect scenes with `get_scene_prose`.
|
|
192
|
+
5. Attach follow-up notes with `flag_scene` where continuity needs a fix.
|
|
193
|
+
|
|
194
|
+
Outcome: you review one narrative thread at a time instead of rereading the entire novel to find contradictions.
|
|
195
|
+
|
|
196
|
+
### 2) Planning and tracking subplot beats during revisions
|
|
197
|
+
|
|
198
|
+
Goal: make sure subplot threads progress intentionally and resolve on time.
|
|
199
|
+
|
|
200
|
+
1. Run `list_threads` for the project.
|
|
201
|
+
2. Use `get_thread_arc` to inspect scene order and beat labels for each thread.
|
|
202
|
+
3. When a beat is missing, call `upsert_thread_link` to add or update it on the right scene.
|
|
203
|
+
4. Re-run `get_thread_arc` to confirm pacing and coverage.
|
|
204
|
+
|
|
205
|
+
Outcome: subplot structure stays visible and auditable, which reduces dropped threads in late drafts.
|
|
206
|
+
|
|
207
|
+
### 3) Tightening scene metadata after heavy prose edits
|
|
208
|
+
|
|
209
|
+
Goal: keep indexes accurate without manually re-tagging everything.
|
|
210
|
+
|
|
211
|
+
1. After rewriting scenes, call `enrich_scene` to re-derive lightweight metadata from current prose.
|
|
212
|
+
2. Use `update_scene_metadata` for intentional editorial fields (for example, beat, POV, timeline position, and tags).
|
|
213
|
+
3. Use `search_metadata` and `find_scenes` to verify scenes are discoverable under the expected filters.
|
|
214
|
+
|
|
215
|
+
Outcome: your AI assistant can reliably find the right scenes without drifting from the manuscript.
|
|
216
|
+
|
|
217
|
+
### 4) Safe AI-assisted line edits with rollback
|
|
218
|
+
|
|
219
|
+
Goal: let AI propose prose edits without losing control of your draft.
|
|
220
|
+
|
|
221
|
+
1. Ask the AI to call `propose_edit` for a specific scene.
|
|
222
|
+
2. Review the staged diff.
|
|
223
|
+
3. Accept with `commit_edit` or reject with `discard_edit`.
|
|
224
|
+
4. Use `list_snapshots` (and optional `snapshot_scene`) to inspect or preserve revision history.
|
|
225
|
+
|
|
226
|
+
Outcome: you get AI speed with explicit approval and recoverable history for every applied change.
|
|
227
|
+
|
|
102
228
|
---
|
|
103
229
|
|
|
104
230
|
## Available tools
|
|
@@ -109,10 +235,14 @@ Universe-level characters and places are shared across all books in that univers
|
|
|
109
235
|
| `find_scenes` | Filter scenes by character, beat, tag, part, chapter, or POV |
|
|
110
236
|
| `get_scene_prose` | Load the full prose for a specific scene |
|
|
111
237
|
| `get_chapter_prose` | Load all prose for a chapter |
|
|
238
|
+
| `get_runtime_config` | Show the active sync dir, DB path, and runtime capabilities |
|
|
112
239
|
| `get_arc` | Ordered scene metadata for all scenes involving a character |
|
|
113
240
|
| `list_characters` | All characters, optionally filtered by project or universe |
|
|
114
|
-
| `get_character_sheet` | Full character metadata, traits, and notes |
|
|
241
|
+
| `get_character_sheet` | Full character metadata, traits, notes, and support notes |
|
|
242
|
+
| `create_character_sheet` | Create a canonical character sheet folder and sidecar |
|
|
115
243
|
| `list_places` | All places |
|
|
244
|
+
| `get_place_sheet` | Full place metadata, tags, associated characters, notes, and support notes |
|
|
245
|
+
| `create_place_sheet` | Create a canonical place sheet folder and sidecar |
|
|
116
246
|
| `search_metadata` | Full-text search across scene titles and loglines |
|
|
117
247
|
| `list_threads` | All subplot threads for a project |
|
|
118
248
|
| `get_thread_arc` | Scenes belonging to a thread, with per-thread beat |
|
|
@@ -121,6 +251,11 @@ Universe-level characters and places are shared across all books in that univers
|
|
|
121
251
|
| `update_scene_metadata` | Write metadata fields back to a scene sidecar |
|
|
122
252
|
| `update_character_sheet` | Write fields back to a character sidecar |
|
|
123
253
|
| `flag_scene` | Mark a scene with a flag for AI follow-up |
|
|
254
|
+
| `propose_edit` | Stage a scene revision for review without writing it |
|
|
255
|
+
| `commit_edit` | Apply a staged prose edit and create a git-backed snapshot |
|
|
256
|
+
| `discard_edit` | Discard a pending staged prose edit |
|
|
257
|
+
| `snapshot_scene` | Create a manual git snapshot for a scene |
|
|
258
|
+
| `list_snapshots` | List snapshot history for a scene |
|
|
124
259
|
|
|
125
260
|
Paginated tools (`find_scenes`, `get_arc`, `list_threads`, `get_thread_arc`, `search_metadata`) accept `page` and `page_size` arguments and return `total_count` / `total_pages` in the response envelope.
|
|
126
261
|
|
|
@@ -166,6 +301,27 @@ npm install
|
|
|
166
301
|
WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
167
302
|
```
|
|
168
303
|
|
|
304
|
+
The `npm start` script automatically includes the `--experimental-sqlite` flag needed for SQLite support in Node.js 22+.
|
|
305
|
+
|
|
306
|
+
## Verify your setup
|
|
307
|
+
|
|
308
|
+
After starting the server, test that it's working:
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
# In a new terminal
|
|
312
|
+
curl http://localhost:3000/healthz
|
|
313
|
+
# Should return: ok
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Then test the MCP endpoint:
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
curl http://localhost:3000/sse
|
|
320
|
+
# Should return a stream endpoint: /message?sessionId=<id>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
If both return successfully, the server is ready to use.
|
|
324
|
+
|
|
169
325
|
## Development
|
|
170
326
|
|
|
171
327
|
```sh
|
|
@@ -173,10 +329,90 @@ npm install
|
|
|
173
329
|
npm test # unit + integration tests
|
|
174
330
|
npm run test:unit # unit tests only (no server required)
|
|
175
331
|
npm run lint:metadata # lint metadata in WRITING_SYNC_DIR or ./sync
|
|
176
|
-
npm run lint:metadata:test # lint fixture metadata in ./test-sync
|
|
177
332
|
```
|
|
178
333
|
|
|
179
|
-
Unit tests use an in-memory SQLite database and temporary directories — no server needed. Integration tests spawn a real server
|
|
334
|
+
Unit tests use an in-memory SQLite database and temporary directories — no server needed. Integration tests generate a fixture sync tree at runtime in temporary directories, spawn a real server on port 3099, and verify all MCP tools end-to-end.
|
|
335
|
+
|
|
336
|
+
For real projects, keep your manuscript sync folder outside this tool repository and point `WRITING_SYNC_DIR` at that external path.
|
|
337
|
+
|
|
338
|
+
## Troubleshooting
|
|
339
|
+
|
|
340
|
+
### "Module not found: sqlite" or "Database support not available"
|
|
341
|
+
|
|
342
|
+
**Cause:** Node.js version is below 22.6.0 or the `--experimental-sqlite` flag was not passed.
|
|
343
|
+
|
|
344
|
+
**Solution:**
|
|
345
|
+
1. Check your Node.js version: `node --version` (should be v22.6.0+)
|
|
346
|
+
2. Update Node.js if needed: use nvm, homebrew, or download from nodejs.org
|
|
347
|
+
3. Restart with `npm start` (which includes the flag automatically)
|
|
348
|
+
|
|
349
|
+
### "EADDRINUSE: address already in use :::3000"
|
|
350
|
+
|
|
351
|
+
**Cause:** Port 3000 is already in use by another application.
|
|
352
|
+
|
|
353
|
+
**Solution:**
|
|
354
|
+
Use a different port:
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
HTTP_PORT=3001 WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Then update your MCP client config to use `http://localhost:3001/sse`.
|
|
361
|
+
|
|
362
|
+
### "ENOENT: no such file or directory, open './writing.db'"
|
|
363
|
+
|
|
364
|
+
**Cause:** The directory for `DB_PATH` does not exist.
|
|
365
|
+
|
|
366
|
+
**Solution:**
|
|
367
|
+
Create the directory first:
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
mkdir -p $(dirname ./writing.db) # if using a subdirectory
|
|
371
|
+
WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Or use an absolute path:
|
|
375
|
+
|
|
376
|
+
```sh
|
|
377
|
+
WRITING_SYNC_DIR=~/my-manuscript DB_PATH=~/writing-data/writing.db npm start
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### "Sync dir not found: ./my-manuscript"
|
|
381
|
+
|
|
382
|
+
**Cause:** The `WRITING_SYNC_DIR` path does not exist.
|
|
383
|
+
|
|
384
|
+
**Solution:**
|
|
385
|
+
Create the sync folder first:
|
|
386
|
+
|
|
387
|
+
```sh
|
|
388
|
+
mkdir -p ./my-manuscript/projects/my-novel
|
|
389
|
+
WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Or point to an existing folder where you've already placed scene files.
|
|
393
|
+
|
|
394
|
+
### "Import failed: unrecognized format"
|
|
395
|
+
|
|
396
|
+
**Cause:** The Scrivener export format was not plain text (`.txt`) or the folder structure is unexpected.
|
|
397
|
+
|
|
398
|
+
**Solution:**
|
|
399
|
+
1. In Scrivener, re-export with **File → Sync → With External Folder**
|
|
400
|
+
2. Ensure the format is set to **Plain text** (not RTF or .docx)
|
|
401
|
+
3. Verify the export folder has a `Draft/` subdirectory with `.txt` files
|
|
402
|
+
4. Try the import again: `node scripts/import.js ~/my-novel-txt /path/to/sync-dir --project my-novel`
|
|
403
|
+
|
|
404
|
+
### Tests fail after updating Node.js
|
|
405
|
+
|
|
406
|
+
**Cause:** SQLite module cache may be stale.
|
|
407
|
+
|
|
408
|
+
**Solution:**
|
|
409
|
+
Clear npm cache and reinstall:
|
|
410
|
+
|
|
411
|
+
```sh
|
|
412
|
+
rm -rf node_modules package-lock.json
|
|
413
|
+
npm install
|
|
414
|
+
npm test
|
|
415
|
+
```
|
|
180
416
|
|
|
181
417
|
## Environment variables
|
|
182
418
|
|
package/git.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if a directory is itself the root of a git repository (not just inside one).
|
|
7
|
+
* This prevents mcp-writing's own .git from being used for prose snapshots when
|
|
8
|
+
* WRITING_SYNC_DIR is a subdirectory of the code repo.
|
|
9
|
+
*/
|
|
10
|
+
export function isGitRepository(dirPath) {
|
|
11
|
+
try {
|
|
12
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
13
|
+
cwd: dirPath,
|
|
14
|
+
stdio: "pipe",
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
}).trim();
|
|
17
|
+
return path.resolve(gitRoot) === path.resolve(dirPath);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize a git repository in a directory
|
|
25
|
+
*/
|
|
26
|
+
export function initGitRepository(dirPath) {
|
|
27
|
+
try {
|
|
28
|
+
execSync("git init", { cwd: dirPath, stdio: "pipe" });
|
|
29
|
+
// Set a dummy user config for commits if not already set
|
|
30
|
+
try {
|
|
31
|
+
execSync("git config user.email", { cwd: dirPath, stdio: "pipe" });
|
|
32
|
+
} catch {
|
|
33
|
+
execSync("git config user.email writing-mcp@local", { cwd: dirPath, stdio: "pipe" });
|
|
34
|
+
execSync("git config user.name writing-mcp", { cwd: dirPath, stdio: "pipe" });
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new Error(`Failed to initialize git repository: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if git is available on PATH
|
|
44
|
+
*/
|
|
45
|
+
export function isGitAvailable() {
|
|
46
|
+
try {
|
|
47
|
+
execSync("git --version", { stdio: "pipe" });
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a git commit for a scene file (pre-edit snapshot)
|
|
56
|
+
* Returns { commit_hash: string, commit_message: string }
|
|
57
|
+
*/
|
|
58
|
+
export function createSnapshot(dirPath, filePath, sceneId, instruction) {
|
|
59
|
+
try {
|
|
60
|
+
const relPath = path.relative(dirPath, filePath);
|
|
61
|
+
execSync(`git add "${relPath}"`, { cwd: dirPath, stdio: "pipe" });
|
|
62
|
+
|
|
63
|
+
const commitMessage = `pre-edit snapshot: ${sceneId} — ${instruction}`;
|
|
64
|
+
// Use 2>&1 so git's stderr (where it prints "[branch hash] msg") is captured in stdout
|
|
65
|
+
const output = execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}" 2>&1`, {
|
|
66
|
+
cwd: dirPath,
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
stdio: "pipe",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// git outputs "[branch hash] message" to stderr; redirect 2>&1 captures it in stdout
|
|
72
|
+
// Regex handles any branch name, with or without (root-commit)
|
|
73
|
+
const match = output.match(/\[\S+(?:\s+\(root-commit\))?\s+([a-f0-9]+)\]/);
|
|
74
|
+
const commitHash = match ? match[1] : null;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
commit_hash: commitHash,
|
|
78
|
+
commit_message: commitMessage,
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Check if nothing changed (no error, just no commit)
|
|
82
|
+
if (err.message.includes("nothing to commit") || err.status === 1) {
|
|
83
|
+
return {
|
|
84
|
+
commit_hash: null,
|
|
85
|
+
commit_message: null,
|
|
86
|
+
reason: "no changes to commit",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Failed to create snapshot: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* List git commits for a file, with timestamps and messages
|
|
95
|
+
* Returns array of { commit_hash, timestamp, message }
|
|
96
|
+
*/
|
|
97
|
+
export function listSnapshots(dirPath, filePath) {
|
|
98
|
+
try {
|
|
99
|
+
const relPath = path.relative(dirPath, filePath);
|
|
100
|
+
const output = execSync(
|
|
101
|
+
`git log --pretty=format:"%h|%ai|%s" -- "${relPath}"`,
|
|
102
|
+
{
|
|
103
|
+
cwd: dirPath,
|
|
104
|
+
stdio: "pipe",
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!output) return [];
|
|
110
|
+
|
|
111
|
+
return output
|
|
112
|
+
.split("\n")
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.map((line) => {
|
|
115
|
+
const [hash, timestamp, ...messageParts] = line.split("|");
|
|
116
|
+
return {
|
|
117
|
+
commit_hash: hash.trim(),
|
|
118
|
+
timestamp: timestamp.trim(),
|
|
119
|
+
message: messageParts.join("|").trim(),
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// If there are no commits yet, return empty array
|
|
124
|
+
if (err.message.includes("your current branch") || err.status === 128) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`Failed to list snapshots: ${err.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get prose content from a specific git commit
|
|
133
|
+
* If commit is null, returns current working tree version
|
|
134
|
+
*/
|
|
135
|
+
export function getSceneProseAtCommit(dirPath, filePath, commitHash) {
|
|
136
|
+
try {
|
|
137
|
+
const relPath = path.relative(dirPath, filePath);
|
|
138
|
+
|
|
139
|
+
if (!commitHash) {
|
|
140
|
+
// Return current working tree version
|
|
141
|
+
return fs.readFileSync(filePath, "utf8");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get version from git
|
|
145
|
+
const content = execSync(`git show ${commitHash}:"${relPath}"`, {
|
|
146
|
+
cwd: dirPath,
|
|
147
|
+
stdio: "pipe",
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return content;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err.code === "ENOENT") {
|
|
154
|
+
throw new Error(`File not found in commit ${commitHash}`);
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`Failed to retrieve scene prose: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if working tree is clean (no uncommitted changes)
|
|
162
|
+
*/
|
|
163
|
+
export function isWorkingTreeClean(dirPath) {
|
|
164
|
+
try {
|
|
165
|
+
const output = execSync("git status --porcelain", {
|
|
166
|
+
cwd: dirPath,
|
|
167
|
+
stdio: "pipe",
|
|
168
|
+
encoding: "utf8",
|
|
169
|
+
});
|
|
170
|
+
return output.trim() === "";
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the HEAD commit hash
|
|
178
|
+
*/
|
|
179
|
+
export function getHeadCommitHash(dirPath) {
|
|
180
|
+
try {
|
|
181
|
+
const hash = execSync("git rev-parse HEAD", {
|
|
182
|
+
cwd: dirPath,
|
|
183
|
+
stdio: "pipe",
|
|
184
|
+
encoding: "utf8",
|
|
185
|
+
}).trim();
|
|
186
|
+
return hash;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|