novelforge-agent 0.1.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/dist/src/cli/index.js +125 -0
  4. package/dist/src/core/contextBuilder.js +128 -0
  5. package/dist/src/core/fileNames.js +41 -0
  6. package/dist/src/core/index.js +9 -0
  7. package/dist/src/core/projectDiscovery.js +141 -0
  8. package/dist/src/core/projectStore.js +85 -0
  9. package/dist/src/core/prompts/en-US.js +363 -0
  10. package/dist/src/core/prompts/types.js +1 -0
  11. package/dist/src/core/prompts/zh-CN.js +362 -0
  12. package/dist/src/core/prompts.js +15 -0
  13. package/dist/src/core/retrieval/chunker.js +77 -0
  14. package/dist/src/core/retrieval/index.js +125 -0
  15. package/dist/src/core/retrieval/tokenizer.js +43 -0
  16. package/dist/src/core/retrieval/types.js +1 -0
  17. package/dist/src/core/schemas.js +91 -0
  18. package/dist/src/core/steps/architecture.js +16 -0
  19. package/dist/src/core/steps/chapter.js +16 -0
  20. package/dist/src/core/steps/chapterReview.js +16 -0
  21. package/dist/src/core/steps/chapterRevision.js +20 -0
  22. package/dist/src/core/steps/continuityReview.js +13 -0
  23. package/dist/src/core/steps/crossChapterReview.js +15 -0
  24. package/dist/src/core/steps/index.js +20 -0
  25. package/dist/src/core/steps/memoryCard.js +22 -0
  26. package/dist/src/core/steps/novelMetadata.js +12 -0
  27. package/dist/src/core/steps/storyBible.js +13 -0
  28. package/dist/src/core/steps/types.js +7 -0
  29. package/dist/src/core/types.js +1 -0
  30. package/dist/src/core/workflow.js +186 -0
  31. package/dist/src/mcp/server.js +13 -0
  32. package/dist/src/mcp/tools.js +126 -0
  33. package/package.json +61 -0
  34. package/src/cli/index.ts +147 -0
  35. package/src/core/contextBuilder.ts +131 -0
  36. package/src/core/fileNames.ts +48 -0
  37. package/src/core/index.ts +9 -0
  38. package/src/core/projectDiscovery.ts +174 -0
  39. package/src/core/projectStore.ts +111 -0
  40. package/src/core/prompts/en-US.ts +376 -0
  41. package/src/core/prompts/types.ts +28 -0
  42. package/src/core/prompts/zh-CN.ts +375 -0
  43. package/src/core/prompts.ts +27 -0
  44. package/src/core/retrieval/chunker.ts +80 -0
  45. package/src/core/retrieval/index.ts +136 -0
  46. package/src/core/retrieval/tokenizer.ts +44 -0
  47. package/src/core/retrieval/types.ts +24 -0
  48. package/src/core/schemas.ts +101 -0
  49. package/src/core/steps/architecture.ts +17 -0
  50. package/src/core/steps/chapter.ts +17 -0
  51. package/src/core/steps/chapterReview.ts +17 -0
  52. package/src/core/steps/chapterRevision.ts +21 -0
  53. package/src/core/steps/continuityReview.ts +14 -0
  54. package/src/core/steps/crossChapterReview.ts +16 -0
  55. package/src/core/steps/index.ts +25 -0
  56. package/src/core/steps/memoryCard.ts +23 -0
  57. package/src/core/steps/novelMetadata.ts +13 -0
  58. package/src/core/steps/storyBible.ts +14 -0
  59. package/src/core/steps/types.ts +21 -0
  60. package/src/core/types.ts +115 -0
  61. package/src/core/workflow.ts +250 -0
  62. package/src/mcp/server.ts +15 -0
  63. package/src/mcp/tools.ts +227 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 linkzhao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # NovelForge Agent
