@tonbo/cli 0.4.0 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +66 -189
  2. package/dist/bin/tonbo.js +1617 -2962
  3. package/package.json +9 -15
package/dist/bin/tonbo.js CHANGED
@@ -1,1009 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/app.ts
4
- import { randomUUID as randomUUID5 } from "node:crypto";
5
- import { Command } from "commander";
4
+ import { Command, InvalidArgumentError, Option } from "commander";
6
5
  import { readFileSync } from "node:fs";
7
6
 
8
- // src/api.ts
9
- import { randomUUID } from "node:crypto";
10
-
11
- // src/http.ts
12
- var HttpError = class extends Error {
13
- constructor(message, status, body) {
14
- super(message);
15
- this.status = status;
16
- this.body = body;
17
- }
18
- };
19
- async function requestJsonResponse(fetcher, url, init = {}) {
20
- const response = await fetcher(url, init);
21
- const body = await response.json().catch(() => null);
22
- if (!response.ok) {
23
- const record = body && typeof body === "object" ? body : {};
24
- const message = [record.detail, record.message, record.error, record.title].find(
25
- (value) => typeof value === "string"
26
- );
27
- throw new HttpError(
28
- typeof message === "string" ? message : `Request failed with HTTP ${response.status}.`,
29
- response.status,
30
- body
31
- );
32
- }
33
- return { body, headers: response.headers, status: response.status };
34
- }
35
- async function requestJson(fetcher, url, init = {}) {
36
- return (await requestJsonResponse(fetcher, url, init)).body;
37
- }
38
-
39
- // src/contracts.ts
40
- import { Ajv2020 } from "ajv/dist/2020.js";
41
-
42
- // src/generated/contracts.ts
43
- var piAgentSchema = {
44
- "$schema": "https://json-schema.org/draft/2020-12/schema",
45
- "$id": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json",
46
- "title": "PI Agent v1",
47
- "type": "object",
48
- "additionalProperties": false,
49
- "required": [
50
- "runtime",
51
- "driver"
52
- ],
53
- "properties": {
54
- "runtime": {
55
- "const": "pi"
56
- },
57
- "secrets": {
58
- "type": "array",
59
- "maxItems": 32,
60
- "uniqueItems": true,
61
- "items": {
62
- "type": "string",
63
- "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
64
- }
65
- },
66
- "driver": {
67
- "oneOf": [
68
- {
69
- "type": "object",
70
- "additionalProperties": false,
71
- "required": [
72
- "kind"
73
- ],
74
- "properties": {
75
- "kind": {
76
- "const": "native"
77
- }
78
- }
79
- },
80
- {
81
- "type": "object",
82
- "additionalProperties": false,
83
- "required": [
84
- "kind",
85
- "protocol",
86
- "command"
87
- ],
88
- "properties": {
89
- "kind": {
90
- "const": "command"
91
- },
92
- "protocol": {
93
- "const": "pi-rpc-v1"
94
- },
95
- "command": {
96
- "type": "array",
97
- "minItems": 1,
98
- "maxItems": 64,
99
- "items": {
100
- "type": "string",
101
- "minLength": 1,
102
- "maxLength": 1024
103
- }
104
- }
105
- }
106
- }
107
- ]
108
- }
109
- }
110
- };
111
- var agentServiceSchema = {
112
- "$schema": "https://json-schema.org/draft/2020-12/schema",
113
- "$id": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json",
114
- "title": "Tonbo Agent application service v1",
115
- "type": "object",
116
- "additionalProperties": false,
117
- "required": [
118
- "command"
119
- ],
120
- "properties": {
121
- "command": {
122
- "type": "array",
123
- "minItems": 1,
124
- "maxItems": 64,
125
- "items": {
126
- "type": "string",
127
- "minLength": 1,
128
- "maxLength": 4096
129
- }
130
- },
131
- "secrets": {
132
- "type": "array",
133
- "maxItems": 32,
134
- "uniqueItems": true,
135
- "items": {
136
- "type": "string",
137
- "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
138
- }
139
- },
140
- "kubernetes": {
141
- "$ref": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json#/$defs/request"
142
- }
143
- }
144
- };
145
- var kubernetesProfilesSchema = {
146
- "$schema": "https://json-schema.org/draft/2020-12/schema",
147
- "$id": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json",
148
- "title": "Tonbo managed Kubernetes profiles v1",
149
- "$defs": {
150
- "request": {
151
- "type": "object",
152
- "additionalProperties": false,
153
- "required": [
154
- "profile"
155
- ],
156
- "properties": {
157
- "profile": false
158
- }
159
- }
160
- },
161
- "x-tonbo-profiles": {}
162
- };
163
- var declarationSchema = {
164
- "$schema": "https://json-schema.org/draft/2020-12/schema",
165
- "$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v2.schema.json",
166
- "title": "Tonbo Agent declaration v2",
167
- "type": "object",
168
- "additionalProperties": false,
169
- "required": [
170
- "version",
171
- "harness"
172
- ],
173
- "properties": {
174
- "version": {
175
- "const": 2
176
- },
177
- "inference": {
178
- "type": "object",
179
- "additionalProperties": false,
180
- "default": {
181
- "model": "claude-sonnet-4-5"
182
- },
183
- "required": [
184
- "model"
185
- ],
186
- "properties": {
187
- "model": {
188
- "type": "string",
189
- "minLength": 1,
190
- "maxLength": 160
191
- }
192
- }
193
- },
194
- "build": {
195
- "type": "object",
196
- "additionalProperties": false,
197
- "required": [
198
- "command"
199
- ],
200
- "properties": {
201
- "command": {
202
- "type": "array",
203
- "minItems": 1,
204
- "maxItems": 64,
205
- "items": {
206
- "type": "string",
207
- "minLength": 1,
208
- "maxLength": 1024
209
- }
210
- }
211
- }
212
- },
213
- "service": {
214
- "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
215
- },
216
- "agent": {
217
- "$ref": "https://contracts.tonbo.dev/agents/agent-name-v1.json#/$defs/publicHostname"
218
- },
219
- "harness": {
220
- "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
221
- }
222
- }
223
- };
224
- var deploymentSchema = {
225
- "$schema": "https://json-schema.org/draft/2020-12/schema",
226
- "$id": "https://contracts.tonbo.dev/agents/managed-deployment-v1.schema.json",
227
- "title": "Managed Agent Deployment v1",
228
- "type": "object",
229
- "additionalProperties": false,
230
- "required": [
231
- "version",
232
- "agent",
233
- "source",
234
- "inference"
235
- ],
236
- "properties": {
237
- "version": {
238
- "const": 1
239
- },
240
- "agent": {
241
- "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
242
- },
243
- "source": {
244
- "type": "object",
245
- "additionalProperties": false,
246
- "required": [
247
- "format",
248
- "sha256",
249
- "size_bytes"
250
- ],
251
- "properties": {
252
- "format": {
253
- "const": "tar-v1"
254
- },
255
- "sha256": {
256
- "type": "string",
257
- "pattern": "^[0-9a-f]{64}$"
258
- },
259
- "size_bytes": {
260
- "type": "integer",
261
- "minimum": 1,
262
- "maximum": 67108864
263
- }
264
- }
265
- },
266
- "inference": {
267
- "type": "object",
268
- "additionalProperties": false,
269
- "required": [
270
- "model"
271
- ],
272
- "properties": {
273
- "model": {
274
- "type": "string",
275
- "minLength": 1,
276
- "maxLength": 160
277
- }
278
- }
279
- },
280
- "service": {
281
- "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
282
- }
283
- }
284
- };
285
- var agentNameContract = {
286
- "$schema": "https://json-schema.org/draft/2020-12/schema",
287
- "$id": "https://contracts.tonbo.dev/agents/agent-name-v1.json",
288
- "x-tonbo-version": 1,
289
- "x-tonbo-public-hostname-apex": "tonbo.sh",
290
- "x-tonbo-public-hostname-template": "<agent>-<organization>-<word>-<word>.tonbo.sh",
291
- "$defs": {
292
- "name": {
293
- "type": "string",
294
- "minLength": 1,
295
- "maxLength": 40,
296
- "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
297
- "not": {
298
- "enum": [
299
- "api",
300
- "artifacts",
301
- "auth",
302
- "inference",
303
- "network-health",
304
- "sandbox-control",
305
- "status",
306
- "streams",
307
- "www"
308
- ]
309
- }
310
- },
311
- "publicHostname": {
312
- "type": "string",
313
- "minLength": 16,
314
- "maxLength": 72,
315
- "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)+\\.tonbo\\.sh$",
316
- "not": {
317
- "enum": [
318
- "api.tonbo.sh",
319
- "artifacts.tonbo.sh",
320
- "auth.tonbo.sh",
321
- "inference.tonbo.sh",
322
- "network-health.tonbo.sh",
323
- "sandbox-control.tonbo.sh",
324
- "status.tonbo.sh",
325
- "streams.tonbo.sh",
326
- "www.tonbo.sh"
327
- ]
328
- }
329
- },
330
- "hostnameWord": {
331
- "type": "string",
332
- "enum": [
333
- "arch",
334
- "bark",
335
- "beam",
336
- "bell",
337
- "bird",
338
- "blue",
339
- "boat",
340
- "bold",
341
- "book",
342
- "calm",
343
- "cave",
344
- "clay",
345
- "coal",
346
- "cool",
347
- "cove",
348
- "dawn",
349
- "deer",
350
- "dove",
351
- "dune",
352
- "dusk",
353
- "echo",
354
- "fern",
355
- "fire",
356
- "flax",
357
- "flow",
358
- "foam",
359
- "fawn",
360
- "frog",
361
- "gate",
362
- "glow",
363
- "gold",
364
- "gray",
365
- "gulf",
366
- "hail",
367
- "halo",
368
- "hare",
369
- "hawk",
370
- "hill",
371
- "iris",
372
- "jade",
373
- "kite",
374
- "lake",
375
- "lamb",
376
- "leaf",
377
- "lime",
378
- "lion",
379
- "lobe",
380
- "loft",
381
- "loom",
382
- "luma",
383
- "lynx",
384
- "mint",
385
- "mist",
386
- "moon",
387
- "moss",
388
- "navy",
389
- "nest",
390
- "nova",
391
- "oak",
392
- "ocean",
393
- "onyx",
394
- "opal",
395
- "otter",
396
- "palm",
397
- "peak",
398
- "pine",
399
- "pond",
400
- "pool",
401
- "rain",
402
- "reed",
403
- "reef",
404
- "rice",
405
- "ring",
406
- "rise",
407
- "road",
408
- "rock",
409
- "rose",
410
- "ruby",
411
- "sage",
412
- "sand",
413
- "seal",
414
- "seed",
415
- "silk",
416
- "sky",
417
- "snow",
418
- "soft",
419
- "star",
420
- "stem",
421
- "stone",
422
- "surf",
423
- "swan",
424
- "teal",
425
- "tide",
426
- "tree",
427
- "vale",
428
- "vine",
429
- "wave",
430
- "west",
431
- "wind",
432
- "wing",
433
- "wolf",
434
- "wood",
435
- "wren",
436
- "yarn",
437
- "zest"
438
- ]
439
- }
440
- }
441
- };
442
- var regionDiscoverySchema = {
443
- "$schema": "https://json-schema.org/draft/2020-12/schema",
444
- "$id": "https://contracts.tonbo.dev/regions/discovery-v1.schema.json",
445
- "title": "Regional allocation discovery v1",
446
- "type": "object",
447
- "additionalProperties": false,
448
- "required": [
449
- "version",
450
- "regions"
451
- ],
452
- "properties": {
453
- "version": {
454
- "const": 1
455
- },
456
- "regions": {
457
- "type": "array",
458
- "items": {
459
- "type": "object",
460
- "additionalProperties": false,
461
- "required": [
462
- "id",
463
- "display_name",
464
- "allocation_enabled"
465
- ],
466
- "properties": {
467
- "id": {
468
- "type": "string",
469
- "pattern": "^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$"
470
- },
471
- "display_name": {
472
- "type": "string",
473
- "minLength": 1
474
- },
475
- "allocation_enabled": {
476
- "type": "boolean"
477
- }
478
- }
479
- }
480
- }
481
- }
482
- };
483
-
484
- // src/contracts.ts
485
- var ajv = new Ajv2020({ allErrors: true, useDefaults: true });
486
- ajv.addKeyword({ keyword: "x-tonbo-profiles" });
487
- ajv.addKeyword({ keyword: "x-tonbo-version" });
488
- ajv.addKeyword({ keyword: "x-tonbo-public-hostname-apex" });
489
- ajv.addKeyword({ keyword: "x-tonbo-public-hostname-template" });
490
- ajv.addSchema(kubernetesProfilesSchema);
491
- ajv.addSchema(piAgentSchema);
492
- ajv.addSchema(agentNameContract);
493
- ajv.addSchema(agentServiceSchema);
494
- var validateDeclaration = ajv.compile(declarationSchema);
495
- var validateDeploymentSpec = ajv.compile(deploymentSchema);
496
- var validateRegionDiscovery = ajv.compile(regionDiscoverySchema);
497
- function parseRegionDiscovery(value) {
498
- if (!validateRegionDiscovery(value)) {
499
- throw new Error(validationMessage("Region discovery", validateRegionDiscovery.errors));
500
- }
501
- const result = value;
502
- if (new Set(result.regions.map((region) => region.id)).size !== result.regions.length)
503
- throw new Error("Region discovery contains duplicate region identities.");
504
- return result;
505
- }
506
- function validationMessage(label, errors) {
507
- const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
508
- return `${label} is invalid${detail ? `: ${detail}` : "."}`;
509
- }
510
- function parseDeclaration(value) {
511
- const candidate = structuredClone(value);
512
- if (!validateDeclaration(candidate)) {
513
- throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
514
- }
515
- return candidate;
516
- }
517
- function assertManagedDeploymentSpec(value) {
518
- if (!validateDeploymentSpec(value)) {
519
- throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
520
- }
521
- }
522
-
523
- // src/api.ts
524
- function turnSubmission(response) {
525
- if (response.status === 202) return { state: "pending" };
526
- const body = response.body ?? {};
527
- if (!body.data || typeof body.data.assistant_text !== "string")
528
- throw new Error("Tonbo answered the Turn without its result.");
529
- return { state: "completed", turn: body.data };
530
- }
531
- var TonboApi = class {
532
- constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
533
- this.fetcher = fetcher;
534
- this.accountOrigin = accountOrigin;
535
- this.managementOrigin = managementOrigin;
536
- }
537
- cancelActivities(agentId, token, body) {
538
- return this.management("POST", `/v1/agents/${agentId}/production/cancellations`, token, body);
539
- }
540
- async listRegions(oauthToken) {
541
- return parseRegionDiscovery(
542
- await requestJson(this.fetcher, `${this.accountOrigin}/api/cli/regions`, {
543
- headers: { authorization: `Bearer ${oauthToken}` }
544
- })
545
- );
546
- }
547
- machineRequest(oauthToken, path6 = "", method = "GET", body) {
548
- return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/machines${path6}`, {
549
- method,
550
- headers: {
551
- authorization: `Bearer ${oauthToken}`,
552
- ...body === void 0 ? {} : { "content-type": "application/json" }
553
- },
554
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
555
- });
556
- }
557
- listAgents(oauthToken) {
558
- return requestJson(
559
- this.fetcher,
560
- `${this.accountOrigin}/api/cli/agents`,
561
- {
562
- headers: { authorization: `Bearer ${oauthToken}` }
563
- }
564
- ).then((body) => body.agents);
565
- }
566
- listOrganizations(oauthToken) {
567
- return requestJson(
568
- this.fetcher,
569
- `${this.accountOrigin}/api/cli/accounts`,
570
- {
571
- headers: { authorization: `Bearer ${oauthToken}` }
572
- }
573
- ).then((body) => body.organizations);
574
- }
575
- /** An omitted region lets the server choose the only region that accepts
576
- * new Agents; once more than one does, it refuses the request. */
577
- createAgent(oauthToken, {
578
- name,
579
- organizationId,
580
- region,
581
- requestId = randomUUID()
582
- }) {
583
- return requestJson(
584
- this.fetcher,
585
- `${this.accountOrigin}/api/cli/agents`,
586
- {
587
- method: "POST",
588
- headers: {
589
- authorization: `Bearer ${oauthToken}`,
590
- "content-type": "application/json"
591
- },
592
- body: JSON.stringify({
593
- name,
594
- requestId,
595
- ...organizationId ? { orgId: organizationId } : {},
596
- ...region ? { region } : {}
597
- })
598
- }
599
- ).then((body) => body.agent).catch((cause) => {
600
- if (cause instanceof Error && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500)
601
- throw cause;
602
- throw new Error(
603
- `Agent creation was not confirmed. Query \`tonbo agent creation ${requestId}${organizationId ? ` --account ${organizationId}` : ""}\` or retry creation with --request-id ${requestId} and the same name, account and region.`,
604
- { cause }
605
- );
606
- });
607
- }
608
- readAgentCreationRequest(oauthToken, requestId, accountId) {
609
- const account = accountId ? `?account=${encodeURIComponent(accountId)}` : "";
610
- return requestJson(
611
- this.fetcher,
612
- `${this.accountOrigin}/api/cli/agents/creations/${encodeURIComponent(requestId)}${account}`,
613
- { headers: { authorization: `Bearer ${oauthToken}` } }
614
- );
615
- }
616
- registerSshKey(oauthToken, key) {
617
- return requestJson(
618
- this.fetcher,
619
- `${this.accountOrigin}/api/cli/ssh-keys`,
620
- {
621
- method: "POST",
622
- headers: {
623
- authorization: `Bearer ${oauthToken}`,
624
- "content-type": "application/json"
625
- },
626
- body: JSON.stringify({
627
- algorithm: key.algorithm,
628
- key_base64: key.keyBase64,
629
- label: key.label
630
- })
631
- }
632
- ).then((body) => body.key);
633
- }
634
- revokeSshKey(oauthToken, fingerprint) {
635
- return requestJson(
636
- this.fetcher,
637
- `${this.accountOrigin}/api/cli/ssh-keys`,
638
- {
639
- method: "DELETE",
640
- headers: {
641
- authorization: `Bearer ${oauthToken}`,
642
- "content-type": "application/json"
643
- },
644
- body: JSON.stringify({ fingerprint })
645
- }
646
- ).then((body) => body.key);
647
- }
648
- exchangeManagementToken(oauthToken, agentId) {
649
- return requestJson(
650
- this.fetcher,
651
- `${this.accountOrigin}/api/cli/agents/${agentId}/token`,
652
- {
653
- method: "POST",
654
- headers: { authorization: `Bearer ${oauthToken}` }
655
- }
656
- ).then((body) => body.access_token);
657
- }
658
- async deploy({
659
- bundle,
660
- origin,
661
- agentId,
662
- promote,
663
- spec,
664
- token
665
- }) {
666
- const descriptor = {
667
- format: bundle.format,
668
- sha256: bundle.sha256,
669
- size_bytes: bundle.size_bytes
670
- };
671
- const bundlesPath = `/v1/agents/${agentId}/source-bundles`;
672
- const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
673
- format: descriptor.format,
674
- size_bytes: descriptor.size_bytes
675
- });
676
- if (prepared.status === "upload") {
677
- if (!prepared.upload_url) throw new Error("Tonbo did not return a source upload URL.");
678
- if (!prepared.upload_headers || prepared.upload_headers["if-none-match"] !== "*")
679
- throw new Error("Missing immutable source upload headers.");
680
- const uploaded = await this.fetcher(prepared.upload_url, {
681
- redirect: "error",
682
- method: "PUT",
683
- headers: prepared.upload_headers,
684
- body: new Blob([new Uint8Array(bundle.bytes)])
685
- });
686
- let uploadError = null;
687
- if (!uploaded.ok) {
688
- uploadError = new Error(`Source upload failed with HTTP ${uploaded.status}.`);
689
- }
690
- try {
691
- await this.management("POST", `${bundlesPath}/${bundle.sha256}/complete`, token, {
692
- format: bundle.format,
693
- size_bytes: bundle.size_bytes
694
- });
695
- } catch (error) {
696
- if (uploadError)
697
- throw new AggregateError([uploadError, error], "Source bundle upload did not complete.");
698
- throw error;
699
- }
700
- }
701
- let deployment = await this.createDeployment(agentId, { spec, origin }, token);
702
- const previous = await this.getProduction(agentId, token);
703
- const previousDeployment = previous ? await this.getDeployment(agentId, previous.deployment_id, token) : null;
704
- const unchanged = previousDeployment?.spec_sha256 === deployment.spec_sha256;
705
- let production = previous;
706
- if (promote) {
707
- production = await this.putProduction(
708
- agentId,
709
- {
710
- deployment_id: deployment.id,
711
- desired_state: "running",
712
- expected_generation: previous ? previous.generation : null
713
- },
714
- token
715
- );
716
- deployment = await this.getDeployment(agentId, deployment.id, token);
717
- }
718
- return { deployment, production, unchanged };
719
- }
720
- createDeployment(agentId, body, token) {
721
- return this.management(
722
- "POST",
723
- `/v1/agents/${agentId}/deployments`,
724
- token,
725
- body
726
- ).then((response) => response.data);
727
- }
728
- listDeployments(agentId, token) {
729
- return this.managementList(`/v1/agents/${agentId}/deployments`, token);
730
- }
731
- getDeployment(agentId, deploymentId, token) {
732
- return this.management(
733
- "GET",
734
- `/v1/agents/${agentId}/deployments/${deploymentId}`,
735
- token
736
- ).then((response) => response.data);
737
- }
738
- getProduction(agentId, token) {
739
- return this.management(
740
- "GET",
741
- `/v1/agents/${agentId}/production`,
742
- token
743
- ).then(
744
- (response) => response.data,
745
- (error) => {
746
- if (error.status === 404) return null;
747
- throw error;
748
- }
749
- );
750
- }
751
- putProduction(agentId, body, token) {
752
- return this.management(
753
- "PUT",
754
- `/v1/agents/${agentId}/production`,
755
- token,
756
- body
757
- ).then((response) => response.data);
758
- }
759
- listRollouts(agentId, token) {
760
- return this.managementList(`/v1/agents/${agentId}/production/rollouts`, token);
761
- }
762
- /** Checks that Production is running and opens a durable Session, or
763
- * reuses the given one. */
764
- async openSession({
765
- agentId,
766
- sessionId,
767
- token
768
- }) {
769
- const production = await this.getProduction(agentId, token);
770
- if (!production)
771
- throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
772
- if (production.observed_state !== "running")
773
- throw new Error(
774
- `Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
775
- );
776
- const session = sessionId ? { id: sessionId } : (await this.management(
777
- "POST",
778
- `/v1/agents/${agentId}/sessions`,
779
- token,
780
- { deployment_id: production.deployment_id }
781
- )).data;
782
- return { production, session };
783
- }
784
- /** Submits one Turn under its own id; sending it again under the same id is
785
- * answered from the same operation. The Management API holds the request
786
- * for up to five minutes: it answers the result, 409 `turn_failed`, a
787
- * refusal, or 202 while the Turn waits or runs. */
788
- async submitTurn({
789
- agentId,
790
- prompt,
791
- sessionId,
792
- signal,
793
- token,
794
- turnId
795
- }) {
796
- return turnSubmission(
797
- await this.managementResponse(
798
- "POST",
799
- `/v1/agents/${agentId}/sessions/${sessionId}/turns`,
800
- token,
801
- { body: { prompt }, idempotencyKey: turnId, signal }
802
- )
803
- );
804
- }
805
- /** One durable Turn; reading it never affects its execution. */
806
- getTurn(agentId, sessionId, turnId, token) {
807
- return this.management(
808
- "GET",
809
- `/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}`,
810
- token
811
- ).then((response) => response.data);
812
- }
813
- turnEvents({
814
- after = 0,
815
- agentId,
816
- sessionId,
817
- token,
818
- turnId
819
- }) {
820
- return this.management(
821
- "GET",
822
- `/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
823
- token
824
- );
825
- }
826
- listAgentSecrets(agentId, token) {
827
- return this.management(
828
- "GET",
829
- `/v1/agents/${agentId}/secrets`,
830
- token
831
- ).then((body) => body.data);
832
- }
833
- setAgentSecret(agentId, name, value, token) {
834
- return this.management(
835
- "PUT",
836
- `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
837
- token,
838
- { value }
839
- );
840
- }
841
- deleteAgentSecret(agentId, name, token) {
842
- return this.management(
843
- "DELETE",
844
- `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
845
- token
846
- );
847
- }
848
- management(method, path6, token, body) {
849
- return this.managementResponse(method, path6, token, { body }).then(
850
- (response) => response.body
851
- );
852
- }
853
- managementResponse(method, path6, token, {
854
- body,
855
- idempotencyKey = randomUUID(),
856
- signal
857
- } = {}) {
858
- return requestJsonResponse(this.fetcher, `${this.managementOrigin}${path6}`, {
859
- method,
860
- headers: {
861
- authorization: `Bearer ${token}`,
862
- ...body === void 0 ? {} : { "content-type": "application/json", "idempotency-key": idempotencyKey }
863
- },
864
- ...body === void 0 ? {} : { body: JSON.stringify(body) },
865
- ...signal ? { signal } : {}
866
- });
867
- }
868
- async managementList(path6, token) {
869
- const values = [];
870
- let cursor = null;
871
- do {
872
- const separator = path6.includes("?") ? "&" : "?";
873
- const page = await this.management(
874
- "GET",
875
- `${path6}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
876
- token
877
- );
878
- values.push(...page.data);
879
- cursor = page.next_cursor;
880
- } while (cursor);
881
- return values;
882
- }
883
- };
884
-
885
- // src/agent-source.ts
886
- import { lstat, readFile } from "node:fs/promises";
887
- import path from "node:path";
888
-
889
- // ../../packages/agent-source-inspector/dist/src/index.js
890
- var AGENTS_INSTRUCTIONS_PATH = "AGENTS.md";
891
- var PI_SETTINGS_PATH = ".pi/settings.json";
892
- var MAX_HARNESS_CONFIG_BYTES = 256 * 1024;
893
- var supportedHarnesses = [
894
- {
895
- command: "pi",
896
- id: "pi",
897
- name: "PI"
898
- }
899
- ];
900
- var supportedExecutionTargets = [
901
- { driver: "native", harness: "pi" },
902
- { driver: "command", harness: "pi" }
903
- ];
904
- var AgentSourceInspectionError = class extends Error {
905
- code;
906
- path;
907
- constructor(code, path6, message, options) {
908
- super(message, options);
909
- this.code = code;
910
- this.path = path6;
911
- this.name = "AgentSourceInspectionError";
912
- }
913
- };
914
- function parsePiPackageCount(contents) {
915
- let settings;
916
- try {
917
- settings = JSON.parse(contents);
918
- } catch (error) {
919
- throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} must contain valid JSON.`, { cause: error });
920
- }
921
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
922
- throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} must contain a JSON object.`);
923
- }
924
- if (!("packages" in settings))
925
- return 0;
926
- const packages = settings.packages;
927
- if (!Array.isArray(packages)) {
928
- throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} packages must be an array.`);
929
- }
930
- return packages.length;
931
- }
932
- async function inspectAgentSource(source) {
933
- const [agentsMd, piSettings] = await Promise.all([
934
- source.has(AGENTS_INSTRUCTIONS_PATH),
935
- source.readText(PI_SETTINGS_PATH, MAX_HARNESS_CONFIG_BYTES)
936
- ]);
937
- if (piSettings !== null) {
938
- return {
939
- version: 1,
940
- instructions: { agentsMd },
941
- harness: {
942
- id: "pi",
943
- reason: "pi_settings_found",
944
- state: "identified"
945
- },
946
- pi: {
947
- packageCount: parsePiPackageCount(piSettings),
948
- settingsFound: true
949
- }
950
- };
951
- }
952
- return {
953
- version: 1,
954
- instructions: { agentsMd },
955
- harness: {
956
- candidates: supportedHarnesses.map((harness) => harness.id),
957
- reason: "no_harness_specific_config",
958
- state: "selection_required"
959
- },
960
- pi: {
961
- packageCount: 0,
962
- settingsFound: false
963
- }
964
- };
965
- }
966
-
967
- // src/agent-source.ts
968
- var LocalAgentSource = class {
969
- constructor(root) {
970
- this.root = root;
971
- }
972
- async has(filename) {
973
- const metadata = await this.metadata(filename);
974
- return metadata?.isFile() === true;
975
- }
976
- async readText(filename, maxBytes) {
977
- const metadata = await this.metadata(filename);
978
- if (!metadata) return null;
979
- if (!metadata.isFile() || metadata.isSymbolicLink()) {
980
- throw new Error(`${filename} must be a regular file.`);
981
- }
982
- if (metadata.size > maxBytes) {
983
- throw new Error(`${filename} exceeds the ${maxBytes}-byte inspection limit.`);
984
- }
985
- return readFile(path.join(this.root, filename), "utf8");
986
- }
987
- async metadata(filename) {
988
- try {
989
- return await lstat(path.join(this.root, filename));
990
- } catch (error) {
991
- if (error.code === "ENOENT") return null;
992
- throw error;
993
- }
994
- }
995
- };
996
- function inspectLocalAgentSource(root) {
997
- return inspectAgentSource(new LocalAgentSource(root));
998
- }
999
-
1000
- // src/auth.ts
7
+ // ../../packages/tonbo-cli-core/dist/src/auth.js
1001
8
  import { createHash, randomBytes } from "node:crypto";
