hive-memory 1.0.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/dist/store.js ADDED
@@ -0,0 +1,993 @@
1
+ import { readFile, writeFile, mkdir, readdir, stat } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join, basename } from "node:path";
4
+ import { EmbedService } from "./embed.js";
5
+ export class CortexStore {
6
+ dataDir;
7
+ localContextFilename;
8
+ embed = new EmbedService();
9
+ constructor(config) {
10
+ this.dataDir = config.dataDir;
11
+ this.localContextFilename = config.localContext.filename;
12
+ }
13
+ async init() {
14
+ const dirs = [
15
+ this.dataDir,
16
+ join(this.dataDir, "projects"),
17
+ join(this.dataDir, "groups"),
18
+ join(this.dataDir, "global"),
19
+ ];
20
+ for (const dir of dirs) {
21
+ if (!existsSync(dir)) {
22
+ await mkdir(dir, { recursive: true });
23
+ }
24
+ }
25
+ if (!existsSync(this.indexPath)) {
26
+ await this.writeJson(this.indexPath, { projects: [] });
27
+ }
28
+ // Initialize semantic search (no-op if native module unavailable)
29
+ await this.embed.init(this.dataDir);
30
+ if (this.embed.available && this.embed.count() === 0) {
31
+ await this.reindexAll();
32
+ }
33
+ }
34
+ // --- Project Index ---
35
+ get indexPath() {
36
+ return join(this.dataDir, "index.json");
37
+ }
38
+ async getIndex() {
39
+ return this.readJson(this.indexPath);
40
+ }
41
+ async saveIndex(index) {
42
+ await this.writeJson(this.indexPath, index);
43
+ }
44
+ async searchProjects(query, limit = 3) {
45
+ const index = await this.getIndex();
46
+ const q = query.toLowerCase().trim();
47
+ // Empty query → return all projects sorted by lastActive
48
+ if (q === "") {
49
+ return index.projects
50
+ .sort((a, b) => new Date(b.lastActive).getTime() -
51
+ new Date(a.lastActive).getTime())
52
+ .slice(0, limit);
53
+ }
54
+ // Find groups matching the query for member project boosting
55
+ const groupIndex = await this.getGroupIndex();
56
+ const matchingGroupProjectIds = new Set();
57
+ for (const g of groupIndex.groups) {
58
+ if (g.name.toLowerCase().includes(q) ||
59
+ g.id.toLowerCase().includes(q) ||
60
+ g.description.toLowerCase().includes(q) ||
61
+ g.tags.some((t) => t.toLowerCase().includes(q))) {
62
+ for (const pid of g.projectIds) {
63
+ matchingGroupProjectIds.add(pid);
64
+ }
65
+ }
66
+ }
67
+ // Build vector score map for semantic boosting
68
+ const vectorScores = new Map();
69
+ const vecHits = this.embed.search(query, limit * 2);
70
+ for (const hit of vecHits) {
71
+ try {
72
+ const meta = hit.metadata ? JSON.parse(hit.metadata) : null;
73
+ if (meta?.type === "project") {
74
+ // Convert distance to a 0-15 score (lower distance = higher score)
75
+ vectorScores.set(meta.project, Math.max(0, 15 * (1 - hit.distance)));
76
+ }
77
+ }
78
+ catch { /* ignore parse errors */ }
79
+ }
80
+ const scored = index.projects.map((p) => {
81
+ let score = 0;
82
+ if (p.name.toLowerCase().includes(q))
83
+ score += 10;
84
+ if (p.id.toLowerCase().includes(q))
85
+ score += 10;
86
+ if (p.description.toLowerCase().includes(q))
87
+ score += 5;
88
+ for (const tag of p.tags) {
89
+ if (tag.toLowerCase().includes(q))
90
+ score += 3;
91
+ }
92
+ // Boost projects that belong to a matching group
93
+ if (matchingGroupProjectIds.has(p.id))
94
+ score += 7;
95
+ // Boost recently active projects
96
+ const daysSince = (Date.now() - new Date(p.lastActive).getTime()) / 86400000;
97
+ if (daysSince < 1)
98
+ score += 3;
99
+ else if (daysSince < 7)
100
+ score += 1;
101
+ // Semantic vector boost
102
+ score += vectorScores.get(p.id) ?? 0;
103
+ return { project: p, score };
104
+ });
105
+ return scored
106
+ .filter((s) => s.score > 0)
107
+ .sort((a, b) => b.score - a.score)
108
+ .slice(0, limit)
109
+ .map((s) => s.project);
110
+ }
111
+ async listProjects(statusFilter) {
112
+ const index = await this.getIndex();
113
+ let projects = index.projects;
114
+ if (statusFilter) {
115
+ projects = projects.filter((p) => p.status === statusFilter);
116
+ }
117
+ return projects.sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime());
118
+ }
119
+ async upsertProject(entry) {
120
+ const index = await this.getIndex();
121
+ const idx = index.projects.findIndex((p) => p.id === entry.id);
122
+ if (idx >= 0) {
123
+ index.projects[idx] = entry;
124
+ }
125
+ else {
126
+ index.projects.push(entry);
127
+ }
128
+ await this.saveIndex(index);
129
+ this.embed.addText(`project:${entry.id}`, `${entry.name} ${entry.description} ${entry.tags.join(" ")}`, JSON.stringify({ type: "project", project: entry.id }));
130
+ }
131
+ async updateProjectStatus(projectId, status) {
132
+ const index = await this.getIndex();
133
+ const proj = index.projects.find((p) => p.id === projectId);
134
+ if (!proj)
135
+ return false;
136
+ proj.status = status;
137
+ await this.saveIndex(index);
138
+ return true;
139
+ }
140
+ async updateProjectMeta(projectId, updates) {
141
+ const index = await this.getIndex();
142
+ const proj = index.projects.find((p) => p.id === projectId);
143
+ if (!proj)
144
+ return false;
145
+ if (updates.name !== undefined)
146
+ proj.name = updates.name;
147
+ if (updates.description !== undefined)
148
+ proj.description = updates.description;
149
+ if (updates.tags !== undefined)
150
+ proj.tags = updates.tags;
151
+ if (updates.path !== undefined)
152
+ proj.path = updates.path;
153
+ await this.saveIndex(index);
154
+ return true;
155
+ }
156
+ // --- Project Summary ---
157
+ projectDir(projectId) {
158
+ return join(this.dataDir, "projects", projectId);
159
+ }
160
+ async getProjectSummary(projectId) {
161
+ const path = join(this.projectDir(projectId), "summary.json");
162
+ if (!existsSync(path))
163
+ return null;
164
+ return this.readJson(path);
165
+ }
166
+ async saveProjectSummary(summary) {
167
+ const dir = this.projectDir(summary.id);
168
+ if (!existsSync(dir))
169
+ await mkdir(dir, { recursive: true });
170
+ await this.writeJson(join(dir, "summary.json"), summary);
171
+ }
172
+ // --- Status (Markdown) ---
173
+ async getProjectStatus(projectId) {
174
+ const path = join(this.projectDir(projectId), "status.md");
175
+ if (!existsSync(path))
176
+ return null;
177
+ return readFile(path, "utf-8");
178
+ }
179
+ async saveProjectStatus(projectId, content) {
180
+ const dir = this.projectDir(projectId);
181
+ if (!existsSync(dir))
182
+ await mkdir(dir, { recursive: true });
183
+ await writeFile(join(dir, "status.md"), content, "utf-8");
184
+ }
185
+ // --- Sessions ---
186
+ async saveSession(projectId, session) {
187
+ const sessionsDir = join(this.projectDir(projectId), "sessions");
188
+ if (!existsSync(sessionsDir))
189
+ await mkdir(sessionsDir, { recursive: true });
190
+ const filename = `${session.date}.md`;
191
+ const content = formatSessionMarkdown(session);
192
+ await writeFile(join(sessionsDir, filename), content, "utf-8");
193
+ // Update project summary with latest session
194
+ let summary = await this.getProjectSummary(projectId);
195
+ if (summary) {
196
+ summary.lastSession = {
197
+ date: session.date,
198
+ summary: session.summary,
199
+ nextTasks: session.nextTasks,
200
+ };
201
+ if (session.nextTasks.length > 0) {
202
+ summary.currentFocus = session.nextTasks[0];
203
+ }
204
+ await this.saveProjectSummary(summary);
205
+ }
206
+ // Update lastActive in index
207
+ const index = await this.getIndex();
208
+ const proj = index.projects.find((p) => p.id === projectId);
209
+ if (proj) {
210
+ proj.lastActive = new Date().toISOString();
211
+ await this.saveIndex(index);
212
+ }
213
+ // Sync local context file into project directory
214
+ await this.syncLocalContext(projectId);
215
+ }
216
+ // --- Memory Entries ---
217
+ async storeMemory(projectId, category, content, tags) {
218
+ const knowledgeDir = join(this.projectDir(projectId), "knowledge");
219
+ if (!existsSync(knowledgeDir))
220
+ await mkdir(knowledgeDir, { recursive: true });
221
+ const entry = {
222
+ id: crypto.randomUUID(),
223
+ project: projectId,
224
+ category,
225
+ content,
226
+ tags,
227
+ createdAt: new Date().toISOString(),
228
+ };
229
+ // Append to category file
230
+ const filename = `${category}s.md`;
231
+ const path = join(knowledgeDir, filename);
232
+ const line = `\n## ${new Date().toISOString().slice(0, 10)}\n\n${content}\n`;
233
+ if (existsSync(path)) {
234
+ const existing = await readFile(path, "utf-8");
235
+ await writeFile(path, existing + line, "utf-8");
236
+ }
237
+ else {
238
+ const header = `# ${capitalize(category)}s — ${projectId}\n`;
239
+ await writeFile(path, header + line, "utf-8");
240
+ }
241
+ this.embed.addText(`memory:${projectId}:${category}:${entry.createdAt}`, content, JSON.stringify({ type: "memory", project: projectId, category }));
242
+ return entry;
243
+ }
244
+ async recallMemories(query, projectId, limit = 5) {
245
+ const index = await this.getIndex();
246
+ const projects = projectId
247
+ ? index.projects.filter((p) => p.id === projectId)
248
+ : index.projects;
249
+ const results = [];
250
+ const q = query.toLowerCase();
251
+ // Keyword search (existing logic)
252
+ for (const proj of projects) {
253
+ const knowledgeDir = join(this.projectDir(proj.id), "knowledge");
254
+ if (!existsSync(knowledgeDir))
255
+ continue;
256
+ const categories = [
257
+ "decision",
258
+ "learning",
259
+ "status",
260
+ "note",
261
+ ];
262
+ for (const cat of categories) {
263
+ const path = join(knowledgeDir, `${cat}s.md`);
264
+ if (!existsSync(path))
265
+ continue;
266
+ const content = await readFile(path, "utf-8");
267
+ const sections = content.split(/^## /m).filter(Boolean);
268
+ for (const section of sections) {
269
+ if (section.toLowerCase().includes(q)) {
270
+ results.push({
271
+ project: proj.id,
272
+ category: cat,
273
+ snippet: section.trim().slice(0, 300),
274
+ score: 10, // keyword match base score
275
+ });
276
+ }
277
+ }
278
+ }
279
+ }
280
+ // Vector search — merge with keyword results
281
+ const vecHits = this.embed.search(query, limit * 2);
282
+ for (const hit of vecHits) {
283
+ try {
284
+ const meta = hit.metadata ? JSON.parse(hit.metadata) : null;
285
+ if (meta?.type !== "memory")
286
+ continue;
287
+ if (projectId && meta.project !== projectId)
288
+ continue;
289
+ const vecScore = Math.max(0, 15 * (1 - hit.distance));
290
+ // Check if this memory already found by keyword
291
+ const existing = results.find((r) => r.project === meta.project && r.category === meta.category &&
292
+ r.snippet.includes(hit.id.split(":").slice(-1)[0]?.slice(0, 10) ?? ""));
293
+ if (existing) {
294
+ existing.score += vecScore;
295
+ }
296
+ else {
297
+ // Vector-only result: extract snippet from ID
298
+ results.push({
299
+ project: meta.project,
300
+ category: meta.category,
301
+ snippet: `[semantic match] ${hit.id}`,
302
+ score: vecScore,
303
+ });
304
+ }
305
+ }
306
+ catch { /* ignore */ }
307
+ }
308
+ // Re-rank by combined score
309
+ return results
310
+ .sort((a, b) => b.score - a.score)
311
+ .slice(0, limit);
312
+ }
313
+ // --- Local Context Sync ---
314
+ /**
315
+ * Write a .cortex.md into the project's actual directory.
316
+ * This gives Claude Code immediate detailed context when opening that project.
317
+ */
318
+ async syncLocalContext(projectId) {
319
+ const index = await this.getIndex();
320
+ const proj = index.projects.find((p) => p.id === projectId);
321
+ if (!proj || !existsSync(proj.path))
322
+ return null;
323
+ const summary = await this.getProjectSummary(projectId);
324
+ const status = await this.getProjectStatus(projectId);
325
+ // Gather recent knowledge
326
+ const knowledgeDir = join(this.projectDir(projectId), "knowledge");
327
+ let recentDecisions = "";
328
+ let recentLearnings = "";
329
+ if (existsSync(knowledgeDir)) {
330
+ const decisionsPath = join(knowledgeDir, "decisions.md");
331
+ const learningsPath = join(knowledgeDir, "learnings.md");
332
+ if (existsSync(decisionsPath)) {
333
+ const content = await readFile(decisionsPath, "utf-8");
334
+ recentDecisions = getLastNSections(content, 5);
335
+ }
336
+ if (existsSync(learningsPath)) {
337
+ const content = await readFile(learningsPath, "utf-8");
338
+ recentLearnings = getLastNSections(content, 5);
339
+ }
340
+ }
341
+ // Get last 3 session logs
342
+ const sessionsDir = join(this.projectDir(projectId), "sessions");
343
+ let recentSessions = "";
344
+ if (existsSync(sessionsDir)) {
345
+ const files = (await readdir(sessionsDir))
346
+ .filter((f) => f.endsWith(".md"))
347
+ .sort()
348
+ .slice(-3);
349
+ for (const file of files) {
350
+ const content = await readFile(join(sessionsDir, file), "utf-8");
351
+ recentSessions += content + "\n---\n\n";
352
+ }
353
+ }
354
+ // Build the local context document
355
+ let md = `<!-- Auto-generated by Cortex. Do not edit manually. -->\n`;
356
+ md += `<!-- Last synced: ${new Date().toISOString()} -->\n\n`;
357
+ md += `# ${proj.name} — Cortex Context\n\n`;
358
+ if (summary) {
359
+ md += `## Overview\n\n`;
360
+ md += `${summary.oneLiner}\n\n`;
361
+ md += `- **Tech**: ${summary.techStack.join(", ")}\n`;
362
+ md += `- **Modules**: ${summary.modules.join(", ")}\n`;
363
+ md += `- **Current Focus**: ${summary.currentFocus}\n\n`;
364
+ }
365
+ if (summary?.lastSession) {
366
+ md += `## Current Status\n\n`;
367
+ md += `Last session: ${summary.lastSession.date}\n\n`;
368
+ md += `${summary.lastSession.summary}\n\n`;
369
+ if (summary.lastSession.nextTasks.length > 0) {
370
+ md += `### Next Tasks\n\n`;
371
+ for (const t of summary.lastSession.nextTasks) {
372
+ md += `- [ ] ${t}\n`;
373
+ }
374
+ md += "\n";
375
+ }
376
+ }
377
+ if (status) {
378
+ md += `## Detailed Status\n\n${status}\n\n`;
379
+ }
380
+ if (recentDecisions) {
381
+ md += `## Recent Decisions\n\n${recentDecisions}\n`;
382
+ }
383
+ if (recentLearnings) {
384
+ md += `## Recent Learnings\n\n${recentLearnings}\n`;
385
+ }
386
+ if (recentSessions) {
387
+ md += `## Recent Sessions\n\n${recentSessions}`;
388
+ }
389
+ // Group context section (guide names only for token efficiency)
390
+ if (proj.groupIds && proj.groupIds.length > 0) {
391
+ const groupIndex = await this.getGroupIndex();
392
+ md += `## Groups\n\n`;
393
+ for (const gid of proj.groupIds) {
394
+ const group = groupIndex.groups.find((g) => g.id === gid);
395
+ if (!group)
396
+ continue;
397
+ md += `- **${group.name}** (${gid})\n`;
398
+ const guidesDir = join(this.groupDir(gid), "guides");
399
+ if (existsSync(guidesDir)) {
400
+ const guideFiles = (await readdir(guidesDir)).filter((f) => f.endsWith(".md"));
401
+ if (guideFiles.length > 0) {
402
+ md += ` Shared: ${guideFiles.map((f) => f.replace(/\.md$/, "")).join(", ")}\n`;
403
+ }
404
+ }
405
+ md += ` → group_context("${gid}") for details\n`;
406
+ }
407
+ md += "\n";
408
+ }
409
+ const localPath = join(proj.path, this.localContextFilename);
410
+ await writeFile(localPath, md, "utf-8");
411
+ return localPath;
412
+ }
413
+ // --- Group Index ---
414
+ get groupIndexPath() {
415
+ return join(this.dataDir, "groups.json");
416
+ }
417
+ groupDir(groupId) {
418
+ return join(this.dataDir, "groups", groupId);
419
+ }
420
+ async getGroupIndex() {
421
+ if (!existsSync(this.groupIndexPath)) {
422
+ return { groups: [] };
423
+ }
424
+ return this.readJson(this.groupIndexPath);
425
+ }
426
+ async saveGroupIndex(index) {
427
+ await this.writeJson(this.groupIndexPath, index);
428
+ }
429
+ async createGroup(entry) {
430
+ const now = new Date().toISOString();
431
+ const group = {
432
+ ...entry,
433
+ createdAt: now,
434
+ lastActive: now,
435
+ };
436
+ // Save to groups.json
437
+ const groupIndex = await this.getGroupIndex();
438
+ groupIndex.groups.push(group);
439
+ await this.saveGroupIndex(groupIndex);
440
+ // Create group directory structure
441
+ const dir = this.groupDir(group.id);
442
+ await mkdir(join(dir, "guides"), { recursive: true });
443
+ await mkdir(join(dir, "knowledge"), { recursive: true });
444
+ // Write default overview.md
445
+ await writeFile(join(dir, "overview.md"), `# ${group.name}\n\n${group.description}\n`, "utf-8");
446
+ // Add bidirectional references to projects
447
+ if (group.projectIds.length > 0) {
448
+ const projectIndex = await this.getIndex();
449
+ for (const pid of group.projectIds) {
450
+ const proj = projectIndex.projects.find((p) => p.id === pid);
451
+ if (proj) {
452
+ if (!proj.groupIds)
453
+ proj.groupIds = [];
454
+ if (!proj.groupIds.includes(group.id)) {
455
+ proj.groupIds.push(group.id);
456
+ }
457
+ }
458
+ }
459
+ await this.saveIndex(projectIndex);
460
+ }
461
+ this.embed.addText(`group:${group.id}`, `${group.name} ${group.description} ${group.tags.join(" ")}`, JSON.stringify({ type: "group", group: group.id }));
462
+ return group;
463
+ }
464
+ async getGroupContext(groupId, detail = "brief") {
465
+ const groupIndex = await this.getGroupIndex();
466
+ const group = groupIndex.groups.find((g) => g.id === groupId);
467
+ if (!group)
468
+ return null;
469
+ const dir = this.groupDir(groupId);
470
+ let md = `# ${group.name}\n\n`;
471
+ md += `${group.description}\n\n`;
472
+ md += `**Tags**: ${group.tags.join(", ")}\n\n`;
473
+ // Overview
474
+ const overviewPath = join(dir, "overview.md");
475
+ if (existsSync(overviewPath)) {
476
+ const overview = await readFile(overviewPath, "utf-8");
477
+ // Skip the auto-generated header line if it matches the name
478
+ const lines = overview.split("\n");
479
+ const body = lines.slice(lines[0]?.startsWith("# ") ? 1 : 0).join("\n").trim();
480
+ if (body && body !== group.description) {
481
+ md += `## Overview\n\n${body}\n\n`;
482
+ }
483
+ }
484
+ // Guides
485
+ const guidesDir = join(dir, "guides");
486
+ if (existsSync(guidesDir)) {
487
+ const guideFiles = (await readdir(guidesDir)).filter((f) => f.endsWith(".md"));
488
+ if (guideFiles.length > 0) {
489
+ md += `## Shared Guides\n\n`;
490
+ if (detail === "brief") {
491
+ for (const f of guideFiles) {
492
+ md += `- ${f.replace(/\.md$/, "")}\n`;
493
+ }
494
+ md += `\n→ Use group_context("${groupId}", detail="full") for full guide contents\n\n`;
495
+ }
496
+ else {
497
+ for (const f of guideFiles) {
498
+ const content = await readFile(join(guidesDir, f), "utf-8");
499
+ md += `### ${f.replace(/\.md$/, "")}\n\n${content}\n\n`;
500
+ }
501
+ }
502
+ }
503
+ }
504
+ // Knowledge (group-level memories)
505
+ if (detail === "full") {
506
+ const knowledgeDir = join(dir, "knowledge");
507
+ if (existsSync(knowledgeDir)) {
508
+ const knowledgeFiles = (await readdir(knowledgeDir)).filter((f) => f.endsWith(".md"));
509
+ if (knowledgeFiles.length > 0) {
510
+ md += `## Group Knowledge\n\n`;
511
+ for (const f of knowledgeFiles) {
512
+ const content = await readFile(join(knowledgeDir, f), "utf-8");
513
+ md += `### ${f.replace(/\.md$/, "")}\n\n${content}\n\n`;
514
+ }
515
+ }
516
+ }
517
+ }
518
+ // Member projects
519
+ if (group.projectIds.length > 0) {
520
+ md += `## Member Projects\n\n`;
521
+ const projectIndex = await this.getIndex();
522
+ for (const pid of group.projectIds) {
523
+ const proj = projectIndex.projects.find((p) => p.id === pid);
524
+ if (proj) {
525
+ const summary = await this.getProjectSummary(pid);
526
+ const focus = summary?.currentFocus ?? "—";
527
+ md += `- **${proj.name}** (${pid}) — ${focus}\n`;
528
+ }
529
+ }
530
+ md += "\n";
531
+ }
532
+ // Update lastActive
533
+ group.lastActive = new Date().toISOString();
534
+ await this.saveGroupIndex(groupIndex);
535
+ return md;
536
+ }
537
+ async addProjectToGroup(groupId, projectId) {
538
+ const groupIndex = await this.getGroupIndex();
539
+ const group = groupIndex.groups.find((g) => g.id === groupId);
540
+ if (!group)
541
+ return false;
542
+ if (!group.projectIds.includes(projectId)) {
543
+ group.projectIds.push(projectId);
544
+ group.lastActive = new Date().toISOString();
545
+ await this.saveGroupIndex(groupIndex);
546
+ }
547
+ // Update project side
548
+ const projectIndex = await this.getIndex();
549
+ const proj = projectIndex.projects.find((p) => p.id === projectId);
550
+ if (proj) {
551
+ if (!proj.groupIds)
552
+ proj.groupIds = [];
553
+ if (!proj.groupIds.includes(groupId)) {
554
+ proj.groupIds.push(groupId);
555
+ await this.saveIndex(projectIndex);
556
+ }
557
+ }
558
+ return true;
559
+ }
560
+ async removeProjectFromGroup(groupId, projectId) {
561
+ const groupIndex = await this.getGroupIndex();
562
+ const group = groupIndex.groups.find((g) => g.id === groupId);
563
+ if (!group)
564
+ return false;
565
+ group.projectIds = group.projectIds.filter((id) => id !== projectId);
566
+ group.lastActive = new Date().toISOString();
567
+ await this.saveGroupIndex(groupIndex);
568
+ // Update project side
569
+ const projectIndex = await this.getIndex();
570
+ const proj = projectIndex.projects.find((p) => p.id === projectId);
571
+ if (proj && proj.groupIds) {
572
+ proj.groupIds = proj.groupIds.filter((id) => id !== groupId);
573
+ if (proj.groupIds.length === 0)
574
+ delete proj.groupIds;
575
+ await this.saveIndex(projectIndex);
576
+ }
577
+ return true;
578
+ }
579
+ async searchGroups(query) {
580
+ const groupIndex = await this.getGroupIndex();
581
+ const q = query.toLowerCase().trim();
582
+ if (q === "")
583
+ return groupIndex.groups;
584
+ // Build vector score map
585
+ const vectorScores = new Map();
586
+ const vecHits = this.embed.search(query, 10);
587
+ for (const hit of vecHits) {
588
+ try {
589
+ const meta = hit.metadata ? JSON.parse(hit.metadata) : null;
590
+ if (meta?.type === "group") {
591
+ vectorScores.set(meta.group, Math.max(0, 15 * (1 - hit.distance)));
592
+ }
593
+ }
594
+ catch { /* ignore */ }
595
+ }
596
+ const scored = groupIndex.groups.map((g) => {
597
+ let score = 0;
598
+ if (g.name.toLowerCase().includes(q))
599
+ score += 10;
600
+ if (g.id.toLowerCase().includes(q))
601
+ score += 10;
602
+ if (g.description.toLowerCase().includes(q))
603
+ score += 5;
604
+ if (g.tags.some((t) => t.toLowerCase().includes(q)))
605
+ score += 3;
606
+ score += vectorScores.get(g.id) ?? 0;
607
+ return { group: g, score };
608
+ });
609
+ return scored
610
+ .filter((s) => s.score > 0)
611
+ .sort((a, b) => b.score - a.score)
612
+ .map((s) => s.group);
613
+ }
614
+ async saveGroupGuide(groupId, filename, content) {
615
+ const guidesDir = join(this.groupDir(groupId), "guides");
616
+ if (!existsSync(guidesDir))
617
+ await mkdir(guidesDir, { recursive: true });
618
+ const safeName = filename.endsWith(".md") ? filename : `${filename}.md`;
619
+ const path = join(guidesDir, safeName);
620
+ await writeFile(path, content, "utf-8");
621
+ // Update lastActive
622
+ const groupIndex = await this.getGroupIndex();
623
+ const group = groupIndex.groups.find((g) => g.id === groupId);
624
+ if (group) {
625
+ group.lastActive = new Date().toISOString();
626
+ await this.saveGroupIndex(groupIndex);
627
+ }
628
+ this.embed.addText(`guide:${groupId}:${safeName}`, content, JSON.stringify({ type: "guide", group: groupId, file: safeName }));
629
+ return path;
630
+ }
631
+ async storeGroupMemory(groupId, category, content, tags) {
632
+ const knowledgeDir = join(this.groupDir(groupId), "knowledge");
633
+ if (!existsSync(knowledgeDir))
634
+ await mkdir(knowledgeDir, { recursive: true });
635
+ const entry = {
636
+ id: crypto.randomUUID(),
637
+ project: `group:${groupId}`,
638
+ category,
639
+ content,
640
+ tags,
641
+ createdAt: new Date().toISOString(),
642
+ };
643
+ const filename = `${category}s.md`;
644
+ const path = join(knowledgeDir, filename);
645
+ const line = `\n## ${new Date().toISOString().slice(0, 10)}\n\n${content}\n`;
646
+ if (existsSync(path)) {
647
+ const existing = await readFile(path, "utf-8");
648
+ await writeFile(path, existing + line, "utf-8");
649
+ }
650
+ else {
651
+ const header = `# ${capitalize(category)}s — group:${groupId}\n`;
652
+ await writeFile(path, header + line, "utf-8");
653
+ }
654
+ this.embed.addText(`gmem:${groupId}:${category}:${entry.createdAt}`, content, JSON.stringify({ type: "gmemory", group: groupId, category }));
655
+ return entry;
656
+ }
657
+ async recallGroupMemories(groupId, query, limit = 5) {
658
+ const results = [];
659
+ const q = query.toLowerCase();
660
+ // Search group-level knowledge (keyword)
661
+ const knowledgeDir = join(this.groupDir(groupId), "knowledge");
662
+ if (existsSync(knowledgeDir)) {
663
+ const categories = ["decision", "learning", "status", "note"];
664
+ for (const cat of categories) {
665
+ const path = join(knowledgeDir, `${cat}s.md`);
666
+ if (!existsSync(path))
667
+ continue;
668
+ const content = await readFile(path, "utf-8");
669
+ const sections = content.split(/^## /m).filter(Boolean);
670
+ for (const section of sections) {
671
+ if (section.toLowerCase().includes(q)) {
672
+ results.push({
673
+ project: `group:${groupId}`,
674
+ category: cat,
675
+ snippet: section.trim().slice(0, 300),
676
+ score: 10,
677
+ });
678
+ }
679
+ }
680
+ }
681
+ }
682
+ // Vector search for group memories
683
+ const vecHits = this.embed.search(query, limit * 2);
684
+ for (const hit of vecHits) {
685
+ try {
686
+ const meta = hit.metadata ? JSON.parse(hit.metadata) : null;
687
+ if (meta?.type === "gmemory" && meta.group === groupId) {
688
+ const vecScore = Math.max(0, 15 * (1 - hit.distance));
689
+ results.push({
690
+ project: `group:${groupId}`,
691
+ category: meta.category,
692
+ snippet: `[semantic match] ${hit.id}`,
693
+ score: vecScore,
694
+ });
695
+ }
696
+ }
697
+ catch { /* ignore */ }
698
+ }
699
+ // Also search member projects
700
+ const groupIndex = await this.getGroupIndex();
701
+ const group = groupIndex.groups.find((g) => g.id === groupId);
702
+ if (group) {
703
+ for (const pid of group.projectIds) {
704
+ const projectMemories = await this.recallMemories(query, pid, limit);
705
+ results.push(...projectMemories.map((m) => ({ ...m, score: m.score ?? 0 })));
706
+ }
707
+ }
708
+ return results
709
+ .sort((a, b) => b.score - a.score)
710
+ .slice(0, limit);
711
+ }
712
+ // --- Onboarding ---
713
+ async scanForProjects(rootPath, depth = 1) {
714
+ const index = await this.getIndex();
715
+ const registeredPaths = new Set(index.projects.map((p) => p.path));
716
+ const registeredIds = new Set(index.projects.map((p) => p.id));
717
+ const candidates = [];
718
+ const scan = async (dir, currentDepth) => {
719
+ if (currentDepth > depth)
720
+ return;
721
+ let entries;
722
+ try {
723
+ entries = await readdir(dir);
724
+ }
725
+ catch {
726
+ return;
727
+ }
728
+ // Check if this directory itself is a project
729
+ const detected = await this.detectProject(dir);
730
+ if (detected) {
731
+ detected.alreadyRegistered =
732
+ registeredPaths.has(dir) || registeredIds.has(detected.suggestedId);
733
+ candidates.push(detected);
734
+ return; // Don't recurse into detected projects
735
+ }
736
+ // Recurse into subdirectories
737
+ for (const entry of entries) {
738
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "target" || entry === "dist")
739
+ continue;
740
+ const fullPath = join(dir, entry);
741
+ try {
742
+ const s = await stat(fullPath);
743
+ if (s.isDirectory()) {
744
+ await scan(fullPath, currentDepth + 1);
745
+ }
746
+ }
747
+ catch {
748
+ continue;
749
+ }
750
+ }
751
+ };
752
+ await scan(rootPath, 0);
753
+ return candidates;
754
+ }
755
+ async detectProject(dir) {
756
+ const hasFile = (name) => existsSync(join(dir, name));
757
+ const hasPackageJson = hasFile("package.json");
758
+ const hasCargoToml = hasFile("Cargo.toml");
759
+ const hasGit = hasFile(".git");
760
+ const hasPyproject = hasFile("pyproject.toml");
761
+ const hasGoMod = hasFile("go.mod");
762
+ // Must have at least one project marker
763
+ if (!hasPackageJson && !hasCargoToml && !hasGit && !hasPyproject && !hasGoMod) {
764
+ return null;
765
+ }
766
+ const dirName = basename(dir);
767
+ let suggestedName = dirName;
768
+ let description = "";
769
+ const techStack = [];
770
+ const modules = [];
771
+ const tags = [];
772
+ // Detect from package.json
773
+ if (hasPackageJson) {
774
+ try {
775
+ const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8"));
776
+ if (pkg.name)
777
+ suggestedName = pkg.name.replace(/^@[^/]+\//, "");
778
+ if (pkg.description)
779
+ description = pkg.description;
780
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
781
+ if (deps["react"] || deps["react-dom"])
782
+ techStack.push("React");
783
+ if (deps["next"])
784
+ techStack.push("Next.js");
785
+ if (deps["vue"])
786
+ techStack.push("Vue");
787
+ if (deps["svelte"])
788
+ techStack.push("Svelte");
789
+ if (deps["typescript"])
790
+ techStack.push("TypeScript");
791
+ if (deps["@anthropic-ai/sdk"])
792
+ techStack.push("Anthropic SDK");
793
+ if (deps["@modelcontextprotocol/sdk"])
794
+ techStack.push("MCP SDK");
795
+ if (deps["express"] || deps["fastify"] || deps["hono"])
796
+ techStack.push("Node.js");
797
+ if (deps["tailwindcss"])
798
+ tags.push("tailwind");
799
+ if (deps["@tauri-apps/api"])
800
+ techStack.push("Tauri");
801
+ if (deps["vitest"] || deps["jest"])
802
+ tags.push("tested");
803
+ if (!techStack.includes("TypeScript") && !techStack.includes("Node.js")) {
804
+ techStack.push("Node.js");
805
+ }
806
+ }
807
+ catch { /* ignore parse errors */ }
808
+ }
809
+ // Detect from Cargo.toml
810
+ if (hasCargoToml) {
811
+ try {
812
+ const cargo = await readFile(join(dir, "Cargo.toml"), "utf-8");
813
+ techStack.push("Rust");
814
+ const nameMatch = cargo.match(/^name\s*=\s*"(.+)"/m);
815
+ if (nameMatch && !hasPackageJson)
816
+ suggestedName = nameMatch[1];
817
+ const descMatch = cargo.match(/^description\s*=\s*"(.+)"/m);
818
+ if (descMatch && !description)
819
+ description = descMatch[1];
820
+ if (cargo.includes("tokio"))
821
+ techStack.push("tokio");
822
+ if (cargo.includes("[workspace]")) {
823
+ // Detect workspace members
824
+ const membersMatch = cargo.match(/members\s*=\s*\[([\s\S]*?)\]/);
825
+ if (membersMatch) {
826
+ const members = membersMatch[1].match(/"([^"]+)"/g);
827
+ if (members) {
828
+ modules.push(...members.map((m) => m.replace(/"/g, "").replace(/.*\//, "")));
829
+ }
830
+ }
831
+ }
832
+ }
833
+ catch { /* ignore */ }
834
+ }
835
+ // Detect from pyproject.toml
836
+ if (hasPyproject) {
837
+ try {
838
+ const content = await readFile(join(dir, "pyproject.toml"), "utf-8");
839
+ techStack.push("Python");
840
+ const nameMatch = content.match(/^name\s*=\s*"(.+)"/m);
841
+ if (nameMatch && !hasPackageJson && !hasCargoToml)
842
+ suggestedName = nameMatch[1];
843
+ const descMatch = content.match(/^description\s*=\s*"(.+)"/m);
844
+ if (descMatch && !description)
845
+ description = descMatch[1];
846
+ if (content.includes("torch"))
847
+ techStack.push("PyTorch");
848
+ if (content.includes("fastapi"))
849
+ techStack.push("FastAPI");
850
+ }
851
+ catch { /* ignore */ }
852
+ }
853
+ // Detect from go.mod
854
+ if (hasGoMod) {
855
+ techStack.push("Go");
856
+ }
857
+ // Generate a clean ID
858
+ const suggestedId = dirName
859
+ .toLowerCase()
860
+ .replace(/[^a-z0-9-]/g, "-")
861
+ .replace(/-+/g, "-")
862
+ .replace(/^-|-$/g, "");
863
+ // Add generic tags from dir name
864
+ tags.push(...techStack.map((t) => t.toLowerCase().replace(/[^a-z0-9]/g, "")));
865
+ if (!description) {
866
+ description = `${suggestedName} project`;
867
+ }
868
+ return {
869
+ path: dir,
870
+ suggestedId,
871
+ suggestedName,
872
+ description,
873
+ techStack: [...new Set(techStack)],
874
+ modules,
875
+ tags: [...new Set(tags)],
876
+ alreadyRegistered: false,
877
+ };
878
+ }
879
+ // --- Reindex (initial population of vector index) ---
880
+ async reindexAll() {
881
+ if (!this.embed.available)
882
+ return;
883
+ // Index all projects
884
+ const index = await this.getIndex();
885
+ for (const p of index.projects) {
886
+ this.embed.addText(`project:${p.id}`, `${p.name} ${p.description} ${p.tags.join(" ")}`, JSON.stringify({ type: "project", project: p.id }));
887
+ }
888
+ // Index all groups
889
+ const groupIndex = await this.getGroupIndex();
890
+ for (const g of groupIndex.groups) {
891
+ this.embed.addText(`group:${g.id}`, `${g.name} ${g.description} ${g.tags.join(" ")}`, JSON.stringify({ type: "group", group: g.id }));
892
+ // Index group guides
893
+ const guidesDir = join(this.groupDir(g.id), "guides");
894
+ if (existsSync(guidesDir)) {
895
+ const files = (await readdir(guidesDir)).filter((f) => f.endsWith(".md"));
896
+ for (const file of files) {
897
+ const content = await readFile(join(guidesDir, file), "utf-8");
898
+ this.embed.addText(`guide:${g.id}:${file}`, content, JSON.stringify({ type: "guide", group: g.id, file }));
899
+ }
900
+ }
901
+ }
902
+ // Index all memories (split by ## sections)
903
+ for (const proj of index.projects) {
904
+ const knowledgeDir = join(this.projectDir(proj.id), "knowledge");
905
+ if (!existsSync(knowledgeDir))
906
+ continue;
907
+ const categories = ["decision", "learning", "status", "note"];
908
+ for (const cat of categories) {
909
+ const path = join(knowledgeDir, `${cat}s.md`);
910
+ if (!existsSync(path))
911
+ continue;
912
+ const content = await readFile(path, "utf-8");
913
+ const sections = content.split(/^## /m).filter(Boolean);
914
+ for (let i = 0; i < sections.length; i++) {
915
+ const section = sections[i].trim();
916
+ if (!section)
917
+ continue;
918
+ // Extract date from first line if present
919
+ const dateMatch = section.match(/^(\d{4}-\d{2}-\d{2})/);
920
+ const ts = dateMatch ? dateMatch[1] : `s${i}`;
921
+ this.embed.addText(`memory:${proj.id}:${cat}:${ts}`, section, JSON.stringify({ type: "memory", project: proj.id, category: cat }));
922
+ }
923
+ }
924
+ }
925
+ // Index group memories
926
+ for (const g of groupIndex.groups) {
927
+ const knowledgeDir = join(this.groupDir(g.id), "knowledge");
928
+ if (!existsSync(knowledgeDir))
929
+ continue;
930
+ const categories = ["decision", "learning", "status", "note"];
931
+ for (const cat of categories) {
932
+ const path = join(knowledgeDir, `${cat}s.md`);
933
+ if (!existsSync(path))
934
+ continue;
935
+ const content = await readFile(path, "utf-8");
936
+ const sections = content.split(/^## /m).filter(Boolean);
937
+ for (let i = 0; i < sections.length; i++) {
938
+ const section = sections[i].trim();
939
+ if (!section)
940
+ continue;
941
+ const dateMatch = section.match(/^(\d{4}-\d{2}-\d{2})/);
942
+ const ts = dateMatch ? dateMatch[1] : `s${i}`;
943
+ this.embed.addText(`gmem:${g.id}:${cat}:${ts}`, section, JSON.stringify({ type: "gmemory", group: g.id, category: cat }));
944
+ }
945
+ }
946
+ }
947
+ }
948
+ // --- Helpers ---
949
+ async readJson(path) {
950
+ const raw = await readFile(path, "utf-8");
951
+ return JSON.parse(raw);
952
+ }
953
+ async writeJson(path, data) {
954
+ await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
955
+ }
956
+ }
957
+ function capitalize(s) {
958
+ return s.charAt(0).toUpperCase() + s.slice(1);
959
+ }
960
+ function getLastNSections(content, n) {
961
+ const sections = content.split(/^## /m).filter(Boolean);
962
+ return sections
963
+ .slice(-n)
964
+ .map((s) => `## ${s}`)
965
+ .join("");
966
+ }
967
+ function formatSessionMarkdown(session) {
968
+ let md = `# Session ${session.date}\n\n`;
969
+ md += `## Summary\n\n${session.summary}\n\n`;
970
+ if (session.nextTasks.length > 0) {
971
+ md += `## Next Tasks\n\n`;
972
+ for (const task of session.nextTasks) {
973
+ md += `- [ ] ${task}\n`;
974
+ }
975
+ md += "\n";
976
+ }
977
+ if (session.decisions.length > 0) {
978
+ md += `## Decisions\n\n`;
979
+ for (const d of session.decisions) {
980
+ md += `- ${d}\n`;
981
+ }
982
+ md += "\n";
983
+ }
984
+ if (session.learnings.length > 0) {
985
+ md += `## Learnings\n\n`;
986
+ for (const l of session.learnings) {
987
+ md += `- ${l}\n`;
988
+ }
989
+ md += "\n";
990
+ }
991
+ return md;
992
+ }
993
+ //# sourceMappingURL=store.js.map