@openanonymity/nanomem 0.1.0 → 0.1.2

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -18
  3. package/package.json +7 -3
  4. package/src/backends/BaseStorage.js +147 -3
  5. package/src/backends/indexeddb.js +21 -8
  6. package/src/browser.js +227 -0
  7. package/src/bullets/parser.js +8 -9
  8. package/src/cli/auth.js +1 -1
  9. package/src/cli/commands.js +58 -9
  10. package/src/cli/config.js +1 -1
  11. package/src/cli/help.js +5 -2
  12. package/src/cli/output.js +4 -0
  13. package/src/cli.js +6 -3
  14. package/src/engine/compactor.js +3 -6
  15. package/src/engine/deleter.js +187 -0
  16. package/src/engine/executors.js +474 -11
  17. package/src/engine/ingester.js +98 -63
  18. package/src/engine/recentConversation.js +110 -0
  19. package/src/engine/retriever.js +243 -37
  20. package/src/engine/toolLoop.js +51 -9
  21. package/src/imports/chatgpt.js +1 -1
  22. package/src/imports/claude.js +85 -0
  23. package/src/imports/importData.js +462 -0
  24. package/src/imports/index.js +10 -0
  25. package/src/index.js +95 -2
  26. package/src/llm/openai.js +204 -58
  27. package/src/llm/tinfoil.js +508 -0
  28. package/src/omf.js +343 -0
  29. package/src/prompt_sets/conversation/ingestion.js +111 -12
  30. package/src/prompt_sets/document/ingestion.js +98 -4
  31. package/src/prompt_sets/index.js +12 -4
  32. package/src/types.js +135 -4
  33. package/src/vendor/tinfoil.browser.d.ts +2 -0
  34. package/src/vendor/tinfoil.browser.js +41596 -0
  35. package/types/backends/BaseStorage.d.ts +19 -0
  36. package/types/backends/indexeddb.d.ts +1 -0
  37. package/types/browser.d.ts +17 -0
  38. package/types/engine/deleter.d.ts +67 -0
  39. package/types/engine/executors.d.ts +56 -2
  40. package/types/engine/recentConversation.d.ts +18 -0
  41. package/types/engine/retriever.d.ts +22 -9
  42. package/types/imports/claude.d.ts +14 -0
  43. package/types/imports/importData.d.ts +29 -0
  44. package/types/imports/index.d.ts +2 -0
  45. package/types/index.d.ts +9 -0
  46. package/types/llm/openai.d.ts +6 -9
  47. package/types/llm/tinfoil.d.ts +13 -0
  48. package/types/omf.d.ts +40 -0
  49. package/types/prompt_sets/conversation/ingestion.d.ts +8 -3
  50. package/types/prompt_sets/document/ingestion.d.ts +8 -3
  51. package/types/types.d.ts +127 -2
  52. package/types/vendor/tinfoil.browser.d.ts +6348 -0
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Tinfoil SDK client with fail-closed attestation verification.
3
+ *
4
+ * In browser environments this loads the vendored browser bundle lazily.
5
+ * In Node environments it imports the `tinfoil` package directly.
6
+ */
7
+ /** @import { ChatCompletionParams, ChatCompletionResponse, LLMClient, MemoryBankLLMConfig, ToolCall } from '../types.js' */
8
+ /**
9
+ * @typedef {Error & { status?: number, retryable?: boolean, retryAfterMs?: number | null, _retryFinalized?: boolean }} ApiError
10
+ */
11
+
12
+ const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
13
+ const RETRYABLE_ERROR_CODES = new Set([
14
+ 'ECONNRESET',
15
+ 'ECONNREFUSED',
16
+ 'ENOTFOUND',
17
+ 'ETIMEDOUT',
18
+ 'EAI_AGAIN',
19
+ 'UND_ERR_CONNECT_TIMEOUT',
20
+ 'UND_ERR_SOCKET',
21
+ ]);
22
+ const MAX_ATTEMPTS = 3;
23
+ const BASE_DELAY_MS = 400;
24
+ const MAX_DELAY_MS = 2500;
25
+ const DEFAULT_CONFIG_REPO = 'tinfoilsh/confidential-model-router';
26
+ const DEFAULT_ENCLAVE_URL = 'https://inference.tinfoil.sh';
27
+
28
+ let browserTinfoilModulePromise = null;
29
+ let nodeTinfoilModulePromise = null;
30
+
31
+ function isBrowserRuntime() {
32
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
33
+ }
34
+
35
+ async function loadBrowserTinfoilModule() {
36
+ if (!browserTinfoilModulePromise) {
37
+ browserTinfoilModulePromise = import('../vendor/tinfoil.browser.js');
38
+ }
39
+ return browserTinfoilModulePromise;
40
+ }
41
+
42
+ async function loadBrowserTinfoilAI() {
43
+ const mod = await loadBrowserTinfoilModule();
44
+ return mod.TinfoilAI || mod.default;
45
+ }
46
+
47
+ async function loadNodeTinfoilAI() {
48
+ if (!nodeTinfoilModulePromise) {
49
+ nodeTinfoilModulePromise = Function('s', 'return import(s)')('tinfoil');
50
+ }
51
+
52
+ let mod;
53
+ try {
54
+ mod = await nodeTinfoilModulePromise;
55
+ } catch (error) {
56
+ if (error?.code === 'ERR_MODULE_NOT_FOUND' || String(error?.message || '').includes("Cannot find package 'tinfoil'")) {
57
+ throw new Error('Missing dependency "tinfoil". Run `npm install` in the nanomem package first.');
58
+ }
59
+ throw error;
60
+ }
61
+
62
+ return mod.TinfoilAI || mod.default;
63
+ }
64
+
65
+ function normalizeBaseUrl(baseUrl) {
66
+ if (!baseUrl) return null;
67
+ const value = String(baseUrl).trim();
68
+ return value.replace(/\/+$/, '');
69
+ }
70
+
71
+ function normalizeEnclaveUrl(enclaveURL, baseUrl) {
72
+ const raw = normalizeBaseUrl(enclaveURL) || normalizeBaseUrl(baseUrl) || DEFAULT_ENCLAVE_URL;
73
+ return raw.endsWith('/v1') ? raw.slice(0, -3).replace(/\/+$/, '') : raw;
74
+ }
75
+
76
+ function formatVerificationSteps(verification) {
77
+ const steps = verification?.steps || {};
78
+ return Object.entries(steps)
79
+ .filter(([, state]) => state?.status)
80
+ .map(([name, state]) => `${name}=${state.status}${state.error ? ` (${state.error})` : ''}`)
81
+ .join(', ');
82
+ }
83
+
84
+ function normalizeToolCalls(toolCalls) {
85
+ return (toolCalls || []).map((tc) => ({
86
+ id: tc.id || '',
87
+ type: 'function',
88
+ function: {
89
+ name: tc.function?.name || '',
90
+ arguments: tc.function?.arguments || '{}',
91
+ },
92
+ }));
93
+ }
94
+
95
+ function extractTextContent(content) {
96
+ if (typeof content === 'string') return content;
97
+ if (!Array.isArray(content)) return '';
98
+ return content
99
+ .map((part) => {
100
+ if (typeof part === 'string') return part;
101
+ if (typeof part?.text === 'string') return part.text;
102
+ return '';
103
+ })
104
+ .join('');
105
+ }
106
+
107
+ function extractReasoningDelta(delta) {
108
+ if (!delta || typeof delta !== 'object') return '';
109
+ if (typeof delta.reasoning === 'string') return delta.reasoning;
110
+ if (typeof delta.reasoning_content === 'string') return delta.reasoning_content;
111
+ return '';
112
+ }
113
+
114
+ function buildRequestBody({ model, messages, tools, max_tokens, temperature }) {
115
+ const body = { model, messages };
116
+ if (temperature != null) body.temperature = temperature;
117
+ if (max_tokens != null) body.max_tokens = max_tokens;
118
+ if (tools && tools.length > 0) body.tools = tools;
119
+ return body;
120
+ }
121
+
122
+ function buildRequestOptions(headers) {
123
+ if (!headers || Object.keys(headers).length === 0) {
124
+ return undefined;
125
+ }
126
+ return { headers };
127
+ }
128
+
129
+ /**
130
+ * @param {MemoryBankLLMConfig} [options]
131
+ * @returns {LLMClient}
132
+ */
133
+ export function createTinfoilClient(options = /** @type {MemoryBankLLMConfig} */ ({ apiKey: '' })) {
134
+ const {
135
+ apiKey,
136
+ baseUrl,
137
+ headers = {},
138
+ enclaveURL,
139
+ configRepo,
140
+ attestationBundleURL,
141
+ transport,
142
+ } = options;
143
+
144
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
145
+ const normalizedEnclaveUrl = normalizeEnclaveUrl(enclaveURL, baseUrl);
146
+ if (!apiKey) {
147
+ throw new Error('createTinfoilClient: options.apiKey is required.');
148
+ }
149
+
150
+ let clientPromise = null;
151
+ let browserClientPromise = null;
152
+
153
+ async function ensureClient() {
154
+ if (!clientPromise) {
155
+ clientPromise = (async () => {
156
+ const TinfoilAI = await loadNodeTinfoilAI();
157
+ const client = new TinfoilAI({
158
+ bearerToken: apiKey,
159
+ ...(normalizedBaseUrl ? { baseURL: normalizedBaseUrl } : {}),
160
+ ...(normalizedEnclaveUrl ? { enclaveURL: normalizedEnclaveUrl } : {}),
161
+ ...(configRepo ? { configRepo } : { configRepo: DEFAULT_CONFIG_REPO }),
162
+ ...(attestationBundleURL ? { attestationBundleURL } : {}),
163
+ ...(transport ? { transport } : {}),
164
+ dangerouslyAllowBrowser: true,
165
+ });
166
+
167
+ await client.ready();
168
+ const verification = await client.getVerificationDocument();
169
+ if (!verification?.securityVerified) {
170
+ throw new Error(`Tinfoil attestation verification failed: ${formatVerificationSteps(verification)}`);
171
+ }
172
+ return client;
173
+ })().catch((error) => {
174
+ clientPromise = null;
175
+ throw error;
176
+ });
177
+ }
178
+
179
+ return clientPromise;
180
+ }
181
+
182
+ async function ensureBrowserClient() {
183
+ if (!browserClientPromise) {
184
+ browserClientPromise = (async () => {
185
+ const TinfoilAI = await loadBrowserTinfoilAI();
186
+ if (typeof TinfoilAI !== 'function') {
187
+ throw new Error('Vendored Tinfoil browser bundle does not export TinfoilAI.');
188
+ }
189
+
190
+ const client = new TinfoilAI({
191
+ bearerToken: apiKey,
192
+ ...(normalizedBaseUrl ? { baseURL: normalizedBaseUrl } : {}),
193
+ ...(normalizedEnclaveUrl ? { enclaveURL: normalizedEnclaveUrl } : {}),
194
+ ...(configRepo ? { configRepo } : { configRepo: DEFAULT_CONFIG_REPO }),
195
+ ...(attestationBundleURL ? { attestationBundleURL } : {}),
196
+ ...(transport ? { transport } : {}),
197
+ dangerouslyAllowBrowser: true,
198
+ });
199
+
200
+ await client.ready();
201
+ const verification = await client.getVerificationDocument();
202
+ if (!verification?.securityVerified) {
203
+ throw new Error(`Tinfoil attestation verification failed: ${formatVerificationSteps(verification)}`);
204
+ }
205
+
206
+ return client;
207
+ })().catch((error) => {
208
+ browserClientPromise = null;
209
+ throw error;
210
+ });
211
+ }
212
+
213
+ return browserClientPromise;
214
+ }
215
+
216
+ async function createChatCompletion(params) {
217
+ const body = buildRequestBody(params);
218
+ const requestOptions = buildRequestOptions(headers);
219
+
220
+ if (isBrowserRuntime()) {
221
+ const response = await withRetry(async () => {
222
+ const client = await ensureBrowserClient();
223
+ return client.chat.completions.create(body, requestOptions);
224
+ }, 'chat completion request');
225
+
226
+ const choice = response?.choices?.[0] || {};
227
+ const message = choice.message || {};
228
+
229
+ return {
230
+ content: extractTextContent(message.content),
231
+ tool_calls: normalizeToolCalls(message.tool_calls),
232
+ finish_reason: choice.finish_reason,
233
+ usage: response?.usage || null,
234
+ };
235
+ }
236
+
237
+ const response = await withRetry(async () => {
238
+ const client = await ensureClient();
239
+ return client.chat.completions.create(body, requestOptions);
240
+ }, 'chat completion request');
241
+
242
+ const choice = response?.choices?.[0] || {};
243
+ const message = choice.message || {};
244
+
245
+ return {
246
+ content: extractTextContent(message.content),
247
+ tool_calls: normalizeToolCalls(message.tool_calls),
248
+ finish_reason: choice.finish_reason,
249
+ usage: response?.usage || null,
250
+ };
251
+ }
252
+
253
+ async function streamChatCompletion({ model, messages, tools, max_tokens, temperature, onDelta, onReasoning }) {
254
+ const body = buildRequestBody({ model, messages, tools, max_tokens, temperature });
255
+ const requestOptions = buildRequestOptions(headers);
256
+
257
+ if (isBrowserRuntime()) {
258
+ return withRetry(async () => {
259
+ const client = await ensureBrowserClient();
260
+ const stream = await client.chat.completions.create({
261
+ ...body,
262
+ stream: true,
263
+ }, requestOptions);
264
+
265
+ let content = '';
266
+ let sawStreamData = false;
267
+ let finishReason = null;
268
+ const toolCallAccumulator = new Map();
269
+
270
+ try {
271
+ for await (const chunk of stream) {
272
+ sawStreamData = true;
273
+
274
+ const choice = chunk?.choices?.[0];
275
+ if (!choice) continue;
276
+ if (choice.finish_reason) finishReason = choice.finish_reason;
277
+
278
+ const delta = choice.delta;
279
+ if (!delta) continue;
280
+
281
+ if (delta.content) {
282
+ content += delta.content;
283
+ onDelta?.(delta.content);
284
+ }
285
+
286
+ const reasoning = extractReasoningDelta(delta);
287
+ if (reasoning) {
288
+ onReasoning?.(reasoning);
289
+ }
290
+
291
+ if (delta.tool_calls) {
292
+ for (const tc of delta.tool_calls) {
293
+ const index = tc.index ?? 0;
294
+ if (!toolCallAccumulator.has(index)) {
295
+ toolCallAccumulator.set(index, {
296
+ id: tc.id || '',
297
+ type: 'function',
298
+ function: { name: '', arguments: '' },
299
+ });
300
+ }
301
+
302
+ const acc = toolCallAccumulator.get(index);
303
+ if (!acc) continue;
304
+ if (tc.id) acc.id = tc.id;
305
+ if (tc.function?.name) acc.function.name += tc.function.name;
306
+ if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
307
+ }
308
+ }
309
+ }
310
+ } catch (error) {
311
+ if (!sawStreamData && isRetryableNetworkError(error)) {
312
+ const retryError = /** @type {any} */ (asError(error));
313
+ retryError.retryable = true;
314
+ throw retryError;
315
+ }
316
+ throw error;
317
+ }
318
+
319
+ const tool_calls = [...toolCallAccumulator.entries()]
320
+ .sort(([a], [b]) => a - b)
321
+ .map(([, value]) => value);
322
+
323
+ return {
324
+ content,
325
+ tool_calls,
326
+ finish_reason: finishReason || undefined,
327
+ usage: null,
328
+ };
329
+ }, 'streaming chat completion');
330
+ }
331
+
332
+ return withRetry(async () => {
333
+ const client = await ensureClient();
334
+ const stream = await client.chat.completions.create({ ...body, stream: true }, requestOptions);
335
+
336
+ let content = '';
337
+ let sawStreamData = false;
338
+ let finishReason = null;
339
+ const toolCallAccumulator = new Map();
340
+
341
+ try {
342
+ for await (const chunk of stream) {
343
+ sawStreamData = true;
344
+
345
+ const choice = chunk?.choices?.[0];
346
+ if (!choice) continue;
347
+ if (choice.finish_reason) finishReason = choice.finish_reason;
348
+
349
+ const delta = choice.delta;
350
+ if (!delta) continue;
351
+
352
+ if (delta.content) {
353
+ content += delta.content;
354
+ onDelta?.(delta.content);
355
+ }
356
+
357
+ const reasoning = extractReasoningDelta(delta);
358
+ if (reasoning) {
359
+ onReasoning?.(reasoning);
360
+ }
361
+
362
+ if (delta.tool_calls) {
363
+ for (const tc of delta.tool_calls) {
364
+ const index = tc.index ?? 0;
365
+ if (!toolCallAccumulator.has(index)) {
366
+ toolCallAccumulator.set(index, {
367
+ id: tc.id || '',
368
+ type: 'function',
369
+ function: { name: '', arguments: '' },
370
+ });
371
+ }
372
+
373
+ const acc = toolCallAccumulator.get(index);
374
+ if (!acc) continue;
375
+ if (tc.id) acc.id = tc.id;
376
+ if (tc.function?.name) acc.function.name += tc.function.name;
377
+ if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
378
+ }
379
+ }
380
+ }
381
+ } catch (error) {
382
+ if (!sawStreamData && isRetryableNetworkError(error)) {
383
+ const retryError = /** @type {any} */ (asError(error));
384
+ retryError.retryable = true;
385
+ throw retryError;
386
+ }
387
+ throw error;
388
+ }
389
+
390
+ const tool_calls = [...toolCallAccumulator.entries()]
391
+ .sort(([a], [b]) => a - b)
392
+ .map(([, value]) => value);
393
+
394
+ return {
395
+ content,
396
+ tool_calls,
397
+ finish_reason: finishReason || undefined,
398
+ usage: null,
399
+ };
400
+ }, 'streaming chat completion');
401
+ }
402
+
403
+ return { createChatCompletion, streamChatCompletion };
404
+ }
405
+
406
+ async function withRetry(fn, context) {
407
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
408
+ try {
409
+ return await fn(attempt);
410
+ } catch (error) {
411
+ const normalized = /** @type {any} */ (asError(error));
412
+ const shouldRetry = attempt < MAX_ATTEMPTS && isRetryableError(normalized);
413
+ if (!shouldRetry) {
414
+ throw finalizeRetryError(normalized, attempt);
415
+ }
416
+
417
+ const delay = getRetryDelay(attempt - 1, normalized.retryAfterMs || null);
418
+ console.warn(`[nanomem/tinfoil] ${context} attempt ${attempt}/${MAX_ATTEMPTS} failed: ${normalized.message}. Retrying in ${Math.round(delay)}ms.`);
419
+ await sleep(delay);
420
+ }
421
+ }
422
+
423
+ throw new Error(`Tinfoil API ${context} failed after ${MAX_ATTEMPTS} attempts.`);
424
+ }
425
+
426
+ function isRetryableError(error) {
427
+ if (!error) return false;
428
+ if (error.retryable === true) return true;
429
+ if (typeof error.status === 'number') {
430
+ return RETRYABLE_STATUS.has(error.status);
431
+ }
432
+ return isRetryableNetworkError(error);
433
+ }
434
+
435
+ function isRetryableNetworkError(error) {
436
+ if (!error || error.isUserAbort) return false;
437
+ if (error.name === 'TypeError' || error.name === 'AbortError') return true;
438
+
439
+ const code = String(error.code || error.cause?.code || '').toUpperCase();
440
+ if (RETRYABLE_ERROR_CODES.has(code)) return true;
441
+
442
+ const message = String(error.message || '').toLowerCase();
443
+ return message.includes('failed to fetch')
444
+ || message.includes('network')
445
+ || message.includes('timeout')
446
+ || message.includes('err_network_changed')
447
+ || message.includes('econnreset')
448
+ || message.includes('connection');
449
+ }
450
+
451
+ function getRetryDelay(attempt, retryAfterMs = null) {
452
+ if (retryAfterMs != null && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
453
+ return Math.min(retryAfterMs, MAX_DELAY_MS);
454
+ }
455
+
456
+ const exponential = BASE_DELAY_MS * Math.pow(2, attempt);
457
+ const jitter = Math.random() * BASE_DELAY_MS;
458
+ return Math.min(exponential + jitter, MAX_DELAY_MS);
459
+ }
460
+
461
+ function sleep(ms) {
462
+ return new Promise((resolve) => setTimeout(resolve, ms));
463
+ }
464
+
465
+ async function createHttpError(response, attempt = 1) {
466
+ const text = await response.text().catch(() => '');
467
+ const suffix = attempt > 1 ? ` after ${attempt} attempts` : '';
468
+ const error = /** @type {ApiError} */ (new Error(`Tinfoil API error ${response.status}${suffix}: ${text}`));
469
+ error.status = response.status;
470
+ error.retryable = RETRYABLE_STATUS.has(response.status);
471
+ error.retryAfterMs = parseRetryAfterMs(response);
472
+ return error;
473
+ }
474
+
475
+ function parseRetryAfterMs(response) {
476
+ const value = response?.headers?.get?.('Retry-After');
477
+ if (!value) return null;
478
+
479
+ const seconds = Number.parseInt(value, 10);
480
+ if (Number.isFinite(seconds) && seconds > 0) {
481
+ return seconds * 1000;
482
+ }
483
+
484
+ const date = Date.parse(value);
485
+ if (Number.isFinite(date)) {
486
+ const ms = date - Date.now();
487
+ return ms > 0 ? ms : null;
488
+ }
489
+
490
+ return null;
491
+ }
492
+
493
+ /**
494
+ * @param {unknown} error
495
+ * @returns {ApiError}
496
+ */
497
+ function asError(error) {
498
+ return /** @type {ApiError} */ (error instanceof Error ? error : new Error(String(error)));
499
+ }
500
+
501
+ function finalizeRetryError(error, attempt) {
502
+ if (!error) return error;
503
+ const typedError = /** @type {any} */ (error);
504
+ if (attempt > 1 && !typedError.retryAttempts) {
505
+ typedError.retryAttempts = attempt;
506
+ }
507
+ return typedError;
508
+ }