@tokenbuddy/tokenbuddy 1.0.6 → 1.0.7

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.
Files changed (43) hide show
  1. package/dist/src/buyer-store.d.ts +28 -1
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +71 -16
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +17 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +201 -32
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/daemon.d.ts +5 -0
  10. package/dist/src/daemon.d.ts.map +1 -1
  11. package/dist/src/daemon.js +279 -72
  12. package/dist/src/daemon.js.map +1 -1
  13. package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
  14. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
  15. package/dist/src/doctor-clawtip-wallet.js +54 -0
  16. package/dist/src/doctor-clawtip-wallet.js.map +1 -0
  17. package/dist/src/doctor-diagnostics.d.ts +2 -0
  18. package/dist/src/doctor-diagnostics.d.ts.map +1 -1
  19. package/dist/src/doctor-diagnostics.js +5 -0
  20. package/dist/src/doctor-diagnostics.js.map +1 -1
  21. package/dist/src/init-clawtip-activation.d.ts +48 -0
  22. package/dist/src/init-clawtip-activation.d.ts.map +1 -0
  23. package/dist/src/init-clawtip-activation.js +395 -0
  24. package/dist/src/init-clawtip-activation.js.map +1 -0
  25. package/dist/src/init-payment-options.d.ts +23 -1
  26. package/dist/src/init-payment-options.d.ts.map +1 -1
  27. package/dist/src/init-payment-options.js +97 -22
  28. package/dist/src/init-payment-options.js.map +1 -1
  29. package/dist/src/terminal-image.d.ts +22 -0
  30. package/dist/src/terminal-image.d.ts.map +1 -0
  31. package/dist/src/terminal-image.js +135 -0
  32. package/dist/src/terminal-image.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/buyer-store.ts +140 -17
  35. package/src/cli.ts +251 -33
  36. package/src/daemon.ts +308 -53
  37. package/src/doctor-clawtip-wallet.ts +70 -0
  38. package/src/doctor-diagnostics.ts +11 -0
  39. package/src/init-clawtip-activation.ts +487 -0
  40. package/src/init-payment-options.ts +140 -22
  41. package/src/terminal-image.ts +187 -0
  42. package/tests/e2e.test.ts +79 -5
  43. package/tests/tokenbuddy.test.ts +745 -19
package/src/cli.ts CHANGED
@@ -36,15 +36,24 @@ import {
36
36
  renderDoctorDiagnosticsProgressively,
37
37
  } from "./doctor-diagnostics.js";
38
38
  import {
39
- buildInitTerminalOptions,
40
39
  buildInitSuccessMessage,
41
- detectExistingClawtipBinding,
40
+ buildInitTerminalSelectionState,
41
+ buildInstalledTerminalMessage,
42
42
  INIT_PAYMENT_OPTIONS,
43
+ inspectClawtipWalletReadiness,
44
+ inspectOpenClawWalletConfig,
43
45
  noteInitComingSoonPayments,
44
46
  OTHER_TERMINAL_OPTION,
45
47
  type InitPaymentMethod,
46
48
  validateInitTerminalSelection,
47
49
  } from "./init-payment-options.js";
50
+ import {
51
+ checkOpenClawRuntime,
52
+ readClawtipPayCredential,
53
+ startClawtipWalletBootstrap,
54
+ waitForClawtipActivationConfirmation,
55
+ } from "./init-clawtip-activation.js";
56
+ import { displayTerminalImage } from "./terminal-image.js";
48
57
 
49
58
  // @ts-ignore
50
59
  import qrcode from "qrcode-terminal";
@@ -162,6 +171,20 @@ async function probeDaemonStatus(controlPort: number): Promise<DaemonProbeResult
162
171
  }
163
172
  }
164
173
 
