agent-yes 1.112.1 → 1.114.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.
- package/dist/{SUPPORTED_CLIS-DxYk0BWq.js → SUPPORTED_CLIS--15XPtGd.js} +2 -2
- package/dist/SUPPORTED_CLIS-DE31jVYi.js +8 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-B6jO9ifl.js → serve-DEDkRdog.js} +128 -10
- package/dist/{subcommands-BdLzSIjI.js → subcommands-8hMGFqkQ.js} +1 -1
- package/dist/{subcommands-CCV37_dc.js → subcommands-Di_pcW0_.js} +2 -2
- package/dist/{ts-gFuntNxO.js → ts-CZQs_t8w.js} +2 -2
- package/dist/{versionChecker-DlY0TAHI.js → versionChecker-C1W2s4ZP.js} +2 -2
- package/lab/ui/console-logic.js +24 -1
- package/lab/ui/index.html +119 -20
- package/package.json +1 -1
- package/ts/serve.ts +142 -4
- package/dist/SUPPORTED_CLIS-OeK9JJli.js +0 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-CZQs_t8w.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
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS--15XPtGd.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./ts-CZQs_t8w.js";
|
|
2
|
+
import "./logger-B9h0djqx.js";
|
|
3
|
+
import "./versionChecker-C1W2s4ZP.js";
|
|
4
|
+
import "./pidStore-DBjlqzo8.js";
|
|
5
|
+
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS--15XPtGd.js";
|
|
7
|
+
|
|
8
|
+
export { SUPPORTED_CLIS };
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { n as logger } from "./logger-B9h0djqx.js";
|
|
3
|
-
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-
|
|
3
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-C1W2s4ZP.js";
|
|
4
4
|
import { argv } from "process";
|
|
5
5
|
import { execFileSync, spawn } from "child_process";
|
|
6
6
|
import ms from "ms";
|
|
@@ -482,7 +482,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
482
482
|
{
|
|
483
483
|
const rawArg = process.argv[2];
|
|
484
484
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
485
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
485
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-8hMGFqkQ.js");
|
|
486
486
|
if (isHelpFlag && process.argv.length === 3) {
|
|
487
487
|
cmdHelp();
|
|
488
488
|
process.exit(0);
|
|
@@ -515,7 +515,7 @@ if (config.useRust) {
|
|
|
515
515
|
}
|
|
516
516
|
}
|
|
517
517
|
if (rustBinary) {
|
|
518
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
518
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DE31jVYi.js");
|
|
519
519
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
520
520
|
if (config.verbose) {
|
|
521
521
|
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-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-CZQs_t8w.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-C1W2s4ZP.js";
|
|
4
4
|
import "./pidStore-DBjlqzo8.js";
|
|
5
5
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
6
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-CZQs_t8w.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-C1W2s4ZP.js";
|
|
4
4
|
import "./pidStore-DBjlqzo8.js";
|
|
5
5
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS--15XPtGd.js";
|
|
7
7
|
import "./remotes-C3xPRtfg.js";
|
|
8
|
-
import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-
|
|
8
|
+
import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-Di_pcW0_.js";
|
|
9
9
|
import yargs from "yargs";
|
|
10
10
|
import { mkdir, open, readFile, writeFile } from "fs/promises";
|
|
11
11
|
import { homedir, hostname, userInfo } from "os";
|
|
@@ -220,6 +220,58 @@ Options:
|
|
|
220
220
|
return null;
|
|
221
221
|
}
|
|
222
222
|
};
|
|
223
|
+
const GIT_TTL_MS = 5e3;
|
|
224
|
+
const gitCache = /* @__PURE__ */ new Map();
|
|
225
|
+
const gitStatus = async (cwd) => {
|
|
226
|
+
if (!cwd) return null;
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
const hit = gitCache.get(cwd);
|
|
229
|
+
if (hit && now - hit.at < GIT_TTL_MS) return hit.val;
|
|
230
|
+
let val = null;
|
|
231
|
+
try {
|
|
232
|
+
const proc = Bun.spawn([
|
|
233
|
+
"git",
|
|
234
|
+
"status",
|
|
235
|
+
"--porcelain",
|
|
236
|
+
"--branch"
|
|
237
|
+
], {
|
|
238
|
+
cwd,
|
|
239
|
+
stdout: "pipe",
|
|
240
|
+
stderr: "ignore",
|
|
241
|
+
signal: AbortSignal.timeout(2e3)
|
|
242
|
+
});
|
|
243
|
+
const out = await new Response(proc.stdout).text();
|
|
244
|
+
await proc.exited;
|
|
245
|
+
if (proc.exitCode === 0) {
|
|
246
|
+
const lines = out.split("\n");
|
|
247
|
+
const h = /^## (.+)$/.exec(lines[0] ?? "")?.[1] ?? "";
|
|
248
|
+
const unborn = /^No commits yet on (.+)$/.exec(h);
|
|
249
|
+
const branch = unborn ? unborn[1] : /^(.+?)(?:\.\.\.|\s|$)/.exec(h)?.[1] || null;
|
|
250
|
+
const ahead = Number(/\bahead (\d+)/.exec(h)?.[1] ?? 0);
|
|
251
|
+
const behind = Number(/\bbehind (\d+)/.exec(h)?.[1] ?? 0);
|
|
252
|
+
const changed = lines.slice(1).filter((l) => l.trim().length > 0).length;
|
|
253
|
+
val = {
|
|
254
|
+
branch,
|
|
255
|
+
dirty: changed > 0,
|
|
256
|
+
changed,
|
|
257
|
+
ahead,
|
|
258
|
+
behind
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
val = null;
|
|
263
|
+
}
|
|
264
|
+
gitCache.set(cwd, {
|
|
265
|
+
at: now,
|
|
266
|
+
val
|
|
267
|
+
});
|
|
268
|
+
return val;
|
|
269
|
+
};
|
|
270
|
+
const withMeta = async (r) => ({
|
|
271
|
+
...r,
|
|
272
|
+
title: await logTitle(r.log_file),
|
|
273
|
+
git: r.status === "exited" ? null : await gitStatus(r.cwd)
|
|
274
|
+
});
|
|
223
275
|
const apiFetch = async (req) => {
|
|
224
276
|
if (!checkAuth(req, token)) return new Response("Unauthorized", { status: 401 });
|
|
225
277
|
const url = new URL(req.url);
|
|
@@ -232,15 +284,81 @@ Options:
|
|
|
232
284
|
});
|
|
233
285
|
try {
|
|
234
286
|
const records = await listRecords(keyword, opts);
|
|
235
|
-
|
|
236
|
-
...r,
|
|
237
|
-
title: await logTitle(r.log_file)
|
|
238
|
-
})));
|
|
239
|
-
return Response.json(withTitles);
|
|
287
|
+
return Response.json(await Promise.all(records.map(withMeta)));
|
|
240
288
|
} catch (e) {
|
|
241
289
|
return new Response(e.message, { status: 500 });
|
|
242
290
|
}
|
|
243
291
|
}
|
|
292
|
+
if (req.method === "GET" && p === "/api/ls/subscribe") {
|
|
293
|
+
const keyword = url.searchParams.get("keyword") ?? void 0;
|
|
294
|
+
const opts = defaultOpts({
|
|
295
|
+
all: url.searchParams.get("all") === "1",
|
|
296
|
+
active: url.searchParams.get("active") === "1"
|
|
297
|
+
});
|
|
298
|
+
const enc = new TextEncoder();
|
|
299
|
+
const stream = new ReadableStream({ async start(ctrl) {
|
|
300
|
+
let closed = false;
|
|
301
|
+
const send = (obj) => {
|
|
302
|
+
try {
|
|
303
|
+
ctrl.enqueue(enc.encode(`data: ${JSON.stringify(obj)}\n\n`));
|
|
304
|
+
} catch {}
|
|
305
|
+
};
|
|
306
|
+
const sent = /* @__PURE__ */ new Map();
|
|
307
|
+
const compute = async () => {
|
|
308
|
+
const records = await listRecords(keyword, opts);
|
|
309
|
+
return Promise.all(records.map(withMeta));
|
|
310
|
+
};
|
|
311
|
+
const tick = async (first) => {
|
|
312
|
+
if (closed) return;
|
|
313
|
+
const list = await compute().catch(() => null);
|
|
314
|
+
if (!list) return;
|
|
315
|
+
const upsert = [];
|
|
316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
317
|
+
for (const r of list) {
|
|
318
|
+
seen.add(r.pid);
|
|
319
|
+
const j = JSON.stringify(r);
|
|
320
|
+
if (sent.get(r.pid) !== j) {
|
|
321
|
+
upsert.push(r);
|
|
322
|
+
sent.set(r.pid, j);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const remove = [];
|
|
326
|
+
for (const pid of sent.keys()) if (!seen.has(pid)) {
|
|
327
|
+
remove.push(pid);
|
|
328
|
+
sent.delete(pid);
|
|
329
|
+
}
|
|
330
|
+
if (first) send({
|
|
331
|
+
full: true,
|
|
332
|
+
upsert: list,
|
|
333
|
+
remove: []
|
|
334
|
+
});
|
|
335
|
+
else if (upsert.length || remove.length) send({
|
|
336
|
+
upsert,
|
|
337
|
+
remove
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
await tick(true);
|
|
341
|
+
const timer = setInterval(() => void tick(false), 1e3);
|
|
342
|
+
const heartbeat = setInterval(() => {
|
|
343
|
+
try {
|
|
344
|
+
ctrl.enqueue(enc.encode(": ping\n\n"));
|
|
345
|
+
} catch {}
|
|
346
|
+
}, 15e3);
|
|
347
|
+
req.signal.addEventListener("abort", () => {
|
|
348
|
+
closed = true;
|
|
349
|
+
clearInterval(timer);
|
|
350
|
+
clearInterval(heartbeat);
|
|
351
|
+
try {
|
|
352
|
+
ctrl.close();
|
|
353
|
+
} catch {}
|
|
354
|
+
});
|
|
355
|
+
} });
|
|
356
|
+
return new Response(stream, { headers: {
|
|
357
|
+
"Content-Type": "text/event-stream",
|
|
358
|
+
"Cache-Control": "no-cache",
|
|
359
|
+
Connection: "keep-alive"
|
|
360
|
+
} });
|
|
361
|
+
}
|
|
244
362
|
if (req.method === "GET" && p === "/api/whoami") {
|
|
245
363
|
let user = "";
|
|
246
364
|
try {
|
|
@@ -560,4 +678,4 @@ Options:
|
|
|
560
678
|
|
|
561
679
|
//#endregion
|
|
562
680
|
export { cmdServe };
|
|
563
|
-
//# sourceMappingURL=serve-
|
|
681
|
+
//# sourceMappingURL=serve-DEDkRdog.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./logger-B9h0djqx.js";
|
|
2
2
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
3
3
|
import "./remotes-C3xPRtfg.js";
|
|
4
|
-
import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-
|
|
4
|
+
import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-Di_pcW0_.js";
|
|
5
5
|
|
|
6
6
|
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -163,7 +163,7 @@ async function runSubcommand(argv) {
|
|
|
163
163
|
case "restart": return await cmdRestart(rest);
|
|
164
164
|
case "note": return await cmdNote(rest);
|
|
165
165
|
case "serve": {
|
|
166
|
-
const { cmdServe } = await import("./serve-
|
|
166
|
+
const { cmdServe } = await import("./serve-DEDkRdog.js");
|
|
167
167
|
return cmdServe(rest);
|
|
168
168
|
}
|
|
169
169
|
case "setup": {
|
|
@@ -1595,4 +1595,4 @@ async function cmdStatus(rest) {
|
|
|
1595
1595
|
|
|
1596
1596
|
//#endregion
|
|
1597
1597
|
export { finalizedLines as a, listRecords as c, renderRawLog as d, resolveOne as f, writeToIpc as g, stopTipForCli as h, cursorAbs as i, matchKeyword as l, snapshotStatus as m, cmdHelp as n, isPidAlive as o, runSubcommand as p, controlCodeFromName as r, isSubcommand as s, GRACEFUL_EXIT_COMMANDS as t, readNotes as u };
|
|
1598
|
-
//# sourceMappingURL=subcommands-
|
|
1598
|
+
//# sourceMappingURL=subcommands-Di_pcW0_.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-C1W2s4ZP.js";
|
|
3
3
|
import { n as agentYesHome, t as PidStore } from "./pidStore-DBjlqzo8.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CJxsoGdb.js";
|
|
5
5
|
import { i as readGlobalPids } from "./globalPidIndex-yVd3mbsV.js";
|
|
@@ -1714,4 +1714,4 @@ function sleep(ms) {
|
|
|
1714
1714
|
|
|
1715
1715
|
//#endregion
|
|
1716
1716
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1717
|
-
//# sourceMappingURL=ts-
|
|
1717
|
+
//# sourceMappingURL=ts-CZQs_t8w.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.
|
|
10
|
+
var version = "1.114.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -221,4 +221,4 @@ async function displayVersion() {
|
|
|
221
221
|
|
|
222
222
|
//#endregion
|
|
223
223
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
224
|
-
//# sourceMappingURL=versionChecker-
|
|
224
|
+
//# sourceMappingURL=versionChecker-C1W2s4ZP.js.map
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -115,6 +115,20 @@ export function tagsFor(e) {
|
|
|
115
115
|
return t;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// Compact git indicator from the record's `git` snapshot (server-side
|
|
119
|
+
// `git status --porcelain --branch`): "±3" changed files, "↑1" ahead, "↓2"
|
|
120
|
+
// behind. Returns "" when there's no git info or the tree is clean and in sync,
|
|
121
|
+
// so a tidy repo adds no noise. Branch itself is shown via the path identity.
|
|
122
|
+
export function gitLabel(e) {
|
|
123
|
+
const g = e.git;
|
|
124
|
+
if (!g) return "";
|
|
125
|
+
const parts = [];
|
|
126
|
+
if (g.changed > 0) parts.push("±" + g.changed);
|
|
127
|
+
if (g.ahead > 0) parts.push("↑" + g.ahead);
|
|
128
|
+
if (g.behind > 0) parts.push("↓" + g.behind);
|
|
129
|
+
return parts.join(" ");
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
// Human age of an agent ("12s" / "5m" / "3h"). `now` is injectable so tests
|
|
119
133
|
// don't depend on the wall clock; the browser calls age(e) and gets Date.now().
|
|
120
134
|
export function age(e, now = Date.now()) {
|
|
@@ -130,7 +144,16 @@ export function age(e, now = Date.now()) {
|
|
|
130
144
|
// case-insensitive substring search over title/prompt/cli/cwd/status.
|
|
131
145
|
export function matches(e, toks) {
|
|
132
146
|
const hay =
|
|
133
|
-
(e.title || "") +
|
|
147
|
+
(e.title || "") +
|
|
148
|
+
" " +
|
|
149
|
+
(e.prompt || "") +
|
|
150
|
+
" " +
|
|
151
|
+
e.cli +
|
|
152
|
+
" " +
|
|
153
|
+
(e.cwd || "") +
|
|
154
|
+
" " +
|
|
155
|
+
e.status +
|
|
156
|
+
(e.git?.dirty ? " dirty" : "");
|
|
134
157
|
return toks.every((tok) => {
|
|
135
158
|
tok = tok.toLowerCase();
|
|
136
159
|
const ci = tok.indexOf(":");
|
package/lab/ui/index.html
CHANGED
|
@@ -505,6 +505,17 @@
|
|
|
505
505
|
color: var(--muted);
|
|
506
506
|
font-size: 11.5px;
|
|
507
507
|
}
|
|
508
|
+
/* git chip: ±changed ↑ahead ↓behind, amber when the worktree is dirty */
|
|
509
|
+
.git {
|
|
510
|
+
font-family: var(--mono);
|
|
511
|
+
font-size: 10.5px;
|
|
512
|
+
color: var(--muted);
|
|
513
|
+
white-space: nowrap;
|
|
514
|
+
flex: none;
|
|
515
|
+
}
|
|
516
|
+
.git.dirty {
|
|
517
|
+
color: var(--amber);
|
|
518
|
+
}
|
|
508
519
|
.detail {
|
|
509
520
|
color: var(--muted);
|
|
510
521
|
font-size: 12.5px;
|
|
@@ -821,6 +832,7 @@
|
|
|
821
832
|
repoBranch,
|
|
822
833
|
ident,
|
|
823
834
|
tagsFor,
|
|
835
|
+
gitLabel,
|
|
824
836
|
age,
|
|
825
837
|
matches,
|
|
826
838
|
nextIndex,
|
|
@@ -1227,7 +1239,7 @@
|
|
|
1227
1239
|
const hasToken = !!localStorage.getItem("ay.localToken");
|
|
1228
1240
|
const enabled = isLocalhost || hasToken || Object.keys(loadRooms()).length === 0;
|
|
1229
1241
|
if (enabled && !sources.has(LOCAL)) {
|
|
1230
|
-
|
|
1242
|
+
const s = {
|
|
1231
1243
|
id: LOCAL,
|
|
1232
1244
|
host: "local",
|
|
1233
1245
|
kind: "local",
|
|
@@ -1237,8 +1249,11 @@
|
|
|
1237
1249
|
tried: true, // no connect phase — polled directly
|
|
1238
1250
|
devices: new Set(),
|
|
1239
1251
|
serverCount: 0,
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1252
|
+
};
|
|
1253
|
+
sources.set(LOCAL, s);
|
|
1254
|
+
subscribeSource(s);
|
|
1255
|
+
} else if (!enabled && sources.has(LOCAL)) {
|
|
1256
|
+
unsubscribeSource(sources.get(LOCAL));
|
|
1242
1257
|
sources.delete(LOCAL);
|
|
1243
1258
|
}
|
|
1244
1259
|
}
|
|
@@ -1246,33 +1261,100 @@
|
|
|
1246
1261
|
// Pull one source's agent list, tagging each row with its origin + a
|
|
1247
1262
|
// composite key (pids can collide across rooms). Updates the source's live
|
|
1248
1263
|
// flag, device set, and server count for the rooms panel.
|
|
1264
|
+
// Stamp a source's raw /api/ls records with their origin + composite key
|
|
1265
|
+
// (pids collide across rooms) and recompute the source's device set + server
|
|
1266
|
+
// count for the rooms panel. Pure transform of the source's current records.
|
|
1267
|
+
function stampAgents(s) {
|
|
1268
|
+
s.devices = new Set();
|
|
1269
|
+
const out = [...(s.byPid?.values() || [])].map((e) => {
|
|
1270
|
+
// codehost stamps _host per agent; agent-yes share rooms fall back to
|
|
1271
|
+
// the room's device label (from /api/whoami). Local stays unlabelled
|
|
1272
|
+
// (no deviceLabel) so a single-machine view keeps its clean path-only
|
|
1273
|
+
// identity instead of a blank "@:" prefix.
|
|
1274
|
+
const host = e._host || s.deviceLabel || "";
|
|
1275
|
+
if (host) s.devices.add(host);
|
|
1276
|
+
return { ...e, _room: s.id, _key: s.id + "#" + e.pid, _host: host };
|
|
1277
|
+
});
|
|
1278
|
+
s.serverCount =
|
|
1279
|
+
s.kind === "ch" ? s.client?.hosts().length || 0 : s.devices.size || (s.live ? 1 : 0);
|
|
1280
|
+
s.agents = out;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Replace a source's whole record set (full /api/ls fetch or a stream
|
|
1284
|
+
// snapshot). Resets byPid so later stream deltas diff against it cleanly.
|
|
1285
|
+
function applyFull(s, arr) {
|
|
1286
|
+
s.byPid = new Map((Array.isArray(arr) ? arr : []).map((r) => [r.pid, r]));
|
|
1287
|
+
s.live = true;
|
|
1288
|
+
stampAgents(s);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Apply one stream event: a full snapshot, or an incremental
|
|
1292
|
+
// { upsert:[changed], remove:[gone pids] } delta from /api/ls/subscribe.
|
|
1293
|
+
function applyDelta(s, ev) {
|
|
1294
|
+
if (ev.full) return applyFull(s, ev.upsert);
|
|
1295
|
+
if (!s.byPid) s.byPid = new Map();
|
|
1296
|
+
for (const r of ev.upsert || []) s.byPid.set(r.pid, r);
|
|
1297
|
+
for (const pid of ev.remove || []) s.byPid.delete(pid);
|
|
1298
|
+
s.live = true;
|
|
1299
|
+
stampAgents(s);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Fallback poll for a source that isn't streaming (older host with no
|
|
1303
|
+
// /api/ls/subscribe, or before its first delta lands).
|
|
1249
1304
|
async function listSource(s) {
|
|
1250
1305
|
try {
|
|
1251
|
-
|
|
1252
|
-
s.
|
|
1253
|
-
s.devices = new Set();
|
|
1254
|
-
const out = (Array.isArray(arr) ? arr : []).map((e) => {
|
|
1255
|
-
// codehost stamps _host per agent; agent-yes share rooms fall back to
|
|
1256
|
-
// the room's device label (from /api/whoami). Local stays unlabelled
|
|
1257
|
-
// (no deviceLabel) so a single-machine view keeps its clean path-only
|
|
1258
|
-
// identity instead of a blank "@:" prefix.
|
|
1259
|
-
const host = e._host || s.deviceLabel || "";
|
|
1260
|
-
if (host) s.devices.add(host);
|
|
1261
|
-
return { ...e, _room: s.id, _key: s.id + "#" + e.pid, _host: host };
|
|
1262
|
-
});
|
|
1263
|
-
s.serverCount =
|
|
1264
|
-
s.kind === "ch" ? s.client?.hosts().length || 0 : s.devices.size || (s.live ? 1 : 0);
|
|
1265
|
-
return out;
|
|
1306
|
+
applyFull(s, await s.tx.fetchJSON("/api/ls?all=1"));
|
|
1307
|
+
return s.agents;
|
|
1266
1308
|
} catch {
|
|
1267
1309
|
s.live = false;
|
|
1268
1310
|
s.serverCount = 0;
|
|
1311
|
+
s.byPid = new Map();
|
|
1312
|
+
s.agents = [];
|
|
1269
1313
|
return [];
|
|
1270
1314
|
}
|
|
1271
1315
|
}
|
|
1272
1316
|
|
|
1317
|
+
// Subscribe to a source's throttled delta stream instead of re-polling its
|
|
1318
|
+
// whole list. The first event flips `streaming` on, so the reconcile poll
|
|
1319
|
+
// skips this source — no duplicate full-list bytes on the wire. Older hosts
|
|
1320
|
+
// without the endpoint never send an event, so the poll keeps covering them.
|
|
1321
|
+
function subscribeSource(s) {
|
|
1322
|
+
if (!s.tx || s.unsub) return;
|
|
1323
|
+
s.unsub = s.tx.subscribe(
|
|
1324
|
+
"/api/ls/subscribe?all=1",
|
|
1325
|
+
(ev) => {
|
|
1326
|
+
if (!ev || typeof ev !== "object") return;
|
|
1327
|
+
s.streaming = true;
|
|
1328
|
+
applyDelta(s, ev);
|
|
1329
|
+
mergeRender();
|
|
1330
|
+
},
|
|
1331
|
+
null,
|
|
1332
|
+
() => {
|
|
1333
|
+
s.streaming = false;
|
|
1334
|
+
},
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
function unsubscribeSource(s) {
|
|
1338
|
+
try {
|
|
1339
|
+
s.unsub?.();
|
|
1340
|
+
} catch {}
|
|
1341
|
+
s.unsub = null;
|
|
1342
|
+
s.streaming = false;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1273
1345
|
// Compact list: one line per agent (dot + cli + title), persisted per device.
|
|
1274
1346
|
let compactList = localStorage.getItem("ay.compactList") === "1";
|
|
1275
1347
|
|
|
1348
|
+
// A small git chip (±changed ↑ahead ↓behind) for a row, amber when the tree
|
|
1349
|
+
// is dirty. Empty for clean/in-sync repos and non-git cwds — no noise.
|
|
1350
|
+
function gitChipHtml(e) {
|
|
1351
|
+
const label = gitLabel(e);
|
|
1352
|
+
if (!label) return "";
|
|
1353
|
+
const g = e.git || {};
|
|
1354
|
+
const tip = `git: ${g.changed || 0} changed · ${g.ahead || 0} ahead · ${g.behind || 0} behind`;
|
|
1355
|
+
return `<span class="git${g.dirty ? " dirty" : ""}" title="${esc(tip)}">${esc(label)}</span>`;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1276
1358
|
function renderList() {
|
|
1277
1359
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
1278
1360
|
const shown = entries.filter((e) => matches(e, toks));
|
|
@@ -1295,6 +1377,7 @@
|
|
|
1295
1377
|
${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
|
|
1296
1378
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
1297
1379
|
<span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
|
|
1380
|
+
${gitChipHtml(e)}
|
|
1298
1381
|
<span class="age">${age(e)}</span></div>`;
|
|
1299
1382
|
})
|
|
1300
1383
|
.join("") || `<div class="empty">no match</div>`;
|
|
@@ -1316,6 +1399,7 @@
|
|
|
1316
1399
|
<div class="r1"><span class="dot ${esc(e.status)}"></span>
|
|
1317
1400
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
1318
1401
|
<span class="badge">pid ${e.pid}</span>
|
|
1402
|
+
${gitChipHtml(e)}
|
|
1319
1403
|
<span class="age">${age(e)}</span></div>
|
|
1320
1404
|
${e.title ? `<div class="rowtitle" title="${esc(e.title)}">${esc(e.title)}</div>` : ""}
|
|
1321
1405
|
${e.prompt ? `<div class="detail" title="${esc(e.prompt)}">${esc(e.prompt)}</div>` : ""}
|
|
@@ -1354,10 +1438,23 @@
|
|
|
1354
1438
|
// composite key (room#pid) or a bare pid (?pid= deep links, legacy ay.sel).
|
|
1355
1439
|
const matchSel = (e, token) => e._key === token || String(e.pid) === String(token);
|
|
1356
1440
|
|
|
1441
|
+
// Reconcile poll: refresh only the sources that aren't streaming (older
|
|
1442
|
+
// hosts, or ones whose stream hasn't delivered its first snapshot yet) and
|
|
1443
|
+
// re-render. Streaming sources keep themselves current via subscribeSource,
|
|
1444
|
+
// so this stays a no-op on the wire for a modern fleet — it still re-renders
|
|
1445
|
+
// every tick, which keeps the relative "age" column fresh.
|
|
1357
1446
|
async function loadList() {
|
|
1358
1447
|
const srcs = [...sources.values()];
|
|
1359
|
-
|
|
1360
|
-
|
|
1448
|
+
await Promise.all(srcs.filter((s) => !s.streaming).map(listSource));
|
|
1449
|
+
mergeRender();
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Merge every source's stamped agents into the flat `entries` list, update
|
|
1453
|
+
// the connection badge + rooms panel, and re-render. Called after every poll
|
|
1454
|
+
// and every stream delta.
|
|
1455
|
+
function mergeRender() {
|
|
1456
|
+
const srcs = [...sources.values()];
|
|
1457
|
+
entries = srcs.flatMap((s) => s.agents || []);
|
|
1361
1458
|
// Badge: total agents + how many rooms are live. Red only when nothing
|
|
1362
1459
|
// at all is reachable (no source answered).
|
|
1363
1460
|
const roomSrcs = srcs.filter((s) => s.id !== LOCAL);
|
|
@@ -1636,6 +1733,7 @@
|
|
|
1636
1733
|
function removeSource(room) {
|
|
1637
1734
|
const s = sources.get(room);
|
|
1638
1735
|
if (!s) return;
|
|
1736
|
+
unsubscribeSource(s);
|
|
1639
1737
|
try {
|
|
1640
1738
|
s.client?.close?.();
|
|
1641
1739
|
s.client?.pc?.close?.();
|
|
@@ -1703,6 +1801,7 @@
|
|
|
1703
1801
|
}
|
|
1704
1802
|
s.tried = true;
|
|
1705
1803
|
renderRoomsIfOpen();
|
|
1804
|
+
if (s.live) subscribeSource(s);
|
|
1706
1805
|
loadList();
|
|
1707
1806
|
}
|
|
1708
1807
|
|
package/package.json
CHANGED
package/ts/serve.ts
CHANGED
|
@@ -291,6 +291,62 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
291
291
|
}
|
|
292
292
|
};
|
|
293
293
|
|
|
294
|
+
// Per-cwd git snapshot for the list: branch + dirty/changed count + ahead/behind
|
|
295
|
+
// vs upstream, all from a single `git status --porcelain --branch`. Cached per
|
|
296
|
+
// cwd with a short TTL so the 1s subscribe tick (and /api/ls polls) spawn at most
|
|
297
|
+
// one git per repo every few seconds — agents sharing a cwd share the result.
|
|
298
|
+
// Non-git dirs, errors, and timeouts cache as null.
|
|
299
|
+
interface GitInfo {
|
|
300
|
+
branch: string | null;
|
|
301
|
+
dirty: boolean;
|
|
302
|
+
changed: number;
|
|
303
|
+
ahead: number;
|
|
304
|
+
behind: number;
|
|
305
|
+
}
|
|
306
|
+
const GIT_TTL_MS = 5000;
|
|
307
|
+
const gitCache = new Map<string, { at: number; val: GitInfo | null }>();
|
|
308
|
+
const gitStatus = async (cwd: string | null | undefined): Promise<GitInfo | null> => {
|
|
309
|
+
if (!cwd) return null;
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
const hit = gitCache.get(cwd);
|
|
312
|
+
if (hit && now - hit.at < GIT_TTL_MS) return hit.val;
|
|
313
|
+
let val: GitInfo | null = null;
|
|
314
|
+
try {
|
|
315
|
+
const proc = Bun.spawn(["git", "status", "--porcelain", "--branch"], {
|
|
316
|
+
cwd,
|
|
317
|
+
stdout: "pipe",
|
|
318
|
+
stderr: "ignore",
|
|
319
|
+
signal: AbortSignal.timeout(2000),
|
|
320
|
+
});
|
|
321
|
+
const out = await new Response(proc.stdout).text();
|
|
322
|
+
await proc.exited;
|
|
323
|
+
if (proc.exitCode === 0) {
|
|
324
|
+
const lines = out.split("\n");
|
|
325
|
+
// Branch header, e.g. "## main...origin/main [ahead 1, behind 2]",
|
|
326
|
+
// "## main" (no upstream), "## HEAD (no branch)", or "## No commits yet on x".
|
|
327
|
+
const h = /^## (.+)$/.exec(lines[0] ?? "")?.[1] ?? "";
|
|
328
|
+
const unborn = /^No commits yet on (.+)$/.exec(h);
|
|
329
|
+
const branch = unborn ? unborn[1]! : /^(.+?)(?:\.\.\.|\s|$)/.exec(h)?.[1] || null;
|
|
330
|
+
const ahead = Number(/\bahead (\d+)/.exec(h)?.[1] ?? 0);
|
|
331
|
+
const behind = Number(/\bbehind (\d+)/.exec(h)?.[1] ?? 0);
|
|
332
|
+
const changed = lines.slice(1).filter((l) => l.trim().length > 0).length;
|
|
333
|
+
val = { branch, dirty: changed > 0, changed, ahead, behind };
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
val = null; // git missing, not a repo, or timed out
|
|
337
|
+
}
|
|
338
|
+
gitCache.set(cwd, { at: now, val });
|
|
339
|
+
return val;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// One agent record decorated for the console: the latest OSC title + a git
|
|
343
|
+
// snapshot (skipped for exited agents — their repo state is no longer live).
|
|
344
|
+
const withMeta = async (r: Awaited<ReturnType<typeof listRecords>>[number]) => ({
|
|
345
|
+
...r,
|
|
346
|
+
title: await logTitle(r.log_file),
|
|
347
|
+
git: r.status === "exited" ? null : await gitStatus(r.cwd),
|
|
348
|
+
});
|
|
349
|
+
|
|
294
350
|
// The whole API as a plain handler: served over HTTP by Bun.serve (--http)
|
|
295
351
|
// and called in-process by the WebRTC bridge (--webrtc) — the latter needs
|
|
296
352
|
// no TCP port at all.
|
|
@@ -311,15 +367,97 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
311
367
|
});
|
|
312
368
|
try {
|
|
313
369
|
const records = await listRecords(keyword, opts);
|
|
314
|
-
|
|
315
|
-
records.map(async (r) => ({ ...r, title: await logTitle(r.log_file) })),
|
|
316
|
-
);
|
|
317
|
-
return Response.json(withTitles);
|
|
370
|
+
return Response.json(await Promise.all(records.map(withMeta)));
|
|
318
371
|
} catch (e) {
|
|
319
372
|
return new Response((e as Error).message, { status: 500 });
|
|
320
373
|
}
|
|
321
374
|
}
|
|
322
375
|
|
|
376
|
+
// GET /api/ls/subscribe — SSE: throttled live deltas of the agent list.
|
|
377
|
+
// The console used to re-poll /api/ls every 3s; this streams the SAME records
|
|
378
|
+
// (incl. each agent's OSC title) but only what CHANGED since the last tick, so
|
|
379
|
+
// an idle fleet costs ~nothing on the wire. The first event is a full snapshot
|
|
380
|
+
// ({ full:true, upsert:[all] }); each later event carries { upsert:[changed
|
|
381
|
+
// records], remove:[gone pids] }. listRecords is a couple of JSONL reads and
|
|
382
|
+
// logTitle is cached by (size,mtime), so the 1s tick stays cheap.
|
|
383
|
+
if (req.method === "GET" && p === "/api/ls/subscribe") {
|
|
384
|
+
const keyword = url.searchParams.get("keyword") ?? undefined;
|
|
385
|
+
const opts = defaultOpts({
|
|
386
|
+
all: url.searchParams.get("all") === "1",
|
|
387
|
+
active: url.searchParams.get("active") === "1",
|
|
388
|
+
});
|
|
389
|
+
const enc = new TextEncoder();
|
|
390
|
+
const stream = new ReadableStream({
|
|
391
|
+
async start(ctrl) {
|
|
392
|
+
let closed = false;
|
|
393
|
+
const send = (obj: unknown) => {
|
|
394
|
+
try {
|
|
395
|
+
ctrl.enqueue(enc.encode(`data: ${JSON.stringify(obj)}\n\n`));
|
|
396
|
+
} catch {
|
|
397
|
+
/* stream already closed */
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
// pid -> JSON of the last record we sent, for cheap change detection.
|
|
401
|
+
const sent = new Map<number, string>();
|
|
402
|
+
const compute = async () => {
|
|
403
|
+
const records = await listRecords(keyword, opts);
|
|
404
|
+
return Promise.all(records.map(withMeta));
|
|
405
|
+
};
|
|
406
|
+
const tick = async (first: boolean) => {
|
|
407
|
+
if (closed) return;
|
|
408
|
+
// Transient read error → skip this tick, retry on the next.
|
|
409
|
+
const list = await compute().catch(() => null);
|
|
410
|
+
if (!list) return;
|
|
411
|
+
const upsert: typeof list = [];
|
|
412
|
+
const seen = new Set<number>();
|
|
413
|
+
for (const r of list) {
|
|
414
|
+
seen.add(r.pid);
|
|
415
|
+
const j = JSON.stringify(r);
|
|
416
|
+
if (sent.get(r.pid) !== j) {
|
|
417
|
+
upsert.push(r);
|
|
418
|
+
sent.set(r.pid, j);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const remove: number[] = [];
|
|
422
|
+
for (const pid of sent.keys())
|
|
423
|
+
if (!seen.has(pid)) {
|
|
424
|
+
remove.push(pid);
|
|
425
|
+
sent.delete(pid);
|
|
426
|
+
}
|
|
427
|
+
if (first) send({ full: true, upsert: list, remove: [] });
|
|
428
|
+
else if (upsert.length || remove.length) send({ upsert, remove });
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
await tick(true);
|
|
432
|
+
const timer = setInterval(() => void tick(false), 1000);
|
|
433
|
+
const heartbeat = setInterval(() => {
|
|
434
|
+
try {
|
|
435
|
+
ctrl.enqueue(enc.encode(": ping\n\n"));
|
|
436
|
+
} catch {
|
|
437
|
+
/* closed */
|
|
438
|
+
}
|
|
439
|
+
}, 15_000);
|
|
440
|
+
req.signal.addEventListener("abort", () => {
|
|
441
|
+
closed = true;
|
|
442
|
+
clearInterval(timer);
|
|
443
|
+
clearInterval(heartbeat);
|
|
444
|
+
try {
|
|
445
|
+
ctrl.close();
|
|
446
|
+
} catch {
|
|
447
|
+
/* already closed */
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
return new Response(stream, {
|
|
453
|
+
headers: {
|
|
454
|
+
"Content-Type": "text/event-stream",
|
|
455
|
+
"Cache-Control": "no-cache",
|
|
456
|
+
Connection: "keep-alive",
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
323
461
|
// GET /api/whoami — this host's device label (user@host), so a remote
|
|
324
462
|
// console can tag each agent with the machine it came from. Unlike codehost,
|
|
325
463
|
// `ay serve --share` carries no per-agent device id; the viewer fetches this
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-gFuntNxO.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-DlY0TAHI.js";
|
|
4
|
-
import "./pidStore-DBjlqzo8.js";
|
|
5
|
-
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DxYk0BWq.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|