@codehourra/llm-iwiki 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { Collector, RawMessage, RawSession } from './types'
5
+ import { clampTitle, deriveTitle, isEphemeralPath, normalizeContentParts } from './util'
6
+
7
+ const APP_SUPPORT = 'Library/Application Support'
8
+ const EXTENSION_DIRS = ['CodeBuddyExtension', 'CodeBuddy CN']
9
+
10
+ interface ConversationMeta {
11
+ id: string
12
+ name?: string
13
+ createdAt?: string
14
+ lastMessageAt?: string
15
+ }
16
+
17
+ function findHistoryDirs(root: string, acc: string[], depth = 0): void {
18
+ if (depth > 6) return
19
+ let entries
20
+ try {
21
+ entries = readdirSync(root, { withFileTypes: true })
22
+ } catch {
23
+ return
24
+ }
25
+ for (const entry of entries) {
26
+ if (!entry.isDirectory()) continue
27
+ const full = join(root, entry.name)
28
+ if (entry.name === 'history') {
29
+ acc.push(full)
30
+ } else if (entry.name !== 'messages') {
31
+ findHistoryDirs(full, acc, depth + 1)
32
+ }
33
+ }
34
+ }
35
+
36
+ function readJson<T>(filePath: string): T | null {
37
+ try {
38
+ return JSON.parse(readFileSync(filePath, 'utf8')) as T
39
+ } catch {
40
+ return null
41
+ }
42
+ }
43
+
44
+ function extractMessageText(messageField: unknown): string {
45
+ if (typeof messageField !== 'string') return normalizeContentParts(messageField)
46
+ const trimmed = messageField.trim()
47
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
48
+ try {
49
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>
50
+ if (parsed && typeof parsed === 'object' && 'content' in parsed) {
51
+ return normalizeContentParts((parsed as { content: unknown }).content)
52
+ }
53
+ return normalizeContentParts(parsed)
54
+ } catch {
55
+ return messageField
56
+ }
57
+ }
58
+ return messageField
59
+ }
60
+
61
+ function extractWorkspaceFolder(text: string): string | null {
62
+ const match = text.match(/Workspace Folder:\s*(.+)/)
63
+ if (!match) return null
64
+ const folder = match[1]!.trim()
65
+ return folder === '' ? null : folder
66
+ }
67
+
68
+ function readConversation(
69
+ convDir: string,
70
+ conversationId: string,
71
+ meta: ConversationMeta,
72
+ ): RawSession | null {
73
+ const convIndex = readJson<{ messages?: Array<{ id: string; role?: string }> }>(join(convDir, 'index.json'))
74
+ if (!convIndex?.messages) return null
75
+
76
+ const messages: RawMessage[] = []
77
+ let rawProjectPath: string | null = null
78
+
79
+ for (const entry of convIndex.messages) {
80
+ const raw = readJson<{ role?: string; message?: unknown }>(join(convDir, 'messages', `${entry.id}.json`))
81
+ if (!raw) continue
82
+ const role = entry.role ?? raw.role ?? 'user'
83
+ if (role !== 'user' && role !== 'assistant') continue
84
+
85
+ const content = extractMessageText(raw.message)
86
+ if (content.trim() === '') continue
87
+
88
+ if (!rawProjectPath && role === 'user') {
89
+ rawProjectPath = extractWorkspaceFolder(content)
90
+ }
91
+ messages.push({ role, content, timestamp: null })
92
+ }
93
+
94
+ if (messages.length === 0) return null
95
+ if (isEphemeralPath(rawProjectPath)) return null
96
+
97
+ const title = meta.name && meta.name.trim() !== '' ? clampTitle(meta.name.trim()) : deriveTitle(messages)
98
+
99
+ return {
100
+ sourceSessionId: conversationId,
101
+ rawPath: convDir,
102
+ rawProjectPath,
103
+ title,
104
+ createdAt: meta.createdAt ?? null,
105
+ updatedAt: meta.lastMessageAt ?? null,
106
+ messages,
107
+ }
108
+ }
109
+
110
+ function collectFromHistory(historyDir: string, sessions: RawSession[]): void {
111
+ for (const workspaceEntry of readdirSync(historyDir, { withFileTypes: true })) {
112
+ if (!workspaceEntry.isDirectory()) continue
113
+ const workspaceDir = join(historyDir, workspaceEntry.name)
114
+ const workspaceIndex = readJson<{ conversations?: ConversationMeta[] }>(join(workspaceDir, 'index.json'))
115
+ if (!workspaceIndex?.conversations?.length) continue
116
+
117
+ for (const conversation of workspaceIndex.conversations) {
118
+ if (!conversation.id) continue
119
+ const convDir = join(workspaceDir, conversation.id)
120
+ if (!existsSync(convDir)) continue
121
+ try {
122
+ const session = readConversation(convDir, conversation.id, conversation)
123
+ if (session) sessions.push(session)
124
+ } catch {
125
+ continue
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ function extensionRoots(homeDir: string): string[] {
132
+ return EXTENSION_DIRS.map((dir) => join(homeDir, APP_SUPPORT, dir)).filter((dir) => existsSync(dir))
133
+ }
134
+
135
+ export const codebuddyCollector: Collector = {
136
+ id: 'codebuddy',
137
+ name: 'CodeBuddy',
138
+
139
+ detect(homeDir: string): boolean {
140
+ return extensionRoots(homeDir).length > 0
141
+ },
142
+
143
+ collect(homeDir: string): RawSession[] {
144
+ const sessions: RawSession[] = []
145
+ const seen = new Set<string>()
146
+ for (const root of extensionRoots(homeDir)) {
147
+ const historyDirs: string[] = []
148
+ findHistoryDirs(join(root, 'Data'), historyDirs)
149
+ for (const historyDir of historyDirs) {
150
+ try {
151
+ if (!statSync(historyDir).isDirectory()) continue
152
+ } catch {
153
+ continue
154
+ }
155
+ collectFromHistory(historyDir, sessions)
156
+ }
157
+ }
158
+ return sessions.filter((session) => {
159
+ if (seen.has(session.sourceSessionId)) return false
160
+ seen.add(session.sourceSessionId)
161
+ return true
162
+ })
163
+ },
164
+ }
@@ -0,0 +1,130 @@
1
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+
4
+ import type { Collector, RawMessage, RawSession } from './types'
5
+ import { deriveTitle, isEphemeralPath } from './util'
6
+
7
+ const SESSION_ROOTS = ['.codex/sessions', '.codex-internal/sessions']
8
+
9
+ function cleanCodexText(text: string): string {
10
+ return text
11
+ .replace(/\[REQ:[0-9a-f-]+\]/gi, '')
12
+ .replace(/\[AMP_DONE:[0-9a-f-]+\]/gi, '')
13
+ .trim()
14
+ }
15
+
16
+ function walkRolloutFiles(dir: string, acc: string[]): void {
17
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
18
+ const full = join(dir, entry.name)
19
+ if (entry.isDirectory()) {
20
+ walkRolloutFiles(full, acc)
21
+ } else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
22
+ acc.push(full)
23
+ }
24
+ }
25
+ }
26
+
27
+ function parseRolloutFile(filePath: string): RawSession | null {
28
+ const raw = readFileSync(filePath, 'utf8')
29
+ const messages: RawMessage[] = []
30
+ let rawProjectPath: string | null = null
31
+ let sourceSessionId: string | null = null
32
+ let createdAt: string | null = null
33
+
34
+ for (const line of raw.split('\n')) {
35
+ const trimmed = line.trim()
36
+ if (trimmed === '') continue
37
+
38
+ let entry: Record<string, unknown>
39
+ try {
40
+ entry = JSON.parse(trimmed) as Record<string, unknown>
41
+ } catch {
42
+ continue
43
+ }
44
+
45
+ const payload = entry.payload
46
+ if (!payload || typeof payload !== 'object') continue
47
+ const payloadRecord = payload as Record<string, unknown>
48
+ const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : null
49
+
50
+ if (entry.type === 'session_meta') {
51
+ if (typeof payloadRecord.id === 'string') sourceSessionId = payloadRecord.id
52
+ if (typeof payloadRecord.cwd === 'string') rawProjectPath = payloadRecord.cwd
53
+ if (typeof payloadRecord.timestamp === 'string') createdAt = payloadRecord.timestamp
54
+ continue
55
+ }
56
+
57
+ if (entry.type !== 'event_msg') continue
58
+ const eventType = payloadRecord.type
59
+ if (eventType !== 'user_message' && eventType !== 'agent_message') continue
60
+ if (typeof payloadRecord.message !== 'string') continue
61
+
62
+ const content = cleanCodexText(payloadRecord.message)
63
+ if (content === '') continue
64
+ messages.push({
65
+ role: eventType === 'user_message' ? 'user' : 'assistant',
66
+ content,
67
+ timestamp,
68
+ })
69
+ }
70
+
71
+ if (messages.length === 0) return null
72
+ if (isEphemeralPath(rawProjectPath)) return null
73
+
74
+ sourceSessionId ??= basename(filePath).replace(/\.jsonl$/, '')
75
+ const timestamps = messages.map((message) => message.timestamp).filter((value): value is string => value != null)
76
+
77
+ return {
78
+ sourceSessionId,
79
+ rawPath: filePath,
80
+ rawProjectPath,
81
+ title: deriveTitle(messages),
82
+ createdAt: createdAt ?? timestamps[0] ?? null,
83
+ updatedAt: timestamps[timestamps.length - 1] ?? null,
84
+ messages,
85
+ }
86
+ }
87
+
88
+ function listRolloutFiles(homeDir: string): string[] {
89
+ const files: string[] = []
90
+ const seenRoots = new Set<string>()
91
+ for (const root of SESSION_ROOTS) {
92
+ const sessionsDir = join(homeDir, root)
93
+ if (!existsSync(sessionsDir)) continue
94
+
95
+ let canonicalRoot: string
96
+ try {
97
+ canonicalRoot = realpathSync(sessionsDir)
98
+ } catch {
99
+ canonicalRoot = sessionsDir
100
+ }
101
+ if (seenRoots.has(canonicalRoot)) continue
102
+ seenRoots.add(canonicalRoot)
103
+
104
+ walkRolloutFiles(sessionsDir, files)
105
+ }
106
+ return files
107
+ }
108
+
109
+ export const codexCollector: Collector = {
110
+ id: 'codex',
111
+ name: 'Codex',
112
+
113
+ detect(homeDir: string): boolean {
114
+ return SESSION_ROOTS.some((root) => existsSync(join(homeDir, root)))
115
+ },
116
+
117
+ collect(homeDir: string): RawSession[] {
118
+ const sessions: RawSession[] = []
119
+ for (const filePath of listRolloutFiles(homeDir)) {
120
+ try {
121
+ if (!statSync(filePath).isFile()) continue
122
+ const session = parseRolloutFile(filePath)
123
+ if (session) sessions.push(session)
124
+ } catch {
125
+ continue
126
+ }
127
+ }
128
+ return sessions
129
+ },
130
+ }
@@ -0,0 +1,178 @@
1
+ import { Database } from 'bun:sqlite'
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ import type { Collector, RawMessage, RawSession } from './types'
6
+ import { clampTitle, deriveTitle, isEphemeralPath } from './util'
7
+
8
+ const CURSOR_USER_DIR = 'Library/Application Support/Cursor/User'
9
+
10
+ interface ComposerMeta {
11
+ cwd: string | null
12
+ name: string | null
13
+ createdAt: string | null
14
+ updatedAt: string | null
15
+ }
16
+
17
+ function epochToIso(value: unknown): string | null {
18
+ if (typeof value !== 'number' || !Number.isFinite(value)) return null
19
+ const date = new Date(value)
20
+ return Number.isNaN(date.getTime()) ? null : date.toISOString()
21
+ }
22
+
23
+ function folderUriToPath(uri: string): string | null {
24
+ if (!uri.startsWith('file://')) return null
25
+ try {
26
+ return decodeURIComponent(uri.replace(/^file:\/\//, '')).replace(/\/+$/, '')
27
+ } catch {
28
+ return null
29
+ }
30
+ }
31
+
32
+ function openReadonly(path: string): Database | null {
33
+ try {
34
+ return new Database(path, { readonly: true })
35
+ } catch {
36
+ return null
37
+ }
38
+ }
39
+
40
+ function collectWorkspaceComposers(userDir: string): Map<string, ComposerMeta> {
41
+ const composers = new Map<string, ComposerMeta>()
42
+ const workspaceStorage = join(userDir, 'workspaceStorage')
43
+ if (!existsSync(workspaceStorage)) return composers
44
+
45
+ for (const entry of readdirSync(workspaceStorage, { withFileTypes: true })) {
46
+ if (!entry.isDirectory()) continue
47
+ const wsDir = join(workspaceStorage, entry.name)
48
+
49
+ let cwd: string | null = null
50
+ const workspaceJson = join(wsDir, 'workspace.json')
51
+ if (existsSync(workspaceJson)) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(workspaceJson, 'utf8')) as { folder?: string }
54
+ if (typeof parsed.folder === 'string') cwd = folderUriToPath(parsed.folder)
55
+ } catch {
56
+ // ignore
57
+ }
58
+ }
59
+
60
+ const dbPath = join(wsDir, 'state.vscdb')
61
+ if (!existsSync(dbPath)) continue
62
+ const db = openReadonly(dbPath)
63
+ if (!db) continue
64
+ try {
65
+ const row = db.query<{ value: string }, [string]>('SELECT value FROM ItemTable WHERE key = ?').get('composer.composerData')
66
+ if (!row) continue
67
+ const data = JSON.parse(row.value) as { allComposers?: Array<Record<string, unknown>> }
68
+ for (const composer of data.allComposers ?? []) {
69
+ const composerId = composer.composerId
70
+ if (typeof composerId !== 'string') continue
71
+ if (composers.has(composerId)) continue
72
+ composers.set(composerId, {
73
+ cwd,
74
+ name: typeof composer.name === 'string' ? composer.name : null,
75
+ createdAt: epochToIso(composer.createdAt),
76
+ updatedAt: epochToIso(composer.lastUpdatedAt),
77
+ })
78
+ }
79
+ } catch {
80
+ // ignore malformed workspace db
81
+ } finally {
82
+ db.close()
83
+ }
84
+ }
85
+
86
+ return composers
87
+ }
88
+
89
+ function buildSession(
90
+ globalDb: Database,
91
+ dbPath: string,
92
+ composerId: string,
93
+ meta: ComposerMeta,
94
+ ): RawSession | null {
95
+ const composerRow = globalDb
96
+ .query<{ value: string }, [string]>('SELECT value FROM cursorDiskKV WHERE key = ?')
97
+ .get(`composerData:${composerId}`)
98
+ if (!composerRow) return null
99
+
100
+ let composer: { name?: string; createdAt?: number; fullConversationHeadersOnly?: Array<{ bubbleId: string; type: number }> }
101
+ try {
102
+ composer = JSON.parse(composerRow.value)
103
+ } catch {
104
+ return null
105
+ }
106
+
107
+ const headers = composer.fullConversationHeadersOnly ?? []
108
+ if (headers.length === 0) return null
109
+
110
+ const bubbleStmt = globalDb.query<{ value: string }, [string]>('SELECT value FROM cursorDiskKV WHERE key = ?')
111
+ const messages: RawMessage[] = []
112
+ for (const header of headers) {
113
+ if (header.type !== 1 && header.type !== 2) continue
114
+ const bubbleRow = bubbleStmt.get(`bubbleId:${composerId}:${header.bubbleId}`)
115
+ if (!bubbleRow) continue
116
+ let bubble: { text?: string }
117
+ try {
118
+ bubble = JSON.parse(bubbleRow.value)
119
+ } catch {
120
+ continue
121
+ }
122
+ const text = typeof bubble.text === 'string' ? bubble.text.trim() : ''
123
+ if (text === '') continue
124
+ messages.push({ role: header.type === 1 ? 'user' : 'assistant', content: text, timestamp: null })
125
+ }
126
+
127
+ if (messages.length === 0) return null
128
+ if (isEphemeralPath(meta.cwd)) return null
129
+
130
+ const name = meta.name ?? composer.name ?? null
131
+ const title = name && name.trim() !== '' ? clampTitle(name.trim()) : deriveTitle(messages)
132
+
133
+ return {
134
+ sourceSessionId: composerId,
135
+ rawPath: `${dbPath}#${composerId}`,
136
+ rawProjectPath: meta.cwd,
137
+ title,
138
+ createdAt: meta.createdAt ?? epochToIso(composer.createdAt),
139
+ updatedAt: meta.updatedAt,
140
+ messages,
141
+ }
142
+ }
143
+
144
+ export const cursorCollector: Collector = {
145
+ id: 'cursor',
146
+ name: 'Cursor',
147
+
148
+ detect(homeDir: string): boolean {
149
+ return existsSync(join(homeDir, CURSOR_USER_DIR, 'globalStorage', 'state.vscdb'))
150
+ },
151
+
152
+ collect(homeDir: string): RawSession[] {
153
+ const userDir = join(homeDir, CURSOR_USER_DIR)
154
+ const globalDbPath = join(userDir, 'globalStorage', 'state.vscdb')
155
+ if (!existsSync(globalDbPath)) return []
156
+
157
+ const composers = collectWorkspaceComposers(userDir)
158
+ if (composers.size === 0) return []
159
+
160
+ const globalDb = openReadonly(globalDbPath)
161
+ if (!globalDb) return []
162
+
163
+ const sessions: RawSession[] = []
164
+ try {
165
+ for (const [composerId, meta] of composers) {
166
+ try {
167
+ const session = buildSession(globalDb, globalDbPath, composerId, meta)
168
+ if (session) sessions.push(session)
169
+ } catch {
170
+ continue
171
+ }
172
+ }
173
+ } finally {
174
+ globalDb.close()
175
+ }
176
+ return sessions
177
+ },
178
+ }
@@ -0,0 +1,141 @@
1
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
2
+ import { basename, dirname, join } from 'node:path'
3
+
4
+ import type { Collector, RawMessage, RawSession } from './types'
5
+ import { deriveTitle, isEphemeralPath } from './util'
6
+
7
+ const INTERNAL_ROOTS = ['.gemini-internal', '.gemini']
8
+
9
+ function loadProjectNameMap(geminiRoot: string): Map<string, string> {
10
+ const nameToPath = new Map<string, string>()
11
+ const projectsFile = join(geminiRoot, 'projects.json')
12
+ if (!existsSync(projectsFile)) return nameToPath
13
+ try {
14
+ const parsed = JSON.parse(readFileSync(projectsFile, 'utf8')) as { projects?: Record<string, string> }
15
+ for (const [path, name] of Object.entries(parsed.projects ?? {})) {
16
+ if (typeof name === 'string' && !nameToPath.has(name)) nameToPath.set(name, path)
17
+ }
18
+ } catch {
19
+ // ignore malformed projects.json
20
+ }
21
+ return nameToPath
22
+ }
23
+
24
+ function looksLikeAbsolutePath(value: string): boolean {
25
+ const trimmed = value.trim()
26
+ return trimmed.startsWith('/') && !trimmed.includes('\n') && trimmed.length < 256
27
+ }
28
+
29
+ function parseChatFile(filePath: string, projectDir: string, nameToPath: Map<string, string>): RawSession | null {
30
+ let parsed: {
31
+ sessionId?: string
32
+ startTime?: string
33
+ lastUpdated?: string
34
+ messages?: Array<{ type?: string; content?: unknown; timestamp?: string }>
35
+ }
36
+ try {
37
+ parsed = JSON.parse(readFileSync(filePath, 'utf8'))
38
+ } catch {
39
+ return null
40
+ }
41
+
42
+ const messages: RawMessage[] = []
43
+ for (const message of parsed.messages ?? []) {
44
+ if (message.type !== 'user' && message.type !== 'gemini') continue
45
+ const content = typeof message.content === 'string' ? message.content : ''
46
+ if (content.trim() === '') continue
47
+ messages.push({
48
+ role: message.type === 'user' ? 'user' : 'assistant',
49
+ content,
50
+ timestamp: typeof message.timestamp === 'string' ? message.timestamp : null,
51
+ })
52
+ }
53
+
54
+ if (messages.length === 0) return null
55
+
56
+ let rawProjectPath = nameToPath.get(projectDir) ?? null
57
+ if (!rawProjectPath) {
58
+ const firstUser = messages.find((message) => message.role === 'user')
59
+ if (firstUser && looksLikeAbsolutePath(firstUser.content)) {
60
+ rawProjectPath = firstUser.content.trim().replace(/\/+$/, '')
61
+ }
62
+ }
63
+ if (isEphemeralPath(rawProjectPath)) return null
64
+
65
+ const timestamps = messages.map((message) => message.timestamp).filter((value): value is string => value != null)
66
+
67
+ return {
68
+ sourceSessionId: parsed.sessionId ?? basename(filePath).replace(/\.json$/, ''),
69
+ rawPath: filePath,
70
+ rawProjectPath,
71
+ title: deriveTitle(messages),
72
+ createdAt: parsed.startTime ?? timestamps[0] ?? null,
73
+ updatedAt: parsed.lastUpdated ?? timestamps[timestamps.length - 1] ?? null,
74
+ messages,
75
+ }
76
+ }
77
+
78
+ function resolveGeminiRoot(homeDir: string): string | null {
79
+ const seen = new Set<string>()
80
+ for (const root of INTERNAL_ROOTS) {
81
+ const dir = join(homeDir, root)
82
+ if (!existsSync(dir)) continue
83
+ let canonical: string
84
+ try {
85
+ canonical = realpathSync(dir)
86
+ } catch {
87
+ canonical = dir
88
+ }
89
+ if (seen.has(canonical)) continue
90
+ seen.add(canonical)
91
+ return dir
92
+ }
93
+ return null
94
+ }
95
+
96
+ function listChatFiles(geminiRoot: string): string[] {
97
+ const tmpDir = join(geminiRoot, 'tmp')
98
+ if (!existsSync(tmpDir)) return []
99
+
100
+ const files: string[] = []
101
+ for (const projectEntry of readdirSync(tmpDir, { withFileTypes: true })) {
102
+ if (!projectEntry.isDirectory()) continue
103
+ const chatsDir = join(tmpDir, projectEntry.name, 'chats')
104
+ if (!existsSync(chatsDir)) continue
105
+ for (const fileEntry of readdirSync(chatsDir, { withFileTypes: true })) {
106
+ if (fileEntry.isFile() && fileEntry.name.startsWith('session-') && fileEntry.name.endsWith('.json')) {
107
+ files.push(join(chatsDir, fileEntry.name))
108
+ }
109
+ }
110
+ }
111
+ return files
112
+ }
113
+
114
+ export const geminiCollector: Collector = {
115
+ id: 'gemini',
116
+ name: 'Gemini',
117
+
118
+ detect(homeDir: string): boolean {
119
+ const root = resolveGeminiRoot(homeDir)
120
+ return root != null && existsSync(join(root, 'tmp'))
121
+ },
122
+
123
+ collect(homeDir: string): RawSession[] {
124
+ const geminiRoot = resolveGeminiRoot(homeDir)
125
+ if (!geminiRoot) return []
126
+ const nameToPath = loadProjectNameMap(geminiRoot)
127
+
128
+ const sessions: RawSession[] = []
129
+ for (const filePath of listChatFiles(geminiRoot)) {
130
+ try {
131
+ if (!statSync(filePath).isFile()) continue
132
+ const projectDir = basename(dirname(dirname(filePath)))
133
+ const session = parseChatFile(filePath, projectDir, nameToPath)
134
+ if (session) sessions.push(session)
135
+ } catch {
136
+ continue
137
+ }
138
+ }
139
+ return sessions
140
+ },
141
+ }
@@ -0,0 +1,20 @@
1
+ import { claudeCodeCollector } from './claude-code'
2
+ import { codebuddyCollector } from './codebuddy'
3
+ import { codexCollector } from './codex'
4
+ import { cursorCollector } from './cursor'
5
+ import { geminiCollector } from './gemini'
6
+ import type { Collector } from './types'
7
+
8
+ export const COLLECTORS: Collector[] = [
9
+ claudeCodeCollector,
10
+ codexCollector,
11
+ cursorCollector,
12
+ geminiCollector,
13
+ codebuddyCollector,
14
+ ]
15
+
16
+ export function getCollector(id: string): Collector | null {
17
+ return COLLECTORS.find((collector) => collector.id === id) ?? null
18
+ }
19
+
20
+ export type { Collector, RawMessage, RawSession } from './types'
@@ -0,0 +1,22 @@
1
+ export interface RawMessage {
2
+ role: string
3
+ content: string
4
+ timestamp: string | null
5
+ }
6
+
7
+ export interface RawSession {
8
+ sourceSessionId: string
9
+ rawPath: string
10
+ rawProjectPath: string | null
11
+ title: string | null
12
+ createdAt: string | null
13
+ updatedAt: string | null
14
+ messages: RawMessage[]
15
+ }
16
+
17
+ export interface Collector {
18
+ id: string
19
+ name: string
20
+ detect(homeDir: string): boolean
21
+ collect(homeDir: string): RawSession[]
22
+ }