@paytaca/opencode-plugin 0.1.16 → 0.2.1

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.
@@ -50,6 +50,17 @@ function log(message) {
50
50
  // step: 'tier_select' (user must pick a tier) or 'approval' (yes/no)
51
51
  const pendingPayments = new Map();
52
52
 
53
+ // Track the last model used per wallet so we can detect model switches and
54
+ // make sure a switched-to model never hits a stale payment prompt.
55
+ const lastModelPerWallet = new Map();
56
+
57
+ // Monotonic id per incoming request. A response may only clear the pending
58
+ // payment created by its own request — concurrent requests from opencode share
59
+ // the wallet hash, and a plain 200 finishing mid-payment must not clobber the
60
+ // pending entry another request just created (that made tier selections
61
+ // "2"/"3" fall through to a fresh 402 and re-show the prompt forever).
62
+ let requestCounter = 0;
63
+
53
64
  // Utility: run shell command and return output
54
65
  function runCommand(cmd, args = []) {
55
66
  return new Promise((resolve, reject) => {
@@ -141,10 +152,17 @@ function sseDone(res) {
141
152
  res.write('data: [DONE]\\n\\n');
142
153
  }
143
154
 
155
+ // Zero-width marker prepended to every synthetic proxy message (tier
156
+ // prompts, credits/plans output, payment notices). The opencode plugin
157
+ // strips marker-carrying assistant messages from LLM context — proxy chatter
158
+ // is not relevant to the coding session — while the user still sees them in
159
+ // the UI (zero-width characters don't render).
160
+ const PROXY_MARKER = String.fromCharCode(0x200b, 0x200b, 0x200b, 0x200b);
161
+
144
162
  // Stream the tier-selection prompt body (SSE lines) into an in-progress response.
145
163
  // When includeRole is false the leading role delta is skipped, so the body can be
146
164
  // appended to a stream that already emitted content (e.g. after a payment failure).
147
- async function streamTierSelectionBody(res, walletHash, modelName, tiers, includeRole) {
165
+ async function streamTierSelectionBody(res, walletHash, modelName, tiers, includeRole, otherModels) {
148
166
  if (includeRole !== false) {
149
167
  sseLine(res, {
150
168
  id: 'tier-1',
@@ -159,7 +177,7 @@ async function streamTierSelectionBody(res, walletHash, modelName, tiers, includ
159
177
  sseLine(res, {
160
178
  id: 'tier-2',
161
179
  object: 'chat.completion.chunk',
162
- choices: [{ index: 0, delta: { content: '⏳ Initializing Paytaca AI provider...\\n' }, finish_reason: null }],
180
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + '⏳ Initializing Paytaca AI provider...\\n' }, finish_reason: null }],
163
181
  });
164
182
 
165
183
  const hasCli = await checkPaytacaCli();
@@ -218,6 +236,16 @@ async function streamTierSelectionBody(res, walletHash, modelName, tiers, includ
218
236
  choices: [{ index: 0, delta: { content: '💳 Select a plan for **' + (modelName || 'AI Model') + '**\\n\\n' }, finish_reason: null }],
219
237
  });
220
238
 
239
+ // If other models still have paid credits, tell the user they can switch
240
+ // instead of buying a new plan (only when there is something to suggest).
241
+ if (otherModels && otherModels.length > 0) {
242
+ sseLine(res, {
243
+ id: 'tier-9b',
244
+ object: 'chat.completion.chunk',
245
+ choices: [{ index: 0, delta: { content: otherModelsHint(otherModels) }, finish_reason: null }],
246
+ });
247
+ }
248
+
221
249
  // Build all tier lines into one string so backtick markdown renders
222
250
  // consistently (same as the 'plans' command).
223
251
  let tiersContent = '';
@@ -252,7 +280,7 @@ async function streamTierSelectionBody(res, walletHash, modelName, tiers, includ
252
280
  }
253
281
 
254
282
  // Build and stream a full tier-selection prompt (headers + body + [DONE]) to the client.
255
- async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
283
+ async function streamTierSelectionPrompt(res, walletHash, modelName, tiers, otherModels) {
256
284
  if (!res.headersSent) {
257
285
  res.writeHead(200, {
258
286
  'Content-Type': 'text/event-stream',
@@ -261,7 +289,7 @@ async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
261
289
  'Connection': 'keep-alive',
262
290
  });
263
291
  }
264
- await streamTierSelectionBody(res, walletHash, modelName, tiers, true);
292
+ await streamTierSelectionBody(res, walletHash, modelName, tiers, true, otherModels);
265
293
  sseDone(res);
266
294
  res.end();
267
295
  }
@@ -269,7 +297,7 @@ async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
269
297
  // Build and stream SSE loading sequence + payment prompt
270
298
  // Stream SSE notice when the upstream (OpenRouter) account lacks balance to fund
271
299
  // the request. Replaces the old single-tier yes/no approval prompt.
272
- async function streamLowBalanceNotice(res, modelName) {
300
+ async function streamLowBalanceNotice(res, modelName, otherModels) {
273
301
  res.writeHead(200, {
274
302
  'Content-Type': 'text/event-stream',
275
303
  'Cache-Control': 'no-cache',
@@ -285,10 +313,13 @@ async function streamLowBalanceNotice(res, modelName) {
285
313
  choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
286
314
  });
287
315
 
316
+ // Include the other-models hint (when available) so the user knows they can
317
+ // switch to a model that still has credits instead of being stuck.
318
+ const hint = otherModelsHint(otherModels);
288
319
  sseLine(res, {
289
320
  id: 'lb-2',
290
321
  object: 'chat.completion.chunk',
291
- choices: [{ index: 0, delta: { content: '⚠️ OpenRouter balance is low — please top up before continuing.\\n' }, finish_reason: 'stop' }],
322
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + '⚠️ OpenRouter balance is low — please top up before continuing.\\n' + hint }, finish_reason: 'stop' }],
292
323
  });
293
324
 
294
325
  sseLine(res, {
@@ -680,7 +711,7 @@ async function streamPaymentFailureAndRetry(res, walletHash, pendingPayload, mes
680
711
  object: 'chat.completion.chunk',
681
712
  created: Math.floor(Date.now() / 1000),
682
713
  model: 'deepseek/deepseek-v4-flash',
683
- choices: [{ index: 0, delta: { content: message }, finish_reason: 'stop' }],
714
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + message }, finish_reason: 'stop' }],
684
715
  });
685
716
  } catch (e) {
686
717
  }
@@ -815,6 +846,58 @@ function getLastUserMessageContent(body) {
815
846
  }
816
847
  }
817
848
 
849
+ // Fetch wallet status and return other models that still have remaining time
850
+ // credits, excluding the model currently being requested. Returns an array of
851
+ // { modelId, displayName, remainingSeconds } or [] when nothing qualifies
852
+ // (or the status endpoint is unreachable). This powers the "you can switch to
853
+ // another model" hint on 402 responses.
854
+ async function getOtherModelsWithCredits(walletHash, excludeModelId) {
855
+ try {
856
+ const statusRes = await fetch(BACKEND_URL + '/v1/wallet/status', {
857
+ headers: { 'X-Wallet-Hash': walletHash }
858
+ });
859
+ if (!statusRes.ok) {
860
+ return [];
861
+ }
862
+ const statusData = await statusRes.json();
863
+ const sessions = Array.isArray(statusData.sessions) ? statusData.sessions : [];
864
+ const others = [];
865
+ for (const s of sessions) {
866
+ const modelId = s.ai_model || s.model_id || '';
867
+ if (excludeModelId && modelId && modelId === excludeModelId) {
868
+ continue;
869
+ }
870
+ const remaining = Number(s.time_remaining_seconds) || 0;
871
+ if (remaining > 0) {
872
+ others.push({
873
+ modelId: modelId,
874
+ displayName: s.display_name || modelId || 'Unknown model',
875
+ remainingSeconds: remaining,
876
+ });
877
+ }
878
+ }
879
+ return others;
880
+ } catch (err) {
881
+ log('Failed to check other models with credits: ' + err.message);
882
+ return [];
883
+ }
884
+ }
885
+
886
+ // Build a hint listing other models that still have remaining credits, so the
887
+ // user knows they can switch instead of buying a new plan. Returns '' when
888
+ // there is nothing worth suggesting.
889
+ function otherModelsHint(otherModels) {
890
+ if (!otherModels || otherModels.length === 0) {
891
+ return '';
892
+ }
893
+ let hint = '\\n💡 You have remaining credits on other models:\\n';
894
+ for (const m of otherModels) {
895
+ hint += ' - **' + m.displayName + '** — ' + formatDuration(m.remainingSeconds) + ' remaining\\n';
896
+ }
897
+ hint += 'Switch to one of these models to keep chatting without a new purchase.\\n\\n';
898
+ return hint;
899
+ }
900
+
818
901
  async function handleTimeCreditsCommand(res, walletHash) {
819
902
  log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');
820
903
  const statusUrl = BACKEND_URL + '/v1/wallet/status';
@@ -859,7 +942,7 @@ async function handleTimeCreditsCommand(res, walletHash) {
859
942
  sseLine(res, {
860
943
  id: 'time-2',
861
944
  object: 'chat.completion.chunk',
862
- choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],
945
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + content + '\\n' }, finish_reason: 'stop' }],
863
946
  });
