@herdingchats/friday 0.1.675 → 0.1.688

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 +125 -42
  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.675", gitHead: "c9f287481b3f9f19c6c089abb87783ca7592ff5f" };
21
+ define_HERD_PACKAGE_default = { version: "0.1.688", gitHead: "a284c931b34fdaa76115c8d2c91da8b2903d19ff" };
22
22
  }
23
23
  });
24
24
 
@@ -3309,7 +3309,7 @@ import { spawn as spawn8 } from "node:child_process";
3309
3309
  import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
3310
3310
  import { homedir as homedir5 } from "node:os";
3311
3311
  import { dirname as dirname10, join as join16 } from "node:path";
3312
- import { createInterface } from "node:readline/promises";
3312
+ import { createInterface as createInterface2 } from "node:readline/promises";
3313
3313
  import { z as z23 } from "zod";
3314
3314
 
3315
3315
  // src/auth.ts
@@ -3368,16 +3368,93 @@ async function postForm(url, fields) {
3368
3368
  body: new URLSearchParams(fields)
3369
3369
  });
3370
3370
  }
3371
- async function login(openBrowser2, { api = apiUrl(), timeoutMs = 10 * 60 * 1e3 } = {}) {
3372
- const discovered = await fetch(`${api}/.well-known/oauth-authorization-server`).catch(() => null);
3373
- const metadata = metadataSchema.safeParse(await discovered?.json().catch(() => null));
3374
- if (!metadata.success) throw new HerdError(`Couldn't find where to sign in for ${api}.`);
3375
- const meta = metadata.data;
3371
+ var deviceRedirectFor = (api) => {
3372
+ const url = new URL(api);
3373
+ url.hostname = url.hostname.replace(/^api\./, "chat.");
3374
+ return `${url.origin}/sign-in/code`;
3375
+ };
3376
+ var wantsDevice = (args, env = process.env) => args.includes("--device") || Boolean(env.SSH_CONNECTION || env.SSH_TTY);
3377
+ function parsePasted(pasted) {
3378
+ const text4 = pasted.trim();
3379
+ if (/^https?:\/\//.test(text4)) {
3380
+ const url = new URL(text4);
3381
+ const code = url.searchParams.get("code");
3382
+ return code ? { state: url.searchParams.get("state") ?? "", code } : null;
3383
+ }
3384
+ const dot = text4.indexOf(".");
3385
+ return dot > 0 && dot < text4.length - 1 ? { state: text4.slice(0, dot), code: text4.slice(dot + 1) } : null;
3386
+ }
3387
+ async function loopback(openBrowser2, timeoutMs) {
3376
3388
  const server = createServer();
3377
3389
  await new Promise((resolve8) => server.listen(0, "127.0.0.1", resolve8));
3378
3390
  const address = server.address();
3379
3391
  const port = typeof address === "object" && address ? address.port : 0;
3380
3392
  const redirectUri = `http://127.0.0.1:${port}/callback`;
3393
+ return {
3394
+ redirectUri,
3395
+ close: () => server.close(),
3396
+ code: async (authorize, state) => {
3397
+ const code = new Promise((resolve8, reject) => {
3398
+ const timer = setTimeout(
3399
+ () => reject(new HerdError("Nobody signed in within 10 minutes. Run herd login again.")),
3400
+ timeoutMs
3401
+ );
3402
+ server.on("request", (req, res) => {
3403
+ const url = new URL(req.url ?? "/", redirectUri);
3404
+ if (url.pathname !== "/callback") return void res.writeHead(404).end();
3405
+ clearTimeout(timer);
3406
+ const error = url.searchParams.get("error");
3407
+ const ok = !error && url.searchParams.get("state") === state;
3408
+ res.writeHead(ok ? 200 : 400, { "Content-Type": "text/plain" }).end(ok ? "Signed in to herd. You can close this tab." : "Sign-in didn't complete.");
3409
+ if (error) return reject(new HerdError(`Sign-in was refused: ${error}.`));
3410
+ if (!ok) return reject(new HerdError("Sign-in came back for a different request."));
3411
+ resolve8(url.searchParams.get("code") ?? "");
3412
+ });
3413
+ });
3414
+ code.catch(() => {
3415
+ });
3416
+ await openBrowser2(authorize);
3417
+ return code;
3418
+ }
3419
+ };
3420
+ }
3421
+ function device(redirectUri, print, readCode) {
3422
+ return {
3423
+ redirectUri,
3424
+ close: () => {
3425
+ },
3426
+ code: async (authorize, state) => {
3427
+ print(`Open this link on any device and sign in:
3428
+
3429
+ ${authorize}
3430
+ `);
3431
+ for (let tries = 0; tries < 3; tries += 1) {
3432
+ const pasted = parsePasted(await readCode("Paste the code it shows: "));
3433
+ if (!pasted) {
3434
+ print("That doesn't look like the code. Copy it with the button on the page.");
3435
+ continue;
3436
+ }
3437
+ if (pasted.state !== state) {
3438
+ throw new HerdError("That code is from a different sign-in. Run herd login again.");
3439
+ }
3440
+ return pasted.code;
3441
+ }
3442
+ throw new HerdError("No code was pasted. Run herd login again.");
3443
+ }
3444
+ };
3445
+ }
3446
+ async function login(how, {
3447
+ api = apiUrl(),
3448
+ timeoutMs = 10 * 60 * 1e3,
3449
+ deviceRedirect = deviceRedirectFor(api)
3450
+ } = {}) {
3451
+ const discovered = await fetch(`${api}/.well-known/oauth-authorization-server`).catch(() => null);
3452
+ const metadata = metadataSchema.safeParse(await discovered?.json().catch(() => null));
3453
+ if (!metadata.success) throw new HerdError(`Couldn't find where to sign in for ${api}.`);
3454
+ const meta = metadata.data;
3455
+ const way = typeof how === "function" ? { openBrowser: how } : how;
3456
+ const source = "openBrowser" in way ? await loopback(way.openBrowser, timeoutMs) : device(deviceRedirect, way.print, way.readCode);
3457
+ const { redirectUri } = source;
3381
3458
  try {
3382
3459
  const registration2 = await fetch(meta.registration_endpoint, {
3383
3460
  method: "POST",
@@ -3406,30 +3483,11 @@ async function login(openBrowser2, { api = apiUrl(), timeoutMs = 10 * 60 * 1e3 }
3406
3483
  state,
3407
3484
  resource: api
3408
3485
  }).toString();
3409
- const code = new Promise((resolve8, reject) => {
3410
- const timer = setTimeout(
3411
- () => reject(new HerdError("Nobody signed in within 10 minutes. Run herd login again.")),
3412
- timeoutMs
3413
- );
3414
- server.on("request", (req, res) => {
3415
- const url = new URL(req.url ?? "/", redirectUri);
3416
- if (url.pathname !== "/callback") return void res.writeHead(404).end();
3417
- clearTimeout(timer);
3418
- const error = url.searchParams.get("error");
3419
- const ok = !error && url.searchParams.get("state") === state;
3420
- res.writeHead(ok ? 200 : 400, { "Content-Type": "text/plain" }).end(ok ? "Signed in to herd. You can close this tab." : "Sign-in didn't complete.");
3421
- if (error) return reject(new HerdError(`Sign-in was refused: ${error}.`));
3422
- if (!ok) return reject(new HerdError("Sign-in came back for a different request."));
3423
- resolve8(url.searchParams.get("code") ?? "");
3424
- });
3425
- });
3426
- code.catch(() => {
3427
- });
3428
- await openBrowser2(authorize.toString());
3486
+ const code = await source.code(authorize.toString(), state);
3429
3487
  const response = await postForm(meta.token_endpoint, {
3430
3488
  grant_type: "authorization_code",
3431
3489
  client_id: client.data.client_id,
3432
- code: await code,
3490
+ code,
3433
3491
  redirect_uri: redirectUri,
3434
3492
  code_verifier: verifier,
3435
3493
  resource: api
@@ -3446,7 +3504,7 @@ async function login(openBrowser2, { api = apiUrl(), timeoutMs = 10 * 60 * 1e3 }
3446
3504
  refreshToken: tokens.data.refresh_token
3447
3505
  });
3448
3506
  } finally {
3449
- server.close();
3507
+ source.close();
3450
3508
  }
3451
3509
  }
3452
3510
  var EXPIRY_MARGIN_MS = 60 * 1e3;
@@ -3508,7 +3566,7 @@ async function apiFetch(path, init = {}) {
3508
3566
  // src/cli-flags.ts
3509
3567
  init_define_HERD_PACKAGE();
3510
3568
  var HERD_FLAGS = {
3511
- login: [],
3569
+ login: ["--device"],
3512
3570
  logout: [],
3513
3571
  daemon: ["--repo"],
3514
3572
  attach: ["--watch"],
@@ -3539,7 +3597,7 @@ var HERD_FLAGS = {
3539
3597
  persona: []
3540
3598
  };
3541
3599
  var FRIDAY_FLAGS = {
3542
- start: ["--agents", "--config", "--verbose"],
3600
+ start: ["--agents", "--config", "--device", "--verbose"],
3543
3601
  stop: [],
3544
3602
  status: [],
3545
3603
  logs: ["--follow", "-f", "-n"]
@@ -3591,6 +3649,7 @@ function commandHelp(overview, command) {
3591
3649
  // src/commands/login.ts
3592
3650
  init_define_HERD_PACKAGE();
3593
3651
  import { spawn } from "node:child_process";
3652
+ import { createInterface } from "node:readline/promises";
3594
3653
  function openBrowser(url) {
3595
3654
  console.log(`Opening your browser to sign in. If it doesn't open, visit:
3596
3655
 
@@ -3603,12 +3662,34 @@ function openBrowser(url) {
3603
3662
  } catch {
3604
3663
  }
3605
3664
  }
3606
- async function runLogin() {
3665
+ async function readCodeFromTerminal(prompt) {
3666
+ if (!process.stdin.isTTY) {
3667
+ throw new HerdError("Signing in with a code needs a terminal to paste it into.");
3668
+ }
3669
+ const lines = createInterface({ input: process.stdin, output: process.stdout });
3670
+ try {
3671
+ return await lines.question(prompt);
3672
+ } finally {
3673
+ lines.close();
3674
+ }
3675
+ }
3676
+ function signInWay(args, env = process.env) {
3677
+ if (wantsDevice(args, env)) {
3678
+ return { readCode: readCodeFromTerminal, print: (line2) => console.log(line2) };
3679
+ }
3680
+ return {
3681
+ openBrowser: (url) => {
3682
+ openBrowser(url);
3683
+ console.log("Signing in on another device, such as over SSH? Run this with --device.\n");
3684
+ }
3685
+ };
3686
+ }
3687
+ async function runLogin(args = []) {
3607
3688
  if (isLocal(apiUrl())) {
3608
3689
  console.log("The local dev server needs no sign-in.");
3609
3690
  return 0;
3610
3691
  }
3611
- await login(openBrowser);
3692
+ await login(signInWay(args));
3612
3693
  console.log("Signed in.");
3613
3694
  return 0;
3614
3695
  }
@@ -6676,7 +6757,7 @@ async function runStart(options) {
6676
6757
  }
6677
6758
  }
6678
6759
  if (!options.signedIn()) {
6679
- print("Signing in through your browser.");
6760
+ print("Signing in.");
6680
6761
  await options.signIn();
6681
6762
  verbose2("signed in");
6682
6763
  } else verbose2("already signed in");
@@ -7043,7 +7124,8 @@ Usage: ${FRIDAY_COMMAND} <command>
7043
7124
 
7044
7125
  start Make this computer a machine, with the folder this runs in as its root, which
7045
7126
  holds your repos, such as ~/code. Signs in through your browser when
7046
- needed, asks how much of each agent you have the first time, starts the daemon
7127
+ needed, or with a code to paste from another device with --device or over
7128
+ SSH. Asks how much of each agent you have the first time, starts the daemon
7047
7129
  now and whenever you log in, then shows what's left to set up. Safe to run
7048
7130
  again. Run in another folder, it offers to move the root there. --config asks
7049
7131
  about the agents again, and --agents claude=lot,codex=little sets their shares
@@ -7120,7 +7202,7 @@ async function friday(args, deps = defaults()) {
7120
7202
  version: HERD_VERSION,
7121
7203
  run: verboseExec(deps.run),
7122
7204
  signedIn: () => isLocal(apiUrl()) || readCredentials() !== null,
7123
- signIn: () => login(openBrowser),
7205
+ signIn: () => login(signInWay(rest)),
7124
7206
  serviceKind: deps.serviceKind ?? await detectService(verboseExec(deps.run)),
7125
7207
  env: passedEnv(),
7126
7208
  print: deps.print,
@@ -7169,7 +7251,7 @@ ${HELP}`);
7169
7251
  }
7170
7252
  }
7171
7253
  async function confirmInTerminal(question) {
7172
- const lines = createInterface({ input: process.stdin, output: process.stdout });
7254
+ const lines = createInterface2({ input: process.stdin, output: process.stdout });
7173
7255
  try {
7174
7256
  return /^y(es)?$/i.test((await lines.question(`${question} [y/N] `)).trim());
7175
7257
  } finally {
@@ -10647,15 +10729,15 @@ async function signInGh(options) {
10647
10729
  const codeTimer = setTimeout(() => resolveCode(false), options.codeWaitMs ?? GH_CODE_WAIT_MS);
10648
10730
  const found = await codeShown;
10649
10731
  clearTimeout(codeTimer);
10650
- const device = readDeviceCode(output2);
10651
- if (!found || !device) {
10732
+ const device2 = readDeviceCode(output2);
10733
+ if (!found || !device2) {
10652
10734
  child.kill();
10653
10735
  const failed = { state: "failed", reason: "no_code" };
10654
10736
  await options.update(failed);
10655
10737
  return failed;
10656
10738
  }
10657
10739
  sent = true;
10658
- await options.update({ state: "code", code: device.code, url: device.url });
10740
+ await options.update({ state: "code", code: device2.code, url: device2.url });
10659
10741
  const approveTimer = setTimeout(() => child.kill(), options.approveWaitMs ?? GH_APPROVE_WAIT_MS);
10660
10742
  await exited;
10661
10743
  clearTimeout(approveTimer);
@@ -15592,7 +15674,8 @@ var HELP4 = `herd: work on HerdingChats conversations from this computer.
15592
15674
  Usage: herd <command>
15593
15675
 
15594
15676
  On this computer:
15595
- login Sign in through your browser
15677
+ login [--device] Sign in through your browser. --device, or running over SSH, signs in from
15678
+ another device with a code to paste
15596
15679
  logout Remove this computer's sign-in
15597
15680
  daemon [--repo <path>]...
15598
15681
  Run this machine's daemon, working from its root folder. Repos given with
@@ -15676,7 +15759,7 @@ async function main(args) {
15676
15759
  console.log(HERD_VERSION);
15677
15760
  return 0;
15678
15761
  case "login":
15679
- return await runLogin();
15762
+ return await runLogin(rest);
15680
15763
  case "logout":
15681
15764
  return await runLogout();
15682
15765
  case "daemon":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herdingchats/friday",
3
- "version": "0.1.675",
3
+ "version": "0.1.688",
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": "c9f287481b3f9f19c6c089abb87783ca7592ff5f"
25
+ "gitHead": "a284c931b34fdaa76115c8d2c91da8b2903d19ff"
26
26
  }