claude-yes 1.148.0 → 1.149.1

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.
@@ -0,0 +1,8 @@
1
+ import "./ts-C21nFgXQ.js";
2
+ import "./logger-CDIsZ-Pp.js";
3
+ import "./versionChecker-C-JJk14E.js";
4
+ import "./pidStore-fqXqTKkh.js";
5
+ import "./globalPidIndex-DlmmJlO8.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DyQjQODe.js";
7
+
8
+ export { SUPPORTED_CLIS };
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-CaCRPaos.js";
1
+ import { t as CLIS_CONFIG } from "./ts-C21nFgXQ.js";
2
2
 
3
3
  //#region ts/SUPPORTED_CLIS.ts
4
4
  const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
5
 
6
6
  //#endregion
7
7
  export { SUPPORTED_CLIS as t };
8
- //# sourceMappingURL=SUPPORTED_CLIS-CmsPbgMe.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-DyQjQODe.js.map
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
3
3
  import { n as logger } from "./logger-CDIsZ-Pp.js";
4
- import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-DfO37H1R.js";
4
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-C-JJk14E.js";
5
5
  import { argv } from "process";
6
6
  import { execFileSync, spawn } from "child_process";
7
7
  import ms from "ms";
@@ -480,7 +480,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
480
480
  const rawArg = process.argv[2];
481
481
  const managerCommands = !invokedCliName(process.argv);
482
482
  const isHelpFlag = rawArg === "-h" || rawArg === "--help";
483
- const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-BQ2tu-y9.js");
483
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-Bv7xJq9E.js");
484
484
  if (isHelpFlag && process.argv.length === 3) {
485
485
  cmdHelp(managerCommands);
486
486
  process.exit(0);
@@ -513,7 +513,7 @@ if (config.useRust) {
513
513
  }
514
514
  }
