@hir4ta/mneme 0.23.0 → 0.23.2
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/.claude-plugin/plugin.json +1 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/lib/save/index.js +784 -0
- package/dist/lib/search/prompt.js +672 -0
- package/dist/lib/session/finalize.js +1001 -0
- package/dist/lib/session/init.js +419 -0
- package/dist/lib/session-finalize.js +8 -8
- package/dist/lib/session-init.js +5 -5
- package/dist/server.js +6 -12
- package/dist/servers/db-server.js +239 -239
- package/dist/servers/search-server.js +8 -8
- package/hooks/lib/common.sh +16 -6
- package/package.json +2 -2
- package/scripts/search-benchmark.ts +1 -1
- package/servers/{db-benchmark.ts → db/benchmark.ts} +3 -3
- package/servers/{db-queries.ts → db/queries.ts} +2 -7
- package/servers/{db-save.ts → db/save.ts} +3 -3
- package/servers/{db-session-summary.ts → db/session-summary.ts} +5 -7
- package/servers/{db-server-tools.ts → db/tools.ts} +9 -14
- package/servers/{db-transcript.ts → db/transcript.ts} +2 -2
- package/servers/{db-utils.ts → db/utils.ts} +1 -5
- package/servers/db-server.ts +10 -15
- package/servers/search-server.ts +2 -3
- package/skills/harvest/SKILL.md +1 -0
- package/skills/report/SKILL.md +3 -1
- package/skills/resume/SKILL.md +1 -0
- package/skills/save/SKILL.md +1 -0
- package/skills/search/SKILL.md +1 -0
- /package/servers/{db-types.ts → db/types.ts} +0 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// lib/search/prompt.ts
|
|
4
|
+
import * as fs6 from "node:fs";
|
|
5
|
+
import * as path6 from "node:path";
|
|
6
|
+
|
|
7
|
+
// lib/search/approved-rules.ts
|
|
8
|
+
import * as fs4 from "node:fs";
|
|
9
|
+
import * as path4 from "node:path";
|
|
10
|
+
|
|
11
|
+
// lib/search/helpers.ts
|
|
12
|
+
import * as fs3 from "node:fs";
|
|
13
|
+
import * as path3 from "node:path";
|
|
14
|
+
|
|
15
|
+
// lib/search/fuzzy.ts
|
|
16
|
+
import * as fs2 from "node:fs";
|
|
17
|
+
import * as path2 from "node:path";
|
|
18
|
+
|
|
19
|
+
// lib/utils.ts
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
function safeReadJson(filePath, fallback) {
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
} catch {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function findJsonFiles(dir) {
|
|
31
|
+
const results = [];
|
|
32
|
+
if (!fs.existsSync(dir)) return results;
|
|
33
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const fullPath = path.join(dir, item.name);
|
|
36
|
+
if (item.isDirectory()) {
|
|
37
|
+
results.push(...findJsonFiles(fullPath));
|
|
38
|
+
} else if (item.name.endsWith(".json")) {
|
|
39
|
+
results.push(fullPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// lib/search/fuzzy.ts
|
|
46
|
+
function levenshtein(a, b) {
|
|
47
|
+
const matrix = [];
|
|
48
|
+
for (let i = 0; i <= a.length; i++) {
|
|
49
|
+
matrix[i] = [i];
|
|
50
|
+
}
|
|
51
|
+
for (let j = 0; j <= b.length; j++) {
|
|
52
|
+
matrix[0][j] = j;
|
|
53
|
+
}
|
|
54
|
+
for (let i = 1; i <= a.length; i++) {
|
|
55
|
+
for (let j = 1; j <= b.length; j++) {
|
|
56
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
57
|
+
matrix[i][j] = Math.min(
|
|
58
|
+
matrix[i - 1][j] + 1,
|
|
59
|
+
// deletion
|
|
60
|
+
matrix[i][j - 1] + 1,
|
|
61
|
+
// insertion
|
|
62
|
+
matrix[i - 1][j - 1] + cost
|
|
63
|
+
// substitution
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return matrix[a.length][b.length];
|
|
68
|
+
}
|
|
69
|
+
function expandAliases(query, tags) {
|
|
70
|
+
const results = /* @__PURE__ */ new Set([query]);
|
|
71
|
+
const lowerQuery = query.toLowerCase();
|
|
72
|
+
for (const tag of tags) {
|
|
73
|
+
const allTerms = [tag.id, tag.label, ...tag.aliases].map(
|
|
74
|
+
(t) => t.toLowerCase()
|
|
75
|
+
);
|
|
76
|
+
if (allTerms.includes(lowerQuery)) {
|
|
77
|
+
results.add(tag.id);
|
|
78
|
+
results.add(tag.label);
|
|
79
|
+
for (const alias of tag.aliases) {
|
|
80
|
+
results.add(alias);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Array.from(results);
|
|
85
|
+
}
|
|
86
|
+
function calculateSimilarity(text, query) {
|
|
87
|
+
const lowerText = text.toLowerCase();
|
|
88
|
+
const lowerQuery = query.toLowerCase();
|
|
89
|
+
if (lowerText === lowerQuery) return 10;
|
|
90
|
+
if (lowerText.includes(lowerQuery)) return 5;
|
|
91
|
+
if (lowerQuery.includes(lowerText)) return 3;
|
|
92
|
+
const distance = levenshtein(lowerText, lowerQuery);
|
|
93
|
+
if (distance <= 2) return 2;
|
|
94
|
+
if (distance <= 3) return 1;
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
async function search(options) {
|
|
98
|
+
const {
|
|
99
|
+
query,
|
|
100
|
+
mnemeDir,
|
|
101
|
+
targets = ["sessions", "decisions"],
|
|
102
|
+
limit = 20,
|
|
103
|
+
timeout = 1e4
|
|
104
|
+
} = options;
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
const results = [];
|
|
107
|
+
const tagsPath = path2.join(mnemeDir, "tags.json");
|
|
108
|
+
const tagsData = safeReadJson(tagsPath, { tags: [] });
|
|
109
|
+
const expandedQueries = expandAliases(query, tagsData.tags);
|
|
110
|
+
if (targets.includes("sessions")) {
|
|
111
|
+
const sessionsDir = path2.join(mnemeDir, "sessions");
|
|
112
|
+
if (fs2.existsSync(sessionsDir)) {
|
|
113
|
+
const files = findJsonFiles(sessionsDir);
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
if (Date.now() - startTime > timeout) break;
|
|
116
|
+
const session = safeReadJson(file, {});
|
|
117
|
+
const score = scoreDocument(session, expandedQueries, [
|
|
118
|
+
"title",
|
|
119
|
+
"goal",
|
|
120
|
+
"tags"
|
|
121
|
+
]);
|
|
122
|
+
if (score > 0) {
|
|
123
|
+
results.push({
|
|
124
|
+
type: "session",
|
|
125
|
+
id: session.id || path2.basename(file, ".json"),
|
|
126
|
+
score,
|
|
127
|
+
title: session.title || "Untitled",
|
|
128
|
+
highlights: []
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (targets.includes("decisions")) {
|
|
135
|
+
const decisionsDir = path2.join(mnemeDir, "decisions");
|
|
136
|
+
if (fs2.existsSync(decisionsDir)) {
|
|
137
|
+
const files = findJsonFiles(decisionsDir);
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
if (Date.now() - startTime > timeout) break;
|
|
140
|
+
const decision = safeReadJson(file, {});
|
|
141
|
+
const score = scoreDocument(decision, expandedQueries, [
|
|
142
|
+
"title",
|
|
143
|
+
"decision",
|
|
144
|
+
"tags"
|
|
145
|
+
]);
|
|
146
|
+
if (score > 0) {
|
|
147
|
+
results.push({
|
|
148
|
+
type: "decision",
|
|
149
|
+
id: decision.id || path2.basename(file, ".json"),
|
|
150
|
+
score,
|
|
151
|
+
title: decision.title || "Untitled",
|
|
152
|
+
highlights: []
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (targets.includes("patterns")) {
|
|
159
|
+
const patternsDir = path2.join(mnemeDir, "patterns");
|
|
160
|
+
if (fs2.existsSync(patternsDir)) {
|
|
161
|
+
const files = findJsonFiles(patternsDir);
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
if (Date.now() - startTime > timeout) break;
|
|
164
|
+
const pattern = safeReadJson(file, {});
|
|
165
|
+
const patterns = pattern.patterns || [];
|
|
166
|
+
for (const p of patterns) {
|
|
167
|
+
const score = scoreDocument(p, expandedQueries, [
|
|
168
|
+
"description",
|
|
169
|
+
"errorPattern",
|
|
170
|
+
"tags"
|
|
171
|
+
]);
|
|
172
|
+
if (score > 0) {
|
|
173
|
+
results.push({
|
|
174
|
+
type: "pattern",
|
|
175
|
+
id: `${path2.basename(file, ".json")}-${p.type || "unknown"}`,
|
|
176
|
+
score,
|
|
177
|
+
title: p.description || "Untitled pattern",
|
|
178
|
+
highlights: []
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
186
|
+
}
|
|
187
|
+
function scoreDocument(doc, queries, fields) {
|
|
188
|
+
let totalScore = 0;
|
|
189
|
+
for (const field of fields) {
|
|
190
|
+
const value = doc[field];
|
|
191
|
+
if (typeof value === "string") {
|
|
192
|
+
for (const q of queries) {
|
|
193
|
+
totalScore += calculateSimilarity(value, q);
|
|
194
|
+
}
|
|
195
|
+
} else if (Array.isArray(value)) {
|
|
196
|
+
for (const item of value) {
|
|
197
|
+
if (typeof item === "string") {
|
|
198
|
+
for (const q of queries) {
|
|
199
|
+
totalScore += calculateSimilarity(item, q);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return totalScore;
|
|
206
|
+
}
|
|
207
|
+
var isMain = process.argv[1]?.endsWith("fuzzy.js") || process.argv[1]?.endsWith("fuzzy.ts");
|
|
208
|
+
if (isMain && process.argv.length > 2) {
|
|
209
|
+
const args = process.argv.slice(2);
|
|
210
|
+
const queryIndex = args.indexOf("--query");
|
|
211
|
+
const query = queryIndex !== -1 ? args[queryIndex + 1] : "";
|
|
212
|
+
const mnemeDir = `${process.cwd()}/.mneme`;
|
|
213
|
+
if (!query) {
|
|
214
|
+
console.error(JSON.stringify({ success: false, error: "Missing --query" }));
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
search({ query, mnemeDir }).then((results) => {
|
|
218
|
+
console.log(JSON.stringify({ success: true, results }));
|
|
219
|
+
}).catch((error) => {
|
|
220
|
+
console.error(JSON.stringify({ success: false, error: String(error) }));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// lib/search/helpers.ts
|
|
225
|
+
function escapeRegex(value) {
|
|
226
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
227
|
+
}
|
|
228
|
+
function countMatches(text, pattern) {
|
|
229
|
+
const matches = text.match(new RegExp(pattern.source, "gi"));
|
|
230
|
+
return matches ? matches.length : 0;
|
|
231
|
+
}
|
|
232
|
+
function fieldScore(text, pattern, baseScore) {
|
|
233
|
+
if (!text) return 0;
|
|
234
|
+
const count = countMatches(text, pattern);
|
|
235
|
+
if (count === 0) return 0;
|
|
236
|
+
return baseScore + (count > 1 ? Math.log2(count) * 0.5 : 0);
|
|
237
|
+
}
|
|
238
|
+
function isFuzzyMatch(word, target, maxDistance = 2) {
|
|
239
|
+
if (word.length < 4) return false;
|
|
240
|
+
const distance = levenshtein(word.toLowerCase(), target.toLowerCase());
|
|
241
|
+
const threshold = Math.min(maxDistance, Math.floor(word.length / 3));
|
|
242
|
+
return distance <= threshold;
|
|
243
|
+
}
|
|
244
|
+
function loadTags(mnemeDir) {
|
|
245
|
+
const tagsPath = path3.join(mnemeDir, "tags.json");
|
|
246
|
+
if (!fs3.existsSync(tagsPath)) return null;
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(fs3.readFileSync(tagsPath, "utf-8"));
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function expandKeywordsWithAliases(keywords, tags) {
|
|
254
|
+
if (!tags) return keywords;
|
|
255
|
+
const expanded = new Set(keywords.map((k) => k.toLowerCase()));
|
|
256
|
+
for (const keyword of keywords) {
|
|
257
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
258
|
+
for (const tag of tags.tags) {
|
|
259
|
+
const matches = tag.id.toLowerCase() === lowerKeyword || tag.label.toLowerCase() === lowerKeyword || tag.aliases?.some((alias) => alias.toLowerCase() === lowerKeyword);
|
|
260
|
+
if (!matches) continue;
|
|
261
|
+
expanded.add(tag.id.toLowerCase());
|
|
262
|
+
expanded.add(tag.label.toLowerCase());
|
|
263
|
+
for (const alias of tag.aliases || []) {
|
|
264
|
+
expanded.add(alias.toLowerCase());
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return Array.from(expanded);
|
|
269
|
+
}
|
|
270
|
+
function walkJsonFiles(dir, callback) {
|
|
271
|
+
if (!fs3.existsSync(dir)) return;
|
|
272
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
const fullPath = path3.join(dir, entry.name);
|
|
275
|
+
if (entry.isDirectory()) {
|
|
276
|
+
walkJsonFiles(fullPath, callback);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
280
|
+
callback(fullPath);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// lib/search/approved-rules.ts
|
|
286
|
+
function isApproved(status) {
|
|
287
|
+
if (typeof status !== "string") return false;
|
|
288
|
+
const s = status.toLowerCase();
|
|
289
|
+
return s === "approved" || s === "active";
|
|
290
|
+
}
|
|
291
|
+
function priorityBoost(priority) {
|
|
292
|
+
if (priority === "p0") return 2;
|
|
293
|
+
if (priority === "p1") return 1;
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
function searchRuleFiles(mnemeDir, pattern) {
|
|
297
|
+
const results = [];
|
|
298
|
+
const rulesDir = path4.join(mnemeDir, "rules");
|
|
299
|
+
if (!fs4.existsSync(rulesDir)) return results;
|
|
300
|
+
for (const fileName of ["dev-rules.json", "review-guidelines.json"]) {
|
|
301
|
+
const filePath = path4.join(rulesDir, fileName);
|
|
302
|
+
if (!fs4.existsSync(filePath)) continue;
|
|
303
|
+
try {
|
|
304
|
+
const doc = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
305
|
+
const items = doc.items || doc.rules || [];
|
|
306
|
+
for (const item of items) {
|
|
307
|
+
if (!isApproved(item.status)) continue;
|
|
308
|
+
let score = 0;
|
|
309
|
+
const matched = [];
|
|
310
|
+
const textScore = fieldScore(item.text, pattern, 4);
|
|
311
|
+
if (textScore > 0) {
|
|
312
|
+
score += textScore;
|
|
313
|
+
matched.push("text");
|
|
314
|
+
}
|
|
315
|
+
const keyScore = fieldScore(item.key, pattern, 3);
|
|
316
|
+
if (keyScore > 0) {
|
|
317
|
+
score += keyScore;
|
|
318
|
+
matched.push("key");
|
|
319
|
+
}
|
|
320
|
+
if (item.tags?.some((t) => pattern.test(t))) {
|
|
321
|
+
score += 1;
|
|
322
|
+
matched.push("tags");
|
|
323
|
+
}
|
|
324
|
+
score += priorityBoost(item.priority);
|
|
325
|
+
if (score > 0) {
|
|
326
|
+
results.push({
|
|
327
|
+
sourceType: "rule",
|
|
328
|
+
id: item.id || item.key || "",
|
|
329
|
+
title: item.text || item.key || "",
|
|
330
|
+
text: item.text || "",
|
|
331
|
+
priority: item.priority,
|
|
332
|
+
tags: item.tags || [],
|
|
333
|
+
score,
|
|
334
|
+
matchedFields: matched
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
function searchDecisionFiles(mnemeDir, pattern) {
|
|
344
|
+
const results = [];
|
|
345
|
+
const decisionsDir = path4.join(mnemeDir, "decisions");
|
|
346
|
+
walkJsonFiles(decisionsDir, (filePath) => {
|
|
347
|
+
try {
|
|
348
|
+
const doc = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
349
|
+
if (!isApproved(doc.status)) return;
|
|
350
|
+
let score = 0;
|
|
351
|
+
const matched = [];
|
|
352
|
+
const titleScore = fieldScore(doc.title, pattern, 3);
|
|
353
|
+
if (titleScore > 0) {
|
|
354
|
+
score += titleScore;
|
|
355
|
+
matched.push("title");
|
|
356
|
+
}
|
|
357
|
+
const decisionScore = fieldScore(doc.decision, pattern, 4);
|
|
358
|
+
if (decisionScore > 0) {
|
|
359
|
+
score += decisionScore;
|
|
360
|
+
matched.push("decision");
|
|
361
|
+
}
|
|
362
|
+
const reasoningScore = fieldScore(doc.reasoning, pattern, 2);
|
|
363
|
+
if (reasoningScore > 0) {
|
|
364
|
+
score += reasoningScore;
|
|
365
|
+
matched.push("reasoning");
|
|
366
|
+
}
|
|
367
|
+
if (doc.tags?.some((t) => pattern.test(t))) {
|
|
368
|
+
score += 1;
|
|
369
|
+
matched.push("tags");
|
|
370
|
+
}
|
|
371
|
+
if (score > 0) {
|
|
372
|
+
results.push({
|
|
373
|
+
sourceType: "decision",
|
|
374
|
+
id: doc.id || "",
|
|
375
|
+
title: doc.title || "",
|
|
376
|
+
text: doc.decision || doc.title || "",
|
|
377
|
+
tags: doc.tags || [],
|
|
378
|
+
score,
|
|
379
|
+
matchedFields: matched
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
return results;
|
|
386
|
+
}
|
|
387
|
+
function searchPatternFiles(mnemeDir, pattern) {
|
|
388
|
+
const results = [];
|
|
389
|
+
const patternsDir = path4.join(mnemeDir, "patterns");
|
|
390
|
+
walkJsonFiles(patternsDir, (filePath) => {
|
|
391
|
+
try {
|
|
392
|
+
const doc = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
393
|
+
const items = doc.items || doc.patterns || [];
|
|
394
|
+
for (const item of items) {
|
|
395
|
+
if (!isApproved(item.status)) continue;
|
|
396
|
+
let score = 0;
|
|
397
|
+
const matched = [];
|
|
398
|
+
const titleScore = fieldScore(item.title, pattern, 3);
|
|
399
|
+
if (titleScore > 0) {
|
|
400
|
+
score += titleScore;
|
|
401
|
+
matched.push("title");
|
|
402
|
+
}
|
|
403
|
+
const patternScore = fieldScore(item.pattern, pattern, 3);
|
|
404
|
+
if (patternScore > 0) {
|
|
405
|
+
score += patternScore;
|
|
406
|
+
matched.push("pattern");
|
|
407
|
+
}
|
|
408
|
+
if (item.tags?.some((t) => pattern.test(t))) {
|
|
409
|
+
score += 1;
|
|
410
|
+
matched.push("tags");
|
|
411
|
+
}
|
|
412
|
+
if (score > 0) {
|
|
413
|
+
results.push({
|
|
414
|
+
sourceType: "pattern",
|
|
415
|
+
id: item.id || "",
|
|
416
|
+
title: item.title || "",
|
|
417
|
+
text: item.pattern || item.title || "",
|
|
418
|
+
tags: item.tags || [],
|
|
419
|
+
score,
|
|
420
|
+
matchedFields: matched
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
return results;
|
|
428
|
+
}
|
|
429
|
+
function searchApprovedRules(options) {
|
|
430
|
+
const { query, mnemeDir, limit = 5 } = options;
|
|
431
|
+
const keywords = query.toLowerCase().split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2);
|
|
432
|
+
if (keywords.length === 0) return [];
|
|
433
|
+
const expanded = expandKeywordsWithAliases(keywords, loadTags(mnemeDir));
|
|
434
|
+
const pattern = new RegExp(expanded.map(escapeRegex).join("|"), "i");
|
|
435
|
+
const results = [
|
|
436
|
+
...searchRuleFiles(mnemeDir, pattern),
|
|
437
|
+
...searchDecisionFiles(mnemeDir, pattern),
|
|
438
|
+
...searchPatternFiles(mnemeDir, pattern)
|
|
439
|
+
];
|
|
440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
441
|
+
return results.sort((a, b) => b.score - a.score).filter((r) => {
|
|
442
|
+
if (seen.has(r.id)) return false;
|
|
443
|
+
seen.add(r.id);
|
|
444
|
+
return true;
|
|
445
|
+
}).slice(0, limit);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// lib/search/core.ts
|
|
449
|
+
import * as fs5 from "node:fs";
|
|
450
|
+
import * as path5 from "node:path";
|
|
451
|
+
function searchInteractions(keywords, projectPath, database, limit = 5) {
|
|
452
|
+
if (!database) return [];
|
|
453
|
+
try {
|
|
454
|
+
const stmt = database.prepare(`
|
|
455
|
+
SELECT
|
|
456
|
+
i.session_id,
|
|
457
|
+
i.content,
|
|
458
|
+
i.timestamp,
|
|
459
|
+
highlight(interactions_fts, 0, '[', ']') as content_highlight
|
|
460
|
+
FROM interactions_fts
|
|
461
|
+
JOIN interactions i ON interactions_fts.rowid = i.id
|
|
462
|
+
WHERE interactions_fts MATCH ?
|
|
463
|
+
AND i.project_path = ?
|
|
464
|
+
ORDER BY rank
|
|
465
|
+
LIMIT ?
|
|
466
|
+
`);
|
|
467
|
+
const rows = stmt.all(keywords.join(" OR "), projectPath, limit);
|
|
468
|
+
return rows.map((row) => ({
|
|
469
|
+
type: "interaction",
|
|
470
|
+
id: row.session_id,
|
|
471
|
+
title: `Interaction from ${row.timestamp}`,
|
|
472
|
+
snippet: (row.content_highlight || row.content).substring(0, 150),
|
|
473
|
+
score: 5,
|
|
474
|
+
matchedFields: ["content"]
|
|
475
|
+
}));
|
|
476
|
+
} catch {
|
|
477
|
+
try {
|
|
478
|
+
const clauses = keywords.map(() => "(content LIKE ? OR thinking LIKE ?)");
|
|
479
|
+
const sql = `
|
|
480
|
+
SELECT DISTINCT session_id, substr(content, 1, 120) as snippet, timestamp
|
|
481
|
+
FROM interactions
|
|
482
|
+
WHERE project_path = ?
|
|
483
|
+
AND (${clauses.join(" OR ")})
|
|
484
|
+
ORDER BY timestamp DESC
|
|
485
|
+
LIMIT ?
|
|
486
|
+
`;
|
|
487
|
+
const args = [projectPath];
|
|
488
|
+
for (const keyword of keywords) {
|
|
489
|
+
const pattern = `%${keyword}%`;
|
|
490
|
+
args.push(pattern, pattern);
|
|
491
|
+
}
|
|
492
|
+
args.push(limit);
|
|
493
|
+
const stmt = database.prepare(sql);
|
|
494
|
+
const rows = stmt.all(...args);
|
|
495
|
+
return rows.map((row) => ({
|
|
496
|
+
type: "interaction",
|
|
497
|
+
id: row.session_id,
|
|
498
|
+
title: `Interaction from ${row.timestamp}`,
|
|
499
|
+
snippet: row.snippet,
|
|
500
|
+
score: 3,
|
|
501
|
+
matchedFields: ["content"]
|
|
502
|
+
}));
|
|
503
|
+
} catch {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function searchSessions(mnemeDir, keywords, limit = 5) {
|
|
509
|
+
const sessionsDir = path5.join(mnemeDir, "sessions");
|
|
510
|
+
const results = [];
|
|
511
|
+
const pattern = new RegExp(keywords.map(escapeRegex).join("|"), "i");
|
|
512
|
+
walkJsonFiles(sessionsDir, (filePath) => {
|
|
513
|
+
try {
|
|
514
|
+
const session = JSON.parse(
|
|
515
|
+
fs5.readFileSync(filePath, "utf-8")
|
|
516
|
+
);
|
|
517
|
+
const title = session.title || session.summary?.title || "";
|
|
518
|
+
let score = 0;
|
|
519
|
+
const matchedFields = [];
|
|
520
|
+
const titleScore = fieldScore(title, pattern, 3);
|
|
521
|
+
if (titleScore > 0) {
|
|
522
|
+
score += titleScore;
|
|
523
|
+
matchedFields.push("title");
|
|
524
|
+
}
|
|
525
|
+
if (session.tags?.some((t) => pattern.test(t))) {
|
|
526
|
+
score += 1;
|
|
527
|
+
matchedFields.push("tags");
|
|
528
|
+
}
|
|
529
|
+
const goalScore = fieldScore(session.summary?.goal, pattern, 2);
|
|
530
|
+
if (goalScore > 0) {
|
|
531
|
+
score += goalScore;
|
|
532
|
+
matchedFields.push("summary.goal");
|
|
533
|
+
}
|
|
534
|
+
const descScore = fieldScore(session.summary?.description, pattern, 2);
|
|
535
|
+
if (descScore > 0) {
|
|
536
|
+
score += descScore;
|
|
537
|
+
matchedFields.push("summary.description");
|
|
538
|
+
}
|
|
539
|
+
if (session.discussions?.some(
|
|
540
|
+
(d) => pattern.test(d.topic || "") || pattern.test(d.decision || "")
|
|
541
|
+
)) {
|
|
542
|
+
score += 2;
|
|
543
|
+
matchedFields.push("discussions");
|
|
544
|
+
}
|
|
545
|
+
if (session.errors?.some(
|
|
546
|
+
(e) => pattern.test(e.error || "") || pattern.test(e.solution || "")
|
|
547
|
+
)) {
|
|
548
|
+
score += 2;
|
|
549
|
+
matchedFields.push("errors");
|
|
550
|
+
}
|
|
551
|
+
if (score === 0 && keywords.length <= 2) {
|
|
552
|
+
const titleWords = (title || "").toLowerCase().split(/\s+/);
|
|
553
|
+
const tagWords = session.tags || [];
|
|
554
|
+
for (const keyword of keywords) {
|
|
555
|
+
if (titleWords.some((w) => isFuzzyMatch(keyword, w))) {
|
|
556
|
+
score += 1;
|
|
557
|
+
matchedFields.push("title~fuzzy");
|
|
558
|
+
}
|
|
559
|
+
if (tagWords.some((t) => isFuzzyMatch(keyword, t))) {
|
|
560
|
+
score += 0.5;
|
|
561
|
+
matchedFields.push("tags~fuzzy");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (score > 0) {
|
|
566
|
+
results.push({
|
|
567
|
+
type: "session",
|
|
568
|
+
id: session.id,
|
|
569
|
+
title: title || session.id,
|
|
570
|
+
snippet: session.summary?.description || session.summary?.goal || "",
|
|
571
|
+
score,
|
|
572
|
+
matchedFields
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
579
|
+
}
|
|
580
|
+
function searchKnowledge(options) {
|
|
581
|
+
const {
|
|
582
|
+
query,
|
|
583
|
+
mnemeDir,
|
|
584
|
+
projectPath,
|
|
585
|
+
database = null,
|
|
586
|
+
types = ["session", "interaction"],
|
|
587
|
+
limit = 10,
|
|
588
|
+
offset = 0
|
|
589
|
+
} = options;
|
|
590
|
+
const keywords = query.toLowerCase().split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 2);
|
|
591
|
+
if (keywords.length === 0) return [];
|
|
592
|
+
const expandedKeywords = expandKeywordsWithAliases(
|
|
593
|
+
keywords,
|
|
594
|
+
loadTags(mnemeDir)
|
|
595
|
+
);
|
|
596
|
+
const results = [];
|
|
597
|
+
const safeOffset = Math.max(0, offset);
|
|
598
|
+
const fetchLimit = Math.max(limit + safeOffset, limit, 10);
|
|
599
|
+
const normalizedTypes = new Set(types);
|
|
600
|
+
if (normalizedTypes.has("session")) {
|
|
601
|
+
results.push(...searchSessions(mnemeDir, expandedKeywords, fetchLimit));
|
|
602
|
+
}
|
|
603
|
+
if (normalizedTypes.has("interaction")) {
|
|
604
|
+
results.push(
|
|
605
|
+
...searchInteractions(
|
|
606
|
+
expandedKeywords,
|
|
607
|
+
projectPath,
|
|
608
|
+
database,
|
|
609
|
+
fetchLimit
|
|
610
|
+
)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const seen = /* @__PURE__ */ new Set();
|
|
614
|
+
return results.sort((a, b) => b.score - a.score).filter((result) => {
|
|
615
|
+
const key = `${result.type}:${result.id}`;
|
|
616
|
+
if (seen.has(key)) return false;
|
|
617
|
+
seen.add(key);
|
|
618
|
+
return true;
|
|
619
|
+
}).slice(safeOffset, safeOffset + limit);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// lib/search/prompt.ts
|
|
623
|
+
var originalEmit = process.emit;
|
|
624
|
+
process.emit = (event, ...args) => {
|
|
625
|
+
if (event === "warning" && typeof args[0] === "object" && args[0] !== null && "name" in args[0] && args[0].name === "ExperimentalWarning" && "message" in args[0] && typeof args[0].message === "string" && args[0].message.includes("SQLite")) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return originalEmit.apply(process, [event, ...args]);
|
|
629
|
+
};
|
|
630
|
+
var { DatabaseSync } = await import("node:sqlite");
|
|
631
|
+
function getArg(args, name) {
|
|
632
|
+
const index = args.indexOf(`--${name}`);
|
|
633
|
+
return index === -1 ? void 0 : args[index + 1];
|
|
634
|
+
}
|
|
635
|
+
function main() {
|
|
636
|
+
const args = process.argv.slice(2);
|
|
637
|
+
const query = getArg(args, "query");
|
|
638
|
+
const projectPath = getArg(args, "project");
|
|
639
|
+
const limit = Number.parseInt(getArg(args, "limit") || "5", 10);
|
|
640
|
+
if (!query || !projectPath) {
|
|
641
|
+
console.log(
|
|
642
|
+
JSON.stringify({ success: false, error: "Missing required args" })
|
|
643
|
+
);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
const mnemeDir = path6.join(projectPath, ".mneme");
|
|
647
|
+
const dbPath = path6.join(mnemeDir, "local.db");
|
|
648
|
+
let database = null;
|
|
649
|
+
try {
|
|
650
|
+
if (fs6.existsSync(dbPath)) {
|
|
651
|
+
database = new DatabaseSync(dbPath);
|
|
652
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
653
|
+
}
|
|
654
|
+
const results = searchKnowledge({
|
|
655
|
+
query,
|
|
656
|
+
mnemeDir,
|
|
657
|
+
projectPath,
|
|
658
|
+
database,
|
|
659
|
+
limit: Number.isFinite(limit) ? Math.max(1, Math.min(limit, 10)) : 5
|
|
660
|
+
});
|
|
661
|
+
const rules = searchApprovedRules({ query, mnemeDir, limit: 5 });
|
|
662
|
+
console.log(JSON.stringify({ success: true, results, rules }));
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.log(
|
|
665
|
+
JSON.stringify({ success: false, error: error.message })
|
|
666
|
+
);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
} finally {
|
|
669
|
+
database?.close();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
main();
|