@echoes-io/mcp-server 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/cli/index.d.ts +2 -0
  2. package/cli/index.js +186 -0
  3. package/package.json +2 -1
  4. package/src/database/index.d.ts +6 -0
  5. package/src/database/index.js +26 -0
  6. package/src/database/relations.d.ts +744 -0
  7. package/src/database/relations.js +52 -0
  8. package/src/database/schema.d.ts +733 -0
  9. package/src/database/schema.js +69 -0
  10. package/src/database/vector.d.ts +25 -0
  11. package/src/database/vector.js +98 -0
  12. package/src/index.d.ts +5 -0
  13. package/src/index.js +5 -0
  14. package/src/rag/character-ner.d.ts +36 -0
  15. package/src/rag/character-ner.js +416 -0
  16. package/src/rag/database-sync.d.ts +38 -0
  17. package/src/rag/database-sync.js +158 -0
  18. package/src/rag/embeddings.d.ts +74 -0
  19. package/src/rag/embeddings.js +164 -0
  20. package/src/rag/graph-rag.d.ts +69 -0
  21. package/src/rag/graph-rag.js +311 -0
  22. package/src/rag/hybrid-rag.d.ts +109 -0
  23. package/src/rag/hybrid-rag.js +255 -0
  24. package/src/rag/index.d.ts +16 -0
  25. package/src/rag/index.js +33 -0
  26. package/src/server.d.ts +43 -0
  27. package/src/server.js +177 -0
  28. package/src/tools/index-rag.d.ts +19 -0
  29. package/src/tools/index-rag.js +85 -0
  30. package/src/tools/index-tracker.d.ts +17 -0
  31. package/src/tools/index-tracker.js +89 -0
  32. package/src/tools/index.d.ts +5 -0
  33. package/src/tools/index.js +5 -0
  34. package/src/tools/rag-context.d.ts +34 -0
  35. package/src/tools/rag-context.js +51 -0
  36. package/src/tools/rag-search.d.ts +35 -0
  37. package/src/tools/rag-search.js +60 -0
  38. package/src/tools/words-count.d.ts +15 -0
  39. package/src/tools/words-count.js +28 -0
  40. package/src/types/frontmatter.d.ts +35 -0
  41. package/src/types/frontmatter.js +1 -0
  42. package/src/utils/index.d.ts +1 -0
  43. package/src/utils/index.js +1 -0
  44. package/src/utils/markdown.d.ts +6 -0
  45. package/src/utils/markdown.js +36 -0
  46. package/src/utils/timeline-detection.d.ts +13 -0
  47. package/src/utils/timeline-detection.js +76 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * RAG Module - GraphRAG + Vector Search for Echoes
