@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.
Files changed (2) hide show
  1. package/dist/baton.mjs +415 -62
  2. 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" ? canonicalAgentMessage(event.payload) : null;
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 { client, cfg, log, isStreamOpen, onTrip } = deps;
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
- client.workers.heartbeat(cfg.machineId),
7785
- new Promise(
7786
- (_, reject) => setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS)
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 existsSync5 } from "node:fs";
7822
- import { dirname as dirname2, join as join8 } from "node:path";
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(join7(tmpdir3(), "baton-title-"));
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 binPath = () => {
7964
- const here = dirname2(fileURLToPath(import.meta.url));
7965
- const devShim = join8(here, "..", "..", "bin", "baton.mjs");
7966
- return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
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 = spawn(process.execPath, [binPath(), "session", "run", String(sessionId)], {
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 = join8(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
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 (!existsSync5(worktreePath)) {
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
- saveProjectConfig(join8(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg));
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 existsSync7 } from "node:fs";
8126
- import { spawn as spawn2 } from "node-pty";
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 existsSync6, readdirSync as readdirSync2 } from "node:fs";
8479
+ import { existsSync as existsSync7, readdirSync as readdirSync2 } from "node:fs";
8140
8480
  import { homedir as homedir3 } from "node:os";
8141
- import { join as join9 } from "node:path";
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 = join9(homedir3(), ".claude", "projects");
8146
- if (!existsSync6(root)) return false;
8485
+ const root = join11(homedir3(), ".claude", "projects");
8486
+ if (!existsSync7(root)) return false;
8147
8487
  for (const dir of readdirSync2(root))
8148
- if (existsSync6(join9(root, dir, `${agentSessionId}.jsonl`))) return true;
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" : !existsSync7(worktreePath) ? `worktree missing (${worktreePath})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
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 = spawn2(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
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") supervisor.remove(cmd.sessionId, cmd.worktreePath);
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 existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
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 dirname3, join as join10 } from "node:path";
8317
- var machineIdPath = (env = process.env) => join10(env.XDG_DATA_HOME ?? join10(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
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 (existsSync8(path)) {
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
- mkdirSync4(dirname3(path), { recursive: true });
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.4",
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.209",
29
- "@openai/codex-sdk": "^0.144.4",
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
  },