@clearlysh/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,363 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================================
3
+ // Beehaven Office - Relay Setup (Login)
4
+ // Browser-based OAuth login flow for Clearly account
5
+ // ============================================================================
6
+ import http from 'http';
7
+ import crypto from 'crypto';
8
+ import { createInterface } from 'readline';
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
10
+ import { homedir } from 'os';
11
+ import { join } from 'path';
12
+ import { spawnSync } from 'child_process';
13
+ import { Relay, CLEARLY_RELAY_URL } from './relay.js';
14
+ import { resolveRelaySelection } from './relay-presets.js';
15
+ import { SESSION_KEYCHAIN_ACCOUNTS, KEYCHAIN_SERVICE } from './keychain.js';
16
+ import { withUtm } from './utm.js';
17
+ const CONFIG_DIR = join(homedir(), '.beehaven');
18
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
19
+ const AUTH_BASE_URL = 'https://clearly.sh/auth/beehaven';
20
+ const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
21
+ /**
22
+ * The raw config, for questions that must be answered BEFORE a token exists.
23
+ * `Relay.loadConfig()` returns null without one, which is the state a first login is in.
24
+ */
25
+ function readConfigFile() {
26
+ if (!existsSync(CONFIG_FILE))
27
+ return {};
28
+ try {
29
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ function saveRelayConfig(token, endpoint) {
36
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
37
+ let existing = {};
38
+ if (existsSync(CONFIG_FILE)) {
39
+ try {
40
+ existing = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
41
+ }
42
+ catch (err) {
43
+ console.warn(`[config] Failed to parse ${CONFIG_FILE}:`, err.message);
44
+ }
45
+ }
46
+ existing.token = token;
47
+ // ⚠ BOTH KEYS, as `beehaven env` writes them — `endpoint` is dialled, `relayUrl` is the alias
48
+ // other surfaces read. Updating one leaves the other naming a different environment.
49
+ existing.endpoint = endpoint;
50
+ existing.relayUrl = endpoint;
51
+ existing.tier = 'connected';
52
+ writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
53
+ chmodSync(CONFIG_FILE, 0o600);
54
+ }
55
+ function prompt(question) {
56
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
57
+ return new Promise((resolve) => {
58
+ rl.question(question, (answer) => {
59
+ rl.close();
60
+ resolve(answer.trim());
61
+ });
62
+ });
63
+ }
64
+ /**
65
+ * Start a local HTTP server on a random port to receive the OAuth callback.
66
+ * Returns a promise for the token, plus helpers to get the port and close.
67
+ */
68
+ function startCallbackServer() {
69
+ let resolveToken;
70
+ let rejectToken;
71
+ let serverRef;
72
+ let settled = false;
73
+ const oauthState = crypto.randomBytes(32).toString('hex');
74
+ const promise = new Promise((resolve, reject) => {
75
+ resolveToken = resolve;
76
+ rejectToken = reject;
77
+ serverRef = http.createServer((req, res) => {
78
+ if (settled)
79
+ return;
80
+ const url = new URL(req.url, `http://localhost`);
81
+ if (url.pathname === '/callback') {
82
+ // Validate OAuth state parameter to prevent CSRF (skip if server doesn't echo it back yet)
83
+ const returnedState = url.searchParams.get('state');
84
+ if (returnedState && returnedState !== oauthState) {
85
+ res.writeHead(403, { 'Content-Type': 'text/html' });
86
+ res.end('<!DOCTYPE html><html><body><h1>Invalid state parameter</h1></body></html>');
87
+ return;
88
+ }
89
+ const token = url.searchParams.get('token');
90
+ if (token) {
91
+ settled = true;
92
+ res.writeHead(200, { 'Content-Type': 'text/html' });
93
+ res.end([
94
+ '<!DOCTYPE html><html><head>',
95
+ '<meta charset="utf-8">',
96
+ '<title>Beehaven - Logged In</title>',
97
+ '<style>',
98
+ 'body{font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#fef9ef}',
99
+ '.card{text-align:center;padding:48px;border-radius:16px;background:white;box-shadow:0 2px 12px rgba(0,0,0,0.08)}',
100
+ 'h1{margin:0 0 8px;font-size:24px}',
101
+ 'p{margin:0;color:#666;font-size:15px}',
102
+ '.bee{font-size:48px;margin-bottom:16px}',
103
+ '</style>',
104
+ '</head><body>',
105
+ '<div class="card">',
106
+ '<div class="bee">&#x1F41D;</div>',
107
+ '<h1>Logged in!</h1>',
108
+ '<p>You can close this tab and return to your terminal.</p>',
109
+ '</div>',
110
+ '</body></html>',
111
+ ].join(''));
112
+ serverRef.close();
113
+ resolve(token);
114
+ }
115
+ else {
116
+ res.writeHead(400, { 'Content-Type': 'text/html' });
117
+ res.end('<!DOCTYPE html><html><body><h1>Missing token</h1></body></html>');
118
+ }
119
+ }
120
+ else {
121
+ res.writeHead(404);
122
+ res.end();
123
+ }
124
+ });
125
+ serverRef.listen(0, '127.0.0.1');
126
+ // Timeout after 2 minutes
127
+ const timer = setTimeout(() => {
128
+ if (!settled) {
129
+ settled = true;
130
+ serverRef.close();
131
+ reject(new Error('Login timed out after 2 minutes.'));
132
+ }
133
+ }, LOGIN_TIMEOUT_MS);
134
+ serverRef.on('close', () => clearTimeout(timer));
135
+ });
136
+ return {
137
+ promise,
138
+ getPort: () => serverRef.address()?.port || 0,
139
+ close: () => {
140
+ settled = true;
141
+ serverRef?.close();
142
+ },
143
+ state: oauthState,
144
+ };
145
+ }
146
+ export async function login() {
147
+ console.log('');
148
+ console.log(' Clearly account required to use Beehaven.');
149
+ console.log('');
150
+ // Check for existing valid config
151
+ const existing = Relay.loadConfig();
152
+ if (existing) {
153
+ console.log(' Checking existing session...');
154
+ const relay = new Relay();
155
+ const profile = await relay.verifyToken(existing.token, existing.endpoint || CLEARLY_RELAY_URL);
156
+ if (profile) {
157
+ if (profile.displayName) {
158
+ console.log(` Already logged in as ${profile.displayName} (${profile.subscriptionPlan})`);
159
+ }
160
+ else {
161
+ console.log(' Already logged in.');
162
+ }
163
+ return true;
164
+ }
165
+ console.log(' Session expired. Please log in again.');
166
+ console.log('');
167
+ }
168
+ // Start local callback server
169
+ const callbackServer = startCallbackServer();
170
+ // Small delay to let server bind
171
+ await new Promise((r) => setTimeout(r, 100));
172
+ const port = callbackServer.getPort();
173
+ if (!port) {
174
+ console.log(' Failed to start auth server.');
175
+ callbackServer.close();
176
+ return false;
177
+ }
178
+ const callbackUrl = `http://localhost:${port}/callback`;
179
+ const authUrl = withUtm(`${AUTH_BASE_URL}?callback=${encodeURIComponent(callbackUrl)}&state=${callbackServer.state}`);
180
+ // Try to open browser
181
+ let browserOpened = false;
182
+ try {
183
+ const openModule = await import('open');
184
+ await openModule.default(authUrl);
185
+ browserOpened = true;
186
+ }
187
+ catch {
188
+ browserOpened = false;
189
+ }
190
+ if (browserOpened) {
191
+ console.log(' Opening browser to sign in with Google...');
192
+ console.log('');
193
+ console.log(' Waiting for login... (press Ctrl+C to cancel)');
194
+ }
195
+ else {
196
+ // Fallback: show URL and offer manual token paste
197
+ console.log(' Could not open browser. Sign in manually:');
198
+ console.log('');
199
+ console.log(` 1. Open: ${authUrl}`);
200
+ console.log(' 2. Sign in with Google');
201
+ console.log('');
202
+ console.log(' Waiting for login callback...');
203
+ console.log(' Or paste your relay token below:');
204
+ console.log('');
205
+ // Race: callback server vs manual token paste
206
+ const manualToken = prompt(' Relay token: ').then((token) => {
207
+ if (token) {
208
+ callbackServer.close();
209
+ return token;
210
+ }
211
+ return null;
212
+ });
213
+ try {
214
+ const token = await Promise.race([
215
+ callbackServer.promise,
216
+ manualToken.then((t) => {
217
+ if (t)
218
+ return t;
219
+ return new Promise(() => { });
220
+ }),
221
+ ]);
222
+ if (token) {
223
+ return await verifyAndSave(token);
224
+ }
225
+ }
226
+ catch (err) {
227
+ console.log(` ${err.message}`);
228
+ return false;
229
+ }
230
+ }
231
+ // Wait for callback
232
+ try {
233
+ const token = await callbackServer.promise;
234
+ return await verifyAndSave(token);
235
+ }
236
+ catch (err) {
237
+ console.log(` ${err.message}`);
238
+ return false;
239
+ }
240
+ }
241
+ async function verifyAndSave(token) {
242
+ // Validate token format before sending to server
243
+ if (!token || !/^[a-zA-Z0-9_\-.]{20,}$/.test(token)) {
244
+ console.log(' Invalid token format.');
245
+ return false;
246
+ }
247
+ console.log('');
248
+ console.log(' Verifying...');
249
+ const relay = new Relay();
250
+ /**
251
+ * ⚠⚠ THE ACTIVE RELAY, NOT A HARDCODED PROD CONSTANT. Both of these were `CLEARLY_RELAY_URL`, so
252
+ * a staging or dev user who signed in here was VERIFIED against prod and then had `endpoint`
253
+ * OVERWRITTEN to prod — silently moving them to another environment as a side effect of logging
254
+ * in. `beehaven env staging` writes the target; a login must honour it, never redefine it.
255
+ *
256
+ * ⚠ Read from the file rather than `Relay.loadConfig()`: that returns null without a token, which
257
+ * is exactly the state a first login is in. `resolveRelaySelection` defaults to prod when the
258
+ * config names nothing, so the no-config case is unchanged.
259
+ */
260
+ const dialUrl = resolveRelaySelection(readConfigFile()).url;
261
+ const profile = await relay.verifyToken(token, dialUrl);
262
+ if (profile) {
263
+ saveRelayConfig(token, dialUrl);
264
+ if (profile.displayName) {
265
+ console.log(` Logged in as ${profile.displayName} (${profile.subscriptionPlan})`);
266
+ }
267
+ else {
268
+ console.log(' Logged in.');
269
+ }
270
+ console.log(' Config saved to ~/.beehaven/config.json');
271
+ console.log('');
272
+ return true;
273
+ }
274
+ else {
275
+ console.log(' Token verification failed. Please try again.');
276
+ console.log('');
277
+ return false;
278
+ }
279
+ }
280
+ export function logout() {
281
+ // Clear the daemon's keychain entries first — without this, the
282
+ // next daemon start finds the still-valid token via
283
+ // `security find-generic-password -s beehaven -a daemon-token`
284
+ // (Relay.loadConfig reads keychain when tokenStorage='keychain'),
285
+ // surfacing as "I signed out and the app auto-signed back in
286
+ // on launch." We catch every error path so a missing /usr/bin/
287
+ // security or a not-found entry doesn't bail us out before the
288
+ // config-file clear below.
289
+ try {
290
+ /**
291
+ * ⚠⚠ THIS USED A BARE `require('child_process')` AND THEREFORE CLEARED NOTHING, EVER.
292
+ *
293
+ * This package is `"type": "module"`, so `require` is not defined and the call threw on its
294
+ * first line — straight into the catch below, which logs and moves on. Every logout and every
295
+ * uninstall reported success while leaving the daemon token, the Firebase refresh token and
296
+ * the IPC secret sitting in the login keychain.
297
+ *
298
+ * ⚠ AND THE COMMENT THAT WAS HERE EXPLAINED WHY, USING A PREMISE THAT WAS ITSELF A BUG: it
299
+ * said this file "is required from the Electron main process via CommonJS `require()`", which
300
+ * was TRUE and was not supposed to be — `loadBeehavenModule` intended `import()` and had been
301
+ * silently downleveled to `require()` by `"module": "CommonJS"`. So a workaround was written
302
+ * to fit broken behaviour, and it only surfaced when that was repaired. The two had to be
303
+ * fixed together, and neither is visible from the other's file.
304
+ */
305
+ const cleared = [];
306
+ for (const account of SESSION_KEYCHAIN_ACCOUNTS) {
307
+ const r = spawnSync('/usr/bin/security', ['delete-generic-password', '-s', KEYCHAIN_SERVICE, '-a', account], { stdio: 'ignore' });
308
+ if (r.status === 0)
309
+ cleared.push(account);
310
+ }
311
+ console.log('[logout] keychain-cleared accounts=' + JSON.stringify(cleared));
312
+ }
313
+ catch (err) {
314
+ console.log('[logout] keychain-clear-failed err=' + ((err && err.message) || String(err)));
315
+ }
316
+ if (!existsSync(CONFIG_FILE)) {
317
+ console.log(' Not logged in.');
318
+ console.log('[logout] config-missing path=' + CONFIG_FILE);
319
+ return;
320
+ }
321
+ try {
322
+ const config = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
323
+ // Capture what we're about to clear so the log line lets us
324
+ // diagnose "tray sign-out didn't take" without a debugger.
325
+ const had = {
326
+ token: !!config.token,
327
+ refreshToken: !!config.refreshToken,
328
+ endpoint: !!config.endpoint,
329
+ user: !!config.user,
330
+ tokenStorage: config.tokenStorage ?? null,
331
+ };
332
+ delete config.token;
333
+ delete config.endpoint;
334
+ delete config.user;
335
+ // Clear the refresh token too — without this, the tray's
336
+ // hasRefreshToken check stays true and the "Sign in to Hive"
337
+ // menu item never reappears after sign-out (so the user can't
338
+ // re-auth from the tray; they'd have to quit + relaunch).
339
+ delete config.refreshToken;
340
+ // tokenStorage is just a hint to loadConfig() — clearing it
341
+ // belt-and-braces: even if a stale keychain entry survived
342
+ // the spawnSync above, the next loadConfig will read the
343
+ // (now-empty) `parsed.token` instead of falling through to
344
+ // `security find-generic-password`.
345
+ delete config.tokenStorage;
346
+ config.tier = 'local';
347
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
348
+ chmodSync(CONFIG_FILE, 0o600);
349
+ console.log('[logout] cleared had=' + JSON.stringify(had));
350
+ console.log(' Logged out. Sign in again from the Clearly Dev Mac app tray.');
351
+ }
352
+ catch (err) {
353
+ console.log(' Failed to update config.');
354
+ console.log('[logout] update-failed err=' + ((err && err.message) || String(err)));
355
+ }
356
+ }
357
+ // Allow running directly: tsx src/setup-relay.ts
358
+ if (process.argv[1]?.endsWith('setup-relay.ts') || process.argv[1]?.endsWith('setup-relay.js')) {
359
+ login().catch((err) => {
360
+ console.error('Login failed:', err.message);
361
+ process.exit(1);
362
+ });
363
+ }