@paytaca/opencode-plugin 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- export declare const PROXY_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca AI Proxy\n * \n * Sits between OpenCode and the Django backend.\n * - Auto-starts by OpenCode plugin\n * - On 402, returns SSE typewriter loading sequence + synthetic payment prompt\n * - Stores pending payments; handles \"yes\"/\"no\" approval internally\n * - Uses only Node.js built-in modules\n * \n * Usage: node proxy.js [backend_url] [proxy_port]\n * Example: node proxy.js https://api.paytaca.ai 8001\n */\n\nconst http = require('http');\nconst https = require('https');\nconst { spawn } = require('child_process');\nconst { Transform } = require('stream');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\nconst PROXY_PORT = parseInt(process.argv[3]) || 8001;\nconst BACKEND_URL = process.argv[2] || 'https://api.paytaca.ai';\nconst parsedUrl = new URL(BACKEND_URL);\nconst DJANGO_HOST = parsedUrl.hostname;\nconst DJANGO_PORT = parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80);\nconst REQUester = parsedUrl.protocol === 'https:' ? https : http;\n\n// Logging setup: write to file instead of console\nconst LOG_DIR = path.join(os.homedir(), '.opencode-paytaca');\nif (!fs.existsSync(LOG_DIR)) {\n fs.mkdirSync(LOG_DIR, { recursive: true });\n}\nconst LOG_FILE = path.join(LOG_DIR, 'proxy.log');\nconst logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });\n\nfunction log(message) {\n const timestamp = new Date().toISOString();\n logStream.write(timestamp + ' [Proxy] ' + message + '\\n');\n}\n\n// Store pending payment requests per wallet hash\n// Each entry: { body, modelId, displayName, durationMinutes, tiers[], step }\n// step: 'tier_select' (user must pick a tier) or 'approval' (yes/no)\nconst pendingPayments = new Map();\n\n// Utility: run shell command and return output\nfunction runCommand(cmd, args = []) {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { shell: false });\n let stdout = '';\n let stderr = '';\n \n child.stdout.on('data', (data) => { stdout += data.toString(); });\n child.stderr.on('data', (data) => { stderr += data.toString(); });\n \n child.on('close', (code) => {\n if (code === 0) resolve(stdout.trim());\n else reject(new Error(stderr.trim() || 'Command exited with code ' + code));\n });\n \n child.on('error', (err) => reject(err));\n });\n}\n\n// Get paytaca command from environment or default to 'paytaca'\nconst PAYTACA_CMD = process.env.PAYTACA_CMD || 'paytaca';\n\n// Utility: check if paytaca CLI exists\nasync function checkPaytacaCli() {\n try {\n // Try to run version check\n await runCommand(PAYTACA_CMD, ['--version']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Utility: get wallet balance in sats\nasync function getWalletBalance() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Balance:\\s*([\\d.]+)\\s*BCH/i);\n if (match) {\n const bch = parseFloat(match[1]);\n return Math.floor(bch * 100000000);\n }\n return null;\n } catch (err) {\n log('Failed to get wallet balance: ' + err.message);\n return null;\n }\n}\n\n// Utility: get receiving address\nasync function getReceivingAddress() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Address:\\s*(bitcoincash:[a-zA-Z0-9]+)/i);\n return match ? match[1] : null;\n } catch {\n return null;\n }\n}\n\n// Utility: check if wallet exists\nasync function checkWallet() {\n try {\n await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Format seconds as MM:SS or HH:MM:SS\nfunction formatDuration(totalSeconds) {\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n if (hours > 0) {\n return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');\n }\n return minutes + ':' + String(secs).padStart(2, '0');\n}\n\n// SSE helper: write a data line\nfunction sseLine(res, data) {\n res.write('data: ' + JSON.stringify(data) + '\\n\\n');\n}\n\n// SSE helper: write [DONE]\nfunction sseDone(res) {\n res.write('data: [DONE]\\n\\n');\n}\n\n// Build and stream SSE tier selection prompt\nasync function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n\n sseLine(res, {\n id: 'tier-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n // Loading sequence\n sseLine(res, {\n id: 'tier-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F3 Initializing Paytaca AI provider...\\n' }, finish_reason: null }],\n });\n\n const hasCli = await checkPaytacaCli();\n sseLine(res, {\n id: 'tier-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking Paytaca CLI... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-4',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasCli ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const hasWallet = hasCli ? await checkWallet() : false;\n sseLine(res, {\n id: 'tier-5',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking wallet... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-6',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasWallet ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const balanceSats = hasWallet ? await getWalletBalance() : null;\n sseLine(res, {\n id: 'tier-7',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Fetching balance... ' }, finish_reason: null }],\n });\n\n let balanceStr;\n if (balanceSats !== null) {\n const bch = (balanceSats / 100000000).toFixed(8);\n balanceStr = bch + ' BCH';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u2705 \u2014 ' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n } else {\n balanceStr = 'Unable to check';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n }\n\n // Tier selection\n sseLine(res, {\n id: 'tier-9',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\uD83D\uDCB3 Select a plan for **' + (modelName || 'AI Model') + '**\\n\\n' }, finish_reason: null }],\n });\n\n for (let i = 0; i < tiers.length; i++) {\n const tier = tiers[i];\n const bchAmount = (tier.price_sats / 100000000).toFixed(8);\n const label = '`(' + String(i + 1) + ')` ';\n sseLine(res, {\n id: 'tier-10-' + i,\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: label + tier.minutes + ' minutes \u2014 PHP ' + tier.price_php.toFixed(2) + ' (' + bchAmount + ' BCH)\\n' }, finish_reason: null }],\n });\n }\n\n sseLine(res, {\n id: 'tier-11',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\nEnter a number (1-' + tiers.length + '), e.g. type ' + tiers[0].minutes + ':' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'tier-12',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n\n sseDone(res);\n res.end();\n}\n\n// Build and stream SSE loading sequence + payment prompt\n// Stream SSE notice when the upstream (OpenRouter) account lacks balance to fund\n// the request. Replaces the old single-tier yes/no approval prompt.\nasync function streamLowBalanceNotice(res, modelName) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n\n sseLine(res, {\n id: 'lb-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName || 'AI Model',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n sseLine(res, {\n id: 'lb-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F OpenRouter balance is low \u2014 please top up before continuing.\\n' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'lb-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n\n sseDone(res);\n res.end();\n}\n\n// Forward request to Django and return response (buffered, for non-streaming)\nfunction forwardToDjango(req, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardToDjango -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n const elapsed = Date.now() - startTime;\n log('Django responded in ' + elapsed + 'ms: status=' + djangoRes.statusCode + ', bodyLen=' + responseBody.length);\n callback(null, djangoRes.statusCode, djangoRes.headers, responseBody);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Forward streaming request to Django\nfunction forwardStreaming(req, res, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardStreaming -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n const elapsed = Date.now() - startTime;\n log('Django response started in ' + elapsed + 'ms: status=' + djangoRes.statusCode);\n\n if (djangoRes.statusCode === 402) {\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n callback(null, 402, djangoRes.headers, responseBody);\n });\n return;\n }\n\n res.writeHead(djangoRes.statusCode, {\n 'Content-Type': djangoRes.headers['content-type'] || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n\n if (res.socket) {\n res.socket.setNoDelay(true);\n }\n\n // Buffer SSE data at event boundaries and inject keepalive between events.\n let sseBuffer = '';\n let lastActivity = Date.now();\n let streamingDone = false;\n let doneForwarded = false;\n\n // Watchdog: inject keepalive only when buffer is empty (between complete events)\n const keepaliveTimer = setInterval(() => {\n if (streamingDone || res.writableEnded || res.destroyed) {\n clearInterval(keepaliveTimer);\n return;\n }\n const now = Date.now();\n if (now - lastActivity >= 2000 && sseBuffer.length === 0) {\n try {\n res.write(': keepalive\\n\\n');\n lastActivity = now;\n } catch (err) {\n log('Keepalive write error: ' + err.message);\n clearInterval(keepaliveTimer);\n }\n }\n }, 500);\n\n const cleanup = () => {\n streamingDone = true;\n clearInterval(keepaliveTimer);\n };\n\n var diagCounter = 0;\n djangoRes.on('data', (chunk) => {\n var chunkStr = chunk.toString();\n var chunkIdx = ++diagCounter;\n sseBuffer += chunkStr;\n lastActivity = Date.now();\n\n var okCount = (sseBuffer.match(/:ok/g) || []).length;\n if (okCount > 0) {\n log('CHUNK#' + chunkIdx + ': ' + okCount + ' :ok in buffer (len=' + sseBuffer.length + ')');\n }\n\n // Strip upstream SSE \":ok\" keepalive comments from anywhere in the buffer.\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (okCount > 0) {\n log('AFTER: stripped ' + okCount + ' :ok, buffer len=' + sseBuffer.length);\n }\n\n var extractedCount = 0;\n let idx;\n while ((idx = sseBuffer.indexOf('\\n\\n')) !== -1) {\n const event = sseBuffer.substring(0, idx + 2);\n sseBuffer = sseBuffer.substring(idx + 2);\n const lines = event.split('\\n').filter(l => !/^:/.test(l) && l.length > 0);\n if (lines.length === 0) continue;\n const cleanEvent = lines.join('\\n') + '\\n\\n';\n extractedCount++;\n var dataContent = lines.map(function(l) { return l.replace(/^data: ?/, ''); }).join('');\n if (dataContent === '[DONE]') { doneForwarded = true; }\n var lastChar = dataContent.slice(-1);\n if (dataContent !== '[DONE]' && lastChar !== '}' && lastChar !== ']') {\n log('FLUSH: truncated event #' + extractedCount + ' (len=' + dataContent.length + ', end=' + JSON.stringify(dataContent.slice(-30)) + ')');\n }\n try {\n res.write(cleanEvent);\n } catch (err) {\n cleanup();\n log('Write error: ' + err.message);\n return;\n }\n }\n if (extractedCount > 0) {\n log('EXTRACT: forwarded ' + extractedCount + ' events in chunk#' + chunkIdx + ', buffer remaining len=' + sseBuffer.length);\n }\n });\n\n djangoRes.on('end', () => {\n if (streamingDone) {\n return;\n }\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (sseBuffer) {\n // Ensure the final written data ends with \\n\\n so the client recognizes the event boundary\n if (sseBuffer.length < 2 || sseBuffer.substring(sseBuffer.length - 2) !== '\\n\\n') {\n sseBuffer += '\\n\\n';\n }\n log('END: writing remaining buffer len=' + sseBuffer.length + ' start=' + JSON.stringify(sseBuffer.substring(0, 80)));\n try { res.write(sseBuffer); } catch (e) {}\n }\n if (!doneForwarded) {\n log('Injecting [DONE] \u2014 upstream closed without sending it');\n try { res.write('data: [DONE]\\n\\n'); } catch (e) {}\n }\n cleanup();\n try { res.end(); } catch (e) {}\n log('Streaming response completed' + (doneForwarded ? '' : ' (injected [DONE])'));\n callback(null, djangoRes.statusCode, {}, '');\n });\n\n djangoRes.on('error', (err) => {\n log('Django stream error: ' + err.message);\n if (!streamingDone) {\n cleanup();\n }\n if (!res.writableEnded) {\n try {\n res.end();\n } catch (e) {}\n }\n callback(null, 200, {}, '');\n });\n\n res.on('close', () => {\n cleanup();\n log('Client connection closed');\n });\n\n res.on('error', (err) => {\n cleanup();\n log('Client connection error: ' + err.message);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django streaming request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django streaming request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Force stream=false in body because paytaca pay reads the response as text\nfunction forceNonStreaming(body) {\n try {\n const data = JSON.parse(body);\n data.stream = false;\n return JSON.stringify(data);\n } catch {\n return body;\n }\n}\n\n// Convert a chat.completion JSON object to SSE format\nfunction jsonToSse(res, chatCompletion) {\n const content = chatCompletion.choices?.[0]?.message?.content || '';\n const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';\n const created = chatCompletion.created || Math.floor(Date.now() / 1000);\n\n if (!res.headersSent) {\n try {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n } catch (e) {\n return;\n }\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-1',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n } catch (e) {\n }\n\n const chunkSize = 20;\n let chunksWritten = 0;\n for (let i = 0; i < content.length; i += chunkSize) {\n try {\n sseLine(res, {\n id: 'chatcmpl-' + (i + 2),\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { content: content.slice(i, i + chunkSize) }, finish_reason: null }],\n });\n chunksWritten++;\n } catch (e) {\n break;\n }\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-done',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: chatCompletion.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n } catch (e) {\n }\n\n try {\n sseDone(res);\n } catch (e) {\n }\n\n try {\n res.end();\n } catch (e) {\n }\n}\n\n// Run paytaca pay internally and return the response\nfunction runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {\n const url = djangoUrl + '/chat/completions?wallet_hash=' + encodeURIComponent(walletHash || '');\n const payBody = forceNonStreaming(body);\n\n // Write body to a temp file to avoid CLI arg length limits\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paytaca-pay-'));\n const bodyFile = path.join(tmpDir, 'body.json');\n const configFile = path.join(tmpDir, 'config.json');\n\n try {\n fs.writeFileSync(bodyFile, payBody, 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp body file: ' + err.message));\n }\n\n const config = {\n url,\n method: 'POST',\n headers: Object.assign({ 'Content-Type': 'application/json' }, extraHeaders || {}),\n bodyFile,\n confirmed: true,\n };\n\n try {\n fs.writeFileSync(configFile, JSON.stringify(config), 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp config file: ' + err.message));\n }\n\n // Path to the wrapper script\n const wrapperScript = path.join(LOG_DIR, 'paytaca-pay-wrapper.mjs');\n log('Running paytaca pay via wrapper script...');\n\n const child = spawn('node', [wrapperScript, configFile], { shell: false });\n let stdout = '';\n let stderr = '';\n\n child.stdout.on('data', (data) => { \n stdout += data.toString(); \n });\n child.stderr.on('data', (data) => { \n stderr += data.toString(); \n });\n\n child.on('close', (code) => {\n // Clean up temp files\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n\n if (code === 0) {\n try {\n const responseJson = JSON.parse(stdout.trim());\n callback(null, responseJson);\n } catch (err) {\n callback(new Error('Could not parse paytaca pay response: ' + err.message));\n }\n } else {\n callback(new Error(stderr.trim() || 'paytaca pay wrapper exited with code ' + code));\n }\n });\n\n child.on('error', (err) => {\n // Clean up temp files on error\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n callback(new Error('Failed to run paytaca pay wrapper: ' + err.message));\n });\n}\n\n// Extract the last user message content from a chat payload\nfunction getLastUserMessageContent(body) {\n try {\n const data = JSON.parse(body);\n const messages = data.messages || [];\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'user') {\n const content = messages[i].content;\n if (Array.isArray(content)) {\n const parts = [];\n for (const part of content) {\n if (part && typeof part === 'object' && part.type === 'text') {\n parts.push(part.text || '');\n } else if (typeof part === 'string') {\n parts.push(part);\n } else {\n parts.push(JSON.stringify(part));\n }\n }\n return parts.join('').trim().toLowerCase();\n }\n return String(content || '').trim().toLowerCase();\n }\n }\n return '';\n } catch {\n return '';\n }\n}\n\nasync function handleTimeCreditsCommand(res, walletHash) {\n log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');\n const statusUrl = BACKEND_URL + '/v1/wallet/status';\n const statusRes = await fetch(statusUrl, {\n headers: { 'X-Wallet-Hash': walletHash }\n });\n let content;\n if (statusRes.ok) {\n const statusData = await statusRes.json();\n const sessions = statusData.sessions || [];\n const activeSessions = sessions.filter(s => s.time_remaining_seconds > 0 && s.model_active);\n const inactiveSessions = sessions.filter(s => s.time_remaining_seconds > 0 && !s.model_active);\n const parts = [];\n if (activeSessions.length > 0) {\n parts.push('**\u23F1\uFE0F Active Time Credits:**');\n activeSessions.forEach(s => {\n const total = formatDuration(s.time_credits_seconds);\n const remaining = formatDuration(s.time_remaining_seconds);\n const used = formatDuration(s.time_used_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + '** \u2014 ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');\n });\n }\n if (inactiveSessions.length > 0) {\n parts.push('\\n**\u26A0\uFE0F Inactive Model:**');\n inactiveSessions.forEach(s => {\n const remaining = formatDuration(s.time_remaining_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + ' (Inactive)** \u2014 ' + remaining + ' remaining');\n });\n }\n content = parts.length > 0 ? parts.join('\\n') : '\u23F1\uFE0F No active time credits.';\n } else {\n content = '\u23F1\uFE0F Unable to check time credits.';\n }\n\n sseLine(res, {\n id: 'time-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'time-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'time-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\nconst isTimeCmd = (s) => s === 'credits';\nconst isPricingCmd = (s) => s === 'plans';\n\n// List all models grouped by tier (Budget / Premium / Frontier / Other) with prices\nasync function handlePricingCommand(res) {\n log('Pricing command requested');\n let content;\n try {\n const configRes = await fetch(BACKEND_URL + '/v1/config');\n if (!configRes.ok) {\n throw new Error('config status ' + configRes.status);\n }\n const config = await configRes.json();\n const models = Array.isArray(config.models) ? config.models : [];\n const groups = { budget: [], premium: [], frontier: [], other: [] };\n for (const m of models) {\n const key = String(m.tier || '').toLowerCase();\n const groupKey = (key === 'budget' || key === 'premium' || key === 'frontier') ? key : 'other';\n groups[groupKey].push(m);\n }\n const lines = ['\uD83D\uDCCB Paytaca AI \u2014 Model Pricing'];\n const order = [\n { key: 'budget', label: 'Budget' },\n { key: 'premium', label: 'Premium' },\n { key: 'frontier', label: 'Frontier' },\n { key: 'other', label: 'Other' },\n ];\n let any = false;\n for (const g of order) {\n if (groups[g.key].length === 0) continue;\n any = true;\n lines.push('');\n lines.push(g.label);\n for (const m of groups[g.key]) {\n lines.push('');\n const tiers = Array.isArray(m.price_tiers) ? m.price_tiers : [];\n if (tiers.length === 0) {\n lines.push('- **' + (m.display_name || m.id) + '**: \u2014 no pricing configured');\n continue;\n }\n const sorted = tiers.slice().sort((a, b) => (a.minutes || 0) - (b.minutes || 0));\n lines.push('**' + (m.display_name || m.id) + '**:');\n sorted.forEach((t, i) => {\n const sats = typeof t.price_sats === 'number' ? t.price_sats : 0;\n const bch = (sats / 100000000).toFixed(8);\n const php = typeof t.price_php === 'number' ? t.price_php.toFixed(2) : '?.??';\n lines.push(' `(' + String(i + 1) + ')` ' + (t.minutes || 0) + ' minutes \u2014 PHP ' + php + ' (' + bch + ' BCH)');\n });\n }\n }\n if (!any) {\n lines.push('');\n lines.push('No models available.');\n }\n content = lines.join('\\n');\n } catch (err) {\n log('Pricing command failed: ' + err.message);\n content = '\uD83D\uDCCB Unable to fetch pricing.';\n }\n\n sseLine(res, {\n id: 'price-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'price-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'price-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\n// Main proxy server\nconst server = http.createServer(async (req, res) => {\n // Enable CORS\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Wallet-Hash, X-Model-Id, X-Duration-Minutes, Payment-Signature, Authorization');\n \n if (req.method === 'OPTIONS') {\n res.writeHead(200);\n res.end();\n return;\n }\n \n // Discovery endpoint - fetch from backend to get actual config\n if (req.url === '/v1/config' && req.method === 'GET') {\n try {\n const backendConfig = await fetch(BACKEND_URL + '/v1/config');\n if (backendConfig.ok) {\n const config = await backendConfig.json();\n // Add proxy-specific info\n config.proxy_url = 'http://localhost:' + PROXY_PORT + '/v1';\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(config));\n return;\n }\n } catch (err) {\n log('Failed to fetch backend config: ' + err.message);\n }\n \n // Fallback to static values if backend unavailable\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n proxy_url: 'http://localhost:' + PROXY_PORT + '/v1',\n django_url: BACKEND_URL + '/v1',\n payment_address: '',\n default_model: 'deepseek/deepseek-v4-flash',\n default_duration_minutes: 30,\n models: [\n {\n id: 'deepseek/deepseek-v4-flash',\n object: 'model',\n display_name: 'DeepSeek V4 Flash',\n provider: 'openrouter',\n price_tiers: [\n { minutes: 10, price_php: 5.0, price_sats: 45000 },\n { minutes: 30, price_php: 12.0, price_sats: 108000 },\n { minutes: 60, price_php: 20.0, price_sats: 180000 },\n ],\n },\n ],\n context_retention_hours: 2,\n }));\n return;\n }\n \n // All other endpoints \u2014 read body and forward to Django\n let body = '';\n req.on('data', chunk => { body += chunk; });\n req.on('end', async () => {\n try {\n const walletHash = req.headers['x-wallet-hash'];\n const lastContent = getLastUserMessageContent(body);\n \n log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));\n \n const stripSysRem = (s) => { let r = (s || ''), a = '<system-reminder>', b = '</system-reminder>', i = r.indexOf(a); while (i !== -1) { let j = r.indexOf(b, i); if (j === -1) break; r = r.substring(0, i) + r.substring(j + b.length); i = r.indexOf(a); } return r.trim(); };\n \n // Guard: wallet hash is required for payment flow\n if (!walletHash) {\n const redactedHeaders = {};\n for (const [k, v] of Object.entries(req.headers)) {\n const lk = k.toLowerCase();\n redactedHeaders[k] = /authorization|payment-signature|api-?key|secret|token/i.test(lk)\n ? '<redacted>'\n : v;\n }\n log('MISSING X-Wallet-Hash. Received headers: ' + JSON.stringify(redactedHeaders));\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n error: 'X-Wallet-Hash header missing',\n message: 'The X-Wallet-Hash header was not sent by the client. It is injected by the paytaca opencode plugin (provider options.headers / chat.headers). Reinstall or restart the plugin, or run paytaca wallet info and verify the plugin loaded.',\n }));\n return;\n }\n \n // Check if there's a pending payment for this wallet\n var pendingPayload = pendingPayments.get(walletHash);\n \n // If there's a pending payment for a different model, clear it so the\n // new request can be forwarded fresh to Django. This prevents the\n // proxy from re-showing a stale payment prompt when the user switches\n // to a different model mid-conversation.\n if (pendingPayload) {\n var reqModel = '';\n try { reqModel = JSON.parse(body).model || ''; } catch (e) {}\n if (reqModel && pendingPayload.modelId && reqModel !== pendingPayload.modelId) {\n pendingPayments.delete(walletHash);\n pendingPayload = null;\n }\n }\n \n if (pendingPayload) {\n // Check for tier selection first\n if (pendingPayload.step === 'tier_select' && pendingPayload.tiers && pendingPayload.tiers.length > 0) {\n const userInput = stripSysRem(lastContent);\n const timeCmd = userInput?.trim().toLowerCase();\n if (isTimeCmd(timeCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(timeCmd)) {\n await handlePricingCommand(res);\n return;\n }\n let selectedIndex = -1;\n \n // Try to parse user input as a number (1-based)\n const num = parseInt(userInput, 10);\n if (!isNaN(num) && num >= 1 && num <= pendingPayload.tiers.length) {\n selectedIndex = num - 1;\n } else {\n // Try to match by duration minutes\n for (let i = 0; i < pendingPayload.tiers.length; i++) {\n if (userInput === String(pendingPayload.tiers[i].minutes) ||\n userInput === pendingPayload.tiers[i].minutes + ' minutes' ||\n userInput === pendingPayload.tiers[i].minutes + ' min') {\n selectedIndex = i;\n break;\n }\n }\n }\n \n if (selectedIndex >= 0) {\n const selectedTier = pendingPayload.tiers[selectedIndex];\n pendingPayload.durationMinutes = selectedTier.minutes;\n pendingPayload.step = 'processing';\n \n log('Tier selected: ' + selectedTier.minutes + ' min for wallet ' + walletHash?.substring(0, 16) + '...');\n \n // Build extra headers for payment wrapper\n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n extraHeaders['X-Duration-Minutes'] = String(selectedTier.minutes);\n \n // Check wallet balance before attempting payment\n const currentBalanceSats = await getWalletBalance();\n if (currentBalanceSats !== null && selectedTier.price_sats && currentBalanceSats < selectedTier.price_sats) {\n log('Insufficient balance for wallet ' + walletHash?.substring(0, 16) + '...: ' + currentBalanceSats + ' sats < ' + selectedTier.price_sats + ' sats needed');\n pendingPayments.delete(walletHash);\n const addr = await getReceivingAddress();\n const neededBch = (selectedTier.price_sats - currentBalanceSats) / 100000000;\n const neededLine = addr ? '\\n\\n\uD83D\uDCE5 **Fund your wallet:** \\`' + addr + '\\`\\nOr run: paytaca receive (in another terminal) for QR code' : '';\n sseLine(res, {\n id: 'balance-err',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n\\n\u274C **Insufficient balance** \u2014 You have **' + (currentBalanceSats / 100000000).toFixed(8) + ' BCH** but need **' + (selectedTier.price_sats / 100000000).toFixed(8) + ' BCH** for this plan. Top up at least **' + neededBch.toFixed(8) + ' BCH** more.' + neededLine + '\\n\\nType \\`balance\\` to re-check or try a different plan:' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'balance-err-done',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n sseDone(res);\n res.end();\n return;\n }\n \n // Keepalive during payment processing\n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n pendingPayments.delete(walletHash);\n clearInterval(keepalive);\n\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n\u274C Payment failed: ' + err.message }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error');\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n \n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion);\n } catch (e) {}\n } else {\n try {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(chatCompletion));\n } catch (e) {}\n }\n });\n return;\n } else {\n // Invalid selection \u2014 reshow the prompt\n log('Invalid tier selection for wallet ' + walletHash?.substring(0, 16) + '...');\n await streamTierSelectionPrompt(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', pendingPayload.tiers);\n return;\n }\n }\n \n // Old flow: user responded to a yes/no payment prompt\n if (stripSysRem(lastContent) === 'yes') {\n log('Payment approved by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n \n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n if (pendingPayload.durationMinutes) {\n extraHeaders['X-Duration-Minutes'] = String(pendingPayload.durationMinutes);\n }\n \n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n clearInterval(keepalive);\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n\u274C Payment failed: ' + err.message }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error');\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n \n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion);\n } catch (e) {}\n } else {\n try {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(chatCompletion));\n } catch (e) {}\n }\n });\n return;\n \n } else if (stripSysRem(lastContent) === 'no') {\n log('Payment declined by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n\n const addr = await getReceivingAddress();\n const fundMsg = addr\n ? 'Fund your wallet: ' + addr\n : 'You can fund your wallet by running: paytaca receive';\n\n const declineCompletion = {\n id: 'payment-declined',\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: pendingPayload.modelId || 'deepseek/deepseek-v4-flash',\n choices: [{\n index: 0,\n message: {\n role: 'assistant',\n content: 'Payment declined. Chat cannot continue without funding.\\n\\n' + fundMsg,\n },\n finish_reason: 'stop',\n }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n };\n jsonToSse(res, declineCompletion);\n return;\n \n } else {\n const innerCmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(innerCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(innerCmd)) {\n await handlePricingCommand(res);\n return;\n }\n log('New message while payment pending for wallet ' + walletHash?.substring(0, 16) + '...');\n }\n }\n \n // Handle credits command \u2014 show remaining time credits\n const cmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(cmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n // Handle pricing command \u2014 show all models grouped by tier\n if (isPricingCmd(cmd)) {\n await handlePricingCommand(res);\n return;\n }\n \n let isStreaming = true;\n try { isStreaming = JSON.parse(body).stream !== false; } catch {}\n\n const handleResponse = async (err, statusCode, headers, responseBody) => {\n if (err) {\n if (!res.headersSent) {\n log('Django connection error: ' + err.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Backend unavailable', details: err.message }));\n }\n return;\n }\n\n if (statusCode === 402) {\n let requestModel = null;\n try { requestModel = JSON.parse(body).model || null; } catch (e) {}\n log('402 intercepted for wallet ' + walletHash?.substring(0, 16)\n + ' x-model-id=' + (req.headers['x-model-id'] || 'null')\n + ' body.model=' + (requestModel || 'null'));\n \n // Parse 402 response for model_id and price_tiers\n let modelId = null;\n let displayName = null;\n let tiers = null;\n try {\n const parsed = JSON.parse(responseBody);\n modelId = parsed.model_id || null;\n displayName = parsed.display_name || null;\n tiers = parsed.price_tiers || null;\n log('402 body: model=' + (modelId || 'null')\n + ' display=' + (displayName || 'null')\n + ' tiers=' + (Array.isArray(tiers) ? tiers.length : String(tiers))\n + ' reason=' + (parsed.reason || 'n/a')\n + ' bodyPrefix=' + responseBody.substring(0, 160).replace(/\\n/g, ' '));\n } catch (e) {\n log('Could not parse 402 body: ' + e.message);\n }\n \n pendingPayments.set(walletHash, {\n body: body,\n modelId: modelId,\n displayName: displayName,\n durationMinutes: null,\n tiers: tiers,\n step: tiers ? 'tier_select' : 'approval'\n });\n \n if (tiers && tiers.length > 0) {\n // New flow: show tier selection prompt\n await streamTierSelectionPrompt(res, walletHash, displayName || modelId || 'AI Model', tiers);\n return;\n }\n \n // Check session status to determine if this is a renewal\n let isRenewal = false;\n let tokensUsed = 0;\n let tokenLimit = 50000;\n let timeRemainingSeconds = 0;\n \n let statusModelId = modelId;\n let statusSnapshot = null;\n try {\n // Extract model from the original request body if not in 402\n if (!statusModelId) {\n try {\n const bodyParsed = JSON.parse(body);\n statusModelId = bodyParsed.model || null;\n } catch (e) {}\n }\n \n const statusPath = '/v1/wallet/status' + (statusModelId ? '?model_id=' + encodeURIComponent(statusModelId) : '');\n const statusResponse = await new Promise((resolve, reject) => {\n const statusReq = REQUester.get({\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: statusPath,\n headers: { 'X-Wallet-Hash': walletHash }\n }, (res) => {\n let data = '';\n res.on('data', chunk => data += chunk);\n res.on('end', () => {\n try {\n resolve(JSON.parse(data));\n } catch {\n resolve({});\n }\n });\n });\n statusReq.on('error', reject);\n statusReq.setTimeout(5000, () => reject(new Error('timeout')));\n });\n \n if (statusResponse) {\n statusSnapshot = statusResponse;\n tokensUsed = statusResponse.tokens_used || 0;\n tokenLimit = statusResponse.token_limit || 50000;\n timeRemainingSeconds = statusResponse.time_remaining_seconds || 0;\n \n // Renewal if session has been used (tokens > 0 or time > 0) but is now exhausted\n isRenewal = (tokensUsed > 0 || statusResponse.time_used_seconds > 0) &&\n (!statusResponse.session_active || timeRemainingSeconds <= 0);\n }\n } catch (err) {\n log('Failed to check session status: ' + err.message);\n }\n \n log('402 status model=' + (statusModelId || 'null')\n + ' snapshot=' + JSON.stringify(statusSnapshot)\n + ' isRenewal=' + isRenewal\n + ' timeRemaining=' + timeRemainingSeconds\n + ' tokensUsed=' + tokensUsed\n + ' tokenLimit=' + tokenLimit);\n \n await streamLowBalanceNotice(res, displayName || statusModelId || modelId || 'AI Model');\n } else {\n if (res.headersSent) {\n log('Streaming response completed and already sent');\n pendingPayments.delete(walletHash);\n return;\n }\n\n log('Forwarding normal response to OpenCode: status=' + statusCode + ', bodyLen=' + responseBody.length);\n pendingPayments.delete(walletHash);\n res.writeHead(statusCode, {\n 'Content-Type': headers['content-type'] || 'application/json',\n });\n res.end(responseBody);\n }\n };\n\n if (isStreaming) {\n forwardStreaming(req, res, body, handleResponse);\n } else {\n forwardToDjango(req, body, handleResponse);\n }\n \n } catch (err) {\n log('Error: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Internal proxy error' }));\n }\n });\n});\n\nserver.on('error', (err) => {\n if (err.code === 'EADDRINUSE') {\n log('Port ' + PROXY_PORT + ' is already in use. Another proxy instance may be running.');\n log('Exiting cleanly (code 0) so the plugin can detect the existing proxy.');\n process.exit(0);\n }\n log('Server error: ' + err.message);\n process.exit(1);\n});\n\nserver.listen(PROXY_PORT, () => {\n log('Paytaca AI Proxy running on http://localhost:' + PROXY_PORT);\n log('Forwarding to Django at ' + BACKEND_URL);\n log('Discovery: http://localhost:' + PROXY_PORT + '/v1/config');\n log('Managed by OpenCode plugin');\n});\n\n// Graceful shutdown\nprocess.on('SIGTERM', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n\nprocess.on('SIGINT', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n";
1
+ export declare const PROXY_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca AI Proxy\n * \n * Sits between OpenCode and the Django backend.\n * - Auto-starts by OpenCode plugin\n * - On 402, returns SSE typewriter loading sequence + synthetic payment prompt\n * - Stores pending payments; handles \"yes\"/\"no\" approval internally\n * - Uses only Node.js built-in modules\n * \n * Usage: node proxy.js [backend_url] [proxy_port]\n * Example: node proxy.js https://api.paytaca.ai 8001\n */\n\nconst http = require('http');\nconst https = require('https');\nconst { spawn } = require('child_process');\nconst { Transform } = require('stream');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\nconst PROXY_PORT = parseInt(process.argv[3]) || 8001;\nconst BACKEND_URL = process.argv[2] || 'https://api.paytaca.ai';\nconst parsedUrl = new URL(BACKEND_URL);\nconst DJANGO_HOST = parsedUrl.hostname;\nconst DJANGO_PORT = parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80);\nconst REQUester = parsedUrl.protocol === 'https:' ? https : http;\n\n// Logging setup: write to file instead of console\nconst LOG_DIR = path.join(os.homedir(), '.opencode-paytaca');\nif (!fs.existsSync(LOG_DIR)) {\n fs.mkdirSync(LOG_DIR, { recursive: true });\n}\nconst LOG_FILE = path.join(LOG_DIR, 'proxy.log');\nconst logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });\n\nfunction log(message) {\n const timestamp = new Date().toISOString();\n logStream.write(timestamp + ' [Proxy] ' + message + '\\n');\n}\n\n// Store pending payment requests per wallet hash\n// Each entry: { body, modelId, displayName, durationMinutes, tiers[], step }\n// step: 'tier_select' (user must pick a tier) or 'approval' (yes/no)\nconst pendingPayments = new Map();\n\n// Utility: run shell command and return output\nfunction runCommand(cmd, args = []) {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { shell: false });\n let stdout = '';\n let stderr = '';\n \n child.stdout.on('data', (data) => { stdout += data.toString(); });\n child.stderr.on('data', (data) => { stderr += data.toString(); });\n \n child.on('close', (code) => {\n if (code === 0) resolve(stdout.trim());\n else reject(new Error(stderr.trim() || 'Command exited with code ' + code));\n });\n \n child.on('error', (err) => reject(err));\n });\n}\n\n// Get paytaca command from environment or default to 'paytaca'\nconst PAYTACA_CMD = process.env.PAYTACA_CMD || 'paytaca';\n\n// Utility: check if paytaca CLI exists\nasync function checkPaytacaCli() {\n try {\n // Try to run version check\n await runCommand(PAYTACA_CMD, ['--version']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Utility: get wallet balance in sats\nasync function getWalletBalance() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Balance:\\s*([\\d.]+)\\s*BCH/i);\n if (match) {\n const bch = parseFloat(match[1]);\n return Math.floor(bch * 100000000);\n }\n return null;\n } catch (err) {\n log('Failed to get wallet balance: ' + err.message);\n return null;\n }\n}\n\n// Utility: get receiving address\nasync function getReceivingAddress() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Address:\\s*(bitcoincash:[a-zA-Z0-9]+)/i);\n return match ? match[1] : null;\n } catch {\n return null;\n }\n}\n\n// Utility: check if wallet exists\nasync function checkWallet() {\n try {\n await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Format seconds as MM:SS or HH:MM:SS\nfunction formatDuration(totalSeconds) {\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n if (hours > 0) {\n return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');\n }\n return minutes + ':' + String(secs).padStart(2, '0');\n}\n\n// SSE helper: write a data line\nfunction sseLine(res, data) {\n res.write('data: ' + JSON.stringify(data) + '\\n\\n');\n}\n\n// SSE helper: write [DONE]\nfunction sseDone(res) {\n res.write('data: [DONE]\\n\\n');\n}\n\n// Stream the tier-selection prompt body (SSE lines) into an in-progress response.\n// When includeRole is false the leading role delta is skipped, so the body can be\n// appended to a stream that already emitted content (e.g. after a payment failure).\nasync function streamTierSelectionBody(res, walletHash, modelName, tiers, includeRole) {\n if (includeRole !== false) {\n sseLine(res, {\n id: 'tier-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n }\n\n // Loading sequence\n sseLine(res, {\n id: 'tier-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F3 Initializing Paytaca AI provider...\\n' }, finish_reason: null }],\n });\n\n const hasCli = await checkPaytacaCli();\n sseLine(res, {\n id: 'tier-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking Paytaca CLI... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-4',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasCli ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const hasWallet = hasCli ? await checkWallet() : false;\n sseLine(res, {\n id: 'tier-5',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking wallet... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-6',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasWallet ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const balanceSats = hasWallet ? await getWalletBalance() : null;\n sseLine(res, {\n id: 'tier-7',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Fetching balance... ' }, finish_reason: null }],\n });\n\n let balanceStr;\n if (balanceSats !== null) {\n const bch = (balanceSats / 100000000).toFixed(8);\n balanceStr = bch + ' BCH';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u2705 \u2014 ' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n } else {\n balanceStr = 'Unable to check';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n }\n\n // Tier selection\n sseLine(res, {\n id: 'tier-9',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\uD83D\uDCB3 Select a plan for **' + (modelName || 'AI Model') + '**\\n\\n' }, finish_reason: null }],\n });\n\n // Build all tier lines into one string so backtick markdown renders\n // consistently (same as the 'plans' command).\n let tiersContent = '';\n for (let i = 0; i < tiers.length; i++) {\n const tier = tiers[i];\n const bchAmount = (tier.price_sats / 100000000).toFixed(8);\n const label = '`(' + String(i + 1) + ')` ';\n // Display USD price if available, fall back to PHP for legacy backends\n const priceDisplay = tier.price_usd !== undefined && tier.price_usd !== null\n ? 'USD ' + tier.price_usd.toFixed(4)\n : 'PHP ' + (tier.price_php ? tier.price_php.toFixed(2) : '?.??');\n tiersContent += label + tier.minutes + ' minutes \u2014 ' + priceDisplay + ' (' + bchAmount + ' BCH)\\n';\n }\n sseLine(res, {\n id: 'tier-10',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: tiersContent }, finish_reason: null }],\n });\n\n sseLine(res, {\n id: 'tier-11',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\nEnter a number (1-' + tiers.length + '), e.g. type ' + tiers[0].minutes + ':' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'tier-12',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n}\n\n// Build and stream a full tier-selection prompt (headers + body + [DONE]) to the client.\nasync function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {\n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n }\n await streamTierSelectionBody(res, walletHash, modelName, tiers, true);\n sseDone(res);\n res.end();\n}\n\n// Build and stream SSE loading sequence + payment prompt\n// Stream SSE notice when the upstream (OpenRouter) account lacks balance to fund\n// the request. Replaces the old single-tier yes/no approval prompt.\nasync function streamLowBalanceNotice(res, modelName) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n\n sseLine(res, {\n id: 'lb-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName || 'AI Model',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n sseLine(res, {\n id: 'lb-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F OpenRouter balance is low \u2014 please top up before continuing.\\n' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'lb-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n\n sseDone(res);\n res.end();\n}\n\n// Forward request to Django and return response (buffered, for non-streaming)\nfunction forwardToDjango(req, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardToDjango -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n const elapsed = Date.now() - startTime;\n log('Django responded in ' + elapsed + 'ms: status=' + djangoRes.statusCode + ', bodyLen=' + responseBody.length);\n callback(null, djangoRes.statusCode, djangoRes.headers, responseBody);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Forward streaming request to Django\nfunction forwardStreaming(req, res, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardStreaming -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n const elapsed = Date.now() - startTime;\n log('Django response started in ' + elapsed + 'ms: status=' + djangoRes.statusCode);\n\n if (djangoRes.statusCode === 402) {\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n callback(null, 402, djangoRes.headers, responseBody);\n });\n return;\n }\n\n res.writeHead(djangoRes.statusCode, {\n 'Content-Type': djangoRes.headers['content-type'] || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n\n if (res.socket) {\n res.socket.setNoDelay(true);\n }\n\n // Buffer SSE data at event boundaries and inject keepalive between events.\n let sseBuffer = '';\n let lastActivity = Date.now();\n let streamingDone = false;\n let doneForwarded = false;\n\n // Watchdog: inject keepalive only when buffer is empty (between complete events)\n const keepaliveTimer = setInterval(() => {\n if (streamingDone || res.writableEnded || res.destroyed) {\n clearInterval(keepaliveTimer);\n return;\n }\n const now = Date.now();\n if (now - lastActivity >= 2000 && sseBuffer.length === 0) {\n try {\n res.write(': keepalive\\n\\n');\n lastActivity = now;\n } catch (err) {\n log('Keepalive write error: ' + err.message);\n clearInterval(keepaliveTimer);\n }\n }\n }, 500);\n\n const cleanup = () => {\n streamingDone = true;\n clearInterval(keepaliveTimer);\n };\n\n var diagCounter = 0;\n djangoRes.on('data', (chunk) => {\n var chunkStr = chunk.toString();\n var chunkIdx = ++diagCounter;\n sseBuffer += chunkStr;\n lastActivity = Date.now();\n\n var okCount = (sseBuffer.match(/:ok/g) || []).length;\n if (okCount > 0) {\n log('CHUNK#' + chunkIdx + ': ' + okCount + ' :ok in buffer (len=' + sseBuffer.length + ')');\n }\n\n // Strip upstream SSE \":ok\" keepalive comments from anywhere in the buffer.\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (okCount > 0) {\n log('AFTER: stripped ' + okCount + ' :ok, buffer len=' + sseBuffer.length);\n }\n\n var extractedCount = 0;\n let idx;\n while ((idx = sseBuffer.indexOf('\\n\\n')) !== -1) {\n const event = sseBuffer.substring(0, idx + 2);\n sseBuffer = sseBuffer.substring(idx + 2);\n const lines = event.split('\\n').filter(l => !/^:/.test(l) && l.length > 0);\n if (lines.length === 0) continue;\n const cleanEvent = lines.join('\\n') + '\\n\\n';\n extractedCount++;\n var dataContent = lines.map(function(l) { return l.replace(/^data: ?/, ''); }).join('');\n if (dataContent === '[DONE]') { doneForwarded = true; }\n var lastChar = dataContent.slice(-1);\n if (dataContent !== '[DONE]' && lastChar !== '}' && lastChar !== ']') {\n log('FLUSH: truncated event #' + extractedCount + ' (len=' + dataContent.length + ', end=' + JSON.stringify(dataContent.slice(-30)) + ')');\n }\n try {\n res.write(cleanEvent);\n } catch (err) {\n cleanup();\n log('Write error: ' + err.message);\n return;\n }\n }\n if (extractedCount > 0) {\n log('EXTRACT: forwarded ' + extractedCount + ' events in chunk#' + chunkIdx + ', buffer remaining len=' + sseBuffer.length);\n }\n });\n\n djangoRes.on('end', () => {\n if (streamingDone) {\n return;\n }\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (sseBuffer) {\n // Ensure the final written data ends with \\n\\n so the client recognizes the event boundary\n if (sseBuffer.length < 2 || sseBuffer.substring(sseBuffer.length - 2) !== '\\n\\n') {\n sseBuffer += '\\n\\n';\n }\n log('END: writing remaining buffer len=' + sseBuffer.length + ' start=' + JSON.stringify(sseBuffer.substring(0, 80)));\n try { res.write(sseBuffer); } catch (e) {}\n }\n if (!doneForwarded) {\n log('Injecting [DONE] \u2014 upstream closed without sending it');\n try { res.write('data: [DONE]\\n\\n'); } catch (e) {}\n }\n cleanup();\n try { res.end(); } catch (e) {}\n log('Streaming response completed' + (doneForwarded ? '' : ' (injected [DONE])'));\n callback(null, djangoRes.statusCode, {}, '');\n });\n\n djangoRes.on('error', (err) => {\n log('Django stream error: ' + err.message);\n if (!streamingDone) {\n cleanup();\n }\n if (!res.writableEnded) {\n try {\n res.end();\n } catch (e) {}\n }\n callback(null, 200, {}, '');\n });\n\n res.on('close', () => {\n cleanup();\n log('Client connection closed');\n });\n\n res.on('error', (err) => {\n cleanup();\n log('Client connection error: ' + err.message);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django streaming request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django streaming request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Force stream=false in body because paytaca pay reads the response as text\nfunction forceNonStreaming(body) {\n try {\n const data = JSON.parse(body);\n data.stream = false;\n return JSON.stringify(data);\n } catch {\n return body;\n }\n}\n\n// Convert a chat.completion JSON object to SSE format\nfunction jsonToSse(res, chatCompletion, opts) {\n opts = opts || {};\n if (res.destroyed || res.writableEnded) {\n log('jsonToSse: response already destroyed/ended, cannot send SSE');\n return;\n }\n const message = chatCompletion.choices?.[0]?.message || {};\n const content = message.content || '';\n const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : null;\n const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';\n const created = chatCompletion.created || Math.floor(Date.now() / 1000);\n\n if (!res.headersSent) {\n try {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n } catch (e) {\n log('jsonToSse writeHead failed: ' + e.message);\n return;\n }\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-1',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n } catch (e) {\n log('jsonToSse: failed to write role delta: ' + e.message);\n }\n\n const allContent = (opts.prependContent || '') + content;\n const chunkSize = 20;\n let chunksWritten = 0;\n for (let i = 0; i < allContent.length; i += chunkSize) {\n try {\n sseLine(res, {\n id: 'chatcmpl-' + (i + 2),\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { content: allContent.slice(i, i + chunkSize) }, finish_reason: null }],\n });\n chunksWritten++;\n } catch (e) {\n log('jsonToSse: failed to write content chunk ' + (i / chunkSize) + ': ' + e.message);\n break;\n }\n }\n\n let finishReason = 'stop';\n if (toolCalls && toolCalls.length > 0) {\n const toolCallDeltas = [];\n for (let i = 0; i < toolCalls.length; i++) {\n const tc = toolCalls[i] || {};\n const fn = tc.function || {};\n let args = fn.arguments;\n if (args !== undefined && typeof args !== 'string') {\n try { args = JSON.stringify(args); } catch (e) { args = String(args); }\n }\n toolCallDeltas.push({\n index: i,\n id: tc.id || ('call_' + i),\n type: 'function',\n function: {\n name: fn.name || '',\n arguments: args === undefined || args === null ? '' : String(args),\n },\n });\n }\n try {\n sseLine(res, {\n id: 'chatcmpl-tools',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { tool_calls: toolCallDeltas }, finish_reason: null }],\n });\n } catch (e) {\n log('jsonToSse: failed to write tool_calls: ' + e.message);\n }\n finishReason = 'tool_calls';\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-done',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: {}, finish_reason: finishReason }],\n usage: chatCompletion.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n } catch (e) {\n log('jsonToSse: failed to write final delta: ' + e.message);\n }\n\n try {\n sseDone(res);\n } catch (e) {\n log('jsonToSse: failed to write [DONE]: ' + e.message);\n }\n\n try {\n res.end();\n } catch (e) {\n log('jsonToSse: res.end() failed: ' + e.message);\n }\n}\n\n// Stream a payment-failure message, then re-show the tier-selection prompt so the\n// user can retry the same or a different plan without sending another message.\n// The pending payment is restored to the tier-select step so the next tier pick is\n// handled by the proxy instead of being forwarded fresh to Django.\nasync function streamPaymentFailureAndRetry(res, walletHash, pendingPayload, message) {\n try {\n sseLine(res, {\n id: 'pay-err',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { content: message }, finish_reason: 'stop' }],\n });\n } catch (e) {\n }\n pendingPayload.step = 'tier_select';\n pendingPayload.durationMinutes = null;\n pendingPayments.set(walletHash, pendingPayload);\n try {\n const tiers = Array.isArray(pendingPayload.tiers) ? pendingPayload.tiers : [];\n if (tiers.length > 0) {\n await streamTierSelectionBody(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', tiers, false);\n }\n sseDone(res);\n res.end();\n } catch (e) {\n try { res.end(); } catch (e2) {}\n }\n}\n\n// Run paytaca pay internally and return the response\nfunction runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {\n const url = djangoUrl + '/chat/completions?wallet_hash=' + encodeURIComponent(walletHash || '');\n const payBody = forceNonStreaming(body);\n\n // Write body to a temp file to avoid CLI arg length limits\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paytaca-pay-'));\n const bodyFile = path.join(tmpDir, 'body.json');\n const configFile = path.join(tmpDir, 'config.json');\n\n try {\n fs.writeFileSync(bodyFile, payBody, 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp body file: ' + err.message));\n }\n\n const config = {\n url,\n method: 'POST',\n headers: Object.assign({ 'Content-Type': 'application/json' }, extraHeaders || {}),\n bodyFile,\n confirmed: true,\n };\n\n try {\n fs.writeFileSync(configFile, JSON.stringify(config), 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp config file: ' + err.message));\n }\n\n // Path to the wrapper script\n const wrapperScript = path.join(LOG_DIR, 'paytaca-pay-wrapper.mjs');\n log('Running paytaca pay via wrapper script...');\n\n const child = spawn('node', [wrapperScript, configFile], { shell: false });\n let stdout = '';\n let stderr = '';\n\n child.stdout.on('data', (data) => { \n stdout += data.toString(); \n });\n child.stderr.on('data', (data) => { \n stderr += data.toString(); \n });\n\n child.on('close', (code) => {\n // Clean up temp files\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n\n if (code === 0) {\n try {\n const responseJson = JSON.parse(stdout.trim());\n callback(null, responseJson);\n } catch (err) {\n callback(new Error('Could not parse paytaca pay response: ' + err.message));\n }\n } else {\n // Try to extract error from stdout (wrapper writes JSON errors to stdout, not stderr)\n let wrapperErr = stderr.trim();\n if (!wrapperErr) {\n try {\n const parsed = JSON.parse(stdout.trim());\n wrapperErr = parsed.error || 'Unknown error';\n } catch {\n wrapperErr = stdout.trim() || 'paytaca pay wrapper exited with code ' + code;\n }\n }\n callback(new Error(wrapperErr));\n }\n });\n\n child.on('error', (err) => {\n // Clean up temp files on error\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n callback(new Error('Failed to run paytaca pay wrapper: ' + err.message));\n });\n}\n\n// Extract the last user message content from a chat payload\nfunction getLastUserMessageContent(body) {\n try {\n const data = JSON.parse(body);\n const messages = data.messages || [];\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'user') {\n const content = messages[i].content;\n if (Array.isArray(content)) {\n const parts = [];\n for (const part of content) {\n if (part && typeof part === 'object' && part.type === 'text') {\n parts.push(part.text || '');\n } else if (typeof part === 'string') {\n parts.push(part);\n } else {\n parts.push(JSON.stringify(part));\n }\n }\n return parts.join('').trim().toLowerCase();\n }\n return String(content || '').trim().toLowerCase();\n }\n }\n return '';\n } catch {\n return '';\n }\n}\n\nasync function handleTimeCreditsCommand(res, walletHash) {\n log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');\n const statusUrl = BACKEND_URL + '/v1/wallet/status';\n const statusRes = await fetch(statusUrl, {\n headers: { 'X-Wallet-Hash': walletHash }\n });\n let content;\n if (statusRes.ok) {\n const statusData = await statusRes.json();\n const sessions = statusData.sessions || [];\n const activeSessions = sessions.filter(s => s.time_remaining_seconds > 0 && s.model_active);\n const inactiveSessions = sessions.filter(s => s.time_remaining_seconds > 0 && !s.model_active);\n const parts = [];\n if (activeSessions.length > 0) {\n parts.push('**\u23F1\uFE0F Active Time Credits:**');\n activeSessions.forEach(s => {\n const total = formatDuration(s.time_credits_seconds);\n const remaining = formatDuration(s.time_remaining_seconds);\n const used = formatDuration(s.time_used_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + '** \u2014 ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');\n });\n }\n if (inactiveSessions.length > 0) {\n parts.push('\\n**\u26A0\uFE0F Inactive Model:**');\n inactiveSessions.forEach(s => {\n const remaining = formatDuration(s.time_remaining_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + ' (Inactive)** \u2014 ' + remaining + ' remaining');\n });\n }\n content = parts.length > 0 ? parts.join('\\n') : '\u23F1\uFE0F No active time credits.';\n } else {\n content = '\u23F1\uFE0F Unable to check time credits.';\n }\n\n sseLine(res, {\n id: 'time-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'time-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'time-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\nconst isTimeCmd = (s) => s === 'credits';\nconst isPricingCmd = (s) => s === 'plans';\n\n// List all models grouped by tier (Budget / Premium / Frontier / Other) with prices\nasync function handlePricingCommand(res) {\n log('Pricing command requested');\n let content;\n try {\n const configRes = await fetch(BACKEND_URL + '/v1/config');\n if (!configRes.ok) {\n throw new Error('config status ' + configRes.status);\n }\n const config = await configRes.json();\n const models = Array.isArray(config.models) ? config.models : [];\n const groups = { budget: [], premium: [], frontier: [], other: [] };\n for (const m of models) {\n const key = String(m.tier || '').toLowerCase();\n const groupKey = (key === 'budget' || key === 'premium' || key === 'frontier') ? key : 'other';\n groups[groupKey].push(m);\n }\n const lines = ['\uD83D\uDCCB Paytaca AI \u2014 Model Pricing'];\n const order = [\n { key: 'budget', label: 'Budget' },\n { key: 'premium', label: 'Premium' },\n { key: 'frontier', label: 'Frontier' },\n { key: 'other', label: 'Other' },\n ];\n let any = false;\n for (const g of order) {\n if (groups[g.key].length === 0) continue;\n any = true;\n lines.push('');\n lines.push(g.label);\n for (const m of groups[g.key]) {\n lines.push('');\n const tiers = Array.isArray(m.price_tiers) ? m.price_tiers : [];\n if (tiers.length === 0) {\n lines.push('- **' + (m.display_name || m.id) + '**: \u2014 no pricing configured');\n continue;\n }\n const sorted = tiers.slice().sort((a, b) => (a.minutes || 0) - (b.minutes || 0));\n lines.push('**' + (m.display_name || m.id) + '**:');\n sorted.forEach((t, i) => {\n const sats = typeof t.price_sats === 'number' ? t.price_sats : 0;\n const bch = (sats / 100000000).toFixed(8);\n const usd = typeof t.price_usd === 'number' ? t.price_usd.toFixed(4) : '?.??';\n lines.push(' `(' + String(i + 1) + ')` ' + (t.minutes || 0) + ' minutes \u2014 USD ' + usd + ' (' + bch + ' BCH)');\n });\n }\n }\n if (!any) {\n lines.push('');\n lines.push('No models available.');\n }\n content = lines.join('\\n');\n } catch (err) {\n log('Pricing command failed: ' + err.message);\n content = '\uD83D\uDCCB Unable to fetch pricing.';\n }\n\n sseLine(res, {\n id: 'price-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'price-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'price-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\n// Main proxy server\nconst server = http.createServer(async (req, res) => {\n // Enable CORS\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Wallet-Hash, X-Model-Id, X-Duration-Minutes, Payment-Signature, Authorization');\n \n if (req.method === 'OPTIONS') {\n res.writeHead(200);\n res.end();\n return;\n }\n \n // Discovery endpoint - fetch from backend to get actual config\n if (req.url === '/v1/config' && req.method === 'GET') {\n try {\n const backendConfig = await fetch(BACKEND_URL + '/v1/config');\n if (backendConfig.ok) {\n const config = await backendConfig.json();\n // Add proxy-specific info\n config.proxy_url = 'http://localhost:' + PROXY_PORT + '/v1';\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(config));\n return;\n }\n } catch (err) {\n log('Failed to fetch backend config: ' + err.message);\n }\n \n // Fallback to static values if backend unavailable\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n proxy_url: 'http://localhost:' + PROXY_PORT + '/v1',\n django_url: BACKEND_URL + '/v1',\n payment_address: '',\n default_model: 'deepseek/deepseek-v4-flash',\n default_duration_minutes: 30,\n models: [\n {\n id: 'deepseek/deepseek-v4-flash',\n object: 'model',\n display_name: 'DeepSeek V4 Flash',\n provider: 'openrouter',\n price_tiers: [\n { minutes: 10, price_php: 5.0, price_sats: 45000 },\n { minutes: 30, price_php: 12.0, price_sats: 108000 },\n { minutes: 60, price_php: 20.0, price_sats: 180000 },\n ],\n },\n ],\n context_retention_hours: 2,\n }));\n return;\n }\n \n // All other endpoints \u2014 read body and forward to Django\n let body = '';\n req.on('data', chunk => { body += chunk; });\n req.on('end', async () => {\n try {\n const walletHash = req.headers['x-wallet-hash'];\n const lastContent = getLastUserMessageContent(body);\n \n log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));\n \n const stripSysRem = (s) => { let r = (s || ''), a = '<system-reminder>', b = '</system-reminder>', i = r.indexOf(a); while (i !== -1) { let j = r.indexOf(b, i); if (j === -1) break; r = r.substring(0, i) + r.substring(j + b.length); i = r.indexOf(a); } return r.trim(); };\n \n // Guard: wallet hash is required for payment flow\n if (!walletHash) {\n const redactedHeaders = {};\n for (const [k, v] of Object.entries(req.headers)) {\n const lk = k.toLowerCase();\n redactedHeaders[k] = /authorization|payment-signature|api-?key|secret|token/i.test(lk)\n ? '<redacted>'\n : v;\n }\n log('MISSING X-Wallet-Hash. Received headers: ' + JSON.stringify(redactedHeaders));\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n error: 'X-Wallet-Hash header missing',\n message: 'The X-Wallet-Hash header was not sent by the client. It is injected by the paytaca opencode plugin (provider options.headers / chat.headers). Reinstall or restart the plugin, or run paytaca wallet info and verify the plugin loaded.',\n }));\n return;\n }\n \n // Check if there's a pending payment for this wallet\n var pendingPayload = pendingPayments.get(walletHash);\n \n // If there's a pending payment for a different model, clear it so the\n // new request can be forwarded fresh to Django. This prevents the\n // proxy from re-showing a stale payment prompt when the user switches\n // to a different model mid-conversation.\n if (pendingPayload) {\n var reqModel = '';\n try { reqModel = JSON.parse(body).model || ''; } catch (e) {}\n if (reqModel && pendingPayload.modelId && reqModel !== pendingPayload.modelId) {\n pendingPayments.delete(walletHash);\n pendingPayload = null;\n }\n }\n \n if (pendingPayload) {\n // Check for tier selection first\n if (pendingPayload.step === 'tier_select' && pendingPayload.tiers && pendingPayload.tiers.length > 0) {\n const userInput = stripSysRem(lastContent);\n const timeCmd = userInput?.trim().toLowerCase();\n if (isTimeCmd(timeCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(timeCmd)) {\n await handlePricingCommand(res);\n return;\n }\n let selectedIndex = -1;\n \n // Try to parse user input as a number (1-based)\n const num = parseInt(userInput, 10);\n if (!isNaN(num) && num >= 1 && num <= pendingPayload.tiers.length) {\n selectedIndex = num - 1;\n } else {\n // Try to match by duration minutes\n for (let i = 0; i < pendingPayload.tiers.length; i++) {\n if (userInput === String(pendingPayload.tiers[i].minutes) ||\n userInput === pendingPayload.tiers[i].minutes + ' minutes' ||\n userInput === pendingPayload.tiers[i].minutes + ' min') {\n selectedIndex = i;\n break;\n }\n }\n }\n \n if (selectedIndex >= 0) {\n const selectedTier = pendingPayload.tiers[selectedIndex];\n pendingPayload.durationMinutes = selectedTier.minutes;\n pendingPayload.step = 'processing';\n \n log('Tier selected: ' + selectedTier.minutes + ' min for wallet ' + walletHash?.substring(0, 16) + '...');\n \n // Build extra headers for payment wrapper\n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n extraHeaders['X-Duration-Minutes'] = String(selectedTier.minutes);\n \n // Check wallet balance before attempting payment\n const currentBalanceSats = await getWalletBalance();\n if (currentBalanceSats !== null && selectedTier.price_sats && currentBalanceSats < selectedTier.price_sats) {\n log('Insufficient balance for wallet ' + walletHash?.substring(0, 16) + '...: ' + currentBalanceSats + ' sats < ' + selectedTier.price_sats + ' sats needed');\n pendingPayments.delete(walletHash);\n const addr = await getReceivingAddress();\n const neededBch = (selectedTier.price_sats - currentBalanceSats) / 100000000;\n const neededLine = addr ? '\\n\\n\uD83D\uDCE5 **Fund your wallet:** \\`' + addr + '\\`\\nOr run: paytaca receive (in another terminal) for QR code' : '';\n sseLine(res, {\n id: 'balance-err',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n\\n\u274C **Insufficient balance** \u2014 You have **' + (currentBalanceSats / 100000000).toFixed(8) + ' BCH** but need **' + (selectedTier.price_sats / 100000000).toFixed(8) + ' BCH** for this plan. Top up at least **' + neededBch.toFixed(8) + ' BCH** more.' + neededLine + '\\n\\nType \\`balance\\` to re-check or try a different plan:' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'balance-err-done',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n sseDone(res);\n res.end();\n return;\n }\n \n // Keepalive during payment processing\n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, async (err, responseJson) => {\n pendingPayments.delete(walletHash);\n clearInterval(keepalive);\n\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, '\\n\\n\u274C Payment failed: ' + err.message + '\\n\\n');\n } else if (!res.headersSent) {\n try {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n } catch (e) { log('Failed to send payment error response: ' + e.message); }\n } else {\n log('Cannot send payment failure \u2014 response already ended or destroyed');\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error') + '\\n\\n';\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n if (isTimeout) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) { log('Failed to send timeout error via SSE: ' + e.message); }\n } else {\n await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, sseContent);\n }\n } else if (!res.headersSent) {\n try {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n } catch (e) { log('Failed to send payment error JSON: ' + e.message); }\n } else {\n log('Cannot send payment error \u2014 response already ended or destroyed');\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n \n if (res.destroyed || res.writableEnded) {\n log('Payment succeeded but response connection is gone \u2014 cannot deliver chat response');\n return;\n }\n\n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion, { prependContent: '\\n\uD83D\uDCB3 Payment successful \u2014 generating your response...\\n\\n' });\n } catch (e) { log('jsonToSse threw: ' + e.message); }\n } else {\n try {\n if (!res.headersSent) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n }\n res.end(JSON.stringify(chatCompletion));\n } catch (e) { log('Failed to send non-streaming response: ' + e.message); }\n }\n });\n return;\n } else {\n // Invalid selection \u2014 reshow the prompt\n log('Invalid tier selection for wallet ' + walletHash?.substring(0, 16) + '...');\n await streamTierSelectionPrompt(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', pendingPayload.tiers);\n return;\n }\n }\n \n // Old flow: user responded to a yes/no payment prompt\n if (stripSysRem(lastContent) === 'yes') {\n log('Payment approved by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n \n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n if (pendingPayload.durationMinutes) {\n extraHeaders['X-Duration-Minutes'] = String(pendingPayload.durationMinutes);\n }\n \n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n clearInterval(keepalive);\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n\u274C Payment failed: ' + err.message }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) { log('Failed to send payment error via SSE: ' + e.message); }\n } else if (!res.headersSent) {\n try {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n } catch (e) { log('Failed to send payment error JSON: ' + e.message); }\n } else {\n log('Cannot send payment failure \u2014 response already ended or destroyed');\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error');\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) { log('Failed to send error via SSE: ' + e.message); }\n } else if (!res.headersSent) {\n try {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n } catch (e) { log('Failed to send error JSON: ' + e.message); }\n } else {\n log('Cannot send payment error \u2014 response already ended or destroyed');\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n\n if (res.destroyed || res.writableEnded) {\n log('Payment succeeded but response connection is gone \u2014 cannot deliver chat response');\n return;\n }\n \n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion, { prependContent: '\\n\uD83D\uDCB3 Payment successful \u2014 generating your response...\\n\\n' });\n } catch (e) { log('jsonToSse threw: ' + e.message); }\n } else {\n try {\n if (!res.headersSent) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n }\n res.end(JSON.stringify(chatCompletion));\n } catch (e) { log('Failed to send non-streaming response: ' + e.message); }\n }\n });\n return;\n \n } else if (stripSysRem(lastContent) === 'no') {\n log('Payment declined by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n\n const addr = await getReceivingAddress();\n const fundMsg = addr\n ? 'Fund your wallet: ' + addr\n : 'You can fund your wallet by running: paytaca receive';\n\n const declineCompletion = {\n id: 'payment-declined',\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: pendingPayload.modelId || 'deepseek/deepseek-v4-flash',\n choices: [{\n index: 0,\n message: {\n role: 'assistant',\n content: 'Payment declined. Chat cannot continue without funding.\\n\\n' + fundMsg,\n },\n finish_reason: 'stop',\n }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n };\n jsonToSse(res, declineCompletion);\n return;\n \n } else {\n const innerCmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(innerCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(innerCmd)) {\n await handlePricingCommand(res);\n return;\n }\n log('New message while payment pending for wallet ' + walletHash?.substring(0, 16) + '...');\n }\n }\n \n // Handle credits command \u2014 show remaining time credits\n const cmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(cmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n // Handle pricing command \u2014 show all models grouped by tier\n if (isPricingCmd(cmd)) {\n await handlePricingCommand(res);\n return;\n }\n \n let isStreaming = true;\n try { isStreaming = JSON.parse(body).stream !== false; } catch {}\n\n const handleResponse = async (err, statusCode, headers, responseBody) => {\n if (err) {\n if (!res.headersSent) {\n log('Django connection error: ' + err.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Backend unavailable', details: err.message }));\n }\n return;\n }\n\n if (statusCode === 402) {\n let requestModel = null;\n try { requestModel = JSON.parse(body).model || null; } catch (e) {}\n log('402 intercepted for wallet ' + walletHash?.substring(0, 16)\n + ' x-model-id=' + (req.headers['x-model-id'] || 'null')\n + ' body.model=' + (requestModel || 'null'));\n \n // Parse 402 response for model_id and price_tiers\n let modelId = null;\n let displayName = null;\n let tiers = null;\n try {\n const parsed = JSON.parse(responseBody);\n modelId = parsed.model_id || null;\n displayName = parsed.display_name || null;\n tiers = parsed.price_tiers || null;\n log('402 body: model=' + (modelId || 'null')\n + ' display=' + (displayName || 'null')\n + ' tiers=' + (Array.isArray(tiers) ? tiers.length : String(tiers))\n + ' reason=' + (parsed.reason || 'n/a')\n + ' bodyPrefix=' + responseBody.substring(0, 160).replace(/\\n/g, ' '));\n } catch (e) {\n log('Could not parse 402 body: ' + e.message);\n }\n \n pendingPayments.set(walletHash, {\n body: body,\n modelId: modelId,\n displayName: displayName,\n durationMinutes: null,\n tiers: tiers,\n step: tiers ? 'tier_select' : 'approval'\n });\n \n if (tiers && tiers.length > 0) {\n // New flow: show tier selection prompt\n await streamTierSelectionPrompt(res, walletHash, displayName || modelId || 'AI Model', tiers);\n return;\n }\n \n // Check session status to determine if this is a renewal\n let isRenewal = false;\n let tokensUsed = 0;\n let tokenLimit = 50000;\n let timeRemainingSeconds = 0;\n \n let statusModelId = modelId;\n let statusSnapshot = null;\n try {\n // Extract model from the original request body if not in 402\n if (!statusModelId) {\n try {\n const bodyParsed = JSON.parse(body);\n statusModelId = bodyParsed.model || null;\n } catch (e) {}\n }\n \n const statusPath = '/v1/wallet/status' + (statusModelId ? '?model_id=' + encodeURIComponent(statusModelId) : '');\n const statusResponse = await new Promise((resolve, reject) => {\n const statusReq = REQUester.get({\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: statusPath,\n headers: { 'X-Wallet-Hash': walletHash }\n }, (res) => {\n let data = '';\n res.on('data', chunk => data += chunk);\n res.on('end', () => {\n try {\n resolve(JSON.parse(data));\n } catch {\n resolve({});\n }\n });\n });\n statusReq.on('error', reject);\n statusReq.setTimeout(5000, () => reject(new Error('timeout')));\n });\n \n if (statusResponse) {\n statusSnapshot = statusResponse;\n tokensUsed = statusResponse.tokens_used || 0;\n tokenLimit = statusResponse.token_limit || 50000;\n timeRemainingSeconds = statusResponse.time_remaining_seconds || 0;\n \n // Renewal if session has been used (tokens > 0 or time > 0) but is now exhausted\n isRenewal = (tokensUsed > 0 || statusResponse.time_used_seconds > 0) &&\n (!statusResponse.session_active || timeRemainingSeconds <= 0);\n }\n } catch (err) {\n log('Failed to check session status: ' + err.message);\n }\n \n log('402 status model=' + (statusModelId || 'null')\n + ' snapshot=' + JSON.stringify(statusSnapshot)\n + ' isRenewal=' + isRenewal\n + ' timeRemaining=' + timeRemainingSeconds\n + ' tokensUsed=' + tokensUsed\n + ' tokenLimit=' + tokenLimit);\n \n await streamLowBalanceNotice(res, displayName || statusModelId || modelId || 'AI Model');\n } else {\n if (res.headersSent) {\n log('Streaming response completed and already sent');\n pendingPayments.delete(walletHash);\n return;\n }\n\n log('Forwarding normal response to OpenCode: status=' + statusCode + ', bodyLen=' + responseBody.length);\n pendingPayments.delete(walletHash);\n res.writeHead(statusCode, {\n 'Content-Type': headers['content-type'] || 'application/json',\n });\n res.end(responseBody);\n }\n };\n\n if (isStreaming) {\n forwardStreaming(req, res, body, handleResponse);\n } else {\n forwardToDjango(req, body, handleResponse);\n }\n \n } catch (err) {\n log('Error: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Internal proxy error' }));\n }\n });\n});\n\nserver.on('error', (err) => {\n if (err.code === 'EADDRINUSE') {\n log('Port ' + PROXY_PORT + ' is already in use. Another proxy instance may be running.');\n log('Exiting cleanly (code 0) so the plugin can detect the existing proxy.');\n process.exit(0);\n }\n log('Server error: ' + err.message);\n process.exit(1);\n});\n\nserver.listen(PROXY_PORT, () => {\n log('Paytaca AI Proxy running on http://localhost:' + PROXY_PORT);\n log('Forwarding to ' + BACKEND_URL);\n log('Discovery: http://localhost:' + PROXY_PORT + '/v1/config');\n log('Managed by OpenCode plugin');\n});\n\n// Graceful shutdown\nprocess.on('SIGTERM', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n\nprocess.on('SIGINT', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n";
2
2
  //# sourceMappingURL=proxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,m+oDA+3ChC,CAAC"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,kz1DAqgDhC,CAAC"}
