@lesscap/baton-cli 0.8.4 → 0.9.4

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 (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/baton.mjs +1094 -202
  3. package/package.json +18 -18
package/dist/baton.mjs CHANGED
@@ -14,7 +14,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
14
14
  throw Error('Dynamic require of "' + x + '" is not supported');
15
15
  });
16
16
  var __commonJS = (cb, mod) => function __require2() {
17
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
18
22
  };
19
23
  var __copyProps = (to, from, except, desc) => {
20
24
  if (from && typeof from === "object" || typeof from === "function") {
@@ -3230,7 +3234,7 @@ var require_stream = __commonJS({
3230
3234
  };
3231
3235
  duplex._final = function(callback) {
3232
3236
  if (ws.readyState === ws.CONNECTING) {
3233
- ws.once("open", function open2() {
3237
+ ws.once("open", function open3() {
3234
3238
  duplex._final(callback);
3235
3239
  });
3236
3240
  return;
@@ -3251,7 +3255,7 @@ var require_stream = __commonJS({
3251
3255
  };
3252
3256
  duplex._write = function(chunk, encoding, callback) {
3253
3257
  if (ws.readyState === ws.CONNECTING) {
3254
- ws.once("open", function open2() {
3258
+ ws.once("open", function open3() {
3255
3259
  duplex._write(chunk, encoding, callback);
3256
3260
  });
3257
3261
  return;
@@ -3724,14 +3728,14 @@ function isUppercase(char = "") {
3724
3728
  if (NUMBER_CHAR_RE.test(char)) return;
3725
3729
  return char !== char.toLowerCase();
3726
3730
  }
3727
- function splitByCase(str3, separators) {
3731
+ function splitByCase(str4, separators) {
3728
3732
  const splitters = separators ?? STR_SPLITTERS;
3729
3733
  const parts = [];
3730
- if (!str3 || typeof str3 !== "string") return parts;
3734
+ if (!str4 || typeof str4 !== "string") return parts;
3731
3735
  let buff = "";
3732
3736
  let previousUpper;
3733
3737
  let previousSplitter;
3734
- for (const char of str3) {
3738
+ for (const char of str4) {
3735
3739
  const isSplitter = splitters.includes(char);
3736
3740
  if (isSplitter === true) {
3737
3741
  parts.push(buff);
@@ -3762,23 +3766,23 @@ function splitByCase(str3, separators) {
3762
3766
  parts.push(buff);
3763
3767
  return parts;
3764
3768
  }
3765
- function upperFirst(str3) {
3766
- return str3 ? str3[0].toUpperCase() + str3.slice(1) : "";
3769
+ function upperFirst(str4) {
3770
+ return str4 ? str4[0].toUpperCase() + str4.slice(1) : "";
3767
3771
  }
3768
- function lowerFirst(str3) {
3769
- return str3 ? str3[0].toLowerCase() + str3.slice(1) : "";
3772
+ function lowerFirst(str4) {
3773
+ return str4 ? str4[0].toLowerCase() + str4.slice(1) : "";
3770
3774
  }
3771
- function pascalCase(str3, opts) {
3772
- return str3 ? (Array.isArray(str3) ? str3 : splitByCase(str3)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
3775
+ function pascalCase(str4, opts) {
3776
+ return str4 ? (Array.isArray(str4) ? str4 : splitByCase(str4)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
3773
3777
  }
3774
- function camelCase(str3, opts) {
3775
- return lowerFirst(pascalCase(str3 || "", opts));
3778
+ function camelCase(str4, opts) {
3779
+ return lowerFirst(pascalCase(str4 || "", opts));
3776
3780
  }
3777
- function kebabCase(str3, joiner) {
3778
- return str3 ? (Array.isArray(str3) ? str3 : splitByCase(str3)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
3781
+ function kebabCase(str4, joiner) {
3782
+ return str4 ? (Array.isArray(str4) ? str4 : splitByCase(str4)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
3779
3783
  }
3780
- function snakeCase(str3) {
3781
- return kebabCase(str3 || "", "_");
3784
+ function snakeCase(str4) {
3785
+ return kebabCase(str4 || "", "_");
3782
3786
  }
3783
3787
 
3784
3788
  // ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
@@ -5488,6 +5492,11 @@ var primeLogin = (baseUrl) => {
5488
5492
  );
5489
5493
  };
5490
5494
 
5495
+ // src/client/files.ts
5496
+ var filesClient = (baseUrl) => ({
5497
+ report: (result) => request(`${baseUrl}/workers/me/files/results`, { method: "PUT", body: result })
5498
+ });
5499
+
5491
5500
  // src/client/loops.ts
5492
5501
  var loopsClient = (baseUrl) => {
5493
5502
  const u = (p) => `${baseUrl}${p}`;
@@ -5667,7 +5676,8 @@ var sessionsClient = (baseUrl) => {
5667
5676
  },
5668
5677
  destroy: async (id) => {
5669
5678
  await request(u(`/sessions/${id}`), { method: "DELETE" });
5670
- }
5679
+ },
5680
+ notify: (id, text) => request(u(`/sessions/${id}/notify`), { method: "POST", body: { text } })
5671
5681
  };
5672
5682
  };
5673
5683
 
@@ -5743,6 +5753,7 @@ var clientFromBase = (baseUrl) => ({
5743
5753
  tasks: taskClient(baseUrl),
5744
5754
  sessions: sessionsClient(baseUrl),
5745
5755
  services: servicesClient(baseUrl),
5756
+ files: filesClient(baseUrl),
5746
5757
  loops: loopsClient(baseUrl),
5747
5758
  workers: workersClient(baseUrl)
5748
5759
  });
@@ -6262,11 +6273,11 @@ var EventSource = class extends EventTarget {
6262
6273
  return;
6263
6274
  }
6264
6275
  const decoder = new TextDecoder(), reader = body.getReader();
6265
- let open2 = true;
6276
+ let open3 = true;
6266
6277
  do {
6267
6278
  const { done, value } = await reader.read();
6268
- value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open2 = false, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));
6269
- } while (open2);
6279
+ value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open3 = false, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));
6280
+ } while (open3);
6270
6281
  }), __privateAdd(this, _onFetchError, (err) => {
6271
6282
  __privateSet(this, _controller, void 0), !(err.name === "AbortError" || err.type === "aborted") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));
6272
6283
  }), __privateAdd(this, _onEvent, (event) => {
@@ -6721,11 +6732,10 @@ import { tmpdir } from "node:os";
6721
6732
  import { join as join2 } from "node:path";
6722
6733
 
6723
6734
  // ../shared/src/domain/attachment.ts
6724
- var isImageAttachment = (att) => att.contentType.startsWith("image/");
6725
6735
  var labelAttachments = (attachments) => {
6726
6736
  const counters = { image: 0, file: 0 };
6727
6737
  return attachments.map((att) => {
6728
- const kind = isImageAttachment(att) ? "image" : "file";
6738
+ const kind = att.contentType.startsWith("image/") ? "image" : "file";
6729
6739
  counters[kind] += 1;
6730
6740
  return `${kind}-${counters[kind]}`;
6731
6741
  });
@@ -6734,6 +6744,77 @@ var labelAttachments = (attachments) => {
6734
6744
  // ../shared/src/domain/channel.ts
6735
6745
  var isMessageFor = (msg, name) => !msg.to || msg.to.length === 0 || msg.to.includes(name);
6736
6746
 
6747
+ // ../shared/src/domain/file.ts
6748
+ var FILE_TEXT_MAX_BYTES = 5 * 1024 * 1024;
6749
+ var FILE_DIRECTORY_MAX_ENTRIES = 2e3;
6750
+ var FILE_SEARCH_MAX_RESULTS = 500;
6751
+ var FILE_SEARCH_DEADLINE_MS = 5e3;
6752
+ var FILE_SEARCH_QUERY_MAX_LENGTH = 200;
6753
+ var FILE_RAW_MAX_BYTES = 5 * 1024 * 1024;
6754
+ var IMAGE_MEDIA_TYPES = {
6755
+ png: "image/png",
6756
+ jpg: "image/jpeg",
6757
+ jpeg: "image/jpeg",
6758
+ gif: "image/gif",
6759
+ webp: "image/webp",
6760
+ avif: "image/avif",
6761
+ bmp: "image/bmp",
6762
+ ico: "image/x-icon",
6763
+ svg: "image/svg+xml"
6764
+ };
6765
+ var RAW_MEDIA_TYPES = {
6766
+ ...IMAGE_MEDIA_TYPES,
6767
+ css: "text/css",
6768
+ woff: "font/woff",
6769
+ woff2: "font/woff2",
6770
+ ttf: "font/ttf",
6771
+ otf: "font/otf"
6772
+ };
6773
+ var mediaTypeIn = (types3) => (path) => {
6774
+ const extension2 = /\.([a-z\d]+)$/i.exec(path)?.[1]?.toLowerCase();
6775
+ return extension2 && types3[extension2] || null;
6776
+ };
6777
+ var imageMediaType = mediaTypeIn(IMAGE_MEDIA_TYPES);
6778
+ var rawMediaType = mediaTypeIn(RAW_MEDIA_TYPES);
6779
+ var normalizeFilePath = (value, allowRoot = false) => {
6780
+ if (value === "") return allowRoot ? "" : null;
6781
+ if (value.includes("\0") || value.includes("\\") || value.startsWith("/")) return null;
6782
+ if (/^[A-Za-z]:/.test(value)) return null;
6783
+ return value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..") ? value : null;
6784
+ };
6785
+ var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([".git", ".ssh", ".aws", ".kube", ".gnupg"]);
6786
+ var SENSITIVE_NAMES = /* @__PURE__ */ new Set([
6787
+ ".baton.json",
6788
+ ".envrc",
6789
+ ".npmrc",
6790
+ ".netrc",
6791
+ ".pypirc",
6792
+ ".git-credentials",
6793
+ ".pgpass",
6794
+ ".htpasswd",
6795
+ ".credentials.json"
6796
+ ]);
6797
+ var SENSITIVE_EXTENSIONS = [".pem", ".key", ".p12", ".pfx", ".jks", ".keystore"];
6798
+ var isSensitiveName = (name) => SENSITIVE_NAMES.has(name) || /^\.env(\..+)?$/.test(name) && !/\.(example|sample|template)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || SENSITIVE_EXTENSIONS.some((extension2) => name.endsWith(extension2));
6799
+ var isSensitiveFilePath = (path) => {
6800
+ const segments = path.toLowerCase().split("/");
6801
+ return segments.some((segment) => SENSITIVE_SEGMENTS.has(segment)) || isSensitiveName(segments.at(-1) ?? "");
6802
+ };
6803
+ var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
6804
+ "node_modules",
6805
+ ".pnpm-store",
6806
+ "dist",
6807
+ "build",
6808
+ "coverage",
6809
+ ".next",
6810
+ ".turbo",
6811
+ ".cache",
6812
+ ".venv",
6813
+ "__pycache__",
6814
+ "target"
6815
+ ]);
6816
+ var isExcludedDirectoryName = (name) => EXCLUDED_DIRECTORIES.has(name);
6817
+
6737
6818
  // ../shared/src/domain/service.ts
6738
6819
  var SERVICE_PUBLIC_URL_MAX_LENGTH = 2048;
6739
6820
  var SERVICE_NOTE_MAX_LENGTH = 500;
@@ -6748,6 +6829,12 @@ var isServicePublicUrl = (value) => {
6748
6829
  };
6749
6830
  var isServiceNote = (value) => value.length > 0 && value.length <= SERVICE_NOTE_MAX_LENGTH;
6750
6831
 
6832
+ // ../shared/src/domain/session.ts
6833
+ var AGENT_KINDS = ["claude-code", "codex", "pi"];
6834
+ var isAgentKind = (v) => typeof v === "string" && AGENT_KINDS.includes(v);
6835
+ var EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
6836
+ var isAgentEffort = (v) => typeof v === "string" && EFFORTS.includes(v);
6837
+
6751
6838
  // ../shared/src/domain/session-event.ts
6752
6839
  var agentMessageText = (payload) => {
6753
6840
  if (typeof payload !== "object" || payload === null) return null;
@@ -6761,6 +6848,15 @@ var agentMessageText = (payload) => {
6761
6848
  return trimmed ? { id, text: trimmed } : null;
6762
6849
  };
6763
6850
 
6851
+ // ../shared/src/domain/worker-creation.ts
6852
+ var WORKER_TEMPLATES = [
6853
+ { id: "mini-glm-codeplan", name: "GLM Code Plan", agentKinds: ["claude-code", "pi"] },
6854
+ { id: "mini-dashscope-codeplan", name: "DashScope Code Plan", agentKinds: ["claude-code", "pi"] },
6855
+ { id: "mini-codex", name: "\u672C\u673A Codex", agentKinds: ["codex"] }
6856
+ ];
6857
+ var workerTemplateAgentKinds = (id) => id === "mini-local" ? AGENT_KINDS : WORKER_TEMPLATES.find((template) => template.id === id)?.agentKinds ?? [];
6858
+ var isWorkerTemplateId = (value) => typeof value === "string" && workerTemplateAgentKinds(value).length > 0;
6859
+
6764
6860
  // src/commands/channel/listen.ts
6765
6861
  var PREVIEW = 280;
6766
6862
  var describe = (e) => {
@@ -7221,6 +7317,34 @@ var loop = defineCommand({
7221
7317
  }
7222
7318
  });
7223
7319
 
7320
+ // src/commands/notify.ts
7321
+ var describe2 = (result) => {
7322
+ if (result.status === "sent") return `notified ${result.devices} device(s)`;
7323
+ if (result.status === "throttled")
7324
+ return `throttled: this session can notify again in ${result.retryAfterSec}s`;
7325
+ return "nobody to notify: no human has messaged this session, or they have not enabled notifications";
7326
+ };
7327
+ var notify = defineCommand({
7328
+ meta: {
7329
+ name: "notify",
7330
+ description: "Push a phone notification to the person working with this session. Use it only when a human needs to act now: something is wrong, a decision is needed, or the work is blocked. Do not use it for routine progress, and not to announce that a turn finished (that is already pushed). At most one notification per session every 10 minutes; text is cut at 200 characters."
7331
+ },
7332
+ args: {
7333
+ text: { type: "positional", required: true, description: "one short line for the lock screen" },
7334
+ ...common
7335
+ },
7336
+ run: async ({ args }) => {
7337
+ const raw = process.env.BATON_SESSION_ID;
7338
+ if (raw === void 0)
7339
+ throw new Error("baton notify only works inside a baton session (BATON_SESSION_ID is unset)");
7340
+ const sessionId = Number(raw);
7341
+ if (!Number.isSafeInteger(sessionId) || sessionId <= 0)
7342
+ throw new Error("invalid BATON_SESSION_ID");
7343
+ const result = await clientFor(args).sessions.notify(sessionId, args.text);
7344
+ console.log(args.json ? toJson(result) : describe2(result));
7345
+ }
7346
+ });
7347
+
7224
7348
  // src/commands/project.ts
7225
7349
  var createProject = (c, input, json) => c.projects.create(input).then((p) => renderOne(p, fmtProject, json));
7226
7350
  var listProjects = (c, workspaceId, json) => c.projects.listByWorkspace(workspaceId).then((ps) => renderList(ps, fmtProject, json));
@@ -7316,6 +7440,37 @@ var absolutePath = (value, field) => {
7316
7440
  throw new Error(`${field} must be an absolute path`);
7317
7441
  return value;
7318
7442
  };
7443
+ var environment = (value) => {
7444
+ if (!object(value)) throw new Error("workerEnv must be an object");
7445
+ return Object.fromEntries(
7446
+ Object.entries(value).map(([key, entry]) => {
7447
+ if (forbiddenEnv.has(key) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof entry !== "string" || entry.includes("\0"))
7448
+ throw new Error(`invalid worker environment key: ${key}`);
7449
+ return [key, entry];
7450
+ })
7451
+ );
7452
+ };
7453
+ var templates = (value) => {
7454
+ if (!object(value)) throw new Error("templates must be an object");
7455
+ return Object.fromEntries(
7456
+ WORKER_TEMPLATES.map((template) => {
7457
+ const raw = value[template.id];
7458
+ if (!object(raw)) throw new Error(`template ${template.id} must be configured`);
7459
+ const workerEnv2 = environment(raw.workerEnv);
7460
+ if (template.id === "mini-codex") {
7461
+ if (raw.pi !== void 0) throw new Error("mini-codex does not support pi");
7462
+ return [template.id, { workerEnv: workerEnv2 }];
7463
+ }
7464
+ const provider = template.id === "mini-glm-codeplan" ? "zai-coding-cn" : "qwen-token-plan-cn";
7465
+ const apiKey = template.id === "mini-glm-codeplan" ? "ZAI_CODING_CN_API_KEY" : "QWEN_TOKEN_PLAN_CN_API_KEY";
7466
+ if (!workerEnv2.ANTHROPIC_BASE_URL || !(workerEnv2.ANTHROPIC_AUTH_TOKEN || workerEnv2.ANTHROPIC_API_KEY) || !workerEnv2[apiKey])
7467
+ throw new Error(`template ${template.id} requires Claude and Pi credentials`);
7468
+ if (!object(raw.pi) || raw.pi.provider !== provider || typeof raw.pi.model !== "string" || !raw.pi.model.trim())
7469
+ throw new Error(`template ${template.id} requires its Pi provider and model`);
7470
+ return [template.id, { workerEnv: workerEnv2, pi: { provider, model: raw.pi.model.trim() } }];
7471
+ })
7472
+ );
7473
+ };
7319
7474
  var commandEnvironment = (config) => ({
7320
7475
  ...process.env,
7321
7476
  PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin"
@@ -7345,21 +7500,14 @@ var loadProvisionerConfig = async (path) => {
7345
7500
  throw new Error("server must be an HTTP(S) URL without credentials, query, or fragment");
7346
7501
  if (!raw.token.trim() || /[\r\n]/.test(raw.token))
7347
7502
  throw new Error("valid provisioner token required");
7348
- if (!object(raw.workerEnv)) throw new Error("workerEnv must be an object");
7349
- const workerEnv2 = Object.fromEntries(
7350
- Object.entries(raw.workerEnv).map(([key, value]) => {
7351
- if (forbiddenEnv.has(key) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== "string" || value.includes("\0"))
7352
- throw new Error(`invalid worker environment key: ${key}`);
7353
- return [key, value];
7354
- })
7355
- );
7356
7503
  const config = {
7357
7504
  server: server.toString().replace(/\/$/, ""),
7358
7505
  token: raw.token.trim(),
7359
7506
  projectsRoot: absolutePath(raw.projectsRoot, "projectsRoot"),
7360
7507
  cliPath: absolutePath(raw.cliPath, "cliPath"),
7361
7508
  nodePath: absolutePath(raw.nodePath ?? process.execPath, "nodePath"),
7362
- workerEnv: workerEnv2
7509
+ workerEnv: environment(raw.workerEnv),
7510
+ templates: templates(raw.templates)
7363
7511
  };
7364
7512
  await access(config.cliPath, constants.R_OK);
7365
7513
  await access(config.nodePath ?? process.execPath, constants.X_OK);
@@ -7399,9 +7547,9 @@ var createProvisionerClient = (config) => {
7399
7547
 
7400
7548
  // src/provisioner/executor.ts
7401
7549
  import { execFile as execFile3 } from "node:child_process";
7402
- import { lstat, mkdir as mkdir2, realpath, writeFile } from "node:fs/promises";
7550
+ import { lstat, mkdir as mkdir3, realpath, writeFile as writeFile2 } from "node:fs/promises";
7403
7551
  import { hostname } from "node:os";
7404
- import { join as join4 } from "node:path";
7552
+ import { join as join5 } from "node:path";
7405
7553
  import { setTimeout as setTimeout2 } from "node:timers/promises";
7406
7554
  import { promisify as promisify3 } from "node:util";
7407
7555
 
@@ -7472,15 +7620,15 @@ var startAgent = async (config, agent, signal) => {
7472
7620
  throw new Error(`launchd start failed; inspect ${plist} and ${agent.log}`);
7473
7621
  }
7474
7622
  };
7475
- var launchWorkerAgent = async (config, creation, repoPath, signal) => startAgent(
7623
+ var launchWorkerAgent = async (config, creation, repoPath, workerEnv2, signal) => startAgent(
7476
7624
  config,
7477
7625
  {
7478
7626
  label: `dev.fmap.baton-worker-creation-${creation.id}`,
7479
7627
  args: [config.nodePath ?? process.execPath, config.cliPath, "worker", "run"],
7480
7628
  cwd: repoPath,
7481
7629
  env: {
7482
- PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin",
7483
- ...config.workerEnv,
7630
+ PATH: workerEnv2.PATH ?? process.env.PATH ?? "/usr/bin:/bin",
7631
+ ...workerEnv2,
7484
7632
  HOME: homedir(),
7485
7633
  XDG_DATA_HOME: workerStatePath(config, creation)
7486
7634
  },
@@ -7503,10 +7651,34 @@ var installProvisionerAgent = async (config, configPath) => startAgent(config, {
7503
7651
  log: join3(homedir(), "Library/Logs/baton-provisioner.log")
7504
7652
  });
7505
7653
 
7654
+ // src/provisioner/worker-template.ts
7655
+ import { mkdir as mkdir2, writeFile } from "node:fs/promises";
7656
+ import { join as join4 } from "node:path";
7657
+ var workerTemplateEnvironment = async (config, creation, state) => {
7658
+ if (!workerTemplateAgentKinds(creation.templateId).includes(creation.agentKind))
7659
+ throw new Error("agentKind is not supported by this template");
7660
+ if (creation.templateId === "mini-local") return config.workerEnv;
7661
+ const profile = config.templates[creation.templateId];
7662
+ if (!profile) throw new Error(`template ${creation.templateId} is not configured`);
7663
+ if (creation.agentKind !== "pi") return profile.workerEnv;
7664
+ if (!profile.pi) throw new Error("Pi provider and model are not configured");
7665
+ const agentDir = join4(state, "pi");
7666
+ await mkdir2(agentDir, { mode: 448 });
7667
+ await writeFile(
7668
+ join4(agentDir, "settings.json"),
7669
+ JSON.stringify({
7670
+ defaultProvider: profile.pi.provider,
7671
+ defaultModel: profile.pi.model
7672
+ }),
7673
+ { flag: "wx", mode: 384 }
7674
+ );
7675
+ return { ...profile.workerEnv, PI_CODING_AGENT_DIR: agentDir };
7676
+ };
7677
+
7506
7678
  // src/provisioner/executor.ts
7507
7679
  var directory = async (path) => {
7508
7680
  try {
7509
- await mkdir2(path, { mode: 448 });
7681
+ await mkdir3(path, { mode: 448 });
7510
7682
  } catch (error) {
7511
7683
  if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
7512
7684
  }
@@ -7514,7 +7686,7 @@ var directory = async (path) => {
7514
7686
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`unsafe directory: ${path}`);
7515
7687
  };
7516
7688
  var provisionWorker = async (config, creation, client, signal) => {
7517
- const repoPath = join4(
7689
+ const repoPath = join5(
7518
7690
  config.projectsRoot,
7519
7691
  `project-${creation.projectId}`,
7520
7692
  `creation-${creation.id}`
@@ -7522,14 +7694,14 @@ var provisionWorker = async (config, creation, client, signal) => {
7522
7694
  const progress = (step) => client.report(creation.id, { status: "running", step, repoPath }, signal);
7523
7695
  await progress("directory");
7524
7696
  signal.throwIfAborted();
7525
- await mkdir2(config.projectsRoot, { recursive: true, mode: 448 });
7697
+ await mkdir3(config.projectsRoot, { recursive: true, mode: 448 });
7526
7698
  const root = await realpath(config.projectsRoot);
7527
- await directory(join4(root, `project-${creation.projectId}`));
7528
- await mkdir2(join4(root, `project-${creation.projectId}`, `creation-${creation.id}`), {
7699
+ await directory(join5(root, `project-${creation.projectId}`));
7700
+ await mkdir3(join5(root, `project-${creation.projectId}`, `creation-${creation.id}`), {
7529
7701
  mode: 448
7530
7702
  });
7531
7703
  await progress("git");
7532
- const git = async (args) => {
7704
+ const git2 = async (args) => {
7533
7705
  try {
7534
7706
  await promisify3(execFile3)("git", ["-C", repoPath, ...args], {
7535
7707
  env: commandEnvironment(config),
@@ -7541,9 +7713,9 @@ var provisionWorker = async (config, creation, client, signal) => {
7541
7713
  throw new Error("Git repository initialization failed", { cause: error });
7542
7714
  }
7543
7715
  };
7544
- await git(["init", "-b", "main"]);
7545
- await writeFile(join4(repoPath, ".git/info/exclude"), "\n.baton.json\n", { flag: "a" });
7546
- await git([
7716
+ await git2(["init", "-b", "main"]);
7717
+ await writeFile2(join5(repoPath, ".git/info/exclude"), "\n.baton.json\n", { flag: "a" });
7718
+ await git2([
7547
7719
  "-c",
7548
7720
  "user.name=Baton",
7549
7721
  "-c",
@@ -7556,11 +7728,11 @@ var provisionWorker = async (config, creation, client, signal) => {
7556
7728
  await progress("register");
7557
7729
  const result = await client.register(creation.id, hostname(), signal);
7558
7730
  const state = workerStatePath(config, creation);
7559
- await directory(join4(root, ".worker-state"));
7560
- await directory(join4(root, ".worker-state", `project-${creation.projectId}`));
7561
- await mkdir2(state, { mode: 448 });
7562
- await mkdir2(join4(state, "baton"), { mode: 448 });
7563
- await writeFile(join4(state, "baton/machine-id"), `${creation.machineId}
7731
+ await directory(join5(root, ".worker-state"));
7732
+ await directory(join5(root, ".worker-state", `project-${creation.projectId}`));
7733
+ await mkdir3(state, { mode: 448 });
7734
+ await mkdir3(join5(state, "baton"), { mode: 448 });
7735
+ await writeFile2(join5(state, "baton/machine-id"), `${creation.machineId}
7564
7736
  `, {
7565
7737
  flag: "wx",
7566
7738
  mode: 384
@@ -7574,14 +7746,15 @@ var provisionWorker = async (config, creation, client, signal) => {
7574
7746
  machineId: result.worker.machineId,
7575
7747
  apiToken: result.apiToken
7576
7748
  });
7577
- await writeFile(join4(repoPath, ".baton.json"), `${JSON.stringify(cfg, null, 2)}
7749
+ await writeFile2(join5(repoPath, ".baton.json"), `${JSON.stringify(cfg, null, 2)}
7578
7750
  `, {
7579
7751
  flag: "wx",
7580
7752
  mode: 384
7581
7753
  });
7582
7754
  await progress("start");
7583
7755
  signal.throwIfAborted();
7584
- await launchWorkerAgent(config, creation, repoPath, signal);
7756
+ const workerEnv2 = await workerTemplateEnvironment(config, creation, state);
7757
+ await launchWorkerAgent(config, creation, repoPath, workerEnv2, signal);
7585
7758
  await progress("connect");
7586
7759
  const deadline = Date.now() + 6e4;
7587
7760
  while (Date.now() < deadline) {
@@ -7598,7 +7771,11 @@ var provisionWorker = async (config, creation, client, signal) => {
7598
7771
  // src/provisioner/daemon.ts
7599
7772
  var taskError = (error, config) => {
7600
7773
  const message = error instanceof Error ? error.message : "unknown creation error";
7601
- return [config.token, ...Object.values(config.workerEnv)].filter((value) => value.length > 0).reduce((safe, secret) => safe.replaceAll(secret, "[redacted]"), message);
7774
+ return [
7775
+ config.token,
7776
+ ...Object.values(config.workerEnv),
7777
+ ...Object.values(config.templates).flatMap((template) => Object.values(template.workerEnv))
7778
+ ].filter((value) => value.length > 0).reduce((safe, secret) => safe.replaceAll(secret, "[redacted]"), message);
7602
7779
  };
7603
7780
  var processCreation = async (id, config, client, signal) => {
7604
7781
  let creation;
@@ -7754,7 +7931,7 @@ var provisioner = defineCommand({
7754
7931
  // src/commands/relay.ts
7755
7932
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
7756
7933
  import { tmpdir as tmpdir2 } from "node:os";
7757
- import { join as join5 } from "node:path";
7934
+ import { join as join6 } from "node:path";
7758
7935
 
7759
7936
  // src/client/relay.ts
7760
7937
  var relayClient = (baseUrl) => {
@@ -7882,11 +8059,11 @@ var listenCommand2 = defineCommand({
7882
8059
  process.stdout.write(`${JSON.stringify(o)}
7883
8060
  `);
7884
8061
  };
7885
- const spillDir = join5(tmpdir2(), "hotline", args.channel);
8062
+ const spillDir = join6(tmpdir2(), "hotline", args.channel);
7886
8063
  mkdirSync2(spillDir, { recursive: true });
7887
8064
  const surface = (m) => {
7888
8065
  const text = typeof m.text === "string" ? m.text : "";
7889
- const file = join5(spillDir, `${m.seq}.txt`);
8066
+ const file = join6(spillDir, `${m.seq}.txt`);
7890
8067
  writeFileSync4(file, text);
7891
8068
  const truncated = text.length > PREVIEW2;
7892
8069
  emit({
@@ -7900,7 +8077,7 @@ var listenCommand2 = defineCommand({
7900
8077
  file
7901
8078
  });
7902
8079
  };
7903
- const describe2 = (e) => {
8080
+ const describe3 = (e) => {
7904
8081
  const ev = e;
7905
8082
  return ev?.code ? `${ev.code}${ev.message ? ` ${ev.message}` : ""}` : String(e);
7906
8083
  };
@@ -7910,7 +8087,7 @@ var listenCommand2 = defineCommand({
7910
8087
  onMessage: (m) => {
7911
8088
  if (m.from !== me) surface(m);
7912
8089
  },
7913
- onError: (e) => emit({ type: "relay.error", error: describe2(e) })
8090
+ onError: (e) => emit({ type: "relay.error", error: describe3(e) })
7914
8091
  });
7915
8092
  emit({ type: "relay.listening", channel: args.channel, from: me, url });
7916
8093
  await new Promise((resolve4) => {
@@ -8361,12 +8538,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
8361
8538
  // src/session/runner/transcript.ts
8362
8539
  import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
8363
8540
  import { homedir as homedir2 } from "node:os";
8364
- import { join as join6 } from "node:path";
8541
+ import { join as join7 } from "node:path";
8365
8542
  var findTranscriptPath = (agentSessionId) => {
8366
- const root = join6(homedir2(), ".claude", "projects");
8543
+ const root = join7(homedir2(), ".claude", "projects");
8367
8544
  try {
8368
8545
  for (const dir of readdirSync(root)) {
8369
- const candidate = join6(root, dir, `${agentSessionId}.jsonl`);
8546
+ const candidate = join7(root, dir, `${agentSessionId}.jsonl`);
8370
8547
  try {
8371
8548
  if (statSync(candidate).isFile()) return candidate;
8372
8549
  } catch {
@@ -8444,13 +8621,13 @@ var readFirstExchange = (agentSessionId) => {
8444
8621
  };
8445
8622
 
8446
8623
  // src/session/runner/turn.ts
8447
- import { mkdir as mkdir4, writeFile as writeFile2 } from "node:fs/promises";
8448
- import { join as join10 } from "node:path";
8624
+ import { mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
8625
+ import { join as join12 } from "node:path";
8449
8626
 
8450
8627
  // src/session/runner/attachments.ts
8451
8628
  import { createWriteStream } from "node:fs";
8452
- import { mkdir as mkdir3 } from "node:fs/promises";
8453
- import { join as join7 } from "node:path";
8629
+ import { mkdir as mkdir4 } from "node:fs/promises";
8630
+ import { join as join8 } from "node:path";
8454
8631
  import { Readable } from "node:stream";
8455
8632
  import { pipeline } from "node:stream/promises";
8456
8633
  var dedupe = (name, taken) => {
@@ -8473,15 +8650,15 @@ var dedupe = (name, taken) => {
8473
8650
  var materializeAttachments = async (opts) => {
8474
8651
  const fetchImpl = opts.fetchImpl ?? fetch;
8475
8652
  const dir = opts.directory;
8476
- await mkdir3(dir, { recursive: true });
8653
+ await mkdir4(dir, { recursive: true });
8477
8654
  const taken = /* @__PURE__ */ new Set();
8478
8655
  const paths = [];
8479
8656
  for (const att of opts.attachments) {
8480
8657
  const name = dedupe(att.filename, taken);
8481
8658
  const res = await fetchImpl(`${opts.serverBase}${att.url}`);
8482
8659
  if (!res.ok || !res.body) throw new Error(`attachment ${att.id} \u2192 ${res.status}`);
8483
- await pipeline(Readable.fromWeb(res.body), createWriteStream(join7(dir, name)));
8484
- paths.push(join7(dir, name));
8660
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(join8(dir, name)));
8661
+ paths.push(join8(dir, name));
8485
8662
  }
8486
8663
  return paths;
8487
8664
  };
@@ -8497,7 +8674,7 @@ ${text}` : header;
8497
8674
  };
8498
8675
 
8499
8676
  // src/session/runner/codex.ts
8500
- import { join as join8 } from "node:path";
8677
+ import { join as join9 } from "node:path";
8501
8678
  import { Codex } from "@openai/codex-sdk";
8502
8679
 
8503
8680
  // src/session/runner/sdk-env.ts
@@ -8510,6 +8687,7 @@ var additionalDirs = (required = []) => {
8510
8687
  const dirs = [...required, ...(raw ?? "").split(":")].map((s) => s.trim()).filter((s) => s !== "" && isAbsolute2(s) && existsSync3(s));
8511
8688
  return dirs.length > 0 ? [...new Set(dirs)] : void 0;
8512
8689
  };
8690
+ var PLAN_MODE_PREFIX = "Plan mode: inspect and propose a plan only. Do not edit files or run mutating commands.\n\n";
8513
8691
  var truthy = (value) => {
8514
8692
  if (value === void 0) return void 0;
8515
8693
  const normalized = value.trim().toLowerCase();
@@ -8530,17 +8708,8 @@ var codexApprovalPolicy = () => {
8530
8708
  return raw;
8531
8709
  return "never";
8532
8710
  };
8533
- var claudeEffort = (raw) => {
8534
- if (raw === "minimal") return "low";
8535
- if (raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
8536
- return raw;
8537
- return void 0;
8538
- };
8539
- var codexEffort = (raw) => {
8540
- if (raw === "minimal" || raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max")
8541
- return raw;
8542
- return void 0;
8543
- };
8711
+ var claudeEffort = (raw) => raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max" ? raw : void 0;
8712
+ var codexEffort = (raw) => isAgentEffort(raw) ? raw : void 0;
8544
8713
 
8545
8714
  // src/session/runner/codex.ts
8546
8715
  var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -8658,17 +8827,16 @@ var toAgentEvent = (raw) => {
8658
8827
  return { type: "raw", raw };
8659
8828
  };
8660
8829
  var withPlanPrefix = (input) => {
8661
- const prefix = "Plan mode: inspect and propose a plan only. Do not edit files or run mutating commands.\n\n";
8662
- if (typeof input === "string") return `${prefix}${input}`;
8830
+ if (typeof input === "string") return `${PLAN_MODE_PREFIX}${input}`;
8663
8831
  return input.map(
8664
- (entry, i) => i === 0 && entry.type === "text" ? { ...entry, text: `${prefix}${entry.text}` } : entry
8832
+ (entry, i) => i === 0 && entry.type === "text" ? { ...entry, text: `${PLAN_MODE_PREFIX}${entry.text}` } : entry
8665
8833
  );
8666
8834
  };
8667
8835
  async function* startCodexEvents(config, input, worker2, opts) {
8668
8836
  const env = compactEnv(buildSdkEnv(opts.envOverlay));
8669
8837
  const codex = new Codex(env ? { env } : void 0);
8670
8838
  const resuming = !config.agentSessionId.startsWith("pending:");
8671
- const addDirs = additionalDirs([join8(config.runtimeDir, "attachments")]);
8839
+ const addDirs = additionalDirs([join9(config.runtimeDir, "attachments")]);
8672
8840
  const sandboxMode = codexSandboxMode(opts.planMode ?? false);
8673
8841
  const approvalPolicy = codexApprovalPolicy();
8674
8842
  const networkAccessEnabled = codexNetworkAccess();
@@ -8706,12 +8874,234 @@ async function* startCodexEvents(config, input, worker2, opts) {
8706
8874
  }
8707
8875
  }
8708
8876
 
8877
+ // src/session/runner/pi.ts
8878
+ import { readFile as readFile2 } from "node:fs/promises";
8879
+ import { homedir as homedir3 } from "node:os";
8880
+ import { join as join10 } from "node:path";
8881
+ var newPiTurnState = (startedAt = Date.now()) => ({
8882
+ startedAt,
8883
+ usage: {},
8884
+ toolArgs: {}
8885
+ });
8886
+ var READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
8887
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8888
+ var str2 = (v) => typeof v === "string" ? v : void 0;
8889
+ var resultText = (result) => isRecord3(result) && Array.isArray(result.content) ? result.content.filter(isRecord3).map((block) => str2(block.text) ?? "").join("") : "";
8890
+ var startedItem = (id, name, args) => {
8891
+ const input = isRecord3(args) ? args : {};
8892
+ if (name === "bash") {
8893
+ return {
8894
+ type: "command_execution",
8895
+ id,
8896
+ status: "in_progress",
8897
+ command: str2(input.command) ?? "",
8898
+ output: ""
8899
+ };
8900
+ }
8901
+ if (name === "write" || name === "edit") {
8902
+ return {
8903
+ type: "file_change",
8904
+ id,
8905
+ status: "in_progress",
8906
+ changes: [{ path: str2(input.path) ?? "", kind: "update" }]
8907
+ };
8908
+ }
8909
+ return { type: "tool_call", id, status: "in_progress", name, input: args ?? null };
8910
+ };
8911
+ var completedItem = (id, name, args, result, isError) => {
8912
+ const status = isError ? "failed" : "completed";
8913
+ const started = startedItem(id, name, args);
8914
+ if (started.type === "command_execution") {
8915
+ const structured = isRecord3(result) && isRecord3(result.structuredContent) ? result.structuredContent : {};
8916
+ return {
8917
+ ...started,
8918
+ status,
8919
+ output: str2(structured.output) ?? resultText(result),
8920
+ exitCode: typeof structured.exit_code === "number" ? structured.exit_code : void 0
8921
+ };
8922
+ }
8923
+ if (started.type === "tool_call") {
8924
+ return { ...started, status, output: resultText(result), isError };
8925
+ }
8926
+ return { ...started, status };
8927
+ };
8928
+ var messageItems = (message) => message.content.flatMap((block, index) => {
8929
+ const id = `${message.responseId ?? message.timestamp}:${index}`;
8930
+ if (block.type === "text" && block.text.trim()) {
8931
+ return [{ type: "agent_message", id, status: "completed", text: block.text }];
8932
+ }
8933
+ if (block.type === "thinking" && block.thinking.trim()) {
8934
+ return [{ type: "reasoning", id, status: "completed", text: block.thinking }];
8935
+ }
8936
+ return [];
8937
+ });
8938
+ var addUsage = (total, usage) => ({
8939
+ inputTokens: (total.inputTokens ?? 0) + usage.input,
8940
+ cachedInputTokens: (total.cachedInputTokens ?? 0) + usage.cacheRead,
8941
+ outputTokens: (total.outputTokens ?? 0) + usage.output,
8942
+ reasoningOutputTokens: (total.reasoningOutputTokens ?? 0) + (usage.reasoning ?? 0),
8943
+ totalCostUsd: (total.totalCostUsd ?? 0) + usage.cost.total,
8944
+ numTurns: (total.numTurns ?? 0) + 1
8945
+ });
8946
+ var piToAgentEvents = (event, state, now = Date.now()) => {
8947
+ if (event.type === "tool_execution_start") {
8948
+ return {
8949
+ events: [
8950
+ { type: "item.started", item: startedItem(event.toolCallId, event.toolName, event.args) }
8951
+ ],
8952
+ state: { ...state, toolArgs: { ...state.toolArgs, [event.toolCallId]: event.args } }
8953
+ };
8954
+ }
8955
+ if (event.type === "tool_execution_end") {
8956
+ const args = state.toolArgs[event.toolCallId];
8957
+ return {
8958
+ events: [
8959
+ {
8960
+ type: "item.completed",
8961
+ item: completedItem(event.toolCallId, event.toolName, args, event.result, event.isError)
8962
+ }
8963
+ ],
8964
+ state
8965
+ };
8966
+ }
8967
+ if (event.type === "message_end" && event.message.role === "assistant") {
8968
+ const message = event.message;
8969
+ return {
8970
+ events: messageItems(message).map((item) => ({ type: "item.completed", item })),
8971
+ // Reset on every assistant message: pi auto-retries, and only the last
8972
+ // response decides whether the turn failed.
8973
+ state: {
8974
+ ...state,
8975
+ usage: addUsage(state.usage, message.usage),
8976
+ error: message.stopReason === "error" ? message.errorMessage ?? "pi turn failed" : void 0
8977
+ }
8978
+ };
8979
+ }
8980
+ if (event.type === "agent_settled") {
8981
+ const usage = { ...state.usage, durationMs: now - state.startedAt };
8982
+ return {
8983
+ events: [
8984
+ state.error ? { type: "turn.failed", error: { message: state.error }, usage } : { type: "turn.completed", usage }
8985
+ ],
8986
+ state
8987
+ };
8988
+ }
8989
+ return { events: [], state };
8990
+ };
8991
+ var resolveModel = (runtime, raw) => {
8992
+ const slash = raw.indexOf("/");
8993
+ const model = slash > 0 ? runtime.getModel(raw.slice(0, slash), raw.slice(slash + 1)) : void 0;
8994
+ if (!model) throw new Error(`unknown pi model "${raw}" (expected provider/model)`);
8995
+ return model;
8996
+ };
8997
+ var promptParts = async (input) => {
8998
+ if (typeof input === "string") return { text: input, images: [] };
8999
+ const text = input.flatMap((entry) => entry.type === "text" ? [entry.text] : []).join("\n");
9000
+ const images = await Promise.all(
9001
+ input.flatMap(
9002
+ (entry) => entry.type === "local_image" ? [
9003
+ readFile2(entry.path).then((data) => ({
9004
+ type: "image",
9005
+ data: data.toString("base64"),
9006
+ mimeType: contentTypeForPath(entry.path)
9007
+ }))
9008
+ ] : []
9009
+ )
9010
+ );
9011
+ return { text, images };
9012
+ };
9013
+ async function* startPiEvents(config, input, opts) {
9014
+ const pi = await import("@earendil-works/pi-coding-agent");
9015
+ const modelRuntime = await pi.ModelRuntime.create();
9016
+ const model = opts.model ? resolveModel(modelRuntime, opts.model) : void 0;
9017
+ const effort = isAgentEffort(opts.effort) ? opts.effort : void 0;
9018
+ const thinkingLevel = effort === "ultra" ? void 0 : effort;
9019
+ const agentDir = pi.getAgentDir();
9020
+ const resourceLoader = new pi.DefaultResourceLoader({
9021
+ cwd: config.cwd,
9022
+ agentDir,
9023
+ // pi discovers .pi/ and .agents/ skills natively; Baton workers deploy theirs
9024
+ // under .claude/skills (project and user level).
9025
+ additionalSkillPaths: [join10(config.cwd, ".claude/skills"), join10(homedir3(), ".claude/skills")]
9026
+ });
9027
+ await resourceLoader.reload();
9028
+ const existing = pi.SessionManager.findById(config.cwd, config.agentSessionId);
9029
+ const sessionManager = existing ? pi.SessionManager.open(existing) : pi.SessionManager.create(config.cwd, void 0, { id: config.agentSessionId });
9030
+ const { session: session2 } = await pi.createAgentSession({
9031
+ cwd: config.cwd,
9032
+ agentDir,
9033
+ modelRuntime,
9034
+ resourceLoader,
9035
+ sessionManager,
9036
+ ...model ? { model } : {},
9037
+ ...thinkingLevel ? { thinkingLevel } : {},
9038
+ ...opts.planMode ? { tools: READ_ONLY_TOOLS } : {}
9039
+ });
9040
+ const modelId = session2.model ? `${session2.model.provider}/${session2.model.id}` : void 0;
9041
+ opts.log(`[pi] ${existing ? "resume" : "new"} session ${session2.sessionId}`);
9042
+ if (modelId) opts.log(`[model] ${modelId}`);
9043
+ if (thinkingLevel) opts.log(`[effort] ${thinkingLevel}`);
9044
+ if (opts.planMode) opts.log("[plan] read-only tools");
9045
+ const queue = [];
9046
+ let wake = () => {
9047
+ };
9048
+ let failure;
9049
+ let finished = false;
9050
+ let settled = false;
9051
+ const unsubscribe = session2.subscribe((event) => {
9052
+ queue.push(event);
9053
+ wake();
9054
+ });
9055
+ const abort = () => {
9056
+ void session2.abort().catch((error) => opts.log(`[pi] abort: ${String(error)}`));
9057
+ };
9058
+ opts.signal?.addEventListener("abort", abort, { once: true });
9059
+ try {
9060
+ yield { type: "thread.started", sessionId: session2.sessionId, model: modelId };
9061
+ yield { type: "turn.started" };
9062
+ const { text, images } = await promptParts(input);
9063
+ if (opts.signal?.aborted) return;
9064
+ const running = session2.prompt(opts.planMode ? `${PLAN_MODE_PREFIX}${text}` : text, images?.length ? { images } : {}).then(
9065
+ () => {
9066
+ finished = true;
9067
+ wake();
9068
+ },
9069
+ (error) => {
9070
+ failure = error;
9071
+ wake();
9072
+ }
9073
+ );
9074
+ let state = newPiTurnState();
9075
+ while (!settled) {
9076
+ const event = queue.shift();
9077
+ if (!event) {
9078
+ if (failure !== void 0) throw failure;
9079
+ if (finished) break;
9080
+ await new Promise((resolve4) => {
9081
+ wake = resolve4;
9082
+ });
9083
+ continue;
9084
+ }
9085
+ const folded = piToAgentEvents(event, state);
9086
+ state = folded.state;
9087
+ settled = event.type === "agent_settled";
9088
+ yield* folded.events;
9089
+ }
9090
+ await running;
9091
+ if (failure !== void 0) throw failure;
9092
+ } finally {
9093
+ opts.signal?.removeEventListener("abort", abort);
9094
+ unsubscribe();
9095
+ session2.dispose();
9096
+ }
9097
+ }
9098
+
8709
9099
  // src/session/runner/query.ts
8710
- import { join as join9 } from "node:path";
9100
+ import { join as join11 } from "node:path";
8711
9101
  var startQuery = (config, text, resuming, queryFn, abortController, log, { envOverlay, planMode = false, model, effort } = {}) => {
8712
9102
  const env = buildSdkEnv(envOverlay);
8713
9103
  const exe = claudeExecutable();
8714
- const addDirs = additionalDirs([join9(config.runtimeDir, "attachments")]);
9104
+ const addDirs = additionalDirs([join11(config.runtimeDir, "attachments")]);
8715
9105
  const level = claudeEffort(effort);
8716
9106
  if (addDirs) log(`[add-dir] ${addDirs.join(", ")}`);
8717
9107
  if (planMode) log("[plan] read-only planning turn");
@@ -8759,34 +9149,34 @@ var streamAgentEvents = async (events, worker2) => {
8759
9149
  return result;
8760
9150
  };
8761
9151
  var streamClaudeSdkEvents = (messages, worker2) => streamAgentEvents(claudeToAgentEvents(messages), worker2);
8762
- var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8763
- var str2 = (v) => typeof v === "string" ? v : void 0;
9152
+ var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
9153
+ var str3 = (v) => typeof v === "string" ? v : void 0;
8764
9154
  var messageContent = (payload) => {
8765
9155
  const msg = payload.message;
8766
- if (!isRecord3(msg) || !Array.isArray(msg.content)) return [];
8767
- return msg.content.filter(isRecord3);
9156
+ if (!isRecord4(msg) || !Array.isArray(msg.content)) return [];
9157
+ return msg.content.filter(isRecord4);
8768
9158
  };
8769
9159
  var textFromToolResult = (raw) => {
8770
9160
  const c = raw.content;
8771
9161
  if (typeof c === "string") return c;
8772
9162
  if (Array.isArray(c)) {
8773
- return c.map((b) => isRecord3(b) && typeof b.text === "string" ? b.text : JSON.stringify(b)).join("\n");
9163
+ return c.map((b) => isRecord4(b) && typeof b.text === "string" ? b.text : JSON.stringify(b)).join("\n");
8774
9164
  }
8775
9165
  return JSON.stringify(c ?? "");
8776
9166
  };
8777
9167
  var commandOf = (input) => {
8778
- if (!isRecord3(input)) return "";
8779
- return str2(input.command) ?? str2(input.cmd) ?? "";
9168
+ if (!isRecord4(input)) return "";
9169
+ return str3(input.command) ?? str3(input.cmd) ?? "";
8780
9170
  };
8781
9171
  var pathOf = (input) => {
8782
- if (!isRecord3(input)) return void 0;
8783
- return str2(input.file_path) ?? str2(input.path);
9172
+ if (!isRecord4(input)) return void 0;
9173
+ return str3(input.file_path) ?? str3(input.path);
8784
9174
  };
8785
9175
  var fileKindOf = (name) => name === "Delete" ? "delete" : "update";
8786
9176
  var todoItems2 = (input) => {
8787
- if (!isRecord3(input) || !Array.isArray(input.todos)) return [];
8788
- return input.todos.filter(isRecord3).map((todo) => ({
8789
- text: str2(todo.content) ?? str2(todo.text) ?? "",
9177
+ if (!isRecord4(input) || !Array.isArray(input.todos)) return [];
9178
+ return input.todos.filter(isRecord4).map((todo) => ({
9179
+ text: str3(todo.content) ?? str3(todo.text) ?? "",
8790
9180
  completed: todo.status === "completed" || todo.completed === true
8791
9181
  }));
8792
9182
  };
@@ -8795,8 +9185,8 @@ var mcpParts = (name) => {
8795
9185
  return match ? { server: match[1] ?? "", tool: match[2] ?? "" } : null;
8796
9186
  };
8797
9187
  var startedItemFromToolUse = (block) => {
8798
- const id = str2(block.id) ?? `tool-${Math.random().toString(36).slice(2)}`;
8799
- const name = str2(block.name) ?? "tool";
9188
+ const id = str3(block.id) ?? `tool-${Math.random().toString(36).slice(2)}`;
9189
+ const name = str3(block.name) ?? "tool";
8800
9190
  const input = block.input ?? null;
8801
9191
  if (name === "Bash") {
8802
9192
  return {
@@ -8832,7 +9222,7 @@ var startedItemFromToolUse = (block) => {
8832
9222
  type: "web_search",
8833
9223
  id,
8834
9224
  status: "in_progress",
8835
- query: isRecord3(input) ? str2(input.query) ?? "" : ""
9225
+ query: isRecord4(input) ? str3(input.query) ?? "" : ""
8836
9226
  };
8837
9227
  }
8838
9228
  if (name === "TodoWrite") {
@@ -8851,9 +9241,9 @@ var completeItem = (item, output, isError) => {
8851
9241
  };
8852
9242
  var resultEvent = (message) => {
8853
9243
  const rec = message;
8854
- const subtype = str2(rec.subtype) ?? "unknown";
9244
+ const subtype = str3(rec.subtype) ?? "unknown";
8855
9245
  const isError = rec.is_error === true || subtype !== "success";
8856
- const resultText = str2(rec.result) ?? "";
9246
+ const resultText2 = str3(rec.result) ?? "";
8857
9247
  const usage = {
8858
9248
  numTurns: typeof rec.num_turns === "number" ? rec.num_turns : void 0,
8859
9249
  totalCostUsd: typeof rec.total_cost_usd === "number" ? rec.total_cost_usd : void 0,
@@ -8861,7 +9251,7 @@ var resultEvent = (message) => {
8861
9251
  };
8862
9252
  return isError ? {
8863
9253
  type: "turn.failed",
8864
- error: { message: resultText || `claude result: ${subtype}`, subtype },
9254
+ error: { message: resultText2 || `claude result: ${subtype}`, subtype },
8865
9255
  usage,
8866
9256
  raw: message
8867
9257
  } : { type: "turn.completed", subtype, usage, raw: message };
@@ -8878,8 +9268,8 @@ async function* claudeToAgentEvents(messages) {
8878
9268
  systemEmitted = true;
8879
9269
  yield {
8880
9270
  type: "thread.started",
8881
- sessionId: str2(p.session_id) ?? "",
8882
- model: str2(p.model) ?? (isRecord3(p.model_info) ? str2(p.model_info.id) : void 0),
9271
+ sessionId: str3(p.session_id) ?? "",
9272
+ model: str3(p.model) ?? (isRecord4(p.model_info) ? str3(p.model_info.id) : void 0),
8883
9273
  raw: message
8884
9274
  };
8885
9275
  yield { type: "turn.started", raw: message };
@@ -8918,7 +9308,7 @@ async function* claudeToAgentEvents(messages) {
8918
9308
  pendingTools.set(item.id, item);
8919
9309
  yield { type: "item.started", item, raw: block };
8920
9310
  } else if (block.type === "tool_result") {
8921
- const id = str2(block.tool_use_id) ?? "";
9311
+ const id = str3(block.tool_use_id) ?? "";
8922
9312
  const item = pendingTools.get(id);
8923
9313
  if (item) {
8924
9314
  const completed = completeItem(item, textFromToolResult(block), block.is_error === true);
@@ -8936,7 +9326,7 @@ async function* claudeToAgentEvents(messages) {
8936
9326
  if (message.type === "user") {
8937
9327
  for (const block of messageContent(p)) {
8938
9328
  if (block.type !== "tool_result") continue;
8939
- const id = str2(block.tool_use_id) ?? "";
9329
+ const id = str3(block.tool_use_id) ?? "";
8940
9330
  const item = pendingTools.get(id);
8941
9331
  if (!item) {
8942
9332
  yield { type: "raw", raw: block };
@@ -8962,19 +9352,19 @@ var UnsettledExecution = class extends Error {
8962
9352
  var materializeImages = async (config, execution) => {
8963
9353
  const images = execution.message.payload.images ?? [];
8964
9354
  if (!images.length) return [];
8965
- const directory2 = join10(
9355
+ const directory2 = join12(
8966
9356
  config.runtimeDir,
8967
9357
  "attachments",
8968
9358
  `message-${execution.message.id}`,
8969
9359
  "pasted"
8970
9360
  );
8971
- await mkdir4(directory2, { recursive: true });
9361
+ await mkdir5(directory2, { recursive: true });
8972
9362
  return Promise.all(
8973
9363
  images.map(async (image, index) => {
8974
9364
  const match = /^data:image\/(png|jpeg|webp|gif);base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image);
8975
9365
  if (!match) throw new Error("unsupported pasted image");
8976
- const path = join10(directory2, `pasted-${index + 1}.${match[1]}`);
8977
- await writeFile2(path, Buffer.from(match[2] ?? "", "base64"));
9366
+ const path = join12(directory2, `pasted-${index + 1}.${match[1]}`);
9367
+ await writeFile3(path, Buffer.from(match[2] ?? "", "base64"));
8978
9368
  return path;
8979
9369
  })
8980
9370
  );
@@ -8982,7 +9372,7 @@ var materializeImages = async (config, execution) => {
8982
9372
  var buildPrompt = async (config, execution, fetchImpl) => {
8983
9373
  const { text: rawText, attachments = [] } = execution.message.payload;
8984
9374
  const paths = attachments.length ? await materializeAttachments({
8985
- directory: join10(
9375
+ directory: join12(
8986
9376
  config.runtimeDir,
8987
9377
  "attachments",
8988
9378
  `message-${execution.message.id}`,
@@ -9065,25 +9455,27 @@ var runTurn = async (config, worker2, execution, resuming, queryFn, log, envOver
9065
9455
  const built = await buildPrompt(config, execution, fetchImpl);
9066
9456
  if (abort.signal.aborted) throw new Error("interrupted before SDK start");
9067
9457
  const { planMode, model, effort } = execution.message.payload;
9068
- return config.agentKind === "codex" ? streamAgentEvents(
9069
- startCodexEvents(config, built.input, worker2, {
9070
- envOverlay,
9071
- planMode,
9072
- model,
9073
- effort,
9074
- signal: abort.signal,
9075
- log
9076
- }),
9077
- output
9078
- ) : streamClaudeSdkEvents(
9079
- startQuery(config, built.text, resuming, queryFn, abort, log, {
9080
- envOverlay,
9081
- planMode,
9082
- model,
9083
- effort
9084
- }),
9085
- output
9086
- );
9458
+ const turn = { planMode, model, effort };
9459
+ const run4 = {
9460
+ "claude-code": () => streamClaudeSdkEvents(
9461
+ startQuery(config, built.text, resuming, queryFn, abort, log, { envOverlay, ...turn }),
9462
+ output
9463
+ ),
9464
+ codex: () => streamAgentEvents(
9465
+ startCodexEvents(config, built.input, worker2, {
9466
+ envOverlay,
9467
+ ...turn,
9468
+ signal: abort.signal,
9469
+ log
9470
+ }),
9471
+ output
9472
+ ),
9473
+ pi: () => streamAgentEvents(
9474
+ startPiEvents(config, built.input, { ...turn, signal: abort.signal, log }),
9475
+ output
9476
+ )
9477
+ };
9478
+ return run4[config.agentKind]();
9087
9479
  };
9088
9480
  const consuming = consume().then(
9089
9481
  (value) => ({ kind: "result", value }),
@@ -9107,7 +9499,7 @@ var runTurn = async (config, worker2, execution, resuming, queryFn, log, envOver
9107
9499
  };
9108
9500
 
9109
9501
  // src/session/runner.ts
9110
- var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "codex" ? !config.agentSessionId.startsWith("pending:") : transcriptPath(config.agentSessionId) !== null;
9502
+ var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "claude-code" ? transcriptPath(config.agentSessionId) !== null : !config.agentSessionId.startsWith("pending:");
9111
9503
  var shouldReap = (lastActivity, now, busy, queueLen, idleMs) => !busy && queueLen === 0 && now - lastActivity >= idleMs;
9112
9504
  var runDaemon = async (config, deps, signal) => {
9113
9505
  const log = deps.log ?? console.log;
@@ -9126,7 +9518,7 @@ var runDaemon = async (config, deps, signal) => {
9126
9518
  });
9127
9519
  const drain = async () => {
9128
9520
  if (draining || closed || stranded) return;
9129
- const run2 = async () => {
9521
+ const run4 = async () => {
9130
9522
  while (!closed && !stranded && !signal.aborted) {
9131
9523
  let claimed;
9132
9524
  try {
@@ -9169,7 +9561,7 @@ var runDaemon = async (config, deps, signal) => {
9169
9561
  }
9170
9562
  }
9171
9563
  };
9172
- draining = run2();
9564
+ draining = run4();
9173
9565
  try {
9174
9566
  await draining;
9175
9567
  } finally {
@@ -9236,16 +9628,16 @@ var runDaemon = async (config, deps, signal) => {
9236
9628
 
9237
9629
  // src/worker/runtime-paths.ts
9238
9630
  import { createHash } from "node:crypto";
9239
- import { homedir as homedir3 } from "node:os";
9240
- import { join as join11 } from "node:path";
9241
- var workerRuntimeDir = (owner, env = process.env) => join11(
9242
- env.XDG_DATA_HOME ?? join11(env.HOME ?? homedir3(), ".local/share"),
9631
+ import { homedir as homedir4 } from "node:os";
9632
+ import { join as join13 } from "node:path";
9633
+ var workerRuntimeDir = (owner, env = process.env) => join13(
9634
+ env.XDG_DATA_HOME ?? join13(env.HOME ?? homedir4(), ".local/share"),
9243
9635
  "baton",
9244
9636
  "runtime",
9245
9637
  createHash("sha256").update(owner.server).digest("hex"),
9246
9638
  `worker-${owner.workerId}`
9247
9639
  );
9248
- var sessionRuntimeDir = (owner, sessionId, env = process.env) => join11(workerRuntimeDir(owner, env), "sessions", String(sessionId));
9640
+ var sessionRuntimeDir = (owner, sessionId, env = process.env) => join13(workerRuntimeDir(owner, env), "sessions", String(sessionId));
9249
9641
 
9250
9642
  // src/commands/session/run.ts
9251
9643
  var bearerFetch = (token) => (input, init2) => fetch(input, {
@@ -9299,6 +9691,7 @@ var sessionRunCommand = defineCommand({
9299
9691
  const sessionId = Number(args.session);
9300
9692
  if (!Number.isInteger(sessionId) || sessionId <= 0)
9301
9693
  throw new Error(`invalid session id: ${args.session}`);
9694
+ process.env.BATON_SESSION_ID = String(sessionId);
9302
9695
  const client = createClient(server, { bearer: workerToken });
9303
9696
  const s = await client.sessions.get(sessionId);
9304
9697
  if (!s.agentSessionId || !s.cwd)
@@ -9606,7 +9999,7 @@ import { hostname as osHostname } from "node:os";
9606
9999
  // src/worker/bundled-skills.ts
9607
10000
  import { spawnSync as spawnSync2 } from "node:child_process";
9608
10001
  import { cpSync, existsSync as existsSync5 } from "node:fs";
9609
- import { dirname as dirname3, join as join12 } from "node:path";
10002
+ import { dirname as dirname3, join as join14 } from "node:path";
9610
10003
  import { fileURLToPath } from "node:url";
9611
10004
 
9612
10005
  // src/worker/git-exclude.ts
@@ -9638,8 +10031,8 @@ var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
9638
10031
  var bundledSkillPath = () => {
9639
10032
  const here = dirname3(fileURLToPath(import.meta.url));
9640
10033
  const candidates = [
9641
- join12(here, "..", "skills", SKILL_NAME),
9642
- join12(here, "..", "..", "skills", SKILL_NAME)
10034
+ join14(here, "..", "skills", SKILL_NAME),
10035
+ join14(here, "..", "..", "skills", SKILL_NAME)
9643
10036
  ];
9644
10037
  const found = candidates.find(existsSync5);
9645
10038
  if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
@@ -9656,8 +10049,427 @@ var syncBundledArtifactSkill = (cwd) => {
9656
10049
  const source = bundledSkillPath();
9657
10050
  TARGETS.filter((target) => !isTracked(cwd, target)).forEach((target) => {
9658
10051
  ensureExcluded(cwd, `${target}/`);
9659
- cpSync(source, join12(cwd, target), { recursive: true, force: true });
10052
+ cpSync(source, join14(cwd, target), { recursive: true, force: true });
10053
+ });
10054
+ };
10055
+
10056
+ // src/worker/files/branches.ts
10057
+ import { realpath as realpath2 } from "node:fs/promises";
10058
+
10059
+ // src/worker/files/git.ts
10060
+ import { execFile as execFile4 } from "node:child_process";
10061
+ import { promisify as promisify4 } from "node:util";
10062
+ var run = promisify4(execFile4);
10063
+ var GIT_TIMEOUT_MS = 3e3;
10064
+ var runGit = async (cwd, args, maxBuffer = 4 * 1024 * 1024) => {
10065
+ const { stdout } = await run("git", ["--literal-pathspecs", "-C", cwd, ...args], {
10066
+ encoding: "buffer",
10067
+ timeout: GIT_TIMEOUT_MS,
10068
+ maxBuffer,
10069
+ env: { ...process.env, LC_ALL: "C", GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
9660
10070
  });
10071
+ return stdout;
10072
+ };
10073
+
10074
+ // src/worker/files/branches.ts
10075
+ var NOT_A_WORK_TREE_EXIT = 128;
10076
+ var git = async (cwd, args) => (await runGit(cwd, args)).toString("utf8");
10077
+ var workTreeTop = async (dir) => {
10078
+ try {
10079
+ return await realpath2((await git(dir, ["rev-parse", "--show-toplevel"])).trim());
10080
+ } catch (error) {
10081
+ if (error.code !== NOT_A_WORK_TREE_EXIT)
10082
+ console.warn("[files] git rev-parse failed; no branches to browse", error);
10083
+ return null;
10084
+ }
10085
+ };
10086
+ var parseWorktrees = (stdout) => stdout.split("\0\0").map((record) => record.split("\0").filter(Boolean)).flatMap((fields) => {
10087
+ const path = fields.find((field) => field.startsWith("worktree "))?.slice("worktree ".length);
10088
+ const ref = fields.find((field) => field.startsWith("branch "))?.slice("branch ".length);
10089
+ const skipped = fields.some((field) => field === "bare" || field.startsWith("prunable"));
10090
+ return path && ref && !skipped ? [{ path, name: ref.replace(/^refs\/heads\//, "") }] : [];
10091
+ });
10092
+ var parseTips = (stdout) => new Map(
10093
+ stdout.split("\n").map((line) => line.split("\0")).flatMap(
10094
+ ([ref, commit]) => ref && commit ? [[ref.replace(/^refs\/heads\//, ""), commit]] : []
10095
+ )
10096
+ );
10097
+ var existingPath = async (path) => {
10098
+ try {
10099
+ return await realpath2(path);
10100
+ } catch (error) {
10101
+ if (error.code !== "ENOENT")
10102
+ console.warn(`[files] cannot resolve worktree ${path}`, error);
10103
+ return null;
10104
+ }
10105
+ };
10106
+ var rank = (branch) => branch.current ? 0 : branch.worktree ? 1 : 2;
10107
+ var discoverBranches = async (baseDir) => {
10108
+ const base = await realpath2(baseDir);
10109
+ if (await workTreeTop(base) !== base) return [];
10110
+ let worktrees;
10111
+ let tips;
10112
+ try {
10113
+ const [listed, refs] = await Promise.all([
10114
+ git(base, ["worktree", "list", "--porcelain", "-z"]),
10115
+ git(base, ["for-each-ref", "--format=%(refname)%00%(objectname)", "refs/heads"])
10116
+ ]);
10117
+ worktrees = parseWorktrees(listed);
10118
+ tips = parseTips(refs);
10119
+ } catch (error) {
10120
+ console.warn("[files] listing git branches failed; no branches to browse", error);
10121
+ return [];
10122
+ }
10123
+ const checkedOut = new Map(
10124
+ (await Promise.all(
10125
+ worktrees.map(async ({ path, name }) => [name, await existingPath(path)])
10126
+ )).filter((entry) => entry[1] !== null)
10127
+ );
10128
+ return [.../* @__PURE__ */ new Set([...checkedOut.keys(), ...tips.keys()])].map((name) => {
10129
+ const worktree = checkedOut.get(name) ?? null;
10130
+ return { name, worktree, commit: tips.get(name) ?? null, current: worktree === base };
10131
+ }).sort((a, b) => rank(a) - rank(b) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
10132
+ };
10133
+
10134
+ // src/worker/files/common.ts
10135
+ var FileError = class extends Error {
10136
+ constructor(code, message) {
10137
+ super(message);
10138
+ this.code = code;
10139
+ }
10140
+ code;
10141
+ };
10142
+ var notFound = (path) => new FileError("file_not_found", `not found: ${path}`);
10143
+ var checkedPath = (path, allowRoot) => {
10144
+ const rel = normalizeFilePath(path, allowRoot);
10145
+ if (rel === null) throw new FileError("invalid_request", `invalid path: ${path}`);
10146
+ if (isSensitiveFilePath(rel)) throw new FileError("forbidden_path", `protected file: ${rel}`);
10147
+ return rel;
10148
+ };
10149
+ var searchNeedle = (query3) => {
10150
+ const trimmed = query3.trim();
10151
+ if (trimmed === "" || trimmed.length > FILE_SEARCH_QUERY_MAX_LENGTH)
10152
+ throw new FileError(
10153
+ "invalid_request",
10154
+ `search query must be 1-${FILE_SEARCH_QUERY_MAX_LENGTH} characters`
10155
+ );
10156
+ return trimmed.toLowerCase();
10157
+ };
10158
+ var byKindThenName = (a, b) => {
10159
+ if (a.kind !== b.kind) return a.kind === "directory" ? -1 : 1;
10160
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
10161
+ };
10162
+ var decodeText = (bytes) => {
10163
+ if (bytes.includes(0)) return null;
10164
+ try {
10165
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10166
+ } catch (error) {
10167
+ if (error instanceof TypeError) return null;
10168
+ throw error;
10169
+ }
10170
+ };
10171
+ var hasBytes = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
10172
+ var hasText = (bytes, text, offset = 0) => bytes.subarray(offset, offset + text.length).toString("latin1") === text;
10173
+ var SVG_START = /^\s*(?:<\?xml[^>]*>\s*)?(?:<!--[\s\S]*?-->\s*)*(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>]/i;
10174
+ var RAW_SIGNATURES = {
10175
+ "image/png": (bytes) => hasBytes(bytes, [137, 80, 78, 71, 13, 10, 26, 10]),
10176
+ "image/jpeg": (bytes) => hasBytes(bytes, [255, 216, 255]),
10177
+ "image/gif": (bytes) => hasText(bytes, "GIF8"),
10178
+ "image/webp": (bytes) => hasText(bytes, "RIFF") && hasText(bytes, "WEBP", 8),
10179
+ "image/avif": (bytes) => hasText(bytes, "ftyp", 4) && bytes.subarray(8, 32).toString("latin1").includes("avif"),
10180
+ "image/bmp": (bytes) => hasText(bytes, "BM"),
10181
+ "image/x-icon": (bytes) => hasBytes(bytes, [0, 0, 1, 0]),
10182
+ "image/svg+xml": (bytes) => {
10183
+ const text = decodeText(bytes);
10184
+ return text !== null && SVG_START.test(text.slice(0, 4096));
10185
+ },
10186
+ "text/css": (bytes) => decodeText(bytes) !== null,
10187
+ "font/woff": (bytes) => hasText(bytes, "wOFF"),
10188
+ "font/woff2": (bytes) => hasText(bytes, "wOF2"),
10189
+ "font/ttf": (bytes) => hasBytes(bytes, [0, 1, 0, 0]) || hasText(bytes, "true"),
10190
+ "font/otf": (bytes) => hasText(bytes, "OTTO")
10191
+ };
10192
+ var rawPayload = (path, mediaType, bytes) => {
10193
+ if (!RAW_SIGNATURES[mediaType]?.(bytes))
10194
+ throw new FileError("unsupported", `not a valid ${mediaType} file: ${path}`);
10195
+ return { mediaType, base64: bytes.toString("base64") };
10196
+ };
10197
+
10198
+ // src/worker/files/commit.ts
10199
+ var MISSING_OBJECT_EXIT = 128;
10200
+ var SEARCH_MAX_OUTPUT = 64 * 1024 * 1024;
10201
+ var REGULAR_FILE_MODES = /* @__PURE__ */ new Set(["100644", "100755"]);
10202
+ var parseTree = (stdout) => stdout.toString("utf8").split("\0").flatMap((record) => {
10203
+ const tab = record.indexOf(" ");
10204
+ if (tab < 0) return [];
10205
+ const [mode = "", type = "", oid = "", size = ""] = record.slice(0, tab).split(/ +/);
10206
+ const bytes = /^\d+$/.test(size) ? Number(size) : null;
10207
+ return [{ mode, type, oid, size: bytes, path: record.slice(tab + 1) }];
10208
+ });
10209
+ var isRegularFile = (entry) => entry.type === "blob" && REGULAR_FILE_MODES.has(entry.mode);
10210
+ var lsTree = async (repo, args) => {
10211
+ try {
10212
+ return parseTree(await runGit(repo, ["ls-tree", "-z", ...args]));
10213
+ } catch (error) {
10214
+ if (error.code === MISSING_OBJECT_EXIT) return null;
10215
+ throw error;
10216
+ }
10217
+ };
10218
+ var listCommitDirectory = async (repo, commit, path) => {
10219
+ const rel = checkedPath(path, true);
10220
+ const children = await lsTree(repo, [`${commit}:${rel}`]);
10221
+ if (children === null) throw notFound(rel || ".");
10222
+ const entries = children.flatMap((entry) => {
10223
+ const child = rel ? `${rel}/${entry.path}` : entry.path;
10224
+ if (isSensitiveFilePath(child)) return [];
10225
+ if (entry.type === "tree")
10226
+ return isExcludedDirectoryName(entry.path) ? [] : [{ name: entry.path, path: child, kind: "directory" }];
10227
+ return isRegularFile(entry) ? [{ name: entry.path, path: child, kind: "file" }] : [];
10228
+ }).sort(byKindThenName);
10229
+ return {
10230
+ path: rel,
10231
+ entries: entries.slice(0, FILE_DIRECTORY_MAX_ENTRIES),
10232
+ truncated: entries.length > FILE_DIRECTORY_MAX_ENTRIES
10233
+ };
10234
+ };
10235
+ var searchCommit = async (repo, commit, query3) => {
10236
+ const needle = searchNeedle(query3);
10237
+ let stdout;
10238
+ try {
10239
+ stdout = await runGit(repo, ["ls-tree", "-r", "-z", commit], SEARCH_MAX_OUTPUT);
10240
+ } catch (error) {
10241
+ if (error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
10242
+ throw new FileError("unavailable", "the branch has too many files to search");
10243
+ throw error;
10244
+ }
10245
+ const matches = parseTree(stdout).filter(
10246
+ (entry) => isRegularFile(entry) && !entry.path.split("/").slice(0, -1).some(isExcludedDirectoryName) && !isSensitiveFilePath(entry.path) && entry.path.toLowerCase().includes(needle)
10247
+ );
10248
+ return {
10249
+ entries: matches.slice(0, FILE_SEARCH_MAX_RESULTS).map((entry) => ({
10250
+ name: entry.path.split("/").at(-1) ?? entry.path,
10251
+ path: entry.path,
10252
+ kind: "file"
10253
+ })),
10254
+ truncated: matches.length > FILE_SEARCH_MAX_RESULTS
10255
+ };
10256
+ };
10257
+ var regularEntry = async (repo, commit, rel) => {
10258
+ const entry = (await lsTree(repo, ["-l", commit, "--", rel]))?.find((item) => item.path === rel);
10259
+ if (!entry || !isRegularFile(entry) || entry.size === null) throw notFound(rel);
10260
+ return { oid: entry.oid, size: entry.size };
10261
+ };
10262
+ var viewCommitFile = async (repo, commit, path) => {
10263
+ const rel = checkedPath(path, false);
10264
+ const entry = await regularEntry(repo, commit, rel);
10265
+ const meta = { path: rel, name: rel.split("/").at(-1) ?? rel, commit };
10266
+ const mediaType = imageMediaType(rel);
10267
+ if (mediaType)
10268
+ return entry.size > FILE_RAW_MAX_BYTES ? { ...meta, size: entry.size, kind: "too_large", limit: FILE_RAW_MAX_BYTES } : { ...meta, size: entry.size, kind: "image", mediaType };
10269
+ if (entry.size > FILE_TEXT_MAX_BYTES)
10270
+ return { ...meta, size: entry.size, kind: "too_large", limit: FILE_TEXT_MAX_BYTES };
10271
+ const bytes = await runGit(repo, ["cat-file", "blob", entry.oid], FILE_TEXT_MAX_BYTES + 1);
10272
+ const content = decodeText(bytes);
10273
+ return content === null ? { ...meta, size: bytes.length, kind: "binary" } : { ...meta, size: bytes.length, kind: "text", content };
10274
+ };
10275
+ var readRawCommitFile = async (repo, commit, path) => {
10276
+ const rel = checkedPath(path, false);
10277
+ const mediaType = rawMediaType(rel);
10278
+ if (!mediaType) throw new FileError("unsupported", `not served raw: ${rel}`);
10279
+ const entry = await regularEntry(repo, commit, rel);
10280
+ if (entry.size > FILE_RAW_MAX_BYTES)
10281
+ throw new FileError("unsupported", `too large to serve raw: ${rel}`);
10282
+ const bytes = await runGit(repo, ["cat-file", "blob", entry.oid], FILE_RAW_MAX_BYTES + 1);
10283
+ return rawPayload(rel, mediaType, bytes);
10284
+ };
10285
+
10286
+ // src/worker/files/disk.ts
10287
+ import { constants as constants2 } from "node:fs";
10288
+ import { open as open2, readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
10289
+ import { isAbsolute as isAbsolute4, join as join15, relative, sep } from "node:path";
10290
+ var isMissing = (error) => ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code ?? "");
10291
+ var childPath = (parent, name) => parent ? `${parent}/${name}` : name;
10292
+ var inside = (root, real) => {
10293
+ const rel = relative(root, real);
10294
+ if (isAbsolute4(rel) || rel === ".." || rel.startsWith(`..${sep}`)) return null;
10295
+ return rel.split(sep).join("/");
10296
+ };
10297
+ var resolveInside = async (root, path, allowRoot) => {
10298
+ const rel = checkedPath(path, allowRoot);
10299
+ let real;
10300
+ try {
10301
+ real = await realpath3(join15(root, rel));
10302
+ } catch (error) {
10303
+ if (isMissing(error)) throw notFound(rel);
10304
+ throw error;
10305
+ }
10306
+ const target = inside(root, real);
10307
+ if (target === null || isSensitiveFilePath(target)) throw notFound(rel);
10308
+ return { path: rel, real };
10309
+ };
10310
+ var symlinkEntry = async (root, linkPath, name, path) => {
10311
+ let real;
10312
+ try {
10313
+ real = await realpath3(linkPath);
10314
+ } catch (error) {
10315
+ if (!isMissing(error)) console.warn(`[files] cannot resolve symlink ${linkPath}`, error);
10316
+ return null;
10317
+ }
10318
+ const target = inside(root, real);
10319
+ if (target === null || isSensitiveFilePath(target)) return null;
10320
+ const info = await stat3(real);
10321
+ if (info.isDirectory())
10322
+ return isExcludedDirectoryName(name) ? null : { name, path, kind: "directory" };
10323
+ return info.isFile() ? { name, path, kind: "file" } : null;
10324
+ };
10325
+ var toEntry = async (root, dir, dirent) => {
10326
+ const path = childPath(dir.path, dirent.name);
10327
+ if (isSensitiveFilePath(path)) return null;
10328
+ if (dirent.isDirectory())
10329
+ return isExcludedDirectoryName(dirent.name) ? null : { name: dirent.name, path, kind: "directory" };
10330
+ if (dirent.isFile()) return { name: dirent.name, path, kind: "file" };
10331
+ return dirent.isSymbolicLink() ? symlinkEntry(root, join15(dir.real, dirent.name), dirent.name, path) : null;
10332
+ };
10333
+ var listDirectory = async (root, path) => {
10334
+ const dir = await resolveInside(root, path, true);
10335
+ if (!(await stat3(dir.real)).isDirectory()) throw notFound(dir.path);
10336
+ const dirents = await readdir(dir.real, { withFileTypes: true });
10337
+ const entries = (await Promise.all(dirents.map((dirent) => toEntry(root, dir, dirent)))).filter((entry) => entry !== null).sort(byKindThenName);
10338
+ return {
10339
+ path: dir.path,
10340
+ entries: entries.slice(0, FILE_DIRECTORY_MAX_ENTRIES),
10341
+ truncated: entries.length > FILE_DIRECTORY_MAX_ENTRIES
10342
+ };
10343
+ };
10344
+ var searchFiles = async (root, query3, now = Date.now) => {
10345
+ const needle = searchNeedle(query3);
10346
+ const deadline = now() + FILE_SEARCH_DEADLINE_MS;
10347
+ const entries = [];
10348
+ const pending = [""];
10349
+ for (let index = 0; index < pending.length; index += 1) {
10350
+ if (now() > deadline) return { entries, truncated: true };
10351
+ const dir = pending[index] ?? "";
10352
+ let dirents;
10353
+ try {
10354
+ dirents = await readdir(join15(root, dir), { withFileTypes: true });
10355
+ } catch (error) {
10356
+ console.warn(`[files] search skipped unreadable directory ${dir || "."}`, error);
10357
+ continue;
10358
+ }
10359
+ for (const dirent of dirents) {
10360
+ const path = childPath(dir, dirent.name);
10361
+ if (isSensitiveFilePath(path)) continue;
10362
+ if (dirent.isDirectory()) {
10363
+ if (!isExcludedDirectoryName(dirent.name)) pending.push(path);
10364
+ } else if (dirent.isFile() && path.toLowerCase().includes(needle)) {
10365
+ if (entries.length === FILE_SEARCH_MAX_RESULTS) return { entries, truncated: true };
10366
+ entries.push({ name: dirent.name, path, kind: "file" });
10367
+ }
10368
+ }
10369
+ }
10370
+ return { entries, truncated: false };
10371
+ };
10372
+ var readWithin = async (file, limit) => {
10373
+ const info = await stat3(file.real);
10374
+ if (!info.isFile()) throw notFound(file.path);
10375
+ if (info.size > limit) return { oversize: info.size };
10376
+ const handle = await open2(file.real, constants2.O_RDONLY | constants2.O_NONBLOCK);
10377
+ try {
10378
+ const opened = await handle.stat();
10379
+ if (!opened.isFile()) throw notFound(file.path);
10380
+ if (opened.size > limit) return { oversize: opened.size };
10381
+ const bytes = await handle.readFile();
10382
+ return bytes.length > limit ? { oversize: bytes.length } : { bytes };
10383
+ } finally {
10384
+ await handle.close();
10385
+ }
10386
+ };
10387
+ var viewFile = async (root, path) => {
10388
+ const file = await resolveInside(root, path, false);
10389
+ const meta = { path: file.path, name: file.path.split("/").at(-1) ?? file.path, commit: null };
10390
+ const mediaType = imageMediaType(file.path);
10391
+ if (mediaType) {
10392
+ const info = await stat3(file.real);
10393
+ if (!info.isFile()) throw notFound(file.path);
10394
+ return info.size > FILE_RAW_MAX_BYTES ? { ...meta, size: info.size, kind: "too_large", limit: FILE_RAW_MAX_BYTES } : { ...meta, size: info.size, kind: "image", mediaType };
10395
+ }
10396
+ const read = await readWithin(file, FILE_TEXT_MAX_BYTES);
10397
+ if ("oversize" in read)
10398
+ return { ...meta, size: read.oversize, kind: "too_large", limit: FILE_TEXT_MAX_BYTES };
10399
+ const content = decodeText(read.bytes);
10400
+ return content === null ? { ...meta, size: read.bytes.length, kind: "binary" } : { ...meta, size: read.bytes.length, kind: "text", content };
10401
+ };
10402
+ var readRawFile = async (root, path) => {
10403
+ const file = await resolveInside(root, path, false);
10404
+ const mediaType = rawMediaType(file.path);
10405
+ if (!mediaType) throw new FileError("unsupported", `not served raw: ${file.path}`);
10406
+ const read = await readWithin(file, FILE_RAW_MAX_BYTES);
10407
+ if ("oversize" in read) throw new FileError("unsupported", `too large to serve raw: ${file.path}`);
10408
+ return rawPayload(file.path, mediaType, read.bytes);
10409
+ };
10410
+
10411
+ // src/worker/files/handler.ts
10412
+ var DEFAULT_CONCURRENCY = 4;
10413
+ var readerFor = (baseDir, branch) => {
10414
+ const { worktree, commit } = branch;
10415
+ if (worktree !== null)
10416
+ return {
10417
+ list: (path) => listDirectory(worktree, path),
10418
+ search: (query3) => searchFiles(worktree, query3),
10419
+ view: (path) => viewFile(worktree, path),
10420
+ raw: (path) => readRawFile(worktree, path)
10421
+ };
10422
+ if (commit === null) throw new FileError("branch_not_found", `branch ${branch.name} is empty`);
10423
+ return {
10424
+ list: (path) => listCommitDirectory(baseDir, commit, path),
10425
+ search: (query3) => searchCommit(baseDir, commit, query3),
10426
+ view: (path) => viewCommitFile(baseDir, commit, path),
10427
+ raw: (path) => readRawCommitFile(baseDir, commit, path)
10428
+ };
10429
+ };
10430
+ var run2 = async (baseDir, query3) => {
10431
+ const branches = await discoverBranches(baseDir);
10432
+ if (query3.op === "branches") return { ok: true, data: branches };
10433
+ const branch = branches.find((candidate) => candidate.name === query3.branch);
10434
+ if (!branch) throw new FileError("branch_not_found", `branch ${query3.branch} does not exist`);
10435
+ const reader = readerFor(baseDir, branch);
10436
+ if (query3.op === "list") return { ok: true, data: await reader.list(query3.path) };
10437
+ if (query3.op === "search") return { ok: true, data: await reader.search(query3.query) };
10438
+ if (query3.op === "raw") return { ok: true, data: await reader.raw(query3.path) };
10439
+ return { ok: true, data: await reader.view(query3.path) };
10440
+ };
10441
+ var handleFileQuery = async (baseDir, query3) => {
10442
+ try {
10443
+ return await run2(baseDir, query3);
10444
+ } catch (error) {
10445
+ if (error instanceof FileError) return { ok: false, code: error.code, message: error.message };
10446
+ console.error(`[files] ${query3.op} query failed`, error);
10447
+ return { ok: false, code: "unavailable", message: "the worker could not complete the read" };
10448
+ }
10449
+ };
10450
+ var createFileQueryRunner = (baseDir, limit = DEFAULT_CONCURRENCY) => {
10451
+ let active = 0;
10452
+ const waiting = [];
10453
+ const acquire = async () => {
10454
+ if (active < limit) {
10455
+ active += 1;
10456
+ return;
10457
+ }
10458
+ await new Promise((resolve4) => waiting.push(resolve4));
10459
+ };
10460
+ const release = () => {
10461
+ const next = waiting.shift();
10462
+ if (next) next();
10463
+ else active -= 1;
10464
+ };
10465
+ return async (query3) => {
10466
+ await acquire();
10467
+ try {
10468
+ return await handleFileQuery(baseDir, query3);
10469
+ } finally {
10470
+ release();
10471
+ }
10472
+ };
9661
10473
  };
9662
10474
 
9663
10475
  // src/worker/watchdog.ts
@@ -9718,13 +10530,13 @@ var createLiveness = (deps) => {
9718
10530
 
9719
10531
  // src/worker/service-supervisor.ts
9720
10532
  import { mkdirSync as mkdirSync4 } from "node:fs";
9721
- import { stat as stat3 } from "node:fs/promises";
9722
- import { join as join14 } from "node:path";
10533
+ import { stat as stat4 } from "node:fs/promises";
10534
+ import { join as join17 } from "node:path";
9723
10535
 
9724
10536
  // src/worker/service-process.ts
9725
10537
  import { spawn as spawn2 } from "node:child_process";
9726
10538
  import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
9727
- import { dirname as dirname4, join as join13 } from "node:path";
10539
+ import { dirname as dirname4, join as join16 } from "node:path";
9728
10540
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9729
10541
  var START_TIMEOUT_MS = 1e4;
9730
10542
  var isWrapperMessage = (message) => {
@@ -9735,7 +10547,7 @@ var isWrapperMessage = (message) => {
9735
10547
  };
9736
10548
  var binPath = () => {
9737
10549
  const here = dirname4(fileURLToPath2(import.meta.url));
9738
- const devShim = join13(here, "..", "..", "bin", "baton.mjs");
10550
+ const devShim = join16(here, "..", "..", "bin", "baton.mjs");
9739
10551
  return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
9740
10552
  };
9741
10553
  var startServiceProcess = (input) => {
@@ -9822,15 +10634,15 @@ var createServiceSupervisor = (deps) => {
9822
10634
  };
9823
10635
  const runOne = async (requestId, cwd, name, argv, publicUrl, note) => {
9824
10636
  try {
9825
- if (!(await stat3(cwd)).isDirectory())
10637
+ if (!(await stat4(cwd)).isDirectory())
9826
10638
  return report({ requestId, ok: false, error: `not a directory: ${cwd}`, status: 400 });
9827
10639
  } catch (error) {
9828
10640
  const code = error.code;
9829
10641
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
9830
10642
  return report({ requestId, ok: false, error: `directory not found: ${cwd}`, status: 400 });
9831
10643
  }
9832
- const logDir = join14(workerRuntimeDir(cfg), "services");
9833
- const logPath = join14(logDir, `${name}.log`);
10644
+ const logDir = join17(workerRuntimeDir(cfg), "services");
10645
+ const logPath = join17(logDir, `${name}.log`);
9834
10646
  mkdirSync4(logDir, { recursive: true, mode: 448 });
9835
10647
  if (entries.has(name))
9836
10648
  return report({
@@ -9870,7 +10682,7 @@ var createServiceSupervisor = (deps) => {
9870
10682
  log(`service ${name} started in ${cwd}`);
9871
10683
  await report({ requestId, ok: true, service: service2, logPath });
9872
10684
  };
9873
- const run2 = async (requestId, cwd, name, argv, publicUrl, note) => {
10685
+ const run4 = async (requestId, cwd, name, argv, publicUrl, note) => {
9874
10686
  try {
9875
10687
  await runOne(requestId, cwd, name, argv, publicUrl, note);
9876
10688
  } catch (error) {
@@ -9888,15 +10700,15 @@ var createServiceSupervisor = (deps) => {
9888
10700
  await report({ requestId, ok: true });
9889
10701
  };
9890
10702
  const killAll = () => entries.forEach(stopEntry);
9891
- return { run: run2, stop, report, killAll };
10703
+ return { run: run4, stop, report, killAll };
9892
10704
  };
9893
10705
 
9894
10706
  // src/worker/session-supervisor.ts
9895
10707
  import { spawn as spawn3 } from "node:child_process";
9896
10708
  import { randomUUID as randomUUID2 } from "node:crypto";
9897
10709
  import { existsSync as existsSync7 } from "node:fs";
9898
- import { mkdir as mkdir5, rm as rm2, stat as stat4 } from "node:fs/promises";
9899
- import { dirname as dirname5, join as join17 } from "node:path";
10710
+ import { mkdir as mkdir6, rm as rm3, stat as stat5 } from "node:fs/promises";
10711
+ import { dirname as dirname5, join as join21 } from "node:path";
9900
10712
  import { fileURLToPath as fileURLToPath3 } from "node:url";
9901
10713
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
9902
10714
 
@@ -9990,7 +10802,7 @@ var generateTitle = async (input) => {
9990
10802
  // src/session/runner/title-codex.ts
9991
10803
  import { mkdtemp, rm } from "node:fs/promises";
9992
10804
  import { tmpdir as tmpdir3 } from "node:os";
9993
- import { join as join15 } from "node:path";
10805
+ import { join as join18 } from "node:path";
9994
10806
  import { Codex as Codex2 } from "@openai/codex-sdk";
9995
10807
  var generateTitleWithCodex = async (input) => {
9996
10808
  const { userText, assistantText } = input;
@@ -9999,7 +10811,7 @@ var generateTitleWithCodex = async (input) => {
9999
10811
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
10000
10812
  let cwd = "";
10001
10813
  try {
10002
- cwd = await mkdtemp(join15(tmpdir3(), "baton-title-"));
10814
+ cwd = await mkdtemp(join18(tmpdir3(), "baton-title-"));
10003
10815
  const client = input.client ?? new Codex2();
10004
10816
  const thread = client.startThread({
10005
10817
  workingDirectory: cwd,
@@ -10025,19 +10837,79 @@ var generateTitleWithCodex = async (input) => {
10025
10837
  }
10026
10838
  };
10027
10839
 
10840
+ // src/session/runner/title-pi.ts
10841
+ import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
10842
+ import { tmpdir as tmpdir4 } from "node:os";
10843
+ import { join as join19 } from "node:path";
10844
+ var generateTitleWithPi = async (input) => {
10845
+ const { userText, assistantText } = input;
10846
+ if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
10847
+ let cwd = "";
10848
+ let timedOut = false;
10849
+ let timer;
10850
+ try {
10851
+ cwd = await mkdtemp2(join19(tmpdir4(), "baton-title-"));
10852
+ const pi = await import("@earendil-works/pi-coding-agent");
10853
+ const agentDir = pi.getAgentDir();
10854
+ const resourceLoader = new pi.DefaultResourceLoader({
10855
+ cwd,
10856
+ agentDir,
10857
+ noExtensions: true,
10858
+ noSkills: true,
10859
+ noPromptTemplates: true,
10860
+ noThemes: true,
10861
+ noContextFiles: true,
10862
+ systemPrompt: "You title chat sessions."
10863
+ });
10864
+ await resourceLoader.reload();
10865
+ const { session: session2 } = await pi.createAgentSession({
10866
+ cwd,
10867
+ agentDir,
10868
+ resourceLoader,
10869
+ sessionManager: pi.SessionManager.inMemory(cwd),
10870
+ noTools: "all"
10871
+ });
10872
+ let error;
10873
+ session2.subscribe((event) => {
10874
+ if (event.type === "message_end" && event.message.role === "assistant")
10875
+ error = event.message.stopReason === "error" ? event.message.errorMessage : void 0;
10876
+ });
10877
+ timer = setTimeout(() => {
10878
+ timedOut = true;
10879
+ void session2.abort().catch((e) => console.error(`[title] pi abort: ${String(e)}`));
10880
+ }, TITLE_TIMEOUT_MS);
10881
+ try {
10882
+ await session2.prompt(buildTitlePrompt(userText, assistantText));
10883
+ if (timedOut) return { kind: "error", reason: `timeout after ${TITLE_TIMEOUT_MS / 1e3}s` };
10884
+ if (error) return { kind: "error", reason: error.slice(0, 300) };
10885
+ return titleOutcome(session2.getLastAssistantText() ?? "");
10886
+ } finally {
10887
+ session2.dispose();
10888
+ }
10889
+ } catch (error) {
10890
+ return { kind: "error", reason: String(error).slice(0, 300) };
10891
+ } finally {
10892
+ clearTimeout(timer);
10893
+ if (cwd)
10894
+ await rm2(cwd, { recursive: true, force: true }).catch(
10895
+ (e) => console.error(`[title] cleanup ${cwd}: ${String(e)}`)
10896
+ );
10897
+ }
10898
+ };
10899
+
10028
10900
  // src/worker/session-process.ts
10029
- import { execFile as execFile4 } from "node:child_process";
10030
- import { readFile as readFile2, rename, writeFile as writeFile3 } from "node:fs/promises";
10031
- import { join as join16 } from "node:path";
10032
- import { promisify as promisify4 } from "node:util";
10033
- var run = promisify4(execFile4);
10901
+ import { execFile as execFile5 } from "node:child_process";
10902
+ import { readFile as readFile3, rename, writeFile as writeFile4 } from "node:fs/promises";
10903
+ import { join as join20 } from "node:path";
10904
+ import { promisify as promisify5 } from "node:util";
10905
+ var run3 = promisify5(execFile5);
10034
10906
  var RUNNER_RECORD = ".baton-runner.json";
10035
- var recordPath = (cwd) => join16(cwd, RUNNER_RECORD);
10907
+ var recordPath = (cwd) => join20(cwd, RUNNER_RECORD);
10036
10908
  var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
10037
10909
  var readRunnerRecord = async (cwd) => {
10038
10910
  let raw;
10039
10911
  try {
10040
- raw = await readFile2(recordPath(cwd), "utf8");
10912
+ raw = await readFile3(recordPath(cwd), "utf8");
10041
10913
  } catch (error) {
10042
10914
  if (codeOf(error) === "ENOENT") return null;
10043
10915
  throw error;
@@ -10050,7 +10922,7 @@ var readRunnerRecord = async (cwd) => {
10050
10922
  var writeRunnerRecord = async (cwd, record) => {
10051
10923
  const path = recordPath(cwd);
10052
10924
  const temporary = `${path}.${record.runnerToken}.tmp`;
10053
- await writeFile3(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
10925
+ await writeFile4(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
10054
10926
  await rename(temporary, path);
10055
10927
  };
10056
10928
  var groupExists = (pgid) => {
@@ -10075,7 +10947,7 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
10075
10947
  if (!groupExists(record.pgid)) return;
10076
10948
  let stdout;
10077
10949
  try {
10078
- const result = await run("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
10950
+ const result = await run3("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
10079
10951
  stdout = result.stdout;
10080
10952
  } catch (error) {
10081
10953
  if (!groupExists(record.pgid)) return;
@@ -10100,7 +10972,7 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
10100
10972
  // src/worker/session-supervisor.ts
10101
10973
  var binPath2 = () => {
10102
10974
  const here = dirname5(fileURLToPath3(import.meta.url));
10103
- const devShim = join17(here, "..", "..", "bin", "baton.mjs");
10975
+ const devShim = join21(here, "..", "..", "bin", "baton.mjs");
10104
10976
  return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
10105
10977
  };
10106
10978
  var createSessionSupervisor = (deps) => {
@@ -10209,7 +11081,7 @@ var createSessionSupervisor = (deps) => {
10209
11081
  return;
10210
11082
  if (wasCanceled()) return log(`session #${sessionId} start canceled before materializing`);
10211
11083
  const directory2 = session2.cwd ?? cwd;
10212
- if (!(await stat4(directory2)).isDirectory()) throw new Error(`not a directory: ${directory2}`);
11084
+ if (!(await stat5(directory2)).isDirectory()) throw new Error(`not a directory: ${directory2}`);
10213
11085
  if (wasCanceled()) return;
10214
11086
  if (!session2.agentSessionId || !session2.cwd) {
10215
11087
  const id = randomUUID2();
@@ -10217,7 +11089,7 @@ var createSessionSupervisor = (deps) => {
10217
11089
  await client.sessions.materialize(sessionId, { agentSessionId, cwd: directory2 }, cfg.apiToken);
10218
11090
  log(`materialized session #${sessionId} (${name}) \u2192 ${directory2}`);
10219
11091
  }
10220
- await mkdir5(sessionRuntimeDir(cfg, sessionId), { recursive: true, mode: 448 });
11092
+ await mkdir6(sessionRuntimeDir(cfg, sessionId), { recursive: true, mode: 448 });
10221
11093
  if (hasTerminal(sessionId))
10222
11094
  return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
10223
11095
  if (wasCanceled()) return;
@@ -10249,7 +11121,7 @@ var createSessionSupervisor = (deps) => {
10249
11121
  return enqueue(sessionId, async () => {
10250
11122
  const entry = children.get(sessionId);
10251
11123
  await stopRecord(sessionId);
10252
- await rm2(sessionRuntimeDir(cfg, sessionId), { recursive: true, force: true });
11124
+ await rm3(sessionRuntimeDir(cfg, sessionId), { recursive: true, force: true });
10253
11125
  if (children.get(sessionId) === entry) children.delete(sessionId);
10254
11126
  });
10255
11127
  };
@@ -10258,15 +11130,12 @@ var createSessionSupervisor = (deps) => {
10258
11130
  const events = await client.sessions.listEvents(sessionId);
10259
11131
  const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
10260
11132
  if (!exchange) return log(`title #${sessionId}: no transcript yet, skipping`);
10261
- const outcome = session2.agentKind === "codex" ? await generateTitleWithCodex({
10262
- userText: exchange.userText,
10263
- assistantText: exchange.assistantText
10264
- }) : await generateTitle({
10265
- cwd: cwd2,
10266
- userText: exchange.userText,
10267
- assistantText: exchange.assistantText,
10268
- queryFn: query2
10269
- });
11133
+ const titlers = {
11134
+ "claude-code": (e) => generateTitle({ cwd: cwd2, ...e, queryFn: query2 }),
11135
+ codex: generateTitleWithCodex,
11136
+ pi: generateTitleWithPi
11137
+ };
11138
+ const outcome = await titlers[session2.agentKind](exchange);
10270
11139
  if (outcome.kind === "error") return log(`title #${sessionId} failed: ${outcome.reason}`);
10271
11140
  if (outcome.kind === "declined")
10272
11141
  return log(`title #${sessionId}: not enough to title yet, skipping`);
@@ -10347,8 +11216,9 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
10347
11216
  // src/worker/pty.ts
10348
11217
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
10349
11218
  import { createRequire as createModuleRequire } from "node:module";
10350
- import { homedir as homedir4 } from "node:os";
10351
- import { join as join18 } from "node:path";
11219
+ import { homedir as homedir5 } from "node:os";
11220
+ import { dirname as dirname6, join as join22 } from "node:path";
11221
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
10352
11222
  var MAX_TERMINALS = 10;
10353
11223
  var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
10354
11224
  file: env.BATON_CLAUDE_BIN?.trim() || "claude",
@@ -10363,13 +11233,25 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
10363
11233
  args: [moduleRequire.resolve("@openai/codex/bin/codex.js"), ...args]
10364
11234
  };
10365
11235
  };
11236
+ var piPtyCommand = (agentSessionId, env = process.env) => {
11237
+ const args = ["--session-id", agentSessionId];
11238
+ const override = env.BATON_PI_BIN?.trim();
11239
+ if (override) return { file: override, args };
11240
+ const entry = fileURLToPath4(import.meta.resolve("@earendil-works/pi-coding-agent"));
11241
+ return { file: process.execPath, args: [join22(dirname6(entry), "bundle", "cli.js"), ...args] };
11242
+ };
10366
11243
  var hasSessionJsonl = (agentSessionId) => {
10367
- const root = join18(homedir4(), ".claude", "projects");
11244
+ const root = join22(homedir5(), ".claude", "projects");
10368
11245
  if (!existsSync8(root)) return false;
10369
11246
  for (const dir of readdirSync2(root))
10370
- if (existsSync8(join18(root, dir, `${agentSessionId}.jsonl`))) return true;
11247
+ if (existsSync8(join22(root, dir, `${agentSessionId}.jsonl`))) return true;
10371
11248
  return false;
10372
11249
  };
11250
+ var ptyCommand = (kind, agentSessionId) => ({
11251
+ "claude-code": () => claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId)),
11252
+ codex: () => codexPtyCommand(agentSessionId),
11253
+ pi: () => piPtyCommand(agentSessionId)
11254
+ })[kind]();
10373
11255
  var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
10374
11256
  var isPositiveDim = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10375
11257
 
@@ -10377,13 +11259,13 @@ var isPositiveDim = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10377
11259
  var createTerminalManager = (deps) => {
10378
11260
  const { cfg, log, hasChild } = deps;
10379
11261
  const terminals = /* @__PURE__ */ new Map();
10380
- const open2 = (sessionId, agentSessionId, cwd) => {
11262
+ const open3 = (sessionId, agentSessionId, cwd) => {
10381
11263
  const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !statSync2(cwd, { throwIfNoEntry: false })?.isDirectory() ? `directory missing (${cwd})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
10382
11264
  if (why) {
10383
11265
  log(`terminal #${sessionId}: ${why} \u2014 refusing`);
10384
11266
  return;
10385
11267
  }
10386
- const command = cfg.agentKind === "codex" ? codexPtyCommand(agentSessionId) : claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId));
11268
+ const command = ptyCommand(cfg.agentKind, agentSessionId);
10387
11269
  const term = spawn4(command.file, command.args, {
10388
11270
  name: "xterm-color",
10389
11271
  cols: 80,
@@ -10435,7 +11317,7 @@ var createTerminalManager = (deps) => {
10435
11317
  }
10436
11318
  };
10437
11319
  return {
10438
- open: open2,
11320
+ open: open3,
10439
11321
  close,
10440
11322
  has: (sessionId) => terminals.has(sessionId),
10441
11323
  killAll: () => {
@@ -10473,6 +11355,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10473
11355
  log,
10474
11356
  hasChild: (id) => supervisor.has(id)
10475
11357
  });
11358
+ const files = createFileQueryRunner(cwd);
10476
11359
  const services = createServiceSupervisor({
10477
11360
  sendReport: (report) => client.services.report(report),
10478
11361
  cfg,
@@ -10528,6 +11411,8 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10528
11411
  void services.run(cmd.requestId, cmd.cwd, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
10529
11412
  else if (cmd.cmd === "service.stop")
10530
11413
  void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
11414
+ else if (cmd.cmd === "files.query")
11415
+ void files(cmd.query).then((reply) => client.files.report({ requestId: cmd.requestId, ...reply })).catch((err) => log(`files query failed: ${String(err)}`));
10531
11416
  } catch (error) {
10532
11417
  log(`invalid command: ${String(error)}`);
10533
11418
  }
@@ -10551,16 +11436,16 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10551
11436
  // src/worker/machine-id.ts
10552
11437
  import { randomUUID as randomUUID3 } from "node:crypto";
10553
11438
  import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
10554
- import { homedir as homedir5 } from "node:os";
10555
- import { dirname as dirname6, join as join19 } from "node:path";
10556
- var machineIdPath = (env = process.env) => join19(env.XDG_DATA_HOME ?? join19(env.HOME ?? homedir5(), ".local/share"), "baton", "machine-id");
11439
+ import { homedir as homedir6 } from "node:os";
11440
+ import { dirname as dirname7, join as join23 } from "node:path";
11441
+ var machineIdPath = (env = process.env) => join23(env.XDG_DATA_HOME ?? join23(env.HOME ?? homedir6(), ".local/share"), "baton", "machine-id");
10557
11442
  var readOrCreateMachineId = (path = machineIdPath()) => {
10558
11443
  if (existsSync9(path)) {
10559
11444
  const s = readFileSync6(path, "utf8").trim();
10560
11445
  if (s) return s;
10561
11446
  }
10562
11447
  const id = randomUUID3();
10563
- mkdirSync5(dirname6(path), { recursive: true });
11448
+ mkdirSync5(dirname7(path), { recursive: true });
10564
11449
  writeFileSync5(path, `${id}
10565
11450
  `, "utf8");
10566
11451
  return id;
@@ -10588,22 +11473,28 @@ var workerCreateCommand = defineCommand({
10588
11473
  args: {
10589
11474
  ...projectArg,
10590
11475
  ...common,
10591
- template: { type: "string", required: true, description: "mini-local" },
11476
+ template: {
11477
+ type: "string",
11478
+ required: true,
11479
+ description: "mini-glm-codeplan | mini-dashscope-codeplan | mini-codex"
11480
+ },
10592
11481
  name: { type: "string", required: true, description: "worker name" },
10593
- agentKind: { type: "string", default: "claude-code", description: "claude-code or codex" }
11482
+ agentKind: { type: "string", description: AGENT_KINDS.join(", ") }
10594
11483
  },
10595
11484
  run: async ({ args }) => {
10596
- if (args.template !== "mini-local") throw new Error("template must be mini-local");
10597
- if (args.agentKind !== "claude-code" && args.agentKind !== "codex")
10598
- throw new Error("invalid agentKind");
11485
+ if (!isWorkerTemplateId(args.template)) throw new Error("invalid worker template");
11486
+ const agentKind = args.agentKind ?? workerTemplateAgentKinds(args.template)[0];
11487
+ if (!isAgentKind(agentKind)) throw new Error("invalid agentKind");
11488
+ if (!workerTemplateAgentKinds(args.template).includes(agentKind))
11489
+ throw new Error("agentKind is not supported by this template");
10599
11490
  const name = args.name.trim();
10600
11491
  if (!name) throw new Error("worker name required");
10601
11492
  const creation = await clientFor(args).workerCreations.create(
10602
11493
  positiveId(resolveProjectId(args)),
10603
11494
  {
10604
- templateId: "mini-local",
11495
+ templateId: args.template,
10605
11496
  name,
10606
- agentKind: args.agentKind
11497
+ agentKind
10607
11498
  }
10608
11499
  );
10609
11500
  console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
@@ -10644,8 +11535,8 @@ var resolveWorker = async (client, projectId, handle) => {
10644
11535
  };
10645
11536
  var parseAgentKind = (value) => {
10646
11537
  const raw = typeof value === "string" && value.trim() ? value.trim() : "claude-code";
10647
- if (raw === "claude-code" || raw === "codex") return raw;
10648
- throw new Error(`invalid agent kind "${raw}" (expected claude-code or codex)`);
11538
+ if (isAgentKind(raw)) return raw;
11539
+ throw new Error(`invalid agent kind "${raw}" (expected ${AGENT_KINDS.join(", ")})`);
10649
11540
  };
10650
11541
  var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10651
11542
  const out = await client.workers.register({
@@ -10690,7 +11581,7 @@ var worker = defineCommand({
10690
11581
  },
10691
11582
  agentKind: {
10692
11583
  type: "string",
10693
- description: "agent engine for new sessions: claude-code or codex"
11584
+ description: `agent engine for new sessions: ${AGENT_KINDS.join(", ")}`
10694
11585
  },
10695
11586
  ...common
10696
11587
  },
@@ -10927,6 +11818,7 @@ var main = defineCommand({
10927
11818
  service,
10928
11819
  artifact,
10929
11820
  loop,
11821
+ notify,
10930
11822
  relay,
10931
11823
  channel,
10932
11824
  workspace,