@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/{chunk-VD4RNZOC.js → chunk-ADGH2R2R.js} +1 -1
- package/dist/{chunk-HL4CXKET.js → chunk-LN7XX5KG.js} +229 -29
- package/dist/{chunk-STNZT6YR.js → chunk-W7PUZZRE.js} +2 -2
- package/dist/cli.cjs +366 -53
- package/dist/cli.js +141 -27
- package/dist/{client-CcuHG7_w.d.ts → client-DskWX_BT.d.cts} +402 -13
- package/dist/{client-CcuHG7_w.d.cts → client-DskWX_BT.d.ts} +402 -13
- package/dist/index.cjs +230 -29
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -3
- package/dist/instance.cjs +228 -29
- package/dist/instance.d.cts +1 -1
- package/dist/instance.d.ts +1 -1
- package/dist/instance.js +1 -1
- package/dist/shielded.cjs +228 -29
- package/dist/shielded.d.cts +1 -1
- package/dist/shielded.d.ts +1 -1
- package/dist/shielded.js +2 -2
- package/package.json +2 -1
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:
|
|
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(
|
|
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(
|
|
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
|
|
623
|
-
return
|
|
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
|
|
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
|
-
/**
|
|
634
|
-
async
|
|
635
|
-
const
|
|
636
|
-
return
|
|
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(
|
|
697
|
+
body: JSON.stringify(opts)
|
|
639
698
|
});
|
|
699
|
+
return data;
|
|
640
700
|
}
|
|
641
|
-
/**
|
|
642
|
-
async
|
|
643
|
-
const
|
|
644
|
-
return
|
|
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
|
-
/**
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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)}
|
|
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
|
-
|
|
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
|
}
|