@lanonasis/recall-forge 1.1.1

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.
Files changed (68) hide show
  1. package/.claw/skills/SKILL.md +347 -0
  2. package/CHANGELOG.md +162 -0
  3. package/LICENSE +21 -0
  4. package/README.md +302 -0
  5. package/SETUP.md +190 -0
  6. package/dist/cli-common.d.ts +25 -0
  7. package/dist/cli-common.js +338 -0
  8. package/dist/cli-memory.d.ts +6 -0
  9. package/dist/cli-memory.js +146 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.js +135 -0
  12. package/dist/client.d.ts +116 -0
  13. package/dist/client.js +643 -0
  14. package/dist/config.d.ts +41 -0
  15. package/dist/config.js +125 -0
  16. package/dist/enrichment/capture-filter.d.ts +4 -0
  17. package/dist/enrichment/capture-filter.js +44 -0
  18. package/dist/enrichment/prompt-safety.d.ts +13 -0
  19. package/dist/enrichment/prompt-safety.js +83 -0
  20. package/dist/enrichment/tag-extractor.d.ts +1 -0
  21. package/dist/enrichment/tag-extractor.js +47 -0
  22. package/dist/enrichment/type-detector.d.ts +2 -0
  23. package/dist/enrichment/type-detector.js +95 -0
  24. package/dist/extraction/cli-extract.d.ts +8 -0
  25. package/dist/extraction/cli-extract.js +66 -0
  26. package/dist/extraction/format-adapters.d.ts +8 -0
  27. package/dist/extraction/format-adapters.js +268 -0
  28. package/dist/extraction/index.d.ts +7 -0
  29. package/dist/extraction/index.js +7 -0
  30. package/dist/extraction/jsonl-extractor.d.ts +32 -0
  31. package/dist/extraction/jsonl-extractor.js +207 -0
  32. package/dist/extraction/markdown-extractor.d.ts +23 -0
  33. package/dist/extraction/markdown-extractor.js +228 -0
  34. package/dist/extraction/secret-redactor.d.ts +7 -0
  35. package/dist/extraction/secret-redactor.js +112 -0
  36. package/dist/extraction/sqlite-extractor.d.ts +15 -0
  37. package/dist/extraction/sqlite-extractor.js +245 -0
  38. package/dist/extraction/types.d.ts +50 -0
  39. package/dist/extraction/types.js +1 -0
  40. package/dist/hooks/capture.d.ts +23 -0
  41. package/dist/hooks/capture.js +162 -0
  42. package/dist/hooks/context-engine.d.ts +4 -0
  43. package/dist/hooks/context-engine.js +54 -0
  44. package/dist/hooks/local-fallback.d.ts +5 -0
  45. package/dist/hooks/local-fallback.js +31 -0
  46. package/dist/hooks/recall.d.ts +21 -0
  47. package/dist/hooks/recall.js +123 -0
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.js +103 -0
  50. package/dist/plugin-sdk-stub.d.ts +53 -0
  51. package/dist/plugin-sdk-stub.js +3 -0
  52. package/dist/privacy/privacy-guard.d.ts +33 -0
  53. package/dist/privacy/privacy-guard.js +130 -0
  54. package/dist/privacy/privacy-log.d.ts +6 -0
  55. package/dist/privacy/privacy-log.js +44 -0
  56. package/dist/tools/memory-forget.d.ts +3 -0
  57. package/dist/tools/memory-forget.js +109 -0
  58. package/dist/tools/memory-get.d.ts +3 -0
  59. package/dist/tools/memory-get.js +46 -0
  60. package/dist/tools/memory-search.d.ts +4 -0
  61. package/dist/tools/memory-search.js +95 -0
  62. package/dist/tools/memory-store.d.ts +5 -0
  63. package/dist/tools/memory-store.js +199 -0
  64. package/openclaw.plugin.json +315 -0
  65. package/package.json +90 -0
  66. package/setup/agents-memory.md +63 -0
  67. package/setup/heartbeat-memory.md +53 -0
  68. package/setup/install.sh +179 -0
