@tonbo/cli 0.0.5 → 0.0.7

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,1981 @@
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 { createHash, 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
+ function stableJson(value) {
37
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
38
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
39
+ const object = value;
40
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
41
+ }
42
+ function digest(value) {
43
+ return createHash("sha256").update(stableJson(value)).digest("hex");
44
+ }
45
+ var TonboApi = class {
46
+ constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
47
+ this.fetcher = fetcher;
48
+ this.accountOrigin = accountOrigin;
49
+ this.managementOrigin = managementOrigin;
50
+ }
51
+ listProjects(oauthToken) {
52
+ return requestJson(
53
+ this.fetcher,
54
+ `${this.accountOrigin}/api/cli/projects`,
55
+ {
56
+ headers: { authorization: `Bearer ${oauthToken}` }
57
+ }
58
+ ).then((body) => body.projects);
59
+ }
60
+ createProject(oauthToken, slug, name) {
61
+ return requestJson(
62
+ this.fetcher,
63
+ `${this.accountOrigin}/api/cli/projects`,
64
+ {
65
+ method: "POST",
66
+ headers: {
67
+ authorization: `Bearer ${oauthToken}`,
68
+ "content-type": "application/json"
69
+ },
70
+ body: JSON.stringify({ slug, ...name ? { name } : {} })
71
+ }
72
+ ).then((body) => body.project);
73
+ }
74
+ registerSshKey(oauthToken, key) {
75
+ return requestJson(
76
+ this.fetcher,
77
+ `${this.accountOrigin}/api/cli/ssh-keys`,
78
+ {
79
+ method: "POST",
80
+ headers: {
81
+ authorization: `Bearer ${oauthToken}`,
82
+ "content-type": "application/json"
83
+ },
84
+ body: JSON.stringify({
85
+ algorithm: key.algorithm,
86
+ key_base64: key.keyBase64,
87
+ label: key.label
88
+ })
89
+ }
90
+ ).then((body) => body.key);
91
+ }
92
+ revokeSshKey(oauthToken, fingerprint) {
93
+ return requestJson(
94
+ this.fetcher,
95
+ `${this.accountOrigin}/api/cli/ssh-keys`,
96
+ {
97
+ method: "DELETE",
98
+ headers: {
99
+ authorization: `Bearer ${oauthToken}`,
100
+ "content-type": "application/json"
101
+ },
102
+ body: JSON.stringify({ fingerprint })
103
+ }
104
+ ).then((body) => body.key);
105
+ }
106
+ exchangeManagementToken(oauthToken, projectId) {
107
+ return requestJson(
108
+ this.fetcher,
109
+ `${this.accountOrigin}/api/cli/projects/${projectId}/token`,
110
+ {
111
+ method: "POST",
112
+ headers: { authorization: `Bearer ${oauthToken}` }
113
+ }
114
+ ).then((body) => body.access_token);
115
+ }
116
+ async deploy({
117
+ bundle,
118
+ projectId,
119
+ spec,
120
+ token
121
+ }) {
122
+ const descriptor = {
123
+ format: bundle.format,
124
+ sha256: bundle.sha256,
125
+ size_bytes: bundle.size_bytes
126
+ };
127
+ const projectPath = `/v1/projects/${projectId}`;
128
+ const bundlesPath = `${projectPath}/source-bundles`;
129
+ const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
130
+ format: descriptor.format,
131
+ size_bytes: descriptor.size_bytes
132
+ });
133
+ if (prepared.status === "upload") {
134
+ if (!prepared.upload_url) throw new Error("Tonbo did not return a source upload URL.");
135
+ const uploaded = await this.fetcher(prepared.upload_url, {
136
+ method: "PUT",
137
+ headers: {
138
+ "content-type": prepared.content_type ?? "application/vnd.tonbo.source+tar",
139
+ "x-upsert": "false"
140
+ },
141
+ body: new Blob([new Uint8Array(bundle.bytes)])
142
+ });
143
+ let uploadError = null;
144
+ if (!uploaded.ok) {
145
+ uploadError = new Error(`Source upload failed with HTTP ${uploaded.status}.`);
146
+ }
147
+ try {
148
+ await this.management("POST", `${bundlesPath}/${bundle.sha256}/complete`, token, {
149
+ format: bundle.format,
150
+ size_bytes: bundle.size_bytes
151
+ });
152
+ } catch (error) {
153
+ if (uploadError)
154
+ throw new AggregateError([uploadError, error], "Source bundle upload did not complete.");
155
+ throw error;
156
+ }
157
+ }
158
+ const revisionsPath = `${projectPath}/revisions`;
159
+ const revisionDigest = digest(spec);
160
+ const revisions = await this.managementList(revisionsPath, token);
161
+ let revision = revisions.find((candidate) => candidate.spec_sha256 === revisionDigest);
162
+ if (!revision) {
163
+ revision = (await this.management("POST", revisionsPath, token, { spec })).data;
164
+ }
165
+ const deploymentPath = `${projectPath}/deployment`;
166
+ const current = await this.management(
167
+ "GET",
168
+ deploymentPath,
169
+ token
170
+ ).catch((error) => {
171
+ if (error.status === 404) return null;
172
+ throw error;
173
+ });
174
+ const deployment = (await this.management("PUT", deploymentPath, token, {
175
+ desired_revision_id: revision.id,
176
+ desired_state: "running",
177
+ expected_generation: current ? Number(current.data.generation) : null
178
+ })).data;
179
+ return { deployment, revision };
180
+ }
181
+ async run({
182
+ projectId,
183
+ prompt,
184
+ sessionId,
185
+ token,
186
+ turnId = randomUUID()
187
+ }) {
188
+ const projectPath = `/v1/projects/${projectId}`;
189
+ const deployment = (await this.management("GET", `${projectPath}/deployment`, token)).data;
190
+ if (deployment.observed_state !== "running")
191
+ throw new Error(`Project Agent is ${deployment.observed_state}; wait for it to be running.`);
192
+ const session = sessionId ? { id: sessionId } : (await this.management(
193
+ "POST",
194
+ `${projectPath}/sessions`,
195
+ token,
196
+ { revision_id: deployment.desired_revision_id }
197
+ )).data;
198
+ const turn = (await this.management(
199
+ "POST",
200
+ `${projectPath}/sessions/${session.id}/turns`,
201
+ token,
202
+ { prompt },
203
+ turnId
204
+ )).data;
205
+ return { deployment, session, turn };
206
+ }
207
+ turnEvents({
208
+ after = 0,
209
+ projectId,
210
+ sessionId,
211
+ token,
212
+ turnId
213
+ }) {
214
+ return this.management(
215
+ "GET",
216
+ `/v1/projects/${projectId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
217
+ token
218
+ );
219
+ }
220
+ listProjectSecrets(projectId, token) {
221
+ return this.management(
222
+ "GET",
223
+ `/v1/projects/${projectId}/secrets`,
224
+ token
225
+ ).then((body) => body.data);
226
+ }
227
+ setProjectSecret(projectId, name, value, token) {
228
+ return this.management(
229
+ "PUT",
230
+ `/v1/projects/${projectId}/secrets/${encodeURIComponent(name)}`,
231
+ token,
232
+ { value }
233
+ );
234
+ }
235
+ deleteProjectSecret(projectId, name, token) {
236
+ return this.management(
237
+ "DELETE",
238
+ `/v1/projects/${projectId}/secrets/${encodeURIComponent(name)}`,
239
+ token
240
+ );
241
+ }
242
+ management(method, path7, token, body, idempotencyKey) {
243
+ return requestJson(this.fetcher, `${this.managementOrigin}${path7}`, {
244
+ method,
245
+ headers: {
246
+ authorization: `Bearer ${token}`,
247
+ ...body === void 0 ? {} : {
248
+ "content-type": "application/json",
249
+ "idempotency-key": idempotencyKey ?? randomUUID()
250
+ }
251
+ },
252
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
253
+ });
254
+ }
255
+ async managementList(path7, token) {
256
+ const values = [];
257
+ let cursor = null;
258
+ do {
259
+ const separator = path7.includes("?") ? "&" : "?";
260
+ const page = await this.management(
261
+ "GET",
262
+ `${path7}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
263
+ token
264
+ );
265
+ values.push(...page.data);
266
+ cursor = page.next_cursor;
267
+ } while (cursor);
268
+ return values;
269
+ }
270
+ };
271
+
272
+ // src/agent-source.ts
273
+ import { lstat, readFile } from "node:fs/promises";
274
+ import path from "node:path";
275
+
276
+ // ../../packages/agent-source-inspector/src/index.ts
277
+ var AGENTS_INSTRUCTIONS_PATH = "AGENTS.md";
278
+ var PI_SETTINGS_PATH = ".pi/settings.json";
279
+ var MAX_HARNESS_CONFIG_BYTES = 256 * 1024;
280
+ var supportedHarnesses = [
281
+ {
282
+ command: "pi",
283
+ id: "pi",
284
+ name: "PI"
285
+ }
286
+ ];
287
+ var supportedExecutionTargets = [
288
+ { driver: "native", harness: "pi" },
289
+ { driver: "command", harness: "pi" }
290
+ ];
291
+ var AgentSourceInspectionError = class extends Error {
292
+ constructor(code, path7, message, options) {
293
+ super(message, options);
294
+ this.code = code;
295
+ this.path = path7;
296
+ this.name = "AgentSourceInspectionError";
297
+ }
298
+ };
299
+ function parsePiPackageCount(contents) {
300
+ let settings;
301
+ try {
302
+ settings = JSON.parse(contents);
303
+ } catch (error) {
304
+ throw new AgentSourceInspectionError(
305
+ "invalid_pi_settings",
306
+ PI_SETTINGS_PATH,
307
+ `${PI_SETTINGS_PATH} must contain valid JSON.`,
308
+ { cause: error }
309
+ );
310
+ }
311
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
312
+ throw new AgentSourceInspectionError(
313
+ "invalid_pi_settings",
314
+ PI_SETTINGS_PATH,
315
+ `${PI_SETTINGS_PATH} must contain a JSON object.`
316
+ );
317
+ }
318
+ if (!("packages" in settings)) return 0;
319
+ const packages = settings.packages;
320
+ if (!Array.isArray(packages)) {
321
+ throw new AgentSourceInspectionError(
322
+ "invalid_pi_settings",
323
+ PI_SETTINGS_PATH,
324
+ `${PI_SETTINGS_PATH} packages must be an array.`
325
+ );
326
+ }
327
+ return packages.length;
328
+ }
329
+ async function inspectAgentSource(source) {
330
+ const [agentsMd, piSettings] = await Promise.all([
331
+ source.has(AGENTS_INSTRUCTIONS_PATH),
332
+ source.readText(PI_SETTINGS_PATH, MAX_HARNESS_CONFIG_BYTES)
333
+ ]);
334
+ if (piSettings !== null) {
335
+ return {
336
+ version: 1,
337
+ instructions: { agentsMd },
338
+ harness: {
339
+ id: "pi",
340
+ reason: "pi_settings_found",
341
+ state: "identified"
342
+ },
343
+ pi: {
344
+ packageCount: parsePiPackageCount(piSettings),
345
+ settingsFound: true
346
+ }
347
+ };
348
+ }
349
+ return {
350
+ version: 1,
351
+ instructions: { agentsMd },
352
+ harness: {
353
+ candidates: supportedHarnesses.map((harness) => harness.id),
354
+ reason: "no_harness_specific_config",
355
+ state: "selection_required"
356
+ },
357
+ pi: {
358
+ packageCount: 0,
359
+ settingsFound: false
360
+ }
361
+ };
362
+ }
363
+
364
+ // src/agent-source.ts
365
+ var LocalAgentSource = class {
366
+ constructor(root) {
367
+ this.root = root;
368
+ }
369
+ async has(filename) {
370
+ const metadata = await this.metadata(filename);
371
+ return metadata?.isFile() === true;
372
+ }
373
+ async readText(filename, maxBytes) {
374
+ const metadata = await this.metadata(filename);
375
+ if (!metadata) return null;
376
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
377
+ throw new Error(`${filename} must be a regular file.`);
378
+ }
379
+ if (metadata.size > maxBytes) {
380
+ throw new Error(`${filename} exceeds the ${maxBytes}-byte inspection limit.`);
381
+ }
382
+ return readFile(path.join(this.root, filename), "utf8");
383
+ }
384
+ async metadata(filename) {
385
+ try {
386
+ return await lstat(path.join(this.root, filename));
387
+ } catch (error) {
388
+ if (error.code === "ENOENT") return null;
389
+ throw error;
390
+ }
391
+ }
392
+ };
393
+ function inspectLocalAgentSource(root) {
394
+ return inspectAgentSource(new LocalAgentSource(root));
395
+ }
396
+
397
+ // src/auth.ts
398
+ import { createHash as createHash2, randomBytes } from "node:crypto";
399
+ import { createServer } from "node:http";
400
+ import { execFile } from "node:child_process";
401
+ import { promisify } from "node:util";
402
+
403
+ // src/callback-page.ts
404
+ var WORDMARK = [
405
+ "\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",
406
+ "\u2588\u2588\u2584 \u2584\u2588\u2588\u2580\u2584 \u2588\u2588 \u2584\u2580\u2588\u2588 \u2584 \u2588\u2588 \u2584\u2580\u2588",
407
+ "\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"
408
+ ].join("\n");
409
+ var OUTCOMES = {
410
+ complete: {
411
+ status: 200,
412
+ title: "You are signed in.",
413
+ body: "Return to your terminal. This tab can be closed."
414
+ },
415
+ denied: {
416
+ status: 400,
417
+ title: "Login was not completed.",
418
+ body: "Nothing was authorized. Return to your terminal to try again."
419
+ },
420
+ invalid: {
421
+ // The state did not match, the code was missing, or the path was wrong.
422
+ // Say that it could not be verified rather than which check failed: the
423
+ // person who can read this page is not necessarily the one who started
424
+ // the login.
425
+ status: 400,
426
+ title: "This callback could not be verified.",
427
+ body: "Return to your terminal and start the login again."
428
+ }
429
+ };
430
+ function renderCallbackPage(outcome) {
431
+ const { title, body } = OUTCOMES[outcome];
432
+ return `<!doctype html>
433
+ <html lang="en">
434
+ <head>
435
+ <meta charset="utf-8">
436
+ <meta name="viewport" content="width=device-width, initial-scale=1">
437
+ <meta name="robots" content="noindex">
438
+ <title>Tonbo CLI</title>
439
+ <style>
440
+ :root {
441
+ --paper: #f3f2ee;
442
+ --ink: #1d1b17;
443
+ --rule: #c9c5ba;
444
+ --quiet: #5c564a;
445
+ --signal: #d64a03;
446
+ }
447
+ @media (prefers-color-scheme: dark) {
448
+ :root {
449
+ --paper: #1d1b17;
450
+ --ink: #f3f2ee;
451
+ --rule: #3a362e;
452
+ --quiet: #a49e8f;
453
+ --signal: #f08b4b;
454
+ }
455
+ }
456
+ * { box-sizing: border-box; }
457
+ body {
458
+ margin: 0;
459
+ min-height: 100vh;
460
+ display: grid;
461
+ grid-template-rows: auto 1fr;
462
+ padding: 0 clamp(20px, 5vw, 72px);
463
+ background: var(--paper);
464
+ color: var(--ink);
465
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
466
+ -webkit-font-smoothing: antialiased;
467
+ }
468
+ header { padding: 28px 0; }
469
+ /* Same metrics as .auth-wordmark in the app: the glyphs are the ground
470
+ and the letters are the gaps, so the 8px/10px/scaleY(0.8) combination is
471
+ what makes the rows meet without seams. Iosevka is not available here,
472
+ so it falls back to the platform monospace. */
473
+ pre.wordmark {
474
+ margin: 0;
475
+ font-family: 'Iosevka', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
476
+ font-size: 8px;
477
+ font-weight: 400;
478
+ line-height: 10px;
479
+ letter-spacing: 0;
480
+ white-space: pre;
481
+ color: var(--signal);
482
+ transform: scaleY(0.8);
483
+ transform-origin: left top;
484
+ }
485
+ main {
486
+ display: flex;
487
+ flex-direction: column;
488
+ justify-content: center;
489
+ max-width: 46ch;
490
+ padding-bottom: 12vh;
491
+ }
492
+ h1 {
493
+ margin: 0;
494
+ font-size: 26px;
495
+ font-weight: 300;
496
+ line-height: 1.16;
497
+ letter-spacing: -0.018em;
498
+ }
499
+ p {
500
+ margin: 14px 0 0;
501
+ font-size: 16px;
502
+ line-height: 1.55;
503
+ color: var(--quiet);
504
+ }
505
+ hr {
506
+ width: 40px;
507
+ margin: 28px 0 0;
508
+ border: 0;
509
+ border-top: 1px solid var(--rule);
510
+ }
511
+ </style>
512
+ </head>
513
+ <body>
514
+ <header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
515
+ <main>
516
+ <h1>${title}</h1>
517
+ <p>${body}</p>
518
+ <hr>
519
+ </main>
520
+ </body>
521
+ </html>
522
+ `;
523
+ }
524
+ function callbackResponse(outcome) {
525
+ return {
526
+ status: OUTCOMES[outcome].status,
527
+ headers: {
528
+ "content-type": "text/html; charset=utf-8",
529
+ "cache-control": "no-store"
530
+ },
531
+ body: renderCallbackPage(outcome)
532
+ };
533
+ }
534
+
535
+ // src/auth.ts
536
+ var exec = promisify(execFile);
537
+ var LOOPBACK_REDIRECT = "http://localhost:17655/callback";
538
+ function base64url(value) {
539
+ return value.toString("base64url");
540
+ }
541
+ function withAbsoluteExpiry(tokens) {
542
+ if (tokens.expires_at || !Number.isInteger(tokens.expires_in) || (tokens.expires_in ?? 0) <= 0)
543
+ return tokens;
544
+ return {
545
+ ...tokens,
546
+ expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 0)
547
+ };
548
+ }
549
+ var AuthClient = class {
550
+ constructor(credentials, fetcher, accountOrigin = "https://tonbo.dev", browserOpener = openBrowser) {
551
+ this.credentials = credentials;
552
+ this.fetcher = fetcher;
553
+ this.accountOrigin = accountOrigin;
554
+ this.browserOpener = browserOpener;
555
+ }
556
+ async accessToken() {
557
+ const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
558
+ if (injected) return injected;
559
+ const tokens = await this.credentials.load();
560
+ if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
561
+ const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
562
+ if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + 30))
563
+ return tokens.access_token;
564
+ if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
565
+ const config = await this.config();
566
+ const refreshed = withAbsoluteExpiry(
567
+ await this.exchange(
568
+ config.token_endpoint,
569
+ new URLSearchParams({
570
+ client_id: config.client_id,
571
+ grant_type: "refresh_token",
572
+ refresh_token: tokens.refresh_token
573
+ })
574
+ )
575
+ );
576
+ if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
577
+ await this.credentials.save(refreshed);
578
+ return refreshed.access_token;
579
+ }
580
+ async login(progress = () => {
581
+ }) {
582
+ progress({ status: "started", step: "account-config" });
583
+ const config = await this.config();
584
+ progress({ status: "completed", step: "account-config" });
585
+ if (config.redirect_uri !== LOOPBACK_REDIRECT)
586
+ throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
587
+ const verifier = base64url(randomBytes(48));
588
+ const challenge = createHash2("sha256").update(verifier).digest("base64url");
589
+ const state = base64url(randomBytes(24));
590
+ const authorization = new URL(config.authorization_endpoint);
591
+ authorization.search = new URLSearchParams({
592
+ client_id: config.client_id,
593
+ code_challenge: challenge,
594
+ code_challenge_method: "S256",
595
+ redirect_uri: config.redirect_uri,
596
+ response_type: "code",
597
+ scope: "openid profile email",
598
+ state
599
+ }).toString();
600
+ const code = await this.listenForCode(
601
+ state,
602
+ () => this.browserOpener(authorization.toString()),
603
+ progress
604
+ );
605
+ progress({ status: "started", step: "token-exchange" });
606
+ const tokens = withAbsoluteExpiry(
607
+ await this.exchange(
608
+ config.token_endpoint,
609
+ new URLSearchParams({
610
+ client_id: config.client_id,
611
+ code,
612
+ code_verifier: verifier,
613
+ grant_type: "authorization_code",
614
+ redirect_uri: config.redirect_uri
615
+ })
616
+ )
617
+ );
618
+ progress({ status: "completed", step: "token-exchange" });
619
+ progress({ status: "started", step: "credential-store" });
620
+ await this.credentials.save(tokens);
621
+ progress({ status: "completed", step: "credential-store" });
622
+ return tokens;
623
+ }
624
+ config() {
625
+ return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/config`);
626
+ }
627
+ exchange(endpoint, body) {
628
+ return requestJson(this.fetcher, endpoint, {
629
+ method: "POST",
630
+ headers: { "content-type": "application/x-www-form-urlencoded" },
631
+ body
632
+ });
633
+ }
634
+ listenForCode(expectedState, ready, progress) {
635
+ return new Promise((resolve, reject) => {
636
+ let settled = false;
637
+ const sockets = /* @__PURE__ */ new Set();
638
+ let authorizationStarted = false;
639
+ const startAuthorization = () => {
640
+ if (authorizationStarted) return;
641
+ authorizationStarted = true;
642
+ progress({ status: "completed", step: "browser" });
643
+ progress({ status: "started", step: "browser-authorization" });
644
+ };
645
+ const finish = (result, completedResponseSocket) => {
646
+ if (settled) return;
647
+ settled = true;
648
+ clearTimeout(timeout);
649
+ const complete = (closeError) => {
650
+ if (closeError) reject(closeError);
651
+ else if ("code" in result) {
652
+ progress({ status: "completed", step: "callback-close" });
653
+ resolve(result.code);
654
+ } else reject(result.error);
655
+ };
656
+ if ("code" in result) progress({ status: "started", step: "callback-close" });
657
+ if (!server.listening) {
658
+ complete();
659
+ return;
660
+ }
661
+ server.close(complete);
662
+ for (const socket of sockets) {
663
+ if (socket !== completedResponseSocket) socket.destroy();
664
+ }
665
+ server.closeIdleConnections();
666
+ };
667
+ const timeout = setTimeout(
668
+ () => {
669
+ finish({ error: new Error("Timed out waiting for browser login.") });
670
+ },
671
+ 5 * 60 * 1e3
672
+ );
673
+ const server = createServer((request, response) => {
674
+ const url = new URL(request.url ?? "/", LOOPBACK_REDIRECT);
675
+ const code = url.searchParams.get("code");
676
+ const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
677
+ if (oauthError) {
678
+ const responseSocket2 = response.socket;
679
+ respond(
680
+ response,
681
+ "denied",
682
+ () => finish({ error: new Error(oauthError) }, responseSocket2)
683
+ );
684
+ return;
685
+ }
686
+ if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState || !code) {
687
+ respond(response, "invalid");
688
+ return;
689
+ }
690
+ startAuthorization();
691
+ progress({ status: "completed", step: "browser-authorization" });
692
+ const responseSocket = response.socket;
693
+ respond(response, "complete", () => finish({ code }, responseSocket));
694
+ });
695
+ server.on("connection", (socket) => {
696
+ sockets.add(socket);
697
+ socket.once("close", () => sockets.delete(socket));
698
+ });
699
+ server.once("error", (error) => finish({ error }));
700
+ progress({ status: "started", step: "callback-server" });
701
+ server.listen(17655, "localhost", () => {
702
+ progress({ status: "completed", step: "callback-server" });
703
+ progress({ status: "started", step: "browser" });
704
+ void ready().then(startAuthorization).catch(
705
+ (error) => finish({
706
+ error: error instanceof Error ? error : new Error("Could not open the login browser.")
707
+ })
708
+ );
709
+ });
710
+ });
711
+ }
712
+ };
713
+ function respond(response, outcome, finished) {
714
+ const page = callbackResponse(outcome);
715
+ if (finished) response.once("finish", finished);
716
+ response.writeHead(page.status, { ...page.headers, connection: "close" });
717
+ response.end(page.body);
718
+ }
719
+ async function openBrowser(url) {
720
+ if (process.platform === "darwin") return void await exec("open", [url]);
721
+ if (process.platform === "win32")
722
+ return void await exec("rundll32", ["url.dll,FileProtocolHandler", url]);
723
+ return void await exec("xdg-open", [url]);
724
+ }
725
+
726
+ // src/commands.ts
727
+ import path4 from "node:path";
728
+
729
+ // src/build.ts
730
+ import { spawn } from "node:child_process";
731
+ async function runBuildCommand(root, command) {
732
+ if (command.length === 0) throw new Error("Build command must not be empty.");
733
+ await new Promise((resolve, reject) => {
734
+ const child = spawn(command[0], command.slice(1), {
735
+ cwd: root,
736
+ env: process.env,
737
+ // Command output is progress, not the CLI result. Keep stdout available
738
+ // for the single JSON document emitted by `tonbo --json deploy`.
739
+ stdio: ["inherit", process.stderr, process.stderr]
740
+ });
741
+ child.once("error", reject);
742
+ child.once("exit", (code, signal) => {
743
+ if (code === 0) resolve();
744
+ else
745
+ reject(
746
+ new Error(
747
+ `Build command failed${signal ? ` with signal ${signal}` : ` with status ${code}`}.`
748
+ )
749
+ );
750
+ });
751
+ });
752
+ }
753
+
754
+ // src/declaration.ts
755
+ import { randomUUID as randomUUID2 } from "node:crypto";
756
+ import { lstat as lstat2, open, readFile as readFile2, rename, rm } from "node:fs/promises";
757
+ import path2 from "node:path";
758
+ import { parse, stringify } from "smol-toml";
759
+
760
+ // src/contracts.ts
761
+ import { Ajv2020 } from "ajv/dist/2020.js";
762
+
763
+ // src/generated/contracts.ts
764
+ var piAgentSchema = {
765
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
766
+ "$id": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json",
767
+ "title": "PI Agent v1",
768
+ "type": "object",
769
+ "additionalProperties": false,
770
+ "required": [
771
+ "runtime",
772
+ "driver"
773
+ ],
774
+ "properties": {
775
+ "runtime": {
776
+ "const": "pi"
777
+ },
778
+ "secrets": {
779
+ "type": "array",
780
+ "maxItems": 32,
781
+ "uniqueItems": true,
782
+ "items": {
783
+ "type": "string",
784
+ "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
785
+ }
786
+ },
787
+ "driver": {
788
+ "oneOf": [
789
+ {
790
+ "type": "object",
791
+ "additionalProperties": false,
792
+ "required": [
793
+ "kind"
794
+ ],
795
+ "properties": {
796
+ "kind": {
797
+ "const": "native"
798
+ }
799
+ }
800
+ },
801
+ {
802
+ "type": "object",
803
+ "additionalProperties": false,
804
+ "required": [
805
+ "kind",
806
+ "protocol",
807
+ "command"
808
+ ],
809
+ "properties": {
810
+ "kind": {
811
+ "const": "command"
812
+ },
813
+ "protocol": {
814
+ "const": "pi-rpc-v1"
815
+ },
816
+ "command": {
817
+ "type": "array",
818
+ "minItems": 1,
819
+ "maxItems": 64,
820
+ "items": {
821
+ "type": "string",
822
+ "minLength": 1,
823
+ "maxLength": 1024
824
+ }
825
+ }
826
+ }
827
+ }
828
+ ]
829
+ }
830
+ }
831
+ };
832
+ var projectServiceSchema = {
833
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
834
+ "$id": "https://contracts.tonbo.dev/agents/project-service-v1.schema.json",
835
+ "title": "Tonbo Project application service v1",
836
+ "type": "object",
837
+ "additionalProperties": false,
838
+ "required": [
839
+ "command"
840
+ ],
841
+ "properties": {
842
+ "command": {
843
+ "type": "array",
844
+ "minItems": 1,
845
+ "maxItems": 64,
846
+ "items": {
847
+ "type": "string",
848
+ "minLength": 1,
849
+ "maxLength": 4096
850
+ }
851
+ },
852
+ "secrets": {
853
+ "type": "array",
854
+ "maxItems": 32,
855
+ "uniqueItems": true,
856
+ "items": {
857
+ "type": "string",
858
+ "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
859
+ }
860
+ },
861
+ "kubernetes": {
862
+ "$ref": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json#/$defs/request"
863
+ }
864
+ }
865
+ };
866
+ var kubernetesProfilesSchema = {
867
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
868
+ "$id": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json",
869
+ "title": "Tonbo managed Kubernetes profiles v1",
870
+ "$defs": {
871
+ "request": {
872
+ "type": "object",
873
+ "additionalProperties": false,
874
+ "required": [
875
+ "profile"
876
+ ],
877
+ "properties": {
878
+ "profile": false
879
+ }
880
+ }
881
+ },
882
+ "x-tonbo-profiles": {}
883
+ };
884
+ var declarationSchema = {
885
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
886
+ "$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v1.schema.json",
887
+ "title": "Tonbo Project Agent declaration v1",
888
+ "type": "object",
889
+ "additionalProperties": false,
890
+ "required": [
891
+ "version",
892
+ "agent"
893
+ ],
894
+ "properties": {
895
+ "version": {
896
+ "const": 1
897
+ },
898
+ "agent": {
899
+ "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
900
+ },
901
+ "inference": {
902
+ "type": "object",
903
+ "additionalProperties": false,
904
+ "default": {
905
+ "model": "claude-sonnet-4-5"
906
+ },
907
+ "required": [
908
+ "model"
909
+ ],
910
+ "properties": {
911
+ "model": {
912
+ "type": "string",
913
+ "minLength": 1,
914
+ "maxLength": 160
915
+ }
916
+ }
917
+ },
918
+ "build": {
919
+ "type": "object",
920
+ "additionalProperties": false,
921
+ "required": [
922
+ "command"
923
+ ],
924
+ "properties": {
925
+ "command": {
926
+ "type": "array",
927
+ "minItems": 1,
928
+ "maxItems": 64,
929
+ "items": {
930
+ "type": "string",
931
+ "minLength": 1,
932
+ "maxLength": 1024
933
+ }
934
+ }
935
+ }
936
+ },
937
+ "service": {
938
+ "$ref": "https://contracts.tonbo.dev/agents/project-service-v1.schema.json"
939
+ }
940
+ }
941
+ };
942
+ var revisionSchema = {
943
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
944
+ "$id": "https://contracts.tonbo.dev/agents/managed-revision-v1.schema.json",
945
+ "title": "Managed Project revision v1",
946
+ "type": "object",
947
+ "additionalProperties": false,
948
+ "required": [
949
+ "version",
950
+ "agent",
951
+ "source",
952
+ "inference"
953
+ ],
954
+ "properties": {
955
+ "version": {
956
+ "const": 1
957
+ },
958
+ "agent": {
959
+ "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
960
+ },
961
+ "source": {
962
+ "type": "object",
963
+ "additionalProperties": false,
964
+ "required": [
965
+ "format",
966
+ "sha256",
967
+ "size_bytes"
968
+ ],
969
+ "properties": {
970
+ "format": {
971
+ "const": "tar-v1"
972
+ },
973
+ "sha256": {
974
+ "type": "string",
975
+ "pattern": "^[0-9a-f]{64}$"
976
+ },
977
+ "size_bytes": {
978
+ "type": "integer",
979
+ "minimum": 1,
980
+ "maximum": 67108864
981
+ }
982
+ }
983
+ },
984
+ "inference": {
985
+ "type": "object",
986
+ "additionalProperties": false,
987
+ "required": [
988
+ "model"
989
+ ],
990
+ "properties": {
991
+ "model": {
992
+ "type": "string",
993
+ "minLength": 1,
994
+ "maxLength": 160
995
+ }
996
+ }
997
+ },
998
+ "service": {
999
+ "$ref": "https://contracts.tonbo.dev/agents/project-service-v1.schema.json"
1000
+ }
1001
+ }
1002
+ };
1003
+ var sourceBundleContract = {
1004
+ "version": 1,
1005
+ "format": "tar-v1",
1006
+ "bucket": "agent-source-bundles",
1007
+ "content_type": "application/vnd.tonbo.source+tar",
1008
+ "max_bytes": 67108864
1009
+ };
1010
+ var piSessionContract = {
1011
+ "version": 1,
1012
+ "adapter": "pi-jsonl-v3",
1013
+ "format_version": 3,
1014
+ "durable_completion_timeout_seconds": 30,
1015
+ "session_directory": "/sessions",
1016
+ "path_template": "/sessions/{session_id}.jsonl",
1017
+ "preflight_command": [
1018
+ "/usr/local/bin/artifacts",
1019
+ "runtime",
1020
+ "session-preflight"
1021
+ ]
1022
+ };
1023
+
1024
+ // src/contracts.ts
1025
+ var ajv = new Ajv2020({ allErrors: true, useDefaults: true });
1026
+ ajv.addKeyword({ keyword: "x-tonbo-profiles" });
1027
+ ajv.addSchema(kubernetesProfilesSchema);
1028
+ ajv.addSchema(piAgentSchema);
1029
+ ajv.addSchema(projectServiceSchema);
1030
+ var validateDeclaration = ajv.compile(declarationSchema);
1031
+ var validateRevision = ajv.compile(revisionSchema);
1032
+ function validationMessage(label, errors) {
1033
+ const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
1034
+ return `${label} is invalid${detail ? `: ${detail}` : "."}`;
1035
+ }
1036
+ function parseDeclaration(value) {
1037
+ const candidate = structuredClone(value);
1038
+ if (!validateDeclaration(candidate)) {
1039
+ throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
1040
+ }
1041
+ return candidate;
1042
+ }
1043
+ function assertManagedRevision(value) {
1044
+ if (!validateRevision(value)) {
1045
+ throw new Error(validationMessage("Managed revision", validateRevision.errors));
1046
+ }
1047
+ }
1048
+
1049
+ // src/declaration.ts
1050
+ var DECLARATION_FILENAME = ".tonbo";
1051
+ var DEFAULT_INFERENCE_MODEL = "claude-sonnet-4-5";
1052
+ function createDeclaration(model = DEFAULT_INFERENCE_MODEL, driver = { kind: "native" }, buildCommand) {
1053
+ return parseDeclaration({
1054
+ version: 1,
1055
+ agent: { runtime: "pi", driver },
1056
+ inference: { model: model.trim() },
1057
+ ...buildCommand ? { build: { command: buildCommand } } : {}
1058
+ });
1059
+ }
1060
+ function renderDeclaration(declaration) {
1061
+ return `# Tonbo Project Agent configuration.
1062
+ ${stringify(declaration)}`;
1063
+ }
1064
+ async function declarationExists(root) {
1065
+ const filename = path2.join(root, DECLARATION_FILENAME);
1066
+ try {
1067
+ const metadata = await lstat2(filename);
1068
+ if (metadata.isSymbolicLink() || !metadata.isFile())
1069
+ throw new Error(`${filename} must be a regular file.`);
1070
+ return true;
1071
+ } catch (error) {
1072
+ if (error.code === "ENOENT") return false;
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ async function saveDeclaration(root, declaration, overwrite) {
1077
+ const filename = path2.join(root, DECLARATION_FILENAME);
1078
+ const exists = await declarationExists(root);
1079
+ if (exists && !overwrite)
1080
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1081
+ const contents = renderDeclaration(declaration);
1082
+ if (!overwrite) {
1083
+ const handle2 = await open(filename, "wx", 420).catch((error) => {
1084
+ if (error.code === "EEXIST")
1085
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1086
+ throw error;
1087
+ });
1088
+ try {
1089
+ await handle2.writeFile(contents, "utf8");
1090
+ await handle2.sync();
1091
+ } finally {
1092
+ await handle2.close();
1093
+ }
1094
+ return;
1095
+ }
1096
+ const temporary = path2.join(root, `.${DECLARATION_FILENAME}.${process.pid}.${randomUUID2()}.tmp`);
1097
+ let handle;
1098
+ try {
1099
+ handle = await open(temporary, "wx", 420);
1100
+ await handle.writeFile(contents, "utf8");
1101
+ await handle.sync();
1102
+ await handle.close();
1103
+ handle = void 0;
1104
+ await rename(temporary, filename);
1105
+ } catch (error) {
1106
+ await handle?.close().catch(() => void 0);
1107
+ await rm(temporary, { force: true }).catch(() => void 0);
1108
+ throw error;
1109
+ }
1110
+ }
1111
+ async function loadDeclaration(declarationRoot) {
1112
+ const filename = path2.join(declarationRoot, DECLARATION_FILENAME);
1113
+ let parsed;
1114
+ try {
1115
+ parsed = parse(await readFile2(filename, "utf8"));
1116
+ } catch (error) {
1117
+ if (error.code === "ENOENT")
1118
+ throw new Error(`No ${DECLARATION_FILENAME} declaration found at ${filename}.`);
1119
+ throw new Error(`Could not read ${filename} as TOML.`, { cause: error });
1120
+ }
1121
+ return parseDeclaration(parsed);
1122
+ }
1123
+ function buildRevision(declaration, source) {
1124
+ const spec = {
1125
+ version: 1,
1126
+ agent: declaration.agent,
1127
+ source: {
1128
+ format: source.format,
1129
+ sha256: source.sha256,
1130
+ size_bytes: source.size_bytes
1131
+ },
1132
+ inference: declaration.inference,
1133
+ ...declaration.service ? { service: declaration.service } : {}
1134
+ };
1135
+ assertManagedRevision(spec);
1136
+ return spec;
1137
+ }
1138
+
1139
+ // src/source.ts
1140
+ import { createHash as createHash3 } from "node:crypto";
1141
+ import { lstat as lstat3, readFile as readFile3, readdir } from "node:fs/promises";
1142
+ import path3 from "node:path";
1143
+ import ignore from "ignore";
1144
+ import tar from "tar-stream";
1145
+ var MAX_BUNDLE_BYTES = sourceBundleContract.max_bytes;
1146
+ var SESSION_SOURCE_DIRECTORY = piSessionContract.session_directory.replace(/^\/+|\/+$/g, "");
1147
+ var DEFAULT_IGNORES = [
1148
+ ".git/",
1149
+ "node_modules/",
1150
+ ".DS_Store",
1151
+ ".env",
1152
+ ".env.*",
1153
+ "!.env.example",
1154
+ ".tonbo-cache/",
1155
+ ".tonbo-system/",
1156
+ ".pi/npm/",
1157
+ ".pi/git/"
1158
+ ];
1159
+ var EXACT_NPM_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1160
+ var GIT_COMMIT = /^[0-9a-f]{40}$/i;
1161
+ function packageSource(value, index) {
1162
+ if (typeof value === "string") return value;
1163
+ if (value && typeof value === "object" && "source" in value && typeof value.source === "string")
1164
+ return value.source;
1165
+ throw new Error(`.pi/settings.json packages[${index}] must be a source string or object.`);
1166
+ }
1167
+ async function validatePackageSource(root, source) {
1168
+ if (source.startsWith("npm:")) {
1169
+ const specifier = source.slice(4);
1170
+ const separator = specifier.lastIndexOf("@");
1171
+ if (separator <= 0 || !EXACT_NPM_VERSION.test(specifier.slice(separator + 1))) {
1172
+ throw new Error(
1173
+ `PI package ${source} must pin an exact npm version, for example npm:my-agent@1.2.3.`
1174
+ );
1175
+ }
1176
+ return;
1177
+ }
1178
+ if (source.startsWith("git:")) {
1179
+ const separator = source.lastIndexOf("@");
1180
+ if (separator <= "git:".length || !GIT_COMMIT.test(source.slice(separator + 1))) {
1181
+ throw new Error(`PI package ${source} must pin a full 40-character Git commit.`);
1182
+ }
1183
+ return;
1184
+ }
1185
+ if (source.startsWith("./") || source.startsWith("../")) {
1186
+ const settingsDirectory = path3.join(root, ".pi");
1187
+ const resolved = path3.resolve(settingsDirectory, source);
1188
+ const relative = path3.relative(root, resolved);
1189
+ if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
1190
+ throw new Error(`Local PI package ${source} resolves outside the deployed project.`);
1191
+ }
1192
+ let metadata;
1193
+ try {
1194
+ metadata = await lstat3(resolved);
1195
+ } catch (error) {
1196
+ if (error.code === "ENOENT") {
1197
+ throw new Error(`Local PI package ${source} does not exist.`);
1198
+ }
1199
+ throw error;
1200
+ }
1201
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
1202
+ throw new Error(`Local PI package ${source} must resolve to a real project directory.`);
1203
+ }
1204
+ return;
1205
+ }
1206
+ throw new Error(
1207
+ `PI package ${source} must use an exact npm version, a full Git commit, or a project-local path.`
1208
+ );
1209
+ }
1210
+ async function validatePiPackages(root) {
1211
+ const filename = path3.join(root, ".pi", "settings.json");
1212
+ let settings;
1213
+ try {
1214
+ settings = JSON.parse(await readFile3(filename, "utf8"));
1215
+ } catch (error) {
1216
+ if (error.code === "ENOENT") return;
1217
+ throw new Error(`Could not read ${filename} as JSON.`, { cause: error });
1218
+ }
1219
+ if (!settings || typeof settings !== "object" || !("packages" in settings)) return;
1220
+ const packages = settings.packages;
1221
+ if (!Array.isArray(packages)) throw new Error(`${filename} packages must be an array.`);
1222
+ await Promise.all(
1223
+ packages.map((value, index) => validatePackageSource(root, packageSource(value, index)))
1224
+ );
1225
+ }
1226
+ async function findDeclarationRoot(start) {
1227
+ let candidate = path3.resolve(start);
1228
+ for (; ; ) {
1229
+ try {
1230
+ if ((await lstat3(path3.join(candidate, ".tonbo"))).isFile()) return candidate;
1231
+ } catch (error) {
1232
+ if (error.code !== "ENOENT") throw error;
1233
+ }
1234
+ const parent = path3.dirname(candidate);
1235
+ if (parent === candidate) {
1236
+ throw new Error(`No .tonbo declaration found above ${path3.resolve(start)}.`);
1237
+ }
1238
+ candidate = parent;
1239
+ }
1240
+ }
1241
+ async function sourceIgnore(root) {
1242
+ const matcher = ignore().add(DEFAULT_IGNORES);
1243
+ try {
1244
+ matcher.add(await readFile3(path3.join(root, ".tonboignore"), "utf8"));
1245
+ } catch (error) {
1246
+ if (error.code !== "ENOENT") throw error;
1247
+ }
1248
+ return matcher;
1249
+ }
1250
+ async function collectFiles(root) {
1251
+ const matcher = await sourceIgnore(root);
1252
+ const files = [];
1253
+ async function visit(directory, relativeDirectory) {
1254
+ const entries = await readdir(directory, { withFileTypes: true });
1255
+ entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
1256
+ for (const entry of entries) {
1257
+ const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
1258
+ const ignored = matcher.ignores(relative + (entry.isDirectory() ? "/" : ""));
1259
+ if (ignored) continue;
1260
+ if (relative === SESSION_SOURCE_DIRECTORY || relative.startsWith(`${SESSION_SOURCE_DIRECTORY}/`)) {
1261
+ throw new Error(
1262
+ `Source path ${SESSION_SOURCE_DIRECTORY}/ is reserved for durable PI history. Rename it or exclude it with .tonboignore.`
1263
+ );
1264
+ }
1265
+ const absolute = path3.join(directory, entry.name);
1266
+ if (entry.isDirectory()) {
1267
+ await visit(absolute, relative);
1268
+ continue;
1269
+ }
1270
+ const metadata = await lstat3(absolute);
1271
+ if (!metadata.isFile()) {
1272
+ throw new Error(
1273
+ `Source path ${relative} is not a regular file. V1 does not follow symlinks or special files.`
1274
+ );
1275
+ }
1276
+ files.push({
1277
+ absolute,
1278
+ mode: metadata.mode & 73 ? 493 : 420,
1279
+ relative,
1280
+ size: metadata.size
1281
+ });
1282
+ }
1283
+ }
1284
+ await visit(root, "");
1285
+ if (!files.some((file) => file.relative === ".tonbo")) {
1286
+ throw new Error("The source bundle must contain .tonbo.");
1287
+ }
1288
+ return files;
1289
+ }
1290
+ function addEntry(pack, file, contents) {
1291
+ return new Promise((resolve, reject) => {
1292
+ pack.entry(
1293
+ {
1294
+ gid: 0,
1295
+ mode: file.mode,
1296
+ mtime: /* @__PURE__ */ new Date(0),
1297
+ name: file.relative,
1298
+ size: contents.length,
1299
+ type: "file",
1300
+ uid: 0
1301
+ },
1302
+ contents,
1303
+ (error) => error ? reject(error) : resolve()
1304
+ );
1305
+ });
1306
+ }
1307
+ async function buildSourceBundle(root) {
1308
+ const resolvedRoot = path3.resolve(root);
1309
+ await validatePiPackages(resolvedRoot);
1310
+ const files = await collectFiles(resolvedRoot);
1311
+ const payloadBytes = files.reduce((total, file) => total + file.size, 0);
1312
+ if (payloadBytes > MAX_BUNDLE_BYTES) {
1313
+ throw new Error(`Source files exceed ${MAX_BUNDLE_BYTES} bytes after ignore rules.`);
1314
+ }
1315
+ const pack = tar.pack();
1316
+ const chunks = [];
1317
+ let size = 0;
1318
+ pack.on("data", (chunk) => {
1319
+ size += chunk.length;
1320
+ if (size > MAX_BUNDLE_BYTES) {
1321
+ pack.destroy(
1322
+ new Error(`Source bundle exceeds ${MAX_BUNDLE_BYTES} bytes after ignore rules.`)
1323
+ );
1324
+ return;
1325
+ }
1326
+ chunks.push(chunk);
1327
+ });
1328
+ const completed = new Promise((resolve, reject) => {
1329
+ pack.on("end", resolve);
1330
+ pack.on("error", reject);
1331
+ });
1332
+ for (const file of files) {
1333
+ await addEntry(pack, file, await readFile3(file.absolute));
1334
+ }
1335
+ pack.finalize();
1336
+ await completed;
1337
+ const bytes = Buffer.concat(chunks);
1338
+ return {
1339
+ bytes,
1340
+ format: sourceBundleContract.format,
1341
+ root: resolvedRoot,
1342
+ sha256: createHash3("sha256").update(bytes).digest("hex"),
1343
+ size_bytes: bytes.length
1344
+ };
1345
+ }
1346
+
1347
+ // src/ssh-key.ts
1348
+ import { createHash as createHash4 } from "node:crypto";
1349
+ import { readFile as readFile4 } from "node:fs/promises";
1350
+ import { homedir } from "node:os";
1351
+ import { basename, join } from "node:path";
1352
+ var DEFAULT_PUBLIC_KEYS = ["id_ed25519.pub", "id_ecdsa.pub", "id_rsa.pub"];
1353
+ var ALGORITHMS = /* @__PURE__ */ new Set(["ssh-ed25519", "ecdsa-sha2-nistp256", "ssh-rsa"]);
1354
+ function parseOpenSshPublicKey(value, label) {
1355
+ const fields = value.trim().split(/\s+/);
1356
+ if (fields.length < 2 || !ALGORITHMS.has(fields[0]))
1357
+ throw new Error("SSH public key must be Ed25519, ECDSA P-256, or RSA.");
1358
+ const blob = Buffer.from(fields[1], "base64");
1359
+ if (blob.length < 32 || blob.length > 16384 || blob.toString("base64") !== fields[1])
1360
+ throw new Error("SSH public key is not canonical base64.");
1361
+ return {
1362
+ algorithm: fields[0],
1363
+ fingerprint: `SHA256:${createHash4("sha256").update(blob).digest("base64").replace(/=$/, "")}`,
1364
+ keyBase64: fields[1],
1365
+ label
1366
+ };
1367
+ }
1368
+ async function readSshPublicKey(path7) {
1369
+ return parseOpenSshPublicKey(await readFile4(path7, "utf8"), basename(path7, ".pub"));
1370
+ }
1371
+ async function readDefaultSshPublicKeys(sshDirectory = join(homedir(), ".ssh")) {
1372
+ const keys = [];
1373
+ for (const name of DEFAULT_PUBLIC_KEYS) {
1374
+ const key = await readSshPublicKey(join(sshDirectory, name)).catch(
1375
+ (error) => {
1376
+ if (error.code === "ENOENT") return null;
1377
+ throw error;
1378
+ }
1379
+ );
1380
+ if (key) keys.push(key);
1381
+ }
1382
+ return keys;
1383
+ }
1384
+
1385
+ // src/ssh.ts
1386
+ import { spawn as spawn2 } from "node:child_process";
1387
+ var PROJECT_SSH_HOST = "tonbo.sh";
1388
+ function projectSshDestination(projectSlug) {
1389
+ return `${projectSlug}@${PROJECT_SSH_HOST}`;
1390
+ }
1391
+ async function launchProjectSsh(projectSlug) {
1392
+ return new Promise((resolve, reject) => {
1393
+ const child = spawn2("ssh", [projectSshDestination(projectSlug)], {
1394
+ stdio: "inherit"
1395
+ });
1396
+ child.once("error", reject);
1397
+ child.once("exit", (code, signal) => {
1398
+ if (signal) reject(new Error(`ssh was terminated by ${signal}`));
1399
+ else resolve(code ?? 1);
1400
+ });
1401
+ });
1402
+ }
1403
+
1404
+ // src/commands.ts
1405
+ async function resolveProject(deps, selector) {
1406
+ const oauthToken = await deps.auth.accessToken();
1407
+ if (selector) {
1408
+ const normalized = selector.endsWith(".tonbo.sh") ? selector.slice(0, -".tonbo.sh".length) : selector;
1409
+ return {
1410
+ oauthToken,
1411
+ project: selectProject(await deps.api.listProjects(oauthToken), normalized)
1412
+ };
1413
+ }
1414
+ const root = await findDeclarationRoot(deps.cwd());
1415
+ const binding = await deps.config.getBinding(root);
1416
+ if (!binding) throw new Error("No Project selected. Run `tonbo project use <project>` first.");
1417
+ return {
1418
+ oauthToken,
1419
+ project: {
1420
+ id: binding.projectId,
1421
+ slug: binding.projectSlug,
1422
+ status: "active"
1423
+ }
1424
+ };
1425
+ }
1426
+ function selectProject(projects, selector) {
1427
+ const matches = projects.filter(
1428
+ (project) => project.id === selector || project.slug === selector
1429
+ );
1430
+ if (matches.length === 0) throw new Error(`Project ${selector} was not found in your account.`);
1431
+ if (matches.length > 1) throw new Error(`Project slug ${selector} is ambiguous; use its ID.`);
1432
+ if (matches[0].status !== "active") throw new Error(`Project ${selector} is not active.`);
1433
+ return matches[0];
1434
+ }
1435
+ async function initCommand(deps, options) {
1436
+ const root = deps.cwd();
1437
+ const exists = await declarationExists(root);
1438
+ let overwrite = options.force === true;
1439
+ if (exists && !overwrite) {
1440
+ if (!deps.interactive())
1441
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1442
+ const answer = (await deps.prompt(`Replace existing ${DECLARATION_FILENAME}? [y/N] `)).trim().toLowerCase();
1443
+ if (answer !== "y" && answer !== "yes") {
1444
+ deps.output({ message: `Kept existing ${DECLARATION_FILENAME}.` });
1445
+ return;
1446
+ }
1447
+ overwrite = true;
1448
+ }
1449
+ const inspection = await deps.inspectSource(root);
1450
+ let harness = options.harness;
1451
+ if (harness !== void 0 && harness !== "pi") {
1452
+ throw new Error("--harness must name a supported Harness: pi.");
1453
+ }
1454
+ let driver = options.driver;
1455
+ if (driver !== void 0 && driver !== "native" && driver !== "command") {
1456
+ throw new Error("--driver must be native or command.");
1457
+ }
1458
+ if (!harness && !driver && deps.interactive() && inspection.harness.state === "selection_required") {
1459
+ const target = await deps.select("How should Tonbo run this project?", piTargetChoices(false));
1460
+ harness = "pi";
1461
+ driver = target;
1462
+ }
1463
+ if (!harness) {
1464
+ if (inspection.harness.state === "identified") harness = inspection.harness.id;
1465
+ else if (!deps.interactive()) {
1466
+ throw new Error("No Harness-specific configuration found. Pass --harness pi.");
1467
+ } else {
1468
+ harness = await deps.select("Which supported Harness should Tonbo use?", [
1469
+ {
1470
+ description: "Run this project with the pi command.",
1471
+ name: "PI (`pi`)",
1472
+ value: "pi"
1473
+ }
1474
+ ]);
1475
+ }
1476
+ }
1477
+ if (harness !== "pi") throw new Error(`Harness ${harness} is not supported.`);
1478
+ if (!driver) {
1479
+ if (!deps.interactive()) {
1480
+ throw new Error("PI execution mode is required. Pass --driver native or --driver command.");
1481
+ }
1482
+ if (inspection.pi.settingsFound) {
1483
+ 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"}.`;
1484
+ deps.output({ message: packageSummary });
1485
+ }
1486
+ driver = await deps.select(
1487
+ "How should Tonbo start PI?",
1488
+ piTargetChoices(inspection.pi.settingsFound)
1489
+ );
1490
+ }
1491
+ if (driver === "native" && (options.agentEntry || options.buildCommand)) {
1492
+ throw new Error("--agent-entry and --build-command require --driver command.");
1493
+ }
1494
+ let entry = options.agentEntry?.trim();
1495
+ if (driver === "command" && !entry && deps.interactive()) {
1496
+ entry = (await deps.prompt("PI SDK entry file [dist/agent.mjs]: ")).trim();
1497
+ }
1498
+ entry ||= "dist/agent.mjs";
1499
+ const model = options.model?.trim() || DEFAULT_INFERENCE_MODEL;
1500
+ const buildCommand = driver === "command" ? options.buildCommand ?? ["npm", "run", "build"] : void 0;
1501
+ const declaration = createDeclaration(
1502
+ model,
1503
+ driver === "native" ? { kind: "native" } : { kind: "command", protocol: "pi-rpc-v1", command: ["node", entry] },
1504
+ buildCommand
1505
+ );
1506
+ await saveDeclaration(root, declaration, overwrite);
1507
+ const driverSummary = driver === "command" ? `PI SDK app
1508
+ Build: ${buildCommand?.join(" ")}
1509
+ Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
1510
+ deps.output({
1511
+ message: `${exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
1512
+ Harness: PI
1513
+ Mode: ${driverSummary}
1514
+ Next: tonbo project create <slug>`,
1515
+ declaration,
1516
+ path: path4.join(root, DECLARATION_FILENAME)
1517
+ });
1518
+ }
1519
+ function piTargetChoices(settingsFound) {
1520
+ const choices = {
1521
+ command: {
1522
+ description: "Run a custom Node.js entrypoint built on the PI SDK.",
1523
+ name: "PI SDK app",
1524
+ value: "command"
1525
+ },
1526
+ native: {
1527
+ 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.",
1528
+ name: "PI CLI (`pi`)",
1529
+ value: "native"
1530
+ }
1531
+ };
1532
+ return supportedExecutionTargets.filter((target) => target.harness === "pi").map((target) => choices[target.driver]);
1533
+ }
1534
+ async function loginCommand(deps) {
1535
+ try {
1536
+ const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
1537
+ deps.progress.start("Preparing SSH access");
1538
+ const keys = await deps.defaultSshPublicKeys();
1539
+ await Promise.all(keys.map((key) => deps.api.registerSshKey(tokens.access_token, key)));
1540
+ deps.progress.succeed("SSH access ready");
1541
+ deps.output({ message: "Logged in to Tonbo." });
1542
+ } catch (error) {
1543
+ deps.progress.fail();
1544
+ throw error;
1545
+ }
1546
+ }
1547
+ var LOGIN_PROGRESS_MESSAGES = {
1548
+ "account-config": {
1549
+ started: "Connecting to Tonbo",
1550
+ completed: "Connected to Tonbo"
1551
+ },
1552
+ "callback-server": {
1553
+ started: "Starting local browser callback",
1554
+ completed: "Local browser callback ready"
1555
+ },
1556
+ browser: {
1557
+ started: "Opening browser",
1558
+ completed: "Browser opened"
1559
+ },
1560
+ "browser-authorization": {
1561
+ started: "Waiting for browser authorization",
1562
+ completed: "Browser authorization received"
1563
+ },
1564
+ "callback-close": {
1565
+ started: "Closing local browser callback",
1566
+ completed: "Local browser callback closed"
1567
+ },
1568
+ "token-exchange": {
1569
+ started: "Exchanging authorization code",
1570
+ completed: "Authorization code exchanged"
1571
+ },
1572
+ "credential-store": {
1573
+ started: "Saving login session",
1574
+ completed: "Login session saved"
1575
+ }
1576
+ };
1577
+ function reportLoginProgress(progress, event) {
1578
+ const messages = LOGIN_PROGRESS_MESSAGES[event.step];
1579
+ if (event.status === "started") progress.start(messages.started);
1580
+ else progress.succeed(messages.completed);
1581
+ }
1582
+ async function sshKeyAddCommand(deps, path7) {
1583
+ const key = await readSshPublicKey(path7);
1584
+ const oauthToken = await deps.auth.accessToken();
1585
+ await deps.api.registerSshKey(oauthToken, key);
1586
+ deps.output({ message: `Registered SSH key ${key.fingerprint}.`, key });
1587
+ }
1588
+ async function sshKeyRemoveCommand(deps, fingerprint) {
1589
+ const oauthToken = await deps.auth.accessToken();
1590
+ const key = await deps.api.revokeSshKey(oauthToken, fingerprint);
1591
+ deps.output({ message: `Revoked SSH key ${key.fingerprint}.`, key });
1592
+ }
1593
+ async function projectUseCommand(deps, selector) {
1594
+ const root = await findDeclarationRoot(deps.cwd());
1595
+ const oauthToken = await deps.auth.accessToken();
1596
+ const project = selectProject(await deps.api.listProjects(oauthToken), selector);
1597
+ await deps.config.setBinding(root, {
1598
+ projectId: project.id,
1599
+ projectSlug: project.slug
1600
+ });
1601
+ deps.output({ message: `Using Project ${project.slug}.`, project });
1602
+ }
1603
+ async function projectCreateCommand(deps, slug, name) {
1604
+ const root = await findDeclarationRoot(deps.cwd());
1605
+ const oauthToken = await deps.auth.accessToken();
1606
+ const project = await deps.api.createProject(oauthToken, slug, name);
1607
+ await deps.config.setBinding(root, {
1608
+ projectId: project.id,
1609
+ projectSlug: project.slug
1610
+ });
1611
+ deps.output({ message: `Created and selected Project ${project.slug}.`, project });
1612
+ }
1613
+ async function deployCommand(deps, selector) {
1614
+ const root = await findDeclarationRoot(deps.cwd());
1615
+ const declaration = await loadDeclaration(root);
1616
+ if (declaration.build) await runBuildCommand(root, declaration.build.command);
1617
+ const source = await buildSourceBundle(root);
1618
+ const oauthToken = await deps.auth.accessToken();
1619
+ let binding = await deps.config.getBinding(root);
1620
+ if (selector) {
1621
+ const project = selectProject(await deps.api.listProjects(oauthToken), selector);
1622
+ binding = { projectId: project.id, projectSlug: project.slug };
1623
+ }
1624
+ if (!binding) throw new Error("No Project selected. Run `tonbo project use <project>` first.");
1625
+ const managementToken = await deps.api.exchangeManagementToken(oauthToken, binding.projectId);
1626
+ const result = await deps.api.deploy({
1627
+ bundle: source,
1628
+ projectId: binding.projectId,
1629
+ spec: buildRevision(declaration, source),
1630
+ token: managementToken
1631
+ });
1632
+ deps.output({
1633
+ message: `Deployed Project ${binding.projectSlug}.`,
1634
+ project: binding,
1635
+ ...result
1636
+ });
1637
+ }
1638
+ async function runCommand(deps, prompt, options) {
1639
+ const root = await findDeclarationRoot(deps.cwd());
1640
+ const oauthToken = await deps.auth.accessToken();
1641
+ let binding = await deps.config.getBinding(root);
1642
+ if (options.project) {
1643
+ const project = selectProject(await deps.api.listProjects(oauthToken), options.project);
1644
+ binding = { projectId: project.id, projectSlug: project.slug };
1645
+ }
1646
+ if (!binding) throw new Error("No Project selected. Run `tonbo project use <project>` first.");
1647
+ const managementToken = await deps.api.exchangeManagementToken(oauthToken, binding.projectId);
1648
+ const result = await deps.api.run({
1649
+ projectId: binding.projectId,
1650
+ prompt,
1651
+ sessionId: options.session,
1652
+ token: managementToken
1653
+ });
1654
+ deps.output({
1655
+ message: result.turn.assistant_text,
1656
+ project: binding,
1657
+ ...result
1658
+ });
1659
+ }
1660
+ async function sshCommand(deps, selector) {
1661
+ const { project } = await resolveProject(deps, selector);
1662
+ const code = await launchProjectSsh(project.slug);
1663
+ if (code !== 0) throw new Error(`ssh exited with status ${code}`);
1664
+ }
1665
+ async function projectManagement(deps, selector) {
1666
+ const { oauthToken, project } = await resolveProject(deps, selector);
1667
+ return {
1668
+ project,
1669
+ token: await deps.api.exchangeManagementToken(oauthToken, project.id)
1670
+ };
1671
+ }
1672
+ async function secretListCommand(deps, selector) {
1673
+ const { project, token } = await projectManagement(deps, selector);
1674
+ const secrets = await deps.api.listProjectSecrets(project.id, token);
1675
+ deps.output({
1676
+ message: secrets.length ? secrets.map((secret) => secret.name).join("\n") : "No Project secrets are configured.",
1677
+ project,
1678
+ secrets
1679
+ });
1680
+ }
1681
+ async function secretSetCommand(deps, name, options) {
1682
+ const value = await deps.secretValue(name, options.fromEnv);
1683
+ const { project, token } = await projectManagement(deps, options.project);
1684
+ const secret = await deps.api.setProjectSecret(project.id, name, value, token);
1685
+ deps.output({
1686
+ message: `Set Project secret ${secret.name}. Redeploy to replace the active runtime with this value.`,
1687
+ project,
1688
+ secret
1689
+ });
1690
+ }
1691
+ async function secretRemoveCommand(deps, name, selector) {
1692
+ const { project, token } = await projectManagement(deps, selector);
1693
+ await deps.api.deleteProjectSecret(project.id, name, token);
1694
+ deps.output({ message: `Removed Project secret ${name}.`, project, name });
1695
+ }
1696
+
1697
+ // src/config.ts
1698
+ import { mkdir, readFile as readFile5, writeFile } from "node:fs/promises";
1699
+ import os from "node:os";
1700
+ import path5 from "node:path";
1701
+ var FileConfigStore = class {
1702
+ constructor(filename = defaultConfigPath()) {
1703
+ this.filename = filename;
1704
+ }
1705
+ async getBinding(declarationRoot) {
1706
+ const config = await this.read();
1707
+ return config.bindings[path5.resolve(declarationRoot)] ?? null;
1708
+ }
1709
+ async setBinding(declarationRoot, binding) {
1710
+ const config = await this.read();
1711
+ config.bindings[path5.resolve(declarationRoot)] = binding;
1712
+ await mkdir(path5.dirname(this.filename), { recursive: true, mode: 448 });
1713
+ await writeFile(this.filename, `${JSON.stringify(config, null, 2)}
1714
+ `, {
1715
+ mode: 384
1716
+ });
1717
+ }
1718
+ async read() {
1719
+ try {
1720
+ const value = JSON.parse(await readFile5(this.filename, "utf8"));
1721
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
1722
+ const bindings = value.bindings;
1723
+ if (!bindings || typeof bindings !== "object" || Array.isArray(bindings)) throw new Error();
1724
+ return { bindings };
1725
+ } catch (error) {
1726
+ if (error.code === "ENOENT") return { bindings: {} };
1727
+ throw new Error(`Could not read Tonbo config at ${this.filename}.`, {
1728
+ cause: error
1729
+ });
1730
+ }
1731
+ }
1732
+ };
1733
+ function defaultConfigPath() {
1734
+ return path5.join(defaultConfigDirectory(), "config.json");
1735
+ }
1736
+ function defaultConfigDirectory() {
1737
+ const base = process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config");
1738
+ return path5.join(base, "tonbo");
1739
+ }
1740
+
1741
+ // src/credentials.ts
1742
+ import { randomUUID as randomUUID3 } from "node:crypto";
1743
+ import { chmod, lstat as lstat4, mkdir as mkdir2, open as open2, readFile as readFile6, rename as rename2, rm as rm2 } from "node:fs/promises";
1744
+ import path6 from "node:path";
1745
+ var FileCredentialStore = class {
1746
+ constructor(filename = path6.join(defaultConfigDirectory(), "credentials.json")) {
1747
+ this.filename = filename;
1748
+ }
1749
+ async load() {
1750
+ try {
1751
+ await assertPrivateRegularFile(this.filename);
1752
+ const parsed = JSON.parse(await readFile6(this.filename, "utf8"));
1753
+ if (!isOAuthTokenSet(parsed)) throw new Error("invalid token set");
1754
+ return parsed;
1755
+ } catch (error) {
1756
+ if (error.code === "ENOENT") return null;
1757
+ throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
1758
+ }
1759
+ }
1760
+ async save(tokens) {
1761
+ if (!isOAuthTokenSet(tokens)) throw new Error("Refusing to store a malformed Tonbo token set.");
1762
+ const directory = path6.dirname(this.filename);
1763
+ await mkdir2(directory, { mode: 448, recursive: true });
1764
+ await preparePrivateDirectory(directory);
1765
+ await assertExistingDestinationIsSafe(this.filename);
1766
+ const temporary = path6.join(
1767
+ directory,
1768
+ `.${path6.basename(this.filename)}.${process.pid}.${randomUUID3()}.tmp`
1769
+ );
1770
+ let handle = null;
1771
+ try {
1772
+ handle = await open2(temporary, "wx", 384);
1773
+ await handle.writeFile(`${JSON.stringify(tokens, null, 2)}
1774
+ `, "utf8");
1775
+ await handle.sync();
1776
+ await handle.close();
1777
+ handle = null;
1778
+ if (process.platform !== "win32") await chmod(temporary, 384);
1779
+ await rename2(temporary, this.filename);
1780
+ } catch (error) {
1781
+ await handle?.close().catch(() => void 0);
1782
+ await rm2(temporary, { force: true }).catch(() => void 0);
1783
+ throw new Error(`Could not store Tonbo credentials at ${this.filename}.`, { cause: error });
1784
+ }
1785
+ }
1786
+ };
1787
+ function isOAuthTokenSet(value) {
1788
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1789
+ const token = value;
1790
+ 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);
1791
+ }
1792
+ async function preparePrivateDirectory(directory) {
1793
+ const stat = await lstat4(directory);
1794
+ if (stat.isSymbolicLink() || !stat.isDirectory())
1795
+ throw new Error("Tonbo config directory must be a real directory.");
1796
+ if (process.platform !== "win32") await chmod(directory, 448);
1797
+ }
1798
+ async function assertExistingDestinationIsSafe(filename) {
1799
+ try {
1800
+ const stat = await lstat4(filename);
1801
+ if (stat.isSymbolicLink() || !stat.isFile())
1802
+ throw new Error("Tonbo credential path must be a regular file.");
1803
+ } catch (error) {
1804
+ if (error.code !== "ENOENT") throw error;
1805
+ }
1806
+ }
1807
+ async function assertPrivateRegularFile(filename) {
1808
+ const stat = await lstat4(filename);
1809
+ if (stat.isSymbolicLink() || !stat.isFile())
1810
+ throw new Error("Tonbo credential path must be a regular file.");
1811
+ if (process.platform !== "win32" && (stat.mode & 63) !== 0)
1812
+ throw new Error("Tonbo credential file must have mode 0600.");
1813
+ }
1814
+
1815
+ // src/progress.ts
1816
+ var FRAMES = ["|", "/", "-", "\\"];
1817
+ var TerminalProgress = class {
1818
+ constructor(stream, intervalMs = 80) {
1819
+ this.stream = stream;
1820
+ this.intervalMs = intervalMs;
1821
+ }
1822
+ activeMessage;
1823
+ frame = 0;
1824
+ lastWidth = 0;
1825
+ timer;
1826
+ start(message) {
1827
+ if (this.activeMessage) this.succeed();
1828
+ this.activeMessage = message;
1829
+ this.frame = 0;
1830
+ if (!this.stream.isTTY) {
1831
+ this.stream.write(`[..] ${message}
1832
+ `);
1833
+ return;
1834
+ }
1835
+ this.render(`[${FRAMES[this.frame]}] ${message}`);
1836
+ this.timer = setInterval(() => {
1837
+ this.frame = (this.frame + 1) % FRAMES.length;
1838
+ this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
1839
+ }, this.intervalMs);
1840
+ this.timer.unref();
1841
+ }
1842
+ succeed(message = this.activeMessage) {
1843
+ this.finish("ok", message);
1844
+ }
1845
+ fail(message = this.activeMessage) {
1846
+ this.finish("!!", message);
1847
+ }
1848
+ finish(marker, message) {
1849
+ if (this.timer) clearInterval(this.timer);
1850
+ this.timer = void 0;
1851
+ this.activeMessage = void 0;
1852
+ if (!message) return;
1853
+ const line = `[${marker}] ${message}`;
1854
+ if (this.stream.isTTY) {
1855
+ this.render(line);
1856
+ this.stream.write("\n");
1857
+ this.lastWidth = 0;
1858
+ return;
1859
+ }
1860
+ this.stream.write(`${line}
1861
+ `);
1862
+ }
1863
+ render(value) {
1864
+ this.stream.write(`\r${value.padEnd(this.lastWidth)}`);
1865
+ this.lastWidth = value.length;
1866
+ }
1867
+ };
1868
+ var silentProgress = {
1869
+ start: () => {
1870
+ },
1871
+ succeed: () => {
1872
+ },
1873
+ fail: () => {
1874
+ }
1875
+ };
1876
+
1877
+ // src/prompt.ts
1878
+ import select from "@inquirer/select";
1879
+ import { createInterface } from "node:readline/promises";
1880
+ async function terminalPrompt(question) {
1881
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
1882
+ try {
1883
+ return await prompt.question(`? ${question}`);
1884
+ } finally {
1885
+ prompt.close();
1886
+ }
1887
+ }
1888
+ async function terminalSelect(question, choices) {
1889
+ return select(
1890
+ {
1891
+ choices,
1892
+ message: question,
1893
+ pageSize: choices.length
1894
+ },
1895
+ {
1896
+ input: process.stdin,
1897
+ output: process.stderr
1898
+ }
1899
+ );
1900
+ }
1901
+
1902
+ // src/app.ts
1903
+ var packageVersion = JSON.parse(
1904
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8")
1905
+ );
1906
+ function createDependencies(json = false) {
1907
+ const accountOrigin = process.env.TONBO_ACCOUNT_ORIGIN || "https://tonbo.dev";
1908
+ const managementOrigin = process.env.TONBO_API_ORIGIN || "https://api.tonbo.dev";
1909
+ return {
1910
+ api: new TonboApi(fetch, accountOrigin, managementOrigin),
1911
+ auth: new AuthClient(new FileCredentialStore(), fetch, accountOrigin),
1912
+ config: new FileConfigStore(),
1913
+ cwd: () => process.cwd(),
1914
+ defaultSshPublicKeys: readDefaultSshPublicKeys,
1915
+ interactive: () => !json && process.stdin.isTTY === true && process.stderr.isTTY === true,
1916
+ inspectSource: inspectLocalAgentSource,
1917
+ output: (value) => {
1918
+ if (json) console.log(JSON.stringify(value));
1919
+ else console.log(value.message ?? value);
1920
+ },
1921
+ progress: json ? silentProgress : new TerminalProgress(process.stderr),
1922
+ prompt: terminalPrompt,
1923
+ select: terminalSelect,
1924
+ secretValue: async (name, fromEnvironment) => {
1925
+ const environmentName = fromEnvironment ?? name;
1926
+ const value = process.env[environmentName];
1927
+ if (!value)
1928
+ throw new Error(
1929
+ `Environment variable ${environmentName} is empty. Set it before running tonbo secret set.`
1930
+ );
1931
+ return value;
1932
+ }
1933
+ };
1934
+ }
1935
+ function createProgram(dependencies = createDependencies) {
1936
+ const program = new Command().name("tonbo").description("Deploy a persistent Project Agent to Tonbo.").version(packageVersion.version).option("--json", "print machine-readable JSON");
1937
+ 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(
1938
+ async (options) => initCommand(dependencies(program.opts().json), options)
1939
+ );
1940
+ program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(program.opts().json)));
1941
+ const project = program.command("project").description("manage the Project bound to this Agent directory");
1942
+ project.command("create <slug>").description("create and bind a Project to this Agent directory").option("--name <name>", "display name for the Project").action(
1943
+ async (slug, options) => projectCreateCommand(dependencies(program.opts().json), slug, options.name)
1944
+ );
1945
+ const sshKey = program.command("ssh-key").description("manage public keys used by native Project SSH");
1946
+ sshKey.command("add <public-key>").description("register an OpenSSH public key with the current Tonbo account").action(async (path7) => sshKeyAddCommand(dependencies(program.opts().json), path7));
1947
+ sshKey.command("remove <fingerprint>").description("revoke an SSH public key from the current Tonbo account").action(
1948
+ async (fingerprint) => sshKeyRemoveCommand(dependencies(program.opts().json), fingerprint)
1949
+ );
1950
+ project.command("use <project>").description("bind this .tonbo directory to a Project slug or ID").action(
1951
+ async (selector) => projectUseCommand(dependencies(program.opts().json), selector)
1952
+ );
1953
+ program.command("deploy").description("upload this directory as the selected Project deployment").option("--project <project>", "override the bound Project for this deploy").action(
1954
+ async (options) => deployCommand(dependencies(program.opts().json), options.project)
1955
+ );
1956
+ program.command("run <prompt>").description("run one prompt in a durable Project session").option("--project <project>", "override the bound Project for this turn").option("--session <session>", "resume an existing Agent Session UUID").action(
1957
+ async (prompt, options) => runCommand(dependencies(program.opts().json), prompt, options)
1958
+ );
1959
+ program.command("ssh").description("open the selected Project's singleton runtime over SSH").option("--project <project>", "override the bound Project").action(
1960
+ async (options) => sshCommand(dependencies(program.opts().json), options.project)
1961
+ );
1962
+ const secret = program.command("secret").description("manage encrypted environment secrets for the Project service");
1963
+ secret.command("list").option("--project <project>", "override the bound Project").action(
1964
+ async (options) => secretListCommand(dependencies(program.opts().json), options.project)
1965
+ );
1966
+ 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").option("--project <project>", "override the bound Project").action(
1967
+ async (name, options) => secretSetCommand(dependencies(program.opts().json), name, options)
1968
+ );
1969
+ secret.command("remove <name>").option("--project <project>", "override the bound Project").action(
1970
+ async (name, options) => secretRemoveCommand(dependencies(program.opts().json), name, options.project)
1971
+ );
1972
+ return program;
1973
+ }
1974
+
1975
+ // src/main.ts
1976
+ try {
1977
+ await createProgram().parseAsync(process.argv);
1978
+ } catch (error) {
1979
+ console.error(error instanceof Error ? error.message : error);
1980
+ process.exitCode = 1;
1981
+ }