Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,10 @@ apps/web/scripts/
.claude/skills/brag
.factory/skills/brag
/brag-output

# tauri app (apps/jarvis)
apps/jarvis/src-tauri/target/
apps/jarvis/src-tauri/gen/
apps/jarvis/img/
apps/jarvis/.gitignore
apps/jarvis/.gitignore
2 changes: 1 addition & 1 deletion apps/supercode-cli/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"open": "^11.0.0",
"vercel-minimax-ai-provider": "^0.0.2",
"yocto-spinner": "^1.2.0",
"zod": "^3.25.2",
"zod": "3.25",
"zod-to-json-schema": "^3.25.2"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion apps/supercode-cli/server/src/cli/ai/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ async function streamAIResponse(
}

if (mode === "chat") {
promptContent += `\n\n## Chat Mode Note\n\nYou are in chat mode. You have access to read,\nsearch, and web tools (read_file, search_files, url_fetch, firecrawl, exa, etc.).\nRead-only shell commands (git status/log/diff, ls, cat, pwd, find, grep) and\nread-only git commands are auto-allowed without prompting.\n\nTools that modify state — write_file, edit_file, git push, git commit, git reset,\nnpm install, rm, mkdir, and any other write/delete command — require explicit\nper-user approval. If the user's task genuinely needs many such operations\nwithout interruptions, call the \`switch_to_agent_mode\` tool ONCE with a clear\nreason; the system will ask for user approval. Do NOT attempt write/exec tools\nin the same response where you call switch_to_agent_mode.`
promptContent += `\n\n## Chat Mode Note\n\nYou are in chat mode. You have access to read,\nsearch, and web tools (read_file, search_files, url_fetch, firecrawl, exa, etc.).\nRead-only shell commands (git status/log/diff, ls, cat, pwd, find, grep) and\nread-only git commands are auto-allowed without prompting.\n\nTools that modify state — write_file, edit_file, git push, git commit, git reset,\nnpm install, rm, mkdir, and any other write/delete command — require explicit\nper-user approval. If the user's task genuinely needs many such operations\nwithout interruptions, call the \`switch_to_agent_mode\` tool ONCE with a clear\nreason; the system will ask for user approval. Do NOT attempt write/exec tools\nin the same response where you call switch_to_agent_mode.\n\n## Tool Use (Mandatory)\n\nWhen the user's request is an action on their repo or workspace — review staged\nchanges, show diff, run a command, read a file, find something, check status,\nfix a file, etc. — you MUST invoke the appropriate tool (run_command,\nread_file, search_files, etc.) BEFORE you respond. Do not just describe what\nyou would do. Do not answer conversationally when the user asked you to do\nsomething. If your first response contains only reasoning or text and no tool\ncall, the system will count the turn as incomplete and the user will not see\nany action taken. Call the tool first, then summarize the result.`
}

if (mode === "plan") {
Expand Down
198 changes: 117 additions & 81 deletions apps/supercode-cli/server/src/cli/ai/server-proxy-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,95 +94,131 @@ export class ServerProxyService {
throw new Error("Not authenticated. Please login first.")
}

const res = await fetch(`${BASE_URL}/api/ai/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token.access_token}`,
},
body: JSON.stringify({
messages,
provider: this.providerName,
model: this.modelName,
tools,
...(this.apiKey && this.providerName === "concentrateai" ? { concentrateAiKey: this.apiKey } : {}),
}),
signal,
})
// Safety timeout: even though the server now bounds the upstream work,
// don't let a stalled server hang the turn forever on the client side.
const controller = new AbortController()
const timeoutMs = Number(process.env.SUPERCODE_REQUEST_TIMEOUT_MS) || 120_000
const timeoutId = setTimeout(() => {
if (!signal?.aborted) controller.abort(new Error("Request timed out"))
}, timeoutMs)
const onAbort = () => controller.abort()
if (signal) {
if (signal.aborted) controller.abort()
else signal.addEventListener("abort", onAbort, { once: true })
}
const cleanup = () => {
clearTimeout(timeoutId)
if (signal) signal.removeEventListener("abort", onAbort)
}

