@tonbo/cli 0.0.6 → 0.1.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.
@@ -0,0 +1,2629 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/app.ts
4
+ import { Command } from "commander";
5
+ import { readFileSync } from "node:fs";
6
+
7
+ // src/api.ts
8
+ import { randomUUID } from "node:crypto";
9
+
10
+ // src/http.ts
11
+ var HttpError = class extends Error {
12
+ constructor(message, status, body) {
13
+ super(message);
14
+ this.status = status;
15
+ this.body = body;
16
+ }
17
+ };
18
+ async function requestJson(fetcher, url, init = {}) {
19
+ const response = await fetcher(url, init);
20
+ const body = await response.json().catch(() => null);
21
+ if (!response.ok) {
22
+ const record = body && typeof body === "object" ? body : {};
23
+ const message = [record.detail, record.message, record.error, record.title].find(
24
+ (value) => typeof value === "string"
25
+ );
26
+ throw new HttpError(
27
+ typeof message === "string" ? message : `Request failed with HTTP ${response.status}.`,
28
+ response.status,
29
+ body
30
+ );
31
+ }
32
+ return body;
33
+ }
34
+
35
+ // src/api.ts
36
+ var TonboApi = class {
37
+ constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
38
+ this.fetcher = fetcher;
39
+ this.accountOrigin = accountOrigin;
40
+ this.managementOrigin = managementOrigin;
41
+ }
42
+ machineRequest(oauthToken, path6 = "", method = "GET", body) {
43
+ return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/machines${path6}`, {
44
+ method,
45
+ headers: {
46
+ authorization: `Bearer ${oauthToken}`,
47
+ ...body === void 0 ? {} : { "content-type": "application/json" }
48
+ },
49
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
50
+ });
51
+ }
52
+ listProjects(oauthToken) {
53
+ return requestJson(
54
+ this.fetcher,
55
+ `${this.accountOrigin}/api/cli/projects`,
56
+ {
57
+ headers: { authorization: `Bearer ${oauthToken}` }
58
+ }
59
+ ).then((body) => body.projects);
60
+ }
61
+ listOrganizations(oauthToken) {
62
+ return requestJson(
63
+ this.fetcher,
64
+ `${this.accountOrigin}/api/cli/accounts`,
65
+ {
66
+ headers: { authorization: `Bearer ${oauthToken}` }
67
+ }
68
+ ).then((body) => body.organizations);
69
+ }
70
+ createProject(oauthToken, name, organizationId) {
71
+ return requestJson(
72
+ this.fetcher,
73
+ `${this.accountOrigin}/api/cli/projects`,
74
+ {
75
+ method: "POST",
76
+ headers: {
77
+ authorization: `Bearer ${oauthToken}`,
78
+ "content-type": "application/json"
79
+ },
80
+ body: JSON.stringify(organizationId ? { name, orgId: organizationId } : { name })
81
+ }
82
+ ).then((body) => body.project);
83
+ }
84
+ registerSshKey(oauthToken, key) {
85
+ return requestJson(
86
+ this.fetcher,
87
+ `${this.accountOrigin}/api/cli/ssh-keys`,
88
+ {
89
+ method: "POST",
90
+ headers: {
91
+ authorization: `Bearer ${oauthToken}`,
92
+ "content-type": "application/json"
93
+ },
94
+ body: JSON.stringify({
95
+ algorithm: key.algorithm,
96
+ key_base64: key.keyBase64,
97
+ label: key.label
98
+ })
99
+ }
100
+ ).then((body) => body.key);
101
+ }
102
+ revokeSshKey(oauthToken, fingerprint) {
103
+ return requestJson(
104
+ this.fetcher,
105
+ `${this.accountOrigin}/api/cli/ssh-keys`,
106
+ {
107
+ method: "DELETE",
108
+ headers: {
109
+ authorization: `Bearer ${oauthToken}`,
110
+ "content-type": "application/json"
111
+ },
112
+ body: JSON.stringify({ fingerprint })
113
+ }
114
+ ).then((body) => body.key);
115
+ }
116
+ exchangeManagementToken(oauthToken, projectId) {
117
+ return requestJson(
118
+ this.fetcher,
119
+ `${this.accountOrigin}/api/cli/projects/${projectId}/token`,
120
+ {
121
+ method: "POST",
122
+ headers: { authorization: `Bearer ${oauthToken}` }
123
+ }
124
+ ).then((body) => body.access_token);
125
+ }
126
+ async deploy({
127
+ bundle,
128
+ origin,
129
+ projectId,
130
+ promote,
131
+ spec,
132
+ token
133
+ }) {
134
+ const descriptor = {
135
+ format: bundle.format,
136
+ sha256: bundle.sha256,
137
+ size_bytes: bundle.size_bytes
138
+ };
139
+ const bundlesPath = `/v1/projects/${projectId}/source-bundles`;
140
+ const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
141
+ format: descriptor.format,
142
+ size_bytes: descriptor.size_bytes
143
+ });
144
+ if (prepared.status === "upload") {
145
+ if (!prepared.upload_url) throw new Error("Tonbo did not return a source upload URL.");
146
+ const uploaded = await this.fetcher(prepared.upload_url, {
147
+ method: "PUT",
148
+ headers: {
149
+ "content-type": prepared.content_type ?? "application/vnd.tonbo.source+tar",
150
+ "x-upsert": "false"
151
+ },
152
+ body: new Blob([new Uint8Array(bundle.bytes)])
153
+ });
154
+ let uploadError = null;
155
+ if (!uploaded.ok) {
156
+ uploadError = new Error(`Source upload failed with HTTP ${uploaded.status}.`);
157
+ }
158
+ try {
159
+ await this.management("POST", `${bundlesPath}/${bundle.sha256}/complete`, token, {
160
+ format: bundle.format,
161
+ size_bytes: bundle.size_bytes
162
+ });
163
+ } catch (error) {
164
+ if (uploadError)
165
+ throw new AggregateError([uploadError, error], "Source bundle upload did not complete.");
166
+ throw error;
167
+ }
168
+ }
169
+ let deployment = await this.createDeployment(projectId, { spec, origin }, token);
170
+ const previous = await this.getProduction(projectId, token);
171
+ const previousDeployment = previous ? await this.getDeployment(projectId, previous.deployment_id, token) : null;
172
+ const unchanged = previousDeployment?.spec_sha256 === deployment.spec_sha256;
173
+ let production = previous;
174
+ if (promote) {
175
+ production = await this.putProduction(
176
+ projectId,
177
+ {
178
+ deployment_id: deployment.id,
179
+ desired_state: "running",
180
+ expected_generation: previous ? previous.generation : null
181
+ },
182
+ token
183
+ );
184
+ deployment = await this.getDeployment(projectId, deployment.id, token);
185
+ }
186
+ return { deployment, production, unchanged };
187
+ }
188
+ createDeployment(projectId, body, token) {
189
+ return this.management(
190
+ "POST",
191
+ `/v1/projects/${projectId}/deployments`,
192
+ token,
193
+ body
194
+ ).then((response) => response.data);
195
+ }
196
+ listDeployments(projectId, token) {
197
+ return this.managementList(`/v1/projects/${projectId}/deployments`, token);
198
+ }
199
+ getDeployment(projectId, deploymentId, token) {
200
+ return this.management(
201
+ "GET",
202
+ `/v1/projects/${projectId}/deployments/${deploymentId}`,
203
+ token
204
+ ).then((response) => response.data);
205
+ }
206
+ getProduction(projectId, token) {
207
+ return this.management(
208
+ "GET",
209
+ `/v1/projects/${projectId}/production`,
210
+ token
211
+ ).then(
212
+ (response) => response.data,
213
+ (error) => {
214
+ if (error.status === 404) return null;
215
+ throw error;
216
+ }
217
+ );
218
+ }
219
+ putProduction(projectId, body, token) {
220
+ return this.management(
221
+ "PUT",
222
+ `/v1/projects/${projectId}/production`,
223
+ token,
224
+ body
225
+ ).then((response) => response.data);
226
+ }
227
+ listRollouts(projectId, token) {
228
+ return this.managementList(
229
+ `/v1/projects/${projectId}/production/rollouts`,
230
+ token
231
+ );
232
+ }
233
+ async run({
234
+ projectId,
235
+ prompt,
236
+ sessionId,
237
+ token,
238
+ turnId = randomUUID()
239
+ }) {
240
+ const projectPath = `/v1/projects/${projectId}`;
241
+ const production = await this.getProduction(projectId, token);
242
+ if (!production)
243
+ throw new Error("Project has no Production Deployment; run `tonbo deploy` first.");
244
+ if (production.observed_state !== "running")
245
+ throw new Error(
246
+ `Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
247
+ );
248
+ const session = sessionId ? { id: sessionId } : (await this.management(
249
+ "POST",
250
+ `${projectPath}/sessions`,
251
+ token,
252
+ { deployment_id: production.deployment_id }
253
+ )).data;
254
+ const turn = (await this.management(
255
+ "POST",
256
+ `${projectPath}/sessions/${session.id}/turns`,
257
+ token,
258
+ { prompt },
259
+ turnId
260
+ )).data;
261
+ return { production, session, turn };
262
+ }
263
+ turnEvents({
264
+ after = 0,
265
+ projectId,
266
+ sessionId,
267
+ token,
268
+ turnId
269
+ }) {
270
+ return this.management(
271
+ "GET",
272
+ `/v1/projects/${projectId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
273
+ token
274
+ );
275
+ }
276
+ listProjectSecrets(projectId, token) {
277
+ return this.management(
278
+ "GET",
279
+ `/v1/projects/${projectId}/secrets`,
280
+ token
281
+ ).then((body) => body.data);
282
+ }
283
+ setProjectSecret(projectId, name, value, token) {
284
+ return this.management(
285
+ "PUT",
286
+ `/v1/projects/${projectId}/secrets/${encodeURIComponent(name)}`,
287
+ token,
288
+ { value }
289
+ );
290
+ }
291
+ deleteProjectSecret(projectId, name, token) {
292
+ return this.management(
293
+ "DELETE",
294
+ `/v1/projects/${projectId}/secrets/${encodeURIComponent(name)}`,
295
+ token
296
+ );
297
+ }
298
+ management(method, path6, token, body, idempotencyKey) {
299
+ return requestJson(this.fetcher, `${this.managementOrigin}${path6}`, {
300
+ method,
301
+ headers: {
302
+ authorization: `Bearer ${token}`,
303
+ ...body === void 0 ? {} : {
304
+ "content-type": "application/json",
305
+ "idempotency-key": idempotencyKey ?? randomUUID()
306
+ }
307
+ },
308
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
309
+ });
310
+ }
311
+ async managementList(path6, token) {
312
+ const values = [];
313
+ let cursor = null;
314
+ do {
315
+ const separator = path6.includes("?") ? "&" : "?";
316
+ const page = await this.management(
317
+ "GET",
318
+ `${path6}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
319
+ token
320
+ );
321
+ values.push(...page.data);
322
+ cursor = page.next_cursor;
323
+ } while (cursor);
324
+ return values;
325
+ }
326
+ };
327
+
328
+ // src/agent-source.ts
329
+ import { lstat, readFile } from "node:fs/promises";
330
+ import path from "node:path";
331
+
332
+ // ../../packages/agent-source-inspector/src/index.ts
333
+ var AGENTS_INSTRUCTIONS_PATH = "AGENTS.md";
334
+ var PI_SETTINGS_PATH = ".pi/settings.json";
335
+ var MAX_HARNESS_CONFIG_BYTES = 256 * 1024;
336
+ var supportedHarnesses = [
337
+ {
338
+ command: "pi",
339
+ id: "pi",
340
+ name: "PI"
341
+ }
342
+ ];
343
+ var supportedExecutionTargets = [
344
+ { driver: "native", harness: "pi" },
345
+ { driver: "command", harness: "pi" }
346
+ ];
347
+ var AgentSourceInspectionError = class extends Error {
348
+ constructor(code, path6, message, options) {
349
+ super(message, options);
350
+ this.code = code;
351
+ this.path = path6;
352
+ this.name = "AgentSourceInspectionError";
353
+ }
354
+ };
355
+ function parsePiPackageCount(contents) {
356
+ let settings;
357
+ try {
358
+ settings = JSON.parse(contents);
359
+ } catch (error) {
360
+ throw new AgentSourceInspectionError(
361
+ "invalid_pi_settings",
362
+ PI_SETTINGS_PATH,
363
+ `${PI_SETTINGS_PATH} must contain valid JSON.`,
364
+ { cause: error }
365
+ );
366
+ }
367
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
368
+ throw new AgentSourceInspectionError(
369
+ "invalid_pi_settings",
370
+ PI_SETTINGS_PATH,
371
+ `${PI_SETTINGS_PATH} must contain a JSON object.`
372
+ );
373
+ }
374
+ if (!("packages" in settings)) return 0;
375
+ const packages = settings.packages;
376
+ if (!Array.isArray(packages)) {
377
+ throw new AgentSourceInspectionError(
378
+ "invalid_pi_settings",
379
+ PI_SETTINGS_PATH,
380
+ `${PI_SETTINGS_PATH} packages must be an array.`
381
+ );
382
+ }
383
+ return packages.length;
384
+ }
385
+ async function inspectAgentSource(source) {
386
+ const [agentsMd, piSettings] = await Promise.all([
387
+ source.has(AGENTS_INSTRUCTIONS_PATH),
388
+ source.readText(PI_SETTINGS_PATH, MAX_HARNESS_CONFIG_BYTES)
389
+ ]);
390
+ if (piSettings !== null) {
391
+ return {
392
+ version: 1,
393
+ instructions: { agentsMd },
394
+ harness: {
395
+ id: "pi",
396
+ reason: "pi_settings_found",
397
+ state: "identified"
398
+ },
399
+ pi: {
400
+ packageCount: parsePiPackageCount(piSettings),
401
+ settingsFound: true
402
+ }
403
+ };
404
+ }
405
+ return {
406
+ version: 1,
407
+ instructions: { agentsMd },
408
+ harness: {
409
+ candidates: supportedHarnesses.map((harness) => harness.id),
410
+ reason: "no_harness_specific_config",
411
+ state: "selection_required"
412
+ },
413
+ pi: {
414
+ packageCount: 0,
415
+ settingsFound: false
416
+ }
417
+ };
418
+ }
419
+
420
+ // src/agent-source.ts
421
+ var LocalAgentSource = class {
422
+ constructor(root) {
423
+ this.root = root;
424
+ }
425
+ async has(filename) {
426
+ const metadata = await this.metadata(filename);
427
+ return metadata?.isFile() === true;
428
+ }
429
+ async readText(filename, maxBytes) {
430
+ const metadata = await this.metadata(filename);
431
+ if (!metadata) return null;
432
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
433
+ throw new Error(`${filename} must be a regular file.`);
434
+ }
435
+ if (metadata.size > maxBytes) {
436
+ throw new Error(`${filename} exceeds the ${maxBytes}-byte inspection limit.`);
437
+ }
438
+ return readFile(path.join(this.root, filename), "utf8");
439
+ }
440
+ async metadata(filename) {
441
+ try {
442
+ return await lstat(path.join(this.root, filename));
443
+ } catch (error) {
444
+ if (error.code === "ENOENT") return null;
445
+ throw error;
446
+ }
447
+ }
448
+ };
449
+ function inspectLocalAgentSource(root) {
450
+ return inspectAgentSource(new LocalAgentSource(root));
451
+ }
452
+
453
+ // src/auth.ts
454
+ import { createHash, randomBytes } from "node:crypto";
455
+ import { createServer } from "node:http";
456
+ import { execFile } from "node:child_process";
457
+ import { promisify } from "node:util";
458
+
459
+ // src/callback-page.ts
460
+ var WORDMARK = [
461
+ "\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",
462
+ "\u2588\u2588\u2584 \u2584\u2588\u2588\u2580\u2584 \u2588\u2588 \u2584\u2580\u2588\u2588 \u2584 \u2588\u2588 \u2584\u2580\u2588",
463
+ "\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2584\u2588\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2588"
464
+ ].join("\n");
465
+ var OUTCOMES = {
466
+ complete: {
467
+ status: 200,
468
+ title: "You are signed in.",
469
+ body: "Return to your terminal. This tab can be closed."
470
+ },
471
+ denied: {
472
+ status: 400,
473
+ title: "Login was not completed.",
474
+ body: "Nothing was authorized. Return to your terminal to try again."
475
+ },
476
+ invalid: {
477
+ // The state did not match, the code was missing, or the path was wrong.
478
+ // Say that it could not be verified rather than which check failed: the
479
+ // person who can read this page is not necessarily the one who started
480
+ // the login.
481
+ status: 400,
482
+ title: "This callback could not be verified.",
483
+ body: "Return to your terminal and start the login again."
484
+ }
485
+ };
486
+ function renderCallbackPage(outcome) {
487
+ const { title, body } = OUTCOMES[outcome];
488
+ return `<!doctype html>
489
+ <html lang="en">
490
+ <head>
491
+ <meta charset="utf-8">
492
+ <meta name="viewport" content="width=device-width, initial-scale=1">
493
+ <meta name="robots" content="noindex">
494
+ <title>Tonbo CLI</title>
495
+ <style>
496
+ :root {
497
+ --paper: #f3f2ee;
498
+ --ink: #1d1b17;
499
+ --rule: #c9c5ba;
500
+ --quiet: #5c564a;
501
+ --signal: #d64a03;
502
+ }
503
+ @media (prefers-color-scheme: dark) {
504
+ :root {
505
+ --paper: #1d1b17;
506
+ --ink: #f3f2ee;
507
+ --rule: #3a362e;
508
+ --quiet: #a49e8f;
509
+ --signal: #f08b4b;
510
+ }
511
+ }
512
+ * { box-sizing: border-box; }
513
+ body {
514
+ margin: 0;
515
+ min-height: 100vh;
516
+ display: grid;
517
+ grid-template-rows: auto 1fr;
518
+ padding: 0 clamp(20px, 5vw, 72px);
519
+ background: var(--paper);
520
+ color: var(--ink);
521
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
522
+ -webkit-font-smoothing: antialiased;
523
+ }
524
+ header { padding: 28px 0; }
525
+ /* Same metrics as .auth-wordmark in the app: the glyphs are the ground
526
+ and the letters are the gaps, so the 8px/10px/scaleY(0.8) combination is
527
+ what makes the rows meet without seams. Iosevka is not available here,
528
+ so it falls back to the platform monospace. */
529
+ pre.wordmark {
530
+ margin: 0;
531
+ font-family: 'Iosevka', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
532
+ font-size: 8px;
533
+ font-weight: 400;
534
+ line-height: 10px;
535
+ letter-spacing: 0;
536
+ white-space: pre;
537
+ color: var(--signal);
538
+ transform: scaleY(0.8);
539
+ transform-origin: left top;
540
+ }
541
+ main {
542
+ display: flex;
543
+ flex-direction: column;
544
+ justify-content: center;
545
+ max-width: 46ch;
546
+ padding-bottom: 12vh;
547
+ }
548
+ h1 {
549
+ margin: 0;
550
+ font-size: 26px;
551
+ font-weight: 300;
552
+ line-height: 1.16;
553
+ letter-spacing: -0.018em;
554
+ }
555
+ p {
556
+ margin: 14px 0 0;
557
+ font-size: 16px;
558
+ line-height: 1.55;
559
+ color: var(--quiet);
560
+ }
561
+ hr {
562
+ width: 40px;
563
+ margin: 28px 0 0;
564
+ border: 0;
565
+ border-top: 1px solid var(--rule);
566
+ }
567
+ </style>
568
+ </head>
569
+ <body>
570
+ <header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
571
+ <main>
572
+ <h1>${title}</h1>
573
+ <p>${body}</p>
574
+ <hr>
575
+ </main>
576
+ </body>
577
+ </html>
578
+ `;
579
+ }
580
+ function callbackResponse(outcome) {
581
+ return {
582
+ status: OUTCOMES[outcome].status,
583
+ headers: {
584
+ "content-type": "text/html; charset=utf-8",
585
+ "cache-control": "no-store"
586
+ },
587
+ body: renderCallbackPage(outcome)
588
+ };
589
+ }
590
+
591
+ // src/auth.ts
592
+ var exec = promisify(execFile);
593
+ var LOOPBACK_REDIRECT = "http://localhost:17655/callback";
594
+ function base64url(value) {
595
+ return value.toString("base64url");
596
+ }
597
+ function withAbsoluteExpiry(tokens) {
598
+ if (tokens.expires_at || !Number.isInteger(tokens.expires_in) || (tokens.expires_in ?? 0) <= 0)
599
+ return tokens;
600
+ return {
601
+ ...tokens,
602
+ expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 0)
603
+ };
604
+ }
605
+ var AuthClient = class {
606
+ constructor(credentials, fetcher, accountOrigin = "https://tonbo.dev", browserOpener = openBrowser) {
607
+ this.credentials = credentials;
608
+ this.fetcher = fetcher;
609
+ this.accountOrigin = accountOrigin;
610
+ this.browserOpener = browserOpener;
611
+ }
612
+ async accessToken() {
613
+ const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
614
+ if (injected) return injected;
615
+ const tokens = await this.credentials.load();
616
+ if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
617
+ const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
618
+ if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + 30))
619
+ return tokens.access_token;
620
+ if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
621
+ const config = await this.config();
622
+ const refreshed = withAbsoluteExpiry(
623
+ await this.exchange(
624
+ config.token_endpoint,
625
+ new URLSearchParams({
626
+ client_id: config.client_id,
627
+ grant_type: "refresh_token",
628
+ refresh_token: tokens.refresh_token
629
+ })
630
+ )
631
+ );
632
+ if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
633
+ await this.credentials.save(refreshed);
634
+ return refreshed.access_token;
635
+ }
636
+ async login(progress = () => {
637
+ }) {
638
+ progress({ status: "started", step: "account-config" });
639
+ const config = await this.config();
640
+ progress({ status: "completed", step: "account-config" });
641
+ if (config.redirect_uri !== LOOPBACK_REDIRECT)
642
+ throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
643
+ const verifier = base64url(randomBytes(48));
644
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
645
+ const state = base64url(randomBytes(24));
646
+ const authorization = new URL(config.authorization_endpoint);
647
+ authorization.search = new URLSearchParams({
648
+ client_id: config.client_id,
649
+ code_challenge: challenge,
650
+ code_challenge_method: "S256",
651
+ redirect_uri: config.redirect_uri,
652
+ response_type: "code",
653
+ scope: "openid profile email",
654
+ state
655
+ }).toString();
656
+ const code = await this.listenForCode(
657
+ state,
658
+ () => this.browserOpener(authorization.toString()),
659
+ progress
660
+ );
661
+ progress({ status: "started", step: "token-exchange" });
662
+ const tokens = withAbsoluteExpiry(
663
+ await this.exchange(
664
+ config.token_endpoint,
665
+ new URLSearchParams({
666
+ client_id: config.client_id,
667
+ code,
668
+ code_verifier: verifier,
669
+ grant_type: "authorization_code",
670
+ redirect_uri: config.redirect_uri
671
+ })
672
+ )
673
+ );
674
+ progress({ status: "completed", step: "token-exchange" });
675
+ progress({ status: "started", step: "credential-store" });
676
+ await this.credentials.save(tokens);
677
+ progress({ status: "completed", step: "credential-store" });
678
+ return tokens;
679
+ }
680
+ config() {
681
+ return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/config`);
682
+ }
683
+ exchange(endpoint, body) {
684
+ return requestJson(this.fetcher, endpoint, {
685
+ method: "POST",
686
+ headers: { "content-type": "application/x-www-form-urlencoded" },
687
+ body
688
+ });
689
+ }
690
+ listenForCode(expectedState, ready, progress) {
691
+ return new Promise((resolve, reject) => {
692
+ let settled = false;
693
+ const sockets = /* @__PURE__ */ new Set();
694
+ let authorizationStarted = false;
695
+ const startAuthorization = () => {
696
+ if (authorizationStarted) return;
697
+ authorizationStarted = true;
698
+ progress({ status: "completed", step: "browser" });
699
+ progress({ status: "started", step: "browser-authorization" });
700
+ };
701
+ const finish = (result, completedResponseSocket) => {
702
+ if (settled) return;
703
+ settled = true;
704
+ clearTimeout(timeout);
705
+ const complete = (closeError) => {
706
+ if (closeError) reject(closeError);
707
+ else if ("code" in result) {
708
+ progress({ status: "completed", step: "callback-close" });
709
+ resolve(result.code);
710
+ } else reject(result.error);
711
+ };
712
+ if ("code" in result) progress({ status: "started", step: "callback-close" });
713
+ if (!server.listening) {
714
+ complete();
715
+ return;
716
+ }
717
+ server.close(complete);
718
+ for (const socket of sockets) {
719
+ if (socket !== completedResponseSocket) socket.destroy();
720
+ }
721
+ server.closeIdleConnections();
722
+ };
723
+ const timeout = setTimeout(
724
+ () => {
725
+ finish({ error: new Error("Timed out waiting for browser login.") });
726
+ },
727
+ 5 * 60 * 1e3
728
+ );
729
+ const server = createServer((request, response) => {
730
+ const url = new URL(request.url ?? "/", LOOPBACK_REDIRECT);
731
+ const code = url.searchParams.get("code");
732
+ const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
733
+ if (oauthError) {
734
+ const responseSocket2 = response.socket;
735
+ respond(
736
+ response,
737
+ "denied",
738
+ () => finish({ error: new Error(oauthError) }, responseSocket2)
739
+ );
740
+ return;
741
+ }
742
+ if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState || !code) {
743
+ respond(response, "invalid");
744
+ return;
745
+ }
746
+ startAuthorization();
747
+ progress({ status: "completed", step: "browser-authorization" });
748
+ const responseSocket = response.socket;
749
+ respond(response, "complete", () => finish({ code }, responseSocket));
750
+ });
751
+ server.on("connection", (socket) => {
752
+ sockets.add(socket);
753
+ socket.once("close", () => sockets.delete(socket));
754
+ });
755
+ server.once("error", (error) => finish({ error }));
756
+ progress({ status: "started", step: "callback-server" });
757
+ server.listen(17655, "localhost", () => {
758
+ progress({ status: "completed", step: "callback-server" });
759
+ progress({ status: "started", step: "browser" });
760
+ void ready().then(startAuthorization).catch(
761
+ (error) => finish({
762
+ error: error instanceof Error ? error : new Error("Could not open the login browser.")
763
+ })
764
+ );
765
+ });
766
+ });
767
+ }
768
+ };
769
+ function respond(response, outcome, finished) {
770
+ const page = callbackResponse(outcome);
771
+ if (finished) response.once("finish", finished);
772
+ response.writeHead(page.status, { ...page.headers, connection: "close" });
773
+ response.end(page.body);
774
+ }
775
+ async function openBrowser(url) {
776
+ if (process.platform === "darwin") return void await exec("open", [url]);
777
+ if (process.platform === "win32")
778
+ return void await exec("rundll32", ["url.dll,FileProtocolHandler", url]);
779
+ return void await exec("xdg-open", [url]);
780
+ }
781
+
782
+ // src/commands.ts
783
+ import path4 from "node:path";
784
+
785
+ // src/build.ts
786
+ import { spawn } from "node:child_process";
787
+ async function runBuildCommand(root, command) {
788
+ if (command.length === 0) throw new Error("Build command must not be empty.");
789
+ await new Promise((resolve, reject) => {
790
+ const child = spawn(command[0], command.slice(1), {
791
+ cwd: root,
792
+ env: process.env,
793
+ // Command output is progress, not the CLI result. Keep stdout available
794
+ // for the single JSON document emitted by `tonbo --json deploy`.
795
+ stdio: ["inherit", process.stderr, process.stderr]
796
+ });
797
+ child.once("error", reject);
798
+ child.once("exit", (code, signal) => {
799
+ if (code === 0) resolve();
800
+ else
801
+ reject(
802
+ new Error(
803
+ `Build command failed${signal ? ` with signal ${signal}` : ` with status ${code}`}.`
804
+ )
805
+ );
806
+ });
807
+ });
808
+ }
809
+
810
+ // src/contracts.ts
811
+ import { Ajv2020 } from "ajv/dist/2020.js";
812
+
813
+ // src/generated/contracts.ts
814
+ var piAgentSchema = {
815
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
816
+ "$id": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json",
817
+ "title": "PI Agent v1",
818
+ "type": "object",
819
+ "additionalProperties": false,
820
+ "required": [
821
+ "runtime",
822
+ "driver"
823
+ ],
824
+ "properties": {
825
+ "runtime": {
826
+ "const": "pi"
827
+ },
828
+ "secrets": {
829
+ "type": "array",
830
+ "maxItems": 32,
831
+ "uniqueItems": true,
832
+ "items": {
833
+ "type": "string",
834
+ "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
835
+ }
836
+ },
837
+ "driver": {
838
+ "oneOf": [
839
+ {
840
+ "type": "object",
841
+ "additionalProperties": false,
842
+ "required": [
843
+ "kind"
844
+ ],
845
+ "properties": {
846
+ "kind": {
847
+ "const": "native"
848
+ }
849
+ }
850
+ },
851
+ {
852
+ "type": "object",
853
+ "additionalProperties": false,
854
+ "required": [
855
+ "kind",
856
+ "protocol",
857
+ "command"
858
+ ],
859
+ "properties": {
860
+ "kind": {
861
+ "const": "command"
862
+ },
863
+ "protocol": {
864
+ "const": "pi-rpc-v1"
865
+ },
866
+ "command": {
867
+ "type": "array",
868
+ "minItems": 1,
869
+ "maxItems": 64,
870
+ "items": {
871
+ "type": "string",
872
+ "minLength": 1,
873
+ "maxLength": 1024
874
+ }
875
+ }
876
+ }
877
+ }
878
+ ]
879
+ }
880
+ }
881
+ };
882
+ var projectServiceSchema = {
883
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
884
+ "$id": "https://contracts.tonbo.dev/agents/project-service-v1.schema.json",
885
+ "title": "Tonbo Project application service v1",
886
+ "type": "object",
887
+ "additionalProperties": false,
888
+ "required": [
889
+ "command"
890
+ ],
891
+ "properties": {
892
+ "command": {
893
+ "type": "array",
894
+ "minItems": 1,
895
+ "maxItems": 64,
896
+ "items": {
897
+ "type": "string",
898
+ "minLength": 1,
899
+ "maxLength": 4096
900
+ }
901
+ },
902
+ "secrets": {
903
+ "type": "array",
904
+ "maxItems": 32,
905
+ "uniqueItems": true,
906
+ "items": {
907
+ "type": "string",
908
+ "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
909
+ }
910
+ },
911
+ "kubernetes": {
912
+ "$ref": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json#/$defs/request"
913
+ }
914
+ }
915
+ };
916
+ var kubernetesProfilesSchema = {
917
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
918
+ "$id": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json",
919
+ "title": "Tonbo managed Kubernetes profiles v1",
920
+ "$defs": {
921
+ "request": {
922
+ "type": "object",
923
+ "additionalProperties": false,
924
+ "required": [
925
+ "profile"
926
+ ],
927
+ "properties": {
928
+ "profile": false
929
+ }
930
+ }
931
+ },
932
+ "x-tonbo-profiles": {}
933
+ };
934
+ var declarationSchema = {
935
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
936
+ "$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v2.schema.json",
937
+ "title": "Tonbo Agent declaration v2",
938
+ "type": "object",
939
+ "additionalProperties": false,
940
+ "required": [
941
+ "version",
942
+ "harness"
943
+ ],
944
+ "properties": {
945
+ "version": {
946
+ "const": 2
947
+ },
948
+ "inference": {
949
+ "type": "object",
950
+ "additionalProperties": false,
951
+ "default": {
952
+ "model": "claude-sonnet-4-5"
953
+ },
954
+ "required": [
955
+ "model"
956
+ ],
957
+ "properties": {
958
+ "model": {
959
+ "type": "string",
960
+ "minLength": 1,
961
+ "maxLength": 160
962
+ }
963
+ }
964
+ },
965
+ "build": {
966
+ "type": "object",
967
+ "additionalProperties": false,
968
+ "required": [
969
+ "command"
970
+ ],
971
+ "properties": {
972
+ "command": {
973
+ "type": "array",
974
+ "minItems": 1,
975
+ "maxItems": 64,
976
+ "items": {
977
+ "type": "string",
978
+ "minLength": 1,
979
+ "maxLength": 1024
980
+ }
981
+ }
982
+ }
983
+ },
984
+ "service": {
985
+ "$ref": "https://contracts.tonbo.dev/agents/project-service-v1.schema.json"
986
+ },
987
+ "agent": {
988
+ "$ref": "https://contracts.tonbo.dev/agents/project-name-v1.json#/$defs/publicHostname"
989
+ },
990
+ "harness": {
991
+ "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
992
+ }
993
+ }
994
+ };
995
+ var deploymentSchema = {
996
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
997
+ "$id": "https://contracts.tonbo.dev/agents/managed-deployment-v1.schema.json",
998
+ "title": "Managed Project Deployment v1",
999
+ "type": "object",
1000
+ "additionalProperties": false,
1001
+ "required": [
1002
+ "version",
1003
+ "agent",
1004
+ "source",
1005
+ "inference"
1006
+ ],
1007
+ "properties": {
1008
+ "version": {
1009
+ "const": 1
1010
+ },
1011
+ "agent": {
1012
+ "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
1013
+ },
1014
+ "source": {
1015
+ "type": "object",
1016
+ "additionalProperties": false,
1017
+ "required": [
1018
+ "format",
1019
+ "sha256",
1020
+ "size_bytes"
1021
+ ],
1022
+ "properties": {
1023
+ "format": {
1024
+ "const": "tar-v1"
1025
+ },
1026
+ "sha256": {
1027
+ "type": "string",
1028
+ "pattern": "^[0-9a-f]{64}$"
1029
+ },
1030
+ "size_bytes": {
1031
+ "type": "integer",
1032
+ "minimum": 1,
1033
+ "maximum": 67108864
1034
+ }
1035
+ }
1036
+ },
1037
+ "inference": {
1038
+ "type": "object",
1039
+ "additionalProperties": false,
1040
+ "required": [
1041
+ "model"
1042
+ ],
1043
+ "properties": {
1044
+ "model": {
1045
+ "type": "string",
1046
+ "minLength": 1,
1047
+ "maxLength": 160
1048
+ }
1049
+ }
1050
+ },
1051
+ "service": {
1052
+ "$ref": "https://contracts.tonbo.dev/agents/project-service-v1.schema.json"
1053
+ }
1054
+ }
1055
+ };
1056
+ var sourceBundleContract = {
1057
+ "version": 1,
1058
+ "format": "tar-v1",
1059
+ "bucket": "agent-source-bundles",
1060
+ "content_type": "application/vnd.tonbo.source+tar",
1061
+ "max_bytes": 67108864
1062
+ };
1063
+ var piSessionContract = {
1064
+ "version": 1,
1065
+ "adapter": "pi-jsonl-v3",
1066
+ "format_version": 3,
1067
+ "durable_completion_timeout_seconds": 30,
1068
+ "session_directory": "/sessions",
1069
+ "path_template": "/sessions/{session_id}.jsonl",
1070
+ "preflight_command": [
1071
+ "/usr/local/bin/artifacts",
1072
+ "runtime",
1073
+ "session-preflight"
1074
+ ]
1075
+ };
1076
+ var projectNameContract = {
1077
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
1078
+ "$id": "https://contracts.tonbo.dev/agents/project-name-v1.json",
1079
+ "x-tonbo-version": 1,
1080
+ "x-tonbo-public-hostname-apex": "tonbo.sh",
1081
+ "x-tonbo-public-hostname-template": "<project>-<organization>.tonbo.sh",
1082
+ "$defs": {
1083
+ "name": {
1084
+ "type": "string",
1085
+ "minLength": 3,
1086
+ "maxLength": 40,
1087
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
1088
+ "not": {
1089
+ "enum": [
1090
+ "api",
1091
+ "artifacts",
1092
+ "auth",
1093
+ "inference",
1094
+ "network-health",
1095
+ "sandbox-control",
1096
+ "status",
1097
+ "streams",
1098
+ "www"
1099
+ ]
1100
+ }
1101
+ },
1102
+ "publicHostname": {
1103
+ "type": "string",
1104
+ "minLength": 16,
1105
+ "maxLength": 72,
1106
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)+\\.tonbo\\.sh$",
1107
+ "not": {
1108
+ "enum": [
1109
+ "api.tonbo.sh",
1110
+ "artifacts.tonbo.sh",
1111
+ "auth.tonbo.sh",
1112
+ "inference.tonbo.sh",
1113
+ "network-health.tonbo.sh",
1114
+ "sandbox-control.tonbo.sh",
1115
+ "status.tonbo.sh",
1116
+ "streams.tonbo.sh",
1117
+ "www.tonbo.sh"
1118
+ ]
1119
+ }
1120
+ }
1121
+ }
1122
+ };
1123
+
1124
+ // src/contracts.ts
1125
+ var ajv = new Ajv2020({ allErrors: true, useDefaults: true });
1126
+ ajv.addKeyword({ keyword: "x-tonbo-profiles" });
1127
+ ajv.addKeyword({ keyword: "x-tonbo-version" });
1128
+ ajv.addKeyword({ keyword: "x-tonbo-public-hostname-apex" });
1129
+ ajv.addKeyword({ keyword: "x-tonbo-public-hostname-template" });
1130
+ ajv.addSchema(kubernetesProfilesSchema);
1131
+ ajv.addSchema(piAgentSchema);
1132
+ ajv.addSchema(projectNameContract);
1133
+ ajv.addSchema(projectServiceSchema);
1134
+ var validateDeclaration = ajv.compile(declarationSchema);
1135
+ var validateDeploymentSpec = ajv.compile(deploymentSchema);
1136
+ function validationMessage(label, errors) {
1137
+ const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
1138
+ return `${label} is invalid${detail ? `: ${detail}` : "."}`;
1139
+ }
1140
+ function parseDeclaration(value) {
1141
+ const candidate = structuredClone(value);
1142
+ if (!validateDeclaration(candidate)) {
1143
+ throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
1144
+ }
1145
+ return candidate;
1146
+ }
1147
+ function assertManagedDeploymentSpec(value) {
1148
+ if (!validateDeploymentSpec(value)) {
1149
+ throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
1150
+ }
1151
+ }
1152
+
1153
+ // src/declaration.ts
1154
+ import { randomUUID as randomUUID2 } from "node:crypto";
1155
+ import { lstat as lstat2, open, readFile as readFile2, rename, rm } from "node:fs/promises";
1156
+ import path2 from "node:path";
1157
+ import { parse, stringify } from "smol-toml";
1158
+ var DECLARATION_FILENAME = ".tonbo";
1159
+ var DEFAULT_INFERENCE_MODEL = "claude-sonnet-4-5";
1160
+ function createDeclaration(model = DEFAULT_INFERENCE_MODEL, driver = { kind: "native" }, buildCommand, projectHostname) {
1161
+ return parseDeclaration({
1162
+ version: 2,
1163
+ ...projectHostname ? { agent: projectHostname } : {},
1164
+ harness: { runtime: "pi", driver },
1165
+ inference: { model: model.trim() },
1166
+ ...buildCommand ? { build: { command: buildCommand } } : {}
1167
+ });
1168
+ }
1169
+ function renderDeclaration(declaration) {
1170
+ return `# Tonbo Agent configuration.
1171
+ # Edit this file directly or run \`tonbo init\` to reconfigure.
1172
+ ${stringify(declaration)}`;
1173
+ }
1174
+ async function bindDeclarationProject(root, projectHostname) {
1175
+ const declaration = await loadDeclaration(root);
1176
+ const bound = parseDeclaration({ ...declaration, agent: projectHostname });
1177
+ await saveDeclaration(root, bound, true);
1178
+ return bound;
1179
+ }
1180
+ async function declarationExists(root) {
1181
+ const filename = path2.join(root, DECLARATION_FILENAME);
1182
+ try {
1183
+ const metadata = await lstat2(filename);
1184
+ if (metadata.isSymbolicLink() || !metadata.isFile())
1185
+ throw new Error(`${filename} must be a regular file.`);
1186
+ return true;
1187
+ } catch (error) {
1188
+ if (error.code === "ENOENT") return false;
1189
+ throw error;
1190
+ }
1191
+ }
1192
+ async function saveDeclaration(root, declaration, overwrite) {
1193
+ const filename = path2.join(root, DECLARATION_FILENAME);
1194
+ const exists = await declarationExists(root);
1195
+ if (exists && !overwrite)
1196
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1197
+ const contents = renderDeclaration(declaration);
1198
+ if (!overwrite) {
1199
+ const handle2 = await open(filename, "wx", 420).catch((error) => {
1200
+ if (error.code === "EEXIST")
1201
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1202
+ throw error;
1203
+ });
1204
+ try {
1205
+ await handle2.writeFile(contents, "utf8");
1206
+ await handle2.sync();
1207
+ } finally {
1208
+ await handle2.close();
1209
+ }
1210
+ return;
1211
+ }
1212
+ const temporary = path2.join(root, `.${DECLARATION_FILENAME}.${process.pid}.${randomUUID2()}.tmp`);
1213
+ let handle;
1214
+ try {
1215
+ handle = await open(temporary, "wx", 420);
1216
+ await handle.writeFile(contents, "utf8");
1217
+ await handle.sync();
1218
+ await handle.close();
1219
+ handle = void 0;
1220
+ await rename(temporary, filename);
1221
+ } catch (error) {
1222
+ await handle?.close().catch(() => void 0);
1223
+ await rm(temporary, { force: true }).catch(() => void 0);
1224
+ throw error;
1225
+ }
1226
+ }
1227
+ async function loadDeclaration(declarationRoot) {
1228
+ const filename = path2.join(declarationRoot, DECLARATION_FILENAME);
1229
+ let parsed;
1230
+ try {
1231
+ parsed = parse(await readFile2(filename, "utf8"));
1232
+ } catch (error) {
1233
+ if (error.code === "ENOENT")
1234
+ throw new Error(`No ${DECLARATION_FILENAME} declaration found at ${filename}.`);
1235
+ throw new Error(`Could not read ${filename} as TOML.`, { cause: error });
1236
+ }
1237
+ return parseDeclaration(parsed);
1238
+ }
1239
+ function buildDeploymentSpec(declaration, source) {
1240
+ const spec = {
1241
+ version: 1,
1242
+ agent: declaration.harness,
1243
+ source: {
1244
+ format: source.format,
1245
+ sha256: source.sha256,
1246
+ size_bytes: source.size_bytes
1247
+ },
1248
+ inference: declaration.inference,
1249
+ ...declaration.service ? { service: declaration.service } : {}
1250
+ };
1251
+ assertManagedDeploymentSpec(spec);
1252
+ return spec;
1253
+ }
1254
+
1255
+ // src/source.ts
1256
+ import { createHash as createHash2 } from "node:crypto";
1257
+ import { lstat as lstat3, readFile as readFile3, readdir } from "node:fs/promises";
1258
+ import path3 from "node:path";
1259
+ import ignore from "ignore";
1260
+ import tar from "tar-stream";
1261
+ var MAX_BUNDLE_BYTES = sourceBundleContract.max_bytes;
1262
+ var SESSION_SOURCE_DIRECTORY = piSessionContract.session_directory.replace(/^\/+|\/+$/g, "");
1263
+ var DEFAULT_IGNORES = [
1264
+ ".git/",
1265
+ "node_modules/",
1266
+ ".DS_Store",
1267
+ ".env",
1268
+ ".env.*",
1269
+ "!.env.example",
1270
+ ".tonbo-cache/",
1271
+ ".tonbo-system/",
1272
+ ".pi/npm/",
1273
+ ".pi/git/"
1274
+ ];
1275
+ var EXACT_NPM_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1276
+ var GIT_COMMIT = /^[0-9a-f]{40}$/i;
1277
+ function packageSource(value, index) {
1278
+ if (typeof value === "string") return value;
1279
+ if (value && typeof value === "object" && "source" in value && typeof value.source === "string")
1280
+ return value.source;
1281
+ throw new Error(`.pi/settings.json packages[${index}] must be a source string or object.`);
1282
+ }
1283
+ async function validatePackageSource(root, source) {
1284
+ if (source.startsWith("npm:")) {
1285
+ const specifier = source.slice(4);
1286
+ const separator = specifier.lastIndexOf("@");
1287
+ if (separator <= 0 || !EXACT_NPM_VERSION.test(specifier.slice(separator + 1))) {
1288
+ throw new Error(
1289
+ `PI package ${source} must pin an exact npm version, for example npm:my-agent@1.2.3.`
1290
+ );
1291
+ }
1292
+ return;
1293
+ }
1294
+ if (source.startsWith("git:")) {
1295
+ const separator = source.lastIndexOf("@");
1296
+ if (separator <= "git:".length || !GIT_COMMIT.test(source.slice(separator + 1))) {
1297
+ throw new Error(`PI package ${source} must pin a full 40-character Git commit.`);
1298
+ }
1299
+ return;
1300
+ }
1301
+ if (source.startsWith("./") || source.startsWith("../")) {
1302
+ const settingsDirectory = path3.join(root, ".pi");
1303
+ const resolved = path3.resolve(settingsDirectory, source);
1304
+ const relative = path3.relative(root, resolved);
1305
+ if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
1306
+ throw new Error(`Local PI package ${source} resolves outside the deployed project.`);
1307
+ }
1308
+ let metadata;
1309
+ try {
1310
+ metadata = await lstat3(resolved);
1311
+ } catch (error) {
1312
+ if (error.code === "ENOENT") {
1313
+ throw new Error(`Local PI package ${source} does not exist.`);
1314
+ }
1315
+ throw error;
1316
+ }
1317
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
1318
+ throw new Error(`Local PI package ${source} must resolve to a real project directory.`);
1319
+ }
1320
+ return;
1321
+ }
1322
+ throw new Error(
1323
+ `PI package ${source} must use an exact npm version, a full Git commit, or a project-local path.`
1324
+ );
1325
+ }
1326
+ async function validatePiPackages(root) {
1327
+ const filename = path3.join(root, ".pi", "settings.json");
1328
+ let settings;
1329
+ try {
1330
+ settings = JSON.parse(await readFile3(filename, "utf8"));
1331
+ } catch (error) {
1332
+ if (error.code === "ENOENT") return;
1333
+ throw new Error(`Could not read ${filename} as JSON.`, { cause: error });
1334
+ }
1335
+ if (!settings || typeof settings !== "object" || !("packages" in settings)) return;
1336
+ const packages = settings.packages;
1337
+ if (!Array.isArray(packages)) throw new Error(`${filename} packages must be an array.`);
1338
+ await Promise.all(
1339
+ packages.map((value, index) => validatePackageSource(root, packageSource(value, index)))
1340
+ );
1341
+ }
1342
+ async function findDeclarationRoot(start) {
1343
+ let candidate = path3.resolve(start);
1344
+ for (; ; ) {
1345
+ try {
1346
+ if ((await lstat3(path3.join(candidate, ".tonbo"))).isFile()) return candidate;
1347
+ } catch (error) {
1348
+ if (error.code !== "ENOENT") throw error;
1349
+ }
1350
+ const parent = path3.dirname(candidate);
1351
+ if (parent === candidate) {
1352
+ throw new Error(`No .tonbo declaration found above ${path3.resolve(start)}.`);
1353
+ }
1354
+ candidate = parent;
1355
+ }
1356
+ }
1357
+ async function sourceIgnore(root) {
1358
+ const matcher = ignore().add(DEFAULT_IGNORES);
1359
+ try {
1360
+ matcher.add(await readFile3(path3.join(root, ".tonboignore"), "utf8"));
1361
+ } catch (error) {
1362
+ if (error.code !== "ENOENT") throw error;
1363
+ }
1364
+ return matcher;
1365
+ }
1366
+ async function collectFiles(root) {
1367
+ const matcher = await sourceIgnore(root);
1368
+ const files = [];
1369
+ async function visit(directory, relativeDirectory) {
1370
+ const entries = await readdir(directory, { withFileTypes: true });
1371
+ entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
1372
+ for (const entry of entries) {
1373
+ const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
1374
+ const ignored = matcher.ignores(relative + (entry.isDirectory() ? "/" : ""));
1375
+ if (ignored) continue;
1376
+ if (relative === SESSION_SOURCE_DIRECTORY || relative.startsWith(`${SESSION_SOURCE_DIRECTORY}/`)) {
1377
+ throw new Error(
1378
+ `Source path ${SESSION_SOURCE_DIRECTORY}/ is reserved for durable PI history. Rename it or exclude it with .tonboignore.`
1379
+ );
1380
+ }
1381
+ const absolute = path3.join(directory, entry.name);
1382
+ if (entry.isDirectory()) {
1383
+ await visit(absolute, relative);
1384
+ continue;
1385
+ }
1386
+ const metadata = await lstat3(absolute);
1387
+ if (!metadata.isFile()) {
1388
+ throw new Error(
1389
+ `Source path ${relative} is not a regular file. V1 does not follow symlinks or special files.`
1390
+ );
1391
+ }
1392
+ files.push({
1393
+ absolute,
1394
+ mode: metadata.mode & 73 ? 493 : 420,
1395
+ relative,
1396
+ size: metadata.size
1397
+ });
1398
+ }
1399
+ }
1400
+ await visit(root, "");
1401
+ if (!files.some((file) => file.relative === ".tonbo")) {
1402
+ throw new Error("The source bundle must contain .tonbo.");
1403
+ }
1404
+ return files;
1405
+ }
1406
+ function addEntry(pack, file, contents) {
1407
+ return new Promise((resolve, reject) => {
1408
+ pack.entry(
1409
+ {
1410
+ gid: 0,
1411
+ mode: file.mode,
1412
+ mtime: /* @__PURE__ */ new Date(0),
1413
+ name: file.relative,
1414
+ size: contents.length,
1415
+ type: "file",
1416
+ uid: 0
1417
+ },
1418
+ contents,
1419
+ (error) => error ? reject(error) : resolve()
1420
+ );
1421
+ });
1422
+ }
1423
+ async function buildSourceBundle(root) {
1424
+ const resolvedRoot = path3.resolve(root);
1425
+ await validatePiPackages(resolvedRoot);
1426
+ const files = await collectFiles(resolvedRoot);
1427
+ const payloadBytes = files.reduce((total, file) => total + file.size, 0);
1428
+ if (payloadBytes > MAX_BUNDLE_BYTES) {
1429
+ throw new Error(`Source files exceed ${MAX_BUNDLE_BYTES} bytes after ignore rules.`);
1430
+ }
1431
+ const pack = tar.pack();
1432
+ const chunks = [];
1433
+ let size = 0;
1434
+ pack.on("data", (chunk) => {
1435
+ size += chunk.length;
1436
+ if (size > MAX_BUNDLE_BYTES) {
1437
+ pack.destroy(
1438
+ new Error(`Source bundle exceeds ${MAX_BUNDLE_BYTES} bytes after ignore rules.`)
1439
+ );
1440
+ return;
1441
+ }
1442
+ chunks.push(chunk);
1443
+ });
1444
+ const completed = new Promise((resolve, reject) => {
1445
+ pack.on("end", resolve);
1446
+ pack.on("error", reject);
1447
+ });
1448
+ for (const file of files) {
1449
+ await addEntry(pack, file, await readFile3(file.absolute));
1450
+ }
1451
+ pack.finalize();
1452
+ await completed;
1453
+ const bytes = Buffer.concat(chunks);
1454
+ return {
1455
+ bytes,
1456
+ format: sourceBundleContract.format,
1457
+ root: resolvedRoot,
1458
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
1459
+ size_bytes: bytes.length
1460
+ };
1461
+ }
1462
+
1463
+ // src/ssh-key.ts
1464
+ import { createHash as createHash3 } from "node:crypto";
1465
+ import { readFile as readFile4 } from "node:fs/promises";
1466
+ import { homedir } from "node:os";
1467
+ import { basename, join } from "node:path";
1468
+ var DEFAULT_PUBLIC_KEYS = ["id_ed25519.pub", "id_ecdsa.pub", "id_rsa.pub"];
1469
+ var ALGORITHMS = /* @__PURE__ */ new Set(["ssh-ed25519", "ecdsa-sha2-nistp256", "ssh-rsa"]);
1470
+ function parseOpenSshPublicKey(value, label) {
1471
+ const fields = value.trim().split(/\s+/);
1472
+ if (fields.length < 2 || !ALGORITHMS.has(fields[0]))
1473
+ throw new Error("SSH public key must be Ed25519, ECDSA P-256, or RSA.");
1474
+ const blob = Buffer.from(fields[1], "base64");
1475
+ if (blob.length < 32 || blob.length > 16384 || blob.toString("base64") !== fields[1])
1476
+ throw new Error("SSH public key is not canonical base64.");
1477
+ return {
1478
+ algorithm: fields[0],
1479
+ fingerprint: `SHA256:${createHash3("sha256").update(blob).digest("base64").replace(/=$/, "")}`,
1480
+ keyBase64: fields[1],
1481
+ label
1482
+ };
1483
+ }
1484
+ async function readSshPublicKey(path6) {
1485
+ return parseOpenSshPublicKey(await readFile4(path6, "utf8"), basename(path6, ".pub"));
1486
+ }
1487
+ async function readDefaultSshPublicKeys(sshDirectory = join(homedir(), ".ssh")) {
1488
+ const keys = [];
1489
+ for (const name of DEFAULT_PUBLIC_KEYS) {
1490
+ const key = await readSshPublicKey(join(sshDirectory, name)).catch(
1491
+ (error) => {
1492
+ if (error.code === "ENOENT") return null;
1493
+ throw error;
1494
+ }
1495
+ );
1496
+ if (key) keys.push(key);
1497
+ }
1498
+ return keys;
1499
+ }
1500
+
1501
+ // src/project-name.ts
1502
+ var PROJECT_NAME_RULE = projectNameContract.$defs.name;
1503
+ var PROJECT_NAME_PATTERN = new RegExp(PROJECT_NAME_RULE.pattern);
1504
+ var RESERVED_PROJECT_NAMES = new Set(PROJECT_NAME_RULE.not.enum);
1505
+ function validateProjectName(name) {
1506
+ const valid = name.length >= PROJECT_NAME_RULE.minLength && name.length <= PROJECT_NAME_RULE.maxLength && PROJECT_NAME_PATTERN.test(name) && !RESERVED_PROJECT_NAMES.has(name);
1507
+ if (!valid) {
1508
+ throw new Error(
1509
+ `Project name must be ${PROJECT_NAME_RULE.minLength}-${PROJECT_NAME_RULE.maxLength} lowercase letters, digits, or single hyphens, start with a letter, and not be reserved.`
1510
+ );
1511
+ }
1512
+ }
1513
+ function projectNameSuggestion(value) {
1514
+ let name = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1515
+ if (!/^[a-z]/.test(name)) name = `agent-${name}`;
1516
+ name = name.slice(0, PROJECT_NAME_RULE.maxLength).replace(/-+$/g, "");
1517
+ if (name.length < PROJECT_NAME_RULE.minLength) {
1518
+ name = `${name || "agent"}-agent`.slice(0, PROJECT_NAME_RULE.maxLength);
1519
+ }
1520
+ if (RESERVED_PROJECT_NAMES.has(name)) name = `${name}-agent`;
1521
+ return name;
1522
+ }
1523
+
1524
+ // src/commands.ts
1525
+ async function resolveProject(deps) {
1526
+ const root = await findDeclarationRoot(deps.cwd());
1527
+ const declaration = await loadDeclaration(root);
1528
+ if (!declaration.agent) {
1529
+ throw new Error("No Project is configured in .tonbo. Run `tonbo project use <project>` first.");
1530
+ }
1531
+ const oauthToken = await deps.auth.accessToken();
1532
+ return {
1533
+ declaration,
1534
+ oauthToken,
1535
+ project: selectProject(await deps.api.listProjects(oauthToken), declaration.agent),
1536
+ root
1537
+ };
1538
+ }
1539
+ function selectProject(projects, selector) {
1540
+ const normalized = selector.toLowerCase();
1541
+ const matches = projects.filter(
1542
+ (project) => project.id === selector || project.name === normalized || project.publicHostname === normalized
1543
+ );
1544
+ if (matches.length === 0) throw new Error(`Project ${selector} was not found in your account.`);
1545
+ if (matches.length > 1)
1546
+ throw new Error(`Project name ${selector} is ambiguous; use its full hostname or ID.`);
1547
+ if (matches[0].status !== "active") throw new Error(`Project ${selector} is not active.`);
1548
+ return matches[0];
1549
+ }
1550
+ async function initCommand(deps, options) {
1551
+ const root = deps.cwd();
1552
+ const exists = await declarationExists(root);
1553
+ let existingDeclaration;
1554
+ if (exists) {
1555
+ existingDeclaration = await loadDeclaration(root).catch(() => void 0);
1556
+ }
1557
+ const interactive = deps.interactive();
1558
+ const overwrite = options.force === true || exists && interactive;
1559
+ if (exists && !overwrite)
1560
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1561
+ const inspection = await deps.inspectSource(root);
1562
+ let harness = options.harness ?? (existingDeclaration ? "pi" : void 0);
1563
+ if (harness !== void 0 && harness !== "pi") {
1564
+ throw new Error("--harness must name a supported Harness: pi.");
1565
+ }
1566
+ let driver = options.driver ?? existingDeclaration?.harness.driver.kind;
1567
+ if (driver !== void 0 && driver !== "native" && driver !== "command") {
1568
+ throw new Error("--driver must be native or command.");
1569
+ }
1570
+ if (!harness) {
1571
+ if (inspection.harness.state === "identified") harness = inspection.harness.id;
1572
+ else if (!interactive) {
1573
+ throw new Error("No Harness-specific configuration found. Pass --harness pi.");
1574
+ } else {
1575
+ deps.output({
1576
+ message: "No Harness-specific configuration was found in this project."
1577
+ });
1578
+ harness = await selectHarness(deps);
1579
+ }
1580
+ }
1581
+ if (!driver) {
1582
+ if (!interactive) {
1583
+ throw new Error("PI execution mode is required. Pass --driver native or --driver command.");
1584
+ }
1585
+ if (inspection.pi.settingsFound) {
1586
+ const packageSummary = inspection.pi.packageCount === 0 ? "Found PI configuration at .pi/settings.json." : `Found PI configuration at .pi/settings.json with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}.`;
1587
+ deps.output({ message: packageSummary });
1588
+ }
1589
+ driver = "native";
1590
+ }
1591
+ if (driver === "native" && (options.agentEntry || options.buildCommand)) {
1592
+ throw new Error("--agent-entry and --build-command require --driver command.");
1593
+ }
1594
+ const existingEntry = existingDeclaration?.harness.driver.kind === "command" && existingDeclaration.harness.driver.command[0] === "node" ? existingDeclaration.harness.driver.command[1] : void 0;
1595
+ let entry = options.agentEntry?.trim() || existingEntry;
1596
+ if (driver === "command" && !entry && interactive) {
1597
+ entry = (await deps.prompt("PI SDK entry file [dist/agent.mjs]: ")).trim();
1598
+ }
1599
+ entry ||= "dist/agent.mjs";
1600
+ let model = options.model?.trim() || existingDeclaration?.inference.model || DEFAULT_INFERENCE_MODEL;
1601
+ let buildCommand = driver === "command" ? options.buildCommand ?? existingDeclaration?.build?.command ?? ["npm", "run", "build"] : void 0;
1602
+ let projectPlan = existingDeclaration?.agent ? {
1603
+ kind: "existing",
1604
+ publicHostname: existingDeclaration.agent
1605
+ } : { kind: "later" };
1606
+ if (interactive && !existingDeclaration?.agent) {
1607
+ projectPlan = await selectInitProject(deps, root);
1608
+ }
1609
+ if (interactive) {
1610
+ while (true) {
1611
+ deps.output({
1612
+ message: renderInitSummary(projectPlan, harness, driver, model, entry, buildCommand)
1613
+ });
1614
+ const saveLabel = initSaveLabel(exists, projectPlan);
1615
+ const action = await deps.select(`${saveLabel} with this configuration?`, [
1616
+ {
1617
+ name: saveLabel,
1618
+ value: "save"
1619
+ },
1620
+ {
1621
+ description: "Change the project, Harness, execution mode, model, or SDK entrypoint.",
1622
+ name: "Edit configuration",
1623
+ value: "edit"
1624
+ },
1625
+ { name: "Cancel", value: "cancel" }
1626
+ ]);
1627
+ if (action === "cancel") {
1628
+ deps.output({
1629
+ message: exists ? `Cancelled. Kept existing ${DECLARATION_FILENAME}.` : `Cancelled. Did not create ${DECLARATION_FILENAME}.`
1630
+ });
1631
+ return;
1632
+ }
1633
+ if (action === "save") break;
1634
+ const setting = await deps.select("What would you like to change?", [
1635
+ {
1636
+ description: initProjectSummary(projectPlan),
1637
+ name: "Project",
1638
+ value: "project"
1639
+ },
1640
+ { description: harnessName(harness), name: "Harness", value: "harness" },
1641
+ {
1642
+ description: driver === "native" ? "PI CLI (`pi`)" : "PI SDK app",
1643
+ name: "Execution mode",
1644
+ value: "driver"
1645
+ },
1646
+ { description: model, name: "Inference model", value: "model" },
1647
+ ...driver === "command" ? [{ description: entry, name: "PI SDK entry file", value: "entry" }] : [],
1648
+ { name: "Back to review", value: "back" }
1649
+ ]);
1650
+ if (setting === "project") projectPlan = await selectInitProject(deps, root);
1651
+ if (setting === "harness") harness = await selectHarness(deps);
1652
+ if (setting === "driver") {
1653
+ driver = await deps.select(
1654
+ "How should Tonbo start PI?",
1655
+ piTargetChoices(inspection.pi.settingsFound)
1656
+ );
1657
+ if (driver === "command") {
1658
+ const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
1659
+ if (answer) entry = answer;
1660
+ buildCommand = options.buildCommand ?? ["npm", "run", "build"];
1661
+ } else {
1662
+ buildCommand = void 0;
1663
+ }
1664
+ }
1665
+ if (setting === "model") {
1666
+ const answer = (await deps.prompt(`Inference model [${model}]: `)).trim();
1667
+ if (answer) model = answer;
1668
+ }
1669
+ if (setting === "entry") {
1670
+ const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
1671
+ if (answer) entry = answer;
1672
+ }
1673
+ }
1674
+ }
1675
+ let projectHostname = projectPlan.kind === "existing" ? projectPlan.publicHostname : void 0;
1676
+ let createdProject;
1677
+ if (projectPlan.kind === "create") {
1678
+ const oauthToken = await deps.auth.accessToken();
1679
+ createdProject = await deps.api.createProject(
1680
+ oauthToken,
1681
+ projectPlan.name,
1682
+ projectPlan.organizationId
1683
+ );
1684
+ projectHostname = createdProject.publicHostname;
1685
+ }
1686
+ const configuredDeclaration = createDeclaration(
1687
+ model,
1688
+ driver === "native" ? { kind: "native" } : { kind: "command", protocol: "pi-rpc-v1", command: ["node", entry] },
1689
+ buildCommand,
1690
+ projectHostname
1691
+ );
1692
+ const declaration = parseDeclaration({
1693
+ ...configuredDeclaration,
1694
+ harness: {
1695
+ ...configuredDeclaration.harness,
1696
+ ...existingDeclaration?.harness.secrets ? { secrets: existingDeclaration.harness.secrets } : {}
1697
+ },
1698
+ ...existingDeclaration?.service ? { service: existingDeclaration.service } : {}
1699
+ });
1700
+ try {
1701
+ await saveDeclaration(root, declaration, overwrite);
1702
+ } catch (error) {
1703
+ if (createdProject) {
1704
+ throw new Error(
1705
+ `Created project ${createdProject.name}, but could not write ${DECLARATION_FILENAME}. Fix the file and run \`tonbo project use ${createdProject.publicHostname}\`.`,
1706
+ { cause: error }
1707
+ );
1708
+ }
1709
+ throw error;
1710
+ }
1711
+ const driverSummary = driver === "command" ? `PI SDK app
1712
+ Build: ${buildCommand?.join(" ")}
1713
+ Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
1714
+ const projectSummary = createdProject ? `Project: ${createdProject.name} (${createdProject.publicHostname})
1715
+ Organization: ${createdProject.organizationName}
1716
+ Application: https://${createdProject.publicHostname}
1717
+ SSH after deploy: ssh ${createdProject.sshDestination}` : projectPlan.kind === "existing" ? `Project: ${projectPlan.name ? `${projectPlan.name} (${projectPlan.publicHostname})` : projectPlan.publicHostname}${projectPlan.organizationName ? `
1718
+ Organization: ${projectPlan.organizationName}` : ""}${projectPlan.sshDestination ? `
1719
+ SSH after deploy: ssh ${projectPlan.sshDestination}` : ""}` : "Next: tonbo project create <name>";
1720
+ deps.output({
1721
+ message: `${createdProject ? `Created project ${createdProject.name} and` : exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
1722
+ ${projectSummary}
1723
+ Harness: PI
1724
+ Mode: ${driverSummary}${projectPlan.kind === "later" ? "" : "\nNext: tonbo deploy"}`,
1725
+ declaration,
1726
+ path: path4.join(root, DECLARATION_FILENAME)
1727
+ });
1728
+ }
1729
+ function initSaveLabel(exists, project) {
1730
+ if (project.kind === "create") {
1731
+ return exists ? `Create project and replace ${DECLARATION_FILENAME}` : `Create project and ${DECLARATION_FILENAME}`;
1732
+ }
1733
+ return exists ? `Replace ${DECLARATION_FILENAME}` : `Create ${DECLARATION_FILENAME}`;
1734
+ }
1735
+ async function selectInitProject(deps, root) {
1736
+ for (; ; ) {
1737
+ const action = await deps.select("How should this Agent connect to Tonbo?", [
1738
+ {
1739
+ description: "Create it only after you confirm the complete configuration.",
1740
+ name: "Create a new project",
1741
+ value: "create"
1742
+ },
1743
+ {
1744
+ description: "Bind this source tree to one project you can access.",
1745
+ name: "Use an existing project",
1746
+ value: "existing"
1747
+ },
1748
+ {
1749
+ description: "Write an unbound declaration and configure its project later.",
1750
+ name: "Set up later",
1751
+ value: "later"
1752
+ }
1753
+ ]);
1754
+ if (action === "later") return { kind: "later" };
1755
+ if (action === "create") {
1756
+ const organization = await selectInitOrganization(deps);
1757
+ const defaultName = projectNameSuggestion(path4.basename(root)) || "tonbo-agent";
1758
+ for (; ; ) {
1759
+ const answer = (await deps.prompt(`Project name [${defaultName}]: `)).trim().toLowerCase();
1760
+ const name = answer || defaultName;
1761
+ try {
1762
+ validateProjectName(name);
1763
+ return {
1764
+ kind: "create",
1765
+ name,
1766
+ organizationId: organization?.id,
1767
+ organizationName: organization?.name
1768
+ };
1769
+ } catch (error) {
1770
+ deps.output({
1771
+ message: error instanceof Error ? error.message : "Project name is invalid."
1772
+ });
1773
+ }
1774
+ }
1775
+ }
1776
+ const oauthToken = await deps.auth.accessToken();
1777
+ const projects = (await deps.api.listProjects(oauthToken)).filter(
1778
+ (project2) => project2.status === "active"
1779
+ );
1780
+ if (projects.length === 0) {
1781
+ deps.output({ message: "No active projects are available. Create a new project instead." });
1782
+ continue;
1783
+ }
1784
+ const id = await deps.select(
1785
+ "Which project should this Agent use?",
1786
+ projects.map((project2) => ({
1787
+ name: project2.name,
1788
+ description: `Organization: ${project2.organizationName} \xB7 ${project2.publicHostname} \xB7 ${project2.id}`,
1789
+ value: project2.id
1790
+ }))
1791
+ );
1792
+ const project = selectProject(projects, id);
1793
+ return {
1794
+ kind: "existing",
1795
+ name: project.name,
1796
+ organizationName: project.organizationName,
1797
+ publicHostname: project.publicHostname,
1798
+ sshDestination: project.sshDestination
1799
+ };
1800
+ }
1801
+ }
1802
+ function initProjectSummary(project) {
1803
+ if (project.kind === "create")
1804
+ return `Create ${project.name}${project.organizationName ? ` in ${project.organizationName}` : ""} (permanent address assigned after creation)`;
1805
+ if (project.kind === "existing") {
1806
+ return project.name ? `${project.name}${project.organizationName ? ` in ${project.organizationName}` : ""} (${project.publicHostname})` : `Keep ${project.publicHostname}`;
1807
+ }
1808
+ return "Set up later";
1809
+ }
1810
+ async function selectInitOrganization(deps) {
1811
+ const oauthToken = await deps.auth.accessToken();
1812
+ const organizations = (await deps.api.listOrganizations(oauthToken)).filter(
1813
+ (organization) => organization.role !== "member"
1814
+ );
1815
+ if (organizations.length === 0)
1816
+ throw new Error("No organization lets you create a project. Ask an admin or owner.");
1817
+ if (organizations.length === 1) return organizations[0];
1818
+ const id = await deps.select(
1819
+ "Which organization should own the new project?",
1820
+ organizations.map((organization) => ({
1821
+ description: organization.role,
1822
+ name: organization.name,
1823
+ value: organization.id
1824
+ }))
1825
+ );
1826
+ return organizations.find((organization) => organization.id === id) ?? organizations[0];
1827
+ }
1828
+ async function selectHarness(deps) {
1829
+ return await deps.select(
1830
+ "Which supported Harness should Tonbo use?",
1831
+ supportedHarnesses.map((supported) => ({
1832
+ description: `Run this project with the ${supported.command} command.`,
1833
+ name: `${supported.name} (\`${supported.command}\`)`,
1834
+ value: supported.id
1835
+ }))
1836
+ );
1837
+ }
1838
+ function harnessName(harness) {
1839
+ const supported = supportedHarnesses.find((candidate) => candidate.id === harness);
1840
+ return supported?.name ?? harness;
1841
+ }
1842
+ function renderInitSummary(project, harness, driver, model, entry, buildCommand) {
1843
+ const lines = [
1844
+ "",
1845
+ "Tonbo Agent configuration:",
1846
+ ` Project: ${initProjectSummary(project)}`,
1847
+ ` Harness: ${harnessName(harness)}`,
1848
+ ` Execution: ${driver === "native" ? "PI CLI (`pi`)" : "PI SDK app"}`,
1849
+ ` Model: ${model}`
1850
+ ];
1851
+ if (driver === "command") {
1852
+ lines.push(` Build: ${buildCommand?.join(" ") ?? "none"}`);
1853
+ lines.push(` Entrypoint: node ${entry}`);
1854
+ }
1855
+ return lines.join("\n");
1856
+ }
1857
+ function piTargetChoices(settingsFound) {
1858
+ const choices = {
1859
+ command: {
1860
+ description: "Run a custom Node.js entrypoint built on the PI SDK.",
1861
+ name: "PI SDK app",
1862
+ value: "command"
1863
+ },
1864
+ native: {
1865
+ description: settingsFound ? "Run pi directly and load the detected project-local PI configuration." : "Run pi directly with AGENTS.md and optional project-local .pi resources.",
1866
+ name: "PI CLI (`pi`)",
1867
+ value: "native"
1868
+ }
1869
+ };
1870
+ return supportedExecutionTargets.filter((target) => target.harness === "pi").map((target) => choices[target.driver]);
1871
+ }
1872
+ async function loginCommand(deps) {
1873
+ try {
1874
+ const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
1875
+ deps.progress.start("Preparing SSH access");
1876
+ const keys = await deps.defaultSshPublicKeys();
1877
+ await Promise.all(keys.map((key) => deps.api.registerSshKey(tokens.access_token, key)));
1878
+ deps.progress.succeed("SSH access ready");
1879
+ deps.output({ message: "Logged in to Tonbo." });
1880
+ } catch (error) {
1881
+ deps.progress.fail();
1882
+ throw error;
1883
+ }
1884
+ }
1885
+ var LOGIN_PROGRESS_MESSAGES = {
1886
+ "account-config": {
1887
+ started: "Connecting to Tonbo",
1888
+ completed: "Connected to Tonbo"
1889
+ },
1890
+ "callback-server": {
1891
+ started: "Starting local browser callback",
1892
+ completed: "Local browser callback ready"
1893
+ },
1894
+ browser: {
1895
+ started: "Opening browser",
1896
+ completed: "Browser opened"
1897
+ },
1898
+ "browser-authorization": {
1899
+ started: "Waiting for browser authorization",
1900
+ completed: "Browser authorization received"
1901
+ },
1902
+ "callback-close": {
1903
+ started: "Closing local browser callback",
1904
+ completed: "Local browser callback closed"
1905
+ },
1906
+ "token-exchange": {
1907
+ started: "Exchanging authorization code",
1908
+ completed: "Authorization code exchanged"
1909
+ },
1910
+ "credential-store": {
1911
+ started: "Saving login session",
1912
+ completed: "Login session saved"
1913
+ }
1914
+ };
1915
+ function reportLoginProgress(progress, event) {
1916
+ const messages = LOGIN_PROGRESS_MESSAGES[event.step];
1917
+ if (event.status === "started") progress.start(messages.started);
1918
+ else progress.succeed(messages.completed);
1919
+ }
1920
+ async function sshKeyAddCommand(deps, path6) {
1921
+ const key = await readSshPublicKey(path6);
1922
+ const oauthToken = await deps.auth.accessToken();
1923
+ await deps.api.registerSshKey(oauthToken, key);
1924
+ deps.output({ message: `Registered SSH key ${key.fingerprint}.`, key });
1925
+ }
1926
+ async function sshKeyRemoveCommand(deps, fingerprint) {
1927
+ const oauthToken = await deps.auth.accessToken();
1928
+ const key = await deps.api.revokeSshKey(oauthToken, fingerprint);
1929
+ deps.output({ message: `Revoked SSH key ${key.fingerprint}.`, key });
1930
+ }
1931
+ async function projectUseCommand(deps, selector, force = false) {
1932
+ const root = await findDeclarationRoot(deps.cwd());
1933
+ const declaration = await loadDeclaration(root);
1934
+ const oauthToken = await deps.auth.accessToken();
1935
+ const projects = await deps.api.listProjects(oauthToken);
1936
+ const project = selectProject(projects, selector);
1937
+ if (declaration.agent === project.publicHostname) {
1938
+ deps.output({ message: `.tonbo already uses project ${project.publicHostname}.`, project });
1939
+ return;
1940
+ }
1941
+ if (declaration.agent && !force) {
1942
+ const current = projects.find((candidate) => candidate.publicHostname === declaration.agent);
1943
+ const currentLabel = current?.publicHostname ?? declaration.agent;
1944
+ if (!deps.interactive()) {
1945
+ throw new Error(
1946
+ `.tonbo already uses project ${currentLabel}. Pass --force to rebind it to ${project.publicHostname}.`
1947
+ );
1948
+ }
1949
+ const action = await deps.select(
1950
+ `Rebind .tonbo from ${currentLabel} to ${project.publicHostname}?`,
1951
+ [
1952
+ { name: `Rebind to ${project.publicHostname}`, value: "rebind" },
1953
+ { name: "Cancel", value: "cancel" }
1954
+ ]
1955
+ );
1956
+ if (action !== "rebind") {
1957
+ deps.output({ message: `Cancelled. .tonbo still uses project ${currentLabel}.` });
1958
+ return;
1959
+ }
1960
+ }
1961
+ await bindDeclarationProject(root, project.publicHostname);
1962
+ deps.output({ message: `Bound .tonbo to project ${project.publicHostname}.`, project });
1963
+ }
1964
+ async function projectCreateCommand(deps, name) {
1965
+ const root = await findDeclarationRoot(deps.cwd());
1966
+ const declaration = await loadDeclaration(root);
1967
+ if (declaration.agent) {
1968
+ throw new Error(
1969
+ `.tonbo is already bound to project ${declaration.agent}. Run \`tonbo project use <project>\` to review a rebind.`
1970
+ );
1971
+ }
1972
+ const oauthToken = await deps.auth.accessToken();
1973
+ validateProjectName(name);
1974
+ const project = await deps.api.createProject(oauthToken, name);
1975
+ try {
1976
+ await bindDeclarationProject(root, project.publicHostname);
1977
+ } catch (error) {
1978
+ throw new Error(
1979
+ `Created project ${project.name}, but could not bind ${DECLARATION_FILENAME}. Fix the file and run \`tonbo project use ${project.publicHostname}\`.`,
1980
+ { cause: error }
1981
+ );
1982
+ }
1983
+ deps.output({ message: `Created project ${project.name} and bound it in .tonbo.`, project });
1984
+ }
1985
+ async function deployCommand(deps, options = { promote: true }) {
1986
+ const { declaration, oauthToken, project, root } = await resolveProject(deps);
1987
+ if (declaration.build) await runBuildCommand(root, declaration.build.command);
1988
+ const source = await buildSourceBundle(root);
1989
+ const origin = { actor: "cli", git: await deps.gitProvenance(root) };
1990
+ const managementToken = await deps.api.exchangeManagementToken(oauthToken, project.id);
1991
+ const result = await deps.api.deploy({
1992
+ bundle: source,
1993
+ origin,
1994
+ projectId: project.id,
1995
+ promote: options.promote,
1996
+ spec: buildDeploymentSpec(declaration, source),
1997
+ token: managementToken
1998
+ });
1999
+ const { deployment } = result;
2000
+ const shortId = shortDeploymentId(deployment.id);
2001
+ const lines = [
2002
+ options.promote ? `Deployed project ${project.name}.` : `Created Deployment ${deploymentName(deployment)} (${shortId}) for project ${project.name} without promoting it.`,
2003
+ `Organization: ${project.organizationName}`,
2004
+ `Deployment: ${deploymentName(deployment)} (${shortId})`,
2005
+ `Source: ${deploymentSourceLabel(deployment)}`,
2006
+ `Production: ${options.promote ? productionStateLabel(deployment) : `not promoted; run \`tonbo deployments promote ${shortId}\` to move Production here`}`,
2007
+ `Application: https://${project.publicHostname}`
2008
+ ];
2009
+ if (result.unchanged) {
2010
+ lines.push(
2011
+ `Contents are identical to the ${options.promote ? "previous" : "current"} Production Deployment; the running Agent does not change.`
2012
+ );
2013
+ }
2014
+ if (options.promote) {
2015
+ lines.push(
2016
+ "",
2017
+ "Agent process: starts with the first turn and stays warm while the runtime is active.",
2018
+ "",
2019
+ "Start the Agent:",
2020
+ ' tonbo run "<prompt>"',
2021
+ "",
2022
+ "Connect with SSH:",
2023
+ ` ssh ${project.sshDestination}`
2024
+ );
2025
+ }
2026
+ deps.output({ message: lines.join("\n"), project, ...result });
2027
+ }
2028
+ async function runCommand(deps, prompt, options) {
2029
+ const { oauthToken, project } = await resolveProject(deps);
2030
+ const managementToken = await deps.api.exchangeManagementToken(oauthToken, project.id);
2031
+ const result = await deps.api.run({
2032
+ projectId: project.id,
2033
+ prompt,
2034
+ sessionId: options.session,
2035
+ token: managementToken
2036
+ });
2037
+ deps.output({
2038
+ message: result.turn.assistant_text,
2039
+ project,
2040
+ ...result
2041
+ });
2042
+ }
2043
+ async function projectShowCommand(deps) {
2044
+ const { project, token } = await projectManagement(deps);
2045
+ const production = await deps.api.getProduction(project.id, token);
2046
+ const deployment = production ? await deps.api.getDeployment(project.id, production.deployment_id, token) : null;
2047
+ const lines = [
2048
+ `project: ${project.name}`,
2049
+ `Organization: ${project.organizationName}`,
2050
+ `application: https://${project.publicHostname}`,
2051
+ `ssh: ssh ${project.sshDestination}`
2052
+ ];
2053
+ if (deployment) {
2054
+ lines.push(
2055
+ `production: ${deploymentName(deployment)} (${shortDeploymentId(deployment.id)}) ${productionStateLabel(deployment)}`
2056
+ );
2057
+ }
2058
+ deps.output({ message: lines.join("\n"), project, production, deployment });
2059
+ }
2060
+ function shortDeploymentId(id) {
2061
+ return id.replace(/-/g, "").slice(0, 8);
2062
+ }
2063
+ function deploymentName(deployment) {
2064
+ const subject = deployment.origin.git?.subject?.trim();
2065
+ return subject ? subject : shortDeploymentId(deployment.id);
2066
+ }
2067
+ function deploymentSourceLabel(deployment) {
2068
+ const git = deployment.origin.git;
2069
+ if (!git) return "tonbo deploy";
2070
+ const sha = git.commit_sha.slice(0, 7);
2071
+ return git.ref ? `${sha} \xB7 ${git.ref}` : sha;
2072
+ }
2073
+ function productionStateLabel(deployment) {
2074
+ const { generation, state } = deployment.production;
2075
+ return generation === null ? state : `${state} (generation ${generation})`;
2076
+ }
2077
+ function selectDeployment(deployments, prefix) {
2078
+ const normalized = prefix.trim().toLowerCase().replace(/-/g, "");
2079
+ if (!/^[0-9a-f]+$/.test(normalized)) {
2080
+ throw new Error(`Deployment id ${prefix} must be a hexadecimal id prefix.`);
2081
+ }
2082
+ const matches = deployments.filter(
2083
+ (deployment) => deployment.id.replace(/-/g, "").startsWith(normalized)
2084
+ );
2085
+ if (matches.length === 0) throw new Error(`Deployment ${prefix} was not found in this project.`);
2086
+ if (matches.length > 1) {
2087
+ throw new Error(
2088
+ `Deployment id ${prefix} is ambiguous; it matches ${matches.map((deployment) => shortDeploymentId(deployment.id)).join(", ")}. Use more characters.`
2089
+ );
2090
+ }
2091
+ return matches[0];
2092
+ }
2093
+ function renderTable(headers, rows) {
2094
+ const widths = headers.map(
2095
+ (header, column) => Math.max(header.length, ...rows.map((row) => row[column].length))
2096
+ );
2097
+ const render = (row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd();
2098
+ return [render(headers), ...rows.map(render)].join("\n");
2099
+ }
2100
+ function formatTimestamp(value) {
2101
+ const date = new Date(value);
2102
+ if (Number.isNaN(date.getTime())) return value;
2103
+ return `${date.toISOString().slice(0, 16)}Z`;
2104
+ }
2105
+ function productionMarker(state) {
2106
+ if (state === "current") return "\u25CF";
2107
+ if (state === "previous") return "\u25CB";
2108
+ return "";
2109
+ }
2110
+ async function deploymentsListCommand(deps) {
2111
+ const { project, token } = await projectManagement(deps);
2112
+ const [deployments, production] = await Promise.all([
2113
+ deps.api.listDeployments(project.id, token),
2114
+ deps.api.getProduction(project.id, token)
2115
+ ]);
2116
+ const message = deployments.length ? renderTable(
2117
+ ["ID", "NAME", "STATUS", "PRODUCTION", "SOURCE", "CREATED", "BY"],
2118
+ deployments.map((deployment) => [
2119
+ shortDeploymentId(deployment.id),
2120
+ deploymentName(deployment),
2121
+ deployment.production.state,
2122
+ productionMarker(deployment.production.state),
2123
+ deploymentSourceLabel(deployment),
2124
+ formatTimestamp(deployment.created_at),
2125
+ deployment.created_by_user_id ? shortDeploymentId(deployment.created_by_user_id) : ""
2126
+ ])
2127
+ ) : `No Deployments exist for project ${project.name}. Run \`tonbo deploy\` to create one.`;
2128
+ deps.output({ message, project, production, deployments });
2129
+ }
2130
+ async function deploymentsShowCommand(deps, prefix) {
2131
+ const { project, token } = await projectManagement(deps);
2132
+ const deployment = selectDeployment(await deps.api.listDeployments(project.id, token), prefix);
2133
+ const rollouts = (await deps.api.listRollouts(project.id, token)).filter(
2134
+ (rollout) => rollout.deployment_id === deployment.id || rollout.previous_deployment_id === deployment.id
2135
+ );
2136
+ deps.output({
2137
+ message: renderDeploymentDetail(deployment, rollouts),
2138
+ project,
2139
+ deployment,
2140
+ rollouts
2141
+ });
2142
+ }
2143
+ function renderDeploymentDetail(deployment, rollouts) {
2144
+ const git = deployment.origin.git;
2145
+ const source = deployment.spec.source;
2146
+ const bundleLabel = typeof source?.sha256 === "string" ? [
2147
+ typeof source.format === "string" ? source.format : "",
2148
+ source.sha256,
2149
+ typeof source.size_bytes === "number" ? `(${source.size_bytes} bytes)` : ""
2150
+ ].filter(Boolean).join(" ") : null;
2151
+ const inference = deployment.spec.inference;
2152
+ const rows = [
2153
+ ["Deployment", `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`],
2154
+ ["ID", deployment.id],
2155
+ ["Status", productionStateLabel(deployment)],
2156
+ ["Source", `${deploymentSourceLabel(deployment)}${git?.dirty ? " (dirty)" : ""}`]
2157
+ ];
2158
+ if (git) rows.push(["Commit", git.commit_sha], ["Author", git.author ?? ""]);
2159
+ rows.push(
2160
+ ["Created", formatTimestamp(deployment.created_at)],
2161
+ ["Created by", deployment.created_by_user_id ?? ""],
2162
+ ["Spec sha256", deployment.spec_sha256]
2163
+ );
2164
+ if (typeof inference?.model === "string") rows.push(["Model", inference.model]);
2165
+ if (bundleLabel) rows.push(["Bundle", bundleLabel]);
2166
+ const label = Math.max(...rows.map(([name]) => name.length)) + 1;
2167
+ const lines = rows.map(([name, value]) => `${`${name}:`.padEnd(label)} ${value}`.trimEnd());
2168
+ lines.push("", "Rollouts:");
2169
+ if (rollouts.length === 0) lines.push(" none");
2170
+ for (const rollout of rollouts) {
2171
+ 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)}`;
2172
+ lines.push(
2173
+ ` generation ${rollout.generation} ${formatTimestamp(rollout.created_at)} ${direction}${rollout.created_by_user_id ? ` by ${shortDeploymentId(rollout.created_by_user_id)}` : ""}`
2174
+ );
2175
+ }
2176
+ return lines.join("\n");
2177
+ }
2178
+ async function deploymentsPromoteCommand(deps, prefix) {
2179
+ const { project, token } = await projectManagement(deps);
2180
+ const target = selectDeployment(await deps.api.listDeployments(project.id, token), prefix);
2181
+ const result = await moveProduction(deps, project.id, target, token);
2182
+ deps.output({
2183
+ message: describeProductionMove(project, result, "Promoted"),
2184
+ project,
2185
+ ...result
2186
+ });
2187
+ }
2188
+ async function deploymentsRollbackCommand(deps, prefix) {
2189
+ const { project, token } = await projectManagement(deps);
2190
+ let target;
2191
+ if (prefix !== void 0) {
2192
+ target = selectDeployment(await deps.api.listDeployments(project.id, token), prefix);
2193
+ } else {
2194
+ const rollouts = await deps.api.listRollouts(project.id, token);
2195
+ const previousId = rollouts.find(
2196
+ (rollout) => rollout.previous_deployment_id !== null
2197
+ )?.previous_deployment_id;
2198
+ if (!previousId) {
2199
+ throw new Error(
2200
+ `Project ${project.name} has no previous Production Deployment to roll back to. Name one with \`tonbo deployments rollback <id>\`.`
2201
+ );
2202
+ }
2203
+ target = await deps.api.getDeployment(project.id, previousId, token);
2204
+ }
2205
+ const result = await moveProduction(deps, project.id, target, token);
2206
+ deps.output({
2207
+ message: describeProductionMove(project, result, "Rolled back"),
2208
+ project,
2209
+ ...result
2210
+ });
2211
+ }
2212
+ async function moveProduction(deps, projectId, target, token) {
2213
+ const previous = await deps.api.getProduction(projectId, token);
2214
+ const production = await deps.api.putProduction(
2215
+ projectId,
2216
+ {
2217
+ deployment_id: target.id,
2218
+ desired_state: "running",
2219
+ expected_generation: previous ? previous.generation : null
2220
+ },
2221
+ token
2222
+ );
2223
+ return {
2224
+ deployment: await deps.api.getDeployment(projectId, target.id, token),
2225
+ previous,
2226
+ production
2227
+ };
2228
+ }
2229
+ function describeProductionMove(project, move, verb) {
2230
+ const { deployment, previous } = move;
2231
+ const name = `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`;
2232
+ const headline = previous?.deployment_id === deployment.id ? `Deployment ${name} is already Production for project ${project.name}; requested it to be running.` : `${verb} ${name} to Production for project ${project.name}${previous ? ` (from ${shortDeploymentId(previous.deployment_id)})` : ""}.`;
2233
+ return `${headline}
2234
+ Production: ${productionStateLabel(deployment)}
2235
+ Application: https://${project.publicHostname}`;
2236
+ }
2237
+ async function projectManagement(deps) {
2238
+ const { oauthToken, project } = await resolveProject(deps);
2239
+ return {
2240
+ project,
2241
+ token: await deps.api.exchangeManagementToken(oauthToken, project.id)
2242
+ };
2243
+ }
2244
+ async function secretListCommand(deps) {
2245
+ const { project, token } = await projectManagement(deps);
2246
+ const secrets = await deps.api.listProjectSecrets(project.id, token);
2247
+ deps.output({
2248
+ message: secrets.length ? secrets.map((secret) => secret.name).join("\n") : "No project secrets are configured.",
2249
+ project,
2250
+ secrets
2251
+ });
2252
+ }
2253
+ async function secretSetCommand(deps, name, options) {
2254
+ const value = await deps.secretValue(name, options.fromEnv);
2255
+ const { project, token } = await projectManagement(deps);
2256
+ const secret = await deps.api.setProjectSecret(project.id, name, value, token);
2257
+ deps.output({
2258
+ message: `Set project secret ${secret.name}. Redeploy to replace the active runtime with this value.`,
2259
+ project,
2260
+ secret
2261
+ });
2262
+ }
2263
+ async function secretRemoveCommand(deps, name) {
2264
+ const { project, token } = await projectManagement(deps);
2265
+ await deps.api.deleteProjectSecret(project.id, name, token);
2266
+ deps.output({ message: `Removed project secret ${name}.`, project, name });
2267
+ }
2268
+
2269
+ // src/credentials.ts
2270
+ import { randomUUID as randomUUID3 } from "node:crypto";
2271
+ import { chmod, lstat as lstat4, mkdir, open as open2, readFile as readFile5, rename as rename2, rm as rm2 } from "node:fs/promises";
2272
+ import os from "node:os";
2273
+ import path5 from "node:path";
2274
+ function defaultConfigDirectory() {
2275
+ const base = process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config");
2276
+ return path5.join(base, "tonbo");
2277
+ }
2278
+ var FileCredentialStore = class {
2279
+ constructor(filename = path5.join(defaultConfigDirectory(), "credentials.json")) {
2280
+ this.filename = filename;
2281
+ }
2282
+ async load() {
2283
+ try {
2284
+ await assertPrivateRegularFile(this.filename);
2285
+ const parsed = JSON.parse(await readFile5(this.filename, "utf8"));
2286
+ if (!isOAuthTokenSet(parsed)) throw new Error("invalid token set");
2287
+ return parsed;
2288
+ } catch (error) {
2289
+ if (error.code === "ENOENT") return null;
2290
+ throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
2291
+ }
2292
+ }
2293
+ async save(tokens) {
2294
+ if (!isOAuthTokenSet(tokens)) throw new Error("Refusing to store a malformed Tonbo token set.");
2295
+ const directory = path5.dirname(this.filename);
2296
+ await mkdir(directory, { mode: 448, recursive: true });
2297
+ await preparePrivateDirectory(directory);
2298
+ await assertExistingDestinationIsSafe(this.filename);
2299
+ const temporary = path5.join(
2300
+ directory,
2301
+ `.${path5.basename(this.filename)}.${process.pid}.${randomUUID3()}.tmp`
2302
+ );
2303
+ let handle = null;
2304
+ try {
2305
+ handle = await open2(temporary, "wx", 384);
2306
+ await handle.writeFile(`${JSON.stringify(tokens, null, 2)}
2307
+ `, "utf8");
2308
+ await handle.sync();
2309
+ await handle.close();
2310
+ handle = null;
2311
+ if (process.platform !== "win32") await chmod(temporary, 384);
2312
+ await rename2(temporary, this.filename);
2313
+ } catch (error) {
2314
+ await handle?.close().catch(() => void 0);
2315
+ await rm2(temporary, { force: true }).catch(() => void 0);
2316
+ throw new Error(`Could not store Tonbo credentials at ${this.filename}.`, { cause: error });
2317
+ }
2318
+ }
2319
+ };
2320
+ function isOAuthTokenSet(value) {
2321
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2322
+ const token = value;
2323
+ return typeof token.access_token === "string" && token.access_token.length > 0 && (token.refresh_token === void 0 || typeof token.refresh_token === "string") && (token.expires_at === void 0 || Number.isInteger(token.expires_at) && (token.expires_at ?? 0) > 0);
2324
+ }
2325
+ async function preparePrivateDirectory(directory) {
2326
+ const stat = await lstat4(directory);
2327
+ if (stat.isSymbolicLink() || !stat.isDirectory())
2328
+ throw new Error("Tonbo config directory must be a real directory.");
2329
+ if (process.platform !== "win32") await chmod(directory, 448);
2330
+ }
2331
+ async function assertExistingDestinationIsSafe(filename) {
2332
+ try {
2333
+ const stat = await lstat4(filename);
2334
+ if (stat.isSymbolicLink() || !stat.isFile())
2335
+ throw new Error("Tonbo credential path must be a regular file.");
2336
+ } catch (error) {
2337
+ if (error.code !== "ENOENT") throw error;
2338
+ }
2339
+ }
2340
+ async function assertPrivateRegularFile(filename) {
2341
+ const stat = await lstat4(filename);
2342
+ if (stat.isSymbolicLink() || !stat.isFile())
2343
+ throw new Error("Tonbo credential path must be a regular file.");
2344
+ if (process.platform !== "win32" && (stat.mode & 63) !== 0)
2345
+ throw new Error("Tonbo credential file must have mode 0600.");
2346
+ }
2347
+
2348
+ // src/git.ts
2349
+ import { execFile as execFile2 } from "node:child_process";
2350
+ var defaultGitRunner = (args, cwd) => new Promise((resolve, reject) => {
2351
+ execFile2(
2352
+ "git",
2353
+ args,
2354
+ {
2355
+ cwd,
2356
+ encoding: "utf8",
2357
+ // Never take the index lock for a read: a concurrent editor or IDE
2358
+ // must not turn provenance collection into a failure.
2359
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
2360
+ windowsHide: true
2361
+ },
2362
+ (error, stdout) => error ? reject(new Error(`git ${args.join(" ")} failed.`, { cause: error })) : resolve(stdout)
2363
+ );
2364
+ });
2365
+ var COMMIT_SHA = /^[0-9a-f]{40}$/;
2366
+ var REF_MAX_LENGTH = 255;
2367
+ var SUBJECT_MAX_LENGTH = 512;
2368
+ var AUTHOR_MAX_LENGTH = 255;
2369
+ async function collectGitProvenance(root, run = defaultGitRunner) {
2370
+ const commitSha = await read(run, ["rev-parse", "HEAD"], root);
2371
+ if (!commitSha || !COMMIT_SHA.test(commitSha)) return null;
2372
+ const [ref, subject, author, status] = await Promise.all([
2373
+ read(run, ["rev-parse", "--abbrev-ref", "HEAD"], root),
2374
+ read(run, ["log", "-1", "--format=%s"], root),
2375
+ read(run, ["log", "-1", "--format=%an"], root),
2376
+ // Only the deployed tree matters: changes elsewhere in the repository do
2377
+ // not alter the uploaded bundle.
2378
+ read(run, ["status", "--porcelain", "--", "."], root)
2379
+ ]);
2380
+ return {
2381
+ commit_sha: commitSha,
2382
+ ref: ref === "HEAD" ? null : clamp(ref, REF_MAX_LENGTH),
2383
+ subject: clamp(subject, SUBJECT_MAX_LENGTH),
2384
+ author: clamp(author, AUTHOR_MAX_LENGTH),
2385
+ // When the status cannot be read the tree cannot be shown to match the
2386
+ // commit, so the Deployment is recorded as dirty rather than clean.
2387
+ dirty: status === null || status.length > 0
2388
+ };
2389
+ }
2390
+ async function read(run, args, cwd) {
2391
+ try {
2392
+ return (await run(args, cwd)).trim();
2393
+ } catch {
2394
+ return null;
2395
+ }
2396
+ }
2397
+ function clamp(value, maxLength) {
2398
+ if (!value) return null;
2399
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
2400
+ }
2401
+
2402
+ // src/progress.ts
2403
+ var FRAMES = ["|", "/", "-", "\\"];
2404
+ var TerminalProgress = class {
2405
+ constructor(stream, intervalMs = 80) {
2406
+ this.stream = stream;
2407
+ this.intervalMs = intervalMs;
2408
+ }
2409
+ activeMessage;
2410
+ frame = 0;
2411
+ lastWidth = 0;
2412
+ timer;
2413
+ start(message) {
2414
+ if (this.activeMessage) this.succeed();
2415
+ this.activeMessage = message;
2416
+ this.frame = 0;
2417
+ if (!this.stream.isTTY) {
2418
+ this.stream.write(`[..] ${message}
2419
+ `);
2420
+ return;
2421
+ }
2422
+ this.render(`[${FRAMES[this.frame]}] ${message}`);
2423
+ this.timer = setInterval(() => {
2424
+ this.frame = (this.frame + 1) % FRAMES.length;
2425
+ this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
2426
+ }, this.intervalMs);
2427
+ this.timer.unref();
2428
+ }
2429
+ succeed(message = this.activeMessage) {
2430
+ this.finish("ok", message);
2431
+ }
2432
+ fail(message = this.activeMessage) {
2433
+ this.finish("!!", message);
2434
+ }
2435
+ finish(marker, message) {
2436
+ if (this.timer) clearInterval(this.timer);
2437
+ this.timer = void 0;
2438
+ this.activeMessage = void 0;
2439
+ if (!message) return;
2440
+ const line = `[${marker}] ${message}`;
2441
+ if (this.stream.isTTY) {
2442
+ this.render(line);
2443
+ this.stream.write("\n");
2444
+ this.lastWidth = 0;
2445
+ return;
2446
+ }
2447
+ this.stream.write(`${line}
2448
+ `);
2449
+ }
2450
+ render(value) {
2451
+ this.stream.write(`\r${value.padEnd(this.lastWidth)}`);
2452
+ this.lastWidth = value.length;
2453
+ }
2454
+ };
2455
+ var silentProgress = {
2456
+ start: () => {
2457
+ },
2458
+ succeed: () => {
2459
+ },
2460
+ fail: () => {
2461
+ }
2462
+ };
2463
+
2464
+ // src/prompt.ts
2465
+ import select from "@inquirer/select";
2466
+ import { createInterface } from "node:readline/promises";
2467
+ async function terminalPrompt(question) {
2468
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
2469
+ try {
2470
+ return await prompt.question(`? ${question}`);
2471
+ } finally {
2472
+ prompt.close();
2473
+ }
2474
+ }
2475
+ async function terminalSelect(question, choices) {
2476
+ return select(
2477
+ {
2478
+ choices,
2479
+ message: question,
2480
+ pageSize: choices.length
2481
+ },
2482
+ {
2483
+ input: process.stdin,
2484
+ output: process.stderr
2485
+ }
2486
+ );
2487
+ }
2488
+
2489
+ // src/app.ts
2490
+ var packageVersion = JSON.parse(
2491
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8")
2492
+ );
2493
+ function createDependencies(json = false) {
2494
+ const accountOrigin = process.env.TONBO_ACCOUNT_ORIGIN || "https://tonbo.dev";
2495
+ const managementOrigin = process.env.TONBO_API_ORIGIN || "https://api.tonbo.dev";
2496
+ return {
2497
+ api: new TonboApi(fetch, accountOrigin, managementOrigin),
2498
+ auth: new AuthClient(new FileCredentialStore(), fetch, accountOrigin),
2499
+ cwd: () => process.cwd(),
2500
+ defaultSshPublicKeys: readDefaultSshPublicKeys,
2501
+ gitProvenance: collectGitProvenance,
2502
+ interactive: () => !json && process.stdin.isTTY === true && process.stderr.isTTY === true,
2503
+ inspectSource: inspectLocalAgentSource,
2504
+ output: (value) => {
2505
+ if (json) console.log(JSON.stringify(value));
2506
+ else console.log(value.message ?? value);
2507
+ },
2508
+ progress: json ? silentProgress : new TerminalProgress(process.stderr),
2509
+ prompt: terminalPrompt,
2510
+ select: terminalSelect,
2511
+ secretValue: (name, fromEnvironment) => {
2512
+ const environmentName = fromEnvironment ?? name;
2513
+ const value = process.env[environmentName];
2514
+ if (!value)
2515
+ throw new Error(
2516
+ `Environment variable ${environmentName} is empty. Set it before running tonbo secret set.`
2517
+ );
2518
+ return Promise.resolve(value);
2519
+ }
2520
+ };
2521
+ }
2522
+ function jsonOutput(program) {
2523
+ return program.opts().json === true;
2524
+ }
2525
+ function createProgram(dependencies = createDependencies) {
2526
+ const program = new Command().name("tonbo").description("Deploy a persistent Project Agent to Tonbo.").version(packageVersion.version).option("--json", "print machine-readable JSON");
2527
+ program.command("init").description("interactively create a Tonbo Agent declaration in this directory").option("--harness <harness>", "Agent Harness (currently pi)").option("--model <model>", "inference model").option("--driver <driver>", "PI driver: native or command").option("--agent-entry <file>", "Node entry file for the command driver").option("--build-command <argv...>", "build command argv for the command driver").option("--force", `replace an existing ${DECLARATION_FILENAME}`).action(
2528
+ async (options) => initCommand(dependencies(jsonOutput(program)), options)
2529
+ );
2530
+ program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(jsonOutput(program))));
2531
+ const machine = program.command("machine").description("manage independent account-owned Machines");
2532
+ machine.command("list").option("--account <account>", "account UUID").action(async (options) => {
2533
+ const deps = dependencies(jsonOutput(program));
2534
+ deps.output(
2535
+ await deps.api.machineRequest(
2536
+ await deps.auth.accessToken(),
2537
+ options.account ? `?account=${encodeURIComponent(options.account)}` : ""
2538
+ )
2539
+ );
2540
+ });
2541
+ machine.command("allocate").requiredOption("--account <account>", "owning account UUID").requiredOption("--region <region>", "compute region (currently us-east-1)").action(async (options) => {
2542
+ const deps = dependencies(jsonOutput(program));
2543
+ deps.output(
2544
+ await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {
2545
+ accountId: options.account,
2546
+ region: options.region
2547
+ })
2548
+ );
2549
+ });
2550
+ for (const operation of ["show", "metrics", "release"]) {
2551
+ machine.command(`${operation} <machine>`).action(async (id) => {
2552
+ const deps = dependencies(jsonOutput(program));
2553
+ const result = await deps.api.machineRequest(
2554
+ await deps.auth.accessToken(),
2555
+ `/${encodeURIComponent(id)}`,
2556
+ operation === "release" ? "DELETE" : "GET"
2557
+ );
2558
+ deps.output(operation === "metrics" ? result.metrics : result);
2559
+ });
2560
+ }
2561
+ machine.command("bind <machine>").requiredOption("--agent <agent>", "Agent UUID").action(async (id, options) => {
2562
+ const deps = dependencies(jsonOutput(program));
2563
+ deps.output(
2564
+ await deps.api.machineRequest(
2565
+ await deps.auth.accessToken(),
2566
+ `/${encodeURIComponent(id)}/binding`,
2567
+ "POST",
2568
+ { agentId: options.agent }
2569
+ )
2570
+ );
2571
+ });
2572
+ machine.command("unbind <machine>").description("detach Agent and reset system disk; retain Agent Workspace and Sessions").action(async (id) => {
2573
+ const deps = dependencies(jsonOutput(program));
2574
+ const token = await deps.auth.accessToken();
2575
+ const path6 = `/${encodeURIComponent(id)}`;
2576
+ const current = await deps.api.machineRequest(token, path6);
2577
+ if (!current.binding) throw new Error("Machine is not bound to an Agent.");
2578
+ deps.output(
2579
+ await deps.api.machineRequest(token, `${path6}/binding`, "DELETE", {
2580
+ agentId: current.binding.agent_id,
2581
+ bindingId: current.binding.id,
2582
+ generation: current.binding.generation
2583
+ })
2584
+ );
2585
+ });
2586
+ const project = program.command("project").description("manage the project bound to this Agent directory");
2587
+ project.command("create <name>").description("create and bind a project to this Agent directory").action(async (name) => projectCreateCommand(dependencies(jsonOutput(program)), name));
2588
+ const sshKey = program.command("ssh-key").description("manage public keys used by native project SSH");
2589
+ sshKey.command("add <public-key>").description("register an OpenSSH public key with the current Tonbo account").action(async (path6) => sshKeyAddCommand(dependencies(jsonOutput(program)), path6));
2590
+ sshKey.command("remove <fingerprint>").description("revoke an SSH public key from the current Tonbo account").action(
2591
+ async (fingerprint) => sshKeyRemoveCommand(dependencies(jsonOutput(program)), fingerprint)
2592
+ );
2593
+ project.command("use <project>").description("write a project ID into this Agent's .tonbo declaration").option("--force", "replace an existing project binding without confirmation").action(
2594
+ async (selector, options) => projectUseCommand(dependencies(jsonOutput(program)), selector, options.force === true)
2595
+ );
2596
+ project.command("show").description("show the project bound to this Agent directory and its connection addresses").action(async () => projectShowCommand(dependencies(jsonOutput(program))));
2597
+ program.command("deploy").description("upload this directory as a new Deployment and promote it to Production").option("--no-promote", "create the Deployment without moving Production").action(
2598
+ async (options) => deployCommand(dependencies(jsonOutput(program)), { promote: options.promote })
2599
+ );
2600
+ const deployments = program.command("deployments").description("list and move between the immutable Deployments of the bound project");
2601
+ deployments.command("list", { isDefault: true }).description("list Deployments, newest first").action(async () => deploymentsListCommand(dependencies(jsonOutput(program))));
2602
+ deployments.command("show <deployment>").description("show one Deployment and its Rollouts by id prefix").action(
2603
+ async (prefix) => deploymentsShowCommand(dependencies(jsonOutput(program)), prefix)
2604
+ );
2605
+ deployments.command("promote <deployment>").description("move Production to a Deployment by id prefix").action(
2606
+ async (prefix) => deploymentsPromoteCommand(dependencies(jsonOutput(program)), prefix)
2607
+ );
2608
+ deployments.command("rollback [deployment]").description("move Production back to the previous Production Deployment, or to a named one").action(
2609
+ async (prefix) => deploymentsRollbackCommand(dependencies(jsonOutput(program)), prefix)
2610
+ );
2611
+ program.command("run <prompt>").description("run one prompt in a durable project session").option("--session <session>", "resume an existing Agent Session UUID").action(
2612
+ async (prompt, options) => runCommand(dependencies(jsonOutput(program)), prompt, options)
2613
+ );
2614
+ const secret = program.command("secret").description("manage encrypted environment secrets for the project service");
2615
+ secret.command("list").action(async () => secretListCommand(dependencies(jsonOutput(program))));
2616
+ secret.command("set <name>").description("set a secret from an environment variable (the same name by default)").option("--from-env <name>", "read the value from another environment variable").action(
2617
+ async (name, options) => secretSetCommand(dependencies(jsonOutput(program)), name, options)
2618
+ );
2619
+ secret.command("remove <name>").action(async (name) => secretRemoveCommand(dependencies(jsonOutput(program)), name));
2620
+ return program;
2621
+ }
2622
+
2623
+ // src/main.ts
2624
+ try {
2625
+ await createProgram().parseAsync(process.argv);
2626
+ } catch (error) {
2627
+ console.error(error instanceof Error ? error.message : error);
2628
+ process.exitCode = 1;
2629
+ }