@echoes-io/mcp-server 3.0.0 → 4.0.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 (46) hide show
  1. package/README.md +2 -2
  2. package/package.json +19 -9
  3. package/cli/index.d.ts +0 -2
  4. package/cli/index.js +0 -6
  5. package/lib/prompts/handlers.d.ts +0 -21
  6. package/lib/prompts/handlers.js +0 -107
  7. package/lib/prompts/index.d.ts +0 -2
  8. package/lib/prompts/index.js +0 -2
  9. package/lib/prompts/substitution.d.ts +0 -2
  10. package/lib/prompts/substitution.js +0 -45
  11. package/lib/prompts/validation.d.ts +0 -8
  12. package/lib/prompts/validation.js +0 -20
  13. package/lib/server.d.ts +0 -55
  14. package/lib/server.js +0 -305
  15. package/lib/tools/book-generate.d.ts +0 -20
  16. package/lib/tools/book-generate.js +0 -36
  17. package/lib/tools/chapter-delete.d.ts +0 -15
  18. package/lib/tools/chapter-delete.js +0 -48
  19. package/lib/tools/chapter-info.d.ts +0 -14
  20. package/lib/tools/chapter-info.js +0 -47
  21. package/lib/tools/chapter-insert.d.ts +0 -21
  22. package/lib/tools/chapter-insert.js +0 -104
  23. package/lib/tools/chapter-refresh.d.ts +0 -12
  24. package/lib/tools/chapter-refresh.js +0 -78
  25. package/lib/tools/episode-info.d.ts +0 -13
  26. package/lib/tools/episode-info.js +0 -45
  27. package/lib/tools/episode-update.d.ts +0 -16
  28. package/lib/tools/episode-update.js +0 -42
  29. package/lib/tools/index.d.ts +0 -14
  30. package/lib/tools/index.js +0 -14
  31. package/lib/tools/rag-characters.d.ts +0 -12
  32. package/lib/tools/rag-characters.js +0 -26
  33. package/lib/tools/rag-context.d.ts +0 -16
  34. package/lib/tools/rag-context.js +0 -54
  35. package/lib/tools/rag-index.d.ts +0 -18
  36. package/lib/tools/rag-index.js +0 -89
  37. package/lib/tools/rag-search.d.ts +0 -17
  38. package/lib/tools/rag-search.js +0 -58
  39. package/lib/tools/stats.d.ts +0 -14
  40. package/lib/tools/stats.js +0 -132
  41. package/lib/tools/timeline-sync.d.ts +0 -15
  42. package/lib/tools/timeline-sync.js +0 -217
  43. package/lib/tools/words-count.d.ts +0 -10
  44. package/lib/tools/words-count.js +0 -30
  45. package/lib/utils.d.ts +0 -1
  46. package/lib/utils.js +0 -2