@@ -141,22 +141,19 @@ function sseDone(res) {
141
141
  res.write('data: [DONE]\\n\\n');
142
142
  }
143
143
 
144
- // Build and stream SSE tier selection prompt
145
- async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
146
- res.writeHead(200, {
147
- 'Content-Type': 'text/event-stream',
148
- 'Cache-Control': 'no-cache',
149
- 'X-Payment-Required': 'true',
150
- 'Connection': 'keep-alive',
151
- });
152
-
153
- sseLine(res, {
154
- id: 'tier-1',
155
- object: 'chat.completion.chunk',
156
- created: Math.floor(Date.now() / 1000),
157
- model: modelName,
158
- choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
159
- });
144
+ // Stream the tier-selection prompt body (SSE lines) into an in-progress response.
145
+ // When includeRole is false the leading role delta is skipped, so the body can be
146
+ // appended to a stream that already emitted content (e.g. after a payment failure).
147
+ async function streamTierSelectionBody(res, walletHash, modelName, tiers, includeRole) {
148
+ if (includeRole !== false) {
149
+ sseLine(res, {
150
+ id: 'tier-1',
151
+ object: 'chat.completion.chunk',
152
+ created: Math.floor(Date.now() / 1000),
153
+ model: modelName,
154
+ choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
155
+ });
156
+ }
160
157
 
161
158
  // Loading sequence
162
159
  sseLine(res, {
@@ -221,16 +218,24 @@ async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
221
218
  choices: [{ index: 0, delta: { content: '💳 Select a plan for **' + (modelName || 'AI Model') + '**\\n\\n' }, finish_reason: null }],
222
219
  });
223
220
 
221
+ // Build all tier lines into one string so backtick markdown renders
222
+ // consistently (same as the 'plans' command).
223
+ let tiersContent = '';
224
224
  for (let i = 0; i < tiers.length; i++) {
225
225
  const tier = tiers[i];
226
226
  const bchAmount = (tier.price_sats / 100000000).toFixed(8);
227
227
  const label = '\`(' + String(i + 1) + ')\` ';
228
- sseLine(res, {
229
- id: 'tier-10-' + i,
230
- object: 'chat.completion.chunk',
231
- choices: [{ index: 0, delta: { content: label + tier.minutes + ' minutes — PHP ' + tier.price_php.toFixed(2) + ' (' + bchAmount + ' BCH)\\n' }, finish_reason: null }],
232
- });
228
+ // Display USD price if available, fall back to PHP for legacy backends
229
+ const priceDisplay = tier.price_usd !== undefined && tier.price_usd !== null
230
+ ? 'USD ' + tier.price_usd.toFixed(4)
231
+ : 'PHP ' + (tier.price_php ? tier.price_php.toFixed(2) : '?.??');
232
+ tiersContent += label + tier.minutes + ' minutes — ' + priceDisplay + ' (' + bchAmount + ' BCH)\\n';
233
233
  }
234
+ sseLine(res, {
235
+ id: 'tier-10',
236
+ object: 'chat.completion.chunk',
237
+ choices: [{ index: 0, delta: { content: tiersContent }, finish_reason: null }],
238
+ });
234
239
 
235
240
  sseLine(res, {
236
241
  id: 'tier-11',
@@ -244,7 +249,19 @@ async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
244
249
  choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
245
250
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
246
251
  });
252
+ }
247
253
 
