codeblog-app 0.4.1 → 0.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
package/src/ai/chat.ts CHANGED
@@ -45,11 +45,21 @@ Use code examples when relevant. Think Juejin / HN / Linux.do vibes — not a co
45
45
  })
46
46
 
47
47
  let full = ""
48
- for await (const chunk of result.textStream) {
49
- full += chunk
50
- callbacks.onToken?.(chunk)
48
+ try {
49
+ for await (const chunk of result.textStream) {
50
+ full += chunk
51
+ callbacks.onToken?.(chunk)
52
+ }
53
+ callbacks.onFinish?.(full)
54
+ } catch (err) {
55
+ const error = err instanceof Error ? err : new Error(String(err))
56
+ log.error("stream error", { error: error.message })
57
+ if (callbacks.onError) {
58
+ callbacks.onError(error)
59
+ } else {
60
+ throw error
61
+ }
51
62
  }
52
- callbacks.onFinish?.(full)
53
63
  return full
54
64
  }
55
65
 
@@ -3,7 +3,7 @@ import { createOpenAI } from "@ai-sdk/openai"
3
3
  import { createGoogleGenerativeAI } from "@ai-sdk/google"
4
4
  import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
5
5
  import { createAzure } from "@ai-sdk/azure"
6
- import { createGoogleGenerativeAI as createVertex } from "@ai-sdk/google"
6
+ import { createVertex } from "@ai-sdk/google-vertex"
7
7
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
8
8
  import { createOpenRouter } from "@openrouter/ai-sdk-provider"
9
9
  import { createXai } from "@ai-sdk/xai"
@@ -157,10 +157,12 @@ export namespace AIProvider {
157
157
  return {}
158
158
  }
159
159
 
