beehaven 0.8.16

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WHY a minimum was published — a closed set, with the wording written in advance.
3
+ *
4
+ * ⚠⚠ THE PUBLISHED FILE CARRIES A KEY, NEVER A SENTENCE. Two reasons, and the second is the one
5
+ * that decides it:
6
+ *
7
+ * · `minimum.json` is served from a PUBLIC CDN. A free-text reason typed while arming a lockout
8
+ * during a security incident publishes the vulnerability to everyone who has not yet updated —
9
+ * the exact population the block exists to protect. A category cannot leak what it does not
10
+ * contain.
11
+ * · The copy is then written calmly, once, and can be improved without republishing a minimum.
12
+ * The alternative is composing user-facing prose at 2am while people are locked out.
13
+ *
14
+ * ⚠ THE FLOOR IS WHAT THIS RAISES. `reason` was optional, so the realistic blocked message was the
15
+ * bare "no longer supported" — true, and the least actionable sentence available. A category is
16
+ * REQUIRED when arming, so there is always a second line saying what kind of thing happened.
17
+ *
18
+ * ⚠⚠ AND IT MUST NOT BECOME A LABEL PEOPLE STOP BELIEVING. "Security update" on a protocol change
19
+ * is a false claim, and two of those teach everyone to discount the word on the day it is true.
20
+ * Each key below means only what it says; if a block does not fit one, that is a signal the block
21
+ * needs more thought, not a new synonym for `security`.
22
+ *
23
+ * ⚠ AN UNKNOWN KEY RENDERS NOTHING, never the raw key. A client older than a category still shows
24
+ * the base message and the upgrade command, which is the whole cure — `security` printed as a bare
25
+ * word would read like a bug at the worst possible moment.
26
+ */
27
+ export const MIN_VERSION_CATEGORIES = ['security', 'compatibility', 'data-safety'];
28
+ /**
29
+ * One sentence per category, addressed to the person who is blocked.
30
+ *
31
+ * ⚠ IT SAYS WHAT TO DO OR WHY IT IS URGENT — never what the flaw was. See the leak note above.
32
+ */
33
+ const COPY = {
34
+ security: 'This is a security update. Please upgrade now.',
35
+ compatibility: 'This version can no longer talk to the service.',
36
+ 'data-safety': 'This version can corrupt or lose your work.',
37
+ };
38
+ /** Is this a category we publish? Used by the arming tool to refuse anything else. */
39
+ export function isMinVersionCategory(v) {
40
+ return typeof v === 'string' && MIN_VERSION_CATEGORIES.includes(v);
41
+ }
42
+ /**
43
+ * The line to show, or `''` when there is nothing trustworthy to say.
44
+ *
45
+ * ⚠ TOTAL AND SILENT ON ANYTHING UNRECOGNISED — an absent key, a key from a newer publisher, junk
46
+ * in the field. Every one of those means "this client cannot explain the block", and the honest
47
+ * response is to say nothing extra rather than to guess or to echo.
48
+ */
49
+ export function minVersionMessage(category) {
50
+ return isMinVersionCategory(category) ? COPY[category] : '';
51
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * min-version — refuse to run a beehaven CLI below the minimum the server names.
3
+ *
4
+ * The CLI half of the mechanism documented in `docs/FORCE_UPDATE.md`. Same contract as the Mac
5
+ * app — a small JSON beside the published artifact — and the same governing rule:
6
+ *
7
+ * BLOCK ONLY ON POSITIVE, COHERENT, ACTIONABLE EVIDENCE.
8
+ *
9
+ * https://downloads.clearly.sh/cli/minimum.json
10
+ * { "minVersion": "0.8.0", "reason": "The relay protocol changed." }
11
+ *
12
+ * ## ⚠ A CLI IS NOT AN APP, AND THREE THINGS CHANGE
13
+ *
14
+ * 1. NO BLOCKING FETCH ON THE COMMAND PATH. The desktop app checks at launch and every four
15
+ * hours across a session that lasts all day; a CLI process lives for milliseconds, and a
16
+ * network round trip on every invocation would put ~100ms of latency on `beehaven status`
17
+ * forever. The verdict is CACHED on disk and refreshed at most once every six hours, in the
18
+ * BACKGROUND — the current command is never delayed by the check it triggers.
19
+ *
20
+ * 2. THE REPAIR DEPENDS ON THE INSTALL. A curl install runs `beehaven update`; a bundled CLI
21
+ * updates with the Mac app. The block must name the right path and allow the update verb.
22
+ *
23
+ * 3. THE ESCAPE HATCHES MUST STAY OPEN. `--version`, `--help` and anything that could upgrade or
24
+ * diagnose the install keep working while blocked. Blocking `beehaven --version` would hide
25
+ * the single fact a person needs to understand what just happened to them.
26
+ *
27
+ * Everything else is the app's design, for the app's reasons: fail open on any uncertainty, and
28
+ * never demand a version that is not actually published (see `shouldBlock`).
29
+ */
30
+ import { mkdirSync, readFileSync, writeFileSync } from 'fs';
31
+ import { minVersionMessage } from './min-version-copy.js';
32
+ import { homedir } from 'os';
33
+ import { join } from 'path';
34
+ // ⚠ ESM: there is no `__filename` here, and the detached refresh needs this module's own path.
35
+ // Using the CommonJS global would throw inside the try and silently disable the refresh for ever
36
+ // — the cache would stay empty and the gate would never fire at all.
37
+ import { fileURLToPath } from 'url';
38
+ import { spawn } from 'child_process';
39
+ /**
40
+ * ⚠ THE CLI'S EXISTING COMPARATOR, not a second one. A private copy here would be the third
41
+ * implementation of "which version is newer" in this codebase, and the failure mode of two that
42
+ * disagree is telling a user on a NEWER build to downgrade. One comparator, one answer.
43
+ */
44
+ import { compareVersions } from './version-utils.js';
45
+ import { installKind, updateHint } from './commands/self-update.js';
46
+ const FEED = process.env.BEEHAVEN_CLI_FEED || 'https://downloads.clearly.sh/cli/';
47
+ const CACHE_FILE = join(homedir(), '.beehaven', 'min-version.json');
48
+ const CACHE_TTL_MS = 6 * 60 * 60_000;
49
+ const FETCH_TIMEOUT_MS = 4000;
50
+ /** How a blocked user fixes it. The single most important string in this file. */
51
+ export const UPGRADE_COMMAND = 'beehaven update';
52
+ /**
53
+ * Commands that must NEVER be blocked.
54
+ *
55
+ * ⚠ `--version` ABOVE ALL. A person told "you are too old" needs to be able to ask what they
56
+ * have; blocking that turns a clear message into a mystery. `upgrade`/`install`/`doctor` are the
57
+ * repair verbs, and blocking a repair verb is how a recoverable state becomes a stuck one.
58
+ */
59
+ const ALWAYS_ALLOWED = new Set([
60
+ '--version', '-v', 'version', '--help', '-h', 'help', 'update', 'upgrade', 'install', 'doctor',
61
+ ]);
62
+ const isVersion = (v) => typeof v === 'string' && /^\d+\.\d+\.\d+([-+].*)?$/.test(v.trim());
63
+ /**
64
+ * THE DECISION, pure and identical in spirit to the app's.
65
+ *
66
+ * ⚠ `latest` IS THE INTERLOCK THAT MATTERS. Requiring a version that is not published means
67
+ * telling every user to install something that does not exist — an unfixable lockout produced by
68
+ * a typo. If the feed cannot confirm a satisfying build exists, nobody is blocked.
69
+ */
70
+ export function shouldBlock(input) {
71
+ const { current, minimum, latest } = input;
72
+ if (!isVersion(current))
73
+ return { block: false, why: 'current-version-unreadable' };
74
+ if (!minimum)
75
+ return { block: false, why: 'no-minimum-published' };
76
+ if (!isVersion(minimum))
77
+ return { block: false, why: 'minimum-unparseable' };
78
+ if (compareVersions(current, minimum) >= 0)
79
+ return { block: false, why: 'already-current' };
80
+ if (!isVersion(latest))
81
+ return { block: false, why: 'feed-unreadable' };
82
+ if (compareVersions(latest, minimum) < 0)
83
+ return { block: false, why: 'minimum-exceeds-published-version' };
84
+ return { block: true, why: 'below-minimum', minVersion: minimum };
85
+ }
86
+ function readCache() {
87
+ try {
88
+ const c = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
89
+ return typeof c?.checkedAt === 'number' ? c : null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ function writeCache(c) {
96
+ try {
97
+ mkdirSync(join(homedir(), '.beehaven'), { recursive: true });
98
+ writeFileSync(CACHE_FILE, JSON.stringify(c), { mode: 0o600 });
99
+ }
100
+ catch { /* cache is an optimisation, never a requirement */ }
101
+ }
102
+ async function getText(url) {
103
+ const ctl = new AbortController();
104
+ const t = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
105
+ try {
106
+ const r = await fetch(`${url}?_=${Date.now()}`, { signal: ctl.signal, cache: 'no-store' });
107
+ return r.ok ? await r.text() : null;
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ finally {
113
+ clearTimeout(t);
114
+ }
115
+ }
116
+ /** Fetch the minimum + the newest published version, and cache the pair. */
117
+ export async function refreshMinVersion() {
118
+ const [minBody, latestBody] = await Promise.all([
119
+ getText(`${FEED}minimum.json`),
120
+ getText(`${FEED}latest.json`),
121
+ ]);
122
+ let minVersion = null;
123
+ let reason = '';
124
+ let category = '';
125
+ try {
126
+ const j = JSON.parse(minBody || '{}');
127
+ if (isVersion(j?.minVersion))
128
+ minVersion = String(j.minVersion).trim();
129
+ if (typeof j?.reason === 'string')
130
+ reason = j.reason.slice(0, 300);
131
+ // ⚠ Stored raw and resolved at RENDER time, so an unrecognised key prints nothing rather than
132
+ // itself — a cache written by a newer publisher must not make an older client echo a word.
133
+ if (typeof j?.category === 'string')
134
+ category = j.category.slice(0, 40);
135
+ }
136
+ catch { /* absent or malformed ⇒ no minimum */ }
137
+ let latest = null;
138
+ try {
139
+ const j = JSON.parse(latestBody || '{}');
140
+ if (isVersion(j?.version))
141
+ latest = String(j.version).trim();
142
+ }
143
+ catch { /* no manifest ⇒ cannot confirm ⇒ never blocks */ }
144
+ const c = { checkedAt: Date.now(), minVersion, latest, category, reason };
145
+ writeCache(c);
146
+ return c;
147
+ }
148
+ /**
149
+ * Gate the CLI. Returns true when the process should stop.
150
+ *
151
+ * ⚠ SYNCHRONOUS AND CHEAP. It reads a cached verdict and — when that is stale — spawns a
152
+ * DETACHED refresh that outlives this process. The current command never waits: at worst it runs
153
+ * once more on an old verdict, which is exactly the trade a millisecond-scale process should
154
+ * make. The alternative, awaiting a network call before every command, is a tax on thousands of
155
+ * invocations to catch a condition that arises once.
156
+ */
157
+ export function enforceMinVersion(argv, currentVersion) {
158
+ try {
159
+ if (process.env.BEEHAVEN_SKIP_VERSION_CHECK === '1')
160
+ return false;
161
+ // The repair verbs and the diagnostic ones always run.
162
+ if (argv.some((a) => ALWAYS_ALLOWED.has(a)))
163
+ return false;
164
+ const cache = readCache();
165
+ const stale = !cache || Date.now() - cache.checkedAt > CACHE_TTL_MS;
166
+ if (stale)
167
+ refreshInBackground();
168
+ if (!cache)
169
+ return false; // never seen the server ⇒ never block
170
+ const verdict = shouldBlock({ current: currentVersion, minimum: cache.minVersion, latest: cache.latest });
171
+ if (!verdict.block)
172
+ return false;
173
+ const line = (s = '') => process.stderr.write(`${s}\n`);
174
+ line();
175
+ line(` beehaven ${currentVersion} is no longer supported.`);
176
+ /**
177
+ * ⚠ THE CATEGORY FIRST, THE LEGACY REASON ONLY IF THERE IS NO CATEGORY. Both would print two
178
+ * explanations of one block, and the older one is the free text this design removed.
179
+ */
180
+ const why = minVersionMessage(cache.category) || cache.reason;
181
+ if (why)
182
+ line(` ${why}`);
183
+ line();
184
+ line(` Version ${verdict.minVersion} or later is required. To update:`);
185
+ line();
186
+ line(` ${installKind() === 'user' ? UPGRADE_COMMAND : updateHint(installKind())}`);
187
+ line();
188
+ return true;
189
+ }
190
+ catch {
191
+ return false; // ⚠ A THROW HERE MUST NOT BLOCK. Nothing about this check is worth an outage.
192
+ }
193
+ }
194
+ /**
195
+ * Refresh without delaying anything.
196
+ *
197
+ * ⚠ DETACHED AND UNREF'D. Doing it in-process would either block the command (defeating the
198
+ * point) or be killed the moment the command exits, so the cache would never populate and the
199
+ * gate would never fire at all.
200
+ */
201
+ function refreshInBackground() {
202
+ try {
203
+ const self = fileURLToPath(import.meta.url);
204
+ const child = spawn(process.execPath, [self, '--refresh-min-version'], {
205
+ detached: true, stdio: 'ignore',
206
+ });
207
+ child.unref();
208
+ }
209
+ catch { /* best-effort */ }
210
+ }
211
+ // Entry point for the detached refresh above.
212
+ if (process.argv[2] === '--refresh-min-version') {
213
+ void refreshMinVersion().finally(() => process.exit(0));
214
+ }
@@ -0,0 +1,23 @@
1
+ /** Public app/command policy shared by the CLI and daemon catalog surfaces. */
2
+ export const PUBLICLY_HIDDEN_APP_NAMES = new Set([
3
+ 'mwah', 'clearly-voice', 'recorder', 'claude-code', 'memory-cloud',
4
+ 'ableton', 'blender', 'logic-pro', 'hue', 'nitepunk-piano',
5
+ 'shopify', 'figma', 'jira', 'gsc', 'marketing', 'backlinks',
6
+ 'vector', 'image', 'bee-canvas',
7
+ ]);
8
+ /** Voice/TTS commands are retained in source but withheld from the public CLI for now. */
9
+ export const HIDDEN_PUBLIC_COMMANDS = new Set(['speak', 'voice', 'tts']);
10
+ /** Avatar actions whose public CLI surface is paused with voice/TTS. */
11
+ export const HIDDEN_PUBLIC_APP_ACTIONS = new Set([
12
+ 'speak', 'stream', 'setTts', 'voices', 'transcribe', 'transcribeCancel', 'transcribeStats',
13
+ ]);
14
+ export function isPubliclyVisibleAppAction(building, action) {
15
+ return String(building ?? '').split('/')[0].toLowerCase() !== 'avatar'
16
+ || !HIDDEN_PUBLIC_APP_ACTIONS.has(String(action ?? ''));
17
+ }
18
+ export function isPubliclyVisibleApp(app) {
19
+ if (process.env.HIVE_DEV_MODE === '1')
20
+ return true;
21
+ const name = String(app?.name ?? '').trim().toLowerCase();
22
+ return !!name && !PUBLICLY_HIDDEN_APP_NAMES.has(name) && app?.internal !== true;
23
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Pure relay-selection helpers — the single READ-side source of truth for
3
+ * "which relay / which env" from a loaded ~/.beehaven/config.json.
4
+ *
5
+ * Kept in its own dependency-free module (no disk, no daemon imports) so EVERY
6
+ * surface that reports the relay — `beehaven env`, `whoami`/status, the admin
7
+ * console URL, auth-origin selection, the daemon spawn path — can import it
8
+ * WITHOUT pulling in the heavy daemon module graph.
9
+ *
10
+ * `endpoint` is the load-bearing key: the relay client (`Relay.loadConfig`)
11
+ * returns null without it and dials `config.endpoint`. `relayUrl` / `envName`
12
+ * are back-compat ALIASES — never trust them over the derived value, or the
13
+ * report drifts from what the daemon actually dials (the bug this fixes: a
14
+ * fresh login / Mac-app install writes only `endpoint`, leaving the aliases
15
+ * stale or unset).
16
+ */
17
+ /** Map env name → relay URL. Used by `beehaven env <name>`. */
18
+ export const RELAY_PRESETS = {
19
+ prod: 'wss://relay.clearly.sh',
20
+ staging: 'wss://bee-relay-staging.throbbing-unit-1b3f.workers.dev',
21
+ dev: 'ws://localhost:8787',
22
+ };
23
+ /**
24
+ * The canonical dial target from a config:
25
+ * 1. `endpoint` — what the relay client actually connects to
26
+ * 2. `relayUrl` — legacy alias, kept in sync for back-compat
27
+ * Returns undefined when neither is set so each caller picks its own default
28
+ * (the daemon falls back to prod; admin commands error instead).
29
+ */
30
+ export function relayUrlFromConfig(cfg) {
31
+ if (cfg && typeof cfg.endpoint === 'string' && cfg.endpoint.length > 0)
32
+ return cfg.endpoint;
33
+ if (cfg && typeof cfg.relayUrl === 'string' && cfg.relayUrl.length > 0)
34
+ return cfg.relayUrl;
35
+ return undefined;
36
+ }
37
+ /**
38
+ * Resolve BOTH the relay URL and the env NAME from a config, keying off the
39
+ * canonical endpoint. The name is derived FROM the resolved URL (reverse
40
+ * RELAY_PRESETS lookup, else 'custom') so a stale `envName` alias can never
41
+ * mislabel the connection. This is the one helper every "which env" surface
42
+ * must use instead of reading cfg.envName / cfg.relayUrl directly.
43
+ */
44
+ export function resolveRelaySelection(cfg) {
45
+ const url = relayUrlFromConfig(cfg) ?? RELAY_PRESETS.prod;
46
+ const envName = Object.keys(RELAY_PRESETS).find((k) => RELAY_PRESETS[k] === url) ?? 'custom';
47
+ return { url, envName };
48
+ }
49
+ /**
50
+ * ⚠⚠ THE ONE ORIGIN RESOLVER ON THIS SIDE — where the WEB APP lives for the env this daemon dials.
51
+ *
52
+ * ── WHY IT EXISTS (CBH-45) ────────────────────────────────────────────────────────────────────
53
+ *
54
+ * `beehaven open <uuid>` printed `https://localhost:3000/documents/…` on one run and
55
+ * `https://www.clearly.sh/documents/…` on the next, same workspace, seconds apart. The server
56
+ * preferred `_uiClientOrigin` — ONE workspace-global slot any browser overwrites on mount — so
57
+ * whichever of ten connected surfaces reported last decided the URL for every caller, and a
58
+ * developer's dev tab made this CLI print a link to a server it has never run. The printed URL is
59
+ * the only thing a CLI user can act on, so that is a dead link that reads as a broken document.
60
+ *
61
+ * The server now prefers the origin the CALLER declares. This is what the CLI declares, and it is
62
+ * derived from the env the daemon is actually dialling — so the answer is a property of THIS
63
+ * machine's configuration and cannot be moved by anyone else's tab.
64
+ *
65
+ * ⚠ THERE WERE AT LEAST THREE SPELLINGS IN THE TREE and this is now the only one:
66
+ * `https://www.clearly.sh` (open), `https://clearly.sh` (canvas.ts — apex, no www) and
67
+ * `https://localhost:3000` (canvas.ts's `--dev` branch). They are not interchangeable: the apex
68
+ * 301s to www, so the apex spelling handed people a link with a redirect in it.
69
+ *
70
+ * ⚠ STAGING RETURNS THE PRODUCTION SITE, AND THAT IS CORRECT. The frontend is prod-only — the
71
+ * `staging.clearly-sh.pages.dev` alias is retired (CLAUDE.md) — so the staging worker's app really
72
+ * is `www.clearly.sh`. Inventing a `staging.clearly.sh` here would be the dead-link bug again.
73
+ *
74
+ * ⚠ `custom` RETURNS undefined, DELIBERATELY. A custom relay says nothing about where its frontend
75
+ * is, and guessing production for it is how you hand someone a link into the wrong deployment.
76
+ * The caller then declares nothing and the server falls back to its own env — which knows.
77
+ */
78
+ export function appOriginForEnv(envName) {
79
+ switch (envName) {
80
+ case 'prod':
81
+ case 'staging':
82
+ return 'https://www.clearly.sh';
83
+ // `npm run dev` is `next dev --experimental-https`, so the local app is HTTPS, not HTTP.
84
+ case 'dev':
85
+ return 'https://localhost:3000';
86
+ default:
87
+ return undefined;
88
+ }
89
+ }