254
+ // Build and stream a full tier-selection prompt (headers + body + [DONE]) to the client.
255
+ async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
256
+ if (!res.headersSent) {
257
+ res.writeHead(200, {
258
+ 'Content-Type': 'text/event-stream',
259
+ 'Cache-Control': 'no-cache',
260
+ 'X-Payment-Required': 'true',
261
+ 'Connection': 'keep-alive',
262
+ });
263
+ }
264
+ await streamTierSelectionBody(res, walletHash, modelName, tiers, true);
248
265
  sseDone(res);
249
266
  res.end();
250
267
  }
@@ -536,8 +553,15 @@ function forceNonStreaming(body) {
536
553
  }
537
554
 
538
555
  // Convert a chat.completion JSON object to SSE format
539
- function jsonToSse(res, chatCompletion) {
540
- const content = chatCompletion.choices?.[0]?.message?.content || '';
556
+ function jsonToSse(res, chatCompletion, opts) {
557
+ opts = opts || {};
558
+ if (res.destroyed || res.writableEnded) {
559
+ log('jsonToSse: response already destroyed/ended, cannot send SSE');
560
+ return;
561
+ }
562
+ const message = chatCompletion.choices?.[0]?.message || {};
563
+ const content = message.content || '';
564
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : null;
541
565
  const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';
542
566
  const created = chatCompletion.created || Math.floor(Date.now() / 1000);
543
567
 
@@ -549,6 +573,7 @@ function jsonToSse(res, chatCompletion) {
549
573
  'Connection': 'keep-alive',
550
574
  });
551
575
  } catch (e) {
576
+ log('jsonToSse writeHead failed: ' + e.message);
552
577
  return;
553
578
  }
