agent-yes 1.262.0 → 1.263.0

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 (30) hide show
  1. package/dist/{SUPPORTED_CLIS-BFLGbQmm.js → SUPPORTED_CLIS--5E9wO4L.js} +3 -3
  2. package/dist/{SUPPORTED_CLIS-CjKxvhlh.js → SUPPORTED_CLIS-DjFlWHpg.js} +2 -2
  3. package/dist/{agentShare-LEETLOm1.js → agentShare-BP6wiu_5.js} +2 -2
  4. package/dist/{callback-lqCCgSy1.js → callback-B0PQUZcK.js} +2 -2
  5. package/dist/{callback-CB3UjryF.js → callback-DFUFLIZQ.js} +3 -3
  6. package/dist/cli.js +5 -5
  7. package/dist/index.js +2 -2
  8. package/dist/{notifyDaemon-P6Q7Eo_B.js → notifyDaemon-BskLhHdg.js} +2 -2
  9. package/dist/{rustBinary-_qRGhAa5.js → rustBinary-Borvwmya.js} +2 -2
  10. package/dist/{schedule-ChAdx2h6.js → schedule-BSEIVKQ0.js} +4 -4
  11. package/dist/{serve-DhtgWGZB.js → serve-BEdLsSpv.js} +159 -17
  12. package/dist/{setup-BlJRaODn.js → setup-DFRvZ_ku.js} +2 -2
  13. package/dist/subcommands-B1YlmRaZ.js +10 -0
  14. package/dist/{subcommands-Zk3brdKt.js → subcommands-Bo1MSwuC.js} +78 -17
  15. package/dist/{terminal-BVWhnQUf.js → terminal-BMQWojIp.js} +2 -2
  16. package/dist/{trayApp-DtThjl0_.js → trayApp-CjB5QhdV.js} +2 -2
  17. package/dist/trayApp-DrdktFnW.js +5 -0
  18. package/dist/{ts-BqaOPFCQ.js → ts-BSHR_dga.js} +2 -2
  19. package/dist/{versionChecker-D-WLKAN6.js → versionChecker-BTLZ3PAb.js} +2 -2
  20. package/dist/{widget-CJyd9t8k.js → widget-BVss0Z9Y.js} +3 -3
  21. package/dist/{ws-mG4yeZu8.js → ws-DMudFezi.js} +2 -2
  22. package/dist/{ws-Vy3mBjNm.js → ws-DxNNwnrc.js} +2 -2
  23. package/lab/ui/index.html +101 -36
  24. package/package.json +2 -1
  25. package/scripts/deepseek-codex.ts +580 -0
  26. package/ts/serve.spec.ts +36 -0
  27. package/ts/serve.ts +144 -1
  28. package/ts/subcommands.ts +109 -1
  29. package/dist/subcommands-D6O9uRvN.js +0 -10
  30. package/dist/trayApp-C6U7Hghi.js +0 -5
package/ts/serve.ts CHANGED
@@ -1,4 +1,15 @@
1
- import { appendFile, mkdir, open, readdir, readFile, stat, unlink, writeFile } from "fs/promises";
1
+ import {
2
+ appendFile,
3
+ mkdir,
4
+ open,
5
+ readdir,
6
+ readFile,
7
+ rename,
8
+ rm,
9
+ stat,
10
+ unlink,
11
+ writeFile,
12
+ } from "fs/promises";
2
13
  import { existsSync, renameSync, watch, writeFileSync } from "node:fs";
3
14
  import { execFileSync } from "node:child_process";
4
15
  import { fileURLToPath } from "node:url";
@@ -566,6 +577,38 @@ async function runCapture(
566
577
  }
567
578
  }
568
579
 
580
+ // A non-GitHub git source (gitlab, self-hosted, …) for /api/spawn's raw-clone
581
+ // path. github.com inputs return null — those go through the provision module
582
+ // (gh auth, setup-repo.sh). Accepts scp-style git@host:path, ssh://, git://,
583
+ // http(s):// — bare owner/repo is github-first and never lands here.
584
+ export function parseGitUrl(s: string): { url: string; owner: string; repo: string } | null {
585
+ let host = "";
586
+ let pathPart = "";
587
+ const scp = /^git@([^:/\s]+):([^\s]+?)(?:\.git)?\/?$/.exec(s);
588
+ if (scp) {
589
+ host = scp[1]!;
590
+ pathPart = scp[2]!;
591
+ } else {
592
+ let u: URL;
593
+ try {
594
+ u = new URL(s);
595
+ } catch {
596
+ return null;
597
+ }
598
+ if (!/^(https?|ssh|git)$/.test(u.protocol.replace(":", ""))) return null;
599
+ host = u.hostname;
600
+ pathPart = u.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
601
+ }
602
+ if (/(^|\.)github\.com$/i.test(host)) return null;
603
+ const segs = pathPart.split("/").filter(Boolean);
604
+ if (segs.length < 2) return null;
605
+ const owner = segs[segs.length - 2]!;
606
+ const repo = segs[segs.length - 1]!.replace(/\.git$/, "");
607
+ if (!owner || !repo || owner === "." || owner === ".." || repo === "." || repo === "..")
608
+ return null;
609
+ return { url: s.replace(/\/+$/, ""), owner, repo };
610
+ }
611
+
569
612
  // 60s memo for /api/ws/repos' gh side (see the endpoint for why).
