@totalreclaw/totalreclaw 1.0.4 → 1.1.0
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 +48 -67
- package/api-client.ts +328 -0
- package/consolidation.test.ts +356 -0
- package/consolidation.ts +227 -0
- package/crypto.ts +351 -0
- package/embedding.ts +84 -0
- package/extractor-dedup.test.ts +168 -0
- package/extractor.ts +237 -0
- package/generate-mnemonic.ts +14 -0
- package/hot-cache-wrapper.ts +126 -0
- package/import-adapters/base-adapter.ts +93 -0
- package/import-adapters/import-adapters.test.ts +595 -0
- package/import-adapters/index.ts +22 -0
- package/import-adapters/mcp-memory-adapter.ts +274 -0
- package/import-adapters/mem0-adapter.ts +233 -0
- package/import-adapters/types.ts +89 -0
- package/index.ts +2661 -0
- package/llm-client.ts +418 -0
- package/lsh.test.ts +463 -0
- package/lsh.ts +257 -0
- package/package.json +18 -33
- package/pocv2-e2e-test.ts +917 -0
- package/reranker.test.ts +594 -0
- package/reranker.ts +537 -0
- package/semantic-dedup.test.ts +392 -0
- package/semantic-dedup.ts +100 -0
- package/setup.sh +19 -0
- package/store-dedup-wiring.test.ts +186 -0
- package/subgraph-search.ts +282 -0
- package/subgraph-store.ts +346 -0
- package/SKILL.md +0 -709
- package/dist/index.js +0 -32154
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { BaseImportAdapter } from './base-adapter.js';
|
|
2
|
+
import type {
|
|
3
|
+
ImportSource,
|
|
4
|
+
NormalizedFact,
|
|
5
|
+
AdapterParseResult,
|
|
6
|
+
ProgressCallback,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MCP Memory Server entity record.
|
|
14
|
+
*/
|
|
15
|
+
interface MCPEntity {
|
|
16
|
+
type: 'entity';
|
|
17
|
+
name: string;
|
|
18
|
+
entityType: string;
|
|
19
|
+
observations: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* MCP Memory Server relation record.
|
|
24
|
+
*/
|
|
25
|
+
interface MCPRelation {
|
|
26
|
+
type: 'relation';
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
relationType: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MCPRecord = MCPEntity | MCPRelation;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Entity type mapping to TotalReclaw fact types.
|
|
36
|
+
*/
|
|
37
|
+
const ENTITY_TYPE_MAP: Record<string, NormalizedFact['type']> = {
|
|
38
|
+
person: 'fact',
|
|
39
|
+
project: 'fact',
|
|
40
|
+
organization: 'fact',
|
|
41
|
+
tool: 'preference',
|
|
42
|
+
technology: 'preference',
|
|
43
|
+
preference: 'preference',
|
|
44
|
+
goal: 'goal',
|
|
45
|
+
event: 'episodic',
|
|
46
|
+
decision: 'decision',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export class MCPMemoryAdapter extends BaseImportAdapter {
|
|
50
|
+
readonly source: ImportSource = 'mcp-memory';
|
|
51
|
+
readonly displayName = 'MCP Memory Server';
|
|
52
|
+
|
|
53
|
+
async parse(
|
|
54
|
+
input: { content?: string; file_path?: string },
|
|
55
|
+
onProgress?: ProgressCallback,
|
|
56
|
+
): Promise<AdapterParseResult> {
|
|
57
|
+
const warnings: string[] = [];
|
|
58
|
+
const errors: string[] = [];
|
|
59
|
+
|
|
60
|
+
let content: string;
|
|
61
|
+
|
|
62
|
+
if (input.content) {
|
|
63
|
+
content = input.content;
|
|
64
|
+
} else if (input.file_path) {
|
|
65
|
+
try {
|
|
66
|
+
const resolvedPath = input.file_path.replace(/^~/, os.homedir());
|
|
67
|
+
content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
68
|
+
} catch (e) {
|
|
69
|
+
errors.push(`Failed to read file: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
70
|
+
return { facts: [], warnings, errors };
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// Try default MCP memory path
|
|
74
|
+
const defaultPath = path.join(os.homedir(), '.mcp-memory', 'memory.jsonl');
|
|
75
|
+
try {
|
|
76
|
+
content = fs.readFileSync(defaultPath, 'utf-8');
|
|
77
|
+
warnings.push(`Using default MCP memory path: ${defaultPath}`);
|
|
78
|
+
} catch {
|
|
79
|
+
errors.push(
|
|
80
|
+
'No content, file_path, or file at default path (~/.mcp-memory/memory.jsonl). ' +
|
|
81
|
+
'Provide the memory.jsonl content or file path.',
|
|
82
|
+
);
|
|
83
|
+
return { facts: [], warnings, errors };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse JSONL records
|
|
88
|
+
const records = this.parseJSONL(content, errors);
|
|
89
|
+
|
|
90
|
+
if (onProgress) {
|
|
91
|
+
onProgress({
|
|
92
|
+
current: 0,
|
|
93
|
+
total: records.length,
|
|
94
|
+
phase: 'parsing',
|
|
95
|
+
message: `Parsing ${records.length} MCP Memory records...`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Separate entities and relations
|
|
100
|
+
const entities = new Map<string, MCPEntity>();
|
|
101
|
+
const relations: MCPRelation[] = [];
|
|
102
|
+
|
|
103
|
+
for (const record of records) {
|
|
104
|
+
if (record.type === 'entity') {
|
|
105
|
+
// Later entities override earlier ones (append-only file)
|
|
106
|
+
entities.set(record.name, record);
|
|
107
|
+
} else if (record.type === 'relation') {
|
|
108
|
+
relations.push(record);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Convert entities to facts
|
|
113
|
+
const rawFacts: Partial<NormalizedFact>[] = [];
|
|
114
|
+
let entityIndex = 0;
|
|
115
|
+
|
|
116
|
+
for (const [name, entity] of entities) {
|
|
117
|
+
const factType = ENTITY_TYPE_MAP[entity.entityType.toLowerCase()] || 'fact';
|
|
118
|
+
|
|
119
|
+
for (const observation of entity.observations) {
|
|
120
|
+
// Prefix observation with entity name for context
|
|
121
|
+
// "Works at Acme Corp" -> "John works at Acme Corp"
|
|
122
|
+
const text = this.contextualizeObservation(name, observation);
|
|
123
|
+
|
|
124
|
+
rawFacts.push({
|
|
125
|
+
text,
|
|
126
|
+
type: factType,
|
|
127
|
+
importance: 6,
|
|
128
|
+
source: 'mcp-memory',
|
|
129
|
+
sourceId: `${name}:${entityIndex}`,
|
|
130
|
+
tags: [entity.entityType],
|
|
131
|
+
});
|
|
132
|
+
entityIndex++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (onProgress) {
|
|
136
|
+
onProgress({
|
|
137
|
+
current: rawFacts.length,
|
|
138
|
+
total: rawFacts.length + relations.length,
|
|
139
|
+
phase: 'parsing',
|
|
140
|
+
message: `Parsed ${rawFacts.length} facts from entities...`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Convert relations to facts
|
|
146
|
+
for (const rel of relations) {
|
|
147
|
+
// Only create a fact if both entities exist
|
|
148
|
+
if (!entities.has(rel.from) || !entities.has(rel.to)) {
|
|
149
|
+
warnings.push(`Relation references unknown entity: ${rel.from} -> ${rel.to}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const text = `${rel.from} ${this.humanizeRelationType(rel.relationType)} ${rel.to}`;
|
|
154
|
+
|
|
155
|
+
rawFacts.push({
|
|
156
|
+
text,
|
|
157
|
+
type: 'fact',
|
|
158
|
+
importance: 5,
|
|
159
|
+
source: 'mcp-memory',
|
|
160
|
+
sourceId: `rel:${rel.from}:${rel.relationType}:${rel.to}`,
|
|
161
|
+
tags: ['relation'],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { facts, invalidCount } = this.validateFacts(rawFacts);
|
|
166
|
+
|
|
167
|
+
if (invalidCount > 0) {
|
|
168
|
+
warnings.push(`${invalidCount} observations had invalid/empty text and were skipped`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
facts,
|
|
173
|
+
warnings,
|
|
174
|
+
errors,
|
|
175
|
+
source_metadata: {
|
|
176
|
+
entities_count: entities.size,
|
|
177
|
+
relations_count: relations.length,
|
|
178
|
+
observations_total: rawFacts.length,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parse JSONL content into records.
|
|
185
|
+
*/
|
|
186
|
+
private parseJSONL(content: string, errors: string[]): MCPRecord[] {
|
|
187
|
+
const records: MCPRecord[] = [];
|
|
188
|
+
const lines = content.split('\n').filter((line) => line.trim().length > 0);
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
try {
|
|
192
|
+
const record = JSON.parse(lines[i]);
|
|
193
|
+
if (record.type === 'entity' || record.type === 'relation') {
|
|
194
|
+
records.push(record as MCPRecord);
|
|
195
|
+
} else {
|
|
196
|
+
// Unknown record type — skip silently (future-proofing)
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
errors.push(`Line ${i + 1}: Invalid JSON — skipped`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return records;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Add entity context to an observation.
|
|
208
|
+
*
|
|
209
|
+
* Heuristic: if the observation already starts with the entity name
|
|
210
|
+
* (case-insensitive) or a pronoun, return as-is. Otherwise prefix.
|
|
211
|
+
*
|
|
212
|
+
* Examples:
|
|
213
|
+
* - ("John", "Works at Acme Corp") -> "John works at Acme Corp"
|
|
214
|
+
* - ("John", "John likes TypeScript") -> "John likes TypeScript" (no double prefix)
|
|
215
|
+
* - ("John", "He prefers React") -> "John prefers React"
|
|
216
|
+
*/
|
|
217
|
+
private contextualizeObservation(entityName: string, observation: string): string {
|
|
218
|
+
const obsLower = observation.toLowerCase().trim();
|
|
219
|
+
const nameLower = entityName.toLowerCase();
|
|
220
|
+
|
|
221
|
+
// Already starts with the entity name
|
|
222
|
+
if (obsLower.startsWith(nameLower)) {
|
|
223
|
+
return observation.trim();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Starts with a pronoun — replace it
|
|
227
|
+
const pronouns = ['he ', 'she ', 'they ', 'it ', 'his ', 'her ', 'their ', 'its '];
|
|
228
|
+
for (const pronoun of pronouns) {
|
|
229
|
+
if (obsLower.startsWith(pronoun)) {
|
|
230
|
+
return entityName + ' ' + observation.trim().slice(pronoun.length);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Starts with a verb (lowercase first word) — prefix entity name
|
|
235
|
+
const firstChar = observation.trim()[0];
|
|
236
|
+
if (firstChar && firstChar === firstChar.toLowerCase()) {
|
|
237
|
+
return `${entityName} ${observation.trim()}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Observation is a standalone sentence — prefix with "About {entity}: "
|
|
241
|
+
return `${entityName}: ${observation.trim()}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Convert relation type slug to human-readable text.
|
|
246
|
+
*
|
|
247
|
+
* "works_on" -> "works on"
|
|
248
|
+
* "MEMBER_OF" -> "is a member of"
|
|
249
|
+
*/
|
|
250
|
+
private humanizeRelationType(relationType: string): string {
|
|
251
|
+
const slug = relationType.toLowerCase().replace(/_/g, ' ');
|
|
252
|
+
|
|
253
|
+
// Common relation type mappings
|
|
254
|
+
const mappings: Record<string, string> = {
|
|
255
|
+
'works on': 'works on',
|
|
256
|
+
'works at': 'works at',
|
|
257
|
+
'member of': 'is a member of',
|
|
258
|
+
'belongs to': 'belongs to',
|
|
259
|
+
'created by': 'was created by',
|
|
260
|
+
'depends on': 'depends on',
|
|
261
|
+
'uses': 'uses',
|
|
262
|
+
'knows': 'knows',
|
|
263
|
+
'related to': 'is related to',
|
|
264
|
+
'part of': 'is part of',
|
|
265
|
+
'friend of': 'is friends with',
|
|
266
|
+
'manages': 'manages',
|
|
267
|
+
'reports to': 'reports to',
|
|
268
|
+
'located in': 'is located in',
|
|
269
|
+
'lives in': 'lives in',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return mappings[slug] || slug;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { BaseImportAdapter } from './base-adapter.js';
|
|
2
|
+
import type {
|
|
3
|
+
ImportSource,
|
|
4
|
+
NormalizedFact,
|
|
5
|
+
AdapterParseResult,
|
|
6
|
+
ProgressCallback,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mem0 memory structure from their API/export.
|
|
11
|
+
*/
|
|
12
|
+
interface Mem0Memory {
|
|
13
|
+
id: string;
|
|
14
|
+
memory: string;
|
|
15
|
+
hash?: string;
|
|
16
|
+
metadata?: {
|
|
17
|
+
category?: string;
|
|
18
|
+
created_at?: string;
|
|
19
|
+
updated_at?: string;
|
|
20
|
+
};
|
|
21
|
+
user_id?: string;
|
|
22
|
+
agent_id?: string;
|
|
23
|
+
categories?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Mem0ApiResponse {
|
|
27
|
+
results: Mem0Memory[];
|
|
28
|
+
next?: string; // pagination cursor
|
|
29
|
+
total?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Mem0ExportFile {
|
|
33
|
+
export_date?: string;
|
|
34
|
+
user_id?: string;
|
|
35
|
+
memories: Mem0Memory[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Category mapping from Mem0 categories to TotalReclaw types.
|
|
40
|
+
*/
|
|
41
|
+
const CATEGORY_MAP: Record<string, NormalizedFact['type']> = {
|
|
42
|
+
preference: 'preference',
|
|
43
|
+
preferences: 'preference',
|
|
44
|
+
like: 'preference',
|
|
45
|
+
dislike: 'preference',
|
|
46
|
+
fact: 'fact',
|
|
47
|
+
personal: 'fact',
|
|
48
|
+
biographical: 'fact',
|
|
49
|
+
decision: 'decision',
|
|
50
|
+
goal: 'goal',
|
|
51
|
+
objective: 'goal',
|
|
52
|
+
experience: 'episodic',
|
|
53
|
+
event: 'episodic',
|
|
54
|
+
memory: 'episodic',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export class Mem0Adapter extends BaseImportAdapter {
|
|
58
|
+
readonly source: ImportSource = 'mem0';
|
|
59
|
+
readonly displayName = 'Mem0';
|
|
60
|
+
|
|
61
|
+
async parse(
|
|
62
|
+
input: { content?: string; api_key?: string; source_user_id?: string; api_url?: string },
|
|
63
|
+
onProgress?: ProgressCallback,
|
|
64
|
+
): Promise<AdapterParseResult> {
|
|
65
|
+
const warnings: string[] = [];
|
|
66
|
+
const errors: string[] = [];
|
|
67
|
+
|
|
68
|
+
let memories: Mem0Memory[];
|
|
69
|
+
|
|
70
|
+
if (input.content) {
|
|
71
|
+
// Parse from pasted content or export file
|
|
72
|
+
memories = this.parseExportContent(input.content, errors);
|
|
73
|
+
} else if (input.api_key) {
|
|
74
|
+
// Fetch from Mem0 API
|
|
75
|
+
memories = await this.fetchFromApi(
|
|
76
|
+
input.api_key,
|
|
77
|
+
input.source_user_id,
|
|
78
|
+
input.api_url,
|
|
79
|
+
onProgress,
|
|
80
|
+
errors,
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
errors.push('Mem0 import requires either content (export file) or api_key');
|
|
84
|
+
return { facts: [], warnings, errors };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (onProgress) {
|
|
88
|
+
onProgress({
|
|
89
|
+
current: 0,
|
|
90
|
+
total: memories.length,
|
|
91
|
+
phase: 'parsing',
|
|
92
|
+
message: `Parsing ${memories.length} Mem0 memories...`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Convert Mem0 memories to NormalizedFacts
|
|
97
|
+
const rawFacts: Partial<NormalizedFact>[] = memories.map((mem) => ({
|
|
98
|
+
text: mem.memory,
|
|
99
|
+
type: this.mapCategory(mem.categories, mem.metadata?.category),
|
|
100
|
+
importance: 6, // Mem0 doesn't provide importance — default to 6 (above threshold)
|
|
101
|
+
source: 'mem0' as ImportSource,
|
|
102
|
+
sourceId: mem.id,
|
|
103
|
+
sourceTimestamp: mem.metadata?.updated_at || mem.metadata?.created_at,
|
|
104
|
+
tags: mem.categories || [],
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const { facts, invalidCount } = this.validateFacts(rawFacts);
|
|
108
|
+
|
|
109
|
+
if (invalidCount > 0) {
|
|
110
|
+
warnings.push(`${invalidCount} memories had invalid/empty text and were skipped`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { facts, warnings, errors, source_metadata: { total_from_source: memories.length } };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse Mem0 export file or pasted JSON.
|
|
118
|
+
*/
|
|
119
|
+
private parseExportContent(content: string, errors: string[]): Mem0Memory[] {
|
|
120
|
+
try {
|
|
121
|
+
const data = JSON.parse(content.trim());
|
|
122
|
+
|
|
123
|
+
// Handle export file format: { memories: [...] }
|
|
124
|
+
if (data.memories && Array.isArray(data.memories)) {
|
|
125
|
+
return data.memories;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle API response format: { results: [...] }
|
|
129
|
+
if (data.results && Array.isArray(data.results)) {
|
|
130
|
+
return data.results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Handle bare array
|
|
134
|
+
if (Array.isArray(data)) {
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
errors.push('Unrecognized Mem0 format. Expected { memories: [...] }, { results: [...] }, or bare array.');
|
|
139
|
+
return [];
|
|
140
|
+
} catch (e) {
|
|
141
|
+
errors.push(`Failed to parse Mem0 JSON: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fetch all memories from Mem0 API with pagination.
|
|
148
|
+
*/
|
|
149
|
+
private async fetchFromApi(
|
|
150
|
+
apiKey: string,
|
|
151
|
+
sourceUserId?: string,
|
|
152
|
+
apiUrl?: string,
|
|
153
|
+
onProgress?: ProgressCallback,
|
|
154
|
+
errors?: string[],
|
|
155
|
+
): Promise<Mem0Memory[]> {
|
|
156
|
+
const baseUrl = apiUrl || 'https://api.mem0.ai';
|
|
157
|
+
const allMemories: Mem0Memory[] = [];
|
|
158
|
+
let page = 1;
|
|
159
|
+
const pageSize = 100;
|
|
160
|
+
let hasMore = true;
|
|
161
|
+
|
|
162
|
+
while (hasMore) {
|
|
163
|
+
try {
|
|
164
|
+
const url = new URL(`${baseUrl}/v1/memories/`);
|
|
165
|
+
url.searchParams.set('page', String(page));
|
|
166
|
+
url.searchParams.set('page_size', String(pageSize));
|
|
167
|
+
if (sourceUserId) {
|
|
168
|
+
url.searchParams.set('user_id', sourceUserId);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const response = await fetch(url.toString(), {
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Token ${apiKey}`,
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
const errorText = await response.text();
|
|
180
|
+
errors?.push(`Mem0 API error (${response.status}): ${errorText.slice(0, 200)}`);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const data: Mem0ApiResponse = await response.json();
|
|
185
|
+
const memories = data.results || [];
|
|
186
|
+
allMemories.push(...memories);
|
|
187
|
+
|
|
188
|
+
if (onProgress) {
|
|
189
|
+
onProgress({
|
|
190
|
+
current: allMemories.length,
|
|
191
|
+
total: data.total || allMemories.length,
|
|
192
|
+
phase: 'fetching',
|
|
193
|
+
message: `Fetched ${allMemories.length} memories from Mem0...`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
hasMore = memories.length === pageSize;
|
|
198
|
+
page++;
|
|
199
|
+
|
|
200
|
+
// Safety limit: 10,000 memories max
|
|
201
|
+
if (allMemories.length >= 10_000) {
|
|
202
|
+
errors?.push('Reached 10,000 memory limit. Some memories may not have been fetched.');
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
errors?.push(`Mem0 API fetch error: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return allMemories;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Map Mem0 category to TotalReclaw fact type.
|
|
216
|
+
*/
|
|
217
|
+
private mapCategory(
|
|
218
|
+
categories?: string[],
|
|
219
|
+
singleCategory?: string,
|
|
220
|
+
): NormalizedFact['type'] {
|
|
221
|
+
const allCategories = [
|
|
222
|
+
...(categories || []),
|
|
223
|
+
...(singleCategory ? [singleCategory] : []),
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
for (const cat of allCategories) {
|
|
227
|
+
const mapped = CATEGORY_MAP[cat.toLowerCase()];
|
|
228
|
+
if (mapped) return mapped;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return 'fact'; // default
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized fact — the common format all adapters produce.
|
|
3
|
+
* Maps directly to what storeExtractedFacts() / client.remember() expect.
|
|
4
|
+
*/
|
|
5
|
+
export interface NormalizedFact {
|
|
6
|
+
/** The atomic fact text (max 512 chars) */
|
|
7
|
+
text: string;
|
|
8
|
+
/** Fact type matching TotalReclaw's taxonomy */
|
|
9
|
+
type: 'fact' | 'preference' | 'decision' | 'episodic' | 'goal';
|
|
10
|
+
/** Importance score 1-10 */
|
|
11
|
+
importance: number;
|
|
12
|
+
/** Original source system */
|
|
13
|
+
source: ImportSource;
|
|
14
|
+
/** Original ID in the source system (for traceability) */
|
|
15
|
+
sourceId?: string;
|
|
16
|
+
/** Original timestamp from source */
|
|
17
|
+
sourceTimestamp?: string;
|
|
18
|
+
/** Additional tags */
|
|
19
|
+
tags?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ImportSource = 'mem0' | 'mcp-memory' | 'memoclaw' | 'generic-json' | 'generic-csv';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the user passes to the import tool.
|
|
26
|
+
*/
|
|
27
|
+
export interface ImportFromInput {
|
|
28
|
+
/** Which source to import from */
|
|
29
|
+
source: ImportSource;
|
|
30
|
+
/** For API-based sources: the API key or auth token */
|
|
31
|
+
api_key?: string;
|
|
32
|
+
/** For API-based sources: the user/agent ID in the source system */
|
|
33
|
+
source_user_id?: string;
|
|
34
|
+
/** For file-based sources: the file content (pasted or read) */
|
|
35
|
+
content?: string;
|
|
36
|
+
/** For file-based sources: path to the file on disk */
|
|
37
|
+
file_path?: string;
|
|
38
|
+
/** Optional: target namespace in TotalReclaw */
|
|
39
|
+
namespace?: string;
|
|
40
|
+
/** Optional: dry run — parse and report without storing */
|
|
41
|
+
dry_run?: boolean;
|
|
42
|
+
/** Optional: API base URL override (for self-hosted instances) */
|
|
43
|
+
api_url?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result of an import operation.
|
|
48
|
+
*/
|
|
49
|
+
export interface ImportResult {
|
|
50
|
+
success: boolean;
|
|
51
|
+
source: ImportSource;
|
|
52
|
+
/** Total facts found in source */
|
|
53
|
+
total_found: number;
|
|
54
|
+
/** Facts successfully imported */
|
|
55
|
+
imported: number;
|
|
56
|
+
/** Facts skipped (duplicates via content fingerprint) */
|
|
57
|
+
skipped_duplicate: number;
|
|
58
|
+
/** Facts skipped (validation errors) */
|
|
59
|
+
skipped_invalid: number;
|
|
60
|
+
/** Individual errors */
|
|
61
|
+
errors: Array<{ index: number; text_preview: string; error: string }>;
|
|
62
|
+
/** Warnings */
|
|
63
|
+
warnings: string[];
|
|
64
|
+
/** Unique import run ID */
|
|
65
|
+
import_id: string;
|
|
66
|
+
/** Duration in ms */
|
|
67
|
+
duration_ms: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Progress callback for long-running imports.
|
|
72
|
+
*/
|
|
73
|
+
export type ProgressCallback = (progress: {
|
|
74
|
+
current: number;
|
|
75
|
+
total: number;
|
|
76
|
+
phase: 'fetching' | 'parsing' | 'storing';
|
|
77
|
+
message: string;
|
|
78
|
+
}) => void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Adapter parse result — returned by each adapter's parse method.
|
|
82
|
+
*/
|
|
83
|
+
export interface AdapterParseResult {
|
|
84
|
+
facts: NormalizedFact[];
|
|
85
|
+
warnings: string[];
|
|
86
|
+
errors: string[];
|
|
87
|
+
/** Metadata about the source (for logging) */
|
|
88
|
+
source_metadata?: Record<string, unknown>;
|
|
89
|
+
}
|