554
579
  }
@@ -562,45 +587,115 @@ function jsonToSse(res, chatCompletion) {
562
587
  choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
563
588
  });
564
589
  } catch (e) {
590
+ log('jsonToSse: failed to write role delta: ' + e.message);
565
591
  }
566
592
 
593
+ const allContent = (opts.prependContent || '') + content;
567
594
  const chunkSize = 20;
568
595
  let chunksWritten = 0;
569
- for (let i = 0; i < content.length; i += chunkSize) {
596
+ for (let i = 0; i < allContent.length; i += chunkSize) {
570
597
  try {
571
598
  sseLine(res, {
572
599
  id: 'chatcmpl-' + (i + 2),
573
600
  object: 'chat.completion.chunk',
574
601
  created,
575
602
  model,
576
- choices: [{ index: 0, delta: { content: content.slice(i, i + chunkSize) }, finish_reason: null }],
603
+ choices: [{ index: 0, delta: { content: allContent.slice(i, i + chunkSize) }, finish_reason: null }],
577
604
  });
578
605
  chunksWritten++;
579
606
  } catch (e) {
607
+ log('jsonToSse: failed to write content chunk ' + (i / chunkSize) + ': ' + e.message);
580
608
  break;
581
609
  }
582
610
  }
583
611
 
612
+ let finishReason = 'stop';
613
+ if (toolCalls && toolCalls.length > 0) {
614
+ const toolCallDeltas = [];
615
+ for (let i = 0; i < toolCalls.length; i++) {
616
+ const tc = toolCalls[i] || {};
617
+ const fn = tc.function || {};
618
+ let args = fn.arguments;
619
+ if (args !== undefined && typeof args !== 'string') {
620
+ try { args = JSON.stringify(args); } catch (e) { args = String(args); }
621
+ }
622
+ toolCallDeltas.push({
623
+ index: i,
624
+ id: tc.id || ('call_' + i),
625
+ type: 'function',
626
+ function: {
627
+ name: fn.name || '',
628
+ arguments: args === undefined || args === null ? '' : String(args),
629
+ },
630
+ });
631
+ }
632
+ try {
633
+ sseLine(res, {
634
+ id: 'chatcmpl-tools',
635
+ object: 'chat.completion.chunk',
636
+ created,
637
+ model,
638
+ choices: [{ index: 0, delta: { tool_calls: toolCallDeltas }, finish_reason: null }],
639
+ });
640
+ } catch (e) {
641
+ log('jsonToSse: failed to write tool_calls: ' + e.message);
642
+ }
643
+ finishReason = 'tool_calls';
644
+ }
645
+
584
646
  try {
585
647
  sseLine(res, {
586
648
  id: 'chatcmpl-done',
587
649
  object: 'chat.completion.chunk',
588
650
  created,
589
651
  model,
590
- choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
652
+ choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
591
653
  usage: chatCompletion.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
592
654
  });
593
655
  } catch (e) {
656
+ log('jsonToSse: failed to write final delta: ' + e.message);
594
657
  }
595
658
 
596
659
  try {
597
660
  sseDone(res);
598
661
  } catch (e) {
662
+ log('jsonToSse: failed to write [DONE]: ' + e.message);
599
663
  }
600
664
 
601
665
  try {
602
666
  res.end();
603
667
  } catch (e) {
668
+ log('jsonToSse: res.end() failed: ' + e.message);
669
+ }
670
+ }
671
+
672
+ // Stream a payment-failure message, then re-show the tier-selection prompt so the
673
+ // user can retry the same or a different plan without sending another message.
674
+ // The pending payment is restored to the tier-select step so the next tier pick is
675
+ // handled by the proxy instead of being forwarded fresh to Django.
676
+ async function streamPaymentFailureAndRetry(res, walletHash, pendingPayload, message) {
677
+ try {
678
+ sseLine(res, {
679
+ id: 'pay-err',
680
+ object: 'chat.completion.chunk',
681
+ created: Math.floor(Date.now() / 1000),
682
+ model: 'deepseek/deepseek-v4-flash',
683
+ choices: [{ index: 0, delta: { content: message }, finish_reason: 'stop' }],
684
+ });
685
+ } catch (e) {
686
+ }
687
+ pendingPayload.step = 'tier_select';
688
+ pendingPayload.durationMinutes = null;
689
+ pendingPayments.set(walletHash, pendingPayload);
690
+ try {
691
+ const tiers = Array.isArray(pendingPayload.tiers) ? pendingPayload.tiers : [];
692
+ if (tiers.length > 0) {
693
+ await streamTierSelectionBody(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', tiers, false);
694
+ }
695
+ sseDone(res);
696
+ res.end();
697
+ } catch (e) {
698
+ try { res.end(); } catch (e2) {}
604
699
  }
605
700
  }
606
701
 
@@ -665,7 +760,17 @@ function runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {
665
760
  callback(new Error('Could not parse paytaca pay response: ' + err.message));
666
761
  }
667
762
  } else {
668
- callback(new Error(stderr.trim() || 'paytaca pay wrapper exited with code ' + code));
763
+ // Try to extract error from stdout (wrapper writes JSON errors to stdout, not stderr)
764
+ let wrapperErr = stderr.trim();
765
+ if (!wrapperErr) {
766
+ try {
767
+ const parsed = JSON.parse(stdout.trim());
768
+ wrapperErr = parsed.error || 'Unknown error';
769
+ } catch {
770
+ wrapperErr = stdout.trim() || 'paytaca pay wrapper exited with code ' + code;
771
+ }
772
+ }
773
+ callback(new Error(wrapperErr));
669
774
  }