if (!res.ok) {
const text = await res.text()
if (text.includes("Insufficient Funds") || text.includes("Credit usage at configured limit")) {
return {
content: "You've used your limits. Resets in 24hrs.",
finishReason: "stop" as FinishReason,
usage: {
inputTokens: 0,
inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
outputTokens: 0,
outputTokenDetails: { textTokens: 0, reasoningTokens: 0 },
totalTokens: 0,
},
toolCalls: [],
try {
const res = await fetch(`${BASE_URL}/api/ai/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token.access_token}`,
},
body: JSON.stringify({
messages,
provider: this.providerName,
model: this.modelName,
tools,
...(this.apiKey && this.providerName === "concentrateai" ? { concentrateAiKey: this.apiKey } : {}),
}),
signal: controller.signal,
})

if (!res.ok) {
const text = await res.text()
if (text.includes("Insufficient Funds") || text.includes("Credit usage at configured limit")) {
cleanup()
return {
content: "You've used your limits. Resets in 24hrs.",
finishReason: "stop" as FinishReason,
usage: {
inputTokens: 0,
inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
outputTokens: 0,
outputTokenDetails: { textTokens: 0, reasoningTokens: 0 },
totalTokens: 0,
},
toolCalls: [],
}
}
cleanup()
throw new Error(text || "AI proxy request failed")
}
throw new Error(text || "AI proxy request failed")
}

const reader = res.body?.getReader()
if (!reader) throw new Error("No response body")
const reader = res.body?.getReader()
if (!reader) {
cleanup()
throw new Error("No response body")
}

const decoder = new TextDecoder()
let buffer = ""
let fullResponse = ""
let finishReason: FinishReason = "stop"
let usage: LanguageModelUsage = {
inputTokens: 0,
inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
outputTokens: 0,
outputTokenDetails: { textTokens: 0, reasoningTokens: 0 },
totalTokens: 0,
}
const decoder = new TextDecoder()
let buffer = ""
let fullResponse = ""
let finishReason: FinishReason = "stop"
let serverError: string | null = null
let usage: LanguageModelUsage = {
inputTokens: 0,
inputTokenDetails: { noCacheTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
outputTokens: 0,
outputTokenDetails: { textTokens: 0, reasoningTokens: 0 },
totalTokens: 0,
}

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) continue
try {
const event = JSON.parse(trimmed)
switch (event.type) {
case "text":
fullResponse += event.content
onChunk?.(event.content)
break
case "reasoning":
onReasoning?.(event.content)
break
case "tool-call":
toolCalls.push({
toolName: event.toolName,
args: event.args,
toolCallId: event.toolCallId || `call_${Date.now()}_${toolCalls.length}`,
})
onToolCall?.({ toolName: event.toolName, args: event.args })
break
case "finish":
finishReason = event.reason || "stop"
if (event.usage) usage = event.usage
break
}
} catch { /* skip malformed */ }
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) continue
try {
const event = JSON.parse(trimmed)
switch (event.type) {
case "text":
fullResponse += event.content
onChunk?.(event.content)
break
case "reasoning":
onReasoning?.(event.content)
break
case "tool-call":
toolCalls.push({
toolName: event.toolName,
args: event.args,
toolCallId: event.toolCallId || `call_${Date.now()}_${toolCalls.length}`,
})
onToolCall?.({ toolName: event.toolName, args: event.args })
break
case "error":
// The server surfaces explicit upstream/empty-result failures
// as an error event. Throw so the caller's error handling runs
// instead of silently reporting an empty turn.
serverError = event.message || "AI proxy error"
break
case "finish":
finishReason = event.reason || "stop"
if (event.usage) usage = event.usage
break
}
} catch { /* skip malformed */ }
}
}
}

return { content: fullResponse, finishReason, usage, toolCalls }
cleanup()
if (serverError) throw new Error(serverError)
return { content: fullResponse, finishReason, usage, toolCalls }
} catch (err) {
cleanup()
throw err
}
}

async sendMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
formatTokenCount,
progressBar,
} from "src/cli/utils/tui.ts"
import { getDailyOpusCount, OPUS_DAILY_LIMIT } from "src/lib/token-budget"
import { getDailyOpusCount, OPUS_DAILY_LIMIT, DAILY_BUDGET_TOKENS } from "src/lib/token-budget"
import { computeCost, getProviderDisplayNameFromRaw, getProviderColor } from "src/lib/pricing"

function todayStart(): Date {
Expand Down Expand Up @@ -143,11 +143,15 @@ export async function tokenLimitCommand(): Promise<void> {
console.log()

const avgCostPerQuery = grandTotalQueries > 0 ? grandTotalCost / grandTotalQueries : 0
const budgetPct = Math.min(100, Math.round((grandTotalTokens / 128_000) * 100))
const budgetPct = Math.min(100, Math.round((grandTotalTokens / DAILY_BUDGET_TOKENS) * 100))
const budgetLabel =
DAILY_BUDGET_TOKENS >= 1_000_000
? `${Math.round(DAILY_BUDGET_TOKENS / 1_000_000)}M budget`
: `${Math.round(DAILY_BUDGET_TOKENS / 1_000)}K budget`

line(chalk.hex(theme.green)("Today's Summary"))
line()
line(` ${chalk.hex(theme.greenGlow)("Total tokens")} ${formatTokenCount(grandTotalTokens).padStart(8)} ${progressBar(grandTotalTokens, 128_000, 16)} ${dim("of 128K budget")}`)
line(` ${chalk.hex(theme.greenGlow)("Total tokens")} ${formatTokenCount(grandTotalTokens).padStart(8)} ${progressBar(grandTotalTokens, DAILY_BUDGET_TOKENS, 16)} ${dim(`of ${budgetLabel}`)}`)
line(` ${chalk.hex(theme.greenGlow)("Total queries")} ${String(grandTotalQueries).padStart(8)} ${progressBar(grandTotalQueries, 50, 16)} ${dim("of 50 queries")}`)
line(` ${chalk.hex(theme.greenGlow)("Total cost")} ${`$${grandTotalCost.toFixed(2)}`.padStart(8)} ${dim(`avg $${avgCostPerQuery.toFixed(3)}/query`)}`)
line()
Expand Down
Loading
Loading