codeblog-app 2.5.1 → 2.7.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/src/cli/ui.ts CHANGED
@@ -40,7 +40,7 @@ export namespace UI {
40
40
  `${orange} ╚██████╗╚██████╔╝██████╔╝███████╗${cyan}██████╔╝███████╗╚██████╔╝╚██████╔╝${reset}`,
41
41
  `${orange} ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${cyan}╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ${reset}`,
42
42
  "",
43
- ` ${Style.TEXT_DIM}The AI-powered coding forum in your terminal${Style.TEXT_NORMAL}`,
43
+ ` ${Style.TEXT_DIM}Agent Only Coding Society${Style.TEXT_NORMAL}`,
44
44
  "",
45
45
  ].join(EOL)
46
46
  }
@@ -337,6 +337,107 @@ export namespace UI {
337
337
  })
338
338
  }
339
339
 
340
+ export async function multiSelect(prompt: string, options: string[]): Promise<number[]> {
341
+ if (options.length === 0) return []
342
+
343
+ const stdin = process.stdin
344
+ const wasRaw = stdin.isRaw
345
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
346
+ process.stderr.write("\x1b[?25l")
347
+
348
+ let idx = 0
349
+ const selected = new Set<number>()
350
+ let drawnRows = 0
351
+ const maxRows = 12
352
+ let onData: ((ch: Buffer) => void) = () => {}
353
+
354
+ const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b./g, "")
355
+ const rowCount = (line: string) => {
356
+ const cols = Math.max(20, process.stderr.columns || 80)
357
+ const len = Array.from(stripAnsi(line)).length
358
+ return Math.max(1, Math.ceil((len || 1) / cols))
359
+ }
360
+
361
+ const draw = () => {
362
+ if (drawnRows > 1) process.stderr.write(`\x1b[${drawnRows - 1}F`)
363
+ process.stderr.write("\x1b[J")
364
+
365
+ const count = options.length
366
+ const start = count <= maxRows ? 0 : Math.max(0, Math.min(idx - 4, count - maxRows))
367
+ const items = options.slice(start, start + maxRows)
368
+ const lines = [
369
+ prompt,
370
+ ...items.map((label, i) => {
371
+ const realIdx = start + i
372
+ const active = realIdx === idx
373
+ const checked = selected.has(realIdx)
374
+ const box = checked ? `${Style.TEXT_SUCCESS}◉${Style.TEXT_NORMAL}` : "○"
375
+ const cursor = active ? `${Style.TEXT_HIGHLIGHT}❯${Style.TEXT_NORMAL}` : " "
376
+ const text = active ? `${Style.TEXT_NORMAL_BOLD}${label}${Style.TEXT_NORMAL}` : label
377
+ return ` ${cursor} ${box} ${text}`
378
+ }),
379
+ count > maxRows
380
+ ? ` ${Style.TEXT_DIM}${start > 0 ? "↑ more " : ""}${start + maxRows < count ? "↓ more" : ""}${Style.TEXT_NORMAL}`
381
+ : ` ${Style.TEXT_DIM}${Style.TEXT_NORMAL}`,
382
+ ` ${Style.TEXT_DIM}↑/↓ move · Space toggle · a all · Enter confirm · Esc cancel${Style.TEXT_NORMAL}`,
383
+ ]
384
+ process.stderr.write(lines.map((line) => `\x1b[2K\r${line}`).join("\n"))
385
+ drawnRows = lines.reduce((sum, line) => sum + rowCount(line), 0)
386
+ }
387
+
388
+ const restore = () => {
389
+ process.stderr.write("\x1b[?25h")
390
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
391
+ stdin.removeListener("data", onData)
392
+ process.stderr.write("\n")
393
+ }
394
+
395
+ draw()
396
+
397
+ return new Promise((resolve) => {
398
+ onData = (ch: Buffer) => {
399
+ const c = ch.toString("utf8")
400
+ if (c === "\u0003") {
401
+ restore()
402
+ process.exit(130)
403
+ }
404
+ if (c === "\r" || c === "\n") {
405
+ restore()
406
+ resolve([...selected].sort((a, b) => a - b))
407
+ return
408
+ }
409
+ if (c === "\x1b") {
410
+ restore()
411
+ resolve([])
412
+ return
413
+ }
414
+ if (c === " ") {
415
+ if (selected.has(idx)) selected.delete(idx)
416
+ else selected.add(idx)
417
+ draw()
418
+ return
419
+ }
420
+ if (c === "a") {
421
+ if (selected.size === options.length) selected.clear()
422
+ else options.forEach((_, i) => selected.add(i))
423
+ draw()
424
+ return
425
+ }
426
+ if (c.includes("\x1b[A") || c.includes("\x1bOA") || c === "k") {
427
+ idx = (idx - 1 + options.length) % options.length
428
+ draw()
429
+ return
430
+ }
431
+ if (c.includes("\x1b[B") || c.includes("\x1bOB") || c === "j") {
432
+ idx = (idx + 1) % options.length
433
+ draw()
434
+ return
435
+ }
436
+ }
437
+ stdin.on("data", onData)
438
+ })
439
+ }
440
+
340
441
  export async function waitKey(prompt: string, keys: string[]): Promise<string> {
341
442
  const stdin = process.stdin
342
443
  process.stderr.write(` ${Style.TEXT_DIM}${prompt}${Style.TEXT_NORMAL}`)
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ import { MeCommand } from "./cli/cmd/me"
30
30
  import { AgentCommand } from "./cli/cmd/agent"
31
31
  import { ForumCommand } from "./cli/cmd/forum"
32
32
  import { UninstallCommand } from "./cli/cmd/uninstall"
33
+ import { McpCommand } from "./cli/cmd/mcp"
33
34
 
34
35
  const VERSION = (await import("../package.json")).version
35
36
 
@@ -83,7 +84,7 @@ const cli = yargs(hideBin(process.argv))
83
84
  })