570
613
  let ghRepoListMemo: { t: number; ok: boolean; repos: string[]; error?: string } | null = null;
571
614
 
@@ -4024,6 +4067,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
4024
4067
  prompt?: string;
4025
4068
  yes?: boolean;
4026
4069
  create?: boolean;
4070
+ branch?: string; // raw-clone path only (github specs embed the branch in `from`)
4027
4071
  fork?: { fromCwd?: string; branch?: string };
4028
4072
  };
4029
4073
  try {
@@ -4161,6 +4205,105 @@ export async function cmdServe(rest: string[]): Promise<number> {
4161
4205
  );
4162
4206
  cwd = result.folder;
4163
4207
  provisioned = { action: result.action, folder: result.folder };
4208
+ } else if (from && parseGitUrl(from)) {
4209
+ // Non-GitHub git URL (gitlab, self-hosted, …): raw `git clone` into the
4210
+ // standard layout <wsRoot>/<owner>/<repo>/tree/<branch> — no provision
4211
+ // module, no setup-repo.sh. Same admission gates as the github path:
4212
+ // provision hook first (it may select credentials), else the allowlist.
4213
+ const gitSrc = parseGitUrl(from)!;
4214
+ const branch = typeof body.branch === "string" ? body.branch.trim() : "";
4215
+ // the branch lands in a filesystem path (tree/<branch>) — no empty/./..
4216
+ // segments, no option-looking names
4217
+ const badBranch = (b: string) =>
4218
+ b.startsWith("-") || b.split("/").some((s) => !s || s === "." || s === "..");
4219
+ if (branch && badBranch(branch))
4220
+ return new Response(`invalid branch name: ${branch}`, { status: 400 });
4221
+ const gitHook = await runProvisionHook(getProvisionRoot() ?? homedir(), {
4222
+ KOHO_ACTION: "from",
4223
+ KOHO_SOURCE: from,
4224
+ KOHO_OWNER: gitSrc.owner,
4225
+ KOHO_REPO: gitSrc.repo,
4226
+ KOHO_BRANCH: branch,
4227
+ KOHO_WS_ROOT: getProvisionRoot() ?? "",
4228
+ });
4229
+ if (gitHook.ran) {
4230
+ if (!gitHook.ok)
4231
+ return new Response(
4232
+ `provision hook denied '${gitSrc.owner}/${gitSrc.repo}' (exit ${gitHook.code})` +
4233
+ (gitHook.detail ? `:\n${gitHook.detail}` : ""),
4234
+ { status: 403 },
4235
+ );
4236
+ } else if (!isProvisionAllowed(gitSrc.owner, gitSrc.repo)) {
4237
+ return new Response(
4238
+ `provisioning '${gitSrc.owner}/${gitSrc.repo}' is not allowed — add the owner to ` +
4239
+ `provisionAllowlist in ~/.agent-yes/config.json (or "*" to allow all), ` +
4240
+ `or set a provisionHook to gate it yourself`,
4241
+ { status: 403 },
4242
+ );
4243
+ }
4244
+ const wsRootDir = getProvisionRoot() ?? path.join(homedir(), "ws");
4245
+ const base = path.join(wsRootDir, gitSrc.owner, gitSrc.repo, "tree");
4246
+ const destFor = (b: string) => path.join(base, b);
4247
+ if (branch && existsSync(path.join(destFor(branch), ".git"))) {
4248
+ // already provisioned — reuse; a fetch/pull is the agent's business
4249
+ cwd = destFor(branch);
4250
+ provisioned = { action: "existing", folder: cwd };
4251
+ } else {
4252
+ const tmp = path.join(base, `.clone-${process.pid}-${Date.now().toString(36)}`);
4253
+ try {
4254
+ await mkdir(base, { recursive: true });
4255
+ } catch (e) {
4256
+ return new Response(`cannot create ${base}: ${(e as Error).message}`, { status: 500 });
4257
+ }
4258
+ const cloneMs = 300_000;
4259
+ let created = false;
4260
+ let r = await runCapture(
4261
+ branch
4262
+ ? ["git", "clone", "--branch", branch, "--", gitSrc.url, tmp]
4263
+ : ["git", "clone", "--", gitSrc.url, tmp],
4264
+ { timeoutMs: cloneMs },
4265
+ );
4266
+ if (!r.ok && branch && body.create === true) {
4267
+ // branch may not exist yet — clone the default and branch off it
4268
+ await rm(tmp, { recursive: true, force: true }).catch(() => {});
4269
+ r = await runCapture(["git", "clone", "--", gitSrc.url, tmp], { timeoutMs: cloneMs });
4270
+ if (r.ok) {
4271
+ const co = await runCapture(["git", "-C", tmp, "checkout", "-b", branch]);
4272
+ if (!co.ok) r = co;
4273
+ else created = true;
4274
+ }
4275
+ }
4276
+ if (!r.ok) {
4277
+ await rm(tmp, { recursive: true, force: true }).catch(() => {});
4278
+ return new Response(
4279
+ `git clone failed: ${(r.stderr.trim() || "unknown").slice(0, 500)}` +
4280
+ (branch && body.create !== true
4281
+ ? "\n(if the branch doesn't exist yet, retry with create:true)"
4282
+ : ""),
4283
+ { status: 502 },
4284
+ );
4285
+ }
4286
+ const head = await runCapture(["git", "-C", tmp, "rev-parse", "--abbrev-ref", "HEAD"]);
4287
+ let actual = branch || (head.ok ? head.stdout.trim() : "") || "main";
4288
+ if (badBranch(actual)) actual = "main";
4289
+ const dest = destFor(actual);
4290
+ if (existsSync(dest)) {
4291
+ // raced/already there — keep the existing checkout, drop ours
4292
+ await rm(tmp, { recursive: true, force: true }).catch(() => {});
4293
+ } else {
4294
+ try {
4295
+ await mkdir(path.dirname(dest), { recursive: true });
4296
+ await rename(tmp, dest);
4297
+ } catch (e) {
4298
+ await rm(tmp, { recursive: true, force: true }).catch(() => {});
4299
+ return new Response(`cannot place checkout at ${dest}: ${(e as Error).message}`, {
4300
+ status: 500,
4301
+ });
4302
+ }
4303
+ }
4304
+ cwd = dest;
4305
+ provisioned = { action: created ? "cloned+new-branch" : "cloned", folder: dest };
4306
+ }
4164
4307
  } else if (from) {
4165
4308
  type Spec = { owner: string; repo: string; branch: string };
4166
4309
  let prov: {
package/ts/subcommands.ts CHANGED
@@ -12,7 +12,9 @@
12
12
  */
13
13
 
14
14
  import { randomBytes } from "crypto";
15
+ import { spawn } from "child_process";
15
16
  import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
17
+ import { fileURLToPath } from "node:url";
16
18
  import ms from "ms";
17
19
  import { homedir } from "os";
18
20
  import path from "path";
@@ -442,6 +444,8 @@ const SUBCOMMANDS = new Set([
442
444
  "expose",
443
445
  "callback",
444
446
  "reap",
447
+ "deepseek",
448
+ "ds",
445
449
  "help",
446
450
  ]);
447
451
 
@@ -512,6 +516,42 @@ export function isUnknownManagerToken(
512
516
  return !supportedClis.includes(rawArg);
513
517
  }
514
518
 
519
+ /**
520
+ * Write to stdout and wait until it has actually been handed off.
521
+ *
522
+ * The CLI ends with `process.exit()`, which DISCARDS bytes still sitting in the
523
+ * pipe buffer. Writing to a file completes synchronously so this never showed
524
+ * there, but any consumer that PIPES us silently lost everything past 64KiB.
525
+ * Measured on `ay ls --json` with a large fleet (symval CTO, 2026-08-05):
526
+ *
527
+ * ay ls --json > file 118055 bytes, valid JSON
528
+ * ay ls --json | consumer 65536 bytes, cut mid-multibyte — will not parse
529
+ *
530
+ * The truncation is invisible to the caller: it looks exactly like a small
531
+ * fleet, and an orchestrator that parses this reported 27 of 71 agents.
532
+ *
533
+ * Only capturing the REAL write's completion works. Probing afterwards with an
534
+ * empty `write("")` — by return value, by drain event, or by callback — reports
535
+ * ready while the big write is still queued (all three verified failing), so do
536
+ * not "simplify" this into a flush helper at the exit site.
537
+ */
538
+ async function writeStdoutFlushed(text: string): Promise<void> {
539
+ // Use the WRITE'S OWN RETURN VALUE, not a probe afterwards. `false` means the
540
+ // pipe buffer is full and bytes are still queued; only then do we wait.
541
+ //
542
+ // Deliberately `=== false`: a stubbed/mocked stdout (tests, embedders) returns
543
+ // undefined, and treating that as backpressure made this await forever — it
544
+ // broke 5 specs before the strict compare went in. The timeout is a second
545
+ // guarantee that a stuck consumer can never hang the CLI.
546
+ const flushed = process.stdout.write(text);
547
+ if (flushed === false) {
548
+ await Promise.race([
549
+ new Promise<void>((resolve) => process.stdout.once("drain", () => resolve())),
550
+ new Promise<void>((resolve) => setTimeout(resolve, 2000)),
551
+ ]);
552
+ }
553
+ }
554
+
515
555
  /**
516
556
  * Top-level entry. Returns the desired process exit code, or null if argv
517
557
  * is not a subcommand invocation.
@@ -629,6 +669,9 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
629
669
  await reaper.sweep();
630
670
  return 0;
631
671
  }
672
+ case "deepseek":
673
+ case "ds":
674
+ return cmdDeepseek(rest);
632
675
  case "help":
633
676
  return cmdHelp(managerCommands);
634
677
  default:
@@ -641,6 +684,47 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
641
684
  }
642
685
  }
643
686
 
687
+ // ---------------------------------------------------------------------------
688
+ // ay deepseek / ay ds
689
+ // ---------------------------------------------------------------------------
690
+
691
+ /**
692
+ * `ay deepseek [-- <args>…]` / `ay ds …`: run the local DeepSeek adapter that
693
+ * bridges Codex's Responses API to DeepSeek's chat completions, then spawn
694
+ * `codex` pointed at it. The adapter script lives in this package's
695
+ * `scripts/deepseek-codex.ts` and is Bun-only (Bun.serve / Bun.spawn), so it is
696
+ * re-exec'd via the same bun runtime that's running us — never imported into
697
+ * this process. Extra args are forwarded verbatim to `codex`.
698
+ */
699
+ export function cmdDeepseek(rest: string[]): Promise<number> {
700
+ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
701
+ const script = path.join(root, "scripts", "deepseek-codex.ts");
702
+ const bunBin = process.execPath.split(/[/\\]/).at(-1)?.startsWith("bun")
703
+ ? process.execPath
704
+ : "bun";
705
+ const child = spawn(bunBin, [script, ...rest], {
706
+ cwd: process.cwd(),
707
+ env: { ...process.env, AGENT_YES_BIN: process.argv[1] },
708
+ stdio: "inherit",
709
+ });
710
+ return new Promise((resolve) => {
711
+ child.on("error", (err) => {
712
+ process.stderr.write(`ay deepseek: failed to start: ${err.message}\n`);
713
+ resolve(1);
714
+ });
715
+ child.on("exit", (code, signal) => {
716
+ if (signal) {
717
+ resolve(128 + (signal === "SIGINT" ? 2 : signal === "SIGTERM" ? 15 : 1));
718
+ } else {
719
+ resolve(code ?? 1);
720
+ }
721
+ });
722
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
723
+ process.on(signal, () => child.kill(signal));
724
+ }
725
+ });
726
+ }
727
+
644
728
  // ---------------------------------------------------------------------------
645
729
  // ay help
646
730
  // ---------------------------------------------------------------------------
@@ -747,6 +831,7 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
747
831
  ` ay result <keyword> [--wait] pull an agent's structured result envelope\n` +
748
832
  ` ay result set '<json>' (inside an agent) deposit your result envelope\n` +
749
833
  ` ay reap kill process groups leaked by dead agents\n` +
834
+ ` ay deepseek|ds [-- <codex args>] run codex via the local DeepSeek adapter (DEEPSEEK_API_KEY)\n` +
750
835
  wsLines +
751
836
  `\n` +
752
837
  `Remote:\n` +
@@ -1849,7 +1934,7 @@ async function cmdLs(rest: string[]): Promise<number> {
1849
1934
  const enriched = await Promise.all(
1850
1935
  records.map(async (r) => ({ ...r, ...(await deriveLiveState(r)) })),
1851
1936
  );
1852
- process.stdout.write(JSON.stringify(enriched, null, 2) + "\n");
1937
+ await writeStdoutFlushed(JSON.stringify(enriched, null, 2) + "\n");
1853
1938
  return 0;
1854
1939
  }
1855
1940
 
@@ -3222,6 +3307,15 @@ async function cmdSend(rest: string[]): Promise<number> {
3222
3307
  })
