diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index 9a98409..69ed780 100644 --- a/apps/supercode-cli/server/package.json +++ b/apps/supercode-cli/server/package.json @@ -1,6 +1,6 @@ { "name": "supercode-cli", - "version": "0.1.67", + "version": "0.1.69", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/package.json.bak2 b/apps/supercode-cli/server/package.json.bak2 new file mode 100644 index 0000000..489c8c3 --- /dev/null +++ b/apps/supercode-cli/server/package.json.bak2 @@ -0,0 +1,84 @@ +{ + "name": "supercode-cli", + "version": "0.1.58", + "description": "AI-powered coding agent CLI", + "main": "dist/main.js", + "bin": { + "supercode": "dist/main.js" + }, + "scripts": { + "dev": "bun src/index.ts", + "dev:cli": "bun src/cli/main.ts", + "start": "bun src/index.ts", + "build": "bun run build:cli", + "build:cli": "bun build --target node --packages bundle --outdir dist --entry ./src/cli/main.ts --external @modelcontextprotocol/sdk && sed -i '' 's|#!/usr/bin/env bun|#!/usr/bin/env node|' dist/main.js", + "test": "bun test", + "typecheck": "tsc --noEmit", + "db:generate": "prisma generate", + "prepublishOnly": "bun run build:cli" + }, + "type": "module", + "files": [ + "dist/main.js", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/yashdev9274/supercli.git" + }, + "keywords": [ + "ai", + "cli", + "coding-agent", + "terminal" + ], + "author": "Yash Dewasthale", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "dependencies": { + "@ai-sdk/google": "^3.0.80", + "@ai-sdk/openai-compatible": "^2.0.48", + "@modelcontextprotocol/sdk": "^1.9.0", + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.8.0", + "@prisma/client-runtime-utils": "^7.8.0", + "@prisma/driver-adapter-utils": "^7.8.0", + "@vercel/connect": "^0.3.2", + "ai": "^6.0.195", + "better-auth": "^1.5.5", + "boxen": "^8.0.1", + "chalk": "^5.6.2", + "commander": "^15.0.0", + "cors": "^2.8.5", + "eve": "^0.22.5", + "express": "^5.1.0", + "marked": "^18.0.4", + "marked-terminal": "^7.3.0", + "open": "^11.0.0", + "vercel-minimax-ai-provider": "^0.0.2", + "yocto-spinner": "^1.2.0", + "zod": "^3.25.2", + "zod-to-json-schema": "^3.25.2" + }, + "devDependencies": { + "@clack/prompts": "^1.5.0", + "@openrouter/ai-sdk-provider": "^2.9.0", + "@openrouter/sdk": "^0.12.79", + "@super/db-terminal": "workspace:*", + "@types/bun": "^1.3.14", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "@types/pg": "^8.18.0", + "api": "^6.1.3", + "dotenv": "^17.3.1", + "oas": "^34.0.1", + "prisma": "^7.4.2", + "typescript": "^5.7.0" + } +} diff --git a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts index abe4b7d..55c335d 100644 --- a/apps/supercode-cli/server/src/cli/ai/chat/chat.ts +++ b/apps/supercode-cli/server/src/cli/ai/chat/chat.ts @@ -355,7 +355,7 @@ async function streamAIResponse( // previous noisy per-tool debug lines. const statusRow = new StepStatusRow() const agentName = mode === "plan" ? "plan" : (mode === "chat" ? "chat" : "build") - statusRow.start(agentName, provider.modelName) + statusRow.start(agentName, provider.modelName, provider.connectionType) // Publish to the module-scoped slot so the persistent footer's resize // handler can notify us too โ€” StepStatusRow reserves no row of its own, // but its render math depends on the current terminal width. @@ -827,8 +827,9 @@ async function streamAIResponse( const elapsedStr = elapsed < 1000 ? `${elapsed}ms` : `${(elapsed / 1000).toFixed(1)}s` const modeLabel = mode === "plan" ? "plan" : (mode === "chat" ? "chat" : "build") + const connBadge = provider.connectionType === "direct" ? " ๐Ÿ”‘" : " โ˜๏ธ" console.log( - ` ${chalk.hex(theme.green)("โ–ฃ")} ${chalk.hex(theme.greenMute)(modeLabel)} ${chalk.hex(theme.greenDim)("ยท")} ${chalk.hex(theme.muted)(provider.modelName)} ${chalk.hex(theme.greenDim)("ยท")} ${chalk.hex(theme.muted)(elapsedStr)}`, + ` ${chalk.hex(theme.green)("โ–ฃ")} ${chalk.hex(theme.greenMute)(modeLabel)} ${chalk.hex(theme.greenDim)("ยท")} ${chalk.hex(theme.muted)(provider.modelName)}${chalk.hex(theme.amber)(connBadge)} ${chalk.hex(theme.greenDim)("ยท")} ${chalk.hex(theme.muted)(elapsedStr)}`, ) console.log() return { @@ -1902,17 +1903,25 @@ export async function chatLoop( provider = newProvider contextWindow = getContextWindow(provider.modelName) const label = result.label || provider.modelName - footer.setModel(provider.modelName) + footer.setModel(provider.modelName) + footer.setConnectionType(provider.connectionType) footer.setContextWindow(contextWindow) process.stdout.write(`\r\n ${chalk.hex(theme.green)("โ—†")} switched to ${chalk.hex(theme.green)(label)}\r\n\n`) saveCliConfig({ provider: result.provider!, model: result.model || provider.modelName, mode: conversation.mode as "chat" | "agent" }) } } else if (result?.type === "connect") { if (result.provider) { - const newProvider = createProvider(result.provider, provider.modelName) + const modelToUse = result.model || provider.modelName + const newProvider = createProvider(result.provider, modelToUse) if (newProvider) { provider = newProvider contextWindow = getContextWindow(provider.modelName) + footer.setModel(provider.modelName) + footer.setConnectionType(provider.connectionType) + footer.setContextWindow(contextWindow) + const label = `${provider.name} ยท ${provider.modelName}` + process.stdout.write(`\r\n ${chalk.hex(theme.green)("โ—†")} switched to ${chalk.hex(theme.green)(label)}\r\n`) + saveCliConfig({ provider: result.provider!, model: modelToUse, mode: conversation.mode as "chat" | "agent" }) } } process.stdout.write(`\r\n`) @@ -2225,7 +2234,7 @@ export async function chatLoop( } export async function startChat( - provider: ModelProvider = "concentrateai", + provider: ModelProvider = "supercode", model?: string, conversationId?: string | null, workspaceInfo?: WorkspaceInfo, diff --git a/apps/supercode-cli/server/src/cli/ai/chat/chatAgent.ts b/apps/supercode-cli/server/src/cli/ai/chat/chatAgent.ts index 332c017..c7d831a 100644 --- a/apps/supercode-cli/server/src/cli/ai/chat/chatAgent.ts +++ b/apps/supercode-cli/server/src/cli/ai/chat/chatAgent.ts @@ -236,7 +236,7 @@ async function saveMessage(conversationId: string, role: string, content: string } export async function startAgentChat( - provider: ModelProvider = "concentrateai", + provider: ModelProvider = "supercode", model?: string, conversationId: string | null = null, workspaceInfo?: WorkspaceInfo, diff --git a/apps/supercode-cli/server/src/cli/ai/chat/step-status-row.ts b/apps/supercode-cli/server/src/cli/ai/chat/step-status-row.ts index 2ad18d7..ddc79d9 100644 --- a/apps/supercode-cli/server/src/cli/ai/chat/step-status-row.ts +++ b/apps/supercode-cli/server/src/cli/ai/chat/step-status-row.ts @@ -41,6 +41,7 @@ export class StepStatusRow { private frameIndex = 0 private agentName = "" private modelName = "" + private connectionType = "" private phase: StepPhase = "idle" private currentToolName = "" private currentToolArgs: unknown = undefined @@ -53,11 +54,12 @@ export class StepStatusRow { // Simple ETA: rolling average of tool durations per tool type (ms) private toolDurationHistory = new Map() - start(agentName: string, modelName: string) { + start(agentName: string, modelName: string, connectionType: string = "") { if (this.running) return this.running = true this.agentName = agentName this.modelName = modelName + this.connectionType = connectionType this.phase = "thinking" this.currentToolName = "" this.currentToolArgs = undefined @@ -171,7 +173,7 @@ export class StepStatusRow { } // Simple ETA: average of past durations ร— estimated remaining steps const estimatedRemaining = this.estimateRemaining() - if (estimatedRemaining > 0) { + if (estimatedRemaining) { parts.push(chalk.hex(theme.greenDim)(`~${estimatedRemaining}`)) } } else if (this.agentName) { @@ -180,6 +182,11 @@ export class StepStatusRow { ? `Model processing tool result` : `โ–ฃ ${this.agentName}${this.modelName ? " ยท " + this.modelName : ""}` parts.push(chalk.hex(theme.greenMute)(modelLabel)) + if (this.connectionType === "direct") { + parts.push(chalk.hex(theme.amber)("๐Ÿ”‘")) + } else if (this.connectionType === "proxy") { + parts.push(chalk.hex(theme.amber)("โ˜๏ธ")) + } } else if (this.modelName) { parts.push(chalk.hex(theme.greenGlow)(this.modelName)) } diff --git a/apps/supercode-cli/server/src/cli/ai/provider.ts b/apps/supercode-cli/server/src/cli/ai/provider.ts index eb77b7d..1975de8 100644 --- a/apps/supercode-cli/server/src/cli/ai/provider.ts +++ b/apps/supercode-cli/server/src/cli/ai/provider.ts @@ -12,11 +12,14 @@ import { openRouterConfig } from "../../config/openrouter.config.ts" import { nvidiaConfig } from "../../config/nvidia.config.ts" import { mergedevConfig } from "../../config/mergedev.config.ts" -export type ModelProvider = "google" | "minimax" | "openrouter" | "nvidia" | "concentrateai" | "mergedev" +export type ModelProvider = "supercode" | "google" | "minimax" | "openrouter" | "nvidia" | "concentrateai" | "mergedev" + +export type ConnectionType = "direct" | "proxy" export interface AIProvider { readonly name: string readonly modelName: string + readonly connectionType: ConnectionType readonly model?: any sendMessage( messages: ModelMessage[], @@ -36,7 +39,8 @@ export interface AIProvider { generateObject?(schema: any, prompt: string): Promise<{ object: unknown }> } -const providerMeta: Record = { +export const providerMeta: Record = { + supercode: { env: "", label: "Supercode Cloud", defaultModel: "deepseek-v4-flash" }, google: { env: "GOOGLE_GENERATIVE_AI_API_KEY", label: "Google Gemini", defaultModel: "gemini-2.5-flash", link: "https://aistudio.google.com/apikey" }, minimax: { env: "MINIMAX_API_KEY", label: "MiniMax", defaultModel: "MiniMax-M2" }, openrouter: { env: "OPENROUTER_API_KEY", label: "OpenRouter", defaultModel: "openai/gpt-oss-120b:free", link: "https://openrouter.ai/keys" }, @@ -46,6 +50,7 @@ const providerMeta: Record string> = { + supercode: () => "", google: () => config.googleApiKey, minimax: () => minimaxConfig.apiKey, openrouter: () => openRouterConfig.apiKey, @@ -57,15 +62,25 @@ const providerConfigs: Record string> = { export function createProvider(provider: ModelProvider, model?: string): AIProvider { const meta = providerMeta[provider] + // Only supercode cloud models are allowed through the proxy without an API key. + // All other providers require the user to connect their own key first. if (!providerConfigs[provider]()) { + if (provider === "supercode") { const svc = new ServerProxyService(provider, model || meta.defaultModel) return { name: provider, modelName: model || meta.defaultModel, + connectionType: "proxy", sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish) => svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish), - generateObject: (schema, prompt) => svc.generateObject(schema, prompt), + generateObject: (schema, prompt) => svc.generateObject(schema, prompt), + } } + throw new Error( + `No API key configured for ${meta.label}. ` + + `Run \`/connect\` and select "${meta.label}" to save your key, ` + + `then select this model again.` + ) } switch (provider) { @@ -74,6 +89,7 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi return { name: "google", modelName: svc.modelName, + connectionType: "direct", model: svc.model, sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult) => svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult), @@ -84,6 +100,7 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi return { name: "openrouter", modelName: svc.modelName, + connectionType: "direct", sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult) => svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult), } @@ -93,6 +110,7 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi return { name: "nvidia", modelName: svc.modelName, + connectionType: "direct", model: svc.model, sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish) => svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish), @@ -103,6 +121,7 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi return { name: "concentrateai", modelName: svc.modelName, + connectionType: "direct", model: svc.model, sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish, onStepBudget) => svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish, onStepBudget), @@ -113,6 +132,7 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi return { name: "mergedev", modelName: svc.modelName, + connectionType: "direct", model: svc.model, sendMessage: (messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish) => svc.sendMessage(messages, onChunk, tools, onToolCall, signal, onReasoning, onToolResult, onStepFinish), diff --git a/apps/supercode-cli/server/src/cli/commands/slashCommands/connect.ts b/apps/supercode-cli/server/src/cli/commands/slashCommands/connect.ts index 6f725af..56c0991 100644 --- a/apps/supercode-cli/server/src/cli/commands/slashCommands/connect.ts +++ b/apps/supercode-cli/server/src/cli/commands/slashCommands/connect.ts @@ -1,16 +1,23 @@ -import { select, isCancel, password, note } from "@clack/prompts" +import { select, isCancel, password, note, text } from "@clack/prompts" import chalk from "chalk" import { theme } from "src/cli/utils/tui.ts" -import { saveProviderApiKey } from "src/lib/cli-config.ts" +import { + getCliConfig, + saveProviderApiKey, + getProviderApiKeys, +} from "src/lib/cli-config.ts" import type { ModelProvider } from "src/cli/ai/provider.ts" +import { BYOK_MODELS } from "./model.ts" const PROVIDERS: Array<{ value: ModelProvider; label: string; hint: string; link: string }> = [ { value: "google", label: "Google Gemini", hint: "gemini-2.5 models", link: "https://aistudio.google.com/apikey" }, { value: "openrouter", label: "OpenRouter", hint: "multi-provider access", link: "https://openrouter.ai/keys" }, { value: "nvidia", label: "NVIDIA NIM", hint: "free NVIDIA hosted models", link: "https://build.nvidia.com/explore/discover" }, + { value: "concentrateai", label: "ConcentrateAI", hint: "deepseek-v4 & glm models", link: "https://concentrate.ai" }, + { value: "mergedev", label: "Merge Dev Gateway", hint: "unified API gateway", link: "https://app.merge.dev" }, ] -export async function connectProvider(): Promise<{ type: "connect"; provider?: ModelProvider }> { +export async function connectProvider(): Promise<{ type: "connect"; provider?: ModelProvider; model?: string }> { console.log() console.log(` ${chalk.hex(theme.amber)("โ—†")} ${chalk.hex(theme.green).bold("connect API key")}`) console.log(` ${chalk.hex(theme.muted)("Save an API key so requests go direct instead of through the proxy.")}`) @@ -29,21 +36,77 @@ export async function connectProvider(): Promise<{ type: "connect"; provider?: M const provider = PROVIDERS.find((p) => p.value === providerChoice)! - note(chalk.hex(theme.muted)(`Get your key at: ${provider.link}`), `${provider.label}`) + // Check if user already has a key saved for this provider + const config = await getCliConfig() + const envKeys = getProviderApiKeys() + const existingKey = config?.apiKeys?.[provider.value] || envKeys[provider.value] - const apiKey = await password({ - message: chalk.hex(theme.green)(`paste your ${provider.label} API key`), - validate: (val: string | undefined) => { - if (!val || val.trim().length < 8) return "Key looks too short โ€” please check and try again" - }, - }) + if (existingKey) { + console.log(` ${chalk.hex(theme.green)("โœ“")} ${chalk.hex(theme.green)(`Already have a key for ${provider.label} โ€” switching to direct mode (๐Ÿ”‘)`)}`) + console.log() + } else { + note(chalk.hex(theme.muted)(`Get your key at: ${provider.link}`), `${provider.label}`) - if (isCancel(apiKey)) return { type: "connect" } + const apiKey = await password({ + message: chalk.hex(theme.green)(`paste your ${provider.label} API key`), + validate: (val: string | undefined) => { + if (!val || val.trim().length < 8) return "Key looks too short โ€” please check and try again" + }, + }) - await saveProviderApiKey(provider.value, apiKey as string) + if (isCancel(apiKey)) return { type: "connect" } - console.log(` ${chalk.hex(theme.green)("โœ“")} ${chalk.hex(theme.green)(`${provider.label} API key saved`)}`) - console.log() + await saveProviderApiKey(provider.value, apiKey as string) + + console.log(` ${chalk.hex(theme.green)("โœ“")} ${chalk.hex(theme.green)(`${provider.label} API key saved โ€” requests will now go direct (๐Ÿ”‘) instead of through the proxy (โ˜๏ธ)`)}`) + console.log() + } + + // Model selection via @clack โ€” safe with the chat loop's stdin management + const modelsForProvider = BYOK_MODELS.filter((m) => m.provider === provider.value) + let chosenModel: string | undefined + + if (modelsForProvider.length > 0) { + const modelChoice = await select({ + message: chalk.hex(theme.green)("select model"), + options: [ + ...modelsForProvider.map((m) => ({ + value: m.value, + label: `${m.label} ${chalk.hex(theme.muted)(m.desc)}`, + hint: m.cost ? `${m.cost}x` : "", + })), + { value: "__custom__", label: "Custom model (type the name)", hint: "" }, + ], + }) + if (isCancel(modelChoice)) { + // No model selected + } else if (modelChoice === "__custom__") { + const customName = await text({ + message: chalk.hex(theme.green)(`enter model name for ${provider.label}`), + placeholder: "e.g. claude-sonnet-4", + }) + if (!isCancel(customName) && (customName as string).trim()) { + chosenModel = (customName as string).trim() + } + } else { + chosenModel = modelChoice as string + } + if (chosenModel) { + const label = modelsForProvider.find((m) => m.value === modelChoice)?.label || chosenModel + console.log(` ${chalk.hex(theme.green)("โœ“")} model set to ${chalk.hex(theme.greenGlow)(label)}`) + console.log() + } + } else { + const customName = await text({ + message: chalk.hex(theme.green)(`enter model name for ${provider.label}`), + placeholder: "e.g. claude-sonnet-4", + }) + if (!isCancel(customName) && (customName as string).trim()) { + chosenModel = (customName as string).trim() + console.log(` ${chalk.hex(theme.green)("โœ“")} model set to ${chalk.hex(theme.greenGlow)(chosenModel)}`) + console.log() + } + } - return { type: "connect", provider: provider.value } + return { type: "connect", provider: provider.value, model: chosenModel } } diff --git a/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts b/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts index 28ec416..9f6e445 100644 --- a/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts +++ b/apps/supercode-cli/server/src/cli/commands/slashCommands/help.ts @@ -8,7 +8,11 @@ const COMMANDS = [ { cmd: "/exit", desc: "End the session" }, ] -const PROVIDERS = [ +const CLOUD_SECTION = [ + { name: "Supercode Cloud", models: ["deepseek-v4-flash", "glm-5.2", "glm-5.1", "kimi-k2-6", "minimax-m3"], note: "free ยท no key needed" }, +] + +const BYOK_SECTION = [ { name: "Google Gemini", models: ["gemini-2.5-flash", "gemini-2.5-pro"], note: "free ยท multimodal" }, { name: "OpenRouter", models: ["openai/gpt-oss-120b:free", "deepseek/deepseek-v4-flash", "minimax/minimax-m3", "z-ai/glm-5.1", "moonshotai/kimi-k2.6"], note: "bring your own key" }, { name: "NVIDIA NIM", models: ["minimaxai/minimax-m3", "deepseek-ai/deepseek-v4-flash", "meta/llama-3.3-70b-instruct"], note: "free API" }, @@ -16,6 +20,8 @@ const PROVIDERS = [ { name: "Merge Dev Gateway", models: ["anthropic/claude-opus-4-8", "anthropic/claude-opus-4-7", "anthropic/claude-fable-5", "openai/gpt-5.5", "deepseek/deepseek-v4-flash"], note: "bring your own key" }, ] +const PROVIDERS = [...CLOUD_SECTION, ...BYOK_SECTION] + function renderCommands(): string { const rows = COMMANDS.map((c) => cardRow({ label: c.cmd, description: c.desc, selected: false }) diff --git a/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts b/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts index 1d23ac9..dfb64c2 100644 --- a/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts +++ b/apps/supercode-cli/server/src/cli/commands/slashCommands/model.ts @@ -1,9 +1,9 @@ import * as readline from "node:readline" -import { isCancel, confirm } from "@clack/prompts" +import { isCancel, confirm, text, password } from "@clack/prompts" import chalk from "chalk" import { theme, heavyDivider } from "src/cli/utils/tui.ts" -import type { ModelProvider } from "src/cli/ai/provider.ts" -import { getCliConfig, saveCliConfig } from "src/lib/cli-config.ts" +import { type ModelProvider, providerMeta } from "src/cli/ai/provider.ts" +import { getCliConfig, saveCliConfig, saveProviderApiKey, getProviderApiKeys } from "src/lib/cli-config.ts" interface ModelEntry { value: string @@ -13,23 +13,119 @@ interface ModelEntry { desc: string } -const MODELS: ModelEntry[] = [ - { value: "glm-5.2", label: "GLM 5.2", provider: "concentrateai", cost: "0.5x", desc: "Latest GLM" }, - { value: "kimi-k2-6", label: "Kimi K2.6", provider: "concentrateai", cost: "0.8x", desc: "Long context" }, - { value: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "concentrateai", cost: "1.0x", desc: "Fast & capable" }, - { value: "minimax-m3", label: "MiniMax M3", provider: "concentrateai", cost: "0.5x", desc: "Fast & smart" }, - // { value: "anthropic/claude-opus-4-8", label: "Opus 4.8", provider: "concentrateai", cost: "40x", desc: "Deep reasoning" }, - // { value: "anthropic/claude-opus-4-8", label: "Opus 4.8", provider: "mergedev", cost: "40x", desc: "Via Merge Dev" }, +const SECTION_CLOUD = "__section_cloud__" +const SECTION_BYOK = "__section_byok__" + +// Models available through the Supercode cloud proxy (no API key needed) +export const CLOUD_MODELS: ModelEntry[] = [ + { value: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "supercode", cost: "free", desc: "Fast & capable" }, + { value: "glm-5.2", label: "GLM 5.2", provider: "supercode", cost: "free", desc: "Latest GLM" }, + { value: "glm-5.1", label: "GLM 5.1", provider: "supercode", cost: "free", desc: "Stable & reliable" }, + { value: "kimi-k2-6", label: "Kimi K2.6", provider: "supercode", cost: "free", desc: "Long context" }, + { value: "minimax-m3", label: "MiniMax M3", provider: "supercode", cost: "free", desc: "Fast & smart" }, +] + +// Models available when you bring your own API key (BYOK) +export const BYOK_MODELS: ModelEntry[] = [ + // โ”€โ”€ ConcentrateAI (BYOK) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + { value: "anthropic/claude-opus-4-8", label: "Claude Opus 4.8", provider: "concentrateai", cost: "", desc: "Deep reasoning" }, + { value: "anthropic/claude-opus-4", label: "Claude Opus 4", provider: "concentrateai", cost: "", desc: "Top-tier reasoning" }, + { value: "anthropic/claude-sonnet-4-5", label: "Claude Sonnet 4.5", provider: "concentrateai", cost: "", desc: "Latest sonnet" }, + { value: "anthropic/claude-sonnet-4", label: "Claude Sonnet 4", provider: "concentrateai", cost: "", desc: "Balanced" }, + { value: "anthropic/claude-3-5-haiku", label: "Claude 3.5 Haiku", provider: "concentrateai", cost: "", desc: "Fast & cheap" }, + { value: "openai/gpt-4o", label: "GPT-4o", provider: "concentrateai", cost: "", desc: "OpenAI flagship" }, + { value: "openai/gpt-4o-mini", label: "GPT-4o Mini", provider: "concentrateai", cost: "", desc: "Cheap & fast" }, + { value: "openai/gpt-4-1", label: "GPT-4.1", provider: "concentrateai", cost: "", desc: "Latest GPT" }, + { value: "openai/o3-mini", label: "o3-mini", provider: "concentrateai", cost: "", desc: "Reasoning mini" }, + { value: "openai/o4-mini", label: "o4-mini", provider: "concentrateai", cost: "", desc: "Reasoning v4 mini" }, + { value: "x-ai/grok-4-5", label: "Grok 4.5", provider: "concentrateai", cost: "", desc: "xAI latest" }, + { value: "x-ai/grok-3", label: "Grok 3", provider: "concentrateai", cost: "", desc: "xAI flagship" }, + { value: "x-ai/grok-3-mini", label: "Grok 3 Mini", provider: "concentrateai", cost: "", desc: "Compact Grok" }, + { value: "deepseek/deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "concentrateai", cost: "", desc: "Fast & capable" }, + { value: "deepseek/deepseek-v3", label: "DeepSeek V3", provider: "concentrateai", cost: "", desc: "DeepSeek flagship" }, + { value: "deepseek/deepseek-r1", label: "DeepSeek R1", provider: "concentrateai", cost: "", desc: "Reasoning model" }, + { value: "meta-llama/llama-4-maverick", label: "Llama 4 Maverick", provider: "concentrateai", cost: "", desc: "Latest Llama" }, + { value: "z-ai/glm-5-2", label: "GLM 5.2", provider: "concentrateai", cost: "", desc: "Latest GLM" }, + { value: "moonshotai/kimi-k2-6", label: "Kimi K2.6", provider: "concentrateai", cost: "", desc: "Long context" }, + { value: "minimax/minimax-m3", label: "MiniMax M3", provider: "concentrateai", cost: "", desc: "Fast & smart" }, + + // โ”€โ”€ Merge Dev Gateway โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + { value: "anthropic/claude-sonnet-4", label: "Claude Sonnet 4", provider: "mergedev", cost: "12x", desc: "Via Merge Dev" }, + { value: "anthropic/claude-opus-4-8", label: "Claude Opus 4.8", provider: "mergedev", cost: "40x", desc: "Deep reasoning" }, + { value: "openai/gpt-4o", label: "GPT-4o", provider: "mergedev", cost: "4x", desc: "Via Merge Dev" }, + { value: "x-ai/grok-3", label: "Grok 3", provider: "mergedev", cost: "10x", desc: "Via Merge Dev" }, + { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "mergedev", cost: "2x", desc: "Via Merge Dev" }, + { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "mergedev", cost: "4x", desc: "Via Merge Dev" }, + { value: "deepseek/deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "mergedev", cost: "1.2x", desc: "Via Merge Dev" }, + + // โ”€โ”€ Google Gemini โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google", cost: "2.0x", desc: "Smart & fast" }, { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "google", cost: "4.0x", desc: "Deep reasoning" }, + { value: "gemini-2.0-flash", label: "Gemini 2.0 Flash", provider: "google", cost: "1.5x", desc: "Previous gen" }, + { value: "gemini-2.5-flash-preview", label: "Gemini 2.5 Flash Preview", provider: "google", cost: "2.0x", desc: "Latest preview" }, + { value: "gemini-2.5-pro-preview", label: "Gemini 2.5 Pro Preview", provider: "google", cost: "4.0x", desc: "Max preview" }, + { value: "learnlm-1.5-pro", label: "LearnLM 1.5 Pro", provider: "google", cost: "1.0x", desc: "Teaching optimized" }, + + // โ”€โ”€ MiniMax โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + { value: "MiniMax-M2", label: "MiniMax M2", provider: "minimax", cost: "0.8x", desc: "MiniMax flagship" }, + { value: "MiniMax-M3", label: "MiniMax M3", provider: "minimax", cost: "0.5x", desc: "Fast & smart" }, + + // โ”€โ”€ NVIDIA NIM โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + { value: "meta/llama-3.1-405b-instruct", label: "Llama 3.1 405B", provider: "nvidia", cost: "2.0x", desc: "Via NVIDIA NIM" }, + { value: "meta/llama-3.3-70b-instruct", label: "Llama 3.3 70B", provider: "nvidia", cost: "1.2x", desc: "Open weights" }, + { value: "meta/llama-3.1-70b-instruct", label: "Llama 3.1 70B", provider: "nvidia", cost: "1.0x", desc: "Via NVIDIA NIM" }, + { value: "meta/llama-3.1-8b-instruct", label: "Llama 3.1 8B", provider: "nvidia", cost: "0.5x", desc: "Via NVIDIA NIM" }, + { value: "nvidia/llama-3.1-nemotron-70b-instruct", label: "Nemotron 70B", provider: "nvidia", cost: "1.2x", desc: "RLHF optimized" }, + { value: "nvidia/llama-3.1-nemotron-ultra-253b", label: "Nemotron Ultra 253B", provider: "nvidia", cost: "2.5x", desc: "Biggest NIM" }, + { value: "mistralai/mistral-7b-instruct-v0.3", label: "Mistral 7B", provider: "nvidia", cost: "0.5x", desc: "Via NVIDIA NIM" }, + { value: "qwen/qwen2.5-72b-instruct", label: "Qwen 2.5 72B", provider: "nvidia", cost: "1.2x", desc: "Via NVIDIA NIM" }, { value: "minimaxai/minimax-m3", label: "MiniMax M3", provider: "nvidia", cost: "0.5x", desc: "Via NVIDIA NIM" }, { value: "deepseek-ai/deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "nvidia", cost: "1.0x", desc: "Via NVIDIA NIM" }, - { value: "meta/llama-3.3-70b-instruct", label: "Llama 3.3 70B", provider: "nvidia", cost: "1.2x", desc: "Open weights" }, - { value: "openai/gpt-oss-120b:free", label: "GPT OSS 120B", provider: "openrouter", cost: "free", desc: "OpenAI open-weight" }, + + // โ”€โ”€ OpenRouter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + { value: "anthropic/claude-opus-4-8", label: "Claude Opus 4.8", provider: "openrouter", cost: "40x", desc: "Deep reasoning" }, + { value: "anthropic/claude-opus-4", label: "Claude Opus 4", provider: "openrouter", cost: "30x", desc: "Top-tier reasoning" }, + { value: "anthropic/claude-sonnet-4", label: "Claude Sonnet 4", provider: "openrouter", cost: "12x", desc: "Balanced" }, + { value: "anthropic/claude-sonnet-4.5", label: "Claude Sonnet 4.5", provider: "openrouter", cost: "10x", desc: "Latest sonnet" }, + { value: "anthropic/claude-3.5-haiku", label: "Claude 3.5 Haiku", provider: "openrouter", cost: "3x", desc: "Fast & cheap" }, + { value: "openai/gpt-4o", label: "GPT-4o", provider: "openrouter", cost: "4x", desc: "OpenAI flagship" }, + { value: "openai/gpt-4o-mini", label: "GPT-4o Mini", provider: "openrouter", cost: "0.5x", desc: "Cheap & fast" }, + { value: "openai/gpt-4.1", label: "GPT-4.1", provider: "openrouter", cost: "3x", desc: "Latest GPT" }, + { value: "openai/gpt-4.1-mini", label: "GPT-4.1 Mini", provider: "openrouter", cost: "1x", desc: "Compact GPT" }, + { value: "openai/gpt-4.1-nano", label: "GPT-4.1 Nano", provider: "openrouter", cost: "0.3x", desc: "Tiny & fast" }, + { value: "openai/o3-mini", label: "o3-mini", provider: "openrouter", cost: "3x", desc: "Reasoning mini" }, + { value: "openai/o4-mini", label: "o4-mini", provider: "openrouter", cost: "3x", desc: "Reasoning v4 mini" }, + { value: "openai/gpt-oss-120b:free", label: "GPT OSS 120B", provider: "openrouter", cost: "free", desc: "Open-weight free" }, + { value: "x-ai/grok-3", label: "Grok 3", provider: "openrouter", cost: "10x", desc: "xAI flagship" }, + { value: "x-ai/grok-3-mini", label: "Grok 3 Mini", provider: "openrouter", cost: "5x", desc: "Compact Grok" }, + { value: "x-ai/grok-3-mini-fast", label: "Grok 3 Mini Fast", provider: "openrouter", cost: "5x", desc: "Fast Grok" }, { value: "deepseek/deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "openrouter", cost: "1.2x", desc: "Via OpenRouter" }, + { value: "deepseek/deepseek-v3", label: "DeepSeek V3", provider: "openrouter", cost: "1.5x", desc: "DeepSeek flagship" }, + { value: "deepseek/deepseek-r1", label: "DeepSeek R1", provider: "openrouter", cost: "4x", desc: "Reasoning model" }, + { value: "meta-llama/llama-4-maverick", label: "Llama 4 Maverick", provider: "openrouter", cost: "2x", desc: "Latest Llama" }, + { value: "meta-llama/llama-4-scout", label: "Llama 4 Scout", provider: "openrouter", cost: "1x", desc: "Lightweight Llama" }, + { value: "meta-llama/llama-3.3-70b", label: "Llama 3.3 70B", provider: "openrouter", cost: "1.2x", desc: "Open weights" }, + { value: "mistral/mistral-large", label: "Mistral Large", provider: "openrouter", cost: "4x", desc: "Mistral flagship" }, + { value: "mistral/mistral-small", label: "Mistral Small", provider: "openrouter", cost: "0.5x", desc: "Compact Mistral" }, + { value: "mistral/codestral-2501", label: "Codestral", provider: "openrouter", cost: "2x", desc: "Code specialist" }, + { value: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "openrouter", cost: "4x", desc: "Via OpenRouter" }, + { value: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "openrouter", cost: "2x", desc: "Via OpenRouter" }, + { value: "qwen/qwen-2.5-72b", label: "Qwen 2.5 72B", provider: "openrouter", cost: "1.2x", desc: "Alibaba flagship" }, + { value: "qwen/qwen-2.5-coder-32b", label: "Qwen 2.5 Coder 32B", provider: "openrouter", cost: "1x", desc: "Coding specialist" }, + { value: "qwen/qwq-32b", label: "QWQ 32B", provider: "openrouter", cost: "1.2x", desc: "Reasoning model" }, + { value: "cohere/command-r-plus", label: "Command R+", provider: "openrouter", cost: "3x", desc: "Cohere flagship" }, { value: "minimax/minimax-m3", label: "MiniMax M3", provider: "openrouter", cost: "3.0x", desc: "Via OpenRouter" }, { value: "z-ai/glm-5.1", label: "GLM 5.1", provider: "openrouter", cost: "1.0x", desc: "Via OpenRouter" }, { value: "moonshotai/kimi-k2.6", label: "Kimi K2.6", provider: "openrouter", cost: "1.5x", desc: "Via OpenRouter" }, + + +] + +export const MODELS: ModelEntry[] = [ + { value: SECTION_CLOUD, label: "Supercode Cloud", provider: "supercode", cost: "", desc: "" }, + ...CLOUD_MODELS, + { value: SECTION_BYOK, label: "Bring Your Own Key", provider: "supercode", cost: "", desc: "" }, + ...BYOK_MODELS, ] export class ModelPicker { @@ -37,46 +133,62 @@ export class ModelPicker { selected = 0 overlayLines = 0 + private isSection(item: ModelEntry): boolean { + return item.value === SECTION_CLOUD || item.value === SECTION_BYOK + } + render( width: number, currentProvider: string, currentModel: string, ): string[] { const lines: string[] = [] - const total = this.items.length - if (total === 0) return lines + const visible = this.items.filter((m) => !this.isSection(m)) + const totalVisible = visible.length + if (totalVisible === 0) return lines const maxVisible = 10 const half = Math.floor(maxVisible / 2) - let start = Math.max(0, this.selected - half) - let end = Math.min(total, start + maxVisible) - if (end - start < maxVisible && start > 0) { - start = Math.max(0, end - maxVisible) + const visibleIdx = visible.indexOf(this.items[this.selected]!) + let startIdx = Math.max(0, visibleIdx - half) + let endIdx = Math.min(totalVisible, startIdx + maxVisible) + if (endIdx - startIdx < maxVisible && startIdx > 0) { + startIdx = Math.max(0, endIdx - maxVisible) } - const hasPrev = start > 0 - const hasNext = end < total + const visibleSlice = visible.slice(startIdx, endIdx) + + const hasPrev = startIdx > 0 + const hasNext = endIdx < totalVisible const divider = heavyDivider() lines.push(divider) lines.push( - ` ${chalk.hex(theme.greenGlow)("Switch Model")} ${chalk.hex(theme.muted)("ยท Multipliers represent cost in Standard Tokens")}`, + ` ${chalk.hex(theme.greenGlow)("Switch Model")} ${chalk.hex(theme.muted)("ยท ")}`, ) lines.push(divider) if (hasPrev) { - lines.push(` ${chalk.hex(theme.greenDim)(`โ–ฒ ${start} more`)}`) + lines.push(` ${chalk.hex(theme.greenDim)(`โ–ฒ ${startIdx} more`)}`) } - for (let i = start; i < end; i++) { - const m = this.items[i]! + let lastSection = "" + for (const m of visibleSlice) { + // Determine section for this model + const section = m.provider === "supercode" ? SECTION_CLOUD : SECTION_BYOK + if (section !== lastSection) { + lastSection = section + const sectionLabel = section === SECTION_CLOUD ? "Supercode Cloud" : "Bring Your Own Key" + lines.push(` ${chalk.hex(theme.greenDim).bold(sectionLabel)}`) + } + const isCurrent = m.provider === currentProvider && m.value === currentModel - const isSelected = i === this.selected + const isSelected = m === this.items[this.selected] const providerTag = - m.provider === "concentrateai" + m.provider === "supercode" || m.provider === "concentrateai" ? "" : ` ${chalk.hex(theme.greenDim)(m.provider)}` @@ -91,12 +203,16 @@ export class ModelPicker { const marker = isCurrent ? ` ${chalk.bgHex(theme.amber).hex(theme.black).bold(" current ")}` : "" + const cloudTag = + m.provider === "supercode" && !isCurrent + ? ` ${chalk.bgHex(theme.green).hex(theme.black).bold(" CLOUD ")}` + : "" const freeTag = - !isCurrent && (m.cost === "free" || m.provider === "concentrateai" || m.provider === "mergedev") + !isCurrent && m.cost === "free" && m.provider !== "supercode" ? ` ${chalk.bgHex(theme.green).hex(theme.black).bold(" FREE ")}` : "" - const label = `${prefix} ${name} ${cost}${desc}${providerTag}${marker}${freeTag}` + const label = `${prefix} ${name} ${cost}${desc}${providerTag}${marker}${cloudTag}${freeTag}` if (isSelected) { const bg = chalk.bgHex(theme.greenDeep) @@ -107,7 +223,7 @@ export class ModelPicker { } if (hasNext) { - lines.push(` ${chalk.hex(theme.greenDim)(`โ–ผ ${total - end} more`)}`) + lines.push(` ${chalk.hex(theme.greenDim)(`โ–ผ ${endIdx - startIdx} more`)}`) } lines.push(divider) @@ -117,13 +233,22 @@ export class ModelPicker { selectNext(): void { if (this.items.length === 0) return - this.selected = (this.selected + 1) % this.items.length + let next = (this.selected + 1) % this.items.length + while (next !== this.selected && this.isSection(this.items[next]!)) { + next = (next + 1) % this.items.length + } + this.selected = next } selectPrev(): void { if (this.items.length === 0) return - this.selected = + let prev = (this.selected - 1 + this.items.length) % this.items.length + while (prev !== this.selected && this.isSection(this.items[prev]!)) { + prev = + (prev - 1 + this.items.length) % this.items.length + } + this.selected = prev } getSelected(): ModelEntry { @@ -195,12 +320,27 @@ function readRawKey(): Promise<"up" | "down" | "enter" | "escape"> { }) } -export async function pickModel(): Promise<{ provider: ModelProvider; model?: string }> { +export async function pickModel( + providerFilter?: ModelProvider, + opts?: { allowCustom?: boolean }, +): Promise<{ provider: ModelProvider; model?: string }> { const stored = await getCliConfig() - const currentProvider = stored?.provider || "concentrateai" - const currentModel = stored?.model || "glm-5.1" + const currentProvider = stored?.provider || "supercode" + const currentModel = stored?.model || "deepseek-v4-flash" const picker = new ModelPicker() + if (providerFilter) { + picker.items = MODELS.filter((m) => m.provider === providerFilter) + if (opts?.allowCustom) { + picker.items.push({ + value: "__custom__", + label: "Custom model", + provider: providerFilter, + cost: "", + desc: "Type any model name", + }) + } + } const cols = process.stdout.columns ?? 80 const draw = () => { @@ -256,6 +396,54 @@ export async function pickModel(): Promise<{ provider: ModelProvider; model?: st return { provider: currentProvider as ModelProvider, model: currentModel } } + // For BYOK providers, check if an API key is configured + if (selected.provider !== "supercode") { + const config = await getCliConfig() + const envKeys = getProviderApiKeys() + const existingKey = config?.apiKeys?.[selected.provider] || envKeys[selected.provider] + if (!existingKey) { + process.stdout.write("\n") + const meta = providerMeta[selected.provider] + process.stdout.write( + ` ${chalk.hex(theme.amber)("โ—†")} ${chalk.hex(theme.green).bold(`${meta.label} needs an API key`)} + + ${chalk.hex(theme.muted)(meta.link ? `Get your key at: ${meta.link}` : "")} + +` + ) + const apiKey = await password({ + message: chalk.hex(theme.green)(`paste your ${meta.label} API key`), + validate: (val: string | undefined) => { + if (!val || val.trim().length < 8) return "Key looks too short โ€” please check and try again" + }, + }) + if (isCancel(apiKey)) { + return { provider: selected.provider, model: undefined } + } + await saveProviderApiKey(selected.provider, apiKey as string) + process.stdout.write( + ` ${chalk.hex(theme.green)("โœ“")} ${meta.label} API key saved โ€” requests will go direct (๐Ÿ”‘)\n\n`, + ) + } + } + + // Handle custom model selection + if (selected.value === "__custom__") { + process.stdout.write(`\n`) + const customName = await text({ + message: chalk.hex(theme.green)(`enter model name for ${selected.provider}`), + placeholder: "e.g. my-custom-model-v1", + }) + if (isCancel(customName) || !(customName as string).trim()) { + return { provider: selected.provider, model: undefined } + } + const trimmed = (customName as string).trim() + process.stdout.write( + ` ${chalk.hex(theme.green)("โœ“")} model set to ${chalk.hex(theme.greenGlow)(trimmed)}\n\n`, + ) + return { provider: selected.provider, model: trimmed } + } + // Ask about setting as default process.stdout.write("\n") const setAsDefault = await confirm({ @@ -278,7 +466,8 @@ export async function pickModel(): Promise<{ provider: ModelProvider; model?: st } export function formatModelChange(p: ModelProvider, m?: string): string { - const label = p === "concentrateai" ? "ConcentrateAI" : + const label = p === "supercode" ? "Supercode Cloud" : + p === "concentrateai" ? "ConcentrateAI" : p === "google" ? "Gemini" : p === "nvidia" ? "NVIDIA" : p === "minimax" ? "MiniMax" : p === "mergedev" ? "Merge Dev" : "OpenRouter" diff --git a/apps/supercode-cli/server/src/cli/utils/tui.ts b/apps/supercode-cli/server/src/cli/utils/tui.ts index 5a5d61d..5be3304 100644 --- a/apps/supercode-cli/server/src/cli/utils/tui.ts +++ b/apps/supercode-cli/server/src/cli/utils/tui.ts @@ -785,6 +785,7 @@ export class PersistentStatusBar { private state = { mode: "chat", model: "", + connectionType: "", cumulativeTokens: 0, contextWindow: 0, elapsed: 0, @@ -888,6 +889,10 @@ export class PersistentStatusBar { this.update({ model }) } + setConnectionType(type: string) { + this.update({ connectionType: type }) + } + setStreaming(streaming: boolean) { this.update({ isStreaming: streaming }) } @@ -942,6 +947,11 @@ export class PersistentStatusBar { parts.push(ansiColor(theme.greenGlow, frame)) } if (this.state.model) parts.push(ansiColor(theme.greenGlow, this.state.model)) + if (this.state.connectionType === "direct") { + parts.push(ansiColor(theme.amber, "๐Ÿ”‘")) + } else if (this.state.connectionType === "proxy") { + parts.push(ansiColor(theme.amber, "โ˜๏ธ")) + } if (this.state.cumulativeTokens > 0) { const formatted = formatTokenCount(this.state.cumulativeTokens) diff --git a/apps/supercode-cli/server/src/config/google.config.ts b/apps/supercode-cli/server/src/config/google.config.ts index ca30139..071c1de 100644 --- a/apps/supercode-cli/server/src/config/google.config.ts +++ b/apps/supercode-cli/server/src/config/google.config.ts @@ -1,6 +1,6 @@ export const config = { - googleApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY || "", - model: process.env.MODEL1 || "gemini-2.5-flash" + get googleApiKey() { return process.env.GOOGLE_GENERATIVE_AI_API_KEY || "" }, + get model() { return process.env.MODEL1 || "gemini-2.5-flash" }, } diff --git a/apps/supercode-cli/server/src/config/mergedev.config.ts b/apps/supercode-cli/server/src/config/mergedev.config.ts index 059242a..d2cdd3c 100644 --- a/apps/supercode-cli/server/src/config/mergedev.config.ts +++ b/apps/supercode-cli/server/src/config/mergedev.config.ts @@ -1,6 +1,6 @@ export const mergedevConfig = { - apiKey: process.env.MERGE_DEV_API_KEY || "", - model: process.env.MERGE_DEV_MODEL || "anthropic/claude-opus-4-8", + get apiKey() { return process.env.MERGE_DEV_API_KEY || "" }, + get model() { return process.env.MERGE_DEV_MODEL || "anthropic/claude-opus-4-8" }, baseUrl: "https://api-gateway.merge.dev/v1/openai", - maxTokens: Number(process.env.MERGE_DEV_MAX_TOKENS) || 128000, + get maxTokens() { return Number(process.env.MERGE_DEV_MAX_TOKENS) || 128000 }, } diff --git a/apps/supercode-cli/server/src/config/minimax.config.ts b/apps/supercode-cli/server/src/config/minimax.config.ts index 55bb1b5..f1c1473 100644 --- a/apps/supercode-cli/server/src/config/minimax.config.ts +++ b/apps/supercode-cli/server/src/config/minimax.config.ts @@ -1,6 +1,6 @@ export const minimaxConfig = { - apiKey: process.env.MINIMAX_API_KEY || "", - model: process.env.MINIMAX_MODEL || "MiniMax-M2", + get apiKey() { return process.env.MINIMAX_API_KEY || "" }, + get model() { return process.env.MINIMAX_MODEL || "MiniMax-M2" }, } diff --git a/apps/supercode-cli/server/src/config/nvidia.config.ts b/apps/supercode-cli/server/src/config/nvidia.config.ts index 0e2d183..73014e3 100644 --- a/apps/supercode-cli/server/src/config/nvidia.config.ts +++ b/apps/supercode-cli/server/src/config/nvidia.config.ts @@ -1,10 +1,10 @@ export const nvidiaConfig = { - apiKey: process.env.NVIDIA_API_KEY || "", - model: process.env.NVIDIA_MODEL || "minimaxai/minimax-m3", - baseUrl: process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1", - maxTokens: Number(process.env.NVIDIA_MAX_TOKENS) || 8192, - temperature: Number(process.env.NVIDIA_TEMPERATURE) || 1, - topP: Number(process.env.NVIDIA_TOP_P) || 0.95, + get apiKey() { return process.env.NVIDIA_API_KEY || "" }, + get model() { return process.env.NVIDIA_MODEL || "minimaxai/minimax-m3" }, + get baseUrl() { return process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1" }, + get maxTokens() { return Number(process.env.NVIDIA_MAX_TOKENS) || 8192 }, + get temperature() { return Number(process.env.NVIDIA_TEMPERATURE) || 1 }, + get topP() { return Number(process.env.NVIDIA_TOP_P) || 0.95 }, } diff --git a/apps/supercode-cli/server/src/config/openrouter.config.ts b/apps/supercode-cli/server/src/config/openrouter.config.ts index 5a5aa1d..3a9f091 100644 --- a/apps/supercode-cli/server/src/config/openrouter.config.ts +++ b/apps/supercode-cli/server/src/config/openrouter.config.ts @@ -1,8 +1,8 @@ export const openRouterConfig = { - apiKey: process.env.OPENROUTER_API_KEY || "", - model: process.env.OPENROUTER_MODEL || "moonshotai/kimi-k2.6", - maxTokens: Number(process.env.OPENROUTER_MAX_TOKENS) || 8192, - siteUrl: process.env.OPENROUTER_SITE_URL || "", - siteTitle: process.env.OPENROUTER_SITE_TITLE || "supercode", + get apiKey() { return process.env.OPENROUTER_API_KEY || "" }, + get model() { return process.env.OPENROUTER_MODEL || "moonshotai/kimi-k2.6" }, + get maxTokens() { return Number(process.env.OPENROUTER_MAX_TOKENS) || 8192 }, + get siteUrl() { return process.env.OPENROUTER_SITE_URL || "" }, + get siteTitle() { return process.env.OPENROUTER_SITE_TITLE || "supercode" }, } diff --git a/apps/supercode-cli/server/src/lib/cli-config.ts b/apps/supercode-cli/server/src/lib/cli-config.ts index 232471c..27494e9 100644 --- a/apps/supercode-cli/server/src/lib/cli-config.ts +++ b/apps/supercode-cli/server/src/lib/cli-config.ts @@ -50,12 +50,13 @@ export interface McpCredentials { const DEFAULTS: CliConfig = { version, - provider: "concentrateai", - model: "glm-5.1", + provider: "supercode", + model: "deepseek-v4-flash", mode: "chat", } const API_KEY_ENV_MAP: Record = { + supercode: "", google: "GOOGLE_GENERATIVE_AI_API_KEY", minimax: "MINIMAX_API_KEY", openrouter: "OPENROUTER_API_KEY",