@tokenbuddy/tokenbuddy 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/daemon.ts CHANGED
@@ -52,6 +52,34 @@ interface UsageSummary {
52
52
  billedMicros: number;
53
53
  }
54
54
 
55
+ interface SellerSettlementSummary {
56
+ requestId: string;
57
+ settledMicros: number;
58
+ settledUsdMicros?: number;
59
+ remainingCreditMicros: number;
60
+ reservedBalanceMicros?: number;
61
+ spentMicros?: number;
62
+ priceVersion?: string;
63
+ }
64
+
65
+ interface SellerBalanceSnapshot {
66
+ creditMicros: number;
67
+ reservedMicros: number;
68
+ spentMicros: number;
69
+ availableMicros: number;
70
+ }
71
+
72
+ function numericHeaderField(value: unknown): number | undefined {
73
+ if (typeof value === "number" && Number.isFinite(value)) {
74
+ return value;
75
+ }
76
+ if (typeof value === "string" && value.trim().length > 0) {
77
+ const parsed = Number(value);
78
+ return Number.isFinite(parsed) ? parsed : undefined;
79
+ }
80
+ return undefined;
81
+ }
82
+
55
83
  interface ResponsesStreamState {
56
84
  itemId: string;
57
85
  text: string;
@@ -271,6 +299,83 @@ class ResponsesStreamNormalizer {
271
299
  }
272
300
  }
273
301
 
302
+ class SellerSettlementStreamExtractor {
303
+ private pending = "";
304
+ private settlement: SellerSettlementSummary | undefined;
305
+
306
+ public push(chunk: string): string {
307
+ this.pending += chunk;
308
+ const blocks = this.pending.split("\n\n");
309
+ this.pending = blocks.pop() || "";
310
+ return blocks
311
+ .map((block) => this.processBlock(block))
312
+ .filter((block) => block.length > 0)
313
+ .join("\n\n");
314
+ }
315
+
316
+ public finish(): { downstream: string; settlement: SellerSettlementSummary | undefined } {
317
+ const downstream = this.pending.trim() ? this.processBlock(this.pending) : "";
318
+ this.pending = "";
319
+ return { downstream, settlement: this.settlement };
320
+ }
321
+
322
+ public current(): SellerSettlementSummary | undefined {
323
+ return this.settlement;
324
+ }
325
+
326
+ private processBlock(block: string): string {
327
+ if (!block.trim()) {
328
+ return "";
329
+ }
330
+ const lines = block.split("\n");
331
+ const eventLine = lines.find((line) => line.startsWith("event:"));
332
+ const eventName = eventLine?.replace(/^event:\s?/, "").trim();
333
+ if (eventName !== "tokenbuddy.settlement") {
334
+ return block;
335
+ }
336
+ const dataLine = lines.find((line) => line.startsWith("data:"));
337
+ if (!dataLine) {
338
+ return "";
339
+ }
340
+ const parsed = parseSellerSettlementObject(dataLine.replace(/^data:\s?/, ""));
341
+ if (parsed) {
342
+ this.settlement = parsed;
343
+ }
344
+ return "";
345
+ }
346
+ }
347
+
348
+ function parseSellerSettlementObject(raw: string): SellerSettlementSummary | undefined {
349
+ try {
350
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
351
+ const requestId = typeof parsed.requestId === "string"
352
+ ? parsed.requestId
353
+ : typeof parsed.request_id === "string"
354
+ ? parsed.request_id
355
+ : undefined;
356
+ const settledMicros = numericHeaderField(parsed.settledMicros ?? parsed.settled_micros);
357
+ const remainingCreditMicros = numericHeaderField(parsed.remainingCreditMicros ?? parsed.remaining_credit_micros);
358
+ if (!requestId || settledMicros === undefined || remainingCreditMicros === undefined) {
359
+ return undefined;
360
+ }
361
+ return {
362
+ requestId,
363
+ settledMicros,
364
+ settledUsdMicros: numericHeaderField(parsed.settledUsdMicros ?? parsed.settled_usd_micros),
365
+ remainingCreditMicros,
366
+ reservedBalanceMicros: numericHeaderField(parsed.reservedBalanceMicros ?? parsed.reserved_balance_micros),
367
+ spentMicros: numericHeaderField(parsed.spentMicros ?? parsed.spent_micros),
368
+ priceVersion: typeof parsed.priceVersion === "string"
369
+ ? parsed.priceVersion
370
+ : typeof parsed.price_version === "string"
371
+ ? parsed.price_version
372
+ : undefined
373
+ };
374
+ } catch {
375
+ return undefined;
376
+ }
377
+ }
378
+
274
379
  interface PurchaseCreateResponse {
275
380
  purchaseId?: string;
276
381
  purchase_id?: string;
@@ -588,6 +693,138 @@ export class TokenbuddyDaemon {
588
693
  }
589
694
  }
