@remnic/import-weclone 9.3.613 → 9.3.614

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.d.ts CHANGED
@@ -88,7 +88,8 @@ interface ParticipantEntity {
88
88
  /**
89
89
  * Build participant entity records from an array of import turns.
90
90
  *
91
- * - The participant with the most messages is tagged "self".
91
+ * - The parser-inferred user participant is tagged "self" when role data exists.
92
+ * - The participant with the most messages is tagged "self" as a fallback.
92
93
  * - Participants with >10% of total messages are "frequent".
93
94
  * - Everyone else is "occasional".
94
95
  * - Turns without `participantId` are silently skipped.
package/dist/index.js CHANGED
@@ -354,6 +354,7 @@ function mapParticipants(turns) {
354
354
  const existing = stats.get(id);
355
355
  if (existing) {
356
356
  existing.count += 1;
357
+ if (turn.role === "user") existing.userRoleCount += 1;
357
358
  if (ts < existing.firstTs) {
358
359
  existing.firstTs = ts;
359
360
  existing.firstRaw = turn.timestamp;
@@ -366,6 +367,7 @@ function mapParticipants(turns) {
366
367
  stats.set(id, {
367
368
  name: turn.participantName ?? id,
368
369
  count: 1,
370
+ userRoleCount: turn.role === "user" ? 1 : 0,
369
371
  firstTs: ts,
370
372
  lastTs: ts,
371
373
  firstRaw: turn.timestamp,
@@ -374,6 +376,14 @@ function mapParticipants(turns) {
374
376
  }
375
377
  }
376
378
  if (stats.size === 0) return [];
379
+ let roleSelfId = "";
380
+ let topUserRoleCount = 0;
381
+ for (const [id, s] of stats.entries()) {
382
+ if (s.userRoleCount > topUserRoleCount) {
383
+ topUserRoleCount = s.userRoleCount;
384
+ roleSelfId = id;
385
+ }
386
+ }
377
387
  let topId = "";
378
388
  let topCount = 0;
379
389
  for (const [id, s] of stats.entries()) {
@@ -384,9 +394,10 @@ function mapParticipants(turns) {
384
394
  }
385
395
  const totalMessages = turns.filter((t) => !!t.participantId).length;
386
396
  const result = [];
397
+ const selfId = roleSelfId || topId;
387
398
  for (const [id, s] of stats.entries()) {
388
399
  let relationship;
389
- if (id === topId) {
400
+ if (id === selfId) {
390
401
  relationship = "self";
391
402
  } else if (s.count / totalMessages > FREQUENT_THRESHOLD) {
392
403
  relationship = "frequent";
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// 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"]}
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 parser-inferred user participant is tagged \"self\" when role data exists.\n * - The participant with the most messages is tagged \"self\" as a fallback.\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 userRoleCount: 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 (turn.role === \"user\") existing.userRoleCount += 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 userRoleCount: turn.role === \"user\" ? 1 : 0,\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 // Prefer the participant the parser already classified as the user.\n let roleSelfId = \"\";\n let topUserRoleCount = 0;\n for (const [id, s] of stats.entries()) {\n if (s.userRoleCount > topUserRoleCount) {\n topUserRoleCount = s.userRoleCount;\n roleSelfId = id;\n }\n }\n\n // Fall back to the top sender for legacy turns without role data.\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 const selfId = roleSelfId || topId;\n for (const [id, s] of stats.entries()) {\n let relationship: string;\n if (id === selfId) {\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": "9.3.613",
3
+ "version": "9.3.614",
4
4
  "description": "Import WeClone-preprocessed chat exports to bootstrap Remnic memory",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,13 +19,13 @@
19
19
  "provenance": true
20
20
  },
21
21
  "peerDependencies": {
22
- "@remnic/core": "^9.3.613"
22
+ "@remnic/core": "^9.3.614"
23
23
  },
24
24
  "devDependencies": {
25
25
  "tsup": "^8.0.0",
26
26
  "typescript": "^5.7.0",
27
27
  "tsx": "^4.0.0",
28
- "@remnic/core": "9.3.613"
28
+ "@remnic/core": "9.3.614"
29
29
  },
30
30
  "license": "MIT",
31
31
  "repository": {