@codehourra/llm-iwiki 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,120 +0,0 @@
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, normalizeContentParts } from './util'
6
-
7
- const PROJECT_ROOTS = ['.claude/projects', '.claude-internal/projects']
8
-
9
- function parseSessionFile(filePath: string): RawSession | null {
10
- const raw = readFileSync(filePath, 'utf8')
11
- const lines = raw.split('\n')
12
-
13
- const messages: RawMessage[] = []
14
- let rawProjectPath: string | null = null
15
- let sourceSessionId: string | null = null
16
-
17
- for (const line of lines) {
18
- const trimmed = line.trim()
19
- if (trimmed === '') continue
20
-
21
- let parsed: Record<string, unknown>
22
- try {
23
- parsed = JSON.parse(trimmed) as Record<string, unknown>
24
- } catch {
25
- continue
26
- }
27
-
28
- if (typeof parsed.sessionId === 'string' && !sourceSessionId) {
29
- sourceSessionId = parsed.sessionId
30
- }
31
- if (typeof parsed.cwd === 'string' && !rawProjectPath) {
32
- rawProjectPath = parsed.cwd
33
- }
34
-
35
- const type = parsed.type
36
- if (type !== 'user' && type !== 'assistant') continue
37
- if (parsed.isSidechain === true) continue
38
-
39
- const message = parsed.message
40
- if (!message || typeof message !== 'object') continue
41
- const messageRecord = message as Record<string, unknown>
42
- const role = typeof messageRecord.role === 'string' ? messageRecord.role : type
43
- const content = normalizeContentParts(messageRecord.content)
44
- if (content.trim() === '') continue
45
-
46
- messages.push({
47
- role,
48
- content,
49
- timestamp: typeof parsed.timestamp === 'string' ? parsed.timestamp : null,
50
- })
51
- }
52
-
53
- if (messages.length === 0) return null
54
- if (isEphemeralPath(rawProjectPath)) return null
55
-
56
- sourceSessionId ??= basename(filePath).replace(/\.jsonl$/, '')
57
- const timestamps = messages.map((message) => message.timestamp).filter((value): value is string => value != null)
58
-
59
- return {
60
- sourceSessionId,
61
- rawPath: filePath,
62
- rawProjectPath,
63
- title: deriveTitle(messages),
64
- createdAt: timestamps[0] ?? null,
65
- updatedAt: timestamps[timestamps.length - 1] ?? null,
66
- messages,
67
- }
68
- }
69
-
70
- function listSessionFiles(homeDir: string): string[] {
71
- const files: string[] = []
72
- const seenRoots = new Set<string>()
73
- for (const root of PROJECT_ROOTS) {
74
- const projectsDir = join(homeDir, root)
75
- if (!existsSync(projectsDir)) continue
76
-
77
- let canonicalRoot: string
78
- try {
79
- canonicalRoot = realpathSync(projectsDir)
80
- } catch {
81
- canonicalRoot = projectsDir
82
- }
83
- if (seenRoots.has(canonicalRoot)) continue
84
- seenRoots.add(canonicalRoot)
85
-
86
- for (const projectEntry of readdirSync(projectsDir, { withFileTypes: true })) {
87
- if (!projectEntry.isDirectory()) continue
88
- const projectDir = join(projectsDir, projectEntry.name)
89
- for (const fileEntry of readdirSync(projectDir, { withFileTypes: true })) {
90
- if (fileEntry.isFile() && fileEntry.name.endsWith('.jsonl')) {
91
- files.push(join(projectDir, fileEntry.name))
92
- }
93
- }
94
- }
95
- }
96
- return files
97
- }
98
-
99
- export const claudeCodeCollector: Collector = {
100
- id: 'claude-code',
101
- name: 'Claude Code',
102
-
103
- detect(homeDir: string): boolean {
104
- return PROJECT_ROOTS.some((root) => existsSync(join(homeDir, root)))
105
- },
106
-
107
- collect(homeDir: string): RawSession[] {
108
- const sessions: RawSession[] = []
109
- for (const filePath of listSessionFiles(homeDir)) {
110
- try {
111
- if (!statSync(filePath).isFile()) continue
112
- const session = parseSessionFile(filePath)
113
- if (session) sessions.push(session)
114
- } catch {
115
- continue
116
- }
117
- }
118
- return sessions
119
- },
120
- }
@@ -1,164 +0,0 @@
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
- }
@@ -1,130 +0,0 @@
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
- }
@@ -1,178 +0,0 @@
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
- }