@telvok/librarian-mcp 1.1.0 → 1.3.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/embeddings.d.ts +21 -0
- package/dist/library/embeddings.js +86 -0
- package/dist/library/parsers/cursor.d.ts +15 -0
- package/dist/library/parsers/cursor.js +168 -0
- package/dist/library/parsers/index.d.ts +4 -0
- package/dist/library/parsers/index.js +3 -0
- package/dist/library/parsers/jsonl.d.ts +9 -0
- package/dist/library/parsers/jsonl.js +53 -0
- package/dist/library/parsers/markdown.d.ts +15 -0
- package/dist/library/parsers/markdown.js +77 -0
- package/dist/library/parsers/types.d.ts +21 -0
- package/dist/library/parsers/types.js +4 -0
- package/dist/library/storage.d.ts +5 -0
- package/dist/library/storage.js +7 -0
- package/dist/library/vector-index.d.ts +55 -0
- package/dist/library/vector-index.js +160 -0
- package/dist/server.js +18 -0
- package/dist/tools/brief.js +60 -2
- package/dist/tools/import-memories.d.ts +32 -0
- package/dist/tools/import-memories.js +215 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/rebuild-index.d.ts +23 -0
- package/dist/tools/rebuild-index.js +105 -0
- package/dist/tools/record.js +21 -0
- package/package.json +3 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get embedding for a text string.
|
|
3
|
+
* Returns a 384-dimensional normalized vector.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getEmbedding(text: string): Promise<number[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Check if embeddings are available (model can load).
|
|
8
|
+
*/
|
|
9
|
+
export declare function isEmbeddingAvailable(): Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* Calculate cosine similarity between two vectors.
|
|
12
|
+
* Since vectors are normalized, this is just the dot product.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cosineSimilarity(a: number[], b: number[]): number;
|
|
15
|
+
/**
|
|
16
|
+
* Split text into chunks at sentence boundaries.
|
|
17
|
+
* Aims for ~500 chars per chunk to preserve semantic meaning.
|
|
18
|
+
*/
|
|
19
|
+
export declare function chunkText(text: string, maxChars?: number): string[];
|
|
20
|
+
export declare const EMBEDDING_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
|
|
21
|
+
export declare const EMBEDDING_DIMENSION = 384;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { pipeline, env } from '@huggingface/transformers';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getLibraryPath } from './storage.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Configuration
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Cache model in .librarian/models
|
|
8
|
+
env.allowRemoteModels = true;
|
|
9
|
+
const MODEL_ID = 'Xenova/all-MiniLM-L6-v2';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Embedding Generation
|
|
12
|
+
// ============================================================================
|
|
13
|
+
let embedder = null;
|
|
14
|
+
/**
|
|
15
|
+
* Get embedding for a text string.
|
|
16
|
+
* Returns a 384-dimensional normalized vector.
|
|
17
|
+
*/
|
|
18
|
+
export async function getEmbedding(text) {
|
|
19
|
+
if (!embedder) {
|
|
20
|
+
// Set local model path on first call
|
|
21
|
+
const libraryPath = getLibraryPath();
|
|
22
|
+
env.localModelPath = path.join(libraryPath, 'models');
|
|
23
|
+
embedder = await pipeline('feature-extraction', MODEL_ID);
|
|
24
|
+
}
|
|
25
|
+
const result = await embedder(text, { pooling: 'mean', normalize: true });
|
|
26
|
+
return Array.from(result.data);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if embeddings are available (model can load).
|
|
30
|
+
*/
|
|
31
|
+
export async function isEmbeddingAvailable() {
|
|
32
|
+
try {
|
|
33
|
+
await getEmbedding('test');
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Similarity Calculation
|
|
42
|
+
// ============================================================================
|
|
43
|
+
/**
|
|
44
|
+
* Calculate cosine similarity between two vectors.
|
|
45
|
+
* Since vectors are normalized, this is just the dot product.
|
|
46
|
+
*/
|
|
47
|
+
export function cosineSimilarity(a, b) {
|
|
48
|
+
if (a.length !== b.length) {
|
|
49
|
+
throw new Error('Vectors must have same dimension');
|
|
50
|
+
}
|
|
51
|
+
return a.reduce((sum, val, i) => sum + val * b[i], 0);
|
|
52
|
+
}
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Text Chunking
|
|
55
|
+
// ============================================================================
|
|
56
|
+
/**
|
|
57
|
+
* Split text into chunks at sentence boundaries.
|
|
58
|
+
* Aims for ~500 chars per chunk to preserve semantic meaning.
|
|
59
|
+
*/
|
|
60
|
+
export function chunkText(text, maxChars = 500) {
|
|
61
|
+
// Split at sentence boundaries (. ! ? followed by whitespace)
|
|
62
|
+
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let current = '';
|
|
65
|
+
for (const sentence of sentences) {
|
|
66
|
+
// If adding this sentence exceeds limit and we have content, start new chunk
|
|
67
|
+
if ((current + ' ' + sentence).length > maxChars && current.trim()) {
|
|
68
|
+
chunks.push(current.trim());
|
|
69
|
+
current = sentence;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
current = current ? current + ' ' + sentence : sentence;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Don't forget the last chunk
|
|
76
|
+
if (current.trim()) {
|
|
77
|
+
chunks.push(current.trim());
|
|
78
|
+
}
|
|
79
|
+
// If no chunks created (e.g., no sentence boundaries), return original text
|
|
80
|
+
return chunks.length > 0 ? chunks : [text];
|
|
81
|
+
}
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Constants
|
|
84
|
+
// ============================================================================
|
|
85
|
+
export const EMBEDDING_MODEL_ID = MODEL_ID;
|
|
86
|
+
export const EMBEDDING_DIMENSION = 384;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ParseResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a Cursor Memory Bank folder (.cursor-memory/).
|
|
4
|
+
*
|
|
5
|
+
* Cursor Memory Bank typically contains:
|
|
6
|
+
* - activeContext.md - Current working context
|
|
7
|
+
* - progress.md - Progress log
|
|
8
|
+
* - projectBrief.md - Project overview
|
|
9
|
+
* - systemPatterns.md - System patterns
|
|
10
|
+
* - decisionLog.md - Decision history
|
|
11
|
+
* - techStack.md - Technology stack info
|
|
12
|
+
*
|
|
13
|
+
* Can also contain JSON files and subdirectories.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseCursorMemory(dirPath: string): Promise<ParseResult>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Cursor Memory Bank Parser
|
|
7
|
+
// ============================================================================
|
|
8
|
+
/**
|
|
9
|
+
* Parse a Cursor Memory Bank folder (.cursor-memory/).
|
|
10
|
+
*
|
|
11
|
+
* Cursor Memory Bank typically contains:
|
|
12
|
+
* - activeContext.md - Current working context
|
|
13
|
+
* - progress.md - Progress log
|
|
14
|
+
* - projectBrief.md - Project overview
|
|
15
|
+
* - systemPatterns.md - System patterns
|
|
16
|
+
* - decisionLog.md - Decision history
|
|
17
|
+
* - techStack.md - Technology stack info
|
|
18
|
+
*
|
|
19
|
+
* Can also contain JSON files and subdirectories.
|
|
20
|
+
*/
|
|
21
|
+
export async function parseCursorMemory(dirPath) {
|
|
22
|
+
const entries = [];
|
|
23
|
+
const errors = [];
|
|
24
|
+
let skipped = 0;
|
|
25
|
+
try {
|
|
26
|
+
const stats = await fs.stat(dirPath);
|
|
27
|
+
if (!stats.isDirectory()) {
|
|
28
|
+
errors.push(`${dirPath} is not a directory`);
|
|
29
|
+
return { entries, skipped, errors };
|
|
30
|
+
}
|
|
31
|
+
// Parse markdown files
|
|
32
|
+
const mdFiles = await glob(path.join(dirPath, '**/*.md'), { nodir: true });
|
|
33
|
+
for (const filePath of mdFiles) {
|
|
34
|
+
try {
|
|
35
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
36
|
+
const { data, content: body } = matter(content);
|
|
37
|
+
const trimmedBody = body.trim();
|
|
38
|
+
if (!trimmedBody) {
|
|
39
|
+
skipped++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Extract title from filename (convert camelCase/snake_case to Title Case)
|
|
43
|
+
const filename = path.basename(filePath, '.md');
|
|
44
|
+
const title = data.title || formatFilename(filename);
|
|
45
|
+
// Determine context based on filename
|
|
46
|
+
const context = data.context || inferContext(filename);
|
|
47
|
+
entries.push({
|
|
48
|
+
title,
|
|
49
|
+
content: trimmedBody,
|
|
50
|
+
context,
|
|
51
|
+
intent: data.intent,
|
|
52
|
+
source: 'cursor',
|
|
53
|
+
originalPath: filePath,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (fileError) {
|
|
57
|
+
errors.push(`${filePath}: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
|
|
58
|
+
skipped++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Parse JSON files
|
|
62
|
+
const jsonFiles = await glob(path.join(dirPath, '**/*.json'), { nodir: true });
|
|
63
|
+
for (const filePath of jsonFiles) {
|
|
64
|
+
try {
|
|
65
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
66
|
+
const data = JSON.parse(content);
|
|
67
|
+
// Handle different JSON structures
|
|
68
|
+
if (Array.isArray(data)) {
|
|
69
|
+
// Array of memory items
|
|
70
|
+
for (const item of data) {
|
|
71
|
+
if (typeof item === 'object' && item !== null) {
|
|
72
|
+
const entry = extractFromJSON(item, filePath);
|
|
73
|
+
if (entry) {
|
|
74
|
+
entries.push(entry);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
skipped++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (typeof data === 'object' && data !== null) {
|
|
83
|
+
// Single memory object
|
|
84
|
+
const entry = extractFromJSON(data, filePath);
|
|
85
|
+
if (entry) {
|
|
86
|
+
entries.push(entry);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
skipped++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (fileError) {
|
|
94
|
+
errors.push(`${filePath}: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
|
|
95
|
+
skipped++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (dirError) {
|
|
100
|
+
errors.push(`Failed to access path: ${dirError instanceof Error ? dirError.message : String(dirError)}`);
|
|
101
|
+
}
|
|
102
|
+
return { entries, skipped, errors };
|
|
103
|
+
}
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Helper Functions
|
|
106
|
+
// ============================================================================
|
|
107
|
+
/**
|
|
108
|
+
* Convert filename to readable title.
|
|
109
|
+
* activeContext -> Active Context
|
|
110
|
+
* system_patterns -> System Patterns
|
|
111
|
+
*/
|
|
112
|
+
function formatFilename(filename) {
|
|
113
|
+
return filename
|
|
114
|
+
// Handle camelCase
|
|
115
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
116
|
+
// Handle snake_case
|
|
117
|
+
.replace(/_/g, ' ')
|
|
118
|
+
// Handle kebab-case
|
|
119
|
+
.replace(/-/g, ' ')
|
|
120
|
+
// Capitalize first letter of each word
|
|
121
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Infer context from Cursor Memory Bank filename patterns.
|
|
125
|
+
*/
|
|
126
|
+
function inferContext(filename) {
|
|
127
|
+
const lower = filename.toLowerCase();
|
|
128
|
+
if (lower.includes('context'))
|
|
129
|
+
return 'context';
|
|
130
|
+
if (lower.includes('progress'))
|
|
131
|
+
return 'progress';
|
|
132
|
+
if (lower.includes('brief') || lower.includes('overview'))
|
|
133
|
+
return 'project';
|
|
134
|
+
if (lower.includes('pattern'))
|
|
135
|
+
return 'patterns';
|
|
136
|
+
if (lower.includes('decision'))
|
|
137
|
+
return 'decisions';
|
|
138
|
+
if (lower.includes('stack') || lower.includes('tech'))
|
|
139
|
+
return 'technology';
|
|
140
|
+
return 'cursor-memory';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Extract a ParsedEntry from a JSON object.
|
|
144
|
+
*/
|
|
145
|
+
function extractFromJSON(obj, filePath) {
|
|
146
|
+
// Try various common keys for title
|
|
147
|
+
const title = obj.title ||
|
|
148
|
+
obj.name ||
|
|
149
|
+
obj.key ||
|
|
150
|
+
path.basename(filePath, '.json');
|
|
151
|
+
// Try various common keys for content
|
|
152
|
+
const content = obj.content ||
|
|
153
|
+
obj.description ||
|
|
154
|
+
obj.text ||
|
|
155
|
+
obj.value ||
|
|
156
|
+
obj.memory;
|
|
157
|
+
if (!content) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
title,
|
|
162
|
+
content,
|
|
163
|
+
context: obj.context || obj.category || obj.type,
|
|
164
|
+
intent: obj.intent,
|
|
165
|
+
source: 'cursor',
|
|
166
|
+
originalPath: filePath,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ParseResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a JSONL file (Anthropic MCP Memory / mcp-knowledge-graph format).
|
|
4
|
+
*
|
|
5
|
+
* Input format:
|
|
6
|
+
* {"type":"entity","name":"Stripe Webhooks","entityType":"concept","observations":["Need idempotency checks"]}
|
|
7
|
+
* {"type":"relation","from":"Stripe Webhooks","to":"Payment Processing","relationType":"part_of"}
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseJSONL(filePath: string): Promise<ParseResult>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a JSONL file (Anthropic MCP Memory / mcp-knowledge-graph format).
|
|
4
|
+
*
|
|
5
|
+
* Input format:
|
|
6
|
+
* {"type":"entity","name":"Stripe Webhooks","entityType":"concept","observations":["Need idempotency checks"]}
|
|
7
|
+
* {"type":"relation","from":"Stripe Webhooks","to":"Payment Processing","relationType":"part_of"}
|
|
8
|
+
*/
|
|
9
|
+
export async function parseJSONL(filePath) {
|
|
10
|
+
const entries = [];
|
|
11
|
+
const errors = [];
|
|
12
|
+
let skipped = 0;
|
|
13
|
+
try {
|
|
14
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
15
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
try {
|
|
19
|
+
const item = JSON.parse(line);
|
|
20
|
+
// Skip safety markers and relations
|
|
21
|
+
if (item.type === '_aim' || item.type === 'relation') {
|
|
22
|
+
skipped++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// Only process entities
|
|
26
|
+
if (item.type === 'entity' && item.name) {
|
|
27
|
+
// Skip if no observations (empty content)
|
|
28
|
+
if (!item.observations || item.observations.length === 0) {
|
|
29
|
+
skipped++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
entries.push({
|
|
33
|
+
title: item.name,
|
|
34
|
+
content: item.observations.join('\n\n'),
|
|
35
|
+
context: item.entityType || undefined,
|
|
36
|
+
source: 'jsonl',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
skipped++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (parseError) {
|
|
44
|
+
errors.push(`Line ${i + 1}: Invalid JSON - ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
45
|
+
skipped++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (fileError) {
|
|
50
|
+
errors.push(`Failed to read file: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
|
|
51
|
+
}
|
|
52
|
+
return { entries, skipped, errors };
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ParseResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a folder of markdown files (Basic Memory MCP / Obsidian / any .md).
|
|
4
|
+
*
|
|
5
|
+
* Input format:
|
|
6
|
+
* ---
|
|
7
|
+
* title: API Rate Limits
|
|
8
|
+
* tags: [api, performance]
|
|
9
|
+
* ---
|
|
10
|
+
*
|
|
11
|
+
* # API Rate Limits
|
|
12
|
+
*
|
|
13
|
+
* Always implement exponential backoff...
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseMarkdown(dirPath: string): Promise<ParseResult>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
/**
|
|
6
|
+
* Parse a folder of markdown files (Basic Memory MCP / Obsidian / any .md).
|
|
7
|
+
*
|
|
8
|
+
* Input format:
|
|
9
|
+
* ---
|
|
10
|
+
* title: API Rate Limits
|
|
11
|
+
* tags: [api, performance]
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* # API Rate Limits
|
|
15
|
+
*
|
|
16
|
+
* Always implement exponential backoff...
|
|
17
|
+
*/
|
|
18
|
+
export async function parseMarkdown(dirPath) {
|
|
19
|
+
const entries = [];
|
|
20
|
+
const errors = [];
|
|
21
|
+
let skipped = 0;
|
|
22
|
+
try {
|
|
23
|
+
// Handle both single file and directory
|
|
24
|
+
const stats = await fs.stat(dirPath);
|
|
25
|
+
const files = stats.isDirectory()
|
|
26
|
+
? await glob(path.join(dirPath, '**/*.md'), { nodir: true })
|
|
27
|
+
: [dirPath];
|
|
28
|
+
for (const filePath of files) {
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
31
|
+
const { data, content: body } = matter(content);
|
|
32
|
+
const frontmatter = data;
|
|
33
|
+
// Skip empty files
|
|
34
|
+
const trimmedBody = body.trim();
|
|
35
|
+
if (!trimmedBody) {
|
|
36
|
+
skipped++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Extract title from frontmatter, H1, or filename
|
|
40
|
+
let title = frontmatter.title;
|
|
41
|
+
if (!title) {
|
|
42
|
+
const headingMatch = trimmedBody.match(/^#\s+(.+)$/m);
|
|
43
|
+
if (headingMatch) {
|
|
44
|
+
title = headingMatch[1].trim();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
title = path.basename(filePath, '.md').replace(/-/g, ' ');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Extract context from tags or context field
|
|
51
|
+
let context;
|
|
52
|
+
if (frontmatter.context) {
|
|
53
|
+
context = frontmatter.context;
|
|
54
|
+
}
|
|
55
|
+
else if (frontmatter.tags && Array.isArray(frontmatter.tags)) {
|
|
56
|
+
context = frontmatter.tags.join(', ');
|
|
57
|
+
}
|
|
58
|
+
entries.push({
|
|
59
|
+
title,
|
|
60
|
+
content: trimmedBody,
|
|
61
|
+
context,
|
|
62
|
+
intent: frontmatter.intent,
|
|
63
|
+
source: 'markdown',
|
|
64
|
+
originalPath: filePath,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (fileError) {
|
|
68
|
+
errors.push(`${filePath}: ${fileError instanceof Error ? fileError.message : String(fileError)}`);
|
|
69
|
+
skipped++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (dirError) {
|
|
74
|
+
errors.push(`Failed to access path: ${dirError instanceof Error ? dirError.message : String(dirError)}`);
|
|
75
|
+
}
|
|
76
|
+
return { entries, skipped, errors };
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A parsed memory entry ready to be converted to Librarian format.
|
|
3
|
+
*/
|
|
4
|
+
export interface ParsedEntry {
|
|
5
|
+
title: string;
|
|
6
|
+
content: string;
|
|
7
|
+
context?: string;
|
|
8
|
+
intent?: string;
|
|
9
|
+
reasoning?: string;
|
|
10
|
+
example?: string;
|
|
11
|
+
source: 'jsonl' | 'markdown' | 'cursor';
|
|
12
|
+
originalPath?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Result of a parse operation.
|
|
16
|
+
*/
|
|
17
|
+
export interface ParseResult {
|
|
18
|
+
entries: ParsedEntry[];
|
|
19
|
+
skipped: number;
|
|
20
|
+
errors: string[];
|
|
21
|
+
}
|
|
@@ -9,8 +9,13 @@ export declare function getLibraryPath(): string;
|
|
|
9
9
|
export declare function getLocalPath(libraryPath: string): string;
|
|
10
10
|
/**
|
|
11
11
|
* Get the imported entries path.
|
|
12
|
+
* @deprecated Use getPackagesPath for marketplace content
|
|
12
13
|
*/
|
|
13
14
|
export declare function getImportedPath(libraryPath: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Get the packages path (marketplace content from others).
|
|
17
|
+
*/
|
|
18
|
+
export declare function getPackagesPath(libraryPath: string): string;
|
|
14
19
|
/**
|
|
15
20
|
* Get the archived entries path.
|
|
16
21
|
*/
|
package/dist/library/storage.js
CHANGED
|
@@ -17,10 +17,17 @@ export function getLocalPath(libraryPath) {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Get the imported entries path.
|
|
20
|
+
* @deprecated Use getPackagesPath for marketplace content
|
|
20
21
|
*/
|
|
21
22
|
export function getImportedPath(libraryPath) {
|
|
22
23
|
return path.join(libraryPath, 'imported');
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the packages path (marketplace content from others).
|
|
27
|
+
*/
|
|
28
|
+
export function getPackagesPath(libraryPath) {
|
|
29
|
+
return path.join(libraryPath, 'packages');
|
|
30
|
+
}
|
|
24
31
|
/**
|
|
25
32
|
* Get the archived entries path.
|
|
26
33
|
*/
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface IndexEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
title: string;
|
|
4
|
+
embedding: number[];
|
|
5
|
+
chunk: number;
|
|
6
|
+
preview: string;
|
|
7
|
+
}
|
|
8
|
+
export interface VectorIndex {
|
|
9
|
+
version: number;
|
|
10
|
+
rebuilt: string;
|
|
11
|
+
modelId: string;
|
|
12
|
+
entries: IndexEntry[];
|
|
13
|
+
}
|
|
14
|
+
export interface SemanticMatch {
|
|
15
|
+
path: string;
|
|
16
|
+
title: string;
|
|
17
|
+
similarity: number;
|
|
18
|
+
preview: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load the vector index from disk.
|
|
22
|
+
* Returns empty index if file doesn't exist or is invalid.
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadIndex(): Promise<VectorIndex>;
|
|
25
|
+
/**
|
|
26
|
+
* Save the vector index to disk.
|
|
27
|
+
*/
|
|
28
|
+
export declare function saveIndex(index: VectorIndex): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Add or update an entry in the index.
|
|
31
|
+
* Chunks the content and generates embeddings for each chunk.
|
|
32
|
+
*/
|
|
33
|
+
export declare function addToIndex(index: VectorIndex, entryPath: string, title: string, content: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Remove an entry from the index.
|
|
36
|
+
*/
|
|
37
|
+
export declare function removeFromIndex(index: VectorIndex, entryPath: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Search the index for entries semantically similar to the query.
|
|
40
|
+
* Returns paths ranked by similarity, deduped to best chunk per entry.
|
|
41
|
+
*/
|
|
42
|
+
export declare function semanticSearch(index: VectorIndex, query: string, limit?: number): Promise<SemanticMatch[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Check if the index might be stale (model changed).
|
|
45
|
+
*/
|
|
46
|
+
export declare function isIndexStale(index: VectorIndex): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Get index statistics.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getIndexStats(index: VectorIndex): {
|
|
51
|
+
entryCount: number;
|
|
52
|
+
chunkCount: number;
|
|
53
|
+
modelId: string;
|
|
54
|
+
rebuilt: string;
|
|
55
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getLibraryPath } from './storage.js';
|
|
4
|
+
import { getEmbedding, chunkText, cosineSimilarity, EMBEDDING_MODEL_ID } from './embeddings.js';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Constants
|
|
7
|
+
// ============================================================================
|
|
8
|
+
const INDEX_FILENAME = 'index.json';
|
|
9
|
+
const CURRENT_VERSION = 1;
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Index File Operations
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Get path to the index file.
|
|
15
|
+
*/
|
|
16
|
+
function getIndexPath() {
|
|
17
|
+
return path.join(getLibraryPath(), INDEX_FILENAME);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load the vector index from disk.
|
|
21
|
+
* Returns empty index if file doesn't exist or is invalid.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadIndex() {
|
|
24
|
+
const indexPath = getIndexPath();
|
|
25
|
+
try {
|
|
26
|
+
const data = await fs.readFile(indexPath, 'utf-8');
|
|
27
|
+
const index = JSON.parse(data);
|
|
28
|
+
// Validate structure
|
|
29
|
+
if (!index.version || !Array.isArray(index.entries)) {
|
|
30
|
+
return createEmptyIndex();
|
|
31
|
+
}
|
|
32
|
+
return index;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// File doesn't exist or is invalid
|
|
36
|
+
return createEmptyIndex();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Save the vector index to disk.
|
|
41
|
+
*/
|
|
42
|
+
export async function saveIndex(index) {
|
|
43
|
+
const indexPath = getIndexPath();
|
|
44
|
+
// Update metadata
|
|
45
|
+
index.rebuilt = new Date().toISOString();
|
|
46
|
+
index.modelId = EMBEDDING_MODEL_ID;
|
|
47
|
+
// Ensure directory exists
|
|
48
|
+
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
49
|
+
// Write atomically by writing to temp file first
|
|
50
|
+
const tempPath = indexPath + '.tmp';
|
|
51
|
+
await fs.writeFile(tempPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
52
|
+
await fs.rename(tempPath, indexPath);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create an empty index.
|
|
56
|
+
*/
|
|
57
|
+
function createEmptyIndex() {
|
|
58
|
+
return {
|
|
59
|
+
version: CURRENT_VERSION,
|
|
60
|
+
rebuilt: '',
|
|
61
|
+
modelId: EMBEDDING_MODEL_ID,
|
|
62
|
+
entries: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Index Operations
|
|
67
|
+
// ============================================================================
|
|
68
|
+
/**
|
|
69
|
+
* Add or update an entry in the index.
|
|
70
|
+
* Chunks the content and generates embeddings for each chunk.
|
|
71
|
+
*/
|
|
72
|
+
export async function addToIndex(index, entryPath, title, content) {
|
|
73
|
+
// Remove any existing entries for this path
|
|
74
|
+
index.entries = index.entries.filter(e => e.path !== entryPath);
|
|
75
|
+
// Chunk the content
|
|
76
|
+
const chunks = chunkText(content);
|
|
77
|
+
// Generate embeddings for each chunk
|
|
78
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
79
|
+
const chunk = chunks[i];
|
|
80
|
+
try {
|
|
81
|
+
const embedding = await getEmbedding(chunk);
|
|
82
|
+
index.entries.push({
|
|
83
|
+
path: entryPath,
|
|
84
|
+
title,
|
|
85
|
+
embedding,
|
|
86
|
+
chunk: i,
|
|
87
|
+
preview: chunk.slice(0, 100) + (chunk.length > 100 ? '...' : ''),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// Log but don't fail - entry will still be searchable via keywords
|
|
92
|
+
console.error(`Failed to embed chunk ${i} for ${entryPath}:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Remove an entry from the index.
|
|
98
|
+
*/
|
|
99
|
+
export function removeFromIndex(index, entryPath) {
|
|
100
|
+
index.entries = index.entries.filter(e => e.path !== entryPath);
|
|
101
|
+
}
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Semantic Search
|
|
104
|
+
// ============================================================================
|
|
105
|
+
/**
|
|
106
|
+
* Search the index for entries semantically similar to the query.
|
|
107
|
+
* Returns paths ranked by similarity, deduped to best chunk per entry.
|
|
108
|
+
*/
|
|
109
|
+
export async function semanticSearch(index, query, limit = 5) {
|
|
110
|
+
if (index.entries.length === 0) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
// Get query embedding
|
|
114
|
+
const queryEmbedding = await getEmbedding(query);
|
|
115
|
+
// Score all entries
|
|
116
|
+
const scored = index.entries.map(entry => ({
|
|
117
|
+
...entry,
|
|
118
|
+
similarity: cosineSimilarity(queryEmbedding, entry.embedding),
|
|
119
|
+
}));
|
|
120
|
+
// Dedupe by path - keep the chunk with highest similarity
|
|
121
|
+
const byPath = new Map();
|
|
122
|
+
for (const entry of scored) {
|
|
123
|
+
const existing = byPath.get(entry.path);
|
|
124
|
+
if (!existing || entry.similarity > existing.similarity) {
|
|
125
|
+
byPath.set(entry.path, entry);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Sort by similarity descending and apply limit
|
|
129
|
+
const results = [...byPath.values()]
|
|
130
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
131
|
+
.slice(0, limit)
|
|
132
|
+
.map(entry => ({
|
|
133
|
+
path: entry.path,
|
|
134
|
+
title: entry.title,
|
|
135
|
+
similarity: entry.similarity,
|
|
136
|
+
preview: entry.preview,
|
|
137
|
+
}));
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Index Health
|
|
142
|
+
// ============================================================================
|
|
143
|
+
/**
|
|
144
|
+
* Check if the index might be stale (model changed).
|
|
145
|
+
*/
|
|
146
|
+
export function isIndexStale(index) {
|
|
147
|
+
return index.modelId !== EMBEDDING_MODEL_ID;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get index statistics.
|
|
151
|
+
*/
|
|
152
|
+
export function getIndexStats(index) {
|
|
153
|
+
const uniquePaths = new Set(index.entries.map(e => e.path));
|
|
154
|
+
return {
|
|
155
|
+
entryCount: uniquePaths.size,
|
|
156
|
+
chunkCount: index.entries.length,
|
|
157
|
+
modelId: index.modelId,
|
|
158
|
+
rebuilt: index.rebuilt,
|
|
159
|
+
};
|
|
160
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -6,6 +6,8 @@ import { briefTool } from './tools/brief.js';
|
|
|
6
6
|
import { recordTool } from './tools/record.js';
|
|
7
7
|
import { adoptTool } from './tools/adopt.js';
|
|
8
8
|
import { markHitTool } from './tools/mark-hit.js';
|
|
9
|
+
import { importMemoriesTool } from './tools/import-memories.js';
|
|
10
|
+
import { rebuildIndexTool } from './tools/rebuild-index.js';
|
|
9
11
|
const server = new Server({
|
|
10
12
|
name: 'librarian',
|
|
11
13
|
version: '1.0.0',
|
|
@@ -38,6 +40,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
38
40
|
description: markHitTool.description,
|
|
39
41
|
inputSchema: markHitTool.inputSchema,
|
|
40
42
|
},
|
|
43
|
+
{
|
|
44
|
+
name: importMemoriesTool.name,
|
|
45
|
+
description: importMemoriesTool.description,
|
|
46
|
+
inputSchema: importMemoriesTool.inputSchema,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: rebuildIndexTool.name,
|
|
50
|
+
description: rebuildIndexTool.description,
|
|
51
|
+
inputSchema: rebuildIndexTool.inputSchema,
|
|
52
|
+
},
|
|
41
53
|
],
|
|
42
54
|
};
|
|
43
55
|
});
|
|
@@ -59,6 +71,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
59
71
|
case 'mark_hit':
|
|
60
72
|
result = await markHitTool.handler(args);
|
|
61
73
|
break;
|
|
74
|
+
case 'import_memories':
|
|
75
|
+
result = await importMemoriesTool.handler(args);
|
|
76
|
+
break;
|
|
77
|
+
case 'rebuild_index':
|
|
78
|
+
result = await rebuildIndexTool.handler(args);
|
|
79
|
+
break;
|
|
62
80
|
default:
|
|
63
81
|
throw new Error(`Unknown tool: ${name}`);
|
|
64
82
|
}
|
package/dist/tools/brief.js
CHANGED
|
@@ -2,7 +2,8 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
import { glob } from 'glob';
|
|
5
|
-
import { getLibraryPath, getLocalPath, getImportedPath } from '../library/storage.js';
|
|
5
|
+
import { getLibraryPath, getLocalPath, getImportedPath, getPackagesPath } from '../library/storage.js';
|
|
6
|
+
import { loadIndex, semanticSearch, isIndexStale } from '../library/vector-index.js';
|
|
6
7
|
// ============================================================================
|
|
7
8
|
// Tool Definition
|
|
8
9
|
// ============================================================================
|
|
@@ -38,7 +39,51 @@ Examples:
|
|
|
38
39
|
const libraryPath = getLibraryPath();
|
|
39
40
|
const localPath = getLocalPath(libraryPath);
|
|
40
41
|
const importedPath = getImportedPath(libraryPath);
|
|
42
|
+
const packagesPath = getPackagesPath(libraryPath);
|
|
41
43
|
let allEntries = [];
|
|
44
|
+
let useSemanticSearch = false;
|
|
45
|
+
let semanticMatches = [];
|
|
46
|
+
// Try semantic search if query is provided
|
|
47
|
+
if (query) {
|
|
48
|
+
try {
|
|
49
|
+
const index = await loadIndex();
|
|
50
|
+
// Only use semantic search if index has entries and isn't stale
|
|
51
|
+
if (index.entries.length > 0 && !isIndexStale(index)) {
|
|
52
|
+
semanticMatches = await semanticSearch(index, query, limit);
|
|
53
|
+
useSemanticSearch = semanticMatches.length > 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Semantic search unavailable, fall back to keyword search
|
|
58
|
+
useSemanticSearch = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (useSemanticSearch && semanticMatches.length > 0) {
|
|
62
|
+
// Load only the entries that matched semantically
|
|
63
|
+
const matchedPaths = new Set(semanticMatches.map(m => m.path));
|
|
64
|
+
for (const match of semanticMatches) {
|
|
65
|
+
const fullPath = path.join(libraryPath, match.path);
|
|
66
|
+
const entry = await readEntry(fullPath, libraryPath);
|
|
67
|
+
if (entry) {
|
|
68
|
+
allEntries.push(entry);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Sort by semantic similarity (order preserved from semanticSearch)
|
|
72
|
+
// Re-order allEntries to match semanticMatches order
|
|
73
|
+
const pathToEntry = new Map(allEntries.map(e => [e.path, e]));
|
|
74
|
+
allEntries = semanticMatches
|
|
75
|
+
.map(m => pathToEntry.get(m.path))
|
|
76
|
+
.filter((e) => e !== undefined);
|
|
77
|
+
const total = allEntries.length;
|
|
78
|
+
const entries = allEntries.slice(0, limit);
|
|
79
|
+
return {
|
|
80
|
+
entries,
|
|
81
|
+
total,
|
|
82
|
+
message: `Found ${total} ${total === 1 ? 'entry' : 'entries'} for "${query}" (semantic search).`,
|
|
83
|
+
libraryPath: localPath,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Fall back to keyword search
|
|
42
87
|
// Read local entries
|
|
43
88
|
try {
|
|
44
89
|
const localFiles = await glob(path.join(localPath, '**/*.md'), { nodir: true });
|
|
@@ -52,7 +97,7 @@ Examples:
|
|
|
52
97
|
catch {
|
|
53
98
|
// No local files yet
|
|
54
99
|
}
|
|
55
|
-
// Read imported entries
|
|
100
|
+
// Read imported entries (legacy - deprecated)
|
|
56
101
|
try {
|
|
57
102
|
const importedFiles = await glob(path.join(importedPath, '**/*.md'), { nodir: true });
|
|
58
103
|
for (const filePath of importedFiles) {
|
|
@@ -65,6 +110,19 @@ Examples:
|
|
|
65
110
|
catch {
|
|
66
111
|
// No imported files
|
|
67
112
|
}
|
|
113
|
+
// Read packages entries (marketplace content)
|
|
114
|
+
try {
|
|
115
|
+
const packagesFiles = await glob(path.join(packagesPath, '**/*.md'), { nodir: true });
|
|
116
|
+
for (const filePath of packagesFiles) {
|
|
117
|
+
const entry = await readEntry(filePath, libraryPath);
|
|
118
|
+
if (entry) {
|
|
119
|
+
allEntries.push(entry);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// No packages files
|
|
125
|
+
}
|
|
68
126
|
// If no entries at all
|
|
69
127
|
if (allEntries.length === 0) {
|
|
70
128
|
return {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ImportResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
imported: number;
|
|
4
|
+
skipped: number;
|
|
5
|
+
errors: string[];
|
|
6
|
+
outputPath: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const importMemoriesTool: {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object";
|
|
14
|
+
properties: {
|
|
15
|
+
format: {
|
|
16
|
+
type: string;
|
|
17
|
+
enum: string[];
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
path: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
source_name: {
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
required: string[];
|
|
30
|
+
};
|
|
31
|
+
handler(args: unknown): Promise<ImportResult>;
|
|
32
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getLibraryPath, getLocalPath } from '../library/storage.js';
|
|
4
|
+
import { loadIndex, saveIndex, addToIndex } from '../library/vector-index.js';
|
|
5
|
+
import { parseJSONL, parseMarkdown, parseCursorMemory } from '../library/parsers/index.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Tool Definition
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export const importMemoriesTool = {
|
|
10
|
+
name: 'import_memories',
|
|
11
|
+
description: `Import memories from other AI tools into Librarian.
|
|
12
|
+
|
|
13
|
+
Supported formats:
|
|
14
|
+
- jsonl: Anthropic MCP Memory, mcp-knowledge-graph (.jsonl files)
|
|
15
|
+
- markdown: Basic Memory, Obsidian, any .md files
|
|
16
|
+
- cursor: Cursor Memory Bank (.cursor-memory/)
|
|
17
|
+
|
|
18
|
+
Imports go to .librarian/local/[source-name]/ and are automatically indexed for semantic search.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
- import_memories({ format: "jsonl", path: "~/.aim/memory.jsonl", source_name: "anthropic-memory" })
|
|
22
|
+
- import_memories({ format: "markdown", path: "~/basic-memory/", source_name: "basic-memory" })
|
|
23
|
+
- import_memories({ format: "cursor", path: ".cursor-memory/", source_name: "cursor-memory" })`,
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
format: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
enum: ['jsonl', 'markdown', 'cursor'],
|
|
30
|
+
description: 'Format of the source memories',
|
|
31
|
+
},
|
|
32
|
+
path: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Path to memory file or folder',
|
|
35
|
+
},
|
|
36
|
+
source_name: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Name for the import folder (e.g., "anthropic-memory"). Auto-generated if not provided.',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ['format', 'path'],
|
|
42
|
+
},
|
|
43
|
+
async handler(args) {
|
|
44
|
+
const { format, path: inputPath, source_name } = args;
|
|
45
|
+
if (!format || !inputPath) {
|
|
46
|
+
throw new Error('format and path are required');
|
|
47
|
+
}
|
|
48
|
+
// Expand ~ to home directory
|
|
49
|
+
const expandedPath = inputPath.replace(/^~/, process.env.HOME || '');
|
|
50
|
+
// Generate source name if not provided
|
|
51
|
+
const sourceName = source_name || generateSourceName(format, expandedPath);
|
|
52
|
+
// Setup output directory
|
|
53
|
+
const libraryPath = getLibraryPath();
|
|
54
|
+
const localPath = getLocalPath(libraryPath);
|
|
55
|
+
const outputPath = path.join(localPath, sourceName);
|
|
56
|
+
// Check if output directory already exists
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(outputPath);
|
|
59
|
+
// Directory exists - we'll add to it but warn about potential duplicates
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Directory doesn't exist - create it
|
|
63
|
+
await fs.mkdir(outputPath, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
// Parse the source based on format
|
|
66
|
+
let parseResult;
|
|
67
|
+
switch (format) {
|
|
68
|
+
case 'jsonl':
|
|
69
|
+
parseResult = await parseJSONL(expandedPath);
|
|
70
|
+
break;
|
|
71
|
+
case 'markdown':
|
|
72
|
+
parseResult = await parseMarkdown(expandedPath);
|
|
73
|
+
break;
|
|
74
|
+
case 'cursor':
|
|
75
|
+
parseResult = await parseCursorMemory(expandedPath);
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unknown format: ${format}`);
|
|
79
|
+
}
|
|
80
|
+
// Convert and save entries
|
|
81
|
+
const index = await loadIndex();
|
|
82
|
+
let imported = 0;
|
|
83
|
+
const errors = [...parseResult.errors];
|
|
84
|
+
for (const entry of parseResult.entries) {
|
|
85
|
+
try {
|
|
86
|
+
const relativePath = await saveEntry(entry, outputPath, libraryPath);
|
|
87
|
+
// Add to vector index
|
|
88
|
+
const fullContent = [
|
|
89
|
+
entry.title,
|
|
90
|
+
entry.intent || '',
|
|
91
|
+
entry.content,
|
|
92
|
+
entry.reasoning || '',
|
|
93
|
+
entry.example || '',
|
|
94
|
+
entry.context || '',
|
|
95
|
+
].filter(Boolean).join('\n\n');
|
|
96
|
+
await addToIndex(index, relativePath, entry.title, fullContent);
|
|
97
|
+
imported++;
|
|
98
|
+
}
|
|
99
|
+
catch (saveError) {
|
|
100
|
+
errors.push(`Failed to save "${entry.title}": ${saveError instanceof Error ? saveError.message : String(saveError)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Save the updated index
|
|
104
|
+
await saveIndex(index);
|
|
105
|
+
const message = imported > 0
|
|
106
|
+
? `Imported ${imported} entries from ${format} format into ${sourceName}/`
|
|
107
|
+
: `No entries imported. ${parseResult.skipped} skipped, ${errors.length} errors.`;
|
|
108
|
+
return {
|
|
109
|
+
success: imported > 0,
|
|
110
|
+
imported,
|
|
111
|
+
skipped: parseResult.skipped,
|
|
112
|
+
errors,
|
|
113
|
+
outputPath: path.relative(libraryPath, outputPath),
|
|
114
|
+
message,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Helper Functions
|
|
120
|
+
// ============================================================================
|
|
121
|
+
/**
|
|
122
|
+
* Generate a source name from the format and path.
|
|
123
|
+
*/
|
|
124
|
+
function generateSourceName(format, inputPath) {
|
|
125
|
+
const basename = path.basename(inputPath, path.extname(inputPath));
|
|
126
|
+
switch (format) {
|
|
127
|
+
case 'jsonl':
|
|
128
|
+
if (basename.includes('memory')) {
|
|
129
|
+
return 'imported-memory';
|
|
130
|
+
}
|
|
131
|
+
return `imported-${basename}`;
|
|
132
|
+
case 'cursor':
|
|
133
|
+
return 'cursor-memory';
|
|
134
|
+
case 'markdown':
|
|
135
|
+
return basename.replace(/[^a-z0-9-]/gi, '-').toLowerCase() || 'imported-markdown';
|
|
136
|
+
default:
|
|
137
|
+
return `imported-${format}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Save a parsed entry to the output directory.
|
|
142
|
+
* Returns the relative path from library root.
|
|
143
|
+
*/
|
|
144
|
+
async function saveEntry(entry, outputPath, libraryPath) {
|
|
145
|
+
const slug = slugify(entry.title);
|
|
146
|
+
const created = new Date().toISOString();
|
|
147
|
+
// Handle filename collisions
|
|
148
|
+
let filename = `${slug}.md`;
|
|
149
|
+
let filePath = path.join(outputPath, filename);
|
|
150
|
+
let counter = 1;
|
|
151
|
+
while (await fileExists(filePath)) {
|
|
152
|
+
filename = `${slug}-${counter}.md`;
|
|
153
|
+
filePath = path.join(outputPath, filename);
|
|
154
|
+
counter++;
|
|
155
|
+
}
|
|
156
|
+
// Build frontmatter
|
|
157
|
+
const frontmatterLines = ['---'];
|
|
158
|
+
if (entry.intent) {
|
|
159
|
+
frontmatterLines.push(`intent: "${escapeYaml(entry.intent)}"`);
|
|
160
|
+
}
|
|
161
|
+
if (entry.context) {
|
|
162
|
+
frontmatterLines.push(`context: "${escapeYaml(entry.context)}"`);
|
|
163
|
+
}
|
|
164
|
+
frontmatterLines.push(`created: "${created}"`);
|
|
165
|
+
frontmatterLines.push(`updated: "${created}"`);
|
|
166
|
+
frontmatterLines.push(`source: "${entry.source}"`);
|
|
167
|
+
if (entry.originalPath) {
|
|
168
|
+
frontmatterLines.push(`original_path: "${escapeYaml(entry.originalPath)}"`);
|
|
169
|
+
}
|
|
170
|
+
frontmatterLines.push('hits: 0');
|
|
171
|
+
frontmatterLines.push('last_hit: null');
|
|
172
|
+
frontmatterLines.push('---');
|
|
173
|
+
// Build body
|
|
174
|
+
const bodyLines = [];
|
|
175
|
+
bodyLines.push(`# ${entry.title}`);
|
|
176
|
+
bodyLines.push('');
|
|
177
|
+
bodyLines.push(entry.content);
|
|
178
|
+
if (entry.reasoning) {
|
|
179
|
+
bodyLines.push('');
|
|
180
|
+
bodyLines.push('## Reasoning');
|
|
181
|
+
bodyLines.push('');
|
|
182
|
+
bodyLines.push(entry.reasoning);
|
|
183
|
+
}
|
|
184
|
+
if (entry.example) {
|
|
185
|
+
bodyLines.push('');
|
|
186
|
+
bodyLines.push('## Example');
|
|
187
|
+
bodyLines.push('');
|
|
188
|
+
bodyLines.push('```');
|
|
189
|
+
bodyLines.push(entry.example);
|
|
190
|
+
bodyLines.push('```');
|
|
191
|
+
}
|
|
192
|
+
// Combine and write
|
|
193
|
+
const fileContent = frontmatterLines.join('\n') + '\n\n' + bodyLines.join('\n') + '\n';
|
|
194
|
+
await fs.writeFile(filePath, fileContent, 'utf-8');
|
|
195
|
+
return path.relative(libraryPath, filePath);
|
|
196
|
+
}
|
|
197
|
+
function slugify(text) {
|
|
198
|
+
return text
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
201
|
+
.replace(/^-+|-+$/g, '')
|
|
202
|
+
.slice(0, 50);
|
|
203
|
+
}
|
|
204
|
+
function escapeYaml(text) {
|
|
205
|
+
return text.replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
206
|
+
}
|
|
207
|
+
async function fileExists(filePath) {
|
|
208
|
+
try {
|
|
209
|
+
await fs.access(filePath);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { briefTool } from './brief.js';
|
|
|
2
2
|
export { recordTool } from './record.js';
|
|
3
3
|
export { adoptTool } from './adopt.js';
|
|
4
4
|
export { markHitTool } from './mark-hit.js';
|
|
5
|
+
export { importMemoriesTool } from './import-memories.js';
|
|
6
|
+
export { rebuildIndexTool } from './rebuild-index.js';
|
package/dist/tools/index.js
CHANGED
|
@@ -2,3 +2,5 @@ export { briefTool } from './brief.js';
|
|
|
2
2
|
export { recordTool } from './record.js';
|
|
3
3
|
export { adoptTool } from './adopt.js';
|
|
4
4
|
export { markHitTool } from './mark-hit.js';
|
|
5
|
+
export { importMemoriesTool } from './import-memories.js';
|
|
6
|
+
export { rebuildIndexTool } from './rebuild-index.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface RebuildResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
indexed: number;
|
|
4
|
+
skipped: number;
|
|
5
|
+
errors: string[];
|
|
6
|
+
stats: {
|
|
7
|
+
entryCount: number;
|
|
8
|
+
chunkCount: number;
|
|
9
|
+
modelId: string;
|
|
10
|
+
rebuilt: string;
|
|
11
|
+
};
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const rebuildIndexTool: {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object";
|
|
19
|
+
properties: {};
|
|
20
|
+
required: never[];
|
|
21
|
+
};
|
|
22
|
+
handler(_args: unknown): Promise<RebuildResult>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { getLibraryPath, getLocalPath, getImportedPath, getPackagesPath } from '../library/storage.js';
|
|
6
|
+
import { saveIndex, addToIndex, getIndexStats } from '../library/vector-index.js';
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Tool Definition
|
|
9
|
+
// ============================================================================
|
|
10
|
+
export const rebuildIndexTool = {
|
|
11
|
+
name: 'rebuild_index',
|
|
12
|
+
description: `Rebuild the semantic search index for all library entries.
|
|
13
|
+
|
|
14
|
+
Use this after:
|
|
15
|
+
- Upgrading to v1.2.0 (existing entries need embeddings)
|
|
16
|
+
- Importing memories from other tools
|
|
17
|
+
- If semantic search seems broken
|
|
18
|
+
|
|
19
|
+
Reads all .md entries and generates embeddings. May take a minute on first run.`,
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {},
|
|
23
|
+
required: [],
|
|
24
|
+
},
|
|
25
|
+
async handler(_args) {
|
|
26
|
+
const libraryPath = getLibraryPath();
|
|
27
|
+
const localPath = getLocalPath(libraryPath);
|
|
28
|
+
const importedPath = getImportedPath(libraryPath);
|
|
29
|
+
const packagesPath = getPackagesPath(libraryPath);
|
|
30
|
+
// Create a fresh index
|
|
31
|
+
const index = {
|
|
32
|
+
version: 1,
|
|
33
|
+
rebuilt: '',
|
|
34
|
+
modelId: '',
|
|
35
|
+
entries: [],
|
|
36
|
+
};
|
|
37
|
+
let indexed = 0;
|
|
38
|
+
let skipped = 0;
|
|
39
|
+
const errors = [];
|
|
40
|
+
// Collect all .md files from all directories
|
|
41
|
+
const allDirs = [localPath, importedPath, packagesPath];
|
|
42
|
+
const allFiles = [];
|
|
43
|
+
for (const dir of allDirs) {
|
|
44
|
+
try {
|
|
45
|
+
const files = await glob(path.join(dir, '**/*.md'), { nodir: true });
|
|
46
|
+
allFiles.push(...files);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Directory doesn't exist, skip
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Process each file
|
|
53
|
+
for (const filePath of allFiles) {
|
|
54
|
+
try {
|
|
55
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
56
|
+
const { data, content: body } = matter(content);
|
|
57
|
+
// Skip empty files
|
|
58
|
+
if (!body.trim()) {
|
|
59
|
+
skipped++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Extract title
|
|
63
|
+
let title = data.title;
|
|
64
|
+
if (!title) {
|
|
65
|
+
const headingMatch = body.match(/^#\s+(.+)$/m);
|
|
66
|
+
if (headingMatch) {
|
|
67
|
+
title = headingMatch[1].trim();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
title = path.basename(filePath, '.md').replace(/-/g, ' ');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Build full content for embedding
|
|
74
|
+
const fullContent = [
|
|
75
|
+
title,
|
|
76
|
+
data.intent || '',
|
|
77
|
+
body.trim(),
|
|
78
|
+
data.context || '',
|
|
79
|
+
].filter(Boolean).join('\n\n');
|
|
80
|
+
const relativePath = path.relative(libraryPath, filePath);
|
|
81
|
+
await addToIndex(index, relativePath, title, fullContent);
|
|
82
|
+
indexed++;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const relativePath = path.relative(libraryPath, filePath);
|
|
86
|
+
errors.push(`${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
skipped++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Save the index
|
|
91
|
+
await saveIndex(index);
|
|
92
|
+
const stats = getIndexStats(index);
|
|
93
|
+
const message = indexed > 0
|
|
94
|
+
? `Rebuilt index with ${indexed} entries (${stats.chunkCount} chunks). ${skipped} skipped.`
|
|
95
|
+
: 'No entries found to index.';
|
|
96
|
+
return {
|
|
97
|
+
success: indexed > 0 || allFiles.length === 0,
|
|
98
|
+
indexed,
|
|
99
|
+
skipped,
|
|
100
|
+
errors,
|
|
101
|
+
stats,
|
|
102
|
+
message,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
package/dist/tools/record.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getLibraryPath, getLocalPath } from '../library/storage.js';
|
|
4
|
+
import { loadIndex, saveIndex, addToIndex } from '../library/vector-index.js';
|
|
4
5
|
// ============================================================================
|
|
5
6
|
// Tool Definition
|
|
6
7
|
// ============================================================================
|
|
@@ -140,6 +141,26 @@ Rich:
|
|
|
140
141
|
const fileContent = frontmatterLines.join('\n') + '\n\n' + bodyLines.join('\n') + '\n';
|
|
141
142
|
await fs.writeFile(filePath, fileContent, 'utf-8');
|
|
142
143
|
const relativePath = path.relative(libraryPath, filePath);
|
|
144
|
+
// Add to vector index for semantic search
|
|
145
|
+
try {
|
|
146
|
+
const index = await loadIndex();
|
|
147
|
+
// Combine all text for embedding
|
|
148
|
+
const fullContent = [
|
|
149
|
+
title,
|
|
150
|
+
intent || '',
|
|
151
|
+
insight,
|
|
152
|
+
reasoning || '',
|
|
153
|
+
example || '',
|
|
154
|
+
context || '',
|
|
155
|
+
].filter(Boolean).join('\n\n');
|
|
156
|
+
await addToIndex(index, relativePath, title, fullContent);
|
|
157
|
+
await saveIndex(index);
|
|
158
|
+
}
|
|
159
|
+
catch (embeddingError) {
|
|
160
|
+
// Don't fail the record operation if embedding fails
|
|
161
|
+
// Entry is still saved and searchable via keywords
|
|
162
|
+
console.error('Failed to add embedding:', embeddingError);
|
|
163
|
+
}
|
|
143
164
|
return {
|
|
144
165
|
success: true,
|
|
145
166
|
path: relativePath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telvok/librarian-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Knowledge capture MCP server - remember what you learn with AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server.js",
|
|
@@ -26,11 +26,12 @@
|
|
|
26
26
|
],
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/telvokdev/librarian.git"
|
|
29
|
+
"url": "git+https://github.com/telvokdev/librarian.git"
|
|
30
30
|
},
|
|
31
31
|
"author": "Telvok",
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@huggingface/transformers": "^3.0.0",
|
|
34
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
35
36
|
"glob": "^11.0.0",
|
|
36
37
|
"gray-matter": "^4.0.3",
|