@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.
- package/LICENSE +21 -0
- package/README.md +64 -18
- 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/bullets/parser.js +8 -9
- package/src/cli/auth.js +1 -1
- package/src/cli/commands.js +58 -9
- 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 +6 -3
- package/src/engine/compactor.js +3 -6
- package/src/engine/deleter.js +187 -0
- package/src/engine/executors.js +474 -11
- package/src/engine/ingester.js +98 -63
- package/src/engine/recentConversation.js +110 -0
- package/src/engine/retriever.js +243 -37
- package/src/engine/toolLoop.js +51 -9
- package/src/imports/chatgpt.js +1 -1
- package/src/imports/claude.js +85 -0
- package/src/imports/importData.js +462 -0
- package/src/imports/index.js +10 -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 +111 -12
- package/src/prompt_sets/document/ingestion.js +98 -4
- package/src/prompt_sets/index.js +12 -4
- package/src/types.js +135 -4
- 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 +56 -2
- package/types/engine/recentConversation.d.ts +18 -0
- package/types/engine/retriever.d.ts +22 -9
- package/types/imports/claude.d.ts +14 -0
- package/types/imports/importData.d.ts +29 -0
- package/types/imports/index.d.ts +2 -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 +127 -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
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @openanonymity/nanomem
|
|
2
2
|
|
|
3
|
+
```
|
|
4
|
+
__ __ ______ __ __ ______ __ __ ______ __ __
|
|
5
|
+
/\ "-.\ \ /\ __ \ /\ "-.\ \ /\ __ \ /\ "-./ \ /\ ___\ /\ "-./ \
|
|
6
|
+
\ \ \-. \ \ \ __ \ \ \ \-. \ \ \ \/\ \ \ \ \-./\ \ \ \ __\ \ \ \-./\ \
|
|
7
|
+
\ \_\\"\_\ \ \_\ \_\ \ \_\\"\_\ \ \_____\ \ \_\ \ \_\ \ \_____\ \ \_\ \ \_\
|
|
8
|
+
\/_/ \/_/ \/_/\/_/ \/_/ \/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/ \/_/
|
|
9
|
+
```
|
|
10
|
+
|
|
3
11
|
**Personal memory you own, in files you can actually read.**
|
|
4
12
|
|
|
5
13
|
`nanomem` turns chats, notes, and exports into a markdown memory system that an LLM can update and retrieve as facts evolve over time. The result stays inspectable, portable, and user-owned instead of disappearing into hidden vector state.
|
|
@@ -23,7 +31,8 @@ Retrieval is only one part of memory. `nanomem` is built for the maintenance lay
|
|
|
23
31
|
- **Evolving memory state.** Keep facts current as they change over time instead of treating memory as an append-only log.
|
|
24
32
|
- **Compaction and cleanup.** Collapse repeated signals into stable knowledge and move stale memory into history.
|
|
25
33
|
- **Conflict-aware updates.** Resolve outdated or contradictory facts using recency, source, and confidence.
|
|
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.
|
|
34
|
+
- **Import your existing history.** Start from ChatGPT exports, Claude exports, [OA Chat](https://chat.openanonymity.ai) exports, transcripts, message arrays, markdown notes, or whole markdown directories.
|
|
35
|
+
- **Portable memory exchange.** Export full memory state as plain text, ZIP, or Open Memory Format (OMF), and merge OMF documents back in programmatically.
|
|
27
36
|
- **Flexible storage.** Run on local files, IndexedDB, in-memory storage, or a custom backend.
|
|
28
37
|
- **Built to plug in.** Use it from the CLI, as a library, or as a memory layer for other agents.
|
|
29
38
|
|
|
@@ -32,7 +41,7 @@ Retrieval is only one part of memory. `nanomem` is built for the maintenance lay
|
|
|
32
41
|
Install:
|
|
33
42
|
|
|
34
43
|
```bash
|
|
35
|
-
npm
|
|
44
|
+
npm i @openanonymity/nanomem
|
|
36
45
|
```
|
|
37
46
|
|
|
38
47
|
Set up once:
|
|
@@ -43,6 +52,13 @@ nanomem login
|
|
|
43
52
|
|
|
44
53
|
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
54
|
|
|
55
|
+
Add facts directly:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
nanomem add "I moved to Seattle and started a new job at Acme."
|
|
59
|
+
nanomem update "Actually I moved to Portland, not Seattle."
|
|
60
|
+
```
|
|
61
|
+
|
|
46
62
|
Import history or notes:
|
|
47
63
|
|
|
48
64
|
```bash
|
|
@@ -58,6 +74,13 @@ nanomem retrieve "what are my hobbies?"
|
|
|
58
74
|
nanomem retrieve "what are my hobbies?" --render
|
|
59
75
|
```
|
|
60
76
|
|
|
77
|
+
Delete facts from memory:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
nanomem delete "I have a dog named Mochi"
|
|
81
|
+
nanomem delete "I have a dog named Mochi" --deep
|
|
82
|
+
```
|
|
83
|
+
|
|
61
84
|
Compact and clean up memory:
|
|
62
85
|
|
|
63
86
|
```bash
|
|
@@ -73,6 +96,17 @@ nanomem login --provider anthropic --api-key sk-ant-... --model claude-sonnet-4-
|
|
|
73
96
|
|
|
74
97
|
Supported providers include OpenAI, Anthropic, Tinfoil, OpenRouter, and OpenAI-compatible endpoints via `--base-url`.
|
|
75
98
|
|
|
99
|
+
When `provider` is `tinfoil`, nanomem now uses the Tinfoil SDK and fails
|
|
100
|
+
closed on enclave attestation verification before any inference request is
|
|
101
|
+
sent. Browser consumers load a vendored SDK bundle, construct `TinfoilAI`,
|
|
102
|
+
and require `await client.getVerificationDocument()` to report
|
|
103
|
+
`securityVerified === true` before inference. The vendored bundle lives at
|
|
104
|
+
`src/vendor/tinfoil.browser.js`; refresh it after SDK upgrades with:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run vendor:tinfoil
|
|
108
|
+
```
|
|
109
|
+
|
|
76
110
|
## How it works
|
|
77
111
|
|
|
78
112
|
```text
|
|
@@ -89,7 +123,9 @@ conversation / notes / exports
|
|
|
89
123
|
| memory retrieve
|
|
90
124
|
| file selection + bullet-level scoring
|
|
91
125
|
v
|
|
92
|
-
|
|
126
|
+
prompt crafting / retrieval
|
|
127
|
+
retrieve -> augment_query(user_query, memory_files)
|
|
128
|
+
-> minimized reviewable prompt
|
|
93
129
|
|
|
|
94
130
|
v
|
|
95
131
|
memory compact
|
|
@@ -109,16 +145,13 @@ Memory is stored as markdown with structured metadata:
|
|
|
109
145
|
```md
|
|
110
146
|
# Memory: Work
|
|
111
147
|
|
|
112
|
-
## Working
|
|
113
|
-
### Current context
|
|
148
|
+
## Working memory (current context subject to change)
|
|
114
149
|
- Preparing for a product launch next month | topic=work | tier=working | status=active | source=user_statement | confidence=high | updated_at=2026-04-07 | review_at=2026-04-20
|
|
115
150
|
|
|
116
|
-
## Long-
|
|
117
|
-
### Stable facts
|
|
151
|
+
## Long-term memory (stable facts that are unlikely to change)
|
|
118
152
|
- Leads the backend team at Acme | topic=work | tier=long_term | status=active | source=user_statement | confidence=high | updated_at=2026-04-07
|
|
119
153
|
|
|
120
|
-
## History
|
|
121
|
-
### No longer current
|
|
154
|
+
## History (no longer current)
|
|
122
155
|
- Previously lived in New York | topic=personal | tier=history | status=superseded | source=user_statement | confidence=high | updated_at=2024-06-01
|
|
123
156
|
```
|
|
124
157
|
|
|
@@ -132,7 +165,7 @@ import { createMemoryBank } from '@openanonymity/nanomem';
|
|
|
132
165
|
const memory = createMemoryBank({
|
|
133
166
|
llm: { apiKey: 'sk-...', model: 'gpt-5.4-mini' },
|
|
134
167
|
storage: 'filesystem',
|
|
135
|
-
storagePath: '
|
|
168
|
+
storagePath: '~/nanomem'
|
|
136
169
|
});
|
|
137
170
|
|
|
138
171
|
await memory.init();
|
|
@@ -144,17 +177,25 @@ await memory.ingest([
|
|
|
144
177
|
|
|
145
178
|
const result = await memory.retrieve('Where do I live now?');
|
|
146
179
|
await memory.compact();
|
|
180
|
+
|
|
181
|
+
const omf = await memory.exportOmf();
|
|
182
|
+
const preview = await memory.previewOmfImport(omf);
|
|
183
|
+
await memory.importOmf(omf);
|
|
147
184
|
```
|
|
148
185
|
|
|
149
186
|
## Common commands
|
|
150
187
|
|
|
151
188
|
```bash
|
|
152
|
-
nanomem
|
|
153
|
-
nanomem
|
|
154
|
-
nanomem
|
|
155
|
-
nanomem
|
|
156
|
-
nanomem
|
|
157
|
-
nanomem
|
|
189
|
+
nanomem add <text> # add new facts
|
|
190
|
+
nanomem update <text> # correct existing facts
|
|
191
|
+
nanomem delete <query> # delete facts matching a query
|
|
192
|
+
nanomem delete <query> --deep # delete across all files (thorough)
|
|
193
|
+
nanomem import <file|dir|-> # import history or notes
|
|
194
|
+
nanomem retrieve <query> [--context <file>] # retrieve relevant context
|
|
195
|
+
nanomem tree # browse memory files
|
|
196
|
+
nanomem compact # deduplicate and archive
|
|
197
|
+
nanomem export --format zip # export everything
|
|
198
|
+
nanomem status # show config and stats
|
|
158
199
|
```
|
|
159
200
|
|
|
160
201
|
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.
|
|
@@ -163,7 +204,8 @@ For terminal use, `--render` will format markdown-heavy output like `read` and `
|
|
|
163
204
|
|
|
164
205
|
`nanomem import` supports:
|
|
165
206
|
|
|
166
|
-
- ChatGPT exports
|
|
207
|
+
- ChatGPT exports (`conversations.json` from "Export data")
|
|
208
|
+
- Claude exports (`conversations.json` from "Export data")
|
|
167
209
|
- [OA Chat](https://chat.openanonymity.ai) exports
|
|
168
210
|
- markdown notes
|
|
169
211
|
- recursive markdown directory imports
|
|
@@ -173,7 +215,9 @@ For terminal use, `--render` will format markdown-heavy output like `read` and `
|
|
|
173
215
|
Import can operate in both conversation-oriented and document-oriented modes, depending on the source or explicit flags.
|
|
174
216
|
|
|
175
217
|
```bash
|
|
176
|
-
nanomem import conversations.json #
|
|
218
|
+
nanomem import conversations.json # auto-detects ChatGPT or Claude format
|
|
219
|
+
nanomem import conversations.json --format claude # explicit Claude format
|
|
220
|
+
nanomem import conversations.json --format chatgpt # explicit ChatGPT format
|
|
177
221
|
nanomem import ./notes/ # document mode (auto for directories)
|
|
178
222
|
nanomem import my-notes.md --format markdown # document mode (explicit)
|
|
179
223
|
```
|
|
@@ -189,6 +233,8 @@ nanomem import my-notes.md --format markdown # document mode (explicit)
|
|
|
189
233
|
|
|
190
234
|
Internals: [docs/memory-system.md](./docs/memory-system.md)
|
|
191
235
|
|
|
236
|
+
OMF spec: [docs/omf.md](./docs/omf.md)
|
|
237
|
+
|
|
192
238
|
## License
|
|
193
239
|
|
|
194
240
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openanonymity/nanomem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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 };
|