@remnic/import-weclone 1.0.1 → 9.3.517
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/dist/index.js +69 -9
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -46,13 +46,51 @@ function isValidMessage(msg) {
|
|
|
46
46
|
const m = msg;
|
|
47
47
|
return isNonEmptyString(m.sender) && isNonEmptyString(m.text) && isNonEmptyString(m.timestamp);
|
|
48
48
|
}
|
|
49
|
+
function invalidOption(name, expected) {
|
|
50
|
+
return new Error(`WeClone import: invalid option ${name}: expected ${expected}`);
|
|
51
|
+
}
|
|
52
|
+
function normalizeParseOptions(options) {
|
|
53
|
+
if (options === void 0) {
|
|
54
|
+
return { strict: false, assistantSenders: [] };
|
|
55
|
+
}
|
|
56
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
57
|
+
throw invalidOption("options", "an object");
|
|
58
|
+
}
|
|
59
|
+
const raw = options;
|
|
60
|
+
if (raw.strict !== void 0 && typeof raw.strict !== "boolean") {
|
|
61
|
+
throw invalidOption("strict", "a boolean");
|
|
62
|
+
}
|
|
63
|
+
if (raw.platform !== void 0 && typeof raw.platform !== "string") {
|
|
64
|
+
throw invalidOption("platform", [...VALID_PLATFORMS].join(", "));
|
|
65
|
+
}
|
|
66
|
+
if (raw.selfSender !== void 0 && !isNonEmptyString(raw.selfSender)) {
|
|
67
|
+
throw invalidOption("selfSender", "a non-empty string");
|
|
68
|
+
}
|
|
69
|
+
if (raw.assistantSenders !== void 0) {
|
|
70
|
+
if (!Array.isArray(raw.assistantSenders)) {
|
|
71
|
+
throw invalidOption("assistantSenders", "an array of non-empty strings");
|
|
72
|
+
}
|
|
73
|
+
for (let i = 0; i < raw.assistantSenders.length; i += 1) {
|
|
74
|
+
if (!isNonEmptyString(raw.assistantSenders[i])) {
|
|
75
|
+
throw invalidOption(`assistantSenders[${i}]`, "a non-empty string");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
strict: raw.strict === true,
|
|
81
|
+
...raw.platform !== void 0 ? { platform: raw.platform } : {},
|
|
82
|
+
...raw.selfSender !== void 0 ? { selfSender: raw.selfSender } : {},
|
|
83
|
+
assistantSenders: raw.assistantSenders ?? []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
49
86
|
function parseWeCloneExport(input, options) {
|
|
50
87
|
if (!input || typeof input !== "object") {
|
|
51
88
|
throw new Error(
|
|
52
89
|
"WeClone import: input must be a non-null object or array"
|
|
53
90
|
);
|
|
54
91
|
}
|
|
55
|
-
const
|
|
92
|
+
const parseOptions = normalizeParseOptions(options);
|
|
93
|
+
const strict = parseOptions.strict;
|
|
56
94
|
let rawMessages;
|
|
57
95
|
let platformStr;
|
|
58
96
|
let exportDate;
|
|
@@ -73,12 +111,12 @@ function parseWeCloneExport(input, options) {
|
|
|
73
111
|
throw new Error("WeClone import: messages array must not be empty");
|
|
74
112
|
}
|
|
75
113
|
const platform = resolvePlatform(
|
|
76
|
-
|
|
114
|
+
parseOptions.platform,
|
|
77
115
|
platformStr
|
|
78
116
|
);
|
|
79
|
-
const assistantSet = new Set(
|
|
117
|
+
const assistantSet = new Set(parseOptions.assistantSenders);
|
|
80
118
|
let inferredSelfSender = "";
|
|
81
|
-
if (
|
|
119
|
+
if (parseOptions.selfSender === void 0) {
|
|
82
120
|
for (const m of rawMessages) {
|
|
83
121
|
if (!isValidMessage(m)) continue;
|
|
84
122
|
if (assistantSet.has(m.sender)) continue;
|
|
@@ -91,7 +129,7 @@ function parseWeCloneExport(input, options) {
|
|
|
91
129
|
inferredSelfSender = firstValid ? firstValid.sender : "";
|
|
92
130
|
}
|
|
93
131
|
}
|
|
94
|
-
const selfSender =
|
|
132
|
+
const selfSender = parseOptions.selfSender ?? inferredSelfSender;
|
|
95
133
|
const turns = [];
|
|
96
134
|
const warnings = [];
|
|
97
135
|
for (let i = 0; i < rawMessages.length; i += 1) {
|
|
@@ -195,6 +233,21 @@ function groupIntoThreads(turns, options) {
|
|
|
195
233
|
`gapThresholdMs must be positive, received ${gapMs}`
|
|
196
234
|
);
|
|
197
235
|
}
|
|
236
|
+
if (options?.gapThresholdMs !== void 0 && (!Number.isFinite(gapMs) || !Number.isInteger(gapMs))) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`gapThresholdMs must be a finite integer, received ${gapMs}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (options?.minThreadSize !== void 0 && (!Number.isFinite(minSize) || !Number.isInteger(minSize))) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`minThreadSize must be a finite integer, received ${minSize}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (options?.minThreadSize !== void 0 && minSize <= 0) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`minThreadSize must be positive, received ${minSize}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
198
251
|
const sorted = [...turns].sort((a, b) => {
|
|
199
252
|
const tsA = parseIsoTimestamp2(a.timestamp) ?? 0;
|
|
200
253
|
const tsB = parseIsoTimestamp2(b.timestamp) ?? 0;
|
|
@@ -363,11 +416,21 @@ function chunkThreads(threads, options) {
|
|
|
363
416
|
if (!threads || threads.length === 0) return [];
|
|
364
417
|
const maxTurns = options?.maxTurnsPerChunk ?? DEFAULT_MAX_TURNS;
|
|
365
418
|
const overlap = options?.overlapTurns ?? DEFAULT_OVERLAP;
|
|
419
|
+
if (options?.maxTurnsPerChunk !== void 0 && (!Number.isFinite(maxTurns) || !Number.isInteger(maxTurns))) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`maxTurnsPerChunk must be a finite integer, received ${maxTurns}`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
366
424
|
if (maxTurns <= 0) {
|
|
367
425
|
throw new Error(
|
|
368
426
|
`maxTurnsPerChunk must be positive, received ${maxTurns}`
|
|
369
427
|
);
|
|
370
428
|
}
|
|
429
|
+
if (options?.overlapTurns !== void 0 && (!Number.isFinite(overlap) || !Number.isInteger(overlap))) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
`overlapTurns must be a finite integer, received ${overlap}`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
371
434
|
if (overlap < 0) {
|
|
372
435
|
throw new Error(
|
|
373
436
|
`overlapTurns must be non-negative, received ${overlap}`
|
|
@@ -436,10 +499,7 @@ function ensureWecloneImportAdapterRegistered() {
|
|
|
436
499
|
registerBulkImportSource(wecloneImportAdapter);
|
|
437
500
|
return true;
|
|
438
501
|
}
|
|
439
|
-
|
|
440
|
-
ensureWecloneImportAdapterRegistered();
|
|
441
|
-
} catch {
|
|
442
|
-
}
|
|
502
|
+
ensureWecloneImportAdapterRegistered();
|
|
443
503
|
export {
|
|
444
504
|
chunkThreads,
|
|
445
505
|
createProgressTracker,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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// Let unexpected registry failures surface at import time so the public\n// \"import registers the adapter\" contract cannot silently become false.\nensureWecloneImportAdapterRegistered();\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\ntype NormalizedParseOptions = {\n platform?: WeClonePlatform;\n strict: boolean;\n selfSender?: string;\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\nfunction invalidOption(name: string, expected: string): Error {\n return new Error(`WeClone import: invalid option ${name}: expected ${expected}`);\n}\n\nfunction normalizeParseOptions(options: ParseOptions | undefined): NormalizedParseOptions {\n if (options === undefined) {\n return { strict: false, assistantSenders: [] };\n }\n if (!options || typeof options !== \"object\" || Array.isArray(options)) {\n throw invalidOption(\"options\", \"an object\");\n }\n\n const raw = options as Record<string, unknown>;\n if (raw.strict !== undefined && typeof raw.strict !== \"boolean\") {\n throw invalidOption(\"strict\", \"a boolean\");\n }\n if (raw.platform !== undefined && typeof raw.platform !== \"string\") {\n throw invalidOption(\"platform\", [...VALID_PLATFORMS].join(\", \"));\n }\n if (raw.selfSender !== undefined && !isNonEmptyString(raw.selfSender)) {\n throw invalidOption(\"selfSender\", \"a non-empty string\");\n }\n if (raw.assistantSenders !== undefined) {\n if (!Array.isArray(raw.assistantSenders)) {\n throw invalidOption(\"assistantSenders\", \"an array of non-empty strings\");\n }\n for (let i = 0; i < raw.assistantSenders.length; i += 1) {\n if (!isNonEmptyString(raw.assistantSenders[i])) {\n throw invalidOption(`assistantSenders[${i}]`, \"a non-empty string\");\n }\n }\n }\n\n return {\n strict: raw.strict === true,\n ...(raw.platform !== undefined\n ? { platform: raw.platform as WeClonePlatform }\n : {}),\n ...(raw.selfSender !== undefined\n ? { selfSender: raw.selfSender as string }\n : {}),\n assistantSenders: (raw.assistantSenders as string[] | undefined) ?? [],\n };\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 parseOptions = normalizeParseOptions(options);\n const strict = parseOptions.strict;\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 parseOptions.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>(parseOptions.assistantSenders);\n let inferredSelfSender = \"\";\n if (parseOptions.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 = parseOptions.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 if (options?.gapThresholdMs !== undefined && (!Number.isFinite(gapMs) || !Number.isInteger(gapMs))) {\n throw new Error(\n `gapThresholdMs must be a finite integer, received ${gapMs}`,\n );\n }\n if (options?.minThreadSize !== undefined && (!Number.isFinite(minSize) || !Number.isInteger(minSize))) {\n throw new Error(\n `minThreadSize must be a finite integer, received ${minSize}`,\n );\n }\n if (options?.minThreadSize !== undefined && minSize <= 0) {\n throw new Error(\n `minThreadSize must be positive, received ${minSize}`,\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 (options?.maxTurnsPerChunk !== undefined && (!Number.isFinite(maxTurns) || !Number.isInteger(maxTurns))) {\n throw new Error(\n `maxTurnsPerChunk must be a finite integer, received ${maxTurns}`,\n );\n }\n\n if (maxTurns <= 0) {\n throw new Error(\n `maxTurnsPerChunk must be positive, received ${maxTurns}`,\n );\n }\n\n if (options?.overlapTurns !== undefined && (!Number.isFinite(overlap) || !Number.isInteger(overlap))) {\n throw new Error(\n `overlapTurns must be a finite integer, received ${overlap}`,\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/import-weclone",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.3.517",
|
|
4
4
|
"description": "Import WeClone-preprocessed chat exports to bootstrap Remnic memory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
"access": "public",
|
|
19
19
|
"provenance": true
|
|
20
20
|
},
|
|
21
|
-
"
|
|
22
|
-
"@remnic/core": "^
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@remnic/core": "^9.3.517"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"tsup": "^8.0.0",
|
|
26
26
|
"typescript": "^5.7.0",
|
|
27
|
-
"tsx": "^4.0.0"
|
|
27
|
+
"tsx": "^4.0.0",
|
|
28
|
+
"@remnic/core": "9.3.517"
|
|
28
29
|
},
|
|
29
30
|
"license": "MIT",
|
|
30
31
|
"repository": {
|
|
@@ -41,6 +42,8 @@
|
|
|
41
42
|
],
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "tsup src/index.ts --format esm --dts",
|
|
44
|
-
"
|
|
45
|
+
"precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
|
|
46
|
+
"check-types": "tsc --noEmit",
|
|
47
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/adapter.test.ts src/chunker.test.ts src/integration.test.ts src/parser.test.ts src/participant.test.ts src/progress.test.ts src/threader.test.ts"
|
|
45
48
|
}
|
|
46
49
|
}
|