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,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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- })