@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,143 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { getLibraryPath, getImportedPath, getLocalPath } from '../library/storage.js';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Tool Definition
|
|
7
|
+
// ============================================================================
|
|
8
|
+
export const adoptTool = {
|
|
9
|
+
name: 'adopt',
|
|
10
|
+
description: `Make imported knowledge ours.
|
|
11
|
+
|
|
12
|
+
When an entry from an imported package proves useful, adopt it into our
|
|
13
|
+
local library. It graduates from "their knowledge" to "our knowledge" -
|
|
14
|
+
now we can edit and evolve it.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
- adopt({ path: "imported/stripe-patterns/webhook-idempotency" })
|
|
18
|
+
- adopt({ path: "imported/auth-patterns/token-refresh", title: "Our token refresh" })`,
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
path: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: "Path to entry to adopt (e.g., 'imported/package-name/entry-name')",
|
|
25
|
+
},
|
|
26
|
+
title: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'New title for adopted entry. Keeps original if not provided.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['path'],
|
|
32
|
+
},
|
|
33
|
+
async handler(args) {
|
|
34
|
+
const { path: entryPath, title: newTitle } = args;
|
|
35
|
+
if (!entryPath) {
|
|
36
|
+
throw new Error('path is required');
|
|
37
|
+
}
|
|
38
|
+
const libraryPath = getLibraryPath();
|
|
39
|
+
const importedPath = getImportedPath(libraryPath);
|
|
40
|
+
const localPath = getLocalPath(libraryPath);
|
|
41
|
+
// Normalize path: strip "imported/" prefix if present, add .md if missing
|
|
42
|
+
let normalizedPath = entryPath;
|
|
43
|
+
if (normalizedPath.startsWith('imported/')) {
|
|
44
|
+
normalizedPath = normalizedPath.slice('imported/'.length);
|
|
45
|
+
}
|
|
46
|
+
if (!normalizedPath.endsWith('.md')) {
|
|
47
|
+
normalizedPath += '.md';
|
|
48
|
+
}
|
|
49
|
+
const sourcePath = path.join(importedPath, normalizedPath);
|
|
50
|
+
// Check if source exists
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(sourcePath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error(`Entry not found: ${entryPath}`);
|
|
56
|
+
}
|
|
57
|
+
// Read source file
|
|
58
|
+
const content = await fs.readFile(sourcePath, 'utf-8');
|
|
59
|
+
const { data, content: body } = matter(content);
|
|
60
|
+
// Extract package name from path
|
|
61
|
+
const pathParts = normalizedPath.split('/');
|
|
62
|
+
const packageName = pathParts[0];
|
|
63
|
+
// Determine title
|
|
64
|
+
let title = newTitle;
|
|
65
|
+
if (!title) {
|
|
66
|
+
// Try to extract from frontmatter or H1
|
|
67
|
+
title = data.title;
|
|
68
|
+
if (!title) {
|
|
69
|
+
const headingMatch = body.match(/^#\s+(.+)$/m);
|
|
70
|
+
if (headingMatch) {
|
|
71
|
+
title = headingMatch[1].trim();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
title = path.basename(sourcePath, '.md').replace(/-/g, ' ');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Generate slug for new filename
|
|
79
|
+
const slug = slugify(title);
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
// Ensure local directory exists
|
|
82
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
83
|
+
// Handle filename collisions
|
|
84
|
+
let filename = `${slug}.md`;
|
|
85
|
+
let destPath = path.join(localPath, filename);
|
|
86
|
+
let counter = 1;
|
|
87
|
+
while (await fileExists(destPath)) {
|
|
88
|
+
filename = `${slug}-${counter}.md`;
|
|
89
|
+
destPath = path.join(localPath, filename);
|
|
90
|
+
counter++;
|
|
91
|
+
}
|
|
92
|
+
// Build new frontmatter
|
|
93
|
+
const newFrontmatter = {
|
|
94
|
+
...data,
|
|
95
|
+
updated: now,
|
|
96
|
+
source: `adopted from ${packageName}`,
|
|
97
|
+
};
|
|
98
|
+
// Update title in frontmatter if changed
|
|
99
|
+
if (newTitle) {
|
|
100
|
+
newFrontmatter.title = newTitle;
|
|
101
|
+
}
|
|
102
|
+
// Update body if title changed
|
|
103
|
+
let newBody = body;
|
|
104
|
+
if (newTitle) {
|
|
105
|
+
// Replace first H1 if exists
|
|
106
|
+
const headingMatch = body.match(/^#\s+.+$/m);
|
|
107
|
+
if (headingMatch) {
|
|
108
|
+
newBody = body.replace(/^#\s+.+$/m, `# ${newTitle}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Prepend title
|
|
112
|
+
newBody = `# ${newTitle}\n\n${body}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Write adopted file
|
|
116
|
+
const fileContent = matter.stringify(newBody, newFrontmatter);
|
|
117
|
+
await fs.writeFile(destPath, fileContent, 'utf-8');
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
from: path.relative(libraryPath, sourcePath),
|
|
121
|
+
to: path.relative(libraryPath, destPath),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Helper Functions
|
|
127
|
+
// ============================================================================
|
|
128
|
+
function slugify(text) {
|
|
129
|
+
return text
|
|
130
|
+
.toLowerCase()
|
|
131
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
132
|
+
.replace(/^-+|-+$/g, '')
|
|
133
|
+
.slice(0, 50);
|
|
134
|
+
}
|
|
135
|
+
async function fileExists(filePath) {
|
|
136
|
+
try {
|
|
137
|
+
await fs.access(filePath);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface BriefEntry {
|
|
2
|
+
title: string;
|
|
3
|
+
intent: string | null;
|
|
4
|
+
context: string | null;
|
|
5
|
+
preview: string;
|
|
6
|
+
path: string;
|
|
7
|
+
created: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BriefResult {
|
|
10
|
+
entries: BriefEntry[];
|
|
11
|
+
total: number;
|
|
12
|
+
message: string;
|
|
13
|
+
libraryPath: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const briefTool: {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object";
|
|
20
|
+
properties: {
|
|
21
|
+
query: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
limit: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
default: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
required: never[];
|
|
32
|
+
};
|
|
33
|
+
handler(args: unknown): Promise<BriefResult>;
|
|
34
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
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 { getLibraryPath, getLocalPath, getImportedPath } from '../library/storage.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Tool Definition
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export const briefTool = {
|
|
10
|
+
name: 'brief',
|
|
11
|
+
description: `Check what we already know before diving in.
|
|
12
|
+
|
|
13
|
+
We've solved problems before. Before thinking through a problem, making
|
|
14
|
+
decisions, or planning - brief yourself on what past-us figured out.
|
|
15
|
+
Searches intent, insight, context, and examples.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
- brief({ query: "stripe webhooks" })
|
|
19
|
+
- brief({ query: "auth token" })
|
|
20
|
+
- brief({}) → returns recent entries`,
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
query: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'What are we working on? Searches our library. Leave empty to see recent entries.',
|
|
27
|
+
},
|
|
28
|
+
limit: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
description: 'Max entries to return',
|
|
31
|
+
default: 5,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: [],
|
|
35
|
+
},
|
|
36
|
+
async handler(args) {
|
|
37
|
+
const { query, limit = 5 } = args;
|
|
38
|
+
const libraryPath = getLibraryPath();
|
|
39
|
+
const localPath = getLocalPath(libraryPath);
|
|
40
|
+
const importedPath = getImportedPath(libraryPath);
|
|
41
|
+
let allEntries = [];
|
|
42
|
+
// Read local entries
|
|
43
|
+
try {
|
|
44
|
+
const localFiles = await glob(path.join(localPath, '**/*.md'), { nodir: true });
|
|
45
|
+
for (const filePath of localFiles) {
|
|
46
|
+
const entry = await readEntry(filePath, libraryPath);
|
|
47
|
+
if (entry) {
|
|
48
|
+
allEntries.push(entry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// No local files yet
|
|
54
|
+
}
|
|
55
|
+
// Read imported entries
|
|
56
|
+
try {
|
|
57
|
+
const importedFiles = await glob(path.join(importedPath, '**/*.md'), { nodir: true });
|
|
58
|
+
for (const filePath of importedFiles) {
|
|
59
|
+
const entry = await readEntry(filePath, libraryPath);
|
|
60
|
+
if (entry) {
|
|
61
|
+
allEntries.push(entry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// No imported files
|
|
67
|
+
}
|
|
68
|
+
// If no entries at all
|
|
69
|
+
if (allEntries.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
entries: [],
|
|
72
|
+
total: 0,
|
|
73
|
+
message: 'No entries yet. Start recording!',
|
|
74
|
+
libraryPath: localPath,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Filter by query if provided
|
|
78
|
+
if (query) {
|
|
79
|
+
const searchTerm = query.toLowerCase();
|
|
80
|
+
allEntries = allEntries.filter(entry => matchesSearch(entry, searchTerm));
|
|
81
|
+
}
|
|
82
|
+
// Sort by created date (most recent first)
|
|
83
|
+
allEntries.sort((a, b) => {
|
|
84
|
+
return new Date(b.created).getTime() - new Date(a.created).getTime();
|
|
85
|
+
});
|
|
86
|
+
const total = allEntries.length;
|
|
87
|
+
// Apply limit
|
|
88
|
+
const entries = allEntries.slice(0, limit);
|
|
89
|
+
// Build message
|
|
90
|
+
let message;
|
|
91
|
+
if (query) {
|
|
92
|
+
message = total === 0
|
|
93
|
+
? `No entries found for "${query}".`
|
|
94
|
+
: `Found ${total} ${total === 1 ? 'entry' : 'entries'} for "${query}".`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
message = `${total} ${total === 1 ? 'entry' : 'entries'} in library.`;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
entries,
|
|
101
|
+
total,
|
|
102
|
+
message,
|
|
103
|
+
libraryPath: localPath,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Helper Functions
|
|
109
|
+
// ============================================================================
|
|
110
|
+
async function readEntry(filePath, libraryPath) {
|
|
111
|
+
try {
|
|
112
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
113
|
+
const { data, content: body } = matter(content);
|
|
114
|
+
// Extract title from H1 or filename
|
|
115
|
+
let title = data.title;
|
|
116
|
+
if (!title) {
|
|
117
|
+
const headingMatch = body.match(/^#\s+(.+)$/m);
|
|
118
|
+
if (headingMatch) {
|
|
119
|
+
title = headingMatch[1].trim();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
title = path.basename(filePath, '.md').replace(/-/g, ' ');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Extract preview - first 100 chars of body content
|
|
126
|
+
const bodyText = body.trim();
|
|
127
|
+
const preview = bodyText.length > 100
|
|
128
|
+
? bodyText.slice(0, 100) + '...'
|
|
129
|
+
: bodyText;
|
|
130
|
+
return {
|
|
131
|
+
title,
|
|
132
|
+
intent: data.intent || null,
|
|
133
|
+
context: data.context || null,
|
|
134
|
+
preview,
|
|
135
|
+
path: path.relative(libraryPath, filePath),
|
|
136
|
+
created: data.created || new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function matchesSearch(entry, searchTerm) {
|
|
144
|
+
// Check title
|
|
145
|
+
if (entry.title.toLowerCase().includes(searchTerm)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
// Check intent
|
|
149
|
+
if (entry.intent && entry.intent.toLowerCase().includes(searchTerm)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
// Check context
|
|
153
|
+
if (entry.context && entry.context.toLowerCase().includes(searchTerm)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// Check preview (basic substring match - Claude does semantic filtering)
|
|
157
|
+
if (entry.preview.toLowerCase().includes(searchTerm)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface RecordResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
path: string;
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const recordTool: {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object";
|
|
11
|
+
properties: {
|
|
12
|
+
insight: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
intent: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
reasoning: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
context: {
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
example: {
|
|
29
|
+
type: string;
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
title: {
|
|
33
|
+
type: string;
|
|
34
|
+
description: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
required: string[];
|
|
38
|
+
};
|
|
39
|
+
handler(args: unknown): Promise<RecordResult>;
|
|
40
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getLibraryPath, getLocalPath } from '../library/storage.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Tool Definition
|
|
6
|
+
// ============================================================================
|
|
7
|
+
export const recordTool = {
|
|
8
|
+
name: 'record',
|
|
9
|
+
description: `Capture knowledge worth keeping. We're building a library together.
|
|
10
|
+
|
|
11
|
+
Every session we learn things that evaporate by tomorrow. This catches
|
|
12
|
+
the good stuff - what we learned, why it matters, how it works.
|
|
13
|
+
|
|
14
|
+
Quality bar: "I wish we knew this yesterday"
|
|
15
|
+
|
|
16
|
+
Good entries:
|
|
17
|
+
- "Stripe retries webhooks but doesn't dedupe - always check idempotency key"
|
|
18
|
+
- "Clock skew between services - add 30s buffer to token validation"
|
|
19
|
+
- "The staging deploy must happen before prod or the migration breaks"
|
|
20
|
+
|
|
21
|
+
Not worth recording:
|
|
22
|
+
- Generic docs (we can search those)
|
|
23
|
+
- Temporary hacks
|
|
24
|
+
- Stuff that'll change next week
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
|
|
28
|
+
Quick:
|
|
29
|
+
- record({ insight: "Stripe webhooks need idempotency checks" })
|
|
30
|
+
|
|
31
|
+
Rich:
|
|
32
|
+
- record({
|
|
33
|
+
intent: "Add Stripe webhook handler",
|
|
34
|
+
insight: "Stripe retries failed webhooks but doesn't dedupe. Always check idempotency key or you'll process payments twice.",
|
|
35
|
+
reasoning: "Their retry logic assumes failures, not slow responses",
|
|
36
|
+
context: "payments",
|
|
37
|
+
example: "if (await isDuplicate(event.id)) return;"
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
WHEN TO CALL THIS:
|
|
41
|
+
- The moment you think "that's useful" - capture it NOW
|
|
42
|
+
- After solving something tricky - what made it work?
|
|
43
|
+
- When you make a decision - why this over alternatives?
|
|
44
|
+
- Before context dies - don't let insights evaporate
|
|
45
|
+
|
|
46
|
+
Multiple calls welcome - one insight per call. Don't batch, don't wait.
|
|
47
|
+
Context compacts, memories disappear. If it's worth knowing tomorrow,
|
|
48
|
+
record it today.`,
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
insight: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'What did we learn? The knowledge worth keeping.',
|
|
55
|
+
},
|
|
56
|
+
intent: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'What were we trying to accomplish?',
|
|
59
|
+
},
|
|
60
|
+
reasoning: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Why does this work? Why this over alternatives?',
|
|
63
|
+
},
|
|
64
|
+
context: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: "Topic, area, or when this applies (e.g., 'auth', 'payments', 'only on Windows')",
|
|
67
|
+
},
|
|
68
|
+
example: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Code snippet or concrete illustration',
|
|
71
|
+
},
|
|
72
|
+
title: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Entry title. Auto-generated from insight if not provided.',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ['insight'],
|
|
78
|
+
},
|
|
79
|
+
async handler(args) {
|
|
80
|
+
const { insight, intent, reasoning, context, example, title: providedTitle } = args;
|
|
81
|
+
if (!insight) {
|
|
82
|
+
throw new Error('insight is required');
|
|
83
|
+
}
|
|
84
|
+
const libraryPath = getLibraryPath();
|
|
85
|
+
const localPath = getLocalPath(libraryPath);
|
|
86
|
+
// Ensure local directory exists
|
|
87
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
88
|
+
// Generate title
|
|
89
|
+
const title = providedTitle || generateTitle(insight, intent);
|
|
90
|
+
// Generate slug for filename
|
|
91
|
+
const slug = slugify(title);
|
|
92
|
+
const created = new Date().toISOString();
|
|
93
|
+
// Handle filename collisions
|
|
94
|
+
let filename = `${slug}.md`;
|
|
95
|
+
let filePath = path.join(localPath, filename);
|
|
96
|
+
let counter = 1;
|
|
97
|
+
while (await fileExists(filePath)) {
|
|
98
|
+
filename = `${slug}-${counter}.md`;
|
|
99
|
+
filePath = path.join(localPath, filename);
|
|
100
|
+
counter++;
|
|
101
|
+
}
|
|
102
|
+
// Build frontmatter
|
|
103
|
+
const frontmatterLines = ['---'];
|
|
104
|
+
if (intent) {
|
|
105
|
+
frontmatterLines.push(`intent: "${escapeYaml(intent)}"`);
|
|
106
|
+
}
|
|
107
|
+
if (context) {
|
|
108
|
+
frontmatterLines.push(`context: "${escapeYaml(context)}"`);
|
|
109
|
+
}
|
|
110
|
+
frontmatterLines.push(`created: "${created}"`);
|
|
111
|
+
frontmatterLines.push(`updated: "${created}"`);
|
|
112
|
+
frontmatterLines.push('source: "local"');
|
|
113
|
+
frontmatterLines.push('---');
|
|
114
|
+
// Build body
|
|
115
|
+
const bodyLines = [];
|
|
116
|
+
bodyLines.push(`# ${title}`);
|
|
117
|
+
bodyLines.push('');
|
|
118
|
+
bodyLines.push(insight);
|
|
119
|
+
if (reasoning) {
|
|
120
|
+
bodyLines.push('');
|
|
121
|
+
bodyLines.push('## Reasoning');
|
|
122
|
+
bodyLines.push('');
|
|
123
|
+
bodyLines.push(reasoning);
|
|
124
|
+
}
|
|
125
|
+
if (example) {
|
|
126
|
+
bodyLines.push('');
|
|
127
|
+
bodyLines.push('## Example');
|
|
128
|
+
bodyLines.push('');
|
|
129
|
+
// Detect if it looks like code
|
|
130
|
+
if (example.includes('\n') || example.includes('{') || example.includes('(')) {
|
|
131
|
+
bodyLines.push('```');
|
|
132
|
+
bodyLines.push(example);
|
|
133
|
+
bodyLines.push('```');
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
bodyLines.push('```');
|
|
137
|
+
bodyLines.push(example);
|
|
138
|
+
bodyLines.push('```');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Combine and write
|
|
142
|
+
const fileContent = frontmatterLines.join('\n') + '\n\n' + bodyLines.join('\n') + '\n';
|
|
143
|
+
await fs.writeFile(filePath, fileContent, 'utf-8');
|
|
144
|
+
const relativePath = path.relative(libraryPath, filePath);
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
path: relativePath,
|
|
148
|
+
title,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Helper Functions
|
|
154
|
+
// ============================================================================
|
|
155
|
+
function generateTitle(insight, intent) {
|
|
156
|
+
// Try to extract from first sentence of insight
|
|
157
|
+
const firstSentence = insight.split(/[.!?\n]/)[0].trim();
|
|
158
|
+
if (firstSentence.length <= 60) {
|
|
159
|
+
return firstSentence;
|
|
160
|
+
}
|
|
161
|
+
// If insight is too long, try intent
|
|
162
|
+
if (intent && intent.length <= 60) {
|
|
163
|
+
return intent;
|
|
164
|
+
}
|
|
165
|
+
// Truncate insight
|
|
166
|
+
return firstSentence.slice(0, 57) + '...';
|
|
167
|
+
}
|
|
168
|
+
function slugify(text) {
|
|
169
|
+
return text
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
172
|
+
.replace(/^-+|-+$/g, '')
|
|
173
|
+
.slice(0, 50);
|
|
174
|
+
}
|
|
175
|
+
function escapeYaml(text) {
|
|
176
|
+
return text.replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
177
|
+
}
|
|
178
|
+
async function fileExists(filePath) {
|
|
179
|
+
try {
|
|
180
|
+
await fs.access(filePath);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telvok/librarian-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Knowledge capture MCP server - remember what you learn with AI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"librarian-mcp": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"claude",
|
|
23
|
+
"ai",
|
|
24
|
+
"knowledge-management",
|
|
25
|
+
"memory"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/kogbuze/librarian.git"
|
|
30
|
+
},
|
|
31
|
+
"author": "Telvok",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
35
|
+
"glob": "^11.0.0",
|
|
36
|
+
"gray-matter": "^4.0.3",
|
|
37
|
+
"uuid": "^11.0.0",
|
|
38
|
+
"zod": "^3.24.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.0.0",
|
|
42
|
+
"@types/uuid": "^10.0.0",
|
|
43
|
+
"typescript": "^5.7.0"
|
|
44
|
+
}
|
|
45
|
+
}
|