agent-yes 1.199.0 → 1.200.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.
@@ -0,0 +1,9 @@
1
+ import "./ts-Oyau4Eap.js";
2
+ import "./logger-CDIsZ-Pp.js";
3
+ import "./versionChecker-CeLrr-kQ.js";
4
+ import "./pidStore-BIvsBQ8X.js";
5
+ import "./globalPidIndex-CoNr7tS8.js";
6
+ import "./messageLog-CxrKJj77.js";
7
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dt-oEBTl.js";
8
+
9
+ export { SUPPORTED_CLIS };
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-Eb2ahFa4.js";
1
+ import { t as CLIS_CONFIG } from "./ts-Oyau4Eap.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-DsL6jtBQ.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-Dt-oEBTl.js.map
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
2
2
  import "./globalPidIndex-CoNr7tS8.js";
3
3
  import "./messageLog-CxrKJj77.js";
4
4
  import "./configShared-aKTg-sa5.js";
5
- import { M as resolveOne } from "./subcommands-BUOLbYrj.js";
5
+ import { M as resolveOne } from "./subcommands-DvXyUjWT.js";
6
6
  import "./e2e-jb0Hp43q.js";
7
7
  import "./webrtcLink-B7REGtK2.js";
8
8
  import "./remotes-Cim0dBU7.js";
@@ -229,4 +229,4 @@ function transformEvent(rawEvent, agentId, forwarded) {
229
229
 
230
230
  //#endregion
231
231
  export { createScopedShare, listShares, revokeAllShares, revokeShare };
232
- //# sourceMappingURL=agentShare-zgnsqnOe.js.map
232
+ //# sourceMappingURL=agentShare-D-9ppbuL.js.map
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
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, t as checkAndAutoUpdate } from "./versionChecker-smYufLRy.js";
5
- import { n as getRustBinary } from "./rustBinary-DO01lk60.js";
4
+ import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-CeLrr-kQ.js";
5
+ import { n as getRustBinary } from "./rustBinary-C5SrmIUM.js";
6
6
  import { argv } from "process";
7
7
  import { spawn } from "child_process";
8
8
  import ms from "ms";
@@ -285,7 +285,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
285
285
  const rawArg = process.argv[2];
286
286
  const managerCommands = !invokedCliName(process.argv);
287
287
  const isHelpFlag = rawArg === "-h" || rawArg === "--help";
288
- const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-CUYXMkbr.js");
288
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-g1kLml5I.js");
289
289
  if (isHelpFlag && process.argv.length === 3) {
290
290
  await cmdHelp(managerCommands);
291
291
  process.exit(0);
@@ -327,7 +327,7 @@ if (config.useRust) {
327
327
  }
328
328
  }
