novelforge-agent 0.2.0 → 0.4.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 (40) hide show
  1. package/README.md +17 -13
  2. package/dist/src/cli/index.js +10 -2
  3. package/dist/src/core/contextBuilder.js +32 -0
  4. package/dist/src/core/fileNames.js +3 -3
  5. package/dist/src/core/projectDiscovery.js +1 -0
  6. package/dist/src/core/projectOps.js +7 -1
  7. package/dist/src/core/projectStore.js +4 -1
  8. package/dist/src/core/prompts/en-US.js +130 -3
  9. package/dist/src/core/prompts/zh-CN.js +130 -3
  10. package/dist/src/core/schemas.js +23 -0
  11. package/dist/src/core/steps/architectureExtension.js +72 -0
  12. package/dist/src/core/steps/chapterReview.js +2 -1
  13. package/dist/src/core/steps/index.js +4 -0
  14. package/dist/src/core/steps/memoryCard.js +22 -1
  15. package/dist/src/core/steps/novelMetadata.js +47 -2
  16. package/dist/src/core/steps/storyBible.js +1 -1
  17. package/dist/src/core/steps/styleGuide.js +12 -0
  18. package/dist/src/core/workflow.js +2 -0
  19. package/dist/src/mcp/tools.js +36 -8
  20. package/package.json +1 -1
  21. package/src/cli/index.ts +11 -3
  22. package/src/core/contextBuilder.ts +30 -0
  23. package/src/core/fileNames.ts +3 -3
  24. package/src/core/projectDiscovery.ts +2 -0
  25. package/src/core/projectOps.ts +9 -1
  26. package/src/core/projectStore.ts +8 -1
  27. package/src/core/prompts/en-US.ts +132 -3
  28. package/src/core/prompts/types.ts +2 -0
  29. package/src/core/prompts/zh-CN.ts +132 -3
  30. package/src/core/schemas.ts +25 -0
  31. package/src/core/steps/architectureExtension.ts +88 -0
  32. package/src/core/steps/chapterReview.ts +2 -1
  33. package/src/core/steps/index.ts +4 -0
  34. package/src/core/steps/memoryCard.ts +23 -1
  35. package/src/core/steps/novelMetadata.ts +48 -2
  36. package/src/core/steps/storyBible.ts +1 -1
  37. package/src/core/steps/styleGuide.ts +13 -0
  38. package/src/core/types.ts +32 -0
  39. package/src/core/workflow.ts +2 -0
  40. package/src/mcp/tools.ts +35 -8
@@ -1,15 +1,61 @@
1
+ import { access, rename } from 'node:fs/promises';
2
+ import { basename, dirname, join } from 'node:path';
1
3
  import { NovelMetadataSchema } from '../schemas.js';
2
4
  import { saveJsonFile } from '../projectStore.js';
3
5
  import { initializeCharacterStates } from '../characterStore.js';
6
+ import { makeProjectSlug } from '../fileNames.js';
4
7
  import { StepHandler, parseJson } from './types.js';
5
8
 
