claudecode-history-viewer 1.0.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.
- package/LICENSE +21 -0
- package/README.en-US.md +125 -0
- package/README.md +125 -0
- package/SECURITY.md +23 -0
- package/claude-history-viewer.png +0 -0
- package/index.html +27 -0
- package/package.json +44 -0
- package/postcss.config.js +6 -0
- package/server/export.js +73 -0
- package/server/index.js +190 -0
- package/server/open-browser.js +37 -0
- package/server/parser-cursor.js +125 -0
- package/server/parser.js +208 -0
- package/server/scanner-cursor.js +177 -0
- package/server/scanner.js +285 -0
- package/server/search-utils.js +93 -0
- package/src/App.vue +338 -0
- package/src/components/ChatView.vue +353 -0
- package/src/components/MarkdownRenderer.vue +63 -0
- package/src/components/MessageBubble.vue +105 -0
- package/src/components/RawJsonView.vue +116 -0
- package/src/components/SessionItem.vue +48 -0
- package/src/components/SessionSidebar.vue +202 -0
- package/src/components/SettingsMenu.vue +63 -0
- package/src/components/ThemeToggle.vue +47 -0
- package/src/components/ToolCallBlock.vue +43 -0
- package/src/components/ToolResultBlock.vue +46 -0
- package/src/composables/useLocale.js +68 -0
- package/src/composables/useTheme.js +49 -0
- package/src/i18n/messages.js +135 -0
- package/src/main.js +7 -0
- package/src/styles/main.css +151 -0
- package/src/utils/format.js +37 -0
- package/src/utils/highlight.js +135 -0
- package/src/utils/hljs-theme.js +22 -0
- package/src/utils/markdown.js +59 -0
- package/tailwind.config.js +44 -0
- package/vite.config.js +16 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { searchMessages, dedupeHitsByMessage } from './search-utils.js'
|
|
3
|
+
|
|
4
|
+
/** 从 Cursor 用户消息中提取可读文本 */
|
|
5
|
+
function extractCursorUserText(content) {
|
|
6
|
+
let text = ''
|
|
7
|
+
if (typeof content === 'string') text = content
|
|
8
|
+
else if (Array.isArray(content)) {
|
|
9
|
+
text = content
|
|
10
|
+
.filter((c) => c?.type === 'text')
|
|
11
|
+
.map((c) => c.text || '')
|
|
12
|
+
.join('\n')
|
|
13
|
+
}
|
|
14
|
+
text = text.replace(/<timestamp>[\s\S]*?<\/timestamp>\s*/gi, '')
|
|
15
|
+
const m = text.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i)
|
|
16
|
+
if (m) text = m[1].trim()
|
|
17
|
+
return text.trim()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 解析 Cursor agent-transcripts JSONL
|
|
22
|
+
* 格式:每行 { role: 'user'|'assistant', message: { content: [...] } }
|
|
23
|
+
*/
|
|
24
|
+
export function parseCursorTranscript(filePath) {
|
|
25
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
26
|
+
const lines = raw.split('\n').filter((l) => l.trim())
|
|
27
|
+
|
|
28
|
+
const messages = []
|
|
29
|
+
let title = ''
|
|
30
|
+
let lineIdx = 0
|
|
31
|
+
let assistantRun = null
|
|
32
|
+
|
|
33
|
+
const flushAssistantRun = () => {
|
|
34
|
+
if (!assistantRun) return
|
|
35
|
+
if (assistantRun.text || assistantRun.toolUses.length > 0 || assistantRun.thinking) {
|
|
36
|
+
messages.push(assistantRun)
|
|
37
|
+
}
|
|
38
|
+
assistantRun = null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
lineIdx++
|
|
43
|
+
let row
|
|
44
|
+
try {
|
|
45
|
+
row = JSON.parse(line)
|
|
46
|
+
} catch {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const role = row.role || row.type
|
|
51
|
+
if (!role) continue
|
|
52
|
+
|
|
53
|
+
if (role === 'user') {
|
|
54
|
+
flushAssistantRun()
|
|
55
|
+
const text = extractCursorUserText(row.message?.content)
|
|
56
|
+
if (text) {
|
|
57
|
+
if (!title) title = text.slice(0, 60)
|
|
58
|
+
messages.push({
|
|
59
|
+
id: `cursor-user-${lineIdx}`,
|
|
60
|
+
role: 'user',
|
|
61
|
+
text,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (role === 'assistant') {
|
|
68
|
+
const content = row.message?.content
|
|
69
|
+
if (!Array.isArray(content)) continue
|
|
70
|
+
|
|
71
|
+
if (!assistantRun) {
|
|
72
|
+
assistantRun = {
|
|
73
|
+
id: `cursor-asst-${lineIdx}`,
|
|
74
|
+
role: 'assistant',
|
|
75
|
+
model: 'Cursor',
|
|
76
|
+
thinking: '',
|
|
77
|
+
text: '',
|
|
78
|
+
toolUses: [],
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const block of content) {
|
|
83
|
+
if (!block || typeof block !== 'object') continue
|
|
84
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
85
|
+
assistantRun.thinking += block.thinking
|
|
86
|
+
}
|
|
87
|
+
if (block.type === 'text' && block.text) {
|
|
88
|
+
assistantRun.text += block.text
|
|
89
|
+
}
|
|
90
|
+
if (block.type === 'tool_use' || block.name) {
|
|
91
|
+
assistantRun.toolUses.push({
|
|
92
|
+
id: block.id || `tool-${lineIdx}-${assistantRun.toolUses.length}`,
|
|
93
|
+
name: block.name || block.type,
|
|
94
|
+
input: block.input || {},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
flushAssistantRun()
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
meta: { title, cwd: '', sessionId: '', source: 'cursor' },
|
|
105
|
+
messages: messages.filter((m) => {
|
|
106
|
+
if (m.role === 'assistant') {
|
|
107
|
+
return m.text || m.thinking || m.toolUses?.length > 0
|
|
108
|
+
}
|
|
109
|
+
return true
|
|
110
|
+
}),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function searchInCursorTranscript(filePath, query) {
|
|
115
|
+
if (!query?.trim()) {
|
|
116
|
+
return { matches: [], hits: [], total: 0 }
|
|
117
|
+
}
|
|
118
|
+
const { messages } = parseCursorTranscript(filePath)
|
|
119
|
+
const matches = searchMessages(messages, query)
|
|
120
|
+
return {
|
|
121
|
+
matches,
|
|
122
|
+
hits: dedupeHitsByMessage(matches),
|
|
123
|
+
total: matches.length,
|
|
124
|
+
}
|
|
125
|
+
}
|
package/server/parser.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { searchMessages, dedupeHitsByMessage } from './search-utils.js'
|
|
3
|
+
|
|
4
|
+
const SKIP_TYPES = new Set([
|
|
5
|
+
'file-history-snapshot',
|
|
6
|
+
'permission-mode',
|
|
7
|
+
'last-prompt',
|
|
8
|
+
'queue-operation',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 解析 JSONL 为前端可渲染的消息列表
|
|
13
|
+
* @returns {{ messages: object[], meta: object }}
|
|
14
|
+
*/
|
|
15
|
+
export function parseTranscript(filePath) {
|
|
16
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
17
|
+
const lines = raw.split('\n').filter((l) => l.trim())
|
|
18
|
+
|
|
19
|
+
const messages = []
|
|
20
|
+
const assistantBuffer = new Map() // messageId -> merged assistant
|
|
21
|
+
let title = ''
|
|
22
|
+
let cwd = ''
|
|
23
|
+
let sessionId = ''
|
|
24
|
+
|
|
25
|
+
const flushAssistant = (msgId) => {
|
|
26
|
+
const buf = assistantBuffer.get(msgId)
|
|
27
|
+
if (!buf) return
|
|
28
|
+
messages.push(buf)
|
|
29
|
+
assistantBuffer.delete(msgId)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
let row
|
|
34
|
+
try {
|
|
35
|
+
row = JSON.parse(line)
|
|
36
|
+
} catch {
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (row.sessionId) sessionId = row.sessionId
|
|
41
|
+
if (row.cwd) cwd = row.cwd
|
|
42
|
+
if (row.type === 'ai-title' && row.aiTitle) title = row.aiTitle
|
|
43
|
+
|
|
44
|
+
if (SKIP_TYPES.has(row.type)) continue
|
|
45
|
+
|
|
46
|
+
if (row.type === 'attachment') {
|
|
47
|
+
const att = row.attachment
|
|
48
|
+
if (att?.type === 'skill_listing') continue // 技能列表太长,默认折叠为摘要
|
|
49
|
+
messages.push({
|
|
50
|
+
id: row.uuid,
|
|
51
|
+
role: 'system',
|
|
52
|
+
subtype: 'attachment',
|
|
53
|
+
timestamp: row.timestamp,
|
|
54
|
+
attachmentType: att?.type,
|
|
55
|
+
text: typeof att?.content === 'string' ? att.content : JSON.stringify(att, null, 2),
|
|
56
|
+
})
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (row.type === 'system') {
|
|
61
|
+
if (row.subtype === 'turn_duration') {
|
|
62
|
+
messages.push({
|
|
63
|
+
id: row.uuid,
|
|
64
|
+
role: 'system',
|
|
65
|
+
subtype: 'turn_duration',
|
|
66
|
+
timestamp: row.timestamp,
|
|
67
|
+
durationMs: row.durationMs,
|
|
68
|
+
messageCount: row.messageCount,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (row.type === 'user') {
|
|
75
|
+
// flush any pending assistants before user turn
|
|
76
|
+
for (const msgId of [...assistantBuffer.keys()]) {
|
|
77
|
+
flushAssistant(msgId)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const content = row.message?.content
|
|
81
|
+
const toolResults = []
|
|
82
|
+
let text = ''
|
|
83
|
+
|
|
84
|
+
if (typeof content === 'string') {
|
|
85
|
+
text = content
|
|
86
|
+
} else if (Array.isArray(content)) {
|
|
87
|
+
for (const block of content) {
|
|
88
|
+
if (block?.type === 'text') text += (block.text || '') + '\n'
|
|
89
|
+
if (block?.type === 'tool_result') {
|
|
90
|
+
toolResults.push({
|
|
91
|
+
toolUseId: block.tool_use_id,
|
|
92
|
+
content: normalizeToolResultContent(block.content),
|
|
93
|
+
isError: !!block.is_error,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
text = text.trim()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (toolResults.length > 0) {
|
|
101
|
+
for (const tr of toolResults) {
|
|
102
|
+
messages.push({
|
|
103
|
+
id: `${row.uuid}-${tr.toolUseId}`,
|
|
104
|
+
role: 'tool_result',
|
|
105
|
+
timestamp: row.timestamp,
|
|
106
|
+
toolUseId: tr.toolUseId,
|
|
107
|
+
content: tr.content,
|
|
108
|
+
isError: tr.isError,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (text) {
|
|
114
|
+
messages.push({
|
|
115
|
+
id: row.uuid,
|
|
116
|
+
role: 'user',
|
|
117
|
+
timestamp: row.timestamp,
|
|
118
|
+
text,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (row.type === 'assistant') {
|
|
125
|
+
const msg = row.message
|
|
126
|
+
if (!msg) continue
|
|
127
|
+
|
|
128
|
+
const msgId = msg.id || row.uuid
|
|
129
|
+
let buf = assistantBuffer.get(msgId)
|
|
130
|
+
if (!buf) {
|
|
131
|
+
buf = {
|
|
132
|
+
id: row.uuid,
|
|
133
|
+
messageId: msgId,
|
|
134
|
+
role: 'assistant',
|
|
135
|
+
timestamp: row.timestamp,
|
|
136
|
+
model: msg.model,
|
|
137
|
+
thinking: '',
|
|
138
|
+
text: '',
|
|
139
|
+
toolUses: [],
|
|
140
|
+
}
|
|
141
|
+
assistantBuffer.set(msgId, buf)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (row.timestamp) buf.timestamp = row.timestamp
|
|
145
|
+
if (msg.model) buf.model = msg.model
|
|
146
|
+
|
|
147
|
+
for (const block of msg.content || []) {
|
|
148
|
+
if (!block || typeof block !== 'object') continue
|
|
149
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
150
|
+
buf.thinking += block.thinking
|
|
151
|
+
}
|
|
152
|
+
if (block.type === 'text' && block.text) {
|
|
153
|
+
buf.text += block.text
|
|
154
|
+
}
|
|
155
|
+
if (block.type === 'tool_use') {
|
|
156
|
+
buf.toolUses.push({
|
|
157
|
+
id: block.id,
|
|
158
|
+
name: block.name,
|
|
159
|
+
input: block.input,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// flush remaining assistants
|
|
167
|
+
for (const msgId of [...assistantBuffer.keys()]) {
|
|
168
|
+
flushAssistant(msgId)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
meta: { sessionId, title, cwd },
|
|
173
|
+
messages: messages.filter((m) => {
|
|
174
|
+
if (m.role === 'assistant') {
|
|
175
|
+
return m.text || m.thinking || (m.toolUses && m.toolUses.length > 0)
|
|
176
|
+
}
|
|
177
|
+
return true
|
|
178
|
+
}),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeToolResultContent(content) {
|
|
183
|
+
if (typeof content === 'string') return content
|
|
184
|
+
if (Array.isArray(content)) {
|
|
185
|
+
return content
|
|
186
|
+
.map((c) => {
|
|
187
|
+
if (typeof c === 'string') return c
|
|
188
|
+
if (c?.type === 'text') return c.text
|
|
189
|
+
return JSON.stringify(c)
|
|
190
|
+
})
|
|
191
|
+
.join('\n')
|
|
192
|
+
}
|
|
193
|
+
return JSON.stringify(content, null, 2)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 在会话 transcript 中全文搜索 */
|
|
197
|
+
export function searchInTranscript(filePath, query) {
|
|
198
|
+
if (!query?.trim()) {
|
|
199
|
+
return { matches: [], hits: [], total: 0 }
|
|
200
|
+
}
|
|
201
|
+
const { messages } = parseTranscript(filePath)
|
|
202
|
+
const matches = searchMessages(messages, query)
|
|
203
|
+
return {
|
|
204
|
+
matches,
|
|
205
|
+
hits: dedupeHitsByMessage(matches),
|
|
206
|
+
total: matches.length,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { decodeProjectSlug } from './scanner.js'
|
|
5
|
+
|
|
6
|
+
const CURSOR_DIR = path.join(os.homedir(), '.cursor')
|
|
7
|
+
const PROJECTS_DIR = path.join(CURSOR_DIR, 'projects')
|
|
8
|
+
const SOURCE = 'cursor'
|
|
9
|
+
const ID_PREFIX = 'cursor::'
|
|
10
|
+
|
|
11
|
+
export function cursorSessionId(uuid) {
|
|
12
|
+
return `${ID_PREFIX}${uuid}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseCursorSessionId(id) {
|
|
16
|
+
if (!id.startsWith(ID_PREFIX)) return null
|
|
17
|
+
const rest = id.slice(ID_PREFIX.length)
|
|
18
|
+
if (rest.includes('::sub::')) {
|
|
19
|
+
const [parent, agent] = rest.split('::sub::')
|
|
20
|
+
return { uuid: parent, parentSessionId: cursorSessionId(parent), agentId: agent, kind: 'subagent' }
|
|
21
|
+
}
|
|
22
|
+
return { uuid: rest, kind: 'main' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function statSafe(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.statSync(filePath)
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractPreview(filePath) {
|
|
34
|
+
let title = ''
|
|
35
|
+
let text = ''
|
|
36
|
+
let messageCount = 0
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
40
|
+
for (const line of content.split('\n').slice(0, 100)) {
|
|
41
|
+
if (!line.trim()) continue
|
|
42
|
+
try {
|
|
43
|
+
const row = JSON.parse(line)
|
|
44
|
+
const role = row.role || row.type
|
|
45
|
+
if (role === 'user' && row.message?.content) {
|
|
46
|
+
messageCount++
|
|
47
|
+
const c = row.message.content
|
|
48
|
+
let t = ''
|
|
49
|
+
if (typeof c === 'string') t = c
|
|
50
|
+
else if (Array.isArray(c)) {
|
|
51
|
+
t = c.filter((x) => x?.type === 'text').map((x) => x.text).join('')
|
|
52
|
+
}
|
|
53
|
+
const m = t.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i)
|
|
54
|
+
if (m) t = m[1]
|
|
55
|
+
t = t.replace(/<timestamp>[\s\S]*?<\/timestamp>/gi, '').trim()
|
|
56
|
+
if (t && !text) text = t.slice(0, 120)
|
|
57
|
+
if (t && !title) title = t.slice(0, 60)
|
|
58
|
+
}
|
|
59
|
+
if (role === 'assistant') messageCount++
|
|
60
|
+
} catch {
|
|
61
|
+
/* skip */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const line of content.split('\n')) {
|
|
65
|
+
if (!line.trim()) continue
|
|
66
|
+
try {
|
|
67
|
+
const row = JSON.parse(line)
|
|
68
|
+
if (row.role === 'user' || row.role === 'assistant') messageCount++
|
|
69
|
+
} catch {
|
|
70
|
+
/* skip */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { title, text, messageCount }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function decodeCursorProjectSlug(slug) {
|
|
81
|
+
if (/^\d+$/.test(slug)) return `Cursor 工作区 #${slug}`
|
|
82
|
+
return decodeProjectSlug(slug)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildRecord({
|
|
86
|
+
id,
|
|
87
|
+
projectSlug,
|
|
88
|
+
filePath,
|
|
89
|
+
kind,
|
|
90
|
+
parentSessionId,
|
|
91
|
+
stat,
|
|
92
|
+
}) {
|
|
93
|
+
const preview = extractPreview(filePath)
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
source: SOURCE,
|
|
97
|
+
kind,
|
|
98
|
+
parentSessionId,
|
|
99
|
+
projectSlug,
|
|
100
|
+
projectPath: decodeCursorProjectSlug(projectSlug),
|
|
101
|
+
filePath,
|
|
102
|
+
title: preview.title || (kind === 'subagent' ? 'Sub-agent' : '未命名会话'),
|
|
103
|
+
preview: preview.text,
|
|
104
|
+
cwd: decodeCursorProjectSlug(projectSlug),
|
|
105
|
+
startedAt: stat?.birthtimeMs,
|
|
106
|
+
updatedAt: stat?.mtimeMs,
|
|
107
|
+
messageCount: preview.messageCount,
|
|
108
|
+
agentType: kind === 'subagent' ? 'cursor-subagent' : null,
|
|
109
|
+
agentDescription: null,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scanCursorSubagents(projectSlug, parentUuid, transcriptsDir) {
|
|
114
|
+
const subDir = path.join(transcriptsDir, parentUuid, 'subagents')
|
|
115
|
+
if (!fs.existsSync(subDir)) return []
|
|
116
|
+
|
|
117
|
+
const list = []
|
|
118
|
+
for (const entry of fs.readdirSync(subDir)) {
|
|
119
|
+
if (!entry.endsWith('.jsonl')) continue
|
|
120
|
+
const filePath = path.join(subDir, entry)
|
|
121
|
+
const stat = statSafe(filePath)
|
|
122
|
+
if (!stat) continue
|
|
123
|
+
|
|
124
|
+
const agentId = entry.replace(/\.jsonl$/, '')
|
|
125
|
+
const parentId = cursorSessionId(parentUuid)
|
|
126
|
+
list.push(
|
|
127
|
+
buildRecord({
|
|
128
|
+
id: `${parentId}::sub::${agentId}`,
|
|
129
|
+
projectSlug,
|
|
130
|
+
filePath,
|
|
131
|
+
kind: 'subagent',
|
|
132
|
+
parentSessionId: parentId,
|
|
133
|
+
stat,
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
list.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
138
|
+
return list
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 扫描 ~/.cursor/projects 下 agent-transcripts 目录中的 jsonl */
|
|
142
|
+
export function scanCursorSessions() {
|
|
143
|
+
const sessions = []
|
|
144
|
+
if (!fs.existsSync(PROJECTS_DIR)) return sessions
|
|
145
|
+
|
|
146
|
+
for (const projectSlug of fs.readdirSync(PROJECTS_DIR)) {
|
|
147
|
+
const transcriptsDir = path.join(PROJECTS_DIR, projectSlug, 'agent-transcripts')
|
|
148
|
+
if (!fs.existsSync(transcriptsDir)) continue
|
|
149
|
+
|
|
150
|
+
for (const sessionUuid of fs.readdirSync(transcriptsDir)) {
|
|
151
|
+
const sessionDir = path.join(transcriptsDir, sessionUuid)
|
|
152
|
+
if (!fs.statSync(sessionDir).isDirectory()) continue
|
|
153
|
+
|
|
154
|
+
const mainFile = path.join(sessionDir, `${sessionUuid}.jsonl`)
|
|
155
|
+
if (fs.existsSync(mainFile) && fs.statSync(mainFile).isFile()) {
|
|
156
|
+
const stat = statSafe(mainFile)
|
|
157
|
+
sessions.push(
|
|
158
|
+
buildRecord({
|
|
159
|
+
id: cursorSessionId(sessionUuid),
|
|
160
|
+
projectSlug,
|
|
161
|
+
filePath: mainFile,
|
|
162
|
+
kind: 'main',
|
|
163
|
+
parentSessionId: null,
|
|
164
|
+
stat,
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
sessions.push(...scanCursorSubagents(projectSlug, sessionUuid, transcriptsDir))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return sessions
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getCursorDir() {
|
|
176
|
+
return CURSOR_DIR
|
|
177
|
+
}
|