@openanonymity/nanomem 0.1.0 → 0.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/LICENSE +21 -0
- package/README.md +46 -8
- package/package.json +7 -3
- package/src/backends/BaseStorage.js +147 -3
- package/src/backends/indexeddb.js +21 -8
- package/src/browser.js +227 -0
- package/src/cli/auth.js +1 -1
- package/src/cli/commands.js +51 -8
- package/src/cli/config.js +1 -1
- package/src/cli/help.js +5 -2
- package/src/cli/output.js +4 -0
- package/src/cli.js +5 -2
- package/src/engine/deleter.js +187 -0
- package/src/engine/executors.js +416 -4
- package/src/engine/ingester.js +83 -61
- package/src/engine/recentConversation.js +110 -0
- package/src/engine/retriever.js +238 -36
- package/src/engine/toolLoop.js +51 -9
- package/src/imports/importData.js +454 -0
- package/src/imports/index.js +5 -0
- package/src/index.js +95 -2
- package/src/llm/openai.js +204 -58
- package/src/llm/tinfoil.js +508 -0
- package/src/omf.js +343 -0
- package/src/prompt_sets/conversation/ingestion.js +101 -11
- package/src/prompt_sets/document/ingestion.js +92 -4
- package/src/prompt_sets/index.js +12 -4
- package/src/types.js +133 -3
- package/src/vendor/tinfoil.browser.d.ts +2 -0
- package/src/vendor/tinfoil.browser.js +41596 -0
- package/types/backends/BaseStorage.d.ts +19 -0
- package/types/backends/indexeddb.d.ts +1 -0
- package/types/browser.d.ts +17 -0
- package/types/engine/deleter.d.ts +67 -0
- package/types/engine/executors.d.ts +54 -0
- package/types/engine/recentConversation.d.ts +18 -0
- package/types/engine/retriever.d.ts +22 -9
- package/types/imports/importData.d.ts +29 -0
- package/types/imports/index.d.ts +1 -0
- package/types/index.d.ts +9 -0
- package/types/llm/openai.d.ts +6 -9
- package/types/llm/tinfoil.d.ts +13 -0
- package/types/omf.d.ts +40 -0
- package/types/prompt_sets/conversation/ingestion.d.ts +8 -3
- package/types/prompt_sets/document/ingestion.d.ts +8 -3
- package/types/types.d.ts +125 -2
- package/types/vendor/tinfoil.browser.d.ts +6348 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open Anonymity Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ Retrieval is only one part of memory. `nanomem` is built for the maintenance lay
|
|
|
24
24
|
- **Compaction and cleanup.** Collapse repeated signals into stable knowledge and move stale memory into history.
|
|
25
25
|
- **Conflict-aware updates.** Resolve outdated or contradictory facts using recency, source, and confidence.
|
|
26
26
|
- **Import your existing history.** Start from ChatGPT exports, [OA Chat](https://chat.openanonymity.ai) exports, transcripts, message arrays, markdown notes, or whole markdown directories.
|
|
27
|
+
- **Portable memory exchange.** Export full memory state as plain text, ZIP, or Open Memory Format (OMF), and merge OMF documents back in programmatically.
|
|
27
28
|
- **Flexible storage.** Run on local files, IndexedDB, in-memory storage, or a custom backend.
|
|
28
29
|
- **Built to plug in.** Use it from the CLI, as a library, or as a memory layer for other agents.
|
|
29
30
|
|
|
@@ -32,7 +33,7 @@ Retrieval is only one part of memory. `nanomem` is built for the maintenance lay
|
|
|
32
33
|
Install:
|
|
33
34
|
|
|
34
35
|
```bash
|
|
35
|
-
npm
|
|
36
|
+
npm i @openanonymity/nanomem
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
Set up once:
|
|
@@ -43,6 +44,13 @@ nanomem login
|
|
|
43
44
|
|
|
44
45
|
This walks you through provider, model, API key, and where to store your memory. Config is saved to `~/.config/nanomem/config.json`. Filesystem memory lives in `~/nanomem/` by default.
|
|
45
46
|
|
|
47
|
+
Add facts directly:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
nanomem add "I moved to Seattle and started a new job at Acme."
|
|
51
|
+
nanomem update "Actually I moved to Portland, not Seattle."
|
|
52
|
+
```
|
|
53
|
+
|
|
46
54
|
Import history or notes:
|
|
47
55
|
|
|
48
56
|
```bash
|
|
@@ -58,6 +66,13 @@ nanomem retrieve "what are my hobbies?"
|
|
|
58
66
|
nanomem retrieve "what are my hobbies?" --render
|
|
59
67
|
```
|
|
60
68
|
|
|
69
|
+
Delete facts from memory:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
nanomem delete "I have a dog named Mochi"
|
|
73
|
+
nanomem delete "I have a dog named Mochi" --deep
|
|
74
|
+
```
|
|
75
|
+
|
|
61
76
|
Compact and clean up memory:
|
|
62
77
|
|
|
63
78
|
```bash
|
|
@@ -73,6 +88,17 @@ nanomem login --provider anthropic --api-key sk-ant-... --model claude-sonnet-4-
|
|
|
73
88
|
|
|
74
89
|
Supported providers include OpenAI, Anthropic, Tinfoil, OpenRouter, and OpenAI-compatible endpoints via `--base-url`.
|
|
75
90
|
|
|
91
|
+
When `provider` is `tinfoil`, nanomem now uses the Tinfoil SDK and fails
|
|
92
|
+
closed on enclave attestation verification before any inference request is
|
|
93
|
+
sent. Browser consumers load a vendored SDK bundle, construct `TinfoilAI`,
|
|
94
|
+
and require `await client.getVerificationDocument()` to report
|
|
95
|
+
`securityVerified === true` before inference. The vendored bundle lives at
|
|
96
|
+
`src/vendor/tinfoil.browser.js`; refresh it after SDK upgrades with:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm run vendor:tinfoil
|
|
100
|
+
```
|
|
101
|
+
|
|
76
102
|
## How it works
|
|
77
103
|
|
|
78
104
|
```text
|
|
@@ -89,7 +115,9 @@ conversation / notes / exports
|
|
|
89
115
|
| memory retrieve
|
|
90
116
|
| file selection + bullet-level scoring
|
|
91
117
|
v
|
|
92
|
-
|
|
118
|
+
prompt crafting / retrieval
|
|
119
|
+
retrieve -> augment_query(user_query, memory_files)
|
|
120
|
+
-> minimized reviewable prompt
|
|
93
121
|
|
|
|
94
122
|
v
|
|
95
123
|
memory compact
|
|
@@ -144,17 +172,25 @@ await memory.ingest([
|
|
|
144
172
|
|
|
145
173
|
const result = await memory.retrieve('Where do I live now?');
|
|
146
174
|
await memory.compact();
|
|
175
|
+
|
|
176
|
+
const omf = await memory.exportOmf();
|
|
177
|
+
const preview = await memory.previewOmfImport(omf);
|
|
178
|
+
await memory.importOmf(omf);
|
|
147
179
|
```
|
|
148
180
|
|
|
149
181
|
## Common commands
|
|
150
182
|
|
|
151
183
|
```bash
|
|
152
|
-
nanomem
|
|
153
|
-
nanomem
|
|
154
|
-
nanomem
|
|
155
|
-
nanomem
|
|
156
|
-
nanomem
|
|
157
|
-
nanomem
|
|
184
|
+
nanomem add <text> # add new facts
|
|
185
|
+
nanomem update <text> # correct existing facts
|
|
186
|
+
nanomem delete <query> # delete facts matching a query
|
|
187
|
+
nanomem delete <query> --deep # delete across all files (thorough)
|
|
188
|
+
nanomem import <file|dir|-> # import history or notes
|
|
189
|
+
nanomem retrieve <query> [--context <file>] # retrieve relevant context
|
|
190
|
+
nanomem tree # browse memory files
|
|
191
|
+
nanomem compact # deduplicate and archive
|
|
192
|
+
nanomem export --format zip # export everything
|
|
193
|
+
nanomem status # show config and stats
|
|
158
194
|
```
|
|
159
195
|
|
|
160
196
|
For terminal use, `--render` will format markdown-heavy output like `read` and `retrieve` into a more readable ANSI-rendered view while leaving `--json` and piped output unchanged.
|
|
@@ -189,6 +225,8 @@ nanomem import my-notes.md --format markdown # document mode (explicit)
|
|
|
189
225
|
|
|
190
226
|
Internals: [docs/memory-system.md](./docs/memory-system.md)
|
|
191
227
|
|
|
228
|
+
OMF spec: [docs/omf.md](./docs/omf.md)
|
|
229
|
+
|
|
192
230
|
## License
|
|
193
231
|
|
|
194
232
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openanonymity/nanomem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "LLM-driven personal memory with agentic retrieval, extraction, and compaction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"url": "https://github.com/openanonymity/nanomem/issues"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"
|
|
20
|
+
"test": "node --test test/**/*.test.js",
|
|
21
|
+
"vendor:tinfoil": "node scripts/vendor-tinfoil.mjs",
|
|
22
|
+
"prepublishOnly": "npm run vendor:tinfoil && npm run build:types",
|
|
21
23
|
"build:types": "tsc",
|
|
22
24
|
"prepack": "tsc"
|
|
23
25
|
},
|
|
@@ -72,10 +74,12 @@
|
|
|
72
74
|
"license": "MIT",
|
|
73
75
|
"devDependencies": {
|
|
74
76
|
"@types/node": "^25.5.2",
|
|
77
|
+
"esbuild": "^0.27.2",
|
|
75
78
|
"typescript": "^6.0.2"
|
|
76
79
|
},
|
|
77
80
|
"dependencies": {
|
|
78
|
-
"@pierre/diffs": "^1.1.12"
|
|
81
|
+
"@pierre/diffs": "^1.1.12",
|
|
82
|
+
"tinfoil": "^1.1.3"
|
|
79
83
|
},
|
|
80
84
|
"directories": {
|
|
81
85
|
"doc": "docs"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* getTree() → string
|
|
21
21
|
*/
|
|
22
22
|
/** @import { ExportRecord, ListResult, SearchResult, StorageMetadata } from '../types.js' */
|
|
23
|
-
import { parseBullets, extractTitles, countBullets } from '../bullets/index.js';
|
|
23
|
+
import { parseBullets, extractTitles, countBullets, normalizeFactText } from '../bullets/index.js';
|
|
24
24
|
|
|
25
25
|
export class BaseStorage {
|
|
26
26
|
|
|
@@ -42,7 +42,41 @@ export class BaseStorage {
|
|
|
42
42
|
* @returns {Promise<string | null>}
|
|
43
43
|
*/
|
|
44
44
|
async read(path) {
|
|
45
|
-
|
|
45
|
+
const requestedPath = this._normalizeRequestedPath(path);
|
|
46
|
+
if (!requestedPath) return null;
|
|
47
|
+
|
|
48
|
+
const exact = await this._readRaw(requestedPath);
|
|
49
|
+
if (exact !== null) {
|
|
50
|
+
return exact;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resolvedPath = await this._resolveReadablePath(requestedPath);
|
|
54
|
+
if (!resolvedPath || resolvedPath === requestedPath) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return this._readRaw(resolvedPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a user/model-supplied path to the canonical readable path.
|
|
63
|
+
*
|
|
64
|
+
* Returns the exact stored path when possible, or a normalized fallback
|
|
65
|
+
* match when the requested path is only approximately correct.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} path
|
|
68
|
+
* @returns {Promise<string | null>}
|
|
69
|
+
*/
|
|
70
|
+
async resolvePath(path) {
|
|
71
|
+
const requestedPath = this._normalizeRequestedPath(path);
|
|
72
|
+
if (!requestedPath) return null;
|
|
73
|
+
|
|
74
|
+
const exact = await this._readRaw(requestedPath);
|
|
75
|
+
if (exact !== null) {
|
|
76
|
+
return requestedPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this._resolveReadablePath(requestedPath);
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
/**
|
|
@@ -135,7 +169,10 @@ export class BaseStorage {
|
|
|
135
169
|
/** Override for efficient path listing. Default uses exportAll(). */
|
|
136
170
|
async _listAllPaths() {
|
|
137
171
|
const all = await this.exportAll();
|
|
138
|
-
return all
|
|
172
|
+
return all
|
|
173
|
+
.filter((record) => typeof record?.path === 'string')
|
|
174
|
+
.filter((record) => this._isInternalPath(record.path) || typeof record?.content === 'string')
|
|
175
|
+
.map((record) => record.path);
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
_parentPath(filePath) {
|
|
@@ -143,6 +180,113 @@ export class BaseStorage {
|
|
|
143
180
|
return lastSlash === -1 ? '' : filePath.slice(0, lastSlash);
|
|
144
181
|
}
|
|
145
182
|
|
|
183
|
+
_basenamePath(filePath) {
|
|
184
|
+
const normalized = this._normalizeRequestedPath(filePath);
|
|
185
|
+
if (!normalized) return '';
|
|
186
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
187
|
+
return lastSlash === -1 ? normalized : normalized.slice(lastSlash + 1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_normalizeRequestedPath(path) {
|
|
191
|
+
return String(path || '')
|
|
192
|
+
.trim()
|
|
193
|
+
.replace(/\\/g, '/')
|
|
194
|
+
.replace(/^\.\//, '')
|
|
195
|
+
.replace(/^\/+/, '')
|
|
196
|
+
.replace(/\/+/g, '/');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_normalizeLookupKey(path, { stripExtension = false } = {}) {
|
|
200
|
+
let normalized = this._normalizeRequestedPath(path);
|
|
201
|
+
if (!normalized) return '';
|
|
202
|
+
|
|
203
|
+
if (stripExtension) {
|
|
204
|
+
normalized = normalized.replace(/\.md$/i, '');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof normalized.normalize === 'function') {
|
|
208
|
+
normalized = normalized.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return normalizeFactText(normalized.replace(/[\/_]/g, ' '));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async _listReadablePaths() {
|
|
215
|
+
const all = await this.exportAll();
|
|
216
|
+
return all
|
|
217
|
+
.filter((record) => typeof record?.path === 'string')
|
|
218
|
+
.filter((record) => !this._isInternalPath(record.path))
|
|
219
|
+
.filter((record) => typeof record?.content === 'string')
|
|
220
|
+
.map((record) => record.path);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async _resolveReadablePath(path) {
|
|
224
|
+
const requestedPath = this._normalizeRequestedPath(path);
|
|
225
|
+
if (!requestedPath) return null;
|
|
226
|
+
|
|
227
|
+
const readablePaths = await this._listReadablePaths();
|
|
228
|
+
if (readablePaths.length === 0) return null;
|
|
229
|
+
|
|
230
|
+
const fullKey = this._normalizeLookupKey(requestedPath);
|
|
231
|
+
const extlessKey = this._normalizeLookupKey(requestedPath, { stripExtension: true });
|
|
232
|
+
|
|
233
|
+
const fullMatches = readablePaths.filter((candidate) => this._normalizeLookupKey(candidate) === fullKey);
|
|
234
|
+
if (fullMatches.length > 0) {
|
|
235
|
+
return this._choosePreferredPath(fullMatches, requestedPath);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const extlessMatches = readablePaths.filter((candidate) => this._normalizeLookupKey(candidate, { stripExtension: true }) === extlessKey);
|
|
239
|
+
if (extlessMatches.length > 0) {
|
|
240
|
+
return this._choosePreferredPath(extlessMatches, requestedPath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const basenameKey = this._normalizeLookupKey(this._basenamePath(requestedPath), { stripExtension: true });
|
|
244
|
+
if (!basenameKey) return null;
|
|
245
|
+
|
|
246
|
+
const basenameMatches = readablePaths.filter((candidate) => (
|
|
247
|
+
this._normalizeLookupKey(this._basenamePath(candidate), { stripExtension: true }) === basenameKey
|
|
248
|
+
));
|
|
249
|
+
if (basenameMatches.length > 0) {
|
|
250
|
+
return this._choosePreferredPath(basenameMatches, requestedPath);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_choosePreferredPath(candidates, requestedPath) {
|
|
257
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
|
258
|
+
if (candidates.length === 1) return candidates[0];
|
|
259
|
+
|
|
260
|
+
const requestedParent = this._normalizeLookupKey(this._parentPath(requestedPath));
|
|
261
|
+
const requestedBase = this._normalizeLookupKey(this._basenamePath(requestedPath), { stripExtension: true });
|
|
262
|
+
|
|
263
|
+
return [...candidates]
|
|
264
|
+
.sort((left, right) => {
|
|
265
|
+
const leftScore = this._pathMatchScore(left, requestedParent, requestedBase);
|
|
266
|
+
const rightScore = this._pathMatchScore(right, requestedParent, requestedBase);
|
|
267
|
+
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
268
|
+
if (left.length !== right.length) return left.length - right.length;
|
|
269
|
+
return left.localeCompare(right);
|
|
270
|
+
})[0];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_pathMatchScore(candidate, requestedParent, requestedBase) {
|
|
274
|
+
let score = 0;
|
|
275
|
+
if (requestedParent) {
|
|
276
|
+
if (this._normalizeLookupKey(this._parentPath(candidate)) === requestedParent) {
|
|
277
|
+
score += 4;
|
|
278
|
+
}
|
|
279
|
+
} else if (!this._parentPath(candidate)) {
|
|
280
|
+
score += 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (requestedBase && this._normalizeLookupKey(this._basenamePath(candidate), { stripExtension: true }) === requestedBase) {
|
|
284
|
+
score += 2;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return score;
|
|
288
|
+
}
|
|
289
|
+
|
|
146
290
|
/** Generate a one-line summary of file content for the index. */
|
|
147
291
|
_generateOneLiner(content) {
|
|
148
292
|
if (!content) return '';
|
|
@@ -45,6 +45,9 @@ class IndexedDBStorage extends BaseStorage {
|
|
|
45
45
|
try { await this._bootstrap(); } catch (err) {
|
|
46
46
|
console.warn('[IndexedDBStorage] Init error:', err);
|
|
47
47
|
}
|
|
48
|
+
try { await this.rebuildTree(); } catch (err) {
|
|
49
|
+
console.warn('[IndexedDBStorage] Tree rebuild error:', err);
|
|
50
|
+
}
|
|
48
51
|
resolve(/** @type {IDBDatabase} */ (this.db));
|
|
49
52
|
};
|
|
50
53
|
|
|
@@ -100,9 +103,9 @@ class IndexedDBStorage extends BaseStorage {
|
|
|
100
103
|
|
|
101
104
|
await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
102
105
|
const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readwrite');
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
tx.objectStore(STORE_NAME).put(record);
|
|
107
|
+
tx.oncomplete = () => resolve();
|
|
108
|
+
tx.onerror = () => reject(tx.error);
|
|
106
109
|
}));
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -116,9 +119,9 @@ class IndexedDBStorage extends BaseStorage {
|
|
|
116
119
|
|
|
117
120
|
await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
118
121
|
const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readwrite');
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
tx.objectStore(STORE_NAME).delete(path);
|
|
123
|
+
tx.oncomplete = () => resolve();
|
|
124
|
+
tx.onerror = () => reject(tx.error);
|
|
122
125
|
}));
|
|
123
126
|
await this.rebuildTree();
|
|
124
127
|
}
|
|
@@ -153,7 +156,7 @@ class IndexedDBStorage extends BaseStorage {
|
|
|
153
156
|
/** @returns {Promise<void>} */
|
|
154
157
|
async rebuildTree() {
|
|
155
158
|
await this.init();
|
|
156
|
-
const all = await this._getAll();
|
|
159
|
+
const all = this._sanitizeRecords(await this._getAll());
|
|
157
160
|
const files = all
|
|
158
161
|
.filter((r) => !this._isInternalPath(r.path))
|
|
159
162
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -181,7 +184,7 @@ class IndexedDBStorage extends BaseStorage {
|
|
|
181
184
|
/** @returns {Promise<ExportRecord[]>} */
|
|
182
185
|
async exportAll() {
|
|
183
186
|
await this.init();
|
|
184
|
-
return this._getAll();
|
|
187
|
+
return this._sanitizeRecords(await this._getAll());
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
// ─── Internal IndexedDB helpers ──────────────────────────────
|
|
@@ -203,6 +206,16 @@ class IndexedDBStorage extends BaseStorage {
|
|
|
203
206
|
request.onerror = () => reject(request.error);
|
|
204
207
|
});
|
|
205
208
|
}
|
|
209
|
+
|
|
210
|
+
_sanitizeRecords(records) {
|
|
211
|
+
return (records || [])
|
|
212
|
+
.filter((record) => typeof record?.path === 'string' && record.path.trim())
|
|
213
|
+
.map((record) => ({
|
|
214
|
+
...record,
|
|
215
|
+
path: record.path.trim()
|
|
216
|
+
}))
|
|
217
|
+
.filter((record) => this._isInternalPath(record.path) || typeof record?.content === 'string');
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
export { IndexedDBStorage };
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe nanomem entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors createMemoryBank from index.js but excludes the filesystem
|
|
5
|
+
* backend so browser bundlers do not try to resolve node:* imports.
|
|
6
|
+
*/
|
|
7
|
+
/** @import { MemoryBank, MemoryBankConfig, MemoryBankLLMConfig, Message, IngestOptions, AugmentQueryResult, RetrievalResult, StorageBackend } from './types.js' */
|
|
8
|
+
|
|
9
|
+
import { createOpenAIClient } from './llm/openai.js';
|
|
10
|
+
import { createAnthropicClient } from './llm/anthropic.js';
|
|
11
|
+
import { MemoryBulletIndex } from './bullets/bulletIndex.js';
|
|
12
|
+
import { MemoryRetriever } from './engine/retriever.js';
|
|
13
|
+
import { MemoryIngester } from './engine/ingester.js';
|
|
14
|
+
import { MemoryCompactor } from './engine/compactor.js';
|
|
15
|
+
import { InMemoryStorage } from './backends/ram.js';
|
|
16
|
+
import { importData as importMemoryData } from './imports/importData.js';
|
|
17
|
+
import { serialize, toZip } from './utils/portability.js';
|
|
18
|
+
import { buildOmfExport, previewOmfImport, importOmf, parseOmfText, validateOmf } from './omf.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Remove review-only [[user_data]] markers before sending the final prompt to
|
|
22
|
+
* the frontier model.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} text
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function stripUserDataTags(text) {
|
|
28
|
+
return String(text ?? '')
|
|
29
|
+
.replace(/\[\[user_data\]\]/g, '')
|
|
30
|
+
.replace(/\[\[\/user_data\]\]/g, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {MemoryBankConfig} [config]
|
|
35
|
+
* @returns {MemoryBank}
|
|
36
|
+
*/
|
|
37
|
+
export function createMemoryBank(config = {}) {
|
|
38
|
+
const llmClient = config.llmClient || createBrowserLlmClient(config.llm);
|
|
39
|
+
const model = config.model || config.llm?.model || 'gpt-4o';
|
|
40
|
+
const backend = createBrowserBackend(config.storage);
|
|
41
|
+
const bulletIndex = new MemoryBulletIndex(backend);
|
|
42
|
+
|
|
43
|
+
const retrieval = new MemoryRetriever({
|
|
44
|
+
backend,
|
|
45
|
+
bulletIndex,
|
|
46
|
+
llmClient,
|
|
47
|
+
model,
|
|
48
|
+
onProgress: config.onProgress,
|
|
49
|
+
onModelText: config.onModelText
|
|
50
|
+
});
|
|
51
|
+
const ingester = new MemoryIngester({
|
|
52
|
+
backend,
|
|
53
|
+
bulletIndex,
|
|
54
|
+
llmClient,
|
|
55
|
+
model,
|
|
56
|
+
onToolCall: config.onToolCall
|
|
57
|
+
});
|
|
58
|
+
const compactor = new MemoryCompactor({
|
|
59
|
+
backend,
|
|
60
|
+
bulletIndex,
|
|
61
|
+
llmClient,
|
|
62
|
+
model,
|
|
63
|
+
onProgress: config.onCompactProgress
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
async function write(path, content) {
|
|
67
|
+
await backend.write(path, content);
|
|
68
|
+
await bulletIndex.refreshPath(path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function remove(path) {
|
|
72
|
+
await backend.delete(path);
|
|
73
|
+
await bulletIndex.refreshPath(path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function rebuildTree() {
|
|
77
|
+
await backend.rebuildTree();
|
|
78
|
+
await bulletIndex.rebuild();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
init: () => backend.init(),
|
|
83
|
+
retrieve: (query, conversationText) => retrieval.retrieveForQuery(query, conversationText),
|
|
84
|
+
augmentQuery: (query, conversationText) => retrieval.augmentQueryForPrompt(query, conversationText),
|
|
85
|
+
ingest: (messages, options) => ingester.ingest(messages, options),
|
|
86
|
+
importData: (input, options) => importMemoryData({
|
|
87
|
+
init: () => backend.init(),
|
|
88
|
+
ingest: (messages, ingestOptions) => ingester.ingest(messages, ingestOptions)
|
|
89
|
+
}, input, options),
|
|
90
|
+
exportOmf: async () => {
|
|
91
|
+
await backend.init();
|
|
92
|
+
return buildOmfExport({
|
|
93
|
+
read: (path) => backend.read(path),
|
|
94
|
+
write: (path, content) => write(path, content),
|
|
95
|
+
delete: (path) => remove(path),
|
|
96
|
+
exists: (path) => backend.exists(path),
|
|
97
|
+
search: (query) => backend.search(query),
|
|
98
|
+
ls: (dirPath) => backend.ls(dirPath),
|
|
99
|
+
getTree: () => backend.getTree(),
|
|
100
|
+
rebuildTree: () => rebuildTree(),
|
|
101
|
+
exportAll: () => backend.exportAll(),
|
|
102
|
+
clear: () => backend.clear(),
|
|
103
|
+
}, { sourceApp: 'nanomem' });
|
|
104
|
+
},
|
|
105
|
+
previewOmfImport: async (doc, options) => {
|
|
106
|
+
await backend.init();
|
|
107
|
+
return previewOmfImport({
|
|
108
|
+
read: (path) => backend.read(path),
|
|
109
|
+
write: (path, content) => write(path, content),
|
|
110
|
+
delete: (path) => remove(path),
|
|
111
|
+
exists: (path) => backend.exists(path),
|
|
112
|
+
search: (query) => backend.search(query),
|
|
113
|
+
ls: (dirPath) => backend.ls(dirPath),
|
|
114
|
+
getTree: () => backend.getTree(),
|
|
115
|
+
rebuildTree: () => rebuildTree(),
|
|
116
|
+
exportAll: () => backend.exportAll(),
|
|
117
|
+
clear: () => backend.clear(),
|
|
118
|
+
}, doc, options);
|
|
119
|
+
},
|
|
120
|
+
importOmf: async (doc, options) => {
|
|
121
|
+
await backend.init();
|
|
122
|
+
return importOmf({
|
|
123
|
+
read: (path) => backend.read(path),
|
|
124
|
+
write: (path, content) => write(path, content),
|
|
125
|
+
delete: (path) => remove(path),
|
|
126
|
+
exists: (path) => backend.exists(path),
|
|
127
|
+
search: (query) => backend.search(query),
|
|
128
|
+
ls: (dirPath) => backend.ls(dirPath),
|
|
129
|
+
getTree: () => backend.getTree(),
|
|
130
|
+
rebuildTree: () => rebuildTree(),
|
|
131
|
+
exportAll: () => backend.exportAll(),
|
|
132
|
+
clear: () => backend.clear(),
|
|
133
|
+
}, doc, options);
|
|
134
|
+
},
|
|
135
|
+
compact: () => compactor.compactAll(),
|
|
136
|
+
storage: {
|
|
137
|
+
read: (path) => backend.read(path),
|
|
138
|
+
resolvePath: (path) => backend.resolvePath ? backend.resolvePath(path) : Promise.resolve(null),
|
|
139
|
+
write: (path, content) => write(path, content),
|
|
140
|
+
delete: (path) => remove(path),
|
|
141
|
+
exists: (path) => backend.exists(path),
|
|
142
|
+
search: (query) => backend.search(query),
|
|
143
|
+
ls: (dirPath) => backend.ls(dirPath),
|
|
144
|
+
getTree: () => backend.getTree(),
|
|
145
|
+
rebuildTree: () => rebuildTree(),
|
|
146
|
+
exportAll: () => backend.exportAll(),
|
|
147
|
+
clear: () => backend.clear()
|
|
148
|
+
},
|
|
149
|
+
serialize: async () => serialize(await backend.exportAll()),
|
|
150
|
+
toZip: async () => toZip(await backend.exportAll()),
|
|
151
|
+
_backend: backend,
|
|
152
|
+
_bulletIndex: bulletIndex
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function createBrowserLlmClient(llmConfig = /** @type {MemoryBankLLMConfig} */ ({ apiKey: '' })) {
|
|
157
|
+
const { apiKey, baseUrl, headers, provider } = llmConfig;
|
|
158
|
+
if (!apiKey) {
|
|
159
|
+
throw new Error('createMemoryBank: config.llm.apiKey is required (or provide config.llmClient)');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const detectedProvider = provider || detectProvider(baseUrl);
|
|
163
|
+
if (detectedProvider === 'anthropic') {
|
|
164
|
+
return createAnthropicClient({ apiKey, baseUrl, headers });
|
|
165
|
+
}
|
|
166
|
+
if (detectedProvider === 'tinfoil') {
|
|
167
|
+
throw new Error(
|
|
168
|
+
'createMemoryBank(browser): Tinfoil provider requires the Node.js entry (src/index.js). ' +
|
|
169
|
+
'Use provider "openai" with baseUrl "https://inference.tinfoil.sh/v1" for browser builds.'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return createOpenAIClient({ apiKey, baseUrl, headers });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function detectProvider(baseUrl) {
|
|
176
|
+
if (!baseUrl) return 'openai';
|
|
177
|
+
const lower = baseUrl.toLowerCase();
|
|
178
|
+
if (lower.includes('anthropic.com')) return 'anthropic';
|
|
179
|
+
if (lower.includes('tinfoil.sh')) return 'tinfoil';
|
|
180
|
+
return 'openai';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createBrowserBackend(storage) {
|
|
184
|
+
if (storage && typeof storage === 'object' && typeof storage.read === 'function') {
|
|
185
|
+
return storage;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const storageType = typeof storage === 'string' ? storage : 'ram';
|
|
189
|
+
switch (storageType) {
|
|
190
|
+
case 'indexeddb':
|
|
191
|
+
return asyncBackend(() => import('./backends/indexeddb.js').then((module) => new module.IndexedDBStorage()));
|
|
192
|
+
case 'filesystem':
|
|
193
|
+
throw new Error('createMemoryBank(browser): filesystem storage is not available in the browser entrypoint.');
|
|
194
|
+
case 'ram':
|
|
195
|
+
default:
|
|
196
|
+
return new InMemoryStorage();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function asyncBackend(loader) {
|
|
201
|
+
let backend = null;
|
|
202
|
+
let loading = null;
|
|
203
|
+
|
|
204
|
+
async function resolve() {
|
|
205
|
+
if (backend) return backend;
|
|
206
|
+
if (!loading) {
|
|
207
|
+
loading = loader().then((instance) => {
|
|
208
|
+
backend = instance;
|
|
209
|
+
return backend;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return loading;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const methods = ['init', 'read', 'resolvePath', 'write', 'delete', 'exists', 'ls', 'search', 'getTree', 'rebuildTree', 'exportAll', 'clear'];
|
|
216
|
+
const proxy = {};
|
|
217
|
+
for (const method of methods) {
|
|
218
|
+
proxy[method] = async (...args) => {
|
|
219
|
+
const resolved = await resolve();
|
|
220
|
+
return resolved[method](...args);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return /** @type {StorageBackend} */ (proxy);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export * from './bullets/index.js';
|
|
227
|
+
export { buildOmfExport, previewOmfImport, importOmf, parseOmfText, validateOmf } from './omf.js';
|
package/src/cli/auth.js
CHANGED
|
@@ -61,7 +61,7 @@ export async function loginInteractive() {
|
|
|
61
61
|
process.stderr.write('\n');
|
|
62
62
|
process.stderr.write(` ${c.bold}${c.cyan}Login${c.reset}\n`);
|
|
63
63
|
process.stderr.write('\n');
|
|
64
|
-
process.stderr.write(` ${c.white}
|
|
64
|
+
process.stderr.write(` ${c.white}nanomem uses an LLM provider for extraction and retrieval.${c.reset}\n`);
|
|
65
65
|
process.stderr.write(` ${c.white}Select your provider, model, and paste your API key to get started.${c.reset}\n`);
|
|
66
66
|
process.stderr.write('\n');
|
|
67
67
|
|