chenbhao Claude Big Pickle commited on
Commit
a457004
Β·
1 Parent(s): 101eacf

fix: desktop server CLI spawn and model listing for CLI auth

Browse files

- Fix CLI process startup failure: resolveCliArgs now prefers the standalone
dist/cli binary (which has the real runHeadless implementation) over the
OSS sidecar binary (which has a stub print.ts without runHeadless).
Uses CLAUDE_APP_ROOT for real filesystem path resolution inside compiled
sidecar where import.meta.dir is a virtual bunfs path.
Fallback changed from broken bin/claude-haha path to PATH lookup.

- Fix desktop /api/models to recognize CLI auth state: added
fetchCliProviderModels() that reads ~/.claude.json directly and fetches
OpenCode models from models.dev/api.json or OpenRouter models from
openrouter.ai/api/v1/models, with 5-minute caching. This makes OpenCode
and OpenRouter models visible in the desktop UI after CLI /login.

Co-Authored-By: Claude Big Pickle <noreply@anthropic.com>

src/server/api/models.ts CHANGED
@@ -161,6 +161,94 @@ function normalizeEffortLevel(value: unknown): (typeof EFFORT_LEVELS)[number] {
161
  : DEFAULT_EFFORT
162
  }
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  // ─── Router ───────────────────────────────────────────────────────────────────
165
 
166
  export async function handleModelsApi(
@@ -217,6 +305,16 @@ async function handleModelsList(): Promise<Response> {
217
  provider: { id: activeProvider.id, name: activeProvider.name },
218
  })
219
  }
 
 
 
 
 
 
 
 
 
 
220
  return Response.json({ models: getStandaloneModelList(), provider: null })
221
  }
222
 
@@ -261,11 +359,16 @@ async function handleCurrentModel(req: Request): Promise<Response> {
261
  const lookupId = contextTier ? `${currentModelId}:${contextTier}` : currentModelId
262
 
263
  // Build available models for name lookup
 
 
 
264
  const availableModels = isOpenAIProviderActive
265
  ? buildOpenAIModelList()
266
  : activeProvider
267
  ? buildProviderModelList(activeProvider.models)
268
- : getStandaloneModelList()
 
 
269
 
270
  const modelEntry = availableModels.find((m) => m.id === lookupId)
271
  || availableModels.find((m) => m.id === currentModelId)
 
161
  : DEFAULT_EFFORT
162
  }
163
 
164
+ // ─── CLI provider model cache (fetched from external APIs) ─────
165
+
166
+ let cliProviderModelCache: ApiModelInfo[] | null = null
167
+ let cliProviderModelCacheTime = 0
168
+ const CLI_PROVIDER_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
169
+
170
+ async function fetchCliProviderModels(): Promise<ApiModelInfo[]> {
171
+ if (cliProviderModelCache && Date.now() - cliProviderModelCacheTime < CLI_PROVIDER_CACHE_TTL) {
172
+ return cliProviderModelCache
173
+ }
174
+
175
+ try {
176
+ // Read ~/.claude.json directly β€” avoids importing CLI-side config modules
177
+ const { homedir } = await import('node:os')
178
+ const { readFileSync } = await import('node:fs')
179
+ const { join } = await import('node:path')
180
+ let config: Record<string, unknown> = {}
181
+ try {
182
+ const raw = readFileSync(join(homedir(), '.claude.json'), 'utf8')
183
+ config = JSON.parse(raw)
184
+ } catch {
185
+ return []
186
+ }
187
+ const authProvider = config.authProvider as string | undefined
188
+
189
+ // ── OpenCode ──────────────────────────────────────────────
190
+ if (authProvider === 'opencode' && config.openCodeApiKey) {
191
+ try {
192
+ const res = await fetch('https://models.dev/api.json')
193
+ if (res.ok) {
194
+ const data = await res.json() as any
195
+ const opencodeModels = data?.opencode?.models || {}
196
+ const models: ApiModelInfo[] = []
197
+
198
+ for (const [modelId, modelCfg] of Object.entries(opencodeModels) as [string, any][]) {
199
+ if (modelCfg.status === 'deprecated') continue
200
+ const isFree = modelCfg.cost?.input === 0 && modelCfg.cost?.output === 0
201
+ models.push({
202
+ id: modelId,
203
+ name: modelCfg.name || modelId,
204
+ description: isFree ? 'Free model' : 'Paid model',
205
+ context: '',
206
+ })
207
+ }
208
+
209
+ if (models.length > 0) {
210
+ cliProviderModelCache = models
211
+ cliProviderModelCacheTime = Date.now()
212
+ return models
213
+ }
214
+ }
215
+ } catch {
216
+ // fall through
217
+ }
218
+ }
219
+
220
+ // ── OpenRouter ────────────────────────────────────────────
221
+ if (authProvider === 'openrouter' && config.openRouterApiKey) {
222
+ try {
223
+ const res = await fetch('https://openrouter.ai/api/v1/models', {
224
+ headers: { Authorization: `Bearer ${config.openRouterApiKey}` },
225
+ })
226
+ if (res.ok) {
227
+ const data = await res.json() as any
228
+ const models: ApiModelInfo[] = (data.data || []).map((m: any) => ({
229
+ id: m.id,
230
+ name: m.name || m.id,
231
+ description: m.description || '',
232
+ context: String(m.context_length || ''),
233
+ }))
234
+
235
+ if (models.length > 0) {
236
+ cliProviderModelCache = models
237
+ cliProviderModelCacheTime = Date.now()
238
+ return models
239
+ }
240
+ }
241
+ } catch {
242
+ // fall through
243
+ }
244
+ }
245
+ } catch {
246
+ // fall through
247
+ }
248
+
249
+ return []
250
+ }
251
+
252
  // ─── Router ───────────────────────────────────────────────────────────────────