package/lib/server.js DELETED
@@ -1,305 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { RAGSystem } from '@echoes-io/rag';
5
- import { Tracker } from '@echoes-io/tracker';
6
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
- import { toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js';
9
- import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
10
- import { getPrompt, listPrompts, validateGitHubRepo } from './prompts/index.js';
11
- import { bookGenerate, bookGenerateSchema, chapterDelete, chapterDeleteSchema, chapterInfo, chapterInfoSchema, chapterInsert, chapterInsertSchema, chapterRefresh, chapterRefreshSchema, episodeInfo, episodeInfoSchema, episodeUpdate, episodeUpdateSchema, ragCharacters, ragCharactersSchema, ragContext, ragContextSchema, ragIndex, ragIndexSchema, ragSearch, ragSearchSchema, stats, statsSchema, timelineSync, timelineSyncSchema, wordsCount, wordsCountSchema, } from './tools/index.js';
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
14
- export function createServer(timelines) {
15
- const server = new Server({
16
- name: pkg.name,
17
- version: pkg.version,
18
- }, {
19
- capabilities: {
20
- tools: {},
21
- prompts: {},
22
- },
23
- });
24
- server.setRequestHandler(ListToolsRequestSchema, async () => {
25
- return {
26
- tools: [
27
- {
28
- name: 'words-count',
29
- description: 'Count words and text statistics in a markdown file',
30
- inputSchema: toJsonSchemaCompat(wordsCountSchema),
31
- },
32
- {
33
- name: 'chapter-info',
34
- description: 'Extract chapter metadata, content preview, and statistics',
35
- inputSchema: toJsonSchemaCompat(chapterInfoSchema),
36
- },
37
- {
38
- name: 'episode-info',
39
- description: 'Get episode information and list of chapters',
40
- inputSchema: toJsonSchemaCompat(episodeInfoSchema),
41
- },
42
- {
43
- name: 'episode-update',
44
- description: 'Update episode metadata (description, title, slug)',
45
- inputSchema: toJsonSchemaCompat(episodeUpdateSchema),
46
- },
47
- {
48
- name: 'chapter-refresh',
49
- description: 'Refresh chapter metadata and statistics from file',
50
- inputSchema: toJsonSchemaCompat(chapterRefreshSchema),
51
- },
52
- {
53
- name: 'chapter-delete',
54
- description: 'Delete chapter from database and optionally from filesystem',
55
- inputSchema: toJsonSchemaCompat(chapterDeleteSchema),
56
- },
57
- {
58
- name: 'chapter-insert',
59
- description: 'Insert new chapter and automatically renumber subsequent chapters',
60
- inputSchema: toJsonSchemaCompat(chapterInsertSchema),
61
- },
62
- {
63
- name: 'timeline-sync',
64
- description: 'Synchronize timeline content with database',
65
- inputSchema: toJsonSchemaCompat(timelineSyncSchema),
66
- },
67
- {
68
- name: 'stats',
69
- description: 'Get statistics for timeline, arc, episode, or POV',
70
- inputSchema: toJsonSchemaCompat(statsSchema),
71
- },
72
- {
73
- name: 'rag-index',
74
- description: 'Index chapters into RAG vector database for semantic search',
75
- inputSchema: toJsonSchemaCompat(ragIndexSchema),
76
- },
77
- {
78
- name: 'rag-search',
79
- description: 'Semantic search across timeline content',
80
- inputSchema: toJsonSchemaCompat(ragSearchSchema),
81
- },
82
- {
83
- name: 'rag-context',
84
- description: 'Retrieve relevant context for AI interactions',
85
- inputSchema: toJsonSchemaCompat(ragContextSchema),
86
- },
87
- {
88
- name: 'rag-characters',
89
- description: 'Get all characters that appear in chapters with a specific character',
90
- inputSchema: toJsonSchemaCompat(ragCharactersSchema),
91
- },
92
- {
93
- name: 'book-generate',
94
- description: 'Generate PDF book from timeline content using LaTeX',
95
- inputSchema: toJsonSchemaCompat(bookGenerateSchema),
96
- },
97
- ],
98
- };
99
- });
100
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
101
- const { name, arguments: args } = request.params;
102
- // Helper to get timeline context
103
- const getContext = (timeline) => {
104
- const ctx = timelines.get(timeline);
105
- if (!ctx) {
106
- throw new Error(`Timeline "${timeline}" not found. Available: ${Array.from(timelines.keys()).join(', ')}`);
107
- }
108
- return ctx;
109
- };
110
- switch (name) {
111
- case 'words-count':
112
- return await wordsCount(wordsCountSchema.parse(args));
113
- case 'chapter-info': {
114
- const parsed = chapterInfoSchema.parse(args);
115
- const { tracker } = getContext(parsed.timeline);
116
- return await chapterInfo(parsed, tracker);
117
- }
118
- case 'chapter-refresh': {
119
- const parsed = chapterRefreshSchema.parse(args);
120
- const { tracker } = getContext(parsed.timeline);
121
- return await chapterRefresh(parsed, tracker);
122
- }
123
- case 'chapter-delete': {
124
- const parsed = chapterDeleteSchema.parse(args);
125
- const { tracker } = getContext(parsed.timeline);
126
- return await chapterDelete(parsed, tracker);
127
- }
128
- case 'chapter-insert': {
129
- const parsed = chapterInsertSchema.parse(args);
130
- const { tracker } = getContext(parsed.timeline);
131
- return await chapterInsert(parsed, tracker);
132
- }
133
- case 'episode-info': {
134
- const parsed = episodeInfoSchema.parse(args);
135
- const { tracker } = getContext(parsed.timeline);
136
- return await episodeInfo(parsed, tracker);
137
- }
138
- case 'episode-update': {
139
- const parsed = episodeUpdateSchema.parse(args);
140
- const { tracker } = getContext(parsed.timeline);
141
- return await episodeUpdate(parsed, tracker);
142
- }
143
- case 'timeline-sync': {
144
- const parsed = timelineSyncSchema.parse(args);
145
- const { tracker, contentPath } = getContext(parsed.timeline);
146
- return await timelineSync({ ...parsed, contentPath }, tracker);
147
- }
148
- case 'stats': {
149
- const parsed = statsSchema.parse(args);
150
- const { tracker } = getContext(parsed.timeline);
151
- return await stats(parsed, tracker);
152
- }
153
- case 'rag-index': {
154
- const parsed = ragIndexSchema.parse(args);
155
- const { tracker, rag, contentPath } = getContext(parsed.timeline);
156
- return await ragIndex({ ...parsed, contentPath }, tracker, rag);
157
- }
158
- case 'rag-search': {
159
- const parsed = ragSearchSchema.parse(args);
160
- const { rag } = getContext(parsed.timeline);
161
- return await ragSearch(parsed, rag);
162
- }
163
- case 'rag-context': {
164
- const parsed = ragContextSchema.parse(args);
165
- const { rag } = getContext(parsed.timeline);
166
- return await ragContext(parsed, rag);
167
- }
168
- case 'rag-characters': {
169
- const parsed = ragCharactersSchema.parse(args);
170
- const { rag } = getContext(parsed.timeline);
171
- return await ragCharacters(parsed, rag);
172
- }
173
- case 'book-generate': {
174
- const parsed = bookGenerateSchema.parse(args);
175
- const { contentPath } = getContext(parsed.timeline);
176
- return await bookGenerate({ ...parsed, contentPath });
177
- }
178
- default:
179
- throw new Error(`Unknown tool: ${name}`);
180
- }
181
- });
182
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
183
- return listPrompts();
184
- });
185
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
186
- const { name, arguments: args } = request.params;
187
- // Get timeline from first available timeline (single-timeline mode)
188
- // or require timeline in args for multi-timeline mode
189
- const timelineNames = Array.from(timelines.keys());
190
- if (timelineNames.length === 0) {
191
- throw new Error('No timelines available');
192
- }
193
- // For single-timeline mode, use the only timeline
194
- // For multi-timeline mode, this would need timeline in args
195
- const timeline = timelineNames[0];
196
- const { tracker } = timelines.get(timeline);
197
- return await getPrompt(name, args || {}, timeline, tracker);
198
- });
199
- return server;
200
- }
201
- export async function runServer() {
202
- const { readdirSync, existsSync } = await import('node:fs');
203
- const { join, basename } = await import('node:path');
204
- const cwd = process.cwd();
205
- const cwdName = basename(cwd);
206
- const timelines = new Map();
207
- const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';
208
- const log = (...args) => {
209
- if (!isTest)
210
- console.error(...args);
211
- };
212
- log(`[DEBUG] Starting from: ${cwd}`);
213
- if (process.env.NODE_ENV === 'test') {
214
- // Test mode: in-memory databases
215
- const tracker = new Tracker(':memory:');
216
- await tracker.init();
217
- const rag = new RAGSystem({
218
- provider: 'qwen3',
219
- dbPath: ':memory:',
220
- });
221
- timelines.set('test', { tracker, rag, contentPath: './test-content' });
222
- log('[DEBUG] Mode: test (in-memory)');
223
- }
224
- else if (cwdName.startsWith('timeline-')) {
225
- // Single timeline mode: running from timeline directory
226
- const timelineName = cwdName.replace('timeline-', '');
227
- const contentPath = join(cwd, 'content');
228
- if (!existsSync(contentPath)) {
229
- throw new Error(`No content directory found in ${cwd}`);
230
- }
231
- const trackerPath = join(cwd, 'tracker.db');
232
- const ragPath = join(cwd, 'lancedb');
233
- const tracker = new Tracker(trackerPath);
234
- await tracker.init();
235
- const provider = (process.env.ECHOES_RAG_PROVIDER || 'qwen3');
236
- const rag = new RAGSystem({
237
- provider,
238
- dbPath: ragPath,
239
- geminiApiKey: process.env.ECHOES_GEMINI_API_KEY,
240
- });
241
- timelines.set(timelineName, { tracker, rag, contentPath });
242
- log(`[DEBUG] Mode: single-timeline "${timelineName}"`);
243
- log(`[DEBUG] Content: ${contentPath}`);
244
- log(`[DEBUG] Tracker: ${trackerPath}`);
245
- log(`[DEBUG] RAG: ${ragPath}`);
246
- }
247
- else if (cwdName === 'mcp-server') {
248
- // Test mode from mcp-server directory: in-memory
249
- const tracker = new Tracker(':memory:');
250
- await tracker.init();
251
- const rag = new RAGSystem({
252
- provider: 'qwen3',
253
- dbPath: ':memory:',
254
- });
255
- timelines.set('test', { tracker, rag, contentPath: './test-content' });
256
- log('[DEBUG] Mode: test from mcp-server (in-memory)');
257
- }
258
- else {
259
- // Multi-timeline mode: discover from parent directory (backward compat for .github)
260
- const parentDir = join(cwd, '..');
261
- const entries = readdirSync(parentDir, { withFileTypes: true });
262
- log(`[DEBUG] Mode: multi-timeline (scanning ${parentDir})`);
263
- for (const entry of entries) {
264
- if (entry.isDirectory() && entry.name.startsWith('timeline-')) {
265
- const timelineName = entry.name.replace('timeline-', '');
266
- const timelinePath = join(parentDir, entry.name);
267
- const contentPath = join(timelinePath, 'content');
268
- if (!existsSync(contentPath)) {
269
- log(`[DEBUG] Skipping ${entry.name}: no content directory`);
270
- continue;
271
- }
272
- const trackerPath = join(timelinePath, 'tracker.db');
273
- const ragPath = join(timelinePath, 'lancedb');
274
- const tracker = new Tracker(trackerPath);
275
- await tracker.init();
276
- const provider = (process.env.ECHOES_RAG_PROVIDER || 'qwen3');
277
- const rag = new RAGSystem({
278
- provider,
279
- dbPath: ragPath,
280
- geminiApiKey: process.env.ECHOES_GEMINI_API_KEY,
281
- });
282
- timelines.set(timelineName, { tracker, rag, contentPath });
283
- log(`[DEBUG] Timeline "${timelineName}": ${trackerPath}`);
284
- }
285
- }
286
- if (timelines.size === 0) {
287
- throw new Error('No timelines found in parent directory');
288
- }
289
- }
290
- const server = createServer(timelines);
291
- const transport = new StdioServerTransport();
292
- await server.connect(transport);
293
- log(`[DEBUG] Server ready with ${timelines.size} timeline(s)`);
294
- // Validate .github repo for prompts
295
- const { exists: githubExists } = validateGitHubRepo();
296
- if (!githubExists) {
297
- log('⚠️ WARNING: .github repository not found');
298
- log(' Expected location: ../.github/.kiro/prompts/');
299
- log(' MCP prompts will not work until .github repo is cloned as sibling.');
300
- log(' Clone: git clone https://github.com/echoes-io/.github ../.github');
301
- }
302
- else {
303
- log('✓ .github repository found');
304
- }
305
- }
@@ -1,20 +0,0 @@
1
- import { z } from 'zod';
2
- export declare const bookGenerateSchema: z.ZodObject<{
3
- timeline: z.ZodString;
4
- outputPath: z.ZodString;
5
- episodes: z.ZodOptional<z.ZodString>;
6
- format: z.ZodOptional<z.ZodEnum<{
7
- a4: "a4";
8
- a5: "a5";
9
- }>>;
10
- }, z.core.$strip>;
11
- type BookGenerateArgs = z.infer<typeof bookGenerateSchema> & {
12
- contentPath: string;
13
- };
14
- export declare function bookGenerate(args: BookGenerateArgs): Promise<{
15
- content: {
16
- type: "text";
17
- text: string;
18
- }[];
19
- }>;
20
- export {};
@@ -1,36 +0,0 @@
1
- import { generateBook } from '@echoes-io/books-generator';
2
- import { z } from 'zod';
3
- export const bookGenerateSchema = z.object({
4
- timeline: z.string().describe('Timeline name'),
5
- outputPath: z.string().describe('Output PDF file path'),
6
- episodes: z.string().optional().describe('Comma-separated episode numbers (e.g., "1,2,3")'),
7
- format: z.enum(['a4', 'a5']).optional().describe('Page format (default: a4)'),
8
- });
9
- export async function bookGenerate(args) {
10
- try {
11
- await generateBook({
12
- contentPath: args.contentPath,
13
- outputPath: args.outputPath,
14
- timeline: args.timeline,
15
- episodes: args.episodes,
16
- format: args.format || 'a4',
17
- });
18
- return {
19
- content: [
20
- {
21
- type: 'text',
22
- text: JSON.stringify({
23
- success: true,
24
- timeline: args.timeline,
25
- outputPath: args.outputPath,
26
- episodes: args.episodes || 'all',
27
- format: args.format || 'a4',
28
- }, null, 2),
29
- },
30
- ],
31
- };
32
- }
33
- catch (error) {
34
- throw new Error(`Failed to generate book: ${error instanceof Error ? error.message : 'Unknown error'}`);
35
- }
36
- }
@@ -1,15 +0,0 @@
1
- import type { Tracker } from '@echoes-io/tracker';
2
- import { z } from 'zod';
3
- export declare const chapterDeleteSchema: z.ZodObject<{
4
- timeline: z.ZodString;
5
- arc: z.ZodString;
6
- episode: z.ZodNumber;
7
- chapter: z.ZodNumber;
8
- file: z.ZodOptional<z.ZodString>;
9
- }, z.core.$strip>;
10
- export declare function chapterDelete(args: z.infer<typeof chapterDeleteSchema>, tracker: Tracker): Promise<{
11
- content: {
12
- type: "text";
13
- text: string;
14
- }[];
15
- }>;
@@ -1,48 +0,0 @@
1
- import { unlinkSync } from 'node:fs';
2
- import { z } from 'zod';
3
- export const chapterDeleteSchema = z.object({
4
- timeline: z.string().describe('Timeline name'),
5
- arc: z.string().describe('Arc name'),
6
- episode: z.number().describe('Episode number'),
7
- chapter: z.number().describe('Chapter number'),
8
- file: z.string().optional().describe('Path to markdown file to delete from filesystem'),
9
- });
10
- export async function chapterDelete(args, tracker) {
11
- try {
12
- const existing = await tracker.getChapter(args.timeline, args.arc, args.episode, args.chapter);
13
- if (!existing) {
14
- throw new Error(`Chapter not found: ${args.timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
15
- }
16
- await tracker.deleteChapter(args.timeline, args.arc, args.episode, args.chapter);
17
- let fileDeleted = false;
18
- if (args.file) {
19
- unlinkSync(args.file);
20
- fileDeleted = true;
21
- }
22
- return {
23
- content: [
24
- {
25
- type: 'text',
26
- text: JSON.stringify({
27
- timeline: args.timeline,
28
- arc: args.arc,
29
- episode: args.episode,
30
- chapter: args.chapter,
31
- deleted: {
32
- pov: existing.pov,
33
- title: existing.title,
34
- words: existing.words,
35
- },
36
- fileDeleted,
37
- message: fileDeleted
38
- ? 'Chapter deleted from database and filesystem'
39
- : 'Chapter deleted from database only',
40
- }, null, 2),
41
- },
42
- ],
43
- };
44
- }
45
- catch (error) {
46
- throw new Error(`Failed to delete chapter: ${error instanceof Error ? error.message : 'Unknown error'}`);
47
- }
48
- }
@@ -1,14 +0,0 @@
1
- import type { Tracker } from '@echoes-io/tracker';
2
- import { z } from 'zod';
3
- export declare const chapterInfoSchema: z.ZodObject<{
4
- timeline: z.ZodString;
5
- arc: z.ZodString;
6
- episode: z.ZodNumber;
7
- chapter: z.ZodNumber;
8
- }, z.core.$strip>;
9
- export declare function chapterInfo(args: z.infer<typeof chapterInfoSchema>, tracker: Tracker): Promise<{
10
- content: {
11
- type: "text";
12
- text: string;
13
- }[];
14
- }>;
@@ -1,47 +0,0 @@
1
- import { z } from 'zod';
2
- export const chapterInfoSchema = z.object({
3
- timeline: z.string().describe('Timeline name'),
4
- arc: z.string().describe('Arc name'),
5
- episode: z.number().describe('Episode number'),
6
- chapter: z.number().describe('Chapter number'),
7
- });
8
- export async function chapterInfo(args, tracker) {
9
- try {
10
- const chapter = await tracker.getChapter(args.timeline, args.arc, args.episode, args.chapter);
11
- if (!chapter) {
12
- throw new Error(`Chapter not found: ${args.timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
13
- }
14
- return {
15
- content: [
16
- {
17
- type: 'text',
18
- text: JSON.stringify({
19
- timeline: args.timeline,
20
- arc: args.arc,
21
- episode: args.episode,
22
- chapter: args.chapter,
23
- metadata: {
24
- pov: chapter.pov,
25
- title: chapter.title,
26
- date: chapter.date,
27
- summary: chapter.summary,
28
- location: chapter.location,
29
- outfit: chapter.outfit,
30
- kink: chapter.kink,
31
- },
32
- stats: {
33
- words: chapter.words,
34
- characters: chapter.characters,
35
- charactersNoSpaces: chapter.charactersNoSpaces,
36
- paragraphs: chapter.paragraphs,
37
- sentences: chapter.sentences,
38
- },
39
- }, null, 2),
40
- },
41
- ],
42
- };
43
- }
44
- catch (error) {
45
- throw new Error(`Failed to get chapter info: ${error instanceof Error ? error.message : 'Unknown error'}`);
46
- }
47
- }
@@ -1,21 +0,0 @@
1
- import type { Tracker } from '@echoes-io/tracker';
2
- import { z } from 'zod';
3
- export declare const chapterInsertSchema: z.ZodObject<{
4
- timeline: z.ZodString;
5
- arc: z.ZodString;
6
- episode: z.ZodNumber;
7
- after: z.ZodNumber;
8
- pov: z.ZodString;
9
- title: z.ZodString;
10
- summary: z.ZodOptional<z.ZodString>;
11
- location: z.ZodOptional<z.ZodString>;
12
- outfit: z.ZodOptional<z.ZodString>;
13
- kink: z.ZodOptional<z.ZodString>;
14
- file: z.ZodOptional<z.ZodString>;
15
- }, z.core.$strip>;
16
- export declare function chapterInsert(args: z.infer<typeof chapterInsertSchema>, tracker: Tracker): Promise<{
17
- content: {
18
- type: "text";
19
- text: string;
20
- }[];
21
- }>;
@@ -1,104 +0,0 @@
1
- import { getTextStats, parseMarkdown } from '@echoes-io/utils';
2
- import { z } from 'zod';
3
- export const chapterInsertSchema = z.object({
4
- timeline: z.string().describe('Timeline name'),
5
- arc: z.string().describe('Arc name'),
6
- episode: z.number().describe('Episode number'),
7
- after: z.number().describe('Insert after this chapter number'),
8
- pov: z.string().describe('Point of view character'),
9
- title: z.string().describe('Chapter title'),
10
- summary: z.string().optional().describe('Chapter summary'),
11
- location: z.string().optional().describe('Chapter location'),
12
- outfit: z.string().optional().describe('Character outfit'),
13
- kink: z.string().optional().describe('Content tags'),
14
- file: z.string().optional().describe('Path to markdown file to read content from'),
15
- });
16
- export async function chapterInsert(args, tracker) {
17
- try {
18
- const episode = await tracker.getEpisode(args.timeline, args.arc, args.episode);
19
- if (!episode) {
20
- throw new Error(`Episode not found: ${args.timeline}/${args.arc}/ep${args.episode}`);
21
- }
22
- const existingChapters = await tracker.getChapters(args.timeline, args.arc, args.episode);
23
- const newChapterNumber = args.after + 1;
24
- const chaptersToRenumber = existingChapters.filter((ch) => ch.number >= newChapterNumber);
25
- for (const chapter of chaptersToRenumber) {
26
- try {
27
- await tracker.updateChapter(args.timeline, args.arc, args.episode, chapter.number, {
28
- number: chapter.number + 1,
29
- });
30
- }
31
- catch (error) {
32
- throw new Error(`Failed to renumber chapter ${chapter.number}: ${error instanceof Error ? error.message : 'Unknown error'}`);
33
- }
34
- }
35
- let words = 0;
36
- let characters = 0;
37
- let charactersNoSpaces = 0;
38
- let paragraphs = 0;
39
- let sentences = 0;
40
- if (args.file) {
41
- const { readFileSync } = await import('node:fs');
42
- const content = readFileSync(args.file, 'utf-8');
43
- const { content: markdownContent } = parseMarkdown(content);
44
- const stats = getTextStats(markdownContent);
45
- words = stats.words;
46
- characters = stats.characters;
47
- charactersNoSpaces = stats.charactersNoSpaces;
48
- paragraphs = stats.paragraphs;
49
- sentences = stats.sentences;
50
- }
51
- const chapterData = {
52
- timelineName: args.timeline,
53
- arcName: args.arc,
54
- episodeNumber: args.episode,
55
- partNumber: 1,
56
- number: newChapterNumber,
57
- timeline: args.timeline,
58
- arc: args.arc,
59
- episode: args.episode,
60
- part: 1,
61
- chapter: newChapterNumber,
62
- pov: args.pov,
63
- title: args.title,
64
- date: new Date().toISOString(),
65
- summary: args.summary || '',
66
- location: args.location || '',
67
- outfit: args.outfit || '',
68
- kink: args.kink || '',
69
- words,
70
- characters,
71
- charactersNoSpaces,
72
- paragraphs,
73
- sentences,
74
- readingTimeMinutes: Math.ceil(words / 200),
75
- };
76
- await tracker.createChapter(chapterData);
77
- return {
78
- content: [
79
- {
80
- type: 'text',
81
- text: JSON.stringify({
82
- timeline: args.timeline,
83
- arc: args.arc,
84
- episode: args.episode,
85
- inserted: {
86
- chapter: newChapterNumber,
87
- pov: args.pov,
88
- title: args.title,
89
- words,
90
- },
91
- renumbered: chaptersToRenumber.map((ch) => ({
92
- oldNumber: ch.number,
93
- newNumber: ch.number + 1,
94
- title: ch.title,
95
- })),
96
- }, null, 2),
97
- },
98
- ],
99
- };
100
- }
101
- catch (error) {
102
- throw new Error(`Failed to insert chapter: ${error instanceof Error ? error.message : 'Unknown error'}`);
103
- }
104
- }
@@ -1,12 +0,0 @@
1
- import type { Tracker } from '@echoes-io/tracker';
2
- import { z } from 'zod';
3
- export declare const chapterRefreshSchema: z.ZodObject<{
4
- timeline: z.ZodString;
5
- file: z.ZodString;
6
- }, z.core.$strip>;
7
- export declare function chapterRefresh(args: z.infer<typeof chapterRefreshSchema>, tracker: Tracker): Promise<{
8
- content: {
9
- type: "text";
10
- text: string;
11
- }[];
12
- }>;