@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
package/src/indexer.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Indexer
|
|
3
|
+
*
|
|
4
|
+
* Manages the vector index for semantic search over:
|
|
5
|
+
* - Journal entries
|
|
6
|
+
* - People files
|
|
7
|
+
* - Project files
|
|
8
|
+
*
|
|
9
|
+
* Uses Vectra for storage and embeddings.ts for vector generation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LocalIndex } from "vectra";
|
|
13
|
+
import { join, basename } from "path";
|
|
14
|
+
import { readFileSync, readdirSync, existsSync, mkdirSync } from "fs";
|
|
15
|
+
import { embed, embedBatch, embedQuery, preloadModel as preloadEmbeddings } from "./embeddings.js";
|
|
16
|
+
import { getIndexDir, getEntitiesDir, getJournalDir } from "./config.js";
|
|
17
|
+
|
|
18
|
+
// Item types for filtering
|
|
19
|
+
export type MemoryItemType = "journal" | "person" | "project";
|
|
20
|
+
|
|
21
|
+
export interface MemoryItem {
|
|
22
|
+
id: string;
|
|
23
|
+
type: MemoryItemType;
|
|
24
|
+
content: string;
|
|
25
|
+
source: string;
|
|
26
|
+
section?: string;
|
|
27
|
+
timestamp?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SearchResult {
|
|
31
|
+
content: string;
|
|
32
|
+
source: string;
|
|
33
|
+
section?: string;
|
|
34
|
+
timestamp?: string;
|
|
35
|
+
type: MemoryItemType;
|
|
36
|
+
score: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cached index instance with path tracking
|
|
40
|
+
let index: LocalIndex | null = null;
|
|
41
|
+
let indexPath: string | null = null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get or create the vector index
|
|
45
|
+
* Re-creates if the configured path has changed
|
|
46
|
+
*/
|
|
47
|
+
async function getIndex(): Promise<LocalIndex> {
|
|
48
|
+
const currentIndexDir = getIndexDir();
|
|
49
|
+
const currentIndexPath = join(currentIndexDir, "vectors");
|
|
50
|
+
|
|
51
|
+
// Invalidate cache if path changed
|
|
52
|
+
if (index && indexPath !== currentIndexPath) {
|
|
53
|
+
index = null;
|
|
54
|
+
indexPath = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (index) return index;
|
|
58
|
+
|
|
59
|
+
// Ensure index directory exists
|
|
60
|
+
if (!existsSync(currentIndexDir)) {
|
|
61
|
+
mkdirSync(currentIndexDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
index = new LocalIndex(currentIndexPath);
|
|
65
|
+
indexPath = currentIndexPath;
|
|
66
|
+
|
|
67
|
+
// Create if doesn't exist
|
|
68
|
+
if (!(await index.isIndexCreated())) {
|
|
69
|
+
console.log("[Indexer] Creating new index...");
|
|
70
|
+
await index.createIndex();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return index;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add or update a single item in the index
|
|
78
|
+
*/
|
|
79
|
+
export async function indexItem(item: MemoryItem): Promise<void> {
|
|
80
|
+
const idx = await getIndex();
|
|
81
|
+
const vector = await embed(item.content);
|
|
82
|
+
|
|
83
|
+
const metadata: Record<string, string | number | boolean> = {
|
|
84
|
+
type: item.type,
|
|
85
|
+
content: item.content,
|
|
86
|
+
source: item.source,
|
|
87
|
+
};
|
|
88
|
+
if (item.section) metadata.section = item.section;
|
|
89
|
+
if (item.timestamp) metadata.timestamp = item.timestamp;
|
|
90
|
+
|
|
91
|
+
await idx.upsertItem({
|
|
92
|
+
id: item.id,
|
|
93
|
+
vector,
|
|
94
|
+
metadata,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Add or update multiple items (batched for efficiency)
|
|
100
|
+
*/
|
|
101
|
+
export async function indexItems(items: MemoryItem[]): Promise<void> {
|
|
102
|
+
if (items.length === 0) return;
|
|
103
|
+
|
|
104
|
+
const idx = await getIndex();
|
|
105
|
+
const vectors = await embedBatch(items.map((i) => i.content));
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < items.length; i++) {
|
|
108
|
+
const item = items[i];
|
|
109
|
+
const metadata: Record<string, string | number | boolean> = {
|
|
110
|
+
type: item.type,
|
|
111
|
+
content: item.content,
|
|
112
|
+
source: item.source,
|
|
113
|
+
};
|
|
114
|
+
if (item.section) metadata.section = item.section;
|
|
115
|
+
if (item.timestamp) metadata.timestamp = item.timestamp;
|
|
116
|
+
|
|
117
|
+
await idx.upsertItem({
|
|
118
|
+
id: item.id,
|
|
119
|
+
vector: vectors[i],
|
|
120
|
+
metadata,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Search the index
|
|
127
|
+
*/
|
|
128
|
+
export async function searchMemory(
|
|
129
|
+
query: string,
|
|
130
|
+
options: {
|
|
131
|
+
limit?: number;
|
|
132
|
+
type?: MemoryItemType;
|
|
133
|
+
since?: string;
|
|
134
|
+
} = {},
|
|
135
|
+
): Promise<SearchResult[]> {
|
|
136
|
+
const { limit = 5, type, since } = options;
|
|
137
|
+
const idx = await getIndex();
|
|
138
|
+
|
|
139
|
+
// Check if index has items
|
|
140
|
+
const stats = await idx.listItems();
|
|
141
|
+
if (stats.length === 0) {
|
|
142
|
+
console.log("[Indexer] Index is empty");
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const queryVector = await embedQuery(query);
|
|
147
|
+
const results = await idx.queryItems(queryVector, query, limit * 2);
|
|
148
|
+
|
|
149
|
+
// Filter results if type or since specified
|
|
150
|
+
let filtered = results;
|
|
151
|
+
if (type || since) {
|
|
152
|
+
filtered = results.filter((item) => {
|
|
153
|
+
const meta = item.item.metadata as Record<string, unknown>;
|
|
154
|
+
if (type && meta.type !== type) return false;
|
|
155
|
+
if (since && meta.timestamp && (meta.timestamp as string) < since) return false;
|
|
156
|
+
return true;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return filtered.slice(0, limit).map((r) => {
|
|
161
|
+
const meta = r.item.metadata as Record<string, unknown>;
|
|
162
|
+
return {
|
|
163
|
+
content: meta.content as string,
|
|
164
|
+
source: meta.source as string,
|
|
165
|
+
section: meta.section as string | undefined,
|
|
166
|
+
timestamp: meta.timestamp as string | undefined,
|
|
167
|
+
type: meta.type as MemoryItemType,
|
|
168
|
+
score: r.score,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse journal files and return items for indexing
|
|
175
|
+
*/
|
|
176
|
+
function parseJournalForIndexing(): MemoryItem[] {
|
|
177
|
+
const items: MemoryItem[] = [];
|
|
178
|
+
const journalDir = getJournalDir();
|
|
179
|
+
|
|
180
|
+
if (!existsSync(journalDir)) return items;
|
|
181
|
+
|
|
182
|
+
const files = readdirSync(journalDir).filter((f) => f.endsWith(".jsonl"));
|
|
183
|
+
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
try {
|
|
186
|
+
const content = readFileSync(join(journalDir, file), "utf-8");
|
|
187
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
try {
|
|
191
|
+
const entry = JSON.parse(lines[i]);
|
|
192
|
+
items.push({
|
|
193
|
+
id: `journal-${file}-${i}`,
|
|
194
|
+
type: "journal",
|
|
195
|
+
content: `[${entry.topic}] ${entry.content}`,
|
|
196
|
+
source: file,
|
|
197
|
+
timestamp: entry.timestamp,
|
|
198
|
+
});
|
|
199
|
+
} catch {
|
|
200
|
+
// Skip malformed lines
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// Skip unreadable files
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return items;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse entity files (people, projects) for indexing
|
|
213
|
+
*/
|
|
214
|
+
function parseEntitiesForIndexing(
|
|
215
|
+
subdir: "people" | "projects",
|
|
216
|
+
type: MemoryItemType,
|
|
217
|
+
): MemoryItem[] {
|
|
218
|
+
const items: MemoryItem[] = [];
|
|
219
|
+
const dir = join(getEntitiesDir(), subdir);
|
|
220
|
+
|
|
221
|
+
if (!existsSync(dir)) return items;
|
|
222
|
+
|
|
223
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
224
|
+
|
|
225
|
+
for (const file of files) {
|
|
226
|
+
try {
|
|
227
|
+
const content = readFileSync(join(dir, file), "utf-8");
|
|
228
|
+
const filename = file.replace(".md", "");
|
|
229
|
+
|
|
230
|
+
// Split by ## headers for section-level indexing
|
|
231
|
+
const sections = content.split(/^## /m);
|
|
232
|
+
|
|
233
|
+
// Preamble (before any ##)
|
|
234
|
+
if (sections[0].trim()) {
|
|
235
|
+
items.push({
|
|
236
|
+
id: `${type}-${filename}-preamble`,
|
|
237
|
+
type,
|
|
238
|
+
content: sections[0].trim(),
|
|
239
|
+
source: `${subdir}/${file}`,
|
|
240
|
+
section: "preamble",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Each section
|
|
245
|
+
for (let i = 1; i < sections.length; i++) {
|
|
246
|
+
const section = sections[i];
|
|
247
|
+
const firstLine = section.split("\n")[0];
|
|
248
|
+
const sectionTitle = firstLine.trim();
|
|
249
|
+
const sectionContent = section.slice(firstLine.length).trim();
|
|
250
|
+
|
|
251
|
+
if (sectionContent) {
|
|
252
|
+
items.push({
|
|
253
|
+
id: `${type}-${filename}-${i}`,
|
|
254
|
+
type,
|
|
255
|
+
content: `## ${sectionTitle}\n\n${sectionContent}`,
|
|
256
|
+
source: `${subdir}/${file}`,
|
|
257
|
+
section: sectionTitle,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Skip unreadable files
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return items;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Rebuild the entire index from scratch
|
|
271
|
+
*/
|
|
272
|
+
export async function rebuildIndex(): Promise<{ itemCount: number }> {
|
|
273
|
+
console.log("[Indexer] Starting full index rebuild...");
|
|
274
|
+
const startTime = Date.now();
|
|
275
|
+
|
|
276
|
+
const allItems: MemoryItem[] = [];
|
|
277
|
+
|
|
278
|
+
// 1. Index journal entries
|
|
279
|
+
console.log("[Indexer] Parsing journal...");
|
|
280
|
+
allItems.push(...parseJournalForIndexing());
|
|
281
|
+
|
|
282
|
+
// 2. Index people
|
|
283
|
+
console.log("[Indexer] Parsing people...");
|
|
284
|
+
allItems.push(...parseEntitiesForIndexing("people", "person"));
|
|
285
|
+
|
|
286
|
+
// 3. Index projects
|
|
287
|
+
console.log("[Indexer] Parsing projects...");
|
|
288
|
+
allItems.push(...parseEntitiesForIndexing("projects", "project"));
|
|
289
|
+
|
|
290
|
+
// Index all items
|
|
291
|
+
console.log(`[Indexer] Indexing ${allItems.length} items...`);
|
|
292
|
+
await indexItems(allItems);
|
|
293
|
+
|
|
294
|
+
const duration = Date.now() - startTime;
|
|
295
|
+
console.log(`[Indexer] Index rebuild complete in ${duration}ms`);
|
|
296
|
+
|
|
297
|
+
return { itemCount: allItems.length };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Index a single journal entry (for incremental updates)
|
|
302
|
+
*/
|
|
303
|
+
export async function indexJournalEntry(entry: {
|
|
304
|
+
timestamp: string;
|
|
305
|
+
topic: string;
|
|
306
|
+
content: string;
|
|
307
|
+
}): Promise<void> {
|
|
308
|
+
const item: MemoryItem = {
|
|
309
|
+
id: `journal-${entry.timestamp}`,
|
|
310
|
+
type: "journal",
|
|
311
|
+
content: `[${entry.topic}] ${entry.content}`,
|
|
312
|
+
source: "journal",
|
|
313
|
+
timestamp: entry.timestamp,
|
|
314
|
+
};
|
|
315
|
+
await indexItem(item);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get index stats
|
|
320
|
+
*/
|
|
321
|
+
export async function getIndexStats(): Promise<{ itemCount: number }> {
|
|
322
|
+
const idx = await getIndex();
|
|
323
|
+
const items = await idx.listItems();
|
|
324
|
+
return { itemCount: items.length };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Index a single entity file (person or project)
|
|
329
|
+
* Called by daemon when files change
|
|
330
|
+
*/
|
|
331
|
+
export async function indexEntityFile(filePath: string): Promise<void> {
|
|
332
|
+
const filename = basename(filePath, ".md");
|
|
333
|
+
|
|
334
|
+
// Determine type from path
|
|
335
|
+
let type: MemoryItemType;
|
|
336
|
+
if (filePath.includes("/people/")) {
|
|
337
|
+
type = "person";
|
|
338
|
+
} else if (filePath.includes("/projects/")) {
|
|
339
|
+
type = "project";
|
|
340
|
+
} else {
|
|
341
|
+
console.error(`[Indexer] Unknown entity type for: ${filePath}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const content = readFileSync(filePath, "utf-8");
|
|
347
|
+
const items: MemoryItem[] = [];
|
|
348
|
+
const subdir = type === "person" ? "people" : "projects";
|
|
349
|
+
|
|
350
|
+
// Split by ## headers for section-level indexing
|
|
351
|
+
const sections = content.split(/^## /m);
|
|
352
|
+
|
|
353
|
+
// Preamble (before any ##)
|
|
354
|
+
if (sections[0].trim()) {
|
|
355
|
+
items.push({
|
|
356
|
+
id: `${type}-${filename}-preamble`,
|
|
357
|
+
type,
|
|
358
|
+
content: sections[0].trim(),
|
|
359
|
+
source: `${subdir}/${basename(filePath)}`,
|
|
360
|
+
section: "preamble",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Each section
|
|
365
|
+
for (let i = 1; i < sections.length; i++) {
|
|
366
|
+
const section = sections[i];
|
|
367
|
+
const firstLine = section.split("\n")[0];
|
|
368
|
+
const sectionTitle = firstLine.trim();
|
|
369
|
+
const sectionContent = section.slice(firstLine.length).trim();
|
|
370
|
+
|
|
371
|
+
if (sectionContent) {
|
|
372
|
+
items.push({
|
|
373
|
+
id: `${type}-${filename}-${i}`,
|
|
374
|
+
type,
|
|
375
|
+
content: `## ${sectionTitle}\n\n${sectionContent}`,
|
|
376
|
+
source: `${subdir}/${basename(filePath)}`,
|
|
377
|
+
section: sectionTitle,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await indexItems(items);
|
|
383
|
+
console.log(`[Indexer] Indexed ${items.length} sections from ${basename(filePath)}`);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.error(`[Indexer] Failed to index ${filePath}: ${String(err)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Preload the embedding model (call during startup)
|
|
391
|
+
*/
|
|
392
|
+
export async function preloadModel(): Promise<void> {
|
|
393
|
+
await preloadEmbeddings();
|
|
394
|
+
}
|