@lesscap/baton-cli 0.8.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/baton.mjs +1223 -210
  2. package/package.json +9 -8
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") {
@@ -2282,7 +2286,7 @@ var require_websocket = __commonJS({
2282
2286
  var net = __require("net");
2283
2287
  var tls = __require("tls");
2284
2288
  var { randomBytes, createHash: createHash2 } = __require("crypto");
2285
- var { Duplex, Readable: Readable2 } = __require("stream");
2289
+ var { Duplex, Readable: Readable4 } = __require("stream");
2286
2290
  var { URL: URL2 } = __require("url");
2287
2291
  var PerMessageDeflate2 = require_permessage_deflate();
2288
2292
  var Receiver2 = require_receiver();
@@ -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
@@ -5433,7 +5437,9 @@ var rawRequest = async (url, init2) => {
5433
5437
  const res = await fetch(url, {
5434
5438
  method: init2.method,
5435
5439
  headers: { ...authHeaders, ...init2.headers ?? {} },
5436
- ...init2.body !== void 0 ? { body: init2.body } : {}
5440
+ ...init2.body !== void 0 ? { body: init2.body } : {},
5441
+ ...init2.body instanceof ReadableStream ? { duplex: "half" } : {},
5442
+ ...init2.signal ? { signal: init2.signal } : {}
5437
5443
  });
5438
5444
  if (!res.ok)
5439
5445
  throw new HttpError(res.status, `${init2.method} ${url} \u2192 ${res.status}: ${await res.text()}`);
@@ -5488,6 +5494,21 @@ var primeLogin = (baseUrl) => {
5488
5494
  );
5489
5495
  };
5490
5496
 
5497
+ // src/client/files.ts
5498
+ var filesClient = (baseUrl) => {
5499
+ const downloadUrl = (requestId) => `${baseUrl}/workers/me/files/downloads/${encodeURIComponent(requestId)}`;
5500
+ return {
5501
+ report: (result) => request(`${baseUrl}/workers/me/files/results`, { method: "PUT", body: result }),
5502
+ uploadDownload: (requestId, size, body, signal) => rawRequest(downloadUrl(requestId), {
5503
+ method: "PUT",
5504
+ body,
5505
+ headers: { "content-type": "application/octet-stream", "content-length": String(size) },
5506
+ ...signal ? { signal } : {}
5507
+ }),
5508
+ rejectDownload: (requestId, failure) => request(downloadUrl(requestId), { method: "PUT", body: failure })
5509
+ };
5510
+ };
5511
+
5491
5512
  // src/client/loops.ts
5492
5513
  var loopsClient = (baseUrl) => {
5493
5514
  const u = (p) => `${baseUrl}${p}`;
@@ -5667,7 +5688,8 @@ var sessionsClient = (baseUrl) => {
5667
5688
  },
5668
5689
  destroy: async (id) => {
5669
5690
  await request(u(`/sessions/${id}`), { method: "DELETE" });
5670
- }
5691
+ },
5692
+ notify: (id, text) => request(u(`/sessions/${id}/notify`), { method: "POST", body: { text } })
5671
5693
  };
5672
5694
  };
5673
5695
 
@@ -5743,6 +5765,7 @@ var clientFromBase = (baseUrl) => ({
5743
5765
  tasks: taskClient(baseUrl),
5744
5766
  sessions: sessionsClient(baseUrl),
5745
5767
  services: servicesClient(baseUrl),
5768
+ files: filesClient(baseUrl),
5746
5769
  loops: loopsClient(baseUrl),
5747
5770
  workers: workersClient(baseUrl)
5748
5771
  });
@@ -6262,11 +6285,11 @@ var EventSource = class extends EventTarget {
6262
6285
  return;
6263
6286
  }
6264
6287
  const decoder = new TextDecoder(), reader = body.getReader();
6265
- let open2 = true;
6288
+ let open3 = true;
6266
6289
  do {
6267
6290
  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);
6291
+ 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));
6292
+ } while (open3);
6270
6293
  }), __privateAdd(this, _onFetchError, (err) => {
6271
6294
  __privateSet(this, _controller, void 0), !(err.name === "AbortError" || err.type === "aborted") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));
6272
6295
  }), __privateAdd(this, _onEvent, (event) => {
@@ -6721,11 +6744,10 @@ import { tmpdir } from "node:os";
6721
6744
  import { join as join2 } from "node:path";
6722
6745
 
6723
6746
  // ../shared/src/domain/attachment.ts
6724
- var isImageAttachment = (att) => att.contentType.startsWith("image/");
6725
6747
  var labelAttachments = (attachments) => {
6726
6748
  const counters = { image: 0, file: 0 };
6727
6749
  return attachments.map((att) => {
6728
- const kind = isImageAttachment(att) ? "image" : "file";
6750
+ const kind = att.contentType.startsWith("image/") ? "image" : "file";
6729
6751
  counters[kind] += 1;
6730
6752
  return `${kind}-${counters[kind]}`;
6731
6753
  });
@@ -6734,6 +6756,79 @@ var labelAttachments = (attachments) => {
6734
6756
  // ../shared/src/domain/channel.ts
6735
6757
  var isMessageFor = (msg, name) => !msg.to || msg.to.length === 0 || msg.to.includes(name);
6736
6758
 
6759
+ // ../shared/src/domain/file.ts
6760
+ var FILE_TEXT_MAX_BYTES = 5 * 1024 * 1024;
6761
+ var FILE_DIRECTORY_MAX_ENTRIES = 2e3;
6762
+ var FILE_SEARCH_MAX_RESULTS = 500;
6763
+ var FILE_SEARCH_DEADLINE_MS = 5e3;
6764
+ var FILE_SEARCH_QUERY_MAX_LENGTH = 200;
6765
+ var FILE_RAW_MAX_BYTES = 5 * 1024 * 1024;
6766
+ var IMAGE_MEDIA_TYPES = {
6767
+ png: "image/png",
6768
+ jpg: "image/jpeg",
6769
+ jpeg: "image/jpeg",
6770
+ gif: "image/gif",
6771
+ webp: "image/webp",
6772
+ avif: "image/avif",
6773
+ bmp: "image/bmp",
6774
+ ico: "image/x-icon",
6775
+ svg: "image/svg+xml"
6776
+ };
6777
+ var RAW_MEDIA_TYPES = {
6778
+ ...IMAGE_MEDIA_TYPES,
6779
+ css: "text/css",
6780
+ woff: "font/woff",
6781
+ woff2: "font/woff2",
6782
+ ttf: "font/ttf",
6783
+ otf: "font/otf"
6784
+ };
6785
+ var mediaTypeIn = (types3) => (path) => {
6786
+ const extension2 = /\.([a-z\d]+)$/i.exec(path)?.[1]?.toLowerCase();
6787
+ return extension2 && types3[extension2] || null;
6788
+ };
6789
+ var imageMediaType = mediaTypeIn(IMAGE_MEDIA_TYPES);
6790
+ var rawMediaType = mediaTypeIn(RAW_MEDIA_TYPES);
6791
+ var FILE_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024;
6792
+ var FILE_DOWNLOAD_TIMEOUT_MS = 3e5;
6793
+ var normalizeFilePath = (value, allowRoot = false) => {
6794
+ if (value === "") return allowRoot ? "" : null;
6795
+ if (value.includes("\0") || value.includes("\\") || value.startsWith("/")) return null;
6796
+ if (/^[A-Za-z]:/.test(value)) return null;
6797
+ return value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..") ? value : null;
6798
+ };
6799
+ var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([".git", ".ssh", ".aws", ".kube", ".gnupg"]);
6800
+ var SENSITIVE_NAMES = /* @__PURE__ */ new Set([
6801
+ ".baton.json",
6802
+ ".envrc",
6803
+ ".npmrc",
6804
+ ".netrc",
6805
+ ".pypirc",
6806
+ ".git-credentials",
6807
+ ".pgpass",
6808
+ ".htpasswd",
6809
+ ".credentials.json"
6810
+ ]);
6811
+ var SENSITIVE_EXTENSIONS = [".pem", ".key", ".p12", ".pfx", ".jks", ".keystore"];
6812
+ 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));
6813
+ var isSensitiveFilePath = (path) => {
6814
+ const segments = path.toLowerCase().split("/");
6815
+ return segments.some((segment) => SENSITIVE_SEGMENTS.has(segment)) || isSensitiveName(segments.at(-1) ?? "");
6816
+ };
6817
+ var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
6818
+ "node_modules",
6819
+ ".pnpm-store",
6820
+ "dist",
6821
+ "build",
6822
+ "coverage",
6823
+ ".next",
6824
+ ".turbo",
6825
+ ".cache",
6826
+ ".venv",
6827
+ "__pycache__",
6828
+ "target"
6829
+ ]);
6830
+ var isExcludedDirectoryName = (name) => EXCLUDED_DIRECTORIES.has(name);
6831
+
6737
6832
  // ../shared/src/domain/service.ts
6738
6833
  var SERVICE_PUBLIC_URL_MAX_LENGTH = 2048;
6739
6834
  var SERVICE_NOTE_MAX_LENGTH = 500;
@@ -6748,6 +6843,12 @@ var isServicePublicUrl = (value) => {
6748
6843
  };
6749
6844
  var isServiceNote = (value) => value.length > 0 && value.length <= SERVICE_NOTE_MAX_LENGTH;
6750
6845
 
6846
+ // ../shared/src/domain/session.ts
6847
+ var AGENT_KINDS = ["claude-code", "codex", "pi"];
6848
+ var isAgentKind = (v) => typeof v === "string" && AGENT_KINDS.includes(v);
6849
+ var EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
6850
+ var isAgentEffort = (v) => typeof v === "string" && EFFORTS.includes(v);
6851
+
6751
6852
  // ../shared/src/domain/session-event.ts
