codeblog-app 0.4.3 → 1.1.0

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.3",
4
+ "version": "1.1.0",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -289,6 +289,17 @@ export namespace AIProvider {
289
289
  return cfg.model
290
290
  }
291
291
 
292
+ // ---------------------------------------------------------------------------
293
+ // Check if any AI provider has a key configured
294
+ // ---------------------------------------------------------------------------
295
+ export async function hasAnyKey(): Promise<boolean> {
296
+ for (const providerID of Object.keys(PROVIDER_ENV)) {
297
+ const key = await getApiKey(providerID)
298
+ if (key) return true
299
+ }
300
+ return false
301
+ }
302
+
292
303
  // ---------------------------------------------------------------------------
293
304
  // List available models with key status (for codeblog config --list)
294
305
  // ---------------------------------------------------------------------------
package/src/api/posts.ts CHANGED
@@ -8,6 +8,7 @@ export namespace Posts {
8
8
  content: string
9
9
  summary: string | null
10
10
  tags: string[]
11
+ language: string
11
12
  upvotes: number
12
13
  downvotes: number
13
14
  comment_count: number
@@ -22,6 +23,7 @@ export namespace Posts {
22
23
  content: string
23
24
  summary: string | null
24
25
  tags: string[]
26
+ language: string
25
27
  upvotes: number
26
28
  downvotes: number
27
29
  humanUpvotes: number
@@ -49,6 +51,7 @@ export namespace Posts {
49
51
  tags?: string[]
50
52
  category?: string
51
53
  source_session?: string
54
+ language?: string
52
55
  }
53
56
 
54
57
  export interface EditPostInput {
package/src/auth/index.ts CHANGED
@@ -24,7 +24,7 @@ export namespace Auth {
24
24
  }
25
25
 
26
26
  export async function set(token: Token) {
27
- await Bun.write(Bun.file(filepath), JSON.stringify(token, null, 2), { mode: 0o600 })
27
+ await Bun.write(Bun.file(filepath, { mode: 0o600 }), JSON.stringify(token, null, 2))
28
28
  }
29
29
 
30
30
  export async function remove() {
@@ -1,6 +1,8 @@
1
1
  import type { CommandModule } from "yargs"
2
2
  import { AIChat } from "../../ai/chat"
3
+ import { AIProvider } from "../../ai/provider"
3
4
  import { Posts } from "../../api/posts"
5
+ import { Config } from "../../config"
4
6
  import { scanAll, parseSession, registerAllScanners } from "../../scanner"
5
7
  import { UI } from "../ui"
6
8
 
@@ -24,9 +26,28 @@ export const AIPublishCommand: CommandModule = {
24
26
  describe: "Max sessions to scan",
25
27
  type: "number",
26
28
  default: 10,
29
+ })
30
+ .option("language", {
31
+ describe: "Content language tag (e.g. English, 中文, 日本語)",
32
+ type: "string",
27
33
  }),
28
34
  handler: async (args) => {
29
35
  try {
36
+ // Check AI key before scanning
37
+ const hasKey = await AIProvider.hasAnyKey()
38
+ if (!hasKey) {
39
+ console.log("")
40
+ UI.warn("No AI provider configured. ai-publish requires an AI API key to generate posts.")
41
+ console.log("")
42
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Configure an AI provider first:${UI.Style.TEXT_NORMAL}`)
43
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider anthropic --api-key sk-ant-...${UI.Style.TEXT_NORMAL}`)
44
+ console.log("")
45
+ console.log(` ${UI.Style.TEXT_DIM}Run: codeblog config --list to see all supported providers${UI.Style.TEXT_NORMAL}`)
46
+ console.log("")
47
+ process.exitCode = 1
48
+ return
49
+ }
50
+
30
51
  UI.info("Scanning IDE sessions...")
31
52
  registerAllScanners()
32
53
  const sessions = scanAll(args.limit as number)
@@ -78,12 +99,14 @@ export const AIPublishCommand: CommandModule = {
78
99
  }
79
100
 
80
101
  UI.info("Publishing to CodeBlog...")
102
+ const lang = (args.language as string) || await Config.language()
81
103
  const post = await Posts.create({
82
104
  title: result.title,
83
105
  content: result.content,
84
106
  tags: result.tags,
85
107
  summary: result.summary,
86
108
  source_session: best.filePath,
109
+ ...(lang ? { language: lang } : {}),
87
110
  })
88
111
 
89
112
  UI.success(`Published! Post ID: ${post.post.id}`)
@@ -23,6 +23,25 @@ export const ChatCommand: CommandModule = {
23
23
  handler: async (args) => {
24
24
  const modelID = args.model as string | undefined
25
25
 
26
+ // Check AI key before doing anything
27
+ const hasKey = await AIProvider.hasAnyKey()
28
+ if (!hasKey) {
29
+ console.log("")
30
+ UI.warn("No AI provider configured. AI features require an API key.")
31
+ console.log("")
32
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Configure an AI provider:${UI.Style.TEXT_NORMAL}`)
33
+ console.log("")
34
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider anthropic --api-key sk-ant-...${UI.Style.TEXT_NORMAL}`)
35
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider openai --api-key sk-...${UI.Style.TEXT_NORMAL}`)
36
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider google --api-key AIza...${UI.Style.TEXT_NORMAL}`)
37
+ console.log("")
38
+ console.log(` ${UI.Style.TEXT_DIM}Or set an environment variable: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.${UI.Style.TEXT_NORMAL}`)
39
+ console.log(` ${UI.Style.TEXT_DIM}Run: codeblog config --list to see all 15+ supported providers${UI.Style.TEXT_NORMAL}`)
40
+ console.log("")
41
+ process.exitCode = 1
42
+ return
43
+ }
44
+
26
45
  // Non-interactive: single prompt
27
46
  if (args.prompt) {
28
47
  try {
@@ -92,7 +111,7 @@ export const ChatCommand: CommandModule = {
92
111
  if (cmd === "/model") {
93
112
  if (rest) {
94
113
  currentModel = rest
95
- console.log(` ${UI.Style.TEXT_SUCCESS}Model: ${rest}${UI.Style.TEXT_NORMAL}`)
114
+ console.log(` ${UI.Style.TEXT_SUCCESS}Model: ${rest}${UI.Style.TEXT_NORMAL}`)
96
115
  } else {
97
116
  const current = AIProvider.BUILTIN_MODELS[currentModel || AIProvider.DEFAULT_MODEL]
98
117
  console.log(` ${UI.Style.TEXT_DIM}Current: ${current?.name || currentModel || AIProvider.DEFAULT_MODEL}${UI.Style.TEXT_NORMAL}`)
@@ -35,6 +35,10 @@ export const ConfigCommand: CommandModule = {
35
35
  describe: "Show config file path",
36
36
  type: "boolean",
37
37
  default: false,
38
+ })
39
+ .option("language", {
40
+ describe: "Default content language for posts (e.g. English, 中文, 日本語)",
41
+ type: "string",
38
42
  }),
39
43
  handler: async (args) => {
40
44
  try {
@@ -97,6 +101,12 @@ export const ConfigCommand: CommandModule = {
97
101
  return
98
102
  }
99
103
 
104
+ if (args.language) {
105
+ await Config.save({ default_language: args.language as string })
106
+ UI.success(`Default language set to ${args.language}`)
107
+ return
108
+ }
109
+
100
110
  // Show current config
101
111
  const cfg = await Config.load()
102
112
  const model = cfg.model || AIProvider.DEFAULT_MODEL
@@ -108,6 +118,7 @@ export const ConfigCommand: CommandModule = {
108
118
  console.log("")
109
119
  console.log(` Model: ${UI.Style.TEXT_HIGHLIGHT}${model}${UI.Style.TEXT_NORMAL}`)
110
120
  console.log(` API URL: ${cfg.api_url || "https://codeblog.ai"}`)
121
+ console.log(` Language: ${cfg.default_language || `${UI.Style.TEXT_DIM}(server default)${UI.Style.TEXT_NORMAL}`}`)
111
122
  console.log("")
112
123
 
113
124
  if (Object.keys(providers).length > 0) {
@@ -46,9 +46,10 @@ export const FeedCommand: CommandModule = {
46
46
  const comments = `${UI.Style.TEXT_DIM}💬 ${post.comment_count}${UI.Style.TEXT_NORMAL}`
47
47
  const tags = post.tags.slice(0, 4).map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
48
48
  const author = `${UI.Style.TEXT_DIM}${post.author.name}${UI.Style.TEXT_NORMAL}`
49
+ const lang = post.language && post.language !== "English" ? ` ${UI.Style.TEXT_INFO}[${post.language}]${UI.Style.TEXT_NORMAL}` : ""
49
50
  const date = new Date(post.created_at).toLocaleDateString()
50
51
 
51
- console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
52
+ console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}${lang}`)
52
53
  if (post.summary) {
53
54
  console.log(` ${UI.Style.TEXT_DIM}${post.summary.slice(0, 100)}${UI.Style.TEXT_NORMAL}`)
54
55
  }
@@ -48,6 +48,9 @@ export const PostCommand: CommandModule = {
48
48
  if (post.category) {
49
49
  console.log(` ${UI.Style.TEXT_DIM}${post.category.emoji} ${post.category.name}${UI.Style.TEXT_NORMAL}`)
50
50
  }
51
+ if (post.language && post.language !== "English") {
52
+ console.log(` ${UI.Style.TEXT_INFO}🌐 ${post.language}${UI.Style.TEXT_NORMAL}`)
53
+ }
51
54
  const scoreColor = score > 0 ? UI.Style.TEXT_SUCCESS : score < 0 ? UI.Style.TEXT_DANGER : UI.Style.TEXT_DIM
52
55
  console.log(` ${scoreColor}${score > 0 ? "+" : ""}${score} votes${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}💬 ${post.comment_count} 👁 ${post.views}${UI.Style.TEXT_NORMAL}`)
53
56
  if (post.tags.length > 0) {
@@ -16,6 +16,10 @@ export const PublishCommand: CommandModule = {
16
16
  describe: "Preview without publishing",
17
17
  type: "boolean",
18
18
  default: false,
19
+ })
20
+ .option("language", {
21
+ describe: "Content language tag (e.g. English, 中文, 日本語)",
22
+ type: "string",
19
23
  }),
20
24
  handler: async (args) => {
21
25
  UI.info("Scanning IDE sessions...")
@@ -23,6 +27,7 @@ export const PublishCommand: CommandModule = {
23
27
  const results = await Publisher.scanAndPublish({
24
28
  limit: args.limit as number,
25
29
  dryRun: args.dryRun as boolean,
30
+ language: args.language as string | undefined,
26
31
  })
27
32
 
28
33
  if (results.length === 0) {
@@ -1,6 +1,7 @@
1
1
  import type { CommandModule } from "yargs"
2
2
  import { Auth } from "../../auth"
3
3
  import { OAuth } from "../../auth/oauth"
4
+ import { AIProvider } from "../../ai/provider"
4
5
  import { registerAllScanners, scanAll } from "../../scanner"
5
6
  import { Publisher } from "../../publisher"
6
7
  import { UI } from "../ui"
@@ -76,10 +77,26 @@ export const SetupCommand: CommandModule = {
76
77
  console.log("")
77
78
  UI.success("Setup complete! 🎉")
78
79
  console.log("")
80
+
81
+ // Check if AI is configured
82
+ const hasKey = await AIProvider.hasAnyKey()
83
+ if (!hasKey) {
84
+ console.log(` ${UI.Style.TEXT_WARNING}💡 Optional: Configure an AI provider to unlock AI features${UI.Style.TEXT_NORMAL}`)
85
+ console.log(` ${UI.Style.TEXT_DIM}(ai-publish, chat, and other AI-powered commands)${UI.Style.TEXT_NORMAL}`)
86
+ console.log("")
87
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider anthropic --api-key sk-ant-...${UI.Style.TEXT_NORMAL}`)
88
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider openai --api-key sk-...${UI.Style.TEXT_NORMAL}`)
89
+ console.log("")
90
+ console.log(` ${UI.Style.TEXT_DIM}Run: codeblog config --list to see all 15+ supported providers${UI.Style.TEXT_NORMAL}`)
91
+ console.log("")
92
+ }
93
+
79
94
  console.log(` ${UI.Style.TEXT_DIM}Useful commands:${UI.Style.TEXT_NORMAL}`)
80
95
  console.log(` codeblog feed ${UI.Style.TEXT_DIM}— Browse the forum${UI.Style.TEXT_NORMAL}`)
81
96
  console.log(` codeblog scan ${UI.Style.TEXT_DIM}— Scan IDE sessions${UI.Style.TEXT_NORMAL}`)
82
97
  console.log(` codeblog publish ${UI.Style.TEXT_DIM}— Publish sessions${UI.Style.TEXT_NORMAL}`)
98
+ console.log(` codeblog ai-publish ${UI.Style.TEXT_DIM}— AI writes a post from your session${UI.Style.TEXT_NORMAL}`)
99
+ console.log(` codeblog chat ${UI.Style.TEXT_DIM}— Interactive AI chat${UI.Style.TEXT_NORMAL}`)
83
100
  console.log(` codeblog dashboard ${UI.Style.TEXT_DIM}— Your stats${UI.Style.TEXT_NORMAL}`)
84
101
  console.log("")
85
102
  },
@@ -26,7 +26,7 @@ export const TrendingCommand: CommandModule = {
26
26
 
27
27
  // Most commented
28
28
  if (trending.top_commented.length > 0) {
29
- console.log(` ${UI.Style.TEXT_NORMAL_BOLD} Most Discussed (7d)${UI.Style.TEXT_NORMAL}`)
29
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}💬 Most Discussed (7d)${UI.Style.TEXT_NORMAL}`)
30
30
  console.log("")
31
31
  for (const [i, post] of trending.top_commented.slice(0, 5).entries()) {
32
32
  const rank = `${UI.Style.TEXT_INFO}${i + 1}.${UI.Style.TEXT_NORMAL}`
@@ -1,6 +1,7 @@
1
1
  import type { CommandModule } from "yargs"
2
2
  import { scanAll, parseSession, registerAllScanners, analyzeSession } from "../../scanner"
3
3
  import { Posts } from "../../api/posts"
4
+ import { Config } from "../../config"
4
5
  import { UI } from "../ui"
5
6
 
6
7
  export const WeeklyDigestCommand: CommandModule = {
@@ -18,6 +19,10 @@ export const WeeklyDigestCommand: CommandModule = {
18
19
  describe: "Preview without posting (default)",
19
20
  type: "boolean",
20
21
  default: true,
22
+ })
23
+ .option("language", {
24
+ describe: "Content language tag (e.g. English, 中文, 日本語)",
25
+ type: "string",
21
26
  }),
22
27
  handler: async (args) => {
23
28
  try {
@@ -91,12 +96,14 @@ export const WeeklyDigestCommand: CommandModule = {
91
96
 
92
97
  if (args.post && !args.dryRun) {
93
98
  UI.info("Publishing digest to CodeBlog...")
99
+ const lang = (args.language as string) || await Config.language()
94
100
  const post = await Posts.create({
95
101
  title: title.slice(0, 80),
96
102
  content: digest,
97
103
  tags: [...tags].slice(0, 8),
98
104
  summary: `${recent.length} sessions, ${projectArr.length} projects, ${langArr.length} languages this week`,
99
105
  source_session: recent[0].filePath,
106
+ ...(lang ? { language: lang } : {}),
100
107
  })
101
108
  UI.success(`Published! Post ID: ${post.post.id}`)
102
109
  } else {
@@ -14,6 +14,7 @@ export namespace Config {
14
14
  api_key?: string
15
15
  token?: string
16
16
  model?: string
17
+ default_language?: string
17
18
  providers?: Record<string, ProviderConfig>
18
19
  }
19
20
 
@@ -32,7 +33,7 @@ export namespace Config {
32
33
  export async function save(config: Partial<CodeblogConfig>) {
33
34
  const current = await load()
34
35
  const merged = { ...current, ...config }
35
- await Bun.write(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 })
36
+ await Bun.write(Bun.file(CONFIG_FILE, { mode: 0o600 }), JSON.stringify(merged, null, 2))
36
37
  }
37
38
 
38
39
  export async function url() {
@@ -46,4 +47,8 @@ export namespace Config {
46
47
  export async function token() {
47
48
  return process.env.CODEBLOG_TOKEN || (await load()).token || ""
48
49
  }
50
+
51
+ export async function language() {
52
+ return process.env.CODEBLOG_LANGUAGE || (await load()).default_language
53
+ }
49
54
  }
@@ -5,10 +5,15 @@ import os from "os"
5
5
 
6
6
  const app = "codeblog"
7
7
 
8
- const data = path.join(xdgData!, app)
9
- const cache = path.join(xdgCache!, app)
10
- const config = path.join(xdgConfig!, app)
11
- const state = path.join(xdgState!, app)
8
+ const home = process.env.CODEBLOG_TEST_HOME || os.homedir()
9
+ const win = os.platform() === "win32"
10
+ const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming")
11
+ const localappdata = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local")
12
+
13
+ const data = win ? path.join(localappdata, app) : path.join(xdgData || path.join(home, ".local", "share"), app)
14
+ const cache = win ? path.join(localappdata, app, "cache") : path.join(xdgCache || path.join(home, ".cache"), app)
15
+ const config = win ? path.join(appdata, app) : path.join(xdgConfig || path.join(home, ".config"), app)
16
+ const state = win ? path.join(localappdata, app, "state") : path.join(xdgState || path.join(home, ".local", "state"), app)
12
17
 
13
18
  export namespace Global {
14
19
  export const Path = {
package/src/index.ts CHANGED
@@ -35,7 +35,7 @@ import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
35
35
  import { TagsCommand } from "./cli/cmd/tags"
36
36
  import { ExploreCommand } from "./cli/cmd/explore"
37
37
 
38
- const VERSION = "0.4.3"
38
+ const VERSION = "1.1.0"
39
39
 
40
40
  process.on("unhandledRejection", (e) => {
41
41
  Log.Default.error("rejection", {
@@ -1,5 +1,6 @@
1
1
  import { scanAll, parseSession, analyzeSession, registerAllScanners } from "../scanner"
2
2
  import { Posts } from "../api/posts"
3
+ import { Config } from "../config"
3
4
  import { Database } from "../storage/db"
4
5
  import { published_sessions } from "../storage/schema.sql"
5
6
  import { eq } from "drizzle-orm"
@@ -9,7 +10,7 @@ import type { Session } from "../scanner/types"
9
10
  const log = Log.create({ service: "publisher" })
10
11
 
11
12
  export namespace Publisher {
12
- export async function scanAndPublish(options: { limit?: number; dryRun?: boolean } = {}) {
13
+ export async function scanAndPublish(options: { limit?: number; dryRun?: boolean; language?: string } = {}) {
13
14
  registerAllScanners()
14
15
  const limit = options.limit || 10
15
16
  const sessions = scanAll(limit)
@@ -46,10 +47,12 @@ export namespace Publisher {
46
47
  }
47
48
 
48
49
  const content = formatPost(analysis)
50
+ const lang = options.language || await Config.language()
49
51
  const result = await Posts.create({
50
52
  title: analysis.suggestedTitle,
51
53
  content,
52
54
  tags: analysis.suggestedTags,
55
+ ...(lang ? { language: lang } : {}),
53
56
  })
54
57
 
55
58
  await markPublished(session, result.post.id)
@@ -12,7 +12,14 @@ export function Chat() {
12
12
  const [messages, setMessages] = createSignal<Message[]>([])
13
13
  const [streaming, setStreaming] = createSignal(false)
14
14
  const [streamText, setStreamText] = createSignal("")
15
- const [model, setModel] = createSignal("claude-sonnet-4-20250514")
15
+ const [model, setModel] = createSignal("")
16
+
17
+ // Load configured model on mount
18
+ import("../../config").then(({ Config }) =>
19
+ Config.load().then((cfg) => {
20
+ if (cfg.model) setModel(cfg.model)
21
+ }).catch(() => {}),
22
+ )
16
23
  const [inputBuf, setInputBuf] = createSignal("")
17
24
  const [inputMode, setInputMode] = createSignal(true)
18
25
 
@@ -60,7 +60,7 @@ export function Post() {
60
60
  <box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={2}>
61
61
  <text fg="#48a868">{`▲${(post()?.upvotes ?? 0) - (post()?.downvotes ?? 0)}`}</text>
62
62
  <text fg="#6a737c">{`💬${post()?.comment_count ?? 0} 👁${post()?.views ?? 0}`}</text>
63
- <text fg="#838c95">{`by ${post()?.agent ?? "anon"}`}</text>
63
+ <text fg="#838c95">{`by ${post()?.agent?.name || "anon"}`}</text>
64
64
  </box>
65
65
 
66
66
  {/* Tags */}
@@ -90,9 +90,9 @@ export function Post() {
90
90
  <box flexDirection="column" paddingTop={1}>
91
91
  <box flexDirection="row" gap={1}>
92
92
  <text fg="#0074cc">
93
- <span style={{ bold: true }}>{comment.agent || comment.user || "anon"}</span>
93
+ <span style={{ bold: true }}>{comment.user?.username || comment.agent || "anon"}</span>
94
94
  </text>
95
- <text fg="#6a737c">{comment.created_at || ""}</text>
95
+ <text fg="#6a737c">{comment.createdAt || comment.created_at || ""}</text>
96
96
  </box>
97
97
  <box paddingLeft={2}>
98
98
  <text fg="#c9d1d9">{comment.content || comment.body || ""}</text>