@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,1643 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readdirSync, unlinkSync, readFileSync, mkdirSync, writeFileSync, renameSync, rmSync } from 'fs';
3
+ import { dirname, resolve, isAbsolute, relative, sep } from 'path';
4
+ import { mkdir, open } from 'fs/promises';
5
+ import { randomBytes } from 'crypto';
6
+ import lockfile from 'proper-lockfile';
7
+ import { tmpdir } from 'os';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ // src/types.ts
11
+ var DEFAULT_MEMORY_PATH = ".agent-brain/mind.mv2";
12
+ var DEFAULT_CONFIG = {
13
+ memoryPath: DEFAULT_MEMORY_PATH,
14
+ maxContextObservations: 20,
15
+ maxContextTokens: 2e3,
16
+ autoCompress: true,
17
+ minConfidence: 0.6,
18
+ debug: false
19
+ };
20
+ function generateId() {
21
+ return randomBytes(8).toString("hex");
22
+ }
23
+ function estimateTokens(text) {
24
+ return Math.ceil(text.length / 4);
25
+ }
26
+ async function readStdin() {
27
+ const chunks = [];
28
+ return new Promise((resolve5, reject) => {
29
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
30
+ process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
31
+ process.stdin.on("error", reject);
32
+ });
33
+ }
34
+ function writeOutput(output) {
35
+ console.log(JSON.stringify(output));
36
+ process.exit(0);
37
+ }
38
+ function debug(message) {
39
+ if (process.env.MEMVID_MIND_DEBUG === "1") {
40
+ console.error(`[memvid-mind] ${message}`);
41
+ }
42
+ }
43
+ function classifyObservationType(toolName, output) {
44
+ const lowerOutput = output.toLowerCase();
45
+ if (lowerOutput.includes("error") || lowerOutput.includes("failed") || lowerOutput.includes("exception")) {
46
+ return "problem";
47
+ }
48
+ if (lowerOutput.includes("success") || lowerOutput.includes("passed") || lowerOutput.includes("completed")) {
49
+ return "success";
50
+ }
51
+ if (lowerOutput.includes("warning") || lowerOutput.includes("deprecated")) {
52
+ return "warning";
53
+ }
54
+ switch (toolName) {
55
+ case "Read":
56
+ case "Glob":
57
+ case "Grep":
58
+ return "discovery";
59
+ case "Edit":
60
+ if (lowerOutput.includes("fix") || lowerOutput.includes("bug")) {
61
+ return "bugfix";
62
+ }
63
+ return "refactor";
64
+ case "Write":
65
+ return "feature";
66
+ default:
67
+ return "discovery";
68
+ }
69
+ }
70
+ var LOCK_OPTIONS = {
71
+ stale: 3e4,
72
+ retries: {
73
+ retries: 1e3,
74
+ minTimeout: 5,
75
+ maxTimeout: 50
76
+ }
77
+ };
78
+ async function withMemvidLock(lockPath, fn) {
79
+ await mkdir(dirname(lockPath), { recursive: true });
80
+ const handle = await open(lockPath, "a");
81
+ await handle.close();
82
+ const release = await lockfile.lock(lockPath, LOCK_OPTIONS);
83
+ try {
84
+ return await fn();
85
+ } finally {
86
+ await release();
87
+ }
88
+ }
89
+ function defaultPlatformRelativePath(platform) {
90
+ const normalizedPlatform = platform.trim().toLowerCase();
91
+ const safePlatform = normalizedPlatform.replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "") || "unknown";
92
+ return `.agent-brain/mind-${safePlatform}.mv2`;
93
+ }
94
+ function resolveInsideProject(projectDir, candidatePath) {
95
+ if (isAbsolute(candidatePath)) {
96
+ return resolve(candidatePath);
97
+ }
98
+ const root = resolve(projectDir);
99
+ const resolved = resolve(root, candidatePath);
100
+ const rel = relative(root, resolved);
101
+ if (rel === ".." || rel.startsWith(`..${sep}`)) {
102
+ throw new Error("Resolved memory path must stay inside projectDir");
103
+ }
104
+ return resolved;
105
+ }
106
+ function resolveMemoryPathPolicy(input) {
107
+ const mode = input.platformOptIn ? "platform_opt_in" : "legacy_first";
108
+ const canonicalRelativePath = input.platformOptIn ? input.platformRelativePath || defaultPlatformRelativePath(input.platform) : input.defaultRelativePath;
109
+ const canonicalPath = resolveInsideProject(input.projectDir, canonicalRelativePath);
110
+ if (existsSync(canonicalPath)) {
111
+ return {
112
+ mode,
113
+ memoryPath: canonicalPath,
114
+ canonicalPath
115
+ };
116
+ }
117
+ const fallbackPaths = (input.legacyRelativePaths || []).map((relativePath) => resolveInsideProject(input.projectDir, relativePath));
118
+ for (const fallbackPath of fallbackPaths) {
119
+ if (existsSync(fallbackPath)) {
120
+ return {
121
+ mode,
122
+ memoryPath: fallbackPath,
123
+ canonicalPath,
124
+ migrationSuggestion: {
125
+ fromPath: fallbackPath,
126
+ toPath: canonicalPath
127
+ }
128
+ };
129
+ }
130
+ }
131
+ if (input.platformOptIn) {
132
+ return {
133
+ mode: "platform_opt_in",
134
+ memoryPath: canonicalPath,
135
+ canonicalPath
136
+ };
137
+ }
138
+ return {
139
+ mode: "legacy_first",
140
+ memoryPath: canonicalPath,
141
+ canonicalPath
142
+ };
143
+ }
144
+
145
+ // src/platforms/platform-detector.ts
146
+ function normalizePlatform(value) {
147
+ if (!value) return void 0;
148
+ const normalized = value.trim().toLowerCase();
149
+ return normalized.length > 0 ? normalized : void 0;
150
+ }
151
+ function detectPlatformFromEnv() {
152
+ const explicitFromEnv = normalizePlatform(process.env.MEMVID_PLATFORM);
153
+ if (explicitFromEnv) {
154
+ return explicitFromEnv;
155
+ }
156
+ if (process.env.OPENCODE === "1") {
157
+ return "opencode";
158
+ }
159
+ return "claude";
160
+ }
161
+ function detectPlatform(input) {
162
+ const explicitFromHook = normalizePlatform(input.platform);
163
+ if (explicitFromHook) {
164
+ return explicitFromHook;
165
+ }
166
+ return detectPlatformFromEnv();
167
+ }
168
+
169
+ // src/core/mind.ts
170
+ function pruneBackups(memoryPath, keepCount) {
171
+ try {
172
+ const dir = dirname(memoryPath);
173
+ const baseName = memoryPath.split("/").pop() || "mind.mv2";
174
+ const backupPattern = new RegExp(`^${baseName.replace(".", "\\.")}\\.backup-\\d+$`);
175
+ const files = readdirSync(dir);
176
+ const backups = files.filter((f) => backupPattern.test(f)).map((f) => ({
177
+ name: f,
178
+ path: resolve(dir, f),
179
+ time: parseInt(f.split("-").pop() || "0", 10)
180
+ })).sort((a, b) => b.time - a.time);
181
+ for (let i = keepCount; i < backups.length; i++) {
182
+ try {
183
+ unlinkSync(backups[i].path);
184
+ console.error(`[memvid-mind] Pruned old backup: ${backups[i].name}`);
185
+ } catch {
186
+ }
187
+ }
188
+ } catch {
189
+ }
190
+ }
191
+ var sdkLoaded = false;
192
+ var use;
193
+ var create;
194
+ async function loadSDK() {
195
+ if (sdkLoaded) return;
196
+ const sdk = await import('@memvid/sdk');
197
+ use = sdk.use;
198
+ create = sdk.create;
199
+ sdkLoaded = true;
200
+ }
201
+ var OBSERVATION_TYPE_KEYS = [
202
+ "discovery",
203
+ "decision",
204
+ "problem",
205
+ "solution",
206
+ "pattern",
207
+ "warning",
208
+ "success",
209
+ "refactor",
210
+ "bugfix",
211
+ "feature"
212
+ ];
213
+ var OBSERVATION_TYPE_SET = new Set(OBSERVATION_TYPE_KEYS);
214
+ function emptyTypeCounts() {
215
+ return {
216
+ discovery: 0,
217
+ decision: 0,
218
+ problem: 0,
219
+ solution: 0,
220
+ pattern: 0,
221
+ warning: 0,
222
+ success: 0,
223
+ refactor: 0,
224
+ bugfix: 0,
225
+ feature: 0
226
+ };
227
+ }
228
+ var Mind = class _Mind {
229
+ memvid;
230
+ config;
231
+ memoryPath;
232
+ sessionId;
233
+ sessionStartTime;
234
+ sessionObservationCount = 0;
235
+ cachedStats = null;
236
+ cachedStatsFrameCount = -1;
237
+ initialized = false;
238
+ constructor(memvid, config, memoryPath) {
239
+ this.memvid = memvid;
240
+ this.config = config;
241
+ this.memoryPath = memoryPath;
242
+ this.sessionId = generateId();
243
+ this.sessionStartTime = Date.now();
244
+ }
245
+ /**
246
+ * Open or create a Mind instance
247
+ */
248
+ static async open(configOverrides = {}) {
249
+ await loadSDK();
250
+ const config = { ...DEFAULT_CONFIG, ...configOverrides };
251
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.OPENCODE_PROJECT_DIR || process.cwd();
252
+ const platform = detectPlatformFromEnv();
253
+ const optIn = process.env.MEMVID_PLATFORM_PATH_OPT_IN === "1";
254
+ const legacyFallbacks = config.memoryPath === DEFAULT_MEMORY_PATH ? [".claude/mind.mv2"] : [];
255
+ const pathPolicy = resolveMemoryPathPolicy({
256
+ projectDir,
257
+ platform,
258
+ defaultRelativePath: config.memoryPath,
259
+ legacyRelativePaths: legacyFallbacks,
260
+ platformRelativePath: process.env.MEMVID_PLATFORM_MEMORY_PATH,
261
+ platformOptIn: optIn
262
+ });
263
+ const memoryPath = pathPolicy.memoryPath;
264
+ const memoryDir = dirname(memoryPath);
265
+ await mkdir(memoryDir, { recursive: true });
266
+ let memvid;
267
+ const MAX_FILE_SIZE_MB = 100;
268
+ const lockPath = `${memoryPath}.lock`;
269
+ await withMemvidLock(lockPath, async () => {
270
+ if (!existsSync(memoryPath)) {
271
+ memvid = await create(memoryPath, "basic");
272
+ return;
273
+ }
274
+ const { statSync, renameSync: renameSync2, unlinkSync: unlinkSync2 } = await import('fs');
275
+ const fileSize = statSync(memoryPath).size;
276
+ const fileSizeMB = fileSize / (1024 * 1024);
277
+ if (fileSizeMB > MAX_FILE_SIZE_MB) {
278
+ console.error(`[memvid-mind] Memory file too large (${fileSizeMB.toFixed(1)}MB), likely corrupted. Creating fresh memory...`);
279
+ const backupPath = `${memoryPath}.backup-${Date.now()}`;
280
+ try {
281
+ renameSync2(memoryPath, backupPath);
282
+ } catch {
283
+ }
284
+ memvid = await create(memoryPath, "basic");
285
+ return;
286
+ }
287
+ try {
288
+ memvid = await use("basic", memoryPath);
289
+ } catch (openError) {
290
+ const errorMessage = openError instanceof Error ? openError.message : String(openError);
291
+ 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")) {
292
+ console.error("[memvid-mind] Memory file corrupted, creating fresh memory...");
293
+ const backupPath = `${memoryPath}.backup-${Date.now()}`;
294
+ try {
295
+ renameSync2(memoryPath, backupPath);
296
+ } catch {
297
+ try {
298
+ unlinkSync2(memoryPath);
299
+ } catch {
300
+ }
301
+ }
302
+ memvid = await create(memoryPath, "basic");
303
+ return;
304
+ }
305
+ throw openError;
306
+ }
307
+ });
308
+ const mind = new _Mind(memvid, config, memoryPath);
309
+ mind.initialized = true;
310
+ pruneBackups(memoryPath, 3);
311
+ if (config.debug) {
312
+ console.error(`[memvid-mind] Opened: ${memoryPath}`);
313
+ }
314
+ return mind;
315
+ }
316
+ async withLock(fn) {
317
+ const memoryPath = this.getMemoryPath();
318
+ const lockPath = `${memoryPath}.lock`;
319
+ return withMemvidLock(lockPath, fn);
320
+ }
321
+ /**
322
+ * Remember an observation
323
+ */
324
+ async remember(input) {
325
+ const observation = {
326
+ id: generateId(),
327
+ timestamp: Date.now(),
328
+ type: input.type,
329
+ tool: input.tool,
330
+ summary: input.summary,
331
+ content: input.content,
332
+ metadata: {
333
+ ...input.metadata,
334
+ sessionId: this.sessionId
335
+ }
336
+ };
337
+ const frameId = await this.withLock(async () => {
338
+ return this.memvid.put({
339
+ title: `[${observation.type}] ${observation.summary}`,
340
+ label: observation.type,
341
+ text: observation.content,
342
+ metadata: {
343
+ observationId: observation.id,
344
+ timestamp: observation.timestamp,
345
+ tool: observation.tool,
346
+ sessionId: this.sessionId,
347
+ ...observation.metadata
348
+ },
349
+ tags: [
350
+ observation.type,
351
+ `session:${this.sessionId}`,
352
+ observation.tool ? `tool:${observation.tool}` : void 0
353
+ ].filter(Boolean)
354
+ });
355
+ });
356
+ if (this.config.debug) {
357
+ console.error(`[memvid-mind] Remembered: ${observation.summary}`);
358
+ }
359
+ this.sessionObservationCount += 1;
360
+ this.cachedStats = null;
361
+ this.cachedStatsFrameCount = -1;
362
+ return frameId;
363
+ }
364
+ /**
365
+ * Search memories by query (uses fast lexical search)
366
+ */
367
+ async search(query, limit = 10) {
368
+ return this.withLock(async () => {
369
+ return this.searchUnlocked(query, limit);
370
+ });
371
+ }
372
+ async searchUnlocked(query, limit) {
373
+ const results = await this.memvid.find(query, { k: limit, mode: "lex" });
374
+ const frames = this.toSearchFrames(results);
375
+ return frames.map((frame) => {
376
+ const rawTags = Array.isArray(frame.tags) ? frame.tags.filter((tag) => typeof tag === "string") : [];
377
+ const prefixedToolTag = rawTags.find((tag) => tag.startsWith("tool:"));
378
+ const labels = Array.isArray(frame.labels) ? frame.labels.filter((label) => typeof label === "string") : [];
379
+ const metadata = frame.metadata && typeof frame.metadata === "object" ? frame.metadata : {};
380
+ const observationType = this.extractObservationType({
381
+ label: frame.label,
382
+ labels
383
+ }) || "discovery";
384
+ const legacyToolTag = rawTags.find((tag) => {
385
+ if (tag.startsWith("tool:") || tag.startsWith("session:")) {
386
+ return false;
387
+ }
388
+ if (!/[A-Z]/.test(tag)) {
389
+ return false;
390
+ }
391
+ return tag.toLowerCase() !== observationType;
392
+ });
393
+ const tool = typeof prefixedToolTag === "string" ? prefixedToolTag.replace(/^tool:/, "") : typeof metadata.tool === "string" ? metadata.tool : legacyToolTag;
394
+ const timestamp = this.normalizeTimestampMs(
395
+ metadata.timestamp || frame.timestamp || (typeof frame.created_at === "string" ? Date.parse(frame.created_at) : 0)
396
+ );
397
+ return {
398
+ observation: {
399
+ id: String(metadata.observationId || frame.frame_id || generateId()),
400
+ timestamp,
401
+ type: observationType,
402
+ tool,
403
+ summary: frame.title?.replace(/^\[.*?\]\s*/, "") || frame.snippet || "",
404
+ content: frame.text || frame.snippet || "",
405
+ metadata: {
406
+ ...metadata,
407
+ labels,
408
+ tags: rawTags
409
+ }
410
+ },
411
+ score: frame.score || 0,
412
+ snippet: frame.snippet || frame.text?.slice(0, 200) || ""
413
+ };
414
+ });
415
+ }
416
+ toTimelineFrames(timelineResult) {
417
+ return Array.isArray(timelineResult) ? timelineResult : timelineResult.frames || [];
418
+ }
419
+ toSearchFrames(searchResult) {
420
+ if (Array.isArray(searchResult?.hits)) {
421
+ return searchResult.hits;
422
+ }
423
+ if (Array.isArray(searchResult?.frames)) {
424
+ return searchResult.frames;
425
+ }
426
+ return [];
427
+ }
428
+ normalizeTimestampMs(value) {
429
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
430
+ return 0;
431
+ }
432
+ if (value < 4102444800) {
433
+ return Math.round(value * 1e3);
434
+ }
435
+ return Math.round(value);
436
+ }
437
+ parseSessionSummary(value) {
438
+ if (!value || typeof value !== "object") {
439
+ return null;
440
+ }
441
+ const candidate = value;
442
+ 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)) {
443
+ return null;
444
+ }
445
+ return {
446
+ id: candidate.id,
447
+ startTime: this.normalizeTimestampMs(candidate.startTime),
448
+ endTime: this.normalizeTimestampMs(candidate.endTime),
449
+ observationCount: Math.max(0, Math.trunc(candidate.observationCount)),
450
+ keyDecisions: candidate.keyDecisions.filter(
451
+ (decision) => typeof decision === "string"
452
+ ),
453
+ filesModified: candidate.filesModified.filter(
454
+ (file) => typeof file === "string"
455
+ ),
456
+ summary: candidate.summary
457
+ };
458
+ }
459
+ extractSessionSummary(frame) {
460
+ const fromMetadata = this.parseSessionSummary(frame.metadata);
461
+ if (fromMetadata) {
462
+ return fromMetadata;
463
+ }
464
+ if (typeof frame.text !== "string") {
465
+ return null;
466
+ }
467
+ try {
468
+ return this.parseSessionSummary(JSON.parse(frame.text));
469
+ } catch {
470
+ return null;
471
+ }
472
+ }
473
+ extractSessionId(frame) {
474
+ const tags = Array.isArray(frame?.tags) ? frame.tags.filter((tag) => typeof tag === "string") : [];
475
+ const sessionTag = tags.find((tag) => tag.startsWith("session:"));
476
+ if (sessionTag) {
477
+ return sessionTag.slice("session:".length);
478
+ }
479
+ const metadataSessionId = frame?.metadata?.sessionId;
480
+ if (typeof metadataSessionId === "string" && metadataSessionId.length > 0) {
481
+ return metadataSessionId;
482
+ }
483
+ if (frame?.label === "session") {
484
+ const summary = this.extractSessionSummary(frame);
485
+ if (summary) {
486
+ return summary.id;
487
+ }
488
+ }
489
+ return null;
490
+ }
491
+ extractObservationType(frame) {
492
+ if (Array.isArray(frame?.labels)) {
493
+ for (const value of frame.labels) {
494
+ if (typeof value === "string") {
495
+ const normalized = value.toLowerCase();
496
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
497
+ return normalized;
498
+ }
499
+ }
500
+ }
501
+ }
502
+ const label = typeof frame?.label === "string" ? frame.label : void 0;
503
+ if (label) {
504
+ const normalized = label.toLowerCase();
505
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
506
+ return normalized;
507
+ }
508
+ }
509
+ const metadataType = frame?.metadata?.type;
510
+ if (typeof metadataType === "string") {
511
+ const normalized = metadataType.toLowerCase();
512
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
513
+ return normalized;
514
+ }
515
+ }
516
+ return null;
517
+ }
518
+ extractPreviewFieldValues(preview, field) {
519
+ if (typeof preview !== "string" || preview.length === 0) {
520
+ return [];
521
+ }
522
+ const match = new RegExp(`(?:^|\\n)${field}:\\s*([^\\n]*)`, "i").exec(preview);
523
+ if (!match?.[1]) {
524
+ return [];
525
+ }
526
+ return match[1].split(/[^a-z0-9:_-]+/i).map((value) => value.trim()).filter(Boolean);
527
+ }
528
+ extractObservationTypeFromPreview(preview) {
529
+ const labels = this.extractPreviewFieldValues(preview, "labels");
530
+ const fromLabels = this.extractObservationType({ labels });
531
+ if (fromLabels) {
532
+ return fromLabels;
533
+ }
534
+ if (typeof preview !== "string" || preview.length === 0) {
535
+ return null;
536
+ }
537
+ const titleMatch = /(?:^|\n)title:\s*\[([^\]]+)\]/i.exec(preview);
538
+ if (!titleMatch?.[1]) {
539
+ return null;
540
+ }
541
+ const normalized = titleMatch[1].trim().toLowerCase();
542
+ if (OBSERVATION_TYPE_SET.has(normalized)) {
543
+ return normalized;
544
+ }
545
+ return null;
546
+ }
547
+ parseLeadingJsonObject(text) {
548
+ const start = text.indexOf("{");
549
+ if (start < 0) {
550
+ return null;
551
+ }
552
+ let depth = 0;
553
+ let inString = false;
554
+ let escaped = false;
555
+ for (let i = start; i < text.length; i++) {
556
+ const ch = text[i];
557
+ if (inString) {
558
+ if (escaped) {
559
+ escaped = false;
560
+ } else if (ch === "\\") {
561
+ escaped = true;
562
+ } else if (ch === '"') {
563
+ inString = false;
564
+ }
565
+ continue;
566
+ }
567
+ if (ch === '"') {
568
+ inString = true;
569
+ continue;
570
+ }
571
+ if (ch === "{") {
572
+ depth += 1;
573
+ } else if (ch === "}") {
574
+ depth -= 1;
575
+ if (depth === 0) {
576
+ const candidate = text.slice(start, i + 1);
577
+ try {
578
+ return JSON.parse(candidate);
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+ }
584
+ }
585
+ return null;
586
+ }
587
+ extractSessionSummaryFromSearchHit(hit) {
588
+ if (typeof hit?.text !== "string") {
589
+ return null;
590
+ }
591
+ const parsed = this.parseLeadingJsonObject(hit.text);
592
+ return this.parseSessionSummary(parsed);
593
+ }
594
+ /**
595
+ * Ask the memory a question (uses fast lexical search)
596
+ */
597
+ async ask(question) {
598
+ return this.withLock(async () => {
599
+ const result = await this.memvid.ask(question, { k: 5, mode: "lex" });
600
+ return result.answer || "No relevant memories found.";
601
+ });
602
+ }
603
+ /**
604
+ * Get context for session start
605
+ */
606
+ async getContext(query) {
607
+ return this.withLock(async () => {
608
+ const timeline = await this.memvid.timeline({
609
+ limit: this.config.maxContextObservations,
610
+ reverse: true
611
+ });
612
+ const frames = this.toTimelineFrames(timeline);
613
+ const recentObservations2 = [];
614
+ const FRAME_INFO_BATCH_SIZE = 20;
615
+ for (let start = 0; start < frames.length; start += FRAME_INFO_BATCH_SIZE) {
616
+ const batch = frames.slice(start, start + FRAME_INFO_BATCH_SIZE);
617
+ const frameInfos = await Promise.all(batch.map(async (frame) => {
618
+ try {
619
+ return await this.memvid.getFrameInfo(frame.frame_id);
620
+ } catch {
621
+ return null;
622
+ }
623
+ }));
624
+ for (let index = 0; index < batch.length; index++) {
625
+ const frame = batch[index];
626
+ const frameInfo = frameInfos[index];
627
+ const labels = Array.isArray(frameInfo?.labels) ? frameInfo.labels : [];
628
+ const tags = Array.isArray(frameInfo?.tags) ? frameInfo.tags : [];
629
+ const metadata = frameInfo?.metadata && typeof frameInfo.metadata === "object" ? frameInfo.metadata : {};
630
+ const toolTag = tags.find((tag) => typeof tag === "string" && tag.startsWith("tool:"));
631
+ const ts = this.normalizeTimestampMs(frameInfo?.timestamp || frame.timestamp || 0);
632
+ const observationType = this.extractObservationType({
633
+ label: labels[0],
634
+ labels,
635
+ metadata
636
+ }) || "discovery";
637
+ recentObservations2.push({
638
+ id: String(metadata.observationId || frame.metadata?.observationId || frame.frame_id),
639
+ timestamp: ts,
640
+ type: observationType,
641
+ tool: typeof toolTag === "string" ? toolTag.replace(/^tool:/, "") : typeof metadata.tool === "string" ? metadata.tool : void 0,
642
+ summary: frameInfo?.title?.replace(/^\[.*?\]\s*/, "") || frame.preview?.slice(0, 100) || "",
643
+ content: frame.preview || "",
644
+ metadata: {
645
+ ...metadata,
646
+ labels,
647
+ tags
648
+ }
649
+ });
650
+ }
651
+ }
652
+ let relevantMemories = [];
653
+ if (query) {
654
+ const searchResults = await this.searchUnlocked(query, 10);
655
+ relevantMemories = searchResults.map((r) => r.observation);
656
+ }
657
+ const summarySearch = await this.memvid.find("Session Summary", {
658
+ k: 20,
659
+ mode: "lex"
660
+ });
661
+ const summaryHits = this.toSearchFrames(summarySearch);
662
+ const seenSessionIds = /* @__PURE__ */ new Set();
663
+ const sessionSummaries = [];
664
+ for (const hit of summaryHits) {
665
+ const summary = this.extractSessionSummaryFromSearchHit(hit);
666
+ if (!summary || seenSessionIds.has(summary.id)) {
667
+ continue;
668
+ }
669
+ seenSessionIds.add(summary.id);
670
+ sessionSummaries.push(summary);
671
+ if (sessionSummaries.length >= 5) {
672
+ break;
673
+ }
674
+ }
675
+ let tokenCount = 0;
676
+ for (const obs of recentObservations2) {
677
+ const text = `[${obs.type}] ${obs.summary}`;
678
+ const tokens = estimateTokens(text);
679
+ if (tokenCount + tokens > this.config.maxContextTokens) break;
680
+ tokenCount += tokens;
681
+ }
682
+ return {
683
+ recentObservations: recentObservations2,
684
+ relevantMemories,
685
+ sessionSummaries,
686
+ tokenCount
687
+ };
688
+ });
689
+ }
690
+ /**
691
+ * Save a session summary
692
+ */
693
+ async saveSessionSummary(summary) {
694
+ return this.withLock(async () => {
695
+ const endTime = Date.now();
696
+ const sessionSummary = {
697
+ id: this.sessionId,
698
+ startTime: this.sessionStartTime,
699
+ endTime,
700
+ observationCount: this.sessionObservationCount,
701
+ keyDecisions: summary.keyDecisions.slice(0, 20),
702
+ filesModified: summary.filesModified.slice(0, 50),
703
+ summary: summary.summary
704
+ };
705
+ const frameId = await this.memvid.put({
706
+ title: `Session Summary: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
707
+ label: "session",
708
+ text: JSON.stringify(sessionSummary, null, 2),
709
+ metadata: {
710
+ ...sessionSummary,
711
+ sessionId: this.sessionId
712
+ },
713
+ tags: ["session", "summary", `session:${this.sessionId}`]
714
+ });
715
+ this.cachedStats = null;
716
+ this.cachedStatsFrameCount = -1;
717
+ return frameId;
718
+ });
719
+ }
720
+ /**
721
+ * Get memory statistics
722
+ */
723
+ async stats() {
724
+ return this.withLock(async () => {
725
+ const stats = await this.memvid.stats();
726
+ const totalFrames = Number(stats.frame_count) || 0;
727
+ if (this.cachedStats && this.cachedStatsFrameCount === totalFrames) {
728
+ return this.cachedStats;
729
+ }
730
+ const timeline = totalFrames > 0 ? await this.memvid.timeline({ limit: totalFrames, reverse: false }) : [];
731
+ const frames = this.toTimelineFrames(timeline);
732
+ const sessionIds = /* @__PURE__ */ new Set();
733
+ const topTypes = emptyTypeCounts();
734
+ let oldestMemory = 0;
735
+ let newestMemory = 0;
736
+ for (const frame of frames) {
737
+ const labels = this.extractPreviewFieldValues(frame.preview, "labels");
738
+ const tags = this.extractPreviewFieldValues(frame.preview, "tags");
739
+ const timestamp = this.normalizeTimestampMs(frame.timestamp || 0);
740
+ if (timestamp > 0) {
741
+ if (oldestMemory === 0 || timestamp < oldestMemory) {
742
+ oldestMemory = timestamp;
743
+ }
744
+ if (newestMemory === 0 || timestamp > newestMemory) {
745
+ newestMemory = timestamp;
746
+ }
747
+ }
748
+ const sessionId = this.extractSessionId({
749
+ ...frame,
750
+ labels,
751
+ tags
752
+ });
753
+ if (sessionId) {
754
+ sessionIds.add(sessionId);
755
+ }
756
+ const observationType = this.extractObservationType({
757
+ ...frame,
758
+ label: labels[0],
759
+ labels,
760
+ tags
761
+ }) || this.extractObservationTypeFromPreview(frame.preview);
762
+ if (observationType) {
763
+ topTypes[observationType] += 1;
764
+ }
765
+ }
766
+ const summarySearch = await this.memvid.find("Session Summary", {
767
+ k: 50,
768
+ mode: "lex"
769
+ });
770
+ const summaryHits = this.toSearchFrames(summarySearch);
771
+ for (const hit of summaryHits) {
772
+ const summary = this.extractSessionSummaryFromSearchHit(hit);
773
+ if (summary) {
774
+ sessionIds.add(summary.id);
775
+ }
776
+ }
777
+ const result = {
778
+ totalObservations: totalFrames,
779
+ totalSessions: sessionIds.size,
780
+ oldestMemory,
781
+ newestMemory,
782
+ fileSize: stats.size_bytes || 0,
783
+ topTypes
784
+ };
785
+ this.cachedStats = result;
786
+ this.cachedStatsFrameCount = totalFrames;
787
+ return result;
788
+ });
789
+ }
790
+ /**
791
+ * Get the session ID
792
+ */
793
+ getSessionId() {
794
+ return this.sessionId;
795
+ }
796
+ /**
797
+ * Get the memory file path
798
+ */
799
+ getMemoryPath() {
800
+ return this.memoryPath;
801
+ }
802
+ /**
803
+ * Check if initialized
804
+ */
805
+ isInitialized() {
806
+ return this.initialized;
807
+ }
808
+ };
809
+ var mindInstance = null;
810
+ async function getMind(config) {
811
+ if (!mindInstance) {
812
+ mindInstance = await Mind.open(config);
813
+ }
814
+ return mindInstance;
815
+ }
816
+
817
+ // src/utils/compression.ts
818
+ var TARGET_COMPRESSED_SIZE = 2e3;
819
+ var COMPRESSION_THRESHOLD = 3e3;
820
+ function compressToolOutput(toolName, toolInput, output) {
821
+ const originalSize = output.length;
822
+ if (originalSize <= COMPRESSION_THRESHOLD) {
823
+ return { compressed: output, wasCompressed: false, originalSize };
824
+ }
825
+ let compressed;
826
+ switch (toolName) {
827
+ case "Read":
828
+ compressed = compressFileRead(toolInput, output);
829
+ break;
830
+ case "Bash":
831
+ compressed = compressBashOutput(toolInput, output);
832
+ break;
833
+ case "Grep":
834
+ compressed = compressGrepOutput(toolInput, output);
835
+ break;
836
+ case "Glob":
837
+ compressed = compressGlobOutput(toolInput, output);
838
+ break;
839
+ case "Edit":
840
+ case "Write":
841
+ compressed = compressEditOutput(toolInput, output);
842
+ break;
843
+ default:
844
+ compressed = compressGeneric(output);
845
+ }
846
+ return {
847
+ compressed: truncateToTarget(compressed),
848
+ wasCompressed: true,
849
+ originalSize
850
+ };
851
+ }
852
+ function compressFileRead(toolInput, output) {
853
+ const filePath = toolInput?.file_path || "unknown";
854
+ const fileName = filePath.split("/").pop() || "file";
855
+ const lines = output.split("\n");
856
+ const totalLines = lines.length;
857
+ const imports = extractImports(output);
858
+ const exports$1 = extractExports(output);
859
+ const functions = extractFunctionSignatures(output);
860
+ const classes = extractClassNames(output);
861
+ const errors = extractErrorPatterns(output);
862
+ const parts = [
863
+ `\u{1F4C4} File: ${fileName} (${totalLines} lines)`
864
+ ];
865
+ if (imports.length > 0) {
866
+ parts.push(`
867
+ \u{1F4E6} Imports: ${imports.slice(0, 10).join(", ")}${imports.length > 10 ? ` (+${imports.length - 10} more)` : ""}`);
868
+ }
869
+ if (exports$1.length > 0) {
870
+ parts.push(`
871
+ \u{1F4E4} Exports: ${exports$1.slice(0, 10).join(", ")}${exports$1.length > 10 ? ` (+${exports$1.length - 10} more)` : ""}`);
872
+ }
873
+ if (functions.length > 0) {
874
+ parts.push(`
875
+ \u26A1 Functions: ${functions.slice(0, 10).join(", ")}${functions.length > 10 ? ` (+${functions.length - 10} more)` : ""}`);
876
+ }
877
+ if (classes.length > 0) {
878
+ parts.push(`
879
+ \u{1F3D7}\uFE0F Classes: ${classes.join(", ")}`);
880
+ }
881
+ if (errors.length > 0) {
882
+ parts.push(`
883
+ \u26A0\uFE0F Errors/TODOs: ${errors.slice(0, 5).join("; ")}`);
884
+ }
885
+ const contextLines = [
886
+ "\n--- First 10 lines ---",
887
+ ...lines.slice(0, 10),
888
+ "\n--- Last 5 lines ---",
889
+ ...lines.slice(-5)
890
+ ];
891
+ parts.push(contextLines.join("\n"));
892
+ return parts.join("");
893
+ }
894
+ function compressBashOutput(toolInput, output) {
895
+ const command = toolInput?.command || "command";
896
+ const shortCmd = command.split("\n")[0].slice(0, 100);
897
+ const lines = output.split("\n");
898
+ const errorLines = lines.filter(
899
+ (l) => l.toLowerCase().includes("error") || l.toLowerCase().includes("failed") || l.toLowerCase().includes("exception") || l.toLowerCase().includes("warning")
900
+ );
901
+ const successLines = lines.filter(
902
+ (l) => l.toLowerCase().includes("success") || l.toLowerCase().includes("passed") || l.toLowerCase().includes("completed") || l.toLowerCase().includes("done")
903
+ );
904
+ const parts = [`\u{1F5A5}\uFE0F Command: ${shortCmd}`];
905
+ if (errorLines.length > 0) {
906
+ parts.push(`
907
+ \u274C Errors (${errorLines.length}):`);
908
+ parts.push(errorLines.slice(0, 10).join("\n"));
909
+ }
910
+ if (successLines.length > 0) {
911
+ parts.push(`
912
+ \u2705 Success indicators:`);
913
+ parts.push(successLines.slice(0, 5).join("\n"));
914
+ }
915
+ parts.push(`
916
+ \u{1F4CA} Output: ${lines.length} lines total`);
917
+ if (lines.length > 20) {
918
+ parts.push("\n--- First 10 lines ---");
919
+ parts.push(lines.slice(0, 10).join("\n"));
920
+ parts.push("\n--- Last 5 lines ---");
921
+ parts.push(lines.slice(-5).join("\n"));
922
+ } else {
923
+ parts.push("\n--- Full output ---");
924
+ parts.push(lines.join("\n"));
925
+ }
926
+ return parts.join("");
927
+ }
928
+ function compressGrepOutput(toolInput, output) {
929
+ const pattern = toolInput?.pattern || "pattern";
930
+ const lines = output.split("\n").filter(Boolean);
931
+ const files = /* @__PURE__ */ new Set();
932
+ lines.forEach((line) => {
933
+ const match = line.match(/^([^:]+):/);
934
+ if (match) files.add(match[1]);
935
+ });
936
+ const parts = [
937
+ `\u{1F50D} Grep: "${pattern.slice(0, 50)}"`,
938
+ `\u{1F4C1} Found in ${files.size} files, ${lines.length} matches`
939
+ ];
940
+ if (files.size > 0) {
941
+ parts.push(`
942
+ \u{1F4C2} Files: ${Array.from(files).slice(0, 15).join(", ")}${files.size > 15 ? ` (+${files.size - 15} more)` : ""}`);
943
+ }
944
+ parts.push("\n--- Top matches ---");
945
+ parts.push(lines.slice(0, 10).join("\n"));
946
+ if (lines.length > 10) {
947
+ parts.push(`
948
+ ... and ${lines.length - 10} more matches`);
949
+ }
950
+ return parts.join("");
951
+ }
952
+ function compressGlobOutput(toolInput, output) {
953
+ const pattern = toolInput?.pattern || "pattern";
954
+ let files = [];
955
+ try {
956
+ const parsed = JSON.parse(output);
957
+ files = parsed.filenames || [];
958
+ } catch {
959
+ files = output.split("\n").filter(Boolean);
960
+ }
961
+ const byDir = {};
962
+ files.forEach((f) => {
963
+ const dir = f.split("/").slice(0, -1).join("/") || "/";
964
+ const file = f.split("/").pop() || f;
965
+ if (!byDir[dir]) byDir[dir] = [];
966
+ byDir[dir].push(file);
967
+ });
968
+ const parts = [
969
+ `\u{1F4C2} Glob: "${pattern.slice(0, 50)}"`,
970
+ `\u{1F4C1} Found ${files.length} files in ${Object.keys(byDir).length} directories`
971
+ ];
972
+ const topDirs = Object.entries(byDir).sort((a, b) => b[1].length - a[1].length).slice(0, 5);
973
+ parts.push("\n--- Top directories ---");
974
+ topDirs.forEach(([dir, dirFiles]) => {
975
+ const shortDir = dir.split("/").slice(-3).join("/");
976
+ parts.push(`${shortDir}/ (${dirFiles.length} files)`);
977
+ });
978
+ parts.push("\n--- Sample files ---");
979
+ parts.push(files.slice(0, 15).map((f) => f.split("/").pop()).join(", "));
980
+ return parts.join("");
981
+ }
982
+ function compressEditOutput(toolInput, output) {
983
+ const filePath = toolInput?.file_path || "unknown";
984
+ const fileName = filePath.split("/").pop() || "file";
985
+ return [
986
+ `\u270F\uFE0F Edited: ${fileName}`,
987
+ `\u{1F4DD} Changes applied successfully`,
988
+ output.slice(0, 500)
989
+ ].join("\n");
990
+ }
991
+ function compressGeneric(output) {
992
+ const lines = output.split("\n");
993
+ if (lines.length <= 30) {
994
+ return output;
995
+ }
996
+ return [
997
+ `\u{1F4CA} Output: ${lines.length} lines`,
998
+ "--- First 15 lines ---",
999
+ ...lines.slice(0, 15),
1000
+ "--- Last 10 lines ---",
1001
+ ...lines.slice(-10)
1002
+ ].join("\n");
1003
+ }
1004
+ function extractImports(code) {
1005
+ const imports = [];
1006
+ const patterns = [
1007
+ /import\s+(?:{\s*([^}]+)\s*}|(\w+))\s+from\s+['"]([^'"]+)['"]/g,
1008
+ /from\s+['"]([^'"]+)['"]\s+import/g,
1009
+ /require\s*\(['"]([^'"]+)['"]\)/g,
1010
+ /use\s+(\w+(?:::\w+)*)/g
1011
+ ];
1012
+ patterns.forEach((pattern) => {
1013
+ let match;
1014
+ while ((match = pattern.exec(code)) !== null) {
1015
+ imports.push(match[3] || match[1] || match[2] || match[0]);
1016
+ }
1017
+ });
1018
+ return [...new Set(imports)];
1019
+ }
1020
+ function extractExports(code) {
1021
+ const exports$1 = [];
1022
+ const patterns = [
1023
+ /export\s+(?:default\s+)?(?:function|class|const|let|var)\s+(\w+)/g,
1024
+ /export\s*{\s*([^}]+)\s*}/g,
1025
+ /pub\s+(?:fn|struct|enum|trait|mod)\s+(\w+)/g
1026
+ ];
1027
+ patterns.forEach((pattern) => {
1028
+ let match;
1029
+ while ((match = pattern.exec(code)) !== null) {
1030
+ const names = (match[1] || "").split(",").map((s) => s.trim());
1031
+ exports$1.push(...names.filter(Boolean));
1032
+ }
1033
+ });
1034
+ return [...new Set(exports$1)];
1035
+ }
1036
+ function extractFunctionSignatures(code) {
1037
+ const functions = [];
1038
+ const patterns = [
1039
+ /(?:async\s+)?function\s+(\w+)/g,
1040
+ /(\w+)\s*:\s*(?:async\s+)?\([^)]*\)\s*=>/g,
1041
+ /(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/g,
1042
+ /fn\s+(\w+)/g,
1043
+ /def\s+(\w+)/g
1044
+ ];
1045
+ patterns.forEach((pattern) => {
1046
+ let match;
1047
+ while ((match = pattern.exec(code)) !== null) {
1048
+ functions.push(match[1]);
1049
+ }
1050
+ });
1051
+ return [...new Set(functions)];
1052
+ }
1053
+ function extractClassNames(code) {
1054
+ const classes = [];
1055
+ const patterns = [
1056
+ /class\s+(\w+)/g,
1057
+ /struct\s+(\w+)/g,
1058
+ /interface\s+(\w+)/g,
1059
+ /type\s+(\w+)\s*=/g
1060
+ ];
1061
+ patterns.forEach((pattern) => {
1062
+ let match;
1063
+ while ((match = pattern.exec(code)) !== null) {
1064
+ classes.push(match[1]);
1065
+ }
1066
+ });
1067
+ return [...new Set(classes)];
1068
+ }
1069
+ function extractErrorPatterns(code) {
1070
+ const errors = [];
1071
+ const lines = code.split("\n");
1072
+ lines.forEach((line) => {
1073
+ if (line.includes("TODO") || line.includes("FIXME") || line.includes("HACK") || line.includes("XXX") || line.includes("BUG")) {
1074
+ errors.push(line.trim().slice(0, 100));
1075
+ }
1076
+ });
1077
+ return errors.slice(0, 10);
1078
+ }
1079
+ function truncateToTarget(text) {
1080
+ if (text.length <= TARGET_COMPRESSED_SIZE) {
1081
+ return text;
1082
+ }
1083
+ return text.slice(0, TARGET_COMPRESSED_SIZE - 20) + "\n... (compressed)";
1084
+ }
1085
+ function getCompressionStats(originalSize, compressedSize) {
1086
+ const saved = originalSize - compressedSize;
1087
+ const ratio = originalSize / compressedSize;
1088
+ const savedPercent = (saved / originalSize * 100).toFixed(1);
1089
+ return { ratio, saved, savedPercent };
1090
+ }
1091
+
1092
+ // src/platforms/registry.ts
1093
+ var AdapterRegistry = class {
1094
+ adapters = /* @__PURE__ */ new Map();
1095
+ register(adapter) {
1096
+ this.adapters.set(adapter.platform, adapter);
1097
+ }
1098
+ resolve(platform) {
1099
+ return this.adapters.get(platform) || null;
1100
+ }
1101
+ listPlatforms() {
1102
+ return [...this.adapters.keys()].sort();
1103
+ }
1104
+ };
1105
+
1106
+ // src/platforms/events.ts
1107
+ function createEventId() {
1108
+ return generateId();
1109
+ }
1110
+
1111
+ // src/platforms/adapters/create-adapter.ts
1112
+ var CONTRACT_VERSION = "1.0.0";
1113
+ function createAdapter(platform) {
1114
+ function projectContext(input) {
1115
+ return {
1116
+ platformProjectId: input.project_id,
1117
+ canonicalPath: input.cwd,
1118
+ cwd: input.cwd
1119
+ };
1120
+ }
1121
+ return {
1122
+ platform,
1123
+ contractVersion: CONTRACT_VERSION,
1124
+ normalizeSessionStart(input) {
1125
+ return {
1126
+ eventId: createEventId(),
1127
+ eventType: "session_start",
1128
+ platform,
1129
+ contractVersion: input.contract_version?.trim() || CONTRACT_VERSION,
1130
+ sessionId: input.session_id,
1131
+ timestamp: Date.now(),
1132
+ projectContext: projectContext(input),
1133
+ payload: {
1134
+ hookEventName: input.hook_event_name,
1135
+ permissionMode: input.permission_mode,
1136
+ transcriptPath: input.transcript_path
1137
+ }
1138
+ };
1139
+ },
1140
+ normalizeToolObservation(input) {
1141
+ if (!input.tool_name) return null;
1142
+ return {
1143
+ eventId: createEventId(),
1144
+ eventType: "tool_observation",
1145
+ platform,
1146
+ contractVersion: input.contract_version?.trim() || CONTRACT_VERSION,
1147
+ sessionId: input.session_id,
1148
+ timestamp: Date.now(),
1149
+ projectContext: projectContext(input),
1150
+ payload: {
1151
+ toolName: input.tool_name,
1152
+ toolInput: input.tool_input,
1153
+ toolResponse: input.tool_response
1154
+ }
1155
+ };
1156
+ },
1157
+ normalizeSessionStop(input) {
1158
+ return {
1159
+ eventId: createEventId(),
1160
+ eventType: "session_stop",
1161
+ platform,
1162
+ contractVersion: input.contract_version?.trim() || CONTRACT_VERSION,
1163
+ sessionId: input.session_id,
1164
+ timestamp: Date.now(),
1165
+ projectContext: projectContext(input),
1166
+ payload: {
1167
+ transcriptPath: input.transcript_path
1168
+ }
1169
+ };
1170
+ }
1171
+ };
1172
+ }
1173
+
1174
+ // src/platforms/adapters/claude.ts
1175
+ var claudeAdapter = createAdapter("claude");
1176
+
1177
+ // src/platforms/adapters/opencode.ts
1178
+ var opencodeAdapter = createAdapter("opencode");
1179
+
1180
+ // src/platforms/contract.ts
1181
+ var SUPPORTED_ADAPTER_CONTRACT_MAJOR = 1;
1182
+ var SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
1183
+ function parseContractMajor(version) {
1184
+ const match = SEMVER_PATTERN.exec(version.trim());
1185
+ if (!match) {
1186
+ return null;
1187
+ }
1188
+ return Number(match[1]);
1189
+ }
1190
+ function validateAdapterContractVersion(version, supportedMajor = SUPPORTED_ADAPTER_CONTRACT_MAJOR) {
1191
+ const adapterMajor = parseContractMajor(version);
1192
+ if (adapterMajor === null) {
1193
+ return {
1194
+ compatible: false,
1195
+ supportedMajor,
1196
+ adapterMajor: null,
1197
+ reason: "invalid_contract_version"
1198
+ };
1199
+ }
1200
+ if (adapterMajor !== supportedMajor) {
1201
+ return {
1202
+ compatible: false,
1203
+ supportedMajor,
1204
+ adapterMajor,
1205
+ reason: "incompatible_contract_major"
1206
+ };
1207
+ }
1208
+ return {
1209
+ compatible: true,
1210
+ supportedMajor,
1211
+ adapterMajor
1212
+ };
1213
+ }
1214
+
1215
+ // src/platforms/diagnostics.ts
1216
+ var DIAGNOSTIC_RETENTION_DAYS = 30;
1217
+ var DAY_MS = 24 * 60 * 60 * 1e3;
1218
+ var DIAGNOSTIC_FILE_NAME = "platform-diagnostics.json";
1219
+ var TEST_DIAGNOSTIC_FILE_NAME = `memvid-platform-diagnostics-${process.pid}.json`;
1220
+ function sanitizeFieldNames(fieldNames) {
1221
+ if (!fieldNames || fieldNames.length === 0) {
1222
+ return void 0;
1223
+ }
1224
+ return [...new Set(fieldNames)].slice(0, 20);
1225
+ }
1226
+ function resolveDiagnosticStorePath() {
1227
+ const explicitPath = process.env.MEMVID_DIAGNOSTIC_PATH?.trim();
1228
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
1229
+ if (explicitPath) {
1230
+ return resolve(projectDir, explicitPath);
1231
+ }
1232
+ if (process.env.VITEST) {
1233
+ return resolve(tmpdir(), TEST_DIAGNOSTIC_FILE_NAME);
1234
+ }
1235
+ return resolve(projectDir, ".claude", DIAGNOSTIC_FILE_NAME);
1236
+ }
1237
+ function isDiagnosticRecord(value) {
1238
+ if (!value || typeof value !== "object") {
1239
+ return false;
1240
+ }
1241
+ const record = value;
1242
+ 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";
1243
+ }
1244
+ function pruneExpired(records, now = Date.now()) {
1245
+ return records.filter((record) => record.expiresAt > now);
1246
+ }
1247
+ var DiagnosticPersistence = class {
1248
+ filePath;
1249
+ constructor(filePath) {
1250
+ this.filePath = filePath;
1251
+ }
1252
+ append(record, now = Date.now()) {
1253
+ this.withFileLock(() => {
1254
+ const latest = this.loadFromDisk();
1255
+ const next = pruneExpired([...latest, record], now);
1256
+ this.persist(next);
1257
+ });
1258
+ }
1259
+ list(now = Date.now()) {
1260
+ return this.withFileLock(() => {
1261
+ const latest = this.loadFromDisk();
1262
+ const pruned = pruneExpired(latest, now);
1263
+ if (pruned.length !== latest.length) {
1264
+ this.persist(pruned);
1265
+ }
1266
+ return [...pruned];
1267
+ });
1268
+ }
1269
+ loadFromDisk() {
1270
+ if (!existsSync(this.filePath)) {
1271
+ return [];
1272
+ }
1273
+ try {
1274
+ const raw = readFileSync(this.filePath, "utf-8").trim();
1275
+ if (!raw) {
1276
+ return [];
1277
+ }
1278
+ const parsed = JSON.parse(raw);
1279
+ if (!Array.isArray(parsed)) {
1280
+ return [];
1281
+ }
1282
+ return parsed.filter(isDiagnosticRecord);
1283
+ } catch {
1284
+ return [];
1285
+ }
1286
+ }
1287
+ withFileLock(fn) {
1288
+ mkdirSync(dirname(this.filePath), { recursive: true });
1289
+ const release = lockfile.lockSync(this.filePath, { realpath: false });
1290
+ try {
1291
+ return fn();
1292
+ } finally {
1293
+ release();
1294
+ }
1295
+ }
1296
+ persist(records) {
1297
+ mkdirSync(dirname(this.filePath), { recursive: true });
1298
+ const tmpPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
1299
+ try {
1300
+ writeFileSync(tmpPath, `${JSON.stringify(records, null, 2)}
1301
+ `, "utf-8");
1302
+ try {
1303
+ renameSync(tmpPath, this.filePath);
1304
+ } catch {
1305
+ rmSync(this.filePath, { force: true });
1306
+ renameSync(tmpPath, this.filePath);
1307
+ }
1308
+ } finally {
1309
+ rmSync(tmpPath, { force: true });
1310
+ }
1311
+ }
1312
+ };
1313
+ var persistence = null;
1314
+ var persistenceFilePath = null;
1315
+ var warnedPathChange = false;
1316
+ function getDiagnosticPersistence() {
1317
+ const resolvedPath = resolveDiagnosticStorePath();
1318
+ if (!persistence) {
1319
+ persistence = new DiagnosticPersistence(resolvedPath);
1320
+ persistenceFilePath = resolvedPath;
1321
+ warnedPathChange = false;
1322
+ return persistence;
1323
+ }
1324
+ if (persistenceFilePath && persistenceFilePath !== resolvedPath && !warnedPathChange) {
1325
+ warnedPathChange = true;
1326
+ console.error(
1327
+ `[memvid-mind] Diagnostic store path changed from "${persistenceFilePath}" to "${resolvedPath}" after initialization; continuing with the original path.`
1328
+ );
1329
+ }
1330
+ return persistence;
1331
+ }
1332
+ function createRedactedDiagnostic(input) {
1333
+ const timestamp = input.now ?? Date.now();
1334
+ const diagnostic = {
1335
+ diagnosticId: generateId(),
1336
+ timestamp,
1337
+ platform: input.platform,
1338
+ errorType: input.errorType,
1339
+ fieldNames: sanitizeFieldNames(input.fieldNames),
1340
+ severity: input.severity ?? "warning",
1341
+ redacted: true,
1342
+ retentionDays: DIAGNOSTIC_RETENTION_DAYS,
1343
+ expiresAt: timestamp + DIAGNOSTIC_RETENTION_DAYS * DAY_MS
1344
+ };
1345
+ try {
1346
+ getDiagnosticPersistence().append(diagnostic);
1347
+ } catch {
1348
+ }
1349
+ return diagnostic;
1350
+ }
1351
+ function resolveCanonicalProjectPath(context) {
1352
+ if (context.canonicalPath) {
1353
+ return resolve(context.canonicalPath);
1354
+ }
1355
+ if (context.cwd) {
1356
+ return resolve(context.cwd);
1357
+ }
1358
+ return void 0;
1359
+ }
1360
+ function resolveProjectIdentityKey(context) {
1361
+ if (context.platformProjectId && context.platformProjectId.trim().length > 0) {
1362
+ return {
1363
+ key: context.platformProjectId.trim(),
1364
+ source: "platform_project_id",
1365
+ canonicalPath: resolveCanonicalProjectPath(context)
1366
+ };
1367
+ }
1368
+ const canonicalPath = resolveCanonicalProjectPath(context);
1369
+ if (canonicalPath) {
1370
+ return {
1371
+ key: canonicalPath,
1372
+ source: "canonical_path",
1373
+ canonicalPath
1374
+ };
1375
+ }
1376
+ return {
1377
+ key: null,
1378
+ source: "unresolved"
1379
+ };
1380
+ }
1381
+
1382
+ // src/platforms/pipeline.ts
1383
+ function skipWithDiagnostic(platform, errorType, fieldNames) {
1384
+ return {
1385
+ skipped: true,
1386
+ reason: errorType,
1387
+ diagnostic: createRedactedDiagnostic({
1388
+ platform,
1389
+ errorType,
1390
+ fieldNames,
1391
+ severity: "warning"
1392
+ })
1393
+ };
1394
+ }
1395
+ function processPlatformEvent(event) {
1396
+ const contractValidation = validateAdapterContractVersion(
1397
+ event.contractVersion,
1398
+ SUPPORTED_ADAPTER_CONTRACT_MAJOR
1399
+ );
1400
+ if (!contractValidation.compatible) {
1401
+ return skipWithDiagnostic(event.platform, contractValidation.reason ?? "incompatible_contract", ["contractVersion"]);
1402
+ }
1403
+ const identity = resolveProjectIdentityKey(event.projectContext);
1404
+ if (!identity.key) {
1405
+ return skipWithDiagnostic(event.platform, "missing_project_identity", [
1406
+ "platformProjectId",
1407
+ "canonicalPath",
1408
+ "cwd"
1409
+ ]);
1410
+ }
1411
+ return {
1412
+ skipped: false,
1413
+ projectIdentityKey: identity.key
1414
+ };
1415
+ }
1416
+
1417
+ // src/platforms/index.ts
1418
+ var defaultRegistry = null;
1419
+ function getDefaultAdapterRegistry() {
1420
+ if (!defaultRegistry) {
1421
+ const registry = new AdapterRegistry();
1422
+ registry.register(claudeAdapter);
1423
+ registry.register(opencodeAdapter);
1424
+ defaultRegistry = Object.freeze({
1425
+ resolve: (platform) => registry.resolve(platform),
1426
+ listPlatforms: () => registry.listPlatforms()
1427
+ });
1428
+ }
1429
+ return defaultRegistry;
1430
+ }
1431
+ var OBSERVED_TOOLS = /* @__PURE__ */ new Set([
1432
+ "Read",
1433
+ "Edit",
1434
+ "Write",
1435
+ "Update",
1436
+ "Bash",
1437
+ "Grep",
1438
+ "Glob",
1439
+ "WebFetch",
1440
+ "WebSearch",
1441
+ "Task",
1442
+ "NotebookEdit"
1443
+ ]);
1444
+ var MIN_OUTPUT_LENGTH = 50;
1445
+ var DEDUP_WINDOW_MS = 6e4;
1446
+ var ALWAYS_CAPTURE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "Update", "NotebookEdit"]);
1447
+ var MAX_OUTPUT_LENGTH = 2500;
1448
+ var recentObservations = /* @__PURE__ */ new Map();
1449
+ function getObservationKey(toolName, toolInput) {
1450
+ const inputStr = toolInput ? JSON.stringify(toolInput).slice(0, 200) : "";
1451
+ return `${toolName}:${inputStr}`;
1452
+ }
1453
+ function isDuplicate(key) {
1454
+ const lastSeen = recentObservations.get(key);
1455
+ if (!lastSeen) return false;
1456
+ return Date.now() - lastSeen < DEDUP_WINDOW_MS;
1457
+ }
1458
+ function markObserved(key) {
1459
+ recentObservations.set(key, Date.now());
1460
+ if (recentObservations.size > 100) {
1461
+ const now = Date.now();
1462
+ for (const [k, v] of recentObservations.entries()) {
1463
+ if (now - v > DEDUP_WINDOW_MS * 2) {
1464
+ recentObservations.delete(k);
1465
+ }
1466
+ }
1467
+ }
1468
+ }
1469
+ function generateSummary(toolName, toolInput, toolOutput) {
1470
+ switch (toolName) {
1471
+ case "Read": {
1472
+ const path = toolInput?.file_path || toolInput?.filePath;
1473
+ const fileName = path?.split("/").pop() || "file";
1474
+ const lines = toolOutput.split("\n").length;
1475
+ return `Read ${fileName} (${lines} lines)`;
1476
+ }
1477
+ case "Edit":
1478
+ case "Update": {
1479
+ const path = toolInput?.file_path || toolInput?.filePath;
1480
+ const fileName = path?.split("/").pop() || "file";
1481
+ return `Edited ${fileName}`;
1482
+ }
1483
+ case "Write": {
1484
+ const path = toolInput?.file_path || toolInput?.filePath;
1485
+ const fileName = path?.split("/").pop() || "file";
1486
+ return `Created ${fileName}`;
1487
+ }
1488
+ case "Bash": {
1489
+ const cmd = toolInput?.command;
1490
+ const shortCmd = cmd?.split("\n")[0].slice(0, 50) || "command";
1491
+ const hasError = toolOutput.toLowerCase().includes("error") || toolOutput.toLowerCase().includes("failed");
1492
+ return hasError ? `Command failed: ${shortCmd}` : `Ran: ${shortCmd}`;
1493
+ }
1494
+ case "Grep": {
1495
+ const pattern = toolInput?.pattern;
1496
+ const matches = toolOutput.split("\n").filter(Boolean).length;
1497
+ return `Found ${matches} matches for "${pattern?.slice(0, 30)}"`;
1498
+ }
1499
+ case "Glob": {
1500
+ const pattern = toolInput?.pattern;
1501
+ const matches = toolOutput.split("\n").filter(Boolean).length;
1502
+ return `Found ${matches} files matching "${pattern?.slice(0, 30)}"`;
1503
+ }
1504
+ case "WebFetch":
1505
+ case "WebSearch": {
1506
+ const url = toolInput?.url || toolInput?.query;
1507
+ return `Fetched: ${url?.slice(0, 50)}`;
1508
+ }
1509
+ default:
1510
+ return `${toolName} completed`;
1511
+ }
1512
+ }
1513
+ function extractMetadata(toolName, toolInput, platform, projectIdentityKey) {
1514
+ const metadata = {
1515
+ platform,
1516
+ projectIdentityKey
1517
+ };
1518
+ if (!toolInput) return metadata;
1519
+ switch (toolName) {
1520
+ case "Read":
1521
+ case "Edit":
1522
+ case "Write":
1523
+ case "Update": {
1524
+ const filePath = toolInput.file_path || toolInput.filePath;
1525
+ if (filePath) {
1526
+ metadata.files = [filePath];
1527
+ }
1528
+ break;
1529
+ }
1530
+ case "Bash":
1531
+ if (toolInput.command) {
1532
+ metadata.command = toolInput.command.slice(0, 200);
1533
+ }
1534
+ break;
1535
+ case "Grep":
1536
+ case "Glob":
1537
+ if (toolInput.pattern) {
1538
+ metadata.pattern = toolInput.pattern;
1539
+ }
1540
+ if (toolInput.path) {
1541
+ metadata.searchPath = toolInput.path;
1542
+ }
1543
+ break;
1544
+ }
1545
+ return metadata;
1546
+ }
1547
+ async function runPostToolUseHook() {
1548
+ try {
1549
+ const input = await readStdin();
1550
+ const hookInput = JSON.parse(input);
1551
+ const platform = detectPlatform(hookInput);
1552
+ const adapter = getDefaultAdapterRegistry().resolve(platform);
1553
+ if (!adapter) {
1554
+ debug(`Skipping capture: unsupported platform ${platform}`);
1555
+ writeOutput({ continue: true });
1556
+ return;
1557
+ }
1558
+ const normalized = adapter.normalizeToolObservation(hookInput);
1559
+ if (!normalized) {
1560
+ writeOutput({ continue: true });
1561
+ return;
1562
+ }
1563
+ const pipelineResult = processPlatformEvent(normalized);
1564
+ if (pipelineResult.skipped || !pipelineResult.projectIdentityKey) {
1565
+ debug(`Skipping event due to pipeline result: ${pipelineResult.reason}`);
1566
+ writeOutput({ continue: true });
1567
+ return;
1568
+ }
1569
+ const { toolName, toolInput, toolResponse } = normalized.payload;
1570
+ if (!toolName || !OBSERVED_TOOLS.has(toolName)) {
1571
+ writeOutput({ continue: true });
1572
+ return;
1573
+ }
1574
+ const dedupKey = getObservationKey(toolName, toolInput);
1575
+ if (isDuplicate(dedupKey)) {
1576
+ debug(`Skipping duplicate observation: ${toolName}`);
1577
+ writeOutput({ continue: true });
1578
+ return;
1579
+ }
1580
+ const rawOutput = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse, null, 2);
1581
+ const alwaysCapture = ALWAYS_CAPTURE_TOOLS.has(toolName);
1582
+ if (!alwaysCapture && (!rawOutput || rawOutput.length < MIN_OUTPUT_LENGTH)) {
1583
+ writeOutput({ continue: true });
1584
+ return;
1585
+ }
1586
+ let effectiveOutput = rawOutput || "";
1587
+ if (alwaysCapture && effectiveOutput.length < MIN_OUTPUT_LENGTH) {
1588
+ const filePath = toolInput?.file_path || toolInput?.filePath || "unknown file";
1589
+ const fileName = filePath.split("/").pop() || "file";
1590
+ effectiveOutput = `File modified: ${fileName}
1591
+ Path: ${filePath}
1592
+ Tool: ${toolName}`;
1593
+ }
1594
+ if (effectiveOutput.includes("<system-reminder>") || effectiveOutput.includes("<memvid-mind-context>")) {
1595
+ writeOutput({ continue: true });
1596
+ return;
1597
+ }
1598
+ const { compressed, wasCompressed, originalSize } = compressToolOutput(
1599
+ toolName,
1600
+ toolInput,
1601
+ effectiveOutput
1602
+ );
1603
+ if (wasCompressed) {
1604
+ const stats = getCompressionStats(originalSize, compressed.length);
1605
+ debug(`Compression: ${stats.savedPercent}% (${originalSize} -> ${compressed.length})`);
1606
+ }
1607
+ const mind = await getMind();
1608
+ const observationType = classifyObservationType(toolName, compressed);
1609
+ const summary = generateSummary(toolName, toolInput, effectiveOutput);
1610
+ const content = compressed.length > MAX_OUTPUT_LENGTH ? `${compressed.slice(0, MAX_OUTPUT_LENGTH)}
1611
+ ... (truncated${wasCompressed ? ", compressed" : ""})` : compressed;
1612
+ const metadata = extractMetadata(
1613
+ toolName,
1614
+ toolInput,
1615
+ platform,
1616
+ pipelineResult.projectIdentityKey
1617
+ );
1618
+ if (wasCompressed) {
1619
+ metadata.compressed = true;
1620
+ metadata.originalSize = originalSize;
1621
+ metadata.compressedSize = compressed.length;
1622
+ }
1623
+ await mind.remember({
1624
+ type: observationType,
1625
+ summary,
1626
+ content,
1627
+ tool: toolName,
1628
+ metadata
1629
+ });
1630
+ markObserved(dedupKey);
1631
+ writeOutput({ continue: true });
1632
+ } catch (error) {
1633
+ debug(`Error: ${error}`);
1634
+ writeOutput({ continue: true });
1635
+ }
1636
+ }
1637
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
1638
+ void runPostToolUseHook();
1639
+ }
1640
+
1641
+ export { runPostToolUseHook };
1642
+ //# sourceMappingURL=post-tool-use.js.map
1643
+ //# sourceMappingURL=post-tool-use.js.map