174
+ interface NormalizedClawtipPaymentPayload {
175
+ orderNo: string;
176
+ amountFen?: number;
177
+ payTo?: string;
178
+ encryptedData?: string;
179
+ indicator: string;
180
+ slug?: string;
181
+ skillId?: string;
182
+ description?: string;
183
+ resourceUrl: string;
184
+ }
185
+
186
+ const CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO = "bootstrap-pay-to";
187
+
165
188
  async function waitForDaemonStatus(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult> {
166
189
  const deadline = Date.now() + timeoutMs;
167
190
  let latest: DaemonProbeResult = { running: false, error: "not checked" };
@@ -335,7 +358,7 @@ function printPaymentList(payments: PaymentConfig[], asJson: boolean): void {
335
358
  console.log(table.toString());
336
359
  }
337
360
 
338
- async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBootstrapResponse> {
361
+ export async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBootstrapResponse> {
339
362
  const response = await fetch(`${bootstrapUrl.replace(/\/+$/, "")}/payments/clawtip/bootstrap`, {
340
363
  method: "POST",
341
364
  headers: { "Content-Type": "application/json" },
@@ -348,9 +371,35 @@ async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBoots
348
371
  if (!body.payment?.orderNo || !body.payment.indicator || !body.payment.resourceUrl) {
349
372
  throw new Error("ClawTip bootstrap response missing payment order fields");
350
373
  }
374
+ if ((body.payment.payTo || "").trim() === CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO) {
375
+ throw new Error(
376
+ [
377
+ `ClawTip bootstrap service is misconfigured: payTo is still the placeholder \`${CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO}\`.`,
378
+ `Bootstrap URL: ${bootstrapUrl}`,
379
+ "Configure the bootstrap service with the real ClawTip merchant pay_to before retrying `tb init`.",
380
+ ].join(" ")
381
+ );
382
+ }
383
+ body.payment.resourceUrl = normalizeClawtipBootstrapResourceUrl(bootstrapUrl, body.payment.resourceUrl);
351
384
  return body;
352
385
  }
353
386
 
387
+ export function normalizeClawtipBootstrapResourceUrl(bootstrapUrl: string, resourceUrl: string): string {
388
+ try {
389
+ const bootstrap = new URL(bootstrapUrl);
390
+ const resource = new URL(resourceUrl);
391
+ if (resource.origin === bootstrap.origin && resource.pathname === "/registry/sellers") {
392
+ resource.pathname = bootstrap.pathname.replace(/\/+$/, "") || "/";
393
+ resource.search = "";
394
+ resource.hash = "";
395
+ return resource.toString().replace(/\/$/, "");
396
+ }
397
+ } catch {
398
+ // Leave the server-provided value unchanged when URL parsing fails.
399
+ }
400
+ return resourceUrl;
401
+ }
402
+
354
403
  function readProof(options: { proofFile?: string; requireProof?: boolean }): string | undefined {
355
404
  const proofFile = options.proofFile || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
356
405
  if (!proofFile) {
@@ -913,15 +962,21 @@ export function buildCli(): Command {
913
962
  const spinner = p.spinner();
914
963
  spinner.start("Scanning local system for programming terminals...");
915
964
  const candidates = detectProviders();
916
- const terminalOptions = buildInitTerminalOptions(candidates);
965
+ const terminalSelection = buildInitTerminalSelectionState(candidates);
917
966
  spinner.stop("Scan completed.");
918
967
 
919
- if (terminalOptions.length === 1 && terminalOptions[0].value === OTHER_TERMINAL_OPTION.value) {
968
+ const installedTerminalMessage = buildInstalledTerminalMessage(terminalSelection.installed);
969
+ if (installedTerminalMessage) {
970
+ p.note(installedTerminalMessage, "Already Configured");
971
+ setupSummaryLines.push(`${terminalSelection.installed.length} terminal${terminalSelection.installed.length === 1 ? "" : "s"} already configured.`);
972
+ }
973
+
974
+ if (terminalSelection.options.length === 1 && terminalSelection.options[0].value === OTHER_TERMINAL_OPTION.value) {
920
975
  p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenCode, OpenClaw or Hermes first.");
921
976
  } else {
922
977
  const selected = await p.multiselect({
923
978
  message: "Select programming terminals to route via TokenBuddy (use Space to select, Enter to confirm):",
924
- options: terminalOptions,
979
+ options: terminalSelection.options,
925
980
  required: false
926
981
  }) as string[];
927
982
 
@@ -1008,7 +1063,9 @@ export function buildCli(): Command {
1008
1063
  if (payMethod === "clawtip") {
1009
1064
  const store = openBuyerStore();
1010
1065
  try {
1011
- const existingClawtip = detectExistingClawtipBinding(store.getPayment("clawtip"));
1066
+ let walletConfig = inspectOpenClawWalletConfig();
1067
+ const clawtipReadiness = inspectClawtipWalletReadiness(store.getPayment("clawtip"), walletConfig);
1068
+ const existingClawtip = clawtipReadiness.reusableBinding;
1012
1069
  if (existingClawtip) {
1013
1070
  store.savePayment({
1014
1071
  method: "clawtip",
@@ -1033,30 +1090,159 @@ export function buildCli(): Command {
1033
1090
  );
1034
1091
  setupSummaryLines.push("ClawTip wallet already bound locally; activation skipped.");
1035
1092
  } else {
1093
+ if (clawtipReadiness.status === "metadata_missing_wallet") {
1094
+ p.note(
1095
+ [
1096
+ clawtipReadiness.message,
1097
+ `Expected: ${walletConfig.expectedPath}`,
1098
+ walletConfig.alternatePaths.length > 0
1099
+ ? `Alternates: ${walletConfig.alternatePaths.join(", ")}`
1100
+ : "Alternates: -"
1101
+ ].join("\n"),
1102
+ "ClawTip"
1103
+ );
1104
+ setupSummaryLines.push("Saved ClawTip metadata found, but local wallet config is missing; activation restarted.");
1105
+ }
1106
+
1107
+ const walletReadyBeforePay = walletConfig.exists;
1108
+ let openClawVersion: string | undefined;
1109
+ if (!walletReadyBeforePay) {
1110
+ spinner.start("Checking OpenClaw CLI before ClawTip wallet bootstrap...");
1111
+ openClawVersion = await checkOpenClawRuntime();
1112
+ spinner.stop("OpenClaw CLI detected.");
1113
+ }
1114
+
1036
1115
  spinner.start("Requesting payment activation payload from public bootstrap registry...");
1037
1116
  const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
1038
- const res = await fetch(`${bootstrapUrl}/payments/clawtip/bootstrap`, {
1039
- method: "POST",
1040
- headers: { "Content-Type": "application/json" },
1041
- body: JSON.stringify({ clientTag: "cli-init" })
1042
- });
1043
- const data: any = await res.json();
1117
+ const bootstrap = await fetchClawtipBootstrap(bootstrapUrl);
1044
1118
  spinner.stop("Bootstrap payload received.");
1045
1119
 
1046
- const qrUrl = data.payment?.resourceUrl || "https://example.com";
1120
+ if (!bootstrap.payment?.orderNo || !bootstrap.payment?.indicator) {
1121
+ throw new Error("ClawTip bootstrap response missing orderNo or indicator.");
1122
+ }
1047
1123
 
1048
- p.note("Scan the QR code below using your JD / WeChat App to complete the 1 Fen payment activation:");
1124
+ const activationPayment = {
1125
+ orderNo: bootstrap.payment.orderNo,
1126
+ amountFen: bootstrap.payment.amountFen ?? bootstrap.activationFeeFen ?? 1,
1127
+ payTo: bootstrap.payment.payTo,
1128
+ encryptedData: bootstrap.payment.encryptedData,
1129
+ indicator: bootstrap.payment.indicator,
1130
+ slug: bootstrap.payment.slug,
1131
+ skillId: bootstrap.payment.skillId,
1132
+ description: bootstrap.payment.description,
1133
+ resourceUrl: bootstrap.payment.resourceUrl,
1134
+ };
1135
+
1136
+ spinner.start("Starting the ClawTip payment activation flow...");
1137
+ let activation = await startClawtipWalletBootstrap(activationPayment);
1138
+ spinner.stop("ClawTip payment activation finished.");
1139
+
1140
+ let payCredential = activation.payCredential;
1141
+ for (
1142
+ let authAttempt = 1;
1143
+ (walletReadyBeforePay ? !payCredential : !walletConfig.exists)
1144
+ && activation.parsedOutput.requiresWalletAuth
1145
+ && authAttempt <= 3;
1146
+ authAttempt += 1
1147
+ ) {
1148
+ let qrDisplayMessage: string | undefined;
1149
+ let manualOpenCommand: string | undefined;
1150
+ if (activation.parsedOutput.mediaPath) {
1151
+ const qrDisplay = await displayTerminalImage(activation.parsedOutput.mediaPath);
1152
+ qrDisplayMessage = qrDisplay.message;
1153
+ manualOpenCommand = qrDisplay.fallbackCommand;
1154
+ }
1155
+ if (!activation.parsedOutput.mediaPath && !activation.parsedOutput.authUrl) {
1156
+ throw new Error(
1157
+ `ClawTip pay requested authorization but did not return QR media or authUrl. Order file: ${activation.orderFile}`
1158
+ );
1159
+ }
1160
+ p.note(
1161
+ [
1162
+ activation.parsedOutput.mediaPath
1163
+ ? `Open or scan this ClawTip wallet QR image with the JD app: ${activation.parsedOutput.mediaPath}`
1164
+ : undefined,
1165
+ activation.parsedOutput.authUrl
1166
+ ? `Open or scan this ClawTip wallet auth URL with the JD app: ${activation.parsedOutput.authUrl}`
1167
+ : undefined,
1168
+ qrDisplayMessage,
1169
+ manualOpenCommand ? `Manual open command: ${manualOpenCommand}` : undefined,
1170
+ activation.parsedOutput.clawtipId ? `clawtipId: ${activation.parsedOutput.clawtipId}` : undefined,
1171
+ activation.parsedOutput.clawtipId
1172
+ ? "After scanning, ClawTip CLI will register the local agent wallet."
1173
+ : "After scanning, TokenBuddy will retry ClawTip payment activation.",
1174
+ `Expected wallet config: ${walletConfig.expectedPath}`,
1175
+ openClawVersion ? `OpenClaw: ${openClawVersion}` : undefined,
1176
+ ].filter(Boolean).join("\n"),
1177
+ activation.parsedOutput.clawtipId ? "ClawTip Wallet QR" : "ClawTip Authorization QR"
1178
+ );
1179
+
1180
+ if (activation.parsedOutput.clawtipId && !walletReadyBeforePay) {
1181
+ spinner.start("Waiting for ClawTip wallet registration. Press Ctrl+C to cancel.");
1182
+ const walletRegistered = await waitForClawtipActivationConfirmation({
1183
+ clawtipId: activation.parsedOutput.clawtipId,
1184
+ });
1185
+ spinner.stop(walletRegistered
1186
+ ? "Detected local OpenClaw ClawTip wallet config."
1187
+ : "ClawTip wallet registration cancelled.");
1188
+ if (!walletRegistered) {
1189
+ setupSummaryLines.push("ClawTip wallet registration was cancelled before local wallet config was saved.");
1190
+ process.exitCode = 1;
1191
+ return;
1192
+ }
1193
+
1194
+ walletConfig = inspectOpenClawWalletConfig();
1195
+ if (!walletConfig.exists) {
1196
+ throw new Error(`ClawTip wallet registration finished but wallet config is still missing: ${walletConfig.expectedPath}`);
1197
+ }
1198
+ setupSummaryLines.push("ClawTip wallet registered locally.");
1199
+ } else {
1200
+ const authorized = await p.confirm({
1201
+ message: "Scan or authorize this ClawTip QR in the JD app, then press Enter to retry payment activation.",
1202
+ initialValue: true,
1203
+ });
1204
+ if (authorized !== true) {
1205
+ setupSummaryLines.push("ClawTip authorization was cancelled before payment activation completed.");
1206
+ process.exitCode = 1;
1207
+ return;
1208
+ }
1209
+ walletConfig = inspectOpenClawWalletConfig();
1210
+ }
1049
1211
 
1050
- // 💡 High fidelity QR code rendering directly inside the CLI terminal
1051
- qrcode.generate(qrUrl, { small: true });
1212
+ payCredential = readClawtipPayCredential(activation.orderFile);
1213
+ if (!payCredential) {
1214
+ spinner.start(`Retrying ClawTip payment activation after authorization (${authAttempt}/3)...`);
1215
+ activation = await startClawtipWalletBootstrap(activationPayment);
1216
+ spinner.stop("ClawTip payment activation retry finished.");
1217
+ payCredential = activation.payCredential;
1218
+ }
1219
+ }
1052
1220
 
1053
- // Start 5-second polling interval
1054
- spinner.start("Waiting for JD收银台 payment confirmation (polling activation status)...");
1055
- for (let i = 0; i < 5; i++) {
1056
- await new Promise(resolve => setTimeout(resolve, 3000));
1057
- // Simulate/Wait confirmed. For real deployment, poll actual backend
1221
+ const refreshedWalletConfig = inspectOpenClawWalletConfig();
1222
+ if (!walletReadyBeforePay && !refreshedWalletConfig.exists) {
1223
+ const paymentQrMessage = activation.parsedOutput.mediaPath
1224
+ ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
1225
+ : " ClawTip pay did not return QR media.";
1226
+ throw new Error(
1227
+ [
1228
+ `ClawTip wallet config is still missing after payment activation: ${refreshedWalletConfig.expectedPath}.`,
1229
+ `Order file: ${activation.orderFile}.`,
1230
+ paymentQrMessage.trim(),
1231
+ ].join(" ")
1232
+ );
1058
1233
  }
1059
- spinner.stop("JD收银台 confirmed payment. ClawTip wallet is active! 🚀");
1234
+ const completedByWalletConfig = !payCredential && !walletReadyBeforePay && refreshedWalletConfig.exists;
1235
+ if (!payCredential && !completedByWalletConfig) {
1236
+ const paymentQrMessage = activation.parsedOutput.mediaPath
1237
+ ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
1238
+ : "";
1239
+ throw new Error(
1240
+ `ClawTip pay did not write payCredential to the order file. Order file: ${activation.orderFile}.${paymentQrMessage}`
1241
+ );
1242
+ }
1243
+ const activationCompletedBy = payCredential
1244
+ ? (refreshedWalletConfig.exists ? "payCredential+wallet-config" : "payCredential")
1245
+ : "wallet-config";
1060
1246
 
1061
1247
  store.savePayment({
1062
1248
  method: "clawtip",
@@ -1064,21 +1250,53 @@ export function buildCli(): Command {
1064
1250
  isDefault: true,
1065
1251
  config: {
1066
1252
  bootstrapUrl,
1067
- orderNo: data.payment?.orderNo,
1068
- amountFen: data.payment?.amountFen ?? data.activationFeeFen,
1069
- indicator: data.payment?.indicator,
1070
- slug: data.payment?.slug,
1071
- skillId: data.payment?.skillId,
1072
- description: data.payment?.description,
1073
- resourceUrl: data.payment?.resourceUrl,
1074
- proofRequired: false
1253
+ orderNo: bootstrap.payment?.orderNo,
1254
+ amountFen: bootstrap.payment?.amountFen ?? bootstrap.activationFeeFen,
1255
+ indicator: bootstrap.payment?.indicator,
1256
+ slug: bootstrap.payment?.slug,
1257
+ skillId: bootstrap.payment?.skillId,
1258
+ description: bootstrap.payment?.description,
1259
+ resourceUrl: bootstrap.payment?.resourceUrl,
1260
+ proofRequired: false,
1261
+ activationOrderFile: activation.orderFile,
1262
+ walletConfigPath: refreshedWalletConfig.expectedPath,
1263
+ walletConfigPresent: refreshedWalletConfig.exists,
1264
+ payCredentialWritten: Boolean(payCredential),
1265
+ activationCompletedBy
1075
1266
  }
1076
1267
  });
1077
1268
  logger.info("payment.channel.added", "clawtip payment channel added during init", {
1078
1269
  method: "clawtip",
1079
- orderNo: data.payment?.orderNo
1270
+ orderNo: bootstrap.payment?.orderNo,
1271
+ payCredentialWritten: Boolean(payCredential),
1272
+ activationCompletedBy
1080
1273
  });
1081
- setupSummaryLines.push("ClawTip wallet activated and set as the default payment method.");
1274
+ if (refreshedWalletConfig.exists) {
1275
+ if (!payCredential) {
1276
+ p.note(
1277
+ [
1278
+ "OpenClaw saved the local ClawTip wallet config, but the ClawTip order file did not contain payCredential.",
1279
+ `Order file: ${activation.orderFile}`,
1280
+ "TokenBuddy saved the wallet binding metadata and will rely on the local wallet for future ClawTip purchases."
1281
+ ].join("\n"),
1282
+ "ClawTip"
1283
+ );
1284
+ }
1285
+ setupSummaryLines.push("ClawTip wallet activated and set as the default payment method.");
1286
+ } else {
1287
+ p.note(
1288
+ [
1289
+ "ClawTip payment metadata was saved, but the local OpenClaw wallet config is still missing.",
1290
+ `Expected: ${refreshedWalletConfig.expectedPath}`,
1291
+ refreshedWalletConfig.alternatePaths.length > 0
1292
+ ? `Nearby files: ${refreshedWalletConfig.alternatePaths.join(", ")}`
1293
+ : "Nearby files: -",
1294
+ "Bind or restore the local wallet before using ClawTip-backed purchases."
1295
+ ].join("\n"),
1296
+ "ClawTip Wallet Required"
1297
+ );
1298
+ setupSummaryLines.push("ClawTip payment metadata saved; local wallet config still needs binding before use.");
1299
+ }
1082
1300
  }
1083
1301
  } catch (err: any) {
1084
1302
  spinner.stop(`Failed to finish ClawTip setup: ${err.message}`);