maesttro 0.1.2 → 0.2.1

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,2431 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/credential-store.ts
4
+ import {
5
+ chmodSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ writeFileSync
11
+ } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+
15
+ // src/run-command.ts
16
+ import { spawnSync } from "node:child_process";
17
+ var runCommand = (command, args, input) => {
18
+ const result = spawnSync(command, args, {
19
+ input,
20
+ encoding: "utf8",
21
+ stdio: ["pipe", "pipe", "ignore"]
22
+ });
23
+ return {
24
+ status: result.error ? null : result.status,
25
+ stdout: result.stdout ?? ""
26
+ };
27
+ };
28
+ function commandExists(run, command) {
29
+ return run("sh", ["-c", `command -v ${command}`]).status === 0;
30
+ }
31
+
32
+ // src/credential-store.ts
33
+ var SERVICE = "maesttro";
34
+ var ACCOUNT = "github";
35
+ function encode(token) {
36
+ return Buffer.from(JSON.stringify(token)).toString("base64");
37
+ }
38
+ function decode(encoded) {
39
+ try {
40
+ const token = JSON.parse(Buffer.from(encoded.trim(), "base64").toString("utf8"));
41
+ return typeof token.access_token === "string" ? token : undefined;
42
+ } catch {
43
+ return;
44
+ }
45
+ }
46
+ function keychainStore(run = runCommand) {
47
+ const identity = ["-s", SERVICE, "-a", ACCOUNT];
48
+ return {
49
+ kind: "macOS keychain",
50
+ load: () => {
51
+ const result = run("security", [
52
+ "find-generic-password",
53
+ ...identity,
54
+ "-w"
55
+ ]);
56
+ return result.status === 0 ? decode(result.stdout) : undefined;
57
+ },
58
+ save: (token) => {
59
+ const command = `add-generic-password -U ${identity.join(" ")} -w ${encode(token)}
60
+ `;
61
+ const result = run("security", ["-i"], command);
62
+ if (result.status !== 0)
63
+ throw new Error("Could not write to keychain");
64
+ },
65
+ clear: () => {
66
+ run("security", ["delete-generic-password", ...identity]);
67
+ }
68
+ };
69
+ }
70
+ function secretToolStore(run = runCommand) {
71
+ const attributes = ["service", SERVICE, "account", ACCOUNT];
72
+ return {
73
+ kind: "Secret Service (secret-tool)",
74
+ load: () => {
75
+ const result = run("secret-tool", ["lookup", ...attributes]);
76
+ return result.status === 0 ? decode(result.stdout) : undefined;
77
+ },
78
+ save: (token) => {
79
+ const result = run("secret-tool", ["store", `--label=${SERVICE}`, ...attributes], encode(token));
80
+ if (result.status !== 0) {
81
+ throw new Error("Could not write to the Secret Service");
82
+ }
83
+ },
84
+ clear: () => {
85
+ run("secret-tool", ["clear", ...attributes]);
86
+ }
87
+ };
88
+ }
89
+ function fileStore(path = join(homedir(), ".config", "maesttro", "credentials.json")) {
90
+ return {
91
+ kind: `file (${path})`,
92
+ load: () => {
93
+ try {
94
+ return decode(readFileSync(path, "utf8"));
95
+ } catch {
96
+ return;
97
+ }
98
+ },
99
+ save: (token) => {
100
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
101
+ const temporary = `${path}.tmp`;
102
+ writeFileSync(temporary, encode(token), { mode: 384 });
103
+ chmodSync(temporary, 384);
104
+ renameSync(temporary, path);
105
+ },
106
+ clear: () => {
107
+ rmSync(path, { force: true });
108
+ }
109
+ };
110
+ }
111
+ function selectCredentialStore({
112
+ platform = process.platform,
113
+ env = process.env,
114
+ run = runCommand
115
+ } = {}) {
116
+ if (env.MAESTTRO_CREDENTIAL_STORE === "file")
117
+ return fileStore();
118
+ if (platform === "darwin")
119
+ return keychainStore(run);
120
+ if (platform === "linux" && commandExists(run, "secret-tool")) {
121
+ return secretToolStore(run);
122
+ }
123
+ return fileStore();
124
+ }
125
+
126
+ // src/github-api.ts
127
+ function githubApiUrl(env = process.env) {
128
+ return env.MAESTTRO_GITHUB_API_URL?.trim() || "https://api.github.com";
129
+ }
130
+
131
+ // src/github-app.ts
132
+ var DEFAULT_GITHUB_APP_CLIENT_ID = "Iv23lig1wvF2RtVsAMWo";
133
+ function githubAppClientId(env = process.env) {
134
+ return env.MAESTTRO_GITHUB_CLIENT_ID?.trim() || DEFAULT_GITHUB_APP_CLIENT_ID;
135
+ }
136
+
137
+ // src/device-flow.ts
138
+ var DEVICE_CODE_URL = "https://github.com/login/device/code";
139
+ var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
140
+ var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
141
+ var SLOW_DOWN_PENALTY_SECONDS = 5;
142
+
143
+ class DeviceFlowError extends Error {
144
+ code;
145
+ constructor(code) {
146
+ super(`GitHub login failed: ${code}`);
147
+ this.code = code;
148
+ this.name = "DeviceFlowError";
149
+ }
150
+ }
151
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
152
+ async function postForm(fetchImpl, url, form) {
153
+ const response = await fetchImpl(url, {
154
+ method: "POST",
155
+ headers: {
156
+ Accept: "application/json",
157
+ "Content-Type": "application/x-www-form-urlencoded"
158
+ },
159
+ body: new URLSearchParams(form),
160
+ signal: AbortSignal.timeout(15000)
161
+ });
162
+ const body = await response.json().catch(() => ({}));
163
+ if (!response.ok && typeof body.error !== "string") {
164
+ throw new DeviceFlowError(`http_${response.status}`);
165
+ }
166
+ return body;
167
+ }
168
+ function asGrant(body) {
169
+ if (typeof body.access_token !== "string") {
170
+ throw new DeviceFlowError(typeof body.error === "string" ? body.error : "malformed_response");
171
+ }
172
+ return {
173
+ access_token: body.access_token,
174
+ expires_in: body.expires_in,
175
+ refresh_token: body.refresh_token,
176
+ refresh_token_expires_in: body.refresh_token_expires_in
177
+ };
178
+ }
179
+ async function requestDeviceCode({
180
+ clientId,
181
+ fetch: fetchImpl = fetch
182
+ }) {
183
+ const body = await postForm(fetchImpl, DEVICE_CODE_URL, {
184
+ client_id: clientId
185
+ });
186
+ if (typeof body.device_code !== "string") {
187
+ throw new DeviceFlowError(typeof body.error === "string" ? body.error : "malformed_response");
188
+ }
189
+ return body;
190
+ }
191
+ async function pollForGrant({
192
+ clientId,
193
+ deviceCode,
194
+ fetch: fetchImpl = fetch,
195
+ sleep = defaultSleep,
196
+ now = Date.now
197
+ }) {
198
+ const deadline = now() + deviceCode.expires_in * 1000;
199
+ let intervalSeconds = deviceCode.interval;
200
+ while (now() < deadline) {
201
+ await sleep(intervalSeconds * 1000);
202
+ const body = await postForm(fetchImpl, ACCESS_TOKEN_URL, {
203
+ client_id: clientId,
204
+ device_code: deviceCode.device_code,
205
+ grant_type: DEVICE_GRANT
206
+ });
207
+ if (body.error === "authorization_pending")
208
+ continue;
209
+ if (body.error === "slow_down") {
210
+ intervalSeconds = typeof body.interval === "number" ? body.interval : intervalSeconds + SLOW_DOWN_PENALTY_SECONDS;
211
+ continue;
212
+ }
213
+ return asGrant(body);
214
+ }
215
+ throw new DeviceFlowError("expired_token");
216
+ }
217
+ async function refreshGrant({
218
+ clientId,
219
+ refreshToken,
220
+ fetch: fetchImpl = fetch
221
+ }) {
222
+ const body = await postForm(fetchImpl, ACCESS_TOKEN_URL, {
223
+ client_id: clientId,
224
+ grant_type: "refresh_token",
225
+ refresh_token: refreshToken
226
+ });
227
+ return asGrant(body);
228
+ }
229
+
230
+ // src/user-token.ts
231
+ var EXPIRY_MARGIN_MS = 5 * 60 * 1000;
232
+ function isoAfter(now, seconds) {
233
+ return seconds === undefined ? undefined : new Date(now + seconds * 1000).toISOString();
234
+ }
235
+ function userTokenFromGrant(grant, now) {
236
+ return {
237
+ access_token: grant.access_token,
238
+ expires_at: isoAfter(now, grant.expires_in),
239
+ refresh_token: grant.refresh_token,
240
+ refresh_expires_at: isoAfter(now, grant.refresh_token_expires_in)
241
+ };
242
+ }
243
+ function stillValid(expiresAt, now) {
244
+ return expiresAt === undefined || Date.parse(expiresAt) - EXPIRY_MARGIN_MS > now;
245
+ }
246
+ function isAccessValid(token, now) {
247
+ return stillValid(token.expires_at, now);
248
+ }
249
+ function isRefreshable(token, now) {
250
+ return token.refresh_token !== undefined && stillValid(token.refresh_expires_at, now);
251
+ }
252
+
253
+ // src/github-login.ts
254
+ async function deviceLogin(dependencies) {
255
+ const now = dependencies.now ?? Date.now;
256
+ const deviceCode = await requestDeviceCode(dependencies);
257
+ dependencies.announce(`To give maesttro GitHub access, open ${deviceCode.verification_uri} and enter code ${deviceCode.user_code}`);
258
+ const grant = await pollForGrant({ ...dependencies, deviceCode });
259
+ const token = userTokenFromGrant(grant, now());
260
+ dependencies.store.save(token);
261
+ return token;
262
+ }
263
+ async function refreshUserToken(dependencies) {
264
+ const now = dependencies.now ?? Date.now;
265
+ const grant = await refreshGrant({
266
+ ...dependencies,
267
+ refreshToken: dependencies.token.refresh_token
268
+ });
269
+ const token = userTokenFromGrant(grant, now());
270
+ dependencies.store.save(token);
271
+ return token;
272
+ }
273
+ async function currentUserToken(dependencies) {
274
+ const now = (dependencies.now ?? Date.now)();
275
+ const stored = dependencies.store.load();
276
+ if (stored && isAccessValid(stored, now))
277
+ return stored;
278
+ if (stored && isRefreshable(stored, now)) {
279
+ try {
280
+ return await refreshUserToken({ ...dependencies, token: stored });
281
+ } catch (error) {
282
+ if (!(error instanceof DeviceFlowError))
283
+ throw error;
284
+ dependencies.store.clear();
285
+ }
286
+ }
287
+ return dependencies.interactive ? deviceLogin(dependencies) : undefined;
288
+ }
289
+
290
+ // src/account.ts
291
+ async function githubLogin(token, fetchImpl = fetch) {
292
+ const response = await fetchImpl(new URL("/user", githubApiUrl()), {
293
+ headers: {
294
+ Accept: "application/vnd.github+json",
295
+ Authorization: `Bearer ${token}`,
296
+ "User-Agent": "maesttro"
297
+ },
298
+ signal: AbortSignal.timeout(1e4)
299
+ });
300
+ if (!response.ok)
301
+ return;
302
+ const body = await response.json();
303
+ return body.login;
304
+ }
305
+ function loginDependencies() {
306
+ return {
307
+ clientId: githubAppClientId(),
308
+ store: selectCredentialStore(),
309
+ announce: (message) => console.log(message)
310
+ };
311
+ }
312
+ function describeExpiry(iso) {
313
+ return iso ? new Date(iso).toLocaleString() : "never";
314
+ }
315
+ async function login() {
316
+ const dependencies = loginDependencies();
317
+ const token = await deviceLogin(dependencies);
318
+ const name = await githubLogin(token.access_token);
319
+ console.log(`Logged in to GitHub${name ? ` as ${name}` : ""}. Stored in ${dependencies.store.kind}.`);
320
+ }
321
+ async function ensureLogin() {
322
+ const dependencies = loginDependencies();
323
+ const token = await currentUserToken({ ...dependencies, interactive: true });
324
+ const name = token && await githubLogin(token.access_token);
325
+ console.log(name ? `Logged in to GitHub as ${name}. Stored in ${dependencies.store.kind}.` : "GitHub login did not complete. Run `maesttro login` to retry.");
326
+ }
327
+ function logout() {
328
+ const { store } = loginDependencies();
329
+ store.clear();
330
+ console.log(`Logged out. Removed GitHub credentials from ${store.kind}.`);
331
+ }
332
+ async function status() {
333
+ const dependencies = loginDependencies();
334
+ const token = await currentUserToken({ ...dependencies, interactive: false });
335
+ if (!token) {
336
+ console.log("Not logged in to GitHub. Run `maesttro login`.");
337
+ process.exitCode = 1;
338
+ return;
339
+ }
340
+ const name = await githubLogin(token.access_token);
341
+ console.log([
342
+ `GitHub: ${name ? `logged in as ${name}` : "token rejected — run `maesttro login`"}`,
343
+ `Store: ${dependencies.store.kind}`,
344
+ `Access token expires: ${describeExpiry(token.expires_at)}`,
345
+ `Refresh token expires: ${describeExpiry(token.refresh_expires_at)}`
346
+ ].join(`
347
+ `));
348
+ if (!name)
349
+ process.exitCode = 1;
350
+ }
351
+
352
+ // src/configure.ts
353
+ import {
354
+ cpSync as cpSync2,
355
+ existsSync as existsSync2,
356
+ mkdirSync as mkdirSync4,
357
+ readdirSync,
358
+ readFileSync as readFileSync4,
359
+ writeFileSync as writeFileSync4
360
+ } from "node:fs";
361
+ import { homedir as homedir2 } from "node:os";
362
+ import { dirname as dirname5, join as join4 } from "node:path";
363
+
364
+ // src/client-configs.ts
365
+ import {
366
+ chmodSync as chmodSync2,
367
+ cpSync,
368
+ existsSync,
369
+ mkdirSync as mkdirSync3,
370
+ mkdtempSync,
371
+ readFileSync as readFileSync3,
372
+ rmSync as rmSync2,
373
+ writeFileSync as writeFileSync3
374
+ } from "node:fs";
375
+ import { tmpdir } from "node:os";
376
+ import { dirname as dirname3, join as join2 } from "node:path";
377
+
378
+ // src/json-file.ts
379
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
380
+ import { dirname as dirname2 } from "node:path";
381
+ function isObject(value) {
382
+ return typeof value === "object" && value !== null && !Array.isArray(value);
383
+ }
384
+ function mergeJson(existing, curated) {
385
+ const merged = { ...existing };
386
+ for (const [key, value] of Object.entries(curated)) {
387
+ const current = merged[key];
388
+ merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
389
+ }
390
+ return merged;
391
+ }
392
+ function readJson(path) {
393
+ try {
394
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
395
+ return isObject(parsed) ? parsed : {};
396
+ } catch {
397
+ return {};
398
+ }
399
+ }
400
+ function writeJson(path, value) {
401
+ mkdirSync2(dirname2(path), { recursive: true });
402
+ writeFileSync2(path, `${JSON.stringify(value, null, 2)}
403
+ `);
404
+ }
405
+ function skipInsignificant(text, from) {
406
+ let index = from;
407
+ while (index < text.length) {
408
+ if (/\s/.test(text[index] ?? "")) {
409
+ index++;
410
+ } else if (text.startsWith("//", index)) {
411
+ while (index < text.length && text[index] !== `
412
+ `)
413
+ index++;
414
+ } else if (text.startsWith("/*", index)) {
415
+ const end = text.indexOf("*/", index + 2);
416
+ index = end === -1 ? text.length : end + 2;
417
+ } else {
418
+ return index;
419
+ }
420
+ }
421
+ return index;
422
+ }
423
+ function stripJsonc(text) {
424
+ let output = "";
425
+ let index = 0;
426
+ while (index < text.length) {
427
+ const char = text[index];
428
+ if (char === '"') {
429
+ const start = index;
430
+ index++;
431
+ while (index < text.length && text[index] !== '"') {
432
+ index += text[index] === "\\" ? 2 : 1;
433
+ }
434
+ index++;
435
+ output += text.slice(start, index);
436
+ continue;
437
+ }
438
+ const significant = skipInsignificant(text, index);
439
+ if (significant > index) {
440
+ output += " ";
441
+ index = significant;
442
+ continue;
443
+ }
444
+ const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
445
+ if (!trailingComma)
446
+ output += char;
447
+ index++;
448
+ }
449
+ return output;
450
+ }
451
+ function parseJsonc(text) {
452
+ const parsed = JSON.parse(stripJsonc(text));
453
+ if (!isObject(parsed))
454
+ throw new Error("expected a JSON object");
455
+ return parsed;
456
+ }
457
+
458
+ // src/maesttro-endpoint.ts
459
+ var DEFAULT_MAESTTRO_URL = "http://100.64.0.2";
460
+ var ORCHESTRATOR_MODEL = "maesttro";
461
+ function maesttroUrl(env = process.env) {
462
+ return (env.MAESTTRO_URL?.trim() || DEFAULT_MAESTTRO_URL).replace(/\/+$/, "");
463
+ }
464
+
465
+ // src/provider-configs.ts
466
+ var PROVIDER_ID = "maesttro";
467
+ var MAESTTRO_API_KEY = "maesttro";
468
+ var DGX_SPARK_PROVIDER_ID = "dgx-spark";
469
+ var DGX_SPARK_URL = "http://100.64.0.1:8001";
470
+ var INJECTED_OPENCODE_PROVIDERS = [PROVIDER_ID, DGX_SPARK_PROVIDER_ID];
471
+ function without(object, keys) {
472
+ if (!isObject(object))
473
+ return {};
474
+ return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key)));
475
+ }
476
+ function stringList(value) {
477
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : undefined;
478
+ }
479
+ function opencodeHostBase(host) {
480
+ return isObject(host.provider) ? { ...host, provider: without(host.provider, INJECTED_OPENCODE_PROVIDERS) } : host;
481
+ }
482
+ var IMAGE_INPUT = {
483
+ attachment: true,
484
+ modalities: { input: ["text", "image"] }
485
+ };
486
+ function opencodeProviders(url) {
487
+ return {
488
+ [PROVIDER_ID]: {
489
+ npm: "@ai-sdk/openai-compatible",
490
+ name: "Maesttro",
491
+ options: { baseURL: `${url}/v1`, apiKey: MAESTTRO_API_KEY },
492
+ models: {
493
+ [ORCHESTRATOR_MODEL]: { name: "Orchestrate", ...IMAGE_INPUT }
494
+ }
495
+ },
496
+ [DGX_SPARK_PROVIDER_ID]: {
497
+ npm: "@ai-sdk/openai-compatible",
498
+ name: "DGX-Spark",
499
+ options: { baseURL: `${DGX_SPARK_URL}/v1`, apiKey: "" },
500
+ models: {
501
+ "maesttro/lorem": {
502
+ name: "Qwen3.8 27B",
503
+ ...IMAGE_INPUT,
504
+ limit: { context: 131072, output: 32768 }
505
+ }
506
+ }
507
+ }
508
+ };
509
+ }
510
+ function opencodeConfig(base, url) {
511
+ const merged = mergeJson({ $schema: "https://opencode.ai/config.json", ...base }, {
512
+ model: `${PROVIDER_ID}/${ORCHESTRATOR_MODEL}`,
513
+ compaction: { auto: true, keep: { tokens: 24000 }, buffer: 32000 }
514
+ });
515
+ const enabled = stringList(base.enabled_providers);
516
+ const disabled = stringList(base.disabled_providers);
517
+ return {
518
+ ...merged,
519
+ ...enabled ? {
520
+ enabled_providers: [
521
+ ...enabled.filter((id) => !INJECTED_OPENCODE_PROVIDERS.includes(id)),
522
+ ...INJECTED_OPENCODE_PROVIDERS
523
+ ]
524
+ } : {},
525
+ ...disabled ? {
526
+ disabled_providers: disabled.filter((id) => !INJECTED_OPENCODE_PROVIDERS.includes(id))
527
+ } : {},
528
+ provider: {
529
+ ...isObject(base.provider) ? base.provider : {},
530
+ ...opencodeProviders(url)
531
+ }
532
+ };
533
+ }
534
+
535
+ // src/secrets.ts
536
+ var SECRET_KEY = /api[_-]?key|token|secret|passw(or)?d|credential|authorization|bearer|private[_-]?key|cookie/i;
537
+ var ENV_NAME_KEY = /(^|_)env(_var|_key|_vars)?$/i;
538
+ var SECRET_MAPS = new Set(["env", "environment", "headers", "http_headers"]);
539
+ var CREDENTIAL_SHAPES = [
540
+ /(^|[^A-Za-z0-9])sk[-_][A-Za-z0-9_-]{16,}/,
541
+ /(^|[^A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{20,}/,
542
+ /github_pat_[A-Za-z0-9_]{20,}/,
543
+ /glpat-[A-Za-z0-9_-]{16,}/,
544
+ /xox[abposr]-[A-Za-z0-9-]{10,}/,
545
+ /(^|[^A-Za-z0-9])(AKIA|ASIA)[0-9A-Z]{16}/,
546
+ /AIza[0-9A-Za-z_-]{30,}/,
547
+ /\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/i,
548
+ /:\/\/[^/\s:@]+:[^/\s@]+@/,
549
+ /[?&](api[_-]?key|key|token|access_token|secret)=/i,
550
+ /hooks\.slack\.com\/services\//
551
+ ];
552
+ var SECRET_FLAG = /^--?[\w-]*(token|api[_-]?key|secret|passw(or)?d)[\w-]*$/i;
553
+ var SECRET_FLAG_WITH_VALUE = /^--?[\w-]*(token|api[_-]?key|secret|passw(or)?d)[\w-]*=/i;
554
+ var REFERENCE = /^\{(env|file):[^}]+\}$|^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/;
555
+ var HARMLESS_MAP_VALUE = /^(\d+(\.\d+)?|true|false|yes|no|on|off)$/i;
556
+ function isSecretString({
557
+ key,
558
+ value,
559
+ inSecretMap
560
+ }) {
561
+ if (value.trim() === "" || REFERENCE.test(value.trim()))
562
+ return false;
563
+ if (CREDENTIAL_SHAPES.some((shape) => shape.test(value)))
564
+ return true;
565
+ if (inSecretMap)
566
+ return !HARMLESS_MAP_VALUE.test(value.trim());
567
+ return SECRET_KEY.test(key) && !ENV_NAME_KEY.test(key);
568
+ }
569
+ function redactValue(value, key, path, inSecretMap, removed) {
570
+ if (typeof value === "string") {
571
+ if (isSecretString({ key, value, inSecretMap })) {
572
+ removed.push(path);
573
+ return { keep: false, value };
574
+ }
575
+ return { keep: true, value };
576
+ }
577
+ if (Array.isArray(value)) {
578
+ const items = value.flatMap((item, index) => {
579
+ const previous = value[index - 1];
580
+ const flagged = typeof item === "string" && (typeof previous === "string" && SECRET_FLAG.test(previous) || SECRET_FLAG_WITH_VALUE.test(item));
581
+ if (flagged) {
582
+ removed.push(`${path}[${index}]`);
583
+ return [];
584
+ }
585
+ const result = redactValue(item, key, `${path}[${index}]`, inSecretMap, removed);
586
+ return result.keep ? [result.value] : [];
587
+ });
588
+ return { keep: true, value: items };
589
+ }
590
+ if (isObject(value)) {
591
+ return {
592
+ keep: true,
593
+ value: redactObject(value, path, inSecretMap || SECRET_MAPS.has(key), removed)
594
+ };
595
+ }
596
+ return { keep: true, value };
597
+ }
598
+ function redactObject(object, path, inSecretMap, removed) {
599
+ const kept = {};
600
+ for (const [key, value] of Object.entries(object)) {
601
+ const result = redactValue(value, key, path ? `${path}.${key}` : key, inSecretMap, removed);
602
+ const emptied = isObject(value) && Object.keys(value).length > 0 && isObject(result.value) && Object.keys(result.value).length === 0;
603
+ if (result.keep && !emptied)
604
+ kept[key] = result.value;
605
+ }
606
+ return kept;
607
+ }
608
+ function redactSecrets(config) {
609
+ const removed = [];
610
+ return { value: redactObject(config, "", false, removed), removed };
611
+ }
612
+
613
+ // src/client-configs.ts
614
+ var CONTAINER_CLIENT_CONFIGS = "/opt/maesttro-clients";
615
+ var OPENCODE_HOST_CONFIGS = [
616
+ ".config/opencode/opencode.json",
617
+ ".config/opencode/opencode.jsonc"
618
+ ];
619
+ var OPENCODE_CONFIG = ".config/opencode/opencode.json";
620
+ var OPENCODE_TUI_STATE = ".local/state/opencode/kv.json";
621
+ var CURATED_OPENCODE_TUI_STATE = {
622
+ thinking_mode: "show",
623
+ thinking_visibility: true
624
+ };
625
+ function readHostFile({
626
+ home,
627
+ file,
628
+ parse,
629
+ warn
630
+ }) {
631
+ const path = join2(home, file);
632
+ if (!existsSync(path))
633
+ return {};
634
+ try {
635
+ return parse(readFileSync3(path, "utf8"));
636
+ } catch {
637
+ warn(`maesttro: ignoring your ~/${file}, it doesn't parse`);
638
+ return {};
639
+ }
640
+ }
641
+ function readHostClientConfigs({
642
+ home,
643
+ warn = (message) => console.error(message)
644
+ }) {
645
+ return {
646
+ opencode: OPENCODE_HOST_CONFIGS.reduce((config, file) => mergeJson(config, readHostFile({ home, file, parse: parseJsonc, warn })), {})
647
+ };
648
+ }
649
+ function json(value) {
650
+ return `${JSON.stringify(value, null, 2)}
651
+ `;
652
+ }
653
+ function buildClientConfigs({
654
+ host,
655
+ url
656
+ }) {
657
+ const opencode = redactSecrets(opencodeHostBase(host.opencode));
658
+ return {
659
+ files: { [OPENCODE_CONFIG]: json(opencodeConfig(opencode.value, url)) },
660
+ removed: opencode.removed.map((path) => `opencode ${path}`)
661
+ };
662
+ }
663
+ function writeClientConfigDir(files, parent = tmpdir()) {
664
+ const root = mkdtempSync(join2(parent, "maesttro-clients-"));
665
+ chmodSync2(root, 448);
666
+ const dir = join2(root, "home");
667
+ for (const [file, content] of Object.entries(files)) {
668
+ const path = join2(dir, file);
669
+ mkdirSync3(dirname3(path), { recursive: true, mode: 493 });
670
+ writeFileSync3(path, content);
671
+ chmodSync2(path, 420);
672
+ }
673
+ chmodSync2(dir, 493);
674
+ return { dir, remove: () => rmSync2(root, { recursive: true, force: true }) };
675
+ }
676
+ function installClientConfigs({
677
+ home,
678
+ source = CONTAINER_CLIENT_CONFIGS
679
+ }) {
680
+ if (!existsSync(source))
681
+ return false;
682
+ cpSync(source, home, { recursive: true, force: true });
683
+ return true;
684
+ }
685
+ function injectOpencodeTuiState({ home }) {
686
+ const path = join2(home, OPENCODE_TUI_STATE);
687
+ writeJson(path, mergeJson(readJson(path), CURATED_OPENCODE_TUI_STATE));
688
+ return path;
689
+ }
690
+
691
+ // src/clients.ts
692
+ var CLIENTS = {
693
+ claude: {
694
+ label: "Claude Code",
695
+ npmPackage: "@anthropic-ai/claude-code",
696
+ binary: "claude",
697
+ configDir: "/home/agent/.claude"
698
+ },
699
+ opencode: {
700
+ label: "opencode",
701
+ npmPackage: "opencode-ai",
702
+ binary: "opencode",
703
+ configDir: "/home/agent/.config/opencode"
704
+ }
705
+ };
706
+ var DEFAULT_CLIENT = "opencode";
707
+ function isClientKey(value) {
708
+ return Object.hasOwn(CLIENTS, value);
709
+ }
710
+
711
+ // src/host-instructions.ts
712
+ var HOST_STAGING_DIR = "/opt/maesttro-host";
713
+ var STAGED_DIRECTORIES = [
714
+ ".claude/agents",
715
+ ".claude/commands",
716
+ ".claude/skills"
717
+ ];
718
+ var INSTRUCTION_FILES = [
719
+ ".claude/CLAUDE.md",
720
+ ".config/opencode/AGENTS.md"
721
+ ];
722
+ var STAGED_PATHS = [...INSTRUCTION_FILES, ...STAGED_DIRECTORIES];
723
+
724
+ // src/paths.ts
725
+ import { dirname as dirname4, join as join3, resolve } from "node:path";
726
+ import { fileURLToPath } from "node:url";
727
+ var PACKAGE_ROOT = resolve(dirname4(fileURLToPath(import.meta.url)), "..");
728
+ var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
729
+ var IMAGE = "maesttro-runner:latest";
730
+ var VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
731
+
732
+ // src/runner-context.ts
733
+ function runnerContextFromEnv(env) {
734
+ return {
735
+ sessionId: env.MAESTTRO_SESSION_ID,
736
+ base: env.MAESTTRO_BASE,
737
+ repo: env.MAESTTRO_REPO,
738
+ githubAccess: env.MAESTTRO_GITHUB === "on"
739
+ };
740
+ }
741
+ function runnerInstructions(context) {
742
+ const base = context.base ?? "the default branch";
743
+ const github = context.githubAccess ? [
744
+ `- \`gh\` works for ${context.repo ?? "this repo"} only: issues, pull requests, comments. Its token is short-lived and refreshed for you.`,
745
+ "- Merging pull requests and deleting remote branches are not possible from here. A person merges."
746
+ ] : [
747
+ "- GitHub access is unavailable in this session, so `gh` has no token. Local git work still works."
748
+ ];
749
+ return [
750
+ "# Maesttro runner",
751
+ "",
752
+ "You are running inside Maesttro's isolated runner container. These rules describe what the environment allows, so plan around them.",
753
+ "",
754
+ "## Your workspace",
755
+ "",
756
+ `- The current directory is an isolated clone of the repository${context.sessionId ? ` for this session (\`${context.sessionId}\`)` : ""}, detached at \`${base}\`. It is the only checkout you can see or change; your main checkout is not here.`,
757
+ "- Before your first commit, create a branch for the task: `git switch -c <short-descriptive-name>`. Keep one branch per task.",
758
+ "- Work in this directory. Anything outside it is not persisted.",
759
+ "- Commit on a branch you create here. The user pushes it from their machine with `maesttro push`; the runner cannot push, merge, or delete remote branches.",
760
+ "",
761
+ "## Pushing",
762
+ "",
763
+ "- `git push` does not work here: the runner has read-only access to the remote. This is deliberate.",
764
+ '- When a branch is ready, commit everything, then tell the user: "Ready to push: <branch>. Run `maesttro push <branch>` from the repository root."',
765
+ "- After the user confirms it is pushed, `git fetch` and continue. Open the pull request with `gh pr create --head <branch>` if asked to.",
766
+ "",
767
+ "## GitHub",
768
+ "",
769
+ ...github,
770
+ ""
771
+ ].join(`
772
+ `);
773
+ }
774
+
775
+ // src/configure.ts
776
+ var RUNNER_MARKER = "/etc/maesttro/runner";
777
+ function withoutComments(value) {
778
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !key.startsWith("_comment")).map(([key, inner]) => [
779
+ key,
780
+ isObject(inner) ? withoutComments(inner) : inner
781
+ ]));
782
+ }
783
+ function applyProviderConfigs({
784
+ home,
785
+ configsDir = join4(DOCKERFILE_DIR, "configs")
786
+ }) {
787
+ const written = [];
788
+ for (const [key, client] of Object.entries(CLIENTS)) {
789
+ const source = join4(configsDir, key);
790
+ if (!existsSync2(source))
791
+ continue;
792
+ const targetDir = join4(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
793
+ for (const name of readdirSync(source)) {
794
+ if (!name.endsWith(".json"))
795
+ continue;
796
+ const curated = withoutComments(readJson(join4(source, name)));
797
+ if (Object.keys(curated).length === 0)
798
+ continue;
799
+ const target = join4(targetDir, name);
800
+ mkdirSync4(targetDir, { recursive: true });
801
+ const merged = mergeJson(readJson(target), curated);
802
+ writeFileSync4(target, `${JSON.stringify(merged, null, 2)}
803
+ `);
804
+ written.push(target);
805
+ }
806
+ }
807
+ return written;
808
+ }
809
+ function stageHostInstructions({
810
+ home,
811
+ staging = HOST_STAGING_DIR,
812
+ context
813
+ }) {
814
+ for (const directory of STAGED_DIRECTORIES) {
815
+ const source = join4(staging, directory);
816
+ if (!existsSync2(source))
817
+ continue;
818
+ try {
819
+ cpSync2(source, join4(home, directory), { recursive: true, force: true });
820
+ } catch {}
821
+ }
822
+ const instructions = runnerInstructions(context);
823
+ for (const file of INSTRUCTION_FILES) {
824
+ const source = join4(staging, file);
825
+ const hostCopy = existsSync2(source) ? readFileSync4(source, "utf8") : "";
826
+ const target = join4(home, file);
827
+ mkdirSync4(dirname5(target), { recursive: true });
828
+ writeFileSync4(target, hostCopy ? `${hostCopy.trimEnd()}
829
+
830
+ ${instructions}` : instructions);
831
+ }
832
+ }
833
+ function configure({
834
+ home = homedir2(),
835
+ inRunner = existsSync2(RUNNER_MARKER),
836
+ env = process.env,
837
+ staging = HOST_STAGING_DIR,
838
+ clientConfigs
839
+ } = {}) {
840
+ applyProviderConfigs({ home });
841
+ if (!inRunner)
842
+ return;
843
+ installClientConfigs({ home, source: clientConfigs });
844
+ injectOpencodeTuiState({ home });
845
+ stageHostInstructions({
846
+ home,
847
+ staging,
848
+ context: runnerContextFromEnv(env)
849
+ });
850
+ }
851
+
852
+ // src/doctor.ts
853
+ import { execSync as execSync2 } from "node:child_process";
854
+
855
+ // src/broker-client.ts
856
+ var DEFAULT_BROKER_URL = "http://100.64.0.2";
857
+ function brokerUrl(env = process.env) {
858
+ return env.MAESTTRO_BROKER_URL?.trim() || DEFAULT_BROKER_URL;
859
+ }
860
+
861
+ class BrokerError extends Error {
862
+ code;
863
+ status;
864
+ constructor(code, status) {
865
+ super(code);
866
+ this.code = code;
867
+ this.status = status;
868
+ this.name = "BrokerError";
869
+ }
870
+ }
871
+ function isSessionToken(body) {
872
+ const candidate = body;
873
+ return typeof candidate?.token === "string" && typeof candidate.expires_at === "string" && typeof candidate.committer?.name === "string" && typeof candidate.committer?.email === "string";
874
+ }
875
+ async function requestSessionToken({
876
+ broker,
877
+ userToken,
878
+ repo,
879
+ fetch: fetchImpl = fetch
880
+ }) {
881
+ let response;
882
+ try {
883
+ response = await fetchImpl(new URL("/stagehand/token", broker), {
884
+ method: "POST",
885
+ headers: {
886
+ Authorization: `Bearer ${userToken}`,
887
+ "Content-Type": "application/json"
888
+ },
889
+ body: JSON.stringify({ repo }),
890
+ signal: AbortSignal.timeout(1e4)
891
+ });
892
+ } catch {
893
+ throw new BrokerError("broker_unreachable");
894
+ }
895
+ const body = await response.json().catch(() => null);
896
+ if (response.ok && isSessionToken(body))
897
+ return body;
898
+ const code = body?.error;
899
+ throw new BrokerError(typeof code === "string" ? code : "broker_unreachable", response.status);
900
+ }
901
+ async function brokerHealthy(broker, fetchImpl = fetch, timeoutMs = 2000) {
902
+ try {
903
+ const response = await fetchImpl(new URL("/stagehand/health", broker), {
904
+ signal: AbortSignal.timeout(timeoutMs)
905
+ });
906
+ const body = await response.json();
907
+ return response.ok && body?.status === "ok";
908
+ } catch {
909
+ return false;
910
+ }
911
+ }
912
+ async function revokeSessionToken(token, fetchImpl = fetch, apiUrl = githubApiUrl()) {
913
+ try {
914
+ await fetchImpl(new URL("/installation/token", apiUrl), {
915
+ method: "DELETE",
916
+ headers: {
917
+ Accept: "application/vnd.github+json",
918
+ Authorization: `Bearer ${token}`,
919
+ "User-Agent": "maesttro"
920
+ },
921
+ signal: AbortSignal.timeout(5000)
922
+ });
923
+ } catch {}
924
+ }
925
+
926
+ // src/host-identity.ts
927
+ function hostIdentityEnv() {
928
+ if (typeof process.getuid !== "function" || typeof process.getgid !== "function") {
929
+ return {};
930
+ }
931
+ return {
932
+ MAESTTRO_HOST_UID: String(process.getuid()),
933
+ MAESTTRO_HOST_GID: String(process.getgid())
934
+ };
935
+ }
936
+ function hostIdentityFlags() {
937
+ return Object.entries(hostIdentityEnv()).map(([name, value]) => `-e ${name}=${value}`).join(" ");
938
+ }
939
+
940
+ // src/legacy-token.ts
941
+ import { execSync } from "node:child_process";
942
+ var LEGACY_HOSTS = "~/.config/gh/hosts.yml";
943
+ var PURGE_LEGACY_TOKEN = [
944
+ `if [ -e ${LEGACY_HOSTS} ]; then rm -f ${LEGACY_HOSTS}; echo FOUND; fi`,
945
+ "git config --global --remove-section credential.https://github.com 2>/dev/null || true",
946
+ "git config --global --remove-section credential.https://gist.github.com 2>/dev/null || true"
947
+ ].join("; ");
948
+ var DETECT_LEGACY_TOKEN = `if [ -e ${LEGACY_HOSTS} ]; then echo FOUND; else echo CLEAN; fi`;
949
+ function inVolume(command) {
950
+ return execSync(`docker run --rm ${hostIdentityFlags()} -v ${VOLUME}:/home/agent ${IMAGE} "${command}"`).toString();
951
+ }
952
+ function purgeLegacyToken() {
953
+ return inVolume(PURGE_LEGACY_TOKEN).includes("FOUND");
954
+ }
955
+ function hasLegacyToken() {
956
+ return inVolume(DETECT_LEGACY_TOKEN).includes("FOUND");
957
+ }
958
+ var REVOKE_NOTICE = "Removed a long-lived GitHub token that an older maesttro stored in the runner volume. Revoke it on GitHub too: https://github.com/settings/tokens?type=beta";
959
+ function purgeLegacyTokenOnce({
960
+ config,
961
+ saveConfig,
962
+ purge = purgeLegacyToken,
963
+ announce
964
+ }) {
965
+ if (config.legacyTokenPurged)
966
+ return;
967
+ if (purge())
968
+ announce(REVOKE_NOTICE);
969
+ saveConfig({ ...config, legacyTokenPurged: true });
970
+ }
971
+
972
+ // src/repo.ts
973
+ import { appendFileSync, mkdirSync as mkdirSync5, realpathSync } from "node:fs";
974
+ import { join as join5 } from "node:path";
975
+ var MINIMUM_GIT = [2, 48];
976
+ var WORKTREES_DIR = ".worktrees";
977
+
978
+ class LaunchError extends Error {
979
+ constructor(message) {
980
+ super(message);
981
+ this.name = "LaunchError";
982
+ }
983
+ }
984
+ function git(run, cwd, args) {
985
+ const result = run("git", ["-C", cwd, ...args]);
986
+ return { ok: result.status === 0, out: result.stdout.trim() };
987
+ }
988
+ function parseGitVersion(output) {
989
+ const match = output.match(/git version (\d+)\.(\d+)/);
990
+ return match ? [Number(match[1]), Number(match[2])] : undefined;
991
+ }
992
+ function meetsMinimumGit(version) {
993
+ const [major, minor] = version;
994
+ return major > MINIMUM_GIT[0] || major === MINIMUM_GIT[0] && minor >= MINIMUM_GIT[1];
995
+ }
996
+ function requireGit(run) {
997
+ const version = parseGitVersion(run("git", ["--version"]).stdout);
998
+ const minimum = MINIMUM_GIT.join(".");
999
+ if (!version) {
1000
+ throw new LaunchError(`git not found. maesttro needs git ${minimum} or newer.`);
1001
+ }
1002
+ if (!meetsMinimumGit(version)) {
1003
+ throw new LaunchError(`git ${version.join(".")} is too old. maesttro needs git ${minimum} or newer; please upgrade.`);
1004
+ }
1005
+ }
1006
+ function repoRoot(run, cwd) {
1007
+ const top = git(run, cwd, ["rev-parse", "--show-toplevel"]);
1008
+ if (!top.ok) {
1009
+ throw new LaunchError("Run maesttro from the root of a git repository.");
1010
+ }
1011
+ const commonDir = git(run, cwd, [
1012
+ "rev-parse",
1013
+ "--path-format=absolute",
1014
+ "--git-common-dir"
1015
+ ]).out;
1016
+ const root = realpathSync(top.out);
1017
+ if (realpathSync(commonDir) !== join5(root, ".git")) {
1018
+ throw new LaunchError("This is a git worktree. Run maesttro from the main checkout; each session gets its own isolated clone.");
1019
+ }
1020
+ if (realpathSync(cwd) !== root) {
1021
+ throw new LaunchError(`Run maesttro from the repository root: ${root}`);
1022
+ }
1023
+ return root;
1024
+ }
1025
+ function fetchOrigin(run, root) {
1026
+ return git(run, root, ["fetch", "--quiet", "origin"]).ok;
1027
+ }
1028
+ function defaultBranch(run, root) {
1029
+ const head = git(run, root, [
1030
+ "symbolic-ref",
1031
+ "--short",
1032
+ "refs/remotes/origin/HEAD"
1033
+ ]);
1034
+ if (head.ok && head.out)
1035
+ return head.out;
1036
+ for (const candidate of ["origin/main", "origin/master"]) {
1037
+ if (git(run, root, ["rev-parse", "--verify", "--quiet", candidate]).ok) {
1038
+ return candidate;
1039
+ }
1040
+ }
1041
+ throw new LaunchError("Can't find the default branch of origin. Run `git remote set-head origin --auto`.");
1042
+ }
1043
+ function ensureWorktreesDir(run, root) {
1044
+ mkdirSync5(join5(root, WORKTREES_DIR), { recursive: true });
1045
+ if (git(run, root, ["check-ignore", "-q", `${WORKTREES_DIR}/`]).ok)
1046
+ return;
1047
+ const info = join5(root, ".git", "info");
1048
+ mkdirSync5(info, { recursive: true });
1049
+ appendFileSync(join5(info, "exclude"), `
1050
+ ${WORKTREES_DIR}/
1051
+ `);
1052
+ }
1053
+ function snapshotRefs(run, root) {
1054
+ const listed = git(run, root, [
1055
+ "for-each-ref",
1056
+ "--format=%(refname) %(objectname)",
1057
+ "refs/heads",
1058
+ "refs/tags"
1059
+ ]);
1060
+ const refs = {};
1061
+ for (const line of listed.out.split(`
1062
+ `)) {
1063
+ const [name, sha] = line.split(" ");
1064
+ if (name && sha)
1065
+ refs[name] = sha;
1066
+ }
1067
+ return refs;
1068
+ }
1069
+ function revision(run, root, rev) {
1070
+ const parsed = git(run, root, [
1071
+ "rev-parse",
1072
+ "--verify",
1073
+ "--quiet",
1074
+ `${rev}^{commit}`
1075
+ ]);
1076
+ return parsed.ok ? parsed.out : undefined;
1077
+ }
1078
+
1079
+ // src/doctor.ts
1080
+ async function check(label, probe) {
1081
+ try {
1082
+ const detail = await probe();
1083
+ console.log(` [ok] ${label}${detail ? ` — ${detail}` : ""}`);
1084
+ return true;
1085
+ } catch (error) {
1086
+ const reason = error instanceof Error && error.message ? error.message : "";
1087
+ console.log(` [fail] ${label}${reason ? ` — ${reason}` : ""}`);
1088
+ return false;
1089
+ }
1090
+ }
1091
+ function inRunner(command, { withVolume = true } = {}) {
1092
+ const volume = withVolume ? `-v ${VOLUME}:/home/agent ` : "";
1093
+ return execSync2(`docker run --rm ${hostIdentityFlags()} ${volume}${IMAGE} "${command}"`).toString();
1094
+ }
1095
+ async function doctor() {
1096
+ console.log(`== maesttro doctor ==
1097
+ `);
1098
+ const broker = brokerUrl();
1099
+ const checks = [
1100
+ [
1101
+ "Docker daemon reachable",
1102
+ () => {
1103
+ execSync2("docker info", { stdio: "ignore" });
1104
+ }
1105
+ ],
1106
+ [
1107
+ "git on this machine is new enough",
1108
+ () => {
1109
+ requireGit(runCommand);
1110
+ return runCommand("git", ["--version"]).stdout.trim();
1111
+ }
1112
+ ],
1113
+ [
1114
+ "Runner image built",
1115
+ () => `built ${execSync2(`docker image inspect ${IMAGE} --format "{{.Created}}"`).toString().trim()}`
1116
+ ],
1117
+ [
1118
+ "Persistent volume exists",
1119
+ () => {
1120
+ execSync2(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
1121
+ }
1122
+ ],
1123
+ [
1124
+ "No long-lived GitHub token in the volume",
1125
+ () => {
1126
+ if (hasLegacyToken())
1127
+ throw new Error("run `maesttro setup` to remove it");
1128
+ }
1129
+ ],
1130
+ [
1131
+ "Logged in to GitHub",
1132
+ async () => {
1133
+ const token = await currentUserToken({
1134
+ clientId: githubAppClientId(),
1135
+ store: selectCredentialStore(),
1136
+ announce: () => {},
1137
+ interactive: false
1138
+ });
1139
+ if (!token)
1140
+ throw new Error("run `maesttro login`");
1141
+ const name = await githubLogin(token.access_token);
1142
+ if (!name)
1143
+ throw new Error("token rejected, run `maesttro login`");
1144
+ return name;
1145
+ }
1146
+ ],
1147
+ [
1148
+ "Token broker reachable",
1149
+ async () => {
1150
+ if (!await brokerHealthy(broker)) {
1151
+ throw new Error(`${broker} (on the tailnet?)`);
1152
+ }
1153
+ return broker;
1154
+ }
1155
+ ],
1156
+ [
1157
+ "Personal SSH key NOT visible inside runner (isolation intact)",
1158
+ () => {
1159
+ const out = inRunner("cat ~/.ssh/id_rsa 2>&1 || echo MISSING");
1160
+ if (!out.includes("MISSING") && !out.includes("No such file")) {
1161
+ throw new Error("isolation broken");
1162
+ }
1163
+ }
1164
+ ],
1165
+ ...Object.values(CLIENTS).map((client) => [
1166
+ `Client available: ${client.label}`,
1167
+ () => {
1168
+ inRunner(`which ${client.binary}`, { withVolume: false });
1169
+ }
1170
+ ])
1171
+ ];
1172
+ const results = [];
1173
+ for (const [label, probe] of checks)
1174
+ results.push(await check(label, probe));
1175
+ console.log();
1176
+ if (results.every(Boolean)) {
1177
+ console.log("All checks passed.");
1178
+ return;
1179
+ }
1180
+ console.log("Some checks failed — run `maesttro setup` or `maesttro update` to fix, or check the failing item above.");
1181
+ process.exit(1);
1182
+ }
1183
+
1184
+ // src/help.ts
1185
+ var HELP = `maesttro — isolated runner for local coding agents
1186
+
1187
+ Usage:
1188
+ maesttro Start a session from the repo root: new clone, default client (opencode)
1189
+ maesttro <client> Same, with a specific client — see \`maesttro list\`
1190
+ maesttro resume <id> Continue a kept session in its clone
1191
+ maesttro sessions List this repo's sessions (\`sessions remove <id>\` to drop one)
1192
+ maesttro push [branch] Push a branch a session created, after showing what goes out
1193
+ maesttro list Show available clients
1194
+ maesttro setup One-time: build image, create volume, log in to GitHub
1195
+ maesttro login Log in to GitHub (device flow) so sessions get repo access
1196
+ maesttro logout Remove the stored GitHub credentials
1197
+ maesttro status Show GitHub login status
1198
+ maesttro configure Apply maesttro's client configs (also runs inside the runner)
1199
+ maesttro update Update the package and rebuild the runner image with refreshed client configs
1200
+ maesttro doctor Health check — Docker, image, volume, GitHub login, broker, isolation, clients
1201
+ maesttro help Show this message
1202
+ `;
1203
+
1204
+ // src/launch.ts
1205
+ import { execSync as execSync3, spawn } from "node:child_process";
1206
+ import { existsSync as existsSync4, rmSync as rmSync6 } from "node:fs";
1207
+ import { constants, homedir as homedir4 } from "node:os";
1208
+ import { join as join11 } from "node:path";
1209
+
1210
+ // src/backup.ts
1211
+ import { createHash } from "node:crypto";
1212
+ import { mkdirSync as mkdirSync7, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1213
+ import { basename, join as join7 } from "node:path";
1214
+
1215
+ // src/maesttro-home.ts
1216
+ import {
1217
+ appendFileSync as appendFileSync2,
1218
+ mkdirSync as mkdirSync6,
1219
+ readFileSync as readFileSync5,
1220
+ renameSync as renameSync2,
1221
+ writeFileSync as writeFileSync5
1222
+ } from "node:fs";
1223
+ import { homedir as homedir3 } from "node:os";
1224
+ import { join as join6 } from "node:path";
1225
+ function maesttroHome() {
1226
+ return join6(homedir3(), ".config", "maesttro");
1227
+ }
1228
+ function configPath() {
1229
+ return join6(maesttroHome(), "config.json");
1230
+ }
1231
+ function readMaesttroConfig() {
1232
+ try {
1233
+ return JSON.parse(readFileSync5(configPath(), "utf8"));
1234
+ } catch {
1235
+ return {};
1236
+ }
1237
+ }
1238
+ function writeMaesttroConfig(config) {
1239
+ mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
1240
+ const temporary = `${configPath()}.tmp`;
1241
+ writeFileSync5(temporary, `${JSON.stringify(config, null, 2)}
1242
+ `);
1243
+ renameSync2(temporary, configPath());
1244
+ }
1245
+ function fileSessionLog(path = join6(maesttroHome(), "maesttro.log")) {
1246
+ return (event, detail) => {
1247
+ try {
1248
+ mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
1249
+ appendFileSync2(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
1250
+ `);
1251
+ } catch {}
1252
+ };
1253
+ }
1254
+
1255
+ // src/backup.ts
1256
+ var BACKUPS_KEPT = 5;
1257
+ function shortHash(value) {
1258
+ return createHash("sha256").update(value).digest("hex").slice(0, 8);
1259
+ }
1260
+ function backupDir(repoRoot, home = maesttroHome()) {
1261
+ return join7(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
1262
+ }
1263
+ function refsFingerprint(refs) {
1264
+ const sorted = Object.keys(refs).sort().map((ref) => `${ref} ${refs[ref]}`);
1265
+ return shortHash(sorted.join(`
1266
+ `));
1267
+ }
1268
+ function backupRepo({
1269
+ run,
1270
+ repoRoot,
1271
+ refs,
1272
+ sessionId,
1273
+ dir = backupDir(repoRoot),
1274
+ now = new Date,
1275
+ keep = BACKUPS_KEPT
1276
+ }) {
1277
+ if (Object.keys(refs).length === 0)
1278
+ return;
1279
+ mkdirSync7(dir, { recursive: true, mode: 448 });
1280
+ const fingerprint = refsFingerprint(refs);
1281
+ const existing = readdirSync2(dir).filter((name) => name.endsWith(".bundle")).sort();
1282
+ const latest = existing.at(-1);
1283
+ if (latest?.endsWith(`-${fingerprint}.bundle`)) {
1284
+ return { path: join7(dir, latest), reused: true };
1285
+ }
1286
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
1287
+ const path = join7(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
1288
+ if (!git(run, repoRoot, ["bundle", "create", "--quiet", path, "--all"]).ok) {
1289
+ return;
1290
+ }
1291
+ for (const stale of [...existing, basename(path)].sort().slice(0, -keep)) {
1292
+ rmSync3(join7(dir, stale), { force: true });
1293
+ }
1294
+ return { path, reused: false };
1295
+ }
1296
+
1297
+ // src/refresher.ts
1298
+ var REFRESH_LEAD_MS = 10 * 60 * 1000;
1299
+ var WALL_CLOCK_TICK_MS = 60 * 1000;
1300
+ var FIRST_BACKOFF_MS = 5 * 1000;
1301
+ var MAX_BACKOFF_MS = 5 * 60 * 1000;
1302
+ var nodeTimers = {
1303
+ setTimeout: (callback, ms) => setTimeout(callback, ms).unref(),
1304
+ clearTimeout: (handle) => clearTimeout(handle),
1305
+ setInterval: (callback, ms) => setInterval(callback, ms).unref(),
1306
+ clearInterval: (handle) => clearInterval(handle)
1307
+ };
1308
+ function startRefresher({
1309
+ initial,
1310
+ refresh,
1311
+ onRefreshed,
1312
+ onFailed,
1313
+ now = Date.now,
1314
+ timers = nodeTimers
1315
+ }) {
1316
+ let current = initial;
1317
+ let nextAttemptAt = 0;
1318
+ let backoffMs = FIRST_BACKOFF_MS;
1319
+ let inFlight;
1320
+ let stopped = false;
1321
+ let pending;
1322
+ function refreshDueAt(token) {
1323
+ const expiresAt = Date.parse(token.expires_at);
1324
+ const halfRemaining = (expiresAt - now()) / 2;
1325
+ return expiresAt - Math.min(REFRESH_LEAD_MS, Math.max(halfRemaining, 0));
1326
+ }
1327
+ nextAttemptAt = refreshDueAt(initial);
1328
+ function scheduleAt(at) {
1329
+ timers.clearTimeout(pending);
1330
+ pending = timers.setTimeout(check, Math.max(at - now(), 0));
1331
+ }
1332
+ async function attempt() {
1333
+ try {
1334
+ const token = await refresh();
1335
+ if (stopped)
1336
+ return;
1337
+ current = token;
1338
+ backoffMs = FIRST_BACKOFF_MS;
1339
+ nextAttemptAt = refreshDueAt(token);
1340
+ onRefreshed(token);
1341
+ } catch (error) {
1342
+ if (stopped)
1343
+ return;
1344
+ nextAttemptAt = now() + backoffMs;
1345
+ onFailed(error, backoffMs);
1346
+ backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
1347
+ }
1348
+ scheduleAt(nextAttemptAt);
1349
+ }
1350
+ function check() {
1351
+ if (stopped)
1352
+ return Promise.resolve();
1353
+ if (inFlight)
1354
+ return inFlight;
1355
+ if (now() < nextAttemptAt)
1356
+ return Promise.resolve();
1357
+ inFlight = attempt().finally(() => {
1358
+ inFlight = undefined;
1359
+ });
1360
+ return inFlight;
1361
+ }
1362
+ const tick = timers.setInterval(check, WALL_CLOCK_TICK_MS);
1363
+ scheduleAt(nextAttemptAt);
1364
+ return {
1365
+ current: () => current,
1366
+ check,
1367
+ stop: () => {
1368
+ stopped = true;
1369
+ timers.clearTimeout(pending);
1370
+ timers.clearInterval(tick);
1371
+ }
1372
+ };
1373
+ }
1374
+
1375
+ // src/session-dir.ts
1376
+ import {
1377
+ chmodSync as chmodSync3,
1378
+ mkdirSync as mkdirSync8,
1379
+ mkdtempSync as mkdtempSync2,
1380
+ renameSync as renameSync3,
1381
+ rmSync as rmSync4,
1382
+ writeFileSync as writeFileSync6
1383
+ } from "node:fs";
1384
+ import { tmpdir as tmpdir2 } from "node:os";
1385
+ import { join as join8 } from "node:path";
1386
+ var CONTAINER_GH_CONFIG_DIR = "/home/agent/.config/gh-maesttro";
1387
+ function writeAtomically(path, content) {
1388
+ const temporary = `${path}.tmp`;
1389
+ writeFileSync6(temporary, content, { mode: 420 });
1390
+ chmodSync3(temporary, 420);
1391
+ renameSync3(temporary, path);
1392
+ }
1393
+ function hostsYaml(token, user) {
1394
+ return [
1395
+ "github.com:",
1396
+ ` oauth_token: ${JSON.stringify(token)}`,
1397
+ ` user: ${JSON.stringify(user)}`,
1398
+ " git_protocol: https",
1399
+ ""
1400
+ ].join(`
1401
+ `);
1402
+ }
1403
+ function createSessionDir(parent = tmpdir2()) {
1404
+ const root = mkdtempSync2(join8(parent, "maesttro-"));
1405
+ chmodSync3(root, 448);
1406
+ const ghDir = join8(root, "gh");
1407
+ mkdirSync8(ghDir);
1408
+ chmodSync3(ghDir, 493);
1409
+ writeAtomically(join8(ghDir, "config.yml"), `version: "1"
1410
+ `);
1411
+ return {
1412
+ ghDir,
1413
+ writeToken: (token, user) => writeAtomically(join8(ghDir, "hosts.yml"), hostsYaml(token, user)),
1414
+ writeStatus: (status) => writeAtomically(join8(ghDir, "status"), `${status}
1415
+ `),
1416
+ remove: () => rmSync4(root, { recursive: true, force: true })
1417
+ };
1418
+ }
1419
+
1420
+ // src/github-session.ts
1421
+ var UNAVAILABLE = "GitHub access unavailable";
1422
+ var WITHOUT = "running without gh";
1423
+ function refusalNotice(code, repo) {
1424
+ switch (code) {
1425
+ case "broker_unreachable":
1426
+ return `${UNAVAILABLE} (broker unreachable) — ${WITHOUT}`;
1427
+ case "app_not_installed":
1428
+ return `${UNAVAILABLE}: the GitHub App is not installed on ${repo} — ${WITHOUT}`;
1429
+ case "repo_not_visible_to_user":
1430
+ return `${UNAVAILABLE}: your GitHub account cannot see ${repo} — ${WITHOUT}`;
1431
+ case "user_token_invalid":
1432
+ case "not_logged_in":
1433
+ return `${UNAVAILABLE}: GitHub login expired, run \`maesttro login\` — ${WITHOUT}`;
1434
+ default:
1435
+ return `${UNAVAILABLE} (${code}) — ${WITHOUT}`;
1436
+ }
1437
+ }
1438
+ function identityEnv(author, committer) {
1439
+ const env = {};
1440
+ if (author) {
1441
+ env.GIT_AUTHOR_NAME = author.name;
1442
+ env.GIT_AUTHOR_EMAIL = author.email;
1443
+ }
1444
+ const effectiveCommitter = committer ?? author;
1445
+ if (effectiveCommitter) {
1446
+ env.GIT_COMMITTER_NAME = effectiveCommitter.name;
1447
+ env.GIT_COMMITTER_EMAIL = effectiveCommitter.email;
1448
+ }
1449
+ return env;
1450
+ }
1451
+ function withEnvFlags(dockerArgs, env) {
1452
+ return [...dockerArgs, ...Object.keys(env).flatMap((name) => ["-e", name])];
1453
+ }
1454
+ function errorCode(error) {
1455
+ if (error instanceof BrokerError)
1456
+ return error.code;
1457
+ if (error instanceof Error && error.name === "DeviceFlowError") {
1458
+ return "not_logged_in";
1459
+ }
1460
+ return error instanceof Error && error.message === "not_logged_in" ? "not_logged_in" : "unexpected_error";
1461
+ }
1462
+ function withoutGitHub(author, notice) {
1463
+ const env = {
1464
+ GH_CONFIG_DIR: CONTAINER_GH_CONFIG_DIR,
1465
+ ...identityEnv(author, undefined)
1466
+ };
1467
+ return {
1468
+ dockerArgs: withEnvFlags([], env),
1469
+ env,
1470
+ notice,
1471
+ close: async () => {}
1472
+ };
1473
+ }
1474
+ async function openGitHubSession({
1475
+ repo,
1476
+ author,
1477
+ broker,
1478
+ login,
1479
+ log,
1480
+ fetch: fetchImpl = fetch,
1481
+ createDir = createSessionDir
1482
+ }) {
1483
+ if (!repo)
1484
+ return withoutGitHub(author);
1485
+ const mint = async ({ interactive }) => {
1486
+ if (!await brokerHealthy(broker, fetchImpl)) {
1487
+ throw new BrokerError("broker_unreachable");
1488
+ }
1489
+ const userToken = await currentUserToken({ ...login, interactive });
1490
+ if (!userToken)
1491
+ throw new Error("not_logged_in");
1492
+ return requestSessionToken({
1493
+ broker,
1494
+ userToken: userToken.access_token,
1495
+ repo,
1496
+ fetch: fetchImpl
1497
+ });
1498
+ };
1499
+ let initial;
1500
+ try {
1501
+ initial = await mint({ interactive: true });
1502
+ } catch (error) {
1503
+ const code = errorCode(error);
1504
+ log("session.github_unavailable", `${repo} ${code}`);
1505
+ return withoutGitHub(author, refusalNotice(code, repo));
1506
+ }
1507
+ const dir = createDir();
1508
+ const publish = (token) => {
1509
+ dir.writeToken(token.token, token.committer.name);
1510
+ dir.writeStatus(`ok, token expires ${token.expires_at}`);
1511
+ };
1512
+ publish(initial);
1513
+ log("session.token_minted", `${repo} expires ${initial.expires_at}`);
1514
+ const refresher = startRefresher({
1515
+ initial,
1516
+ refresh: () => mint({ interactive: false }),
1517
+ onRefreshed: (token) => {
1518
+ publish(token);
1519
+ log("session.token_refreshed", `${repo} expires ${token.expires_at}`);
1520
+ },
1521
+ onFailed: (error, retryInMs) => {
1522
+ const code = errorCode(error);
1523
+ dir.writeStatus(`refresh failing (${code}), retrying in ${Math.round(retryInMs / 1000)}s`);
1524
+ log("session.refresh_failed", `${repo} ${code}`);
1525
+ }
1526
+ });
1527
+ const env = {
1528
+ GH_CONFIG_DIR: CONTAINER_GH_CONFIG_DIR,
1529
+ ...identityEnv(author, initial.committer)
1530
+ };
1531
+ return {
1532
+ dockerArgs: withEnvFlags(["-v", `${dir.ghDir}:${CONTAINER_GH_CONFIG_DIR}:ro`], env),
1533
+ env,
1534
+ close: async () => {
1535
+ refresher.stop();
1536
+ await revokeSessionToken(refresher.current().token, fetchImpl);
1537
+ dir.remove();
1538
+ log("session.closed", repo);
1539
+ }
1540
+ };
1541
+ }
1542
+
1543
+ // src/prompt.ts
1544
+ import { createInterface } from "node:readline";
1545
+ function ask(question) {
1546
+ const readline = createInterface({
1547
+ input: process.stdin,
1548
+ output: process.stdout
1549
+ });
1550
+ return new Promise((resolve) => readline.question(question, (answer) => {
1551
+ readline.close();
1552
+ resolve(answer.trim());
1553
+ }));
1554
+ }
1555
+
1556
+ // src/session-clone.ts
1557
+ import {
1558
+ copyFileSync,
1559
+ existsSync as existsSync3,
1560
+ lstatSync,
1561
+ mkdirSync as mkdirSync9,
1562
+ readdirSync as readdirSync3,
1563
+ readFileSync as readFileSync6
1564
+ } from "node:fs";
1565
+ import { join as join9 } from "node:path";
1566
+
1567
+ // src/sanitize.ts
1568
+ function stripControl(text) {
1569
+ let out = "";
1570
+ for (const char of text) {
1571
+ const code = char.codePointAt(0) ?? 0;
1572
+ if (code >= 32 && code !== 127)
1573
+ out += char;
1574
+ }
1575
+ return out;
1576
+ }
1577
+ var VALID_BRANCH = /^[A-Za-z0-9](?:[A-Za-z0-9._/-]*[A-Za-z0-9_-])?$/;
1578
+ function isSafeBranchName(name) {
1579
+ if (name.length === 0 || name.length > 255)
1580
+ return false;
1581
+ if (!VALID_BRANCH.test(name))
1582
+ return false;
1583
+ if (name.includes(".."))
1584
+ return false;
1585
+ if (name.includes("//"))
1586
+ return false;
1587
+ if (name.includes("@{"))
1588
+ return false;
1589
+ if (name.endsWith(".lock"))
1590
+ return false;
1591
+ for (const segment of name.split("/")) {
1592
+ if (segment.length === 0 || segment.startsWith(".") || segment.endsWith(".")) {
1593
+ return false;
1594
+ }
1595
+ }
1596
+ return true;
1597
+ }
1598
+
1599
+ // src/session-clone.ts
1600
+ var HARDENING = [
1601
+ "-c",
1602
+ "core.fsmonitor=",
1603
+ "-c",
1604
+ "core.hooksPath=/dev/null",
1605
+ "-c",
1606
+ "core.pager=cat",
1607
+ "-c",
1608
+ "protocol.file.allow=never"
1609
+ ];
1610
+ var SHA = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
1611
+ var OBJECT_SUBDIR = /^[0-9a-f]{2}$/;
1612
+ var HEADS_PREFIX = "refs/heads/";
1613
+ function agentGit(run, dir, args) {
1614
+ return git(run, dir, [...HARDENING, ...args]);
1615
+ }
1616
+ function createSessionClone(run, root, clonePath, baseSha) {
1617
+ const cloned = git(run, root, [
1618
+ "clone",
1619
+ "--quiet",
1620
+ "--shared",
1621
+ "--no-checkout",
1622
+ root,
1623
+ clonePath
1624
+ ]);
1625
+ if (!cloned.ok) {
1626
+ throw new LaunchError(`Could not create the session clone at ${clonePath}`);
1627
+ }
1628
+ const heads = agentGit(run, clonePath, [
1629
+ "for-each-ref",
1630
+ "--format=delete %(refname)",
1631
+ "refs/heads"
1632
+ ]);
1633
+ if (heads.out) {
1634
+ run("git", ["-C", clonePath, "update-ref", "--stdin"], `${heads.out}
1635
+ `);
1636
+ }
1637
+ const detached = agentGit(run, clonePath, ["checkout", "--detach", baseSha]);
1638
+ if (!detached.ok) {
1639
+ throw new LaunchError(`Could not check out ${baseSha} in the session clone`);
1640
+ }
1641
+ }
1642
+ function realDir(path) {
1643
+ try {
1644
+ return lstatSync(path).isDirectory();
1645
+ } catch {
1646
+ return false;
1647
+ }
1648
+ }
1649
+ function dirents(dir) {
1650
+ try {
1651
+ return readdirSync3(dir, { withFileTypes: true });
1652
+ } catch {
1653
+ return [];
1654
+ }
1655
+ }
1656
+ function cloneGitDir(clonePath) {
1657
+ const gitDir = join9(clonePath, ".git");
1658
+ return realDir(gitDir) ? gitDir : undefined;
1659
+ }
1660
+ function readSha(path) {
1661
+ try {
1662
+ if (!lstatSync(path).isFile())
1663
+ return;
1664
+ const text = readFileSync6(path, "utf8").trim();
1665
+ return SHA.test(text) ? text : undefined;
1666
+ } catch {
1667
+ return;
1668
+ }
1669
+ }
1670
+ function packedRefs(gitDir) {
1671
+ const refs = new Map;
1672
+ let text;
1673
+ try {
1674
+ text = readFileSync6(join9(gitDir, "packed-refs"), "utf8");
1675
+ } catch {
1676
+ return refs;
1677
+ }
1678
+ for (const line of text.split(`
1679
+ `)) {
1680
+ if (!line || line.startsWith("#") || line.startsWith("^"))
1681
+ continue;
1682
+ const [sha, ref] = line.split(" ");
1683
+ if (sha && ref && SHA.test(sha))
1684
+ refs.set(ref, sha);
1685
+ }
1686
+ return refs;
1687
+ }
1688
+ function cloneBranches(clonePath) {
1689
+ const gitDir = cloneGitDir(clonePath);
1690
+ if (!gitDir)
1691
+ return [];
1692
+ const names = new Set;
1693
+ const walk = (dir, prefix) => {
1694
+ for (const entry of dirents(dir)) {
1695
+ if (entry.isSymbolicLink())
1696
+ continue;
1697
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
1698
+ if (entry.isDirectory())
1699
+ walk(join9(dir, entry.name), name);
1700
+ else if (entry.isFile())
1701
+ names.add(name);
1702
+ }
1703
+ };
1704
+ walk(join9(gitDir, "refs", "heads"), "");
1705
+ for (const ref of packedRefs(gitDir).keys()) {
1706
+ if (ref.startsWith(HEADS_PREFIX))
1707
+ names.add(ref.slice(HEADS_PREFIX.length));
1708
+ }
1709
+ return [...names].filter(isSafeBranchName).sort();
1710
+ }
1711
+ function branchTip(clonePath, branch) {
1712
+ if (!isSafeBranchName(branch))
1713
+ return;
1714
+ const gitDir = cloneGitDir(clonePath);
1715
+ if (!gitDir)
1716
+ return;
1717
+ return readSha(join9(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
1718
+ }
1719
+ function cloneHead(clonePath) {
1720
+ const gitDir = cloneGitDir(clonePath);
1721
+ if (!gitDir)
1722
+ return;
1723
+ let text;
1724
+ try {
1725
+ if (!lstatSync(join9(gitDir, "HEAD")).isFile())
1726
+ return;
1727
+ text = readFileSync6(join9(gitDir, "HEAD"), "utf8").trim();
1728
+ } catch {
1729
+ return;
1730
+ }
1731
+ if (text.startsWith("ref:")) {
1732
+ const ref = text.slice(4).trim();
1733
+ return ref.startsWith(HEADS_PREFIX) ? { branch: ref.slice(HEADS_PREFIX.length) } : {};
1734
+ }
1735
+ return SHA.test(text) ? { detached: text } : {};
1736
+ }
1737
+ function cloneHasWork(clonePath, baseSha) {
1738
+ if (cloneBranches(clonePath).length > 0)
1739
+ return true;
1740
+ const head = cloneHead(clonePath);
1741
+ return head?.detached !== undefined && head.detached !== baseSha;
1742
+ }
1743
+ function copyObjectFiles(fromDir, intoDir) {
1744
+ let created = false;
1745
+ for (const entry of dirents(fromDir)) {
1746
+ if (!entry.isFile())
1747
+ continue;
1748
+ const dest = join9(intoDir, entry.name);
1749
+ if (existsSync3(dest))
1750
+ continue;
1751
+ if (!created) {
1752
+ mkdirSync9(intoDir, { recursive: true });
1753
+ created = true;
1754
+ }
1755
+ copyFileSync(join9(fromDir, entry.name), dest);
1756
+ }
1757
+ }
1758
+ function importObjects(run, root, clonePath, tip) {
1759
+ const gitDir = cloneGitDir(clonePath);
1760
+ if (!gitDir)
1761
+ return false;
1762
+ const objects = join9(gitDir, "objects");
1763
+ if (!realDir(objects))
1764
+ return false;
1765
+ const into = join9(root, ".git", "objects");
1766
+ for (const entry of dirents(objects)) {
1767
+ if (entry.name === "info" || entry.isSymbolicLink() || !entry.isDirectory()) {
1768
+ continue;
1769
+ }
1770
+ if (!OBJECT_SUBDIR.test(entry.name) && entry.name !== "pack")
1771
+ continue;
1772
+ copyObjectFiles(join9(objects, entry.name), join9(into, entry.name));
1773
+ }
1774
+ return git(run, root, ["cat-file", "-e", `${tip}^{commit}`]).ok;
1775
+ }
1776
+
1777
+ // src/sessions.ts
1778
+ import { randomBytes } from "node:crypto";
1779
+ import {
1780
+ mkdirSync as mkdirSync10,
1781
+ readdirSync as readdirSync4,
1782
+ readFileSync as readFileSync7,
1783
+ renameSync as renameSync4,
1784
+ rmSync as rmSync5,
1785
+ writeFileSync as writeFileSync7
1786
+ } from "node:fs";
1787
+ import { join as join10 } from "node:path";
1788
+ function newSessionId() {
1789
+ return `maesttro-${randomBytes(4).toString("hex")}`;
1790
+ }
1791
+ function sessionsDir(home = maesttroHome()) {
1792
+ return join10(home, "sessions");
1793
+ }
1794
+ function saveSession(record, dir = sessionsDir()) {
1795
+ mkdirSync10(dir, { recursive: true, mode: 448 });
1796
+ const path = join10(dir, `${record.id}.json`);
1797
+ writeFileSync7(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
1798
+ `);
1799
+ renameSync4(`${path}.tmp`, path);
1800
+ }
1801
+ function readSession(id, dir = sessionsDir()) {
1802
+ try {
1803
+ return JSON.parse(readFileSync7(join10(dir, `${id}.json`), "utf8"));
1804
+ } catch {
1805
+ return;
1806
+ }
1807
+ }
1808
+ function deleteSession(id, dir = sessionsDir()) {
1809
+ rmSync5(join10(dir, `${id}.json`), { force: true });
1810
+ }
1811
+ function sessionsForRepo(repoRoot, dir = sessionsDir()) {
1812
+ let names;
1813
+ try {
1814
+ names = readdirSync4(dir).filter((name) => name.endsWith(".json"));
1815
+ } catch {
1816
+ return [];
1817
+ }
1818
+ return names.map((name) => readSession(name.slice(0, -".json".length), dir)).filter((record) => record?.repoRoot === repoRoot).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1819
+ }
1820
+
1821
+ // src/workspace-git.ts
1822
+ var GITHUB_REMOTE = /^(?:https:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/;
1823
+ function githubRepoFromRemote(remote) {
1824
+ const match = remote.trim().match(GITHUB_REMOTE);
1825
+ return match ? `${match[1]}/${match[2]}` : undefined;
1826
+ }
1827
+ function gitOutput(run, workspace, args) {
1828
+ const result = run("git", ["-C", workspace, ...args]);
1829
+ const value = result.stdout.trim();
1830
+ return result.status === 0 && value ? value : undefined;
1831
+ }
1832
+ function workspaceRepo(run, workspace) {
1833
+ const expanded = gitOutput(run, workspace, ["remote", "get-url", "origin"]);
1834
+ const configured = gitOutput(run, workspace, [
1835
+ "config",
1836
+ "--get",
1837
+ "remote.origin.url"
1838
+ ]);
1839
+ return [expanded, configured].map((remote) => remote ? githubRepoFromRemote(remote) : undefined).find(Boolean);
1840
+ }
1841
+ function workspaceAuthor(run, workspace) {
1842
+ const name = gitOutput(run, workspace, ["config", "user.name"]);
1843
+ const email = gitOutput(run, workspace, ["config", "user.email"]);
1844
+ return name && email ? { name, email } : undefined;
1845
+ }
1846
+
1847
+ // src/launch.ts
1848
+ var CONTAINER_REPO = "/workspace";
1849
+ var CONTAINER_PACKAGE = "/opt/maesttro";
1850
+ function resolveCommand(args) {
1851
+ const [first, ...rest] = args;
1852
+ if (first === undefined)
1853
+ return CLIENTS[DEFAULT_CLIENT].binary;
1854
+ if (isClientKey(first))
1855
+ return [CLIENTS[first].binary, ...rest].join(" ");
1856
+ return args.join(" ");
1857
+ }
1858
+ function containerCommand(command) {
1859
+ return `node ${CONTAINER_PACKAGE}/dist/maesttro.js configure || echo "maesttro: in-container setup failed" >&2; ${command}`;
1860
+ }
1861
+ function sessionMounts({
1862
+ repoRoot,
1863
+ sessionDir,
1864
+ packageRoot,
1865
+ hostHome,
1866
+ stagedPaths,
1867
+ clientConfigs,
1868
+ sharedSkills
1869
+ }) {
1870
+ const objects = join11(repoRoot, ".git", "objects");
1871
+ const mounts = [
1872
+ { host: VOLUME, container: "/home/agent" },
1873
+ { host: objects, container: objects, readOnly: true },
1874
+ { host: sessionDir, container: CONTAINER_REPO },
1875
+ { host: packageRoot, container: CONTAINER_PACKAGE, readOnly: true },
1876
+ ...stagedPaths.map((path) => ({
1877
+ host: join11(hostHome, path),
1878
+ container: `${HOST_STAGING_DIR}/${path}`,
1879
+ readOnly: true
1880
+ })),
1881
+ {
1882
+ host: clientConfigs,
1883
+ container: CONTAINER_CLIENT_CONFIGS,
1884
+ readOnly: true
1885
+ }
1886
+ ];
1887
+ if (sharedSkills) {
1888
+ mounts.push({
1889
+ host: sharedSkills,
1890
+ container: "/home/agent/.claude/skills",
1891
+ readOnly: true
1892
+ });
1893
+ }
1894
+ return { mounts, workdir: CONTAINER_REPO };
1895
+ }
1896
+ function dockerRunArgs({
1897
+ command,
1898
+ mounts,
1899
+ workdir,
1900
+ extra = []
1901
+ }) {
1902
+ const volumes = mounts.flatMap(({ host, container, readOnly }) => [
1903
+ "-v",
1904
+ `${host}:${container}${readOnly ? ":ro" : ""}`
1905
+ ]);
1906
+ return [
1907
+ "run",
1908
+ "-it",
1909
+ "--rm",
1910
+ ...volumes,
1911
+ ...extra,
1912
+ "-w",
1913
+ workdir,
1914
+ IMAGE,
1915
+ command
1916
+ ];
1917
+ }
1918
+ function listClients() {
1919
+ const rows = Object.entries(CLIENTS).map(([key, client]) => ` ${key.padEnd(10)} ${client.label} (${client.npmPackage})`);
1920
+ return [
1921
+ `Available clients:
1922
+ `,
1923
+ ...rows,
1924
+ `
1925
+ Default: ${DEFAULT_CLIENT}. Run \`maesttro <client>\` to pick one, or \`maesttro <client> -- <args>\` to pass args through.`
1926
+ ].join(`
1927
+ `);
1928
+ }
1929
+ function ensureReady() {
1930
+ try {
1931
+ execSync3(`docker image inspect ${IMAGE}`, { stdio: "ignore" });
1932
+ execSync3(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
1933
+ } catch {
1934
+ throw new LaunchError("Runner isn't set up yet. Run `maesttro setup` first.");
1935
+ }
1936
+ }
1937
+ function sharedSkillsDir() {
1938
+ const dir = process.env.AGENT_SHARED_SKILLS ?? join11(homedir4(), "agent-shared", "skills");
1939
+ return existsSync4(dir) ? dir : undefined;
1940
+ }
1941
+ async function resolveAuthor(workspace) {
1942
+ const fromRepo = workspaceAuthor(runCommand, workspace);
1943
+ if (fromRepo)
1944
+ return fromRepo;
1945
+ const config = readMaesttroConfig();
1946
+ if (config.author)
1947
+ return config.author;
1948
+ if (!process.stdin.isTTY)
1949
+ return;
1950
+ console.log("No git identity found for this repo. Commits in the runner need an author (stored in ~/.config/maesttro/config.json).");
1951
+ const name = await ask("Author name: ");
1952
+ const email = await ask("Author email: ");
1953
+ if (!name || !email)
1954
+ return;
1955
+ writeMaesttroConfig({ ...config, author: { name, email } });
1956
+ return { name, email };
1957
+ }
1958
+ function startSession(run, root) {
1959
+ ensureWorktreesDir(run, root);
1960
+ if (!fetchOrigin(run, root)) {
1961
+ console.log("Couldn't fetch origin; starting from the last fetched state.");
1962
+ }
1963
+ const base = defaultBranch(run, root);
1964
+ const baseSha = revision(run, root, base);
1965
+ if (!baseSha)
1966
+ throw new LaunchError(`Can't resolve ${base}.`);
1967
+ const id = newSessionId();
1968
+ const sessionDir = join11(root, WORKTREES_DIR, id);
1969
+ createSessionClone(run, root, sessionDir, baseSha);
1970
+ return {
1971
+ id,
1972
+ repoRoot: root,
1973
+ worktree: sessionDir,
1974
+ base,
1975
+ baseSha,
1976
+ createdAt: new Date().toISOString(),
1977
+ refs: {}
1978
+ };
1979
+ }
1980
+ function resumeSession(root, id) {
1981
+ const session = readSession(id);
1982
+ if (!session || session.repoRoot !== root) {
1983
+ throw new LaunchError(`No session ${id} in this repo. See \`maesttro sessions\`.`);
1984
+ }
1985
+ if (!existsSync4(session.worktree)) {
1986
+ throw new LaunchError(`The clone for ${id} is gone: ${session.worktree}`);
1987
+ }
1988
+ return session;
1989
+ }
1990
+ function preserveBranches(run, session) {
1991
+ const preserved = [];
1992
+ for (const branch of cloneBranches(session.worktree)) {
1993
+ const tip = branchTip(session.worktree, branch);
1994
+ if (!tip)
1995
+ continue;
1996
+ if (importObjects(run, session.repoRoot, session.worktree, tip)) {
1997
+ git(run, session.repoRoot, [
1998
+ "update-ref",
1999
+ `refs/maesttro/${session.id}/${branch}`,
2000
+ tip
2001
+ ]);
2002
+ preserved.push(branch);
2003
+ }
2004
+ }
2005
+ return preserved;
2006
+ }
2007
+ function finishSession(run, session) {
2008
+ if (!existsSync4(session.worktree)) {
2009
+ deleteSession(session.id);
2010
+ return;
2011
+ }
2012
+ const preserved = preserveBranches(run, session);
2013
+ if (!cloneHasWork(session.worktree, session.baseSha)) {
2014
+ rmSync6(session.worktree, { recursive: true, force: true });
2015
+ deleteSession(session.id);
2016
+ return;
2017
+ }
2018
+ const head = cloneHead(session.worktree);
2019
+ const detachedWork = head?.detached !== undefined && head.detached !== session.baseSha;
2020
+ console.log(`
2021
+ Session ${session.id} kept. Continue with \`maesttro resume ${session.id}\`.`);
2022
+ if (preserved.length > 0) {
2023
+ console.log(`Branches from this session: ${preserved.join(", ")}. Push with \`maesttro push\`.`);
2024
+ } else if (detachedWork) {
2025
+ console.log("You have commits on a detached HEAD. Resume and `git switch -c <name>` to keep them.");
2026
+ }
2027
+ }
2028
+ function exitCodeOf(code, signal) {
2029
+ if (code !== null)
2030
+ return code;
2031
+ return signal ? 128 + (constants.signals[signal] ?? 0) : 1;
2032
+ }
2033
+ var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
2034
+ async function launch({
2035
+ args,
2036
+ resume
2037
+ }) {
2038
+ const run = runCommand;
2039
+ ensureReady();
2040
+ requireGit(run);
2041
+ const root = repoRoot(run, process.cwd());
2042
+ purgeLegacyTokenOnce({
2043
+ config: readMaesttroConfig(),
2044
+ saveConfig: writeMaesttroConfig,
2045
+ announce: (message) => console.log(message)
2046
+ });
2047
+ const started = resume ? resumeSession(root, resume) : startSession(run, root);
2048
+ const backup = backupRepo({
2049
+ run,
2050
+ repoRoot: root,
2051
+ refs: snapshotRefs(run, root),
2052
+ sessionId: started.id
2053
+ });
2054
+ const saved = { ...started, backup: started.backup ?? backup?.path };
2055
+ saveSession(saved);
2056
+ console.log(`Session ${saved.id}: clone .worktrees/${saved.id} at ${saved.base}${backup ? `, backup ${backup.reused ? "reused" : "written"}` : ""}.`);
2057
+ const repo = workspaceRepo(run, root);
2058
+ if (repo && !resume) {
2059
+ git(run, saved.worktree, [
2060
+ "remote",
2061
+ "set-url",
2062
+ "origin",
2063
+ `https://github.com/${repo}.git`
2064
+ ]);
2065
+ }
2066
+ const github = await openGitHubSession({
2067
+ repo,
2068
+ author: await resolveAuthor(root),
2069
+ broker: brokerUrl(),
2070
+ login: {
2071
+ clientId: githubAppClientId(),
2072
+ store: selectCredentialStore(),
2073
+ announce: (message) => console.log(message)
2074
+ },
2075
+ log: fileSessionLog()
2076
+ });
2077
+ if (github.notice)
2078
+ console.log(github.notice);
2079
+ const hostHome = homedir4();
2080
+ const clientConfigs = buildClientConfigs({
2081
+ host: readHostClientConfigs({ home: hostHome }),
2082
+ url: maesttroUrl()
2083
+ });
2084
+ if (clientConfigs.removed.length > 0) {
2085
+ console.log(`Kept out of the runner as secrets: ${clientConfigs.removed.join(", ")}`);
2086
+ }
2087
+ const clientConfigDir = writeClientConfigDir(clientConfigs.files);
2088
+ const { mounts, workdir } = sessionMounts({
2089
+ repoRoot: root,
2090
+ sessionDir: saved.worktree,
2091
+ packageRoot: PACKAGE_ROOT,
2092
+ hostHome,
2093
+ stagedPaths: STAGED_PATHS.filter((path) => existsSync4(join11(hostHome, path))),
2094
+ clientConfigs: clientConfigDir.dir,
2095
+ sharedSkills: sharedSkillsDir()
2096
+ });
2097
+ const sessionEnv = {
2098
+ ...github.env,
2099
+ MAESTTRO_SESSION_ID: saved.id,
2100
+ MAESTTRO_BASE: saved.base,
2101
+ MAESTTRO_GITHUB: github.notice || !repo ? "off" : "on",
2102
+ ...hostIdentityEnv(),
2103
+ ...repo ? { MAESTTRO_REPO: repo } : {}
2104
+ };
2105
+ const envFlags = [
2106
+ "MAESTTRO_SESSION_ID",
2107
+ "MAESTTRO_BASE",
2108
+ "MAESTTRO_GITHUB",
2109
+ "MAESTTRO_HOST_UID",
2110
+ "MAESTTRO_HOST_GID"
2111
+ ].concat(repo ? ["MAESTTRO_REPO"] : []).flatMap((name) => ["-e", name]);
2112
+ const child = spawn("docker", dockerRunArgs({
2113
+ command: containerCommand(resolveCommand(args)),
2114
+ mounts,
2115
+ workdir,
2116
+ extra: [...github.dockerArgs, ...envFlags]
2117
+ }), { stdio: "inherit", env: { ...process.env, ...sessionEnv } });
2118
+ const forward = (signal) => child.kill(signal);
2119
+ for (const signal of FORWARDED_SIGNALS)
2120
+ process.on(signal, forward);
2121
+ const exitCode = await new Promise((resolve) => {
2122
+ child.on("exit", (code, signal) => resolve(exitCodeOf(code, signal)));
2123
+ child.on("error", () => resolve(127));
2124
+ });
2125
+ for (const signal of FORWARDED_SIGNALS)
2126
+ process.off(signal, forward);
2127
+ await github.close();
2128
+ clientConfigDir.remove();
2129
+ finishSession(run, saved);
2130
+ process.exit(exitCode);
2131
+ }
2132
+
2133
+ // src/push.ts
2134
+ import { spawnSync as spawnSync2 } from "node:child_process";
2135
+ function pushCandidates(sessions) {
2136
+ const candidates = new Map;
2137
+ for (const session of sessions) {
2138
+ const defaultBranch = session.base.replace(/^origin\//, "");
2139
+ for (const branch of cloneBranches(session.worktree)) {
2140
+ if (branch === defaultBranch || !isSafeBranchName(branch))
2141
+ continue;
2142
+ candidates.set(branch, {
2143
+ branch,
2144
+ base: session.base,
2145
+ baseSha: session.baseSha,
2146
+ sessionId: session.id,
2147
+ dir: session.worktree
2148
+ });
2149
+ }
2150
+ }
2151
+ return candidates;
2152
+ }
2153
+ function planPush({
2154
+ requested,
2155
+ candidates
2156
+ }) {
2157
+ const names = [...candidates.keys()];
2158
+ if (requested) {
2159
+ const candidate = candidates.get(requested);
2160
+ if (candidate)
2161
+ return { ok: true, candidate };
2162
+ return {
2163
+ ok: false,
2164
+ candidates: names,
2165
+ reason: `${requested} wasn't created by a maesttro session in this repo (or is the default branch).`
2166
+ };
2167
+ }
2168
+ const [only] = names;
2169
+ if (names.length === 1 && only) {
2170
+ return { ok: true, candidate: candidates.get(only) };
2171
+ }
2172
+ return {
2173
+ ok: false,
2174
+ candidates: names,
2175
+ reason: names.length === 0 ? "No session in this repo has a branch ready to push." : "Several session branches are ready; pick one."
2176
+ };
2177
+ }
2178
+ async function choose(candidates) {
2179
+ if (!process.stdin.isTTY)
2180
+ return;
2181
+ console.log(candidates.map((name, index) => ` ${index + 1}) ${name}`).join(`
2182
+ `));
2183
+ const answer = await ask("Branch to push: ");
2184
+ return candidates[Number(answer) - 1];
2185
+ }
2186
+ function describe(run, root, branch, base, tip) {
2187
+ const commits = git(run, root, [
2188
+ "log",
2189
+ "--format= %h %s (%an / %cn)",
2190
+ `${base}..${tip}`
2191
+ ]).out;
2192
+ const stat = git(run, root, ["diff", "--stat", `${base}...${tip}`]).out;
2193
+ return [
2194
+ `Pushing ${branch} to origin (compared with ${base}):`,
2195
+ stripControl(commits) || " (no new commits)",
2196
+ stat ? `
2197
+ ${stripControl(stat)}` : ""
2198
+ ].join(`
2199
+ `);
2200
+ }
2201
+ async function push({ args }) {
2202
+ const run = runCommand;
2203
+ const yes = args.includes("--yes") || args.includes("-y");
2204
+ const requested = args.find((arg) => !arg.startsWith("-"));
2205
+ let root;
2206
+ try {
2207
+ root = repoRoot(run, process.cwd());
2208
+ } catch (error) {
2209
+ throw error instanceof LaunchError ? error : new LaunchError(String(error));
2210
+ }
2211
+ const candidates = pushCandidates(sessionsForRepo(root));
2212
+ let plan = planPush({ requested, candidates });
2213
+ if (!plan.ok && !requested && plan.candidates.length > 1) {
2214
+ const picked = await choose(plan.candidates);
2215
+ if (picked)
2216
+ plan = planPush({ requested: picked, candidates });
2217
+ }
2218
+ if (!plan.ok) {
2219
+ console.error(plan.reason);
2220
+ process.exitCode = 1;
2221
+ return;
2222
+ }
2223
+ const { branch, base, baseSha, dir, sessionId } = plan.candidate;
2224
+ const tip = branchTip(dir, branch);
2225
+ if (!tip || !importObjects(run, root, dir, tip)) {
2226
+ console.error(`Could not read ${branch} from session ${sessionId}.`);
2227
+ process.exitCode = 1;
2228
+ return;
2229
+ }
2230
+ git(run, root, ["update-ref", `refs/maesttro/${sessionId}/${branch}`, tip]);
2231
+ console.log(describe(run, root, branch, base, tip));
2232
+ if (!yes) {
2233
+ if (!process.stdin.isTTY) {
2234
+ console.error("Not a terminal; re-run with --yes to push without confirming.");
2235
+ process.exitCode = 1;
2236
+ return;
2237
+ }
2238
+ const answer = await ask(`
2239
+ Push? [y/N] `);
2240
+ if (!/^y(es)?$/i.test(answer)) {
2241
+ console.log("Not pushed.");
2242
+ return;
2243
+ }
2244
+ }
2245
+ const pushed = spawnSync2("git", ["-C", root, "push", "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
2246
+ if (pushed.status === 0) {
2247
+ console.log(`Pushed ${branch}. Base for comparison was ${baseSha.slice(0, 8)}.`);
2248
+ }
2249
+ process.exitCode = pushed.status ?? 1;
2250
+ }
2251
+
2252
+ // src/sessions-command.ts
2253
+ import { existsSync as existsSync5, rmSync as rmSync7 } from "node:fs";
2254
+ function sessionsCommand({ args }) {
2255
+ const run = runCommand;
2256
+ const root = repoRoot(run, process.cwd());
2257
+ const [action, id] = args;
2258
+ if (action === "remove") {
2259
+ const session = id ? readSession(id) : undefined;
2260
+ if (!session || session.repoRoot !== root) {
2261
+ console.error(`No session ${id ?? "(missing id)"} in this repo.`);
2262
+ process.exitCode = 1;
2263
+ return;
2264
+ }
2265
+ if (existsSync5(session.worktree)) {
2266
+ const preserved = preserveBranches(run, session);
2267
+ const head = cloneHead(session.worktree);
2268
+ if (preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha) {
2269
+ console.error(`${session.id} has commits on a detached HEAD not on any branch; resume and create a branch first.`);
2270
+ process.exitCode = 1;
2271
+ return;
2272
+ }
2273
+ rmSync7(session.worktree, { recursive: true, force: true });
2274
+ const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
2275
+ console.log(`Removed session ${session.id}.${kept}`);
2276
+ } else {
2277
+ console.log(`Removed session ${session.id}.`);
2278
+ }
2279
+ deleteSession(session.id);
2280
+ return;
2281
+ }
2282
+ const sessions = sessionsForRepo(root);
2283
+ if (sessions.length === 0) {
2284
+ console.log("No maesttro sessions in this repo.");
2285
+ return;
2286
+ }
2287
+ for (const session of sessions) {
2288
+ const branches = existsSync5(session.worktree) ? cloneBranches(session.worktree) : undefined;
2289
+ const where = branches === undefined ? "clone missing" : branches.length > 0 ? `branches: ${branches.join(", ")}` : "no branches yet";
2290
+ console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}`);
2291
+ }
2292
+ console.log("\n`maesttro resume <id>` continues one; `maesttro sessions remove <id>` removes its clone.");
2293
+ }
2294
+
2295
+ // src/setup.ts
2296
+ import { execSync as execSync4 } from "node:child_process";
2297
+ function run(command) {
2298
+ execSync4(command, { stdio: "inherit" });
2299
+ }
2300
+ function ensureDocker() {
2301
+ try {
2302
+ execSync4("docker info", { stdio: "ignore" });
2303
+ } catch {
2304
+ console.error("Docker doesn't seem to be running. Start Docker Desktop (or the docker daemon) and try again.");
2305
+ process.exit(1);
2306
+ }
2307
+ }
2308
+ async function setup({ args }) {
2309
+ console.log(`== maesttro setup ==
2310
+ `);
2311
+ ensureDocker();
2312
+ console.log("-> Building runner image (this pulls node:22-slim + installs CLIs, first run only)...");
2313
+ run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`);
2314
+ console.log(`
2315
+ -> Ensuring persistent volume exists...`);
2316
+ execSync4(`docker volume create ${VOLUME}`, { stdio: "ignore" });
2317
+ console.log(`
2318
+ -> Removing any long-lived GitHub token from the volume...`);
2319
+ if (purgeLegacyToken())
2320
+ console.log(` ${REVOKE_NOTICE}`);
2321
+ writeMaesttroConfig({ ...readMaesttroConfig(), legacyTokenPurged: true });
2322
+ if (args[0]) {
2323
+ console.log(" Ignoring the token argument: sessions now get short-lived tokens through `maesttro login`.");
2324
+ }
2325
+ console.log(`
2326
+ -> Logging in to GitHub...`);
2327
+ await ensureLogin();
2328
+ console.log(`
2329
+ -> Verifying isolation...`);
2330
+ console.log(" personal SSH key should NOT be visible:");
2331
+ run(`docker run --rm ${hostIdentityFlags()} -v ${VOLUME}:/home/agent ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`);
2332
+ console.log("\nSetup complete. Run `maesttro` from any project directory to start a session,");
2333
+ console.log("or `maesttro list` to see available clients.");
2334
+ }
2335
+
2336
+ // src/update.ts
2337
+ import { execSync as execSync5 } from "node:child_process";
2338
+ import { readFileSync as readFileSync8 } from "node:fs";
2339
+ import { join as join12 } from "node:path";
2340
+ function installedPackageName() {
2341
+ const manifest = JSON.parse(readFileSync8(join12(PACKAGE_ROOT, "package.json"), "utf8"));
2342
+ return manifest.name;
2343
+ }
2344
+ function update() {
2345
+ console.log(`== maesttro update ==
2346
+ `);
2347
+ const packageName = installedPackageName();
2348
+ console.log(`-> Updating ${packageName} package...`);
2349
+ execSync5(`npm i -g ${packageName}@latest`, { stdio: "inherit" });
2350
+ console.log(`
2351
+ -> Rebuilding runner image with refreshed client configs...`);
2352
+ execSync5(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`, { stdio: "inherit" });
2353
+ console.log(`
2354
+ Update complete. Bot identity and your project files are untouched —`);
2355
+ console.log("only the runner image (client versions + curated configs) was refreshed.");
2356
+ }
2357
+
2358
+ // src/main.ts
2359
+ var defaultCommands = {
2360
+ launch,
2361
+ push,
2362
+ sessions: sessionsCommand,
2363
+ configure: () => configure(),
2364
+ setup,
2365
+ update,
2366
+ doctor,
2367
+ login,
2368
+ logout,
2369
+ status,
2370
+ print: (text) => console.log(text)
2371
+ };
2372
+ async function main(argv, commands = defaultCommands) {
2373
+ const [command, ...rest] = argv;
2374
+ switch (command) {
2375
+ case undefined:
2376
+ await commands.launch({ args: [] });
2377
+ return;
2378
+ case "setup":
2379
+ await commands.setup({ args: rest });
2380
+ return;
2381
+ case "update":
2382
+ await commands.update();
2383
+ return;
2384
+ case "doctor":
2385
+ await commands.doctor();
2386
+ return;
2387
+ case "login":
2388
+ await commands.login();
2389
+ return;
2390
+ case "logout":
2391
+ await commands.logout();
2392
+ return;
2393
+ case "status":
2394
+ await commands.status();
2395
+ return;
2396
+ case "push":
2397
+ await commands.push({ args: rest });
2398
+ return;
2399
+ case "sessions":
2400
+ await commands.sessions({ args: rest });
2401
+ return;
2402
+ case "resume": {
2403
+ const [id, ...clientArgs] = rest;
2404
+ if (!id) {
2405
+ commands.print("Usage: maesttro resume <session-id> [client] [args...]");
2406
+ return;
2407
+ }
2408
+ await commands.launch({ args: clientArgs, resume: id });
2409
+ return;
2410
+ }
2411
+ case "configure":
2412
+ await commands.configure();
2413
+ return;
2414
+ case "list":
2415
+ commands.print(listClients());
2416
+ return;
2417
+ case "help":
2418
+ case "--help":
2419
+ case "-h":
2420
+ commands.print(HELP);
2421
+ return;
2422
+ default:
2423
+ await commands.launch({ args: argv });
2424
+ }
2425
+ }
2426
+
2427
+ // src/cli.ts
2428
+ main(process.argv.slice(2)).catch((error) => {
2429
+ console.error(error instanceof Error ? error.message : String(error));
2430
+ process.exit(1);
2431
+ });