@openanonymity/nanomem 0.1.1 → 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/README.md +18 -10
- package/package.json +1 -1
- package/src/bullets/parser.js +8 -9
- package/src/cli/commands.js +7 -1
- package/src/cli.js +1 -1
- package/src/engine/compactor.js +3 -6
- package/src/engine/executors.js +58 -7
- package/src/engine/ingester.js +29 -16
- package/src/engine/retriever.js +6 -2
- package/src/imports/chatgpt.js +1 -1
- package/src/imports/claude.js +85 -0
- package/src/imports/importData.js +9 -1
- package/src/imports/index.js +5 -0
- package/src/prompt_sets/conversation/ingestion.js +16 -7
- package/src/prompt_sets/document/ingestion.js +10 -4
- package/src/types.js +2 -1
- package/types/engine/executors.d.ts +2 -2
- package/types/imports/claude.d.ts +14 -0
- package/types/imports/index.d.ts +1 -0
- package/types/prompt_sets/conversation/ingestion.d.ts +2 -2
- package/types/prompt_sets/document/ingestion.d.ts +1 -1
- package/types/types.d.ts +2 -0
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,7 @@ 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.
|
|
27
35
|
- **Portable memory exchange.** Export full memory state as plain text, ZIP, or Open Memory Format (OMF), and merge OMF documents back in programmatically.
|
|
28
36
|
- **Flexible storage.** Run on local files, IndexedDB, in-memory storage, or a custom backend.
|
|
29
37
|
- **Built to plug in.** Use it from the CLI, as a library, or as a memory layer for other agents.
|
|
@@ -137,16 +145,13 @@ Memory is stored as markdown with structured metadata:
|
|
|
137
145
|
```md
|
|
138
146
|
# Memory: Work
|
|
139
147
|
|
|
140
|
-
## Working
|
|
141
|
-
### Current context
|
|
148
|
+
## Working memory (current context subject to change)
|
|
142
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
|
|
143
150
|
|
|
144
|
-
## Long-
|
|
145
|
-
### Stable facts
|
|
151
|
+
## Long-term memory (stable facts that are unlikely to change)
|
|
146
152
|
- Leads the backend team at Acme | topic=work | tier=long_term | status=active | source=user_statement | confidence=high | updated_at=2026-04-07
|
|
147
153
|
|
|
148
|
-
## History
|
|
149
|
-
### No longer current
|
|
154
|
+
## History (no longer current)
|
|
150
155
|
- Previously lived in New York | topic=personal | tier=history | status=superseded | source=user_statement | confidence=high | updated_at=2024-06-01
|
|
151
156
|
```
|
|
152
157
|
|
|
@@ -160,7 +165,7 @@ import { createMemoryBank } from '@openanonymity/nanomem';
|
|
|
160
165
|
const memory = createMemoryBank({
|
|
161
166
|
llm: { apiKey: 'sk-...', model: 'gpt-5.4-mini' },
|
|
162
167
|
storage: 'filesystem',
|
|
163
|
-
storagePath: '
|
|
168
|
+
storagePath: '~/nanomem'
|
|
164
169
|
});
|
|
165
170
|
|
|
166
171
|
await memory.init();
|
|
@@ -199,7 +204,8 @@ For terminal use, `--render` will format markdown-heavy output like `read` and `
|
|
|
199
204
|
|
|
200
205
|
`nanomem import` supports:
|
|
201
206
|
|
|
202
|
-
- ChatGPT exports
|
|
207
|
+
- ChatGPT exports (`conversations.json` from "Export data")
|
|
208
|
+
- Claude exports (`conversations.json` from "Export data")
|
|
203
209
|
- [OA Chat](https://chat.openanonymity.ai) exports
|
|
204
210
|
- markdown notes
|
|
205
211
|
- recursive markdown directory imports
|
|
@@ -209,7 +215,9 @@ For terminal use, `--render` will format markdown-heavy output like `read` and `
|
|
|
209
215
|
Import can operate in both conversation-oriented and document-oriented modes, depending on the source or explicit flags.
|
|
210
216
|
|
|
211
217
|
```bash
|
|
212
|
-
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
|
|
213
221
|
nanomem import ./notes/ # document mode (auto for directories)
|
|
214
222
|
nanomem import my-notes.md --format markdown # document mode (explicit)
|
|
215
223
|
```
|
package/package.json
CHANGED
package/src/bullets/parser.js
CHANGED
|
@@ -33,11 +33,11 @@ export function parseBullets(content) {
|
|
|
33
33
|
const headingMatch = line.match(HEADING_REGEX);
|
|
34
34
|
if (headingMatch) {
|
|
35
35
|
currentHeading = headingMatch[1].trim() || currentHeading;
|
|
36
|
-
if (/^
|
|
36
|
+
if (/^working/i.test(currentHeading)) {
|
|
37
37
|
section = 'working';
|
|
38
|
-
} else if (/^(long[- ]?term|active)
|
|
38
|
+
} else if (/^(long[- ]?term|active)/i.test(currentHeading)) {
|
|
39
39
|
section = 'long_term';
|
|
40
|
-
} else if (/^(history|archive)
|
|
40
|
+
} else if (/^(history|archive)/i.test(currentHeading)) {
|
|
41
41
|
section = 'history';
|
|
42
42
|
}
|
|
43
43
|
continue;
|
|
@@ -120,7 +120,7 @@ export function extractTitles(content) {
|
|
|
120
120
|
|
|
121
121
|
const title = headingMatch[1].trim();
|
|
122
122
|
if (!title) continue;
|
|
123
|
-
if (/^(working|long[- ]?term|history|active|archive
|
|
123
|
+
if (/^(working|long[- ]?term|history|active|archive)/i.test(title)) continue;
|
|
124
124
|
titles.push(title);
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -160,9 +160,8 @@ function inferDocumentTopic(bullets, fallback = 'general') {
|
|
|
160
160
|
return firstTopic || fallback;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function renderSection(lines, title,
|
|
163
|
+
function renderSection(lines, title, bullets, forceHistory = false) {
|
|
164
164
|
lines.push(`## ${title}`);
|
|
165
|
-
lines.push(`### ${subsectionTitle}`);
|
|
166
165
|
|
|
167
166
|
if (!bullets || bullets.length === 0) {
|
|
168
167
|
lines.push('_No entries yet._');
|
|
@@ -189,11 +188,11 @@ export function renderCompactedDocument(working, longTerm, history, options = {}
|
|
|
189
188
|
const docTopic = normalizeTopic(options.titleTopic || inferDocumentTopic([...working, ...longTerm, ...history], 'general'));
|
|
190
189
|
lines.push(`# Memory: ${topicHeading(docTopic)}`);
|
|
191
190
|
lines.push('');
|
|
192
|
-
renderSection(lines, 'Working
|
|
191
|
+
renderSection(lines, 'Working memory (current context subject to change)', working);
|
|
193
192
|
lines.push('');
|
|
194
|
-
renderSection(lines, 'Long-
|
|
193
|
+
renderSection(lines, 'Long-term memory (stable facts that are unlikely to change)', longTerm);
|
|
195
194
|
lines.push('');
|
|
196
|
-
renderSection(lines, 'History
|
|
195
|
+
renderSection(lines, 'History (no longer current)', history, true);
|
|
197
196
|
|
|
198
197
|
return lines.join('\n').trim();
|
|
199
198
|
}
|
package/src/cli/commands.js
CHANGED
|
@@ -8,6 +8,7 @@ import { serialize, toZip } from '../utils/portability.js';
|
|
|
8
8
|
import { safeDateIso } from '../bullets/normalize.js';
|
|
9
9
|
import { extractSessionsFromOAFastchatExport } from '../imports/oaFastchat.js';
|
|
10
10
|
import { isChatGptExport, parseChatGptExport } from '../imports/chatgpt.js';
|
|
11
|
+
import { isClaudeExport, parseClaudeExport } from '../imports/claude.js';
|
|
11
12
|
import { parseMarkdownFiles } from '../imports/markdown.js';
|
|
12
13
|
import { loginInteractive } from './auth.js';
|
|
13
14
|
import { writeConfigFile, CONFIG_PATH } from './config.js';
|
|
@@ -77,6 +78,11 @@ function parseConversations(input, flags) {
|
|
|
77
78
|
return parseChatGptExport(parsed);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// Claude export (conversations.json)
|
|
82
|
+
if (isClaudeExport(parsed)) {
|
|
83
|
+
return parseClaudeExport(parsed);
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
// Plain messages array
|
|
81
87
|
if (Array.isArray(parsed)) {
|
|
82
88
|
return [{ title: null, messages: parsed }];
|
|
@@ -129,7 +135,7 @@ export async function retrieve(positionals, flags, mem) {
|
|
|
129
135
|
|
|
130
136
|
const result = await mem.retrieve(query, conversationText);
|
|
131
137
|
if (!result || !result.assembledContext) {
|
|
132
|
-
return
|
|
138
|
+
return 'No relevant context found.';
|
|
133
139
|
}
|
|
134
140
|
return result;
|
|
135
141
|
}
|
package/src/cli.js
CHANGED
|
@@ -104,11 +104,11 @@ async function main() {
|
|
|
104
104
|
const TOOL_LABELS = {
|
|
105
105
|
create_new_file: 'creating file',
|
|
106
106
|
append_memory: 'appending',
|
|
107
|
-
update_memory: 'updating',
|
|
108
107
|
archive_memory: 'archiving',
|
|
109
108
|
delete_memory: 'cleaning up',
|
|
110
109
|
read_file: 'reading',
|
|
111
110
|
list_files: 'scanning',
|
|
111
|
+
update_bullets: 'updating',
|
|
112
112
|
delete_bullet: 'deleting',
|
|
113
113
|
};
|
|
114
114
|
memOpts.onToolCall = (name) => {
|
package/src/engine/compactor.js
CHANGED
|
@@ -33,16 +33,13 @@ Input is one memory file. Rewrite it into:
|
|
|
33
33
|
|
|
34
34
|
# Memory: <Topic>
|
|
35
35
|
|
|
36
|
-
## Working
|
|
37
|
-
### <Topic>
|
|
36
|
+
## Working memory (current context subject to change)
|
|
38
37
|
- fact | topic=<topic> | tier=working | status=active | source=user_statement|assistant_summary|inference|system | confidence=high|medium|low | updated_at=YYYY-MM-DD | review_at=YYYY-MM-DD(optional) | expires_at=YYYY-MM-DD(optional)
|
|
39
38
|
|
|
40
|
-
## Long-
|
|
41
|
-
### <Topic>
|
|
39
|
+
## Long-term memory (stable facts that are unlikely to change)
|
|
42
40
|
- fact | topic=<topic> | tier=long_term | status=active | source=user_statement|assistant_summary|inference|system | confidence=high|medium|low | updated_at=YYYY-MM-DD | expires_at=YYYY-MM-DD(optional)
|
|
43
41
|
|
|
44
|
-
## History
|
|
45
|
-
### <Topic>
|
|
42
|
+
## History (no longer current)
|
|
46
43
|
- fact | topic=<topic> | tier=history | status=superseded|expired|uncertain | source=user_statement|assistant_summary|inference|system | confidence=high|medium|low | updated_at=YYYY-MM-DD | expires_at=YYYY-MM-DD(optional)
|
|
47
44
|
|
|
48
45
|
Rules:
|
package/src/engine/executors.js
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
/** @import { ChatCompletionResponse, ExtractionExecutorHooks, LLMClient, StorageBackend, ToolDefinition } from '../types.js' */
|
|
13
13
|
import {
|
|
14
14
|
compactBullets,
|
|
15
|
+
ensureBulletMetadata,
|
|
15
16
|
inferTopicFromPath,
|
|
16
17
|
normalizeFactText,
|
|
17
18
|
parseBullets,
|
|
18
19
|
renderCompactedDocument,
|
|
20
|
+
todayIsoDate,
|
|
19
21
|
} from '../bullets/index.js';
|
|
20
22
|
import { trimRecentConversation } from './recentConversation.js';
|
|
21
23
|
|
|
@@ -396,7 +398,7 @@ export function createAugmentQueryExecutor({ backend, llmClient, model, query, c
|
|
|
396
398
|
* @param {ExtractionExecutorHooks} [hooks]
|
|
397
399
|
*/
|
|
398
400
|
export function createExtractionExecutors(backend, hooks = {}) {
|
|
399
|
-
const { normalizeContent, mergeWithExisting, refreshIndex, onWrite } = hooks;
|
|
401
|
+
const { normalizeContent, mergeWithExisting, refreshIndex, onWrite, updatedAt } = hooks;
|
|
400
402
|
|
|
401
403
|
return {
|
|
402
404
|
read_file: async ({ path }) => {
|
|
@@ -406,7 +408,7 @@ export function createExtractionExecutors(backend, hooks = {}) {
|
|
|
406
408
|
},
|
|
407
409
|
create_new_file: async ({ path, content }) => {
|
|
408
410
|
const exists = await backend.exists(path);
|
|
409
|
-
if (exists) return JSON.stringify({ error: `File already exists: ${path}. Use append_memory or
|
|
411
|
+
if (exists) return JSON.stringify({ error: `File already exists: ${path}. Use append_memory or update_bullets instead.` });
|
|
410
412
|
const normalized = normalizeContent ? normalizeContent(content, path) : content;
|
|
411
413
|
await backend.write(path, normalized);
|
|
412
414
|
if (refreshIndex) await refreshIndex(path);
|
|
@@ -423,13 +425,62 @@ export function createExtractionExecutors(backend, hooks = {}) {
|
|
|
423
425
|
onWrite?.(path, existing ?? '', newContent);
|
|
424
426
|
return JSON.stringify({ success: true, path, action: 'appended' });
|
|
425
427
|
},
|
|
426
|
-
|
|
428
|
+
update_bullets: async ({ path, updates }) => {
|
|
427
429
|
const before = await backend.read(path);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
+
if (!before) return JSON.stringify({ error: `File not found: ${path}` });
|
|
431
|
+
if (!Array.isArray(updates) || updates.length === 0) return JSON.stringify({ error: 'updates must be a non-empty array' });
|
|
432
|
+
|
|
433
|
+
const parsed = parseBullets(before);
|
|
434
|
+
const defaultTopic = inferTopicFromPath(path);
|
|
435
|
+
const effectiveUpdatedAt = updatedAt || todayIsoDate();
|
|
436
|
+
let matchedCount = 0;
|
|
437
|
+
const errors = [];
|
|
438
|
+
|
|
439
|
+
for (const { old_fact, new_fact } of updates) {
|
|
440
|
+
const factText = typeof old_fact === 'string' && old_fact.includes('|')
|
|
441
|
+
? old_fact.split('|')[0].trim()
|
|
442
|
+
: String(old_fact || '').trim();
|
|
443
|
+
const target = normalizeFactText(factText);
|
|
444
|
+
if (!target) { errors.push('empty old_fact'); continue; }
|
|
445
|
+
|
|
446
|
+
const idx = parsed.findIndex((b) => normalizeFactText(b.text) === target);
|
|
447
|
+
if (idx === -1) { errors.push(`No match: ${factText}`); continue; }
|
|
448
|
+
|
|
449
|
+
// Supersede the old bullet and push a new active replacement.
|
|
450
|
+
// Strip any metadata the LLM may have included in new_fact.
|
|
451
|
+
const oldBullet = parsed[idx];
|
|
452
|
+
const rawNewFact = String(new_fact || '').trim();
|
|
453
|
+
const cleanNewFact = rawNewFact.includes('|')
|
|
454
|
+
? rawNewFact.split('|')[0].trim()
|
|
455
|
+
: rawNewFact;
|
|
456
|
+
parsed[idx] = { ...oldBullet, status: 'superseded', tier: 'history' };
|
|
457
|
+
parsed.push(ensureBulletMetadata(
|
|
458
|
+
{
|
|
459
|
+
text: cleanNewFact,
|
|
460
|
+
topic: oldBullet.topic,
|
|
461
|
+
source: oldBullet.source,
|
|
462
|
+
confidence: oldBullet.confidence,
|
|
463
|
+
},
|
|
464
|
+
{ defaultTopic, updatedAt: effectiveUpdatedAt }
|
|
465
|
+
));
|
|
466
|
+
matchedCount++;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (matchedCount === 0) {
|
|
470
|
+
return JSON.stringify({ error: errors.join('; ') || 'No bullets matched' });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const compacted = compactBullets(parsed, { defaultTopic, maxActivePerTopic: 1000 });
|
|
474
|
+
const after = renderCompactedDocument(
|
|
475
|
+
compacted.working, compacted.longTerm, compacted.history,
|
|
476
|
+
{ titleTopic: defaultTopic }
|
|
477
|
+
);
|
|
478
|
+
await backend.write(path, after);
|
|
430
479
|
if (refreshIndex) await refreshIndex(path);
|
|
431
|
-
onWrite?.(path, before
|
|
432
|
-
|
|
480
|
+
onWrite?.(path, before, after);
|
|
481
|
+
const result = { success: true, path, action: 'bullets_updated', updated: matchedCount };
|
|
482
|
+
if (errors.length) result.errors = errors;
|
|
483
|
+
return JSON.stringify(result);
|
|
433
484
|
},
|
|
434
485
|
archive_memory: async ({ path, item_text }) => {
|
|
435
486
|
const existing = await backend.read(path);
|
package/src/engine/ingester.js
CHANGED
|
@@ -70,35 +70,46 @@ const T_APPEND_MEMORY = {
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
/** @type {ToolDefinition} */
|
|
73
|
-
const
|
|
73
|
+
const T_UPDATE_BULLETS = {
|
|
74
74
|
type: 'function',
|
|
75
75
|
function: {
|
|
76
|
-
name: '
|
|
77
|
-
description: '
|
|
76
|
+
name: 'update_bullets',
|
|
77
|
+
description: 'Replace one or more bullet facts in an existing memory file in a single call. Each entry requires the exact existing fact text and its corrected replacement. Only matched bullets are changed — the rest of the file is untouched.',
|
|
78
78
|
parameters: {
|
|
79
79
|
type: 'object',
|
|
80
80
|
properties: {
|
|
81
|
-
path: { type: 'string', description: 'File path to update' },
|
|
82
|
-
|
|
81
|
+
path: { type: 'string', description: 'File path containing the bullets to update' },
|
|
82
|
+
updates: {
|
|
83
|
+
type: 'array',
|
|
84
|
+
description: 'List of bullet updates to apply',
|
|
85
|
+
items: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
old_fact: { type: 'string', description: 'Exact fact text of the bullet to replace (pipe-delimited metadata is fine)' },
|
|
89
|
+
new_fact: { type: 'string', description: 'Corrected fact text (plain text only, no metadata)' }
|
|
90
|
+
},
|
|
91
|
+
required: ['old_fact', 'new_fact']
|
|
92
|
+
}
|
|
93
|
+
}
|
|
83
94
|
},
|
|
84
|
-
required: ['path', '
|
|
95
|
+
required: ['path', 'updates']
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
};
|
|
88
99
|
|
|
89
100
|
/**
|
|
90
101
|
* Tool sets per ingestion mode.
|
|
91
|
-
* `add` — can only write new content
|
|
102
|
+
* `add` — can only write new content.
|
|
92
103
|
* `update` — can only edit existing files (no create/append).
|
|
93
104
|
* Others — full access.
|
|
94
105
|
* @type {Record<string, ToolDefinition[]>}
|
|
95
106
|
*/
|
|
96
107
|
const TOOLS_BY_MODE = {
|
|
97
|
-
add:
|
|
98
|
-
update:
|
|
108
|
+
add: [T_READ_FILE, T_CREATE_NEW_FILE, T_APPEND_MEMORY],
|
|
109
|
+
update: [T_READ_FILE, T_UPDATE_BULLETS, T_APPEND_MEMORY, T_CREATE_NEW_FILE],
|
|
99
110
|
};
|
|
100
111
|
|
|
101
|
-
const EXTRACTION_TOOLS = [T_READ_FILE, T_CREATE_NEW_FILE, T_APPEND_MEMORY,
|
|
112
|
+
const EXTRACTION_TOOLS = [T_READ_FILE, T_CREATE_NEW_FILE, T_APPEND_MEMORY, T_UPDATE_BULLETS];
|
|
102
113
|
|
|
103
114
|
class MemoryIngester {
|
|
104
115
|
constructor({ backend, bulletIndex, llmClient, model, onToolCall }) {
|
|
@@ -140,11 +151,13 @@ class MemoryIngester {
|
|
|
140
151
|
mergeWithExisting: (existing, incoming, path) => this._mergeWithExisting(existing, incoming, path, updatedAt, isDocument),
|
|
141
152
|
refreshIndex: (path) => this._bulletIndex.refreshPath(path),
|
|
142
153
|
onWrite: (path, before, after) => writes.push({ path, before, after }),
|
|
154
|
+
updatedAt,
|
|
143
155
|
});
|
|
144
156
|
|
|
157
|
+
const dateNote = `\nFrom ${updatedAt}. Use this date when writing date references in facts.\n`;
|
|
145
158
|
const userMessage = isDocument
|
|
146
|
-
?
|
|
147
|
-
:
|
|
159
|
+
? `${dateNote}Document content:\n\`\`\`\n${conversationText}\n\`\`\``
|
|
160
|
+
: `${dateNote}Conversation:\n\`\`\`\n${conversationText}\n\`\`\``;
|
|
148
161
|
|
|
149
162
|
const tools = TOOLS_BY_MODE[mode] || EXTRACTION_TOOLS;
|
|
150
163
|
|
|
@@ -172,7 +185,7 @@ class MemoryIngester {
|
|
|
172
185
|
return { status: 'error', writeCalls: 0, error: message };
|
|
173
186
|
}
|
|
174
187
|
|
|
175
|
-
const writeTools = ['create_new_file', 'append_memory', '
|
|
188
|
+
const writeTools = ['create_new_file', 'append_memory', 'update_bullets', 'archive_memory', 'delete_memory'];
|
|
176
189
|
const writeCalls = toolCallLog.filter(e => writeTools.includes(e.name));
|
|
177
190
|
|
|
178
191
|
return { status: 'processed', writeCalls: writeCalls.length, writes };
|
|
@@ -207,9 +220,9 @@ class MemoryIngester {
|
|
|
207
220
|
|
|
208
221
|
const defaultTopic = inferTopicFromPath(path);
|
|
209
222
|
const normalized = incomingBullets.map((bullet) => {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
const b = ensureBulletMetadata(bullet, { defaultTopic, updatedAt });
|
|
223
|
+
// Clear the LLM-written date so the conversation-level updatedAt
|
|
224
|
+
// is used as the fallback (falls back to today when not provided).
|
|
225
|
+
const b = ensureBulletMetadata({ ...bullet, updatedAt: null }, { defaultTopic, updatedAt });
|
|
213
226
|
if (isDocument && b.source === 'user_statement') b.source = 'document';
|
|
214
227
|
return b;
|
|
215
228
|
});
|
package/src/engine/retriever.js
CHANGED
|
@@ -274,7 +274,7 @@ class MemoryRetriever {
|
|
|
274
274
|
{ role: 'user', content: userContent }
|
|
275
275
|
],
|
|
276
276
|
terminalTool: isAugmentMode ? 'augment_query' : 'assemble_context',
|
|
277
|
-
maxIterations: isAugmentMode ? 12 :
|
|
277
|
+
maxIterations: isAugmentMode ? 12 : 10,
|
|
278
278
|
maxOutputTokens: 4000,
|
|
279
279
|
temperature: 0,
|
|
280
280
|
executeTerminalTool: isAugmentMode,
|
|
@@ -377,11 +377,15 @@ class MemoryRetriever {
|
|
|
377
377
|
const files = await collectReadFiles(toolCallLog, this._backend);
|
|
378
378
|
const paths = files.map(f => f.path);
|
|
379
379
|
|
|
380
|
+
const terminalWasCalled = terminalToolResult != null;
|
|
380
381
|
const assembledContext = terminalToolResult?.arguments?.content || null;
|
|
381
382
|
|
|
383
|
+
// LLM explicitly said nothing relevant — respect that, don't fall back to snippet context.
|
|
384
|
+
if (terminalWasCalled && !assembledContext) return null;
|
|
385
|
+
|
|
382
386
|
if (files.length === 0 && !assembledContext) return null;
|
|
383
387
|
|
|
384
|
-
const snippetContext = await this._buildSnippetContext(paths, query, conversationText);
|
|
388
|
+
const snippetContext = terminalWasCalled ? null : await this._buildSnippetContext(paths, query, conversationText);
|
|
385
389
|
|
|
386
390
|
onProgress?.({
|
|
387
391
|
stage: 'complete',
|
package/src/imports/chatgpt.js
CHANGED
|
@@ -68,7 +68,7 @@ function normalizeChatGptConversation(conversation) {
|
|
|
68
68
|
|
|
69
69
|
const title = (conversation?.title || '').trim() || null;
|
|
70
70
|
const updatedAt = conversation?.update_time
|
|
71
|
-
? safeDateIso(
|
|
71
|
+
? safeDateIso(conversation.update_time * 1000)
|
|
72
72
|
: null;
|
|
73
73
|
return { title, messages, updatedAt };
|
|
74
74
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude export parser.
|
|
3
|
+
*
|
|
4
|
+
* Handles conversations.json from Claude's data export.
|
|
5
|
+
* Format: array of conversation objects, each with a flat `chat_messages` array.
|
|
6
|
+
* Messages have a `content` array of typed blocks (text, tool_use, tool_result, token_budget).
|
|
7
|
+
*/
|
|
8
|
+
/** @import { ChatGptSession, Message } from '../types.js' */
|
|
9
|
+
import { safeDateIso } from '../bullets/normalize.js';
|
|
10
|
+
|
|
11
|
+
const SKIP_CONTENT_TYPES = new Set([
|
|
12
|
+
'tool_use',
|
|
13
|
+
'tool_result',
|
|
14
|
+
'token_budget',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect whether parsed JSON is a Claude export.
|
|
19
|
+
* Claude exports are arrays of objects with `chat_messages` and `uuid`.
|
|
20
|
+
* @param {unknown} parsed
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function isClaudeExport(parsed) {
|
|
24
|
+
if (!Array.isArray(parsed)) return false;
|
|
25
|
+
if (parsed.length === 0) return false;
|
|
26
|
+
const first = parsed[0];
|
|
27
|
+
return first && typeof first === 'object' && 'chat_messages' in first && 'uuid' in first;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a Claude export into normalized sessions.
|
|
32
|
+
* @param {unknown[]} conversations — the parsed JSON array
|
|
33
|
+
* @returns {ChatGptSession[]}
|
|
34
|
+
*/
|
|
35
|
+
export function parseClaudeExport(conversations) {
|
|
36
|
+
if (!Array.isArray(conversations)) {
|
|
37
|
+
throw new Error('Claude export should be an array of conversations.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return conversations
|
|
41
|
+
.map(normalizeClaudeConversation)
|
|
42
|
+
.filter(session => session.messages.length > 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @returns {ChatGptSession} */
|
|
46
|
+
function normalizeClaudeConversation(conversation) {
|
|
47
|
+
const chatMessages = conversation?.chat_messages || [];
|
|
48
|
+
/** @type {Message[]} */
|
|
49
|
+
const messages = [];
|
|
50
|
+
|
|
51
|
+
for (const msg of chatMessages) {
|
|
52
|
+
const sender = msg?.sender;
|
|
53
|
+
if (sender !== 'human' && sender !== 'assistant') continue;
|
|
54
|
+
|
|
55
|
+
/** @type {'user' | 'assistant'} */
|
|
56
|
+
const role = sender === 'human' ? 'user' : 'assistant';
|
|
57
|
+
const text = extractText(msg?.content);
|
|
58
|
+
|
|
59
|
+
if (!text.trim()) continue;
|
|
60
|
+
|
|
61
|
+
messages.push({ role, content: text });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const title = (conversation?.name || '').trim() || null;
|
|
65
|
+
const updatedAt = conversation?.updated_at
|
|
66
|
+
? safeDateIso(conversation.updated_at)
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
return { title, messages, updatedAt };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractText(contentBlocks) {
|
|
73
|
+
if (!Array.isArray(contentBlocks)) return '';
|
|
74
|
+
|
|
75
|
+
const parts = [];
|
|
76
|
+
for (const block of contentBlocks) {
|
|
77
|
+
if (!block || typeof block !== 'object') continue;
|
|
78
|
+
if (SKIP_CONTENT_TYPES.has(block.type)) continue;
|
|
79
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
80
|
+
parts.push(block.text);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return parts.join('\n').trim();
|
|
85
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { safeDateIso } from '../bullets/normalize.js';
|
|
4
4
|
import { extractSessionsFromOAFastchatExport } from './oaFastchat.js';
|
|
5
5
|
import { isChatGptExport, parseChatGptExport } from './chatgpt.js';
|
|
6
|
+
import { isClaudeExport, parseClaudeExport } from './claude.js';
|
|
6
7
|
import { parseMarkdownFiles } from './markdown.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -173,6 +174,13 @@ export function parseImportInput(input, options = {}) {
|
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
if (requestedFormat === 'claude' || (requestedFormat === 'auto' && isClaudeExport(parsedJson))) {
|
|
178
|
+
return {
|
|
179
|
+
items: normalizeSessions(parseClaudeExport(parsedJson), 'conversation'),
|
|
180
|
+
mode: options.mode || 'conversation'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
176
184
|
if (requestedFormat === 'messages' || (requestedFormat === 'auto' && Array.isArray(parsedJson) && parsedJson.every(isMessageLike))) {
|
|
177
185
|
return {
|
|
178
186
|
items: normalizeSessions([{
|
|
@@ -229,7 +237,7 @@ function normalizeUpdatedAt(value) {
|
|
|
229
237
|
/**
|
|
230
238
|
* @param {string | undefined} format
|
|
231
239
|
* @param {string} sourceName
|
|
232
|
-
* @returns {'auto' | 'normalized' | 'oa-fastchat' | 'chatgpt' | 'messages' | 'transcript' | 'markdown'}
|
|
240
|
+
* @returns {'auto' | 'normalized' | 'oa-fastchat' | 'chatgpt' | 'claude' | 'messages' | 'transcript' | 'markdown'}
|
|
233
241
|
*/
|
|
234
242
|
function normalizeRequestedFormat(format, sourceName) {
|
|
235
243
|
const normalized = String(format || '').trim().toLowerCase();
|
package/src/imports/index.js
CHANGED
|
@@ -28,9 +28,9 @@ Bullet format: "- Fact text | topic=topic-name | source=user_statement | confide
|
|
|
28
28
|
|
|
29
29
|
If nothing new is worth saving, stop without calling any tools.`;
|
|
30
30
|
|
|
31
|
-
export const updatePrompt = `You are a memory manager.
|
|
31
|
+
export const updatePrompt = `You are a memory manager. Update the user's memory based on the text below.
|
|
32
32
|
|
|
33
|
-
CRITICAL: Only
|
|
33
|
+
CRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate.
|
|
34
34
|
|
|
35
35
|
Current memory index:
|
|
36
36
|
\`\`\`
|
|
@@ -38,11 +38,20 @@ Current memory index:
|
|
|
38
38
|
\`\`\`
|
|
39
39
|
|
|
40
40
|
Steps:
|
|
41
|
-
1. Identify which existing file(s) hold facts that are
|
|
42
|
-
2. Use read_file to read the current content.
|
|
43
|
-
3.
|
|
41
|
+
1. Identify which existing file(s) might hold facts that are stale or contradicted by the new information.
|
|
42
|
+
2. Use read_file to read the current content and find the exact bullet text to replace.
|
|
43
|
+
3. If a matching old fact exists, use update_bullets with all corrections for that file in a single call, passing the exact old fact text and the corrected fact text for each.
|
|
44
|
+
4. If no existing fact matches — the information is entirely new — use append_memory to add it to an existing file that covers the same domain, or create_new_file if no existing file is thematically close.
|
|
45
|
+
|
|
46
|
+
Rules:
|
|
47
|
+
- Prefer update_bullets when an existing fact is directly contradicted or corrected.
|
|
48
|
+
- Only change bullets that are directly contradicted or corrected by the new information.
|
|
49
|
+
- Do not touch any other bullets in the file.
|
|
50
|
+
- Pass old_fact exactly as it appears in the file (including pipe-delimited metadata is fine).
|
|
51
|
+
- Pass new_fact as plain text only — no metadata.
|
|
52
|
+
- When appending or creating, use this bullet format: "- Fact text | topic=topic-name | source=user_statement | confidence=high | updated_at=YYYY-MM-DD"
|
|
44
53
|
|
|
45
|
-
If
|
|
54
|
+
If nothing new or changed is worth saving, stop without calling any tools.`;
|
|
46
55
|
|
|
47
56
|
export const ingestionPrompt = `You are a memory manager. After reading a conversation, decide if any concrete, reusable facts should be saved to the user's memory files.
|
|
48
57
|
|
|
@@ -85,7 +94,7 @@ Rules:
|
|
|
85
94
|
- Favor broad thematic files. A file can hold multiple related sub-topics — only truly unrelated facts need separate files.
|
|
86
95
|
- Only create a new file when nothing in the index is thematically close. When in doubt, append.
|
|
87
96
|
- When creating a new file, choose a broad, thematic name that can absorb future related facts — not a narrow label for a single detail.
|
|
88
|
-
- Use
|
|
97
|
+
- Use update_bullets only if a fact is now stale or contradicted. Pass all corrections for a file in one call.
|
|
89
98
|
- When a new explicit user statement contradicts an older one on the same topic, prefer the newer statement. If a user statement conflicts with an inference, the user statement always wins.
|
|
90
99
|
- If a conflict is ambiguous, preserve both versions rather than deleting one.
|
|
91
100
|
- Do not skip obvious facts just because the schema supports extra metadata.
|
|
@@ -30,7 +30,7 @@ If nothing new is worth saving, stop without calling any tools.`;
|
|
|
30
30
|
|
|
31
31
|
export const updatePrompt = `You are a memory manager. Correct or update facts already saved in memory based on the document below.
|
|
32
32
|
|
|
33
|
-
CRITICAL: Only edit files that already exist. Do NOT create new files.
|
|
33
|
+
CRITICAL: Only edit files that already exist. Do NOT create new files. Do NOT rewrite whole files.
|
|
34
34
|
|
|
35
35
|
Current memory index:
|
|
36
36
|
\`\`\`
|
|
@@ -39,10 +39,16 @@ Current memory index:
|
|
|
39
39
|
|
|
40
40
|
Steps:
|
|
41
41
|
1. Identify which existing file(s) hold facts that are now stale or contradicted by the document.
|
|
42
|
-
2. Use read_file to read the current content.
|
|
43
|
-
3. Use
|
|
42
|
+
2. Use read_file to read the current content and find the exact bullet text to replace.
|
|
43
|
+
3. Use update_bullets with all corrections for that file in a single call, passing the exact old fact text and the corrected fact text for each.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Rules:
|
|
46
|
+
- Only change bullets that are directly contradicted or corrected by the new information.
|
|
47
|
+
- Do not touch any other bullets in the file.
|
|
48
|
+
- Pass old_fact exactly as it appears in the file (including pipe-delimited metadata is fine).
|
|
49
|
+
- Pass new_fact as plain text only — no metadata.
|
|
50
|
+
|
|
51
|
+
If nothing needs updating, stop without calling any tools.`;
|
|
46
52
|
|
|
47
53
|
export const ingestionPrompt = `You are a memory manager. You are reading documents (notes, README files, code repositories, articles) and extracting facts about the subject into a structured memory bank.
|
|
48
54
|
|
package/src/types.js
CHANGED
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
/**
|
|
121
121
|
* @typedef {object} ToolFunctionParameters
|
|
122
122
|
* @property {'object'} type
|
|
123
|
-
* @property {Record<string, { type: string; description?: string }>} properties
|
|
123
|
+
* @property {Record<string, { type: string; description?: string; items?: object }>} properties
|
|
124
124
|
* @property {string[]} required
|
|
125
125
|
*/
|
|
126
126
|
|
|
@@ -287,6 +287,7 @@
|
|
|
287
287
|
* @property {(existing: string | null, incoming: string, path: string) => string} [mergeWithExisting]
|
|
288
288
|
* @property {(path: string) => Promise<void>} [refreshIndex]
|
|
289
289
|
* @property {(path: string, before: string, after: string) => void} [onWrite]
|
|
290
|
+
* @property {string} [updatedAt]
|
|
290
291
|
*/
|
|
291
292
|
|
|
292
293
|
/**
|
|
@@ -60,9 +60,9 @@ export function createExtractionExecutors(backend: StorageBackend, hooks?: Extra
|
|
|
60
60
|
path: any;
|
|
61
61
|
content: any;
|
|
62
62
|
}) => Promise<string>;
|
|
63
|
-
|
|
63
|
+
update_bullets: ({ path, updates }: {
|
|
64
64
|
path: any;
|
|
65
|
-
|
|
65
|
+
updates: any;
|
|
66
66
|
}) => Promise<string>;
|
|
67
67
|
archive_memory: ({ path, item_text }: {
|
|
68
68
|
path: any;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect whether parsed JSON is a Claude export.
|
|
3
|
+
* Claude exports are arrays of objects with `chat_messages` and `uuid`.
|
|
4
|
+
* @param {unknown} parsed
|
|
5
|
+
* @returns {boolean}
|
|
6
|
+
*/
|
|
7
|
+
export function isClaudeExport(parsed: unknown): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Parse a Claude export into normalized sessions.
|
|
10
|
+
* @param {unknown[]} conversations — the parsed JSON array
|
|
11
|
+
* @returns {ChatGptSession[]}
|
|
12
|
+
*/
|
|
13
|
+
export function parseClaudeExport(conversations: unknown[]): ChatGptSession[];
|
|
14
|
+
import type { ChatGptSession } from '../types.js';
|
package/types/imports/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { parseMarkdownFiles } from "./markdown.js";
|
|
2
2
|
export { extractSessionsFromOAFastchatExport, extractConversationFromOAFastchatExport, listOAFastchatSessions } from "./oaFastchat.js";
|
|
3
3
|
export { isChatGptExport, parseChatGptExport } from "./chatgpt.js";
|
|
4
|
+
export { isClaudeExport, parseClaudeExport } from "./claude.js";
|
|
4
5
|
export { importData, parseImportInput } from "./importData.js";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* updatePrompt — `nanomem update`: only edit EXISTING facts (no new files).
|
|
7
7
|
*/
|
|
8
8
|
export const addPrompt: "You are a memory manager. Save NEW facts from the text that do not yet exist in memory.\n\nCRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate.\n\nCurrent memory index:\n```\n{INDEX}\n```\n\nFor each new fact, decide:\n- Use append_memory if an existing file already covers the same domain or topic.\n- Use create_new_file only if no existing file is thematically close.\n\nDo NOT save:\n- Facts already present in memory\n- Transient details (greetings, one-off questions with no lasting answer)\n- Sensitive secrets (passwords, tokens, keys)\n\nBullet format: \"- Fact text | topic=topic-name | source=user_statement | confidence=high | updated_at=YYYY-MM-DD\"\n\nIf nothing new is worth saving, stop without calling any tools.";
|
|
9
|
-
export const updatePrompt: "You are a memory manager.
|
|
10
|
-
export const ingestionPrompt: "You are a memory manager. After reading a conversation, decide if any concrete, reusable facts should be saved to the user's memory files.\n\nCRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate information.\n\nSave information that is likely to help in a future conversation. Be selective \u2014 only save durable facts, not transient conversation details.\n\nDo NOT save:\n- Anything the user did not explicitly say (no inferences, no extrapolations, no \"likely\" facts)\n- Information already present in existing files\n- Transient details (greetings, \"help me with this\", \"thanks\", questions without lasting answers)\n- The assistant's own reasoning, suggestions, or knowledge \u2014 only what the user stated\n- Sensitive secrets (passwords, auth tokens, private keys, full payment data, government IDs)\n- Opinions the assistant expressed unless the user explicitly agreed with them\n\nCurrent memory index:\n```\n{INDEX}\n```\n\n**Key principle: Prefer fewer, broader files over many narrow ones.** Organize files into folders by domain (e.g. health/, work/, personal/). Within each folder, group related facts into the same file rather than splitting every sub-topic into its own file. Before creating a new file, check whether an existing file in the same domain could absorb the facts. A single file with many bullets on related sub-topics is better than many files with one or two bullets each.\n\nInstructions:\n1. Read the conversation below and identify facts the user explicitly stated.\n2. Do not read files before writing. The memory index is sufficient to decide where to append. Only read a file if the index entry is ambiguous and you need the exact current content to avoid duplicating a fact.\n3. If no relevant file exists yet, create_new_file directly.\n4. Default to append_memory when an existing file covers the same domain or a closely related topic. Only use create_new_file when no existing file is thematically close.\n5. Use this bullet format: \"- Fact text | topic=topic-name | source=SOURCE | confidence=LEVEL | updated_at=YYYY-MM-DD\"\n6. Source values:\n - source=user_statement \u2014 the user directly said this. This is the PRIMARY source. Use it for the vast majority of saved facts.\n - source=llm_infer \u2014 use ONLY when combining multiple explicit user statements into an obvious conclusion (e.g. user said \"I work at Acme\" and \"Acme is in SF\" \u2192 \"Works in SF\"). Never use this to guess, extrapolate, or fill in gaps. When in doubt, do not save.\n7. Confidence: high for direct user statements, medium for llm_infer. Never save low-confidence items.\n8. You may optionally add tier=working for clearly short-term or in-progress context. If you are unsure, omit tier and just save the fact.\n9. Facts worth saving: allergies, health conditions, location, job/role, tech stack, pets, family members, durable preferences, and active plans \u2014 but ONLY if the user explicitly mentioned them.\n10. If a fact is time-sensitive, include date context in the text. You may optionally add review_at or expires_at.\n11. If nothing new is worth remembering, simply stop without calling any write tools. Saving nothing is better than saving something wrong.\n\nRules:\n- Write facts in a timeless, archival format: use absolute dates (YYYY-MM-DD) rather than relative terms like \"recently\", \"currently\", \"just\", or \"last week\". A fact must be interpretable correctly even years after it was written.\n- Favor broad thematic files. A file can hold multiple related sub-topics \u2014 only truly unrelated facts need separate files.\n- Only create a new file when nothing in the index is thematically close. When in doubt, append.\n- When creating a new file, choose a broad, thematic name that can absorb future related facts \u2014 not a narrow label for a single detail.\n- Use
|
|
9
|
+
export const updatePrompt: "You are a memory manager. Update the user's memory based on the text below.\n\nCRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate.\n\nCurrent memory index:\n```\n{INDEX}\n```\n\nSteps:\n1. Identify which existing file(s) might hold facts that are stale or contradicted by the new information.\n2. Use read_file to read the current content and find the exact bullet text to replace.\n3. If a matching old fact exists, use update_bullets with all corrections for that file in a single call, passing the exact old fact text and the corrected fact text for each.\n4. If no existing fact matches \u2014 the information is entirely new \u2014 use append_memory to add it to an existing file that covers the same domain, or create_new_file if no existing file is thematically close.\n\nRules:\n- Prefer update_bullets when an existing fact is directly contradicted or corrected.\n- Only change bullets that are directly contradicted or corrected by the new information.\n- Do not touch any other bullets in the file.\n- Pass old_fact exactly as it appears in the file (including pipe-delimited metadata is fine).\n- Pass new_fact as plain text only \u2014 no metadata.\n- When appending or creating, use this bullet format: \"- Fact text | topic=topic-name | source=user_statement | confidence=high | updated_at=YYYY-MM-DD\"\n\nIf nothing new or changed is worth saving, stop without calling any tools.";
|
|
10
|
+
export const ingestionPrompt: "You are a memory manager. After reading a conversation, decide if any concrete, reusable facts should be saved to the user's memory files.\n\nCRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate information.\n\nSave information that is likely to help in a future conversation. Be selective \u2014 only save durable facts, not transient conversation details.\n\nDo NOT save:\n- Anything the user did not explicitly say (no inferences, no extrapolations, no \"likely\" facts)\n- Information already present in existing files\n- Transient details (greetings, \"help me with this\", \"thanks\", questions without lasting answers)\n- The assistant's own reasoning, suggestions, or knowledge \u2014 only what the user stated\n- Sensitive secrets (passwords, auth tokens, private keys, full payment data, government IDs)\n- Opinions the assistant expressed unless the user explicitly agreed with them\n\nCurrent memory index:\n```\n{INDEX}\n```\n\n**Key principle: Prefer fewer, broader files over many narrow ones.** Organize files into folders by domain (e.g. health/, work/, personal/). Within each folder, group related facts into the same file rather than splitting every sub-topic into its own file. Before creating a new file, check whether an existing file in the same domain could absorb the facts. A single file with many bullets on related sub-topics is better than many files with one or two bullets each.\n\nInstructions:\n1. Read the conversation below and identify facts the user explicitly stated.\n2. Do not read files before writing. The memory index is sufficient to decide where to append. Only read a file if the index entry is ambiguous and you need the exact current content to avoid duplicating a fact.\n3. If no relevant file exists yet, create_new_file directly.\n4. Default to append_memory when an existing file covers the same domain or a closely related topic. Only use create_new_file when no existing file is thematically close.\n5. Use this bullet format: \"- Fact text | topic=topic-name | source=SOURCE | confidence=LEVEL | updated_at=YYYY-MM-DD\"\n6. Source values:\n - source=user_statement \u2014 the user directly said this. This is the PRIMARY source. Use it for the vast majority of saved facts.\n - source=llm_infer \u2014 use ONLY when combining multiple explicit user statements into an obvious conclusion (e.g. user said \"I work at Acme\" and \"Acme is in SF\" \u2192 \"Works in SF\"). Never use this to guess, extrapolate, or fill in gaps. When in doubt, do not save.\n7. Confidence: high for direct user statements, medium for llm_infer. Never save low-confidence items.\n8. You may optionally add tier=working for clearly short-term or in-progress context. If you are unsure, omit tier and just save the fact.\n9. Facts worth saving: allergies, health conditions, location, job/role, tech stack, pets, family members, durable preferences, and active plans \u2014 but ONLY if the user explicitly mentioned them.\n10. If a fact is time-sensitive, include date context in the text. You may optionally add review_at or expires_at.\n11. If nothing new is worth remembering, simply stop without calling any write tools. Saving nothing is better than saving something wrong.\n\nRules:\n- Write facts in a timeless, archival format: use absolute dates (YYYY-MM-DD) rather than relative terms like \"recently\", \"currently\", \"just\", or \"last week\". A fact must be interpretable correctly even years after it was written.\n- Favor broad thematic files. A file can hold multiple related sub-topics \u2014 only truly unrelated facts need separate files.\n- Only create a new file when nothing in the index is thematically close. When in doubt, append.\n- When creating a new file, choose a broad, thematic name that can absorb future related facts \u2014 not a narrow label for a single detail.\n- Use update_bullets only if a fact is now stale or contradicted. Pass all corrections for a file in one call.\n- When a new explicit user statement contradicts an older one on the same topic, prefer the newer statement. If a user statement conflicts with an inference, the user statement always wins.\n- If a conflict is ambiguous, preserve both versions rather than deleting one.\n- Do not skip obvious facts just because the schema supports extra metadata.\n- Content should be raw facts only \u2014 no filler commentary.";
|
|
11
11
|
export const deletePrompt: "You are a memory manager performing a TARGETED deletion.\n\nThe user wants to remove: \"{QUERY}\"\n\nRULES \u2014 read carefully before acting:\n1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.\n - If the query names a specific entity (a pet, person, place, project), delete every fact about that entity \u2014 not just the one line that introduces it.\n - Example: \"I have dog mochi\" \u2192 delete ALL facts about Mochi (habits, toys, traits, the introduction line, etc.).\n2. Do NOT delete facts about unrelated subjects, even if they appear in the same file.\n3. When genuinely unsure whether a bullet is about the target subject, SKIP it.\n4. Never delete an entire file \u2014 only individual bullets via delete_bullet.\n5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.\n\nCurrent memory index:\n```\n{INDEX}\n```\n\nSteps:\n1. Identify which file(s) likely contain the content to delete from the index above.\n2. Use retrieve_file or list_directory if the relevant file is not obvious from the index.\n3. Use read_file to read the identified file(s).\n4. Call delete_bullet for each bullet that is about the subject(s) in the deletion request.\n5. If nothing matches, stop without calling delete_bullet.";
|
|
12
12
|
export const deepDeletePrompt: "You are a memory manager performing a COMPREHENSIVE deletion across ALL memory files.\n\nThe user wants to remove: \"{QUERY}\"\n\nRULES \u2014 read carefully before acting:\n1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.\n - If the query names a specific entity (a pet, person, place, project), delete every fact about that entity across every file \u2014 not just the one line that introduces it.\n - Example: \"I have dog mochi\" \u2192 delete ALL facts about Mochi wherever they appear.\n2. Do NOT delete facts about unrelated subjects.\n3. When genuinely unsure whether a bullet is about the target subject, SKIP it.\n4. Never delete an entire file \u2014 only individual bullets via delete_bullet.\n5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.\n\nYou MUST read every file listed below and check it for matching content.\n\nFiles to check:\n{FILE_LIST}\n\nSteps:\n1. Use read_file to read each file listed above, one by one.\n2. For each file, call delete_bullet for any bullet that is about the subject(s) in the deletion request.\n3. Continue until every file has been checked.\n4. If a file has no matching bullets, move on without calling delete_bullet for it.";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* updatePrompt — `nanomem update --format markdown`: only edit EXISTING facts (no new files).
|
|
7
7
|
*/
|
|
8
8
|
export const addPrompt: "You are a memory manager. Extract NEW facts from the document that do not yet exist in memory.\n\nYou may extract and reasonably infer facts from what the document shows \u2014 not just word-for-word statements. Use good judgment: extract what is clearly supported by the content, avoid speculation.\n\nCurrent memory index:\n```\n{INDEX}\n```\n\nFor each new fact, decide:\n- Use append_memory if an existing file already covers the same domain or topic.\n- Use create_new_file only if no existing file is thematically close.\n\nDo NOT save:\n- Facts already present in memory\n- Boilerplate (installation steps, license text, generic disclaimers)\n- Sensitive secrets (passwords, tokens, keys)\n\nBullet format: \"- Fact text | topic=topic-name | source=document | confidence=high | updated_at=YYYY-MM-DD\"\n\nIf nothing new is worth saving, stop without calling any tools.";
|
|
9
|
-
export const updatePrompt: "You are a memory manager. Correct or update facts already saved in memory based on the document below.\n\nCRITICAL: Only edit files that already exist. Do NOT create new files.\n\nCurrent memory index:\n```\n{INDEX}\n```\n\nSteps:\n1. Identify which existing file(s) hold facts that are now stale or contradicted by the document.\n2. Use read_file to read the current content.\n3. Use
|
|
9
|
+
export const updatePrompt: "You are a memory manager. Correct or update facts already saved in memory based on the document below.\n\nCRITICAL: Only edit files that already exist. Do NOT create new files. Do NOT rewrite whole files.\n\nCurrent memory index:\n```\n{INDEX}\n```\n\nSteps:\n1. Identify which existing file(s) hold facts that are now stale or contradicted by the document.\n2. Use read_file to read the current content and find the exact bullet text to replace.\n3. Use update_bullets with all corrections for that file in a single call, passing the exact old fact text and the corrected fact text for each.\n\nRules:\n- Only change bullets that are directly contradicted or corrected by the new information.\n- Do not touch any other bullets in the file.\n- Pass old_fact exactly as it appears in the file (including pipe-delimited metadata is fine).\n- Pass new_fact as plain text only \u2014 no metadata.\n\nIf nothing needs updating, stop without calling any tools.";
|
|
10
10
|
export const ingestionPrompt: "You are a memory manager. You are reading documents (notes, README files, code repositories, articles) and extracting facts about the subject into a structured memory bank.\n\nUnlike conversation ingestion, you may extract and reasonably infer facts from what the documents show \u2014 not just what was explicitly stated word-for-word. Use good judgment: extract what is clearly supported by the content, avoid speculation.\n\nSave information that would be useful when answering questions about this subject in the future. Be generous \u2014 capture expertise, projects, preferences, philosophy, and patterns that emerge from the documents.\n\nDo NOT save:\n- Speculation or guesses not supported by the content\n- Boilerplate (installation steps, license text, generic disclaimers)\n- Information already present in existing files\n- Sensitive secrets (passwords, auth tokens, private keys)\n\nCurrent memory index:\n```\n{INDEX}\n```\n\n**Key principle: Create a NEW file for each distinct topic.** Organize into domain folders (e.g. projects/, expertise/, education/, philosophy/) with topic-specific files within them.\n\nInstructions:\n1. Read the document content and identify concrete, reusable facts about the subject.\n2. Do not read files before writing. The memory index is sufficient to decide where to append or create. Only read a file if the index entry is ambiguous and you need the exact current content to avoid duplicating a fact.\n3. Use create_new_file for new topics, append_memory to add to existing files.\n4. Use this bullet format: \"- Fact text | topic=topic-name | source=SOURCE | confidence=LEVEL | updated_at=YYYY-MM-DD\"\n5. Source values (IMPORTANT \u2014 never use source=user_statement here):\n - source=document \u2014 the fact is directly stated or clearly shown in the document. Use for the majority of facts.\n - source=document_infer \u2014 a reasonable inference from what multiple parts of the document collectively show (e.g. a repo with only C files and a README praising simplicity \u2192 \"prefers low-level, minimal implementations\"). Use sparingly.\n6. Confidence: high for source=document facts, medium for source=document_infer.\n7. Facts worth extracting: skills and expertise, projects built, stated opinions and philosophy, tools and languages used, patterns across work, goals and motivations, background and experience.\n8. If nothing meaningful can be extracted from a document, stop without calling any write tools.\n\nRules:\n- Write facts in a timeless, archival format: use absolute dates (YYYY-MM-DD) rather than relative terms like \"recently\", \"currently\", \"just\", or \"last week\". A fact must be interpretable correctly even years after it was written.\n- One file per distinct topic. Do NOT put unrelated facts in the same file.\n- Create new files freely \u2014 focused files are better than bloated ones.\n- Content should be raw facts only \u2014 no filler commentary.";
|
|
11
11
|
export const deletePrompt: "You are a memory manager performing a TARGETED deletion.\n\nThe user wants to remove: \"{QUERY}\"\n\nRULES \u2014 read carefully before acting:\n1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.\n - If the query names a specific entity (a person, project, tool, concept), delete every fact about that entity \u2014 not just the one line that introduces it.\n - Example: \"project recipe-app\" \u2192 delete ALL facts about the recipe-app project (tech stack, goals, status, etc.).\n2. Do NOT delete facts about unrelated subjects, even if they appear in the same file.\n3. When genuinely unsure whether a bullet is about the target subject, SKIP it.\n4. Never delete an entire file \u2014 only individual bullets via delete_bullet.\n5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.\n\nCurrent memory index:\n```\n{INDEX}\n```\n\nSteps:\n1. Identify which file(s) likely contain the content to delete from the index above.\n2. Use retrieve_file or list_directory if the relevant file is not obvious from the index.\n3. Use read_file to read the identified file(s).\n4. Call delete_bullet for each bullet that is about the subject(s) in the deletion request.\n5. If nothing matches, stop without calling delete_bullet.";
|
|
12
12
|
export const deepDeletePrompt: "You are a memory manager performing a COMPREHENSIVE deletion across ALL memory files.\n\nThe user wants to remove: \"{QUERY}\"\n\nRULES \u2014 read carefully before acting:\n1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.\n - If the query names a specific entity (a person, project, tool, concept), delete every fact about that entity wherever it appears.\n - Example: \"project recipe-app\" \u2192 delete ALL facts about the recipe-app project across every file.\n2. Do NOT delete facts about unrelated subjects.\n3. When genuinely unsure whether a bullet is about the target subject, SKIP it.\n4. Never delete an entire file \u2014 only individual bullets via delete_bullet.\n5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.\n\nYou MUST read every file listed below and check it for matching content.\n\nFiles to check:\n{FILE_LIST}\n\nSteps:\n1. Use read_file to read each file listed above, one by one.\n2. For each file, call delete_bullet for any bullet that is about the subject(s) in the deletion request.\n3. Continue until every file has been checked.\n4. If a file has no matching bullets, move on without calling delete_bullet for it.";
|
package/types/types.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export type ToolFunctionParameters = {
|
|
|
77
77
|
properties: Record<string, {
|
|
78
78
|
type: string;
|
|
79
79
|
description?: string;
|
|
80
|
+
items?: object;
|
|
80
81
|
}>;
|
|
81
82
|
required: string[];
|
|
82
83
|
};
|
|
@@ -226,6 +227,7 @@ export type ExtractionExecutorHooks = {
|
|
|
226
227
|
mergeWithExisting?: ((existing: string | null, incoming: string, path: string) => string) | undefined;
|
|
227
228
|
refreshIndex?: ((path: string) => Promise<void>) | undefined;
|
|
228
229
|
onWrite?: ((path: string, before: string, after: string) => void) | undefined;
|
|
230
|
+
updatedAt?: string | undefined;
|
|
229
231
|
};
|
|
230
232
|
export type ToolExecutor = (args: any) => Promise<string>;
|
|
231
233
|
export type StorageBackend = {
|