3223
3308
  .help(false)
3224
3309
  .version(false)
3310
+ // An UNKNOWN flag must be an error, never a silent reinterpretation of the
3311
+ // message. yargs otherwise swallows `--body-file /tmp/x` as an ad-hoc option
3312
+ // whose VALUE is the next token — which removes the positional entirely and
3313
+ // sends an EMPTY body. That failure is invisible from both ends: the sender
3314
+ // sees a normal exit, the recipient sees a blank message and reads it as an
3315
+ // idle lane. Observed 2026-07-30: four consecutive replies vanished this way,
3316
+ // and the receiving lane concluded the sender had stopped working and began
3317
+ // taking over the work.
3318
+ .strictOptions()
3225
3319
  .exitProcess(false);
3226
3320
 
3227
3321
  const argv = await y.parseAsync();
@@ -3238,6 +3332,17 @@ async function cmdSend(rest: string[]): Promise<number> {
3238
3332
  if (!keyword)
3239
3333
  throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
3240
3334
 
3335
+ // Second line of defence, independent of how the message went missing: never
3336
+ // deliver nothing. Sending an empty body is never what anyone meant, and its
3337
+ // whole cost is paid by the RECIPIENT, who cannot tell "no message" from
3338
+ // "nothing to say". `-` is exempt here and validated after stdin is read.
3339
+ if (rawMessage !== "-" && rawMessage.trim() === "") {
3340
+ throw new Error(
3341
+ "ay send: refusing to send an empty message. Pass the text as a single argument, " +
3342
+ "or use `-` to read the body from stdin (e.g. `ay send <keyword> - < file.txt`).",
3343
+ );
3344
+ }
3345
+
3241
3346
  const codeName = argv.code.toLowerCase();
3242
3347
  {
3243
3348
  const remote = await resolveRemoteSpec(keyword);
@@ -3271,6 +3376,9 @@ async function cmdSend(rest: string[]): Promise<number> {
3271
3376
  const chunks: Buffer[] = [];
3272
3377
  for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
3273
3378
  body = Buffer.concat(chunks).toString("utf-8").trimEnd();
3379
+ if (body.trim() === "") {
3380
+ throw new Error("ay send: refusing to send an empty message (stdin was empty).");
3381
+ }
3274
3382
  } else {
3275
3383
  body = rawMessage;
3276
3384
  }
@@ -1,10 +0,0 @@
1
- import "./logger-CDIsZ-Pp.js";
2
- import "./globalPidIndex-pwp7Vb7o.js";
3
- import "./messageLog-BYFwioFY.js";
4
- import "./e2e-CUMZC53I.js";
5
- import "./configShared-DtHMBCl7.js";
6
- import { A as readPtysize, B as runSubcommand, C as lastStdinAt, D as readAgentPtysize, E as menuSelectKeys, F as renderRawLogLines, G as waitForLogQuiet, H as stdinActivityPath, I as resolveOne, K as writeKeysPaced, L as resolveReadWindow, M as recentReadEdges, N as renderLogTailLines, O as readLogForRender, P as renderRawLog, R as resolveResumeArgs, S as isUserTyping, T as matchKeyword, U as stopTipForCli, V as snapshotStatus, W as submitAndConfirm, _ as isExitRequest, a as backoffWhileTyping, b as isSubcommand, c as cursorAbs, d as extractBadges, f as extractMenu, g as isAgentStuck, h as finalizedLines, i as TYPING_WINDOW_MS, j as recentMessageEdges, k as readNotes, l as deriveLiveState, m as extractTaskCounts, n as MAX_RENDER_BYTES, o as cmdHelp, p as extractNeedsInput, q as writeToIpc, r as READ_PAGE_DEFAULT, s as controlCodeFromName, t as GRACEFUL_EXIT_COMMANDS, u as deriveLiveStatus, v as isPidAlive, w as listRecords, x as isUnknownManagerToken, y as isSlashCommand, z as restartHintLines } from "./subcommands-Zk3brdKt.js";
7
- import "./webrtcLink-CS4D-3G1.js";
8
- import "./remotes-Cn-wszbK.js";
9
-
10
- export { cmdHelp, isSubcommand, isUnknownManagerToken, runSubcommand };
@@ -1,5 +0,0 @@
1
- import "./versionChecker-D-WLKAN6.js";
2
- import "./rustBinary-_qRGhAa5.js";
3
- import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-DtThjl0_.js";
4
-
5
- export { cmdTray };