import type { ChatRole } from "./chat-types"; import { CHAT_PROVIDER_IDS, type ChatProviderId } from "./chat-types"; export interface ProviderOnboardingContract { providerId: ChatProviderId; defaultScanRoots: string[]; pathDiscovery: string; sessionIdStrategy: string; roleMapping: { sourceRoles: Record; derivedRoles: Record; }; dedupe: { messageIdentity: string; snapshotHandling: "none" | "skip-known-snapshots" | "content-signature"; }; replay: { malformedRecordPolicy: "skip" | "fail"; missingTimestampPolicy: "skip" | "synthesize"; sinceFiltering: "file-mtime+message-timestamp" | "message-timestamp-only"; }; } export const CHAT_PROVIDER_ONBOARDING_CONTRACTS = { claude: { providerId: "claude", defaultScanRoots: ["~/.claude/projects"], pathDiscovery: "scan project directories under ~/.claude/projects and parse *.jsonl except agent-*.jsonl", sessionIdStrategy: "record.sessionId fallback to conversation filename", roleMapping: { sourceRoles: { user: "user", assistant: "assistant", }, derivedRoles: {}, }, dedupe: { messageIdentity: "record.uuid fallback to -", snapshotHandling: "skip-known-snapshots", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "skip", sinceFiltering: "file-mtime+message-timestamp", }, }, claude_desktop: { providerId: "claude_desktop", defaultScanRoots: [ "~/Library/Application Support/Claude/local-agent-mode-sessions", ], pathDiscovery: "walk ///local_*.json under the Claude Desktop userData local-agent-mode-sessions tree. Each file is one agent-mode session; regular claude.ai chats are server-side and not captured here.", sessionIdStrategy: "record.sessionId (UUID prefixed 'local_')", roleMapping: { sourceRoles: { initialMessage: "user", }, derivedRoles: {}, }, dedupe: { messageIdentity: ":initial (synthesized from initialMessage)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "skip", sinceFiltering: "message-timestamp-only", }, }, gemini: { providerId: "gemini", defaultScanRoots: ["~/.gemini/tmp"], pathDiscovery: "scan direct and nested projectHash dirs with chats/session-*.json", sessionIdStrategy: "session.sessionId fallback to filename", roleMapping: { sourceRoles: { user: "user", gemini: "assistant", tool: "tool", }, derivedRoles: { default: "system", }, }, dedupe: { messageIdentity: "message.id fallback to stable(threadId,index)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "skip", sinceFiltering: "file-mtime+message-timestamp", }, }, codex: { providerId: "codex", defaultScanRoots: ["~/.codex/sessions"], pathDiscovery: "walk ~/.codex/sessions recursively for *.jsonl", sessionIdStrategy: "session_meta.payload.id fallback to filename", roleMapping: { sourceRoles: { user: "user", assistant: "assistant", tool: "tool", }, derivedRoles: { function_call: "tool", function_call_output: "tool", reasoning: "assistant", }, }, dedupe: { messageIdentity: "payload.id/call_id fallback to stable(threadId,index)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "skip", sinceFiltering: "file-mtime+message-timestamp", }, }, opencode: { providerId: "opencode", defaultScanRoots: ["~/.local/share/opencode/storage"], pathDiscovery: "scan storage roots containing session/ + message/ and join session->message->part", sessionIdStrategy: "session.id", roleMapping: { sourceRoles: { user: "user", assistant: "assistant", tool: "tool", }, derivedRoles: { patch: "tool", "step-start": "system", "step-finish": "system", }, }, dedupe: { messageIdentity: "part.id fallback to stable(messageId,index)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "synthesize", sinceFiltering: "file-mtime+message-timestamp", }, }, cursor: { providerId: "cursor", defaultScanRoots: ["~/.cursor/chats"], pathDiscovery: "scan ~/.cursor/chats for nested chat dirs containing store.db and resolve workspaceHash using workspaceStorage/*/workspace.json", sessionIdStrategy: "meta.agentId fallback to chat directory basename", roleMapping: { sourceRoles: { user: "user", assistant: "assistant", tool: "tool", }, derivedRoles: { default: "system", }, }, dedupe: { messageIdentity: "blob message id + row order; dedupe repeated snapshots by content signature", snapshotHandling: "content-signature", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "synthesize", sinceFiltering: "file-mtime+message-timestamp", }, }, copilot: { providerId: "copilot", defaultScanRoots: [ "~/Library/Application Support/Code/User/workspaceStorage", "~/Library/Application Support/Code - Insiders/User/workspaceStorage", ], pathDiscovery: "scan workspaceStorage//state.vscdb SQLite; query ItemTable WHERE key='interactive.sessions'; resolve project path from workspace.json", sessionIdStrategy: "session.sessionId fallback to workspaceHash", roleMapping: { sourceRoles: { user: "user", assistant: "assistant", }, derivedRoles: {}, }, dedupe: { messageIdentity: "stable(sessionId, requestIndex, role)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "synthesize", sinceFiltering: "file-mtime+message-timestamp", }, }, amp: { providerId: "amp", defaultScanRoots: ["~/.config/amp/threads"], pathDiscovery: "scan ~/.config/amp/threads/*.jsonl; Amp stores threads server-side — local files only exist if user exports via --stream-json redirect", sessionIdStrategy: "record.session_id fallback to filename", roleMapping: { sourceRoles: { user: "user", assistant: "assistant", }, derivedRoles: { default: "system", }, }, dedupe: { messageIdentity: "record.uuid fallback to stable(sessionId,index)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "synthesize", sinceFiltering: "file-mtime+message-timestamp", }, }, cline: { providerId: "cline", defaultScanRoots: [ "~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev", "~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline", ], pathDiscovery: "scan globalStorage//tasks//ui_messages.json; index from state/taskHistory.json", sessionIdStrategy: "taskHistory.id (directory name under tasks/)", roleMapping: { sourceRoles: { "say:task": "user", "say:user_feedback": "user", "say:text": "assistant", "say:completion_result": "assistant", "say:tool": "tool", "ask:tool": "tool", "ask:command": "tool", }, derivedRoles: { default: "system", }, }, dedupe: { messageIdentity: "stable(taskId, messageIndex, type-index)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "synthesize", sinceFiltering: "file-mtime+message-timestamp", }, }, grok: { providerId: "grok", defaultScanRoots: ["~/.grok/sessions"], pathDiscovery: "walk ~/.grok/sessions/// dirs; parse chat_history.jsonl (raw chat messages sent to the model) and read summary.json for title/timestamps/model. cwd from the group dir's .cwd file or URL-decoded group name.", sessionIdStrategy: " directory basename fallback to summary.sessionId", roleMapping: { sourceRoles: { user: "user", assistant: "assistant", tool: "tool", system: "system", }, derivedRoles: { default: "system", }, }, dedupe: { messageIdentity: "stable(sessionId, index)", snapshotHandling: "none", }, replay: { malformedRecordPolicy: "skip", missingTimestampPolicy: "synthesize", sinceFiltering: "file-mtime+message-timestamp", }, }, } as const satisfies Record; export function getProviderOnboardingContract( providerId: ChatProviderId, ): ProviderOnboardingContract { return CHAT_PROVIDER_ONBOARDING_CONTRACTS[providerId]; } export function listProviderOnboardingContracts(): ProviderOnboardingContract[] { return CHAT_PROVIDER_IDS.map((providerId) => getProviderOnboardingContract(providerId), ); }