@herdingchats/friday 0.1.563 → 0.1.569
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/herd.mjs +246 -91
- package/package.json +2 -2
package/herd.mjs
CHANGED
|
@@ -18,7 +18,7 @@ var __export = (target, all) => {
|
|
|
18
18
|
var define_HERD_PACKAGE_default;
|
|
19
19
|
var init_define_HERD_PACKAGE = __esm({
|
|
20
20
|
"<define:HERD_PACKAGE>"() {
|
|
21
|
-
define_HERD_PACKAGE_default = { version: "0.1.
|
|
21
|
+
define_HERD_PACKAGE_default = { version: "0.1.569", gitHead: "ac28f0e97b92fbe2178387595a511d35c3ae4953" };
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -1319,6 +1319,19 @@ var init_events = __esm({
|
|
|
1319
1319
|
}),
|
|
1320
1320
|
event("terminal.exited", { terminalId: nonEmpty6, exitCode: z12.number().int().nullable() }),
|
|
1321
1321
|
event("terminal.notice_failed", { terminalId: nonEmpty6, reason: nonEmpty6 }),
|
|
1322
|
+
/**
|
|
1323
|
+
* The agent's start didn't check in with `herd inbox` within its window, so it looks like it
|
|
1324
|
+
* needs someone at its terminal. `agent` is its CLI's name, and `detail` what the screen showed,
|
|
1325
|
+
* when herd recognized it.
|
|
1326
|
+
*/
|
|
1327
|
+
event("terminal.needs_attention", {
|
|
1328
|
+
terminalId: nonEmpty6,
|
|
1329
|
+
agent: nonEmpty6,
|
|
1330
|
+
machineName: nonEmpty6,
|
|
1331
|
+
detail: z12.string().max(200).optional()
|
|
1332
|
+
}),
|
|
1333
|
+
/** A start that needed attention checked in after all. */
|
|
1334
|
+
event("terminal.checked_in", { terminalId: nonEmpty6 }),
|
|
1322
1335
|
/** The daemon closed one of Claude Code's own dialogs that it has recorded, named by its title. */
|
|
1323
1336
|
event("terminal.dialog_closed", { terminalId: nonEmpty6, dialog: nonEmpty6 }),
|
|
1324
1337
|
// A subscribed event that didn't wake the conversation, with the reason. One that woke it is a
|
|
@@ -1466,6 +1479,9 @@ var init_conversation_state = __esm({
|
|
|
1466
1479
|
"author_not_linked",
|
|
1467
1480
|
// The only agents that could take it showed their own sign-in, so a person has to sign in.
|
|
1468
1481
|
"agent_signed_out",
|
|
1482
|
+
// Its agent started and never checked in with `herd inbox`, so it looks like it needs someone
|
|
1483
|
+
// at its terminal.
|
|
1484
|
+
"agent_needs_attention",
|
|
1469
1485
|
// The only machines that could take it have a folder that isn't there, such as a drive not
|
|
1470
1486
|
// yet mounted.
|
|
1471
1487
|
"folder_unavailable",
|
|
@@ -1568,6 +1584,7 @@ var init_conversation_state = __esm({
|
|
|
1568
1584
|
author_not_linked: "the author's account isn't linked",
|
|
1569
1585
|
delegate_waiting: "a delegate is waiting on someone",
|
|
1570
1586
|
agent_signed_out: "its agent isn't signed in on its machine",
|
|
1587
|
+
agent_needs_attention: "its agent looks like it needs someone at its terminal",
|
|
1571
1588
|
folder_unavailable: "its machine's folder isn't there",
|
|
1572
1589
|
no_suitable_worker: "no worker may take it under its configuration",
|
|
1573
1590
|
notice_failed: "a notice didn't reach the agent's terminal",
|
|
@@ -3156,6 +3173,7 @@ import { spawn as spawn6 } from "node:child_process";
|
|
|
3156
3173
|
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
3157
3174
|
import { homedir as homedir4 } from "node:os";
|
|
3158
3175
|
import { dirname as dirname10, join as join14 } from "node:path";
|
|
3176
|
+
import { createInterface } from "node:readline/promises";
|
|
3159
3177
|
import { z as z22 } from "zod";
|
|
3160
3178
|
|
|
3161
3179
|
// src/auth.ts
|
|
@@ -3384,7 +3402,7 @@ var HERD_FLAGS = {
|
|
|
3384
3402
|
persona: []
|
|
3385
3403
|
};
|
|
3386
3404
|
var FRIDAY_FLAGS = {
|
|
3387
|
-
start: ["--agents"],
|
|
3405
|
+
start: ["--agents", "--config"],
|
|
3388
3406
|
stop: [],
|
|
3389
3407
|
status: [],
|
|
3390
3408
|
logs: ["--follow", "-f", "-n"]
|
|
@@ -4472,6 +4490,8 @@ var localRequestSchema = z19.object({
|
|
|
4472
4490
|
"repo",
|
|
4473
4491
|
"activate"
|
|
4474
4492
|
]),
|
|
4493
|
+
// The key of the agent's start, from `HERD_START`, which `herd inbox` proves the start with.
|
|
4494
|
+
start: z19.string().min(1).max(100).optional(),
|
|
4475
4495
|
// `herd repo`: the repo it added to the conversation's folder, for the daemon to record.
|
|
4476
4496
|
repo: z19.object({
|
|
4477
4497
|
remote: z19.string().min(1).max(500),
|
|
@@ -4820,6 +4840,50 @@ async function permissionHook(raw, options = {}) {
|
|
|
4820
4840
|
}
|
|
4821
4841
|
}
|
|
4822
4842
|
|
|
4843
|
+
// src/timing.ts
|
|
4844
|
+
init_define_HERD_PACKAGE();
|
|
4845
|
+
var CHECK_IN_MS = 15e3;
|
|
4846
|
+
var VARIABLES = {
|
|
4847
|
+
settleMs: "HERD_SETTLE_MS",
|
|
4848
|
+
pollMs: "HERD_POLL_MS",
|
|
4849
|
+
confirmMs: "HERD_CONFIRM_MS",
|
|
4850
|
+
pasteMs: "HERD_PASTE_MS",
|
|
4851
|
+
renewMs: "HERD_RENEW_MS",
|
|
4852
|
+
hookSettleMs: "HERD_HOOK_SETTLE_MS",
|
|
4853
|
+
agyIdleSettleMs: "HERD_AGY_IDLE_SETTLE_MS",
|
|
4854
|
+
promptPollMs: "HERD_PROMPT_POLL_MS",
|
|
4855
|
+
noticeRetryMs: "HERD_NOTICE_RETRY_MS",
|
|
4856
|
+
investigateAfterMs: "HERD_INVESTIGATE_AFTER_MS",
|
|
4857
|
+
idleMs: "HERD_IDLE_MS",
|
|
4858
|
+
watchMs: "HERD_WATCH_MS",
|
|
4859
|
+
usageReportMs: "HERD_USAGE_REPORT_MS",
|
|
4860
|
+
usageStaleMs: "HERD_USAGE_STALE_MS",
|
|
4861
|
+
activationMs: "HERD_ACTIVATION_MS",
|
|
4862
|
+
activationPollMs: "HERD_ACTIVATION_POLL_MS",
|
|
4863
|
+
signInRestartMs: "HERD_SIGN_IN_RESTART_MS",
|
|
4864
|
+
checkInMs: "HERD_CHECK_IN_MS",
|
|
4865
|
+
machineCheckMs: "HERD_MACHINE_CHECK_MS"
|
|
4866
|
+
};
|
|
4867
|
+
function timingFromEnv(env) {
|
|
4868
|
+
const timing = {};
|
|
4869
|
+
for (const [key, name] of Object.entries(VARIABLES)) {
|
|
4870
|
+
const raw = env[name];
|
|
4871
|
+
if (raw === void 0 || raw === "") continue;
|
|
4872
|
+
const value = Number(raw);
|
|
4873
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
4874
|
+
throw new HerdError(`${name} must be a number of milliseconds, not "${raw}".`);
|
|
4875
|
+
}
|
|
4876
|
+
timing[key] = value;
|
|
4877
|
+
}
|
|
4878
|
+
return timing;
|
|
4879
|
+
}
|
|
4880
|
+
function driverTiming(timing) {
|
|
4881
|
+
const { settleMs, pollMs, confirmMs } = timing;
|
|
4882
|
+
return Object.fromEntries(
|
|
4883
|
+
Object.entries({ settleMs, pollMs, confirmMs }).filter(([, value]) => value !== void 0)
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4823
4887
|
// src/activation.ts
|
|
4824
4888
|
var ACTIVATION_DEADLINE_MS = 3 * 60 * 1e3;
|
|
4825
4889
|
var ACTIVATION_POLL_MS = 500;
|
|
@@ -5026,6 +5090,7 @@ var Activator = class {
|
|
|
5026
5090
|
const { agent } = current;
|
|
5027
5091
|
const deadline = Date.now() + (this.options.deadlineMs ?? ACTIVATION_DEADLINE_MS);
|
|
5028
5092
|
const restartMs = this.options.signInRestartMs ?? SIGN_IN_RESTART_MS;
|
|
5093
|
+
const checkInMs = this.options.checkInMs ?? CHECK_IN_MS;
|
|
5029
5094
|
const pollMs = this.options.pollMs ?? ACTIVATION_POLL_MS;
|
|
5030
5095
|
let seen = "didnt_start";
|
|
5031
5096
|
let startedAt = 0;
|
|
@@ -5060,7 +5125,7 @@ var Activator = class {
|
|
|
5060
5125
|
await start();
|
|
5061
5126
|
while (!current.checkedIn && Date.now() < deadline && !this.stopped) {
|
|
5062
5127
|
const reading = await driver.read(current.terminal).catch(() => null);
|
|
5063
|
-
if (reading?.state === "signed_out") {
|
|
5128
|
+
if (reading?.state === "signed_out" && Date.now() - startedAt >= checkInMs) {
|
|
5064
5129
|
seen = "signed_out";
|
|
5065
5130
|
if (!signingIn) {
|
|
5066
5131
|
signingIn = true;
|
|
@@ -5170,16 +5235,18 @@ async function chooseAgents(options, print) {
|
|
|
5170
5235
|
}
|
|
5171
5236
|
const saved = loaded.status === "ok" ? loaded.config : void 0;
|
|
5172
5237
|
const choices = agentChoices(installed, saved);
|
|
5173
|
-
const asked = options.given ? null : await (options.pick?.(choices) ?? Promise.resolve(null));
|
|
5238
|
+
const asked = options.given || !options.ask ? null : await (options.pick?.(choices) ?? Promise.resolve(null));
|
|
5174
5239
|
const shares = options.given ?? asked ?? // No terminal: what was chosen before, or what's installed.
|
|
5175
5240
|
Object.fromEntries(choices.map((choice) => [choice.cli, choice.share]));
|
|
5176
|
-
|
|
5241
|
+
const choosing = options.ask || options.given !== void 0;
|
|
5242
|
+
if (choosing && (!sameChoice(saved, shares) || !existsSync5(options.configPath))) {
|
|
5177
5243
|
mkdirSync6(dirname6(options.configPath), { recursive: true, mode: 448 });
|
|
5178
5244
|
writeFileSync5(options.configPath, configSource(withChoice(saved, shares)), { mode: 384 });
|
|
5179
5245
|
}
|
|
5180
5246
|
const chosen = ADAPTERS.map(({ cli }) => cli).filter((cli) => (shares[cli] ?? "off") !== "off");
|
|
5247
|
+
const change = options.ask ? "" : ` ${START_COMMAND} --config changes that.`;
|
|
5181
5248
|
print(
|
|
5182
|
-
chosen.length > 0 ? `This machine runs ${chosen.map((cli) => `${label2(cli)} (${SHARE_LABELS[shares[cli]]})`).join(", ")}
|
|
5249
|
+
chosen.length > 0 ? `This machine runs ${chosen.map((cli) => `${label2(cli)} (${SHARE_LABELS[shares[cli]]})`).join(", ")}.${change}` : `This machine runs no agents, so it takes no work. ${START_COMMAND} --config chooses some.`
|
|
5183
5250
|
);
|
|
5184
5251
|
return chosen;
|
|
5185
5252
|
}
|
|
@@ -5888,16 +5955,25 @@ async function runStart(options) {
|
|
|
5888
5955
|
const existing = readRoot2(paths);
|
|
5889
5956
|
const wanted = chooseRoot(options.cwd, options.home);
|
|
5890
5957
|
const running = runningSupervisor(paths);
|
|
5958
|
+
let moving = false;
|
|
5891
5959
|
if (existing && real(existing) !== real(wanted)) {
|
|
5892
5960
|
print(`This machine's root is ${existing}, which holds the repos Friday works on.`);
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5961
|
+
moving = await options.confirm?.(
|
|
5962
|
+
`Make ${wanted} the root instead? Conversations already going carry on where they are.`
|
|
5963
|
+
) ?? false;
|
|
5964
|
+
if (!moving) {
|
|
5965
|
+
print(
|
|
5966
|
+
options.confirm ? "The root stays where it is." : `Run this in a terminal to move the root to ${wanted}.`
|
|
5967
|
+
);
|
|
5968
|
+
if (running) {
|
|
5969
|
+
print(`Friday is running there, version ${running.version}, so nothing changed.`);
|
|
5970
|
+
await waitForAgents(options, await chooseAndAsk(options, print), print);
|
|
5971
|
+
await printSetup(options, print);
|
|
5972
|
+
return 0;
|
|
5973
|
+
}
|
|
5898
5974
|
}
|
|
5899
5975
|
}
|
|
5900
|
-
const root = existing ?? wanted;
|
|
5976
|
+
const root = moving ? wanted : existing ?? wanted;
|
|
5901
5977
|
if (running && root === existing) {
|
|
5902
5978
|
print(`Friday is running from ${root}, version ${running.version}. Nothing changed.`);
|
|
5903
5979
|
await waitForAgents(options, await chooseAndAsk(options, print), print);
|
|
@@ -5906,6 +5982,7 @@ async function runStart(options) {
|
|
|
5906
5982
|
}
|
|
5907
5983
|
const repo = insideWorkTree(root);
|
|
5908
5984
|
if (repo) {
|
|
5985
|
+
if (moving) print("The root stays where it is.");
|
|
5909
5986
|
throw new StartError(
|
|
5910
5987
|
`${root} is inside the git repo ${repo}, so it can't be the machine's root: every conversation would load that repo's settings. Run ${START_COMMAND} in a folder outside any repo, such as the one that holds your checkouts, like ~/code.`
|
|
5911
5988
|
);
|
|
@@ -5929,13 +6006,18 @@ async function runStart(options) {
|
|
|
5929
6006
|
`Stopped the old daemon: removed ${retired.join(" and ")}. Its checkout is left as it was.`
|
|
5930
6007
|
);
|
|
5931
6008
|
await ensureInstalled(options, print);
|
|
5932
|
-
mkdirSync10(root, { recursive: true });
|
|
5933
|
-
writeRoot(paths, root);
|
|
5934
6009
|
const command = [process.execPath, binOf(paths.current, "friday"), "supervise"];
|
|
5935
6010
|
const env = { HERD_HOME: paths.herdHome, ...options.env };
|
|
5936
6011
|
const service = (options.service ?? defaultService(options))(command, env);
|
|
6012
|
+
if (moving && running) {
|
|
6013
|
+
print(`Stopping Friday at ${existing} to start it again from ${root}.`);
|
|
6014
|
+
await service.stop();
|
|
6015
|
+
await stopped(options);
|
|
6016
|
+
}
|
|
6017
|
+
mkdirSync10(root, { recursive: true });
|
|
6018
|
+
writeRoot(paths, root);
|
|
5937
6019
|
const changed = await service.install();
|
|
5938
|
-
await service.start(changed);
|
|
6020
|
+
await service.start(changed || moving);
|
|
5939
6021
|
print(
|
|
5940
6022
|
`Friday is running from ${root}, version ${currentVersion(paths)}, ${service.kind === "launchd" ? "and starts whenever you log in" : service.kind === "systemd" ? "and starts whenever you log in, as a systemd user service" : "in the background"}.`
|
|
5941
6023
|
);
|
|
@@ -5946,10 +6028,22 @@ async function runStart(options) {
|
|
|
5946
6028
|
);
|
|
5947
6029
|
return 0;
|
|
5948
6030
|
}
|
|
6031
|
+
async function stopped(options) {
|
|
6032
|
+
const deadline = Date.now() + (options.stopWaitMs ?? 15e3);
|
|
6033
|
+
while (runningSupervisor(options.paths)) {
|
|
6034
|
+
if (Date.now() > deadline) {
|
|
6035
|
+
throw new StartError(
|
|
6036
|
+
`Friday didn't stop within ${Math.round((options.stopWaitMs ?? 15e3) / 1e3)} seconds, so the root wasn't moved. Run ${FRIDAY_COMMAND} stop, then this again.`
|
|
6037
|
+
);
|
|
6038
|
+
}
|
|
6039
|
+
await new Promise((done) => setTimeout(done, options.stopPollMs ?? 250));
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
5949
6042
|
async function chooseAndAsk(options, print) {
|
|
5950
6043
|
if (!options.agents) return null;
|
|
5951
6044
|
const chosen = await chooseAgents(options.agents, print);
|
|
5952
6045
|
if (!chosen) return null;
|
|
6046
|
+
if (!options.agents.ask && !options.agents.given) return null;
|
|
5953
6047
|
return { chosen, since: askToActivate(options.paths.herdHome) };
|
|
5954
6048
|
}
|
|
5955
6049
|
async function waitForAgents(options, asked, print) {
|
|
@@ -6235,10 +6329,12 @@ Usage: ${FRIDAY_COMMAND} <command>
|
|
|
6235
6329
|
|
|
6236
6330
|
start Make this computer a machine, with the folder this runs in as its root, which
|
|
6237
6331
|
holds your repos, such as ~/code. Signs in through your browser when
|
|
6238
|
-
needed, asks how much of each agent you have, starts the daemon
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6332
|
+
needed, asks how much of each agent you have the first time, starts the daemon
|
|
6333
|
+
now and whenever you log in, then shows what's left to set up. Safe to run
|
|
6334
|
+
again. Run in another folder, it offers to move the root there. --config asks
|
|
6335
|
+
about the agents again, and --agents claude=lot,codex=little sets their shares
|
|
6336
|
+
without asking: off, little, some or lot, where a bare name is some and one
|
|
6337
|
+
left out off
|
|
6242
6338
|
stop Stop the daemon until you log in again, or run start again
|
|
6243
6339
|
status Whether the daemon is running, which version, and its root
|
|
6244
6340
|
logs [-f] Show the daemon's latest log lines. -f keeps following them
|
|
@@ -6312,7 +6408,11 @@ async function friday(args, deps = defaults()) {
|
|
|
6312
6408
|
print: deps.print,
|
|
6313
6409
|
setup: fetchSetup,
|
|
6314
6410
|
...process.env.HERD_PACKAGE_SPEC ? { install: { spec: process.env.HERD_PACKAGE_SPEC } } : {},
|
|
6411
|
+
...process.stdin.isTTY && process.stdout.isTTY ? { confirm: confirmInTerminal } : {},
|
|
6315
6412
|
agents: {
|
|
6413
|
+
// The choice lives in herd.config.ts, so it's asked for only before there is one, or
|
|
6414
|
+
// with --config.
|
|
6415
|
+
ask: rest.includes("--config") || !existsSync11(herdConfigIn(deps.paths.herdHome)),
|
|
6316
6416
|
installed: async () => installedAgents(),
|
|
6317
6417
|
load: () => loadHerdConfig(herdConfigIn(deps.paths.herdHome)),
|
|
6318
6418
|
configPath: herdConfigIn(deps.paths.herdHome),
|
|
@@ -6345,6 +6445,14 @@ ${HELP}`);
|
|
|
6345
6445
|
throw error;
|
|
6346
6446
|
}
|
|
6347
6447
|
}
|
|
6448
|
+
async function confirmInTerminal(question) {
|
|
6449
|
+
const lines = createInterface({ input: process.stdin, output: process.stdout });
|
|
6450
|
+
try {
|
|
6451
|
+
return /^y(es)?$/i.test((await lines.question(`${question} [y/N] `)).trim());
|
|
6452
|
+
} finally {
|
|
6453
|
+
lines.close();
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6348
6456
|
function agentsFlag(args) {
|
|
6349
6457
|
const at = args.findIndex((arg) => arg === "--agents" || arg.startsWith("--agents="));
|
|
6350
6458
|
if (at < 0) return void 0;
|
|
@@ -6572,6 +6680,10 @@ init_define_HERD_PACKAGE();
|
|
|
6572
6680
|
// src/commands/terminal.ts
|
|
6573
6681
|
init_define_HERD_PACKAGE();
|
|
6574
6682
|
var DAEMON_WAIT_MS = Number(process.env.HERD_DAEMON_WAIT_MS ?? 3e4);
|
|
6683
|
+
function withStart(request) {
|
|
6684
|
+
const start = process.env.HERD_START;
|
|
6685
|
+
return start ? { start, ...request } : request;
|
|
6686
|
+
}
|
|
6575
6687
|
async function fromTerminal(request) {
|
|
6576
6688
|
const socket = process.env.HERD_SOCKET;
|
|
6577
6689
|
const secret = process.env.HERD_TERMINAL;
|
|
@@ -6581,7 +6693,7 @@ async function fromTerminal(request) {
|
|
|
6581
6693
|
let reply = null;
|
|
6582
6694
|
const deadline = Date.now() + DAEMON_WAIT_MS;
|
|
6583
6695
|
while (!reply) {
|
|
6584
|
-
reply = await askDaemon(socket, { secret, ...request }).catch(() => null);
|
|
6696
|
+
reply = await askDaemon(socket, { secret, ...withStart(request) }).catch(() => null);
|
|
6585
6697
|
if (reply) break;
|
|
6586
6698
|
if (Date.now() > deadline) throw new HerdError("Couldn't reach herd's daemon. Is it running?");
|
|
6587
6699
|
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
@@ -7404,6 +7516,9 @@ var ownPrompt = (draft) => OWN_PROMPTS.find(
|
|
|
7404
7516
|
) ?? (draft.startsWith(DELEGATE_START) || draft.length >= 40 && DELEGATE_START.startsWith(draft) ? draft : null);
|
|
7405
7517
|
var SESSION_ID = /^[A-Za-z0-9-]{1,100}$/;
|
|
7406
7518
|
var SUBMIT_RETRIES = 3;
|
|
7519
|
+
function withStartKey(command, startKey) {
|
|
7520
|
+
return startKey ? `HERD_START=${startKey} ${command}` : command;
|
|
7521
|
+
}
|
|
7407
7522
|
var DEFAULT_TIMING = { settleMs: 800, pollMs: 150, startMs: 6e4, confirmMs: 3e3 };
|
|
7408
7523
|
var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
7409
7524
|
var EXIT_WAIT_MS = 5e3;
|
|
@@ -7460,12 +7575,12 @@ var AgentDriver = class {
|
|
|
7460
7575
|
* `claude --resume <id>`. Resuming without an ID types `/resume` and picks the newest
|
|
7461
7576
|
* conversation, which is this worktree's only agent.
|
|
7462
7577
|
*/
|
|
7463
|
-
async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2 } = {}) {
|
|
7578
|
+
async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2, startKey = null } = {}) {
|
|
7464
7579
|
const known = sessionId !== null && SESSION_ID.test(sessionId);
|
|
7465
7580
|
const byId = resume && known;
|
|
7466
7581
|
const command = byId ? `claude --resume ${sessionId}` : !resume && known ? `claude --session-id ${sessionId}` : "claude";
|
|
7467
7582
|
await this.settle(name);
|
|
7468
|
-
await this.terminals.type(name, command);
|
|
7583
|
+
await this.terminals.type(name, withStartKey(command, startKey));
|
|
7469
7584
|
await this.terminals.press(name, "enter");
|
|
7470
7585
|
const first = await this.until(
|
|
7471
7586
|
name,
|
|
@@ -7758,11 +7873,11 @@ var AgyDriver = class {
|
|
|
7758
7873
|
* waits for its prompt box. It trusts the folder only in a worktree the daemon made. agy chooses
|
|
7759
7874
|
* a session's ID itself, and its `SessionStart` hook reports it.
|
|
7760
7875
|
*/
|
|
7761
|
-
async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2 } = {}) {
|
|
7876
|
+
async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2, startKey = null } = {}) {
|
|
7762
7877
|
const byId = resume && sessionId !== null && SESSION_ID.test(sessionId);
|
|
7763
7878
|
const command = byId ? `agy --conversation=${sessionId} ${AGY_FLAGS}` : `agy ${AGY_FLAGS}`;
|
|
7764
7879
|
await this.settle(name);
|
|
7765
|
-
await this.terminals.type(name, command);
|
|
7880
|
+
await this.terminals.type(name, withStartKey(command, startKey));
|
|
7766
7881
|
await this.terminals.press(name, "enter");
|
|
7767
7882
|
const first = await this.until(
|
|
7768
7883
|
name,
|
|
@@ -8749,11 +8864,11 @@ var CodexDriver = class {
|
|
|
8749
8864
|
* Types `codex` into the settled shell, or `codex resume <id>` to continue a session, and waits
|
|
8750
8865
|
* for its composer. Codex chooses a session's ID itself, when it takes the first prompt.
|
|
8751
8866
|
*/
|
|
8752
|
-
async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2 } = {}) {
|
|
8867
|
+
async start(name, cwd, { resume = false, sessionId = null, ownFolder: ownFolder2, startKey = null } = {}) {
|
|
8753
8868
|
const byId = resume && sessionId !== null && SESSION_ID.test(sessionId);
|
|
8754
8869
|
const command = byId ? `codex resume ${sessionId} ${CODEX_FLAGS}` : `codex ${CODEX_FLAGS}`;
|
|
8755
8870
|
await this.settle(name);
|
|
8756
|
-
await this.terminals.type(name, command);
|
|
8871
|
+
await this.terminals.type(name, withStartKey(command, startKey));
|
|
8757
8872
|
await this.terminals.press(name, "enter");
|
|
8758
8873
|
const first = await this.until(
|
|
8759
8874
|
name,
|
|
@@ -9764,7 +9879,7 @@ var HostTerminals = class {
|
|
|
9764
9879
|
await this.askFor(name, { op: "kill", name });
|
|
9765
9880
|
}
|
|
9766
9881
|
async retire(canStop) {
|
|
9767
|
-
const
|
|
9882
|
+
const stopped2 = [];
|
|
9768
9883
|
for (const host of await this.discover()) {
|
|
9769
9884
|
if (host.current) continue;
|
|
9770
9885
|
const version = host.hello.version ?? "unversioned";
|
|
@@ -9776,12 +9891,12 @@ var HostTerminals = class {
|
|
|
9776
9891
|
continue;
|
|
9777
9892
|
}
|
|
9778
9893
|
if (await this.stop(host)) {
|
|
9779
|
-
|
|
9894
|
+
stopped2.push(version);
|
|
9780
9895
|
this.log(`Retired terminal host ${version}, which had nothing left running.`);
|
|
9781
9896
|
}
|
|
9782
9897
|
}
|
|
9783
|
-
if (
|
|
9784
|
-
return
|
|
9898
|
+
if (stopped2.length > 0) await this.discover();
|
|
9899
|
+
return stopped2;
|
|
9785
9900
|
}
|
|
9786
9901
|
/** Stops a host: politely where it knows how, and otherwise by its process. */
|
|
9787
9902
|
async stop(host) {
|
|
@@ -9809,48 +9924,6 @@ var HostTerminals = class {
|
|
|
9809
9924
|
}
|
|
9810
9925
|
};
|
|
9811
9926
|
|
|
9812
|
-
// src/timing.ts
|
|
9813
|
-
init_define_HERD_PACKAGE();
|
|
9814
|
-
var VARIABLES = {
|
|
9815
|
-
settleMs: "HERD_SETTLE_MS",
|
|
9816
|
-
pollMs: "HERD_POLL_MS",
|
|
9817
|
-
confirmMs: "HERD_CONFIRM_MS",
|
|
9818
|
-
pasteMs: "HERD_PASTE_MS",
|
|
9819
|
-
renewMs: "HERD_RENEW_MS",
|
|
9820
|
-
hookSettleMs: "HERD_HOOK_SETTLE_MS",
|
|
9821
|
-
agyIdleSettleMs: "HERD_AGY_IDLE_SETTLE_MS",
|
|
9822
|
-
promptPollMs: "HERD_PROMPT_POLL_MS",
|
|
9823
|
-
noticeRetryMs: "HERD_NOTICE_RETRY_MS",
|
|
9824
|
-
investigateAfterMs: "HERD_INVESTIGATE_AFTER_MS",
|
|
9825
|
-
idleMs: "HERD_IDLE_MS",
|
|
9826
|
-
watchMs: "HERD_WATCH_MS",
|
|
9827
|
-
usageReportMs: "HERD_USAGE_REPORT_MS",
|
|
9828
|
-
usageStaleMs: "HERD_USAGE_STALE_MS",
|
|
9829
|
-
activationMs: "HERD_ACTIVATION_MS",
|
|
9830
|
-
activationPollMs: "HERD_ACTIVATION_POLL_MS",
|
|
9831
|
-
signInRestartMs: "HERD_SIGN_IN_RESTART_MS",
|
|
9832
|
-
machineCheckMs: "HERD_MACHINE_CHECK_MS"
|
|
9833
|
-
};
|
|
9834
|
-
function timingFromEnv(env) {
|
|
9835
|
-
const timing = {};
|
|
9836
|
-
for (const [key, name] of Object.entries(VARIABLES)) {
|
|
9837
|
-
const raw = env[name];
|
|
9838
|
-
if (raw === void 0 || raw === "") continue;
|
|
9839
|
-
const value = Number(raw);
|
|
9840
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
9841
|
-
throw new HerdError(`${name} must be a number of milliseconds, not "${raw}".`);
|
|
9842
|
-
}
|
|
9843
|
-
timing[key] = value;
|
|
9844
|
-
}
|
|
9845
|
-
return timing;
|
|
9846
|
-
}
|
|
9847
|
-
function driverTiming(timing) {
|
|
9848
|
-
const { settleMs, pollMs, confirmMs } = timing;
|
|
9849
|
-
return Object.fromEntries(
|
|
9850
|
-
Object.entries({ settleMs, pollMs, confirmMs }).filter(([, value]) => value !== void 0)
|
|
9851
|
-
);
|
|
9852
|
-
}
|
|
9853
|
-
|
|
9854
9927
|
// src/usage-reporter.ts
|
|
9855
9928
|
init_define_HERD_PACKAGE();
|
|
9856
9929
|
var USAGE_REPORT_MS = 5 * 60 * 1e3;
|
|
@@ -10115,6 +10188,17 @@ var workspaceFields = z25.object({
|
|
|
10115
10188
|
codexOffset: z25.number().default(0),
|
|
10116
10189
|
/** When the agent was last started, so Codex's new session file is told from older ones. */
|
|
10117
10190
|
agentStartedAt: z25.number().default(0),
|
|
10191
|
+
/**
|
|
10192
|
+
* The key the agent was last started with, as `HERD_START`. Its `herd inbox` carrying this key
|
|
10193
|
+
* is what proves the start works: never a hook, a prompt being taken, or the screen.
|
|
10194
|
+
*/
|
|
10195
|
+
startKey: z25.string().nullable().default(null),
|
|
10196
|
+
/** Whether the current start has checked in. One from before keys counts as proven. */
|
|
10197
|
+
startProven: z25.boolean().default(true),
|
|
10198
|
+
/** When the check-in window of an unproven start began, or null once it's proven or for none. */
|
|
10199
|
+
checkInFrom: z25.number().nullable().default(null),
|
|
10200
|
+
/** Whether this start has been flagged as needing someone at its terminal, so it's said once. */
|
|
10201
|
+
needsAttention: z25.boolean().default(false),
|
|
10118
10202
|
/** What agy's status line last said its agent was doing, and when. */
|
|
10119
10203
|
agentState: z25.enum(["working", "idle"]).nullable().default(null),
|
|
10120
10204
|
agentStateAt: z25.number().default(0)
|
|
@@ -10191,7 +10275,7 @@ async function claimChange(ws, workspace, change) {
|
|
|
10191
10275
|
|
|
10192
10276
|
// src/workspaces/delivery.ts
|
|
10193
10277
|
init_define_HERD_PACKAGE();
|
|
10194
|
-
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
10278
|
+
import { randomBytes as randomBytes6, randomUUID as randomUUID6 } from "node:crypto";
|
|
10195
10279
|
import { join as join30 } from "node:path";
|
|
10196
10280
|
|
|
10197
10281
|
// src/workspaces/repos.ts
|
|
@@ -10491,6 +10575,10 @@ async function setUp(ws, conversationId) {
|
|
|
10491
10575
|
codexFile: null,
|
|
10492
10576
|
codexOffset: 0,
|
|
10493
10577
|
agentStartedAt: 0,
|
|
10578
|
+
startKey: null,
|
|
10579
|
+
startProven: true,
|
|
10580
|
+
checkInFrom: null,
|
|
10581
|
+
needsAttention: false,
|
|
10494
10582
|
agentState: null,
|
|
10495
10583
|
agentStateAt: 0
|
|
10496
10584
|
};
|
|
@@ -10534,6 +10622,12 @@ async function openLead(ws, workspace) {
|
|
|
10534
10622
|
});
|
|
10535
10623
|
}
|
|
10536
10624
|
|
|
10625
|
+
// src/workspaces/watch.ts
|
|
10626
|
+
init_define_HERD_PACKAGE();
|
|
10627
|
+
|
|
10628
|
+
// src/workspaces/check-in.ts
|
|
10629
|
+
init_define_HERD_PACKAGE();
|
|
10630
|
+
|
|
10537
10631
|
// src/workspaces/signed-out.ts
|
|
10538
10632
|
init_define_HERD_PACKAGE();
|
|
10539
10633
|
init_src();
|
|
@@ -10554,14 +10648,50 @@ async function releaseSignedOut(ws, workspace) {
|
|
|
10554
10648
|
terminalNotice: false,
|
|
10555
10649
|
idleSince: null,
|
|
10556
10650
|
noticeFailedAt: null,
|
|
10557
|
-
noticeFailure: null
|
|
10651
|
+
noticeFailure: null,
|
|
10652
|
+
checkInFrom: null,
|
|
10653
|
+
needsAttention: false
|
|
10558
10654
|
});
|
|
10559
10655
|
if (workspace.status !== "exited") await ws.report(id2, "exited");
|
|
10560
10656
|
await ws.post(`${ws.conversationPath(id2)}/release`, { reason: "agent_signed_out" });
|
|
10561
10657
|
}
|
|
10562
10658
|
|
|
10563
|
-
// src/workspaces/
|
|
10564
|
-
|
|
10659
|
+
// src/workspaces/check-in.ts
|
|
10660
|
+
async function checkIn(ws, workspace, grant, start) {
|
|
10661
|
+
if (workspace.startProven || !start || start !== workspace.startKey) return;
|
|
10662
|
+
if (grant.terminalId !== workspace.lead) return;
|
|
10663
|
+
const id2 = workspace.conversationId;
|
|
10664
|
+
ws.update(id2, { startProven: true, checkInFrom: null, needsAttention: false });
|
|
10665
|
+
if (!workspace.needsAttention) return;
|
|
10666
|
+
ws.log(`${id2}: the agent checked in after all.`);
|
|
10667
|
+
await ws.terminalEvent(id2, {
|
|
10668
|
+
type: "terminal.checked_in",
|
|
10669
|
+
payload: { terminalId: workspace.lead }
|
|
10670
|
+
});
|
|
10671
|
+
}
|
|
10672
|
+
function checkInOverdue(ws, workspace, now = Date.now()) {
|
|
10673
|
+
return !workspace.startProven && workspace.checkInFrom !== null && now - workspace.checkInFrom >= (ws.options.checkInMs ?? CHECK_IN_MS);
|
|
10674
|
+
}
|
|
10675
|
+
async function followCheckIn(ws, workspace, reading) {
|
|
10676
|
+
if (!checkInOverdue(ws, workspace)) return false;
|
|
10677
|
+
if (reading?.state === "signed_out") {
|
|
10678
|
+
await releaseSignedOut(ws, workspace);
|
|
10679
|
+
return true;
|
|
10680
|
+
}
|
|
10681
|
+
if (workspace.needsAttention) return false;
|
|
10682
|
+
const id2 = workspace.conversationId;
|
|
10683
|
+
ws.update(id2, { needsAttention: true });
|
|
10684
|
+
ws.log(`${id2}: ${workspace.agent} started but didn't check in, so it needs someone.`);
|
|
10685
|
+
await ws.terminalEvent(id2, {
|
|
10686
|
+
type: "terminal.needs_attention",
|
|
10687
|
+
payload: {
|
|
10688
|
+
terminalId: workspace.lead,
|
|
10689
|
+
agent: workspace.agent,
|
|
10690
|
+
machineName: ws.options.machineName()
|
|
10691
|
+
}
|
|
10692
|
+
});
|
|
10693
|
+
return false;
|
|
10694
|
+
}
|
|
10565
10695
|
|
|
10566
10696
|
// src/workspaces/investigation.ts
|
|
10567
10697
|
init_define_HERD_PACKAGE();
|
|
@@ -10569,11 +10699,19 @@ import { randomUUID as randomUUID5 } from "node:crypto";
|
|
|
10569
10699
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync21 } from "node:fs";
|
|
10570
10700
|
import { join as join29 } from "node:path";
|
|
10571
10701
|
function needsInvestigator(ws, workspace, reading) {
|
|
10702
|
+
const allowed = ws.options.canInvestigate ? ws.options.canInvestigate() : workspace.agent === "claude-code";
|
|
10703
|
+
if (!allowed || !reading || workspace.investigation) return false;
|
|
10704
|
+
if (reading.state !== "unknown" || reading.knownDialog || reading.program === "shell") {
|
|
10705
|
+
return false;
|
|
10706
|
+
}
|
|
10707
|
+
const trigger = investigationTrigger(ws, workspace);
|
|
10708
|
+
return trigger !== null && workspace.investigatedFailure !== trigger;
|
|
10709
|
+
}
|
|
10710
|
+
function investigationTrigger(ws, workspace) {
|
|
10711
|
+
if (workspace.needsAttention && checkInOverdue(ws, workspace)) return workspace.checkInFrom;
|
|
10572
10712
|
const failedAt = workspace.noticeFailedAt;
|
|
10573
|
-
|
|
10574
|
-
return
|
|
10575
|
-
reading && failedAt !== null && workspace.deferred && !workspace.investigation && workspace.investigatedFailure !== failedAt && Date.now() - failedAt >= (ws.options.investigateAfterMs ?? INVESTIGATE_AFTER_MS) && reading.state === "unknown" && !reading.knownDialog && reading.program !== "shell"
|
|
10576
|
-
);
|
|
10713
|
+
const after = ws.options.investigateAfterMs ?? INVESTIGATE_AFTER_MS;
|
|
10714
|
+
return failedAt !== null && workspace.deferred && Date.now() - failedAt >= after ? failedAt : null;
|
|
10577
10715
|
}
|
|
10578
10716
|
async function openInvestigator(ws, workspace) {
|
|
10579
10717
|
const id2 = workspace.conversationId;
|
|
@@ -10583,10 +10721,11 @@ async function openInvestigator(ws, workspace) {
|
|
|
10583
10721
|
);
|
|
10584
10722
|
const secret = ws.options.local().grant({ terminalId: terminal, conversationId: id2, investigates: workspace.lead });
|
|
10585
10723
|
ws.update(id2, {
|
|
10586
|
-
investigatedFailure: workspace
|
|
10724
|
+
investigatedFailure: investigationTrigger(ws, workspace),
|
|
10587
10725
|
investigation: { terminal, secret, startedAt: Date.now(), screen, key: null, promptId: null }
|
|
10588
10726
|
});
|
|
10589
10727
|
ws.log(`${id2}: opened an investigator for a screen herd can't read.`);
|
|
10728
|
+
if (workspace.agent !== "claude-code") configureClaude(ws, workspace);
|
|
10590
10729
|
const shell = ws.options.shell ?? process.env.SHELL ?? "/bin/sh";
|
|
10591
10730
|
try {
|
|
10592
10731
|
await ws.options.terminals.create({
|
|
@@ -10757,7 +10896,8 @@ async function watchOne(ws, conversationId, terminals, rows = []) {
|
|
|
10757
10896
|
lead = (await ws.options.terminals.list()).find((t) => t.name === workspace?.lead);
|
|
10758
10897
|
}
|
|
10759
10898
|
let reading = lead && !lead.exited ? await ws.driverFor(workspace).read(workspace.lead) : null;
|
|
10760
|
-
if (
|
|
10899
|
+
if (await followCheckIn(ws, workspace, reading)) return;
|
|
10900
|
+
workspace = ws.get(conversationId) ?? workspace;
|
|
10761
10901
|
if (reading && !reading.knownDialog && workspace.closedDialog) {
|
|
10762
10902
|
workspace = ws.update(conversationId, { closedDialog: null });
|
|
10763
10903
|
}
|
|
@@ -10856,12 +10996,12 @@ async function retireHosts(ws) {
|
|
|
10856
10996
|
if (!terminals.retire || Date.now() - ws.retiredAt < every) return;
|
|
10857
10997
|
ws.retiredAt = Date.now();
|
|
10858
10998
|
const inHost = (version) => Object.values(ws.load()).filter((workspace) => workspace.hostVersion === version);
|
|
10859
|
-
const
|
|
10999
|
+
const stopped2 = await terminals.retire(
|
|
10860
11000
|
(version) => inHost(version).every(
|
|
10861
11001
|
(workspace) => !workspace.claimId && (workspace.status === "idle" || workspace.status === "exited")
|
|
10862
11002
|
)
|
|
10863
11003
|
);
|
|
10864
|
-
for (const version of
|
|
11004
|
+
for (const version of stopped2) {
|
|
10865
11005
|
for (const workspace of inHost(version)) {
|
|
10866
11006
|
const id2 = workspace.conversationId;
|
|
10867
11007
|
await ws.serially(id2, async () => {
|
|
@@ -10939,11 +11079,18 @@ async function deliver(ws, conversationId, delivery) {
|
|
|
10939
11079
|
});
|
|
10940
11080
|
}
|
|
10941
11081
|
const resume = workspace.started && (!ownId || workspace.sessionId !== null);
|
|
10942
|
-
workspace = ws.update(conversationId, {
|
|
11082
|
+
workspace = ws.update(conversationId, {
|
|
11083
|
+
agentStartedAt: Date.now(),
|
|
11084
|
+
startKey: randomBytes6(12).toString("hex"),
|
|
11085
|
+
startProven: false,
|
|
11086
|
+
checkInFrom: null,
|
|
11087
|
+
needsAttention: false
|
|
11088
|
+
});
|
|
10943
11089
|
await ws.driverFor(workspace).start(workspace.lead, workspace.folder, {
|
|
10944
11090
|
resume,
|
|
10945
11091
|
sessionId: workspace.sessionId,
|
|
10946
|
-
ownFolder: isHerdFolder(workspace.folder, rootOf(ws))
|
|
11092
|
+
ownFolder: isHerdFolder(workspace.folder, rootOf(ws)),
|
|
11093
|
+
startKey: workspace.startKey
|
|
10947
11094
|
});
|
|
10948
11095
|
workspace = ws.update(conversationId, {
|
|
10949
11096
|
started: true,
|
|
@@ -10952,16 +11099,15 @@ async function deliver(ws, conversationId, delivery) {
|
|
|
10952
11099
|
askedToReply: false,
|
|
10953
11100
|
handledThisTurn: false
|
|
10954
11101
|
});
|
|
10955
|
-
if ((await ws.driverFor(workspace).read(workspace.lead)).state === "signed_out") {
|
|
10956
|
-
return releaseSignedOut(ws, workspace);
|
|
10957
|
-
}
|
|
10958
11102
|
if (!await claim(ws, workspace)) return;
|
|
10959
11103
|
const result2 = await submitKeepingClaim(
|
|
10960
11104
|
ws,
|
|
10961
11105
|
conversationId,
|
|
10962
11106
|
workspace.readSeq === 0 ? workspace.parent ? delegatePrompt(workspace.parent) : STARTING_PROMPT : NOTICE
|
|
10963
11107
|
);
|
|
10964
|
-
|
|
11108
|
+
workspace = ws.get(conversationId) ?? workspace;
|
|
11109
|
+
if (!workspace.startProven) workspace = ws.update(conversationId, { checkInFrom: Date.now() });
|
|
11110
|
+
return afterSubmit(ws, workspace, result2);
|
|
10965
11111
|
}
|
|
10966
11112
|
let reading = await ws.driverFor(workspace).read(workspace.lead);
|
|
10967
11113
|
if (reading.knownDialog) {
|
|
@@ -11352,7 +11498,10 @@ async function answer(ws, grant, request, gone = new AbortController().signal) {
|
|
|
11352
11498
|
if (request.op === "activate") {
|
|
11353
11499
|
return { ok: false, error: "Only an agent's activation terminal can check in." };
|
|
11354
11500
|
}
|
|
11355
|
-
if (request.op === "inbox")
|
|
11501
|
+
if (request.op === "inbox") {
|
|
11502
|
+
await checkIn(ws, workspace, grant, request.start);
|
|
11503
|
+
return inbox(ws, ws.get(grant.conversationId) ?? workspace);
|
|
11504
|
+
}
|
|
11356
11505
|
if (request.op === "send") return send(ws, workspace, request);
|
|
11357
11506
|
if (request.op === "channels") return channels(ws, workspace);
|
|
11358
11507
|
if (request.op === "watch") return watch(ws, workspace, request);
|
|
@@ -12643,6 +12792,11 @@ async function runDaemon(args) {
|
|
|
12643
12792
|
activations: {
|
|
12644
12793
|
fromTerminal: async (grant, request) => activator?.fromTerminal(grant, request) ?? { ok: false, error: "This activation is over." }
|
|
12645
12794
|
},
|
|
12795
|
+
// The investigator is Claude Code, so any agent's stuck screen gets one where it's activated.
|
|
12796
|
+
canInvestigate: () => Boolean(
|
|
12797
|
+
daemon?.chosenClis().some((cli) => cli.name === "claude-code") && activator?.states()["claude-code"]?.state === "activated"
|
|
12798
|
+
),
|
|
12799
|
+
...timing.checkInMs === void 0 ? {} : { checkInMs: timing.checkInMs },
|
|
12646
12800
|
// The agent stops being offered before its conversation goes back, then activates again.
|
|
12647
12801
|
agentSignedOut: async (agent) => {
|
|
12648
12802
|
const version = daemon?.chosenClis().find((cli) => cli.name === agent)?.version ?? "unknown";
|
|
@@ -12680,6 +12834,7 @@ async function runDaemon(args) {
|
|
|
12680
12834
|
...timing.activationMs === void 0 ? {} : { deadlineMs: timing.activationMs },
|
|
12681
12835
|
...timing.activationPollMs === void 0 ? {} : { pollMs: timing.activationPollMs },
|
|
12682
12836
|
...timing.signInRestartMs === void 0 ? {} : { signInRestartMs: timing.signInRestartMs },
|
|
12837
|
+
...timing.checkInMs === void 0 ? {} : { checkInMs: timing.checkInMs },
|
|
12683
12838
|
log
|
|
12684
12839
|
});
|
|
12685
12840
|
checks = new MachineChecks({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herdingchats/friday",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.569",
|
|
4
4
|
"description": "Friday, HerdingChats' agent daemon, and the herd command it runs agents with.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
"satori": "0.32.0",
|
|
23
23
|
"zod": "4.6.7"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "ac28f0e97b92fbe2178387595a511d35c3ae4953"
|
|
26
26
|
}
|