253
 
254
  export async function handleModelsApi(
 
305
  provider: { id: activeProvider.id, name: activeProvider.name },
306
  })
307
  }
308
+
309
+ // No cc-haha provider active β€” check if CLI has an auth provider configured
310
+ const cliModels = await fetchCliProviderModels()
311
+ if (cliModels.length > 0) {
312
+ return Response.json({
313
+ models: cliModels,
314
+ provider: { id: 'cli', name: 'CLI Provider' },
315
+ })
316
+ }
317
+
318
  return Response.json({ models: getStandaloneModelList(), provider: null })
319
  }
320
 
 
359
  const lookupId = contextTier ? `${currentModelId}:${contextTier}` : currentModelId
360
 
361
  // Build available models for name lookup
362
+ const cliModelsFallback = !isOpenAIProviderActive && !activeProvider
363
+ ? await fetchCliProviderModels()
364
+ : []
365
  const availableModels = isOpenAIProviderActive
366
  ? buildOpenAIModelList()
367
  : activeProvider
368
  ? buildProviderModelList(activeProvider.models)
369
+ : cliModelsFallback.length > 0
370
+ ? cliModelsFallback
371
+ : getStandaloneModelList()
372
 
373
  const modelEntry = availableModels.find((m) => m.id === lookupId)
374
  || availableModels.find((m) => m.id === currentModelId)
src/server/services/conversationService.ts CHANGED
@@ -1103,6 +1103,27 @@ export class ConversationService {
1103
  }
1104
 
1105
  private resolveCliArgs(baseArgs: string[]): string[] {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1106
  const launcher = resolveClaudeCliLauncher({
1107
  cliPath: process.env.CLAUDE_CLI_PATH,
1108
  execPath: process.execPath,
@@ -1118,7 +1139,8 @@ export class ConversationService {
1118
  ...baseArgs,
1119
  ]
1120
  }
1121
- return [path.resolve(import.meta.dir, '../../../bin/claude-haha'), ...baseArgs]
 
1122
  }
1123
 
1124
  return buildClaudeCliArgs(launcher, baseArgs, process.env.CLAUDE_APP_ROOT)
 
1103
  }
1104
 
1105
  private resolveCliArgs(baseArgs: string[]): string[] {
1106
+ // Prefer the standalone CLI binary (dist/cli) when available. This binary
1107
+ // has the full implementation including runHeadless for SDK mode, unlike
1108
+ // the OSS-build sidecar which uses a stub src/cli/print.ts that doesn't
1109
+ // export runHeadless, causing the subprocess to crash during startup.
1110
+ //
1111
+ // When running inside the compiled sidecar, import.meta.dir resolves to a
1112
+ // virtual bunfs path β€” use CLAUDE_APP_ROOT (set by the sidecar launcher)
1113
+ // to construct the real filesystem path instead.
1114
+ const appRoot = process.env.CLAUDE_APP_ROOT
1115
+ if (appRoot) {
1116
+ const standaloneCli = path.resolve(appRoot, '../../../../dist/cli')
1117
+ if (fs.existsSync(standaloneCli)) {
1118
+ return [standaloneCli, ...baseArgs]
1119
+ }
1120
+ }
1121
+ // Fallback for direct bun dev mode (outside the sidecar)
1122
+ const standaloneCli = path.resolve(import.meta.dir, '../../../dist/cli')
1123
+ if (fs.existsSync(standaloneCli)) {
1124
+ return [standaloneCli, ...baseArgs]
1125
+ }
1126
+
1127
  const launcher = resolveClaudeCliLauncher({
1128
  cliPath: process.env.CLAUDE_CLI_PATH,
1129
  execPath: process.execPath,
 
1139
  ...baseArgs,
1140
  ]
1141
  }
1142
+ // Try claude-haha from PATH (installed via npm/pip)
1143
+ return ['claude-haha', ...baseArgs]
1144
  }
1145
 
1146
  return buildClaudeCliArgs(launcher, baseArgs, process.env.CLAUDE_APP_ROOT)