@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.
- package/README.md +66 -189
- package/dist/bin/tonbo.js +1617 -2962
- 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 {
|
|
5
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
6
5
|
import { readFileSync } from "node:fs";
|
|
7
6
|
|
|
8
|
-
// src/
|
|
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.
|
|
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
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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/
|
|
1748
|
-
var
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
|
|
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
|
|
1786
|
-
|
|
1787
|
-
const body =
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
const
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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/
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
|
2073
|
-
if (
|
|
2074
|
-
return
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
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
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
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
|
-
"
|
|
2246
|
-
|
|
2247
|
-
|
|
521
|
+
"listRegions": {
|
|
522
|
+
"method": "GET",
|
|
523
|
+
"path": "/regions",
|
|
524
|
+
"pathParams": [],
|
|
525
|
+
"query": [],
|
|
526
|
+
"idempotencyKey": false,
|
|
527
|
+
"cli": "tonbo region list"
|
|
2248
528
|
},
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
-
"
|
|
2254
|
-
|
|
2255
|
-
|
|
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
|
-
"
|
|
2258
|
-
|
|
2259
|
-
|
|
550
|
+
"createAgent": {
|
|
551
|
+
"method": "POST",
|
|
552
|
+
"path": "/agents",
|
|
553
|
+
"pathParams": [],
|
|
554
|
+
"query": [],
|
|
555
|
+
"idempotencyKey": true,
|
|
556
|
+
"cli": "tonbo agent create"
|
|
2260
557
|
},
|
|
2261
|
-
"
|
|
2262
|
-
|
|
2263
|
-
|
|
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
|
-
"
|
|
2266
|
-
|
|
2267
|
-
|
|
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
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
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
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
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
|
-
|
|
2320
|
-
|
|
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
|
-
|
|
2331
|
-
|
|
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
|
-
|
|
2349
|
-
|
|
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
|
-
|
|
2390
|
-
|
|
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
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
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
|
-
|
|
2664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
},
|
|
2802
|
-
|
|
2803
|
-
)
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
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.
|
|
2865
|
-
import { randomUUID
|
|
2866
|
-
import { chmod, lstat
|
|
2867
|
-
import
|
|
2868
|
-
import
|
|
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 ||
|
|
2871
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
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 =
|
|
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
|
|
2901
|
-
await handle.writeFile(`${JSON.stringify(
|
|
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")
|
|
2907
|
-
|
|
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
|
|
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
|
|
2916
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
|
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")
|
|
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
|
|
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")
|
|
967
|
+
if (error.code !== "ENOENT")
|
|
968
|
+
throw error;
|
|
2933
969
|
}
|
|
2934
970
|
}
|
|
2935
971
|
async function assertPrivateRegularFile(filename) {
|
|
2936
|
-
const stat = await
|
|
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/
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
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
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
var
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
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
|
|
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
|
-
|
|
2990
|
-
} catch {
|
|
2991
|
-
|
|
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
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
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.
|
|
1103
|
+
// ../../packages/tonbo-cli-core/dist/src/progress.js
|
|
3000
1104
|
var FRAMES = ["|", "/", "-", "\\"];
|
|
3001
1105
|
var TerminalProgress = class {
|
|
3002
|
-
|
|
3003
|
-
|
|
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)
|
|
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)
|
|
1140
|
+
if (this.timer)
|
|
1141
|
+
clearInterval(this.timer);
|
|
3034
1142
|
this.timer = void 0;
|
|
3035
1143
|
this.activeMessage = void 0;
|
|
3036
|
-
if (!message)
|
|
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
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
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(
|
|
3091
|
-
const
|
|
3092
|
-
const
|
|
1717
|
+
function createDependencies() {
|
|
1718
|
+
const api = apiOrigin();
|
|
1719
|
+
const credentials = new FileCredentialStore();
|
|
3093
1720
|
return {
|
|
3094
|
-
api: new
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
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
|
|
3120
|
-
|
|
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("
|
|
3124
|
-
program.command("
|
|
3125
|
-
|
|
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
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
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
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
async (
|
|
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
|
-
|
|
3231
|
-
|
|
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
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
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
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
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
|
-
|
|
3243
|
-
|
|
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
|
-
|
|
3246
|
-
|
|
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
|
-
|
|
3249
|
-
|
|
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
|
-
|
|
3252
|
-
"--
|
|
3253
|
-
"
|
|
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 (
|
|
1893
|
+
async (name, options) => databaseDeleteCommand(dependencies(), name, options)
|
|
3256
1894
|
);
|
|
3257
|
-
|
|
3258
|
-
"
|
|
3259
|
-
|
|
3260
|
-
|
|
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
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
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
|
|