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.
Files changed (76) hide show
  1. package/package.json +9 -23
  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 +14 -14
  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,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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }