@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.
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/agent/index.d.ts +689 -0
- package/dist/agent/index.js +34 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/chunk-5V7QLCGB.js +184 -0
- package/dist/chunk-5V7QLCGB.js.map +1 -0
- package/dist/chunk-AYQJIAR5.js +152 -0
- package/dist/chunk-AYQJIAR5.js.map +1 -0
- package/dist/chunk-BZ4VDWC2.js +76 -0
- package/dist/chunk-BZ4VDWC2.js.map +1 -0
- package/dist/chunk-CFY6Z6Q6.js +95 -0
- package/dist/chunk-CFY6Z6Q6.js.map +1 -0
- package/dist/chunk-CZHO4DBN.js +1719 -0
- package/dist/chunk-CZHO4DBN.js.map +1 -0
- package/dist/chunk-D7DXZQWT.js +208 -0
- package/dist/chunk-D7DXZQWT.js.map +1 -0
- package/dist/chunk-ED6ROH6B.js +2021 -0
- package/dist/chunk-ED6ROH6B.js.map +1 -0
- package/dist/chunk-PEB6HEFT.js +606 -0
- package/dist/chunk-PEB6HEFT.js.map +1 -0
- package/dist/chunk-QL66XSDB.js +157 -0
- package/dist/chunk-QL66XSDB.js.map +1 -0
- package/dist/chunk-TPNFOQA7.js +288 -0
- package/dist/chunk-TPNFOQA7.js.map +1 -0
- package/dist/chunk-ZEFRNAX6.js +1771 -0
- package/dist/chunk-ZEFRNAX6.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2432 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index-Dp3QSGog.d.ts +837 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/invites-OFXIQLLN.js +23 -0
- package/dist/invites-OFXIQLLN.js.map +1 -0
- package/dist/join-CQXEF65B.js +15 -0
- package/dist/join-CQXEF65B.js.map +1 -0
- package/dist/room-Z7KDRVYD.js +1099 -0
- package/dist/room-Z7KDRVYD.js.map +1 -0
- package/dist/run-DOADB4T7.js +90 -0
- package/dist/run-DOADB4T7.js.map +1 -0
- package/dist/update-JGE3WCQB.js +11 -0
- package/dist/update-JGE3WCQB.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALL_INTERFACES,
|
|
3
|
+
INVITES_DIR,
|
|
4
|
+
LOOPBACK,
|
|
5
|
+
agentEmoji,
|
|
6
|
+
lanAddress,
|
|
7
|
+
listRoomSessions,
|
|
8
|
+
removeRoomSession,
|
|
9
|
+
roomEmoji,
|
|
10
|
+
roomRulesPath,
|
|
11
|
+
saveRoomSession,
|
|
12
|
+
tailnetAddress,
|
|
13
|
+
tmuxAvailable,
|
|
14
|
+
tmuxKillSession,
|
|
15
|
+
tmuxSessionExists
|
|
16
|
+
} from "./chunk-CZHO4DBN.js";
|
|
17
|
+
import "./chunk-PEB6HEFT.js";
|
|
18
|
+
import {
|
|
19
|
+
buildShareUrl,
|
|
20
|
+
extractToken
|
|
21
|
+
} from "./chunk-AYQJIAR5.js";
|
|
22
|
+
import "./chunk-D7DXZQWT.js";
|
|
23
|
+
import "./chunk-QL66XSDB.js";
|
|
24
|
+
import "./chunk-BZ4VDWC2.js";
|
|
25
|
+
|
|
26
|
+
// src/cli/room.ts
|
|
27
|
+
import { spawn, execFileSync as execFileSync3 } from "child_process";
|
|
28
|
+
import {
|
|
29
|
+
existsSync as existsSync4,
|
|
30
|
+
mkdirSync as mkdirSync3,
|
|
31
|
+
writeFileSync as writeFileSync2
|
|
32
|
+
} from "fs";
|
|
33
|
+
import { homedir as homedir3 } from "os";
|
|
34
|
+
import { join as pathJoin2 } from "path";
|
|
35
|
+
import { createInterface } from "readline";
|
|
36
|
+
|
|
37
|
+
// src/cli/new-repo.ts
|
|
38
|
+
import { execFileSync } from "child_process";
|
|
39
|
+
import { existsSync, mkdirSync, readdirSync } from "fs";
|
|
40
|
+
var GIT_TIMEOUT_MS = 15e3;
|
|
41
|
+
function isInsideGitRepo(dir) {
|
|
42
|
+
try {
|
|
43
|
+
const out = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
44
|
+
cwd: dir,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
+
// execFileSync blocks the event loop, so a git that waits on something
|
|
48
|
+
// (a credential or editor prompt on a machine configured differently)
|
|
49
|
+
// hangs the whole process — and in a test run nothing can interrupt it,
|
|
50
|
+
// because a synchronous hang never trips vitest's own timeout.
|
|
51
|
+
timeout: GIT_TIMEOUT_MS
|
|
52
|
+
});
|
|
53
|
+
return out.trim() === "true";
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isNonEmptyDir(dir) {
|
|
59
|
+
try {
|
|
60
|
+
return existsSync(dir) && readdirSync(dir).length > 0;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function createRepoDir(dir, opts) {
|
|
66
|
+
let created = false;
|
|
67
|
+
try {
|
|
68
|
+
if (!existsSync(dir)) {
|
|
69
|
+
mkdirSync(dir, { recursive: true });
|
|
70
|
+
created = true;
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return { ok: false, error: `could not create ${dir}: ${err instanceof Error ? err.message : String(err)}` };
|
|
74
|
+
}
|
|
75
|
+
if (!opts?.gitInit) return { ok: true, created, gitInitialised: false };
|
|
76
|
+
if (isInsideGitRepo(dir)) return { ok: true, created, gitInitialised: false };
|
|
77
|
+
try {
|
|
78
|
+
execFileSync("git", ["init", "--quiet"], { cwd: dir, stdio: "ignore", timeout: GIT_TIMEOUT_MS });
|
|
79
|
+
return { ok: true, created, gitInitialised: true };
|
|
80
|
+
} catch {
|
|
81
|
+
return { ok: true, created, gitInitialised: false };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/cli/repoPicker.ts
|
|
86
|
+
import {
|
|
87
|
+
existsSync as existsSync2,
|
|
88
|
+
readdirSync as readdirSync2,
|
|
89
|
+
readFileSync,
|
|
90
|
+
writeFileSync,
|
|
91
|
+
mkdirSync as mkdirSync2,
|
|
92
|
+
statSync
|
|
93
|
+
} from "fs";
|
|
94
|
+
import { homedir } from "os";
|
|
95
|
+
import { join as pathJoin, resolve } from "path";
|
|
96
|
+
var APIARY_DIR = pathJoin(homedir(), ".apiary");
|
|
97
|
+
var RECENT_PATH = pathJoin(APIARY_DIR, "recent_repos.json");
|
|
98
|
+
var MAX_RECENT = 10;
|
|
99
|
+
var MAX_VISIBLE = 12;
|
|
100
|
+
var SCAN_ROOTS = ["", "Desktop", "code", "Projects", "dev", "src", "work", "repos"];
|
|
101
|
+
var KEY_CTRL_C = "";
|
|
102
|
+
var KEY_ESC = "\x1B";
|
|
103
|
+
var KEY_ESC_ALT = "\x1B\x1B";
|
|
104
|
+
var KEY_UP = "\x1B[A";
|
|
105
|
+
var KEY_DOWN = "\x1B[B";
|
|
106
|
+
var C = "\x1B[36m";
|
|
107
|
+
var D = "\x1B[2m";
|
|
108
|
+
var B = "\x1B[1m";
|
|
109
|
+
var G = "\x1B[32m";
|
|
110
|
+
var Y = "\x1B[33m";
|
|
111
|
+
var M = "\x1B[35m";
|
|
112
|
+
var R = "\x1B[0m";
|
|
113
|
+
function expandTilde(p) {
|
|
114
|
+
if (p === "~") return homedir();
|
|
115
|
+
if (p.startsWith("~/")) return pathJoin(homedir(), p.slice(2));
|
|
116
|
+
return p;
|
|
117
|
+
}
|
|
118
|
+
function isGitRepo(p) {
|
|
119
|
+
try {
|
|
120
|
+
const s = statSync(pathJoin(p, ".git"));
|
|
121
|
+
return s.isDirectory() || s.isFile();
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function shortenPath(p) {
|
|
127
|
+
const home = homedir();
|
|
128
|
+
if (p === home) return "~";
|
|
129
|
+
if (p.startsWith(home + "/")) return "~/" + p.slice(home.length + 1);
|
|
130
|
+
return p;
|
|
131
|
+
}
|
|
132
|
+
function loadRecent() {
|
|
133
|
+
try {
|
|
134
|
+
const data = JSON.parse(readFileSync(RECENT_PATH, "utf-8"));
|
|
135
|
+
return Array.isArray(data.paths) ? data.paths.filter((p) => typeof p === "string" && existsSync2(p)) : [];
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function recordRecentRepo(path) {
|
|
141
|
+
const resolved = resolve(path);
|
|
142
|
+
const existing = loadRecent().filter((p) => p !== resolved);
|
|
143
|
+
const next = [resolved, ...existing].slice(0, MAX_RECENT);
|
|
144
|
+
try {
|
|
145
|
+
if (!existsSync2(APIARY_DIR)) mkdirSync2(APIARY_DIR, { recursive: true });
|
|
146
|
+
writeFileSync(RECENT_PATH, JSON.stringify({ paths: next }, null, 2));
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function discoverRepos() {
|
|
151
|
+
const found = [];
|
|
152
|
+
const home = homedir();
|
|
153
|
+
for (const sub of SCAN_ROOTS) {
|
|
154
|
+
const root = sub ? pathJoin(home, sub) : home;
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = readdirSync2(root);
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (entry.startsWith(".")) continue;
|
|
163
|
+
const full = pathJoin(root, entry);
|
|
164
|
+
try {
|
|
165
|
+
if (!statSync(full).isDirectory()) continue;
|
|
166
|
+
if (isGitRepo(full)) found.push(full);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return found;
|
|
172
|
+
}
|
|
173
|
+
var CUSTOM = /* @__PURE__ */ Symbol("custom");
|
|
174
|
+
var NEW_REPO = /* @__PURE__ */ Symbol("new-repo");
|
|
175
|
+
function arrowPicker(items, header, escHint) {
|
|
176
|
+
return new Promise((resolve2) => {
|
|
177
|
+
const stdin = process.stdin;
|
|
178
|
+
const stdout = process.stdout;
|
|
179
|
+
let idx = 0;
|
|
180
|
+
let printed = 0;
|
|
181
|
+
const visible = Math.min(items.length, MAX_VISIBLE);
|
|
182
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
183
|
+
stdin.setRawMode(true);
|
|
184
|
+
stdin.resume();
|
|
185
|
+
stdin.setEncoding("utf8");
|
|
186
|
+
const render = () => {
|
|
187
|
+
if (printed > 0) stdout.write(`\x1B[${printed}A\r`);
|
|
188
|
+
stdout.write("\x1B[J");
|
|
189
|
+
stdout.write(header + "\n");
|
|
190
|
+
const start = Math.max(0, Math.min(idx - Math.floor(visible / 2), items.length - visible));
|
|
191
|
+
const end = Math.min(start + visible, items.length);
|
|
192
|
+
for (let i = start; i < end; i++) {
|
|
193
|
+
const it = items[i];
|
|
194
|
+
const isCursor = i === idx;
|
|
195
|
+
const marker = isCursor ? `${Y}\u276F${R}` : " ";
|
|
196
|
+
const label = isCursor ? `${C}${B}${it.label}${R}` : it.label;
|
|
197
|
+
const hint = it.hint ? ` ${D}${it.hint}${R}` : "";
|
|
198
|
+
stdout.write(` ${marker} ${label}${hint}
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
const more = items.length > visible ? ` ${D}(${items.length} options \u2014 \u2191/\u2193 scrolls)${R}
|
|
202
|
+
` : "";
|
|
203
|
+
stdout.write(more);
|
|
204
|
+
stdout.write(` ${D}\u2191/\u2193 navigate \xB7 enter select \xB7 esc ${escHint}${R}
|
|
205
|
+
`);
|
|
206
|
+
printed = end - start + 1 + (more ? 1 : 0) + 1;
|
|
207
|
+
};
|
|
208
|
+
const cleanup = () => {
|
|
209
|
+
stdin.removeListener("data", onData);
|
|
210
|
+
if (!wasRaw) stdin.setRawMode(false);
|
|
211
|
+
stdin.pause();
|
|
212
|
+
};
|
|
213
|
+
const onData = (key) => {
|
|
214
|
+
if (key === KEY_CTRL_C) {
|
|
215
|
+
cleanup();
|
|
216
|
+
process.exit(130);
|
|
217
|
+
}
|
|
218
|
+
if (key === KEY_ESC || key === KEY_ESC_ALT) {
|
|
219
|
+
cleanup();
|
|
220
|
+
stdout.write("\n");
|
|
221
|
+
resolve2(null);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (key === "\r" || key === "\n") {
|
|
225
|
+
cleanup();
|
|
226
|
+
stdout.write("\n");
|
|
227
|
+
resolve2(items[idx]);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (key === KEY_UP || key === "k") {
|
|
231
|
+
idx = (idx - 1 + items.length) % items.length;
|
|
232
|
+
render();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (key === KEY_DOWN || key === "j") {
|
|
236
|
+
idx = (idx + 1) % items.length;
|
|
237
|
+
render();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
stdin.on("data", onData);
|
|
242
|
+
render();
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async function askRepoPath(opts) {
|
|
246
|
+
const cwd = resolve(opts.defaultPath);
|
|
247
|
+
const recent = loadRecent();
|
|
248
|
+
const discovered = discoverRepos();
|
|
249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
250
|
+
const items = [];
|
|
251
|
+
const add = (path, hint) => {
|
|
252
|
+
const r = resolve(path);
|
|
253
|
+
if (seen.has(r)) return;
|
|
254
|
+
seen.add(r);
|
|
255
|
+
items.push({ label: shortenPath(r), value: r, hint });
|
|
256
|
+
};
|
|
257
|
+
add(cwd, "current dir");
|
|
258
|
+
for (const p of recent) add(p, "recent");
|
|
259
|
+
for (const p of discovered) add(p);
|
|
260
|
+
items.push({ label: `${M}\u270E${R} Type a custom path\u2026`, value: CUSTOM });
|
|
261
|
+
items.push({ label: `${M}\u271A${R} New project\u2026`, value: NEW_REPO, hint: "creates the folder" });
|
|
262
|
+
const header = ` \u{1F4C1} ${B}Repo${R} ${D}for ${opts.alias}${R}`;
|
|
263
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
264
|
+
const typed = await opts.ask(` Repo path ${D}[${shortenPath(cwd)}]${R}: `);
|
|
265
|
+
const final2 = typed ? resolve(expandTilde(typed)) : cwd;
|
|
266
|
+
recordRecentRepo(final2);
|
|
267
|
+
return final2;
|
|
268
|
+
}
|
|
269
|
+
const picked = await arrowPicker(items, header, "to type a path");
|
|
270
|
+
let final;
|
|
271
|
+
if (picked && picked.value === NEW_REPO) {
|
|
272
|
+
final = await askNewRepoPath(opts, cwd);
|
|
273
|
+
} else if (!picked || picked.value === CUSTOM) {
|
|
274
|
+
const typed = await opts.ask(` Custom repo path ${D}[${shortenPath(cwd)}]${R}: `);
|
|
275
|
+
final = typed ? resolve(expandTilde(typed)) : cwd;
|
|
276
|
+
await ensureExists(final, opts);
|
|
277
|
+
} else {
|
|
278
|
+
final = picked.value;
|
|
279
|
+
}
|
|
280
|
+
recordRecentRepo(final);
|
|
281
|
+
return final;
|
|
282
|
+
}
|
|
283
|
+
async function askNewRepoPath(opts, cwd) {
|
|
284
|
+
const typed = await opts.ask(` New project path ${D}(e.g. ~/Desktop/my-thing)${R}: `);
|
|
285
|
+
const target = typed ? resolve(expandTilde(typed)) : cwd;
|
|
286
|
+
await ensureExists(target, opts, { assumeNew: true });
|
|
287
|
+
return target;
|
|
288
|
+
}
|
|
289
|
+
async function ensureExists(dir, opts, hints) {
|
|
290
|
+
if (existsSync2(dir)) {
|
|
291
|
+
if (!hints?.assumeNew) return;
|
|
292
|
+
if (isNonEmptyDir(dir)) console.log(` ${D}${shortenPath(dir)} already exists \u2014 using it.${R}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const answer = hints?.assumeNew ? "y" : await opts.ask(` ${shortenPath(dir)} doesn't exist. Create it? ${D}[Y/n]${R}: `) || "y";
|
|
296
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
297
|
+
console.log(` ${D}Left alone \u2014 the agent for this repo won't start until it exists.${R}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const wantsGit = isInsideGitRepo(dir) ? false : /^y(es)?$/i.test((await opts.ask(` Run git init in it? ${D}[Y/n]${R}: `) || "y").trim());
|
|
301
|
+
const res = createRepoDir(dir, { gitInit: wantsGit });
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
console.log(` ${M}\u2717${R} ${res.error}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const bits = [res.created ? "created" : "exists"];
|
|
307
|
+
if (res.gitInitialised) bits.push("git initialised");
|
|
308
|
+
console.log(` ${G}\u2713${R} ${shortenPath(dir)} ${D}(${bits.join(", ")})${R}`);
|
|
309
|
+
}
|
|
310
|
+
var RUNTIME_HINTS = {
|
|
311
|
+
claude: "Claude Code",
|
|
312
|
+
codex: "OpenAI Codex CLI"
|
|
313
|
+
};
|
|
314
|
+
async function askRuntime(opts) {
|
|
315
|
+
if (opts.available.length === 0) return void 0;
|
|
316
|
+
if (opts.available.length === 1) return opts.available[0];
|
|
317
|
+
const runtimeGlyph = { claude: "\u{1F9E0}", codex: "\u2328\uFE0F " };
|
|
318
|
+
const items = opts.available.map((rt) => ({
|
|
319
|
+
label: `${runtimeGlyph[rt] ?? "\u2699\uFE0F "} ${rt}`,
|
|
320
|
+
value: rt,
|
|
321
|
+
hint: RUNTIME_HINTS[rt]
|
|
322
|
+
}));
|
|
323
|
+
const header = ` \u2699\uFE0F ${B}Runtime${R} ${D}for ${opts.alias}${R}`;
|
|
324
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
325
|
+
const typed = await opts.ask(` Runtime ${D}[claude]${R} ${D}(${opts.available.join("/")})${R}: `) || "claude";
|
|
326
|
+
return opts.available.includes(typed) ? typed : "claude";
|
|
327
|
+
}
|
|
328
|
+
const picked = await arrowPicker(items, header, "to skip");
|
|
329
|
+
return picked ? picked.value : opts.available[0];
|
|
330
|
+
}
|
|
331
|
+
async function askReach(opts) {
|
|
332
|
+
const items = opts.options.map((o) => ({
|
|
333
|
+
label: o.label,
|
|
334
|
+
value: o.value,
|
|
335
|
+
hint: o.hint
|
|
336
|
+
}));
|
|
337
|
+
const header = ` \u{1F310} ${B}Who can reach this room?${R}`;
|
|
338
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return opts.options[0].value;
|
|
339
|
+
const picked = await arrowPicker(items, header, "for this machine only");
|
|
340
|
+
if (!picked || typeof picked.value !== "string") return opts.options[0].value;
|
|
341
|
+
return picked.value;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/cli/foundation.ts
|
|
345
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
346
|
+
import { existsSync as existsSync3 } from "fs";
|
|
347
|
+
import { join } from "path";
|
|
348
|
+
import { homedir as homedir2 } from "os";
|
|
349
|
+
function foundationInit(cwd) {
|
|
350
|
+
const dir = cwd.replace(/^~/, homedir2());
|
|
351
|
+
if (!existsSync3(dir)) return { ok: false, how: "no-dir" };
|
|
352
|
+
if (existsSync3(join(dir, "docs", "QUEUE.md"))) return { ok: true, how: "already" };
|
|
353
|
+
const attempts = [
|
|
354
|
+
{ cmd: "foundation", args: ["init", dir], how: "local" },
|
|
355
|
+
{ cmd: "npx", args: ["--yes", "github:gorlitzer-labs/foundation", "init", dir], how: "npx" }
|
|
356
|
+
];
|
|
357
|
+
for (const a of attempts) {
|
|
358
|
+
try {
|
|
359
|
+
execFileSync2(a.cmd, a.args, { stdio: "ignore", timeout: 9e4 });
|
|
360
|
+
return { ok: true, how: a.how };
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return { ok: false, how: "unavailable" };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/cli/room.ts
|
|
368
|
+
var DAEMON_SHARE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
369
|
+
var TIER_TOKENS = {
|
|
370
|
+
owner: "product_owner",
|
|
371
|
+
product_owner: "product_owner",
|
|
372
|
+
admin: "admin",
|
|
373
|
+
member: "member",
|
|
374
|
+
guest: "guest"
|
|
375
|
+
};
|
|
376
|
+
var TYPE_TOKENS = {
|
|
377
|
+
agent: "agent",
|
|
378
|
+
human: "human"
|
|
379
|
+
};
|
|
380
|
+
var MODE_TOKENS = {
|
|
381
|
+
standby: "standby-everyone",
|
|
382
|
+
"standby-everyone": "standby-everyone",
|
|
383
|
+
"standby-people": "standby-people",
|
|
384
|
+
"standby-agents": "standby-agents",
|
|
385
|
+
active: "everyone",
|
|
386
|
+
everyone: "everyone",
|
|
387
|
+
people: "people",
|
|
388
|
+
agents: "agents"
|
|
389
|
+
};
|
|
390
|
+
var MODEL_TOKENS = /* @__PURE__ */ new Set([
|
|
391
|
+
"opus",
|
|
392
|
+
"sonnet",
|
|
393
|
+
"haiku"
|
|
394
|
+
// also accept full model ids verbatim
|
|
395
|
+
]);
|
|
396
|
+
function isModelToken(tok) {
|
|
397
|
+
const lc = tok.toLowerCase();
|
|
398
|
+
if (MODEL_TOKENS.has(lc)) return true;
|
|
399
|
+
return /^claude-(opus|sonnet|haiku)-\d/.test(lc);
|
|
400
|
+
}
|
|
401
|
+
function parseAliasSpec(input) {
|
|
402
|
+
const parts = input.split(":").map((p) => p.trim()).filter(Boolean);
|
|
403
|
+
if (parts.length === 0) return null;
|
|
404
|
+
const alias = parts[0];
|
|
405
|
+
let type = "agent";
|
|
406
|
+
let tier = "member";
|
|
407
|
+
let role = "agent";
|
|
408
|
+
let model;
|
|
409
|
+
let mode;
|
|
410
|
+
for (const tok of parts.slice(1)) {
|
|
411
|
+
const lc = tok.toLowerCase();
|
|
412
|
+
if (TYPE_TOKENS[lc]) {
|
|
413
|
+
type = TYPE_TOKENS[lc];
|
|
414
|
+
role = type;
|
|
415
|
+
} else if (TIER_TOKENS[lc]) {
|
|
416
|
+
tier = TIER_TOKENS[lc];
|
|
417
|
+
} else if (MODE_TOKENS[lc]) {
|
|
418
|
+
mode = MODE_TOKENS[lc];
|
|
419
|
+
} else if (isModelToken(tok)) {
|
|
420
|
+
model = lc;
|
|
421
|
+
} else {
|
|
422
|
+
role = lc;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { alias, type, tier, role, model, mode };
|
|
426
|
+
}
|
|
427
|
+
function applyDefaultModes(participants) {
|
|
428
|
+
let seenActiveAgent = false;
|
|
429
|
+
for (const p of participants) {
|
|
430
|
+
if (p.role !== "agent") continue;
|
|
431
|
+
if (p.mode !== void 0) {
|
|
432
|
+
if (!p.mode.startsWith("standby-")) seenActiveAgent = true;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (!seenActiveAgent) {
|
|
436
|
+
p.mode = "everyone";
|
|
437
|
+
seenActiveAgent = true;
|
|
438
|
+
} else {
|
|
439
|
+
p.mode = "standby-everyone";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function makePrompt() {
|
|
444
|
+
return {
|
|
445
|
+
ask: (q) => new Promise((res) => {
|
|
446
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
447
|
+
rl.question(q, (a) => {
|
|
448
|
+
rl.close();
|
|
449
|
+
res(a.trim());
|
|
450
|
+
});
|
|
451
|
+
}),
|
|
452
|
+
close: () => {
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function spawnDaemonServer(opts) {
|
|
457
|
+
const scriptPath = process.argv[1];
|
|
458
|
+
const ttl = opts.shareTtlMs ?? DAEMON_SHARE_TTL_MS;
|
|
459
|
+
const args = ["serve", "--headless", "--share-token-ttl", String(ttl)];
|
|
460
|
+
if (opts.room) args.push("--room", opts.room);
|
|
461
|
+
if (opts.port) args.push("--port", String(opts.port));
|
|
462
|
+
if (opts.share) args.push("--share");
|
|
463
|
+
if (opts.expose) args.push("--expose");
|
|
464
|
+
if (opts.bind) args.push("--bind", opts.bind);
|
|
465
|
+
return new Promise((resolve2) => {
|
|
466
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
467
|
+
detached: true,
|
|
468
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
469
|
+
});
|
|
470
|
+
let stdoutBuf = "";
|
|
471
|
+
let stderrBuf = "";
|
|
472
|
+
let settled = false;
|
|
473
|
+
const settle = (v) => {
|
|
474
|
+
if (settled) return;
|
|
475
|
+
settled = true;
|
|
476
|
+
resolve2(v);
|
|
477
|
+
};
|
|
478
|
+
child.stdout.on("data", (chunk) => {
|
|
479
|
+
stdoutBuf += chunk.toString();
|
|
480
|
+
const line = stdoutBuf.split("\n").find((l) => l.trim().startsWith("{"));
|
|
481
|
+
if (line) {
|
|
482
|
+
try {
|
|
483
|
+
const result = JSON.parse(line.trim());
|
|
484
|
+
child.unref();
|
|
485
|
+
settle({ ...result, pid: child.pid });
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
child.stderr.on("data", (chunk) => {
|
|
491
|
+
stderrBuf += chunk.toString();
|
|
492
|
+
});
|
|
493
|
+
child.on("error", (err) => settle({ reason: err.message, stderr: stderrBuf }));
|
|
494
|
+
child.on("exit", (code) => {
|
|
495
|
+
if (code !== 0) {
|
|
496
|
+
const portInUse = /EADDRINUSE|already in use/i.test(stderrBuf);
|
|
497
|
+
settle({
|
|
498
|
+
reason: portInUse ? `port ${opts.port ?? 7890} already in use` : `daemon exited with code ${code}`,
|
|
499
|
+
stderr: stderrBuf,
|
|
500
|
+
portInUse
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
setTimeout(() => settle({ reason: "timed out waiting for daemon startup", stderr: stderrBuf }), 15e3);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function isDaemonResult(v) {
|
|
508
|
+
return "serverUrl" in v;
|
|
509
|
+
}
|
|
510
|
+
function writeInvite(alias, joinUrl) {
|
|
511
|
+
mkdirSync3(INVITES_DIR, { recursive: true });
|
|
512
|
+
writeFileSync2(pathJoin2(INVITES_DIR, alias), joinUrl, { mode: 384 });
|
|
513
|
+
}
|
|
514
|
+
function isLocalPath(p) {
|
|
515
|
+
try {
|
|
516
|
+
return existsSync4(p.replace(/^~/, homedir3()));
|
|
517
|
+
} catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function httpJoin(serverUrl, shareToken, name = "[system]") {
|
|
522
|
+
try {
|
|
523
|
+
const res = await fetch(`${serverUrl}/join`, {
|
|
524
|
+
method: "POST",
|
|
525
|
+
headers: { "Content-Type": "application/json" },
|
|
526
|
+
body: JSON.stringify({ shareToken, name, type: "agent" })
|
|
527
|
+
});
|
|
528
|
+
if (!res.ok) return null;
|
|
529
|
+
const data = await res.json();
|
|
530
|
+
return data.sessionToken ?? null;
|
|
531
|
+
} catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function httpDisconnect(serverUrl, sessionToken) {
|
|
536
|
+
await fetch(`${serverUrl}/disconnect`, {
|
|
537
|
+
method: "POST",
|
|
538
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` }
|
|
539
|
+
}).catch(() => {
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async function postSystemMessage(serverUrl, adminShareToken, content) {
|
|
543
|
+
const sessionToken = await httpJoin(serverUrl, adminShareToken, "[system]");
|
|
544
|
+
if (!sessionToken) return;
|
|
545
|
+
await fetch(`${serverUrl}/message`, {
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
|
|
548
|
+
body: JSON.stringify({ content })
|
|
549
|
+
}).catch(() => {
|
|
550
|
+
});
|
|
551
|
+
await httpDisconnect(serverUrl, sessionToken);
|
|
552
|
+
}
|
|
553
|
+
async function fetchServerState(serverUrl, adminShareToken, publicUrl) {
|
|
554
|
+
const sessionToken = await httpJoin(serverUrl, adminShareToken, "[system]");
|
|
555
|
+
if (!sessionToken) return { connectedNames: /* @__PURE__ */ new Set() };
|
|
556
|
+
let memberToken;
|
|
557
|
+
let connectedNames = /* @__PURE__ */ new Set();
|
|
558
|
+
try {
|
|
559
|
+
const [shareRes, participantsRes] = await Promise.all([
|
|
560
|
+
fetch(`${serverUrl}/share`, {
|
|
561
|
+
method: "POST",
|
|
562
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
|
|
563
|
+
body: JSON.stringify({})
|
|
564
|
+
}),
|
|
565
|
+
fetch(`${serverUrl}/participants`, {
|
|
566
|
+
headers: { Authorization: `Bearer ${sessionToken}` }
|
|
567
|
+
})
|
|
568
|
+
]);
|
|
569
|
+
if (shareRes.ok) {
|
|
570
|
+
const data = await shareRes.json();
|
|
571
|
+
const memberUrl = data.links?.["member"];
|
|
572
|
+
if (memberUrl) memberToken = extractToken(memberUrl) ?? void 0;
|
|
573
|
+
}
|
|
574
|
+
if (participantsRes.ok) {
|
|
575
|
+
const data = await participantsRes.json();
|
|
576
|
+
connectedNames = new Set(data.participants.map((p) => p.name.toLowerCase()));
|
|
577
|
+
}
|
|
578
|
+
} finally {
|
|
579
|
+
await httpDisconnect(serverUrl, sessionToken);
|
|
580
|
+
}
|
|
581
|
+
return { memberToken, connectedNames };
|
|
582
|
+
}
|
|
583
|
+
function detectRuntimes() {
|
|
584
|
+
const has = (cmd) => {
|
|
585
|
+
try {
|
|
586
|
+
execFileSync3("which", [cmd], { stdio: "ignore" });
|
|
587
|
+
return true;
|
|
588
|
+
} catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
return { claude: has("claude"), codex: has("codex") };
|
|
593
|
+
}
|
|
594
|
+
var A = "\x1B[33m";
|
|
595
|
+
var RESET = "\x1B[0m";
|
|
596
|
+
var BEE_BANNER = [
|
|
597
|
+
`${A} _ \\ \\${RESET}`,
|
|
598
|
+
`${A} ____ _____ (_)___ ________ __ \\ \\ \\${RESET}`,
|
|
599
|
+
`${A} / __ \`/ __ \\/ / __ \`/ ___/ / / / (o o)${RESET}`,
|
|
600
|
+
`${A}/ /_/ / /_/ / / /_/ / / / /_/ / )=BzZz=(${RESET}`,
|
|
601
|
+
`${A}\\__,_/ .___/_/\\__,_/_/ \\__, / / / /${RESET}`,
|
|
602
|
+
`${A} /_/ /____/ / / /${RESET}`
|
|
603
|
+
].join("\n");
|
|
604
|
+
function spawnAgentBackground(alias, cwd, runtime, tier = "member", model, mode) {
|
|
605
|
+
const resolvedCwd = cwd.replace(/^~/, homedir3());
|
|
606
|
+
if (!existsSync4(resolvedCwd)) return { ok: false, reason: `${shortenPath(cwd)} doesn't exist` };
|
|
607
|
+
const args = [process.argv[1], runtime, alias, "--background"];
|
|
608
|
+
if (tier === "admin" || tier === "product_owner") args.push("--admin");
|
|
609
|
+
if (model) args.push("--model", model);
|
|
610
|
+
if (mode) args.push("--mode", mode);
|
|
611
|
+
try {
|
|
612
|
+
const child = spawn(process.execPath, args, {
|
|
613
|
+
detached: true,
|
|
614
|
+
stdio: "ignore",
|
|
615
|
+
cwd: resolvedCwd
|
|
616
|
+
});
|
|
617
|
+
child.unref();
|
|
618
|
+
return { ok: true };
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return { ok: false, reason: err instanceof Error ? err.message : "spawn failed" };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function mintTierTokens(serverUrl, adminShareToken, shareBase, tiers) {
|
|
624
|
+
const result = {};
|
|
625
|
+
const sessionToken = await httpJoin(serverUrl, adminShareToken, "[system]");
|
|
626
|
+
if (!sessionToken) return result;
|
|
627
|
+
try {
|
|
628
|
+
for (const tier of tiers) {
|
|
629
|
+
try {
|
|
630
|
+
const res = await fetch(`${serverUrl}/share`, {
|
|
631
|
+
method: "POST",
|
|
632
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionToken}` },
|
|
633
|
+
body: JSON.stringify({ authority: tier })
|
|
634
|
+
});
|
|
635
|
+
if (!res.ok) continue;
|
|
636
|
+
const data = await res.json();
|
|
637
|
+
const linkForTier = data.links?.[tier];
|
|
638
|
+
const token = linkForTier ? extractToken(linkForTier) : data.token;
|
|
639
|
+
if (token) result[tier] = buildShareUrl(shareBase, token);
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} finally {
|
|
644
|
+
await httpDisconnect(serverUrl, sessionToken);
|
|
645
|
+
}
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
function printInvite(alias, joinUrl, sameHost) {
|
|
649
|
+
const C2 = "\x1B[36m";
|
|
650
|
+
const D2 = "\x1B[2m";
|
|
651
|
+
const G2 = "\x1B[32m";
|
|
652
|
+
const R2 = "\x1B[0m";
|
|
653
|
+
if (sameHost) {
|
|
654
|
+
writeInvite(alias, joinUrl);
|
|
655
|
+
console.log(` ${G2}\u2713${R2} ${alias} ${D2}(invite queued \u2014 auto-consumed on next launch)${R2}`);
|
|
656
|
+
} else {
|
|
657
|
+
console.log(` ${alias}:`);
|
|
658
|
+
}
|
|
659
|
+
console.log(` ${C2}apiary join${R2} "${joinUrl}"`);
|
|
660
|
+
}
|
|
661
|
+
async function roomCreate(opts) {
|
|
662
|
+
const Y2 = "\x1B[33m";
|
|
663
|
+
const B2 = "\x1B[1m";
|
|
664
|
+
const D2 = "\x1B[2m";
|
|
665
|
+
const C2 = "\x1B[36m";
|
|
666
|
+
const R2 = "\x1B[0m";
|
|
667
|
+
const { ask, close } = makePrompt();
|
|
668
|
+
console.log(`
|
|
669
|
+
${BEE_BANNER}
|
|
670
|
+
`);
|
|
671
|
+
console.log(` ${Y2}${B2}apiary${R2} \u2014 create room
|
|
672
|
+
`);
|
|
673
|
+
const roomName = opts.room ?? (await ask(` Room name ${D2}[random]${R2}: `) || void 0);
|
|
674
|
+
const hostName = await ask(` Your name ${D2}[random]${R2}: `) || void 0;
|
|
675
|
+
const ttlInput = await ask(` Session duration ${D2}[7d]${R2}: `);
|
|
676
|
+
const shareTtlMs = parseDuration(ttlInput) ?? DAEMON_SHARE_TTL_MS;
|
|
677
|
+
const rts = detectRuntimes();
|
|
678
|
+
const availableRuntimes = [...rts.claude ? ["claude"] : [], ...rts.codex ? ["codex"] : []];
|
|
679
|
+
if (availableRuntimes.length === 0) {
|
|
680
|
+
console.log(`
|
|
681
|
+
${D2}(neither claude nor codex found in PATH \u2014 agents will need to join manually)${R2}`);
|
|
682
|
+
} else if (availableRuntimes.length === 1) {
|
|
683
|
+
console.log(`
|
|
684
|
+
${D2}Runtime auto-detected: ${availableRuntimes[0]}${R2}`);
|
|
685
|
+
}
|
|
686
|
+
const participants = [];
|
|
687
|
+
const G2 = "\x1B[32m";
|
|
688
|
+
const M2 = "\x1B[35m";
|
|
689
|
+
const divider = ` ${D2}${"\u2500".repeat(56)}${R2}`;
|
|
690
|
+
console.log(`
|
|
691
|
+
${B2}Invite participants${R2}`);
|
|
692
|
+
console.log(divider);
|
|
693
|
+
while (true) {
|
|
694
|
+
const n = participants.length + 1;
|
|
695
|
+
console.log(`
|
|
696
|
+
\u{1F41D} ${M2}${B2}Participant ${n}${R2}`);
|
|
697
|
+
const aliasInput = await ask(
|
|
698
|
+
` ${C2}\u2192${R2} Alias ${D2}(blank \u21B5 to finish \xB7 suffix :human, :owner, :admin, :guest, :standby)${R2}: `
|
|
699
|
+
);
|
|
700
|
+
if (!aliasInput) {
|
|
701
|
+
const count = participants.length;
|
|
702
|
+
const summary = count === 0 ? `${D2}no participants \u2014 starting an empty room${R2}` : `${G2}\u2713${R2} ${B2}${count}${R2} participant${count === 1 ? "" : "s"} ready \u2014 starting room\u2026`;
|
|
703
|
+
console.log(` ${summary}`);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
const spec = parseAliasSpec(aliasInput);
|
|
707
|
+
if (!spec) continue;
|
|
708
|
+
const { alias, role, tier, model, mode } = spec;
|
|
709
|
+
const cwd = await askRepoPath({ alias, defaultPath: process.cwd(), ask });
|
|
710
|
+
const runtime = role === "agent" ? await askRuntime({ alias, available: availableRuntimes, ask }) : void 0;
|
|
711
|
+
participants.push({ alias, cwd, role, runtime, tier, model, mode });
|
|
712
|
+
const bug = role === "agent" ? agentEmoji(alias) : role === "human" ? "\u{1F464}" : "\u270E";
|
|
713
|
+
const runtimeBadge = runtime ? ` \xB7 ${C2}${runtime}${R2}` : "";
|
|
714
|
+
const modelBadge = model ? ` ${Y2}[${model}]${R2}` : "";
|
|
715
|
+
const tierBadge = tier !== "member" ? ` ${Y2}[${tier === "product_owner" ? "owner" : tier}]${R2}` : "";
|
|
716
|
+
const modeBadge = mode ? ` ${Y2}[${mode}]${R2}` : "";
|
|
717
|
+
console.log(
|
|
718
|
+
` ${G2}\u2705${R2} ${bug} ${B2}${alias}${R2}${tierBadge}${modelBadge}${modeBadge} ${D2}${role}${R2}${runtimeBadge} ${D2}${shortenPath(cwd)}${R2}`
|
|
719
|
+
);
|
|
720
|
+
console.log(divider);
|
|
721
|
+
}
|
|
722
|
+
applyDefaultModes(participants);
|
|
723
|
+
let bindAddress = opts.bind;
|
|
724
|
+
if (!bindAddress && !opts.expose) {
|
|
725
|
+
const ts = tailnetAddress();
|
|
726
|
+
const lan = lanAddress();
|
|
727
|
+
const reach = await askReach({
|
|
728
|
+
ask,
|
|
729
|
+
options: [
|
|
730
|
+
{ label: "This machine only", value: LOOPBACK, hint: "safest default" },
|
|
731
|
+
...ts ? [{
|
|
732
|
+
label: `Tailnet ${D2}(${ts})${R2}`,
|
|
733
|
+
value: ts,
|
|
734
|
+
// The room is on your VPN only: reachable from your other devices
|
|
735
|
+
// anywhere, invisible to whatever wifi this machine is on.
|
|
736
|
+
hint: "your devices anywhere, via Tailscale \u2014 not the local network"
|
|
737
|
+
}] : [],
|
|
738
|
+
...lan ? [{
|
|
739
|
+
label: `This network ${D2}(${lan})${R2}`,
|
|
740
|
+
value: lan,
|
|
741
|
+
hint: "anyone on the same wifi"
|
|
742
|
+
}] : [],
|
|
743
|
+
{ label: "All interfaces", value: ALL_INTERFACES, hint: "including untrusted networks \u2014 rarely what you want" }
|
|
744
|
+
]
|
|
745
|
+
});
|
|
746
|
+
if (reach !== LOOPBACK) bindAddress = reach;
|
|
747
|
+
}
|
|
748
|
+
close();
|
|
749
|
+
if (opts.foundation !== false) {
|
|
750
|
+
const seen = /* @__PURE__ */ new Set();
|
|
751
|
+
for (const p of participants) {
|
|
752
|
+
if (!isLocalPath(p.cwd)) continue;
|
|
753
|
+
const key = p.cwd.replace(/^~/, homedir3());
|
|
754
|
+
if (seen.has(key)) continue;
|
|
755
|
+
seen.add(key);
|
|
756
|
+
const r = foundationInit(p.cwd);
|
|
757
|
+
const where = shortenPath(p.cwd);
|
|
758
|
+
if (r.how === "already") console.log(` ${D2}Foundation already set up in ${where}${R2}`);
|
|
759
|
+
else if (r.ok) console.log(` ${G2}\u2713${R2} Foundation installed in ${D2}${where}${R2} ${D2}(${r.how})${R2}`);
|
|
760
|
+
else console.log(` ${D2}Foundation skipped for ${where} \u2014 run: foundation init${R2}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
console.log(`
|
|
764
|
+
Starting server in background...`);
|
|
765
|
+
const daemonRes = await spawnDaemonServer({
|
|
766
|
+
room: roomName,
|
|
767
|
+
port: opts.port,
|
|
768
|
+
share: opts.share,
|
|
769
|
+
expose: opts.expose,
|
|
770
|
+
bind: bindAddress,
|
|
771
|
+
shareTtlMs
|
|
772
|
+
});
|
|
773
|
+
if (!isDaemonResult(daemonRes)) {
|
|
774
|
+
console.error(`
|
|
775
|
+
${Y2}\u2717${R2} Failed to start server: ${B2}${daemonRes.reason}${R2}`);
|
|
776
|
+
if (daemonRes.portInUse) {
|
|
777
|
+
console.error(` ${D2}A previous room daemon is probably still bound. Inspect with:${R2} ${C2}apiary ps${R2}`);
|
|
778
|
+
console.error(` ${D2}Stop it with:${R2} ${C2}apiary stop --all${R2} ${D2}(or ${R2}${C2}apiary stop <room>${R2}${D2})${R2}`);
|
|
779
|
+
} else if (daemonRes.stderr?.trim()) {
|
|
780
|
+
console.error(` ${D2}stderr:${R2}
|
|
781
|
+
${daemonRes.stderr.split("\n").map((l) => " " + l).join("\n")}`);
|
|
782
|
+
}
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
const daemon = daemonRes;
|
|
786
|
+
saveRoomSession({
|
|
787
|
+
roomName: daemon.roomName,
|
|
788
|
+
serverUrl: daemon.serverUrl,
|
|
789
|
+
publicUrl: daemon.publicUrl,
|
|
790
|
+
adminToken: daemon.adminToken,
|
|
791
|
+
memberToken: daemon.memberToken,
|
|
792
|
+
pid: daemon.pid,
|
|
793
|
+
lastActive: Date.now(),
|
|
794
|
+
hostName,
|
|
795
|
+
participants
|
|
796
|
+
});
|
|
797
|
+
const shareBase = daemon.publicUrl !== daemon.serverUrl ? daemon.publicUrl : daemon.serverUrl;
|
|
798
|
+
const memberJoinUrl = buildShareUrl(shareBase, daemon.memberToken);
|
|
799
|
+
const tieredJoinUrls = { member: memberJoinUrl };
|
|
800
|
+
const nonMemberTiers = Array.from(
|
|
801
|
+
new Set(participants.map((p) => p.tier).filter((t) => t !== "member"))
|
|
802
|
+
);
|
|
803
|
+
if (nonMemberTiers.length > 0) {
|
|
804
|
+
const mintedUrls = await mintTierTokens(daemon.serverUrl, daemon.adminToken, shareBase, nonMemberTiers);
|
|
805
|
+
Object.assign(tieredJoinUrls, mintedUrls);
|
|
806
|
+
}
|
|
807
|
+
const canSpawn = tmuxAvailable();
|
|
808
|
+
if (participants.length > 0) {
|
|
809
|
+
console.log(`
|
|
810
|
+
${B2}Invite links:${R2}
|
|
811
|
+
`);
|
|
812
|
+
const hasRuntimeParticipants = participants.some((p) => p.runtime);
|
|
813
|
+
if (!canSpawn && hasRuntimeParticipants) {
|
|
814
|
+
console.log(` ${D2}(tmux not found \u2014 agents will need to join manually via the invite links below)${R2}
|
|
815
|
+
`);
|
|
816
|
+
}
|
|
817
|
+
const allLocal = participants.every((p) => isLocalPath(p.cwd));
|
|
818
|
+
for (const p of participants) {
|
|
819
|
+
const sameHost = allLocal || isLocalPath(p.cwd);
|
|
820
|
+
const joinUrl = tieredJoinUrls[p.tier] ?? memberJoinUrl;
|
|
821
|
+
printInvite(p.alias, joinUrl, sameHost);
|
|
822
|
+
if (sameHost && canSpawn && p.runtime) {
|
|
823
|
+
const spawned = spawnAgentBackground(p.alias, p.cwd, p.runtime, p.tier, p.model, p.mode);
|
|
824
|
+
if (spawned.ok) {
|
|
825
|
+
const modeNote = p.mode?.startsWith("standby-") ? ` ${D2}\xB7${R2} ${Y2}standby${R2} ${D2}(wakes on @mention, /ping or a whisper)${R2}` : p.mode ? ` ${D2}\xB7 listening to everything${R2}` : "";
|
|
826
|
+
console.log(` ${D2}\u2192 spawned ${p.runtime} session (tmux attach -t apiary_${p.alias})${R2}${modeNote}`);
|
|
827
|
+
} else {
|
|
828
|
+
console.log(` ${Y2}\u2717${R2} ${B2}${p.alias}${R2} not started \u2014 ${spawned.reason}.`);
|
|
829
|
+
console.log(` ${D2}Create it, then: ${R2}${C2}apiary room resume ${roomName}${R2}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (participants.some((p) => p.role === "agent" && p.mode?.startsWith("standby-"))) {
|
|
834
|
+
console.log(
|
|
835
|
+
`
|
|
836
|
+
${D2}Only the first agent listens to everything \u2014 the rest wake when addressed,${R2}
|
|
837
|
+
${D2}so one remark costs one agent turn instead of all of them.${R2}
|
|
838
|
+
${D2}Change any of it live with ${R2}${C2}/setmode <name> <mode>${R2}${D2}, or up front with ${R2}${C2}alias:standby${R2}${D2} / ${R2}${C2}alias:active${R2}${D2}.${R2}`
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
console.log("");
|
|
842
|
+
} else {
|
|
843
|
+
console.log(`
|
|
844
|
+
${D2}Member join link:${R2} ${C2}${memberJoinUrl}${R2}
|
|
845
|
+
`);
|
|
846
|
+
}
|
|
847
|
+
console.log(` ${D2}Rules:${R2} ${C2}${shortenPath(roomRulesPath(daemon.roomName))}${R2} ${D2}(edit to customize \u2014 hot-reloads)${R2}`);
|
|
848
|
+
console.log(` Press Ctrl+C to leave \u2014 server keeps running.
|
|
849
|
+
`);
|
|
850
|
+
const { join: join2 } = await import("./join-CQXEF65B.js");
|
|
851
|
+
const adminJoinUrl = buildShareUrl(daemon.serverUrl, daemon.adminToken);
|
|
852
|
+
const expectedAgents = participants.filter((p) => p.role === "agent").map((p) => p.alias);
|
|
853
|
+
await join2({ server: adminJoinUrl, name: hostName, expectedAgents });
|
|
854
|
+
console.log(`
|
|
855
|
+
Server still running ${roomEmoji(daemon.roomName)} ${Y2}${daemon.roomName}${R2}`);
|
|
856
|
+
console.log(` Rejoin: ${C2}apiary room resume ${daemon.roomName}${R2}
|
|
857
|
+
`);
|
|
858
|
+
}
|
|
859
|
+
async function roomResume(name) {
|
|
860
|
+
const Y2 = "\x1B[33m";
|
|
861
|
+
const B2 = "\x1B[1m";
|
|
862
|
+
const D2 = "\x1B[2m";
|
|
863
|
+
const C2 = "\x1B[36m";
|
|
864
|
+
const G2 = "\x1B[32m";
|
|
865
|
+
const W = "\x1B[33m";
|
|
866
|
+
const R2 = "\x1B[0m";
|
|
867
|
+
const sessions = listRoomSessions();
|
|
868
|
+
const session = sessions.find((s) => s.roomName === name);
|
|
869
|
+
if (!session) {
|
|
870
|
+
console.error(` No saved session for room "${name}".`);
|
|
871
|
+
console.error(` Run: ${C2}apiary room list${R2} to see saved rooms.`);
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
let serverUrl = session.serverUrl;
|
|
875
|
+
let adminToken = session.adminToken;
|
|
876
|
+
let memberToken = session.memberToken;
|
|
877
|
+
let publicUrl = session.publicUrl;
|
|
878
|
+
let pid = session.pid;
|
|
879
|
+
const alive = isServerAlive(session);
|
|
880
|
+
let connectedNames = /* @__PURE__ */ new Set();
|
|
881
|
+
if (alive) {
|
|
882
|
+
console.log(`
|
|
883
|
+
${Y2}${B2}${name}${R2} ${D2}server running (PID ${pid})${R2}
|
|
884
|
+
`);
|
|
885
|
+
const state = await fetchServerState(serverUrl, adminToken, publicUrl);
|
|
886
|
+
if (state.memberToken) memberToken = state.memberToken;
|
|
887
|
+
connectedNames = state.connectedNames;
|
|
888
|
+
} else {
|
|
889
|
+
console.log(`
|
|
890
|
+
${Y2}${B2}${name}${R2} ${D2}server stopped \u2014 restarting...${R2}
|
|
891
|
+
`);
|
|
892
|
+
const daemonRes = await spawnDaemonServer({ room: name, bind: session.bind });
|
|
893
|
+
if (!isDaemonResult(daemonRes)) {
|
|
894
|
+
console.error(`
|
|
895
|
+
${W}\u2717${R2} Failed to restart server: ${B2}${daemonRes.reason}${R2}`);
|
|
896
|
+
if (daemonRes.portInUse) {
|
|
897
|
+
console.error(` ${D2}Inspect:${R2} ${C2}apiary ps${R2} ${D2}Stop:${R2} ${C2}apiary stop --all${R2}`);
|
|
898
|
+
} else if (daemonRes.stderr?.trim()) {
|
|
899
|
+
console.error(` ${D2}stderr:${R2}
|
|
900
|
+
${daemonRes.stderr.split("\n").map((l) => " " + l).join("\n")}`);
|
|
901
|
+
}
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
const daemon = daemonRes;
|
|
905
|
+
serverUrl = daemon.serverUrl;
|
|
906
|
+
adminToken = daemon.adminToken;
|
|
907
|
+
memberToken = daemon.memberToken;
|
|
908
|
+
publicUrl = daemon.publicUrl;
|
|
909
|
+
pid = daemon.pid;
|
|
910
|
+
saveRoomSession({
|
|
911
|
+
...session,
|
|
912
|
+
serverUrl,
|
|
913
|
+
publicUrl,
|
|
914
|
+
adminToken,
|
|
915
|
+
memberToken,
|
|
916
|
+
pid,
|
|
917
|
+
lastActive: Date.now()
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const shareBase = publicUrl !== serverUrl ? publicUrl : serverUrl;
|
|
921
|
+
const memberJoinUrl = buildShareUrl(shareBase, memberToken);
|
|
922
|
+
if (session.participants && session.participants.length > 0) {
|
|
923
|
+
const allLocal = session.participants.every((p) => isLocalPath(p.cwd));
|
|
924
|
+
let hasConnected = false;
|
|
925
|
+
let hasDisconnected = false;
|
|
926
|
+
for (const p of session.participants) {
|
|
927
|
+
const connected = connectedNames.has(p.alias.toLowerCase());
|
|
928
|
+
if (connected) {
|
|
929
|
+
if (!hasConnected) console.log(` ${B2}Connected:${R2}`);
|
|
930
|
+
hasConnected = true;
|
|
931
|
+
console.log(` ${G2}\u25CF${R2} ${p.alias}`);
|
|
932
|
+
} else {
|
|
933
|
+
if (!hasDisconnected) {
|
|
934
|
+
console.log(`
|
|
935
|
+
${B2}Rejoin links:${R2}
|
|
936
|
+
`);
|
|
937
|
+
if (!tmuxAvailable() && session.participants?.some((pp) => pp.runtime)) {
|
|
938
|
+
console.log(` ${D2}(tmux not found \u2014 agents will need to join manually via the invite links below)${R2}
|
|
939
|
+
`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
hasDisconnected = true;
|
|
943
|
+
console.log(` ${W}\u25CB${R2} ${p.alias} ${D2}disconnected${R2}`);
|
|
944
|
+
const sameHost = allLocal || isLocalPath(p.cwd);
|
|
945
|
+
printInvite(p.alias, memberJoinUrl, sameHost);
|
|
946
|
+
if (sameHost && tmuxAvailable() && p.runtime && !tmuxSessionExists(`apiary_${p.alias}`)) {
|
|
947
|
+
const spawned = spawnAgentBackground(p.alias, p.cwd, p.runtime, p.tier, p.model, p.mode);
|
|
948
|
+
if (spawned.ok) {
|
|
949
|
+
console.log(` ${D2}\u2192 respawned ${p.runtime} session (tmux attach -t apiary_${p.alias})${R2}`);
|
|
950
|
+
} else {
|
|
951
|
+
console.log(` ${W}\u2717${R2} ${B2}${p.alias}${R2} not restarted \u2014 ${spawned.reason}.`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (hasConnected && alive) {
|
|
957
|
+
await postSystemMessage(
|
|
958
|
+
serverUrl,
|
|
959
|
+
adminToken,
|
|
960
|
+
`[SYSTEM] Session resumed by admin. You are still connected.`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
console.log("");
|
|
964
|
+
}
|
|
965
|
+
let resumeHostName = session.hostName;
|
|
966
|
+
if (!resumeHostName) {
|
|
967
|
+
const { ask, close } = makePrompt();
|
|
968
|
+
resumeHostName = await ask(` Your name ${D2}[random]${R2}: `) || void 0;
|
|
969
|
+
close();
|
|
970
|
+
if (resumeHostName) {
|
|
971
|
+
saveRoomSession({ ...session, hostName: resumeHostName, lastActive: Date.now() });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
console.log(` Press Ctrl+C to leave \u2014 server keeps running.
|
|
975
|
+
`);
|
|
976
|
+
const { join: join2 } = await import("./join-CQXEF65B.js");
|
|
977
|
+
const adminJoinUrl = buildShareUrl(serverUrl, adminToken);
|
|
978
|
+
await join2({ server: adminJoinUrl, name: resumeHostName });
|
|
979
|
+
console.log(`
|
|
980
|
+
Server still running (room: ${Y2}${name}${R2})`);
|
|
981
|
+
console.log(` Rejoin: ${C2}apiary room resume ${name}${R2}
|
|
982
|
+
`);
|
|
983
|
+
}
|
|
984
|
+
function roomList() {
|
|
985
|
+
const Y2 = "\x1B[33m";
|
|
986
|
+
const B2 = "\x1B[1m";
|
|
987
|
+
const D2 = "\x1B[2m";
|
|
988
|
+
const C2 = "\x1B[36m";
|
|
989
|
+
const G2 = "\x1B[32m";
|
|
990
|
+
const X = "\x1B[31m";
|
|
991
|
+
const R2 = "\x1B[0m";
|
|
992
|
+
const sessions = listRoomSessions();
|
|
993
|
+
if (sessions.length === 0) {
|
|
994
|
+
console.log(" No saved rooms. Run: apiary room create");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
console.log("");
|
|
998
|
+
for (const s of sessions) {
|
|
999
|
+
const alive = isServerAlive(s);
|
|
1000
|
+
const status = alive ? `${G2}running${R2} ${D2}(PID ${s.pid})${R2}` : `${X}stopped${R2}`;
|
|
1001
|
+
const lastActive = s.lastActive ? `${D2}last active: ${formatRelativeTime(s.lastActive)}${R2}` : "";
|
|
1002
|
+
console.log(` ${Y2}${B2}${s.roomName}${R2} ${status} ${lastActive}`);
|
|
1003
|
+
if (s.participants && s.participants.length > 0) {
|
|
1004
|
+
const names = s.participants.map((p) => `${p.alias} ${D2}[${p.role}]${R2}`).join(" ");
|
|
1005
|
+
console.log(` ${D2}Participants:${R2} ${names}`);
|
|
1006
|
+
} else {
|
|
1007
|
+
console.log(` ${D2}(no participant metadata \u2014 created with older apiary)${R2}`);
|
|
1008
|
+
}
|
|
1009
|
+
console.log(` ${D2}Resume:${R2} ${C2}apiary room resume ${s.roomName}${R2}`);
|
|
1010
|
+
if (alive) {
|
|
1011
|
+
const shareBase = s.publicUrl !== s.serverUrl ? s.publicUrl : s.serverUrl;
|
|
1012
|
+
const joinUrl = buildShareUrl(shareBase, s.memberToken);
|
|
1013
|
+
console.log(` ${D2}Join:${R2} ${C2}${joinUrl}${R2}`);
|
|
1014
|
+
}
|
|
1015
|
+
console.log("");
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async function roomStop(name, options = {}) {
|
|
1019
|
+
const Y2 = "\x1B[33m";
|
|
1020
|
+
const B2 = "\x1B[1m";
|
|
1021
|
+
const G2 = "\x1B[32m";
|
|
1022
|
+
const D2 = "\x1B[2m";
|
|
1023
|
+
const R2 = "\x1B[0m";
|
|
1024
|
+
const session = options.session ?? listRoomSessions().find((s) => s.roomName === name);
|
|
1025
|
+
if (!session) {
|
|
1026
|
+
console.error(` No saved session for room "${name}".`);
|
|
1027
|
+
console.error(` Run: apiary room list to see saved rooms.`);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
const alive = isServerAlive(session);
|
|
1031
|
+
if (!alive && options.quietIfDead) {
|
|
1032
|
+
removeRoomSession(name);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
console.log(`
|
|
1036
|
+
Stopping ${Y2}${B2}${name}${R2}...
|
|
1037
|
+
`);
|
|
1038
|
+
if (session.participants && tmuxAvailable()) {
|
|
1039
|
+
for (const p of session.participants) {
|
|
1040
|
+
const tsName = `apiary_${p.alias}`;
|
|
1041
|
+
if (tmuxSessionExists(tsName)) {
|
|
1042
|
+
tmuxKillSession(tsName);
|
|
1043
|
+
console.log(` ${G2}\u2713${R2} ${p.alias} ${D2}agent stopped${R2}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (alive) {
|
|
1048
|
+
try {
|
|
1049
|
+
process.kill(session.pid, "SIGTERM");
|
|
1050
|
+
console.log(` ${G2}\u2713${R2} server ${D2}(PID ${session.pid}) stopped${R2}`);
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
removeRoomSession(name);
|
|
1055
|
+
console.log(`
|
|
1056
|
+
Room "${name}" stopped. Start fresh: apiary room create
|
|
1057
|
+
`);
|
|
1058
|
+
}
|
|
1059
|
+
function isServerAlive(session) {
|
|
1060
|
+
try {
|
|
1061
|
+
process.kill(session.pid, 0);
|
|
1062
|
+
return true;
|
|
1063
|
+
} catch {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function parseDuration(input) {
|
|
1068
|
+
if (!input) return null;
|
|
1069
|
+
const m = input.trim().match(/^(\d+)\s*(d|h|m)?$/i);
|
|
1070
|
+
if (!m) return null;
|
|
1071
|
+
const n = parseInt(m[1], 10);
|
|
1072
|
+
const unit = (m[2] ?? "d").toLowerCase();
|
|
1073
|
+
if (unit === "d") return n * 24 * 60 * 60 * 1e3;
|
|
1074
|
+
if (unit === "h") return n * 60 * 60 * 1e3;
|
|
1075
|
+
if (unit === "m") return n * 60 * 1e3;
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
function formatRelativeTime(ts) {
|
|
1079
|
+
const diff = Date.now() - ts;
|
|
1080
|
+
const mins = Math.floor(diff / 6e4);
|
|
1081
|
+
if (mins < 1) return "just now";
|
|
1082
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1083
|
+
const hours = Math.floor(mins / 60);
|
|
1084
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1085
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
1086
|
+
}
|
|
1087
|
+
export {
|
|
1088
|
+
applyDefaultModes,
|
|
1089
|
+
isDaemonResult,
|
|
1090
|
+
isServerAlive,
|
|
1091
|
+
parseAliasSpec,
|
|
1092
|
+
roomCreate,
|
|
1093
|
+
roomList,
|
|
1094
|
+
roomResume,
|
|
1095
|
+
roomStop,
|
|
1096
|
+
spawnDaemonServer,
|
|
1097
|
+
writeInvite
|
|
1098
|
+
};
|
|
1099
|
+
//# sourceMappingURL=room-Z7KDRVYD.js.map
|