670
775
  });
671
776
 
@@ -811,8 +916,8 @@ async function handlePricingCommand(res) {
811
916
  sorted.forEach((t, i) => {
812
917
  const sats = typeof t.price_sats === 'number' ? t.price_sats : 0;
813
918
  const bch = (sats / 100000000).toFixed(8);
814
- const php = typeof t.price_php === 'number' ? t.price_php.toFixed(2) : '?.??';
815
- lines.push(' \`(' + String(i + 1) + ')\` ' + (t.minutes || 0) + ' minutes — PHP ' + php + ' (' + bch + ' BCH)');
919
+ const usd = typeof t.price_usd === 'number' ? t.price_usd.toFixed(4) : '?.??';
920
+ lines.push(' \`(' + String(i + 1) + ')\` ' + (t.minutes || 0) + ' minutes — USD ' + usd + ' (' + bch + ' BCH)');
816
921
  });
817
922
  }
818
923
  }
@@ -1031,42 +1136,49 @@ const server = http.createServer(async (req, res) => {
1031
1136
  res.write(': keepalive\\n\\n');
1032
1137
  }, 2000);
1033
1138
 
1034
- runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {
1139
+ runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, async (err, responseJson) => {
1035
1140
  pendingPayments.delete(walletHash);
1036
1141
  clearInterval(keepalive);
1037
1142
 
1038
1143
  if (err) {
1039
1144
  log('paytaca pay failed: ' + err.message);
1040
- if (res.headersSent) {
1145
+ if (res.headersSent && !res.destroyed && !res.writableEnded) {
1146
+ await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, '\\n\\n❌ Payment failed: ' + err.message + '\\n\\n');
1147
+ } else if (!res.headersSent) {
1041
1148
  try {
1042
- sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n❌ Payment failed: ' + err.message }, finish_reason: 'stop' }] });
1043
- sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1044
- sseDone(res);
1045
- res.end();
1046
- } catch (e) {}
1149
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1150
+ res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
1151
+ } catch (e) { log('Failed to send payment error response: ' + e.message); }
1047
1152
  } else {
1048
- res.writeHead(500, { 'Content-Type': 'application/json' });
1049
- res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
1153
+ log('Cannot send payment failure — response already ended or destroyed');
1050
1154
  }
1051
1155
  return;
1052
1156
  }
1053
1157
 
1054
1158
  if (!responseJson.success) {
1055
1159
  const isTimeout = responseJson.timeout;
1056
- const sseContent = isTimeout ? '\\n\\n⏱️ Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n❌ Payment failed: ' + (responseJson.error || 'Unknown error');
1160
+ const sseContent = isTimeout ? '\\n\\n⏱️ Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n❌ Payment failed: ' + (responseJson.error || 'Unknown error') + '\\n\\n';
1057
1161
  const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';
1058
1162
  const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;
1059
1163
  const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';
1060
- if (res.headersSent) {
1164
+ if (res.headersSent && !res.destroyed && !res.writableEnded) {
1165
+ if (isTimeout) {
1166
+ try {
1167
+ sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });
1168
+ sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1169
+ sseDone(res);
1170
+ res.end();
1171
+ } catch (e) { log('Failed to send timeout error via SSE: ' + e.message); }
1172
+ } else {
1173
+ await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, sseContent);
1174
+ }
1175
+ } else if (!res.headersSent) {
1061
1176
  try {
1062
- sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });
1063
- sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1064
- sseDone(res);
1065
- res.end();
1066
- } catch (e) {}
1177
+ res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
1178
+ res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
1179
+ } catch (e) { log('Failed to send payment error JSON: ' + e.message); }
1067
1180
  } else {
1068
- res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
1069
- res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
1181
+ log('Cannot send payment error response already ended or destroyed');
1070
1182
  }