9
+ async function pathExists(path: string): Promise<boolean> {
10
+ try {
11
+ await access(path);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ async function uniqueProjectPath(parentDir: string, baseName: string, currentPath: string): Promise<string> {
19
+ let candidate = join(parentDir, baseName);
20
+ if (candidate === currentPath || !(await pathExists(candidate))) return candidate;
21
+
22
+ for (let index = 2; index < 100; index += 1) {
23
+ candidate = join(parentDir, `${baseName}-${index}`);
24
+ if (candidate === currentPath || !(await pathExists(candidate))) return candidate;
25
+ }
26
+
27
+ throw new Error(`Unable to find available project directory for ${baseName}`);
28
+ }
29
+
30
+ async function renameProjectForTitle(projectPath: string, title: string): Promise<string> {
31
+ const parentDir = dirname(projectPath);
32
+ const currentName = basename(projectPath);
33
+ const suffix = currentName.match(/-([a-f0-9]{6})$/i)?.[1];
34
+ const titleSlug = makeProjectSlug(title);
35
+ const nextName = suffix ? `${titleSlug}-${suffix}` : titleSlug;
36
+ if (nextName === currentName) return projectPath;
37
+
38
+ const nextPath = await uniqueProjectPath(parentDir, nextName, projectPath);
39
+ if (nextPath === projectPath) return projectPath;
40
+ await rename(projectPath, nextPath);
41
+ return nextPath;
42
+ }
43
+
6
44
  export const novelMetadataHandler: StepHandler = async (state, content) => {
7
45
  const parsed = NovelMetadataSchema.parse(parseJson(content));
8
46
  const path = await saveJsonFile(state.projectPath, 'novel.json', parsed);
9
47
  const charactersPath = await initializeCharacterStates(state.projectPath, parsed.coreCast);
48
+ const projectPath = await renameProjectForTitle(state.projectPath, parsed.title);
10
49
  return {
11
- savedPaths: [path, charactersPath],
50
+ savedPaths: [
51
+ projectPath === state.projectPath ? path : join(projectPath, 'novel.json'),
52
+ projectPath === state.projectPath ? charactersPath : join(projectPath, 'characters.json'),
53
+ ],
12
54
  fileEntries: { novel: 'novel.json', characters: 'characters.json' },
13
- next: { kind: 'linear', nextStep: 'story_bible' },
55
+ next: {
56
+ kind: 'linear',
57
+ nextStep: 'story_bible',
58
+ statePatch: { projectPath },
59
+ },
14
60
  };
15
61
  };
@@ -9,6 +9,6 @@ export const storyBibleHandler: StepHandler = async (state, content) => {
9
9
  return {
10
10
  savedPaths: [path],
11
11
  fileEntries: { storyBible: 'story-bible.md' },
12
- next: { kind: 'linear', nextStep: 'architecture' },
12
+ next: { kind: 'linear', nextStep: 'style_guide' },
13
13
  };
14
14
  };
@@ -0,0 +1,13 @@
1
+ import { StyleGuideSchema } from '../schemas.js';
2
+ import { saveJsonFile } from '../projectStore.js';
3
+ import { StepHandler, parseJson } from './types.js';
4
+
5
+ export const styleGuideHandler: StepHandler = async (state, content) => {
6
+ const parsed = StyleGuideSchema.parse(parseJson(content));
7
+ const path = await saveJsonFile(state.projectPath, 'style-guide.json', parsed);
8
+ return {
9
+ savedPaths: [path],
10
+ fileEntries: { styleGuide: 'style-guide.json' },
11
+ next: { kind: 'linear', nextStep: 'architecture' },
12
+ };
13
+ };
package/src/core/types.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export type WorkflowStep =
2
2
  | 'novel_metadata'
3
3
  | 'story_bible'
4
+ | 'style_guide'
4
5
  | 'architecture'
6
+ | 'architecture_extension'
5
7
  | 'chapter'
6
8
  | 'memory_card'
7
9
  | 'continuity_review'
@@ -44,6 +46,7 @@ export interface ChapterAcceptanceGate {
44
46
  characterProgress: ChapterAcceptanceCheck;
45
47
  foreshadowProgress: ChapterAcceptanceCheck;
46
48
  storyBibleConsistency: ChapterAcceptanceCheck;
49
+ proseRhythm: ChapterAcceptanceCheck;
47
50
  endingHook: ChapterAcceptanceCheck;
48
51
  repetition: ChapterAcceptanceCheck;
49
52
  }
@@ -82,6 +85,23 @@ export interface NovelMetadata {
82
85
  coreCast: CoreCastMember[];
83
86
  }
84
87
 
88
+ export interface StyleGuide {
89
+ narrativeVoice: string;
90
+ pacing: string;
91
+ diction: string;
92
+ dialogueRules: string[];
93
+ prohibitedPatterns: string[];
94
+ proseRhythm: {
95
+ sentenceRhythm: string;
96
+ paragraphing: string;
97
+ interiorityMode: string;
98
+ emphasisBudget: string;
99
+ antiPatterns: string[];
100
+ };
101
+ sampleParagraph: string;
102
+ consistencyChecks: string[];
103
+ }
104
+
85
105
  export interface VolumeArchitecture {
86
106
  id: string;
87
107
  title: string;
@@ -121,6 +141,13 @@ export interface ArchitecturePayload {
121
141
  chapters: ChapterArchitecture[];
122
142
  }
123
143
 
144
+ export interface ArchitectureExtensionPayload {
145
+ fullUpdate?: string;
146
+ volumes?: VolumeArchitecture[];
147
+ volumePacing?: VolumePacingBoard[];
148
+ chapters: ChapterArchitecture[];
149
+ }
150
+
124
151
  export type ThreadStatus = 'planted' | 'building' | 'paid' | 'dropped';
125
152
 
126
153
  export type ThreadActionKind = 'plant' | 'build' | 'pay' | 'drop';
@@ -196,7 +223,12 @@ export interface AgentState {
196
223
  projectPath: string;
197
224
  initialPrompt: string;
198
225
  language: 'zh-CN' | 'en-US';
226
+ /**
227
+ * Number of chapters to plan in each architecture batch.
228
+ * The whole-book target lives in plannedTotalChapters.
229
+ */
199
230
  targetChapters: number;
231
+ plannedTotalChapters: number;
200
232
  currentStep: WorkflowStep;
201
233
  currentChapter: number;
202
234
  completedSteps: WorkflowStep[];
@@ -37,6 +37,8 @@ export interface RequestSideTrackInput {
37
37
  type ContextRecipe = (state: AgentState) => Omit<BuildContextInput, 'projectPath'>;
38
38
 
39
39
  const CONTEXT_RECIPES: Partial<Record<WorkflowStep, ContextRecipe>> = {
40
+ style_guide: () => ({ purpose: 'style_guide' }),
41
+ architecture_extension: (s) => ({ purpose: 'architecture_extension', chapterNumber: s.currentChapter }),
40
42
  chapter: (s) => ({ purpose: 'chapter_generation', chapterNumber: s.currentChapter }),
41
43
  memory_card: (s) => ({ purpose: 'memory_extraction', chapterNumber: s.currentChapter }),
42
44
  continuity_review: () => ({ purpose: 'continuity_review' }),
package/src/mcp/tools.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { resolve } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
4
6
  import {
5
7
  amendStoryBible,
6
8
  assertProjectPath,
@@ -21,7 +23,24 @@ import {
21
23
  updateThread,
22
24
  } from '../core/index.js';
23
25
 
24
- const MCP_SERVER_VERSION = '0.2.0';
26
+ function packageVersion(): string {
27
+ let dir = dirname(fileURLToPath(import.meta.url));
28
+ while (true) {
29
+ try {
30
+ const raw = readFileSync(join(dir, 'package.json'), 'utf8');
31
+ const parsed = JSON.parse(raw) as { version?: unknown };
32
+ if (typeof parsed.version === 'string' && parsed.version) return parsed.version;
33
+ } catch {
34
+ // keep walking upward until the package root is found
35
+ }
36
+
37
+ const parent = dirname(dir);
38
+ if (parent === dir) return '0.0.0';
39
+ dir = parent;
40
+ }
41
+ }
42
+
43
+ const MCP_SERVER_VERSION = packageVersion();
25
44
 
26
45
  export interface CreateNovelAgentServerOptions {
27
46
  workspaceRoot: string;
@@ -59,15 +78,17 @@ export function createNovelAgentServer(options: CreateNovelAgentServerOptions):
59
78
  prompt: z.string().min(1),
60
79
  language: z.enum(['zh-CN', 'en-US']).default('zh-CN'),
61
80
  outputDir: z.string().default('novels'),
62
- targetChapters: z.number().int().positive().default(3),
81
+ targetChapters: z.number().int().positive().default(5),
82
+ plannedTotalChapters: z.number().int().positive().default(12),
63
83
  },
64
- async ({ prompt, language, outputDir, targetChapters }) => {
84
+ async ({ prompt, language, outputDir, targetChapters, plannedTotalChapters }) => {
65
85
  const result = await createProject({
66
86
  workspaceRoot: options.workspaceRoot,
67
87
  prompt,
68
88
  language,
69
89
  outputDir,
70
90
  targetChapters,
91
+ plannedTotalChapters,
71
92
  });
72
93
  return textResult({ state: result.state, next: await getNextStep(result.state.projectPath) });
73
94
  }
@@ -105,7 +126,9 @@ export function createNovelAgentServer(options: CreateNovelAgentServerOptions):
105
126
  step: z.enum([
106
127
  'novel_metadata',
107
128
  'story_bible',
129
+ 'style_guide',
108
130
  'architecture',
131
+ 'architecture_extension',
109
132
  'chapter',
110
133
  'memory_card',
111
134
  'continuity_review',
@@ -127,6 +150,8 @@ export function createNovelAgentServer(options: CreateNovelAgentServerOptions):
127
150
  projectPath: z.string().min(1),
128
151
  purpose: z.enum([
129
152
  'chapter_generation',
153
+ 'style_guide',
154
+ 'architecture_extension',
130
155
  'memory_extraction',
131
156
  'continuity_review',
132
157
  'revision',
@@ -336,6 +361,7 @@ export function createNovelAgentServer(options: CreateNovelAgentServerOptions):
336
361
  step: z.enum([
337
362
  'novel_metadata',
338
363
  'story_bible',
364
+ 'style_guide',
339
365
  'architecture',
340
366
  'chapter',
341
367
  'memory_card',
@@ -354,14 +380,15 @@ export function createNovelAgentServer(options: CreateNovelAgentServerOptions):
354
380
  'Start a brand new novel project under the configured workspace.',
355
381
  {
356
382
  prompt: z.string().describe('User idea / premise / genre, in any language.'),
357
- chapters: z.string().optional().describe('Target number of chapters as a string. Defaults to 5.'),
383
+ chapters: z.string().optional().describe('Planning batch size as a string. Defaults to 5.'),
384
+ totalChapters: z.string().optional().describe('Whole-book target chapter count as a string. Defaults to 12.'),
358
385
  },
359
- ({ prompt, chapters }) => ({
386
+ ({ prompt, chapters, totalChapters }) => ({
360
387
  messages: [{
361
388
  role: 'user' as const,
362
389
  content: {
363
390
  type: 'text' as const,
364
- text: `Use the novelforge MCP server. Call start_novel_project with prompt="${prompt}", targetChapters=${chapters ?? '5'}, then enter the autonomous loop: read next.instruction, generate the requested content, call submit_step_result, repeat until currentStep is "complete". Show me the projectPath after start_novel_project returns.`,
391
+ text: `Use the novelforge MCP server. Call start_novel_project with prompt="${prompt}", targetChapters=${chapters ?? '5'}, plannedTotalChapters=${totalChapters ?? '12'}, then enter the autonomous loop: read next.instruction, generate the requested content, call submit_step_result, repeat until currentStep is "complete". Show me the projectPath after start_novel_project returns.`,
365
392
  },
366
393
  }],
367
394
  })
@@ -393,7 +420,7 @@ export function createNovelAgentServer(options: CreateNovelAgentServerOptions):
393
420
  role: 'user' as const,
394
421
  content: {
395
422
  type: 'text' as const,
396
- text: 'Use the novelforge MCP server: call list_projects with no arguments. Show me the result in a compact table (title, currentStep, chaptersWritten/targetChapters, updatedAt, projectPath).',
423
+ text: 'Use the novelforge MCP server: call list_projects with no arguments. Show me the result in a compact table (title, currentStep, chaptersWritten/plannedTotalChapters, updatedAt, projectPath).',
397
424
  },
398
425
  }],
399
426
  })