@echoes-io/mcp-server 1.0.0 → 1.2.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.
package/README.md CHANGED
@@ -15,7 +15,10 @@ Add to your MCP client configuration (e.g., `~/.config/q/mcp.json` for Amazon Q)
15
15
  "mcpServers": {
16
16
  "echoes": {
17
17
  "command": "npx",
18
- "args": ["-y", "@echoes-io/mcp-server"]
18
+ "args": ["-y", "@echoes-io/mcp-server"],
19
+ "env": {
20
+ "ECHOES_TIMELINE": "your-timeline-name"
21
+ }
19
22
  }
20
23
  }
21
24
  }
@@ -33,21 +36,77 @@ Then configure:
33
36
  {
34
37
  "mcpServers": {
35
38
  "echoes": {
36
- "command": "echoes-mcp-server"
39
+ "command": "echoes-mcp-server",
40
+ "env": {
41
+ "ECHOES_TIMELINE": "your-timeline-name",
42
+ "ECHOES_RAG_PROVIDER": "e5-small",
43
+ "ECHOES_CHROMA_URL": "./rag_data"
44
+ }
37
45
  }
38
46
  }
39
47
  }
40
48
  ```
41
49
 
42
- ## Usage
43
-
44
- Once configured, the server provides tools for:
45
- - Content management (chapters, episodes, arcs)
46
- - Word counting and text statistics
47
- - Database operations via @echoes-io/tracker
48
- - Timeline synchronization
49
-
50
- Available tools will be listed by your MCP client.
50
+ **Important:** The `ECHOES_TIMELINE` environment variable must be set to specify which timeline to work with. All tools operate on this timeline.
51
+
52
+ **Optional RAG Configuration:**
53
+ - `ECHOES_RAG_PROVIDER`: Embedding provider (`e5-small`, `e5-large`, or `gemini`). Default: `e5-small`
54
+ - `ECHOES_GEMINI_API_KEY`: Required if using `gemini` provider
55
+ - `ECHOES_CHROMA_URL`: ChromaDB storage path. Default: `./rag_data`
56
+
57
+ ## Available Tools
58
+
59
+ All tools operate on the timeline specified by the `ECHOES_TIMELINE` environment variable.
60
+
61
+ ### Content Operations
62
+ - **`words-count`** - Count words and text statistics in markdown files
63
+ - Input: `file` (path to markdown file)
64
+
65
+ - **`chapter-info`** - Extract chapter metadata from database
66
+ - Input: `arc`, `episode`, `chapter`
67
+
68
+ - **`chapter-refresh`** - Refresh chapter metadata and word counts from file
69
+ - Input: `file` (path to chapter file)
70
+
71
+ - **`chapter-insert`** - Insert new chapter with automatic renumbering
72
+ - Input: `arc`, `episode`, `after`, `pov`, `title`, optional: `excerpt`, `location`, `outfit`, `kink`, `file`
73
+
74
+ - **`chapter-delete`** - Delete chapter from database and optionally from filesystem
75
+ - Input: `arc`, `episode`, `chapter`, optional: `file` (to delete from filesystem)
76
+
77
+ ### Episode Operations
78
+ - **`episode-info`** - Get episode information and list of chapters
79
+ - Input: `arc`, `episode`
80
+
81
+ - **`episode-update`** - Update episode metadata (description, title, slug)
82
+ - Input: `arc`, `episode`, optional: `description`, `title`, `slug`
83
+
84
+ ### Timeline Operations
85
+ - **`timeline-sync`** - Synchronize filesystem content with database
86
+ - Input: `contentPath` (path to content directory)
87
+
88
+ ### Statistics
89
+ - **`stats`** - Get aggregate statistics with optional filters
90
+ - Input: optional: `arc`, `episode`, `pov`
91
+ - Output: Total words/chapters, POV distribution, arc/episode breakdown, longest/shortest chapters
92
+ - Examples:
93
+ - No filters: Overall timeline statistics
94
+ - `arc: "arc1"`: Statistics for specific arc
95
+ - `arc: "arc1", episode: 1`: Statistics for specific episode
96
+ - `pov: "Alice"`: Statistics for specific POV across timeline
97
+
98
+ ### RAG (Semantic Search)
99
+ - **`rag-index`** - Index chapters into vector database for semantic search
100
+ - Input: optional: `arc`, `episode` (to index specific content)
101
+ - Output: Number of chapters indexed
102
+
103
+ - **`rag-search`** - Semantic search across timeline content
104
+ - Input: `query`, optional: `arc`, `pov`, `maxResults`
105
+ - Output: Relevant chapters with similarity scores and previews
106
+
107
+ - **`rag-context`** - Retrieve relevant context for AI interactions
108
+ - Input: `query`, optional: `arc`, `pov`, `maxChapters`
109
+ - Output: Full chapter content for AI context
51
110
 
52
111
  ## Development
53
112
 
@@ -73,10 +132,23 @@ npm run lint:fix
73
132
  ### Tech Stack
74
133
 
75
134
  - **Language**: TypeScript (strict mode)
76
- - **Testing**: Vitest
135
+ - **Testing**: Vitest (97%+ coverage)
77
136
  - **Linting**: Biome
78
137
  - **Build**: TypeScript compiler
79
138
 
139
+ ### Architecture
140
+
141
+ - **MCP Protocol**: Standard Model Context Protocol implementation
142
+ - **Database**: SQLite via @echoes-io/tracker (singleton pattern)
143
+ - **Validation**: Zod schemas for type-safe inputs
144
+ - **Testing**: Comprehensive unit and integration tests
145
+ - **Environment**: Uses `ECHOES_TIMELINE` env var for timeline context
146
+
147
+ ## Roadmap
148
+
149
+ ### Planned Features
150
+ - **Book generation** - LaTeX/PDF compilation tools for creating publishable books from timeline content
151
+
80
152
  ## License
81
153
 
82
154
  MIT
package/lib/server.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { RAGSystem } from '@echoes-io/rag';
2
+ import { Tracker } from '@echoes-io/tracker';
1
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- export declare function createServer(): Server<{
4
+ export declare function createServer(tracker: Tracker, rag: RAGSystem): Server<{
3
5
  method: string;
4
6
  params?: {
5
7
  [x: string]: unknown;
package/lib/server.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { RAGSystem } from '@echoes-io/rag';
5
+ import { Tracker } from '@echoes-io/tracker';
4
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
+ import { zodToJsonSchema } from 'zod-to-json-schema';
10
+ import { chapterDelete, chapterDeleteSchema, chapterInfo, chapterInfoSchema, chapterInsert, chapterInsertSchema, chapterRefresh, chapterRefreshSchema, episodeInfo, episodeInfoSchema, episodeUpdate, episodeUpdateSchema, ragContext, ragContextSchema, ragIndex, ragIndexSchema, ragSearch, ragSearchSchema, stats, statsSchema, timelineSync, timelineSyncSchema, wordsCount, wordsCountSchema, } from './tools/index.js';
7
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
12
  const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
9
- export function createServer() {
13
+ export function createServer(tracker, rag) {
10
14
  const server = new Server({
11
15
  name: pkg.name,
12
16
  version: pkg.version,
@@ -17,16 +21,119 @@ export function createServer() {
17
21
  });
18
22
  server.setRequestHandler(ListToolsRequestSchema, async () => {
19
23
  return {
20
- tools: [],
24
+ tools: [
25
+ {
26
+ name: 'words-count',
27
+ description: 'Count words and text statistics in a markdown file',
28
+ inputSchema: zodToJsonSchema(wordsCountSchema),
29
+ },
30
+ {
31
+ name: 'chapter-info',
32
+ description: 'Extract chapter metadata, content preview, and statistics',
33
+ inputSchema: zodToJsonSchema(chapterInfoSchema),
34
+ },
35
+ {
36
+ name: 'episode-info',
37
+ description: 'Get episode information and list of chapters',
38
+ inputSchema: zodToJsonSchema(episodeInfoSchema),
39
+ },
40
+ {
41
+ name: 'episode-update',
42
+ description: 'Update episode metadata (description, title, slug)',
43
+ inputSchema: zodToJsonSchema(episodeUpdateSchema),
44
+ },
45
+ {
46
+ name: 'chapter-refresh',
47
+ description: 'Refresh chapter metadata and statistics from file',
48
+ inputSchema: zodToJsonSchema(chapterRefreshSchema),
49
+ },
50
+ {
51
+ name: 'chapter-delete',
52
+ description: 'Delete chapter from database and optionally from filesystem',
53
+ inputSchema: zodToJsonSchema(chapterDeleteSchema),
54
+ },
55
+ {
56
+ name: 'chapter-insert',
57
+ description: 'Insert new chapter and automatically renumber subsequent chapters',
58
+ inputSchema: zodToJsonSchema(chapterInsertSchema),
59
+ },
60
+ {
61
+ name: 'timeline-sync',
62
+ description: 'Synchronize timeline content with database',
63
+ inputSchema: zodToJsonSchema(timelineSyncSchema),
64
+ },
65
+ {
66
+ name: 'stats',
67
+ description: 'Get statistics for timeline, arc, episode, or POV',
68
+ inputSchema: zodToJsonSchema(statsSchema),
69
+ },
70
+ {
71
+ name: 'rag-index',
72
+ description: 'Index chapters into RAG vector database for semantic search',
73
+ inputSchema: zodToJsonSchema(ragIndexSchema),
74
+ },
75
+ {
76
+ name: 'rag-search',
77
+ description: 'Semantic search across timeline content',
78
+ inputSchema: zodToJsonSchema(ragSearchSchema),
79
+ },
80
+ {
81
+ name: 'rag-context',
82
+ description: 'Retrieve relevant context for AI interactions',
83
+ inputSchema: zodToJsonSchema(ragContextSchema),
84
+ },
85
+ ],
21
86
  };
22
87
  });
23
88
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
24
- throw new Error(`Unknown tool: ${request.params.name}`);
89
+ const { name, arguments: args } = request.params;
90
+ switch (name) {
91
+ case 'words-count':
92
+ return await wordsCount(wordsCountSchema.parse(args));
93
+ case 'chapter-info':
94
+ return await chapterInfo(chapterInfoSchema.parse(args), tracker);
95
+ case 'chapter-refresh':
96
+ return await chapterRefresh(chapterRefreshSchema.parse(args), tracker);
97
+ case 'chapter-delete':
98
+ return await chapterDelete(chapterDeleteSchema.parse(args), tracker);
99
+ case 'chapter-insert':
100
+ return await chapterInsert(chapterInsertSchema.parse(args), tracker);
101
+ case 'episode-info':
102
+ return await episodeInfo(episodeInfoSchema.parse(args), tracker);
103
+ case 'episode-update':
104
+ return await episodeUpdate(episodeUpdateSchema.parse(args), tracker);
105
+ case 'timeline-sync':
106
+ return await timelineSync(timelineSyncSchema.parse(args), tracker);
107
+ case 'stats':
108
+ return await stats(statsSchema.parse(args), tracker);
109
+ case 'rag-index':
110
+ return await ragIndex(ragIndexSchema.parse(args), tracker, rag);
111
+ case 'rag-search':
112
+ return await ragSearch(ragSearchSchema.parse(args), rag);
113
+ case 'rag-context':
114
+ return await ragContext(ragContextSchema.parse(args), rag);
115
+ default:
116
+ throw new Error(`Unknown tool: ${name}`);
117
+ }
25
118
  });
26
119
  return server;
27
120
  }
28
121
  export async function runServer() {
29
- const server = createServer();
122
+ // Initialize tracker database in appropriate location
123
+ const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : './tracker.db';
124
+ const tracker = new Tracker(dbPath);
125
+ await tracker.init();
126
+ console.error(`Tracker database initialized: ${dbPath}`);
127
+ // Initialize RAG system
128
+ const chromaUrl = process.env.ECHOES_CHROMA_URL || (process.env.NODE_ENV === 'test' ? ':memory:' : './rag_data');
129
+ const provider = (process.env.ECHOES_RAG_PROVIDER || 'e5-small');
130
+ const rag = new RAGSystem({
131
+ provider,
132
+ chromaUrl,
133
+ geminiApiKey: process.env.ECHOES_GEMINI_API_KEY,
134
+ });
135
+ console.error(`RAG system initialized: ${chromaUrl} (provider: ${provider})`);
136
+ const server = createServer(tracker, rag);
30
137
  const transport = new StdioServerTransport();
31
138
  await server.connect(transport);
32
139
  console.error('Echoes MCP Server running on stdio');
@@ -0,0 +1,24 @@
1
+ import type { Tracker } from '@echoes-io/tracker';
2
+ import { z } from 'zod';
3
+ export declare const chapterDeleteSchema: z.ZodObject<{
4
+ arc: z.ZodString;
5
+ episode: z.ZodNumber;
6
+ chapter: z.ZodNumber;
7
+ file: z.ZodOptional<z.ZodString>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ arc: string;
10
+ episode: number;
11
+ chapter: number;
12
+ file?: string | undefined;
13
+ }, {
14
+ arc: string;
15
+ episode: number;
16
+ chapter: number;
17
+ file?: string | undefined;
18
+ }>;
19
+ export declare function chapterDelete(args: z.infer<typeof chapterDeleteSchema>, tracker: Tracker): Promise<{
20
+ content: {
21
+ type: "text";
22
+ text: string;
23
+ }[];
24
+ }>;
@@ -0,0 +1,49 @@
1
+ import { unlinkSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ import { getTimeline } from '../utils.js';
4
+ export const chapterDeleteSchema = z.object({
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 timeline = getTimeline();
13
+ const existing = await tracker.getChapter(timeline, args.arc, args.episode, args.chapter);
14
+ if (!existing) {
15
+ throw new Error(`Chapter not found: ${timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
16
+ }
17
+ await tracker.deleteChapter(timeline, args.arc, args.episode, args.chapter);
18
+ let fileDeleted = false;
19
+ if (args.file) {
20
+ unlinkSync(args.file);
21
+ fileDeleted = true;
22
+ }
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: JSON.stringify({
28
+ timeline,
29
+ arc: args.arc,
30
+ episode: args.episode,
31
+ chapter: args.chapter,
32
+ deleted: {
33
+ pov: existing.pov,
34
+ title: existing.title,
35
+ words: existing.words,
36
+ },
37
+ fileDeleted,
38
+ message: fileDeleted
39
+ ? 'Chapter deleted from database and filesystem'
40
+ : 'Chapter deleted from database only',
41
+ }, null, 2),
42
+ },
43
+ ],
44
+ };
45
+ }
46
+ catch (error) {
47
+ throw new Error(`Failed to delete chapter: ${error instanceof Error ? error.message : 'Unknown error'}`);
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ import type { Tracker } from '@echoes-io/tracker';
2
+ import { z } from 'zod';
3
+ export declare const chapterInfoSchema: z.ZodObject<{
4
+ arc: z.ZodString;
5
+ episode: z.ZodNumber;
6
+ chapter: z.ZodNumber;
7
+ }, "strip", z.ZodTypeAny, {
8
+ arc: string;
9
+ episode: number;
10
+ chapter: number;
11
+ }, {
12
+ arc: string;
13
+ episode: number;
14
+ chapter: number;
15
+ }>;
16
+ export declare function chapterInfo(args: z.infer<typeof chapterInfoSchema>, tracker: Tracker): Promise<{
17
+ content: {
18
+ type: "text";
19
+ text: string;
20
+ }[];
21
+ }>;
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ import { getTimeline } from '../utils.js';
3
+ export const chapterInfoSchema = z.object({
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 timeline = getTimeline();
11
+ const chapter = await tracker.getChapter(timeline, args.arc, args.episode, args.chapter);
12
+ if (!chapter) {
13
+ throw new Error(`Chapter not found: ${timeline}/${args.arc}/ep${args.episode}/ch${args.chapter}`);
14
+ }
15
+ return {
16
+ content: [
17
+ {
18
+ type: 'text',
19
+ text: JSON.stringify({
20
+ timeline,
21
+ arc: args.arc,
22
+ episode: args.episode,
23
+ chapter: args.chapter,
24
+ metadata: {
25
+ pov: chapter.pov,
26
+ title: chapter.title,
27
+ date: chapter.date,
28
+ excerpt: chapter.excerpt,
29
+ location: chapter.location,
30
+ outfit: chapter.outfit,
31
+ kink: chapter.kink,
32
+ },
33
+ stats: {
34
+ words: chapter.words,
35
+ characters: chapter.characters,
36
+ charactersNoSpaces: chapter.charactersNoSpaces,
37
+ paragraphs: chapter.paragraphs,
38
+ sentences: chapter.sentences,
39
+ },
40
+ }, null, 2),
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ catch (error) {
46
+ throw new Error(`Failed to get chapter info: ${error instanceof Error ? error.message : 'Unknown error'}`);
47
+ }
48
+ }
@@ -0,0 +1,42 @@
1
+ import type { Tracker } from '@echoes-io/tracker';
2
+ import { z } from 'zod';
3
+ export declare const chapterInsertSchema: z.ZodObject<{
4
+ arc: z.ZodString;
5
+ episode: z.ZodNumber;
6
+ after: z.ZodNumber;
7
+ pov: z.ZodString;
8
+ title: z.ZodString;
9
+ excerpt: z.ZodOptional<z.ZodString>;
10
+ location: z.ZodOptional<z.ZodString>;
11
+ outfit: z.ZodOptional<z.ZodString>;
12
+ kink: z.ZodOptional<z.ZodString>;
13
+ file: z.ZodOptional<z.ZodString>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ arc: string;
16
+ episode: number;
17
+ after: number;
18
+ pov: string;
19
+ title: string;
20
+ file?: string | undefined;
21
+ excerpt?: string | undefined;
22
+ location?: string | undefined;
23
+ outfit?: string | undefined;
24
+ kink?: string | undefined;
25
+ }, {
26
+ arc: string;
27
+ episode: number;
28
+ after: number;
29
+ pov: string;
30
+ title: string;
31
+ file?: string | undefined;
32
+ excerpt?: string | undefined;
33
+ location?: string | undefined;
34
+ outfit?: string | undefined;
35
+ kink?: string | undefined;
36
+ }>;
37
+ export declare function chapterInsert(args: z.infer<typeof chapterInsertSchema>, tracker: Tracker): Promise<{
38
+ content: {
39
+ type: "text";
40
+ text: string;
41
+ }[];
42
+ }>;
@@ -0,0 +1,99 @@
1
+ import { getTextStats, parseMarkdown } from '@echoes-io/utils';
2
+ import { z } from 'zod';
3
+ import { getTimeline } from '../utils.js';
4
+ export const chapterInsertSchema = z.object({
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
+ excerpt: z.string().optional().describe('Chapter excerpt'),
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 timeline = getTimeline();
19
+ const episode = await tracker.getEpisode(timeline, args.arc, args.episode);
20
+ if (!episode) {
21
+ throw new Error(`Episode not found: ${timeline}/${args.arc}/ep${args.episode}`);
22
+ }
23
+ const existingChapters = await tracker.getChapters(timeline, args.arc, args.episode);
24
+ const newChapterNumber = args.after + 1;
25
+ const chaptersToRenumber = existingChapters.filter((ch) => ch.number >= newChapterNumber);
26
+ for (const chapter of chaptersToRenumber) {
27
+ try {
28
+ await tracker.updateChapter(timeline, args.arc, args.episode, chapter.number, {
29
+ number: chapter.number + 1,
30
+ });
31
+ }
32
+ catch (error) {
33
+ throw new Error(`Failed to renumber chapter ${chapter.number}: ${error instanceof Error ? error.message : 'Unknown error'}`);
34
+ }
35
+ }
36
+ let words = 0;
37
+ let characters = 0;
38
+ let charactersNoSpaces = 0;
39
+ let paragraphs = 0;
40
+ let sentences = 0;
41
+ if (args.file) {
42
+ const { readFileSync } = await import('node:fs');
43
+ const content = readFileSync(args.file, 'utf-8');
44
+ const { content: markdownContent } = parseMarkdown(content);
45
+ const stats = getTextStats(markdownContent);
46
+ words = stats.words;
47
+ characters = stats.characters;
48
+ charactersNoSpaces = stats.charactersNoSpaces;
49
+ paragraphs = stats.paragraphs;
50
+ sentences = stats.sentences;
51
+ }
52
+ await tracker.createChapter({
53
+ timelineName: timeline,
54
+ arcName: args.arc,
55
+ episodeNumber: args.episode,
56
+ partNumber: 1,
57
+ number: newChapterNumber,
58
+ pov: args.pov,
59
+ title: args.title,
60
+ date: new Date(),
61
+ excerpt: args.excerpt || '',
62
+ location: args.location || '',
63
+ outfit: args.outfit || '',
64
+ kink: args.kink || '',
65
+ words,
66
+ characters,
67
+ charactersNoSpaces,
68
+ paragraphs,
69
+ sentences,
70
+ readingTimeMinutes: Math.ceil(words / 200),
71
+ });
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: JSON.stringify({
77
+ timeline,
78
+ arc: args.arc,
79
+ episode: args.episode,
80
+ inserted: {
81
+ chapter: newChapterNumber,
82
+ pov: args.pov,
83
+ title: args.title,
84
+ words,
85
+ },
86
+ renumbered: chaptersToRenumber.map((ch) => ({
87
+ oldNumber: ch.number,
88
+ newNumber: ch.number + 1,
89
+ title: ch.title,
90
+ })),
91
+ }, null, 2),
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ catch (error) {
97
+ throw new Error(`Failed to insert chapter: ${error instanceof Error ? error.message : 'Unknown error'}`);
98
+ }
99
+ }
@@ -0,0 +1,15 @@
1
+ import type { Tracker } from '@echoes-io/tracker';
2
+ import { z } from 'zod';
3
+ export declare const chapterRefreshSchema: z.ZodObject<{
4
+ file: z.ZodString;
5
+ }, "strip", z.ZodTypeAny, {
6
+ file: string;
7
+ }, {
8
+ file: string;
9
+ }>;
10
+ export declare function chapterRefresh(args: z.infer<typeof chapterRefreshSchema>, tracker: Tracker): Promise<{
11
+ content: {
12
+ type: "text";
13
+ text: string;
14
+ }[];
15
+ }>;
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { getTextStats, parseMarkdown } from '@echoes-io/utils';
3
+ import { z } from 'zod';
4
+ import { getTimeline } from '../utils.js';
5
+ export const chapterRefreshSchema = z.object({
6
+ file: z.string().describe('Path to chapter markdown file'),
7
+ });
8
+ export async function chapterRefresh(args, tracker) {
9
+ try {
10
+ const content = readFileSync(args.file, 'utf-8');
11
+ const { metadata, content: markdownContent } = parseMarkdown(content);
12
+ const stats = getTextStats(markdownContent);
13
+ const timeline = getTimeline();
14
+ const arc = metadata.arc;
15
+ const episode = metadata.episode;
16
+ const chapter = metadata.chapter;
17
+ if (!arc || !episode || !chapter) {
18
+ throw new Error('Missing required metadata: arc, episode, or chapter');
19
+ }
20
+ const existing = await tracker.getChapter(timeline, arc, episode, chapter);
21
+ if (!existing) {
22
+ throw new Error(`Chapter not found in database: ${timeline}/${arc}/ep${episode}/ch${chapter}`);
23
+ }
24
+ const chapterData = {
25
+ timelineName: timeline,
26
+ arcName: arc,
27
+ episodeNumber: episode,
28
+ partNumber: metadata.part || 1,
29
+ number: chapter,
30
+ pov: metadata.pov || 'Unknown',
31
+ title: metadata.title || 'Untitled',
32
+ date: new Date(metadata.date || Date.now()),
33
+ excerpt: metadata.excerpt || '',
34
+ location: metadata.location || '',
35
+ outfit: metadata.outfit || '',
36
+ kink: metadata.kink || '',
37
+ words: stats.words,
38
+ characters: stats.characters,
39
+ charactersNoSpaces: stats.charactersNoSpaces,
40
+ paragraphs: stats.paragraphs,
41
+ sentences: stats.sentences,
42
+ readingTimeMinutes: Math.ceil(stats.words / 200),
43
+ };
44
+ await tracker.updateChapter(timeline, arc, episode, chapter, chapterData);
45
+ return {
46
+ content: [
47
+ {
48
+ type: 'text',
49
+ text: JSON.stringify({
50
+ file: args.file,
51
+ timeline,
52
+ arc,
53
+ episode,
54
+ chapter,
55
+ updated: {
56
+ metadata: {
57
+ pov: chapterData.pov,
58
+ title: chapterData.title,
59
+ date: chapterData.date,
60
+ excerpt: chapterData.excerpt,
61
+ location: chapterData.location,
62
+ },
63
+ stats: {
64
+ words: stats.words,
65
+ characters: stats.characters,
66
+ paragraphs: stats.paragraphs,
67
+ sentences: stats.sentences,
68
+ readingTimeMinutes: chapterData.readingTimeMinutes,
69
+ },
70
+ },
71
+ }, null, 2),
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ catch (error) {
77
+ throw new Error(`Failed to refresh chapter: ${error instanceof Error ? error.message : 'Unknown error'}`);
78
+ }
79
+ }
@@ -0,0 +1,18 @@
1
+ import type { Tracker } from '@echoes-io/tracker';
2
+ import { z } from 'zod';
3
+ export declare const episodeInfoSchema: z.ZodObject<{
4
+ arc: z.ZodString;
5
+ episode: z.ZodNumber;
6
+ }, "strip", z.ZodTypeAny, {
7
+ arc: string;
8
+ episode: number;
9
+ }, {
10
+ arc: string;
11
+ episode: number;
12
+ }>;
13
+ export declare function episodeInfo(args: z.infer<typeof episodeInfoSchema>, tracker: Tracker): Promise<{
14
+ content: {
15
+ type: "text";
16
+ text: string;
17
+ }[];
18
+ }>;