package/dist/client.js ADDED
@@ -0,0 +1,643 @@
1
+ /**
2
+ * @module LanonasisClient
3
+ *
4
+ * Privacy-first API client for the LanOnasis memory service.
5
+ *
6
+ * SECURITY SCOPE — what this module reads and where it sends data:
7
+ *
8
+ * Credential resolution (read-only, in-process only):
9
+ * 1. `cfg.apiKey` — caller-supplied key (constructor argument)
10
+ * 2. `LANONASIS_API_KEY` — environment variable (LanOnasis-namespaced)
11
+ * 3. `LANONASIS_VENDOR_KEY` — environment variable (LanOnasis-namespaced)
12
+ * 4. `~/.lanonasis/api-key.enc` — AES-256-GCM encrypted local file (machine-bound key)
13
+ * 5. `~/.lanonasis/mcp-tokens.enc` — AES-256-GCM encrypted OAuth token store
14
+ * 6. `~/.maas/config.json` — CLI session token (written by `lanonasis auth login`)
15
+ * 7. `keytar` (optional) — OS native credential store, if installed
16
+ *
17
+ * No other environment variables are read. No credentials are logged,
18
+ * forwarded, or stored — they are consumed in-process to produce a single
19
+ * `X-API-Key` or `Authorization: Bearer` header.
20
+ *
21
+ * Network destination:
22
+ * ALL HTTP requests go exclusively to `cfg.baseUrl` (default: api.lanonasis.com).
23
+ * No third-party endpoints, no telemetry calls, no exfiltration paths.
24
+ * IPv4 is forced via undici dispatcher to guarantee predictable routing.
25
+ *
26
+ * Secret protection:
27
+ * Content sent to the memory API is pre-processed by the secret-redactor
28
+ * pipeline (extraction/secret-redactor.ts) which strips 30+ credential
29
+ * patterns BEFORE the payload leaves the process. This is the core
30
+ * privacy guarantee — credentials never reach memory storage.
31
+ */
32
+ // Phase 2 - LanOnasis API Client
33
+ import { Agent, fetch as undiciFetch } from "undici";
34
+ import { redactSecrets } from "./extraction/secret-redactor.js";
35
+ // Force IPv4 — Node v24 built-in fetch ignores setGlobalDispatcher; use undici fetch with explicit dispatcher
36
+ const ipv4Agent = new Agent({ connect: { family: 4 } });
37
+ const JWT_PATTERN = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
38
+ const API_KEY_ENV_VARS = ["LANONASIS_API_KEY", "LANONASIS_VENDOR_KEY"];
39
+ const TOKEN_ENV_VARS = [
40
+ "LANONASIS_AUTH_TOKEN",
41
+ "LANONASIS_BEARER_TOKEN",
42
+ "MCP_BEARER_TOKEN",
43
+ ];
44
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
45
+ function sanitizeMemoryText(text) {
46
+ if (typeof text !== "string")
47
+ return text;
48
+ return redactSecrets(text).text;
49
+ }
50
+ class LanonasisHttpError extends Error {
51
+ status;
52
+ body;
53
+ constructor(status, message, body) {
54
+ super(message);
55
+ this.name = "LanonasisHttpError";
56
+ this.status = status;
57
+ this.body = body;
58
+ }
59
+ }
60
+ async function tryReadKeytarPassword(service, account) {
61
+ try {
62
+ // keytar is an optional native credential store dependency.
63
+ // Direct import() is used — if the module is absent, the catch silently skips it.
64
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
65
+ // @ts-ignore — keytar is not in devDependencies; absence is expected
66
+ const keytarModule = await import("keytar");
67
+ const keytar = (keytarModule.default ?? keytarModule);
68
+ if (typeof keytar.getPassword === "function") {
69
+ return (await keytar.getPassword(service, account)) ?? null;
70
+ }
71
+ }
72
+ catch {
73
+ // Keytar is optional; fall back to the encrypted file storage below.
74
+ }
75
+ return null;
76
+ }
77
+ function parseStoredString(jsonOrString) {
78
+ const trimmed = jsonOrString.trim();
79
+ if (!trimmed)
80
+ return null;
81
+ try {
82
+ const parsed = JSON.parse(trimmed);
83
+ if (typeof parsed.apiKey === "string" && parsed.apiKey.trim()) {
84
+ return parsed.apiKey.trim();
85
+ }
86
+ if (typeof parsed.access_token === "string" && parsed.access_token.trim()) {
87
+ return parsed.access_token.trim();
88
+ }
89
+ }
90
+ catch {
91
+ // Plain string payloads are valid in some older fallback formats.
92
+ }
93
+ return trimmed;
94
+ }
95
+ async function decryptStoredFile(fileName, salt) {
96
+ try {
97
+ const [{ readFile }, os, path, crypto] = await Promise.all([
98
+ import("node:fs/promises"),
99
+ import("node:os"),
100
+ import("node:path"),
101
+ import("node:crypto"),
102
+ ]);
103
+ const filePath = path.join(os.homedir(), ".lanonasis", fileName);
104
+ const encryptedPayload = await readFile(filePath, "utf8");
105
+ const parts = encryptedPayload.split(":");
106
+ const machineId = os.hostname() + os.userInfo().username;
107
+ const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, "sha256");
108
+ if (parts.length === 3) {
109
+ const [ivHex, authTagHex, encrypted] = parts;
110
+ if (!ivHex || !authTagHex || !encrypted)
111
+ return null;
112
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
113
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
114
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
115
+ decrypted += decipher.final("utf8");
116
+ return decrypted;
117
+ }
118
+ if (parts.length === 2) {
119
+ const [ivHex, encrypted] = parts;
120
+ if (!ivHex || !encrypted)
121
+ return null;
122
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, Buffer.from(ivHex, "hex"));
123
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
124
+ decrypted += decipher.final("utf8");
125
+ return decrypted;
126
+ }
127
+ }
128
+ catch {
129
+ // Missing file or unreadable file means no stored local credential is available.
130
+ }
131
+ return null;
132
+ }
133
+ async function readCliConfigToken() {
134
+ try {
135
+ const [{ readFile }, os, path] = await Promise.all([
136
+ import("node:fs/promises"),
137
+ import("node:os"),
138
+ import("node:path"),
139
+ ]);
140
+ const configPath = path.join(os.homedir(), ".maas", "config.json");
141
+ const raw = await readFile(configPath, "utf8");
142
+ const parsed = JSON.parse(raw);
143
+ return typeof parsed.token === "string" && parsed.token.trim()
144
+ ? parsed.token.trim()
145
+ : null;
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ export class LanonasisClient {
152
+ baseUrl;
153
+ apiKey;
154
+ projectId;
155
+ cache;
156
+ rateLimit;
157
+ CACHE_TTL_MS;
158
+ CACHE_MAX_SIZE;
159
+ RATE_LIMIT_WINDOW_MS;
160
+ RATE_LIMIT_MAX_REQ;
161
+ constructor(cfg) {
162
+ if (!cfg.projectId) {
163
+ throw new Error("LanonasisClient: projectId is required");
164
+ }
165
+ this.baseUrl = cfg.baseUrl.replace(/\/$/, ""); // trailing slash
166
+ this.apiKey = cfg.apiKey || "";
167
+ this.projectId = cfg.projectId;
168
+ this.cache = new Map();
169
+ this.rateLimit = { timestamps: [] };
170
+ this.CACHE_TTL_MS = cfg.cacheTtlMs;
171
+ this.CACHE_MAX_SIZE = cfg.cacheMaxSize;
172
+ this.RATE_LIMIT_WINDOW_MS = cfg.rateLimitWindowMs;
173
+ this.RATE_LIMIT_MAX_REQ = cfg.rateLimitMaxReq;
174
+ }
175
+ // LRU Cache helpers
176
+ cacheKey(method, params) {
177
+ return `${this.projectId}:${method}:${JSON.stringify(params)}`;
178
+ }
179
+ getFromCache(key) {
180
+ const entry = this.cache.get(key);
181
+ if (!entry)
182
+ return null;
183
+ if (Date.now() > entry.expiry) {
184
+ this.cache.delete(key);
185
+ return null;
186
+ }
187
+ return entry.result;
188
+ }
189
+ setCache(key, result) {
190
+ // Evict oldest if at capacity
191
+ if (this.cache.size >= this.CACHE_MAX_SIZE) {
192
+ const firstKey = this.cache.keys().next().value;
193
+ if (firstKey)
194
+ this.cache.delete(firstKey);
195
+ }
196
+ this.cache.set(key, {
197
+ result,
198
+ expiry: Date.now() + this.CACHE_TTL_MS,
199
+ });
200
+ }
201
+ // Rate limiting - wait silently if needed
202
+ async enforceRateLimit() {
203
+ const now = Date.now();
204
+ // Remove timestamps outside the window
205
+ this.rateLimit.timestamps = this.rateLimit.timestamps.filter((ts) => now - ts < this.RATE_LIMIT_WINDOW_MS);
206
+ if (this.rateLimit.timestamps.length >= this.RATE_LIMIT_MAX_REQ) {
207
+ const oldest = this.rateLimit.timestamps[0];
208
+ const waitMs = this.RATE_LIMIT_WINDOW_MS - (now - oldest) + 100;
209
+ if (waitMs > 0) {
210
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
211
+ }
212
+ }
213
+ this.rateLimit.timestamps.push(Date.now());
214
+ }
215
+ // HTTP request wrapper
216
+ async request(method, path, body) {
217
+ await this.enforceRateLimit();
218
+ const url = this.buildRequestUrl(path);
219
+ const authHeader = await this.resolveAuthHeader();
220
+ const options = {
221
+ method,
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ "X-Project-Scope": this.projectId,
225
+ [authHeader.name]: authHeader.value,
226
+ },
227
+ };
228
+ if (body) {
229
+ options.body = JSON.stringify(body);
230
+ }
231
+ try {
232
+ const response = await undiciFetch(url, { ...options, dispatcher: ipv4Agent });
233
+ // Handle 429 - retry once
234
+ if (response.status === 429) {
235
+ const retryAfter = response.headers.get("Retry-After");
236
+ const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 2000;
237
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
238
+ const retryResponse = await undiciFetch(url, { ...options, dispatcher: ipv4Agent });
239
+ return this.handleResponse(retryResponse);
240
+ }
241
+ return this.handleResponse(response);
242
+ }
243
+ catch (err) {
244
+ const message = err instanceof Error ? err.message : "Unknown error";
245
+ const cause = err?.cause;
246
+ const causeMsg = cause?.message
247
+ ? ` (cause: ${cause.message}${cause.code ? ` ${cause.code}` : ""})`
248
+ : "";
249
+ throw new Error(`LanOnasis unreachable: ${message}${causeMsg} [url=${url}]`);
250
+ }
251
+ }
252
+ async handleResponse(response) {
253
+ const status = response.status;
254
+ const body = await response.text();
255
+ if (status === 401) {
256
+ throw new LanonasisHttpError(status, "LanOnasis authentication failed — refresh with `lanonasis auth login` or set LANONASIS_API_KEY", body);
257
+ }
258
+ if (status >= 500) {
259
+ throw new LanonasisHttpError(status, `LanOnasis server error (${status}): ${body}`, body);
260
+ }
261
+ if (!response.ok) {
262
+ throw new LanonasisHttpError(status, `LanOnasis error (${status}): ${body}`, body);
263
+ }
264
+ // Issue 4 fix: empty body (e.g. 204 No Content) — return undefined safely
265
+ const trimmed = body.trim();
266
+ if (!trimmed)
267
+ return undefined;
268
+ return JSON.parse(trimmed);
269
+ }
270
+ buildRequestUrl(path) {
271
+ return `${this.baseUrl}${path}`;
272
+ }
273
+ shouldUseCompatibilityFallback(error) {
274
+ if (error instanceof LanonasisHttpError &&
275
+ (error.status === 404 || error.status === 405)) {
276
+ return true;
277
+ }
278
+ const message = error instanceof Error ? error.message : String(error ?? "");
279
+ return /\b404\b/.test(message) || /\b405\b/.test(message);
280
+ }
281
+ shouldUseLegacyGetFallback(error) {
282
+ if (this.shouldUseCompatibilityFallback(error)) {
283
+ return true;
284
+ }
285
+ if (error instanceof LanonasisHttpError && error.status === 400) {
286
+ return /Memory ID is required/i.test(error.body);
287
+ }
288
+ const message = error instanceof Error ? error.message : String(error ?? "");
289
+ return /\b400\b/.test(message) && /Memory ID is required/i.test(message);
290
+ }
291
+ normalizeMemory(payload) {
292
+ const unwrapped = this.unwrap(payload);
293
+ if (!unwrapped || typeof unwrapped !== "object") {
294
+ return unwrapped;
295
+ }
296
+ const memory = { ...unwrapped };
297
+ const normalizedType = typeof memory.memory_type === "string"
298
+ ? memory.memory_type
299
+ : typeof memory.type === "string"
300
+ ? memory.type
301
+ : undefined;
302
+ if (normalizedType) {
303
+ memory.type = normalizedType;
304
+ memory.memory_type = normalizedType;
305
+ }
306
+ return memory;
307
+ }
308
+ normalizeMemoryListResult(raw) {
309
+ const envelope = raw;
310
+ const payload = this.unwrap(raw);
311
+ const items = Array.isArray(payload)
312
+ ? payload
313
+ : Array.isArray(envelope?.data)
314
+ ? envelope.data
315
+ : [];
316
+ const memories = items.map((item) => this.normalizeMemory(item));
317
+ const total = envelope?.pagination?.total ?? memories.length;
318
+ return { memories, total };
319
+ }
320
+ getEnvCredential(names) {
321
+ for (const envName of names) {
322
+ const value = process.env[envName];
323
+ if (typeof value === "string" && value.trim()) {
324
+ return value.trim();
325
+ }
326
+ }
327
+ return undefined;
328
+ }
329
+ toAuthHeader(credential, kind = "auto") {
330
+ const trimmed = credential.trim();
331
+ if (!trimmed) {
332
+ throw new Error("LanOnasis authentication required — run `lanonasis auth login` or set LANONASIS_API_KEY");
333
+ }
334
+ if (kind === "token") {
335
+ return {
336
+ name: "Authorization",
337
+ value: trimmed.startsWith("Bearer ") ? trimmed : `Bearer ${trimmed}`,
338
+ };
339
+ }
340
+ if (kind === "api_key") {
341
+ return {
342
+ name: "X-API-Key",
343
+ value: trimmed,
344
+ };
345
+ }
346
+ if (trimmed.startsWith("Bearer ") || JWT_PATTERN.test(trimmed)) {
347
+ return {
348
+ name: "Authorization",
349
+ value: trimmed.startsWith("Bearer ") ? trimmed : `Bearer ${trimmed}`,
350
+ };
351
+ }
352
+ return {
353
+ name: "X-API-Key",
354
+ value: trimmed,
355
+ };
356
+ }
357
+ async getStoredAuthHeader() {
358
+ const storedApiKey = parseStoredString((await tryReadKeytarPassword("lanonasis-mcp", "lanonasis_api_key")) ??
359
+ (await decryptStoredFile("api-key.enc", "lanonasis-mcp-api-key-2024")) ??
360
+ "");
361
+ if (storedApiKey) {
362
+ return this.toAuthHeader(storedApiKey, "api_key");
363
+ }
364
+ const storedAccessToken = parseStoredString((await tryReadKeytarPassword("lanonasis-mcp", "tokens")) ??
365
+ (await decryptStoredFile("mcp-tokens.enc", "lanonasis-mcp-oauth-2024")) ??
366
+ (await readCliConfigToken()) ??
367
+ "");
368
+ if (storedAccessToken) {
369
+ return this.toAuthHeader(storedAccessToken, "token");
370
+ }
371
+ return null;
372
+ }
373
+ async resolveAuthHeader() {
374
+ const configuredApiKey = typeof this.apiKey === "string" && this.apiKey.trim()
375
+ ? this.apiKey.trim()
376
+ : "";
377
+ if (configuredApiKey) {
378
+ return this.toAuthHeader(configuredApiKey, "auto");
379
+ }
380
+ const envApiKey = this.getEnvCredential(API_KEY_ENV_VARS);
381
+ if (envApiKey) {
382
+ return this.toAuthHeader(envApiKey, "api_key");
383
+ }
384
+ const storedAuthHeader = await this.getStoredAuthHeader();
385
+ if (storedAuthHeader) {
386
+ return storedAuthHeader;
387
+ }
388
+ const envToken = this.getEnvCredential(TOKEN_ENV_VARS);
389
+ if (envToken) {
390
+ return this.toAuthHeader(envToken, "token");
391
+ }
392
+ throw new Error("LanOnasis authentication required — run `lanonasis auth login` or set LANONASIS_API_KEY");
393
+ }
394
+ // Issue 1 fix: unwrap { success, data } envelope — defensive, handles both
395
+ // wrapped ({ data: T }) and direct (T) responses
396
+ unwrap(response) {
397
+ if (response !== null &&
398
+ typeof response === "object" &&
399
+ "data" in response) {
400
+ return response.data;
401
+ }
402
+ return response;
403
+ }
404
+ // Issue 3 fix: clear all search/list cache entries on any write
405
+ invalidateSearchCache() {
406
+ for (const key of this.cache.keys()) {
407
+ if (key.includes(":search:") || key.includes(":list:")) {
408
+ this.cache.delete(key);
409
+ }
410
+ }
411
+ }
412
+ isUuid(value) {
413
+ return UUID_PATTERN.test(value);
414
+ }
415
+ async resolveMemoryId(idOrPrefix) {
416
+ const candidate = idOrPrefix.trim();
417
+ if (!candidate) {
418
+ throw new Error("Memory ID is required.");
419
+ }
420
+ if (this.isUuid(candidate)) {
421
+ return candidate;
422
+ }
423
+ if (candidate.length < 8) {
424
+ throw new Error("Memory ID prefix must be at least 8 characters or a full UUID.");
425
+ }
426
+ const matches = [];
427
+ const limit = 100;
428
+ let page = 1;
429
+ while (true) {
430
+ const result = await this.listMemories({ limit, page });
431
+ if (!result.memories || result.memories.length === 0) {
432
+ break;
433
+ }
434
+ for (const memory of result.memories) {
435
+ if (memory.id.startsWith(candidate)) {
436
+ matches.push(memory.id);
437
+ }
438
+ }
439
+ if (result.total <= page * limit) {
440
+ break;
441
+ }
442
+ page += 1;
443
+ }
444
+ if (matches.length === 0) {
445
+ throw new Error(`Memory not found for ID/prefix: ${candidate}`);
446
+ }
447
+ if (matches.length > 1) {
448
+ throw new Error(`Memory ID prefix is ambiguous: ${candidate}. Matches: ${matches.slice(0, 5).join(", ")}`);
449
+ }
450
+ return matches[0];
451
+ }
452
+ async searchMemories(params) {
453
+ const cacheKey = this.cacheKey("search", params);
454
+ const cached = this.getFromCache(cacheKey);
455
+ if (cached)
456
+ return cached;
457
+ const requestBody = {
458
+ query: params.query,
459
+ threshold: params.threshold,
460
+ limit: params.limit,
461
+ };
462
+ if (params.type)
463
+ requestBody.type = params.type;
464
+ if (params.tags)
465
+ requestBody.tags = params.tags;
466
+ if (params.topic_key)
467
+ requestBody.topic_key = params.topic_key;
468
+ if (params.include_deleted !== undefined) {
469
+ requestBody.include_deleted = params.include_deleted;
470
+ }
471
+ if (params.response_mode)
472
+ requestBody.response_mode = params.response_mode;
473
+ if (params.metadata)
474
+ requestBody.metadata = params.metadata;
475
+ let raw;
476
+ try {
477
+ raw = await this.request("POST", "/api/v1/memories/search", requestBody);
478
+ }
479
+ catch (error) {
480
+ if (!this.shouldUseCompatibilityFallback(error))
481
+ throw error;
482
+ raw = await this.request("POST", "/api/v1/memory/search", requestBody);
483
+ }
484
+ const unwrapped = this.unwrap(raw);
485
+ const result = Array.isArray(unwrapped)
486
+ ? unwrapped.map((item) => this.normalizeMemory(item))
487
+ : [];
488
+ this.setCache(cacheKey, result);
489
+ return result;
490
+ }
491
+ async createMemory(params) {
492
+ const requestBody = {
493
+ title: params.title,
494
+ content: params.content,
495
+ type: params.type,
496
+ };
497
+ if (params.tags)
498
+ requestBody.tags = params.tags;
499
+ if (params.topic_key)
500
+ requestBody.topic_key = params.topic_key;
501
+ if (params.metadata)
502
+ requestBody.metadata = params.metadata;
503
+ if (params.idempotency_key)
504
+ requestBody.idempotency_key = params.idempotency_key;
505
+ if (params.continuity_key)
506
+ requestBody.continuity_key = params.continuity_key;
507
+ if (params.write_intent)
508
+ requestBody.write_intent = params.write_intent;
509
+ let raw;
510
+ try {
511
+ raw = await this.request("POST", "/api/v1/memories", requestBody);
512
+ }
513
+ catch (error) {
514
+ if (!this.shouldUseCompatibilityFallback(error))
515
+ throw error;
516
+ raw = await this.request("POST", "/api/v1/memory", requestBody);
517
+ }
518
+ // Issue 3 fix: invalidate stale search results
519
+ this.invalidateSearchCache();
520
+ return this.normalizeMemory(raw);
521
+ }
522
+ async getMemory(id) {
523
+ const resolvedId = await this.resolveMemoryId(id);
524
+ let raw;
525
+ try {
526
+ raw = await this.request("GET", `/api/v1/memories/${encodeURIComponent(resolvedId)}`);
527
+ }
528
+ catch (error) {
529
+ if (!this.shouldUseLegacyGetFallback(error))
530
+ throw error;
531
+ raw = await this.request("GET", `/api/v1/memory/get?id=${encodeURIComponent(resolvedId)}`);
532
+ }
533
+ return this.normalizeMemory(raw);
534
+ }
535
+ async listMemories(params) {
536
+ const cacheKey = this.cacheKey("list", params ?? {});
537
+ const cached = this.getFromCache(cacheKey);
538
+ if (cached)
539
+ return cached;
540
+ const queryParams = new URLSearchParams();
541
+ if (params?.limit)
542
+ queryParams.set("limit", params.limit.toString());
543
+ if (params?.page)
544
+ queryParams.set("page", params.page.toString());
545
+ if (params?.type)
546
+ queryParams.set("type", params.type);
547
+ if (params?.tags) {
548
+ params.tags.forEach((tag) => queryParams.append("tags", tag));
549
+ }
550
+ if (params?.topic_key)
551
+ queryParams.set("topic_key", params.topic_key);
552
+ if (params?.include_deleted !== undefined) {
553
+ queryParams.set("include_deleted", String(params.include_deleted));
554
+ }
555
+ if (params?.sort)
556
+ queryParams.set("sort", params.sort);
557
+ if (params?.order)
558
+ queryParams.set("order", params.order);
559
+ const query = queryParams.toString();
560
+ const canonicalPath = `/api/v1/memories${query ? `?${query}` : ""}`;
561
+ let raw;
562
+ try {
563
+ raw = await this.request("GET", canonicalPath);
564
+ }
565
+ catch (error) {
566
+ if (!this.shouldUseCompatibilityFallback(error))
567
+ throw error;
568
+ const listPayload = {};
569
+ if (params?.limit)
570
+ listPayload.limit = params.limit;
571
+ if (params?.page) {
572
+ listPayload.page = params.page;
573
+ listPayload.offset = Math.max(0, (params.page - 1) * (params.limit ?? 20));
574
+ }
575
+ if (params?.type)
576
+ listPayload.memory_type = params.type;
577
+ if (params?.tags)
578
+ listPayload.tags = params.tags;
579
+ if (params?.topic_key)
580
+ listPayload.topic_key = params.topic_key;
581
+ if (params?.include_deleted !== undefined) {
582
+ listPayload.include_deleted = params.include_deleted;
583
+ }
584
+ if (params?.sort)
585
+ listPayload.sort_by = params.sort;
586
+ if (params?.order)
587
+ listPayload.sort_order = params.order;
588
+ raw = await this.request("POST", "/api/v1/memories/list", listPayload);
589
+ }
590
+ const result = this.normalizeMemoryListResult(raw);
591
+ this.setCache(cacheKey, result);
592
+ return result;
593
+ }
594
+ async updateMemory(id, updates) {
595
+ const resolvedId = await this.resolveMemoryId(id);
596
+ const sanitizedUpdates = Object.fromEntries(Object.entries({
597
+ ...updates,
598
+ title: sanitizeMemoryText(updates.title),
599
+ content: sanitizeMemoryText(updates.content),
600
+ }).filter(([, value]) => value !== undefined));
601
+ let raw;
602
+ try {
603
+ raw = await this.request("PUT", `/api/v1/memories/${encodeURIComponent(resolvedId)}`, sanitizedUpdates);
604
+ }
605
+ catch (error) {
606
+ if (!this.shouldUseCompatibilityFallback(error))
607
+ throw error;
608
+ raw = await this.request("POST", `/api/v1/memory/update`, { id: resolvedId, ...sanitizedUpdates });
609
+ }
610
+ // Issue 3 fix: invalidate stale search/list results
611
+ this.invalidateSearchCache();
612
+ return this.normalizeMemory(raw);
613
+ }
614
+ async deleteMemory(id) {
615
+ const resolvedId = await this.resolveMemoryId(id);
616
+ try {
617
+ await this.request("DELETE", `/api/v1/memories/${encodeURIComponent(resolvedId)}`);
618
+ }
619
+ catch (error) {
620
+ if (!this.shouldUseCompatibilityFallback(error))
621
+ throw error;
622
+ await this.request("DELETE", `/api/v1/memory/delete?id=${encodeURIComponent(resolvedId)}`);
623
+ }
624
+ // Issue 3 fix: invalidate stale search/list results
625
+ this.invalidateSearchCache();
626
+ }
627
+ async getHealth() {
628
+ // Health endpoint returns direct object, no data envelope
629
+ return this.request("GET", "/api/v1/health");
630
+ }
631
+ async getStats() {
632
+ let raw;
633
+ try {
634
+ raw = await this.request("GET", "/api/v1/memories/stats");
635
+ }
636
+ catch (error) {
637
+ if (!this.shouldUseCompatibilityFallback(error))
638
+ throw error;
639
+ raw = await this.request("GET", "/api/v1/memory/stats");
640
+ }
641
+ return this.unwrap(raw);
642
+ }
643
+ }
@@ -0,0 +1,41 @@
1
+ export type CaptureMode = "auto" | "explicit" | "hybrid";
2
+ export type MemoryMode = "remote" | "local" | "hybrid";
3
+ export type SyncMode = "realtime" | "batch" | "manual";
4
+ export type RecallMode = "auto" | "ondemand";
5
+ export type PrivacyMode = "off" | "detect" | "mask";
6
+ export type LanonasisConfig = {
7
+ apiKey: string;
8
+ baseUrl: string;
9
+ projectId: string;
10
+ agentId: string;
11
+ autoRecall: boolean;
12
+ recallMode: RecallMode;
13
+ maxRecallChars: number;
14
+ captureMode: CaptureMode;
15
+ localFallback: boolean;
16
+ searchThreshold: number;
17
+ dedupeThreshold: number;
18
+ maxRecallResults: number;
19
+ memoryMode: MemoryMode;
20
+ sharedNamespace: string;
21
+ syncMode: SyncMode;
22
+ queueOnFailure: boolean;
23
+ autoIndexOnFirstUse: boolean;
24
+ extractSourceFormats: string[];
25
+ embeddingProvider: string;
26
+ embeddingModel: string;
27
+ queryEmbeddingModel: string;
28
+ embeddingDimensions: number;
29
+ embeddingProfileId: string;
30
+ privacyMode: PrivacyMode;
31
+ privacyLocale: string;
32
+ privacyNotifyUrl: string;
33
+ defaultChannel: string;
34
+ cacheTtlMs: number;
35
+ cacheMaxSize: number;
36
+ rateLimitMaxReq: number;
37
+ rateLimitWindowMs: number;
38
+ };
39
+ export declare const lanonasisConfigSchema: {
40
+ parse: (value: unknown) => LanonasisConfig;
41
+ };