@gorlitzer-labs/apiary 1.13.9 → 1.14.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/cli/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  join
4
- } from "../chunk-7JXPJO3R.js";
4
+ } from "../chunk-LNSDH2V5.js";
5
5
  import {
6
6
  deliverInvite,
7
7
  sameApiaryServer
8
- } from "../chunk-MEWZT5KA.js";
8
+ } from "../chunk-XP5F5NCQ.js";
9
9
  import {
10
10
  checkForUpdate,
11
11
  printUpdateNotice
@@ -16,27 +16,16 @@ import {
16
16
  isAgentAlive,
17
17
  listAgentSessions,
18
18
  listRoomSessions,
19
- resetTerminal,
20
19
  resolveBindAddress,
21
20
  saveAgentSession,
22
- serve,
23
- tmuxAttach,
24
- tmuxAvailable,
25
- tmuxCapturePane,
26
- tmuxCreateSession,
27
- tmuxInjectPaste,
28
- tmuxInjectText,
29
- tmuxKillSession,
30
- tmuxPaneIsShell,
31
- tmuxSendCommand,
32
- tmuxSendEnter,
33
- tmuxSendKey,
34
- tmuxSessionExists
35
- } from "../chunk-BCQQJR5E.js";
21
+ serve
22
+ } from "../chunk-LOONM4G3.js";
36
23
  import "../chunk-PEB6HEFT.js";
37
24
  import {
38
- buildRuntimeMcpOptions
39
- } from "../chunk-XLUTCAY6.js";
25
+ TmuxBridge,
26
+ buildRuntimeMcpOptions,
27
+ composerStillHolds
28
+ } from "../chunk-36WX2UAF.js";
40
29
  import {
41
30
  EventProcessor,
42
31
  RUNTIME_TOOL_NAMES,
@@ -46,8 +35,20 @@ import {
46
35
  shouldReportActivity
47
36
  } from "../chunk-WJD5EKO6.js";
48
37
  import {
49
- buildShareUrl
50
- } from "../chunk-4A7MREYX.js";
38
+ buildShareUrl,
39
+ resetTerminal,
40
+ tmuxAttach,
41
+ tmuxAvailable,
42
+ tmuxCapturePane,
43
+ tmuxCreateSession,
44
+ tmuxInjectPaste,
45
+ tmuxKillSession,
46
+ tmuxPaneIsShell,
47
+ tmuxSendCommand,
48
+ tmuxSendEnter,
49
+ tmuxSendKey,
50
+ tmuxSessionExists
51
+ } from "../chunk-N2LPKMPO.js";
51
52
  import {
52
53
  randomName
53
54
  } from "../chunk-D7DXZQWT.js";
