codeblog-app 1.6.5 → 2.0.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/package.json +9 -23
- 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 +14 -14
- 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
package/src/scanner/types.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export interface Session {
|
|
2
|
-
id: string
|
|
3
|
-
source: SourceType
|
|
4
|
-
project: string
|
|
5
|
-
projectPath?: string
|
|
6
|
-
projectDescription?: string
|
|
7
|
-
title: string
|
|
8
|
-
messageCount: number
|
|
9
|
-
humanMessages: number
|
|
10
|
-
aiMessages: number
|
|
11
|
-
preview: string
|
|
12
|
-
filePath: string
|
|
13
|
-
modifiedAt: Date
|
|
14
|
-
sizeBytes: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ConversationTurn {
|
|
18
|
-
role: "human" | "assistant" | "system" | "tool"
|
|
19
|
-
content: string
|
|
20
|
-
timestamp?: Date
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ParsedSession extends Session {
|
|
24
|
-
turns: ConversationTurn[]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SessionAnalysis {
|
|
28
|
-
summary: string
|
|
29
|
-
topics: string[]
|
|
30
|
-
languages: string[]
|
|
31
|
-
keyInsights: string[]
|
|
32
|
-
codeSnippets: Array<{
|
|
33
|
-
language: string
|
|
34
|
-
code: string
|
|
35
|
-
context: string
|
|
36
|
-
}>
|
|
37
|
-
problems: string[]
|
|
38
|
-
solutions: string[]
|
|
39
|
-
suggestedTitle: string
|
|
40
|
-
suggestedTags: string[]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type SourceType =
|
|
44
|
-
| "claude-code"
|
|
45
|
-
| "cursor"
|
|
46
|
-
| "windsurf"
|
|
47
|
-
| "codex"
|
|
48
|
-
| "warp"
|
|
49
|
-
| "vscode-copilot"
|
|
50
|
-
| "aider"
|
|
51
|
-
| "continue"
|
|
52
|
-
| "zed"
|
|
53
|
-
| "unknown"
|
|
54
|
-
|
|
55
|
-
export interface Scanner {
|
|
56
|
-
name: string
|
|
57
|
-
sourceType: SourceType
|
|
58
|
-
description: string
|
|
59
|
-
getSessionDirs(): string[]
|
|
60
|
-
scan(limit: number): Session[]
|
|
61
|
-
parse(filePath: string, maxTurns?: number): ParsedSession | null
|
|
62
|
-
}
|
|
@@ -1,125 +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, listDirs, safeReadJson, safeStats } from "./fs-utils"
|
|
6
|
-
|
|
7
|
-
export const vscodeCopilotScanner: Scanner = {
|
|
8
|
-
name: "VS Code Copilot Chat",
|
|
9
|
-
sourceType: "vscode-copilot",
|
|
10
|
-
description: "GitHub Copilot Chat sessions in VS Code",
|
|
11
|
-
|
|
12
|
-
getSessionDirs(): string[] {
|
|
13
|
-
const home = getHome()
|
|
14
|
-
const platform = getPlatform()
|
|
15
|
-
const candidates: string[] = []
|
|
16
|
-
const variants = ["Code", "Code - Insiders", "VSCodium"]
|
|
17
|
-
for (const variant of variants) {
|
|
18
|
-
if (platform === "macos") {
|
|
19
|
-
candidates.push(path.join(home, "Library", "Application Support", variant, "User", "workspaceStorage"))
|
|
20
|
-
candidates.push(path.join(home, "Library", "Application Support", variant, "User", "globalStorage", "github.copilot-chat"))
|
|
21
|
-
} else if (platform === "windows") {
|
|
22
|
-
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming")
|
|
23
|
-
candidates.push(path.join(appData, variant, "User", "workspaceStorage"))
|
|
24
|
-
candidates.push(path.join(appData, variant, "User", "globalStorage", "github.copilot-chat"))
|
|
25
|
-
} else {
|
|
26
|
-
candidates.push(path.join(home, ".config", variant, "User", "workspaceStorage"))
|
|
27
|
-
candidates.push(path.join(home, ".config", variant, "User", "globalStorage", "github.copilot-chat"))
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return candidates.filter((d) => {
|
|
31
|
-
try { return fs.existsSync(d) } catch { return false }
|
|
32
|
-
})
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
scan(limit: number): Session[] {
|
|
36
|
-
const sessions: Session[] = []
|
|
37
|
-
for (const baseDir of this.getSessionDirs()) {
|
|
38
|
-
if (baseDir.includes("globalStorage")) {
|
|
39
|
-
for (const filePath of listFiles(baseDir, [".json"])) {
|
|
40
|
-
const session = tryParseConversationFile(filePath)
|
|
41
|
-
if (session) sessions.push(session)
|
|
42
|
-
}
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
for (const hashDir of listDirs(baseDir)) {
|
|
46
|
-
for (const filePath of listFiles(path.join(hashDir, "github.copilot-chat"), [".json"])) {
|
|
47
|
-
const session = tryParseConversationFile(filePath)
|
|
48
|
-
if (session) sessions.push(session)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
|
|
53
|
-
return sessions.slice(0, limit)
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
parse(filePath: string, maxTurns?: number): ParsedSession | null {
|
|
57
|
-
const data = safeReadJson<Record<string, unknown>>(filePath)
|
|
58
|
-
if (!data) return null
|
|
59
|
-
const stats = safeStats(filePath)
|
|
60
|
-
const turns = extractCopilotTurns(data, maxTurns)
|
|
61
|
-
if (turns.length === 0) return null
|
|
62
|
-
const humanMsgs = turns.filter((t) => t.role === "human")
|
|
63
|
-
const aiMsgs = turns.filter((t) => t.role === "assistant")
|
|
64
|
-
return {
|
|
65
|
-
id: path.basename(filePath, ".json"), source: "vscode-copilot",
|
|
66
|
-
project: path.basename(path.dirname(filePath)),
|
|
67
|
-
title: humanMsgs[0]?.content.slice(0, 80) || "Copilot Chat session",
|
|
68
|
-
messageCount: turns.length, humanMessages: humanMsgs.length, aiMessages: aiMsgs.length,
|
|
69
|
-
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
70
|
-
filePath, modifiedAt: stats?.mtime || new Date(), sizeBytes: stats?.size || 0, turns,
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function tryParseConversationFile(filePath: string): Session | null {
|
|
76
|
-
const stats = safeStats(filePath)
|
|
77
|
-
if (!stats || stats.size < 100) return null
|
|
78
|
-
const data = safeReadJson<Record<string, unknown>>(filePath)
|
|
79
|
-
if (!data) return null
|
|
80
|
-
const turns = extractCopilotTurns(data)
|
|
81
|
-
if (turns.length < 2) return null
|
|
82
|
-
const humanMsgs = turns.filter((t) => t.role === "human")
|
|
83
|
-
const preview = humanMsgs[0]?.content.slice(0, 200) || "(copilot session)"
|
|
84
|
-
return {
|
|
85
|
-
id: path.basename(filePath, ".json"), source: "vscode-copilot",
|
|
86
|
-
project: path.basename(path.dirname(filePath)),
|
|
87
|
-
title: preview.slice(0, 80), messageCount: turns.length,
|
|
88
|
-
humanMessages: humanMsgs.length, aiMessages: turns.length - humanMsgs.length,
|
|
89
|
-
preview, filePath, modifiedAt: stats.mtime, sizeBytes: stats.size,
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function extractCopilotTurns(data: Record<string, unknown>, maxTurns?: number): ConversationTurn[] {
|
|
94
|
-
const turns: ConversationTurn[] = []
|
|
95
|
-
if (Array.isArray(data.conversations)) {
|
|
96
|
-
for (const conv of data.conversations) {
|
|
97
|
-
if (!conv || typeof conv !== "object") continue
|
|
98
|
-
const c = conv as Record<string, unknown>
|
|
99
|
-
if (!Array.isArray(c.turns)) continue
|
|
100
|
-
for (const turn of c.turns) {
|
|
101
|
-
if (maxTurns && turns.length >= maxTurns) break
|
|
102
|
-
const t = turn as Record<string, unknown>
|
|
103
|
-
if (t.request && typeof t.request === "string") turns.push({ role: "human", content: t.request })
|
|
104
|
-
if (t.response && typeof t.response === "string") turns.push({ role: "assistant", content: t.response })
|
|
105
|
-
if (t.message && typeof t.message === "string") {
|
|
106
|
-
turns.push({ role: t.role === "user" ? "human" : "assistant", content: t.message })
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return turns
|
|
111
|
-
}
|
|
112
|
-
for (const arr of [data.messages, data.history, data.entries]) {
|
|
113
|
-
if (!Array.isArray(arr)) continue
|
|
114
|
-
for (const msg of arr) {
|
|
115
|
-
if (maxTurns && turns.length >= maxTurns) break
|
|
116
|
-
if (!msg || typeof msg !== "object") continue
|
|
117
|
-
const m = msg as Record<string, unknown>
|
|
118
|
-
const content = (m.content || m.text || m.message) as string | undefined
|
|
119
|
-
if (typeof content !== "string") continue
|
|
120
|
-
turns.push({ role: m.role === "user" ? "human" : "assistant", content })
|
|
121
|
-
}
|
|
122
|
-
if (turns.length > 0) return turns
|
|
123
|
-
}
|
|
124
|
-
return turns
|
|
125
|
-
}
|
package/src/scanner/warp.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Scanner, Session, ParsedSession } from "./types"
|
|
2
|
-
|
|
3
|
-
export const warpScanner: Scanner = {
|
|
4
|
-
name: "Warp Terminal",
|
|
5
|
-
sourceType: "warp",
|
|
6
|
-
description: "Warp Terminal (AI chat is cloud-only, no local history)",
|
|
7
|
-
|
|
8
|
-
getSessionDirs(): string[] {
|
|
9
|
-
return []
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
scan(_limit: number): Session[] {
|
|
13
|
-
return []
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
parse(_filePath: string, _maxTurns?: number): ParsedSession | null {
|
|
17
|
-
return null
|
|
18
|
-
},
|
|
19
|
-
}
|
package/src/scanner/windsurf.ts
DELETED
|
@@ -1,147 +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 { listDirs, safeReadJson, safeStats, extractProjectDescription } from "./fs-utils"
|
|
7
|
-
|
|
8
|
-
interface VscdbChatIndex {
|
|
9
|
-
version: number
|
|
10
|
-
entries: Record<string, VscdbChatEntry>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface VscdbChatEntry {
|
|
14
|
-
messages?: Array<{ role?: string; content?: string; text?: string }>
|
|
15
|
-
[key: string]: unknown
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const windsurfScanner: Scanner = {
|
|
19
|
-
name: "Windsurf",
|
|
20
|
-
sourceType: "windsurf",
|
|
21
|
-
description: "Windsurf (Codeium) Cascade chat sessions (SQLite)",
|
|
22
|
-
|
|
23
|
-
getSessionDirs(): string[] {
|
|
24
|
-
const home = getHome()
|
|
25
|
-
const platform = getPlatform()
|
|
26
|
-
const candidates: string[] = []
|
|
27
|
-
if (platform === "macos") {
|
|
28
|
-
candidates.push(path.join(home, "Library", "Application Support", "Windsurf", "User", "workspaceStorage"))
|
|
29
|
-
} else if (platform === "windows") {
|
|
30
|
-
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming")
|
|
31
|
-
candidates.push(path.join(appData, "Windsurf", "User", "workspaceStorage"))
|
|
32
|
-
} else {
|
|
33
|
-
candidates.push(path.join(home, ".config", "Windsurf", "User", "workspaceStorage"))
|
|
34
|
-
}
|
|
35
|
-
return candidates.filter((d) => {
|
|
36
|
-
try { return fs.existsSync(d) } catch { return false }
|
|
37
|
-
})
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
scan(limit: number): Session[] {
|
|
41
|
-
const sessions: Session[] = []
|
|
42
|
-
for (const baseDir of this.getSessionDirs()) {
|
|
43
|
-
for (const wsDir of listDirs(baseDir)) {
|
|
44
|
-
const dbPath = path.join(wsDir, "state.vscdb")
|
|
45
|
-
if (!fs.existsSync(dbPath)) continue
|
|
46
|
-
const wsJson = safeReadJson<{ folder?: string }>(path.join(wsDir, "workspace.json"))
|
|
47
|
-
let projectPath: string | undefined
|
|
48
|
-
if (wsJson?.folder) {
|
|
49
|
-
try { projectPath = decodeURIComponent(new URL(wsJson.folder).pathname) } catch { /* */ }
|
|
50
|
-
}
|
|
51
|
-
const project = projectPath ? path.basename(projectPath) : path.basename(wsDir)
|
|
52
|
-
const projectDescription = projectPath ? extractProjectDescription(projectPath) || undefined : undefined
|
|
53
|
-
const chatData = readVscdbChatSessions(dbPath)
|
|
54
|
-
if (!chatData || Object.keys(chatData.entries).length === 0) continue
|
|
55
|
-
for (const [sessionId, entry] of Object.entries(chatData.entries)) {
|
|
56
|
-
const messages = extractVscdbMessages(entry)
|
|
57
|
-
if (messages.length < 2) continue
|
|
58
|
-
const humanMsgs = messages.filter((m) => m.role === "human")
|
|
59
|
-
const preview = humanMsgs[0]?.content.slice(0, 200) || "(windsurf session)"
|
|
60
|
-
const dbStats = safeStats(dbPath)
|
|
61
|
-
sessions.push({
|
|
62
|
-
id: sessionId, source: "windsurf", project, projectPath, projectDescription,
|
|
63
|
-
title: preview.slice(0, 80), messageCount: messages.length,
|
|
64
|
-
humanMessages: humanMsgs.length, aiMessages: messages.length - humanMsgs.length,
|
|
65
|
-
preview, filePath: `${dbPath}|${sessionId}`,
|
|
66
|
-
modifiedAt: dbStats?.mtime || new Date(), sizeBytes: dbStats?.size || 0,
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime())
|
|
72
|
-
return sessions.slice(0, limit)
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
parse(filePath: string, maxTurns?: number): ParsedSession | null {
|
|
76
|
-
const sepIdx = filePath.lastIndexOf("|")
|
|
77
|
-
const dbPath = sepIdx > 0 ? filePath.slice(0, sepIdx) : filePath
|
|
78
|
-
const targetSessionId = sepIdx > 0 ? filePath.slice(sepIdx + 1) : null
|
|
79
|
-
const chatData = readVscdbChatSessions(dbPath)
|
|
80
|
-
if (!chatData) return null
|
|
81
|
-
const stats = safeStats(dbPath)
|
|
82
|
-
const entries = Object.entries(chatData.entries)
|
|
83
|
-
if (entries.length === 0) return null
|
|
84
|
-
let targetEntry: VscdbChatEntry | null = null
|
|
85
|
-
let targetId = path.basename(path.dirname(dbPath))
|
|
86
|
-
if (targetSessionId && chatData.entries[targetSessionId]) {
|
|
87
|
-
targetEntry = chatData.entries[targetSessionId]
|
|
88
|
-
targetId = targetSessionId
|
|
89
|
-
} else {
|
|
90
|
-
for (const [id, entry] of entries) {
|
|
91
|
-
const msgs = extractVscdbMessages(entry)
|
|
92
|
-
if (msgs.length >= 2) { targetEntry = entry; targetId = id; break }
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (!targetEntry) return null
|
|
96
|
-
const allTurns = extractVscdbMessages(targetEntry)
|
|
97
|
-
const turns = maxTurns ? allTurns.slice(0, maxTurns) : allTurns
|
|
98
|
-
if (turns.length === 0) return null
|
|
99
|
-
const humanMsgs = turns.filter((t) => t.role === "human")
|
|
100
|
-
const aiMsgs = turns.filter((t) => t.role === "assistant")
|
|
101
|
-
return {
|
|
102
|
-
id: targetId, source: "windsurf", project: path.basename(path.dirname(filePath)),
|
|
103
|
-
title: humanMsgs[0]?.content.slice(0, 80) || "Windsurf session",
|
|
104
|
-
messageCount: turns.length, humanMessages: humanMsgs.length, aiMessages: aiMsgs.length,
|
|
105
|
-
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
106
|
-
filePath: dbPath, modifiedAt: stats?.mtime || new Date(), sizeBytes: stats?.size || 0, turns,
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function readVscdbChatSessions(dbPath: string): VscdbChatIndex | null {
|
|
112
|
-
try {
|
|
113
|
-
const db = new BunDatabase(dbPath, { readonly: true })
|
|
114
|
-
let row: { value: string | Buffer } | undefined
|
|
115
|
-
try {
|
|
116
|
-
row = db.prepare("SELECT value FROM ItemTable WHERE key = 'chat.ChatSessionStore.index'").get() as any
|
|
117
|
-
} finally { db.close() }
|
|
118
|
-
if (!row?.value) return null
|
|
119
|
-
const valueStr = typeof row.value === "string" ? row.value : (row.value as Buffer).toString("utf-8")
|
|
120
|
-
return JSON.parse(valueStr) as VscdbChatIndex
|
|
121
|
-
} catch { return null }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function extractVscdbMessages(entry: VscdbChatEntry): ConversationTurn[] {
|
|
125
|
-
const turns: ConversationTurn[] = []
|
|
126
|
-
if (Array.isArray(entry.messages)) {
|
|
127
|
-
for (const msg of entry.messages) {
|
|
128
|
-
if (!msg || typeof msg !== "object") continue
|
|
129
|
-
const content = msg.content || msg.text
|
|
130
|
-
if (typeof content !== "string" || !content.trim()) continue
|
|
131
|
-
turns.push({ role: msg.role === "user" || msg.role === "human" ? "human" : "assistant", content })
|
|
132
|
-
}
|
|
133
|
-
return turns
|
|
134
|
-
}
|
|
135
|
-
for (const [, value] of Object.entries(entry)) {
|
|
136
|
-
if (!Array.isArray(value)) continue
|
|
137
|
-
for (const item of value) {
|
|
138
|
-
if (!item || typeof item !== "object") continue
|
|
139
|
-
const m = item as Record<string, unknown>
|
|
140
|
-
const content = (m.content || m.text) as string | undefined
|
|
141
|
-
if (typeof content !== "string" || !content.trim()) continue
|
|
142
|
-
turns.push({ role: m.role === "user" || m.role === "human" ? "human" : "assistant", content })
|
|
143
|
-
}
|
|
144
|
-
if (turns.length > 0) return turns
|
|
145
|
-
}
|
|
146
|
-
return turns
|
|
147
|
-
}
|
package/src/scanner/zed.ts
DELETED
|
@@ -1,88 +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 zedScanner: Scanner = {
|
|
8
|
-
name: "Zed",
|
|
9
|
-
sourceType: "zed",
|
|
10
|
-
description: "Zed editor AI assistant conversations",
|
|
11
|
-
|
|
12
|
-
getSessionDirs(): string[] {
|
|
13
|
-
const home = getHome()
|
|
14
|
-
const platform = getPlatform()
|
|
15
|
-
const candidates: string[] = []
|
|
16
|
-
if (platform === "macos") {
|
|
17
|
-
candidates.push(path.join(home, "Library", "Application Support", "Zed", "conversations"))
|
|
18
|
-
}
|
|
19
|
-
if (platform === "windows") {
|
|
20
|
-
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming")
|
|
21
|
-
candidates.push(path.join(appData, "Zed", "conversations"))
|
|
22
|
-
}
|
|
23
|
-
candidates.push(path.join(home, ".config", "zed", "conversations"))
|
|
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", ".zed"], true)) {
|
|
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 = extractZedTurns(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) || "(zed session)"
|
|
41
|
-
sessions.push({
|
|
42
|
-
id: path.basename(filePath).replace(/\.\w+$/, ""), source: "zed",
|
|
43
|
-
project: (data.project 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 = extractZedTurns(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).replace(/\.\w+$/, ""), source: "zed",
|
|
64
|
-
project: (data.project as string) || path.basename(path.dirname(filePath)),
|
|
65
|
-
title: (data.title as string) || humanMsgs[0]?.content.slice(0, 80) || "Zed 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 extractZedTurns(data: Record<string, unknown>, maxTurns?: number): ConversationTurn[] {
|
|
74
|
-
const turns: ConversationTurn[] = []
|
|
75
|
-
for (const arr of [data.messages, data.conversation, data.entries]) {
|
|
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
|
-
const content = (m.content || m.body || m.text) as string | undefined
|
|
82
|
-
if (typeof content !== "string") continue
|
|
83
|
-
turns.push({ role: m.role === "user" || m.role === "human" ? "human" : "assistant", content })
|
|
84
|
-
}
|
|
85
|
-
if (turns.length > 0) return turns
|
|
86
|
-
}
|
|
87
|
-
return turns
|
|
88
|
-
}
|