864
947
  sseLine(res, {
865
948
  id: 'time-3',
@@ -941,7 +1024,7 @@ async function handlePricingCommand(res) {
941
1024
  sseLine(res, {
942
1025
  id: 'price-2',
943
1026
  object: 'chat.completion.chunk',
944
- choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],
1027
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + content + '\\n' }, finish_reason: 'stop' }],
945
1028
  });
946
1029
  sseLine(res, {
947
1030
  id: 'price-3',
@@ -1014,6 +1097,7 @@ const server = http.createServer(async (req, res) => {
1014
1097
  req.on('end', async () => {
1015
1098
  try {
1016
1099
  const walletHash = req.headers['x-wallet-hash'];
1100
+ const proxyReqId = ++requestCounter;
1017
1101
  const lastContent = getLastUserMessageContent(body);
1018
1102
 
1019
1103
  log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));
@@ -1040,19 +1124,35 @@ const server = http.createServer(async (req, res) => {
1040
1124
 
1041
1125
  // Check if there's a pending payment for this wallet
1042
1126
  var pendingPayload = pendingPayments.get(walletHash);
1127
+
1128
+ // Parse the model requested by this call — used for switch detection
1129
+ // and for clearing stale pending payments tied to a previous model.
1130
+ var reqModel = '';
1131
+ try { reqModel = JSON.parse(body).model || ''; } catch (e) {}
1043
1132
 
1044
1133
  // If there's a pending payment for a different model, clear it so the
1045
1134
  // new request can be forwarded fresh to Django. This prevents the
1046
1135
  // proxy from re-showing a stale payment prompt when the user switches
1047
1136
  // to a different model mid-conversation.
1048
1137
  if (pendingPayload) {
1049
- var reqModel = '';
1050
- try { reqModel = JSON.parse(body).model || ''; } catch (e) {}
1051
1138
  if (reqModel && pendingPayload.modelId && reqModel !== pendingPayload.modelId) {
1052
1139
  pendingPayments.delete(walletHash);
1053
1140
  pendingPayload = null;
1054
1141
  }
1055
1142
  }
1143
+
1144
+ // Model-switch detection: remember which model this wallet last used.
1145
+ // When a switch is detected, log it — opencode carries the full
1146
+ // conversation history on the next message, so the last prompt is
1147
+ // effectively re-sent to the new model. If that model has no credits,
1148
+ // the standard 402 flow shows the buy-plan prompt for it.
1149
+ const prevModel = lastModelPerWallet.get(walletHash) || '';
1150
+ if (reqModel && prevModel && reqModel !== prevModel) {
1151
+ log('Model switch detected for wallet ' + (walletHash?.substring(0, 16) || 'none') + ': ' + prevModel + ' -> ' + reqModel);
1152
+ }
1153
+ if (reqModel) {
1154
+ lastModelPerWallet.set(walletHash, reqModel);
1155
+ }
1056
1156
 
1057
1157
  if (pendingPayload) {
1058
1158
  // Check for tier selection first
@@ -1110,7 +1210,7 @@ const server = http.createServer(async (req, res) => {
1110
1210
  sseLine(res, {
1111
1211
  id: 'balance-err',
1112
1212
  object: 'chat.completion.chunk',
1113
- choices: [{ index: 0, delta: { content: '\\n\\n❌ **Insufficient balance** — 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' }],
1213
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + '\\n\\n❌ **Insufficient balance** — 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' }],
1114
1214
  });
1115
1215
  sseLine(res, {
1116
1216
  id: 'balance-err-done',
@@ -1164,7 +1264,7 @@ const server = http.createServer(async (req, res) => {
1164
1264
  if (res.headersSent && !res.destroyed && !res.writableEnded) {
1165
1265
  if (isTimeout) {
1166
1266
  try {
1167
- sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });
1267
+ 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: PROXY_MARKER + sseContent }, finish_reason: 'stop' }] });
1168
1268
  sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1169
1269
  sseDone(res);
1170
1270
  res.end();
@@ -1251,7 +1351,7 @@ const server = http.createServer(async (req, res) => {
1251
1351
  log('paytaca pay failed: ' + err.message);
1252
1352
  if (res.headersSent && !res.destroyed && !res.writableEnded) {
1253
1353
  try {
1254
- sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n❌ Payment failed: ' + err.message }, finish_reason: 'stop' }] });
1354
+ 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: PROXY_MARKER + '\\n\\n❌ Payment failed: ' + err.message }, finish_reason: 'stop' }] });
1255
1355
  sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1256
1356
  sseDone(res);
1257
1357
  res.end();
@@ -1275,7 +1375,7 @@ const server = http.createServer(async (req, res) => {
1275
1375
  const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';
1276
1376
  if (res.headersSent && !res.destroyed && !res.writableEnded) {
1277
1377
  try {
1278
- sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });
1378
+ 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: PROXY_MARKER + sseContent }, finish_reason: 'stop' }] });
1279
1379
  sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
1280
1380
  sseDone(res);
1281
1381
  res.end();
@@ -1338,7 +1438,7 @@ const server = http.createServer(async (req, res) => {
1338
1438
  index: 0,
1339
1439
  message: {
1340
1440
  role: 'assistant',
1341
- content: 'Payment declined. Chat cannot continue without funding.\\n\\n' + fundMsg,
1441
+ content: PROXY_MARKER + 'Payment declined. Chat cannot continue without funding.\\n\\n' + fundMsg,
1342
1442
  },
1343
1443
  finish_reason: 'stop',
1344
1444
  }],
@@ -1412,6 +1512,7 @@ const server = http.createServer(async (req, res) => {
1412
1512
  }
1413
1513
 
1414
1514
  pendingPayments.set(walletHash, {
1515
+ reqId: proxyReqId,
1415
1516
  body: body,
1416
1517
  modelId: modelId,
1417
1518
  displayName: displayName,
@@ -1421,8 +1522,11 @@ const server = http.createServer(async (req, res) => {
1421
1522
  });
1422
1523
 
1423
1524
  if (tiers && tiers.length > 0) {
1424
- // New flow: show tier selection prompt
1425
- await streamTierSelectionPrompt(res, walletHash, displayName || modelId || 'AI Model', tiers);
1525
+ // New flow: show tier selection prompt. Also tell the user about
1526
+ // other models that still have paid credits, so they can switch
1527
+ // instead of buying a plan for the currently selected model.
1528
+ const otherModels = await getOtherModelsWithCredits(walletHash, modelId || requestModel);
1529
+ await streamTierSelectionPrompt(res, walletHash, displayName || modelId || 'AI Model', tiers, otherModels);
1426
1530
  return;
1427
1531
  }
1428
1532
 
@@ -1486,16 +1590,23 @@ const server = http.createServer(async (req, res) => {
1486
1590
  + ' tokensUsed=' + tokensUsed
1487
1591
  + ' tokenLimit=' + tokenLimit);
1488
1592
 
1489
- await streamLowBalanceNotice(res, displayName || statusModelId || modelId || 'AI Model');
1593
+ const lowBalanceOtherModels = await getOtherModelsWithCredits(walletHash, statusModelId || modelId);
1594
+ await streamLowBalanceNotice(res, displayName || statusModelId || modelId || 'AI Model', lowBalanceOtherModels);
1490
1595
  } else {
1491
1596
  if (res.headersSent) {
1492
1597
  log('Streaming response completed and already sent');
1493
- pendingPayments.delete(walletHash);
1598
+ const settled = pendingPayments.get(walletHash);
1599
+ if (settled && settled.reqId === proxyReqId) {
1600
+ pendingPayments.delete(walletHash);
1601
+ }
1494
1602
  return;
1495
1603
  }
1496
1604
 
1497
1605
  log('Forwarding normal response to OpenCode: status=' + statusCode + ', bodyLen=' + responseBody.length);
1498
- pendingPayments.delete(walletHash);
1606
+ const settled = pendingPayments.get(walletHash);
1607
+ if (settled && settled.reqId === proxyReqId) {
1608
+ pendingPayments.delete(walletHash);
1609
+ }
1499
1610
  res.writeHead(statusCode, {
1500
1611
  'Content-Type': headers['content-type'] || 'application/json',
1501
1612
  });
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqgDnC,CAAC"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAonDnC,CAAC"}
package/dist/config.d.ts CHANGED
@@ -10,4 +10,5 @@ export declare function getPidFile(configDir: string): string;
10
10
  export declare function getProxyScript(configDir: string): string;
11
11
  export declare function getLogFile(configDir: string): string;
12
12
  export declare function getWrapperScript(configDir: string): string;
13
+ export declare function getMcpScript(configDir: string): string;
13
14
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAGvC,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAkCpD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAIlE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAGvC,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAkCpD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAIlE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtD"}
package/dist/config.js CHANGED
@@ -43,15 +43,17 @@ exports.getPidFile = getPidFile;
43
43
  exports.getProxyScript = getProxyScript;
44
44
  exports.getLogFile = getLogFile;
45
45
  exports.getWrapperScript = getWrapperScript;
46
+ exports.getMcpScript = getMcpScript;
46
47
  const fs = __importStar(require("fs"));
48
+ const os = __importStar(require("os"));
47
49
  const path = __importStar(require("path"));
48
50
  // Default production backend
49
51
  exports.DEFAULT_BACKEND_URL = 'https://api.paytaca.ai';
50
52
  exports.DEFAULT_PROXY_PORT = 8001;
51
53
  // Config directory: ~/.opencode-paytaca/
52
54
  function getConfigDir() {
53
- const home = process.env.HOME || process.env.USERPROFILE || '.';
54
- return path.join(home, '.opencode-paytaca');
55
+ // os.homedir() resolves HOME/USERPROFILE correctly on all platforms.
56
+ return path.join(os.homedir(), '.opencode-paytaca');
55
57
  }
56
58
  function ensureConfigDir(configDir) {
57
59
  if (!fs.existsSync(configDir)) {
@@ -111,4 +113,7 @@ function getLogFile(configDir) {
111
113
  function getWrapperScript(configDir) {
112
114
  return path.join(configDir, 'paytaca-pay-wrapper.mjs');
113
115
  }
116
+ function getMcpScript(configDir) {
117
+ return path.join(configDir, 'mcp-server.js');
118
+ }
114
119
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare const PROXY_MARKER: string;
2
+ export declare const PAYMENT_SUCCESS_LINE = "\uD83D\uDCB3 Payment successful \u2014 generating your response...";
3
+ export declare function filterProxyChatter(messages: any[]): any[];
4
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,YAAY,QAAsD,CAAC;AAKhF,eAAO,MAAM,oBAAoB,uEAAwD,CAAC;AAgE1F,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,CAqBzD"}
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PAYMENT_SUCCESS_LINE = exports.PROXY_MARKER = void 0;
4
+ exports.filterProxyChatter = filterProxyChatter;
5
+ // Zero-width marker the proxy prepends to every synthetic message it streams
6
+ // (tier-selection prompts, credits/plans output, payment notices). The
7
+ // messages stay visible in the opencode UI — zero-width characters don't
8
+ // render — but carrying the marker lets this plugin strip them from the
9
+ // context passed to the LLM, since proxy/payment chatter is not relevant to
10
+ // the coding session.
11
+ exports.PROXY_MARKER = String.fromCharCode(0x200b, 0x200b, 0x200b, 0x200b);
12
+ // The success line the proxy prepends to a real model response after a
13
+ // payment completes. Useful during streaming UX, but it is proxy chatter —
14
+ // stripped from assistant text before the LLM sees it.
15
+ exports.PAYMENT_SUCCESS_LINE = '💳 Payment successful — generating your response...';
16
+ // Replies that are part of a proxy interactive flow (tier pick, approval,
17
+ // credits/plans shortcuts). Only removed when they immediately follow a
18
+ // marked proxy message, so genuine user messages are never dropped.
19
+ const SELECTION_RE = /^\s*(?:\d{1,3}|yes|no|credits|plans|balance)\s*$/i;
20
+ function stripSystemReminders(text) {
21
+ let r = text || '';
22
+ const open = '<system-reminder>';
23
+ const close = '</system-reminder>';
24
+ let i = r.indexOf(open);
25
+ while (i !== -1) {
26
+ const j = r.indexOf(close, i);
27
+ if (j === -1)
28
+ break;
29
+ r = r.substring(0, i) + r.substring(j + close.length);
30
+ i = r.indexOf(open);
31
+ }
32
+ return r;
33
+ }
34
+ function textOf(msg) {
35
+ const parts = msg && msg.parts;
36
+ if (!Array.isArray(parts))
37
+ return '';
38
+ let out = '';
39
+ for (const part of parts) {
40
+ if (part && part.type === 'text' && typeof part.text === 'string') {
41
+ out += part.text;
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ function isMarkedProxyMessage(msg) {
47
+ return !!(msg && msg.info && msg.info.role === 'assistant' && textOf(msg).indexOf(exports.PROXY_MARKER) !== -1);
48
+ }
49
+ function isSelectionReply(msg) {
50
+ if (!msg || !msg.info || msg.info.role !== 'user')
51
+ return false;
52
+ return SELECTION_RE.test(stripSystemReminders(textOf(msg)).trim());
53
+ }
54
+ // Remove the payment-success preamble from an assistant message without
55
+ // mutating opencode's stored objects — returns a shallow-cloned wrapper when
56
+ // a change is made, or null when the message is clean.
57
+ function withoutPaymentSuccessLine(msg) {
58
+ const parts = msg && msg.parts;
59
+ if (!Array.isArray(parts))
60
+ return null;
61
+ for (let i = 0; i < parts.length; i++) {
62
+ const part = parts[i];
63
+ if (part && part.type === 'text' && typeof part.text === 'string' && part.text.indexOf(exports.PAYMENT_SUCCESS_LINE) !== -1) {
64
+ const cleaned = part.text.split(exports.PAYMENT_SUCCESS_LINE).join('').replace(/\n{3,}/g, '\n\n');
65
+ const newParts = parts.slice();
66
+ newParts[i] = { ...part, text: cleaned };
67
+ return { ...msg, parts: newParts };
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ // Drop proxy-generated assistant messages (and the interactive selection
73
+ // replies directly following them) from the LLM context. The final message —
74
+ // the turn currently being answered — is always kept, so the proxy can still
75
+ // detect tier selections and approval replies in the live request.
76
+ function filterProxyChatter(messages) {
77
+ if (!Array.isArray(messages) || messages.length === 0)
78
+ return messages;
79
+ const kept = [];
80
+ for (let i = 0; i < messages.length; i++) {
81
+ const msg = messages[i];
82
+ const isLast = i === messages.length - 1;
83
+ if (!isLast && isMarkedProxyMessage(msg)) {
84
+ const next = messages[i + 1];
85
+ if (next && (i + 1) < messages.length - 1 && isSelectionReply(next)) {
86
+ i++;
87
+ }
88
+ continue;
89
+ }
90
+ if (msg && msg.info && msg.info.role === 'assistant') {
91
+ const cleaned = withoutPaymentSuccessLine(msg);
92
+ kept.push(cleaned || msg);
93
+ continue;
94
+ }
95
+ kept.push(msg);
96
+ }
97
+ return kept;
98
+ }
99
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  declare function OpencodePlugin(_input?: any, _options?: any): Promise<{
2
2
  config?: undefined;
3
3
  "chat.headers"?: undefined;
4
+ "experimental.chat.messages.transform"?: undefined;
4
5
  } | {
5
6
  config: (cfg: any) => Promise<void>;
6
7
  "chat.headers": (_input: any, output: any) => Promise<void>;
8
+ "experimental.chat.messages.transform": (_input: any, output: any) => Promise<void>;
7
9
  }>;
8
10
  declare const _default: {
9
11
  id: string;
@@ -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;;;;kBAgHlC,GAAG;6BA0DQ,GAAG,UAAU,GAAG;GA8BlD;;;;;AAED,kBAAoE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,iBAAe,cAAc,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,GAAG;;;;;kBA+HlC,GAAG;6BAsFQ,GAAG,UAAU,GAAG;qDA6BQ,GAAG,UAAU,GAAG;GAiB1E;;;;;AAED,kBAAoE"}
package/dist/index.js CHANGED
@@ -35,6 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  const config_1 = require("./config");
36
36
  const wallet_1 = require("./wallet");
37
37
  const proxy_1 = require("./proxy");
38
+ const mcp_1 = require("./bundled/mcp");
39
+ const context_1 = require("./context");
40
+ const selfheal_1 = require("./selfheal");
38
41
  const fs = __importStar(require("fs"));
39
42
  const path = __importStar(require("path"));
40
43
  const os = __importStar(require("os"));
@@ -42,6 +45,9 @@ async function OpencodePlugin(_input, _options) {
42
45
  const configDir = (0, config_1.getConfigDir)();
43
46
  (0, config_1.ensureConfigDir)(configDir);
44
47
  let config = (0, config_1.loadConfig)(configDir);
48
+ // Best-effort: if a newer version is published, clear stale pins/caches so
49
+ // the next install or session picks it up. Never blocks startup.
50
+ void (0, selfheal_1.runSelfHeal)();
45
51
  // Ensure paytaca binary is on PATH for internal use
46
52
  (0, wallet_1.ensurePaytacaOnPath)();
47
53
  // Check if paytaca-cli is installed
@@ -112,6 +118,17 @@ async function OpencodePlugin(_input, _options) {
112
118
  }
113
119
  // Start or reuse proxy
114
120
  const proxy = await (0, proxy_1.startProxy)(configDir, config);
121
+ // Write the MCP server script so opencode can spawn it (registered in the
122
+ // config hook below). It exposes read-only account tools (credits, balance,
123
+ // models, plans) so the assistant can answer account questions directly.
124
+ const mcpScript = (0, config_1.getMcpScript)(configDir);
125
+ try {
126
+ fs.writeFileSync(mcpScript, mcp_1.MCP_SERVER_CONTENT, 'utf8');
127
+ fs.chmodSync(mcpScript, '755');
128
+ }
129
+ catch (e) {
130
+ console.error('Failed to write MCP server script:', e.message);
131
+ }
115
132
  // Auto-install paytaca-wallet skill globally (copy from paytaca-cli dependency)
116
133
  try {
117
134
  const skillCandidates = [
@@ -197,6 +214,32 @@ async function OpencodePlugin(_input, _options) {
197
214
  'X-Wallet-Hash': cachedWalletHash,
198
215
  };
199
216
  }
217
+ // Register the local MCP server so the assistant can answer questions
218
+ // about credits, balance, models, and plans with real account data.
219
+ // opencode spawns it automatically and loads its tools into the session.
220
+ cfg.mcp = cfg.mcp || {};
221
+ cfg.mcp['paytaca'] = {
222
+ type: 'local',
223
+ command: ['node', mcpScript],
224
+ environment: {
225
+ PAYTACA_CONFIG_DIR: configDir,
226
+ PAYTACA_CMD: (0, proxy_1.getPaytacaCommand)(),
227
+ PAYTACA_BACKEND_URL: config.backendUrl || '',
228
+ },
229
+ enabled: true,
230
+ };
231
+ // The MCP send and buy_plan tools move real funds — make opencode
232
+ // prompt the user before they run (tool names are <server>_<tool> =
233
+ // 'paytaca_send' / 'paytaca_buy_plan'). A user configured choice is
234
+ // respected; only unset defaults become 'ask'.
235
+ const permissions = (cfg.permission || {});
236
+ if (permissions['paytaca_send'] === undefined) {
237
+ permissions['paytaca_send'] = 'ask';
238
+ }
239
+ if (permissions['paytaca_buy_plan'] === undefined) {
240
+ permissions['paytaca_buy_plan'] = 'ask';
241
+ }
242
+ cfg.permission = permissions;
200
243
  },
201
244
  "chat.headers": async (_input, output) => {
202
245
  // Secondary fallback delivery path. Never send an empty value —
@@ -227,6 +270,23 @@ async function OpencodePlugin(_input, _options) {
227
270
  // Leave headers untouched — the proxy will surface the missing header.
228
271
  }
229
272
  },
273
+ "experimental.chat.messages.transform": async (_input, output) => {
274
+ // Keep proxy/payment chatter (tier prompts, credits/plans output,
275
+ // payment notices and their selection replies) out of the context sent
276
+ // to the LLM. The messages remain in the session UI for the user.
277
+ try {
278
+ if (output && Array.isArray(output.messages) && output.messages.length > 0) {
279
+ const filtered = (0, context_1.filterProxyChatter)(output.messages);
280
+ output.messages.length = 0;
281
+ for (const m of filtered) {
282
+ output.messages.push(m);
283
+ }
284
+ }
285
+ }
286
+ catch (e) {
287
+ console.error('Failed to filter proxy chatter from context:', e.message);
288
+ }
289
+ },
230
290
  };
231
291
  }
232
292
  module.exports = { id: '@paytaca/opencode-plugin', server: OpencodePlugin };