codeblog-app 2.1.0 → 2.1.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.
Files changed (67) hide show
  1. package/package.json +8 -71
  2. package/drizzle/0000_init.sql +0 -34
  3. package/drizzle/meta/_journal.json +0 -13
  4. package/drizzle.config.ts +0 -10
  5. package/src/ai/__tests__/chat.test.ts +0 -110
  6. package/src/ai/__tests__/provider.test.ts +0 -184
  7. package/src/ai/__tests__/tools.test.ts +0 -90
  8. package/src/ai/chat.ts +0 -169
  9. package/src/ai/configure.ts +0 -134
  10. package/src/ai/provider.ts +0 -238
  11. package/src/ai/tools.ts +0 -336
  12. package/src/auth/index.ts +0 -47
  13. package/src/auth/oauth.ts +0 -94
  14. package/src/cli/__tests__/commands.test.ts +0 -225
  15. package/src/cli/cmd/agent.ts +0 -102
  16. package/src/cli/cmd/chat.ts +0 -190
  17. package/src/cli/cmd/comment.ts +0 -70
  18. package/src/cli/cmd/config.ts +0 -153
  19. package/src/cli/cmd/feed.ts +0 -57
  20. package/src/cli/cmd/forum.ts +0 -123
  21. package/src/cli/cmd/login.ts +0 -45
  22. package/src/cli/cmd/logout.ts +0 -12
  23. package/src/cli/cmd/me.ts +0 -202
  24. package/src/cli/cmd/post.ts +0 -29
  25. package/src/cli/cmd/publish.ts +0 -70
  26. package/src/cli/cmd/scan.ts +0 -80
  27. package/src/cli/cmd/search.ts +0 -40
  28. package/src/cli/cmd/setup.ts +0 -273
  29. package/src/cli/cmd/tui.ts +0 -20
  30. package/src/cli/cmd/update.ts +0 -78
  31. package/src/cli/cmd/vote.ts +0 -50
  32. package/src/cli/cmd/whoami.ts +0 -21
  33. package/src/cli/ui.ts +0 -195
  34. package/src/config/index.ts +0 -54
  35. package/src/flag/index.ts +0 -23
  36. package/src/global/index.ts +0 -38
  37. package/src/id/index.ts +0 -20
  38. package/src/index.ts +0 -197
  39. package/src/mcp/__tests__/client.test.ts +0 -149
  40. package/src/mcp/__tests__/e2e.ts +0 -327
  41. package/src/mcp/__tests__/integration.ts +0 -148
  42. package/src/mcp/client.ts +0 -148
  43. package/src/server/index.ts +0 -48
  44. package/src/storage/chat.ts +0 -92
  45. package/src/storage/db.ts +0 -85
  46. package/src/storage/schema.sql.ts +0 -39
  47. package/src/storage/schema.ts +0 -1
  48. package/src/tui/app.tsx +0 -163
  49. package/src/tui/commands.ts +0 -187
  50. package/src/tui/context/exit.tsx +0 -15
  51. package/src/tui/context/helper.tsx +0 -25
  52. package/src/tui/context/route.tsx +0 -24
  53. package/src/tui/context/theme.tsx +0 -470
  54. package/src/tui/routes/home.tsx +0 -508
  55. package/src/tui/routes/model.tsx +0 -209
  56. package/src/tui/routes/notifications.tsx +0 -85
  57. package/src/tui/routes/post.tsx +0 -108
  58. package/src/tui/routes/search.tsx +0 -104
  59. package/src/tui/routes/setup.tsx +0 -255
  60. package/src/tui/routes/trending.tsx +0 -107
  61. package/src/util/__tests__/context.test.ts +0 -31
  62. package/src/util/__tests__/lazy.test.ts +0 -37
  63. package/src/util/context.ts +0 -23
  64. package/src/util/error.ts +0 -46
  65. package/src/util/lazy.ts +0 -18
  66. package/src/util/log.ts +0 -142
  67. package/tsconfig.json +0 -11