1002
9
  import { createServer } from "node:http";
1003
10
  import { execFile } from "node:child_process";
11
+ import os from "node:os";
1004
12
  import { promisify } from "node:util";
1005
13
 
1006
- // src/callback-page.ts
14
+ // ../../packages/tonbo-cli-core/dist/src/callback-page.js
1007
15
  var WORDMARK = [
1008
16
  "\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
1009
17
  "\u2588\u2588\u2584 \u2584\u2588\u2588\u2580\u2584 \u2588\u2588 \u2584\u2580\u2588\u2588 \u2584 \u2588\u2588 \u2584\u2580\u2588",
@@ -1111,1904 +119,1003 @@ function renderCallbackPage(outcome) {
1111
119
  border: 0;
1112
120
  border-top: 1px solid var(--rule);
1113
121
  }
1114
- </style>
1115
- </head>
1116
- <body>
1117
- <header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
1118
- <main>
1119
- <h1>${title}</h1>
1120
- <p>${body}</p>
1121
- <hr>
1122
- </main>
1123
- </body>
1124
- </html>
1125
- `;
1126
- }
1127
- function callbackResponse(outcome) {
1128
- return {
1129
- status: OUTCOMES[outcome].status,
1130
- headers: {
1131
- "content-type": "text/html; charset=utf-8",
1132
- "cache-control": "no-store"
1133
- },
1134
- body: renderCallbackPage(outcome)
1135
- };
1136
- }
1137
-
1138
- // src/auth.ts
1139
- var exec = promisify(execFile);
1140
- var LOOPBACK_REDIRECT = "http://localhost:17655/callback";
1141
- function base64url(value) {
1142
- return value.toString("base64url");
1143
- }
1144
- function withAbsoluteExpiry(tokens) {
1145
- if (tokens.expires_at || !Number.isInteger(tokens.expires_in) || (tokens.expires_in ?? 0) <= 0)
1146
- return tokens;
1147
- return {
1148
- ...tokens,
1149
- expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 0)
1150
- };
1151
- }
1152
- var AuthClient = class {
1153
- constructor(credentials, fetcher, accountOrigin = "https://tonbo.dev", browserOpener = openBrowser) {
1154
- this.credentials = credentials;
1155
- this.fetcher = fetcher;
1156
- this.accountOrigin = accountOrigin;
1157
- this.browserOpener = browserOpener;
1158
- }
1159
- /** The stored access token, refreshed first when it has less than
1160
- * `minimumLifetimeSeconds` left. */
1161
- async accessToken(minimumLifetimeSeconds = 30) {
1162
- const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
1163
- if (injected) return injected;
1164
- const tokens = await this.credentials.load();
1165
- if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
1166
- const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
1167
- if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + minimumLifetimeSeconds))
1168
- return tokens.access_token;
1169
- if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
1170
- const config = await this.config();
1171
- const refreshed = withAbsoluteExpiry(
1172
- await this.exchange(
1173
- config.token_endpoint,
1174
- new URLSearchParams({
1175
- client_id: config.client_id,
1176
- grant_type: "refresh_token",
1177
- refresh_token: tokens.refresh_token
1178
- })
1179
- )
1180
- );
1181
- if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
1182
- await this.credentials.save(refreshed);
1183
- return refreshed.access_token;
1184
- }
1185
- async login(progress = () => {
1186
- }) {
1187
- progress({ status: "started", step: "account-config" });
1188
- const config = await this.config();
1189
- progress({ status: "completed", step: "account-config" });
1190
- if (config.redirect_uri !== LOOPBACK_REDIRECT)
1191
- throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
1192
- const verifier = base64url(randomBytes(48));
1193
- const challenge = createHash("sha256").update(verifier).digest("base64url");
1194
- const state = base64url(randomBytes(24));
1195
- const authorization = new URL(config.authorization_endpoint);
1196
- authorization.search = new URLSearchParams({
1197
- client_id: config.client_id,
1198
- code_challenge: challenge,
1199
- code_challenge_method: "S256",
1200
- redirect_uri: config.redirect_uri,
1201
- response_type: "code",
1202
- scope: "openid profile email",
1203
- state
1204
- }).toString();
1205
- const code = await this.listenForCode(
1206
- state,
1207
- () => this.browserOpener(authorization.toString()),
1208
- progress
1209
- );
1210
- progress({ status: "started", step: "token-exchange" });
1211
- const tokens = withAbsoluteExpiry(
1212
- await this.exchange(
1213
- config.token_endpoint,
1214
- new URLSearchParams({
1215
- client_id: config.client_id,
1216
- code,
1217
- code_verifier: verifier,
1218
- grant_type: "authorization_code",
1219
- redirect_uri: config.redirect_uri
1220
- })
1221
- )
1222
- );
1223
- progress({ status: "completed", step: "token-exchange" });
1224
- progress({ status: "started", step: "credential-store" });
1225
- await this.credentials.save(tokens);
1226
- progress({ status: "completed", step: "credential-store" });
1227
- return tokens;
1228
- }
1229
- config() {
1230
- return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/config`);
1231
- }
1232
- exchange(endpoint, body) {
1233
- return requestJson(this.fetcher, endpoint, {
1234
- method: "POST",
1235
- headers: { "content-type": "application/x-www-form-urlencoded" },
1236
- body
1237
- });
1238
- }
1239
- listenForCode(expectedState, ready, progress) {
1240
- return new Promise((resolve, reject) => {
1241
- let settled = false;
1242
- const sockets = /* @__PURE__ */ new Set();
1243
- let authorizationStarted = false;
1244
- const startAuthorization = () => {
1245
- if (authorizationStarted) return;
1246
- authorizationStarted = true;
1247
- progress({ status: "completed", step: "browser" });
1248
- progress({ status: "started", step: "browser-authorization" });
1249
- };
1250
- const finish = (result, completedResponseSocket) => {
1251
- if (settled) return;
1252
- settled = true;
1253
- clearTimeout(timeout);
1254
- const complete = (closeError) => {
1255
- if (closeError) reject(closeError);
1256
- else if ("code" in result) {
1257
- progress({ status: "completed", step: "callback-close" });
1258
- resolve(result.code);
1259
- } else reject(result.error);
1260
- };
1261
- if ("code" in result) progress({ status: "started", step: "callback-close" });
1262
- if (!server.listening) {
1263
- complete();
1264
- return;
1265
- }
1266
- server.close(complete);
1267
- for (const socket of sockets) {
1268
- if (socket !== completedResponseSocket) socket.destroy();
1269
- }
1270
- server.closeIdleConnections();
1271
- };
1272
- const timeout = setTimeout(
1273
- () => {
1274
- finish({ error: new Error("Timed out waiting for browser login.") });
1275
- },
1276
- 5 * 60 * 1e3
1277
- );
1278
- const server = createServer((request, response) => {
1279
- const url = new URL(request.url ?? "/", LOOPBACK_REDIRECT);
1280
- const code = url.searchParams.get("code");
1281
- const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
1282
- if (oauthError) {
1283
- const responseSocket2 = response.socket;
1284
- respond(
1285
- response,
1286
- "denied",
1287
- () => finish({ error: new Error(oauthError) }, responseSocket2)
1288
- );
1289
- return;
1290
- }
1291
- if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState || !code) {
1292
- respond(response, "invalid");
1293
- return;
1294
- }
1295
- startAuthorization();
1296
- progress({ status: "completed", step: "browser-authorization" });
1297
- const responseSocket = response.socket;
1298
- respond(response, "complete", () => finish({ code }, responseSocket));
1299
- });
1300
- server.on("connection", (socket) => {
1301
- sockets.add(socket);
1302
- socket.once("close", () => sockets.delete(socket));
1303
- });
1304
- server.once("error", (error) => finish({ error }));
1305
- progress({ status: "started", step: "callback-server" });
1306
- server.listen(17655, "localhost", () => {
1307
- progress({ status: "completed", step: "callback-server" });
1308
- progress({ status: "started", step: "browser" });
1309
- void ready().then(startAuthorization).catch(
1310
- (error) => finish({
1311
- error: error instanceof Error ? error : new Error("Could not open the login browser.")
1312
- })
1313
- );
1314
- });
1315
- });
1316
- }
1317
- };
1318
- function respond(response, outcome, finished) {
1319
- const page = callbackResponse(outcome);
1320
- if (finished) response.once("finish", finished);
1321
- response.writeHead(page.status, { ...page.headers, connection: "close" });
1322
- response.end(page.body);
1323
- }
1324
- async function openBrowser(url) {
1325
- if (process.platform === "darwin") return void await exec("open", [url]);
1326
- if (process.platform === "win32")
1327
- return void await exec("rundll32", ["url.dll,FileProtocolHandler", url]);
1328
- return void await exec("xdg-open", [url]);
1329
- }
1330
-
1331
- // ../../packages/agent-source-bundler/dist/src/generated/contracts.js
1332
- var inferenceDefault = { "model": "claude-sonnet-5" };
1333
- var sourceBundleContract = { "version": 1, "format": "tar-v1", "key_prefix": "agent-source-bundles", "content_type": "application/vnd.tonbo.source+tar", "max_bytes": 67108864, "runtime_cache_directories": [".pi/npm", ".pi/git"] };
1334
- var piSessionContract = { "version": 1, "adapter": "pi-jsonl-v3", "format_version": 3, "durable_completion_timeout_seconds": 30, "turn_claim_seconds": 30, "turn_claim_renew_seconds": 10, "session_directory": "/sessions", "path_template": "/sessions/{session_id}.jsonl", "preflight_command": ["/usr/local/bin/artifacts", "runtime", "session-preflight"] };
1335
- var piSlackTemplate = { "repository": "tonbo-io/pi-slack-agent", "source_directory": "examples/pi-slack-agent", "service": { "command": ["node", "slack/server.mjs"], "secrets": ["SLACK_SIGNING_SECRET", "SLACK_BOT_TOKEN", "SLACK_BOT_USER_ID", "SLACK_TEAM_ID", "TONBO_AGENT_API_KEY", "TONBO_AGENT_API_ORIGIN", "TONBO_IAM_ORIGIN"] } };
1336
-
1337
- // ../../packages/agent-source-bundler/dist/src/inference-default.js
1338
- var DEFAULT_INFERENCE_MODEL = inferenceDefault.model;
1339
-
1340
- // ../../packages/agent-source-bundler/dist/src/deployment-origin.js
1341
- function deploymentOriginLabel(actor) {
1342
- return actor === "onboarding" ? "Setup guide" : "tonbo deploy";
1343
- }
1344
- function githubRepositoryUrl(remote) {
1345
- if (typeof remote !== "string" || !remote || /\s/.test(remote) || [...remote].some((character) => character.charCodeAt(0) < 32))
1346
- return null;
1347
- let path6;
1348
- const ssh = /^git@github\.com:([^?#]+)$/.exec(remote);
1349
- if (ssh)
1350
- path6 = ssh[1];
1351
- else {
1352
- try {
1353
- const url = new URL(remote);
1354
- if (url.hostname !== "github.com" || url.port || !["https:", "ssh:"].includes(url.protocol))
1355
- return null;
1356
- path6 = url.pathname.slice(1);
1357
- } catch {
1358
- return null;
1359
- }
1360
- }
1361
- path6 = path6.replace(/\/$/, "").replace(/\.git$/, "");
1362
- const parts = path6.split("/");
1363
- if (parts.length !== 2 || !/^[a-z\d](?:[a-z\d-]{0,38})$/i.test(parts[0]) || !/^[a-z\d._-]{1,100}$/i.test(parts[1]) || [".", ".."].includes(parts[1]))
1364
- return null;
1365
- return `https://github.com/${parts[0]}/${parts[1]}`;
1366
- }
1367
-
1368
- // src/commands.ts
1369
- import path4 from "node:path";
1370
- import { randomUUID as randomUUID3 } from "node:crypto";
1371
-
1372
- // src/build.ts
1373
- import { spawn } from "node:child_process";
1374
- async function runBuildCommand(root, command) {
1375
- if (command.length === 0) throw new Error("Build command must not be empty.");
1376
- await new Promise((resolve, reject) => {
1377
- const child = spawn(command[0], command.slice(1), {
1378
- cwd: root,
1379
- env: process.env,
1380
- // Command output is progress, not the CLI result. Keep stdout available
1381
- // for the single JSON document emitted by `tonbo --json deploy`.
1382
- stdio: ["inherit", process.stderr, process.stderr]
1383
- });
1384
- child.once("error", reject);
1385
- child.once("exit", (code, signal) => {
1386
- if (code === 0) resolve();
1387
- else
1388
- reject(
1389
- new Error(
1390
- `Build command failed${signal ? ` with signal ${signal}` : ` with status ${code}`}.`
1391
- )
1392
- );
1393
- });
1394
- });
1395
- }
1396
-
1397
- // ../../packages/agent-source-bundler/dist/src/native-pi.js
1398
- import { stringify } from "smol-toml";
1399
- var PI_SLACK_TEMPLATE_SERVICE = piSlackTemplate.service;
1400
- function nativePiDeclaration(model, service) {
1401
- return {
1402
- version: 2,
1403
- harness: { runtime: "pi", driver: { kind: "native" } },
1404
- inference: { model },
1405
- ...service ? { service } : {}
1406
- };
1407
- }
1408
-
1409
- // src/declaration.ts
1410
- import { randomUUID as randomUUID2 } from "node:crypto";
1411
- import { lstat as lstat2, open, readFile as readFile2, rename, rm } from "node:fs/promises";
1412
- import path2 from "node:path";
1413
- import { parse, stringify as stringify2 } from "smol-toml";
1414
- var DECLARATION_FILENAME = ".tonbo";
1415
- function createDeclaration(model = DEFAULT_INFERENCE_MODEL, driver = { kind: "native" }, buildCommand, agentHostname) {
1416
- const base = nativePiDeclaration(model.trim());
1417
- return parseDeclaration({
1418
- ...base,
1419
- ...agentHostname ? { agent: agentHostname } : {},
1420
- harness: { ...base.harness, driver },
1421
- ...buildCommand ? { build: { command: buildCommand } } : {}
1422
- });
1423
- }
1424
- function renderDeclaration(declaration) {
1425
- return `# Tonbo Agent configuration.
1426
- # Edit this file directly or run \`tonbo init\` to reconfigure.
1427
- ${stringify2(declaration)}`;
1428
- }
1429
- async function bindDeclarationAgent(root, agentHostname) {
1430
- const declaration = await loadDeclaration(root);
1431
- const bound = parseDeclaration({ ...declaration, agent: agentHostname });
1432
- await saveDeclaration(root, bound, true);
1433
- return bound;
1434
- }
1435
- async function declarationExists(root) {
1436
- const filename = path2.join(root, DECLARATION_FILENAME);
1437
- try {
1438
- const metadata = await lstat2(filename);
1439
- if (metadata.isSymbolicLink() || !metadata.isFile())
1440
- throw new Error(`${filename} must be a regular file.`);
1441
- return true;
1442
- } catch (error) {
1443
- if (error.code === "ENOENT") return false;
1444
- throw error;
1445
- }
1446
- }
1447
- async function saveDeclaration(root, declaration, overwrite) {
1448
- const filename = path2.join(root, DECLARATION_FILENAME);
1449
- const exists = await declarationExists(root);
1450
- if (exists && !overwrite)
1451
- throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1452
- const contents = renderDeclaration(declaration);
1453
- if (!overwrite) {
1454
- const handle2 = await open(filename, "wx", 420).catch((error) => {
1455
- if (error.code === "EEXIST")
1456
- throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1457
- throw error;
1458
- });
1459
- try {
1460
- await handle2.writeFile(contents, "utf8");
1461
- await handle2.sync();
1462
- } finally {
1463
- await handle2.close();
1464
- }
1465
- return;
1466
- }
1467
- const temporary = path2.join(root, `.${DECLARATION_FILENAME}.${process.pid}.${randomUUID2()}.tmp`);
1468
- let handle;
1469
- try {
1470
- handle = await open(temporary, "wx", 420);
1471
- await handle.writeFile(contents, "utf8");
1472
- await handle.sync();
1473
- await handle.close();
1474
- handle = void 0;
1475
- await rename(temporary, filename);
1476
- } catch (error) {
1477
- await handle?.close().catch(() => void 0);
1478
- await rm(temporary, { force: true }).catch(() => void 0);
1479
- throw error;
1480
- }
1481
- }
1482
- async function loadDeclaration(declarationRoot) {
1483
- const filename = path2.join(declarationRoot, DECLARATION_FILENAME);
1484
- let parsed;
1485
- try {
1486
- parsed = parse(await readFile2(filename, "utf8"));
1487
- } catch (error) {
1488
- if (error.code === "ENOENT")
1489
- throw new Error(`No ${DECLARATION_FILENAME} declaration found at ${filename}.`);
1490
- throw new Error(`Could not read ${filename} as TOML.`, { cause: error });
1491
- }
1492
- return parseDeclaration(parsed);
1493
- }
1494
- function buildDeploymentSpec(declaration, source) {
1495
- const spec = {
1496
- version: 1,
1497
- agent: declaration.harness,
1498
- source: {
1499
- format: source.format,
1500
- sha256: source.sha256,
1501
- size_bytes: source.size_bytes
1502
- },
1503
- inference: declaration.inference,
1504
- ...declaration.service ? { service: declaration.service } : {}
1505
- };
1506
- assertManagedDeploymentSpec(spec);
1507
- return spec;
1508
- }
1509
-
1510
- // ../../packages/agent-source-bundler/dist/src/index.js
1511
- import { createHash as createHash2 } from "node:crypto";
1512
- import { lstat as lstat3, readFile as readFile3, readdir } from "node:fs/promises";
1513
- import path3 from "node:path";
1514
- import ignore from "ignore";
1515
- import tar from "tar-stream";
1516
- var MAX_BUNDLE_BYTES = sourceBundleContract.max_bytes;
1517
- var SESSION_SOURCE_DIRECTORY = piSessionContract.session_directory.replace(/^\/+|\/+$/g, "");
1518
- var DEFAULT_IGNORES = [
1519
- ".git/",
1520
- "node_modules/",
1521
- ".DS_Store",
1522
- ".env",
1523
- ".env.*",
1524
- "!.env.example",
1525
- ".tonbo-cache/",
1526
- ".tonbo-system/",
1527
- ...sourceBundleContract.runtime_cache_directories.map((directory) => `${directory}/`)
1528
- ];
1529
- var EXACT_NPM_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1530
- var GIT_COMMIT = /^[0-9a-f]{40}$/i;
1531
- function packageSource(value, index) {
1532
- if (typeof value === "string")
1533
- return value;
1534
- if (value && typeof value === "object" && "source" in value && typeof value.source === "string")
1535
- return value.source;
1536
- throw new Error(`.pi/settings.json packages[${index}] must be a source string or object.`);
1537
- }
1538
- async function validatePackageSource(root, source) {
1539
- if (source.startsWith("npm:")) {
1540
- const specifier = source.slice(4);
1541
- const separator = specifier.lastIndexOf("@");
1542
- if (separator <= 0 || !EXACT_NPM_VERSION.test(specifier.slice(separator + 1))) {
1543
- throw new Error(`PI package ${source} must pin an exact npm version, for example npm:my-agent@1.2.3.`);
1544
- }
1545
- return;
1546
- }
1547
- if (source.startsWith("git:")) {
1548
- const separator = source.lastIndexOf("@");
1549
- if (separator <= "git:".length || !GIT_COMMIT.test(source.slice(separator + 1))) {
1550
- throw new Error(`PI package ${source} must pin a full 40-character Git commit.`);
1551
- }
1552
- return;
1553
- }
1554
- if (source.startsWith("./") || source.startsWith("../")) {
1555
- const settingsDirectory = path3.join(root, ".pi");
1556
- const resolved = path3.resolve(settingsDirectory, source);
1557
- const relative = path3.relative(root, resolved);
1558
- if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
1559
- throw new Error(`Local PI package ${source} resolves outside the deployed agent.`);
1560
- }
1561
- let metadata;
1562
- try {
1563
- metadata = await lstat3(resolved);
1564
- } catch (error) {
1565
- if (error.code === "ENOENT") {
1566
- throw new Error(`Local PI package ${source} does not exist.`);
1567
- }
1568
- throw error;
1569
- }
1570
- if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
1571
- throw new Error(`Local PI package ${source} must resolve to a real agent directory.`);
1572
- }
1573
- return;
1574
- }
1575
- throw new Error(`PI package ${source} must use an exact npm version, a full Git commit, or a agent-local path.`);
1576
- }
1577
- async function validatePiPackages(root) {
1578
- const filename = path3.join(root, ".pi", "settings.json");
1579
- let settings;
1580
- try {
1581
- settings = JSON.parse(await readFile3(filename, "utf8"));
1582
- } catch (error) {
1583
- if (error.code === "ENOENT")
1584
- return;
1585
- throw new Error(`Could not read ${filename} as JSON.`, { cause: error });
1586
- }
1587
- if (!settings || typeof settings !== "object" || !("packages" in settings))
1588
- return;
1589
- const packages = settings.packages;
1590
- if (!Array.isArray(packages))
1591
- throw new Error(`${filename} packages must be an array.`);
1592
- await Promise.all(packages.map((value, index) => validatePackageSource(root, packageSource(value, index))));
1593
- }
1594
- async function findDeclarationRoot(start) {
1595
- let candidate = path3.resolve(start);
1596
- for (; ; ) {
1597
- try {
1598
- if ((await lstat3(path3.join(candidate, ".tonbo"))).isFile())
1599
- return candidate;
1600
- } catch (error) {
1601
- if (error.code !== "ENOENT")
1602
- throw error;
1603
- }
1604
- const parent = path3.dirname(candidate);
1605
- if (parent === candidate) {
1606
- throw new Error(`No .tonbo declaration found above ${path3.resolve(start)}.`);
1607
- }
1608
- candidate = parent;
1609
- }
1610
- }
1611
- async function sourceIgnore(root) {
1612
- const matcher = ignore().add(DEFAULT_IGNORES);
1613
- try {
1614
- matcher.add(await readFile3(path3.join(root, ".tonboignore"), "utf8"));
1615
- } catch (error) {
1616
- if (error.code !== "ENOENT")
1617
- throw error;
1618
- }
1619
- return matcher;
1620
- }
1621
- async function collectFiles(root) {
1622
- const matcher = await sourceIgnore(root);
1623
- const files = [];
1624
- async function visit(directory, relativeDirectory) {
1625
- const entries = await readdir(directory, { withFileTypes: true });
1626
- entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
1627
- for (const entry of entries) {
1628
- const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
1629
- const ignored = matcher.ignores(relative + (entry.isDirectory() ? "/" : ""));
1630
- if (ignored)
1631
- continue;
1632
- if (relative === SESSION_SOURCE_DIRECTORY || relative.startsWith(`${SESSION_SOURCE_DIRECTORY}/`)) {
1633
- throw new Error(`Source path ${SESSION_SOURCE_DIRECTORY}/ is reserved for durable PI history. Rename it or exclude it with .tonboignore.`);
1634
- }
1635
- const absolute = path3.join(directory, entry.name);
1636
- if (entry.isDirectory()) {
1637
- await visit(absolute, relative);
1638
- continue;
1639
- }
1640
- const metadata = await lstat3(absolute);
1641
- if (!metadata.isFile()) {
1642
- throw new Error(`Source path ${relative} is not a regular file. V1 does not follow symlinks or special files.`);
1643
- }
1644
- files.push({
1645
- absolute,
1646
- mode: metadata.mode & 73 ? 493 : 420,
1647
- relative,
1648
- size: metadata.size
1649
- });
1650
- }
1651
- }
1652
- await visit(root, "");
1653
- if (!files.some((file) => file.relative === ".tonbo")) {
1654
- throw new Error("The source bundle must contain .tonbo.");
1655
- }
1656
- return files;
1657
- }
1658
- function addEntry(pack, file, contents) {
1659
- return new Promise((resolve, reject) => {
1660
- pack.entry({
1661
- gid: 0,
1662
- mode: file.mode,
1663
- mtime: /* @__PURE__ */ new Date(0),
1664
- name: file.relative,
1665
- size: contents.length,
1666
- type: "file",
1667
- uid: 0
1668
- }, contents, (error) => error ? reject(error) : resolve());
1669
- });
1670
- }
1671
- async function buildSourceBundle(root) {
1672
- const resolvedRoot = path3.resolve(root);
1673
- await validatePiPackages(resolvedRoot);
1674
- const files = await collectFiles(resolvedRoot);
1675
- const payloadBytes = files.reduce((total, file) => total + file.size, 0);
1676
- if (payloadBytes > MAX_BUNDLE_BYTES) {
1677
- throw new Error(`Source files exceed ${MAX_BUNDLE_BYTES} bytes after ignore rules.`);
1678
- }
1679
- const pack = tar.pack();
1680
- const chunks = [];
1681
- let size = 0;
1682
- pack.on("data", (chunk) => {
1683
- size += chunk.length;
1684
- if (size > MAX_BUNDLE_BYTES) {
1685
- pack.destroy(new Error(`Source bundle exceeds ${MAX_BUNDLE_BYTES} bytes after ignore rules.`));
1686
- return;
1687
- }
1688
- chunks.push(chunk);
1689
- });
1690
- const completed = new Promise((resolve, reject) => {
1691
- pack.on("end", resolve);
1692
- pack.on("error", reject);
1693
- });
1694
- for (const file of files) {
1695
- await addEntry(pack, file, await readFile3(file.absolute));
1696
- }
1697
- pack.finalize();
1698
- await completed;
1699
- const bytes = Buffer.concat(chunks);
1700
- return {
1701
- bytes,
1702
- format: sourceBundleContract.format,
1703
- root: resolvedRoot,
1704
- sha256: createHash2("sha256").update(bytes).digest("hex"),
1705
- size_bytes: bytes.length
1706
- };
122
+ </style>
123
+ </head>
124
+ <body>
125
+ <header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
126
+ <main>
127
+ <h1>${title}</h1>
128
+ <p>${body}</p>
129
+ <hr>
130
+ </main>
131
+ </body>
132
+ </html>
133
+ `;
1707
134
  }
1708
-
1709
- // src/ssh-key.ts
1710
- import { createHash as createHash3 } from "node:crypto";
1711
- import { readFile as readFile4 } from "node:fs/promises";
1712
- import { homedir } from "node:os";
1713
- import { basename, join } from "node:path";
1714
- var DEFAULT_PUBLIC_KEYS = ["id_ed25519.pub", "id_ecdsa.pub", "id_rsa.pub"];
1715
- var ALGORITHMS = /* @__PURE__ */ new Set(["ssh-ed25519", "ecdsa-sha2-nistp256", "ssh-rsa"]);
1716
- function parseOpenSshPublicKey(value, label) {
1717
- const fields = value.trim().split(/\s+/);
1718
- if (fields.length < 2 || !ALGORITHMS.has(fields[0]))
1719
- throw new Error("SSH public key must be Ed25519, ECDSA P-256, or RSA.");
1720
- const blob = Buffer.from(fields[1], "base64");
1721
- if (blob.length < 32 || blob.length > 16384 || blob.toString("base64") !== fields[1])
1722
- throw new Error("SSH public key is not canonical base64.");
135
+ function callbackResponse(outcome) {
1723
136
  return {
1724
- algorithm: fields[0],
1725
- fingerprint: `SHA256:${createHash3("sha256").update(blob).digest("base64").replace(/=$/, "")}`,
1726
- keyBase64: fields[1],
1727
- label
137
+ status: OUTCOMES[outcome].status,
138
+ headers: {
139
+ "content-type": "text/html; charset=utf-8",
140
+ "cache-control": "no-store"
141
+ },
142
+ body: renderCallbackPage(outcome)
1728
143
  };
1729
144
  }
1730
- async function readSshPublicKey(path6) {
1731
- return parseOpenSshPublicKey(await readFile4(path6, "utf8"), basename(path6, ".pub"));
1732
- }
1733
- async function readDefaultSshPublicKeys(sshDirectory = join(homedir(), ".ssh")) {
1734
- const keys = [];
1735
- for (const name of DEFAULT_PUBLIC_KEYS) {
1736
- const key = await readSshPublicKey(join(sshDirectory, name)).catch(
1737
- (error) => {
1738
- if (error.code === "ENOENT") return null;
1739
- throw error;
1740
- }
1741
- );
1742
- if (key) keys.push(key);
1743
- }
1744
- return keys;
1745
- }
1746
145
 
1747
- // src/agent-name.ts
1748
- var AGENT_NAME_RULE = agentNameContract.$defs.name;
1749
- var AGENT_NAME_PATTERN = new RegExp(AGENT_NAME_RULE.pattern);
1750
- var RESERVED_AGENT_NAMES = new Set(AGENT_NAME_RULE.not.enum);
1751
- function validateAgentName(name) {
1752
- const valid = name.length >= AGENT_NAME_RULE.minLength && name.length <= AGENT_NAME_RULE.maxLength && AGENT_NAME_PATTERN.test(name) && !RESERVED_AGENT_NAMES.has(name);
1753
- if (!valid) {
1754
- throw new Error(
1755
- `Agent name must be ${AGENT_NAME_RULE.minLength}-${AGENT_NAME_RULE.maxLength} lowercase letters, digits, or single hyphens, start with a letter, and not be reserved.`
1756
- );
1757
- }
1758
- }
1759
- function agentNameSuggestion(value) {
1760
- let name = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1761
- if (!/^[a-z]/.test(name)) name = `agent-${name}`;
1762
- name = name.slice(0, AGENT_NAME_RULE.maxLength).replace(/-+$/g, "");
1763
- if (name.length < AGENT_NAME_RULE.minLength) {
1764
- name = `${name || "agent"}-agent`.slice(0, AGENT_NAME_RULE.maxLength);
146
+ // ../../packages/tonbo-cli-core/dist/src/http.js
147
+ var HttpError = class extends Error {
148
+ status;
149
+ body;
150
+ requestId;
151
+ constructor(message, status, body, requestId) {
152
+ super(message);
153
+ this.status = status;
154
+ this.body = body;
155
+ this.requestId = requestId;
1765
156
  }
1766
- if (RESERVED_AGENT_NAMES.has(name)) name = `${name}-agent`;
1767
- return name;
1768
- }
1769
-
1770
- // src/turns.ts
1771
- var TURN_OUTCOME_GRACE_MS = 3 * 60 * 1e3;
1772
- var POLL_MIN_MS = 1e3;
1773
- var POLL_MAX_MS = 1e4;
1774
- var READ_FAILURE_LIMIT = 10;
1775
- var TurnFailedError = class extends Error {
1776
- constructor(turnId, failureCode, detail) {
1777
- super(
1778
- `Turn ${turnId} failed${failureCode ? ` (${failureCode})` : ""}${detail ? `: ${detail}` : "."}`
1779
- );
1780
- this.turnId = turnId;
1781
- this.failureCode = failureCode;
1782
- this.detail = detail;
157
+ /** The problem `code` of a Management API refusal, such as `unauthenticated`. */
158
+ get code() {
159
+ const record = this.body && typeof this.body === "object" ? this.body : {};
160
+ const code = record.code;
161
+ return typeof code === "string" ? code : null;
1783
162
  }
1784
163
  };
1785
- function problemCode(error) {
1786
- if (!(error instanceof HttpError)) return null;
1787
- const body = error.body && typeof error.body === "object" ? error.body : {};
1788
- const code = body.code;
1789
- return typeof code === "string" ? code : null;
1790
- }
1791
- function submissionProblem(error, turnId) {
1792
- if (!(error instanceof HttpError)) return error;
1793
- const body = error.body ?? {};
1794
- const code = problemCode(error);
1795
- if (error.status === 409 && code === "turn_failed")
1796
- return new TurnFailedError(
1797
- turnId,
1798
- typeof body.failure_code === "string" ? body.failure_code : null,
1799
- typeof body.detail === "string" ? body.detail : null
1800
- );
1801
- if (error.status === 409 && code === "agent_busy")
1802
- return new Error(
1803
- `Turn ${turnId} was not admitted: the Agent already has the most Turns waiting to start (agent_busy). Try again with a new Turn later.`,
1804
- { cause: error }
1805
- );
1806
- return new Error(`Turn ${turnId} was not admitted: ${error.message}`, { cause: error });
1807
- }
1808
- function isUnauthenticated(error) {
1809
- return error instanceof HttpError && error.status === 401;
1810
- }
1811
- function isNotFound(error) {
1812
- return error instanceof HttpError && error.status === 404;
1813
- }
1814
- function isTransient(error) {
1815
- if (error instanceof HttpError) return error.status === 429 || error.status >= 500;
1816
- return error instanceof TypeError;
1817
- }
1818
- function turnWaitBound(turn) {
1819
- const deadline = turn.deadline_at ?? turn.start_deadline_at;
1820
- if (!deadline) return null;
1821
- const at = Date.parse(deadline);
1822
- return Number.isFinite(at) ? at + TURN_OUTCOME_GRACE_MS : null;
1823
- }
1824
- async function followTurn({
1825
- now,
1826
- read: read2,
1827
- resume,
1828
- sleep,
1829
- turnId
1830
- }) {
1831
- let delay = POLL_MIN_MS;
1832
- let failures = 0;
1833
- for (; ; ) {
1834
- let state;
1835
- try {
1836
- state = await read2();
1837
- failures = 0;
1838
- } catch (error) {
1839
- const transient = isTransient(error);
1840
- if (transient && (failures += 1) < READ_FAILURE_LIMIT) {
1841
- await sleep(POLL_MAX_MS);
1842
- continue;
1843
- }
1844
- if (!transient && !(error instanceof HttpError)) throw error;
1845
- throw new Error(
1846
- `${error instanceof Error ? error.message : String(error)} Waiting stopped, which does not stop the Turn; ${resume}`,
1847
- { cause: error }
1848
- );
1849
- }
1850
- if ("result" in state) return state.result;
1851
- const bound = state.pending ? turnWaitBound(state.pending) : null;
1852
- if (bound !== null && now() > bound)
1853
- throw new Error(
1854
- `Turn ${turnId} is still pending after its deadline, and the platform has not recorded its outcome. Waiting stopped, which does not stop the Turn; ${resume}`
1855
- );
1856
- await sleep(delay);
1857
- delay = Math.min(delay * 2, POLL_MAX_MS);
1858
- }
164
+ async function requestJson(fetcher, url, init = {}) {
165
+ const response = await fetcher(url, init);
166
+ const body = await response.json().catch(() => null);
167
+ if (response.ok)
168
+ return body;
169
+ const record = body && typeof body === "object" ? body : {};
170
+ const reason = [record.detail, record.title, record.error].find((value) => typeof value === "string" && value.length > 0);
171
+ const requestId = response.headers.get("x-request-id") ?? (typeof record.request_id === "string" ? record.request_id : null);
172
+ const issues = Array.isArray(record.errors) ? record.errors.filter((issue) => typeof issue?.field === "string" && typeof issue.code === "string").map((issue) => `${issue.field}: ${issue.code}`) : [];
173
+ const message = [
174
+ reason ?? `Request failed with HTTP ${response.status}.`,
175
+ ...issues.length ? [`(${issues.join(", ")})`] : []
176
+ ].join(" ");
177
+ throw new HttpError(requestId ? `${message} (request ${requestId})` : message, response.status, body, requestId);
1859
178
  }
1860
179
 
1861
- // src/commands.ts
1862
- async function resolveAgent(deps, minimumTokenLifetimeSeconds) {
1863
- const root = await findDeclarationRoot(deps.cwd());
1864
- const declaration = await loadDeclaration(root);
1865
- if (!declaration.agent) {
1866
- throw new Error("No Agent is configured in .tonbo. Run `tonbo agent use <agent>` first.");
1867
- }
1868
- const oauthToken = await deps.auth.accessToken(minimumTokenLifetimeSeconds);
1869
- return {
1870
- declaration,
1871
- oauthToken,
1872
- agent: selectAgent(await deps.api.listAgents(oauthToken), declaration.agent),
1873
- root
1874
- };
180
+ // ../../packages/tonbo-cli-core/dist/src/auth.js
181
+ var exec = promisify(execFile);
182
+ var DEFAULT_LOGIN_HINT = "tonbo login";
183
+ var LOOPBACK_HOST = "127.0.0.1";
184
+ function loginHostname(hostname = os.hostname()) {
185
+ const cleaned = hostname.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
186
+ return cleaned || "computer";
1875
187
  }
1876
- function selectAgent(agents, selector) {
1877
- const normalized = selector.toLowerCase();
1878
- const matches = agents.filter(
1879
- (agent) => agent.id === selector || agent.name === normalized || agent.publicHostname === normalized
1880
- );
1881
- if (matches.length === 0) throw new Error(`Agent ${selector} was not found in your account.`);
1882
- if (matches.length > 1)
1883
- throw new Error(`Agent name ${selector} is ambiguous; use its full hostname or ID.`);
1884
- if (matches[0].status !== "active") throw new Error(`Agent ${selector} is not active.`);
1885
- return matches[0];
1886
- }
1887
- async function initCommand(deps, options) {
1888
- const root = deps.cwd();
1889
- const exists = await declarationExists(root);
1890
- let existingDeclaration;
1891
- if (exists) {
1892
- existingDeclaration = await loadDeclaration(root).catch(() => void 0);
1893
- }
1894
- const interactive = deps.interactive();
1895
- const overwrite = options.force === true || exists && interactive;
1896
- if (exists && !overwrite)
1897
- throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1898
- if (options.region !== void 0 && !interactive)
1899
- throw new Error(
1900
- "--region applies only to an agent that interactive init creates. Run `tonbo agent create <name> --region <region>` instead."
1901
- );
1902
- const inspection = await deps.inspectSource(root);
1903
- let harness = options.harness ?? (existingDeclaration ? "pi" : void 0);
1904
- if (harness !== void 0 && harness !== "pi") {
1905
- throw new Error("--harness must name a supported Harness: pi.");
1906
- }
1907
- let driver = options.driver ?? existingDeclaration?.harness.driver.kind;
1908
- if (driver !== void 0 && driver !== "native" && driver !== "command") {
1909
- throw new Error("--driver must be native or command.");
188
+ var AuthClient = class {
189
+ credentials;
190
+ fetcher;
191
+ loginOrigin;
192
+ browserOpener;
193
+ hostname;
194
+ loginHint;
195
+ constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT) {
196
+ this.credentials = credentials;
197
+ this.fetcher = fetcher;
198
+ this.loginOrigin = loginOrigin2;
199
+ this.browserOpener = browserOpener;
200
+ this.hostname = hostname;
201
+ this.loginHint = loginHint;
202
+ }
203
+ /** The personal key requests are made with: TONBO_API_KEY, or the saved
204
+ * login while it has not expired. */
205
+ async apiKey() {
206
+ const injected = process.env.TONBO_API_KEY?.trim();
207
+ if (injected)
208
+ return injected;
209
+ const login = await this.credentials.load();
210
+ if (!login)
211
+ throw new Error(`Not logged in. Run \`${this.loginHint}\`.`);
212
+ if (login.expires_at && Date.parse(login.expires_at) <= Date.now())
213
+ throw new Error(`Your Tonbo login has expired. Run \`${this.loginHint}\`.`);
214
+ return login.key;
215
+ }
216
+ /**
217
+ * The browser handoff of /cli/login: PKCE S256 with a loopback redirect on
218
+ * a free port, then the one-time code is redeemed for a personal key named
219
+ * "CLI on <hostname>", which replaces that host's earlier login key.
220
+ */
221
+ async login(progress = () => {
222
+ }, account) {
223
+ const verifier = randomBytes(48).toString("base64url");
224
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
225
+ const state = randomBytes(24).toString("base64url");
226
+ const code = await this.listenForCode(state, (redirectUri) => {
227
+ const authorization = new URL("/cli/login", this.loginOrigin);
228
+ authorization.search = new URLSearchParams({
229
+ code_challenge: challenge,
230
+ code_challenge_method: "S256",
231
+ redirect_uri: redirectUri,
232
+ state,
233
+ hostname: this.hostname,
234
+ ...account ? { account } : {}
235
+ }).toString();
236
+ return this.browserOpener(authorization.toString());
237
+ }, progress);
238
+ progress({ status: "started", step: "key-redemption" });
239
+ const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
240
+ method: "POST",
241
+ headers: { "content-type": "application/json" },
242
+ body: JSON.stringify({ code, code_verifier: verifier })
243
+ });
244
+ progress({ status: "completed", step: "key-redemption" });
245
+ const login = {
246
+ version: 1,
247
+ account_id: redeemed.account_id,
248
+ key_id: redeemed.api_key.id,
249
+ key: redeemed.key,
250
+ expires_at: redeemed.api_key.expires_at
251
+ };
252
+ progress({ status: "started", step: "credential-store" });
253
+ await this.credentials.save(login);
254
+ progress({ status: "completed", step: "credential-store" });
255
+ return login;
1910
256
  }
