repomemory 0.1.0 → 0.2.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/README.md +151 -104
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +162 -188
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/dashboard.d.ts +5 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +520 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +33 -34
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +2 -1
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +151 -35
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +87 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +57 -27
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/wizard.d.ts +4 -0
- package/dist/commands/wizard.d.ts.map +1 -0
- package/dist/commands/wizard.js +184 -0
- package/dist/commands/wizard.js.map +1 -0
- package/dist/index.js +37 -42
- package/dist/index.js.map +1 -1
- package/dist/lib/ai-provider.d.ts +11 -0
- package/dist/lib/ai-provider.d.ts.map +1 -1
- package/dist/lib/ai-provider.js +138 -69
- package/dist/lib/ai-provider.js.map +1 -1
- package/dist/lib/config.d.ts +11 -15
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +33 -21
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-store.d.ts +11 -0
- package/dist/lib/context-store.d.ts.map +1 -1
- package/dist/lib/context-store.js +51 -18
- package/dist/lib/context-store.js.map +1 -1
- package/dist/lib/git.d.ts +1 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +34 -20
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/json-repair.d.ts +24 -0
- package/dist/lib/json-repair.d.ts.map +1 -0
- package/dist/lib/json-repair.js +140 -0
- package/dist/lib/json-repair.js.map +1 -0
- package/dist/lib/repo-scanner.d.ts.map +1 -1
- package/dist/lib/repo-scanner.js +103 -26
- package/dist/lib/repo-scanner.js.map +1 -1
- package/dist/lib/search.d.ts +10 -4
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +136 -51
- package/dist/lib/search.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +128 -54
- package/dist/mcp/server.js.map +1 -1
- package/package.json +20 -8
package/dist/lib/search.js
CHANGED
|
@@ -1,17 +1,50 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
let initSqlJsPromise = null;
|
|
4
|
+
async function getSqlJs() {
|
|
5
|
+
if (!initSqlJsPromise) {
|
|
6
|
+
initSqlJsPromise = import("sql.js").then((mod) => {
|
|
7
|
+
const initSqlJs = mod.default;
|
|
8
|
+
return initSqlJs();
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return initSqlJsPromise;
|
|
12
|
+
}
|
|
3
13
|
export class SearchIndex {
|
|
4
|
-
db;
|
|
14
|
+
db = null;
|
|
15
|
+
dbPath;
|
|
5
16
|
store;
|
|
17
|
+
initialized = false;
|
|
6
18
|
constructor(contextDir, store) {
|
|
7
|
-
|
|
8
|
-
this.db = new Database(dbPath);
|
|
19
|
+
this.dbPath = join(contextDir, ".search.db");
|
|
9
20
|
this.store = store;
|
|
10
|
-
this.init();
|
|
11
21
|
}
|
|
12
|
-
|
|
13
|
-
this.db
|
|
14
|
-
|
|
22
|
+
async ensureDb() {
|
|
23
|
+
if (this.db)
|
|
24
|
+
return this.db;
|
|
25
|
+
const SQL = await getSqlJs();
|
|
26
|
+
if (existsSync(this.dbPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const buffer = readFileSync(this.dbPath);
|
|
29
|
+
this.db = new SQL.Database(buffer);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
this.db = new SQL.Database();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.db = new SQL.Database();
|
|
37
|
+
}
|
|
38
|
+
if (!this.initialized) {
|
|
39
|
+
this.initSchema();
|
|
40
|
+
this.initialized = true;
|
|
41
|
+
}
|
|
42
|
+
return this.db;
|
|
43
|
+
}
|
|
44
|
+
initSchema() {
|
|
45
|
+
if (!this.db)
|
|
46
|
+
return;
|
|
47
|
+
this.db.run(`
|
|
15
48
|
CREATE TABLE IF NOT EXISTS documents (
|
|
16
49
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
50
|
category TEXT NOT NULL,
|
|
@@ -23,7 +56,7 @@ export class SearchIndex {
|
|
|
23
56
|
UNIQUE(category, filename)
|
|
24
57
|
)
|
|
25
58
|
`);
|
|
26
|
-
this.db.
|
|
59
|
+
this.db.run(`
|
|
27
60
|
CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(
|
|
28
61
|
title,
|
|
29
62
|
content,
|
|
@@ -33,20 +66,19 @@ export class SearchIndex {
|
|
|
33
66
|
tokenize='porter unicode61'
|
|
34
67
|
)
|
|
35
68
|
`);
|
|
36
|
-
|
|
37
|
-
this.db.exec(`
|
|
69
|
+
this.db.run(`
|
|
38
70
|
CREATE TRIGGER IF NOT EXISTS documents_ai AFTER INSERT ON documents BEGIN
|
|
39
71
|
INSERT INTO documents_fts(rowid, title, content, category)
|
|
40
72
|
VALUES (new.id, new.title, new.content, new.category);
|
|
41
73
|
END
|
|
42
74
|
`);
|
|
43
|
-
this.db.
|
|
75
|
+
this.db.run(`
|
|
44
76
|
CREATE TRIGGER IF NOT EXISTS documents_ad AFTER DELETE ON documents BEGIN
|
|
45
77
|
INSERT INTO documents_fts(documents_fts, rowid, title, content, category)
|
|
46
78
|
VALUES ('delete', old.id, old.title, old.content, old.category);
|
|
47
79
|
END
|
|
48
80
|
`);
|
|
49
|
-
this.db.
|
|
81
|
+
this.db.run(`
|
|
50
82
|
CREATE TRIGGER IF NOT EXISTS documents_au AFTER UPDATE ON documents BEGIN
|
|
51
83
|
INSERT INTO documents_fts(documents_fts, rowid, title, content, category)
|
|
52
84
|
VALUES ('delete', old.id, old.title, old.content, old.category);
|
|
@@ -55,38 +87,53 @@ export class SearchIndex {
|
|
|
55
87
|
END
|
|
56
88
|
`);
|
|
57
89
|
}
|
|
58
|
-
rebuild() {
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
async rebuild() {
|
|
91
|
+
const db = await this.ensureDb();
|
|
92
|
+
db.run("DELETE FROM documents");
|
|
61
93
|
const entries = this.store.listEntries();
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
db.run(`INSERT OR REPLACE INTO documents (category, filename, title, content, relative_path, updated_at)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
97
|
+
entry.category,
|
|
98
|
+
entry.filename,
|
|
99
|
+
entry.title,
|
|
100
|
+
entry.content,
|
|
101
|
+
entry.relativePath,
|
|
102
|
+
entry.lastModified.toISOString(),
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
this.save();
|
|
106
|
+
}
|
|
107
|
+
async indexEntry(entry) {
|
|
108
|
+
const db = await this.ensureDb();
|
|
109
|
+
// Upsert: delete then insert to trigger FTS sync
|
|
110
|
+
db.run("DELETE FROM documents WHERE category = ? AND filename = ?", [entry.category, entry.filename]);
|
|
111
|
+
db.run(`INSERT INTO documents (category, filename, title, content, relative_path, updated_at)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
113
|
+
entry.category,
|
|
114
|
+
entry.filename,
|
|
115
|
+
entry.title,
|
|
116
|
+
entry.content,
|
|
117
|
+
entry.relativePath,
|
|
118
|
+
entry.lastModified.toISOString(),
|
|
119
|
+
]);
|
|
120
|
+
this.save();
|
|
72
121
|
}
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
78
|
-
`)
|
|
79
|
-
.run(entry.category, entry.filename, entry.title, entry.content, entry.relativePath, entry.lastModified.toISOString());
|
|
122
|
+
async removeEntry(category, filename) {
|
|
123
|
+
const db = await this.ensureDb();
|
|
124
|
+
db.run("DELETE FROM documents WHERE category = ? AND filename = ?", [category, filename]);
|
|
125
|
+
this.save();
|
|
80
126
|
}
|
|
81
|
-
search(query, category, limit = 10) {
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
async search(query, category, limit = 10) {
|
|
128
|
+
const db = await this.ensureDb();
|
|
129
|
+
// Build FTS5 query: use AND semantics (implicit AND in FTS5)
|
|
130
|
+
const terms = query
|
|
84
131
|
.replace(/['"]/g, "")
|
|
85
132
|
.split(/\s+/)
|
|
86
133
|
.filter(Boolean)
|
|
87
134
|
.map((term) => `"${term}"`)
|
|
88
|
-
.join("
|
|
89
|
-
if (!
|
|
135
|
+
.join(" ");
|
|
136
|
+
if (!terms)
|
|
90
137
|
return [];
|
|
91
138
|
let sql = `
|
|
92
139
|
SELECT
|
|
@@ -100,27 +147,51 @@ export class SearchIndex {
|
|
|
100
147
|
JOIN documents d ON d.id = documents_fts.rowid
|
|
101
148
|
WHERE documents_fts MATCH ?
|
|
102
149
|
`;
|
|
103
|
-
const params = [
|
|
150
|
+
const params = [terms];
|
|
104
151
|
if (category) {
|
|
105
152
|
sql += " AND d.category = ?";
|
|
106
153
|
params.push(category);
|
|
107
154
|
}
|
|
108
155
|
sql += " ORDER BY rank LIMIT ?";
|
|
109
156
|
params.push(limit);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
try {
|
|
158
|
+
const results = db.exec(sql, params);
|
|
159
|
+
if (!results.length || !results[0].values.length) {
|
|
160
|
+
// Fallback: try OR semantics if AND returned nothing
|
|
161
|
+
const orTerms = query
|
|
162
|
+
.replace(/['"]/g, "")
|
|
163
|
+
.split(/\s+/)
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.map((term) => `"${term}"`)
|
|
166
|
+
.join(" OR ");
|
|
167
|
+
if (orTerms !== terms) {
|
|
168
|
+
params[0] = orTerms;
|
|
169
|
+
const orResults = db.exec(sql, params);
|
|
170
|
+
if (orResults.length && orResults[0].values.length) {
|
|
171
|
+
return this.mapResults(orResults[0], query);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
return this.mapResults(results[0], query);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
mapResults(result, query) {
|
|
183
|
+
return result.values.map((row) => ({
|
|
184
|
+
category: row[0],
|
|
185
|
+
filename: row[1],
|
|
186
|
+
title: row[2],
|
|
187
|
+
snippet: this.extractSnippet(row[3], query),
|
|
188
|
+
score: row[5],
|
|
189
|
+
relativePath: row[4],
|
|
118
190
|
}));
|
|
119
191
|
}
|
|
120
192
|
extractSnippet(content, query, maxLength = 500) {
|
|
121
193
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
122
194
|
const lines = content.split("\n");
|
|
123
|
-
// Find lines containing query terms
|
|
124
195
|
const matchingLines = [];
|
|
125
196
|
for (let i = 0; i < lines.length; i++) {
|
|
126
197
|
const lower = lines[i].toLowerCase();
|
|
@@ -132,16 +203,30 @@ export class SearchIndex {
|
|
|
132
203
|
if (matchingLines.length === 0) {
|
|
133
204
|
return content.slice(0, maxLength);
|
|
134
205
|
}
|
|
135
|
-
// Sort by match count, take best matches
|
|
136
206
|
matchingLines.sort((a, b) => b.matchCount - a.matchCount);
|
|
137
|
-
// Build snippet from best matching region
|
|
138
207
|
const bestMatch = matchingLines[0];
|
|
139
208
|
const start = Math.max(0, bestMatch.index - 2);
|
|
140
209
|
const end = Math.min(lines.length, bestMatch.index + 5);
|
|
141
210
|
return lines.slice(start, end).join("\n").slice(0, maxLength);
|
|
142
211
|
}
|
|
212
|
+
save() {
|
|
213
|
+
if (!this.db)
|
|
214
|
+
return;
|
|
215
|
+
try {
|
|
216
|
+
const data = this.db.export();
|
|
217
|
+
writeFileSync(this.dbPath, Buffer.from(data));
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Silently fail — search degradation is acceptable
|
|
221
|
+
}
|
|
222
|
+
}
|
|
143
223
|
close() {
|
|
144
|
-
this.db
|
|
224
|
+
if (this.db) {
|
|
225
|
+
this.save();
|
|
226
|
+
this.db.close();
|
|
227
|
+
this.db = null;
|
|
228
|
+
}
|
|
229
|
+
this.initialized = false;
|
|
145
230
|
}
|
|
146
231
|
}
|
|
147
232
|
//# sourceMappingURL=search.js.map
|
package/dist/lib/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/lib/search.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/lib/search.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAI1D,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CAgdf"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,14 +3,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { ContextStore } from "../lib/context-store.js";
|
|
5
5
|
import { SearchIndex } from "../lib/search.js";
|
|
6
|
+
const VALID_CATEGORIES = ["facts", "decisions", "regressions", "sessions", "changelog"];
|
|
6
7
|
export async function startMcpServer(repoRoot, config) {
|
|
7
8
|
const store = new ContextStore(repoRoot, config);
|
|
8
9
|
let searchIndex = null;
|
|
9
|
-
// Initialize search index if .context exists
|
|
10
10
|
if (store.exists()) {
|
|
11
11
|
try {
|
|
12
12
|
searchIndex = new SearchIndex(store.path, store);
|
|
13
|
-
searchIndex.rebuild();
|
|
13
|
+
await searchIndex.rebuild();
|
|
14
14
|
}
|
|
15
15
|
catch (e) {
|
|
16
16
|
console.error("Warning: Could not initialize search index:", e);
|
|
@@ -18,7 +18,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
18
18
|
}
|
|
19
19
|
const server = new Server({
|
|
20
20
|
name: "repomemory",
|
|
21
|
-
version: "0.
|
|
21
|
+
version: "0.2.0",
|
|
22
22
|
}, {
|
|
23
23
|
capabilities: {
|
|
24
24
|
tools: {},
|
|
@@ -31,7 +31,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
31
31
|
tools: [
|
|
32
32
|
{
|
|
33
33
|
name: "context_search",
|
|
34
|
-
description: "Search the repository's persistent knowledge base. Returns relevant facts, decisions, regressions, and session notes. Use this at the start of
|
|
34
|
+
description: "Search the repository's persistent knowledge base. Returns relevant facts, decisions, regressions, and session notes. Use this FIRST at the start of every task to find relevant context, or when you need to understand why something is the way it is. This prevents re-discovering architecture and re-debating past decisions.",
|
|
35
35
|
inputSchema: {
|
|
36
36
|
type: "object",
|
|
37
37
|
properties: {
|
|
@@ -41,7 +41,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
41
41
|
},
|
|
42
42
|
category: {
|
|
43
43
|
type: "string",
|
|
44
|
-
enum:
|
|
44
|
+
enum: VALID_CATEGORIES,
|
|
45
45
|
description: "Optional: filter results to a specific category. Omit to search all.",
|
|
46
46
|
},
|
|
47
47
|
limit: {
|
|
@@ -60,7 +60,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
60
60
|
properties: {
|
|
61
61
|
category: {
|
|
62
62
|
type: "string",
|
|
63
|
-
enum:
|
|
63
|
+
enum: VALID_CATEGORIES,
|
|
64
64
|
description: `Category for the knowledge:
|
|
65
65
|
- facts: Architecture, patterns, how things work
|
|
66
66
|
- decisions: Why something was chosen (include alternatives considered)
|
|
@@ -84,15 +84,34 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
84
84
|
required: ["category", "filename", "content"],
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: "context_delete",
|
|
89
|
+
description: "Delete a knowledge entry from the repository context. Use this to remove stale or incorrect information. Knowledge quality matters more than quantity — prune aggressively.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
category: {
|
|
94
|
+
type: "string",
|
|
95
|
+
enum: VALID_CATEGORIES,
|
|
96
|
+
description: "The category of the entry to delete.",
|
|
97
|
+
},
|
|
98
|
+
filename: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "The filename to delete (with or without .md extension).",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ["category", "filename"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
87
106
|
{
|
|
88
107
|
name: "context_list",
|
|
89
|
-
description: "List all knowledge entries in the repository context, optionally filtered by category. Returns filenames and
|
|
108
|
+
description: "List all knowledge entries in the repository context, optionally filtered by category. Returns filenames, titles, and age for browsing. Use this to understand what knowledge already exists before writing new entries.",
|
|
90
109
|
inputSchema: {
|
|
91
110
|
type: "object",
|
|
92
111
|
properties: {
|
|
93
112
|
category: {
|
|
94
113
|
type: "string",
|
|
95
|
-
enum:
|
|
114
|
+
enum: VALID_CATEGORIES,
|
|
96
115
|
description: "Optional: filter to a specific category.",
|
|
97
116
|
},
|
|
98
117
|
},
|
|
@@ -124,22 +143,29 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
124
143
|
switch (name) {
|
|
125
144
|
case "context_search": {
|
|
126
145
|
const { query, category, limit = 5 } = args;
|
|
146
|
+
// Validate category if provided
|
|
147
|
+
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: `Invalid category: ${category}. Valid categories: ${VALID_CATEGORIES.join(", ")}`,
|
|
152
|
+
}],
|
|
153
|
+
isError: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
127
156
|
if (!store.exists()) {
|
|
128
157
|
return {
|
|
129
|
-
content: [
|
|
130
|
-
{
|
|
158
|
+
content: [{
|
|
131
159
|
type: "text",
|
|
132
160
|
text: "No .context/ directory found. Run `repomemory init && repomemory analyze` to set up.",
|
|
133
|
-
},
|
|
134
|
-
],
|
|
161
|
+
}],
|
|
135
162
|
};
|
|
136
163
|
}
|
|
137
|
-
// Rebuild index if needed
|
|
138
164
|
if (!searchIndex) {
|
|
139
165
|
searchIndex = new SearchIndex(store.path, store);
|
|
140
|
-
searchIndex.rebuild();
|
|
166
|
+
await searchIndex.rebuild();
|
|
141
167
|
}
|
|
142
|
-
const results = searchIndex.search(query, category, limit);
|
|
168
|
+
const results = await searchIndex.search(query, category, limit);
|
|
143
169
|
if (results.length === 0) {
|
|
144
170
|
// Fallback to simple text search
|
|
145
171
|
const entries = store.listEntries(category);
|
|
@@ -150,12 +176,10 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
150
176
|
.slice(0, limit);
|
|
151
177
|
if (matched.length === 0) {
|
|
152
178
|
return {
|
|
153
|
-
content: [
|
|
154
|
-
{
|
|
179
|
+
content: [{
|
|
155
180
|
type: "text",
|
|
156
181
|
text: `No results found for "${query}"${category ? ` in ${category}` : ""}. Try a different query or browse with context_list.`,
|
|
157
|
-
},
|
|
158
|
-
],
|
|
182
|
+
}],
|
|
159
183
|
};
|
|
160
184
|
}
|
|
161
185
|
const text = matched
|
|
@@ -170,6 +194,16 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
170
194
|
}
|
|
171
195
|
case "context_write": {
|
|
172
196
|
const { category, filename, content, append = false } = args;
|
|
197
|
+
// Validate category
|
|
198
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
199
|
+
return {
|
|
200
|
+
content: [{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `Invalid category: ${category}. Valid categories: ${VALID_CATEGORIES.join(", ")}`,
|
|
203
|
+
}],
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
173
207
|
if (!store.exists()) {
|
|
174
208
|
store.scaffold();
|
|
175
209
|
}
|
|
@@ -180,40 +214,79 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
180
214
|
else {
|
|
181
215
|
relativePath = store.writeEntry(category, filename, content);
|
|
182
216
|
}
|
|
183
|
-
//
|
|
217
|
+
// Incremental index update (not full rebuild)
|
|
184
218
|
if (searchIndex) {
|
|
185
|
-
|
|
219
|
+
const entries = store.listEntries(category);
|
|
220
|
+
const entry = entries.find((e) => e.relativePath === relativePath || e.filename === filename + ".md");
|
|
221
|
+
if (entry) {
|
|
222
|
+
await searchIndex.indexEntry(entry);
|
|
223
|
+
}
|
|
186
224
|
}
|
|
187
225
|
return {
|
|
188
|
-
content: [
|
|
189
|
-
{
|
|
226
|
+
content: [{
|
|
190
227
|
type: "text",
|
|
191
|
-
text:
|
|
192
|
-
},
|
|
193
|
-
|
|
228
|
+
text: `\u2713 Written to ${relativePath}${append ? " (appended)" : ""}. This knowledge will persist across sessions.`,
|
|
229
|
+
}],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case "context_delete": {
|
|
233
|
+
const { category, filename } = args;
|
|
234
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{
|
|
237
|
+
type: "text",
|
|
238
|
+
text: `Invalid category: ${category}. Valid categories: ${VALID_CATEGORIES.join(", ")}`,
|
|
239
|
+
}],
|
|
240
|
+
isError: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const fname = filename.endsWith(".md") ? filename : filename + ".md";
|
|
244
|
+
const deleted = store.deleteEntry(category, fname);
|
|
245
|
+
if (!deleted) {
|
|
246
|
+
return {
|
|
247
|
+
content: [{
|
|
248
|
+
type: "text",
|
|
249
|
+
text: `File not found: ${category}/${fname}. Use context_list to see available files.`,
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Remove from search index
|
|
254
|
+
if (searchIndex) {
|
|
255
|
+
await searchIndex.removeEntry(category, fname);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
content: [{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: `\u2713 Deleted ${category}/${fname}. Stale knowledge removed.`,
|
|
261
|
+
}],
|
|
194
262
|
};
|
|
195
263
|
}
|
|
196
264
|
case "context_list": {
|
|
197
265
|
const { category } = (args || {});
|
|
266
|
+
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
267
|
+
return {
|
|
268
|
+
content: [{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: `Invalid category: ${category}. Valid categories: ${VALID_CATEGORIES.join(", ")}`,
|
|
271
|
+
}],
|
|
272
|
+
isError: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
198
275
|
if (!store.exists()) {
|
|
199
276
|
return {
|
|
200
|
-
content: [
|
|
201
|
-
{
|
|
277
|
+
content: [{
|
|
202
278
|
type: "text",
|
|
203
279
|
text: "No .context/ directory found. Run `repomemory init` first.",
|
|
204
|
-
},
|
|
205
|
-
],
|
|
280
|
+
}],
|
|
206
281
|
};
|
|
207
282
|
}
|
|
208
283
|
const entries = store.listEntries(category);
|
|
209
284
|
if (entries.length === 0) {
|
|
210
285
|
return {
|
|
211
|
-
content: [
|
|
212
|
-
{
|
|
286
|
+
content: [{
|
|
213
287
|
type: "text",
|
|
214
288
|
text: `No entries found${category ? ` in ${category}` : ""}. Run \`repomemory analyze\` to populate, or use context_write to add entries.`,
|
|
215
|
-
},
|
|
216
|
-
],
|
|
289
|
+
}],
|
|
217
290
|
};
|
|
218
291
|
}
|
|
219
292
|
const grouped = {};
|
|
@@ -228,7 +301,7 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
228
301
|
for (const entry of catEntries) {
|
|
229
302
|
const sizeKb = (entry.sizeBytes / 1024).toFixed(1);
|
|
230
303
|
const age = getRelativeTime(entry.lastModified);
|
|
231
|
-
text += `- **${entry.filename}**
|
|
304
|
+
text += `- **${entry.filename}** \u2014 ${entry.title} (${sizeKb}KB, ${age})\n`;
|
|
232
305
|
}
|
|
233
306
|
text += "\n";
|
|
234
307
|
}
|
|
@@ -240,36 +313,30 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
240
313
|
const content = store.readEntry(category, fname);
|
|
241
314
|
if (!content) {
|
|
242
315
|
return {
|
|
243
|
-
content: [
|
|
244
|
-
{
|
|
316
|
+
content: [{
|
|
245
317
|
type: "text",
|
|
246
318
|
text: `File not found: ${category}/${fname}. Use context_list to see available files.`,
|
|
247
|
-
},
|
|
248
|
-
],
|
|
319
|
+
}],
|
|
249
320
|
};
|
|
250
321
|
}
|
|
251
322
|
return {
|
|
252
|
-
content: [
|
|
253
|
-
{
|
|
323
|
+
content: [{
|
|
254
324
|
type: "text",
|
|
255
325
|
text: `# ${category}/${fname}\n\n${content}`,
|
|
256
|
-
},
|
|
257
|
-
],
|
|
326
|
+
}],
|
|
258
327
|
};
|
|
259
328
|
}
|
|
260
329
|
default:
|
|
261
330
|
return {
|
|
262
|
-
content: [
|
|
263
|
-
{
|
|
331
|
+
content: [{
|
|
264
332
|
type: "text",
|
|
265
333
|
text: `Unknown tool: ${name}`,
|
|
266
|
-
},
|
|
267
|
-
],
|
|
334
|
+
}],
|
|
268
335
|
isError: true,
|
|
269
336
|
};
|
|
270
337
|
}
|
|
271
338
|
});
|
|
272
|
-
// --- Resources
|
|
339
|
+
// --- Resources ---
|
|
273
340
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
274
341
|
if (!store.exists()) {
|
|
275
342
|
return { resources: [] };
|
|
@@ -296,21 +363,28 @@ export async function startMcpServer(repoRoot, config) {
|
|
|
296
363
|
throw new Error(`Resource not found: ${uri}`);
|
|
297
364
|
}
|
|
298
365
|
return {
|
|
299
|
-
contents: [
|
|
300
|
-
{
|
|
366
|
+
contents: [{
|
|
301
367
|
uri,
|
|
302
368
|
mimeType: "text/markdown",
|
|
303
369
|
text: content,
|
|
304
|
-
},
|
|
305
|
-
],
|
|
370
|
+
}],
|
|
306
371
|
};
|
|
307
372
|
});
|
|
308
|
-
//
|
|
373
|
+
// Graceful shutdown
|
|
374
|
+
const cleanup = () => {
|
|
375
|
+
if (searchIndex) {
|
|
376
|
+
searchIndex.close();
|
|
377
|
+
searchIndex = null;
|
|
378
|
+
}
|
|
379
|
+
process.exit(0);
|
|
380
|
+
};
|
|
381
|
+
process.on("SIGTERM", cleanup);
|
|
382
|
+
process.on("SIGINT", cleanup);
|
|
309
383
|
const transport = new StdioServerTransport();
|
|
310
384
|
await server.connect(transport);
|
|
311
385
|
}
|
|
312
386
|
function getRelativeTime(date) {
|
|
313
|
-
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
387
|
+
const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
|
|
314
388
|
if (seconds < 60)
|
|
315
389
|
return "just now";
|
|
316
390
|
if (seconds < 3600)
|