6752
6853
  var agentMessageText = (payload) => {
6753
6854
  if (typeof payload !== "object" || payload === null) return null;
@@ -6761,6 +6862,15 @@ var agentMessageText = (payload) => {
6761
6862
  return trimmed ? { id, text: trimmed } : null;
6762
6863
  };
6763
6864
 
6865
+ // ../shared/src/domain/worker-creation.ts
6866
+ var WORKER_TEMPLATES = [
6867
+ { id: "mini-glm-codeplan", name: "GLM Code Plan", agentKinds: ["claude-code", "pi"] },
6868
+ { id: "mini-dashscope-codeplan", name: "DashScope Code Plan", agentKinds: ["claude-code", "pi"] },
6869
+ { id: "mini-codex", name: "\u672C\u673A Codex", agentKinds: ["codex"] }
6870
+ ];
6871
+ var workerTemplateAgentKinds = (id) => id === "mini-local" ? AGENT_KINDS : WORKER_TEMPLATES.find((template) => template.id === id)?.agentKinds ?? [];
6872
+ var isWorkerTemplateId = (value) => typeof value === "string" && workerTemplateAgentKinds(value).length > 0;
6873
+
6764
6874
  // src/commands/channel/listen.ts
6765
6875
  var PREVIEW = 280;
6766
6876
  var describe = (e) => {
@@ -7221,6 +7331,34 @@ var loop = defineCommand({
7221
7331
  }
7222
7332
  });
7223
7333
 
7334
+ // src/commands/notify.ts
7335
+ var describe2 = (result) => {
7336
+ if (result.status === "sent") return `notified ${result.devices} device(s)`;
7337
+ if (result.status === "throttled")
7338
+ return `throttled: this session can notify again in ${result.retryAfterSec}s`;
7339
+ return "nobody to notify: no human has messaged this session, or they have not enabled notifications";
7340
+ };
7341
+ var notify = defineCommand({
7342
+ meta: {
7343
+ name: "notify",
7344
+ 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."
7345
+ },
7346
+ args: {
7347
+ text: { type: "positional", required: true, description: "one short line for the lock screen" },
7348
+ ...common
7349
+ },
7350
+ run: async ({ args }) => {
7351
+ const raw = process.env.BATON_SESSION_ID;
7352
+ if (raw === void 0)
7353
+ throw new Error("baton notify only works inside a baton session (BATON_SESSION_ID is unset)");
7354
+ const sessionId = Number(raw);
7355
+ if (!Number.isSafeInteger(sessionId) || sessionId <= 0)
7356
+ throw new Error("invalid BATON_SESSION_ID");
7357
+ const result = await clientFor(args).sessions.notify(sessionId, args.text);
7358
+ console.log(args.json ? toJson(result) : describe2(result));
7359
+ }
7360
+ });
7361
+
7224
7362
  // src/commands/project.ts
7225
7363
  var createProject = (c, input, json) => c.projects.create(input).then((p) => renderOne(p, fmtProject, json));
7226
7364
  var listProjects = (c, workspaceId, json) => c.projects.listByWorkspace(workspaceId).then((ps) => renderList(ps, fmtProject, json));
@@ -7316,6 +7454,37 @@ var absolutePath = (value, field) => {
7316
7454
  throw new Error(`${field} must be an absolute path`);
7317
7455
  return value;
7318
7456
  };
7457
+ var environment = (value) => {
7458
+ if (!object(value)) throw new Error("workerEnv must be an object");
7459
+ return Object.fromEntries(
7460
+ Object.entries(value).map(([key, entry]) => {
7461
+ if (forbiddenEnv.has(key) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof entry !== "string" || entry.includes("\0"))
7462
+ throw new Error(`invalid worker environment key: ${key}`);
7463
+ return [key, entry];
7464
+ })
7465
+ );
7466
+ };
7467
+ var templates = (value) => {
7468
+ if (!object(value)) throw new Error("templates must be an object");
7469
+ return Object.fromEntries(
7470
+ WORKER_TEMPLATES.map((template) => {
7471
+ const raw = value[template.id];
7472
+ if (!object(raw)) throw new Error(`template ${template.id} must be configured`);
7473
+ const workerEnv2 = environment(raw.workerEnv);
7474
+ if (template.id === "mini-codex") {
7475
+ if (raw.pi !== void 0) throw new Error("mini-codex does not support pi");
7476
+ return [template.id, { workerEnv: workerEnv2 }];
7477
+ }
7478
+ const provider = template.id === "mini-glm-codeplan" ? "zai-coding-cn" : "qwen-token-plan-cn";
7479
+ const apiKey = template.id === "mini-glm-codeplan" ? "ZAI_CODING_CN_API_KEY" : "QWEN_TOKEN_PLAN_CN_API_KEY";
7480
+ if (!workerEnv2.ANTHROPIC_BASE_URL || !(workerEnv2.ANTHROPIC_AUTH_TOKEN || workerEnv2.ANTHROPIC_API_KEY) || !workerEnv2[apiKey])
7481
+ throw new Error(`template ${template.id} requires Claude and Pi credentials`);
7482
+ if (!object(raw.pi) || raw.pi.provider !== provider || typeof raw.pi.model !== "string" || !raw.pi.model.trim())
7483
+ throw new Error(`template ${template.id} requires its Pi provider and model`);
7484
+ return [template.id, { workerEnv: workerEnv2, pi: { provider, model: raw.pi.model.trim() } }];
7485
+ })
7486
+ );
7487
+ };
7319
7488
  var commandEnvironment = (config) => ({
7320
7489
  ...process.env,
7321
7490
  PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin"
@@ -7345,21 +7514,14 @@ var loadProvisionerConfig = async (path) => {
7345
7514
  throw new Error("server must be an HTTP(S) URL without credentials, query, or fragment");
7346
7515
  if (!raw.token.trim() || /[\r\n]/.test(raw.token))
7347
7516
  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
7517
  const config = {
7357
7518
  server: server.toString().replace(/\/$/, ""),
7358
7519
  token: raw.token.trim(),
7359
7520
  projectsRoot: absolutePath(raw.projectsRoot, "projectsRoot"),
7360
7521
  cliPath: absolutePath(raw.cliPath, "cliPath"),
7361
7522
  nodePath: absolutePath(raw.nodePath ?? process.execPath, "nodePath"),
7362
- workerEnv: workerEnv2
7523
+ workerEnv: environment(raw.workerEnv),
7524
+ templates: templates(raw.templates)
7363
7525
  };
7364
7526
  await access(config.cliPath, constants.R_OK);
7365
7527
  await access(config.nodePath ?? process.execPath, constants.X_OK);
@@ -7399,9 +7561,9 @@ var createProvisionerClient = (config) => {
7399
7561
 
7400
7562
  // src/provisioner/executor.ts
7401
7563
  import { execFile as execFile3 } from "node:child_process";
7402
- import { lstat, mkdir as mkdir2, realpath, writeFile } from "node:fs/promises";
7564
+ import { lstat, mkdir as mkdir3, realpath, writeFile as writeFile2 } from "node:fs/promises";
7403
7565
  import { hostname } from "node:os";
7404
- import { join as join4 } from "node:path";
7566
+ import { join as join5 } from "node:path";
7405
7567
  import { setTimeout as setTimeout2 } from "node:timers/promises";
7406
7568
  import { promisify as promisify3 } from "node:util";
7407
7569
 
@@ -7472,15 +7634,15 @@ var startAgent = async (config, agent, signal) => {
7472
7634
  throw new Error(`launchd start failed; inspect ${plist} and ${agent.log}`);
7473
7635
  }
7474
7636
  };
7475
- var launchWorkerAgent = async (config, creation, repoPath, signal) => startAgent(
7637
+ var launchWorkerAgent = async (config, creation, repoPath, workerEnv2, signal) => startAgent(
7476
7638
  config,
7477
7639
  {
7478
7640
  label: `dev.fmap.baton-worker-creation-${creation.id}`,
7479
7641
  args: [config.nodePath ?? process.execPath, config.cliPath, "worker", "run"],
7480
7642
  cwd: repoPath,
7481
7643
  env: {
7482
- PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin",
7483
- ...config.workerEnv,
7644
+ PATH: workerEnv2.PATH ?? process.env.PATH ?? "/usr/bin:/bin",
7645
+ ...workerEnv2,
7484
7646
  HOME: homedir(),
7485
7647
  XDG_DATA_HOME: workerStatePath(config, creation)
7486
7648
  },
@@ -7503,10 +7665,34 @@ var installProvisionerAgent = async (config, configPath) => startAgent(config, {
7503
7665
  log: join3(homedir(), "Library/Logs/baton-provisioner.log")
7504
7666
  });
7505
7667
 
7668
+ // src/provisioner/worker-template.ts
7669
+ import { mkdir as mkdir2, writeFile } from "node:fs/promises";
7670
+ import { join as join4 } from "node:path";
7671
+ var workerTemplateEnvironment = async (config, creation, state) => {
7672
+ if (!workerTemplateAgentKinds(creation.templateId).includes(creation.agentKind))
7673
+ throw new Error("agentKind is not supported by this template");
7674
+ if (creation.templateId === "mini-local") return config.workerEnv;
7675
+ const profile = config.templates[creation.templateId];
7676
+ if (!profile) throw new Error(`template ${creation.templateId} is not configured`);
7677
+ if (creation.agentKind !== "pi") return profile.workerEnv;
7678
+ if (!profile.pi) throw new Error("Pi provider and model are not configured");
7679
+ const agentDir = join4(state, "pi");
7680
+ await mkdir2(agentDir, { mode: 448 });
7681
+ await writeFile(
7682
+ join4(agentDir, "settings.json"),
7683
+ JSON.stringify({
7684
+ defaultProvider: profile.pi.provider,
7685
+ defaultModel: profile.pi.model
7686
+ }),
7687
+ { flag: "wx", mode: 384 }
7688
+ );
7689
+ return { ...profile.workerEnv, PI_CODING_AGENT_DIR: agentDir };
7690
+ };
7691
+
7506
7692
  // src/provisioner/executor.ts
7507
7693
  var directory = async (path) => {
7508
7694
  try {
7509
- await mkdir2(path, { mode: 448 });
7695
+ await mkdir3(path, { mode: 448 });
7510
7696
  } catch (error) {
7511
7697
  if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
7512
7698
  }
@@ -7514,7 +7700,7 @@ var directory = async (path) => {
7514
7700
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`unsafe directory: ${path}`);
7515
7701
  };
7516
7702
  var provisionWorker = async (config, creation, client, signal) => {
7517
- const repoPath = join4(
7703
+ const repoPath = join5(
7518
7704
  config.projectsRoot,
7519
7705
  `project-${creation.projectId}`,
7520
7706
  `creation-${creation.id}`
@@ -7522,14 +7708,14 @@ var provisionWorker = async (config, creation, client, signal) => {
7522
7708
  const progress = (step) => client.report(creation.id, { status: "running", step, repoPath }, signal);
7523
7709
  await progress("directory");
7524
7710
  signal.throwIfAborted();
7525
- await mkdir2(config.projectsRoot, { recursive: true, mode: 448 });
7711
+ await mkdir3(config.projectsRoot, { recursive: true, mode: 448 });
7526
7712
  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}`), {
7713
+ await directory(join5(root, `project-${creation.projectId}`));
7714
+ await mkdir3(join5(root, `project-${creation.projectId}`, `creation-${creation.id}`), {
7529
7715
  mode: 448
7530
7716
  });
7531
7717
  await progress("git");
7532
- const git = async (args) => {
7718
+ const git2 = async (args) => {
7533
7719
  try {
7534
7720
  await promisify3(execFile3)("git", ["-C", repoPath, ...args], {
7535
7721
  env: commandEnvironment(config),
@@ -7541,9 +7727,9 @@ var provisionWorker = async (config, creation, client, signal) => {
7541
7727
  throw new Error("Git repository initialization failed", { cause: error });
7542
7728
  }
7543
7729
  };
7544
- await git(["init", "-b", "main"]);
7545
- await writeFile(join4(repoPath, ".git/info/exclude"), "\n.baton.json\n", { flag: "a" });
7546
- await git([
7730
+ await git2(["init", "-b", "main"]);
7731
+ await writeFile2(join5(repoPath, ".git/info/exclude"), "\n.baton.json\n", { flag: "a" });
7732
+ await git2([
7547
7733
  "-c",
7548
7734
  "user.name=Baton",
7549
7735
  "-c",
@@ -7556,11 +7742,11 @@ var provisionWorker = async (config, creation, client, signal) => {
7556
7742
  await progress("register");
7557
7743
  const result = await client.register(creation.id, hostname(), signal);
7558
7744
  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}
7745
+ await directory(join5(root, ".worker-state"));
7746
+ await directory(join5(root, ".worker-state", `project-${creation.projectId}`));
7747
+ await mkdir3(state, { mode: 448 });
7748
+ await mkdir3(join5(state, "baton"), { mode: 448 });
7749
+ await writeFile2(join5(state, "baton/machine-id"), `${creation.machineId}
7564
7750
  `, {
7565
7751
  flag: "wx",
7566
7752
  mode: 384
@@ -7574,14 +7760,15 @@ var provisionWorker = async (config, creation, client, signal) => {
7574
7760
  machineId: result.worker.machineId,
7575
7761
  apiToken: result.apiToken
7576
7762
  });
7577
- await writeFile(join4(repoPath, ".baton.json"), `${JSON.stringify(cfg, null, 2)}
7763
+ await writeFile2(join5(repoPath, ".baton.json"), `${JSON.stringify(cfg, null, 2)}
7578
7764
  `, {
7579
7765
  flag: "wx",
7580
7766
  mode: 384
7581
7767
  });
7582
7768
  await progress("start");
7583
7769
  signal.throwIfAborted();
7584
- await launchWorkerAgent(config, creation, repoPath, signal);
7770
+ const workerEnv2 = await workerTemplateEnvironment(config, creation, state);
7771
+ await launchWorkerAgent(config, creation, repoPath, workerEnv2, signal);
7585
7772
  await progress("connect");
7586
7773
  const deadline = Date.now() + 6e4;
7587
7774
  while (Date.now() < deadline) {
@@ -7598,7 +7785,11 @@ var provisionWorker = async (config, creation, client, signal) => {
7598
7785
  // src/provisioner/daemon.ts
7599
7786
  var taskError = (error, config) => {
7600
7787
  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);
7788
+ return [
7789
+ config.token,
7790
+ ...Object.values(config.workerEnv),
7791
+ ...Object.values(config.templates).flatMap((template) => Object.values(template.workerEnv))
7792
+ ].filter((value) => value.length > 0).reduce((safe, secret) => safe.replaceAll(secret, "[redacted]"), message);
7602
7793
  };
7603
7794
  var processCreation = async (id, config, client, signal) => {
7604
7795
  let creation;
@@ -7754,7 +7945,7 @@ var provisioner = defineCommand({
7754
7945
  // src/commands/relay.ts
7755
7946
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
7756
7947
  import { tmpdir as tmpdir2 } from "node:os";
7757
- import { join as join5 } from "node:path";
7948
+ import { join as join6 } from "node:path";
7758
7949
 
7759
7950
  // src/client/relay.ts
7760
7951
  var relayClient = (baseUrl) => {
@@ -7882,11 +8073,11 @@ var listenCommand2 = defineCommand({
7882
8073
  process.stdout.write(`${JSON.stringify(o)}
7883
8074
  `);
7884
8075
  };
7885
- const spillDir = join5(tmpdir2(), "hotline", args.channel);
8076
+ const spillDir = join6(tmpdir2(), "hotline", args.channel);
7886
8077
  mkdirSync2(spillDir, { recursive: true });
7887
8078
  const surface = (m) => {
7888
8079
  const text = typeof m.text === "string" ? m.text : "";
7889
- const file = join5(spillDir, `${m.seq}.txt`);
8080
+ const file = join6(spillDir, `${m.seq}.txt`);
7890
8081
  writeFileSync4(file, text);
7891
8082
  const truncated = text.length > PREVIEW2;
7892
8083
  emit({
@@ -7900,7 +8091,7 @@ var listenCommand2 = defineCommand({
7900
8091
  file
7901
8092
  });
7902
8093
  };
7903
- const describe2 = (e) => {
8094
+ const describe3 = (e) => {
7904
8095
  const ev = e;
7905
8096
  return ev?.code ? `${ev.code}${ev.message ? ` ${ev.message}` : ""}` : String(e);
7906
8097
  };
@@ -7910,7 +8101,7 @@ var listenCommand2 = defineCommand({
7910
8101
  onMessage: (m) => {
7911
8102
  if (m.from !== me) surface(m);
7912
8103
  },
7913
- onError: (e) => emit({ type: "relay.error", error: describe2(e) })
8104
+ onError: (e) => emit({ type: "relay.error", error: describe3(e) })
7914
8105
  });
7915
8106
  emit({ type: "relay.listening", channel: args.channel, from: me, url });
7916
8107
  await new Promise((resolve4) => {
@@ -8361,12 +8552,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
8361
8552
  // src/session/runner/transcript.ts
8362
8553
  import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
8363
8554
  import { homedir as homedir2 } from "node:os";
8364
- import { join as join6 } from "node:path";
8555
+ import { join as join7 } from "node:path";
8365
8556
  var findTranscriptPath = (agentSessionId) => {
8366
- const root = join6(homedir2(), ".claude", "projects");
8557
+ const root = join7(homedir2(), ".claude", "projects");
8367
8558
  try {
8368
8559
  for (const dir of readdirSync(root)) {
8369
- const candidate = join6(root, dir, `${agentSessionId}.jsonl`);
8560
+ const candidate = join7(root, dir, `${agentSessionId}.jsonl`);
8370
8561
  try {
8371
8562
  if (statSync(candidate).isFile()) return candidate;
8372
8563
  } catch {
@@ -8444,13 +8635,13 @@ var readFirstExchange = (agentSessionId) => {
8444
8635
  };
8445
8636
 
8446
8637
  // 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";
8638
+ import { mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
8639
+ import { join as join12 } from "node:path";
8449
8640
 
8450
8641
  // src/session/runner/attachments.ts
8451
8642
  import { createWriteStream } from "node:fs";
8452
- import { mkdir as mkdir3 } from "node:fs/promises";
8453
- import { join as join7 } from "node:path";
8643
+ import { mkdir as mkdir4 } from "node:fs/promises";
8644
+ import { join as join8 } from "node:path";
8454
8645
  import { Readable } from "node:stream";
8455
8646
  import { pipeline } from "node:stream/promises";
8456
8647
  var dedupe = (name, taken) => {
@@ -8473,15 +8664,15 @@ var dedupe = (name, taken) => {
8473
8664
  var materializeAttachments = async (opts) => {
8474
8665
  const fetchImpl = opts.fetchImpl ?? fetch;
8475
8666
  const dir = opts.directory;
8476
- await mkdir3(dir, { recursive: true });
8667
+ await mkdir4(dir, { recursive: true });
8477
8668
  const taken = /* @__PURE__ */ new Set();
8478
8669
  const paths = [];
8479
8670
  for (const att of opts.attachments) {
8480
8671
  const name = dedupe(att.filename, taken);
8481
8672
  const res = await fetchImpl(`${opts.serverBase}${att.url}`);
8482
8673
  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));
8674
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(join8(dir, name)));
8675
+ paths.push(join8(dir, name));
8485
8676
  }
8486
8677
  return paths;
8487
8678
  };
@@ -8497,7 +8688,7 @@ ${text}` : header;
8497
8688
  };
8498
8689
 
8499
8690
  // src/session/runner/codex.ts
8500
- import { join as join8 } from "node:path";
8691
+ import { join as join9 } from "node:path";
8501
8692
  import { Codex } from "@openai/codex-sdk";
8502
8693
 
8503
8694
  // src/session/runner/sdk-env.ts
@@ -8510,6 +8701,7 @@ var additionalDirs = (required = []) => {
8510
8701
  const dirs = [...required, ...(raw ?? "").split(":")].map((s) => s.trim()).filter((s) => s !== "" && isAbsolute2(s) && existsSync3(s));
8511
8702
  return dirs.length > 0 ? [...new Set(dirs)] : void 0;
8512
8703
  };
8704
+ var PLAN_MODE_PREFIX = "Plan mode: inspect and propose a plan only. Do not edit files or run mutating commands.\n\n";
8513
8705
  var truthy = (value) => {
8514
8706
  if (value === void 0) return void 0;
8515
8707
  const normalized = value.trim().toLowerCase();
@@ -8530,17 +8722,8 @@ var codexApprovalPolicy = () => {
8530
8722
  return raw;
8531
8723
  return "never";
8532
8724
  };
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
- };
8725
+ var claudeEffort = (raw) => raw === "low" || raw === "medium" || raw === "high" || raw === "xhigh" || raw === "max" ? raw : void 0;
8726
+ var codexEffort = (raw) => isAgentEffort(raw) ? raw : void 0;
8544
8727
 
8545
8728
  // src/session/runner/codex.ts
8546
8729
  var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -8658,17 +8841,16 @@ var toAgentEvent = (raw) => {
8658
8841
  return { type: "raw", raw };
8659
8842
  };
8660
8843
  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}`;
8844
+ if (typeof input === "string") return `${PLAN_MODE_PREFIX}${input}`;
8663
8845
  return input.map(
8664
- (entry, i) => i === 0 && entry.type === "text" ? { ...entry, text: `${prefix}${entry.text}` } : entry
8846
+ (entry, i) => i === 0 && entry.type === "text" ? { ...entry, text: `${PLAN_MODE_PREFIX}${entry.text}` } : entry
8665
8847
  );
8666
8848
  };
8667
8849
  async function* startCodexEvents(config, input, worker2, opts) {
8668
8850
  const env = compactEnv(buildSdkEnv(opts.envOverlay));
8669
8851
  const codex = new Codex(env ? { env } : void 0);
8670
8852
  const resuming = !config.agentSessionId.startsWith("pending:");
8671
- const addDirs = additionalDirs([join8(config.runtimeDir, "attachments")]);
8853
+ const addDirs = additionalDirs([join9(config.runtimeDir, "attachments")]);
8672
8854
  const sandboxMode = codexSandboxMode(opts.planMode ?? false);
8673
8855
  const approvalPolicy = codexApprovalPolicy();
8674
8856
  const networkAccessEnabled = codexNetworkAccess();
@@ -8706,12 +8888,234 @@ async function* startCodexEvents(config, input, worker2, opts) {
8706
8888
  }
8707
8889
  }
8708
8890
 
8891
+ // src/session/runner/pi.ts
8892
+ import { readFile as readFile2 } from "node:fs/promises";
8893
+ import { homedir as homedir3 } from "node:os";
8894
+ import { join as join10 } from "node:path";
8895
+ var newPiTurnState = (startedAt = Date.now()) => ({
8896
+ startedAt,
8897
+ usage: {},
8898
+ toolArgs: {}
8899
+ });
8900
+ var READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
8901
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8902
+ var str2 = (v) => typeof v === "string" ? v : void 0;
8903
+ var resultText = (result) => isRecord3(result) && Array.isArray(result.content) ? result.content.filter(isRecord3).map((block) => str2(block.text) ?? "").join("") : "";
8904
+ var startedItem = (id, name, args) => {
8905
+ const input = isRecord3(args) ? args : {};
8906
+ if (name === "bash") {
8907
+ return {
8908
+ type: "command_execution",
8909
+ id,
8910
+ status: "in_progress",
8911
+ command: str2(input.command) ?? "",
8912
+ output: ""
8913
+ };
8914
+ }
8915
+ if (name === "write" || name === "edit") {
8916
+ return {
8917
+ type: "file_change",
8918
+ id,
8919
+ status: "in_progress",
8920
+ changes: [{ path: str2(input.path) ?? "", kind: "update" }]
8921
+ };
8922
+ }
8923
+ return { type: "tool_call", id, status: "in_progress", name, input: args ?? null };
8924
+ };
8925
+ var completedItem = (id, name, args, result, isError) => {
8926
+ const status = isError ? "failed" : "completed";
8927
+ const started = startedItem(id, name, args);
8928
+ if (started.type === "command_execution") {
8929
+ const structured = isRecord3(result) && isRecord3(result.structuredContent) ? result.structuredContent : {};
8930
+ return {
8931
+ ...started,
8932
+ status,
8933
+ output: str2(structured.output) ?? resultText(result),
8934
+ exitCode: typeof structured.exit_code === "number" ? structured.exit_code : void 0
8935
+ };
8936
+ }
8937
+ if (started.type === "tool_call") {
8938
+ return { ...started, status, output: resultText(result), isError };
8939
+ }
8940
+ return { ...started, status };
8941
+ };
8942
+ var messageItems = (message) => message.content.flatMap((block, index) => {
8943
+ const id = `${message.responseId ?? message.timestamp}:${index}`;
8944
+ if (block.type === "text" && block.text.trim()) {
8945
+ return [{ type: "agent_message", id, status: "completed", text: block.text }];
8946
+ }
8947
+ if (block.type === "thinking" && block.thinking.trim()) {
8948
+ return [{ type: "reasoning", id, status: "completed", text: block.thinking }];
8949
+ }
8950
+ return [];
8951
+ });
8952
+ var addUsage = (total, usage) => ({
8953
+ inputTokens: (total.inputTokens ?? 0) + usage.input,
8954
+ cachedInputTokens: (total.cachedInputTokens ?? 0) + usage.cacheRead,
8955
+ outputTokens: (total.outputTokens ?? 0) + usage.output,
8956
+ reasoningOutputTokens: (total.reasoningOutputTokens ?? 0) + (usage.reasoning ?? 0),
8957
+ totalCostUsd: (total.totalCostUsd ?? 0) + usage.cost.total,
8958
+ numTurns: (total.numTurns ?? 0) + 1
8959
+ });
8960
+ var piToAgentEvents = (event, state, now = Date.now()) => {
8961
+ if (event.type === "tool_execution_start") {
8962
+ return {
8963
+ events: [
8964
+ { type: "item.started", item: startedItem(event.toolCallId, event.toolName, event.args) }
8965
+ ],
8966
+ state: { ...state, toolArgs: { ...state.toolArgs, [event.toolCallId]: event.args } }
8967
+ };
8968
+ }
8969
+ if (event.type === "tool_execution_end") {
8970
+ const args = state.toolArgs[event.toolCallId];
8971
+ return {
8972
+ events: [
8973
+ {
8974
+ type: "item.completed",
8975
+ item: completedItem(event.toolCallId, event.toolName, args, event.result, event.isError)
8976
+ }
8977
+ ],
8978
+ state
8979
+ };
8980
+ }
8981
+ if (event.type === "message_end" && event.message.role === "assistant") {
8982
+ const message = event.message;
8983
+ return {
8984
+ events: messageItems(message).map((item) => ({ type: "item.completed", item })),
8985
+ // Reset on every assistant message: pi auto-retries, and only the last
8986
+ // response decides whether the turn failed.
8987
+ state: {
8988
+ ...state,
8989
+ usage: addUsage(state.usage, message.usage),
8990
+ error: message.stopReason === "error" ? message.errorMessage ?? "pi turn failed" : void 0
8991
+ }
8992
+ };
8993
+ }
8994
+ if (event.type === "agent_settled") {
8995
+ const usage = { ...state.usage, durationMs: now - state.startedAt };
8996
+ return {
8997
+ events: [
8998
+ state.error ? { type: "turn.failed", error: { message: state.error }, usage } : { type: "turn.completed", usage }
8999
+ ],
9000
+ state
9001
+ };
9002
+ }
9003
+ return { events: [], state };
9004
+ };
9005
+ var resolveModel = (runtime, raw) => {
9006
+ const slash = raw.indexOf("/");
9007
+ const model = slash > 0 ? runtime.getModel(raw.slice(0, slash), raw.slice(slash + 1)) : void 0;
9008
+ if (!model) throw new Error(`unknown pi model "${raw}" (expected provider/model)`);
9009
+ return model;
9010
+ };
9011
+ var promptParts = async (input) => {
9012
+ if (typeof input === "string") return { text: input, images: [] };
9013
+ const text = input.flatMap((entry) => entry.type === "text" ? [entry.text] : []).join("\n");
9014
+ const images = await Promise.all(
9015
+ input.flatMap(
9016
+ (entry) => entry.type === "local_image" ? [
9017
+ readFile2(entry.path).then((data) => ({
9018
+ type: "image",
9019
+ data: data.toString("base64"),
9020
+ mimeType: contentTypeForPath(entry.path)
9021
+ }))
9022
+ ] : []
9023
+ )
9024
+ );
9025
+ return { text, images };
9026
+ };
9027
+ async function* startPiEvents(config, input, opts) {
9028
+ const pi = await import("@earendil-works/pi-coding-agent");
9029
+ const modelRuntime = await pi.ModelRuntime.create();
9030
+ const model = opts.model ? resolveModel(modelRuntime, opts.model) : void 0;
9031
+ const effort = isAgentEffort(opts.effort) ? opts.effort : void 0;
9032
+ const thinkingLevel = effort === "ultra" ? void 0 : effort;
9033
+ const agentDir = pi.getAgentDir();
9034
+ const resourceLoader = new pi.DefaultResourceLoader({
9035
+ cwd: config.cwd,
9036
+ agentDir,
9037
+ // pi discovers .pi/ and .agents/ skills natively; Baton workers deploy theirs
9038
+ // under .claude/skills (project and user level).
9039
+ additionalSkillPaths: [join10(config.cwd, ".claude/skills"), join10(homedir3(), ".claude/skills")]
9040
+ });
9041
+ await resourceLoader.reload();
9042
+ const existing = pi.SessionManager.findById(config.cwd, config.agentSessionId);
9043
+ const sessionManager = existing ? pi.SessionManager.open(existing) : pi.SessionManager.create(config.cwd, void 0, { id: config.agentSessionId });
9044
+ const { session: session2 } = await pi.createAgentSession({
9045
+ cwd: config.cwd,
9046
+ agentDir,
9047
+ modelRuntime,
9048
+ resourceLoader,
9049
+ sessionManager,
9050
+ ...model ? { model } : {},
9051
+ ...thinkingLevel ? { thinkingLevel } : {},
9052
+ ...opts.planMode ? { tools: READ_ONLY_TOOLS } : {}
9053
+ });
9054
+ const modelId = session2.model ? `${session2.model.provider}/${session2.model.id}` : void 0;
9055
+ opts.log(`[pi] ${existing ? "resume" : "new"} session ${session2.sessionId}`);
9056
+ if (modelId) opts.log(`[model] ${modelId}`);
9057
+ if (thinkingLevel) opts.log(`[effort] ${thinkingLevel}`);
9058
+ if (opts.planMode) opts.log("[plan] read-only tools");
9059
+ const queue = [];
9060
+ let wake = () => {
9061
+ };
9062
+ let failure;
9063
+ let finished = false;
9064
+ let settled = false;
9065
+ const unsubscribe = session2.subscribe((event) => {
9066
+ queue.push(event);
9067
+ wake();
9068
+ });
9069
+ const abort = () => {
9070
+ void session2.abort().catch((error) => opts.log(`[pi] abort: ${String(error)}`));
9071
+ };
9072
+ opts.signal?.addEventListener("abort", abort, { once: true });
9073
+ try {
9074
+ yield { type: "thread.started", sessionId: session2.sessionId, model: modelId };
9075
+ yield { type: "turn.started" };
9076
+ const { text, images } = await promptParts(input);
9077
+ if (opts.signal?.aborted) return;
9078
+ const running = session2.prompt(opts.planMode ? `${PLAN_MODE_PREFIX}${text}` : text, images?.length ? { images } : {}).then(
9079
+ () => {
9080
+ finished = true;
9081
+ wake();
9082
+ },
9083
+ (error) => {
9084
+ failure = error;
9085
+ wake();
9086
+ }
9087
+ );
9088
+ let state = newPiTurnState();
9089
+ while (!settled) {
9090
+ const event = queue.shift();
9091
+ if (!event) {
9092
+ if (failure !== void 0) throw failure;
9093
+ if (finished) break;
9094
+ await new Promise((resolve4) => {
9095
+ wake = resolve4;
9096
+ });
9097
+ continue;
9098
+ }
9099
+ const folded = piToAgentEvents(event, state);
9100
+ state = folded.state;
9101
+ settled = event.type === "agent_settled";
9102
+ yield* folded.events;
9103
+ }
9104
+ await running;
9105
+ if (failure !== void 0) throw failure;
9106
+ } finally {
9107
+ opts.signal?.removeEventListener("abort", abort);
9108
+ unsubscribe();
9109
+ session2.dispose();
9110
+ }
9111
+ }
9112
+
8709
9113
  // src/session/runner/query.ts
8710
- import { join as join9 } from "node:path";
9114
+ import { join as join11 } from "node:path";
8711
9115
  var startQuery = (config, text, resuming, queryFn, abortController, log, { envOverlay, planMode = false, model, effort } = {}) => {
8712
9116
  const env = buildSdkEnv(envOverlay);
8713
9117
  const exe = claudeExecutable();
8714
- const addDirs = additionalDirs([join9(config.runtimeDir, "attachments")]);
9118
+ const addDirs = additionalDirs([join11(config.runtimeDir, "attachments")]);
8715
9119
  const level = claudeEffort(effort);
8716
9120
  if (addDirs) log(`[add-dir] ${addDirs.join(", ")}`);
8717
9121
  if (planMode) log("[plan] read-only planning turn");
@@ -8759,34 +9163,34 @@ var streamAgentEvents = async (events, worker2) => {
8759
9163
  return result;
8760
9164
  };
8761
9165
  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;
9166
+ var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
9167
+ var str3 = (v) => typeof v === "string" ? v : void 0;
8764
9168
  var messageContent = (payload) => {
8765
9169
  const msg = payload.message;
8766
- if (!isRecord3(msg) || !Array.isArray(msg.content)) return [];
8767
- return msg.content.filter(isRecord3);
9170
+ if (!isRecord4(msg) || !Array.isArray(msg.content)) return [];
9171
+ return msg.content.filter(isRecord4);
8768
9172
  };
8769
9173
  var textFromToolResult = (raw) => {
8770
9174
  const c = raw.content;
8771
9175
  if (typeof c === "string") return c;
8772
9176
  if (Array.isArray(c)) {
8773
- return c.map((b) => isRecord3(b) && typeof b.text === "string" ? b.text : JSON.stringify(b)).join("\n");
9177
+ return c.map((b) => isRecord4(b) && typeof b.text === "string" ? b.text : JSON.stringify(b)).join("\n");
8774
9178
  }
8775
9179
  return JSON.stringify(c ?? "");
8776
9180
  };
8777
9181
  var commandOf = (input) => {
8778
- if (!isRecord3(input)) return "";
8779
- return str2(input.command) ?? str2(input.cmd) ?? "";
9182
+ if (!isRecord4(input)) return "";
9183
+ return str3(input.command) ?? str3(input.cmd) ?? "";
8780
9184
  };
8781
9185
  var pathOf = (input) => {
8782
- if (!isRecord3(input)) return void 0;
8783
- return str2(input.file_path) ?? str2(input.path);
9186
+ if (!isRecord4(input)) return void 0;
9187
+ return str3(input.file_path) ?? str3(input.path);
8784
9188
  };
8785
9189
  var fileKindOf = (name) => name === "Delete" ? "delete" : "update";
8786
9190
  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) ?? "",
9191
+ if (!isRecord4(input) || !Array.isArray(input.todos)) return [];
9192
+ return input.todos.filter(isRecord4).map((todo) => ({
9193
+ text: str3(todo.content) ?? str3(todo.text) ?? "",
8790
9194
  completed: todo.status === "completed" || todo.completed === true
8791
9195
  }));
8792
9196
  };
@@ -8795,8 +9199,8 @@ var mcpParts = (name) => {
8795
9199
  return match ? { server: match[1] ?? "", tool: match[2] ?? "" } : null;
8796
9200
  };
8797
9201
  var startedItemFromToolUse = (block) => {
8798
- const id = str2(block.id) ?? `tool-${Math.random().toString(36).slice(2)}`;
8799
- const name = str2(block.name) ?? "tool";
9202
+ const id = str3(block.id) ?? `tool-${Math.random().toString(36).slice(2)}`;
9203
+ const name = str3(block.name) ?? "tool";
8800
9204
  const input = block.input ?? null;
8801
9205
  if (name === "Bash") {
8802
9206
  return {
@@ -8832,7 +9236,7 @@ var startedItemFromToolUse = (block) => {
8832
9236
  type: "web_search",
8833
9237
  id,
8834
9238
  status: "in_progress",
8835
- query: isRecord3(input) ? str2(input.query) ?? "" : ""
9239
+ query: isRecord4(input) ? str3(input.query) ?? "" : ""
8836
9240
  };
8837
9241
  }
8838
9242
  if (name === "TodoWrite") {
@@ -8851,9 +9255,9 @@ var completeItem = (item, output, isError) => {
8851
9255
  };
8852
9256
  var resultEvent = (message) => {
8853
9257
  const rec = message;
8854
- const subtype = str2(rec.subtype) ?? "unknown";
9258
+ const subtype = str3(rec.subtype) ?? "unknown";
8855
9259
  const isError = rec.is_error === true || subtype !== "success";
8856
- const resultText = str2(rec.result) ?? "";
9260
+ const resultText2 = str3(rec.result) ?? "";
8857
9261
  const usage = {
8858
9262
  numTurns: typeof rec.num_turns === "number" ? rec.num_turns : void 0,
8859
9263
  totalCostUsd: typeof rec.total_cost_usd === "number" ? rec.total_cost_usd : void 0,
@@ -8861,7 +9265,7 @@ var resultEvent = (message) => {
8861
9265
  };
8862
9266
  return isError ? {
8863
9267
  type: "turn.failed",
8864
- error: { message: resultText || `claude result: ${subtype}`, subtype },
9268
+ error: { message: resultText2 || `claude result: ${subtype}`, subtype },
8865
9269
  usage,
8866
9270
  raw: message
8867
9271
  } : { type: "turn.completed", subtype, usage, raw: message };
@@ -8878,8 +9282,8 @@ async function* claudeToAgentEvents(messages) {
8878
9282
  systemEmitted = true;
8879
9283
  yield {
8880
9284
  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),
9285
+ sessionId: str3(p.session_id) ?? "",
9286
+ model: str3(p.model) ?? (isRecord4(p.model_info) ? str3(p.model_info.id) : void 0),
8883
9287
  raw: message
8884
9288
  };
8885
9289
  yield { type: "turn.started", raw: message };
@@ -8918,7 +9322,7 @@ async function* claudeToAgentEvents(messages) {
8918
9322
  pendingTools.set(item.id, item);
8919
9323
  yield { type: "item.started", item, raw: block };
8920
9324
  } else if (block.type === "tool_result") {
8921
- const id = str2(block.tool_use_id) ?? "";
9325
+ const id = str3(block.tool_use_id) ?? "";
8922
9326
  const item = pendingTools.get(id);
8923
9327
  if (item) {
8924
9328
  const completed = completeItem(item, textFromToolResult(block), block.is_error === true);
@@ -8936,7 +9340,7 @@ async function* claudeToAgentEvents(messages) {
8936
9340
  if (message.type === "user") {
8937
9341
  for (const block of messageContent(p)) {
8938
9342
  if (block.type !== "tool_result") continue;
8939
- const id = str2(block.tool_use_id) ?? "";
9343
+ const id = str3(block.tool_use_id) ?? "";
8940
9344
  const item = pendingTools.get(id);
8941
9345
  if (!item) {
8942
9346
  yield { type: "raw", raw: block };
@@ -8962,19 +9366,19 @@ var UnsettledExecution = class extends Error {
8962
9366
  var materializeImages = async (config, execution) => {
8963
9367
  const images = execution.message.payload.images ?? [];
8964
9368
  if (!images.length) return [];
8965
- const directory2 = join10(
9369
+ const directory2 = join12(
8966
9370
  config.runtimeDir,
8967
9371
  "attachments",
8968
9372
  `message-${execution.message.id}`,
8969
9373
  "pasted"
8970
9374
  );
8971
- await mkdir4(directory2, { recursive: true });
9375
+ await mkdir5(directory2, { recursive: true });
8972
9376
  return Promise.all(
8973
9377
  images.map(async (image, index) => {
8974
9378
  const match = /^data:image\/(png|jpeg|webp|gif);base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image);
8975
9379
  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"));
9380
+ const path = join12(directory2, `pasted-${index + 1}.${match[1]}`);
9381
+ await writeFile3(path, Buffer.from(match[2] ?? "", "base64"));
8978
9382
  return path;
8979
9383
  })
8980
9384
  );
@@ -8982,7 +9386,7 @@ var materializeImages = async (config, execution) => {
8982
9386
  var buildPrompt = async (config, execution, fetchImpl) => {
8983
9387
  const { text: rawText, attachments = [] } = execution.message.payload;
8984
9388
  const paths = attachments.length ? await materializeAttachments({
8985
- directory: join10(
9389
+ directory: join12(
8986
9390
  config.runtimeDir,
8987
9391
  "attachments",
8988
9392
  `message-${execution.message.id}`,
@@ -9065,25 +9469,27 @@ var runTurn = async (config, worker2, execution, resuming, queryFn, log, envOver
9065
9469
  const built = await buildPrompt(config, execution, fetchImpl);
9066
9470
  if (abort.signal.aborted) throw new Error("interrupted before SDK start");
9067
9471
  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
- );
9472
+ const turn = { planMode, model, effort };
9473
+ const run4 = {
9474
+ "claude-code": () => streamClaudeSdkEvents(
9475
+ startQuery(config, built.text, resuming, queryFn, abort, log, { envOverlay, ...turn }),
9476
+ output
9477
+ ),
9478
+ codex: () => streamAgentEvents(
9479
+ startCodexEvents(config, built.input, worker2, {
9480
+ envOverlay,
9481
+ ...turn,
9482
+ signal: abort.signal,
9483
+ log
9484
+ }),
9485
+ output
9486
+ ),
9487
+ pi: () => streamAgentEvents(
9488
+ startPiEvents(config, built.input, { ...turn, signal: abort.signal, log }),
9489
+ output
9490
+ )
9491
+ };
9492
+ return run4[config.agentKind]();
9087
9493
  };
9088
9494
  const consuming = consume().then(
9089
9495
  (value) => ({ kind: "result", value }),
@@ -9107,7 +9513,7 @@ var runTurn = async (config, worker2, execution, resuming, queryFn, log, envOver
9107
9513
  };
9108
9514
 
9109
9515
  // src/session/runner.ts
9110
- var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "codex" ? !config.agentSessionId.startsWith("pending:") : transcriptPath(config.agentSessionId) !== null;
9516
+ var isAgentConversationResumable = (config, transcriptPath = findTranscriptPath) => config.agentKind === "claude-code" ? transcriptPath(config.agentSessionId) !== null : !config.agentSessionId.startsWith("pending:");
9111
9517
  var shouldReap = (lastActivity, now, busy, queueLen, idleMs) => !busy && queueLen === 0 && now - lastActivity >= idleMs;
9112
9518
  var runDaemon = async (config, deps, signal) => {
9113
9519
  const log = deps.log ?? console.log;
@@ -9126,7 +9532,7 @@ var runDaemon = async (config, deps, signal) => {
9126
9532
  });
9127
9533
  const drain = async () => {
9128
9534
  if (draining || closed || stranded) return;
9129
- const run2 = async () => {
9535
+ const run4 = async () => {
9130
9536
  while (!closed && !stranded && !signal.aborted) {
9131
9537
  let claimed;
9132
9538
  try {
@@ -9169,7 +9575,7 @@ var runDaemon = async (config, deps, signal) => {
9169
9575
  }
9170
9576
  }
9171
9577
  };
9172
- draining = run2();
9578
+ draining = run4();
9173
9579
  try {
9174
9580
  await draining;
9175
9581
  } finally {
@@ -9236,16 +9642,16 @@ var runDaemon = async (config, deps, signal) => {
9236
9642
 
9237
9643
  // src/worker/runtime-paths.ts
9238
9644
  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"),
9645
+ import { homedir as homedir4 } from "node:os";
9646
+ import { join as join13 } from "node:path";
9647
+ var workerRuntimeDir = (owner, env = process.env) => join13(
9648
+ env.XDG_DATA_HOME ?? join13(env.HOME ?? homedir4(), ".local/share"),
9243
9649
  "baton",
9244
9650
  "runtime",
9245
9651
  createHash("sha256").update(owner.server).digest("hex"),
9246
9652
  `worker-${owner.workerId}`
9247
9653
  );
9248
- var sessionRuntimeDir = (owner, sessionId, env = process.env) => join11(workerRuntimeDir(owner, env), "sessions", String(sessionId));
9654
+ var sessionRuntimeDir = (owner, sessionId, env = process.env) => join13(workerRuntimeDir(owner, env), "sessions", String(sessionId));
9249
9655
 
9250
9656
  // src/commands/session/run.ts
9251
9657
  var bearerFetch = (token) => (input, init2) => fetch(input, {
@@ -9299,6 +9705,7 @@ var sessionRunCommand = defineCommand({
9299
9705
  const sessionId = Number(args.session);
9300
9706
  if (!Number.isInteger(sessionId) || sessionId <= 0)
9301
9707
  throw new Error(`invalid session id: ${args.session}`);
9708
+ process.env.BATON_SESSION_ID = String(sessionId);
9302
9709
  const client = createClient(server, { bearer: workerToken });
9303
9710
  const s = await client.sessions.get(sessionId);
9304
9711
  if (!s.agentSessionId || !s.cwd)
@@ -9606,7 +10013,7 @@ import { hostname as osHostname } from "node:os";
9606
10013
  // src/worker/bundled-skills.ts
9607
10014
  import { spawnSync as spawnSync2 } from "node:child_process";
9608
10015
  import { cpSync, existsSync as existsSync5 } from "node:fs";
9609
- import { dirname as dirname3, join as join12 } from "node:path";
10016
+ import { dirname as dirname3, join as join14 } from "node:path";
9610
10017
  import { fileURLToPath } from "node:url";
9611
10018
 
9612
10019
  // src/worker/git-exclude.ts
@@ -9638,8 +10045,8 @@ var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
9638
10045
  var bundledSkillPath = () => {
9639
10046
  const here = dirname3(fileURLToPath(import.meta.url));
9640
10047
  const candidates = [
9641
- join12(here, "..", "skills", SKILL_NAME),
9642
- join12(here, "..", "..", "skills", SKILL_NAME)
10048
+ join14(here, "..", "skills", SKILL_NAME),
10049
+ join14(here, "..", "..", "skills", SKILL_NAME)
9643
10050
  ];
9644
10051
  const found = candidates.find(existsSync5);
9645
10052
  if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
@@ -9656,8 +10063,528 @@ var syncBundledArtifactSkill = (cwd) => {
9656
10063
  const source = bundledSkillPath();
9657
10064
  TARGETS.filter((target) => !isTracked(cwd, target)).forEach((target) => {
9658
10065
  ensureExcluded(cwd, `${target}/`);
9659
- cpSync(source, join12(cwd, target), { recursive: true, force: true });
10066
+ cpSync(source, join14(cwd, target), { recursive: true, force: true });
10067
+ });
10068
+ };
10069
+
10070
+ // src/worker/files/branches.ts
10071
+ import { realpath as realpath2 } from "node:fs/promises";
10072
+
10073
+ // src/worker/files/git.ts
10074
+ import { execFile as execFile4 } from "node:child_process";
10075
+ import { promisify as promisify4 } from "node:util";
10076
+ var run = promisify4(execFile4);
10077
+ var GIT_TIMEOUT_MS = 3e3;
10078
+ var runGit = async (cwd, args, maxBuffer = 4 * 1024 * 1024) => {
10079
+ const { stdout } = await run("git", ["--literal-pathspecs", "-C", cwd, ...args], {
10080
+ encoding: "buffer",
10081
+ timeout: GIT_TIMEOUT_MS,
10082
+ maxBuffer,
10083
+ env: { ...process.env, LC_ALL: "C", GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
9660
10084
  });
10085
+ return stdout;
10086
+ };
10087
+
10088
+ // src/worker/files/branches.ts
10089
+ var NOT_A_WORK_TREE_EXIT = 128;
10090
+ var git = async (cwd, args) => (await runGit(cwd, args)).toString("utf8");
10091
+ var workTreeTop = async (dir) => {
10092
+ try {
10093
+ return await realpath2((await git(dir, ["rev-parse", "--show-toplevel"])).trim());
10094
+ } catch (error) {
10095
+ if (error.code !== NOT_A_WORK_TREE_EXIT)
10096
+ console.warn("[files] git rev-parse failed; no branches to browse", error);
10097
+ return null;
10098
+ }
10099
+ };
10100
+ var parseWorktrees = (stdout) => stdout.split("\0\0").map((record) => record.split("\0").filter(Boolean)).flatMap((fields) => {
10101
+ const path = fields.find((field) => field.startsWith("worktree "))?.slice("worktree ".length);
10102
+ const ref = fields.find((field) => field.startsWith("branch "))?.slice("branch ".length);
10103
+ const skipped = fields.some((field) => field === "bare" || field.startsWith("prunable"));
10104
+ return path && ref && !skipped ? [{ path, name: ref.replace(/^refs\/heads\//, "") }] : [];
10105
+ });
10106
+ var parseTips = (stdout) => new Map(
10107
+ stdout.split("\n").map((line) => line.split("\0")).flatMap(
10108
+ ([ref, commit]) => ref && commit ? [[ref.replace(/^refs\/heads\//, ""), commit]] : []
10109
+ )
10110
+ );
10111
+ var existingPath = async (path) => {
10112
+ try {
10113
+ return await realpath2(path);
10114
+ } catch (error) {
10115
+ if (error.code !== "ENOENT")
10116
+ console.warn(`[files] cannot resolve worktree ${path}`, error);
10117
+ return null;
10118
+ }
10119
+ };
10120
+ var rank = (branch) => branch.current ? 0 : branch.worktree ? 1 : 2;
10121
+ var discoverBranches = async (baseDir) => {
10122
+ const base = await realpath2(baseDir);
10123
+ if (await workTreeTop(base) !== base) return [];
10124
+ let worktrees;
10125
+ let tips;
10126
+ try {
10127
+ const [listed, refs] = await Promise.all([
10128
+ git(base, ["worktree", "list", "--porcelain", "-z"]),
10129
+ git(base, ["for-each-ref", "--format=%(refname)%00%(objectname)", "refs/heads"])
10130
+ ]);
10131
+ worktrees = parseWorktrees(listed);
10132
+ tips = parseTips(refs);
10133
+ } catch (error) {
10134
+ console.warn("[files] listing git branches failed; no branches to browse", error);
10135
+ return [];
10136
+ }
10137
+ const checkedOut = new Map(
10138
+ (await Promise.all(
10139
+ worktrees.map(async ({ path, name }) => [name, await existingPath(path)])
10140
+ )).filter((entry) => entry[1] !== null)
10141
+ );
10142
+ return [.../* @__PURE__ */ new Set([...checkedOut.keys(), ...tips.keys()])].map((name) => {
10143
+ const worktree = checkedOut.get(name) ?? null;
10144
+ return { name, worktree, commit: tips.get(name) ?? null, current: worktree === base };
10145
+ }).sort((a, b) => rank(a) - rank(b) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
10146
+ };
10147
+
10148
+ // src/worker/files/commit.ts
10149
+ import { spawn as spawn2 } from "node:child_process";
10150
+ import { Readable as Readable2 } from "node:stream";
10151
+
10152
+ // src/worker/files/common.ts
10153
+ var FileError = class extends Error {
10154
+ constructor(code, message) {
10155
+ super(message);
10156
+ this.code = code;
10157
+ }
10158
+ code;
10159
+ };
10160
+ var notFound = (path) => new FileError("file_not_found", `not found: ${path}`);
10161
+ var checkDownloadSize = (path, size) => {
10162
+ if (size > FILE_DOWNLOAD_MAX_BYTES)
10163
+ throw new FileError("file_too_large", `too large to download (${size} bytes): ${path}`);
10164
+ };
10165
+ var checkedPath = (path, allowRoot) => {
10166
+ const rel = normalizeFilePath(path, allowRoot);
10167
+ if (rel === null) throw new FileError("invalid_request", `invalid path: ${path}`);
10168
+ if (isSensitiveFilePath(rel)) throw new FileError("forbidden_path", `protected file: ${rel}`);
10169
+ return rel;
10170
+ };
10171
+ var searchNeedle = (query3) => {
10172
+ const trimmed = query3.trim();
10173
+ if (trimmed === "" || trimmed.length > FILE_SEARCH_QUERY_MAX_LENGTH)
10174
+ throw new FileError(
10175
+ "invalid_request",
10176
+ `search query must be 1-${FILE_SEARCH_QUERY_MAX_LENGTH} characters`
10177
+ );
10178
+ return trimmed.toLowerCase();
10179
+ };
10180
+ var byKindThenName = (a, b) => {
10181
+ if (a.kind !== b.kind) return a.kind === "directory" ? -1 : 1;
10182
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
10183
+ };
10184
+ var decodeText = (bytes) => {
10185
+ if (bytes.includes(0)) return null;
10186
+ try {
10187
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10188
+ } catch (error) {
10189
+ if (error instanceof TypeError) return null;
10190
+ throw error;
10191
+ }
10192
+ };
10193
+ var hasBytes = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
10194
+ var hasText = (bytes, text, offset = 0) => bytes.subarray(offset, offset + text.length).toString("latin1") === text;
10195
+ var SVG_START = /^\s*(?:<\?xml[^>]*>\s*)?(?:<!--[\s\S]*?-->\s*)*(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>]/i;
10196
+ var RAW_SIGNATURES = {
10197
+ "image/png": (bytes) => hasBytes(bytes, [137, 80, 78, 71, 13, 10, 26, 10]),
10198
+ "image/jpeg": (bytes) => hasBytes(bytes, [255, 216, 255]),
10199
+ "image/gif": (bytes) => hasText(bytes, "GIF8"),
10200
+ "image/webp": (bytes) => hasText(bytes, "RIFF") && hasText(bytes, "WEBP", 8),
10201
+ "image/avif": (bytes) => hasText(bytes, "ftyp", 4) && bytes.subarray(8, 32).toString("latin1").includes("avif"),
10202
+ "image/bmp": (bytes) => hasText(bytes, "BM"),
10203
+ "image/x-icon": (bytes) => hasBytes(bytes, [0, 0, 1, 0]),
10204
+ "image/svg+xml": (bytes) => {
10205
+ const text = decodeText(bytes);
10206
+ return text !== null && SVG_START.test(text.slice(0, 4096));
10207
+ },
10208
+ "text/css": (bytes) => decodeText(bytes) !== null,
10209
+ "font/woff": (bytes) => hasText(bytes, "wOFF"),
10210
+ "font/woff2": (bytes) => hasText(bytes, "wOF2"),
10211
+ "font/ttf": (bytes) => hasBytes(bytes, [0, 1, 0, 0]) || hasText(bytes, "true"),
10212
+ "font/otf": (bytes) => hasText(bytes, "OTTO")
10213
+ };
10214
+ var rawPayload = (path, mediaType, bytes) => {
10215
+ if (!RAW_SIGNATURES[mediaType]?.(bytes))
10216
+ throw new FileError("unsupported", `not a valid ${mediaType} file: ${path}`);
10217
+ return { mediaType, base64: bytes.toString("base64") };
10218
+ };
10219
+
10220
+ // src/worker/files/commit.ts
10221
+ var MISSING_OBJECT_EXIT = 128;
10222
+ var SEARCH_MAX_OUTPUT = 64 * 1024 * 1024;
10223
+ var REGULAR_FILE_MODES = /* @__PURE__ */ new Set(["100644", "100755"]);
10224
+ var parseTree = (stdout) => stdout.toString("utf8").split("\0").flatMap((record) => {
10225
+ const tab = record.indexOf(" ");
10226
+ if (tab < 0) return [];
10227
+ const [mode = "", type = "", oid = "", size = ""] = record.slice(0, tab).split(/ +/);
10228
+ const bytes = /^\d+$/.test(size) ? Number(size) : null;
10229
+ return [{ mode, type, oid, size: bytes, path: record.slice(tab + 1) }];
10230
+ });
10231
+ var isRegularFile = (entry) => entry.type === "blob" && REGULAR_FILE_MODES.has(entry.mode);
10232
+ var lsTree = async (repo, args) => {
10233
+ try {
10234
+ return parseTree(await runGit(repo, ["ls-tree", "-z", ...args]));
10235
+ } catch (error) {
10236
+ if (error.code === MISSING_OBJECT_EXIT) return null;
10237
+ throw error;
10238
+ }
10239
+ };
10240
+ var listCommitDirectory = async (repo, commit, path) => {
10241
+ const rel = checkedPath(path, true);
10242
+ const children = await lsTree(repo, [`${commit}:${rel}`]);
10243
+ if (children === null) throw notFound(rel || ".");
10244
+ const entries = children.flatMap((entry) => {
10245
+ const child = rel ? `${rel}/${entry.path}` : entry.path;
10246
+ if (isSensitiveFilePath(child)) return [];
10247
+ if (entry.type === "tree")
10248
+ return isExcludedDirectoryName(entry.path) ? [] : [{ name: entry.path, path: child, kind: "directory" }];
10249
+ return isRegularFile(entry) ? [{ name: entry.path, path: child, kind: "file" }] : [];
10250
+ }).sort(byKindThenName);
10251
+ return {
10252
+ path: rel,
10253
+ entries: entries.slice(0, FILE_DIRECTORY_MAX_ENTRIES),
10254
+ truncated: entries.length > FILE_DIRECTORY_MAX_ENTRIES
10255
+ };
10256
+ };
10257
+ var searchCommit = async (repo, commit, query3) => {
10258
+ const needle = searchNeedle(query3);
10259
+ let stdout;
10260
+ try {
10261
+ stdout = await runGit(repo, ["ls-tree", "-r", "-z", commit], SEARCH_MAX_OUTPUT);
10262
+ } catch (error) {
10263
+ if (error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
10264
+ throw new FileError("unavailable", "the branch has too many files to search");
10265
+ throw error;
10266
+ }
10267
+ const matches = parseTree(stdout).filter(
10268
+ (entry) => isRegularFile(entry) && !entry.path.split("/").slice(0, -1).some(isExcludedDirectoryName) && !isSensitiveFilePath(entry.path) && entry.path.toLowerCase().includes(needle)
10269
+ );
10270
+ return {
10271
+ entries: matches.slice(0, FILE_SEARCH_MAX_RESULTS).map((entry) => ({
10272
+ name: entry.path.split("/").at(-1) ?? entry.path,
10273
+ path: entry.path,
10274
+ kind: "file"
10275
+ })),
10276
+ truncated: matches.length > FILE_SEARCH_MAX_RESULTS
10277
+ };
10278
+ };
10279
+ var regularEntry = async (repo, commit, rel) => {
10280
+ const entry = (await lsTree(repo, ["-l", commit, "--", rel]))?.find((item) => item.path === rel);
10281
+ if (!entry || !isRegularFile(entry) || entry.size === null) throw notFound(rel);
10282
+ return { oid: entry.oid, size: entry.size };
10283
+ };
10284
+ var viewCommitFile = async (repo, commit, path) => {
10285
+ const rel = checkedPath(path, false);
10286
+ const entry = await regularEntry(repo, commit, rel);
10287
+ const meta = { path: rel, name: rel.split("/").at(-1) ?? rel, commit };
10288
+ const mediaType = imageMediaType(rel);
10289
+ if (mediaType)
10290
+ 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 };
10291
+ if (entry.size > FILE_TEXT_MAX_BYTES)
10292
+ return { ...meta, size: entry.size, kind: "too_large", limit: FILE_TEXT_MAX_BYTES };
10293
+ const bytes = await runGit(repo, ["cat-file", "blob", entry.oid], FILE_TEXT_MAX_BYTES + 1);
10294
+ const content = decodeText(bytes);
10295
+ return content === null ? { ...meta, size: bytes.length, kind: "binary" } : { ...meta, size: bytes.length, kind: "text", content };
10296
+ };
10297
+ var readRawCommitFile = async (repo, commit, path) => {
10298
+ const rel = checkedPath(path, false);
10299
+ const mediaType = rawMediaType(rel);
10300
+ if (!mediaType) throw new FileError("unsupported", `not served raw: ${rel}`);
10301
+ const entry = await regularEntry(repo, commit, rel);
10302
+ if (entry.size > FILE_RAW_MAX_BYTES)
10303
+ throw new FileError("unsupported", `too large to serve raw: ${rel}`);
10304
+ const bytes = await runGit(repo, ["cat-file", "blob", entry.oid], FILE_RAW_MAX_BYTES + 1);
10305
+ return rawPayload(rel, mediaType, bytes);
10306
+ };
10307
+ var openCommitDownload = async (repo, commit, path, signal) => {
10308
+ const rel = checkedPath(path, false);
10309
+ const entry = await regularEntry(repo, commit, rel);
10310
+ checkDownloadSize(rel, entry.size);
10311
+ const child = spawn2("git", ["--literal-pathspecs", "-C", repo, "cat-file", "blob", entry.oid], {
10312
+ stdio: ["ignore", "pipe", "ignore"],
10313
+ signal,
10314
+ env: { ...process.env, LC_ALL: "C", GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
10315
+ });
10316
+ const exited = new Promise((resolve4) => {
10317
+ child.once("error", resolve4);
10318
+ child.once(
10319
+ "close",
10320
+ (code, killed) => resolve4(code === 0 ? null : new Error(`git cat-file exited with ${code ?? killed}`))
10321
+ );
10322
+ });
10323
+ const body = Readable2.toWeb(child.stdout).pipeThrough(
10324
+ new TransformStream({
10325
+ async flush() {
10326
+ const failure = await exited;
10327
+ if (failure) throw failure;
10328
+ }
10329
+ })
10330
+ );
10331
+ return { size: entry.size, body, close: () => child.kill() };
10332
+ };
10333
+
10334
+ // src/worker/files/disk.ts
10335
+ import { constants as constants2 } from "node:fs";
10336
+ import { open as open2, readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
10337
+ import { isAbsolute as isAbsolute4, join as join15, relative, sep } from "node:path";
10338
+ import { Readable as Readable3 } from "node:stream";
10339
+ var isMissing = (error) => ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code ?? "");
10340
+ var childPath = (parent, name) => parent ? `${parent}/${name}` : name;
10341
+ var inside = (root, real) => {
10342
+ const rel = relative(root, real);
10343
+ if (isAbsolute4(rel) || rel === ".." || rel.startsWith(`..${sep}`)) return null;
10344
+ return rel.split(sep).join("/");
10345
+ };
10346
+ var resolveInside = async (root, path, allowRoot) => {
10347
+ const rel = checkedPath(path, allowRoot);
10348
+ let real;
10349
+ try {
10350
+ real = await realpath3(join15(root, rel));
10351
+ } catch (error) {
10352
+ if (isMissing(error)) throw notFound(rel);
10353
+ throw error;
10354
+ }
10355
+ const target = inside(root, real);
10356
+ if (target === null || isSensitiveFilePath(target)) throw notFound(rel);
10357
+ return { path: rel, real };
10358
+ };
10359
+ var symlinkEntry = async (root, linkPath, name, path) => {
10360
+ let real;
10361
+ try {
10362
+ real = await realpath3(linkPath);
10363
+ } catch (error) {
10364
+ if (!isMissing(error)) console.warn(`[files] cannot resolve symlink ${linkPath}`, error);
10365
+ return null;
10366
+ }
10367
+ const target = inside(root, real);
10368
+ if (target === null || isSensitiveFilePath(target)) return null;
10369
+ const info = await stat3(real);
10370
+ if (info.isDirectory())
10371
+ return isExcludedDirectoryName(name) ? null : { name, path, kind: "directory" };
10372
+ return info.isFile() ? { name, path, kind: "file" } : null;
10373
+ };
10374
+ var toEntry = async (root, dir, dirent) => {
10375
+ const path = childPath(dir.path, dirent.name);
10376
+ if (isSensitiveFilePath(path)) return null;
10377
+ if (dirent.isDirectory())
10378
+ return isExcludedDirectoryName(dirent.name) ? null : { name: dirent.name, path, kind: "directory" };
10379
+ if (dirent.isFile()) return { name: dirent.name, path, kind: "file" };
10380
+ return dirent.isSymbolicLink() ? symlinkEntry(root, join15(dir.real, dirent.name), dirent.name, path) : null;
10381
+ };
10382
+ var listDirectory = async (root, path) => {
10383
+ const dir = await resolveInside(root, path, true);
10384
+ if (!(await stat3(dir.real)).isDirectory()) throw notFound(dir.path);
10385
+ const dirents = await readdir(dir.real, { withFileTypes: true });
10386
+ const entries = (await Promise.all(dirents.map((dirent) => toEntry(root, dir, dirent)))).filter((entry) => entry !== null).sort(byKindThenName);
10387
+ return {
10388
+ path: dir.path,
10389
+ entries: entries.slice(0, FILE_DIRECTORY_MAX_ENTRIES),
10390
+ truncated: entries.length > FILE_DIRECTORY_MAX_ENTRIES
10391
+ };
10392
+ };
10393
+ var searchFiles = async (root, query3, now = Date.now) => {
10394
+ const needle = searchNeedle(query3);
10395
+ const deadline = now() + FILE_SEARCH_DEADLINE_MS;
10396
+ const entries = [];
10397
+ const pending = [""];
10398
+ for (let index = 0; index < pending.length; index += 1) {
10399
+ if (now() > deadline) return { entries, truncated: true };
10400
+ const dir = pending[index] ?? "";
10401
+ let dirents;
10402
+ try {
10403
+ dirents = await readdir(join15(root, dir), { withFileTypes: true });
10404
+ } catch (error) {
10405
+ console.warn(`[files] search skipped unreadable directory ${dir || "."}`, error);
10406
+ continue;
10407
+ }
10408
+ for (const dirent of dirents) {
10409
+ const path = childPath(dir, dirent.name);
10410
+ if (isSensitiveFilePath(path)) continue;
10411
+ if (dirent.isDirectory()) {
10412
+ if (!isExcludedDirectoryName(dirent.name)) pending.push(path);
10413
+ } else if (dirent.isFile() && path.toLowerCase().includes(needle)) {
10414
+ if (entries.length === FILE_SEARCH_MAX_RESULTS) return { entries, truncated: true };
10415
+ entries.push({ name: dirent.name, path, kind: "file" });
10416
+ }
10417
+ }
10418
+ }
10419
+ return { entries, truncated: false };
10420
+ };
10421
+ var readWithin = async (file, limit) => {
10422
+ const info = await stat3(file.real);
10423
+ if (!info.isFile()) throw notFound(file.path);
10424
+ if (info.size > limit) return { oversize: info.size };
10425
+ const handle = await open2(file.real, constants2.O_RDONLY | constants2.O_NONBLOCK);
10426
+ try {
10427
+ const opened = await handle.stat();
10428
+ if (!opened.isFile()) throw notFound(file.path);
10429
+ if (opened.size > limit) return { oversize: opened.size };
10430
+ const bytes = await handle.readFile();
10431
+ return bytes.length > limit ? { oversize: bytes.length } : { bytes };
10432
+ } finally {
10433
+ await handle.close();
10434
+ }
10435
+ };
10436
+ var viewFile = async (root, path) => {
10437
+ const file = await resolveInside(root, path, false);
10438
+ const meta = { path: file.path, name: file.path.split("/").at(-1) ?? file.path, commit: null };
10439
+ const mediaType = imageMediaType(file.path);
10440
+ if (mediaType) {
10441
+ const info = await stat3(file.real);
10442
+ if (!info.isFile()) throw notFound(file.path);
10443
+ 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 };
10444
+ }
10445
+ const read = await readWithin(file, FILE_TEXT_MAX_BYTES);
10446
+ if ("oversize" in read)
10447
+ return { ...meta, size: read.oversize, kind: "too_large", limit: FILE_TEXT_MAX_BYTES };
10448
+ const content = decodeText(read.bytes);
10449
+ return content === null ? { ...meta, size: read.bytes.length, kind: "binary" } : { ...meta, size: read.bytes.length, kind: "text", content };
10450
+ };
10451
+ var readRawFile = async (root, path) => {
10452
+ const file = await resolveInside(root, path, false);
10453
+ const mediaType = rawMediaType(file.path);
10454
+ if (!mediaType) throw new FileError("unsupported", `not served raw: ${file.path}`);
10455
+ const read = await readWithin(file, FILE_RAW_MAX_BYTES);
10456
+ if ("oversize" in read) throw new FileError("unsupported", `too large to serve raw: ${file.path}`);
10457
+ return rawPayload(file.path, mediaType, read.bytes);
10458
+ };
10459
+ var openDiskDownload = async (root, path, signal) => {
10460
+ const file = await resolveInside(root, path, false);
10461
+ const handle = await open2(file.real, constants2.O_RDONLY | constants2.O_NONBLOCK);
10462
+ try {
10463
+ const opened = await handle.stat();
10464
+ if (!opened.isFile()) throw notFound(file.path);
10465
+ checkDownloadSize(file.path, opened.size);
10466
+ const stream = handle.createReadStream({ signal });
10467
+ return {
10468
+ size: opened.size,
10469
+ body: Readable3.toWeb(stream),
10470
+ close: () => stream.destroy()
10471
+ };
10472
+ } catch (error) {
10473
+ await handle.close();
10474
+ throw error;
10475
+ }
10476
+ };
10477
+
10478
+ // src/worker/files/download.ts
10479
+ var exactLength = (size) => {
10480
+ let seen = 0;
10481
+ return new TransformStream({
10482
+ transform(chunk, controller) {
10483
+ seen += chunk.byteLength;
10484
+ if (seen > size) throw new Error(`file grew past its ${size} bytes while downloading`);
10485
+ controller.enqueue(chunk);
10486
+ },
10487
+ flush() {
10488
+ if (seen !== size) throw new Error(`file ended at ${seen} of its ${size} bytes`);
10489
+ }
10490
+ });
10491
+ };
10492
+ var openSource = async (baseDir, branch, path, signal) => {
10493
+ if (branch.worktree !== null) return openDiskDownload(branch.worktree, path, signal);
10494
+ if (branch.commit === null)
10495
+ throw new FileError("branch_not_found", `branch ${branch.name} is empty`);
10496
+ return openCommitDownload(baseDir, branch.commit, path, signal);
10497
+ };
10498
+ var openFileDownload = async (baseDir, query3, signal) => {
10499
+ const branches = await discoverBranches(baseDir);
10500
+ const branch = branches.find((candidate) => candidate.name === query3.branch);
10501
+ if (!branch) throw new FileError("branch_not_found", `branch ${query3.branch} does not exist`);
10502
+ const source = await openSource(baseDir, branch, query3.path, signal);
10503
+ return { ...source, body: source.body.pipeThrough(exactLength(source.size)) };
10504
+ };
10505
+ var failureOf = (error) => {
10506
+ if (error instanceof FileError) return { ok: false, code: error.code, message: error.message };
10507
+ console.error("[files] opening a download failed", error);
10508
+ return { ok: false, code: "unavailable", message: "the worker could not read the file" };
10509
+ };
10510
+ var sendFileDownload = async (baseDir, command, client, signal) => {
10511
+ const bounded = AbortSignal.any([signal, AbortSignal.timeout(FILE_DOWNLOAD_TIMEOUT_MS)]);
10512
+ let source;
10513
+ try {
10514
+ source = await openFileDownload(baseDir, command.query, bounded);
10515
+ } catch (error) {
10516
+ await client.rejectDownload(command.requestId, failureOf(error));
10517
+ return;
10518
+ }
10519
+ try {
10520
+ await client.uploadDownload(command.requestId, source.size, source.body, bounded);
10521
+ } finally {
10522
+ source.close();
10523
+ }
10524
+ };
10525
+
10526
+ // src/worker/files/handler.ts
10527
+ var DEFAULT_CONCURRENCY = 4;
10528
+ var readerFor = (baseDir, branch) => {
10529
+ const { worktree, commit } = branch;
10530
+ if (worktree !== null)
10531
+ return {
10532
+ list: (path) => listDirectory(worktree, path),
10533
+ search: (query3) => searchFiles(worktree, query3),
10534
+ view: (path) => viewFile(worktree, path),
10535
+ raw: (path) => readRawFile(worktree, path)
10536
+ };
10537
+ if (commit === null) throw new FileError("branch_not_found", `branch ${branch.name} is empty`);
10538
+ return {
10539
+ list: (path) => listCommitDirectory(baseDir, commit, path),
10540
+ search: (query3) => searchCommit(baseDir, commit, query3),
10541
+ view: (path) => viewCommitFile(baseDir, commit, path),
10542
+ raw: (path) => readRawCommitFile(baseDir, commit, path)
10543
+ };
10544
+ };
10545
+ var run2 = async (baseDir, query3) => {
10546
+ const branches = await discoverBranches(baseDir);
10547
+ if (query3.op === "branches") return { ok: true, data: branches };
10548
+ const branch = branches.find((candidate) => candidate.name === query3.branch);
10549
+ if (!branch) throw new FileError("branch_not_found", `branch ${query3.branch} does not exist`);
10550
+ const reader = readerFor(baseDir, branch);
10551
+ if (query3.op === "list") return { ok: true, data: await reader.list(query3.path) };
10552
+ if (query3.op === "search") return { ok: true, data: await reader.search(query3.query) };
10553
+ if (query3.op === "raw") return { ok: true, data: await reader.raw(query3.path) };
10554
+ return { ok: true, data: await reader.view(query3.path) };
10555
+ };
10556
+ var handleFileQuery = async (baseDir, query3) => {
10557
+ try {
10558
+ return await run2(baseDir, query3);
10559
+ } catch (error) {
10560
+ if (error instanceof FileError) return { ok: false, code: error.code, message: error.message };
10561
+ console.error(`[files] ${query3.op} query failed`, error);
10562
+ return { ok: false, code: "unavailable", message: "the worker could not complete the read" };
10563
+ }
10564
+ };
10565
+ var createFileQueryRunner = (baseDir, limit = DEFAULT_CONCURRENCY) => {
10566
+ let active = 0;
10567
+ const waiting = [];
10568
+ const acquire = async () => {
10569
+ if (active < limit) {
10570
+ active += 1;
10571
+ return;
10572
+ }
10573
+ await new Promise((resolve4) => waiting.push(resolve4));
10574
+ };
10575
+ const release = () => {
10576
+ const next = waiting.shift();
10577
+ if (next) next();
10578
+ else active -= 1;
10579
+ };
10580
+ return async (query3) => {
10581
+ await acquire();
10582
+ try {
10583
+ return await handleFileQuery(baseDir, query3);
10584
+ } finally {
10585
+ release();
10586
+ }
10587
+ };
9661
10588
  };
9662
10589
 
9663
10590
  // src/worker/watchdog.ts
@@ -9718,13 +10645,13 @@ var createLiveness = (deps) => {
9718
10645
 
9719
10646
  // src/worker/service-supervisor.ts
9720
10647
  import { mkdirSync as mkdirSync4 } from "node:fs";
9721
- import { stat as stat3 } from "node:fs/promises";
9722
- import { join as join14 } from "node:path";
10648
+ import { stat as stat4 } from "node:fs/promises";
10649
+ import { join as join17 } from "node:path";
9723
10650
 
9724
10651
  // src/worker/service-process.ts
9725
- import { spawn as spawn2 } from "node:child_process";
10652
+ import { spawn as spawn3 } from "node:child_process";
9726
10653
  import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
9727
- import { dirname as dirname4, join as join13 } from "node:path";
10654
+ import { dirname as dirname4, join as join16 } from "node:path";
9728
10655
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9729
10656
  var START_TIMEOUT_MS = 1e4;
9730
10657
  var isWrapperMessage = (message) => {
@@ -9735,14 +10662,14 @@ var isWrapperMessage = (message) => {
9735
10662
  };
9736
10663
  var binPath = () => {
9737
10664
  const here = dirname4(fileURLToPath2(import.meta.url));
9738
- const devShim = join13(here, "..", "..", "bin", "baton.mjs");
10665
+ const devShim = join16(here, "..", "..", "bin", "baton.mjs");
9739
10666
  return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
9740
10667
  };
9741
10668
  var startServiceProcess = (input) => {
9742
10669
  const logFd = openSync(input.logPath, "a");
9743
10670
  let wrapper;
9744
10671
  try {
9745
- wrapper = spawn2(process.execPath, [binPath(), "service", "child"], {
10672
+ wrapper = spawn3(process.execPath, [binPath(), "service", "child"], {
9746
10673
  detached: false,
9747
10674
  stdio: ["ignore", logFd, logFd, "ipc"],
9748
10675
  env: process.env
@@ -9822,15 +10749,15 @@ var createServiceSupervisor = (deps) => {
9822
10749
  };
9823
10750
  const runOne = async (requestId, cwd, name, argv, publicUrl, note) => {
9824
10751
  try {
9825
- if (!(await stat3(cwd)).isDirectory())
10752
+ if (!(await stat4(cwd)).isDirectory())
9826
10753
  return report({ requestId, ok: false, error: `not a directory: ${cwd}`, status: 400 });
9827
10754
  } catch (error) {
9828
10755
  const code = error.code;
9829
10756
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
9830
10757
  return report({ requestId, ok: false, error: `directory not found: ${cwd}`, status: 400 });
9831
10758
  }
9832
- const logDir = join14(workerRuntimeDir(cfg), "services");
9833
- const logPath = join14(logDir, `${name}.log`);
10759
+ const logDir = join17(workerRuntimeDir(cfg), "services");
10760
+ const logPath = join17(logDir, `${name}.log`);
9834
10761
  mkdirSync4(logDir, { recursive: true, mode: 448 });
9835
10762
  if (entries.has(name))
9836
10763
  return report({
@@ -9870,7 +10797,7 @@ var createServiceSupervisor = (deps) => {
9870
10797
  log(`service ${name} started in ${cwd}`);
9871
10798
  await report({ requestId, ok: true, service: service2, logPath });
9872
10799
  };
9873
- const run2 = async (requestId, cwd, name, argv, publicUrl, note) => {
10800
+ const run4 = async (requestId, cwd, name, argv, publicUrl, note) => {
9874
10801
  try {
9875
10802
  await runOne(requestId, cwd, name, argv, publicUrl, note);
9876
10803
  } catch (error) {
@@ -9888,15 +10815,15 @@ var createServiceSupervisor = (deps) => {
9888
10815
  await report({ requestId, ok: true });
9889
10816
  };
9890
10817
  const killAll = () => entries.forEach(stopEntry);
9891
- return { run: run2, stop, report, killAll };
10818
+ return { run: run4, stop, report, killAll };
9892
10819
  };
9893
10820
 
9894
10821
  // src/worker/session-supervisor.ts
9895
- import { spawn as spawn3 } from "node:child_process";
10822
+ import { spawn as spawn4 } from "node:child_process";
9896
10823
  import { randomUUID as randomUUID2 } from "node:crypto";
9897
10824
  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";
10825
+ import { mkdir as mkdir6, rm as rm3, stat as stat5 } from "node:fs/promises";
10826
+ import { dirname as dirname5, join as join21 } from "node:path";
9900
10827
  import { fileURLToPath as fileURLToPath3 } from "node:url";
9901
10828
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
9902
10829
 
@@ -9990,7 +10917,7 @@ var generateTitle = async (input) => {
9990
10917
  // src/session/runner/title-codex.ts
9991
10918
  import { mkdtemp, rm } from "node:fs/promises";
9992
10919
  import { tmpdir as tmpdir3 } from "node:os";
9993
- import { join as join15 } from "node:path";
10920
+ import { join as join18 } from "node:path";
9994
10921
  import { Codex as Codex2 } from "@openai/codex-sdk";
9995
10922
  var generateTitleWithCodex = async (input) => {
9996
10923
  const { userText, assistantText } = input;
@@ -9999,7 +10926,7 @@ var generateTitleWithCodex = async (input) => {
9999
10926
  const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
10000
10927
  let cwd = "";
10001
10928
  try {
10002
- cwd = await mkdtemp(join15(tmpdir3(), "baton-title-"));
10929
+ cwd = await mkdtemp(join18(tmpdir3(), "baton-title-"));
10003
10930
  const client = input.client ?? new Codex2();
10004
10931
  const thread = client.startThread({
10005
10932
  workingDirectory: cwd,
@@ -10025,19 +10952,79 @@ var generateTitleWithCodex = async (input) => {
10025
10952
  }
10026
10953
  };
10027
10954
 
10955
+ // src/session/runner/title-pi.ts
10956
+ import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
10957
+ import { tmpdir as tmpdir4 } from "node:os";
10958
+ import { join as join19 } from "node:path";
10959
+ var generateTitleWithPi = async (input) => {
10960
+ const { userText, assistantText } = input;
10961
+ if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
10962
+ let cwd = "";
10963
+ let timedOut = false;
10964
+ let timer;
10965
+ try {
10966
+ cwd = await mkdtemp2(join19(tmpdir4(), "baton-title-"));
10967
+ const pi = await import("@earendil-works/pi-coding-agent");
10968
+ const agentDir = pi.getAgentDir();
10969
+ const resourceLoader = new pi.DefaultResourceLoader({
10970
+ cwd,
10971
+ agentDir,
10972
+ noExtensions: true,
10973
+ noSkills: true,
10974
+ noPromptTemplates: true,
10975
+ noThemes: true,
10976
+ noContextFiles: true,
10977
+ systemPrompt: "You title chat sessions."
10978
+ });
10979
+ await resourceLoader.reload();
10980
+ const { session: session2 } = await pi.createAgentSession({
10981
+ cwd,
10982
+ agentDir,
10983
+ resourceLoader,
10984
+ sessionManager: pi.SessionManager.inMemory(cwd),
10985
+ noTools: "all"
10986
+ });
10987
+ let error;
10988
+ session2.subscribe((event) => {
10989
+ if (event.type === "message_end" && event.message.role === "assistant")
10990
+ error = event.message.stopReason === "error" ? event.message.errorMessage : void 0;
10991
+ });
10992
+ timer = setTimeout(() => {
10993
+ timedOut = true;
10994
+ void session2.abort().catch((e) => console.error(`[title] pi abort: ${String(e)}`));
10995
+ }, TITLE_TIMEOUT_MS);
10996
+ try {
10997
+ await session2.prompt(buildTitlePrompt(userText, assistantText));
10998
+ if (timedOut) return { kind: "error", reason: `timeout after ${TITLE_TIMEOUT_MS / 1e3}s` };
10999
+ if (error) return { kind: "error", reason: error.slice(0, 300) };
11000
+ return titleOutcome(session2.getLastAssistantText() ?? "");
11001
+ } finally {
11002
+ session2.dispose();
11003
+ }
11004
+ } catch (error) {
11005
+ return { kind: "error", reason: String(error).slice(0, 300) };
11006
+ } finally {
11007
+ clearTimeout(timer);
11008
+ if (cwd)
11009
+ await rm2(cwd, { recursive: true, force: true }).catch(
11010
+ (e) => console.error(`[title] cleanup ${cwd}: ${String(e)}`)
11011
+ );
11012
+ }
11013
+ };
11014
+
10028
11015
  // 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);
11016
+ import { execFile as execFile5 } from "node:child_process";
11017
+ import { readFile as readFile3, rename, writeFile as writeFile4 } from "node:fs/promises";
11018
+ import { join as join20 } from "node:path";
11019
+ import { promisify as promisify5 } from "node:util";
11020
+ var run3 = promisify5(execFile5);
10034
11021
  var RUNNER_RECORD = ".baton-runner.json";
10035
- var recordPath = (cwd) => join16(cwd, RUNNER_RECORD);
11022
+ var recordPath = (cwd) => join20(cwd, RUNNER_RECORD);
10036
11023
  var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
10037
11024
  var readRunnerRecord = async (cwd) => {
10038
11025
  let raw;
10039
11026
  try {
10040
- raw = await readFile2(recordPath(cwd), "utf8");
11027
+ raw = await readFile3(recordPath(cwd), "utf8");
10041
11028
  } catch (error) {
10042
11029
  if (codeOf(error) === "ENOENT") return null;
10043
11030
  throw error;
@@ -10050,7 +11037,7 @@ var readRunnerRecord = async (cwd) => {
10050
11037
  var writeRunnerRecord = async (cwd, record) => {
10051
11038
  const path = recordPath(cwd);
10052
11039
  const temporary = `${path}.${record.runnerToken}.tmp`;
10053
- await writeFile3(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
11040
+ await writeFile4(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
10054
11041
  await rename(temporary, path);
10055
11042
  };
10056
11043
  var groupExists = (pgid) => {
@@ -10075,7 +11062,7 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
10075
11062
  if (!groupExists(record.pgid)) return;
10076
11063
  let stdout;
10077
11064
  try {
10078
- const result = await run("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
11065
+ const result = await run3("ps", ["-ww", "-p", String(record.pid), "-o", "pgid=", "-o", "args="]);
10079
11066
  stdout = result.stdout;
10080
11067
  } catch (error) {
10081
11068
  if (!groupExists(record.pgid)) return;
@@ -10100,7 +11087,7 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
10100
11087
  // src/worker/session-supervisor.ts
10101
11088
  var binPath2 = () => {
10102
11089
  const here = dirname5(fileURLToPath3(import.meta.url));
10103
- const devShim = join17(here, "..", "..", "bin", "baton.mjs");
11090
+ const devShim = join21(here, "..", "..", "bin", "baton.mjs");
10104
11091
  return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
10105
11092
  };
10106
11093
  var createSessionSupervisor = (deps) => {
@@ -10137,7 +11124,7 @@ var createSessionSupervisor = (deps) => {
10137
11124
  if (children.has(sessionId)) return;
10138
11125
  await stopRecord(sessionId);
10139
11126
  const runnerToken = randomUUID2();
10140
- const child = spawn3(
11127
+ const child = spawn4(
10141
11128
  process.execPath,
10142
11129
  [binPath2(), "session", "run", String(sessionId), "--runner-token", runnerToken],
10143
11130
  {
@@ -10209,7 +11196,7 @@ var createSessionSupervisor = (deps) => {
10209
11196
  return;
10210
11197
  if (wasCanceled()) return log(`session #${sessionId} start canceled before materializing`);
10211
11198
  const directory2 = session2.cwd ?? cwd;
10212
- if (!(await stat4(directory2)).isDirectory()) throw new Error(`not a directory: ${directory2}`);
11199
+ if (!(await stat5(directory2)).isDirectory()) throw new Error(`not a directory: ${directory2}`);
10213
11200
  if (wasCanceled()) return;
10214
11201
  if (!session2.agentSessionId || !session2.cwd) {
10215
11202
  const id = randomUUID2();
@@ -10217,7 +11204,7 @@ var createSessionSupervisor = (deps) => {
10217
11204
  await client.sessions.materialize(sessionId, { agentSessionId, cwd: directory2 }, cfg.apiToken);
10218
11205
  log(`materialized session #${sessionId} (${name}) \u2192 ${directory2}`);
10219
11206
  }
10220
- await mkdir5(sessionRuntimeDir(cfg, sessionId), { recursive: true, mode: 448 });
11207
+ await mkdir6(sessionRuntimeDir(cfg, sessionId), { recursive: true, mode: 448 });
10221
11208
  if (hasTerminal(sessionId))
10222
11209
  return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
10223
11210
  if (wasCanceled()) return;
@@ -10249,7 +11236,7 @@ var createSessionSupervisor = (deps) => {
10249
11236
  return enqueue(sessionId, async () => {
10250
11237
  const entry = children.get(sessionId);
10251
11238
  await stopRecord(sessionId);
10252
- await rm2(sessionRuntimeDir(cfg, sessionId), { recursive: true, force: true });
11239
+ await rm3(sessionRuntimeDir(cfg, sessionId), { recursive: true, force: true });
10253
11240
  if (children.get(sessionId) === entry) children.delete(sessionId);
10254
11241
  });
10255
11242
  };
@@ -10258,15 +11245,12 @@ var createSessionSupervisor = (deps) => {
10258
11245
  const events = await client.sessions.listEvents(sessionId);
10259
11246
  const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
10260
11247
  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
- });
11248
+ const titlers = {
11249
+ "claude-code": (e) => generateTitle({ cwd: cwd2, ...e, queryFn: query2 }),
11250
+ codex: generateTitleWithCodex,
11251
+ pi: generateTitleWithPi
11252
+ };
11253
+ const outcome = await titlers[session2.agentKind](exchange);
10270
11254
  if (outcome.kind === "error") return log(`title #${sessionId} failed: ${outcome.reason}`);
10271
11255
  if (outcome.kind === "declined")
10272
11256
  return log(`title #${sessionId}: not enough to title yet, skipping`);
@@ -10332,7 +11316,7 @@ var createSessionSupervisor = (deps) => {
10332
11316
 
10333
11317
  // src/worker/terminal-manager.ts
10334
11318
  import { statSync as statSync2 } from "node:fs";
10335
- import { spawn as spawn4 } from "node-pty";
11319
+ import { spawn as spawn5 } from "node-pty";
10336
11320
 
10337
11321
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
10338
11322
  var import_stream2 = __toESM(require_stream(), 1);
@@ -10347,8 +11331,9 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
10347
11331
  // src/worker/pty.ts
10348
11332
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
10349
11333
  import { createRequire as createModuleRequire } from "node:module";
10350
- import { homedir as homedir4 } from "node:os";
10351
- import { join as join18 } from "node:path";
11334
+ import { homedir as homedir5 } from "node:os";
11335
+ import { dirname as dirname6, join as join22 } from "node:path";
11336
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
10352
11337
  var MAX_TERMINALS = 10;
10353
11338
  var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
10354
11339
  file: env.BATON_CLAUDE_BIN?.trim() || "claude",
@@ -10363,13 +11348,25 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
10363
11348
  args: [moduleRequire.resolve("@openai/codex/bin/codex.js"), ...args]
10364
11349
  };
10365
11350
  };
11351
+ var piPtyCommand = (agentSessionId, env = process.env) => {
11352
+ const args = ["--session-id", agentSessionId];
11353
+ const override = env.BATON_PI_BIN?.trim();
11354
+ if (override) return { file: override, args };
11355
+ const entry = fileURLToPath4(import.meta.resolve("@earendil-works/pi-coding-agent"));
11356
+ return { file: process.execPath, args: [join22(dirname6(entry), "bundle", "cli.js"), ...args] };
11357
+ };
10366
11358
  var hasSessionJsonl = (agentSessionId) => {
10367
- const root = join18(homedir4(), ".claude", "projects");
11359
+ const root = join22(homedir5(), ".claude", "projects");
10368
11360
  if (!existsSync8(root)) return false;
10369
11361
  for (const dir of readdirSync2(root))
10370
- if (existsSync8(join18(root, dir, `${agentSessionId}.jsonl`))) return true;
11362
+ if (existsSync8(join22(root, dir, `${agentSessionId}.jsonl`))) return true;
10371
11363
  return false;
10372
11364
  };
11365
+ var ptyCommand = (kind, agentSessionId) => ({
11366
+ "claude-code": () => claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId)),
11367
+ codex: () => codexPtyCommand(agentSessionId),
11368
+ pi: () => piPtyCommand(agentSessionId)
11369
+ })[kind]();
10373
11370
  var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
10374
11371
  var isPositiveDim = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10375
11372
 
@@ -10377,14 +11374,14 @@ var isPositiveDim = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10377
11374
  var createTerminalManager = (deps) => {
10378
11375
  const { cfg, log, hasChild } = deps;
10379
11376
  const terminals = /* @__PURE__ */ new Map();
10380
- const open2 = (sessionId, agentSessionId, cwd) => {
11377
+ const open3 = (sessionId, agentSessionId, cwd) => {
10381
11378
  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
11379
  if (why) {
10383
11380
  log(`terminal #${sessionId}: ${why} \u2014 refusing`);
10384
11381
  return;
10385
11382
  }
10386
- const command = cfg.agentKind === "codex" ? codexPtyCommand(agentSessionId) : claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId));
10387
- const term = spawn4(command.file, command.args, {
11383
+ const command = ptyCommand(cfg.agentKind, agentSessionId);
11384
+ const term = spawn5(command.file, command.args, {
10388
11385
  name: "xterm-color",
10389
11386
  cols: 80,
10390
11387
  rows: 24,
@@ -10435,7 +11432,7 @@ var createTerminalManager = (deps) => {
10435
11432
  }
10436
11433
  };
10437
11434
  return {
10438
- open: open2,
11435
+ open: open3,
10439
11436
  close,
10440
11437
  has: (sessionId) => terminals.has(sessionId),
10441
11438
  killAll: () => {
@@ -10473,6 +11470,8 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10473
11470
  log,
10474
11471
  hasChild: (id) => supervisor.has(id)
10475
11472
  });
11473
+ const files = createFileQueryRunner(cwd);
11474
+ const downloads = new AbortController();
10476
11475
  const services = createServiceSupervisor({
10477
11476
  sendReport: (report) => client.services.report(report),
10478
11477
  cfg,
@@ -10528,6 +11527,12 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10528
11527
  void services.run(cmd.requestId, cmd.cwd, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
10529
11528
  else if (cmd.cmd === "service.stop")
10530
11529
  void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
11530
+ else if (cmd.cmd === "files.query")
11531
+ void files(cmd.query).then((reply) => client.files.report({ requestId: cmd.requestId, ...reply })).catch((err) => log(`files query failed: ${String(err)}`));
11532
+ else if (cmd.cmd === "files.download")
11533
+ void sendFileDownload(cwd, cmd, client.files, downloads.signal).catch(
11534
+ (err) => log(`files download failed: ${String(err)}`)
11535
+ );
10531
11536
  } catch (error) {
10532
11537
  log(`invalid command: ${String(error)}`);
10533
11538
  }
@@ -10541,6 +11546,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10541
11546
  signal.addEventListener("abort", () => resolve4(), { once: true });
10542
11547
  });
10543
11548
  es.close();
11549
+ downloads.abort();
10544
11550
  liveness.stop();
10545
11551
  services.killAll();
10546
11552
  await supervisor.killAll();
@@ -10551,16 +11557,16 @@ var runWorkerDaemon = async (cfg, client, signal) => {
10551
11557
  // src/worker/machine-id.ts
10552
11558
  import { randomUUID as randomUUID3 } from "node:crypto";
10553
11559
  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");
11560
+ import { homedir as homedir6 } from "node:os";
11561
+ import { dirname as dirname7, join as join23 } from "node:path";
11562
+ var machineIdPath = (env = process.env) => join23(env.XDG_DATA_HOME ?? join23(env.HOME ?? homedir6(), ".local/share"), "baton", "machine-id");
10557
11563
  var readOrCreateMachineId = (path = machineIdPath()) => {
10558
11564
  if (existsSync9(path)) {
10559
11565
  const s = readFileSync6(path, "utf8").trim();
10560
11566
  if (s) return s;
10561
11567
  }
10562
11568
  const id = randomUUID3();
10563
- mkdirSync5(dirname6(path), { recursive: true });
11569
+ mkdirSync5(dirname7(path), { recursive: true });
10564
11570
  writeFileSync5(path, `${id}
10565
11571
  `, "utf8");
10566
11572
  return id;
@@ -10588,22 +11594,28 @@ var workerCreateCommand = defineCommand({
10588
11594
  args: {
10589
11595
  ...projectArg,
10590
11596
  ...common,
10591
- template: { type: "string", required: true, description: "mini-local" },
11597
+ template: {
11598
+ type: "string",
11599
+ required: true,
11600
+ description: "mini-glm-codeplan | mini-dashscope-codeplan | mini-codex"
11601
+ },
10592
11602
  name: { type: "string", required: true, description: "worker name" },
10593
- agentKind: { type: "string", default: "claude-code", description: "claude-code or codex" }
11603
+ agentKind: { type: "string", description: AGENT_KINDS.join(", ") }
10594
11604
  },
10595
11605
  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");
11606
+ if (!isWorkerTemplateId(args.template)) throw new Error("invalid worker template");
11607
+ const agentKind = args.agentKind ?? workerTemplateAgentKinds(args.template)[0];
11608
+ if (!isAgentKind(agentKind)) throw new Error("invalid agentKind");
11609
+ if (!workerTemplateAgentKinds(args.template).includes(agentKind))
11610
+ throw new Error("agentKind is not supported by this template");
10599
11611
  const name = args.name.trim();
10600
11612
  if (!name) throw new Error("worker name required");
10601
11613
  const creation = await clientFor(args).workerCreations.create(
10602
11614
  positiveId(resolveProjectId(args)),
10603
11615
  {
10604
- templateId: "mini-local",
11616
+ templateId: args.template,
10605
11617
  name,
10606
- agentKind: args.agentKind
11618
+ agentKind
10607
11619
  }
10608
11620
  );
10609
11621
  console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
@@ -10644,8 +11656,8 @@ var resolveWorker = async (client, projectId, handle) => {
10644
11656
  };
10645
11657
  var parseAgentKind = (value) => {
10646
11658
  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)`);
11659
+ if (isAgentKind(raw)) return raw;
11660
+ throw new Error(`invalid agent kind "${raw}" (expected ${AGENT_KINDS.join(", ")})`);
10649
11661
  };
10650
11662
  var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
10651
11663
  const out = await client.workers.register({
@@ -10690,7 +11702,7 @@ var worker = defineCommand({
10690
11702
  },
10691
11703
  agentKind: {
10692
11704
  type: "string",
10693
- description: "agent engine for new sessions: claude-code or codex"
11705
+ description: `agent engine for new sessions: ${AGENT_KINDS.join(", ")}`
10694
11706
  },
10695
11707
  ...common
10696
11708
  },
@@ -10927,6 +11939,7 @@ var main = defineCommand({
10927
11939
  service,
10928
11940
  artifact,
10929
11941
  loop,
11942
+ notify,
10930
11943
  relay,
10931
11944
  channel,
10932
11945
  workspace,