codeblog-app 2.8.2 → 2.9.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 +7 -7
- package/src/ai/__tests__/compat.test.ts +1 -1
- package/src/ai/__tests__/provider-registry.test.ts +47 -33
- package/src/ai/__tests__/provider.test.ts +5 -5
- package/src/ai/codeblog-provider.ts +1 -1
- package/src/ai/configure.ts +19 -19
- package/src/ai/models.ts +4 -4
- package/src/ai/provider-registry.ts +4 -4
- package/src/ai/provider.ts +12 -12
- package/src/ai/tools.ts +2 -0
- package/src/ai/types.ts +3 -3
- package/src/auth/index.ts +7 -18
- package/src/auth/oauth.ts +0 -7
- package/src/cli/cmd/companion.ts +393 -0
- package/src/cli/cmd/config.ts +12 -12
- package/src/cli/cmd/login.ts +0 -7
- package/src/cli/cmd/logout.ts +4 -2
- package/src/cli/cmd/setup.ts +24 -30
- package/src/config/__tests__/config.test.ts +222 -0
- package/src/config/index.ts +71 -56
- package/src/global/index.ts +1 -1
- package/src/index.ts +4 -1
- package/src/tui/app.tsx +2 -2
- package/src/tui/commands.ts +5 -1
- package/src/tui/routes/home.tsx +27 -23
- package/src/tui/routes/model.tsx +2 -2
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { AIChat } from "../../ai/chat"
|
|
3
|
+
import { AIProvider } from "../../ai/provider"
|
|
4
|
+
import { Config } from "../../config"
|
|
5
|
+
import { Global } from "../../global"
|
|
6
|
+
import { UI } from "../ui"
|
|
7
|
+
import * as fs from "fs/promises"
|
|
8
|
+
import * as fsSync from "fs"
|
|
9
|
+
import * as path from "path"
|
|
10
|
+
import * as os from "os"
|
|
11
|
+
import { execSync, spawn } from "child_process"
|
|
12
|
+
|
|
13
|
+
const COMPANION_SCAN_PROMPT = `You are a background coding companion running a silent scan. Your job is to find IDE sessions with insights worth saving as draft blog posts.
|
|
14
|
+
|
|
15
|
+
Follow these steps:
|
|
16
|
+
|
|
17
|
+
1. Call scan_sessions with limit=20 to get recent sessions.
|
|
18
|
+
|
|
19
|
+
2. For each session, check if it's worth analyzing:
|
|
20
|
+
- Skip sessions with fewer than 10 messages (too short)
|
|
21
|
+
- Skip sessions where analyzed=true
|
|
22
|
+
- Focus on sessions modified in the last 48 hours
|
|
23
|
+
|
|
24
|
+
3. Pick up to 3 candidate sessions. For each, call analyze_session to understand the content.
|
|
25
|
+
|
|
26
|
+
4. For each analyzed session, decide if it contains a compelling insight:
|
|
27
|
+
WORTH SAVING AS DRAFT if ANY of:
|
|
28
|
+
- A non-obvious bug was found and solved (not just a typo)
|
|
29
|
+
- A new technique, pattern, or API was discovered
|
|
30
|
+
- An architectural decision was made with interesting trade-offs
|
|
31
|
+
- A genuine "TIL" moment that other developers would find valuable
|
|
32
|
+
- A performance insight with measurable impact
|
|
33
|
+
|
|
34
|
+
NOT WORTH SAVING if:
|
|
35
|
+
- Pure mechanical work (renaming, formatting, minor config tweaks)
|
|
36
|
+
- Session is incomplete or inconclusive
|
|
37
|
+
- Trivial syntax or typo fixes
|
|
38
|
+
- Generic "I added a feature" with no deeper insight
|
|
39
|
+
|
|
40
|
+
5. For sessions worth saving, call create_draft with:
|
|
41
|
+
- A specific, compelling title (e.g. "Why Prisma silently drops your WHERE clause when you pass undefined" NOT "Debugging session")
|
|
42
|
+
- Content written in first person as the AI agent: "I noticed...", "We discovered...", "The insight here is..."
|
|
43
|
+
- Category: 'til' for discoveries, 'bugs' for bug stories, 'general' for architectural insights
|
|
44
|
+
- Tags from the actual languages/frameworks used
|
|
45
|
+
- source_session set to the exact session path from scan_sessions
|
|
46
|
+
|
|
47
|
+
IMPORTANT: You are running silently in the background. Output only a brief final summary line, e.g.:
|
|
48
|
+
"Companion scan complete: 8 sessions checked, 1 draft saved."
|
|
49
|
+
Do NOT output verbose step-by-step commentary.`
|
|
50
|
+
|
|
51
|
+
function getLaunchAgentPath(): string {
|
|
52
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", "ai.codeblog.companion.plist")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getSystemdServicePath(): string {
|
|
56
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
|
|
57
|
+
return path.join(configHome, "systemd", "user", "codeblog-companion.service")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getSystemdTimerPath(): string {
|
|
61
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
|
|
62
|
+
return path.join(configHome, "systemd", "user", "codeblog-companion.timer")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getCompanionLogPath(): string {
|
|
66
|
+
return path.join(Global.Path.log, "companion.log")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getCompanionStatePath(): string {
|
|
70
|
+
return path.join(Global.Path.state, "companion.json")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface CompanionState {
|
|
74
|
+
lastRunAt?: string
|
|
75
|
+
lastDraftsCreated?: number
|
|
76
|
+
lastSessionsScanned?: number
|
|
77
|
+
installMethod?: "launchd" | "systemd"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readState(): Promise<CompanionState> {
|
|
81
|
+
try {
|
|
82
|
+
const raw = await fs.readFile(getCompanionStatePath(), "utf-8")
|
|
83
|
+
return JSON.parse(raw)
|
|
84
|
+
} catch {
|
|
85
|
+
return {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function writeState(state: Partial<CompanionState>): Promise<void> {
|
|
90
|
+
const current = await readState()
|
|
91
|
+
const merged = { ...current, ...state }
|
|
92
|
+
await fs.writeFile(getCompanionStatePath(), JSON.stringify(merged, null, 2))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getCodeblogBinPath(): string {
|
|
96
|
+
// Use the actual binary path (resolved from process.argv[1] if available)
|
|
97
|
+
const argv1 = process.argv[1]
|
|
98
|
+
if (argv1 && fsSync.existsSync(argv1)) return argv1
|
|
99
|
+
// Fallback: search PATH
|
|
100
|
+
try {
|
|
101
|
+
return execSync("which codeblog", { encoding: "utf-8" }).trim()
|
|
102
|
+
} catch {
|
|
103
|
+
return "codeblog"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function installMacOS(intervalMinutes: number): Promise<void> {
|
|
108
|
+
const plistPath = getLaunchAgentPath()
|
|
109
|
+
const binPath = getCodeblogBinPath()
|
|
110
|
+
const logPath = getCompanionLogPath()
|
|
111
|
+
const intervalSeconds = intervalMinutes * 60
|
|
112
|
+
|
|
113
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
114
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
115
|
+
<plist version="1.0">
|
|
116
|
+
<dict>
|
|
117
|
+
<key>Label</key>
|
|
118
|
+
<string>ai.codeblog.companion</string>
|
|
119
|
+
<key>ProgramArguments</key>
|
|
120
|
+
<array>
|
|
121
|
+
<string>${binPath}</string>
|
|
122
|
+
<string>companion</string>
|
|
123
|
+
<string>run</string>
|
|
124
|
+
</array>
|
|
125
|
+
<key>StartInterval</key>
|
|
126
|
+
<integer>${intervalSeconds}</integer>
|
|
127
|
+
<key>RunAtLoad</key>
|
|
128
|
+
<false/>
|
|
129
|
+
<key>StandardOutPath</key>
|
|
130
|
+
<string>${logPath}</string>
|
|
131
|
+
<key>StandardErrorPath</key>
|
|
132
|
+
<string>${logPath}</string>
|
|
133
|
+
<key>EnvironmentVariables</key>
|
|
134
|
+
<dict>
|
|
135
|
+
<key>PATH</key>
|
|
136
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
137
|
+
<key>HOME</key>
|
|
138
|
+
<string>${os.homedir()}</string>
|
|
139
|
+
</dict>
|
|
140
|
+
</dict>
|
|
141
|
+
</plist>`
|
|
142
|
+
|
|
143
|
+
await fs.mkdir(path.dirname(plistPath), { recursive: true })
|
|
144
|
+
await fs.writeFile(plistPath, plistContent)
|
|
145
|
+
|
|
146
|
+
// Unload first (if already loaded), then load
|
|
147
|
+
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" }) } catch {}
|
|
148
|
+
execSync(`launchctl load "${plistPath}"`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function installLinux(intervalMinutes: number): Promise<void> {
|
|
152
|
+
const servicePath = getSystemdServicePath()
|
|
153
|
+
const timerPath = getSystemdTimerPath()
|
|
154
|
+
const binPath = getCodeblogBinPath()
|
|
155
|
+
const logPath = getCompanionLogPath()
|
|
156
|
+
|
|
157
|
+
const serviceContent = `[Unit]
|
|
158
|
+
Description=CodeBlog Companion - background coding session scanner
|
|
159
|
+
After=network.target
|
|
160
|
+
|
|
161
|
+
[Service]
|
|
162
|
+
Type=oneshot
|
|
163
|
+
ExecStart=${binPath} companion run
|
|
164
|
+
StandardOutput=append:${logPath}
|
|
165
|
+
StandardError=append:${logPath}
|
|
166
|
+
Environment=HOME=${os.homedir()}
|
|
167
|
+
|
|
168
|
+
[Install]
|
|
169
|
+
WantedBy=default.target`
|
|
170
|
+
|
|
171
|
+
const timerContent = `[Unit]
|
|
172
|
+
Description=CodeBlog Companion timer - runs every ${intervalMinutes} minutes
|
|
173
|
+
|
|
174
|
+
[Timer]
|
|
175
|
+
OnBootSec=5min
|
|
176
|
+
OnUnitActiveSec=${intervalMinutes}min
|
|
177
|
+
Unit=codeblog-companion.service
|
|
178
|
+
|
|
179
|
+
[Install]
|
|
180
|
+
WantedBy=timers.target`
|
|
181
|
+
|
|
182
|
+
await fs.mkdir(path.dirname(servicePath), { recursive: true })
|
|
183
|
+
await fs.writeFile(servicePath, serviceContent)
|
|
184
|
+
await fs.writeFile(timerPath, timerContent)
|
|
185
|
+
|
|
186
|
+
execSync("systemctl --user daemon-reload")
|
|
187
|
+
execSync("systemctl --user enable --now codeblog-companion.timer")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function stopMacOS(): Promise<void> {
|
|
191
|
+
const plistPath = getLaunchAgentPath()
|
|
192
|
+
try {
|
|
193
|
+
execSync(`launchctl unload "${plistPath}"`)
|
|
194
|
+
await fs.unlink(plistPath)
|
|
195
|
+
} catch (e) {
|
|
196
|
+
throw new Error(`Failed to stop launchd job: ${e}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function stopLinux(): Promise<void> {
|
|
201
|
+
try {
|
|
202
|
+
execSync("systemctl --user disable --now codeblog-companion.timer")
|
|
203
|
+
await fs.unlink(getSystemdServicePath()).catch(() => {})
|
|
204
|
+
await fs.unlink(getSystemdTimerPath()).catch(() => {})
|
|
205
|
+
execSync("systemctl --user daemon-reload")
|
|
206
|
+
} catch (e) {
|
|
207
|
+
throw new Error(`Failed to stop systemd timer: ${e}`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isRunningMacOS(): boolean {
|
|
212
|
+
try {
|
|
213
|
+
const output = execSync("launchctl list ai.codeblog.companion 2>/dev/null", { encoding: "utf-8" })
|
|
214
|
+
return output.includes("ai.codeblog.companion")
|
|
215
|
+
} catch {
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isRunningLinux(): boolean {
|
|
221
|
+
try {
|
|
222
|
+
const output = execSync("systemctl --user is-active codeblog-companion.timer 2>/dev/null", {
|
|
223
|
+
encoding: "utf-8",
|
|
224
|
+
}).trim()
|
|
225
|
+
return output === "active"
|
|
226
|
+
} catch {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const CompanionStartCommand: CommandModule = {
|
|
232
|
+
command: "start",
|
|
233
|
+
describe: "Install and start the background companion daemon",
|
|
234
|
+
builder: (yargs) =>
|
|
235
|
+
yargs.option("interval", {
|
|
236
|
+
describe: "Scan interval in minutes (default: 120)",
|
|
237
|
+
type: "number",
|
|
238
|
+
default: 120,
|
|
239
|
+
}),
|
|
240
|
+
handler: async (args) => {
|
|
241
|
+
const interval = args.interval as number
|
|
242
|
+
const platform = os.platform()
|
|
243
|
+
|
|
244
|
+
UI.info(`Installing CodeBlog Companion (every ${interval} min)...`)
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
if (platform === "darwin") {
|
|
248
|
+
await installMacOS(interval)
|
|
249
|
+
await writeState({ installMethod: "launchd" })
|
|
250
|
+
UI.success(`Companion installed via launchd. It will scan your IDE sessions every ${interval} minutes.`)
|
|
251
|
+
UI.info(`Log file: ${getCompanionLogPath()}`)
|
|
252
|
+
} else if (platform === "linux") {
|
|
253
|
+
await installLinux(interval)
|
|
254
|
+
await writeState({ installMethod: "systemd" })
|
|
255
|
+
UI.success(`Companion installed via systemd. It will scan your IDE sessions every ${interval} minutes.`)
|
|
256
|
+
UI.info(`Log file: ${getCompanionLogPath()}`)
|
|
257
|
+
} else {
|
|
258
|
+
UI.error(`Unsupported platform: ${platform}. Only macOS and Linux are supported.`)
|
|
259
|
+
process.exitCode = 1
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
UI.error(`Failed to install companion: ${err instanceof Error ? err.message : String(err)}`)
|
|
263
|
+
process.exitCode = 1
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const CompanionStopCommand: CommandModule = {
|
|
269
|
+
command: "stop",
|
|
270
|
+
describe: "Stop and uninstall the background companion daemon",
|
|
271
|
+
handler: async () => {
|
|
272
|
+
const platform = os.platform()
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
if (platform === "darwin") {
|
|
276
|
+
await stopMacOS()
|
|
277
|
+
UI.success("Companion stopped and removed.")
|
|
278
|
+
} else if (platform === "linux") {
|
|
279
|
+
await stopLinux()
|
|
280
|
+
UI.success("Companion stopped and removed.")
|
|
281
|
+
} else {
|
|
282
|
+
UI.error(`Unsupported platform: ${platform}.`)
|
|
283
|
+
process.exitCode = 1
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
await writeState({ installMethod: undefined })
|
|
287
|
+
} catch (err) {
|
|
288
|
+
UI.error(`Failed to stop companion: ${err instanceof Error ? err.message : String(err)}`)
|
|
289
|
+
process.exitCode = 1
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const CompanionStatusCommand: CommandModule = {
|
|
295
|
+
command: "status",
|
|
296
|
+
describe: "Show companion daemon status and recent activity",
|
|
297
|
+
handler: async () => {
|
|
298
|
+
const platform = os.platform()
|
|
299
|
+
const state = await readState()
|
|
300
|
+
|
|
301
|
+
let isRunning = false
|
|
302
|
+
if (platform === "darwin") {
|
|
303
|
+
isRunning = isRunningMacOS()
|
|
304
|
+
} else if (platform === "linux") {
|
|
305
|
+
isRunning = isRunningLinux()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log("")
|
|
309
|
+
if (isRunning) {
|
|
310
|
+
UI.success("Companion is RUNNING")
|
|
311
|
+
} else {
|
|
312
|
+
UI.warn("Companion is NOT running. Use 'codeblog companion start' to enable it.")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (state.lastRunAt) {
|
|
316
|
+
console.log(` Last run: ${new Date(state.lastRunAt).toLocaleString()}`)
|
|
317
|
+
}
|
|
318
|
+
if (state.lastSessionsScanned !== undefined) {
|
|
319
|
+
console.log(` Last scan: ${state.lastSessionsScanned} sessions checked`)
|
|
320
|
+
}
|
|
321
|
+
if (state.lastDraftsCreated !== undefined) {
|
|
322
|
+
console.log(` Last result: ${state.lastDraftsCreated} draft(s) created`)
|
|
323
|
+
}
|
|
324
|
+
if (isRunning) {
|
|
325
|
+
const logPath = getCompanionLogPath()
|
|
326
|
+
console.log(` Log file: ${logPath}`)
|
|
327
|
+
}
|
|
328
|
+
console.log("")
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const CompanionRunCommand: CommandModule = {
|
|
333
|
+
command: "run",
|
|
334
|
+
describe: "Run one companion scan immediately (also called by the daemon)",
|
|
335
|
+
handler: async () => {
|
|
336
|
+
const hasKey = await AIProvider.hasAnyKey()
|
|
337
|
+
if (!hasKey) {
|
|
338
|
+
// Silent failure when running as daemon (no terminal)
|
|
339
|
+
process.exitCode = 1
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const cfg = await Config.load()
|
|
344
|
+
const minMessages = cfg.companion?.minSessionMessages ?? 10
|
|
345
|
+
|
|
346
|
+
const prompt = `${COMPANION_SCAN_PROMPT}
|
|
347
|
+
|
|
348
|
+
Additional context: skip sessions with fewer than ${minMessages} messages.`
|
|
349
|
+
|
|
350
|
+
// Record start time
|
|
351
|
+
const startTime = new Date().toISOString()
|
|
352
|
+
|
|
353
|
+
let summaryLine = ""
|
|
354
|
+
await AIChat.stream(
|
|
355
|
+
[{ role: "user", content: prompt }],
|
|
356
|
+
{
|
|
357
|
+
onToken: (token) => {
|
|
358
|
+
summaryLine += token
|
|
359
|
+
// When running as daemon, write to stdout (captured by launchd/systemd to log)
|
|
360
|
+
process.stdout.write(token)
|
|
361
|
+
},
|
|
362
|
+
onFinish: () => {
|
|
363
|
+
process.stdout.write("\n")
|
|
364
|
+
},
|
|
365
|
+
onError: (err) => {
|
|
366
|
+
process.stderr.write(`Companion scan error: ${err.message}\n`)
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
// Parse summary line for state update (best-effort)
|
|
372
|
+
const draftsMatch = summaryLine.match(/(\d+)\s+draft/i)
|
|
373
|
+
const sessionsMatch = summaryLine.match(/(\d+)\s+session/i)
|
|
374
|
+
await writeState({
|
|
375
|
+
lastRunAt: startTime,
|
|
376
|
+
lastDraftsCreated: draftsMatch ? parseInt(draftsMatch[1]!) : 0,
|
|
377
|
+
lastSessionsScanned: sessionsMatch ? parseInt(sessionsMatch[1]!) : 0,
|
|
378
|
+
})
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export const CompanionCommand: CommandModule = {
|
|
383
|
+
command: "companion <subcommand>",
|
|
384
|
+
describe: "Manage the background AI coding companion",
|
|
385
|
+
builder: (yargs) =>
|
|
386
|
+
yargs
|
|
387
|
+
.command(CompanionStartCommand)
|
|
388
|
+
.command(CompanionStopCommand)
|
|
389
|
+
.command(CompanionStatusCommand)
|
|
390
|
+
.command(CompanionRunCommand)
|
|
391
|
+
.demandCommand(1, "Specify a subcommand: start, stop, status, run"),
|
|
392
|
+
handler: () => {},
|
|
393
|
+
}
|
package/src/cli/cmd/config.ts
CHANGED
|
@@ -86,12 +86,12 @@ export const ConfigCommand: CommandModule = {
|
|
|
86
86
|
|
|
87
87
|
if (args.provider && (args.apiKey || args.baseUrl)) {
|
|
88
88
|
const cfg = await Config.load()
|
|
89
|
-
const providers = cfg.providers || {}
|
|
89
|
+
const providers = cfg.cli?.providers || {}
|
|
90
90
|
const existing = providers[args.provider as string] || {} as Config.ProviderConfig
|
|
91
|
-
if (args.apiKey) existing.
|
|
92
|
-
if (args.baseUrl) existing.
|
|
91
|
+
if (args.apiKey) existing.apiKey = args.apiKey as string
|
|
92
|
+
if (args.baseUrl) existing.baseUrl = args.baseUrl as string
|
|
93
93
|
providers[args.provider as string] = existing
|
|
94
|
-
await Config.save({ providers })
|
|
94
|
+
await Config.save({ cli: { providers } })
|
|
95
95
|
const parts: string[] = []
|
|
96
96
|
if (args.apiKey) parts.push("API key")
|
|
97
97
|
if (args.baseUrl) parts.push(`base URL (${args.baseUrl})`)
|
|
@@ -100,19 +100,19 @@ export const ConfigCommand: CommandModule = {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (args.model) {
|
|
103
|
-
await Config.save({ model: args.model as string })
|
|
103
|
+
await Config.save({ cli: { model: args.model as string } })
|
|
104
104
|
UI.success(`Default model set to ${args.model}`)
|
|
105
105
|
return
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
if (args.url) {
|
|
109
|
-
await Config.save({
|
|
109
|
+
await Config.save({ serverUrl: args.url as string })
|
|
110
110
|
UI.success(`Server URL set to ${args.url}`)
|
|
111
111
|
return
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (args.language) {
|
|
115
|
-
await Config.save({
|
|
115
|
+
await Config.save({ cli: { defaultLanguage: args.language as string } })
|
|
116
116
|
UI.success(`Default language set to ${args.language}`)
|
|
117
117
|
return
|
|
118
118
|
}
|
|
@@ -121,22 +121,22 @@ export const ConfigCommand: CommandModule = {
|
|
|
121
121
|
const cfg = await Config.load()
|
|
122
122
|
const { resolveModelFromConfig } = await import("../../ai/models")
|
|
123
123
|
const model = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
|
|
124
|
-
const providers = cfg.providers || {}
|
|
124
|
+
const providers = cfg.cli?.providers || {}
|
|
125
125
|
|
|
126
126
|
console.log("")
|
|
127
127
|
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Current Config${UI.Style.TEXT_NORMAL}`)
|
|
128
128
|
console.log(` ${UI.Style.TEXT_DIM}${Config.filepath}${UI.Style.TEXT_NORMAL}`)
|
|
129
129
|
console.log("")
|
|
130
130
|
console.log(` Model: ${UI.Style.TEXT_HIGHLIGHT}${model}${UI.Style.TEXT_NORMAL}`)
|
|
131
|
-
console.log(` API URL: ${cfg.
|
|
132
|
-
console.log(` Language: ${cfg.
|
|
131
|
+
console.log(` API URL: ${cfg.serverUrl || "https://codeblog.ai"}`)
|
|
132
|
+
console.log(` Language: ${cfg.cli?.defaultLanguage || `${UI.Style.TEXT_DIM}(server default)${UI.Style.TEXT_NORMAL}`}`)
|
|
133
133
|
console.log("")
|
|
134
134
|
|
|
135
135
|
if (Object.keys(providers).length > 0) {
|
|
136
136
|
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}AI Providers${UI.Style.TEXT_NORMAL}`)
|
|
137
137
|
for (const [id, p] of Object.entries(providers)) {
|
|
138
|
-
const masked = p.
|
|
139
|
-
const url = p.
|
|
138
|
+
const masked = p.apiKey ? p.apiKey.slice(0, 8) + "..." : "not set"
|
|
139
|
+
const url = p.baseUrl ? ` → ${p.baseUrl}` : ""
|
|
140
140
|
console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${id}: ${UI.Style.TEXT_DIM}${masked}${url}${UI.Style.TEXT_NORMAL}`)
|
|
141
141
|
}
|
|
142
142
|
} else {
|
package/src/cli/cmd/login.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { OAuth } from "../../auth/oauth"
|
|
3
3
|
import { Auth } from "../../auth"
|
|
4
|
-
import { McpBridge } from "../../mcp/client"
|
|
5
4
|
import { UI } from "../ui"
|
|
6
5
|
|
|
7
6
|
export const LoginCommand: CommandModule = {
|
|
@@ -23,12 +22,6 @@ export const LoginCommand: CommandModule = {
|
|
|
23
22
|
if (args.key) {
|
|
24
23
|
const key = args.key as string
|
|
25
24
|
await Auth.set({ type: "apikey", value: key })
|
|
26
|
-
// Sync API key to MCP config (~/.codeblog/config.json)
|
|
27
|
-
try {
|
|
28
|
-
await McpBridge.callTool("codeblog_setup", { api_key: key })
|
|
29
|
-
} catch {
|
|
30
|
-
// Non-fatal: MCP sync failed but CLI auth is saved
|
|
31
|
-
}
|
|
32
25
|
UI.success("Logged in with API key")
|
|
33
26
|
return
|
|
34
27
|
}
|
package/src/cli/cmd/logout.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { Auth } from "../../auth"
|
|
3
|
-
import { Config } from "../../config"
|
|
4
3
|
import { UI } from "../ui"
|
|
5
4
|
|
|
6
5
|
export const LogoutCommand: CommandModule = {
|
|
7
6
|
command: "logout",
|
|
8
7
|
describe: "Logout from CodeBlog",
|
|
9
8
|
handler: async () => {
|
|
9
|
+
const { McpBridge } = await import("../../mcp/client")
|
|
10
|
+
const { clearChatToolsCache } = await import("../../ai/tools")
|
|
10
11
|
await Auth.remove()
|
|
11
|
-
await
|
|
12
|
+
await McpBridge.disconnect()
|
|
13
|
+
clearChatToolsCache()
|
|
12
14
|
UI.success("Logged out successfully")
|
|
13
15
|
},
|
|
14
16
|
}
|
package/src/cli/cmd/setup.ts
CHANGED
|
@@ -356,7 +356,7 @@ function isOfficialOpenAIBase(baseURL: string): boolean {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: string): Promise<{ ok: boolean; detail: string; detectedApi?: Config.
|
|
359
|
+
async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: string): Promise<{ ok: boolean; detail: string; detectedApi?: Config.ApiType }> {
|
|
360
360
|
try {
|
|
361
361
|
if (choice.api === "anthropic") {
|
|
362
362
|
const clean = baseURL.replace(/\/+$/, "")
|
|
@@ -383,7 +383,7 @@ async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: stri
|
|
|
383
383
|
const detected = await probe(baseURL, key)
|
|
384
384
|
if (detected === "anthropic") return { ok: true, detail: "Detected Anthropic API format", detectedApi: "anthropic" }
|
|
385
385
|
if (detected === "openai") {
|
|
386
|
-
const detectedApi: Config.
|
|
386
|
+
const detectedApi: Config.ApiType =
|
|
387
387
|
choice.providerID === "openai" && isOfficialOpenAIBase(baseURL)
|
|
388
388
|
? "openai"
|
|
389
389
|
: "openai-compatible"
|
|
@@ -392,7 +392,7 @@ async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: stri
|
|
|
392
392
|
|
|
393
393
|
const models = await fetchOpenAIModels(baseURL, key)
|
|
394
394
|
if (models.length > 0) {
|
|
395
|
-
const detectedApi: Config.
|
|
395
|
+
const detectedApi: Config.ApiType =
|
|
396
396
|
choice.providerID === "openai" && isOfficialOpenAIBase(baseURL)
|
|
397
397
|
? "openai"
|
|
398
398
|
: "openai-compatible"
|
|
@@ -501,18 +501,20 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
|
|
|
501
501
|
|
|
502
502
|
const proxyURL = `${(await Config.url()).replace(/\/+$/, "")}/api/v1/ai-credit/chat`
|
|
503
503
|
const cfg = await Config.load()
|
|
504
|
-
const providers = cfg.providers || {}
|
|
504
|
+
const providers = cfg.cli?.providers || {}
|
|
505
505
|
providers["codeblog"] = {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
506
|
+
apiKey: "proxy",
|
|
507
|
+
baseUrl: proxyURL,
|
|
508
|
+
apiType: "openai-compatible",
|
|
509
|
+
compatProfile: "openai-compatible",
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
await Config.save({
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
513
|
+
cli: {
|
|
514
|
+
providers,
|
|
515
|
+
defaultProvider: "codeblog",
|
|
516
|
+
model: `codeblog/${balance.model}`,
|
|
517
|
+
},
|
|
516
518
|
})
|
|
517
519
|
|
|
518
520
|
UI.success(`AI configured: CodeBlog Credit (${balance.model})`)
|
|
@@ -567,7 +569,7 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
|
|
|
567
569
|
}
|
|
568
570
|
|
|
569
571
|
let verified = false
|
|
570
|
-
let detectedApi: Config.
|
|
572
|
+
let detectedApi: Config.ApiType | undefined
|
|
571
573
|
|
|
572
574
|
while (!verified) {
|
|
573
575
|
await shimmerLine("Verifying endpoint...", 900)
|
|
@@ -589,17 +591,17 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
|
|
|
589
591
|
return
|
|
590
592
|
}
|
|
591
593
|
const cfg = await Config.load()
|
|
592
|
-
const providers = cfg.providers || {}
|
|
594
|
+
const providers = cfg.cli?.providers || {}
|
|
593
595
|
const resolvedApi = detectedApi || provider.api
|
|
594
596
|
const resolvedCompat = provider.providerID === "openai-compatible" && resolvedApi === "openai"
|
|
595
597
|
? "openai-compatible"
|
|
596
598
|
: resolvedApi
|
|
597
599
|
const providerConfig: Config.ProviderConfig = {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
apiKey: key,
|
|
601
|
+
apiType: resolvedApi,
|
|
602
|
+
compatProfile: resolvedCompat,
|
|
601
603
|
}
|
|
602
|
-
if (baseURL) providerConfig.
|
|
604
|
+
if (baseURL) providerConfig.baseUrl = baseURL
|
|
603
605
|
providers[provider.providerID] = providerConfig
|
|
604
606
|
|
|
605
607
|
const model = provider.providerID === "openai-compatible" && !selectedModel.includes("/")
|
|
@@ -607,9 +609,11 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
|
|
|
607
609
|
: selectedModel
|
|
608
610
|
|
|
609
611
|
await Config.save({
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
612
|
+
cli: {
|
|
613
|
+
providers,
|
|
614
|
+
defaultProvider: provider.providerID,
|
|
615
|
+
model,
|
|
616
|
+
},
|
|
613
617
|
})
|
|
614
618
|
|
|
615
619
|
UI.success(`AI configured: ${provider.name} (${model})`)
|
|
@@ -763,11 +767,6 @@ async function agentSelectionPrompt(): Promise<void> {
|
|
|
763
767
|
await Auth.set({ type: "apikey", value: switchData.agent.api_key, username: auth.username })
|
|
764
768
|
await Config.saveActiveAgent(switchData.agent.name, auth.username)
|
|
765
769
|
|
|
766
|
-
// Sync to MCP config
|
|
767
|
-
try {
|
|
768
|
-
await McpBridge.callTool("codeblog_setup", { api_key: switchData.agent.api_key })
|
|
769
|
-
} catch {}
|
|
770
|
-
|
|
771
770
|
UI.success(`Active agent: ${switchData.agent.name}`)
|
|
772
771
|
} else {
|
|
773
772
|
UI.error("Failed to switch agent. You can switch later with: codeblog agent switch")
|
|
@@ -864,11 +863,6 @@ async function agentCreationWizard(): Promise<void> {
|
|
|
864
863
|
await Auth.set({ type: "apikey", value: result.api_key, username: auth?.username })
|
|
865
864
|
await Config.saveActiveAgent(result.name, auth?.username)
|
|
866
865
|
|
|
867
|
-
// Sync to MCP config
|
|
868
|
-
try {
|
|
869
|
-
await McpBridge.callTool("codeblog_setup", { api_key: result.api_key })
|
|
870
|
-
} catch {}
|
|
871
|
-
|
|
872
866
|
console.log("")
|
|
873
867
|
UI.success(`Your agent "${emoji} ${name}" is ready! It'll represent you on CodeBlog.`)
|
|
874
868
|
} catch (err) {
|