160
- // Refresh models.dev in background
161
- if (typeof globalThis.setTimeout !== "undefined") {
160
+ // Refresh models.dev in background (lazy, only on first use)
161
+ let modelsDevInitialized = false
162
+ function ensureModelsDev() {
163
+ if (modelsDevInitialized) return
164
+ modelsDevInitialized = true
162
165
  fetchModelsDev().catch(() => {})
163
- setInterval(() => fetchModelsDev().catch(() => {}), 60 * 60 * 1000).unref?.()
164
166
  }
165
167
 
166
168
  // ---------------------------------------------------------------------------
@@ -271,11 +273,10 @@ export namespace AIProvider {
271
273
  sdkCache.set(cacheKey, sdk)
272
274
  }
273
275
 
274
- // OpenAI uses responses API
275
- if (providerID === "openai" && "responses" in (sdk as any)) {
276
- return (sdk as any).responses(modelID)
276
+ if (typeof (sdk as any).languageModel === "function") {
277
+ return (sdk as any).languageModel(modelID)
277
278
  }
278
- return (sdk as any).languageModel?.(modelID) ?? (sdk as any)(modelID)
279
+ return (sdk as any)(modelID)
279
280
  }
280
281
 
281
282
  function noKeyError(providerID: string): Error {
@@ -0,0 +1,58 @@
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
+ }
@@ -0,0 +1,110 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { scanAll, parseSession, registerAllScanners, analyzeSession } from "../../scanner"
3
+ import { Posts } from "../../api/posts"
4
+ import { UI } from "../ui"
5
+
6
+ export const WeeklyDigestCommand: CommandModule = {
7
+ command: "weekly-digest",
8
+ aliases: ["digest"],
9
+ describe: "Generate a weekly coding digest from your IDE sessions",
10
+ builder: (yargs) =>
11
+ yargs
12
+ .option("post", {
13
+ describe: "Auto-post the digest to CodeBlog",
14
+ type: "boolean",
15
+ default: false,
16
+ })
17
+ .option("dry-run", {
18
+ describe: "Preview without posting (default)",
19
+ type: "boolean",
20
+ default: true,
21
+ }),
22
+ handler: async (args) => {
23
+ try {
24
+ registerAllScanners()
25
+ const sessions = scanAll(50)
26
+ const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
27
+ const recent = sessions.filter((s) => s.modifiedAt >= cutoff)
28
+
29
+ if (recent.length === 0) {
30
+ UI.warn("No coding sessions found in the last 7 days.")
31
+ return
32
+ }
33
+
34
+ const languages = new Set<string>()
35
+ const topics = new Set<string>()
36
+ const tags = new Set<string>()
37
+ const problems: string[] = []
38
+ const insights: string[] = []
39
+ const projects = new Set<string>()
40
+ const sources = new Set<string>()
41
+ let turns = 0
42
+
43
+ for (const session of recent) {
44
+ projects.add(session.project)
45
+ sources.add(session.source)
46
+ turns += session.messageCount
47
+ const parsed = parseSession(session.filePath, session.source, 30)
48
+ if (!parsed || parsed.turns.length === 0) continue
49
+ const analysis = analyzeSession(parsed)
50
+ analysis.languages.forEach((l) => languages.add(l))
51
+ analysis.topics.forEach((t) => topics.add(t))
52
+ analysis.suggestedTags.forEach((t) => tags.add(t))
53
+ problems.push(...analysis.problems.slice(0, 2))
54
+ insights.push(...analysis.keyInsights.slice(0, 2))
55
+ }
56
+
57
+ const projectArr = [...projects]
58
+ const langArr = [...languages]
59
+
60
+ let digest = `## This Week in Code\n\n`
61
+ digest += `*${recent.length} sessions across ${projectArr.length} project${projectArr.length > 1 ? "s" : ""}*\n\n`
62
+ digest += `### Overview\n`
63
+ digest += `- **Sessions:** ${recent.length}\n`
64
+ digest += `- **Total messages:** ${turns}\n`
65
+ digest += `- **Projects:** ${projectArr.slice(0, 5).join(", ")}\n`
66
+ digest += `- **IDEs:** ${[...sources].join(", ")}\n`
67
+ if (langArr.length > 0) digest += `- **Languages:** ${langArr.join(", ")}\n`
68
+ if (topics.size > 0) digest += `- **Topics:** ${[...topics].join(", ")}\n`
69
+ digest += `\n`
70
+
71
+ if (problems.length > 0) {
72
+ digest += `### Problems Tackled\n`
73
+ for (const p of [...new Set(problems)].slice(0, 5)) digest += `- ${p.slice(0, 150)}\n`
74
+ digest += `\n`
75
+ }
76
+
77
+ if (insights.length > 0) {
78
+ digest += `### Key Insights\n`
79
+ for (const i of [...new Set(insights)].slice(0, 5)) digest += `- ${i.slice(0, 150)}\n`
80
+ digest += `\n`
81
+ }
82
+
83
+ digest += `---\n\n*Weekly digest generated from ${[...sources].join(", ")} sessions*\n`
84
+
85
+ const title = `Weekly Digest: ${projectArr.slice(0, 2).join(" & ")} — ${langArr.slice(0, 3).join(", ") || "coding"} week`
86
+
87
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Title:${UI.Style.TEXT_NORMAL} ${title}`)
88
+ console.log(` ${UI.Style.TEXT_DIM}Tags: ${[...tags].slice(0, 8).join(", ")}${UI.Style.TEXT_NORMAL}`)
89
+ console.log("")
90
+ console.log(digest)
91
+
92
+ if (args.post && !args.dryRun) {
93
+ UI.info("Publishing digest to CodeBlog...")
94
+ const post = await Posts.create({
95
+ title: title.slice(0, 80),
96
+ content: digest,
97
+ tags: [...tags].slice(0, 8),
98
+ summary: `${recent.length} sessions, ${projectArr.length} projects, ${langArr.length} languages this week`,
99
+ source_session: recent[0].filePath,
100
+ })
101
+ UI.success(`Published! Post ID: ${post.post.id}`)
102
+ } else {
103
+ console.log(` ${UI.Style.TEXT_DIM}Use --post --no-dry-run to publish this digest.${UI.Style.TEXT_NORMAL}`)
104
+ }
105
+ } catch (err) {
106
+ UI.error(`Weekly digest failed: ${err instanceof Error ? err.message : String(err)}`)
107
+ process.exitCode = 1
108
+ }
109
+ },
110
+ }
package/src/index.ts CHANGED
@@ -31,8 +31,10 @@ import { ChatCommand } from "./cli/cmd/chat"
31
31
  import { ConfigCommand } from "./cli/cmd/config"
32
32
  import { AIPublishCommand } from "./cli/cmd/ai-publish"
33
33
  import { TuiCommand } from "./cli/cmd/tui"
34
+ import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
35
+ import { TagsCommand } from "./cli/cmd/tags"
34
36
 
35
- const VERSION = "0.4.1"
37
+ const VERSION = "0.4.2"
36
38
 
37
39
  process.on("unhandledRejection", (e) => {
38
40
  Log.Default.error("rejection", {
@@ -98,9 +100,12 @@ const cli = yargs(hideBin(process.argv))
98
100
  .command(ScanCommand)
99
101
  .command(PublishCommand)
100
102
  .command(AIPublishCommand)
103
+ .command(WeeklyDigestCommand)
101
104
  // AI
102
105
  .command(ChatCommand)
103
106
  .command(ConfigCommand)
107
+ // Browse
108
+ .command(TagsCommand)
104
109
  // TUI
105
110
  .command(TuiCommand)
106
111
  // Account
package/src/tui/app.tsx CHANGED
@@ -123,7 +123,7 @@ function App() {
123
123
  {loggedIn() ? "● " : "○ "}
124
124
  </text>
125
125
  <text fg="#6a737c">{loggedIn() ? "logged in" : "not logged in"}</text>
126
- <text fg="#6a737c">{" codeblog v0.4.1"}</text>
126
+ <text fg="#6a737c">{" codeblog v0.4.2"}</text>
127
127
  </box>
128
128
  </box>
129
129
  )
@@ -20,12 +20,16 @@ export function Home() {
20
20
  const [loading, setLoading] = createSignal(true)
21
21
  const [selected, setSelected] = createSignal(0)
22
22
 
23
+ const [error, setError] = createSignal("")
24
+
23
25
  onMount(async () => {
24
26
  try {
25
27
  const { Feed } = await import("../../api/feed")
26
28
  const result = await Feed.list()
27
29
  setPosts(result.posts as unknown as FeedPost[])
28
- } catch {
30
+ } catch (err) {
31
+ const msg = err instanceof Error ? err.message : String(err)
32
+ setError(msg.includes("401") ? "Not logged in. Run: codeblog login" : msg)
29
33
  setPosts([])
30
34
  }
31
35
  setLoading(false)
@@ -72,7 +76,13 @@ export function Home() {
72
76
  </box>
73
77
  </Show>
74
78
 
75
- <Show when={!loading() && posts().length === 0}>
79
+ <Show when={error()}>
80
+ <box paddingLeft={4} paddingTop={1}>
81
+ <text fg="#d73a49">{error()}</text>
82
+ </box>
83
+ </Show>
84
+
85
+ <Show when={!loading() && !error() && posts().length === 0}>
76
86
  <box paddingLeft={4} paddingTop={1}>
77
87
  <text fg="#6a737c">No posts yet. Press c to start an AI chat.</text>
78
88
  </box>
@@ -14,8 +14,9 @@ export function Post() {
14
14
  try {
15
15
  const { Posts } = await import("../../api/posts")
16
16
  const result = await Posts.detail(postId())
17
- setPost(result.post || result)
18
- setComments(result.comments || [])
17
+ const p = result.post || result
18
+ setPost(p)
19
+ setComments(p.comments || [])
19
20
  } catch {
20
21
  setPost(null)
21
22
  }
@@ -15,7 +15,7 @@ export function Search() {
15
15
  setSearched(true)
16
16
  try {
17
17
  const { Search } = await import("../../api/search")
18
- const result = await Search.query({ q: q.trim() })
18
+ const result = await Search.query(q.trim())
19
19
  setResults(result.results || result.posts || [])
20
20
  } catch {
21
21
  setResults([])
@@ -10,7 +10,7 @@ export function Trending() {
10
10
  try {
11
11
  const { Trending } = await import("../../api/trending")
12
12
  const result = await Trending.get()
13
- setData(result)
13
+ setData(result.trending || result)
14
14
  } catch {
15
15
  setData(null)
16
16
  }
@@ -54,7 +54,7 @@ export function Trending() {
54
54
  {/* Posts tab */}
55
55
  <Show when={tab() === "posts"}>
56
56
  <box flexDirection="column" paddingTop={1}>
57
- <For each={data()?.posts || []}>
57
+ <For each={data()?.top_upvoted || data()?.posts || []}>
58
58
  {(post: any) => (
59
59
  <box flexDirection="row" paddingLeft={2} paddingRight={2}>
60
60
  <box width={6} justifyContent="flex-end" marginRight={1}>
@@ -64,7 +64,7 @@ export function Trending() {
64
64
  <text fg="#e7e9eb">
65
65
  <span style={{ bold: true }}>{post.title}</span>
66
66
  </text>
67
- <text fg="#6a737c">{`💬${post.comment_count ?? 0} by ${post.agent ?? "anon"}`}</text>
67
+ <text fg="#6a737c">{`👁${post.views ?? 0} 💬${post.comments ?? post.comment_count ?? 0} by ${post.agent ?? "anon"}`}</text>
68
68
  </box>
69
69
  </box>
70
70
  )}
@@ -75,10 +75,10 @@ export function Trending() {
75
75
  {/* Tags tab */}
76
76
  <Show when={tab() === "tags"}>
77
77
  <box flexDirection="column" paddingTop={1} paddingLeft={2}>
78
- <For each={data()?.tags || []}>
78
+ <For each={data()?.trending_tags || data()?.tags || []}>
79
79
  {(tag: any) => (
80
80
  <box flexDirection="row" gap={2}>
81
- <text fg="#39739d">{`#${tag.name || tag}`}</text>
81
+ <text fg="#39739d">{`#${tag.tag || tag.name || tag}`}</text>
82
82
  <text fg="#6a737c">{`${tag.count ?? ""} posts`}</text>
83
83
  </box>
84
84
  )}
@@ -89,13 +89,13 @@ export function Trending() {
89
89
  {/* Agents tab */}
90
90
  <Show when={tab() === "agents"}>
91
91
  <box flexDirection="column" paddingTop={1} paddingLeft={2}>
92
- <For each={data()?.agents || []}>
92
+ <For each={data()?.top_agents || data()?.agents || []}>
93
93
  {(agent: any) => (
94
94
  <box flexDirection="row" gap={2}>
95
95
  <text fg="#0074cc">
96
96
  <span style={{ bold: true }}>{agent.name || agent.username || agent}</span>
97
97
  </text>
98
- <text fg="#6a737c">{`${agent.post_count ?? ""} posts`}</text>
98
+ <text fg="#6a737c">{`${agent.posts ?? agent.post_count ?? ""} posts`}</text>
99
99
  </box>
100
100
  )}
101
101
  </For>