@paytaca/opencode-plugin 0.1.7 → 0.1.8

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
@@ -33,7 +33,7 @@ Once installed and configured in your OpenCode settings, the plugin automaticall
33
33
 
34
34
  - Creates a wallet on first run (recovery phrase is printed — save it securely)
35
35
  - Starts a local proxy that manages x402 payment flows transparently
36
- - Provides the `paytaca-ai` provider with the `deepseek-ai/DeepSeek-V4-Flash` model
36
+ - Provides the `paytaca-ai` provider with the `deepseek/deepseek-v4-flash` model
37
37
 
38
38
  ### Wallet management
39
39
 
@@ -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 15 seconds without a heartbeat (when all editor windows close).
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).
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 = 15000; // 15 seconds\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) + '\uFE0F\u20E3 ';\n sseLine(res, {\n id: 'tier-10-' + i,\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: label + tier.minutes + ' minutes \u2014 \u20B1' + 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\nasync function streamPaymentPrompt(res, walletHash, isRenewal = false, tokensUsed = 0, tokenLimit = 50000, timeRemainingSeconds = 0) {\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 // Fetch dynamic pricing from backend config\n let costPhp = 10.00;\n let costBch = '0.00080000';\n let costSats = 80000;\n let usingDefaultRate = false;\n \n try {\n const configRes = await fetch(BACKEND_URL + '/v1/config');\n if (configRes.ok) {\n const config = await configRes.json();\n costPhp = config.cost_php || 10.00;\n costBch = config.cost_bch || '0.00080000';\n costSats = config.cost_sats || 80000;\n }\n } catch (e) {\n // Backend unreachable \u2014 will warn user below\n usingDefaultRate = true;\n }\n \n const baseId = isRenewal ? 'renewal' : 'payment';\n\n sseLine(res, {\n id: baseId + '-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek-ai/DeepSeek-V4-Flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let balanceStr;\n let hasCli, hasWallet, balanceSats;\n\n if (isRenewal) {\n // For renewals, skip the full loading sequence and fetch balance quietly\n hasCli = await checkPaytacaCli();\n hasWallet = hasCli ? await checkWallet() : false;\n balanceSats = hasWallet ? await getWalletBalance() : null;\n if (balanceSats !== null) {\n balanceStr = (balanceSats / 100000000).toFixed(8) + ' BCH';\n } else {\n balanceStr = 'Unable to check (try restarting)';\n }\n } else {\n // First-time users: show full loading sequence\n sseLine(res, {\n id: baseId + '-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F3 Initializing Paytaca AI provider...\\n' }, finish_reason: null }],\n });\n\n hasCli = await checkPaytacaCli();\n sseLine(res, {\n id: baseId + '-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking Paytaca CLI... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: baseId + '-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 hasWallet = hasCli ? await checkWallet() : false;\n sseLine(res, {\n id: baseId + '-5',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking wallet... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: baseId + '-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 balanceSats = hasWallet ? await getWalletBalance() : null;\n sseLine(res, {\n id: baseId + '-7',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Fetching balance... ' }, finish_reason: null }],\n });\n\n if (balanceSats !== null) {\n balanceStr = (balanceSats / 100000000).toFixed(8) + ' BCH';\n sseLine(res, {\n id: baseId + '-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u2705\\n\\n' }, finish_reason: null }],\n });\n } else {\n balanceStr = 'Unable to check (try restarting)';\n sseLine(res, {\n id: baseId + '-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u274C\\n\\n' }, finish_reason: null }],\n });\n }\n }\n\n let promptHeader = isRenewal\n ? '\uD83D\uDCB3 Session Expired \u2014 Payment Required to Continue\\n\\n'\n : '\uD83D\uDCB3 Paytaca AI \u2014 Payment Required\\n\\n';\n \n sseLine(res, {\n id: baseId + '-9',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: promptHeader }, finish_reason: null }],\n });\n \n sseLine(res, {\n id: baseId + '-10',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Cost: ' + costPhp.toFixed(2) + ' PHP (~' + costBch + ' BCH)\\n' }, finish_reason: null }],\n });\n \n if (usingDefaultRate) {\n sseLine(res, {\n id: baseId + '-10b',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F Could not reach backend for live pricing. Using default rate.\\n' }, finish_reason: null }],\n });\n }\n \n if (isRenewal) {\n const usedMinutes = Math.round(timeRemainingSeconds / 60);\n const remainingAttr = usedMinutes > 0 ? usedMinutes + ' min remaining' : 'depleted';\n sseLine(res, {\n id: baseId + '-11',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Previous Session: ' + tokensUsed.toLocaleString() + ' / ' + tokenLimit.toLocaleString() + ' tokens used\\n' }, finish_reason: null }],\n });\n sseLine(res, {\n id: baseId + '-12',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F1 Time Credits: ' + remainingAttr + '\\n' }, finish_reason: null }],\n });\n }\n \n sseLine(res, {\n id: baseId + '-13',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Wallet Balance: ' + balanceStr + '\\n' }, finish_reason: null }],\n });\n \n if (balanceSats !== null) {\n const affordable = Math.floor(balanceSats / costSats);\n sseLine(res, {\n id: baseId + '-14',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'You could afford about ~' + affordable + ' sessions\\n\\n' }, finish_reason: null }],\n });\n }\n \n if (balanceSats !== null && balanceSats < costSats) {\n const addr = await getReceivingAddress();\n if (addr) {\n sseLine(res, {\n id: baseId + '-15',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F Insufficient balance for a session.\\nFund your wallet: ' + addr + '\\nOr run: paytaca receive (in another terminal) for QR code\\n\\n' }, finish_reason: null }],\n });\n }\n }\n \n if (balanceSats === null || balanceSats > 0) {\n sseLine(res, {\n id: baseId + '-16',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Approve payment? (yes/no)' }, finish_reason: 'stop' }],\n });\n }\n \n sseLine(res, {\n id: baseId + '-17',\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 const djangoReq = REQUester.request(options, (djangoRes) => {\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(30000, () => {\n djangoReq.destroy();\n callback(new Error('Django request timed out after 30s'));\n });\n\n djangoReq.on('error', (err) => {\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 const djangoReq = REQUester.request(options, (djangoRes) => {\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 djangoRes.pipe(res);\n \n res.on('close', () => {\n log('Client connection closed');\n });\n \n djangoRes.on('end', () => {\n callback(null, djangoRes.statusCode, {}, '');\n });\n });\n\n djangoReq.setTimeout(30000, () => {\n djangoReq.destroy();\n callback(new Error('Django streaming request timed out after 30s'));\n });\n\n djangoReq.on('error', (err) => {\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-ai/DeepSeek-V4-Flash';\n const created = chatCompletion.created || Math.floor(Date.now() / 1000);\n\n\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 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\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-ai/DeepSeek-V4-Flash',\n default_duration_minutes: 30,\n models: [\n {\n id: 'deepseek-ai/DeepSeek-V4-Flash',\n object: 'model',\n display_name: 'DeepSeek V4 Flash',\n provider: 'gmi',\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 // DEBUG: Log full body and parsed content\n \n log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));\n \n // Guard: wallet hash is required for payment flow\n if (!walletHash) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'X-Wallet-Hash header required' }));\n return;\n }\n \n // Check if there's a pending payment for this wallet\n const pendingPayload = pendingPayments.get(walletHash);\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 = lastContent.trim();\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 runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n pendingPayments.delete(walletHash);\n \n if (err) {\n log('paytaca pay failed: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: err.message,\n details: 'Please check your wallet balance and try again.'\n }));\n return;\n }\n \n if (!responseJson.success) {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: responseJson.error,\n details: 'Payment was not successful. Please check your balance and try again.'\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 (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 runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n if (err) {\n log('paytaca pay failed: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: err.message,\n details: 'Please check your wallet balance and try again.'\n }));\n return;\n }\n \n if (!responseJson.success) {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: responseJson.error,\n details: 'Payment was not successful. Please check your balance and try again.'\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 (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-ai/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 log('New message while payment pending for wallet ' + walletHash?.substring(0, 16) + '...');\n }\n }\n \n // Handle time/credits command \u2014 show remaining time credits\n const cmd = lastContent?.trim().toLowerCase();\n if (cmd === 'time' || cmd === 'credits') {\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 if (sessions.length === 0) {\n content = '\u23F1\uFE0F No active time credits.';\n } else {\n const lines = sessions.map(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 return ' - **' + (s.display_name || s.ai_model) + '** \u2014 ' + remaining + ' remaining of ' + total + ' purchased (' + used + ' used)';\n });\n content = '\u23F1\uFE0F **Active Time Credits:**\\n' + lines.join('\\n');\n }\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-ai/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 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 log('402 intercepted for wallet ' + walletHash?.substring(0, 16) + '...');\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 } 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 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 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 await streamPaymentPrompt(res, walletHash, isRenewal, tokensUsed, tokenLimit, timeRemainingSeconds);\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// 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) + '\uFE0F\u20E3 ';\n sseLine(res, {\n id: 'tier-10-' + i,\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: label + tier.minutes + ' minutes \u2014 \u20B1' + 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\nasync function streamPaymentPrompt(res, walletHash, isRenewal = false, tokensUsed = 0, tokenLimit = 50000, timeRemainingSeconds = 0) {\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 // Fetch dynamic pricing from backend config\n let costPhp = 10.00;\n let costBch = '0.00080000';\n let costSats = 80000;\n let usingDefaultRate = false;\n \n try {\n const configRes = await fetch(BACKEND_URL + '/v1/config');\n if (configRes.ok) {\n const config = await configRes.json();\n costPhp = config.cost_php || 10.00;\n costBch = config.cost_bch || '0.00080000';\n costSats = config.cost_sats || 80000;\n }\n } catch (e) {\n // Backend unreachable \u2014 will warn user below\n usingDefaultRate = true;\n }\n \n const baseId = isRenewal ? 'renewal' : 'payment';\n\n sseLine(res, {\n id: baseId + '-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\n let balanceStr;\n let hasCli, hasWallet, balanceSats;\n\n if (isRenewal) {\n // For renewals, skip the full loading sequence and fetch balance quietly\n hasCli = await checkPaytacaCli();\n hasWallet = hasCli ? await checkWallet() : false;\n balanceSats = hasWallet ? await getWalletBalance() : null;\n if (balanceSats !== null) {\n balanceStr = (balanceSats / 100000000).toFixed(8) + ' BCH';\n } else {\n balanceStr = 'Unable to check (try restarting)';\n }\n } else {\n // First-time users: show full loading sequence\n sseLine(res, {\n id: baseId + '-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F3 Initializing Paytaca AI provider...\\n' }, finish_reason: null }],\n });\n\n hasCli = await checkPaytacaCli();\n sseLine(res, {\n id: baseId + '-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking Paytaca CLI... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: baseId + '-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 hasWallet = hasCli ? await checkWallet() : false;\n sseLine(res, {\n id: baseId + '-5',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking wallet... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: baseId + '-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 balanceSats = hasWallet ? await getWalletBalance() : null;\n sseLine(res, {\n id: baseId + '-7',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Fetching balance... ' }, finish_reason: null }],\n });\n\n if (balanceSats !== null) {\n balanceStr = (balanceSats / 100000000).toFixed(8) + ' BCH';\n sseLine(res, {\n id: baseId + '-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u2705\\n\\n' }, finish_reason: null }],\n });\n } else {\n balanceStr = 'Unable to check (try restarting)';\n sseLine(res, {\n id: baseId + '-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u274C\\n\\n' }, finish_reason: null }],\n });\n }\n }\n\n let promptHeader = isRenewal\n ? '\uD83D\uDCB3 Session Expired \u2014 Payment Required to Continue\\n\\n'\n : '\uD83D\uDCB3 Paytaca AI \u2014 Payment Required\\n\\n';\n \n sseLine(res, {\n id: baseId + '-9',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: promptHeader }, finish_reason: null }],\n });\n \n sseLine(res, {\n id: baseId + '-10',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Cost: ' + costPhp.toFixed(2) + ' PHP (~' + costBch + ' BCH)\\n' }, finish_reason: null }],\n });\n \n if (usingDefaultRate) {\n sseLine(res, {\n id: baseId + '-10b',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F Could not reach backend for live pricing. Using default rate.\\n' }, finish_reason: null }],\n });\n }\n \n if (isRenewal) {\n const usedMinutes = Math.round(timeRemainingSeconds / 60);\n const remainingAttr = usedMinutes > 0 ? usedMinutes + ' min remaining' : 'depleted';\n sseLine(res, {\n id: baseId + '-11',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Previous Session: ' + tokensUsed.toLocaleString() + ' / ' + tokenLimit.toLocaleString() + ' tokens used\\n' }, finish_reason: null }],\n });\n sseLine(res, {\n id: baseId + '-12',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F1 Time Credits: ' + remainingAttr + '\\n' }, finish_reason: null }],\n });\n }\n \n sseLine(res, {\n id: baseId + '-13',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Wallet Balance: ' + balanceStr + '\\n' }, finish_reason: null }],\n });\n \n if (balanceSats !== null) {\n const affordable = Math.floor(balanceSats / costSats);\n sseLine(res, {\n id: baseId + '-14',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'You could afford about ~' + affordable + ' sessions\\n\\n' }, finish_reason: null }],\n });\n }\n \n if (balanceSats !== null && balanceSats < costSats) {\n const addr = await getReceivingAddress();\n if (addr) {\n sseLine(res, {\n id: baseId + '-15',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F Insufficient balance for a session.\\nFund your wallet: ' + addr + '\\nOr run: paytaca receive (in another terminal) for QR code\\n\\n' }, finish_reason: null }],\n });\n }\n }\n \n if (balanceSats === null || balanceSats > 0) {\n sseLine(res, {\n id: baseId + '-16',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Approve payment? (yes/no)' }, finish_reason: 'stop' }],\n });\n }\n \n sseLine(res, {\n id: baseId + '-17',\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 const djangoReq = REQUester.request(options, (djangoRes) => {\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(30000, () => {\n djangoReq.destroy();\n callback(new Error('Django request timed out after 30s'));\n });\n\n djangoReq.on('error', (err) => {\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 const djangoReq = REQUester.request(options, (djangoRes) => {\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 djangoRes.pipe(res);\n \n res.on('close', () => {\n log('Client connection closed');\n });\n \n djangoRes.on('end', () => {\n callback(null, djangoRes.statusCode, {}, '');\n });\n });\n\n djangoReq.setTimeout(30000, () => {\n djangoReq.destroy();\n callback(new Error('Django streaming request timed out after 30s'));\n });\n\n djangoReq.on('error', (err) => {\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\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 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\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 // DEBUG: Log full body and parsed content\n \n log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));\n \n // Guard: wallet hash is required for payment flow\n if (!walletHash) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'X-Wallet-Hash header required' }));\n return;\n }\n \n // Check if there's a pending payment for this wallet\n const pendingPayload = pendingPayments.get(walletHash);\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 = lastContent.trim();\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 runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n pendingPayments.delete(walletHash);\n \n if (err) {\n log('paytaca pay failed: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: err.message,\n details: 'Please check your wallet balance and try again.'\n }));\n return;\n }\n \n if (!responseJson.success) {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: responseJson.error,\n details: 'Payment was not successful. Please check your balance and try again.'\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 (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 runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n if (err) {\n log('paytaca pay failed: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: err.message,\n details: 'Please check your wallet balance and try again.'\n }));\n return;\n }\n \n if (!responseJson.success) {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n error: 'Payment failed', \n message: responseJson.error,\n details: 'Payment was not successful. Please check your balance and try again.'\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 (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 log('New message while payment pending for wallet ' + walletHash?.substring(0, 16) + '...');\n }\n }\n \n // Handle time/credits command \u2014 show remaining time credits\n const cmd = lastContent?.trim().toLowerCase();\n if (cmd === 'time' || cmd === 'credit' || cmd === 'credits') {\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 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 log('402 intercepted for wallet ' + walletHash?.substring(0, 16) + '...');\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 } 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 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 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 await streamPaymentPrompt(res, walletHash, isRenewal, tokensUsed, tokenLimit, timeRemainingSeconds);\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";
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,gi3CAiuChC,CAAC"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,gp4CA0uChC,CAAC"}
@@ -47,7 +47,7 @@ function log(message) {
47
47
 
48
48
  // Heartbeat monitoring - proxy exits if heartbeat is stale
49
49
  const HEARTBEAT_FILE = path.join(LOG_DIR, 'heartbeat');
50
- const HEARTBEAT_TIMEOUT = 15000; // 15 seconds
50
+ const HEARTBEAT_TIMEOUT = 300000; // 5 minutes
51
51
 
52
52
  function checkHeartbeat() {
53
53
  try {
@@ -314,7 +314,7 @@ async function streamPaymentPrompt(res, walletHash, isRenewal = false, tokensUse
314
314
  id: baseId + '-1',
315
315
  object: 'chat.completion.chunk',
316
316
  created: Math.floor(Date.now() / 1000),
317
- model: 'deepseek-ai/DeepSeek-V4-Flash',
317
+ model: 'deepseek/deepseek-v4-flash',
318
318
  choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
319
319
  });
320
320
 
@@ -587,7 +587,7 @@ function forceNonStreaming(body) {
587
587
  // Convert a chat.completion JSON object to SSE format
588
588
  function jsonToSse(res, chatCompletion) {
589
589
  const content = chatCompletion.choices?.[0]?.message?.content || '';
590
- const model = chatCompletion.model || chatCompletion.model_id || 'deepseek-ai/DeepSeek-V4-Flash';
590
+ const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';
591
591
  const created = chatCompletion.created || Math.floor(Date.now() / 1000);
592
592
 
593
593
 
@@ -793,14 +793,14 @@ const server = http.createServer(async (req, res) => {
793
793
  proxy_url: 'http://localhost:' + PROXY_PORT + '/v1',
794
794
  django_url: BACKEND_URL + '/v1',
795
795
  payment_address: '',
796
- default_model: 'deepseek-ai/DeepSeek-V4-Flash',
796
+ default_model: 'deepseek/deepseek-v4-flash',
797
797
  default_duration_minutes: 30,
798
798
  models: [
799
799
  {
800
- id: 'deepseek-ai/DeepSeek-V4-Flash',
800
+ id: 'deepseek/deepseek-v4-flash',
801
801
  object: 'model',
802
802
  display_name: 'DeepSeek V4 Flash',
803
- provider: 'gmi',
803
+ provider: 'openrouter',
804
804
  price_tiers: [
805
805
  { minutes: 10, price_php: 5.0, price_sats: 45000 },
806
806
  { minutes: 30, price_php: 12.0, price_sats: 108000 },
@@ -1018,7 +1018,7 @@ const server = http.createServer(async (req, res) => {
1018
1018
  id: 'payment-declined',
1019
1019
  object: 'chat.completion',
1020
1020
  created: Math.floor(Date.now() / 1000),
1021
- model: pendingPayload.modelId || 'deepseek-ai/DeepSeek-V4-Flash',
1021
+ model: pendingPayload.modelId || 'deepseek/deepseek-v4-flash',
1022
1022
  choices: [{
1023
1023
  index: 0,
1024
1024
  message: {
@@ -1039,7 +1039,7 @@ const server = http.createServer(async (req, res) => {
1039
1039
 
1040
1040
  // Handle time/credits command — show remaining time credits
1041
1041
  const cmd = lastContent?.trim().toLowerCase();
1042
- if (cmd === 'time' || cmd === 'credits') {
1042
+ if (cmd === 'time' || cmd === 'credit' || cmd === 'credits') {
1043
1043
  log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');
1044
1044
  const statusUrl = BACKEND_URL + '/v1/wallet/status';
1045
1045
  const statusRes = await fetch(statusUrl, {
@@ -1049,17 +1049,26 @@ const server = http.createServer(async (req, res) => {
1049
1049
  if (statusRes.ok) {
1050
1050
  const statusData = await statusRes.json();
1051
1051
  const sessions = statusData.sessions || [];
1052
- if (sessions.length === 0) {
1053
- content = '⏱️ No active time credits.';
1054
- } else {
1055
- const lines = sessions.map(s => {
1052
+ const activeSessions = sessions.filter(s => s.time_remaining_seconds > 0 && s.model_active);
1053
+ const inactiveSessions = sessions.filter(s => s.time_remaining_seconds > 0 && !s.model_active);
1054
+ const parts = [];
1055
+ if (activeSessions.length > 0) {
1056
+ parts.push('**⏱️ Active Time Credits:**');
1057
+ activeSessions.forEach(s => {
1056
1058
  const total = formatDuration(s.time_credits_seconds);
1057
1059
  const remaining = formatDuration(s.time_remaining_seconds);
1058
1060
  const used = formatDuration(s.time_used_seconds);
1059
- return ' - **' + (s.display_name || s.ai_model) + '** — ' + remaining + ' remaining of ' + total + ' purchased (' + used + ' used)';
1061
+ parts.push(' - **' + (s.display_name || s.ai_model) + '** — ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');
1062
+ });
1063
+ }
1064
+ if (inactiveSessions.length > 0) {
1065
+ parts.push('\\n**⚠️ Inactive Model:**');
1066
+ inactiveSessions.forEach(s => {
1067
+ const remaining = formatDuration(s.time_remaining_seconds);
1068
+ parts.push(' - **' + (s.display_name || s.ai_model) + ' (Inactive)** — ' + remaining + ' remaining');
1060
1069
  });
1061
- content = '⏱️ **Active Time Credits:**\\n' + lines.join('\\n');
1062
1070
  }
1071
+ content = parts.length > 0 ? parts.join('\\n') : '⏱️ No active time credits.';
1063
1072
  } else {
1064
1073
  content = '⏱️ Unable to check time credits.';
1065
1074
  }
@@ -1068,7 +1077,7 @@ const server = http.createServer(async (req, res) => {
1068
1077
  id: 'time-1',
1069
1078
  object: 'chat.completion.chunk',
1070
1079
  created: Math.floor(Date.now() / 1000),
1071
- model: 'deepseek-ai/DeepSeek-V4-Flash',
1080
+ model: 'deepseek/deepseek-v4-flash',
1072
1081
  choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
1073
1082
  });
1074
1083
  sseLine(res, {
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiuCnC,CAAC"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0uCnC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,iBAAe,cAAc,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,GAAG;;;;kBA+DlC,GAAG;6BA8CQ,GAAG,UAAU,GAAG;GAMlD;;;;;AAED,kBAAoE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,iBAAe,cAAc,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,GAAG;;;;kBA6FlC,GAAG;6BA8CQ,GAAG,UAAU,GAAG;GAMlD;;;;;AAED,kBAAoE"}
package/dist/index.js CHANGED
@@ -94,6 +94,36 @@ async function OpencodePlugin(_input, _options) {
94
94
  }
95
95
  // Start or reuse proxy
96
96
  const proxy = await (0, proxy_1.startProxy)(configDir, config);
97
+ // Auto-install paytaca-wallet skill globally (copy from paytaca-cli dependency)
98
+ try {
99
+ const skillCandidates = [
100
+ path.join(os.homedir(), '.config', 'opencode', 'skills'),
101
+ path.join(os.homedir(), 'Library', 'Application Support', 'opencode', 'skills'),
102
+ ];
103
+ if (process.env.APPDATA) {
104
+ skillCandidates.push(path.join(process.env.APPDATA, 'opencode', 'skills'));
105
+ }
106
+ let globalSkillsDir = skillCandidates[0];
107
+ for (const d of skillCandidates) {
108
+ if (fs.existsSync(d) || d === skillCandidates[0]) {
109
+ globalSkillsDir = d;
110
+ break;
111
+ }
112
+ }
113
+ const walletSkillDir = path.join(globalSkillsDir, 'paytaca-wallet');
114
+ if (!fs.existsSync(walletSkillDir)) {
115
+ const paytacaCliPkg = require.resolve('paytaca-cli/package.json');
116
+ const srcSkillDir = path.resolve(path.dirname(paytacaCliPkg), 'skills', 'paytaca-wallet');
117
+ const srcSkillFile = path.join(srcSkillDir, 'SKILL.md');
118
+ if (fs.existsSync(srcSkillFile)) {
119
+ fs.mkdirSync(walletSkillDir, { recursive: true });
120
+ fs.copyFileSync(srcSkillFile, path.join(walletSkillDir, 'SKILL.md'));
121
+ }
122
+ }
123
+ }
124
+ catch (e) {
125
+ console.error('Failed to install paytaca-wallet skill:', e.message);
126
+ }
97
127
  return {
98
128
  config: async (cfg) => {
99
129
  cfg.provider = cfg.provider || {};
@@ -122,7 +152,7 @@ async function OpencodePlugin(_input, _options) {
122
152
  }
123
153
  // Fallback if no models fetched
124
154
  if (Object.keys(models).length === 0) {
125
- models['deepseek-ai/DeepSeek-V4-Flash'] = {
155
+ models['deepseek/deepseek-v4-flash'] = {
126
156
  name: 'DeepSeek V4 Flash',
127
157
  limit: {
128
158
  context: 128000,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paytaca/opencode-plugin",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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",