agent-yes 1.287.0 → 1.288.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/{SUPPORTED_CLIS-DKdFELZL.js → SUPPORTED_CLIS-BHowMgMY.js} +2 -2
  2. package/dist/{SUPPORTED_CLIS-5YPYQ8Iw.js → SUPPORTED_CLIS-ChhJyq4h.js} +3 -3
  3. package/dist/{agentShare-B525ly6v.js → agentShare-T3RskvDT.js} +2 -2
  4. package/dist/{callback-C-awM4qn.js → callback-CuohbCJA.js} +3 -3
  5. package/dist/{callback-BLTW4Mgp.js → callback-DeaETuHt.js} +2 -2
  6. package/dist/cli.js +5 -5
  7. package/dist/{cmdPs-BRtmnpsM.js → cmdPs-Bm4JG7hi.js} +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/{notifyDaemon-DshQvO-4.js → notifyDaemon-BORqPeAl.js} +2 -2
  10. package/dist/{parentPingLoop-Bjw6hKoZ.js → parentPingLoop-COePRYr9.js} +2 -2
  11. package/dist/{rustBinary-C57KWEtX.js → rustBinary-BmXzqj5z.js} +2 -2
  12. package/dist/{rustBinary-BXhp8yBp.js → rustBinary-D12k7iOD.js} +2 -2
  13. package/dist/{schedule-Boa0nE0V.js → schedule-DLoUmyJh.js} +4 -4
  14. package/dist/{serve-7mcHebep.js → serve-DaDlOtss.js} +17 -17
  15. package/dist/{setup-DXRL-Mdo.js → setup-Cqy8nRrq.js} +2 -2
  16. package/dist/{subcommands-BftthBK7.js → subcommands-BfiOyfz7.js} +15 -15
  17. package/dist/{subcommands-BQJ-Uqtl.js → subcommands-DKTa3E6O.js} +1 -1
  18. package/dist/{terminal-DrYf3HP0.js → terminal-C0tnGnrZ.js} +2 -2
  19. package/dist/trayApp-BrAMlCY7.js +5 -0
  20. package/dist/{trayApp-DG2fQnhx.js → trayApp-DvL9qW6V.js} +2 -2
  21. package/dist/{ts-LDnoqCV2.js → ts-DJLp2_bv.js} +3 -3
  22. package/dist/{versionChecker-CGmyt2qE.js → versionChecker-CkO7wYyV.js} +2 -2
  23. package/dist/{widget-B2SluyG1.js → widget-CJqpScCG.js} +3 -3
  24. package/dist/{ws-DWA90-uE.js → ws-DGENGNrr.js} +2 -2
  25. package/dist/{ws-CTOnVXg8.js → ws-DzCKvzWD.js} +2 -2
  26. package/lab/ui/index.html +95 -67
  27. package/package.json +2 -1
  28. package/ts/JsonlStore.bun.spec.ts +204 -0
  29. package/ts/ReadyManager.bun.spec.ts +72 -0
  30. package/ts/agentNice.bun.spec.ts +24 -0
  31. package/ts/agentPermissions.bun.spec.ts +117 -0
  32. package/ts/agentShare.bun.spec.ts +240 -0
  33. package/ts/agentTree.bun.spec.ts +92 -0
  34. package/ts/agentYesHome.bun.spec.ts +25 -0
  35. package/ts/askApi.bun.spec.ts +358 -0
  36. package/ts/askCli.bun.spec.ts +304 -0
  37. package/ts/autoRetry.bun.spec.ts +123 -0
  38. package/ts/badges.bun.spec.ts +187 -0
  39. package/ts/buildRustArgs.bun.spec.ts +351 -0
  40. package/ts/callbackCore.bun.spec.ts +139 -0
  41. package/ts/channels/hlc.bun.spec.ts +56 -0
  42. package/ts/channels/link.bun.spec.ts +72 -0
  43. package/ts/channels/op.bun.spec.ts +78 -0
  44. package/ts/channels/store.browser.bun.spec.ts +60 -0
  45. package/ts/channels/store.bun.spec.ts +145 -0
  46. package/ts/channels/store.node.bun.spec.ts +76 -0
  47. package/ts/channels/trust.bun.spec.ts +86 -0
  48. package/ts/channels.bun.spec.ts +212 -0
  49. package/ts/cli-idle.bun.spec.ts +20 -0
  50. package/ts/cmdDsh.bun.spec.ts +90 -0
  51. package/ts/configLoader.bun.spec.ts +273 -0
  52. package/ts/cwdConflictWarn.bun.spec.ts +60 -0
  53. package/ts/cwdPassthroughHint.bun.spec.ts +74 -0
  54. package/ts/e2e-crypto.bun.spec.ts +235 -0
  55. package/ts/forkNested.bun.spec.ts +240 -0
  56. package/ts/histStore.bun.spec.ts +355 -0
  57. package/ts/identity.bun.spec.ts +190 -0
  58. package/ts/idleWaiter.bun.spec.ts +91 -0
  59. package/ts/initMsg.bun.spec.ts +132 -0
  60. package/ts/invokedCli.bun.spec.ts +30 -0
  61. package/ts/ipcLock.bun.spec.ts +107 -0
  62. package/ts/logger.bun.spec.ts +27 -0
  63. package/ts/lsWatch.bun.spec.ts +61 -0
  64. package/ts/messageLog.bun.spec.ts +145 -0
  65. package/ts/needsInput.bun.spec.ts +157 -0
  66. package/ts/nodeRuntime.bun.spec.ts +99 -0
  67. package/ts/notifyDaemon.bun.spec.ts +297 -0
  68. package/ts/notifyInbox.bun.spec.ts +277 -0
  69. package/ts/notifyRouter.bun.spec.ts +351 -0
  70. package/ts/notifyStore.bun.spec.ts +403 -0
  71. package/ts/ownerHeartbeat.bun.spec.ts +175 -0
  72. package/ts/parentLink.bun.spec.ts +64 -0
  73. package/ts/parentPing.bun.spec.ts +176 -0
  74. package/ts/parentPingLoop.bun.spec.ts +79 -0
  75. package/ts/parentPingSend.bun.spec.ts +195 -0
  76. package/ts/parentWatching.bun.spec.ts +61 -0
  77. package/ts/pidStore.bun.spec.ts +372 -0
  78. package/ts/procStartTime.bun.spec.ts +117 -0
  79. package/ts/reaper.bun.spec.ts +86 -0
  80. package/ts/removeControlCharacters.bun.spec.ts +94 -0
  81. package/ts/resolveBinary.bun.spec.ts +133 -0
  82. package/ts/resultEnvelope.bun.spec.ts +46 -0
  83. package/ts/runningLock.bun.spec.ts +485 -0
  84. package/ts/rustBinary.bun.spec.ts +81 -0
  85. package/ts/schedule.bun.spec.ts +30 -0
  86. package/ts/serve.bun.spec.ts +166 -0
  87. package/ts/serveLock.bun.spec.ts +220 -0
  88. package/ts/session-integration.bun.spec.ts +93 -0
  89. package/ts/setup.bun.spec.ts +42 -0
  90. package/ts/share.bun.spec.ts +36 -0
  91. package/ts/sizeNego.bun.spec.ts +56 -0
  92. package/ts/statusText.bun.spec.ts +17 -0
  93. package/ts/strayServe.bun.spec.ts +50 -0
  94. package/ts/subcommandMirror.bun.spec.ts +76 -0
  95. package/ts/systemPathLink.bun.spec.ts +79 -0
  96. package/ts/termToken.bun.spec.ts +74 -0
  97. package/ts/terminal.bun.spec.ts +50 -0
  98. package/ts/terminalReply.bun.spec.ts +36 -0
  99. package/ts/titleScanner.bun.spec.ts +65 -0
  100. package/ts/todoAutomation.bun.spec.ts +253 -0
  101. package/ts/todoBlock.bun.spec.ts +73 -0
  102. package/ts/todoDigest.bun.spec.ts +184 -0
  103. package/ts/todoIdentity.bun.spec.ts +85 -0
  104. package/ts/todoLifecycle.bun.spec.ts +79 -0
  105. package/ts/todoParse.bun.spec.ts +98 -0
  106. package/ts/todoRoot.bun.spec.ts +117 -0
  107. package/ts/todoStore.bun.spec.ts +727 -0
  108. package/ts/tryCatch.bun.spec.ts +275 -0
  109. package/ts/webrtcLink.bun.spec.ts +45 -0
  110. package/ts/workspaceConfig.bun.spec.ts +319 -0
  111. package/dist/trayApp-1AQ6X2V9.js +0 -5
