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/README.md +17 -11
- package/dist/maesttro.js +2098 -648
- package/docker/Dockerfile +4 -8
- package/docker/entrypoint.sh +6 -3
- package/package.json +1 -1
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 {
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
|
1429
|
+
function announcingOnce(announce, alreadySaid) {
|
|
1430
|
+
let last = alreadySaid;
|
|
181
1431
|
return {
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
198
|
-
|
|
1438
|
+
reset: () => {
|
|
1439
|
+
last = undefined;
|
|
199
1440
|
}
|
|
200
1441
|
};
|
|
201
1442
|
}
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
1881
|
+
import { dirname as dirname7, join as join11 } from "node:path";
|
|
454
1882
|
|
|
455
|
-
// src/
|
|
456
|
-
import {
|
|
457
|
-
|
|
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/
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
1906
|
+
function installedClaudeVersion(root = "/usr/local/lib/node_modules/@anthropic-ai/claude-code") {
|
|
484
1907
|
try {
|
|
485
|
-
|
|
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
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
output += " ";
|
|
532
|
-
index = significant;
|
|
1963
|
+
if (stat.isDirectory()) {
|
|
1964
|
+
copyTree(from, to, filter);
|
|
533
1965
|
continue;
|
|
534
1966
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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 = "
|
|
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: "
|
|
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 =
|
|
762
|
-
if (!
|
|
2188
|
+
const path = join10(home, file);
|
|
2189
|
+
if (!existsSync2(path))
|
|
763
2190
|
return {};
|
|
764
2191
|
try {
|
|
765
|
-
return parse(
|
|
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 =
|
|
798
|
-
const root =
|
|
799
|
-
|
|
800
|
-
const dir =
|
|
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 =
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
808
|
-
return { dir, remove: () =>
|
|
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 (!
|
|
2242
|
+
if (!existsSync2(source))
|
|
815
2243
|
return false;
|
|
816
|
-
const merged = MERGED_INTO_HOME.map((file) =>
|
|
817
|
-
|
|
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 =
|
|
824
|
-
if (!
|
|
2247
|
+
const staged = join10(source, file);
|
|
2248
|
+
if (!existsSync2(staged))
|
|
825
2249
|
continue;
|
|
826
|
-
const target =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
962
|
-
if (!
|
|
2357
|
+
const source = join11(configsDir, key);
|
|
2358
|
+
if (!existsSync3(source))
|
|
963
2359
|
continue;
|
|
964
|
-
const targetDir =
|
|
965
|
-
for (const name of
|
|
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(
|
|
2364
|
+
const curated = withoutComments(readJson(join11(source, name)));
|
|
969
2365
|
if (Object.keys(curated).length === 0)
|
|
970
2366
|
continue;
|
|
971
|
-
const target =
|
|
972
|
-
|
|
2367
|
+
const target = join11(targetDir, name);
|
|
2368
|
+
mkdirSync10(targetDir, { recursive: true });
|
|
973
2369
|
const merged = mergeJson(readJson(target), curated);
|
|
974
|
-
|
|
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 =
|
|
988
|
-
if (!
|
|
2383
|
+
const source = join11(staging, directory);
|
|
2384
|
+
if (!existsSync3(source))
|
|
989
2385
|
continue;
|
|
990
2386
|
try {
|
|
991
|
-
|
|
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 =
|
|
997
|
-
const hostCopy =
|
|
998
|
-
const target =
|
|
999
|
-
|
|
1000
|
-
|
|
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 =
|
|
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
|
|
1170
|
-
import { join as
|
|
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) !==
|
|
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
|
-
|
|
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 =
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2722
|
+
print(` [fail] ${label}${reason ? ` — ${reason}` : ""}`);
|
|
1284
2723
|
return false;
|
|
1285
2724
|
}
|
|
1286
2725
|
}
|
|
1287
|
-
function inRunner(command
|
|
1288
|
-
|
|
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}
|
|
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>
|
|
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,
|
|
1410
|
-
maesttro login
|
|
1411
|
-
|
|
1412
|
-
maesttro
|
|
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,
|
|
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
|
|
1421
|
-
import { existsSync as
|
|
1422
|
-
import { constants, homedir as
|
|
1423
|
-
import { join as
|
|
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
|
|
1428
|
-
import { basename, join as
|
|
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
|
|
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
|
-
|
|
2905
|
+
mkdirSync12(dir, { recursive: true, mode: 448 });
|
|
1495
2906
|
const fingerprint = refsFingerprint(refs);
|
|
1496
|
-
const existing =
|
|
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:
|
|
2910
|
+
return { path: join13(dir, latest), reused: true };
|
|
1500
2911
|
}
|
|
1501
2912
|
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
1502
|
-
const path =
|
|
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
|
-
|
|
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
|
|
1593
|
-
mkdirSync as
|
|
1594
|
-
mkdtempSync as
|
|
1595
|
-
renameSync as
|
|
1596
|
-
rmSync as
|
|
1597
|
-
writeFileSync as
|
|
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
|
|
1600
|
-
import { join as
|
|
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
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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 =
|
|
1619
|
-
const root =
|
|
1620
|
-
|
|
1621
|
-
const ghDir =
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
writeAtomically(
|
|
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(
|
|
1629
|
-
writeStatus: (status) => writeAtomically(
|
|
2961
|
+
writeToken: (token, user) => writeAtomically(join14(ghDir, "hosts.yml"), hostsYaml(token, user)),
|
|
2962
|
+
writeStatus: (status) => writeAtomically(join14(ghDir, "status"), `${status}
|
|
1630
2963
|
`),
|
|
1631
|
-
remove: () =>
|
|
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
|
|
1783
|
-
lstatSync,
|
|
1784
|
-
mkdirSync as
|
|
1785
|
-
readdirSync as
|
|
1786
|
-
readFileSync as
|
|
1787
|
-
writeFileSync as
|
|
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
|
|
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
|
|
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
|
|
3217
|
+
return readdirSync6(dir, { withFileTypes: true });
|
|
1884
3218
|
} catch {
|
|
1885
3219
|
return [];
|
|
1886
3220
|
}
|
|
1887
3221
|
}
|
|
1888
3222
|
function cloneGitDir(clonePath) {
|
|
1889
|
-
const gitDir =
|
|
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 (!
|
|
3228
|
+
if (!lstatSync5(path).isFile())
|
|
1895
3229
|
return;
|
|
1896
|
-
const text =
|
|
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 =
|
|
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(
|
|
3265
|
+
walk(join15(dir, entry.name), name);
|
|
1932
3266
|
else if (entry.isFile())
|
|
1933
3267
|
names.add(name);
|
|
1934
3268
|
}
|
|
1935
3269
|
};
|
|
1936
|
-
walk(
|
|
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(
|
|
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 (!
|
|
3291
|
+
if (!lstatSync5(join15(gitDir, "HEAD")).isFile())
|
|
1958
3292
|
return;
|
|
1959
|
-
text =
|
|
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 =
|
|
1981
|
-
if (
|
|
3314
|
+
const dest = join15(intoDir, entry.name);
|
|
3315
|
+
if (existsSync4(dest))
|
|
1982
3316
|
continue;
|
|
1983
3317
|
if (!created) {
|
|
1984
|
-
|
|
3318
|
+
mkdirSync14(intoDir, { recursive: true });
|
|
1985
3319
|
created = true;
|
|
1986
3320
|
}
|
|
1987
|
-
|
|
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 =
|
|
1995
|
-
const 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 =
|
|
3333
|
+
const alternates = join15(info, "alternates");
|
|
2000
3334
|
let stat;
|
|
2001
3335
|
try {
|
|
2002
|
-
stat =
|
|
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 (
|
|
2012
|
-
|
|
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 =
|
|
3353
|
+
const objects = join15(gitDir, "objects");
|
|
2020
3354
|
if (!realDir(objects))
|
|
2021
3355
|
return false;
|
|
2022
|
-
const into =
|
|
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(
|
|
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 =
|
|
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 =
|
|
3443
|
+
const objects = join16(repoRoot, ".git", "objects");
|
|
2129
3444
|
const mounts = [
|
|
2130
|
-
{ host:
|
|
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:
|
|
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
|
|
3477
|
+
const volumes = mounts.flatMap(({ host, container, readOnly }) => [
|
|
2162
3478
|
"--mount",
|
|
2163
|
-
`type
|
|
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
|
-
|
|
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 ??
|
|
2202
|
-
return
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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 + (
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3789
|
+
log: sessionLog
|
|
2358
3790
|
});
|
|
2359
3791
|
if (github.notice)
|
|
2360
3792
|
console.log(github.notice);
|
|
2361
|
-
const hostHome =
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
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, ...
|
|
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
|
-
|
|
3855
|
+
closeSession({ run, session: saved });
|
|
2421
3856
|
process.exit(exitCode);
|
|
2422
3857
|
}
|
|
2423
3858
|
|
|
2424
3859
|
// src/push.ts
|
|
2425
|
-
import { spawnSync as
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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
|
-
->
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
console.log(` ${
|
|
3754
|
-
|
|
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()}
|
|
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
|
|
3847
|
-
import { join as
|
|
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(
|
|
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 =
|
|
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({
|