@herdingchats/friday 0.1.635 → 0.1.645
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 +299 -239
- 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.645", gitHead: "768ab1770e2447667409893dd18699acc30e2c03" };
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -762,7 +762,10 @@ var init_machines = __esm({
|
|
|
762
762
|
// Whether its GitHub CLI is signed in, and as whom.
|
|
763
763
|
gh: gitHubCliSchema.optional(),
|
|
764
764
|
// A folder it works in that isn't there, while it takes no work.
|
|
765
|
-
folderUnavailable: folderUnavailableSchema.optional()
|
|
765
|
+
folderUnavailable: folderUnavailableSchema.optional(),
|
|
766
|
+
// The daemon's own package version, or `dev` from a checkout. Daemons from before send none,
|
|
767
|
+
// which reads as older than any that does.
|
|
768
|
+
herdVersion: z8.string().min(1).max(50).optional()
|
|
766
769
|
});
|
|
767
770
|
machineSchema = machineRegistrationSchema.extend({
|
|
768
771
|
ownerId: nonEmpty4,
|
|
@@ -2385,7 +2388,7 @@ var init_widget = __esm({
|
|
|
2385
2388
|
});
|
|
2386
2389
|
|
|
2387
2390
|
// ../common-board/src/widgets/conversations.tsx
|
|
2388
|
-
import { z as
|
|
2391
|
+
import { z as z29 } from "zod";
|
|
2389
2392
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2390
2393
|
function badge(item) {
|
|
2391
2394
|
if (pullRequestBadge(item)) {
|
|
@@ -2429,10 +2432,10 @@ var init_conversations = __esm({
|
|
|
2429
2432
|
conversations2 = defineWidget({
|
|
2430
2433
|
kind: "conversations",
|
|
2431
2434
|
describe: "Blocked, working and queued conversations, then the latest done, with what each is doing, where it came from and who holds it",
|
|
2432
|
-
options:
|
|
2433
|
-
states:
|
|
2434
|
-
limit:
|
|
2435
|
-
show:
|
|
2435
|
+
options: z29.object({
|
|
2436
|
+
states: z29.array(conversationStateSchema).min(1).default(["stuck", "working", "waiting", "done"]),
|
|
2437
|
+
limit: z29.number().int().min(1).max(BOARD_ITEMS).default(4),
|
|
2438
|
+
show: z29.array(z29.enum(DETAILS)).default([...DETAILS])
|
|
2436
2439
|
}),
|
|
2437
2440
|
reads: ["items", "machines"],
|
|
2438
2441
|
Component: ({ board, options, theme, now }) => {
|
|
@@ -2476,7 +2479,7 @@ var init_conversations = __esm({
|
|
|
2476
2479
|
});
|
|
2477
2480
|
|
|
2478
2481
|
// ../common-board/src/widgets/counts.tsx
|
|
2479
|
-
import { z as
|
|
2482
|
+
import { z as z30 } from "zod";
|
|
2480
2483
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2481
2484
|
var COUNTS, LABELS2, counts;
|
|
2482
2485
|
var init_counts = __esm({
|
|
@@ -2496,8 +2499,8 @@ var init_counts = __esm({
|
|
|
2496
2499
|
counts = defineWidget({
|
|
2497
2500
|
kind: "counts",
|
|
2498
2501
|
describe: "How many conversations are stuck, unanswered, waiting and working, and done today",
|
|
2499
|
-
options:
|
|
2500
|
-
show:
|
|
2502
|
+
options: z30.object({
|
|
2503
|
+
show: z30.array(z30.enum(COUNTS)).min(1).max(5).default([...COUNTS])
|
|
2501
2504
|
}),
|
|
2502
2505
|
reads: ["counts", "doneToday"],
|
|
2503
2506
|
Component: ({ board, options, theme }) => {
|
|
@@ -2536,7 +2539,7 @@ var init_counts = __esm({
|
|
|
2536
2539
|
});
|
|
2537
2540
|
|
|
2538
2541
|
// ../common-board/src/widgets/header.tsx
|
|
2539
|
-
import { z as
|
|
2542
|
+
import { z as z31 } from "zod";
|
|
2540
2543
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2541
2544
|
function summary(board) {
|
|
2542
2545
|
const parts = [
|
|
@@ -2561,9 +2564,9 @@ var init_header = __esm({
|
|
|
2561
2564
|
header = defineWidget({
|
|
2562
2565
|
kind: "header",
|
|
2563
2566
|
describe: "The board's title, a line of how much is open, and when it was drawn",
|
|
2564
|
-
options:
|
|
2565
|
-
title:
|
|
2566
|
-
summary:
|
|
2567
|
+
options: z31.object({
|
|
2568
|
+
title: z31.string().min(1).max(40).default(`@${BOT_NAME}`),
|
|
2569
|
+
summary: z31.boolean().default(true)
|
|
2567
2570
|
}),
|
|
2568
2571
|
reads: ["counts"],
|
|
2569
2572
|
Component: ({ board, options, theme, now }) => {
|
|
@@ -2581,7 +2584,7 @@ var init_header = __esm({
|
|
|
2581
2584
|
});
|
|
2582
2585
|
|
|
2583
2586
|
// ../common-board/src/widgets/machines.tsx
|
|
2584
|
-
import { z as
|
|
2587
|
+
import { z as z32 } from "zod";
|
|
2585
2588
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2586
2589
|
function usageText(worker) {
|
|
2587
2590
|
const usage = worker.usage;
|
|
@@ -2612,7 +2615,7 @@ var init_machines2 = __esm({
|
|
|
2612
2615
|
machines = defineWidget({
|
|
2613
2616
|
kind: "machines",
|
|
2614
2617
|
describe: "One bubble per agent on an online machine, with its usage, most recently active first",
|
|
2615
|
-
options:
|
|
2618
|
+
options: z32.object({ limit: z32.number().int().min(1).max(8).default(3) }),
|
|
2616
2619
|
reads: ["machines"],
|
|
2617
2620
|
Component: ({ board, options, theme }) => {
|
|
2618
2621
|
const workers2 = board.machines.filter((machine) => machine.online).flatMap((machine) => machine.workers.map((worker) => ({ ...worker, machine: machine.name }))).sort((a, b) => (b.reportedAt ?? 0) - (a.reportedAt ?? 0));
|
|
@@ -2729,9 +2732,9 @@ var init_board2 = __esm({
|
|
|
2729
2732
|
});
|
|
2730
2733
|
|
|
2731
2734
|
// ../common-board/src/composition.ts
|
|
2732
|
-
import { z as
|
|
2735
|
+
import { z as z33 } from "zod";
|
|
2733
2736
|
function readComposition(stored) {
|
|
2734
|
-
const shape =
|
|
2737
|
+
const shape = z33.object({ widgets: z33.array(placedWidgetSchema) }).safeParse(stored);
|
|
2735
2738
|
if (!shape.success) return DEFAULT_COMPOSITION;
|
|
2736
2739
|
const known = shape.data.widgets.filter(
|
|
2737
2740
|
(widget) => WIDGETS[widget.kind]?.options.safeParse(widget.options).success
|
|
@@ -2746,11 +2749,11 @@ var init_composition = __esm({
|
|
|
2746
2749
|
init_registry();
|
|
2747
2750
|
MAX_WIDGETS = 12;
|
|
2748
2751
|
MAX_COMPOSITION_BYTES = 4096;
|
|
2749
|
-
placedWidgetSchema =
|
|
2750
|
-
kind:
|
|
2751
|
-
options:
|
|
2752
|
+
placedWidgetSchema = z33.object({
|
|
2753
|
+
kind: z33.string().min(1),
|
|
2754
|
+
options: z33.record(z33.string(), z33.unknown()).default({})
|
|
2752
2755
|
});
|
|
2753
|
-
compositionSchema =
|
|
2756
|
+
compositionSchema = z33.object({ widgets: z33.array(placedWidgetSchema).max(MAX_WIDGETS) }).superRefine((composition, context2) => {
|
|
2754
2757
|
if (JSON.stringify(composition).length > MAX_COMPOSITION_BYTES) {
|
|
2755
2758
|
context2.addIssue({
|
|
2756
2759
|
code: "custom",
|
|
@@ -4302,6 +4305,7 @@ function hasUnpushed(path) {
|
|
|
4302
4305
|
}
|
|
4303
4306
|
|
|
4304
4307
|
// src/config.ts
|
|
4308
|
+
init_self();
|
|
4305
4309
|
var repoSchema = z18.object({ name: z18.string(), path: z18.string(), remote: z18.string() });
|
|
4306
4310
|
var configSchema = z18.object({
|
|
4307
4311
|
machineId: z18.string(),
|
|
@@ -4432,7 +4436,8 @@ function registration(config, clis, facts = {}, cloned = []) {
|
|
|
4432
4436
|
...facts.config ? { config: facts.config } : {},
|
|
4433
4437
|
...facts.configError ? { configError: facts.configError.slice(0, 1e3) } : {},
|
|
4434
4438
|
...facts.gh ? { gh: facts.gh } : {},
|
|
4435
|
-
...facts.folderUnavailable ? { folderUnavailable: facts.folderUnavailable } : {}
|
|
4439
|
+
...facts.folderUnavailable ? { folderUnavailable: facts.folderUnavailable } : {},
|
|
4440
|
+
herdVersion: HERD_VERSION
|
|
4436
4441
|
};
|
|
4437
4442
|
}
|
|
4438
4443
|
function describeCli(name, version) {
|
|
@@ -4567,6 +4572,7 @@ var localRequestSchema = z19.object({
|
|
|
4567
4572
|
"watch",
|
|
4568
4573
|
"repos",
|
|
4569
4574
|
"repo",
|
|
4575
|
+
"repo-add",
|
|
4570
4576
|
"activate"
|
|
4571
4577
|
]),
|
|
4572
4578
|
// The key of the agent's start, from `HERD_START`, which `herd inbox` proves the start with.
|
|
@@ -4578,6 +4584,11 @@ var localRequestSchema = z19.object({
|
|
|
4578
4584
|
base: z19.string().min(1).max(300).optional(),
|
|
4579
4585
|
fetchFailed: z19.string().max(1e3).optional()
|
|
4580
4586
|
}).optional(),
|
|
4587
|
+
// `herd repo`: what the agent named and the folder it asked for, for the daemon to add.
|
|
4588
|
+
repoAdd: z19.object({
|
|
4589
|
+
asked: z19.string().min(1).max(500),
|
|
4590
|
+
folder: z19.string().min(1).max(100).optional()
|
|
4591
|
+
}).optional(),
|
|
4581
4592
|
// `herd delegate --repo`, given once for each repo the delegate works in.
|
|
4582
4593
|
repos: z19.array(z19.string().min(1).max(500)).max(10).optional(),
|
|
4583
4594
|
// `herd watch`, `herd unwatch` and `herd watching`: what to do, and the pull request.
|
|
@@ -9809,6 +9820,7 @@ var DispatchConnection = class {
|
|
|
9809
9820
|
const Impl = this.options.WebSocketImpl ?? WebSocket;
|
|
9810
9821
|
const socket = new Impl(target.url, { headers: target.headers });
|
|
9811
9822
|
this.socket = socket;
|
|
9823
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
9812
9824
|
socket.onopen = () => {
|
|
9813
9825
|
this.backoff = MIN_BACKOFF_MS2;
|
|
9814
9826
|
this.pinger = setInterval(() => {
|
|
@@ -9825,7 +9837,17 @@ var DispatchConnection = class {
|
|
|
9825
9837
|
return;
|
|
9826
9838
|
}
|
|
9827
9839
|
const frame = dispatchFrameSchema.safeParse(parsed);
|
|
9828
|
-
if (!frame.success)
|
|
9840
|
+
if (!frame.success) {
|
|
9841
|
+
const type = parsed?.type;
|
|
9842
|
+
const name = typeof type === "string" ? type.slice(0, 40) : "(no type)";
|
|
9843
|
+
if (dropped.has(name)) return;
|
|
9844
|
+
dropped.add(name);
|
|
9845
|
+
const where = frame.error.issues.slice(0, 3).map((issue) => issue.path.join(".") || "(frame)").join(", ");
|
|
9846
|
+
this.options.onDropped?.(
|
|
9847
|
+
`Dropped a "${name}" frame this version can't read (${where}). The service may be newer than this daemon.`
|
|
9848
|
+
);
|
|
9849
|
+
return;
|
|
9850
|
+
}
|
|
9829
9851
|
if (frame.data.type === "reconnect") {
|
|
9830
9852
|
this.backoff = MIN_BACKOFF_MS2;
|
|
9831
9853
|
socket.close(1e3, "Reconnecting as asked");
|
|
@@ -9899,6 +9921,7 @@ var Daemon = class {
|
|
|
9899
9921
|
this.options.onConnected?.();
|
|
9900
9922
|
},
|
|
9901
9923
|
onClose: (reason) => this.log(`Disconnected (${reason}). Reconnecting.`),
|
|
9924
|
+
onDropped: (line2) => this.log(line2),
|
|
9902
9925
|
...this.options.WebSocketImpl ? { WebSocketImpl: this.options.WebSocketImpl } : {}
|
|
9903
9926
|
});
|
|
9904
9927
|
this.connection.start();
|
|
@@ -11071,6 +11094,142 @@ import { join as join32 } from "node:path";
|
|
|
11071
11094
|
// src/workspaces/repos.ts
|
|
11072
11095
|
init_define_HERD_PACKAGE();
|
|
11073
11096
|
import { join as join29 } from "node:path";
|
|
11097
|
+
|
|
11098
|
+
// src/commands/repo.ts
|
|
11099
|
+
init_define_HERD_PACKAGE();
|
|
11100
|
+
import { z as z27 } from "zod";
|
|
11101
|
+
var REPO_HELP = `Usage: herd repo <url or owner/name> [folder]
|
|
11102
|
+
|
|
11103
|
+
Checks a repo out in this conversation's folder, on a branch named for the conversation, started
|
|
11104
|
+
from the freshly fetched default branch, and records it in the conversation. It comes from your
|
|
11105
|
+
owner's own checkout of the repo in the machine's root, and gets that checkout's local files git
|
|
11106
|
+
ignores, such as .env, copied in. A repo the root doesn't have is cloned into it first, with your
|
|
11107
|
+
owner's own git credentials. owner/name means one of the repos herd repos lists, or else
|
|
11108
|
+
github.com/owner/name. The folder is the repo's name, or <owner>-<name> when that's taken, unless
|
|
11109
|
+
you give one. Running it again for a repo that's already here changes nothing.`;
|
|
11110
|
+
var REPOS_HELP = `Usage: herd repos [--json]
|
|
11111
|
+
|
|
11112
|
+
Lists the repos in this conversation's folder, the repos checked out in this machine's root, and
|
|
11113
|
+
the repos your owner's herd.config.ts describes, with what each is for. Add one with herd repo.`;
|
|
11114
|
+
var repoContextSchema = z27.object({
|
|
11115
|
+
folder: z27.string(),
|
|
11116
|
+
root: z27.string(),
|
|
11117
|
+
branch: z27.string(),
|
|
11118
|
+
legacy: z27.boolean(),
|
|
11119
|
+
legacyReason: z27.string().optional(),
|
|
11120
|
+
recorded: z27.array(z27.object({ folder: z27.string(), remote: z27.string(), branch: z27.string() })),
|
|
11121
|
+
cloned: z27.array(z27.object({ key: z27.string(), remote: z27.string() })),
|
|
11122
|
+
described: z27.array(
|
|
11123
|
+
z27.object({ remote: z27.string(), key: z27.string().nullable(), about: z27.string().optional() })
|
|
11124
|
+
)
|
|
11125
|
+
});
|
|
11126
|
+
var isUrl = (text4) => /^[a-z][a-z0-9+.-]*:\/\//i.test(text4) || text4.startsWith("/") || /^[^@/\s]+@[^:/\s]+:/.test(text4);
|
|
11127
|
+
function resolveRepo(asked, context2) {
|
|
11128
|
+
const text4 = asked.trim().replace(/\/+$/, "");
|
|
11129
|
+
if (!text4) throw new HerdError(REPO_HELP);
|
|
11130
|
+
if (isUrl(text4)) return text4;
|
|
11131
|
+
const known = [
|
|
11132
|
+
...context2.recorded.map((repo) => repo.remote),
|
|
11133
|
+
...context2.cloned.map((repo) => repo.remote),
|
|
11134
|
+
...context2.described.map((repo) => repo.remote)
|
|
11135
|
+
];
|
|
11136
|
+
const wanted = text4.toLowerCase().replace(/\.git$/, "");
|
|
11137
|
+
const parts = wanted.split("/");
|
|
11138
|
+
const matches = known.filter((remote) => {
|
|
11139
|
+
const key = repoKey(remote);
|
|
11140
|
+
if (!key) return false;
|
|
11141
|
+
return parts.length === 1 ? repoName(key).toLowerCase() === wanted : key.toLowerCase() === wanted || key.toLowerCase().endsWith(`/${wanted}`);
|
|
11142
|
+
});
|
|
11143
|
+
const keys = [...new Set(matches.map((remote) => repoKey(remote)))];
|
|
11144
|
+
if (keys.length === 1 && matches[0]) return matches[0];
|
|
11145
|
+
if (keys.length > 1) {
|
|
11146
|
+
throw new HerdError(
|
|
11147
|
+
`More than one repo is called ${text4}: ${keys.join(", ")}. Name one of them.`
|
|
11148
|
+
);
|
|
11149
|
+
}
|
|
11150
|
+
if (parts.length === 1) {
|
|
11151
|
+
throw new HerdError(
|
|
11152
|
+
`herd doesn't know a repo called ${text4}. Name it as owner/name or a URL. herd repos lists the ones it knows.`
|
|
11153
|
+
);
|
|
11154
|
+
}
|
|
11155
|
+
return parts.length >= 3 && parts[0]?.includes(".") ? `https://${text4}` : `https://github.com/${text4}`;
|
|
11156
|
+
}
|
|
11157
|
+
async function context() {
|
|
11158
|
+
const reply = await fromTerminal({ op: "repos" });
|
|
11159
|
+
const parsed = repoContextSchema.safeParse(reply);
|
|
11160
|
+
if (!parsed.success)
|
|
11161
|
+
throw new HerdError("The daemon's answer about this folder wasn't readable.");
|
|
11162
|
+
return parsed.data;
|
|
11163
|
+
}
|
|
11164
|
+
async function runRepo(rest) {
|
|
11165
|
+
const args = rest.filter((arg) => arg !== "--");
|
|
11166
|
+
if (args.length === 0 || args.length > 2 || args[0] === "--help" || args[0] === "-h") {
|
|
11167
|
+
console.log(REPO_HELP);
|
|
11168
|
+
return args[0] === "--help" || args[0] === "-h" ? 0 : 1;
|
|
11169
|
+
}
|
|
11170
|
+
const [asked = "", folderName] = args;
|
|
11171
|
+
if (folderName !== void 0 && !validFolderName(folderName)) {
|
|
11172
|
+
throw new HerdError(
|
|
11173
|
+
`${folderName} can't be a folder name here. Use letters, digits, . _ or -.`
|
|
11174
|
+
);
|
|
11175
|
+
}
|
|
11176
|
+
const reply = await fromTerminal({
|
|
11177
|
+
op: "repo-add",
|
|
11178
|
+
repoAdd: { asked, ...folderName !== void 0 ? { folder: folderName } : {} }
|
|
11179
|
+
});
|
|
11180
|
+
const lines = z27.array(z27.string()).safeParse(reply.lines);
|
|
11181
|
+
if (!lines.success) throw new HerdError("The daemon's answer about this repo wasn't readable.");
|
|
11182
|
+
console.log(lines.data.join("\n"));
|
|
11183
|
+
return 0;
|
|
11184
|
+
}
|
|
11185
|
+
function formatRepos(here) {
|
|
11186
|
+
const inFolder = new Set(here.recorded.map((repo) => repoKey(repo.remote)));
|
|
11187
|
+
const lines = ["In this conversation's folder:"];
|
|
11188
|
+
if (here.recorded.length === 0) lines.push(" none yet");
|
|
11189
|
+
for (const repo of here.recorded)
|
|
11190
|
+
lines.push(` ${repo.folder} ${repo.remote} on ${repo.branch}`);
|
|
11191
|
+
const cloned = here.cloned.filter((repo) => !inFolder.has(repo.key));
|
|
11192
|
+
lines.push("", "Checked out in the root:");
|
|
11193
|
+
if (cloned.length === 0) lines.push(" none others");
|
|
11194
|
+
const about = new Map(here.described.map((repo) => [repo.key, repo.about]));
|
|
11195
|
+
for (const repo of cloned) {
|
|
11196
|
+
const said = about.get(repo.key);
|
|
11197
|
+
lines.push(` ${repo.key}${said ? ` ${said}` : ""}`);
|
|
11198
|
+
}
|
|
11199
|
+
const clonedKeys = new Set(here.cloned.map((repo) => repo.key));
|
|
11200
|
+
const described = here.described.filter((repo) => !repo.key || !clonedKeys.has(repo.key));
|
|
11201
|
+
if (described.length > 0) {
|
|
11202
|
+
lines.push("", "Described in herd.config.ts, not cloned yet:");
|
|
11203
|
+
for (const repo of described) {
|
|
11204
|
+
lines.push(` ${repo.key ?? repo.remote}${repo.about ? ` ${repo.about}` : ""}`);
|
|
11205
|
+
}
|
|
11206
|
+
}
|
|
11207
|
+
lines.push("", "Add one with herd repo <url or owner/name> [folder].");
|
|
11208
|
+
return lines.join("\n");
|
|
11209
|
+
}
|
|
11210
|
+
async function runRepos(rest) {
|
|
11211
|
+
const unknown = rest.find((arg) => arg !== "--json");
|
|
11212
|
+
if (unknown === "--help" || unknown === "-h") {
|
|
11213
|
+
console.log(REPOS_HELP);
|
|
11214
|
+
return 0;
|
|
11215
|
+
}
|
|
11216
|
+
if (unknown) throw new HerdError(`herd repos doesn't understand "${unknown}".`);
|
|
11217
|
+
const here = await context();
|
|
11218
|
+
if (rest.includes("--json")) {
|
|
11219
|
+
console.log(
|
|
11220
|
+
JSON.stringify(
|
|
11221
|
+
{ conversation: here.recorded, cloned: here.cloned, described: here.described },
|
|
11222
|
+
null,
|
|
11223
|
+
2
|
|
11224
|
+
)
|
|
11225
|
+
);
|
|
11226
|
+
} else {
|
|
11227
|
+
console.log(formatRepos(here));
|
|
11228
|
+
}
|
|
11229
|
+
return 0;
|
|
11230
|
+
}
|
|
11231
|
+
|
|
11232
|
+
// src/workspaces/repos.ts
|
|
11074
11233
|
var rootOf = (ws) => ws.options.root?.() ?? ws.options.config().root ?? defaultRoot();
|
|
11075
11234
|
var gitEnvOf = (ws) => ws.options.gitEnv?.() ?? loginShellEnv();
|
|
11076
11235
|
function knownRemotes(ws) {
|
|
@@ -11237,6 +11396,91 @@ function handoffRepos(workspace) {
|
|
|
11237
11396
|
unpushed: hasUnpushed(join29(workspace.folder, repo.folder))
|
|
11238
11397
|
}));
|
|
11239
11398
|
}
|
|
11399
|
+
async function addRepo(ws, workspace, request) {
|
|
11400
|
+
if (workspace.legacy) return { ok: false, error: LEGACY };
|
|
11401
|
+
const wanted = request.repoAdd;
|
|
11402
|
+
if (!wanted) return { ok: false, error: REPO_HELP };
|
|
11403
|
+
if (wanted.folder !== void 0 && !validFolderName(wanted.folder)) {
|
|
11404
|
+
return {
|
|
11405
|
+
ok: false,
|
|
11406
|
+
error: `${wanted.folder} can't be a folder name here. Use letters, digits, . _ or -.`
|
|
11407
|
+
};
|
|
11408
|
+
}
|
|
11409
|
+
const here = repoContextSchema.parse(repoContext(ws, workspace));
|
|
11410
|
+
let source;
|
|
11411
|
+
try {
|
|
11412
|
+
source = resolveRepo(wanted.asked, here);
|
|
11413
|
+
} catch (error) {
|
|
11414
|
+
if (error instanceof HerdError) return { ok: false, error: error.message };
|
|
11415
|
+
throw error;
|
|
11416
|
+
}
|
|
11417
|
+
const key = repoKey(source);
|
|
11418
|
+
if (!key) return { ok: false, error: `${wanted.asked} isn't a repo's URL.` };
|
|
11419
|
+
const place = folderFor(workspace.folder, key, wanted.folder);
|
|
11420
|
+
if (!place) {
|
|
11421
|
+
return {
|
|
11422
|
+
ok: false,
|
|
11423
|
+
error: wanted.folder ? `${wanted.folder} is already taken in this conversation's folder. Give another name.` : `Both ${repoName(key)} and ${repoOwner(key)}-${repoName(key)} are taken in this conversation's folder. Give a folder name: herd repo ${wanted.asked} <folder>.`
|
|
11424
|
+
};
|
|
11425
|
+
}
|
|
11426
|
+
const path = join29(workspace.folder, place.name);
|
|
11427
|
+
const already = here.recorded.find((repo) => repo.folder === place.name);
|
|
11428
|
+
if (place.existing && already) {
|
|
11429
|
+
return {
|
|
11430
|
+
ok: true,
|
|
11431
|
+
lines: [`${key} is already here, in ${path}, on ${already.branch}. Nothing changed.`]
|
|
11432
|
+
};
|
|
11433
|
+
}
|
|
11434
|
+
let added;
|
|
11435
|
+
try {
|
|
11436
|
+
added = await addWorktree({
|
|
11437
|
+
root: here.root,
|
|
11438
|
+
remote: cloneSource(source, [
|
|
11439
|
+
...here.cloned.map((repo) => repo.remote),
|
|
11440
|
+
...here.described.map((repo) => repo.remote)
|
|
11441
|
+
]),
|
|
11442
|
+
path,
|
|
11443
|
+
branch: here.branch,
|
|
11444
|
+
env: gitEnvOf(ws),
|
|
11445
|
+
...ws.options.storeLock ? { lock: ws.options.storeLock } : {}
|
|
11446
|
+
});
|
|
11447
|
+
} catch (error) {
|
|
11448
|
+
if (error instanceof StoreError) {
|
|
11449
|
+
return {
|
|
11450
|
+
ok: false,
|
|
11451
|
+
error: `Couldn't add ${key}, so nothing was added. git said:
|
|
11452
|
+
${error.message}`
|
|
11453
|
+
};
|
|
11454
|
+
}
|
|
11455
|
+
throw error;
|
|
11456
|
+
}
|
|
11457
|
+
const found = reposInFolder(workspace.folder).find((repo) => repo.folder === place.name);
|
|
11458
|
+
const recorded = await recordRepo(ws, workspace.conversationId, {
|
|
11459
|
+
folder: place.name,
|
|
11460
|
+
remote: key,
|
|
11461
|
+
branch: found?.branch ?? here.branch,
|
|
11462
|
+
...added.created ? { base: added.base } : {},
|
|
11463
|
+
fetchFailed: added.fetchFailed
|
|
11464
|
+
});
|
|
11465
|
+
const lines = [
|
|
11466
|
+
added.created ? `Added ${key} in ${path}, on ${added.branch} from ${added.base}.${added.cloned ? " herd cloned it into the root first." : ""}` : `${key} is already here, in ${path}.`
|
|
11467
|
+
];
|
|
11468
|
+
if (added.fetchFailed) {
|
|
11469
|
+
lines.push(
|
|
11470
|
+
`Couldn't fetch it, so it started from the last fetched ${added.base}: ${added.fetchFailed}`
|
|
11471
|
+
);
|
|
11472
|
+
}
|
|
11473
|
+
if (added.copied.length > 0) lines.push(describeCopied(place.name, added.copied));
|
|
11474
|
+
if (added.created) lines.push("Its own AGENTS.md or README says how to set it up.");
|
|
11475
|
+
if (added.created && confinementOn()) {
|
|
11476
|
+
ws.update(workspace.conversationId, { exitRequested: true });
|
|
11477
|
+
lines.push(
|
|
11478
|
+
"You can read and edit its files now. git in it works from your next turn, once herd restarts this session with it in reach, which it does when this turn ends."
|
|
11479
|
+
);
|
|
11480
|
+
}
|
|
11481
|
+
if (!recorded) lines.push("The conversation couldn't record it yet.");
|
|
11482
|
+
return { ok: true, lines };
|
|
11483
|
+
}
|
|
11240
11484
|
|
|
11241
11485
|
// src/workspaces/setup.ts
|
|
11242
11486
|
init_define_HERD_PACKAGE();
|
|
@@ -12107,19 +12351,21 @@ function resolveAttachments(worktree, files) {
|
|
|
12107
12351
|
// src/workers.ts
|
|
12108
12352
|
init_define_HERD_PACKAGE();
|
|
12109
12353
|
init_src();
|
|
12110
|
-
import { z as
|
|
12111
|
-
var workerListingSchema =
|
|
12112
|
-
workers:
|
|
12113
|
-
machines:
|
|
12114
|
-
|
|
12115
|
-
id:
|
|
12116
|
-
name:
|
|
12117
|
-
online:
|
|
12118
|
-
missing:
|
|
12119
|
-
configError:
|
|
12354
|
+
import { z as z28 } from "zod";
|
|
12355
|
+
var workerListingSchema = z28.object({
|
|
12356
|
+
workers: z28.array(workerSchema),
|
|
12357
|
+
machines: z28.array(
|
|
12358
|
+
z28.object({
|
|
12359
|
+
id: z28.string(),
|
|
12360
|
+
name: z28.string(),
|
|
12361
|
+
online: z28.boolean(),
|
|
12362
|
+
missing: z28.array(z28.string()),
|
|
12363
|
+
configError: z28.string().optional(),
|
|
12364
|
+
// Servers from before versions were reported send none.
|
|
12365
|
+
herdVersion: z28.string().nullable().optional()
|
|
12120
12366
|
})
|
|
12121
12367
|
),
|
|
12122
|
-
configType:
|
|
12368
|
+
configType: z28.string()
|
|
12123
12369
|
});
|
|
12124
12370
|
async function readWorkers(fetch2) {
|
|
12125
12371
|
const response = await fetch2("/api/workers");
|
|
@@ -12177,26 +12423,30 @@ function formatWorkers(listing, current = null) {
|
|
|
12177
12423
|
...machine.configError ? [` ${machine.name}: ${machine.configError}`] : []
|
|
12178
12424
|
]);
|
|
12179
12425
|
if (notes.length > 0) lines.push("", "Configuration:", ...notes);
|
|
12426
|
+
const versions = listing.machines.map(
|
|
12427
|
+
(machine) => ` ${machine.name}: ${machine.herdVersion ?? "a version too old to say"}`
|
|
12428
|
+
);
|
|
12429
|
+
if (versions.length > 0) lines.push("", "Daemon versions:", ...versions);
|
|
12180
12430
|
lines.push(
|
|
12181
12431
|
"",
|
|
12182
12432
|
"Each machine's ~/.herd/herd.config.ts says what it's for, and changes herd's defaults, marked (default). `herd workers --types` shows its type."
|
|
12183
12433
|
);
|
|
12184
12434
|
return lines.join("\n");
|
|
12185
12435
|
}
|
|
12186
|
-
var routingCheckSchema =
|
|
12187
|
-
suits:
|
|
12188
|
-
eligible:
|
|
12189
|
-
removed:
|
|
12190
|
-
questions:
|
|
12191
|
-
|
|
12436
|
+
var routingCheckSchema = z28.object({
|
|
12437
|
+
suits: z28.array(z28.object({ key: z28.string(), name: z28.string() })),
|
|
12438
|
+
eligible: z28.array(z28.object({ key: z28.string(), name: z28.string() })),
|
|
12439
|
+
removed: z28.array(z28.object({ key: z28.string(), name: z28.string(), rule: z28.string() })),
|
|
12440
|
+
questions: z28.array(
|
|
12441
|
+
z28.object({ id: z28.string(), question: z28.string(), probability: z28.number().nullable() })
|
|
12192
12442
|
),
|
|
12193
|
-
fallback:
|
|
12194
|
-
fits:
|
|
12195
|
-
|
|
12196
|
-
key:
|
|
12197
|
-
fit:
|
|
12198
|
-
statement:
|
|
12199
|
-
level:
|
|
12443
|
+
fallback: z28.string().optional(),
|
|
12444
|
+
fits: z28.array(
|
|
12445
|
+
z28.object({
|
|
12446
|
+
key: z28.string(),
|
|
12447
|
+
fit: z28.number(),
|
|
12448
|
+
statement: z28.string().optional(),
|
|
12449
|
+
level: z28.string().optional()
|
|
12200
12450
|
})
|
|
12201
12451
|
).optional()
|
|
12202
12452
|
});
|
|
@@ -12319,6 +12569,7 @@ async function answer(ws, grant, request, gone = new AbortController().signal) {
|
|
|
12319
12569
|
if (request.op === "work") return workObjects(ws, workspace, request);
|
|
12320
12570
|
if (request.op === "repos") return repoContext(ws, workspace);
|
|
12321
12571
|
if (request.op === "repo") return repoAdded(ws, workspace, request);
|
|
12572
|
+
if (request.op === "repo-add") return addRepo(ws, workspace, request);
|
|
12322
12573
|
if (request.op === "workers") return workers(ws, workspace);
|
|
12323
12574
|
if (request.op === "check") return check(ws, workspace, request);
|
|
12324
12575
|
if (request.op === "delegate") return delegate(ws, workspace, request);
|
|
@@ -14410,197 +14661,6 @@ async function runTerminalHost(rest) {
|
|
|
14410
14661
|
return 0;
|
|
14411
14662
|
}
|
|
14412
14663
|
|
|
14413
|
-
// src/commands/repo.ts
|
|
14414
|
-
init_define_HERD_PACKAGE();
|
|
14415
|
-
import { join as join37 } from "node:path";
|
|
14416
|
-
import { z as z33 } from "zod";
|
|
14417
|
-
var REPO_HELP = `Usage: herd repo <url or owner/name> [folder]
|
|
14418
|
-
|
|
14419
|
-
Checks a repo out in this conversation's folder, on a branch named for the conversation, started
|
|
14420
|
-
from the freshly fetched default branch, and records it in the conversation. It comes from your
|
|
14421
|
-
owner's own checkout of the repo in the machine's root, and gets that checkout's local files git
|
|
14422
|
-
ignores, such as .env, copied in. A repo the root doesn't have is cloned into it first, with your
|
|
14423
|
-
owner's own git credentials. owner/name means one of the repos herd repos lists, or else
|
|
14424
|
-
github.com/owner/name. The folder is the repo's name, or <owner>-<name> when that's taken, unless
|
|
14425
|
-
you give one. Running it again for a repo that's already here changes nothing.`;
|
|
14426
|
-
var REPOS_HELP = `Usage: herd repos [--json]
|
|
14427
|
-
|
|
14428
|
-
Lists the repos in this conversation's folder, the repos checked out in this machine's root, and
|
|
14429
|
-
the repos your owner's herd.config.ts describes, with what each is for. Add one with herd repo.`;
|
|
14430
|
-
var repoContextSchema = z33.object({
|
|
14431
|
-
folder: z33.string(),
|
|
14432
|
-
root: z33.string(),
|
|
14433
|
-
branch: z33.string(),
|
|
14434
|
-
legacy: z33.boolean(),
|
|
14435
|
-
legacyReason: z33.string().optional(),
|
|
14436
|
-
recorded: z33.array(z33.object({ folder: z33.string(), remote: z33.string(), branch: z33.string() })),
|
|
14437
|
-
cloned: z33.array(z33.object({ key: z33.string(), remote: z33.string() })),
|
|
14438
|
-
described: z33.array(
|
|
14439
|
-
z33.object({ remote: z33.string(), key: z33.string().nullable(), about: z33.string().optional() })
|
|
14440
|
-
)
|
|
14441
|
-
});
|
|
14442
|
-
var isUrl = (text4) => /^[a-z][a-z0-9+.-]*:\/\//i.test(text4) || text4.startsWith("/") || /^[^@/\s]+@[^:/\s]+:/.test(text4);
|
|
14443
|
-
function resolveRepo(asked, context2) {
|
|
14444
|
-
const text4 = asked.trim().replace(/\/+$/, "");
|
|
14445
|
-
if (!text4) throw new HerdError(REPO_HELP);
|
|
14446
|
-
if (isUrl(text4)) return text4;
|
|
14447
|
-
const known = [
|
|
14448
|
-
...context2.recorded.map((repo) => repo.remote),
|
|
14449
|
-
...context2.cloned.map((repo) => repo.remote),
|
|
14450
|
-
...context2.described.map((repo) => repo.remote)
|
|
14451
|
-
];
|
|
14452
|
-
const wanted = text4.toLowerCase().replace(/\.git$/, "");
|
|
14453
|
-
const parts = wanted.split("/");
|
|
14454
|
-
const matches = known.filter((remote) => {
|
|
14455
|
-
const key = repoKey(remote);
|
|
14456
|
-
if (!key) return false;
|
|
14457
|
-
return parts.length === 1 ? repoName(key).toLowerCase() === wanted : key.toLowerCase() === wanted || key.toLowerCase().endsWith(`/${wanted}`);
|
|
14458
|
-
});
|
|
14459
|
-
const keys = [...new Set(matches.map((remote) => repoKey(remote)))];
|
|
14460
|
-
if (keys.length === 1 && matches[0]) return matches[0];
|
|
14461
|
-
if (keys.length > 1) {
|
|
14462
|
-
throw new HerdError(
|
|
14463
|
-
`More than one repo is called ${text4}: ${keys.join(", ")}. Name one of them.`
|
|
14464
|
-
);
|
|
14465
|
-
}
|
|
14466
|
-
if (parts.length === 1) {
|
|
14467
|
-
throw new HerdError(
|
|
14468
|
-
`herd doesn't know a repo called ${text4}. Name it as owner/name or a URL. herd repos lists the ones it knows.`
|
|
14469
|
-
);
|
|
14470
|
-
}
|
|
14471
|
-
return parts.length >= 3 && parts[0]?.includes(".") ? `https://${text4}` : `https://github.com/${text4}`;
|
|
14472
|
-
}
|
|
14473
|
-
async function context() {
|
|
14474
|
-
const reply = await fromTerminal({ op: "repos" });
|
|
14475
|
-
const parsed = repoContextSchema.safeParse(reply);
|
|
14476
|
-
if (!parsed.success)
|
|
14477
|
-
throw new HerdError("The daemon's answer about this folder wasn't readable.");
|
|
14478
|
-
return parsed.data;
|
|
14479
|
-
}
|
|
14480
|
-
async function runRepo(rest) {
|
|
14481
|
-
const args = rest.filter((arg) => arg !== "--");
|
|
14482
|
-
if (args.length === 0 || args.length > 2 || args[0] === "--help" || args[0] === "-h") {
|
|
14483
|
-
console.log(REPO_HELP);
|
|
14484
|
-
return args[0] === "--help" || args[0] === "-h" ? 0 : 1;
|
|
14485
|
-
}
|
|
14486
|
-
const [asked = "", folderName] = args;
|
|
14487
|
-
if (folderName !== void 0 && !validFolderName(folderName)) {
|
|
14488
|
-
throw new HerdError(
|
|
14489
|
-
`${folderName} can't be a folder name here. Use letters, digits, . _ or -.`
|
|
14490
|
-
);
|
|
14491
|
-
}
|
|
14492
|
-
const here = await context();
|
|
14493
|
-
if (here.legacy) throw new HerdError(here.legacyReason ?? "This conversation can't add repos.");
|
|
14494
|
-
const source = resolveRepo(asked, here);
|
|
14495
|
-
const key = repoKey(source);
|
|
14496
|
-
if (!key) throw new HerdError(`${asked} isn't a repo's URL.`);
|
|
14497
|
-
const place = folderFor(here.folder, key, folderName);
|
|
14498
|
-
if (!place) {
|
|
14499
|
-
throw new HerdError(
|
|
14500
|
-
folderName ? `${folderName} is already taken in this conversation's folder. Give another name.` : `Both ${repoName(key)} and ${repoOwner(key)}-${repoName(key)} are taken in this conversation's folder. Give a folder name: herd repo ${asked} <folder>.`
|
|
14501
|
-
);
|
|
14502
|
-
}
|
|
14503
|
-
const path = join37(here.folder, place.name);
|
|
14504
|
-
const recorded = here.recorded.some((repo) => repo.folder === place.name);
|
|
14505
|
-
if (place.existing && recorded) {
|
|
14506
|
-
const branch = here.recorded.find((repo) => repo.folder === place.name)?.branch ?? here.branch;
|
|
14507
|
-
console.log(`${key} is already here, in ${path}, on ${branch}. Nothing changed.`);
|
|
14508
|
-
return 0;
|
|
14509
|
-
}
|
|
14510
|
-
let added;
|
|
14511
|
-
try {
|
|
14512
|
-
added = await addWorktree({
|
|
14513
|
-
root: here.root,
|
|
14514
|
-
remote: cloneSource(source, [
|
|
14515
|
-
...here.cloned.map((repo) => repo.remote),
|
|
14516
|
-
...here.described.map((repo) => repo.remote)
|
|
14517
|
-
]),
|
|
14518
|
-
path,
|
|
14519
|
-
branch: here.branch,
|
|
14520
|
-
env: process.env
|
|
14521
|
-
});
|
|
14522
|
-
} catch (error) {
|
|
14523
|
-
if (error instanceof StoreError) {
|
|
14524
|
-
throw new HerdError(`Couldn't add ${key}, so nothing was added. git said:
|
|
14525
|
-
${error.message}`);
|
|
14526
|
-
}
|
|
14527
|
-
throw error;
|
|
14528
|
-
}
|
|
14529
|
-
let note = "";
|
|
14530
|
-
try {
|
|
14531
|
-
const reply = await fromTerminal({
|
|
14532
|
-
op: "repo",
|
|
14533
|
-
repo: {
|
|
14534
|
-
remote: key,
|
|
14535
|
-
folder: place.name,
|
|
14536
|
-
...added.created ? { base: added.base } : {},
|
|
14537
|
-
...added.fetchFailed ? { fetchFailed: added.fetchFailed } : {}
|
|
14538
|
-
}
|
|
14539
|
-
});
|
|
14540
|
-
if (reply.recorded === false) note = " The conversation couldn't record it yet.";
|
|
14541
|
-
} catch (error) {
|
|
14542
|
-
note = ` The conversation couldn't record it: ${error instanceof Error ? error.message : error}`;
|
|
14543
|
-
}
|
|
14544
|
-
const lines = [
|
|
14545
|
-
added.created ? `Added ${key} in ${path}, on ${added.branch} from ${added.base}.${added.cloned ? " herd cloned it into the root first." : ""}` : `${key} is already here, in ${path}.`
|
|
14546
|
-
];
|
|
14547
|
-
if (added.fetchFailed) {
|
|
14548
|
-
lines.push(
|
|
14549
|
-
`Couldn't fetch it, so it started from the last fetched ${added.base}: ${added.fetchFailed}`
|
|
14550
|
-
);
|
|
14551
|
-
}
|
|
14552
|
-
if (added.copied.length > 0) lines.push(describeCopied(place.name, added.copied));
|
|
14553
|
-
if (added.created) lines.push("Its own AGENTS.md or README says how to set it up.");
|
|
14554
|
-
console.log(lines.join("\n") + note);
|
|
14555
|
-
return 0;
|
|
14556
|
-
}
|
|
14557
|
-
function formatRepos(here) {
|
|
14558
|
-
const inFolder = new Set(here.recorded.map((repo) => repoKey(repo.remote)));
|
|
14559
|
-
const lines = ["In this conversation's folder:"];
|
|
14560
|
-
if (here.recorded.length === 0) lines.push(" none yet");
|
|
14561
|
-
for (const repo of here.recorded)
|
|
14562
|
-
lines.push(` ${repo.folder} ${repo.remote} on ${repo.branch}`);
|
|
14563
|
-
const cloned = here.cloned.filter((repo) => !inFolder.has(repo.key));
|
|
14564
|
-
lines.push("", "Checked out in the root:");
|
|
14565
|
-
if (cloned.length === 0) lines.push(" none others");
|
|
14566
|
-
const about = new Map(here.described.map((repo) => [repo.key, repo.about]));
|
|
14567
|
-
for (const repo of cloned) {
|
|
14568
|
-
const said = about.get(repo.key);
|
|
14569
|
-
lines.push(` ${repo.key}${said ? ` ${said}` : ""}`);
|
|
14570
|
-
}
|
|
14571
|
-
const clonedKeys = new Set(here.cloned.map((repo) => repo.key));
|
|
14572
|
-
const described = here.described.filter((repo) => !repo.key || !clonedKeys.has(repo.key));
|
|
14573
|
-
if (described.length > 0) {
|
|
14574
|
-
lines.push("", "Described in herd.config.ts, not cloned yet:");
|
|
14575
|
-
for (const repo of described) {
|
|
14576
|
-
lines.push(` ${repo.key ?? repo.remote}${repo.about ? ` ${repo.about}` : ""}`);
|
|
14577
|
-
}
|
|
14578
|
-
}
|
|
14579
|
-
lines.push("", "Add one with herd repo <url or owner/name> [folder].");
|
|
14580
|
-
return lines.join("\n");
|
|
14581
|
-
}
|
|
14582
|
-
async function runRepos(rest) {
|
|
14583
|
-
const unknown = rest.find((arg) => arg !== "--json");
|
|
14584
|
-
if (unknown === "--help" || unknown === "-h") {
|
|
14585
|
-
console.log(REPOS_HELP);
|
|
14586
|
-
return 0;
|
|
14587
|
-
}
|
|
14588
|
-
if (unknown) throw new HerdError(`herd repos doesn't understand "${unknown}".`);
|
|
14589
|
-
const here = await context();
|
|
14590
|
-
if (rest.includes("--json")) {
|
|
14591
|
-
console.log(
|
|
14592
|
-
JSON.stringify(
|
|
14593
|
-
{ conversation: here.recorded, cloned: here.cloned, described: here.described },
|
|
14594
|
-
null,
|
|
14595
|
-
2
|
|
14596
|
-
)
|
|
14597
|
-
);
|
|
14598
|
-
} else {
|
|
14599
|
-
console.log(formatRepos(here));
|
|
14600
|
-
}
|
|
14601
|
-
return 0;
|
|
14602
|
-
}
|
|
14603
|
-
|
|
14604
14664
|
// src/commands/send.ts
|
|
14605
14665
|
init_define_HERD_PACKAGE();
|
|
14606
14666
|
init_src();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herdingchats/friday",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.645",
|
|
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": "768ab1770e2447667409893dd18699acc30e2c03"
|
|
26
26
|
}
|