@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.
- package/dist/library/manager.d.ts +42 -0
- package/dist/library/manager.js +218 -0
- package/dist/library/query.d.ts +26 -0
- package/dist/library/query.js +104 -0
- package/dist/library/schemas.d.ts +324 -0
- package/dist/library/schemas.js +79 -0
- package/dist/library/storage.d.ts +17 -0
- package/dist/library/storage.js +29 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +82 -0
- package/dist/tools/adopt.d.ts +24 -0
- package/dist/tools/adopt.js +143 -0
- package/dist/tools/brief.d.ts +34 -0
- package/dist/tools/brief.js +161 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/record.d.ts +40 -0
- package/dist/tools/record.js +186 -0
- package/package.json +45 -0
|
@@ -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
|
+
}
|