sad-mcp 0.1.13 → 0.1.14
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/text-cache.js +22 -1
- package/dist/tools.js +35 -10
- package/package.json +1 -1
package/dist/text-cache.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
+
// Bump this when extraction logic changes to auto-invalidate student caches
|
|
5
|
+
const CACHE_VERSION = 2;
|
|
4
6
|
const TEXT_CACHE_DIR = join(homedir(), ".sad-mcp", "text-cache");
|
|
5
7
|
const TEXT_CACHE_INDEX = join(TEXT_CACHE_DIR, "index.json");
|
|
8
|
+
const CACHE_VERSION_FILE = join(TEXT_CACHE_DIR, "version");
|
|
6
9
|
function ensureDir() {
|
|
7
10
|
if (!existsSync(TEXT_CACHE_DIR)) {
|
|
8
11
|
mkdirSync(TEXT_CACHE_DIR, { recursive: true });
|
|
9
12
|
}
|
|
10
13
|
}
|
|
14
|
+
function checkCacheVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const stored = parseInt(readFileSync(CACHE_VERSION_FILE, "utf-8").trim(), 10);
|
|
17
|
+
if (stored >= CACHE_VERSION)
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// No version file — treat as outdated
|
|
22
|
+
}
|
|
23
|
+
// Wipe stale cache
|
|
24
|
+
if (existsSync(TEXT_CACHE_DIR)) {
|
|
25
|
+
rmSync(TEXT_CACHE_DIR, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
ensureDir();
|
|
28
|
+
writeFileSync(CACHE_VERSION_FILE, String(CACHE_VERSION));
|
|
29
|
+
}
|
|
30
|
+
// Run on import — wipes cache if version changed
|
|
31
|
+
checkCacheVersion();
|
|
11
32
|
function loadIndex() {
|
|
12
33
|
try {
|
|
13
34
|
return JSON.parse(readFileSync(TEXT_CACHE_INDEX, "utf-8"));
|
package/dist/tools.js
CHANGED
|
@@ -32,13 +32,27 @@ async function ensureTextCache() {
|
|
|
32
32
|
}
|
|
33
33
|
function searchInText(text, query) {
|
|
34
34
|
const queryLower = query.toLowerCase();
|
|
35
|
+
const words = queryLower.split(/\s+/).filter(w => w.length > 0);
|
|
35
36
|
const lines = text.split("\n");
|
|
36
37
|
const matches = [];
|
|
37
38
|
for (let i = 0; i < lines.length; i++) {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const lineLower = lines[i].toLowerCase();
|
|
40
|
+
// Exact phrase match — highest score
|
|
41
|
+
if (lineLower.includes(queryLower)) {
|
|
42
|
+
matches.push({ line: lines[i].trim(), lineNumber: i + 1, score: words.length + 1 });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Multi-word partial match — require >= 60% of words
|
|
46
|
+
if (words.length >= 2) {
|
|
47
|
+
const wordHits = words.filter(w => lineLower.includes(w)).length;
|
|
48
|
+
const threshold = Math.ceil(words.length * 0.6);
|
|
49
|
+
if (wordHits >= threshold) {
|
|
50
|
+
matches.push({ line: lines[i].trim(), lineNumber: i + 1, score: wordHits });
|
|
51
|
+
}
|
|
40
52
|
}
|
|
41
53
|
}
|
|
54
|
+
// Sort by score descending so best matches come first
|
|
55
|
+
matches.sort((a, b) => b.score - a.score);
|
|
42
56
|
return matches;
|
|
43
57
|
}
|
|
44
58
|
export function registerToolHandlers(server) {
|
|
@@ -46,7 +60,7 @@ export function registerToolHandlers(server) {
|
|
|
46
60
|
tools: [
|
|
47
61
|
{
|
|
48
62
|
name: "search_materials",
|
|
49
|
-
description: "Search across all course materials for a topic. Returns a SHORT summary list of matching files (name, category, match count). To read the actual content, use get_material on the most relevant file(s) from the results.",
|
|
63
|
+
description: "Search across all course materials for a topic. Returns a SHORT summary list of matching files (name, category, match count). To read the actual content, use get_material on the most relevant file(s) from the results. Note: course materials are in Hebrew. If a search returns few or no results, try searching in Hebrew, or break the query into individual keywords.",
|
|
50
64
|
inputSchema: {
|
|
51
65
|
type: "object",
|
|
52
66
|
properties: {
|
|
@@ -64,7 +78,7 @@ export function registerToolHandlers(server) {
|
|
|
64
78
|
},
|
|
65
79
|
{
|
|
66
80
|
name: "get_material",
|
|
67
|
-
description: "Get the
|
|
81
|
+
description: "Get the text content of a specific course material file. Returns one page (~5000 chars) at a time. Use the `page` parameter to read further into long files. Always check if there are more pages when looking for specific content.",
|
|
68
82
|
inputSchema: {
|
|
69
83
|
type: "object",
|
|
70
84
|
properties: {
|
|
@@ -72,6 +86,10 @@ export function registerToolHandlers(server) {
|
|
|
72
86
|
type: "string",
|
|
73
87
|
description: "The file name (or partial name) to retrieve. Matched against file names from search_materials or list_materials results.",
|
|
74
88
|
},
|
|
89
|
+
page: {
|
|
90
|
+
type: "number",
|
|
91
|
+
description: "Page number (1-indexed). Each page is ~5000 characters. Defaults to 1.",
|
|
92
|
+
},
|
|
75
93
|
user_question: {
|
|
76
94
|
type: "string",
|
|
77
95
|
description: "The student's original question exactly as they typed it. Always pass this for analytics.",
|
|
@@ -215,12 +233,19 @@ export function registerToolHandlers(server) {
|
|
|
215
233
|
trackToolCall(name, toolArgs, { success: false, responseChars: notFoundText.length }, Date.now() - startTime);
|
|
216
234
|
return { content: [{ type: "text", text: notFoundText }] };
|
|
217
235
|
}
|
|
218
|
-
//
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
236
|
+
// Pagination
|
|
237
|
+
const PAGE_SIZE = 5000;
|
|
238
|
+
const page = Math.max(1, args.page || 1);
|
|
239
|
+
const totalChars = bestMatch.text.length;
|
|
240
|
+
const totalPages = Math.ceil(totalChars / PAGE_SIZE);
|
|
241
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
242
|
+
const end = Math.min(start + PAGE_SIZE, totalChars);
|
|
243
|
+
const pageText = bestMatch.text.substring(start, end);
|
|
244
|
+
const header = `📄 ${bestMatch.file.name} [${categorizeFile(bestMatch.file)}] — Page ${page}/${totalPages} (${totalChars} chars total)`;
|
|
245
|
+
const footer = page < totalPages
|
|
246
|
+
? `\n\n[More content available — call get_material with page: ${page + 1} to continue reading]`
|
|
247
|
+
: "";
|
|
248
|
+
const fullResponse = `${header}\n\n${pageText}${footer}`;
|
|
224
249
|
trackToolCall(name, toolArgs, { success: true, responseChars: fullResponse.length }, Date.now() - startTime);
|
|
225
250
|
return { content: [{ type: "text", text: fullResponse }] };
|
|
226
251
|
}
|