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.
Files changed (76) hide show
  1. package/package.json +4 -18
  2. package/src/ai/__tests__/chat.test.ts +110 -0
  3. package/src/ai/__tests__/provider.test.ts +184 -0
  4. package/src/ai/__tests__/tools.test.ts +90 -0
  5. package/src/ai/chat.ts +81 -50
  6. package/src/ai/provider.ts +24 -250
  7. package/src/ai/tools.ts +46 -281
  8. package/src/auth/oauth.ts +7 -0
  9. package/src/cli/__tests__/commands.test.ts +225 -0
  10. package/src/cli/__tests__/setup.test.ts +57 -0
  11. package/src/cli/cmd/agent.ts +102 -0
  12. package/src/cli/cmd/chat.ts +1 -1
  13. package/src/cli/cmd/comment.ts +47 -16
  14. package/src/cli/cmd/feed.ts +18 -30
  15. package/src/cli/cmd/forum.ts +123 -0
  16. package/src/cli/cmd/login.ts +9 -2
  17. package/src/cli/cmd/me.ts +202 -0
  18. package/src/cli/cmd/post.ts +6 -88
  19. package/src/cli/cmd/publish.ts +44 -23
  20. package/src/cli/cmd/scan.ts +45 -34
  21. package/src/cli/cmd/search.ts +8 -70
  22. package/src/cli/cmd/setup.ts +160 -62
  23. package/src/cli/cmd/vote.ts +29 -14
  24. package/src/cli/cmd/whoami.ts +7 -36
  25. package/src/cli/ui.ts +50 -0
  26. package/src/index.ts +80 -59
  27. package/src/mcp/__tests__/client.test.ts +149 -0
  28. package/src/mcp/__tests__/e2e.ts +327 -0
  29. package/src/mcp/__tests__/integration.ts +148 -0
  30. package/src/mcp/client.ts +148 -0
  31. package/src/api/agents.ts +0 -103
  32. package/src/api/bookmarks.ts +0 -25
  33. package/src/api/client.ts +0 -96
  34. package/src/api/debates.ts +0 -35
  35. package/src/api/feed.ts +0 -25
  36. package/src/api/notifications.ts +0 -31
  37. package/src/api/posts.ts +0 -116
  38. package/src/api/search.ts +0 -29
  39. package/src/api/tags.ts +0 -13
  40. package/src/api/trending.ts +0 -38
  41. package/src/api/users.ts +0 -8
  42. package/src/cli/cmd/agents.ts +0 -77
  43. package/src/cli/cmd/ai-publish.ts +0 -118
  44. package/src/cli/cmd/bookmark.ts +0 -27
  45. package/src/cli/cmd/bookmarks.ts +0 -42
  46. package/src/cli/cmd/dashboard.ts +0 -59
  47. package/src/cli/cmd/debate.ts +0 -89
  48. package/src/cli/cmd/delete.ts +0 -35
  49. package/src/cli/cmd/edit.ts +0 -42
  50. package/src/cli/cmd/explore.ts +0 -63
  51. package/src/cli/cmd/follow.ts +0 -34
  52. package/src/cli/cmd/myposts.ts +0 -50
  53. package/src/cli/cmd/notifications.ts +0 -65
  54. package/src/cli/cmd/tags.ts +0 -58
  55. package/src/cli/cmd/trending.ts +0 -64
  56. package/src/cli/cmd/weekly-digest.ts +0 -117
  57. package/src/publisher/index.ts +0 -139
  58. package/src/scanner/__tests__/analyzer.test.ts +0 -67
  59. package/src/scanner/__tests__/fs-utils.test.ts +0 -50
  60. package/src/scanner/__tests__/platform.test.ts +0 -27
  61. package/src/scanner/__tests__/registry.test.ts +0 -56
  62. package/src/scanner/aider.ts +0 -96
  63. package/src/scanner/analyzer.ts +0 -237
  64. package/src/scanner/claude-code.ts +0 -188
  65. package/src/scanner/codex.ts +0 -127
  66. package/src/scanner/continue-dev.ts +0 -95
  67. package/src/scanner/cursor.ts +0 -299
  68. package/src/scanner/fs-utils.ts +0 -123
  69. package/src/scanner/index.ts +0 -26
  70. package/src/scanner/platform.ts +0 -44
  71. package/src/scanner/registry.ts +0 -68
  72. package/src/scanner/types.ts +0 -62
  73. package/src/scanner/vscode-copilot.ts +0 -125
  74. package/src/scanner/warp.ts +0 -19
  75. package/src/scanner/windsurf.ts +0 -147
  76. 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
- }
@@ -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
- }
@@ -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
- }
@@ -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"
@@ -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
- }
@@ -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
- }