@amemhq/core 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,932 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli-migrate.ts
32
+ var cli_migrate_exports = {};
33
+ __export(cli_migrate_exports, {
34
+ parseArgs: () => parseArgs
35
+ });
36
+ module.exports = __toCommonJS(cli_migrate_exports);
37
+
38
+ // src/embedding.ts
39
+ var pipeline = null;
40
+ var extractor = null;
41
+ var loadedKey = null;
42
+ var cachedDim = null;
43
+ var DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
44
+ function getEmbeddingModel() {
45
+ return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
46
+ }
47
+ var CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
48
+ "bge-m3",
49
+ "bge-base-zh-v1.5",
50
+ "bge-small-zh-v1.5",
51
+ "bge-base-en-v1.5",
52
+ "bge-small-en-v1.5",
53
+ "bge-large-en-v1.5",
54
+ "gte-multilingual-base",
55
+ "gte-modernbert-base",
56
+ "gte-large-en-v1.5",
57
+ "snowflake-arctic-embed-m",
58
+ "snowflake-arctic-embed-l"
59
+ ]);
60
+ function getEmbeddingPooling() {
61
+ const explicit = process.env.AMEM_EMBED_POOLING?.trim().toLowerCase();
62
+ if (explicit === "mean" || explicit === "cls") return explicit;
63
+ const basename = getEmbeddingModel().split("/").pop()?.toLowerCase() ?? "";
64
+ return CLS_POOLED_MODELS.has(basename) ? "cls" : "mean";
65
+ }
66
+ function getEmbeddingDevice() {
67
+ return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
68
+ }
69
+ function getEmbeddingDtype() {
70
+ return process.env.AMEM_EMBED_DTYPE?.trim() || void 0;
71
+ }
72
+ function extractorKey() {
73
+ return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
74
+ }
75
+ async function getExtractor() {
76
+ const wanted = extractorKey();
77
+ if (extractor && loadedKey === wanted) return extractor;
78
+ if (!pipeline) {
79
+ const mod = await import("@huggingface/transformers");
80
+ pipeline = mod.pipeline;
81
+ }
82
+ const device = getEmbeddingDevice();
83
+ const dtype = getEmbeddingDtype();
84
+ extractor = await pipeline("feature-extraction", getEmbeddingModel(), {
85
+ revision: "main",
86
+ // Omitted entirely when unset, so an unconfigured install gets exactly the
87
+ // library defaults it got before these existed.
88
+ ...device ? { device } : {},
89
+ ...dtype ? { dtype } : {}
90
+ });
91
+ loadedKey = wanted;
92
+ cachedDim = null;
93
+ return extractor;
94
+ }
95
+ async function getEmbeddingDim() {
96
+ if (cachedDim !== null && loadedKey === extractorKey()) return cachedDim;
97
+ const probe = await encode("dimension probe");
98
+ cachedDim = probe.length;
99
+ return cachedDim;
100
+ }
101
+ function poolNormalize(output, attentionMask, mode) {
102
+ const seqLen = output.length;
103
+ const dim = output[0].length;
104
+ const pooled = new Array(dim).fill(0);
105
+ if (mode === "cls") {
106
+ for (let j = 0; j < dim; j++) pooled[j] = output[0][j];
107
+ } else {
108
+ let maskSum = 0;
109
+ for (let i = 0; i < seqLen; i++) {
110
+ const m = attentionMask[i];
111
+ maskSum += m;
112
+ for (let j = 0; j < dim; j++) {
113
+ pooled[j] += output[i][j] * m;
114
+ }
115
+ }
116
+ for (let j = 0; j < dim; j++) {
117
+ pooled[j] /= Math.max(maskSum, 1e-9);
118
+ }
119
+ }
120
+ let norm = 0;
121
+ for (const v of pooled) norm += v * v;
122
+ norm = Math.sqrt(norm);
123
+ return pooled.map((v) => v / Math.max(norm, 1e-9));
124
+ }
125
+ async function encode(text) {
126
+ const ext = await getExtractor();
127
+ const pooling = getEmbeddingPooling();
128
+ const result = await ext(text, { pooling, normalize: true });
129
+ if (result && result.data) {
130
+ return Array.from(result.data);
131
+ }
132
+ const tensor = result;
133
+ if (tensor.dims && tensor.dims.length === 3) {
134
+ const seqLen = tensor.dims[1];
135
+ const dim = tensor.dims[2];
136
+ const raw = [];
137
+ for (let i = 0; i < seqLen; i++) {
138
+ const row = [];
139
+ for (let j = 0; j < dim; j++) {
140
+ row.push(tensor.data[i * dim + j]);
141
+ }
142
+ raw.push(row);
143
+ }
144
+ return poolNormalize(raw, new Array(seqLen).fill(1), pooling);
145
+ }
146
+ throw new Error("Unexpected embedding output shape");
147
+ }
148
+
149
+ // src/storage.ts
150
+ var QDRANT_URL = "http://localhost:6333";
151
+ var getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
152
+ async function recordCollectionModel(collection, model) {
153
+ try {
154
+ await qdrant("PATCH", `/collections/${collection}`, { metadata: { embedding_model: model } });
155
+ } catch {
156
+ }
157
+ }
158
+ async function qdrant(method, path2, body) {
159
+ const res = await fetch(`${QDRANT_URL}${path2}`, {
160
+ method,
161
+ headers: { "Content-Type": "application/json" },
162
+ body: body ? JSON.stringify(body) : void 0
163
+ });
164
+ const data = await res.json();
165
+ if (!res.ok || data.status && data.status !== "ok" && data.status !== "acknowledged") {
166
+ throw new Error(`Qdrant ${method} ${path2} failed: ${data.error || JSON.stringify(data)}`);
167
+ }
168
+ return data.result;
169
+ }
170
+ async function scrollAllRaw(collection, limit = 1e4) {
171
+ const out = [];
172
+ let offset = void 0;
173
+ for (; ; ) {
174
+ const body = { with_payload: true, with_vector: true, limit };
175
+ if (offset !== void 0 && offset !== null) body.offset = offset;
176
+ const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
177
+ out.push(...res.points);
178
+ offset = res.next_page_offset;
179
+ if (offset === void 0 || offset === null || res.points.length === 0) break;
180
+ }
181
+ return out;
182
+ }
183
+ async function countPointsRaw(collection) {
184
+ const res = await qdrant("POST", `/collections/${collection}/points/count`, { exact: true });
185
+ return res.count;
186
+ }
187
+ async function collectionDimRaw(collection) {
188
+ try {
189
+ const info = await qdrant("GET", `/collections/${collection}`);
190
+ return info.config?.params?.vectors?.size ?? null;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ async function createCollectionRaw(collection, size) {
196
+ await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
197
+ await recordCollectionModel(collection, getEmbeddingModel());
198
+ for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
199
+ await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
200
+ }
201
+ }
202
+ async function upsertPointsRaw(collection, points) {
203
+ await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
204
+ }
205
+ function noteToPoint(note) {
206
+ return {
207
+ id: note.id,
208
+ vector: note.embedding,
209
+ payload: {
210
+ content: note.content,
211
+ keywords: note.keywords,
212
+ tags: note.tags,
213
+ context: note.context,
214
+ links: note.links,
215
+ timestamp: note.timestamp,
216
+ agent_id: note.agent_id,
217
+ hash: note.hash,
218
+ // 13-A
219
+ retrieval_count: note.retrieval_count ?? 0,
220
+ last_accessed: note.last_accessed || note.timestamp,
221
+ // 13-B: stored as JSON string (Qdrant payload can't handle nested array-of-objects)
222
+ evolution_history: JSON.stringify(note.evolution_history ?? []),
223
+ // 13-E
224
+ category: note.category || "General",
225
+ is_active: note.is_active !== false,
226
+ // 26B
227
+ topics: note.topics ?? [],
228
+ // 26A
229
+ note_type: note.note_type || "memory",
230
+ // 29
231
+ pending_merge: note.pending_merge ?? false,
232
+ // 30
233
+ evolution_type: note.evolution_type || "",
234
+ conflict: note.conflict ?? false,
235
+ conflicts_with: note.conflicts_with ?? [],
236
+ conflict_reason: note.conflict_reason ?? "",
237
+ conflict_scanned_at: note.conflict_scanned_at ?? "",
238
+ subjects: note.subjects ?? [],
239
+ // 31
240
+ ephemeral: note.ephemeral ?? false,
241
+ low_quality: note.low_quality ?? false,
242
+ // 32
243
+ owner: note.owner || note.agent_id,
244
+ readers: note.readers ?? [note.agent_id],
245
+ writers: note.writers ?? [note.agent_id]
246
+ }
247
+ };
248
+ }
249
+ function pointToNote(point) {
250
+ const p = point.payload;
251
+ const timestamp = p.timestamp || "";
252
+ let evolutionHistory = [];
253
+ try {
254
+ const raw = p.evolution_history;
255
+ if (typeof raw === "string" && raw.length > 0) {
256
+ evolutionHistory = JSON.parse(raw);
257
+ } else if (Array.isArray(raw)) {
258
+ evolutionHistory = raw;
259
+ }
260
+ } catch {
261
+ evolutionHistory = [];
262
+ }
263
+ return {
264
+ id: String(point.id),
265
+ content: p.content || "",
266
+ keywords: p.keywords || [],
267
+ tags: p.tags || [],
268
+ context: p.context || "",
269
+ links: p.links || [],
270
+ timestamp,
271
+ agent_id: p.agent_id || "main",
272
+ embedding: point.vector || [],
273
+ hash: p.hash || "",
274
+ // 13-A
275
+ retrieval_count: typeof p.retrieval_count === "number" ? p.retrieval_count : 0,
276
+ last_accessed: p.last_accessed || timestamp,
277
+ // 13-B
278
+ evolution_history: evolutionHistory,
279
+ // 13-E
280
+ category: p.category || "General",
281
+ is_active: p.is_active !== false,
282
+ // 26A
283
+ note_type: p.note_type === "knowledge" ? "knowledge" : "memory",
284
+ // 26B
285
+ topics: Array.isArray(p.topics) ? p.topics : [],
286
+ // 29
287
+ pending_merge: p.pending_merge === true,
288
+ // 30
289
+ evolution_type: typeof p.evolution_type === "string" && ["EVOLVE", "CONFLICT", "EXPAND", "NEW"].includes(p.evolution_type) ? p.evolution_type : void 0,
290
+ conflict: p.conflict === true,
291
+ conflicts_with: Array.isArray(p.conflicts_with) ? p.conflicts_with.filter((v) => typeof v === "string") : [],
292
+ conflict_reason: typeof p.conflict_reason === "string" ? p.conflict_reason : "",
293
+ conflict_scanned_at: typeof p.conflict_scanned_at === "string" ? p.conflict_scanned_at : "",
294
+ subjects: Array.isArray(p.subjects) ? p.subjects.filter((v) => typeof v === "string") : [],
295
+ // 31
296
+ ephemeral: p.ephemeral === true,
297
+ low_quality: p.low_quality === true,
298
+ // 32
299
+ owner: p.owner || p.agent_id || "main",
300
+ readers: Array.isArray(p.readers) ? p.readers : [p.agent_id || "main"],
301
+ writers: Array.isArray(p.writers) ? p.writers : [p.agent_id || "main"]
302
+ };
303
+ }
304
+
305
+ // src/llm.ts
306
+ var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
307
+ var import_openai = __toESM(require("openai"), 1);
308
+
309
+ // src/prompts.ts
310
+ var LOCALE = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
311
+ var en = {
312
+ crudDecision: (userText, assistantText, memoryList) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.
313
+
314
+ ## Conversation
315
+
316
+ User: ${userText}
317
+ Assistant: ${assistantText}
318
+
319
+ ## Existing relevant memories (identified by integer idx)
320
+
321
+ ${memoryList}
322
+
323
+ ## Task
324
+
325
+ Extract only genuinely important long-term facts (decisions, preferences, account info, project status, key insights). Skip small talk, confirmations, and information already captured in existing memories.
326
+
327
+ ## Operation types
328
+ - NEW: Extract a brand new fact not present in existing memories
329
+ - UPDATE: New information refines or supersedes an existing memory; specify existingIdx
330
+ - DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content
331
+ - NONE: Nothing worth recording, or information already fully captured
332
+
333
+ ## Output format
334
+
335
+ Return a JSON array. Each item:
336
+ {"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "fact content", "existingIdx": integer or omit, "reason": "optional"}
337
+
338
+ Return at most 3 operations. If nothing is worth recording, return [].
339
+ Return only the JSON array, no other text.
340
+
341
+ Examples:
342
+
343
+ 1. New preference:
344
+ [{"action": "NEW", "fact": "User prefers TypeScript over JavaScript", "reason": "Explicitly stated tech preference"}]
345
+
346
+ 2. Updating an existing memory (idx 0 was "User is evaluating React and Vue"):
347
+ [{"action": "UPDATE", "fact": "User decided to use React (dropped Vue)", "existingIdx": 0, "reason": "Decision finalized, update evaluation status"}]
348
+
349
+ 3. Conversation is just "Sure, thanks" / "Got it" with no new info:
350
+ []`,
351
+ shouldMerge: (contentA, contentB) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.
352
+
353
+ Memory A: ${contentA}
354
+ Memory B: ${contentB}
355
+
356
+ Rules:
357
+ - If both memories express the same core fact (possibly different wording or granularity), return:
358
+ {"shouldMerge": true, "merged": "Concise merged statement preserving key details from both, more complete than either alone"}
359
+ - If the memories are complementary, on different topics, or contain different specific facts, return:
360
+ {"shouldMerge": false}
361
+
362
+ Return only JSON, no other text.
363
+
364
+ Examples:
365
+
366
+ 1. Should merge (different granularity):
367
+ A: "Project uses PostgreSQL"
368
+ B: "Project's primary database is PostgreSQL 16, deployed on AWS RDS"
369
+ -> {"shouldMerge": true, "merged": "Project uses PostgreSQL 16 as primary database, deployed on AWS RDS"}
370
+
371
+ 2. Should NOT merge (complementary but distinct):
372
+ A: "User prefers VS Code"
373
+ B: "User's VS Code uses One Dark Pro theme"
374
+ -> {"shouldMerge": false}`,
375
+ evolutionJudge: (oldContent, newContent) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.
376
+
377
+ Old memory: ${oldContent}
378
+ New memory: ${newContent}
379
+
380
+ Classification rules:
381
+
382
+ - EVOLVE: New content deepens or updates the old memory (e.g. "Considering Next.js" -> "Decided on Next.js 14 App Router")
383
+ Return: {"type": "EVOLVE", "mergedContent": "Merged content preserving the evolution trajectory"}
384
+
385
+ - CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. "Uses MySQL as primary DB" vs "Migrated to PostgreSQL")
386
+ Return: {"type": "CONFLICT"}
387
+
388
+ - EXPAND: New information supplements the old memory on the same topic (e.g. "Handles backend dev" + "Backend uses Go and gRPC")
389
+ Return: {"type": "EXPAND", "mergedContent": "Merged content integrating both pieces of information"}
390
+
391
+ - NEW: Completely unrelated information, no substantive connection to the old memory
392
+ Return: {"type": "NEW"}
393
+
394
+ Return only JSON, no other text.`,
395
+ conflictScan: (numberedNotes) => `You are auditing a person's memory store for CONTRADICTIONS.
396
+
397
+ Below are numbered memories. Find pairs that CANNOT both be true of the same person at the same time.
398
+
399
+ ${numberedNotes}
400
+
401
+ What counts as a contradiction:
402
+ - The same attribute holding two incompatible values ("lives in Paris" vs "moved to Berlin")
403
+ - A stated preference or constraint that a later memory violates ("is vegetarian" vs "loved the steak")
404
+ - A fact that a later memory supersedes ("uses MySQL" vs "migrated to PostgreSQL")
405
+
406
+ What does NOT count \u2014 be strict, these are the common false positives:
407
+ - Additive facts. Two things can both be true ("has a dog named Buddy" + "adopted a second dog, Scout" is NOT a contradiction)
408
+ - Change over time that both memories already acknowledge
409
+ - Merely similar or related topics
410
+ - Different contexts (likes coffee at work, tea at home)
411
+
412
+ For each contradicting pair, also say which one is SUPERSEDED \u2014 the one that is
413
+ no longer true. Judge this from the WORDING, not from any assumed order: phrases
414
+ like "used to", "back in 2019", "moved last month", "switched to" tell you which
415
+ statement describes the past. The memories are NOT listed in chronological order,
416
+ and the number does not imply age.
417
+
418
+ If you cannot tell which one is superseded, set it to null. That is a normal and
419
+ useful answer \u2014 say null rather than guessing, because a wrong guess retires a
420
+ memory that is still true.
421
+
422
+ Return ONLY a JSON array. Empty array if nothing genuinely contradicts:
423
+ [{"a": 0, "b": 3, "superseded": 0, "reason": "one short sentence naming the incompatible attribute"}]
424
+
425
+ "superseded" must be either the value of "a", the value of "b", or null.
426
+ Use the numbers shown. Report a pair once. Prefer returning nothing over guessing.`
427
+ };
428
+ var zh = {
429
+ crudDecision: (userText, assistantText, memoryList) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u7BA1\u7406 agent\uFF0C\u8D1F\u8D23\u5206\u6790\u5BF9\u8BDD\u5185\u5BB9\u5E76\u51B3\u5B9A\u5982\u4F55\u64CD\u4F5C\u8BB0\u5FC6\u5E93\u3002
430
+
431
+ ## \u5BF9\u8BDD\u5185\u5BB9
432
+
433
+ \u7528\u6237\uFF1A${userText}
434
+ \u52A9\u624B\uFF1A${assistantText}
435
+
436
+ ## \u5DF2\u6709\u76F8\u5173\u8BB0\u5FC6\uFF08\u7528\u6574\u6570 idx \u6807\u8BC6\uFF09
437
+
438
+ ${memoryList}
439
+
440
+ ## \u4EFB\u52A1
441
+
442
+ \u5206\u6790\u4E0A\u8FF0\u5BF9\u8BDD\uFF0C\u51B3\u5B9A\u9700\u8981\u54EA\u4E9B\u8BB0\u5FC6\u64CD\u4F5C\u3002\u53EA\u63D0\u53D6\u771F\u6B63\u91CD\u8981\u7684\u957F\u671F\u4E8B\u5B9E\uFF08\u51B3\u7B56\u3001\u504F\u597D\u3001\u8D26\u53F7\u4FE1\u606F\u3001\u9879\u76EE\u72B6\u6001\u3001\u5173\u952E\u6D1E\u5BDF\uFF09\u3002\u8DF3\u8FC7\u95F2\u804A\u3001\u786E\u8BA4\u8BED\u3001\u91CD\u590D\u4FE1\u606F\u3002
443
+
444
+ ## \u64CD\u4F5C\u7C7B\u578B
445
+ - NEW\uFF1A\u63D0\u53D6\u5168\u65B0\u4E8B\u5B9E\uFF08\u5DF2\u6709\u8BB0\u5FC6\u4E2D\u6CA1\u6709\u7684\u4FE1\u606F\uFF09
446
+ - UPDATE\uFF1A\u65B0\u4FE1\u606F\u66F4\u65B0\u4E86\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\uFF0C\u7528 existingIdx \u6307\u5B9A\u8981\u66F4\u65B0\u7684\u6761\u76EE
447
+ - DELETE\uFF1A\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\u5DF2\u7ECF\u8FC7\u65F6\u3001\u53D1\u751F\u51B2\u7A81\u6216\u9519\u8BEF\uFF0C\u7528 existingIdx \u6307\u5B9A\uFF0Cfact \u586B\u539F\u5185\u5BB9
448
+ - NONE\uFF1A\u4E0D\u503C\u5F97\u8BB0\u5F55\u6216\u5DF2\u6709\u5B8C\u5168\u76F8\u540C\u7684\u4FE1\u606F
449
+
450
+ ## \u8F93\u51FA\u683C\u5F0F
451
+
452
+ \u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u6BCF\u6761\u683C\u5F0F\uFF1A
453
+ {"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "\u4E8B\u5B9E\u5185\u5BB9", "existingIdx": \u6574\u6570\u6216\u7701\u7565, "reason": "\u539F\u56E0\uFF08\u53EF\u9009\uFF09"}
454
+
455
+ \u6BCF\u6B21\u6700\u591A\u8FD4\u56DE 3 \u6761\u64CD\u4F5C\u3002\u5982\u679C\u6CA1\u6709\u503C\u5F97\u64CD\u4F5C\u7684\u5185\u5BB9\uFF0C\u8FD4\u56DE []\u3002
456
+ \u53EA\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
457
+
458
+ \u793A\u4F8B\uFF1A
459
+
460
+ 1. \u63D0\u53D6\u65B0\u504F\u597D\uFF1A
461
+ [{"action": "NEW", "fact": "\u7528\u6237\u504F\u597D TypeScript \u800C\u975E JavaScript", "reason": "\u660E\u786E\u8868\u8FBE\u7684\u6280\u672F\u504F\u597D"}]
462
+
463
+ 2. \u66F4\u65B0\u5DF2\u6709\u8BB0\u5FC6\uFF08idx 0 \u539F\u4E3A"\u7528\u6237\u6B63\u5728\u8BC4\u4F30 React \u548C Vue"\uFF09\uFF1A
464
+ [{"action": "UPDATE", "fact": "\u7528\u6237\u51B3\u5B9A\u4F7F\u7528 React\uFF08\u653E\u5F03\u4E86 Vue\uFF09", "existingIdx": 0, "reason": "\u51B3\u7B56\u5DF2\u660E\u786E\uFF0C\u66F4\u65B0\u8BC4\u4F30\u72B6\u6001"}]
465
+
466
+ 3. \u5BF9\u8BDD\u4EC5\u4E3A"\u597D\u7684\uFF0C\u8C22\u8C22"/"\u6CA1\u95EE\u9898"\u7B49\u786E\u8BA4\u8BED\uFF0C\u65E0\u65B0\u4FE1\u606F\uFF1A
467
+ []`,
468
+ shouldMerge: (contentA, contentB) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u53BB\u91CD\u52A9\u624B\uFF0C\u8D1F\u8D23\u5224\u65AD\u4E24\u6761\u8BB0\u5FC6\u662F\u5426\u8868\u8FBE\u4E86\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\u3002
469
+
470
+ \u8BB0\u5FC6A\uFF1A${contentA}
471
+ \u8BB0\u5FC6B\uFF1A${contentB}
472
+
473
+ \u5224\u65AD\u89C4\u5219\uFF1A
474
+ - \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u8868\u8FBE\u7684\u662F\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\uFF08\u53EF\u80FD\u63AA\u8F9E\u4E0D\u540C\u3001\u7C92\u5EA6\u4E0D\u540C\uFF0C\u4F46\u6838\u5FC3\u4E8B\u5B9E\u4E00\u81F4\uFF09\uFF0C\u8FD4\u56DE JSON\uFF1A
475
+ {"shouldMerge": true, "merged": "\u5408\u5E76\u540E\u7684\u7B80\u6D01\u8868\u8FF0\uFF0C\u4FDD\u7559\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u952E\u4FE1\u606F\uFF0C\u6BD4\u4EFB\u4F55\u4E00\u6761\u90FD\u66F4\u5B8C\u6574"}
476
+ - \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u662F\u4E92\u8865\u4FE1\u606F\u3001\u4E0D\u540C\u4E3B\u9898\u3001\u6216\u5305\u542B\u4E0D\u540C\u7684\u5177\u4F53\u4E8B\u5B9E\uFF0C\u8FD4\u56DE JSON\uFF1A
477
+ {"shouldMerge": false}
478
+
479
+ \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
480
+
481
+ \u793A\u4F8B\uFF1A
482
+
483
+ 1. \u5E94\u5408\u5E76\uFF08\u7C92\u5EA6\u4E0D\u540C\uFF09\uFF1A
484
+ A: "\u9879\u76EE\u4F7F\u7528 PostgreSQL \u6570\u636E\u5E93"
485
+ B: "\u9879\u76EE\u7684\u4E3B\u6570\u636E\u5E93\u662F PostgreSQL 16\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"
486
+ \u2192 {"shouldMerge": true, "merged": "\u9879\u76EE\u4F7F\u7528 PostgreSQL 16 \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"}
487
+
488
+ 2. \u4E0D\u5E94\u5408\u5E76\uFF08\u4E92\u8865\u4F46\u4E0D\u540C\uFF09\uFF1A
489
+ A: "\u7528\u6237\u559C\u6B22\u7528 VS Code"
490
+ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
491
+ \u2192 {"shouldMerge": false}`,
492
+ evolutionJudge: (oldContent, newContent) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u6F14\u5316\u5224\u65AD\u52A9\u624B\u3002\u5206\u6790\u4EE5\u4E0B\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u7CFB\u5E76\u8FD4\u56DE JSON\u3002
493
+
494
+ \u65E7\u8BB0\u5FC6\uFF1A${oldContent}
495
+ \u65B0\u8BB0\u5FC6\uFF1A${newContent}
496
+
497
+ \u5224\u65AD\u89C4\u5219\uFF1A
498
+
499
+ - EVOLVE\uFF1A\u65B0\u5185\u5BB9\u662F\u5BF9\u65E7\u8BB0\u5FC6\u7684\u6DF1\u5316/\u66F4\u65B0\uFF08\u5982\u300C\u6B63\u5728\u8003\u8651\u7528 Next.js\u300D\u2192\u300C\u51B3\u5B9A\u7528 Next.js 14 App Router\u300D\uFF09
500
+ \u8FD4\u56DE\uFF1A{"type": "EVOLVE", "mergedContent": "\u878D\u5408\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u4FDD\u7559\u6F14\u5316\u8F68\u8FF9"}
501
+
502
+ - CONFLICT\uFF1A\u65B0\u65E7\u4FE1\u606F\u5728\u540C\u4E00\u5C5E\u6027\u4E0A\u76F4\u63A5\u77DB\u76FE\uFF08\u5982\u300C\u4F7F\u7528 MySQL \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
503
+ \u8FD4\u56DE\uFF1A{"type": "CONFLICT"}
504
+
505
+ - EXPAND\uFF1A\u65B0\u4FE1\u606F\u662F\u5BF9\u65E7\u8BB0\u5FC6\u540C\u4E00\u4E3B\u9898\u7684\u8865\u5145\u6269\u5C55\uFF08\u5982\u300C\u8D1F\u8D23\u540E\u7AEF\u5F00\u53D1\u300D+\u300C\u540E\u7AEF\u4F7F\u7528 Go \u548C gRPC\u300D\uFF09
506
+ \u8FD4\u56DE\uFF1A{"type": "EXPAND", "mergedContent": "\u5408\u5E76\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u6574\u5408\u53CC\u65B9\u4FE1\u606F"}
507
+
508
+ - NEW\uFF1A\u5168\u65B0\u4FE1\u606F\uFF0C\u4E0E\u65E7\u8BB0\u5FC6\u65E0\u5B9E\u8D28\u5173\u8054\uFF08\u5982\u300C\u559C\u6B22 dark mode\u300Dvs\u300C\u4E0B\u5468\u8981\u53BB\u51FA\u5DEE\u300D\uFF09
509
+ \u8FD4\u56DE\uFF1A{"type": "NEW"}
510
+
511
+ \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`,
512
+ conflictScan: (numberedNotes) => `\u4F60\u5728\u5BA1\u8BA1\u4E00\u4E2A\u4EBA\u7684\u8BB0\u5FC6\u5E93\uFF0C\u627E\u51FA\u5176\u4E2D**\u4E92\u76F8\u77DB\u76FE**\u7684\u6761\u76EE\u3002
513
+
514
+ \u4E0B\u9762\u662F\u7F16\u53F7\u7684\u8BB0\u5FC6\u3002\u627E\u51FA\u90A3\u4E9B**\u4E0D\u53EF\u80FD\u540C\u65F6\u4E3A\u771F**\u7684\u914D\u5BF9\u3002
515
+
516
+ ${numberedNotes}
517
+
518
+ \u7B97\u77DB\u76FE\u7684\u60C5\u51B5\uFF1A
519
+ - \u540C\u4E00\u5C5E\u6027\u4E0A\u51FA\u73B0\u4E92\u65A5\u7684\u503C\uFF08\u300C\u4F4F\u5728\u5DF4\u9ECE\u300Dvs\u300C\u642C\u5230\u4E86\u67CF\u6797\u300D\uFF09
520
+ - \u540E\u6765\u7684\u8BB0\u5FC6\u8FDD\u53CD\u4E86\u5148\u524D\u9648\u8FF0\u7684\u504F\u597D\u6216\u7EA6\u675F\uFF08\u300C\u5403\u7D20\u300Dvs\u300C\u90A3\u5757\u725B\u6392\u5F88\u597D\u5403\u300D\uFF09
521
+ - \u540E\u6765\u7684\u4E8B\u5B9E\u53D6\u4EE3\u4E86\u5148\u524D\u7684\uFF08\u300C\u7528 MySQL\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
522
+
523
+ **\u4E0D\u7B97**\u77DB\u76FE \u2014\u2014 \u8BF7\u4E25\u683C\uFF0C\u4EE5\u4E0B\u662F\u6700\u5E38\u89C1\u7684\u8BEF\u5224\uFF1A
524
+ - \u7D2F\u52A0\u7684\u4E8B\u5B9E\u3002\u4E24\u8005\u53EF\u4EE5\u540C\u65F6\u6210\u7ACB\uFF08\u300C\u517B\u4E86\u4E00\u53EA\u72D7\u53EB Buddy\u300D+\u300C\u53C8\u9886\u517B\u4E86\u7B2C\u4E8C\u53EA\u53EB Scout\u300D**\u4E0D\u662F**\u77DB\u76FE\uFF09
525
+ - \u4E24\u6761\u8BB0\u5FC6\u672C\u8EAB\u5DF2\u7ECF\u4F53\u73B0\u4E86\u968F\u65F6\u95F4\u7684\u53D8\u5316
526
+ - \u53EA\u662F\u4E3B\u9898\u76F8\u4F3C\u6216\u76F8\u5173
527
+ - \u573A\u666F\u4E0D\u540C\uFF08\u5728\u516C\u53F8\u559D\u5496\u5561\uFF0C\u5728\u5BB6\u559D\u8336\uFF09
528
+
529
+ \u5BF9\u6BCF\u4E00\u5BF9\u77DB\u76FE\uFF0C\u8FD8\u8981\u6307\u51FA\u54EA\u4E00\u6761\u662F**\u5DF2\u5931\u6548\u7684**\uFF08\u4E0D\u518D\u4E3A\u771F\u7684\u90A3\u6761\uFF09\u3002\u8BF7\u4ECE**\u63AA\u8F9E**\u5224\u65AD\uFF0C\u4E0D\u8981\u5047\u8BBE\u987A\u5E8F\uFF1A
530
+ \u300C\u4EE5\u524D\u300D\u300C2019 \u5E74\u90A3\u4F1A\u513F\u300D\u300C\u4E0A\u4E2A\u6708\u642C\u4E86\u300D\u300C\u6539\u7528\u4E86\u300D\u8FD9\u7C7B\u8BF4\u6CD5\u80FD\u544A\u8BC9\u4F60\u54EA\u6761\u63CF\u8FF0\u7684\u662F\u8FC7\u53BB\u3002
531
+ \u8FD9\u4E9B\u8BB0\u5FC6**\u4E0D\u662F\u6309\u65F6\u95F4\u987A\u5E8F\u6392\u5217\u7684**\uFF0C\u7F16\u53F7\u4E5F\u4E0D\u4EE3\u8868\u65B0\u65E7\u3002
532
+
533
+ \u5982\u679C\u65E0\u6CD5\u5224\u65AD\u54EA\u6761\u5DF2\u5931\u6548\uFF0C\u5C31\u586B null\u3002\u8FD9\u662F\u4E00\u4E2A**\u6B63\u5E38\u4E14\u6709\u7528**\u7684\u56DE\u7B54 \u2014\u2014 \u5B81\u53EF\u586B null \u4E5F\u4E0D\u8981\u731C\uFF0C
534
+ \u56E0\u4E3A\u731C\u9519\u4F1A\u8BA9\u4E00\u6761**\u4ECD\u7136\u4E3A\u771F**\u7684\u8BB0\u5FC6\u88AB\u505C\u7528\u3002
535
+
536
+ \u53EA\u8FD4\u56DE JSON \u6570\u7EC4\u3002\u6CA1\u6709\u771F\u6B63\u77DB\u76FE\u5C31\u8FD4\u56DE\u7A7A\u6570\u7EC4\uFF1A
537
+ [{"a": 0, "b": 3, "superseded": 0, "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u662F\u54EA\u4E2A\u5C5E\u6027\u4E92\u65A5"}]
538
+
539
+ "superseded" \u53EA\u80FD\u662F "a" \u7684\u503C\u3001"b" \u7684\u503C\uFF0C\u6216 null\u3002
540
+ \u4F7F\u7528\u4E0A\u9762\u663E\u793A\u7684\u7F16\u53F7\u3002\u540C\u4E00\u5BF9\u53EA\u62A5\u4E00\u6B21\u3002**\u5B81\u53EF\u4E0D\u62A5\uFF0C\u4E5F\u4E0D\u8981\u731C\u3002**`
541
+ };
542
+ var templates = { en, zh };
543
+ var t = templates[LOCALE];
544
+
545
+ // src/llm.ts
546
+ var _override = {};
547
+ var _warned = /* @__PURE__ */ new Set();
548
+ function warnOnce(key, message) {
549
+ if (_warned.has(key)) return;
550
+ _warned.add(key);
551
+ console.error(message);
552
+ }
553
+ function resolveProvider(role = "fast") {
554
+ const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
555
+ const p = (raw || process.env.AMEM_LLM_PROVIDER || _override.provider || "anthropic").trim().toLowerCase();
556
+ if (p !== "anthropic" && p !== "openai") {
557
+ warnOnce(`provider:${p}`, `[amem] unknown LLM provider "${p}"; falling back to anthropic`);
558
+ }
559
+ return p;
560
+ }
561
+ function resolveModel(role = "fast") {
562
+ const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_MODEL || _override.strong?.model || void 0 : void 0;
563
+ return strong || process.env.AMEM_LLM_MODEL || _override.model || (resolveProvider(role) === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
564
+ }
565
+ function resolveBaseURL(role = "fast") {
566
+ const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_BASE_URL || _override.strong?.baseURL || void 0 : void 0;
567
+ return strong || process.env.AMEM_LLM_BASE_URL || _override.baseURL || void 0;
568
+ }
569
+ var DEFAULT_TIMEOUT_MS = 3e4;
570
+ function resolveTimeoutMs() {
571
+ const envVal = Number(process.env.AMEM_LLM_TIMEOUT);
572
+ if (Number.isFinite(envVal) && envVal > 0) return envVal;
573
+ if (_override.timeoutMs && _override.timeoutMs > 0) return _override.timeoutMs;
574
+ return DEFAULT_TIMEOUT_MS;
575
+ }
576
+ var _anthropicClients = /* @__PURE__ */ new Map();
577
+ function anthropic(baseURL) {
578
+ const key = baseURL ?? "";
579
+ let client = _anthropicClients.get(key);
580
+ if (!client) {
581
+ client = new import_sdk.default({
582
+ ...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
583
+ ...baseURL && { baseURL },
584
+ timeout: resolveTimeoutMs()
585
+ });
586
+ _anthropicClients.set(key, client);
587
+ }
588
+ return client;
589
+ }
590
+ var _openaiClients = /* @__PURE__ */ new Map();
591
+ function openai(baseURL) {
592
+ const key = baseURL ?? "";
593
+ let client = _openaiClients.get(key);
594
+ if (!client) {
595
+ client = new import_openai.default({
596
+ // AMEM_LLM_API_KEY first (engine convention), then the SDK's own
597
+ // OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
598
+ // env fallback, so read it here. Placeholder last, so keyless local servers
599
+ // (Ollama, vLLM) still work.
600
+ apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
601
+ ...baseURL && { baseURL },
602
+ timeout: resolveTimeoutMs()
603
+ });
604
+ _openaiClients.set(key, client);
605
+ }
606
+ return client;
607
+ }
608
+ async function llmCall(prompt, maxTokens = 500, role = "fast") {
609
+ const provider = resolveProvider(role);
610
+ const model = resolveModel(role);
611
+ const baseURL = resolveBaseURL(role);
612
+ const isThinking = model.includes("gemini") || model.includes("pro-agent");
613
+ const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
614
+ try {
615
+ return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
616
+ } catch (e) {
617
+ console.error(`[amem] LLM call failed: ${e.message}`);
618
+ return null;
619
+ }
620
+ }
621
+ async function anthropicCall(prompt, model, maxTokens, baseURL) {
622
+ const resp = await anthropic(baseURL).messages.create({
623
+ model,
624
+ max_tokens: maxTokens,
625
+ messages: [{ role: "user", content: prompt }]
626
+ });
627
+ for (const block of resp.content) {
628
+ if (block.type === "text") return block.text.trim();
629
+ }
630
+ return null;
631
+ }
632
+ async function openaiCall(prompt, model, maxTokens, baseURL) {
633
+ const isReasoning = /^o\d/.test(model) || model.startsWith("gpt-5");
634
+ const resp = await openai(baseURL).chat.completions.create({
635
+ model,
636
+ ...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
637
+ messages: [{ role: "user", content: prompt }]
638
+ });
639
+ return resp.choices[0]?.message?.content?.trim() ?? null;
640
+ }
641
+ function stripReasoning(raw) {
642
+ return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<\|(?:eot_id|im_start|im_end|begin_of_text|end_of_text|endoftext)\|>/g, "").trim();
643
+ }
644
+ function stripFences(raw) {
645
+ raw = stripReasoning(raw);
646
+ if (raw.startsWith("```")) {
647
+ const lines = raw.split("\n");
648
+ lines.shift();
649
+ if (lines[lines.length - 1] === "```") lines.pop();
650
+ raw = lines.join("\n").trim();
651
+ }
652
+ if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
653
+ try {
654
+ raw = JSON.parse(raw);
655
+ } catch {
656
+ }
657
+ }
658
+ return raw;
659
+ }
660
+ function parseJsonLoose(raw) {
661
+ const cleaned = stripFences(raw);
662
+ try {
663
+ return JSON.parse(cleaned);
664
+ } catch (e) {
665
+ const m = cleaned.match(/\{[\s\S]*\}/);
666
+ if (m) return JSON.parse(m[0]);
667
+ throw e;
668
+ }
669
+ }
670
+ var VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
671
+ var VALID_CATEGORIES = /* @__PURE__ */ new Set([
672
+ "Technical",
673
+ "Business",
674
+ "Personal",
675
+ "Project",
676
+ "Research",
677
+ "System",
678
+ "General"
679
+ ]);
680
+ async function llmConstructNote(content) {
681
+ const prompt = `Analyze the following text and respond with valid JSON only (no markdown fences, no explanation, no comments). All string values must use standard double quotes and be properly escaped:
682
+ {
683
+ "keywords": ["keyword1", "keyword2"],
684
+ "tags": ["tag1", "tag2"],
685
+ "context": "one sentence summary in the same language as the input",
686
+ "category": "Technical|Business|Personal|Project|Research|System|General",
687
+ "note_type": "memory|knowledge",
688
+ "topics": ["Topic1", "Topic2"],
689
+ "confidence": "high|medium|low"
690
+ }
691
+
692
+ Category guide:
693
+ - Technical: code, tools, configuration, APIs, debugging
694
+ - Business: company, finance, compliance, contracts, invoices
695
+ - Personal: personal state, habits, preferences, emotions
696
+ - Project: project progress, decisions, milestones
697
+ - Research: research, literature, evaluation, comparison
698
+ - System: system services, monitoring, operations
699
+ - General: anything that does not fit the above
700
+
701
+ note_type guide:
702
+ - knowledge: books, methodologies, tools, domain knowledge, reference material \u2014 durable, no strong time component
703
+ - memory: events, decisions, preferences, states, observations \u2014 episodic, time-sensitive
704
+
705
+ topics guide (Story 26B):
706
+ - Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].
707
+ - List 1-5 concise subject tags representing the main topics of this knowledge, e.g. ["TypeScript", "Qdrant", "Vector DB"].
708
+
709
+ confidence guide (Story 27):
710
+ - high: note_type is unambiguous \u2014 clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)
711
+ - medium: some ambiguity \u2014 e.g. "learned X method" could be either memory or knowledge
712
+ - low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
713
+
714
+ Text: ${content}`;
715
+ const raw = await llmCall(prompt, 400);
716
+ if (!raw)
717
+ return {
718
+ keywords: [],
719
+ tags: [],
720
+ context: "",
721
+ category: "General",
722
+ note_type: "memory",
723
+ topics: [],
724
+ confidence: "medium"
725
+ };
726
+ try {
727
+ const data = parseJsonLoose(raw);
728
+ const rawCategory = typeof data.category === "string" ? data.category : "General";
729
+ const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
730
+ const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
731
+ const topics = note_type === "knowledge" && Array.isArray(data.topics) ? data.topics.filter((v) => typeof v === "string") : [];
732
+ const rawConfidence = typeof data.confidence === "string" ? data.confidence : "medium";
733
+ const confidence = VALID_CONFIDENCE.has(rawConfidence) ? rawConfidence : "medium";
734
+ return {
735
+ keywords: Array.isArray(data.keywords) ? data.keywords : [],
736
+ tags: Array.isArray(data.tags) ? data.tags : [],
737
+ context: typeof data.context === "string" ? data.context : "",
738
+ category,
739
+ note_type,
740
+ topics,
741
+ confidence
742
+ };
743
+ } catch (e) {
744
+ console.error(`[amem] Note construction parse failed: ${e.message}`);
745
+ return {
746
+ keywords: [],
747
+ tags: [],
748
+ context: "",
749
+ category: "General",
750
+ note_type: "memory",
751
+ topics: [],
752
+ confidence: "medium"
753
+ };
754
+ }
755
+ }
756
+
757
+ // src/memory.ts
758
+ var import_uuid = require("uuid");
759
+
760
+ // src/config.ts
761
+ var os = __toESM(require("os"), 1);
762
+ var path = __toESM(require("path"), 1);
763
+ var _dataDir = process.env.AMEM_DATA_DIR || path.join(os.homedir(), ".amem");
764
+
765
+ // src/memory.ts
766
+ var import_jieba = require("@node-rs/jieba");
767
+ function buildEmbedText(note) {
768
+ let text = note.content;
769
+ if (note.keywords.length) text += " " + note.keywords.join(" ");
770
+ if (note.tags.length) text += " " + note.tags.join(" ");
771
+ if (note.context) text += " " + note.context;
772
+ return text;
773
+ }
774
+
775
+ // src/migrate.ts
776
+ function missingDerivedFields(n) {
777
+ return n.keywords.length === 0 || n.tags.length === 0;
778
+ }
779
+ async function migrateCollection(opts) {
780
+ const { from, to } = opts;
781
+ const refreshFields = opts.refreshFields !== false;
782
+ const dryRun = opts.dryRun !== false;
783
+ const log = opts.logger?.info ?? ((m) => console.log(m));
784
+ const warn = opts.logger?.warn ?? ((m) => console.warn(m));
785
+ if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
786
+ const model = getEmbeddingModel();
787
+ const targetDim = await getEmbeddingDim();
788
+ const sourceDim = await collectionDimRaw(from);
789
+ if (sourceDim === null) throw new Error(`migrate: source collection "${from}" does not exist`);
790
+ const points = await scrollAllRaw(from);
791
+ const notes = points.map(pointToNote);
792
+ const missingDerived = notes.filter(missingDerivedFields).length;
793
+ log(
794
+ `[migrate] ${from} (${sourceDim}d, ${notes.length} notes) \u2192 ${to} (${targetDim}d, ${model}); ${missingDerived} note(s) missing keywords/tags`
795
+ );
796
+ if (dryRun) {
797
+ log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
798
+ return { total: notes.length, missingDerived, refreshed: 0, migrated: 0, sourceDim, targetDim, model, dryRun: true };
799
+ }
800
+ const existingTargetDim = await collectionDimRaw(to);
801
+ if (existingTargetDim === null) {
802
+ await createCollectionRaw(to, targetDim);
803
+ log(`[migrate] created ${to} at ${targetDim}d`);
804
+ } else {
805
+ if (existingTargetDim !== targetDim) {
806
+ throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
807
+ }
808
+ const existingCount = await countPointsRaw(to);
809
+ if (existingCount > 0) {
810
+ throw new Error(`migrate: target "${to}" already holds ${existingCount} point(s); use an empty collection`);
811
+ }
812
+ }
813
+ let refreshed = 0;
814
+ let migrated = 0;
815
+ const BATCH = 64;
816
+ let buffer = [];
817
+ const flush = async () => {
818
+ if (!buffer.length) return;
819
+ await upsertPointsRaw(to, buffer);
820
+ migrated += buffer.length;
821
+ buffer = [];
822
+ };
823
+ for (const note of notes) {
824
+ if (refreshFields && missingDerivedFields(note)) {
825
+ try {
826
+ const built = await llmConstructNote(note.content);
827
+ if (note.keywords.length === 0) note.keywords = built.keywords;
828
+ if (note.tags.length === 0) note.tags = built.tags;
829
+ if (!note.context) note.context = built.context;
830
+ refreshed++;
831
+ } catch (e) {
832
+ warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
833
+ }
834
+ }
835
+ const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
836
+ buffer.push(point);
837
+ if (buffer.length >= BATCH) {
838
+ await flush();
839
+ log(`[migrate] ${migrated}/${notes.length}`);
840
+ }
841
+ }
842
+ await flush();
843
+ const finalCount = await countPointsRaw(to);
844
+ if (finalCount !== notes.length) {
845
+ warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
846
+ }
847
+ log(
848
+ `[migrate] done: ${migrated} migrated, ${refreshed} re-extracted. "${from}" is untouched \u2014 switch with AMEM_COLLECTION=${to}, and keep the old one until you are satisfied.`
849
+ );
850
+ return { total: notes.length, missingDerived, refreshed, migrated, sourceDim, targetDim, model, dryRun: false };
851
+ }
852
+
853
+ // src/cli-migrate.ts
854
+ var USAGE = `amem-migrate \u2014 rebuild a collection under the current embedding model
855
+
856
+ npx --package=@amemhq/core amem-migrate --to <collection> [options]
857
+
858
+ Options
859
+ --to <name> Target collection. Required. Must not already hold points.
860
+ --from <name> Source collection. Defaults to AMEM_COLLECTION.
861
+ --apply Actually write. Without this it is a dry run.
862
+ --no-refresh-fields Skip re-extracting keywords/tags for notes that never
863
+ had them. Faster, and makes no LLM calls at all.
864
+ -h, --help
865
+
866
+ The model is whatever AMEM_EMBED_MODEL says, so set that first:
867
+
868
+ AMEM_EMBED_MODEL=Xenova/bge-m3 \\
869
+ npx --package=@amemhq/core amem-migrate --to amem_notes_v2 --apply
870
+
871
+ The source is only ever read. If the result looks wrong, point AMEM_COLLECTION
872
+ back at it and nothing has been lost.`;
873
+ function parseArgs(argv) {
874
+ const value = (flag) => {
875
+ const i = argv.indexOf(flag);
876
+ return i === -1 ? void 0 : argv[i + 1];
877
+ };
878
+ return {
879
+ help: argv.includes("-h") || argv.includes("--help"),
880
+ to: value("--to"),
881
+ from: value("--from"),
882
+ dryRun: !argv.includes("--apply"),
883
+ refreshFields: !argv.includes("--no-refresh-fields")
884
+ };
885
+ }
886
+ async function main() {
887
+ const args = parseArgs(process.argv.slice(2));
888
+ if (args.help) {
889
+ console.log(USAGE);
890
+ return;
891
+ }
892
+ const to = args.to;
893
+ if (!to) {
894
+ console.error("amem-migrate: --to <collection> is required\n");
895
+ console.error(USAGE);
896
+ process.exitCode = 1;
897
+ return;
898
+ }
899
+ const from = args.from ?? getCollection();
900
+ const { dryRun, refreshFields } = args;
901
+ console.log(`model: ${getEmbeddingModel()}`);
902
+ console.log(`from: ${from}`);
903
+ console.log(`to: ${to}`);
904
+ console.log(`mode: ${dryRun ? "dry run \u2014 nothing will be written" : "APPLY"}
905
+ `);
906
+ const result = await migrateCollection({ from, to, dryRun, refreshFields });
907
+ console.log(`
908
+ ${result.total} notes in source (${result.sourceDim}-dim)`);
909
+ console.log(`target is ${result.targetDim}-dim`);
910
+ if (result.missingDerived > 0) {
911
+ console.log(
912
+ `${result.missingDerived} note(s) predate the extraction pipeline` + (refreshFields ? `, ${result.refreshed} re-extracted` : " \u2014 skipped (--no-refresh-fields)")
913
+ );
914
+ }
915
+ if (dryRun) {
916
+ console.log(`
917
+ Nothing was written. Re-run with --apply to migrate.`);
918
+ return;
919
+ }
920
+ console.log(`
921
+ ${result.migrated} notes written to "${to}".`);
922
+ console.log(`Point AMEM_COLLECTION at "${to}" to start using it. "${from}" is untouched.`);
923
+ }
924
+ main().catch((err) => {
925
+ console.error(`amem-migrate: ${err instanceof Error ? err.message : String(err)}`);
926
+ process.exitCode = 1;
927
+ });
928
+ // Annotate the CommonJS export names for ESM import in node:
929
+ 0 && (module.exports = {
930
+ parseArgs
931
+ });
932
+ //# sourceMappingURL=cli-migrate.cjs.map