@paytaca/opencode-plugin 0.1.7 → 0.1.9
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 +2 -2
- package/dist/bundled/proxy.d.ts +1 -1
- package/dist/bundled/proxy.d.ts.map +1 -1
- package/dist/bundled/proxy.js +323 -102
- package/dist/bundled/proxy.js.map +1 -1
- package/dist/bundled/wrapper.d.ts +1 -1
- package/dist/bundled/wrapper.d.ts.map +1 -1
- package/dist/bundled/wrapper.js +15 -5
- package/dist/bundled/wrapper.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/proxy.js +3 -3
- package/dist/proxy.js.map +1 -1
- package/package.json +3 -2
- package/scripts/sync-cache.js +10 -0
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
|
|
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
|
|
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
|
|
package/dist/bundled/proxy.d.ts
CHANGED
|
@@ -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 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 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 cleanup();\n log('Django stream error: ' + err.message);\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\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 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 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 (timeCmd === 'time' || timeCmd === 'credit' || timeCmd === 'credits') {\n await handleTimeCreditsCommand(res, walletHash);\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 \\'time\\' 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 \\'time\\'.' : '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 \\'time\\' 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 \\'time\\'.' : '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 (innerCmd === 'time' || innerCmd === 'credit' || innerCmd === 'credits') {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\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 = stripSysRem(lastContent?.trim().toLowerCase());\n if (cmd === 'time' || cmd === 'credit' || cmd === 'credits') {\n await handleTimeCreditsCommand(res, walletHash);\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,
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,6qsDA87ChC,CAAC"}
|
package/dist/bundled/proxy.js
CHANGED
|
@@ -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 =
|
|
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
|
|
317
|
+
model: 'deepseek/deepseek-v4-flash',
|
|
318
318
|
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
319
319
|
});
|
|
320
320
|
|
|
@@ -488,7 +488,14 @@ function forwardToDjango(req, body, callback) {
|
|
|
488
488
|
const startTime = Date.now();
|
|
489
489
|
log('forwardToDjango -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);
|
|
490
490
|
|
|
491
|
+
let timeoutCleared = false;
|
|
491
492
|
const djangoReq = REQUester.request(options, (djangoRes) => {
|
|
493
|
+
// Response started; clear the connect/first-byte timeout so slow streams aren't killed.
|
|
494
|
+
if (!timeoutCleared) {
|
|
495
|
+
djangoReq.clearTimeout();
|
|
496
|
+
timeoutCleared = true;
|
|
497
|
+
}
|
|
498
|
+
|
|
492
499
|
let responseBody = '';
|
|
493
500
|
djangoRes.on('data', chunk => { responseBody += chunk; });
|
|
494
501
|
djangoRes.on('end', () => {
|
|
@@ -498,12 +505,16 @@ function forwardToDjango(req, body, callback) {
|
|
|
498
505
|
});
|
|
499
506
|
});
|
|
500
507
|
|
|
501
|
-
djangoReq.setTimeout(
|
|
508
|
+
djangoReq.setTimeout(300000, () => {
|
|
502
509
|
djangoReq.destroy();
|
|
503
|
-
callback(new Error('Django request timed out after
|
|
510
|
+
callback(new Error('Django request timed out after 300s'));
|
|
504
511
|
});
|
|
505
512
|
|
|
506
513
|
djangoReq.on('error', (err) => {
|
|
514
|
+
if (!timeoutCleared) {
|
|
515
|
+
djangoReq.clearTimeout();
|
|
516
|
+
timeoutCleared = true;
|
|
517
|
+
}
|
|
507
518
|
log('Django request error: ' + err.message);
|
|
508
519
|
callback(err);
|
|
509
520
|
});
|
|
@@ -529,7 +540,14 @@ function forwardStreaming(req, res, body, callback) {
|
|
|
529
540
|
const startTime = Date.now();
|
|
530
541
|
log('forwardStreaming -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);
|
|
531
542
|
|
|
543
|
+
let timeoutCleared = false;
|
|
532
544
|
const djangoReq = REQUester.request(options, (djangoRes) => {
|
|
545
|
+
// Response started; clear the connect/first-byte timeout so slow streams aren't killed.
|
|
546
|
+
if (!timeoutCleared) {
|
|
547
|
+
djangoReq.clearTimeout();
|
|
548
|
+
timeoutCleared = true;
|
|
549
|
+
}
|
|
550
|
+
|
|
533
551
|
const elapsed = Date.now() - startTime;
|
|
534
552
|
log('Django response started in ' + elapsed + 'ms: status=' + djangoRes.statusCode);
|
|
535
553
|
|
|
@@ -548,23 +566,131 @@ function forwardStreaming(req, res, body, callback) {
|
|
|
548
566
|
'Connection': 'keep-alive',
|
|
549
567
|
});
|
|
550
568
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
569
|
+
if (res.socket) {
|
|
570
|
+
res.socket.setNoDelay(true);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Buffer SSE data at event boundaries and inject keepalive between events.
|
|
574
|
+
let sseBuffer = '';
|
|
575
|
+
let lastActivity = Date.now();
|
|
576
|
+
let streamingDone = false;
|
|
577
|
+
let doneForwarded = false;
|
|
578
|
+
|
|
579
|
+
// Watchdog: inject keepalive only when buffer is empty (between complete events)
|
|
580
|
+
const keepaliveTimer = setInterval(() => {
|
|
581
|
+
if (streamingDone || res.writableEnded || res.destroyed) {
|
|
582
|
+
clearInterval(keepaliveTimer);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const now = Date.now();
|
|
586
|
+
if (now - lastActivity >= 2000 && sseBuffer.length === 0) {
|
|
587
|
+
try {
|
|
588
|
+
res.write(': keepalive\\n\\n');
|
|
589
|
+
lastActivity = now;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
log('Keepalive write error: ' + err.message);
|
|
592
|
+
clearInterval(keepaliveTimer);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}, 500);
|
|
596
|
+
|
|
597
|
+
const cleanup = () => {
|
|
598
|
+
streamingDone = true;
|
|
599
|
+
clearInterval(keepaliveTimer);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
var diagCounter = 0;
|
|
603
|
+
djangoRes.on('data', (chunk) => {
|
|
604
|
+
var chunkStr = chunk.toString();
|
|
605
|
+
var chunkIdx = ++diagCounter;
|
|
606
|
+
sseBuffer += chunkStr;
|
|
607
|
+
lastActivity = Date.now();
|
|
608
|
+
|
|
609
|
+
var okCount = (sseBuffer.match(/:ok/g) || []).length;
|
|
610
|
+
if (okCount > 0) {
|
|
611
|
+
log('CHUNK#' + chunkIdx + ': ' + okCount + ' :ok in buffer (len=' + sseBuffer.length + ')');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Strip upstream SSE ":ok" keepalive comments from anywhere in the buffer.
|
|
615
|
+
sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');
|
|
616
|
+
if (okCount > 0) {
|
|
617
|
+
log('AFTER: stripped ' + okCount + ' :ok, buffer len=' + sseBuffer.length);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
var extractedCount = 0;
|
|
621
|
+
let idx;
|
|
622
|
+
while ((idx = sseBuffer.indexOf('\\n\\n')) !== -1) {
|
|
623
|
+
const event = sseBuffer.substring(0, idx + 2);
|
|
624
|
+
sseBuffer = sseBuffer.substring(idx + 2);
|
|
625
|
+
const lines = event.split('\\n').filter(l => !/^:/.test(l) && l.length > 0);
|
|
626
|
+
if (lines.length === 0) continue;
|
|
627
|
+
const cleanEvent = lines.join('\\n') + '\\n\\n';
|
|
628
|
+
extractedCount++;
|
|
629
|
+
var dataContent = lines.map(function(l) { return l.replace(/^data: ?/, ''); }).join('');
|
|
630
|
+
if (dataContent === '[DONE]') { doneForwarded = true; }
|
|
631
|
+
var lastChar = dataContent.slice(-1);
|
|
632
|
+
if (dataContent !== '[DONE]' && lastChar !== '}' && lastChar !== ']') {
|
|
633
|
+
log('FLUSH: truncated event #' + extractedCount + ' (len=' + dataContent.length + ', end=' + JSON.stringify(dataContent.slice(-30)) + ')');
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
res.write(cleanEvent);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
cleanup();
|
|
639
|
+
log('Write error: ' + err.message);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (extractedCount > 0) {
|
|
644
|
+
log('EXTRACT: forwarded ' + extractedCount + ' events in chunk#' + chunkIdx + ', buffer remaining len=' + sseBuffer.length);
|
|
645
|
+
}
|
|
555
646
|
});
|
|
556
|
-
|
|
647
|
+
|
|
557
648
|
djangoRes.on('end', () => {
|
|
649
|
+
sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');
|
|
650
|
+
if (sseBuffer) {
|
|
651
|
+
// Ensure the final written data ends with \\n\\n so the client recognizes the event boundary
|
|
652
|
+
if (sseBuffer.length < 2 || sseBuffer.substring(sseBuffer.length - 2) !== '\\n\\n') {
|
|
653
|
+
sseBuffer += '\\n\\n';
|
|
654
|
+
}
|
|
655
|
+
log('END: writing remaining buffer len=' + sseBuffer.length + ' start=' + JSON.stringify(sseBuffer.substring(0, 80)));
|
|
656
|
+
try { res.write(sseBuffer); } catch (e) {}
|
|
657
|
+
}
|
|
658
|
+
if (!doneForwarded) {
|
|
659
|
+
log('Injecting [DONE] — upstream closed without sending it');
|
|
660
|
+
try { res.write('data: [DONE]\\n\\n'); } catch (e) {}
|
|
661
|
+
}
|
|
662
|
+
cleanup();
|
|
663
|
+
try { res.end(); } catch (e) {}
|
|
664
|
+
log('Streaming response completed' + (doneForwarded ? '' : ' (injected [DONE])'));
|
|
558
665
|
callback(null, djangoRes.statusCode, {}, '');
|
|
559
666
|
});
|
|
667
|
+
|
|
668
|
+
djangoRes.on('error', (err) => {
|
|
669
|
+
cleanup();
|
|
670
|
+
log('Django stream error: ' + err.message);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
res.on('close', () => {
|
|
674
|
+
cleanup();
|
|
675
|
+
log('Client connection closed');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
res.on('error', (err) => {
|
|
679
|
+
cleanup();
|
|
680
|
+
log('Client connection error: ' + err.message);
|
|
681
|
+
});
|
|
560
682
|
});
|
|
561
683
|
|
|
562
|
-
djangoReq.setTimeout(
|
|
684
|
+
djangoReq.setTimeout(300000, () => {
|
|
563
685
|
djangoReq.destroy();
|
|
564
|
-
callback(new Error('Django streaming request timed out after
|
|
686
|
+
callback(new Error('Django streaming request timed out after 300s'));
|
|
565
687
|
});
|
|
566
688
|
|
|
567
689
|
djangoReq.on('error', (err) => {
|
|
690
|
+
if (!timeoutCleared) {
|
|
691
|
+
djangoReq.clearTimeout();
|
|
692
|
+
timeoutCleared = true;
|
|
693
|
+
}
|
|
568
694
|
log('Django streaming request error: ' + err.message);
|
|
569
695
|
callback(err);
|
|
570
696
|
});
|
|
@@ -587,18 +713,19 @@ function forceNonStreaming(body) {
|
|
|
587
713
|
// Convert a chat.completion JSON object to SSE format
|
|
588
714
|
function jsonToSse(res, chatCompletion) {
|
|
589
715
|
const content = chatCompletion.choices?.[0]?.message?.content || '';
|
|
590
|
-
const model = chatCompletion.model || chatCompletion.model_id || 'deepseek
|
|
716
|
+
const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';
|
|
591
717
|
const created = chatCompletion.created || Math.floor(Date.now() / 1000);
|
|
592
718
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
719
|
+
if (!res.headersSent) {
|
|
720
|
+
try {
|
|
721
|
+
res.writeHead(200, {
|
|
722
|
+
'Content-Type': 'text/event-stream',
|
|
723
|
+
'Cache-Control': 'no-cache',
|
|
724
|
+
'Connection': 'keep-alive',
|
|
725
|
+
});
|
|
726
|
+
} catch (e) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
602
729
|
}
|
|
603
730
|
|
|
604
731
|
try {
|
|
@@ -758,6 +885,62 @@ function getLastUserMessageContent(body) {
|
|
|
758
885
|
}
|
|
759
886
|
}
|
|
760
887
|
|
|
888
|
+
async function handleTimeCreditsCommand(res, walletHash) {
|
|
889
|
+
log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');
|
|
890
|
+
const statusUrl = BACKEND_URL + '/v1/wallet/status';
|
|
891
|
+
const statusRes = await fetch(statusUrl, {
|
|
892
|
+
headers: { 'X-Wallet-Hash': walletHash }
|
|
893
|
+
});
|
|
894
|
+
let content;
|
|
895
|
+
if (statusRes.ok) {
|
|
896
|
+
const statusData = await statusRes.json();
|
|
897
|
+
const sessions = statusData.sessions || [];
|
|
898
|
+
const activeSessions = sessions.filter(s => s.time_remaining_seconds > 0 && s.model_active);
|
|
899
|
+
const inactiveSessions = sessions.filter(s => s.time_remaining_seconds > 0 && !s.model_active);
|
|
900
|
+
const parts = [];
|
|
901
|
+
if (activeSessions.length > 0) {
|
|
902
|
+
parts.push('**⏱️ Active Time Credits:**');
|
|
903
|
+
activeSessions.forEach(s => {
|
|
904
|
+
const total = formatDuration(s.time_credits_seconds);
|
|
905
|
+
const remaining = formatDuration(s.time_remaining_seconds);
|
|
906
|
+
const used = formatDuration(s.time_used_seconds);
|
|
907
|
+
parts.push(' - **' + (s.display_name || s.ai_model) + '** — ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (inactiveSessions.length > 0) {
|
|
911
|
+
parts.push('\\n**⚠️ Inactive Model:**');
|
|
912
|
+
inactiveSessions.forEach(s => {
|
|
913
|
+
const remaining = formatDuration(s.time_remaining_seconds);
|
|
914
|
+
parts.push(' - **' + (s.display_name || s.ai_model) + ' (Inactive)** — ' + remaining + ' remaining');
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
content = parts.length > 0 ? parts.join('\\n') : '⏱️ No active time credits.';
|
|
918
|
+
} else {
|
|
919
|
+
content = '⏱️ Unable to check time credits.';
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
sseLine(res, {
|
|
923
|
+
id: 'time-1',
|
|
924
|
+
object: 'chat.completion.chunk',
|
|
925
|
+
created: Math.floor(Date.now() / 1000),
|
|
926
|
+
model: 'deepseek/deepseek-v4-flash',
|
|
927
|
+
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
928
|
+
});
|
|
929
|
+
sseLine(res, {
|
|
930
|
+
id: 'time-2',
|
|
931
|
+
object: 'chat.completion.chunk',
|
|
932
|
+
choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],
|
|
933
|
+
});
|
|
934
|
+
sseLine(res, {
|
|
935
|
+
id: 'time-3',
|
|
936
|
+
object: 'chat.completion.chunk',
|
|
937
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
938
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
939
|
+
});
|
|
940
|
+
sseDone(res);
|
|
941
|
+
res.end();
|
|
942
|
+
}
|
|
943
|
+
|
|
761
944
|
// Main proxy server
|
|
762
945
|
const server = http.createServer(async (req, res) => {
|
|
763
946
|
// Enable CORS
|
|
@@ -793,14 +976,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
793
976
|
proxy_url: 'http://localhost:' + PROXY_PORT + '/v1',
|
|
794
977
|
django_url: BACKEND_URL + '/v1',
|
|
795
978
|
payment_address: '',
|
|
796
|
-
default_model: 'deepseek
|
|
979
|
+
default_model: 'deepseek/deepseek-v4-flash',
|
|
797
980
|
default_duration_minutes: 30,
|
|
798
981
|
models: [
|
|
799
982
|
{
|
|
800
|
-
id: 'deepseek
|
|
983
|
+
id: 'deepseek/deepseek-v4-flash',
|
|
801
984
|
object: 'model',
|
|
802
985
|
display_name: 'DeepSeek V4 Flash',
|
|
803
|
-
provider: '
|
|
986
|
+
provider: 'openrouter',
|
|
804
987
|
price_tiers: [
|
|
805
988
|
{ minutes: 10, price_php: 5.0, price_sats: 45000 },
|
|
806
989
|
{ minutes: 30, price_php: 12.0, price_sats: 108000 },
|
|
@@ -821,10 +1004,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
821
1004
|
const walletHash = req.headers['x-wallet-hash'];
|
|
822
1005
|
const lastContent = getLastUserMessageContent(body);
|
|
823
1006
|
|
|
824
|
-
// DEBUG: Log full body and parsed content
|
|
825
|
-
|
|
826
1007
|
log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));
|
|
827
1008
|
|
|
1009
|
+
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(); };
|
|
1010
|
+
|
|
828
1011
|
// Guard: wallet hash is required for payment flow
|
|
829
1012
|
if (!walletHash) {
|
|
830
1013
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -833,13 +1016,30 @@ const server = http.createServer(async (req, res) => {
|
|
|
833
1016
|
}
|
|
834
1017
|
|
|
835
1018
|
// Check if there's a pending payment for this wallet
|
|
836
|
-
|
|
1019
|
+
var pendingPayload = pendingPayments.get(walletHash);
|
|
837
1020
|
|
|
1021
|
+
// If there's a pending payment for a different model, clear it so the
|
|
1022
|
+
// new request can be forwarded fresh to Django. This prevents the
|
|
1023
|
+
// proxy from re-showing a stale payment prompt when the user switches
|
|
1024
|
+
// to a different model mid-conversation.
|
|
1025
|
+
if (pendingPayload) {
|
|
1026
|
+
var reqModel = '';
|
|
1027
|
+
try { reqModel = JSON.parse(body).model || ''; } catch (e) {}
|
|
1028
|
+
if (reqModel && pendingPayload.modelId && reqModel !== pendingPayload.modelId) {
|
|
1029
|
+
pendingPayments.delete(walletHash);
|
|
1030
|
+
pendingPayload = null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
838
1033
|
|
|
839
1034
|
if (pendingPayload) {
|
|
840
1035
|
// Check for tier selection first
|
|
841
1036
|
if (pendingPayload.step === 'tier_select' && pendingPayload.tiers && pendingPayload.tiers.length > 0) {
|
|
842
|
-
const userInput = lastContent
|
|
1037
|
+
const userInput = stripSysRem(lastContent);
|
|
1038
|
+
const timeCmd = userInput?.trim().toLowerCase();
|
|
1039
|
+
if (timeCmd === 'time' || timeCmd === 'credit' || timeCmd === 'credits') {
|
|
1040
|
+
await handleTimeCreditsCommand(res, walletHash);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
843
1043
|
let selectedIndex = -1;
|
|
844
1044
|
|
|
845
1045
|
// Try to parse user input as a number (1-based)
|
|
@@ -895,27 +1095,57 @@ const server = http.createServer(async (req, res) => {
|
|
|
895
1095
|
return;
|
|
896
1096
|
}
|
|
897
1097
|
|
|
1098
|
+
// Keepalive during payment processing
|
|
1099
|
+
if (!res.headersSent) {
|
|
1100
|
+
res.writeHead(200, {
|
|
1101
|
+
'Content-Type': 'text/event-stream',
|
|
1102
|
+
'Cache-Control': 'no-cache',
|
|
1103
|
+
'Connection': 'keep-alive',
|
|
1104
|
+
'X-Payment-Processing': 'true',
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
const keepalive = setInterval(() => {
|
|
1108
|
+
if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }
|
|
1109
|
+
res.write(': keepalive\\n\\n');
|
|
1110
|
+
}, 2000);
|
|
1111
|
+
|
|
898
1112
|
runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {
|
|
899
1113
|
pendingPayments.delete(walletHash);
|
|
900
|
-
|
|
1114
|
+
clearInterval(keepalive);
|
|
1115
|
+
|
|
901
1116
|
if (err) {
|
|
902
1117
|
log('paytaca pay failed: ' + err.message);
|
|
903
|
-
res.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1118
|
+
if (res.headersSent) {
|
|
1119
|
+
try {
|
|
1120
|
+
sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n❌ Payment failed: ' + err.message }, finish_reason: 'stop' }] });
|
|
1121
|
+
sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
1122
|
+
sseDone(res);
|
|
1123
|
+
res.end();
|
|
1124
|
+
} catch (e) {}
|
|
1125
|
+
} else {
|
|
1126
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1127
|
+
res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
|
|
1128
|
+
}
|
|
909
1129
|
return;
|
|
910
1130
|
}
|
|
911
1131
|
|
|
912
1132
|
if (!responseJson.success) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1133
|
+
const isTimeout = responseJson.timeout;
|
|
1134
|
+
const sseContent = isTimeout ? '\\n\\n⏱️ Response timed out. Your payment was processed \\u2014 check credits with \\'time\\' and try again' : '\\n\\n❌ Payment failed: ' + (responseJson.error || 'Unknown error');
|
|
1135
|
+
const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';
|
|
1136
|
+
const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;
|
|
1137
|
+
const errDetails = isTimeout ? 'Try again or check credits with \\'time\\'.' : 'Please check your balance and try again.';
|
|
1138
|
+
if (res.headersSent) {
|
|
1139
|
+
try {
|
|
1140
|
+
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' }] });
|
|
1141
|
+
sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
1142
|
+
sseDone(res);
|
|
1143
|
+
res.end();
|
|
1144
|
+
} catch (e) {}
|
|
1145
|
+
} else {
|
|
1146
|
+
res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
|
|
1147
|
+
res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
|
|
1148
|
+
}
|
|
919
1149
|
return;
|
|
920
1150
|
}
|
|
921
1151
|
|
|
@@ -949,7 +1179,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
949
1179
|
}
|
|
950
1180
|
|
|
951
1181
|
// Old flow: user responded to a yes/no payment prompt
|
|
952
|
-
if (lastContent === 'yes') {
|
|
1182
|
+
if (stripSysRem(lastContent) === 'yes') {
|
|
953
1183
|
log('Payment approved by wallet ' + walletHash?.substring(0, 16) + '...');
|
|
954
1184
|
pendingPayments.delete(walletHash);
|
|
955
1185
|
|
|
@@ -961,25 +1191,54 @@ const server = http.createServer(async (req, res) => {
|
|
|
961
1191
|
extraHeaders['X-Duration-Minutes'] = String(pendingPayload.durationMinutes);
|
|
962
1192
|
}
|
|
963
1193
|
|
|
1194
|
+
if (!res.headersSent) {
|
|
1195
|
+
res.writeHead(200, {
|
|
1196
|
+
'Content-Type': 'text/event-stream',
|
|
1197
|
+
'Cache-Control': 'no-cache',
|
|
1198
|
+
'Connection': 'keep-alive',
|
|
1199
|
+
'X-Payment-Processing': 'true',
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
const keepalive = setInterval(() => {
|
|
1203
|
+
if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }
|
|
1204
|
+
res.write(': keepalive\\n\\n');
|
|
1205
|
+
}, 2000);
|
|
1206
|
+
|
|
964
1207
|
runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {
|
|
1208
|
+
clearInterval(keepalive);
|
|
965
1209
|
if (err) {
|
|
966
1210
|
log('paytaca pay failed: ' + err.message);
|
|
967
|
-
res.
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1211
|
+
if (res.headersSent) {
|
|
1212
|
+
try {
|
|
1213
|
+
sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n❌ Payment failed: ' + err.message }, finish_reason: 'stop' }] });
|
|
1214
|
+
sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
1215
|
+
sseDone(res);
|
|
1216
|
+
res.end();
|
|
1217
|
+
} catch (e) {}
|
|
1218
|
+
} else {
|
|
1219
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1220
|
+
res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
|
|
1221
|
+
}
|
|
973
1222
|
return;
|
|
974
1223
|
}
|
|
975
1224
|
|
|
976
1225
|
if (!responseJson.success) {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1226
|
+
const isTimeout = responseJson.timeout;
|
|
1227
|
+
const sseContent = isTimeout ? '\\n\\n⏱️ Response timed out. Your payment was processed \\u2014 check credits with \\'time\\' and try again' : '\\n\\n❌ Payment failed: ' + (responseJson.error || 'Unknown error');
|
|
1228
|
+
const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';
|
|
1229
|
+
const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;
|
|
1230
|
+
const errDetails = isTimeout ? 'Try again or check credits with \\'time\\'.' : 'Please check your balance and try again.';
|
|
1231
|
+
if (res.headersSent) {
|
|
1232
|
+
try {
|
|
1233
|
+
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' }] });
|
|
1234
|
+
sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
1235
|
+
sseDone(res);
|
|
1236
|
+
res.end();
|
|
1237
|
+
} catch (e) {}
|
|
1238
|
+
} else {
|
|
1239
|
+
res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
|
|
1240
|
+
res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
|
|
1241
|
+
}
|
|
983
1242
|
return;
|
|
984
1243
|
}
|
|
985
1244
|
|
|
@@ -1005,7 +1264,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1005
1264
|
});
|
|
1006
1265
|
return;
|
|
1007
1266
|
|
|
1008
|
-
} else if (lastContent === 'no') {
|
|
1267
|
+
} else if (stripSysRem(lastContent) === 'no') {
|
|
1009
1268
|
log('Payment declined by wallet ' + walletHash?.substring(0, 16) + '...');
|
|
1010
1269
|
pendingPayments.delete(walletHash);
|
|
1011
1270
|
|
|
@@ -1018,7 +1277,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1018
1277
|
id: 'payment-declined',
|
|
1019
1278
|
object: 'chat.completion',
|
|
1020
1279
|
created: Math.floor(Date.now() / 1000),
|
|
1021
|
-
model: pendingPayload.modelId || 'deepseek
|
|
1280
|
+
model: pendingPayload.modelId || 'deepseek/deepseek-v4-flash',
|
|
1022
1281
|
choices: [{
|
|
1023
1282
|
index: 0,
|
|
1024
1283
|
message: {
|
|
@@ -1033,57 +1292,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
1033
1292
|
return;
|
|
1034
1293
|
|
|
1035
1294
|
} else {
|
|
1295
|
+
const innerCmd = stripSysRem(lastContent?.trim().toLowerCase());
|
|
1296
|
+
if (innerCmd === 'time' || innerCmd === 'credit' || innerCmd === 'credits') {
|
|
1297
|
+
await handleTimeCreditsCommand(res, walletHash);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1036
1300
|
log('New message while payment pending for wallet ' + walletHash?.substring(0, 16) + '...');
|
|
1037
1301
|
}
|
|
1038
1302
|
}
|
|
1039
1303
|
|
|
1040
1304
|
// Handle time/credits command — show remaining time credits
|
|
1041
|
-
const cmd = lastContent?.trim().toLowerCase();
|
|
1042
|
-
if (cmd === 'time' || cmd === 'credits') {
|
|
1043
|
-
|
|
1044
|
-
const statusUrl = BACKEND_URL + '/v1/wallet/status';
|
|
1045
|
-
const statusRes = await fetch(statusUrl, {
|
|
1046
|
-
headers: { 'X-Wallet-Hash': walletHash }
|
|
1047
|
-
});
|
|
1048
|
-
let content;
|
|
1049
|
-
if (statusRes.ok) {
|
|
1050
|
-
const statusData = await statusRes.json();
|
|
1051
|
-
const sessions = statusData.sessions || [];
|
|
1052
|
-
if (sessions.length === 0) {
|
|
1053
|
-
content = '⏱️ No active time credits.';
|
|
1054
|
-
} else {
|
|
1055
|
-
const lines = sessions.map(s => {
|
|
1056
|
-
const total = formatDuration(s.time_credits_seconds);
|
|
1057
|
-
const remaining = formatDuration(s.time_remaining_seconds);
|
|
1058
|
-
const used = formatDuration(s.time_used_seconds);
|
|
1059
|
-
return ' - **' + (s.display_name || s.ai_model) + '** — ' + remaining + ' remaining of ' + total + ' purchased (' + used + ' used)';
|
|
1060
|
-
});
|
|
1061
|
-
content = '⏱️ **Active Time Credits:**\\n' + lines.join('\\n');
|
|
1062
|
-
}
|
|
1063
|
-
} else {
|
|
1064
|
-
content = '⏱️ Unable to check time credits.';
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
sseLine(res, {
|
|
1068
|
-
id: 'time-1',
|
|
1069
|
-
object: 'chat.completion.chunk',
|
|
1070
|
-
created: Math.floor(Date.now() / 1000),
|
|
1071
|
-
model: 'deepseek-ai/DeepSeek-V4-Flash',
|
|
1072
|
-
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
1073
|
-
});
|
|
1074
|
-
sseLine(res, {
|
|
1075
|
-
id: 'time-2',
|
|
1076
|
-
object: 'chat.completion.chunk',
|
|
1077
|
-
choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],
|
|
1078
|
-
});
|
|
1079
|
-
sseLine(res, {
|
|
1080
|
-
id: 'time-3',
|
|
1081
|
-
object: 'chat.completion.chunk',
|
|
1082
|
-
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
1083
|
-
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
1084
|
-
});
|
|
1085
|
-
sseDone(res);
|
|
1086
|
-
res.end();
|
|
1305
|
+
const cmd = stripSysRem(lastContent?.trim().toLowerCase());
|
|
1306
|
+
if (cmd === 'time' || cmd === 'credit' || cmd === 'credits') {
|
|
1307
|
+
await handleTimeCreditsCommand(res, walletHash);
|
|
1087
1308
|
return;
|
|
1088
1309
|
}
|
|
1089
1310
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA87CnC,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\nasync function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n });\n\n const responseHeaders = {};\n response.headers.forEach((value, key) => { responseHeaders[key] = value; });\n const responseText = await response.text();\n let responseData;\n try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }\n\n if (response.status === 402) {\n const paymentRequired = parsePaymentRequiredJson(responseData);\n if (!paymentRequired) {\n return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };\n }\n const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');\n if (!requirements) {\n return {\n success: false, status: 402, error: 'Server does not accept BCH payment',\n data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },\n };\n }\n\n const payerAddress = x402Payer.getPayerAddress();\n const address = requirements.payTo;\n const amountBch = Number(requirements.amount) / 1e8;\n const changeAddressSet = bchWallet.getAddressSetAt(0);\n const changeAddress = changeAddressSet.change;\n\n if (!confirmed) {\n return {\n success: false, status: 402, error: 'Payment not confirmed.',\n payment: { required: true, amount: requirements.amount, payTo: address },\n };\n }\n\n const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);\n if (!sendResult.success) {\n return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };\n }\n\n const txid = sendResult.txid;\n const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);\n headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);\n\n
|
|
1
|
+
export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\nasync function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(120000),\n });\n\n const responseHeaders = {};\n response.headers.forEach((value, key) => { responseHeaders[key] = value; });\n const responseText = await response.text();\n let responseData;\n try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }\n\n if (response.status === 402) {\n const paymentRequired = parsePaymentRequiredJson(responseData);\n if (!paymentRequired) {\n return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };\n }\n const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');\n if (!requirements) {\n return {\n success: false, status: 402, error: 'Server does not accept BCH payment',\n data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },\n };\n }\n\n const payerAddress = x402Payer.getPayerAddress();\n const address = requirements.payTo;\n const amountBch = Number(requirements.amount) / 1e8;\n const changeAddressSet = bchWallet.getAddressSetAt(0);\n const changeAddress = changeAddressSet.change;\n\n if (!confirmed) {\n return {\n success: false, status: 402, error: 'Payment not confirmed.',\n payment: { required: true, amount: requirements.amount, payTo: address },\n };\n }\n\n const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);\n if (!sendResult.success) {\n return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };\n }\n\n const txid = sendResult.txid;\n const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);\n headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);\n\n let retryResponse;\n try {\n retryResponse = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(120000),\n });\n } catch (e) {\n if (e.name === 'AbortError') {\n return { success: false, timeout: true, error: 'Response timed out from server.' };\n }\n throw e;\n }\n const retryResponseHeaders = {};\n retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });\n const retryResponseText = await retryResponse.text();\n let retryResponseData;\n try { retryResponseData = JSON.parse(retryResponseText); } catch { retryResponseData = retryResponseText; }\n\n return {\n success: retryResponse.ok,\n status: retryResponse.status,\n statusText: retryResponse.statusText,\n headers: retryResponseHeaders,\n data: retryResponseData,\n payment: { required: true, txid, recipientAddress: address },\n };\n }\n\n return {\n success: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data: responseData,\n payment: { required: false },\n };\n}\n\nmain();\n";
|
|
2
2
|
//# sourceMappingURL=wrapper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,i/NAgMlC,CAAC"}
|
package/dist/bundled/wrapper.js
CHANGED
|
@@ -111,6 +111,7 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
|
|
|
111
111
|
method,
|
|
112
112
|
headers,
|
|
113
113
|
body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
|
|
114
|
+
signal: AbortSignal.timeout(120000),
|
|
114
115
|
});
|
|
115
116
|
|
|
116
117
|
const responseHeaders = {};
|
|
@@ -154,11 +155,20 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
|
|
|
154
155
|
const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);
|
|
155
156
|
headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
let retryResponse;
|
|
159
|
+
try {
|
|
160
|
+
retryResponse = await fetch(url, {
|
|
161
|
+
method,
|
|
162
|
+
headers,
|
|
163
|
+
body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
|
|
164
|
+
signal: AbortSignal.timeout(120000),
|
|
165
|
+
});
|
|
166
|
+
} catch (e) {
|
|
167
|
+
if (e.name === 'AbortError') {
|
|
168
|
+
return { success: false, timeout: true, error: 'Response timed out from server.' };
|
|
169
|
+
}
|
|
170
|
+
throw e;
|
|
171
|
+
}
|
|
162
172
|
const retryResponseHeaders = {};
|
|
163
173
|
retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });
|
|
164
174
|
const retryResponseText = await retryResponse.text();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG
|
|
1
|
+
{"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgMrC,CAAC"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;;;;
|
|
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
|
|
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/dist/proxy.js
CHANGED
|
@@ -152,6 +152,9 @@ async function startProxy(configDir, config) {
|
|
|
152
152
|
const wrapperScript = (0, config_1.getWrapperScript)(configDir);
|
|
153
153
|
// Ensure config directory exists
|
|
154
154
|
(0, config_1.ensureConfigDir)(configDir);
|
|
155
|
+
// Always rewrite proxy script so code changes take effect on next restart
|
|
156
|
+
fs.writeFileSync(proxyScript, proxy_1.PROXY_SCRIPT_CONTENT, 'utf8');
|
|
157
|
+
fs.chmodSync(proxyScript, '755');
|
|
155
158
|
// Check if proxy already running
|
|
156
159
|
const existingStatus = await getProxyStatus(configDir);
|
|
157
160
|
if (existingStatus.running && existingStatus.pid && existingStatus.port) {
|
|
@@ -175,9 +178,6 @@ async function startProxy(configDir, config) {
|
|
|
175
178
|
}
|
|
176
179
|
// Find available port
|
|
177
180
|
const port = await findAvailablePort(8001, 8010);
|
|
178
|
-
// Write bundled proxy script to config directory
|
|
179
|
-
fs.writeFileSync(proxyScript, proxy_1.PROXY_SCRIPT_CONTENT, 'utf8');
|
|
180
|
-
fs.chmodSync(proxyScript, '755');
|
|
181
181
|
// Write bundled wrapper script to config directory
|
|
182
182
|
fs.writeFileSync(wrapperScript, wrapper_1.WRAPPER_SCRIPT_CONTENT, 'utf8');
|
|
183
183
|
fs.chmodSync(wrapperScript, '755');
|
package/dist/proxy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paytaca/opencode-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"dist/",
|
|
15
15
|
"bin/",
|
|
16
|
+
"scripts/",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"build": "tsc",
|
|
20
|
-
"prepare": "npm run build"
|
|
21
|
+
"prepare": "npm run build && node scripts/sync-cache.js"
|
|
21
22
|
},
|
|
22
23
|
"keywords": [
|
|
23
24
|
"opencode",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const cacheDist = path.join(os.homedir(), '.cache/opencode/packages/@paytaca/opencode-plugin@latest/node_modules/@paytaca/opencode-plugin/dist');
|
|
6
|
+
const myDist = path.join(__dirname, '..', 'dist');
|
|
7
|
+
|
|
8
|
+
if (fs.existsSync(cacheDist) && fs.existsSync(myDist)) {
|
|
9
|
+
fs.cpSync(myDist, cacheDist, { recursive: true, force: true });
|
|
10
|
+
}
|