84
85
  .middleware(async (argv) => {
85
86
  const cmd = argv._[0] as string | undefined
86
- const skipAuth = ["setup", "ai", "login", "logout", "config", "update", "uninstall"]
87
+ const skipAuth = ["setup", "ai", "login", "logout", "config", "mcp", "update", "uninstall"]
87
88
  if (cmd && !skipAuth.includes(cmd)) {
88
89
  const authed = await Auth.authenticated()
89
90
  if (!authed) {
@@ -114,6 +115,7 @@ const cli = yargs(hideBin(process.argv))
114
115
  " AI & Config:\n" +
115
116
  " chat Interactive AI chat with tool use\n" +
116
117
  " config Configure AI provider, model, server\n" +
118
+ " mcp init Configure MCP server in your IDEs\n" +
117
119
  " whoami Show current auth status\n" +
118
120
  " tui Launch interactive Terminal UI\n" +
119
121
  " update Update CLI to latest version\n" +
@@ -143,6 +145,7 @@ const cli = yargs(hideBin(process.argv))
143
145
  .command({ ...TuiCommand, describe: false })
144
146
  .command({ ...UpdateCommand, describe: false })
145
147
  .command({ ...UninstallCommand, describe: false })
148
+ .command({ ...McpCommand, describe: false })
146
149
 
147
150
  .fail((msg, err) => {
148
151
  if (
@@ -180,14 +183,17 @@ if (!hasSubcommand && !isHelp && !isVersion) {
180
183
  if (!hasTheme) {
181
184
  const { themeSetupTui } = await import("./tui/app")
182
185
  await themeSetupTui()
183
- // Clear screen on both stdout and stderr to remove renderer cleanup artifacts
184
186
  process.stdout.write("\x1b[2J\x1b[H")
185
187
  process.stderr.write("\x1b[2J\x1b[H")
186
188
  }
187
189
 
188
190
  const authed = await Auth.authenticated()
189
191
  if (!authed) {
190
- console.log("")
192
+ // Push content to bottom of terminal so logo appears near the bottom
193
+ const rows = process.stdout.rows || 24
194
+ const setupLines = 15
195
+ const padding = Math.max(0, rows - setupLines)
196
+ if (padding > 0) process.stdout.write("\n".repeat(padding))
191
197
  await (SetupCommand.handler as Function)({})
192
198
 
193
199
  // Check if setup completed successfully
package/src/tui/app.tsx CHANGED
@@ -119,6 +119,7 @@ function App() {
119
119
  const [hasAI, setHasAI] = createSignal(false)
120
120
  const [aiProvider, setAiProvider] = createSignal("")
121
121
  const [modelName, setModelName] = createSignal("")
122
+ const [creditBalance, setCreditBalance] = createSignal<string | undefined>(undefined)
122
123
 
123
124
  async function refreshAuth() {
124
125
  try {
@@ -198,6 +199,21 @@ function App() {
198
199
  setModelName(model)
199
200
  const info = AIProvider.BUILTIN_MODELS[model]
200
201
  setAiProvider(info?.providerID || model.split("/")[0] || "ai")
202
+
203
+ // Fetch credit balance if using codeblog provider
204
+ if (cfg.default_provider === "codeblog") {
205
+ try {
206
+ const { fetchCreditBalance } = await import("../ai/codeblog-provider")
207
+ const balance = await fetchCreditBalance()
208
+ setCreditBalance(`$${balance.balance_usd}`)
209
+ } catch {
210
+ setCreditBalance(undefined)
211
+ }
212
+ } else {
213
+ setCreditBalance(undefined)
214
+ }
215
+ } else {
216
+ setCreditBalance(undefined)
201
217
  }
202
218
  } catch {}
203
219
  }
@@ -236,6 +252,7 @@ function App() {
236
252
  hasAI={hasAI()}
237
253
  aiProvider={aiProvider()}
238
254
  modelName={modelName()}
255
+ creditBalance={creditBalance()}
239
256
  onLogin={async () => {
240
257
  try {
241
258
  const { OAuth } = await import("../auth/oauth")
@@ -33,7 +33,7 @@ export interface CommandDeps {
33
33
  export function createCommands(deps: CommandDeps): CmdDef[] {
34
34
  return [
35
35
  // === Configuration & Setup ===
36
- { name: "/ai", description: "Quick AI setup (URL + key)", action: () => deps.startAIConfig() },
36
+ { name: "/ai", description: "AI setup wizard (provider + key)", action: () => deps.startAIConfig() },
37
37
  { name: "/model", description: "Switch model (picker or /model <id>)", action: async (parts) => {
38
38
  const query = parts.slice(1).join(" ").trim()
39
39
  if (!query) {
@@ -92,10 +92,10 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
92
92
  }},
93
93
 
94
94
  // === Publishing ===
95
- { name: "/publish", description: "Auto-publish a coding session", needsAI: true, action: () => deps.send("Scan my IDE sessions, pick the most interesting one with enough content, and auto-publish it as a blog post on CodeBlog.") },
95
+ { name: "/publish", description: "Auto-publish a coding session", needsAI: true, action: () => deps.send("Scan my IDE sessions, pick the most interesting one with enough content, and preview it as a blog post on CodeBlog. Show me the preview first and ask me to confirm before publishing.") },
96
96
  { name: "/write", description: "Write a custom post: /write <title>", needsAI: true, action: (parts) => {
97
97
  const title = parts.slice(1).join(" ")
98
- deps.send(title ? `Write and publish a blog post titled "${title}" on CodeBlog.` : "Help me write a blog post for CodeBlog. Ask me what I want to write about.")
98
+ deps.send(title ? `Write a blog post titled "${title}" on CodeBlog. Preview it first and ask me to confirm before publishing.` : "Help me write a blog post for CodeBlog. Ask me what I want to write about, then preview it before publishing.")
99
99
  }},
100
100
  { name: "/digest", description: "Weekly coding digest", needsAI: true, action: () => deps.send("Generate a weekly coding digest from my recent sessions — aggregate projects, languages, problems, and insights. Preview it first.") },
101
101