* `clawrouter phone ...` — wallet-facing phone CLI. * * All operations POST to the local proxy at `/v1/phone/...` (or `/v1/voice/...`), * which transparently handles x402 payment via the wallet. The CLI just renders * upstream JSON in a human format.
( port: number, subcommand: "numbers" | "lookup" | "fraud", numbersAction: "list" | "buy" | "renew" | "release" | undefined, arg: string | undefined, areaCode: string | undefined, )
| 378 | * upstream JSON in a human format. |
| 379 | */ |
| 380 | async function cmdPhone( |
| 381 | port: number, |
| 382 | subcommand: "numbers" | "lookup" | "fraud", |
| 383 | numbersAction: "list" | "buy" | "renew" | "release" | undefined, |
| 384 | arg: string | undefined, |
| 385 | areaCode: string | undefined, |
| 386 | ): Promise<void> { |
| 387 | const base = `http://127.0.0.1:${port}`; |
| 388 | |
| 389 | async function postJson(path: string, body: unknown): Promise<unknown> { |
| 390 | const resp = await fetch(`${base}${path}`, { |
| 391 | method: "POST", |
| 392 | headers: { "content-type": "application/json" }, |
| 393 | body: JSON.stringify(body), |
| 394 | }); |
| 395 | const text = await resp.text(); |
| 396 | if (!resp.ok) { |
| 397 | if (resp.status === 402) { |
| 398 | throw new Error( |
| 399 | `Insufficient wallet balance (HTTP 402). Fund wallet via clawrouter wallet.\n${text}`, |
| 400 | ); |
| 401 | } |
| 402 | throw new Error(`HTTP ${resp.status}: ${text}`); |
| 403 | } |
| 404 | try { |
| 405 | return JSON.parse(text); |
| 406 | } catch { |
| 407 | return text; |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | function fmtExpiry(expiresAt: string): string { |
| 412 | const ms = new Date(expiresAt).getTime(); |
| 413 | if (!Number.isFinite(ms)) return expiresAt; |
| 414 | const days = Math.round((ms - Date.now()) / (1000 * 60 * 60 * 24)); |
| 415 | const dateStr = new Date(ms).toISOString().split("T")[0]; |
| 416 | if (days < 0) return `${dateStr} (expired ${-days}d ago)`; |
| 417 | if (days <= 2) return `${dateStr} (in ${days}d) ⚠ renew soon`; |
| 418 | return `${dateStr} (in ${days}d)`; |
| 419 | } |
| 420 | |
| 421 | try { |
| 422 | if (subcommand === "numbers") { |
| 423 | if (!numbersAction || numbersAction === "list") { |
| 424 | const result = (await postJson("/v1/phone/numbers/list", {})) as { |
| 425 | numbers?: Array<{ phone_number: string; expires_at: string; country?: string }>; |
| 426 | }; |
| 427 | const numbers = result.numbers ?? []; |
| 428 | if (numbers.length === 0) { |
| 429 | console.log("\nNo phone numbers owned by this wallet.\n"); |
| 430 | console.log("Buy one with: clawrouter phone numbers buy US [--area-code 415]\n"); |
| 431 | return; |
| 432 | } |
| 433 | console.log(`\nActive numbers (${numbers.length}):\n`); |
| 434 | const numWidth = Math.max(...numbers.map((n) => n.phone_number.length), 16); |
| 435 | for (const n of numbers) { |
| 436 | const country = (n.country ?? "??").padEnd(3); |
| 437 | console.log( |