@tangle-network/tcloud 0.2.0 → 0.3.0

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/cli.cjs CHANGED
@@ -287,8 +287,10 @@ var DEFAULT_RETRY = {
287
287
  retryableStatuses: [429, 500, 502, 503, 504]
288
288
  };
289
289
  var DEFAULT_TIMEOUT_MS = 6e4;
290
+ var DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
290
291
  var TCloudClient = class _TCloudClient {
291
292
  baseURL;
293
+ platformURL;
292
294
  apiKey;
293
295
  model;
294
296
  headers;
@@ -305,6 +307,7 @@ var TCloudClient = class _TCloudClient {
305
307
  static OPERATORS_TTL_MS = 5 * 60 * 1e3;
306
308
  constructor(config = {}) {
307
309
  this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
310
+ this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
308
311
  this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
309
312
  this.model = config.model || "gpt-4o-mini";
310
313
  this.privacy = config.privacy;
@@ -493,10 +496,11 @@ var TCloudClient = class _TCloudClient {
493
496
  return res;
494
497
  }
495
498
  /**
496
- * Prepare headers for chat requests — operator routing + SpendAuth.
499
+ * Prepare headers for chat requests — operator routing + SpendAuth +
500
+ * bridge short-circuit headers when `options.bridge` is set.
497
501
  * Shared between chat() and chatStream() to eliminate duplication.
498
502
  */
499
- async _prepareChatRequest(model) {
503
+ async _prepareChatRequest(model, bridge) {
500
504
  const headers = { ...this.headers };
501
505
  if (this.spendAuthFn) {
502
506
  const auth2 = await this.spendAuthFn();
@@ -513,12 +517,28 @@ var TCloudClient = class _TCloudClient {
513
517
  delete headers["Authorization"];
514
518
  }
515
519
  }
520
+ if (bridge) {
521
+ headers["X-Bridge-Unlock"] = bridge.unlock;
522
+ if (bridge.resume) headers["X-Resume"] = bridge.resume;
523
+ if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
524
+ if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
525
+ }
516
526
  return { headers, baseURL };
517
527
  }
528
+ /**
529
+ * Resolve the effective model string. When a bridge is set, rewrite to
530
+ * `bridge/<harness>/<model>` (or `bridge/<harness>` if no model).
531
+ */
532
+ _effectiveModel(options) {
533
+ if (options.bridge) {
534
+ return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
535
+ }
536
+ return options.model || this.model;
537
+ }
518
538
  /** Build the chat completions request body */
519
539
  _chatBody(options, stream) {
520
540
  return JSON.stringify({
521
- model: options.model || this.model,
541
+ model: this._effectiveModel(options),
522
542
  messages: options.messages,
523
543
  temperature: options.temperature,
524
544
  max_tokens: options.maxTokens,
@@ -530,13 +550,17 @@ var TCloudClient = class _TCloudClient {
530
550
  response_format: options.responseFormat,
531
551
  tools: options.tools,
532
552
  tool_choice: options.toolChoice,
553
+ ...options.gateway ? { gateway: options.gateway } : {},
533
554
  ...options.providerOptions
534
555
  });
535
556
  }
536
557
  /** Chat completion (non-streaming) */
537
558
  async chat(options) {
538
559
  this.checkLimits();
539
- const { headers, baseURL } = await this._prepareChatRequest(options.model || this.model);
560
+ const { headers, baseURL } = await this._prepareChatRequest(
561
+ this._effectiveModel(options),
562
+ options.bridge
563
+ );
540
564
  const res = await this._doFetch(`${baseURL}/chat/completions`, {
541
565
  method: "POST",
542
566
  headers,
@@ -550,7 +574,10 @@ var TCloudClient = class _TCloudClient {
550
574
  async *chatStream(options) {
551
575
  this.checkLimits();
552
576
  this._requestCount++;
553
- const { headers, baseURL } = await this._prepareChatRequest(options.model || this.model);
577
+ const { headers, baseURL } = await this._prepareChatRequest(
578
+ this._effectiveModel(options),
579
+ options.bridge
580
+ );
554
581
  const res = await this._doFetch(`${baseURL}/chat/completions`, {
555
582
  method: "POST",
556
583
  headers,
@@ -579,6 +606,24 @@ var TCloudClient = class _TCloudClient {
579
606
  }
580
607
  }
581
608
  }
609
+ /**
610
+ * Bridge — scoped helper for a subscription-backed CLI harness behind
611
+ * the Tangle Router's cli-bridge. Returns a mini-client bound to
612
+ * (harness, unlock, resume) so you don't thread those through every
613
+ * call.
614
+ *
615
+ * ```ts
616
+ * const kimi = tcloud.bridge({ harness: 'kimi', model: 'kimi-for-coding', unlock: UNLOCK, resume: 'pr-42' })
617
+ * await kimi.ask('review this diff…')
618
+ * for await (const chunk of kimi.stream('continue…')) process.stdout.write(chunk)
619
+ * ```
620
+ *
621
+ * Sessions persist across process restarts — use the same `resume` id
622
+ * to land on the same CLI conversation (context intact, no replay tax).
623
+ */
624
+ bridge(cfg) {
625
+ return new BridgeSession(this, cfg);
626
+ }
582
627
  /** Convenience: send a single message and get the text response */
583
628
  async ask(message, modelOrOptions) {
584
629
  const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
@@ -617,43 +662,89 @@ var TCloudClient = class _TCloudClient {
617
662
  const apiRoot = this.baseURL.replace(/\/v1$/, "");
618
663
  return this._fetch(`${apiRoot}/api/operators`);
619
664
  }
665
+ // ── Billing (via id.tangle.tools) ──
620
666
  /** Get credit balance */
621
667
  async credits() {
622
- const apiRoot = this.baseURL.replace(/\/v1$/, "");
623
- return this._fetch(`${apiRoot}/api/billing`);
668
+ const { data } = await this._fetch(`${this.platformURL}/v1/billing/balance`);
669
+ return data;
624
670
  }
625
- /** Add credits */
671
+ /** Add credits via Stripe checkout. Returns the checkout URL. */
626
672
  async addCredits(amount) {
627
- const apiRoot = this.baseURL.replace(/\/v1$/, "");
628
- return this._fetch(`${apiRoot}/api/billing`, {
673
+ const { data } = await this._fetch(`${this.platformURL}/v1/billing/topup`, {
629
674
  method: "POST",
630
675
  body: JSON.stringify({ amount })
631
676
  });
677
+ return data;
632
678
  }
633
- /** Create a new API key */
634
- async createKey(name) {
635
- const apiRoot = this.baseURL.replace(/\/v1$/, "");
636
- return this._fetch(`${apiRoot}/api/keys`, {
679
+ /** Get transaction history */
680
+ async transactions(limit = 50) {
681
+ const { data } = await this._fetch(`${this.platformURL}/v1/billing/transactions?limit=${limit}`);
682
+ return data;
683
+ }
684
+ // ── API Keys (via id.tangle.tools) ──
685
+ /**
686
+ * Create a new API key.
687
+ * When called with an API key (not session), the new key is automatically
688
+ * a child of the calling key — enabling hierarchical key delegation.
689
+ *
690
+ * Pass `parentKeyId` explicitly to create a child of a specific key.
691
+ * Child keys inherit the parent's product scope, allowedModels, and rpmLimit
692
+ * if not specified. Budget cannot exceed the parent's remaining budget.
693
+ */
694
+ async createKey(opts) {
695
+ const { data } = await this._fetch(`${this.platformURL}/v1/keys`, {
637
696
  method: "POST",
638
- body: JSON.stringify({ name })
697
+ body: JSON.stringify(opts)
639
698
  });
699
+ return data;
640
700
  }
641
- /** List API keys */
642
- async keys() {
643
- const apiRoot = this.baseURL.replace(/\/v1$/, "");
644
- return this._fetch(`${apiRoot}/api/keys`);
701
+ /** Get a single API key by ID */
702
+ async getKey(id) {
703
+ const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`);
704
+ return data;
645
705
  }
646
- /** Revoke an API key */
706
+ /**
707
+ * List API keys.
708
+ * Pass `children: true` to list child keys of the calling API key.
709
+ */
710
+ async keys(opts) {
711
+ const q = opts?.children ? "?children=true" : "";
712
+ const { data } = await this._fetch(`${this.platformURL}/v1/keys${q}`);
713
+ return data;
714
+ }
715
+ /**
716
+ * Update an API key's limits.
717
+ * Can adjust budget, allowedModels, rpmLimit, expiresAt, and name.
718
+ */
719
+ async updateKey(id, updates) {
720
+ const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`, {
721
+ method: "PATCH",
722
+ body: JSON.stringify(updates)
723
+ });
724
+ return data;
725
+ }
726
+ /** Revoke an API key. If the key has children, they are also revoked recursively. */
647
727
  async revokeKey(id) {
648
- const apiRoot = this.baseURL.replace(/\/v1$/, "");
649
- const res = await proxiedFetch(this.privacy, `${apiRoot}/api/keys/${id}`, {
650
- method: "DELETE",
651
- headers: this.headers
652
- }, false);
653
- if (!res.ok) {
654
- const err = await res.json().catch(() => ({ error: res.statusText }));
655
- throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
656
- }
728
+ await this._fetch(`${this.platformURL}/v1/keys/${id}`, { method: "DELETE" });
729
+ }
730
+ /** Rotate an API key — creates new key with same config, revokes old */
731
+ async rotateKey(id) {
732
+ const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}/rotate`, { method: "POST" });
733
+ return data;
734
+ }
735
+ // ── Projects (via id.tangle.tools) ──
736
+ /** Create a project for usage attribution */
737
+ async createProject(name, product) {
738
+ const { data } = await this._fetch(`${this.platformURL}/v1/projects`, {
739
+ method: "POST",
740
+ body: JSON.stringify({ name, product })
741
+ });
742
+ return data;
743
+ }
744
+ /** List projects */
745
+ async projects() {
746
+ const { data } = await this._fetch(`${this.platformURL}/v1/projects`);
747
+ return data;
657
748
  }
658
749
  /** Generate embeddings */
659
750
  async embeddings(options) {
@@ -964,6 +1055,61 @@ var TCloudClient = class _TCloudClient {
964
1055
  };
965
1056
  });
966
1057
  }
1058
+ // ── Eval ──────────────────────────────────────────────────────────────
1059
+ get _apiRoot() {
1060
+ return this.baseURL.replace(/\/v1$/, "");
1061
+ }
1062
+ async eval(opts) {
1063
+ return this._request(`${this._apiRoot}/api/eval`, { method: "POST", body: JSON.stringify(opts) });
1064
+ }
1065
+ async createSuite(opts) {
1066
+ return this._request(`${this._apiRoot}/api/eval/suites`, { method: "POST", body: JSON.stringify(opts) });
1067
+ }
1068
+ async listSuites() {
1069
+ return this._fetch(`${this._apiRoot}/api/eval/suites`);
1070
+ }
1071
+ async runSuite(suiteId, opts) {
1072
+ return this._request(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`, { method: "POST", body: JSON.stringify(opts || {}) });
1073
+ }
1074
+ async listRuns(suiteId) {
1075
+ return this._fetch(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`);
1076
+ }
1077
+ async getRun(runId) {
1078
+ return this._fetch(`${this._apiRoot}/api/eval/runs/${runId}`);
1079
+ }
1080
+ async setBaseline(runId) {
1081
+ await this._request(`${this._apiRoot}/api/eval/runs/${runId}`, { method: "PATCH", body: JSON.stringify({ baseline: true }) });
1082
+ }
1083
+ // ── Sandbox ──────────────────────────────────────────────────────────
1084
+ async sandboxPricing(opts) {
1085
+ const p = new URLSearchParams();
1086
+ if (opts?.cpu) p.set("cpu", String(opts.cpu));
1087
+ if (opts?.ram) p.set("ram", String(opts.ram));
1088
+ if (opts?.disk) p.set("disk", String(opts.disk));
1089
+ return this._fetch(`${this._apiRoot}/api/sandbox/pricing?${p}`);
1090
+ }
1091
+ async sandboxStatus() {
1092
+ return this._fetch(`${this._apiRoot}/api/sandbox/link-key`);
1093
+ }
1094
+ async sandboxProvision() {
1095
+ return this._request(`${this._apiRoot}/api/sandbox/provision`, { method: "POST" });
1096
+ }
1097
+ async sandboxCreate(opts) {
1098
+ return this._request(`${this._apiRoot}/api/sandbox/sessions`, { method: "POST", body: JSON.stringify(opts) });
1099
+ }
1100
+ async sandboxList() {
1101
+ return this._fetch(`${this._apiRoot}/api/sandbox/sessions`);
1102
+ }
1103
+ async sandboxStats(sandboxId) {
1104
+ return this._fetch(`${this._apiRoot}/api/sandbox/stats/${sandboxId}`);
1105
+ }
1106
+ async sandboxDestroy(sessionId) {
1107
+ return this._request(`${this._apiRoot}/api/sandbox/sessions/${sessionId}`, { method: "DELETE" });
1108
+ }
1109
+ // ── User Info ────────────────────────────────────────────────────────
1110
+ async userInfo() {
1111
+ return this._fetch(`${this._apiRoot}/api/auth/userinfo`);
1112
+ }
967
1113
  };
968
1114
  var ALL_TIERS = [
969
1115
  { name: "cpu-only", cpu: 4, ramGb: 16, gpu: 0, tee: false },
@@ -974,6 +1120,59 @@ var ALL_TIERS = [
974
1120
  { name: "max-gpu", cpu: 64, ramGb: 256, gpu: 4, tee: false },
975
1121
  { name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
976
1122
  ];
1123
+ var BridgeSession = class _BridgeSession {
1124
+ constructor(client, cfg) {
1125
+ this.client = client;
1126
+ this.cfg = cfg;
1127
+ }
1128
+ /** Full chat completion (non-streaming). */
1129
+ async chat(options) {
1130
+ return this.client.chat({ ...options, bridge: this.cfg });
1131
+ }
1132
+ /** Stream OpenAI chat.completion.chunks. */
1133
+ chatStream(options) {
1134
+ return this.client.chatStream({ ...options, bridge: this.cfg });
1135
+ }
1136
+ /** One-shot: send a string, get the assistant text. */
1137
+ async ask(message, extra) {
1138
+ const completion = await this.chat({
1139
+ messages: [{ role: "user", content: message }],
1140
+ ...extra
1141
+ });
1142
+ return completion.choices[0]?.message?.content || "";
1143
+ }
1144
+ /** One-shot: send a string, stream text deltas. */
1145
+ async *stream(message, extra) {
1146
+ for await (const chunk of this.chatStream({
1147
+ messages: [{ role: "user", content: message }],
1148
+ ...extra
1149
+ })) {
1150
+ const content = chunk.choices?.[0]?.delta?.content;
1151
+ if (content) yield content;
1152
+ }
1153
+ }
1154
+ /** Turn-based: send full message history, get assistant text. */
1155
+ async turn(messages, extra) {
1156
+ const completion = await this.chat({ messages, ...extra });
1157
+ return completion.choices[0]?.message?.content || "";
1158
+ }
1159
+ /** Clone with a new resume id — same harness, different logical conversation. */
1160
+ withResume(resume) {
1161
+ return new _BridgeSession(this.client, { ...this.cfg, resume });
1162
+ }
1163
+ /** Clone with a different model inside the same harness. */
1164
+ withModel(model) {
1165
+ return new _BridgeSession(this.client, { ...this.cfg, model });
1166
+ }
1167
+ /** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
1168
+ get model() {
1169
+ return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
1170
+ }
1171
+ /** The resume id currently bound to this session, if any. */
1172
+ get resume() {
1173
+ return this.cfg.resume;
1174
+ }
1175
+ };
977
1176
  function selectTiers(all, n) {
978
1177
  if (n >= all.length) return [...all];
979
1178
  if (n <= 1) return [all[0]];
@@ -1294,6 +1493,7 @@ var TCloud = class _TCloud extends TCloudClient {
1294
1493
  var fs = __toESM(require("fs"), 1);
1295
1494
  var path = __toESM(require("path"), 1);
1296
1495
  var readline = __toESM(require("readline"), 1);
1496
+ var import_child_process = require("child_process");
1297
1497
  var CONFIG_DIR = path.join(process.env.HOME || "~", ".tcloud");
1298
1498
  var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
1299
1499
  var WALLETS_FILE = path.join(CONFIG_DIR, "wallets.json");
@@ -1342,23 +1542,52 @@ program.command("config").description("View or update configuration").option("--
1342
1542
  console.log(JSON.stringify(c, null, 2));
1343
1543
  });
1344
1544
  var auth = program.command("auth").description("Authentication");
1345
- auth.command("login").description("Log in via browser (device flow)").action(async () => {
1545
+ function openBrowser(url) {
1546
+ if (process.env.NO_BROWSER || process.env.CI) return false;
1547
+ if (!process.stdout.isTTY) return false;
1548
+ try {
1549
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1550
+ const child = (0, import_child_process.spawn)(cmd, [url], { stdio: "ignore", detached: true });
1551
+ child.unref();
1552
+ return true;
1553
+ } catch {
1554
+ return false;
1555
+ }
1556
+ }
1557
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1558
+ async function runDeviceFlow(mode) {
1346
1559
  const config = loadConfig();
1560
+ const initRes = await fetch(`${config.apiUrl}/api/auth/device`, { method: "POST" });
1561
+ if (!initRes.ok) {
1562
+ console.error(`
1563
+ \u2717 Auth server returned ${initRes.status}.`);
1564
+ process.exit(1);
1565
+ }
1566
+ const d = await initRes.json();
1567
+ const urlWithCode = `${d.verification_url}?user_code=${encodeURIComponent(d.user_code)}`;
1568
+ const opened = openBrowser(urlWithCode);
1569
+ const header = mode === "signup" ? "Creating your Tangle account" : "Signing you in";
1570
+ console.log(`
1571
+ ${header}
1572
+ `);
1573
+ console.log(` ${opened ? "Browser opened:" : "Open this URL:"}`);
1574
+ console.log(` ${urlWithCode}
1575
+ `);
1576
+ console.log(` Verification code: ${d.user_code}
1577
+ `);
1578
+ const deadline = Date.now() + (d.expires_in || 600) * 1e3;
1579
+ const interval = Math.max(2, d.interval || 5) * 1e3;
1580
+ const spinnerEnabled = process.stdout.isTTY && !process.env.CI;
1581
+ let frame = 0;
1582
+ function tickSpinner() {
1583
+ if (!spinnerEnabled) return;
1584
+ const remaining = Math.max(0, Math.round((deadline - Date.now()) / 1e3));
1585
+ process.stdout.write(`\r ${SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length]} waiting for browser confirmation (${remaining}s remaining) `);
1586
+ }
1587
+ const spinnerTimer = spinnerEnabled ? setInterval(tickSpinner, 100) : null;
1347
1588
  try {
1348
- const res = await fetch(`${config.apiUrl}/api/auth/device`, { method: "POST" });
1349
- if (!res.ok) {
1350
- console.error("Auth server error");
1351
- process.exit(1);
1352
- }
1353
- const d = await res.json();
1354
- console.log(`
1355
- Open: ${d.verification_url}
1356
- Code: ${d.user_code}
1357
-
1358
- Waiting...`);
1359
- const deadline = Date.now() + (d.expires_in || 600) * 1e3;
1360
1589
  while (Date.now() < deadline) {
1361
- await new Promise((r2) => setTimeout(r2, (d.interval || 5) * 1e3));
1590
+ await new Promise((r2) => setTimeout(r2, interval));
1362
1591
  const r = await fetch(`${config.apiUrl}/api/auth/device/token`, {
1363
1592
  method: "POST",
1364
1593
  headers: { "Content-Type": "application/json" },
@@ -1368,31 +1597,112 @@ auth.command("login").description("Log in via browser (device flow)").action(asy
1368
1597
  if (t.access_token) {
1369
1598
  config.apiKey = t.access_token;
1370
1599
  saveConfig(config);
1371
- console.log("\n Authenticated!");
1600
+ if (spinnerTimer) {
1601
+ clearInterval(spinnerTimer);
1602
+ process.stdout.write("\r" + " ".repeat(80) + "\r");
1603
+ }
1604
+ console.log(` \u2713 Authenticated`);
1605
+ console.log(` \u2713 API key saved to ${CONFIG_FILE}
1606
+ `);
1607
+ console.log(` Try it:`);
1608
+ console.log(` tcloud whoami`);
1609
+ console.log(` tcloud chat "hello world"
1610
+ `);
1372
1611
  return;
1373
1612
  }
1374
1613
  if (t.error === "expired_token") {
1375
- console.error("\n Code expired.");
1614
+ if (spinnerTimer) {
1615
+ clearInterval(spinnerTimer);
1616
+ process.stdout.write("\r" + " ".repeat(80) + "\r");
1617
+ }
1618
+ console.error(` \u2717 Code expired. Run 'tcloud auth ${mode}' again.`);
1376
1619
  process.exit(1);
1377
1620
  }
1378
- process.stdout.write(".");
1379
1621
  }
1380
- console.error("\n Timed out.");
1381
- } catch (e) {
1382
- console.error("Failed:", e.message);
1622
+ if (spinnerTimer) {
1623
+ clearInterval(spinnerTimer);
1624
+ process.stdout.write("\r" + " ".repeat(80) + "\r");
1625
+ }
1626
+ console.error(` \u2717 Timed out after ${Math.round((d.expires_in || 600) / 60)} minutes.`);
1627
+ process.exit(1);
1628
+ } finally {
1629
+ if (spinnerTimer) clearInterval(spinnerTimer);
1383
1630
  }
1631
+ }
1632
+ auth.command("signup").description("Create an account via browser (device flow)").action(() => runDeviceFlow("signup"));
1633
+ auth.command("login").description("Log in via browser (device flow)").action(() => runDeviceFlow("login"));
1634
+ auth.command("logout").description("Remove stored credentials").action(() => {
1635
+ const c = loadConfig();
1636
+ delete c.apiKey;
1637
+ saveConfig(c);
1638
+ console.log(` \u2713 Logged out. Config kept at ${CONFIG_FILE}.`);
1384
1639
  });
1385
1640
  auth.command("set-key").description("Set API key directly").argument("<key>").action((key) => {
1386
1641
  const c = loadConfig();
1387
1642
  c.apiKey = key;
1388
1643
  saveConfig(c);
1389
- console.log("API key saved.");
1644
+ console.log(" \u2713 API key saved.");
1390
1645
  });
1391
1646
  auth.command("status").description("Show auth status").action(() => {
1392
1647
  const c = loadConfig();
1393
- console.log(c.apiKey ? `Authenticated: ${c.apiKey.slice(0, 15)}...` : "Not authenticated");
1648
+ console.log(c.apiKey ? ` \u2713 Authenticated: ${c.apiKey.slice(0, 15)}...${c.apiKey.slice(-4)}` : " \u2717 Not authenticated. Run: tcloud auth signup");
1394
1649
  const w = loadWallets();
1395
- if (w.length) console.log(`Shielded wallets: ${w.length}`);
1650
+ if (w.length) console.log(` Shielded wallets: ${w.length}`);
1651
+ });
1652
+ auth.command("whoami").description("Show logged-in account details").action(async () => {
1653
+ const c = loadConfig();
1654
+ if (!c.apiKey) {
1655
+ console.log(" \u2717 Not authenticated. Run: tcloud auth signup");
1656
+ return;
1657
+ }
1658
+ try {
1659
+ const res = await fetch(`${c.apiUrl}/api/auth/userinfo`, {
1660
+ headers: { Authorization: `Bearer ${c.apiKey}` }
1661
+ });
1662
+ if (!res.ok) {
1663
+ console.log(` \u2717 Auth check failed (${res.status}). Your key may be revoked \u2014 run: tcloud auth login`);
1664
+ return;
1665
+ }
1666
+ const me = await res.json();
1667
+ const user = me.user ?? {};
1668
+ const sub = me.subscription;
1669
+ console.log(` Email: ${user.email ?? "n/a"}`);
1670
+ console.log(` User: ${user.name ?? user.id ?? "n/a"}`);
1671
+ console.log(` Plan: ${sub?.plan ?? "free"}`);
1672
+ console.log(` Balance: $${Number(me.balance ?? 0).toFixed(4)}`);
1673
+ console.log(` Key: ${c.apiKey.slice(0, 15)}...${c.apiKey.slice(-4)}`);
1674
+ console.log(` API: ${c.apiUrl}`);
1675
+ } catch (e) {
1676
+ console.log(` \u2717 ${e.message ?? e}`);
1677
+ }
1678
+ });
1679
+ program.command("signup").description("Create an account via browser (alias for `auth signup`)").action(() => runDeviceFlow("signup"));
1680
+ program.command("login").description("Log in via browser (alias for `auth login`)").action(() => runDeviceFlow("login"));
1681
+ program.command("logout").description("Remove stored credentials (alias for `auth logout`)").action(() => {
1682
+ const c = loadConfig();
1683
+ delete c.apiKey;
1684
+ saveConfig(c);
1685
+ console.log(` \u2713 Logged out.`);
1686
+ });
1687
+ program.command("whoami").description("Show logged-in account (alias for `auth whoami`)").action(async () => {
1688
+ const c = loadConfig();
1689
+ if (!c.apiKey) {
1690
+ console.log(" \u2717 Not authenticated. Run: tcloud signup");
1691
+ return;
1692
+ }
1693
+ try {
1694
+ const res = await fetch(`${c.apiUrl}/api/auth/userinfo`, { headers: { Authorization: `Bearer ${c.apiKey}` } });
1695
+ if (!res.ok) {
1696
+ console.log(` \u2717 Auth failed (${res.status}). Run: tcloud login`);
1697
+ return;
1698
+ }
1699
+ const me = await res.json();
1700
+ const user = me.user ?? {};
1701
+ const sub = me.subscription;
1702
+ console.log(` ${user.email ?? user.name ?? user.id} \xB7 $${Number(me.balance ?? 0).toFixed(4)} \xB7 ${sub?.plan ?? "free"}`);
1703
+ } catch (e) {
1704
+ console.log(` \u2717 ${e.message ?? e}`);
1705
+ }
1396
1706
  });
1397
1707
  var wallet = program.command("wallet").description("Shielded wallet management");
1398
1708
  wallet.command("generate").description("Generate ephemeral wallet").option("-l, --label <name>").action((opts) => {
@@ -1513,7 +1823,10 @@ credits.command("add").description("Add credits").argument("<amount>").action(as
1513
1823
  const client = getClient();
1514
1824
  try {
1515
1825
  const data = await client.addCredits(parseFloat(amount));
1516
- console.log(`Credits added. New balance: $${data.balance.toFixed(4)}`);
1826
+ if (data.url) {
1827
+ console.log(`Checkout URL: ${data.url}`);
1828
+ console.log("Complete payment to add credits.");
1829
+ }
1517
1830
  } catch (e) {
1518
1831
  console.error("Error:", e.message);
1519
1832
  }