3
+ *
4
+ * Provides hybrid search capabilities combining:
5
+ * - GraphRAG: Semantic relationships, character connections, temporal sequences
6
+ * - Vector Search: Fast similarity search with sqlite-vec fallback
7
+ */
8
+ // Embedding providers
9
+ export { BGEBaseEmbedding, batchArray, cosineSimilarity, createEmbeddingProvider, E5SmallEmbedding, GeminiEmbedding, normalizeEmbedding, } from './embeddings.js';
10
+ // Core GraphRAG implementation
11
+ export { GraphRAG, } from './graph-rag.js';
12
+ // Hybrid RAG system
13
+ export { DEFAULT_HYBRID_CONFIG, HybridRAG, } from './hybrid-rag.js';
14
+ import { DEFAULT_HYBRID_CONFIG, HybridRAG } from './hybrid-rag.js';
15
+ export function createHybridRAG(db, config = {}) {
16
+ const fullConfig = {
17
+ ...DEFAULT_HYBRID_CONFIG,
18
+ ...config,
19
+ embedding: {
20
+ ...DEFAULT_HYBRID_CONFIG.embedding,
21
+ ...config.embedding,
22
+ },
23
+ graphRAG: {
24
+ ...DEFAULT_HYBRID_CONFIG.graphRAG,
25
+ ...config.graphRAG,
26
+ },
27
+ fallback: {
28
+ ...DEFAULT_HYBRID_CONFIG.fallback,
29
+ ...config.fallback,
30
+ },
31
+ };
32
+ return new HybridRAG(db, fullConfig);
33
+ }
@@ -0,0 +1,43 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ declare const server: Server<{
3
+ method: string;
4
+ params?: {
5
+ [x: string]: unknown;
6
+ task?: {
7
+ [x: string]: unknown;
8
+ ttl?: number | null | undefined;
9
+ pollInterval?: number | undefined;
10
+ } | undefined;
11
+ _meta?: {
12
+ [x: string]: unknown;
13
+ progressToken?: string | number | undefined;
14
+ "io.modelcontextprotocol/related-task"?: {
15
+ [x: string]: unknown;
16
+ taskId: string;
17
+ } | undefined;
18
+ } | undefined;
19
+ } | undefined;
20
+ }, {
21
+ method: string;
22
+ params?: {
23
+ [x: string]: unknown;
24
+ _meta?: {
25
+ [x: string]: unknown;
26
+ "io.modelcontextprotocol/related-task"?: {
27
+ [x: string]: unknown;
28
+ taskId: string;
29
+ } | undefined;
30
+ } | undefined;
31
+ } | undefined;
32
+ }, {
33
+ [x: string]: unknown;
34
+ _meta?: {
35
+ [x: string]: unknown;
36
+ "io.modelcontextprotocol/related-task"?: {
37
+ [x: string]: unknown;
38
+ taskId: string;
39
+ } | undefined;
40
+ } | undefined;
41
+ }>;
42
+ export declare function runServer(): Promise<void>;
43
+ export { server };
package/src/server.js ADDED
@@ -0,0 +1,177 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { indexRag, indexRagSchema } from './tools/index-rag.js';
5
+ import { indexTracker, indexTrackerSchema } from './tools/index-tracker.js';
6
+ import { ragContext, ragContextSchema } from './tools/rag-context.js';
7
+ import { ragSearch, ragSearchSchema } from './tools/rag-search.js';
8
+ import { wordsCount, wordsCountSchema } from './tools/words-count.js';
9
+ const server = new Server({
10
+ name: 'echoes-mcp-server',
11
+ version: '3.0.0',
12
+ }, {
13
+ capabilities: {
14
+ tools: {},
15
+ },
16
+ });
17
+ // List available tools
18
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
19
+ return {
20
+ tools: [
21
+ {
22
+ name: 'words-count',
23
+ description: 'Count words and text statistics in markdown files',
24
+ inputSchema: wordsCountSchema,
25
+ },
26
+ {
27
+ name: 'index-tracker',
28
+ description: 'Synchronize filesystem content with database',
29
+ inputSchema: indexTrackerSchema,
30
+ },
31
+ {
32
+ name: 'index-rag',
33
+ description: 'Index chapters into GraphRAG for semantic search',
34
+ inputSchema: indexRagSchema,
35
+ },
36
+ {
37
+ name: 'rag-search',
38
+ description: 'Search chapters using semantic similarity and character filtering',
39
+ inputSchema: ragSearchSchema,
40
+ },
41
+ {
42
+ name: 'rag-context',
43
+ description: 'Retrieve full chapter content for AI context using semantic search',
44
+ inputSchema: ragContextSchema,
45
+ },
46
+ ],
47
+ };
48
+ });
49
+ // Handle tool calls
50
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
51
+ const { name, arguments: args } = request.params;
52
+ switch (name) {
53
+ case 'words-count':
54
+ try {
55
+ const result = await wordsCount(args);
56
+ return {
57
+ content: [
58
+ {
59
+ type: 'text',
60
+ text: JSON.stringify(result, null, 2),
61
+ },
62
+ ],
63
+ };
64
+ }
65
+ catch (error) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
71
+ },
72
+ ],
73
+ isError: true,
74
+ };
75
+ }
76
+ case 'index-tracker':
77
+ try {
78
+ const result = await indexTracker(args);
79
+ return {
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: JSON.stringify(result, null, 2),
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ catch (error) {
89
+ return {
90
+ content: [
91
+ {
92
+ type: 'text',
93
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
94
+ },
95
+ ],
96
+ isError: true,
97
+ };
98
+ }
99
+ case 'index-rag':
100
+ try {
101
+ const result = await indexRag(args);
102
+ return {
103
+ content: [
104
+ {
105
+ type: 'text',
106
+ text: JSON.stringify(result, null, 2),
107
+ },
108
+ ],
109
+ };
110
+ }
111
+ catch (error) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
117
+ },
118
+ ],
119
+ isError: true,
120
+ };
121
+ }
122
+ case 'rag-search':
123
+ try {
124
+ const result = await ragSearch(args);
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: JSON.stringify(result, null, 2),
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ catch (error) {
135
+ return {
136
+ content: [
137
+ {
138
+ type: 'text',
139
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
140
+ },
141
+ ],
142
+ isError: true,
143
+ };
144
+ }
145
+ case 'rag-context':
146
+ try {
147
+ const result = await ragContext(args);
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text',
152
+ text: JSON.stringify(result, null, 2),
153
+ },
154
+ ],
155
+ };
156
+ }
157
+ catch (error) {
158
+ return {
159
+ content: [
160
+ {
161
+ type: 'text',
162
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
163
+ },
164
+ ],
165
+ isError: true,
166
+ };
167
+ }
168
+ default:
169
+ throw new Error(`Unknown tool: ${name}`);
170
+ }
171
+ });
172
+ export async function runServer() {
173
+ const transport = new StdioServerTransport();
174
+ await server.connect(transport);
175
+ console.error('Echoes MCP Server running on stdio');
176
+ }
177
+ export { server };
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export declare const indexRagSchema: z.ZodObject<{
3
+ timeline: z.ZodString;
4
+ contentPath: z.ZodString;
5
+ arc: z.ZodOptional<z.ZodString>;
6
+ episode: z.ZodOptional<z.ZodNumber>;
7
+ }, z.core.$strip>;
8
+ export type IndexRagInput = z.infer<typeof indexRagSchema>;
9
+ export interface IndexRagOutput {
10
+ indexed: number;
11
+ graphNodes: number;
12
+ vectorEmbeddings: number;
13
+ relationships: number;
14
+ timelines: number;
15
+ arcs: number;
16
+ episodes: number;
17
+ chapters: number;
18
+ }
19
+ export declare function indexRag(input: IndexRagInput): Promise<IndexRagOutput>;
@@ -0,0 +1,85 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { initDatabase } from '../database/index.js';
5
+ import { ItalianCharacterNER } from '../rag/character-ner.js';
6
+ import { createHybridRAG } from '../rag/index.js';
7
+ import { parseMarkdown } from '../utils/markdown.js';
8
+ export const indexRagSchema = z.object({
9
+ timeline: z.string().describe('Timeline name'),
10
+ contentPath: z.string().describe('Path to content directory'),
11
+ arc: z.string().optional().describe('Filter by specific arc'),
12
+ episode: z.number().optional().describe('Filter by specific episode'),
13
+ });
14
+ export async function indexRag(input) {
15
+ const { timeline, contentPath, arc, episode } = indexRagSchema.parse(input);
16
+ // Initialize database and RAG system
17
+ const db = await initDatabase(':memory:');
18
+ const rag = createHybridRAG(db);
19
+ const ner = new ItalianCharacterNER();
20
+ // Scan filesystem for markdown files
21
+ const chapters = [];
22
+ function scanDirectory(dir) {
23
+ try {
24
+ const entries = readdirSync(dir);
25
+ for (const entry of entries) {
26
+ const fullPath = join(dir, entry);
27
+ const stat = statSync(fullPath);
28
+ if (stat.isDirectory()) {
29
+ scanDirectory(fullPath);
30
+ }
31
+ else if (entry.endsWith('.md')) {
32
+ try {
33
+ const fileContent = readFileSync(fullPath, 'utf-8');
34
+ const { metadata, content } = parseMarkdown(fileContent);
35
+ // Apply filters
36
+ if (arc && metadata.arc !== arc)
37
+ continue;
38
+ if (episode !== undefined && metadata.episode !== episode)
39
+ continue;
40
+ // Extract characters using NER
41
+ const characters = ner.extractCharacters(content);
42
+ chapters.push({
43
+ id: crypto.randomUUID(),
44
+ content,
45
+ characters,
46
+ metadata: {
47
+ chapterId: crypto.randomUUID(),
48
+ arc: metadata.arc || 'unknown',
49
+ episode: metadata.episode || 1,
50
+ chapter: metadata.chapter || 1,
51
+ pov: metadata.pov || 'unknown',
52
+ location: metadata.location,
53
+ timeline,
54
+ title: metadata.title,
55
+ summary: metadata.summary,
56
+ filePath: fullPath,
57
+ },
58
+ });
59
+ }
60
+ catch (error) {
61
+ console.warn(`Failed to parse ${fullPath}:`, error);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ catch (error) {
67
+ console.warn(`Failed to scan directory ${dir}:`, error);
68
+ }
69
+ }
70
+ scanDirectory(contentPath);
71
+ // Index into RAG system
72
+ const result = await rag.indexChapters(chapters);
73
+ // Calculate relationships (edges in graph)
74
+ const relationships = result.graphNodes > 0 ? result.graphNodes * 2 : 0; // Rough estimate
75
+ return {
76
+ indexed: chapters.length,
77
+ graphNodes: result.graphNodes,
78
+ vectorEmbeddings: result.vectorEmbeddings,
79
+ relationships,
80
+ timelines: result.dbSync.timelines,
81
+ arcs: result.dbSync.arcs,
82
+ episodes: result.dbSync.episodes,
83
+ chapters: result.dbSync.chapters,
84
+ };
85
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export declare const indexTrackerSchema: z.ZodObject<{
3
+ timeline: z.ZodString;
4
+ contentPath: z.ZodString;
5
+ }, z.core.$strip>;
6
+ export type IndexTrackerInput = z.infer<typeof indexTrackerSchema>;
7
+ export interface IndexTrackerOutput {
8
+ scanned: number;
9
+ added: number;
10
+ updated: number;
11
+ deleted: number;
12
+ timelines: number;
13
+ arcs: number;
14
+ episodes: number;
15
+ chapters: number;
16
+ }
17
+ export declare function indexTracker(input: IndexTrackerInput): Promise<IndexTrackerOutput>;
@@ -0,0 +1,89 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { initDatabase } from '../database/index.js';
5
+ import { DatabaseSync } from '../rag/database-sync.js';
6
+ import { parseMarkdown } from '../utils/markdown.js';
7
+ export const indexTrackerSchema = z.object({
8
+ timeline: z.string().describe('Timeline name'),
9
+ contentPath: z.string().describe('Path to content directory'),
10
+ });
11
+ export async function indexTracker(input) {
12
+ const { timeline, contentPath } = indexTrackerSchema.parse(input);
13
+ // Initialize database (use in-memory for now, or pass dbPath as parameter)
14
+ const db = await initDatabase(':memory:');
15
+ const dbSync = new DatabaseSync(db);
16
+ // Scan filesystem for markdown files
17
+ const chapterRecords = [];
18
+ let scanned = 0;
19
+ function scanDirectory(dir) {
20
+ try {
21
+ const entries = readdirSync(dir);
22
+ for (const entry of entries) {
23
+ const fullPath = join(dir, entry);
24
+ const stat = statSync(fullPath);
25
+ if (stat.isDirectory()) {
26
+ scanDirectory(fullPath);
27
+ }
28
+ else if (entry.endsWith('.md')) {
29
+ try {
30
+ const content = readFileSync(fullPath, 'utf-8');
31
+ const { metadata } = parseMarkdown(content);
32
+ // Extract path info for fallback
33
+ const relativePath = fullPath.replace(contentPath, '').replace(/^\//, '');
34
+ const pathParts = relativePath.split('/');
35
+ // Use metadata or infer from path
36
+ const arc = metadata.arc || pathParts[0] || 'unknown';
37
+ const episode = metadata.episode || extractEpisodeFromPath(pathParts[1]) || 1;
38
+ const chapter = metadata.chapter || extractChapterFromFilename(entry) || 1;
39
+ const pov = metadata.pov || 'unknown';
40
+ chapterRecords.push({
41
+ chapterId: crypto.randomUUID(),
42
+ timeline,
43
+ arc,
44
+ episode,
45
+ chapter,
46
+ pov,
47
+ title: metadata.title,
48
+ summary: metadata.summary,
49
+ location: metadata.location,
50
+ filePath: fullPath,
51
+ });
52
+ scanned++;
53
+ }
54
+ catch (error) {
55
+ console.warn(`Failed to parse ${fullPath}:`, error);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ catch (error) {
61
+ console.warn(`Failed to scan directory ${dir}:`, error);
62
+ }
63
+ }
64
+ scanDirectory(contentPath);
65
+ // Sync to database
66
+ const syncStats = await dbSync.syncChapters(chapterRecords);
67
+ return {
68
+ scanned,
69
+ added: syncStats.chapters, // New chapters added
70
+ updated: 0, // TODO: Track updates
71
+ deleted: 0, // TODO: Track deletions
72
+ timelines: syncStats.timelines,
73
+ arcs: syncStats.arcs,
74
+ episodes: syncStats.episodes,
75
+ chapters: syncStats.chapters,
76
+ };
77
+ }
78
+ function extractEpisodeFromPath(pathSegment) {
79
+ if (!pathSegment)
80
+ return null;
81
+ // Match patterns like "ep01-title", "episode-1", "01-title"
82
+ const match = pathSegment.match(/(?:ep|episode)?[-_]?(\d+)/i);
83
+ return match ? parseInt(match[1], 10) : null;
84
+ }
85
+ function extractChapterFromFilename(filename) {
86
+ // Match patterns like "ep01-ch001-title.md", "chapter-1.md", "001-title.md"
87
+ const match = filename.match(/(?:ch|chapter)?[-_]?(\d+)/i);
88
+ return match ? parseInt(match[1], 10) : null;
89
+ }
@@ -0,0 +1,5 @@
1
+ export * from './index-rag.js';
2
+ export * from './index-tracker.js';
3
+ export * from './rag-context.js';
4
+ export * from './rag-search.js';
5
+ export * from './words-count.js';
@@ -0,0 +1,5 @@
1
+ export * from './index-rag.js';
2
+ export * from './index-tracker.js';
3
+ export * from './rag-context.js';
4
+ export * from './rag-search.js';
5
+ export * from './words-count.js';
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export declare const ragContextSchema: z.ZodObject<{
3
+ timeline: z.ZodString;
4
+ query: z.ZodString;
5
+ maxChapters: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
6
+ characters: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ allCharacters: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
8
+ arc: z.ZodOptional<z.ZodString>;
9
+ pov: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>;
11
+ export type RagContextInput = z.infer<typeof ragContextSchema>;
12
+ export interface RagContextOutput {
13
+ chapters: Array<{
14
+ id: string;
15
+ chapterId: string;
16
+ fullContent: string;
17
+ characters: string[];
18
+ metadata: {
19
+ arc?: string;
20
+ episode?: number;
21
+ chapter?: number;
22
+ pov?: string;
23
+ title?: string;
24
+ location?: string;
25
+ [key: string]: unknown;
26
+ };
27
+ score: number;
28
+ source: 'graphrag' | 'vector';
29
+ }>;
30
+ totalChapters: number;
31
+ searchTime: number;
32
+ contextLength: number;
33
+ }
34
+ export declare function ragContext(input: RagContextInput): Promise<RagContextOutput>;
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ import { initDatabase } from '../database/index.js';
3
+ import { createHybridRAG } from '../rag/index.js';
4
+ export const ragContextSchema = z.object({
5
+ timeline: z.string().min(1, 'Timeline name is required').describe('Timeline name'),
6
+ query: z.string().min(1, 'Query is required').describe('Search query'),
7
+ maxChapters: z.number().optional().default(5).describe('Maximum number of chapters to return'),
8
+ characters: z.array(z.string()).optional().describe('Filter by character names'),
9
+ allCharacters: z
10
+ .boolean()
11
+ .optional()
12
+ .default(false)
13
+ .describe('Require all characters (AND) vs any (OR)'),
14
+ arc: z.string().optional().describe('Filter by arc'),
15
+ pov: z.string().optional().describe('Filter by point of view'),
16
+ });
17
+ export async function ragContext(input) {
18
+ const { timeline, query, maxChapters, characters, allCharacters, arc, pov } = ragContextSchema.parse(input);
19
+ const startTime = Date.now();
20
+ // Initialize database and RAG system
21
+ const db = await initDatabase(':memory:');
22
+ const rag = createHybridRAG(db);
23
+ // Perform search to get relevant chapters
24
+ const searchResults = await rag.search(query, {
25
+ topK: maxChapters,
26
+ characters,
27
+ allCharacters,
28
+ arc,
29
+ pov,
30
+ useGraphRAG: true,
31
+ });
32
+ const searchTime = Date.now() - startTime;
33
+ // Transform results to include full content for AI context
34
+ const chapters = searchResults.map((result) => ({
35
+ id: result.id,
36
+ chapterId: result.chapterId,
37
+ fullContent: result.content, // Full chapter content for AI context
38
+ characters: result.characters,
39
+ metadata: result.metadata,
40
+ score: result.score,
41
+ source: result.source,
42
+ }));
43
+ // Calculate total context length
44
+ const contextLength = chapters.reduce((total, chapter) => total + chapter.fullContent.length, 0);
45
+ return {
46
+ chapters,
47
+ totalChapters: chapters.length,
48
+ searchTime,
49
+ contextLength,
50
+ };
51
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ export declare const ragSearchSchema: z.ZodObject<{
3
+ timeline: z.ZodString;
4
+ query: z.ZodString;
5
+ topK: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
6
+ characters: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ allCharacters: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
8
+ arc: z.ZodOptional<z.ZodString>;
9
+ pov: z.ZodOptional<z.ZodString>;
10
+ useGraphRAG: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
11
+ }, z.core.$strip>;
12
+ export type RagSearchInput = z.infer<typeof ragSearchSchema>;
13
+ export interface RagSearchOutput {
14
+ results: Array<{
15
+ id: string;
16
+ chapterId: string;
17
+ content: string;
18
+ characters: string[];
19
+ metadata: {
20
+ arc?: string;
21
+ episode?: number;
22
+ chapter?: number;
23
+ pov?: string;
24
+ title?: string;
25
+ location?: string;
26
+ [key: string]: unknown;
27
+ };
28
+ score: number;
29
+ source: 'graphrag' | 'vector';
30
+ }>;
31
+ totalResults: number;
32
+ searchTime: number;
33
+ source: 'graphrag' | 'vector' | 'hybrid';
34
+ }
35
+ export declare function ragSearch(input: RagSearchInput): Promise<RagSearchOutput>;
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ import { initDatabase } from '../database/index.js';
3
+ import { createHybridRAG } from '../rag/index.js';
4
+ export const ragSearchSchema = z.object({
5
+ timeline: z.string().min(1, 'Timeline name is required').describe('Timeline name'),
6
+ query: z.string().min(1, 'Query is required').describe('Search query'),
7
+ topK: z.number().optional().default(10).describe('Maximum number of results'),
8
+ characters: z.array(z.string()).optional().describe('Filter by character names'),
9
+ allCharacters: z
10
+ .boolean()
11
+ .optional()
12
+ .default(false)
13
+ .describe('Require all characters (AND) vs any (OR)'),
14
+ arc: z.string().optional().describe('Filter by arc'),
15
+ pov: z.string().optional().describe('Filter by point of view'),
16
+ useGraphRAG: z
17
+ .boolean()
18
+ .optional()
19
+ .default(true)
20
+ .describe('Use GraphRAG (true) or vector search only (false)'),
21
+ });
22
+ export async function ragSearch(input) {
23
+ const { timeline, query, topK, characters, allCharacters, arc, pov, useGraphRAG } = ragSearchSchema.parse(input);
24
+ const startTime = Date.now();
25
+ // Initialize database and RAG system
26
+ const db = await initDatabase(':memory:');
27
+ const rag = createHybridRAG(db);
28
+ // Perform search
29
+ const results = await rag.search(query, {
30
+ topK,
31
+ characters,
32
+ allCharacters,
33
+ arc,
34
+ pov,
35
+ useGraphRAG,
36
+ });
37
+ const searchTime = Date.now() - startTime;
38
+ // Determine primary source
39
+ let primarySource = 'hybrid';
40
+ if (results.length > 0) {
41
+ const sources = new Set(results.map((r) => r.source));
42
+ if (sources.size === 1) {
43
+ primarySource = Array.from(sources)[0];
44
+ }
45
+ }
46
+ return {
47
+ results: results.map((result) => ({
48
+ id: result.id,
49
+ chapterId: result.chapterId,
50
+ content: result.content,
51
+ characters: result.characters,
52
+ metadata: result.metadata,
53
+ score: result.score,
54
+ source: result.source,
55
+ })),
56
+ totalResults: results.length,
57
+ searchTime,
58
+ source: primarySource,
59
+ };
60
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ export declare const wordsCountSchema: z.ZodObject<{
3
+ filePath: z.ZodString;
4
+ detailed: z.ZodOptional<z.ZodBoolean>;
5
+ }, z.core.$strip>;
6
+ export type WordsCountInput = z.infer<typeof wordsCountSchema>;
7
+ export interface WordsCountOutput {
8
+ words: number;
9
+ characters: number;
10
+ charactersNoSpaces: number;
11
+ readingTimeMinutes: number;
12
+ sentences?: number;
13
+ paragraphs?: number;
14
+ }
15
+ export declare function wordsCount(input: WordsCountInput): Promise<WordsCountOutput>;