@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 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-Term
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: './memory'
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 # conversation mode
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openanonymity/nanomem",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "LLM-driven personal memory with agentic retrieval, extraction, and compaction",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 (/^(working)$/i.test(currentHeading)) {
36
+ if (/^working/i.test(currentHeading)) {
37
37
  section = 'working';
38
- } else if (/^(long[- ]?term|active)$/i.test(currentHeading)) {
38
+ } else if (/^(long[- ]?term|active)/i.test(currentHeading)) {
39
39
  section = 'long_term';
40
- } else if (/^(history|archive)$/i.test(currentHeading)) {
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|current context|stable facts|no longer current)$/i.test(title)) continue;
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, subsectionTitle, bullets, forceHistory = false) {
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', 'Current context', working);
191
+ renderSection(lines, 'Working memory (current context subject to change)', working);
193
192
  lines.push('');
194
- renderSection(lines, 'Long-Term', 'Stable facts', longTerm);
193
+ renderSection(lines, 'Long-term memory (stable facts that are unlikely to change)', longTerm);
195
194
  lines.push('');
196
- renderSection(lines, 'History', 'No longer current', history, true);
195
+ renderSection(lines, 'History (no longer current)', history, true);
197
196
 
198
197
  return lines.join('\n').trim();
199
198
  }
@@ -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 { assembledContext: null, message: 'No relevant context found.' };
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) => {
@@ -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-Term
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:
@@ -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 update_memory instead.` });
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
- update_memory: async ({ path, content }) => {
428
+ update_bullets: async ({ path, updates }) => {
427
429
  const before = await backend.read(path);
428
- const normalized = normalizeContent ? normalizeContent(content, path) : content;
429
- await backend.write(path, normalized);
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 ?? '', normalized);
432
- return JSON.stringify({ success: true, path, action: 'updated' });
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);
@@ -70,35 +70,46 @@ const T_APPEND_MEMORY = {
70
70
  };
71
71
 
72
72
  /** @type {ToolDefinition} */
73
- const T_UPDATE_MEMORY = {
73
+ const T_UPDATE_BULLETS = {
74
74
  type: 'function',
75
75
  function: {
76
- name: 'update_memory',
77
- description: 'Overwrite an existing memory file. Use when existing content is stale or contradicted.',
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
- content: { type: 'string', description: 'Complete new content for the file' }
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', 'content']
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 (no update_memory).
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: [T_READ_FILE, T_CREATE_NEW_FILE, T_APPEND_MEMORY],
98
- update: [T_READ_FILE, T_UPDATE_MEMORY],
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, T_UPDATE_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
- ? `Document content:\n\`\`\`\n${conversationText}\n\`\`\``
147
- : `Conversation:\n\`\`\`\n${conversationText}\n\`\`\``;
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', 'update_memory', 'archive_memory', 'delete_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
- // Preserve existing updatedAt so update_memory doesn't re-stamp unchanged bullets.
211
- // Bullets without a date still fall back to updatedAt (today).
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
  });
@@ -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 : 8,
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',
@@ -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(String(conversation.update_time * 1000))
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();
@@ -9,6 +9,11 @@ export {
9
9
  parseChatGptExport,
10
10
  } from './chatgpt.js';
11
11
 
12
+ export {
13
+ isClaudeExport,
14
+ parseClaudeExport,
15
+ } from './claude.js';
16
+
12
17
  export {
13
18
  parseMarkdownFiles,
14
19
  } from './markdown.js';
@@ -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. Correct or update facts already saved in memory based on the text below.
31
+ export const updatePrompt = `You are a memory manager. Update the user's memory based on the text below.
32
32
 
33
- CRITICAL: Only edit files that already exist. Do NOT create new files.
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 now stale or contradicted.
42
- 2. Use read_file to read the current content.
43
- 3. Use update_memory to write the corrected version.
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 no existing fact needs updating, stop without calling any tools.`;
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 update_memory only if a fact is now stale or contradicted.
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 update_memory to write the corrected version.
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
- If no existing fact needs updating, stop without calling any tools.`;
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
- update_memory: ({ path, content }: {
63
+ update_bullets: ({ path, updates }: {
64
64
  path: any;
65
- content: any;
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';
@@ -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. Correct or update facts already saved in memory based on the text 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.\n2. Use read_file to read the current content.\n3. Use update_memory to write the corrected version.\n\nIf no existing fact needs updating, 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_memory only if a fact is now stale or contradicted.\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.";
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 update_memory to write the corrected version.\n\nIf no existing fact needs updating, 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. 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 = {