@gorlitzer-labs/apiary 1.13.3

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/agent/index.d.ts +689 -0
  4. package/dist/agent/index.js +34 -0
  5. package/dist/agent/index.js.map +1 -0
  6. package/dist/chunk-5V7QLCGB.js +184 -0
  7. package/dist/chunk-5V7QLCGB.js.map +1 -0
  8. package/dist/chunk-AYQJIAR5.js +152 -0
  9. package/dist/chunk-AYQJIAR5.js.map +1 -0
  10. package/dist/chunk-BZ4VDWC2.js +76 -0
  11. package/dist/chunk-BZ4VDWC2.js.map +1 -0
  12. package/dist/chunk-CFY6Z6Q6.js +95 -0
  13. package/dist/chunk-CFY6Z6Q6.js.map +1 -0
  14. package/dist/chunk-CZHO4DBN.js +1719 -0
  15. package/dist/chunk-CZHO4DBN.js.map +1 -0
  16. package/dist/chunk-D7DXZQWT.js +208 -0
  17. package/dist/chunk-D7DXZQWT.js.map +1 -0
  18. package/dist/chunk-ED6ROH6B.js +2021 -0
  19. package/dist/chunk-ED6ROH6B.js.map +1 -0
  20. package/dist/chunk-PEB6HEFT.js +606 -0
  21. package/dist/chunk-PEB6HEFT.js.map +1 -0
  22. package/dist/chunk-QL66XSDB.js +157 -0
  23. package/dist/chunk-QL66XSDB.js.map +1 -0
  24. package/dist/chunk-TPNFOQA7.js +288 -0
  25. package/dist/chunk-TPNFOQA7.js.map +1 -0
  26. package/dist/chunk-ZEFRNAX6.js +1771 -0
  27. package/dist/chunk-ZEFRNAX6.js.map +1 -0
  28. package/dist/cli/index.d.ts +1 -0
  29. package/dist/cli/index.js +2432 -0
  30. package/dist/cli/index.js.map +1 -0
  31. package/dist/index-Dp3QSGog.d.ts +837 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +22 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/invites-OFXIQLLN.js +23 -0
  36. package/dist/invites-OFXIQLLN.js.map +1 -0
  37. package/dist/join-CQXEF65B.js +15 -0
  38. package/dist/join-CQXEF65B.js.map +1 -0
  39. package/dist/room-Z7KDRVYD.js +1099 -0
  40. package/dist/room-Z7KDRVYD.js.map +1 -0
  41. package/dist/run-DOADB4T7.js +90 -0
  42. package/dist/run-DOADB4T7.js.map +1 -0
  43. package/dist/update-JGE3WCQB.js +11 -0
  44. package/dist/update-JGE3WCQB.js.map +1 -0
  45. package/package.json +72 -0
