@brianluby/agent-brain 1.1.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.
@@ -0,0 +1,1386 @@
1
+ #!/usr/bin/env node
2
+ import { constants, existsSync, readdirSync, unlinkSync, readFileSync, mkdirSync, writeFileSync, renameSync, rmSync } from 'fs';
3
+ import { dirname, resolve, isAbsolute, relative, sep, join } from 'path';
4
+ import { access, readFile, mkdir, open } from 'fs/promises';
5
+ import { randomBytes } from 'crypto';
6
+ import lockfile from 'proper-lockfile';
7
+ import { tmpdir } from 'os';
8
+ import { execSync } from 'child_process';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ // src/types.ts
12
+ var DEFAULT_MEMORY_PATH = ".agent-brain/mind.mv2";
13
+ var DEFAULT_CONFIG = {
14
+ memoryPath: DEFAULT_MEMORY_PATH,
15
+ maxContextObservations: 20,
16
+ maxContextTokens: 2e3,
17
+ autoCompress: true,
18
+ minConfidence: 0.6,
19
+ debug: false
20
+ };
21
+ function generateId() {
22
+ return randomBytes(8).toString("hex");
23
+ }
24
+ function estimateTokens(text) {
25
+ return Math.ceil(text.length / 4);
26
+ }
27
+ async function readStdin() {
28
+ const chunks = [];
29
+ return new Promise((resolve6, reject) => {
30
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
31
+ process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
32
+ process.stdin.on("error", reject);
33
+ });
34
+ }
35
+ function writeOutput(output) {
36
+ console.log(JSON.stringify(output));
37
+ process.exit(0);
38
+ }
39
+ function debug(message) {
40
+ if (process.env.MEMVID_MIND_DEBUG === "1") {
41
+ console.error(`[memvid-mind] ${message}`);
42
+ }
43
+ }
44
+ var LOCK_OPTIONS = {
45
+ stale: 3e4,
46
+ retries: {
47
+ retries: 1e3,
48
+ minTimeout: 5,
49
+ maxTimeout: 50
50
+ }
51
+ };
52
+ async function withMemvidLock(lockPath, fn) {
53
+ await mkdir(dirname(lockPath), { recursive: true });
54
+ const handle = await open(lockPath, "a");
55
+ await handle.close();
56
+ const release = await lockfile.lock(lockPath, LOCK_OPTIONS);
57
+ try {
58
+ return await fn();
59
+ } finally {
60
+ await release();
61
+ }
62
+ }
63
+ function defaultPlatformRelativePath(platform) {
64
+ const normalizedPlatform = platform.trim().toLowerCase();
65
+ const safePlatform = normalizedPlatform.replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "") || "unknown";
66
+ return `.agent-brain/mind-${safePlatform}.mv2`;
67
+ }
68
+ function resolveInsideProject(projectDir, candidatePath) {
69
+ if (isAbsolute(candidatePath)) {
70
+ return resolve(candidatePath);
71
+ }
72
+ const root = resolve(projectDir);
73
+ const resolved = resolve(root, candidatePath);
74
+ const rel = relative(root, resolved);
75
+ if (rel === ".." || rel.startsWith(`..${sep}`)) {
76
+ throw new Error("Resolved memory path must stay inside projectDir");
77
+ }
78
+ return resolved;
79
+ }
80
+ function resolveMemoryPathPolicy(input) {
81
+ const mode = input.platformOptIn ? "platform_opt_in" : "legacy_first";
82
+ const canonicalRelativePath = input.platformOptIn ? input.platformRelativePath || defaultPlatformRelativePath(input.platform) : input.defaultRelativePath;
83
+ const canonicalPath = resolveInsideProject(input.projectDir, canonicalRelativePath);
84
+ if (existsSync(canonicalPath)) {
85
+ return {
86
+ mode,
87
+ memoryPath: canonicalPath,
88
+ canonicalPath
89
+ };
90
+ }
91
+ const fallbackPaths = (input.legacyRelativePaths || []).map((relativePath) => resolveInsideProject(input.projectDir, relativePath));
92
+ for (const fallbackPath of fallbackPaths) {
93
+ if (existsSync(fallbackPath)) {
94
+ return {
95
+ mode,
96
+ memoryPath: fallbackPath,
97
+ canonicalPath,
98
+ migrationSuggestion: {
99
+ fromPath: fallbackPath,
100
+ toPath: canonicalPath
101
+ }
102
+ };
103
+ }
104
+ }
105
+ if (input.platformOptIn) {
106
+ return {
107
+ mode: "platform_opt_in",
108
+ memoryPath: canonicalPath,
109
+ canonicalPath
110
+ };
111
+ }
112
+ return {
113
+ mode: "legacy_first",
114
+ memoryPath: canonicalPath,
115
+ canonicalPath
116
+ };
117
+ }
118
+
119
+ // src/platforms/platform-detector.ts
120
+ function normalizePlatform(value) {
121
+ if (!value) return void 0;
122
+ const normalized = value.trim().toLowerCase();
123
+ return normalized.length > 0 ? normalized : void 0;
124
+ }
125
+ function detectPlatformFromEnv() {
126
+ const explicitFromEnv = normalizePlatform(process.env.MEMVID_PLATFORM);
127
+ if (explicitFromEnv) {
128
+ return explicitFromEnv;
129
+ }
130
+ if (process.env.OPENCODE === "1") {
131
+ return "opencode";
132
+ }
133
+ return "claude";
134
+ }
135
+ function detectPlatform(input) {
136
+ const explicitFromHook = normalizePlatform(input.platform);
137
+ if (explicitFromHook) {
138
+ return explicitFromHook;
139
+ }
140
+ return detectPlatformFromEnv();
141
+ }
142
+
143
+ // src/core/mind.ts
144
+ function pruneBackups(memoryPath, keepCount) {
145
+ try {
146
+ const dir = dirname(memoryPath);
147
+ const baseName = memoryPath.split("/").pop() || "mind.mv2";
148
+ const backupPattern = new RegExp(`^${baseName.replace(".", "\\.")}\\.backup-\\d+$`);
149
+ const files = readdirSync(dir);
150
+ const backups = files.filter((f) => backupPattern.test(f)).map((f) => ({
151
+ name: f,
152
+ path: resolve(dir, f),
153
+ time: parseInt(f.split("-").pop() || "0", 10)
154
+ })).sort((a, b) => b.time - a.time);
155
+ for (let i = keepCount; i < backups.length; i++) {
156
+ try {
157
+ unlinkSync(backups[i].path);
158
+ console.error(`[memvid-mind] Pruned old backup: ${backups[i].name}`);
159
+ } catch {
160
+ }
161
+ }
162
+ } catch {
163
+ }
164
+ }
165
+ var sdkLoaded = false;
166
+ var use;
167
+ var create;
168
+ async function loadSDK() {
169
+ if (sdkLoaded) return;
170
+ const sdk = await import('@memvid/sdk');
171
+ use = sdk.use;
172
+ create = sdk.create;
173
+ sdkLoaded = true;
174
+ }
175
+ var OBSERVATION_TYPE_KEYS = [
176
+ "discovery",
177
+ "decision",
178
+ "problem",
179
+ "solution",
180
+ "pattern",
181
+ "warning",
182
+ "success",
183
+ "refactor",
184
+ "bugfix",
185
+ "feature"
186
+ ];
187
+ var OBSERVATION_TYPE_SET = new Set(OBSERVATION_TYPE_KEYS);
188
+ function emptyTypeCounts() {
189
+ return {
190
+ discovery: 0,
191
+ decision: 0,
192
+ problem: 0,
193
+ solution: 0,
194
+ pattern: 0,
195
+ warning: 0,
196
+ success: 0,
197
+ refactor: 0,
198
+ bugfix: 0,
199
+ feature: 0
200
+ };
201
+ }
202
+ var Mind = class _Mind {
203
+ memvid;
204
+ config;
205
+ memoryPath;
206
+ sessionId;
207
+ sessionStartTime;
208
+ sessionObservationCount = 0;
209
+ cachedStats = null;
210
+ cachedStatsFrameCount = -1;
211
+ initialized = false;
212
+ constructor(memvid, config, memoryPath) {
213
+ this.memvid = memvid;
214
+ this.config = config;
215
+ this.memoryPath = memoryPath;
216
+ this.sessionId = generateId();
217
+ this.sessionStartTime = Date.now();
218
+ }
219
+ /**
220
+ * Open or create a Mind instance
221
+ */
222
+ static async open(configOverrides = {}) {
223
+ await loadSDK();
224
+ const config = { ...DEFAULT_CONFIG, ...configOverrides };
225
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.OPENCODE_PROJECT_DIR || process.cwd();
226
+ const platform = detectPlatformFromEnv();
227
+ const optIn = process.env.MEMVID_PLATFORM_PATH_OPT_IN === "1";
228
+ const legacyFallbacks = config.memoryPath === DEFAULT_MEMORY_PATH ? [".claude/mind.mv2"] : [];
229
+ const pathPolicy = resolveMemoryPathPolicy({
230
+ projectDir,
231
+ platform,
232
+ defaultRelativePath: config.memoryPath,
233
+ legacyRelativePaths: legacyFallbacks,
234
+ platformRelativePath: process.env.MEMVID_PLATFORM_MEMORY_PATH,
235
+ platformOptIn: optIn
236
+ });
237
+ const memoryPath = pathPolicy.memoryPath;
238
+ const memoryDir = dirname(memoryPath);
239
+ await mkdir(memoryDir, { recursive: true });
240
+ let memvid;
241
+ const MAX_FILE_SIZE_MB = 100;
242
+ const lockPath = `${memoryPath}.lock`;
243
+ await withMemvidLock(lockPath, async () => {
244
+ if (!existsSync(memoryPath)) {
245
+ memvid = await create(memoryPath, "basic");
246
+ return;
247
+ }
248
+ const { statSync, renameSync: renameSync2, unlinkSync: unlinkSync2 } = await import('fs');
249
+ const fileSize = statSync(memoryPath).size;
250
+ const fileSizeMB = fileSize / (1024 * 1024);
251
+ if (fileSizeMB > MAX_FILE_SIZE_MB) {
252
+ console.error(`[memvid-mind] Memory file too large (${fileSizeMB.toFixed(1)}MB), likely corrupted. Creating fresh memory...`);
253
+ const backupPath = `${memoryPath}.backup-${Date.now()}`;
254
+ try {
255
+ renameSync2(memoryPath, backupPath);
256
+ } catch {
257
+ }
258
+ memvid = await create(memoryPath, "basic");
259
+ return;
260
+ }
261
+ try {
262
+ memvid = await use("basic", memoryPath);
263
+ } catch (openError) {
264
+ const errorMessage = openError instanceof Error ? openError.message : String(openError);
265
+ if (errorMessage.includes("Deserialization") || errorMessage.includes("UnexpectedVariant") || errorMessage.includes("Invalid") || errorMessage.includes("corrupt") || errorMessage.includes("validation failed") || errorMessage.includes("unable to recover") || errorMessage.includes("table of contents")) {
266
+ console.error("[memvid-mind] Memory file corrupted, creating fresh memory...");
267
+ const backupPath = `${memoryPath}.backup-${Date.now()}`;
268
+ try {
269
+ renameSync2(memoryPath, backupPath);
270
+ } catch {
271
+ try {
272
+ unlinkSync2(memoryPath);
273
+ } catch {
274
+ }
275
+ }
276
+ memvid = await create(memoryPath, "basic");
277
+ return;
278
+ }
279
+ throw openError;
280
+ }
281
+ });
282
+ const mind = new _Mind(memvid, config, memoryPath);
283
+ mind.initialized = true;
284
+ pruneBackups(memoryPath, 3);
285
+ if (config.debug) {
286
+ console.error(`[memvid-mind] Opened: ${memoryPath}`);
287
+ }
288
+ return mind;
289
+ }
290
+ async withLock(fn) {
291
+ const memoryPath = this.getMemoryPath();
292
+ const lockPath = `${memoryPath}.lock`;
293
+ return withMemvidLock(lockPath, fn);
294
+ }
295
+ /**
296
+ * Remember an observation
297
+ */
298
+ async remember(input) {
299
+ const observation = {
300
+ id: generateId(),
301
+ timestamp: Date.now(),
302
+ type: input.type,
303
+ tool: input.tool,
304
+ summary: input.summary,
305
+ content: input.content,
306
+ metadata: {
307
+ ...input.metadata,
308
+ sessionId: this.sessionId
309
+ }
310
+ };
311
+ const frameId = await this.withLock(async () => {
312
+ return this.memvid.put({
313
+ title: `[${observation.type}] ${observation.summary}`,
314
+ label: observation.type,
315
+ text: observation.content,
316
+ metadata: {
317
+ observationId: observation.id,
318
+ timestamp: observation.timestamp,
319
+ tool: observation.tool,
320
+ sessionId: this.sessionId,
321
+ ...observation.metadata
322
+ },
323
+ tags: [
324
+ observation.type,
325
+ `session:${this.sessionId}`,
326
+ observation.tool ? `tool:${observation.tool}` : void 0
327
+ ].filter(Boolean)
328
+ });
329
+ });
330
+ if (this.config.debug) {
331
+ console.error(`[memvid-mind] Remembered: ${observation.summary}`);
332
+ }
333
+ this.sessionObservationCount += 1;
334
+ this.cachedStats = null;
335
+ this.cachedStatsFrameCount = -1;
336
+ return frameId;
337
+ }
338
+ /**
339
+ * Search memories by query (uses fast lexical search)
340
+ */
341
+ async search(query, limit = 10) {
342
+ return this.withLock(async () => {
343
+ return this.searchUnlocked(query, limit);
344
+ });
345
+ }
346
+ async searchUnlocked(query, limit) {
347
+ const results = await this.memvid.find(query, { k: limit, mode: "lex" });
348
+ const frames = this.toSearchFrames(results);
349
+ return frames.map((frame) => {
350
+ const rawTags = Array.isArray(frame.tags) ? frame.tags.filter((tag) => typeof tag === "string") : [];
351
+ const prefixedToolTag = rawTags.find((tag) => tag.startsWith("tool:"));
352
+ const labels = Array.isArray(frame.labels) ? frame.labels.filter((label) => typeof label === "string") : [];
353
+ const metadata = frame.metadata && typeof frame.metadata === "object" ? frame.metadata : {};
354
+ const observationType = this.extractObservationType({
355
+ label: frame.label,
356
+ labels
357
+ }) || "discovery";
358
+ const legacyToolTag = rawTags.find((tag) => {
359
+ if (tag.startsWith("tool:") || tag.startsWith("session:")) {
360
+ return false;
361
+ }
362
+ if (!/[A-Z]/.test(tag)) {
363
+ return false;
364
+ }
365
+ return tag.toLowerCase() !== observationType;
366
+ });
367
+ const tool = typeof prefixedToolTag === "string" ? prefixedToolTag.replace(/^tool:/, "") : typeof metadata.tool === "string" ? metadata.tool : legacyToolTag;
368
+ const timestamp = this.normalizeTimestampMs(
369
+ metadata.timestamp || frame.timestamp || (typeof frame.created_at === "string" ? Date.parse(frame.created_at) : 0)
370
+ );
371
+ return {
372
+ observation: {
373
+ id: String(metadata.observationId || frame.frame_id || generateId()),
374
+ timestamp,
375
+ type: observationType,
376
+ tool,
377
+ summary: frame.title?.replace(/^\[.*?\]\s*/, "") || frame.snippet || "",
378
+ content: frame.text || frame.snippet || "",
379
+ metadata: {
380
+ ...metadata,
381
+ labels,
382
+ tags: rawTags
383
+ }
384
+ },
385
+ score: frame.score || 0,
386
+ snippet: frame.snippet || frame.text?.slice(0, 200) || ""
387
+ };
388
+ });
389
+ }
390
+ toTimelineFrames(timelineResult) {
391
+ return Array.isArray(timelineResult) ? timelineResult : timelineResult.frames || [];
392
+ }
393
+ toSearchFrames(searchResult) {
394
+ if (Array.isArray(searchResult?.hits)) {
395
+ return searchResult.hits;
396
+ }
397
+ if (Array.isArray(searchResult?.frames)) {
398
+ return searchResult.frames;
399
+ }
400
+ return [];
401
+ }
402
+ normalizeTimestampMs(value) {
403
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
404
+ return 0;
405
+ }
406
+ if (value < 4102444800) {
407
+ return Math.round(value * 1e3);
408
+ }
409
+ return Math.round(value);
410
+ }
411
+ parseSessionSummary(value) {
412
+ if (!value || typeof value !== "object") {
413
+ return null;
414
+ }
415
+ const candidate = value;
416
+ if (typeof candidate.id !== "string" || typeof candidate.startTime !== "number" || typeof candidate.endTime !== "number" || typeof candidate.observationCount !== "number" || typeof candidate.summary !== "string" || !Array.isArray(candidate.keyDecisions) || !Array.isArray(candidate.filesModified)) {
417
+ return null;
418
+ }
419
+ return {
420
+ id: candidate.id,
421
+ startTime: this.normalizeTimestampMs(candidate.startTime),
422
+ endTime: this.normalizeTimestampMs(candidate.endTime),
423
+ observationCount: Math.max(0, Math.trunc(candidate.observationCount)),
424
+ keyDecisions: candidate.keyDecisions.filter(
425
+ (decision) => typeof decision === "string"
426
+ ),
427
+ filesModified: candidate.filesModified.filter(
428
+ (file) => typeof file === "string"
429
+ ),
430
+ summary: candidate.summary
431
+ };
432
+ }
433
+ extractSessionSummary(frame) {
434
+ const fromMetadata = this.parseSessionSummary(frame.metadata);
435
+ if (fromMetadata) {
436
+ return fromMetadata;
437
+ }
438
+ if (typeof frame.text !== "string") {
439
+ return null;
440
+ }
441
+ try {
442
+ return this.parseSessionSummary(JSON.parse(frame.text));
443
+ } catch {
444
+ return null;
445
+ }
446
+ }
447
+ extractSessionId(frame) {
448
+ const tags = Array.isArray(frame?.tags) ? frame.tags.filter((tag) => typeof tag === "string") : [];
449
+ const sessionTag = tags.find((tag) => tag.startsWith("session:"));
450
+ if (sessionTag) {
451
+ return sessionTag.slice("session:".length);
452
+ }
453
+ const metadataSessionId = frame?.metadata?.sessionId;
454
+ if (typeof metadataSessionId === "string" && metadataSessionId.length > 0) {
455
+ return metadataSessionId;
456
+ }
457
+ if (frame?.label === "session") {
458
+ const summary = this.extractSessionSummary(frame);
459
+ if (summary) {
460
+ return summary.id;
461
+ }
462
+ }
463
+ return null;
464
+ }
465
+ extractObservationType(frame) {
466
+ if (Array.isArray(frame?.labels)) {
467
+ for (const value of frame.labels) {
468
+ if (typeof value === "string") {
469
+ const normalized = value.toLowerCase();
470
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
471
+ return normalized;
472
+ }
473
+ }
474
+ }
475
+ }
476
+ const label = typeof frame?.label === "string" ? frame.label : void 0;
477
+ if (label) {
478
+ const normalized = label.toLowerCase();
479
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
480
+ return normalized;
481
+ }
482
+ }
483
+ const metadataType = frame?.metadata?.type;
484
+ if (typeof metadataType === "string") {
485
+ const normalized = metadataType.toLowerCase();
486
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
487
+ return normalized;
488
+ }
489
+ }
490
+ return null;
491
+ }
492
+ extractPreviewFieldValues(preview, field) {
493
+ if (typeof preview !== "string" || preview.length === 0) {
494
+ return [];
495
+ }
496
+ const match = new RegExp(`(?:^|\\n)${field}:\\s*([^\\n]*)`, "i").exec(preview);
497
+ if (!match?.[1]) {
498
+ return [];
499
+ }
500
+ return match[1].split(/[^a-z0-9:_-]+/i).map((value) => value.trim()).filter(Boolean);
501
+ }
502
+ extractObservationTypeFromPreview(preview) {
503
+ const labels = this.extractPreviewFieldValues(preview, "labels");
504
+ const fromLabels = this.extractObservationType({ labels });
505
+ if (fromLabels) {
506
+ return fromLabels;
507
+ }
508
+ if (typeof preview !== "string" || preview.length === 0) {
509
+ return null;
510
+ }
511
+ const titleMatch = /(?:^|\n)title:\s*\[([^\]]+)\]/i.exec(preview);
512
+ if (!titleMatch?.[1]) {
513
+ return null;
514
+ }
515
+ const normalized = titleMatch[1].trim().toLowerCase();
516
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
517
+ return normalized;
518
+ }
519
+ return null;
520
+ }
521
+ parseLeadingJsonObject(text) {
522
+ const start = text.indexOf("{");
523
+ if (start < 0) {
524
+ return null;
525
+ }
526
+ let depth = 0;
527
+ let inString = false;
528
+ let escaped = false;
529
+ for (let i = start; i < text.length; i++) {
530
+ const ch = text[i];
531
+ if (inString) {
532
+ if (escaped) {
533
+ escaped = false;
534
+ } else if (ch === "\\") {
535
+ escaped = true;
536
+ } else if (ch === '"') {
537
+ inString = false;
538
+ }
539
+ continue;
540
+ }
541
+ if (ch === '"') {
542
+ inString = true;
543
+ continue;
544
+ }
545
+ if (ch === "{") {
546
+ depth += 1;
547
+ } else if (ch === "}") {
548
+ depth -= 1;
549
+ if (depth === 0) {
550
+ const candidate = text.slice(start, i + 1);
551
+ try {
552
+ return JSON.parse(candidate);
553
+ } catch {
554
+ return null;
555
+ }
556
+ }
557
+ }
558
+ }
559
+ return null;
560
+ }
561
+ extractSessionSummaryFromSearchHit(hit) {
562
+ if (typeof hit?.text !== "string") {
563
+ return null;
564
+ }
565
+ const parsed = this.parseLeadingJsonObject(hit.text);
566
+ return this.parseSessionSummary(parsed);
567
+ }
568
+ /**
569
+ * Ask the memory a question (uses fast lexical search)
570
+ */
571
+ async ask(question) {
572
+ return this.withLock(async () => {
573
+ const result = await this.memvid.ask(question, { k: 5, mode: "lex" });
574
+ return result.answer || "No relevant memories found.";
575
+ });
576
+ }
577
+ /**
578
+ * Get context for session start
579
+ */
580
+ async getContext(query) {
581
+ return this.withLock(async () => {
582
+ const timeline = await this.memvid.timeline({
583
+ limit: this.config.maxContextObservations,
584
+ reverse: true
585
+ });
586
+ const frames = this.toTimelineFrames(timeline);
587
+ const recentObservations = [];
588
+ const FRAME_INFO_BATCH_SIZE = 20;
589
+ for (let start = 0; start < frames.length; start += FRAME_INFO_BATCH_SIZE) {
590
+ const batch = frames.slice(start, start + FRAME_INFO_BATCH_SIZE);
591
+ const frameInfos = await Promise.all(batch.map(async (frame) => {
592
+ try {
593
+ return await this.memvid.getFrameInfo(frame.frame_id);
594
+ } catch {
595
+ return null;
596
+ }
597
+ }));
598
+ for (let index = 0; index < batch.length; index++) {
599
+ const frame = batch[index];
600
+ const frameInfo = frameInfos[index];
601
+ const labels = Array.isArray(frameInfo?.labels) ? frameInfo.labels : [];
602
+ const tags = Array.isArray(frameInfo?.tags) ? frameInfo.tags : [];
603
+ const metadata = frameInfo?.metadata && typeof frameInfo.metadata === "object" ? frameInfo.metadata : {};
604
+ const toolTag = tags.find((tag) => typeof tag === "string" && tag.startsWith("tool:"));
605
+ const ts = this.normalizeTimestampMs(frameInfo?.timestamp || frame.timestamp || 0);
606
+ const observationType = this.extractObservationType({
607
+ label: labels[0],
608
+ labels,
609
+ metadata
610
+ }) || "discovery";
611
+ recentObservations.push({
612
+ id: String(metadata.observationId || frame.metadata?.observationId || frame.frame_id),
613
+ timestamp: ts,
614
+ type: observationType,
615
+ tool: typeof toolTag === "string" ? toolTag.replace(/^tool:/, "") : typeof metadata.tool === "string" ? metadata.tool : void 0,
616
+ summary: frameInfo?.title?.replace(/^\[.*?\]\s*/, "") || frame.preview?.slice(0, 100) || "",
617
+ content: frame.preview || "",
618
+ metadata: {
619
+ ...metadata,
620
+ labels,
621
+ tags
622
+ }
623
+ });
624
+ }
625
+ }
626
+ let relevantMemories = [];
627
+ if (query) {
628
+ const searchResults = await this.searchUnlocked(query, 10);
629
+ relevantMemories = searchResults.map((r) => r.observation);
630
+ }
631
+ const summarySearch = await this.memvid.find("Session Summary", {
632
+ k: 20,
633
+ mode: "lex"
634
+ });
635
+ const summaryHits = this.toSearchFrames(summarySearch);
636
+ const seenSessionIds = /* @__PURE__ */ new Set();
637
+ const sessionSummaries = [];
638
+ for (const hit of summaryHits) {
639
+ const summary = this.extractSessionSummaryFromSearchHit(hit);
640
+ if (!summary || seenSessionIds.has(summary.id)) {
641
+ continue;
642
+ }
643
+ seenSessionIds.add(summary.id);
644
+ sessionSummaries.push(summary);
645
+ if (sessionSummaries.length >= 5) {
646
+ break;
647
+ }
648
+ }
649
+ let tokenCount = 0;
650
+ for (const obs of recentObservations) {
651
+ const text = `[${obs.type}] ${obs.summary}`;
652
+ const tokens = estimateTokens(text);
653
+ if (tokenCount + tokens > this.config.maxContextTokens) break;
654
+ tokenCount += tokens;
655
+ }
656
+ return {
657
+ recentObservations,
658
+ relevantMemories,
659
+ sessionSummaries,
660
+ tokenCount
661
+ };
662
+ });
663
+ }
664
+ /**
665
+ * Save a session summary
666
+ */
667
+ async saveSessionSummary(summary) {
668
+ return this.withLock(async () => {
669
+ const endTime = Date.now();
670
+ const sessionSummary = {
671
+ id: this.sessionId,
672
+ startTime: this.sessionStartTime,
673
+ endTime,
674
+ observationCount: this.sessionObservationCount,
675
+ keyDecisions: summary.keyDecisions.slice(0, 20),
676
+ filesModified: summary.filesModified.slice(0, 50),
677
+ summary: summary.summary
678
+ };
679
+ const frameId = await this.memvid.put({
680
+ title: `Session Summary: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
681
+ label: "session",
682
+ text: JSON.stringify(sessionSummary, null, 2),
683
+ metadata: {
684
+ ...sessionSummary,
685
+ sessionId: this.sessionId
686
+ },
687
+ tags: ["session", "summary", `session:${this.sessionId}`]
688
+ });
689
+ this.cachedStats = null;
690
+ this.cachedStatsFrameCount = -1;
691
+ return frameId;
692
+ });
693
+ }
694
+ /**
695
+ * Get memory statistics
696
+ */
697
+ async stats() {
698
+ return this.withLock(async () => {
699
+ const stats = await this.memvid.stats();
700
+ const totalFrames = Number(stats.frame_count) || 0;
701
+ if (this.cachedStats && this.cachedStatsFrameCount === totalFrames) {
702
+ return this.cachedStats;
703
+ }
704
+ const timeline = totalFrames > 0 ? await this.memvid.timeline({ limit: totalFrames, reverse: false }) : [];
705
+ const frames = this.toTimelineFrames(timeline);
706
+ const sessionIds = /* @__PURE__ */ new Set();
707
+ const topTypes = emptyTypeCounts();
708
+ let oldestMemory = 0;
709
+ let newestMemory = 0;
710
+ for (const frame of frames) {
711
+ const labels = this.extractPreviewFieldValues(frame.preview, "labels");
712
+ const tags = this.extractPreviewFieldValues(frame.preview, "tags");
713
+ const timestamp = this.normalizeTimestampMs(frame.timestamp || 0);
714
+ if (timestamp > 0) {
715
+ if (oldestMemory === 0 || timestamp < oldestMemory) {
716
+ oldestMemory = timestamp;
717
+ }
718
+ if (newestMemory === 0 || timestamp > newestMemory) {
719
+ newestMemory = timestamp;
720
+ }
721
+ }
722
+ const sessionId = this.extractSessionId({
723
+ ...frame,
724
+ labels,
725
+ tags
726
+ });
727
+ if (sessionId) {
728
+ sessionIds.add(sessionId);
729
+ }
730
+ const observationType = this.extractObservationType({
731
+ ...frame,
732
+ label: labels[0],
733
+ labels,
734
+ tags
735
+ }) || this.extractObservationTypeFromPreview(frame.preview);
736
+ if (observationType) {
737
+ topTypes[observationType] += 1;
738
+ }
739
+ }
740
+ const summarySearch = await this.memvid.find("Session Summary", {
741
+ k: 50,
742
+ mode: "lex"
743
+ });
744
+ const summaryHits = this.toSearchFrames(summarySearch);
745
+ for (const hit of summaryHits) {
746
+ const summary = this.extractSessionSummaryFromSearchHit(hit);
747
+ if (summary) {
748
+ sessionIds.add(summary.id);
749
+ }
750
+ }
751
+ const result = {
752
+ totalObservations: totalFrames,
753
+ totalSessions: sessionIds.size,
754
+ oldestMemory,
755
+ newestMemory,
756
+ fileSize: stats.size_bytes || 0,
757
+ topTypes
758
+ };
759
+ this.cachedStats = result;
760
+ this.cachedStatsFrameCount = totalFrames;
761
+ return result;
762
+ });
763
+ }
764
+ /**
765
+ * Get the session ID
766
+ */
767
+ getSessionId() {
768
+ return this.sessionId;
769
+ }
770
+ /**
771
+ * Get the memory file path
772
+ */
773
+ getMemoryPath() {
774
+ return this.memoryPath;
775
+ }
776
+ /**
777
+ * Check if initialized
778
+ */
779
+ isInitialized() {
780
+ return this.initialized;
781
+ }
782
+ };
783
+ var mindInstance = null;
784
+ async function getMind(config) {
785
+ if (!mindInstance) {
786
+ mindInstance = await Mind.open(config);
787
+ }
788
+ return mindInstance;
789
+ }
790
+
791
+ // src/platforms/registry.ts
792
+ var AdapterRegistry = class {
793
+ adapters = /* @__PURE__ */ new Map();
794
+ register(adapter) {
795
+ this.adapters.set(adapter.platform, adapter);
796
+ }
797
+ resolve(platform) {
798
+ return this.adapters.get(platform) || null;
799
+ }
800
+ listPlatforms() {
801
+ return [...this.adapters.keys()].sort();
802
+ }
803
+ };
804
+
805
+ // src/platforms/events.ts
806
+ function createEventId() {
807
+ return generateId();
808
+ }
809
+
810
+ // src/platforms/adapters/create-adapter.ts
811
+ var CONTRACT_VERSION = "1.0.0";
812
+ function createAdapter(platform) {
813
+ function projectContext(input) {
814
+ return {
815
+ platformProjectId: input.project_id,
816
+ canonicalPath: input.cwd,
817
+ cwd: input.cwd
818
+ };
819
+ }
820
+ return {
821
+ platform,
822
+ contractVersion: CONTRACT_VERSION,
823
+ normalizeSessionStart(input) {
824
+ return {
825
+ eventId: createEventId(),
826
+ eventType: "session_start",
827
+ platform,
828
+ contractVersion: input.contract_version?.trim() || CONTRACT_VERSION,
829
+ sessionId: input.session_id,
830
+ timestamp: Date.now(),
831
+ projectContext: projectContext(input),
832
+ payload: {
833
+ hookEventName: input.hook_event_name,
834
+ permissionMode: input.permission_mode,
835
+ transcriptPath: input.transcript_path
836
+ }
837
+ };
838
+ },
839
+ normalizeToolObservation(input) {
840
+ if (!input.tool_name) return null;
841
+ return {
842
+ eventId: createEventId(),
843
+ eventType: "tool_observation",
844
+ platform,
845
+ contractVersion: input.contract_version?.trim() || CONTRACT_VERSION,
846
+ sessionId: input.session_id,
847
+ timestamp: Date.now(),
848
+ projectContext: projectContext(input),
849
+ payload: {
850
+ toolName: input.tool_name,
851
+ toolInput: input.tool_input,
852
+ toolResponse: input.tool_response
853
+ }
854
+ };
855
+ },
856
+ normalizeSessionStop(input) {
857
+ return {
858
+ eventId: createEventId(),
859
+ eventType: "session_stop",
860
+ platform,
861
+ contractVersion: input.contract_version?.trim() || CONTRACT_VERSION,
862
+ sessionId: input.session_id,
863
+ timestamp: Date.now(),
864
+ projectContext: projectContext(input),
865
+ payload: {
866
+ transcriptPath: input.transcript_path
867
+ }
868
+ };
869
+ }
870
+ };
871
+ }
872
+
873
+ // src/platforms/adapters/claude.ts
874
+ var claudeAdapter = createAdapter("claude");
875
+
876
+ // src/platforms/adapters/opencode.ts
877
+ var opencodeAdapter = createAdapter("opencode");
878
+
879
+ // src/platforms/contract.ts
880
+ var SUPPORTED_ADAPTER_CONTRACT_MAJOR = 1;
881
+ var SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
882
+ function parseContractMajor(version) {
883
+ const match = SEMVER_PATTERN.exec(version.trim());
884
+ if (!match) {
885
+ return null;
886
+ }
887
+ return Number(match[1]);
888
+ }
889
+ function validateAdapterContractVersion(version, supportedMajor = SUPPORTED_ADAPTER_CONTRACT_MAJOR) {
890
+ const adapterMajor = parseContractMajor(version);
891
+ if (adapterMajor === null) {
892
+ return {
893
+ compatible: false,
894
+ supportedMajor,
895
+ adapterMajor: null,
896
+ reason: "invalid_contract_version"
897
+ };
898
+ }
899
+ if (adapterMajor !== supportedMajor) {
900
+ return {
901
+ compatible: false,
902
+ supportedMajor,
903
+ adapterMajor,
904
+ reason: "incompatible_contract_major"
905
+ };
906
+ }
907
+ return {
908
+ compatible: true,
909
+ supportedMajor,
910
+ adapterMajor
911
+ };
912
+ }
913
+
914
+ // src/platforms/diagnostics.ts
915
+ var DIAGNOSTIC_RETENTION_DAYS = 30;
916
+ var DAY_MS = 24 * 60 * 60 * 1e3;
917
+ var DIAGNOSTIC_FILE_NAME = "platform-diagnostics.json";
918
+ var TEST_DIAGNOSTIC_FILE_NAME = `memvid-platform-diagnostics-${process.pid}.json`;
919
+ function sanitizeFieldNames(fieldNames) {
920
+ if (!fieldNames || fieldNames.length === 0) {
921
+ return void 0;
922
+ }
923
+ return [...new Set(fieldNames)].slice(0, 20);
924
+ }
925
+ function resolveDiagnosticStorePath() {
926
+ const explicitPath = process.env.MEMVID_DIAGNOSTIC_PATH?.trim();
927
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
928
+ if (explicitPath) {
929
+ return resolve(projectDir, explicitPath);
930
+ }
931
+ if (process.env.VITEST) {
932
+ return resolve(tmpdir(), TEST_DIAGNOSTIC_FILE_NAME);
933
+ }
934
+ return resolve(projectDir, ".claude", DIAGNOSTIC_FILE_NAME);
935
+ }
936
+ function isDiagnosticRecord(value) {
937
+ if (!value || typeof value !== "object") {
938
+ return false;
939
+ }
940
+ const record = value;
941
+ return typeof record.diagnosticId === "string" && typeof record.timestamp === "number" && typeof record.platform === "string" && typeof record.errorType === "string" && (record.fieldNames === void 0 || Array.isArray(record.fieldNames) && record.fieldNames.every((name) => typeof name === "string")) && (record.severity === "warning" || record.severity === "error") && record.redacted === true && typeof record.retentionDays === "number" && typeof record.expiresAt === "number";
942
+ }
943
+ function pruneExpired(records, now = Date.now()) {
944
+ return records.filter((record) => record.expiresAt > now);
945
+ }
946
+ var DiagnosticPersistence = class {
947
+ filePath;
948
+ constructor(filePath) {
949
+ this.filePath = filePath;
950
+ }
951
+ append(record, now = Date.now()) {
952
+ this.withFileLock(() => {
953
+ const latest = this.loadFromDisk();
954
+ const next = pruneExpired([...latest, record], now);
955
+ this.persist(next);
956
+ });
957
+ }
958
+ list(now = Date.now()) {
959
+ return this.withFileLock(() => {
960
+ const latest = this.loadFromDisk();
961
+ const pruned = pruneExpired(latest, now);
962
+ if (pruned.length !== latest.length) {
963
+ this.persist(pruned);
964
+ }
965
+ return [...pruned];
966
+ });
967
+ }
968
+ loadFromDisk() {
969
+ if (!existsSync(this.filePath)) {
970
+ return [];
971
+ }
972
+ try {
973
+ const raw = readFileSync(this.filePath, "utf-8").trim();
974
+ if (!raw) {
975
+ return [];
976
+ }
977
+ const parsed = JSON.parse(raw);
978
+ if (!Array.isArray(parsed)) {
979
+ return [];
980
+ }
981
+ return parsed.filter(isDiagnosticRecord);
982
+ } catch {
983
+ return [];
984
+ }
985
+ }
986
+ withFileLock(fn) {
987
+ mkdirSync(dirname(this.filePath), { recursive: true });
988
+ const release = lockfile.lockSync(this.filePath, { realpath: false });
989
+ try {
990
+ return fn();
991
+ } finally {
992
+ release();
993
+ }
994
+ }
995
+ persist(records) {
996
+ mkdirSync(dirname(this.filePath), { recursive: true });
997
+ const tmpPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
998
+ try {
999
+ writeFileSync(tmpPath, `${JSON.stringify(records, null, 2)}
1000
+ `, "utf-8");
1001
+ try {
1002
+ renameSync(tmpPath, this.filePath);
1003
+ } catch {
1004
+ rmSync(this.filePath, { force: true });
1005
+ renameSync(tmpPath, this.filePath);
1006
+ }
1007
+ } finally {
1008
+ rmSync(tmpPath, { force: true });
1009
+ }
1010
+ }
1011
+ };
1012
+ var persistence = null;
1013
+ var persistenceFilePath = null;
1014
+ var warnedPathChange = false;
1015
+ function getDiagnosticPersistence() {
1016
+ const resolvedPath = resolveDiagnosticStorePath();
1017
+ if (!persistence) {
1018
+ persistence = new DiagnosticPersistence(resolvedPath);
1019
+ persistenceFilePath = resolvedPath;
1020
+ warnedPathChange = false;
1021
+ return persistence;
1022
+ }
1023
+ if (persistenceFilePath && persistenceFilePath !== resolvedPath && !warnedPathChange) {
1024
+ warnedPathChange = true;
1025
+ console.error(
1026
+ `[memvid-mind] Diagnostic store path changed from "${persistenceFilePath}" to "${resolvedPath}" after initialization; continuing with the original path.`
1027
+ );
1028
+ }
1029
+ return persistence;
1030
+ }
1031
+ function createRedactedDiagnostic(input) {
1032
+ const timestamp = input.now ?? Date.now();
1033
+ const diagnostic = {
1034
+ diagnosticId: generateId(),
1035
+ timestamp,
1036
+ platform: input.platform,
1037
+ errorType: input.errorType,
1038
+ fieldNames: sanitizeFieldNames(input.fieldNames),
1039
+ severity: input.severity ?? "warning",
1040
+ redacted: true,
1041
+ retentionDays: DIAGNOSTIC_RETENTION_DAYS,
1042
+ expiresAt: timestamp + DIAGNOSTIC_RETENTION_DAYS * DAY_MS
1043
+ };
1044
+ try {
1045
+ getDiagnosticPersistence().append(diagnostic);
1046
+ } catch {
1047
+ }
1048
+ return diagnostic;
1049
+ }
1050
+ function resolveCanonicalProjectPath(context) {
1051
+ if (context.canonicalPath) {
1052
+ return resolve(context.canonicalPath);
1053
+ }
1054
+ if (context.cwd) {
1055
+ return resolve(context.cwd);
1056
+ }
1057
+ return void 0;
1058
+ }
1059
+ function resolveProjectIdentityKey(context) {
1060
+ if (context.platformProjectId && context.platformProjectId.trim().length > 0) {
1061
+ return {
1062
+ key: context.platformProjectId.trim(),
1063
+ source: "platform_project_id",
1064
+ canonicalPath: resolveCanonicalProjectPath(context)
1065
+ };
1066
+ }
1067
+ const canonicalPath = resolveCanonicalProjectPath(context);
1068
+ if (canonicalPath) {
1069
+ return {
1070
+ key: canonicalPath,
1071
+ source: "canonical_path",
1072
+ canonicalPath
1073
+ };
1074
+ }
1075
+ return {
1076
+ key: null,
1077
+ source: "unresolved"
1078
+ };
1079
+ }
1080
+
1081
+ // src/platforms/pipeline.ts
1082
+ function skipWithDiagnostic(platform, errorType, fieldNames) {
1083
+ return {
1084
+ skipped: true,
1085
+ reason: errorType,
1086
+ diagnostic: createRedactedDiagnostic({
1087
+ platform,
1088
+ errorType,
1089
+ fieldNames,
1090
+ severity: "warning"
1091
+ })
1092
+ };
1093
+ }
1094
+ function processPlatformEvent(event) {
1095
+ const contractValidation = validateAdapterContractVersion(
1096
+ event.contractVersion,
1097
+ SUPPORTED_ADAPTER_CONTRACT_MAJOR
1098
+ );
1099
+ if (!contractValidation.compatible) {
1100
+ return skipWithDiagnostic(event.platform, contractValidation.reason ?? "incompatible_contract", ["contractVersion"]);
1101
+ }
1102
+ const identity = resolveProjectIdentityKey(event.projectContext);
1103
+ if (!identity.key) {
1104
+ return skipWithDiagnostic(event.platform, "missing_project_identity", [
1105
+ "platformProjectId",
1106
+ "canonicalPath",
1107
+ "cwd"
1108
+ ]);
1109
+ }
1110
+ return {
1111
+ skipped: false,
1112
+ projectIdentityKey: identity.key
1113
+ };
1114
+ }
1115
+
1116
+ // src/platforms/index.ts
1117
+ var defaultRegistry = null;
1118
+ function getDefaultAdapterRegistry() {
1119
+ if (!defaultRegistry) {
1120
+ const registry = new AdapterRegistry();
1121
+ registry.register(claudeAdapter);
1122
+ registry.register(opencodeAdapter);
1123
+ defaultRegistry = Object.freeze({
1124
+ resolve: (platform) => registry.resolve(platform),
1125
+ listPlatforms: () => registry.listPlatforms()
1126
+ });
1127
+ }
1128
+ return defaultRegistry;
1129
+ }
1130
+ var MIN_OBSERVATIONS_FOR_SUMMARY = 3;
1131
+ function hasRepoMarker(path) {
1132
+ return existsSync(join(path, ".git")) || existsSync(join(path, "package.json"));
1133
+ }
1134
+ function findNearestRepoRoot(startPath) {
1135
+ let current = resolve(startPath);
1136
+ while (true) {
1137
+ if (hasRepoMarker(current)) {
1138
+ return current;
1139
+ }
1140
+ const parent = dirname(current);
1141
+ if (parent === current) {
1142
+ return null;
1143
+ }
1144
+ current = parent;
1145
+ }
1146
+ }
1147
+ function findRepoRoot(memoryPath) {
1148
+ const candidates = [
1149
+ process.env.REPO_ROOT,
1150
+ process.env.CLAUDE_PROJECT_DIR,
1151
+ process.env.OPENCODE_PROJECT_DIR,
1152
+ dirname(memoryPath),
1153
+ process.cwd()
1154
+ ].filter((candidate) => typeof candidate === "string" && candidate.trim().length > 0).map((candidate) => resolve(candidate));
1155
+ for (const candidate of candidates) {
1156
+ const repoRoot = findNearestRepoRoot(candidate);
1157
+ if (repoRoot) {
1158
+ return repoRoot;
1159
+ }
1160
+ }
1161
+ return resolve(dirname(memoryPath));
1162
+ }
1163
+ async function captureFileChanges(mind) {
1164
+ try {
1165
+ const memoryPath = mind.getMemoryPath();
1166
+ const workDir = findRepoRoot(memoryPath);
1167
+ const allChangedFiles = [];
1168
+ let gitDiffContent = "";
1169
+ try {
1170
+ const diffNames = execSync("git diff --name-only HEAD 2>/dev/null || git diff --name-only 2>/dev/null || echo ''", {
1171
+ cwd: workDir,
1172
+ encoding: "utf-8",
1173
+ timeout: 3e3,
1174
+ stdio: ["pipe", "pipe", "pipe"]
1175
+ }).trim();
1176
+ const stagedNames = execSync("git diff --cached --name-only 2>/dev/null || echo ''", {
1177
+ cwd: workDir,
1178
+ encoding: "utf-8",
1179
+ timeout: 3e3,
1180
+ stdio: ["pipe", "pipe", "pipe"]
1181
+ }).trim();
1182
+ const gitFiles = [.../* @__PURE__ */ new Set([
1183
+ ...diffNames.split("\n").filter(Boolean),
1184
+ ...stagedNames.split("\n").filter(Boolean)
1185
+ ])];
1186
+ allChangedFiles.push(...gitFiles);
1187
+ if (gitFiles.length > 0) {
1188
+ try {
1189
+ gitDiffContent = execSync("git diff HEAD --stat 2>/dev/null | head -30", {
1190
+ cwd: workDir,
1191
+ encoding: "utf-8",
1192
+ timeout: 3e3,
1193
+ stdio: ["pipe", "pipe", "pipe"]
1194
+ }).trim();
1195
+ } catch {
1196
+ }
1197
+ }
1198
+ } catch {
1199
+ }
1200
+ try {
1201
+ const recentFiles = execSync(
1202
+ `find . -maxdepth 4 -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -o -name "*.md" -o -name "*.json" -o -name "*.py" -o -name "*.rs" \\) -mmin -30 ! -path "*/node_modules/*" ! -path "*/.git/*" ! -path "*/dist/*" ! -path "*/build/*" ! -path "*/.next/*" ! -path "*/target/*" 2>/dev/null | head -30`,
1203
+ {
1204
+ cwd: workDir,
1205
+ encoding: "utf-8",
1206
+ timeout: 5e3,
1207
+ stdio: ["pipe", "pipe", "pipe"]
1208
+ }
1209
+ ).trim();
1210
+ const recentFilesList = recentFiles.split("\n").filter(Boolean).map((f) => f.replace(/^\.\//, ""));
1211
+ for (const file of recentFilesList) {
1212
+ if (!allChangedFiles.includes(file)) {
1213
+ allChangedFiles.push(file);
1214
+ }
1215
+ }
1216
+ } catch {
1217
+ }
1218
+ if (allChangedFiles.length === 0) {
1219
+ debug("No file changes detected");
1220
+ return;
1221
+ }
1222
+ debug(`Capturing ${allChangedFiles.length} changed files`);
1223
+ const contentParts = [`## Files Modified This Session
1224
+
1225
+ ${allChangedFiles.map((f) => `- ${f}`).join("\n")}`];
1226
+ if (gitDiffContent) {
1227
+ contentParts.push(`
1228
+ ## Git Changes Summary
1229
+ \`\`\`
1230
+ ${gitDiffContent}
1231
+ \`\`\``);
1232
+ }
1233
+ await mind.remember({
1234
+ type: "refactor",
1235
+ summary: `Session edits: ${allChangedFiles.length} file(s) modified`,
1236
+ content: contentParts.join("\n"),
1237
+ tool: "FileChanges",
1238
+ metadata: {
1239
+ files: allChangedFiles,
1240
+ fileCount: allChangedFiles.length,
1241
+ captureMethod: "git-diff-plus-recent"
1242
+ }
1243
+ });
1244
+ for (const file of allChangedFiles) {
1245
+ const fileName = file.split("/").pop() || file;
1246
+ const isImportant = /^(README|CHANGELOG|package\.json|Cargo\.toml|\.env)/i.test(fileName);
1247
+ if (isImportant) {
1248
+ await mind.remember({
1249
+ type: "refactor",
1250
+ summary: `Modified ${fileName}`,
1251
+ content: `File edited: ${file}
1252
+ This file was modified during the session.`,
1253
+ tool: "FileEdit",
1254
+ metadata: {
1255
+ files: [file],
1256
+ fileName
1257
+ }
1258
+ });
1259
+ debug(`Stored individual edit: ${fileName}`);
1260
+ }
1261
+ }
1262
+ debug(`Stored file changes: ${allChangedFiles.length} files`);
1263
+ } catch (error) {
1264
+ debug(`Failed to capture file changes: ${error}`);
1265
+ }
1266
+ }
1267
+ async function runStopHook() {
1268
+ try {
1269
+ const input = await readStdin();
1270
+ const hookInput = JSON.parse(input);
1271
+ debug(`Session stopping: ${hookInput.session_id}`);
1272
+ const platform = detectPlatform(hookInput);
1273
+ const adapter = getDefaultAdapterRegistry().resolve(platform);
1274
+ if (!adapter) {
1275
+ debug(`Unsupported platform at stop hook: ${platform}`);
1276
+ writeOutput({ continue: true });
1277
+ return;
1278
+ }
1279
+ const normalizedStop = adapter.normalizeSessionStop(hookInput);
1280
+ const pipelineResult = processPlatformEvent(normalizedStop);
1281
+ if (pipelineResult.skipped) {
1282
+ debug(`Skipping stop processing: ${pipelineResult.reason}`);
1283
+ writeOutput({ continue: true });
1284
+ return;
1285
+ }
1286
+ const mind = await getMind();
1287
+ const stats = await mind.stats();
1288
+ await captureFileChanges(mind);
1289
+ let transcriptContent = "";
1290
+ if (hookInput.transcript_path) {
1291
+ try {
1292
+ await access(hookInput.transcript_path, constants.R_OK);
1293
+ transcriptContent = await readFile(hookInput.transcript_path, "utf-8");
1294
+ } catch {
1295
+ }
1296
+ }
1297
+ const context = await mind.getContext();
1298
+ const sessionObservations = context.recentObservations.filter(
1299
+ (obs) => obs.metadata?.sessionId === mind.getSessionId()
1300
+ );
1301
+ if (sessionObservations.length >= MIN_OBSERVATIONS_FOR_SUMMARY) {
1302
+ const summary = generateSessionSummary(
1303
+ sessionObservations,
1304
+ transcriptContent
1305
+ );
1306
+ await mind.saveSessionSummary(summary);
1307
+ debug(
1308
+ `Session summary saved: ${summary.keyDecisions.length} decisions, ${summary.filesModified.length} files`
1309
+ );
1310
+ }
1311
+ debug(
1312
+ `Session complete. Total memories: ${stats.totalObservations}, File: ${mind.getMemoryPath()}`
1313
+ );
1314
+ const output = {
1315
+ continue: true
1316
+ };
1317
+ writeOutput(output);
1318
+ } catch (error) {
1319
+ debug(`Error: ${error}`);
1320
+ writeOutput({ continue: true });
1321
+ }
1322
+ }
1323
+ function generateSessionSummary(observations, transcript) {
1324
+ const keyDecisions = [];
1325
+ const filesModified = /* @__PURE__ */ new Set();
1326
+ for (const obs of observations) {
1327
+ if (obs.type === "decision" || obs.summary.toLowerCase().includes("chose") || obs.summary.toLowerCase().includes("decided")) {
1328
+ keyDecisions.push(obs.summary);
1329
+ }
1330
+ const files = obs.metadata?.files;
1331
+ if (files) {
1332
+ files.forEach((f) => filesModified.add(f));
1333
+ }
1334
+ }
1335
+ if (transcript) {
1336
+ const filePatterns = [
1337
+ /(?:Read|Edit|Write)[^"]*"([^"]+)"/g,
1338
+ /file_path["\s:]+([^\s"]+)/g
1339
+ ];
1340
+ for (const pattern of filePatterns) {
1341
+ let match;
1342
+ while ((match = pattern.exec(transcript)) !== null) {
1343
+ const path = match[1];
1344
+ if (path && !path.includes("node_modules") && !path.startsWith(".")) {
1345
+ filesModified.add(path);
1346
+ }
1347
+ }
1348
+ }
1349
+ }
1350
+ const typeCounts = {};
1351
+ for (const obs of observations) {
1352
+ typeCounts[obs.type] = (typeCounts[obs.type] || 0) + 1;
1353
+ }
1354
+ const summaryParts = [];
1355
+ if (typeCounts.feature) {
1356
+ summaryParts.push(`Added ${typeCounts.feature} feature(s)`);
1357
+ }
1358
+ if (typeCounts.bugfix) {
1359
+ summaryParts.push(`Fixed ${typeCounts.bugfix} bug(s)`);
1360
+ }
1361
+ if (typeCounts.refactor) {
1362
+ summaryParts.push(`Refactored ${typeCounts.refactor} item(s)`);
1363
+ }
1364
+ if (typeCounts.discovery) {
1365
+ summaryParts.push(`Made ${typeCounts.discovery} discovery(ies)`);
1366
+ }
1367
+ if (typeCounts.problem) {
1368
+ summaryParts.push(`Encountered ${typeCounts.problem} problem(s)`);
1369
+ }
1370
+ if (typeCounts.solution) {
1371
+ summaryParts.push(`Found ${typeCounts.solution} solution(s)`);
1372
+ }
1373
+ const summary = summaryParts.length > 0 ? summaryParts.join(". ") + "." : `Session with ${observations.length} observations.`;
1374
+ return {
1375
+ keyDecisions: keyDecisions.slice(0, 10),
1376
+ filesModified: Array.from(filesModified).slice(0, 20),
1377
+ summary
1378
+ };
1379
+ }
1380
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
1381
+ void runStopHook();
1382
+ }
1383
+
1384
+ export { runStopHook };
1385
+ //# sourceMappingURL=stop.js.map
1386
+ //# sourceMappingURL=stop.js.map