codeblog-app 1.0.0 → 1.2.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": "1.0.0",
4
+ "version": "1.2.0",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -53,7 +53,7 @@ export namespace AIProvider {
53
53
  // Provider env key mapping
54
54
  // ---------------------------------------------------------------------------
55
55
  const PROVIDER_ENV: Record<string, string[]> = {
56
- anthropic: ["ANTHROPIC_API_KEY"],
56
+ anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
57
57
  openai: ["OPENAI_API_KEY"],
58
58
  google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_API_KEY"],
59
59
  "amazon-bedrock": ["AWS_ACCESS_KEY_ID"],
@@ -70,6 +70,22 @@ export namespace AIProvider {
70
70
  "openai-compatible": ["OPENAI_COMPATIBLE_API_KEY"],
71
71
  }
72
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // Provider base URL env mapping (for third-party API proxies)
75
+ // ---------------------------------------------------------------------------
76
+ const PROVIDER_BASE_URL_ENV: Record<string, string[]> = {
77
+ anthropic: ["ANTHROPIC_BASE_URL"],
78
+ openai: ["OPENAI_BASE_URL", "OPENAI_API_BASE"],
79
+ google: ["GOOGLE_API_BASE_URL"],
80
+ azure: ["AZURE_OPENAI_BASE_URL"],
81
+ xai: ["XAI_BASE_URL"],
82
+ mistral: ["MISTRAL_BASE_URL"],
83
+ groq: ["GROQ_BASE_URL"],
84
+ deepinfra: ["DEEPINFRA_BASE_URL"],
85
+ openrouter: ["OPENROUTER_BASE_URL"],
86
+ "openai-compatible": ["OPENAI_COMPATIBLE_BASE_URL"],
87
+ }
88
+
73
89
  // ---------------------------------------------------------------------------
74
90
  // Provider → npm package mapping
75
91
  // ---------------------------------------------------------------------------
@@ -177,6 +193,18 @@ export namespace AIProvider {
177
193
  return cfg.providers?.[providerID]?.api_key
178
194
  }
179
195
 
196
+ // ---------------------------------------------------------------------------
197
+ // Get base URL for a provider (env var or config)
198
+ // ---------------------------------------------------------------------------
199
+ export async function getBaseUrl(providerID: string): Promise<string | undefined> {
200
+ const envKeys = PROVIDER_BASE_URL_ENV[providerID] || []
201
+ for (const key of envKeys) {
202
+ if (process.env[key]) return process.env[key]
203
+ }
204
+ const cfg = await Config.load()
205
+ return cfg.providers?.[providerID]?.base_url
206
+ }
207
+
180
208
  // ---------------------------------------------------------------------------
181
209
  // List all available providers with their models
182
210
  // ---------------------------------------------------------------------------
@@ -223,7 +251,8 @@ export namespace AIProvider {
223
251
  if (builtin) {
224
252
  const apiKey = await getApiKey(builtin.providerID)
225
253
  if (!apiKey) throw noKeyError(builtin.providerID)
226
- return getLanguageModel(builtin.providerID, id, apiKey)
254
+ const base = await getBaseUrl(builtin.providerID)
255
+ return getLanguageModel(builtin.providerID, id, apiKey, undefined, base)
227
256
  }
228
257
 
229
258
  // Try models.dev
@@ -234,7 +263,8 @@ export namespace AIProvider {
234
263
  const apiKey = await getApiKey(providerID)
235
264
  if (!apiKey) throw noKeyError(providerID)
236
265
  const npm = p.models[id].provider?.npm || p.npm || "@ai-sdk/openai-compatible"
237
- return getLanguageModel(providerID, id, apiKey, npm, p.api)
266
+ const base = await getBaseUrl(providerID)
267
+ return getLanguageModel(providerID, id, apiKey, npm, base || p.api)
238
268
  }
239
269
  }
240
270
 
@@ -244,7 +274,8 @@ export namespace AIProvider {
244
274
  const mid = rest.join("/")
245
275
  const apiKey = await getApiKey(providerID)
246
276
  if (!apiKey) throw noKeyError(providerID)
247
- return getLanguageModel(providerID, mid, apiKey)
277
+ const base = await getBaseUrl(providerID)
278
+ return getLanguageModel(providerID, mid, apiKey, undefined, base)
248
279
  }
249
280
 
250
281
  throw new Error(`Unknown model: ${id}. Run: codeblog config --list`)
@@ -289,6 +320,25 @@ export namespace AIProvider {
289
320
  return cfg.model
290
321
  }
291
322
 
323
+ // ---------------------------------------------------------------------------
324
+ // Check if any AI provider has a key configured
325
+ // ---------------------------------------------------------------------------
326
+ export async function hasAnyKey(): Promise<boolean> {
327
+ // Check env vars
328
+ for (const providerID of Object.keys(PROVIDER_ENV)) {
329
+ const key = await getApiKey(providerID)
330
+ if (key) return true
331
+ }
332
+ // Check config file (covers third-party providers not in PROVIDER_ENV)
333
+ const cfg = await Config.load()
334
+ if (cfg.providers) {
335
+ for (const p of Object.values(cfg.providers)) {
336
+ if (p.api_key) return true
337
+ }
338
+ }
339
+ return false
340
+ }
341
+
292
342
  // ---------------------------------------------------------------------------
293
343
  // List available models with key status (for codeblog config --list)
294
344
  // ---------------------------------------------------------------------------
@@ -1,5 +1,6 @@
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"
4
5
  import { Config } from "../../config"
5
6
  import { scanAll, parseSession, registerAllScanners } from "../../scanner"
@@ -32,6 +33,21 @@ export const AIPublishCommand: CommandModule = {
32
33
  }),
33
34
  handler: async (args) => {
34
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
+
35
51
  UI.info("Scanning IDE sessions...")
36
52
  registerAllScanners()
37
53
  const sessions = scanAll(args.limit as number)
@@ -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 {
@@ -36,6 +36,10 @@ export const ConfigCommand: CommandModule = {
36
36
  type: "boolean",
37
37
  default: false,
38
38
  })
39
+ .option("base-url", {
40
+ describe: "Custom base URL for the provider (for third-party API proxies)",
41
+ type: "string",
42
+ })
39
43
  .option("language", {
40
44
  describe: "Default content language for posts (e.g. English, 中文, 日本語)",
41
45
  type: "string",
@@ -80,12 +84,18 @@ export const ConfigCommand: CommandModule = {
80
84
  return
81
85
  }
82
86
 
83
- if (args.provider && args.apiKey) {
87
+ if (args.provider && (args.apiKey || args.baseUrl)) {
84
88
  const cfg = await Config.load()
85
89
  const providers = cfg.providers || {}
86
- providers[args.provider as string] = { ...providers[args.provider as string], api_key: args.apiKey as string }
90
+ const existing = providers[args.provider as string] || {} as Config.ProviderConfig
91
+ if (args.apiKey) existing.api_key = args.apiKey as string
92
+ if (args.baseUrl) existing.base_url = args.baseUrl as string
93
+ providers[args.provider as string] = existing
87
94
  await Config.save({ providers })
88
- UI.success(`${args.provider} API key saved`)
95
+ const parts: string[] = []
96
+ if (args.apiKey) parts.push("API key")
97
+ if (args.baseUrl) parts.push(`base URL (${args.baseUrl})`)
98
+ UI.success(`${args.provider} ${parts.join(" + ")} saved`)
89
99
  return
90
100
  }
91
101
 
@@ -125,12 +135,14 @@ export const ConfigCommand: CommandModule = {
125
135
  console.log(` ${UI.Style.TEXT_NORMAL_BOLD}AI Providers${UI.Style.TEXT_NORMAL}`)
126
136
  for (const [id, p] of Object.entries(providers)) {
127
137
  const masked = p.api_key ? p.api_key.slice(0, 8) + "..." : "not set"
128
- console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${id}: ${UI.Style.TEXT_DIM}${masked}${UI.Style.TEXT_NORMAL}`)
138
+ const url = p.base_url ? `${p.base_url}` : ""
139
+ console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${id}: ${UI.Style.TEXT_DIM}${masked}${url}${UI.Style.TEXT_NORMAL}`)
129
140
  }
130
141
  } else {
131
142
  console.log(` ${UI.Style.TEXT_DIM}No AI providers configured.${UI.Style.TEXT_NORMAL}`)
132
143
  console.log(` ${UI.Style.TEXT_DIM}Set one: codeblog config --provider anthropic --api-key sk-...${UI.Style.TEXT_NORMAL}`)
133
- console.log(` ${UI.Style.TEXT_DIM}Or use env: ANTHROPIC_API_KEY=sk-...${UI.Style.TEXT_NORMAL}`)
144
+ console.log(` ${UI.Style.TEXT_DIM}Third-party proxy: codeblog config --provider anthropic --api-key sk-... --base-url https://proxy.example.com${UI.Style.TEXT_NORMAL}`)
145
+ console.log(` ${UI.Style.TEXT_DIM}Or use env: ANTHROPIC_API_KEY + ANTHROPIC_BASE_URL${UI.Style.TEXT_NORMAL}`)
134
146
  }
135
147
  console.log("")
136
148
  } catch (err) {
@@ -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
  },
@@ -6,10 +6,14 @@ import os from "os"
6
6
  const app = "codeblog"
7
7
 
8
8
  const home = process.env.CODEBLOG_TEST_HOME || os.homedir()
9
- const data = path.join(xdgData || path.join(home, ".local", "share"), app)
10
- const cache = path.join(xdgCache || path.join(home, ".cache"), app)
11
- const config = path.join(xdgConfig || path.join(home, ".config"), app)
12
- const state = path.join(xdgState || path.join(home, ".local", "state"), app)
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)
13
17
 
14
18
  export namespace Global {
15
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.2.0"
39
39
 
40
40
  process.on("unhandledRejection", (e) => {
41
41
  Log.Default.error("rejection", {