@lesscap/baton-cli 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/baton.mjs +415 -62
- package/package.json +3 -3
package/dist/baton.mjs
CHANGED
|
@@ -4815,7 +4815,7 @@ var viewWorker = (cfg) => {
|
|
|
4815
4815
|
apiToken: worker2.apiToken
|
|
4816
4816
|
};
|
|
4817
4817
|
};
|
|
4818
|
-
var worktreeConfig = (cfg) => ({
|
|
4818
|
+
var worktreeConfig = (cfg, session2) => ({
|
|
4819
4819
|
server: cfg.server,
|
|
4820
4820
|
project: cfg.projectId,
|
|
4821
4821
|
baseBranch: cfg.baseBranch,
|
|
@@ -4825,7 +4825,8 @@ var worktreeConfig = (cfg) => ({
|
|
|
4825
4825
|
agentKind: cfg.agentKind,
|
|
4826
4826
|
machineId: cfg.machineId,
|
|
4827
4827
|
apiToken: cfg.apiToken
|
|
4828
|
-
}
|
|
4828
|
+
},
|
|
4829
|
+
...session2 === void 0 ? {} : { session: session2 }
|
|
4829
4830
|
});
|
|
4830
4831
|
|
|
4831
4832
|
// src/config.ts
|
|
@@ -4990,6 +4991,19 @@ var withRetry = async (fn, opts = {}) => {
|
|
|
4990
4991
|
throw lastErr;
|
|
4991
4992
|
};
|
|
4992
4993
|
|
|
4994
|
+
// src/client/services.ts
|
|
4995
|
+
var servicesClient = (baseUrl) => {
|
|
4996
|
+
const u = (path) => `${baseUrl}${path}`;
|
|
4997
|
+
return {
|
|
4998
|
+
listByProject: (projectId) => request(u(`/projects/${projectId}/services`), { method: "GET" }),
|
|
4999
|
+
run: (workerId, input) => request(u(`/workers/${workerId}/services`), { method: "POST", body: input }),
|
|
5000
|
+
stop: (workerId, name) => request(u(`/workers/${workerId}/services/${encodeURIComponent(name)}`), {
|
|
5001
|
+
method: "DELETE"
|
|
5002
|
+
}),
|
|
5003
|
+
report: (input) => request(u("/workers/me/services"), { method: "PUT", body: input })
|
|
5004
|
+
};
|
|
5005
|
+
};
|
|
5006
|
+
|
|
4993
5007
|
// src/client/sessions.ts
|
|
4994
5008
|
var sessionsClient = (baseUrl) => {
|
|
4995
5009
|
const u = (p) => `${baseUrl}${p}`;
|
|
@@ -5113,6 +5127,7 @@ var clientFromBase = (baseUrl) => ({
|
|
|
5113
5127
|
requirements: requirementClient(baseUrl),
|
|
5114
5128
|
tasks: taskClient(baseUrl),
|
|
5115
5129
|
sessions: sessionsClient(baseUrl),
|
|
5130
|
+
services: servicesClient(baseUrl),
|
|
5116
5131
|
loops: loopsClient(baseUrl),
|
|
5117
5132
|
workers: workersClient(baseUrl)
|
|
5118
5133
|
});
|
|
@@ -5376,6 +5391,17 @@ var labelAttachments = (attachments) => {
|
|
|
5376
5391
|
var isMessageFor = (msg, name) => !msg.to || msg.to.length === 0 || msg.to.includes(name);
|
|
5377
5392
|
|
|
5378
5393
|
// ../shared/src/domain/session-event.ts
|
|
5394
|
+
var agentMessageText = (payload) => {
|
|
5395
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
5396
|
+
const event = payload;
|
|
5397
|
+
if (!["item.started", "item.updated", "item.completed"].includes(String(event.type))) return null;
|
|
5398
|
+
const item = event.item;
|
|
5399
|
+
if (typeof item !== "object" || item === null) return null;
|
|
5400
|
+
const { type, id, text } = item;
|
|
5401
|
+
if (type !== "agent_message" || typeof id !== "string" || typeof text !== "string") return null;
|
|
5402
|
+
const trimmed = text.trim();
|
|
5403
|
+
return trimmed ? { id, text: trimmed } : null;
|
|
5404
|
+
};
|
|
5379
5405
|
var startedMessageIds = (events) => {
|
|
5380
5406
|
const ids = /* @__PURE__ */ new Set();
|
|
5381
5407
|
for (const e of events) {
|
|
@@ -6240,6 +6266,162 @@ var requirement = defineCommand({
|
|
|
6240
6266
|
}
|
|
6241
6267
|
});
|
|
6242
6268
|
|
|
6269
|
+
// src/worker/service-child.ts
|
|
6270
|
+
import { spawn } from "node:child_process";
|
|
6271
|
+
|
|
6272
|
+
// src/worker/proc.ts
|
|
6273
|
+
var killProcessGroup = (proc) => {
|
|
6274
|
+
if (proc.pid === void 0) return;
|
|
6275
|
+
killPidGroup(proc.pid);
|
|
6276
|
+
};
|
|
6277
|
+
var killPidGroup = (pid, signal = "SIGTERM") => {
|
|
6278
|
+
try {
|
|
6279
|
+
process.kill(-pid, signal);
|
|
6280
|
+
} catch {
|
|
6281
|
+
}
|
|
6282
|
+
};
|
|
6283
|
+
|
|
6284
|
+
// src/worker/service-child.ts
|
|
6285
|
+
var isStart = (message) => {
|
|
6286
|
+
if (typeof message !== "object" || message === null) return false;
|
|
6287
|
+
const value = message;
|
|
6288
|
+
return value.type === "start" && typeof value.cwd === "string" && Array.isArray(value.argv) && value.argv.length > 0 && value.argv.every((arg) => typeof arg === "string");
|
|
6289
|
+
};
|
|
6290
|
+
var isStop = (message) => typeof message === "object" && message !== null && message.type === "stop";
|
|
6291
|
+
var send = (message) => {
|
|
6292
|
+
if (process.connected) process.send?.(message);
|
|
6293
|
+
};
|
|
6294
|
+
var runServiceChild = () => {
|
|
6295
|
+
let app = null;
|
|
6296
|
+
let killTimer = null;
|
|
6297
|
+
let stopping = false;
|
|
6298
|
+
const stop = () => {
|
|
6299
|
+
if (stopping) return;
|
|
6300
|
+
stopping = true;
|
|
6301
|
+
if (!app?.pid) {
|
|
6302
|
+
process.exit(0);
|
|
6303
|
+
return;
|
|
6304
|
+
}
|
|
6305
|
+
killPidGroup(app.pid);
|
|
6306
|
+
killTimer = setTimeout(() => {
|
|
6307
|
+
if (app?.pid) killPidGroup(app.pid, "SIGKILL");
|
|
6308
|
+
}, 5e3);
|
|
6309
|
+
killTimer.unref();
|
|
6310
|
+
};
|
|
6311
|
+
process.on("disconnect", stop);
|
|
6312
|
+
process.on("SIGINT", stop);
|
|
6313
|
+
process.on("SIGTERM", stop);
|
|
6314
|
+
process.on("message", (message) => {
|
|
6315
|
+
if (isStop(message)) return stop();
|
|
6316
|
+
if (!isStart(message) || app) return;
|
|
6317
|
+
const [command, ...args] = message.argv;
|
|
6318
|
+
app = spawn(command ?? "", args, {
|
|
6319
|
+
cwd: message.cwd,
|
|
6320
|
+
detached: true,
|
|
6321
|
+
stdio: "inherit",
|
|
6322
|
+
env: process.env
|
|
6323
|
+
});
|
|
6324
|
+
app.once("spawn", () => send({ type: "started", pid: app?.pid }));
|
|
6325
|
+
app.once("error", (error) => {
|
|
6326
|
+
send({ type: "error", error: error.message });
|
|
6327
|
+
process.exit(1);
|
|
6328
|
+
});
|
|
6329
|
+
app.once("exit", (code, signal) => {
|
|
6330
|
+
if (killTimer) clearTimeout(killTimer);
|
|
6331
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
6332
|
+
});
|
|
6333
|
+
});
|
|
6334
|
+
};
|
|
6335
|
+
var serviceChildCommand = defineCommand({
|
|
6336
|
+
meta: { name: "child", description: "internal service process wrapper", hidden: true },
|
|
6337
|
+
run: runServiceChild
|
|
6338
|
+
});
|
|
6339
|
+
|
|
6340
|
+
// src/commands/service.ts
|
|
6341
|
+
var serviceHandle = (service2) => `W-${service2.workerId}/${service2.name}`;
|
|
6342
|
+
var formatUptime = (startedAt, now = Date.now()) => {
|
|
6343
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
6344
|
+
if (seconds >= 86400) return `${Math.floor(seconds / 86400)}d`;
|
|
6345
|
+
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h`;
|
|
6346
|
+
if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
|
|
6347
|
+
return `${seconds}s`;
|
|
6348
|
+
};
|
|
6349
|
+
var currentContext = () => {
|
|
6350
|
+
const config = loadProjectConfig(projectConfigPath());
|
|
6351
|
+
if (!config.worker || !config.session)
|
|
6352
|
+
throw new Error("service run requires a Baton session worktree");
|
|
6353
|
+
return { workerId: config.worker.id, sessionId: config.session };
|
|
6354
|
+
};
|
|
6355
|
+
var currentWorker = () => {
|
|
6356
|
+
const worker2 = loadProjectConfig(projectConfigPath()).worker;
|
|
6357
|
+
if (!worker2) throw new Error("no worker in scope");
|
|
6358
|
+
return worker2.id;
|
|
6359
|
+
};
|
|
6360
|
+
var parseTarget = (target) => {
|
|
6361
|
+
const explicit = target.match(/^W-(\d+)\/(.+)$/i);
|
|
6362
|
+
return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: currentWorker(), name: target };
|
|
6363
|
+
};
|
|
6364
|
+
var runCommand2 = defineCommand({
|
|
6365
|
+
meta: { name: "run", description: "run a service on the current worker" },
|
|
6366
|
+
args: {
|
|
6367
|
+
name: { type: "positional", required: true, description: "service name" },
|
|
6368
|
+
...common
|
|
6369
|
+
},
|
|
6370
|
+
run: async ({ args, rawArgs }) => {
|
|
6371
|
+
const separator = rawArgs.indexOf("--");
|
|
6372
|
+
const argv = separator === -1 ? [] : rawArgs.slice(separator + 1);
|
|
6373
|
+
if (argv.length === 0) throw new Error("command required after `--`");
|
|
6374
|
+
const context = currentContext();
|
|
6375
|
+
const result = await clientFor(args).services.run(context.workerId, {
|
|
6376
|
+
sessionId: context.sessionId,
|
|
6377
|
+
name: args.name,
|
|
6378
|
+
argv
|
|
6379
|
+
});
|
|
6380
|
+
if (!result.ok) throw new Error(result.error);
|
|
6381
|
+
if (args.json) return console.log(toJson(result));
|
|
6382
|
+
console.log(`running W-${context.workerId}/${args.name}`);
|
|
6383
|
+
if (result.logPath) console.log(`logs: ${result.logPath}`);
|
|
6384
|
+
}
|
|
6385
|
+
});
|
|
6386
|
+
var lsCommand = defineCommand({
|
|
6387
|
+
meta: { name: "ls", description: "list live services in the current project" },
|
|
6388
|
+
args: common,
|
|
6389
|
+
run: async ({ args }) => {
|
|
6390
|
+
const client = clientFor(args);
|
|
6391
|
+
const projectId = resolveProjectId({});
|
|
6392
|
+
const services = await client.services.listByProject(projectId);
|
|
6393
|
+
if (args.json) return console.log(toJson(services));
|
|
6394
|
+
if (services.length === 0) return console.log("(none)");
|
|
6395
|
+
const workers = await client.workers.listByProject(projectId);
|
|
6396
|
+
const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
|
|
6397
|
+
services.sort((a, b) => a.workerId - b.workerId || a.name.localeCompare(b.name)).map(
|
|
6398
|
+
(item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} session=${item.sessionId} uptime=${formatUptime(item.startedAt)}`
|
|
6399
|
+
).forEach((line) => {
|
|
6400
|
+
console.log(line);
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
});
|
|
6404
|
+
var stopCommand = defineCommand({
|
|
6405
|
+
meta: { name: "stop", description: "stop and remove a live service" },
|
|
6406
|
+
args: {
|
|
6407
|
+
service: {
|
|
6408
|
+
type: "positional",
|
|
6409
|
+
required: true,
|
|
6410
|
+
description: "service name, or W-N/name"
|
|
6411
|
+
},
|
|
6412
|
+
...common
|
|
6413
|
+
},
|
|
6414
|
+
run: async ({ args }) => {
|
|
6415
|
+
const target = parseTarget(args.service);
|
|
6416
|
+
await clientFor(args).services.stop(target.workerId, target.name);
|
|
6417
|
+
console.log(args.json ? toJson({ ok: true, service: args.service }) : `stopped ${args.service}`);
|
|
6418
|
+
}
|
|
6419
|
+
});
|
|
6420
|
+
var service = defineCommand({
|
|
6421
|
+
meta: { name: "service", description: "run and stop worker-managed development services" },
|
|
6422
|
+
subCommands: { run: runCommand2, ls: lsCommand, stop: stopCommand, child: serviceChildCommand }
|
|
6423
|
+
});
|
|
6424
|
+
|
|
6243
6425
|
// src/commands/session/abort.ts
|
|
6244
6426
|
var sessionAbortCommand = defineCommand({
|
|
6245
6427
|
meta: {
|
|
@@ -6494,14 +6676,6 @@ var parseFirstExchange = (content) => {
|
|
|
6494
6676
|
}
|
|
6495
6677
|
return userText || assistantText ? { userText, assistantText } : null;
|
|
6496
6678
|
};
|
|
6497
|
-
var canonicalAgentMessage = (payload) => {
|
|
6498
|
-
if (!isRecord(payload) || !["item.started", "item.updated", "item.completed"].includes(String(payload.type)))
|
|
6499
|
-
return null;
|
|
6500
|
-
const item = payload.item;
|
|
6501
|
-
if (!isRecord(item) || item.type !== "agent_message" || typeof item.id !== "string") return null;
|
|
6502
|
-
const text = textOf(item);
|
|
6503
|
-
return text ? { id: item.id, text } : null;
|
|
6504
|
-
};
|
|
6505
6679
|
var parseFirstExchangeFromEvents = (events) => {
|
|
6506
6680
|
let userText = "";
|
|
6507
6681
|
const itemOrder = [];
|
|
@@ -6514,7 +6688,7 @@ var parseFirstExchangeFromEvents = (events) => {
|
|
|
6514
6688
|
}
|
|
6515
6689
|
if (!userText) continue;
|
|
6516
6690
|
if (event.type === "turn_complete" || event.type === "turn_error") break;
|
|
6517
|
-
const agent = event.type === "agent_event" ?
|
|
6691
|
+
const agent = event.type === "agent_event" ? agentMessageText(event.payload) : null;
|
|
6518
6692
|
if (agent) {
|
|
6519
6693
|
if (!agentText.has(agent.id)) itemOrder.push(agent.id);
|
|
6520
6694
|
agentText.set(agent.id, agent.text);
|
|
@@ -7774,21 +7948,26 @@ var HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
|
7774
7948
|
var HEARTBEAT_DEAD_MS = 15e4;
|
|
7775
7949
|
var STREAM_STALE_MS = 9e4;
|
|
7776
7950
|
var createLiveness = (deps) => {
|
|
7777
|
-
const {
|
|
7951
|
+
const { heartbeat, log, isStreamOpen, reportRuntime, onTrip } = deps;
|
|
7778
7952
|
let heartbeatOkAt = Date.now();
|
|
7779
7953
|
let streamOkAt = Date.now();
|
|
7780
7954
|
let timer = null;
|
|
7781
7955
|
const ping = async () => {
|
|
7956
|
+
if (reportRuntime)
|
|
7957
|
+
void reportRuntime().catch((error) => log(`service report failed: ${String(error)}`));
|
|
7958
|
+
let timeout;
|
|
7782
7959
|
try {
|
|
7783
7960
|
await Promise.race([
|
|
7784
|
-
|
|
7785
|
-
new Promise(
|
|
7786
|
-
|
|
7787
|
-
)
|
|
7961
|
+
heartbeat(),
|
|
7962
|
+
new Promise((_, reject) => {
|
|
7963
|
+
timeout = setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS);
|
|
7964
|
+
})
|
|
7788
7965
|
]);
|
|
7789
7966
|
heartbeatOkAt = Date.now();
|
|
7790
7967
|
} catch (e) {
|
|
7791
7968
|
log(`heartbeat failed: ${String(e)}`);
|
|
7969
|
+
} finally {
|
|
7970
|
+
if (timeout) clearTimeout(timeout);
|
|
7792
7971
|
}
|
|
7793
7972
|
};
|
|
7794
7973
|
const tick = () => {
|
|
@@ -7815,20 +7994,185 @@ var createLiveness = (deps) => {
|
|
|
7815
7994
|
};
|
|
7816
7995
|
};
|
|
7817
7996
|
|
|
7997
|
+
// src/worker/service-supervisor.ts
|
|
7998
|
+
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
7999
|
+
import { join as join8 } from "node:path";
|
|
8000
|
+
|
|
8001
|
+
// src/worker/service-process.ts
|
|
8002
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
8003
|
+
import { closeSync, existsSync as existsSync5, openSync } from "node:fs";
|
|
8004
|
+
import { dirname as dirname2, join as join7 } from "node:path";
|
|
8005
|
+
import { fileURLToPath } from "node:url";
|
|
8006
|
+
var START_TIMEOUT_MS = 1e4;
|
|
8007
|
+
var isWrapperMessage = (message) => {
|
|
8008
|
+
if (typeof message !== "object" || message === null) return false;
|
|
8009
|
+
const value = message;
|
|
8010
|
+
if (value.type === "started") return typeof value.pid === "number";
|
|
8011
|
+
return value.type === "error" && typeof value.error === "string";
|
|
8012
|
+
};
|
|
8013
|
+
var binPath = () => {
|
|
8014
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
8015
|
+
const devShim = join7(here, "..", "..", "bin", "baton.mjs");
|
|
8016
|
+
return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
|
|
8017
|
+
};
|
|
8018
|
+
var startServiceProcess = (input) => {
|
|
8019
|
+
const logFd = openSync(input.logPath, "a");
|
|
8020
|
+
let wrapper;
|
|
8021
|
+
try {
|
|
8022
|
+
wrapper = spawn2(process.execPath, [binPath(), "service", "child"], {
|
|
8023
|
+
detached: false,
|
|
8024
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
8025
|
+
env: process.env
|
|
8026
|
+
});
|
|
8027
|
+
} finally {
|
|
8028
|
+
closeSync(logFd);
|
|
8029
|
+
}
|
|
8030
|
+
const started = new Promise((resolve2, reject) => {
|
|
8031
|
+
let settled = false;
|
|
8032
|
+
let timer;
|
|
8033
|
+
const cleanup = () => {
|
|
8034
|
+
clearTimeout(timer);
|
|
8035
|
+
wrapper.off("message", onMessage);
|
|
8036
|
+
wrapper.off("error", onError);
|
|
8037
|
+
wrapper.off("exit", onExit);
|
|
8038
|
+
};
|
|
8039
|
+
const finish = (result) => {
|
|
8040
|
+
if (settled) return;
|
|
8041
|
+
settled = true;
|
|
8042
|
+
cleanup();
|
|
8043
|
+
if (result instanceof Error) reject(result);
|
|
8044
|
+
else resolve2(result);
|
|
8045
|
+
};
|
|
8046
|
+
const onMessage = (message) => {
|
|
8047
|
+
if (!isWrapperMessage(message)) return;
|
|
8048
|
+
if (message.type === "started") finish(message.pid);
|
|
8049
|
+
else finish(new Error(message.error));
|
|
8050
|
+
};
|
|
8051
|
+
const onError = (error) => finish(error);
|
|
8052
|
+
const onExit = (code) => finish(new Error(`service wrapper exited before start (code=${code ?? -1})`));
|
|
8053
|
+
wrapper.on("message", onMessage);
|
|
8054
|
+
wrapper.once("error", onError);
|
|
8055
|
+
wrapper.once("exit", onExit);
|
|
8056
|
+
timer = setTimeout(() => {
|
|
8057
|
+
if (wrapper.connected) wrapper.send({ type: "stop" });
|
|
8058
|
+
finish(new Error("service wrapper did not start in time"));
|
|
8059
|
+
}, START_TIMEOUT_MS);
|
|
8060
|
+
timer.unref();
|
|
8061
|
+
wrapper.send({ type: "start", argv: input.argv, cwd: input.cwd }, (error) => {
|
|
8062
|
+
if (error) finish(error);
|
|
8063
|
+
});
|
|
8064
|
+
});
|
|
8065
|
+
return { wrapper, started };
|
|
8066
|
+
};
|
|
8067
|
+
|
|
8068
|
+
// src/worker/service-supervisor.ts
|
|
8069
|
+
var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve2) => child.once("exit", () => resolve2()));
|
|
8070
|
+
var createServiceSupervisor = (deps) => {
|
|
8071
|
+
const { getSession, sendReport, cfg, log } = deps;
|
|
8072
|
+
const entries = /* @__PURE__ */ new Map();
|
|
8073
|
+
let reportQueue = Promise.resolve();
|
|
8074
|
+
const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ sessionId, name, startedAt }) => ({ sessionId, name, startedAt }));
|
|
8075
|
+
const report = (result) => {
|
|
8076
|
+
const body = { services: snapshot(), ...result ? { result } : {} };
|
|
8077
|
+
const request2 = reportQueue.then(() => sendReport(body)).then(() => {
|
|
8078
|
+
});
|
|
8079
|
+
reportQueue = request2.catch(() => {
|
|
8080
|
+
});
|
|
8081
|
+
return request2;
|
|
8082
|
+
};
|
|
8083
|
+
const remove = (entry) => {
|
|
8084
|
+
if (entries.get(entry.name) !== entry) return;
|
|
8085
|
+
entries.delete(entry.name);
|
|
8086
|
+
void report().catch(() => {
|
|
8087
|
+
});
|
|
8088
|
+
};
|
|
8089
|
+
const stopEntry = (entry) => {
|
|
8090
|
+
if (entry.wrapper.connected) {
|
|
8091
|
+
entry.wrapper.send({ type: "stop" });
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
8094
|
+
if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
|
|
8095
|
+
};
|
|
8096
|
+
const runOne = async (requestId, sessionId, name, argv) => {
|
|
8097
|
+
if (entries.has(name))
|
|
8098
|
+
return report({
|
|
8099
|
+
requestId,
|
|
8100
|
+
ok: false,
|
|
8101
|
+
error: `service "${name}" is already running`,
|
|
8102
|
+
status: 409
|
|
8103
|
+
});
|
|
8104
|
+
const session2 = await getSession(sessionId);
|
|
8105
|
+
if (session2.workerId !== cfg.workerId || !session2.worktreePath)
|
|
8106
|
+
return report({
|
|
8107
|
+
requestId,
|
|
8108
|
+
ok: false,
|
|
8109
|
+
error: "session is not materialized on this worker",
|
|
8110
|
+
status: 400
|
|
8111
|
+
});
|
|
8112
|
+
const logDir = join8(session2.worktreePath, ".baton-services");
|
|
8113
|
+
const logPath = join8(logDir, `${name}.log`);
|
|
8114
|
+
mkdirSync4(logDir, { recursive: true });
|
|
8115
|
+
const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
|
|
8116
|
+
const entry = {
|
|
8117
|
+
wrapper,
|
|
8118
|
+
sessionId,
|
|
8119
|
+
name,
|
|
8120
|
+
startedAt: Date.now()
|
|
8121
|
+
};
|
|
8122
|
+
entries.set(name, entry);
|
|
8123
|
+
wrapper.once("exit", (code) => {
|
|
8124
|
+
log(`service ${name} exited (code=${code ?? -1})`);
|
|
8125
|
+
remove(entry);
|
|
8126
|
+
});
|
|
8127
|
+
try {
|
|
8128
|
+
entry.appPid = await started;
|
|
8129
|
+
} catch (error) {
|
|
8130
|
+
remove(entry);
|
|
8131
|
+
return report({ requestId, ok: false, error: String(error), status: 500 });
|
|
8132
|
+
}
|
|
8133
|
+
const service2 = {
|
|
8134
|
+
workerId: cfg.workerId,
|
|
8135
|
+
sessionId,
|
|
8136
|
+
name,
|
|
8137
|
+
startedAt: entry.startedAt
|
|
8138
|
+
};
|
|
8139
|
+
log(`service ${name} started in ${session2.worktreePath}`);
|
|
8140
|
+
await report({ requestId, ok: true, service: service2, logPath });
|
|
8141
|
+
};
|
|
8142
|
+
const run = async (requestId, sessionId, name, argv) => {
|
|
8143
|
+
try {
|
|
8144
|
+
await runOne(requestId, sessionId, name, argv);
|
|
8145
|
+
} catch (error) {
|
|
8146
|
+
await report({ requestId, ok: false, error: String(error), status: 500 });
|
|
8147
|
+
}
|
|
8148
|
+
};
|
|
8149
|
+
const stop = async (requestId, name) => {
|
|
8150
|
+
const entry = entries.get(name);
|
|
8151
|
+
if (!entry)
|
|
8152
|
+
return report({ requestId, ok: false, error: `service "${name}" not found`, status: 404 });
|
|
8153
|
+
stopEntry(entry);
|
|
8154
|
+
await waitForExit(entry.wrapper);
|
|
8155
|
+
remove(entry);
|
|
8156
|
+
await report({ requestId, ok: true });
|
|
8157
|
+
};
|
|
8158
|
+
const stopSession = async (sessionId) => {
|
|
8159
|
+
const matches = [...entries.values()].filter((entry) => entry.sessionId === sessionId);
|
|
8160
|
+
matches.forEach(stopEntry);
|
|
8161
|
+
await Promise.all(matches.map((entry) => waitForExit(entry.wrapper)));
|
|
8162
|
+
matches.forEach(remove);
|
|
8163
|
+
};
|
|
8164
|
+
const killAll = () => entries.forEach(stopEntry);
|
|
8165
|
+
return { run, stop, stopSession, report, killAll };
|
|
8166
|
+
};
|
|
8167
|
+
|
|
7818
8168
|
// src/worker/session-supervisor.ts
|
|
7819
|
-
import { spawn } from "node:child_process";
|
|
8169
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
7820
8170
|
import { randomUUID } from "node:crypto";
|
|
7821
|
-
import { existsSync as
|
|
7822
|
-
import { dirname as
|
|
7823
|
-
import { fileURLToPath } from "node:url";
|
|
8171
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
8172
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
8173
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7824
8174
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
7825
8175
|
|
|
7826
|
-
// src/session/runner/title-codex.ts
|
|
7827
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
7828
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
7829
|
-
import { join as join7 } from "node:path";
|
|
7830
|
-
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
7831
|
-
|
|
7832
8176
|
// src/session/runner/title.ts
|
|
7833
8177
|
var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
|
|
7834
8178
|
var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
|
|
@@ -7917,6 +8261,10 @@ var generateTitle = async (input) => {
|
|
|
7917
8261
|
};
|
|
7918
8262
|
|
|
7919
8263
|
// src/session/runner/title-codex.ts
|
|
8264
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
8265
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
8266
|
+
import { join as join9 } from "node:path";
|
|
8267
|
+
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
7920
8268
|
var generateTitleWithCodex = async (input) => {
|
|
7921
8269
|
const { userText, assistantText } = input;
|
|
7922
8270
|
if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
|
|
@@ -7924,7 +8272,7 @@ var generateTitleWithCodex = async (input) => {
|
|
|
7924
8272
|
const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
|
|
7925
8273
|
let cwd = "";
|
|
7926
8274
|
try {
|
|
7927
|
-
cwd = await mkdtemp(
|
|
8275
|
+
cwd = await mkdtemp(join9(tmpdir3(), "baton-title-"));
|
|
7928
8276
|
const client = input.client ?? new Codex2();
|
|
7929
8277
|
const thread = client.startThread({
|
|
7930
8278
|
workingDirectory: cwd,
|
|
@@ -7950,20 +8298,11 @@ var generateTitleWithCodex = async (input) => {
|
|
|
7950
8298
|
}
|
|
7951
8299
|
};
|
|
7952
8300
|
|
|
7953
|
-
// src/worker/proc.ts
|
|
7954
|
-
var killProcessGroup = (proc) => {
|
|
7955
|
-
if (proc.pid === void 0) return;
|
|
7956
|
-
try {
|
|
7957
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
7958
|
-
} catch {
|
|
7959
|
-
}
|
|
7960
|
-
};
|
|
7961
|
-
|
|
7962
8301
|
// src/worker/session-supervisor.ts
|
|
7963
|
-
var
|
|
7964
|
-
const here =
|
|
7965
|
-
const devShim =
|
|
7966
|
-
return
|
|
8302
|
+
var binPath2 = () => {
|
|
8303
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
8304
|
+
const devShim = join10(here, "..", "..", "bin", "baton.mjs");
|
|
8305
|
+
return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
|
|
7967
8306
|
};
|
|
7968
8307
|
var createSessionSupervisor = (deps) => {
|
|
7969
8308
|
const { client, cfg, repo, log, hasTerminal, closeTerminal } = deps;
|
|
@@ -7990,7 +8329,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
7990
8329
|
};
|
|
7991
8330
|
const spawnChild = (sessionId, worktreePath) => {
|
|
7992
8331
|
if (children.has(sessionId)) return;
|
|
7993
|
-
const child =
|
|
8332
|
+
const child = spawn3(process.execPath, [binPath2(), "session", "run", String(sessionId)], {
|
|
7994
8333
|
detached: true,
|
|
7995
8334
|
stdio: "inherit",
|
|
7996
8335
|
env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
|
|
@@ -8016,7 +8355,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
8016
8355
|
if (!session2.agentSessionId || !worktreePath) {
|
|
8017
8356
|
const sessionCode = randomUUID();
|
|
8018
8357
|
const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
|
|
8019
|
-
worktreePath =
|
|
8358
|
+
worktreePath = join10(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
|
|
8020
8359
|
const base = await syncedBase();
|
|
8021
8360
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
|
|
8022
8361
|
createWorktree({
|
|
@@ -8028,13 +8367,14 @@ var createSessionSupervisor = (deps) => {
|
|
|
8028
8367
|
await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
|
|
8029
8368
|
if (wasCanceled()) return log(`session #${sessionId} start canceled after materializing`);
|
|
8030
8369
|
log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
|
|
8031
|
-
} else if (!
|
|
8370
|
+
} else if (!existsSync6(worktreePath)) {
|
|
8032
8371
|
await restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8), baseBranch);
|
|
8033
8372
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during worktree restore`);
|
|
8034
8373
|
log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
|
|
8035
8374
|
}
|
|
8036
8375
|
ensureExcluded(repo, PROJECT_CONFIG_NAME);
|
|
8037
|
-
|
|
8376
|
+
ensureExcluded(repo, ".baton-services/");
|
|
8377
|
+
saveProjectConfig(join10(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
|
|
8038
8378
|
if (hasTerminal(sessionId))
|
|
8039
8379
|
return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
|
|
8040
8380
|
spawnChild(sessionId, worktreePath);
|
|
@@ -8122,8 +8462,8 @@ var createSessionSupervisor = (deps) => {
|
|
|
8122
8462
|
};
|
|
8123
8463
|
|
|
8124
8464
|
// src/worker/terminal-manager.ts
|
|
8125
|
-
import { existsSync as
|
|
8126
|
-
import { spawn as
|
|
8465
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
8466
|
+
import { spawn as spawn4 } from "node-pty";
|
|
8127
8467
|
|
|
8128
8468
|
// ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
|
|
8129
8469
|
var import_stream2 = __toESM(require_stream(), 1);
|
|
@@ -8136,16 +8476,16 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
8136
8476
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
8137
8477
|
|
|
8138
8478
|
// src/worker/pty.ts
|
|
8139
|
-
import { existsSync as
|
|
8479
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2 } from "node:fs";
|
|
8140
8480
|
import { homedir as homedir3 } from "node:os";
|
|
8141
|
-
import { join as
|
|
8481
|
+
import { join as join11 } from "node:path";
|
|
8142
8482
|
var MAX_TERMINALS = 10;
|
|
8143
8483
|
var ptyArgs = (agentSessionId, hasJsonl) => hasJsonl ? ["--resume", agentSessionId] : ["--session-id", agentSessionId];
|
|
8144
8484
|
var hasSessionJsonl = (agentSessionId) => {
|
|
8145
|
-
const root =
|
|
8146
|
-
if (!
|
|
8485
|
+
const root = join11(homedir3(), ".claude", "projects");
|
|
8486
|
+
if (!existsSync7(root)) return false;
|
|
8147
8487
|
for (const dir of readdirSync2(root))
|
|
8148
|
-
if (
|
|
8488
|
+
if (existsSync7(join11(root, dir, `${agentSessionId}.jsonl`))) return true;
|
|
8149
8489
|
return false;
|
|
8150
8490
|
};
|
|
8151
8491
|
var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
|
|
@@ -8156,13 +8496,13 @@ var createTerminalManager = (deps) => {
|
|
|
8156
8496
|
const { cfg, log, hasChild } = deps;
|
|
8157
8497
|
const terminals = /* @__PURE__ */ new Map();
|
|
8158
8498
|
const open = (sessionId, agentSessionId, worktreePath) => {
|
|
8159
|
-
const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !
|
|
8499
|
+
const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !existsSync8(worktreePath) ? `worktree missing (${worktreePath})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
|
|
8160
8500
|
if (why) {
|
|
8161
8501
|
log(`terminal #${sessionId}: ${why} \u2014 refusing`);
|
|
8162
8502
|
return;
|
|
8163
8503
|
}
|
|
8164
8504
|
const claudeBin = process.env.BATON_CLAUDE_BIN ?? "claude";
|
|
8165
|
-
const term =
|
|
8505
|
+
const term = spawn4(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
|
|
8166
8506
|
name: "xterm-color",
|
|
8167
8507
|
cols: 80,
|
|
8168
8508
|
rows: 24,
|
|
@@ -8248,14 +8588,20 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8248
8588
|
log,
|
|
8249
8589
|
hasChild: (id) => supervisor.has(id)
|
|
8250
8590
|
});
|
|
8591
|
+
const services = createServiceSupervisor({
|
|
8592
|
+
getSession: (sessionId) => client.sessions.get(sessionId),
|
|
8593
|
+
sendReport: (report) => client.services.report(report),
|
|
8594
|
+
cfg,
|
|
8595
|
+
log
|
|
8596
|
+
});
|
|
8251
8597
|
let fatal = false;
|
|
8252
8598
|
let stop = () => {
|
|
8253
8599
|
};
|
|
8254
8600
|
const liveness = createLiveness({
|
|
8255
|
-
client,
|
|
8256
|
-
cfg,
|
|
8601
|
+
heartbeat: () => client.workers.heartbeat(cfg.machineId),
|
|
8257
8602
|
log,
|
|
8258
8603
|
isStreamOpen: () => es.readyState === ES_OPEN,
|
|
8604
|
+
reportRuntime: () => services.report(),
|
|
8259
8605
|
onTrip: (reason) => {
|
|
8260
8606
|
if (fatal) return;
|
|
8261
8607
|
log(`${reason} \u2014 exiting so the supervisor restarts a clean worker`);
|
|
@@ -8275,6 +8621,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8275
8621
|
es.onopen = () => {
|
|
8276
8622
|
liveness.markStreamOk();
|
|
8277
8623
|
void supervisor.reconcile().catch((err) => log(`reconcile failed: ${String(err)}`));
|
|
8624
|
+
void services.report().catch((err) => log(`service report failed: ${String(err)}`));
|
|
8278
8625
|
};
|
|
8279
8626
|
es.onmessage = (e) => {
|
|
8280
8627
|
liveness.markStreamOk();
|
|
@@ -8283,14 +8630,18 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8283
8630
|
if (cmd.cmd === "session.start")
|
|
8284
8631
|
void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
|
|
8285
8632
|
else if (cmd.cmd === "session.stop") supervisor.stop(cmd.sessionId);
|
|
8286
|
-
else if (cmd.cmd === "session.delete")
|
|
8633
|
+
else if (cmd.cmd === "session.delete")
|
|
8634
|
+
void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
|
|
8287
8635
|
else if (cmd.cmd === "session.title")
|
|
8288
8636
|
void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath).catch((err) => log(`title failed: ${String(err)}`));
|
|
8289
8637
|
else if (cmd.cmd === "session.terminal") {
|
|
8290
8638
|
if (cmd.action === "open")
|
|
8291
8639
|
terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
|
|
8292
8640
|
else terminals.close(cmd.sessionId);
|
|
8293
|
-
}
|
|
8641
|
+
} else if (cmd.cmd === "service.run")
|
|
8642
|
+
void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv).catch((err) => log(`service run failed: ${String(err)}`));
|
|
8643
|
+
else if (cmd.cmd === "service.stop")
|
|
8644
|
+
void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
|
|
8294
8645
|
} catch {
|
|
8295
8646
|
}
|
|
8296
8647
|
};
|
|
@@ -8304,6 +8655,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8304
8655
|
});
|
|
8305
8656
|
es.close();
|
|
8306
8657
|
liveness.stop();
|
|
8658
|
+
services.killAll();
|
|
8307
8659
|
supervisor.killAll();
|
|
8308
8660
|
terminals.killAll();
|
|
8309
8661
|
if (fatal) process.exit(1);
|
|
@@ -8311,17 +8663,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8311
8663
|
|
|
8312
8664
|
// src/worker/machine-id.ts
|
|
8313
8665
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8314
|
-
import { existsSync as
|
|
8666
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8315
8667
|
import { homedir as homedir4 } from "node:os";
|
|
8316
|
-
import { dirname as
|
|
8317
|
-
var machineIdPath = (env = process.env) =>
|
|
8668
|
+
import { dirname as dirname4, join as join12 } from "node:path";
|
|
8669
|
+
var machineIdPath = (env = process.env) => join12(env.XDG_DATA_HOME ?? join12(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
|
|
8318
8670
|
var readOrCreateMachineId = (path = machineIdPath()) => {
|
|
8319
|
-
if (
|
|
8671
|
+
if (existsSync9(path)) {
|
|
8320
8672
|
const s = readFileSync6(path, "utf8").trim();
|
|
8321
8673
|
if (s) return s;
|
|
8322
8674
|
}
|
|
8323
8675
|
const id = randomUUID2();
|
|
8324
|
-
|
|
8676
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
8325
8677
|
writeFileSync5(path, `${id}
|
|
8326
8678
|
`, "utf8");
|
|
8327
8679
|
return id;
|
|
@@ -8636,6 +8988,7 @@ var main = defineCommand({
|
|
|
8636
8988
|
init,
|
|
8637
8989
|
worker,
|
|
8638
8990
|
session,
|
|
8991
|
+
service,
|
|
8639
8992
|
loop,
|
|
8640
8993
|
relay,
|
|
8641
8994
|
channel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lesscap/baton-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@baton/shared": "0.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
29
|
-
"@openai/codex-sdk": "^0.
|
|
28
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
|
29
|
+
"@openai/codex-sdk": "^0.146.0",
|
|
30
30
|
"node-pty": "^1.1.0",
|
|
31
31
|
"ws": "^8.21.0"
|
|
32
32
|
},
|