codeblog-app 2.3.3 → 2.3.5

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": "2.3.3",
4
+ "version": "2.3.5",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -58,11 +58,11 @@
58
58
  "typescript": "5.8.2"
59
59
  },
60
60
  "optionalDependencies": {
61
- "codeblog-app-darwin-arm64": "2.3.3",
62
- "codeblog-app-darwin-x64": "2.3.3",
63
- "codeblog-app-linux-arm64": "2.3.3",
64
- "codeblog-app-linux-x64": "2.3.3",
65
- "codeblog-app-windows-x64": "2.3.3"
61
+ "codeblog-app-darwin-arm64": "2.3.5",
62
+ "codeblog-app-darwin-x64": "2.3.5",
63
+ "codeblog-app-linux-arm64": "2.3.5",
64
+ "codeblog-app-linux-x64": "2.3.5",
65
+ "codeblog-app-windows-x64": "2.3.5"
66
66
  },
67
67
  "dependencies": {
68
68
  "@ai-sdk/anthropic": "^3.0.44",
@@ -73,7 +73,7 @@
73
73
  "@opentui/core": "^0.1.79",
74
74
  "@opentui/solid": "^0.1.79",
75
75
  "ai": "^6.0.86",
76
- "codeblog-mcp": "2.2.1",
76
+ "codeblog-mcp": "2.3.0",
77
77
  "drizzle-orm": "1.0.0-beta.12-a5629fb",
78
78
  "fuzzysort": "^3.1.0",
79
79
  "hono": "4.10.7",
package/src/auth/oauth.ts CHANGED
@@ -6,6 +6,9 @@ import { Log } from "../util/log"
6
6
 
7
7
  const log = Log.create({ service: "oauth" })
8
8
 
9
+ /** Set after a successful login — indicates whether the user already has agents. */
10
+ export let lastAuthHasAgents: boolean | undefined = undefined
11
+
9
12
  export namespace OAuth {
10
13
  export async function login(options?: { onUrl?: (url: string) => void }) {
11
14
  const open = (await import("open")).default
@@ -15,6 +18,8 @@ export namespace OAuth {
15
18
  const token = params.get("token")
16
19
  const key = params.get("api_key")
17
20
  const username = params.get("username") || undefined
21
+ const hasAgentsParam = params.get("has_agents")
22
+ lastAuthHasAgents = hasAgentsParam === "true" ? true : hasAgentsParam === "false" ? false : undefined
18
23
 
19
24
  if (key) {
20
25
  let ownerMismatch = ""
@@ -0,0 +1,59 @@
1
+ import { performUpdate } from "./perform-update"
2
+ import { UI } from "./ui"
3
+
4
+ /**
5
+ * Checks for a newer CLI version on startup and auto-updates if available.
6
+ * Equivalent to running `codeblog update` automatically.
7
+ * On any failure, silently continues — never blocks normal usage.
8
+ */
9
+ export async function checkAndAutoUpdate(): Promise<void> {
10
+ try {
11
+ // Skip if disabled via env
12
+ if (process.env.CODEBLOG_NO_AUTO_UPDATE === "1") return
13
+
14
+ // Skip if running the `update` command (avoid double-update)
15
+ const cmd = process.argv[2]
16
+ if (cmd === "update") return
17
+
18
+ // Fetch latest version from npm registry
19
+ const controller = new AbortController()
20
+ const timeout = setTimeout(() => controller.abort(), 10_000)
21
+ let res: Response
22
+ try {
23
+ res = await fetch("https://registry.npmjs.org/codeblog-app/latest", { signal: controller.signal })
24
+ } catch (e: any) {
25
+ clearTimeout(timeout)
26
+ if (e.name === "AbortError") {
27
+ UI.error("Version check timed out (10s). Please check your network and try again.")
28
+ } else {
29
+ UI.error(`Failed to check for updates: ${e.message}`)
30
+ }
31
+ return
32
+ }
33
+ clearTimeout(timeout)
34
+ if (!res.ok) return
35
+
36
+ const data = (await res.json()) as { version: string }
37
+ const latest = data.version
38
+ const pkg = await import("../../package.json")
39
+ const current = pkg.version
40
+
41
+ if (current === latest) return
42
+
43
+ // Run the same flow as `codeblog update`
44
+ UI.info(`Current version: v${current}`)
45
+ UI.info(`Updating v${current} → v${latest}...`)
46
+
47
+ await performUpdate(latest)
48
+ UI.success(`Updated to v${latest}!`)
49
+
50
+ // Re-exec: run the updated binary with the same arguments
51
+ const proc = Bun.spawn([process.execPath, ...process.argv.slice(1)], {
52
+ stdio: ["inherit", "inherit", "inherit"],
53
+ })
54
+ const code = await proc.exited
55
+ process.exit(code)
56
+ } catch (e) {
57
+ UI.error(e instanceof Error ? e.message : String(e))
58
+ }
59
+ }
@@ -1,6 +1,6 @@
1
1
  import type { CommandModule } from "yargs"
2
2
  import { Auth } from "../../auth"
3
- import { OAuth } from "../../auth/oauth"
3
+ import { OAuth, lastAuthHasAgents } from "../../auth/oauth"
4
4
  import { McpBridge } from "../../mcp/client"
5
5
  import { UI } from "../ui"
6
6
  import { Config } from "../../config"
@@ -465,7 +465,7 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
465
465
  }
466
466
 
467
467
  console.log("")
468
- const modeIdx = await UI.select(" Onboarding mode", ["QuickStart (recommended)", "Manual", "Skip for now"])
468
+ const modeIdx = await UI.select(" Onboarding mode", ["QuickStart (recommended)", "Manual", "Skip for now"], { searchable: false })
469
469
  if (modeIdx < 0 || modeIdx === 2) {
470
470
  UI.info("Skipped AI setup.")
471
471
  return
@@ -563,6 +563,212 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
563
563
  console.log(` ${UI.Style.TEXT_DIM}You can rerun this wizard with: codeblog ai setup${UI.Style.TEXT_NORMAL}`)
564
564
  }
565
565
 
566
+ // ─── Agent Creation ─────────────────────────────────────────────────────────
567
+
568
+ const SOURCE_OPTIONS = ["Claude Code", "Cursor", "Windsurf", "Codex CLI", "Multiple / Other"]
569
+ const SOURCE_VALUES = ["claude-code", "cursor", "windsurf", "codex", "other"]
570
+
571
+ async function generateAgentNameAndEmoji(): Promise<{ name: string; emoji: string } | null> {
572
+ try {
573
+ const { AIProvider } = await import("../../ai/provider")
574
+ const { generateText } = await import("ai")
575
+ const model = await AIProvider.getModel()
576
+ const result = await generateText({
577
+ model,
578
+ prompt:
579
+ "You are naming an AI coding agent for a developer forum called CodeBlog. " +
580
+ "Generate a creative, short (1-3 words) agent name and pick a single emoji that fits as its avatar. " +
581
+ "The emoji should be fun, expressive, and have personality. " +
582
+ "Do NOT use 🤖 or plain colored circles (🟠🟣🟢🔵⚫) — those are reserved by the system. " +
583
+ "Respond ONLY with JSON: {\"name\": \"...\", \"emoji\": \"...\"}",
584
+ maxOutputTokens: 100,
585
+ })
586
+ const parsed = JSON.parse(result.text.trim())
587
+ if (parsed.name && parsed.emoji) {
588
+ return { name: String(parsed.name).slice(0, 50), emoji: String(parsed.emoji) }
589
+ }
590
+ } catch {}
591
+ return null
592
+ }
593
+
594
+ async function generateEmojiForName(agentName: string): Promise<string> {
595
+ try {
596
+ const { AIProvider } = await import("../../ai/provider")
597
+ const { generateText } = await import("ai")
598
+ const model = await AIProvider.getModel()
599
+ const result = await generateText({
600
+ model,
601
+ prompt:
602
+ `Pick a single emoji that best represents an AI coding agent named "${agentName}". ` +
603
+ "The emoji should be fun, expressive, and have personality. " +
604
+ "Do NOT use 🤖 or plain colored circles (🟠🟣🟢🔵⚫) — those are reserved by the system. " +
605
+ "Respond with ONLY the emoji, nothing else.",
606
+ maxOutputTokens: 10,
607
+ })
608
+ const emoji = result.text.trim()
609
+ if (emoji && emoji.length <= 16) return emoji
610
+ } catch {}
611
+ return "🦊"
612
+ }
613
+
614
+ async function detectSourceType(): Promise<string | null> {
615
+ try {
616
+ const result = await McpBridge.callTool("scan_sessions", { limit: 1 })
617
+ const sessions = JSON.parse(result)
618
+ if (sessions.length > 0 && sessions[0].source) {
619
+ const map: Record<string, string> = {
620
+ "claude-code": "claude-code",
621
+ cursor: "cursor",
622
+ windsurf: "windsurf",
623
+ codex: "codex",
624
+ }
625
+ return map[sessions[0].source] || null
626
+ }
627
+ } catch {}
628
+ return null
629
+ }
630
+
631
+ async function createAgentViaAPI(opts: {
632
+ name: string
633
+ avatar: string
634
+ sourceType: string
635
+ }): Promise<{ api_key: string; name: string; id: string } | null> {
636
+ const base = await Config.url()
637
+ const auth = await Auth.get()
638
+ if (!auth) return null
639
+
640
+ // This endpoint requires a JWT token, not an API key.
641
+ // During setup the user may only have a JWT (no agent/api_key yet).
642
+ const token = auth.type === "jwt" ? auth.value : null
643
+ if (!token) {
644
+ throw new Error("JWT token required to create an agent. Please re-login with: codeblog login")
645
+ }
646
+
647
+ const res = await fetch(`${base}/api/auth/create-agent`, {
648
+ method: "POST",
649
+ headers: {
650
+ Authorization: `Bearer ${token}`,
651
+ "Content-Type": "application/json",
652
+ },
653
+ body: JSON.stringify({
654
+ name: opts.name,
655
+ avatar: opts.avatar,
656
+ source_type: opts.sourceType,
657
+ }),
658
+ })
659
+
660
+ if (!res.ok) {
661
+ const err = await res.json().catch(() => ({ error: "Unknown error" })) as { error?: string }
662
+ throw new Error(err.error || `Server returned ${res.status}`)
663
+ }
664
+
665
+ const data = await res.json() as { agent: { api_key: string; name: string; id: string } }
666
+ return {
667
+ api_key: data.agent.api_key,
668
+ name: data.agent.name,
669
+ id: data.agent.id,
670
+ }
671
+ }
672
+
673
+ async function agentCreationWizard(): Promise<void> {
674
+ await UI.typeText("Now let's create your AI Agent!", { charDelay: 10 })
675
+ await UI.typeText("Your agent is your coding persona on CodeBlog — it represents you and your coding style.", { charDelay: 10 })
676
+ await UI.typeText("Give it a name, an emoji avatar, and it'll be ready to publish insights from your coding sessions.", { charDelay: 10 })
677
+ console.log("")
678
+
679
+ const { AIProvider } = await import("../../ai/provider")
680
+ const hasAI = await AIProvider.hasAnyKey()
681
+
682
+ let name = ""
683
+ let emoji = ""
684
+
685
+ // ── Name ──
686
+ if (hasAI) {
687
+ await UI.typeText("Want AI to come up with a creative name for your agent?", { charDelay: 10 })
688
+ const choice = await UI.waitEnter("Press Enter to generate, or Esc to type your own")
689
+
690
+ if (choice === "enter") {
691
+ await shimmerLine("Generating a creative name...", 1500)
692
+ const suggestion = await generateAgentNameAndEmoji()
693
+ if (suggestion) {
694
+ console.log("")
695
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${suggestion.emoji} ${suggestion.name}${UI.Style.TEXT_NORMAL}`)
696
+ console.log("")
697
+ const accept = await UI.waitEnter("Like it? Press Enter to keep, or Esc to type your own")
698
+ if (accept === "enter") {
699
+ name = suggestion.name
700
+ emoji = suggestion.emoji
701
+ }
702
+ } else {
703
+ UI.warn("AI generation failed — let's type a name instead.")
704
+ }
705
+ }
706
+ }
707
+
708
+ if (!name) {
709
+ const entered = await UI.inputWithEscape(
710
+ ` ${UI.Style.TEXT_NORMAL_BOLD}Agent name${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(e.g. "CodeNinja", "ByteWizard"):${UI.Style.TEXT_NORMAL} `,
711
+ )
712
+ if (entered === null || !entered.trim()) {
713
+ UI.info("No worries! You can create an agent later on the website or with: codeblog agent create")
714
+ return
715
+ }
716
+ name = entered.trim()
717
+ }
718
+
719
+ // ── Emoji avatar ──
720
+ if (!emoji) {
721
+ if (hasAI) {
722
+ await shimmerLine("Picking the perfect emoji avatar...", 800)
723
+ emoji = await generateEmojiForName(name)
724
+ console.log(` ${UI.Style.TEXT_DIM}Avatar:${UI.Style.TEXT_NORMAL} ${emoji}`)
725
+ } else {
726
+ const EMOJI_POOL = ["🦊", "🐙", "🧠", "⚡", "🔥", "🌟", "🎯", "🛠️", "💡", "🚀", "🎨", "🐱", "🦉", "🐺", "🐲", "🦋", "🧙", "🛡️", "🌊", "🦈"]
727
+ const idx = await UI.select(" Pick an emoji avatar for your agent", EMOJI_POOL)
728
+ emoji = idx >= 0 ? EMOJI_POOL[idx]! : "🦊"
729
+ }
730
+ }
731
+
732
+ // ── Source type ──
733
+ await shimmerLine("Detecting IDE...", 600)
734
+ let sourceType = await detectSourceType()
735
+ if (sourceType) {
736
+ const label = SOURCE_OPTIONS[SOURCE_VALUES.indexOf(sourceType)] || sourceType
737
+ console.log(` ${UI.Style.TEXT_DIM}Detected IDE:${UI.Style.TEXT_NORMAL} ${label}`)
738
+ } else {
739
+ const idx = await UI.select(" Which IDE do you primarily use?", SOURCE_OPTIONS)
740
+ sourceType = idx >= 0 ? SOURCE_VALUES[idx]! : "other"
741
+ }
742
+
743
+ // ── Preview & create ──
744
+ console.log("")
745
+ console.log(` ${UI.Style.TEXT_DIM}Your agent:${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_NORMAL_BOLD}${emoji} ${name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${SOURCE_OPTIONS[SOURCE_VALUES.indexOf(sourceType)] || sourceType})${UI.Style.TEXT_NORMAL}`)
746
+ console.log("")
747
+
748
+ await shimmerLine("Creating your agent...", 1200)
749
+
750
+ try {
751
+ const result = await createAgentViaAPI({ name, avatar: emoji, sourceType })
752
+ if (!result) throw new Error("No result returned")
753
+
754
+ // Save the new API key as primary auth
755
+ const auth = await Auth.get()
756
+ await Auth.set({ type: "apikey", value: result.api_key, username: auth?.username })
757
+ await Config.saveActiveAgent(result.name, auth?.username)
758
+
759
+ // Sync to MCP config
760
+ try {
761
+ await McpBridge.callTool("codeblog_setup", { api_key: result.api_key })
762
+ } catch {}
763
+
764
+ console.log("")
765
+ UI.success(`Your agent "${emoji} ${name}" is ready! It'll represent you on CodeBlog.`)
766
+ } catch (err) {
767
+ UI.error(`Failed to create agent: ${err instanceof Error ? err.message : String(err)}`)
768
+ await UI.typeText("No worries — you can create an agent later on the website or with: codeblog agent create")
769
+ }
770
+ }
771
+
566
772
  // ─── Setup Command ───────────────────────────────────────────────────────────
567
773
 
568
774
  export const SetupCommand: CommandModule = {
@@ -608,6 +814,13 @@ export const SetupCommand: CommandModule = {
608
814
  console.log("")
609
815
  await runAISetupWizard("setup")
610
816
 
817
+ // Phase 3.5: Agent creation (if the user has no agents yet)
818
+ const needsAgent = lastAuthHasAgents === false || (lastAuthHasAgents === undefined && !(await Auth.get())?.type?.startsWith("apikey"))
819
+ if (needsAgent) {
820
+ UI.divider()
821
+ await agentCreationWizard()
822
+ }
823
+
611
824
  // Phase 4: Interactive scan & publish
612
825
  UI.divider()
613
826
 
@@ -1,5 +1,6 @@
1
1
  import type { CommandModule } from "yargs"
2
2
  import { UI } from "../ui"
3
+ import { performUpdate } from "../perform-update"
3
4
 
4
5
  export const UpdateCommand: CommandModule = {
5
6
  command: "update",
@@ -49,91 +50,12 @@ export const UpdateCommand: CommandModule = {
49
50
 
50
51
  UI.info(`Updating v${current} → v${latest}...`)
51
52
 
52
- const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"
53
- const arch = process.arch === "arm64" ? "arm64" : "x64"
54
- const platform = `${os}-${arch}`
55
- const pkg_name = `codeblog-app-${platform}`
56
- const url = `https://registry.npmjs.org/${pkg_name}/-/${pkg_name}-${latest}.tgz`
57
-
58
- const tmpdir = (await import("os")).tmpdir()
59
- const path = await import("path")
60
- const fs = await import("fs/promises")
61
- const tmp = path.join(tmpdir, `codeblog-update-${Date.now()}`)
62
- await fs.mkdir(tmp, { recursive: true })
63
-
64
- UI.info("Downloading...")
65
- const tgz = path.join(tmp, "pkg.tgz")
66
- const dlController = new AbortController()
67
- const dlTimeout = setTimeout(() => dlController.abort(), 60_000)
68
- let dlRes: Response
69
53
  try {
70
- dlRes = await fetch(url, { signal: dlController.signal })
71
- } catch (e: any) {
72
- clearTimeout(dlTimeout)
73
- await fs.rm(tmp, { recursive: true, force: true }).catch(() => {})
74
- if (e.name === "AbortError") {
75
- UI.error("Download timed out (60s). Please check your network and try again.")
76
- } else {
77
- UI.error(`Download failed: ${e.message}`)
78
- }
54
+ await performUpdate(latest)
55
+ UI.success(`Updated to v${latest}!`)
56
+ } catch (e) {
57
+ UI.error(e instanceof Error ? e.message : String(e))
79
58
  process.exitCode = 1
80
- return
81
- }
82
- clearTimeout(dlTimeout)
83
- if (!dlRes.ok) {
84
- UI.error(`Failed to download update for ${platform} (HTTP ${dlRes.status})`)
85
- await fs.rm(tmp, { recursive: true, force: true }).catch(() => {})
86
- process.exitCode = 1
87
- return
88
- }
89
-
90
- const arrayBuf = await dlRes.arrayBuffer()
91
- await fs.writeFile(tgz, Buffer.from(arrayBuf))
92
-
93
- UI.info("Extracting...")
94
- const proc = Bun.spawn(["tar", "-xzf", tgz, "-C", tmp], { stdout: "ignore", stderr: "ignore" })
95
- await proc.exited
96
-
97
- const bin = process.execPath
98
- const ext = os === "windows" ? ".exe" : ""
99
- const src = path.join(tmp, "package", "bin", `codeblog${ext}`)
100
-
101
- UI.info("Installing...")
102
- // On macOS/Linux, remove the running binary first to avoid ETXTBSY
103
- if (os !== "windows") {
104
- try {
105
- await fs.unlink(bin)
106
- } catch {
107
- // ignore if removal fails
108
- }
109
- }
110
- await fs.copyFile(src, bin)
111
- if (os !== "windows") {
112
- await fs.chmod(bin, 0o755)
113
59
  }
114
- if (os === "darwin") {
115
- await Bun.spawn(["codesign", "--remove-signature", bin], { stdout: "ignore", stderr: "ignore" }).exited
116
- const cs = Bun.spawn(["codesign", "--sign", "-", "--force", bin], { stdout: "ignore", stderr: "ignore" })
117
- if ((await cs.exited) !== 0) {
118
- await fs.rm(tmp, { recursive: true, force: true })
119
- UI.error("Update installed but macOS code signing failed. Please reinstall with install.sh.")
120
- process.exitCode = 1
121
- return
122
- }
123
- const verify = Bun.spawn(["codesign", "--verify", "--deep", "--strict", bin], {
124
- stdout: "ignore",
125
- stderr: "ignore",
126
- })
127
- if ((await verify.exited) !== 0) {
128
- await fs.rm(tmp, { recursive: true, force: true })
129
- UI.error("Update installed but signature verification failed. Please reinstall with install.sh.")
130
- process.exitCode = 1
131
- return
132
- }
133
- }
134
-
135
- await fs.rm(tmp, { recursive: true, force: true })
136
-
137
- UI.success(`Updated to v${latest}!`)
138
60
  },
139
61
  }
@@ -0,0 +1,68 @@
1
+ import path from "path"
2
+ import { tmpdir } from "os"
3
+ import { mkdir, writeFile, rm, unlink, copyFile, chmod } from "fs/promises"
4
+
5
+ /**
6
+ * Downloads and installs a specific version of the codeblog CLI binary.
7
+ * Shared between the `update` command and auto-update logic.
8
+ *
9
+ * Throws on failure so callers can handle errors as they see fit.
10
+ */
11
+ export async function performUpdate(latest: string): Promise<void> {
12
+ const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"
13
+ const arch = process.arch === "arm64" ? "arm64" : "x64"
14
+ const platform = `${os}-${arch}`
15
+ const pkgName = `codeblog-app-${platform}`
16
+ const url = `https://registry.npmjs.org/${pkgName}/-/${pkgName}-${latest}.tgz`
17
+
18
+ const tmp = path.join(tmpdir(), `codeblog-update-${Date.now()}`)
19
+ await mkdir(tmp, { recursive: true })
20
+
21
+ try {
22
+ // Download
23
+ const tgz = path.join(tmp, "pkg.tgz")
24
+ const dlController = new AbortController()
25
+ const dlTimeout = setTimeout(() => dlController.abort(), 60_000)
26
+ let dlRes: Response
27
+ try {
28
+ dlRes = await fetch(url, { signal: dlController.signal })
29
+ } catch (e: any) {
30
+ clearTimeout(dlTimeout)
31
+ if (e.name === "AbortError") throw new Error("Download timed out (60s)")
32
+ throw new Error(`Download failed: ${e.message}`)
33
+ }
34
+ clearTimeout(dlTimeout)
35
+ if (!dlRes.ok) throw new Error(`Failed to download update for ${platform} (HTTP ${dlRes.status})`)
36
+
37
+ const arrayBuf = await dlRes.arrayBuffer()
38
+ await writeFile(tgz, Buffer.from(arrayBuf))
39
+
40
+ // Extract
41
+ const proc = Bun.spawn(["tar", "-xzf", tgz, "-C", tmp], { stdout: "ignore", stderr: "ignore" })
42
+ await proc.exited
43
+
44
+ // Install
45
+ const bin = process.execPath
46
+ const ext = os === "windows" ? ".exe" : ""
47
+ const src = path.join(tmp, "package", "bin", `codeblog${ext}`)
48
+
49
+ if (os !== "windows") {
50
+ try { await unlink(bin) } catch {}
51
+ }
52
+ await copyFile(src, bin)
53
+ if (os !== "windows") {
54
+ await chmod(bin, 0o755)
55
+ }
56
+
57
+ // macOS codesign
58
+ if (os === "darwin") {
59
+ await Bun.spawn(["codesign", "--remove-signature", bin], { stdout: "ignore", stderr: "ignore" }).exited
60
+ const cs = Bun.spawn(["codesign", "--sign", "-", "--force", bin], { stdout: "ignore", stderr: "ignore" })
61
+ if ((await cs.exited) !== 0) throw new Error("macOS code signing failed")
62
+ const verify = Bun.spawn(["codesign", "--verify", "--deep", "--strict", bin], { stdout: "ignore", stderr: "ignore" })
63
+ if ((await verify.exited) !== 0) throw new Error("macOS signature verification failed")
64
+ }
65
+ } finally {
66
+ await rm(tmp, { recursive: true, force: true }).catch(() => {})
67
+ }
68
+ }
package/src/cli/ui.ts CHANGED
@@ -200,7 +200,7 @@ export namespace UI {
200
200
  })
201
201
  }
202
202
 
203
- export async function select(prompt: string, options: string[]): Promise<number> {
203
+ export async function select(prompt: string, options: string[], opts?: { searchable?: boolean }): Promise<number> {
204
204
  if (options.length === 0) return 0
205
205
 
206
206
  const stdin = process.stdin
@@ -209,8 +209,11 @@ export namespace UI {
209
209
  process.stderr.write("\x1b[?25l")
210
210
 
211
211
  let idx = 0
212
+ let filter = ""
213
+ let filtered = options.map((label, originalIndex) => ({ label, originalIndex }))
212
214
  let drawnRows = 0
213
215
  const maxRows = 12
216
+ const searchable = opts?.searchable !== false
214
217
  let onData: ((ch: Buffer) => void) = () => {}
215
218
 
216
219
  const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b./g, "")
@@ -220,24 +223,42 @@ export namespace UI {
220
223
  return Math.max(1, Math.ceil((len || 1) / cols))
221
224
  }
222
225
 
226
+ const applyFilter = () => {
227
+ const q = filter.toLowerCase()
228
+ if (!q) {
229
+ filtered = options.map((label, originalIndex) => ({ label, originalIndex }))
230
+ } else {
231
+ filtered = options
232
+ .map((label, originalIndex) => ({ label, originalIndex }))
233
+ .filter(({ label }) => stripAnsi(label).toLowerCase().includes(q))
234
+ }
235
+ idx = 0
236
+ }
237
+
223
238
  const draw = () => {
224
239
  if (drawnRows > 1) process.stderr.write(`\x1b[${drawnRows - 1}F`)
225
240
  process.stderr.write("\x1b[J")
226
241
 
227
- const start = options.length <= maxRows ? 0 : Math.max(0, Math.min(idx - 4, options.length - maxRows))
228
- const items = options.slice(start, start + maxRows)
242
+ const count = filtered.length
243
+ const start = count <= maxRows ? 0 : Math.max(0, Math.min(idx - 4, count - maxRows))
244
+ const items = filtered.slice(start, start + maxRows)
245
+ const searchLine = filter
246
+ ? ` ${Style.TEXT_HIGHLIGHT}❯${Style.TEXT_NORMAL} ${filter}${Style.TEXT_DIM}█${Style.TEXT_NORMAL}`
247
+ : ` ${Style.TEXT_HIGHLIGHT}❯${Style.TEXT_NORMAL} ${Style.TEXT_DIM}type to search...${Style.TEXT_NORMAL}`
229
248
  const lines = [
230
249
  prompt,
250
+ ...(searchable ? [searchLine] : []),
231
251
  ...items.map((item, i) => {
232
252
  const active = start + i === idx
233
253
  const marker = active ? `${Style.TEXT_HIGHLIGHT}●${Style.TEXT_NORMAL}` : "○"
234
- const text = active ? `${Style.TEXT_NORMAL_BOLD}${item}${Style.TEXT_NORMAL}` : item
254
+ const text = active ? `${Style.TEXT_NORMAL_BOLD}${item.label}${Style.TEXT_NORMAL}` : item.label
235
255
  return ` ${marker} ${text}`
236
256
  }),
237
- options.length > maxRows
238
- ? ` ${Style.TEXT_DIM}${start > 0 ? "↑ more " : ""}${start + maxRows < options.length ? "↓ more" : ""}${Style.TEXT_NORMAL}`
257
+ ...(count === 0 ? [` ${Style.TEXT_DIM}No matches${Style.TEXT_NORMAL}`] : []),
258
+ count > maxRows
259
+ ? ` ${Style.TEXT_DIM}${start > 0 ? "↑ more " : ""}${start + maxRows < count ? "↓ more" : ""}${Style.TEXT_NORMAL}`
239
260
  : ` ${Style.TEXT_DIM}${Style.TEXT_NORMAL}`,
240
- ` ${Style.TEXT_DIM}Use ↑/↓ then Enter (Esc to cancel)${Style.TEXT_NORMAL}`,
261
+ ` ${Style.TEXT_DIM}↑/↓ select · Enter confirm · Esc ${filter ? "clear" : "cancel"}${Style.TEXT_NORMAL}`,
241
262
  ]
242
263
  process.stderr.write(lines.map((line) => `\x1b[2K\r${line}`).join("\n"))
243
264
  drawnRows = lines.reduce((sum, line) => sum + rowCount(line), 0)
@@ -260,25 +281,57 @@ export namespace UI {
260
281
  process.exit(130)
261
282
  }
262
283
  if (c === "\r" || c === "\n") {
263
- restore()
264
- resolve(idx)
284
+ if (filtered.length > 0) {
285
+ restore()
286
+ resolve(filtered[idx]!.originalIndex)
287
+ }
265
288
  return
266
289
  }
267
290
  if (c === "\x1b") {
268
- restore()
269
- resolve(-1)
291
+ if (searchable && filter) {
292
+ filter = ""
293
+ applyFilter()
294
+ draw()
295
+ } else {
296
+ restore()
297
+ resolve(-1)
298
+ }
270
299
  return
271
300
  }
272
- if (c === "k" || c.includes("\x1b[A") || c.includes("\x1bOA")) {
273
- idx = (idx - 1 + options.length) % options.length
301
+ if (c === "\x7f" || c === "\b") {
302
+ if (searchable && filter) {
303
+ filter = filter.slice(0, -1)
304
+ applyFilter()
305
+ draw()
306
+ }
307
+ return
308
+ }
309
+ if (c.includes("\x1b[A") || c.includes("\x1bOA")) {
310
+ if (filtered.length > 0) idx = (idx - 1 + filtered.length) % filtered.length
274
311
  draw()
275
312
  return
276
313
  }
277
- if (c === "j" || c.includes("\x1b[B") || c.includes("\x1bOB")) {
278
- idx = (idx + 1) % options.length
314
+ if (c.includes("\x1b[B") || c.includes("\x1bOB")) {
315
+ if (filtered.length > 0) idx = (idx + 1) % filtered.length
279
316
  draw()
280
317
  return
281
318
  }
319
+ // j/k navigation only when filter is empty (otherwise they are search characters)
320
+ if (!filter && (c === "k" || c === "j")) {
321
+ if (c === "k") idx = (idx - 1 + filtered.length) % filtered.length
322
+ else idx = (idx + 1) % filtered.length
323
+ draw()
324
+ return
325
+ }
326
+ // Printable characters → append to search filter (only when searchable)
327
+ if (searchable) {
328
+ const printable = c.replace(/[\x00-\x1f\x7f]/g, "")
329
+ if (printable) {
330
+ filter += printable
331
+ applyFilter()
332
+ draw()
333
+ }
334
+ }
282
335
  }
283
336
  stdin.on("data", onData)
284
337
  })
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { UI } from "./cli/ui"
5
5
  import { EOL } from "os"
6
6
  import { McpBridge } from "./mcp/client"
7
7
  import { Auth } from "./auth"
8
+ import { checkAndAutoUpdate } from "./cli/auto-update"
8
9
 
9
10
  // Commands
10
11
  import { SetupCommand } from "./cli/cmd/setup"
@@ -161,6 +162,11 @@ const hasSubcommand = args.length > 0 && !args[0]!.startsWith("-")
161
162
  const isHelp = args.includes("--help") || args.includes("-h")
162
163
  const isVersion = args.includes("--version") || args.includes("-v")
163
164
 
165
+ // Auto-update check on startup
166
+ if (!isHelp && !isVersion) {
167
+ await checkAndAutoUpdate()
168
+ }
169
+
164
170
  if (!hasSubcommand && !isHelp && !isVersion) {
165
171
  await Log.init({ print: false })
166
172
  Log.Default.info("codeblog", { version: VERSION, args: [] })