@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,34 @@
1
+ import {
2
+ EventMultiplexer,
3
+ EventProcessor,
4
+ LocalRoomDataSource,
5
+ RefMap,
6
+ RemoteRoomDataSource,
7
+ SseMultiplexer,
8
+ StoopsEngagement,
9
+ classifyEvent,
10
+ createRuntimeMcpServer
11
+ } from "../chunk-ZEFRNAX6.js";
12
+ import {
13
+ contentPartsToString,
14
+ formatEvent,
15
+ messageRef,
16
+ participantLabel
17
+ } from "../chunk-QL66XSDB.js";
18
+ import "../chunk-BZ4VDWC2.js";
19
+ export {
20
+ EventMultiplexer,
21
+ EventProcessor,
22
+ LocalRoomDataSource,
23
+ RefMap,
24
+ RemoteRoomDataSource,
25
+ SseMultiplexer,
26
+ StoopsEngagement,
27
+ classifyEvent,
28
+ contentPartsToString,
29
+ createRuntimeMcpServer,
30
+ formatEvent,
31
+ messageRef,
32
+ participantLabel
33
+ };
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,184 @@
1
+ // src/cli/update.ts
2
+ import { execFileSync, spawn } from "child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
7
+ var __filename = fileURLToPath(import.meta.url);
8
+ function findRepoRoot(start) {
9
+ let dir = dirname(start);
10
+ for (let i = 0; i < 6; i++) {
11
+ if (existsSync(join(dir, "package.json"))) return dir;
12
+ const parent = dirname(dir);
13
+ if (parent === dir) break;
14
+ dir = parent;
15
+ }
16
+ return dirname(start);
17
+ }
18
+ var REPO_ROOT = findRepoRoot(__filename);
19
+ var APIARY_DIR = join(homedir(), ".apiary");
20
+ var CACHE_PATH = join(APIARY_DIR, "version-check.json");
21
+ var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
22
+ function readCache() {
23
+ try {
24
+ return JSON.parse(readFileSync(CACHE_PATH, "utf-8"));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function writeCache(cache) {
30
+ try {
31
+ if (!existsSync(APIARY_DIR)) mkdirSync(APIARY_DIR, { recursive: true });
32
+ writeFileSync(CACHE_PATH, JSON.stringify(cache));
33
+ } catch {
34
+ }
35
+ }
36
+ function isGitRepo() {
37
+ return existsSync(join(REPO_ROOT, ".git"));
38
+ }
39
+ function getLocalHead() {
40
+ try {
41
+ return execFileSync("git", ["rev-parse", "HEAD"], { cwd: REPO_ROOT, encoding: "utf-8" }).trim();
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function getLocalVersion() {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(join(REPO_ROOT, "package.json"), "utf-8"));
49
+ return pkg.version ?? "unknown";
50
+ } catch {
51
+ return "unknown";
52
+ }
53
+ }
54
+ function readPackageName() {
55
+ try {
56
+ const pkg = JSON.parse(readFileSync(join(REPO_ROOT, "package.json"), "utf-8"));
57
+ return typeof pkg.name === "string" ? pkg.name : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ function printUpdateNotice() {
63
+ try {
64
+ const cache = readCache();
65
+ if (!cache) return;
66
+ if (cache.localHead === cache.remoteHead) return;
67
+ const localHead = getLocalHead();
68
+ if (!localHead || localHead === cache.remoteHead) return;
69
+ const v = getLocalVersion();
70
+ console.error(`\x1B[33m A new version of apiary is available.\x1B[0m \x1B[2m(current: v${v})\x1B[0m`);
71
+ console.error(`\x1B[2m Run \x1B[36mapiary update\x1B[0m\x1B[2m to upgrade.\x1B[0m
72
+ `);
73
+ } catch {
74
+ }
75
+ }
76
+ function checkForUpdate() {
77
+ try {
78
+ if (!isGitRepo()) return;
79
+ const cache = readCache();
80
+ if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) return;
81
+ const localHead = getLocalHead();
82
+ if (!localHead) return;
83
+ const child = spawn("git", ["ls-remote", "origin", "HEAD"], {
84
+ cwd: REPO_ROOT,
85
+ stdio: ["ignore", "pipe", "ignore"],
86
+ detached: true
87
+ });
88
+ let stdout = "";
89
+ child.stdout.on("data", (chunk) => {
90
+ stdout += chunk.toString();
91
+ });
92
+ child.on("close", (code) => {
93
+ if (code !== 0) return;
94
+ const remoteHead = stdout.split(/\s/)[0]?.trim();
95
+ if (!remoteHead) return;
96
+ writeCache({ checkedAt: Date.now(), localHead, remoteHead });
97
+ });
98
+ child.stdout.unref?.();
99
+ child.unref();
100
+ } catch {
101
+ }
102
+ }
103
+ async function runUpdate(targetVersion) {
104
+ if (!isGitRepo()) {
105
+ const pkgName = readPackageName() ?? "@gorlitzer-labs/apiary";
106
+ console.log(`\x1B[36m Running: npm update -g ${pkgName}\x1B[0m`);
107
+ try {
108
+ execFileSync("npm", ["update", "-g", pkgName], { stdio: "inherit" });
109
+ } catch {
110
+ console.error("\n\x1B[31m npm update failed.\x1B[0m");
111
+ process.exit(1);
112
+ }
113
+ try {
114
+ unlinkSync(CACHE_PATH);
115
+ } catch {
116
+ }
117
+ console.log("\n\x1B[32m Updated apiary.\x1B[0m");
118
+ return;
119
+ }
120
+ const fromVersion = getLocalVersion();
121
+ const fromHead = getLocalHead();
122
+ console.log(`\x1B[2m Current version: v${fromVersion}\x1B[0m`);
123
+ if (targetVersion) {
124
+ const tag = targetVersion.startsWith("v") ? targetVersion : `v${targetVersion}`;
125
+ console.log(`\x1B[36m Switching to ${tag}...\x1B[0m`);
126
+ try {
127
+ execFileSync("git", ["fetch", "--tags"], { cwd: REPO_ROOT, stdio: "inherit" });
128
+ execFileSync("git", ["checkout", tag], { cwd: REPO_ROOT, stdio: "inherit" });
129
+ } catch {
130
+ console.error(`
131
+ \x1B[31m Version ${tag} not found.\x1B[0m`);
132
+ process.exit(1);
133
+ }
134
+ } else {
135
+ console.log(`\x1B[36m Pulling latest...\x1B[0m`);
136
+ try {
137
+ execFileSync("git", ["pull", "--ff-only"], { cwd: REPO_ROOT, stdio: "inherit" });
138
+ } catch {
139
+ console.error("\n\x1B[31m git pull failed.\x1B[0m Check for uncommitted changes or merge conflicts.");
140
+ process.exit(1);
141
+ }
142
+ }
143
+ const toHead = getLocalHead();
144
+ if (toHead && fromHead && toHead !== fromHead) {
145
+ try {
146
+ const changed = execFileSync("git", ["diff", `${fromHead}..${toHead}`, "--name-only"], {
147
+ cwd: REPO_ROOT,
148
+ encoding: "utf-8"
149
+ });
150
+ if (changed.includes("package-lock.json") || changed.includes("package.json")) {
151
+ console.log(`\x1B[36m Dependencies changed, installing...\x1B[0m`);
152
+ execFileSync("npm", ["install"], { cwd: REPO_ROOT, stdio: "inherit" });
153
+ }
154
+ } catch {
155
+ }
156
+ }
157
+ console.log(`\x1B[36m Building...\x1B[0m`);
158
+ try {
159
+ execFileSync("npm", ["run", "build"], { cwd: REPO_ROOT, stdio: "inherit" });
160
+ } catch {
161
+ console.error("\n\x1B[31m Build failed.\x1B[0m");
162
+ process.exit(1);
163
+ }
164
+ const toVersion = getLocalVersion();
165
+ try {
166
+ unlinkSync(CACHE_PATH);
167
+ } catch {
168
+ }
169
+ console.log("");
170
+ if (fromVersion !== toVersion) {
171
+ console.log(`\x1B[32m Updated apiary: v${fromVersion} \u2192 v${toVersion}\x1B[0m`);
172
+ } else if (fromHead !== toHead) {
173
+ console.log(`\x1B[32m Updated apiary to latest (v${toVersion})\x1B[0m`);
174
+ } else {
175
+ console.log(`\x1B[33m Already up to date (v${fromVersion})\x1B[0m`);
176
+ }
177
+ }
178
+
179
+ export {
180
+ printUpdateNotice,
181
+ checkForUpdate,
182
+ runUpdate
183
+ };
184
+ //# sourceMappingURL=chunk-5V7QLCGB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/update.ts"],"sourcesContent":["/**\n * Self-update system for apiary CLI.\n *\n * - `printUpdateNotice()` — sync, reads cache, prints one-liner if update available\n * - `checkForUpdate()` — async fire-and-forget, compares local/remote HEAD\n * - `runUpdate()` — the `apiary update` command: git pull + build\n */\n\nimport { execFileSync, spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, resolve, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n// ── Paths ───────────────────────────────────────────────────────────────────\n\nconst __filename = fileURLToPath(import.meta.url);\n\n/**\n * Walk up from this file's location until we find a package.json.\n *\n * `src/cli/update.ts` (3 levels deep) and `dist/cli/update.js` (3 levels deep)\n * both correctly resolve via `../..`. But tsup hoists modules into shared\n * chunks at `dist/chunk-<hash>.js` (2 levels deep), so a fixed `../..` would\n * point at the project's PARENT directory instead. Walking up by package.json\n * works regardless of where the bundler stashes us.\n */\nfunction findRepoRoot(start: string): string {\n let dir = dirname(start);\n for (let i = 0; i < 6; i++) {\n if (existsSync(join(dir, \"package.json\"))) return dir;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return dirname(start);\n}\n\nconst REPO_ROOT = findRepoRoot(__filename);\nconst APIARY_DIR = join(homedir(), \".apiary\");\nconst CACHE_PATH = join(APIARY_DIR, \"version-check.json\");\nconst CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\n\n// ── Cache ───────────────────────────────────────────────────────────────────\n\ninterface VersionCache {\n checkedAt: number;\n localHead: string;\n remoteHead: string;\n}\n\nfunction readCache(): VersionCache | null {\n try {\n return JSON.parse(readFileSync(CACHE_PATH, \"utf-8\"));\n } catch {\n return null;\n }\n}\n\nfunction writeCache(cache: VersionCache): void {\n try {\n if (!existsSync(APIARY_DIR)) mkdirSync(APIARY_DIR, { recursive: true });\n writeFileSync(CACHE_PATH, JSON.stringify(cache));\n } catch { /* best effort */ }\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction isGitRepo(): boolean {\n return existsSync(join(REPO_ROOT, \".git\"));\n}\n\nfunction getLocalHead(): string | null {\n try {\n return execFileSync(\"git\", [\"rev-parse\", \"HEAD\"], { cwd: REPO_ROOT, encoding: \"utf-8\" }).trim();\n } catch {\n return null;\n }\n}\n\nfunction getLocalVersion(): string {\n try {\n const pkg = JSON.parse(readFileSync(join(REPO_ROOT, \"package.json\"), \"utf-8\"));\n return pkg.version ?? \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nfunction readPackageName(): string | null {\n try {\n const pkg = JSON.parse(readFileSync(join(REPO_ROOT, \"package.json\"), \"utf-8\"));\n return typeof pkg.name === \"string\" ? pkg.name : null;\n } catch {\n return null;\n }\n}\n\n// ── Print update notice (sync, called on startup) ───────────────────────────\n\nexport function printUpdateNotice(): void {\n try {\n const cache = readCache();\n if (!cache) return;\n if (cache.localHead === cache.remoteHead) return;\n\n const localHead = getLocalHead();\n if (!localHead || localHead === cache.remoteHead) return;\n\n const v = getLocalVersion();\n console.error(`\\x1b[33m A new version of apiary is available.\\x1b[0m \\x1b[2m(current: v${v})\\x1b[0m`);\n console.error(`\\x1b[2m Run \\x1b[36mapiary update\\x1b[0m\\x1b[2m to upgrade.\\x1b[0m\\n`);\n } catch { /* never break the CLI */ }\n}\n\n// ── Check for update (non-blocking, fire-and-forget) ────────────────────────\n\nexport function checkForUpdate(): void {\n try {\n if (!isGitRepo()) return;\n\n const cache = readCache();\n if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) return;\n\n const localHead = getLocalHead();\n if (!localHead) return;\n\n // Fire off git ls-remote in background\n const child = spawn(\"git\", [\"ls-remote\", \"origin\", \"HEAD\"], {\n cwd: REPO_ROOT,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n detached: true,\n });\n\n let stdout = \"\";\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.on(\"close\", (code) => {\n if (code !== 0) return;\n const remoteHead = stdout.split(/\\s/)[0]?.trim();\n if (!remoteHead) return;\n writeCache({ checkedAt: Date.now(), localHead, remoteHead });\n });\n\n // unref() on the process handle doesn't unref the stdio pipe streams —\n // an open stdout pipe will keep Node's event loop alive after the CLI\n // command is otherwise done. Unref both so the CLI can exit promptly.\n // child.stdout is typed Readable but is a Socket at runtime.\n (child.stdout as unknown as { unref?: () => void }).unref?.();\n child.unref();\n } catch { /* never break the CLI */ }\n}\n\n// ── Run update (the `apiary update` command) ────────────────────────────────\n\nexport async function runUpdate(targetVersion?: string): Promise<void> {\n if (!isGitRepo()) {\n // Real npm install (not a symlinked git checkout) — run npm update directly.\n const pkgName = readPackageName() ?? \"@gorlitzer-labs/apiary\";\n console.log(`\\x1b[36m Running: npm update -g ${pkgName}\\x1b[0m`);\n try {\n execFileSync(\"npm\", [\"update\", \"-g\", pkgName], { stdio: \"inherit\" });\n } catch {\n console.error(\"\\n\\x1b[31m npm update failed.\\x1b[0m\");\n process.exit(1);\n }\n try { unlinkSync(CACHE_PATH); } catch { /* fine */ }\n console.log(\"\\n\\x1b[32m Updated apiary.\\x1b[0m\");\n return;\n }\n\n const fromVersion = getLocalVersion();\n const fromHead = getLocalHead();\n\n console.log(`\\x1b[2m Current version: v${fromVersion}\\x1b[0m`);\n\n if (targetVersion) {\n // Checkout a specific version tag\n const tag = targetVersion.startsWith(\"v\") ? targetVersion : `v${targetVersion}`;\n console.log(`\\x1b[36m Switching to ${tag}...\\x1b[0m`);\n try {\n execFileSync(\"git\", [\"fetch\", \"--tags\"], { cwd: REPO_ROOT, stdio: \"inherit\" });\n execFileSync(\"git\", [\"checkout\", tag], { cwd: REPO_ROOT, stdio: \"inherit\" });\n } catch {\n console.error(`\\n\\x1b[31m Version ${tag} not found.\\x1b[0m`);\n process.exit(1);\n }\n } else {\n console.log(`\\x1b[36m Pulling latest...\\x1b[0m`);\n try {\n execFileSync(\"git\", [\"pull\", \"--ff-only\"], { cwd: REPO_ROOT, stdio: \"inherit\" });\n } catch {\n console.error(\"\\n\\x1b[31m git pull failed.\\x1b[0m Check for uncommitted changes or merge conflicts.\");\n process.exit(1);\n }\n }\n\n // Check if deps changed\n const toHead = getLocalHead();\n if (toHead && fromHead && toHead !== fromHead) {\n try {\n const changed = execFileSync(\"git\", [\"diff\", `${fromHead}..${toHead}`, \"--name-only\"], {\n cwd: REPO_ROOT,\n encoding: \"utf-8\",\n });\n if (changed.includes(\"package-lock.json\") || changed.includes(\"package.json\")) {\n console.log(`\\x1b[36m Dependencies changed, installing...\\x1b[0m`);\n execFileSync(\"npm\", [\"install\"], { cwd: REPO_ROOT, stdio: \"inherit\" });\n }\n } catch { /* proceed to build anyway */ }\n }\n\n console.log(`\\x1b[36m Building...\\x1b[0m`);\n try {\n execFileSync(\"npm\", [\"run\", \"build\"], { cwd: REPO_ROOT, stdio: \"inherit\" });\n } catch {\n console.error(\"\\n\\x1b[31m Build failed.\\x1b[0m\");\n process.exit(1);\n }\n\n const toVersion = getLocalVersion();\n\n // Clear cache\n try { unlinkSync(CACHE_PATH); } catch { /* fine */ }\n\n console.log(\"\");\n if (fromVersion !== toVersion) {\n console.log(`\\x1b[32m Updated apiary: v${fromVersion} → v${toVersion}\\x1b[0m`);\n } else if (fromHead !== toHead) {\n console.log(`\\x1b[32m Updated apiary to latest (v${toVersion})\\x1b[0m`);\n } else {\n console.log(`\\x1b[33m Already up to date (v${fromVersion})\\x1b[0m`);\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,152 @@
1
+ import {
2
+ TIER_ORDER
3
+ } from "./chunk-QL66XSDB.js";
4
+
5
+ // src/cli/auth.ts
6
+ import { randomBytes } from "crypto";
7
+ var DEFAULT_SESSION_TTL = 24 * 60 * 60 * 1e3;
8
+ var DEFAULT_SHARE_TTL = 24 * 60 * 60 * 1e3;
9
+ var TokenManager = class {
10
+ /** share token → share data */
11
+ _shareTokens = /* @__PURE__ */ new Map();
12
+ /** session token → participant data */
13
+ _sessionTokens = /* @__PURE__ */ new Map();
14
+ _sessionTtlMs;
15
+ _shareTtlMs;
16
+ constructor(options) {
17
+ this._sessionTtlMs = options?.sessionTtlMs ?? DEFAULT_SESSION_TTL;
18
+ this._shareTtlMs = options?.shareTtlMs ?? DEFAULT_SHARE_TTL;
19
+ }
20
+ /**
21
+ * Generate a share token at the given authority tier.
22
+ * Callers can only generate tokens at their own tier or below.
23
+ *
24
+ * `neverExpires` is for the room's OWN founding pair, not for links handed
25
+ * to anybody. Those two tokens are written into ~/.apiary/sessions and are
26
+ * the only recorded way back into a running room, so expiring them locks the
27
+ * owner out of their own live server: observed at 26 hours, with a room
28
+ * still serving, agents still working inside it, and `apiary room resume`
29
+ * answering "Invalid share token" with no hint that time was the cause.
30
+ *
31
+ * A TTL there protected nothing — the token sits in a file already readable
32
+ * only by its owner — while the thing it broke was the operator's way in. A
33
+ * link generated by /share is a genuine hand-out and still expires.
34
+ */
35
+ generateShareToken(callerAuthority, targetAuthority, opts) {
36
+ if (!canGrant(callerAuthority, targetAuthority)) return null;
37
+ const token = randomBytes(16).toString("hex");
38
+ const now = Date.now();
39
+ this._shareTokens.set(token, {
40
+ authority: targetAuthority,
41
+ createdAt: now,
42
+ expiresAt: opts?.neverExpires === true ? null : now + this._shareTtlMs
43
+ });
44
+ return token;
45
+ }
46
+ /** Validate a share token and return its authority level. Deletes if expired. */
47
+ validateShareToken(token) {
48
+ const data = this._shareTokens.get(token);
49
+ if (!data) return null;
50
+ if (data.expiresAt !== null && Date.now() > data.expiresAt) {
51
+ this._shareTokens.delete(token);
52
+ return null;
53
+ }
54
+ return data.authority;
55
+ }
56
+ /** Create a session token for a participant. */
57
+ createSessionToken(participantId, authority) {
58
+ const token = randomBytes(16).toString("hex");
59
+ const now = Date.now();
60
+ this._sessionTokens.set(token, {
61
+ participantId,
62
+ authority,
63
+ createdAt: now,
64
+ expiresAt: now + this._sessionTtlMs
65
+ });
66
+ return token;
67
+ }
68
+ /** Validate a session token and return participant data. Deletes if expired. */
69
+ validateSessionToken(token) {
70
+ const data = this._sessionTokens.get(token);
71
+ if (!data) return null;
72
+ if (Date.now() > data.expiresAt) {
73
+ this._sessionTokens.delete(token);
74
+ return null;
75
+ }
76
+ return data;
77
+ }
78
+ /** Revoke a session token (on disconnect). */
79
+ revokeSessionToken(token) {
80
+ this._sessionTokens.delete(token);
81
+ }
82
+ /** Update the authority level for an existing session. */
83
+ updateSessionAuthority(token, newAuthority) {
84
+ const data = this._sessionTokens.get(token);
85
+ if (!data) return false;
86
+ data.authority = newAuthority;
87
+ return true;
88
+ }
89
+ /** Find a session token by participant ID (for cleanup). */
90
+ findSessionByParticipant(participantId) {
91
+ for (const [token, data] of this._sessionTokens) {
92
+ if (data.participantId === participantId) return token;
93
+ }
94
+ return null;
95
+ }
96
+ /** Rotate a session token: create new with same participant data, delete old. */
97
+ rotateSessionToken(oldToken) {
98
+ const data = this._sessionTokens.get(oldToken);
99
+ if (!data) return null;
100
+ if (Date.now() > data.expiresAt) {
101
+ this._sessionTokens.delete(oldToken);
102
+ return null;
103
+ }
104
+ this._sessionTokens.delete(oldToken);
105
+ const newToken = randomBytes(16).toString("hex");
106
+ const now = Date.now();
107
+ const newData = {
108
+ participantId: data.participantId,
109
+ authority: data.authority,
110
+ createdAt: now,
111
+ expiresAt: now + this._sessionTtlMs
112
+ };
113
+ this._sessionTokens.set(newToken, newData);
114
+ return { newToken, data: newData };
115
+ }
116
+ /** Sweep expired tokens from both maps. */
117
+ pruneExpired() {
118
+ const now = Date.now();
119
+ for (const [token, data] of this._shareTokens) {
120
+ if (data.expiresAt !== null && now > data.expiresAt) this._shareTokens.delete(token);
121
+ }
122
+ for (const [token, data] of this._sessionTokens) {
123
+ if (now > data.expiresAt) this._sessionTokens.delete(token);
124
+ }
125
+ }
126
+ };
127
+ function canGrant(callerLevel, targetLevel) {
128
+ if (targetLevel === "admin" || targetLevel === "product_owner") {
129
+ return callerLevel === "admin";
130
+ }
131
+ return TIER_ORDER[callerLevel] >= TIER_ORDER[targetLevel];
132
+ }
133
+ function buildShareUrl(baseUrl, token) {
134
+ const url = new URL(baseUrl);
135
+ url.searchParams.set("token", token);
136
+ return url.toString();
137
+ }
138
+ function extractToken(url) {
139
+ try {
140
+ const parsed = new URL(url);
141
+ return parsed.searchParams.get("token");
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ export {
148
+ TokenManager,
149
+ buildShareUrl,
150
+ extractToken
151
+ };
152
+ //# sourceMappingURL=chunk-AYQJIAR5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/auth.ts"],"sourcesContent":["/**\n * Auth token system for apiary share links and session management.\n *\n * Two token types:\n * - Share tokens — embedded in URLs, map to an authority tier.\n * Anyone with the link joins at that tier.\n * - Session tokens — issued on join, identify a participant + authority.\n * Used for all subsequent API calls.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { AuthorityLevel } from \"../core/types.js\";\nimport { TIER_ORDER } from \"../core/authority.js\";\n\ninterface SessionData {\n participantId: string;\n authority: AuthorityLevel;\n createdAt: number;\n expiresAt: number;\n}\n\ninterface ShareTokenData {\n authority: AuthorityLevel;\n createdAt: number;\n /** null = never expires. Only the room's own founding pair gets that. */\n expiresAt: number | null;\n}\n\nexport interface TokenManagerOptions {\n /** Session token TTL in ms. Default: 24 hours. */\n sessionTtlMs?: number;\n /** Share token TTL in ms. Default: 1 hour. */\n shareTtlMs?: number;\n}\n\nconst DEFAULT_SESSION_TTL = 24 * 60 * 60 * 1000; // 24h\nconst DEFAULT_SHARE_TTL = 24 * 60 * 60 * 1000; // 24h\n\nexport class TokenManager {\n /** share token → share data */\n private _shareTokens = new Map<string, ShareTokenData>();\n /** session token → participant data */\n private _sessionTokens = new Map<string, SessionData>();\n\n private _sessionTtlMs: number;\n private _shareTtlMs: number;\n\n constructor(options?: TokenManagerOptions) {\n this._sessionTtlMs = options?.sessionTtlMs ?? DEFAULT_SESSION_TTL;\n this._shareTtlMs = options?.shareTtlMs ?? DEFAULT_SHARE_TTL;\n }\n\n /**\n * Generate a share token at the given authority tier.\n * Callers can only generate tokens at their own tier or below.\n *\n * `neverExpires` is for the room's OWN founding pair, not for links handed\n * to anybody. Those two tokens are written into ~/.apiary/sessions and are\n * the only recorded way back into a running room, so expiring them locks the\n * owner out of their own live server: observed at 26 hours, with a room\n * still serving, agents still working inside it, and `apiary room resume`\n * answering \"Invalid share token\" with no hint that time was the cause.\n *\n * A TTL there protected nothing — the token sits in a file already readable\n * only by its owner — while the thing it broke was the operator's way in. A\n * link generated by /share is a genuine hand-out and still expires.\n */\n generateShareToken(\n callerAuthority: AuthorityLevel,\n targetAuthority: AuthorityLevel,\n opts?: { neverExpires?: boolean },\n ): string | null {\n if (!canGrant(callerAuthority, targetAuthority)) return null;\n const token = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n this._shareTokens.set(token, {\n authority: targetAuthority,\n createdAt: now,\n expiresAt: opts?.neverExpires === true ? null : now + this._shareTtlMs,\n });\n return token;\n }\n\n /** Validate a share token and return its authority level. Deletes if expired. */\n validateShareToken(token: string): AuthorityLevel | null {\n const data = this._shareTokens.get(token);\n if (!data) return null;\n // A null expiry is the room's own founding token: it lives as long as the\n // room does, because it IS the room's credential rather than a hand-out.\n if (data.expiresAt !== null && Date.now() > data.expiresAt) {\n this._shareTokens.delete(token);\n return null;\n }\n return data.authority;\n }\n\n /** Create a session token for a participant. */\n createSessionToken(participantId: string, authority: AuthorityLevel): string {\n const token = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n this._sessionTokens.set(token, {\n participantId,\n authority,\n createdAt: now,\n expiresAt: now + this._sessionTtlMs,\n });\n return token;\n }\n\n /** Validate a session token and return participant data. Deletes if expired. */\n validateSessionToken(token: string): SessionData | null {\n const data = this._sessionTokens.get(token);\n if (!data) return null;\n if (Date.now() > data.expiresAt) {\n this._sessionTokens.delete(token);\n return null;\n }\n return data;\n }\n\n /** Revoke a session token (on disconnect). */\n revokeSessionToken(token: string): void {\n this._sessionTokens.delete(token);\n }\n\n /** Update the authority level for an existing session. */\n updateSessionAuthority(token: string, newAuthority: AuthorityLevel): boolean {\n const data = this._sessionTokens.get(token);\n if (!data) return false;\n data.authority = newAuthority;\n return true;\n }\n\n /** Find a session token by participant ID (for cleanup). */\n findSessionByParticipant(participantId: string): string | null {\n for (const [token, data] of this._sessionTokens) {\n if (data.participantId === participantId) return token;\n }\n return null;\n }\n\n /** Rotate a session token: create new with same participant data, delete old. */\n rotateSessionToken(oldToken: string): { newToken: string; data: SessionData } | null {\n const data = this._sessionTokens.get(oldToken);\n if (!data) return null;\n if (Date.now() > data.expiresAt) {\n this._sessionTokens.delete(oldToken);\n return null;\n }\n this._sessionTokens.delete(oldToken);\n const newToken = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n const newData: SessionData = {\n participantId: data.participantId,\n authority: data.authority,\n createdAt: now,\n expiresAt: now + this._sessionTtlMs,\n };\n this._sessionTokens.set(newToken, newData);\n return { newToken, data: newData };\n }\n\n /** Sweep expired tokens from both maps. */\n pruneExpired(): void {\n const now = Date.now();\n for (const [token, data] of this._shareTokens) {\n // `now > null` is true in JS — null coerces to 0 — so an unguarded sweep\n // would delete every never-expiring token on its first pass and undo the\n // whole point of them.\n if (data.expiresAt !== null && now > data.expiresAt) this._shareTokens.delete(token);\n }\n for (const [token, data] of this._sessionTokens) {\n if (now > data.expiresAt) this._sessionTokens.delete(token);\n }\n }\n}\n\n/**\n * Can a caller at `callerLevel` grant a share token at `targetLevel`?\n * Admin/product_owner links can only be granted by admins.\n * Everyone else can grant links at or below their own tier (excluding PO/admin).\n */\nfunction canGrant(callerLevel: AuthorityLevel, targetLevel: AuthorityLevel): boolean {\n if (targetLevel === \"admin\" || targetLevel === \"product_owner\") {\n return callerLevel === \"admin\";\n }\n return TIER_ORDER[callerLevel] >= TIER_ORDER[targetLevel];\n}\n\n/** Build a share URL from a base URL and token. */\nexport function buildShareUrl(baseUrl: string, token: string): string {\n const url = new URL(baseUrl);\n url.searchParams.set(\"token\", token);\n return url.toString();\n}\n\n/** Extract a token from a share URL. */\nexport function extractToken(url: string): string | null {\n try {\n const parsed = new URL(url);\n return parsed.searchParams.get(\"token\");\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,76 @@
1
+ // src/core/types.ts
2
+ import { z } from "zod";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ var EventCategory = {
5
+ MESSAGE: "MESSAGE",
6
+ PRESENCE: "PRESENCE",
7
+ ACTIVITY: "ACTIVITY",
8
+ MENTION: "MENTION"
9
+ };
10
+ var AttachmentSchema = z.discriminatedUnion("type", [
11
+ z.object({
12
+ type: z.literal("path"),
13
+ id: z.string(),
14
+ name: z.string(),
15
+ mime_type: z.string(),
16
+ size: z.number().optional(),
17
+ path: z.string()
18
+ }),
19
+ z.object({
20
+ type: z.literal("upload"),
21
+ id: z.string(),
22
+ name: z.string(),
23
+ mime_type: z.string(),
24
+ size: z.number(),
25
+ url: z.string()
26
+ })
27
+ ]);
28
+ var MessageSchema = z.object({
29
+ id: z.string().default(() => uuidv4()),
30
+ room_id: z.string(),
31
+ sender_id: z.string(),
32
+ sender_name: z.string(),
33
+ content: z.string(),
34
+ reply_to_id: z.string().nullable().default(null),
35
+ recipients: z.array(z.string()).default([]),
36
+ attachments: z.array(AttachmentSchema).default([]),
37
+ image_url: z.string().nullable().default(null),
38
+ image_mime_type: z.string().nullable().default(null),
39
+ image_size_bytes: z.number().int().nullable().default(null),
40
+ timestamp: z.date().default(() => /* @__PURE__ */ new Date())
41
+ });
42
+
43
+ // src/core/events.ts
44
+ import { v4 as uuidv42 } from "uuid";
45
+ var EVENT_ROLE = {
46
+ MessageSent: "message",
47
+ Mentioned: "mention",
48
+ Pinged: "ping",
49
+ ParticipantJoined: "ambient",
50
+ ParticipantLeft: "ambient",
51
+ ParticipantKicked: "ambient",
52
+ AuthorityChanged: "ambient",
53
+ ReactionAdded: "ambient",
54
+ ContextCompacted: "ambient",
55
+ MessageEdited: "internal",
56
+ MessageDeleted: "internal",
57
+ ReactionRemoved: "internal",
58
+ StatusChanged: "internal",
59
+ ToolUse: "internal",
60
+ Activity: "internal",
61
+ RoomCleared: "internal",
62
+ RulesChanged: "ambient",
63
+ WhisperNotified: "ambient"
64
+ };
65
+ function createEvent(data) {
66
+ return { id: uuidv42(), timestamp: /* @__PURE__ */ new Date(), ...data };
67
+ }
68
+
69
+ export {
70
+ EventCategory,
71
+ AttachmentSchema,
72
+ MessageSchema,
73
+ EVENT_ROLE,
74
+ createEvent
75
+ };
76
+ //# sourceMappingURL=chunk-BZ4VDWC2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/types.ts","../src/core/events.ts"],"sourcesContent":["/** Core types for apiary — messages, participants, pagination. */\n\nimport { z } from \"zod\";\nimport { v4 as uuidv4 } from \"uuid\";\n\n// ── Event categories ─────────────────────────────────────────────────────────\n\n/**\n * The four top-level categories events are grouped under.\n *\n * Channels subscribe to one or more categories — they only receive events in\n * their subscription set. Use this to filter what an agent or observer sees:\n *\n * - MESSAGE — chat messages and reactions (sent, edited, deleted, reacted)\n * - PRESENCE — participants joining and leaving\n * - ACTIVITY — agent activity (thinking, tool use, mode changes, compaction)\n * - MENTION — direct @mentions delivered only to the mentioned participant\n */\nexport const EventCategory = {\n MESSAGE: \"MESSAGE\",\n PRESENCE: \"PRESENCE\",\n ACTIVITY: \"ACTIVITY\",\n MENTION: \"MENTION\",\n} as const;\n\nexport type EventCategory = (typeof EventCategory)[keyof typeof EventCategory];\n\n// ── Attachment ───────────────────────────────────────────────────────────────\n\n/**\n * A file or image attached to a message. Discriminated union on `type`.\n *\n * - `type: \"path\"` — local filesystem reference, intended for same-machine rooms.\n * Receivers should gracefully handle the case where the path is not accessible\n * on their machine (e.g. multi-machine rooms with a tunnel).\n *\n * - `type: \"upload\"` — server-hosted bytes, fetched via `GET /attachment/:id`\n * with a valid session token. URL uses the server's `publicUrl` (tunnel URL\n * when active, otherwise localhost).\n */\nexport const AttachmentSchema = z.discriminatedUnion(\"type\", [\n z.object({\n type: z.literal(\"path\"),\n id: z.string(),\n name: z.string(),\n mime_type: z.string(),\n size: z.number().optional(),\n path: z.string(),\n }),\n z.object({\n type: z.literal(\"upload\"),\n id: z.string(),\n name: z.string(),\n mime_type: z.string(),\n size: z.number(),\n url: z.string(),\n }),\n]);\n\nexport type Attachment = z.infer<typeof AttachmentSchema>;\n\n// ── Message ──────────────────────────────────────────────────────────────────\n\n/**\n * A chat message. Immutable once stored — edits and deletes are separate events.\n *\n * - `id` — UUID, assigned by the room on creation\n * - `room_id` — the room this message belongs to\n * - `sender_id` — participant ID of the author\n * - `sender_name` — display name at the time of sending (denormalized)\n * - `content` — text body (may be empty if the message is image-only)\n * - `reply_to_id` — if set, this message is a reply to that message ID\n * - `recipients` — if non-empty, this is a whisper: only these participant IDs (plus the sender) can read it\n * - `attachments` — zero or more file/image attachments\n * - `image_url` — legacy single-image URL (kept for backward compat)\n * - `image_mime_type` — MIME type of the legacy image\n * - `image_size_bytes` — size of the legacy image in bytes\n * - `timestamp` — creation time (UTC)\n */\nexport const MessageSchema = z.object({\n id: z.string().default(() => uuidv4()),\n room_id: z.string(),\n sender_id: z.string(),\n sender_name: z.string(),\n content: z.string(),\n reply_to_id: z.string().nullable().default(null),\n recipients: z.array(z.string()).default([]),\n attachments: z.array(AttachmentSchema).default([]),\n image_url: z.string().nullable().default(null),\n image_mime_type: z.string().nullable().default(null),\n image_size_bytes: z.number().int().nullable().default(null),\n timestamp: z.date().default(() => new Date()),\n});\n\nexport type Message = z.infer<typeof MessageSchema>;\n\n// ── Authority ────────────────────────────────────────────────────────────────\n\n/**\n * Authority level — what a participant is allowed to do.\n *\n * - `admin` — full control: kick, promote, clear, tunnel, generate all links\n * - `product_owner` — can mute/unmute members, set engagement modes, generate member/guest links\n * - `member` — can send messages, set own mode, generate member/guest links\n * - `guest` — read-only: can catch up and search, but can't send or act\n *\n * Set on join via share token. Admins can change authority mid-session via /promote, /demote, /mute, /unmute.\n */\nexport type AuthorityLevel = \"admin\" | \"product_owner\" | \"member\" | \"guest\";\n\n// ── Participant ───────────────────────────────────────────────────────────────\n\n/** Whether a participant is a human or an agent. */\nexport type ParticipantType = \"human\" | \"agent\";\n\n/**\n * A participant in a room.\n *\n * - `id` — stable unique ID across all rooms and sessions\n * - `name` — display name (mutable — participants can rename)\n * - `status` — current presence status (\"online\", \"offline\", etc.)\n * - `type` — \"human\" or \"agent\"\n * - `identifier` — optional stable @-mention slug, e.g. \"my-agent\".\n * Unlike `name`, this never changes on rename.\n * Used for @-mention matching in addition to the display name.\n * Not all participants have one — guests and anonymous users\n * typically don't.\n * - `authority` — optional authority level. Defaults to \"member\" if unset.\n */\nexport interface Participant {\n id: string;\n name: string;\n status: string;\n type: ParticipantType;\n identifier?: string;\n authority?: AuthorityLevel;\n}\n\n// ── Pagination ────────────────────────────────────────────────────────────────\n\n/**\n * A page of results with a cursor for fetching the previous page.\n *\n * All paginated queries return results newest-first. Pass `next_cursor` back\n * to the same query to continue paginating backwards through history.\n *\n * - `items` — results for this page (newest-first)\n * - `next_cursor` — pass this to get the next (older) page; null when exhausted\n * - `has_more` — true if there are older items beyond this page\n */\nexport interface PaginatedResult<T> {\n items: T[];\n next_cursor: string | null;\n has_more: boolean;\n}\n","/**\n * Typed events for rooms — a discriminated union on the `type` field.\n *\n * Every event has:\n * id — UUID assigned at creation\n * type — the discriminant (e.g. \"MessageSent\", \"ParticipantJoined\")\n * category — the EventCategory for subscription filtering\n * room_id — the room this event belongs to\n * participant_id — the participant who caused the event\n * timestamp — UTC creation time\n *\n * Use `createEvent()` to build events — it fills in `id` and `timestamp`.\n * Use `EVENT_ROLE` to classify events by their semantic role.\n */\n\nimport { v4 as uuidv4 } from \"uuid\";\nimport type { AuthorityLevel, Message, Participant } from \"./types.js\";\n\n// ── Base ─────────────────────────────────────────────────────────────────────\n\ninterface BaseRoomEvent {\n /** UUID assigned at creation. */\n id: string;\n}\n\n// ── MESSAGE category ─────────────────────────────────────────────────────────\n\n/** Someone sent a message. `message` contains the full content including any image. */\nexport interface MessageSentEvent extends BaseRoomEvent {\n type: \"MessageSent\";\n category: \"MESSAGE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n message: Message;\n}\n\n/** A message's text content was changed. Does not update image attachments. */\nexport interface MessageEditedEvent extends BaseRoomEvent {\n type: \"MessageEdited\";\n category: \"MESSAGE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n message_id: string;\n new_content: string;\n old_content: string;\n}\n\n/** A message was removed. The message ID is preserved for reference but content is gone. */\nexport interface MessageDeletedEvent extends BaseRoomEvent {\n type: \"MessageDeleted\";\n category: \"MESSAGE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n message_id: string;\n}\n\n/** A participant added an emoji reaction to a message. */\nexport interface ReactionAddedEvent extends BaseRoomEvent {\n type: \"ReactionAdded\";\n category: \"MESSAGE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n message_id: string;\n emoji: string;\n}\n\n/** A participant removed their emoji reaction from a message. */\nexport interface ReactionRemovedEvent extends BaseRoomEvent {\n type: \"ReactionRemoved\";\n category: \"MESSAGE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n message_id: string;\n emoji: string;\n}\n\n// ── PRESENCE category ─────────────────────────────────────────────────────────\n\n/**\n * A participant connected to the room.\n *\n * Not emitted for silent connects (`Room.connect(..., silent: true)`).\n * Agents connect silently to avoid polluting the chat with join noise.\n */\nexport interface ParticipantJoinedEvent extends BaseRoomEvent {\n type: \"ParticipantJoined\";\n category: \"PRESENCE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n /** Full participant snapshot at join time. */\n participant: Participant;\n}\n\n/**\n * A participant disconnected from the room.\n *\n * Not emitted for silent disconnects (`channel.disconnect(true)`).\n */\nexport interface ParticipantLeftEvent extends BaseRoomEvent {\n type: \"ParticipantLeft\";\n category: \"PRESENCE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n /** Full participant snapshot at leave time. */\n participant: Participant;\n}\n\n/**\n * A participant's presence status changed.\n *\n * Emitted by the server for all presence transitions:\n * - `online → unresponsive` — first ping timeout threshold missed (reason: \"ping_timeout\")\n * - `unresponsive → online` — SSE reconnect recovery (reason: \"recovered\")\n * - `unresponsive → offline` — sustained timeout, second threshold missed (reason: \"ping_timeout\")\n * - `online → offline` — clean disconnect (reason: \"left\") or admin kick (reason: \"kicked\")\n *\n * The `\"left\"` and `\"kicked\"` variants are companion events emitted alongside\n * `ParticipantLeftEvent` / `ParticipantKickedEvent` for state-machine consumers\n * that only want a single event type for presence tracking.\n *\n * `previous_status: \"offline\"` never appears — re-entry is `ParticipantJoinedEvent`.\n */\nexport interface StatusChangedEvent extends BaseRoomEvent {\n type: \"StatusChanged\";\n category: \"PRESENCE\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n /** Display name of the affected participant. */\n name: string;\n status: \"online\" | \"unresponsive\" | \"offline\";\n previous_status: \"online\" | \"unresponsive\" | \"offline\";\n reason: \"ping_timeout\" | \"recovered\" | \"left\" | \"kicked\";\n}\n\n/**\n * A participant was kicked from the room by an admin.\n *\n * Emitted before the channel disconnect so all participants see it.\n * The subsequent `ParticipantLeft` event is suppressed (silent disconnect).\n */\nexport interface ParticipantKickedEvent extends BaseRoomEvent {\n type: \"ParticipantKicked\";\n category: \"PRESENCE\";\n room_id: string;\n /** The kicked participant. */\n participant_id: string;\n timestamp: Date;\n /** Full participant snapshot at kick time. */\n participant: Participant;\n /** Display name of the admin who kicked them. */\n kicked_by: string;\n}\n\n/**\n * A participant's authority level was changed by an admin.\n *\n * Covers mute (→ guest), unmute (→ member), and promotion (→ admin).\n */\nexport interface AuthorityChangedEvent extends BaseRoomEvent {\n type: \"AuthorityChanged\";\n category: \"PRESENCE\";\n room_id: string;\n /** The affected participant. */\n participant_id: string;\n timestamp: Date;\n /** Full participant snapshot (with updated authority). */\n participant: Participant;\n /** The new authority level. */\n new_authority: AuthorityLevel;\n /** Display name of the admin who made the change. */\n changed_by: string;\n}\n\n// ── ACTIVITY category ─────────────────────────────────────────────────────────\n\n\n/**\n * An agent made an MCP tool call.\n *\n * Emitted twice per tool call: once with `status: \"started\"` (before the call)\n * and once with `status: \"completed\"` (after). Useful for showing live tool\n * progress in a UI.\n */\nexport interface ToolUseEvent extends BaseRoomEvent {\n type: \"ToolUse\";\n category: \"ACTIVITY\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n tool_name: string;\n /** \"started\" before the call, \"completed\" after. */\n status: \"started\" | \"completed\";\n}\n\n/**\n * A generic activity event for platform-specific actions.\n *\n * Used when no specific event type fits. The `action` field identifies the\n * action (e.g. `\"mode_changed\"`), and `detail` carries structured metadata.\n *\n * Current usages:\n * - action: \"mode_changed\", detail: { mode: EngagementMode }\n * Emitted when an agent's engagement mode changes for a room.\n */\nexport interface ActivityEvent extends BaseRoomEvent {\n type: \"Activity\";\n category: \"ACTIVITY\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n action: string;\n detail: Record<string, unknown> | null;\n}\n\n/**\n * An agent's context window was compacted.\n *\n * Fired when the LLM backend summarizes and compresses the conversation history\n * to free up context space. After compaction, the agent re-reads all rooms via\n * `catch_up` to rebuild its working context.\n */\nexport interface ContextCompactedEvent extends BaseRoomEvent {\n type: \"ContextCompacted\";\n category: \"ACTIVITY\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n /** Full participant snapshot (for display name). */\n participant: Participant;\n}\n\n/**\n * An admin cleared the room history.\n *\n * All clients should discard their local event/message state and start fresh.\n * The event itself is broadcast but NOT persisted (storage is already wiped).\n */\nexport interface RoomClearedEvent extends BaseRoomEvent {\n type: \"RoomCleared\";\n category: \"ACTIVITY\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n /** Display name of the admin who cleared the room. */\n cleared_by: string;\n}\n\n/**\n * Room rules were created or updated.\n *\n * Emitted when the rules file changes — either via `PUT /rules` (admin) or\n * when the underlying `~/.apiary/rooms/<name>.rules.md` file is hand-edited\n * (mtime watch). Surfaced by `apiary__catch_up` so long-running agents\n * re-discover the current ruleset after compaction or mid-session edits.\n */\nexport interface RulesChangedEvent extends BaseRoomEvent {\n type: \"RulesChanged\";\n category: \"ACTIVITY\";\n room_id: string;\n participant_id: string;\n timestamp: Date;\n /** The full ruleset in effect after this change. */\n rules: string[];\n /** Display name of the actor who updated the rules; \"[system]\" if hand-edit. */\n updated_by: string;\n}\n\n// ── MENTION category ─────────────────────────────────────────────────────────\n\n/**\n * A participant was @mentioned in a message.\n *\n * Delivered only to the mentioned participant's channel — not broadcast to the\n * room. The `participant_id` is the **recipient** (the person mentioned), not\n * the sender. The sender is in `message.sender_id`.\n *\n * @mention detection is case-insensitive and matches on both `identifier`\n * (e.g. `@my-agent`) and display `name` (e.g. `@Alice`).\n *\n * Agents in standby modes wake up on this event type only.\n */\nexport interface MentionedEvent extends BaseRoomEvent {\n type: \"Mentioned\";\n category: \"MENTION\";\n room_id: string;\n /** The mentioned participant (the recipient of the @mention). */\n participant_id: string;\n timestamp: Date;\n /** The full message that contained the @mention. */\n message: Message;\n}\n\n/**\n * A participant was pinged for a status check.\n *\n * Delivered to the pinged participant's channel and all observers.\n * Unlike @mentions, pings are non-blocking: active agents see them as\n * buffered context (\"content\"), not triggers. Standby agents wake on ping.\n */\nexport interface PingedEvent extends BaseRoomEvent {\n type: \"Pinged\";\n category: \"MENTION\";\n room_id: string;\n /** The pinged participant (the recipient). */\n participant_id: string;\n timestamp: Date;\n /** Participant ID of the person who sent the ping. */\n pinger_id: string;\n /** Display name of the person who sent the ping. */\n pinger_name: string;\n}\n\n/**\n * A whisper was sent to specific participants.\n *\n * Broadcast to all participants NOT in the whisper, so they know a private\n * conversation is happening without seeing the content. Sender and recipients\n * receive the full `MessageSentEvent` instead. Not persisted to storage.\n */\nexport interface WhisperNotifiedEvent extends BaseRoomEvent {\n type: \"WhisperNotified\";\n category: \"ACTIVITY\";\n room_id: string;\n /** The sender's participant ID (follows the common event convention). */\n participant_id: string;\n timestamp: Date;\n sender_id: string;\n sender_name: string;\n recipient_ids: string[];\n recipient_names: string[];\n}\n\n// ── Union ─────────────────────────────────────────────────────────────────────\n\nexport type RoomEvent =\n | MessageSentEvent\n | MessageEditedEvent\n | MessageDeletedEvent\n | ReactionAddedEvent\n | ReactionRemovedEvent\n | ParticipantJoinedEvent\n | ParticipantLeftEvent\n | ParticipantKickedEvent\n | AuthorityChangedEvent\n | StatusChangedEvent\n | ToolUseEvent\n | ActivityEvent\n | MentionedEvent\n | PingedEvent\n | ContextCompactedEvent\n | RoomClearedEvent\n | RulesChangedEvent\n | WhisperNotifiedEvent;\n\n// ── Event roles ───────────────────────────────────────────────────────────────\n\n/**\n * Semantic role of an event, used by the engagement system to decide how to\n * handle it relative to an agent.\n *\n * - \"message\" — a direct chat message; may trigger LLM evaluation\n * - \"mention\" — an @mention directed at a specific participant\n * - \"ambient\" — background activity (joins, leaves, reactions) — buffered as\n * context but doesn't trigger evaluation on its own\n * - \"internal\" — platform bookkeeping (edits, deletes, status changes, agent\n * activity) — always dropped by the engagement system\n */\nexport type EventRole = \"message\" | \"mention\" | \"ping\" | \"ambient\" | \"internal\";\n\n/** Maps each event type to its semantic role for the engagement system. */\nexport const EVENT_ROLE: Record<RoomEvent[\"type\"], EventRole> = {\n MessageSent: \"message\",\n Mentioned: \"mention\",\n Pinged: \"ping\",\n ParticipantJoined: \"ambient\",\n ParticipantLeft: \"ambient\",\n ParticipantKicked: \"ambient\",\n AuthorityChanged: \"ambient\",\n ReactionAdded: \"ambient\",\n ContextCompacted: \"ambient\",\n MessageEdited: \"internal\",\n MessageDeleted: \"internal\",\n ReactionRemoved: \"internal\",\n StatusChanged: \"internal\",\n ToolUse: \"internal\",\n Activity: \"internal\",\n RoomCleared: \"internal\",\n RulesChanged: \"ambient\",\n WhisperNotified: \"ambient\",\n};\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\ntype EventData<T extends RoomEvent> = Omit<T, \"id\" | \"timestamp\"> & {\n id?: string;\n timestamp?: Date;\n};\n\n/**\n * Create a typed room event, filling in `id` (UUID) and `timestamp` (now).\n *\n * @example\n * const event = createEvent<MessageSentEvent>({\n * type: \"MessageSent\",\n * category: \"MESSAGE\",\n * room_id: \"room-1\",\n * participant_id: \"user-1\",\n * message: { ... },\n * });\n */\nexport function createEvent<T extends RoomEvent>(data: EventData<T>): T {\n return { id: uuidv4(), timestamp: new Date(), ...data } as T;\n}\n"],"mappings":";AAEA,SAAS,SAAS;AAClB,SAAS,MAAM,cAAc;AAetB,IAAM,gBAAgB;AAAA,EAC3B,SAAU;AAAA,EACV,UAAU;AAAA,EACV,UAAU;AAAA,EACV,SAAU;AACZ;AAiBO,IAAM,mBAAmB,EAAE,mBAAmB,QAAQ;AAAA,EAC3D,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,MAAM;AAAA,IACtB,IAAI,EAAE,OAAO;AAAA,IACb,MAAM,EAAE,OAAO;AAAA,IACf,WAAW,EAAE,OAAO;AAAA,IACpB,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,IAC1B,MAAM,EAAE,OAAO;AAAA,EACjB,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,QAAQ;AAAA,IACxB,IAAI,EAAE,OAAO;AAAA,IACb,MAAM,EAAE,OAAO;AAAA,IACf,WAAW,EAAE,OAAO;AAAA,IACpB,MAAM,EAAE,OAAO;AAAA,IACf,KAAK,EAAE,OAAO;AAAA,EAChB,CAAC;AACH,CAAC;AAsBM,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,IAAI,EAAE,OAAO,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,EACrC,SAAS,EAAE,OAAO;AAAA,EAClB,WAAW,EAAE,OAAO;AAAA,EACpB,aAAa,EAAE,OAAO;AAAA,EACtB,SAAS,EAAE,OAAO;AAAA,EAClB,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,IAAI;AAAA,EAC/C,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,EAC1C,aAAa,EAAE,MAAM,gBAAgB,EAAE,QAAQ,CAAC,CAAC;AAAA,EACjD,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,IAAI;AAAA,EAC7C,iBAAiB,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,IAAI;AAAA,EACnD,kBAAkB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,IAAI;AAAA,EAC1D,WAAW,EAAE,KAAK,EAAE,QAAQ,MAAM,oBAAI,KAAK,CAAC;AAC9C,CAAC;;;AC7ED,SAAS,MAAMA,eAAc;AA2WtB,IAAM,aAAmD;AAAA,EAC9D,aAAmB;AAAA,EACnB,WAAmB;AAAA,EACnB,QAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,iBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,kBAAmB;AAAA,EACnB,eAAmB;AAAA,EACnB,kBAAmB;AAAA,EACnB,eAAmB;AAAA,EACnB,gBAAmB;AAAA,EACnB,iBAAmB;AAAA,EACnB,eAAmB;AAAA,EACnB,SAAoB;AAAA,EACpB,UAAoB;AAAA,EACpB,aAAoB;AAAA,EACpB,cAAoB;AAAA,EACpB,iBAAoB;AACtB;AAqBO,SAAS,YAAiC,MAAuB;AACtE,SAAO,EAAE,IAAIA,QAAO,GAAG,WAAW,oBAAI,KAAK,GAAG,GAAG,KAAK;AACxD;","names":["uuidv4"]}
@@ -0,0 +1,95 @@
1
+ import {
2
+ INVITES_DIR
3
+ } from "./chunk-CZHO4DBN.js";
4
+
5
+ // src/cli/invites.ts
6
+ import { existsSync, readFileSync, unlinkSync } from "fs";
7
+ import { join as pathJoin } from "path";
8
+ function peekInvite(agentName) {
9
+ const path = pathJoin(INVITES_DIR, agentName);
10
+ if (!existsSync(path)) return null;
11
+ try {
12
+ const url = readFileSync(path, "utf-8").trim();
13
+ return url || null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ function clearInvite(agentName) {
19
+ try {
20
+ unlinkSync(pathJoin(INVITES_DIR, agentName));
21
+ } catch {
22
+ }
23
+ }
24
+ function consumeInvite(agentName) {
25
+ const url = peekInvite(agentName);
26
+ if (url !== null) clearInvite(agentName);
27
+ return url;
28
+ }
29
+ function sameApiaryServer(a, b) {
30
+ try {
31
+ return new URL(a).origin === new URL(b).origin;
32
+ } catch {
33
+ return true;
34
+ }
35
+ }
36
+ function invitePrompt(url) {
37
+ return [{
38
+ type: "text",
39
+ text: `Please join the apiary room you were invited to by calling the join_room tool with this URL: ${url}`
40
+ }];
41
+ }
42
+ var POLL_STEP_MS = 500;
43
+ async function deliverInvite(opts) {
44
+ const {
45
+ agentName,
46
+ deliver,
47
+ hasJoined,
48
+ maxAttempts = 6,
49
+ retryIntervalMs = 2e4,
50
+ signal,
51
+ // unref'd: a pending retry must never be the reason the process stays up.
52
+ sleep = (ms) => new Promise((r) => {
53
+ setTimeout(r, ms).unref?.();
54
+ })
55
+ } = opts;
56
+ const url = peekInvite(agentName);
57
+ if (url === null) return "no-invite";
58
+ if (hasJoined(url)) {
59
+ clearInvite(agentName);
60
+ return "joined";
61
+ }
62
+ const parts = invitePrompt(url);
63
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
64
+ if (signal?.aborted) return "cancelled";
65
+ try {
66
+ await deliver(parts);
67
+ } catch {
68
+ }
69
+ const steps = Math.max(1, Math.ceil(retryIntervalMs / POLL_STEP_MS));
70
+ for (let step = 0; step < steps; step++) {
71
+ if (signal?.aborted) return "cancelled";
72
+ if (hasJoined(url)) {
73
+ clearInvite(agentName);
74
+ return "joined";
75
+ }
76
+ await sleep(POLL_STEP_MS);
77
+ }
78
+ }
79
+ if (signal?.aborted) return "cancelled";
80
+ if (hasJoined(url)) {
81
+ clearInvite(agentName);
82
+ return "joined";
83
+ }
84
+ return "abandoned";
85
+ }
86
+
87
+ export {
88
+ peekInvite,
89
+ clearInvite,
90
+ consumeInvite,
91
+ sameApiaryServer,
92
+ invitePrompt,
93
+ deliverInvite
94
+ };
95
+ //# sourceMappingURL=chunk-CFY6Z6Q6.js.map