@herdingchats/friday 0.1.635 → 0.1.641

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 +273 -236
  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.641", gitHead: "e1411e26f2203efeb542823bfefded0e5dd8b296" };
22
22
  }
23
23
  });
24
24
 
@@ -2385,7 +2385,7 @@ var init_widget = __esm({
2385
2385
  });
2386
2386
 
2387
2387
  // ../common-board/src/widgets/conversations.tsx
2388
- import { z as z28 } from "zod";
2388
+ import { z as z29 } from "zod";
2389
2389
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2390
2390
  function badge(item) {
2391
2391
  if (pullRequestBadge(item)) {
@@ -2429,10 +2429,10 @@ var init_conversations = __esm({
2429
2429
  conversations2 = defineWidget({
2430
2430
  kind: "conversations",
2431
2431
  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])
2432
+ options: z29.object({
2433
+ states: z29.array(conversationStateSchema).min(1).default(["stuck", "working", "waiting", "done"]),
2434
+ limit: z29.number().int().min(1).max(BOARD_ITEMS).default(4),
2435
+ show: z29.array(z29.enum(DETAILS)).default([...DETAILS])
2436
2436
  }),
2437
2437
  reads: ["items", "machines"],
2438
2438
  Component: ({ board, options, theme, now }) => {
@@ -2476,7 +2476,7 @@ var init_conversations = __esm({
2476
2476
  });
2477
2477
 
2478
2478
  // ../common-board/src/widgets/counts.tsx
2479
- import { z as z29 } from "zod";
2479
+ import { z as z30 } from "zod";
2480
2480
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2481
2481
  var COUNTS, LABELS2, counts;
2482
2482
  var init_counts = __esm({
@@ -2496,8 +2496,8 @@ var init_counts = __esm({
2496
2496
  counts = defineWidget({
2497
2497
  kind: "counts",
2498
2498
  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])
2499
+ options: z30.object({
2500
+ show: z30.array(z30.enum(COUNTS)).min(1).max(5).default([...COUNTS])
2501
2501
  }),
2502
2502
  reads: ["counts", "doneToday"],
2503
2503
  Component: ({ board, options, theme }) => {
@@ -2536,7 +2536,7 @@ var init_counts = __esm({
2536
2536
  });
2537
2537
 
2538
2538
  // ../common-board/src/widgets/header.tsx
2539
- import { z as z30 } from "zod";
2539
+ import { z as z31 } from "zod";
2540
2540
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2541
2541
  function summary(board) {
2542
2542
  const parts = [
@@ -2561,9 +2561,9 @@ var init_header = __esm({
2561
2561
  header = defineWidget({
2562
2562
  kind: "header",
2563
2563
  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)
2564
+ options: z31.object({
2565
+ title: z31.string().min(1).max(40).default(`@${BOT_NAME}`),
2566
+ summary: z31.boolean().default(true)
2567
2567
  }),
2568
2568
  reads: ["counts"],
2569
2569
  Component: ({ board, options, theme, now }) => {
@@ -2581,7 +2581,7 @@ var init_header = __esm({
2581
2581
  });
2582
2582
 
2583
2583
  // ../common-board/src/widgets/machines.tsx
2584
- import { z as z31 } from "zod";
2584
+ import { z as z32 } from "zod";
2585
2585
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2586
2586
  function usageText(worker) {
2587
2587
  const usage = worker.usage;
@@ -2612,7 +2612,7 @@ var init_machines2 = __esm({
2612
2612
  machines = defineWidget({
2613
2613
  kind: "machines",
2614
2614
  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) }),
2615
+ options: z32.object({ limit: z32.number().int().min(1).max(8).default(3) }),
2616
2616
  reads: ["machines"],
2617
2617
  Component: ({ board, options, theme }) => {
2618
2618
  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 +2729,9 @@ var init_board2 = __esm({
2729
2729
  });
2730
2730
 
2731
2731
  // ../common-board/src/composition.ts
2732
- import { z as z32 } from "zod";
2732
+ import { z as z33 } from "zod";
2733
2733
  function readComposition(stored) {
2734
- const shape = z32.object({ widgets: z32.array(placedWidgetSchema) }).safeParse(stored);
2734
+ const shape = z33.object({ widgets: z33.array(placedWidgetSchema) }).safeParse(stored);
2735
2735
  if (!shape.success) return DEFAULT_COMPOSITION;
2736
2736
  const known = shape.data.widgets.filter(
2737
2737
  (widget) => WIDGETS[widget.kind]?.options.safeParse(widget.options).success
@@ -2746,11 +2746,11 @@ var init_composition = __esm({
2746
2746
  init_registry();
2747
2747
  MAX_WIDGETS = 12;
2748
2748
  MAX_COMPOSITION_BYTES = 4096;
2749
- placedWidgetSchema = z32.object({
2750
- kind: z32.string().min(1),
2751
- options: z32.record(z32.string(), z32.unknown()).default({})
2749
+ placedWidgetSchema = z33.object({
2750
+ kind: z33.string().min(1),
2751
+ options: z33.record(z33.string(), z33.unknown()).default({})
2752
2752
  });
2753
- compositionSchema = z32.object({ widgets: z32.array(placedWidgetSchema).max(MAX_WIDGETS) }).superRefine((composition, context2) => {
2753
+ compositionSchema = z33.object({ widgets: z33.array(placedWidgetSchema).max(MAX_WIDGETS) }).superRefine((composition, context2) => {
2754
2754
  if (JSON.stringify(composition).length > MAX_COMPOSITION_BYTES) {
2755
2755
  context2.addIssue({
2756
2756
  code: "custom",
@@ -4567,6 +4567,7 @@ var localRequestSchema = z19.object({
4567
4567
  "watch",
4568
4568
  "repos",
4569
4569
  "repo",
4570
+ "repo-add",
4570
4571
  "activate"
4571
4572
  ]),
4572
4573
  // The key of the agent's start, from `HERD_START`, which `herd inbox` proves the start with.
@@ -4578,6 +4579,11 @@ var localRequestSchema = z19.object({
4578
4579
  base: z19.string().min(1).max(300).optional(),
4579
4580
  fetchFailed: z19.string().max(1e3).optional()
4580
4581
  }).optional(),
4582
+ // `herd repo`: what the agent named and the folder it asked for, for the daemon to add.
4583
+ repoAdd: z19.object({
4584
+ asked: z19.string().min(1).max(500),
4585
+ folder: z19.string().min(1).max(100).optional()
4586
+ }).optional(),
4581
4587
  // `herd delegate --repo`, given once for each repo the delegate works in.
4582
4588
  repos: z19.array(z19.string().min(1).max(500)).max(10).optional(),
4583
4589
  // `herd watch`, `herd unwatch` and `herd watching`: what to do, and the pull request.
@@ -11071,6 +11077,142 @@ import { join as join32 } from "node:path";
11071
11077
  // src/workspaces/repos.ts
11072
11078
  init_define_HERD_PACKAGE();
11073
11079
  import { join as join29 } from "node:path";
11080
+
11081
+ // src/commands/repo.ts
11082
+ init_define_HERD_PACKAGE();
11083
+ import { z as z27 } from "zod";
11084
+ var REPO_HELP = `Usage: herd repo <url or owner/name> [folder]
11085
+
11086
+ Checks a repo out in this conversation's folder, on a branch named for the conversation, started
11087
+ from the freshly fetched default branch, and records it in the conversation. It comes from your
11088
+ owner's own checkout of the repo in the machine's root, and gets that checkout's local files git
11089
+ ignores, such as .env, copied in. A repo the root doesn't have is cloned into it first, with your
11090
+ owner's own git credentials. owner/name means one of the repos herd repos lists, or else
11091
+ github.com/owner/name. The folder is the repo's name, or <owner>-<name> when that's taken, unless
11092
+ you give one. Running it again for a repo that's already here changes nothing.`;
11093
+ var REPOS_HELP = `Usage: herd repos [--json]
11094
+
11095
+ Lists the repos in this conversation's folder, the repos checked out in this machine's root, and
11096
+ the repos your owner's herd.config.ts describes, with what each is for. Add one with herd repo.`;
11097
+ var repoContextSchema = z27.object({
11098
+ folder: z27.string(),
11099
+ root: z27.string(),
11100
+ branch: z27.string(),
11101
+ legacy: z27.boolean(),
11102
+ legacyReason: z27.string().optional(),
11103
+ recorded: z27.array(z27.object({ folder: z27.string(), remote: z27.string(), branch: z27.string() })),
11104
+ cloned: z27.array(z27.object({ key: z27.string(), remote: z27.string() })),
11105
+ described: z27.array(
11106
+ z27.object({ remote: z27.string(), key: z27.string().nullable(), about: z27.string().optional() })
11107
+ )
11108
+ });
11109
+ var isUrl = (text4) => /^[a-z][a-z0-9+.-]*:\/\//i.test(text4) || text4.startsWith("/") || /^[^@/\s]+@[^:/\s]+:/.test(text4);
11110
+ function resolveRepo(asked, context2) {
11111
+ const text4 = asked.trim().replace(/\/+$/, "");
11112
+ if (!text4) throw new HerdError(REPO_HELP);
11113
+ if (isUrl(text4)) return text4;
11114
+ const known = [
11115
+ ...context2.recorded.map((repo) => repo.remote),
11116
+ ...context2.cloned.map((repo) => repo.remote),
11117
+ ...context2.described.map((repo) => repo.remote)
11118
+ ];
11119
+ const wanted = text4.toLowerCase().replace(/\.git$/, "");
11120
+ const parts = wanted.split("/");
11121
+ const matches = known.filter((remote) => {
11122
+ const key = repoKey(remote);
11123
+ if (!key) return false;
11124
+ return parts.length === 1 ? repoName(key).toLowerCase() === wanted : key.toLowerCase() === wanted || key.toLowerCase().endsWith(`/${wanted}`);
11125
+ });
11126
+ const keys = [...new Set(matches.map((remote) => repoKey(remote)))];
11127
+ if (keys.length === 1 && matches[0]) return matches[0];
11128
+ if (keys.length > 1) {
11129
+ throw new HerdError(
11130
+ `More than one repo is called ${text4}: ${keys.join(", ")}. Name one of them.`
11131
+ );
11132
+ }
11133
+ if (parts.length === 1) {
11134
+ throw new HerdError(
11135
+ `herd doesn't know a repo called ${text4}. Name it as owner/name or a URL. herd repos lists the ones it knows.`
11136
+ );
11137
+ }
11138
+ return parts.length >= 3 && parts[0]?.includes(".") ? `https://${text4}` : `https://github.com/${text4}`;
11139
+ }
11140
+ async function context() {
11141
+ const reply = await fromTerminal({ op: "repos" });
11142
+ const parsed = repoContextSchema.safeParse(reply);
11143
+ if (!parsed.success)
11144
+ throw new HerdError("The daemon's answer about this folder wasn't readable.");
11145
+ return parsed.data;
11146
+ }
11147
+ async function runRepo(rest) {
11148
+ const args = rest.filter((arg) => arg !== "--");
11149
+ if (args.length === 0 || args.length > 2 || args[0] === "--help" || args[0] === "-h") {
11150
+ console.log(REPO_HELP);
11151
+ return args[0] === "--help" || args[0] === "-h" ? 0 : 1;
11152
+ }
11153
+ const [asked = "", folderName] = args;
11154
+ if (folderName !== void 0 && !validFolderName(folderName)) {
11155
+ throw new HerdError(
11156
+ `${folderName} can't be a folder name here. Use letters, digits, . _ or -.`
11157
+ );
11158
+ }
11159
+ const reply = await fromTerminal({
11160
+ op: "repo-add",
11161
+ repoAdd: { asked, ...folderName !== void 0 ? { folder: folderName } : {} }
11162
+ });
11163
+ const lines = z27.array(z27.string()).safeParse(reply.lines);
11164
+ if (!lines.success) throw new HerdError("The daemon's answer about this repo wasn't readable.");
11165
+ console.log(lines.data.join("\n"));
11166
+ return 0;
11167
+ }
11168
+ function formatRepos(here) {
11169
+ const inFolder = new Set(here.recorded.map((repo) => repoKey(repo.remote)));
11170
+ const lines = ["In this conversation's folder:"];
11171
+ if (here.recorded.length === 0) lines.push(" none yet");
11172
+ for (const repo of here.recorded)
11173
+ lines.push(` ${repo.folder} ${repo.remote} on ${repo.branch}`);
11174
+ const cloned = here.cloned.filter((repo) => !inFolder.has(repo.key));
11175
+ lines.push("", "Checked out in the root:");
11176
+ if (cloned.length === 0) lines.push(" none others");
11177
+ const about = new Map(here.described.map((repo) => [repo.key, repo.about]));
11178
+ for (const repo of cloned) {
11179
+ const said = about.get(repo.key);
11180
+ lines.push(` ${repo.key}${said ? ` ${said}` : ""}`);
11181
+ }
11182
+ const clonedKeys = new Set(here.cloned.map((repo) => repo.key));
11183
+ const described = here.described.filter((repo) => !repo.key || !clonedKeys.has(repo.key));
11184
+ if (described.length > 0) {
11185
+ lines.push("", "Described in herd.config.ts, not cloned yet:");
11186
+ for (const repo of described) {
11187
+ lines.push(` ${repo.key ?? repo.remote}${repo.about ? ` ${repo.about}` : ""}`);
11188
+ }
11189
+ }
11190
+ lines.push("", "Add one with herd repo <url or owner/name> [folder].");
11191
+ return lines.join("\n");
11192
+ }
11193
+ async function runRepos(rest) {
11194
+ const unknown = rest.find((arg) => arg !== "--json");
11195
+ if (unknown === "--help" || unknown === "-h") {
11196
+ console.log(REPOS_HELP);
11197
+ return 0;
11198
+ }
11199
+ if (unknown) throw new HerdError(`herd repos doesn't understand "${unknown}".`);
11200
+ const here = await context();
11201
+ if (rest.includes("--json")) {
11202
+ console.log(
11203
+ JSON.stringify(
11204
+ { conversation: here.recorded, cloned: here.cloned, described: here.described },
11205
+ null,
11206
+ 2
11207
+ )
11208
+ );
11209
+ } else {
11210
+ console.log(formatRepos(here));
11211
+ }
11212
+ return 0;
11213
+ }
11214
+
11215
+ // src/workspaces/repos.ts
11074
11216
  var rootOf = (ws) => ws.options.root?.() ?? ws.options.config().root ?? defaultRoot();
11075
11217
  var gitEnvOf = (ws) => ws.options.gitEnv?.() ?? loginShellEnv();
11076
11218
  function knownRemotes(ws) {
@@ -11237,6 +11379,91 @@ function handoffRepos(workspace) {
11237
11379
  unpushed: hasUnpushed(join29(workspace.folder, repo.folder))
11238
11380
  }));
11239
11381
  }
11382
+ async function addRepo(ws, workspace, request) {
11383
+ if (workspace.legacy) return { ok: false, error: LEGACY };
11384
+ const wanted = request.repoAdd;
11385
+ if (!wanted) return { ok: false, error: REPO_HELP };
11386
+ if (wanted.folder !== void 0 && !validFolderName(wanted.folder)) {
11387
+ return {
11388
+ ok: false,
11389
+ error: `${wanted.folder} can't be a folder name here. Use letters, digits, . _ or -.`
11390
+ };
11391
+ }
11392
+ const here = repoContextSchema.parse(repoContext(ws, workspace));
11393
+ let source;
11394
+ try {
11395
+ source = resolveRepo(wanted.asked, here);
11396
+ } catch (error) {
11397
+ if (error instanceof HerdError) return { ok: false, error: error.message };
11398
+ throw error;
11399
+ }
11400
+ const key = repoKey(source);
11401
+ if (!key) return { ok: false, error: `${wanted.asked} isn't a repo's URL.` };
11402
+ const place = folderFor(workspace.folder, key, wanted.folder);
11403
+ if (!place) {
11404
+ return {
11405
+ ok: false,
11406
+ 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>.`
11407
+ };
11408
+ }
11409
+ const path = join29(workspace.folder, place.name);
11410
+ const already = here.recorded.find((repo) => repo.folder === place.name);
11411
+ if (place.existing && already) {
11412
+ return {
11413
+ ok: true,
11414
+ lines: [`${key} is already here, in ${path}, on ${already.branch}. Nothing changed.`]
11415
+ };
11416
+ }
11417
+ let added;
11418
+ try {
11419
+ added = await addWorktree({
11420
+ root: here.root,
11421
+ remote: cloneSource(source, [
11422
+ ...here.cloned.map((repo) => repo.remote),
11423
+ ...here.described.map((repo) => repo.remote)
11424
+ ]),
11425
+ path,
11426
+ branch: here.branch,
11427
+ env: gitEnvOf(ws),
11428
+ ...ws.options.storeLock ? { lock: ws.options.storeLock } : {}
11429
+ });
11430
+ } catch (error) {
11431
+ if (error instanceof StoreError) {
11432
+ return {
11433
+ ok: false,
11434
+ error: `Couldn't add ${key}, so nothing was added. git said:
11435
+ ${error.message}`
11436
+ };
11437
+ }
11438
+ throw error;
11439
+ }
11440
+ const found = reposInFolder(workspace.folder).find((repo) => repo.folder === place.name);
11441
+ const recorded = await recordRepo(ws, workspace.conversationId, {
11442
+ folder: place.name,
11443
+ remote: key,
11444
+ branch: found?.branch ?? here.branch,
11445
+ ...added.created ? { base: added.base } : {},
11446
+ fetchFailed: added.fetchFailed
11447
+ });
11448
+ const lines = [
11449
+ 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}.`
11450
+ ];
11451
+ if (added.fetchFailed) {
11452
+ lines.push(
11453
+ `Couldn't fetch it, so it started from the last fetched ${added.base}: ${added.fetchFailed}`
11454
+ );
11455
+ }
11456
+ if (added.copied.length > 0) lines.push(describeCopied(place.name, added.copied));
11457
+ if (added.created) lines.push("Its own AGENTS.md or README says how to set it up.");
11458
+ if (added.created && confinementOn()) {
11459
+ ws.update(workspace.conversationId, { exitRequested: true });
11460
+ lines.push(
11461
+ "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."
11462
+ );
11463
+ }
11464
+ if (!recorded) lines.push("The conversation couldn't record it yet.");
11465
+ return { ok: true, lines };
11466
+ }
11240
11467
 
11241
11468
  // src/workspaces/setup.ts
11242
11469
  init_define_HERD_PACKAGE();
@@ -12107,19 +12334,19 @@ function resolveAttachments(worktree, files) {
12107
12334
  // src/workers.ts
12108
12335
  init_define_HERD_PACKAGE();
12109
12336
  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()
12337
+ import { z as z28 } from "zod";
12338
+ var workerListingSchema = z28.object({
12339
+ workers: z28.array(workerSchema),
12340
+ machines: z28.array(
12341
+ z28.object({
12342
+ id: z28.string(),
12343
+ name: z28.string(),
12344
+ online: z28.boolean(),
12345
+ missing: z28.array(z28.string()),
12346
+ configError: z28.string().optional()
12120
12347
  })
12121
12348
  ),
12122
- configType: z27.string()
12349
+ configType: z28.string()
12123
12350
  });
12124
12351
  async function readWorkers(fetch2) {
12125
12352
  const response = await fetch2("/api/workers");
@@ -12183,20 +12410,20 @@ function formatWorkers(listing, current = null) {
12183
12410
  );
12184
12411
  return lines.join("\n");
12185
12412
  }
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() })
12413
+ var routingCheckSchema = z28.object({
12414
+ suits: z28.array(z28.object({ key: z28.string(), name: z28.string() })),
12415
+ eligible: z28.array(z28.object({ key: z28.string(), name: z28.string() })),
12416
+ removed: z28.array(z28.object({ key: z28.string(), name: z28.string(), rule: z28.string() })),
12417
+ questions: z28.array(
12418
+ z28.object({ id: z28.string(), question: z28.string(), probability: z28.number().nullable() })
12192
12419
  ),
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()
12420
+ fallback: z28.string().optional(),
12421
+ fits: z28.array(
12422
+ z28.object({
12423
+ key: z28.string(),
12424
+ fit: z28.number(),
12425
+ statement: z28.string().optional(),
12426
+ level: z28.string().optional()
12200
12427
  })
12201
12428
  ).optional()
12202
12429
  });
@@ -12319,6 +12546,7 @@ async function answer(ws, grant, request, gone = new AbortController().signal) {
12319
12546
  if (request.op === "work") return workObjects(ws, workspace, request);
12320
12547
  if (request.op === "repos") return repoContext(ws, workspace);
12321
12548
  if (request.op === "repo") return repoAdded(ws, workspace, request);
12549
+ if (request.op === "repo-add") return addRepo(ws, workspace, request);
12322
12550
  if (request.op === "workers") return workers(ws, workspace);
12323
12551
  if (request.op === "check") return check(ws, workspace, request);
12324
12552
  if (request.op === "delegate") return delegate(ws, workspace, request);
@@ -14410,197 +14638,6 @@ async function runTerminalHost(rest) {
14410
14638
  return 0;
14411
14639
  }
14412
14640
 
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
14641
  // src/commands/send.ts
14605
14642
  init_define_HERD_PACKAGE();
14606
14643
  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.641",
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": "e1411e26f2203efeb542823bfefded0e5dd8b296"
26
26
  }