@tokenbuddy/tokenbuddy 1.0.5 → 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.
- package/dist/src/buyer-store.d.ts +48 -1
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +144 -17
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +17 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +560 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +11 -5
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +574 -161
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
- package/dist/src/doctor-clawtip-wallet.js +54 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -0
- package/dist/src/doctor-diagnostics.d.ts +99 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -0
- package/dist/src/doctor-diagnostics.js +552 -0
- package/dist/src/doctor-diagnostics.js.map +1 -0
- package/dist/src/init-clawtip-activation.d.ts +48 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -0
- package/dist/src/init-clawtip-activation.js +395 -0
- package/dist/src/init-clawtip-activation.js.map +1 -0
- package/dist/src/init-payment-options.d.ts +56 -0
- package/dist/src/init-payment-options.d.ts.map +1 -0
- package/dist/src/init-payment-options.js +165 -0
- package/dist/src/init-payment-options.js.map +1 -0
- package/dist/src/provider-install.d.ts +37 -2
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +317 -67
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +79 -0
- package/dist/src/seller-catalog.d.ts.map +1 -0
- package/dist/src/seller-catalog.js +126 -0
- package/dist/src/seller-catalog.js.map +1 -0
- package/dist/src/tb-proxyd.js +13 -2
- package/dist/src/tb-proxyd.js.map +1 -1
- package/dist/src/terminal-image.d.ts +22 -0
- package/dist/src/terminal-image.d.ts.map +1 -0
- package/dist/src/terminal-image.js +135 -0
- package/dist/src/terminal-image.js.map +1 -0
- package/package.json +1 -1
- package/src/buyer-store.ts +253 -18
- package/src/cli.ts +709 -68
- package/src/daemon.ts +651 -167
- package/src/doctor-clawtip-wallet.ts +70 -0
- package/src/doctor-diagnostics.ts +861 -0
- package/src/init-clawtip-activation.ts +487 -0
- package/src/init-payment-options.ts +249 -0
- package/src/provider-install.ts +426 -76
- package/src/seller-catalog.ts +222 -0
- package/src/tb-proxyd.ts +14 -2
- package/src/terminal-image.ts +187 -0
- package/tests/e2e.test.ts +88 -5
- package/tests/tokenbuddy.test.ts +1362 -27
package/src/cli.ts
CHANGED
|
@@ -6,10 +6,54 @@ import * as os from "os";
|
|
|
6
6
|
import { execSync, spawn } from "child_process";
|
|
7
7
|
import Table from "cli-table3";
|
|
8
8
|
import { BuyerStore, PaymentConfig } from "./buyer-store.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
applyProviderInstall,
|
|
11
|
+
detectProviders,
|
|
12
|
+
getProviderModelSelectionKind,
|
|
13
|
+
getProviderProtocolPreference,
|
|
14
|
+
type ClaudeCodeModelMappingConfig,
|
|
15
|
+
type ProviderId,
|
|
16
|
+
type ProviderSelections,
|
|
17
|
+
type SingleModelProviderRuntimeConfig,
|
|
18
|
+
} from "./provider-install.js";
|
|
10
19
|
import { createModuleLogger } from "@tokenbuddy/logging";
|
|
11
20
|
import * as crypto from "crypto";
|
|
12
21
|
import { fileURLToPath } from "url";
|
|
22
|
+
import {
|
|
23
|
+
discoverSellerBackedModels,
|
|
24
|
+
filterCatalogByProtocol,
|
|
25
|
+
filterCatalogBySeller,
|
|
26
|
+
type ModelCatalogEntry,
|
|
27
|
+
type SellerCatalogResult,
|
|
28
|
+
type SellerRoutingPreference,
|
|
29
|
+
} from "./seller-catalog.js";
|
|
30
|
+
import {
|
|
31
|
+
collectDoctorDiagnostics,
|
|
32
|
+
collectDoctorModelsSummary,
|
|
33
|
+
printDoctorProviders,
|
|
34
|
+
printDoctorModelsSummary,
|
|
35
|
+
readDoctorProviders,
|
|
36
|
+
renderDoctorDiagnosticsProgressively,
|
|
37
|
+
} from "./doctor-diagnostics.js";
|
|
38
|
+
import {
|
|
39
|
+
buildInitSuccessMessage,
|
|
40
|
+
buildInitTerminalSelectionState,
|
|
41
|
+
buildInstalledTerminalMessage,
|
|
42
|
+
INIT_PAYMENT_OPTIONS,
|
|
43
|
+
inspectClawtipWalletReadiness,
|
|
44
|
+
inspectOpenClawWalletConfig,
|
|
45
|
+
noteInitComingSoonPayments,
|
|
46
|
+
OTHER_TERMINAL_OPTION,
|
|
47
|
+
type InitPaymentMethod,
|
|
48
|
+
validateInitTerminalSelection,
|
|
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";
|
|
13
57
|
|
|
14
58
|
// @ts-ignore
|
|
15
59
|
import qrcode from "qrcode-terminal";
|
|
@@ -53,6 +97,12 @@ interface ClawtipBootstrapResponse {
|
|
|
53
97
|
};
|
|
54
98
|
}
|
|
55
99
|
|
|
100
|
+
interface SelectOption {
|
|
101
|
+
value: string;
|
|
102
|
+
label: string;
|
|
103
|
+
hint?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
56
106
|
function isSupportedPaymentMethod(method: string): method is SupportedPaymentMethod {
|
|
57
107
|
return (SUPPORTED_PAYMENT_METHODS as readonly string[]).includes(method);
|
|
58
108
|
}
|
|
@@ -121,6 +171,20 @@ async function probeDaemonStatus(controlPort: number): Promise<DaemonProbeResult
|
|
|
121
171
|
}
|
|
122
172
|
}
|
|
123
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
|
+
|
|
124
188
|
async function waitForDaemonStatus(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult> {
|
|
125
189
|
const deadline = Date.now() + timeoutMs;
|
|
126
190
|
let latest: DaemonProbeResult = { running: false, error: "not checked" };
|
|
@@ -294,7 +358,7 @@ function printPaymentList(payments: PaymentConfig[], asJson: boolean): void {
|
|
|
294
358
|
console.log(table.toString());
|
|
295
359
|
}
|
|
296
360
|
|
|
297
|
-
async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBootstrapResponse> {
|
|
361
|
+
export async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBootstrapResponse> {
|
|
298
362
|
const response = await fetch(`${bootstrapUrl.replace(/\/+$/, "")}/payments/clawtip/bootstrap`, {
|
|
299
363
|
method: "POST",
|
|
300
364
|
headers: { "Content-Type": "application/json" },
|
|
@@ -307,9 +371,35 @@ async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBoots
|
|
|
307
371
|
if (!body.payment?.orderNo || !body.payment.indicator || !body.payment.resourceUrl) {
|
|
308
372
|
throw new Error("ClawTip bootstrap response missing payment order fields");
|
|
309
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);
|
|
310
384
|
return body;
|
|
311
385
|
}
|
|
312
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
|
+
|
|
313
403
|
function readProof(options: { proofFile?: string; requireProof?: boolean }): string | undefined {
|
|
314
404
|
const proofFile = options.proofFile || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
|
|
315
405
|
if (!proofFile) {
|
|
@@ -328,6 +418,251 @@ function readProof(options: { proofFile?: string; requireProof?: boolean }): str
|
|
|
328
418
|
return proof;
|
|
329
419
|
}
|
|
330
420
|
|
|
421
|
+
function sellerRegistryUrlForInit(): string {
|
|
422
|
+
return process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function stableModelChoices(models: ModelCatalogEntry[]): SelectOption[] {
|
|
426
|
+
const grouped = new Map<string, ModelCatalogEntry[]>();
|
|
427
|
+
for (const entry of models) {
|
|
428
|
+
const list = grouped.get(entry.id) || [];
|
|
429
|
+
list.push(entry);
|
|
430
|
+
grouped.set(entry.id, list);
|
|
431
|
+
}
|
|
432
|
+
return Array.from(grouped.entries()).map(([modelId, entries]) => {
|
|
433
|
+
const sellerIds = Array.from(new Set(entries.map((entry) => entry.sellerId)));
|
|
434
|
+
const protocols = Array.from(
|
|
435
|
+
new Set(entries.flatMap((entry) => entry.supportedProtocols)),
|
|
436
|
+
);
|
|
437
|
+
return {
|
|
438
|
+
value: modelId,
|
|
439
|
+
label: modelId,
|
|
440
|
+
hint: `${sellerIds.join(",")} · ${protocols.join(",") || "no-protocol"}`,
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function promptSellerRoutingPreference(catalog: SellerCatalogResult): Promise<SellerRoutingPreference> {
|
|
446
|
+
const healthySellers = catalog.sellers.filter((seller) => seller.status === "ok");
|
|
447
|
+
const mode = await p.select({
|
|
448
|
+
message: "Choose seller routing mode for tb-proxyd:",
|
|
449
|
+
options: [
|
|
450
|
+
{
|
|
451
|
+
value: "auto",
|
|
452
|
+
label: "Auto",
|
|
453
|
+
hint: "Automatically choose a compatible seller based on the requested model.",
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
value: "fixed",
|
|
457
|
+
label: "Fixed Seller",
|
|
458
|
+
hint: "Pin tb-proxyd to one seller and only use models from that seller.",
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
}) as SellerRoutingPreference["mode"] | symbol;
|
|
462
|
+
|
|
463
|
+
if (typeof mode !== "string") {
|
|
464
|
+
throw new Error("seller routing selection was cancelled");
|
|
465
|
+
}
|
|
466
|
+
if (mode === "auto") {
|
|
467
|
+
return { mode };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (healthySellers.length === 0) {
|
|
471
|
+
throw new Error("no healthy sellers available for fixed routing");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const sellerId = await p.select({
|
|
475
|
+
message: "Choose the seller to pin tb-proxyd to:",
|
|
476
|
+
options: healthySellers.map((seller) => ({
|
|
477
|
+
value: seller.id,
|
|
478
|
+
label: seller.name ? `${seller.name} (${seller.id})` : seller.id,
|
|
479
|
+
hint: [
|
|
480
|
+
seller.discountRatio != null ? `discount x${seller.discountRatio}` : null,
|
|
481
|
+
seller.modelCount != null ? `${seller.modelCount} models` : null,
|
|
482
|
+
seller.supportedProtocols?.length ? seller.supportedProtocols.join(",") : null,
|
|
483
|
+
seller.paymentMethods?.length ? seller.paymentMethods.join(",") : null,
|
|
484
|
+
]
|
|
485
|
+
.filter(Boolean)
|
|
486
|
+
.join(" · ") || seller.url,
|
|
487
|
+
})),
|
|
488
|
+
}) as string | symbol;
|
|
489
|
+
|
|
490
|
+
if (typeof sellerId !== "string") {
|
|
491
|
+
throw new Error("fixed seller selection was cancelled");
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
mode,
|
|
495
|
+
sellerId,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function promptSingleModelSelection(
|
|
500
|
+
providerId: ProviderId,
|
|
501
|
+
models: ModelCatalogEntry[],
|
|
502
|
+
sellerRouting: SellerRoutingPreference,
|
|
503
|
+
): Promise<SingleModelProviderRuntimeConfig> {
|
|
504
|
+
const protocolPreference = getProviderProtocolPreference(providerId);
|
|
505
|
+
const protocolFiltered = protocolPreference
|
|
506
|
+
? filterCatalogByProtocol(models, protocolPreference)
|
|
507
|
+
: models;
|
|
508
|
+
const choices = stableModelChoices(protocolFiltered);
|
|
509
|
+
if (choices.length === 0) {
|
|
510
|
+
throw new Error(`no compatible models available for ${providerId}`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const labelMap: Record<string, string> = {
|
|
514
|
+
opencode: "OpenCode",
|
|
515
|
+
codex: "Codex",
|
|
516
|
+
openclaw: "OpenClaw",
|
|
517
|
+
hermes: "Hermes",
|
|
518
|
+
"claude-desktop": "Claude Desktop",
|
|
519
|
+
"claude-code": "Claude Code",
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const selectedModel = await p.select({
|
|
523
|
+
message: `Choose the default model for ${labelMap[providerId] || providerId}:`,
|
|
524
|
+
options: choices,
|
|
525
|
+
}) as string | symbol;
|
|
526
|
+
|
|
527
|
+
if (typeof selectedModel !== "string") {
|
|
528
|
+
throw new Error(`default model selection was cancelled for ${providerId}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const selectedEntry = protocolFiltered.find((entry) => entry.id === selectedModel);
|
|
532
|
+
return {
|
|
533
|
+
selectionKind: "single-model",
|
|
534
|
+
protocolPreference,
|
|
535
|
+
defaultModel: selectedModel,
|
|
536
|
+
sellerId: sellerRouting.mode === "fixed" ? selectedEntry?.sellerId : undefined,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function defaultClaudeDisplayName(modelId: string): string {
|
|
541
|
+
return modelId.trim();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function makeClaudeRoleMapping(modelId: string): ClaudeCodeModelMappingConfig {
|
|
545
|
+
const displayName = defaultClaudeDisplayName(modelId);
|
|
546
|
+
return {
|
|
547
|
+
selectionKind: "claude-role-mapping",
|
|
548
|
+
protocolPreference: "messages",
|
|
549
|
+
fallbackModel: modelId,
|
|
550
|
+
roles: {
|
|
551
|
+
sonnet: {
|
|
552
|
+
upstreamModel: modelId,
|
|
553
|
+
displayName,
|
|
554
|
+
declareOneM: true,
|
|
555
|
+
},
|
|
556
|
+
opus: {
|
|
557
|
+
upstreamModel: modelId,
|
|
558
|
+
displayName,
|
|
559
|
+
declareOneM: true,
|
|
560
|
+
},
|
|
561
|
+
haiku: {
|
|
562
|
+
upstreamModel: modelId,
|
|
563
|
+
displayName,
|
|
564
|
+
declareOneM: false,
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function promptClaudeCodeModelSelection(
|
|
571
|
+
models: ModelCatalogEntry[],
|
|
572
|
+
): Promise<ClaudeCodeModelMappingConfig> {
|
|
573
|
+
const protocolFiltered = filterCatalogByProtocol(models, "messages");
|
|
574
|
+
const choices = stableModelChoices(protocolFiltered);
|
|
575
|
+
if (choices.length === 0) {
|
|
576
|
+
throw new Error("no compatible message models available for Claude Code");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const sonnetModel = await p.select({
|
|
580
|
+
message: "Choose the default Sonnet model for Claude Code:",
|
|
581
|
+
options: choices,
|
|
582
|
+
}) as string | symbol;
|
|
583
|
+
|
|
584
|
+
if (typeof sonnetModel !== "string") {
|
|
585
|
+
throw new Error("Claude Code model selection was cancelled");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const mirrorAllRoles = await p.confirm({
|
|
589
|
+
message: "Use the same model for Opus and Haiku as well?",
|
|
590
|
+
initialValue: true,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (typeof mirrorAllRoles !== "boolean") {
|
|
594
|
+
throw new Error("Claude Code role mapping confirmation was cancelled");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (mirrorAllRoles) {
|
|
598
|
+
return makeClaudeRoleMapping(sonnetModel);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const opusModel = await p.select({
|
|
602
|
+
message: "Choose the default Opus model for Claude Code:",
|
|
603
|
+
options: choices,
|
|
604
|
+
}) as string | symbol;
|
|
605
|
+
if (typeof opusModel !== "string") {
|
|
606
|
+
throw new Error("Claude Code Opus model selection was cancelled");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const haikuModel = await p.select({
|
|
610
|
+
message: "Choose the default Haiku model for Claude Code:",
|
|
611
|
+
options: choices,
|
|
612
|
+
}) as string | symbol;
|
|
613
|
+
if (typeof haikuModel !== "string") {
|
|
614
|
+
throw new Error("Claude Code Haiku model selection was cancelled");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
selectionKind: "claude-role-mapping",
|
|
619
|
+
protocolPreference: "messages",
|
|
620
|
+
fallbackModel: sonnetModel,
|
|
621
|
+
roles: {
|
|
622
|
+
sonnet: {
|
|
623
|
+
upstreamModel: sonnetModel,
|
|
624
|
+
displayName: defaultClaudeDisplayName(sonnetModel),
|
|
625
|
+
declareOneM: true,
|
|
626
|
+
},
|
|
627
|
+
opus: {
|
|
628
|
+
upstreamModel: opusModel,
|
|
629
|
+
displayName: defaultClaudeDisplayName(opusModel),
|
|
630
|
+
declareOneM: true,
|
|
631
|
+
},
|
|
632
|
+
haiku: {
|
|
633
|
+
upstreamModel: haikuModel,
|
|
634
|
+
displayName: defaultClaudeDisplayName(haikuModel),
|
|
635
|
+
declareOneM: false,
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function promptProviderSelections(
|
|
642
|
+
providerIds: ProviderId[],
|
|
643
|
+
catalog: SellerCatalogResult,
|
|
644
|
+
sellerRouting: SellerRoutingPreference,
|
|
645
|
+
): Promise<ProviderSelections> {
|
|
646
|
+
const baseModels = sellerRouting.mode === "fixed"
|
|
647
|
+
? filterCatalogBySeller(catalog.models, sellerRouting.sellerId)
|
|
648
|
+
: catalog.models;
|
|
649
|
+
|
|
650
|
+
const selections: ProviderSelections = {};
|
|
651
|
+
for (const providerId of providerIds) {
|
|
652
|
+
const selectionKind = getProviderModelSelectionKind(providerId);
|
|
653
|
+
if (selectionKind === "claude-role-mapping") {
|
|
654
|
+
selections[providerId] = await promptClaudeCodeModelSelection(baseModels);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
selections[providerId] = await promptSingleModelSelection(
|
|
658
|
+
providerId,
|
|
659
|
+
baseModels,
|
|
660
|
+
sellerRouting,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return selections;
|
|
664
|
+
}
|
|
665
|
+
|
|
331
666
|
export function buildCli(): Command {
|
|
332
667
|
const program = new Command();
|
|
333
668
|
program
|
|
@@ -352,7 +687,6 @@ export function buildCli(): Command {
|
|
|
352
687
|
const plistPath = process.platform === "darwin"
|
|
353
688
|
? path.join(os.homedir(), "Library", "LaunchAgents", "com.tokenbuddy.proxyd.plist")
|
|
354
689
|
: undefined;
|
|
355
|
-
const candidates = detectProviders();
|
|
356
690
|
let probe = await probeDaemonStatus(controlPort);
|
|
357
691
|
let repair: DaemonRepairResult = { attempted: false, fixed: false };
|
|
358
692
|
if (!probe.running && options.fix) {
|
|
@@ -363,11 +697,23 @@ export function buildCli(): Command {
|
|
|
363
697
|
const daemonInfo = probe.status;
|
|
364
698
|
const daemonRunning = probe.running;
|
|
365
699
|
const daemonError = probe.error;
|
|
700
|
+
const daemonStatus = daemonInfo && typeof daemonInfo === "object"
|
|
701
|
+
? daemonInfo as { selectionMode?: string; sellerRoutingMode?: string; selectedSellerId?: string; sellerRegistryUrl?: string }
|
|
702
|
+
: undefined;
|
|
703
|
+
const providers = readDoctorProviders();
|
|
366
704
|
if (options.fix && repair.attempted && !repair.fixed) {
|
|
367
705
|
process.exitCode = 1;
|
|
368
706
|
}
|
|
369
707
|
|
|
370
708
|
if (options.json) {
|
|
709
|
+
const diagnostics = await collectDoctorDiagnostics({
|
|
710
|
+
controlPort,
|
|
711
|
+
proxyPort,
|
|
712
|
+
daemonRunning,
|
|
713
|
+
daemonError,
|
|
714
|
+
providers,
|
|
715
|
+
sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
|
|
716
|
+
});
|
|
371
717
|
console.log(JSON.stringify({
|
|
372
718
|
daemon: {
|
|
373
719
|
running: daemonRunning,
|
|
@@ -387,7 +733,7 @@ export function buildCli(): Command {
|
|
|
387
733
|
plistPath,
|
|
388
734
|
plistExists: plistPath ? fs.existsSync(plistPath) : false
|
|
389
735
|
},
|
|
390
|
-
|
|
736
|
+
...diagnostics,
|
|
391
737
|
}, null, 2));
|
|
392
738
|
return;
|
|
393
739
|
}
|
|
@@ -422,12 +768,29 @@ export function buildCli(): Command {
|
|
|
422
768
|
}
|
|
423
769
|
}
|
|
424
770
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
771
|
+
if (daemonStatus) {
|
|
772
|
+
console.log(` Control Plane URL: ${controlUrl}`);
|
|
773
|
+
console.log(` Proxy Plane URL: http://127.0.0.1:${proxyPort}`);
|
|
774
|
+
if (daemonStatus.sellerRoutingMode || daemonStatus.selectionMode) {
|
|
775
|
+
console.log(` Routing Mode: ${daemonStatus.sellerRoutingMode || daemonStatus.selectionMode}`);
|
|
776
|
+
}
|
|
777
|
+
if (daemonStatus.selectedSellerId) {
|
|
778
|
+
console.log(` Selected Seller: ${daemonStatus.selectedSellerId}`);
|
|
779
|
+
}
|
|
780
|
+
if (daemonStatus.sellerRegistryUrl) {
|
|
781
|
+
console.log(` Registry URL: ${daemonStatus.sellerRegistryUrl}`);
|
|
782
|
+
}
|
|
430
783
|
}
|
|
784
|
+
|
|
785
|
+
printDoctorProviders(providers);
|
|
786
|
+
await renderDoctorDiagnosticsProgressively({
|
|
787
|
+
controlPort,
|
|
788
|
+
proxyPort,
|
|
789
|
+
daemonRunning,
|
|
790
|
+
daemonError,
|
|
791
|
+
sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
|
|
792
|
+
providers,
|
|
793
|
+
});
|
|
431
794
|
});
|
|
432
795
|
|
|
433
796
|
// 2. tb payment
|
|
@@ -555,22 +918,32 @@ export function buildCli(): Command {
|
|
|
555
918
|
.option("--json", "Output model list as JSON")
|
|
556
919
|
.action(async (options: { json?: boolean }) => {
|
|
557
920
|
try {
|
|
921
|
+
const controlPort = configuredControlPort();
|
|
922
|
+
const proxyPort = configuredProxyPort();
|
|
923
|
+
const status = await probeDaemonStatus(controlPort);
|
|
924
|
+
const daemonInfo = status.status && typeof status.status === "object"
|
|
925
|
+
? status.status as { sellerRegistryUrl?: string }
|
|
926
|
+
: undefined;
|
|
927
|
+
const models = await collectDoctorModelsSummary({
|
|
928
|
+
controlPort,
|
|
929
|
+
proxyPort,
|
|
930
|
+
daemonRunning: status.running,
|
|
931
|
+
daemonError: status.error,
|
|
932
|
+
sellerRegistryUrl: daemonInfo?.sellerRegistryUrl,
|
|
933
|
+
});
|
|
934
|
+
|
|
558
935
|
if (options.json) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
throw new Error(body || `HTTP ${response.status}`);
|
|
936
|
+
console.log(JSON.stringify(models, null, 2));
|
|
937
|
+
if (!models.available) {
|
|
938
|
+
process.exitCode = 1;
|
|
563
939
|
}
|
|
564
|
-
JSON.parse(body);
|
|
565
|
-
console.log(body);
|
|
566
940
|
return;
|
|
567
941
|
}
|
|
568
942
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
console.log(table.toString());
|
|
943
|
+
printDoctorModelsSummary(models);
|
|
944
|
+
if (!models.available) {
|
|
945
|
+
process.exitCode = 1;
|
|
946
|
+
}
|
|
574
947
|
} catch (err: any) {
|
|
575
948
|
console.error("Error connecting to local proxy:", err.message);
|
|
576
949
|
process.exitCode = 1;
|
|
@@ -583,88 +956,354 @@ export function buildCli(): Command {
|
|
|
583
956
|
.description("Launch step-by-step interactive setup wizard")
|
|
584
957
|
.action(async () => {
|
|
585
958
|
p.intro("🚀 Welcome to TokenBuddy Interactive Wizard!");
|
|
959
|
+
const setupSummaryLines: string[] = [];
|
|
586
960
|
|
|
587
961
|
// Step 1: Scan coding terminals
|
|
588
962
|
const spinner = p.spinner();
|
|
589
963
|
spinner.start("Scanning local system for programming terminals...");
|
|
590
964
|
const candidates = detectProviders();
|
|
591
|
-
const
|
|
965
|
+
const terminalSelection = buildInitTerminalSelectionState(candidates);
|
|
592
966
|
spinner.stop("Scan completed.");
|
|
593
967
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
label: c.name,
|
|
600
|
-
hint: c.configPath
|
|
601
|
-
}));
|
|
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
|
+
}
|
|
602
973
|
|
|
974
|
+
if (terminalSelection.options.length === 1 && terminalSelection.options[0].value === OTHER_TERMINAL_OPTION.value) {
|
|
975
|
+
p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenCode, OpenClaw or Hermes first.");
|
|
976
|
+
} else {
|
|
603
977
|
const selected = await p.multiselect({
|
|
604
978
|
message: "Select programming terminals to route via TokenBuddy (use Space to select, Enter to confirm):",
|
|
605
|
-
options:
|
|
979
|
+
options: terminalSelection.options,
|
|
606
980
|
required: false
|
|
607
981
|
}) as string[];
|
|
608
982
|
|
|
609
|
-
|
|
610
|
-
|
|
983
|
+
const selectionError = validateInitTerminalSelection(selected);
|
|
984
|
+
if (selectionError) {
|
|
985
|
+
throw new Error(selectionError);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const selectedActionable = selected.filter((value) => !value.endsWith(":installed"));
|
|
989
|
+
const selectedOther = selectedActionable.includes(OTHER_TERMINAL_OPTION.value);
|
|
990
|
+
const selectedProviders = selectedActionable.filter((value) => value !== OTHER_TERMINAL_OPTION.value);
|
|
991
|
+
|
|
992
|
+
if (selectedOther) {
|
|
993
|
+
p.note(
|
|
994
|
+
[
|
|
995
|
+
"✅ OpenAI-compatible Proxy",
|
|
996
|
+
" URL: http://127.0.0.1:17821/v1",
|
|
997
|
+
" Probe: http://127.0.0.1:17821/v1/models",
|
|
998
|
+
" Token: TOKENBUDDY_PROXY",
|
|
999
|
+
"",
|
|
1000
|
+
"✅ Anthropic-compatible Proxy",
|
|
1001
|
+
" URL: http://127.0.0.1:17821"
|
|
1002
|
+
].join("\n"),
|
|
1003
|
+
"TokenBuddy Proxy Interfaces"
|
|
1004
|
+
);
|
|
1005
|
+
setupSummaryLines.push("Manual terminal setup selected via Other.");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (selectedProviders.length > 0) {
|
|
1009
|
+
spinner.start("Fetching seller-backed model catalog...");
|
|
611
1010
|
const proxyUrl = `http://127.0.0.1:${PROXY_PORT}`;
|
|
612
|
-
const
|
|
1011
|
+
const registryUrl = sellerRegistryUrlForInit();
|
|
1012
|
+
let catalog: SellerCatalogResult;
|
|
1013
|
+
try {
|
|
1014
|
+
catalog = await discoverSellerBackedModels(registryUrl);
|
|
1015
|
+
} catch (error: unknown) {
|
|
1016
|
+
spinner.stop("Failed to fetch seller-backed models.");
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
spinner.stop("Seller-backed model catalog loaded.");
|
|
1020
|
+
|
|
1021
|
+
const providerIds = selectedProviders.filter((provider): provider is ProviderId => {
|
|
1022
|
+
return [
|
|
1023
|
+
"codex",
|
|
1024
|
+
"claude-code",
|
|
1025
|
+
"claude-desktop",
|
|
1026
|
+
"openclaw",
|
|
1027
|
+
"opencode",
|
|
1028
|
+
"hermes",
|
|
1029
|
+
].includes(provider);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
const sellerRouting = await promptSellerRoutingPreference(catalog);
|
|
1033
|
+
const providerSelections = await promptProviderSelections(
|
|
1034
|
+
providerIds,
|
|
1035
|
+
catalog,
|
|
1036
|
+
sellerRouting,
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
spinner.start("Configuring proxy routing in selected terminals...");
|
|
613
1040
|
const store = openBuyerStore();
|
|
614
1041
|
try {
|
|
615
1042
|
applyProviderInstall({
|
|
616
|
-
providers:
|
|
1043
|
+
providers: providerIds,
|
|
617
1044
|
proxyUrl,
|
|
618
|
-
|
|
1045
|
+
providerSelections,
|
|
1046
|
+
sellerRouting,
|
|
619
1047
|
}, store);
|
|
620
1048
|
} finally {
|
|
621
1049
|
store.close();
|
|
622
1050
|
}
|
|
623
1051
|
spinner.stop("Selected terminals successfully configured.");
|
|
1052
|
+
setupSummaryLines.push(`${providerIds.length} programming terminal${providerIds.length === 1 ? "" : "s"} configured for TokenBuddy.`);
|
|
624
1053
|
}
|
|
625
1054
|
}
|
|
626
1055
|
|
|
627
1056
|
// Step 2: Choose Payment Method & Scan QR Activation
|
|
1057
|
+
noteInitComingSoonPayments();
|
|
628
1058
|
const payMethod = await p.select({
|
|
629
1059
|
message: "Choose your primary payment method for LLM token purchases:",
|
|
630
|
-
options:
|
|
631
|
-
|
|
632
|
-
{ value: "mock", label: "Mock Wallet (For local development and tests)" }
|
|
633
|
-
]
|
|
634
|
-
}) as string;
|
|
1060
|
+
options: INIT_PAYMENT_OPTIONS
|
|
1061
|
+
}) as InitPaymentMethod;
|
|
635
1062
|
|
|
636
1063
|
if (payMethod === "clawtip") {
|
|
637
|
-
|
|
1064
|
+
const store = openBuyerStore();
|
|
638
1065
|
try {
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1066
|
+
let walletConfig = inspectOpenClawWalletConfig();
|
|
1067
|
+
const clawtipReadiness = inspectClawtipWalletReadiness(store.getPayment("clawtip"), walletConfig);
|
|
1068
|
+
const existingClawtip = clawtipReadiness.reusableBinding;
|
|
1069
|
+
if (existingClawtip) {
|
|
1070
|
+
store.savePayment({
|
|
1071
|
+
method: "clawtip",
|
|
1072
|
+
enabled: true,
|
|
1073
|
+
isDefault: true,
|
|
1074
|
+
config: existingClawtip.config
|
|
1075
|
+
});
|
|
1076
|
+
const details = [
|
|
1077
|
+
existingClawtip.orderNo ? `Order: ${existingClawtip.orderNo}` : undefined,
|
|
1078
|
+
existingClawtip.resourceUrl ? `ResourceUrl: ${existingClawtip.resourceUrl}` : undefined
|
|
1079
|
+
].filter(Boolean).join("\n");
|
|
1080
|
+
logger.info("payment.channel.reused", "clawtip payment channel already configured locally", {
|
|
1081
|
+
method: "clawtip",
|
|
1082
|
+
hasOrderNo: Boolean(existingClawtip.orderNo),
|
|
1083
|
+
hasResourceUrl: Boolean(existingClawtip.resourceUrl)
|
|
1084
|
+
});
|
|
1085
|
+
p.note(
|
|
1086
|
+
details
|
|
1087
|
+
? `ClawTip wallet is already configured locally.\n${details}`
|
|
1088
|
+
: "ClawTip wallet is already configured locally.",
|
|
1089
|
+
"ClawTip"
|
|
1090
|
+
);
|
|
1091
|
+
setupSummaryLines.push("ClawTip wallet already bound locally; activation skipped.");
|
|
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
|
+
|
|
1115
|
+
spinner.start("Requesting payment activation payload from public bootstrap registry...");
|
|
1116
|
+
const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
|
|
1117
|
+
const bootstrap = await fetchClawtipBootstrap(bootstrapUrl);
|
|
1118
|
+
spinner.stop("Bootstrap payload received.");
|
|
1119
|
+
|
|
1120
|
+
if (!bootstrap.payment?.orderNo || !bootstrap.payment?.indicator) {
|
|
1121
|
+
throw new Error("ClawTip bootstrap response missing orderNo or indicator.");
|
|
1122
|
+
}
|
|
1123
|
+
|
|
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
|
+
}
|
|
1211
|
+
|
|
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
|
+
}
|
|
1220
|
+
|
|
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
|
+
);
|
|
1233
|
+
}
|
|
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";
|
|
1246
|
+
|
|
1247
|
+
store.savePayment({
|
|
1248
|
+
method: "clawtip",
|
|
1249
|
+
enabled: true,
|
|
1250
|
+
isDefault: true,
|
|
1251
|
+
config: {
|
|
1252
|
+
bootstrapUrl,
|
|
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
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
logger.info("payment.channel.added", "clawtip payment channel added during init", {
|
|
1269
|
+
method: "clawtip",
|
|
1270
|
+
orderNo: bootstrap.payment?.orderNo,
|
|
1271
|
+
payCredentialWritten: Boolean(payCredential),
|
|
1272
|
+
activationCompletedBy
|
|
1273
|
+
});
|
|
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
|
+
}
|
|
661
1300
|
}
|
|
662
|
-
spinner.stop("JD收银台 confirmed payment. ClawTip wallet is active! 🚀");
|
|
663
1301
|
} catch (err: any) {
|
|
664
|
-
spinner.stop(`Failed to
|
|
1302
|
+
spinner.stop(`Failed to finish ClawTip setup: ${err.message}`);
|
|
1303
|
+
setupSummaryLines.push("ClawTip activation requires follow-up because the bootstrap step did not complete.");
|
|
1304
|
+
} finally {
|
|
1305
|
+
store.close();
|
|
665
1306
|
}
|
|
666
|
-
} else {
|
|
667
|
-
p.note("Mock Wallet selected. No real payments will be made. Status is active.");
|
|
668
1307
|
}
|
|
669
1308
|
|
|
670
1309
|
// Step 3: Install Launchd Daemon Service
|
|
@@ -716,6 +1355,7 @@ export function buildCli(): Command {
|
|
|
716
1355
|
} catch {}
|
|
717
1356
|
execSync(`launchctl load ${plistPath}`);
|
|
718
1357
|
spinner.stop("LaunchAgent daemon successfully registered and started! 🚀");
|
|
1358
|
+
setupSummaryLines.push("Background tb-proxyd launchd service installed.");
|
|
719
1359
|
} catch (err: any) {
|
|
720
1360
|
spinner.stop(`Failed to write launchd plist: ${err.message}`);
|
|
721
1361
|
}
|
|
@@ -723,9 +1363,10 @@ export function buildCli(): Command {
|
|
|
723
1363
|
} else {
|
|
724
1364
|
// Run background dettached child process in linux/windows
|
|
725
1365
|
p.note("System daemon is active. Process runs in dettached background.");
|
|
1366
|
+
setupSummaryLines.push("Background daemon mode is available on this system.");
|
|
726
1367
|
}
|
|
727
1368
|
|
|
728
|
-
p.outro(
|
|
1369
|
+
p.outro(buildInitSuccessMessage(setupSummaryLines));
|
|
729
1370
|
});
|
|
730
1371
|
|
|
731
1372
|
return program;
|