2
+
3
+ A local-first long-form novel workflow engine for any MCP host (Claude Code, Codex CLI, …) or any CLI shell.
4
+
5
+ **The host's LLM writes the prose. This package does everything else** — it manages a 9-step state machine, returns the exact instruction and packed context the host should follow next, validates returned content against zod schemas, persists Markdown + JSON to a project directory, archives chapter versions on revision, and provides BM25 lexical retrieval over every word the project has ever produced.
6
+
7
+ No external API. No LLM dependency. No vendor lock-in.
8
+
9
+ ## What it gives the host
10
+
11
+ | Phase | Step | What the host does | What the agent saves |
12
+ |-------|------|---------------------|----------------------|
13
+ | Setup | `novel_metadata` | Output JSON: title, genre, premise, cast | `novel.json` |
14
+ | | `story_bible` | Output Markdown: characters, world rules, plot threads | `story-bible.md` |
15
+ | | `architecture` | Output JSON: full / volume / chapter outlines | `architecture/{full.md, volumes.json, chapters.json}` |
16
+ | Loop | `chapter` | Write chapter N Markdown | `chapters/NNN.md` |
17
+ | | `memory_card` | Extract chapter N memory JSON | `memory/chapter-NNN.json` |
18
+ | Wrap | `continuity_review` | Audit chapters 1..N for conflicts | `reviews/continuity-S-E.json` |
19
+ | Side-track | `chapter_review` | Single-chapter editorial review | `reviews/chapter/chapter-NNN.json` |
20
+ | | `chapter_revision` | Rewrite a chapter; previous version auto-archived | `chapters/.versions/NNN.<ts>.md` |
21
+ | | `cross_chapter_review` | Cross-chapter continuity audit | `reviews/cross/cross-S-E.json` |
22
+
23
+ Each chapter / bible / memory write also feeds a per-project BM25 index (`.index/`) so the agent can hand the host semantically relevant snippets when later chapters are generated, or answer ad-hoc `retrieve` queries from the host.
24
+
25
+ ## Install
26
+
27
+ Requires Node 20+.
28
+
29
+ ```bash
30
+ git clone <this repo>
31
+ cd novelforge-agent
32
+ npm install
33
+ npm run build
34
+ ```
35
+
36
+ Run the test suite to confirm:
37
+
38
+ ```bash
39
+ npm test
40
+ ```
41
+
42
+ ## Use it from a CLI shell
43
+
44
+ ```bash
45
+ # 1. Start a new project
46
+ node dist/src/cli/index.js start --prompt "写一本赛博修仙小说" --chapters 5
47
+ # → prints { state, next } — next.instruction is the prompt for step 1
48
+
49
+ # 2. List existing projects
50
+ node dist/src/cli/index.js list
51
+ # → newest first, with current step and chapter count
52
+
53
+ # 3. Show one project's status
54
+ node dist/src/cli/index.js status novels/<slug>
55
+
56
+ # 4. Get the next step's instruction + context
57
+ node dist/src/cli/index.js next novels/<slug>
58
+
59
+ # 5. Submit your generated content (file containing JSON or Markdown)
60
+ node dist/src/cli/index.js submit novels/<slug> --step chapter --file ch1.md
61
+
62
+ # 6. Trigger a single-chapter review
63
+ node dist/src/cli/index.js review novels/<slug> --chapter 3
64
+
65
+ # 7. Trigger a revision (feedback can be a literal string or --feedback-file)
66
+ node dist/src/cli/index.js revise novels/<slug> --chapter 3 --feedback "让节奏更紧"
67
+
68
+ # 8. Cross-chapter audit (defaults to all generated chapters)
69
+ node dist/src/cli/index.js cross-review novels/<slug> --start 1 --end 5
70
+
71
+ # 9. Lexical retrieval over chapters + bible + memory cards
72
+ node dist/src/cli/index.js retrieve novels/<slug> \
73
+ --query "昆吾剑" --top-k 8 --types chapter,memory --start 1 --end 5
74
+
75
+ # 10. Build purpose-specific context (useful for debugging prompts)
76
+ node dist/src/cli/index.js context novels/<slug> \
77
+ --purpose chapter_generation --chapter 4
78
+ ```
79
+
80
+ English projects: pass `--language en-US` to `start`. Every prompt has a parallel English form in [src/core/prompts/en-US.ts](src/core/prompts/en-US.ts).
81
+
82
+ ## Use it as an MCP server
83
+
84
+ ### Claude Code
85
+
86
+ ```jsonc
87
+ // ~/.claude.json (or your project's .mcp.json)
88
+ {
89
+ "mcpServers": {
90
+ "novelforge": {
91
+ "command": "node",
92
+ "args": ["/absolute/path/to/novelforge-agent/dist/src/mcp/server.js"],
93
+ "env": {
94
+ "NOVELFORGE_WORKSPACE": "/absolute/path/where/projects/should/live"
95
+ }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ Reload Claude Code and type:
102
+
103
+ > 我想写一本赛博修仙小说
104
+
105
+ Claude will discover the `start_novel_project` tool, call it, get back the first prompt for `novel_metadata`, generate the JSON, call `submit_step_result`, get back the next prompt, and continue autonomously until `complete`.
106
+
107
+ ### Codex CLI
108
+
109
+ ```toml
110
+ # ~/.codex/config.toml
111
+ [mcp_servers.novelforge]
112
+ command = "node"
113
+ args = ["/absolute/path/to/novelforge-agent/dist/src/mcp/server.js"]
114
+
115
+ [mcp_servers.novelforge.env]
116
+ NOVELFORGE_WORKSPACE = "/absolute/path/where/projects/should/live"
117
+ ```
118
+
119
+ ### Resuming work in a later session
120
+
121
+ `list_projects` finds every project under `NOVELFORGE_WORKSPACE/novels/`, sorted newest first. The host should call it before anything else when a session opens; pick the desired `projectPath` from the result, then `get_project_status` for a one-screen briefing, then `get_next_step` to resume.
122
+
123
+ ## Tool reference (13 MCP tools)
124
+
125
+ ### Project lifecycle
126
+ - **`start_novel_project`** `(prompt, language?, outputDir?, targetChapters?)` — create a new project under `<workspaceRoot>/<outputDir>/<slug>-<rand6>/` and return the first step's instruction.
127
+ - **`list_projects`** `(outputDir?)` — list all projects in the workspace, newest first.
128
+ - **`get_project_status`** `(projectPath)` — compact summary: current step, chapters written, open threads, latest review verdict.
129
+ - **`get_next_step`** `(projectPath)` — return the prompt + packed context for whatever the workflow expects next.
130
+
131
+ ### Workflow advancement
132
+ - **`submit_step_result`** `(projectPath, step, content)` — validate `content` against the step's zod schema, persist it, advance the state machine. On failure the bad submission is written to `.agent-recovery/failed-*.txt` and the state does not advance.
133
+ - **`get_context`** `(projectPath, purpose, chapterNumber?, start?, end?)` — build purpose-specific context without changing state. Useful when the host wants to read what the agent *would* have packed.
134
+
135
+ ### Semantic actions (verb-style; safe to call any time)
136
+ - **`generate_chapter`** `(projectPath, chapterNumber)` — return generation context for a specific chapter.
137
+ - **`extract_memory_card`** `(projectPath, chapterNumber)` — return memory-extraction context for a specific chapter.
138
+ - **`review_chapter`** `(projectPath, chapterNumber)` — switch into a single-chapter editorial review side-track and return its prompt. After `submit_step_result(step="chapter_review")`, the workflow resumes its prior step automatically.
139
+ - **`revise_chapter`** `(projectPath, chapterNumber, feedback?)` — switch into a chapter-revision side-track. Submitting `chapter_revision` content auto-archives the previous version under `chapters/.versions/`.
140
+ - **`cross_chapter_review`** `(projectPath, start?, end?)` — switch into a cross-chapter audit side-track over the given range (defaults to all generated chapters).
141
+ - **`save_chapter`** `(projectPath, chapterNumber, title, content)` — write a chapter Markdown file directly, without going through the state machine.
142
+
143
+ ### Retrieval
144
+ - **`retrieve`** `(projectPath, query, topK?, types?, chapterStart?, chapterEnd?)` — BM25-style lexical retrieval over indexed paragraphs (chapters), bible H2 sections, and memory cards. Supports CJK + Latin queries via a built-in CJK bigram tokenizer; no external embedding model.
145
+
146
+ ## Project layout
147
+
148
+ A project on disk:
149
+
150
+ ```
151
+ novels/<slug>-<rand6>/
152
+ ├── agent-state.json # workflow state (currentStep, currentChapter, files map, …)
153
+ ├── novel.json # metadata (NovelMetadataSchema)
154
+ ├── story-bible.md
155
+ ├── architecture/
156
+ │ ├── full.md
157
+ │ ├── volumes.json
158
+ │ └── chapters.json
159
+ ├── chapters/
160
+ │ ├── 001.md
161
+ │ ├── 002.md
162
+ │ └── .versions/ # archived pre-revision chapter snapshots
163
+ ├── memory/
164
+ │ └── chapter-001.json
165
+ ├── reviews/
166
+ │ ├── continuity-1-N.json
167
+ │ ├── chapter/chapter-NNN.json
168
+ │ └── cross/cross-S-E.json
169
+ ├── .index/
170
+ │ ├── lexical.json # MiniSearch serialization
171
+ │ └── manifest.json # external doc id list
172
+ └── .agent-recovery/
173
+ ├── failed-*.txt # rejected submissions kept for inspection
174
+ └── side-track.json # resume hint when in a review/revision side-track
175
+ ```
176
+
177
+ The whole directory is self-contained — copy it, share it, delete it.
178
+
179
+ ## How the workflow advances
180
+
181
+ ```
182
+ novel_metadata → story_bible → architecture → chapter
183
+
184
+ memory_card
185
+
186
+ ┌───────────────┴───────────────┐
187
+ (more chapters) (all done)
188
+ ↓ ↓
189
+ chapter continuity_review
190
+
191
+ complete
192
+ ```
193
+
194
+ Side-track steps (`chapter_review`, `chapter_revision`, `cross_chapter_review`) can be triggered at any moment via the semantic-action tools. When the side-track completes via `submit_step_result`, the workflow returns to whatever `currentStep` it was on before the side-track started.
195
+
196
+ The full transition map lives in the `next:` declaration of each handler under [src/core/steps/](src/core/steps/). To change the workflow, edit those files — that is the entire state machine, there is no graph engine.
197
+
198
+ ## Architecture
199
+
200
+ ```
201
+ src/
202
+ ├── core/ # pure domain logic, no transport
203
+ │ ├── types.ts # AgentState, WorkflowStep, MemoryCard, …
204
+ │ ├── schemas.ts # zod schemas (the only validator)
205
+ │ ├── projectStore.ts # filesystem persistence
206
+ │ ├── projectDiscovery.ts # list / status
207
+ │ ├── prompts/ # per-language prompt packs (zh-CN, en-US)
208
+ │ ├── steps/ # one file per WorkflowStep handler
209
+ │ ├── retrieval/ # BM25 index + CJK tokenizer + chunker
210
+ │ ├── contextBuilder.ts # purpose-specific context packing
211
+ │ └── workflow.ts # dispatcher: contextForStep + side-track + submit
212
+ ├── mcp/
213
+ │ ├── server.ts # stdio entrypoint
214
+ │ └── tools.ts # 13 MCP tool registrations
215
+ └── cli/
216
+ └── index.ts # equivalent CLI subcommands
217
+ ```
218
+
219
+ The agent has no LLM dependency:
220
+
221
+ ```bash
222
+ $ grep -RIl "anthropic\|openai\|@google" src package.json
223
+ # (no results)
224
+ ```
225
+
226
+ Only `@modelcontextprotocol/sdk`, `zod`, `minisearch`.
227
+
228
+ ## Adding a new workflow step
229
+
230
+ 1. Add the step name to `WorkflowStep` in [src/core/types.ts](src/core/types.ts).
231
+ 2. Add a zod schema in [src/core/schemas.ts](src/core/schemas.ts) (if the step accepts structured content).
232
+ 3. Add a prompt builder in [src/core/prompts/zh-CN.ts](src/core/prompts/zh-CN.ts) and [src/core/prompts/en-US.ts](src/core/prompts/en-US.ts).
233
+ 4. Create a handler under `src/core/steps/<name>.ts` returning a `StepApplyResult`.
234
+ 5. Register it in [src/core/steps/index.ts](src/core/steps/index.ts).
235
+ 6. (If the step needs packed context) add an entry to `CONTEXT_RECIPES` in [src/core/workflow.ts](src/core/workflow.ts).
236
+ 7. Add the step name to the `step` enum in [src/mcp/tools.ts](src/mcp/tools.ts) `submit_step_result`.
237
+
238
+ ## Design principle
239
+
240
+ The host's LLM is the only thing in this system that thinks. The agent is a pure I/O machine that knows the *order* of work, the *shape* of every artifact, and the *vocabulary* of the domain — and refuses to let the host save anything that violates those rules. Long-form fiction needs that discipline more than it needs another LLM wrapper.
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { buildContext, createProject, getNextStep, getProjectStatus, listProjects, requestSideTrack, retrieve, submitStepResult, } from '../core/index.js';
4
+ function valueAfter(args, name) {
5
+ const index = args.indexOf(name);
6
+ return index >= 0 ? args[index + 1] : undefined;
7
+ }
8
+ function parseLanguage(value) {
9
+ if (value === 'zh-CN' || value === 'en-US')
10
+ return value;
11
+ throw new Error('Invalid --language. Use zh-CN or en-US');
12
+ }
13
+ export async function runCli(argv = process.argv.slice(2), cwd = process.cwd()) {
14
+ const [command, projectPath] = argv;
15
+ if (command === 'start') {
16
+ const prompt = valueAfter(argv, '--prompt') || '';
17
+ if (!prompt.trim())
18
+ throw new Error('Missing --prompt');
19
+ const language = parseLanguage(valueAfter(argv, '--language') || 'zh-CN');
20
+ const chapters = Number(valueAfter(argv, '--chapters') || 3);
21
+ const outputDir = valueAfter(argv, '--output') || 'novels';
22
+ const result = await createProject({ workspaceRoot: cwd, prompt, language, outputDir, targetChapters: chapters });
23
+ const next = await getNextStep(result.state.projectPath);
24
+ console.log(JSON.stringify({ state: result.state, next }, null, 2));
25
+ return;
26
+ }
27
+ if (command === 'next') {
28
+ if (!projectPath)
29
+ throw new Error('Missing projectPath');
30
+ console.log(JSON.stringify(await getNextStep(projectPath), null, 2));
31
+ return;
32
+ }
33
+ if (command === 'submit') {
34
+ if (!projectPath)
35
+ throw new Error('Missing projectPath');
36
+ const step = valueAfter(argv, '--step');
37
+ const file = valueAfter(argv, '--file');
38
+ if (!step || !file)
39
+ throw new Error('Missing --step or --file');
40
+ const content = await readFile(file, 'utf8');
41
+ console.log(JSON.stringify(await submitStepResult({ projectPath, step: step, content }), null, 2));
42
+ return;
43
+ }
44
+ if (command === 'context') {
45
+ if (!projectPath)
46
+ throw new Error('Missing projectPath');
47
+ const purpose = valueAfter(argv, '--purpose') || 'chapter_generation';
48
+ const chapter = valueAfter(argv, '--chapter');
49
+ const start = valueAfter(argv, '--start');
50
+ const end = valueAfter(argv, '--end');
51
+ console.log(await buildContext({
52
+ projectPath,
53
+ purpose: purpose,
54
+ chapterNumber: chapter ? Number(chapter) : undefined,
55
+ range: start && end ? { start: Number(start), end: Number(end) } : undefined,
56
+ }));
57
+ return;
58
+ }
59
+ if (command === 'review') {
60
+ if (!projectPath)
61
+ throw new Error('Missing projectPath');
62
+ const chapter = valueAfter(argv, '--chapter');
63
+ if (!chapter)
64
+ throw new Error('Missing --chapter');
65
+ console.log(JSON.stringify(await requestSideTrack({ projectPath, step: 'chapter_review', chapterNumber: Number(chapter) }), null, 2));
66
+ return;
67
+ }
68
+ if (command === 'revise') {
69
+ if (!projectPath)
70
+ throw new Error('Missing projectPath');
71
+ const chapter = valueAfter(argv, '--chapter');
72
+ if (!chapter)
73
+ throw new Error('Missing --chapter');
74
+ const feedbackFile = valueAfter(argv, '--feedback-file');
75
+ const feedback = feedbackFile ? await readFile(feedbackFile, 'utf8') : valueAfter(argv, '--feedback');
76
+ console.log(JSON.stringify(await requestSideTrack({ projectPath, step: 'chapter_revision', chapterNumber: Number(chapter), feedback }), null, 2));
77
+ return;
78
+ }
79
+ if (command === 'cross-review') {
80
+ if (!projectPath)
81
+ throw new Error('Missing projectPath');
82
+ const start = valueAfter(argv, '--start');
83
+ const end = valueAfter(argv, '--end');
84
+ const range = start && end ? { start: Number(start), end: Number(end) } : undefined;
85
+ console.log(JSON.stringify(await requestSideTrack({ projectPath, step: 'cross_chapter_review', range }), null, 2));
86
+ return;
87
+ }
88
+ if (command === 'list') {
89
+ const outputDir = valueAfter(argv, '--output') || 'novels';
90
+ console.log(JSON.stringify(await listProjects({ workspaceRoot: cwd, outputDir }), null, 2));
91
+ return;
92
+ }
93
+ if (command === 'status') {
94
+ if (!projectPath)
95
+ throw new Error('Missing projectPath');
96
+ console.log(JSON.stringify(await getProjectStatus(projectPath), null, 2));
97
+ return;
98
+ }
99
+ if (command === 'retrieve') {
100
+ if (!projectPath)
101
+ throw new Error('Missing projectPath');
102
+ const query = valueAfter(argv, '--query');
103
+ if (!query)
104
+ throw new Error('Missing --query');
105
+ const topK = valueAfter(argv, '--top-k');
106
+ const start = valueAfter(argv, '--start');
107
+ const end = valueAfter(argv, '--end');
108
+ const typesArg = valueAfter(argv, '--types');
109
+ const types = typesArg ? typesArg.split(',') : undefined;
110
+ const hits = await retrieve(projectPath, query, {
111
+ topK: topK ? Number(topK) : undefined,
112
+ types,
113
+ chapterRange: start && end ? { start: Number(start), end: Number(end) } : undefined,
114
+ });
115
+ console.log(JSON.stringify({ query, hits }, null, 2));
116
+ return;
117
+ }
118
+ throw new Error('Usage: novelforge-agent start|list|status|next|submit|context|review|revise|cross-review|retrieve');
119
+ }
120
+ if (import.meta.url === `file://${process.argv[1]}`) {
121
+ runCli().catch((error) => {
122
+ console.error(error.message);
123
+ process.exitCode = 1;
124
+ });
125
+ }
@@ -0,0 +1,128 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { chapterFileName, chapterReviewFileName, memoryFileName } from './fileNames.js';
4
+ import { formatHits, retrieve } from './retrieval/index.js';
5
+ async function readOptional(path) {
6
+ try {
7
+ return await readFile(path, 'utf8');
8
+ }
9
+ catch {
10
+ return '';
11
+ }
12
+ }
13
+ export async function buildContext(input) {
14
+ const parts = [];
15
+ const metadata = await readOptional(join(input.projectPath, 'novel.json'));
16
+ const storyBible = await readOptional(join(input.projectPath, 'story-bible.md'));
17
+ const chaptersJson = await readOptional(join(input.projectPath, 'architecture/chapters.json'));
18
+ if (metadata)
19
+ parts.push(`## Novel Metadata\n${metadata}`);
20
+ if (storyBible)
21
+ parts.push(`## Story Bible\n${storyBible.slice(0, 4000)}`);
22
+ if (input.purpose === 'chapter_generation' && input.chapterNumber) {
23
+ let currentArchitectureForQuery;
24
+ if (chaptersJson) {
25
+ const chapters = JSON.parse(chaptersJson);
26
+ const chapter = chapters.find((item) => item.chapterNumber === input.chapterNumber);
27
+ if (chapter) {
28
+ currentArchitectureForQuery = chapter;
29
+ parts.push(`## Current Chapter Architecture\n${JSON.stringify(chapter, null, 2)}`);
30
+ }
31
+ }
32
+ if (input.chapterNumber > 1) {
33
+ const previous = await readOptional(join(input.projectPath, 'chapters', chapterFileName(input.chapterNumber - 1)));
34
+ const previousMemory = await readOptional(join(input.projectPath, 'memory', memoryFileName(input.chapterNumber - 1)));
35
+ if (previous)
36
+ parts.push(`## Previous Chapter Ending\n${previous.slice(-1600)}`);
37
+ if (previousMemory)
38
+ parts.push(`## Previous Chapter Memory\n${previousMemory}`);
39
+ const queryPieces = [];
40
+ if (currentArchitectureForQuery?.title)
41
+ queryPieces.push(currentArchitectureForQuery.title);
42
+ if (currentArchitectureForQuery?.summary)
43
+ queryPieces.push(currentArchitectureForQuery.summary);
44
+ if (currentArchitectureForQuery?.requiredBeats?.length) {
45
+ queryPieces.push(currentArchitectureForQuery.requiredBeats.join(' '));
46
+ }
47
+ const query = queryPieces.join(' ').trim();
48
+ if (query) {
49
+ const hits = await retrieve(input.projectPath, query, {
50
+ topK: 5,
51
+ chapterRange: { start: 1, end: input.chapterNumber - 1 },
52
+ });
53
+ const formatted = formatHits(hits);
54
+ if (formatted)
55
+ parts.push(`## Retrieved Relevant Snippets (lexical, BM25-style)\n${formatted}`);
56
+ }
57
+ }
58
+ }
59
+ if (input.purpose === 'memory_extraction' && input.chapterNumber) {
60
+ const chapter = await readOptional(join(input.projectPath, 'chapters', chapterFileName(input.chapterNumber)));
61
+ if (chapter)
62
+ parts.push(`## Current Chapter\n${chapter}`);
63
+ }
64
+ if (input.purpose === 'continuity_review') {
65
+ if (chaptersJson)
66
+ parts.push(`## Chapter Architecture List\n${chaptersJson}`);
67
+ const memoryParts = [];
68
+ for (let i = 1; i <= 20; i += 1) {
69
+ const memory = await readOptional(join(input.projectPath, 'memory', memoryFileName(i)));
70
+ if (memory)
71
+ memoryParts.push(`### Chapter ${i}\n${memory}`);
72
+ }
73
+ if (memoryParts.length)
74
+ parts.push(`## Memory Cards\n${memoryParts.join('\n')}`);
75
+ }
76
+ if (input.purpose === 'chapter_review' && input.chapterNumber) {
77
+ if (chaptersJson) {
78
+ const chapters = JSON.parse(chaptersJson);
79
+ const arch = chapters.find((item) => item.chapterNumber === input.chapterNumber);
80
+ if (arch)
81
+ parts.push(`## Target Chapter Architecture\n${JSON.stringify(arch, null, 2)}`);
82
+ }
83
+ const chapter = await readOptional(join(input.projectPath, 'chapters', chapterFileName(input.chapterNumber)));
84
+ if (chapter)
85
+ parts.push(`## Chapter ${input.chapterNumber} Text\n${chapter}`);
86
+ if (input.chapterNumber > 1) {
87
+ const prevMemory = await readOptional(join(input.projectPath, 'memory', memoryFileName(input.chapterNumber - 1)));
88
+ if (prevMemory)
89
+ parts.push(`## Previous Chapter Memory\n${prevMemory}`);
90
+ }
91
+ }
92
+ if (input.purpose === 'revision' && input.chapterNumber) {
93
+ const chapter = await readOptional(join(input.projectPath, 'chapters', chapterFileName(input.chapterNumber)));
94
+ if (chapter)
95
+ parts.push(`## Current Chapter Text\n${chapter}`);
96
+ const review = await readOptional(join(input.projectPath, 'reviews/chapter', chapterReviewFileName(input.chapterNumber)));
97
+ if (review)
98
+ parts.push(`## Editor Review\n${review}`);
99
+ if (input.feedback)
100
+ parts.push(`## Additional Feedback\n${input.feedback}`);
101
+ if (input.chapterNumber > 1) {
102
+ const previous = await readOptional(join(input.projectPath, 'chapters', chapterFileName(input.chapterNumber - 1)));
103
+ if (previous)
104
+ parts.push(`## Previous Chapter Ending\n${previous.slice(-1600)}`);
105
+ }
106
+ }
107
+ if (input.purpose === 'cross_chapter_review' && input.range) {
108
+ if (chaptersJson)
109
+ parts.push(`## Chapter Architecture List\n${chaptersJson}`);
110
+ const memoryParts = [];
111
+ for (let i = input.range.start; i <= input.range.end; i += 1) {
112
+ const memory = await readOptional(join(input.projectPath, 'memory', memoryFileName(i)));
113
+ if (memory)
114
+ memoryParts.push(`### Chapter ${i} Memory\n${memory}`);
115
+ }
116
+ if (memoryParts.length)
117
+ parts.push(`## Memory Cards In Range\n${memoryParts.join('\n')}`);
118
+ const tailParts = [];
119
+ for (let i = input.range.start; i <= input.range.end; i += 1) {
120
+ const chapter = await readOptional(join(input.projectPath, 'chapters', chapterFileName(i)));
121
+ if (chapter)
122
+ tailParts.push(`### Chapter ${i} Last 800 Chars\n${chapter.slice(-800)}`);
123
+ }
124
+ if (tailParts.length)
125
+ parts.push(`## Chapter Tails\n${tailParts.join('\n')}`);
126
+ }
127
+ return parts.join('\n\n').trim();
128
+ }
@@ -0,0 +1,41 @@
1
+ const PINYIN_FALLBACK = {
2
+ 星: 'xing',
3
+ 火: 'huo',
4
+ 长: 'chang',
5
+ 夜: 'ye',
6
+ };
7
+ export function makeProjectSlug(title) {
8
+ const replaced = title
9
+ .trim()
10
+ .split('')
11
+ .map((char) => PINYIN_FALLBACK[char] || char)
12
+ .join('-')
13
+ .normalize('NFKD')
14
+ .replace(/[\u0300-\u036f]/g, '')
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '');
18
+ return replaced || `novel-${Date.now()}`;
19
+ }
20
+ export function padChapterNumber(chapterNumber) {
21
+ if (!Number.isInteger(chapterNumber) || chapterNumber <= 0) {
22
+ throw new Error(`Invalid chapter number: ${chapterNumber}`);
23
+ }
24
+ return String(chapterNumber).padStart(3, '0');
25
+ }
26
+ export function chapterFileName(chapterNumber) {
27
+ return `${padChapterNumber(chapterNumber)}.md`;
28
+ }
29
+ export function memoryFileName(chapterNumber) {
30
+ return `chapter-${padChapterNumber(chapterNumber)}.json`;
31
+ }
32
+ export function chapterReviewFileName(chapterNumber) {
33
+ return `chapter-${padChapterNumber(chapterNumber)}.json`;
34
+ }
35
+ export function crossChapterReviewFileName(start, end) {
36
+ return `cross-${padChapterNumber(start)}-${padChapterNumber(end)}.json`;
37
+ }
38
+ export function chapterVersionFileName(chapterNumber, timestamp) {
39
+ const safeTs = timestamp.replace(/[:.]/g, '-');
40
+ return `${padChapterNumber(chapterNumber)}.${safeTs}.md`;
41
+ }
@@ -0,0 +1,9 @@
1
+ export * from './types.js';
2
+ export * from './schemas.js';
3
+ export * from './fileNames.js';
4
+ export * from './projectStore.js';
5
+ export * from './workflow.js';
6
+ export * from './contextBuilder.js';
7
+ export * from './prompts.js';
8
+ export * from './retrieval/index.js';
9
+ export * from './projectDiscovery.js';