codeblog-app 2.3.1 → 2.3.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.
- package/package.json +8 -73
- package/drizzle/0000_init.sql +0 -34
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/src/ai/__tests__/chat.test.ts +0 -188
- package/src/ai/__tests__/compat.test.ts +0 -46
- package/src/ai/__tests__/home.ai-stream.integration.test.ts +0 -77
- package/src/ai/__tests__/provider-registry.test.ts +0 -61
- package/src/ai/__tests__/provider.test.ts +0 -238
- package/src/ai/__tests__/stream-events.test.ts +0 -152
- package/src/ai/__tests__/tools.test.ts +0 -93
- package/src/ai/chat.ts +0 -336
- package/src/ai/configure.ts +0 -143
- package/src/ai/models.ts +0 -26
- package/src/ai/provider-registry.ts +0 -150
- package/src/ai/provider.ts +0 -264
- package/src/ai/stream-events.ts +0 -64
- package/src/ai/tools.ts +0 -118
- package/src/ai/types.ts +0 -105
- package/src/auth/index.ts +0 -49
- package/src/auth/oauth.ts +0 -123
- package/src/cli/__tests__/commands.test.ts +0 -229
- package/src/cli/cmd/agent.ts +0 -97
- package/src/cli/cmd/ai.ts +0 -10
- package/src/cli/cmd/chat.ts +0 -190
- package/src/cli/cmd/comment.ts +0 -67
- package/src/cli/cmd/config.ts +0 -153
- package/src/cli/cmd/feed.ts +0 -53
- package/src/cli/cmd/forum.ts +0 -106
- package/src/cli/cmd/login.ts +0 -45
- package/src/cli/cmd/logout.ts +0 -12
- package/src/cli/cmd/me.ts +0 -188
- package/src/cli/cmd/post.ts +0 -25
- package/src/cli/cmd/publish.ts +0 -64
- package/src/cli/cmd/scan.ts +0 -78
- package/src/cli/cmd/search.ts +0 -35
- package/src/cli/cmd/setup.ts +0 -622
- package/src/cli/cmd/tui.ts +0 -20
- package/src/cli/cmd/uninstall.ts +0 -281
- package/src/cli/cmd/update.ts +0 -123
- package/src/cli/cmd/vote.ts +0 -50
- package/src/cli/cmd/whoami.ts +0 -18
- package/src/cli/mcp-print.ts +0 -6
- package/src/cli/ui.ts +0 -357
- package/src/config/index.ts +0 -92
- package/src/flag/index.ts +0 -23
- package/src/global/index.ts +0 -38
- package/src/id/index.ts +0 -20
- package/src/index.ts +0 -203
- package/src/mcp/__tests__/client.test.ts +0 -149
- package/src/mcp/__tests__/e2e.ts +0 -331
- package/src/mcp/__tests__/integration.ts +0 -148
- package/src/mcp/client.ts +0 -118
- package/src/server/index.ts +0 -48
- package/src/storage/chat.ts +0 -73
- package/src/storage/db.ts +0 -85
- package/src/storage/schema.sql.ts +0 -39
- package/src/storage/schema.ts +0 -1
- package/src/tui/__tests__/input-intent.test.ts +0 -27
- package/src/tui/__tests__/stream-assembler.test.ts +0 -33
- package/src/tui/ai-stream.ts +0 -28
- package/src/tui/app.tsx +0 -210
- package/src/tui/commands.ts +0 -220
- package/src/tui/context/exit.tsx +0 -15
- package/src/tui/context/helper.tsx +0 -25
- package/src/tui/context/route.tsx +0 -24
- package/src/tui/context/theme.tsx +0 -471
- package/src/tui/input-intent.ts +0 -26
- package/src/tui/routes/home.tsx +0 -1060
- package/src/tui/routes/model.tsx +0 -210
- package/src/tui/routes/notifications.tsx +0 -87
- package/src/tui/routes/post.tsx +0 -102
- package/src/tui/routes/search.tsx +0 -105
- package/src/tui/routes/setup.tsx +0 -267
- package/src/tui/routes/trending.tsx +0 -107
- package/src/tui/stream-assembler.ts +0 -49
- package/src/util/__tests__/context.test.ts +0 -31
- package/src/util/__tests__/lazy.test.ts +0 -37
- package/src/util/context.ts +0 -23
- package/src/util/error.ts +0 -46
- package/src/util/lazy.ts +0 -18
- package/src/util/log.ts +0 -144
- package/tsconfig.json +0 -11
package/src/cli/cmd/setup.ts
DELETED
|
@@ -1,622 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
import { Auth } from "../../auth"
|
|
3
|
-
import { OAuth } from "../../auth/oauth"
|
|
4
|
-
import { McpBridge } from "../../mcp/client"
|
|
5
|
-
import { UI } from "../ui"
|
|
6
|
-
import { Config } from "../../config"
|
|
7
|
-
|
|
8
|
-
export let setupCompleted = false
|
|
9
|
-
|
|
10
|
-
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
async function authBrowser(): Promise<boolean> {
|
|
13
|
-
try {
|
|
14
|
-
console.log(` ${UI.Style.TEXT_DIM}Opening browser for login...${UI.Style.TEXT_NORMAL}`)
|
|
15
|
-
|
|
16
|
-
await OAuth.login({
|
|
17
|
-
onUrl: (url) => {
|
|
18
|
-
console.log(` ${UI.Style.TEXT_DIM}If the browser didn't open, visit:${UI.Style.TEXT_NORMAL}`)
|
|
19
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}${url}${UI.Style.TEXT_NORMAL}`)
|
|
20
|
-
console.log("")
|
|
21
|
-
console.log(` ${UI.Style.TEXT_DIM}Waiting for authentication...${UI.Style.TEXT_NORMAL}`)
|
|
22
|
-
},
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
return true
|
|
26
|
-
} catch (err) {
|
|
27
|
-
UI.error(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
28
|
-
return false
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ─── Scan & Publish ──────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
async function shimmerLine(text: string, durationMs = 2000): Promise<void> {
|
|
35
|
-
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
36
|
-
const startTime = Date.now()
|
|
37
|
-
let i = 0
|
|
38
|
-
while (Date.now() - startTime < durationMs) {
|
|
39
|
-
Bun.stderr.write(`\r ${UI.Style.TEXT_HIGHLIGHT}${frames[i % frames.length]}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}${text}${UI.Style.TEXT_NORMAL}`)
|
|
40
|
-
i++
|
|
41
|
-
await Bun.sleep(80)
|
|
42
|
-
}
|
|
43
|
-
Bun.stderr.write(`\r ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${text}\n`)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function scanAndPublish(): Promise<void> {
|
|
47
|
-
// Scan with shimmer animation
|
|
48
|
-
const scanPromise = McpBridge.callTool("scan_sessions", { limit: 10 })
|
|
49
|
-
await shimmerLine("Scanning local IDE sessions...", 1500)
|
|
50
|
-
|
|
51
|
-
let sessions: Array<{ id: string; source: string; project: string; title: string }>
|
|
52
|
-
try {
|
|
53
|
-
const text = await scanPromise
|
|
54
|
-
try {
|
|
55
|
-
sessions = JSON.parse(text)
|
|
56
|
-
} catch {
|
|
57
|
-
console.log(` ${text}`)
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
} catch (err) {
|
|
61
|
-
UI.warn(`Could not scan sessions: ${err instanceof Error ? err.message : String(err)}`)
|
|
62
|
-
await UI.typeText("No worries — you can scan later with /scan in the app.")
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (sessions.length === 0) {
|
|
67
|
-
await UI.typeText("No IDE sessions found yet. That's okay!")
|
|
68
|
-
await UI.typeText("You can scan later with /scan once you've used an AI-powered IDE.")
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Show what we found
|
|
73
|
-
const sources = [...new Set(sessions.map((s) => s.source))]
|
|
74
|
-
console.log("")
|
|
75
|
-
await UI.typeText(
|
|
76
|
-
`Found ${sessions.length} session${sessions.length > 1 ? "s" : ""} across ${sources.length} IDE${sources.length > 1 ? "s" : ""}: ${sources.join(", ")}`,
|
|
77
|
-
{ charDelay: 10 },
|
|
78
|
-
)
|
|
79
|
-
console.log("")
|
|
80
|
-
|
|
81
|
-
for (const s of sessions.slice(0, 3)) {
|
|
82
|
-
console.log(` ${UI.Style.TEXT_INFO}[${s.source}]${UI.Style.TEXT_NORMAL} ${s.project} — ${s.title.slice(0, 60)}`)
|
|
83
|
-
}
|
|
84
|
-
if (sessions.length > 3) {
|
|
85
|
-
console.log(` ${UI.Style.TEXT_DIM}... and ${sessions.length - 3} more${UI.Style.TEXT_NORMAL}`)
|
|
86
|
-
}
|
|
87
|
-
console.log("")
|
|
88
|
-
|
|
89
|
-
// Analyze with shimmer — show the thinking process step by step
|
|
90
|
-
await shimmerLine("Analyzing sessions for interesting insights...", 1200)
|
|
91
|
-
|
|
92
|
-
// Dry run — preview (with shimmer while waiting)
|
|
93
|
-
let preview: string
|
|
94
|
-
try {
|
|
95
|
-
const postPromise = McpBridge.callTool("auto_post", { dry_run: true })
|
|
96
|
-
await shimmerLine("Crafting a blog post from your best session...", 2000)
|
|
97
|
-
preview = await postPromise
|
|
98
|
-
} catch (err) {
|
|
99
|
-
UI.warn(`Could not generate post: ${err instanceof Error ? err.message : String(err)}`)
|
|
100
|
-
await UI.typeText("You can try again later with /publish in the app.")
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Display preview with structured layout
|
|
105
|
-
const cleaned = UI.cleanMarkdown(preview)
|
|
106
|
-
console.log("")
|
|
107
|
-
UI.divider()
|
|
108
|
-
|
|
109
|
-
// Parse out key fields for better display
|
|
110
|
-
const lines = cleaned.split("\n")
|
|
111
|
-
let title = ""
|
|
112
|
-
let tags = ""
|
|
113
|
-
let category = ""
|
|
114
|
-
const bodyLines: string[] = []
|
|
115
|
-
|
|
116
|
-
for (const line of lines) {
|
|
117
|
-
const trimmed = line.trim()
|
|
118
|
-
if (!trimmed || trimmed.startsWith("DRY RUN") || trimmed === "---" || trimmed.match(/^─+$/)) continue
|
|
119
|
-
if (trimmed.startsWith("Title:")) { title = trimmed.replace("Title:", "").trim(); continue }
|
|
120
|
-
if (trimmed.startsWith("Tags:")) { tags = trimmed.replace("Tags:", "").trim(); continue }
|
|
121
|
-
if (trimmed.startsWith("Category:")) { category = trimmed.replace("Category:", "").trim(); continue }
|
|
122
|
-
if (trimmed.startsWith("Session:")) continue
|
|
123
|
-
bodyLines.push(trimmed)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Structured display
|
|
127
|
-
if (title) {
|
|
128
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}📝 ${title}${UI.Style.TEXT_NORMAL}`)
|
|
129
|
-
console.log("")
|
|
130
|
-
}
|
|
131
|
-
if (category || tags) {
|
|
132
|
-
const meta: string[] = []
|
|
133
|
-
if (category) meta.push(`Category: ${category}`)
|
|
134
|
-
if (tags) meta.push(`Tags: ${tags}`)
|
|
135
|
-
console.log(` ${UI.Style.TEXT_DIM}${meta.join(" · ")}${UI.Style.TEXT_NORMAL}`)
|
|
136
|
-
console.log("")
|
|
137
|
-
}
|
|
138
|
-
if (bodyLines.length > 0) {
|
|
139
|
-
// Show a preview snippet (first few meaningful lines)
|
|
140
|
-
const snippet = bodyLines.slice(0, 6)
|
|
141
|
-
for (const line of snippet) {
|
|
142
|
-
console.log(` ${line}`)
|
|
143
|
-
}
|
|
144
|
-
if (bodyLines.length > 6) {
|
|
145
|
-
console.log(` ${UI.Style.TEXT_DIM}... (${bodyLines.length - 6} more lines)${UI.Style.TEXT_NORMAL}`)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
UI.divider()
|
|
150
|
-
|
|
151
|
-
// Confirm publish
|
|
152
|
-
const choice = await UI.waitEnter("Press Enter to publish, or Esc to skip")
|
|
153
|
-
|
|
154
|
-
if (choice === "escape") {
|
|
155
|
-
await UI.typeText("Skipped. You can publish later with /publish in the app.")
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Publish with shimmer
|
|
160
|
-
const publishPromise = McpBridge.callTool("auto_post", { dry_run: false })
|
|
161
|
-
await shimmerLine("Publishing your post...", 1500)
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const result = await publishPromise
|
|
165
|
-
console.log("")
|
|
166
|
-
|
|
167
|
-
// Extract URL and details from result
|
|
168
|
-
const urlMatch = result.match(/(?:URL|View at|view at)[:\s]*(https?:\/\/\S+)/i)
|
|
169
|
-
if (urlMatch) {
|
|
170
|
-
UI.success("Post published successfully!")
|
|
171
|
-
console.log("")
|
|
172
|
-
if (title) {
|
|
173
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${title}${UI.Style.TEXT_NORMAL}`)
|
|
174
|
-
}
|
|
175
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}${urlMatch[1]}${UI.Style.TEXT_NORMAL}`)
|
|
176
|
-
console.log("")
|
|
177
|
-
await UI.typeText("Your first post is live! Others can now read, comment, and vote on it.", { charDelay: 10 })
|
|
178
|
-
} else {
|
|
179
|
-
UI.success("Post published!")
|
|
180
|
-
// Fallback: show cleaned result
|
|
181
|
-
const cleanResult = UI.cleanMarkdown(result)
|
|
182
|
-
for (const line of cleanResult.split("\n").slice(0, 5)) {
|
|
183
|
-
if (line.trim()) console.log(` ${line.trim()}`)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch (err) {
|
|
187
|
-
UI.error(`Publish failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
188
|
-
await UI.typeText("You can try again later with /publish.")
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ─── AI Configuration ────────────────────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
async function aiQuickConfigPrompt(): Promise<void> {
|
|
195
|
-
const { AIProvider } = await import("../../ai/provider")
|
|
196
|
-
const hasKey = await AIProvider.hasAnyKey()
|
|
197
|
-
|
|
198
|
-
if (hasKey) {
|
|
199
|
-
UI.success("AI provider already configured!")
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
UI.divider()
|
|
204
|
-
|
|
205
|
-
await UI.typeText("One more thing — would you like to configure an AI chat provider?")
|
|
206
|
-
console.log("")
|
|
207
|
-
await UI.typeText("With AI configured, you can interact with the forum using natural language:", { charDelay: 8 })
|
|
208
|
-
console.log(` ${UI.Style.TEXT_DIM}"Show me trending posts about TypeScript"${UI.Style.TEXT_NORMAL}`)
|
|
209
|
-
console.log(` ${UI.Style.TEXT_DIM}"Analyze my latest coding session"${UI.Style.TEXT_NORMAL}`)
|
|
210
|
-
console.log(` ${UI.Style.TEXT_DIM}"Write a post about my React refactoring"${UI.Style.TEXT_NORMAL}`)
|
|
211
|
-
console.log("")
|
|
212
|
-
|
|
213
|
-
const choice = await UI.waitEnter("Press Enter to configure AI, or Esc to skip")
|
|
214
|
-
|
|
215
|
-
if (choice === "escape") {
|
|
216
|
-
console.log("")
|
|
217
|
-
await UI.typeText("No problem! You can configure AI later with /ai in the app.")
|
|
218
|
-
console.log("")
|
|
219
|
-
await UI.typeText("Even without AI, you can use slash commands to interact:", { charDelay: 8 })
|
|
220
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/scan${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Scan IDE sessions${UI.Style.TEXT_NORMAL}`)
|
|
221
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/publish${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Publish a post${UI.Style.TEXT_NORMAL}`)
|
|
222
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/feed${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Browse the forum${UI.Style.TEXT_NORMAL}`)
|
|
223
|
-
console.log(` ${UI.Style.TEXT_HIGHLIGHT}/theme${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}Change color theme${UI.Style.TEXT_NORMAL}`)
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// AI config flow: URL → Key with ESC support
|
|
228
|
-
console.log("")
|
|
229
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}API URL${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(or press Enter to skip URL, Esc to cancel):${UI.Style.TEXT_NORMAL}`)
|
|
230
|
-
const urlResult = await UI.inputWithEscape(` ${UI.Style.TEXT_HIGHLIGHT}❯ ${UI.Style.TEXT_NORMAL}`)
|
|
231
|
-
|
|
232
|
-
if (urlResult === null) {
|
|
233
|
-
// User pressed Esc
|
|
234
|
-
console.log("")
|
|
235
|
-
await UI.typeText("Skipped AI configuration. You can configure later with /ai in the app.")
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const url = urlResult.trim()
|
|
240
|
-
|
|
241
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}API Key${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(press Esc to cancel):${UI.Style.TEXT_NORMAL}`)
|
|
242
|
-
const keyResult = await UI.inputWithEscape(` ${UI.Style.TEXT_HIGHLIGHT}❯ ${UI.Style.TEXT_NORMAL}`)
|
|
243
|
-
|
|
244
|
-
if (keyResult === null) {
|
|
245
|
-
// User pressed Esc
|
|
246
|
-
console.log("")
|
|
247
|
-
await UI.typeText("Skipped AI configuration. You can configure later with /ai in the app.")
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const key = keyResult.trim()
|
|
252
|
-
|
|
253
|
-
// Both empty → friendly skip
|
|
254
|
-
if (!url && !key) {
|
|
255
|
-
console.log("")
|
|
256
|
-
UI.info("No AI configuration provided — skipping for now.")
|
|
257
|
-
await UI.typeText("You can configure AI later with /ai in the app.")
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Key empty but URL provided → friendly skip
|
|
262
|
-
if (!key) {
|
|
263
|
-
console.log("")
|
|
264
|
-
UI.info("No API key provided — skipping AI configuration.")
|
|
265
|
-
await UI.typeText("You can configure AI later with /ai in the app.")
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (key.length < 5) {
|
|
270
|
-
UI.warn("API key seems too short, skipping AI configuration.")
|
|
271
|
-
await UI.typeText("You can configure AI later with /ai in the app.")
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
const { saveProvider } = await import("../../ai/configure")
|
|
277
|
-
await shimmerLine("Detecting API format...", 1500)
|
|
278
|
-
const result = await saveProvider(url, key)
|
|
279
|
-
if (result.error) {
|
|
280
|
-
UI.warn(result.error)
|
|
281
|
-
await UI.typeText("You can try again later with /ai in the app.")
|
|
282
|
-
} else {
|
|
283
|
-
UI.success(`AI configured! (${result.provider})`)
|
|
284
|
-
}
|
|
285
|
-
} catch (err) {
|
|
286
|
-
UI.warn(`Configuration failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
287
|
-
await UI.typeText("You can try again later with /ai in the app.")
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
type WizardMode = "quick" | "manual"
|
|
292
|
-
|
|
293
|
-
interface ProviderChoice {
|
|
294
|
-
name: string
|
|
295
|
-
providerID: string
|
|
296
|
-
api: "anthropic" | "openai" | "google" | "openai-compatible"
|
|
297
|
-
baseURL?: string
|
|
298
|
-
hint?: string
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const PROVIDER_CHOICES: ProviderChoice[] = [
|
|
302
|
-
{ name: "OpenAI", providerID: "openai", api: "openai", baseURL: "https://api.openai.com", hint: "Codex OAuth + API key style" },
|
|
303
|
-
{ name: "Anthropic", providerID: "anthropic", api: "anthropic", baseURL: "https://api.anthropic.com", hint: "Claude API key" },
|
|
304
|
-
{ name: "Google", providerID: "google", api: "google", baseURL: "https://generativelanguage.googleapis.com", hint: "Gemini API key" },
|
|
305
|
-
{ name: "OpenRouter", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://openrouter.ai/api", hint: "OpenAI-compatible" },
|
|
306
|
-
{ name: "vLLM", providerID: "openai-compatible", api: "openai-compatible", baseURL: "http://127.0.0.1:8000", hint: "Local/self-hosted OpenAI-compatible" },
|
|
307
|
-
{ name: "MiniMax", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://api.minimax.io", hint: "OpenAI-compatible endpoint" },
|
|
308
|
-
{ name: "Moonshot AI (Kimi K2.5)", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://api.moonshot.ai", hint: "OpenAI-compatible endpoint" },
|
|
309
|
-
{ name: "xAI (Grok)", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://api.x.ai", hint: "OpenAI-compatible endpoint" },
|
|
310
|
-
{ name: "Qianfan", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://qianfan.baidubce.com", hint: "OpenAI-compatible endpoint" },
|
|
311
|
-
{ name: "Vercel AI Gateway", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://ai-gateway.vercel.sh", hint: "OpenAI-compatible endpoint" },
|
|
312
|
-
{ name: "OpenCode Zen", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://opencode.ai/zen", hint: "OpenAI-compatible endpoint" },
|
|
313
|
-
{ name: "Xiaomi", providerID: "anthropic", api: "anthropic", baseURL: "https://api.xiaomimimo.com/anthropic", hint: "Anthropic-compatible endpoint" },
|
|
314
|
-
{ name: "Synthetic", providerID: "anthropic", api: "anthropic", baseURL: "https://api.synthetic.new", hint: "Anthropic-compatible endpoint" },
|
|
315
|
-
{ name: "Together AI", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://api.together.xyz", hint: "OpenAI-compatible endpoint" },
|
|
316
|
-
{ name: "Hugging Face", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://router.huggingface.co", hint: "OpenAI-compatible endpoint" },
|
|
317
|
-
{ name: "Venice AI", providerID: "openai-compatible", api: "openai-compatible", baseURL: "https://api.venice.ai/api", hint: "OpenAI-compatible endpoint" },
|
|
318
|
-
{ name: "LiteLLM", providerID: "openai-compatible", api: "openai-compatible", baseURL: "http://localhost:4000", hint: "Unified OpenAI-compatible gateway" },
|
|
319
|
-
{ name: "Cloudflare AI Gateway", providerID: "anthropic", api: "anthropic", hint: "Enter full Anthropic gateway URL manually" },
|
|
320
|
-
{ name: "Custom Provider", providerID: "openai-compatible", api: "openai-compatible", hint: "Any OpenAI-compatible URL" },
|
|
321
|
-
]
|
|
322
|
-
|
|
323
|
-
async function fetchOpenAIModels(baseURL: string, key: string): Promise<string[]> {
|
|
324
|
-
try {
|
|
325
|
-
const clean = baseURL.replace(/\/+$/, "")
|
|
326
|
-
const url = clean.endsWith("/v1") ? `${clean}/models` : `${clean}/v1/models`
|
|
327
|
-
const r = await fetch(url, {
|
|
328
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
329
|
-
signal: AbortSignal.timeout(8000),
|
|
330
|
-
})
|
|
331
|
-
if (!r.ok) return []
|
|
332
|
-
const data = await r.json() as { data?: Array<{ id: string }> }
|
|
333
|
-
return data.data?.map((m) => m.id) || []
|
|
334
|
-
} catch {
|
|
335
|
-
return []
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function isOfficialOpenAIBase(baseURL: string): boolean {
|
|
340
|
-
try {
|
|
341
|
-
const u = new URL(baseURL)
|
|
342
|
-
return u.hostname === "api.openai.com"
|
|
343
|
-
} catch {
|
|
344
|
-
return false
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: string): Promise<{ ok: boolean; detail: string; detectedApi?: Config.ModelApi }> {
|
|
349
|
-
try {
|
|
350
|
-
if (choice.api === "anthropic") {
|
|
351
|
-
const clean = baseURL.replace(/\/+$/, "")
|
|
352
|
-
const r = await fetch(`${clean}/v1/messages`, {
|
|
353
|
-
method: "POST",
|
|
354
|
-
headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json" },
|
|
355
|
-
body: JSON.stringify({ model: "claude-3-5-haiku-latest", max_tokens: 1, messages: [{ role: "user", content: "ping" }] }),
|
|
356
|
-
signal: AbortSignal.timeout(8000),
|
|
357
|
-
})
|
|
358
|
-
if (r.status !== 404) return { ok: true, detail: `Anthropic endpoint reachable (${r.status})`, detectedApi: "anthropic" }
|
|
359
|
-
return { ok: false, detail: "Anthropic endpoint returned 404" }
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (choice.api === "google") {
|
|
363
|
-
const clean = baseURL.replace(/\/+$/, "")
|
|
364
|
-
const r = await fetch(`${clean}/v1beta/models?key=${encodeURIComponent(key)}`, {
|
|
365
|
-
signal: AbortSignal.timeout(8000),
|
|
366
|
-
})
|
|
367
|
-
if (r.ok || r.status === 401 || r.status === 403) return { ok: true, detail: `Google endpoint reachable (${r.status})` }
|
|
368
|
-
return { ok: false, detail: `Google endpoint responded ${r.status}` }
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const { probe } = await import("../../ai/configure")
|
|
372
|
-
const detected = await probe(baseURL, key)
|
|
373
|
-
if (detected === "anthropic") return { ok: true, detail: "Detected Anthropic API format", detectedApi: "anthropic" }
|
|
374
|
-
if (detected === "openai") {
|
|
375
|
-
const detectedApi: Config.ModelApi =
|
|
376
|
-
choice.providerID === "openai" && isOfficialOpenAIBase(baseURL)
|
|
377
|
-
? "openai"
|
|
378
|
-
: "openai-compatible"
|
|
379
|
-
return { ok: true, detail: "Detected OpenAI API format", detectedApi }
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const models = await fetchOpenAIModels(baseURL, key)
|
|
383
|
-
if (models.length > 0) {
|
|
384
|
-
const detectedApi: Config.ModelApi =
|
|
385
|
-
choice.providerID === "openai" && isOfficialOpenAIBase(baseURL)
|
|
386
|
-
? "openai"
|
|
387
|
-
: "openai-compatible"
|
|
388
|
-
return { ok: true, detail: `Model endpoint reachable (${models.length} models)`, detectedApi }
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return { ok: false, detail: "Could not detect endpoint format or list models" }
|
|
392
|
-
} catch (err) {
|
|
393
|
-
return { ok: false, detail: err instanceof Error ? err.message : String(err) }
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function chooseProvider(): Promise<ProviderChoice | undefined> {
|
|
398
|
-
console.log("")
|
|
399
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Model/auth provider${UI.Style.TEXT_NORMAL}`)
|
|
400
|
-
const idx = await UI.select(
|
|
401
|
-
" Choose a provider",
|
|
402
|
-
[...PROVIDER_CHOICES.map((p) => p.hint ? `${p.name} (${p.hint})` : p.name), "Skip for now"],
|
|
403
|
-
)
|
|
404
|
-
if (idx < 0 || idx >= PROVIDER_CHOICES.length) return undefined
|
|
405
|
-
return PROVIDER_CHOICES[idx]
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
async function chooseModel(choice: ProviderChoice, mode: WizardMode, baseURL: string, key: string): Promise<string | undefined> {
|
|
409
|
-
const { AIProvider } = await import("../../ai/provider")
|
|
410
|
-
const builtin = Object.values(AIProvider.BUILTIN_MODELS).filter((m) => m.providerID === choice.providerID).map((m) => m.id)
|
|
411
|
-
const openaiCustom = choice.providerID === "openai" && !isOfficialOpenAIBase(baseURL)
|
|
412
|
-
const useRemote = choice.providerID === "openai-compatible" || openaiCustom
|
|
413
|
-
|
|
414
|
-
if (mode === "quick") {
|
|
415
|
-
if (choice.providerID === "anthropic") return "claude-sonnet-4-20250514"
|
|
416
|
-
if (choice.providerID === "openai" && !openaiCustom) return "gpt-4o-mini"
|
|
417
|
-
if (choice.providerID === "google") return "gemini-2.5-flash"
|
|
418
|
-
const remote = await fetchOpenAIModels(baseURL, key)
|
|
419
|
-
return remote[0] || "gpt-4o-mini"
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
let options = builtin
|
|
423
|
-
if (useRemote) {
|
|
424
|
-
const remote = await fetchOpenAIModels(baseURL, key)
|
|
425
|
-
options = remote
|
|
426
|
-
}
|
|
427
|
-
if (options.length === 0) {
|
|
428
|
-
const typed = await UI.input(` Model ID: `)
|
|
429
|
-
return typed.trim() || "gpt-4o-mini"
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const idx = await UI.select(" Choose a model", [...options, "Custom model id"])
|
|
433
|
-
if (idx < 0) return undefined
|
|
434
|
-
if (idx >= options.length) {
|
|
435
|
-
const typed = await UI.input(` Model ID: `)
|
|
436
|
-
return typed.trim() || options[0]!
|
|
437
|
-
}
|
|
438
|
-
return options[idx]!
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
export async function runAISetupWizard(source: "setup" | "command" = "command"): Promise<void> {
|
|
442
|
-
const { AIProvider } = await import("../../ai/provider")
|
|
443
|
-
const hasKey = await AIProvider.hasAnyKey()
|
|
444
|
-
|
|
445
|
-
UI.divider()
|
|
446
|
-
if (source === "setup") {
|
|
447
|
-
await UI.typeText("AI onboarding")
|
|
448
|
-
} else {
|
|
449
|
-
await UI.typeText("CodeBlog AI setup wizard")
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (hasKey) {
|
|
453
|
-
const keep = await UI.waitEnter("AI is already configured. Press Enter to reconfigure, or Esc to keep current config")
|
|
454
|
-
if (keep === "escape") return
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
console.log("")
|
|
458
|
-
const modeIdx = await UI.select(" Onboarding mode", ["QuickStart (recommended)", "Manual", "Skip for now"])
|
|
459
|
-
if (modeIdx < 0 || modeIdx === 2) {
|
|
460
|
-
UI.info("Skipped AI setup.")
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
const mode = modeIdx === 0 ? "quick" : "manual"
|
|
464
|
-
|
|
465
|
-
const provider = await chooseProvider()
|
|
466
|
-
if (!provider) {
|
|
467
|
-
UI.info("Skipped AI setup.")
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
if (provider.hint) UI.info(`${provider.name}: ${provider.hint}`)
|
|
471
|
-
|
|
472
|
-
const defaultBaseURL = provider.baseURL || ""
|
|
473
|
-
const needsBasePrompt =
|
|
474
|
-
mode === "manual" ||
|
|
475
|
-
provider.providerID === "openai-compatible" ||
|
|
476
|
-
provider.providerID === "openai" ||
|
|
477
|
-
!defaultBaseURL
|
|
478
|
-
let baseURL = defaultBaseURL
|
|
479
|
-
|
|
480
|
-
if (needsBasePrompt) {
|
|
481
|
-
const endpointHint = defaultBaseURL ? ` [${defaultBaseURL}]` : ""
|
|
482
|
-
const entered = await UI.inputWithEscape(` Endpoint base URL${endpointHint}: `)
|
|
483
|
-
if (entered === null) {
|
|
484
|
-
UI.info("Skipped AI setup.")
|
|
485
|
-
return
|
|
486
|
-
}
|
|
487
|
-
baseURL = entered.trim() || defaultBaseURL
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const keyRaw = await UI.inputWithEscape(` API key / Bearer token: `)
|
|
491
|
-
if (keyRaw === null) {
|
|
492
|
-
UI.info("Skipped AI setup.")
|
|
493
|
-
return
|
|
494
|
-
}
|
|
495
|
-
const key = keyRaw.trim()
|
|
496
|
-
if (!key || key.length < 5) {
|
|
497
|
-
UI.warn("Credential seems invalid, setup skipped.")
|
|
498
|
-
return
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (!baseURL) {
|
|
502
|
-
UI.warn("Endpoint URL is required for this provider.")
|
|
503
|
-
return
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
let verified = false
|
|
507
|
-
let detectedApi: Config.ModelApi | undefined
|
|
508
|
-
|
|
509
|
-
while (!verified) {
|
|
510
|
-
await shimmerLine("Verifying endpoint...", 900)
|
|
511
|
-
const verify = await verifyEndpoint(provider, baseURL, key)
|
|
512
|
-
detectedApi = verify.detectedApi
|
|
513
|
-
if (verify.ok) {
|
|
514
|
-
UI.success(verify.detail)
|
|
515
|
-
verified = true
|
|
516
|
-
break
|
|
517
|
-
}
|
|
518
|
-
UI.warn(`Endpoint verification failed: ${verify.detail}`)
|
|
519
|
-
const retry = await UI.waitEnter("Press Enter to retry verification, or Esc to continue anyway")
|
|
520
|
-
if (retry === "escape") break
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const selectedModel = await chooseModel(provider, mode, baseURL, key)
|
|
524
|
-
if (!selectedModel) {
|
|
525
|
-
UI.info("Skipped AI setup.")
|
|
526
|
-
return
|
|
527
|
-
}
|
|
528
|
-
const cfg = await Config.load()
|
|
529
|
-
const providers = cfg.providers || {}
|
|
530
|
-
const resolvedApi = detectedApi || provider.api
|
|
531
|
-
const resolvedCompat = provider.providerID === "openai-compatible" && resolvedApi === "openai"
|
|
532
|
-
? "openai-compatible"
|
|
533
|
-
: resolvedApi
|
|
534
|
-
const providerConfig: Config.ProviderConfig = {
|
|
535
|
-
api_key: key,
|
|
536
|
-
api: resolvedApi,
|
|
537
|
-
compat_profile: resolvedCompat,
|
|
538
|
-
}
|
|
539
|
-
if (baseURL) providerConfig.base_url = baseURL
|
|
540
|
-
providers[provider.providerID] = providerConfig
|
|
541
|
-
|
|
542
|
-
const model = provider.providerID === "openai-compatible" && !selectedModel.includes("/")
|
|
543
|
-
? `openai-compatible/${selectedModel}`
|
|
544
|
-
: selectedModel
|
|
545
|
-
|
|
546
|
-
await Config.save({
|
|
547
|
-
providers,
|
|
548
|
-
default_provider: provider.providerID,
|
|
549
|
-
model,
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
UI.success(`AI configured: ${provider.name} (${model})`)
|
|
553
|
-
console.log(` ${UI.Style.TEXT_DIM}You can rerun this wizard with: codeblog ai setup${UI.Style.TEXT_NORMAL}`)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// ─── Setup Command ───────────────────────────────────────────────────────────
|
|
557
|
-
|
|
558
|
-
export const SetupCommand: CommandModule = {
|
|
559
|
-
command: "setup",
|
|
560
|
-
describe: "First-time setup wizard: authenticate, scan, publish, configure AI",
|
|
561
|
-
handler: async () => {
|
|
562
|
-
// Phase 1: Welcome
|
|
563
|
-
console.log(UI.logo())
|
|
564
|
-
await UI.typeText("Welcome to CodeBlog!", { charDelay: 20 })
|
|
565
|
-
await UI.typeText("The AI-powered coding forum in your terminal.", { charDelay: 15 })
|
|
566
|
-
console.log("")
|
|
567
|
-
|
|
568
|
-
// Phase 2: Authentication
|
|
569
|
-
const alreadyAuthed = await Auth.authenticated()
|
|
570
|
-
let authenticated = alreadyAuthed
|
|
571
|
-
|
|
572
|
-
if (alreadyAuthed) {
|
|
573
|
-
const token = await Auth.get()
|
|
574
|
-
UI.success(`Already authenticated as ${token?.username || "user"}!`)
|
|
575
|
-
} else {
|
|
576
|
-
await UI.typeText("Let's get you set up. First, we need to authenticate your account.")
|
|
577
|
-
await UI.typeText("You may need to sign up or log in on the website first.", { charDelay: 10 })
|
|
578
|
-
console.log("")
|
|
579
|
-
|
|
580
|
-
await UI.waitEnter("Press Enter to open browser...")
|
|
581
|
-
|
|
582
|
-
authenticated = await authBrowser()
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (!authenticated) {
|
|
586
|
-
console.log("")
|
|
587
|
-
UI.info("You can try again with: codeblog setup")
|
|
588
|
-
return
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const token = await Auth.get()
|
|
592
|
-
UI.success(`Authenticated as ${token?.username || "user"}!`)
|
|
593
|
-
|
|
594
|
-
// Phase 3: AI configuration (OpenClaw-like provider chooser)
|
|
595
|
-
UI.divider()
|
|
596
|
-
await UI.typeText("Let's connect your AI provider first.", { charDelay: 10 })
|
|
597
|
-
await UI.typeText("Choose a provider, enter key/endpoint, and we'll verify it.", { charDelay: 10 })
|
|
598
|
-
console.log("")
|
|
599
|
-
await runAISetupWizard("setup")
|
|
600
|
-
|
|
601
|
-
// Phase 4: Interactive scan & publish
|
|
602
|
-
UI.divider()
|
|
603
|
-
|
|
604
|
-
await UI.typeText("Great! Let's see what you've been working on.")
|
|
605
|
-
await UI.typeText("I'll scan your local IDE sessions to find interesting coding experiences.", { charDelay: 10 })
|
|
606
|
-
console.log("")
|
|
607
|
-
|
|
608
|
-
const scanChoice = await UI.waitEnter("Press Enter to continue, or Esc to skip")
|
|
609
|
-
|
|
610
|
-
if (scanChoice === "enter") {
|
|
611
|
-
await scanAndPublish()
|
|
612
|
-
} else {
|
|
613
|
-
await UI.typeText("Skipped. You can scan and publish later in the app.")
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Phase 5: Transition to TUI
|
|
617
|
-
UI.divider()
|
|
618
|
-
setupCompleted = true
|
|
619
|
-
await UI.typeText("All set! Launching CodeBlog...", { charDelay: 20 })
|
|
620
|
-
await Bun.sleep(800)
|
|
621
|
-
},
|
|
622
|
-
}
|
package/src/cli/cmd/tui.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { CommandModule } from "yargs"
|
|
2
|
-
|
|
3
|
-
export const TuiCommand: CommandModule = {
|
|
4
|
-
command: "tui",
|
|
5
|
-
aliases: ["ui"],
|
|
6
|
-
describe: "Launch interactive TUI — browse feed, chat with AI, manage posts",
|
|
7
|
-
builder: (yargs) =>
|
|
8
|
-
yargs
|
|
9
|
-
.option("model", {
|
|
10
|
-
alias: "m",
|
|
11
|
-
describe: "Default AI model",
|
|
12
|
-
type: "string",
|
|
13
|
-
}),
|
|
14
|
-
handler: async (args) => {
|
|
15
|
-
const { tui } = await import("../../tui/app")
|
|
16
|
-
await tui({
|
|
17
|
-
onExit: async () => {},
|
|
18
|
-
})
|
|
19
|
-
},
|
|
20
|
-
}
|