@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/{chunk-HL4CXKET.js → chunk-AKR7CS4P.js} +231 -30
- package/dist/{chunk-VD4RNZOC.js → chunk-MKLCBBHZ.js} +1 -1
- package/dist/{chunk-STNZT6YR.js → chunk-QNIJ7KAA.js} +2 -2
- package/dist/cli.cjs +368 -54
- package/dist/cli.js +141 -27
- package/dist/{client-CcuHG7_w.d.ts → client-_ghO89WM.d.cts} +402 -13
- package/dist/{client-CcuHG7_w.d.cts → client-_ghO89WM.d.ts} +402 -13
- package/dist/index.cjs +232 -30
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -3
- package/dist/instance.cjs +230 -30
- package/dist/instance.d.cts +1 -1
- package/dist/instance.d.ts +1 -1
- package/dist/instance.js +1 -1
- package/dist/shielded.cjs +230 -30
- 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
|
@@ -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":
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
623
|
-
return
|
|
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
|
|
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
|
-
/**
|
|
634
|
-
async
|
|
635
|
-
const
|
|
636
|
-
return
|
|
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(
|
|
698
|
+
body: JSON.stringify(opts)
|
|
639
699
|
});
|
|
700
|
+
return data;
|
|
640
701
|
}
|
|
641
|
-
/**
|
|
642
|
-
async
|
|
643
|
-
const
|
|
644
|
-
return
|
|
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
|
-
/**
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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)}
|
|
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
|
-
|
|
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
|
}
|