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