package/lab/ui/index.html CHANGED
@@ -3622,12 +3622,6 @@
3622
3622
  // Always-on header-badge traffic ring (independent of the opt-in HUD below).
3623
3623
  if (kind === "in") traf.cin += n || 0;
3624
3624
  else if (kind === "out") traf.cout += n || 0;
3625
- // Also accumulate into the selected agent's per-agent ring (left-panel heatmap).
3626
- if (sel) {
3627
- const r = ioRing(sel);
3628
- if (kind === "in") r.i += n || 0;
3629
- else if (kind === "out") r.o += n || 0;
3630
- }
3631
3625
  if (!perfOn) return;
3632
3626
  if (kind === "in") {
3633
3627
  perf.inEvents++;
@@ -3725,28 +3719,9 @@
3725
3719
  traf.cin = traf.cout = 0;
3726
3720
  traf.hist = [];
3727
3721
  }
3728
- // Per-agent I/O ring for the LEFT-PANEL heatmap. Only the SELECTED agent has
3729
- // live I/O (the fwd/term.write hooks fire for it alone), so this map holds a
3730
- // rolling window of the traffic each agent saw while it was selected. Other
3731
- // rows keep their last-known window (empty → transparent). Mirrors traf's
3732
- // shape but is keyed by composite _key, sampled on the same 1s tick below.
3733
- const IO_N = 60;
3734
- const ioByKey = new Map();
3735
- function ioRing(key) {
3736
- let r = ioByKey.get(key);
3737
- if (!r) {
3738
- r = { i: 0, o: 0, hist: [] };
3739
- ioByKey.set(key, r);
3740
- }
3741
- return r;
3742
- }
3743
- function ioReset(key) {
3744
- const r = ioByKey.get(key);
3745
- if (r) {
3746
- r.i = r.o = 0;
3747
- r.hist = [];
3748
- }
3749
- }
3722
+ // Cached /api/host response (system load + managed/unattributed split) for
3723
+ // the room header line. Refreshed every list tick.
3724
+ let hostRes = null;
3750
3725
  // The last winsize the agent's PTY reported (from /api/size), so the tooltip
