@telvok/librarian-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ import type { LibraryEntry } from './schemas.js';
2
+ export declare class LibraryManager {
3
+ private libraryPath;
4
+ constructor();
5
+ /**
6
+ * Initialize the library directory structure.
7
+ */
8
+ initialize(): Promise<void>;
9
+ /**
10
+ * Get all entries from local library.
11
+ */
12
+ getLocalEntries(): Promise<LibraryEntry[]>;
13
+ /**
14
+ * Get all entries from imported libraries.
15
+ */
16
+ getImportedEntries(): Promise<LibraryEntry[]>;
17
+ /**
18
+ * Get all archived entries.
19
+ */
20
+ getArchivedEntries(): Promise<LibraryEntry[]>;
21
+ /**
22
+ * Query entries by topic.
23
+ */
24
+ queryByTopic(topic: string): Promise<LibraryEntry[]>;
25
+ /**
26
+ * Record a new entry to local library.
27
+ */
28
+ record(topics: string[], content: string): Promise<{
29
+ entry: LibraryEntry;
30
+ path: string;
31
+ }>;
32
+ /**
33
+ * Archive an entry (move to archived/).
34
+ */
35
+ archive(entryId: string): Promise<{
36
+ success: boolean;
37
+ message: string;
38
+ }>;
39
+ private readEntriesFromPath;
40
+ private findEntryById;
41
+ private fileExists;
42
+ }
@@ -0,0 +1,218 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { glob } from 'glob';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import { getLibraryPath, getLocalPath, getImportedPath, getArchivedPath, } from './storage.js';
7
+ // ============================================================================
8
+ // Library Manager
9
+ // ============================================================================
10
+ export class LibraryManager {
11
+ libraryPath;
12
+ constructor() {
13
+ this.libraryPath = getLibraryPath();
14
+ }
15
+ /**
16
+ * Initialize the library directory structure.
17
+ */
18
+ async initialize() {
19
+ const dirs = [
20
+ getLocalPath(this.libraryPath),
21
+ getImportedPath(this.libraryPath),
22
+ getArchivedPath(this.libraryPath),
23
+ ];
24
+ for (const dir of dirs) {
25
+ await fs.mkdir(dir, { recursive: true });
26
+ }
27
+ }
28
+ /**
29
+ * Get all entries from local library.
30
+ */
31
+ async getLocalEntries() {
32
+ const localPath = getLocalPath(this.libraryPath);
33
+ return this.readEntriesFromPath(localPath, 'local');
34
+ }
35
+ /**
36
+ * Get all entries from imported libraries.
37
+ */
38
+ async getImportedEntries() {
39
+ const importedPath = getImportedPath(this.libraryPath);
40
+ return this.readEntriesFromPath(importedPath, 'imported');
41
+ }
42
+ /**
43
+ * Get all archived entries.
44
+ */
45
+ async getArchivedEntries() {
46
+ const archivedPath = getArchivedPath(this.libraryPath);
47
+ return this.readEntriesFromPath(archivedPath, 'archived');
48
+ }
49
+ /**
50
+ * Query entries by topic.
51
+ */
52
+ async queryByTopic(topic) {
53
+ const [local, imported] = await Promise.all([
54
+ this.getLocalEntries(),
55
+ this.getImportedEntries(),
56
+ ]);
57
+ const allEntries = [...local, ...imported];
58
+ const searchTerm = topic.toLowerCase();
59
+ return allEntries.filter(entry => entry.topics.some(t => t.toLowerCase().includes(searchTerm)) ||
60
+ entry.content.toLowerCase().includes(searchTerm));
61
+ }
62
+ /**
63
+ * Record a new entry to local library.
64
+ */
65
+ async record(topics, content) {
66
+ const localPath = getLocalPath(this.libraryPath);
67
+ await fs.mkdir(localPath, { recursive: true });
68
+ const id = uuidv4();
69
+ const created = new Date().toISOString();
70
+ const entry = {
71
+ id,
72
+ topics,
73
+ content,
74
+ created,
75
+ source: 'local',
76
+ origin: 'manual',
77
+ };
78
+ // Generate filename
79
+ const slug = topics[0]
80
+ .toLowerCase()
81
+ .replace(/[^a-z0-9]+/g, '-')
82
+ .replace(/^-|-$/g, '');
83
+ const timestamp = created.slice(0, 10);
84
+ let filename = `${slug}-${timestamp}.md`;
85
+ let filePath = path.join(localPath, filename);
86
+ // Handle collisions
87
+ let counter = 1;
88
+ while (await this.fileExists(filePath)) {
89
+ filename = `${slug}-${timestamp}-${counter}.md`;
90
+ filePath = path.join(localPath, filename);
91
+ counter++;
92
+ }
93
+ // Write file
94
+ const frontmatter = {
95
+ id,
96
+ topics,
97
+ created,
98
+ source: 'manual',
99
+ };
100
+ const fileContent = matter.stringify(content, frontmatter);
101
+ await fs.writeFile(filePath, fileContent, 'utf-8');
102
+ return {
103
+ entry,
104
+ path: path.relative(this.libraryPath, filePath),
105
+ };
106
+ }
107
+ /**
108
+ * Archive an entry (move to archived/).
109
+ */
110
+ async archive(entryId) {
111
+ const localPath = getLocalPath(this.libraryPath);
112
+ const archivedPath = getArchivedPath(this.libraryPath);
113
+ // Find the entry
114
+ const found = await this.findEntryById(localPath, entryId);
115
+ if (!found) {
116
+ return { success: false, message: `Entry not found: ${entryId}` };
117
+ }
118
+ await fs.mkdir(archivedPath, { recursive: true });
119
+ const filename = path.basename(found.filePath);
120
+ const newPath = path.join(archivedPath, filename);
121
+ await fs.rename(found.filePath, newPath);
122
+ return {
123
+ success: true,
124
+ message: `Archived to ${path.relative(this.libraryPath, newPath)}`,
125
+ };
126
+ }
127
+ // ============================================================================
128
+ // Private Helpers
129
+ // ============================================================================
130
+ async readEntriesFromPath(dirPath, source) {
131
+ const entries = [];
132
+ try {
133
+ const files = await glob(path.join(dirPath, '**/*.md'), { nodir: true });
134
+ for (const filePath of files) {
135
+ try {
136
+ const content = await fs.readFile(filePath, 'utf-8');
137
+ const { data, content: body } = matter(content);
138
+ let topics;
139
+ if (Array.isArray(data.topics)) {
140
+ topics = data.topics;
141
+ }
142
+ else if (data.topic) {
143
+ topics = [data.topic];
144
+ }
145
+ else {
146
+ topics = ['general'];
147
+ }
148
+ entries.push({
149
+ id: data.id || uuidv4(),
150
+ topics,
151
+ content: body.trim(),
152
+ created: data.created || new Date().toISOString(),
153
+ source,
154
+ origin: data.source,
155
+ imported_from: data.imported_from,
156
+ });
157
+ }
158
+ catch {
159
+ // Skip unreadable files
160
+ }
161
+ }
162
+ }
163
+ catch {
164
+ // Directory doesn't exist
165
+ }
166
+ return entries;
167
+ }
168
+ async findEntryById(dirPath, entryId) {
169
+ try {
170
+ const files = await glob(path.join(dirPath, '**/*.md'), { nodir: true });
171
+ for (const filePath of files) {
172
+ try {
173
+ const content = await fs.readFile(filePath, 'utf-8');
174
+ const { data, content: body } = matter(content);
175
+ if (data.id === entryId) {
176
+ let topics;
177
+ if (Array.isArray(data.topics)) {
178
+ topics = data.topics;
179
+ }
180
+ else if (data.topic) {
181
+ topics = [data.topic];
182
+ }
183
+ else {
184
+ topics = ['general'];
185
+ }
186
+ return {
187
+ entry: {
188
+ id: data.id,
189
+ topics,
190
+ content: body.trim(),
191
+ created: data.created || new Date().toISOString(),
192
+ source: 'local',
193
+ origin: data.source,
194
+ },
195
+ filePath,
196
+ };
197
+ }
198
+ }
199
+ catch {
200
+ // Skip unreadable files
201
+ }
202
+ }
203
+ }
204
+ catch {
205
+ // Directory doesn't exist
206
+ }
207
+ return null;
208
+ }
209
+ async fileExists(filePath) {
210
+ try {
211
+ await fs.access(filePath);
212
+ return true;
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,26 @@
1
+ import type { LibraryEntry } from './schemas.js';
2
+ /**
3
+ * Score how well an entry matches a search term.
4
+ */
5
+ export declare function scoreMatch(entry: LibraryEntry, searchTerm: string): number;
6
+ /**
7
+ * Detect potential conflicts between entries.
8
+ * Returns true if entries on the same topic give contradictory advice.
9
+ */
10
+ export declare function detectConflict(entries: LibraryEntry[]): boolean;
11
+ /**
12
+ * Generate a conflict summary.
13
+ */
14
+ export declare function generateConflictSummary(entries: LibraryEntry[]): string;
15
+ /**
16
+ * Filter entries by topic.
17
+ */
18
+ export declare function filterByTopic(entries: LibraryEntry[], topic: string): LibraryEntry[];
19
+ /**
20
+ * Sort entries by relevance to search term.
21
+ */
22
+ export declare function sortByRelevance(entries: LibraryEntry[], searchTerm: string): LibraryEntry[];
23
+ /**
24
+ * Group entries by source.
25
+ */
26
+ export declare function groupBySource(entries: LibraryEntry[]): Record<string, LibraryEntry[]>;
@@ -0,0 +1,104 @@
1
+ // ============================================================================
2
+ // Query Utilities
3
+ // ============================================================================
4
+ /**
5
+ * Score how well an entry matches a search term.
6
+ */
7
+ export function scoreMatch(entry, searchTerm) {
8
+ const term = searchTerm.toLowerCase();
9
+ let score = 0;
10
+ // Topic matches (highest weight)
11
+ for (const topic of entry.topics) {
12
+ if (topic.toLowerCase() === term) {
13
+ score += 10; // Exact match
14
+ }
15
+ else if (topic.toLowerCase().includes(term)) {
16
+ score += 5; // Partial match
17
+ }
18
+ }
19
+ // Content matches
20
+ const contentLower = entry.content.toLowerCase();
21
+ if (contentLower.includes(term)) {
22
+ // Count occurrences
23
+ const matches = contentLower.split(term).length - 1;
24
+ score += Math.min(matches * 2, 8); // Cap at 8
25
+ }
26
+ return score;
27
+ }
28
+ /**
29
+ * Detect potential conflicts between entries.
30
+ * Returns true if entries on the same topic give contradictory advice.
31
+ */
32
+ export function detectConflict(entries) {
33
+ if (entries.length < 2)
34
+ return false;
35
+ // Simple heuristic: look for negation patterns
36
+ const negationPatterns = [
37
+ /don't|dont|never|avoid|stop/i,
38
+ /always|must|should/i,
39
+ ];
40
+ let hasNegation = false;
41
+ let hasAffirmation = false;
42
+ for (const entry of entries) {
43
+ if (negationPatterns[0].test(entry.content)) {
44
+ hasNegation = true;
45
+ }
46
+ if (negationPatterns[1].test(entry.content)) {
47
+ hasAffirmation = true;
48
+ }
49
+ }
50
+ // This is a very simple heuristic - could be improved with semantic analysis
51
+ return hasNegation && hasAffirmation;
52
+ }
53
+ /**
54
+ * Generate a conflict summary.
55
+ */
56
+ export function generateConflictSummary(entries) {
57
+ if (entries.length < 2)
58
+ return '';
59
+ const sources = entries.map(e => {
60
+ if (e.imported_from) {
61
+ return `imported/${e.imported_from}`;
62
+ }
63
+ return e.source;
64
+ });
65
+ const uniqueSources = [...new Set(sources)];
66
+ return `Found ${entries.length} entries that may conflict. Sources: ${uniqueSources.join(', ')}. Review and decide which to keep.`;
67
+ }
68
+ /**
69
+ * Filter entries by topic.
70
+ */
71
+ export function filterByTopic(entries, topic) {
72
+ const term = topic.toLowerCase();
73
+ return entries.filter(entry => entry.topics.some(t => t.toLowerCase().includes(term)));
74
+ }
75
+ /**
76
+ * Sort entries by relevance to search term.
77
+ */
78
+ export function sortByRelevance(entries, searchTerm) {
79
+ return [...entries].sort((a, b) => {
80
+ const scoreA = scoreMatch(a, searchTerm);
81
+ const scoreB = scoreMatch(b, searchTerm);
82
+ return scoreB - scoreA;
83
+ });
84
+ }
85
+ /**
86
+ * Group entries by source.
87
+ */
88
+ export function groupBySource(entries) {
89
+ const groups = {
90
+ local: [],
91
+ imported: [],
92
+ archived: [],
93
+ };
94
+ for (const entry of entries) {
95
+ const key = entry.source;
96
+ if (groups[key]) {
97
+ groups[key].push(entry);
98
+ }
99
+ else {
100
+ groups.local.push(entry); // Default to local
101
+ }
102
+ }
103
+ return groups;
104
+ }