@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/src/omf.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/** @import { ExportRecord, OmfDocument, OmfImportOptions, OmfImportPreview, OmfImportResult, StorageFacade } from './types.js' */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseBullets,
|
|
5
|
+
inferTopicFromPath,
|
|
6
|
+
isExpiredBullet,
|
|
7
|
+
todayIsoDate,
|
|
8
|
+
ensureBulletMetadata,
|
|
9
|
+
compactBullets,
|
|
10
|
+
renderCompactedDocument,
|
|
11
|
+
normalizeFactText,
|
|
12
|
+
} from './bullets/index.js';
|
|
13
|
+
|
|
14
|
+
const SUPPORTED_OMF_VERSIONS = ['1.0'];
|
|
15
|
+
const DEFAULT_SOURCE_APP = 'nanomem';
|
|
16
|
+
|
|
17
|
+
function categoryFromPath(filePath) {
|
|
18
|
+
if (!filePath) return null;
|
|
19
|
+
let category = filePath.replace(/\.md$/i, '');
|
|
20
|
+
category = category.replace(/\/about$/, '');
|
|
21
|
+
return category || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function targetPathForItem(item) {
|
|
25
|
+
const appExtensions = item.extensions?.['oa-chat'] || item.extensions?.nanomem || null;
|
|
26
|
+
if (appExtensions?.file_path) {
|
|
27
|
+
return appExtensions.file_path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (item.category) {
|
|
31
|
+
const category = String(item.category).replace(/^\/+|\/+$/g, '').trim();
|
|
32
|
+
if (category) {
|
|
33
|
+
if (category.includes('/')) {
|
|
34
|
+
return `${category}.md`;
|
|
35
|
+
}
|
|
36
|
+
return `${category}/about.md`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return 'personal/imported.md';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isDocumentItem(item) {
|
|
44
|
+
const extensions = item.extensions?.['oa-chat'] || item.extensions?.nanomem || null;
|
|
45
|
+
if (extensions?.document) return true;
|
|
46
|
+
return item.content.length > 500 && item.content.includes('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function statusToSection(status) {
|
|
50
|
+
if (status === 'archived' || status === 'expired') return 'history';
|
|
51
|
+
return 'long_term';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} doc
|
|
56
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
57
|
+
*/
|
|
58
|
+
export function validateOmf(doc) {
|
|
59
|
+
if (!doc || typeof doc !== 'object') {
|
|
60
|
+
return { valid: false, error: 'Not a valid JSON object' };
|
|
61
|
+
}
|
|
62
|
+
const d = /** @type {any} */ (doc);
|
|
63
|
+
if (!d.omf) {
|
|
64
|
+
return { valid: false, error: 'Missing "omf" version field. Is this an OMF file?' };
|
|
65
|
+
}
|
|
66
|
+
if (!SUPPORTED_OMF_VERSIONS.includes(String(d.omf))) {
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
error: `Unsupported OMF version "${d.omf}". Supported: ${SUPPORTED_OMF_VERSIONS.join(', ')}`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(d.memories)) {
|
|
73
|
+
return { valid: false, error: 'Missing or invalid "memories" array' };
|
|
74
|
+
}
|
|
75
|
+
for (let index = 0; index < d.memories.length; index += 1) {
|
|
76
|
+
const memory = d.memories[index];
|
|
77
|
+
if (!memory || typeof memory !== 'object') {
|
|
78
|
+
return { valid: false, error: `Memory item at index ${index} is not an object` };
|
|
79
|
+
}
|
|
80
|
+
if (!memory.content || typeof memory.content !== 'string' || !memory.content.trim()) {
|
|
81
|
+
return { valid: false, error: `Memory item at index ${index} has empty or missing "content"` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { valid: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} text
|
|
89
|
+
* @returns {OmfDocument}
|
|
90
|
+
*/
|
|
91
|
+
export function parseOmfText(text) {
|
|
92
|
+
return JSON.parse(text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {StorageFacade} storage
|
|
97
|
+
* @param {{ sourceApp?: string }} [options]
|
|
98
|
+
* @returns {Promise<OmfDocument>}
|
|
99
|
+
*/
|
|
100
|
+
export async function buildOmfExport(storage, options = {}) {
|
|
101
|
+
const allFiles = await storage.exportAll();
|
|
102
|
+
const today = todayIsoDate();
|
|
103
|
+
const memories = [];
|
|
104
|
+
|
|
105
|
+
for (const file of allFiles) {
|
|
106
|
+
if (file.path.endsWith('_tree.md')) continue;
|
|
107
|
+
if (!file.content?.trim()) continue;
|
|
108
|
+
|
|
109
|
+
const category = categoryFromPath(file.path);
|
|
110
|
+
const bullets = parseBullets(file.content);
|
|
111
|
+
|
|
112
|
+
if (bullets.length > 0) {
|
|
113
|
+
for (const bullet of bullets) {
|
|
114
|
+
if (!bullet.text?.trim()) continue;
|
|
115
|
+
|
|
116
|
+
const item = { content: bullet.text };
|
|
117
|
+
if (category) item.category = category;
|
|
118
|
+
|
|
119
|
+
const inferredTopic = inferTopicFromPath(file.path);
|
|
120
|
+
if (bullet.topic && bullet.topic !== inferredTopic) {
|
|
121
|
+
item.tags = [bullet.topic];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (isExpiredBullet(bullet, today)) {
|
|
125
|
+
item.status = 'expired';
|
|
126
|
+
} else if (bullet.section === 'history' || bullet.section === 'archive') {
|
|
127
|
+
item.status = 'archived';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (file.createdAt) {
|
|
131
|
+
item.created_at = new Date(file.createdAt).toISOString().slice(0, 10);
|
|
132
|
+
}
|
|
133
|
+
if (bullet.updatedAt) item.updated_at = bullet.updatedAt;
|
|
134
|
+
if (bullet.expiresAt) item.expires_at = bullet.expiresAt;
|
|
135
|
+
|
|
136
|
+
item.extensions = {
|
|
137
|
+
nanomem: {
|
|
138
|
+
file_path: file.path,
|
|
139
|
+
heading: bullet.heading || null,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
memories.push(item);
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const item = { content: file.content.trim() };
|
|
149
|
+
if (category) item.category = category;
|
|
150
|
+
if (file.createdAt) {
|
|
151
|
+
item.created_at = new Date(file.createdAt).toISOString().slice(0, 10);
|
|
152
|
+
}
|
|
153
|
+
if (file.updatedAt) {
|
|
154
|
+
item.updated_at = new Date(file.updatedAt).toISOString().slice(0, 10);
|
|
155
|
+
}
|
|
156
|
+
item.extensions = {
|
|
157
|
+
nanomem: {
|
|
158
|
+
file_path: file.path,
|
|
159
|
+
document: true,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
memories.push(item);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
omf: SUPPORTED_OMF_VERSIONS[0],
|
|
167
|
+
exported_at: new Date().toISOString(),
|
|
168
|
+
source: {
|
|
169
|
+
app: options.sourceApp || DEFAULT_SOURCE_APP,
|
|
170
|
+
},
|
|
171
|
+
memories,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {StorageFacade} storage
|
|
177
|
+
* @param {OmfDocument} doc
|
|
178
|
+
* @param {OmfImportOptions} [options]
|
|
179
|
+
* @returns {Promise<OmfImportPreview>}
|
|
180
|
+
*/
|
|
181
|
+
export async function previewOmfImport(storage, doc, options = {}) {
|
|
182
|
+
const { includeArchived = true } = options;
|
|
183
|
+
const validation = validateOmf(doc);
|
|
184
|
+
if (!validation.valid) {
|
|
185
|
+
throw new Error(validation.error);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const total = doc.memories.length;
|
|
189
|
+
let filtered = 0;
|
|
190
|
+
let duplicates = 0;
|
|
191
|
+
const byFile = /** @type {Record<string, {new: number, duplicate: number, document?: boolean}>} */ ({});
|
|
192
|
+
|
|
193
|
+
for (const item of doc.memories) {
|
|
194
|
+
if (!includeArchived && (item.status === 'archived' || item.status === 'expired')) {
|
|
195
|
+
filtered += 1;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isDocumentItem(item)) {
|
|
200
|
+
const path = targetPathForItem(item);
|
|
201
|
+
byFile[path] = byFile[path] || { new: 0, duplicate: 0, document: true };
|
|
202
|
+
byFile[path].new += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const path = targetPathForItem(item);
|
|
207
|
+
byFile[path] = byFile[path] || { new: 0, duplicate: 0 };
|
|
208
|
+
|
|
209
|
+
const existing = await storage.read(path);
|
|
210
|
+
if (existing) {
|
|
211
|
+
const existingBullets = parseBullets(existing);
|
|
212
|
+
const normalizedNew = normalizeFactText(item.content);
|
|
213
|
+
const isDuplicate = existingBullets.some((bullet) => normalizeFactText(bullet.text) === normalizedNew);
|
|
214
|
+
if (isDuplicate) {
|
|
215
|
+
duplicates += 1;
|
|
216
|
+
byFile[path].duplicate += 1;
|
|
217
|
+
} else {
|
|
218
|
+
byFile[path].new += 1;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
byFile[path].new += 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const existingFiles = new Set();
|
|
226
|
+
const newFiles = new Set();
|
|
227
|
+
for (const path of Object.keys(byFile)) {
|
|
228
|
+
if (await storage.exists(path)) {
|
|
229
|
+
existingFiles.add(path);
|
|
230
|
+
} else {
|
|
231
|
+
newFiles.add(path);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
total,
|
|
237
|
+
filtered,
|
|
238
|
+
toImport: total - filtered - duplicates,
|
|
239
|
+
duplicates,
|
|
240
|
+
newFiles: newFiles.size,
|
|
241
|
+
existingFiles: existingFiles.size,
|
|
242
|
+
byFile,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {StorageFacade} storage
|
|
248
|
+
* @param {OmfDocument} doc
|
|
249
|
+
* @param {OmfImportOptions} [options]
|
|
250
|
+
* @returns {Promise<OmfImportResult>}
|
|
251
|
+
*/
|
|
252
|
+
export async function importOmf(storage, doc, options = {}) {
|
|
253
|
+
const { includeArchived = true } = options;
|
|
254
|
+
const validation = validateOmf(doc);
|
|
255
|
+
if (!validation.valid) {
|
|
256
|
+
throw new Error(validation.error);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const today = todayIsoDate();
|
|
260
|
+
const errors = [];
|
|
261
|
+
let skipped = 0;
|
|
262
|
+
const groups = new Map();
|
|
263
|
+
|
|
264
|
+
for (const item of doc.memories) {
|
|
265
|
+
if (!includeArchived && (item.status === 'archived' || item.status === 'expired')) {
|
|
266
|
+
skipped += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const path = targetPathForItem(item);
|
|
271
|
+
if (!groups.has(path)) {
|
|
272
|
+
groups.set(path, []);
|
|
273
|
+
}
|
|
274
|
+
groups.get(path).push(item);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let imported = 0;
|
|
278
|
+
let duplicates = 0;
|
|
279
|
+
let filesWritten = 0;
|
|
280
|
+
|
|
281
|
+
for (const [path, items] of groups.entries()) {
|
|
282
|
+
try {
|
|
283
|
+
const documentItems = items.filter(isDocumentItem);
|
|
284
|
+
const bulletItems = items.filter((item) => !isDocumentItem(item));
|
|
285
|
+
const defaultTopic = inferTopicFromPath(path);
|
|
286
|
+
|
|
287
|
+
if (documentItems.length > 0 && bulletItems.length === 0) {
|
|
288
|
+
const documentItem = documentItems[documentItems.length - 1];
|
|
289
|
+
await storage.write(path, documentItem.content);
|
|
290
|
+
imported += documentItems.length;
|
|
291
|
+
filesWritten += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const existing = await storage.read(path);
|
|
296
|
+
const existingBullets = existing ? parseBullets(existing) : [];
|
|
297
|
+
|
|
298
|
+
const incomingBullets = bulletItems.map((item) => ensureBulletMetadata({
|
|
299
|
+
text: item.content,
|
|
300
|
+
topic: item.tags?.[0] || null,
|
|
301
|
+
updatedAt: item.updated_at || today,
|
|
302
|
+
expiresAt: item.expires_at || null,
|
|
303
|
+
section: statusToSection(item.status),
|
|
304
|
+
}, { updatedAt: today, defaultTopic }));
|
|
305
|
+
|
|
306
|
+
const existingNormalized = new Set(existingBullets.map((bullet) => normalizeFactText(bullet.text)));
|
|
307
|
+
let itemDuplicates = 0;
|
|
308
|
+
for (const bullet of incomingBullets) {
|
|
309
|
+
if (existingNormalized.has(normalizeFactText(bullet.text))) {
|
|
310
|
+
itemDuplicates += 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
duplicates += itemDuplicates;
|
|
314
|
+
|
|
315
|
+
const allBullets = [...existingBullets, ...incomingBullets];
|
|
316
|
+
const compacted = compactBullets(allBullets, { today, defaultTopic });
|
|
317
|
+
const markdown = renderCompactedDocument(
|
|
318
|
+
compacted.working,
|
|
319
|
+
compacted.longTerm,
|
|
320
|
+
compacted.history
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
await storage.write(path, markdown);
|
|
324
|
+
imported += bulletItems.length - itemDuplicates;
|
|
325
|
+
filesWritten += 1;
|
|
326
|
+
|
|
327
|
+
if (documentItems.length > 0) {
|
|
328
|
+
imported += documentItems.length;
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
errors.push(`Error writing ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
total: doc.memories.length,
|
|
337
|
+
imported,
|
|
338
|
+
duplicates,
|
|
339
|
+
skipped,
|
|
340
|
+
filesWritten,
|
|
341
|
+
errors,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prompt set for conversation ingestion.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ingestionPrompt — general import: full discretion over create/append/update.
|
|
5
|
+
* addPrompt — `nanomem add`: only write NEW facts (create or append, no updates).
|
|
6
|
+
* updatePrompt — `nanomem update`: only edit EXISTING facts (no new files).
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
export const addPrompt = `You are a memory manager. Save NEW facts from the text that do not yet exist in memory.
|
|
10
|
+
|
|
11
|
+
CRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate.
|
|
12
|
+
|
|
13
|
+
Current memory index:
|
|
14
|
+
\`\`\`
|
|
15
|
+
{INDEX}
|
|
16
|
+
\`\`\`
|
|
17
|
+
|
|
18
|
+
For each new fact, decide:
|
|
19
|
+
- Use append_memory if an existing file already covers the same domain or topic.
|
|
20
|
+
- Use create_new_file only if no existing file is thematically close.
|
|
21
|
+
|
|
22
|
+
Do NOT save:
|
|
23
|
+
- Facts already present in memory
|
|
24
|
+
- Transient details (greetings, one-off questions with no lasting answer)
|
|
25
|
+
- Sensitive secrets (passwords, tokens, keys)
|
|
26
|
+
|
|
27
|
+
Bullet format: "- Fact text | topic=topic-name | source=user_statement | confidence=high | updated_at=YYYY-MM-DD"
|
|
28
|
+
|
|
29
|
+
If nothing new is worth saving, stop without calling any tools.`;
|
|
30
|
+
|
|
31
|
+
export const updatePrompt = `You are a memory manager. Update the user's memory based on the text below.
|
|
32
|
+
|
|
33
|
+
CRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate.
|
|
34
|
+
|
|
35
|
+
Current memory index:
|
|
36
|
+
\`\`\`
|
|
37
|
+
{INDEX}
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
Steps:
|
|
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"
|
|
53
|
+
|
|
54
|
+
If nothing new or changed is worth saving, stop without calling any tools.`;
|
|
55
|
+
|
|
8
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.
|
|
9
57
|
|
|
10
58
|
CRITICAL: Only save facts the user explicitly stated. Do NOT infer, extrapolate, or fabricate information.
|
|
@@ -13,7 +61,7 @@ Save information that is likely to help in a future conversation. Be selective
|
|
|
13
61
|
|
|
14
62
|
Do NOT save:
|
|
15
63
|
- Anything the user did not explicitly say (no inferences, no extrapolations, no "likely" facts)
|
|
16
|
-
- Information already present in existing files
|
|
64
|
+
- Information already present in existing files
|
|
17
65
|
- Transient details (greetings, "help me with this", "thanks", questions without lasting answers)
|
|
18
66
|
- The assistant's own reasoning, suggestions, or knowledge — only what the user stated
|
|
19
67
|
- Sensitive secrets (passwords, auth tokens, private keys, full payment data, government IDs)
|
|
@@ -28,24 +76,75 @@ Current memory index:
|
|
|
28
76
|
|
|
29
77
|
Instructions:
|
|
30
78
|
1. Read the conversation below and identify facts the user explicitly stated.
|
|
31
|
-
2.
|
|
32
|
-
3.
|
|
33
|
-
4.
|
|
79
|
+
2. 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.
|
|
80
|
+
3. If no relevant file exists yet, create_new_file directly.
|
|
81
|
+
4. 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.
|
|
82
|
+
5. Use this bullet format: "- Fact text | topic=topic-name | source=SOURCE | confidence=LEVEL | updated_at=YYYY-MM-DD"
|
|
83
|
+
6. Source values:
|
|
34
84
|
- source=user_statement — the user directly said this. This is the PRIMARY source. Use it for the vast majority of saved facts.
|
|
35
85
|
- source=llm_infer — 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" → "Works in SF"). Never use this to guess, extrapolate, or fill in gaps. When in doubt, do not save.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
7. Confidence: high for direct user statements, medium for llm_infer. Never save low-confidence items.
|
|
87
|
+
8. 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.
|
|
88
|
+
9. Facts worth saving: allergies, health conditions, location, job/role, tech stack, pets, family members, durable preferences, and active plans — but ONLY if the user explicitly mentioned them.
|
|
89
|
+
10. If a fact is time-sensitive, include date context in the text. You may optionally add review_at or expires_at.
|
|
90
|
+
11. If nothing new is worth remembering, simply stop without calling any write tools. Saving nothing is better than saving something wrong.
|
|
41
91
|
|
|
42
92
|
Rules:
|
|
43
93
|
- 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.
|
|
44
94
|
- Favor broad thematic files. A file can hold multiple related sub-topics — only truly unrelated facts need separate files.
|
|
45
95
|
- Only create a new file when nothing in the index is thematically close. When in doubt, append.
|
|
46
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.
|
|
47
|
-
- Use
|
|
97
|
+
- Use update_bullets only if a fact is now stale or contradicted. Pass all corrections for a file in one call.
|
|
48
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.
|
|
49
99
|
- If a conflict is ambiguous, preserve both versions rather than deleting one.
|
|
50
100
|
- Do not skip obvious facts just because the schema supports extra metadata.
|
|
51
101
|
- Content should be raw facts only — no filler commentary.`;
|
|
102
|
+
|
|
103
|
+
export const deletePrompt = `You are a memory manager performing a TARGETED deletion.
|
|
104
|
+
|
|
105
|
+
The user wants to remove: "{QUERY}"
|
|
106
|
+
|
|
107
|
+
RULES — read carefully before acting:
|
|
108
|
+
1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.
|
|
109
|
+
- If the query names a specific entity (a pet, person, place, project), delete every fact about that entity — not just the one line that introduces it.
|
|
110
|
+
- Example: "I have dog mochi" → delete ALL facts about Mochi (habits, toys, traits, the introduction line, etc.).
|
|
111
|
+
2. Do NOT delete facts about unrelated subjects, even if they appear in the same file.
|
|
112
|
+
3. When genuinely unsure whether a bullet is about the target subject, SKIP it.
|
|
113
|
+
4. Never delete an entire file — only individual bullets via delete_bullet.
|
|
114
|
+
5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.
|
|
115
|
+
|
|
116
|
+
Current memory index:
|
|
117
|
+
\`\`\`
|
|
118
|
+
{INDEX}
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
Steps:
|
|
122
|
+
1. Identify which file(s) likely contain the content to delete from the index above.
|
|
123
|
+
2. Use retrieve_file or list_directory if the relevant file is not obvious from the index.
|
|
124
|
+
3. Use read_file to read the identified file(s).
|
|
125
|
+
4. Call delete_bullet for each bullet that is about the subject(s) in the deletion request.
|
|
126
|
+
5. If nothing matches, stop without calling delete_bullet.`;
|
|
127
|
+
|
|
128
|
+
export const deepDeletePrompt = `You are a memory manager performing a COMPREHENSIVE deletion across ALL memory files.
|
|
129
|
+
|
|
130
|
+
The user wants to remove: "{QUERY}"
|
|
131
|
+
|
|
132
|
+
RULES — read carefully before acting:
|
|
133
|
+
1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.
|
|
134
|
+
- If the query names a specific entity (a pet, person, place, project), delete every fact about that entity across every file — not just the one line that introduces it.
|
|
135
|
+
- Example: "I have dog mochi" → delete ALL facts about Mochi wherever they appear.
|
|
136
|
+
2. Do NOT delete facts about unrelated subjects.
|
|
137
|
+
3. When genuinely unsure whether a bullet is about the target subject, SKIP it.
|
|
138
|
+
4. Never delete an entire file — only individual bullets via delete_bullet.
|
|
139
|
+
5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.
|
|
140
|
+
|
|
141
|
+
You MUST read every file listed below and check it for matching content.
|
|
142
|
+
|
|
143
|
+
Files to check:
|
|
144
|
+
{FILE_LIST}
|
|
145
|
+
|
|
146
|
+
Steps:
|
|
147
|
+
1. Use read_file to read each file listed above, one by one.
|
|
148
|
+
2. For each file, call delete_bullet for any bullet that is about the subject(s) in the deletion request.
|
|
149
|
+
3. Continue until every file has been checked.
|
|
150
|
+
4. If a file has no matching bullets, move on without calling delete_bullet for it.`;
|
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prompt set for document ingestion.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ingestionPrompt — general import: full discretion over create/append/update.
|
|
5
|
+
* addPrompt — `nanomem add --format markdown`: only write NEW facts (create or append, no updates).
|
|
6
|
+
* updatePrompt — `nanomem update --format markdown`: only edit EXISTING facts (no new files).
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
export const addPrompt = `You are a memory manager. Extract NEW facts from the document that do not yet exist in memory.
|
|
10
|
+
|
|
11
|
+
You may extract and reasonably infer facts from what the document shows — not just word-for-word statements. Use good judgment: extract what is clearly supported by the content, avoid speculation.
|
|
12
|
+
|
|
13
|
+
Current memory index:
|
|
14
|
+
\`\`\`
|
|
15
|
+
{INDEX}
|
|
16
|
+
\`\`\`
|
|
17
|
+
|
|
18
|
+
For each new fact, decide:
|
|
19
|
+
- Use append_memory if an existing file already covers the same domain or topic.
|
|
20
|
+
- Use create_new_file only if no existing file is thematically close.
|
|
21
|
+
|
|
22
|
+
Do NOT save:
|
|
23
|
+
- Facts already present in memory
|
|
24
|
+
- Boilerplate (installation steps, license text, generic disclaimers)
|
|
25
|
+
- Sensitive secrets (passwords, tokens, keys)
|
|
26
|
+
|
|
27
|
+
Bullet format: "- Fact text | topic=topic-name | source=document | confidence=high | updated_at=YYYY-MM-DD"
|
|
28
|
+
|
|
29
|
+
If nothing new is worth saving, stop without calling any tools.`;
|
|
30
|
+
|
|
31
|
+
export const updatePrompt = `You are a memory manager. Correct or update facts already saved in memory based on the document below.
|
|
32
|
+
|
|
33
|
+
CRITICAL: Only edit files that already exist. Do NOT create new files. Do NOT rewrite whole files.
|
|
34
|
+
|
|
35
|
+
Current memory index:
|
|
36
|
+
\`\`\`
|
|
37
|
+
{INDEX}
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
Steps:
|
|
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 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
|
+
|
|
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.`;
|
|
52
|
+
|
|
8
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.
|
|
9
54
|
|
|
10
55
|
Unlike conversation ingestion, you may extract and reasonably infer facts from what the documents show — not just what was explicitly stated word-for-word. Use good judgment: extract what is clearly supported by the content, avoid speculation.
|
|
@@ -14,7 +59,7 @@ Save information that would be useful when answering questions about this subjec
|
|
|
14
59
|
Do NOT save:
|
|
15
60
|
- Speculation or guesses not supported by the content
|
|
16
61
|
- Boilerplate (installation steps, license text, generic disclaimers)
|
|
17
|
-
- Information already present in existing files
|
|
62
|
+
- Information already present in existing files
|
|
18
63
|
- Sensitive secrets (passwords, auth tokens, private keys)
|
|
19
64
|
|
|
20
65
|
Current memory index:
|
|
@@ -26,7 +71,7 @@ Current memory index:
|
|
|
26
71
|
|
|
27
72
|
Instructions:
|
|
28
73
|
1. Read the document content and identify concrete, reusable facts about the subject.
|
|
29
|
-
2.
|
|
74
|
+
2. 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.
|
|
30
75
|
3. Use create_new_file for new topics, append_memory to add to existing files.
|
|
31
76
|
4. Use this bullet format: "- Fact text | topic=topic-name | source=SOURCE | confidence=LEVEL | updated_at=YYYY-MM-DD"
|
|
32
77
|
5. Source values (IMPORTANT — never use source=user_statement here):
|
|
@@ -41,3 +86,52 @@ Rules:
|
|
|
41
86
|
- One file per distinct topic. Do NOT put unrelated facts in the same file.
|
|
42
87
|
- Create new files freely — focused files are better than bloated ones.
|
|
43
88
|
- Content should be raw facts only — no filler commentary.`;
|
|
89
|
+
|
|
90
|
+
export const deletePrompt = `You are a memory manager performing a TARGETED deletion.
|
|
91
|
+
|
|
92
|
+
The user wants to remove: "{QUERY}"
|
|
93
|
+
|
|
94
|
+
RULES — read carefully before acting:
|
|
95
|
+
1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.
|
|
96
|
+
- If the query names a specific entity (a person, project, tool, concept), delete every fact about that entity — not just the one line that introduces it.
|
|
97
|
+
- Example: "project recipe-app" → delete ALL facts about the recipe-app project (tech stack, goals, status, etc.).
|
|
98
|
+
2. Do NOT delete facts about unrelated subjects, even if they appear in the same file.
|
|
99
|
+
3. When genuinely unsure whether a bullet is about the target subject, SKIP it.
|
|
100
|
+
4. Never delete an entire file — only individual bullets via delete_bullet.
|
|
101
|
+
5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.
|
|
102
|
+
|
|
103
|
+
Current memory index:
|
|
104
|
+
\`\`\`
|
|
105
|
+
{INDEX}
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
Steps:
|
|
109
|
+
1. Identify which file(s) likely contain the content to delete from the index above.
|
|
110
|
+
2. Use retrieve_file or list_directory if the relevant file is not obvious from the index.
|
|
111
|
+
3. Use read_file to read the identified file(s).
|
|
112
|
+
4. Call delete_bullet for each bullet that is about the subject(s) in the deletion request.
|
|
113
|
+
5. If nothing matches, stop without calling delete_bullet.`;
|
|
114
|
+
|
|
115
|
+
export const deepDeletePrompt = `You are a memory manager performing a COMPREHENSIVE deletion across ALL memory files.
|
|
116
|
+
|
|
117
|
+
The user wants to remove: "{QUERY}"
|
|
118
|
+
|
|
119
|
+
RULES — read carefully before acting:
|
|
120
|
+
1. Delete all bullets that are ABOUT the subject(s) or entity mentioned in the deletion request.
|
|
121
|
+
- If the query names a specific entity (a person, project, tool, concept), delete every fact about that entity wherever it appears.
|
|
122
|
+
- Example: "project recipe-app" → delete ALL facts about the recipe-app project across every file.
|
|
123
|
+
2. Do NOT delete facts about unrelated subjects.
|
|
124
|
+
3. When genuinely unsure whether a bullet is about the target subject, SKIP it.
|
|
125
|
+
4. Never delete an entire file — only individual bullets via delete_bullet.
|
|
126
|
+
5. Pass the EXACT bullet text as it appears in the file, including all | metadata after the fact.
|
|
127
|
+
|
|
128
|
+
You MUST read every file listed below and check it for matching content.
|
|
129
|
+
|
|
130
|
+
Files to check:
|
|
131
|
+
{FILE_LIST}
|
|
132
|
+
|
|
133
|
+
Steps:
|
|
134
|
+
1. Use read_file to read each file listed above, one by one.
|
|
135
|
+
2. For each file, call delete_bullet for any bullet that is about the subject(s) in the deletion request.
|
|
136
|
+
3. Continue until every file has been checked.
|
|
137
|
+
4. If a file has no matching bullets, move on without calling delete_bullet for it.`;
|
package/src/prompt_sets/index.js
CHANGED
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
* then add it to PROMPT_SETS below.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { ingestionPrompt as conversationIngestion } from './conversation/ingestion.js';
|
|
12
|
-
import { ingestionPrompt as documentIngestion } from './document/ingestion.js';
|
|
11
|
+
import { ingestionPrompt as conversationIngestion, addPrompt, updatePrompt, deletePrompt, deepDeletePrompt } from './conversation/ingestion.js';
|
|
12
|
+
import { ingestionPrompt as documentIngestion, addPrompt as documentAddPrompt, updatePrompt as documentUpdatePrompt, deletePrompt as documentDeletePrompt, deepDeletePrompt as documentDeepDeletePrompt } from './document/ingestion.js';
|
|
13
13
|
|
|
14
14
|
/** @type {Record<string, { ingestionPrompt: string }>} */
|
|
15
15
|
const PROMPT_SETS = {
|
|
16
|
-
conversation:
|
|
17
|
-
document:
|
|
16
|
+
conversation: { ingestionPrompt: conversationIngestion },
|
|
17
|
+
document: { ingestionPrompt: documentIngestion },
|
|
18
|
+
add: { ingestionPrompt: addPrompt },
|
|
19
|
+
update: { ingestionPrompt: updatePrompt },
|
|
20
|
+
document_add: { ingestionPrompt: documentAddPrompt },
|
|
21
|
+
document_update: { ingestionPrompt: documentUpdatePrompt },
|
|
22
|
+
delete: { ingestionPrompt: deletePrompt },
|
|
23
|
+
deep_delete: { ingestionPrompt: deepDeletePrompt },
|
|
24
|
+
document_delete: { ingestionPrompt: documentDeletePrompt },
|
|
25
|
+
document_deep_delete: { ingestionPrompt: documentDeepDeletePrompt },
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
/**
|