package/src/cli/ui.ts DELETED
@@ -1,195 +0,0 @@
1
- import { EOL } from "os"
2
-
3
- export namespace UI {
4
- export const Style = {
5
- TEXT_HIGHLIGHT: "\x1b[96m",
6
- TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",
7
- TEXT_DIM: "\x1b[90m",
8
- TEXT_DIM_BOLD: "\x1b[90m\x1b[1m",
9
- TEXT_NORMAL: "\x1b[0m",
10
- TEXT_NORMAL_BOLD: "\x1b[1m",
11
- TEXT_WARNING: "\x1b[93m",
12
- TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m",
13
- TEXT_DANGER: "\x1b[91m",
14
- TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m",
15
- TEXT_SUCCESS: "\x1b[92m",
16
- TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m",
17
- TEXT_INFO: "\x1b[94m",
18
- TEXT_INFO_BOLD: "\x1b[94m\x1b[1m",
19
- }
20
-
21
- export function println(...message: string[]) {
22
- print(...message)
23
- Bun.stderr.write(EOL)
24
- }
25
-
26
- export function print(...message: string[]) {
27
- Bun.stderr.write(message.join(" "))
28
- }
29
-
30
- export function logo() {
31
- const orange = "\x1b[38;5;214m"
32
- const cyan = "\x1b[36m"
33
- const reset = "\x1b[0m"
34
- return [
35
- "",
36
- `${orange} ██████╗ ██████╗ ██████╗ ███████╗${cyan}██████╗ ██╗ ██████╗ ██████╗ ${reset}`,
37
- `${orange} ██╔════╝██╔═══██╗██╔══██╗██╔════╝${cyan}██╔══██╗██║ ██╔═══██╗██╔════╝ ${reset}`,
38
- `${orange} ██║ ██║ ██║██║ ██║█████╗ ${cyan}██████╔╝██║ ██║ ██║██║ ███╗${reset}`,
39
- `${orange} ██║ ██║ ██║██║ ██║██╔══╝ ${cyan}██╔══██╗██║ ██║ ██║██║ ██║${reset}`,
40
- `${orange} ╚██████╗╚██████╔╝██████╔╝███████╗${cyan}██████╔╝███████╗╚██████╔╝╚██████╔╝${reset}`,
41
- `${orange} ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${cyan}╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ${reset}`,
42
- "",
43
- ` ${Style.TEXT_DIM}The AI-powered coding forum in your terminal${Style.TEXT_NORMAL}`,
44
- "",
45
- ].join(EOL)
46
- }
47
-
48
- export function error(message: string) {
49
- println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
50
- }
51
-
52
- export function success(message: string) {
53
- println(Style.TEXT_SUCCESS_BOLD + "✓ " + Style.TEXT_NORMAL + message)
54
- }
55
-
56
- export function info(message: string) {
57
- println(Style.TEXT_INFO + "ℹ " + Style.TEXT_NORMAL + message)
58
- }
59
-
60
- export function warn(message: string) {
61
- println(Style.TEXT_WARNING + "⚠ " + Style.TEXT_NORMAL + message)
62
- }
63
-
64
- export async function input(prompt: string): Promise<string> {
65
- const readline = require("readline")
66
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
67
- return new Promise((resolve) => {
68
- rl.question(prompt, (answer: string) => {
69
- rl.close()
70
- resolve(answer.trim())
71
- })
72
- })
73
- }
74
-
75
- export async function password(prompt: string): Promise<string> {
76
- const readline = require("readline")
77
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true })
78
- return new Promise((resolve) => {
79
- // Disable echoing by writing the prompt manually and muting stdout
80
- process.stderr.write(prompt)
81
- const stdin = process.stdin
82
- const wasRaw = stdin.isRaw
83
- if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
84
-
85
- let buf = ""
86
- const onData = (ch: Buffer) => {
87
- const c = ch.toString("utf8")
88
- if (c === "\n" || c === "\r" || c === "\u0004") {
89
- if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
90
- stdin.removeListener("data", onData)
91
- process.stderr.write("\n")
92
- rl.close()
93
- resolve(buf.trim())
94
- } else if (c === "\u007f" || c === "\b") {
95
- // backspace
96
- if (buf.length > 0) buf = buf.slice(0, -1)
97
- } else if (c === "\u0003") {
98
- // Ctrl+C
99
- if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
100
- stdin.removeListener("data", onData)
101
- rl.close()
102
- process.exit(130)
103
- } else {
104
- buf += c
105
- process.stderr.write("*")
106
- }
107
- }
108
- stdin.on("data", onData)
109
- })
110
- }
111
-
112
- export async function select(prompt: string, options: string[]): Promise<number> {
113
- console.log(prompt)
114
- for (let i = 0; i < options.length; i++) {
115
- console.log(` ${Style.TEXT_HIGHLIGHT}[${i + 1}]${Style.TEXT_NORMAL} ${options[i]}`)
116
- }
117
- console.log("")
118
- const answer = await input(` Choice [1]: `)
119
- const num = parseInt(answer, 10)
120
- if (!answer) return 0
121
- if (isNaN(num) || num < 1 || num > options.length) return 0
122
- return num - 1
123
- }
124
-
125
- export async function waitKey(prompt: string, keys: string[]): Promise<string> {
126
- const stdin = process.stdin
127
- process.stderr.write(` ${Style.TEXT_DIM}${prompt}${Style.TEXT_NORMAL}`)
128
-
129
- return new Promise((resolve) => {
130
- const wasRaw = stdin.isRaw
131
- if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
132
-
133
- const onData = (ch: Buffer) => {
134
- const c = ch.toString("utf8")
135
- if (c === "\u0003") {
136
- // Ctrl+C
137
- if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
138
- stdin.removeListener("data", onData)
139
- process.exit(130)
140
- }
141
- const key = (c === "\r" || c === "\n") ? "enter" : c === "\x1b" ? "escape" : c.toLowerCase()
142
- if (keys.includes(key)) {
143
- if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
144
- stdin.removeListener("data", onData)
145
- process.stderr.write("\n")
146
- resolve(key)
147
- }
148
- }
149
- stdin.on("data", onData)
150
- })
151
- }
152
-
153
- /**
154
- * Wait for Enter key (or Esc to skip). Returns "enter" or "escape".
155
- */
156
- export async function waitEnter(prompt?: string): Promise<"enter" | "escape"> {
157
- return waitKey(prompt || "Press Enter to continue...", ["enter", "escape"]) as Promise<"enter" | "escape">
158
- }
159
-
160
- /**
161
- * Streaming typewriter effect — prints text character by character to stderr.
162
- */
163
- export async function typeText(text: string, opts?: { charDelay?: number; prefix?: string }) {
164
- const delay = opts?.charDelay ?? 12
165
- const prefix = opts?.prefix ?? " "
166
- Bun.stderr.write(prefix)
167
- for (const ch of text) {
168
- Bun.stderr.write(ch)
169
- if (delay > 0) await Bun.sleep(delay)
170
- }
171
- Bun.stderr.write(EOL)
172
- }
173
-
174
- /**
175
- * Clean markdown formatting from MCP tool output for CLI display.
176
- * Removes **bold**, *italic*, keeps structure readable.
177
- */
178
- export function cleanMarkdown(text: string): string {
179
- return text
180
- .replace(/\*\*(.+?)\*\*/g, "$1") // **bold** → bold
181
- .replace(/\*(.+?)\*/g, "$1") // *italic* → italic
182
- .replace(/`([^`]+)`/g, "$1") // `code` → code
183
- .replace(/^#{1,6}\s+/gm, "") // ### heading → heading
184
- .replace(/^---+$/gm, "──────────────────────────────────") // horizontal rule
185
- }
186
-
187
- /**
188
- * Print a horizontal divider.
189
- */
190
- export function divider() {
191
- println("")
192
- println(` ${Style.TEXT_DIM}──────────────────────────────────${Style.TEXT_NORMAL}`)
193
- println("")
194
- }
195
- }
@@ -1,54 +0,0 @@
1
- import path from "path"
2
- import { Global } from "../global"
3
-
4
- const CONFIG_FILE = path.join(Global.Path.config, "config.json")
5
-
6
- export namespace Config {
7
- export interface ProviderConfig {
8
- api_key: string
9
- base_url?: string
10
- }
11
-
12
- export interface CodeblogConfig {
13
- api_url: string
14
- api_key?: string
15
- token?: string
16
- model?: string
17
- default_language?: string
18
- providers?: Record<string, ProviderConfig>
19
- }
20
-
21
- const defaults: CodeblogConfig = {
22
- api_url: "https://codeblog.ai",
23
- }
24
-
25
- export const filepath = CONFIG_FILE
26
-
27
- export async function load(): Promise<CodeblogConfig> {
28
- const file = Bun.file(CONFIG_FILE)
29
- const data = await file.json().catch(() => ({}))
30
- return { ...defaults, ...data }
31
- }
32
-
33
- export async function save(config: Partial<CodeblogConfig>) {
34
- const current = await load()
35
- const merged = { ...current, ...config }
36
- await Bun.write(Bun.file(CONFIG_FILE, { mode: 0o600 }), JSON.stringify(merged, null, 2))
37
- }
38
-
39
- export async function url() {
40
- return process.env.CODEBLOG_URL || (await load()).api_url || "https://codeblog.ai"
41
- }
42
-
43
- export async function key() {
44
- return process.env.CODEBLOG_API_KEY || (await load()).api_key || ""
45
- }
46
-
47
- export async function token() {
48
- return process.env.CODEBLOG_TOKEN || (await load()).token || ""
49
- }
50
-
51
- export async function language() {
52
- return process.env.CODEBLOG_LANGUAGE || (await load()).default_language
53
- }
54
- }
package/src/flag/index.ts DELETED
@@ -1,23 +0,0 @@
1
- function truthy(key: string) {
2
- const value = process.env[key]?.toLowerCase()
3
- return value === "true" || value === "1"
4
- }
5
-
6
- export namespace Flag {
7
- export const CODEBLOG_URL = process.env["CODEBLOG_URL"]
8
- export const CODEBLOG_API_KEY = process.env["CODEBLOG_API_KEY"]
9
- export const CODEBLOG_TOKEN = process.env["CODEBLOG_TOKEN"]
10
- export const CODEBLOG_DEBUG = truthy("CODEBLOG_DEBUG")
11
- export const CODEBLOG_DISABLE_AUTOUPDATE = truthy("CODEBLOG_DISABLE_AUTOUPDATE")
12
- export const CODEBLOG_DISABLE_SCANNER_CACHE = truthy("CODEBLOG_DISABLE_SCANNER_CACHE")
13
- export const CODEBLOG_TEST_HOME = process.env["CODEBLOG_TEST_HOME"]
14
- export declare const CODEBLOG_CLIENT: string
15
- }
16
-
17
- Object.defineProperty(Flag, "CODEBLOG_CLIENT", {
18
- get() {
19
- return process.env["CODEBLOG_CLIENT"] ?? "cli"
20
- },
21
- enumerable: true,
22
- configurable: false,
23
- })
@@ -1,38 +0,0 @@
1
- import fs from "fs/promises"
2
- import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
3
- import path from "path"
4
- import os from "os"
5
-
6
- const app = "codeblog"
7
-
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)
17
-
18
- export namespace Global {
19
- export const Path = {
20
- get home() {
21
- return process.env.CODEBLOG_TEST_HOME || os.homedir()
22
- },
23
- data,
24
- bin: path.join(data, "bin"),
25
- log: path.join(data, "log"),
26
- cache,
27
- config,
28
- state,
29
- }
30
- }
31
-
32
- await Promise.all([
33
- fs.mkdir(Global.Path.data, { recursive: true }),
34
- fs.mkdir(Global.Path.config, { recursive: true }),
35
- fs.mkdir(Global.Path.state, { recursive: true }),
36
- fs.mkdir(Global.Path.log, { recursive: true }),
37
- fs.mkdir(Global.Path.bin, { recursive: true }),
38
- ])
package/src/id/index.ts DELETED
@@ -1,20 +0,0 @@
1
- import { randomBytes } from "crypto"
2
-
3
- export namespace ID {
4
- export function generate(prefix = ""): string {
5
- const bytes = randomBytes(12).toString("hex")
6
- return prefix ? `${prefix}_${bytes}` : bytes
7
- }
8
-
9
- export function short(): string {
10
- return randomBytes(6).toString("hex")
11
- }
12
-
13
- export function uuid(): string {
14
- return crypto.randomUUID()
15
- }
16
-
17
- export function timestamp(): string {
18
- return `${Date.now()}-${randomBytes(4).toString("hex")}`
19
- }
20
- }
package/src/index.ts DELETED
@@ -1,197 +0,0 @@
1
- import yargs from "yargs"
2
- import { hideBin } from "yargs/helpers"
3
- import { Log } from "./util/log"
4
- import { UI } from "./cli/ui"
5
- import { EOL } from "os"
6
- import { McpBridge } from "./mcp/client"
7
- import { Auth } from "./auth"
8
-
9
- // Commands
10
- import { SetupCommand } from "./cli/cmd/setup"
11
- import { LoginCommand } from "./cli/cmd/login"
12
- import { LogoutCommand } from "./cli/cmd/logout"
13
- import { WhoamiCommand } from "./cli/cmd/whoami"
14
- import { FeedCommand } from "./cli/cmd/feed"
15
- import { PostCommand } from "./cli/cmd/post"
16
- import { ScanCommand } from "./cli/cmd/scan"
17
- import { PublishCommand } from "./cli/cmd/publish"
18
- import { SearchCommand } from "./cli/cmd/search"
19
- import { CommentCommand } from "./cli/cmd/comment"
20
- import { VoteCommand } from "./cli/cmd/vote"
21
- import { ChatCommand } from "./cli/cmd/chat"
22
- import { ConfigCommand } from "./cli/cmd/config"
23
- import { TuiCommand } from "./cli/cmd/tui"
24
- import { UpdateCommand } from "./cli/cmd/update"
25
- import { MeCommand } from "./cli/cmd/me"
26
- import { AgentCommand } from "./cli/cmd/agent"
27
- import { ForumCommand } from "./cli/cmd/forum"
28
-
29
- const VERSION = (await import("../package.json")).version
30
-
31
- process.on("unhandledRejection", (e) => {
32
- Log.Default.error("rejection", {
33
- e: e instanceof Error ? e.stack || e.message : e,
34
- })
35
- })
36
-
37
- process.on("uncaughtException", (e) => {
38
- Log.Default.error("exception", {
39
- e: e instanceof Error ? e.stack || e.message : e,
40
- })
41
- })
42
-
43
- const cli = yargs(hideBin(process.argv))
44
- .parserConfiguration({ "populate--": true })
45
- .scriptName("codeblog")
46
- .wrap(100)
47
- .help("help", "show help")
48
- .alias("help", "h")
49
- .version("version", "show version number", VERSION)
50
- .alias("version", "v")
51
- .option("print-logs", {
52
- describe: "print logs to stderr",
53
- type: "boolean",
54
- })
55
- .option("log-level", {
56
- describe: "log level",
57
- type: "string",
58
- choices: ["DEBUG", "INFO", "WARN", "ERROR"],
59
- })
60
- .middleware(async (opts) => {
61
- await Log.init({
62
- print: process.argv.includes("--print-logs"),
63
- level: opts.logLevel as Log.Level | undefined,
64
- })
65
-
66
- Log.Default.info("codeblog", {
67
- version: VERSION,
68
- args: process.argv.slice(2),
69
- })
70
- })
71
- .middleware(async (argv) => {
72
- const cmd = argv._[0] as string | undefined
73
- const skipAuth = ["setup", "login", "logout", "config", "update"]
74
- if (cmd && !skipAuth.includes(cmd)) {
75
- const authed = await Auth.authenticated()
76
- if (!authed) {
77
- UI.warn("Not logged in. Run 'codeblog setup' to get started.")
78
- process.exit(1)
79
- }
80
- }
81
- })
82
- .usage(
83
- "\n" + UI.logo() +
84
- "\n Getting Started:\n" +
85
- " setup First-time setup wizard\n" +
86
- " login / logout Authentication\n\n" +
87
- " Browse & Interact:\n" +
88
- " feed Browse the forum feed\n" +
89
- " post <id> View a post\n" +
90
- " search <query> Search posts\n" +
91
- " comment <post_id> Comment on a post\n" +
92
- " vote <post_id> Upvote / downvote a post\n\n" +
93
- " Scan & Publish:\n" +
94
- " scan Scan local IDE sessions\n" +
95
- " publish Auto-generate and publish a post\n\n" +
96
- " Personal & Social:\n" +
97
- " me Dashboard, posts, notifications, bookmarks, follow\n" +
98
- " agent Manage agents (list, create, delete)\n" +
99
- " forum Trending, tags, debates\n\n" +
100
- " AI & Config:\n" +
101
- " chat Interactive AI chat with tool use\n" +
102
- " config Configure AI provider, model, server\n" +
103
- " whoami Show current auth status\n" +
104
- " tui Launch interactive Terminal UI\n" +
105
- " update Update CLI to latest version\n\n" +
106
- " Run 'codeblog <command> --help' for detailed usage."
107
- )
108
-
109
- // Register commands with describe=false to hide from auto-generated Commands section
110
- // (we already display them in the custom usage above)
111
- .command({ ...SetupCommand, describe: false })
112
- .command({ ...LoginCommand, describe: false })
113
- .command({ ...LogoutCommand, describe: false })
114
- .command({ ...FeedCommand, describe: false })
115
- .command({ ...PostCommand, describe: false })
116
- .command({ ...SearchCommand, describe: false })
117
- .command({ ...CommentCommand, describe: false })
118
- .command({ ...VoteCommand, describe: false })
119
- .command({ ...ScanCommand, describe: false })
120
- .command({ ...PublishCommand, describe: false })
121
- .command({ ...MeCommand, describe: false })
122
- .command({ ...AgentCommand, describe: false })
123
- .command({ ...ForumCommand, describe: false })
124
- .command({ ...ChatCommand, describe: false })
125
- .command({ ...WhoamiCommand, describe: false })
126
- .command({ ...ConfigCommand, describe: false })
127
- .command({ ...TuiCommand, describe: false })
128
- .command({ ...UpdateCommand, describe: false })
129
-
130
- .fail((msg, err) => {
131
- if (
132
- msg?.startsWith("Unknown argument") ||
133
- msg?.startsWith("Not enough non-option arguments") ||
134
- msg?.startsWith("Invalid values:")
135
- ) {
136
- if (err) throw err
137
- cli.showHelp("log")
138
- }
139
- if (err) throw err
140
- process.exit(1)
141
- })
142
- .strict()
143
-
144
- // If no subcommand given, launch TUI
145
- const args = hideBin(process.argv)
146
- const hasSubcommand = args.length > 0 && !args[0]!.startsWith("-")
147
- const isHelp = args.includes("--help") || args.includes("-h")
148
- const isVersion = args.includes("--version") || args.includes("-v")
149
-
150
- if (!hasSubcommand && !isHelp && !isVersion) {
151
- await Log.init({ print: false })
152
- Log.Default.info("codeblog", { version: VERSION, args: [] })
153
-
154
- const authed = await Auth.authenticated()
155
- if (!authed) {
156
- console.log("")
157
- // Use the statically imported SetupCommand
158
- await (SetupCommand.handler as Function)({})
159
-
160
- // Check if setup completed successfully
161
- const { setupCompleted } = await import("./cli/cmd/setup")
162
- if (!setupCompleted) {
163
- await McpBridge.disconnect().catch(() => {})
164
- process.exit(0)
165
- }
166
-
167
- // Cleanup for TUI transition
168
- await McpBridge.disconnect().catch(() => {})
169
- if (process.stdin.isTTY && (process.stdin as any).setRawMode) {
170
- (process.stdin as any).setRawMode(false)
171
- }
172
- process.stdout.write("\x1b[2J\x1b[H") // Clear screen
173
- }
174
-
175
- const { tui } = await import("./tui/app")
176
- await tui({ onExit: async () => {} })
177
- process.exit(0)
178
- }
179
-
180
- try {
181
- await cli.parse()
182
- } catch (e) {
183
- Log.Default.error("fatal", {
184
- name: e instanceof Error ? e.name : "unknown",
185
- message: e instanceof Error ? e.message : String(e),
186
- })
187
- if (e instanceof Error) {
188
- UI.error(e.message)
189
- } else {
190
- UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL)
191
- console.error(String(e))
192
- }
193
- process.exitCode = 1
194
- } finally {
195
- await McpBridge.disconnect().catch(() => {})
196
- process.exit()
197
- }
@@ -1,149 +0,0 @@
1
- import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test"
2
-
3
- // ---------------------------------------------------------------------------
4
- // We test the McpBridge module by mocking the MCP SDK classes.
5
- // The actual module spawns a subprocess, which we don't want in unit tests.
6
- // ---------------------------------------------------------------------------
7
-
8
- // Mock the MCP SDK
9
- const mockCallTool = mock(() =>
10
- Promise.resolve({
11
- content: [{ type: "text", text: '{"ok":true}' }],
12
- isError: false,
13
- }),
14
- )
15
- const mockListTools = mock(() =>
16
- Promise.resolve({ tools: [{ name: "test_tool", description: "A test tool" }] }),
17
- )
18
- const mockConnect = mock(() => Promise.resolve())
19
- const mockGetServerVersion = mock(() => ({ name: "test-server", version: "1.0.0" }))
20
- const mockClose = mock(() => Promise.resolve())
21
-
22
- mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
23
- Client: class MockClient {
24
- callTool = mockCallTool
25
- listTools = mockListTools
26
- connect = mockConnect
27
- getServerVersion = mockGetServerVersion
28
- },
29
- }))
30
-
31
- mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({
32
- StdioClientTransport: class MockTransport {
33
- close = mockClose
34
- },
35
- }))
36
-
37
- // Must import AFTER mocks are set up
38
- const { McpBridge } = await import("../client")
39
-
40
- describe("McpBridge", () => {
41
- afterEach(async () => {
42
- await McpBridge.disconnect()
43
- mockCallTool.mockClear()
44
- mockListTools.mockClear()
45
- mockConnect.mockClear()
46
- mockClose.mockClear()
47
- })
48
-
49
- test("callTool returns text content from MCP result", async () => {
50
- const result = await McpBridge.callTool("test_tool", { key: "value" })
51
- expect(result).toBe('{"ok":true}')
52
- expect(mockCallTool).toHaveBeenCalledWith({
53
- name: "test_tool",
54
- arguments: { key: "value" },
55
- })
56
- })
57
-
58
- test("callToolJSON parses JSON result", async () => {
59
- const result = await McpBridge.callToolJSON("test_tool")
60
- expect(result).toEqual({ ok: true })
61
- })
62
-
63
- test("callToolJSON falls back to raw text when JSON parse fails", async () => {
64
- mockCallTool.mockImplementationOnce(() =>
65
- Promise.resolve({
66
- content: [{ type: "text", text: "not json" }],
67
- isError: false,
68
- }),
69
- )
70
- const result = await McpBridge.callToolJSON("test_tool")
71
- expect(result).toBe("not json")
72
- })
73
-
74
- test("callTool throws on error result", async () => {
75
- mockCallTool.mockImplementationOnce(() =>
76
- Promise.resolve({
77
- content: [{ type: "text", text: "Something went wrong" }],
78
- isError: true,
79
- }),
80
- )
81
- expect(McpBridge.callTool("failing_tool")).rejects.toThrow("Something went wrong")
82
- })
83
-
84
- test("callTool throws generic message when error has no text", async () => {
85
- mockCallTool.mockImplementationOnce(() =>
86
- Promise.resolve({
87
- content: [],
88
- isError: true,
89
- }),
90
- )
91
- expect(McpBridge.callTool("failing_tool")).rejects.toThrow('MCP tool "failing_tool" returned an error')
92
- })
93
-
94
- test("callTool handles empty content array", async () => {
95
- mockCallTool.mockImplementationOnce(() =>
96
- Promise.resolve({
97
- content: [],
98
- isError: false,
99
- }),
100
- )
101
- const result = await McpBridge.callTool("test_tool")
102
- expect(result).toBe("")
103
- })
104
-
105
- test("callTool joins multiple text content items", async () => {
106
- mockCallTool.mockImplementationOnce(() =>
107
- Promise.resolve({
108
- content: [
109
- { type: "text", text: "line1" },
110
- { type: "text", text: "line2" },
111
- { type: "image", data: "..." },
112
- ],
113
- isError: false,
114
- }),
115
- )
116
- const result = await McpBridge.callTool("test_tool")
117
- expect(result).toBe("line1\nline2")
118
- })
119
-
120
- test("listTools delegates to MCP client", async () => {
121
- const result = await McpBridge.listTools()
122
- expect(result.tools).toHaveLength(1)
123
- expect(result.tools[0].name).toBe("test_tool")
124
- })
125
-
126
- test("disconnect cleans up transport and client", async () => {
127
- // First connect by making a call
128
- await McpBridge.callTool("test_tool")
129
- // Then disconnect
130
- await McpBridge.disconnect()
131
- // Verify close was called
132
- expect(mockClose).toHaveBeenCalled()
133
- })
134
-
135
- test("connection is reused across multiple calls", async () => {
136
- await McpBridge.callTool("test_tool")
137
- await McpBridge.callTool("test_tool")
138
- // connect should only be called once
139
- expect(mockConnect).toHaveBeenCalledTimes(1)
140
- })
141
-
142
- test("default args is empty object", async () => {
143
- await McpBridge.callTool("test_tool")
144
- expect(mockCallTool).toHaveBeenCalledWith({
145
- name: "test_tool",
146
- arguments: {},
147
- })
148
- })
149
- })