@paytaca/opencode-plugin 0.1.13 → 0.1.14

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.
package/README.md CHANGED
@@ -81,7 +81,7 @@ Override via `PAYTACA_BACKEND_URL` environment variable (highest priority).
81
81
  └─────────────────┘
82
82
  ```
83
83
 
84
- The proxy runs as a detached Node.js process with heartbeat monitoring. It auto-exits after 5 minutes without a heartbeat (when all editor windows close).
84
+ The proxy runs as a detached Node.js process. It stays running from the first OpenCode session until the machine shuts down (or the process is killed with `kill <pid>`), so it survives laptop sleep and subsequent OpenCode launches reuse it — no context loss, faster startup.
85
85
 
86
86
  ## License
87
87
 
@@ -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// Heartbeat monitoring - proxy exits if heartbeat is stale\nconst HEARTBEAT_FILE = path.join(LOG_DIR, 'heartbeat');\nconst HEARTBEAT_TIMEOUT = 300000; // 5 minutes\n\nfunction checkHeartbeat() {\n try {\n if (!fs.existsSync(HEARTBEAT_FILE)) {\n // No heartbeat file yet, wait a bit\n return true;\n }\n const heartbeat = parseInt(fs.readFileSync(HEARTBEAT_FILE, 'utf8'));\n if (heartbeat === 0) {\n // Special value: plugin is stopping\n log('Heartbeat = 0, shutting down...');\n return false;\n }\n const elapsed = Date.now() - heartbeat;\n if (elapsed > HEARTBEAT_TIMEOUT) {\n log('Heartbeat stale (' + elapsed + 'ms), shutting down...');\n return false;\n }\n return true;\n } catch (err) {\n // If we can't read heartbeat, keep running (graceful degradation)\n return true;\n }\n}\n\n// Heartbeat checker reference (will be started after server creation)\nlet heartbeatChecker = null;\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 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// Start heartbeat checker after server is created\nheartbeatChecker = setInterval(() => {\n if (!checkHeartbeat()) {\n clearInterval(heartbeatChecker);\n log('Closing server due to missing heartbeat');\n server.close(() => {\n process.exit(0);\n });\n // Force exit after 2 seconds if graceful shutdown fails\n setTimeout(() => process.exit(0), 2000);\n }\n}, 5000);\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// 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";
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,4wrDA06ChC,CAAC"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,m+oDA+3ChC,CAAC"}
@@ -45,37 +45,6 @@ function log(message) {
45
45
  logStream.write(timestamp + ' [Proxy] ' + message + '\\n');
46
46
  }
47
47
 
48
- // Heartbeat monitoring - proxy exits if heartbeat is stale
49
- const HEARTBEAT_FILE = path.join(LOG_DIR, 'heartbeat');
50
- const HEARTBEAT_TIMEOUT = 300000; // 5 minutes
51
-
52
- function checkHeartbeat() {
53
- try {
54
- if (!fs.existsSync(HEARTBEAT_FILE)) {
55
- // No heartbeat file yet, wait a bit
56
- return true;
57
- }
58
- const heartbeat = parseInt(fs.readFileSync(HEARTBEAT_FILE, 'utf8'));
59
- if (heartbeat === 0) {
60
- // Special value: plugin is stopping
61
- log('Heartbeat = 0, shutting down...');
62
- return false;
63
- }
64
- const elapsed = Date.now() - heartbeat;
65
- if (elapsed > HEARTBEAT_TIMEOUT) {
66
- log('Heartbeat stale (' + elapsed + 'ms), shutting down...');
67
- return false;
68
- }
69
- return true;
70
- } catch (err) {
71
- // If we can't read heartbeat, keep running (graceful degradation)
72
- return true;
73
- }
74
- }
75
-
76
- // Heartbeat checker reference (will be started after server creation)
77
- let heartbeatChecker = null;
78
-
79
48
  // Store pending payment requests per wallet hash
80
49
  // Each entry: { body, modelId, displayName, durationMinutes, tiers[], step }
81
50
  // step: 'tier_select' (user must pick a tier) or 'approval' (yes/no)
@@ -1311,6 +1280,7 @@ const server = http.createServer(async (req, res) => {
1311
1280
  pendingPayments.set(walletHash, {
1312
1281
  body: body,
1313
1282
  modelId: modelId,
1283
+ displayName: displayName,
1314
1284
  durationMinutes: null,
1315
1285
  tiers: tiers,
1316
1286
  step: tiers ? 'tier_select' : 'approval'
@@ -1430,19 +1400,6 @@ server.listen(PROXY_PORT, () => {
1430
1400
  log('Managed by OpenCode plugin');
1431
1401
  });
1432
1402
 
1433
- // Start heartbeat checker after server is created
1434
- heartbeatChecker = setInterval(() => {
1435
- if (!checkHeartbeat()) {
1436
- clearInterval(heartbeatChecker);
1437
- log('Closing server due to missing heartbeat');
1438
- server.close(() => {
1439
- process.exit(0);
1440
- });
1441
- // Force exit after 2 seconds if graceful shutdown fails
1442
- setTimeout(() => process.exit(0), 2000);
1443
- }
1444
- }, 5000);
1445
-
1446
1403
  // Graceful shutdown
1447
1404
  process.on('SIGTERM', () => {
1448
1405
  log('Shutting down proxy...');
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA06CnC,CAAC"}
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"}
package/dist/config.d.ts CHANGED
@@ -10,7 +10,4 @@ export declare function getPidFile(configDir: string): string;
10
10
  export declare function getProxyScript(configDir: string): string;
11
11
  export declare function getLogFile(configDir: string): string;
12
12
  export declare function getWrapperScript(configDir: string): string;
13
- export declare function getHeartbeatFile(configDir: string): string;
14
- export declare function updateHeartbeat(configDir: string): void;
15
- export declare function startHeartbeat(configDir: string): NodeJS.Timeout;
16
13
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAGvC,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAiCpD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAIlE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAGD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAQvD;AAGD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAOhE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAGvC,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAkCpD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAIlE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D"}
package/dist/config.js CHANGED
@@ -43,9 +43,6 @@ exports.getPidFile = getPidFile;
43
43
  exports.getProxyScript = getProxyScript;
44
44
  exports.getLogFile = getLogFile;
45
45
  exports.getWrapperScript = getWrapperScript;
46
- exports.getHeartbeatFile = getHeartbeatFile;
47
- exports.updateHeartbeat = updateHeartbeat;
48
- exports.startHeartbeat = startHeartbeat;
49
46
  const fs = __importStar(require("fs"));
50
47
  const path = __importStar(require("path"));
51
48
  // Default production backend
@@ -84,6 +81,7 @@ function loadConfig(configDir) {
84
81
  proxyPort: parsed.proxyPort || exports.DEFAULT_PROXY_PORT,
85
82
  walletHash: parsed.walletHash,
86
83
  proxyPid: parsed.proxyPid,
84
+ proxyScriptHash: parsed.proxyScriptHash,
87
85
  };
88
86
  }
89
87
  catch (err) {
@@ -113,27 +111,4 @@ function getLogFile(configDir) {
113
111
  function getWrapperScript(configDir) {
114
112
  return path.join(configDir, 'paytaca-pay-wrapper.mjs');
115
113
  }
116
- function getHeartbeatFile(configDir) {
117
- return path.join(configDir, 'heartbeat');
118
- }
119
- // Touch the heartbeat file to signal the proxy we're still alive
120
- function updateHeartbeat(configDir) {
121
- const heartbeatFile = getHeartbeatFile(configDir);
122
- try {
123
- // Write current timestamp
124
- fs.writeFileSync(heartbeatFile, Date.now().toString());
125
- }
126
- catch (err) {
127
- // Ignore errors
128
- }
129
- }
130
- // Start heartbeat interval that updates every 5 seconds
131
- function startHeartbeat(configDir) {
132
- // Update immediately
133
- updateHeartbeat(configDir);
134
- // Then every 5 seconds
135
- return setInterval(() => {
136
- updateHeartbeat(configDir);
137
- }, 5000);
138
- }
139
114
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/proxy.d.ts CHANGED
@@ -8,7 +8,6 @@ export declare function getProxyStatus(configDir: string): Promise<{
8
8
  pid?: number;
9
9
  }>;
10
10
  export declare function startProxy(configDir: string, config: Config): Promise<ProxyInfo>;
11
- export declare function stopProxy(configDir: string): Promise<void>;
12
11
  export declare function getProxyConfig(configDir: string): Promise<{
13
12
  backendUrl: string;
14
13
  port: number;
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAuE5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYpE;AAED,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,CAkGtF;AAED,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBhE;AAsBD,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;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"}
package/dist/proxy.js CHANGED
@@ -38,16 +38,14 @@ exports.findAvailablePort = findAvailablePort;
38
38
  exports.isProcessRunning = isProcessRunning;
39
39
  exports.getProxyStatus = getProxyStatus;
40
40
  exports.startProxy = startProxy;
41
- exports.stopProxy = stopProxy;
42
41
  exports.getProxyConfig = getProxyConfig;
43
42
  const fs = __importStar(require("fs"));
44
43
  const path = __importStar(require("path"));
44
+ const crypto = __importStar(require("crypto"));
45
45
  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
- // Store heartbeat interval reference
50
- let heartbeatInterval = null;
51
49
  // Get path to paytaca binary (multi-strategy resolution)
52
50
  function getPaytacaCommand() {
53
51
  // Priority 1: Local node_modules (via require.resolve)
@@ -100,6 +98,10 @@ function getPaytacaCommand() {
100
98
  }
101
99
  async function isPortAvailable(port) {
102
100
  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).
103
105
  const server = require('net').createServer();
104
106
  server.once('error', () => {
105
107
  resolve(false);
@@ -108,9 +110,53 @@ async function isPortAvailable(port) {
108
110
  server.close();
109
111
  resolve(true);
110
112
  });
111
- server.listen(port, '127.0.0.1');
113
+ server.listen(port);
112
114
  });
113
115
  }
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
+ async function waitForPortFree(port, timeout = 10000) {
119
+ const start = Date.now();
120
+ while (Date.now() - start < timeout) {
121
+ if (await isPortAvailable(port)) {
122
+ return true;
123
+ }
124
+ await new Promise(resolve => setTimeout(resolve, 300));
125
+ }
126
+ return false;
127
+ }
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
+ function scriptHash() {
131
+ return crypto.createHash('sha1').update(proxy_1.PROXY_SCRIPT_CONTENT).digest('hex');
132
+ }
133
+ // Persist the pid file + config so every future lookup points at the current
134
+ // (known-good) proxy. Centralizes the bookkeeping.
135
+ function persistProxyInfo(configDir, config, port, pid, currentHash) {
136
+ fs.writeFileSync((0, config_1.getPidFile)(configDir), pid.toString());
137
+ config.proxyPort = port;
138
+ config.proxyPid = pid;
139
+ config.proxyScriptHash = currentHash;
140
+ (0, config_1.saveConfig)(configDir, config);
141
+ }
142
+ // Reuse an existing (fresh) proxy: correct pid file + config and hand its
143
+ // port back to the caller.
144
+ async function reuseExistingProxy(configDir, config, status, currentHash) {
145
+ persistProxyInfo(configDir, config, status.port, status.pid, currentHash);
146
+ return { port: status.port, pid: status.pid };
147
+ }
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
+ async function tryAdoptExistingFreshProxy(configDir, config, currentHash) {
152
+ const status = await getProxyStatus(configDir);
153
+ if (status.running && status.pid && status.port && status.pid > 0 &&
154
+ (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
+ return await reuseExistingProxy(configDir, config, { port: status.port, pid: status.pid }, currentHash);
157
+ }
158
+ return null;
159
+ }
114
160
  async function findAvailablePort(startPort = 8001, endPort = 8010) {
115
161
  for (let port = startPort; port <= endPort; port++) {
116
162
  if (await isPortAvailable(port)) {
@@ -146,113 +192,128 @@ async function getProxyStatus(configDir) {
146
192
  return { running: false };
147
193
  }
148
194
  async function startProxy(configDir, config) {
149
- const pidFile = (0, config_1.getPidFile)(configDir);
150
195
  const proxyScript = (0, config_1.getProxyScript)(configDir);
151
196
  const logFile = (0, config_1.getLogFile)(configDir);
152
197
  const wrapperScript = (0, config_1.getWrapperScript)(configDir);
153
198
  // Ensure config directory exists
154
199
  (0, config_1.ensureConfigDir)(configDir);
200
+ // Detect if the running proxy was started from different code than this build
201
+ const currentHash = scriptHash();
202
+ const persistedConfig = (0, config_1.loadConfig)(configDir);
203
+ // Check if proxy already running
204
+ 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
+ const needsRestart = existingStatus.running &&
208
+ (existingStatus.pid || 0) > 0 &&
209
+ persistedConfig.proxyScriptHash !== currentHash;
155
210
  // Always rewrite proxy script so code changes take effect on next restart
156
211
  fs.writeFileSync(proxyScript, proxy_1.PROXY_SCRIPT_CONTENT, 'utf8');
157
212
  fs.chmodSync(proxyScript, '755');
158
- // Check if proxy already running
159
- const existingStatus = await getProxyStatus(configDir);
160
- if (existingStatus.running && existingStatus.pid && existingStatus.port) {
161
- // Just reuse existing proxy, update heartbeat
162
- const heartbeatFile = (0, config_1.getHeartbeatFile)(configDir);
163
- fs.writeFileSync(heartbeatFile, Date.now().toString());
164
- // Start heartbeat updates
165
- if (heartbeatInterval) {
166
- clearInterval(heartbeatInterval);
213
+ if (existingStatus.running && existingStatus.pid && existingStatus.port && !needsRestart) {
214
+ return await reuseExistingProxy(configDir, config, { port: existingStatus.port, pid: existingStatus.pid }, currentHash);
215
+ }
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
+ if (existingStatus.running && existingStatus.pid) {
219
+ console.log(`[paytaca] Proxy (pid ${existingStatus.pid}) runs outdated code; restarting with updated script...`);
220
+ try {
221
+ process.kill(existingStatus.pid);
222
+ }
223
+ catch (e) {
224
+ console.log(`[paytaca] Could not signal proxy ${existingStatus.pid}: ${e.message}`);
167
225
  }
168
- heartbeatInterval = setInterval(() => {
169
- try {
170
- fs.writeFileSync(heartbeatFile, Date.now().toString());
226
+ 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.`);
171
230
  }