1911
- if (!harness) {
1912
- if (inspection.harness.state === "identified") harness = inspection.harness.id;
1913
- else if (!interactive) {
1914
- throw new Error("No Harness-specific configuration found. Pass --harness pi.");
1915
- } else {
1916
- deps.output({
1917
- message: "No Harness-specific configuration was found in this agent."
257
+ listenForCode(expectedState, ready, progress) {
258
+ return new Promise((resolve, reject) => {
259
+ let settled = false;
260
+ const sockets = /* @__PURE__ */ new Set();
261
+ let authorizationStarted = false;
262
+ const startAuthorization = () => {
263
+ if (authorizationStarted)
264
+ return;
265
+ authorizationStarted = true;
266
+ progress({ status: "completed", step: "browser" });
267
+ progress({ status: "started", step: "browser-authorization" });
268
+ };
269
+ const finish = (result, completedResponseSocket) => {
270
+ if (settled)
271
+ return;
272
+ settled = true;
273
+ clearTimeout(timeout);
274
+ const complete = (closeError) => {
275
+ if (closeError)
276
+ reject(closeError);
277
+ else if ("code" in result) {
278
+ progress({ status: "completed", step: "callback-close" });
279
+ resolve(result.code);
280
+ } else
281
+ reject(result.error);
282
+ };
283
+ if ("code" in result)
284
+ progress({ status: "started", step: "callback-close" });
285
+ if (!server.listening) {
286
+ complete();
287
+ return;
288
+ }
289
+ server.close(complete);
290
+ for (const socket of sockets) {
291
+ if (socket !== completedResponseSocket)
292
+ socket.destroy();
293
+ }
294
+ server.closeIdleConnections();
295
+ };
296
+ const timeout = setTimeout(() => {
297
+ finish({ error: new Error("Timed out waiting for browser login.") });
298
+ }, 5 * 60 * 1e3);
299
+ const server = createServer((request, response) => {
300
+ const url = new URL(request.url ?? "/", `http://${LOOPBACK_HOST}`);
301
+ if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState) {
302
+ respond(response, "invalid");
303
+ return;
304
+ }
305
+ const denied = url.searchParams.get("error");
306
+ if (denied) {
307
+ const responseSocket2 = response.socket;
308
+ respond(response, "denied", () => finish({ error: new Error(`The login was not approved (${denied}).`) }, responseSocket2));
309
+ return;
310
+ }
311
+ const code = url.searchParams.get("code");
312
+ if (!code) {
313
+ respond(response, "invalid");
314
+ return;
315
+ }
316
+ startAuthorization();
317
+ progress({ status: "completed", step: "browser-authorization" });
318
+ const responseSocket = response.socket;
319
+ respond(response, "complete", () => finish({ code }, responseSocket));
1918
320
  });
1919
- harness = await selectHarness(deps);
1920
- }
1921
- }
1922
- if (!driver) {
1923
- if (!interactive) {
1924
- throw new Error("PI execution mode is required. Pass --driver native or --driver command.");
1925
- }
1926
- if (inspection.pi.settingsFound) {
1927
- const packageSummary = inspection.pi.packageCount === 0 ? "Found PI configuration at .pi/settings.json." : `Found PI configuration at .pi/settings.json with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}.`;
1928
- deps.output({ message: packageSummary });
1929
- }
1930
- driver = "native";
1931
- }
1932
- if (driver === "native" && (options.agentEntry || options.buildCommand)) {
1933
- throw new Error("--agent-entry and --build-command require --driver command.");
1934
- }
1935
- const existingEntry = existingDeclaration?.harness.driver.kind === "command" && existingDeclaration.harness.driver.command[0] === "node" ? existingDeclaration.harness.driver.command[1] : void 0;
1936
- let entry = options.agentEntry?.trim() || existingEntry;
1937
- if (driver === "command" && !entry && interactive) {
1938
- entry = (await deps.prompt("PI SDK entry file [dist/agent.mjs]: ")).trim();
1939
- }
1940
- entry ||= "dist/agent.mjs";
1941
- let model = options.model?.trim() || existingDeclaration?.inference.model || DEFAULT_INFERENCE_MODEL;
1942
- let buildCommand = driver === "command" ? options.buildCommand ?? existingDeclaration?.build?.command ?? ["npm", "run", "build"] : void 0;
1943
- let agentPlan = existingDeclaration?.agent ? {
1944
- kind: "existing",
1945
- publicHostname: existingDeclaration.agent
1946
- } : { kind: "later" };
1947
- if (interactive && !existingDeclaration?.agent) {
1948
- agentPlan = await selectInitAgent(deps, root, options.region);
1949
- }
1950
- if (interactive) {
1951
- while (true) {
1952
- deps.output({
1953
- message: renderInitSummary(agentPlan, harness, driver, model, entry, buildCommand)
321
+ server.on("connection", (socket) => {
322
+ sockets.add(socket);
323
+ socket.once("close", () => sockets.delete(socket));
324
+ });
325
+ server.once("error", (error) => finish({ error }));
326
+ progress({ status: "started", step: "callback-server" });
327
+ server.listen(0, LOOPBACK_HOST, () => {
328
+ const { port } = server.address();
329
+ progress({ status: "completed", step: "callback-server" });
330
+ progress({ status: "started", step: "browser" });
331
+ void ready(`http://${LOOPBACK_HOST}:${port}/callback`).then(startAuthorization).catch((error) => finish({
332
+ error: error instanceof Error ? error : new Error("Could not open the login browser.")
333
+ }));
1954
334
  });
1955
- const saveLabel = initSaveLabel(exists, agentPlan);
1956
- const action = await deps.select(`${saveLabel} with this configuration?`, [
1957
- {
1958
- name: saveLabel,
1959
- value: "save"
1960
- },
1961
- {
1962
- description: "Change the agent, Harness, execution mode, model, or SDK entrypoint.",
1963
- name: "Edit configuration",
1964
- value: "edit"
1965
- },
1966
- { name: "Cancel", value: "cancel" }
1967
- ]);
1968
- if (action === "cancel") {
1969
- deps.output({
1970
- message: exists ? `Cancelled. Kept existing ${DECLARATION_FILENAME}.` : `Cancelled. Did not create ${DECLARATION_FILENAME}.`
1971
- });
1972
- return;
1973
- }
1974
- if (action === "save") break;
1975
- const setting = await deps.select("What would you like to change?", [
1976
- {
1977
- description: initAgentSummary(agentPlan),
1978
- name: "Agent",
1979
- value: "agent"
1980
- },
1981
- { description: harnessName(harness), name: "Harness", value: "harness" },
1982
- {
1983
- description: driver === "native" ? "PI CLI (`pi`)" : "PI SDK app",
1984
- name: "Execution mode",
1985
- value: "driver"
1986
- },
1987
- { description: model, name: "Inference model", value: "model" },
1988
- ...driver === "command" ? [{ description: entry, name: "PI SDK entry file", value: "entry" }] : [],
1989
- { name: "Back to review", value: "back" }
1990
- ]);
1991
- if (setting === "agent") agentPlan = await selectInitAgent(deps, root, options.region);
1992
- if (setting === "harness") harness = await selectHarness(deps);
1993
- if (setting === "driver") {
1994
- driver = await deps.select(
1995
- "How should Tonbo start PI?",
1996
- piTargetChoices(inspection.pi.settingsFound)
1997
- );
1998
- if (driver === "command") {
1999
- const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
2000
- if (answer) entry = answer;
2001
- buildCommand = options.buildCommand ?? ["npm", "run", "build"];
2002
- } else {
2003
- buildCommand = void 0;
2004
- }
2005
- }
2006
- if (setting === "model") {
2007
- const answer = (await deps.prompt(`Inference model [${model}]: `)).trim();
2008
- if (answer) model = answer;
2009
- }
2010
- if (setting === "entry") {
2011
- const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
2012
- if (answer) entry = answer;
2013
- }
2014
- }
2015
- }
2016
- let agentHostname = agentPlan.kind === "existing" ? agentPlan.publicHostname : void 0;
2017
- let createdAgent;
2018
- if (agentPlan.kind === "create") {
2019
- const oauthToken = await deps.auth.accessToken();
2020
- createdAgent = await deps.api.createAgent(oauthToken, {
2021
- name: agentPlan.name,
2022
- organizationId: agentPlan.organizationId,
2023
- region: agentPlan.region
2024
335
  });
2025
- agentHostname = createdAgent.publicHostname;
2026
- }
2027
- const configuredDeclaration = createDeclaration(
2028
- model,
2029
- driver === "native" ? { kind: "native" } : { kind: "command", protocol: "pi-rpc-v1", command: ["node", entry] },
2030
- buildCommand,
2031
- agentHostname
2032
- );
2033
- const declaration = parseDeclaration({
2034
- ...configuredDeclaration,
2035
- harness: {
2036
- ...configuredDeclaration.harness,
2037
- ...existingDeclaration?.harness.secrets ? { secrets: existingDeclaration.harness.secrets } : {}
2038
- },
2039
- ...existingDeclaration?.service ? { service: existingDeclaration.service } : {}
2040
- });
2041
- try {
2042
- await saveDeclaration(root, declaration, overwrite);
2043
- } catch (error) {
2044
- if (createdAgent) {
2045
- throw new Error(
2046
- `Created agent ${createdAgent.name}, but could not write ${DECLARATION_FILENAME}. Fix the file and run \`tonbo agent use ${createdAgent.publicHostname}\`.`,
2047
- { cause: error }
2048
- );
2049
- }
2050
- throw error;
2051
336
  }
2052
- const driverSummary = driver === "command" ? `PI SDK app
2053
- Build: ${buildCommand?.join(" ")}
2054
- Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
2055
- const agentSummary = createdAgent ? `Agent: ${createdAgent.name} (${createdAgent.publicHostname})
2056
- Organization: ${createdAgent.organizationName}
2057
- Region: ${createdAgent.region}
2058
- Application: https://${createdAgent.publicHostname}
2059
- SSH after deploy: ssh ${createdAgent.sshDestination}` : agentPlan.kind === "existing" ? `Agent: ${agentPlan.name ? `${agentPlan.name} (${agentPlan.publicHostname})` : agentPlan.publicHostname}${agentPlan.organizationName ? `
2060
- Organization: ${agentPlan.organizationName}` : ""}${agentPlan.region ? `
2061
- Region: ${agentPlan.region}` : ""}${agentPlan.sshDestination ? `
2062
- SSH after deploy: ssh ${agentPlan.sshDestination}` : ""}` : "Next: tonbo agent create <name>";
2063
- deps.output({
2064
- message: `${createdAgent ? `Created agent ${createdAgent.name} and` : exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
2065
- ${agentSummary}
2066
- Harness: PI
2067
- Mode: ${driverSummary}${agentPlan.kind === "later" ? "" : "\nNext: tonbo deploy"}`,
2068
- declaration,
2069
- path: path4.join(root, DECLARATION_FILENAME)
2070
- });
337
+ };
338
+ function respond(response, outcome, finished) {
339
+ const page = callbackResponse(outcome);
340
+ if (finished)
341
+ response.once("finish", finished);
342
+ response.writeHead(page.status, { ...page.headers, connection: "close" });
343
+ response.end(page.body);
2071
344
  }
2072
- function initSaveLabel(exists, agent) {
2073
- if (agent.kind === "create") {
2074
- return exists ? `Create agent and replace ${DECLARATION_FILENAME}` : `Create agent and ${DECLARATION_FILENAME}`;
2075
- }
2076
- return exists ? `Replace ${DECLARATION_FILENAME}` : `Create ${DECLARATION_FILENAME}`;
2077
- }
2078
- async function selectInitAgent(deps, root, requestedRegion) {
2079
- for (; ; ) {
2080
- const action = await deps.select("How should this directory connect to Tonbo?", [
2081
- {
2082
- description: "Create it only after you confirm the complete configuration.",
2083
- name: "Create a new agent",
2084
- value: "create"
2085
- },
2086
- {
2087
- description: "Bind this source tree to one agent you can access.",
2088
- name: "Use an existing agent",
2089
- value: "existing"
2090
- },
2091
- {
2092
- description: "Write an unbound declaration and configure its agent later.",
2093
- name: "Set up later",
2094
- value: "later"
345
+ async function openBrowser(url) {
346
+ if (process.platform === "darwin")
347
+ return void await exec("open", [url]);
348
+ if (process.platform === "win32")
349
+ return void await exec("rundll32", ["url.dll,FileProtocolHandler", url]);
350
+ return void await exec("xdg-open", [url]);
351
+ }
352
+
353
+ // ../../packages/tonbo-cli-core/dist/src/generated/contracts.js
354
+ var agentNameContract = {
355
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
356
+ "$id": "https://contracts.tonbo.dev/agents/agent-name-v1.json",
357
+ "x-tonbo-version": 1,
358
+ "x-tonbo-public-hostname-apex": "tonbo.sh",
359
+ "x-tonbo-public-hostname-template": "<agent>-<organization>-<word>-<word>.tonbo.sh",
360
+ "$defs": {
361
+ "name": {
362
+ "type": "string",
363
+ "minLength": 1,
364
+ "maxLength": 40,
365
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
366
+ "not": {
367
+ "enum": [
368
+ "api",
369
+ "artifacts",
370
+ "auth",
371
+ "inference",
372
+ "network-health",
373
+ "sandbox-control",
374
+ "status",
375
+ "streams",
376
+ "www"
377
+ ]
2095
378
  }
2096
- ]);
2097
- if (action === "later") return { kind: "later" };
2098
- if (action === "create") {
2099
- const organization = await selectInitOrganization(deps);
2100
- const region = requestedRegion ?? await selectAgentRegion(deps);
2101
- const defaultName = agentNameSuggestion(path4.basename(root)) || "tonbo-agent";
2102
- for (; ; ) {
2103
- const answer = (await deps.prompt(`Agent name [${defaultName}]: `)).trim().toLowerCase();
2104
- const name = answer || defaultName;
2105
- try {
2106
- validateAgentName(name);
2107
- return {
2108
- kind: "create",
2109
- name,
2110
- organizationId: organization?.id,
2111
- organizationName: organization?.name,
2112
- ...region ? { region } : {}
2113
- };
2114
- } catch (error) {
2115
- deps.output({
2116
- message: error instanceof Error ? error.message : "Agent name is invalid."
2117
- });
2118
- }
379
+ },
380
+ "publicHostname": {
381
+ "type": "string",
382
+ "minLength": 16,
383
+ "maxLength": 72,
384
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)+\\.tonbo\\.sh$",
385
+ "not": {
386
+ "enum": [
387
+ "api.tonbo.sh",
388
+ "artifacts.tonbo.sh",
389
+ "auth.tonbo.sh",
390
+ "inference.tonbo.sh",
391
+ "network-health.tonbo.sh",
392
+ "sandbox-control.tonbo.sh",
393
+ "status.tonbo.sh",
394
+ "streams.tonbo.sh",
395
+ "www.tonbo.sh"
396
+ ]
2119
397
  }
2120
- }
2121
- const oauthToken = await deps.auth.accessToken();
2122
- const agents = (await deps.api.listAgents(oauthToken)).filter(
2123
- (agent2) => agent2.status === "active"
2124
- );
2125
- if (agents.length === 0) {
2126
- deps.output({ message: "No active agents are available. Create a new agent instead." });
2127
- continue;
2128
- }
2129
- const id = await deps.select(
2130
- "Which agent should this directory use?",
2131
- agents.map((agent2) => ({
2132
- name: agent2.name,
2133
- description: `Organization: ${agent2.organizationName} \xB7 ${agent2.region} \xB7 ${agent2.publicHostname} \xB7 ${agent2.id}`,
2134
- value: agent2.id
2135
- }))
2136
- );
2137
- const agent = selectAgent(agents, id);
2138
- return {
2139
- kind: "existing",
2140
- name: agent.name,
2141
- organizationName: agent.organizationName,
2142
- publicHostname: agent.publicHostname,
2143
- region: agent.region,
2144
- sshDestination: agent.sshDestination
2145
- };
2146
- }
2147
- }
2148
- async function selectAgentRegion(deps) {
2149
- const { regions } = await deps.api.listRegions(await deps.auth.accessToken());
2150
- const open3 = regions.filter((region) => region.allocation_enabled);
2151
- if (open3.length < 2) return void 0;
2152
- return deps.select(
2153
- "Which region should hold the new agent? It stays there permanently.",
2154
- open3.map((region) => ({ description: region.id, name: region.display_name, value: region.id }))
2155
- );
2156
- }
2157
- function initAgentSummary(agent) {
2158
- if (agent.kind === "create")
2159
- return `Create ${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""}${agent.region ? `, region ${agent.region}` : ""} (permanent address assigned after creation)`;
2160
- if (agent.kind === "existing") {
2161
- return agent.name ? `${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""} (${agent.publicHostname})` : `Keep ${agent.publicHostname}`;
2162
- }
2163
- return "Set up later";
2164
- }
2165
- async function selectInitOrganization(deps) {
2166
- const oauthToken = await deps.auth.accessToken();
2167
- const organizations = (await deps.api.listOrganizations(oauthToken)).filter(
2168
- (organization) => organization.role !== "member"
2169
- );
2170
- if (organizations.length === 0)
2171
- throw new Error("No organization lets you create a agent. Ask an admin or owner.");
2172
- if (organizations.length === 1) return organizations[0];
2173
- const id = await deps.select(
2174
- "Which organization should own the new agent?",
2175
- organizations.map((organization) => ({
2176
- description: organization.role,
2177
- name: organization.name,
2178
- value: organization.id
2179
- }))
2180
- );
2181
- return organizations.find((organization) => organization.id === id) ?? organizations[0];
2182
- }
2183
- async function selectHarness(deps) {
2184
- return await deps.select(
2185
- "Which supported Harness should Tonbo use?",
2186
- supportedHarnesses.map((supported) => ({
2187
- description: `Run this agent with the ${supported.command} command.`,
2188
- name: `${supported.name} (\`${supported.command}\`)`,
2189
- value: supported.id
2190
- }))
2191
- );
2192
- }
2193
- function harnessName(harness) {
2194
- const supported = supportedHarnesses.find((candidate) => candidate.id === harness);
2195
- return supported?.name ?? harness;
2196
- }
2197
- function renderInitSummary(agent, harness, driver, model, entry, buildCommand) {
2198
- const lines = [
2199
- "",
2200
- "Tonbo Agent configuration:",
2201
- ` Agent: ${initAgentSummary(agent)}`,
2202
- ` Harness: ${harnessName(harness)}`,
2203
- ` Execution: ${driver === "native" ? "PI CLI (`pi`)" : "PI SDK app"}`,
2204
- ` Model: ${model}`
2205
- ];
2206
- if (driver === "command") {
2207
- lines.push(` Build: ${buildCommand?.join(" ") ?? "none"}`);
2208
- lines.push(` Entrypoint: node ${entry}`);
2209
- }
2210
- return lines.join("\n");
2211
- }
2212
- function piTargetChoices(settingsFound) {
2213
- const choices = {
2214
- command: {
2215
- description: "Run a custom Node.js entrypoint built on the PI SDK.",
2216
- name: "PI SDK app",
2217
- value: "command"
2218
398
  },
2219
- native: {
2220
- description: settingsFound ? "Run pi directly and load the detected agent-local PI configuration." : "Run pi directly with AGENTS.md and optional agent-local .pi resources.",
2221
- name: "PI CLI (`pi`)",
2222
- value: "native"
399
+ "hostnameWord": {
400
+ "type": "string",
401
+ "enum": [
402
+ "arch",
403
+ "bark",
404
+ "beam",
405
+ "bell",
406
+ "bird",
407
+ "blue",
408
+ "boat",
409
+ "bold",
410
+ "book",
411
+ "calm",
412
+ "cave",
413
+ "clay",
414
+ "coal",
415
+ "cool",
416
+ "cove",
417
+ "dawn",
418
+ "deer",
419
+ "dove",
420
+ "dune",
421
+ "dusk",
422
+ "echo",
423
+ "fern",
424
+ "fire",
425
+ "flax",
426
+ "flow",
427
+ "foam",
428
+ "fawn",
429
+ "frog",
430
+ "gate",
431
+ "glow",
432
+ "gold",
433
+ "gray",
434
+ "gulf",
435
+ "hail",
436
+ "halo",
437
+ "hare",
438
+ "hawk",
439
+ "hill",
440
+ "iris",
441
+ "jade",
442
+ "kite",
443
+ "lake",
444
+ "lamb",
445
+ "leaf",
446
+ "lime",
447
+ "lion",
448
+ "lobe",
449
+ "loft",
450
+ "loom",
451
+ "luma",
452
+ "lynx",
453
+ "mint",
454
+ "mist",
455
+ "moon",
456
+ "moss",
457
+ "navy",
458
+ "nest",
459
+ "nova",
460
+ "oak",
461
+ "ocean",
462
+ "onyx",
463
+ "opal",
464
+ "otter",
465
+ "palm",
466
+ "peak",
467
+ "pine",
468
+ "pond",
469
+ "pool",
470
+ "rain",
471
+ "reed",
472
+ "reef",
473
+ "rice",
474
+ "ring",
475
+ "rise",
476
+ "road",
477
+ "rock",
478
+ "rose",
479
+ "ruby",
480
+ "sage",
481
+ "sand",
482
+ "seal",
483
+ "seed",
484
+ "silk",
485
+ "sky",
486
+ "snow",
487
+ "soft",
488
+ "star",
489
+ "stem",
490
+ "stone",
491
+ "surf",
492
+ "swan",
493
+ "teal",
494
+ "tide",
495
+ "tree",
496
+ "vale",
497
+ "vine",
498
+ "wave",
499
+ "west",
500
+ "wind",
501
+ "wing",
502
+ "wolf",
503
+ "wood",
504
+ "wren",
505
+ "yarn",
506
+ "zest"
507
+ ]
2223
508
  }
2224
- };
2225
- return supportedExecutionTargets.filter((target) => target.harness === "pi").map((target) => choices[target.driver]);
2226
- }
2227
- async function loginCommand(deps) {
2228
- try {
2229
- const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
2230
- deps.progress.start("Preparing SSH access");
2231
- const keys = await deps.defaultSshPublicKeys();
2232
- await Promise.all(keys.map((key) => deps.api.registerSshKey(tokens.access_token, key)));
2233
- deps.progress.succeed("SSH access ready");
2234
- deps.output({ message: "Logged in to Tonbo." });
2235
- } catch (error) {
2236
- deps.progress.fail();
2237
- throw error;
2238
509
  }
2239
- }
2240
- var LOGIN_PROGRESS_MESSAGES = {
2241
- "account-config": {
2242
- started: "Connecting to Tonbo",
2243
- completed: "Connected to Tonbo"
510
+ };
511
+ var managementApiBasePath = "/v1";
512
+ var managementOperations = {
513
+ "getAccount": {
514
+ "method": "GET",
515
+ "path": "/account",
516
+ "pathParams": [],
517
+ "query": [],
518
+ "idempotencyKey": false,
519
+ "cli": "tonbo account show"
2244
520
  },
2245
- "callback-server": {
2246
- started: "Starting local browser callback",
2247
- completed: "Local browser callback ready"
521
+ "listRegions": {
522
+ "method": "GET",
523
+ "path": "/regions",
524
+ "pathParams": [],
525
+ "query": [],
526
+ "idempotencyKey": false,
527
+ "cli": "tonbo region list"
2248
528
  },
2249
- browser: {
2250
- started: "Opening browser",
2251
- completed: "Browser opened"
529
+ "listMembers": {
530
+ "method": "GET",
531
+ "path": "/members",
532
+ "pathParams": [],
533
+ "query": [
534
+ "cursor",
535
+ "limit"
536
+ ],
537
+ "idempotencyKey": false,
538
+ "cli": "tonbo member list"
2252
539
  },
2253
- "browser-authorization": {
2254
- started: "Waiting for browser authorization",
2255
- completed: "Browser authorization received"
540
+ "updateMember": {
541
+ "method": "PATCH",
542
+ "path": "/members/{subject}",
543
+ "pathParams": [
544
+ "subject"
545
+ ],
546
+ "query": [],
547
+ "idempotencyKey": false,
548
+ "cli": "tonbo member update"
2256
549
  },
2257
- "callback-close": {
2258
- started: "Closing local browser callback",
2259
- completed: "Local browser callback closed"
550
+ "createAgent": {
551
+ "method": "POST",
552
+ "path": "/agents",
553
+ "pathParams": [],
554
+ "query": [],
555
+ "idempotencyKey": true,
556
+ "cli": "tonbo agent create"
2260
557
  },
2261
- "token-exchange": {
2262
- started: "Exchanging authorization code",
2263
- completed: "Authorization code exchanged"
558
+ "listApiKeys": {
559
+ "method": "GET",
560
+ "path": "/api-keys",
561
+ "pathParams": [],
562
+ "query": [
563
+ "cursor",
564
+ "limit"
565
+ ],
566
+ "idempotencyKey": false,
567
+ "cli": "tonbo api-key list"
2264
568
  },
2265
- "credential-store": {
2266
- started: "Saving login session",
2267
- completed: "Login session saved"
569
+ "createApiKey": {
570
+ "method": "POST",
571
+ "path": "/api-keys",
572
+ "pathParams": [],
573
+ "query": [],
574
+ "idempotencyKey": false,
575
+ "cli": "tonbo api-key create"
576
+ },
577
+ "getApiKey": {
578
+ "method": "GET",
579
+ "path": "/api-keys/{key_id}",
580
+ "pathParams": [
581
+ "key_id"
582
+ ],
583
+ "query": [],
584
+ "idempotencyKey": false,
585
+ "cli": "tonbo api-key show"
586
+ },
587
+ "revokeApiKey": {
588
+ "method": "DELETE",
589
+ "path": "/api-keys/{key_id}",
590
+ "pathParams": [
591
+ "key_id"
592
+ ],
593
+ "query": [],
594
+ "idempotencyKey": false,
595
+ "cli": "tonbo api-key revoke"
596
+ },
597
+ "listBuckets": {
598
+ "method": "GET",
599
+ "path": "/buckets",
600
+ "pathParams": [],
601
+ "query": [
602
+ "cursor",
603
+ "limit"
604
+ ],
605
+ "idempotencyKey": false,
606
+ "cli": "tonbo bucket list"
607
+ },
608
+ "createBucket": {
609
+ "method": "POST",
610
+ "path": "/buckets",
611
+ "pathParams": [],
612
+ "query": [],
613
+ "idempotencyKey": true,
614
+ "cli": "tonbo bucket create"
615
+ },
616
+ "getBucket": {
617
+ "method": "GET",
618
+ "path": "/buckets/{name}",
619
+ "pathParams": [
620
+ "name"
621
+ ],
622
+ "query": [],
623
+ "idempotencyKey": false,
624
+ "cli": "tonbo bucket show"
625
+ },
626
+ "updateBucket": {
627
+ "method": "PATCH",
628
+ "path": "/buckets/{name}",
629
+ "pathParams": [
630
+ "name"
631
+ ],
632
+ "query": [],
633
+ "idempotencyKey": false,
634
+ "cli": "tonbo bucket update"
635
+ },
636
+ "deleteBucket": {
637
+ "method": "DELETE",
638
+ "path": "/buckets/{name}",
639
+ "pathParams": [
640
+ "name"
641
+ ],
642
+ "query": [],
643
+ "idempotencyKey": false,
644
+ "cli": "tonbo bucket delete"
645
+ },
646
+ "getUsage": {
647
+ "method": "GET",
648
+ "path": "/usage",
649
+ "pathParams": [],
650
+ "query": [
651
+ "period"
652
+ ],
653
+ "idempotencyKey": false,
654
+ "cli": "tonbo usage show"
655
+ },
656
+ "listDatabases": {
657
+ "method": "GET",
658
+ "path": "/databases",
659
+ "pathParams": [],
660
+ "query": [
661
+ "prefix",
662
+ "cursor",
663
+ "limit"
664
+ ],
665
+ "idempotencyKey": false,
666
+ "cli": "tonbo database list"
667
+ },
668
+ "createDatabase": {
669
+ "method": "POST",
670
+ "path": "/databases",
671
+ "pathParams": [],
672
+ "query": [],
673
+ "idempotencyKey": true,
674
+ "cli": "tonbo database create"
675
+ },
676
+ "getDatabase": {
677
+ "method": "GET",
678
+ "path": "/databases/{name}",
679
+ "pathParams": [
680
+ "name"
681
+ ],
682
+ "query": [],
683
+ "idempotencyKey": false,
684
+ "cli": "tonbo database show"
685
+ },
686
+ "updateDatabase": {
687
+ "method": "PATCH",
688
+ "path": "/databases/{name}",
689
+ "pathParams": [
690
+ "name"
691
+ ],
692
+ "query": [],
693
+ "idempotencyKey": false,
694
+ "cli": "tonbo database update"
695
+ },
696
+ "deleteDatabase": {
697
+ "method": "DELETE",
698
+ "path": "/databases/{name}",
699
+ "pathParams": [
700
+ "name"
701
+ ],
702
+ "query": [],
703
+ "idempotencyKey": false,
704
+ "cli": "tonbo database delete"
705
+ },
706
+ "listDatabaseUsage": {
707
+ "method": "GET",
708
+ "path": "/usage/databases",
709
+ "pathParams": [],
710
+ "query": [
711
+ "period",
712
+ "sort",
713
+ "prefix",
714
+ "cursor",
715
+ "limit"
716
+ ],
717
+ "idempotencyKey": false,
718
+ "cli": "tonbo database usage"
2268
719
  }
2269
720
  };
2270
- function reportLoginProgress(progress, event) {
2271
- const messages = LOGIN_PROGRESS_MESSAGES[event.step];
2272
- if (event.status === "started") progress.start(messages.started);
2273
- else progress.succeed(messages.completed);
2274
- }
2275
- async function sshKeyAddCommand(deps, path6) {
2276
- const key = await readSshPublicKey(path6);
2277
- const oauthToken = await deps.auth.accessToken();
2278
- await deps.api.registerSshKey(oauthToken, key);
2279
- deps.output({ message: `Registered SSH key ${key.fingerprint}.`, key });
2280
- }
2281
- async function sshKeyRemoveCommand(deps, fingerprint) {
2282
- const oauthToken = await deps.auth.accessToken();
2283
- const key = await deps.api.revokeSshKey(oauthToken, fingerprint);
2284
- deps.output({ message: `Revoked SSH key ${key.fingerprint}.`, key });
2285
- }
2286
- async function agentUseCommand(deps, selector, force = false) {
2287
- const root = await findDeclarationRoot(deps.cwd());
2288
- const declaration = await loadDeclaration(root);
2289
- const oauthToken = await deps.auth.accessToken();
2290
- const agents = await deps.api.listAgents(oauthToken);
2291
- const agent = selectAgent(agents, selector);
2292
- if (declaration.agent === agent.publicHostname) {
2293
- deps.output({
2294
- message: `.tonbo already uses agent ${agent.publicHostname}.`,
2295
- agent
721
+ var accountRoles = [
722
+ "owner",
723
+ "admin",
724
+ "member"
725
+ ];
726
+ var apiKeyExpirations = [
727
+ "15_days",
728
+ "1_month",
729
+ "6_months",
730
+ "1_year",
731
+ "never"
732
+ ];
733
+
734
+ // ../../packages/tonbo-cli-core/dist/src/api.js
735
+ var DEFAULT_API_ORIGIN = "https://api.tonbo.dev";
736
+ var ManagementApi = class {
737
+ fetcher;
738
+ apiOrigin;
739
+ loginHint;
740
+ constructor(fetcher, apiOrigin2 = DEFAULT_API_ORIGIN, loginHint = DEFAULT_LOGIN_HINT) {
741
+ this.fetcher = fetcher;
742
+ this.apiOrigin = apiOrigin2;
743
+ this.loginHint = loginHint;
744
+ }
745
+ /** The answer's `data`. A refusal throws an HttpError naming the request id. */
746
+ async call(operationId, key, request = {}) {
747
+ return (await this.request(operationId, key, request)).data;
748
+ }
749
+ /** One page of a list operation: its `data` and `next_cursor`, with the
750
+ * operation's other query parameters, if any. */
751
+ page(operationId, key, { limit, cursor } = {}, query = {}) {
752
+ return this.request(operationId, key, { query: { ...query, limit, cursor } });
753
+ }
754
+ async request(operationId, key, { params = {}, query = {}, body, idempotencyKey }) {
755
+ const operation = managementOperations[operationId];
756
+ const path2 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
757
+ const value = params[name];
758
+ if (value === void 0)
759
+ throw new Error(`${operationId} needs the path parameter ${name}.`);
760
+ return encodeURIComponent(value);
2296
761
  });
2297
- return;
2298
- }
2299
- if (declaration.agent && !force) {
2300
- const current = agents.find((candidate) => candidate.publicHostname === declaration.agent);
2301
- const currentLabel = current?.publicHostname ?? declaration.agent;
2302
- if (!deps.interactive()) {
2303
- throw new Error(
2304
- `.tonbo already uses agent ${currentLabel}. Pass --force to rebind it to ${agent.publicHostname}.`
2305
- );
762
+ const search = new URLSearchParams();
763
+ for (const [name, value] of Object.entries(query)) {
764
+ if (value === void 0)
765
+ continue;
766
+ if (!operation.query.includes(name))
767
+ throw new Error(`${operationId} takes no query parameter ${name}.`);
768
+ search.set(name, String(value));
2306
769
  }
2307
- const action = await deps.select(
2308
- `Rebind .tonbo from ${currentLabel} to ${agent.publicHostname}?`,
2309
- [
2310
- { name: `Rebind to ${agent.publicHostname}`, value: "rebind" },
2311
- { name: "Cancel", value: "cancel" }
2312
- ]
2313
- );
2314
- if (action !== "rebind") {
2315
- deps.output({ message: `Cancelled. .tonbo still uses agent ${currentLabel}.` });
2316
- return;
770
+ if (operation.idempotencyKey && !idempotencyKey)
771
+ throw new Error(`${operationId} needs an Idempotency-Key.`);
772
+ const headers = { authorization: `Bearer ${key}` };
773
+ if (body !== void 0)
774
+ headers["content-type"] = "application/json";
775
+ if (idempotencyKey)
776
+ headers["idempotency-key"] = idempotencyKey;
777
+ const queryString = search.toString();
778
+ const url = `${this.apiOrigin}${managementApiBasePath}${path2}${queryString ? `?${queryString}` : ""}`;
779
+ try {
780
+ return await requestJson(this.fetcher, url, {
781
+ method: operation.method,
782
+ headers,
783
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
784
+ });
785
+ } catch (error) {
786
+ if (error instanceof HttpError && error.status === 401)
787
+ throw new HttpError(`${error.message} Run \`${this.loginHint}\`.`, error.status, error.body, error.requestId);
788
+ throw error;
2317
789
  }
2318
790
  }
2319
- await bindDeclarationAgent(root, agent.publicHostname);
2320
- deps.output({ message: `Bound .tonbo to agent ${agent.publicHostname}.`, agent });
2321
- }
2322
- async function agentCreateCommand(deps, name, options = {}) {
2323
- const root = await findDeclarationRoot(deps.cwd());
2324
- const declaration = await loadDeclaration(root);
2325
- if (declaration.agent) {
2326
- throw new Error(
2327
- `.tonbo is already bound to agent ${declaration.agent}. Run \`tonbo agent use <agent>\` to review a rebind.`
2328
- );
791
+ getAccount(key) {
792
+ return this.call("getAccount", key);
2329
793
  }
2330
- const oauthToken = await deps.auth.accessToken();
2331
- validateAgentName(name);
2332
- const requestId = options.requestId ?? randomUUID3();
2333
- const region = options.region ?? (deps.interactive() ? await selectAgentRegion(deps) : void 0);
2334
- const agent = await deps.api.createAgent(oauthToken, {
2335
- name,
2336
- organizationId: options.account,
2337
- region,
2338
- requestId
2339
- });
2340
- try {
2341
- await bindDeclarationAgent(root, agent.publicHostname);
2342
- } catch (error) {
2343
- throw new Error(
2344
- `Created agent ${agent.name}, but could not bind ${DECLARATION_FILENAME}. Fix the file and run \`tonbo agent use ${agent.publicHostname}\`.`,
2345
- { cause: error }
2346
- );
794
+ listRegions(key) {
795
+ return this.call("listRegions", key);
2347
796
  }
2348
- deps.output({
2349
- message: [
2350
- `Created agent ${agent.name} and bound it in .tonbo.`,
2351
- `Organization: ${agent.organizationName}`,
2352
- `Region: ${agent.region}`,
2353
- `Application: https://${agent.publicHostname}`,
2354
- `SSH after deploy: ssh ${agent.sshDestination}`
2355
- ].join("\n"),
2356
- agent,
2357
- requestId
2358
- });
2359
- }
2360
- async function deployCommand(deps, options = { promote: true }) {
2361
- const { declaration, oauthToken, agent, root } = await resolveAgent(deps);
2362
- if (declaration.build) await runBuildCommand(root, declaration.build.command);
2363
- const source = await buildSourceBundle(root);
2364
- const origin = { actor: "cli", git: await deps.gitProvenance(root) };
2365
- const managementToken = await deps.api.exchangeManagementToken(oauthToken, agent.id);
2366
- const result = await deps.api.deploy({
2367
- bundle: source,
2368
- origin,
2369
- agentId: agent.id,
2370
- promote: options.promote,
2371
- spec: buildDeploymentSpec(declaration, source),
2372
- token: managementToken
2373
- });
2374
- const { deployment } = result;
2375
- const shortId = shortDeploymentId(deployment.id);
2376
- const lines = [
2377
- options.promote ? `Deployed agent ${agent.name}.` : `Created Deployment ${deploymentName(deployment)} (${shortId}) for agent ${agent.name} without promoting it.`,
2378
- `Organization: ${agent.organizationName}`,
2379
- `Deployment: ${deploymentName(deployment)} (${shortId})`,
2380
- `Source: ${deploymentSourceLabel(deployment)}`,
2381
- `Production: ${options.promote ? productionStateLabel(deployment) : `not promoted; run \`tonbo deployments promote ${shortId}\` to move Production here`}`,
2382
- `Application: https://${agent.publicHostname}`
2383
- ];
2384
- if (result.unchanged) {
2385
- lines.push(
2386
- `Contents are identical to the ${options.promote ? "previous" : "current"} Production Deployment; the running Agent does not change.`
2387
- );
797
+ listMembers(key, page) {
798
+ return this.page("listMembers", key, page);
2388
799
  }
2389
- if (options.promote) {
2390
- lines.push(
2391
- "",
2392
- "Agent process: starts with the first turn and stays warm while the runtime is active.",
2393
- "",
2394
- "Start the Agent:",
2395
- ' tonbo run "<prompt>"',
2396
- "",
2397
- "Connect with SSH:",
2398
- ` ssh ${agent.sshDestination}`
2399
- );
800
+ updateMember(key, subject, change) {
801
+ return this.call("updateMember", key, { params: { subject }, body: change });
2400
802
  }
2401
- deps.output({ message: lines.join("\n"), agent, ...result });
2402
- }
2403
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2404
- var ADMISSION_WAIT_MS = 15 * 60 * 1e3;
2405
- var RESUBMIT_MS = 5e3;
2406
- var SUBMISSION_TOKEN_LIFETIME_SECONDS = 240;
2407
- function managementTokens(deps, agentId, oauthToken) {
2408
- let current = deps.api.exchangeManagementToken(oauthToken, agentId);
2409
- return async function withToken(call) {
2410
- const token = await current;
2411
- try {
2412
- return await call(token);
2413
- } catch (error) {
2414
- if (!isUnauthenticated(error)) throw error;
2415
- current = deps.auth.accessToken().then((oauth) => deps.api.exchangeManagementToken(oauth, agentId));
2416
- return call(await current);
2417
- }
2418
- };
2419
- }
2420
- function waitCommand(turnId, sessionId) {
2421
- return `tonbo turn wait ${turnId} --session ${sessionId}`;
2422
- }
2423
- function turnNotFound(turnId, sessionId, agentName) {
2424
- return new Error(`Turn ${turnId} was not found in Session ${sessionId} of agent ${agentName}.`);
2425
- }
2426
- function sessionTurn(turn) {
2427
- return {
2428
- assistant_text: turn.assistant_text ?? "",
2429
- session_id: turn.session_id,
2430
- turn_id: turn.id
2431
- };
2432
- }
2433
- function holdSubmission(deps, withToken, submission) {
2434
- const controller = new AbortController();
2435
- const held = { controller, settled: Promise.resolve() };
2436
- held.settled = withToken(
2437
- (token) => deps.api.submitTurn({ ...submission, signal: controller.signal, token })
2438
- ).then(
2439
- (value) => {
2440
- held.answer = { value };
2441
- },
2442
- (error) => {
2443
- held.answer = { error };
2444
- }
2445
- );
2446
- return held;
2447
- }
2448
- async function submitAndFollow(deps, withToken, submission, { agentName, outcome }) {
2449
- const { agentId, sessionId, turnId } = submission;
2450
- const now = deps.now ?? Date.now;
2451
- const sleep = deps.sleep ?? pause;
2452
- const submittedAt = now();
2453
- const state = {
2454
- admitted: false,
2455
- announced: false,
2456
- held: holdSubmission(deps, withToken, submission),
2457
- resubmitAt: 0,
2458
- unanswered: null
2459
- };
2460
- const read2 = async () => {
2461
- const answer = state.held?.answer;
2462
- if (answer) {
2463
- state.held = null;
2464
- if ("error" in answer) {
2465
- const problem = submissionProblem(answer.error, turnId);
2466
- if (problem instanceof TurnFailedError) throw problem;
2467
- if (!state.admitted) {
2468
- if (!isTransient(answer.error)) throw problem;
2469
- state.unanswered = answer.error;
2470
- state.resubmitAt = now() + RESUBMIT_MS;
2471
- }
2472
- } else if (answer.value.state === "completed") {
2473
- return { result: { completed: answer.value.turn } };
2474
- }
2475
- }
2476
- let turn = null;
2477
- try {
2478
- turn = await withToken((token) => deps.api.getTurn(agentId, sessionId, turnId, token));
2479
- } catch (error) {
2480
- if (!isNotFound(error)) throw error;
2481
- if (state.admitted) throw turnNotFound(turnId, sessionId, agentName);
2482
- }
2483
- if (turn) {
2484
- state.admitted = true;
2485
- if (turn.status === "completed") return { result: { completed: sessionTurn(turn) } };
2486
- if (turn.status === "failed")
2487
- throw new TurnFailedError(turn.id, turn.failure_code, turn.failure);
2488
- if (!outcome) return { result: { admitted: turn } };
2489
- if (!state.announced) {
2490
- state.announced = true;
2491
- deps.progress.start(
2492
- `Turn ${turnId} is running on the platform in Session ${sessionId}; waiting for its result. Interrupting the wait does not stop the Turn.`
2493
- );
2494
- }
2495
- return { pending: turn };
2496
- }
2497
- if (now() - submittedAt > ADMISSION_WAIT_MS) {
2498
- const cause = state.unanswered;
2499
- throw new Error(
2500
- `Turn ${turnId} was not admitted within ${ADMISSION_WAIT_MS / 6e4} minutes${cause instanceof Error ? ` (the last submission got no answer: ${cause.message})` : ""}. Waiting stopped; if the platform admits it later, \`${waitCommand(turnId, sessionId)}\` follows it.`,
2501
- { cause }
2502
- );
2503
- }
2504
- if (!state.held && now() >= state.resubmitAt)
2505
- state.held = holdSubmission(deps, withToken, submission);
2506
- return { pending: null };
2507
- };
2508
- try {
2509
- const followed = await followTurn({
2510
- now,
2511
- read: read2,
2512
- resume: `wait for it with \`${waitCommand(turnId, sessionId)}\`.`,
2513
- // An answer to the held submission ends a pause at once.
2514
- sleep: async (ms) => {
2515
- const held = state.held;
2516
- if (!held) return sleep(ms);
2517
- const wake = new AbortController();
2518
- await Promise.race([sleep(ms, wake.signal), held.settled]);
2519
- wake.abort();
2520
- },
2521
- turnId
2522
- });
2523
- if (state.announced) deps.progress.succeed(`Turn ${turnId} completed.`);
2524
- return followed;
2525
- } catch (error) {
2526
- if (state.announced) deps.progress.fail();
2527
- throw error;
2528
- } finally {
2529
- state.held?.controller.abort();
803
+ /** A replay with the same idempotency key returns the Agent the first
804
+ * request created, so an unconfirmed creation is retried with that key. */
805
+ createAgent(key, request, idempotencyKey) {
806
+ return this.call("createAgent", key, {
807
+ body: withoutUndefined(request),
808
+ idempotencyKey
809
+ }).catch(unconfirmed("Agent creation", idempotencyKey, "the same name, role and region"));
2530
810
  }
2531
- }
2532
- function pause(ms, signal) {
2533
- return new Promise((resolve) => {
2534
- const timer = setTimeout(resolve, ms);
2535
- signal?.addEventListener(
2536
- "abort",
2537
- () => {
2538
- clearTimeout(timer);
2539
- resolve();
2540
- },
2541
- { once: true }
2542
- );
2543
- });
2544
- }
2545
- async function runCommand(deps, prompt, options) {
2546
- const { oauthToken, agent } = await resolveAgent(deps, SUBMISSION_TOKEN_LIFETIME_SECONDS);
2547
- const withToken = managementTokens(deps, agent.id, oauthToken);
2548
- const { production, session } = await withToken(
2549
- (token) => deps.api.openSession({ agentId: agent.id, sessionId: options.session, token })
2550
- );
2551
- const turnId = randomUUID3();
2552
- const followed = await submitAndFollow(
2553
- deps,
2554
- withToken,
2555
- { agentId: agent.id, prompt, sessionId: session.id, turnId },
2556
- { agentName: agent.name, outcome: options.wait !== false }
2557
- );
2558
- if ("admitted" in followed) {
2559
- deps.output({
2560
- message: [
2561
- `Turn ${turnId} was admitted in Session ${session.id}; the platform runs it to its end.`,
2562
- "Wait for its result:",
2563
- ` ${waitCommand(turnId, session.id)}`
2564
- ].join("\n"),
2565
- agent,
2566
- production,
2567
- session,
2568
- turn_id: turnId,
2569
- turn: followed.admitted
2570
- });
2571
- return;
811
+ listApiKeys(key, page) {
812
+ return this.page("listApiKeys", key, page);
2572
813
  }
2573
- deps.output({
2574
- message: followed.completed.assistant_text,
2575
- agent,
2576
- production,
2577
- session,
2578
- turn: followed.completed
2579
- });
2580
- }
2581
- async function turnWaitCommand(deps, turnId, options) {
2582
- if (!UUID.test(turnId) || !UUID.test(options.session))
2583
- throw new Error("The Turn and its Session are identified by UUIDs.");
2584
- const { oauthToken, agent } = await resolveAgent(deps);
2585
- const withToken = managementTokens(deps, agent.id, oauthToken);
2586
- deps.progress.start(`Waiting for Turn ${turnId}. Interrupting the wait does not stop the Turn.`);
2587
- try {
2588
- const turn = await followTurn({
2589
- now: deps.now ?? Date.now,
2590
- read: async () => {
2591
- let current;
2592
- try {
2593
- current = await withToken(
2594
- (token) => deps.api.getTurn(agent.id, options.session, turnId, token)
2595
- );
2596
- } catch (error) {
2597
- if (!isNotFound(error)) throw error;
2598
- throw turnNotFound(turnId, options.session, agent.name);
2599
- }
2600
- if (current.status === "completed") return { result: current };
2601
- if (current.status === "failed")
2602
- throw new TurnFailedError(current.id, current.failure_code, current.failure);
2603
- return { pending: current };
2604
- },
2605
- resume: `run \`${waitCommand(turnId, options.session)}\` again.`,
2606
- sleep: deps.sleep ?? pause,
2607
- turnId
814
+ /** Not idempotent: the secret is never stored, so a replay could not
815
+ * return it, and a blind retry of a live name answers 409. */
816
+ createApiKey(key, request) {
817
+ return this.call("createApiKey", key, { body: withoutUndefined(request) });
818
+ }
819
+ getApiKey(key, keyId) {
820
+ return this.call("getApiKey", key, { params: { key_id: keyId } });
821
+ }
822
+ revokeApiKey(key, keyId) {
823
+ return this.call("revokeApiKey", key, {
824
+ params: { key_id: keyId }
2608
825
  });
2609
- deps.progress.succeed(`Turn ${turnId} completed.`);
2610
- deps.output({ message: turn.assistant_text ?? "", agent, turn });
2611
- } catch (error) {
2612
- deps.progress.fail();
2613
- throw error;
2614
826
  }
2615
- }
2616
- async function agentListCommand(deps) {
2617
- const agents = await deps.api.listAgents(await deps.auth.accessToken());
2618
- deps.output({
2619
- message: agents.length ? agents.map((agent) => `${agent.name} \xB7 ${agent.publicHostname} \xB7 ${agent.region}`).join("\n") : "No agents are available.",
2620
- agents
2621
- });
2622
- }
2623
- async function agentShowCommand(deps) {
2624
- const { agent, token } = await agentManagement(deps);
2625
- const production = await deps.api.getProduction(agent.id, token);
2626
- const deployment = production ? await deps.api.getDeployment(agent.id, production.deployment_id, token) : null;
2627
- const lines = [
2628
- `agent: ${agent.name}`,
2629
- `Organization: ${agent.organizationName}`,
2630
- `region: ${agent.region}`,
2631
- `application: https://${agent.publicHostname}`,
2632
- `ssh: ssh ${agent.sshDestination}`
2633
- ];
2634
- if (deployment) {
2635
- lines.push(
2636
- `production: ${deploymentName(deployment)} (${shortDeploymentId(deployment.id)}) ${productionStateLabel(deployment)}`
2637
- );
827
+ listBuckets(key, page) {
828
+ return this.page("listBuckets", key, page);
2638
829
  }
2639
- deps.output({ message: lines.join("\n"), agent, production, deployment });
2640
- }
2641
- function shortDeploymentId(id) {
2642
- return id.replace(/-/g, "").slice(0, 8);
2643
- }
2644
- function deploymentName(deployment) {
2645
- const subject = deployment.origin.git?.subject?.trim();
2646
- return subject ? subject : shortDeploymentId(deployment.id);
2647
- }
2648
- function deploymentSourceLabel(deployment) {
2649
- const git = deployment.origin.git;
2650
- if (!git) return deploymentOriginLabel(deployment.origin.actor);
2651
- const sha = git.commit_sha.slice(0, 7);
2652
- return git.ref ? `${sha} \xB7 ${git.ref}` : sha;
2653
- }
2654
- function productionStateLabel(deployment) {
2655
- const { generation, state } = deployment.production;
2656
- return generation === null ? state : `${state} (generation ${generation})`;
2657
- }
2658
- function selectDeployment(deployments, prefix) {
2659
- const normalized = prefix.trim().toLowerCase().replace(/-/g, "");
2660
- if (!/^[0-9a-f]+$/.test(normalized)) {
2661
- throw new Error(`Deployment id ${prefix} must be a hexadecimal id prefix.`);
830
+ /** A replay with the same idempotency key returns the bucket the first
831
+ * request created. */
832
+ createBucket(key, request, idempotencyKey) {
833
+ return this.call("createBucket", key, {
834
+ body: withoutUndefined(request),
835
+ idempotencyKey
836
+ }).catch(unconfirmed("Bucket creation", idempotencyKey, "the same name and region"));
2662
837
  }
2663
- const matches = deployments.filter(
2664
- (deployment) => deployment.id.replace(/-/g, "").startsWith(normalized)
2665
- );
2666
- if (matches.length === 0) throw new Error(`Deployment ${prefix} was not found in this agent.`);
2667
- if (matches.length > 1) {
2668
- throw new Error(
2669
- `Deployment id ${prefix} is ambiguous; it matches ${matches.map((deployment) => shortDeploymentId(deployment.id)).join(", ")}. Use more characters.`
2670
- );
838
+ getBucket(key, name) {
839
+ return this.call("getBucket", key, { params: { name } });
2671
840
  }
2672
- return matches[0];
2673
- }
2674
- function renderTable(headers, rows) {
2675
- const widths = headers.map(
2676
- (header, column) => Math.max(header.length, ...rows.map((row) => row[column].length))
2677
- );
2678
- const render = (row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd();
2679
- return [render(headers), ...rows.map(render)].join("\n");
2680
- }
2681
- function formatTimestamp(value) {
2682
- const date = new Date(value);
2683
- if (Number.isNaN(date.getTime())) return value;
2684
- return `${date.toISOString().slice(0, 16)}Z`;
2685
- }
2686
- function productionMarker(state) {
2687
- if (state === "current") return "\u25CF";
2688
- if (state === "previous") return "\u25CB";
2689
- return "";
2690
- }
2691
- async function deploymentsListCommand(deps) {
2692
- const { agent, token } = await agentManagement(deps);
2693
- const [deployments, production] = await Promise.all([
2694
- deps.api.listDeployments(agent.id, token),
2695
- deps.api.getProduction(agent.id, token)
2696
- ]);
2697
- const message = deployments.length ? renderTable(
2698
- ["ID", "NAME", "STATUS", "PRODUCTION", "SOURCE", "CREATED", "BY"],
2699
- deployments.map((deployment) => [
2700
- shortDeploymentId(deployment.id),
2701
- deploymentName(deployment),
2702
- deployment.production.state,
2703
- productionMarker(deployment.production.state),
2704
- deploymentSourceLabel(deployment),
2705
- formatTimestamp(deployment.created_at),
2706
- deployment.created_by_user_id ? shortDeploymentId(deployment.created_by_user_id) : ""
2707
- ])
2708
- ) : `No Deployments exist for agent ${agent.name}. Run \`tonbo deploy\` to create one.`;
2709
- deps.output({ message, agent, production, deployments });
2710
- }
2711
- async function deploymentsShowCommand(deps, prefix) {
2712
- const { agent, token } = await agentManagement(deps);
2713
- const deployment = selectDeployment(await deps.api.listDeployments(agent.id, token), prefix);
2714
- const rollouts = (await deps.api.listRollouts(agent.id, token)).filter(
2715
- (rollout) => rollout.deployment_id === deployment.id || rollout.previous_deployment_id === deployment.id
2716
- );
2717
- deps.output({
2718
- message: renderDeploymentDetail(deployment, rollouts),
2719
- agent,
2720
- deployment,
2721
- rollouts
2722
- });
2723
- }
2724
- function renderDeploymentDetail(deployment, rollouts) {
2725
- const git = deployment.origin.git;
2726
- const source = deployment.spec.source;
2727
- const bundleLabel = typeof source?.sha256 === "string" ? [
2728
- typeof source.format === "string" ? source.format : "",
2729
- source.sha256,
2730
- typeof source.size_bytes === "number" ? `(${source.size_bytes} bytes)` : ""
2731
- ].filter(Boolean).join(" ") : null;
2732
- const inference = deployment.spec.inference;
2733
- const rows = [
2734
- ["Deployment", `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`],
2735
- ["ID", deployment.id],
2736
- ["Status", productionStateLabel(deployment)],
2737
- ["Source", `${deploymentSourceLabel(deployment)}${git?.dirty ? " (dirty)" : ""}`]
2738
- ];
2739
- if (git) rows.push(["Commit", git.commit_sha], ["Author", git.author ?? ""]);
2740
- rows.push(
2741
- ["Created", formatTimestamp(deployment.created_at)],
2742
- ["Created by", deployment.created_by_user_id ?? ""],
2743
- ["Spec sha256", deployment.spec_sha256]
2744
- );
2745
- if (typeof inference?.model === "string") rows.push(["Model", inference.model]);
2746
- if (bundleLabel) rows.push(["Bundle", bundleLabel]);
2747
- const label = Math.max(...rows.map(([name]) => name.length)) + 1;
2748
- const lines = rows.map(([name, value]) => `${`${name}:`.padEnd(label)} ${value}`.trimEnd());
2749
- lines.push("", "Rollouts:");
2750
- if (rollouts.length === 0) lines.push(" none");
2751
- for (const rollout of rollouts) {
2752
- const direction = rollout.deployment_id === deployment.id ? `${rollout.kind} to this Deployment${rollout.previous_deployment_id ? ` from ${shortDeploymentId(rollout.previous_deployment_id)}` : ""}` : `${rollout.kind} away to ${shortDeploymentId(rollout.deployment_id)}`;
2753
- lines.push(
2754
- ` generation ${rollout.generation} ${formatTimestamp(rollout.created_at)} ${direction}${rollout.created_by_user_id ? ` by ${shortDeploymentId(rollout.created_by_user_id)}` : ""}`
2755
- );
841
+ updateBucket(key, name, change) {
842
+ return this.call("updateBucket", key, { params: { name }, body: change });
2756
843
  }
2757
- return lines.join("\n");
2758
- }
2759
- async function deploymentsPromoteCommand(deps, prefix) {
2760
- const { agent, token } = await agentManagement(deps);
2761
- const target = selectDeployment(await deps.api.listDeployments(agent.id, token), prefix);
2762
- const result = await moveProduction(deps, agent.id, target, token);
2763
- deps.output({
2764
- message: describeProductionMove(agent, result, "Promoted"),
2765
- agent,
2766
- ...result
2767
- });
2768
- }
2769
- async function deploymentsRollbackCommand(deps, prefix) {
2770
- const { agent, token } = await agentManagement(deps);
2771
- let target;
2772
- if (prefix !== void 0) {
2773
- target = selectDeployment(await deps.api.listDeployments(agent.id, token), prefix);
2774
- } else {
2775
- const rollouts = await deps.api.listRollouts(agent.id, token);
2776
- const previousId = rollouts.find(
2777
- (rollout) => rollout.previous_deployment_id !== null
2778
- )?.previous_deployment_id;
2779
- if (!previousId) {
2780
- throw new Error(
2781
- `Agent ${agent.name} has no previous Production Deployment to roll back to. Name one with \`tonbo deployments rollback <id>\`.`
2782
- );
2783
- }
2784
- target = await deps.api.getDeployment(agent.id, previousId, token);
844
+ deleteBucket(key, name) {
845
+ return this.call("deleteBucket", key, { params: { name } });
2785
846
  }
2786
- const result = await moveProduction(deps, agent.id, target, token);
2787
- deps.output({
2788
- message: describeProductionMove(agent, result, "Rolled back"),
2789
- agent,
2790
- ...result
2791
- });
2792
- }
2793
- async function moveProduction(deps, agentId, target, token) {
2794
- const previous = await deps.api.getProduction(agentId, token);
2795
- const production = await deps.api.putProduction(
2796
- agentId,
2797
- {
2798
- deployment_id: target.id,
2799
- desired_state: "running",
2800
- expected_generation: previous ? previous.generation : null
2801
- },
2802
- token
2803
- );
2804
- return {
2805
- deployment: await deps.api.getDeployment(agentId, target.id, token),
2806
- previous,
2807
- production
2808
- };
2809
- }
2810
- function describeProductionMove(agent, move, verb) {
2811
- const { deployment, previous } = move;
2812
- const name = `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`;
2813
- const headline = previous?.deployment_id === deployment.id ? `Deployment ${name} is already Production for agent ${agent.name}; requested it to be running.` : `${verb} ${name} to Production for agent ${agent.name}${previous ? ` (from ${shortDeploymentId(previous.deployment_id)})` : ""}.`;
2814
- return `${headline}
2815
- Production: ${productionStateLabel(deployment)}
2816
- Application: https://${agent.publicHostname}`;
2817
- }
2818
- async function agentManagement(deps) {
2819
- const { oauthToken, agent } = await resolveAgent(deps);
2820
- return {
2821
- agent,
2822
- token: await deps.api.exchangeManagementToken(oauthToken, agent.id)
2823
- };
2824
- }
2825
- async function secretListCommand(deps) {
2826
- const { agent, token } = await agentManagement(deps);
2827
- const secrets = await deps.api.listAgentSecrets(agent.id, token);
2828
- deps.output({
2829
- message: secrets.length ? secrets.map((secret) => secret.name).join("\n") : "No agent secrets are configured.",
2830
- agent,
2831
- secrets
2832
- });
2833
- }
2834
- async function secretSetCommand(deps, name, options) {
2835
- const value = await deps.secretValue(name, options.fromEnv);
2836
- const { agent, token } = await agentManagement(deps);
2837
- const secret = await deps.api.setAgentSecret(agent.id, name, value, token);
2838
- deps.output({
2839
- message: `Set agent secret ${secret.name}. Redeploy to replace the active runtime with this value.`,
2840
- agent,
2841
- secret
2842
- });
2843
- }
2844
- async function secretRemoveCommand(deps, name) {
2845
- const { agent, token } = await agentManagement(deps);
2846
- await deps.api.deleteAgentSecret(agent.id, name, token);
2847
- deps.output({ message: `Removed agent secret ${name}.`, agent, name });
2848
- }
2849
- async function cancelActivitiesCommand(deps, options) {
2850
- const { oauthToken, agent } = await resolveAgent(deps);
2851
- const token = await deps.api.exchangeManagementToken(oauthToken, agent.id);
2852
- const result = await deps.api.cancelActivities(agent.id, token, {
2853
- sandbox_id: options.sandbox,
2854
- runtime_session_id: options.runtimeSession,
2855
- work_keys: options.workKey,
2856
- reason: options.reason
2857
- });
2858
- deps.output({
2859
- message: "Activity cancellation requested. Physical retirement and application checkpoint cancellation must still be verified.",
2860
- cancellation: result
2861
- });
847
+ listDatabases(key, page, prefix) {
848
+ return this.page("listDatabases", key, page, { prefix });
849
+ }
850
+ /** A replay with the same idempotency key returns the database the first
851
+ * request created. */
852
+ createDatabase(key, request, idempotencyKey) {
853
+ return this.call("createDatabase", key, {
854
+ body: withoutUndefined(request),
855
+ idempotencyKey
856
+ }).catch(unconfirmed("Database creation", idempotencyKey, "the same name and region"));
857
+ }
858
+ getDatabase(key, name) {
859
+ return this.call("getDatabase", key, { params: { name } });
860
+ }
861
+ updateDatabase(key, name, change) {
862
+ return this.call("updateDatabase", key, { params: { name }, body: change });
863
+ }
864
+ deleteDatabase(key, name) {
865
+ return this.call("deleteDatabase", key, { params: { name } });
866
+ }
867
+ /** The whole usage page: its rows, `other`, prices and checkpoint. */
868
+ listDatabaseUsage(key, page, query = {}) {
869
+ return this.page("listDatabaseUsage", key, page, query);
870
+ }
871
+ getUsage(key, period) {
872
+ return this.call("getUsage", key, { query: { period } });
873
+ }
874
+ };
875
+ function withoutUndefined(value) {
876
+ return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
877
+ }
878
+ function unconfirmed(what, idempotencyKey, same) {
879
+ return (cause) => {
880
+ if (cause instanceof HttpError && cause.status < 500)
881
+ throw cause;
882
+ throw new Error(`${what} was not confirmed: ${cause instanceof Error ? cause.message : String(cause)} Retry with --idempotency-key ${idempotencyKey} and ${same}.`, { cause });
883
+ };
2862
884
  }
2863
885
 
2864
- // src/credentials.ts
2865
- import { randomUUID as randomUUID4 } from "node:crypto";
2866
- import { chmod, lstat as lstat4, mkdir, open as open2, readFile as readFile5, rename as rename2, rm as rm2 } from "node:fs/promises";
2867
- import os from "node:os";
2868
- import path5 from "node:path";
886
+ // ../../packages/tonbo-cli-core/dist/src/credentials.js
887
+ import { randomUUID } from "node:crypto";
888
+ import { chmod, lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
889
+ import os2 from "node:os";
890
+ import path from "node:path";
2869
891
  function defaultConfigDirectory() {
2870
- const base = process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config");
2871
- return path5.join(base, "tonbo");
892
+ const base = process.env.XDG_CONFIG_HOME || path.join(os2.homedir(), ".config");
893
+ return path.join(base, "tonbo");
2872
894
  }
895
+ var UnusableLoginError = class extends Error {
896
+ };
2873
897
  var FileCredentialStore = class {
2874
- constructor(filename = path5.join(defaultConfigDirectory(), "credentials.json")) {
898
+ filename;
899
+ loginHint;
900
+ constructor(filename = path.join(defaultConfigDirectory(), "credentials.json"), loginHint = DEFAULT_LOGIN_HINT) {
2875
901
  this.filename = filename;
902
+ this.loginHint = loginHint;
2876
903
  }
2877
904
  async load() {
905
+ let parsed;
2878
906
  try {
2879
907
  await assertPrivateRegularFile(this.filename);
2880
- const parsed = JSON.parse(await readFile5(this.filename, "utf8"));
2881
- if (!isOAuthTokenSet(parsed)) throw new Error("invalid token set");
2882
- return parsed;
908
+ parsed = JSON.parse(await readFile(this.filename, "utf8"));
2883
909
  } catch (error) {
2884
- if (error.code === "ENOENT") return null;
910
+ if (error.code === "ENOENT")
911
+ return null;
2885
912
  throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
2886
913
  }
2887
- }
2888
- async save(tokens) {
2889
- if (!isOAuthTokenSet(tokens)) throw new Error("Refusing to store a malformed Tonbo token set.");
2890
- const directory = path5.dirname(this.filename);
914
+ if (!isStoredLogin(parsed))
915
+ throw new UnusableLoginError(`The saved login at ${this.filename} is not usable by this CLI. Run \`${this.loginHint}\`.`);
916
+ return parsed;
917
+ }
918
+ async save(login) {
919
+ if (!isStoredLogin(login))
920
+ throw new Error("Refusing to store a malformed Tonbo login.");
921
+ const directory = path.dirname(this.filename);
2891
922
  await mkdir(directory, { mode: 448, recursive: true });
2892
923
  await preparePrivateDirectory(directory);
2893
924
  await assertExistingDestinationIsSafe(this.filename);
2894
- const temporary = path5.join(
2895
- directory,
2896
- `.${path5.basename(this.filename)}.${process.pid}.${randomUUID4()}.tmp`
2897
- );
925
+ const temporary = path.join(directory, `.${path.basename(this.filename)}.${process.pid}.${randomUUID()}.tmp`);
2898
926
  let handle = null;
2899
927
  try {
2900
- handle = await open2(temporary, "wx", 384);
2901
- await handle.writeFile(`${JSON.stringify(tokens, null, 2)}
928
+ handle = await open(temporary, "wx", 384);
929
+ await handle.writeFile(`${JSON.stringify(login, null, 2)}
2902
930
  `, "utf8");
2903
931
  await handle.sync();
2904
932
  await handle.close();
2905
933
  handle = null;
2906
- if (process.platform !== "win32") await chmod(temporary, 384);
2907
- await rename2(temporary, this.filename);
934
+ if (process.platform !== "win32")
935
+ await chmod(temporary, 384);
936
+ await rename(temporary, this.filename);
2908
937
  } catch (error) {
2909
938
  await handle?.close().catch(() => void 0);
2910
- await rm2(temporary, { force: true }).catch(() => void 0);
939
+ await rm(temporary, { force: true }).catch(() => void 0);
2911
940
  throw new Error(`Could not store Tonbo credentials at ${this.filename}.`, { cause: error });
2912
941
  }
2913
942
  }
943
+ async remove() {
944
+ await assertExistingDestinationIsSafe(this.filename);
945
+ await rm(this.filename, { force: true });
946
+ }
2914
947
  };
2915
- function isOAuthTokenSet(value) {
2916
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2917
- const token = value;
2918
- return typeof token.access_token === "string" && token.access_token.length > 0 && (token.refresh_token === void 0 || typeof token.refresh_token === "string") && (token.expires_at === void 0 || Number.isInteger(token.expires_at) && (token.expires_at ?? 0) > 0);
948
+ function isStoredLogin(value) {
949
+ if (!value || typeof value !== "object" || Array.isArray(value))
950
+ return false;
951
+ const login = value;
952
+ return login.version === 1 && typeof login.account_id === "string" && login.account_id.length > 0 && typeof login.key_id === "string" && login.key_id.length > 0 && typeof login.key === "string" && login.key.startsWith("tbo_") && (login.expires_at === null || typeof login.expires_at === "string" && !Number.isNaN(Date.parse(login.expires_at)));
2919
953
  }
2920
954
  async function preparePrivateDirectory(directory) {
2921
- const stat = await lstat4(directory);
955
+ const stat = await lstat(directory);
2922
956
  if (stat.isSymbolicLink() || !stat.isDirectory())
2923
957
  throw new Error("Tonbo config directory must be a real directory.");
2924
- if (process.platform !== "win32") await chmod(directory, 448);
958
+ if (process.platform !== "win32")
959
+ await chmod(directory, 448);
2925
960
  }
2926
961
  async function assertExistingDestinationIsSafe(filename) {
2927
962
  try {
2928
- const stat = await lstat4(filename);
963
+ const stat = await lstat(filename);
2929
964
  if (stat.isSymbolicLink() || !stat.isFile())
2930
965
  throw new Error("Tonbo credential path must be a regular file.");
2931
966
  } catch (error) {
2932
- if (error.code !== "ENOENT") throw error;
967
+ if (error.code !== "ENOENT")
968
+ throw error;
2933
969
  }
2934
970
  }
2935
971
  async function assertPrivateRegularFile(filename) {
2936
- const stat = await lstat4(filename);
972
+ const stat = await lstat(filename);
2937
973
  if (stat.isSymbolicLink() || !stat.isFile())
2938
974
  throw new Error("Tonbo credential path must be a regular file.");
2939
975
  if (process.platform !== "win32" && (stat.mode & 63) !== 0)
2940
976
  throw new Error("Tonbo credential file must have mode 0600.");
2941
977
  }
2942
978
 
2943
- // src/git.ts
2944
- import { execFile as execFile2 } from "node:child_process";
2945
- var defaultGitRunner = (args, cwd) => new Promise((resolve, reject) => {
2946
- execFile2(
2947
- "git",
2948
- args,
2949
- {
2950
- cwd,
2951
- encoding: "utf8",
2952
- // Never take the index lock for a read: a concurrent editor or IDE
2953
- // must not turn provenance collection into a failure.
2954
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
2955
- windowsHide: true
2956
- },
2957
- (error, stdout) => error ? reject(new Error(`git ${args.join(" ")} failed.`, { cause: error })) : resolve(stdout)
2958
- );
979
+ // ../../packages/tonbo-cli-core/dist/src/format.js
980
+ function table(headings, rows) {
981
+ const widths = headings.map((heading, column) => Math.max(heading.length, ...rows.map((row) => (row[column] ?? "").length)));
982
+ return [headings, ...rows].map((row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd()).join("\n");
983
+ }
984
+ function fields(pairs) {
985
+ const width = Math.max(...pairs.map(([label]) => label.length + 1));
986
+ return pairs.map(([label, value]) => `${`${label}:`.padEnd(width)} ${value}`).join("\n");
987
+ }
988
+ var USD = new Intl.NumberFormat("en-US", {
989
+ style: "currency",
990
+ currency: "USD",
991
+ minimumFractionDigits: 2,
992
+ maximumFractionDigits: 6
2959
993
  });
2960
- var COMMIT_SHA = /^[0-9a-f]{40}$/;
2961
- var REF_MAX_LENGTH = 255;
2962
- var SUBJECT_MAX_LENGTH = 512;
2963
- var AUTHOR_MAX_LENGTH = 255;
2964
- async function collectGitProvenance(root, run = defaultGitRunner) {
2965
- const commitSha = await read(run, ["rev-parse", "HEAD"], root);
2966
- if (!commitSha || !COMMIT_SHA.test(commitSha)) return null;
2967
- const [ref, subject, author, status, remote] = await Promise.all([
2968
- read(run, ["rev-parse", "--abbrev-ref", "HEAD"], root),
2969
- read(run, ["log", "-1", "--format=%s"], root),
2970
- read(run, ["log", "-1", "--format=%an"], root),
2971
- // Only the deployed tree matters: changes elsewhere in the repository do
2972
- // not alter the uploaded bundle.
2973
- read(run, ["status", "--porcelain", "--", "."], root),
2974
- read(run, ["remote", "get-url", "origin"], root)
2975
- ]);
2976
- return {
2977
- ...githubRepositoryUrl(remote) ? { repository_url: githubRepositoryUrl(remote) } : {},
2978
- commit_sha: commitSha,
2979
- ref: ref === "HEAD" ? null : clamp(ref, REF_MAX_LENGTH),
2980
- subject: clamp(subject, SUBJECT_MAX_LENGTH),
2981
- author: clamp(author, AUTHOR_MAX_LENGTH),
2982
- // When the status cannot be read the tree cannot be shown to match the
2983
- // commit, so the Deployment is recorded as dirty rather than clean.
2984
- dirty: status === null || status.length > 0
2985
- };
994
+ function usd(micros) {
995
+ return USD.format(micros / 1e6);
996
+ }
997
+ var BYTE_UNITS = ["KiB", "MiB", "GiB", "TiB"];
998
+ function bytes(count, absent = "unknown") {
999
+ if (count === null)
1000
+ return absent;
1001
+ if (count < 1024)
1002
+ return `${count} B`;
1003
+ let value = count;
1004
+ let unit = "B";
1005
+ for (const next of BYTE_UNITS) {
1006
+ if (value < 1024)
1007
+ break;
1008
+ value /= 1024;
1009
+ unit = next;
1010
+ }
1011
+ return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })} ${unit}`;
1012
+ }
1013
+ function when(timestamp, absent = "never") {
1014
+ return timestamp ?? absent;
1015
+ }
1016
+
1017
+ // ../../packages/tonbo-cli-core/dist/src/login.js
1018
+ async function loginCommand(deps, options = {}) {
1019
+ try {
1020
+ const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account);
1021
+ deps.output(`Logged in to account ${login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`);
1022
+ } catch (error) {
1023
+ deps.progress.fail();
1024
+ throw error;
1025
+ }
1026
+ }
1027
+ var LOGIN_PROGRESS_MESSAGES = {
1028
+ "callback-server": {
1029
+ started: "Starting local browser callback",
1030
+ completed: "Local browser callback ready"
1031
+ },
1032
+ browser: {
1033
+ started: "Opening browser",
1034
+ completed: "Browser opened"
1035
+ },
1036
+ "browser-authorization": {
1037
+ started: "Waiting for browser approval",
1038
+ completed: "Browser approval received"
1039
+ },
1040
+ "callback-close": {
1041
+ started: "Closing local browser callback",
1042
+ completed: "Local browser callback closed"
1043
+ },
1044
+ "key-redemption": {
1045
+ started: "Receiving the CLI key",
1046
+ completed: "CLI key received"
1047
+ },
1048
+ "credential-store": {
1049
+ started: "Saving login",
1050
+ completed: "Login saved"
1051
+ }
1052
+ };
1053
+ function reportLoginProgress(progress, event) {
1054
+ const messages = LOGIN_PROGRESS_MESSAGES[event.step];
1055
+ if (event.status === "started")
1056
+ progress.start(messages.started);
1057
+ else
1058
+ progress.succeed(messages.completed);
2986
1059
  }
2987
- async function read(run, args, cwd) {
1060
+ async function logoutCommand(deps) {
1061
+ let login;
1062
+ try {
1063
+ login = await deps.credentials.load();
1064
+ } catch (error) {
1065
+ if (!(error instanceof UnusableLoginError))
1066
+ throw error;
1067
+ await deps.credentials.remove();
1068
+ deps.output("Removed a saved login this CLI cannot use; it had no key to revoke.");
1069
+ return;
1070
+ }
1071
+ if (!login) {
1072
+ deps.output("Not logged in.");
1073
+ return;
1074
+ }
1075
+ let failure;
2988
1076
  try {
2989
- return (await run(args, cwd)).trim();
2990
- } catch {
2991
- return null;
1077
+ await deps.api.revokeApiKey(login.key, login.key_id);
1078
+ } catch (error) {
1079
+ if (!(error instanceof HttpError && (error.status === 401 || error.status === 404)))
1080
+ failure = error instanceof Error ? error : new Error("The revocation failed.");
2992
1081
  }
1082
+ await deps.credentials.remove();
1083
+ if (failure)
1084
+ throw new Error(`Removed the saved login, but could not revoke its key ${login.key_id}: ${failure.message} Revoke it under API keys in the Tonbo console.`, { cause: failure });
1085
+ deps.output("Logged out. The CLI key is revoked.");
2993
1086
  }
2994
- function clamp(value, maxLength) {
2995
- if (!value) return null;
2996
- return value.length > maxLength ? value.slice(0, maxLength) : value;
1087
+
1088
+ // ../../packages/tonbo-cli-core/dist/src/origin.js
1089
+ function apiOrigin(configured = process.env.TONBO_API_ORIGIN) {
1090
+ const url = new URL(configured?.trim() || DEFAULT_API_ORIGIN);
1091
+ const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
1092
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
1093
+ throw new Error("TONBO_API_ORIGIN must be an https origin.");
1094
+ return url.origin;
1095
+ }
1096
+ function loginOrigin(api) {
1097
+ const url = new URL(api);
1098
+ if (url.hostname.startsWith("api."))
1099
+ url.hostname = url.hostname.slice("api.".length);
1100
+ return url.origin;
2997
1101
  }
2998
1102
 
2999
- // src/progress.ts
1103
+ // ../../packages/tonbo-cli-core/dist/src/progress.js
3000
1104
  var FRAMES = ["|", "/", "-", "\\"];
3001
1105
  var TerminalProgress = class {
3002
- constructor(stream, intervalMs = 80) {
3003
- this.stream = stream;
3004
- this.intervalMs = intervalMs;
3005
- }
1106
+ stream;
1107
+ intervalMs;
3006
1108
  activeMessage;
3007
1109
  frame = 0;
3008
1110
  lastWidth = 0;
3009
1111
  timer;
1112
+ constructor(stream, intervalMs = 80) {
1113
+ this.stream = stream;
1114
+ this.intervalMs = intervalMs;
1115
+ }
3010
1116
  start(message) {
3011
- if (this.activeMessage) this.succeed();
1117
+ if (this.activeMessage)
1118
+ this.succeed();
3012
1119
  this.activeMessage = message;
3013
1120
  this.frame = 0;
3014
1121
  if (!this.stream.isTTY) {
@@ -3030,10 +1137,12 @@ var TerminalProgress = class {
3030
1137
  this.finish("!!", message);
3031
1138
  }
3032
1139
  finish(marker, message) {
3033
- if (this.timer) clearInterval(this.timer);
1140
+ if (this.timer)
1141
+ clearInterval(this.timer);
3034
1142
  this.timer = void 0;
3035
1143
  this.activeMessage = void 0;
3036
- if (!message) return;
1144
+ if (!message)
1145
+ return;
3037
1146
  const line = `[${marker}] ${message}`;
3038
1147
  if (this.stream.isTTY) {
3039
1148
  this.render(line);
@@ -3049,26 +1158,536 @@ var TerminalProgress = class {
3049
1158
  this.lastWidth = value.length;
3050
1159
  }
3051
1160
  };
3052
- var silentProgress = {
3053
- start: () => {
3054
- },
3055
- succeed: () => {
3056
- },
3057
- fail: () => {
1161
+
1162
+ // src/commands.ts
1163
+ import { randomUUID as randomUUID2 } from "node:crypto";
1164
+
1165
+ // src/agent-name.ts
1166
+ var AGENT_NAME_RULE = agentNameContract.$defs.name;
1167
+ var AGENT_NAME_PATTERN = new RegExp(AGENT_NAME_RULE.pattern);
1168
+ var RESERVED_AGENT_NAMES = new Set(AGENT_NAME_RULE.not.enum);
1169
+ function validateAgentName(name) {
1170
+ const valid = name.length >= AGENT_NAME_RULE.minLength && name.length <= AGENT_NAME_RULE.maxLength && AGENT_NAME_PATTERN.test(name) && !RESERVED_AGENT_NAMES.has(name);
1171
+ if (!valid) {
1172
+ throw new Error(
1173
+ `Agent name must be ${AGENT_NAME_RULE.minLength}-${AGENT_NAME_RULE.maxLength} lowercase letters, digits, or single hyphens, start with a letter, and not be reserved.`
1174
+ );
3058
1175
  }
3059
- };
1176
+ }
1177
+
1178
+ // src/commands.ts
1179
+ function print(deps, json, data, human) {
1180
+ deps.output(json ? JSON.stringify(data) : human);
1181
+ }
1182
+ function printPage(deps, json, page, human) {
1183
+ print(deps, json, page.data, human(page.data));
1184
+ if (page.next_cursor) deps.notice(`More results: run again with --cursor ${page.next_cursor}`);
1185
+ }
1186
+ async function confirm(deps, yes, action, question, expected) {
1187
+ if (yes) return;
1188
+ if (!deps.interactive())
1189
+ throw new Error(`Refusing to ${action} without confirmation: pass --yes.`);
1190
+ if (await deps.ask(await question()) !== expected)
1191
+ throw new Error("Cancelled; nothing was changed.");
1192
+ }
1193
+ async function regionListCommand(deps, options = {}) {
1194
+ const regions = await deps.api.listRegions(await deps.auth.apiKey());
1195
+ print(
1196
+ deps,
1197
+ options.json,
1198
+ regions,
1199
+ regions.length ? table(
1200
+ ["REGION", "URL", "DATABASE DEFAULT"],
1201
+ regions.map((region) => [region.id, region.url, region.default ? "yes" : ""])
1202
+ ) : "No region accepts new resources."
1203
+ );
1204
+ }
1205
+ async function selectAgentRegion(deps, key) {
1206
+ const regions = await deps.api.listRegions(key);
1207
+ if (regions.length < 2) return void 0;
1208
+ return deps.select(
1209
+ "Which region should hold the new agent? It stays there permanently.",
1210
+ regions.map((region) => ({ name: region.id, value: region.id }))
1211
+ );
1212
+ }
1213
+ async function agentCreateCommand(deps, name, options = {}) {
1214
+ validateAgentName(name);
1215
+ const key = await deps.auth.apiKey();
1216
+ const region = options.region ?? (!options.json && deps.interactive() ? await selectAgentRegion(deps, key) : void 0);
1217
+ const agent = await deps.api.createAgent(
1218
+ key,
1219
+ { name, region, ...options.role === void 0 ? {} : { role: options.role } },
1220
+ options.idempotencyKey ?? randomUUID2()
1221
+ );
1222
+ print(
1223
+ deps,
1224
+ options.json,
1225
+ agent,
1226
+ [
1227
+ `Created agent ${agent.name}.`,
1228
+ `ID: ${agent.id}`,
1229
+ `Role: ${agent.role}`,
1230
+ `Region: ${agent.region}`
1231
+ ].join("\n")
1232
+ );
1233
+ }
1234
+ async function accountShowCommand(deps, options = {}) {
1235
+ const account = await deps.api.getAccount(await deps.auth.apiKey());
1236
+ print(
1237
+ deps,
1238
+ options.json,
1239
+ account,
1240
+ fields([
1241
+ ["ID", account.id],
1242
+ ["Name", account.name],
1243
+ ["Kind", account.kind],
1244
+ ["Your role", account.member.role],
1245
+ ["Your subject", account.member.subject],
1246
+ ["Bucket limit", String(account.limits.buckets)]
1247
+ ])
1248
+ );
1249
+ }
1250
+ function memberName(member) {
1251
+ return member.name ?? member.display_name ?? "";
1252
+ }
1253
+ async function memberListCommand(deps, options = {}) {
1254
+ const page = await deps.api.listMembers(await deps.auth.apiKey(), {
1255
+ limit: options.limit,
1256
+ cursor: options.cursor
1257
+ });
1258
+ printPage(
1259
+ deps,
1260
+ options.json,
1261
+ page,
1262
+ (members) => members.length ? table(
1263
+ ["SUBJECT", "KIND", "NAME", "ROLE", "CREATED"],
1264
+ members.map((member) => [
1265
+ member.subject,
1266
+ member.kind,
1267
+ memberName(member),
1268
+ member.role,
1269
+ member.created_at
1270
+ ])
1271
+ ) : "No members."
1272
+ );
1273
+ }
1274
+ async function resolveMemberSubject(deps, key, member) {
1275
+ if (member.startsWith("agent.")) return member;
1276
+ const subjects = /* @__PURE__ */ new Set();
1277
+ let cursor;
1278
+ do {
1279
+ const page = await deps.api.listMembers(key, { cursor });
1280
+ for (const candidate of page.data)
1281
+ if (candidate.subject === member || candidate.subject === `agent.${member}` || candidate.kind === "agent" && candidate.name === member)
1282
+ subjects.add(candidate.subject);
1283
+ cursor = page.next_cursor ?? void 0;
1284
+ } while (cursor);
1285
+ const [subject, ...others] = subjects;
1286
+ if (!subject)
1287
+ throw new Error(`No member of this account is ${member}. See \`tonbo member list\`.`);
1288
+ if (others.length)
1289
+ throw new Error(
1290
+ `${member} names several members (${[...subjects].join(", ")}); give the member's subject.`
1291
+ );
1292
+ return subject;
1293
+ }
1294
+ async function memberUpdateCommand(deps, member, options = {}) {
1295
+ if (options.role === void 0 && options.name === void 0)
1296
+ throw new Error("Nothing to change: pass --role, --name or both.");
1297
+ if (options.name !== void 0) validateAgentName(options.name);
1298
+ const key = await deps.auth.apiKey();
1299
+ const subject = await resolveMemberSubject(deps, key, member);
1300
+ const updated = await deps.api.updateMember(key, subject, {
1301
+ ...options.role === void 0 ? {} : { role: options.role },
1302
+ ...options.name === void 0 ? {} : { name: options.name }
1303
+ });
1304
+ print(
1305
+ deps,
1306
+ options.json,
1307
+ updated,
1308
+ [
1309
+ `Updated ${updated.kind === "agent" ? "Agent" : "member"} ${memberName(updated) || updated.subject}.`,
1310
+ fields([
1311
+ ["Subject", updated.subject],
1312
+ ["Name", memberName(updated)],
1313
+ ["Role", updated.role]
1314
+ ])
1315
+ ].join("\n")
1316
+ );
1317
+ }
1318
+ function keyAccess(apiKey) {
1319
+ return apiKey.read_only ? "read-only" : "read-write";
1320
+ }
1321
+ function narrowedByDatabase(apiKey) {
1322
+ return apiKey.databases.length + apiKey.database_prefixes.length > 0;
1323
+ }
1324
+ function keyBuckets(apiKey) {
1325
+ if (apiKey.buckets.length) return apiKey.buckets.join(", ");
1326
+ return narrowedByDatabase(apiKey) ? "none" : "all";
1327
+ }
1328
+ function keyDatabases(apiKey) {
1329
+ if (narrowedByDatabase(apiKey))
1330
+ return [...apiKey.databases, ...apiKey.database_prefixes.map((prefix) => `${prefix}*`)].join(
1331
+ ", "
1332
+ );
1333
+ return apiKey.buckets.length ? "none" : "all";
1334
+ }
1335
+ function apiKeyFields(apiKey) {
1336
+ return fields([
1337
+ ["ID", apiKey.id],
1338
+ ["Name", apiKey.name],
1339
+ ["Member", `${apiKey.member} (${apiKey.member_kind})`],
1340
+ ["Ends in", apiKey.last4],
1341
+ ["Access", keyAccess(apiKey)],
1342
+ ["Buckets", keyBuckets(apiKey)],
1343
+ ["Databases", keyDatabases(apiKey)],
1344
+ ["Expires", when(apiKey.expires_at)],
1345
+ ["Last used", when(apiKey.last_used_at)],
1346
+ ["Created", apiKey.created_at]
1347
+ ]);
1348
+ }
1349
+ async function apiKeyListCommand(deps, options = {}) {
1350
+ const page = await deps.api.listApiKeys(await deps.auth.apiKey(), {
1351
+ limit: options.limit,
1352
+ cursor: options.cursor
1353
+ });
1354
+ printPage(
1355
+ deps,
1356
+ options.json,
1357
+ page,
1358
+ (keys) => keys.length ? table(
1359
+ [
1360
+ "ID",
1361
+ "NAME",
1362
+ "MEMBER",
1363
+ "ENDS IN",
1364
+ "ACCESS",
1365
+ "BUCKETS",
1366
+ "DATABASES",
1367
+ "EXPIRES",
1368
+ "LAST USED"
1369
+ ],
1370
+ keys.map((apiKey) => [
1371
+ apiKey.id,
1372
+ apiKey.name,
1373
+ apiKey.member,
1374
+ apiKey.last4,
1375
+ keyAccess(apiKey),
1376
+ keyBuckets(apiKey),
1377
+ keyDatabases(apiKey),
1378
+ when(apiKey.expires_at),
1379
+ when(apiKey.last_used_at)
1380
+ ])
1381
+ ) : "No API keys."
1382
+ );
1383
+ }
1384
+ async function apiKeyCreateCommand(deps, name, options = {}) {
1385
+ const key = await deps.auth.apiKey();
1386
+ const member = options.member === void 0 ? void 0 : await resolveMemberSubject(deps, key, options.member);
1387
+ const created = await deps.api.createApiKey(key, {
1388
+ name,
1389
+ member,
1390
+ read_only: options.readOnly,
1391
+ buckets: options.bucket,
1392
+ databases: options.database,
1393
+ database_prefixes: options.databasePrefix,
1394
+ expires_in: options.expiresIn
1395
+ });
1396
+ print(
1397
+ deps,
1398
+ options.json,
1399
+ created,
1400
+ [
1401
+ `Created API key ${created.name} for ${created.member}.`,
1402
+ "",
1403
+ ` ${created.key}`,
1404
+ "",
1405
+ apiKeyFields(created)
1406
+ ].join("\n")
1407
+ );
1408
+ deps.notice(
1409
+ "This is the only time the key's secret is shown: store it now. Anyone holding it acts as its member."
1410
+ );
1411
+ }
1412
+ async function apiKeyShowCommand(deps, keyId, options = {}) {
1413
+ const apiKey = await deps.api.getApiKey(await deps.auth.apiKey(), keyId);
1414
+ print(deps, options.json, apiKey, apiKeyFields(apiKey));
1415
+ }
1416
+ async function apiKeyRevokeCommand(deps, keyId, options = {}) {
1417
+ const key = await deps.auth.apiKey();
1418
+ await confirm(
1419
+ deps,
1420
+ options.yes,
1421
+ `revoke API key ${keyId}`,
1422
+ async () => {
1423
+ const apiKey = await deps.api.getApiKey(key, keyId);
1424
+ return `Revoke API key ${apiKey.name} (${apiKey.member}, ends in ${apiKey.last4})? Every token minted from it stops working. Type yes to confirm:`;
1425
+ },
1426
+ "yes"
1427
+ );
1428
+ const revoked = await deps.api.revokeApiKey(key, keyId);
1429
+ print(deps, options.json, revoked, `Revoked API key ${revoked.id} at ${revoked.revoked_at}.`);
1430
+ }
1431
+ function bucketAccess(bucket) {
1432
+ return bucket.public_read ? "public" : "private";
1433
+ }
1434
+ function bucketFields(bucket) {
1435
+ return fields([
1436
+ ["Name", bucket.name],
1437
+ ["Region", bucket.region],
1438
+ ["State", bucket.state],
1439
+ ["Read access", bucketAccess(bucket)],
1440
+ ["URL", bucket.url],
1441
+ ["Created", bucket.created_at]
1442
+ ]);
1443
+ }
1444
+ async function bucketListCommand(deps, options = {}) {
1445
+ const page = await deps.api.listBuckets(await deps.auth.apiKey(), {
1446
+ limit: options.limit,
1447
+ cursor: options.cursor
1448
+ });
1449
+ printPage(
1450
+ deps,
1451
+ options.json,
1452
+ page,
1453
+ (buckets) => buckets.length ? table(
1454
+ ["NAME", "REGION", "STATE", "READ ACCESS", "URL"],
1455
+ buckets.map((bucket) => [
1456
+ bucket.name,
1457
+ bucket.region,
1458
+ bucket.state,
1459
+ bucketAccess(bucket),
1460
+ bucket.url
1461
+ ])
1462
+ ) : "No buckets."
1463
+ );
1464
+ }
1465
+ async function bucketCreateCommand(deps, name, options = {}) {
1466
+ const bucket = await deps.api.createBucket(
1467
+ await deps.auth.apiKey(),
1468
+ { name, region: options.region },
1469
+ options.idempotencyKey ?? randomUUID2()
1470
+ );
1471
+ print(
1472
+ deps,
1473
+ options.json,
1474
+ bucket,
1475
+ [`Created bucket ${bucket.name}.`, bucketFields(bucket)].join("\n")
1476
+ );
1477
+ }
1478
+ async function bucketShowCommand(deps, name, options = {}) {
1479
+ const bucket = await deps.api.getBucket(await deps.auth.apiKey(), name);
1480
+ print(deps, options.json, bucket, bucketFields(bucket));
1481
+ }
1482
+ async function bucketUpdateCommand(deps, name, options = {}) {
1483
+ if (options.public === options.private)
1484
+ throw new Error("Pass exactly one of --public and --private.");
1485
+ const bucket = await deps.api.updateBucket(await deps.auth.apiKey(), name, {
1486
+ public_read: options.public === true
1487
+ });
1488
+ print(
1489
+ deps,
1490
+ options.json,
1491
+ bucket,
1492
+ bucket.public_read ? `Bucket ${bucket.name} is public: anyone can read it at ${bucket.url}.` : `Bucket ${bucket.name} is private: only the account's keys can read it.`
1493
+ );
1494
+ }
1495
+ async function bucketDeleteCommand(deps, name, options = {}) {
1496
+ await confirm(
1497
+ deps,
1498
+ options.yes,
1499
+ `delete bucket ${name}`,
1500
+ () => Promise.resolve(
1501
+ `Delete bucket ${name} and every stream in it? This cannot be undone, and the name is never used again. Type the bucket's name to confirm:`
1502
+ ),
1503
+ name
1504
+ );
1505
+ const bucket = await deps.api.deleteBucket(await deps.auth.apiKey(), name);
1506
+ print(
1507
+ deps,
1508
+ options.json,
1509
+ bucket,
1510
+ `Bucket ${bucket.name} is ${bucket.state}; its streams are being purged.`
1511
+ );
1512
+ }
1513
+ function databaseAccess(database) {
1514
+ return database.public_read ? "public" : "private";
1515
+ }
1516
+ function databaseSize(database) {
1517
+ const { log, snapshot } = database.stored_bytes;
1518
+ if (log === null && snapshot === null) return "unknown";
1519
+ return `${bytes(log)} log, ${bytes(snapshot, "no")} snapshot`;
1520
+ }
1521
+ function databaseOwner(database) {
1522
+ return database.owner_id ? `${database.kind} ${database.owner_id}` : database.kind;
1523
+ }
1524
+ function databaseFields(database) {
1525
+ return fields([
1526
+ ["Name", database.name],
1527
+ ["Kind", databaseOwner(database)],
1528
+ ["Region", database.region],
1529
+ ["State", database.state],
1530
+ ["Read access", databaseAccess(database)],
1531
+ ["URL", database.url],
1532
+ ["Incarnation", database.incarnation ?? "none yet"],
1533
+ ["Last write", when(database.last_write_at)],
1534
+ ["Stored", databaseSize(database)],
1535
+ ["Created", `${database.created_at} by ${database.created_by}`]
1536
+ ]);
1537
+ }
1538
+ function noticeUnavailable(deps, regions, what) {
1539
+ if (regions.length)
1540
+ deps.notice(
1541
+ `Could not read ${what} in ${regions.join(", ")}; they show as unknown or are missing.`
1542
+ );
1543
+ }
1544
+ async function databaseListCommand(deps, options = {}) {
1545
+ const page = await deps.api.listDatabases(
1546
+ await deps.auth.apiKey(),
1547
+ { limit: options.limit, cursor: options.cursor },
1548
+ options.prefix
1549
+ );
1550
+ printPage(
1551
+ deps,
1552
+ options.json,
1553
+ page,
1554
+ (databases) => databases.length ? table(
1555
+ ["NAME", "KIND", "REGION", "STATE", "READ ACCESS", "LAST WRITE", "STORED", "URL"],
1556
+ databases.map((database) => [
1557
+ database.name,
1558
+ databaseOwner(database),
1559
+ database.region,
1560
+ database.state,
1561
+ databaseAccess(database),
1562
+ when(database.last_write_at),
1563
+ databaseSize(database),
1564
+ database.url
1565
+ ])
1566
+ ) : "No databases."
1567
+ );
1568
+ noticeUnavailable(deps, page.unavailable_regions, "sizes");
1569
+ }
1570
+ async function databaseCreateCommand(deps, name, options = {}) {
1571
+ const database = await deps.api.createDatabase(
1572
+ await deps.auth.apiKey(),
1573
+ { name, region: options.region },
1574
+ options.idempotencyKey ?? randomUUID2()
1575
+ );
1576
+ print(
1577
+ deps,
1578
+ options.json,
1579
+ database,
1580
+ [
1581
+ database.state === "active" ? `Created database ${database.name}.` : `Database ${database.name} is being created; \`tonbo database show ${database.name}\` reports it active when it is ready.`,
1582
+ databaseFields(database)
1583
+ ].join("\n")
1584
+ );
1585
+ }
1586
+ async function databaseShowCommand(deps, name, options = {}) {
1587
+ const database = await deps.api.getDatabase(await deps.auth.apiKey(), name);
1588
+ print(deps, options.json, database, databaseFields(database));
1589
+ }
1590
+ async function databaseUpdateCommand(deps, name, options = {}) {
1591
+ if (options.public === options.private)
1592
+ throw new Error("Pass exactly one of --public and --private.");
1593
+ const database = await deps.api.updateDatabase(await deps.auth.apiKey(), name, {
1594
+ public_read: options.public === true
1595
+ });
1596
+ print(
1597
+ deps,
1598
+ options.json,
1599
+ database,
1600
+ database.public_read ? `Database ${database.name} is public: anyone can read it at ${database.url}.` : `Database ${database.name} is private: only the account's keys can read it.`
1601
+ );
1602
+ }
1603
+ async function databaseDeleteCommand(deps, name, options = {}) {
1604
+ await confirm(
1605
+ deps,
1606
+ options.yes,
1607
+ `delete database ${name}`,
1608
+ () => Promise.resolve(
1609
+ `Delete database ${name} and everything in it? This cannot be undone. Type the database's name to confirm:`
1610
+ ),
1611
+ name
1612
+ );
1613
+ const database = await deps.api.deleteDatabase(await deps.auth.apiKey(), name);
1614
+ print(
1615
+ deps,
1616
+ options.json,
1617
+ database,
1618
+ database.state === "deleted" ? `Deleted database ${database.name}; its name is free again.` : `Database ${database.name} is ${database.state}; its name is free again once it is deleted.`
1619
+ );
1620
+ }
1621
+ function databaseUsageSort(value) {
1622
+ if (value === "-list_value" || value === "list_value") return "-list_value";
1623
+ if (value === "name") return "name";
1624
+ throw new Error("--sort is list_value or name.");
1625
+ }
1626
+ async function databaseUsageCommand(deps, options = {}) {
1627
+ const usage = await deps.api.listDatabaseUsage(
1628
+ await deps.auth.apiKey(),
1629
+ { limit: options.limit, cursor: options.cursor },
1630
+ { period: options.period, sort: options.sort, prefix: options.prefix }
1631
+ );
1632
+ if (options.json) deps.output(JSON.stringify(usage));
1633
+ else
1634
+ deps.output(
1635
+ [
1636
+ `Database usage in ${usage.period} at list price, before the monthly allowance and credit (${usage.currency.toUpperCase()}; metered up to ${when(usage.as_of, "not yet metered")})`,
1637
+ usage.data.length ? table(
1638
+ [
1639
+ "NAME",
1640
+ "REGION",
1641
+ "STATE",
1642
+ "WRITE UNITS",
1643
+ "LOG GIB-MONTHS",
1644
+ "SNAPSHOT GIB-MONTHS",
1645
+ "LIST VALUE"
1646
+ ],
1647
+ usage.data.map((row) => [
1648
+ row.name,
1649
+ row.region,
1650
+ row.state,
1651
+ row.write_units,
1652
+ row.retention_gib_months,
1653
+ row.snapshot_gib_months ?? "",
1654
+ usd(row.list_value_micros)
1655
+ ])
1656
+ ) : "No database usage.",
1657
+ `Other metered usage: ${usd(usage.other.list_value_micros)}`,
1658
+ "Charges, after the allowance and credit, are in `tonbo usage show`."
1659
+ ].join("\n")
1660
+ );
1661
+ if (usage.next_cursor) deps.notice(`More results: run again with --cursor ${usage.next_cursor}`);
1662
+ noticeUnavailable(deps, usage.unavailable_regions, "usage");
1663
+ }
1664
+ async function usageShowCommand(deps, options = {}) {
1665
+ const usage = await deps.api.getUsage(await deps.auth.apiKey(), options.period);
1666
+ print(
1667
+ deps,
1668
+ options.json,
1669
+ usage,
1670
+ [
1671
+ `Usage in ${usage.period} (${usage.currency.toUpperCase()})`,
1672
+ usage.lines.length ? table(
1673
+ ["PRODUCT", "REGION", "DIMENSION", "MODEL", "QUANTITY", "AMOUNT"],
1674
+ usage.lines.map((line) => [
1675
+ line.product,
1676
+ line.region,
1677
+ line.dimension,
1678
+ line.model ?? "",
1679
+ line.quantity,
1680
+ usd(line.amount_micros)
1681
+ ])
1682
+ ) : "No rated usage.",
1683
+ `Total: ${usd(usage.total_micros)}`
1684
+ ].join("\n")
1685
+ );
1686
+ }
3060
1687
 
3061
1688
  // src/prompt.ts
3062
1689
  import select from "@inquirer/select";
3063
1690
  import { createInterface } from "node:readline/promises";
3064
- async function terminalPrompt(question) {
3065
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
3066
- try {
3067
- return await prompt.question(`? ${question}`);
3068
- } finally {
3069
- prompt.close();
3070
- }
3071
- }
3072
1691
  async function terminalSelect(question, choices) {
3073
1692
  return select(
3074
1693
  {
@@ -3082,189 +1701,225 @@ async function terminalSelect(question, choices) {
3082
1701
  }
3083
1702
  );
3084
1703
  }
1704
+ async function terminalAsk(question) {
1705
+ const terminal = createInterface({ input: process.stdin, output: process.stderr });
1706
+ try {
1707
+ return (await terminal.question(`${question} `)).trim();
1708
+ } finally {
1709
+ terminal.close();
1710
+ }
1711
+ }
3085
1712
 
3086
1713
  // src/app.ts
3087
1714
  var packageVersion = JSON.parse(
3088
1715
  readFileSync(new URL("../../package.json", import.meta.url), "utf8")
3089
1716
  );
3090
- function createDependencies(json = false) {
3091
- const accountOrigin = process.env.TONBO_ACCOUNT_ORIGIN || "https://tonbo.dev";
3092
- const managementOrigin = process.env.TONBO_API_ORIGIN || "https://api.tonbo.dev";
1717
+ function createDependencies() {
1718
+ const api = apiOrigin();
1719
+ const credentials = new FileCredentialStore();
3093
1720
  return {
3094
- api: new TonboApi(fetch, accountOrigin, managementOrigin),
3095
- auth: new AuthClient(new FileCredentialStore(), fetch, accountOrigin),
3096
- cwd: () => process.cwd(),
3097
- defaultSshPublicKeys: readDefaultSshPublicKeys,
3098
- gitProvenance: collectGitProvenance,
3099
- interactive: () => !json && process.stdin.isTTY === true && process.stderr.isTTY === true,
3100
- inspectSource: inspectLocalAgentSource,
3101
- output: (value) => {
3102
- if (json) console.log(JSON.stringify(value));
3103
- else console.log(value.message ?? value);
3104
- },
3105
- progress: json ? silentProgress : new TerminalProgress(process.stderr),
3106
- prompt: terminalPrompt,
3107
- select: terminalSelect,
3108
- secretValue: (name, fromEnvironment) => {
3109
- const environmentName = fromEnvironment ?? name;
3110
- const value = process.env[environmentName];
3111
- if (!value)
3112
- throw new Error(
3113
- `Environment variable ${environmentName} is empty. Set it before running tonbo secret set.`
3114
- );
3115
- return Promise.resolve(value);
3116
- }
1721
+ api: new ManagementApi(fetch, api),
1722
+ ask: terminalAsk,
1723
+ auth: new AuthClient(credentials, fetch, loginOrigin(api)),
1724
+ credentials,
1725
+ interactive: () => process.stdin.isTTY === true && process.stderr.isTTY === true,
1726
+ output: (text) => console.log(text),
1727
+ notice: (text) => console.error(text),
1728
+ progress: new TerminalProgress(process.stderr),
1729
+ select: terminalSelect
3117
1730
  };
3118
1731
  }
3119
- function jsonOutput(program) {
3120
- return program.opts().json === true;
1732
+ function parseLimit(value) {
1733
+ if (!/^[1-9][0-9]*$/.test(value)) throw new InvalidArgumentError("Not a positive integer.");
1734
+ return Number(value);
3121
1735
  }
1736
+ function operationOptions(command, operationId) {
1737
+ const query = managementOperations[operationId].query;
1738
+ if (query.includes("limit"))
1739
+ command.option("--limit <count>", "the most items to return", parseLimit);
1740
+ if (query.includes("cursor"))
1741
+ command.option("--cursor <cursor>", "continue from the cursor a previous page printed");
1742
+ return command.option("--json", "print the API data as JSON");
1743
+ }
1744
+ var repeated = (value, previous = []) => [...previous, value];
1745
+ var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
3122
1746
  function createProgram(dependencies = createDependencies) {
3123
- const program = new Command().name("tonbo").description("Deploy a persistent Agent to Tonbo.").version(packageVersion.version).option("--json", "print machine-readable JSON");
3124
- program.command("init").description("interactively create a Tonbo Agent declaration in this directory").option("--harness <harness>", "Agent Harness (currently pi)").option("--model <model>", "inference model").option("--driver <driver>", "PI driver: native or command").option("--agent-entry <file>", "Node entry file for the command driver").option("--build-command <argv...>", "build command argv for the command driver").option("--region <region>", "home region of an agent init creates (see tonbo region list)").option("--force", `replace an existing ${DECLARATION_FILENAME}`).action(
3125
- async (options) => initCommand(dependencies(jsonOutput(program)), options)
1747
+ const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
1748
+ program.command("login").description("sign in through the browser and save a personal key for this computer").option("--account <name>", "the account to log in to; defaults to your personal account").action(async (options) => loginCommand(dependencies(), options));
1749
+ program.command("logout").description("revoke this computer's key and remove the saved login").action(async () => logoutCommand(dependencies()));
1750
+ const account = program.command("account").description("the account you are logged in to");
1751
+ operationOptions(
1752
+ account.command("show").description("show the account, your role in it and its limits"),
1753
+ "getAccount"
1754
+ ).action(async (options) => accountShowCommand(dependencies(), options));
1755
+ operationOptions(
1756
+ program.command("region").description("regions new resources can be created in").command("list").description("list the regions"),
1757
+ "listRegions"
1758
+ ).action(async (options) => regionListCommand(dependencies(), options));
1759
+ const member = program.command("member").description("the people and Agents of the account");
1760
+ operationOptions(
1761
+ member.command("list").description("list the members and their roles"),
1762
+ "listMembers"
1763
+ ).action(
1764
+ async (options) => memberListCommand(dependencies(), options)
3126
1765
  );
3127
- program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(jsonOutput(program))));
3128
- program.command("region").description("discover regions and Machine allocation availability").command("list").action(async () => {
3129
- const deps = dependencies(jsonOutput(program));
3130
- deps.output(await deps.api.listRegions(await deps.auth.accessToken()));
3131
- });
3132
- const machine = program.command("machine").description("manage independent account-owned Machines");
3133
- machine.command("list").option("--account <account>", "account UUID").action(async (options) => {
3134
- const deps = dependencies(jsonOutput(program));
3135
- deps.output(
3136
- await deps.api.machineRequest(
3137
- await deps.auth.accessToken(),
3138
- options.account ? `?account=${encodeURIComponent(options.account)}` : ""
3139
- )
3140
- );
3141
- });
3142
- machine.command("allocate").requiredOption("--account <account>", "owning account UUID").requiredOption("--region <region>", "compute region (see tonbo region list)").option(
3143
- "--request-id <uuid>",
3144
- "stable allocation request UUID; reuse it after an uncertain response"
3145
- ).action(async (options) => {
3146
- const deps = dependencies(jsonOutput(program));
3147
- const requestId = options.requestId ?? randomUUID5();
3148
- try {
3149
- const result = await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {
3150
- accountId: options.account,
3151
- region: options.region,
3152
- requestId
3153
- });
3154
- deps.output({ ...result, requestId });
3155
- } catch (cause) {
3156
- throw new Error(
3157
- `Machine allocation was not confirmed. Retry with --request-id ${requestId} and the same account and region.`,
3158
- { cause }
3159
- );
3160
- }
3161
- });
3162
- machine.command("allocation <requestId>").description("query the original Machine for an allocation request").requiredOption("--account <account>", "owning account UUID").action(async (requestId, options) => {
3163
- const deps = dependencies(jsonOutput(program));
3164
- deps.output(
3165
- await deps.api.machineRequest(
3166
- await deps.auth.accessToken(),
3167
- `/allocations/${encodeURIComponent(requestId)}?account=${encodeURIComponent(options.account)}`
3168
- )
3169
- );
3170
- });
3171
- for (const operation of ["show", "metrics", "release"]) {
3172
- machine.command(`${operation} <machine>`).action(async (id) => {
3173
- const deps = dependencies(jsonOutput(program));
3174
- const result = await deps.api.machineRequest(
3175
- await deps.auth.accessToken(),
3176
- `/${encodeURIComponent(id)}`,
3177
- operation === "release" ? "DELETE" : "GET"
3178
- );
3179
- deps.output(operation === "metrics" ? result.metrics : result);
3180
- });
3181
- }
3182
- machine.command("bind <machine>").requiredOption("--agent <agent>", "Agent UUID").action(async (id, options) => {
3183
- const deps = dependencies(jsonOutput(program));
3184
- deps.output(
3185
- await deps.api.machineRequest(
3186
- await deps.auth.accessToken(),
3187
- `/${encodeURIComponent(id)}/binding`,
3188
- "POST",
3189
- { agentId: options.agent }
3190
- )
3191
- );
3192
- });
3193
- machine.command("unbind <machine>").description("detach Agent and reset system disk; retain Agent Workspace and Sessions").action(async (id) => {
3194
- const deps = dependencies(jsonOutput(program));
3195
- const token = await deps.auth.accessToken();
3196
- const path6 = `/${encodeURIComponent(id)}`;
3197
- const current = await deps.api.machineRequest(token, path6);
3198
- if (!current.binding) throw new Error("Machine is not bound to an Agent.");
3199
- deps.output(
3200
- await deps.api.machineRequest(token, `${path6}/binding`, "DELETE", {
3201
- agentId: current.binding.agent_id,
3202
- bindingId: current.binding.id,
3203
- generation: current.binding.generation
3204
- })
3205
- );
3206
- });
3207
- const agent = program.command("agent").description("manage the Agent bound to this source directory");
3208
- agent.command("create <name>").description("create an agent and bind this directory to it").option("--account <account>", "owning account UUID; defaults to your personal account").option(
3209
- "--region <region>",
3210
- "permanent home region (see tonbo region list); required once several regions accept new agents"
3211
- ).option("--request-id <uuid>", "reuse the same creation UUID after an uncertain response").action(
3212
- async (name, options) => agentCreateCommand(dependencies(jsonOutput(program)), name, options)
1766
+ operationOptions(
1767
+ member.command("update <member>").description(
1768
+ "change a member's role, or rename an Agent; <member> is a subject, or an Agent's id or name"
1769
+ ).addOption(roleOption("the member's new role; only an owner grants owner")).option("--name <name>", "the Agent's new name"),
1770
+ "updateMember"
1771
+ ).action(
1772
+ async (subject, options) => memberUpdateCommand(dependencies(), subject, options)
3213
1773
  );
3214
- agent.command("creation <requestId>").description("query the original Agent for a creation request").option("--account <account>", "owning account UUID; defaults to your personal account").action(async (requestId, options) => {
3215
- const deps = dependencies(jsonOutput(program));
3216
- deps.output(
3217
- await deps.api.readAgentCreationRequest(
3218
- await deps.auth.accessToken(),
3219
- requestId,
3220
- options.account
1774
+ operationOptions(
1775
+ program.command("agent").description("manage the Agents of your account").command("create <name>").description("create an agent").addOption(roleOption("the Agent's role; member by default")).option(
1776
+ "--region <region>",
1777
+ "permanent home region (see tonbo region list); required once several regions accept new agents"
1778
+ ).option(
1779
+ "--idempotency-key <key>",
1780
+ "reuse the key of an unconfirmed creation to get the Agent it created"
1781
+ ),
1782
+ "createAgent"
1783
+ ).action(
1784
+ async (name, options) => agentCreateCommand(dependencies(), name, options)
1785
+ );
1786
+ const apiKey = program.command("api-key").description("the keys of the account's members");
1787
+ operationOptions(
1788
+ apiKey.command("list").description("list your keys, or every key of the account for an owner or admin"),
1789
+ "listApiKeys"
1790
+ ).action(
1791
+ async (options) => apiKeyListCommand(dependencies(), options)
1792
+ );
1793
+ operationOptions(
1794
+ apiKey.command("create <name>").description("create a key and print its secret, once").option(
1795
+ "--member <member>",
1796
+ "an Agent (subject, id or name) to create the key for; yourself by default"
1797
+ ).option("--read-only", "narrow the key to read-only actions, for its whole life").option(
1798
+ "--bucket <name>",
1799
+ "narrow the key to this bucket, for its whole life; repeat for several",
1800
+ repeated
1801
+ ).option(
1802
+ "--database <name>",
1803
+ "narrow the key to this database, for its whole life; repeat for several; never with --bucket",
1804
+ repeated
1805
+ ).option(
1806
+ "--database-prefix <prefix>",
1807
+ "narrow the key to the databases whose name starts with this, for its whole life; repeat for several; never with --bucket",
1808
+ repeated
1809
+ ).addOption(
1810
+ new Option("--expires-in <period>", "when the key expires; never by default").choices(
1811
+ apiKeyExpirations
3221
1812
  )
3222
- );
3223
- });
3224
- agent.command("list").description("list the agents available to your account").action(async () => agentListCommand(dependencies(jsonOutput(program))));
3225
- const sshKey = program.command("ssh-key").description("manage public keys used by native agent SSH");
3226
- sshKey.command("add <public-key>").description("register an OpenSSH public key with the current Tonbo account").action(async (path6) => sshKeyAddCommand(dependencies(jsonOutput(program)), path6));
3227
- sshKey.command("remove <fingerprint>").description("revoke an SSH public key from the current Tonbo account").action(
3228
- async (fingerprint) => sshKeyRemoveCommand(dependencies(jsonOutput(program)), fingerprint)
1813
+ ),
1814
+ "createApiKey"
1815
+ ).action(
1816
+ async (name, options) => apiKeyCreateCommand(dependencies(), name, options)
1817
+ );
1818
+ operationOptions(apiKey.command("show <key-id>").description("show one key"), "getApiKey").action(
1819
+ async (keyId, options) => apiKeyShowCommand(dependencies(), keyId, options)
1820
+ );
1821
+ operationOptions(
1822
+ apiKey.command("revoke <key-id>").description("revoke a key and every token minted from it").option("--yes", "revoke without asking; required when stdin is not a terminal"),
1823
+ "revokeApiKey"
1824
+ ).action(
1825
+ async (keyId, options) => apiKeyRevokeCommand(dependencies(), keyId, options)
3229
1826
  );
3230
- agent.command("use <agent>").description("write the agent hostname into this directory's .tonbo declaration").option("--force", "replace an existing agent binding without confirmation").action(
3231
- async (selector, options) => agentUseCommand(dependencies(jsonOutput(program)), selector, options.force === true)
1827
+ const bucket = program.command("bucket").description("the account's Streams buckets");
1828
+ operationOptions(bucket.command("list").description("list the buckets"), "listBuckets").action(
1829
+ async (options) => bucketListCommand(dependencies(), options)
3232
1830
  );
3233
- agent.command("show").description("show the agent bound to this directory and its connection addresses").action(async () => agentShowCommand(dependencies(jsonOutput(program))));
3234
- program.command("deploy").description("upload this directory as a new Deployment and promote it to Production").option("--no-promote", "create the Deployment without moving Production").action(
3235
- async (options) => deployCommand(dependencies(jsonOutput(program)), { promote: options.promote })
1831
+ operationOptions(
1832
+ bucket.command("create <name>").description("create a bucket").option(
1833
+ "--region <region>",
1834
+ "the bucket's region (see tonbo region list); the account's default region by default"
1835
+ ).option(
1836
+ "--idempotency-key <key>",
1837
+ "reuse the key of an unconfirmed creation to get the bucket it created"
1838
+ ),
1839
+ "createBucket"
1840
+ ).action(
1841
+ async (name, options) => bucketCreateCommand(dependencies(), name, options)
1842
+ );
1843
+ operationOptions(bucket.command("show <name>").description("show a bucket"), "getBucket").action(
1844
+ async (name, options) => bucketShowCommand(dependencies(), name, options)
1845
+ );
1846
+ operationOptions(
1847
+ bucket.command("update <name>").description("publish a bucket for anonymous reads, or take it private").addOption(new Option("--public", "serve anonymous reads").conflicts("private")).addOption(new Option("--private", "serve only the account's keys").conflicts("public")),
1848
+ "updateBucket"
1849
+ ).action(
1850
+ async (name, options) => bucketUpdateCommand(dependencies(), name, options)
3236
1851
  );
3237
- const deployments = program.command("deployments").description("list and move between the immutable Deployments of the bound agent");
3238
- deployments.command("list", { isDefault: true }).description("list Deployments, newest first").action(async () => deploymentsListCommand(dependencies(jsonOutput(program))));
3239
- deployments.command("show <deployment>").description("show one Deployment and its Rollouts by id prefix").action(
3240
- async (prefix) => deploymentsShowCommand(dependencies(jsonOutput(program)), prefix)
1852
+ operationOptions(
1853
+ bucket.command("delete <name>").description("delete a bucket and every stream in it").option("--yes", "delete without asking; required when stdin is not a terminal"),
1854
+ "deleteBucket"
1855
+ ).action(
1856
+ async (name, options) => bucketDeleteCommand(dependencies(), name, options)
1857
+ );
1858
+ const database = program.command("database").description("the account's databases");
1859
+ operationOptions(
1860
+ database.command("list").description("list the databases in every region").option("--prefix <prefix>", "only databases whose name starts with this"),
1861
+ "listDatabases"
1862
+ ).action(
1863
+ async (options) => databaseListCommand(dependencies(), options)
3241
1864
  );
3242
- deployments.command("promote <deployment>").description("move Production to a Deployment by id prefix").action(
3243
- async (prefix) => deploymentsPromoteCommand(dependencies(jsonOutput(program)), prefix)
1865
+ operationOptions(
1866
+ database.command("create <name>").description("create a database; its region is fixed for its life").option(
1867
+ "--region <region>",
1868
+ "the database's region (see tonbo region list); the database default region by default"
1869
+ ).option(
1870
+ "--idempotency-key <key>",
1871
+ "reuse the key of an unconfirmed creation to get the database it created"
1872
+ ),
1873
+ "createDatabase"
1874
+ ).action(
1875
+ async (name, options) => databaseCreateCommand(dependencies(), name, options)
3244
1876
  );
3245
- deployments.command("cancel-activities").description("cancel selected named work on an exact retired runtime").requiredOption("--sandbox <id>", "retired Sandbox UUID").requiredOption("--runtime-session <id>", "exact activation UUID").requiredOption("--work-key <keys...>", "named work keys to cancel").requiredOption("--reason <reason>", "reason retained in the cancellation audit").action(
3246
- async (options) => cancelActivitiesCommand(dependencies(jsonOutput(program)), options)
1877
+ operationOptions(
1878
+ database.command("show <name>").description("show a database, its URL and size"),
1879
+ "getDatabase"
1880
+ ).action(
1881
+ async (name, options) => databaseShowCommand(dependencies(), name, options)
3247
1882
  );
3248
- deployments.command("rollback [deployment]").description("move Production back to the previous Production Deployment, or to a named one").action(
3249
- async (prefix) => deploymentsRollbackCommand(dependencies(jsonOutput(program)), prefix)
1883
+ operationOptions(
1884
+ database.command("update <name>").description("publish a database for anonymous reads, or take it private (owners and admins)").addOption(new Option("--public", "serve anonymous reads").conflicts("private")).addOption(new Option("--private", "serve only the account's keys").conflicts("public")),
1885
+ "updateDatabase"
1886
+ ).action(
1887
+ async (name, options) => databaseUpdateCommand(dependencies(), name, options)
3250
1888
  );
3251
- program.command("run <prompt>").description("run one prompt as a durable Turn in an agent session and wait for its result").option("--session <session>", "resume an existing Agent Session UUID").option(
3252
- "--no-wait",
3253
- "return once the Turn is admitted; the platform runs it to its end (see tonbo turn wait)"
1889
+ operationOptions(
1890
+ database.command("delete <name>").description("delete a database and everything in it").option("--yes", "delete without asking; required when stdin is not a terminal"),
1891
+ "deleteDatabase"
3254
1892
  ).action(
3255
- async (prompt, options) => runCommand(dependencies(jsonOutput(program)), prompt, options)
1893
+ async (name, options) => databaseDeleteCommand(dependencies(), name, options)
3256
1894
  );
3257
- program.command("turn").description("follow durable Turns of the bound agent").command("wait <turn>").description(
3258
- "wait for a Turn's result; the Turn runs on the platform whether or not anyone waits"
3259
- ).requiredOption("--session <session>", "the Turn's Agent Session UUID").action(
3260
- async (turnId, options) => turnWaitCommand(dependencies(jsonOutput(program)), turnId, options)
1895
+ operationOptions(
1896
+ database.command("usage").description("each database's usage of one month at list price, before allowance and credit").option(
1897
+ "--period <month>",
1898
+ "the calendar month in UTC, as YYYY-MM; the current one by default"
1899
+ ).option(
1900
+ "--sort <order>",
1901
+ "list_value (highest first, the default) or name",
1902
+ (value) => {
1903
+ try {
1904
+ return databaseUsageSort(value);
1905
+ } catch (error) {
1906
+ throw new InvalidArgumentError(error.message);
1907
+ }
1908
+ }
1909
+ ).option("--prefix <prefix>", "only databases whose name starts with this"),
1910
+ "listDatabaseUsage"
1911
+ ).action(
1912
+ async (options) => databaseUsageCommand(dependencies(), options)
3261
1913
  );
3262
- const secret = program.command("secret").description("manage encrypted environment secrets for the agent service");
3263
- secret.command("list").action(async () => secretListCommand(dependencies(jsonOutput(program))));
3264
- secret.command("set <name>").description("set a secret from an environment variable (the same name by default)").option("--from-env <name>", "read the value from another environment variable").action(
3265
- async (name, options) => secretSetCommand(dependencies(jsonOutput(program)), name, options)
1914
+ operationOptions(
1915
+ program.command("usage").description("the account's rated usage").command("show").description("show one month's rated usage").option(
1916
+ "--period <month>",
1917
+ "the calendar month in UTC, as YYYY-MM; the current one by default"
1918
+ ),
1919
+ "getUsage"
1920
+ ).action(
1921
+ async (options) => usageShowCommand(dependencies(), options)
3266
1922
  );
3267
- secret.command("remove <name>").action(async (name) => secretRemoveCommand(dependencies(jsonOutput(program)), name));
3268
1923
  return program;
3269
1924
  }
3270
1925