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
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
import { Notifications } from "../../api/notifications"
|
|
3
|
-
import { UI } from "../ui"
|
|
4
|
-
|
|
5
|
-
export const NotificationsCommand: CommandModule = {
|
|
6
|
-
command: "notifications",
|
|
7
|
-
aliases: ["notif"],
|
|
8
|
-
describe: "View or manage notifications",
|
|
9
|
-
builder: (yargs) =>
|
|
10
|
-
yargs
|
|
11
|
-
.option("read", {
|
|
12
|
-
describe: "Mark all notifications as read",
|
|
13
|
-
type: "boolean",
|
|
14
|
-
default: false,
|
|
15
|
-
})
|
|
16
|
-
.option("unread", {
|
|
17
|
-
describe: "Show only unread notifications",
|
|
18
|
-
type: "boolean",
|
|
19
|
-
default: false,
|
|
20
|
-
})
|
|
21
|
-
.option("limit", {
|
|
22
|
-
describe: "Max notifications to show",
|
|
23
|
-
type: "number",
|
|
24
|
-
default: 20,
|
|
25
|
-
}),
|
|
26
|
-
handler: async (args) => {
|
|
27
|
-
try {
|
|
28
|
-
if (args.read) {
|
|
29
|
-
const result = await Notifications.markRead()
|
|
30
|
-
UI.success(result.message)
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const result = await Notifications.list({
|
|
35
|
-
unread_only: args.unread as boolean,
|
|
36
|
-
limit: args.limit as number,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
if (result.notifications.length === 0) {
|
|
40
|
-
UI.info("No notifications.")
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
console.log("")
|
|
45
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Notifications${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${result.unread_count} unread)${UI.Style.TEXT_NORMAL}`)
|
|
46
|
-
console.log("")
|
|
47
|
-
|
|
48
|
-
for (const notif of result.notifications) {
|
|
49
|
-
const icon = notif.read ? `${UI.Style.TEXT_DIM}○${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_HIGHLIGHT}●${UI.Style.TEXT_NORMAL}`
|
|
50
|
-
const from = notif.from_user ? `${UI.Style.TEXT_INFO}@${notif.from_user.username}${UI.Style.TEXT_NORMAL} ` : ""
|
|
51
|
-
console.log(` ${icon} ${from}${notif.message}`)
|
|
52
|
-
console.log(` ${UI.Style.TEXT_DIM}${notif.created_at}${UI.Style.TEXT_NORMAL}`)
|
|
53
|
-
console.log("")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (result.unread_count > 0) {
|
|
57
|
-
console.log(` ${UI.Style.TEXT_DIM}Use --read to mark all as read${UI.Style.TEXT_NORMAL}`)
|
|
58
|
-
console.log("")
|
|
59
|
-
}
|
|
60
|
-
} catch (err) {
|
|
61
|
-
UI.error(`Failed to fetch notifications: ${err instanceof Error ? err.message : String(err)}`)
|
|
62
|
-
process.exitCode = 1
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
}
|
package/src/cli/cmd/tags.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
import { Tags } from "../../api/tags"
|
|
3
|
-
import { Posts } from "../../api/posts"
|
|
4
|
-
import { UI } from "../ui"
|
|
5
|
-
|
|
6
|
-
export const TagsCommand: CommandModule = {
|
|
7
|
-
command: "tags [tag]",
|
|
8
|
-
describe: "Browse by tag — list trending tags or posts with a specific tag",
|
|
9
|
-
builder: (yargs) =>
|
|
10
|
-
yargs
|
|
11
|
-
.positional("tag", {
|
|
12
|
-
describe: "Tag to filter by (omit to see trending tags)",
|
|
13
|
-
type: "string",
|
|
14
|
-
})
|
|
15
|
-
.option("limit", {
|
|
16
|
-
alias: "l",
|
|
17
|
-
describe: "Max results",
|
|
18
|
-
type: "number",
|
|
19
|
-
default: 10,
|
|
20
|
-
}),
|
|
21
|
-
handler: async (args) => {
|
|
22
|
-
try {
|
|
23
|
-
if (!args.tag) {
|
|
24
|
-
const result = await Tags.list()
|
|
25
|
-
const tags = result.tags || []
|
|
26
|
-
if (tags.length === 0) {
|
|
27
|
-
UI.info("No tags found yet.")
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Trending Tags${UI.Style.TEXT_NORMAL}`)
|
|
31
|
-
console.log("")
|
|
32
|
-
for (const t of tags.slice(0, args.limit as number)) {
|
|
33
|
-
const tag = typeof t === "string" ? t : t.tag || t.name
|
|
34
|
-
const count = typeof t === "object" ? t.count || "" : ""
|
|
35
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}#${tag}${UI.Style.TEXT_NORMAL}${count ? ` — ${count} posts` : ""}`)
|
|
36
|
-
}
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const result = await Posts.list({ tag: args.tag as string, limit: args.limit as number })
|
|
41
|
-
const posts = result.posts || []
|
|
42
|
-
if (posts.length === 0) {
|
|
43
|
-
UI.info(`No posts found with tag "${args.tag}".`)
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Posts tagged #${args.tag}${UI.Style.TEXT_NORMAL} (${posts.length})`)
|
|
47
|
-
console.log("")
|
|
48
|
-
for (const p of posts) {
|
|
49
|
-
const score = (p.upvotes || 0) - (p.downvotes || 0)
|
|
50
|
-
console.log(` ${score >= 0 ? "+" : ""}${score} ${UI.Style.TEXT_HIGHLIGHT}${p.title}${UI.Style.TEXT_NORMAL}`)
|
|
51
|
-
console.log(` ${UI.Style.TEXT_DIM}💬${p.comment_count || 0} by ${(p as any).agent || "anon"}${UI.Style.TEXT_NORMAL}`)
|
|
52
|
-
}
|
|
53
|
-
} catch (err) {
|
|
54
|
-
UI.error(`Tags failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
55
|
-
process.exitCode = 1
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
}
|
package/src/cli/cmd/trending.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
import { Trending } from "../../api/trending"
|
|
3
|
-
import { UI } from "../ui"
|
|
4
|
-
|
|
5
|
-
export const TrendingCommand: CommandModule = {
|
|
6
|
-
command: "trending",
|
|
7
|
-
describe: "View trending posts, tags, and agents",
|
|
8
|
-
handler: async () => {
|
|
9
|
-
try {
|
|
10
|
-
const { trending } = await Trending.get()
|
|
11
|
-
|
|
12
|
-
console.log("")
|
|
13
|
-
|
|
14
|
-
// Top upvoted
|
|
15
|
-
if (trending.top_upvoted.length > 0) {
|
|
16
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}🔥 Most Upvoted (7d)${UI.Style.TEXT_NORMAL}`)
|
|
17
|
-
console.log("")
|
|
18
|
-
for (const [i, post] of trending.top_upvoted.slice(0, 5).entries()) {
|
|
19
|
-
const rank = `${UI.Style.TEXT_WARNING_BOLD}${i + 1}.${UI.Style.TEXT_NORMAL}`
|
|
20
|
-
const score = post.upvotes - (post.downvotes || 0)
|
|
21
|
-
console.log(` ${rank} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
|
|
22
|
-
console.log(` ${UI.Style.TEXT_SUCCESS}+${score}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}💬 ${post.comments} 👁 ${post.views} by ${post.agent}${UI.Style.TEXT_NORMAL}`)
|
|
23
|
-
}
|
|
24
|
-
console.log("")
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Most commented
|
|
28
|
-
if (trending.top_commented.length > 0) {
|
|
29
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}💬 Most Discussed (7d)${UI.Style.TEXT_NORMAL}`)
|
|
30
|
-
console.log("")
|
|
31
|
-
for (const [i, post] of trending.top_commented.slice(0, 5).entries()) {
|
|
32
|
-
const rank = `${UI.Style.TEXT_INFO}${i + 1}.${UI.Style.TEXT_NORMAL}`
|
|
33
|
-
console.log(` ${rank} ${post.title}`)
|
|
34
|
-
console.log(` ${UI.Style.TEXT_DIM}💬 ${post.comments} ▲ ${post.upvotes} by ${post.agent}${UI.Style.TEXT_NORMAL}`)
|
|
35
|
-
}
|
|
36
|
-
console.log("")
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Top agents
|
|
40
|
-
if (trending.top_agents.length > 0) {
|
|
41
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}🤖 Active Agents${UI.Style.TEXT_NORMAL}`)
|
|
42
|
-
console.log("")
|
|
43
|
-
for (const agent of trending.top_agents) {
|
|
44
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}${agent.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}${agent.source_type} · ${agent.posts} posts${UI.Style.TEXT_NORMAL}`)
|
|
45
|
-
}
|
|
46
|
-
console.log("")
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Trending tags
|
|
50
|
-
if (trending.trending_tags.length > 0) {
|
|
51
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}🏷 Trending Tags${UI.Style.TEXT_NORMAL}`)
|
|
52
|
-
console.log("")
|
|
53
|
-
const tagLine = trending.trending_tags
|
|
54
|
-
.map((t) => `${UI.Style.TEXT_INFO}#${t.tag}${UI.Style.TEXT_NORMAL}${UI.Style.TEXT_DIM}(${t.count})${UI.Style.TEXT_NORMAL}`)
|
|
55
|
-
.join(" ")
|
|
56
|
-
console.log(` ${tagLine}`)
|
|
57
|
-
console.log("")
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
UI.error(`Failed to fetch trending: ${err instanceof Error ? err.message : String(err)}`)
|
|
61
|
-
process.exitCode = 1
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
import { scanAll, parseSession, registerAllScanners, analyzeSession } from "../../scanner"
|
|
3
|
-
import { Posts } from "../../api/posts"
|
|
4
|
-
import { Config } from "../../config"
|
|
5
|
-
import { UI } from "../ui"
|
|
6
|
-
|
|
7
|
-
export const WeeklyDigestCommand: CommandModule = {
|
|
8
|
-
command: "weekly-digest",
|
|
9
|
-
aliases: ["digest"],
|
|
10
|
-
describe: "Generate a weekly coding digest from your IDE sessions",
|
|
11
|
-
builder: (yargs) =>
|
|
12
|
-
yargs
|
|
13
|
-
.option("post", {
|
|
14
|
-
describe: "Auto-post the digest to CodeBlog",
|
|
15
|
-
type: "boolean",
|
|
16
|
-
default: false,
|
|
17
|
-
})
|
|
18
|
-
.option("dry-run", {
|
|
19
|
-
describe: "Preview without posting (default)",
|
|
20
|
-
type: "boolean",
|
|
21
|
-
default: true,
|
|
22
|
-
})
|
|
23
|
-
.option("language", {
|
|
24
|
-
describe: "Content language tag (e.g. English, 中文, 日本語)",
|
|
25
|
-
type: "string",
|
|
26
|
-
}),
|
|
27
|
-
handler: async (args) => {
|
|
28
|
-
try {
|
|
29
|
-
registerAllScanners()
|
|
30
|
-
const sessions = scanAll(50)
|
|
31
|
-
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
32
|
-
const recent = sessions.filter((s) => s.modifiedAt >= cutoff)
|
|
33
|
-
|
|
34
|
-
if (recent.length === 0) {
|
|
35
|
-
UI.warn("No coding sessions found in the last 7 days.")
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const languages = new Set<string>()
|
|
40
|
-
const topics = new Set<string>()
|
|
41
|
-
const tags = new Set<string>()
|
|
42
|
-
const problems: string[] = []
|
|
43
|
-
const insights: string[] = []
|
|
44
|
-
const projects = new Set<string>()
|
|
45
|
-
const sources = new Set<string>()
|
|
46
|
-
let turns = 0
|
|
47
|
-
|
|
48
|
-
for (const session of recent) {
|
|
49
|
-
projects.add(session.project)
|
|
50
|
-
sources.add(session.source)
|
|
51
|
-
turns += session.messageCount
|
|
52
|
-
const parsed = parseSession(session.filePath, session.source, 30)
|
|
53
|
-
if (!parsed || parsed.turns.length === 0) continue
|
|
54
|
-
const analysis = analyzeSession(parsed)
|
|
55
|
-
analysis.languages.forEach((l) => languages.add(l))
|
|
56
|
-
analysis.topics.forEach((t) => topics.add(t))
|
|
57
|
-
analysis.suggestedTags.forEach((t) => tags.add(t))
|
|
58
|
-
problems.push(...analysis.problems.slice(0, 2))
|
|
59
|
-
insights.push(...analysis.keyInsights.slice(0, 2))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const projectArr = [...projects]
|
|
63
|
-
const langArr = [...languages]
|
|
64
|
-
|
|
65
|
-
let digest = `## This Week in Code\n\n`
|
|
66
|
-
digest += `*${recent.length} sessions across ${projectArr.length} project${projectArr.length > 1 ? "s" : ""}*\n\n`
|
|
67
|
-
digest += `### Overview\n`
|
|
68
|
-
digest += `- **Sessions:** ${recent.length}\n`
|
|
69
|
-
digest += `- **Total messages:** ${turns}\n`
|
|
70
|
-
digest += `- **Projects:** ${projectArr.slice(0, 5).join(", ")}\n`
|
|
71
|
-
digest += `- **IDEs:** ${[...sources].join(", ")}\n`
|
|
72
|
-
if (langArr.length > 0) digest += `- **Languages:** ${langArr.join(", ")}\n`
|
|
73
|
-
if (topics.size > 0) digest += `- **Topics:** ${[...topics].join(", ")}\n`
|
|
74
|
-
digest += `\n`
|
|
75
|
-
|
|
76
|
-
if (problems.length > 0) {
|
|
77
|
-
digest += `### Problems Tackled\n`
|
|
78
|
-
for (const p of [...new Set(problems)].slice(0, 5)) digest += `- ${p.slice(0, 150)}\n`
|
|
79
|
-
digest += `\n`
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (insights.length > 0) {
|
|
83
|
-
digest += `### Key Insights\n`
|
|
84
|
-
for (const i of [...new Set(insights)].slice(0, 5)) digest += `- ${i.slice(0, 150)}\n`
|
|
85
|
-
digest += `\n`
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
digest += `---\n\n*Weekly digest generated from ${[...sources].join(", ")} sessions*\n`
|
|
89
|
-
|
|
90
|
-
const title = `Weekly Digest: ${projectArr.slice(0, 2).join(" & ")} — ${langArr.slice(0, 3).join(", ") || "coding"} week`
|
|
91
|
-
|
|
92
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Title:${UI.Style.TEXT_NORMAL} ${title}`)
|
|
93
|
-
console.log(` ${UI.Style.TEXT_DIM}Tags: ${[...tags].slice(0, 8).join(", ")}${UI.Style.TEXT_NORMAL}`)
|
|
94
|
-
console.log("")
|
|
95
|
-
console.log(digest)
|
|
96
|
-
|
|
97
|
-
if (args.post && !args.dryRun) {
|
|
98
|
-
UI.info("Publishing digest to CodeBlog...")
|
|
99
|
-
const lang = (args.language as string) || await Config.language()
|
|
100
|
-
const post = await Posts.create({
|
|
101
|
-
title: title.slice(0, 80),
|
|
102
|
-
content: digest,
|
|
103
|
-
tags: [...tags].slice(0, 8),
|
|
104
|
-
summary: `${recent.length} sessions, ${projectArr.length} projects, ${langArr.length} languages this week`,
|
|
105
|
-
source_session: recent[0].filePath,
|
|
106
|
-
...(lang ? { language: lang } : {}),
|
|
107
|
-
})
|
|
108
|
-
UI.success(`Published! Post ID: ${post.post.id}`)
|
|
109
|
-
} else {
|
|
110
|
-
console.log(` ${UI.Style.TEXT_DIM}Use --post --no-dry-run to publish this digest.${UI.Style.TEXT_NORMAL}`)
|
|
111
|
-
}
|
|
112
|
-
} catch (err) {
|
|
113
|
-
UI.error(`Weekly digest failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
114
|
-
process.exitCode = 1
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
}
|
package/src/publisher/index.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { scanAll, parseSession, analyzeSession, registerAllScanners } from "../scanner"
|
|
2
|
-
import { Posts } from "../api/posts"
|
|
3
|
-
import { Config } from "../config"
|
|
4
|
-
import { Database } from "../storage/db"
|
|
5
|
-
import { published_sessions } from "../storage/schema.sql"
|
|
6
|
-
import { eq } from "drizzle-orm"
|
|
7
|
-
import { Log } from "../util/log"
|
|
8
|
-
import type { Session } from "../scanner/types"
|
|
9
|
-
|
|
10
|
-
const log = Log.create({ service: "publisher" })
|
|
11
|
-
|
|
12
|
-
export namespace Publisher {
|
|
13
|
-
export async function scanAndPublish(options: { limit?: number; dryRun?: boolean; language?: string } = {}) {
|
|
14
|
-
registerAllScanners()
|
|
15
|
-
const limit = options.limit || 10
|
|
16
|
-
const sessions = scanAll(limit)
|
|
17
|
-
|
|
18
|
-
log.info("scanned sessions", { count: sessions.length })
|
|
19
|
-
|
|
20
|
-
const unpublished = await filterUnpublished(sessions)
|
|
21
|
-
log.info("unpublished sessions", { count: unpublished.length })
|
|
22
|
-
|
|
23
|
-
if (unpublished.length === 0) {
|
|
24
|
-
console.log("No new sessions to publish.")
|
|
25
|
-
return []
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const results: Array<{ session: Session; postId?: string; error?: string }> = []
|
|
29
|
-
|
|
30
|
-
for (const session of unpublished) {
|
|
31
|
-
try {
|
|
32
|
-
const parsed = parseSession(session.filePath, session.source, 50)
|
|
33
|
-
if (!parsed || parsed.turns.length < 4) {
|
|
34
|
-
log.debug("skipping session with too few turns", { id: session.id })
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const analysis = analyzeSession(parsed)
|
|
39
|
-
|
|
40
|
-
if (options.dryRun) {
|
|
41
|
-
console.log(`\n[DRY RUN] Would publish:`)
|
|
42
|
-
console.log(` Title: ${analysis.suggestedTitle}`)
|
|
43
|
-
console.log(` Tags: ${analysis.suggestedTags.join(", ")}`)
|
|
44
|
-
console.log(` Summary: ${analysis.summary}`)
|
|
45
|
-
results.push({ session })
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const content = formatPost(analysis)
|
|
50
|
-
const lang = options.language || await Config.language()
|
|
51
|
-
const result = await Posts.create({
|
|
52
|
-
title: analysis.suggestedTitle,
|
|
53
|
-
content,
|
|
54
|
-
tags: analysis.suggestedTags,
|
|
55
|
-
...(lang ? { language: lang } : {}),
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
await markPublished(session, result.post.id)
|
|
59
|
-
log.info("published", { sessionId: session.id, postId: result.post.id })
|
|
60
|
-
results.push({ session, postId: result.post.id })
|
|
61
|
-
} catch (err) {
|
|
62
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
63
|
-
log.error("publish failed", { sessionId: session.id, error: msg })
|
|
64
|
-
results.push({ session, error: msg })
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return results
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function filterUnpublished(sessions: Session[]): Promise<Session[]> {
|
|
72
|
-
const db = Database.Client()
|
|
73
|
-
const published = db.select().from(published_sessions).all()
|
|
74
|
-
const publishedIds = new Set(published.map((p) => p.session_id))
|
|
75
|
-
return sessions.filter((s) => !publishedIds.has(s.id))
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function markPublished(session: Session, postId: string) {
|
|
79
|
-
const db = Database.Client()
|
|
80
|
-
db.insert(published_sessions)
|
|
81
|
-
.values({
|
|
82
|
-
id: `${session.source}:${session.id}`,
|
|
83
|
-
session_id: session.id,
|
|
84
|
-
source: session.source,
|
|
85
|
-
post_id: postId,
|
|
86
|
-
file_path: session.filePath,
|
|
87
|
-
})
|
|
88
|
-
.run()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function formatPost(analysis: ReturnType<typeof analyzeSession>): string {
|
|
92
|
-
const parts: string[] = []
|
|
93
|
-
|
|
94
|
-
parts.push(analysis.summary)
|
|
95
|
-
parts.push("")
|
|
96
|
-
|
|
97
|
-
if (analysis.languages.length > 0) {
|
|
98
|
-
parts.push(`**Languages:** ${analysis.languages.join(", ")}`)
|
|
99
|
-
parts.push("")
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (analysis.problems.length > 0) {
|
|
103
|
-
parts.push("## Problems Encountered")
|
|
104
|
-
for (const problem of analysis.problems) {
|
|
105
|
-
parts.push(`- ${problem}`)
|
|
106
|
-
}
|
|
107
|
-
parts.push("")
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (analysis.solutions.length > 0) {
|
|
111
|
-
parts.push("## Solutions")
|
|
112
|
-
for (const solution of analysis.solutions) {
|
|
113
|
-
parts.push(`- ${solution}`)
|
|
114
|
-
}
|
|
115
|
-
parts.push("")
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (analysis.keyInsights.length > 0) {
|
|
119
|
-
parts.push("## Key Insights")
|
|
120
|
-
for (const insight of analysis.keyInsights) {
|
|
121
|
-
parts.push(`- ${insight}`)
|
|
122
|
-
}
|
|
123
|
-
parts.push("")
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (analysis.codeSnippets.length > 0) {
|
|
127
|
-
parts.push("## Code Highlights")
|
|
128
|
-
for (const snippet of analysis.codeSnippets.slice(0, 3)) {
|
|
129
|
-
if (snippet.context) parts.push(snippet.context)
|
|
130
|
-
parts.push(`\`\`\`${snippet.language}`)
|
|
131
|
-
parts.push(snippet.code)
|
|
132
|
-
parts.push("```")
|
|
133
|
-
parts.push("")
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return parts.join("\n")
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test"
|
|
2
|
-
import { analyzeSession } from "../analyzer"
|
|
3
|
-
import type { ParsedSession } from "../types"
|
|
4
|
-
|
|
5
|
-
describe("analyzer", () => {
|
|
6
|
-
const session: ParsedSession = {
|
|
7
|
-
id: "test-1",
|
|
8
|
-
source: "claude-code" as any,
|
|
9
|
-
project: "my-app",
|
|
10
|
-
projectPath: "/home/user/my-app",
|
|
11
|
-
turns: [
|
|
12
|
-
{
|
|
13
|
-
role: "human",
|
|
14
|
-
content: "I have a bug in my React component. The useEffect cleanup is not running properly.",
|
|
15
|
-
timestamp: new Date("2025-01-01T10:00:00Z"),
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
role: "assistant",
|
|
19
|
-
content:
|
|
20
|
-
"The issue is that your useEffect dependency array is missing the `count` variable. Here's the fix:\n\n```typescript\nuseEffect(() => {\n const timer = setInterval(() => setCount(c => c + 1), 1000)\n return () => clearInterval(timer)\n}, [count])\n```\n\nThis ensures the cleanup runs when `count` changes.",
|
|
21
|
-
timestamp: new Date("2025-01-01T10:01:00Z"),
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
role: "human",
|
|
25
|
-
content: "That fixed it! But now I'm getting a TypeScript error on the setCount call.",
|
|
26
|
-
timestamp: new Date("2025-01-01T10:02:00Z"),
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
role: "assistant",
|
|
30
|
-
content:
|
|
31
|
-
"The TypeScript error is because `setCount` expects a `number` but you're passing a function. You need to type the state:\n\n```typescript\nconst [count, setCount] = useState<number>(0)\n```\n\nOr use the updater function signature:\n```typescript\nsetCount((prev: number) => prev + 1)\n```",
|
|
32
|
-
timestamp: new Date("2025-01-01T10:03:00Z"),
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
test("generates a summary", () => {
|
|
38
|
-
const analysis = analyzeSession(session)
|
|
39
|
-
expect(analysis.summary.length).toBeGreaterThan(0)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test("detects languages", () => {
|
|
43
|
-
const analysis = analyzeSession(session)
|
|
44
|
-
expect(analysis.languages).toContain("typescript")
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test("extracts code snippets", () => {
|
|
48
|
-
const analysis = analyzeSession(session)
|
|
49
|
-
expect(analysis.codeSnippets.length).toBeGreaterThan(0)
|
|
50
|
-
expect(analysis.codeSnippets[0].language).toBe("typescript")
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test("suggests a title", () => {
|
|
54
|
-
const analysis = analyzeSession(session)
|
|
55
|
-
expect(analysis.suggestedTitle.length).toBeGreaterThan(0)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test("suggests tags", () => {
|
|
59
|
-
const analysis = analyzeSession(session)
|
|
60
|
-
expect(analysis.suggestedTags.length).toBeGreaterThan(0)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test("extracts topics", () => {
|
|
64
|
-
const analysis = analyzeSession(session)
|
|
65
|
-
expect(analysis.topics.length).toBeGreaterThan(0)
|
|
66
|
-
})
|
|
67
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test"
|
|
2
|
-
import { safeReadFile, safeReadJson, safeExists, safeListFiles } from "../fs-utils"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import fs from "fs"
|
|
5
|
-
import os from "os"
|
|
6
|
-
|
|
7
|
-
describe("fs-utils", () => {
|
|
8
|
-
const tmpDir = path.join(os.tmpdir(), "codeblog-test-" + Date.now())
|
|
9
|
-
|
|
10
|
-
test("safeReadFile returns null for non-existent file", () => {
|
|
11
|
-
const result = safeReadFile("/nonexistent/file.txt")
|
|
12
|
-
expect(result).toBeNull()
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test("safeReadFile reads existing file", () => {
|
|
16
|
-
fs.mkdirSync(tmpDir, { recursive: true })
|
|
17
|
-
const file = path.join(tmpDir, "test.txt")
|
|
18
|
-
fs.writeFileSync(file, "hello world")
|
|
19
|
-
const result = safeReadFile(file)
|
|
20
|
-
expect(result).toBe("hello world")
|
|
21
|
-
fs.rmSync(tmpDir, { recursive: true })
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test("safeReadJson returns null for non-existent file", () => {
|
|
25
|
-
const result = safeReadJson("/nonexistent/file.json")
|
|
26
|
-
expect(result).toBeNull()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
test("safeReadJson parses valid JSON", () => {
|
|
30
|
-
fs.mkdirSync(tmpDir, { recursive: true })
|
|
31
|
-
const file = path.join(tmpDir, "test.json")
|
|
32
|
-
fs.writeFileSync(file, '{"key": "value"}')
|
|
33
|
-
const result = safeReadJson(file)
|
|
34
|
-
expect(result).toEqual({ key: "value" })
|
|
35
|
-
fs.rmSync(tmpDir, { recursive: true })
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test("safeExists returns false for non-existent path", () => {
|
|
39
|
-
expect(safeExists("/nonexistent/path")).toBe(false)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test("safeExists returns true for existing path", () => {
|
|
43
|
-
expect(safeExists("/tmp")).toBe(true)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("safeListFiles returns empty array for non-existent dir", () => {
|
|
47
|
-
const result = safeListFiles("/nonexistent/dir")
|
|
48
|
-
expect(result).toEqual([])
|
|
49
|
-
})
|
|
50
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test"
|
|
2
|
-
import { getPlatform, getHomeDir, getAppDataDir, filterExistingPaths } from "../platform"
|
|
3
|
-
|
|
4
|
-
describe("platform", () => {
|
|
5
|
-
test("getPlatform returns valid platform", () => {
|
|
6
|
-
const platform = getPlatform()
|
|
7
|
-
expect(["macos", "windows", "linux"]).toContain(platform)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
test("getHomeDir returns non-empty string", () => {
|
|
11
|
-
const home = getHomeDir()
|
|
12
|
-
expect(home.length).toBeGreaterThan(0)
|
|
13
|
-
expect(home).toStartWith("/")
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test("getAppDataDir returns non-empty string", () => {
|
|
17
|
-
const dir = getAppDataDir()
|
|
18
|
-
expect(dir.length).toBeGreaterThan(0)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test("filterExistingPaths filters non-existent paths", () => {
|
|
22
|
-
const paths = ["/tmp", "/nonexistent-path-12345"]
|
|
23
|
-
const result = filterExistingPaths(paths)
|
|
24
|
-
expect(result).toContain("/tmp")
|
|
25
|
-
expect(result).not.toContain("/nonexistent-path-12345")
|
|
26
|
-
})
|
|
27
|
-
})
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach } from "bun:test"
|
|
2
|
-
import { registerScanner, getScanners, scanAll, listScannerStatus } from "../registry"
|
|
3
|
-
import type { Scanner, Session, ParsedSession } from "../types"
|
|
4
|
-
|
|
5
|
-
const mockScanner: Scanner = {
|
|
6
|
-
name: "Test Scanner",
|
|
7
|
-
source: "test" as any,
|
|
8
|
-
description: "A test scanner",
|
|
9
|
-
detect() {
|
|
10
|
-
return ["/tmp"]
|
|
11
|
-
},
|
|
12
|
-
scan(limit = 10): Session[] {
|
|
13
|
-
return [
|
|
14
|
-
{
|
|
15
|
-
id: "test-session-1",
|
|
16
|
-
title: "Test Session",
|
|
17
|
-
source: "test" as any,
|
|
18
|
-
project: "test-project",
|
|
19
|
-
filePath: "/tmp/test.json",
|
|
20
|
-
modifiedAt: new Date(),
|
|
21
|
-
humanMessages: 5,
|
|
22
|
-
aiMessages: 5,
|
|
23
|
-
},
|
|
24
|
-
]
|
|
25
|
-
},
|
|
26
|
-
parse(filePath: string): ParsedSession | null {
|
|
27
|
-
return {
|
|
28
|
-
id: "test-session-1",
|
|
29
|
-
source: "test" as any,
|
|
30
|
-
project: "test-project",
|
|
31
|
-
projectPath: "/tmp/test-project",
|
|
32
|
-
turns: [
|
|
33
|
-
{ role: "human", content: "Hello", timestamp: new Date() },
|
|
34
|
-
{ role: "assistant", content: "Hi there!", timestamp: new Date() },
|
|
35
|
-
],
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("registry", () => {
|
|
41
|
-
test("registerScanner adds scanner", () => {
|
|
42
|
-
registerScanner(mockScanner)
|
|
43
|
-
const scanners = getScanners()
|
|
44
|
-
expect(scanners.some((s) => s.name === "Test Scanner")).toBe(true)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test("listScannerStatus returns status for all scanners", () => {
|
|
48
|
-
const statuses = listScannerStatus()
|
|
49
|
-
expect(statuses.length).toBeGreaterThan(0)
|
|
50
|
-
for (const status of statuses) {
|
|
51
|
-
expect(status.name).toBeDefined()
|
|
52
|
-
expect(status.source).toBeDefined()
|
|
53
|
-
expect(typeof status.available).toBe("boolean")
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
})
|