1071
1183
  return;
1072
1184
  }
@@ -1080,15 +1192,22 @@ const server = http.createServer(async (req, res) => {
1080
1192
 
1081
1193
  log('paytaca pay succeeded. Returning chat response.');
1082
1194
 
1195
+ if (res.destroyed || res.writableEnded) {
1196
+ log('Payment succeeded but response connection is gone — cannot deliver chat response');
1197
+ return;
1198
+ }
1199
+
1083
1200
  if (wasStreaming) {
1084
1201
  try {
1085
- jsonToSse(res, chatCompletion);
1086
- } catch (e) {}
1202
+ jsonToSse(res, chatCompletion, { prependContent: '\\n💳 Payment successful — generating your response...\\n\\n' });
1203
+ } catch (e) { log('jsonToSse threw: ' + e.message); }
1087
1204
  } else {
1088
1205
  try {
1089
- res.writeHead(200, { 'Content-Type': 'application/json' });
1206
+ if (!res.headersSent) {
1207
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1208
+ }
1090
1209
  res.end(JSON.stringify(chatCompletion));
1091
- } catch (e) {}
1210
+ } catch (e) { log('Failed to send non-streaming response: ' + e.message); }
1092
1211
  }
1093
1212
  });
1094
1213
  return;
@@ -1130,16 +1249,20 @@ const server = http.createServer(async (req, res) => {
1130
1249
  clearInterval(keepalive);
1131
1250
  if (err) {
1132
1251
  log('paytaca pay failed: ' + err.message);
1133
- if (res.headersSent) {
1252
+ if (res.headersSent && !res.destroyed && !res.writableEnded) {
1134
1253
  try {
1135
1254
  sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n❌ Payment failed: ' + err.message }, finish_reason: 'stop' }] });
1136
1255
  sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1137
1256
  sseDone(res);
1138
1257
  res.end();
1139
- } catch (e) {}
1258
+ } catch (e) { log('Failed to send payment error via SSE: ' + e.message); }
1259
+ } else if (!res.headersSent) {
1260
+ try {
1261
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1262
+ res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
1263
+ } catch (e) { log('Failed to send payment error JSON: ' + e.message); }
1140
1264
  } else {
1141
- res.writeHead(500, { 'Content-Type': 'application/json' });
1142
- res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
1265
+ log('Cannot send payment failure — response already ended or destroyed');
1143
1266
  }
1144
1267
  return;
1145
1268
  }
@@ -1150,16 +1273,20 @@ const server = http.createServer(async (req, res) => {
1150
1273
  const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';
1151
1274
  const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;
1152
1275
  const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';
1153
- if (res.headersSent) {
1276
+ if (res.headersSent && !res.destroyed && !res.writableEnded) {
1154
1277
  try {
1155
1278
  sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });
1156
1279
  sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1157
1280
  sseDone(res);
1158
1281
  res.end();
1159
- } catch (e) {}
1282
+ } catch (e) { log('Failed to send error via SSE: ' + e.message); }
1283
+ } else if (!res.headersSent) {
1284
+ try {
1285
+ res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
1286
+ res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
1287
+ } catch (e) { log('Failed to send error JSON: ' + e.message); }
1160
1288
  } else {
1161
- res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
1162
- res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
1289
+ log('Cannot send payment error response already ended or destroyed');
1163
1290
  }
1164
1291
  return;
1165
1292
  }
@@ -1172,16 +1299,23 @@ const server = http.createServer(async (req, res) => {
1172
1299
  } catch {}
1173
1300
 
1174
1301
  log('paytaca pay succeeded. Returning chat response.');
1302
+
1303
+ if (res.destroyed || res.writableEnded) {
1304
+ log('Payment succeeded but response connection is gone — cannot deliver chat response');
1305
+ return;
1306
+ }
1175
1307
 
1176
1308
  if (wasStreaming) {
1177
1309
  try {
1178
- jsonToSse(res, chatCompletion);
1179
- } catch (e) {}
1310
+ jsonToSse(res, chatCompletion, { prependContent: '\\n💳 Payment successful — generating your response...\\n\\n' });
1311
+ } catch (e) { log('jsonToSse threw: ' + e.message); }
1180
1312
  } else {
1181
1313
  try {
1182
- res.writeHead(200, { 'Content-Type': 'application/json' });
1314
+ if (!res.headersSent) {
1315
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1316
+ }
1183
1317
  res.end(JSON.stringify(chatCompletion));
1184
- } catch (e) {}
1318
+ } catch (e) { log('Failed to send non-streaming response: ' + e.message); }
1185
1319
  }
