@echoes-io/mcp-server 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -12
- package/lib/server.d.ts +2 -1
- package/lib/server.js +80 -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 +9 -0
- package/lib/tools/index.js +9 -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 +1 -1
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,57 @@ 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
|
+
}
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
```
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
**Important:** The `ECHOES_TIMELINE` environment variable must be set to specify which timeline to work with. All tools operate on this timeline.
|
|
49
|
+
|
|
50
|
+
## Available Tools
|
|
51
|
+
|
|
52
|
+
All tools operate on the timeline specified by the `ECHOES_TIMELINE` environment variable.
|
|
53
|
+
|
|
54
|
+
### Content Operations
|
|
55
|
+
- **`words-count`** - Count words and text statistics in markdown files
|
|
56
|
+
- Input: `file` (path to markdown file)
|
|
57
|
+
|
|
58
|
+
- **`chapter-info`** - Extract chapter metadata from database
|
|
59
|
+
- Input: `arc`, `episode`, `chapter`
|
|
60
|
+
|
|
61
|
+
- **`chapter-refresh`** - Refresh chapter metadata and word counts from file
|
|
62
|
+
- Input: `file` (path to chapter file)
|
|
63
|
+
|
|
64
|
+
- **`chapter-insert`** - Insert new chapter with automatic renumbering
|
|
65
|
+
- Input: `arc`, `episode`, `after`, `pov`, `title`, optional: `excerpt`, `location`, `outfit`, `kink`, `file`
|
|
66
|
+
|
|
67
|
+
- **`chapter-delete`** - Delete chapter from database and optionally from filesystem
|
|
68
|
+
- Input: `arc`, `episode`, `chapter`, optional: `file` (to delete from filesystem)
|
|
69
|
+
|
|
70
|
+
### Episode Operations
|
|
71
|
+
- **`episode-info`** - Get episode information and list of chapters
|
|
72
|
+
- Input: `arc`, `episode`
|
|
73
|
+
|
|
74
|
+
- **`episode-update`** - Update episode metadata (description, title, slug)
|
|
75
|
+
- Input: `arc`, `episode`, optional: `description`, `title`, `slug`
|
|
76
|
+
|
|
77
|
+
### Timeline Operations
|
|
78
|
+
- **`timeline-sync`** - Synchronize filesystem content with database
|
|
79
|
+
- Input: `contentPath` (path to content directory)
|
|
80
|
+
|
|
81
|
+
### Statistics
|
|
82
|
+
- **`stats`** - Get aggregate statistics with optional filters
|
|
83
|
+
- Input: optional: `arc`, `episode`, `pov`
|
|
84
|
+
- Output: Total words/chapters, POV distribution, arc/episode breakdown, longest/shortest chapters
|
|
85
|
+
- Examples:
|
|
86
|
+
- No filters: Overall timeline statistics
|
|
87
|
+
- `arc: "arc1"`: Statistics for specific arc
|
|
88
|
+
- `arc: "arc1", episode: 1`: Statistics for specific episode
|
|
89
|
+
- `pov: "Alice"`: Statistics for specific POV across timeline
|
|
51
90
|
|
|
52
91
|
## Development
|
|
53
92
|
|
|
@@ -73,10 +112,24 @@ npm run lint:fix
|
|
|
73
112
|
### Tech Stack
|
|
74
113
|
|
|
75
114
|
- **Language**: TypeScript (strict mode)
|
|
76
|
-
- **Testing**: Vitest
|
|
115
|
+
- **Testing**: Vitest (97%+ coverage)
|
|
77
116
|
- **Linting**: Biome
|
|
78
117
|
- **Build**: TypeScript compiler
|
|
79
118
|
|
|
119
|
+
### Architecture
|
|
120
|
+
|
|
121
|
+
- **MCP Protocol**: Standard Model Context Protocol implementation
|
|
122
|
+
- **Database**: SQLite via @echoes-io/tracker (singleton pattern)
|
|
123
|
+
- **Validation**: Zod schemas for type-safe inputs
|
|
124
|
+
- **Testing**: Comprehensive unit and integration tests
|
|
125
|
+
- **Environment**: Uses `ECHOES_TIMELINE` env var for timeline context
|
|
126
|
+
|
|
127
|
+
## Roadmap
|
|
128
|
+
|
|
129
|
+
### Planned Features
|
|
130
|
+
- **Book generation** - LaTeX/PDF compilation tools for creating publishable books from timeline content
|
|
131
|
+
- **RAG system** - Retrieval-Augmented Generation integration for AI-assisted writing and content analysis
|
|
132
|
+
|
|
80
133
|
## License
|
|
81
134
|
|
|
82
135
|
MIT
|
package/lib/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Tracker } from '@echoes-io/tracker';
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
export declare function createServer(): Server<{
|
|
3
|
+
export declare function createServer(tracker: Tracker): Server<{
|
|
3
4
|
method: string;
|
|
4
5
|
params?: {
|
|
5
6
|
[x: string]: unknown;
|
package/lib/server.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { Tracker } from '@echoes-io/tracker';
|
|
4
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
7
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
9
|
+
import { chapterDelete, chapterDeleteSchema, chapterInfo, chapterInfoSchema, chapterInsert, chapterInsertSchema, chapterRefresh, chapterRefreshSchema, episodeInfo, episodeInfoSchema, episodeUpdate, episodeUpdateSchema, stats, statsSchema, timelineSync, timelineSyncSchema, wordsCount, wordsCountSchema, } from './tools/index.js';
|
|
7
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
11
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
9
|
-
export function createServer() {
|
|
12
|
+
export function createServer(tracker) {
|
|
10
13
|
const server = new Server({
|
|
11
14
|
name: pkg.name,
|
|
12
15
|
version: pkg.version,
|
|
@@ -17,16 +20,89 @@ export function createServer() {
|
|
|
17
20
|
});
|
|
18
21
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
19
22
|
return {
|
|
20
|
-
tools: [
|
|
23
|
+
tools: [
|
|
24
|
+
{
|
|
25
|
+
name: 'words-count',
|
|
26
|
+
description: 'Count words and text statistics in a markdown file',
|
|
27
|
+
inputSchema: zodToJsonSchema(wordsCountSchema),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'chapter-info',
|
|
31
|
+
description: 'Extract chapter metadata, content preview, and statistics',
|
|
32
|
+
inputSchema: zodToJsonSchema(chapterInfoSchema),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'episode-info',
|
|
36
|
+
description: 'Get episode information and list of chapters',
|
|
37
|
+
inputSchema: zodToJsonSchema(episodeInfoSchema),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'episode-update',
|
|
41
|
+
description: 'Update episode metadata (description, title, slug)',
|
|
42
|
+
inputSchema: zodToJsonSchema(episodeUpdateSchema),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'chapter-refresh',
|
|
46
|
+
description: 'Refresh chapter metadata and statistics from file',
|
|
47
|
+
inputSchema: zodToJsonSchema(chapterRefreshSchema),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'chapter-delete',
|
|
51
|
+
description: 'Delete chapter from database and optionally from filesystem',
|
|
52
|
+
inputSchema: zodToJsonSchema(chapterDeleteSchema),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'chapter-insert',
|
|
56
|
+
description: 'Insert new chapter and automatically renumber subsequent chapters',
|
|
57
|
+
inputSchema: zodToJsonSchema(chapterInsertSchema),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'timeline-sync',
|
|
61
|
+
description: 'Synchronize timeline content with database',
|
|
62
|
+
inputSchema: zodToJsonSchema(timelineSyncSchema),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'stats',
|
|
66
|
+
description: 'Get statistics for timeline, arc, episode, or POV',
|
|
67
|
+
inputSchema: zodToJsonSchema(statsSchema),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
21
70
|
};
|
|
22
71
|
});
|
|
23
72
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
24
|
-
|
|
73
|
+
const { name, arguments: args } = request.params;
|
|
74
|
+
switch (name) {
|
|
75
|
+
case 'words-count':
|
|
76
|
+
return await wordsCount(wordsCountSchema.parse(args));
|
|
77
|
+
case 'chapter-info':
|
|
78
|
+
return await chapterInfo(chapterInfoSchema.parse(args), tracker);
|
|
79
|
+
case 'chapter-refresh':
|
|
80
|
+
return await chapterRefresh(chapterRefreshSchema.parse(args), tracker);
|
|
81
|
+
case 'chapter-delete':
|
|
82
|
+
return await chapterDelete(chapterDeleteSchema.parse(args), tracker);
|
|
83
|
+
case 'chapter-insert':
|
|
84
|
+
return await chapterInsert(chapterInsertSchema.parse(args), tracker);
|
|
85
|
+
case 'episode-info':
|
|
86
|
+
return await episodeInfo(episodeInfoSchema.parse(args), tracker);
|
|
87
|
+
case 'episode-update':
|
|
88
|
+
return await episodeUpdate(episodeUpdateSchema.parse(args), tracker);
|
|
89
|
+
case 'timeline-sync':
|
|
90
|
+
return await timelineSync(timelineSyncSchema.parse(args), tracker);
|
|
91
|
+
case 'stats':
|
|
92
|
+
return await stats(statsSchema.parse(args), tracker);
|
|
93
|
+
default:
|
|
94
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
95
|
+
}
|
|
25
96
|
});
|
|
26
97
|
return server;
|
|
27
98
|
}
|
|
28
99
|
export async function runServer() {
|
|
29
|
-
|
|
100
|
+
// Initialize tracker database in appropriate location
|
|
101
|
+
const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : './tracker.db';
|
|
102
|
+
const tracker = new Tracker(dbPath);
|
|
103
|
+
await tracker.init();
|
|
104
|
+
console.error(`Tracker database initialized: ${dbPath}`);
|
|
105
|
+
const server = createServer(tracker);
|
|
30
106
|
const transport = new StdioServerTransport();
|
|
31
107
|
await server.connect(transport);
|
|
32
108
|
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
|
+
}>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getTimeline } from '../utils.js';
|
|
3
|
+
export const episodeInfoSchema = z.object({
|
|
4
|
+
arc: z.string().describe('Arc name'),
|
|
5
|
+
episode: z.number().describe('Episode number'),
|
|
6
|
+
});
|
|
7
|
+
export async function episodeInfo(args, tracker) {
|
|
8
|
+
try {
|
|
9
|
+
const timeline = getTimeline();
|
|
10
|
+
const episode = await tracker.getEpisode(timeline, args.arc, args.episode);
|
|
11
|
+
if (!episode) {
|
|
12
|
+
throw new Error(`Episode not found: ${timeline}/${args.arc}/ep${args.episode}`);
|
|
13
|
+
}
|
|
14
|
+
const chapters = await tracker.getChapters(timeline, args.arc, args.episode);
|
|
15
|
+
return {
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: 'text',
|
|
19
|
+
text: JSON.stringify({
|
|
20
|
+
timeline,
|
|
21
|
+
arc: args.arc,
|
|
22
|
+
episodeInfo: {
|
|
23
|
+
number: episode.number,
|
|
24
|
+
title: episode.title,
|
|
25
|
+
slug: episode.slug,
|
|
26
|
+
description: episode.description,
|
|
27
|
+
},
|
|
28
|
+
chapters: chapters.map((ch) => ({
|
|
29
|
+
number: ch.number,
|
|
30
|
+
pov: ch.pov,
|
|
31
|
+
title: ch.title,
|
|
32
|
+
words: ch.words,
|
|
33
|
+
})),
|
|
34
|
+
stats: {
|
|
35
|
+
totalChapters: chapters.length,
|
|
36
|
+
totalWords: chapters.reduce((sum, ch) => sum + ch.words, 0),
|
|
37
|
+
},
|
|
38
|
+
}, null, 2),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(`Failed to get episode info: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Tracker } from '@echoes-io/tracker';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const episodeUpdateSchema: z.ZodObject<{
|
|
4
|
+
arc: z.ZodString;
|
|
5
|
+
episode: z.ZodNumber;
|
|
6
|
+
description: z.ZodOptional<z.ZodString>;
|
|
7
|
+
title: z.ZodOptional<z.ZodString>;
|
|
8
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
arc: string;
|
|
11
|
+
episode: number;
|
|
12
|
+
title?: string | undefined;
|
|
13
|
+
description?: string | undefined;
|
|
14
|
+
slug?: string | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
arc: string;
|
|
17
|
+
episode: number;
|
|
18
|
+
title?: string | undefined;
|
|
19
|
+
description?: string | undefined;
|
|
20
|
+
slug?: string | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function episodeUpdate(args: z.infer<typeof episodeUpdateSchema>, tracker: Tracker): Promise<{
|
|
23
|
+
content: {
|
|
24
|
+
type: "text";
|
|
25
|
+
text: string;
|
|
26
|
+
}[];
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getTimeline } from '../utils.js';
|
|
3
|
+
export const episodeUpdateSchema = z.object({
|
|
4
|
+
arc: z.string().describe('Arc name'),
|
|
5
|
+
episode: z.number().describe('Episode number'),
|
|
6
|
+
description: z.string().optional().describe('Episode description'),
|
|
7
|
+
title: z.string().optional().describe('Episode title'),
|
|
8
|
+
slug: z.string().optional().describe('Episode slug'),
|
|
9
|
+
});
|
|
10
|
+
export async function episodeUpdate(args, tracker) {
|
|
11
|
+
try {
|
|
12
|
+
const timeline = getTimeline();
|
|
13
|
+
const existing = await tracker.getEpisode(timeline, args.arc, args.episode);
|
|
14
|
+
if (!existing) {
|
|
15
|
+
throw new Error(`Episode not found: ${timeline}/${args.arc}/ep${args.episode}`);
|
|
16
|
+
}
|
|
17
|
+
const updateData = {};
|
|
18
|
+
if (args.description !== undefined)
|
|
19
|
+
updateData.description = args.description;
|
|
20
|
+
if (args.title !== undefined)
|
|
21
|
+
updateData.title = args.title;
|
|
22
|
+
if (args.slug !== undefined)
|
|
23
|
+
updateData.slug = args.slug;
|
|
24
|
+
await tracker.updateEpisode(timeline, args.arc, args.episode, updateData);
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: JSON.stringify({
|
|
30
|
+
timeline,
|
|
31
|
+
arc: args.arc,
|
|
32
|
+
episode: args.episode,
|
|
33
|
+
updated: updateData,
|
|
34
|
+
message: 'Episode successfully updated',
|
|
35
|
+
}, null, 2),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Failed to update episode: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { chapterDelete, chapterDeleteSchema } from './chapter-delete.js';
|
|
2
|
+
export { chapterInfo, chapterInfoSchema } from './chapter-info.js';
|
|
3
|
+
export { chapterInsert, chapterInsertSchema } from './chapter-insert.js';
|
|
4
|
+
export { chapterRefresh, chapterRefreshSchema } from './chapter-refresh.js';
|
|
5
|
+
export { episodeInfo, episodeInfoSchema } from './episode-info.js';
|
|
6
|
+
export { episodeUpdate, episodeUpdateSchema } from './episode-update.js';
|
|
7
|
+
export { stats, statsSchema } from './stats.js';
|
|
8
|
+
export { timelineSync, timelineSyncSchema } from './timeline-sync.js';
|
|
9
|
+
export { wordsCount, wordsCountSchema } from './words-count.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { chapterDelete, chapterDeleteSchema } from './chapter-delete.js';
|
|
2
|
+
export { chapterInfo, chapterInfoSchema } from './chapter-info.js';
|
|
3
|
+
export { chapterInsert, chapterInsertSchema } from './chapter-insert.js';
|
|
4
|
+
export { chapterRefresh, chapterRefreshSchema } from './chapter-refresh.js';
|
|
5
|
+
export { episodeInfo, episodeInfoSchema } from './episode-info.js';
|
|
6
|
+
export { episodeUpdate, episodeUpdateSchema } from './episode-update.js';
|
|
7
|
+
export { stats, statsSchema } from './stats.js';
|
|
8
|
+
export { timelineSync, timelineSyncSchema } from './timeline-sync.js';
|
|
9
|
+
export { wordsCount, wordsCountSchema } from './words-count.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Tracker } from '@echoes-io/tracker';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const statsSchema: z.ZodObject<{
|
|
4
|
+
arc: z.ZodOptional<z.ZodString>;
|
|
5
|
+
episode: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
pov: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
arc?: string | undefined;
|
|
9
|
+
episode?: number | undefined;
|
|
10
|
+
pov?: string | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
arc?: string | undefined;
|
|
13
|
+
episode?: number | undefined;
|
|
14
|
+
pov?: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function stats(args: z.infer<typeof statsSchema>, tracker: Tracker): Promise<{
|
|
17
|
+
content: {
|
|
18
|
+
type: "text";
|
|
19
|
+
text: string;
|
|
20
|
+
}[];
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getTimeline } from '../utils.js';
|
|
3
|
+
export const statsSchema = z.object({
|
|
4
|
+
arc: z.string().optional().describe('Filter by arc name'),
|
|
5
|
+
episode: z.number().optional().describe('Filter by episode number'),
|
|
6
|
+
pov: z.string().optional().describe('Filter by POV character'),
|
|
7
|
+
});
|
|
8
|
+
export async function stats(args, tracker) {
|
|
9
|
+
try {
|
|
10
|
+
const timeline = getTimeline();
|
|
11
|
+
let chapters = [];
|
|
12
|
+
// Get chapters based on filters
|
|
13
|
+
if (args.arc && args.episode) {
|
|
14
|
+
chapters = await tracker.getChapters(timeline, args.arc, args.episode);
|
|
15
|
+
}
|
|
16
|
+
else if (args.arc) {
|
|
17
|
+
const episodes = await tracker.getEpisodes(timeline, args.arc);
|
|
18
|
+
for (const ep of episodes) {
|
|
19
|
+
const epChapters = await tracker.getChapters(timeline, args.arc, ep.number);
|
|
20
|
+
chapters.push(...epChapters);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const arcs = await tracker.getArcs(timeline);
|
|
25
|
+
for (const arc of arcs) {
|
|
26
|
+
const episodes = await tracker.getEpisodes(timeline, arc.name);
|
|
27
|
+
for (const ep of episodes) {
|
|
28
|
+
const epChapters = await tracker.getChapters(timeline, arc.name, ep.number);
|
|
29
|
+
chapters.push(...epChapters);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Filter by POV if specified
|
|
34
|
+
if (args.pov) {
|
|
35
|
+
chapters = chapters.filter((ch) => ch.pov === args.pov);
|
|
36
|
+
}
|
|
37
|
+
// Calculate statistics
|
|
38
|
+
const totalWords = chapters.reduce((sum, ch) => sum + ch.words, 0);
|
|
39
|
+
const totalChapters = chapters.length;
|
|
40
|
+
// POV distribution
|
|
41
|
+
const povStats = {};
|
|
42
|
+
for (const ch of chapters) {
|
|
43
|
+
if (!povStats[ch.pov]) {
|
|
44
|
+
povStats[ch.pov] = { chapters: 0, words: 0 };
|
|
45
|
+
}
|
|
46
|
+
povStats[ch.pov].chapters++;
|
|
47
|
+
povStats[ch.pov].words += ch.words;
|
|
48
|
+
}
|
|
49
|
+
// Arc breakdown (if not filtered by arc)
|
|
50
|
+
const arcStats = {};
|
|
51
|
+
if (!args.arc) {
|
|
52
|
+
for (const ch of chapters) {
|
|
53
|
+
if (!arcStats[ch.arcName]) {
|
|
54
|
+
arcStats[ch.arcName] = { chapters: 0, words: 0, episodes: new Set() };
|
|
55
|
+
}
|
|
56
|
+
arcStats[ch.arcName].chapters++;
|
|
57
|
+
arcStats[ch.arcName].words += ch.words;
|
|
58
|
+
arcStats[ch.arcName].episodes.add(ch.episodeNumber);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Episode breakdown (if filtered by arc but not episode)
|
|
62
|
+
const episodeStats = {};
|
|
63
|
+
if (args.arc && !args.episode) {
|
|
64
|
+
for (const ch of chapters) {
|
|
65
|
+
if (!episodeStats[ch.episodeNumber]) {
|
|
66
|
+
episodeStats[ch.episodeNumber] = { chapters: 0, words: 0 };
|
|
67
|
+
}
|
|
68
|
+
episodeStats[ch.episodeNumber].chapters++;
|
|
69
|
+
episodeStats[ch.episodeNumber].words += ch.words;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const result = {
|
|
73
|
+
timeline,
|
|
74
|
+
filters: {
|
|
75
|
+
arc: args.arc || null,
|
|
76
|
+
episode: args.episode || null,
|
|
77
|
+
pov: args.pov || null,
|
|
78
|
+
},
|
|
79
|
+
summary: {
|
|
80
|
+
totalChapters,
|
|
81
|
+
totalWords,
|
|
82
|
+
averageChapterLength: totalChapters > 0 ? Math.round(totalWords / totalChapters) : 0,
|
|
83
|
+
},
|
|
84
|
+
povDistribution: Object.entries(povStats).map(([pov, stats]) => ({
|
|
85
|
+
pov,
|
|
86
|
+
chapters: stats.chapters,
|
|
87
|
+
words: stats.words,
|
|
88
|
+
percentage: totalWords > 0 ? Math.round((stats.words / totalWords) * 100) : 0,
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
if (!args.arc && Object.keys(arcStats).length > 0) {
|
|
92
|
+
result.arcBreakdown = Object.entries(arcStats).map(([arc, stats]) => ({
|
|
93
|
+
arc,
|
|
94
|
+
chapters: stats.chapters,
|
|
95
|
+
words: stats.words,
|
|
96
|
+
episodes: stats.episodes.size,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
if (args.arc && !args.episode && Object.keys(episodeStats).length > 0) {
|
|
100
|
+
result.episodeBreakdown = Object.entries(episodeStats).map(([episode, stats]) => ({
|
|
101
|
+
episode: Number(episode),
|
|
102
|
+
chapters: stats.chapters,
|
|
103
|
+
words: stats.words,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
if (totalChapters > 0) {
|
|
107
|
+
const sortedByWords = [...chapters].sort((a, b) => b.words - a.words);
|
|
108
|
+
result.extremes = {
|
|
109
|
+
longest: {
|
|
110
|
+
title: sortedByWords[0].title,
|
|
111
|
+
pov: sortedByWords[0].pov,
|
|
112
|
+
words: sortedByWords[0].words,
|
|
113
|
+
},
|
|
114
|
+
shortest: {
|
|
115
|
+
title: sortedByWords[sortedByWords.length - 1].title,
|
|
116
|
+
pov: sortedByWords[sortedByWords.length - 1].pov,
|
|
117
|
+
words: sortedByWords[sortedByWords.length - 1].words,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: JSON.stringify(result, null, 2),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw new Error(`Failed to get stats: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Tracker } from '@echoes-io/tracker';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const timelineSyncSchema: z.ZodObject<{
|
|
4
|
+
contentPath: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
contentPath: string;
|
|
7
|
+
}, {
|
|
8
|
+
contentPath: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function timelineSync(args: z.infer<typeof timelineSyncSchema>, tracker: Tracker): Promise<{
|
|
11
|
+
content: {
|
|
12
|
+
type: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
import { getTextStats, parseMarkdown } from '@echoes-io/utils';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { getTimeline } from '../utils.js';
|
|
6
|
+
export const timelineSyncSchema = z.object({
|
|
7
|
+
contentPath: z.string().describe('Path to content directory'),
|
|
8
|
+
});
|
|
9
|
+
export async function timelineSync(args, tracker) {
|
|
10
|
+
try {
|
|
11
|
+
const timeline = getTimeline();
|
|
12
|
+
let added = 0, updated = 0, deleted = 0, errors = 0;
|
|
13
|
+
let timelineRecord = await tracker.getTimeline(timeline);
|
|
14
|
+
if (!timelineRecord) {
|
|
15
|
+
timelineRecord = await tracker.createTimeline({
|
|
16
|
+
name: timeline,
|
|
17
|
+
description: `Timeline ${timeline}`,
|
|
18
|
+
});
|
|
19
|
+
added++;
|
|
20
|
+
}
|
|
21
|
+
const arcs = readdirSync(args.contentPath, { withFileTypes: true })
|
|
22
|
+
.filter((entry) => entry.isDirectory())
|
|
23
|
+
.map((entry) => entry.name);
|
|
24
|
+
for (const arcName of arcs) {
|
|
25
|
+
const arcPath = join(args.contentPath, arcName);
|
|
26
|
+
let arc = await tracker.getArc(timeline, arcName);
|
|
27
|
+
if (!arc) {
|
|
28
|
+
arc = await tracker.createArc({
|
|
29
|
+
timelineName: timeline,
|
|
30
|
+
name: arcName,
|
|
31
|
+
number: 1,
|
|
32
|
+
description: `Arc ${arcName}`,
|
|
33
|
+
});
|
|
34
|
+
added++;
|
|
35
|
+
}
|
|
36
|
+
const episodes = readdirSync(arcPath, { withFileTypes: true })
|
|
37
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('ep'))
|
|
38
|
+
.map((entry) => ({
|
|
39
|
+
name: entry.name,
|
|
40
|
+
number: Number.parseInt(entry.name.match(/ep(\d+)/)?.[1] || '0', 10),
|
|
41
|
+
}));
|
|
42
|
+
for (const ep of episodes) {
|
|
43
|
+
const episodePath = join(arcPath, ep.name);
|
|
44
|
+
let episode = await tracker.getEpisode(timeline, arcName, ep.number);
|
|
45
|
+
if (!episode) {
|
|
46
|
+
episode = await tracker.createEpisode({
|
|
47
|
+
timelineName: timeline,
|
|
48
|
+
arcName: arcName,
|
|
49
|
+
number: ep.number,
|
|
50
|
+
slug: ep.name,
|
|
51
|
+
title: ep.name,
|
|
52
|
+
description: `Episode ${ep.number}`,
|
|
53
|
+
});
|
|
54
|
+
added++;
|
|
55
|
+
}
|
|
56
|
+
const chapters = readdirSync(episodePath)
|
|
57
|
+
.filter((file) => extname(file) === '.md')
|
|
58
|
+
.map((file) => {
|
|
59
|
+
try {
|
|
60
|
+
const filePath = join(episodePath, file);
|
|
61
|
+
const content = require('node:fs').readFileSync(filePath, 'utf-8');
|
|
62
|
+
const { metadata, content: markdownContent } = parseMarkdown(content);
|
|
63
|
+
const stats = getTextStats(markdownContent);
|
|
64
|
+
return {
|
|
65
|
+
file: filePath,
|
|
66
|
+
metadata,
|
|
67
|
+
stats,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (_error) {
|
|
71
|
+
errors++;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.filter((ch) => ch !== null);
|
|
76
|
+
for (const chapterData of chapters) {
|
|
77
|
+
if (!chapterData)
|
|
78
|
+
continue;
|
|
79
|
+
const chNumber = chapterData.metadata.chapter;
|
|
80
|
+
if (!chNumber)
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const existing = await tracker.getChapter(timeline, arcName, ep.number, chNumber);
|
|
84
|
+
const data = {
|
|
85
|
+
timelineName: timeline,
|
|
86
|
+
arcName: arcName,
|
|
87
|
+
episodeNumber: ep.number,
|
|
88
|
+
partNumber: chapterData.metadata.part || 1,
|
|
89
|
+
number: chNumber,
|
|
90
|
+
pov: chapterData.metadata.pov || 'Unknown',
|
|
91
|
+
title: chapterData.metadata.title || 'Untitled',
|
|
92
|
+
date: new Date(chapterData.metadata.date || Date.now()),
|
|
93
|
+
excerpt: chapterData.metadata.excerpt || '',
|
|
94
|
+
location: chapterData.metadata.location || '',
|
|
95
|
+
outfit: chapterData.metadata.outfit || '',
|
|
96
|
+
kink: chapterData.metadata.kink || '',
|
|
97
|
+
words: chapterData.stats.words,
|
|
98
|
+
characters: chapterData.stats.characters,
|
|
99
|
+
charactersNoSpaces: chapterData.stats.charactersNoSpaces,
|
|
100
|
+
paragraphs: chapterData.stats.paragraphs,
|
|
101
|
+
sentences: chapterData.stats.sentences,
|
|
102
|
+
readingTimeMinutes: Math.ceil(chapterData.stats.words / 200),
|
|
103
|
+
};
|
|
104
|
+
if (existing) {
|
|
105
|
+
await tracker.updateChapter(timeline, arcName, ep.number, chNumber, data);
|
|
106
|
+
updated++;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await tracker.createChapter(data);
|
|
110
|
+
added++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (_error) {
|
|
114
|
+
errors++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const dbArcs = await tracker.getArcs(timeline);
|
|
121
|
+
for (const arc of dbArcs) {
|
|
122
|
+
const dbEpisodes = await tracker.getEpisodes(timeline, arc.name);
|
|
123
|
+
for (const episode of dbEpisodes) {
|
|
124
|
+
const allChapters = await tracker.getChapters(timeline, arc.name, episode.number);
|
|
125
|
+
for (const dbChapter of allChapters) {
|
|
126
|
+
let fileExists = false;
|
|
127
|
+
try {
|
|
128
|
+
const arcPath = join(args.contentPath, dbChapter.arcName);
|
|
129
|
+
if (existsSync(arcPath)) {
|
|
130
|
+
const episodeDirs = readdirSync(arcPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() &&
|
|
131
|
+
entry.name.startsWith(`ep${dbChapter.episodeNumber.toString().padStart(2, '0')}-`));
|
|
132
|
+
for (const episodeDir of episodeDirs) {
|
|
133
|
+
const episodePath = join(arcPath, episodeDir.name);
|
|
134
|
+
const chapterFiles = readdirSync(episodePath).filter((file) => file.includes(`ch${dbChapter.number.toString().padStart(3, '0')}`) &&
|
|
135
|
+
file.endsWith('.md'));
|
|
136
|
+
if (chapterFiles.length > 0) {
|
|
137
|
+
fileExists = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (_error) {
|
|
144
|
+
fileExists = false;
|
|
145
|
+
}
|
|
146
|
+
if (!fileExists) {
|
|
147
|
+
try {
|
|
148
|
+
await tracker.deleteChapter(dbChapter.timelineName, dbChapter.arcName, dbChapter.episodeNumber, dbChapter.number);
|
|
149
|
+
deleted++;
|
|
150
|
+
}
|
|
151
|
+
catch (_error) {
|
|
152
|
+
errors++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (_error) {
|
|
160
|
+
errors++;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: JSON.stringify({
|
|
167
|
+
timeline,
|
|
168
|
+
contentPath: args.contentPath,
|
|
169
|
+
summary: {
|
|
170
|
+
added,
|
|
171
|
+
updated,
|
|
172
|
+
deleted,
|
|
173
|
+
errors,
|
|
174
|
+
},
|
|
175
|
+
}, null, 2),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw new Error(`Failed to sync timeline: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const wordsCountSchema: z.ZodObject<{
|
|
3
|
+
file: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
file: string;
|
|
6
|
+
}, {
|
|
7
|
+
file: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function wordsCount(args: z.infer<typeof wordsCountSchema>): Promise<{
|
|
10
|
+
content: {
|
|
11
|
+
type: "text";
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { getTextStats } from '@echoes-io/utils';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export const wordsCountSchema = z.object({
|
|
5
|
+
file: z.string().describe('Path to markdown file'),
|
|
6
|
+
});
|
|
7
|
+
export async function wordsCount(args) {
|
|
8
|
+
try {
|
|
9
|
+
const content = readFileSync(args.file, 'utf-8');
|
|
10
|
+
const stats = getTextStats(content);
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: 'text',
|
|
15
|
+
text: JSON.stringify({
|
|
16
|
+
file: args.file,
|
|
17
|
+
words: stats.words,
|
|
18
|
+
characters: stats.characters,
|
|
19
|
+
charactersNoSpaces: stats.charactersNoSpaces,
|
|
20
|
+
paragraphs: stats.paragraphs,
|
|
21
|
+
sentences: stats.sentences,
|
|
22
|
+
}, null, 2),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`Failed to count words: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getTimeline(): string;
|
package/lib/utils.js
ADDED
package/package.json
CHANGED