515
515
  if (rustBinary) {
516
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DpHVL8Mi.js");
516
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DhVolHXQ.js");
517
517
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
518
518
  if (config.verbose) {
519
519
  console.log(`[rust] Using binary: ${rustBinary}`);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-CaCRPaos.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-C21nFgXQ.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-DfO37H1R.js";
3
+ import "./versionChecker-C-JJk14E.js";
4
4
  import "./pidStore-fqXqTKkh.js";
5
5
  import "./globalPidIndex-DlmmJlO8.js";
6
6
 
@@ -1,10 +1,10 @@
1
- import "./ts-CaCRPaos.js";
1
+ import "./ts-C21nFgXQ.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-DfO37H1R.js";
3
+ import "./versionChecker-C-JJk14E.js";
4
4
  import "./pidStore-fqXqTKkh.js";
5
5
  import "./globalPidIndex-DlmmJlO8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CmsPbgMe.js";
7
- import { n as resolveSpawnCwd } from "./workspaceConfig-B3ylOZAO.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DyQjQODe.js";
7
+ import { i as resolveSpawnCwd } from "./workspaceConfig-B9VX8tVO.js";
8
8
  import { createHash } from "node:crypto";
9
9
 
10
10
  //#region ts/oxmgrService.ts
@@ -141,4 +141,4 @@ async function cmdSchedule(rest) {
141
141
 
142
142
  //#endregion
143
143
  export { cmdSchedule };
144
- //# sourceMappingURL=schedule-DkTLO7No.js.map
144
+ //# sourceMappingURL=schedule-BJk96cOy.js.map
@@ -1,15 +1,16 @@
1
- import "./ts-CaCRPaos.js";
1
+ import "./ts-C21nFgXQ.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import { r as getInstalledPackage } from "./versionChecker-DfO37H1R.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-C-JJk14E.js";
4
4
  import "./pidStore-fqXqTKkh.js";
5
5
  import { a as updateGlobalPidStatus } from "./globalPidIndex-DlmmJlO8.js";
6
6
  import { t as pgidForWrapper } from "./reaper-C-eWAxIj.js";
7
7
  import "./configShared-C1C04bbq.js";
8
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CmsPbgMe.js";
8
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DyQjQODe.js";
9
9
  import "./e2e-ClOI_aqV.js";
10
10
  import "./webrtcLink-BWhuA74k.js";
11
11
  import "./remotes-qK6uozO4.js";
12
- import { S as snapshotStatus, _ as renderRawLog, c as extractTaskCounts, g as readNotes, i as controlCodeFromName, m as listRecords, o as deriveLiveStatus, w as writeToIpc, y as resolveOne } from "./subcommands-Df_SjmLN.js";
12
+ import { i as resolveSpawnCwd, r as isProvisionAllowed, t as getProvisionRoot } from "./workspaceConfig-B9VX8tVO.js";
13
+ import { S as snapshotStatus, _ as renderRawLog, c as extractTaskCounts, g as readNotes, i as controlCodeFromName, m as listRecords, o as deriveLiveStatus, w as writeToIpc, y as resolveOne } from "./subcommands-OyK7Y447.js";
13
14
  import yargs from "yargs";
14
15
  import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
15
16
  import { homedir, hostname, userInfo } from "os";
@@ -20,6 +21,22 @@ import { randomBytes, timingSafeEqual } from "crypto";
20
21
 
21
22
  //#region ts/serve.ts
22
23
  const DEFAULT_PORT = 7432;
24
+ /**
25
+ * Normalize a user-supplied GitHub-ish source into the standard
26
+ * `<owner>/<repo>/tree/<branch>` path that codehost/provision's `parseSpec`
27
+ * understands. Interim fallback used only when the linked codehost build
28
+ * predates `parseSource` (the canonical normalizer in the standard):
29
+ * https://github.com/o/r/tree/b · github.com/o/r/tree/b · o/r/tree/b
30
+ * o/r@branch · o/r (→ default branch main)
31
+ */
32
+ function normalizeGithubSource(s) {
33
+ let v = s.trim().replace(/^https?:\/\//, "").replace(/^github\.com\//, "");
34
+ v = v.replace(/[?#].*$/, "").replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
35
+ const at = v.match(/^([^/]+)\/([^/@]+)@(.+)$/);
36
+ if (at) return `${at[1]}/${at[2]}/tree/${at[3]}`;
37
+ if (/^[^/]+\/[^/]+$/.test(v)) return `${v}/tree/main`;
38
+ return v;
39
+ }
23
40
  function agentYesHome() {
24
41
  return process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
25
42
  }
@@ -1031,9 +1048,47 @@ Options:
1031
1048
  }
1032
1049
  const cli = String(body.cli ?? "claude");
1033
1050
  if (!SUPPORTED_CLIS.includes(cli)) return new Response(`unsupported cli: ${cli}`, { status: 400 });
1034
- const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
1035
1051
  const prompt = String(body.prompt ?? "");
1036
- process.stderr.write(`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd})\n`);
1052
+ let cwd;
1053
+ let provisioned = null;
1054
+ const from = typeof body.from === "string" ? body.from.trim() : "";
1055
+ if (from) {
1056
+ let prov;
1057
+ try {
1058
+ prov = await import("codehost/provision");
1059
+ } catch (e) {
1060
+ return new Response(`spawn-from needs the 'codehost' package (codehost/provision) — install it (npm i -g codehost) or 'bun link' it for local dev: ${e.message}`, { status: 501 });
1061
+ }
1062
+ let spec;
1063
+ try {
1064
+ spec = typeof prov.parseSource === "function" ? prov.parseSource(from) : prov.parseSpec(normalizeGithubSource(from));
1065
+ } catch {
1066
+ spec = null;
1067
+ }
1068
+ if (!spec) return new Response(`unrecognized spawn source: ${from}`, { status: 400 });
1069
+ if (!isProvisionAllowed(spec.owner, spec.repo)) return new Response(`provisioning '${spec.owner}/${spec.repo}' is not allowed — add the owner to provisionAllowlist in ~/.agent-yes/config.json (or "*" to allow all)`, { status: 403 });
1070
+ let result;
1071
+ try {
1072
+ const wsRoot = getProvisionRoot();
1073
+ result = await prov.provision(spec, wsRoot ? { wsRoot } : void 0);
1074
+ } catch (e) {
1075
+ return new Response(`provision failed: ${e.message}`, { status: 502 });
1076
+ }
1077
+ if (!result?.ok) return new Response(result?.error ?? "provision failed", { status: 502 });
1078
+ cwd = result.folder;
1079
+ provisioned = {
1080
+ action: result.action,
1081
+ folder: result.folder
1082
+ };
1083
+ } else {
1084
+ cwd = resolveSpawnCwd(body.cwd);
1085
+ try {
1086
+ await mkdir(cwd, { recursive: true });
1087
+ } catch (e) {
1088
+ return new Response(`cannot create cwd ${cwd}: ${e.message}`, { status: 500 });
1089
+ }
1090
+ }
1091
+ process.stderr.write(`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd}${provisioned ? `, ${provisioned.action} from ${from}` : ""})\n`);
1037
1092
  const ayBin = Bun.which("ay") ?? process.argv[1];
1038
1093
  const ayCmd = process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe") ? [ayBin] : [process.execPath, ayBin];
1039
1094
  try {
@@ -1053,7 +1108,8 @@ Options:
1053
1108
  ok: true,
1054
1109
  pid: child.pid,
1055
1110
  cli,
1056
- cwd
1111
+ cwd,
1112
+ ...provisioned ? { provisioned } : {}
1057
1113
  });
1058
1114
  } catch (e) {
1059
1115
  return new Response(e.message, { status: 500 });
@@ -1179,4 +1235,4 @@ Options:
1179
1235
 
1180
1236
  //#endregion
1181
1237
  export { cmdServe };
1182
- //# sourceMappingURL=serve-DQK6u2KS.js.map
1238
+ //# sourceMappingURL=serve-CaIDZfhy.js.map
@@ -1,4 +1,4 @@
1
- import { r as setWorkspaceRoot, t as getWorkspaceRoot } from "./workspaceConfig-B3ylOZAO.js";
1
+ import { a as setWorkspaceRoot, n as getWorkspaceRoot } from "./workspaceConfig-B9VX8tVO.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import { createInterface } from "node:readline/promises";
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
32
32
  if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
33
33
  if (noShare) return 0;
34
34
  process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
35
- const { cmdServe } = await import("./serve-DQK6u2KS.js");
35
+ const { cmdServe } = await import("./serve-CaIDZfhy.js");
36
36
  return cmdServe([
37
37
  "install",
38
38
  "--share",
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
42
42
 
43
43
  //#endregion
44
44
  export { cmdSetup };
45
- //# sourceMappingURL=setup-6R_yJsKX.js.map
45
+ //# sourceMappingURL=setup-B15dHEd1.js.map
@@ -4,6 +4,6 @@ import "./configShared-C1C04bbq.js";
4
4
  import "./e2e-ClOI_aqV.js";
5
5
  import "./webrtcLink-BWhuA74k.js";
6
6
  import "./remotes-qK6uozO4.js";
7
- import { C as stopTipForCli, S as snapshotStatus, _ as renderRawLog, a as cursorAbs, b as resolveReadWindow, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLogLines, w as writeToIpc, x as runSubcommand, y as resolveOne } from "./subcommands-Df_SjmLN.js";
7
+ import { C as stopTipForCli, S as snapshotStatus, _ as renderRawLog, a as cursorAbs, b as resolveReadWindow, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLogLines, w as writeToIpc, x as runSubcommand, y as resolveOne } from "./subcommands-OyK7Y447.js";
8
8
 
9
9
  export { cmdHelp, isSubcommand, runSubcommand };
@@ -554,15 +554,15 @@ async function runSubcommand(argv) {
554
554
  case "restart": return await cmdRestart(rest);
555
555
  case "note": return await cmdNote(rest);
556
556
  case "serve": {
557
- const { cmdServe } = await import("./serve-DQK6u2KS.js");
557
+ const { cmdServe } = await import("./serve-CaIDZfhy.js");
558
558
  return cmdServe(rest);
559
559
  }
560
560
  case "setup": {
561
- const { cmdSetup } = await import("./setup-6R_yJsKX.js");
561
+ const { cmdSetup } = await import("./setup-B15dHEd1.js");
562
562
  return cmdSetup(rest);
563
563
  }
564
564
  case "schedule": {
565
- const { cmdSchedule } = await import("./schedule-DkTLO7No.js");
565
+ const { cmdSchedule } = await import("./schedule-BJk96cOy.js");
566
566
  return cmdSchedule(rest);
567
567
  }
568
568
  case "remote": {
@@ -2381,7 +2381,7 @@ async function cmdStatus(rest) {
2381
2381
  cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
2382
2382
  };
2383
2383
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
2384
- if (!keyword) throw new Error("usage: ay status <keyword> [--watch | --wait | --wait-idle] [--timeout=Ns]");
2384
+ if (!keyword) throw new Error("usage: ay status <keyword> [--watch | --wait | --wait-idle] [--timeout=Ns] [--cwd=DIR] [--latest]");
2385
2385
  {
2386
2386
  const remote = await resolveRemoteSpec(keyword);
2387
2387
  if (remote) return runRemoteStatus(remote);
@@ -2593,4 +2593,4 @@ async function cmdResultSet(rest) {
2593
2593
 
2594
2594
  //#endregion
2595
2595
  export { stopTipForCli as C, snapshotStatus as S, renderRawLog as _, cursorAbs as a, resolveReadWindow as b, extractTaskCounts as c, isExitRequest as d, isPidAlive as f, readNotes as g, matchKeyword as h, controlCodeFromName as i, finalizedLines as l, listRecords as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isSubcommand as p, cmdHelp as r, extractNeedsInput as s, GRACEFUL_EXIT_COMMANDS as t, isAgentStuck as u, renderRawLogLines as v, writeToIpc as w, runSubcommand as x, resolveOne as y };
2596
- //# sourceMappingURL=subcommands-Df_SjmLN.js.map
2596
+ //# sourceMappingURL=subcommands-OyK7Y447.js.map
@@ -1,5 +1,5 @@
1
1
  import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
2
- import { r as getInstalledPackage } from "./versionChecker-DfO37H1R.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-C-JJk14E.js";
3
3
  import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
4
4
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-V4qvXgAw.js";
5
5
  import { t as PidStore } from "./pidStore-fqXqTKkh.js";
@@ -1800,4 +1800,4 @@ function sleep(ms) {
1800
1800
 
1801
1801
  //#endregion
1802
1802
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1803
- //# sourceMappingURL=ts-CaCRPaos.js.map
1803
+ //# sourceMappingURL=ts-C21nFgXQ.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "claude-yes";
10
- var version = "1.148.0";
10
+ var version = "1.149.1";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-DfO37H1R.js.map
218
+ //# sourceMappingURL=versionChecker-C-JJk14E.js.map
@@ -0,0 +1,91 @@
1
+ import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import path from "path";
5
+
6
+ //#region ts/workspaceConfig.ts
7
+ function configPath() {
8
+ return path.join(agentYesHome(), "config.json");
9
+ }
10
+ function readConfig() {
11
+ try {
12
+ return JSON.parse(readFileSync(configPath(), "utf-8"));
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+ /** Expand a leading `~` (`~` or `~/x`) to an absolute home-based path. */
18
+ function expandTilde(p) {
19
+ const s = p.trim();
20
+ if (s === "~") return homedir();
21
+ if (s.startsWith("~/") || s.startsWith("~\\")) return path.join(homedir(), s.slice(2));
22
+ return s;
23
+ }
24
+ /** The configured workspace root (absolute), or the home dir if unset. */
25
+ function getWorkspaceRoot() {
26
+ const w = readConfig().workspace;
27
+ return w && w.trim() ? w : homedir();
28
+ }
29
+ /**
30
+ * Root for `from`-provisioned worktrees, handed to codehost/provision so they
31
+ * land in `<root>/<owner>/<repo>/tree/<branch>`. Resolution order: env
32
+ * `CODEHOST_WS_ROOT` wins (ops override), then the configured `provisionRoot`,
33
+ * else undefined — letting codehost/provision fall back to its own `~/ws`
34
+ * default. Kept separate from `workspace` (the plain-cwd default), which may be a
35
+ * specific project dir rather than a root.
36
+ */
37
+ function getProvisionRoot() {
38
+ const env = process.env.CODEHOST_WS_ROOT?.trim();
39
+ if (env) return path.resolve(expandTilde(env));
40
+ const r = readConfig().provisionRoot;
41
+ return r && r.trim() ? path.resolve(expandTilde(r)) : void 0;
42
+ }
43
+ /**
44
+ * Owner/repo allowlist for `from`-provisioning. Provisioning clones a repo and
45
+ * runs its setup script (dependency installs + package lifecycle hooks = code
46
+ * execution on the host), so an **empty allowlist means DENY ALL** — a secure
47
+ * default the host opts out of by listing owners it trusts. Entries match
48
+ * `<owner>` (any repo of that owner), `<owner>/<repo>` (exact), or `*` (allow
49
+ * all — an explicit opt-in to the wide-open behavior). Env
50
+ * `CODEHOST_PROVISION_ALLOWLIST` (comma-separated) overrides the config.
51
+ */
52
+ function getProvisionAllowlist() {
53
+ const env = process.env.CODEHOST_PROVISION_ALLOWLIST?.trim();
54
+ return (env ? env.split(",") : readConfig().provisionAllowlist ?? []).map((s) => s.trim().toLowerCase()).filter(Boolean);
55
+ }
56
+ /** Whether `<owner>/<repo>` may be `from`-provisioned, per {@link getProvisionAllowlist}. */
57
+ function isProvisionAllowed(owner, repo) {
58
+ const list = getProvisionAllowlist();
59
+ if (list.includes("*")) return true;
60
+ const o = owner.toLowerCase();
61
+ const full = `${owner}/${repo}`.toLowerCase();
62
+ return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
63
+ }
64
+ /** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
65
+ function setWorkspaceRoot(dir) {
66
+ const abs = path.resolve(expandTilde(dir));
67
+ const cfg = readConfig();
68
+ cfg.workspace = abs;
69
+ mkdirSync(agentYesHome(), { recursive: true });
70
+ writeFileSync(configPath(), JSON.stringify(cfg, null, 2));
71
+ return abs;
72
+ }
73
+ /**
74
+ * Resolve a user-supplied spawn location to an absolute cwd:
75
+ * - empty → the workspace root
76
+ * - a bare name → `<workspace>/<name>` (so "myproject" lands under the root)
77
+ * - `~`-prefixed → home-based absolute
78
+ * - anything with a path separator → resolved as-is
79
+ */
80
+ function resolveSpawnCwd(input) {
81
+ const root = getWorkspaceRoot();
82
+ const v = (input ?? "").trim();
83
+ if (!v) return root;
84
+ if (v.startsWith("~")) return path.resolve(expandTilde(v));
85
+ if (v.includes("/") || v.includes("\\") || path.isAbsolute(v)) return path.resolve(v);
86
+ return path.join(root, v);
87
+ }
88
+
89
+ //#endregion
90
+ export { setWorkspaceRoot as a, resolveSpawnCwd as i, getWorkspaceRoot as n, isProvisionAllowed as r, getProvisionRoot as t };
91
+ //# sourceMappingURL=workspaceConfig-B9VX8tVO.js.map
package/lab/ui/index.html CHANGED
@@ -4334,6 +4334,7 @@
4334
4334
  const before = new Set(entries.filter((e) => e._room === target.id).map((e) => e.pid));
4335
4335
  const res = await target.tx.post("/api/spawn", {
4336
4336
  cli: spec.cli || "claude",
4337
+ from: spec.from || undefined,
4337
4338
  cwd: spec.cwd || undefined,
4338
4339
  prompt: spec.prompt || undefined,
4339
4340
  });
@@ -4385,7 +4386,8 @@
4385
4386
  $("newform").innerHTML = `<div class="lcard">
4386
4387
  <div class="ltitle">New agent · ${esc(where)}</div>
4387
4388
  <div class="nfield"><label>CLI</label><input id="nf-cli" value="claude" spellcheck="false" autocapitalize="off" /></div>
4388
- <div class="nfield"><label>Working dir</label><input id="nf-cwd" value="${esc(cwd)}" placeholder="(host default)" spellcheck="false" autocapitalize="off" /></div>
4389
+ <div class="nfield"><label>Spawn from — optional</label><input id="nf-from" placeholder="github URL / owner/repo@branch — provisions a worktree" spellcheck="false" autocapitalize="off" /></div>
4390
+ <div class="nfield"><label>Working dir</label><input id="nf-cwd" value="${esc(cwd)}" placeholder="(host default — ignored when Spawn from is set)" spellcheck="false" autocapitalize="off" /></div>
4389
4391
  <div class="nfield"><label>Prompt — optional</label><textarea id="nf-prompt" rows="3" placeholder="initial message to send the agent…"></textarea></div>
4390
4392
  <div class="lrow">
4391
4393
  <button class="lfleet" id="nf-go">▷ Launch</button>
@@ -4401,6 +4403,7 @@
4401
4403
  if (!go || go.disabled) return;
4402
4404
  const spec = {
4403
4405
  cli: ($("nf-cli").value || "claude").trim(),
4406
+ from: $("nf-from")?.value.trim() || "",
4404
4407
  cwd: $("nf-cwd").value.trim(),
4405
4408
  prompt: $("nf-prompt").value,
4406
4409
  };
@@ -4409,7 +4412,7 @@
4409
4412
  // target fleet. A 2nd `claude -c` in the same repo fights over the same
4410
4413
  // session/files and usually exits on startup — which looks like "the new
4411
4414
  // agent never appears". Let the user pick a different dir or proceed.
4412
- if (spec.cwd) {
4415
+ if (spec.cwd && !spec.from) {
4413
4416
  await loadList();
4414
4417
  const norm = (p) => (p || "").replace(/\/+$/, "");
4415
4418
  const busy = entries.some(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.148.0",
3
+ "version": "1.149.1",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
package/ts/serve.ts CHANGED
@@ -21,9 +21,33 @@ import { updateGlobalPidStatus } from "./globalPidIndex.ts";
21
21
  import { pgidForWrapper } from "./reaper.ts";
22
22
  import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
23
23
  import { getInstalledPackage } from "./versionChecker.ts";
24
+ import { getProvisionRoot, isProvisionAllowed, resolveSpawnCwd } from "./workspaceConfig.ts";
24
25
 
25
26
  const DEFAULT_PORT = 7432;
26
27
 
28
+ /**
29
+ * Normalize a user-supplied GitHub-ish source into the standard
30
+ * `<owner>/<repo>/tree/<branch>` path that codehost/provision's `parseSpec`
31
+ * understands. Interim fallback used only when the linked codehost build
32
+ * predates `parseSource` (the canonical normalizer in the standard):
33
+ * https://github.com/o/r/tree/b · github.com/o/r/tree/b · o/r/tree/b
34
+ * o/r@branch · o/r (→ default branch main)
35
+ */
36
+ function normalizeGithubSource(s: string): string {
37
+ let v = s
38
+ .trim()
39
+ .replace(/^https?:\/\//, "")
40
+ .replace(/^github\.com\//, "");
41
+ v = v
42
+ .replace(/[?#].*$/, "")
43
+ .replace(/\.git$/, "")
44
+ .replace(/^\/+|\/+$/g, "");
45
+ const at = v.match(/^([^/]+)\/([^/@]+)@(.+)$/);
46
+ if (at) return `${at[1]}/${at[2]}/tree/${at[3]}`;
47
+ if (/^[^/]+\/[^/]+$/.test(v)) return `${v}/tree/main`;
48
+ return v;
49
+ }
50
+
27
51
  function agentYesHome(): string {
28
52
  return process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
29
53
  }
@@ -1448,9 +1472,13 @@ export async function cmdServe(rest: string[]): Promise<number> {
1448
1472
  return Response.json(live);
1449
1473
  }
1450
1474
 
1451
- // POST /api/spawn body {cli, cwd, prompt} — launch a new agent
1475
+ // POST /api/spawn body {cli, cwd?, from?, prompt?} — launch a new agent.
1476
+ // `from` (a GitHub URL / owner/repo@branch / owner/repo/tree/branch) is
1477
+ // resolved to a ready worktree via codehost/provision (the shared workspace
1478
+ // standard); otherwise `cwd` is resolved against the workspace root and
1479
+ // created if missing (Layer-0 plain-dir provisioning — no more ENOENT 500).
1452
1480
  if (req.method === "POST" && p === "/api/spawn") {
1453
- let body: { cli?: string; cwd?: string; prompt?: string };
1481
+ let body: { cli?: string; cwd?: string; from?: string; prompt?: string };
1454
1482
  try {
1455
1483
  body = (await req.json()) as typeof body;
1456
1484
  } catch {
@@ -1459,10 +1487,73 @@ export async function cmdServe(rest: string[]): Promise<number> {
1459
1487
  const cli = String(body.cli ?? "claude");
1460
1488
  if (!SUPPORTED_CLIS.includes(cli as never))
1461
1489
  return new Response(`unsupported cli: ${cli}`, { status: 400 });
1462
- const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
1463
1490
  const prompt = String(body.prompt ?? "");
1491
+
1492
+ // Resolve the working directory. A `from` source is provisioned (clone /
1493
+ // worktree) through codehost/provision; a plain `cwd` is resolved to the
1494
+ // workspace root and mkdir-p'd so a missing dir no longer ENOENTs.
1495
+ let cwd: string;
1496
+ let provisioned: { action: string; folder: string } | null = null;
1497
+ const from = typeof body.from === "string" ? body.from.trim() : "";
1498
+ if (from) {
1499
+ type Spec = { owner: string; repo: string; branch: string };
1500
+ let prov: {
1501
+ parseSource?: (s: string) => Spec | null;
1502
+ parseSpec: (s: string) => Spec | null;
1503
+ provision: (
1504
+ spec: Spec,
1505
+ opts?: { wsRoot?: string },
1506
+ ) => Promise<{ ok: boolean; folder: string; action: string; error?: string }>;
1507
+ };
1508
+ try {
1509
+ prov = (await import("codehost/provision")) as typeof prov;
1510
+ } catch (e) {
1511
+ return new Response(
1512
+ `spawn-from needs the 'codehost' package (codehost/provision) — install it ` +
1513
+ `(npm i -g codehost) or 'bun link' it for local dev: ${(e as Error).message}`,
1514
+ { status: 501 },
1515
+ );
1516
+ }
1517
+ // Malformed input (e.g. bad %-encoding) must surface as 400, not a 500.
1518
+ let spec: Spec | null;
1519
+ try {
1520
+ spec =
1521
+ typeof prov.parseSource === "function"
1522
+ ? prov.parseSource(from)
1523
+ : prov.parseSpec(normalizeGithubSource(from));
1524
+ } catch {
1525
+ spec = null;
1526
+ }
1527
+ if (!spec) return new Response(`unrecognized spawn source: ${from}`, { status: 400 });
1528
+ // Provisioning clones the repo and runs its setup script (dependency
1529
+ // installs + package lifecycle hooks = code execution on the host), so
1530
+ // gate it behind an owner/repo allowlist — empty allowlist = deny all.
1531
+ if (!isProvisionAllowed(spec.owner, spec.repo))
1532
+ return new Response(
1533
+ `provisioning '${spec.owner}/${spec.repo}' is not allowed — add the owner to ` +
1534
+ `provisionAllowlist in ~/.agent-yes/config.json (or "*" to allow all)`,
1535
+ { status: 403 },
1536
+ );
1537
+ let result: { ok: boolean; folder: string; action: string; error?: string };
1538
+ try {
1539
+ const wsRoot = getProvisionRoot();
1540
+ result = await prov.provision(spec, wsRoot ? { wsRoot } : undefined);
1541
+ } catch (e) {
1542
+ return new Response(`provision failed: ${(e as Error).message}`, { status: 502 });
1543
+ }
1544
+ if (!result?.ok) return new Response(result?.error ?? "provision failed", { status: 502 });
1545
+ cwd = result.folder;
1546
+ provisioned = { action: result.action, folder: result.folder };
1547
+ } else {
1548
+ cwd = resolveSpawnCwd(body.cwd);
1549
+ try {
1550
+ await mkdir(cwd, { recursive: true });
1551
+ } catch (e) {
1552
+ return new Response(`cannot create cwd ${cwd}: ${(e as Error).message}`, { status: 500 });
1553
+ }
1554
+ }
1464
1555
  process.stderr.write(
1465
- `→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd})\n`,
1556
+ `→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd}${provisioned ? `, ${provisioned.action} from ${from}` : ""})\n`,
1466
1557
  );
1467
1558
  // Resolve `ay` to an absolute command. The detached daemon (oxmgr/launchd/
1468
1559
  // pm2) usually has a PATH WITHOUT ~/.bun/bin, so a bare "ay" fails with
@@ -1483,7 +1574,13 @@ export async function cmdServe(rest: string[]): Promise<number> {
1483
1574
  stderr: "ignore",
1484
1575
  });
1485
1576
  child.unref();
1486
- return Response.json({ ok: true, pid: child.pid, cli, cwd });
1577
+ return Response.json({
1578
+ ok: true,
1579
+ pid: child.pid,
1580
+ cli,
1581
+ cwd,
1582
+ ...(provisioned ? { provisioned } : {}),
1583
+ });
1487
1584
  } catch (e) {
1488
1585
  return new Response((e as Error).message, { status: 500 });
1489
1586
  }
package/ts/subcommands.ts CHANGED
@@ -2911,7 +2911,9 @@ async function cmdStatus(rest: string[]): Promise<number> {
2911
2911
  const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
2912
2912
 
2913
2913
  if (!keyword)
2914
- throw new Error("usage: ay status <keyword> [--watch | --wait | --wait-idle] [--timeout=Ns]");
2914
+ throw new Error(
2915
+ "usage: ay status <keyword> [--watch | --wait | --wait-idle] [--timeout=Ns] [--cwd=DIR] [--latest]",
2916
+ );
2915
2917
 
2916
2918
  {
2917
2919
  const remote = await resolveRemoteSpec(keyword);
@@ -1,10 +1,13 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { mkdtempSync, rmSync } from "fs";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
3
3
  import { homedir, tmpdir } from "os";
4
4
  import path from "path";
5
5
  import {
6
6
  expandTilde,
7
+ getProvisionAllowlist,
8
+ getProvisionRoot,
7
9
  getWorkspaceRoot,
10
+ isProvisionAllowed,
8
11
  resolveSpawnCwd,
9
12
  setWorkspaceRoot,
10
13
  } from "./workspaceConfig.ts";
@@ -20,6 +23,8 @@ describe("workspaceConfig", () => {
20
23
  afterEach(() => {
21
24
  if (original === undefined) delete process.env.AGENT_YES_HOME;
22
25
  else process.env.AGENT_YES_HOME = original;
26
+ delete process.env.CODEHOST_WS_ROOT;
27
+ delete process.env.CODEHOST_PROVISION_ALLOWLIST;
23
28
  rmSync(tmp, { recursive: true, force: true });
24
29
  });
25
30
 
@@ -71,4 +76,70 @@ describe("workspaceConfig", () => {
71
76
  expect(resolveSpawnCwd("a/b")).toBe(path.resolve("a/b"));
72
77
  });
73
78
  });
79
+
80
+ const writeConfig = (c: Record<string, unknown>) =>
81
+ writeFileSync(path.join(tmp, "config.json"), JSON.stringify(c));
82
+
83
+ describe("getProvisionRoot", () => {
84
+ it("is undefined when neither env nor config is set", () => {
85
+ expect(getProvisionRoot()).toBeUndefined();
86
+ });
87
+
88
+ it("returns the configured provisionRoot, resolved", () => {
89
+ writeConfig({ provisionRoot: "/code" });
90
+ expect(getProvisionRoot()).toBe(path.resolve("/code"));
91
+ });
92
+
93
+ it("env CODEHOST_WS_ROOT overrides the config and expands ~", () => {
94
+ writeConfig({ provisionRoot: "/code" });
95
+ process.env.CODEHOST_WS_ROOT = "~/ws";
96
+ expect(getProvisionRoot()).toBe(path.join(homedir(), "ws"));
97
+ });
98
+
99
+ it("ignores a blank configured value", () => {
100
+ writeConfig({ provisionRoot: " " });
101
+ expect(getProvisionRoot()).toBeUndefined();
102
+ });
103
+ });
104
+
105
+ describe("getProvisionAllowlist", () => {
106
+ it("is empty when unset", () => {
107
+ expect(getProvisionAllowlist()).toEqual([]);
108
+ });
109
+
110
+ it("reads and normalizes the configured list (trim/lowercase/drop empties)", () => {
111
+ writeConfig({ provisionAllowlist: [" Snomiao ", "Acme/Repo", ""] });
112
+ expect(getProvisionAllowlist()).toEqual(["snomiao", "acme/repo"]);
113
+ });
114
+
115
+ it("env CODEHOST_PROVISION_ALLOWLIST (comma-separated) overrides config", () => {
116
+ writeConfig({ provisionAllowlist: ["snomiao"] });
117
+ process.env.CODEHOST_PROVISION_ALLOWLIST = "Foo, bar/baz ,";
118
+ expect(getProvisionAllowlist()).toEqual(["foo", "bar/baz"]);
119
+ });
120
+ });
121
+
122
+ describe("isProvisionAllowed", () => {
123
+ it("denies everything when the allowlist is empty (secure default)", () => {
124
+ expect(isProvisionAllowed("snomiao", "agent-yes")).toBe(false);
125
+ });
126
+
127
+ it("'*' allows any owner/repo", () => {
128
+ writeConfig({ provisionAllowlist: ["*"] });
129
+ expect(isProvisionAllowed("anyone", "anything")).toBe(true);
130
+ });
131
+
132
+ it("matches by owner case-insensitively and rejects others", () => {
133
+ writeConfig({ provisionAllowlist: ["snomiao"] });
134
+ expect(isProvisionAllowed("SNOMIAO", "x")).toBe(true);
135
+ expect(isProvisionAllowed("evil", "x")).toBe(false);
136
+ });
137
+
138
+ it("matches an exact owner/repo and an owner/* glob", () => {
139
+ writeConfig({ provisionAllowlist: ["acme/widget", "org/*"] });
140
+ expect(isProvisionAllowed("acme", "widget")).toBe(true);
141
+ expect(isProvisionAllowed("acme", "other")).toBe(false);
142
+ expect(isProvisionAllowed("org", "anything")).toBe(true);
143
+ });
144
+ });
74
145
  });
@@ -15,6 +15,10 @@ import { agentYesHome } from "./agentYesHome.ts";
15
15
 
16
16
  interface Config {
17
17
  workspace?: string;
18
+ /** Root for `from`-provisioned worktrees (`<root>/<owner>/<repo>/tree/<branch>`). */
19
+ provisionRoot?: string;
20
+ /** Owners/repos permitted for `from`-provisioning; empty = deny all. */
21
+ provisionAllowlist?: string[];
18
22
  }
19
23
 
20
24
  function configPath(): string {
@@ -43,6 +47,45 @@ export function getWorkspaceRoot(): string {
43
47
  return w && w.trim() ? w : homedir();
44
48
  }
45
49
 
50
+ /**
51
+ * Root for `from`-provisioned worktrees, handed to codehost/provision so they
52
+ * land in `<root>/<owner>/<repo>/tree/<branch>`. Resolution order: env
53
+ * `CODEHOST_WS_ROOT` wins (ops override), then the configured `provisionRoot`,
54
+ * else undefined — letting codehost/provision fall back to its own `~/ws`
55
+ * default. Kept separate from `workspace` (the plain-cwd default), which may be a
56
+ * specific project dir rather than a root.
57
+ */
58
+ export function getProvisionRoot(): string | undefined {
59
+ const env = process.env.CODEHOST_WS_ROOT?.trim();
60
+ if (env) return path.resolve(expandTilde(env));
61
+ const r = readConfig().provisionRoot;
62
+ return r && r.trim() ? path.resolve(expandTilde(r)) : undefined;
63
+ }
64
+
65
+ /**
66
+ * Owner/repo allowlist for `from`-provisioning. Provisioning clones a repo and
67
+ * runs its setup script (dependency installs + package lifecycle hooks = code
68
+ * execution on the host), so an **empty allowlist means DENY ALL** — a secure
69
+ * default the host opts out of by listing owners it trusts. Entries match
70
+ * `<owner>` (any repo of that owner), `<owner>/<repo>` (exact), or `*` (allow
71
+ * all — an explicit opt-in to the wide-open behavior). Env
72
+ * `CODEHOST_PROVISION_ALLOWLIST` (comma-separated) overrides the config.
73
+ */
74
+ export function getProvisionAllowlist(): string[] {
75
+ const env = process.env.CODEHOST_PROVISION_ALLOWLIST?.trim();
76
+ const raw = env ? env.split(",") : (readConfig().provisionAllowlist ?? []);
77
+ return raw.map((s) => s.trim().toLowerCase()).filter(Boolean);
78
+ }
79
+
80
+ /** Whether `<owner>/<repo>` may be `from`-provisioned, per {@link getProvisionAllowlist}. */
81
+ export function isProvisionAllowed(owner: string, repo: string): boolean {
82
+ const list = getProvisionAllowlist();
83
+ if (list.includes("*")) return true;
84
+ const o = owner.toLowerCase();
85
+ const full = `${owner}/${repo}`.toLowerCase();
86
+ return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
87
+ }
88
+
46
89
  /** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
47
90
  export function setWorkspaceRoot(dir: string): string {
48
91
  const abs = path.resolve(expandTilde(dir));
@@ -1,8 +0,0 @@
1
- import "./ts-CaCRPaos.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-DfO37H1R.js";
4
- import "./pidStore-fqXqTKkh.js";
5
- import "./globalPidIndex-DlmmJlO8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CmsPbgMe.js";
7
-
8
- export { SUPPORTED_CLIS };
@@ -1,56 +0,0 @@
1
- import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
2
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
- import { homedir } from "os";
4
- import path from "path";
5
-
6
- //#region ts/workspaceConfig.ts
7
- function configPath() {
8
- return path.join(agentYesHome(), "config.json");
9
- }
10
- function readConfig() {
11
- try {
12
- return JSON.parse(readFileSync(configPath(), "utf-8"));
13
- } catch {
14
- return {};
15
- }
16
- }
17
- /** Expand a leading `~` (`~` or `~/x`) to an absolute home-based path. */
18
- function expandTilde(p) {
19
- const s = p.trim();
20
- if (s === "~") return homedir();
21
- if (s.startsWith("~/") || s.startsWith("~\\")) return path.join(homedir(), s.slice(2));
22
- return s;
23
- }
24
- /** The configured workspace root (absolute), or the home dir if unset. */
25
- function getWorkspaceRoot() {
26
- const w = readConfig().workspace;
27
- return w && w.trim() ? w : homedir();
28
- }
29
- /** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
30
- function setWorkspaceRoot(dir) {
31
- const abs = path.resolve(expandTilde(dir));
32
- const cfg = readConfig();
33
- cfg.workspace = abs;
34
- mkdirSync(agentYesHome(), { recursive: true });
35
- writeFileSync(configPath(), JSON.stringify(cfg, null, 2));
36
- return abs;
37
- }
38
- /**
39
- * Resolve a user-supplied spawn location to an absolute cwd:
40
- * - empty → the workspace root
41
- * - a bare name → `<workspace>/<name>` (so "myproject" lands under the root)
42
- * - `~`-prefixed → home-based absolute
43
- * - anything with a path separator → resolved as-is
44
- */
45
- function resolveSpawnCwd(input) {
46
- const root = getWorkspaceRoot();
47
- const v = (input ?? "").trim();
48
- if (!v) return root;
49
- if (v.startsWith("~")) return path.resolve(expandTilde(v));
50
- if (v.includes("/") || v.includes("\\") || path.isAbsolute(v)) return path.resolve(v);
51
- return path.join(root, v);
52
- }
53
-
54
- //#endregion
55
- export { resolveSpawnCwd as n, setWorkspaceRoot as r, getWorkspaceRoot as t };
56
- //# sourceMappingURL=workspaceConfig-B3ylOZAO.js.map