@totalreclaw/totalreclaw 1.4.0 → 1.6.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/import-adapters/base-adapter.ts +4 -5
- package/import-adapters/chatgpt-adapter.ts +323 -0
- package/import-adapters/claude-adapter.ts +146 -0
- package/import-adapters/import-adapters.test.ts +533 -5
- package/import-adapters/index.ts +6 -0
- package/import-adapters/mcp-memory-adapter.ts +4 -2
- package/import-adapters/mem0-adapter.ts +2 -2
- package/import-adapters/types.ts +25 -2
- package/index.ts +448 -13
- package/package.json +1 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for import adapters
|
|
2
|
+
* Unit tests for import adapters.
|
|
3
3
|
*
|
|
4
4
|
* Run with: npx tsx import-adapters/import-adapters.test.ts
|
|
5
5
|
*
|
|
6
6
|
* Uses TAP-style output (no test framework dependency).
|
|
7
|
+
*
|
|
8
|
+
* ChatGPT and Claude adapters return conversation CHUNKS (not facts).
|
|
9
|
+
* Fact extraction is delegated to the LLM via extractFacts().
|
|
10
|
+
* Mem0 and MCP Memory adapters return pre-structured facts.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { Mem0Adapter } from './mem0-adapter.js';
|
|
10
14
|
import { MCPMemoryAdapter } from './mcp-memory-adapter.js';
|
|
15
|
+
import { ChatGPTAdapter } from './chatgpt-adapter.js';
|
|
16
|
+
import { ClaudeAdapter } from './claude-adapter.js';
|
|
11
17
|
import { BaseImportAdapter } from './base-adapter.js';
|
|
12
18
|
import { getAdapter } from './index.js';
|
|
13
19
|
import type {
|
|
@@ -58,7 +64,7 @@ class TestAdapter extends BaseImportAdapter {
|
|
|
58
64
|
readonly displayName = 'Test Adapter';
|
|
59
65
|
|
|
60
66
|
async parse(): Promise<AdapterParseResult> {
|
|
61
|
-
return { facts: [], warnings: [], errors: [] };
|
|
67
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings: [], errors: [] };
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
// Expose protected methods for testing
|
|
@@ -102,6 +108,7 @@ async function runTests(): Promise<void> {
|
|
|
102
108
|
assert(result.facts[0].source === 'mem0', 'Mem0: source is mem0');
|
|
103
109
|
assert(result.facts[1].type === 'fact', 'Mem0: fact category mapped');
|
|
104
110
|
assert(result.facts[1].sourceId === 'mem-2', 'Mem0: sourceId preserved');
|
|
111
|
+
assert(result.chunks.length === 0, 'Mem0: no chunks (pre-structured source)');
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
// --- parses export file format ---
|
|
@@ -250,13 +257,11 @@ async function runTests(): Promise<void> {
|
|
|
250
257
|
const result = await adapter.parse({ content });
|
|
251
258
|
|
|
252
259
|
assert(result.facts.length === 3, 'MCP: 2 entities with 3 total observations -> 3 facts');
|
|
253
|
-
// "Works at Acme Corp" starts with uppercase verb -> prefixed: "John: Works at Acme Corp"
|
|
254
|
-
// Actually the adapter checks if it starts lowercase (verb) or uppercase (standalone sentence)
|
|
255
|
-
// "Works" starts uppercase -> "John: Works at Acme Corp"
|
|
256
260
|
assert(result.facts[0].text.includes('John'), 'MCP: first fact includes entity name');
|
|
257
261
|
assert(result.facts[0].text.includes('Acme Corp'), 'MCP: first fact includes observation');
|
|
258
262
|
assert(result.facts[1].text.includes('TypeScript'), 'MCP: second fact includes TypeScript');
|
|
259
263
|
assert(result.facts[0].source === 'mcp-memory', 'MCP: source is mcp-memory');
|
|
264
|
+
assert(result.chunks.length === 0, 'MCP: no chunks (pre-structured source)');
|
|
260
265
|
}
|
|
261
266
|
|
|
262
267
|
// --- contextualizes observations correctly ---
|
|
@@ -537,6 +542,521 @@ async function runTests(): Promise<void> {
|
|
|
537
542
|
assert(invalidCount === 2, `Base: validateFacts counts 2 invalid (got ${invalidCount})`);
|
|
538
543
|
}
|
|
539
544
|
|
|
545
|
+
// =========================================================================
|
|
546
|
+
// ChatGPTAdapter — conversations.json (returns chunks, not facts)
|
|
547
|
+
// =========================================================================
|
|
548
|
+
|
|
549
|
+
console.log('# ChatGPTAdapter — conversations.json (chunks)');
|
|
550
|
+
|
|
551
|
+
// --- returns conversation chunks with user + assistant messages ---
|
|
552
|
+
{
|
|
553
|
+
const conversations = [
|
|
554
|
+
{
|
|
555
|
+
id: 'conv-1',
|
|
556
|
+
title: 'Test Conversation',
|
|
557
|
+
create_time: 1700000000,
|
|
558
|
+
mapping: {
|
|
559
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
560
|
+
msg1: {
|
|
561
|
+
id: 'msg1',
|
|
562
|
+
message: {
|
|
563
|
+
id: 'msg1',
|
|
564
|
+
author: { role: 'user' },
|
|
565
|
+
content: { content_type: 'text', parts: ['I work at Google as a software engineer'] },
|
|
566
|
+
create_time: 1700000001,
|
|
567
|
+
},
|
|
568
|
+
parent: 'root',
|
|
569
|
+
children: ['msg2'],
|
|
570
|
+
},
|
|
571
|
+
msg2: {
|
|
572
|
+
id: 'msg2',
|
|
573
|
+
message: {
|
|
574
|
+
id: 'msg2',
|
|
575
|
+
author: { role: 'assistant' },
|
|
576
|
+
content: { content_type: 'text', parts: ['That sounds great! Tell me more about your work.'] },
|
|
577
|
+
create_time: 1700000002,
|
|
578
|
+
},
|
|
579
|
+
parent: 'msg1',
|
|
580
|
+
children: ['msg3'],
|
|
581
|
+
},
|
|
582
|
+
msg3: {
|
|
583
|
+
id: 'msg3',
|
|
584
|
+
message: {
|
|
585
|
+
id: 'msg3',
|
|
586
|
+
author: { role: 'user' },
|
|
587
|
+
content: { content_type: 'text', parts: ['I prefer TypeScript over JavaScript for new projects'] },
|
|
588
|
+
create_time: 1700000003,
|
|
589
|
+
},
|
|
590
|
+
parent: 'msg2',
|
|
591
|
+
children: [],
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
|
|
597
|
+
const adapter = new ChatGPTAdapter();
|
|
598
|
+
const result = await adapter.parse({ content: JSON.stringify(conversations) });
|
|
599
|
+
|
|
600
|
+
assert(result.facts.length === 0, 'ChatGPT conv: no pre-extracted facts (uses chunks)');
|
|
601
|
+
assert(result.chunks.length === 1, `ChatGPT conv: 1 conversation chunk (got ${result.chunks.length})`);
|
|
602
|
+
assert(result.chunks[0].title === 'Test Conversation', 'ChatGPT conv: chunk title matches conversation title');
|
|
603
|
+
assert(result.chunks[0].messages.length === 3, `ChatGPT conv: 3 messages (user + assistant) (got ${result.chunks[0].messages.length})`);
|
|
604
|
+
assert(result.chunks[0].messages[0].role === 'user', 'ChatGPT conv: first message is user');
|
|
605
|
+
assert(result.chunks[0].messages[0].text.includes('Google'), 'ChatGPT conv: first message text correct');
|
|
606
|
+
assert(result.chunks[0].messages[1].role === 'assistant', 'ChatGPT conv: second message is assistant');
|
|
607
|
+
assert(result.chunks[0].messages[2].role === 'user', 'ChatGPT conv: third message is user');
|
|
608
|
+
assert(result.chunks[0].messages[2].text.includes('TypeScript'), 'ChatGPT conv: third message text correct');
|
|
609
|
+
assert(result.chunks[0].timestamp !== undefined, 'ChatGPT conv: chunk has timestamp');
|
|
610
|
+
assert(result.totalMessages === 3, `ChatGPT conv: totalMessages is 3 (got ${result.totalMessages})`);
|
|
611
|
+
assert(result.errors.length === 0, 'ChatGPT conv: no errors');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// --- includes both user and assistant messages ---
|
|
615
|
+
{
|
|
616
|
+
const conversations = [
|
|
617
|
+
{
|
|
618
|
+
title: 'Context Test',
|
|
619
|
+
mapping: {
|
|
620
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
621
|
+
msg1: {
|
|
622
|
+
id: 'msg1',
|
|
623
|
+
message: {
|
|
624
|
+
id: 'msg1',
|
|
625
|
+
author: { role: 'user' },
|
|
626
|
+
content: { content_type: 'text', parts: ['I want to migrate to TypeScript'] },
|
|
627
|
+
},
|
|
628
|
+
parent: 'root',
|
|
629
|
+
children: ['msg2'],
|
|
630
|
+
},
|
|
631
|
+
msg2: {
|
|
632
|
+
id: 'msg2',
|
|
633
|
+
message: {
|
|
634
|
+
id: 'msg2',
|
|
635
|
+
author: { role: 'assistant' },
|
|
636
|
+
content: { content_type: 'text', parts: ['TypeScript migration involves setting up tsconfig and converting files'] },
|
|
637
|
+
},
|
|
638
|
+
parent: 'msg1',
|
|
639
|
+
children: [],
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
const adapter = new ChatGPTAdapter();
|
|
646
|
+
const result = await adapter.parse({ content: JSON.stringify(conversations) });
|
|
647
|
+
|
|
648
|
+
assert(result.chunks.length === 1, 'ChatGPT: includes assistant messages for context');
|
|
649
|
+
assert(result.chunks[0].messages.length === 2, 'ChatGPT: both user and assistant in chunk');
|
|
650
|
+
assert(result.chunks[0].messages[1].role === 'assistant', 'ChatGPT: assistant message preserved');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// --- skips system and tool messages ---
|
|
654
|
+
{
|
|
655
|
+
const conversations = [
|
|
656
|
+
{
|
|
657
|
+
title: 'Test',
|
|
658
|
+
mapping: {
|
|
659
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
660
|
+
msg1: {
|
|
661
|
+
id: 'msg1',
|
|
662
|
+
message: {
|
|
663
|
+
id: 'msg1',
|
|
664
|
+
author: { role: 'system' },
|
|
665
|
+
content: { content_type: 'text', parts: ['You are a helpful assistant'] },
|
|
666
|
+
},
|
|
667
|
+
parent: 'root',
|
|
668
|
+
children: ['msg2'],
|
|
669
|
+
},
|
|
670
|
+
msg2: {
|
|
671
|
+
id: 'msg2',
|
|
672
|
+
message: {
|
|
673
|
+
id: 'msg2',
|
|
674
|
+
author: { role: 'tool' },
|
|
675
|
+
content: { content_type: 'text', parts: ['Tool output: search results...'] },
|
|
676
|
+
},
|
|
677
|
+
parent: 'msg1',
|
|
678
|
+
children: ['msg3'],
|
|
679
|
+
},
|
|
680
|
+
msg3: {
|
|
681
|
+
id: 'msg3',
|
|
682
|
+
message: {
|
|
683
|
+
id: 'msg3',
|
|
684
|
+
author: { role: 'user' },
|
|
685
|
+
content: { content_type: 'text', parts: ['I work at Google as a senior engineer'] },
|
|
686
|
+
},
|
|
687
|
+
parent: 'msg2',
|
|
688
|
+
children: [],
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
];
|
|
693
|
+
|
|
694
|
+
const adapter = new ChatGPTAdapter();
|
|
695
|
+
const result = await adapter.parse({ content: JSON.stringify(conversations) });
|
|
696
|
+
|
|
697
|
+
assert(result.chunks.length === 1, 'ChatGPT: has 1 chunk');
|
|
698
|
+
assert(result.chunks[0].messages.length === 1, 'ChatGPT: only user message (skips system + tool)');
|
|
699
|
+
assert(result.chunks[0].messages[0].role === 'user', 'ChatGPT: the surviving message is user');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// --- chunks large conversations into batches of 20 ---
|
|
703
|
+
{
|
|
704
|
+
// Build a conversation with 45 messages
|
|
705
|
+
const mapping: Record<string, any> = {
|
|
706
|
+
root: { id: 'root', message: null, parent: null, children: ['msg-0'] },
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
for (let i = 0; i < 45; i++) {
|
|
710
|
+
const role = i % 2 === 0 ? 'user' : 'assistant';
|
|
711
|
+
mapping[`msg-${i}`] = {
|
|
712
|
+
id: `msg-${i}`,
|
|
713
|
+
message: {
|
|
714
|
+
id: `msg-${i}`,
|
|
715
|
+
author: { role },
|
|
716
|
+
content: { content_type: 'text', parts: [`Message number ${i} from ${role} about various topics`] },
|
|
717
|
+
},
|
|
718
|
+
parent: i === 0 ? 'root' : `msg-${i - 1}`,
|
|
719
|
+
children: i < 44 ? [`msg-${i + 1}`] : [],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const conversations = [{ title: 'Long Conversation', mapping }];
|
|
724
|
+
|
|
725
|
+
const adapter = new ChatGPTAdapter();
|
|
726
|
+
const result = await adapter.parse({ content: JSON.stringify(conversations) });
|
|
727
|
+
|
|
728
|
+
assert(result.chunks.length === 3, `ChatGPT: 45 messages -> 3 chunks (got ${result.chunks.length})`);
|
|
729
|
+
assert(result.chunks[0].messages.length === 20, `ChatGPT: first chunk has 20 messages (got ${result.chunks[0].messages.length})`);
|
|
730
|
+
assert(result.chunks[1].messages.length === 20, `ChatGPT: second chunk has 20 messages (got ${result.chunks[1].messages.length})`);
|
|
731
|
+
assert(result.chunks[2].messages.length === 5, `ChatGPT: third chunk has 5 messages (got ${result.chunks[2].messages.length})`);
|
|
732
|
+
assert(result.chunks[0].title.includes('part 1/3'), `ChatGPT: first chunk title has part indicator (got: ${result.chunks[0].title})`);
|
|
733
|
+
assert(result.chunks[2].title.includes('part 3/3'), `ChatGPT: last chunk title has part indicator (got: ${result.chunks[2].title})`);
|
|
734
|
+
assert(result.totalMessages === 45, `ChatGPT: totalMessages is 45 (got ${result.totalMessages})`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// --- handles single conversation object (not array) ---
|
|
738
|
+
{
|
|
739
|
+
const conv = {
|
|
740
|
+
title: 'Single',
|
|
741
|
+
mapping: {
|
|
742
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
743
|
+
msg1: {
|
|
744
|
+
id: 'msg1',
|
|
745
|
+
message: { id: 'msg1', author: { role: 'user' }, content: { content_type: 'text', parts: ['I live in San Francisco near the park'] } },
|
|
746
|
+
parent: 'root', children: [],
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const adapter = new ChatGPTAdapter();
|
|
752
|
+
const result = await adapter.parse({ content: JSON.stringify(conv) });
|
|
753
|
+
|
|
754
|
+
assert(result.chunks.length === 1, 'ChatGPT: single conversation object parses into 1 chunk');
|
|
755
|
+
assert(result.chunks[0].messages[0].text.includes('San Francisco'), 'ChatGPT: single conv message correct');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// --- handles null/non-string parts ---
|
|
759
|
+
{
|
|
760
|
+
const conversations = [
|
|
761
|
+
{
|
|
762
|
+
title: 'Null Parts',
|
|
763
|
+
mapping: {
|
|
764
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
765
|
+
msg1: {
|
|
766
|
+
id: 'msg1',
|
|
767
|
+
message: {
|
|
768
|
+
id: 'msg1',
|
|
769
|
+
author: { role: 'user' },
|
|
770
|
+
content: { content_type: 'text', parts: [null, { type: 'image' }, 'I prefer dark mode in all my applications'] },
|
|
771
|
+
},
|
|
772
|
+
parent: 'root', children: [],
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
];
|
|
777
|
+
|
|
778
|
+
const adapter = new ChatGPTAdapter();
|
|
779
|
+
const result = await adapter.parse({ content: JSON.stringify(conversations) });
|
|
780
|
+
|
|
781
|
+
assert(result.chunks.length === 1, 'ChatGPT: handles null/non-string parts');
|
|
782
|
+
assert(result.chunks[0].messages[0].text.includes('dark mode'), 'ChatGPT: extracted text from valid part');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// --- invalid JSON returns error ---
|
|
786
|
+
{
|
|
787
|
+
const adapter = new ChatGPTAdapter();
|
|
788
|
+
const result2 = await adapter.parse({ content: '[invalid json array' });
|
|
789
|
+
assert(result2.chunks.length === 0, 'ChatGPT: invalid JSON array yields 0 chunks');
|
|
790
|
+
assert(result2.errors.length > 0, 'ChatGPT: invalid JSON produces error');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// --- empty input returns error ---
|
|
794
|
+
{
|
|
795
|
+
const adapter = new ChatGPTAdapter();
|
|
796
|
+
const result = await adapter.parse({});
|
|
797
|
+
|
|
798
|
+
assert(result.chunks.length === 0, 'ChatGPT: no input yields 0 chunks');
|
|
799
|
+
assert(result.errors.length > 0, 'ChatGPT: no input produces error');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// --- conversation with no text messages produces no chunks ---
|
|
803
|
+
{
|
|
804
|
+
const conversations = [
|
|
805
|
+
{
|
|
806
|
+
title: 'Empty',
|
|
807
|
+
mapping: {
|
|
808
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
809
|
+
msg1: {
|
|
810
|
+
id: 'msg1',
|
|
811
|
+
message: {
|
|
812
|
+
id: 'msg1',
|
|
813
|
+
author: { role: 'user' },
|
|
814
|
+
content: { content_type: 'text', parts: [null] },
|
|
815
|
+
},
|
|
816
|
+
parent: 'root', children: [],
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
const adapter = new ChatGPTAdapter();
|
|
823
|
+
const result = await adapter.parse({ content: JSON.stringify(conversations) });
|
|
824
|
+
|
|
825
|
+
assert(result.chunks.length === 0, 'ChatGPT: conversation with no text -> no chunks');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// --- multiple conversations produce multiple chunks ---
|
|
829
|
+
{
|
|
830
|
+
const conversations = [
|
|
831
|
+
{
|
|
832
|
+
title: 'Conv 1',
|
|
833
|
+
create_time: 1700000000,
|
|
834
|
+
mapping: {
|
|
835
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
836
|
+
msg1: {
|
|
837
|
+
id: 'msg1',
|
|
838
|
+
message: { id: 'msg1', author: { role: 'user' }, content: { content_type: 'text', parts: ['First conversation message'] } },
|
|
839
|
+
parent: 'root', children: [],
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
title: 'Conv 2',
|
|
845
|
+
create_time: 1700100000,
|
|
846
|
+
mapping: {
|
|
847
|
+
root: { id: 'root', message: null, parent: null, children: ['msg1'] },
|
|
848
|
+
msg1: {
|
|
849
|
+
id: 'msg1',
|
|
850
|
+
message: { id: 'msg1', author: { role: 'user' }, content: { content_type: 'text', parts: ['Second conversation message'] } },
|
|
851
|
+
parent: 'root', children: [],
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
];
|
|
856
|
+
|
|
857
|
+
const adapter = new ChatGPTAdapter();
|
|
858
|
+
const result = await adapter.parse({ content: JSON.stringify(conversations) });
|
|
859
|
+
|
|
860
|
+
assert(result.chunks.length === 2, `ChatGPT: 2 conversations -> 2 chunks (got ${result.chunks.length})`);
|
|
861
|
+
assert(result.chunks[0].title === 'Conv 1', 'ChatGPT: first chunk title correct');
|
|
862
|
+
assert(result.chunks[1].title === 'Conv 2', 'ChatGPT: second chunk title correct');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// =========================================================================
|
|
866
|
+
// ChatGPTAdapter — memories text (returns chunks, not facts)
|
|
867
|
+
// =========================================================================
|
|
868
|
+
|
|
869
|
+
console.log('# ChatGPTAdapter — memories text (chunks)');
|
|
870
|
+
|
|
871
|
+
// --- parses plain text memories into chunks ---
|
|
872
|
+
{
|
|
873
|
+
const memoriesText = `User prefers dark mode
|
|
874
|
+
User works at Google as a software engineer
|
|
875
|
+
User lives in San Francisco
|
|
876
|
+
User likes hiking on weekends`;
|
|
877
|
+
|
|
878
|
+
const adapter = new ChatGPTAdapter();
|
|
879
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
880
|
+
|
|
881
|
+
assert(result.facts.length === 0, 'ChatGPT memories: no pre-extracted facts');
|
|
882
|
+
assert(result.chunks.length === 1, `ChatGPT memories: 4 lines -> 1 chunk (got ${result.chunks.length})`);
|
|
883
|
+
assert(result.chunks[0].messages.length === 4, `ChatGPT memories: 4 messages in chunk (got ${result.chunks[0].messages.length})`);
|
|
884
|
+
assert(result.chunks[0].messages[0].text === 'User prefers dark mode', 'ChatGPT memories: first message text correct');
|
|
885
|
+
assert(result.chunks[0].messages[0].role === 'user', 'ChatGPT memories: all messages are user role');
|
|
886
|
+
assert(result.totalMessages === 4, `ChatGPT memories: totalMessages is 4 (got ${result.totalMessages})`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// --- handles bullet points and numbered lists ---
|
|
890
|
+
{
|
|
891
|
+
const memoriesText = `- User prefers TypeScript
|
|
892
|
+
* User works remotely
|
|
893
|
+
1. User lives in Berlin
|
|
894
|
+
2) User likes coffee in the morning`;
|
|
895
|
+
|
|
896
|
+
const adapter = new ChatGPTAdapter();
|
|
897
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
898
|
+
|
|
899
|
+
assert(result.chunks.length === 1, 'ChatGPT memories: 4 lines -> 1 chunk');
|
|
900
|
+
assert(result.chunks[0].messages.length === 4, `ChatGPT memories: handles bullets/numbers (got ${result.chunks[0].messages.length})`);
|
|
901
|
+
assert(!result.chunks[0].messages[0].text.startsWith('-'), 'ChatGPT memories: bullet stripped');
|
|
902
|
+
assert(!result.chunks[0].messages[2].text.startsWith('1'), 'ChatGPT memories: number stripped');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// --- skips empty lines and header lines ---
|
|
906
|
+
{
|
|
907
|
+
const memoriesText = `Memories:
|
|
908
|
+
|
|
909
|
+
User prefers dark mode
|
|
910
|
+
|
|
911
|
+
User lives in London`;
|
|
912
|
+
|
|
913
|
+
const adapter = new ChatGPTAdapter();
|
|
914
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
915
|
+
|
|
916
|
+
assert(result.chunks.length === 1, 'ChatGPT memories: skips header/blank');
|
|
917
|
+
assert(result.chunks[0].messages.length === 2, `ChatGPT memories: only 2 valid lines (got ${result.chunks[0].messages.length})`);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// --- empty text input ---
|
|
921
|
+
{
|
|
922
|
+
const adapter = new ChatGPTAdapter();
|
|
923
|
+
const result = await adapter.parse({ content: '' });
|
|
924
|
+
|
|
925
|
+
assert(result.chunks.length === 0, 'ChatGPT memories: empty text yields 0 chunks');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// --- large memory list chunks into multiple batches ---
|
|
929
|
+
{
|
|
930
|
+
const lines = Array.from({ length: 50 }, (_, i) => `User memory item number ${i + 1} about their preferences`);
|
|
931
|
+
const memoriesText = lines.join('\n');
|
|
932
|
+
|
|
933
|
+
const adapter = new ChatGPTAdapter();
|
|
934
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
935
|
+
|
|
936
|
+
assert(result.chunks.length === 3, `ChatGPT memories: 50 lines -> 3 chunks (got ${result.chunks.length})`);
|
|
937
|
+
assert(result.chunks[0].messages.length === 20, `ChatGPT memories: first chunk has 20 (got ${result.chunks[0].messages.length})`);
|
|
938
|
+
assert(result.chunks[1].messages.length === 20, `ChatGPT memories: second chunk has 20 (got ${result.chunks[1].messages.length})`);
|
|
939
|
+
assert(result.chunks[2].messages.length === 10, `ChatGPT memories: third chunk has 10 (got ${result.chunks[2].messages.length})`);
|
|
940
|
+
assert(result.totalMessages === 50, `ChatGPT memories: totalMessages is 50 (got ${result.totalMessages})`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// =========================================================================
|
|
944
|
+
// ClaudeAdapter (returns chunks, not facts)
|
|
945
|
+
// =========================================================================
|
|
946
|
+
|
|
947
|
+
console.log('# ClaudeAdapter (chunks)');
|
|
948
|
+
|
|
949
|
+
// --- parses plain text memories into chunks ---
|
|
950
|
+
{
|
|
951
|
+
const memoriesText = `User prefers functional programming
|
|
952
|
+
User works at a startup in Berlin
|
|
953
|
+
User decided to use Rust for the backend
|
|
954
|
+
User wants to learn machine learning`;
|
|
955
|
+
|
|
956
|
+
const adapter = new ClaudeAdapter();
|
|
957
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
958
|
+
|
|
959
|
+
assert(result.facts.length === 0, 'Claude: no pre-extracted facts (uses chunks)');
|
|
960
|
+
assert(result.chunks.length === 1, `Claude: 4 memories -> 1 chunk (got ${result.chunks.length})`);
|
|
961
|
+
assert(result.chunks[0].messages.length === 4, `Claude: 4 messages in chunk (got ${result.chunks[0].messages.length})`);
|
|
962
|
+
assert(result.chunks[0].messages[0].role === 'user', 'Claude: all messages are user role');
|
|
963
|
+
assert(result.chunks[0].messages[0].text === 'User prefers functional programming', 'Claude: first message text correct');
|
|
964
|
+
assert(result.totalMessages === 4, `Claude: totalMessages is 4 (got ${result.totalMessages})`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// --- handles date prefixes (strips them from text) ---
|
|
968
|
+
{
|
|
969
|
+
const memoriesText = `[2026-03-15] - User prefers dark mode
|
|
970
|
+
[2026-03-10] - User works at Google
|
|
971
|
+
No date prefix here but still a memory`;
|
|
972
|
+
|
|
973
|
+
const adapter = new ClaudeAdapter();
|
|
974
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
975
|
+
|
|
976
|
+
assert(result.chunks.length === 1, 'Claude: parsed into 1 chunk');
|
|
977
|
+
assert(result.chunks[0].messages.length === 3, `Claude: 3 messages (got ${result.chunks[0].messages.length})`);
|
|
978
|
+
assert(!result.chunks[0].messages[0].text.includes('[2026'), 'Claude: date prefix stripped from text');
|
|
979
|
+
assert(result.chunks[0].messages[0].text === 'User prefers dark mode', 'Claude: cleaned text correct');
|
|
980
|
+
assert(result.chunks[0].timestamp === '2026-03-15', 'Claude: chunk timestamp from first dated entry');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// --- handles bullet points and numbered lists ---
|
|
984
|
+
{
|
|
985
|
+
const memoriesText = `- User prefers TypeScript
|
|
986
|
+
* User works remotely from home
|
|
987
|
+
1. User lives in Lisbon, Portugal
|
|
988
|
+
2) User likes exploring new restaurants`;
|
|
989
|
+
|
|
990
|
+
const adapter = new ClaudeAdapter();
|
|
991
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
992
|
+
|
|
993
|
+
assert(result.chunks.length === 1, 'Claude: 4 lines -> 1 chunk');
|
|
994
|
+
assert(result.chunks[0].messages.length === 4, `Claude: handles bullets/numbers (got ${result.chunks[0].messages.length})`);
|
|
995
|
+
assert(!result.chunks[0].messages[0].text.startsWith('-'), 'Claude: bullet stripped');
|
|
996
|
+
assert(!result.chunks[0].messages[2].text.startsWith('1'), 'Claude: number stripped');
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// --- skips empty lines and header lines ---
|
|
1000
|
+
{
|
|
1001
|
+
const memoriesText = `Claude Memories:
|
|
1002
|
+
|
|
1003
|
+
User prefers dark mode in editors
|
|
1004
|
+
|
|
1005
|
+
User lives in Tokyo`;
|
|
1006
|
+
|
|
1007
|
+
const adapter = new ClaudeAdapter();
|
|
1008
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
1009
|
+
|
|
1010
|
+
assert(result.chunks.length === 1, 'Claude: skips header/blank');
|
|
1011
|
+
assert(result.chunks[0].messages.length === 2, `Claude: only 2 valid lines (got ${result.chunks[0].messages.length})`);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// --- empty input returns error ---
|
|
1015
|
+
{
|
|
1016
|
+
const adapter = new ClaudeAdapter();
|
|
1017
|
+
const result = await adapter.parse({});
|
|
1018
|
+
|
|
1019
|
+
assert(result.chunks.length === 0, 'Claude: no input yields 0 chunks');
|
|
1020
|
+
assert(result.errors.length > 0, 'Claude: no input produces error');
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// --- empty text yields 0 chunks ---
|
|
1024
|
+
{
|
|
1025
|
+
const adapter = new ClaudeAdapter();
|
|
1026
|
+
const result = await adapter.parse({ content: '' });
|
|
1027
|
+
|
|
1028
|
+
assert(result.chunks.length === 0, 'Claude: empty text yields 0 chunks');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// --- skips very short memories ---
|
|
1032
|
+
{
|
|
1033
|
+
const memoriesText = `ok
|
|
1034
|
+
ab
|
|
1035
|
+
User prefers TypeScript over JavaScript`;
|
|
1036
|
+
|
|
1037
|
+
const adapter = new ClaudeAdapter();
|
|
1038
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
1039
|
+
|
|
1040
|
+
// "ok" and "ab" are < 3 chars after validation
|
|
1041
|
+
assert(result.chunks.length === 1, 'Claude: has 1 chunk');
|
|
1042
|
+
assert(result.chunks[0].messages.length === 1, `Claude: skips short memories (got ${result.chunks[0].messages.length})`);
|
|
1043
|
+
assert(result.chunks[0].messages[0].text.includes('TypeScript'), 'Claude: valid memory kept');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// --- large memory list chunks correctly ---
|
|
1047
|
+
{
|
|
1048
|
+
const lines = Array.from({ length: 25 }, (_, i) => `Claude memory item number ${i + 1} about their workflow`);
|
|
1049
|
+
const memoriesText = lines.join('\n');
|
|
1050
|
+
|
|
1051
|
+
const adapter = new ClaudeAdapter();
|
|
1052
|
+
const result = await adapter.parse({ content: memoriesText });
|
|
1053
|
+
|
|
1054
|
+
assert(result.chunks.length === 2, `Claude: 25 lines -> 2 chunks (got ${result.chunks.length})`);
|
|
1055
|
+
assert(result.chunks[0].messages.length === 20, `Claude: first chunk has 20 (got ${result.chunks[0].messages.length})`);
|
|
1056
|
+
assert(result.chunks[1].messages.length === 5, `Claude: second chunk has 5 (got ${result.chunks[1].messages.length})`);
|
|
1057
|
+
assert(result.totalMessages === 25, `Claude: totalMessages is 25 (got ${result.totalMessages})`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
540
1060
|
// =========================================================================
|
|
541
1061
|
// getAdapter factory
|
|
542
1062
|
// =========================================================================
|
|
@@ -550,6 +1070,12 @@ async function runTests(): Promise<void> {
|
|
|
550
1070
|
|
|
551
1071
|
const mcp = getAdapter('mcp-memory');
|
|
552
1072
|
assert(mcp instanceof MCPMemoryAdapter, 'getAdapter("mcp-memory") returns MCPMemoryAdapter');
|
|
1073
|
+
|
|
1074
|
+
const chatgpt = getAdapter('chatgpt');
|
|
1075
|
+
assert(chatgpt instanceof ChatGPTAdapter, 'getAdapter("chatgpt") returns ChatGPTAdapter');
|
|
1076
|
+
|
|
1077
|
+
const claude = getAdapter('claude');
|
|
1078
|
+
assert(claude instanceof ClaudeAdapter, 'getAdapter("claude") returns ClaudeAdapter');
|
|
553
1079
|
}
|
|
554
1080
|
|
|
555
1081
|
// --- unknown source throws ---
|
|
@@ -569,6 +1095,8 @@ async function runTests(): Promise<void> {
|
|
|
569
1095
|
const msg = (e as Error).message;
|
|
570
1096
|
assert(msg.includes('mem0'), 'getAdapter error lists "mem0" as valid source');
|
|
571
1097
|
assert(msg.includes('mcp-memory'), 'getAdapter error lists "mcp-memory" as valid source');
|
|
1098
|
+
assert(msg.includes('chatgpt'), 'getAdapter error lists "chatgpt" as valid source');
|
|
1099
|
+
assert(msg.includes('claude'), 'getAdapter error lists "claude" as valid source');
|
|
572
1100
|
}
|
|
573
1101
|
}
|
|
574
1102
|
|
package/import-adapters/index.ts
CHANGED
|
@@ -2,15 +2,21 @@ export { BaseImportAdapter } from './base-adapter.js';
|
|
|
2
2
|
export * from './types.js';
|
|
3
3
|
export { Mem0Adapter } from './mem0-adapter.js';
|
|
4
4
|
export { MCPMemoryAdapter } from './mcp-memory-adapter.js';
|
|
5
|
+
export { ChatGPTAdapter } from './chatgpt-adapter.js';
|
|
6
|
+
export { ClaudeAdapter } from './claude-adapter.js';
|
|
5
7
|
|
|
6
8
|
import type { ImportSource } from './types.js';
|
|
7
9
|
import { Mem0Adapter } from './mem0-adapter.js';
|
|
8
10
|
import { MCPMemoryAdapter } from './mcp-memory-adapter.js';
|
|
11
|
+
import { ChatGPTAdapter } from './chatgpt-adapter.js';
|
|
12
|
+
import { ClaudeAdapter } from './claude-adapter.js';
|
|
9
13
|
import type { BaseImportAdapter } from './base-adapter.js';
|
|
10
14
|
|
|
11
15
|
const ADAPTERS: Partial<Record<ImportSource, () => BaseImportAdapter>> = {
|
|
12
16
|
'mem0': () => new Mem0Adapter(),
|
|
13
17
|
'mcp-memory': () => new MCPMemoryAdapter(),
|
|
18
|
+
'chatgpt': () => new ChatGPTAdapter(),
|
|
19
|
+
'claude': () => new ClaudeAdapter(),
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
export function getAdapter(source: ImportSource): BaseImportAdapter {
|
|
@@ -67,7 +67,7 @@ export class MCPMemoryAdapter extends BaseImportAdapter {
|
|
|
67
67
|
content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
68
68
|
} catch (e) {
|
|
69
69
|
errors.push(`Failed to read file: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
70
|
-
return { facts: [], warnings, errors };
|
|
70
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
|
|
71
71
|
}
|
|
72
72
|
} else {
|
|
73
73
|
// Try default MCP memory path
|
|
@@ -80,7 +80,7 @@ export class MCPMemoryAdapter extends BaseImportAdapter {
|
|
|
80
80
|
'No content, file_path, or file at default path (~/.mcp-memory/memory.jsonl). ' +
|
|
81
81
|
'Provide the memory.jsonl content or file path.',
|
|
82
82
|
);
|
|
83
|
-
return { facts: [], warnings, errors };
|
|
83
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -170,6 +170,8 @@ export class MCPMemoryAdapter extends BaseImportAdapter {
|
|
|
170
170
|
|
|
171
171
|
return {
|
|
172
172
|
facts,
|
|
173
|
+
chunks: [],
|
|
174
|
+
totalMessages: 0,
|
|
173
175
|
warnings,
|
|
174
176
|
errors,
|
|
175
177
|
source_metadata: {
|
|
@@ -81,7 +81,7 @@ export class Mem0Adapter extends BaseImportAdapter {
|
|
|
81
81
|
);
|
|
82
82
|
} else {
|
|
83
83
|
errors.push('Mem0 import requires either content (export file) or api_key');
|
|
84
|
-
return { facts: [], warnings, errors };
|
|
84
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (onProgress) {
|
|
@@ -110,7 +110,7 @@ export class Mem0Adapter extends BaseImportAdapter {
|
|
|
110
110
|
warnings.push(`${invalidCount} memories had invalid/empty text and were skipped`);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
return { facts, warnings, errors, source_metadata: { total_from_source: memories.length } };
|
|
113
|
+
return { facts, chunks: [], totalMessages: 0, warnings, errors, source_metadata: { total_from_source: memories.length } };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|