3751
3726
  // can show OUR viewport vs the agent's render size. Reset on agent switch.
3752
3727
  let agentSize = null;
@@ -3881,15 +3856,9 @@
3881
3856
  traf.hist.push({ i: traf.cin, o: traf.cout });
3882
3857
  if (traf.hist.length > TRAF_N) traf.hist.shift();
3883
3858
  traf.cin = traf.cout = 0;
3884
- // Per-agent ring: sample the selected agent's current second, then repaint
3885
- // the list so its row heatmap advances (and paint the header badge).
3886
- const r = sel ? ioRing(sel) : null;
3887
- if (r) {
3888
- r.hist.push({ i: r.i, o: r.o });
3889
- if (r.hist.length > IO_N) r.hist.shift();
3890
- r.i = r.o = 0;
3891
- renderList();
3892
- }
3859
+ // The per-agent heatmap now comes from the server sampler (/api/ls res
3860
+ // field), refreshed on the 3s list poll — no front-end per-second
3861
+ // sampling needed. Repaint the header badge (traffic tooltip) only.
3893
3862
  if (sel) paintHeaderBadge();
3894
3863
  }, 1000);
3895
3864
 
@@ -5934,6 +5903,28 @@ my.translate = async (text, lang) => {
5934
5903
  // as agent rows so the hierarchy reads as one tree. The room/peer layers
5935
5904
  // only appear here when there are ≥2 of them (single ones fold away in
5936
5905
  // layeredRows), so a header always disambiguates something real.
5906
+ // Resource summary for the LOCAL room header line, sourced from /api/host:
5907
+ // XX%/YY% XXG/YYG of ZZG
5908
+ // where XX = agent-yes managed, YY = unattributed (rest of the box). Only
5909
+ // renders when hostRes has been fetched and the sampler has data.
5910
+ function resLineHtml() {
5911
+ if (!hostRes || !hostRes.resources) return "";
5912
+ const r = hostRes.resources;
5913
+ const memTotal = hostRes.mem && hostRes.mem.total ? hostRes.mem.total : null;
5914
+ const managed = hsize(r.managed_rss);
5915
+ const unattributed = hsize(r.unattributed_rss);
5916
+ const total = memTotal ? hsize(memTotal) : null;
5917
+ if (!total) return "";
5918
+ return ` — <span title="agent-yes managed / unattributed">${managed}/${unattributed}</span> of ${total}`;
5919
+ }
5920
+ function hsize(bytes) {
5921
+ if (bytes === null || !Number.isFinite(bytes)) return "-";
5922
+ if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(1) + "G";
5923
+ if (bytes >= 1048576) return Math.round(bytes / 1048576) + "M";
5924
+ if (bytes >= 1024) return Math.round(bytes / 1024) + "K";
5925
+ return bytes + "B";
5926
+ }
5927
+
5937
5928
  function headerHtml(r) {
5938
5929
  const icon = r.kind === "room" ? "▤" : "▣";
5939
5930
  // Peer headers carry their host's connection type (lan/wan/relay), so a
@@ -5942,7 +5933,8 @@ my.translate = async (text, lang) => {
5942
5933
  r.kind === "peer"
5943
5934
  ? connTagHtml(hostKinds.get((r.room || "") + " " + (r.host || "")))
5944
5935
  : "";
5945
- return `<div class="ghead g-${r.kind}">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="gicon">${icon}</span><span class="glabel">${esc(r.label || "")}</span>${ctag}<span class="gkind">${r.kind}</span></div>`;
5936
+ const resSuffix = r.kind === "room" ? resLineHtml() : "";
5937
+ return `<div class="ghead g-${r.kind}">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="gicon">${icon}</span><span class="glabel">${esc(r.label || "")}${resSuffix}</span>${ctag}<span class="gkind">${r.kind}</span></div>`;
5946
5938
  }
5947
5939
 
5948
5940
  // Extra row classes for collaborative presence: a yellow bar when a human
@@ -5982,29 +5974,42 @@ my.translate = async (text, lang) => {
5982
5974
  : "";
5983
5975
  }
5984
5976
 
5985
- // Build the low-contrast oklch heatmap backdrop for one agent row. The overlay
5986
- // sits BEHIND the text (absolute, low opacity), a horizontal gradient from
5987
- // transparent (old, left) → colour (recent, right). OUT (green) and IN (accent)
5988
- // are layered as two gradients so both directions read, but at low alpha so
5989
- // the agent name / status stay crisp on top.
5990
- function rowIoHeat(key) {
5991
- const r = ioByKey.get(key);
5992
- if (!r || !r.hist.length) return "";
5993
- const max = Math.max(1, ...r.hist.map((h) => Math.max(h.i, h.o)));
5994
- // A right-side "brightness ramp" is a hard-stop gradient; we approximate the
5995
- // per-second intensity by summing the last few buckets (rightmost = hottest).
5996
- const tail = Math.min(8, r.hist.length);
5997
- let o = 0, i = 0;
5998
- for (let k = r.hist.length - tail; k < r.hist.length; k++) { o += r.hist[k].o; i += r.hist[k].i; }
5999
- const aOut = Math.min(1, Math.log1p(o) / Math.log1p(max * tail));
6000
- const aIn = Math.min(1, Math.log1p(i) / Math.log1p(max * tail));
6001
- // Low-contrast: cap opacity well below what would muddle the label.
6002
- const oA = (aOut * 0.28).toFixed(2);
6003
- const iA = (aIn * 0.28).toFixed(2);
6004
- // oklch, two stacked linear-gradients, right-bright → left-fade.
6005
- return `<div class="ioheat" style="background:${JSON.stringify(
6006
- `linear-gradient(90deg, oklch(65% 0.14 152 / 0) 0%, oklch(65% 0.14 152 / ${oA}) 100%), linear-gradient(90deg, oklch(62% 0.16 255 / 0) 0%, oklch(62% 0.16 255 / ${iA}) 100%)`
6007
- )}"></div>`;
5977
+ // Build the low-contrast oklch heatmap backdrop for one agent row, from the
5978
+ // SERVER-side per-agent resource window (`entry.res`, fed by ayrs' sampler —
5979
+ // CPU-seconds, RSS and process count per bucket, one point per minute-ish).
5980
+ // Every row gets its own window (no selection required), painted BEHIND the
5981
+ // text (absolute, low opacity). Two stacked rows: CPU (top, warm) and RSS
5982
+ // (bottom, cool), each a per-bucket grid so a busy/fat burst reads as a solid
5983
+ // block and idle as near-transparent. Right edge is the newest bucket.
5984
+ function rowIoHeat(e) {
5985
+ const res = e && e.res;
5986
+ if (!res || !Array.isArray(res.cpu_seconds) || !res.cpu_seconds.length) return "";
5987
+ const n = res.cpu_seconds.length;
5988
+ const bucketSecs = res.bucket_secs || 60;
5989
+ // CPU: normalize cpu_seconds (summed across the tree) to "fraction of one
5990
+ // core" by dividing by the bucket width. Log-scaled so a 1% vs 100% spread
5991
+ // reads better than a linear 0..1.
5992
+ const cpuMax = Math.max(1e-9, ...res.cpu_seconds.map((s) => Math.abs(s) / bucketSecs));
5993
+ const rssMax = Math.max(1, ...(res.rss || []).map((b) => b || 0));
5994
+ const log1p = (v) => (v <= 0 ? 0 : Math.min(1, Math.log1p(v) / Math.log1p(cpuMax)));
5995
+ const logRss = (v) =>
5996
+ v <= 0 ? 0 : Math.min(1, Math.log1p(v / 1e6) / Math.log1p(rssMax / 1e6));
5997
+ const w = 96,
5998
+ h = 22,
5999
+ colW = w / n,
6000
+ rowH = (h - 2) / 2;
6001
+ const cells = [];
6002
+ for (let k = 0; k < n; k++) {
6003
+ const x = k * colW;
6004
+ const aCpu = log1p((res.cpu_seconds[k] || 0) / bucketSecs);
6005
+ const aRss = logRss(res.rss ? res.rss[k] || 0 : 0);
6006
+ // Low-contrast caps so the agent name / status stay crisp on top.
6007
+ cells.push(
6008
+ `<rect x="${x.toFixed(2)}" y="0" width="${(colW - 0.5).toFixed(2)}" height="${rowH.toFixed(2)}" fill="oklch(62% 0.15 35)" opacity="${(aCpu * 0.3).toFixed(2)}"/>`,
6009
+ `<rect x="${x.toFixed(2)}" y="${(rowH + 2).toFixed(2)}" width="${(colW - 0.5).toFixed(2)}" height="${rowH.toFixed(2)}" fill="oklch(65% 0.14 262)" opacity="${(aRss * 0.3).toFixed(2)}"/>`,
6010
+ );
6011
+ }
6012
+ return `<div class="ioheat"><svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" shape-rendering="crispEdges" style="position:absolute;inset:0;width:100%;height:100%">${cells.join("")}</svg></div>`;
6008
6013
  }
6009
6014
  function renderList() {
6010
6015
  // The graph view (/r/) only exists on the hosted origin — a local
@@ -6049,7 +6054,7 @@ my.translate = async (text, lang) => {
6049
6054
  const t = e.title || e.status_text || e.prompt || "";
6050
6055
  const id = compactIdent(e, ctx, 3, r.parentEntry);
6051
6056
  const cli = cliLabel(e);
6052
- return `<div class="row crow ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">${rowIoHeat(e._key)}
6057
+ return `<div class="row crow ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">${rowIoHeat(e)}
6053
6058
  ${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}
6054
6059
  ${pinMarkHtml(e)}
6055
6060
  <span class="dot ${esc(e.status)}"></span>
@@ -6081,7 +6086,7 @@ my.translate = async (text, lang) => {
6081
6086
  `<span class="rtag" data-k="${k}"><span style="opacity:.55">${k}:</span>${esc(v)}</span>`,
6082
6087
  )
6083
6088
  .join("");
6084
- return `<div class="row ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">${rowIoHeat(e._key)}
6089
+ return `<div class="row ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">${rowIoHeat(e)}
6085
6090
  <div class="r1">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}${pinMarkHtml(e)}<span class="dot ${esc(e.status)}"></span>
6086
6091
  <span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
6087
6092
  <span class="badge">pid ${e.pid}</span>
@@ -6157,6 +6162,14 @@ my.translate = async (text, lang) => {
6157
6162
  // so this stays a no-op on the wire for a modern fleet — it still re-renders
6158
6163
  // every tick, which keeps the relative "age" column fresh.
6159
6164
  async function loadList() {
6165
+ // Refresh the local-host system snapshot for the room header line.
6166
+ localTx
6167
+ .fetchJSON("/api/host")
6168
+ .then((h) => {
6169
+ hostRes = h;
6170
+ })
6171
+ .catch(() => {});
6172
+
6160
6173
  const srcs = [...sources.values()];
6161
6174
  await Promise.all(srcs.filter((s) => !s.streaming).map(listSource));
6162
6175
  mergeRender();
@@ -9430,9 +9443,18 @@ my.translate = async (text, lang) => {
9430
9443
  // The handle itself lives inside the tip; hit-test against it directly.
9431
9444
  if (!tip.contains(e.target)) return;
9432
9445
  if (e.clientY - tr.top > HANDLE) return;
9433
- drag = { id: e.pointerId, x: e.clientX, y: e.clientY, ox: tr.left, oy: tr.top, moved: false };
9446
+ drag = {
9447
+ id: e.pointerId,
9448
+ x: e.clientX,
9449
+ y: e.clientY,
9450
+ ox: tr.left,
9451
+ oy: tr.top,
9452
+ moved: false,
9453
+ };
9434
9454
  suppressClick = false;
9435
- try { tip.setPointerCapture(e.pointerId); } catch {}
9455
+ try {
9456
+ tip.setPointerCapture(e.pointerId);
9457
+ } catch {}
9436
9458
  e.preventDefault();
9437
9459
  });
9438
9460
  pill.addEventListener("pointermove", (e) => {
@@ -9456,7 +9478,11 @@ my.translate = async (text, lang) => {
9456
9478
  const tip = tipEl();
9457
9479
  drag = null;
9458
9480
  if (tip) tip.classList.remove("dragging");
9459
- if (tip) { try { tip.releasePointerCapture(e.pointerId); } catch {} }
9481
+ if (tip) {
9482
+ try {
9483
+ tip.releasePointerCapture(e.pointerId);
9484
+ } catch {}
9485
+ }
9460
9486
  if (!moved) return;
9461
9487
  if (tip) {
9462
9488
  pos = {
@@ -9464,7 +9490,9 @@ my.translate = async (text, lang) => {
9464
9490
  y: parseFloat(tip.style.top) || 8,
9465
9491
  };
9466
9492
  }
9467
- try { localStorage.setItem(KEY, JSON.stringify(pos)); } catch {}
9493
+ try {
9494
+ localStorage.setItem(KEY, JSON.stringify(pos));
9495
+ } catch {}
9468
9496
  suppressClick = true;
9469
9497
  setTimeout(() => (suppressClick = false), 0);
9470
9498
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.287.0",
3
+ "version": "1.288.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",
@@ -127,6 +127,7 @@
127
127
  "release": "standard-version && npm publish",
128
128
  "release:beta": "standard-version && npm publish --tag beta",
129
129
  "test": "vitest run",
130
+ "test:bun": "bun test --timeout 30000 .bun.",
130
131
  "test:coverage": "vitest run --coverage",
131
132
  "test:ui": "vitest run --config tests/ui-test/vitest.config.ts",
132
133
  "test:ui-dom": "vitest run --config tests/ui-dom/vitest.config.ts",
@@ -0,0 +1,204 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test";
2
+ import { JsonlStore } from "./JsonlStore";
3
+ import { rm, readFile, writeFile, mkdir } from "fs/promises";
4
+ import path from "path";
5
+ import os from "os";
6
+
7
+ const TEST_DIR = path.join(os.tmpdir(), "jsonlstore-test-" + process.pid);
8
+ const TEST_FILE = path.join(TEST_DIR, "test.jsonl");
9
+
10
+ describe("JsonlStore", () => {
11
+ let store: JsonlStore;
12
+
13
+ beforeEach(async () => {
14
+ try {
15
+ await rm(TEST_DIR, { recursive: true, force: true });
16
+ } catch {
17
+ // ignore cleanup failures (e.g. Windows lock files from previous test)
18
+ }
19
+ await mkdir(TEST_DIR, { recursive: true });
20
+ store = new JsonlStore(TEST_FILE);
21
+ });
22
+
23
+ afterEach(async () => {
24
+ try {
25
+ await rm(TEST_DIR, { recursive: true, force: true });
26
+ } catch {
27
+ // ignore cleanup failures
28
+ }
29
+ });
30
+
31
+ describe("load", () => {
32
+ it("should return empty map for new file", async () => {
33
+ const docs = await store.load();
34
+ expect(docs.size).toBe(0);
35
+ });
36
+
37
+ it("should load existing JSONL data", async () => {
38
+ await writeFile(TEST_FILE, '{"_id":"1","name":"Alice"}\n{"_id":"2","name":"Bob"}\n');
39
+ const docs = await store.load();
40
+ expect(docs.size).toBe(2);
41
+ expect(docs.get("1")).toEqual({ _id: "1", name: "Alice" });
42
+ });
43
+
44
+ it("should merge duplicate IDs (last wins)", async () => {
45
+ await writeFile(
46
+ TEST_FILE,
47
+ '{"_id":"1","name":"Alice","age":30}\n{"_id":"1","name":"Alice Updated"}\n',
48
+ );
49
+ const docs = await store.load();
50
+ expect(docs.size).toBe(1);
51
+ expect(docs.get("1")).toEqual({ _id: "1", name: "Alice Updated", age: 30 });
52
+ });
53
+
54
+ it("should handle $$deleted tombstones", async () => {
55
+ await writeFile(TEST_FILE, '{"_id":"1","name":"Alice"}\n{"_id":"1","$$deleted":true}\n');
56
+ const docs = await store.load();
57
+ expect(docs.size).toBe(0);
58
+ });
59
+
60
+ it("should skip lines without _id", async () => {
61
+ await writeFile(TEST_FILE, '{"name":"no id"}\n{"_id":"1","name":"valid"}\n');
62
+ const docs = await store.load();
63
+ expect(docs.size).toBe(1);
64
+ });
65
+
66
+ it("should skip corrupt lines gracefully", async () => {
67
+ await writeFile(
68
+ TEST_FILE,
69
+ '{"_id":"1","name":"ok"}\n{corrupt json\n{"_id":"2","name":"also ok"}\n',
70
+ );
71
+ const docs = await store.load();
72
+ expect(docs.size).toBe(2);
73
+ });
74
+
75
+ it("should recover from temp file when main file missing", async () => {
76
+ const tempFile = TEST_FILE + "~";
77
+ await writeFile(tempFile, '{"_id":"1","name":"recovered"}\n');
78
+ const docs = await store.load();
79
+ expect(docs.size).toBe(1);
80
+ expect(docs.get("1")!.name).toBe("recovered");
81
+ });
82
+ });
83
+
84
+ describe("getAll / getById / find / findOne", () => {
85
+ beforeEach(async () => {
86
+ await writeFile(
87
+ TEST_FILE,
88
+ '{"_id":"1","name":"Alice","age":30}\n{"_id":"2","name":"Bob","age":25}\n{"_id":"3","name":"Charlie","age":35}\n',
89
+ );
90
+ await store.load();
91
+ });
92
+
93
+ it("getAll returns all documents", () => {
94
+ expect(store.getAll()).toHaveLength(3);
95
+ });
96
+
97
+ it("getById returns correct doc", () => {
98
+ expect(store.getById("2")).toEqual({ _id: "2", name: "Bob", age: 25 });
99
+ });
100
+
101
+ it("getById returns undefined for missing id", () => {
102
+ expect(store.getById("999")).toBeUndefined();
103
+ });
104
+
105
+ it("find returns matching docs", () => {
106
+ const result = store.find((d) => d.age > 28);
107
+ expect(result).toHaveLength(2);
108
+ });
109
+
110
+ it("findOne returns first match", () => {
111
+ const result = store.findOne((d) => d.age > 28);
112
+ expect(result).toBeDefined();
113
+ expect(result!.age).toBeGreaterThan(28);
114
+ });
115
+
116
+ it("findOne returns undefined when no match", () => {
117
+ expect(store.findOne((d) => d.age > 100)).toBeUndefined();
118
+ });
119
+ });
120
+
121
+ describe("append", () => {
122
+ it("should append and return doc with generated id", async () => {
123
+ await store.load();
124
+ const doc = await store.append({ name: "test" } as any);
125
+ expect(doc._id).toBeDefined();
126
+ expect(doc.name).toBe("test");
127
+ expect(store.getAll()).toHaveLength(1);
128
+ });
129
+
130
+ it("should append with provided _id", async () => {
131
+ await store.load();
132
+ const doc = await store.append({ _id: "custom-id", name: "test" } as any);
133
+ expect(doc._id).toBe("custom-id");
134
+ });
135
+
136
+ it("should merge with existing doc of same _id", async () => {
137
+ await store.load();
138
+ await store.append({ _id: "x", name: "first", value: 1 } as any);
139
+ await store.append({ _id: "x", name: "second" } as any);
140
+ const doc = store.getById("x")!;
141
+ expect(doc.name).toBe("second");
142
+ expect(doc.value).toBe(1);
143
+ });
144
+ });
145
+
146
+ describe("updateById", () => {
147
+ it("should update existing doc", async () => {
148
+ await store.load();
149
+ await store.append({ _id: "u1", name: "original", status: "active" } as any);
150
+ await store.updateById("u1", { status: "done" });
151
+ expect(store.getById("u1")!.status).toBe("done");
152
+ expect(store.getById("u1")!.name).toBe("original");
153
+ });
154
+
155
+ it("should no-op for non-existent id", async () => {
156
+ await store.load();
157
+ await store.updateById("nonexistent", { status: "done" });
158
+ expect(store.getAll()).toHaveLength(0);
159
+ });
160
+ });
161
+
162
+ describe("deleteById", () => {
163
+ it("should remove doc from memory and write tombstone", async () => {
164
+ await store.load();
165
+ await store.append({ _id: "d1", name: "to delete" } as any);
166
+ expect(store.getAll()).toHaveLength(1);
167
+
168
+ await store.deleteById("d1");
169
+ expect(store.getAll()).toHaveLength(0);
170
+ expect(store.getById("d1")).toBeUndefined();
171
+
172
+ // Tombstone should be written to file
173
+ const content = await readFile(TEST_FILE, "utf-8");
174
+ expect(content).toContain('"$$deleted":true');
175
+ });
176
+ });
177
+
178
+ describe("compact", () => {
179
+ it("should deduplicate entries", async () => {
180
+ await store.load();
181
+ await store.append({ _id: "c1", name: "v1" } as any);
182
+ await store.updateById("c1", { name: "v2" });
183
+ await store.updateById("c1", { name: "v3" });
184
+
185
+ // Before compact: 3 lines
186
+ const before = (await readFile(TEST_FILE, "utf-8")).trim().split("\n");
187
+ expect(before).toHaveLength(3);
188
+
189
+ await store.compact();
190
+
191
+ // After compact: 1 line
192
+ const after = (await readFile(TEST_FILE, "utf-8")).trim().split("\n");
193
+ expect(after).toHaveLength(1);
194
+ expect(JSON.parse(after[0]!).name).toBe("v3");
195
+ });
196
+
197
+ it("should handle empty store", async () => {
198
+ await store.load();
199
+ await store.compact();
200
+ const content = await readFile(TEST_FILE, "utf-8");
201
+ expect(content).toBe("");
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { ReadyManager } from "./ReadyManager";
3
+
4
+ describe("ReadyManager", () => {
5
+ it("should start in not ready state", () => {
6
+ const manager = new ReadyManager();
7
+ expect(manager.wait()).toBeInstanceOf(Promise);
8
+ });
9
+
10
+ it("should resolve wait when ready is called", async () => {
11
+ const manager = new ReadyManager();
12
+ const waitPromise = manager.wait();
13
+
14
+ manager.ready();
15
+
16
+ await expect(waitPromise).resolves.toBeUndefined();
17
+ });
18
+
19
+ it("should resolve immediately if already ready", async () => {
20
+ const manager = new ReadyManager();
21
+ manager.ready();
22
+
23
+ const result = manager.wait();
24
+ expect(result).toBeUndefined();
25
+ });
26
+
27
+ it("should handle multiple waiters", async () => {
28
+ const manager = new ReadyManager();
29
+ const wait1 = manager.wait();
30
+ const wait2 = manager.wait();
31
+ const wait3 = manager.wait();
32
+
33
+ manager.ready();
34
+
35
+ await Promise.all([
36
+ expect(wait1).resolves.toBeUndefined(),
37
+ expect(wait2).resolves.toBeUndefined(),
38
+ expect(wait3).resolves.toBeUndefined(),
39
+ ]);
40
+ });
41
+
42
+ it("should reset to not ready when unready is called", async () => {
43
+ const manager = new ReadyManager();
44
+ manager.ready();
45
+ manager.unready();
46
+
47
+ expect(manager.wait()).toBeInstanceOf(Promise);
48
+ });
49
+
50
+ it("should handle ready with no waiting queue", () => {
51
+ const manager = new ReadyManager();
52
+ manager.ready(); // Should not throw even if no one is waiting
53
+ expect(manager.wait()).toBeUndefined(); // Should be ready now
54
+ });
55
+
56
+ it("should handle multiple ready/unready cycles", async () => {
57
+ const manager = new ReadyManager();
58
+
59
+ // First cycle
60
+ const wait1 = manager.wait();
61
+ manager.ready();
62
+ await wait1;
63
+
64
+ // Reset
65
+ manager.unready();
66
+
67
+ // Second cycle
68
+ const wait2 = manager.wait();
69
+ manager.ready();
70
+ await expect(wait2).resolves.toBeUndefined();
71
+ });
72
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { agentNiceValue } from "./agentNice.ts";
3
+
4
+ describe("agentNice.agentNiceValue", () => {
5
+ it("defaults to 5 when unset or empty", () => {
6
+ expect(agentNiceValue({})).toBe(5);
7
+ expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "" })).toBe(5);
8
+ });
9
+
10
+ it("honors a valid positive value", () => {
11
+ expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "10" })).toBe(10);
12
+ expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "0" })).toBe(0); // 0 = disabled
13
+ });
14
+
15
+ it("clamps into the 0..19 nice range (never elevates)", () => {
16
+ expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "-5" })).toBe(0); // no negative (needs CAP_SYS_NICE)
17
+ expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "40" })).toBe(19);
18
+ });
19
+
20
+ it("falls back to the default on garbage", () => {
21
+ expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "abc" })).toBe(5);
22
+ expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "3.9" })).toBe(3); // truncates
23
+ });
24
+ });