@geoql/mdr 0.0.1
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/LICENSE +22 -0
- package/README.md +152 -0
- package/USAGE.md +59 -0
- package/bin/detect-user.sh +53 -0
- package/bin/index-conversations.ts +34 -0
- package/bin/macrodata-daemon.ts +28 -0
- package/bin/macrodata-hook.sh +277 -0
- package/dist/bin/index-conversations.js +31 -0
- package/dist/bin/macrodata-daemon.js +30 -0
- package/dist/opencode/context.js +210 -0
- package/dist/opencode/conversations.js +367 -0
- package/dist/opencode/index.js +155 -0
- package/dist/opencode/journal.js +108 -0
- package/dist/opencode/logger.js +29 -0
- package/dist/opencode/search.js +210 -0
- package/dist/opencode/skills/macrodata-distill/SKILL.md +171 -0
- package/dist/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
- package/dist/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
- package/dist/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
- package/dist/opencode/tools.js +367 -0
- package/dist/src/config.js +55 -0
- package/dist/src/conversations.js +513 -0
- package/dist/src/daemon.js +582 -0
- package/dist/src/detect-user.js +73 -0
- package/dist/src/embeddings.js +190 -0
- package/dist/src/index.js +413 -0
- package/dist/src/indexer.js +286 -0
- package/opencode/context.ts +322 -0
- package/opencode/conversations.ts +467 -0
- package/opencode/index.ts +208 -0
- package/opencode/journal.ts +153 -0
- package/opencode/logger.ts +32 -0
- package/opencode/search.ts +288 -0
- package/opencode/skills/macrodata-distill/SKILL.md +171 -0
- package/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
- package/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
- package/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
- package/opencode/tools.ts +453 -0
- package/package.json +87 -0
- package/src/config.ts +66 -0
- package/src/conversations.ts +709 -0
- package/src/daemon.ts +785 -0
- package/src/detect-user.ts +97 -0
- package/src/embeddings.ts +262 -0
- package/src/index.ts +726 -0
- package/src/indexer.ts +394 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { getIndexDir } from "./config.js";
|
|
2
|
+
import { embedBatch, embedQuery } from "./embeddings.js";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
4
|
+
import { basename, join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { LocalIndex } from "vectra";
|
|
7
|
+
|
|
8
|
+
//#region src/conversations.ts
|
|
9
|
+
/**
|
|
10
|
+
* Claude Conversation Log Parser and Indexer
|
|
11
|
+
*
|
|
12
|
+
* Indexes conversation "exchanges" from Claude Code's log files for semantic search.
|
|
13
|
+
* Each exchange = user prompt + assistant's first text response.
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Project-biased search (current project first, then global)
|
|
17
|
+
* - Time-weighted scoring (recent > old)
|
|
18
|
+
* - Metadata: project, branch, timestamp, session
|
|
19
|
+
*/
|
|
20
|
+
function getIndexStatePath() {
|
|
21
|
+
return join(getIndexDir(), "conversations-state.json");
|
|
22
|
+
}
|
|
23
|
+
function loadIndexState() {
|
|
24
|
+
const statePath = getIndexStatePath();
|
|
25
|
+
if (existsSync(statePath)) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(statePath, "utf-8"));
|
|
28
|
+
} catch {
|
|
29
|
+
console.warn("[Conversations] Index state corrupted, starting fresh");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
files: {},
|
|
34
|
+
lastUpdate: ""
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function saveIndexState(state) {
|
|
38
|
+
const statePath = getIndexStatePath();
|
|
39
|
+
const dir = getIndexDir();
|
|
40
|
+
if (!existsSync(dir)) {
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
44
|
+
}
|
|
45
|
+
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
46
|
+
const PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
47
|
+
let convIndex = null;
|
|
48
|
+
let convIndexPath = null;
|
|
49
|
+
async function getConversationIndex() {
|
|
50
|
+
const currentIndexDir = getIndexDir();
|
|
51
|
+
const currentIndexPath = join(currentIndexDir, "conversations");
|
|
52
|
+
if (convIndex && convIndexPath !== currentIndexPath) {
|
|
53
|
+
convIndex = null;
|
|
54
|
+
convIndexPath = null;
|
|
55
|
+
}
|
|
56
|
+
if (convIndex) return convIndex;
|
|
57
|
+
if (!existsSync(currentIndexDir)) {
|
|
58
|
+
const { mkdirSync } = await import("fs");
|
|
59
|
+
mkdirSync(currentIndexDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
convIndex = new LocalIndex(currentIndexPath);
|
|
62
|
+
convIndexPath = currentIndexPath;
|
|
63
|
+
if (!await convIndex.isIndexCreated()) {
|
|
64
|
+
console.log("[Conversations] Creating new conversation index...");
|
|
65
|
+
await convIndex.createIndex();
|
|
66
|
+
}
|
|
67
|
+
return convIndex;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Decode project directory name back to path
|
|
71
|
+
* e.g., "-Users-alex-Repos-my-project" -> "/Users/alex/Repos/my-project"
|
|
72
|
+
*/
|
|
73
|
+
function decodeProjectPath(encoded) {
|
|
74
|
+
return encoded.replace(/^-/, "/").replace(/-/g, "/");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract project name from path
|
|
78
|
+
*/
|
|
79
|
+
function getProjectName(projectPath) {
|
|
80
|
+
return basename(projectPath);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extract first text content from assistant message
|
|
84
|
+
*/
|
|
85
|
+
function extractAssistantText(content) {
|
|
86
|
+
if (typeof content === "string") {
|
|
87
|
+
return content.slice(0, 500);
|
|
88
|
+
}
|
|
89
|
+
for (const block of content) {
|
|
90
|
+
if (block.type === "text" && block.text) {
|
|
91
|
+
return block.text.slice(0, 500);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if user message content is a tool result (not an actual user prompt)
|
|
98
|
+
*/
|
|
99
|
+
function isToolResult(content) {
|
|
100
|
+
if (Array.isArray(content)) {
|
|
101
|
+
return content.some((item) => item.type === "tool_result" || item.tool_use_id !== undefined);
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if content is noise we should skip indexing
|
|
107
|
+
*/
|
|
108
|
+
function isNoiseContent(content) {
|
|
109
|
+
if (content.startsWith("This session is being continued from a previous conversation")) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (content.includes("<local-command-stdout>") || content.includes("<local-command-caveat>") || content.includes("<command-name>")) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (content.startsWith("<current_time>") || content.startsWith("<context_status>") || content.startsWith("<state_files>") || content.startsWith("<system-reminder>") || content.startsWith("## Current State Files") || content.startsWith("Base directory for this skill:")) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (content.trim().length < 1) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Extract actual user text from message content, filtering out tool results
|
|
125
|
+
*/
|
|
126
|
+
function extractUserText(content) {
|
|
127
|
+
let text = "";
|
|
128
|
+
if (typeof content === "string") {
|
|
129
|
+
text = content;
|
|
130
|
+
} else {
|
|
131
|
+
for (const block of content) {
|
|
132
|
+
const b = block;
|
|
133
|
+
/* v8 ignore next 3 -- defensive: callers pre-filter tool-result arrays via
|
|
134
|
+
isToolResult(), so this redundant guard is never the deciding skip. */
|
|
135
|
+
if (b.type === "tool_result" || b.tool_use_id !== undefined) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
139
|
+
text = b.text;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!text) return "";
|
|
145
|
+
if (text.startsWith("# Agent Context") || text.includes("\nUser message: ")) {
|
|
146
|
+
const userMsgMatch = text.match(/\nUser message: (.+)$/s);
|
|
147
|
+
if (userMsgMatch) {
|
|
148
|
+
return userMsgMatch[1].trim();
|
|
149
|
+
}
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
return text;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Parse a conversation file and extract exchanges
|
|
156
|
+
*/
|
|
157
|
+
function parseConversationFile(filePath, projectPath) {
|
|
158
|
+
const exchanges = [];
|
|
159
|
+
const projectName = getProjectName(projectPath);
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(filePath, "utf-8");
|
|
162
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
163
|
+
let currentUser = null;
|
|
164
|
+
let malformedLines = 0;
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
try {
|
|
167
|
+
const msg = JSON.parse(line);
|
|
168
|
+
if (msg.type === "user" && msg.message?.content) {
|
|
169
|
+
if (isToolResult(msg.message.content)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const userText = extractUserText(msg.message.content);
|
|
173
|
+
if (!userText || isNoiseContent(userText)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
currentUser = {
|
|
177
|
+
msg,
|
|
178
|
+
text: userText
|
|
179
|
+
};
|
|
180
|
+
} else if (msg.type === "assistant" && currentUser && msg.message?.content) {
|
|
181
|
+
const assistantText = extractAssistantText(msg.message.content);
|
|
182
|
+
exchanges.push({
|
|
183
|
+
id: `conv-${currentUser.msg.sessionId}-${currentUser.msg.uuid}`,
|
|
184
|
+
userPrompt: currentUser.text.slice(0, 1e3),
|
|
185
|
+
assistantSummary: assistantText,
|
|
186
|
+
project: projectName,
|
|
187
|
+
projectPath,
|
|
188
|
+
branch: currentUser.msg.gitBranch,
|
|
189
|
+
timestamp: currentUser.msg.timestamp || new Date().toISOString(),
|
|
190
|
+
sessionId: currentUser.msg.sessionId || basename(filePath, ".jsonl"),
|
|
191
|
+
sessionPath: filePath,
|
|
192
|
+
messageUuid: currentUser.msg.uuid || ""
|
|
193
|
+
});
|
|
194
|
+
currentUser = null;
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
malformedLines++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (malformedLines > 0) {
|
|
201
|
+
console.warn(`[Conversations] Skipped ${malformedLines} malformed lines in ${filePath}`);
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error(`[Conversations] Failed to parse ${filePath}: ${String(err)}`);
|
|
205
|
+
}
|
|
206
|
+
return exchanges;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Scan all Claude project directories for conversation files
|
|
210
|
+
*/
|
|
211
|
+
function* scanConversationFiles() {
|
|
212
|
+
if (!existsSync(PROJECTS_DIR)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const projectDirs = readdirSync(PROJECTS_DIR);
|
|
216
|
+
for (const projectDir of projectDirs) {
|
|
217
|
+
if (projectDir.startsWith(".")) continue;
|
|
218
|
+
const projectPath = decodeProjectPath(projectDir);
|
|
219
|
+
const projectFullPath = join(PROJECTS_DIR, projectDir);
|
|
220
|
+
if (!statSync(projectFullPath).isDirectory()) continue;
|
|
221
|
+
const files = readdirSync(projectFullPath);
|
|
222
|
+
for (const file of files) {
|
|
223
|
+
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
224
|
+
const filePath = join(projectFullPath, file);
|
|
225
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
226
|
+
yield {
|
|
227
|
+
filePath,
|
|
228
|
+
projectPath,
|
|
229
|
+
mtime
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Rebuild the conversation index from scratch
|
|
236
|
+
*/
|
|
237
|
+
async function rebuildConversationIndex() {
|
|
238
|
+
console.log("[Conversations] Starting full index rebuild...");
|
|
239
|
+
const startTime = Date.now();
|
|
240
|
+
const allExchanges = [];
|
|
241
|
+
const newState = {
|
|
242
|
+
files: {},
|
|
243
|
+
lastUpdate: new Date().toISOString()
|
|
244
|
+
};
|
|
245
|
+
for (const { filePath, projectPath, mtime } of scanConversationFiles()) {
|
|
246
|
+
const exchanges = parseConversationFile(filePath, projectPath);
|
|
247
|
+
allExchanges.push(...exchanges);
|
|
248
|
+
newState.files[filePath] = {
|
|
249
|
+
mtime,
|
|
250
|
+
exchangeIds: exchanges.map((e) => e.id)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
console.log(`[Conversations] Found ${allExchanges.length} exchanges`);
|
|
254
|
+
if (allExchanges.length === 0) {
|
|
255
|
+
saveIndexState(newState);
|
|
256
|
+
return { exchangeCount: 0 };
|
|
257
|
+
}
|
|
258
|
+
const texts = allExchanges.map((e) => `${e.project}${e.branch ? ` (${e.branch})` : ""}: ${e.userPrompt}`);
|
|
259
|
+
console.log(`[Conversations] Generating embeddings...`);
|
|
260
|
+
const vectors = await embedBatch(texts);
|
|
261
|
+
const idx = await getConversationIndex();
|
|
262
|
+
await idx.beginUpdate();
|
|
263
|
+
try {
|
|
264
|
+
for (let i = 0; i < allExchanges.length; i++) {
|
|
265
|
+
const exchange = allExchanges[i];
|
|
266
|
+
await idx.upsertItem({
|
|
267
|
+
id: exchange.id,
|
|
268
|
+
vector: vectors[i],
|
|
269
|
+
metadata: {
|
|
270
|
+
userPrompt: exchange.userPrompt,
|
|
271
|
+
assistantSummary: exchange.assistantSummary,
|
|
272
|
+
project: exchange.project,
|
|
273
|
+
projectPath: exchange.projectPath,
|
|
274
|
+
branch: exchange.branch || "",
|
|
275
|
+
timestamp: exchange.timestamp,
|
|
276
|
+
sessionId: exchange.sessionId,
|
|
277
|
+
sessionPath: exchange.sessionPath,
|
|
278
|
+
messageUuid: exchange.messageUuid
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
await idx.endUpdate();
|
|
283
|
+
} catch (err) {
|
|
284
|
+
idx.cancelUpdate();
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
saveIndexState(newState);
|
|
288
|
+
const duration = Date.now() - startTime;
|
|
289
|
+
console.log(`[Conversations] Full rebuild complete in ${duration}ms`);
|
|
290
|
+
return { exchangeCount: allExchanges.length };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Incrementally update the conversation index (only changed files)
|
|
294
|
+
*/
|
|
295
|
+
async function updateConversationIndex() {
|
|
296
|
+
console.log("[Conversations] Starting incremental update...");
|
|
297
|
+
const startTime = Date.now();
|
|
298
|
+
const state = loadIndexState();
|
|
299
|
+
const idx = await getConversationIndex();
|
|
300
|
+
/* v8 ignore next 5 -- defensive: getConversationIndex() above always creates
|
|
301
|
+
the index, so isIndexCreated() is never false here; this full-rebuild
|
|
302
|
+
fallback only guards a future change to that contract. */
|
|
303
|
+
if (!await idx.isIndexCreated()) {
|
|
304
|
+
console.log("[Conversations] No existing index, doing full rebuild");
|
|
305
|
+
const result = await rebuildConversationIndex();
|
|
306
|
+
return {
|
|
307
|
+
exchangeCount: result.exchangeCount,
|
|
308
|
+
filesUpdated: 0,
|
|
309
|
+
skipped: 0
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
let filesUpdated = 0;
|
|
313
|
+
let skipped = 0;
|
|
314
|
+
let totalExchanges = 0;
|
|
315
|
+
const currentFiles = new Set();
|
|
316
|
+
for (const { filePath, projectPath, mtime } of scanConversationFiles()) {
|
|
317
|
+
currentFiles.add(filePath);
|
|
318
|
+
const cached = state.files[filePath];
|
|
319
|
+
if (cached && cached.mtime === mtime) {
|
|
320
|
+
skipped++;
|
|
321
|
+
totalExchanges += cached.exchangeIds.length;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const exchanges = parseConversationFile(filePath, projectPath);
|
|
325
|
+
if (exchanges.length > 0) {
|
|
326
|
+
const texts = exchanges.map((e) => `${e.project}${e.branch ? ` (${e.branch})` : ""}: ${e.userPrompt}`);
|
|
327
|
+
const vectors = await embedBatch(texts);
|
|
328
|
+
await idx.beginUpdate();
|
|
329
|
+
try {
|
|
330
|
+
for (let i = 0; i < exchanges.length; i++) {
|
|
331
|
+
const exchange = exchanges[i];
|
|
332
|
+
await idx.upsertItem({
|
|
333
|
+
id: exchange.id,
|
|
334
|
+
vector: vectors[i],
|
|
335
|
+
metadata: {
|
|
336
|
+
userPrompt: exchange.userPrompt,
|
|
337
|
+
assistantSummary: exchange.assistantSummary,
|
|
338
|
+
project: exchange.project,
|
|
339
|
+
projectPath: exchange.projectPath,
|
|
340
|
+
branch: exchange.branch || "",
|
|
341
|
+
timestamp: exchange.timestamp,
|
|
342
|
+
sessionId: exchange.sessionId,
|
|
343
|
+
sessionPath: exchange.sessionPath,
|
|
344
|
+
messageUuid: exchange.messageUuid
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
await idx.endUpdate();
|
|
349
|
+
} catch (err) {
|
|
350
|
+
idx.cancelUpdate();
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
state.files[filePath] = {
|
|
355
|
+
mtime,
|
|
356
|
+
exchangeIds: exchanges.map((e) => e.id)
|
|
357
|
+
};
|
|
358
|
+
filesUpdated++;
|
|
359
|
+
totalExchanges += exchanges.length;
|
|
360
|
+
}
|
|
361
|
+
for (const filePath of Object.keys(state.files)) {
|
|
362
|
+
if (!currentFiles.has(filePath)) {
|
|
363
|
+
delete state.files[filePath];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
state.lastUpdate = new Date().toISOString();
|
|
367
|
+
saveIndexState(state);
|
|
368
|
+
const duration = Date.now() - startTime;
|
|
369
|
+
console.log(`[Conversations] Incremental update complete in ${duration}ms (${filesUpdated} files updated, ${skipped} skipped)`);
|
|
370
|
+
return {
|
|
371
|
+
exchangeCount: totalExchanges,
|
|
372
|
+
filesUpdated,
|
|
373
|
+
skipped
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Calculate time-based weight for scoring
|
|
378
|
+
* Recent = higher weight
|
|
379
|
+
*/
|
|
380
|
+
function getTimeWeight(timestamp) {
|
|
381
|
+
const age = Date.now() - new Date(timestamp).getTime();
|
|
382
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
383
|
+
if (age < 7 * dayMs) return 1;
|
|
384
|
+
if (age < 30 * dayMs) return .9;
|
|
385
|
+
if (age < 90 * dayMs) return .7;
|
|
386
|
+
if (age < 365 * dayMs) return .5;
|
|
387
|
+
return .3;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Search conversations with project bias and time weighting
|
|
391
|
+
*/
|
|
392
|
+
async function searchConversations(query, options = {}) {
|
|
393
|
+
const { currentProject, limit = 5, projectOnly = false } = options;
|
|
394
|
+
const idx = await getConversationIndex();
|
|
395
|
+
const stats = await idx.listItems();
|
|
396
|
+
if (stats.length === 0) {
|
|
397
|
+
console.log("[Conversations] Index is empty");
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
const queryVector = await embedQuery(query);
|
|
401
|
+
const results = await idx.queryItems(queryVector, query, limit * 3);
|
|
402
|
+
const searchResults = results.map((r) => {
|
|
403
|
+
const meta = r.item.metadata;
|
|
404
|
+
const exchange = {
|
|
405
|
+
id: r.item.id,
|
|
406
|
+
userPrompt: meta.userPrompt,
|
|
407
|
+
assistantSummary: meta.assistantSummary,
|
|
408
|
+
project: meta.project,
|
|
409
|
+
projectPath: meta.projectPath,
|
|
410
|
+
branch: meta.branch || undefined,
|
|
411
|
+
timestamp: meta.timestamp,
|
|
412
|
+
sessionId: meta.sessionId,
|
|
413
|
+
sessionPath: meta.sessionPath,
|
|
414
|
+
messageUuid: meta.messageUuid
|
|
415
|
+
};
|
|
416
|
+
let adjustedScore = r.score;
|
|
417
|
+
adjustedScore *= getTimeWeight(exchange.timestamp);
|
|
418
|
+
if (currentProject && exchange.projectPath === currentProject) {
|
|
419
|
+
adjustedScore *= 1.5;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
exchange,
|
|
423
|
+
score: r.score,
|
|
424
|
+
adjustedScore
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
let filtered = searchResults;
|
|
428
|
+
if (projectOnly && currentProject) {
|
|
429
|
+
filtered = searchResults.filter((r) => r.exchange.projectPath === currentProject);
|
|
430
|
+
}
|
|
431
|
+
return filtered.sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, limit);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Load full conversation context around a specific message
|
|
435
|
+
*/
|
|
436
|
+
async function expandConversation(sessionPath, messageUuid, contextMessages = 10) {
|
|
437
|
+
if (!existsSync(sessionPath)) {
|
|
438
|
+
throw new Error(`Session file not found: ${sessionPath}`);
|
|
439
|
+
}
|
|
440
|
+
const content = readFileSync(sessionPath, "utf-8");
|
|
441
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
442
|
+
const messages = [];
|
|
443
|
+
let project = "";
|
|
444
|
+
let branch;
|
|
445
|
+
let malformedLines = 0;
|
|
446
|
+
for (const line of lines) {
|
|
447
|
+
try {
|
|
448
|
+
const msg = JSON.parse(line);
|
|
449
|
+
if (msg.type === "user" && msg.message?.content) {
|
|
450
|
+
if (isToolResult(msg.message.content)) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const text = extractUserText(msg.message.content);
|
|
454
|
+
if (!text || isNoiseContent(text)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
messages.push({
|
|
458
|
+
role: "user",
|
|
459
|
+
content: text,
|
|
460
|
+
timestamp: msg.timestamp,
|
|
461
|
+
uuid: msg.uuid
|
|
462
|
+
});
|
|
463
|
+
if (!project && msg.cwd) {
|
|
464
|
+
project = getProjectName(msg.cwd);
|
|
465
|
+
}
|
|
466
|
+
if (!branch && msg.gitBranch) {
|
|
467
|
+
branch = msg.gitBranch;
|
|
468
|
+
}
|
|
469
|
+
} else if (msg.type === "assistant" && msg.message?.content) {
|
|
470
|
+
const text = extractAssistantText(msg.message.content);
|
|
471
|
+
if (text) {
|
|
472
|
+
messages.push({
|
|
473
|
+
role: "assistant",
|
|
474
|
+
content: text,
|
|
475
|
+
timestamp: msg.timestamp,
|
|
476
|
+
uuid: msg.uuid
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
malformedLines++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (malformedLines > 0) {
|
|
485
|
+
console.warn(`[Conversations] Skipped ${malformedLines} malformed lines in ${sessionPath}`);
|
|
486
|
+
}
|
|
487
|
+
const targetIdx = messages.findIndex((m) => m.uuid === messageUuid);
|
|
488
|
+
if (targetIdx === -1) {
|
|
489
|
+
return {
|
|
490
|
+
messages: messages.slice(-contextMessages).map(({ uuid: _uuid, ...rest }) => rest),
|
|
491
|
+
project,
|
|
492
|
+
branch
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const start = Math.max(0, targetIdx - Math.floor(contextMessages / 2));
|
|
496
|
+
const end = Math.min(messages.length, start + contextMessages);
|
|
497
|
+
return {
|
|
498
|
+
messages: messages.slice(start, end).map(({ uuid: _uuid, ...rest }) => rest),
|
|
499
|
+
project,
|
|
500
|
+
branch
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get conversation index stats
|
|
505
|
+
*/
|
|
506
|
+
async function getConversationIndexStats() {
|
|
507
|
+
const idx = await getConversationIndex();
|
|
508
|
+
const items = await idx.listItems();
|
|
509
|
+
return { exchangeCount: items.length };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
export { expandConversation, getConversationIndexStats, rebuildConversationIndex, searchConversations, updateConversationIndex };
|