@telvok/librarian-mcp 1.0.2 → 1.1.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/server.js +9 -0
- package/dist/tools/brief.d.ts +2 -0
- package/dist/tools/brief.js +33 -4
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/mark-hit.d.ts +20 -0
- package/dist/tools/mark-hit.js +71 -0
- package/dist/tools/record.js +5 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
|
|
|
5
5
|
import { briefTool } from './tools/brief.js';
|
|
6
6
|
import { recordTool } from './tools/record.js';
|
|
7
7
|
import { adoptTool } from './tools/adopt.js';
|
|
8
|
+
import { markHitTool } from './tools/mark-hit.js';
|
|
8
9
|
const server = new Server({
|
|
9
10
|
name: 'librarian',
|
|
10
11
|
version: '1.0.0',
|
|
@@ -32,6 +33,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
32
33
|
description: adoptTool.description,
|
|
33
34
|
inputSchema: adoptTool.inputSchema,
|
|
34
35
|
},
|
|
36
|
+
{
|
|
37
|
+
name: markHitTool.name,
|
|
38
|
+
description: markHitTool.description,
|
|
39
|
+
inputSchema: markHitTool.inputSchema,
|
|
40
|
+
},
|
|
35
41
|
],
|
|
36
42
|
};
|
|
37
43
|
});
|
|
@@ -50,6 +56,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
50
56
|
case 'adopt':
|
|
51
57
|
result = await adoptTool.handler(args);
|
|
52
58
|
break;
|
|
59
|
+
case 'mark_hit':
|
|
60
|
+
result = await markHitTool.handler(args);
|
|
61
|
+
break;
|
|
53
62
|
default:
|
|
54
63
|
throw new Error(`Unknown tool: ${name}`);
|
|
55
64
|
}
|
package/dist/tools/brief.d.ts
CHANGED
package/dist/tools/brief.js
CHANGED
|
@@ -79,10 +79,9 @@ Examples:
|
|
|
79
79
|
const searchTerm = query.toLowerCase();
|
|
80
80
|
allEntries = allEntries.filter(entry => matchesSearch(entry, searchTerm));
|
|
81
81
|
}
|
|
82
|
-
// Sort by
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
82
|
+
// Sort by blended score: 60% recency + 40% hits
|
|
83
|
+
// Entries that helped before bubble up, but new entries still surface
|
|
84
|
+
allEntries = rankEntries(allEntries);
|
|
86
85
|
const total = allEntries.length;
|
|
87
86
|
// Apply limit
|
|
88
87
|
const entries = allEntries.slice(0, limit);
|
|
@@ -134,6 +133,8 @@ async function readEntry(filePath, libraryPath) {
|
|
|
134
133
|
preview,
|
|
135
134
|
path: path.relative(libraryPath, filePath),
|
|
136
135
|
created: data.created || new Date().toISOString(),
|
|
136
|
+
hits: typeof data.hits === 'number' ? data.hits : 0,
|
|
137
|
+
last_hit: data.last_hit || null,
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
140
|
catch {
|
|
@@ -159,3 +160,31 @@ function matchesSearch(entry, searchTerm) {
|
|
|
159
160
|
}
|
|
160
161
|
return false;
|
|
161
162
|
}
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Smart Ranking
|
|
165
|
+
// ============================================================================
|
|
166
|
+
const RECENCY_WEIGHT = 0.6;
|
|
167
|
+
const HITS_WEIGHT = 0.4;
|
|
168
|
+
const RECENCY_DECAY_DAYS = 30; // Entries older than this get minimal recency score
|
|
169
|
+
function rankEntries(entries) {
|
|
170
|
+
if (entries.length === 0)
|
|
171
|
+
return entries;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
// Find max hits for normalization (avoid divide by zero)
|
|
174
|
+
const maxHits = Math.max(1, ...entries.map(e => e.hits));
|
|
175
|
+
// Calculate scores
|
|
176
|
+
const scored = entries.map(entry => {
|
|
177
|
+
// Recency score: 1.0 for today, decays over RECENCY_DECAY_DAYS
|
|
178
|
+
const ageMs = now - new Date(entry.created).getTime();
|
|
179
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
180
|
+
const recencyScore = Math.max(0, 1 - (ageDays / RECENCY_DECAY_DAYS));
|
|
181
|
+
// Hits score: normalized 0-1 against max hits in library
|
|
182
|
+
const hitsScore = entry.hits / maxHits;
|
|
183
|
+
// Blended score
|
|
184
|
+
const score = (RECENCY_WEIGHT * recencyScore) + (HITS_WEIGHT * hitsScore);
|
|
185
|
+
return { entry, score };
|
|
186
|
+
});
|
|
187
|
+
// Sort by score descending
|
|
188
|
+
scored.sort((a, b) => b.score - a.score);
|
|
189
|
+
return scored.map(s => s.entry);
|
|
190
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
package/dist/tools/index.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface MarkHitResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
path: string;
|
|
4
|
+
hits: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const markHitTool: {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object";
|
|
11
|
+
properties: {
|
|
12
|
+
path: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
required: string[];
|
|
18
|
+
};
|
|
19
|
+
handler(args: unknown): Promise<MarkHitResult>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { getLibraryPath } from '../library/storage.js';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Tool Definition
|
|
7
|
+
// ============================================================================
|
|
8
|
+
export const markHitTool = {
|
|
9
|
+
name: 'mark_hit',
|
|
10
|
+
description: `Mark a library entry as helpful - call this when knowledge from the library helped solve a problem.
|
|
11
|
+
|
|
12
|
+
When an entry from brief() actually helped you complete a task or make a decision,
|
|
13
|
+
call mark_hit() on it. This helps the library learn which entries are most useful.
|
|
14
|
+
|
|
15
|
+
Entries with more hits bubble up in future brief() results.
|
|
16
|
+
|
|
17
|
+
Fire and forget - call it and move on.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
- mark_hit({ path: "local/stripe-webhooks-need-idempotency.md" })`,
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
path: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Path to the entry that helped (from brief() results)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ['path'],
|
|
30
|
+
},
|
|
31
|
+
async handler(args) {
|
|
32
|
+
const { path: entryPath } = args;
|
|
33
|
+
if (!entryPath) {
|
|
34
|
+
throw new Error('path is required');
|
|
35
|
+
}
|
|
36
|
+
const libraryPath = getLibraryPath();
|
|
37
|
+
// Resolve the full path
|
|
38
|
+
let fullPath;
|
|
39
|
+
if (path.isAbsolute(entryPath)) {
|
|
40
|
+
fullPath = entryPath;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
fullPath = path.join(libraryPath, entryPath);
|
|
44
|
+
}
|
|
45
|
+
// Read existing file
|
|
46
|
+
let content;
|
|
47
|
+
try {
|
|
48
|
+
content = await fs.readFile(fullPath, 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`Entry not found: ${entryPath}`);
|
|
52
|
+
}
|
|
53
|
+
// Parse frontmatter
|
|
54
|
+
const { data, content: body } = matter(content);
|
|
55
|
+
// Increment hits
|
|
56
|
+
const currentHits = typeof data.hits === 'number' ? data.hits : 0;
|
|
57
|
+
const newHits = currentHits + 1;
|
|
58
|
+
// Update frontmatter
|
|
59
|
+
data.hits = newHits;
|
|
60
|
+
data.last_hit = new Date().toISOString();
|
|
61
|
+
// Rebuild file content
|
|
62
|
+
const updatedContent = matter.stringify(body, data);
|
|
63
|
+
// Write back
|
|
64
|
+
await fs.writeFile(fullPath, updatedContent, 'utf-8');
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
path: entryPath,
|
|
68
|
+
hits: newHits,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
package/dist/tools/record.js
CHANGED
|
@@ -36,9 +36,11 @@ Quick:
|
|
|
36
36
|
|
|
37
37
|
Rich:
|
|
38
38
|
- record({
|
|
39
|
+
intent: "Setting up GitHub org for Telvok",
|
|
39
40
|
insight: "GitHub org names are first-come-first-served regardless of domain ownership",
|
|
40
41
|
context: "GitHub, npm, branding",
|
|
41
|
-
reasoning: "We owned telvok.com but someone squatted telvok org years ago"
|
|
42
|
+
reasoning: "We owned telvok.com but someone squatted telvok org years ago",
|
|
43
|
+
example: "Had to use telvokdev instead of telvok"
|
|
42
44
|
})`,
|
|
43
45
|
inputSchema: {
|
|
44
46
|
type: 'object',
|
|
@@ -104,6 +106,8 @@ Rich:
|
|
|
104
106
|
frontmatterLines.push(`created: "${created}"`);
|
|
105
107
|
frontmatterLines.push(`updated: "${created}"`);
|
|
106
108
|
frontmatterLines.push('source: "local"');
|
|
109
|
+
frontmatterLines.push('hits: 0');
|
|
110
|
+
frontmatterLines.push('last_hit: null');
|
|
107
111
|
frontmatterLines.push('---');
|
|
108
112
|
// Build body
|
|
109
113
|
const bodyLines = [];
|