From dbb54c60402af1b7fe10d2e269f7ec8290ee02a4 Mon Sep 17 00:00:00 2001 From: Yash Dewasthale Date: Sat, 18 Jul 2026 00:01:37 +0530 Subject: [PATCH] feat: update supercode-cli version to 0.1.73 and enhance AI provider functionality: - Bumped version in package.json to 0.1.73. - Introduced new "supercode" model provider with enhanced API integration for chat and task generation. - Added support for user interaction through new question and todo management tools. - Improved permission management by adding new permissions for question handling. --- apps/supercode-cli/server/package.json | 2 +- .../server/src/cli/ai/provider.ts | 6 +- apps/supercode-cli/server/src/index.ts | 170 ++++++++++++++++++ .../server/src/tools/definitions/question.ts | 152 ++++++++++++++++ .../server/src/tools/definitions/todowrite.ts | 119 ++++++++++++ .../server/src/tools/permission-manager.ts | 1 + .../server/src/tools/registry.ts | 6 + 7 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 apps/supercode-cli/server/src/tools/definitions/question.ts create mode 100644 apps/supercode-cli/server/src/tools/definitions/todowrite.ts diff --git a/apps/supercode-cli/server/package.json b/apps/supercode-cli/server/package.json index 4b9a436..38bc810 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.72", + "version": "0.1.73", "description": "AI-powered coding agent CLI", "main": "dist/main.js", "bin": { diff --git a/apps/supercode-cli/server/src/cli/ai/provider.ts b/apps/supercode-cli/server/src/cli/ai/provider.ts index 1975de8..f12fd93 100644 --- a/apps/supercode-cli/server/src/cli/ai/provider.ts +++ b/apps/supercode-cli/server/src/cli/ai/provider.ts @@ -66,7 +66,11 @@ export function createProvider(provider: ModelProvider, model?: string): AIProvi // 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) + // Cloud models run through ConcentrateAI with the server's own API key. + // Send "concentrateai" as the provider so the server's existing handler + // picks it up — it falls back to CONCENTRATEAI_API_KEY when no forwarded + // key is provided. + const svc = new ServerProxyService("concentrateai", model || meta.defaultModel) return { name: provider, modelName: model || meta.defaultModel, diff --git a/apps/supercode-cli/server/src/index.ts b/apps/supercode-cli/server/src/index.ts index 8de3f83..ddbc7bd 100644 --- a/apps/supercode-cli/server/src/index.ts +++ b/apps/supercode-cli/server/src/index.ts @@ -812,6 +812,152 @@ app.post("/api/ai/chat", async (req, res) => { res.end() break } + case "supercode": { + const apiKey = process.env.CONCENTRATEAI_API_KEY + if (!apiKey) { res.status(500).json({ error: "Supercode Cloud not configured on server" }); return } + const modelName = modelParam || "deepseek-v4-flash" + const scStart = Date.now() + const bodyObj: any = { + model: modelName, + messages: nonSystemMessages.map((m: any) => { + const msg: any = { + role: m.role, + content: m.content !== null && m.content !== undefined ? String(m.content) : "", + } + if (m.tool_calls) msg.tool_calls = m.tool_calls + if (m.tool_call_id) msg.tool_call_id = m.tool_call_id + return msg + }), + max_tokens: getModelMaxTokens(modelName), + temperature: 0.7, + stream: true, + } + if (system && nonSystemMessages.length > 0) { + bodyObj.messages = [{ role: "system", content: system }, ...bodyObj.messages] + } + if (tools) { + bodyObj.tools = Object.entries(tools).map(([name, fn]: [string, any]) => ({ + type: "function", + function: { name, description: fn.description || "", parameters: toolParams(fn) }, + })) + } + const response = await fetch("https://api.concentrate.ai/v1/chat/completions", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(bodyObj), + }) + if (!response.ok) { + const errText = await response.text().catch(() => "unknown error") + res.status(response.status).json({ error: `Supercode Cloud API ${response.status}: ${errText}` }) + return + } + const reader = response.body?.getReader() + if (!reader) { res.status(500).json({ error: "No response body" }); return } + const decoder = new TextDecoder() + let buffer = "" + let inputTokens = 0 + let outputTokens = 0 + let fullContent = "" + let sawToolCalls = false + let pendingToolCalls: Record = {} + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() || "" + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed || !trimmed.startsWith("data: ")) continue + const jsonStr = trimmed.slice(6) + if (jsonStr === "[DONE]") break + try { + const data = JSON.parse(jsonStr) + const delta = data.choices?.[0]?.delta + if (delta?.content) { + fullContent += delta.content + res.write(JSON.stringify({ type: "text", content: delta.content }) + "\n") + } + if (delta?.tool_calls) { + sawToolCalls = true + for (const tc of delta.tool_calls) { + const index = tc.index ?? 0 + if (!pendingToolCalls[index]) { + pendingToolCalls[index] = { id: "", name: "", args: "" } + } + if (tc.id) pendingToolCalls[index].id = tc.id + if (tc.function?.name) pendingToolCalls[index].name = tc.function.name + if (tc.function?.arguments) pendingToolCalls[index].args += tc.function.arguments + } + } + const finishReason = data.choices?.[0]?.finish_reason + if (finishReason === "tool_calls") { + sawToolCalls = true + for (const [, call] of Object.entries(pendingToolCalls)) { + if (call.name && call.args) { + try { + const parsed = JSON.parse(call.args) + res.write(JSON.stringify({ type: "tool-call", toolName: call.name, args: parsed, toolCallId: call.id }) + "\n") + } catch { /* skip malformed args */ } + } + } + pendingToolCalls = {} + } + if (data.usage) { + inputTokens = data.usage.prompt_tokens ?? 0 + outputTokens = data.usage.completion_tokens ?? 0 + } + } catch { /* skip malformed */ } + } + } + if (!fullContent.trim() && !sawToolCalls) { + const fbBody: any = { + model: modelName, + messages: nonSystemMessages.map((m: any) => { + const msg: any = { + role: m.role, + content: m.content !== null && m.content !== undefined ? String(m.content) : "", + } + if (m.tool_calls) msg.tool_calls = m.tool_calls + if (m.tool_call_id) msg.tool_call_id = m.tool_call_id + return msg + }), + max_tokens: getModelMaxTokens(modelName), + temperature: 0.7, + stream: false, + } + if (system && nonSystemMessages.length > 0) { + fbBody.messages = [{ role: "system", content: system }, ...fbBody.messages] + } + const fbRes = await fetch("https://api.concentrate.ai/v1/chat/completions", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(fbBody), + }) + if (fbRes.ok) { + const fbData: any = await fbRes.json() + const fbContent = fbData?.choices?.[0]?.message?.content ?? "" + if (fbContent) { + res.write(JSON.stringify({ type: "text", content: fbContent }) + "\n") + } + inputTokens = fbData?.usage?.prompt_tokens ?? 0 + outputTokens = fbData?.usage?.completion_tokens ?? 0 + } + } + recordUsage({ + provider: "supercode", model: modelName, + inputTokens, outputTokens, cachedInputTokens: 0, + totalTokens: inputTokens + outputTokens, + costUsd: computeCost(modelName, inputTokens, outputTokens, 0), + durationMs: Date.now() - scStart, + }) + res.write(JSON.stringify({ + type: "finish", reason: "stop", + usage: { inputTokens, outputTokenDetails: { textTokens: outputTokens, reasoningTokens: 0 }, outputTokens, inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, totalTokens: inputTokens + outputTokens } + }) + "\n") + res.end() + break + } default: { res.status(400).json({ error: `Unknown provider: ${provider}` }) } @@ -935,6 +1081,30 @@ app.post("/api/ai/generate-object", async (req, res) => { res.json({ object: { content: data.choices?.[0]?.message?.content || "" } }) break } + case "supercode": { + const apiKey = process.env.CONCENTRATEAI_API_KEY + if (!apiKey) { res.status(500).json({ error: "Supercode Cloud not configured on server" }); return } + const modelName = modelParam || "deepseek-v4-flash" + const response = await fetch("https://api.concentrate.ai/v1/chat/completions", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + model: modelName, + messages: [{ role: "user", content: prompt }], + max_tokens: getModelMaxTokens(modelName), + temperature: 0.7, + stream: false, + }), + }) + if (!response.ok) { + const errText = await response.text().catch(() => "unknown error") + res.status(response.status).json({ error: `Supercode Cloud API ${response.status}: ${errText}` }) + return + } + const data: any = await response.json() + res.json({ object: { content: data.choices?.[0]?.message?.content || "" } }) + break + } default: { res.status(400).json({ error: `Provider ${provider} not supported for structured generation` }) } diff --git a/apps/supercode-cli/server/src/tools/definitions/question.ts b/apps/supercode-cli/server/src/tools/definitions/question.ts new file mode 100644 index 0000000..6843a93 --- /dev/null +++ b/apps/supercode-cli/server/src/tools/definitions/question.ts @@ -0,0 +1,152 @@ +import { z } from "zod" +import chalk from "chalk" +import boxen from "boxen" +import * as readline from "readline" +import { theme } from "../../cli/utils/tui" +import { serialize, ok, fail } from "../../cli/ai/tool-result" + +const questionOptionSchema = z.object({ + label: z.string().describe("Display text (1-5 words, concise)"), + description: z.string().optional().describe("Explanation of the choice"), +}) + +const questionItemSchema = z.object({ + question: z.string().describe("Complete question to ask the user"), + header: z.string().optional().describe("Very short label (max 30 chars) for the question category"), + options: z + .array(questionOptionSchema) + .optional() + .describe("Available choices for the user to select from"), + multiple: z + .boolean() + .optional() + .default(false) + .describe("Allow selecting multiple choices. When enabled, user can pick several options."), +}) + +const questionSchema = z.object({ + questions: z + .array(questionItemSchema) + .min(1) + .max(5) + .describe("Questions to ask the user"), +}) + +export type QuestionArgs = z.infer + +function styledLabel(num: number, label: string): string { + const numTag = chalk.hex(theme.green)(` ${num}.`) + return `${numTag} ${chalk.bold(label)}` +} + +function styledDesc(desc: string): string { + return chalk.hex(theme.muted)(` ${desc}`) +} + +function askOne(item: z.infer): Promise { + return new Promise((resolve) => { + const stdin = process.stdin + const wasRaw = stdin.isRaw + + if (stdin.isTTY) { + stdin.setRawMode(false) + } + + console.log() + + const header = item.header + ? chalk.hex(theme.green)(` ${item.header} `) + : "" + + const boxContent = item.header + ? chalk.bold(item.question) + : chalk.bold(item.question) + + console.log( + boxen(boxContent, { + title: item.header || undefined, + titleAlignment: "left", + borderColor: theme.green, + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: 0, + borderStyle: "round", + }), + ) + + const rl = readline.createInterface({ input: stdin, output: process.stdout, terminal: true }) + + if (!item.options || item.options.length === 0) { + rl.question(chalk.hex(theme.green)(" Your answer: "), (answer) => { + rl.close() + if (stdin.isTTY && wasRaw) stdin.setRawMode(true) + resolve(answer.trim()) + }) + return + } + + console.log() + item.options.forEach((opt, i) => { + console.log(styledLabel(i + 1, opt.label)) + if (opt.description) { + console.log(styledDesc(opt.description)) + } + }) + console.log() + + const promptText = item.multiple + ? chalk.hex(theme.green)(" Select options (comma-separated numbers, e.g. 1,3): ") + : chalk.hex(theme.green)(" Your choice (number): ") + + rl.question(promptText, (raw) => { + rl.close() + if (stdin.isTTY && wasRaw) stdin.setRawMode(true) + + const trimmed = raw.trim() + if (!trimmed) { + resolve([]) + return + } + + if (item.multiple) { + const indices = trimmed + .split(",") + .map((s) => parseInt(s.trim(), 10)) + .filter((n) => !isNaN(n) && n >= 1 && n <= item.options!.length) + if (indices.length === 0) { + resolve([]) + return + } + resolve(indices.map((i) => item.options![i - 1].label)) + } else { + const num = parseInt(trimmed, 10) + if (isNaN(num) || num < 1 || num > item.options.length) { + resolve("(invalid selection)") + return + } + resolve(item.options[num - 1].label) + } + }) + }) +} + +export const questionTool = { + description: + "Ask the user questions to gather context, clarify requirements, " + + "or get decisions on implementation choices. Renders numbered MCQ-style " + + "options for structured feedback. Supports single-choice, multi-select, " + + "and free-form answers. Use before complex tasks to ensure you understand " + + "what the user needs. When asking for preferences or decisions, provide " + + "options with clear labels and optional descriptions.", + parameters: questionSchema, + execute: async (args: QuestionArgs) => + serialize(async () => { + const answers: Array<{ question: string; answer: string | string[] }> = [] + + for (const item of args.questions) { + const answer = await askOne(item) + answers.push({ question: item.question, answer }) + } + + return ok({ answers }) + }), +} diff --git a/apps/supercode-cli/server/src/tools/definitions/todowrite.ts b/apps/supercode-cli/server/src/tools/definitions/todowrite.ts new file mode 100644 index 0000000..984ebc5 --- /dev/null +++ b/apps/supercode-cli/server/src/tools/definitions/todowrite.ts @@ -0,0 +1,119 @@ +import { z } from "zod" +import chalk from "chalk" +import boxen from "boxen" +import { theme } from "../../cli/utils/tui" +import { serialize, ok, fail } from "../../cli/ai/tool-result" + +export interface TodoItem { + content: string + status: "pending" | "in_progress" | "completed" | "cancelled" + priority: "high" | "medium" | "low" +} + +let currentTodos: TodoItem[] = [] + +export function setTodos(todos: TodoItem[]): TodoItem[] { + currentTodos = todos.map((t) => ({ ...t })) + return getTodos() +} + +export function getTodos(): TodoItem[] { + return currentTodos.map((t) => ({ ...t })) +} + +export function clearTodos(): void { + currentTodos = [] +} + +function renderTodoList(todos: TodoItem[]): string { + if (todos.length === 0) { + return chalk.hex(theme.muted)(" No todos") + } + + const rows: string[] = [] + let inProgressIdx = -1 + + for (let i = 0; i < todos.length; i++) { + const t = todos[i] + if (t.status === "in_progress") inProgressIdx = i + + const statusIcon = + t.status === "completed" + ? chalk.hex(theme.green)("✓") + : t.status === "in_progress" + ? chalk.hex(theme.amber)("●") + : t.status === "cancelled" + ? chalk.hex(theme.red)("✗") + : chalk.hex(theme.muted)("○") + + const prioTag = + t.priority === "high" + ? chalk.hex(theme.red)(" high") + : t.priority === "medium" + ? chalk.hex(theme.amber)(" med") + : chalk.hex(theme.muted)(" low") + + rows.push(` ${statusIcon} ${chalk.bold(t.content)}${prioTag}`) + } + + const completed = todos.filter((t) => t.status === "completed").length + const total = todos.length + const summary = chalk.hex(theme.greenDim)( + ` ${completed}/${total} · ${inProgressIdx >= 0 ? `active: ${todos[inProgressIdx].content}` : "none in progress"}`, + ) + + return [...rows, "", summary].join("\n") +} + +const todoItemSchema = z.object({ + content: z.string().describe("Brief description of the task"), + status: z + .enum(["pending", "in_progress", "completed", "cancelled"]) + .describe("Current status of the task"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level of the task"), +}) + +const todowriteSchema = z.object({ + todos: z + .array(todoItemSchema) + .min(1) + .describe( + "The updated todo list for the session. Each item has content, status, and priority. " + + "Only one item should be 'in_progress' at a time.", + ), +}) + +export type TodowriteArgs = z.infer + +export const todowriteTool = { + description: + "Create and maintain a structured task list for the current coding session. " + + "Tracks progress, organizes multi-step work, and surfaces status to the user. " + + "Use proactively when the task requires 3+ distinct steps or non-trivial work. " + + "Only one item should have status 'in_progress' at a time. " + + "Update the list as work progresses by calling this tool again with the updated state.", + parameters: todowriteSchema, + execute: async (args: TodowriteArgs) => + serialize(async () => { + setTodos(args.todos) + + const box = boxen(renderTodoList(args.todos), { + title: " Task List ", + borderColor: theme.green, + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: 0, + borderStyle: "round", + }) + + console.log("\n" + box + "\n") + + return ok({ + todos: args.todos, + count: args.todos.length, + inProgress: args.todos.filter((t) => t.status === "in_progress").length, + completed: args.todos.filter((t) => t.status === "completed").length, + }) + }), +} diff --git a/apps/supercode-cli/server/src/tools/permission-manager.ts b/apps/supercode-cli/server/src/tools/permission-manager.ts index 52b8a06..dff692f 100644 --- a/apps/supercode-cli/server/src/tools/permission-manager.ts +++ b/apps/supercode-cli/server/src/tools/permission-manager.ts @@ -346,6 +346,7 @@ const DEFAULT_RULES: RulesetArray = [ { permission: "read_instructions", pattern: "*", action: "allow" }, { permission: "todo_read", pattern: "*", action: "allow" }, { permission: "todo_write", pattern: "*", action: "allow" }, + { permission: "question", pattern: "*", action: "allow" }, { permission: "task", pattern: "*", action: "allow" }, { permission: "delegate", pattern: "*", action: "allow" }, { permission: "switch_to_agent_mode", pattern: "*", action: "allow" }, diff --git a/apps/supercode-cli/server/src/tools/registry.ts b/apps/supercode-cli/server/src/tools/registry.ts index 88b33cd..f70d67b 100644 --- a/apps/supercode-cli/server/src/tools/registry.ts +++ b/apps/supercode-cli/server/src/tools/registry.ts @@ -16,6 +16,8 @@ import { codeExecTool } from "./definitions/code-exec.ts" import { readInstructionsTool } from "./definitions/read-instructions.ts" import { switchToAgentModeTool } from "./definitions/switch-to-agent-mode.ts" import { delegateTool, taskTool } from "./definitions/delegate.ts" +import { questionTool } from "./definitions/question.ts" +import { todowriteTool } from "./definitions/todowrite.ts" import { permissionManager } from "./permission-manager.ts" // @@ -89,6 +91,8 @@ export const toolMeta: Record = { switch_to_agent_mode: { category: "agent", requiresPermission: false, description: switchToAgentModeTool.description }, delegate: { category: "agent", requiresPermission: false, description: delegateTool.description }, task: { category: "agent", requiresPermission: false, description: taskTool.description }, + question: { category: "agent", requiresPermission: false, description: questionTool.description }, + todowrite: { category: "agent", requiresPermission: false, description: todowriteTool.description }, } export const tools = { @@ -109,4 +113,6 @@ export const tools = { switch_to_agent_mode: defineTool(switchToAgentModeTool), delegate: defineTool(delegateTool), task: defineTool(taskTool), + question: defineTool(questionTool), + todowrite: defineTool(todowriteTool), }