1186
1320
  });
1187
1321
  return;
@@ -1395,7 +1529,7 @@ server.on('error', (err) => {
1395
1529
 
1396
1530
  server.listen(PROXY_PORT, () => {
1397
1531
  log('Paytaca AI Proxy running on http://localhost:' + PROXY_PORT);
1398
- log('Forwarding to Django at ' + BACKEND_URL);
1532
+ log('Forwarding to ' + BACKEND_URL);
1399
1533
  log('Discovery: http://localhost:' + PROXY_PORT + '/v1/config');
1400
1534
  log('Managed by OpenCode plugin');
1401
1535
  });
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+3CnC,CAAC"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqgDnC,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\nasync function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(120000),\n });\n\n const responseHeaders = {};\n response.headers.forEach((value, key) => { responseHeaders[key] = value; });\n const responseText = await response.text();\n let responseData;\n try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }\n\n if (response.status === 402) {\n const paymentRequired = parsePaymentRequiredJson(responseData);\n if (!paymentRequired) {\n return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };\n }\n const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');\n if (!requirements) {\n return {\n success: false, status: 402, error: 'Server does not accept BCH payment',\n data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },\n };\n }\n\n const payerAddress = x402Payer.getPayerAddress();\n const address = requirements.payTo;\n const amountBch = Number(requirements.amount) / 1e8;\n const changeAddressSet = bchWallet.getAddressSetAt(0);\n const changeAddress = changeAddressSet.change;\n\n if (!confirmed) {\n return {\n success: false, status: 402, error: 'Payment not confirmed.',\n payment: { required: true, amount: requirements.amount, payTo: address },\n };\n }\n\n const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);\n if (!sendResult.success) {\n return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };\n }\n\n const txid = sendResult.txid;\n const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);\n headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);\n\n let retryResponse;\n try {\n retryResponse = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(120000),\n });\n } catch (e) {\n if (e.name === 'AbortError') {\n return { success: false, timeout: true, error: 'Response timed out from server.' };\n }\n throw e;\n }\n const retryResponseHeaders = {};\n retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });\n const retryResponseText = await retryResponse.text();\n let retryResponseData;\n try { retryResponseData = JSON.parse(retryResponseText); } catch { retryResponseData = retryResponseText; }\n\n return {\n success: retryResponse.ok,\n status: retryResponse.status,\n statusText: retryResponse.statusText,\n headers: retryResponseHeaders,\n data: retryResponseData,\n payment: { required: true, txid, recipientAddress: address },\n };\n }\n\n return {\n success: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data: responseData,\n payment: { required: false },\n };\n}\n\nmain();\n";
1
+ export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// How long to wait for the server to respond before treating the payment as timed out.\n// Default 240s so heavy non-streaming generations (large context / long output) can\n// complete; override with PAYTACA_PAY_TIMEOUT_MS.\nconst PAY_TIMEOUT_MS = Number(process.env.PAYTACA_PAY_TIMEOUT_MS || 240000);\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\nasync function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n\n const responseHeaders = {};\n response.headers.forEach((value, key) => { responseHeaders[key] = value; });\n const responseText = await response.text();\n let responseData;\n try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }\n\n if (response.status === 402) {\n const paymentRequired = parsePaymentRequiredJson(responseData);\n if (!paymentRequired) {\n return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };\n }\n const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');\n if (!requirements) {\n return {\n success: false, status: 402, error: 'Server does not accept BCH payment',\n data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },\n };\n }\n\n const payerAddress = x402Payer.getPayerAddress();\n const address = requirements.payTo;\n const amountBch = Number(requirements.amount) / 1e8;\n const changeAddressSet = bchWallet.getAddressSetAt(0);\n const changeAddress = changeAddressSet.change;\n\n if (!confirmed) {\n return {\n success: false, status: 402, error: 'Payment not confirmed.',\n payment: { required: true, amount: requirements.amount, payTo: address },\n };\n }\n\n const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);\n if (!sendResult.success) {\n return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };\n }\n\n const txid = sendResult.txid;\n const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);\n headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);\n\n let retryResponse;\n try {\n retryResponse = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n } catch (e) {\n if (e.name === 'AbortError') {\n return { success: false, timeout: true, error: 'Response timed out from server.' };\n }\n throw e;\n }\n const retryResponseHeaders = {};\n retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });\n const retryResponseText = await retryResponse.text();\n let retryResponseData;\n try { retryResponseData = JSON.parse(retryResponseText); } catch { retryResponseData = retryResponseText; }\n\n return {\n success: retryResponse.ok,\n status: retryResponse.status,\n statusText: retryResponse.statusText,\n headers: retryResponseHeaders,\n data: retryResponseData,\n payment: { required: true, txid, recipientAddress: address },\n };\n }\n\n return {\n success: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data: responseData,\n payment: { required: false },\n };\n}\n\nmain();\n";
2
2
  //# sourceMappingURL=wrapper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,i/NAgMlC,CAAC"}
1
+ {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,ozOAqMlC,CAAC"}
@@ -15,6 +15,11 @@ import { execSync } from 'child_process';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { dirname, join } from 'path';
17
17
 
18
+ // How long to wait for the server to respond before treating the payment as timed out.
19
+ // Default 240s so heavy non-streaming generations (large context / long output) can
20
+ // complete; override with PAYTACA_PAY_TIMEOUT_MS.
21
+ const PAY_TIMEOUT_MS = Number(process.env.PAYTACA_PAY_TIMEOUT_MS || 240000);
22
+
18
23
  // Find paytaca-cli installation
19
24
  function findPaytacaCliPath() {
20
25
  const possiblePaths = [];
@@ -111,7 +116,7 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
111
116
  method,
112
117
  headers,
113
118
  body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
114
- signal: AbortSignal.timeout(120000),
119
+ signal: AbortSignal.timeout(PAY_TIMEOUT_MS),
115
120
  });
116
121
 
117
122
  const responseHeaders = {};
@@ -161,7 +166,7 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
161
166
  method,
162
167
  headers,
163
168
  body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
164
- signal: AbortSignal.timeout(120000),
169
+ signal: AbortSignal.timeout(PAY_TIMEOUT_MS),
165
170
  });
