diff --git a/.github/workflows/build-opencode-cli-docker.yml b/.github/workflows/build-opencode-cli-docker.yml.disabled similarity index 100% rename from .github/workflows/build-opencode-cli-docker.yml rename to .github/workflows/build-opencode-cli-docker.yml.disabled diff --git a/Dockerfile b/Dockerfile index 3c3afe6..9ecfad0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,11 @@ # Web-based PowerShell + SST OpenCode terminal -# Uses pre-built OpenCode CLI from GitHub releases +# Builds OpenCode CLI from source during Docker build FROM ubuntu:24.04 ARG PWSH_VERSION=7.4.6 ARG NODE_VERSION=20.18.1 ARG TTYD_VERSION=1.7.7 +ARG BUN_VERSION=1.3.8 ENV DEBIAN_FRONTEND=noninteractive \ TERM=xterm-256color \ @@ -23,6 +24,7 @@ RUN apt-get update \ tini \ libicu-dev \ libssl-dev \ + unzip \ && rm -rf /var/lib/apt/lists/* # Install Node.js @@ -33,6 +35,10 @@ RUN curl -fsSL -o /tmp/node.tar.xz \ && ln -sf /usr/local/bin/npm /usr/bin/npm \ && rm -f /tmp/node.tar.xz +# Install Bun +RUN npm install -g bun@${BUN_VERSION} \ + && bun --version + # Install PowerShell RUN curl -fsSL -o /tmp/powershell.tar.gz \ "https://github.com/PowerShell/PowerShell/releases/download/v${PWSH_VERSION}/powershell-${PWSH_VERSION}-linux-x64.tar.gz" \ @@ -47,10 +53,20 @@ RUN curl -fsSL -o /usr/local/bin/ttyd \ "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" \ && chmod +x /usr/local/bin/ttyd -# Download OpenCode CLI from GitHub releases -# This will download the latest release - requires the release to exist first -RUN curl -fsSL -L -o /usr/local/bin/opencode \ - "https://github.com/southseact-3d/shopify-ai-backup/releases/latest/download/opencode-linux-x64" \ +# Build OpenCode CLI from source +COPY opencode /opt/opencode-src +WORKDIR /opt/opencode-src + +# Configure git for any workspace dependencies that use git URLs +RUN git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" + +# Install dependencies and build OpenCode CLI +ENV HUSKY=0 +RUN bun install 2>&1 \ + && bun run ./packages/opencode/script/build.ts --single + +# Copy the built binary to /usr/local/bin +RUN cp /opt/opencode-src/packages/opencode/dist/opencode-linux-x64/bin/opencode /usr/local/bin/opencode \ && chmod +x /usr/local/bin/opencode \ && opencode --version diff --git a/chat/server.js b/chat/server.js index a90349a..14e204d 100644 --- a/chat/server.js +++ b/chat/server.js @@ -10483,19 +10483,6 @@ async function sendToOpencodeWithFallback({ session, model, content, message, cl log('✓ sendToOpencodeWithFallback: Using token usage captured during streaming', { tokensUsed, messageId: message?.id }); } - // Try session query - if (!tokensUsed && opencodeSessionId && session?.workspaceDir) { - log('🔍 sendToOpencodeWithFallback: Attempting session token query', { sessionId: opencodeSessionId, messageId: message?.id }); - tokensUsed = await getOpencodeSessionTokenUsage(opencodeSessionId, session.workspaceDir); - if (tokensUsed > 0) { - tokenSource = 'session'; - tokenExtractionLog.push({ method: 'session_query', success: true, value: tokensUsed, context: 'sendToOpencodeWithFallback' }); - log('✓ sendToOpencodeWithFallback: Got tokens from session', { tokensUsed, messageId: message?.id }); - } else { - tokenExtractionLog.push({ method: 'session_query', success: false, reason: 'returned 0 tokens', context: 'sendToOpencodeWithFallback' }); - } - } - // Use estimation as last resort if (!tokensUsed) { const inputTokens = estimateTokensFromMessages([messageContent], ''); @@ -10923,24 +10910,6 @@ async function processMessage(sessionId, message) { log('✓ processMessage: Using token usage captured during streaming', { tokensUsed, messageId: message.id }); } - // Try session query if still no tokens - if (!tokensUsed && session.opencodeSessionId && session.workspaceDir) { - try { - log('🔍 processMessage: Attempting session token query as fallback', { sessionId: session.opencodeSessionId, messageId: message.id }); - tokensUsed = await getOpencodeSessionTokenUsage(session.opencodeSessionId, session.workspaceDir); - if (tokensUsed > 0) { - tokenSource = 'session'; - tokenExtractionLog.push({ method: 'session_query', success: true, value: tokensUsed, context: 'processMessage fallback' }); - log('✓ processMessage: Got tokens from session query', { tokensUsed, messageId: message.id }); - } else { - tokenExtractionLog.push({ method: 'session_query', success: false, reason: 'returned 0 tokens', context: 'processMessage fallback' }); - } - } catch (sessionErr) { - tokenExtractionLog.push({ method: 'session_query', success: false, error: String(sessionErr), context: 'processMessage fallback' }); - log('✗ processMessage: Session token query failed', { error: String(sessionErr), messageId: message.id }); - } - } - // If still no tokens, use estimation with detailed logging if (!tokensUsed) { const inputTokens = estimateTokensFromMessages([message.content], ''); @@ -18158,135 +18127,10 @@ async function listOpencodeSessions(cwd) { return []; } -async function getOpencodeSessionTokenUsage(sessionId, cwd) { - if (!sessionId || !cwd) { - log('⚠️ getOpencodeSessionTokenUsage: Missing required parameters', { hasSessionId: !!sessionId, hasCwd: !!cwd }); - return 0; - } - - const cliCommand = resolveCliCommand('opencode'); - const candidates = [ - ['session', 'info', '--id', sessionId, '--json'], - ['sessions', 'info', '--id', sessionId, '--json'], - ['session', 'info', sessionId, '--json'], - ['session', 'usage', '--id', sessionId, '--json'], - ['session', 'show', '--id', sessionId, '--json'], - ]; - - log('🔍 getOpencodeSessionTokenUsage: Starting session token query', { - sessionId, - cwd, - cliCommand, - candidateCount: candidates.length, - candidates: candidates.map(c => c.join(' ')) - }); - - const attemptResults = []; - - for (const args of candidates) { - const cmdStr = args.join(' '); - try { - log(` → Trying: ${cliCommand} ${cmdStr}`, { sessionId }); - const { stdout, stderr } = await runCommand(cliCommand, args, { timeout: 10000, cwd }); - - const hasStdout = stdout && stdout.trim(); - const hasStderr = stderr && stderr.trim(); - - log(` ← Response received`, { - args: cmdStr, - hasStdout, - hasStderr, - stdoutLength: stdout?.length || 0, - stderrLength: stderr?.length || 0, - stdoutSample: stdout?.substring(0, 300), - stderrSample: stderr?.substring(0, 200) - }); - - if (hasStdout) { - // Try JSON parsing first - try { - const parsed = JSON.parse(stdout); - log(' ✓ JSON parse successful', { - args: cmdStr, - parsedKeys: Object.keys(parsed), - hasUsage: !!parsed.usage, - hasTokens: !!parsed.tokens, - hasTokensUsed: !!parsed.tokensUsed, - hasSession: !!parsed.session - }); - - const extracted = extractTokenUsage(parsed) || extractTokenUsage(parsed.session) || null; - const tokens = extracted?.tokens || 0; - - if (typeof tokens === 'number' && tokens > 0) { - log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from JSON', { - sessionId, - tokens, - command: cmdStr, - extractionPath: extracted?.source || 'unknown' - }); - attemptResults.push({ command: cmdStr, success: true, tokens, source: 'json' }); - return tokens; - } else { - const reason = typeof tokens !== 'number' ? `tokens is ${typeof tokens}, not number` : 'tokens is 0 or negative'; - log(' ✗ JSON parsed but no valid token count', { args: cmdStr, tokens, reason }); - attemptResults.push({ command: cmdStr, success: false, reason, parsedTokens: tokens, source: 'json' }); - } - } catch (jsonErr) { - log(' ✗ JSON parse failed, trying text parse', { - args: cmdStr, - error: jsonErr.message, - stdoutSample: stdout.substring(0, 200) - }); - - // Try to parse token count from text output - const tokenMatch = stdout.match(/total[_\s-]?tokens?\s*[:=]?\s*(\d+)/i) || - stdout.match(/tokens?\s*[:=]?\s*(\d+)/i) || - stdout.match(/token\s*count\s*[:=]?\s*(\d+)/i); - if (tokenMatch) { - const tokens = parseInt(tokenMatch[1], 10); - if (tokens > 0) { - log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from text', { - sessionId, - tokens, - command: cmdStr, - pattern: tokenMatch[0] - }); - attemptResults.push({ command: cmdStr, success: true, tokens, source: 'text', pattern: tokenMatch[0] }); - return tokens; - } else { - log(' ✗ Text pattern matched but tokens <= 0', { args: cmdStr, tokens, pattern: tokenMatch[0] }); - attemptResults.push({ command: cmdStr, success: false, reason: 'matched text pattern but tokens <= 0', parsedTokens: tokens, source: 'text' }); - } - } else { - log(' ✗ No text patterns matched', { args: cmdStr, stdoutSample: stdout.substring(0, 200) }); - attemptResults.push({ command: cmdStr, success: false, reason: 'no text patterns matched', source: 'text' }); - } - } - } else { - const reason = !stdout ? 'no stdout' : 'stdout is empty'; - log(' ✗ No stdout to parse', { args: cmdStr, reason, hasStderr }); - attemptResults.push({ command: cmdStr, success: false, reason, stderr: stderr?.substring(0, 200) }); - } - } catch (err) { - const errorDetails = { - message: err.message, - stderr: err.stderr?.substring(0, 200), - stdout: err.stdout?.substring(0, 200), - code: err.code - }; - log(' ✗ Command execution failed', { args: cmdStr, error: errorDetails }); - attemptResults.push({ command: cmdStr, success: false, error: errorDetails }); - } - } - - log('❌ getOpencodeSessionTokenUsage: All commands failed', { - sessionId, - totalAttempts: attemptResults.length, - attemptResults - }); - return 0; -} +// Note: Session token usage queries removed - OpenCode CLI doesn't support session info/usage/show commands. +// Token tracking now relies on: +// 1. Streaming capture during CLI execution (primary method) +// 2. Estimation as fallback when streaming fails async function handleGit(req, res, action) { // Validate git action