codeblog-app 1.6.4 → 2.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/package.json +4 -18
- package/src/ai/__tests__/chat.test.ts +110 -0
- package/src/ai/__tests__/provider.test.ts +184 -0
- package/src/ai/__tests__/tools.test.ts +90 -0
- package/src/ai/chat.ts +81 -50
- package/src/ai/provider.ts +24 -250
- package/src/ai/tools.ts +46 -281
- package/src/auth/oauth.ts +7 -0
- package/src/cli/__tests__/commands.test.ts +225 -0
- package/src/cli/__tests__/setup.test.ts +57 -0
- package/src/cli/cmd/agent.ts +102 -0
- package/src/cli/cmd/chat.ts +1 -1
- package/src/cli/cmd/comment.ts +47 -16
- package/src/cli/cmd/feed.ts +18 -30
- package/src/cli/cmd/forum.ts +123 -0
- package/src/cli/cmd/login.ts +9 -2
- package/src/cli/cmd/me.ts +202 -0
- package/src/cli/cmd/post.ts +6 -88
- package/src/cli/cmd/publish.ts +44 -23
- package/src/cli/cmd/scan.ts +45 -34
- package/src/cli/cmd/search.ts +8 -70
- package/src/cli/cmd/setup.ts +160 -62
- package/src/cli/cmd/vote.ts +29 -14
- package/src/cli/cmd/whoami.ts +7 -36
- package/src/cli/ui.ts +50 -0
- package/src/index.ts +80 -59
- package/src/mcp/__tests__/client.test.ts +149 -0
- package/src/mcp/__tests__/e2e.ts +327 -0
- package/src/mcp/__tests__/integration.ts +148 -0
- package/src/mcp/client.ts +148 -0
- package/src/api/agents.ts +0 -103
- package/src/api/bookmarks.ts +0 -25
- package/src/api/client.ts +0 -96
- package/src/api/debates.ts +0 -35
- package/src/api/feed.ts +0 -25
- package/src/api/notifications.ts +0 -31
- package/src/api/posts.ts +0 -116
- package/src/api/search.ts +0 -29
- package/src/api/tags.ts +0 -13
- package/src/api/trending.ts +0 -38
- package/src/api/users.ts +0 -8
- package/src/cli/cmd/agents.ts +0 -77
- package/src/cli/cmd/ai-publish.ts +0 -118
- package/src/cli/cmd/bookmark.ts +0 -27
- package/src/cli/cmd/bookmarks.ts +0 -42
- package/src/cli/cmd/dashboard.ts +0 -59
- package/src/cli/cmd/debate.ts +0 -89
- package/src/cli/cmd/delete.ts +0 -35
- package/src/cli/cmd/edit.ts +0 -42
- package/src/cli/cmd/explore.ts +0 -63
- package/src/cli/cmd/follow.ts +0 -34
- package/src/cli/cmd/myposts.ts +0 -50
- package/src/cli/cmd/notifications.ts +0 -65
- package/src/cli/cmd/tags.ts +0 -58
- package/src/cli/cmd/trending.ts +0 -64
- package/src/cli/cmd/weekly-digest.ts +0 -117
- package/src/publisher/index.ts +0 -139
- package/src/scanner/__tests__/analyzer.test.ts +0 -67
- package/src/scanner/__tests__/fs-utils.test.ts +0 -50
- package/src/scanner/__tests__/platform.test.ts +0 -27
- package/src/scanner/__tests__/registry.test.ts +0 -56
- package/src/scanner/aider.ts +0 -96
- package/src/scanner/analyzer.ts +0 -237
- package/src/scanner/claude-code.ts +0 -188
- package/src/scanner/codex.ts +0 -127
- package/src/scanner/continue-dev.ts +0 -95
- package/src/scanner/cursor.ts +0 -299
- package/src/scanner/fs-utils.ts +0 -123
- package/src/scanner/index.ts +0 -26
- package/src/scanner/platform.ts +0 -44
- package/src/scanner/registry.ts +0 -68
- package/src/scanner/types.ts +0 -62
- package/src/scanner/vscode-copilot.ts +0 -125
- package/src/scanner/warp.ts +0 -19
- package/src/scanner/windsurf.ts +0 -147
- package/src/scanner/zed.ts +0 -88
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import * as path from "path"
|
|
2
|
-
import * as fs from "fs"
|
|
3
|
-
import type { Scanner, Session, ParsedSession, ConversationTurn } from "./types"
|
|
4
|
-
import { getHome, getPlatform } from "./platform"
|
|
5
|
-
import { listFiles, safeReadJson, safeStats } from "./fs-utils"
|
|
6
|
-
|
|
7
|
-
export const continueDevScanner: Scanner = {
|
|
8
|
-
name: "Continue.dev",
|
|
9
|
-
sourceType: "continue",
|
|
10
|
-
description: "Continue.dev AI coding assistant sessions",
|
|
11
|
-
|
|
12
|
-
getSessionDirs(): string[] {
|
|
13
|
-
const home = getHome()
|
|
14
|
-
const platform = getPlatform()
|
|
15
|
-
const candidates: string[] = [path.join(home, ".continue", "sessions")]
|
|
16
|
-
if (platform === "macos") {
|
|
17
|
-
candidates.push(path.join(home, "Library", "Application Support", "Continue", "sessions"))
|
|
18
|
-
} else if (platform === "windows") {
|
|
19
|
-
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming")
|
|
20
|
-
candidates.push(path.join(appData, "Continue", "sessions"))
|
|
21
|
-
} else {
|
|
22
|
-
candidates.push(path.join(home, ".config", "continue", "sessions"))
|
|
23
|
-
}
|
|
24
|
-
return candidates.filter((d) => {
|
|
25
|
-
try { return fs.existsSync(d) } catch { return false }
|
|
26
|
-
})
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
scan(limit: number): Session[] {
|
|
30
|
-
const sessions: Session[] = []
|
|
31
|
-
for (const dir of this.getSessionDirs()) {
|
|
32
|
-
for (const filePath of listFiles(dir, [".json"])) {
|
|
33
|
-
const stats = safeStats(filePath)
|
|
34
|
-
if (!stats || stats.size < 100) continue
|
|
35
|
-
const data = safeReadJson<Record<string, unknown>>(filePath)
|
|
36
|
-
if (!data) continue
|
|
37
|
-
const turns = extractContinueTurns(data)
|
|
38
|
-
if (turns.length < 2) continue
|
|
39
|
-
const humanMsgs = turns.filter((t) => t.role === "human")
|
|
40
|
-
const preview = humanMsgs[0]?.content.slice(0, 200) || "(continue session)"
|
|
41
|
-
sessions.push({
|
|
42
|
-
id: path.basename(filePath, ".json"), source: "continue",
|
|
43
|
-
project: (data.workspacePath as string) || path.basename(path.dirname(filePath)),
|
|
44
|
-
title: (data.title as string) || preview.slice(0, 80),
|
|
45
|
-
messageCount: turns.length, humanMessages: humanMsgs.length, aiMessages: turns.length - humanMsgs.length,
|
|
46
|
-
preview, filePath, modifiedAt: stats.mtime, sizeBytes: stats.size,
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
|
|
51
|
-
return sessions.slice(0, limit)
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
parse(filePath: string, maxTurns?: number): ParsedSession | null {
|
|
55
|
-
const data = safeReadJson<Record<string, unknown>>(filePath)
|
|
56
|
-
if (!data) return null
|
|
57
|
-
const stats = safeStats(filePath)
|
|
58
|
-
const turns = extractContinueTurns(data, maxTurns)
|
|
59
|
-
if (turns.length === 0) return null
|
|
60
|
-
const humanMsgs = turns.filter((t) => t.role === "human")
|
|
61
|
-
const aiMsgs = turns.filter((t) => t.role === "assistant")
|
|
62
|
-
return {
|
|
63
|
-
id: path.basename(filePath, ".json"), source: "continue",
|
|
64
|
-
project: (data.workspacePath as string) || path.basename(path.dirname(filePath)),
|
|
65
|
-
title: (data.title as string) || humanMsgs[0]?.content.slice(0, 80) || "Continue session",
|
|
66
|
-
messageCount: turns.length, humanMessages: humanMsgs.length, aiMessages: aiMsgs.length,
|
|
67
|
-
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
68
|
-
filePath, modifiedAt: stats?.mtime || new Date(), sizeBytes: stats?.size || 0, turns,
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function extractContinueTurns(data: Record<string, unknown>, maxTurns?: number): ConversationTurn[] {
|
|
74
|
-
const turns: ConversationTurn[] = []
|
|
75
|
-
for (const arr of [data.history, data.messages, data.steps]) {
|
|
76
|
-
if (!Array.isArray(arr)) continue
|
|
77
|
-
for (const msg of arr) {
|
|
78
|
-
if (maxTurns && turns.length >= maxTurns) break
|
|
79
|
-
if (!msg || typeof msg !== "object") continue
|
|
80
|
-
const m = msg as Record<string, unknown>
|
|
81
|
-
if (m.name === "UserInput" && typeof m.description === "string") {
|
|
82
|
-
turns.push({ role: "human", content: m.description }); continue
|
|
83
|
-
}
|
|
84
|
-
if (m.name === "DefaultModelEditCodeStep" || m.name === "ChatModelResponse") {
|
|
85
|
-
if (typeof m.description === "string") turns.push({ role: "assistant", content: m.description })
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
const content = (m.content || m.text || m.message) as string | undefined
|
|
89
|
-
if (typeof content !== "string") continue
|
|
90
|
-
turns.push({ role: m.role === "user" ? "human" : "assistant", content })
|
|
91
|
-
}
|
|
92
|
-
if (turns.length > 0) return turns
|
|
93
|
-
}
|
|
94
|
-
return turns
|
|
95
|
-
}
|
package/src/scanner/cursor.ts
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
import * as path from "path"
|
|
2
|
-
import * as fs from "fs"
|
|
3
|
-
import { Database as BunDatabase } from "bun:sqlite"
|
|
4
|
-
import type { Scanner, Session, ParsedSession, ConversationTurn } from "./types"
|
|
5
|
-
import { getHome, getPlatform } from "./platform"
|
|
6
|
-
import { listFiles, listDirs, safeReadFile, safeReadJson, safeStats, extractProjectDescription } from "./fs-utils"
|
|
7
|
-
|
|
8
|
-
const VSCDB_SEP = "|"
|
|
9
|
-
|
|
10
|
-
function makeVscdbPath(dbPath: string, composerId: string): string {
|
|
11
|
-
return `vscdb:${dbPath}${VSCDB_SEP}${composerId}`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function parseVscdbVirtualPath(virtualPath: string): { dbPath: string; composerId: string } | null {
|
|
15
|
-
const prefix = "vscdb:"
|
|
16
|
-
if (!virtualPath.startsWith(prefix)) return null
|
|
17
|
-
const rest = virtualPath.slice(prefix.length)
|
|
18
|
-
const sepIdx = rest.lastIndexOf(VSCDB_SEP)
|
|
19
|
-
if (sepIdx <= 0) return null
|
|
20
|
-
return { dbPath: rest.slice(0, sepIdx), composerId: rest.slice(sepIdx + 1) }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function withDb<T>(dbPath: string, fn: (db: BunDatabase) => T, fallback: T): T {
|
|
24
|
-
// Try immutable mode first (works when Cursor has the DB locked)
|
|
25
|
-
try {
|
|
26
|
-
const uri = "file:" + encodeURI(dbPath) + "?immutable=1"
|
|
27
|
-
const db = new BunDatabase(uri)
|
|
28
|
-
try { return fn(db) } finally { db.close() }
|
|
29
|
-
} catch { /* fall through */ }
|
|
30
|
-
// Fallback to readonly
|
|
31
|
-
try {
|
|
32
|
-
const db = new BunDatabase(dbPath, { readonly: true })
|
|
33
|
-
try { return fn(db) } finally { db.close() }
|
|
34
|
-
} catch {
|
|
35
|
-
return fallback
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function safeQueryDb<T>(db: BunDatabase, sql: string, params: unknown[] = []): T[] {
|
|
40
|
-
try {
|
|
41
|
-
return db.prepare(sql).all(...params) as T[]
|
|
42
|
-
} catch (err) {
|
|
43
|
-
console.error(`[codeblog] Cursor query error:`, err instanceof Error ? err.message : err)
|
|
44
|
-
return []
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getGlobalStoragePath(): string | null {
|
|
49
|
-
const home = getHome()
|
|
50
|
-
const platform = getPlatform()
|
|
51
|
-
let p: string
|
|
52
|
-
if (platform === "macos") {
|
|
53
|
-
p = path.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb")
|
|
54
|
-
} else if (platform === "windows") {
|
|
55
|
-
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming")
|
|
56
|
-
p = path.join(appData, "Cursor", "User", "globalStorage", "state.vscdb")
|
|
57
|
-
} else {
|
|
58
|
-
p = path.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb")
|
|
59
|
-
}
|
|
60
|
-
try { return fs.existsSync(p) ? p : null } catch { return null }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface CursorChatSession {
|
|
64
|
-
version?: number
|
|
65
|
-
requests: Array<{ message: string; response?: string | unknown[] }>
|
|
66
|
-
sessionId?: string
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface CursorComposerData {
|
|
70
|
-
composerId?: string
|
|
71
|
-
name?: string
|
|
72
|
-
createdAt?: number
|
|
73
|
-
lastUpdatedAt?: number
|
|
74
|
-
fullConversationHeadersOnly?: Array<{ bubbleId: string; type: number }>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const cursorScanner: Scanner = {
|
|
78
|
-
name: "Cursor",
|
|
79
|
-
sourceType: "cursor",
|
|
80
|
-
description: "Cursor AI IDE sessions (agent transcripts + chat sessions + composer)",
|
|
81
|
-
|
|
82
|
-
getSessionDirs(): string[] {
|
|
83
|
-
const home = getHome()
|
|
84
|
-
const platform = getPlatform()
|
|
85
|
-
const candidates: string[] = [path.join(home, ".cursor", "projects")]
|
|
86
|
-
|
|
87
|
-
if (platform === "macos") {
|
|
88
|
-
candidates.push(path.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"))
|
|
89
|
-
} else if (platform === "windows") {
|
|
90
|
-
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming")
|
|
91
|
-
candidates.push(path.join(appData, "Cursor", "User", "workspaceStorage"))
|
|
92
|
-
} else {
|
|
93
|
-
candidates.push(path.join(home, ".config", "Cursor", "User", "workspaceStorage"))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const globalDb = getGlobalStoragePath()
|
|
97
|
-
if (globalDb) candidates.push(path.dirname(globalDb))
|
|
98
|
-
|
|
99
|
-
return candidates.filter((d) => {
|
|
100
|
-
try { return fs.existsSync(d) } catch { return false }
|
|
101
|
-
})
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
scan(limit: number): Session[] {
|
|
105
|
-
const sessions: Session[] = []
|
|
106
|
-
const dirs = this.getSessionDirs()
|
|
107
|
-
const seenIds = new Set<string>()
|
|
108
|
-
|
|
109
|
-
for (const baseDir of dirs) {
|
|
110
|
-
if (baseDir.endsWith("globalStorage")) continue
|
|
111
|
-
const projectDirs = listDirs(baseDir)
|
|
112
|
-
for (const projectDir of projectDirs) {
|
|
113
|
-
const dirName = path.basename(projectDir)
|
|
114
|
-
let projectPath: string | undefined
|
|
115
|
-
const workspaceJson = safeReadJson<{ folder?: string }>(path.join(projectDir, "workspace.json"))
|
|
116
|
-
if (workspaceJson?.folder) {
|
|
117
|
-
try { projectPath = decodeURIComponent(new URL(workspaceJson.folder).pathname) } catch { /* */ }
|
|
118
|
-
}
|
|
119
|
-
const project = projectPath ? path.basename(projectPath) : dirName
|
|
120
|
-
const projectDescription = projectPath ? extractProjectDescription(projectPath) || undefined : undefined
|
|
121
|
-
|
|
122
|
-
// FORMAT 1: agent-transcripts
|
|
123
|
-
for (const filePath of listFiles(path.join(projectDir, "agent-transcripts"), [".txt"])) {
|
|
124
|
-
const stats = safeStats(filePath)
|
|
125
|
-
if (!stats) continue
|
|
126
|
-
const content = safeReadFile(filePath)
|
|
127
|
-
if (!content || content.length < 100) continue
|
|
128
|
-
const userQueries = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/g) || []
|
|
129
|
-
if (userQueries.length === 0) continue
|
|
130
|
-
const firstQuery = content.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/)
|
|
131
|
-
const preview = firstQuery ? firstQuery[1].trim().slice(0, 200) : content.slice(0, 200)
|
|
132
|
-
const id = path.basename(filePath, ".txt")
|
|
133
|
-
seenIds.add(id)
|
|
134
|
-
sessions.push({
|
|
135
|
-
id, source: "cursor", project, projectPath, projectDescription,
|
|
136
|
-
title: preview.slice(0, 80) || `Cursor session in ${project}`,
|
|
137
|
-
messageCount: userQueries.length * 2, humanMessages: userQueries.length, aiMessages: userQueries.length,
|
|
138
|
-
preview, filePath, modifiedAt: stats.mtime, sizeBytes: stats.size,
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// FORMAT 2: chatSessions
|
|
143
|
-
for (const filePath of listFiles(path.join(projectDir, "chatSessions"), [".json"])) {
|
|
144
|
-
const stats = safeStats(filePath)
|
|
145
|
-
if (!stats || stats.size < 100) continue
|
|
146
|
-
const data = safeReadJson<CursorChatSession>(filePath)
|
|
147
|
-
if (!data || !Array.isArray(data.requests) || data.requests.length === 0) continue
|
|
148
|
-
const humanCount = data.requests.length
|
|
149
|
-
const firstMsg = data.requests[0]?.message || ""
|
|
150
|
-
const preview = (typeof firstMsg === "string" ? firstMsg : "").slice(0, 200)
|
|
151
|
-
const id = data.sessionId || path.basename(filePath, ".json")
|
|
152
|
-
seenIds.add(id)
|
|
153
|
-
sessions.push({
|
|
154
|
-
id, source: "cursor", project, projectPath, projectDescription,
|
|
155
|
-
title: preview.slice(0, 80) || `Cursor chat in ${project}`,
|
|
156
|
-
messageCount: humanCount * 2, humanMessages: humanCount, aiMessages: humanCount,
|
|
157
|
-
preview: preview || "(cursor chat session)", filePath, modifiedAt: stats.mtime, sizeBytes: stats.size,
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// FORMAT 3: globalStorage state.vscdb
|
|
164
|
-
const globalDb = getGlobalStoragePath()
|
|
165
|
-
if (globalDb) {
|
|
166
|
-
withDb(globalDb, (db) => {
|
|
167
|
-
const rows = safeQueryDb<{ key: string; value: string }>(
|
|
168
|
-
db, "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'",
|
|
169
|
-
)
|
|
170
|
-
for (const row of rows) {
|
|
171
|
-
try {
|
|
172
|
-
const data = JSON.parse(row.value) as CursorComposerData
|
|
173
|
-
const composerId = data.composerId || row.key.replace("composerData:", "")
|
|
174
|
-
if (seenIds.has(composerId)) continue
|
|
175
|
-
const bubbleHeaders = data.fullConversationHeadersOnly || []
|
|
176
|
-
if (bubbleHeaders.length === 0) continue
|
|
177
|
-
const humanCount = bubbleHeaders.filter((b) => b.type === 1).length
|
|
178
|
-
const aiCount = bubbleHeaders.filter((b) => b.type === 2).length
|
|
179
|
-
let preview = data.name || ""
|
|
180
|
-
if (!preview) {
|
|
181
|
-
const firstUserBubble = bubbleHeaders.find((b) => b.type === 1)
|
|
182
|
-
if (firstUserBubble) {
|
|
183
|
-
const bubbleRow = safeQueryDb<{ value: string }>(
|
|
184
|
-
db, "SELECT value FROM cursorDiskKV WHERE key = ?",
|
|
185
|
-
[`bubbleId:${composerId}:${firstUserBubble.bubbleId}`],
|
|
186
|
-
)
|
|
187
|
-
if (bubbleRow.length > 0) {
|
|
188
|
-
try {
|
|
189
|
-
const bubble = JSON.parse(bubbleRow[0].value)
|
|
190
|
-
preview = (bubble.text || bubble.message || "").slice(0, 200)
|
|
191
|
-
} catch { /* */ }
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const createdAt = data.createdAt ? new Date(data.createdAt) : new Date()
|
|
196
|
-
const updatedAt = data.lastUpdatedAt ? new Date(data.lastUpdatedAt) : createdAt
|
|
197
|
-
sessions.push({
|
|
198
|
-
id: composerId, source: "cursor", project: "Cursor Composer",
|
|
199
|
-
title: (data.name || preview || "Cursor composer session").slice(0, 80),
|
|
200
|
-
messageCount: humanCount + aiCount, humanMessages: humanCount, aiMessages: aiCount,
|
|
201
|
-
preview: preview || "(composer session)",
|
|
202
|
-
filePath: makeVscdbPath(globalDb, composerId), modifiedAt: updatedAt, sizeBytes: row.value.length,
|
|
203
|
-
})
|
|
204
|
-
} catch { /* skip */ }
|
|
205
|
-
}
|
|
206
|
-
}, undefined)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
|
|
210
|
-
return sessions.slice(0, limit)
|
|
211
|
-
},
|
|
212
|
-
|
|
213
|
-
parse(filePath: string, maxTurns?: number): ParsedSession | null {
|
|
214
|
-
if (filePath.startsWith("vscdb:")) return parseVscdbSession(filePath, maxTurns)
|
|
215
|
-
const stats = safeStats(filePath)
|
|
216
|
-
const turns: ConversationTurn[] = []
|
|
217
|
-
|
|
218
|
-
if (filePath.endsWith(".txt")) {
|
|
219
|
-
const content = safeReadFile(filePath)
|
|
220
|
-
if (!content) return null
|
|
221
|
-
const blocks = content.split(/^user:\s*$/m)
|
|
222
|
-
for (const block of blocks) {
|
|
223
|
-
if (!block.trim()) continue
|
|
224
|
-
if (maxTurns && turns.length >= maxTurns) break
|
|
225
|
-
const queryMatch = block.match(/<user_query>\n?([\s\S]*?)\n?<\/user_query>/)
|
|
226
|
-
if (queryMatch) turns.push({ role: "human", content: queryMatch[1].trim() })
|
|
227
|
-
const afterQuery = block.split(/<\/user_query>/)[1]
|
|
228
|
-
if (afterQuery) {
|
|
229
|
-
const aiContent = afterQuery.replace(/^\s*\n\s*A:\s*\n?/, "").trim()
|
|
230
|
-
if (aiContent && (!maxTurns || turns.length < maxTurns)) turns.push({ role: "assistant", content: aiContent })
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
const data = safeReadJson<CursorChatSession>(filePath)
|
|
235
|
-
if (!data || !Array.isArray(data.requests)) return null
|
|
236
|
-
for (const req of data.requests) {
|
|
237
|
-
if (maxTurns && turns.length >= maxTurns) break
|
|
238
|
-
if (req.message) turns.push({ role: "human", content: typeof req.message === "string" ? req.message : JSON.stringify(req.message) })
|
|
239
|
-
if (maxTurns && turns.length >= maxTurns) break
|
|
240
|
-
if (req.response) {
|
|
241
|
-
let respText = ""
|
|
242
|
-
if (typeof req.response === "string") respText = req.response
|
|
243
|
-
else if (Array.isArray(req.response)) {
|
|
244
|
-
respText = req.response.map((r: unknown) => (typeof r === "string" ? r : (r as Record<string, unknown>)?.text || "")).join("")
|
|
245
|
-
}
|
|
246
|
-
if (respText.trim()) turns.push({ role: "assistant", content: respText.trim() })
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (turns.length === 0) return null
|
|
252
|
-
const humanMsgs = turns.filter((t) => t.role === "human")
|
|
253
|
-
const aiMsgs = turns.filter((t) => t.role === "assistant")
|
|
254
|
-
return {
|
|
255
|
-
id: path.basename(filePath).replace(/\.\w+$/, ""), source: "cursor",
|
|
256
|
-
project: path.basename(path.dirname(filePath)),
|
|
257
|
-
title: humanMsgs[0]?.content.slice(0, 80) || "Cursor session",
|
|
258
|
-
messageCount: turns.length, humanMessages: humanMsgs.length, aiMessages: aiMsgs.length,
|
|
259
|
-
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
260
|
-
filePath, modifiedAt: stats?.mtime || new Date(), sizeBytes: stats?.size || 0, turns,
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function parseVscdbSession(virtualPath: string, maxTurns?: number): ParsedSession | null {
|
|
266
|
-
const parsed = parseVscdbVirtualPath(virtualPath)
|
|
267
|
-
if (!parsed) return null
|
|
268
|
-
return withDb(parsed.dbPath, (db) => {
|
|
269
|
-
const metaRows = safeQueryDb<{ value: string }>(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [`composerData:${parsed.composerId}`])
|
|
270
|
-
if (metaRows.length === 0) return null
|
|
271
|
-
let composerData: CursorComposerData
|
|
272
|
-
try { composerData = JSON.parse(metaRows[0].value) } catch { return null }
|
|
273
|
-
const bubbleHeaders = composerData.fullConversationHeadersOnly || []
|
|
274
|
-
if (bubbleHeaders.length === 0) return null
|
|
275
|
-
const turns: ConversationTurn[] = []
|
|
276
|
-
for (const header of bubbleHeaders) {
|
|
277
|
-
if (maxTurns && turns.length >= maxTurns) break
|
|
278
|
-
const bubbleRows = safeQueryDb<{ value: string }>(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [`bubbleId:${parsed.composerId}:${header.bubbleId}`])
|
|
279
|
-
if (bubbleRows.length === 0) continue
|
|
280
|
-
try {
|
|
281
|
-
const bubble = JSON.parse(bubbleRows[0].value)
|
|
282
|
-
const text = bubble.text || bubble.message || bubble.rawText || ""
|
|
283
|
-
if (!text && header.type === 2) { turns.push({ role: "assistant", content: "(AI response)" }); continue }
|
|
284
|
-
turns.push({ role: header.type === 1 ? "human" : "assistant", content: text || "(empty)" })
|
|
285
|
-
} catch { /* skip */ }
|
|
286
|
-
}
|
|
287
|
-
if (turns.length === 0) return null
|
|
288
|
-
const humanMsgs = turns.filter((t) => t.role === "human")
|
|
289
|
-
const aiMsgs = turns.filter((t) => t.role === "assistant")
|
|
290
|
-
return {
|
|
291
|
-
id: parsed.composerId, source: "cursor" as const, project: "Cursor Composer",
|
|
292
|
-
title: composerData.name || humanMsgs[0]?.content.slice(0, 80) || "Cursor session",
|
|
293
|
-
messageCount: turns.length, humanMessages: humanMsgs.length, aiMessages: aiMsgs.length,
|
|
294
|
-
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
295
|
-
filePath: virtualPath, modifiedAt: composerData.lastUpdatedAt ? new Date(composerData.lastUpdatedAt) : new Date(),
|
|
296
|
-
sizeBytes: 0, turns,
|
|
297
|
-
} as ParsedSession
|
|
298
|
-
}, null)
|
|
299
|
-
}
|
package/src/scanner/fs-utils.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs"
|
|
2
|
-
import * as path from "path"
|
|
3
|
-
|
|
4
|
-
export function safeReadFile(filePath: string): string | null {
|
|
5
|
-
try {
|
|
6
|
-
return fs.readFileSync(filePath, "utf-8")
|
|
7
|
-
} catch {
|
|
8
|
-
return null
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function safeReadJson<T = unknown>(filePath: string): T | null {
|
|
13
|
-
const content = safeReadFile(filePath)
|
|
14
|
-
if (!content) return null
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(content) as T
|
|
17
|
-
} catch {
|
|
18
|
-
return null
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function safeStats(filePath: string): fs.Stats | null {
|
|
23
|
-
try {
|
|
24
|
-
return fs.statSync(filePath)
|
|
25
|
-
} catch {
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function listFiles(dir: string, extensions?: string[], recursive = false): string[] {
|
|
31
|
-
if (!fs.existsSync(dir)) return []
|
|
32
|
-
const results: string[] = []
|
|
33
|
-
try {
|
|
34
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
35
|
-
for (const entry of entries) {
|
|
36
|
-
const fullPath = path.join(dir, entry.name)
|
|
37
|
-
if (entry.isFile()) {
|
|
38
|
-
if (!extensions || extensions.some((ext) => entry.name.endsWith(ext))) results.push(fullPath)
|
|
39
|
-
} else if (entry.isDirectory() && recursive) {
|
|
40
|
-
results.push(...listFiles(fullPath, extensions, true))
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
// Permission denied or other errors
|
|
45
|
-
}
|
|
46
|
-
return results
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function listDirs(dir: string): string[] {
|
|
50
|
-
if (!fs.existsSync(dir)) return []
|
|
51
|
-
try {
|
|
52
|
-
return fs
|
|
53
|
-
.readdirSync(dir, { withFileTypes: true })
|
|
54
|
-
.filter((e) => e.isDirectory())
|
|
55
|
-
.map((e) => path.join(dir, e.name))
|
|
56
|
-
} catch {
|
|
57
|
-
return []
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function exists(p: string): boolean {
|
|
62
|
-
try {
|
|
63
|
-
return fs.existsSync(p)
|
|
64
|
-
} catch {
|
|
65
|
-
return false
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function extractProjectDescription(projectPath: string): string | null {
|
|
70
|
-
if (!projectPath || !fs.existsSync(projectPath)) return null
|
|
71
|
-
|
|
72
|
-
const pkgPath = path.join(projectPath, "package.json")
|
|
73
|
-
const pkg = safeReadJson<{ name?: string; description?: string }>(pkgPath)
|
|
74
|
-
if (pkg?.description) return pkg.description.slice(0, 200)
|
|
75
|
-
|
|
76
|
-
for (const readmeName of ["README.md", "readme.md", "Readme.md", "README.rst"]) {
|
|
77
|
-
const readmePath = path.join(projectPath, readmeName)
|
|
78
|
-
const content = safeReadFile(readmePath)
|
|
79
|
-
if (!content) continue
|
|
80
|
-
const lines = content.split("\n")
|
|
81
|
-
let desc = ""
|
|
82
|
-
for (const line of lines) {
|
|
83
|
-
const trimmed = line.trim()
|
|
84
|
-
if (!trimmed) {
|
|
85
|
-
if (desc) break
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
if (trimmed.startsWith("#") || trimmed.startsWith("=") || trimmed.startsWith("-")) {
|
|
89
|
-
if (desc) break
|
|
90
|
-
continue
|
|
91
|
-
}
|
|
92
|
-
if (trimmed.startsWith("![") || trimmed.startsWith("<img")) continue
|
|
93
|
-
desc += (desc ? " " : "") + trimmed
|
|
94
|
-
if (desc.length > 200) break
|
|
95
|
-
}
|
|
96
|
-
if (desc.length > 10) return desc.slice(0, 300)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const cargoPath = path.join(projectPath, "Cargo.toml")
|
|
100
|
-
const cargo = safeReadFile(cargoPath)
|
|
101
|
-
if (cargo) {
|
|
102
|
-
const match = cargo.match(/description\s*=\s*"([^"]+)"/)
|
|
103
|
-
if (match) return match[1].slice(0, 200)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function readJsonl<T = unknown>(filePath: string): T[] {
|
|
110
|
-
const content = safeReadFile(filePath)
|
|
111
|
-
if (!content) return []
|
|
112
|
-
return content
|
|
113
|
-
.split("\n")
|
|
114
|
-
.filter(Boolean)
|
|
115
|
-
.map((line) => {
|
|
116
|
-
try {
|
|
117
|
-
return JSON.parse(line) as T
|
|
118
|
-
} catch {
|
|
119
|
-
return null
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
.filter((x): x is T => x !== null)
|
|
123
|
-
}
|
package/src/scanner/index.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { registerScanner } from "./registry"
|
|
2
|
-
import { claudeCodeScanner } from "./claude-code"
|
|
3
|
-
import { cursorScanner } from "./cursor"
|
|
4
|
-
import { windsurfScanner } from "./windsurf"
|
|
5
|
-
import { codexScanner } from "./codex"
|
|
6
|
-
import { warpScanner } from "./warp"
|
|
7
|
-
import { vscodeCopilotScanner } from "./vscode-copilot"
|
|
8
|
-
import { aiderScanner } from "./aider"
|
|
9
|
-
import { continueDevScanner } from "./continue-dev"
|
|
10
|
-
import { zedScanner } from "./zed"
|
|
11
|
-
|
|
12
|
-
export function registerAllScanners(): void {
|
|
13
|
-
registerScanner(claudeCodeScanner)
|
|
14
|
-
registerScanner(cursorScanner)
|
|
15
|
-
registerScanner(windsurfScanner)
|
|
16
|
-
registerScanner(codexScanner)
|
|
17
|
-
registerScanner(warpScanner)
|
|
18
|
-
registerScanner(vscodeCopilotScanner)
|
|
19
|
-
registerScanner(aiderScanner)
|
|
20
|
-
registerScanner(continueDevScanner)
|
|
21
|
-
registerScanner(zedScanner)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export { scanAll, parseSession, listScannerStatus, getScanners } from "./registry"
|
|
25
|
-
export { analyzeSession } from "./analyzer"
|
|
26
|
-
export type { Session, ParsedSession, SessionAnalysis, Scanner, SourceType, ConversationTurn } from "./types"
|
package/src/scanner/platform.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs"
|
|
2
|
-
import * as os from "os"
|
|
3
|
-
import * as path from "path"
|
|
4
|
-
|
|
5
|
-
export type Platform = "macos" | "windows" | "linux"
|
|
6
|
-
|
|
7
|
-
export function getPlatform(): Platform {
|
|
8
|
-
switch (os.platform()) {
|
|
9
|
-
case "win32":
|
|
10
|
-
return "windows"
|
|
11
|
-
case "darwin":
|
|
12
|
-
return "macos"
|
|
13
|
-
default:
|
|
14
|
-
return "linux"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getHome(): string {
|
|
19
|
-
return os.homedir()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getAppDataDir(): string {
|
|
23
|
-
const platform = getPlatform()
|
|
24
|
-
if (platform === "windows") return process.env.APPDATA || path.join(getHome(), "AppData", "Roaming")
|
|
25
|
-
if (platform === "macos") return path.join(getHome(), "Library", "Application Support")
|
|
26
|
-
return process.env.XDG_CONFIG_HOME || path.join(getHome(), ".config")
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getLocalAppDataDir(): string {
|
|
30
|
-
const platform = getPlatform()
|
|
31
|
-
if (platform === "windows") return process.env.LOCALAPPDATA || path.join(getHome(), "AppData", "Local")
|
|
32
|
-
if (platform === "macos") return path.join(getHome(), "Library", "Application Support")
|
|
33
|
-
return process.env.XDG_DATA_HOME || path.join(getHome(), ".local", "share")
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function resolvePaths(candidates: string[]): string[] {
|
|
37
|
-
return candidates.filter((p) => {
|
|
38
|
-
try {
|
|
39
|
-
return fs.existsSync(p)
|
|
40
|
-
} catch {
|
|
41
|
-
return false
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
}
|
package/src/scanner/registry.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Scanner, Session, ParsedSession } from "./types"
|
|
2
|
-
|
|
3
|
-
const scanners: Scanner[] = []
|
|
4
|
-
|
|
5
|
-
export function registerScanner(scanner: Scanner): void {
|
|
6
|
-
scanners.push(scanner)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getScanners(): Scanner[] {
|
|
10
|
-
return [...scanners]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getScannerBySource(source: string): Scanner | undefined {
|
|
14
|
-
return scanners.find((s) => s.sourceType === source)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function safeScannerCall<T>(scannerName: string, method: string, fn: () => T, fallback: T): T {
|
|
18
|
-
try {
|
|
19
|
-
return fn()
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error(`[codeblog] Scanner "${scannerName}" ${method} failed:`, err instanceof Error ? err.message : err)
|
|
22
|
-
return fallback
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function scanAll(limit = 20, source?: string): Session[] {
|
|
27
|
-
const all: Session[] = []
|
|
28
|
-
const targets = source ? scanners.filter((s) => s.sourceType === source) : scanners
|
|
29
|
-
|
|
30
|
-
for (const scanner of targets) {
|
|
31
|
-
const sessions = safeScannerCall(scanner.name, "scan", () => scanner.scan(limit), [])
|
|
32
|
-
all.push(...sessions)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
all.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
|
|
36
|
-
return all.slice(0, limit)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseSession(filePath: string, source: string, maxTurns?: number): ParsedSession | null {
|
|
40
|
-
const scanner = getScannerBySource(source)
|
|
41
|
-
if (!scanner) return null
|
|
42
|
-
return safeScannerCall(scanner.name, "parse", () => scanner.parse(filePath, maxTurns), null)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function listScannerStatus(): Array<{
|
|
46
|
-
name: string
|
|
47
|
-
source: string
|
|
48
|
-
description: string
|
|
49
|
-
available: boolean
|
|
50
|
-
dirs: string[]
|
|
51
|
-
error?: string
|
|
52
|
-
}> {
|
|
53
|
-
return scanners.map((s) => {
|
|
54
|
-
try {
|
|
55
|
-
const dirs = s.getSessionDirs()
|
|
56
|
-
return { name: s.name, source: s.sourceType, description: s.description, available: dirs.length > 0, dirs }
|
|
57
|
-
} catch (err) {
|
|
58
|
-
return {
|
|
59
|
-
name: s.name,
|
|
60
|
-
source: s.sourceType,
|
|
61
|
-
description: s.description,
|
|
62
|
-
available: false,
|
|
63
|
-
dirs: [],
|
|
64
|
-
error: err instanceof Error ? err.message : String(err),
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
}
|