172
- catch { }
173
- }, 5000);
174
- return {
175
- port: existingStatus.port,
176
- pid: existingStatus.pid
177
- };
231
+ }
178
232
  }
179
- // Find available port
180
- const port = await findAvailablePort(8001, 8010);
181
- // Write bundled wrapper script to config directory
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
+ const adopted = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
236
+ if (adopted) {
237
+ return adopted;
238
+ }
239
+ // Write bundled wrapper script to config directory (shared across attempts)
182
240
  fs.writeFileSync(wrapperScript, wrapper_1.WRAPPER_SCRIPT_CONTENT, 'utf8');
183
241
  fs.chmodSync(wrapperScript, '755');
184
242
  // Start proxy (detached to avoid receiving SIGINT from terminal)
185
243
  const paytacaCmd = getPaytacaCommand();
186
- const proxy = (0, child_process_1.spawn)('node', [
187
- proxyScript,
188
- config.backendUrl,
189
- port.toString()
190
- ], {
191
- detached: true,
192
- stdio: ['ignore', 'pipe', 'pipe'],
193
- env: {
194
- ...process.env,
195
- PAYTACA_CMD: paytacaCmd
244
+ const MAX_ATTEMPTS = 3;
245
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
246
+ const port = await findAvailablePort(8001, 8010);
247
+ const proxy = (0, child_process_1.spawn)('node', [
248
+ proxyScript,
249
+ config.backendUrl,
250
+ port.toString()
251
+ ], {
252
+ detached: true,
253
+ stdio: ['ignore', 'pipe', 'pipe'],
254
+ env: {
255
+ ...process.env,
256
+ PAYTACA_CMD: paytacaCmd
257
+ }
258
+ });
259
+ proxy.unref();
260
+ if (!proxy.pid) {
261
+ console.log(`[paytaca] Spawn attempt ${attempt} failed (no PID); retrying...`);
262
+ continue;
196
263
  }
197
- });
198
- proxy.unref();
199
- if (!proxy.pid) {
200
- throw new Error('Failed to start proxy: no PID available');
201
- }
202
- // Write PID file
203
- fs.writeFileSync(pidFile, proxy.pid.toString());
204
- // Update config - new proxy (preserve wallet hash from passed config)
205
- config.proxyPort = port;
206
- config.proxyPid = proxy.pid;
207
- (0, config_1.saveConfig)(configDir, config);
208
- // Initialize heartbeat file
209
- const heartbeatFile = (0, config_1.getHeartbeatFile)(configDir);
210
- fs.writeFileSync(heartbeatFile, Date.now().toString());
211
- // Start heartbeat updates
212
- if (heartbeatInterval) {
213
- clearInterval(heartbeatInterval);
214
- }
215
- heartbeatInterval = setInterval(() => {
216
- try {
217
- fs.writeFileSync(heartbeatFile, Date.now().toString());
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
+ let childExited = false;
267
+ proxy.once('exit', () => { childExited = true; });
268
+ // Handle proxy output
269
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
270
+ proxy.stdout?.pipe(logStream);
271
+ proxy.stderr?.pipe(logStream);
272
+ const ready = await waitForProxy(port, 15000);
273
+ 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
+ const a = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
278
+ if (a) {
279
+ return a;
280
+ }
281
+ continue;
218
282
  }
219
- catch { }
220
- }, 5000);
221
- // Handle proxy output
222
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
223
- proxy.stdout?.pipe(logStream);
224
- proxy.stderr?.pipe(logStream);
225
- // Wait for proxy to be ready
226
- await waitForProxy(port);
227
- return {
228
- port,
229
- pid: proxy.pid
230
- };
231
- }
232
- async function stopProxy(configDir) {
233
- // Stop heartbeat updates
234
- if (heartbeatInterval) {
235
- clearInterval(heartbeatInterval);
236
- heartbeatInterval = null;
237
- }
238
- // Note: We don't kill the proxy here anymore.
239
- // The proxy monitors the heartbeat file and exits itself when stale.
240
- // This handles multi-window scenarios correctly.
241
- // Optional: Write a special "stopping" timestamp to speed up proxy shutdown
242
- const heartbeatFile = (0, config_1.getHeartbeatFile)(configDir);
243
- try {
244
- fs.writeFileSync(heartbeatFile, '0'); // Special value: stopping
245
- // Remove heartbeat file after a short delay
246
- setTimeout(() => {
247
- try {
248
- if (fs.existsSync(heartbeatFile)) {
249
- fs.unlinkSync(heartbeatFile);
283
+ 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
+ if (childExited) {
287
+ const a = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
288
+ if (a) {
289
+ return a;
250
290
  }
291
+ continue;
292
+ }
293
+ persistProxyInfo(configDir, config, port, proxy.pid, currentHash);
294
+ console.log(`[paytaca] Proxy started on port ${port} (pid ${proxy.pid}).`);
295
+ return { port, pid: proxy.pid };
296
+ }
297
+ // Child alive but never became ready — give it one more window, then retry.
298
+ const ready2 = await waitForProxy(port, 15000);
299
+ if (ready2 && !childExited) {
300
+ persistProxyInfo(configDir, config, port, proxy.pid, currentHash);
301
+ console.log(`[paytaca] Proxy started on port ${port} (pid ${proxy.pid}).`);
302
+ return { port, pid: proxy.pid };
303
+ }
304
+ if (childExited) {
305
+ const a = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
306
+ if (a) {
307
+ return a;
251
308
  }
252
- catch { }
253
- }, 100);
309
+ }
310
+ console.log(`[paytaca] Spawn attempt ${attempt} not ready on port ${port}; retrying...`);
311
+ try {
312
+ process.kill(proxy.pid);
313
+ }
314
+ catch { }
254
315
  }
255
- catch { }
316
+ throw new Error(`Proxy failed to start within ${MAX_ATTEMPTS} attempts`);
256
317
  }
257
318
  async function waitForProxy(port, timeout = 30000) {
258
319
  const start = Date.now();
@@ -262,7 +323,7 @@ async function waitForProxy(port, timeout = 30000) {
262
323
  signal: AbortSignal.timeout(8000)
263
324
  });
264
325
  if (response.ok) {
265
- return;
326
+ return true;
266
327
  }
267
328
  }
268
329
  catch {
@@ -270,7 +331,8 @@ async function waitForProxy(port, timeout = 30000) {
270
331
  }
271
332
  await new Promise(resolve => setTimeout(resolve, 500));
272
333
  }
273
- throw new Error(`Proxy failed to start on port ${port} within ${timeout}ms`);
334
+ console.log(`[paytaca] Proxy did not become ready on port ${port} within ${timeout}ms`);
335
+ return false;
274
336
  }
275
337
  async function getProxyConfig(configDir) {
276
338
  const status = await getProxyStatus(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/dist/types.d.ts CHANGED
@@ -3,6 +3,7 @@ export interface Config {
3
3
  proxyPort: number;
4
4
  walletHash?: string;
5
5
  proxyPid?: number;
6
+ proxyScriptHash?: string;
6
7
  }
7
8
  export interface WalletInfo {
8
9
  exists: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paytaca/opencode-plugin",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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",