@remnic/import-weclone 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joshua Warren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # @remnic/import-weclone
2
+
3
+ Import [WeClone](https://github.com/xming521/weclone)-preprocessed chat exports
4
+ (Telegram, WhatsApp, Discord, Slack) into Remnic to bootstrap a memory store
5
+ instantly, rather than waiting for organic memory accumulation through daily AI
6
+ tool usage.
7
+
8
+ Part of [Remnic](https://github.com/joshuaswarren/remnic), the universal memory
9
+ layer for AI agents.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @remnic/import-weclone
15
+ ```
16
+
17
+ Importing this package registers the WeClone adapter with the core
18
+ bulk-import registry as a side effect, so `openclaw engram bulk-import
19
+ --source weclone ...` works without any explicit setup. Tests that call
20
+ `clearBulkImportSources()` can re-register via
21
+ `ensureWecloneImportAdapterRegistered()`.
22
+
23
+ ## Why WeClone?
24
+
25
+ WeClone already handles the hard parts of chat ingestion:
26
+
27
+ - Platform-specific export parsing (Telegram JSON, WhatsApp, Discord, Slack)
28
+ - PII detection and redaction (Microsoft Presidio)
29
+ - Message deduplication and basic cleanup
30
+
31
+ Rather than duplicate that pipeline, this package consumes WeClone's
32
+ preprocessed JSON directly and maps it into the bulk-import contract defined by
33
+ `@remnic/core`.
34
+
35
+ ## Pipeline
36
+
37
+ ```
38
+ WeClone preprocessed JSON
39
+ |
40
+ v
41
+ +---------------------------------+
42
+ | parseWeCloneExport | parser.ts
43
+ | - platform resolution |
44
+ | - role inference (self/bot) |
45
+ | - schema validation |
46
+ +---------------+-----------------+
47
+ | BulkImportSource
48
+ v
49
+ +---------------------------------+
50
+ | groupIntoThreads | threader.ts
51
+ | - sort by timestamp |
52
+ | - split on >30 min time gaps |
53
+ | - merge via reply chains |
54
+ +---------------+-----------------+
55
+ | ThreadGroup[]
56
+ v
57
+ +---------------------------------+
58
+ | mapParticipants | participant.ts
59
+ | - count messages per sender |
60
+ | - classify self/frequent/ |
61
+ | occasional |
62
+ +---------------+-----------------+
63
+ | ParticipantEntity[]
64
+ v
65
+ +---------------------------------+
66
+ | chunkThreads | chunker.ts
67
+ | - split long threads with |
68
+ | overlap for context |
69
+ +---------------+-----------------+
70
+ | ImportTurn[][]
71
+ v
72
+ +---------------------------------+
73
+ | runBulkImportPipeline | @remnic/core
74
+ | - batch extraction |
75
+ | - dedup against existing |
76
+ | - store with trustLevel=import |
77
+ +---------------------------------+
78
+ ```
79
+
80
+ ## CLI usage
81
+
82
+ The importer is exposed via the `engram bulk-import` subcommand in
83
+ `@remnic/core`:
84
+
85
+ ```bash
86
+ # Dry-run: parse, validate, and report counts without persisting
87
+ openclaw engram bulk-import \
88
+ --source weclone \
89
+ --file ./preprocessed_telegram.json \
90
+ --platform telegram \
91
+ --dry-run
92
+
93
+ # Persist: run extraction over the export and write memories to disk
94
+ openclaw engram bulk-import \
95
+ --source weclone \
96
+ --file ./preprocessed_telegram.json \
97
+ --platform telegram
98
+
99
+ # Strict mode: fail on any invalid message instead of skipping
100
+ openclaw engram bulk-import \
101
+ --source weclone \
102
+ --file ./preprocessed_telegram.json \
103
+ --platform telegram \
104
+ --strict
105
+ ```
106
+
107
+ Persistence is wired through the Remnic orchestrator's extraction pipeline.
108
+ Each batch is buffered and extracted the same way an organic conversation
109
+ would be; memories land under the orchestrator's default-namespace root
110
+ (`memoryDir/facts/` by default). Use `--dry-run` to validate an export
111
+ before committing to the extraction cost. Per-invocation namespace
112
+ override for bulk-import writes is not yet wired — see the note in
113
+ [`docs/import-export.md`](../../docs/import-export.md).
114
+
115
+ ## Programmatic usage
116
+
117
+ ```ts
118
+ import { readFileSync } from "node:fs";
119
+ import {
120
+ parseWeCloneExport,
121
+ groupIntoThreads,
122
+ mapParticipants,
123
+ chunkThreads,
124
+ // Side-effect import also registers the bulk-import adapter; see `index.ts`.
125
+ wecloneImportAdapter,
126
+ } from "@remnic/import-weclone";
127
+ import {
128
+ getBulkImportSource,
129
+ runBulkImportPipeline,
130
+ } from "@remnic/core";
131
+
132
+ // The adapter is registered automatically on import. Look it up:
133
+ const adapter = getBulkImportSource("weclone");
134
+
135
+ // 1) Parse a WeClone-preprocessed export.
136
+ const raw = JSON.parse(readFileSync("./export.json", "utf8"));
137
+ const source = parseWeCloneExport(raw, { platform: "telegram" });
138
+
139
+ // 2) Pre-process into threads, participants, chunks.
140
+ const threads = groupIntoThreads(source.turns);
141
+ const participants = mapParticipants(source.turns);
142
+ const chunks = chunkThreads(threads, { maxTurnsPerChunk: 20 });
143
+
144
+ // 3) Run the bulk-import pipeline. In dryRun mode `processBatch` is never
145
+ // called; for real persistence the host CLI supplies an `ingestBatch`
146
+ // callback that wraps `orchestrator.ingestBulkImportBatch` (see
147
+ // `openclaw engram bulk-import`).
148
+ const result = await runBulkImportPipeline(
149
+ source,
150
+ { batchSize: 20, dryRun: true, dedup: true, trustLevel: "import" },
151
+ async () => ({ memoriesCreated: 0, duplicatesSkipped: 0 }),
152
+ );
153
+ ```
154
+
155
+ ## Supported platforms
156
+
157
+ | Platform | `--platform` value |
158
+ |-----------|--------------------|
159
+ | Telegram | `telegram` |
160
+ | WhatsApp | `whatsapp` |
161
+ | Discord | `discord` |
162
+ | Slack | `slack` |
163
+
164
+ The parser defaults to `telegram` when neither `options.platform` nor an
165
+ export-level `platform` field is provided. Unknown platforms are rejected.
166
+
167
+ ## Input schema
168
+
169
+ The parser accepts either:
170
+
171
+ 1. A wrapper object:
172
+
173
+ ```json
174
+ {
175
+ "platform": "telegram",
176
+ "export_date": "2025-01-10T00:00:00.000Z",
177
+ "messages": [
178
+ {
179
+ "sender": "Alice",
180
+ "text": "hello",
181
+ "timestamp": "2025-01-10T08:00:00.000Z",
182
+ "message_id": "m-001",
183
+ "reply_to_id": "m-000"
184
+ }
185
+ ]
186
+ }
187
+ ```
188
+
189
+ 2. A raw array of messages (platform defaults to `telegram`).
190
+
191
+ Required per-message fields: `sender`, `text`, `timestamp` (ISO-8601). Optional:
192
+ `message_id`, `reply_to_id`.
193
+
194
+ ## Role inference
195
+
196
+ Each `sender` is mapped to one of the `ImportTurn` roles:
197
+
198
+ - `user` - the "self" sender (first non-bot sender encountered, or override
199
+ via `selfSender`).
200
+ - `assistant` - any sender matching a bot heuristic (`bot`, `assistant`, `ai`,
201
+ `chatgpt`, `gpt`, `claude`, `copilot`, `llama`) or listed in
202
+ `assistantSenders`.
203
+ - `other` - everyone else.
204
+
205
+ The heuristic uses word-boundary matching so human names that happen to contain
206
+ substrings like `ai` (e.g. `Aidan`, `Caitlin`) are not mis-classified.
207
+
208
+ ## Design notes
209
+
210
+ - **Imported memories are tagged with `trustLevel: "import"`** in the
211
+ pipeline options so downstream ranking/dedup can reason about their
212
+ provenance. The confidence-weighted ranking discount described in the
213
+ original issue is tracked as a follow-up — today the flag is plumbed but
214
+ not consumed by the recall ranker.
215
+ - **Threads are conversation boundaries, not days.** The default 30-minute
216
+ gap with reply-chain merging produces coherent extraction batches without
217
+ relying on calendar boundaries.
218
+ - **Entity bootstrapping is best-effort.** `mapParticipants` emits
219
+ lightweight `ParticipantEntity` records; the core entity graph is populated
220
+ as the pipeline processes chunks.
221
+ - **Idempotent re-imports.** The pipeline's dedup pass (when wired)
222
+ fingerprint-matches against existing memories, so re-running the importer
223
+ after appending new messages is safe.
224
+
225
+ ## License
226
+
227
+ MIT. See [LICENSE](https://github.com/joshuaswarren/remnic/blob/main/LICENSE).
@@ -0,0 +1,145 @@
1
+ import { BulkImportSourceAdapter, ImportTurn, BulkImportSource } from '@remnic/core';
2
+
3
+ /**
4
+ * Adapter that conforms to `BulkImportSourceAdapter` from `@remnic/core`.
5
+ * Delegates parsing to `parseWeCloneExport`.
6
+ */
7
+ declare const wecloneImportAdapter: BulkImportSourceAdapter;
8
+
9
+ /**
10
+ * Extended ImportTurn that carries the original WeClone message_id so that the
11
+ * threader can resolve reply chains (core's ImportTurn has no messageId field).
12
+ */
13
+ interface WeCloneImportTurn extends ImportTurn {
14
+ messageId?: string;
15
+ }
16
+ type WeClonePlatform = "telegram" | "whatsapp" | "discord" | "slack";
17
+ interface WeClonePreprocessedMessage {
18
+ sender: string;
19
+ text: string;
20
+ timestamp: string;
21
+ reply_to_id?: string;
22
+ message_id?: string;
23
+ }
24
+ interface WeClonePreprocessedExport {
25
+ platform: WeClonePlatform;
26
+ messages: WeClonePreprocessedMessage[];
27
+ export_date?: string;
28
+ }
29
+ interface ParseOptions {
30
+ /** Override the platform field from the export. */
31
+ platform?: WeClonePlatform;
32
+ /** When true, throw on any validation failure instead of skipping. */
33
+ strict?: boolean;
34
+ /**
35
+ * Sender name that identifies the user (i.e. "self").
36
+ * Defaults to the first sender encountered in the messages array.
37
+ */
38
+ selfSender?: string;
39
+ /**
40
+ * Sender names that should be treated as "assistant" (bot/AI).
41
+ * Messages from other senders are assigned the "other" role.
42
+ */
43
+ assistantSenders?: string[];
44
+ }
45
+ /**
46
+ * Parse a WeClone preprocessed export into a `BulkImportSource`.
47
+ *
48
+ * Accepts either:
49
+ * - A `WeClonePreprocessedExport` object with `messages` array
50
+ * - A raw array of `WeClonePreprocessedMessage` items
51
+ */
52
+ declare function parseWeCloneExport(input: unknown, options?: ParseOptions): BulkImportSource;
53
+
54
+ interface ThreadGroup {
55
+ turns: ImportTurn[];
56
+ threadId: string;
57
+ startTime: string;
58
+ endTime: string;
59
+ }
60
+ interface ThreaderOptions {
61
+ /** Maximum time gap (ms) before starting a new thread. Default: 30 minutes. */
62
+ gapThresholdMs?: number;
63
+ /** Minimum number of messages for a thread to be kept. Default: 2. */
64
+ minThreadSize?: number;
65
+ }
66
+ /**
67
+ * Group an array of import turns into conversation threads.
68
+ *
69
+ * Algorithm (two-pass):
70
+ * 1. Sort turns by timestamp.
71
+ * 2. Split into initial thread segments by time gap.
72
+ * 3. Merge segments linked by reply chains (if a turn's `replyToId`
73
+ * references a `participantId` in a different segment, merge them).
74
+ * 4. Filter out threads smaller than `minThreadSize`.
75
+ * 5. Assign sequential thread IDs.
76
+ */
77
+ declare function groupIntoThreads(turns: ImportTurn[], options?: ThreaderOptions): ThreadGroup[];
78
+
79
+ interface ParticipantEntity {
80
+ id: string;
81
+ name: string;
82
+ messageCount: number;
83
+ firstSeen: string;
84
+ lastSeen: string;
85
+ /** Inferred relationship: "self", "frequent", or "occasional". */
86
+ relationship?: string;
87
+ }
88
+ /**
89
+ * Build participant entity records from an array of import turns.
90
+ *
91
+ * - The participant with the most messages is tagged "self".
92
+ * - Participants with >10% of total messages are "frequent".
93
+ * - Everyone else is "occasional".
94
+ * - Turns without `participantId` are silently skipped.
95
+ */
96
+ declare function mapParticipants(turns: ImportTurn[]): ParticipantEntity[];
97
+
98
+ interface ChunkOptions {
99
+ /** Maximum turns per chunk. Default: 20. */
100
+ maxTurnsPerChunk?: number;
101
+ /** Number of overlapping turns between consecutive chunks. Default: 2. */
102
+ overlapTurns?: number;
103
+ }
104
+ /**
105
+ * Split thread groups into extraction-sized chunks.
106
+ *
107
+ * - Threads shorter than `maxTurnsPerChunk` stay as a single chunk.
108
+ * - Longer threads are split with `overlapTurns` overlap at boundaries
109
+ * to preserve conversational context.
110
+ */
111
+ declare function chunkThreads(threads: ThreadGroup[], options?: ChunkOptions): ImportTurn[][];
112
+
113
+ interface ImportProgress {
114
+ phase: "parsing" | "threading" | "chunking" | "extracting" | "complete";
115
+ totalMessages: number;
116
+ threadsFound: number;
117
+ chunksCreated: number;
118
+ chunksProcessed: number;
119
+ memoriesExtracted: number;
120
+ duplicatesSkipped: number;
121
+ entitiesCreated: number;
122
+ elapsed: number;
123
+ }
124
+ type ProgressCallback = (progress: ImportProgress) => void;
125
+ /**
126
+ * Create a progress tracker that maintains state and optionally notifies
127
+ * a callback on every update.
128
+ */
129
+ declare function createProgressTracker(callback?: ProgressCallback): {
130
+ update(partial: Partial<ImportProgress>): void;
131
+ snapshot(): ImportProgress;
132
+ };
133
+
134
+ /**
135
+ * Idempotently register the WeClone adapter with the core bulk-import
136
+ * registry. Callable multiple times without throwing (CLAUDE.md #13:
137
+ * secondary calls must not crash host processes that pre-register the
138
+ * adapter for test fixtures).
139
+ *
140
+ * Returns true when the adapter was newly registered, false when an adapter
141
+ * with the same name already exists.
142
+ */
143
+ declare function ensureWecloneImportAdapterRegistered(): boolean;
144
+
145
+ export { type ChunkOptions, type ImportProgress, type ParseOptions, type ParticipantEntity, type ProgressCallback, type ThreadGroup, type ThreaderOptions, type WeCloneImportTurn, type WeClonePlatform, type WeClonePreprocessedExport, type WeClonePreprocessedMessage, chunkThreads, createProgressTracker, ensureWecloneImportAdapterRegistered, groupIntoThreads, mapParticipants, parseWeCloneExport, wecloneImportAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,452 @@
1
+ // openclaw-engram: Local-first memory plugin
2
+
3
+ // src/index.ts
4
+ import {
5
+ getBulkImportSource,
6
+ registerBulkImportSource
7
+ } from "@remnic/core";
8
+
9
+ // src/parser.ts
10
+ import { validateImportTurn, parseIsoTimestamp } from "@remnic/core";
11
+ var VALID_PLATFORMS = /* @__PURE__ */ new Set([
12
+ "telegram",
13
+ "whatsapp",
14
+ "discord",
15
+ "slack"
16
+ ]);
17
+ var AI_SENDER_HINTS = [
18
+ "bot",
19
+ "assistant",
20
+ "ai",
21
+ "chatgpt",
22
+ "gpt",
23
+ "claude",
24
+ "copilot",
25
+ "llama"
26
+ ];
27
+ var AI_SENDER_PATTERNS = AI_SENDER_HINTS.map(
28
+ (hint) => new RegExp(`\\b${hint}\\b`, "i")
29
+ );
30
+ function looksLikeBot(sender) {
31
+ for (const pattern of AI_SENDER_PATTERNS) {
32
+ if (pattern.test(sender)) return true;
33
+ }
34
+ return false;
35
+ }
36
+ function resolveRole(sender, selfSender, assistantSenders) {
37
+ if (sender === selfSender) return "user";
38
+ if (assistantSenders.has(sender) || looksLikeBot(sender)) return "assistant";
39
+ return "other";
40
+ }
41
+ function isNonEmptyString(value) {
42
+ return typeof value === "string" && value.trim().length > 0;
43
+ }
44
+ function isValidMessage(msg) {
45
+ if (!msg || typeof msg !== "object") return false;
46
+ const m = msg;
47
+ return isNonEmptyString(m.sender) && isNonEmptyString(m.text) && isNonEmptyString(m.timestamp);
48
+ }
49
+ function parseWeCloneExport(input, options) {
50
+ if (!input || typeof input !== "object") {
51
+ throw new Error(
52
+ "WeClone import: input must be a non-null object or array"
53
+ );
54
+ }
55
+ const strict = options?.strict === true;
56
+ let rawMessages;
57
+ let platformStr;
58
+ let exportDate;
59
+ if (Array.isArray(input)) {
60
+ rawMessages = input;
61
+ } else {
62
+ const obj = input;
63
+ if (!Array.isArray(obj.messages)) {
64
+ throw new Error(
65
+ "WeClone import: input must have a 'messages' array"
66
+ );
67
+ }
68
+ rawMessages = obj.messages;
69
+ platformStr = typeof obj.platform === "string" ? obj.platform : void 0;
70
+ exportDate = typeof obj.export_date === "string" ? obj.export_date : void 0;
71
+ }
72
+ if (rawMessages.length === 0) {
73
+ throw new Error("WeClone import: messages array must not be empty");
74
+ }
75
+ const platform = resolvePlatform(
76
+ options?.platform,
77
+ platformStr
78
+ );
79
+ const assistantSet = new Set(options?.assistantSenders ?? []);
80
+ let inferredSelfSender = "";
81
+ if (options?.selfSender === void 0) {
82
+ for (const m of rawMessages) {
83
+ if (!isValidMessage(m)) continue;
84
+ if (assistantSet.has(m.sender)) continue;
85
+ if (looksLikeBot(m.sender)) continue;
86
+ inferredSelfSender = m.sender;
87
+ break;
88
+ }
89
+ if (inferredSelfSender === "") {
90
+ const firstValid = rawMessages.find(isValidMessage);
91
+ inferredSelfSender = firstValid ? firstValid.sender : "";
92
+ }
93
+ }
94
+ const selfSender = options?.selfSender ?? inferredSelfSender;
95
+ const turns = [];
96
+ const warnings = [];
97
+ for (let i = 0; i < rawMessages.length; i += 1) {
98
+ const raw = rawMessages[i];
99
+ if (!isValidMessage(raw)) {
100
+ const msg = `WeClone import: message at index ${i} is invalid (must have sender, text, timestamp as non-empty strings)`;
101
+ if (strict) throw new Error(msg);
102
+ warnings.push(msg);
103
+ continue;
104
+ }
105
+ const turn = {
106
+ role: resolveRole(raw.sender, selfSender, assistantSet),
107
+ content: raw.text,
108
+ timestamp: raw.timestamp,
109
+ participantId: raw.sender,
110
+ participantName: raw.sender,
111
+ ...raw.reply_to_id != null ? { replyToId: raw.reply_to_id } : {},
112
+ ...raw.message_id != null ? { messageId: raw.message_id } : {}
113
+ };
114
+ const issues = validateImportTurn(turn, i);
115
+ if (issues.length > 0) {
116
+ const detail = issues.map((iss) => iss.message).join("; ");
117
+ if (strict) {
118
+ throw new Error(
119
+ `WeClone import: turn at index ${i} failed validation: ${detail}`
120
+ );
121
+ }
122
+ warnings.push(
123
+ `WeClone import: skipping message at index ${i}: ${detail}`
124
+ );
125
+ continue;
126
+ }
127
+ turns.push(turn);
128
+ }
129
+ if (turns.length === 0) {
130
+ throw new Error(
131
+ "WeClone import: no valid turns after parsing all messages"
132
+ );
133
+ }
134
+ if (warnings.length > 0) {
135
+ for (const w of warnings) {
136
+ console.warn(w);
137
+ }
138
+ }
139
+ const timestamps = turns.map((t) => parseIsoTimestamp(t.timestamp)).filter((ts) => ts !== null);
140
+ timestamps.sort((a, b) => a - b);
141
+ const from = timestamps.length > 0 ? new Date(timestamps[0]).toISOString() : turns[0].timestamp;
142
+ const to = timestamps.length > 0 ? new Date(timestamps[timestamps.length - 1]).toISOString() : turns[turns.length - 1].timestamp;
143
+ return {
144
+ turns,
145
+ metadata: {
146
+ source: `weclone-${platform}`,
147
+ exportDate: exportDate ?? (/* @__PURE__ */ new Date()).toISOString(),
148
+ messageCount: turns.length,
149
+ dateRange: { from, to }
150
+ }
151
+ };
152
+ }
153
+ function resolvePlatform(optionsPlatform, exportPlatform) {
154
+ if (optionsPlatform !== void 0) {
155
+ if (!VALID_PLATFORMS.has(optionsPlatform)) {
156
+ throw new Error(
157
+ `WeClone import: invalid platform '${optionsPlatform}'. Valid: ${[...VALID_PLATFORMS].join(", ")}`
158
+ );
159
+ }
160
+ return optionsPlatform;
161
+ }
162
+ if (exportPlatform !== void 0) {
163
+ if (!VALID_PLATFORMS.has(exportPlatform)) {
164
+ throw new Error(
165
+ `WeClone import: invalid platform '${exportPlatform}' in export data. Valid: ${[...VALID_PLATFORMS].join(", ")}`
166
+ );
167
+ }
168
+ return exportPlatform;
169
+ }
170
+ return "telegram";
171
+ }
172
+
173
+ // src/adapter.ts
174
+ var wecloneImportAdapter = {
175
+ name: "weclone",
176
+ parse(input, options) {
177
+ const parseOpts = options ? {
178
+ strict: options.strict,
179
+ ...options.platform !== void 0 ? { platform: options.platform } : {}
180
+ } : void 0;
181
+ return parseWeCloneExport(input, parseOpts);
182
+ }
183
+ };
184
+
185
+ // src/threader.ts
186
+ import { parseIsoTimestamp as parseIsoTimestamp2 } from "@remnic/core";
187
+ var DEFAULT_GAP_THRESHOLD_MS = 18e5;
188
+ var DEFAULT_MIN_THREAD_SIZE = 2;
189
+ function groupIntoThreads(turns, options) {
190
+ if (!turns || turns.length === 0) return [];
191
+ const gapMs = options?.gapThresholdMs ?? DEFAULT_GAP_THRESHOLD_MS;
192
+ const minSize = options?.minThreadSize ?? DEFAULT_MIN_THREAD_SIZE;
193
+ if (options?.gapThresholdMs !== void 0 && gapMs <= 0) {
194
+ throw new Error(
195
+ `gapThresholdMs must be positive, received ${gapMs}`
196
+ );
197
+ }
198
+ const sorted = [...turns].sort((a, b) => {
199
+ const tsA = parseIsoTimestamp2(a.timestamp) ?? 0;
200
+ const tsB = parseIsoTimestamp2(b.timestamp) ?? 0;
201
+ if (tsA !== tsB) return tsA - tsB;
202
+ return 0;
203
+ });
204
+ const segments = [];
205
+ let currentSegment = [];
206
+ let prevTs = null;
207
+ for (const turn of sorted) {
208
+ const ts = parseIsoTimestamp2(turn.timestamp) ?? 0;
209
+ if (prevTs !== null && ts - prevTs > gapMs) {
210
+ if (currentSegment.length > 0) {
211
+ segments.push(currentSegment);
212
+ currentSegment = [];
213
+ }
214
+ }
215
+ currentSegment.push(turn);
216
+ prevTs = ts;
217
+ }
218
+ if (currentSegment.length > 0) {
219
+ segments.push(currentSegment);
220
+ }
221
+ const parent = segments.map((_, i) => i);
222
+ function find(x) {
223
+ while (parent[x] !== x) {
224
+ parent[x] = parent[parent[x]];
225
+ x = parent[x];
226
+ }
227
+ return x;
228
+ }
229
+ function union(a, b) {
230
+ const ra = find(a);
231
+ const rb = find(b);
232
+ if (ra !== rb) {
233
+ if (ra < rb) {
234
+ parent[rb] = ra;
235
+ } else {
236
+ parent[ra] = rb;
237
+ }
238
+ }
239
+ }
240
+ const idToSegment = /* @__PURE__ */ new Map();
241
+ for (let segIdx = 0; segIdx < segments.length; segIdx += 1) {
242
+ for (let turnIdx = 0; turnIdx < segments[segIdx].length; turnIdx += 1) {
243
+ const turn = segments[segIdx][turnIdx];
244
+ if (turn.messageId) {
245
+ idToSegment.set(turn.messageId, segIdx);
246
+ }
247
+ }
248
+ }
249
+ for (let segIdx = 0; segIdx < segments.length; segIdx += 1) {
250
+ for (const turn of segments[segIdx]) {
251
+ if (turn.replyToId != null) {
252
+ const targetSeg = idToSegment.get(turn.replyToId);
253
+ if (targetSeg !== void 0) {
254
+ union(segIdx, targetSeg);
255
+ }
256
+ }
257
+ }
258
+ }
259
+ const mergedMap = /* @__PURE__ */ new Map();
260
+ for (let segIdx = 0; segIdx < segments.length; segIdx += 1) {
261
+ const root = find(segIdx);
262
+ const existing = mergedMap.get(root);
263
+ if (existing) {
264
+ existing.push(...segments[segIdx]);
265
+ } else {
266
+ mergedMap.set(root, [...segments[segIdx]]);
267
+ }
268
+ }
269
+ const result = [];
270
+ let threadSeq = 1;
271
+ const sortedRoots = [...mergedMap.keys()].sort((a, b) => a - b);
272
+ for (const root of sortedRoots) {
273
+ const threadTurns = mergedMap.get(root);
274
+ if (threadTurns.length < minSize) continue;
275
+ threadTurns.sort((a, b) => {
276
+ const tsA = parseIsoTimestamp2(a.timestamp) ?? 0;
277
+ const tsB = parseIsoTimestamp2(b.timestamp) ?? 0;
278
+ return tsA - tsB;
279
+ });
280
+ result.push({
281
+ turns: threadTurns,
282
+ threadId: `thread-${String(threadSeq).padStart(4, "0")}`,
283
+ startTime: threadTurns[0].timestamp,
284
+ endTime: threadTurns[threadTurns.length - 1].timestamp
285
+ });
286
+ threadSeq += 1;
287
+ }
288
+ return result;
289
+ }
290
+
291
+ // src/participant.ts
292
+ import { parseIsoTimestamp as parseIsoTimestamp3 } from "@remnic/core";
293
+ var FREQUENT_THRESHOLD = 0.1;
294
+ function mapParticipants(turns) {
295
+ if (!turns || turns.length === 0) return [];
296
+ const stats = /* @__PURE__ */ new Map();
297
+ for (const turn of turns) {
298
+ const id = turn.participantId;
299
+ if (!id) continue;
300
+ const ts = parseIsoTimestamp3(turn.timestamp) ?? 0;
301
+ const existing = stats.get(id);
302
+ if (existing) {
303
+ existing.count += 1;
304
+ if (ts < existing.firstTs) {
305
+ existing.firstTs = ts;
306
+ existing.firstRaw = turn.timestamp;
307
+ }
308
+ if (ts > existing.lastTs) {
309
+ existing.lastTs = ts;
310
+ existing.lastRaw = turn.timestamp;
311
+ }
312
+ } else {
313
+ stats.set(id, {
314
+ name: turn.participantName ?? id,
315
+ count: 1,
316
+ firstTs: ts,
317
+ lastTs: ts,
318
+ firstRaw: turn.timestamp,
319
+ lastRaw: turn.timestamp
320
+ });
321
+ }
322
+ }
323
+ if (stats.size === 0) return [];
324
+ let topId = "";
325
+ let topCount = 0;
326
+ for (const [id, s] of stats.entries()) {
327
+ if (s.count > topCount) {
328
+ topCount = s.count;
329
+ topId = id;
330
+ }
331
+ }
332
+ const totalMessages = turns.filter((t) => !!t.participantId).length;
333
+ const result = [];
334
+ for (const [id, s] of stats.entries()) {
335
+ let relationship;
336
+ if (id === topId) {
337
+ relationship = "self";
338
+ } else if (s.count / totalMessages > FREQUENT_THRESHOLD) {
339
+ relationship = "frequent";
340
+ } else {
341
+ relationship = "occasional";
342
+ }
343
+ result.push({
344
+ id,
345
+ name: s.name,
346
+ messageCount: s.count,
347
+ firstSeen: s.firstRaw,
348
+ lastSeen: s.lastRaw,
349
+ relationship
350
+ });
351
+ }
352
+ result.sort((a, b) => {
353
+ if (b.messageCount !== a.messageCount) return b.messageCount - a.messageCount;
354
+ return a.id.localeCompare(b.id);
355
+ });
356
+ return result;
357
+ }
358
+
359
+ // src/chunker.ts
360
+ var DEFAULT_MAX_TURNS = 20;
361
+ var DEFAULT_OVERLAP = 2;
362
+ function chunkThreads(threads, options) {
363
+ if (!threads || threads.length === 0) return [];
364
+ const maxTurns = options?.maxTurnsPerChunk ?? DEFAULT_MAX_TURNS;
365
+ const overlap = options?.overlapTurns ?? DEFAULT_OVERLAP;
366
+ if (maxTurns <= 0) {
367
+ throw new Error(
368
+ `maxTurnsPerChunk must be positive, received ${maxTurns}`
369
+ );
370
+ }
371
+ if (overlap < 0) {
372
+ throw new Error(
373
+ `overlapTurns must be non-negative, received ${overlap}`
374
+ );
375
+ }
376
+ if (overlap >= maxTurns) {
377
+ throw new Error(
378
+ `overlapTurns (${overlap}) must be less than maxTurnsPerChunk (${maxTurns}); otherwise chunks would either never advance or silently clamp step to 1 and massively inflate work`
379
+ );
380
+ }
381
+ const step = Math.max(1, maxTurns - overlap);
382
+ const chunks = [];
383
+ for (const thread of threads) {
384
+ const turns = thread.turns;
385
+ if (turns.length === 0) continue;
386
+ if (turns.length <= maxTurns) {
387
+ chunks.push([...turns]);
388
+ continue;
389
+ }
390
+ for (let start = 0; start < turns.length; start += step) {
391
+ const end = Math.min(start + maxTurns, turns.length);
392
+ chunks.push(turns.slice(start, end));
393
+ if (end === turns.length) break;
394
+ }
395
+ }
396
+ return chunks;
397
+ }
398
+
399
+ // src/progress.ts
400
+ function defaultProgress() {
401
+ return {
402
+ phase: "parsing",
403
+ totalMessages: 0,
404
+ threadsFound: 0,
405
+ chunksCreated: 0,
406
+ chunksProcessed: 0,
407
+ memoriesExtracted: 0,
408
+ duplicatesSkipped: 0,
409
+ entitiesCreated: 0,
410
+ elapsed: 0
411
+ };
412
+ }
413
+ function createProgressTracker(callback) {
414
+ const state = defaultProgress();
415
+ const startTime = Date.now();
416
+ return {
417
+ update(partial) {
418
+ Object.assign(state, partial);
419
+ state.elapsed = Date.now() - startTime;
420
+ if (callback) {
421
+ callback({ ...state });
422
+ }
423
+ },
424
+ snapshot() {
425
+ state.elapsed = Date.now() - startTime;
426
+ return { ...state };
427
+ }
428
+ };
429
+ }
430
+
431
+ // src/index.ts
432
+ function ensureWecloneImportAdapterRegistered() {
433
+ if (getBulkImportSource(wecloneImportAdapter.name) !== void 0) {
434
+ return false;
435
+ }
436
+ registerBulkImportSource(wecloneImportAdapter);
437
+ return true;
438
+ }
439
+ try {
440
+ ensureWecloneImportAdapterRegistered();
441
+ } catch {
442
+ }
443
+ export {
444
+ chunkThreads,
445
+ createProgressTracker,
446
+ ensureWecloneImportAdapterRegistered,
447
+ groupIntoThreads,
448
+ mapParticipants,
449
+ parseWeCloneExport,
450
+ wecloneImportAdapter
451
+ };
452
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/parser.ts","../src/adapter.ts","../src/threader.ts","../src/participant.ts","../src/chunker.ts","../src/progress.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// @remnic/import-weclone — public surface\n// ---------------------------------------------------------------------------\n\nimport {\n getBulkImportSource,\n registerBulkImportSource,\n} from \"@remnic/core\";\n\nimport { wecloneImportAdapter } from \"./adapter.js\";\n\nexport { wecloneImportAdapter } from \"./adapter.js\";\n\nexport {\n parseWeCloneExport,\n type WeCloneImportTurn,\n type WeClonePlatform,\n type WeClonePreprocessedMessage,\n type WeClonePreprocessedExport,\n type ParseOptions,\n} from \"./parser.js\";\n\nexport {\n groupIntoThreads,\n type ThreadGroup,\n type ThreaderOptions,\n} from \"./threader.js\";\n\nexport {\n mapParticipants,\n type ParticipantEntity,\n} from \"./participant.js\";\n\nexport {\n chunkThreads,\n type ChunkOptions,\n} from \"./chunker.js\";\n\nexport {\n createProgressTracker,\n type ImportProgress,\n type ProgressCallback,\n} from \"./progress.js\";\n\n/**\n * Idempotently register the WeClone adapter with the core bulk-import\n * registry. Callable multiple times without throwing (CLAUDE.md #13:\n * secondary calls must not crash host processes that pre-register the\n * adapter for test fixtures).\n *\n * Returns true when the adapter was newly registered, false when an adapter\n * with the same name already exists.\n */\nexport function ensureWecloneImportAdapterRegistered(): boolean {\n if (getBulkImportSource(wecloneImportAdapter.name) !== undefined) {\n return false;\n }\n registerBulkImportSource(wecloneImportAdapter);\n return true;\n}\n\n// Side-effect registration: importing this module registers the adapter.\n// Callers that need to manage registration manually (e.g. tests that call\n// `clearBulkImportSources()`) can re-invoke\n// `ensureWecloneImportAdapterRegistered()` after clearing.\n//\n// The try/catch keeps import-time errors from breaking unrelated callers —\n// the adapter's `parse` is pure, so a failure here would be surprising, but\n// defensive coding keeps CLI startup resilient.\ntry {\n ensureWecloneImportAdapterRegistered();\n} catch {\n // Swallow — explicit callers can re-invoke ensureWecloneImportAdapterRegistered().\n}\n","// ---------------------------------------------------------------------------\n// WeClone preprocessed export parser\n// ---------------------------------------------------------------------------\n\nimport type { BulkImportSource, ImportTurn } from \"@remnic/core\";\nimport { validateImportTurn, parseIsoTimestamp } from \"@remnic/core\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Extended ImportTurn that carries the original WeClone message_id so that the\n * threader can resolve reply chains (core's ImportTurn has no messageId field).\n */\nexport interface WeCloneImportTurn extends ImportTurn {\n messageId?: string;\n}\n\nexport type WeClonePlatform = \"telegram\" | \"whatsapp\" | \"discord\" | \"slack\";\n\nconst VALID_PLATFORMS: ReadonlySet<string> = new Set([\n \"telegram\",\n \"whatsapp\",\n \"discord\",\n \"slack\",\n]);\n\nexport interface WeClonePreprocessedMessage {\n sender: string;\n text: string;\n timestamp: string;\n reply_to_id?: string;\n message_id?: string;\n}\n\nexport interface WeClonePreprocessedExport {\n platform: WeClonePlatform;\n messages: WeClonePreprocessedMessage[];\n export_date?: string;\n}\n\nexport interface ParseOptions {\n /** Override the platform field from the export. */\n platform?: WeClonePlatform;\n /** When true, throw on any validation failure instead of skipping. */\n strict?: boolean;\n /**\n * Sender name that identifies the user (i.e. \"self\").\n * Defaults to the first sender encountered in the messages array.\n */\n selfSender?: string;\n /**\n * Sender names that should be treated as \"assistant\" (bot/AI).\n * Messages from other senders are assigned the \"other\" role.\n */\n assistantSenders?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Hints used to detect bot/AI senders. Each hint is matched as a whole word\n * (`\\b` boundary) so that short tokens like \"ai\" don't false-positive on\n * human names such as \"Aidan\", \"Craig\", or \"Caitlin\".\n */\nconst AI_SENDER_HINTS: readonly string[] = [\n \"bot\",\n \"assistant\",\n \"ai\",\n \"chatgpt\",\n \"gpt\",\n \"claude\",\n \"copilot\",\n \"llama\",\n];\n\n/** Pre-compiled word-boundary regexps (one per hint). */\nconst AI_SENDER_PATTERNS: readonly RegExp[] = AI_SENDER_HINTS.map(\n (hint) => new RegExp(`\\\\b${hint}\\\\b`, \"i\"),\n);\n\nfunction looksLikeBot(sender: string): boolean {\n for (const pattern of AI_SENDER_PATTERNS) {\n if (pattern.test(sender)) return true;\n }\n return false;\n}\n\nfunction resolveRole(\n sender: string,\n selfSender: string,\n assistantSenders: ReadonlySet<string>,\n): ImportTurn[\"role\"] {\n if (sender === selfSender) return \"user\";\n if (assistantSenders.has(sender) || looksLikeBot(sender)) return \"assistant\";\n return \"other\";\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n\nfunction isValidMessage(\n msg: unknown,\n): msg is WeClonePreprocessedMessage {\n if (!msg || typeof msg !== \"object\") return false;\n const m = msg as Record<string, unknown>;\n return isNonEmptyString(m.sender) && isNonEmptyString(m.text) && isNonEmptyString(m.timestamp);\n}\n\n// ---------------------------------------------------------------------------\n// Parser\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a WeClone preprocessed export into a `BulkImportSource`.\n *\n * Accepts either:\n * - A `WeClonePreprocessedExport` object with `messages` array\n * - A raw array of `WeClonePreprocessedMessage` items\n */\nexport function parseWeCloneExport(\n input: unknown,\n options?: ParseOptions,\n): BulkImportSource {\n if (!input || typeof input !== \"object\") {\n throw new Error(\n \"WeClone import: input must be a non-null object or array\",\n );\n }\n\n const strict = options?.strict === true;\n\n // Accept raw array or object-with-messages\n let rawMessages: unknown[];\n let platformStr: string | undefined;\n let exportDate: string | undefined;\n\n if (Array.isArray(input)) {\n rawMessages = input;\n } else {\n const obj = input as Record<string, unknown>;\n if (!Array.isArray(obj.messages)) {\n throw new Error(\n \"WeClone import: input must have a 'messages' array\",\n );\n }\n rawMessages = obj.messages;\n platformStr = typeof obj.platform === \"string\"\n ? obj.platform\n : undefined;\n exportDate = typeof obj.export_date === \"string\"\n ? obj.export_date\n : undefined;\n }\n\n if (rawMessages.length === 0) {\n throw new Error(\"WeClone import: messages array must not be empty\");\n }\n\n // Resolve platform\n const platform: WeClonePlatform = resolvePlatform(\n options?.platform,\n platformStr,\n );\n\n // Determine self sender. When caller omits `selfSender`, infer it from\n // the first valid message sender that does NOT look like a bot and is\n // not explicitly listed in `assistantSenders`. Falling back to the\n // very first sender would misclassify bot greetings (e.g. \"ChatGPT Bot\")\n // as `user` and demote the actual human to `other`, corrupting roles\n // for downstream extraction. If every sender looks bot-like (rare),\n // fall through to the first valid sender rather than leaving `selfSender`\n // empty (which would make every message `other` or `assistant`).\n const assistantSet = new Set<string>(options?.assistantSenders ?? []);\n let inferredSelfSender = \"\";\n if (options?.selfSender === undefined) {\n for (const m of rawMessages) {\n if (!isValidMessage(m)) continue;\n if (assistantSet.has(m.sender)) continue;\n if (looksLikeBot(m.sender)) continue;\n inferredSelfSender = m.sender;\n break;\n }\n if (inferredSelfSender === \"\") {\n const firstValid = rawMessages.find(isValidMessage);\n inferredSelfSender = firstValid ? firstValid.sender : \"\";\n }\n }\n const selfSender = options?.selfSender ?? inferredSelfSender;\n\n // Map messages to WeCloneImportTurn[]\n const turns: WeCloneImportTurn[] = [];\n const warnings: string[] = [];\n\n for (let i = 0; i < rawMessages.length; i += 1) {\n const raw = rawMessages[i];\n if (!isValidMessage(raw)) {\n const msg =\n `WeClone import: message at index ${i} is invalid ` +\n `(must have sender, text, timestamp as non-empty strings)`;\n if (strict) throw new Error(msg);\n warnings.push(msg);\n continue;\n }\n\n const turn: WeCloneImportTurn = {\n role: resolveRole(raw.sender, selfSender, assistantSet),\n content: raw.text,\n timestamp: raw.timestamp,\n participantId: raw.sender,\n participantName: raw.sender,\n ...(raw.reply_to_id != null ? { replyToId: raw.reply_to_id } : {}),\n ...(raw.message_id != null ? { messageId: raw.message_id } : {}),\n };\n\n // Validate the turn using core's validator\n const issues = validateImportTurn(turn, i);\n if (issues.length > 0) {\n const detail = issues.map((iss) => iss.message).join(\"; \");\n if (strict) {\n throw new Error(\n `WeClone import: turn at index ${i} failed validation: ${detail}`,\n );\n }\n warnings.push(\n `WeClone import: skipping message at index ${i}: ${detail}`,\n );\n continue;\n }\n\n turns.push(turn);\n }\n\n if (turns.length === 0) {\n throw new Error(\n \"WeClone import: no valid turns after parsing all messages\",\n );\n }\n\n // Log warnings (non-strict mode)\n if (warnings.length > 0) {\n for (const w of warnings) {\n // eslint-disable-next-line no-console\n console.warn(w);\n }\n }\n\n // Build metadata\n const timestamps = turns\n .map((t) => parseIsoTimestamp(t.timestamp))\n .filter((ts): ts is number => ts !== null);\n timestamps.sort((a, b) => a - b);\n\n const from = timestamps.length > 0\n ? new Date(timestamps[0]).toISOString()\n : turns[0].timestamp;\n const to = timestamps.length > 0\n ? new Date(timestamps[timestamps.length - 1]).toISOString()\n : turns[turns.length - 1].timestamp;\n\n return {\n turns,\n metadata: {\n source: `weclone-${platform}`,\n exportDate: exportDate ?? new Date().toISOString(),\n messageCount: turns.length,\n dateRange: { from, to },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Platform resolution\n// ---------------------------------------------------------------------------\n\nfunction resolvePlatform(\n optionsPlatform: WeClonePlatform | undefined,\n exportPlatform: string | undefined,\n): WeClonePlatform {\n if (optionsPlatform !== undefined) {\n if (!VALID_PLATFORMS.has(optionsPlatform)) {\n throw new Error(\n `WeClone import: invalid platform '${optionsPlatform}'. ` +\n `Valid: ${[...VALID_PLATFORMS].join(\", \")}`,\n );\n }\n return optionsPlatform;\n }\n if (exportPlatform !== undefined) {\n if (!VALID_PLATFORMS.has(exportPlatform)) {\n throw new Error(\n `WeClone import: invalid platform '${exportPlatform}' in export data. ` +\n `Valid: ${[...VALID_PLATFORMS].join(\", \")}`,\n );\n }\n return exportPlatform as WeClonePlatform;\n }\n // Default to telegram when neither options nor export specify a platform\n return \"telegram\";\n}\n","// ---------------------------------------------------------------------------\n// WeClone bulk-import source adapter\n// ---------------------------------------------------------------------------\n\nimport type { BulkImportSourceAdapter, BulkImportSource } from \"@remnic/core\";\nimport { parseWeCloneExport, type ParseOptions } from \"./parser.js\";\n\n/**\n * Adapter that conforms to `BulkImportSourceAdapter` from `@remnic/core`.\n * Delegates parsing to `parseWeCloneExport`.\n */\nexport const wecloneImportAdapter: BulkImportSourceAdapter = {\n name: \"weclone\",\n parse(\n input: unknown,\n options?: { strict?: boolean; platform?: string },\n ): BulkImportSource {\n const parseOpts: ParseOptions | undefined = options\n ? {\n strict: options.strict,\n ...(options.platform !== undefined\n ? { platform: options.platform as ParseOptions[\"platform\"] }\n : {}),\n }\n : undefined;\n return parseWeCloneExport(input, parseOpts);\n },\n};\n","// ---------------------------------------------------------------------------\n// Conversation threader — groups flat message lists into threads\n// ---------------------------------------------------------------------------\n\nimport type { ImportTurn } from \"@remnic/core\";\nimport { parseIsoTimestamp } from \"@remnic/core\";\nimport type { WeCloneImportTurn } from \"./parser.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ThreadGroup {\n turns: ImportTurn[];\n threadId: string;\n startTime: string;\n endTime: string;\n}\n\nexport interface ThreaderOptions {\n /** Maximum time gap (ms) before starting a new thread. Default: 30 minutes. */\n gapThresholdMs?: number;\n /** Minimum number of messages for a thread to be kept. Default: 2. */\n minThreadSize?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_GAP_THRESHOLD_MS = 1_800_000; // 30 minutes\nconst DEFAULT_MIN_THREAD_SIZE = 2;\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Group an array of import turns into conversation threads.\n *\n * Algorithm (two-pass):\n * 1. Sort turns by timestamp.\n * 2. Split into initial thread segments by time gap.\n * 3. Merge segments linked by reply chains (if a turn's `replyToId`\n * references a `participantId` in a different segment, merge them).\n * 4. Filter out threads smaller than `minThreadSize`.\n * 5. Assign sequential thread IDs.\n */\nexport function groupIntoThreads(\n turns: ImportTurn[],\n options?: ThreaderOptions,\n): ThreadGroup[] {\n if (!turns || turns.length === 0) return [];\n\n const gapMs = options?.gapThresholdMs ?? DEFAULT_GAP_THRESHOLD_MS;\n const minSize = options?.minThreadSize ?? DEFAULT_MIN_THREAD_SIZE;\n\n if (options?.gapThresholdMs !== undefined && gapMs <= 0) {\n throw new Error(\n `gapThresholdMs must be positive, received ${gapMs}`,\n );\n }\n\n // Sort by timestamp (stable)\n const sorted = [...turns].sort((a, b) => {\n const tsA = parseIsoTimestamp(a.timestamp) ?? 0;\n const tsB = parseIsoTimestamp(b.timestamp) ?? 0;\n if (tsA !== tsB) return tsA - tsB;\n return 0;\n });\n\n // Pass 1: split into segments by time gap\n const segments: ImportTurn[][] = [];\n let currentSegment: ImportTurn[] = [];\n let prevTs: number | null = null;\n\n for (const turn of sorted) {\n const ts = parseIsoTimestamp(turn.timestamp) ?? 0;\n\n if (prevTs !== null && (ts - prevTs) > gapMs) {\n if (currentSegment.length > 0) {\n segments.push(currentSegment);\n currentSegment = [];\n }\n }\n\n currentSegment.push(turn);\n prevTs = ts;\n }\n if (currentSegment.length > 0) {\n segments.push(currentSegment);\n }\n\n // Pass 2: merge segments linked by reply chains.\n // Build a map from messageId -> segment index for turns that carry a\n // WeClone message_id. Then for turns with replyToId, if the referenced\n // messageId is in a different segment, merge them via union-find.\n\n // Union-find helpers\n const parent: number[] = segments.map((_, i) => i);\n\n function find(x: number): number {\n while (parent[x] !== x) {\n parent[x] = parent[parent[x]]; // path compression\n x = parent[x];\n }\n return x;\n }\n\n function union(a: number, b: number): void {\n const ra = find(a);\n const rb = find(b);\n if (ra !== rb) {\n // Always merge into the lower index (earlier segment)\n if (ra < rb) {\n parent[rb] = ra;\n } else {\n parent[ra] = rb;\n }\n }\n }\n\n // Map messageId -> segment index.\n // WeCloneImportTurn carries `messageId` from the source export. When\n // present we key by messageId so that `replyToId` lookups resolve correctly.\n // Falls back to a stringified turn index within the segment as a last resort.\n const idToSegment = new Map<string, number>();\n for (let segIdx = 0; segIdx < segments.length; segIdx += 1) {\n for (let turnIdx = 0; turnIdx < segments[segIdx].length; turnIdx += 1) {\n const turn = segments[segIdx][turnIdx] as WeCloneImportTurn;\n if (turn.messageId) {\n idToSegment.set(turn.messageId, segIdx);\n }\n }\n }\n\n // Merge segments linked by reply chains\n for (let segIdx = 0; segIdx < segments.length; segIdx += 1) {\n for (const turn of segments[segIdx]) {\n if (turn.replyToId != null) {\n const targetSeg = idToSegment.get(turn.replyToId);\n if (targetSeg !== undefined) {\n union(segIdx, targetSeg);\n }\n }\n }\n }\n\n // Collect merged segments\n const mergedMap = new Map<number, ImportTurn[]>();\n for (let segIdx = 0; segIdx < segments.length; segIdx += 1) {\n const root = find(segIdx);\n const existing = mergedMap.get(root);\n if (existing) {\n existing.push(...segments[segIdx]);\n } else {\n mergedMap.set(root, [...segments[segIdx]]);\n }\n }\n\n // Sort each merged thread by timestamp, filter by min size, assign IDs\n const result: ThreadGroup[] = [];\n let threadSeq = 1;\n\n // Process in segment order (keys are root indices, already ordered)\n const sortedRoots = [...mergedMap.keys()].sort((a, b) => a - b);\n for (const root of sortedRoots) {\n const threadTurns = mergedMap.get(root)!;\n if (threadTurns.length < minSize) continue;\n\n threadTurns.sort((a, b) => {\n const tsA = parseIsoTimestamp(a.timestamp) ?? 0;\n const tsB = parseIsoTimestamp(b.timestamp) ?? 0;\n return tsA - tsB;\n });\n\n result.push({\n turns: threadTurns,\n threadId: `thread-${String(threadSeq).padStart(4, \"0\")}`,\n startTime: threadTurns[0].timestamp,\n endTime: threadTurns[threadTurns.length - 1].timestamp,\n });\n threadSeq += 1;\n }\n\n return result;\n}\n","// ---------------------------------------------------------------------------\n// Participant mapper — extracts entity-like records from import turns\n// ---------------------------------------------------------------------------\n\nimport type { ImportTurn } from \"@remnic/core\";\nimport { parseIsoTimestamp } from \"@remnic/core\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ParticipantEntity {\n id: string;\n name: string;\n messageCount: number;\n firstSeen: string;\n lastSeen: string;\n /** Inferred relationship: \"self\", \"frequent\", or \"occasional\". */\n relationship?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Participants with more than this fraction of total messages are \"frequent\". */\nconst FREQUENT_THRESHOLD = 0.1;\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Build participant entity records from an array of import turns.\n *\n * - The participant with the most messages is tagged \"self\".\n * - Participants with >10% of total messages are \"frequent\".\n * - Everyone else is \"occasional\".\n * - Turns without `participantId` are silently skipped.\n */\nexport function mapParticipants(turns: ImportTurn[]): ParticipantEntity[] {\n if (!turns || turns.length === 0) return [];\n\n const stats = new Map<\n string,\n {\n name: string;\n count: number;\n firstTs: number;\n lastTs: number;\n firstRaw: string;\n lastRaw: string;\n }\n >();\n\n for (const turn of turns) {\n const id = turn.participantId;\n if (!id) continue;\n\n const ts = parseIsoTimestamp(turn.timestamp) ?? 0;\n const existing = stats.get(id);\n\n if (existing) {\n existing.count += 1;\n if (ts < existing.firstTs) {\n existing.firstTs = ts;\n existing.firstRaw = turn.timestamp;\n }\n if (ts > existing.lastTs) {\n existing.lastTs = ts;\n existing.lastRaw = turn.timestamp;\n }\n } else {\n stats.set(id, {\n name: turn.participantName ?? id,\n count: 1,\n firstTs: ts,\n lastTs: ts,\n firstRaw: turn.timestamp,\n lastRaw: turn.timestamp,\n });\n }\n }\n\n if (stats.size === 0) return [];\n\n // Find the top sender (most messages)\n let topId = \"\";\n let topCount = 0;\n for (const [id, s] of stats.entries()) {\n if (s.count > topCount) {\n topCount = s.count;\n topId = id;\n }\n }\n\n // Match the stats-loop filter above (`if (!id) continue;`) so the\n // denominator only counts turns that actually contributed to a\n // participant's stats. Turns with empty-string `participantId`\n // are excluded from both sides, keeping frequency ratios accurate.\n const totalMessages = turns.filter((t) => !!t.participantId).length;\n\n const result: ParticipantEntity[] = [];\n for (const [id, s] of stats.entries()) {\n let relationship: string;\n if (id === topId) {\n relationship = \"self\";\n } else if (s.count / totalMessages > FREQUENT_THRESHOLD) {\n relationship = \"frequent\";\n } else {\n relationship = \"occasional\";\n }\n\n result.push({\n id,\n name: s.name,\n messageCount: s.count,\n firstSeen: s.firstRaw,\n lastSeen: s.lastRaw,\n relationship,\n });\n }\n\n // Sort by message count descending, then by id for stability\n result.sort((a, b) => {\n if (b.messageCount !== a.messageCount) return b.messageCount - a.messageCount;\n return a.id.localeCompare(b.id);\n });\n\n return result;\n}\n","// ---------------------------------------------------------------------------\n// Thread chunker — splits long threads into extraction-sized batches\n// ---------------------------------------------------------------------------\n\nimport type { ImportTurn } from \"@remnic/core\";\nimport type { ThreadGroup } from \"./threader.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ChunkOptions {\n /** Maximum turns per chunk. Default: 20. */\n maxTurnsPerChunk?: number;\n /** Number of overlapping turns between consecutive chunks. Default: 2. */\n overlapTurns?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_MAX_TURNS = 20;\nconst DEFAULT_OVERLAP = 2;\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Split thread groups into extraction-sized chunks.\n *\n * - Threads shorter than `maxTurnsPerChunk` stay as a single chunk.\n * - Longer threads are split with `overlapTurns` overlap at boundaries\n * to preserve conversational context.\n */\nexport function chunkThreads(\n threads: ThreadGroup[],\n options?: ChunkOptions,\n): ImportTurn[][] {\n if (!threads || threads.length === 0) return [];\n\n const maxTurns = options?.maxTurnsPerChunk ?? DEFAULT_MAX_TURNS;\n const overlap = options?.overlapTurns ?? DEFAULT_OVERLAP;\n\n if (maxTurns <= 0) {\n throw new Error(\n `maxTurnsPerChunk must be positive, received ${maxTurns}`,\n );\n }\n\n if (overlap < 0) {\n throw new Error(\n `overlapTurns must be non-negative, received ${overlap}`,\n );\n }\n\n if (overlap >= maxTurns) {\n throw new Error(\n `overlapTurns (${overlap}) must be less than maxTurnsPerChunk ` +\n `(${maxTurns}); otherwise chunks would either never advance ` +\n `or silently clamp step to 1 and massively inflate work`,\n );\n }\n\n // Effective step: how many new turns each chunk advances by\n const step = Math.max(1, maxTurns - overlap);\n\n const chunks: ImportTurn[][] = [];\n\n for (const thread of threads) {\n const turns = thread.turns;\n if (turns.length === 0) continue;\n\n if (turns.length <= maxTurns) {\n chunks.push([...turns]);\n continue;\n }\n\n // Sliding window with overlap\n for (let start = 0; start < turns.length; start += step) {\n const end = Math.min(start + maxTurns, turns.length);\n chunks.push(turns.slice(start, end));\n // If we've reached the end, stop\n if (end === turns.length) break;\n }\n }\n\n return chunks;\n}\n","// ---------------------------------------------------------------------------\n// Import progress tracker\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ImportProgress {\n phase: \"parsing\" | \"threading\" | \"chunking\" | \"extracting\" | \"complete\";\n totalMessages: number;\n threadsFound: number;\n chunksCreated: number;\n chunksProcessed: number;\n memoriesExtracted: number;\n duplicatesSkipped: number;\n entitiesCreated: number;\n elapsed: number;\n}\n\nexport type ProgressCallback = (progress: ImportProgress) => void;\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\nfunction defaultProgress(): ImportProgress {\n return {\n phase: \"parsing\",\n totalMessages: 0,\n threadsFound: 0,\n chunksCreated: 0,\n chunksProcessed: 0,\n memoriesExtracted: 0,\n duplicatesSkipped: 0,\n entitiesCreated: 0,\n elapsed: 0,\n };\n}\n\n/**\n * Create a progress tracker that maintains state and optionally notifies\n * a callback on every update.\n */\nexport function createProgressTracker(callback?: ProgressCallback): {\n update(partial: Partial<ImportProgress>): void;\n snapshot(): ImportProgress;\n} {\n const state: ImportProgress = defaultProgress();\n const startTime = Date.now();\n\n return {\n update(partial: Partial<ImportProgress>): void {\n Object.assign(state, partial);\n state.elapsed = Date.now() - startTime;\n if (callback) {\n callback({ ...state });\n }\n },\n\n snapshot(): ImportProgress {\n state.elapsed = Date.now() - startTime;\n return { ...state };\n },\n };\n}\n"],"mappings":"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","names":["parseIsoTimestamp","parseIsoTimestamp"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@remnic/import-weclone",
3
+ "version": "1.0.1",
4
+ "description": "Import WeClone-preprocessed chat exports to bootstrap Remnic memory",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "provenance": true
20
+ },
21
+ "dependencies": {
22
+ "@remnic/core": "^1.0.3"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.7.0",
27
+ "tsx": "^4.0.0"
28
+ },
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/joshuaswarren/remnic.git",
33
+ "directory": "packages/import-weclone"
34
+ },
35
+ "keywords": [
36
+ "remnic",
37
+ "memory",
38
+ "weclone",
39
+ "import",
40
+ "chat-history"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsup src/index.ts --format esm --dts",
44
+ "test": "tsx --test 'src/**/*.test.ts'"
45
+ }
46
+ }