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