590
695
 
696
+ private parseSellerSettlementSummary(headers: Headers): SellerSettlementSummary | undefined {
697
+ const raw = headers.get("x-tokenbuddy-settlement");
698
+ if (!raw) {
699
+ return undefined;
700
+ }
701
+ return parseSellerSettlementObject(raw);
702
+ }
703
+
704
+ private recordReconciledInference(
705
+ route: SellerRoute,
706
+ endpoint: string,
707
+ requestId: string,
708
+ usage: UsageSummary,
709
+ settlement: SellerSettlementSummary | undefined,
710
+ prompt: string | undefined,
711
+ response?: string
712
+ ): void {
713
+ if (settlement) {
714
+ this.tokenStore.reconcileTokenBalance({
715
+ sellerKey: route.seller.id,
716
+ balanceMicros: settlement.remainingCreditMicros,
717
+ reservedMicros: settlement.reservedBalanceMicros ?? 0,
718
+ spentMicros: settlement.spentMicros ?? 0,
719
+ balanceSource: "seller_settlement_summary"
720
+ });
721
+ }
722
+
723
+ const settledMicros = settlement?.settledMicros;
724
+ this.tokenStore.recordInferenceLedger({
725
+ requestId: settlement?.requestId || requestId,
726
+ sellerKey: route.seller.id,
727
+ modelId: route.modelId,
728
+ endpoint,
729
+ status: settlement ? "settled" : "estimated",
730
+ promptTokens: usage.promptTokens,
731
+ completionTokens: usage.completionTokens,
732
+ billedMicros: settledMicros ?? usage.billedMicros,
733
+ estimatedMicros: usage.billedMicros,
734
+ settledMicros,
735
+ settledUsdMicros: settlement?.settledUsdMicros,
736
+ priceVersion: settlement?.priceVersion,
737
+ balanceSnapshotMicros: settlement?.remainingCreditMicros,
738
+ balanceSource: settlement ? "seller_authoritative" : "estimated",
739
+ prompt,
740
+ response
741
+ });
742
+ logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
743
+ requestId: settlement?.requestId || requestId,
744
+ sellerKey: route.seller.id,
745
+ model: route.modelId,
746
+ endpoint,
747
+ status: settlement ? "settled" : "estimated",
748
+ estimatedMicros: usage.billedMicros,
749
+ settledMicros,
750
+ balanceSource: settlement ? "seller_authoritative" : "estimated"
751
+ });
752
+ }
753
+
754
+ private async refreshSellerBalance(route: SellerRoute, token: string, balanceSource: string): Promise<SellerBalanceSnapshot | undefined> {
755
+ const sellerKey = route.seller.id;
756
+ const sellerUrl = normalizeSellerUrl(route.seller);
757
+ const response = await fetch(`${sellerUrl}/v1/balance`, {
758
+ headers: { "Authorization": `Bearer ${token}` }
759
+ });
760
+ if (!response.ok) {
761
+ logger.warn("token.balance_refresh.failed", "seller balance refresh failed", {
762
+ sellerKey,
763
+ model: route.modelId,
764
+ status: response.status
765
+ });
766
+ return undefined;
767
+ }
768
+ const data = await response.json() as Record<string, unknown>;
769
+ const creditMicros = numericHeaderField(data.creditMicros ?? data.credit_micros) ?? 0;
770
+ const reservedMicros = numericHeaderField(data.reservedMicros ?? data.reserved_micros) ?? 0;
771
+ const spentMicros = numericHeaderField(data.spentMicros ?? data.spent_micros) ?? 0;
772
+ const snapshot = {
773
+ creditMicros,
774
+ reservedMicros,
775
+ spentMicros,
776
+ availableMicros: Math.max(0, creditMicros - reservedMicros)
777
+ };
778
+ this.tokenStore.reconcileTokenBalance({
779
+ sellerKey,
780
+ balanceMicros: snapshot.availableMicros,
781
+ reservedMicros,
782
+ spentMicros,
783
+ balanceSource
784
+ });
785
+ logger.info("token.balance_refresh.succeeded", "seller balance refreshed", {
786
+ sellerKey,
787
+ model: route.modelId,
788
+ availableMicros: snapshot.availableMicros,
789
+ reservedMicros,
790
+ spentMicros,
791
+ balanceSource
792
+ });
793
+ return snapshot;
794
+ }
795
+
796
+ private isInsufficientFundsResponse(status: number, bodyText: string): boolean {
797
+ if (status !== 402) {
798
+ return false;
799
+ }
800
+ try {
801
+ const parsed = JSON.parse(bodyText) as { error?: { code?: string; message?: string } };
802
+ const code = parsed.error?.code || "";
803
+ const message = parsed.error?.message || "";
804
+ return code === "insufficient_funds" || /insufficient funds/i.test(message);
805
+ } catch {
806
+ return /insufficient funds/i.test(bodyText);
807
+ }
808
+ }
809
+
810
+ private async recoverFromInsufficientFunds(route: SellerRoute, token: string): Promise<string> {
811
+ const sellerKey = route.seller.id;
812
+ this.tokenStore.markTokenStale(sellerKey);
813
+ const snapshot = await this.refreshSellerBalance(route, token, "seller_402_refresh");
814
+ const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
815
+ if (!snapshot || snapshot.availableMicros <= rebuyMinBalanceMicros) {
816
+ logger.info("purchase.retry_after_402.started", "seller 402 triggered one-shot auto purchase retry", {
817
+ sellerKey,
818
+ model: route.modelId,
819
+ availableMicros: snapshot?.availableMicros ?? 0,
820
+ rebuyMinBalanceMicros
821
+ });
822
+ return await this.getOrPurchaseToken(route);
823
+ }
824
+ const cached = this.tokenStore.getToken(sellerKey);
825
+ return cached?.token || token;
826
+ }
827
+
591
828
  private inferPromptForHash(body: unknown): string | undefined {
592
829
  if (!body || typeof body !== "object") {
593
830
  return undefined;
@@ -930,21 +1167,12 @@ export class TokenbuddyDaemon {
930
1167
  endpoint,
931
1168
  stream: Boolean((body as { stream?: unknown }).stream)
932
1169
  });
933
- const token = await this.getOrPurchaseToken(route);
934
1170
  const sellerUrl = normalizeSellerUrl(route.seller);
935
1171
  const upstreamBody = this.applyResolvedModelToBody(endpoint, {
936
1172
  ...(body as Record<string, unknown>),
937
1173
  requestId
938
1174
  }, modelId);
939
-
940
- logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
941
- requestId,
942
- sellerKey,
943
- model: modelId,
944
- endpoint,
945
- stream: Boolean((body as { stream?: unknown }).stream)
946
- });
947
- const upstreamResponse = await fetch(`${sellerUrl}${endpoint}`, {
1175
+ const sendSellerRequest = async (token: string) => fetch(`${sellerUrl}${endpoint}`, {
948
1176
  method: "POST",
949
1177
  headers: {
950
1178
  "Content-Type": "application/json",
@@ -955,8 +1183,45 @@ export class TokenbuddyDaemon {
955
1183
  body: JSON.stringify(upstreamBody)
956
1184
  });
957
1185
 
1186
+ logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
1187
+ requestId,
1188
+ sellerKey,
1189
+ model: modelId,
1190
+ endpoint,
1191
+ stream: Boolean((body as { stream?: unknown }).stream)
1192
+ });
1193
+ let token = await this.getOrPurchaseToken(route);
1194
+ let upstreamResponse = await sendSellerRequest(token);
1195
+
958
1196
  if (!upstreamResponse.ok) {
959
1197
  const errorBody = await upstreamResponse.text();
1198
+ if (this.isInsufficientFundsResponse(upstreamResponse.status, errorBody)) {
1199
+ token = await this.recoverFromInsufficientFunds(route, token);
1200
+ upstreamResponse = await sendSellerRequest(token);
1201
+ if (upstreamResponse.ok) {
1202
+ logger.info("proxy.retry_after_402.succeeded", "seller request succeeded after one-shot auto purchase retry", {
1203
+ requestId,
1204
+ sellerKey,
1205
+ model: modelId,
1206
+ endpoint,
1207
+ durationMs: Date.now() - startedAt
1208
+ });
1209
+ } else {
1210
+ const retryErrorBody = await upstreamResponse.text();
1211
+ logger.warn("proxy.retry_after_402.failed", "seller request still failed after one-shot auto purchase retry", {
1212
+ requestId,
1213
+ sellerKey,
1214
+ model: modelId,
1215
+ endpoint,
1216
+ status: upstreamResponse.status,
1217
+ durationMs: Date.now() - startedAt
1218
+ });
1219
+ this.copyUpstreamHeaders(upstreamResponse, res);
1220
+ res.status(upstreamResponse.status);
1221
+ res.send(retryErrorBody);
1222
+ return;
1223
+ }
1224
+ } else {
960
1225
  logger.warn("proxy.upstream_fetch.failed", "proxy upstream fetch returned non-ok status", {
961
1226
  requestId,
962
1227
  sellerKey,
@@ -974,6 +1239,7 @@ export class TokenbuddyDaemon {
974
1239
  res.status(upstreamResponse.status);
975
1240
  res.send(errorBody);
976
1241
  return;
1242
+ }
977
1243
  }
978
1244
 
979
1245
  this.copyUpstreamHeaders(upstreamResponse, res);
@@ -997,20 +1263,36 @@ export class TokenbuddyDaemon {
997
1263
  let bytes = 0;
998
1264
  const decoder = new TextDecoder();
999
1265
  const responsesStreamNormalizer = new ResponsesStreamNormalizer();
1266
+ const settlementExtractor = new SellerSettlementStreamExtractor();
1000
1267
  while (true) {
1001
1268
  const { done, value } = await reader.read();
1002
1269
  if (done) {
1003
1270
  break;
1004
1271
  }
1005
1272
  bytes += value.byteLength;
1273
+ const chunk = decoder.decode(value, { stream: true });
1274
+ const sellerChunk = settlementExtractor.push(chunk);
1275
+ if (sellerChunk.length === 0) {
1276
+ continue;
1277
+ }
1006
1278
  if (endpoint === "/v1/responses") {
1007
- const chunk = decoder.decode(value, { stream: true });
1008
- const normalized = responsesStreamNormalizer.push(chunk);
1279
+ const normalized = responsesStreamNormalizer.push(sellerChunk);
1009
1280
  if (normalized.length > 0) {
1010
1281
  res.write(`${normalized}\n\n`);
1011
1282
  }
1012
1283
  } else {
1013
- res.write(Buffer.from(value));
1284
+ res.write(sellerChunk);
1285
+ }
1286
+ }
1287
+ const settlementTrailing = settlementExtractor.finish();
1288
+ if (settlementTrailing.downstream.length > 0) {
1289
+ if (endpoint === "/v1/responses") {
1290
+ const normalized = responsesStreamNormalizer.push(settlementTrailing.downstream);
1291
+ if (normalized.length > 0) {
1292
+ res.write(`${normalized}\n\n`);
1293
+ }
1294
+ } else {
1295
+ res.write(settlementTrailing.downstream);
1014
1296
  }
1015
1297
  }
1016
1298
  if (endpoint === "/v1/responses") {
@@ -1020,56 +1302,29 @@ export class TokenbuddyDaemon {
1020
1302
  }
1021
1303
  }
1022
1304
  res.end();
1023
- const billedMicros = Math.max(1, bytes);
1024
- this.tokenStore.deductBalance(sellerKey, billedMicros);
1025
- this.tokenStore.recordInferenceLedger({
1026
- requestId,
1027
- sellerKey,
1028
- modelId,
1305
+ this.recordReconciledInference(
1306
+ route,
1029
1307
  endpoint,
1030
- status: "settled",
1031
- promptTokens: 0,
1032
- completionTokens: 0,
1033
- billedMicros,
1034
- prompt: this.inferPromptForHash(body)
1035
- });
1036
- logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
1037
1308
  requestId,
1038
- sellerKey,
1039
- model: modelId,
1040
- endpoint,
1041
- status: "settled",
1042
- billedMicros
1043
- });
1309
+ { promptTokens: 0, completionTokens: 0, billedMicros: Math.max(1, bytes) },
1310
+ this.parseSellerSettlementSummary(upstreamResponse.headers) ?? settlementTrailing.settlement ?? settlementExtractor.current(),
1311
+ this.inferPromptForHash(body)
1312
+ );
1044
1313
  return;
1045
1314
  }
1046
1315
 
1047
1316
  const responseBody = await upstreamResponse.text();
1048
1317
  res.send(responseBody);
1049
1318
  const usage = this.readUsage(responseBody);
1050
- this.tokenStore.deductBalance(sellerKey, usage.billedMicros);
1051
- this.tokenStore.recordInferenceLedger({
1052
- requestId,
1053
- sellerKey,
1054
- modelId,
1319
+ this.recordReconciledInference(
1320
+ route,
1055
1321
  endpoint,
1056
- status: "settled",
1057
- promptTokens: usage.promptTokens,
1058
- completionTokens: usage.completionTokens,
1059
- billedMicros: usage.billedMicros,
1060
- prompt: this.inferPromptForHash(body),
1061
- response: responseBody
1062
- });
1063
- logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
1064
1322
  requestId,
1065
- sellerKey,
1066
- model: modelId,
1067
- endpoint,
1068
- status: "settled",
1069
- promptTokens: usage.promptTokens,
1070
- completionTokens: usage.completionTokens,
1071
- billedMicros: usage.billedMicros
1072
- });
1323
+ usage,
1324
+ this.parseSellerSettlementSummary(upstreamResponse.headers),
1325
+ this.inferPromptForHash(body),
1326
+ responseBody
1327
+ );
1073
1328
  return;
1074
1329
  } catch (routeError: unknown) {
1075
1330
  lastError = routeError;
@@ -0,0 +1,70 @@
1
+ import { BuyerStore } from "./buyer-store.js";
2
+ import {
3
+ inspectClawtipWalletReadiness,
4
+ type ClawtipWalletStatus,
5
+ } from "./init-payment-options.js";
6
+
7
+ export interface DoctorClawtipWalletSummary {
8
+ status: ClawtipWalletStatus;
9
+ ready: boolean;
10
+ paymentMetadataPresent: boolean;
11
+ walletConfigPresent: boolean;
12
+ configsDirExists: boolean;
13
+ expectedPath: string;
14
+ alternatePaths: string[];
15
+ message: string;
16
+ }
17
+
18
+ function clawtipWalletStatusIcon(status: ClawtipWalletStatus): string {
19
+ if (status === "ready") {
20
+ return "✅";
21
+ }
22
+ if (status === "metadata_missing_wallet") {
23
+ return "❌";
24
+ }
25
+ if (status === "wallet_missing_metadata") {
26
+ return "🟡";
27
+ }
28
+ return "🔘";
29
+ }
30
+
31
+ export function readDoctorClawtipWallet(): DoctorClawtipWalletSummary {
32
+ const store = new BuyerStore();
33
+ try {
34
+ const readiness = inspectClawtipWalletReadiness(store.getPayment("clawtip"));
35
+ return {
36
+ status: readiness.status,
37
+ ready: readiness.status === "ready",
38
+ paymentMetadataPresent: Boolean(readiness.savedBinding),
39
+ walletConfigPresent: readiness.walletConfig.exists,
40
+ configsDirExists: readiness.walletConfig.configsDirExists,
41
+ expectedPath: readiness.walletConfig.expectedPath,
42
+ alternatePaths: readiness.walletConfig.alternatePaths,
43
+ message: readiness.message,
44
+ };
45
+ } finally {
46
+ store.close();
47
+ }
48
+ }
49
+
50
+ export function printDoctorClawtipWallet(
51
+ wallet: DoctorClawtipWalletSummary,
52
+ writeLine: (line: string) => void,
53
+ ): void {
54
+ writeLine("\n--- ClawTip Wallet ---");
55
+ writeLine(`${clawtipWalletStatusIcon(wallet.status)} ClawTip Wallet [${wallet.status}]`);
56
+ writeLine(` Payment metadata: ${wallet.paymentMetadataPresent ? "present" : "missing"}`);
57
+ writeLine(` Wallet config: ${wallet.walletConfigPresent ? "found" : "missing"}`);
58
+ writeLine(` Expected: ${wallet.expectedPath}`);
59
+ if (wallet.alternatePaths.length > 0) {
60
+ writeLine(` Nearby files: ${wallet.alternatePaths.join(", ")}`);
61
+ }
62
+ writeLine(` Notes: ${wallet.message}`);
63
+ if (wallet.status === "metadata_missing_wallet") {
64
+ writeLine(" Action: Run `tb init` and choose ClawTip to bind the local wallet again.");
65
+ } else if (wallet.status === "wallet_missing_metadata") {
66
+ writeLine(" Action: Run `tb init` and choose ClawTip so TokenBuddy can save payment metadata.");
67
+ } else if (!wallet.ready) {
68
+ writeLine(" Action: Run `tb init` and choose ClawTip to bind a wallet before using ClawTip-backed purchases.");
69
+ }
70
+ }
@@ -13,6 +13,11 @@ import {
13
13
  type ModelCatalogEntry,
14
14
  type SellerCatalogEntry,
15
15
  } from "./seller-catalog.js";
16
+ import {
17
+ printDoctorClawtipWallet,
18
+ readDoctorClawtipWallet,
19
+ type DoctorClawtipWalletSummary,
20
+ } from "./doctor-clawtip-wallet.js";
16
21
 
17
22
  export interface DoctorProviderView extends ProviderCandidate {
18
23
  runtimeConfig?: ProviderRuntimeConfig;
@@ -34,6 +39,7 @@ export interface DoctorSellerEntry {
34
39
 
35
40
  export interface DoctorDiagnostics {
36
41
  access: DoctorAccessSummary;
42
+ clawtipWallet: DoctorClawtipWalletSummary;
37
43
  models: DoctorModelsSummary;
38
44
  providers: DoctorProviderView[];
39
45
  sellers: DoctorSellersSummary;
@@ -749,6 +755,7 @@ export async function collectDoctorDiagnostics(options: DoctorCollectOptions): P
749
755
  modelsError: models.error,
750
756
  },
751
757
  ),
758
+ clawtipWallet: readDoctorClawtipWallet(),
752
759
  models,
753
760
  providers: options.providers,
754
761
  sellers,
@@ -784,6 +791,7 @@ export async function collectDoctorModelsSummary(options: Omit<DoctorCollectOpti
784
791
 
785
792
  export async function renderDoctorDiagnosticsProgressively(options: DoctorRenderOptions): Promise<DoctorDiagnostics> {
786
793
  const writeLine = options.writeLine || defaultWriter;
794
+ const clawtipWallet = readDoctorClawtipWallet();
787
795
  const fetches = startDoctorFetches(
788
796
  options.controlPort,
789
797
  options.proxyPort,
@@ -792,6 +800,8 @@ export async function renderDoctorDiagnosticsProgressively(options: DoctorRender
792
800
  options.sellerRegistryUrl,
793
801
  );
794
802
 
803
+ printDoctorClawtipWallet(clawtipWallet, writeLine);
804
+
795
805
  writeLine("\n--- Access Interfaces ---");
796
806
  writeLine("Checking local control plane and proxy endpoints...");
797
807
  const [healthResult, proxyModelsResult] = await Promise.all([
@@ -843,6 +853,7 @@ export async function renderDoctorDiagnosticsProgressively(options: DoctorRender
843
853
 
844
854
  return {
845
855
  access,
856
+ clawtipWallet,
846
857
  models,
847
858
  providers: options.providers,
848
859
  sellers: finalSellers,