@@ -61,349 +62,6 @@ import { writeFileSync as writeFileSync2, mkdtempSync, mkdirSync, rmSync, chmodS
61
62
  import { join as join4 } from "path";
62
63
  import { tmpdir, homedir as homedir3 } from "os";
63
64
 
64
- // src/cli/composer.ts
65
- var PROMPT_MARKER = /^\s*[›❯>]\s?/;
66
- function composerBlock(lines) {
67
- const tail = lines.slice(-25);
68
- let start = -1;
69
- for (let i = tail.length - 1; i >= 0; i--) {
70
- if (PROMPT_MARKER.test(tail[i])) {
71
- start = i;
72
- break;
73
- }
74
- }
75
- if (start === -1) return "";
76
- return tail.slice(start).join(" ");
77
- }
78
- function submissionProbe(text) {
79
- return text.replace(/\s+/g, " ").trim().slice(0, 40);
80
- }
81
- function composerStillHolds(lines, text) {
82
- const probe = submissionProbe(text);
83
- if (probe.length < 8) return false;
84
- const composer = composerBlock(lines).replace(/\s+/g, " ");
85
- return composer.includes(probe);
86
- }
87
-
88
- // src/cli/claude/tmux-bridge.ts
89
- var SPINNER_CHARS = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
90
- var BUSY_PATTERNS = [
91
- // "← 1 agent" / "← 3 agents"
92
- /←\s*\d+\s+agents?\b/
93
- ];
94
- var DIALOG_PATTERNS = [
95
- "Enter to select",
96
- "to navigate",
97
- "Esc to cancel",
98
- "Ready to code?",
99
- "Review your answers",
100
- "ctrl+g to edit in"
101
- ];
102
- var PERMISSION_PATTERNS = [
103
- "(Y)",
104
- "Allow ",
105
- "Deny ",
106
- "approve",
107
- "Yes / No",
108
- // Current Claude Code wording, captured from a live prompt. Without these a
109
- // permission dialog reads as idle and the bridge pastes a room event into
110
- // it, answering on the agent's behalf.
111
- "Do you want to proceed?",
112
- "don't ask again",
113
- // The folder-trust modal, which no flag skips.
114
- "Yes, I trust this folder"
115
- ];
116
- var BLOCKING_PATTERNS = [
117
- "Do you want to proceed?",
118
- "don't ask again",
119
- "Yes, I trust this folder",
120
- "Ready to code?",
121
- "Enter to select",
122
- /❯\s*1\.\s*Yes/,
123
- /\(y\s*\/\s*n\)/i,
124
- // The arrow glyphs claude renders beside it. Bare "to navigate" is prose.
125
- /[↑↓⬆⬇]\s*(?:[↑↓⬆⬇]\s*)?to navigate/
126
- ];
127
- function matchesAny(text, patterns) {
128
- const haystack = text.toLowerCase();
129
- for (const pattern of patterns) {
130
- if (typeof pattern === "string") {
131
- if (haystack.includes(pattern.toLowerCase())) return true;
132
- } else if (pattern.test(text)) {
133
- return true;
134
- }
135
- }
136
- return false;
137
- }
138
- function defaultUndeliveredWarning(text) {
139
- const preview = text.replace(/\s+/g, " ").slice(0, 80);
140
- process.stderr.write(`apiary: message NOT delivered to the agent \u2014 still in its composer: ${preview}
141
- `);
142
- }
143
- var TmuxBridge = class _TmuxBridge {
144
- session;
145
- queue = [];
146
- pollTimer = null;
147
- pollIntervalMs;
148
- keystrokeDelayMs;
149
- onUndelivered;
150
- stopped = false;
151
- blockedCount = 0;
152
- static BLOCKED_THRESHOLD = 150;
153
- // ~30s at 200ms poll — force inject if stuck
154
- constructor(session, opts) {
155
- this.session = session;
156
- this.pollIntervalMs = opts?.pollIntervalMs ?? 200;
157
- this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;
158
- this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning;
159
- }
160
- /**
161
- * Delivery callback — drop-in replacement for the raw tmuxDeliver lambda.
162
- * Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().
163
- */
164
- async deliver(parts, opts) {
165
- const text = contentPartsToString(parts);
166
- if (!text.trim()) return;
167
- this.inject(text, opts?.dedupe === true);
168
- }
169
- /**
170
- * Detect the current TUI state by reading the screen.
171
- * Exported for testing — the heuristic logic is in detectStateFromLines().
172
- */
173
- detectState() {
174
- if (tmuxPaneIsShell(this.session)) return "absent";
175
- const lines = this.captureScreen();
176
- return detectStateFromLines(lines);
177
- }
178
- /**
179
- * State to REPORT to the room, as opposed to the state used to decide
180
- * whether it is safe to type. Same screen, stricter about claiming a human
181
- * is needed — see detectReportedStateFromLines.
182
- */
183
- detectReportedState() {
184
- if (tmuxPaneIsShell(this.session)) return "absent";
185
- return detectReportedStateFromLines(this.captureScreen());
186
- }
187
- /**
188
- * Extract claude's live status label from the pane (the "Sautéed for 12s",
189
- * "Compacting conversation… 23%" lines under the prompt). Returns null when
190
- * the agent is idle / no status visible.
191
- *
192
- * The room TUI calls this through the agent runtime's poll loop to surface
193
- * "is the agent actually working or stuck?" without making the user attach
194
- * to each tmux pane.
195
- */
196
- getActivityLabel() {
197
- return extractActivityLabel(this.captureScreen());
198
- }
199
- /**
200
- * Try to inject text, choosing strategy based on TUI state.
201
- * If the state is unsafe, queues the text and starts polling.
202
- *
203
- * Text is flattened to a single line before injection to avoid triggering
204
- * Claude Code's paste detection. When multi-line text arrives via
205
- * `send-keys -l`, Claude Code detects it as a paste and collapses it into
206
- * "[Pasted text #1 +N lines]" which may not reliably submit with Enter.
207
- */
208
- inject(text, dedupe) {
209
- const flat = text.replace(/\n/g, " ");
210
- const state = this.detectState();
211
- switch (state) {
212
- case "idle":
213
- this.injectIdle(flat);
214
- break;
215
- case "typing":
216
- this.injectWhileTyping(flat);
217
- break;
218
- default:
219
- this.enqueue(flat, dedupe);
220
- break;
221
- }
222
- }
223
- /** Capture the screen via tmux capture-pane. */
224
- captureScreen() {
225
- return tmuxCapturePane(this.session);
226
- }
227
- /**
228
- * Inject into an idle prompt: type text + Enter, then check it went.
229
- *
230
- * This used to fire a second Enter unconditionally as a "safety net" against
231
- * Claude Code's paste detection swallowing the first. That is a guess in both
232
- * directions: it cannot tell whether the first Enter worked, and it cannot
233
- * tell whether the second one did either. Reading the composer back answers
234
- * the question instead, and only sends the second Enter when it is needed.
235
- */
236
- injectIdle(text) {
237
- tmuxInjectText(this.session, text);
238
- tmuxSendEnter(this.session);
239
- this.confirmSubmitted(text);
240
- }
241
- /**
242
- * Check the message left the composer, and press Enter once more if it did not.
243
- *
244
- * A submitted message moves into the transcript; one still in the composer was
245
- * never sent. If it is still there after the retry, the room is told rather
246
- * than left to read the agent's silence as a choice.
247
- */
248
- confirmSubmitted(text) {
249
- this.sleep(80);
250
- if (!composerStillHolds(this.captureScreen(), text)) return;
251
- tmuxSendEnter(this.session);
252
- this.sleep(120);
253
- if (!composerStillHolds(this.captureScreen(), text)) return;
254
- this.onUndelivered(text);
255
- }
256
- /**
257
- * Inject while the user is typing:
258
- * 1. Ctrl+U — cut line to kill ring
259
- * 2. Inject our text + Enter
260
- * 3. Ctrl+Y — paste the user's text back
261
- */
262
- injectWhileTyping(text) {
263
- tmuxSendKey(this.session, "C-u");
264
- this.sleep(this.keystrokeDelayMs);
265
- tmuxInjectText(this.session, text);
266
- tmuxSendEnter(this.session);
267
- this.confirmSubmitted(text);
268
- this.sleep(this.keystrokeDelayMs);
269
- tmuxSendKey(this.session, "C-y");
270
- }
271
- /** Add to queue and start polling if not already. */
272
- enqueue(text, dedupe) {
273
- if (dedupe && this.queue.includes(text)) return;
274
- this.queue.push(text);
275
- this.startPolling();
276
- }
277
- /** Start the polling timer to drain queued events. */
278
- startPolling() {
279
- if (this.pollTimer || this.stopped) return;
280
- this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);
281
- }
282
- /** Stop the polling timer. */
283
- stopPolling() {
284
- if (this.pollTimer) {
285
- clearInterval(this.pollTimer);
286
- this.pollTimer = null;
287
- }
288
- }
289
- /**
290
- * Try to drain one queued event if the state is safe.
291
- *
292
- * Drains one event at a time rather than batching all into one multi-line
293
- * string — multi-line text triggers Claude Code's paste detection which
294
- * collapses it into "[Pasted text #1 +N lines]".
295
- *
296
- * After injecting one event, the poll continues. The next cycle re-checks
297
- * state: if Claude is busy (streaming), remaining events wait. If idle,
298
- * the next event is injected. Events are already flattened in inject().
299
- */
300
- drainQueue() {
301
- if (this.queue.length === 0) {
302
- this.stopPolling();
303
- this.blockedCount = 0;
304
- return;
305
- }
306
- const state = this.detectState();
307
- if (state === "idle" || state === "typing") {
308
- this.blockedCount = 0;
309
- const text = this.queue.shift();
310
- if (state === "idle") {
311
- this.injectIdle(text);
312
- } else {
313
- this.injectWhileTyping(text);
314
- }
315
- if (this.queue.length === 0) {
316
- this.stopPolling();
317
- }
318
- } else {
319
- this.blockedCount++;
320
- if (this.blockedCount >= _TmuxBridge.BLOCKED_THRESHOLD) {
321
- this.blockedCount = 0;
322
- const text = this.queue.shift();
323
- this.injectIdle(text);
324
- if (this.queue.length === 0) this.stopPolling();
325
- }
326
- }
327
- }
328
- /** Cleanup. */
329
- stop() {
330
- this.stopped = true;
331
- this.stopPolling();
332
- this.queue.length = 0;
333
- }
334
- /** Synchronous sleep — only used for tiny keystroke delays. */
335
- sleep(ms) {
336
- if (ms <= 0) return;
337
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
338
- }
339
- };
340
- var ACTIVITY_LIVE = /([A-Za-zÀ-ÿ]+…\s*\([^)\n]*\))/;
341
- var ACTIVITY_COOK = /([A-Za-zÀ-ÿ]+ed\s+for\s+\d+s)/;
342
- var ACTIVITY_PROG = /((?:Compacting|Thinking|Loading|Connecting)[^()]*?(?:\s+\d+%)?)(?=\s*[(\n]|\s*$)/;
343
- function extractActivityLabel(lines) {
344
- if (lines.length === 0) return null;
345
- let best = null;
346
- for (const line of lines.slice(-25)) {
347
- const m = line.match(ACTIVITY_PROG) ?? line.match(ACTIVITY_COOK) ?? line.match(ACTIVITY_LIVE);
348
- if (m) best = m[1].trim();
349
- }
350
- return best;
351
- }
352
- function detectStateFromLines(lines, opts) {
353
- if (lines.length === 0) return "unknown";
354
- const tail = lines.slice(-30);
355
- const tailText = tail.join("\n");
356
- if (opts?.ignorePrompts !== true) {
357
- for (const pattern of DIALOG_PATTERNS) {
358
- if (tailText.includes(pattern)) return "dialog";
359
- }
360
- for (const pattern of PERMISSION_PATTERNS) {
361
- if (tailText.includes(pattern)) return "permission";
362
- }
363
- }
364
- const lastFew = tail.slice(-5).join("");
365
- for (const ch of SPINNER_CHARS) {
366
- if (lastFew.includes(ch)) return "streaming";
367
- }
368
- for (const pattern of BUSY_PATTERNS) {
369
- if (pattern.test(tailText)) return "streaming";
370
- }
371
- const promptChar = /^[❯›](\s|$)/;
372
- const footerChar = /^[❯›]{2}\s/;
373
- const separatorLine = /^[─━─\-]{10,}/;
374
- for (let i = tail.length - 1; i >= 0; i--) {
375
- const line = tail[i].trimStart();
376
- if (footerChar.test(line)) {
377
- for (let j = i - 1; j >= 0; j--) {
378
- const above = tail[j].trimStart();
379
- if (promptChar.test(above)) {
380
- const content = above.replace(/^[❯›]\s*/, "").trim();
381
- return content.length === 0 ? "idle" : "typing";
382
- }
383
- }
384
- break;
385
- }
386
- }
387
- for (let i = tail.length - 1; i >= 0; i--) {
388
- const line = tail[i].trimStart();
389
- if (promptChar.test(line)) {
390
- const above = i > 0 ? tail[i - 1].trimStart() : "";
391
- const below = i < tail.length - 1 ? tail[i + 1].trimStart() : "";
392
- if (separatorLine.test(above) || separatorLine.test(below)) {
393
- const content = line.replace(/^[❯›]\s*/, "").trim();
394
- return content.length === 0 ? "idle" : "typing";
395
- }
396
- }
397
- }
398
- return "unknown";
399
- }
400
- function detectReportedStateFromLines(lines) {
401
- const loose = detectStateFromLines(lines);
402
- if (loose !== "dialog" && loose !== "permission") return loose;
403
- if (matchesAny(lines.slice(-30).join("\n"), BLOCKING_PATTERNS)) return loose;
404
- return detectStateFromLines(lines, { ignorePrompts: true });
405
- }
406
-
407
65
  // src/cli/runtime-setup.ts
408
66
  async function setupAgentRuntime(options) {
409
67
  const agentName = options.name ?? randomName();
@@ -1343,7 +1001,7 @@ function queueCodexMessage(threadId, text, run = defaultRunner) {
1343
1001
  }
1344
1002
 
1345
1003
  // src/cli/codex/tmux-bridge.ts
1346
- var SPINNER_CHARS2 = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
1004
+ var SPINNER_CHARS = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
1347
1005
  var APPROVAL_PATTERNS = [
1348
1006
  "Would you like to",
1349
1007
  "needs your approval",
@@ -1390,7 +1048,7 @@ function codexMentionPopupIsOpen(lines) {
1390
1048
  const tail = lines.slice(-20).join("\n");
1391
1049
  return MENTION_POPUP_PATTERNS.some((p) => p.test(tail));
1392
1050
  }
1393
- function defaultUndeliveredWarning2(text) {
1051
+ function defaultUndeliveredWarning(text) {
1394
1052
  const preview = text.replace(/\s+/g, " ").slice(0, 80);
1395
1053
  process.stderr.write(`apiary: message NOT delivered to the agent \u2014 still in its composer: ${preview}
1396
1054
  `);
@@ -1412,7 +1070,7 @@ var CodexTmuxBridge = class {
1412
1070
  this.pasteDelayMs = opts?.pasteDelayMs ?? 150;
1413
1071
  this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;
1414
1072
  this.resolveThreadId = opts?.resolveThreadId;
1415
- this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning2;
1073
+ this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning;
1416
1074
  }
1417
1075
  /**
1418
1076
  * The Codex session id, resolved once and remembered.
@@ -1620,7 +1278,7 @@ var CodexTmuxBridge = class {
1620
1278
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1621
1279
  }
1622
1280
  };
1623
- function matchesAny2(text, patterns) {
1281
+ function matchesAny(text, patterns) {
1624
1282
  const haystack = text.toLowerCase();
1625
1283
  for (const pattern of patterns) {
1626
1284
  if (typeof pattern === "string") {
@@ -1635,13 +1293,13 @@ function detectCodexStateFromLines(lines) {
1635
1293
  if (lines.length === 0) return "unknown";
1636
1294
  const tail = lines.slice(-20);
1637
1295
  const tailText = tail.join("\n");
1638
- if (matchesAny2(tailText, BLOCKED_PATTERNS)) return "blocked";
1639
- if (matchesAny2(tailText, APPROVAL_PATTERNS)) return "approval";
1296
+ if (matchesAny(tailText, BLOCKED_PATTERNS)) return "blocked";
1297
+ if (matchesAny(tailText, APPROVAL_PATTERNS)) return "approval";
1640
1298
  for (const pattern of STREAMING_PATTERNS) {
1641
1299
  if (pattern.test(tailText)) return "streaming";
1642
1300
  }
1643
1301
  const lastFew = tail.slice(-5).join("");
1644
- for (const ch of SPINNER_CHARS2) {
1302
+ for (const ch of SPINNER_CHARS) {
1645
1303
  if (lastFew.includes(ch)) return "streaming";
1646
1304
  }
1647
1305
  const nonEmpty = tail.filter((l) => l.trim().length > 0);
@@ -2155,7 +1813,7 @@ async function main() {
2155
1813
  const all = rest.includes("--all");
2156
1814
  const positional = rest.find((a) => !a.startsWith("--"));
2157
1815
  const name = getFlag("name", rest) ?? positional;
2158
- const { roomStop } = await import("../room-ID4DQDPT.js");
1816
+ const { roomStop } = await import("../room-XC546NTX.js");
2159
1817
  if (all) {
2160
1818
  for (const r of listRoomSessions()) {
2161
1819
  try {
@@ -2229,7 +1887,7 @@ async function main() {
2229
1887
  if (sub === "create") {
2230
1888
  const portStr = getFlag("port", roomArgs);
2231
1889
  const port = portStr ? parseInt(portStr, 10) : void 0;
2232
- const { roomCreate } = await import("../room-ID4DQDPT.js");
1890
+ const { roomCreate } = await import("../room-XC546NTX.js");
2233
1891
  await roomCreate({
2234
1892
  room: getFlag("room", roomArgs) ?? roomArgs.find((a) => !a.startsWith("--")),
2235
1893
  port,
@@ -2246,12 +1904,12 @@ async function main() {
2246
1904
  console.error("Usage: apiary room resume <name>");
2247
1905
  process.exit(1);
2248
1906
  }
2249
- const { roomResume } = await import("../room-ID4DQDPT.js");
1907
+ const { roomResume } = await import("../room-XC546NTX.js");
2250
1908
  await roomResume(name);
2251
1909
  return;
2252
1910
  }
2253
1911
  if (sub === "list" || sub === "ls") {
2254
- const { roomList } = await import("../room-ID4DQDPT.js");
1912
+ const { roomList } = await import("../room-XC546NTX.js");
2255
1913
  roomList();
2256
1914
  return;
2257
1915
  }
@@ -2261,7 +1919,7 @@ async function main() {
2261
1919
  console.error("Usage: apiary room stop <name>");
2262
1920
  process.exit(1);
2263
1921
  }
2264
- const { roomStop } = await import("../room-ID4DQDPT.js");
1922
+ const { roomStop } = await import("../room-XC546NTX.js");
2265
1923
  await roomStop(name);
2266
1924
  return;
2267
1925
  }
@@ -2276,7 +1934,7 @@ async function main() {
2276
1934
  console.error(`Invalid port: ${portStr}`);
2277
1935
  process.exit(1);
2278
1936
  }
2279
- const { spawnDaemonServer, isDaemonResult } = await import("../room-ID4DQDPT.js");
1937
+ const { spawnDaemonServer, isDaemonResult } = await import("../room-XC546NTX.js");
2280
1938
  const daemonRes = await spawnDaemonServer({
2281
1939
  room: roomName,
2282
1940
  port,
@@ -2318,11 +1976,11 @@ async function main() {
2318
1976
  const name = getFlag("name", restArgs) ?? positionalName;
2319
1977
  const pendingUrls = [...joinUrls];
2320
1978
  if (name) {
2321
- const { consumeInvite } = await import("../invites-GRROUSCE.js");
1979
+ const { consumeInvite } = await import("../invites-ID5SRM3D.js");
2322
1980
  const inviteUrl = consumeInvite(name);
2323
1981
  if (inviteUrl) pendingUrls.push(inviteUrl);
2324
1982
  }
2325
- const { runMcpServer } = await import("../run-C22WQZVK.js");
1983
+ const { runMcpServer } = await import("../run-6JRFWMAQ.js");
2326
1984
  await runMcpServer({
2327
1985
  joinUrls: pendingUrls.length > 0 ? pendingUrls : void 0,
2328
1986
  name,