@ulpi/cli 0.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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/auth-PN7TMQHV-2W4ICG64.js +15 -0
- package/dist/chunk-247GVVKK.js +2259 -0
- package/dist/chunk-2CLNOKPA.js +793 -0
- package/dist/chunk-2HEE5OKX.js +79 -0
- package/dist/chunk-2MZER6ND.js +415 -0
- package/dist/chunk-3SBPZRB5.js +772 -0
- package/dist/chunk-4VNS5WPM.js +42 -0
- package/dist/chunk-6JCMYYBT.js +1546 -0
- package/dist/chunk-6OCEY7JY.js +422 -0
- package/dist/chunk-74WVVWJ4.js +375 -0
- package/dist/chunk-7AL4DOEJ.js +131 -0
- package/dist/chunk-7LXY5UVC.js +330 -0
- package/dist/chunk-DBMUNBNB.js +3048 -0
- package/dist/chunk-JWUUVXIV.js +13694 -0
- package/dist/chunk-KIKPIH6N.js +4048 -0
- package/dist/chunk-KLEASXUR.js +70 -0
- package/dist/chunk-MIAQVCFW.js +39 -0
- package/dist/chunk-NNUWU6CV.js +1610 -0
- package/dist/chunk-PKD4ASEM.js +115 -0
- package/dist/chunk-Q4HIY43N.js +4230 -0
- package/dist/chunk-QJ5GSMEC.js +146 -0
- package/dist/chunk-SIAQVRKG.js +2163 -0
- package/dist/chunk-SPOI23SB.js +197 -0
- package/dist/chunk-YM2HV4IA.js +505 -0
- package/dist/codemap-RRJIDBQ5.js +636 -0
- package/dist/config-EGAXXCGL.js +127 -0
- package/dist/dist-6G7JC2RA.js +90 -0
- package/dist/dist-7LHZ65GC.js +418 -0
- package/dist/dist-LZKZFPVX.js +140 -0
- package/dist/dist-R5F4MX3I.js +107 -0
- package/dist/dist-R5ZJ4LX5.js +56 -0
- package/dist/dist-RJGCUS3L.js +87 -0
- package/dist/dist-RKOGLK7R.js +151 -0
- package/dist/dist-W7K4WPAF.js +597 -0
- package/dist/export-import-4A5MWLIA.js +53 -0
- package/dist/history-ATTUKOHO.js +934 -0
- package/dist/index.js +2120 -0
- package/dist/init-AY5C2ZAS.js +393 -0
- package/dist/launchd-LF2QMSKZ.js +148 -0
- package/dist/log-TVTUXAYD.js +75 -0
- package/dist/mcp-installer-NQCGKQ23.js +124 -0
- package/dist/memory-J3G24QHS.js +406 -0
- package/dist/ollama-3XCUZMZT-FYKHW4TZ.js +7 -0
- package/dist/openai-E7G2YAHU-UYY4ZWON.js +8 -0
- package/dist/projects-ATHDD3D6.js +271 -0
- package/dist/review-ADUPV3PN.js +152 -0
- package/dist/rules-E427DKYJ.js +134 -0
- package/dist/server-MOYPE4SM-N7SE2AN7.js +18 -0
- package/dist/server-X5P6WH2M-7K2RY34N.js +11 -0
- package/dist/skills/ulpi-generate-guardian/SKILL.md +511 -0
- package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +692 -0
- package/dist/skills/ulpi-generate-guardian/references/language-rules.md +596 -0
- package/dist/skills-CX73O3IV.js +76 -0
- package/dist/status-4DFHDJMN.js +66 -0
- package/dist/templates/biome.yml +24 -0
- package/dist/templates/conventional-commits.yml +18 -0
- package/dist/templates/django.yml +30 -0
- package/dist/templates/docker.yml +30 -0
- package/dist/templates/eslint.yml +13 -0
- package/dist/templates/express.yml +20 -0
- package/dist/templates/fastapi.yml +23 -0
- package/dist/templates/git-flow.yml +26 -0
- package/dist/templates/github-flow.yml +27 -0
- package/dist/templates/go.yml +33 -0
- package/dist/templates/jest.yml +24 -0
- package/dist/templates/laravel.yml +30 -0
- package/dist/templates/monorepo.yml +26 -0
- package/dist/templates/nestjs.yml +21 -0
- package/dist/templates/nextjs.yml +31 -0
- package/dist/templates/nodejs.yml +33 -0
- package/dist/templates/npm.yml +15 -0
- package/dist/templates/php.yml +25 -0
- package/dist/templates/pnpm.yml +15 -0
- package/dist/templates/prettier.yml +23 -0
- package/dist/templates/prisma.yml +21 -0
- package/dist/templates/python.yml +33 -0
- package/dist/templates/quality-of-life.yml +111 -0
- package/dist/templates/ruby.yml +25 -0
- package/dist/templates/rust.yml +34 -0
- package/dist/templates/typescript.yml +14 -0
- package/dist/templates/vitest.yml +24 -0
- package/dist/templates/yarn.yml +15 -0
- package/dist/templates-U7T6MARD.js +156 -0
- package/dist/ui-L7UAWXDY.js +167 -0
- package/dist/ui.html +698 -0
- package/dist/ulpi-RMMCUAGP-JCJ273T6.js +161 -0
- package/dist/uninstall-6SW35IK4.js +25 -0
- package/dist/update-M2B4RLGH.js +61 -0
- package/dist/version-checker-ANCS3IHR.js +10 -0
- package/package.json +92 -0
|
@@ -0,0 +1,1546 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEmbedder
|
|
3
|
+
} from "./chunk-DBMUNBNB.js";
|
|
4
|
+
import {
|
|
5
|
+
readEvents
|
|
6
|
+
} from "./chunk-YM2HV4IA.js";
|
|
7
|
+
import {
|
|
8
|
+
ClassificationResultSchema,
|
|
9
|
+
MemoryConfigSchema
|
|
10
|
+
} from "./chunk-74WVVWJ4.js";
|
|
11
|
+
import {
|
|
12
|
+
getMemoryBranch,
|
|
13
|
+
memoryConfigFile,
|
|
14
|
+
memoryEntriesDir,
|
|
15
|
+
memoryHistoryDir,
|
|
16
|
+
memoryLanceDir,
|
|
17
|
+
memoryStatsFile,
|
|
18
|
+
memoryWatermarksDir,
|
|
19
|
+
projectMemoryDir
|
|
20
|
+
} from "./chunk-7LXY5UVC.js";
|
|
21
|
+
|
|
22
|
+
// ../../packages/memory-engine/dist/index.js
|
|
23
|
+
import * as fs from "fs";
|
|
24
|
+
import * as path from "path";
|
|
25
|
+
import * as fs2 from "fs";
|
|
26
|
+
import * as path2 from "path";
|
|
27
|
+
import { createHash } from "crypto";
|
|
28
|
+
import { connect } from "@lancedb/lancedb";
|
|
29
|
+
import * as fs3 from "fs";
|
|
30
|
+
import * as path3 from "path";
|
|
31
|
+
import { execFileSync, spawn } from "child_process";
|
|
32
|
+
import * as fs4 from "fs";
|
|
33
|
+
import * as os from "os";
|
|
34
|
+
import * as path4 from "path";
|
|
35
|
+
import * as fs5 from "fs";
|
|
36
|
+
import * as os2 from "os";
|
|
37
|
+
import * as path5 from "path";
|
|
38
|
+
import { createHash as createHash2 } from "crypto";
|
|
39
|
+
import * as fs7 from "fs";
|
|
40
|
+
import * as path7 from "path";
|
|
41
|
+
import * as fs6 from "fs";
|
|
42
|
+
import * as path6 from "path";
|
|
43
|
+
import * as fs8 from "fs";
|
|
44
|
+
import * as path8 from "path";
|
|
45
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
46
|
+
import * as fs9 from "fs";
|
|
47
|
+
import * as os3 from "os";
|
|
48
|
+
import * as path9 from "path";
|
|
49
|
+
var DEFAULT_MEMORY_CONFIG = MemoryConfigSchema.parse({});
|
|
50
|
+
function loadMemoryConfig(projectDir) {
|
|
51
|
+
const configPath = memoryConfigFile(projectDir);
|
|
52
|
+
if (!fs.existsSync(configPath)) {
|
|
53
|
+
return DEFAULT_MEMORY_CONFIG;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
return MemoryConfigSchema.parse(parsed);
|
|
59
|
+
} catch {
|
|
60
|
+
return DEFAULT_MEMORY_CONFIG;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function saveMemoryConfig(projectDir, config) {
|
|
64
|
+
const configPath = memoryConfigFile(projectDir);
|
|
65
|
+
const dir = path.dirname(configPath);
|
|
66
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
const tmpPath = configPath + ".tmp";
|
|
68
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
69
|
+
fs.renameSync(tmpPath, configPath);
|
|
70
|
+
}
|
|
71
|
+
function isMemoryEnabled(projectDir) {
|
|
72
|
+
const config = loadMemoryConfig(projectDir);
|
|
73
|
+
return config.enabled;
|
|
74
|
+
}
|
|
75
|
+
function generateMemoryId(type, content) {
|
|
76
|
+
return createHash("sha256").update(`${type}:${content}`).digest("hex").slice(0, 32);
|
|
77
|
+
}
|
|
78
|
+
function walkDirSize(dir) {
|
|
79
|
+
if (!fs2.existsSync(dir)) return 0;
|
|
80
|
+
let total = 0;
|
|
81
|
+
const entries = fs2.readdirSync(dir);
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const fullPath = path2.join(dir, entry);
|
|
84
|
+
try {
|
|
85
|
+
const stat = fs2.statSync(fullPath);
|
|
86
|
+
if (stat.isDirectory()) {
|
|
87
|
+
total += walkDirSize(fullPath);
|
|
88
|
+
} else {
|
|
89
|
+
total += stat.size;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return total;
|
|
95
|
+
}
|
|
96
|
+
var MemoryVectorStore = class {
|
|
97
|
+
db = null;
|
|
98
|
+
table = null;
|
|
99
|
+
indexDir;
|
|
100
|
+
constructor(projectDir) {
|
|
101
|
+
this.indexDir = memoryLanceDir(projectDir);
|
|
102
|
+
}
|
|
103
|
+
async initialize() {
|
|
104
|
+
fs2.mkdirSync(this.indexDir, { recursive: true });
|
|
105
|
+
this.db = await connect(this.indexDir);
|
|
106
|
+
const tables = await this.db.tableNames();
|
|
107
|
+
if (tables.includes("memories")) {
|
|
108
|
+
this.table = await this.db.openTable("memories");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async upsertItems(items) {
|
|
112
|
+
if (items.length === 0) return;
|
|
113
|
+
const records = items.map((item) => ({
|
|
114
|
+
id: item.id,
|
|
115
|
+
vector: item.vector,
|
|
116
|
+
type: item.metadata.type,
|
|
117
|
+
importance: item.metadata.importance,
|
|
118
|
+
snippet: item.metadata.snippet,
|
|
119
|
+
createdAt: item.metadata.createdAt,
|
|
120
|
+
tags: item.metadata.tags
|
|
121
|
+
}));
|
|
122
|
+
if (!this.table) {
|
|
123
|
+
this.table = await this.db.createTable("memories", records);
|
|
124
|
+
} else {
|
|
125
|
+
await this.table.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(records);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async removeItems(ids) {
|
|
129
|
+
if (!this.table || ids.length === 0) return;
|
|
130
|
+
const quoted = ids.map((id) => `'${id.replace(/'/g, "''")}'`).join(", ");
|
|
131
|
+
await this.table.delete(`id IN (${quoted})`);
|
|
132
|
+
}
|
|
133
|
+
async query(vector, topK = 10) {
|
|
134
|
+
if (!this.table) return [];
|
|
135
|
+
const results = await this.table.vectorSearch(vector).distanceType("cosine").limit(topK).toArray();
|
|
136
|
+
return results.map((r) => ({
|
|
137
|
+
id: r.id,
|
|
138
|
+
type: r.type,
|
|
139
|
+
score: 1 - (r._distance ?? 0),
|
|
140
|
+
snippet: r.snippet,
|
|
141
|
+
importance: r.importance
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
async getItemCount() {
|
|
145
|
+
if (!this.table) return 0;
|
|
146
|
+
return this.table.countRows();
|
|
147
|
+
}
|
|
148
|
+
getIndexSizeBytes() {
|
|
149
|
+
return walkDirSize(this.indexDir);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
function saveEntry(projectDir, entry) {
|
|
153
|
+
const dir = memoryEntriesDir(projectDir);
|
|
154
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
155
|
+
const filePath = path2.join(dir, `${entry.id}.json`);
|
|
156
|
+
const tmpPath = filePath + ".tmp";
|
|
157
|
+
fs2.writeFileSync(tmpPath, JSON.stringify(entry, null, 2) + "\n", "utf-8");
|
|
158
|
+
fs2.renameSync(tmpPath, filePath);
|
|
159
|
+
}
|
|
160
|
+
function loadEntry(projectDir, id) {
|
|
161
|
+
const filePath = path2.join(memoryEntriesDir(projectDir), `${id}.json`);
|
|
162
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function removeEntry(projectDir, id) {
|
|
170
|
+
const filePath = path2.join(memoryEntriesDir(projectDir), `${id}.json`);
|
|
171
|
+
try {
|
|
172
|
+
fs2.unlinkSync(filePath);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function listEntries(projectDir) {
|
|
179
|
+
const dir = memoryEntriesDir(projectDir);
|
|
180
|
+
if (!fs2.existsSync(dir)) return [];
|
|
181
|
+
const entries = [];
|
|
182
|
+
const files = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
try {
|
|
185
|
+
const raw = fs2.readFileSync(path2.join(dir, file), "utf-8");
|
|
186
|
+
entries.push(JSON.parse(raw));
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return entries;
|
|
191
|
+
}
|
|
192
|
+
function updateEntry(projectDir, id, updates) {
|
|
193
|
+
const entry = loadEntry(projectDir, id);
|
|
194
|
+
if (!entry) return null;
|
|
195
|
+
const updated = { ...entry, ...updates, id: entry.id };
|
|
196
|
+
saveEntry(projectDir, updated);
|
|
197
|
+
return updated;
|
|
198
|
+
}
|
|
199
|
+
var DEFAULT_PATTERNS = [
|
|
200
|
+
// API keys
|
|
201
|
+
/(?:api[_-]?key|apikey|secret[_-]?key)\s*[:=]\s*["']?[A-Za-z0-9_\-]{20,}["']?/gi,
|
|
202
|
+
// Bearer tokens
|
|
203
|
+
/Bearer\s+[A-Za-z0-9_\-.]{20,}/g,
|
|
204
|
+
// AWS keys
|
|
205
|
+
/AKIA[A-Z0-9]{16}/g,
|
|
206
|
+
// Generic secrets in env format
|
|
207
|
+
/(?:PASSWORD|SECRET|TOKEN|PRIVATE_KEY)\s*=\s*\S+/gi,
|
|
208
|
+
// Base64 encoded long values that look like secrets
|
|
209
|
+
/(?:eyJ[A-Za-z0-9_-]{50,})/g
|
|
210
|
+
];
|
|
211
|
+
function redactContent(text, userPatterns = []) {
|
|
212
|
+
let result = text;
|
|
213
|
+
for (const pattern of DEFAULT_PATTERNS) {
|
|
214
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
215
|
+
}
|
|
216
|
+
for (const patternStr of userPatterns) {
|
|
217
|
+
try {
|
|
218
|
+
const pattern = new RegExp(patternStr, "gi");
|
|
219
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
var MAX_TRANSCRIPT_SIZE = 10485760;
|
|
226
|
+
function sessionCaptureDir(projectDir, sessionId) {
|
|
227
|
+
return path3.join(memoryHistoryDir(projectDir), `sess_${sessionId}`);
|
|
228
|
+
}
|
|
229
|
+
function appendMemoryEvent(sessionId, event, projectDir) {
|
|
230
|
+
const dir = sessionCaptureDir(projectDir, sessionId);
|
|
231
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
232
|
+
const logPath = path3.join(dir, "events.jsonl");
|
|
233
|
+
fs3.appendFileSync(logPath, JSON.stringify(event) + "\n", "utf-8");
|
|
234
|
+
}
|
|
235
|
+
function toClassificationEvent(ev) {
|
|
236
|
+
return {
|
|
237
|
+
ts: ev.ts,
|
|
238
|
+
event: ev.event,
|
|
239
|
+
hookEvent: ev.hookEvent,
|
|
240
|
+
toolName: ev.toolName,
|
|
241
|
+
filePath: ev.filePath,
|
|
242
|
+
command: ev.command,
|
|
243
|
+
message: ev.message
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function finalizeCapture(sessionId, state, projectDir) {
|
|
247
|
+
const config = loadMemoryConfig(projectDir);
|
|
248
|
+
const dir = sessionCaptureDir(projectDir, sessionId);
|
|
249
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
250
|
+
const redactPatterns = config.redactPatterns;
|
|
251
|
+
if (config.captureMode === "end_of_session") {
|
|
252
|
+
bulkCaptureFromSessionLog(sessionId, projectDir, redactPatterns);
|
|
253
|
+
}
|
|
254
|
+
let transcriptCaptured = false;
|
|
255
|
+
if (state.transcriptPath) {
|
|
256
|
+
transcriptCaptured = copyTranscript(
|
|
257
|
+
state.transcriptPath,
|
|
258
|
+
path3.join(dir, "transcript.jsonl"),
|
|
259
|
+
redactPatterns
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const meta = {
|
|
263
|
+
sessionId,
|
|
264
|
+
projectDir,
|
|
265
|
+
startedAt: state.startedAt,
|
|
266
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
267
|
+
branch: state.branch,
|
|
268
|
+
sessionName: state.sessionName,
|
|
269
|
+
filesRead: state.filesRead.length,
|
|
270
|
+
filesWritten: state.filesWritten.length,
|
|
271
|
+
commandsRun: state.commandsRun.length,
|
|
272
|
+
transcriptCaptured,
|
|
273
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
274
|
+
};
|
|
275
|
+
const metaPath = path3.join(dir, "meta.json");
|
|
276
|
+
fs3.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
277
|
+
}
|
|
278
|
+
function bulkCaptureFromSessionLog(sessionId, projectDir, redactPatterns) {
|
|
279
|
+
const events = readEvents(sessionId, projectDir);
|
|
280
|
+
if (events.length === 0) return;
|
|
281
|
+
const dir = sessionCaptureDir(projectDir, sessionId);
|
|
282
|
+
const logPath = path3.join(dir, "events.jsonl");
|
|
283
|
+
const lines = events.map((ev) => {
|
|
284
|
+
const ce = toClassificationEvent(ev);
|
|
285
|
+
if (ce.command && redactPatterns.length > 0) {
|
|
286
|
+
ce.command = redactContent(ce.command, redactPatterns);
|
|
287
|
+
}
|
|
288
|
+
if (ce.message && redactPatterns.length > 0) {
|
|
289
|
+
ce.message = redactContent(ce.message, redactPatterns);
|
|
290
|
+
}
|
|
291
|
+
return JSON.stringify(ce);
|
|
292
|
+
});
|
|
293
|
+
fs3.writeFileSync(logPath, lines.join("\n") + "\n", "utf-8");
|
|
294
|
+
}
|
|
295
|
+
function copyTranscript(sourcePath, destPath, redactPatterns) {
|
|
296
|
+
try {
|
|
297
|
+
const stat = fs3.statSync(sourcePath);
|
|
298
|
+
if (stat.size > MAX_TRANSCRIPT_SIZE) {
|
|
299
|
+
const fd = fs3.openSync(sourcePath, "r");
|
|
300
|
+
const buffer = Buffer.alloc(MAX_TRANSCRIPT_SIZE);
|
|
301
|
+
fs3.readSync(fd, buffer, 0, MAX_TRANSCRIPT_SIZE, 0);
|
|
302
|
+
fs3.closeSync(fd);
|
|
303
|
+
let content = buffer.toString("utf-8");
|
|
304
|
+
if (redactPatterns.length > 0) {
|
|
305
|
+
content = redactContent(content, redactPatterns);
|
|
306
|
+
}
|
|
307
|
+
fs3.writeFileSync(destPath, content, "utf-8");
|
|
308
|
+
} else {
|
|
309
|
+
let content = fs3.readFileSync(sourcePath, "utf-8");
|
|
310
|
+
if (redactPatterns.length > 0) {
|
|
311
|
+
content = redactContent(content, redactPatterns);
|
|
312
|
+
}
|
|
313
|
+
fs3.writeFileSync(destPath, content, "utf-8");
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function isSessionCaptured(projectDir, sessionId) {
|
|
321
|
+
const metaPath = path3.join(sessionCaptureDir(projectDir, sessionId), "meta.json");
|
|
322
|
+
return fs3.existsSync(metaPath);
|
|
323
|
+
}
|
|
324
|
+
function listCapturedSessions(projectDir) {
|
|
325
|
+
const dir = memoryHistoryDir(projectDir);
|
|
326
|
+
if (!fs3.existsSync(dir)) return [];
|
|
327
|
+
const sessions = [];
|
|
328
|
+
const dirs = fs3.readdirSync(dir).filter((d) => d.startsWith("sess_"));
|
|
329
|
+
for (const d of dirs) {
|
|
330
|
+
const metaPath = path3.join(dir, d, "meta.json");
|
|
331
|
+
try {
|
|
332
|
+
const raw = fs3.readFileSync(metaPath, "utf-8");
|
|
333
|
+
sessions.push(JSON.parse(raw));
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return sessions.sort((a, b) => a.capturedAt.localeCompare(b.capturedAt));
|
|
338
|
+
}
|
|
339
|
+
function readCapturedEvents(projectDir, sessionId) {
|
|
340
|
+
const logPath = path3.join(sessionCaptureDir(projectDir, sessionId), "events.jsonl");
|
|
341
|
+
try {
|
|
342
|
+
const raw = fs3.readFileSync(logPath, "utf-8");
|
|
343
|
+
const events = [];
|
|
344
|
+
for (const line of raw.split("\n")) {
|
|
345
|
+
const trimmed = line.trim();
|
|
346
|
+
if (!trimmed) continue;
|
|
347
|
+
try {
|
|
348
|
+
events.push(JSON.parse(trimmed));
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return events;
|
|
353
|
+
} catch {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function readCapturedTranscript(projectDir, sessionId, maxChars = 1e4) {
|
|
358
|
+
const transcriptPath = path3.join(
|
|
359
|
+
sessionCaptureDir(projectDir, sessionId),
|
|
360
|
+
"transcript.jsonl"
|
|
361
|
+
);
|
|
362
|
+
try {
|
|
363
|
+
const content = fs3.readFileSync(transcriptPath, "utf-8");
|
|
364
|
+
return content.length > maxChars ? content.slice(0, maxChars) : content;
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function resolveClaudePath() {
|
|
370
|
+
try {
|
|
371
|
+
const result = execFileSync("which", ["claude"], { stdio: "pipe", timeout: 3e3 }).toString().trim();
|
|
372
|
+
if (result) return result;
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
const home = os.homedir();
|
|
376
|
+
const candidates = [
|
|
377
|
+
path4.join(home, ".local", "bin", "claude"),
|
|
378
|
+
path4.join(home, ".claude", "bin", "claude"),
|
|
379
|
+
"/usr/local/bin/claude"
|
|
380
|
+
];
|
|
381
|
+
for (const p of candidates) {
|
|
382
|
+
try {
|
|
383
|
+
if (fs4.existsSync(p)) return p;
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
var cachedClaudePath;
|
|
390
|
+
function getClaudePath() {
|
|
391
|
+
if (cachedClaudePath === void 0) {
|
|
392
|
+
cachedClaudePath = resolveClaudePath();
|
|
393
|
+
}
|
|
394
|
+
return cachedClaudePath;
|
|
395
|
+
}
|
|
396
|
+
var TRIVIAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
397
|
+
"ls",
|
|
398
|
+
"pwd",
|
|
399
|
+
"echo",
|
|
400
|
+
"cat",
|
|
401
|
+
"head",
|
|
402
|
+
"tail",
|
|
403
|
+
"which",
|
|
404
|
+
"cd",
|
|
405
|
+
"whoami",
|
|
406
|
+
"date",
|
|
407
|
+
"wc"
|
|
408
|
+
]);
|
|
409
|
+
function filterSignificantEvents(events) {
|
|
410
|
+
return events.filter((ev) => {
|
|
411
|
+
if (ev.event === "tool_blocked" || ev.event === "permission_denied" || ev.event === "postcondition_failed") {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
if (ev.event === "tool_used" && ev.toolName === "Read") {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
if (ev.toolName === "Bash" && ev.command) {
|
|
418
|
+
const cmd = ev.command.trim().split(/\s+/)[0];
|
|
419
|
+
if (TRIVIAL_COMMANDS.has(cmd)) return false;
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function buildClassificationWindows(sessionId, projectDir, events, config) {
|
|
425
|
+
if (events.length === 0) return [];
|
|
426
|
+
const windowSize = config.classifier.windowSize;
|
|
427
|
+
const step = windowSize;
|
|
428
|
+
const windows = [];
|
|
429
|
+
for (let i = 0; i < events.length; i += step) {
|
|
430
|
+
const windowEvents = events.slice(i, i + windowSize);
|
|
431
|
+
if (windowEvents.length === 0) break;
|
|
432
|
+
let transcriptExcerpt;
|
|
433
|
+
if (i === 0 && config.classifier.includeTranscript) {
|
|
434
|
+
transcriptExcerpt = readCapturedTranscript(
|
|
435
|
+
projectDir,
|
|
436
|
+
sessionId,
|
|
437
|
+
config.classifier.maxTranscriptChars
|
|
438
|
+
) ?? void 0;
|
|
439
|
+
}
|
|
440
|
+
windows.push({
|
|
441
|
+
sessionId,
|
|
442
|
+
projectDir,
|
|
443
|
+
events: windowEvents,
|
|
444
|
+
transcriptExcerpt,
|
|
445
|
+
startTs: windowEvents[0].ts,
|
|
446
|
+
endTs: windowEvents[windowEvents.length - 1].ts
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return windows;
|
|
450
|
+
}
|
|
451
|
+
var CLASSIFIER_SCHEMA = JSON.stringify({
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
memories: {
|
|
455
|
+
type: "array",
|
|
456
|
+
items: {
|
|
457
|
+
type: "object",
|
|
458
|
+
properties: {
|
|
459
|
+
type: {
|
|
460
|
+
type: "string",
|
|
461
|
+
enum: ["DECISION", "PATTERN", "BUG_ROOT_CAUSE", "PREFERENCE", "CONSTRAINT", "CONTEXT", "LESSON", "RELATIONSHIP"]
|
|
462
|
+
},
|
|
463
|
+
summary: { type: "string", description: "Concise memory text (max 2000 chars)" },
|
|
464
|
+
detail: { type: "string", description: "Additional context (optional)" },
|
|
465
|
+
importance: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
466
|
+
tags: { type: "array", items: { type: "string" }, description: "Categorization tags" },
|
|
467
|
+
relatedFiles: { type: "array", items: { type: "string" }, description: "Related file paths" }
|
|
468
|
+
},
|
|
469
|
+
required: ["type", "summary", "importance"]
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
required: ["memories"]
|
|
474
|
+
});
|
|
475
|
+
function buildPromptPreamble(maxMemories) {
|
|
476
|
+
return [
|
|
477
|
+
"You are extracting structured memories from an AI coding agent's session.",
|
|
478
|
+
'A "memory" is knowledge worth retaining for future sessions.',
|
|
479
|
+
"",
|
|
480
|
+
`Extract 0-${maxMemories} memories from the following session events.`,
|
|
481
|
+
"Only extract genuinely useful, specific information. Skip generic observations.",
|
|
482
|
+
"",
|
|
483
|
+
"Memory types:",
|
|
484
|
+
"- DECISION: Architecture/design decisions with rationale",
|
|
485
|
+
"- PATTERN: Code patterns adopted or enforced in this project",
|
|
486
|
+
"- BUG_ROOT_CAUSE: Root causes of bugs (not symptoms)",
|
|
487
|
+
"- PREFERENCE: User preferences for code style, tools, workflow",
|
|
488
|
+
"- CONSTRAINT: Project constraints (performance budgets, compatibility)",
|
|
489
|
+
"- CONTEXT: Domain knowledge, terminology, relationships between concepts",
|
|
490
|
+
"- LESSON: Lessons learned from mistakes or difficulties",
|
|
491
|
+
"- RELATIONSHIP: How components/modules/files relate to each other",
|
|
492
|
+
""
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
function formatEventsSection(events) {
|
|
496
|
+
const lines = ["## Session Events"];
|
|
497
|
+
for (const ev of events) {
|
|
498
|
+
const parts = [ev.ts, ev.event, ev.hookEvent];
|
|
499
|
+
if (ev.toolName) parts.push(`tool=${ev.toolName}`);
|
|
500
|
+
if (ev.filePath) parts.push(`file=${ev.filePath}`);
|
|
501
|
+
if (ev.command) parts.push(`cmd=${ev.command.slice(0, 200)}`);
|
|
502
|
+
if (ev.message) parts.push(`msg=${ev.message.slice(0, 200)}`);
|
|
503
|
+
lines.push(parts.join(" | "));
|
|
504
|
+
}
|
|
505
|
+
lines.push("");
|
|
506
|
+
return lines;
|
|
507
|
+
}
|
|
508
|
+
function buildPromptGuidelines() {
|
|
509
|
+
return [
|
|
510
|
+
"## Guidelines",
|
|
511
|
+
'- Be specific: "User prefers const over let" not "User has coding preferences"',
|
|
512
|
+
"- Include file paths when relevant",
|
|
513
|
+
"- Set importance based on how impactful the memory is",
|
|
514
|
+
'- Use tags to categorize (e.g., ["typescript", "testing", "auth"])',
|
|
515
|
+
"- Skip routine operations (file reads, standard commands)",
|
|
516
|
+
"- Focus on decisions, learnings, and project-specific knowledge",
|
|
517
|
+
"",
|
|
518
|
+
"Fill in the structured output."
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
function buildClassificationPrompt(window) {
|
|
522
|
+
const sections = [
|
|
523
|
+
...buildPromptPreamble(10),
|
|
524
|
+
...formatEventsSection(window.events)
|
|
525
|
+
];
|
|
526
|
+
if (window.transcriptExcerpt) {
|
|
527
|
+
sections.push("## Transcript Excerpt");
|
|
528
|
+
sections.push(window.transcriptExcerpt);
|
|
529
|
+
sections.push("");
|
|
530
|
+
}
|
|
531
|
+
sections.push(...buildPromptGuidelines());
|
|
532
|
+
return sections.join("\n");
|
|
533
|
+
}
|
|
534
|
+
function buildSingleClassificationPrompt(events, transcriptExcerpt) {
|
|
535
|
+
const sections = [
|
|
536
|
+
...buildPromptPreamble(20),
|
|
537
|
+
...formatEventsSection(events)
|
|
538
|
+
];
|
|
539
|
+
if (transcriptExcerpt) {
|
|
540
|
+
sections.push("## Transcript Excerpt");
|
|
541
|
+
sections.push(transcriptExcerpt);
|
|
542
|
+
sections.push("");
|
|
543
|
+
}
|
|
544
|
+
sections.push(...buildPromptGuidelines());
|
|
545
|
+
return sections.join("\n");
|
|
546
|
+
}
|
|
547
|
+
function invokeClassifier(prompt, model, timeout) {
|
|
548
|
+
const claudePath = getClaudePath();
|
|
549
|
+
if (!claudePath) {
|
|
550
|
+
throw new Error("Claude CLI not found \u2014 cannot classify memories");
|
|
551
|
+
}
|
|
552
|
+
return new Promise((resolve2, reject) => {
|
|
553
|
+
const proc = spawn(claudePath, [
|
|
554
|
+
"--print",
|
|
555
|
+
"--model",
|
|
556
|
+
model,
|
|
557
|
+
"--output-format",
|
|
558
|
+
"json",
|
|
559
|
+
"--json-schema",
|
|
560
|
+
CLASSIFIER_SCHEMA,
|
|
561
|
+
"--permission-mode",
|
|
562
|
+
"bypassPermissions"
|
|
563
|
+
], {
|
|
564
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
565
|
+
});
|
|
566
|
+
let stdout = "";
|
|
567
|
+
let stderr = "";
|
|
568
|
+
proc.stdout.on("data", (data) => {
|
|
569
|
+
stdout += data.toString();
|
|
570
|
+
});
|
|
571
|
+
proc.stderr.on("data", (data) => {
|
|
572
|
+
stderr += data.toString();
|
|
573
|
+
});
|
|
574
|
+
proc.stdin.write(prompt);
|
|
575
|
+
proc.stdin.end();
|
|
576
|
+
const timer = setTimeout(() => {
|
|
577
|
+
proc.kill("SIGTERM");
|
|
578
|
+
reject(new Error("Classification timed out"));
|
|
579
|
+
}, timeout);
|
|
580
|
+
proc.on("close", (code) => {
|
|
581
|
+
clearTimeout(timer);
|
|
582
|
+
if (code !== 0) {
|
|
583
|
+
reject(new Error(`claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
584
|
+
} else {
|
|
585
|
+
try {
|
|
586
|
+
resolve2(parseClassifierOutput(stdout));
|
|
587
|
+
} catch (err) {
|
|
588
|
+
reject(err);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
proc.on("error", (err) => {
|
|
593
|
+
clearTimeout(timer);
|
|
594
|
+
reject(new Error(`Failed to run claude CLI: ${err.message}`));
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function classifyWindow(window, model, timeout) {
|
|
599
|
+
const prompt = buildClassificationPrompt(window);
|
|
600
|
+
return invokeClassifier(prompt, model, timeout);
|
|
601
|
+
}
|
|
602
|
+
function classifySinglePrompt(events, transcriptExcerpt, model, timeout) {
|
|
603
|
+
const prompt = buildSingleClassificationPrompt(events, transcriptExcerpt);
|
|
604
|
+
return invokeClassifier(prompt, model, timeout);
|
|
605
|
+
}
|
|
606
|
+
function parseClassifierOutput(response) {
|
|
607
|
+
try {
|
|
608
|
+
const envelope = JSON.parse(response.trim());
|
|
609
|
+
if (envelope.structured_output && typeof envelope.structured_output === "object") {
|
|
610
|
+
return ClassificationResultSchema.parse(envelope.structured_output);
|
|
611
|
+
}
|
|
612
|
+
if (typeof envelope.result === "string" && envelope.result.trim().startsWith("{")) {
|
|
613
|
+
return ClassificationResultSchema.parse(JSON.parse(envelope.result));
|
|
614
|
+
}
|
|
615
|
+
if (Array.isArray(envelope.memories)) {
|
|
616
|
+
return ClassificationResultSchema.parse(envelope);
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
const jsonMatch = response.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
|
|
621
|
+
if (jsonMatch) {
|
|
622
|
+
try {
|
|
623
|
+
return ClassificationResultSchema.parse(JSON.parse(jsonMatch[1]));
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
return ClassificationResultSchema.parse(JSON.parse(response.trim()));
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
return { memories: [] };
|
|
632
|
+
}
|
|
633
|
+
async function deduplicateMemories(projectDir, candidates, embeddings, store, threshold) {
|
|
634
|
+
const result = {
|
|
635
|
+
unique: [],
|
|
636
|
+
duplicates: [],
|
|
637
|
+
reinforced: []
|
|
638
|
+
};
|
|
639
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
640
|
+
const candidate = candidates[i];
|
|
641
|
+
const vector = embeddings[i];
|
|
642
|
+
const hits = await store.query(vector, 1);
|
|
643
|
+
if (hits.length > 0 && hits[0].score >= threshold) {
|
|
644
|
+
const existingId = hits[0].id;
|
|
645
|
+
result.duplicates.push({
|
|
646
|
+
candidate,
|
|
647
|
+
existingId,
|
|
648
|
+
similarity: hits[0].score
|
|
649
|
+
});
|
|
650
|
+
updateEntry(projectDir, existingId, {
|
|
651
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
652
|
+
accessCount: (loadAccessCount(projectDir, existingId) ?? 0) + 1
|
|
653
|
+
});
|
|
654
|
+
result.reinforced.push(existingId);
|
|
655
|
+
} else {
|
|
656
|
+
result.unique.push(candidate);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
function loadAccessCount(projectDir, id) {
|
|
662
|
+
const entries = listEntries(projectDir);
|
|
663
|
+
const entry = entries.find((e) => e.id === id);
|
|
664
|
+
return entry?.accessCount ?? null;
|
|
665
|
+
}
|
|
666
|
+
var DEFAULT_IMPORTANCE_WEIGHTS = {
|
|
667
|
+
critical: 1,
|
|
668
|
+
high: 0.8,
|
|
669
|
+
medium: 0.6,
|
|
670
|
+
low: 0.4
|
|
671
|
+
};
|
|
672
|
+
function computeDecayFactor(importance, createdAt) {
|
|
673
|
+
if (importance === "critical") return 1;
|
|
674
|
+
const ageMs = Date.now() - new Date(createdAt).getTime();
|
|
675
|
+
const ageDays = ageMs / (1e3 * 60 * 60 * 24);
|
|
676
|
+
const halfLife = {
|
|
677
|
+
high: 180,
|
|
678
|
+
// 6 months
|
|
679
|
+
medium: 90,
|
|
680
|
+
// 3 months
|
|
681
|
+
low: 30
|
|
682
|
+
// 1 month
|
|
683
|
+
};
|
|
684
|
+
const hl = halfLife[importance] ?? 90;
|
|
685
|
+
const decay = Math.pow(0.5, ageDays / hl);
|
|
686
|
+
return Math.max(0.1, decay);
|
|
687
|
+
}
|
|
688
|
+
function applyRanking(results, config) {
|
|
689
|
+
const weights = config?.importanceWeights ?? DEFAULT_IMPORTANCE_WEIGHTS;
|
|
690
|
+
const boostPerHit = config?.accessBoostPerHit ?? 0.05;
|
|
691
|
+
const boostCap = config?.accessBoostCap ?? 0.2;
|
|
692
|
+
for (const result of results) {
|
|
693
|
+
const entry = result.entry;
|
|
694
|
+
const importance = entry.importance;
|
|
695
|
+
const importanceWeight = weights[importance] ?? 0.6;
|
|
696
|
+
const decayFactor = computeDecayFactor(importance, entry.createdAt);
|
|
697
|
+
const accessBoost = Math.min(entry.accessCount * boostPerHit, boostCap);
|
|
698
|
+
result.finalScore = result.score * importanceWeight * decayFactor * (1 + accessBoost);
|
|
699
|
+
}
|
|
700
|
+
results.sort((a, b) => b.finalScore - a.finalScore);
|
|
701
|
+
return results;
|
|
702
|
+
}
|
|
703
|
+
var STALE_TIMEOUT_MS = 6e4;
|
|
704
|
+
var ACQUIRE_TIMEOUT_MS = 1e4;
|
|
705
|
+
var POLL_INTERVAL_MS = 500;
|
|
706
|
+
function sanitizeSlug(projectDir) {
|
|
707
|
+
const hash = createHash2("sha256").update(path5.resolve(projectDir)).digest("hex").slice(0, 16);
|
|
708
|
+
const name = path5.basename(projectDir).replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
709
|
+
return `${name}-${hash}`;
|
|
710
|
+
}
|
|
711
|
+
function lockFilePath(projectDir) {
|
|
712
|
+
const slug = sanitizeSlug(projectDir);
|
|
713
|
+
return path5.join(os2.tmpdir(), `ulpi-memory-${slug}.lock`);
|
|
714
|
+
}
|
|
715
|
+
function isProcessAlive(pid) {
|
|
716
|
+
try {
|
|
717
|
+
process.kill(pid, 0);
|
|
718
|
+
return true;
|
|
719
|
+
} catch {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function isLockStale(lockPath) {
|
|
724
|
+
try {
|
|
725
|
+
const content = fs5.readFileSync(lockPath, "utf-8");
|
|
726
|
+
const data = JSON.parse(content);
|
|
727
|
+
if (!isProcessAlive(data.pid)) return true;
|
|
728
|
+
const lockTime = new Date(data.timestamp).getTime();
|
|
729
|
+
if (Date.now() - lockTime > STALE_TIMEOUT_MS) return true;
|
|
730
|
+
return false;
|
|
731
|
+
} catch {
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function acquireMemoryLock(projectDir) {
|
|
736
|
+
const lockPath = lockFilePath(projectDir);
|
|
737
|
+
const startTime = Date.now();
|
|
738
|
+
while (true) {
|
|
739
|
+
if (fs5.existsSync(lockPath)) {
|
|
740
|
+
if (isLockStale(lockPath)) {
|
|
741
|
+
try {
|
|
742
|
+
fs5.unlinkSync(lockPath);
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
if (Date.now() - startTime > ACQUIRE_TIMEOUT_MS) {
|
|
747
|
+
throw new Error("Timeout acquiring memory lock");
|
|
748
|
+
}
|
|
749
|
+
const waitUntil = Date.now() + POLL_INTERVAL_MS;
|
|
750
|
+
while (Date.now() < waitUntil) {
|
|
751
|
+
}
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const lockData = {
|
|
756
|
+
pid: process.pid,
|
|
757
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
758
|
+
};
|
|
759
|
+
try {
|
|
760
|
+
fs5.writeFileSync(lockPath, JSON.stringify(lockData), { flag: "wx" });
|
|
761
|
+
return;
|
|
762
|
+
} catch {
|
|
763
|
+
if (Date.now() - startTime > ACQUIRE_TIMEOUT_MS) {
|
|
764
|
+
throw new Error("Timeout acquiring memory lock");
|
|
765
|
+
}
|
|
766
|
+
const waitUntil = Date.now() + POLL_INTERVAL_MS;
|
|
767
|
+
while (Date.now() < waitUntil) {
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function releaseMemoryLock(projectDir) {
|
|
773
|
+
const lockPath = lockFilePath(projectDir);
|
|
774
|
+
try {
|
|
775
|
+
const content = fs5.readFileSync(lockPath, "utf-8");
|
|
776
|
+
const data = JSON.parse(content);
|
|
777
|
+
if (data.pid === process.pid) {
|
|
778
|
+
fs5.unlinkSync(lockPath);
|
|
779
|
+
}
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function isMemoryLocked(projectDir) {
|
|
784
|
+
const lockPath = lockFilePath(projectDir);
|
|
785
|
+
if (!fs5.existsSync(lockPath)) return false;
|
|
786
|
+
return !isLockStale(lockPath);
|
|
787
|
+
}
|
|
788
|
+
async function searchMemory(projectDir, options) {
|
|
789
|
+
const startMs = Date.now();
|
|
790
|
+
const config = loadMemoryConfig(projectDir);
|
|
791
|
+
const limit = options.limit ?? 10;
|
|
792
|
+
const embedder = await createEmbedder(config.embedding);
|
|
793
|
+
const [queryVector] = await embedder.embed([options.query]);
|
|
794
|
+
acquireMemoryLock(projectDir);
|
|
795
|
+
let results;
|
|
796
|
+
let trimmed;
|
|
797
|
+
try {
|
|
798
|
+
const store = new MemoryVectorStore(projectDir);
|
|
799
|
+
await store.initialize();
|
|
800
|
+
const candidateCount = Math.min(limit * 3, 150);
|
|
801
|
+
const vectorResults = await store.query(queryVector, candidateCount);
|
|
802
|
+
results = [];
|
|
803
|
+
for (const vr of vectorResults) {
|
|
804
|
+
const entry = loadEntry(projectDir, vr.id);
|
|
805
|
+
if (!entry) continue;
|
|
806
|
+
if (entry.supersededBy && !options.includeSuperseded) continue;
|
|
807
|
+
if (options.types && options.types.length > 0) {
|
|
808
|
+
if (!options.types.includes(entry.type)) continue;
|
|
809
|
+
}
|
|
810
|
+
if (options.importance && options.importance.length > 0) {
|
|
811
|
+
if (!options.importance.includes(entry.importance)) continue;
|
|
812
|
+
}
|
|
813
|
+
if (options.tags && options.tags.length > 0) {
|
|
814
|
+
const hasTag = options.tags.some((t) => entry.tags.includes(t));
|
|
815
|
+
if (!hasTag) continue;
|
|
816
|
+
}
|
|
817
|
+
if (options.since && entry.createdAt < options.since) continue;
|
|
818
|
+
results.push({
|
|
819
|
+
entry,
|
|
820
|
+
score: vr.score,
|
|
821
|
+
finalScore: vr.score
|
|
822
|
+
// Will be updated by ranking
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
applyRanking(results, config.ranking);
|
|
826
|
+
trimmed = results.slice(0, limit);
|
|
827
|
+
for (const result of trimmed) {
|
|
828
|
+
try {
|
|
829
|
+
updateEntry(projectDir, result.entry.id, {
|
|
830
|
+
accessCount: result.entry.accessCount + 1,
|
|
831
|
+
lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
832
|
+
});
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} finally {
|
|
837
|
+
releaseMemoryLock(projectDir);
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
query: options.query,
|
|
841
|
+
durationMs: Date.now() - startMs,
|
|
842
|
+
results: trimmed
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function getTopMemories(projectDir, limit) {
|
|
846
|
+
const config = loadMemoryConfig(projectDir);
|
|
847
|
+
const maxResults = limit ?? config.surfaceLimit;
|
|
848
|
+
const entries = listEntries(projectDir).filter((e) => !e.supersededBy);
|
|
849
|
+
if (entries.length === 0) return [];
|
|
850
|
+
const scored = entries.map((entry) => {
|
|
851
|
+
let score = 0;
|
|
852
|
+
const importanceScores = {
|
|
853
|
+
critical: 1,
|
|
854
|
+
high: 0.8,
|
|
855
|
+
medium: 0.5,
|
|
856
|
+
low: 0.2
|
|
857
|
+
};
|
|
858
|
+
score += importanceScores[entry.importance] ?? 0.5;
|
|
859
|
+
if (entry.lastAccessedAt) {
|
|
860
|
+
const daysSinceAccess = (Date.now() - new Date(entry.lastAccessedAt).getTime()) / (1e3 * 60 * 60 * 24);
|
|
861
|
+
if (daysSinceAccess < 7) score += 0.3;
|
|
862
|
+
else if (daysSinceAccess < 30) score += 0.1;
|
|
863
|
+
}
|
|
864
|
+
score += Math.min(entry.accessCount * 0.02, 0.2);
|
|
865
|
+
return { entry, score };
|
|
866
|
+
});
|
|
867
|
+
scored.sort((a, b) => b.score - a.score);
|
|
868
|
+
return scored.slice(0, maxResults).map((s) => s.entry);
|
|
869
|
+
}
|
|
870
|
+
function formatMemoriesForAgent(memories) {
|
|
871
|
+
if (memories.length === 0) return "";
|
|
872
|
+
const lines = [];
|
|
873
|
+
const width = 56;
|
|
874
|
+
lines.push(`\u256D${"\u2500".repeat(3)} Agent Memory ${"\u2500".repeat(width - 17)}\u256E`);
|
|
875
|
+
lines.push(`\u2502 ${memories.length} memories loaded for this codebase${" ".repeat(width - 37 - String(memories.length).length)}\u2502`);
|
|
876
|
+
lines.push(`\u251C${"\u2500".repeat(width)}\u2524`);
|
|
877
|
+
for (const mem of memories) {
|
|
878
|
+
const typeLabel = mem.type.replace(/_/g, " ");
|
|
879
|
+
const summary = mem.summary.length > width - typeLabel.length - 5 ? mem.summary.slice(0, width - typeLabel.length - 8) + "..." : mem.summary;
|
|
880
|
+
const line = `${typeLabel}: ${summary}`;
|
|
881
|
+
const padded = line.length > width - 4 ? line.slice(0, width - 4) : line + " ".repeat(width - 4 - line.length);
|
|
882
|
+
lines.push(`\u2502 ${padded} \u2502`);
|
|
883
|
+
}
|
|
884
|
+
lines.push(`\u2570${"\u2500".repeat(width)}\u256F`);
|
|
885
|
+
return lines.join("\n");
|
|
886
|
+
}
|
|
887
|
+
function watermarkPath(projectDir, sessionId) {
|
|
888
|
+
return path6.join(memoryWatermarksDir(projectDir), `${sessionId}.json`);
|
|
889
|
+
}
|
|
890
|
+
function loadWatermark(projectDir, sessionId) {
|
|
891
|
+
const filePath = watermarkPath(projectDir, sessionId);
|
|
892
|
+
if (!fs6.existsSync(filePath)) return null;
|
|
893
|
+
try {
|
|
894
|
+
return JSON.parse(fs6.readFileSync(filePath, "utf-8"));
|
|
895
|
+
} catch {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function saveWatermark(projectDir, watermark) {
|
|
900
|
+
const dir = memoryWatermarksDir(projectDir);
|
|
901
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
902
|
+
const filePath = watermarkPath(projectDir, watermark.sessionId);
|
|
903
|
+
const tmpPath = filePath + ".tmp";
|
|
904
|
+
fs6.writeFileSync(tmpPath, JSON.stringify(watermark, null, 2) + "\n", "utf-8");
|
|
905
|
+
fs6.renameSync(tmpPath, filePath);
|
|
906
|
+
}
|
|
907
|
+
function listWatermarks(projectDir) {
|
|
908
|
+
const dir = memoryWatermarksDir(projectDir);
|
|
909
|
+
if (!fs6.existsSync(dir)) return [];
|
|
910
|
+
const watermarks = [];
|
|
911
|
+
const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
912
|
+
for (const file of files) {
|
|
913
|
+
try {
|
|
914
|
+
const raw = fs6.readFileSync(path6.join(dir, file), "utf-8");
|
|
915
|
+
watermarks.push(JSON.parse(raw));
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return watermarks;
|
|
920
|
+
}
|
|
921
|
+
function isMemoryInitialized(projectDir) {
|
|
922
|
+
return fs7.existsSync(projectMemoryDir(projectDir));
|
|
923
|
+
}
|
|
924
|
+
async function getMemoryStats(projectDir) {
|
|
925
|
+
if (!isMemoryInitialized(projectDir)) {
|
|
926
|
+
return {
|
|
927
|
+
totalMemories: 0,
|
|
928
|
+
memoriesByType: {},
|
|
929
|
+
memoriesByImportance: {},
|
|
930
|
+
totalAccesses: 0,
|
|
931
|
+
sessionsCaptured: 0,
|
|
932
|
+
sessionsClassified: 0,
|
|
933
|
+
indexSizeBytes: 0,
|
|
934
|
+
lastClassifiedAt: null,
|
|
935
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
936
|
+
initialized: false
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const entries = listEntries(projectDir);
|
|
940
|
+
const sessions = listCapturedSessions(projectDir);
|
|
941
|
+
const watermarks = listWatermarks(projectDir);
|
|
942
|
+
const byType = {};
|
|
943
|
+
const byImportance = {};
|
|
944
|
+
let totalAccesses = 0;
|
|
945
|
+
for (const entry of entries) {
|
|
946
|
+
byType[entry.type] = (byType[entry.type] ?? 0) + 1;
|
|
947
|
+
byImportance[entry.importance] = (byImportance[entry.importance] ?? 0) + 1;
|
|
948
|
+
totalAccesses += entry.accessCount;
|
|
949
|
+
}
|
|
950
|
+
const store = new MemoryVectorStore(projectDir);
|
|
951
|
+
const indexSizeBytes = store.getIndexSizeBytes();
|
|
952
|
+
const classified = watermarks.filter((w) => w.status === "complete");
|
|
953
|
+
let lastClassifiedAt = null;
|
|
954
|
+
for (const w of classified) {
|
|
955
|
+
if (!lastClassifiedAt || w.updatedAt > lastClassifiedAt) {
|
|
956
|
+
lastClassifiedAt = w.updatedAt;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
totalMemories: entries.length,
|
|
961
|
+
memoriesByType: byType,
|
|
962
|
+
memoriesByImportance: byImportance,
|
|
963
|
+
totalAccesses,
|
|
964
|
+
sessionsCaptured: sessions.length,
|
|
965
|
+
sessionsClassified: classified.length,
|
|
966
|
+
indexSizeBytes,
|
|
967
|
+
lastClassifiedAt,
|
|
968
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
969
|
+
initialized: true
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function writeMemoryStats(projectDir, stats) {
|
|
973
|
+
const statsPath = memoryStatsFile(projectDir);
|
|
974
|
+
const tmpPath = statsPath + ".tmp";
|
|
975
|
+
fs7.mkdirSync(path7.dirname(statsPath), { recursive: true });
|
|
976
|
+
fs7.writeFileSync(tmpPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
977
|
+
fs7.renameSync(tmpPath, statsPath);
|
|
978
|
+
}
|
|
979
|
+
function loadSessionMeta(projectDir, sessionId) {
|
|
980
|
+
const metaPath = path8.join(sessionCaptureDir(projectDir, sessionId), "meta.json");
|
|
981
|
+
try {
|
|
982
|
+
return JSON.parse(fs8.readFileSync(metaPath, "utf-8"));
|
|
983
|
+
} catch {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function isSessionSignificant(meta, filteredEvents, minEvents) {
|
|
988
|
+
const hasWrites = meta ? meta.filesWritten > 0 : true;
|
|
989
|
+
const hasEnoughEvents = filteredEvents.length >= minEvents;
|
|
990
|
+
const hasErrorEvents = filteredEvents.some(
|
|
991
|
+
(ev) => ev.event === "tool_blocked" || ev.event === "permission_denied" || ev.event === "postcondition_failed"
|
|
992
|
+
);
|
|
993
|
+
return hasWrites || hasEnoughEvents || hasErrorEvents;
|
|
994
|
+
}
|
|
995
|
+
async function classifySession(projectDir, sessionId, onProgress) {
|
|
996
|
+
const startMs = Date.now();
|
|
997
|
+
const config = loadMemoryConfig(projectDir);
|
|
998
|
+
onProgress?.({
|
|
999
|
+
phase: "reading",
|
|
1000
|
+
current: 0,
|
|
1001
|
+
total: 0,
|
|
1002
|
+
message: "Reading and filtering events..."
|
|
1003
|
+
});
|
|
1004
|
+
const allEvents = readCapturedEvents(projectDir, sessionId);
|
|
1005
|
+
if (allEvents.length === 0) {
|
|
1006
|
+
return {
|
|
1007
|
+
sessionId,
|
|
1008
|
+
windowsProcessed: 0,
|
|
1009
|
+
memoriesExtracted: 0,
|
|
1010
|
+
memoriesStored: 0,
|
|
1011
|
+
durationMs: Date.now() - startMs
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const watermark = loadWatermark(projectDir, sessionId);
|
|
1015
|
+
const startIdx = watermark?.lastProcessedEventIndex ?? 0;
|
|
1016
|
+
if (startIdx >= allEvents.length) {
|
|
1017
|
+
return {
|
|
1018
|
+
sessionId,
|
|
1019
|
+
windowsProcessed: 0,
|
|
1020
|
+
memoriesExtracted: 0,
|
|
1021
|
+
memoriesStored: 0,
|
|
1022
|
+
durationMs: Date.now() - startMs
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const unprocessedEvents = allEvents.slice(startIdx);
|
|
1026
|
+
const filteredEvents = config.classifier.filterNoiseEvents ? filterSignificantEvents(unprocessedEvents) : unprocessedEvents;
|
|
1027
|
+
const meta = loadSessionMeta(projectDir, sessionId);
|
|
1028
|
+
if (!isSessionSignificant(meta, filteredEvents, config.classifier.minSignificantEvents)) {
|
|
1029
|
+
saveWatermark(projectDir, {
|
|
1030
|
+
sessionId,
|
|
1031
|
+
lastProcessedEventIndex: allEvents.length,
|
|
1032
|
+
lastProcessedTs: allEvents[allEvents.length - 1].ts,
|
|
1033
|
+
status: "complete",
|
|
1034
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1035
|
+
});
|
|
1036
|
+
return {
|
|
1037
|
+
sessionId,
|
|
1038
|
+
windowsProcessed: 0,
|
|
1039
|
+
memoriesExtracted: 0,
|
|
1040
|
+
memoriesStored: 0,
|
|
1041
|
+
durationMs: Date.now() - startMs
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
const allMemories = [];
|
|
1045
|
+
let windowsProcessed = 0;
|
|
1046
|
+
if (filteredEvents.length <= config.classifier.maxSinglePromptEvents) {
|
|
1047
|
+
onProgress?.({
|
|
1048
|
+
phase: "classifying",
|
|
1049
|
+
current: 1,
|
|
1050
|
+
total: 1,
|
|
1051
|
+
message: `Classifying ${filteredEvents.length} events in single prompt...`
|
|
1052
|
+
});
|
|
1053
|
+
let transcriptExcerpt;
|
|
1054
|
+
if (config.classifier.includeTranscript) {
|
|
1055
|
+
transcriptExcerpt = readCapturedTranscript(
|
|
1056
|
+
projectDir,
|
|
1057
|
+
sessionId,
|
|
1058
|
+
config.classifier.maxTranscriptChars
|
|
1059
|
+
) ?? void 0;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
const result = await classifySinglePrompt(
|
|
1063
|
+
filteredEvents,
|
|
1064
|
+
transcriptExcerpt,
|
|
1065
|
+
config.classifier.model,
|
|
1066
|
+
config.classifier.timeout
|
|
1067
|
+
);
|
|
1068
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1069
|
+
for (const mem of result.memories) {
|
|
1070
|
+
allMemories.push(toMemoryEntry(mem, sessionId, now));
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
windowsProcessed = 1;
|
|
1075
|
+
} else {
|
|
1076
|
+
const windows = buildClassificationWindows(
|
|
1077
|
+
sessionId,
|
|
1078
|
+
projectDir,
|
|
1079
|
+
filteredEvents,
|
|
1080
|
+
config
|
|
1081
|
+
);
|
|
1082
|
+
if (windows.length === 0) {
|
|
1083
|
+
saveWatermark(projectDir, {
|
|
1084
|
+
sessionId,
|
|
1085
|
+
lastProcessedEventIndex: allEvents.length,
|
|
1086
|
+
lastProcessedTs: allEvents[allEvents.length - 1].ts,
|
|
1087
|
+
status: "complete",
|
|
1088
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1089
|
+
});
|
|
1090
|
+
return {
|
|
1091
|
+
sessionId,
|
|
1092
|
+
windowsProcessed: 0,
|
|
1093
|
+
memoriesExtracted: 0,
|
|
1094
|
+
memoriesStored: 0,
|
|
1095
|
+
durationMs: Date.now() - startMs
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
let windowIdx = 0;
|
|
1099
|
+
for (const window of windows) {
|
|
1100
|
+
windowIdx++;
|
|
1101
|
+
onProgress?.({
|
|
1102
|
+
phase: "classifying",
|
|
1103
|
+
current: windowIdx,
|
|
1104
|
+
total: windows.length,
|
|
1105
|
+
message: `Classifying window ${windowIdx}/${windows.length}...`
|
|
1106
|
+
});
|
|
1107
|
+
try {
|
|
1108
|
+
const result = await classifyWindow(
|
|
1109
|
+
window,
|
|
1110
|
+
config.classifier.model,
|
|
1111
|
+
config.classifier.timeout
|
|
1112
|
+
);
|
|
1113
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1114
|
+
for (const mem of result.memories) {
|
|
1115
|
+
allMemories.push(toMemoryEntry(mem, sessionId, now));
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
windowsProcessed = windows.length;
|
|
1122
|
+
}
|
|
1123
|
+
const lastEvent = allEvents[allEvents.length - 1];
|
|
1124
|
+
if (allMemories.length === 0) {
|
|
1125
|
+
saveWatermark(projectDir, {
|
|
1126
|
+
sessionId,
|
|
1127
|
+
lastProcessedEventIndex: allEvents.length,
|
|
1128
|
+
lastProcessedTs: lastEvent.ts,
|
|
1129
|
+
status: "complete",
|
|
1130
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1131
|
+
});
|
|
1132
|
+
return {
|
|
1133
|
+
sessionId,
|
|
1134
|
+
windowsProcessed,
|
|
1135
|
+
memoriesExtracted: 0,
|
|
1136
|
+
memoriesStored: 0,
|
|
1137
|
+
durationMs: Date.now() - startMs
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
onProgress?.({
|
|
1141
|
+
phase: "embedding",
|
|
1142
|
+
current: 0,
|
|
1143
|
+
total: allMemories.length,
|
|
1144
|
+
message: `Embedding ${allMemories.length} memories...`
|
|
1145
|
+
});
|
|
1146
|
+
const embedder = await createEmbedder(config.embedding);
|
|
1147
|
+
const texts = allMemories.map((m) => m.summary);
|
|
1148
|
+
const embeddings = await embedWithBatch(embedder, texts);
|
|
1149
|
+
onProgress?.({
|
|
1150
|
+
phase: "storing",
|
|
1151
|
+
current: 0,
|
|
1152
|
+
total: allMemories.length,
|
|
1153
|
+
message: "Deduplicating and storing..."
|
|
1154
|
+
});
|
|
1155
|
+
acquireMemoryLock(projectDir);
|
|
1156
|
+
try {
|
|
1157
|
+
const store = new MemoryVectorStore(projectDir);
|
|
1158
|
+
await store.initialize();
|
|
1159
|
+
const dedupResult = await deduplicateMemories(
|
|
1160
|
+
projectDir,
|
|
1161
|
+
allMemories,
|
|
1162
|
+
embeddings,
|
|
1163
|
+
store,
|
|
1164
|
+
config.retention.deduplicationThreshold
|
|
1165
|
+
);
|
|
1166
|
+
if (dedupResult.unique.length > 0) {
|
|
1167
|
+
const vectorItems = [];
|
|
1168
|
+
for (let i = 0; i < allMemories.length; i++) {
|
|
1169
|
+
const mem = allMemories[i];
|
|
1170
|
+
if (!dedupResult.unique.includes(mem)) continue;
|
|
1171
|
+
saveEntry(projectDir, mem);
|
|
1172
|
+
vectorItems.push({
|
|
1173
|
+
id: mem.id,
|
|
1174
|
+
vector: embeddings[i],
|
|
1175
|
+
metadata: {
|
|
1176
|
+
id: mem.id,
|
|
1177
|
+
type: mem.type,
|
|
1178
|
+
importance: mem.importance,
|
|
1179
|
+
snippet: mem.summary.slice(0, 200),
|
|
1180
|
+
createdAt: mem.createdAt,
|
|
1181
|
+
tags: mem.tags.join(",")
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
await store.upsertItems(vectorItems);
|
|
1186
|
+
}
|
|
1187
|
+
saveWatermark(projectDir, {
|
|
1188
|
+
sessionId,
|
|
1189
|
+
lastProcessedEventIndex: allEvents.length,
|
|
1190
|
+
lastProcessedTs: lastEvent.ts,
|
|
1191
|
+
status: "complete",
|
|
1192
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1193
|
+
});
|
|
1194
|
+
onProgress?.({
|
|
1195
|
+
phase: "finalizing",
|
|
1196
|
+
current: dedupResult.unique.length,
|
|
1197
|
+
total: allMemories.length,
|
|
1198
|
+
message: `Stored ${dedupResult.unique.length} new memories (${dedupResult.duplicates.length} duplicates)`
|
|
1199
|
+
});
|
|
1200
|
+
return {
|
|
1201
|
+
sessionId,
|
|
1202
|
+
windowsProcessed,
|
|
1203
|
+
memoriesExtracted: allMemories.length,
|
|
1204
|
+
memoriesStored: dedupResult.unique.length,
|
|
1205
|
+
durationMs: Date.now() - startMs
|
|
1206
|
+
};
|
|
1207
|
+
} finally {
|
|
1208
|
+
releaseMemoryLock(projectDir);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function toMemoryEntry(mem, sessionId, now) {
|
|
1212
|
+
return {
|
|
1213
|
+
id: generateMemoryId(mem.type, mem.summary),
|
|
1214
|
+
type: mem.type,
|
|
1215
|
+
summary: mem.summary,
|
|
1216
|
+
detail: mem.detail ?? null,
|
|
1217
|
+
importance: mem.importance,
|
|
1218
|
+
tags: mem.tags,
|
|
1219
|
+
relatedFiles: mem.relatedFiles,
|
|
1220
|
+
source: {
|
|
1221
|
+
type: "classifier",
|
|
1222
|
+
sessionId
|
|
1223
|
+
},
|
|
1224
|
+
createdAt: now,
|
|
1225
|
+
updatedAt: now,
|
|
1226
|
+
accessCount: 0
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
async function reindexMemories(projectDir, onProgress) {
|
|
1230
|
+
const startMs = Date.now();
|
|
1231
|
+
const config = loadMemoryConfig(projectDir);
|
|
1232
|
+
const entries = listEntries(projectDir);
|
|
1233
|
+
if (entries.length === 0) {
|
|
1234
|
+
return { reindexed: 0, durationMs: Date.now() - startMs };
|
|
1235
|
+
}
|
|
1236
|
+
onProgress?.({
|
|
1237
|
+
phase: "embedding",
|
|
1238
|
+
current: 0,
|
|
1239
|
+
total: entries.length,
|
|
1240
|
+
message: `Re-embedding ${entries.length} memories...`
|
|
1241
|
+
});
|
|
1242
|
+
const embedder = await createEmbedder(config.embedding);
|
|
1243
|
+
const texts = entries.map((m) => m.summary);
|
|
1244
|
+
const embeddings = await embedWithBatch(embedder, texts);
|
|
1245
|
+
onProgress?.({
|
|
1246
|
+
phase: "storing",
|
|
1247
|
+
current: 0,
|
|
1248
|
+
total: entries.length,
|
|
1249
|
+
message: "Rebuilding vector index..."
|
|
1250
|
+
});
|
|
1251
|
+
acquireMemoryLock(projectDir);
|
|
1252
|
+
try {
|
|
1253
|
+
const lancePath = memoryLanceDir(projectDir);
|
|
1254
|
+
if (fs8.existsSync(lancePath)) {
|
|
1255
|
+
fs8.rmSync(lancePath, { recursive: true });
|
|
1256
|
+
}
|
|
1257
|
+
const store = new MemoryVectorStore(projectDir);
|
|
1258
|
+
await store.initialize();
|
|
1259
|
+
const vectorItems = entries.map((entry, i) => ({
|
|
1260
|
+
id: entry.id,
|
|
1261
|
+
vector: embeddings[i],
|
|
1262
|
+
metadata: {
|
|
1263
|
+
id: entry.id,
|
|
1264
|
+
type: entry.type,
|
|
1265
|
+
importance: entry.importance,
|
|
1266
|
+
snippet: entry.summary.slice(0, 200),
|
|
1267
|
+
createdAt: entry.createdAt,
|
|
1268
|
+
tags: entry.tags.join(",")
|
|
1269
|
+
}
|
|
1270
|
+
}));
|
|
1271
|
+
await store.upsertItems(vectorItems);
|
|
1272
|
+
onProgress?.({
|
|
1273
|
+
phase: "finalizing",
|
|
1274
|
+
current: entries.length,
|
|
1275
|
+
total: entries.length,
|
|
1276
|
+
message: `Re-indexed ${entries.length} memories`
|
|
1277
|
+
});
|
|
1278
|
+
return { reindexed: entries.length, durationMs: Date.now() - startMs };
|
|
1279
|
+
} finally {
|
|
1280
|
+
releaseMemoryLock(projectDir);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function embedWithBatch(embedder, texts) {
|
|
1284
|
+
if (texts.length === 0) return [];
|
|
1285
|
+
if (embedder.supportsBatch && embedder.submitBatch && embedder.pollBatch) {
|
|
1286
|
+
const batchId = await embedder.submitBatch(texts);
|
|
1287
|
+
const POLL_MS = 2e3;
|
|
1288
|
+
while (true) {
|
|
1289
|
+
const status = await embedder.pollBatch(batchId);
|
|
1290
|
+
if (status.status === "completed" && status.results) {
|
|
1291
|
+
return status.results;
|
|
1292
|
+
}
|
|
1293
|
+
if (status.status === "failed") {
|
|
1294
|
+
throw new Error(`Batch embedding failed (batch ${batchId})`);
|
|
1295
|
+
}
|
|
1296
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_MS));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return embedder.embed(texts);
|
|
1300
|
+
}
|
|
1301
|
+
async function rememberMemory(projectDir, entry) {
|
|
1302
|
+
const config = loadMemoryConfig(projectDir);
|
|
1303
|
+
const embedder = await createEmbedder(config.embedding);
|
|
1304
|
+
const [vector] = await embedder.embed([entry.summary]);
|
|
1305
|
+
acquireMemoryLock(projectDir);
|
|
1306
|
+
try {
|
|
1307
|
+
const store = new MemoryVectorStore(projectDir);
|
|
1308
|
+
await store.initialize();
|
|
1309
|
+
saveEntry(projectDir, entry);
|
|
1310
|
+
await store.upsertItems([{
|
|
1311
|
+
id: entry.id,
|
|
1312
|
+
vector,
|
|
1313
|
+
metadata: {
|
|
1314
|
+
id: entry.id,
|
|
1315
|
+
type: entry.type,
|
|
1316
|
+
importance: entry.importance,
|
|
1317
|
+
snippet: entry.summary.slice(0, 200),
|
|
1318
|
+
createdAt: entry.createdAt,
|
|
1319
|
+
tags: entry.tags.join(",")
|
|
1320
|
+
}
|
|
1321
|
+
}]);
|
|
1322
|
+
} finally {
|
|
1323
|
+
releaseMemoryLock(projectDir);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function memoryBranchExists(projectDir) {
|
|
1327
|
+
const branch = getMemoryBranch();
|
|
1328
|
+
try {
|
|
1329
|
+
execFileSync2("git", ["rev-parse", "--verify", `refs/heads/${branch}`], {
|
|
1330
|
+
cwd: projectDir,
|
|
1331
|
+
stdio: "pipe",
|
|
1332
|
+
timeout: 5e3
|
|
1333
|
+
});
|
|
1334
|
+
return true;
|
|
1335
|
+
} catch {
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function initMemoryBranch(projectDir) {
|
|
1340
|
+
const branch = getMemoryBranch();
|
|
1341
|
+
if (memoryBranchExists(projectDir)) return;
|
|
1342
|
+
const tmpDir = fs9.mkdtempSync(path9.join(os3.tmpdir(), "ulpi-memory-"));
|
|
1343
|
+
try {
|
|
1344
|
+
execFileSync2("git", ["worktree", "add", "--detach", tmpDir], {
|
|
1345
|
+
cwd: projectDir,
|
|
1346
|
+
stdio: "pipe",
|
|
1347
|
+
timeout: 1e4
|
|
1348
|
+
});
|
|
1349
|
+
execFileSync2("git", ["checkout", "--orphan", branch], {
|
|
1350
|
+
cwd: tmpDir,
|
|
1351
|
+
stdio: "pipe",
|
|
1352
|
+
timeout: 5e3
|
|
1353
|
+
});
|
|
1354
|
+
execFileSync2("git", ["rm", "-rf", "--cached", "."], {
|
|
1355
|
+
cwd: tmpDir,
|
|
1356
|
+
stdio: "pipe",
|
|
1357
|
+
timeout: 5e3
|
|
1358
|
+
});
|
|
1359
|
+
fs9.writeFileSync(
|
|
1360
|
+
path9.join(tmpDir, "README.md"),
|
|
1361
|
+
"# ULPI Agent Memory\n\nThis branch stores agent memory data.\n"
|
|
1362
|
+
);
|
|
1363
|
+
execFileSync2("git", ["add", "README.md"], {
|
|
1364
|
+
cwd: tmpDir,
|
|
1365
|
+
stdio: "pipe",
|
|
1366
|
+
timeout: 5e3
|
|
1367
|
+
});
|
|
1368
|
+
execFileSync2("git", ["commit", "-m", "init: agent memory branch"], {
|
|
1369
|
+
cwd: tmpDir,
|
|
1370
|
+
stdio: "pipe",
|
|
1371
|
+
timeout: 1e4
|
|
1372
|
+
});
|
|
1373
|
+
} finally {
|
|
1374
|
+
try {
|
|
1375
|
+
execFileSync2("git", ["worktree", "remove", "--force", tmpDir], {
|
|
1376
|
+
cwd: projectDir,
|
|
1377
|
+
stdio: "pipe",
|
|
1378
|
+
timeout: 1e4
|
|
1379
|
+
});
|
|
1380
|
+
} catch {
|
|
1381
|
+
try {
|
|
1382
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function exportMemories(projectDir) {
|
|
1389
|
+
const branch = getMemoryBranch();
|
|
1390
|
+
if (!memoryBranchExists(projectDir)) {
|
|
1391
|
+
initMemoryBranch(projectDir);
|
|
1392
|
+
}
|
|
1393
|
+
const entries = listEntries(projectDir);
|
|
1394
|
+
const config = loadMemoryConfig(projectDir);
|
|
1395
|
+
const exportData = {
|
|
1396
|
+
version: 1,
|
|
1397
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1398
|
+
projectDir,
|
|
1399
|
+
memories: entries,
|
|
1400
|
+
config
|
|
1401
|
+
};
|
|
1402
|
+
const tmpDir = fs9.mkdtempSync(path9.join(os3.tmpdir(), "ulpi-memory-export-"));
|
|
1403
|
+
try {
|
|
1404
|
+
execFileSync2("git", ["worktree", "add", tmpDir, branch], {
|
|
1405
|
+
cwd: projectDir,
|
|
1406
|
+
stdio: "pipe",
|
|
1407
|
+
timeout: 1e4
|
|
1408
|
+
});
|
|
1409
|
+
fs9.writeFileSync(
|
|
1410
|
+
path9.join(tmpDir, "memories.json"),
|
|
1411
|
+
JSON.stringify(exportData, null, 2) + "\n"
|
|
1412
|
+
);
|
|
1413
|
+
execFileSync2("git", ["add", "memories.json"], {
|
|
1414
|
+
cwd: tmpDir,
|
|
1415
|
+
stdio: "pipe",
|
|
1416
|
+
timeout: 5e3
|
|
1417
|
+
});
|
|
1418
|
+
try {
|
|
1419
|
+
execFileSync2("git", ["diff", "--cached", "--quiet"], {
|
|
1420
|
+
cwd: tmpDir,
|
|
1421
|
+
stdio: "pipe",
|
|
1422
|
+
timeout: 5e3
|
|
1423
|
+
});
|
|
1424
|
+
const sha2 = execFileSync2("git", ["rev-parse", "HEAD"], {
|
|
1425
|
+
cwd: tmpDir,
|
|
1426
|
+
encoding: "utf-8",
|
|
1427
|
+
timeout: 5e3
|
|
1428
|
+
}).trim();
|
|
1429
|
+
return { branchName: branch, commitSha: sha2, memoriesExported: entries.length };
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
execFileSync2("git", [
|
|
1433
|
+
"commit",
|
|
1434
|
+
"-m",
|
|
1435
|
+
`memory: export ${entries.length} memories`
|
|
1436
|
+
], {
|
|
1437
|
+
cwd: tmpDir,
|
|
1438
|
+
stdio: "pipe",
|
|
1439
|
+
timeout: 1e4
|
|
1440
|
+
});
|
|
1441
|
+
const sha = execFileSync2("git", ["rev-parse", "HEAD"], {
|
|
1442
|
+
cwd: tmpDir,
|
|
1443
|
+
encoding: "utf-8",
|
|
1444
|
+
timeout: 5e3
|
|
1445
|
+
}).trim();
|
|
1446
|
+
return { branchName: branch, commitSha: sha, memoriesExported: entries.length };
|
|
1447
|
+
} finally {
|
|
1448
|
+
try {
|
|
1449
|
+
execFileSync2("git", ["worktree", "remove", "--force", tmpDir], {
|
|
1450
|
+
cwd: projectDir,
|
|
1451
|
+
stdio: "pipe",
|
|
1452
|
+
timeout: 1e4
|
|
1453
|
+
});
|
|
1454
|
+
} catch {
|
|
1455
|
+
try {
|
|
1456
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function importMemories(projectDir) {
|
|
1463
|
+
const branch = getMemoryBranch();
|
|
1464
|
+
if (!memoryBranchExists(projectDir)) {
|
|
1465
|
+
return { success: false, memoriesImported: 0, message: "Memory branch does not exist" };
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
const raw = execFileSync2(
|
|
1469
|
+
"git",
|
|
1470
|
+
["show", `${branch}:memories.json`],
|
|
1471
|
+
{ cwd: projectDir, encoding: "utf-8", timeout: 1e4 }
|
|
1472
|
+
);
|
|
1473
|
+
const exportData = JSON.parse(raw);
|
|
1474
|
+
if (!exportData.memories || !Array.isArray(exportData.memories)) {
|
|
1475
|
+
return { success: false, memoriesImported: 0, message: "Invalid export data" };
|
|
1476
|
+
}
|
|
1477
|
+
const entriesDir = memoryEntriesDir(projectDir);
|
|
1478
|
+
fs9.mkdirSync(entriesDir, { recursive: true });
|
|
1479
|
+
let imported = 0;
|
|
1480
|
+
for (const entry of exportData.memories) {
|
|
1481
|
+
const filePath = path9.join(entriesDir, `${entry.id}.json`);
|
|
1482
|
+
if (!fs9.existsSync(filePath)) {
|
|
1483
|
+
fs9.writeFileSync(filePath, JSON.stringify(entry, null, 2) + "\n", "utf-8");
|
|
1484
|
+
imported++;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
success: true,
|
|
1489
|
+
memoriesImported: imported,
|
|
1490
|
+
message: `Imported ${imported} new memories (${exportData.memories.length - imported} already exist)`
|
|
1491
|
+
};
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1494
|
+
return { success: false, memoriesImported: 0, message };
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
export {
|
|
1499
|
+
DEFAULT_MEMORY_CONFIG,
|
|
1500
|
+
loadMemoryConfig,
|
|
1501
|
+
saveMemoryConfig,
|
|
1502
|
+
isMemoryEnabled,
|
|
1503
|
+
generateMemoryId,
|
|
1504
|
+
MemoryVectorStore,
|
|
1505
|
+
saveEntry,
|
|
1506
|
+
loadEntry,
|
|
1507
|
+
removeEntry,
|
|
1508
|
+
listEntries,
|
|
1509
|
+
updateEntry,
|
|
1510
|
+
redactContent,
|
|
1511
|
+
appendMemoryEvent,
|
|
1512
|
+
toClassificationEvent,
|
|
1513
|
+
finalizeCapture,
|
|
1514
|
+
isSessionCaptured,
|
|
1515
|
+
listCapturedSessions,
|
|
1516
|
+
readCapturedEvents,
|
|
1517
|
+
readCapturedTranscript,
|
|
1518
|
+
filterSignificantEvents,
|
|
1519
|
+
buildClassificationWindows,
|
|
1520
|
+
buildClassificationPrompt,
|
|
1521
|
+
buildSingleClassificationPrompt,
|
|
1522
|
+
classifyWindow,
|
|
1523
|
+
classifySinglePrompt,
|
|
1524
|
+
parseClassifierOutput,
|
|
1525
|
+
deduplicateMemories,
|
|
1526
|
+
applyRanking,
|
|
1527
|
+
acquireMemoryLock,
|
|
1528
|
+
releaseMemoryLock,
|
|
1529
|
+
isMemoryLocked,
|
|
1530
|
+
searchMemory,
|
|
1531
|
+
getTopMemories,
|
|
1532
|
+
formatMemoriesForAgent,
|
|
1533
|
+
loadWatermark,
|
|
1534
|
+
saveWatermark,
|
|
1535
|
+
listWatermarks,
|
|
1536
|
+
isMemoryInitialized,
|
|
1537
|
+
getMemoryStats,
|
|
1538
|
+
writeMemoryStats,
|
|
1539
|
+
classifySession,
|
|
1540
|
+
reindexMemories,
|
|
1541
|
+
rememberMemory,
|
|
1542
|
+
memoryBranchExists,
|
|
1543
|
+
initMemoryBranch,
|
|
1544
|
+
exportMemories,
|
|
1545
|
+
importMemories
|
|
1546
|
+
};
|