@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.
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/commands/ask.md +27 -0
- package/commands/recent.md +27 -0
- package/commands/search.md +27 -0
- package/commands/stats.md +21 -0
- package/dist/hooks/hooks.json +44 -0
- package/dist/hooks/post-tool-use.js +1643 -0
- package/dist/hooks/post-tool-use.js.map +1 -0
- package/dist/hooks/session-start.js +554 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/smart-install.js +109 -0
- package/dist/hooks/smart-install.js.map +1 -0
- package/dist/hooks/stop.js +1386 -0
- package/dist/hooks/stop.js.map +1 -0
- package/dist/index.d.ts +397 -0
- package/dist/index.js +1236 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode/plugin.d.ts +5 -0
- package/dist/opencode/plugin.js +1820 -0
- package/dist/opencode/plugin.js.map +1 -0
- package/dist/scripts/ask.js +213 -0
- package/dist/scripts/ask.js.map +1 -0
- package/dist/scripts/find.js +213 -0
- package/dist/scripts/find.js.map +1 -0
- package/dist/scripts/stats.js +225 -0
- package/dist/scripts/stats.js.map +1 -0
- package/dist/scripts/timeline.js +210 -0
- package/dist/scripts/timeline.js.map +1 -0
- package/package.json +91 -0
- package/skills/memory/SKILL.md +71 -0
- package/skills/mind/SKILL.md +71 -0
|
@@ -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
|