@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.
- package/dist/index.js +514 -0
- package/package.json +7 -8
- package/src/ai-yaml.ts +0 -82
- package/src/cli.ts +0 -485
- package/src/collectors/claude-code.ts +0 -120
- package/src/collectors/codebuddy.ts +0 -164
- package/src/collectors/codex.ts +0 -130
- package/src/collectors/cursor.ts +0 -178
- package/src/collectors/gemini.ts +0 -141
- package/src/collectors/index.ts +0 -20
- package/src/collectors/types.ts +0 -22
- package/src/collectors/util.ts +0 -58
- package/src/compaction.ts +0 -63
- package/src/config.ts +0 -57
- package/src/db.ts +0 -152
- package/src/experiences.ts +0 -235
- package/src/index.ts +0 -10
- package/src/obsidian.ts +0 -345
- package/src/paths.ts +0 -23
- package/src/projects.ts +0 -192
- package/src/sessions.ts +0 -119
- package/src/skills.ts +0 -168
- package/src/summarize.ts +0 -122
- package/src/sync.ts +0 -207
- package/src/types.ts +0 -26
|
@@ -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
|
-
}
|
package/src/collectors/codex.ts
DELETED
|
@@ -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
|
-
}
|
package/src/collectors/cursor.ts
DELETED
|
@@ -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
|
-
}
|