@@ -0,0 +1,2021 @@
1
+ import {
2
+ isAgentAlive,
3
+ listAgentSessions,
4
+ listRoomSessions,
5
+ loadConfig,
6
+ roomEmoji,
7
+ saveConfig,
8
+ saveRoomSession,
9
+ tmuxCapturePane,
10
+ tmuxInjectText,
11
+ tmuxSendEnter,
12
+ tmuxSessionExists
13
+ } from "./chunk-CZHO4DBN.js";
14
+ import {
15
+ extractToken
16
+ } from "./chunk-AYQJIAR5.js";
17
+ import {
18
+ randomName
19
+ } from "./chunk-D7DXZQWT.js";
20
+ import {
21
+ can
22
+ } from "./chunk-QL66XSDB.js";
23
+
24
+ // src/cli/join.ts
25
+ import { randomUUID } from "crypto";
26
+ import { createInterface } from "readline";
27
+
28
+ // src/cli/tui.tsx
29
+ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
30
+ import { render, Box, Text, Static, useStdout, useInput } from "ink";
31
+ import { exec } from "child_process";
32
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
33
+ var C = {
34
+ cyan: "#00d4ff",
35
+ purple: "#8b5cf6",
36
+ orange: "#ff8c42",
37
+ pink: "#f472b6",
38
+ green: "#34d399",
39
+ yellow: "#fbbf24",
40
+ danger: "#f87171",
41
+ text: "#eceff4",
42
+ secondary: "#b0b7c4",
43
+ dim: "#7e8798",
44
+ muted: "#5b6679",
45
+ border: "#475264"
46
+ };
47
+ var AGENT_COLORS = [C.cyan, C.purple, C.orange, C.pink, C.green, C.yellow];
48
+ var SIGILS = ["\u25C6", "\u25B2", "\u25CF", "\u25A0", "\u2605", "\u25C9", "\u25C8", "\u25B8"];
49
+ var AMB = "#fbbf24";
50
+ var WNG = "#e0e7ee";
51
+ var BLK = "#3a3a4a";
52
+ var BANNER = [
53
+ [
54
+ { text: " _ ", color: "#ffb347" },
55
+ { text: "\\ ", color: BLK },
56
+ { text: "\\ ", color: WNG }
57
+ ],
58
+ [
59
+ { text: " ____ _____ (_)___ ________ __ ", color: "#ffa733" },
60
+ { text: "\\ ", color: WNG },
61
+ { text: "\\ ", color: BLK },
62
+ { text: "\\ ", color: WNG }
63
+ ],
64
+ [
65
+ { text: " / __ `/ __ \\/ / __ `/ ___/ / / / ", color: "#ff9b1f" },
66
+ { text: "(o o)", color: AMB }
67
+ ],
68
+ [
69
+ { text: "/ /_/ / /_/ / / /_/ / / / /_/ / ", color: "#ff8f0b" },
70
+ { text: ")=BzZz=(", color: AMB }
71
+ ],
72
+ [
73
+ { text: "\\__,_/ .___/_/\\__,_/_/ \\__, / ", color: "#f7a600" },
74
+ { text: "/ ", color: WNG },
75
+ { text: "/ ", color: BLK },
76
+ { text: "/ ", color: WNG }
77
+ ],
78
+ [
79
+ { text: " /_/ /____/ ", color: "#f0c000" },
80
+ { text: "/ ", color: BLK },
81
+ { text: "/ ", color: WNG },
82
+ { text: "/ ", color: BLK }
83
+ ]
84
+ ];
85
+ var FIGHT_BANNER = [
86
+ [{ text: "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557", color: "#f87171" }],
87
+ [{ text: "\u2551", color: "#f87171" }, { text: " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2557 ", color: "#ff4444" }, { text: "\u2551", color: "#f87171" }],
88
+ [{ text: "\u2551", color: "#f87171" }, { text: " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2551 ", color: "#ff6b6b" }, { text: "\u2551", color: "#f87171" }],
89
+ [{ text: "\u2551", color: "#f87171" }, { text: " \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 ", color: "#ff8888" }, { text: "\u2551", color: "#f87171" }],
90
+ [{ text: "\u2551", color: "#f87171" }, { text: " \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2550\u255D\u255A\u2550\u255D ", color: "#ff6b6b" }, { text: "\u2551", color: "#f87171" }],
91
+ [{ text: "\u2551", color: "#f87171" }, { text: " \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2557 ", color: "#ff4444" }, { text: "\u2551", color: "#f87171" }],
92
+ [{ text: "\u2551", color: "#f87171" }, { text: " \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D ", color: "#ff4444" }, { text: "\u2551", color: "#f87171" }],
93
+ [{ text: "\u2551", color: "#f87171" }, { text: " ", color: "#f87171" }, { text: "\u2551", color: "#f87171" }],
94
+ [{ text: "\u2551", color: "#f87171" }, { text: " \u2694\uFE0F NO MERCY \xB7 NO RULES \xB7 NO FILTER \u2694\uFE0F ", color: "#fbbf24" }, { text: "\u2551", color: "#f87171" }],
95
+ [{ text: "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D", color: "#f87171" }]
96
+ ];
97
+ var FIGHT_HITS = ["POW!", "WHAM!", "K.O.!", "BAM!", "CRACK!", "BOOM!", "SLAM!", "ZAP!", "THWACK!", "KAPOW!", "HADOUKEN!", "FATALITY!", "COMBO!", "ULTRA!"];
98
+ var FIGHT_COLORS = ["#ff4444", "#ff6b6b", "#fbbf24", "#ff8c42", "#f472b6", "#8b5cf6"];
99
+ function pickFightHit(messageId) {
100
+ let h = 0;
101
+ for (let i = 0; i < messageId.length; i++) h = (h << 5) - h + messageId.charCodeAt(i) | 0;
102
+ const idx = Math.abs(h);
103
+ return {
104
+ word: FIGHT_HITS[idx % FIGHT_HITS.length],
105
+ color: FIGHT_COLORS[idx % FIGHT_COLORS.length]
106
+ };
107
+ }
108
+ var ENGAGEMENT_MODES = [
109
+ "everyone",
110
+ "people",
111
+ "agents",
112
+ "fight",
113
+ "standby-everyone",
114
+ "standby-people",
115
+ "standby-agents"
116
+ ];
117
+ var SLASH_COMMANDS = [
118
+ { name: "/help", description: "Show commands" },
119
+ { name: "/who", description: "List participants" },
120
+ { name: "/rules", description: "Show the room rules" },
121
+ { name: "/watch", description: "Open an agent's terminal in a new window", params: [
122
+ { label: "name", completions: "participants" }
123
+ ] },
124
+ { name: "/leave", description: "Disconnect and exit" },
125
+ { name: "/share", description: "Generate share links", op: "create_share_link" },
126
+ { name: "/kick", description: "Remove a participant", op: "kick", params: [
127
+ { label: "name", completions: "participants" }
128
+ ] },
129
+ { name: "/mute", description: "Make read-only (guest)", op: "mute", params: [
130
+ { label: "name", completions: "participants" }
131
+ ] },
132
+ { name: "/unmute", description: "Restore to member", op: "unmute", params: [
133
+ { label: "name", completions: "participants" }
134
+ ] },
135
+ { name: "/setmode", description: "Set engagement mode", op: "set_mode_for", params: [
136
+ { label: "name", completions: "participants" },
137
+ { label: "mode", completions: ENGAGEMENT_MODES }
138
+ ] },
139
+ { name: "/promote", description: "Promote to product owner", op: "promote", params: [
140
+ { label: "name", completions: "participants" }
141
+ ] },
142
+ { name: "/demote", description: "Demote product owner to member", op: "demote", params: [
143
+ { label: "name", completions: "participants" }
144
+ ] },
145
+ { name: "/ping", description: "Ping a participant", op: "ping", params: [
146
+ { label: "name", completions: "participants" }
147
+ ] },
148
+ { name: "/peek", description: "Show tmux command to view an agent's session", params: [
149
+ { label: "name", completions: "participants" }
150
+ ] },
151
+ { name: "/clear", description: "Wipe room history", op: "clear_history" },
152
+ { name: "/tunnel", description: "Start a cloudflared tunnel", op: "start_tunnel" },
153
+ { name: "/sound", description: "Toggle notification sounds" }
154
+ ];
155
+ var CMD_DISPLAY_COL = 26;
156
+ var OBSERVED_TTL_MS = 1e4;
157
+ var INFERRED_DECAY_MS = OBSERVED_TTL_MS * 2;
158
+ function shouldApplyAgentState(opts) {
159
+ if (opts.observed) return true;
160
+ if (opts.lastObservedAt === void 0) return true;
161
+ return opts.now - opts.lastObservedAt >= (opts.ttlMs ?? OBSERVED_TTL_MS);
162
+ }
163
+ function formatTokens(n) {
164
+ if (!Number.isFinite(n) || n < 0) return "0";
165
+ if (n < 1e3) return String(Math.round(n));
166
+ if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
167
+ return `${(n / 1e6).toFixed(n < 1e7 ? 1 : 0)}M`;
168
+ }
169
+ function seedHash(s) {
170
+ let h = 0;
171
+ for (let i = 0; i < s.length; i++) h = (h << 5) - h + s.charCodeAt(i) | 0;
172
+ return Math.abs(h);
173
+ }
174
+ function makeIdentityAssigner() {
175
+ const map = /* @__PURE__ */ new Map();
176
+ return (name) => {
177
+ if (!map.has(name)) {
178
+ const h = seedHash(name);
179
+ map.set(name, {
180
+ color: AGENT_COLORS[h % AGENT_COLORS.length],
181
+ sigil: SIGILS[Math.floor(h / AGENT_COLORS.length) % SIGILS.length]
182
+ });
183
+ }
184
+ return map.get(name);
185
+ };
186
+ }
187
+ var SHELL_MAX_OUTPUT_BYTES = 64 * 1024;
188
+ var SHELL_TIMEOUT_MS = 6e4;
189
+ function runLocalShell(cmd, push) {
190
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
191
+ const startId = `sh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
192
+ push({ id: `${startId}-cmd`, ts, kind: "system", content: `$ ${cmd}` });
193
+ exec(cmd, {
194
+ maxBuffer: SHELL_MAX_OUTPUT_BYTES,
195
+ timeout: SHELL_TIMEOUT_MS,
196
+ shell: process.env.SHELL || "/bin/sh"
197
+ }, (err, stdout, stderr) => {
198
+ const doneTs = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
199
+ const out = (stdout || "").toString().replace(/\s+$/, "");
200
+ const errOut = (stderr || "").toString().replace(/\s+$/, "");
201
+ if (out) push({ id: `${startId}-out`, ts: doneTs, kind: "system", content: out });
202
+ if (errOut) push({ id: `${startId}-err`, ts: doneTs, kind: "system", content: errOut });
203
+ if (err) {
204
+ const code = err.code;
205
+ const exitCode = err.code;
206
+ const tag = code === "ETIMEDOUT" ? "timeout" : `exit ${exitCode ?? "?"}`;
207
+ push({ id: `${startId}-end`, ts: doneTs, kind: "system", content: `\u21B3 ${tag}` });
208
+ } else if (!out && !errOut) {
209
+ push({ id: `${startId}-end`, ts: doneTs, kind: "system", content: "\u21B3 (no output)" });
210
+ }
211
+ });
212
+ }
213
+ function wordWrap(text, width) {
214
+ if (width < 10) width = 10;
215
+ const lines = [];
216
+ for (const paragraph of text.split("\n")) {
217
+ if (paragraph.length === 0) {
218
+ lines.push("");
219
+ continue;
220
+ }
221
+ let line = "";
222
+ for (const word of paragraph.split(/( +)/)) {
223
+ if (line.length + word.length > width && line.length > 0) {
224
+ lines.push(line);
225
+ line = word.replace(/^ +/, "");
226
+ } else {
227
+ line += word;
228
+ }
229
+ }
230
+ if (line.length > 0) lines.push(line);
231
+ }
232
+ if (lines.length === 0) lines.push("");
233
+ return lines;
234
+ }
235
+ function StyledContent({
236
+ text,
237
+ contentColor,
238
+ identify
239
+ }) {
240
+ const parts = [];
241
+ const pattern = /@([a-zA-Z0-9_-]+)/g;
242
+ let lastIdx = 0;
243
+ let match;
244
+ let key = 0;
245
+ while ((match = pattern.exec(text)) !== null) {
246
+ if (match.index > lastIdx) {
247
+ parts.push(/* @__PURE__ */ jsx(Text, { color: contentColor, children: text.slice(lastIdx, match.index) }, key++));
248
+ }
249
+ const mentionName = match[1];
250
+ const { color } = identify(mentionName);
251
+ parts.push(/* @__PURE__ */ jsxs(Text, { color, bold: true, children: [
252
+ "@",
253
+ mentionName
254
+ ] }, key++));
255
+ lastIdx = match.index + match[0].length;
256
+ }
257
+ if (lastIdx < text.length) {
258
+ parts.push(/* @__PURE__ */ jsx(Text, { color: contentColor, children: text.slice(lastIdx) }, key++));
259
+ }
260
+ if (parts.length === 0) {
261
+ return /* @__PURE__ */ jsx(Text, { color: contentColor, children: text });
262
+ }
263
+ return /* @__PURE__ */ jsx(Fragment, { children: parts });
264
+ }
265
+ function EventLine({
266
+ event,
267
+ identify,
268
+ cols,
269
+ zebra,
270
+ fightMode
271
+ }) {
272
+ const ts = /* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
273
+ event.ts,
274
+ " "
275
+ ] });
276
+ if (event.kind === "message") {
277
+ const { color, sigil } = identify(event.senderName);
278
+ const isSelf = event.isSelf;
279
+ const isFight = fightMode && !isSelf;
280
+ const nameColor = isSelf ? C.text : event.senderType === "agent" ? color : C.secondary;
281
+ const sigilColor = isSelf ? C.dim : event.senderType === "agent" ? color : C.dim;
282
+ const sigilChar = isSelf ? "\u203A" : isFight ? "\u2694" : event.senderType === "agent" ? sigil : "\xB7";
283
+ const contentColor = isSelf ? C.text : zebra ? "#d0d5de" : C.secondary;
284
+ const borderColor = isFight ? FIGHT_COLORS[Math.abs(seedHash(event.id)) % FIGHT_COLORS.length] : isSelf ? C.border : event.senderType === "agent" ? color : C.dim;
285
+ const borderStyle = isFight ? "double" : "round";
286
+ const boxChrome = 4;
287
+ const innerWidth = Math.max(10, cols - 2 - boxChrome);
288
+ const wrapped = wordWrap(event.content, innerWidth);
289
+ const hit = isFight ? pickFightHit(event.id) : null;
290
+ const replyTag = event.replyToName ? /* @__PURE__ */ jsxs(Fragment, { children: [
291
+ /* @__PURE__ */ jsx(Text, { color: C.dim, children: " \u21A9 " }),
292
+ /* @__PURE__ */ jsx(Text, { color: C.secondary, children: event.replyToName })
293
+ ] }) : null;
294
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(
295
+ Box,
296
+ {
297
+ flexDirection: "column",
298
+ borderStyle,
299
+ borderColor,
300
+ paddingX: 1,
301
+ width: cols - 2,
302
+ children: [
303
+ /* @__PURE__ */ jsxs(Box, { children: [
304
+ ts,
305
+ /* @__PURE__ */ jsxs(Text, { color: sigilColor, children: [
306
+ sigilChar,
307
+ " "
308
+ ] }),
309
+ /* @__PURE__ */ jsx(Text, { color: nameColor, bold: isSelf || isFight, children: event.senderName }),
310
+ replyTag,
311
+ hit && /* @__PURE__ */ jsx(Text, { color: hit.color, bold: true, children: ` \xAB ${hit.word} \xBB` })
312
+ ] }),
313
+ wrapped.map((line, i) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: /* @__PURE__ */ jsx(StyledContent, { text: line, contentColor: isFight ? C.text : contentColor, identify }) }) }, i))
314
+ ]
315
+ }
316
+ ) }) });
317
+ }
318
+ if (event.kind === "join") {
319
+ const isAgent = event.participantType === "agent";
320
+ const { color, sigil } = isAgent ? identify(event.name) : { color: C.dim, sigil: "\xB7" };
321
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
322
+ ts,
323
+ /* @__PURE__ */ jsxs(Text, { color: isAgent ? color : C.dim, children: [
324
+ sigil,
325
+ " "
326
+ ] }),
327
+ /* @__PURE__ */ jsx(Text, { color: isAgent ? color : C.dim, children: event.name }),
328
+ /* @__PURE__ */ jsx(Text, { color: C.green, children: " joined" })
329
+ ] });
330
+ }
331
+ if (event.kind === "leave") {
332
+ const isAgent = event.participantType === "agent";
333
+ const { color: nameColor } = isAgent ? identify(event.name) : { color: C.muted };
334
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
335
+ ts,
336
+ /* @__PURE__ */ jsx(Text, { color: C.muted, children: "\xB7 " }),
337
+ /* @__PURE__ */ jsx(Text, { color: nameColor, children: event.name }),
338
+ /* @__PURE__ */ jsx(Text, { color: C.danger, children: " left" })
339
+ ] });
340
+ }
341
+ if (event.kind === "mode") {
342
+ if (event.mode === "fight") {
343
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
344
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: " " }) }),
345
+ FIGHT_BANNER.map((segments, i) => /* @__PURE__ */ jsx(Text, { children: segments.map((s, j) => /* @__PURE__ */ jsx(Text, { color: s.color, bold: true, children: s.text }, j)) }, i)),
346
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: " " }) })
347
+ ] });
348
+ }
349
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
350
+ ts,
351
+ /* @__PURE__ */ jsx(Text, { color: C.dim, children: "mode \u2192 " }),
352
+ /* @__PURE__ */ jsx(Text, { color: C.yellow, bold: true, children: event.mode })
353
+ ] });
354
+ }
355
+ if (event.kind === "ping") {
356
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
357
+ ts,
358
+ /* @__PURE__ */ jsx(Text, { color: C.yellow, children: "\u{1F514} " }),
359
+ /* @__PURE__ */ jsx(Text, { color: C.yellow, bold: true, children: event.pingerName }),
360
+ /* @__PURE__ */ jsx(Text, { color: C.yellow, children: " pinged you." })
361
+ ] });
362
+ }
363
+ if (event.kind === "system") {
364
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
365
+ ts,
366
+ /* @__PURE__ */ jsx(Text, { color: C.dim, children: " " }),
367
+ /* @__PURE__ */ jsx(Text, { color: C.secondary, children: event.content })
368
+ ] });
369
+ }
370
+ return null;
371
+ }
372
+ function App({
373
+ roomName,
374
+ onSend,
375
+ onCtrlC,
376
+ onWatchAgent,
377
+ onReady,
378
+ readOnly,
379
+ authority,
380
+ initialSound = true
381
+ }) {
382
+ const [events, setEvents] = useState([]);
383
+ const [agentNames, setAgentNames] = useState([]);
384
+ const [participants, setParticipants] = useState([]);
385
+ const [input, setInput] = useState("");
386
+ const [cursorPos, setCursorPos] = useState(0);
387
+ const [selectedIndex, setSelectedIndex] = useState(0);
388
+ const [soundEnabled, setSoundEnabled] = useState(initialSound);
389
+ const [agentStates, setAgentStatesMap] = useState(/* @__PURE__ */ new Map());
390
+ const [agentActivities, setAgentActivitiesMap] = useState(/* @__PURE__ */ new Map());
391
+ const [agentMetrics, setAgentMetricsMap] = useState(/* @__PURE__ */ new Map());
392
+ const [, setStaleTick] = useState(0);
393
+ useEffect(() => {
394
+ const t = setInterval(() => setStaleTick((n) => n + 1), 5e3);
395
+ return () => clearInterval(t);
396
+ }, []);
397
+ const [fightMode, setFightMode] = useState(false);
398
+ const fightModeRef = useRef(false);
399
+ const soundRef = useRef(initialSound);
400
+ const stateRef = useRef(/* @__PURE__ */ new Map());
401
+ const activityRef = useRef(/* @__PURE__ */ new Map());
402
+ const metricsRef = useRef(/* @__PURE__ */ new Map());
403
+ const decayTimers = useRef(/* @__PURE__ */ new Map());
404
+ const { stdout } = useStdout();
405
+ const identify = useMemo(makeIdentityAssigner, []);
406
+ const setInputAt = useCallback((newInput, newPos) => {
407
+ setInput(newInput);
408
+ setCursorPos(Math.max(0, Math.min(newPos, newInput.length)));
409
+ }, []);
410
+ useEffect(() => {
411
+ setCursorPos((p) => Math.min(p, input.length));
412
+ }, [input]);
413
+ const eventBuffer = useRef([]);
414
+ const eventFlushTimer = useRef(null);
415
+ const MAX_EVENTS = 5e3;
416
+ const push = useCallback((event) => {
417
+ const shouldBell = event.kind === "ping" || event.kind === "message" && !event.isSelf;
418
+ if (shouldBell && soundRef.current && stdout.isTTY) stdout.write("\x07");
419
+ if (event.kind === "mode") {
420
+ const entering = event.mode === "fight";
421
+ setFightMode(entering);
422
+ fightModeRef.current = entering;
423
+ }
424
+ eventBuffer.current.push(event);
425
+ if (!eventFlushTimer.current) {
426
+ eventFlushTimer.current = setTimeout(() => {
427
+ eventFlushTimer.current = null;
428
+ const batch = eventBuffer.current.splice(0);
429
+ if (batch.length > 0) {
430
+ setEvents((prev) => {
431
+ const next = [...prev, ...batch];
432
+ return next.length > MAX_EVENTS ? next.slice(next.length - MAX_EVENTS) : next;
433
+ });
434
+ }
435
+ }, 80);
436
+ }
437
+ }, [stdout]);
438
+ const clear = useCallback(() => {
439
+ if (eventFlushTimer.current) {
440
+ clearTimeout(eventFlushTimer.current);
441
+ eventFlushTimer.current = null;
442
+ }
443
+ eventBuffer.current.length = 0;
444
+ setEvents([]);
445
+ if (stdout.isTTY) stdout.write("\x1B[2J\x1B[H");
446
+ }, [stdout]);
447
+ const toggleSound = useCallback(() => {
448
+ const next = !soundRef.current;
449
+ soundRef.current = next;
450
+ setSoundEnabled(next);
451
+ return next;
452
+ }, []);
453
+ const observedAtRef = React.useRef(/* @__PURE__ */ new Map());
454
+ const [stateSource, setStateSource] = useState(/* @__PURE__ */ new Map());
455
+ const DEFAULT_WORKING_DECAY_MS = INFERRED_DECAY_MS;
456
+ const PENDING_STALL_MS = 3e4;
457
+ const setAgentState = useCallback(
458
+ (name, state, opts) => {
459
+ const observed = opts?.observed === true;
460
+ if (!shouldApplyAgentState({
461
+ observed,
462
+ lastObservedAt: observedAtRef.current.get(name),
463
+ now: Date.now()
464
+ })) return;
465
+ if (observed) observedAtRef.current.set(name, Date.now());
466
+ setStateSource((prev) => {
467
+ const next = new Map(prev);
468
+ next.set(name, observed ? "reported" : "inferred");
469
+ return next;
470
+ });
471
+ const existing = decayTimers.current.get(name);
472
+ if (existing) {
473
+ clearTimeout(existing);
474
+ decayTimers.current.delete(name);
475
+ }
476
+ stateRef.current.set(name, state);
477
+ setAgentStatesMap(new Map(stateRef.current));
478
+ if (state === "working") {
479
+ const ms = opts?.decayMs ?? DEFAULT_WORKING_DECAY_MS;
480
+ decayTimers.current.set(name, setTimeout(() => {
481
+ if (stateRef.current.get(name) === "working") {
482
+ stateRef.current.set(name, "unknown");
483
+ setAgentStatesMap(new Map(stateRef.current));
484
+ }
485
+ decayTimers.current.delete(name);
486
+ }, ms));
487
+ } else if (state === "pending") {
488
+ decayTimers.current.set(name, setTimeout(() => {
489
+ if (stateRef.current.get(name) === "pending") {
490
+ stateRef.current.set(name, "stalled");
491
+ setAgentStatesMap(new Map(stateRef.current));
492
+ }
493
+ decayTimers.current.delete(name);
494
+ }, PENDING_STALL_MS));
495
+ }
496
+ },
497
+ []
498
+ );
499
+ const setAgentActivity = useCallback((name, label) => {
500
+ if (label && label.trim()) {
501
+ activityRef.current.set(name, { label: label.trim(), updatedAt: Date.now() });
502
+ } else {
503
+ activityRef.current.delete(name);
504
+ }
505
+ setAgentActivitiesMap(new Map(activityRef.current));
506
+ }, []);
507
+ const setAgentMetrics = useCallback((name, m) => {
508
+ metricsRef.current.set(name, m);
509
+ setAgentMetricsMap(new Map(metricsRef.current));
510
+ }, []);
511
+ useEffect(() => {
512
+ onReady({ push, clear, setAgentNames, setParticipants, toggleSound, setAgentState, setAgentActivity, setAgentMetrics });
513
+ return () => {
514
+ if (eventFlushTimer.current) clearTimeout(eventFlushTimer.current);
515
+ for (const t of decayTimers.current.values()) clearTimeout(t);
516
+ decayTimers.current.clear();
517
+ };
518
+ }, []);
519
+ useEffect(() => {
520
+ fightModeRef.current = fightMode;
521
+ }, [fightMode]);
522
+ const suggestionState = useMemo(() => {
523
+ const textBeforeCursor = input.slice(0, cursorPos);
524
+ const mentionMatch = textBeforeCursor.match(/@([a-zA-Z0-9_-]*)$/);
525
+ if (mentionMatch && participants.length > 0) {
526
+ const prefix = mentionMatch[1].toLowerCase();
527
+ const candidates = ["all", ...participants.filter((p) => p.toLowerCase() !== "all")];
528
+ const filtered2 = candidates.filter((p) => p.toLowerCase().startsWith(prefix));
529
+ if (filtered2.length > 0) {
530
+ const before = input.slice(0, mentionMatch.index);
531
+ const after = input.slice(cursorPos);
532
+ const items2 = filtered2.map((p) => ({
533
+ kind: "mention",
534
+ value: p,
535
+ insert: before + "@" + p + " " + after
536
+ }));
537
+ const ghostHint2 = prefix.length === 0 ? "" : filtered2[0].slice(prefix.length);
538
+ return { items: items2, ghostHint: ghostHint2 };
539
+ }
540
+ }
541
+ if (!input.startsWith("/")) return { items: [], ghostHint: "" };
542
+ const spaceIdx = input.indexOf(" ");
543
+ if (spaceIdx === -1) {
544
+ const prefix = input.toLowerCase();
545
+ const items2 = SLASH_COMMANDS.filter((cmd2) => {
546
+ if (cmd2.op && !can(authority, cmd2.op)) return false;
547
+ return cmd2.name.startsWith(prefix);
548
+ }).map((cmd2) => ({
549
+ kind: "command",
550
+ cmd: cmd2,
551
+ insert: cmd2.name + " "
552
+ }));
553
+ return { items: items2, ghostHint: "" };
554
+ }
555
+ const cmdName = input.slice(0, spaceIdx).toLowerCase();
556
+ const cmd = SLASH_COMMANDS.find((c) => c.name === cmdName && (!c.op || can(authority, c.op)));
557
+ if (!cmd?.params) return { items: [], ghostHint: "" };
558
+ const rest = input.slice(spaceIdx + 1);
559
+ const words = rest.split(/\s+/);
560
+ const hasTrailingSpace = rest.endsWith(" ") || rest === "";
561
+ const completedCount = hasTrailingSpace ? words.filter(Boolean).length : Math.max(0, words.length - 1);
562
+ const currentPrefix = hasTrailingSpace ? "" : (words[words.length - 1] ?? "").toLowerCase();
563
+ const paramIdx = completedCount;
564
+ if (paramIdx >= cmd.params.length) return { items: [], ghostHint: "" };
565
+ const param = cmd.params[paramIdx];
566
+ const ghostStart = currentPrefix ? paramIdx + 1 : paramIdx;
567
+ const ghostHint = cmd.params.slice(ghostStart).map((p) => `<${p.label}>`).join(" ");
568
+ if (!param.completions) return { items: [], ghostHint };
569
+ const values = param.completions === "participants" ? participants : param.completions;
570
+ const filtered = currentPrefix ? values.filter((v) => v.toLowerCase().startsWith(currentPrefix)) : values;
571
+ const completedWords = words.slice(0, completedCount).filter(Boolean);
572
+ const base = cmdName + (completedWords.length ? " " + completedWords.join(" ") : "") + " ";
573
+ const items = filtered.map((v) => ({
574
+ kind: "param",
575
+ value: v,
576
+ insert: base + v + " "
577
+ }));
578
+ return { items, ghostHint };
579
+ }, [input, cursorPos, authority, participants]);
580
+ const suggestions = suggestionState.items;
581
+ useEffect(() => {
582
+ setSelectedIndex(0);
583
+ }, [input]);
584
+ useInput((char, key) => {
585
+ if (key.ctrl && char === "c") {
586
+ onCtrlC?.();
587
+ return;
588
+ }
589
+ if (key.ctrl && char >= "1" && char <= "9") {
590
+ const idx = Number(char) - 1;
591
+ const target = agentNames[idx];
592
+ if (target) onWatchAgent?.(target);
593
+ return;
594
+ }
595
+ if (readOnly || !onSend) return;
596
+ if (suggestions.length > 0) {
597
+ if (key.downArrow) {
598
+ setSelectedIndex((i) => Math.min(i + 1, suggestions.length - 1));
599
+ return;
600
+ }
601
+ if (key.upArrow) {
602
+ setSelectedIndex((i) => Math.max(i - 1, 0));
603
+ return;
604
+ }
605
+ if (key.return || key.tab) {
606
+ const picked = suggestions[selectedIndex];
607
+ if (!picked) return;
608
+ if (key.return && picked.kind === "command" && !picked.cmd.params) {
609
+ onSend(picked.cmd.name);
610
+ setInputAt("", 0);
611
+ return;
612
+ }
613
+ setInputAt(picked.insert, picked.insert.length);
614
+ return;
615
+ }
616
+ if (key.escape) {
617
+ setInputAt("", 0);
618
+ return;
619
+ }
620
+ }
621
+ if (key.leftArrow) {
622
+ setCursorPos((p) => Math.max(0, p - 1));
623
+ return;
624
+ }
625
+ if (key.rightArrow) {
626
+ setCursorPos((p) => Math.min(p + 1, input.length));
627
+ return;
628
+ }
629
+ if (key.ctrl && char === "a") {
630
+ setCursorPos(0);
631
+ return;
632
+ }
633
+ if (key.ctrl && char === "e") {
634
+ setCursorPos(input.length);
635
+ return;
636
+ }
637
+ if (key.ctrl && char === "w") {
638
+ if (cursorPos === 0) return;
639
+ let pos = cursorPos - 1;
640
+ while (pos > 0 && input[pos] === " ") pos--;
641
+ while (pos > 0 && input[pos - 1] !== " ") pos--;
642
+ setInputAt(input.slice(0, pos) + input.slice(cursorPos), pos);
643
+ return;
644
+ }
645
+ if (key.return && key.meta) {
646
+ setInputAt(input.slice(0, cursorPos) + "\n" + input.slice(cursorPos), cursorPos + 1);
647
+ return;
648
+ }
649
+ if (key.return) {
650
+ const content = input.trim();
651
+ if (!content) {
652
+ setInputAt("", 0);
653
+ return;
654
+ }
655
+ if (content.startsWith("!")) {
656
+ const cmd = content.slice(1).trim();
657
+ setInputAt("", 0);
658
+ if (cmd) runLocalShell(cmd, push);
659
+ return;
660
+ }
661
+ onSend(content);
662
+ setInputAt("", 0);
663
+ return;
664
+ }
665
+ if (key.backspace || key.delete) {
666
+ if (cursorPos > 0) {
667
+ setInputAt(input.slice(0, cursorPos - 1) + input.slice(cursorPos), cursorPos - 1);
668
+ }
669
+ return;
670
+ }
671
+ if (key.ctrl || key.meta || key.escape || key.tab || key.upArrow || key.downArrow) {
672
+ return;
673
+ }
674
+ if (char) {
675
+ const normalized = char.replace(/\r\n?/g, "\n");
676
+ setInputAt(
677
+ input.slice(0, cursorPos) + normalized + input.slice(cursorPos),
678
+ cursorPos + normalized.length
679
+ );
680
+ }
681
+ });
682
+ const cols = stdout.columns ?? 80;
683
+ const entries = useMemo(() => {
684
+ let msgIdx = 0;
685
+ return [{ id: "__banner__" }, ...events.map((e) => {
686
+ const isMsg = e.kind === "message" && !e.isSelf;
687
+ const entry = { id: e.id, event: e, zebra: isMsg ? msgIdx % 2 === 1 : void 0 };
688
+ if (isMsg) msgIdx++;
689
+ return entry;
690
+ })];
691
+ }, [events]);
692
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
693
+ /* @__PURE__ */ jsx(Static, { items: entries, children: (entry) => {
694
+ if (!entry.event) {
695
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingTop: 1, paddingBottom: 1, children: [
696
+ BANNER.map((segments, i) => /* @__PURE__ */ jsx(Text, { children: segments.map((s, j) => /* @__PURE__ */ jsx(Text, { color: s.color, children: s.text }, j)) }, i)),
697
+ /* @__PURE__ */ jsx(Text, { color: C.muted, italic: true, children: " ah \u2014 it's not a sad alien, it's a bee" }),
698
+ /* @__PURE__ */ jsx(Text, { children: " " }),
699
+ /* @__PURE__ */ jsxs(Text, { children: [
700
+ /* @__PURE__ */ jsx(Text, { color: C.dim, children: " room " }),
701
+ /* @__PURE__ */ jsx(Text, { color: C.cyan, bold: true, children: roomName })
702
+ ] })
703
+ ] }, entry.id);
704
+ }
705
+ return /* @__PURE__ */ jsx(EventLine, { event: entry.event, identify, cols, zebra: entry.zebra, fightMode }, entry.id);
706
+ } }),
707
+ fightMode ? /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
708
+ /* @__PURE__ */ jsx(Text, { color: "#f87171", bold: true, children: "\u2550" }),
709
+ /* @__PURE__ */ jsx(Text, { color: "#ff4444", children: "\u2550".repeat(Math.max(0, cols - 4)) }),
710
+ /* @__PURE__ */ jsx(Text, { color: "#f87171", bold: true, children: "\u2550" })
711
+ ] }) : /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
712
+ /* @__PURE__ */ jsx(Text, { color: C.orange, children: "\u2500" }),
713
+ /* @__PURE__ */ jsx(Text, { color: C.border, children: "\u2500".repeat(Math.max(0, cols - 4)) }),
714
+ /* @__PURE__ */ jsx(Text, { color: C.yellow, children: "\u2500" })
715
+ ] }),
716
+ agentMetrics.size > 0 && (() => {
717
+ let totalCtx = 0;
718
+ for (const m of agentMetrics.values()) {
719
+ totalCtx += m.ctxTokens;
720
+ }
721
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
722
+ /* @__PURE__ */ jsx(Text, { color: C.muted, children: " room " }),
723
+ /* @__PURE__ */ jsx(Text, { color: C.muted, children: `${formatTokens(totalCtx)} ctx total` })
724
+ ] });
725
+ })(),
726
+ agentNames.length > 0 && /* @__PURE__ */ jsx(Box, { paddingX: 1, flexWrap: "wrap", children: agentNames.map((name, i) => {
727
+ const { color, sigil } = identify(name);
728
+ const state = agentStates.get(name) ?? "unknown";
729
+ const activity = agentActivities.get(name);
730
+ const metrics = agentMetrics.get(name);
731
+ const stateSuffix = (() => {
732
+ switch (state) {
733
+ case "pending":
734
+ return { glyph: " \u23F3 booting", color: C.yellow };
735
+ case "stalled":
736
+ return { glyph: " \u26A0 stalled", color: C.danger };
737
+ case "idle":
738
+ return { glyph: " \u2713", color: C.green };
739
+ case "blocked":
740
+ return { glyph: " \u23F8 needs you", color: C.orange };
741
+ case "working":
742
+ return { glyph: " \u2026", color: C.yellow };
743
+ case "unresponsive":
744
+ return { glyph: " \u26A0", color: C.danger };
745
+ case "unknown":
746
+ default:
747
+ return { glyph: " zzz", color: C.muted };
748
+ }
749
+ })();
750
+ const inferred = stateSource.get(name) === "inferred";
751
+ const glyphColor = inferred ? C.muted : stateSuffix.color;
752
+ const STALE_MS = 3e4;
753
+ const fresh = activity && Date.now() - activity.updatedAt <= STALE_MS;
754
+ const metricsText = metrics ? ` ${formatTokens(metrics.ctxTokens)}` : "";
755
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
756
+ i > 0 && /* @__PURE__ */ jsx(Text, { color: C.border, children: " \xB7 " }),
757
+ /* @__PURE__ */ jsxs(Text, { color, children: [
758
+ sigil,
759
+ " ",
760
+ name
761
+ ] }),
762
+ /* @__PURE__ */ jsx(Text, { color: glyphColor, children: stateSuffix.glyph }),
763
+ i < 9 && /* @__PURE__ */ jsx(Text, { color: C.border, children: ` \u29C9${i + 1}` }),
764
+ fresh && /* @__PURE__ */ jsxs(Text, { color: C.yellow, children: [
765
+ " ",
766
+ activity.label
767
+ ] }),
768
+ metricsText && /* @__PURE__ */ jsx(Text, { color: C.muted, children: metricsText })
769
+ ] }, name);
770
+ }) }),
771
+ readOnly || !onSend ? /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: C.muted, children: " watching as guest" }) }) : /* @__PURE__ */ jsx(Box, { paddingX: 1, flexDirection: "column", children: (() => {
772
+ const MAX_INPUT_LINES = 6;
773
+ const lines = (input || "").split("\n");
774
+ let charsSoFar = 0;
775
+ let cursorLine = lines.length - 1;
776
+ let cursorCol = 0;
777
+ for (let i = 0; i < lines.length; i++) {
778
+ const lineEnd = charsSoFar + lines[i].length;
779
+ if (cursorPos <= lineEnd) {
780
+ cursorLine = i;
781
+ cursorCol = cursorPos - charsSoFar;
782
+ break;
783
+ }
784
+ charsSoFar += lines[i].length + 1;
785
+ }
786
+ let scrollTop = 0;
787
+ if (lines.length > MAX_INPUT_LINES) {
788
+ scrollTop = Math.min(
789
+ Math.max(0, cursorLine - Math.floor(MAX_INPUT_LINES / 2)),
790
+ lines.length - MAX_INPUT_LINES
791
+ );
792
+ }
793
+ const visibleLines = lines.slice(scrollTop, scrollTop + MAX_INPUT_LINES);
794
+ const hasMore = lines.length > MAX_INPUT_LINES;
795
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
796
+ hasMore && scrollTop > 0 && /* @__PURE__ */ jsx(Box, { width: Math.max(0, cols - 2), children: /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
797
+ " \u2191 ",
798
+ scrollTop,
799
+ " more line",
800
+ scrollTop > 1 ? "s" : ""
801
+ ] }) }),
802
+ visibleLines.map((line, vi) => {
803
+ const i = scrollTop + vi;
804
+ return /* @__PURE__ */ jsxs(Box, { width: Math.max(0, cols - 2), children: [
805
+ /* @__PURE__ */ jsx(Text, { color: fightMode ? "#f87171" : C.cyan, bold: true, children: i === 0 ? fightMode ? "\u2694 " : "\u203A " : " " }),
806
+ /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
807
+ i === cursorLine ? /* @__PURE__ */ jsxs(Fragment, { children: [
808
+ /* @__PURE__ */ jsx(Text, { children: line.slice(0, cursorCol) }),
809
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: cursorCol < line.length ? line[cursorCol] : " " }),
810
+ /* @__PURE__ */ jsx(Text, { children: cursorCol < line.length ? line.slice(cursorCol + 1) : "" })
811
+ ] }) : line,
812
+ i === lines.length - 1 && cursorPos === input.length && suggestionState.ghostHint !== "" && /* @__PURE__ */ jsx(Text, { color: C.muted, children: suggestionState.ghostHint })
813
+ ] })
814
+ ] }, i);
815
+ }),
816
+ hasMore && scrollTop + MAX_INPUT_LINES < lines.length && /* @__PURE__ */ jsx(Box, { width: Math.max(0, cols - 2), children: /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
817
+ " \u2193 ",
818
+ lines.length - scrollTop - MAX_INPUT_LINES,
819
+ " more line",
820
+ lines.length - scrollTop - MAX_INPUT_LINES > 1 ? "s" : ""
821
+ ] }) })
822
+ ] });
823
+ })() }),
824
+ suggestions.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: suggestions.map((s, i) => {
825
+ const selected = i === selectedIndex;
826
+ if (s.kind === "command") {
827
+ const paramHint = s.cmd.params ? " " + s.cmd.params.map((p) => `<${p.label}>`).join(" ") : "";
828
+ const display = s.cmd.name + paramHint;
829
+ return /* @__PURE__ */ jsxs(Box, { children: [
830
+ /* @__PURE__ */ jsx(Text, { color: selected ? C.cyan : C.muted, children: selected ? "\u203A " : " " }),
831
+ /* @__PURE__ */ jsxs(Text, { children: [
832
+ /* @__PURE__ */ jsx(Text, { color: selected ? C.cyan : C.secondary, bold: selected, children: s.cmd.name }),
833
+ /* @__PURE__ */ jsx(Text, { color: C.muted, children: paramHint.padEnd(CMD_DISPLAY_COL - display.length + paramHint.length) })
834
+ ] }),
835
+ /* @__PURE__ */ jsx(Text, { color: C.dim, children: s.cmd.description })
836
+ ] }, s.cmd.name);
837
+ }
838
+ return /* @__PURE__ */ jsxs(Box, { children: [
839
+ /* @__PURE__ */ jsx(Text, { color: selected ? C.cyan : C.muted, children: selected ? "\u203A " : " " }),
840
+ s.kind === "mention" && /* @__PURE__ */ jsx(Text, { color: C.dim, children: "@" }),
841
+ /* @__PURE__ */ jsx(Text, { color: selected ? C.cyan : C.secondary, bold: selected, children: s.value })
842
+ ] }, s.value);
843
+ }) })
844
+ ] });
845
+ }
846
+ function startTUI(opts) {
847
+ let handle = null;
848
+ const queue = [];
849
+ const onReady = (h) => {
850
+ handle = h;
851
+ for (const event of queue.splice(0)) h.push(event);
852
+ };
853
+ const { unmount } = render(
854
+ /* @__PURE__ */ jsx(
855
+ App,
856
+ {
857
+ roomName: opts.roomName,
858
+ onSend: opts.onSend,
859
+ onCtrlC: opts.onCtrlC,
860
+ onWatchAgent: opts.onWatchAgent,
861
+ onReady,
862
+ readOnly: opts.readOnly,
863
+ authority: opts.authority,
864
+ initialSound: opts.soundEnabled ?? true
865
+ }
866
+ ),
867
+ { exitOnCtrlC: false }
868
+ );
869
+ return {
870
+ push(event) {
871
+ if (handle) handle.push(event);
872
+ else queue.push(event);
873
+ },
874
+ clear() {
875
+ handle?.clear();
876
+ },
877
+ setAgentNames(names) {
878
+ handle?.setAgentNames(names);
879
+ },
880
+ setParticipants(names) {
881
+ handle?.setParticipants(names);
882
+ },
883
+ toggleSound() {
884
+ return handle?.toggleSound() ?? false;
885
+ },
886
+ setAgentState(name, state, opts2) {
887
+ handle?.setAgentState(name, state, opts2);
888
+ },
889
+ setAgentActivity(name, label) {
890
+ handle?.setAgentActivity(name, label);
891
+ },
892
+ setAgentMetrics(name, m) {
893
+ handle?.setAgentMetrics(name, m);
894
+ },
895
+ stop() {
896
+ unmount();
897
+ }
898
+ };
899
+ }
900
+
901
+ // src/cli/watch.ts
902
+ import { execFileSync, spawn } from "child_process";
903
+ import { existsSync } from "fs";
904
+ function resolveTmuxPath(env = process.env) {
905
+ try {
906
+ const found = execFileSync("which", ["tmux"], { encoding: "utf-8", env }).trim();
907
+ if (found) return found;
908
+ } catch {
909
+ }
910
+ for (const candidate of ["/opt/homebrew/bin/tmux", "/usr/local/bin/tmux", "/usr/bin/tmux"]) {
911
+ if (existsSync(candidate)) return candidate;
912
+ }
913
+ return null;
914
+ }
915
+ function osaQuote(value) {
916
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
917
+ }
918
+ var LAUNCHERS = [
919
+ {
920
+ kind: "iterm",
921
+ available: (env) => env.TERM_PROGRAM === "iTerm.app" || existsSync("/Applications/iTerm.app"),
922
+ argv: (command) => [
923
+ "osascript",
924
+ "-e",
925
+ `tell application "iTerm"
926
+ create window with default profile command "${osaQuote(command)}"
927
+ activate
928
+ end tell`
929
+ ]
930
+ },
931
+ {
932
+ kind: "ghostty",
933
+ available: (env) => env.TERM_PROGRAM === "ghostty" || existsSync("/Applications/Ghostty.app"),
934
+ argv: (command) => ["open", "-na", "Ghostty", "--args", "-e", command]
935
+ },
936
+ {
937
+ kind: "wezterm",
938
+ available: () => commandExists("wezterm"),
939
+ argv: (command) => ["wezterm", "start", "--", "sh", "-c", command]
940
+ },
941
+ {
942
+ kind: "kitty",
943
+ available: () => commandExists("kitty"),
944
+ argv: (command) => ["kitty", "sh", "-c", command]
945
+ },
946
+ {
947
+ kind: "alacritty",
948
+ available: () => commandExists("alacritty"),
949
+ argv: (command) => ["alacritty", "-e", "sh", "-c", command]
950
+ },
951
+ {
952
+ // Last resort: every mac has it, but it steals focus and ignores profiles.
953
+ kind: "apple-terminal",
954
+ available: () => existsSync("/System/Applications/Utilities/Terminal.app") || existsSync("/Applications/Utilities/Terminal.app"),
955
+ argv: (command) => [
956
+ "osascript",
957
+ "-e",
958
+ `tell application "Terminal"
959
+ do script "${osaQuote(command)}"
960
+ activate
961
+ end tell`
962
+ ]
963
+ }
964
+ ];
965
+ function commandExists(bin) {
966
+ try {
967
+ execFileSync("which", [bin], { stdio: "ignore" });
968
+ return true;
969
+ } catch {
970
+ return false;
971
+ }
972
+ }
973
+ function pickTerminal(env = process.env, launchers = LAUNCHERS) {
974
+ const current = launchers.find((l) => l.kind === terminalFromEnv(env));
975
+ if (current?.available(env)) return current;
976
+ return launchers.find((l) => l.available(env)) ?? null;
977
+ }
978
+ function terminalFromEnv(env = process.env) {
979
+ switch (env.TERM_PROGRAM) {
980
+ case "iTerm.app":
981
+ return "iterm";
982
+ case "Apple_Terminal":
983
+ return "apple-terminal";
984
+ case "ghostty":
985
+ return "ghostty";
986
+ case "WezTerm":
987
+ return "wezterm";
988
+ default:
989
+ break;
990
+ }
991
+ if (env.KITTY_WINDOW_ID) return "kitty";
992
+ if (env.ALACRITTY_WINDOW_ID) return "alacritty";
993
+ return null;
994
+ }
995
+ function watchCommand(tmuxPath, tmuxSession, opts) {
996
+ const flags = opts?.control ? ["attach"] : ["attach", "-r"];
997
+ return [tmuxPath, ...flags, "-t", tmuxSession].join(" ");
998
+ }
999
+ function openAgentWatcher(tmuxSession, opts) {
1000
+ const env = opts?.env ?? process.env;
1001
+ const tmuxPath = resolveTmuxPath(env);
1002
+ if (!tmuxPath) return { ok: false, reason: "tmux not found" };
1003
+ const launcher = pickTerminal(env);
1004
+ if (!launcher) return { ok: false, reason: "no supported terminal found" };
1005
+ const [bin, ...args] = launcher.argv(watchCommand(tmuxPath, tmuxSession, opts));
1006
+ try {
1007
+ const child = spawn(bin, args, { detached: true, stdio: "ignore" });
1008
+ child.unref();
1009
+ } catch {
1010
+ return { ok: false, reason: `could not launch ${launcher.kind}` };
1011
+ }
1012
+ return { ok: true, terminal: launcher.kind, readOnly: !opts?.control };
1013
+ }
1014
+
1015
+ // src/cli/join.ts
1016
+ function formatTimestamp(date) {
1017
+ const h = String(date.getHours()).padStart(2, "0");
1018
+ const m = String(date.getMinutes()).padStart(2, "0");
1019
+ const s = String(date.getSeconds()).padStart(2, "0");
1020
+ return `${h}:${m}:${s}`;
1021
+ }
1022
+ var HEADLESS_READY = "apiary: ready";
1023
+ function persistAgentModel(roomName, alias, model) {
1024
+ const session = listRoomSessions().find((s) => s.roomName === roomName);
1025
+ if (!session?.participants) return;
1026
+ const p = session.participants.find((x) => x.alias === alias);
1027
+ if (!p) return;
1028
+ p.model = model;
1029
+ saveRoomSession(session);
1030
+ }
1031
+ function sendSlashToAgent(session, slashCommand) {
1032
+ tmuxInjectText(session, slashCommand);
1033
+ tmuxSendEnter(session);
1034
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 80);
1035
+ tmuxSendEnter(session);
1036
+ }
1037
+ function findAgentTmuxSession(agentName) {
1038
+ const wanted = agentName.toLowerCase();
1039
+ for (const runtime of ["claude", "codex"]) {
1040
+ for (const s of listAgentSessions(runtime)) {
1041
+ if (s.agentName.toLowerCase() !== wanted) continue;
1042
+ if (!isAgentAlive(s)) return null;
1043
+ return s.tmuxSession ?? `apiary_${s.agentName}`;
1044
+ }
1045
+ }
1046
+ return null;
1047
+ }
1048
+ async function join(options) {
1049
+ const token = extractToken(options.server);
1050
+ let serverUrl;
1051
+ try {
1052
+ const parsed = new URL(options.server);
1053
+ parsed.search = "";
1054
+ serverUrl = parsed.toString().replace(/\/$/, "");
1055
+ } catch {
1056
+ serverUrl = options.server.replace(/\/$/, "");
1057
+ }
1058
+ const name = options.name ?? randomName();
1059
+ const isGuest = options.guest ?? false;
1060
+ let sessionToken;
1061
+ let participantId;
1062
+ let roomName;
1063
+ let authority;
1064
+ let participants;
1065
+ try {
1066
+ const joinBody = {};
1067
+ if (token) {
1068
+ joinBody.token = token;
1069
+ joinBody.type = "human";
1070
+ joinBody.name = name;
1071
+ } else if (isGuest) {
1072
+ joinBody.type = "guest";
1073
+ } else {
1074
+ joinBody.type = "human";
1075
+ joinBody.name = name;
1076
+ }
1077
+ const res = await fetch(`${serverUrl}/join`, {
1078
+ method: "POST",
1079
+ headers: { "Content-Type": "application/json" },
1080
+ body: JSON.stringify(joinBody)
1081
+ });
1082
+ if (!res.ok) {
1083
+ const err = await res.text();
1084
+ console.error(`Failed to join: ${err}`);
1085
+ process.exit(1);
1086
+ }
1087
+ const data = await res.json();
1088
+ sessionToken = String(data.sessionToken ?? "");
1089
+ participantId = String(data.participantId);
1090
+ roomName = String(data.roomName);
1091
+ authority = data.authority ?? "member";
1092
+ participants = data.participants ?? [];
1093
+ } catch (err) {
1094
+ console.error(`Cannot reach apiary server at ${serverUrl}. Is it running?`);
1095
+ console.error(` Error: ${err instanceof Error ? err.message : err}`);
1096
+ process.exit(1);
1097
+ }
1098
+ let disconnected = false;
1099
+ let cleanupStream = null;
1100
+ const disconnect = async () => {
1101
+ if (disconnected) return;
1102
+ disconnected = true;
1103
+ cleanupStream?.();
1104
+ try {
1105
+ await fetch(`${serverUrl}/disconnect`, {
1106
+ method: "POST",
1107
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
1108
+ body: JSON.stringify({})
1109
+ });
1110
+ } catch {
1111
+ }
1112
+ };
1113
+ if (options.headless) {
1114
+ const sseController = new AbortController();
1115
+ const cleanup = async () => {
1116
+ sseController.abort();
1117
+ await disconnect();
1118
+ };
1119
+ process.on("SIGINT", async () => {
1120
+ await cleanup();
1121
+ process.exit(0);
1122
+ });
1123
+ process.on("SIGTERM", async () => {
1124
+ await cleanup();
1125
+ process.exit(0);
1126
+ });
1127
+ const rl = createInterface({ input: process.stdin, terminal: false });
1128
+ rl.on("line", async (line) => {
1129
+ const content = line.trim();
1130
+ if (!content || authority === "guest") return;
1131
+ try {
1132
+ await fetch(`${serverUrl}/message`, {
1133
+ method: "POST",
1134
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
1135
+ body: JSON.stringify({ content })
1136
+ });
1137
+ } catch {
1138
+ }
1139
+ });
1140
+ try {
1141
+ const res = await fetch(`${serverUrl}/events`, {
1142
+ method: "POST",
1143
+ headers: { Accept: "text/event-stream", Authorization: `Bearer ${sessionToken}` },
1144
+ signal: sseController.signal
1145
+ });
1146
+ if (!res.ok || !res.body) {
1147
+ process.stderr.write("Failed to connect event stream\n");
1148
+ await cleanup();
1149
+ process.exit(1);
1150
+ }
1151
+ process.stderr.write(`${HEADLESS_READY} ${roomName}
1152
+ `);
1153
+ const reader = res.body.getReader();
1154
+ const decoder = new TextDecoder();
1155
+ let buf = "";
1156
+ while (true) {
1157
+ const { done, value } = await reader.read();
1158
+ if (done) break;
1159
+ buf += decoder.decode(value, { stream: true });
1160
+ const parts = buf.split("\n\n");
1161
+ buf = parts.pop();
1162
+ for (const part of parts) {
1163
+ const dataLine = part.split("\n").find((l) => l.startsWith("data: "));
1164
+ if (!dataLine) continue;
1165
+ try {
1166
+ const event = JSON.parse(dataLine.slice(6));
1167
+ process.stdout.write(JSON.stringify(event) + "\n");
1168
+ } catch {
1169
+ }
1170
+ }
1171
+ }
1172
+ } catch {
1173
+ }
1174
+ if (!disconnected) {
1175
+ await cleanup();
1176
+ }
1177
+ process.exit(0);
1178
+ }
1179
+ const isReadOnly = authority === "guest" || isGuest;
1180
+ function systemEvent(content) {
1181
+ tui.push({
1182
+ id: randomUUID(),
1183
+ ts: formatTimestamp(/* @__PURE__ */ new Date()),
1184
+ kind: "system",
1185
+ content
1186
+ });
1187
+ }
1188
+ async function resolveParticipant(name2) {
1189
+ try {
1190
+ const res = await fetch(`${serverUrl}/participants`, { headers: { Authorization: `Bearer ${sessionToken}` } });
1191
+ if (!res.ok) {
1192
+ systemEvent("Failed to get participant list.");
1193
+ return null;
1194
+ }
1195
+ const data = await res.json();
1196
+ const target = data.participants.find((p) => p.name.toLowerCase() === name2.toLowerCase());
1197
+ if (!target) {
1198
+ systemEvent(`Participant "${name2}" not found.`);
1199
+ return null;
1200
+ }
1201
+ return target;
1202
+ } catch {
1203
+ systemEvent("Failed to reach server.");
1204
+ return null;
1205
+ }
1206
+ }
1207
+ async function participantAction(opts) {
1208
+ try {
1209
+ const res = await fetch(`${serverUrl}${opts.path}`, {
1210
+ method: "POST",
1211
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
1212
+ body: JSON.stringify(opts.body)
1213
+ });
1214
+ if (!res.ok) {
1215
+ systemEvent(`Failed to ${opts.failVerb}: ${await res.text()}`);
1216
+ return;
1217
+ }
1218
+ systemEvent(opts.success);
1219
+ } catch {
1220
+ systemEvent("Failed to reach server.");
1221
+ }
1222
+ }
1223
+ async function handleSlashCommand(input) {
1224
+ const parts = input.slice(1).split(/\s+/);
1225
+ const cmd = parts[0]?.toLowerCase();
1226
+ const args = parts.slice(1);
1227
+ switch (cmd) {
1228
+ // ── /help ─────────────────────────────────────────────────────
1229
+ case "help": {
1230
+ const lines = [
1231
+ "/who list participants",
1232
+ "/ping <name> ping for a status check",
1233
+ "/peek <name> show how to view an agent's tmux session",
1234
+ "/share generate share links",
1235
+ "/sound toggle notification sounds",
1236
+ "/leave disconnect"
1237
+ ];
1238
+ if (authority === "product_owner") {
1239
+ lines.push(
1240
+ "",
1241
+ "/mute <name> demote to guest",
1242
+ "/unmute <name> restore to member",
1243
+ "/setmode <n> <m> set engagement mode"
1244
+ );
1245
+ }
1246
+ if (authority === "admin") {
1247
+ lines.push(
1248
+ "",
1249
+ "/promote <name> promote to product owner",
1250
+ "/demote <name> demote to member",
1251
+ "/kick <name> remove a participant",
1252
+ "/mute <name> demote to guest",
1253
+ "/unmute <name> restore to member",
1254
+ "/setmode <n> <m> set engagement mode",
1255
+ "/clear wipe room history",
1256
+ "/clear <name> clear an agent's claude context",
1257
+ "/model <n> <id> swap an agent's model (sonnet/opus/haiku)",
1258
+ "/tunnel start cloudflared tunnel"
1259
+ );
1260
+ }
1261
+ systemEvent(lines.join("\n"));
1262
+ return;
1263
+ }
1264
+ // ── /watch ────────────────────────────────────────────────────
1265
+ // Opens the agent's tmux session in a NEW terminal window. The room TUI
1266
+ // is untouched and stays usable — this attaches a second client rather
1267
+ // than moving the agent's pane anywhere.
1268
+ case "watch": {
1269
+ const target = args[0];
1270
+ if (!target) {
1271
+ systemEvent("Usage: /watch <agent> [--control] (read-only unless --control)");
1272
+ return;
1273
+ }
1274
+ const control = args.includes("--control");
1275
+ const session = findAgentTmuxSession(target);
1276
+ if (!session) {
1277
+ systemEvent(
1278
+ `No local tmux session for "${target}". /watch only works for agents started on this machine by apiary claude/codex.`
1279
+ );
1280
+ return;
1281
+ }
1282
+ const res = openAgentWatcher(session, { control });
1283
+ systemEvent(
1284
+ res.ok ? `Opened ${target} in a new ${res.terminal} window (${res.readOnly ? "read-only" : "INTERACTIVE \u2014 your keystrokes go to the agent"}).` : `Could not open a window for ${target}: ${res.reason}`
1285
+ );
1286
+ return;
1287
+ }
1288
+ // ── /who ──────────────────────────────────────────────────────
1289
+ case "who": {
1290
+ try {
1291
+ const res = await fetch(`${serverUrl}/participants`, { headers: { Authorization: `Bearer ${sessionToken}` } });
1292
+ if (!res.ok) {
1293
+ systemEvent("Failed to get participant list.");
1294
+ return;
1295
+ }
1296
+ const data = await res.json();
1297
+ const lines = data.participants.map((p) => {
1298
+ const auth = p.authority ?? "member";
1299
+ const label = auth === "product_owner" ? "owner" : auth;
1300
+ return ` ${p.type === "agent" ? "agent" : "human"} ${p.name} (${label})`;
1301
+ });
1302
+ systemEvent(`Participants:
1303
+ ${lines.join("\n")}`);
1304
+ } catch {
1305
+ systemEvent("Failed to reach server.");
1306
+ }
1307
+ return;
1308
+ }
1309
+ // ── /peek ─────────────────────────────────────────────────────
1310
+ // Capture a snapshot of the agent's tmux pane and render it inline.
1311
+ // For sustained interactive viewing the user can still `tmux attach`
1312
+ // (hint printed below), but the common case — "what is this agent
1313
+ // showing right now?" — is answered with a single keystroke.
1314
+ case "peek": {
1315
+ const name2 = args[0];
1316
+ if (!name2) {
1317
+ systemEvent("Usage: /peek <name>");
1318
+ return;
1319
+ }
1320
+ const session = `apiary_${name2}`;
1321
+ if (!tmuxSessionExists(session)) {
1322
+ systemEvent(
1323
+ `No local tmux session for "${name2}" (apiary_${name2} not found).
1324
+ This is normal for agents on another host \u2014 peek only works for local same-host agents.`
1325
+ );
1326
+ return;
1327
+ }
1328
+ const lines = tmuxCapturePane(session);
1329
+ let lastIdx = lines.length - 1;
1330
+ while (lastIdx >= 0 && lines[lastIdx].trim() === "") lastIdx--;
1331
+ const tail = lines.slice(Math.max(0, lastIdx - 24), lastIdx + 1);
1332
+ if (tail.length === 0) {
1333
+ systemEvent(`Peek ${name2}: pane is empty.`);
1334
+ return;
1335
+ }
1336
+ systemEvent(
1337
+ `Peek ${name2} (last ${tail.length} lines):
1338
+ ` + tail.map((l) => "\u2502 " + l).join("\n") + `
1339
+ For interactive view: detach apiary (Ctrl+C \xD72) then tmux attach -t ${session} (Ctrl+B D to leave the agent).`
1340
+ );
1341
+ return;
1342
+ }
1343
+ // ── /rules ────────────────────────────────────────────────────
1344
+ // Read-only display of the room's current rules. Any participant
1345
+ // (including guests) can view.
1346
+ case "rules": {
1347
+ try {
1348
+ const res = await fetch(`${serverUrl}/rules`, { headers: { Authorization: `Bearer ${sessionToken}` } });
1349
+ if (!res.ok) {
1350
+ systemEvent("Failed to fetch rules.");
1351
+ return;
1352
+ }
1353
+ const data = await res.json();
1354
+ if (data.rules.length === 0) {
1355
+ systemEvent("No rules set for this room.");
1356
+ return;
1357
+ }
1358
+ const lines = ["Room rules:"];
1359
+ data.rules.forEach((r, i) => lines.push(` ${i + 1}. ${r}`));
1360
+ systemEvent(lines.join("\n"));
1361
+ } catch {
1362
+ systemEvent("Failed to reach server.");
1363
+ }
1364
+ return;
1365
+ }
1366
+ // ── /leave ────────────────────────────────────────────────────
1367
+ case "leave": {
1368
+ await disconnect();
1369
+ tui.stop();
1370
+ process.exit(0);
1371
+ return;
1372
+ }
1373
+ // ── /kick <name> (admin only) ─────────────────────────────────
1374
+ case "kick": {
1375
+ if (!can(authority, "kick")) {
1376
+ systemEvent("Only admins can kick.");
1377
+ return;
1378
+ }
1379
+ const targetName = args[0];
1380
+ if (!targetName) {
1381
+ systemEvent("Usage: /kick <name>");
1382
+ return;
1383
+ }
1384
+ const target = await resolveParticipant(targetName);
1385
+ if (!target) return;
1386
+ await participantAction({
1387
+ path: "/kick",
1388
+ body: { participantId: target.id },
1389
+ failVerb: "kick",
1390
+ success: `Kicked ${targetName}.`
1391
+ });
1392
+ return;
1393
+ }
1394
+ // ── /mute <name> (admin or product_owner) — demote to guest ─────
1395
+ case "mute": {
1396
+ if (!can(authority, "mute")) {
1397
+ systemEvent("Only admins and product owners can mute.");
1398
+ return;
1399
+ }
1400
+ const targetName = args[0];
1401
+ if (!targetName) {
1402
+ systemEvent("Usage: /mute <name>");
1403
+ return;
1404
+ }
1405
+ const target = await resolveParticipant(targetName);
1406
+ if (!target) return;
1407
+ await participantAction({
1408
+ path: "/set-authority",
1409
+ body: { participantId: target.id, authority: "guest" },
1410
+ failVerb: "mute",
1411
+ success: `Muted ${targetName} (guest).`
1412
+ });
1413
+ return;
1414
+ }
1415
+ // ── /unmute <name> (admin or product_owner) — restore to member ──
1416
+ case "unmute": {
1417
+ if (!can(authority, "unmute")) {
1418
+ systemEvent("Only admins and product owners can unmute.");
1419
+ return;
1420
+ }
1421
+ const targetName = args[0];
1422
+ if (!targetName) {
1423
+ systemEvent("Usage: /unmute <name>");
1424
+ return;
1425
+ }
1426
+ const target = await resolveParticipant(targetName);
1427
+ if (!target) return;
1428
+ await participantAction({
1429
+ path: "/set-authority",
1430
+ body: { participantId: target.id, authority: "member" },
1431
+ failVerb: "unmute",
1432
+ success: `Unmuted ${targetName} (member).`
1433
+ });
1434
+ return;
1435
+ }
1436
+ // ── /setmode <name> <mode> (admin or product_owner) ──────────
1437
+ case "setmode": {
1438
+ if (!can(authority, "set_mode_for")) {
1439
+ systemEvent("Only admins and product owners can set modes.");
1440
+ return;
1441
+ }
1442
+ const targetName = args[0];
1443
+ const mode = args[1];
1444
+ if (!targetName || !mode) {
1445
+ systemEvent("Usage: /setmode <name> <mode>");
1446
+ return;
1447
+ }
1448
+ const target = await resolveParticipant(targetName);
1449
+ if (!target) return;
1450
+ await participantAction({
1451
+ path: "/set-mode",
1452
+ body: { participantId: target.id, mode },
1453
+ failVerb: "set mode",
1454
+ success: `Set ${targetName} to ${mode}.`
1455
+ });
1456
+ return;
1457
+ }
1458
+ // ── /promote <name> (admin only) — elevate to product_owner ──
1459
+ case "promote": {
1460
+ if (!can(authority, "promote")) {
1461
+ systemEvent("Only admins can promote.");
1462
+ return;
1463
+ }
1464
+ const targetName = args[0];
1465
+ if (!targetName) {
1466
+ systemEvent("Usage: /promote <name>");
1467
+ return;
1468
+ }
1469
+ const target = await resolveParticipant(targetName);
1470
+ if (!target) return;
1471
+ await participantAction({
1472
+ path: "/set-authority",
1473
+ body: { participantId: target.id, authority: "product_owner" },
1474
+ failVerb: "promote",
1475
+ success: `${targetName} is now a product owner.`
1476
+ });
1477
+ return;
1478
+ }
1479
+ // ── /demote <name> (admin only) — restore product_owner to member ──
1480
+ case "demote": {
1481
+ if (!can(authority, "demote")) {
1482
+ systemEvent("Only admins can demote.");
1483
+ return;
1484
+ }
1485
+ const targetName = args[0];
1486
+ if (!targetName) {
1487
+ systemEvent("Usage: /demote <name>");
1488
+ return;
1489
+ }
1490
+ const target = await resolveParticipant(targetName);
1491
+ if (!target) return;
1492
+ await participantAction({
1493
+ path: "/set-authority",
1494
+ body: { participantId: target.id, authority: "member" },
1495
+ failVerb: "demote",
1496
+ success: `${targetName} is now a member.`
1497
+ });
1498
+ return;
1499
+ }
1500
+ // ── /ping <name> ─────────────────────────────────────────────
1501
+ case "ping": {
1502
+ const targetName = args[0];
1503
+ if (!targetName) {
1504
+ systemEvent("Usage: /ping <name>");
1505
+ return;
1506
+ }
1507
+ const target = await resolveParticipant(targetName);
1508
+ if (!target) return;
1509
+ await participantAction({
1510
+ path: "/ping",
1511
+ body: { participantId: target.id },
1512
+ failVerb: "ping",
1513
+ success: `Pinged ${targetName}.`
1514
+ });
1515
+ return;
1516
+ }
1517
+ // ── /share [--as <tier>] ──────────────────────────────────────
1518
+ case "share": {
1519
+ if (authority === "guest") {
1520
+ systemEvent("Guests cannot create share links.");
1521
+ return;
1522
+ }
1523
+ let targetAuthority;
1524
+ if (args[0] === "--as" && args[1]) {
1525
+ targetAuthority = args[1];
1526
+ }
1527
+ try {
1528
+ const shareBody = {};
1529
+ if (targetAuthority) shareBody.authority = targetAuthority;
1530
+ const res = await fetch(`${serverUrl}/share`, {
1531
+ method: "POST",
1532
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
1533
+ body: JSON.stringify(shareBody)
1534
+ });
1535
+ if (!res.ok) {
1536
+ systemEvent(`Failed: ${await res.text()}`);
1537
+ return;
1538
+ }
1539
+ const data = await res.json();
1540
+ const lines = Object.entries(data.links).map(
1541
+ ([tier, url]) => ` ${tier}: apiary join ${url}`
1542
+ );
1543
+ systemEvent(`Share links:
1544
+ ${lines.join("\n")}`);
1545
+ } catch {
1546
+ systemEvent("Failed to reach server.");
1547
+ }
1548
+ return;
1549
+ }
1550
+ // ── /sound — toggle notification sounds ───────────────────────
1551
+ case "sound": {
1552
+ const enabled = tui.toggleSound();
1553
+ const cfg = loadConfig();
1554
+ cfg.sound = enabled;
1555
+ saveConfig(cfg);
1556
+ systemEvent(`Sound ${enabled ? "on" : "off"} (saved for future rooms).`);
1557
+ return;
1558
+ }
1559
+ // ── /clear — wipe room history OR a specific agent's claude context
1560
+ // /clear → admin: wipe room history (storage + all clients)
1561
+ // /clear <agent-name> → admin: inject /clear into the agent's tmux pane,
1562
+ // clearing claude's local conversation. Frees
1563
+ // token budget mid-session without restart.
1564
+ case "clear": {
1565
+ if (!can(authority, "clear_history")) {
1566
+ systemEvent("Only admins can clear.");
1567
+ return;
1568
+ }
1569
+ const targetName = args[0];
1570
+ if (targetName) {
1571
+ const session = `apiary_${targetName}`;
1572
+ if (!tmuxSessionExists(session)) {
1573
+ systemEvent(
1574
+ `No local tmux session for "${targetName}". /clear <name> only works for same-host agents.`
1575
+ );
1576
+ return;
1577
+ }
1578
+ try {
1579
+ sendSlashToAgent(session, "/clear");
1580
+ systemEvent(`Sent /clear to ${targetName}.`);
1581
+ } catch (err) {
1582
+ systemEvent(`Failed to send /clear: ${err.message}`);
1583
+ }
1584
+ return;
1585
+ }
1586
+ try {
1587
+ const res = await fetch(`${serverUrl}/clear`, {
1588
+ method: "POST",
1589
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` }
1590
+ });
1591
+ if (!res.ok) {
1592
+ systemEvent(`Failed to clear: ${await res.text()}`);
1593
+ return;
1594
+ }
1595
+ } catch {
1596
+ systemEvent("Failed to reach server.");
1597
+ }
1598
+ return;
1599
+ }
1600
+ // ── /model — swap an agent's claude model mid-session ───────────
1601
+ // /model <agent-name> <model-id> e.g. /model bf sonnet
1602
+ // Injects claude's built-in /model command into the agent's tmux. No
1603
+ // restart — conversation continues at the new model.
1604
+ case "model": {
1605
+ if (!can(authority, "clear_history")) {
1606
+ systemEvent("Only admins can swap models.");
1607
+ return;
1608
+ }
1609
+ const targetName = args[0];
1610
+ const modelId = args[1];
1611
+ if (!targetName || !modelId) {
1612
+ systemEvent("Usage: /model <agent> <model-id>");
1613
+ return;
1614
+ }
1615
+ const session = `apiary_${targetName}`;
1616
+ if (!tmuxSessionExists(session)) {
1617
+ systemEvent(`No local tmux session for "${targetName}".`);
1618
+ return;
1619
+ }
1620
+ try {
1621
+ sendSlashToAgent(session, `/model ${modelId}`);
1622
+ persistAgentModel(roomName, targetName, modelId);
1623
+ systemEvent(`Sent /model ${modelId} to ${targetName} (will persist on resume).`);
1624
+ } catch (err) {
1625
+ systemEvent(`Failed to swap model: ${err.message}`);
1626
+ }
1627
+ return;
1628
+ }
1629
+ // ── /tunnel — start cloudflared tunnel mid-session ────────────
1630
+ case "tunnel": {
1631
+ if (!can(authority, "start_tunnel")) {
1632
+ systemEvent("Only admins can start tunnels.");
1633
+ return;
1634
+ }
1635
+ try {
1636
+ const res = await fetch(`${serverUrl}/tunnel`, {
1637
+ method: "POST",
1638
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` }
1639
+ });
1640
+ if (!res.ok) {
1641
+ systemEvent(`Failed: ${(await res.json()).error}`);
1642
+ return;
1643
+ }
1644
+ const data = await res.json();
1645
+ if (data.alreadyRunning) {
1646
+ systemEvent(`Tunnel already running: ${data.url}`);
1647
+ } else {
1648
+ systemEvent(`Tunnel started: ${data.url}
1649
+ Use /share to generate share links.`);
1650
+ }
1651
+ } catch {
1652
+ systemEvent("Failed to reach server.");
1653
+ }
1654
+ return;
1655
+ }
1656
+ default:
1657
+ systemEvent(`Unknown command: /${cmd}`);
1658
+ }
1659
+ }
1660
+ process.stdout.write(`\x1B]0;${roomEmoji(`${roomName}\xB7${name}`)} ${roomName} \xB7 ${name}\x07`);
1661
+ if (options.shareUrl) {
1662
+ console.log();
1663
+ console.log(` \x1B[2mShare:\x1B[0m \x1B[33m${options.shareUrl}\x1B[0m`);
1664
+ console.log();
1665
+ console.log(` \x1B[2mJoin:\x1B[0m \x1B[36mapiary join\x1B[0m \x1B[2m<url>\x1B[0m`);
1666
+ console.log(` \x1B[2mAgent:\x1B[0m \x1B[36mapiary claude\x1B[0m \x1B[2m<name> \u2192 tell it the URL, it joins\x1B[0m`);
1667
+ console.log();
1668
+ }
1669
+ const config = loadConfig();
1670
+ const tui = startTUI({
1671
+ roomName,
1672
+ readOnly: isReadOnly,
1673
+ authority,
1674
+ soundEnabled: config.sound ?? true,
1675
+ onSend: isReadOnly ? void 0 : async (content) => {
1676
+ if (content.startsWith("/")) {
1677
+ await handleSlashCommand(content);
1678
+ return;
1679
+ }
1680
+ try {
1681
+ await fetch(`${serverUrl}/message`, {
1682
+ method: "POST",
1683
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
1684
+ body: JSON.stringify({ content })
1685
+ });
1686
+ } catch {
1687
+ }
1688
+ },
1689
+ onWatchAgent: (agentName) => {
1690
+ const session = findAgentTmuxSession(agentName);
1691
+ if (!session) {
1692
+ systemEvent(
1693
+ `No local tmux session for "${agentName}". Watching only works for agents started on this machine by apiary claude/codex.`
1694
+ );
1695
+ return;
1696
+ }
1697
+ const res = openAgentWatcher(session);
1698
+ systemEvent(
1699
+ res.ok ? `Opened ${agentName} in a new ${res.terminal} window (read-only). Use "/watch ${agentName} --control" to type into it.` : `Could not open a window for ${agentName}: ${res.reason}`
1700
+ );
1701
+ },
1702
+ onCtrlC: /* @__PURE__ */ (() => {
1703
+ let lastPress = 0;
1704
+ const CONFIRM_MS = 2e3;
1705
+ return async () => {
1706
+ const now = Date.now();
1707
+ if (now - lastPress < CONFIRM_MS) {
1708
+ await disconnect();
1709
+ tui.stop();
1710
+ process.exit(0);
1711
+ }
1712
+ lastPress = now;
1713
+ systemEvent("Press Ctrl+C again within 2s to leave (server keeps running).");
1714
+ };
1715
+ })()
1716
+ });
1717
+ if (!isReadOnly) {
1718
+ const hint = authority === "admin" ? "/help for commands (admin)" : "/help for commands";
1719
+ systemEvent(hint);
1720
+ }
1721
+ const agentNames = participants.filter((p) => p.type === "agent").map((p) => p.name);
1722
+ const pendingAgentSet = new Set(
1723
+ (options.expectedAgents ?? []).filter((n) => !agentNames.includes(n))
1724
+ );
1725
+ const renderAgentList = (joined) => {
1726
+ const merged = /* @__PURE__ */ new Set([...joined, ...pendingAgentSet]);
1727
+ tui.setAgentNames([...merged]);
1728
+ };
1729
+ if (agentNames.length + pendingAgentSet.size > 0) {
1730
+ renderAgentList(agentNames);
1731
+ for (const n of pendingAgentSet) tui.setAgentState(n, "pending");
1732
+ }
1733
+ if (agentNames.length > 0) {
1734
+ const INIT_FRESHNESS_MS = 5 * 6e4;
1735
+ try {
1736
+ const msgRes = await fetch(`${serverUrl}/messages?count=20`, {
1737
+ headers: { Authorization: `Bearer ${sessionToken}` }
1738
+ });
1739
+ if (msgRes.ok) {
1740
+ const { items: messages } = await msgRes.json();
1741
+ const agentSet = new Set(agentNames);
1742
+ const idleAgents = /* @__PURE__ */ new Set();
1743
+ let humanMsg = null;
1744
+ for (const msg of messages) {
1745
+ const sType = participants.find((p) => p.id === msg.sender_id)?.type;
1746
+ if (sType === "agent" && agentSet.has(msg.sender_name)) {
1747
+ idleAgents.add(msg.sender_name);
1748
+ } else if (sType === "human") {
1749
+ humanMsg = msg;
1750
+ break;
1751
+ }
1752
+ }
1753
+ const fresh = humanMsg?.timestamp ? Date.now() - new Date(humanMsg.timestamp).getTime() < INIT_FRESHNESS_MS : false;
1754
+ if (humanMsg && fresh) {
1755
+ const targetedAgents = humanMsg.recipients && humanMsg.recipients.length > 0 ? new Set(
1756
+ participants.filter((p) => p.type === "agent" && humanMsg.recipients.includes(p.id)).map((p) => p.name)
1757
+ ) : agentSet;
1758
+ for (const agent of agentSet) {
1759
+ if (idleAgents.has(agent)) {
1760
+ tui.setAgentState(agent, "idle");
1761
+ } else if (targetedAgents.has(agent)) {
1762
+ tui.setAgentState(agent, "working");
1763
+ }
1764
+ }
1765
+ }
1766
+ }
1767
+ } catch {
1768
+ }
1769
+ }
1770
+ const participantNames = new Set(
1771
+ participants.filter((p) => p.id !== participantId).map((p) => p.name)
1772
+ );
1773
+ tui.setParticipants([...participantNames]);
1774
+ {
1775
+ const participantTypes = /* @__PURE__ */ new Map();
1776
+ const participantNameById = /* @__PURE__ */ new Map();
1777
+ for (const p of participants) {
1778
+ participantTypes.set(p.id, p.type);
1779
+ participantNameById.set(p.id, p.name);
1780
+ }
1781
+ const currentAgents = new Set(agentNames);
1782
+ const PING_DECAY_MS = 1e4;
1783
+ let sseController = null;
1784
+ cleanupStream = () => {
1785
+ if (sseController) {
1786
+ sseController.abort();
1787
+ sseController = null;
1788
+ }
1789
+ };
1790
+ const connectSSE = async () => {
1791
+ sseController = new AbortController();
1792
+ try {
1793
+ const res = await fetch(`${serverUrl}/events`, {
1794
+ method: "POST",
1795
+ headers: {
1796
+ Accept: "text/event-stream",
1797
+ Authorization: `Bearer ${sessionToken}`
1798
+ },
1799
+ signal: sseController.signal
1800
+ });
1801
+ if (!res.ok || !res.body) {
1802
+ console.error("Failed to connect event stream");
1803
+ await disconnect();
1804
+ process.exit(1);
1805
+ }
1806
+ const reader = res.body.getReader();
1807
+ const decoder = new TextDecoder();
1808
+ let buffer = "";
1809
+ while (true) {
1810
+ const { done, value } = await reader.read();
1811
+ if (done) break;
1812
+ buffer += decoder.decode(value, { stream: true });
1813
+ const parts = buffer.split("\n\n");
1814
+ buffer = parts.pop();
1815
+ for (const part of parts) {
1816
+ const dataLine = part.split("\n").find((l) => l.startsWith("data: "));
1817
+ if (!dataLine) continue;
1818
+ try {
1819
+ const event = JSON.parse(dataLine.slice(6));
1820
+ if (event.type === "RoomCleared") {
1821
+ tui.clear();
1822
+ systemEvent(`Room cleared by ${event.cleared_by ?? "admin"}.`);
1823
+ continue;
1824
+ }
1825
+ if (event.type === "ParticipantJoined") {
1826
+ participantTypes.set(event.participant.id, event.participant.type);
1827
+ }
1828
+ const displayEvent = toDisplayEvent(event, participantId, participantTypes);
1829
+ if (displayEvent) {
1830
+ tui.push(displayEvent);
1831
+ }
1832
+ if (event.type === "ParticipantJoined") {
1833
+ participantNameById.set(event.participant.id, event.participant.name);
1834
+ if (event.participant.type === "agent") {
1835
+ currentAgents.add(event.participant.name);
1836
+ pendingAgentSet.delete(event.participant.name);
1837
+ renderAgentList(currentAgents);
1838
+ tui.setAgentState(event.participant.name, "idle");
1839
+ }
1840
+ if (event.participant.id !== participantId) {
1841
+ participantNames.add(event.participant.name);
1842
+ tui.setParticipants([...participantNames]);
1843
+ }
1844
+ }
1845
+ if (event.type === "MessageSent") {
1846
+ const senderType = participantTypes.get(event.message.sender_id);
1847
+ if (senderType === "agent" && currentAgents.has(event.message.sender_name)) {
1848
+ tui.setAgentState(event.message.sender_name, "idle");
1849
+ } else if (senderType === "human") {
1850
+ const recipients = event.message.recipients ?? [];
1851
+ if (recipients.length > 0) {
1852
+ for (const recipientId of recipients) {
1853
+ const name2 = participantNameById.get(recipientId);
1854
+ if (name2 && currentAgents.has(name2)) {
1855
+ tui.setAgentState(name2, "working");
1856
+ }
1857
+ }
1858
+ } else {
1859
+ for (const agent of currentAgents) tui.setAgentState(agent, "working");
1860
+ }
1861
+ }
1862
+ }
1863
+ if (event.type === "Pinged") {
1864
+ const pinged = event;
1865
+ if (pinged.participant_id) {
1866
+ const name2 = participantNameById.get(pinged.participant_id);
1867
+ if (name2 && currentAgents.has(name2)) {
1868
+ tui.setAgentState(name2, "working", { decayMs: PING_DECAY_MS });
1869
+ }
1870
+ }
1871
+ }
1872
+ if (event.type === "Activity" && event.action === "status_label") {
1873
+ const detail = event.detail ?? {};
1874
+ const name2 = typeof detail.participant_name === "string" ? detail.participant_name : void 0;
1875
+ const label = typeof detail.label === "string" ? detail.label : null;
1876
+ const observed = detail.state === "idle" || detail.state === "working" || detail.state === "blocked" ? detail.state : void 0;
1877
+ if (name2 && currentAgents.has(name2)) {
1878
+ tui.setAgentActivity(name2, label);
1879
+ if (observed) tui.setAgentState(name2, observed, { observed: true });
1880
+ }
1881
+ }
1882
+ if (event.type === "Activity" && event.action === "metrics") {
1883
+ const detail = event.detail ?? {};
1884
+ const name2 = typeof detail.participant_name === "string" ? detail.participant_name : void 0;
1885
+ if (name2 && currentAgents.has(name2)) {
1886
+ tui.setAgentMetrics(name2, {
1887
+ ctxTokens: Number(detail.last_ctx_tokens) || 0,
1888
+ model: typeof detail.model === "string" ? detail.model : void 0
1889
+ });
1890
+ }
1891
+ }
1892
+ if (event.type === "StatusChanged") {
1893
+ const sc = event;
1894
+ if (currentAgents.has(sc.name)) {
1895
+ if (sc.status === "unresponsive") {
1896
+ tui.setAgentState(sc.name, "unresponsive");
1897
+ } else if (sc.status === "online" && sc.previous_status === "unresponsive") {
1898
+ tui.setAgentState(sc.name, "idle");
1899
+ }
1900
+ }
1901
+ }
1902
+ if (event.type === "ParticipantLeft" || event.type === "ParticipantKicked") {
1903
+ if (event.participant.type === "agent") {
1904
+ currentAgents.delete(event.participant.name);
1905
+ renderAgentList(currentAgents);
1906
+ }
1907
+ participantTypes.delete(event.participant.id);
1908
+ participantNameById.delete(event.participant.id);
1909
+ participantNames.delete(event.participant.name);
1910
+ tui.setParticipants([...participantNames]);
1911
+ }
1912
+ } catch {
1913
+ }
1914
+ }
1915
+ }
1916
+ if (!disconnected) {
1917
+ tui.stop();
1918
+ console.log("\nServer disconnected.");
1919
+ process.exit(0);
1920
+ }
1921
+ } catch {
1922
+ if (!disconnected) {
1923
+ tui.stop();
1924
+ console.log("\nServer disconnected.");
1925
+ process.exit(0);
1926
+ }
1927
+ }
1928
+ };
1929
+ connectSSE();
1930
+ }
1931
+ process.on("SIGINT", async () => {
1932
+ await disconnect();
1933
+ tui.stop();
1934
+ process.exit(0);
1935
+ });
1936
+ process.on("SIGTERM", async () => {
1937
+ await disconnect();
1938
+ tui.stop();
1939
+ process.exit(0);
1940
+ });
1941
+ }
1942
+ function toDisplayEvent(event, selfId, participantTypes) {
1943
+ const ts = formatTimestamp(new Date(event.timestamp));
1944
+ switch (event.type) {
1945
+ case "MessageSent": {
1946
+ const msg = event.message;
1947
+ const senderType = participantTypes.get(msg.sender_id) ?? "human";
1948
+ return {
1949
+ id: msg.id,
1950
+ ts,
1951
+ kind: "message",
1952
+ senderName: msg.sender_name,
1953
+ senderType,
1954
+ isSelf: msg.sender_id === selfId,
1955
+ content: msg.content,
1956
+ replyToName: event._replyToName ?? void 0
1957
+ };
1958
+ }
1959
+ case "ParticipantJoined":
1960
+ return {
1961
+ id: randomUUID(),
1962
+ ts,
1963
+ kind: "join",
1964
+ name: event.participant.name,
1965
+ participantType: event.participant.type
1966
+ };
1967
+ case "ParticipantLeft":
1968
+ return {
1969
+ id: randomUUID(),
1970
+ ts,
1971
+ kind: "leave",
1972
+ name: event.participant.name,
1973
+ participantType: event.participant.type
1974
+ };
1975
+ case "ParticipantKicked":
1976
+ return {
1977
+ id: randomUUID(),
1978
+ ts,
1979
+ kind: "system",
1980
+ content: `${event.participant.name} was kicked`
1981
+ };
1982
+ case "AuthorityChanged": {
1983
+ const name = event.participant.name;
1984
+ if (event.new_authority === "guest") {
1985
+ return { id: randomUUID(), ts, kind: "system", content: `${name} was muted` };
1986
+ }
1987
+ if (event.new_authority === "member") {
1988
+ return { id: randomUUID(), ts, kind: "system", content: `${name} was unmuted` };
1989
+ }
1990
+ if (event.new_authority === "product_owner") {
1991
+ return { id: randomUUID(), ts, kind: "system", content: `${name} was promoted to product owner` };
1992
+ }
1993
+ return { id: randomUUID(), ts, kind: "system", content: `${name} \u2192 ${event.new_authority}` };
1994
+ }
1995
+ case "Pinged":
1996
+ return {
1997
+ id: randomUUID(),
1998
+ ts,
1999
+ kind: "ping",
2000
+ pingerName: event.pinger_name ?? "someone"
2001
+ };
2002
+ case "Activity":
2003
+ if (event.action === "mode_changed") {
2004
+ return {
2005
+ id: randomUUID(),
2006
+ ts,
2007
+ kind: "mode",
2008
+ mode: String(event.detail?.mode ?? "")
2009
+ };
2010
+ }
2011
+ return null;
2012
+ default:
2013
+ return null;
2014
+ }
2015
+ }
2016
+
2017
+ export {
2018
+ HEADLESS_READY,
2019
+ join
2020
+ };
2021
+ //# sourceMappingURL=chunk-ED6ROH6B.js.map