@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 +84 -12
- package/lib/server.d.ts +3 -1
- package/lib/server.js +111 -4
- package/lib/tools/chapter-delete.d.ts +24 -0
- package/lib/tools/chapter-delete.js +49 -0
- package/lib/tools/chapter-info.d.ts +21 -0
- package/lib/tools/chapter-info.js +48 -0
- package/lib/tools/chapter-insert.d.ts +42 -0
- package/lib/tools/chapter-insert.js +99 -0
- package/lib/tools/chapter-refresh.d.ts +15 -0
- package/lib/tools/chapter-refresh.js +79 -0
- package/lib/tools/episode-info.d.ts +18 -0
- package/lib/tools/episode-info.js +46 -0
- package/lib/tools/episode-update.d.ts +27 -0
- package/lib/tools/episode-update.js +43 -0
- package/lib/tools/index.d.ts +12 -0
- package/lib/tools/index.js +12 -0
- package/lib/tools/rag-context.d.ts +24 -0
- package/lib/tools/rag-context.js +49 -0
- package/lib/tools/rag-index.d.ts +19 -0
- package/lib/tools/rag-index.js +56 -0
- package/lib/tools/rag-search.d.ts +24 -0
- package/lib/tools/rag-search.js +48 -0
- package/lib/tools/stats.d.ts +21 -0
- package/lib/tools/stats.js +133 -0
- package/lib/tools/timeline-sync.d.ts +15 -0
- package/lib/tools/timeline-sync.js +183 -0
- package/lib/tools/words-count.d.ts +14 -0
- package/lib/tools/words-count.js +30 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +7 -0
- package/package.json +3 -2
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}>;
|