@hanna84/mcp-writing 1.0.0 → 1.3.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 +87 -0
- package/README.md +126 -12
- package/git.js +190 -0
- package/index.js +563 -13
- package/metadata-lint.js +117 -4
- package/package.json +15 -3
- package/scripts/import.js +95 -166
- package/scripts/manual-validation.mjs +273 -0
- package/scripts/mcp-debug-client.mjs +43 -0
- package/scripts/new-world-entity.js +160 -0
- package/sync.js +43 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.3.6](https://github.com/hannasdev/mcp-writing/compare/v1.3.5...v1.3.6) (2026-04-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **ci:** restore npm trusted publishing auth config ([f043368](https://github.com/hannasdev/mcp-writing/commit/f0433681e984e34d1c38725f2d85dc3d14031626))
|
|
9
|
+
|
|
10
|
+
## [1.3.5](https://github.com/hannasdev/mcp-writing/compare/v1.3.4...v1.3.5) (2026-04-18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **ci:** force OIDC-only npm publish auth ([c9f1216](https://github.com/hannasdev/mcp-writing/commit/c9f1216f2d3aa49fcc2aad5e6c4032962ad843d1))
|
|
16
|
+
|
|
17
|
+
## [1.3.4](https://github.com/hannasdev/mcp-writing/compare/v1.3.3...v1.3.4) (2026-04-18)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **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))
|
|
23
|
+
|
|
24
|
+
## [1.3.3](https://github.com/hannasdev/mcp-writing/compare/v1.3.2...v1.3.3) (2026-04-18)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **ci:** use tag trigger for npm publish (matches n8n pattern) ([6841de7](https://github.com/hannasdev/mcp-writing/commit/6841de78a90c3b4c121761d28a09b02f9d360a96))
|
|
30
|
+
|
|
31
|
+
## [1.3.2](https://github.com/hannasdev/mcp-writing/compare/v1.3.1...v1.3.2) (2026-04-18)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
* **ci:** publish from main release commits for npm trusted publishing ([135ab25](https://github.com/hannasdev/mcp-writing/commit/135ab254fb91ee6427a79cdecdc5b9bf55cfb4d1))
|
|
37
|
+
|
|
38
|
+
## [1.3.1](https://github.com/hannasdev/mcp-writing/compare/v1.3.0...v1.3.1) (2026-04-18)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Bug Fixes
|
|
42
|
+
|
|
43
|
+
* **ci:** set npm environment for trusted publishing ([b75fb08](https://github.com/hannasdev/mcp-writing/commit/b75fb0897dbbbd9df2d136cbb78a06218f0c0b50))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Miscellaneous Chores
|
|
47
|
+
|
|
48
|
+
* 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))
|
|
49
|
+
|
|
50
|
+
## [1.3.0](https://github.com/hannasdev/mcp-writing/compare/v1.2.0...v1.3.0) (2026-04-17)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Features
|
|
54
|
+
|
|
55
|
+
* 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))
|
|
56
|
+
|
|
57
|
+
## [1.2.0](https://github.com/hannasdev/mcp-writing/compare/v1.1.1...v1.2.0) (2026-04-17)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Features
|
|
61
|
+
|
|
62
|
+
* Phase 3 git-backed prose editing, runtime config tool, startup path logging ([f30a7c8](https://github.com/hannasdev/mcp-writing/commit/f30a7c8b5e5aaf070b0b6cfaec38479141c5f60b))
|
|
63
|
+
|
|
64
|
+
## [1.1.1](https://github.com/hannasdev/mcp-writing/compare/v1.1.0...v1.1.1) (2026-04-16)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Bug Fixes
|
|
68
|
+
|
|
69
|
+
* use dedicated token for release-please workflow ([#10](https://github.com/hannasdev/mcp-writing/issues/10)) ([6167a59](https://github.com/hannasdev/mcp-writing/commit/6167a598999c173ba04bcdc61223259449e1cf24))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
### Miscellaneous Chores
|
|
73
|
+
|
|
74
|
+
* fix lint in MCP validation scripts ([2b2385e](https://github.com/hannasdev/mcp-writing/commit/2b2385e3eddd8fd30716d0f72dfa6346ec2d6e5f))
|
|
75
|
+
* include chore commits in release automation ([5a28f27](https://github.com/hannasdev/mcp-writing/commit/5a28f277d1551e35c7fd717836c259badbf8a1d9))
|
|
76
|
+
* include chore commits in release-please ([c437850](https://github.com/hannasdev/mcp-writing/commit/c4378501ee7dc29d582ce78bc1d227927e719810))
|
|
77
|
+
* keep reusable MCP validation scripts ([65f44cb](https://github.com/hannasdev/mcp-writing/commit/65f44cb730176aa4b340c42685dccda05a6725e6))
|
|
78
|
+
* keep reusable MCP validation scripts ([9d4181e](https://github.com/hannasdev/mcp-writing/commit/9d4181e32d6ba9a76a33659c5cfedfe9cf296619))
|
|
79
|
+
* trigger CI for release-please branches ([09e599a](https://github.com/hannasdev/mcp-writing/commit/09e599a399e6b9e039ef2670cfa51a88518dbd56))
|
|
80
|
+
|
|
81
|
+
## [1.1.0](https://github.com/hannasdev/mcp-writing/compare/v1.0.0...v1.1.0) (2026-04-16)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
### Features
|
|
85
|
+
|
|
86
|
+
* reconcile Scrivener reorders via stable external IDs ([977a1c3](https://github.com/hannasdev/mcp-writing/commit/977a1c3770861b5f8b0dd9ec8bcdcb00ff39d18a))
|
|
87
|
+
* stable Scrivener identity and reorder reconciliation ([14c165f](https://github.com/hannasdev/mcp-writing/commit/14c165fd80050c59e162fd35bd84cbd39b767cee))
|
package/README.md
CHANGED
|
@@ -9,16 +9,22 @@ 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.
|
|
14
20
|
|
|
15
21
|
## Quick start with Scrivener
|
|
16
22
|
|
|
17
|
-
If you write in [Scrivener](https://www.literatureandlatte.com/scrivener), you can seed `mcp-writing` from a Scrivener external-sync export
|
|
23
|
+
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
24
|
|
|
19
25
|
### 1. Export from Scrivener
|
|
20
26
|
|
|
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/`.
|
|
27
|
+
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
28
|
|
|
23
29
|
### 2. Import into mcp-writing
|
|
24
30
|
|
|
@@ -27,11 +33,11 @@ node scripts/import.js ~/my-novel-txt /path/to/sync-dir --project my-novel
|
|
|
27
33
|
```
|
|
28
34
|
|
|
29
35
|
The importer:
|
|
36
|
+
|
|
30
37
|
- 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
38
|
- Skips beat-marker files (`-Setup-`, `-Catalyst-`, etc.), chapter-intro files, epigraphs, and trashed files.
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
Non-draft content is not inferred from `Notes/`. Put it directly into the target sync dir using the `world/` folder conventions described below.
|
|
35
41
|
|
|
36
42
|
### 3. Start the server
|
|
37
43
|
|
|
@@ -80,13 +86,15 @@ Alternatively, metadata can live in a sidecar file named `<scene-file>.meta.yaml
|
|
|
80
86
|
|
|
81
87
|
### Project structure
|
|
82
88
|
|
|
83
|
-
```
|
|
89
|
+
```bash
|
|
84
90
|
/sync-root/
|
|
85
91
|
/universes/
|
|
86
92
|
/my-series/
|
|
87
93
|
/world/
|
|
88
|
-
characters/elena.md
|
|
89
|
-
|
|
94
|
+
characters/elena/sheet.md
|
|
95
|
+
characters/elena/arc.md
|
|
96
|
+
places/harbor-district/sheet.md
|
|
97
|
+
reference/vampire-biology.md
|
|
90
98
|
/book-1/
|
|
91
99
|
/part-1/chapter-1/scene-001.md
|
|
92
100
|
/projects/
|
|
@@ -94,11 +102,107 @@ Alternatively, metadata can live in a sidecar file named `<scene-file>.meta.yaml
|
|
|
94
102
|
/world/
|
|
95
103
|
characters/
|
|
96
104
|
places/
|
|
105
|
+
reference/
|
|
97
106
|
/part-1/chapter-1/scene-001.md
|
|
98
107
|
```
|
|
99
108
|
|
|
109
|
+
Character/place folders use one canonical sheet file for entity indexing:
|
|
110
|
+
|
|
111
|
+
- `world/characters/<slug>/sheet.md` or `sheet.txt`
|
|
112
|
+
- `world/places/<slug>/sheet.md` or `sheet.txt`
|
|
113
|
+
|
|
114
|
+
Additional files in the same folder are treated as support notes, not separate entities.
|
|
115
|
+
|
|
100
116
|
Universe-level characters and places are shared across all books in that universe. Standalone projects are fully isolated.
|
|
101
117
|
|
|
118
|
+
### Scaffolding templates
|
|
119
|
+
|
|
120
|
+
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.
|
|
121
|
+
|
|
122
|
+
Use the scaffold script to create a canonical character or place folder:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
npm run new:entity -- --sync-dir /path/to/sync-root --kind character --scope universe --universe my-series --name "Mira Nystrom"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
npm run new:entity -- --sync-dir /path/to/sync-root --kind place --scope project --project my-series/book-1 --name "University Hospital"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This creates:
|
|
133
|
+
|
|
134
|
+
- `world/characters/<slug>/sheet.md` plus `arc.md` for character arcs
|
|
135
|
+
- `world/places/<slug>/sheet.md` for places
|
|
136
|
+
- `sheet.meta.yaml` with the required entity ID and starter fields
|
|
137
|
+
|
|
138
|
+
Generated Markdown follows one formatting contract so scaffolded files are predictable to edit:
|
|
139
|
+
|
|
140
|
+
- the first line is a top-level title (`# Name`)
|
|
141
|
+
- every heading is followed by a blank line
|
|
142
|
+
- every generated `.md` file ends with a trailing blank line
|
|
143
|
+
|
|
144
|
+
Use `--dry-run` to preview the path without writing files.
|
|
145
|
+
|
|
146
|
+
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.
|
|
147
|
+
|
|
148
|
+
Recommended workflow:
|
|
149
|
+
|
|
150
|
+
1. Scaffold the entity folder and canonical sheet.
|
|
151
|
+
2. Fill in the metadata fields you care about first.
|
|
152
|
+
3. For characters, fill in `arc.md` as a separate arc-analysis document.
|
|
153
|
+
4. Add any other nearby notes like `relationships.md` or `history.md` as needed.
|
|
154
|
+
5. Run `sync` to index the new entity.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Real-world usage scenarios
|
|
159
|
+
|
|
160
|
+
The tool list is useful as reference. These example workflows show how people actually use `mcp-writing` while drafting and revising.
|
|
161
|
+
|
|
162
|
+
### 1) Continuity pass before sending chapters to beta readers
|
|
163
|
+
|
|
164
|
+
Goal: catch inconsistencies before sharing pages.
|
|
165
|
+
|
|
166
|
+
1. Run `sync` after your latest writing session.
|
|
167
|
+
2. Ask `find_scenes` for scenes involving a specific character or tag (for example, all scenes tagged `injury` or `promise`).
|
|
168
|
+
3. Use `get_arc` to review that character's ordered progression across the manuscript.
|
|
169
|
+
4. Load only the suspect scenes with `get_scene_prose`.
|
|
170
|
+
5. Attach follow-up notes with `flag_scene` where continuity needs a fix.
|
|
171
|
+
|
|
172
|
+
Outcome: you review one narrative thread at a time instead of rereading the entire novel to find contradictions.
|
|
173
|
+
|
|
174
|
+
### 2) Planning and tracking subplot beats during revisions
|
|
175
|
+
|
|
176
|
+
Goal: make sure subplot threads progress intentionally and resolve on time.
|
|
177
|
+
|
|
178
|
+
1. Run `list_threads` for the project.
|
|
179
|
+
2. Use `get_thread_arc` to inspect scene order and beat labels for each thread.
|
|
180
|
+
3. When a beat is missing, call `upsert_thread_link` to add or update it on the right scene.
|
|
181
|
+
4. Re-run `get_thread_arc` to confirm pacing and coverage.
|
|
182
|
+
|
|
183
|
+
Outcome: subplot structure stays visible and auditable, which reduces dropped threads in late drafts.
|
|
184
|
+
|
|
185
|
+
### 3) Tightening scene metadata after heavy prose edits
|
|
186
|
+
|
|
187
|
+
Goal: keep indexes accurate without manually re-tagging everything.
|
|
188
|
+
|
|
189
|
+
1. After rewriting scenes, call `enrich_scene` to re-derive lightweight metadata from current prose.
|
|
190
|
+
2. Use `update_scene_metadata` for intentional editorial fields (for example, beat, POV, timeline position, and tags).
|
|
191
|
+
3. Use `search_metadata` and `find_scenes` to verify scenes are discoverable under the expected filters.
|
|
192
|
+
|
|
193
|
+
Outcome: your AI assistant can reliably find the right scenes without drifting from the manuscript.
|
|
194
|
+
|
|
195
|
+
### 4) Safe AI-assisted line edits with rollback
|
|
196
|
+
|
|
197
|
+
Goal: let AI propose prose edits without losing control of your draft.
|
|
198
|
+
|
|
199
|
+
1. Ask the AI to call `propose_edit` for a specific scene.
|
|
200
|
+
2. Review the staged diff.
|
|
201
|
+
3. Accept with `commit_edit` or reject with `discard_edit`.
|
|
202
|
+
4. Use `list_snapshots` (and optional `snapshot_scene`) to inspect or preserve revision history.
|
|
203
|
+
|
|
204
|
+
Outcome: you get AI speed with explicit approval and recoverable history for every applied change.
|
|
205
|
+
|
|
102
206
|
---
|
|
103
207
|
|
|
104
208
|
## Available tools
|
|
@@ -109,10 +213,14 @@ Universe-level characters and places are shared across all books in that univers
|
|
|
109
213
|
| `find_scenes` | Filter scenes by character, beat, tag, part, chapter, or POV |
|
|
110
214
|
| `get_scene_prose` | Load the full prose for a specific scene |
|
|
111
215
|
| `get_chapter_prose` | Load all prose for a chapter |
|
|
216
|
+
| `get_runtime_config` | Show the active sync dir, DB path, and runtime capabilities |
|
|
112
217
|
| `get_arc` | Ordered scene metadata for all scenes involving a character |
|
|
113
218
|
| `list_characters` | All characters, optionally filtered by project or universe |
|
|
114
|
-
| `get_character_sheet` | Full character metadata, traits, and notes |
|
|
219
|
+
| `get_character_sheet` | Full character metadata, traits, notes, and support notes |
|
|
220
|
+
| `create_character_sheet` | Create a canonical character sheet folder and sidecar |
|
|
115
221
|
| `list_places` | All places |
|
|
222
|
+
| `get_place_sheet` | Full place metadata, tags, associated characters, notes, and support notes |
|
|
223
|
+
| `create_place_sheet` | Create a canonical place sheet folder and sidecar |
|
|
116
224
|
| `search_metadata` | Full-text search across scene titles and loglines |
|
|
117
225
|
| `list_threads` | All subplot threads for a project |
|
|
118
226
|
| `get_thread_arc` | Scenes belonging to a thread, with per-thread beat |
|
|
@@ -121,6 +229,11 @@ Universe-level characters and places are shared across all books in that univers
|
|
|
121
229
|
| `update_scene_metadata` | Write metadata fields back to a scene sidecar |
|
|
122
230
|
| `update_character_sheet` | Write fields back to a character sidecar |
|
|
123
231
|
| `flag_scene` | Mark a scene with a flag for AI follow-up |
|
|
232
|
+
| `propose_edit` | Stage a scene revision for review without writing it |
|
|
233
|
+
| `commit_edit` | Apply a staged prose edit and create a git-backed snapshot |
|
|
234
|
+
| `discard_edit` | Discard a pending staged prose edit |
|
|
235
|
+
| `snapshot_scene` | Create a manual git snapshot for a scene |
|
|
236
|
+
| `list_snapshots` | List snapshot history for a scene |
|
|
124
237
|
|
|
125
238
|
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
239
|
|
|
@@ -173,10 +286,11 @@ npm install
|
|
|
173
286
|
npm test # unit + integration tests
|
|
174
287
|
npm run test:unit # unit tests only (no server required)
|
|
175
288
|
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
289
|
```
|
|
178
290
|
|
|
179
|
-
Unit tests use an in-memory SQLite database and temporary directories — no server needed. Integration tests spawn a real server
|
|
291
|
+
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.
|
|
292
|
+
|
|
293
|
+
For real projects, keep your manuscript sync folder outside this tool repository and point `WRITING_SYNC_DIR` at that external path.
|
|
180
294
|
|
|
181
295
|
## Environment variables
|
|
182
296
|
|
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
|
+
}
|