166
171
  } catch (e) {
167
172
  if (e.name === 'AbortError') {
@@ -1 +1 @@
1
- {"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgMrC,CAAC"}
1
+ {"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqMrC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAmE5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBpE;AAmDD,wBAAsB,iBAAiB,CAAC,SAAS,GAAE,MAAa,EAAE,OAAO,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrD;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBlH;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA6ItF;AAuBD,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAU5G"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AA4D5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYpE;AAuCD,wBAAsB,iBAAiB,CAAC,SAAS,GAAE,MAAa,EAAE,OAAO,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrD;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBlH;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA+GtF;AAqBD,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAU5G"}
package/dist/proxy.js CHANGED
@@ -46,20 +46,16 @@ const child_process_1 = require("child_process");
46
46
  const config_1 = require("./config");
47
47
  const proxy_1 = require("./bundled/proxy");
48
48
  const wrapper_1 = require("./bundled/wrapper");
49
- // Get path to paytaca binary (multi-strategy resolution)
50
49
  function getPaytacaCommand() {
51
- // Priority 1: Local node_modules (via require.resolve)
52
50
  try {
53
51
  const paytacaCliPkg = require.resolve('paytaca-cli/package.json');
54
52
  return path.resolve(path.dirname(paytacaCliPkg), 'bin', 'paytaca.js');
55
53
  }
56
54
  catch { }
57
- // Priority 2: Local .bin symlink
58
55
  const localPaytaca = path.join(__dirname, '..', 'node_modules', '.bin', 'paytaca');
59
56
  if (fs.existsSync(localPaytaca)) {
60
57
  return localPaytaca;
61
58
  }
62
- // Priority 3: Global npm root
63
59
  try {
64
60
  const globalRoot = (0, child_process_1.execSync)('npm root -g', { encoding: 'utf8' }).trim();
65
61
  const pathsToCheck = [
@@ -73,7 +69,6 @@ function getPaytacaCommand() {
73
69
  }
74
70
  }
75
71
  catch { }
76
- // Priority 4: Common global installation paths
77
72
  const commonPaths = [
78
73
  '/usr/lib/node_modules/paytaca-cli/bin/paytaca.js',
79
74
  '/usr/local/lib/node_modules/paytaca-cli/bin/paytaca.js',
@@ -84,7 +79,6 @@ function getPaytacaCommand() {
84
79
  return p;
85
80
  }
86
81
  }
87
- // Priority 5: which/where on PATH
88
82
  try {
89
83
  const which = process.platform === 'win32' ? 'where' : 'which';
90
84
  const result = (0, child_process_1.execSync)(`${which} paytaca`, { encoding: 'utf8' }).trim().split('\n')[0];
@@ -93,15 +87,10 @@ function getPaytacaCommand() {
93
87
  }
94
88
  }
95
89
  catch { }
96
- // Priority 6: Bare command (rely on PATH at runtime)
97
90
  return 'paytaca';
98
91
  }
99
92
  async function isPortAvailable(port) {
100
93
  return new Promise((resolve) => {
101
- // Bind EXACTLY like the proxy does (no host -> wildcard / dual-stack).
102
- // Probing only 127.0.0.1 would miss an existing proxy listener on the
103
- // IPv6 wildcard address (Node's default listen()), making findAvailablePort
104
- // hand out a port that's actually in use (-> EADDRINUSE on spawn).
105
94
  const server = require('net').createServer();
106
95
  server.once('error', () => {
107
96
  resolve(false);
@@ -113,8 +102,6 @@ async function isPortAvailable(port) {
113
102
  server.listen(port);
114
103
  });
115
104
  }
116
- // Wait until a port is free (or the timeout elapses). Used after killing an
117
- // outdated proxy so the replacement can bind the same port.
118
105
  async function waitForPortFree(port, timeout = 10000) {
119
106
  const start = Date.now();
120
107
  while (Date.now() - start < timeout) {
@@ -125,13 +112,9 @@ async function waitForPortFree(port, timeout = 10000) {
125
112
  }
126
113
  return false;
127
114
  }
128
- // Hash of the bundled proxy script so we can detect when the code changed and
129
- // restart a running proxy instead of silently reusing stale code.
130
115
  function scriptHash() {
131
116
  return crypto.createHash('sha1').update(proxy_1.PROXY_SCRIPT_CONTENT).digest('hex');
132
117
  }
133
- // Persist the pid file + config so every future lookup points at the current
134
- // (known-good) proxy. Centralizes the bookkeeping.
135
118
  function persistProxyInfo(configDir, config, port, pid, currentHash) {
136
119
  fs.writeFileSync((0, config_1.getPidFile)(configDir), pid.toString());
137
120
  config.proxyPort = port;
@@ -139,20 +122,14 @@ function persistProxyInfo(configDir, config, port, pid, currentHash) {
139
122
  config.proxyScriptHash = currentHash;
140
123
  (0, config_1.saveConfig)(configDir, config);
141
124
  }
142
- // Reuse an existing (fresh) proxy: correct pid file + config and hand its
143
- // port back to the caller.
144
125
  async function reuseExistingProxy(configDir, config, status, currentHash) {
145
126
  persistProxyInfo(configDir, config, status.port, status.pid, currentHash);
146
127
  return { port: status.port, pid: status.pid };
147
128
  }
148
- // If a healthy proxy running the current code already exists (e.g. another
149
- // opencode window won the spawn race), adopt it instead of spawning a second
150
- // one. Returns null when no suitable proxy is found.
151
129
  async function tryAdoptExistingFreshProxy(configDir, config, currentHash) {
152
130
  const status = await getProxyStatus(configDir);
153
131
  if (status.running && status.pid && status.port && status.pid > 0 &&
154
132
  (0, config_1.loadConfig)(configDir).proxyScriptHash === currentHash) {
155
- console.log(`[paytaca] Using existing proxy (pid ${status.pid}, port ${status.port}) that started during this session.`);
156
133
  return await reuseExistingProxy(configDir, config, { port: status.port, pid: status.pid }, currentHash);
157
134
  }
158
135
  return null;
@@ -187,7 +164,6 @@ async function getProxyStatus(configDir) {
187
164
  }
188
165
  }
189
166
  catch {
190
- // Invalid PID file
191
167
  }
192
168
  return { running: false };
193
169
  }
@@ -195,51 +171,33 @@ async function startProxy(configDir, config) {
195
171
  const proxyScript = (0, config_1.getProxyScript)(configDir);
196
172
  const logFile = (0, config_1.getLogFile)(configDir);
197
173
  const wrapperScript = (0, config_1.getWrapperScript)(configDir);
198
- // Ensure config directory exists
199
174
  (0, config_1.ensureConfigDir)(configDir);
200
- // Detect if the running proxy was started from different code than this build
201
175
  const currentHash = scriptHash();
202
176
  const persistedConfig = (0, config_1.loadConfig)(configDir);
203
- // Check if proxy already running
204
177
  let existingStatus = await getProxyStatus(configDir);
205
- // A running proxy is stale if it was spawned before the current code hash
206
- // was persisted (i.e. the bundled script has changed since it was started).
207
178
  const needsRestart = existingStatus.running &&
208
179
  (existingStatus.pid || 0) > 0 &&
209
180
  persistedConfig.proxyScriptHash !== currentHash;
210
- // Always rewrite proxy script so code changes take effect on next restart
211
181
  fs.writeFileSync(proxyScript, proxy_1.PROXY_SCRIPT_CONTENT, 'utf8');
212
182
  fs.chmodSync(proxyScript, '755');
213
183
  if (existingStatus.running && existingStatus.pid && existingStatus.port && !needsRestart) {
214
184
  return await reuseExistingProxy(configDir, config, { port: existingStatus.port, pid: existingStatus.pid }, currentHash);
215
185
  }
216
- // A proxy is running but with outdated code — shut it down so the
217
- // replacement can take over (fixes stale code persisting across restarts).
218
186
  if (existingStatus.running && existingStatus.pid) {
219
- console.log(`[paytaca] Proxy (pid ${existingStatus.pid}) runs outdated code; restarting with updated script...`);
220
187
  try {
221
188
  process.kill(existingStatus.pid);
222
189
  }
223
- catch (e) {
224
- console.log(`[paytaca] Could not signal proxy ${existingStatus.pid}: ${e.message}`);
225
- }
190
+ catch { }
226
191
  if (existingStatus.port) {
227
- const freed = await waitForPortFree(existingStatus.port);
228
- if (!freed) {
229
- console.log(`[paytaca] Port ${existingStatus.port} still busy after kill; will use next available port.`);
230
- }
192
+ await waitForPortFree(existingStatus.port);
231
193
  }
232
194
  }
233
- // Another window may have started a fresh proxy while we were deciding
234
- // (e.g. it won the same-port spawn race) — adopt it rather than double-spawn.
235
195
  const adopted = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
236
196
  if (adopted) {
237
197
  return adopted;
238
198
  }
239
- // Write bundled wrapper script to config directory (shared across attempts)
240
199
  fs.writeFileSync(wrapperScript, wrapper_1.WRAPPER_SCRIPT_CONTENT, 'utf8');
241
200
  fs.chmodSync(wrapperScript, '755');
242
- // Start proxy (detached to avoid receiving SIGINT from terminal)
243
201
  const paytacaCmd = getPaytacaCommand();
244
202
  const MAX_ATTEMPTS = 3;
245
203
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
@@ -258,22 +216,15 @@ async function startProxy(configDir, config) {
258
216
  });
259
217
  proxy.unref();
260
218
  if (!proxy.pid) {
261
- console.log(`[paytaca] Spawn attempt ${attempt} failed (no PID); retrying...`);
262
219
  continue;
263
220
  }
264
- // Track whether OUR child exited — if so the port is served by another
265
- // proxy that won the bind race, and we should adopt it instead.
266
221
  let childExited = false;
267
222
  proxy.once('exit', () => { childExited = true; });
268
- // Handle proxy output
269
223
  const logStream = fs.createWriteStream(logFile, { flags: 'a' });
270
224
  proxy.stdout?.pipe(logStream);
271
225
  proxy.stderr?.pipe(logStream);
272
226
  const ready = await waitForProxy(port, 15000);
273
227
  if (childExited) {
274
- // Our child lost the bind race (EADDRINUSE) or crashed immediately.
275
- // Adopt an existing fresh proxy if there is one; otherwise try again.
276
- console.log(`[paytaca] Spawned proxy (pid ${proxy.pid}) exited early on port ${port}; checking for an existing proxy...`);
277
228
  const a = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
278
229
  if (a) {
279
230
  return a;
@@ -281,8 +232,6 @@ async function startProxy(configDir, config) {
281
232
  continue;
282
233
  }
283
234
  if (ready) {
284
- // Confirm our child is genuinely the one serving (it could have exited
285
- // between the readiness poll and here). Persist only when healthy.
286
235
  if (childExited) {
287
236
  const a = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
288
237
  if (a) {
@@ -291,14 +240,11 @@ async function startProxy(configDir, config) {
291
240
  continue;
292
241
  }
293
242
  persistProxyInfo(configDir, config, port, proxy.pid, currentHash);
294
- console.log(`[paytaca] Proxy started on port ${port} (pid ${proxy.pid}).`);
295
243
  return { port, pid: proxy.pid };
296
244
  }
297
- // Child alive but never became ready — give it one more window, then retry.
298
245
  const ready2 = await waitForProxy(port, 15000);
299
246
  if (ready2 && !childExited) {
300
247
  persistProxyInfo(configDir, config, port, proxy.pid, currentHash);
301
- console.log(`[paytaca] Proxy started on port ${port} (pid ${proxy.pid}).`);
302
248
  return { port, pid: proxy.pid };
303
249
  }
304
250
  if (childExited) {
@@ -307,12 +253,12 @@ async function startProxy(configDir, config) {
307
253
  return a;
308
254
  }
309
255
  }
310
- console.log(`[paytaca] Spawn attempt ${attempt} not ready on port ${port}; retrying...`);
311
256
  try {
312
257
  process.kill(proxy.pid);
313
258
  }
314
259
  catch { }
315
260
  }
261
+ console.error(`[paytaca] Proxy failed to start within ${MAX_ATTEMPTS} attempts`);
316
262
  throw new Error(`Proxy failed to start within ${MAX_ATTEMPTS} attempts`);
317
263
  }
318
264
  async function waitForProxy(port, timeout = 30000) {
@@ -327,11 +273,9 @@ async function waitForProxy(port, timeout = 30000) {
327
273
  }
328
274
  }
329
275
  catch {
330
- // Not ready yet
331
276
  }
332
277
  await new Promise(resolve => setTimeout(resolve, 500));
333
278
  }
334
- console.log(`[paytaca] Proxy did not become ready on port ${port} within ${timeout}ms`);
335
279
  return false;
336
280
  }
337
281
  async function getProxyConfig(configDir) {
package/dist/proxy.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paytaca/opencode-plugin",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "OpenCode plugin for Paytaca AI - AI inference provider powered by Bitcoin Cash micropayments",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@opencode-ai/plugin": "^1.17.8",
36
- "paytaca-cli": "^0.4.0"
36
+ "paytaca-cli": "^0.4.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^20.0.0",