maesttro 0.5.0 → 0.6.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.
package/dist/maesttro.js CHANGED
@@ -94,14 +94,53 @@ var require_src = __commonJS(function(exports, module) {
94
94
  // src/credential-store.ts
95
95
  import {
96
96
  chmodSync,
97
+ mkdirSync as mkdirSync2,
98
+ readFileSync as readFileSync2,
99
+ renameSync as renameSync2,
100
+ rmSync,
101
+ writeFileSync as writeFileSync2
102
+ } from "node:fs";
103
+ import { dirname, join as join2 } from "node:path";
104
+
105
+ // src/maesttro-home.ts
106
+ import {
107
+ appendFileSync,
97
108
  mkdirSync,
98
109
  readFileSync,
99
110
  renameSync,
100
- rmSync,
101
111
  writeFileSync
102
112
  } from "node:fs";
103
113
  import { homedir } from "node:os";
104
- import { dirname, join } from "node:path";
114
+ import { join } from "node:path";
115
+ function maesttroHome() {
116
+ return process.env.MAESTTRO_HOME ?? join(homedir(), ".config", "maesttro");
117
+ }
118
+ function configPath() {
119
+ return join(maesttroHome(), "config.json");
120
+ }
121
+ function readMaesttroConfig() {
122
+ try {
123
+ return JSON.parse(readFileSync(configPath(), "utf8"));
124
+ } catch {
125
+ return {};
126
+ }
127
+ }
128
+ function writeMaesttroConfig(config) {
129
+ mkdirSync(maesttroHome(), { recursive: true, mode: 448 });
130
+ const temporary = `${configPath()}.tmp`;
131
+ writeFileSync(temporary, `${JSON.stringify(config, null, 2)}
132
+ `);
133
+ renameSync(temporary, configPath());
134
+ }
135
+ function fileSessionLog(path = join(maesttroHome(), "maesttro.log")) {
136
+ return (event, detail) => {
137
+ try {
138
+ mkdirSync(maesttroHome(), { recursive: true, mode: 448 });
139
+ appendFileSync(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
140
+ `);
141
+ } catch {}
142
+ };
143
+ }
105
144
 
106
145
  // src/run-command.ts
107
146
  import { spawnSync } from "node:child_process";
@@ -120,98 +159,1375 @@ function commandExists(run, command) {
120
159
  return run("sh", ["-c", `command -v ${command}`]).status === 0;
121
160
  }
122
161
 
123
- // src/credential-store.ts
124
- var SERVICE = "maesttro";
125
- var ACCOUNT = "github";
126
- function encode(token) {
127
- return Buffer.from(JSON.stringify(token)).toString("base64");
162
+ // src/credential-store.ts
163
+ var SERVICE = "maesttro";
164
+ var GITHUB_CODEC = {
165
+ account: "github",
166
+ accepts: (value) => typeof value?.access_token === "string"
167
+ };
168
+ function encode(value) {
169
+ return Buffer.from(JSON.stringify(value)).toString("base64");
170
+ }
171
+ function decoderFor(codec) {
172
+ return (encoded) => {
173
+ try {
174
+ const value = JSON.parse(Buffer.from(encoded.trim(), "base64").toString("utf8"));
175
+ return codec.accepts(value) ? value : undefined;
176
+ } catch {
177
+ return;
178
+ }
179
+ };
180
+ }
181
+ function defaultFilePath(account) {
182
+ const name = account === "github" ? "credentials.json" : `credentials-${account}.json`;
183
+ return join2(maesttroHome(), name);
184
+ }
185
+ function keychainStore(run = runCommand, codec = GITHUB_CODEC) {
186
+ const identity = ["-s", SERVICE, "-a", codec.account];
187
+ const decode = decoderFor(codec);
188
+ return {
189
+ kind: "macOS keychain",
190
+ load: () => {
191
+ const result = run("security", [
192
+ "find-generic-password",
193
+ ...identity,
194
+ "-w"
195
+ ]);
196
+ return result.status === 0 ? decode(result.stdout) : undefined;
197
+ },
198
+ save: (value) => {
199
+ const command = `add-generic-password -U ${identity.join(" ")} -w ${encode(value)}
200
+ `;
201
+ const result = run("security", ["-i"], command);
202
+ if (result.status !== 0)
203
+ throw new Error("Could not write to keychain");
204
+ },
205
+ clear: () => {
206
+ run("security", ["delete-generic-password", ...identity]);
207
+ }
208
+ };
209
+ }
210
+ function secretToolStore(run = runCommand, codec = GITHUB_CODEC) {
211
+ const attributes = ["service", SERVICE, "account", codec.account];
212
+ const decode = decoderFor(codec);
213
+ return {
214
+ kind: "Secret Service (secret-tool)",
215
+ load: () => {
216
+ const result = run("secret-tool", ["lookup", ...attributes]);
217
+ return result.status === 0 ? decode(result.stdout) : undefined;
218
+ },
219
+ save: (value) => {
220
+ const result = run("secret-tool", ["store", `--label=${SERVICE}`, ...attributes], encode(value));
221
+ if (result.status !== 0) {
222
+ throw new Error("Could not write to the Secret Service");
223
+ }
224
+ },
225
+ clear: () => {
226
+ run("secret-tool", ["clear", ...attributes]);
227
+ }
228
+ };
229
+ }
230
+ function fileStore(path, codec = GITHUB_CODEC) {
231
+ const resolvedPath = path ?? defaultFilePath(codec.account);
232
+ const decode = decoderFor(codec);
233
+ return {
234
+ kind: `file (${resolvedPath})`,
235
+ load: () => {
236
+ try {
237
+ return decode(readFileSync2(resolvedPath, "utf8"));
238
+ } catch {
239
+ return;
240
+ }
241
+ },
242
+ save: (value) => {
243
+ mkdirSync2(dirname(resolvedPath), { recursive: true, mode: 448 });
244
+ const temporary = `${resolvedPath}.tmp`;
245
+ writeFileSync2(temporary, encode(value), { mode: 384 });
246
+ chmodSync(temporary, 384);
247
+ renameSync2(temporary, resolvedPath);
248
+ },
249
+ clear: () => {
250
+ rmSync(resolvedPath, { force: true });
251
+ }
252
+ };
253
+ }
254
+ function selectCredentialStore({
255
+ platform = process.platform,
256
+ env = process.env,
257
+ run = runCommand,
258
+ codec = GITHUB_CODEC
259
+ } = {}) {
260
+ if (env.MAESTTRO_CREDENTIAL_STORE === "file")
261
+ return fileStore(undefined, codec);
262
+ if (platform === "darwin")
263
+ return keychainStore(run, codec);
264
+ if (platform === "linux" && commandExists(run, "secret-tool")) {
265
+ return secretToolStore(run, codec);
266
+ }
267
+ return fileStore(undefined, codec);
268
+ }
269
+
270
+ // src/claude-credentials.ts
271
+ function oauthOf(value) {
272
+ const oauth = value?.claudeAiOauth;
273
+ return oauth && typeof oauth === "object" ? oauth : undefined;
274
+ }
275
+ function nonEmpty(value) {
276
+ return typeof value === "string" && value.length > 0;
277
+ }
278
+ function isClaudeCredentials(value) {
279
+ const oauth = oauthOf(value);
280
+ return oauth !== undefined && nonEmpty(oauth.accessToken) && typeof oauth.expiresAt === "number" && oauth.expiresAt > 0;
281
+ }
282
+ function isMasterLogin(value) {
283
+ return isClaudeCredentials(value) && nonEmpty(oauthOf(value)?.refreshToken);
284
+ }
285
+ function carriesRefreshToken(value) {
286
+ return nonEmpty(oauthOf(value)?.refreshToken);
287
+ }
288
+ function isBlankedLogin(value) {
289
+ const oauth = oauthOf(value);
290
+ if (!oauth)
291
+ return false;
292
+ return oauth.accessToken === "" || oauth.refreshToken === "" || typeof oauth.expiresAt === "number" && oauth.expiresAt <= 0;
293
+ }
294
+ function withoutRefreshToken(credentials) {
295
+ const { refreshToken: _, ...oauth } = credentials.claudeAiOauth;
296
+ return { claudeAiOauth: oauth };
297
+ }
298
+ function markedExpired({
299
+ credentials,
300
+ now
301
+ }) {
302
+ return {
303
+ claudeAiOauth: { ...credentials.claudeAiOauth, expiresAt: now - 60000 }
304
+ };
305
+ }
306
+ function acceptRefreshed({
307
+ previous,
308
+ candidate,
309
+ now
310
+ }) {
311
+ if (!isMasterLogin(candidate))
312
+ return;
313
+ if (candidate.claudeAiOauth.expiresAt <= now)
314
+ return;
315
+ if (candidate.claudeAiOauth.accessToken === previous.claudeAiOauth.accessToken)
316
+ return;
317
+ return candidate;
318
+ }
319
+ function markedRejected({
320
+ credentials,
321
+ now
322
+ }) {
323
+ return { ...credentials, rejectedAt: now };
324
+ }
325
+ function claudeLoginState({
326
+ credentials,
327
+ now
328
+ }) {
329
+ if (!credentials)
330
+ return "missing";
331
+ if (credentials.rejectedAt !== undefined)
332
+ return "expired";
333
+ const { refreshTokenExpiresAt } = credentials.claudeAiOauth;
334
+ if (refreshTokenExpiresAt !== undefined && refreshTokenExpiresAt <= now) {
335
+ return "expired";
336
+ }
337
+ return "active";
338
+ }
339
+ var LOGIN_WARNING_MS = 5 * 24 * 60 * 60 * 1000;
340
+ var DAY_MS = 24 * 60 * 60 * 1000;
341
+ function claudeLoginWarning({
342
+ credentials,
343
+ now
344
+ }) {
345
+ if (claudeLoginState({ credentials, now }) !== "active")
346
+ return;
347
+ const expiresAt = credentials?.claudeAiOauth.refreshTokenExpiresAt;
348
+ if (expiresAt === undefined || expiresAt - now > LOGIN_WARNING_MS) {
349
+ return;
350
+ }
351
+ const days = Math.floor((expiresAt - now) / DAY_MS);
352
+ const within = days === 0 ? "within a day" : `in ${days} day${days === 1 ? "" : "s"}`;
353
+ return `Claude login expires ${within} (${new Date(expiresAt).toLocaleString()}): run \`maesttro login claude\` before then`;
354
+ }
355
+ var CLAUDE_CODEC = {
356
+ account: "claude",
357
+ accepts: isMasterLogin
358
+ };
359
+ function claudeStore() {
360
+ return selectCredentialStore({ codec: CLAUDE_CODEC });
361
+ }
362
+
363
+ // src/claude-keeper.ts
364
+ import { spawn, spawnSync as spawnSync2 } from "node:child_process";
365
+ import { randomBytes } from "node:crypto";
366
+ import {
367
+ chmodSync as chmodSync2,
368
+ mkdirSync as mkdirSync3,
369
+ mkdtempSync,
370
+ readFileSync as readFileSync3,
371
+ rmSync as rmSync2,
372
+ writeFileSync as writeFileSync3
373
+ } from "node:fs";
374
+ import { tmpdir } from "node:os";
375
+ import { join as join4 } from "node:path";
376
+
377
+ // src/host-identity.ts
378
+ function hostIdentityEnv() {
379
+ if (typeof process.getuid !== "function" || typeof process.getgid !== "function") {
380
+ return {};
381
+ }
382
+ return {
383
+ MAESTTRO_HOST_UID: String(process.getuid()),
384
+ MAESTTRO_HOST_GID: String(process.getgid())
385
+ };
386
+ }
387
+ function hostIdentityFlags() {
388
+ return Object.entries(hostIdentityEnv()).map(([name, value]) => `-e ${name}=${value}`).join(" ");
389
+ }
390
+
391
+ // src/paths.ts
392
+ import { dirname as dirname2, join as join3, resolve } from "node:path";
393
+ import { fileURLToPath } from "node:url";
394
+ var PACKAGE_ROOT = resolve(dirname2(fileURLToPath(import.meta.url)), "..");
395
+ var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
396
+ var IMAGE = "maesttro-runner:latest";
397
+ var LEGACY_VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
398
+
399
+ // src/claude-keeper.ts
400
+ var KEEPER_COMMAND = 'claude -p --model haiku "ok"';
401
+ var KEEPER_TIMEOUT_MS = 75 * 1000;
402
+ var KEEPER_REMOVE_TIMEOUT_MS = 10 * 1000;
403
+ var KEEPER_EXIT_GRACE_MS = 5 * 1000;
404
+ var LOGIN_COMMAND = "claude auth login";
405
+ var REASON_MAX_LENGTH = 200;
406
+ var dockerCli = (args, { interactive }) => {
407
+ const result = spawnSync2("docker", args, {
408
+ stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"]
409
+ });
410
+ return {
411
+ status: result.status ?? null,
412
+ stderr: result.stderr?.toString() ?? "",
413
+ error: result.error
414
+ };
415
+ };
416
+ function collect(child) {
417
+ let stdout = "";
418
+ let stderr = "";
419
+ child.stdout?.on("data", (chunk) => {
420
+ stdout += String(chunk);
421
+ });
422
+ child.stderr?.on("data", (chunk) => {
423
+ stderr += String(chunk);
424
+ });
425
+ return new Promise((resolve) => {
426
+ child.on("error", (error) => resolve({ status: null, stdout, stderr, error }));
427
+ child.on("close", (status) => resolve({ status, stdout, stderr }));
428
+ });
429
+ }
430
+ var keeperDockerCli = {
431
+ start: (args) => {
432
+ const child = spawn("docker", args, {
433
+ stdio: ["ignore", "ignore", "pipe"],
434
+ detached: true
435
+ });
436
+ return { exited: collect(child), kill: () => child.kill("SIGKILL") };
437
+ },
438
+ exec: (args, timeoutMs) => collect(spawn("docker", args, {
439
+ stdio: ["ignore", "pipe", "pipe"],
440
+ detached: true,
441
+ timeout: timeoutMs,
442
+ killSignal: "SIGKILL"
443
+ }))
444
+ };
445
+ function keeperNamePrefix(uid = process.getuid?.()) {
446
+ return `claude-keeper-${uid ?? "user"}-`;
447
+ }
448
+ function keeperContainerName() {
449
+ return `${keeperNamePrefix()}${randomBytes(4).toString("hex")}`;
450
+ }
451
+ function keeperWorkDir() {
452
+ return join4(maesttroHome(), "claude-keeper");
453
+ }
454
+ function runArgs({
455
+ claudeDir,
456
+ identity,
457
+ interactive,
458
+ command,
459
+ name
460
+ }) {
461
+ return [
462
+ "run",
463
+ ...interactive ? ["-it"] : [],
464
+ "--rm",
465
+ ...Object.entries(identity).flatMap(([envName, value]) => [
466
+ "-e",
467
+ `${envName}=${value}`
468
+ ]),
469
+ ...name ? ["--name", name] : [],
470
+ "--mount",
471
+ `type=bind,source=${claudeDir},target=/home/agent/.claude`,
472
+ IMAGE,
473
+ command
474
+ ];
475
+ }
476
+ function keeperArgs({
477
+ claudeDir,
478
+ identity,
479
+ name
480
+ }) {
481
+ return runArgs({
482
+ claudeDir,
483
+ identity,
484
+ interactive: false,
485
+ command: KEEPER_COMMAND,
486
+ name
487
+ });
488
+ }
489
+ function loginArgs({
490
+ claudeDir,
491
+ identity
492
+ }) {
493
+ return runArgs({
494
+ claudeDir,
495
+ identity,
496
+ interactive: true,
497
+ command: LOGIN_COMMAND
498
+ });
499
+ }
500
+ function privateDir(parent = tmpdir()) {
501
+ mkdirSync3(parent, { recursive: true, mode: 448 });
502
+ const dir = mkdtempSync(join4(parent, "maesttro-claude-"));
503
+ chmodSync2(dir, 448);
504
+ return dir;
505
+ }
506
+ function readKeeperCredentials(dir) {
507
+ try {
508
+ return JSON.parse(readFileSync3(join4(dir, ".credentials.json"), "utf8"));
509
+ } catch {
510
+ return;
511
+ }
512
+ }
513
+ function inPrivateDir(makeDir, use) {
514
+ const dir = makeDir();
515
+ try {
516
+ return use(dir);
517
+ } finally {
518
+ rmSync2(dir, { recursive: true, force: true });
519
+ }
520
+ }
521
+ function lastLine(text) {
522
+ const lines = text.split(`
523
+ `).map((line) => line.trim()).filter(Boolean);
524
+ return (lines.at(-1) ?? "").slice(-REASON_MAX_LENGTH);
525
+ }
526
+ function failureReason({
527
+ result,
528
+ timedOut,
529
+ name
530
+ }) {
531
+ if (result.error && !timedOut) {
532
+ return result.error.message.replaceAll(name, "claude-keeper");
533
+ }
534
+ const summary = timedOut ? "the keeper timed out" : result.status === 0 ? "the keeper left the login unchanged" : `the keeper exited with status ${result.status}`;
535
+ const detail = lastLine(result.stderr.replaceAll(name, "claude-keeper"));
536
+ return detail ? `${summary}: ${detail}` : summary;
537
+ }
538
+ var TIMED_OUT = Symbol("timed out");
539
+ function sleepFor(ms) {
540
+ return new Promise((resolve) => setTimeout(resolve, ms));
541
+ }
542
+ async function runBounded({
543
+ docker,
544
+ args,
545
+ name,
546
+ sleep
547
+ }) {
548
+ const run = docker.start(args);
549
+ const first = await Promise.race([
550
+ run.exited,
551
+ sleep(KEEPER_TIMEOUT_MS).then(() => TIMED_OUT)
552
+ ]);
553
+ if (first !== TIMED_OUT)
554
+ return { result: first, timedOut: false, gone: true };
555
+ const removal = await docker.exec(["rm", "-f", name], KEEPER_REMOVE_TIMEOUT_MS);
556
+ const settled = await Promise.race([
557
+ run.exited,
558
+ sleep(KEEPER_EXIT_GRACE_MS).then(() => TIMED_OUT)
559
+ ]);
560
+ if (settled !== TIMED_OUT) {
561
+ return { result: settled, timedOut: true, gone: true };
562
+ }
563
+ run.kill();
564
+ return {
565
+ result: await run.exited,
566
+ timedOut: true,
567
+ gone: removal.status === 0
568
+ };
569
+ }
570
+ async function removeLingeringKeepers({
571
+ docker = keeperDockerCli
572
+ } = {}) {
573
+ const listed = await docker.exec(["ps", "-aq", "--filter", `name=^${keeperNamePrefix()}`], KEEPER_REMOVE_TIMEOUT_MS);
574
+ if (listed.status !== 0) {
575
+ throw new Error(lastLine(listed.stderr) || "docker ps failed");
576
+ }
577
+ const ids = (listed.stdout ?? "").split(`
578
+ `).filter(Boolean);
579
+ if (ids.length === 0)
580
+ return;
581
+ const removed = await docker.exec(["rm", "-f", ...ids], KEEPER_REMOVE_TIMEOUT_MS);
582
+ if (removed.status !== 0) {
583
+ throw new Error(lastLine(removed.stderr) || "docker rm failed");
584
+ }
585
+ }
586
+ async function refreshWithKeeper({
587
+ master,
588
+ now,
589
+ workDir = keeperWorkDir(),
590
+ docker = keeperDockerCli,
591
+ identity = hostIdentityEnv(),
592
+ sleep = sleepFor
593
+ }) {
594
+ const claudeDir = privateDir(workDir);
595
+ let outcome = {
596
+ kind: "inconclusive",
597
+ reason: "the keeper never ran"
598
+ };
599
+ let keepDir = false;
600
+ try {
601
+ writeFileSync3(join4(claudeDir, ".credentials.json"), JSON.stringify(markedExpired({ credentials: master, now })), { mode: 384 });
602
+ const name = keeperContainerName();
603
+ const { result, timedOut, gone } = await runBounded({
604
+ docker,
605
+ args: keeperArgs({ claudeDir, identity, name }),
606
+ name,
607
+ sleep
608
+ });
609
+ const candidate = readKeeperCredentials(claudeDir);
610
+ const refreshed = acceptRefreshed({ previous: master, candidate, now });
611
+ if (refreshed) {
612
+ outcome = { kind: "refreshed", credentials: refreshed };
613
+ return outcome;
614
+ }
615
+ if (!gone) {
616
+ keepDir = true;
617
+ return {
618
+ kind: "inconclusive",
619
+ reason: "the keeper couldn't be stopped; its result is kept for the next refresh"
620
+ };
621
+ }
622
+ outcome = isBlankedLogin(candidate) ? { kind: "rejected" } : {
623
+ kind: "inconclusive",
624
+ reason: failureReason({ result, timedOut, name })
625
+ };
626
+ return outcome;
627
+ } finally {
628
+ if (outcome.kind !== "refreshed" && !keepDir) {
629
+ rmSync2(claudeDir, { recursive: true, force: true });
630
+ }
631
+ }
632
+ }
633
+ function captureLogin({
634
+ docker = dockerCli,
635
+ makeDir = () => privateDir(),
636
+ identity = hostIdentityEnv()
637
+ } = {}) {
638
+ return inPrivateDir(makeDir, (claudeDir) => {
639
+ const { status } = docker(loginArgs({ claudeDir, identity }), {
640
+ interactive: true
641
+ });
642
+ if (status !== 0)
643
+ return;
644
+ const credentials = readKeeperCredentials(claudeDir);
645
+ return isMasterLogin(credentials) ? credentials : undefined;
646
+ });
647
+ }
648
+
649
+ // src/claude-refresher.ts
650
+ import { randomBytes as randomBytes4 } from "node:crypto";
651
+ import {
652
+ closeSync as closeSync2,
653
+ existsSync,
654
+ linkSync,
655
+ lstatSync as lstatSync3,
656
+ mkdirSync as mkdirSync7,
657
+ openSync as openSync2,
658
+ readdirSync as readdirSync2,
659
+ readFileSync as readFileSync7,
660
+ renameSync as renameSync5,
661
+ rmSync as rmSync5,
662
+ statSync,
663
+ utimesSync,
664
+ writeSync
665
+ } from "node:fs";
666
+ import { dirname as dirname4, join as join7 } from "node:path";
667
+
668
+ // src/held-signals.ts
669
+ var HELD_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
670
+ var processSignals = {
671
+ listenerCount: (signal) => process.listenerCount(signal),
672
+ on: (signal, handler) => {
673
+ process.on(signal, handler);
674
+ },
675
+ off: (signal, handler) => {
676
+ process.off(signal, handler);
677
+ },
678
+ raise: (signal) => {
679
+ process.kill(process.pid, signal);
680
+ }
681
+ };
682
+ function holdSignals({
683
+ host = processSignals,
684
+ onHeld = () => {}
685
+ }) {
686
+ let received;
687
+ let released = false;
688
+ const handlers = [];
689
+ const detach = () => {
690
+ for (const { signal, handler } of handlers)
691
+ host.off(signal, handler);
692
+ handlers.length = 0;
693
+ };
694
+ for (const signal of HELD_SIGNALS) {
695
+ if (host.listenerCount(signal) > 0)
696
+ continue;
697
+ const handler = () => {
698
+ if (received) {
699
+ released = true;
700
+ detach();
701
+ host.raise(signal);
702
+ return;
703
+ }
704
+ received = signal;
705
+ onHeld();
706
+ };
707
+ host.on(signal, handler);
708
+ handlers.push({ signal, handler });
709
+ }
710
+ return {
711
+ pending: () => received !== undefined,
712
+ release: () => {
713
+ if (released)
714
+ return;
715
+ released = true;
716
+ detach();
717
+ if (received)
718
+ host.raise(received);
719
+ }
720
+ };
721
+ }
722
+ function signalRouter({
723
+ base = processSignals,
724
+ onUnrouted
725
+ }) {
726
+ const routed = new Map;
727
+ const listeners = HELD_SIGNALS.map((signal) => {
728
+ const listener = () => {
729
+ const handlers = [...routed.get(signal) ?? []];
730
+ if (handlers.length === 0)
731
+ return onUnrouted(signal);
732
+ for (const handler of handlers)
733
+ handler();
734
+ };
735
+ base.on(signal, listener);
736
+ return { signal, listener };
737
+ });
738
+ return {
739
+ listenerCount: (signal) => routed.get(signal)?.size ?? 0,
740
+ on: (signal, handler) => {
741
+ routed.set(signal, (routed.get(signal) ?? new Set).add(handler));
742
+ },
743
+ off: (signal, handler) => {
744
+ routed.get(signal)?.delete(handler);
745
+ },
746
+ raise: (signal) => onUnrouted(signal),
747
+ dispose: () => {
748
+ for (const { signal, listener } of listeners)
749
+ base.off(signal, listener);
750
+ }
751
+ };
752
+ }
753
+
754
+ // src/refresher.ts
755
+ var REFRESH_LEAD_MS = 10 * 60 * 1000;
756
+ var WALL_CLOCK_TICK_MS = 60 * 1000;
757
+ var FIRST_BACKOFF_MS = 5 * 1000;
758
+ var MAX_BACKOFF_MS = 5 * 60 * 1000;
759
+ var nodeTimers = {
760
+ setTimeout: (callback, ms) => setTimeout(callback, ms).unref(),
761
+ clearTimeout: (handle) => clearTimeout(handle),
762
+ setInterval: (callback, ms) => setInterval(callback, ms).unref(),
763
+ clearInterval: (handle) => clearInterval(handle)
764
+ };
765
+ function refreshDueAt({
766
+ expiresAt,
767
+ now
768
+ }) {
769
+ const halfRemaining = (expiresAt - now) / 2;
770
+ return expiresAt - Math.min(REFRESH_LEAD_MS, Math.max(halfRemaining, 0));
771
+ }
772
+ function startRefresher({
773
+ initial,
774
+ expiresAtOf,
775
+ firstAttemptAt,
776
+ refresh,
777
+ onRefreshed,
778
+ onFailed,
779
+ now = Date.now,
780
+ timers = nodeTimers
781
+ }) {
782
+ let current = initial;
783
+ let nextAttemptAt = 0;
784
+ let backoffMs = FIRST_BACKOFF_MS;
785
+ let inFlight;
786
+ let stopped = false;
787
+ let pending;
788
+ function dueAt(token) {
789
+ return refreshDueAt({ expiresAt: expiresAtOf(token), now: now() });
790
+ }
791
+ nextAttemptAt = firstAttemptAt ?? dueAt(initial);
792
+ function scheduleAt(at) {
793
+ timers.clearTimeout(pending);
794
+ pending = timers.setTimeout(check, Math.max(at - now(), 0));
795
+ }
796
+ async function attempt() {
797
+ try {
798
+ const token = await refresh();
799
+ if (stopped)
800
+ return;
801
+ current = token;
802
+ backoffMs = FIRST_BACKOFF_MS;
803
+ nextAttemptAt = dueAt(token);
804
+ onRefreshed(token);
805
+ } catch (error) {
806
+ if (stopped)
807
+ return;
808
+ nextAttemptAt = now() + backoffMs;
809
+ onFailed(error, backoffMs);
810
+ backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
811
+ }
812
+ scheduleAt(nextAttemptAt);
813
+ }
814
+ function check() {
815
+ if (stopped)
816
+ return Promise.resolve();
817
+ if (inFlight)
818
+ return inFlight;
819
+ if (now() < nextAttemptAt)
820
+ return Promise.resolve();
821
+ inFlight = attempt().finally(() => {
822
+ inFlight = undefined;
823
+ });
824
+ return inFlight;
825
+ }
826
+ const tick = timers.setInterval(check, WALL_CLOCK_TICK_MS);
827
+ scheduleAt(nextAttemptAt);
828
+ return {
829
+ current: () => current,
830
+ check,
831
+ stop: () => {
832
+ stopped = true;
833
+ timers.clearTimeout(pending);
834
+ timers.clearInterval(tick);
835
+ return inFlight ?? Promise.resolve();
836
+ },
837
+ busy: () => inFlight !== undefined
838
+ };
839
+ }
840
+
841
+ // src/session-home.ts
842
+ import { randomBytes as randomBytes3 } from "node:crypto";
843
+ import {
844
+ closeSync,
845
+ constants,
846
+ fchmodSync,
847
+ fstatSync,
848
+ lstatSync as lstatSync2,
849
+ mkdirSync as mkdirSync6,
850
+ openSync,
851
+ readFileSync as readFileSync6,
852
+ renameSync as renameSync4,
853
+ rmSync as rmSync4,
854
+ unlinkSync,
855
+ writeFileSync as writeFileSync6
856
+ } from "node:fs";
857
+ import { join as join6 } from "node:path";
858
+
859
+ // src/json-file.ts
860
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
861
+ import { dirname as dirname3 } from "node:path";
862
+ function isObject(value) {
863
+ return typeof value === "object" && value !== null && !Array.isArray(value);
864
+ }
865
+ function mergeJson(existing, curated) {
866
+ const merged = { ...existing };
867
+ for (const [key, value] of Object.entries(curated)) {
868
+ const current = merged[key];
869
+ merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
870
+ }
871
+ return merged;
872
+ }
873
+ function readJson(path) {
874
+ try {
875
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
876
+ return isObject(parsed) ? parsed : {};
877
+ } catch {
878
+ return {};
879
+ }
880
+ }
881
+ function writeJson(path, value) {
882
+ mkdirSync4(dirname3(path), { recursive: true });
883
+ writeFileSync4(path, `${JSON.stringify(value, null, 2)}
884
+ `);
885
+ }
886
+ function skipInsignificant(text, from) {
887
+ let index = from;
888
+ while (index < text.length) {
889
+ if (/\s/.test(text[index] ?? "")) {
890
+ index++;
891
+ } else if (text.startsWith("//", index)) {
892
+ while (index < text.length && text[index] !== `
893
+ `)
894
+ index++;
895
+ } else if (text.startsWith("/*", index)) {
896
+ const end = text.indexOf("*/", index + 2);
897
+ index = end === -1 ? text.length : end + 2;
898
+ } else {
899
+ return index;
900
+ }
901
+ }
902
+ return index;
903
+ }
904
+ function stripJsonc(text) {
905
+ let output = "";
906
+ let index = 0;
907
+ while (index < text.length) {
908
+ const char = text[index];
909
+ if (char === '"') {
910
+ const start = index;
911
+ index++;
912
+ while (index < text.length && text[index] !== '"') {
913
+ index += text[index] === "\\" ? 2 : 1;
914
+ }
915
+ index++;
916
+ output += text.slice(start, index);
917
+ continue;
918
+ }
919
+ const significant = skipInsignificant(text, index);
920
+ if (significant > index) {
921
+ output += " ";
922
+ index = significant;
923
+ continue;
924
+ }
925
+ const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
926
+ if (!trailingComma)
927
+ output += char;
928
+ index++;
929
+ }
930
+ return output;
931
+ }
932
+ function parseJsonc(text) {
933
+ const parsed = JSON.parse(stripJsonc(text));
934
+ if (!isObject(parsed))
935
+ throw new Error("expected a JSON object");
936
+ return parsed;
937
+ }
938
+
939
+ // src/sessions.ts
940
+ import { randomBytes as randomBytes2 } from "node:crypto";
941
+ import {
942
+ chmodSync as chmodSync3,
943
+ lstatSync,
944
+ mkdirSync as mkdirSync5,
945
+ readdirSync,
946
+ readFileSync as readFileSync5,
947
+ renameSync as renameSync3,
948
+ rmSync as rmSync3,
949
+ writeFileSync as writeFileSync5
950
+ } from "node:fs";
951
+ import { join as join5 } from "node:path";
952
+ function newSessionId() {
953
+ return `maesttro-${randomBytes2(4).toString("hex")}`;
954
+ }
955
+ function sessionsDir(home = maesttroHome()) {
956
+ return join5(home, "sessions");
957
+ }
958
+ function saveSession(record, dir = sessionsDir()) {
959
+ mkdirSync5(dir, { recursive: true, mode: 448 });
960
+ const path = join5(dir, `${record.id}.json`);
961
+ writeFileSync5(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
962
+ `);
963
+ renameSync3(`${path}.tmp`, path);
964
+ }
965
+ function readSession(id, dir = sessionsDir()) {
966
+ try {
967
+ return JSON.parse(readFileSync5(join5(dir, `${id}.json`), "utf8"));
968
+ } catch {
969
+ return;
970
+ }
971
+ }
972
+ function unlockTree(path) {
973
+ let entry;
974
+ try {
975
+ entry = lstatSync(path);
976
+ } catch {
977
+ return;
978
+ }
979
+ if (!entry.isDirectory())
980
+ return;
981
+ chmodSync3(path, 448);
982
+ for (const name of readdirSync(path))
983
+ unlockTree(join5(path, name));
984
+ }
985
+ function removeTree(path) {
986
+ try {
987
+ rmSync3(path, { recursive: true, force: true });
988
+ } catch {
989
+ unlockTree(path);
990
+ rmSync3(path, { recursive: true, force: true });
991
+ }
992
+ }
993
+ function deleteSession(id, dir = sessionsDir()) {
994
+ removeTree(join5(dir, id));
995
+ rmSync3(join5(dir, `${id}.json`), { force: true });
996
+ }
997
+ function sessionsForRepo(repoRoot, dir = sessionsDir()) {
998
+ let names;
999
+ try {
1000
+ names = readdirSync(dir).filter((name) => name.endsWith(".json"));
1001
+ } catch {
1002
+ return [];
1003
+ }
1004
+ return names.map((name) => readSession(name.slice(0, -".json".length), dir)).filter((record) => record?.repoRoot === repoRoot).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1005
+ }
1006
+
1007
+ // src/session-home.ts
1008
+ var OPEN_DIRECTORY_NO_FOLLOW = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
1009
+ var CREATE_NEW_NO_FOLLOW = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW;
1010
+ var READ_NO_FOLLOW_NO_BLOCK = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
1011
+ function sessionHome({
1012
+ id,
1013
+ dir = sessionsDir()
1014
+ }) {
1015
+ return join6(dir, id, "home");
128
1016
  }
129
- function decode(encoded) {
1017
+ function sessionClaudeDir({
1018
+ id,
1019
+ dir = sessionsDir()
1020
+ }) {
1021
+ return join6(dir, id, "claude");
1022
+ }
1023
+ function credentialsPath(claudeDir) {
1024
+ return join6(claudeDir, ".credentials.json");
1025
+ }
1026
+ function entryAt(path) {
1027
+ try {
1028
+ return lstatSync2(path);
1029
+ } catch (error) {
1030
+ if (error.code === "ENOENT")
1031
+ return;
1032
+ throw error;
1033
+ }
1034
+ }
1035
+ function requireRealDirectory(path) {
1036
+ if (!lstatSync2(path).isDirectory()) {
1037
+ throw new Error(`Not a directory: ${path}`);
1038
+ }
1039
+ }
1040
+ function withFd(fd, use) {
1041
+ try {
1042
+ return use(fd);
1043
+ } finally {
1044
+ closeSync(fd);
1045
+ }
1046
+ }
1047
+ function ensurePrivateDir(path) {
1048
+ const entry = entryAt(path);
1049
+ if (!entry?.isDirectory()) {
1050
+ if (entry)
1051
+ unlinkSync(path);
1052
+ mkdirSync6(path, { recursive: true, mode: 448 });
1053
+ }
1054
+ withFd(openSync(path, OPEN_DIRECTORY_NO_FOLLOW), (fd) => fchmodSync(fd, 448));
1055
+ }
1056
+ function ensureSessionHome({
1057
+ id,
1058
+ dir = sessionsDir()
1059
+ }) {
1060
+ const home = sessionHome({ id, dir });
1061
+ const claudeDir = sessionClaudeDir({ id, dir });
1062
+ ensurePrivateDir(home);
1063
+ ensurePrivateDir(claudeDir);
1064
+ ensurePrivateDir(join6(home, ".claude"));
1065
+ return { home, claudeDir };
1066
+ }
1067
+ function supersedes({
1068
+ credentials,
1069
+ current
1070
+ }) {
1071
+ if (carriesRefreshToken(current))
1072
+ return false;
1073
+ if (!isClaudeCredentials(current))
1074
+ return true;
1075
+ return credentials.claudeAiOauth.expiresAt > current.claudeAiOauth.expiresAt;
1076
+ }
1077
+ function writeSessionCredentials({
1078
+ claudeDir,
1079
+ credentials
1080
+ }) {
1081
+ requireRealDirectory(claudeDir);
1082
+ const current = readSessionCredentials({ claudeDir });
1083
+ if (!supersedes({ credentials, current }))
1084
+ return false;
1085
+ const temporary = join6(claudeDir, `.credentials.json.maesttro-${randomBytes3(8).toString("hex")}`);
1086
+ const fd = openSync(temporary, CREATE_NEW_NO_FOLLOW, 384);
1087
+ try {
1088
+ withFd(fd, () => {
1089
+ writeFileSync6(fd, JSON.stringify({
1090
+ ...isObject(current) ? current : {},
1091
+ ...withoutRefreshToken(credentials)
1092
+ }));
1093
+ fchmodSync(fd, 384);
1094
+ });
1095
+ renameSync4(temporary, credentialsPath(claudeDir));
1096
+ } catch (error) {
1097
+ rmSync4(temporary, { force: true });
1098
+ throw error;
1099
+ }
1100
+ return true;
1101
+ }
1102
+ function readSessionCredentials({
1103
+ claudeDir
1104
+ }) {
130
1105
  try {
131
- const token = JSON.parse(Buffer.from(encoded.trim(), "base64").toString("utf8"));
132
- return typeof token.access_token === "string" ? token : undefined;
1106
+ return withFd(openSync(credentialsPath(claudeDir), READ_NO_FOLLOW_NO_BLOCK), (fd) => fstatSync(fd).isFile() ? JSON.parse(readFileSync6(fd, "utf8")) : undefined);
133
1107
  } catch {
134
1108
  return;
135
1109
  }
136
1110
  }
137
- function keychainStore(run = runCommand) {
138
- const identity = ["-s", SERVICE, "-a", ACCOUNT];
139
- return {
140
- kind: "macOS keychain",
141
- load: () => {
142
- const result = run("security", [
143
- "find-generic-password",
144
- ...identity,
145
- "-w"
146
- ]);
147
- return result.status === 0 ? decode(result.stdout) : undefined;
148
- },
149
- save: (token) => {
150
- const command = `add-generic-password -U ${identity.join(" ")} -w ${encode(token)}
151
- `;
152
- const result = run("security", ["-i"], command);
153
- if (result.status !== 0)
154
- throw new Error("Could not write to keychain");
155
- },
156
- clear: () => {
157
- run("security", ["delete-generic-password", ...identity]);
1111
+ function removeHostWrittenCredentials({
1112
+ claudeDir
1113
+ }) {
1114
+ requireRealDirectory(claudeDir);
1115
+ const path = credentialsPath(claudeDir);
1116
+ const entry = entryAt(path);
1117
+ if (!entry)
1118
+ return;
1119
+ const inSessionLogin = entry.isFile() && carriesRefreshToken(readSessionCredentials({ claudeDir }));
1120
+ if (!inSessionLogin)
1121
+ unlinkSync(path);
1122
+ }
1123
+
1124
+ // src/claude-refresher.ts
1125
+ var LOCK_STALE_MS = 2 * 60 * 1000;
1126
+ var LOCK_HEARTBEAT_MS = 20 * 1000;
1127
+ var SESSION_SYNC_MS = 60 * 1000;
1128
+ var LAUNCH_RETRY_MS = 60 * 1000;
1129
+ var WAITING_FOR_REFRESH = "Waiting for another maesttro's Claude refresh…";
1130
+
1131
+ class ClaudeLoginExpired extends Error {
1132
+ constructor() {
1133
+ super("Claude login expired: run `maesttro login claude`");
1134
+ }
1135
+ }
1136
+
1137
+ class ClaudeRefreshFailed extends Error {
1138
+ constructor(reason) {
1139
+ super(`Couldn't refresh the Claude login (will retry): ${reason}`);
1140
+ }
1141
+ }
1142
+
1143
+ class ClaudeLoggedOut extends Error {
1144
+ constructor() {
1145
+ super("Not logged in to Claude: run `maesttro login claude`");
1146
+ }
1147
+ }
1148
+ function claudeLockPath() {
1149
+ return join7(maesttroHome(), "claude-refresh.lock");
1150
+ }
1151
+ function lockToken() {
1152
+ return `${process.pid}-${randomBytes4(8).toString("hex")}`;
1153
+ }
1154
+ function tryAcquire(path, token) {
1155
+ let fd;
1156
+ try {
1157
+ fd = openSync2(path, "wx", 384);
1158
+ } catch (error) {
1159
+ if (error.code === "EEXIST")
1160
+ return false;
1161
+ throw error;
1162
+ }
1163
+ try {
1164
+ writeSync(fd, token);
1165
+ } catch (error) {
1166
+ rmSync5(path, { force: true });
1167
+ throw error;
1168
+ } finally {
1169
+ closeSync2(fd);
1170
+ }
1171
+ return true;
1172
+ }
1173
+ function isStale(path, now, staleMs) {
1174
+ try {
1175
+ return now - statSync(path).mtimeMs > staleMs;
1176
+ } catch {
1177
+ return false;
1178
+ }
1179
+ }
1180
+ function reclaimStaleLock({
1181
+ path,
1182
+ now,
1183
+ staleMs
1184
+ }) {
1185
+ const staging = `${path}.stale-${lockToken()}`;
1186
+ try {
1187
+ renameSync5(path, staging);
1188
+ } catch (error) {
1189
+ if (error.code === "ENOENT")
1190
+ return;
1191
+ throw error;
1192
+ }
1193
+ if (!isStale(staging, now, staleMs)) {
1194
+ try {
1195
+ linkSync(staging, path);
1196
+ } catch {}
1197
+ }
1198
+ rmSync5(staging, { force: true });
1199
+ }
1200
+ function touchIfOwned(path, token) {
1201
+ try {
1202
+ if (readFileSync7(path, "utf8") === token) {
1203
+ const now = new Date;
1204
+ utimesSync(path, now, now);
158
1205
  }
159
- };
1206
+ } catch {}
160
1207
  }
161
- function secretToolStore(run = runCommand) {
162
- const attributes = ["service", SERVICE, "account", ACCOUNT];
163
- return {
164
- kind: "Secret Service (secret-tool)",
165
- load: () => {
166
- const result = run("secret-tool", ["lookup", ...attributes]);
167
- return result.status === 0 ? decode(result.stdout) : undefined;
168
- },
169
- save: (token) => {
170
- const result = run("secret-tool", ["store", `--label=${SERVICE}`, ...attributes], encode(token));
171
- if (result.status !== 0) {
172
- throw new Error("Could not write to the Secret Service");
1208
+ function releaseIfOwned(path, token) {
1209
+ try {
1210
+ if (readFileSync7(path, "utf8") === token)
1211
+ rmSync5(path, { force: true });
1212
+ } catch {}
1213
+ }
1214
+ function sleepFor2(ms) {
1215
+ return new Promise((resolve) => setTimeout(resolve, ms));
1216
+ }
1217
+ async function withRefreshLock({
1218
+ path,
1219
+ now = Date.now,
1220
+ staleMs = LOCK_STALE_MS,
1221
+ pollMs = 500,
1222
+ sleep = sleepFor2,
1223
+ onWait = () => {},
1224
+ timers = nodeTimers
1225
+ }, action) {
1226
+ mkdirSync7(dirname4(path), { recursive: true, mode: 448 });
1227
+ const token = lockToken();
1228
+ let waited = false;
1229
+ while (!tryAcquire(path, token)) {
1230
+ if (isStale(path, now(), staleMs)) {
1231
+ reclaimStaleLock({ path, now: now(), staleMs });
1232
+ } else {
1233
+ if (!waited)
1234
+ onWait();
1235
+ waited = true;
1236
+ await sleep(pollMs);
1237
+ }
1238
+ }
1239
+ const heartbeat = timers.setInterval(() => touchIfOwned(path, token), LOCK_HEARTBEAT_MS);
1240
+ try {
1241
+ return await action();
1242
+ } finally {
1243
+ timers.clearInterval(heartbeat);
1244
+ releaseIfOwned(path, token);
1245
+ }
1246
+ }
1247
+ function isDue({
1248
+ credentials,
1249
+ now
1250
+ }) {
1251
+ return credentials.claudeAiOauth.expiresAt - now <= REFRESH_LEAD_MS;
1252
+ }
1253
+ var FINISHING_REFRESH = "Finishing the Claude refresh before exiting… (press Ctrl-C again to abort)";
1254
+ var FINISHING_BACKGROUND_REFRESH = "Finishing a Claude refresh before exiting…";
1255
+ var runKeeper = ({ master, now, workDir }) => refreshWithKeeper({ master, now, workDir });
1256
+ function loadMaster(store) {
1257
+ const master = store.load();
1258
+ if (!master)
1259
+ throw new ClaudeLoggedOut;
1260
+ return master;
1261
+ }
1262
+ function keeperLeftovers(workDir) {
1263
+ try {
1264
+ return readdirSync2(workDir).map((name) => join7(workDir, name)).filter((path) => lstatSync3(path).isDirectory());
1265
+ } catch {
1266
+ return [];
1267
+ }
1268
+ }
1269
+ function discardLeftovers(workDir) {
1270
+ for (const dir of keeperLeftovers(workDir)) {
1271
+ rmSync5(dir, { recursive: true, force: true });
1272
+ }
1273
+ }
1274
+ function discardKeeperLeftovers({
1275
+ workDir = keeperWorkDir()
1276
+ } = {}) {
1277
+ discardLeftovers(workDir);
1278
+ }
1279
+ function saveRotated({
1280
+ store,
1281
+ credentials,
1282
+ workDir,
1283
+ onRotated
1284
+ }) {
1285
+ try {
1286
+ store.save(credentials);
1287
+ } catch (error) {
1288
+ onRotated(credentials);
1289
+ throw new ClaudeRefreshFailed(`couldn't save the refreshed login: ${messageOf(error)}`);
1290
+ }
1291
+ discardLeftovers(workDir);
1292
+ onRotated(credentials);
1293
+ }
1294
+ function adoptLeftover({
1295
+ master,
1296
+ workDir
1297
+ }) {
1298
+ let best;
1299
+ for (const dir of keeperLeftovers(workDir)) {
1300
+ const candidate = readKeeperCredentials(dir);
1301
+ if (!isMasterLogin(candidate))
1302
+ continue;
1303
+ const { accessToken, expiresAt } = candidate.claudeAiOauth;
1304
+ const newest = best ?? master;
1305
+ if (accessToken !== master.claudeAiOauth.accessToken && expiresAt > newest.claudeAiOauth.expiresAt) {
1306
+ best = { claudeAiOauth: candidate.claudeAiOauth };
1307
+ }
1308
+ }
1309
+ return best;
1310
+ }
1311
+ function sameLogin(current, expected) {
1312
+ return current?.claudeAiOauth.accessToken === expected.claudeAiOauth.accessToken;
1313
+ }
1314
+ function replacedMeanwhile({
1315
+ store,
1316
+ handed,
1317
+ workDir
1318
+ }) {
1319
+ const current = store.load();
1320
+ if (sameLogin(current, handed))
1321
+ return;
1322
+ if (!current)
1323
+ throw new ClaudeLoggedOut;
1324
+ discardLeftovers(workDir);
1325
+ return current;
1326
+ }
1327
+ async function clearEarlierKeepers(clearLingering) {
1328
+ try {
1329
+ await clearLingering();
1330
+ } catch (error) {
1331
+ throw new ClaudeRefreshFailed(`couldn't stop an earlier keeper: ${messageOf(error)}`);
1332
+ }
1333
+ }
1334
+ async function refreshMaster({
1335
+ store,
1336
+ keeper = runKeeper,
1337
+ lockPath = claudeLockPath(),
1338
+ workDir = keeperWorkDir(),
1339
+ clearLingering = () => removeLingeringKeepers(),
1340
+ now = Date.now,
1341
+ announce,
1342
+ sleep,
1343
+ signals,
1344
+ onRotated = () => {}
1345
+ }) {
1346
+ const loaded = loadMaster(store);
1347
+ if (keeperLeftovers(workDir).length === 0) {
1348
+ if (loaded.rejectedAt !== undefined)
1349
+ throw new ClaudeLoginExpired;
1350
+ if (!isDue({ credentials: loaded, now: now() }))
1351
+ return loaded;
1352
+ }
1353
+ const onWait = announce ? () => announce(WAITING_FOR_REFRESH) : undefined;
1354
+ const onHeld = announce ? () => announce(FINISHING_REFRESH) : undefined;
1355
+ let hold;
1356
+ try {
1357
+ return await withRefreshLock({ path: lockPath, now, sleep, onWait }, async () => {
1358
+ if (signals)
1359
+ hold = holdSignals({ host: signals, onHeld });
1360
+ await clearEarlierKeepers(clearLingering);
1361
+ let stored = store.load();
1362
+ if (!stored)
1363
+ throw new ClaudeLoggedOut;
1364
+ const adopted = adoptLeftover({ master: stored, workDir });
1365
+ if (adopted) {
1366
+ saveRotated({ store, credentials: adopted, workDir, onRotated });
1367
+ stored = adopted;
173
1368
  }
174
- },
175
- clear: () => {
176
- run("secret-tool", ["clear", ...attributes]);
1369
+ discardLeftovers(workDir);
1370
+ if (stored.rejectedAt !== undefined)
1371
+ throw new ClaudeLoginExpired;
1372
+ if (!isDue({ credentials: stored, now: now() }))
1373
+ return stored;
1374
+ if (hold?.pending()) {
1375
+ throw new ClaudeRefreshFailed("interrupted before the keeper ran");
1376
+ }
1377
+ const outcome = await keeper({ master: stored, now: now(), workDir });
1378
+ if (outcome.kind === "inconclusive") {
1379
+ throw new ClaudeRefreshFailed(outcome.reason);
1380
+ }
1381
+ const newer = replacedMeanwhile({ store, handed: stored, workDir });
1382
+ if (newer)
1383
+ return newer;
1384
+ if (outcome.kind === "rejected") {
1385
+ try {
1386
+ store.save(markedRejected({ credentials: stored, now: now() }));
1387
+ } catch {}
1388
+ throw new ClaudeLoginExpired;
1389
+ }
1390
+ saveRotated({
1391
+ store,
1392
+ credentials: outcome.credentials,
1393
+ workDir,
1394
+ onRotated
1395
+ });
1396
+ return outcome.credentials;
1397
+ });
1398
+ } finally {
1399
+ hold?.release();
1400
+ }
1401
+ }
1402
+ function runningSessionIds({
1403
+ run = runCommand
1404
+ } = {}) {
1405
+ const result = run("docker", ["ps", "--format", "{{.Names}}"]);
1406
+ if (result.status !== 0)
1407
+ return [];
1408
+ return result.stdout.split(`
1409
+ `).map((name) => name.trim()).filter((name) => name.startsWith("maesttro-"));
1410
+ }
1411
+ function messageOf(error) {
1412
+ return error instanceof Error ? error.message : String(error);
1413
+ }
1414
+ function pushCredentials({
1415
+ credentials,
1416
+ claudeDirs,
1417
+ announce
1418
+ }) {
1419
+ for (const claudeDir of new Set(claudeDirs)) {
1420
+ if (!existsSync(claudeDir))
1421
+ continue;
1422
+ try {
1423
+ writeSessionCredentials({ claudeDir, credentials });
1424
+ } catch (error) {
1425
+ announce(`Couldn't update the Claude login in ${claudeDir}: ${messageOf(error)}`);
177
1426
  }
178
- };
1427
+ }
179
1428
  }
180
- function fileStore(path = join(homedir(), ".config", "maesttro", "credentials.json")) {
1429
+ function announcingOnce(announce, alreadySaid) {
1430
+ let last = alreadySaid;
181
1431
  return {
182
- kind: `file (${path})`,
183
- load: () => {
184
- try {
185
- return decode(readFileSync(path, "utf8"));
186
- } catch {
1432
+ say: (message) => {
1433
+ if (message === last)
187
1434
  return;
188
- }
189
- },
190
- save: (token) => {
191
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
192
- const temporary = `${path}.tmp`;
193
- writeFileSync(temporary, encode(token), { mode: 384 });
194
- chmodSync(temporary, 384);
195
- renameSync(temporary, path);
1435
+ last = message;
1436
+ announce(message);
196
1437
  },
197
- clear: () => {
198
- rmSync(path, { force: true });
1438
+ reset: () => {
1439
+ last = undefined;
199
1440
  }
200
1441
  };
201
1442
  }
202
- function selectCredentialStore({
203
- platform = process.platform,
204
- env = process.env,
205
- run = runCommand
206
- } = {}) {
207
- if (env.MAESTTRO_CREDENTIAL_STORE === "file")
208
- return fileStore();
209
- if (platform === "darwin")
210
- return keychainStore(run);
211
- if (platform === "linux" && commandExists(run, "secret-tool")) {
212
- return secretToolStore(run);
213
- }
214
- return fileStore();
1443
+ function syncSessionCredentials({
1444
+ store,
1445
+ claudeDir,
1446
+ now = Date.now
1447
+ }) {
1448
+ const master = store.load();
1449
+ if (!master || master.claudeAiOauth.expiresAt <= now())
1450
+ return;
1451
+ writeSessionCredentials({ claudeDir, credentials: master });
1452
+ }
1453
+ function pushToRunningSessions({
1454
+ credentials,
1455
+ claudeDir,
1456
+ run = runCommand,
1457
+ claudeDirOf = (id) => sessionClaudeDir({ id }),
1458
+ announce
1459
+ }) {
1460
+ pushCredentials({
1461
+ credentials,
1462
+ claudeDirs: [claudeDir, ...runningSessionIds({ run }).map(claudeDirOf)],
1463
+ announce
1464
+ });
1465
+ }
1466
+ function startClaudeRefresher({
1467
+ claudeDir,
1468
+ store,
1469
+ keeper,
1470
+ lockPath,
1471
+ workDir,
1472
+ clearLingering,
1473
+ run = runCommand,
1474
+ claudeDirOf = (id) => sessionClaudeDir({ id }),
1475
+ announce,
1476
+ launchFailure,
1477
+ now = Date.now,
1478
+ timers = nodeTimers
1479
+ }) {
1480
+ const initial = store.load();
1481
+ if (!initial)
1482
+ return;
1483
+ const refreshNotices = announcingOnce(announce, launchFailure);
1484
+ const syncNotices = announcingOnce(announce);
1485
+ const refresher = startRefresher({
1486
+ initial,
1487
+ expiresAtOf: (credentials) => credentials.claudeAiOauth.expiresAt,
1488
+ firstAttemptAt: launchFailure === undefined ? undefined : now() + LAUNCH_RETRY_MS,
1489
+ refresh: () => refreshMaster({
1490
+ store,
1491
+ keeper,
1492
+ lockPath,
1493
+ workDir,
1494
+ clearLingering,
1495
+ now,
1496
+ onRotated: (credentials) => pushToRunningSessions({
1497
+ credentials,
1498
+ claudeDir,
1499
+ run,
1500
+ claudeDirOf,
1501
+ announce: refreshNotices.say
1502
+ })
1503
+ }),
1504
+ onRefreshed: (credentials) => {
1505
+ refreshNotices.reset();
1506
+ pushCredentials({
1507
+ credentials,
1508
+ claudeDirs: [claudeDir],
1509
+ announce: refreshNotices.say
1510
+ });
1511
+ },
1512
+ onFailed: (error) => refreshNotices.say(messageOf(error)),
1513
+ now,
1514
+ timers
1515
+ });
1516
+ const sync = timers.setInterval(() => {
1517
+ try {
1518
+ syncSessionCredentials({ store, claudeDir, now });
1519
+ syncNotices.reset();
1520
+ } catch (error) {
1521
+ syncNotices.say(messageOf(error));
1522
+ }
1523
+ }, SESSION_SYNC_MS);
1524
+ return {
1525
+ stop: () => {
1526
+ timers.clearInterval(sync);
1527
+ return refresher.stop();
1528
+ },
1529
+ busy: refresher.busy
1530
+ };
215
1531
  }
216
1532
 
217
1533
  // src/github-api.ts
@@ -403,7 +1719,7 @@ function loginDependencies() {
403
1719
  function describeExpiry(iso) {
404
1720
  return iso ? new Date(iso).toLocaleString() : "never";
405
1721
  }
406
- async function login() {
1722
+ async function loginGitHub() {
407
1723
  const dependencies = loginDependencies();
408
1724
  const token = await deviceLogin(dependencies);
409
1725
  const name = await githubLogin(token.access_token);
@@ -413,137 +1729,245 @@ async function ensureLogin() {
413
1729
  const dependencies = loginDependencies();
414
1730
  const token = await currentUserToken({ ...dependencies, interactive: true });
415
1731
  const name = token && await githubLogin(token.access_token);
416
- console.log(name ? `Logged in to GitHub as ${name}. Stored in ${dependencies.store.kind}.` : "GitHub login did not complete. Run `maesttro login` to retry.");
1732
+ console.log(name ? `Logged in to GitHub as ${name}. Stored in ${dependencies.store.kind}.` : "GitHub login did not complete. Run `maesttro login github` to retry.");
417
1733
  }
418
- function logout() {
1734
+ function logoutGitHub() {
419
1735
  const { store } = loginDependencies();
420
1736
  store.clear();
421
1737
  console.log(`Logged out. Removed GitHub credentials from ${store.kind}.`);
422
1738
  }
423
- async function status() {
1739
+ async function statusGitHub() {
424
1740
  const dependencies = loginDependencies();
425
1741
  const token = await currentUserToken({ ...dependencies, interactive: false });
426
1742
  if (!token) {
427
- console.log("Not logged in to GitHub. Run `maesttro login`.");
1743
+ console.log("Not logged in to GitHub. Run `maesttro login github`.");
428
1744
  process.exitCode = 1;
429
1745
  return;
430
1746
  }
431
1747
  const name = await githubLogin(token.access_token);
432
1748
  console.log([
433
- `GitHub: ${name ? `logged in as ${name}` : "token rejected — run `maesttro login`"}`,
1749
+ `GitHub: ${name ? `logged in as ${name}` : "token rejected — run `maesttro login github`"}`,
434
1750
  `Store: ${dependencies.store.kind}`,
435
1751
  `Access token expires: ${describeExpiry(token.expires_at)}`,
436
1752
  `Refresh token expires: ${describeExpiry(token.refresh_expires_at)}`
437
1753
  ].join(`
438
1754
  `));
439
- if (!name)
440
- process.exitCode = 1;
1755
+ if (!name)
1756
+ process.exitCode = 1;
1757
+ }
1758
+ function parseLoginTargets(args) {
1759
+ const [target, ...rest] = args;
1760
+ if (rest.length > 0)
1761
+ return;
1762
+ if (target === undefined)
1763
+ return ["github", "claude"];
1764
+ return target === "github" || target === "claude" ? [target] : undefined;
1765
+ }
1766
+ function claudeLogin({
1767
+ capture = () => captureLogin(),
1768
+ store = claudeStore(),
1769
+ forgetLeftovers = () => discardKeeperLeftovers(),
1770
+ announce = (message) => console.log(message)
1771
+ } = {}) {
1772
+ announce("Logging in to Claude in a throwaway runner (opens a URL; paste the code back)...");
1773
+ const credentials = capture();
1774
+ if (!credentials) {
1775
+ announce("Claude login did not complete. Run `maesttro login claude` to retry.");
1776
+ return false;
1777
+ }
1778
+ store.save(credentials);
1779
+ forgetLeftovers();
1780
+ announce(`Logged in to Claude. Stored in ${store.kind}.`);
1781
+ return true;
1782
+ }
1783
+ function describeClaudeStatus({
1784
+ credentials,
1785
+ now
1786
+ }) {
1787
+ if (!credentials) {
1788
+ return {
1789
+ loggedIn: false,
1790
+ lines: ["Claude: not logged in — run `maesttro login claude`"]
1791
+ };
1792
+ }
1793
+ const { expiresAt, refreshTokenExpiresAt } = credentials.claudeAiOauth;
1794
+ const loggedIn = claudeLoginState({ credentials, now }) === "active";
1795
+ const warning = claudeLoginWarning({ credentials, now });
1796
+ return {
1797
+ loggedIn,
1798
+ lines: [
1799
+ loggedIn ? "Claude: logged in" : "Claude: login expired — run `maesttro login claude`",
1800
+ ...warning ? [`Warning: ${warning}`] : [],
1801
+ `Access token expires: ${new Date(expiresAt).toLocaleString()}`,
1802
+ `Refresh token expires: ${refreshTokenExpiresAt ? new Date(refreshTokenExpiresAt).toLocaleString() : "unknown"}`
1803
+ ]
1804
+ };
1805
+ }
1806
+ function unknownTarget(command) {
1807
+ console.log(`Usage: maesttro ${command} [github|claude]`);
1808
+ process.exitCode = 1;
1809
+ }
1810
+ async function login({ args }) {
1811
+ const targets = parseLoginTargets(args);
1812
+ if (!targets)
1813
+ return unknownTarget("login");
1814
+ if (targets.includes("github"))
1815
+ await loginGitHub();
1816
+ if (targets.includes("claude") && !claudeLogin())
1817
+ process.exitCode = 1;
1818
+ }
1819
+ function logout({ args }) {
1820
+ const targets = parseLoginTargets(args);
1821
+ if (!targets) {
1822
+ unknownTarget("logout");
1823
+ return;
1824
+ }
1825
+ if (targets.includes("github"))
1826
+ logoutGitHub();
1827
+ if (targets.includes("claude")) {
1828
+ const store = claudeStore();
1829
+ store.clear();
1830
+ discardKeeperLeftovers();
1831
+ console.log(`Logged out. Removed Claude credentials from ${store.kind}.`);
1832
+ }
1833
+ }
1834
+ async function status({ args }) {
1835
+ const targets = parseLoginTargets(args);
1836
+ if (!targets)
1837
+ return unknownTarget("status");
1838
+ if (targets.includes("github"))
1839
+ await statusGitHub();
1840
+ if (targets.includes("claude")) {
1841
+ const { loggedIn, lines } = describeClaudeStatus({
1842
+ credentials: claudeStore().load(),
1843
+ now: Date.now()
1844
+ });
1845
+ console.log(lines.join(`
1846
+ `));
1847
+ if (!loggedIn)
1848
+ process.exitCode = 1;
1849
+ }
1850
+ }
1851
+
1852
+ // src/clients.ts
1853
+ var CLIENTS = {
1854
+ claude: {
1855
+ label: "Claude Code",
1856
+ npmPackage: "@anthropic-ai/claude-code",
1857
+ binary: "claude",
1858
+ configDir: "/home/agent/.claude"
1859
+ },
1860
+ opencode: {
1861
+ label: "opencode",
1862
+ npmPackage: "opencode-ai",
1863
+ binary: "opencode",
1864
+ configDir: "/home/agent/.config/opencode"
1865
+ }
1866
+ };
1867
+ var DEFAULT_CLIENT = "opencode";
1868
+ function isClientKey(value) {
1869
+ return Object.hasOwn(CLIENTS, value);
441
1870
  }
442
1871
 
443
1872
  // src/configure.ts
444
1873
  import {
445
- cpSync as cpSync2,
446
- existsSync as existsSync2,
447
- mkdirSync as mkdirSync4,
448
- readdirSync,
449
- readFileSync as readFileSync4,
450
- writeFileSync as writeFileSync4
1874
+ existsSync as existsSync3,
1875
+ mkdirSync as mkdirSync10,
1876
+ readdirSync as readdirSync4,
1877
+ readFileSync as readFileSync10,
1878
+ writeFileSync as writeFileSync9
451
1879
  } from "node:fs";
452
1880
  import { homedir as homedir2 } from "node:os";
453
- import { dirname as dirname5, join as join4 } from "node:path";
1881
+ import { dirname as dirname7, join as join11 } from "node:path";
454
1882
 
455
- // src/client-configs.ts
456
- import {
457
- chmodSync as chmodSync2,
458
- cpSync,
459
- existsSync,
460
- mkdirSync as mkdirSync3,
461
- mkdtempSync,
462
- readFileSync as readFileSync3,
463
- rmSync as rmSync2,
464
- writeFileSync as writeFileSync3
465
- } from "node:fs";
466
- import { tmpdir } from "node:os";
467
- import { dirname as dirname3, join as join2 } from "node:path";
1883
+ // src/claude-state.ts
1884
+ import { readFileSync as readFileSync8, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "node:fs";
1885
+ import { join as join8 } from "node:path";
468
1886
 
469
- // src/json-file.ts
470
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
471
- import { dirname as dirname2 } from "node:path";
472
- function isObject(value) {
473
- return typeof value === "object" && value !== null && !Array.isArray(value);
474
- }
475
- function mergeJson(existing, curated) {
476
- const merged = { ...existing };
477
- for (const [key, value] of Object.entries(curated)) {
478
- const current = merged[key];
479
- merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
480
- }
481
- return merged;
1887
+ // src/runner-paths.ts
1888
+ var CONTAINER_REPO_PATH = "/workspace";
1889
+
1890
+ // src/claude-state.ts
1891
+ function seededClaudeState({
1892
+ existing,
1893
+ claudeVersion
1894
+ }) {
1895
+ const projects = isObject(existing.projects) ? existing.projects : {};
1896
+ return {
1897
+ ...existing,
1898
+ hasCompletedOnboarding: true,
1899
+ ...claudeVersion ? { lastOnboardingVersion: claudeVersion } : {},
1900
+ theme: existing.theme ?? "dark",
1901
+ projects: mergeJson(projects, {
1902
+ [CONTAINER_REPO_PATH]: { hasTrustDialogAccepted: true }
1903
+ })
1904
+ };
482
1905
  }
483
- function readJson(path) {
1906
+ function installedClaudeVersion(root = "/usr/local/lib/node_modules/@anthropic-ai/claude-code") {
484
1907
  try {
485
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
486
- return isObject(parsed) ? parsed : {};
1908
+ return JSON.parse(readFileSync8(join8(root, "package.json"), "utf8")).version;
487
1909
  } catch {
488
- return {};
1910
+ return;
489
1911
  }
490
1912
  }
491
- function writeJson(path, value) {
492
- mkdirSync2(dirname2(path), { recursive: true });
493
- writeFileSync2(path, `${JSON.stringify(value, null, 2)}
1913
+ function seedClaudeState({
1914
+ home,
1915
+ claudeVersion = installedClaudeVersion()
1916
+ }) {
1917
+ const path = join8(home, ".claude.json");
1918
+ const state = seededClaudeState({ existing: readJson(path), claudeVersion });
1919
+ writeFileSync7(`${path}.tmp`, `${JSON.stringify(state, null, 2)}
494
1920
  `);
1921
+ renameSync6(`${path}.tmp`, path);
495
1922
  }
496
- function skipInsignificant(text, from) {
497
- let index = from;
498
- while (index < text.length) {
499
- if (/\s/.test(text[index] ?? "")) {
500
- index++;
501
- } else if (text.startsWith("//", index)) {
502
- while (index < text.length && text[index] !== `
503
- `)
504
- index++;
505
- } else if (text.startsWith("/*", index)) {
506
- const end = text.indexOf("*/", index + 2);
507
- index = end === -1 ? text.length : end + 2;
508
- } else {
509
- return index;
510
- }
511
- }
512
- return index;
513
- }
514
- function stripJsonc(text) {
515
- let output = "";
516
- let index = 0;
517
- while (index < text.length) {
518
- const char = text[index];
519
- if (char === '"') {
520
- const start = index;
521
- index++;
522
- while (index < text.length && text[index] !== '"') {
523
- index += text[index] === "\\" ? 2 : 1;
524
- }
525
- index++;
526
- output += text.slice(start, index);
1923
+
1924
+ // src/client-configs.ts
1925
+ import {
1926
+ chmodSync as chmodSync5,
1927
+ existsSync as existsSync2,
1928
+ mkdirSync as mkdirSync9,
1929
+ mkdtempSync as mkdtempSync2,
1930
+ readFileSync as readFileSync9,
1931
+ rmSync as rmSync7,
1932
+ writeFileSync as writeFileSync8
1933
+ } from "node:fs";
1934
+ import { tmpdir as tmpdir2 } from "node:os";
1935
+ import { dirname as dirname6, join as join10 } from "node:path";
1936
+
1937
+ // src/copy-tree.ts
1938
+ import {
1939
+ chmodSync as chmodSync4,
1940
+ copyFileSync,
1941
+ lstatSync as lstatSync4,
1942
+ mkdirSync as mkdirSync8,
1943
+ readdirSync as readdirSync3,
1944
+ readlinkSync,
1945
+ rmSync as rmSync6,
1946
+ symlinkSync
1947
+ } from "node:fs";
1948
+ import { dirname as dirname5, isAbsolute, join as join9, resolve as resolve2 } from "node:path";
1949
+ function copyTree(source, target, filter = () => true) {
1950
+ mkdirSync8(target, { recursive: true });
1951
+ for (const name of readdirSync3(source)) {
1952
+ const from = join9(source, name);
1953
+ if (!filter(from))
1954
+ continue;
1955
+ const to = join9(target, name);
1956
+ const stat = lstatSync4(from);
1957
+ if (stat.isSymbolicLink()) {
1958
+ const link = readlinkSync(from);
1959
+ rmSync6(to, { force: true });
1960
+ symlinkSync(isAbsolute(link) ? link : resolve2(dirname5(from), link), to);
527
1961
  continue;
528
1962
  }
529
- const significant = skipInsignificant(text, index);
530
- if (significant > index) {
531
- output += " ";
532
- index = significant;
1963
+ if (stat.isDirectory()) {
1964
+ copyTree(from, to, filter);
533
1965
  continue;
534
1966
  }
535
- const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
536
- if (!trailingComma)
537
- output += char;
538
- index++;
1967
+ rmSync6(to, { force: true });
1968
+ copyFileSync(from, to);
1969
+ chmodSync4(to, stat.mode & 511);
539
1970
  }
540
- return output;
541
- }
542
- function parseJsonc(text) {
543
- const parsed = JSON.parse(stripJsonc(text));
544
- if (!isObject(parsed))
545
- throw new Error("expected a JSON object");
546
- return parsed;
547
1971
  }
548
1972
 
549
1973
  // src/maesttro-endpoint.ts
@@ -577,7 +2001,7 @@ async function fetchOrchestratorWindow({
577
2001
  }
578
2002
 
579
2003
  // src/provider-configs.ts
580
- var PROVIDER_ID = "maesttro";
2004
+ var PROVIDER_ID = "esatto";
581
2005
  var MAESTTRO_API_KEY = "maesttro";
582
2006
  var DGX_SPARK_PROVIDER_ID = "dgx-spark";
583
2007
  var DGX_SPARK_URL = "http://100.64.0.1:8001";
@@ -604,7 +2028,7 @@ function opencodeProviders(url) {
604
2028
  name: "Maesttro",
605
2029
  options: { baseURL: `${url}/v1`, apiKey: MAESTTRO_API_KEY },
606
2030
  models: {
607
- [ORCHESTRATOR_MODEL]: { name: "Orchestrate", ...IMAGE_INPUT }
2031
+ [ORCHESTRATOR_MODEL]: { name: "Maesttro", ...IMAGE_INPUT }
608
2032
  }
609
2033
  },
610
2034
  [DGX_SPARK_PROVIDER_ID]: {
@@ -621,10 +2045,11 @@ function opencodeProviders(url) {
621
2045
  }
622
2046
  };
623
2047
  }
624
- function opencodeConfig(base, url) {
2048
+ function opencodeConfig(base, url, isStrict = false) {
625
2049
  const merged = mergeJson({ $schema: "https://opencode.ai/config.json", ...base }, {
626
2050
  model: `${PROVIDER_ID}/${ORCHESTRATOR_MODEL}`,
627
- compaction: { auto: true, keep: { tokens: 24000 }, buffer: 32000 }
2051
+ compaction: { auto: true, keep: { tokens: 24000 }, buffer: 32000 },
2052
+ ...isStrict ? {} : { permission: "allow" }
628
2053
  });
629
2054
  const enabled = stringList(base.enabled_providers);
630
2055
  const disabled = stringList(base.disabled_providers);
@@ -647,7 +2072,8 @@ function opencodeConfig(base, url) {
647
2072
  }
648
2073
  function claudeSettings({
649
2074
  url,
650
- orchestratorWindow
2075
+ orchestratorWindow,
2076
+ isStrict
651
2077
  }) {
652
2078
  return {
653
2079
  env: {
@@ -656,7 +2082,8 @@ function claudeSettings({
656
2082
  ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: "Maesttro",
657
2083
  ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION: "Maesttro orchestrator",
658
2084
  ...orchestratorWindow ? { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(orchestratorWindow) } : {}
659
- }
2085
+ },
2086
+ ...isStrict ? {} : { permissions: { defaultMode: "bypassPermissions" } }
660
2087
  };
661
2088
  }
662
2089
 
@@ -758,11 +2185,11 @@ function readHostFile({
758
2185
  parse,
759
2186
  warn
760
2187
  }) {
761
- const path = join2(home, file);
762
- if (!existsSync(path))
2188
+ const path = join10(home, file);
2189
+ if (!existsSync2(path))
763
2190
  return {};
764
2191
  try {
765
- return parse(readFileSync3(path, "utf8"));
2192
+ return parse(readFileSync9(path, "utf8"));
766
2193
  } catch {
767
2194
  warn(`maesttro: ignoring your ~/${file}, it doesn't parse`);
768
2195
  return {};
@@ -783,77 +2210,54 @@ function json(value) {
783
2210
  function buildClientConfigs({
784
2211
  host,
785
2212
  url,
786
- orchestratorWindow
2213
+ orchestratorWindow,
2214
+ isStrict
787
2215
  }) {
788
2216
  const opencode = redactSecrets(opencodeHostBase(host.opencode));
789
2217
  return {
790
2218
  files: {
791
- [OPENCODE_CONFIG]: json(opencodeConfig(opencode.value, url)),
792
- [CLAUDE_SETTINGS]: json(claudeSettings({ url, orchestratorWindow }))
2219
+ [OPENCODE_CONFIG]: json(opencodeConfig(opencode.value, url, isStrict)),
2220
+ [CLAUDE_SETTINGS]: json(claudeSettings({ url, orchestratorWindow, isStrict }))
793
2221
  },
794
2222
  removed: opencode.removed.map((path) => `opencode ${path}`)
795
2223
  };
796
2224
  }
797
- function writeClientConfigDir(files, parent = tmpdir()) {
798
- const root = mkdtempSync(join2(parent, "maesttro-clients-"));
799
- chmodSync2(root, 448);
800
- const dir = join2(root, "home");
2225
+ function writeClientConfigDir(files, parent = tmpdir2()) {
2226
+ const root = mkdtempSync2(join10(parent, "maesttro-clients-"));
2227
+ chmodSync5(root, 448);
2228
+ const dir = join10(root, "home");
801
2229
  for (const [file, content] of Object.entries(files)) {
802
- const path = join2(dir, file);
803
- mkdirSync3(dirname3(path), { recursive: true, mode: 493 });
804
- writeFileSync3(path, content);
805
- chmodSync2(path, 420);
2230
+ const path = join10(dir, file);
2231
+ mkdirSync9(dirname6(path), { recursive: true, mode: 493 });
2232
+ writeFileSync8(path, content);
2233
+ chmodSync5(path, 420);
806
2234
  }
807
- chmodSync2(dir, 493);
808
- return { dir, remove: () => rmSync2(root, { recursive: true, force: true }) };
2235
+ chmodSync5(dir, 493);
2236
+ return { dir, remove: () => rmSync7(root, { recursive: true, force: true }) };
809
2237
  }
810
2238
  function installClientConfigs({
811
2239
  home,
812
2240
  source = CONTAINER_CLIENT_CONFIGS
813
2241
  }) {
814
- if (!existsSync(source))
2242
+ if (!existsSync2(source))
815
2243
  return false;
816
- const merged = MERGED_INTO_HOME.map((file) => join2(source, file));
817
- cpSync(source, home, {
818
- recursive: true,
819
- force: true,
820
- filter: (path) => !merged.includes(path)
821
- });
2244
+ const merged = MERGED_INTO_HOME.map((file) => join10(source, file));
2245
+ copyTree(source, home, (path) => !merged.includes(path));
822
2246
  for (const file of MERGED_INTO_HOME) {
823
- const staged = join2(source, file);
824
- if (!existsSync(staged))
2247
+ const staged = join10(source, file);
2248
+ if (!existsSync2(staged))
825
2249
  continue;
826
- const target = join2(home, file);
2250
+ const target = join10(home, file);
827
2251
  writeJson(target, mergeJson(readJson(target), readJson(staged)));
828
2252
  }
829
2253
  return true;
830
2254
  }
831
2255
  function injectOpencodeTuiState({ home }) {
832
- const path = join2(home, OPENCODE_TUI_STATE);
2256
+ const path = join10(home, OPENCODE_TUI_STATE);
833
2257
  writeJson(path, mergeJson(readJson(path), CURATED_OPENCODE_TUI_STATE));
834
2258
  return path;
835
2259
  }
836
2260
 
837
- // src/clients.ts
838
- var CLIENTS = {
839
- claude: {
840
- label: "Claude Code",
841
- npmPackage: "@anthropic-ai/claude-code",
842
- binary: "claude",
843
- configDir: "/home/agent/.claude"
844
- },
845
- opencode: {
846
- label: "opencode",
847
- npmPackage: "opencode-ai",
848
- binary: "opencode",
849
- configDir: "/home/agent/.config/opencode"
850
- }
851
- };
852
- var DEFAULT_CLIENT = "opencode";
853
- function isClientKey(value) {
854
- return Object.hasOwn(CLIENTS, value);
855
- }
856
-
857
2261
  // src/host-instructions.ts
858
2262
  var HOST_STAGING_DIR = "/opt/maesttro-host";
859
2263
  var STAGED_DIRECTORIES = [
@@ -867,14 +2271,6 @@ var INSTRUCTION_FILES = [
867
2271
  ];
868
2272
  var STAGED_PATHS = [...INSTRUCTION_FILES, ...STAGED_DIRECTORIES];
869
2273
 
870
- // src/paths.ts
871
- import { dirname as dirname4, join as join3, resolve } from "node:path";
872
- import { fileURLToPath } from "node:url";
873
- var PACKAGE_ROOT = resolve(dirname4(fileURLToPath(import.meta.url)), "..");
874
- var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
875
- var IMAGE = "maesttro-runner:latest";
876
- var VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
877
-
878
2274
  // src/runner-context.ts
879
2275
  function githubMode(value) {
880
2276
  return value === "on" || value === "strict" ? value : "off";
@@ -927,7 +2323,7 @@ function runnerInstructions(context) {
927
2323
  "",
928
2324
  "- Preinstalled: Node 24 (npm, pnpm, yarn), TypeScript (`tsc`), Bun, Python 3.14 and uv (`uv venv`, `uv pip`, `uv tool`; plain `pip` and `python -m venv` work too), the .NET 10 SDK, a C/C++ build toolchain, and common CLI tools (jq, ripgrep, fd, unzip, zip, sqlite3).",
929
2325
  "- You can install more yourself without root: `npm i -g`, `bun add -g`, `pip install`, `uv tool install`, `uv python install`, `dotnet tool install -g`. They work as usual and are on `PATH` right away.",
930
- "- Installs last only until this container exits; install again when a new session needs them. Don't install into your home directory to make them stick.",
2326
+ "- Installs last only until this container exits; install again when a new session needs them. Your home directory belongs to this session: it survives `maesttro resume` but no other session sees it.",
931
2327
  "- System packages are unavailable: there is no root, `sudo` or `apt`. If a task needs one, tell the user which package and why.",
932
2328
  "",
933
2329
  "## Pushing",
@@ -954,24 +2350,24 @@ function withoutComments(value) {
954
2350
  }
955
2351
  function applyProviderConfigs({
956
2352
  home,
957
- configsDir = join4(DOCKERFILE_DIR, "configs")
2353
+ configsDir = join11(DOCKERFILE_DIR, "configs")
958
2354
  }) {
959
2355
  const written = [];
960
2356
  for (const [key, client] of Object.entries(CLIENTS)) {
961
- const source = join4(configsDir, key);
962
- if (!existsSync2(source))
2357
+ const source = join11(configsDir, key);
2358
+ if (!existsSync3(source))
963
2359
  continue;
964
- const targetDir = join4(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
965
- for (const name of readdirSync(source)) {
2360
+ const targetDir = join11(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
2361
+ for (const name of readdirSync4(source)) {
966
2362
  if (!name.endsWith(".json"))
967
2363
  continue;
968
- const curated = withoutComments(readJson(join4(source, name)));
2364
+ const curated = withoutComments(readJson(join11(source, name)));
969
2365
  if (Object.keys(curated).length === 0)
970
2366
  continue;
971
- const target = join4(targetDir, name);
972
- mkdirSync4(targetDir, { recursive: true });
2367
+ const target = join11(targetDir, name);
2368
+ mkdirSync10(targetDir, { recursive: true });
973
2369
  const merged = mergeJson(readJson(target), curated);
974
- writeFileSync4(target, `${JSON.stringify(merged, null, 2)}
2370
+ writeFileSync9(target, `${JSON.stringify(merged, null, 2)}
975
2371
  `);
976
2372
  written.push(target);
977
2373
  }
@@ -984,27 +2380,27 @@ function stageHostInstructions({
984
2380
  context
985
2381
  }) {
986
2382
  for (const directory of STAGED_DIRECTORIES) {
987
- const source = join4(staging, directory);
988
- if (!existsSync2(source))
2383
+ const source = join11(staging, directory);
2384
+ if (!existsSync3(source))
989
2385
  continue;
990
2386
  try {
991
- cpSync2(source, join4(home, directory), { recursive: true, force: true });
2387
+ copyTree(source, join11(home, directory));
992
2388
  } catch {}
993
2389
  }
994
2390
  const instructions = runnerInstructions(context);
995
2391
  for (const file of INSTRUCTION_FILES) {
996
- const source = join4(staging, file);
997
- const hostCopy = existsSync2(source) ? readFileSync4(source, "utf8") : "";
998
- const target = join4(home, file);
999
- mkdirSync4(dirname5(target), { recursive: true });
1000
- writeFileSync4(target, hostCopy ? `${hostCopy.trimEnd()}
2392
+ const source = join11(staging, file);
2393
+ const hostCopy = existsSync3(source) ? readFileSync10(source, "utf8") : "";
2394
+ const target = join11(home, file);
2395
+ mkdirSync10(dirname7(target), { recursive: true });
2396
+ writeFileSync9(target, hostCopy ? `${hostCopy.trimEnd()}
1001
2397
 
1002
2398
  ${instructions}` : instructions);
1003
2399
  }
1004
2400
  }
1005
2401
  function configure({
1006
2402
  home = homedir2(),
1007
- inRunner = existsSync2(RUNNER_MARKER),
2403
+ inRunner = existsSync3(RUNNER_MARKER),
1008
2404
  env = process.env,
1009
2405
  staging = HOST_STAGING_DIR,
1010
2406
  clientConfigs
@@ -1013,6 +2409,7 @@ function configure({
1013
2409
  if (!inRunner)
1014
2410
  return;
1015
2411
  installClientConfigs({ home, source: clientConfigs });
2412
+ seedClaudeState({ home });
1016
2413
  injectOpencodeTuiState({ home });
1017
2414
  stageHostInstructions({
1018
2415
  home,
@@ -1095,52 +2492,6 @@ async function revokeSessionToken(token, fetchImpl = fetch, apiUrl = githubApiUr
1095
2492
  } catch {}
1096
2493
  }
1097
2494
 
1098
- // src/host-identity.ts
1099
- function hostIdentityEnv() {
1100
- if (typeof process.getuid !== "function" || typeof process.getgid !== "function") {
1101
- return {};
1102
- }
1103
- return {
1104
- MAESTTRO_HOST_UID: String(process.getuid()),
1105
- MAESTTRO_HOST_GID: String(process.getgid())
1106
- };
1107
- }
1108
- function hostIdentityFlags() {
1109
- return Object.entries(hostIdentityEnv()).map(([name, value]) => `-e ${name}=${value}`).join(" ");
1110
- }
1111
-
1112
- // src/legacy-token.ts
1113
- import { execSync } from "node:child_process";
1114
- var LEGACY_HOSTS = "~/.config/gh/hosts.yml";
1115
- var PURGE_LEGACY_TOKEN = [
1116
- `if [ -e ${LEGACY_HOSTS} ]; then rm -f ${LEGACY_HOSTS}; echo FOUND; fi`,
1117
- "git config --global --remove-section credential.https://github.com 2>/dev/null || true",
1118
- "git config --global --remove-section credential.https://gist.github.com 2>/dev/null || true"
1119
- ].join("; ");
1120
- var DETECT_LEGACY_TOKEN = `if [ -e ${LEGACY_HOSTS} ]; then echo FOUND; else echo CLEAN; fi`;
1121
- function inVolume(command) {
1122
- return execSync(`docker run --rm ${hostIdentityFlags()} -v ${VOLUME}:/home/agent ${IMAGE} "${command}"`).toString();
1123
- }
1124
- function purgeLegacyToken() {
1125
- return inVolume(PURGE_LEGACY_TOKEN).includes("FOUND");
1126
- }
1127
- function hasLegacyToken() {
1128
- return inVolume(DETECT_LEGACY_TOKEN).includes("FOUND");
1129
- }
1130
- 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";
1131
- function purgeLegacyTokenOnce({
1132
- config,
1133
- saveConfig,
1134
- purge = purgeLegacyToken,
1135
- announce
1136
- }) {
1137
- if (config.legacyTokenPurged)
1138
- return;
1139
- if (purge())
1140
- announce(REVOKE_NOTICE);
1141
- saveConfig({ ...config, legacyTokenPurged: true });
1142
- }
1143
-
1144
2495
  // src/network-probe.ts
1145
2496
  import { connect } from "node:net";
1146
2497
  async function reachable(host, port, {
@@ -1166,8 +2517,8 @@ async function reachable(host, port, {
1166
2517
  }
1167
2518
 
1168
2519
  // src/repo.ts
1169
- import { appendFileSync, mkdirSync as mkdirSync5, realpathSync } from "node:fs";
1170
- import { join as join5 } from "node:path";
2520
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync11, realpathSync } from "node:fs";
2521
+ import { join as join12 } from "node:path";
1171
2522
  var MINIMUM_GIT = [2, 48];
1172
2523
  var WORKTREES_DIR = ".worktrees";
1173
2524
 
@@ -1210,7 +2561,7 @@ function repoRoot(run, cwd) {
1210
2561
  "--git-common-dir"
1211
2562
  ]).out;
1212
2563
  const root = realpathSync(top.out);
1213
- if (realpathSync(commonDir) !== join5(root, ".git")) {
2564
+ if (realpathSync(commonDir) !== join12(root, ".git")) {
1214
2565
  throw new LaunchError("This is a git worktree. Run maesttro from the main checkout; each session gets its own isolated clone.");
1215
2566
  }
1216
2567
  if (realpathSync(cwd) !== root) {
@@ -1237,56 +2588,143 @@ function defaultBranch(run, root) {
1237
2588
  throw new LaunchError("Can't find the default branch of origin. Run `git remote set-head origin --auto`.");
1238
2589
  }
1239
2590
  function ensureWorktreesDir(run, root) {
1240
- mkdirSync5(join5(root, WORKTREES_DIR), { recursive: true });
2591
+ mkdirSync11(join12(root, WORKTREES_DIR), { recursive: true });
1241
2592
  if (git(run, root, ["check-ignore", "-q", `${WORKTREES_DIR}/`]).ok)
1242
2593
  return;
1243
- const info = join5(root, ".git", "info");
1244
- mkdirSync5(info, { recursive: true });
1245
- appendFileSync(join5(info, "exclude"), `
1246
- ${WORKTREES_DIR}/
1247
- `);
1248
- }
1249
- function snapshotRefs(run, root) {
1250
- const listed = git(run, root, [
1251
- "for-each-ref",
1252
- "--format=%(refname) %(objectname)",
1253
- "refs/heads",
1254
- "refs/tags"
1255
- ]);
1256
- const refs = {};
1257
- for (const line of listed.out.split(`
1258
- `)) {
1259
- const [name, sha] = line.split(" ");
1260
- if (name && sha)
1261
- refs[name] = sha;
2594
+ const info = join12(root, ".git", "info");
2595
+ mkdirSync11(info, { recursive: true });
2596
+ appendFileSync2(join12(info, "exclude"), `
2597
+ ${WORKTREES_DIR}/
2598
+ `);
2599
+ }
2600
+ function snapshotRefs(run, root) {
2601
+ const listed = git(run, root, [
2602
+ "for-each-ref",
2603
+ "--format=%(refname) %(objectname)",
2604
+ "refs/heads",
2605
+ "refs/tags"
2606
+ ]);
2607
+ const refs = {};
2608
+ for (const line of listed.out.split(`
2609
+ `)) {
2610
+ const [name, sha] = line.split(" ");
2611
+ if (name && sha)
2612
+ refs[name] = sha;
2613
+ }
2614
+ return refs;
2615
+ }
2616
+ function revision(run, root, rev) {
2617
+ const parsed = git(run, root, [
2618
+ "rev-parse",
2619
+ "--verify",
2620
+ "--quiet",
2621
+ `${rev}^{commit}`
2622
+ ]);
2623
+ return parsed.ok ? parsed.out : undefined;
2624
+ }
2625
+
2626
+ // src/volume-migration.ts
2627
+ import { execSync } from "node:child_process";
2628
+ var LEGACY_HOSTS = "~/.config/gh/hosts.yml";
2629
+ var PURGE_LEGACY_TOKEN = [
2630
+ `if [ -e ${LEGACY_HOSTS} ]; then rm -f ${LEGACY_HOSTS}; echo FOUND; fi`,
2631
+ "git config --global --remove-section credential.https://github.com 2>/dev/null || true",
2632
+ "git config --global --remove-section credential.https://gist.github.com 2>/dev/null || true"
2633
+ ].join("; ");
2634
+ 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";
2635
+ var OLD_SESSIONS_RUNNING = `Old sessions still use ${LEGACY_VOLUME}; will move the Claude login once they exit.`;
2636
+ function dockerExec(command) {
2637
+ try {
2638
+ return execSync(command, { stdio: ["ignore", "pipe", "pipe"] }).toString();
2639
+ } catch (error) {
2640
+ const stderr = error?.stderr;
2641
+ throw new Error(stderr ? stderr.toString().trim() : error.message);
2642
+ }
2643
+ }
2644
+ function legacyVolumeExists({
2645
+ exec = dockerExec
2646
+ } = {}) {
2647
+ try {
2648
+ exec(`docker volume inspect ${LEGACY_VOLUME}`);
2649
+ return true;
2650
+ } catch (error) {
2651
+ if (error instanceof Error && /no such volume/i.test(error.message)) {
2652
+ return false;
2653
+ }
2654
+ throw error;
2655
+ }
2656
+ }
2657
+ function dockerVolume({
2658
+ exec = dockerExec
2659
+ } = {}) {
2660
+ const inVolume = (command) => exec(`docker run --rm ${hostIdentityFlags()} -v ${LEGACY_VOLUME}:/home/agent ${IMAGE} "${command}"`);
2661
+ return {
2662
+ exists: () => legacyVolumeExists({ exec }),
2663
+ inUse: () => exec(`docker ps -q --filter volume=${LEGACY_VOLUME}`).trim().length > 0,
2664
+ readClaudeCredentials: () => {
2665
+ const output = inVolume("cat ~/.claude/.credentials.json 2>/dev/null || echo null");
2666
+ try {
2667
+ return JSON.parse(output);
2668
+ } catch {
2669
+ return;
2670
+ }
2671
+ },
2672
+ purgeGitHubToken: () => inVolume(PURGE_LEGACY_TOKEN).includes("FOUND")
2673
+ };
2674
+ }
2675
+ function migrateFromVolumeOnce({
2676
+ config,
2677
+ saveConfig,
2678
+ store,
2679
+ volume = dockerVolume(),
2680
+ announce
2681
+ }) {
2682
+ if (config.volumeMigrated)
2683
+ return;
2684
+ try {
2685
+ if (volume.exists()) {
2686
+ if (!config.legacyTokenPurged && volume.purgeGitHubToken()) {
2687
+ announce(REVOKE_NOTICE);
2688
+ }
2689
+ if (volume.inUse()) {
2690
+ announce(OLD_SESSIONS_RUNNING);
2691
+ saveConfig({ ...config, legacyTokenPurged: true });
2692
+ return;
2693
+ }
2694
+ const credentials = volume.readClaudeCredentials();
2695
+ if (!store.load() && isMasterLogin(credentials)) {
2696
+ store.save(credentials);
2697
+ announce(`Moved the runner's Claude login into ${store.kind}.`);
2698
+ }
2699
+ announce(`Sessions no longer use the shared ${LEGACY_VOLUME} volume. Remove it with \`docker volume rm ${LEGACY_VOLUME}\` once nothing runs.`);
2700
+ }
2701
+ } catch {
2702
+ announce(`Couldn't check the legacy ${LEGACY_VOLUME} volume; will retry next launch.`);
2703
+ return;
1262
2704
  }
1263
- return refs;
1264
- }
1265
- function revision(run, root, rev) {
1266
- const parsed = git(run, root, [
1267
- "rev-parse",
1268
- "--verify",
1269
- "--quiet",
1270
- `${rev}^{commit}`
1271
- ]);
1272
- return parsed.ok ? parsed.out : undefined;
2705
+ saveConfig({ ...config, volumeMigrated: true });
1273
2706
  }
1274
2707
 
1275
2708
  // src/doctor.ts
1276
- async function check(label, probe) {
2709
+ class DoctorWarning extends Error {
2710
+ }
2711
+ async function check(label, probe, print = console.log) {
1277
2712
  try {
1278
2713
  const detail = await probe();
1279
- console.log(` [ok] ${label}${detail ? ` — ${detail}` : ""}`);
2714
+ print(` [ok] ${label}${detail ? ` — ${detail}` : ""}`);
1280
2715
  return true;
1281
2716
  } catch (error) {
2717
+ if (error instanceof DoctorWarning) {
2718
+ print(` [warn] ${label} — ${error.message}`);
2719
+ return true;
2720
+ }
1282
2721
  const reason = error instanceof Error && error.message ? error.message : "";
1283
- console.log(` [fail] ${label}${reason ? ` — ${reason}` : ""}`);
2722
+ print(` [fail] ${label}${reason ? ` — ${reason}` : ""}`);
1284
2723
  return false;
1285
2724
  }
1286
2725
  }
1287
- function inRunner(command, { withVolume = true } = {}) {
1288
- const volume = withVolume ? `-v ${VOLUME}:/home/agent ` : "";
1289
- return execSync2(`docker run --rm ${hostIdentityFlags()} ${volume}${IMAGE} "${command}"`).toString();
2726
+ function inRunner(command) {
2727
+ return execSync2(`docker run --rm ${hostIdentityFlags()} ${IMAGE} "${command}"`).toString();
1290
2728
  }
1291
2729
  async function doctor() {
1292
2730
  console.log(`== maesttro doctor ==
@@ -1310,19 +2748,6 @@ async function doctor() {
1310
2748
  "Runner image built",
1311
2749
  () => `built ${execSync2(`docker image inspect ${IMAGE} --format "{{.Created}}"`).toString().trim()}`
1312
2750
  ],
1313
- [
1314
- "Persistent volume exists",
1315
- () => {
1316
- execSync2(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
1317
- }
1318
- ],
1319
- [
1320
- "No long-lived GitHub token in the volume",
1321
- () => {
1322
- if (hasLegacyToken())
1323
- throw new Error("run `maesttro setup` to remove it");
1324
- }
1325
- ],
1326
2751
  [
1327
2752
  "Logged in to GitHub",
1328
2753
  async () => {
@@ -1333,13 +2758,36 @@ async function doctor() {
1333
2758
  interactive: false
1334
2759
  });
1335
2760
  if (!token)
1336
- throw new Error("run `maesttro login`");
2761
+ throw new Error("run `maesttro login github`");
1337
2762
  const name = await githubLogin(token.access_token);
1338
2763
  if (!name)
1339
- throw new Error("token rejected, run `maesttro login`");
2764
+ throw new Error("token rejected, run `maesttro login github`");
1340
2765
  return name;
1341
2766
  }
1342
2767
  ],
2768
+ [
2769
+ "Logged in to Claude",
2770
+ () => {
2771
+ const credentials = claudeStore().load();
2772
+ if (!credentials)
2773
+ throw new Error("run `maesttro login claude`");
2774
+ if (claudeLoginState({ credentials, now: Date.now() }) === "expired") {
2775
+ throw new ClaudeLoginExpired;
2776
+ }
2777
+ const warning = claudeLoginWarning({ credentials, now: Date.now() });
2778
+ if (warning)
2779
+ throw new DoctorWarning(warning);
2780
+ return `access token until ${new Date(credentials.claudeAiOauth.expiresAt).toLocaleString()}`;
2781
+ }
2782
+ ],
2783
+ [
2784
+ "Old shared volume removed",
2785
+ () => {
2786
+ if (legacyVolumeExists()) {
2787
+ throw new Error(`docker volume rm ${LEGACY_VOLUME}`);
2788
+ }
2789
+ }
2790
+ ],
1343
2791
  [
1344
2792
  "Token broker reachable",
1345
2793
  async () => {
@@ -1371,7 +2819,7 @@ async function doctor() {
1371
2819
  ...Object.values(CLIENTS).map((client) => [
1372
2820
  `Client available: ${client.label}`,
1373
2821
  () => {
1374
- inRunner(`which ${client.binary}`, { withVolume: false });
2822
+ inRunner(`which ${client.binary}`);
1375
2823
  }
1376
2824
  ])
1377
2825
  ];
@@ -1395,7 +2843,9 @@ Usage:
1395
2843
  maesttro <client> Same, with a specific client — see \`maesttro list\`
1396
2844
  maesttro --strict [client]
1397
2845
  Same, in strict mode: no GitHub token reaches the runner, so \`gh\` is off
1398
- maesttro resume <id> Continue a kept session in its clone, in the mode it started with
2846
+ maesttro resume [client] <id>
2847
+ Continue a kept session in its clone, in the mode it started with,
2848
+ with its last client unless one is given
1399
2849
  (\`--strict\` or \`--no-strict\` after the id changes it)
1400
2850
  maesttro sessions List this repo's sessions
1401
2851
  maesttro sessions remove <id>
@@ -1406,74 +2856,35 @@ Usage:
1406
2856
  maesttro push [branch] [--force]
1407
2857
  Push a branch a session created, after showing what goes out
1408
2858
  maesttro list Show available clients
1409
- maesttro setup One-time: build image, create volume, log in to GitHub
1410
- maesttro login Log in to GitHub (device flow) so sessions get repo access
1411
- maesttro logout Remove the stored GitHub credentials
1412
- maesttro status Show GitHub login status
2859
+ maesttro setup One-time: build image, move off the old shared volume, log in to GitHub and Claude
2860
+ maesttro login [github|claude]
2861
+ Log in to GitHub (device flow) and/or Claude; without a target, does both
2862
+ maesttro logout [github|claude]
2863
+ Remove the stored credentials; without a target, does both
2864
+ maesttro status [github|claude]
2865
+ Show login status; without a target, does both
1413
2866
  maesttro configure Apply maesttro's client configs (also runs inside the runner)
1414
2867
  maesttro update Update the package and rebuild the runner image with refreshed client configs
1415
- maesttro doctor Health check — Docker, image, volume, GitHub login, broker, isolation, clients
2868
+ maesttro doctor Health check — Docker, image, GitHub and Claude logins, old volume gone, broker, isolation, clients
1416
2869
  maesttro help Show this message
1417
2870
  `;
1418
2871
 
1419
2872
  // src/launch.ts
1420
- import { execSync as execSync3, spawn, spawnSync as spawnSync2 } from "node:child_process";
1421
- import { existsSync as existsSync4, rmSync as rmSync6 } from "node:fs";
1422
- import { constants, homedir as homedir4 } from "node:os";
1423
- import { join as join11 } from "node:path";
2873
+ import { execSync as execSync3, spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
2874
+ import { existsSync as existsSync5, rmSync as rmSync10 } from "node:fs";
2875
+ import { constants as constants2, homedir as homedir3 } from "node:os";
2876
+ import { join as join16 } from "node:path";
1424
2877
 
1425
2878
  // src/backup.ts
1426
2879
  import { createHash } from "node:crypto";
1427
- import { mkdirSync as mkdirSync7, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1428
- import { basename, join as join7 } from "node:path";
1429
-
1430
- // src/maesttro-home.ts
1431
- import {
1432
- appendFileSync as appendFileSync2,
1433
- mkdirSync as mkdirSync6,
1434
- readFileSync as readFileSync5,
1435
- renameSync as renameSync2,
1436
- writeFileSync as writeFileSync5
1437
- } from "node:fs";
1438
- import { homedir as homedir3 } from "node:os";
1439
- import { join as join6 } from "node:path";
1440
- function maesttroHome() {
1441
- return join6(homedir3(), ".config", "maesttro");
1442
- }
1443
- function configPath() {
1444
- return join6(maesttroHome(), "config.json");
1445
- }
1446
- function readMaesttroConfig() {
1447
- try {
1448
- return JSON.parse(readFileSync5(configPath(), "utf8"));
1449
- } catch {
1450
- return {};
1451
- }
1452
- }
1453
- function writeMaesttroConfig(config) {
1454
- mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
1455
- const temporary = `${configPath()}.tmp`;
1456
- writeFileSync5(temporary, `${JSON.stringify(config, null, 2)}
1457
- `);
1458
- renameSync2(temporary, configPath());
1459
- }
1460
- function fileSessionLog(path = join6(maesttroHome(), "maesttro.log")) {
1461
- return (event, detail) => {
1462
- try {
1463
- mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
1464
- appendFileSync2(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
1465
- `);
1466
- } catch {}
1467
- };
1468
- }
1469
-
1470
- // src/backup.ts
2880
+ import { mkdirSync as mkdirSync12, readdirSync as readdirSync5, rmSync as rmSync8 } from "node:fs";
2881
+ import { basename, join as join13 } from "node:path";
1471
2882
  var BACKUPS_KEPT = 5;
1472
2883
  function shortHash(value) {
1473
2884
  return createHash("sha256").update(value).digest("hex").slice(0, 8);
1474
2885
  }
1475
2886
  function backupDir(repoRoot, home = maesttroHome()) {
1476
- return join7(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
2887
+ return join13(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
1477
2888
  }
1478
2889
  function refsFingerprint(refs) {
1479
2890
  const sorted = Object.keys(refs).sort().map((ref) => `${ref} ${refs[ref]}`);
@@ -1491,119 +2902,41 @@ function backupRepo({
1491
2902
  }) {
1492
2903
  if (Object.keys(refs).length === 0)
1493
2904
  return;
1494
- mkdirSync7(dir, { recursive: true, mode: 448 });
2905
+ mkdirSync12(dir, { recursive: true, mode: 448 });
1495
2906
  const fingerprint = refsFingerprint(refs);
1496
- const existing = readdirSync2(dir).filter((name) => name.endsWith(".bundle")).sort();
2907
+ const existing = readdirSync5(dir).filter((name) => name.endsWith(".bundle")).sort();
1497
2908
  const latest = existing.at(-1);
1498
2909
  if (latest?.endsWith(`-${fingerprint}.bundle`)) {
1499
- return { path: join7(dir, latest), reused: true };
2910
+ return { path: join13(dir, latest), reused: true };
1500
2911
  }
1501
2912
  const stamp = now.toISOString().replace(/[:.]/g, "-");
1502
- const path = join7(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
2913
+ const path = join13(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
1503
2914
  if (!git(run, repoRoot, ["bundle", "create", "--quiet", path, "--all"]).ok) {
1504
2915
  return;
1505
2916
  }
1506
2917
  for (const stale of [...existing, basename(path)].sort().slice(0, -keep)) {
1507
- rmSync3(join7(dir, stale), { force: true });
2918
+ rmSync8(join13(dir, stale), { force: true });
1508
2919
  }
1509
2920
  return { path, reused: false };
1510
2921
  }
1511
2922
 
1512
- // src/refresher.ts
1513
- var REFRESH_LEAD_MS = 10 * 60 * 1000;
1514
- var WALL_CLOCK_TICK_MS = 60 * 1000;
1515
- var FIRST_BACKOFF_MS = 5 * 1000;
1516
- var MAX_BACKOFF_MS = 5 * 60 * 1000;
1517
- var nodeTimers = {
1518
- setTimeout: (callback, ms) => setTimeout(callback, ms).unref(),
1519
- clearTimeout: (handle) => clearTimeout(handle),
1520
- setInterval: (callback, ms) => setInterval(callback, ms).unref(),
1521
- clearInterval: (handle) => clearInterval(handle)
1522
- };
1523
- function startRefresher({
1524
- initial,
1525
- refresh,
1526
- onRefreshed,
1527
- onFailed,
1528
- now = Date.now,
1529
- timers = nodeTimers
1530
- }) {
1531
- let current = initial;
1532
- let nextAttemptAt = 0;
1533
- let backoffMs = FIRST_BACKOFF_MS;
1534
- let inFlight;
1535
- let stopped = false;
1536
- let pending;
1537
- function refreshDueAt(token) {
1538
- const expiresAt = Date.parse(token.expires_at);
1539
- const halfRemaining = (expiresAt - now()) / 2;
1540
- return expiresAt - Math.min(REFRESH_LEAD_MS, Math.max(halfRemaining, 0));
1541
- }
1542
- nextAttemptAt = refreshDueAt(initial);
1543
- function scheduleAt(at) {
1544
- timers.clearTimeout(pending);
1545
- pending = timers.setTimeout(check, Math.max(at - now(), 0));
1546
- }
1547
- async function attempt() {
1548
- try {
1549
- const token = await refresh();
1550
- if (stopped)
1551
- return;
1552
- current = token;
1553
- backoffMs = FIRST_BACKOFF_MS;
1554
- nextAttemptAt = refreshDueAt(token);
1555
- onRefreshed(token);
1556
- } catch (error) {
1557
- if (stopped)
1558
- return;
1559
- nextAttemptAt = now() + backoffMs;
1560
- onFailed(error, backoffMs);
1561
- backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
1562
- }
1563
- scheduleAt(nextAttemptAt);
1564
- }
1565
- function check() {
1566
- if (stopped)
1567
- return Promise.resolve();
1568
- if (inFlight)
1569
- return inFlight;
1570
- if (now() < nextAttemptAt)
1571
- return Promise.resolve();
1572
- inFlight = attempt().finally(() => {
1573
- inFlight = undefined;
1574
- });
1575
- return inFlight;
1576
- }
1577
- const tick = timers.setInterval(check, WALL_CLOCK_TICK_MS);
1578
- scheduleAt(nextAttemptAt);
1579
- return {
1580
- current: () => current,
1581
- check,
1582
- stop: () => {
1583
- stopped = true;
1584
- timers.clearTimeout(pending);
1585
- timers.clearInterval(tick);
1586
- }
1587
- };
1588
- }
1589
-
1590
2923
  // src/session-dir.ts
1591
2924
  import {
1592
- chmodSync as chmodSync3,
1593
- mkdirSync as mkdirSync8,
1594
- mkdtempSync as mkdtempSync2,
1595
- renameSync as renameSync3,
1596
- rmSync as rmSync4,
1597
- writeFileSync as writeFileSync6
2925
+ chmodSync as chmodSync6,
2926
+ mkdirSync as mkdirSync13,
2927
+ mkdtempSync as mkdtempSync3,
2928
+ renameSync as renameSync7,
2929
+ rmSync as rmSync9,
2930
+ writeFileSync as writeFileSync10
1598
2931
  } from "node:fs";
1599
- import { tmpdir as tmpdir2 } from "node:os";
1600
- import { join as join8 } from "node:path";
2932
+ import { tmpdir as tmpdir3 } from "node:os";
2933
+ import { join as join14 } from "node:path";
1601
2934
  var CONTAINER_GH_CONFIG_DIR = "/home/agent/.config/gh-maesttro";
1602
2935
  function writeAtomically(path, content) {
1603
2936
  const temporary = `${path}.tmp`;
1604
- writeFileSync6(temporary, content, { mode: 420 });
1605
- chmodSync3(temporary, 420);
1606
- renameSync3(temporary, path);
2937
+ writeFileSync10(temporary, content, { mode: 420 });
2938
+ chmodSync6(temporary, 420);
2939
+ renameSync7(temporary, path);
1607
2940
  }
1608
2941
  function hostsYaml(token, user) {
1609
2942
  return [
@@ -1615,20 +2948,20 @@ function hostsYaml(token, user) {
1615
2948
  ].join(`
1616
2949
  `);
1617
2950
  }
1618
- function createSessionDir(parent = tmpdir2()) {
1619
- const root = mkdtempSync2(join8(parent, "maesttro-"));
1620
- chmodSync3(root, 448);
1621
- const ghDir = join8(root, "gh");
1622
- mkdirSync8(ghDir);
1623
- chmodSync3(ghDir, 493);
1624
- writeAtomically(join8(ghDir, "config.yml"), `version: "1"
2951
+ function createSessionDir(parent = tmpdir3()) {
2952
+ const root = mkdtempSync3(join14(parent, "maesttro-"));
2953
+ chmodSync6(root, 448);
2954
+ const ghDir = join14(root, "gh");
2955
+ mkdirSync13(ghDir);
2956
+ chmodSync6(ghDir, 493);
2957
+ writeAtomically(join14(ghDir, "config.yml"), `version: "1"
1625
2958
  `);
1626
2959
  return {
1627
2960
  ghDir,
1628
- writeToken: (token, user) => writeAtomically(join8(ghDir, "hosts.yml"), hostsYaml(token, user)),
1629
- writeStatus: (status) => writeAtomically(join8(ghDir, "status"), `${status}
2961
+ writeToken: (token, user) => writeAtomically(join14(ghDir, "hosts.yml"), hostsYaml(token, user)),
2962
+ writeStatus: (status) => writeAtomically(join14(ghDir, "status"), `${status}
1630
2963
  `),
1631
- remove: () => rmSync4(root, { recursive: true, force: true })
2964
+ remove: () => rmSync9(root, { recursive: true, force: true })
1632
2965
  };
1633
2966
  }
1634
2967
 
@@ -1646,7 +2979,7 @@ function refusalNotice(code, repo) {
1646
2979
  return `${UNAVAILABLE}: your GitHub account cannot see ${repo} — ${WITHOUT}`;
1647
2980
  case "user_token_invalid":
1648
2981
  case "not_logged_in":
1649
- return `${UNAVAILABLE}: GitHub login expired, run \`maesttro login\` — ${WITHOUT}`;
2982
+ return `${UNAVAILABLE}: GitHub login expired, run \`maesttro login github\` — ${WITHOUT}`;
1650
2983
  default:
1651
2984
  return `${UNAVAILABLE} (${code}) — ${WITHOUT}`;
1652
2985
  }
@@ -1733,6 +3066,7 @@ async function openGitHubSession({
1733
3066
  log("session.token_minted", `${repo} expires ${initial.expires_at}`);
1734
3067
  const refresher = startRefresher({
1735
3068
  initial,
3069
+ expiresAtOf: (token) => Date.parse(token.expires_at),
1736
3070
  refresh: () => mint({ interactive: false }),
1737
3071
  onRefreshed: (token) => {
1738
3072
  publish(token);
@@ -1778,15 +3112,15 @@ function ask(question) {
1778
3112
 
1779
3113
  // src/session-clone.ts
1780
3114
  import {
1781
- copyFileSync,
1782
- existsSync as existsSync3,
1783
- lstatSync,
1784
- mkdirSync as mkdirSync9,
1785
- readdirSync as readdirSync3,
1786
- readFileSync as readFileSync6,
1787
- writeFileSync as writeFileSync7
3115
+ copyFileSync as copyFileSync2,
3116
+ existsSync as existsSync4,
3117
+ lstatSync as lstatSync5,
3118
+ mkdirSync as mkdirSync14,
3119
+ readdirSync as readdirSync6,
3120
+ readFileSync as readFileSync11,
3121
+ writeFileSync as writeFileSync11
1788
3122
  } from "node:fs";
1789
- import { join as join9 } from "node:path";
3123
+ import { join as join15 } from "node:path";
1790
3124
 
1791
3125
  // src/sanitize.ts
1792
3126
  function stripControl(text) {
@@ -1873,27 +3207,27 @@ function createSessionClone(run, root, clonePath, baseSha) {
1873
3207
  }
1874
3208
  function realDir(path) {
1875
3209
  try {
1876
- return lstatSync(path).isDirectory();
3210
+ return lstatSync5(path).isDirectory();
1877
3211
  } catch {
1878
3212
  return false;
1879
3213
  }
1880
3214
  }
1881
3215
  function dirents(dir) {
1882
3216
  try {
1883
- return readdirSync3(dir, { withFileTypes: true });
3217
+ return readdirSync6(dir, { withFileTypes: true });
1884
3218
  } catch {
1885
3219
  return [];
1886
3220
  }
1887
3221
  }
1888
3222
  function cloneGitDir(clonePath) {
1889
- const gitDir = join9(clonePath, ".git");
3223
+ const gitDir = join15(clonePath, ".git");
1890
3224
  return realDir(gitDir) ? gitDir : undefined;
1891
3225
  }
1892
3226
  function readSha(path) {
1893
3227
  try {
1894
- if (!lstatSync(path).isFile())
3228
+ if (!lstatSync5(path).isFile())
1895
3229
  return;
1896
- const text = readFileSync6(path, "utf8").trim();
3230
+ const text = readFileSync11(path, "utf8").trim();
1897
3231
  return SHA.test(text) ? text : undefined;
1898
3232
  } catch {
1899
3233
  return;
@@ -1903,7 +3237,7 @@ function packedRefs(gitDir) {
1903
3237
  const refs = new Map;
1904
3238
  let text;
1905
3239
  try {
1906
- text = readFileSync6(join9(gitDir, "packed-refs"), "utf8");
3240
+ text = readFileSync11(join15(gitDir, "packed-refs"), "utf8");
1907
3241
  } catch {
1908
3242
  return refs;
1909
3243
  }
@@ -1928,12 +3262,12 @@ function cloneBranches(clonePath) {
1928
3262
  continue;
1929
3263
  const name = prefix ? `${prefix}/${entry.name}` : entry.name;
1930
3264
  if (entry.isDirectory())
1931
- walk(join9(dir, entry.name), name);
3265
+ walk(join15(dir, entry.name), name);
1932
3266
  else if (entry.isFile())
1933
3267
  names.add(name);
1934
3268
  }
1935
3269
  };
1936
- walk(join9(gitDir, "refs", "heads"), "");
3270
+ walk(join15(gitDir, "refs", "heads"), "");
1937
3271
  for (const ref of packedRefs(gitDir).keys()) {
1938
3272
  if (ref.startsWith(HEADS_PREFIX))
1939
3273
  names.add(ref.slice(HEADS_PREFIX.length));
@@ -1946,7 +3280,7 @@ function branchTip(clonePath, branch) {
1946
3280
  const gitDir = cloneGitDir(clonePath);
1947
3281
  if (!gitDir)
1948
3282
  return;
1949
- return readSha(join9(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
3283
+ return readSha(join15(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
1950
3284
  }
1951
3285
  function cloneHead(clonePath) {
1952
3286
  const gitDir = cloneGitDir(clonePath);
@@ -1954,9 +3288,9 @@ function cloneHead(clonePath) {
1954
3288
  return;
1955
3289
  let text;
1956
3290
  try {
1957
- if (!lstatSync(join9(gitDir, "HEAD")).isFile())
3291
+ if (!lstatSync5(join15(gitDir, "HEAD")).isFile())
1958
3292
  return;
1959
- text = readFileSync6(join9(gitDir, "HEAD"), "utf8").trim();
3293
+ text = readFileSync11(join15(gitDir, "HEAD"), "utf8").trim();
1960
3294
  } catch {
1961
3295
  return;
1962
3296
  }
@@ -1977,29 +3311,29 @@ function copyObjectFiles(fromDir, intoDir) {
1977
3311
  for (const entry of dirents(fromDir)) {
1978
3312
  if (!entry.isFile())
1979
3313
  continue;
1980
- const dest = join9(intoDir, entry.name);
1981
- if (existsSync3(dest))
3314
+ const dest = join15(intoDir, entry.name);
3315
+ if (existsSync4(dest))
1982
3316
  continue;
1983
3317
  if (!created) {
1984
- mkdirSync9(intoDir, { recursive: true });
3318
+ mkdirSync14(intoDir, { recursive: true });
1985
3319
  created = true;
1986
3320
  }
1987
- copyFileSync(join9(fromDir, entry.name), dest);
3321
+ copyFileSync2(join15(fromDir, entry.name), dest);
1988
3322
  }
1989
3323
  }
1990
3324
  function pinAlternates(clonePath, containerObjects) {
1991
3325
  const gitDir = cloneGitDir(clonePath);
1992
3326
  if (!gitDir)
1993
3327
  return;
1994
- const objects = join9(gitDir, "objects");
1995
- const info = join9(objects, "info");
3328
+ const objects = join15(gitDir, "objects");
3329
+ const info = join15(objects, "info");
1996
3330
  if (!realDir(objects) || !realDir(info)) {
1997
3331
  throw new LaunchError(`Unsafe objects tree in the session clone: ${clonePath}`);
1998
3332
  }
1999
- const alternates = join9(info, "alternates");
3333
+ const alternates = join15(info, "alternates");
2000
3334
  let stat;
2001
3335
  try {
2002
- stat = lstatSync(alternates);
3336
+ stat = lstatSync5(alternates);
2003
3337
  } catch {
2004
3338
  return;
2005
3339
  }
@@ -2008,73 +3342,29 @@ function pinAlternates(clonePath, containerObjects) {
2008
3342
  }
2009
3343
  const desired = `${containerObjects}
2010
3344
  `;
2011
- if (readFileSync6(alternates, "utf8") !== desired) {
2012
- writeFileSync7(alternates, desired);
3345
+ if (readFileSync11(alternates, "utf8") !== desired) {
3346
+ writeFileSync11(alternates, desired);
2013
3347
  }
2014
3348
  }
2015
3349
  function importObjects(run, root, clonePath, tip) {
2016
3350
  const gitDir = cloneGitDir(clonePath);
2017
3351
  if (!gitDir)
2018
3352
  return false;
2019
- const objects = join9(gitDir, "objects");
3353
+ const objects = join15(gitDir, "objects");
2020
3354
  if (!realDir(objects))
2021
3355
  return false;
2022
- const into = join9(root, ".git", "objects");
3356
+ const into = join15(root, ".git", "objects");
2023
3357
  for (const entry of dirents(objects)) {
2024
3358
  if (entry.name === "info" || entry.isSymbolicLink() || !entry.isDirectory()) {
2025
3359
  continue;
2026
3360
  }
2027
3361
  if (!OBJECT_SUBDIR.test(entry.name) && entry.name !== "pack")
2028
3362
  continue;
2029
- copyObjectFiles(join9(objects, entry.name), join9(into, entry.name));
3363
+ copyObjectFiles(join15(objects, entry.name), join15(into, entry.name));
2030
3364
  }
2031
3365
  return git(run, root, ["cat-file", "-e", `${tip}^{commit}`]).ok;
2032
3366
  }
2033
3367
 
2034
- // src/sessions.ts
2035
- import { randomBytes } from "node:crypto";
2036
- import {
2037
- mkdirSync as mkdirSync10,
2038
- readdirSync as readdirSync4,
2039
- readFileSync as readFileSync7,
2040
- renameSync as renameSync4,
2041
- rmSync as rmSync5,
2042
- writeFileSync as writeFileSync8
2043
- } from "node:fs";
2044
- import { join as join10 } from "node:path";
2045
- function newSessionId() {
2046
- return `maesttro-${randomBytes(4).toString("hex")}`;
2047
- }
2048
- function sessionsDir(home = maesttroHome()) {
2049
- return join10(home, "sessions");
2050
- }
2051
- function saveSession(record, dir = sessionsDir()) {
2052
- mkdirSync10(dir, { recursive: true, mode: 448 });
2053
- const path = join10(dir, `${record.id}.json`);
2054
- writeFileSync8(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
2055
- `);
2056
- renameSync4(`${path}.tmp`, path);
2057
- }
2058
- function readSession(id, dir = sessionsDir()) {
2059
- try {
2060
- return JSON.parse(readFileSync7(join10(dir, `${id}.json`), "utf8"));
2061
- } catch {
2062
- return;
2063
- }
2064
- }
2065
- function deleteSession(id, dir = sessionsDir()) {
2066
- rmSync5(join10(dir, `${id}.json`), { force: true });
2067
- }
2068
- function sessionsForRepo(repoRoot, dir = sessionsDir()) {
2069
- let names;
2070
- try {
2071
- names = readdirSync4(dir).filter((name) => name.endsWith(".json"));
2072
- } catch {
2073
- return [];
2074
- }
2075
- return names.map((name) => readSession(name.slice(0, -".json".length), dir)).filter((record) => record?.repoRoot === repoRoot).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2076
- }
2077
-
2078
3368
  // src/workspace-git.ts
2079
3369
  var GITHUB_REMOTE = /^(?:https:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/;
2080
3370
  function githubRepoFromRemote(remote) {
@@ -2102,9 +3392,18 @@ function workspaceAuthor(run, workspace) {
2102
3392
  }
2103
3393
 
2104
3394
  // src/launch.ts
2105
- var CONTAINER_REPO = "/workspace";
3395
+ var CONTAINER_REPO = CONTAINER_REPO_PATH;
2106
3396
  var CONTAINER_PACKAGE = "/opt/maesttro";
2107
3397
  var CONTAINER_GIT_OBJECTS = "/opt/maesttro-objects";
3398
+ function sessionClientArgs({
3399
+ args,
3400
+ session
3401
+ }) {
3402
+ const chosen = args.length > 0 ? args : session.client ? [session.client] : [];
3403
+ const [first] = chosen;
3404
+ const client = first === undefined ? DEFAULT_CLIENT : isClientKey(first) ? first : undefined;
3405
+ return { args: chosen, client };
3406
+ }
2108
3407
  function resolveCommand(args) {
2109
3408
  const [first, ...rest] = args;
2110
3409
  if (first === undefined)
@@ -2116,6 +3415,20 @@ function resolveCommand(args) {
2116
3415
  function containerCommand(command) {
2117
3416
  return `node ${CONTAINER_PACKAGE}/dist/maesttro.js configure || echo "maesttro: in-container setup failed" >&2; ${command}`;
2118
3417
  }
3418
+ function sessionEnvFlags({ repo }) {
3419
+ const names = [
3420
+ "MAESTTRO_SESSION_ID",
3421
+ "MAESTTRO_BASE",
3422
+ "MAESTTRO_GITHUB",
3423
+ "MAESTTRO_HOST_UID",
3424
+ "MAESTTRO_HOST_GID",
3425
+ ...repo ? ["MAESTTRO_REPO"] : [],
3426
+ "COLORTERM",
3427
+ "TERM_PROGRAM",
3428
+ "TERM=xterm-256color"
3429
+ ];
3430
+ return names.flatMap((name) => ["-e", name]);
3431
+ }
2119
3432
  function sessionMounts({
2120
3433
  repoRoot,
2121
3434
  sessionDir,
@@ -2123,16 +3436,19 @@ function sessionMounts({
2123
3436
  hostHome,
2124
3437
  stagedPaths,
2125
3438
  clientConfigs,
2126
- sharedSkills
3439
+ sharedSkills,
3440
+ home,
3441
+ claudeDir
2127
3442
  }) {
2128
- const objects = join11(repoRoot, ".git", "objects");
3443
+ const objects = join16(repoRoot, ".git", "objects");
2129
3444
  const mounts = [
2130
- { host: VOLUME, container: "/home/agent", type: "volume" },
3445
+ { host: home, container: "/home/agent" },
3446
+ { host: claudeDir, container: "/home/agent/.claude" },
2131
3447
  { host: objects, container: CONTAINER_GIT_OBJECTS, readOnly: true },
2132
3448
  { host: sessionDir, container: CONTAINER_REPO },
2133
3449
  { host: packageRoot, container: CONTAINER_PACKAGE, readOnly: true },
2134
3450
  ...stagedPaths.map((path) => ({
2135
- host: join11(hostHome, path),
3451
+ host: join16(hostHome, path),
2136
3452
  container: `${HOST_STAGING_DIR}/${path}`,
2137
3453
  readOnly: true
2138
3454
  })),
@@ -2158,9 +3474,9 @@ function dockerRunArgs({
2158
3474
  extra = [],
2159
3475
  name
2160
3476
  }) {
2161
- const volumes = mounts.flatMap(({ host, container, readOnly, type = "bind" }) => [
3477
+ const volumes = mounts.flatMap(({ host, container, readOnly }) => [
2162
3478
  "--mount",
2163
- `type=${type},source=${host},target=${container}${readOnly ? ",readonly" : ""}`
3479
+ `type=bind,source=${host},target=${container}${readOnly ? ",readonly" : ""}`
2164
3480
  ]);
2165
3481
  return [
2166
3482
  "run",
@@ -2189,17 +3505,54 @@ Default: ${DEFAULT_CLIENT}. Run \`maesttro <client>\` to pick one, or \`maesttro
2189
3505
  function ensureReady() {
2190
3506
  try {
2191
3507
  execSync3(`docker image inspect ${IMAGE}`, { stdio: "ignore" });
2192
- execSync3(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
2193
3508
  } catch {
2194
3509
  throw new LaunchError("Runner isn't set up yet. Run `maesttro setup` first.");
2195
3510
  }
2196
3511
  }
2197
3512
  function clearStaleContainer(id) {
2198
- spawnSync2("docker", ["rm", "-f", id], { stdio: "ignore" });
3513
+ spawnSync3("docker", ["rm", "-f", id], { stdio: "ignore" });
3514
+ }
3515
+ async function establishClaudeCredentials({
3516
+ claudeDir,
3517
+ store,
3518
+ refresh = refreshMaster,
3519
+ onRotated = (credentials) => pushToRunningSessions({ credentials, claudeDir, announce }),
3520
+ signals = processSignals,
3521
+ announce,
3522
+ now = Date.now
3523
+ }) {
3524
+ const credentials = store.load();
3525
+ if (credentials && claudeLoginState({ credentials, now: now() }) === "active" && isDue({ credentials, now: now() })) {
3526
+ announce("Refreshing Claude login…");
3527
+ }
3528
+ try {
3529
+ writeSessionCredentials({
3530
+ claudeDir,
3531
+ credentials: await refresh({ store, announce, onRotated, signals })
3532
+ });
3533
+ return {};
3534
+ } catch (error) {
3535
+ if (error instanceof ClaudeLoggedOut) {
3536
+ announce(error.message);
3537
+ removeHostWrittenCredentials({ claudeDir });
3538
+ return { failure: error.message };
3539
+ }
3540
+ if (error instanceof ClaudeLoginExpired || error instanceof ClaudeRefreshFailed) {
3541
+ announce(error.message);
3542
+ const master = store.load();
3543
+ if (master && master.claudeAiOauth.expiresAt > now()) {
3544
+ writeSessionCredentials({ claudeDir, credentials: master });
3545
+ } else if (error instanceof ClaudeLoginExpired) {
3546
+ removeHostWrittenCredentials({ claudeDir });
3547
+ }
3548
+ return { failure: error.message };
3549
+ }
3550
+ throw error;
3551
+ }
2199
3552
  }
2200
3553
  function sharedSkillsDir() {
2201
- const dir = process.env.AGENT_SHARED_SKILLS ?? join11(homedir4(), "agent-shared", "skills");
2202
- return existsSync4(dir) ? dir : undefined;
3554
+ const dir = process.env.AGENT_SHARED_SKILLS ?? join16(homedir3(), "agent-shared", "skills");
3555
+ return existsSync5(dir) ? dir : undefined;
2203
3556
  }
2204
3557
  async function resolveAuthor(workspace) {
2205
3558
  const fromRepo = workspaceAuthor(runCommand, workspace);
@@ -2228,7 +3581,7 @@ function startSession(run, root) {
2228
3581
  if (!baseSha)
2229
3582
  throw new LaunchError(`Can't resolve ${base}.`);
2230
3583
  const id = newSessionId();
2231
- const sessionDir = join11(root, WORKTREES_DIR, id);
3584
+ const sessionDir = join16(root, WORKTREES_DIR, id);
2232
3585
  createSessionClone(run, root, sessionDir, baseSha);
2233
3586
  return {
2234
3587
  id,
@@ -2245,7 +3598,7 @@ function resumeSession(root, id) {
2245
3598
  if (!session || session.repoRoot !== root) {
2246
3599
  throw new LaunchError(`No session ${id} in this repo. See \`maesttro sessions\`.`);
2247
3600
  }
2248
- if (!existsSync4(session.worktree)) {
3601
+ if (!existsSync5(session.worktree)) {
2249
3602
  throw new LaunchError(`The clone for ${id} is gone: ${session.worktree}`);
2250
3603
  }
2251
3604
  return session;
@@ -2268,13 +3621,13 @@ function preserveBranches(run, session) {
2268
3621
  return preserved;
2269
3622
  }
2270
3623
  function finishSession(run, session) {
2271
- if (!existsSync4(session.worktree)) {
3624
+ if (!existsSync5(session.worktree)) {
2272
3625
  deleteSession(session.id);
2273
3626
  return;
2274
3627
  }
2275
3628
  const preserved = preserveBranches(run, session);
2276
3629
  if (!cloneHasWork(session.worktree, session.baseSha)) {
2277
- rmSync6(session.worktree, { recursive: true, force: true });
3630
+ rmSync10(session.worktree, { recursive: true, force: true });
2278
3631
  deleteSession(session.id);
2279
3632
  return;
2280
3633
  }
@@ -2303,9 +3656,45 @@ function runnerGitHubMode({
2303
3656
  function exitCodeOf(code, signal) {
2304
3657
  if (code !== null)
2305
3658
  return code;
2306
- return signal ? 128 + (constants.signals[signal] ?? 0) : 1;
3659
+ return signal ? 128 + (constants2.signals[signal] ?? 0) : 1;
2307
3660
  }
2308
3661
  var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
3662
+ var ABANDON_GRACE_MS = 3000;
3663
+ function closeSession({
3664
+ run,
3665
+ session
3666
+ }) {
3667
+ try {
3668
+ finishSession(run, session);
3669
+ } catch (error) {
3670
+ console.log(`
3671
+ Couldn't clean up session ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
3672
+ }
3673
+ }
3674
+ function claudeNoticeSink({
3675
+ log,
3676
+ print = (message) => console.log(message)
3677
+ }) {
3678
+ let quiet = false;
3679
+ let unseen;
3680
+ return {
3681
+ announce: (message) => {
3682
+ if (!quiet)
3683
+ return print(message);
3684
+ log("claude.notice", message);
3685
+ unseen = message;
3686
+ },
3687
+ quiet: () => {
3688
+ quiet = true;
3689
+ },
3690
+ replay: () => {
3691
+ if (unseen)
3692
+ print(`
3693
+ ${unseen}`);
3694
+ unseen = undefined;
3695
+ }
3696
+ };
3697
+ }
2309
3698
  async function launch({
2310
3699
  args,
2311
3700
  resume,
@@ -2315,9 +3704,10 @@ async function launch({
2315
3704
  ensureReady();
2316
3705
  requireGit(run);
2317
3706
  const root = repoRoot(run, process.cwd());
2318
- purgeLegacyTokenOnce({
3707
+ migrateFromVolumeOnce({
2319
3708
  config: readMaesttroConfig(),
2320
3709
  saveConfig: writeMaesttroConfig,
3710
+ store: claudeStore(),
2321
3711
  announce: (message) => console.log(message)
2322
3712
  });
2323
3713
  const started = resume ? resumeSession(root, resume) : startSession(run, root);
@@ -2328,13 +3718,55 @@ async function launch({
2328
3718
  sessionId: started.id
2329
3719
  });
2330
3720
  const strict = isStrict(requestedStrict, started);
3721
+ const client = sessionClientArgs({ args, session: started });
2331
3722
  const saved = {
2332
3723
  ...started,
2333
3724
  backup: started.backup ?? backup?.path,
2334
- strict
3725
+ strict,
3726
+ client: client.client ?? started.client
2335
3727
  };
2336
3728
  saveSession(saved);
3729
+ let github;
3730
+ let claudeRefresher;
3731
+ let clientConfigDir;
3732
+ let abandoning = false;
3733
+ const interrupts = signalRouter({
3734
+ onUnrouted: async (signal) => {
3735
+ const code = exitCodeOf(null, signal);
3736
+ if (abandoning)
3737
+ process.exit(code);
3738
+ abandoning = true;
3739
+ console.log(`
3740
+ Interrupted; cleaning up session ${saved.id}…`);
3741
+ claudeRefresher?.stop();
3742
+ clientConfigDir?.remove();
3743
+ if (github) {
3744
+ await Promise.race([
3745
+ github.close(),
3746
+ new Promise((resolve) => setTimeout(resolve, ABANDON_GRACE_MS))
3747
+ ]);
3748
+ }
3749
+ closeSession({ run, session: saved });
3750
+ process.exit(code);
3751
+ }
3752
+ });
2337
3753
  console.log(`Session ${saved.id}${strict ? " (strict)" : ""}: clone .worktrees/${saved.id} at ${saved.base}${backup ? `, backup ${backup.reused ? "reused" : "written"}` : ""}.`);
3754
+ const { home, claudeDir } = ensureSessionHome({ id: saved.id });
3755
+ const claude = claudeStore();
3756
+ const sessionLog = fileSessionLog();
3757
+ const claudeNotices = claudeNoticeSink({ log: sessionLog });
3758
+ const { failure } = await establishClaudeCredentials({
3759
+ claudeDir,
3760
+ store: claude,
3761
+ announce: claudeNotices.announce,
3762
+ signals: interrupts
3763
+ });
3764
+ claudeRefresher = startClaudeRefresher({
3765
+ claudeDir,
3766
+ store: claude,
3767
+ announce: claudeNotices.announce,
3768
+ launchFailure: failure
3769
+ });
2338
3770
  const repo = workspaceRepo(run, root);
2339
3771
  if (repo && !resume) {
2340
3772
  git(run, saved.worktree, [
@@ -2344,7 +3776,7 @@ async function launch({
2344
3776
  `https://github.com/${repo}.git`
2345
3777
  ]);
2346
3778
  }
2347
- const github = await openGitHubSession({
3779
+ github = await openGitHubSession({
2348
3780
  repo,
2349
3781
  author: await resolveAuthor(root),
2350
3782
  strict,
@@ -2354,11 +3786,11 @@ async function launch({
2354
3786
  store: selectCredentialStore(),
2355
3787
  announce: (message) => console.log(message)
2356
3788
  },
2357
- log: fileSessionLog()
3789
+ log: sessionLog
2358
3790
  });
2359
3791
  if (github.notice)
2360
3792
  console.log(github.notice);
2361
- const hostHome = homedir4();
3793
+ const hostHome = homedir3();
2362
3794
  const url = maesttroUrl();
2363
3795
  const orchestratorWindow = await fetchOrchestratorWindow({ url });
2364
3796
  if (orchestratorWindow === undefined) {
@@ -2367,20 +3799,23 @@ async function launch({
2367
3799
  const clientConfigs = buildClientConfigs({
2368
3800
  host: readHostClientConfigs({ home: hostHome }),
2369
3801
  url,
2370
- orchestratorWindow
3802
+ orchestratorWindow,
3803
+ isStrict: strict
2371
3804
  });
2372
3805
  if (clientConfigs.removed.length > 0) {
2373
3806
  console.log(`Kept out of the runner as secrets: ${clientConfigs.removed.join(", ")}`);
2374
3807
  }
2375
- const clientConfigDir = writeClientConfigDir(clientConfigs.files);
3808
+ clientConfigDir = writeClientConfigDir(clientConfigs.files);
2376
3809
  const { mounts, workdir } = sessionMounts({
2377
3810
  repoRoot: root,
2378
3811
  sessionDir: saved.worktree,
2379
3812
  packageRoot: PACKAGE_ROOT,
2380
3813
  hostHome,
2381
- stagedPaths: STAGED_PATHS.filter((path) => existsSync4(join11(hostHome, path))),
3814
+ stagedPaths: STAGED_PATHS.filter((path) => existsSync5(join16(hostHome, path))),
2382
3815
  clientConfigs: clientConfigDir.dir,
2383
- sharedSkills: sharedSkillsDir()
3816
+ sharedSkills: sharedSkillsDir(),
3817
+ home,
3818
+ claudeDir
2384
3819
  });
2385
3820
  const sessionEnv = {
2386
3821
  ...github.env,
@@ -2390,22 +3825,17 @@ async function launch({
2390
3825
  ...hostIdentityEnv(),
2391
3826
  ...repo ? { MAESTTRO_REPO: repo } : {}
2392
3827
  };
2393
- const envFlags = [
2394
- "MAESTTRO_SESSION_ID",
2395
- "MAESTTRO_BASE",
2396
- "MAESTTRO_GITHUB",
2397
- "MAESTTRO_HOST_UID",
2398
- "MAESTTRO_HOST_GID"
2399
- ].concat(repo ? ["MAESTTRO_REPO"] : []).flatMap((name) => ["-e", name]);
2400
3828
  clearStaleContainer(saved.id);
2401
3829
  pinAlternates(saved.worktree, CONTAINER_GIT_OBJECTS);
2402
- const child = spawn("docker", dockerRunArgs({
2403
- command: containerCommand(resolveCommand(args)),
3830
+ const child = spawn2("docker", dockerRunArgs({
3831
+ command: containerCommand(resolveCommand(client.args)),
2404
3832
  mounts,
2405
3833
  workdir,
2406
3834
  name: saved.id,
2407
- extra: [...github.dockerArgs, ...envFlags]
3835
+ extra: [...github.dockerArgs, ...sessionEnvFlags({ repo })]
2408
3836
  }), { stdio: "inherit", env: { ...process.env, ...sessionEnv } });
3837
+ interrupts.dispose();
3838
+ claudeNotices.quiet();
2409
3839
  const forward = (signal) => child.kill(signal);
2410
3840
  for (const signal of FORWARDED_SIGNALS)
2411
3841
  process.on(signal, forward);
@@ -2416,13 +3846,18 @@ async function launch({
2416
3846
  for (const signal of FORWARDED_SIGNALS)
2417
3847
  process.off(signal, forward);
2418
3848
  await github.close();
3849
+ if (claudeRefresher?.busy())
3850
+ console.log(`
3851
+ ${FINISHING_BACKGROUND_REFRESH}`);
3852
+ await claudeRefresher?.stop();
3853
+ claudeNotices.replay();
2419
3854
  clientConfigDir.remove();
2420
- finishSession(run, saved);
3855
+ closeSession({ run, session: saved });
2421
3856
  process.exit(exitCode);
2422
3857
  }
2423
3858
 
2424
3859
  // src/push.ts
2425
- import { spawnSync as spawnSync3 } from "node:child_process";
3860
+ import { spawnSync as spawnSync4 } from "node:child_process";
2426
3861
 
2427
3862
  // ../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
2428
3863
  import { stdout, stdin } from "node:process";
@@ -3614,7 +5049,7 @@ async function push({ args }) {
3614
5049
  return;
3615
5050
  }
3616
5051
  }
3617
- const pushed = spawnSync3("git", ["-C", root, "push", ...gitFlags, "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
5052
+ const pushed = spawnSync4("git", ["-C", root, "push", ...gitFlags, "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
3618
5053
  if (pushed.status === 0) {
3619
5054
  const done = `Pushed ${branch}. Base for comparison was ${baseSha.slice(0, 8)}.`;
3620
5055
  if (tty && !yes)
@@ -3626,7 +5061,7 @@ async function push({ args }) {
3626
5061
  }
3627
5062
 
3628
5063
  // src/sessions-command.ts
3629
- import { existsSync as existsSync5, rmSync as rmSync7 } from "node:fs";
5064
+ import { existsSync as existsSync6, rmSync as rmSync11 } from "node:fs";
3630
5065
  function parseRemoveArgs(args) {
3631
5066
  let all = false;
3632
5067
  let force = false;
@@ -3642,7 +5077,7 @@ function parseRemoveArgs(args) {
3642
5077
  return { all, force, id };
3643
5078
  }
3644
5079
  async function removeSession(run, session, force, ask, interactive) {
3645
- if (existsSync5(session.worktree)) {
5080
+ if (existsSync6(session.worktree)) {
3646
5081
  const preserved = preserveBranches(run, session);
3647
5082
  const head = cloneHead(session.worktree);
3648
5083
  const unbranched = preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha;
@@ -3660,7 +5095,7 @@ async function removeSession(run, session, force, ask, interactive) {
3660
5095
  return "kept";
3661
5096
  }
3662
5097
  }
3663
- rmSync7(session.worktree, { recursive: true, force: true });
5098
+ rmSync11(session.worktree, { recursive: true, force: true });
3664
5099
  const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
3665
5100
  console.log(`Removed session ${session.id}.${kept}`);
3666
5101
  } else {
@@ -3717,7 +5152,7 @@ async function sessionsCommand({
3717
5152
  return;
3718
5153
  }
3719
5154
  for (const session of sessions) {
3720
- const branches = existsSync5(session.worktree) ? cloneBranches(session.worktree) : undefined;
5155
+ const branches = existsSync6(session.worktree) ? cloneBranches(session.worktree) : undefined;
3721
5156
  const where = branches === undefined ? "clone missing" : branches.length > 0 ? `branches: ${branches.join(", ")}` : "no branches yet";
3722
5157
  const mode = session.strict ? " strict" : "";
3723
5158
  console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}${mode}`);
@@ -3745,23 +5180,31 @@ async function setup({ args }) {
3745
5180
  console.log("-> Building runner image (this pulls node:22-slim + installs CLIs, first run only)...");
3746
5181
  run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`);
3747
5182
  console.log(`
3748
- -> Ensuring persistent volume exists...`);
3749
- execSync4(`docker volume create ${VOLUME}`, { stdio: "ignore" });
3750
- console.log(`
3751
- -> Removing any long-lived GitHub token from the volume...`);
3752
- if (purgeLegacyToken())
3753
- console.log(` ${REVOKE_NOTICE}`);
3754
- writeMaesttroConfig({ ...readMaesttroConfig(), legacyTokenPurged: true });
5183
+ -> Checking for the old shared runner volume...`);
5184
+ migrateFromVolumeOnce({
5185
+ config: readMaesttroConfig(),
5186
+ saveConfig: writeMaesttroConfig,
5187
+ store: claudeStore(),
5188
+ announce: (message) => console.log(` ${message}`)
5189
+ });
3755
5190
  if (args[0]) {
3756
- console.log(" Ignoring the token argument: sessions now get short-lived tokens through `maesttro login`.");
5191
+ console.log(" Ignoring the token argument: sessions now get short-lived tokens through `maesttro login github`.");
3757
5192
  }
3758
5193
  console.log(`
3759
5194
  -> Logging in to GitHub...`);
3760
5195
  await ensureLogin();
3761
5196
  console.log(`
5197
+ -> Logging in to Claude...`);
5198
+ const claude = claudeStore();
5199
+ if (claudeLoginState({ credentials: claude.load(), now: Date.now() }) === "active") {
5200
+ console.log(` Already logged in. Stored in ${claude.kind}.`);
5201
+ } else if (!claudeLogin({ announce: (message) => console.log(` ${message}`) })) {
5202
+ console.log(" Setup continues without it; Claude Code sessions will show their own login screen.");
5203
+ }
5204
+ console.log(`
3762
5205
  -> Verifying isolation...`);
3763
5206
  console.log(" personal SSH key should NOT be visible:");
3764
- run(`docker run --rm ${hostIdentityFlags()} -v ${VOLUME}:/home/agent ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`);
5207
+ run(`docker run --rm ${hostIdentityFlags()} ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`);
3765
5208
  console.log("\nSetup complete. Run `maesttro` from any project directory to start a session,");
3766
5209
  console.log("or `maesttro list` to see available clients.");
3767
5210
  }
@@ -3843,15 +5286,15 @@ function removeStaleRunnerImages({
3843
5286
 
3844
5287
  // src/version-check.ts
3845
5288
  import { execFile } from "node:child_process";
3846
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
3847
- import { join as join12 } from "node:path";
5289
+ import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
5290
+ import { join as join17 } from "node:path";
3848
5291
  var LOOKUP_TIMEOUT_MS = 3000;
3849
5292
  function installedManifest() {
3850
- return JSON.parse(readFileSync8(join12(PACKAGE_ROOT, "package.json"), "utf8"));
5293
+ return JSON.parse(readFileSync12(join17(PACKAGE_ROOT, "package.json"), "utf8"));
3851
5294
  }
3852
5295
  function canOfferUpdate({
3853
5296
  interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
3854
- inRunner = existsSync6(RUNNER_MARKER)
5297
+ inRunner = existsSync7(RUNNER_MARKER)
3855
5298
  } = {}) {
3856
5299
  return interactive && !inRunner;
3857
5300
  }
@@ -3945,6 +5388,13 @@ var MODE_FLAGS = {
3945
5388
  "--strict": true,
3946
5389
  "--no-strict": false
3947
5390
  };
5391
+ function clientFirst(args) {
5392
+ const [first, second, ...rest] = args;
5393
+ if (first !== undefined && second !== undefined && isClientKey(first)) {
5394
+ return [second, first, ...rest];
5395
+ }
5396
+ return args;
5397
+ }
3948
5398
  function takeModeFlags(args) {
3949
5399
  let strict;
3950
5400
  let index = 0;
@@ -3996,13 +5446,13 @@ async function main(argv, commands = defaultCommands) {
3996
5446
  await commands.doctor();
3997
5447
  return;
3998
5448
  case "login":
3999
- await commands.login();
5449
+ await commands.login({ args: rest });
4000
5450
  return;
4001
5451
  case "logout":
4002
- await commands.logout();
5452
+ await commands.logout({ args: rest });
4003
5453
  return;
4004
5454
  case "status":
4005
- await commands.status();
5455
+ await commands.status({ args: rest });
4006
5456
  return;
4007
5457
  case "push":
4008
5458
  await commands.push({ args: rest });
@@ -4012,11 +5462,11 @@ async function main(argv, commands = defaultCommands) {
4012
5462
  return;
4013
5463
  case "resume": {
4014
5464
  const beforeId = takeModeFlags(rest);
4015
- const [id, ...afterId] = beforeId.rest;
5465
+ const [id, ...afterId] = clientFirst(beforeId.rest);
4016
5466
  const afterIdFlags = takeModeFlags(afterId);
4017
5467
  const strict = afterIdFlags.strict ?? beforeId.strict ?? leading.strict;
4018
5468
  if (!id) {
4019
- commands.print("Usage: maesttro resume <session-id> [--strict|--no-strict] [client] [args...]");
5469
+ commands.print("Usage: maesttro resume [client] <session-id> [--strict|--no-strict] [client] [args...]");
4020
5470
  return;
4021
5471
  }
4022
5472
  await commands.launch({