@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.
- package/.claw/skills/SKILL.md +347 -0
- package/CHANGELOG.md +162 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/SETUP.md +190 -0
- package/dist/cli-common.d.ts +25 -0
- package/dist/cli-common.js +338 -0
- package/dist/cli-memory.d.ts +6 -0
- package/dist/cli-memory.js +146 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +135 -0
- package/dist/client.d.ts +116 -0
- package/dist/client.js +643 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +125 -0
- package/dist/enrichment/capture-filter.d.ts +4 -0
- package/dist/enrichment/capture-filter.js +44 -0
- package/dist/enrichment/prompt-safety.d.ts +13 -0
- package/dist/enrichment/prompt-safety.js +83 -0
- package/dist/enrichment/tag-extractor.d.ts +1 -0
- package/dist/enrichment/tag-extractor.js +47 -0
- package/dist/enrichment/type-detector.d.ts +2 -0
- package/dist/enrichment/type-detector.js +95 -0
- package/dist/extraction/cli-extract.d.ts +8 -0
- package/dist/extraction/cli-extract.js +66 -0
- package/dist/extraction/format-adapters.d.ts +8 -0
- package/dist/extraction/format-adapters.js +268 -0
- package/dist/extraction/index.d.ts +7 -0
- package/dist/extraction/index.js +7 -0
- package/dist/extraction/jsonl-extractor.d.ts +32 -0
- package/dist/extraction/jsonl-extractor.js +207 -0
- package/dist/extraction/markdown-extractor.d.ts +23 -0
- package/dist/extraction/markdown-extractor.js +228 -0
- package/dist/extraction/secret-redactor.d.ts +7 -0
- package/dist/extraction/secret-redactor.js +112 -0
- package/dist/extraction/sqlite-extractor.d.ts +15 -0
- package/dist/extraction/sqlite-extractor.js +245 -0
- package/dist/extraction/types.d.ts +50 -0
- package/dist/extraction/types.js +1 -0
- package/dist/hooks/capture.d.ts +23 -0
- package/dist/hooks/capture.js +162 -0
- package/dist/hooks/context-engine.d.ts +4 -0
- package/dist/hooks/context-engine.js +54 -0
- package/dist/hooks/local-fallback.d.ts +5 -0
- package/dist/hooks/local-fallback.js +31 -0
- package/dist/hooks/recall.d.ts +21 -0
- package/dist/hooks/recall.js +123 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +103 -0
- package/dist/plugin-sdk-stub.d.ts +53 -0
- package/dist/plugin-sdk-stub.js +3 -0
- package/dist/privacy/privacy-guard.d.ts +33 -0
- package/dist/privacy/privacy-guard.js +130 -0
- package/dist/privacy/privacy-log.d.ts +6 -0
- package/dist/privacy/privacy-log.js +44 -0
- package/dist/tools/memory-forget.d.ts +3 -0
- package/dist/tools/memory-forget.js +109 -0
- package/dist/tools/memory-get.d.ts +3 -0
- package/dist/tools/memory-get.js +46 -0
- package/dist/tools/memory-search.d.ts +4 -0
- package/dist/tools/memory-search.js +95 -0
- package/dist/tools/memory-store.d.ts +5 -0
- package/dist/tools/memory-store.js +199 -0
- package/openclaw.plugin.json +315 -0
- package/package.json +90 -0
- package/setup/agents-memory.md +63 -0
- package/setup/heartbeat-memory.md +53 -0
- 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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
};
|