@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.
Files changed (2) hide show
  1. package/herd.mjs +299 -239
  2. 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.635", gitHead: "e7cd8275c714def3c588160cdc85614626bc228d" };
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 z28 } from "zod";
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: z28.object({
2433
- states: z28.array(conversationStateSchema).min(1).default(["stuck", "working", "waiting", "done"]),
2434
- limit: z28.number().int().min(1).max(BOARD_ITEMS).default(4),
2435
- show: z28.array(z28.enum(DETAILS)).default([...DETAILS])
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 z29 } from "zod";
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: z29.object({
2500
- show: z29.array(z29.enum(COUNTS)).min(1).max(5).default([...COUNTS])
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 z30 } from "zod";
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: z30.object({
2565
- title: z30.string().min(1).max(40).default(`@${BOT_NAME}`),
2566
- summary: z30.boolean().default(true)
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 z31 } from "zod";
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: z31.object({ limit: z31.number().int().min(1).max(8).default(3) }),
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 z32 } from "zod";
2735
+ import { z as z33 } from "zod";
2733
2736
  function readComposition(stored) {
2734
- const shape = z32.object({ widgets: z32.array(placedWidgetSchema) }).safeParse(stored);
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 = z32.object({
2750
- kind: z32.string().min(1),
2751
- options: z32.record(z32.string(), z32.unknown()).default({})
2752
+ placedWidgetSchema = z33.object({
2753
+ kind: z33.string().min(1),
2754
+ options: z33.record(z33.string(), z33.unknown()).default({})
2752
2755
  });
2753
- compositionSchema = z32.object({ widgets: z32.array(placedWidgetSchema).max(MAX_WIDGETS) }).superRefine((composition, context2) => {
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) return;
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 z27 } from "zod";
12111
- var workerListingSchema = z27.object({
12112
- workers: z27.array(workerSchema),
12113
- machines: z27.array(
12114
- z27.object({
12115
- id: z27.string(),
12116
- name: z27.string(),
12117
- online: z27.boolean(),
12118
- missing: z27.array(z27.string()),
12119
- configError: z27.string().optional()
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: z27.string()
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 = z27.object({
12187
- suits: z27.array(z27.object({ key: z27.string(), name: z27.string() })),
12188
- eligible: z27.array(z27.object({ key: z27.string(), name: z27.string() })),
12189
- removed: z27.array(z27.object({ key: z27.string(), name: z27.string(), rule: z27.string() })),
12190
- questions: z27.array(
12191
- z27.object({ id: z27.string(), question: z27.string(), probability: z27.number().nullable() })
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: z27.string().optional(),
12194
- fits: z27.array(
12195
- z27.object({
12196
- key: z27.string(),
12197
- fit: z27.number(),
12198
- statement: z27.string().optional(),
12199
- level: z27.string().optional()
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.635",
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": "e7cd8275c714def3c588160cdc85614626bc228d"
25
+ "gitHead": "768ab1770e2447667409893dd18699acc30e2c03"
26
26
  }