@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.
- package/dist/baton.mjs +1223 -210
- 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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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(
|
|
3731
|
+
function splitByCase(str4, separators) {
|
|
3728
3732
|
const splitters = separators ?? STR_SPLITTERS;
|
|
3729
3733
|
const parts = [];
|
|
3730
|
-
if (!
|
|
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
|
|
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(
|
|
3766
|
-
return
|
|
3769
|
+
function upperFirst(str4) {
|
|
3770
|
+
return str4 ? str4[0].toUpperCase() + str4.slice(1) : "";
|
|
3767
3771
|
}
|
|
3768
|
-
function lowerFirst(
|
|
3769
|
-
return
|
|
3772
|
+
function lowerFirst(str4) {
|
|
3773
|
+
return str4 ? str4[0].toLowerCase() + str4.slice(1) : "";
|
|
3770
3774
|
}
|
|
3771
|
-
function pascalCase(
|
|
3772
|
-
return
|
|
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(
|
|
3775
|
-
return lowerFirst(pascalCase(
|
|
3778
|
+
function camelCase(str4, opts) {
|
|
3779
|
+
return lowerFirst(pascalCase(str4 || "", opts));
|
|
3776
3780
|
}
|
|
3777
|
-
function kebabCase(
|
|
3778
|
-
return
|
|
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(
|
|
3781
|
-
return kebabCase(
|
|
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
|
|
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 && (
|
|
6269
|
-
} while (
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
7483
|
-
...
|
|
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
|
|
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 =
|
|
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
|
|
7711
|
+
await mkdir3(config.projectsRoot, { recursive: true, mode: 448 });
|
|
7526
7712
|
const root = await realpath(config.projectsRoot);
|
|
7527
|
-
await directory(
|
|
7528
|
-
await
|
|
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
|
|
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
|
|
7545
|
-
await
|
|
7546
|
-
await
|
|
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(
|
|
7560
|
-
await directory(
|
|
7561
|
-
await
|
|
7562
|
-
await
|
|
7563
|
-
await
|
|
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
|
|
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
|
|
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 [
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
8555
|
+
import { join as join7 } from "node:path";
|
|
8365
8556
|
var findTranscriptPath = (agentSessionId) => {
|
|
8366
|
-
const root =
|
|
8557
|
+
const root = join7(homedir2(), ".claude", "projects");
|
|
8367
8558
|
try {
|
|
8368
8559
|
for (const dir of readdirSync(root)) {
|
|
8369
|
-
const candidate =
|
|
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
|
|
8448
|
-
import { join as
|
|
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
|
|
8453
|
-
import { join as
|
|
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
|
|
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(
|
|
8484
|
-
paths.push(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: `${
|
|
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([
|
|
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
|
|
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([
|
|
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
|
|
8763
|
-
var
|
|
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 (!
|
|
8767
|
-
return msg.content.filter(
|
|
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) =>
|
|
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 (!
|
|
8779
|
-
return
|
|
9182
|
+
if (!isRecord4(input)) return "";
|
|
9183
|
+
return str3(input.command) ?? str3(input.cmd) ?? "";
|
|
8780
9184
|
};
|
|
8781
9185
|
var pathOf = (input) => {
|
|
8782
|
-
if (!
|
|
8783
|
-
return
|
|
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 (!
|
|
8788
|
-
return input.todos.filter(
|
|
8789
|
-
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 =
|
|
8799
|
-
const name =
|
|
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:
|
|
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 =
|
|
9258
|
+
const subtype = str3(rec.subtype) ?? "unknown";
|
|
8855
9259
|
const isError = rec.is_error === true || subtype !== "success";
|
|
8856
|
-
const
|
|
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:
|
|
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:
|
|
8882
|
-
model:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
9369
|
+
const directory2 = join12(
|
|
8966
9370
|
config.runtimeDir,
|
|
8967
9371
|
"attachments",
|
|
8968
9372
|
`message-${execution.message.id}`,
|
|
8969
9373
|
"pasted"
|
|
8970
9374
|
);
|
|
8971
|
-
await
|
|
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 =
|
|
8977
|
-
await
|
|
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:
|
|
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
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
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 === "
|
|
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
|
|
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 =
|
|
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
|
|
9240
|
-
import { join as
|
|
9241
|
-
var workerRuntimeDir = (owner, env = process.env) =>
|
|
9242
|
-
env.XDG_DATA_HOME ??
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
9642
|
-
|
|
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,
|
|
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
|
|
9722
|
-
import { join as
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
9833
|
-
const logPath =
|
|
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
|
|
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:
|
|
10818
|
+
return { run: run4, stop, report, killAll };
|
|
9892
10819
|
};
|
|
9893
10820
|
|
|
9894
10821
|
// src/worker/session-supervisor.ts
|
|
9895
|
-
import { spawn as
|
|
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
|
|
9899
|
-
import { dirname as dirname5, join as
|
|
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
|
|
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(
|
|
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
|
|
10030
|
-
import { readFile as
|
|
10031
|
-
import { join as
|
|
10032
|
-
import { promisify as
|
|
10033
|
-
var
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
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
|
|
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
|
|
10351
|
-
import { join as
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
10387
|
-
const term =
|
|
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:
|
|
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
|
|
10555
|
-
import { dirname as
|
|
10556
|
-
var machineIdPath = (env = process.env) =>
|
|
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(
|
|
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: {
|
|
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",
|
|
11603
|
+
agentKind: { type: "string", description: AGENT_KINDS.join(", ") }
|
|
10594
11604
|
},
|
|
10595
11605
|
run: async ({ args }) => {
|
|
10596
|
-
if (args.template
|
|
10597
|
-
|
|
10598
|
-
|
|
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:
|
|
11616
|
+
templateId: args.template,
|
|
10605
11617
|
name,
|
|
10606
|
-
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
|
|
10648
|
-
throw new Error(`invalid agent kind "${raw}" (expected
|
|
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:
|
|
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,
|