329
329
  if (rustBinary) {
330
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-CwLbzhgz.js");
330
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-D8PoWxsA.js");
331
331
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
332
332
  if (config.verbose) {
333
333
  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-Eb2ahFa4.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-Oyau4Eap.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-smYufLRy.js";
3
+ import "./versionChecker-CeLrr-kQ.js";
4
4
  import "./pidStore-BIvsBQ8X.js";
5
5
  import "./globalPidIndex-CoNr7tS8.js";
6
6
  import "./messageLog-CxrKJj77.js";
@@ -2,7 +2,7 @@ import { n as logger } from "./logger-CDIsZ-Pp.js";
2
2
  import "./globalPidIndex-CoNr7tS8.js";
3
3
  import "./messageLog-CxrKJj77.js";
4
4
  import "./configShared-aKTg-sa5.js";
5
- import { $ as notifyDir, G as gcInboxes, J as liveWatchers, K as hostId, Q as daemonLockOwnerPath, S as listRecords, W as appendEvent, X as shouldStealLock, Y as readInbox, Z as daemonLockDir, _ as isPidAlive, c as deriveLiveState, k as renderLogTailLines, q as listInboxParents } from "./subcommands-BUOLbYrj.js";
5
+ import { $ as notifyDir, G as gcInboxes, J as liveWatchers, K as hostId, Q as daemonLockOwnerPath, S as listRecords, W as appendEvent, X as shouldStealLock, Y as readInbox, Z as daemonLockDir, _ as isPidAlive, c as deriveLiveState, k as renderLogTailLines, q as listInboxParents } from "./subcommands-DvXyUjWT.js";
6
6
  import "./e2e-jb0Hp43q.js";
7
7
  import "./webrtcLink-B7REGtK2.js";
8
8
  import "./remotes-Cim0dBU7.js";
@@ -589,4 +589,4 @@ async function ensureDaemon() {
589
589
 
590
590
  //#endregion
591
591
  export { daemonStatus, ensureDaemon, requestDaemonStop, runDaemon };
592
- //# sourceMappingURL=notifyDaemon-BzAh0fhX.js.map
592
+ //# sourceMappingURL=notifyDaemon-CQZ_wPhN.js.map
@@ -1,4 +1,4 @@
1
- import { r as getInstalledPackage } from "./versionChecker-smYufLRy.js";
1
+ import { r as getInstalledPackage } from "./versionChecker-CeLrr-kQ.js";
2
2
  import { execFileSync } from "child_process";
3
3
  import { existsSync, mkdirSync, unlinkSync } from "fs";
4
4
  import { chmod, copyFile } from "fs/promises";
@@ -225,4 +225,4 @@ async function getRustBinary(options = {}) {
225
225
 
226
226
  //#endregion
227
227
  export { getRustBinary as n, findSpawnHiddenLauncher as t };
228
- //# sourceMappingURL=rustBinary-DO01lk60.js.map
228
+ //# sourceMappingURL=rustBinary-C5SrmIUM.js.map
@@ -1,10 +1,10 @@
1
- import "./ts-Eb2ahFa4.js";
1
+ import "./ts-Oyau4Eap.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-smYufLRy.js";
3
+ import "./versionChecker-CeLrr-kQ.js";
4
4
  import "./pidStore-BIvsBQ8X.js";
5
5
  import "./globalPidIndex-CoNr7tS8.js";
6
6
  import "./messageLog-CxrKJj77.js";
7
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DsL6jtBQ.js";
7
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dt-oEBTl.js";
8
8
  import { d as resolveSpawnCwd } from "./workspaceConfig-CjaRvTjf.js";
9
9
  import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-CwNJuwH5.js";
10
10
  import { createHash } from "node:crypto";
@@ -151,4 +151,4 @@ async function cmdSchedule(rest) {
151
151
 
152
152
  //#endregion
153
153
  export { cmdSchedule };
154
- //# sourceMappingURL=schedule-5bHcF_VU.js.map
154
+ //# sourceMappingURL=schedule-lPUt6BkE.js.map
@@ -1,14 +1,14 @@
1
- import "./ts-Eb2ahFa4.js";
1
+ import "./ts-Oyau4Eap.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import { r as getInstalledPackage } from "./versionChecker-smYufLRy.js";
4
- import { t as findSpawnHiddenLauncher } from "./rustBinary-DO01lk60.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-CeLrr-kQ.js";
4
+ import { t as findSpawnHiddenLauncher } from "./rustBinary-C5SrmIUM.js";
5
5
  import "./pidStore-BIvsBQ8X.js";
6
6
  import { a as updateGlobalPidStatus } from "./globalPidIndex-CoNr7tS8.js";
7
7
  import { r as recordInbox } from "./messageLog-CxrKJj77.js";
8
8
  import { t as pgidForWrapper } from "./reaper-BUHCyxdF.js";
9
9
  import "./configShared-aKTg-sa5.js";
10
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DsL6jtBQ.js";
11
- import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, et as TYPING_BADGE, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, u as extractBadges } from "./subcommands-BUOLbYrj.js";
10
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dt-oEBTl.js";
11
+ import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, et as TYPING_BADGE, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, u as extractBadges } from "./subcommands-DvXyUjWT.js";
12
12
  import "./e2e-jb0Hp43q.js";
13
13
  import "./webrtcLink-B7REGtK2.js";
14
14
  import "./remotes-Cim0dBU7.js";
@@ -22,7 +22,7 @@ import path from "path";
22
22
  import { execFileSync } from "node:child_process";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { randomBytes, timingSafeEqual } from "crypto";
25
- import { renameSync, watch, writeFileSync } from "node:fs";
25
+ import { existsSync, renameSync, watch, writeFileSync } from "node:fs";
26
26
 
27
27
  //#region ts/terminalReply.ts
28
28
  /**
@@ -1441,6 +1441,48 @@ Options:
1441
1441
  provision: hasProvisionHook()
1442
1442
  }
1443
1443
  });
1444
+ if (req.method === "GET" && p === "/api/ws") {
1445
+ const ws = await import("./ws-DZDjlG3o.js");
1446
+ try {
1447
+ await ws.loadProvision();
1448
+ } catch (e) {
1449
+ return new Response(e.message, { status: 501 });
1450
+ }
1451
+ try {
1452
+ const { wsRoot, workspaces } = await ws.collectWorkspaces();
1453
+ return Response.json({
1454
+ schema: ws.WS_JSON_SCHEMA,
1455
+ wsRoot,
1456
+ workspaces
1457
+ });
1458
+ } catch (e) {
1459
+ return new Response(`workspace walk failed: ${e.message}`, { status: 500 });
1460
+ }
1461
+ }
1462
+ if (req.method === "GET" && p === "/api/ws/status") {
1463
+ const dirRaw = url.searchParams.get("path");
1464
+ if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
1465
+ const ws = await import("./ws-DZDjlG3o.js");
1466
+ let prov;
1467
+ try {
1468
+ prov = await ws.loadProvision();
1469
+ } catch (e) {
1470
+ return new Response(e.message, { status: 501 });
1471
+ }
1472
+ const wsRoot = prov.resolveWsRoot(getProvisionRoot());
1473
+ const dir = path.resolve(dirRaw);
1474
+ if (!ws.isPathInside(wsRoot, dir)) return new Response(`path is outside the workspace root ${wsRoot}`, { status: 400 });
1475
+ if (!existsSync(path.join(dir, ".git"))) return new Response(`not a workspace checkout (no .git): ${dir}`, { status: 404 });
1476
+ try {
1477
+ const workspace = await ws.workspaceStatus(dir);
1478
+ return Response.json({
1479
+ schema: ws.WS_JSON_SCHEMA,
1480
+ workspace
1481
+ });
1482
+ } catch (e) {
1483
+ return new Response(`git status failed: ${e.message}`, { status: 502 });
1484
+ }
1485
+ }
1444
1486
  if (req.method === "GET" && p === "/api/notes") {
1445
1487
  const notes = await readNotes();
1446
1488
  return Response.json(Object.fromEntries(notes));
@@ -1976,7 +2018,7 @@ Options:
1976
2018
  const perm = body.perm ?? "r";
1977
2019
  if (perm !== "r" && perm !== "rw") return new Response(`invalid perm ${perm} (want r or rw)`, { status: 400 });
1978
2020
  try {
1979
- const { createScopedShare } = await import("./agentShare-zgnsqnOe.js");
2021
+ const { createScopedShare } = await import("./agentShare-D-9ppbuL.js");
1980
2022
  const share = await createScopedShare({
1981
2023
  agent: body.agent,
1982
2024
  perm,
@@ -1991,12 +2033,12 @@ Options:
1991
2033
  }
1992
2034
  }
1993
2035
  if (req.method === "GET" && p === "/api/shares") {
1994
- const { listShares } = await import("./agentShare-zgnsqnOe.js");
2036
+ const { listShares } = await import("./agentShare-D-9ppbuL.js");
1995
2037
  return Response.json(listShares());
1996
2038
  }
1997
2039
  const revokeM = /^\/api\/share\/([^/]+)$/.exec(p);
1998
2040
  if (req.method === "DELETE" && revokeM) {
1999
- const { revokeShare } = await import("./agentShare-zgnsqnOe.js");
2041
+ const { revokeShare } = await import("./agentShare-D-9ppbuL.js");
2000
2042
  const ok = revokeShare(decodeURIComponent(revokeM[1]));
2001
2043
  return new Response(ok ? "revoked" : "no such share", { status: ok ? 200 : 404 });
2002
2044
  }
@@ -2178,7 +2220,7 @@ Options:
2178
2220
  const shutdown = (resolve) => {
2179
2221
  if (heartbeat) clearInterval(heartbeat);
2180
2222
  closeShare?.();
2181
- import("./agentShare-zgnsqnOe.js").then((m) => m.revokeAllShares()).catch(() => {});
2223
+ import("./agentShare-D-9ppbuL.js").then((m) => m.revokeAllShares()).catch(() => {});
2182
2224
  server?.stop();
2183
2225
  resolve();
2184
2226
  };
@@ -2191,4 +2233,4 @@ Options:
2191
2233
 
2192
2234
  //#endregion
2193
2235
  export { cmdServe };
2194
- //# sourceMappingURL=serve-D9lJ0uN6.js.map
2236
+ //# sourceMappingURL=serve-DlUJ7Hg9.js.map
@@ -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-D9lJ0uN6.js");
35
+ const { cmdServe } = await import("./serve-DlUJ7Hg9.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-CNxIl1NG.js.map
45
+ //# sourceMappingURL=setup-Dalhb8_0.js.map
@@ -1184,19 +1184,19 @@ async function runSubcommand(argv) {
1184
1184
  case "restart": return await cmdRestart(rest);
1185
1185
  case "note": return await cmdNote(rest);
1186
1186
  case "serve": {
1187
- const { cmdServe } = await import("./serve-D9lJ0uN6.js");
1187
+ const { cmdServe } = await import("./serve-DlUJ7Hg9.js");
1188
1188
  return cmdServe(rest);
1189
1189
  }
1190
1190
  case "setup": {
1191
- const { cmdSetup } = await import("./setup-CNxIl1NG.js");
1191
+ const { cmdSetup } = await import("./setup-Dalhb8_0.js");
1192
1192
  return cmdSetup(rest);
1193
1193
  }
1194
1194
  case "ws": {
1195
- const { cmdWs } = await import("./ws-Bv1ZqkyE.js");
1195
+ const { cmdWs } = await import("./ws-DZDjlG3o.js");
1196
1196
  return cmdWs(rest);
1197
1197
  }
1198
1198
  case "schedule": {
1199
- const { cmdSchedule } = await import("./schedule-5bHcF_VU.js");
1199
+ const { cmdSchedule } = await import("./schedule-lPUt6BkE.js");
1200
1200
  return cmdSchedule(rest);
1201
1201
  }
1202
1202
  case "remote": {
@@ -4216,7 +4216,7 @@ async function cmdNotify(rest) {
4216
4216
  }
4217
4217
  const ensure = async () => {
4218
4218
  if (!argv["ensure-daemon"]) return;
4219
- const { ensureDaemon } = await import("./notifyDaemon-BzAh0fhX.js");
4219
+ const { ensureDaemon } = await import("./notifyDaemon-CQZ_wPhN.js");
4220
4220
  await ensureDaemon().catch(() => null);
4221
4221
  };
4222
4222
  await heartbeatWatcher(parent, selfStartedAt);
@@ -4290,7 +4290,7 @@ async function cmdNotifyCursor(args) {
4290
4290
  }
4291
4291
  async function cmdNotifyd(rest) {
4292
4292
  const sub = rest[0] ?? "status";
4293
- const daemon = await import("./notifyDaemon-BzAh0fhX.js");
4293
+ const daemon = await import("./notifyDaemon-CQZ_wPhN.js");
4294
4294
  switch (sub) {
4295
4295
  case "run": return daemon.runDaemon();
4296
4296
  case "once": return daemon.runDaemon({ once: true });
@@ -4317,4 +4317,4 @@ async function cmdNotifyd(rest) {
4317
4317
 
4318
4318
  //#endregion
4319
4319
  export { notifyDir as $, renderRawLog as A, submitAndConfirm as B, matchKeyword as C, recentMessageEdges as D, readPtysize as E, restartHintLines as F, gcInboxes as G, writeKeysPaced as H, runSubcommand as I, liveWatchers as J, hostId as K, snapshotStatus as L, resolveOne as M, resolveReadWindow as N, recentReadEdges as O, resolveResumeArgs as P, daemonLockOwnerPath as Q, stdinActivityPath as R, listRecords as S, readNotes as T, writeToIpc as U, waitForLogQuiet as V, appendEvent as W, shouldStealLock as X, readInbox as Y, daemonLockDir as Z, isPidAlive as _, cmdHelp as a, isUserTyping as b, deriveLiveState as c, extractMenu as d, TYPING_BADGE as et, extractNeedsInput as f, isExitRequest as g, isAgentStuck as h, backoffWhileTyping as i, renderRawLogLines as j, renderLogTailLines as k, deriveLiveStatus as l, finalizedLines as m, READ_PAGE_DEFAULT as n, controlCodeFromName as o, extractTaskCounts as p, listInboxParents as q, TYPING_WINDOW_MS as r, cursorAbs as s, GRACEFUL_EXIT_COMMANDS as t, extractBadges as u, isSlashCommand as v, menuSelectKeys as w, lastStdinAt as x, isSubcommand as y, stopTipForCli as z };
4320
- //# sourceMappingURL=subcommands-BUOLbYrj.js.map
4320
+ //# sourceMappingURL=subcommands-DvXyUjWT.js.map
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
2
2
  import "./globalPidIndex-CoNr7tS8.js";
3
3
  import "./messageLog-CxrKJj77.js";
4
4
  import "./configShared-aKTg-sa5.js";
5
- import { A as renderRawLog, B as submitAndConfirm, C as matchKeyword, D as recentMessageEdges, E as readPtysize, F as restartHintLines, H as writeKeysPaced, I as runSubcommand, L as snapshotStatus, M as resolveOne, N as resolveReadWindow, O as recentReadEdges, P as resolveResumeArgs, R as stdinActivityPath, S as listRecords, T as readNotes, U as writeToIpc, V as waitForLogQuiet, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as renderRawLogLines, k as renderLogTailLines, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as stopTipForCli } from "./subcommands-BUOLbYrj.js";
5
+ import { A as renderRawLog, B as submitAndConfirm, C as matchKeyword, D as recentMessageEdges, E as readPtysize, F as restartHintLines, H as writeKeysPaced, I as runSubcommand, L as snapshotStatus, M as resolveOne, N as resolveReadWindow, O as recentReadEdges, P as resolveResumeArgs, R as stdinActivityPath, S as listRecords, T as readNotes, U as writeToIpc, V as waitForLogQuiet, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as renderRawLogLines, k as renderLogTailLines, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as stopTipForCli } from "./subcommands-DvXyUjWT.js";
6
6
  import "./e2e-jb0Hp43q.js";
7
7
  import "./webrtcLink-B7REGtK2.js";
8
8
  import "./remotes-Cim0dBU7.js";
@@ -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-smYufLRy.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-CeLrr-kQ.js";
3
3
  import { t as agentYesHome } from "./agentYesHome-CtHb5b71.js";
4
4
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CNMl13dC.js";
5
5
  import { t as PidStore } from "./pidStore-BIvsBQ8X.js";
@@ -1901,4 +1901,4 @@ function sleep(ms) {
1901
1901
 
1902
1902
  //#endregion
1903
1903
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1904
- //# sourceMappingURL=ts-Eb2ahFa4.js.map
1904
+ //# sourceMappingURL=ts-Oyau4Eap.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.199.0";
10
+ var version = "1.200.0";
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-smYufLRy.js.map
218
+ //# sourceMappingURL=versionChecker-CeLrr-kQ.js.map
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
2
2
  import "./globalPidIndex-CoNr7tS8.js";
3
3
  import "./messageLog-CxrKJj77.js";
4
4
  import "./configShared-aKTg-sa5.js";
5
- import { S as listRecords } from "./subcommands-BUOLbYrj.js";
5
+ import { S as listRecords } from "./subcommands-DvXyUjWT.js";
6
6
  import "./e2e-jb0Hp43q.js";
7
7
  import "./webrtcLink-B7REGtK2.js";
8
8
  import "./remotes-Cim0dBU7.js";
@@ -208,12 +208,12 @@ function parseFlags(args, known) {
208
208
  positional
209
209
  };
210
210
  }
211
- async function cmdWsLs(args) {
212
- const { flags, positional } = parseFlags(args, {
213
- status: "bool",
214
- json: "bool"
215
- });
216
- if (positional.length > 0) throw new Error(`ws ls takes no positional args`);
211
+ /**
212
+ * Data API shared by `ay ws ls` and serve's `GET /api/ws`: every workspace
213
+ * under the standard layout with its live-agent count, optionally joined with
214
+ * git status (bounded concurrency — readStatus can block on git's timeout).
215
+ */
216
+ async function collectWorkspaces(opts) {
217
217
  const prov = await loadProvision();
218
218
  const wsRoot = prov.resolveWsRoot(getProvisionRoot());
219
219
  const [bare, records] = await Promise.all([walkWorkspaces(wsRoot), listRecords(void 0, {
@@ -227,7 +227,7 @@ async function cmdWsLs(args) {
227
227
  ...w,
228
228
  agents: { live: liveAgentsIn(records, w.path).length }
229
229
  }));
230
- if (flags.status) entries = await mapBounded(entries, STATUS_CONCURRENCY, async (e) => {
230
+ if (opts?.status) entries = await mapBounded(entries, STATUS_CONCURRENCY, async (e) => {
231
231
  try {
232
232
  return {
233
233
  ...e,
@@ -241,6 +241,18 @@ async function cmdWsLs(args) {
241
241
  };
242
242
  }
243
243
  });
244
+ return {
245
+ wsRoot,
246
+ workspaces: entries
247
+ };
248
+ }
249
+ async function cmdWsLs(args) {
250
+ const { flags, positional } = parseFlags(args, {
251
+ status: "bool",
252
+ json: "bool"
253
+ });
254
+ if (positional.length > 0) throw new Error(`ws ls takes no positional args`);
255
+ const { wsRoot, workspaces: entries } = await collectWorkspaces({ status: !!flags.status });
244
256
  if (flags.json) {
245
257
  process.stdout.write(JSON.stringify({
246
258
  schema: WS_JSON_SCHEMA,
@@ -277,20 +289,16 @@ function gitSummary(e) {
277
289
  if (!g.hasUpstream) parts.push("no-upstream");
278
290
  return parts.length ? parts.join(", ") : "clean";
279
291
  }
280
- async function cmdWsStatus(args) {
281
- const { flags, positional } = parseFlags(args, {
282
- json: "bool",
283
- path: "bool",
284
- spec: "bool"
285
- });
286
- if (flags.path && flags.spec) throw new Error("--path and --spec are mutually exclusive");
287
- if (positional.length > 1) throw new Error("ws status takes at most one target");
292
+ /**
293
+ * One workspace's WsEntry by checkout dir — git status + live-agent count, the
294
+ * owner/repo/branch back-derived from the layout when `spec` isn't provided.
295
+ * Shared by `ay ws status` and serve's `GET /api/ws/status`.
296
+ */
297
+ async function workspaceStatus(dir, spec) {
288
298
  const prov = await loadProvision();
289
- const wsRoot = getProvisionRoot();
290
- const { dir, spec } = await resolveOperand(prov, positional[0] ?? ".", flags.path ? "path" : flags.spec ? "spec" : "auto", wsRoot);
291
299
  const git = await prov.readStatus(dir);
292
300
  const layoutSpec = spec ?? (() => {
293
- const root = prov.resolveWsRoot(wsRoot);
301
+ const root = prov.resolveWsRoot(getProvisionRoot());
294
302
  if (!isPathInside(root, dir)) return null;
295
303
  const segs = path.relative(root, dir).split(path.sep);
296
304
  return segs.length >= 4 && segs[2] === "tree" ? {
@@ -299,7 +307,7 @@ async function cmdWsStatus(args) {
299
307
  branch: segs.slice(3).join("/")
300
308
  } : null;
301
309
  })();
302
- const entry = {
310
+ return {
303
311
  owner: layoutSpec?.owner ?? "",
304
312
  repo: layoutSpec?.repo ?? "",
305
313
  branch: layoutSpec?.branch ?? git.branch,
@@ -313,6 +321,19 @@ async function cmdWsStatus(args) {
313
321
  }), dir).length },
314
322
  git
315
323
  };
324
+ }
325
+ async function cmdWsStatus(args) {
326
+ const { flags, positional } = parseFlags(args, {
327
+ json: "bool",
328
+ path: "bool",
329
+ spec: "bool"
330
+ });
331
+ if (flags.path && flags.spec) throw new Error("--path and --spec are mutually exclusive");
332
+ if (positional.length > 1) throw new Error("ws status takes at most one target");
333
+ const prov = await loadProvision();
334
+ const wsRoot = getProvisionRoot();
335
+ const { dir, spec } = await resolveOperand(prov, positional[0] ?? ".", flags.path ? "path" : flags.spec ? "spec" : "auto", wsRoot);
336
+ const entry = await workspaceStatus(dir, spec);
316
337
  if (flags.json) {
317
338
  process.stdout.write(JSON.stringify({
318
339
  schema: WS_JSON_SCHEMA,
@@ -320,7 +341,8 @@ async function cmdWsStatus(args) {
320
341
  }, null, 2) + "\n");
321
342
  return 0;
322
343
  }
323
- process.stdout.write(`${dir}\n` + (layoutSpec ? ` spec: ${layoutSpec.owner}/${layoutSpec.repo}@${layoutSpec.branch}\n` : "") + ` branch: ${git.branch} @ ${git.head}\n state: ${gitSummary(entry)}\n agents: ${entry.agents.live} live\n`);
344
+ const git = entry.git;
345
+ process.stdout.write(`${dir}\n` + (entry.owner ? ` spec: ${entry.owner}/${entry.repo}@${entry.branch}\n` : "") + ` branch: ${git.branch} @ ${git.head}\n state: ${gitSummary(entry)}\n agents: ${entry.agents.live} live\n`);
324
346
  if (entry.agents.live > 0) process.stderr.write(`\n ay ls --cwd ${dir}\n`);
325
347
  return 0;
326
348
  }
@@ -395,5 +417,5 @@ async function cmdWs(args) {
395
417
  }
396
418
 
397
419
  //#endregion
398
- export { cmdWs };
399
- //# sourceMappingURL=ws-Bv1ZqkyE.js.map
420
+ export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, workspaceStatus };
421
+ //# sourceMappingURL=ws-DZDjlG3o.js.map
package/lab/ui/index.html CHANGED
@@ -2086,7 +2086,7 @@
2086
2086
  <div class="omnibox">
2087
2087
  <input
2088
2088
  id="omni-input"
2089
- placeholder="Search agents by title, then output…"
2089
+ placeholder="Search agents by title, then output… · &quot;/&quot; commands · /ws workspaces"
2090
2090
  spellcheck="false"
2091
2091
  autocapitalize="off"
2092
2092
  autocomplete="off"
@@ -5520,16 +5520,19 @@
5520
5520
  // the omnibox falls through to its normal search/spawn behaviour.
5521
5521
  function omniCommandRows(q) {
5522
5522
  const target = omniAnchor();
5523
- if (!target) return [];
5524
5523
  // Match on the FIRST token so a command can carry args after it
5525
5524
  // (e.g. "/fork fix the auth bug" → cmd "fork", topic "fix the auth bug").
5526
5525
  const parts = q.slice(1).trim().split(/\s+/).filter(Boolean);
5527
5526
  const cmdTerm = (parts[0] || "").toLowerCase(); // command token after "/"
5528
- const rows = OMNI_COMMANDS.filter((c) => c.cmd.slice(1).startsWith(cmdTerm)).map((c) => ({
5529
- kind: "cmd",
5530
- cmd: c,
5531
- target,
5532
- }));
5527
+ // Agent-targeted commands need an open agent to act on; /ws (below)
5528
+ // doesn't, so the target check gates only these rows, not the builder.
5529
+ const rows = !target
5530
+ ? []
5531
+ : OMNI_COMMANDS.filter((c) => c.cmd.slice(1).startsWith(cmdTerm)).map((c) => ({
5532
+ kind: "cmd",
5533
+ cmd: c,
5534
+ target,
5535
+ }));
5533
5536
  // "/fork [topic]" — a slash alias for the fork spawn row so fork is reachable
5534
5537
  // straight from the command palette (⌘K → "/fork"), not only via the row that
5535
5538
  // auto-appends to a search. It's a spawn (kind:"fork"), not a kind:"cmd" that
@@ -5549,9 +5552,124 @@
5549
5552
  src: src._src,
5550
5553
  });
5551
5554
  }
5555
+ // "/ws" discoverability — typing "/" (or "/w") lists the workspace
5556
+ // browser next to the agent commands; a full "/ws…" query is intercepted
5557
+ // in runOmni before this builder runs. Needs no anchor: workspaces exist
5558
+ // independently of any open agent.
5559
+ if ("ws".startsWith(cmdTerm)) rows.push({ kind: "wshint" });
5552
5560
  return rows;
5553
5561
  }
5554
5562
 
5563
+ // ---- "/ws" — workspace browser (⌘K → /ws) ------------------------------
5564
+ // Lists every workspace under each connected host's codehost layout
5565
+ // (<wsRoot>/<owner>/<repo>/tree/<branch>) via GET /api/ws — including ones
5566
+ // with NO live agents, which the agent list can never show. Enter spawns a
5567
+ // new agent in the highlighted workspace; a "/ws owner/repo[@branch]" query
5568
+ // that parses as a source adds a provision row (POST /api/spawn {from},
5569
+ // gated server-side by the provision hook/allowlist). Fetched at most once
5570
+ // per TTL from every writable host; hosts without the endpoint (older
5571
+ // daemon) or without codehost (501) just drop out of the merge.
5572
+ const OMNI_WS_TTL = 30_000;
5573
+ let omniWsCache = null; // { at, items: [{ ws, srcId, host }] }
5574
+ let omniWsFetchSeq = 0;
5575
+ let omniWsFetching = false;
5576
+ function omniWsFetch() {
5577
+ const seq = ++omniWsFetchSeq;
5578
+ omniWsFetching = true;
5579
+ const srcs = [...sources.values()].filter((s) => s.tx?.fetchJSON && s.live && !s.readonly);
5580
+ Promise.all(
5581
+ srcs.map(async (s) => {
5582
+ try {
5583
+ const r = await s.tx.fetchJSON("/api/ws");
5584
+ const host = s.deviceLabel || (s.id === LOCAL ? "local" : s.room || s.id);
5585
+ return (r?.workspaces || []).map((ws) => ({ ws, srcId: s.id, host }));
5586
+ } catch {
5587
+ return []; // 404 (older daemon) / 501 (no codehost) / transport error
5588
+ }
5589
+ }),
5590
+ ).then((all) => {
5591
+ if (seq !== omniWsFetchSeq) return; // superseded by a newer fetch
5592
+ omniWsFetching = false;
5593
+ omniWsCache = { at: Date.now(), items: all.flat() };
5594
+ // Repaint only if the user is still looking at a /ws view.
5595
+ if (omniOpen() && /^\/ws(\s|$)/i.test($("omni-input").value.trim())) runOmni();
5596
+ });
5597
+ }
5598
+ const wsSpecOf = (w) => `${w.owner}/${w.repo}@${w.branch}`;
5599
+ // JS twin of ts/ws.ts gitSummary — keep the vocabulary identical.
5600
+ function wsGitSummary(ws) {
5601
+ if (ws.gitError) return "error: " + ws.gitError;
5602
+ const g = ws.git;
5603
+ if (!g) return "";
5604
+ const parts = [];
5605
+ if (g.dirty) parts.push("dirty");
5606
+ if (g.ahead > 0) parts.push("ahead " + g.ahead);
5607
+ if (g.behind > 0) parts.push("behind " + g.behind);
5608
+ if (!g.hasUpstream) parts.push("no-upstream");
5609
+ return parts.length ? parts.join(", ") : "clean";
5610
+ }
5611
+ function omniWsRows(q) {
5612
+ const arg = q.replace(/^\/ws\s*/i, "").trim();
5613
+ if (!omniWsCache) {
5614
+ if (!omniWsFetching) omniWsFetch();
5615
+ return [{ kind: "wsinfo", text: "loading workspaces…" }];
5616
+ }
5617
+ // Stale cache: refresh behind the (still rendered) cached view.
5618
+ if (Date.now() - omniWsCache.at > OMNI_WS_TTL && !omniWsFetching) omniWsFetch();
5619
+ const toks = arg.toLowerCase().split(/\s+/).filter(Boolean);
5620
+ const rows = omniWsCache.items
5621
+ .filter((it) =>
5622
+ toks.every((t) =>
5623
+ (wsSpecOf(it.ws) + " " + it.ws.path + " " + it.host).toLowerCase().includes(t),
5624
+ ),
5625
+ )
5626
+ .sort(
5627
+ (a, b) =>
5628
+ (b.ws.agents?.live || 0) - (a.ws.agents?.live || 0) ||
5629
+ wsSpecOf(a.ws).localeCompare(wsSpecOf(b.ws)),
5630
+ )
5631
+ .slice(0, 30)
5632
+ .map((it) => ({ kind: "ws", ws: it.ws, srcId: it.srcId, host: it.host }));
5633
+ // A single owner/repo[@branch] token that parses as a source gets a
5634
+ // provision row even when similar workspaces exist (the user may want a
5635
+ // branch that isn't checked out yet).
5636
+ if (/^[\w.-]+\/[\w.-]+(@[\w./-]+)?$/.test(arg)) rows.push({ kind: "wsnew", spec: arg });
5637
+ if (!rows.length)
5638
+ rows.push({
5639
+ kind: "wsinfo",
5640
+ text: omniWsCache.items.length
5641
+ ? "no workspaces match — /ws owner/repo@branch provisions a new one"
5642
+ : "no workspaces on any connected host (layout <wsRoot>/<owner>/<repo>/tree/<branch>)",
5643
+ });
5644
+ return rows;
5645
+ }
5646
+ // Lazy git status for the HIGHLIGHTED workspace only — a full-status join
5647
+ // is O(workspaces × git status) and measured in seconds, so state loads
5648
+ // per-row on selection, debounced like the terminal preview. Results land
5649
+ // on the CACHED ws object, so they survive re-filtering keystrokes.
5650
+ let omniWsStatusTimer = null;
5651
+ function omniWsStatusPeek() {
5652
+ const row = omniRows[omniIdx];
5653
+ if (!row || row.kind !== "ws" || row.ws.git !== undefined || row.ws._st) return;
5654
+ if (omniWsStatusTimer) clearTimeout(omniWsStatusTimer);
5655
+ omniWsStatusTimer = setTimeout(async () => {
5656
+ if (!omniOpen()) return;
5657
+ row.ws._st = 1;
5658
+ const s = sources.get(row.srcId);
5659
+ if (!s?.tx?.fetchJSON) return;
5660
+ try {
5661
+ const r = await s.tx.fetchJSON(
5662
+ "/api/ws/status?path=" + encodeURIComponent(row.ws.path),
5663
+ );
5664
+ if (r?.workspace) Object.assign(row.ws, r.workspace);
5665
+ else row.ws.git = null;
5666
+ } catch {
5667
+ row.ws.git = null; // no status chip — the row stays useful without it
5668
+ }
5669
+ if (omniOpen() && omniRows.includes(row)) renderOmni();
5670
+ }, 150);
5671
+ }
5672
+
5555
5673
  // Which agent the highlighted candidate should preview in the background: an
5556
5674
  // agent row → that agent; the spawn row / an empty list → the launch-time
5557
5675
  // agent (so the backdrop shows the spawn target's cwd).
@@ -5638,6 +5756,39 @@
5638
5756
  <div class="omni-sub">⏎ to choose a working dir &nbsp;·&nbsp; prompt: ${esc(q || "(empty)")}</div>
5639
5757
  </div></div>`;
5640
5758
  }
5759
+ if (r.kind === "wshint") {
5760
+ return `<div class="omni-row omni-spawn${sc}" data-i="${i}">
5761
+ <span class="dot active"></span>
5762
+ <div class="omni-main">
5763
+ <div class="omni-title">▤ Workspaces <span style="opacity:.55">/ws</span></div>
5764
+ <div class="omni-sub">browse every workspace on the fleet — spawn into idle checkouts &nbsp;·&nbsp; ⏎ to open</div>
5765
+ </div></div>`;
5766
+ }
5767
+ if (r.kind === "wsinfo") {
5768
+ return `<div class="omni-row${sc}" data-i="${i}">
5769
+ <div class="omni-main"><div class="omni-sub">${esc(r.text)}</div></div></div>`;
5770
+ }
5771
+ if (r.kind === "ws") {
5772
+ const live = r.ws.agents?.live || 0;
5773
+ const st = wsGitSummary(r.ws);
5774
+ const multi = sources.size > 1;
5775
+ return `<div class="omni-row omni-spawn${sc}" data-i="${i}">
5776
+ <span class="dot ${live ? "active" : "exited"}"></span>
5777
+ <div class="omni-main">
5778
+ <div class="omni-title">▤ ${esc(wsSpecOf(r.ws))}${
5779
+ live ? ` <span style="opacity:.7">· ${live} agent${live > 1 ? "s" : ""}</span>` : ""
5780
+ }${multi ? ` <span style="opacity:.55">@ ${esc(r.host)}</span>` : ""}</div>
5781
+ <div class="omni-sub">${esc(r.ws.path)}${st ? " &nbsp;·&nbsp; <b>" + esc(st) + "</b>" : ""} &nbsp;·&nbsp; ⏎ spawn agent here</div>
5782
+ </div></div>`;
5783
+ }
5784
+ if (r.kind === "wsnew") {
5785
+ return `<div class="omni-row omni-spawn${sc}" data-i="${i}">
5786
+ <span class="dot active"></span>
5787
+ <div class="omni-main">
5788
+ <div class="omni-title">⊕ Provision workspace <b>${esc(r.spec)}</b> &amp; spawn ${esc(anchor?.cli || "claude")}</div>
5789
+ <div class="omni-sub">clone/refresh via codehost — gated by the host's provision hook/allowlist</div>
5790
+ </div></div>`;
5791
+ }
5641
5792
  if (r.kind === "cmd") {
5642
5793
  const t = r.target;
5643
5794
  const dz = r.cmd.danger ? " danger" : "";
@@ -5662,6 +5813,7 @@
5662
5813
  $("omni-results").innerHTML = html || `<div class="omni-empty">no matches</div>`;
5663
5814
  const selrow = $("omni-results").querySelector(".omni-row.sel");
5664
5815
  if (selrow) selrow.scrollIntoView({ block: "nearest" });
5816
+ omniWsStatusPeek(); // lazy git state for a highlighted /ws row (no-op otherwise)
5665
5817
  }
5666
5818
 
5667
5819
  function runOmni() {
@@ -5669,6 +5821,16 @@
5669
5821
  // A "/…" query is a slash command against the open agent, not a search —
5670
5822
  // show the matching command rows and skip the agent/spawn/tail machinery.
5671
5823
  if (q.startsWith("/")) {
5824
+ // "/ws …" opens the workspace browser — its rows come from /api/ws,
5825
+ // not the agent list, and it must never fall through to agent search
5826
+ // (the query is a workspace filter, not a spawn prompt).
5827
+ if (/^\/ws(\s|$)/i.test(q)) {
5828
+ if (omniTailTimer) clearTimeout(omniTailTimer);
5829
+ omniRows = omniWsRows(q);
5830
+ omniIdx = 0;
5831
+ renderOmni();
5832
+ return;
5833
+ }
5672
5834
  const cmds = omniCommandRows(q);
5673
5835
  if (cmds.length) {
5674
5836
  if (omniTailTimer) clearTimeout(omniTailTimer);
@@ -5777,6 +5939,25 @@
5777
5939
  await row.cmd.run(row.target);
5778
5940
  return;
5779
5941
  }
5942
+ // /ws rows — handled BEFORE the ⌘⏎ spawn-here fallthrough: in the
5943
+ // workspace browser the typed text is a filter, not a prompt, so
5944
+ // "spawn here with this prompt" is never the right reading of it.
5945
+ if (row && row.kind === "wshint") {
5946
+ $("omni-input").value = "/ws ";
5947
+ runOmni();
5948
+ return; // stays open — the browser replaces the command list
5949
+ }
5950
+ if (row && row.kind === "wsinfo") return; // informational, not activatable
5951
+ if (row && row.kind === "ws") {
5952
+ closeOmni(false);
5953
+ await spawnAndSelect({ cli: anchor?.cli || "claude", cwd: row.ws.path }, row.srcId);
5954
+ return;
5955
+ }
5956
+ if (row && row.kind === "wsnew") {
5957
+ closeOmni(false);
5958
+ await spawnAndSelect({ cli: anchor?.cli || "claude", from: row.spec }, anchor?._src);
5959
+ return;
5960
+ }
5780
5961
  // ⌘⏎ always = spawn-here (the fast default), regardless of the selected row.
5781
5962
  if (forceSpawn || (row && row.kind === "spawn")) {
5782
5963
  closeOmni(false); // committing — keep the bg as-is, spawnAndSelect takes over
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.199.0",
3
+ "version": "1.200.0",
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
@@ -1,5 +1,5 @@
1
1
  import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
2
- import { renameSync, watch, writeFileSync } from "node:fs";
2
+ import { existsSync, renameSync, watch, writeFileSync } from "node:fs";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createHash, randomBytes, timingSafeEqual } from "crypto";
@@ -2026,6 +2026,57 @@ export async function cmdServe(rest: string[]): Promise<number> {
2026
2026
  });
2027
2027
  }
2028
2028
 
2029
+ // GET /api/ws — every workspace under the codehost layout
2030
+ // (<wsRoot>/<owner>/<repo>/tree/<branch>) with live-agent counts. This is
2031
+ // what the console's ⌘K "/ws" browser renders: unlike /api/ls it also shows
2032
+ // workspaces with NO agents in them, so you can spawn into an idle checkout.
2033
+ // Read-only and cheap (a directory walk, no git); per-workspace git state is
2034
+ // fetched lazily via /api/ws/status below. 501 mirrors the fork path when
2035
+ // codehost/provision isn't installed on this host.
2036
+ if (req.method === "GET" && p === "/api/ws") {
2037
+ const ws = await import("./ws.ts");
2038
+ try {
2039
+ await ws.loadProvision();
2040
+ } catch (e) {
2041
+ return new Response((e as Error).message, { status: 501 });
2042
+ }
2043
+ try {
2044
+ const { wsRoot, workspaces } = await ws.collectWorkspaces();
2045
+ return Response.json({ schema: ws.WS_JSON_SCHEMA, wsRoot, workspaces });
2046
+ } catch (e) {
2047
+ return new Response(`workspace walk failed: ${(e as Error).message}`, { status: 500 });
2048
+ }
2049
+ }
2050
+
2051
+ // GET /api/ws/status?path=<dir> — one workspace's git state (dirty / ahead /
2052
+ // behind / no-upstream) + live-agent count. The path must sit inside the
2053
+ // workspace root: this endpoint runs `git status` on the directory, and a
2054
+ // console client with the share token is not necessarily a host admin, so it
2055
+ // must not be able to probe arbitrary host paths.
2056
+ if (req.method === "GET" && p === "/api/ws/status") {
2057
+ const dirRaw = url.searchParams.get("path");
2058
+ if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
2059
+ const ws = await import("./ws.ts");
2060
+ let prov: Awaited<ReturnType<typeof ws.loadProvision>>;
2061
+ try {
2062
+ prov = await ws.loadProvision();
2063
+ } catch (e) {
2064
+ return new Response((e as Error).message, { status: 501 });
2065
+ }
2066
+ const wsRoot = prov.resolveWsRoot(getProvisionRoot());
2067
+ const dir = path.resolve(dirRaw);
2068
+ if (!ws.isPathInside(wsRoot, dir))
2069
+ return new Response(`path is outside the workspace root ${wsRoot}`, { status: 400 });
2070
+ if (!existsSync(path.join(dir, ".git")))
2071
+ return new Response(`not a workspace checkout (no .git): ${dir}`, { status: 404 });
2072
+ try {
2073
+ const workspace = await ws.workspaceStatus(dir);
2074
+ return Response.json({ schema: ws.WS_JSON_SCHEMA, workspace });
2075
+ } catch (e) {
2076
+ return new Response(`git status failed: ${(e as Error).message}`, { status: 502 });
2077
+ }
2078
+ }
2079
+
2029
2080
  // GET /api/notes
2030
2081
  if (req.method === "GET" && p === "/api/notes") {
2031
2082
  const notes = await readNotes();
package/ts/ws.spec.ts CHANGED
@@ -2,7 +2,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { mkdtempSync, mkdirSync, realpathSync, writeFileSync, symlinkSync, rmSync } from "fs";
3
3
  import os from "os";
4
4
  import path from "path";
5
- import { cmdWs, isPathInside, resolveOperand, walkWorkspaces, WS_JSON_SCHEMA } from "./ws.ts";
5
+ import {
6
+ cmdWs,
7
+ collectWorkspaces,
8
+ isPathInside,
9
+ resolveOperand,
10
+ walkWorkspaces,
11
+ workspaceStatus,
12
+ WS_JSON_SCHEMA,
13
+ } from "./ws.ts";
6
14
 
7
15
  // Mutable fake for codehost/provision — each test overrides what it needs.
8
16
  // ws.ts imports the module lazily; vitest's registry mock intercepts that too.
@@ -433,3 +441,66 @@ describe("cmdWs (mocked provision)", () => {
433
441
  }
434
442
  });
435
443
  });
444
+
445
+ // Data API consumed by serve's GET /api/ws and /api/ws/status (the console's
446
+ // ⌘K /ws browser) — same mocked provision, no stdout involved.
447
+ describe("collectWorkspaces / workspaceStatus (mocked provision)", () => {
448
+ let root: string;
449
+ let homeBackup: string | undefined;
450
+
451
+ beforeEach(() => {
452
+ root = realpathSync(mkdtempSync(path.join(os.tmpdir(), "ay-ws-api-")));
453
+ prov.wsRoot = root;
454
+ prov.readStatus
455
+ .mockReset()
456
+ .mockResolvedValue({ branch: "main", head: "abc123", ahead: 1, behind: 0, dirty: true, hasUpstream: true });
457
+ homeBackup = process.env.AGENT_YES_HOME;
458
+ process.env.AGENT_YES_HOME = path.join(root, ".ay-home");
459
+ });
460
+ afterEach(() => {
461
+ if (homeBackup === undefined) delete process.env.AGENT_YES_HOME;
462
+ else process.env.AGENT_YES_HOME = homeBackup;
463
+ rmSync(root, { recursive: true, force: true });
464
+ });
465
+
466
+ const mkCheckout = (rel: string) => {
467
+ const dir = path.join(root, rel);
468
+ mkdirSync(path.join(dir, ".git"), { recursive: true });
469
+ return dir;
470
+ };
471
+
472
+ it("collectWorkspaces: entries without git by default, joined when status:true", async () => {
473
+ const dir = mkCheckout("o/r/tree/main");
474
+ const bare = await collectWorkspaces();
475
+ expect(bare.wsRoot).toBe(root);
476
+ expect(bare.workspaces).toEqual([
477
+ { owner: "o", repo: "r", branch: "main", path: dir, agents: { live: 0 } },
478
+ ]);
479
+ expect(prov.readStatus).not.toHaveBeenCalled();
480
+
481
+ const withStatus = await collectWorkspaces({ status: true });
482
+ expect(withStatus.workspaces[0]!.git).toMatchObject({ dirty: true, ahead: 1 });
483
+ });
484
+
485
+ it("workspaceStatus: back-derives the layout spec (slash branch) and counts agents", async () => {
486
+ const dir = mkCheckout("o/r/tree/feat/x");
487
+ const entry = await workspaceStatus(dir);
488
+ expect(entry).toMatchObject({
489
+ owner: "o",
490
+ repo: "r",
491
+ branch: "feat/x",
492
+ path: dir,
493
+ agents: { live: 0 },
494
+ });
495
+ expect(entry.git).toMatchObject({ dirty: true });
496
+ });
497
+
498
+ it("workspaceStatus: a dir outside the layout keeps git's branch, no spec", async () => {
499
+ const dir = path.join(root, "elsewhere");
500
+ mkdirSync(path.join(dir, ".git"), { recursive: true });
501
+ prov.wsRoot = path.join(root, "not-here");
502
+ const entry = await workspaceStatus(dir);
503
+ expect(entry.owner).toBe("");
504
+ expect(entry.branch).toBe("main"); // from readStatus, not the layout
505
+ });
506
+ });
package/ts/ws.ts CHANGED
@@ -69,7 +69,7 @@ interface Provision {
69
69
  }): Promise<ProvisionResult>;
70
70
  }
71
71
 
72
- async function loadProvision(): Promise<Provision> {
72
+ export async function loadProvision(): Promise<Provision> {
73
73
  try {
74
74
  return (await import("codehost/provision")) as unknown as Provision;
75
75
  } catch (e) {
@@ -298,9 +298,14 @@ function parseFlags(
298
298
  // subcommands
299
299
  // ---------------------------------------------------------------------------
300
300
 
301
- async function cmdWsLs(args: string[]): Promise<number> {
302
- const { flags, positional } = parseFlags(args, { status: "bool", json: "bool" });
303
- if (positional.length > 0) throw new Error(`ws ls takes no positional args`);
301
+ /**
302
+ * Data API shared by `ay ws ls` and serve's `GET /api/ws`: every workspace
303
+ * under the standard layout with its live-agent count, optionally joined with
304
+ * git status (bounded concurrency — readStatus can block on git's timeout).
305
+ */
306
+ export async function collectWorkspaces(opts?: {
307
+ status?: boolean;
308
+ }): Promise<{ wsRoot: string; workspaces: WsEntry[] }> {
304
309
  const prov = await loadProvision();
305
310
  const wsRoot = prov.resolveWsRoot(getProvisionRoot());
306
311
 
@@ -314,7 +319,7 @@ async function cmdWsLs(args: string[]): Promise<number> {
314
319
  agents: { live: liveAgentsIn(records, w.path).length },
315
320
  }));
316
321
 
317
- if (flags.status) {
322
+ if (opts?.status) {
318
323
  entries = await mapBounded(entries, STATUS_CONCURRENCY, async (e) => {
319
324
  try {
320
325
  return { ...e, git: await prov.readStatus(e.path) };
@@ -323,6 +328,13 @@ async function cmdWsLs(args: string[]): Promise<number> {
323
328
  }
324
329
  });
325
330
  }
331
+ return { wsRoot, workspaces: entries };
332
+ }
333
+
334
+ async function cmdWsLs(args: string[]): Promise<number> {
335
+ const { flags, positional } = parseFlags(args, { status: "bool", json: "bool" });
336
+ if (positional.length > 0) throw new Error(`ws ls takes no positional args`);
337
+ const { wsRoot, workspaces: entries } = await collectWorkspaces({ status: !!flags.status });
326
338
 
327
339
  if (flags.json) {
328
340
  process.stdout.write(
@@ -366,22 +378,20 @@ function gitSummary(e: WsEntry): string {
366
378
  return parts.length ? parts.join(", ") : "clean";
367
379
  }
368
380
 
369
- async function cmdWsStatus(args: string[]): Promise<number> {
370
- const { flags, positional } = parseFlags(args, { json: "bool", path: "bool", spec: "bool" });
371
- if (flags.path && flags.spec) throw new Error("--path and --spec are mutually exclusive");
372
- if (positional.length > 1) throw new Error("ws status takes at most one target");
381
+ /**
382
+ * One workspace's WsEntry by checkout dir git status + live-agent count, the
383
+ * owner/repo/branch back-derived from the layout when `spec` isn't provided.
384
+ * Shared by `ay ws status` and serve's `GET /api/ws/status`.
385
+ */
386
+ export async function workspaceStatus(dir: string, spec?: RepoSpec | null): Promise<WsEntry> {
373
387
  const prov = await loadProvision();
374
- const wsRoot = getProvisionRoot();
375
- const operand = positional[0] ?? ".";
376
- const mode = flags.path ? "path" : flags.spec ? "spec" : "auto";
377
- const { dir, spec } = await resolveOperand(prov, operand, mode, wsRoot);
378
388
  const git = await prov.readStatus(dir);
379
389
 
380
390
  // Fill owner/repo/branch from the layout when addressed by path, best-effort.
381
391
  const layoutSpec =
382
392
  spec ??
383
393
  (() => {
384
- const root = prov.resolveWsRoot(wsRoot);
394
+ const root = prov.resolveWsRoot(getProvisionRoot());
385
395
  if (!isPathInside(root, dir)) return null;
386
396
  const segs = path.relative(root, dir).split(path.sep);
387
397
  return segs.length >= 4 && segs[2] === "tree"
@@ -389,7 +399,7 @@ async function cmdWsStatus(args: string[]): Promise<number> {
389
399
  : null;
390
400
  })();
391
401
 
392
- const entry: WsEntry = {
402
+ return {
393
403
  owner: layoutSpec?.owner ?? "",
394
404
  repo: layoutSpec?.repo ?? "",
395
405
  branch: layoutSpec?.branch ?? git.branch,
@@ -408,14 +418,27 @@ async function cmdWsStatus(args: string[]): Promise<number> {
408
418
  },
409
419
  git,
410
420
  };
421
+ }
422
+
423
+ async function cmdWsStatus(args: string[]): Promise<number> {
424
+ const { flags, positional } = parseFlags(args, { json: "bool", path: "bool", spec: "bool" });
425
+ if (flags.path && flags.spec) throw new Error("--path and --spec are mutually exclusive");
426
+ if (positional.length > 1) throw new Error("ws status takes at most one target");
427
+ const prov = await loadProvision();
428
+ const wsRoot = getProvisionRoot();
429
+ const operand = positional[0] ?? ".";
430
+ const mode = flags.path ? "path" : flags.spec ? "spec" : "auto";
431
+ const { dir, spec } = await resolveOperand(prov, operand, mode, wsRoot);
432
+ const entry = await workspaceStatus(dir, spec);
411
433
 
412
434
  if (flags.json) {
413
435
  process.stdout.write(JSON.stringify({ schema: WS_JSON_SCHEMA, workspace: entry }, null, 2) + "\n");
414
436
  return 0;
415
437
  }
438
+ const git = entry.git!;
416
439
  process.stdout.write(
417
440
  `${dir}\n` +
418
- (layoutSpec ? ` spec: ${layoutSpec.owner}/${layoutSpec.repo}@${layoutSpec.branch}\n` : "") +
441
+ (entry.owner ? ` spec: ${entry.owner}/${entry.repo}@${entry.branch}\n` : "") +
419
442
  ` branch: ${git.branch} @ ${git.head}\n` +
420
443
  ` state: ${gitSummary(entry)}\n` +
421
444
  ` agents: ${entry.agents.live} live\n`,
@@ -1,9 +0,0 @@
1
- import "./ts-Eb2ahFa4.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-smYufLRy.js";
4
- import "./pidStore-BIvsBQ8X.js";
5
- import "./globalPidIndex-CoNr7tS8.js";
6
- import "./messageLog-CxrKJj77.js";
7
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DsL6jtBQ.js";
8
-
9
- export { SUPPORTED_CLIS };