flyspace-cli 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -68,6 +68,7 @@ import {
68
68
  getWorkspaceById,
69
69
  getWorkspaceBySlug,
70
70
  getWorkspaceByStripeCustomerId,
71
+ getWorkspaceGreetedAt,
71
72
  getWorkspacePlanStatus,
72
73
  installUsageRecording,
73
74
  isAllowedModel,
@@ -103,6 +104,7 @@ import {
103
104
  lookupById,
104
105
  markInviteResent,
105
106
  markWorkspaceAgentConnected,
107
+ markWorkspaceGreeted,
106
108
  markWorkspaceProjectsSetup,
107
109
  markdownToBlocks,
108
110
  mcpStrictConfigEnabled,
@@ -153,7 +155,7 @@ import {
153
155
  warnOnBrokenChannelCwds,
154
156
  workspaceSkillsDir,
155
157
  writeSkillFile
156
- } from "./chunk-VKTJBYA3.js";
158
+ } from "./chunk-DA7SYYKU.js";
157
159
  import {
158
160
  firstReplyBody,
159
161
  getThread,
@@ -220,8 +222,8 @@ import { dirname, join } from "path";
220
222
  var REQUIRED_ENV = ["SUPABASE_URL", "SUPABASE_SECRET_KEY"];
221
223
  var RENOTIFY_AFTER_MS = 30 * 60 * 1e3;
222
224
  var ts = () => (/* @__PURE__ */ new Date()).toISOString();
223
- function findMissingEnv(env = process.env) {
224
- const required = slackConfigured(env) ? [...SLACK_ENV, ...REQUIRED_ENV] : REQUIRED_ENV;
225
+ function findMissingEnv(env = process.env, supabaseViaHost = false) {
226
+ const required = [...slackConfigured(env) ? SLACK_ENV : [], ...supabaseViaHost ? [] : REQUIRED_ENV];
225
227
  return required.filter((name) => !env[name]?.trim());
226
228
  }
227
229
  function shouldNotify(missing, statePath, now) {
@@ -290,7 +292,7 @@ async function preflight(opts = {}) {
290
292
  const statePath = opts.statePath ?? join(paths.DATA_DIR, "preflight-alert.json");
291
293
  const notify = opts.notify ?? notifyOwnerOverSlack;
292
294
  const exit = opts.exit ?? ((code) => process.exit(code));
293
- const missing = findMissingEnv(env);
295
+ const missing = findMissingEnv(env, opts.supabaseViaHost);
294
296
  if (missing.length === 0) {
295
297
  rmSync(statePath, { force: true });
296
298
  return;
@@ -307,15 +309,386 @@ async function preflight(opts = {}) {
307
309
  exit(1);
308
310
  }
309
311
 
312
+ // src/auth/device-auth.ts
313
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
314
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
315
+ import { dirname as dirname2, join as join2 } from "path";
316
+ var DEFAULT_API_URL = "https://app.flyspace.ai";
317
+ var EXPIRY_SKEW_MS = 6e4;
318
+ var AuthApiError = class extends Error {
319
+ constructor(message, status) {
320
+ super(message);
321
+ this.status = status;
322
+ this.name = "AuthApiError";
323
+ }
324
+ };
325
+ function apiUrl() {
326
+ return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
327
+ }
328
+ function deviceKeyPath() {
329
+ return join2(paths.BASE_CONFIG_DIR, "device.key");
330
+ }
331
+ function credentialsPath() {
332
+ return join2(paths.BASE_CONFIG_DIR, "credentials.json");
333
+ }
334
+ function ensureDeviceKey(path = deviceKeyPath()) {
335
+ let created = false;
336
+ if (!existsSync(path)) {
337
+ const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
338
+ mkdirSync2(dirname2(path), { recursive: true });
339
+ writeFileSync2(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
340
+ created = true;
341
+ }
342
+ const privateKey = createPrivateKey(readFileSync2(path, "utf8"));
343
+ const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
344
+ "base64"
345
+ );
346
+ return { privateKey, publicKeyBase64, created };
347
+ }
348
+ function loadCredentials(path = credentialsPath()) {
349
+ if (!existsSync(path)) return null;
350
+ try {
351
+ return JSON.parse(readFileSync2(path, "utf8"));
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+ function saveCredentials(creds, path = credentialsPath()) {
357
+ mkdirSync2(dirname2(path), { recursive: true });
358
+ writeFileSync2(path, `${JSON.stringify(creds, null, 2)}
359
+ `, { mode: 384 });
360
+ }
361
+ function clearLocalAuth() {
362
+ rmSync2(credentialsPath(), { force: true });
363
+ rmSync2(deviceKeyPath(), { force: true });
364
+ }
365
+ async function post(base, path, body, token) {
366
+ return fetch(`${base}${path}`, {
367
+ method: "POST",
368
+ headers: {
369
+ "content-type": "application/json",
370
+ ...token ? { authorization: `Bearer ${token}` } : {}
371
+ },
372
+ body: JSON.stringify(body)
373
+ });
374
+ }
375
+ async function readJson(res) {
376
+ try {
377
+ return await res.json();
378
+ } catch {
379
+ return {};
380
+ }
381
+ }
382
+ async function expectOk(res, what) {
383
+ const json = await readJson(res);
384
+ if (!res.ok) {
385
+ const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
386
+ throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
387
+ }
388
+ return json;
389
+ }
390
+ function toSlugs(value) {
391
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
392
+ }
393
+ function toGrant(json) {
394
+ const user = json["user"] ?? {};
395
+ return {
396
+ accessToken: String(json["access_token"]),
397
+ expiresIn: Number(json["expires_in"]),
398
+ deviceId: String(json["device_id"]),
399
+ userEmail: String(user["email"] ?? ""),
400
+ workspaces: toSlugs(json["workspaces"])
401
+ };
402
+ }
403
+ async function requestDeviceCode(base, input2) {
404
+ const res = await post(base, "/api/auth/device/code", {
405
+ public_key: input2.publicKey,
406
+ name: input2.name,
407
+ platform: input2.platform
408
+ });
409
+ const json = await expectOk(res, "Requesting a device code");
410
+ return {
411
+ deviceCode: String(json["device_code"]),
412
+ userCode: String(json["user_code"]),
413
+ verificationUri: String(json["verification_uri"]),
414
+ expiresIn: Number(json["expires_in"]),
415
+ interval: Number(json["interval"] ?? 5)
416
+ };
417
+ }
418
+ async function pollDeviceToken(base, deviceCode) {
419
+ const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
420
+ const json = await readJson(res);
421
+ if (res.ok) return { status: "approved", grant: toGrant(json) };
422
+ switch (json["error"]) {
423
+ case "authorization_pending":
424
+ return { status: "pending" };
425
+ case "slow_down":
426
+ return { status: "slow_down" };
427
+ case "access_denied":
428
+ return { status: "denied" };
429
+ case "expired_token":
430
+ return { status: "expired" };
431
+ default:
432
+ throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
433
+ }
434
+ }
435
+ async function refreshAccessToken(creds, privateKey) {
436
+ const challenge = await expectOk(
437
+ await post(creds.apiUrl, "/api/auth/device/challenge", { device_id: creds.deviceId }),
438
+ "Requesting a refresh challenge"
439
+ );
440
+ const nonce = String(challenge["nonce"]);
441
+ const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
442
+ const json = await expectOk(
443
+ await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
444
+ "Refreshing the access token"
445
+ );
446
+ return {
447
+ ...creds,
448
+ accessToken: String(json["access_token"]),
449
+ accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
450
+ };
451
+ }
452
+ async function fetchWhoAmI(base, token) {
453
+ const res = await fetch(`${base}/api/auth/device/me`, { headers: { authorization: `Bearer ${token}` } });
454
+ const json = await expectOk(res, "Checking the session");
455
+ const user = json["user"] ?? {};
456
+ const device = json["device"] ?? {};
457
+ return {
458
+ userEmail: String(user["email"] ?? ""),
459
+ deviceId: String(device["id"] ?? ""),
460
+ deviceName: String(device["name"] ?? ""),
461
+ workspaces: toSlugs(json["workspaces"])
462
+ };
463
+ }
464
+ async function boundWorkspaces() {
465
+ const creds = loadCredentials();
466
+ if (!creds) return null;
467
+ try {
468
+ const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
469
+ saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
470
+ return { workspaces: me.workspaces, fresh: true };
471
+ } catch {
472
+ return { workspaces: creds.workspaces ?? [], fresh: false };
473
+ }
474
+ }
475
+ async function revokeDevice(base, token) {
476
+ await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
477
+ }
478
+ async function getAccessToken() {
479
+ const fromEnv = process.env["FLYSPACE_TOKEN"];
480
+ if (fromEnv) return fromEnv;
481
+ const creds = loadCredentials();
482
+ if (!creds) return null;
483
+ if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
484
+ const { privateKey } = ensureDeviceKey();
485
+ const refreshed = await refreshAccessToken(creds, privateKey);
486
+ saveCredentials(refreshed);
487
+ return refreshed.accessToken;
488
+ }
489
+
490
+ // src/dashboard/host-db-client.ts
491
+ import { createClient } from "@supabase/supabase-js";
492
+ import { randomUUID } from "crypto";
493
+
494
+ // src/dashboard/host-lock.ts
495
+ import { execFileSync } from "child_process";
496
+ import { randomBytes } from "crypto";
497
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
498
+ import { join as join3 } from "path";
499
+ function hostLockPath() {
500
+ return join3(paths.BASE_DATA_DIR, "host.lock");
501
+ }
502
+ function isLiveFlyspace(pid) {
503
+ try {
504
+ process.kill(pid, 0);
505
+ const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
506
+ encoding: "utf8"
507
+ });
508
+ return command.includes("flyspace") || command.includes("host-server");
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+ function readLockPid(path) {
514
+ try {
515
+ const pid = Number(JSON.parse(readFileSync3(path, "utf8")).pid);
516
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+ function acquireHostLock() {
522
+ const path = hostLockPath();
523
+ mkdirSync3(paths.BASE_DATA_DIR, { recursive: true });
524
+ const body = JSON.stringify({
525
+ pid: process.pid,
526
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
527
+ execPath: process.argv[1] ?? ""
528
+ });
529
+ for (let attempt = 0; attempt < 2; attempt++) {
530
+ try {
531
+ writeFileSync3(path, body, { flag: "wx", mode: 384 });
532
+ const release2 = () => {
533
+ if (readLockPid(path) === process.pid) rmSync3(path, { force: true });
534
+ };
535
+ process.once("exit", release2);
536
+ return release2;
537
+ } catch (err) {
538
+ if (err.code !== "EEXIST") throw err;
539
+ const holder = readLockPid(path);
540
+ if (holder === process.pid) return () => {
541
+ };
542
+ if (holder && isLiveFlyspace(holder)) {
543
+ throw new Error(
544
+ `Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
545
+ );
546
+ }
547
+ rmSync3(path, { force: true });
548
+ }
549
+ }
550
+ throw new Error(`Could not take the Host lock at ${path}`);
551
+ }
552
+ function publishHostEndpoint(port) {
553
+ const path = hostLockPath();
554
+ if (readLockPid(path) !== process.pid) return null;
555
+ const token = randomBytes(32).toString("hex");
556
+ const lock = JSON.parse(readFileSync3(path, "utf8"));
557
+ writeFileSync3(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
558
+ return token;
559
+ }
560
+ function readHostEndpoint() {
561
+ try {
562
+ const { pid, port, token } = JSON.parse(readFileSync3(hostLockPath(), "utf8"));
563
+ if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
564
+ return { port, token };
565
+ } catch {
566
+ return null;
567
+ }
568
+ }
569
+
570
+ // src/dashboard/host-db-client.ts
571
+ var REQUEST_TIMEOUT_MS = 3e4;
572
+ var pending = /* @__PURE__ */ new Map();
573
+ var sender = null;
574
+ var onSender = [];
575
+ function setHostDbSender(fn) {
576
+ sender = fn;
577
+ if (!fn) return;
578
+ const waiting = onSender;
579
+ onSender = [];
580
+ for (const wake of waiting) wake();
581
+ }
582
+ function handleHostDbResult(id, res) {
583
+ const p = pending.get(id);
584
+ if (!p) return;
585
+ pending.delete(id);
586
+ clearTimeout(p.timer);
587
+ p.resolve(res);
588
+ }
589
+ function whenConnected() {
590
+ if (sender) return Promise.resolve(sender);
591
+ return new Promise((resolve7, reject) => {
592
+ const timer2 = setTimeout(() => {
593
+ onSender = onSender.filter((w) => w !== wake);
594
+ reject(new Error("Host is not connected to the Data API"));
595
+ }, REQUEST_TIMEOUT_MS);
596
+ const wake = () => {
597
+ clearTimeout(timer2);
598
+ resolve7(sender);
599
+ };
600
+ onSender.push(wake);
601
+ });
602
+ }
603
+ async function requestHostDb(req) {
604
+ const send2 = await whenConnected();
605
+ const id = randomUUID();
606
+ return new Promise((resolve7, reject) => {
607
+ const timer2 = setTimeout(() => {
608
+ pending.delete(id);
609
+ reject(new Error("Data API did not answer in time"));
610
+ }, REQUEST_TIMEOUT_MS);
611
+ pending.set(id, { resolve: resolve7, reject, timer: timer2 });
612
+ send2({ type: "db", id, req });
613
+ });
614
+ }
615
+ var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
616
+ function dbFetch(send2) {
617
+ return async (input2, init) => {
618
+ const req = new Request(input2, init);
619
+ const url = new URL(req.url);
620
+ const method = req.method.toUpperCase();
621
+ const res = await send2({
622
+ method,
623
+ path: `${url.pathname}${url.search}`,
624
+ headers: Object.fromEntries(req.headers.entries()),
625
+ body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
626
+ });
627
+ const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
628
+ return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
629
+ };
630
+ }
631
+ var hostDbFetch = dbFetch(requestHostDb);
632
+ var LOCAL_DB_PATH = "/internal/db";
633
+ async function requestViaLocalHost(req) {
634
+ const deadline = Date.now() + REQUEST_TIMEOUT_MS;
635
+ let endpoint = readHostEndpoint();
636
+ while (!endpoint && Date.now() < deadline) {
637
+ await new Promise((r) => setTimeout(r, 1e3));
638
+ endpoint = readHostEndpoint();
639
+ }
640
+ if (!endpoint) throw new Error("No local Host to reach the Data API through");
641
+ const res = await fetch(`http://127.0.0.1:${endpoint.port}${LOCAL_DB_PATH}`, {
642
+ method: "POST",
643
+ headers: { authorization: `Bearer ${endpoint.token}`, "content-type": "application/json" },
644
+ body: JSON.stringify(req),
645
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS + 5e3)
646
+ });
647
+ if (!res.ok) throw new Error(`Local Host DB relay answered ${res.status}`);
648
+ return await res.json();
649
+ }
650
+ function relayChannel(topic) {
651
+ const channel = {
652
+ on: () => channel,
653
+ subscribe: (callback) => {
654
+ queueMicrotask(() => callback?.("SUBSCRIBED"));
655
+ return channel;
656
+ },
657
+ send: async (msg) => {
658
+ if (msg.type !== "broadcast" || !sender) return "error";
659
+ sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
660
+ return "ok";
661
+ },
662
+ unsubscribe: async () => "ok"
663
+ };
664
+ return channel;
665
+ }
666
+ function createHostDbClient(via = "socket") {
667
+ const client2 = createClient("https://host-db.invalid", "host-device", {
668
+ auth: { persistSession: false, autoRefreshToken: false },
669
+ global: { fetch: via === "socket" ? hostDbFetch : dbFetch(requestViaLocalHost) }
670
+ });
671
+ return new Proxy(client2, {
672
+ get(target, prop) {
673
+ if (prop === "channel") return relayChannel;
674
+ if (prop === "removeChannel") return async () => "ok";
675
+ const value = Reflect.get(target, prop, target);
676
+ return typeof value === "function" ? value.bind(target) : value;
677
+ }
678
+ });
679
+ }
680
+
310
681
  // src/commands/start.ts
311
682
  async function runStart() {
312
683
  loadEnvFiles();
313
- await preflight();
684
+ const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
685
+ if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
686
+ await preflight({ supabaseViaHost: viaHost });
314
687
  await startServer();
315
688
  }
316
689
 
317
690
  // src/commands/config.ts
318
- import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
691
+ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
319
692
  import { spawn } from "child_process";
320
693
  import { resolve } from "path";
321
694
  function maskToken(value) {
@@ -333,12 +706,12 @@ function printConfig() {
333
706
  console.log("\n Paths:");
334
707
  console.log(` Config dir: ${paths.CONFIG_DIR}`);
335
708
  console.log(` Data dir: ${paths.DATA_DIR}`);
336
- console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
337
- console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
338
- console.log(` Database: ${paths.DB_FILE} ${existsSync(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
709
+ console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync2(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
710
+ console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync2(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
711
+ console.log(` Database: ${paths.DB_FILE} ${existsSync2(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
339
712
  console.log(` Log dir: ${paths.LOG_DIR}`);
340
713
  const repoEnv = resolve(process.cwd(), ".env");
341
- console.log(` Repo .env: ${repoEnv} ${existsSync(repoEnv) ? "(exists)" : "(not found)"}`);
714
+ console.log(` Repo .env: ${repoEnv} ${existsSync2(repoEnv) ? "(exists)" : "(not found)"}`);
342
715
  const envKeys = [
343
716
  "SLACK_BOT_TOKEN",
344
717
  "SLACK_APP_TOKEN",
@@ -362,13 +735,13 @@ function printConfig() {
362
735
  }
363
736
  function editConfig() {
364
737
  ensureDirs();
365
- if (!existsSync(paths.CONFIG_FILE)) {
738
+ if (!existsSync2(paths.CONFIG_FILE)) {
366
739
  const defaultEnv = resolve(paths.PACKAGE_ROOT, ".env.example");
367
- if (existsSync(defaultEnv)) {
368
- const content = readFileSync2(defaultEnv, "utf-8");
369
- writeFileSync2(paths.CONFIG_FILE, content);
740
+ if (existsSync2(defaultEnv)) {
741
+ const content = readFileSync4(defaultEnv, "utf-8");
742
+ writeFileSync4(paths.CONFIG_FILE, content);
370
743
  } else {
371
- writeFileSync2(paths.CONFIG_FILE, "");
744
+ writeFileSync4(paths.CONFIG_FILE, "");
372
745
  }
373
746
  console.log(`[flyspace] Created ${paths.CONFIG_FILE}`);
374
747
  }
@@ -402,14 +775,14 @@ async function runConfig(args) {
402
775
 
403
776
  // src/commands/setup.ts
404
777
  import { execSync as execSync4 } from "child_process";
405
- import { mkdirSync as mkdirSync5 } from "fs";
406
- import { join as join7 } from "path";
778
+ import { mkdirSync as mkdirSync6 } from "fs";
779
+ import { join as join8 } from "path";
407
780
  import { confirm as confirm2 } from "@inquirer/prompts";
408
781
 
409
782
  // src/service.ts
410
783
  import { execSync } from "child_process";
411
- import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
412
- import { basename, dirname as dirname2, join as join2 } from "path";
784
+ import { existsSync as existsSync3, readFileSync as readFileSync5, realpathSync } from "fs";
785
+ import { basename, dirname as dirname3, join as join4 } from "path";
413
786
  import { homedir, platform } from "os";
414
787
  function flyspaceHome(entry = process.argv[1]) {
415
788
  let cliPath;
@@ -422,8 +795,8 @@ function flyspaceHome(entry = process.argv[1]) {
422
795
  if (at <= 0) return void 0;
423
796
  const npmPrefix = cliPath.slice(0, at);
424
797
  if (basename(npmPrefix) !== "cli") return void 0;
425
- const home = dirname2(npmPrefix);
426
- return existsSync2(join2(home, "bin", "flyspace")) && existsSync2(join2(home, "node", "bin", "node")) ? home : void 0;
798
+ const home = dirname3(npmPrefix);
799
+ return existsSync3(join4(home, "bin", "flyspace")) && existsSync3(join4(home, "node", "bin", "node")) ? home : void 0;
427
800
  }
428
801
  function serviceLabel(profile) {
429
802
  return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
@@ -432,40 +805,40 @@ function plistName(profile) {
432
805
  return `${serviceLabel(profile)}.plist`;
433
806
  }
434
807
  function plistPath(profile) {
435
- return join2(homedir(), "Library", "LaunchAgents", plistName(profile));
808
+ return join4(homedir(), "Library", "LaunchAgents", plistName(profile));
436
809
  }
437
810
  function legacyServiceLabel(profile) {
438
811
  return profile === DEFAULT_PROFILE ? "io.flycoder.flyspace" : `io.flycoder.flyspace.${profile}`;
439
812
  }
440
813
  function legacyPlistPath(profile) {
441
- return join2(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
814
+ return join4(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
442
815
  }
443
816
  function systemdUnitName(profile) {
444
817
  return profile === DEFAULT_PROFILE ? "flyspace" : `flyspace-${profile}`;
445
818
  }
446
819
  function systemdUnitPath(profile) {
447
- return join2(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
820
+ return join4(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
448
821
  }
449
822
  function isServiceInstalled(profile) {
450
- if (platform() === "darwin") return existsSync2(plistPath(profile));
451
- if (platform() === "linux") return existsSync2(systemdUnitPath(profile));
823
+ if (platform() === "darwin") return existsSync3(plistPath(profile));
824
+ if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
452
825
  return false;
453
826
  }
454
827
  function serviceBinaryMissing(file) {
455
828
  let text;
456
829
  try {
457
- text = readFileSync3(file, "utf-8");
830
+ text = readFileSync5(file, "utf-8");
458
831
  } catch {
459
832
  return false;
460
833
  }
461
834
  const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
462
- return !!bin && !existsSync2(bin);
835
+ return !!bin && !existsSync3(bin);
463
836
  }
464
837
  function profileServiceFile(profile) {
465
838
  return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
466
839
  }
467
840
  function hostServiceFile() {
468
- return platform() === "darwin" ? hostPlistPath() : join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
841
+ return platform() === "darwin" ? hostPlistPath() : join4(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
469
842
  }
470
843
  function restartService(profile) {
471
844
  const os = platform();
@@ -495,10 +868,10 @@ function restartService(profile) {
495
868
  var HOST_SERVICE_LABEL = "ai.flyspace.dashboard";
496
869
  var HOST_SYSTEMD_UNIT = "flyspace-host";
497
870
  function hostPlistPath() {
498
- return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
871
+ return join4(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
499
872
  }
500
873
  function isHostServiceInstalled() {
501
- if (platform() === "darwin" || platform() === "linux") return existsSync2(hostServiceFile());
874
+ if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
502
875
  return false;
503
876
  }
504
877
  function restartHostService() {
@@ -538,9 +911,9 @@ function isHostServiceRunning() {
538
911
  }
539
912
 
540
913
  // src/commands/install.ts
541
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
914
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "fs";
542
915
  import { execSync as execSync2 } from "child_process";
543
- import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
916
+ import { dirname as dirname4, join as join5, resolve as resolve2 } from "path";
544
917
  import { homedir as homedir2, platform as platform2 } from "os";
545
918
  function log(msg) {
546
919
  console.log(`
@@ -561,23 +934,23 @@ function run(cmd, opts = {}) {
561
934
  function detectClaudeBinDir() {
562
935
  try {
563
936
  const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
564
- return claudePath ? dirname3(claudePath) : void 0;
937
+ return claudePath ? dirname4(claudePath) : void 0;
565
938
  } catch {
566
939
  return void 0;
567
940
  }
568
941
  }
569
942
  function detectNodeBinDir() {
570
943
  const home = flyspaceHome();
571
- return home ? join3(home, "node", "bin") : dirname3(process.execPath);
944
+ return home ? join5(home, "node", "bin") : dirname4(process.execPath);
572
945
  }
573
946
  function nodeShimDirs() {
574
- return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
575
- (d) => existsSync3(d)
947
+ return [join5(homedir2(), ".local", "share", "mise", "shims"), join5(homedir2(), ".asdf", "shims")].filter(
948
+ (d) => existsSync4(d)
576
949
  );
577
950
  }
578
951
  function detectCustieBin() {
579
952
  const home = flyspaceHome();
580
- if (home) return join3(home, "bin", "flyspace");
953
+ if (home) return join5(home, "bin", "flyspace");
581
954
  const argv1 = process.argv[1];
582
955
  if (argv1) {
583
956
  return resolve2(argv1);
@@ -589,9 +962,9 @@ function detectCustieBin() {
589
962
  }
590
963
  }
591
964
  function readEnvFile() {
592
- if (!existsSync3(paths.CONFIG_FILE)) return {};
965
+ if (!existsSync4(paths.CONFIG_FILE)) return {};
593
966
  const vars = {};
594
- for (const line of readFileSync4(paths.CONFIG_FILE, "utf-8").split("\n")) {
967
+ for (const line of readFileSync6(paths.CONFIG_FILE, "utf-8").split("\n")) {
595
968
  const trimmed = line.trim();
596
969
  if (!trimmed || trimmed.startsWith("#")) continue;
597
970
  const eq = trimmed.indexOf("=");
@@ -635,9 +1008,9 @@ async function installMacOS(spec) {
635
1008
  const custieBin = detectCustieBin();
636
1009
  const claudeBinDir = detectClaudeBinDir();
637
1010
  const logDir = paths.LOG_DIR;
638
- mkdirSync2(logDir, { recursive: true });
1011
+ mkdirSync4(logDir, { recursive: true });
639
1012
  const envVars = spec.env;
640
- const homebrewBin = existsSync3("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
1013
+ const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
641
1014
  const pathParts = [
642
1015
  detectNodeBinDir(),
643
1016
  claudeBinDir,
@@ -676,14 +1049,14 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
676
1049
  <true/>
677
1050
 
678
1051
  <key>StandardOutPath</key>
679
- <string>${join3(logDir, `${spec.logName}.log`)}</string>
1052
+ <string>${join5(logDir, `${spec.logName}.log`)}</string>
680
1053
 
681
1054
  <key>StandardErrorPath</key>
682
- <string>${join3(logDir, `${spec.logName}-error.log`)}</string>
1055
+ <string>${join5(logDir, `${spec.logName}-error.log`)}</string>
683
1056
  </dict>
684
1057
  </plist>`;
685
1058
  const agentPlistPath = spec.plistPath;
686
- mkdirSync2(dirname3(agentPlistPath), { recursive: true });
1059
+ mkdirSync4(dirname4(agentPlistPath), { recursive: true });
687
1060
  try {
688
1061
  run(`launchctl unload "${agentPlistPath}"`, { silent: true });
689
1062
  } catch {
@@ -692,7 +1065,7 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
692
1065
  unlinkSync(agentPlistPath);
693
1066
  } catch {
694
1067
  }
695
- writeFileSync3(agentPlistPath, plist);
1068
+ writeFileSync5(agentPlistPath, plist);
696
1069
  run(`launchctl load "${agentPlistPath}"`, { silent: true });
697
1070
  success(`LaunchAgent installed and loaded (${spec.label}).`);
698
1071
  console.log(` Logs: ${logDir}`);
@@ -703,9 +1076,9 @@ async function installLinux(spec) {
703
1076
  const custieBin = detectCustieBin();
704
1077
  const claudeBinDir = detectClaudeBinDir();
705
1078
  const logDir = paths.LOG_DIR;
706
- mkdirSync2(logDir, { recursive: true });
1079
+ mkdirSync4(logDir, { recursive: true });
707
1080
  const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
708
- const linuxbrewBin = existsSync3("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
1081
+ const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
709
1082
  const pathParts = [
710
1083
  detectNodeBinDir(),
711
1084
  claudeBinDir,
@@ -729,16 +1102,16 @@ RestartSec=5
729
1102
  ${envLines}
730
1103
  Environment="PATH=${pathParts.join(":")}"
731
1104
 
732
- StandardOutput=append:${join3(logDir, `${spec.logName}.log`)}
733
- StandardError=append:${join3(logDir, `${spec.logName}-error.log`)}
1105
+ StandardOutput=append:${join5(logDir, `${spec.logName}.log`)}
1106
+ StandardError=append:${join5(logDir, `${spec.logName}-error.log`)}
734
1107
 
735
1108
  [Install]
736
1109
  WantedBy=default.target
737
1110
  `;
738
1111
  const unitName = spec.unitName;
739
- const unitDir = join3(homedir2(), ".config", "systemd", "user");
740
- mkdirSync2(unitDir, { recursive: true });
741
- writeFileSync3(join3(unitDir, `${unitName}.service`), unit);
1112
+ const unitDir = join5(homedir2(), ".config", "systemd", "user");
1113
+ mkdirSync4(unitDir, { recursive: true });
1114
+ writeFileSync5(join5(unitDir, `${unitName}.service`), unit);
742
1115
  run("systemctl --user daemon-reload", { silent: true });
743
1116
  run(`systemctl --user enable --now ${unitName}`, { silent: true });
744
1117
  success(`systemd user service installed and started (${unitName}).`);
@@ -748,277 +1121,100 @@ WantedBy=default.target
748
1121
  async function installService(spec) {
749
1122
  const os = platform2();
750
1123
  if (os === "darwin") await installMacOS(spec);
751
- else if (os === "linux") await installLinux(spec);
752
- else {
753
- warn(`Unsupported platform: ${os}. You'll need to configure autostart manually.`);
754
- return false;
755
- }
756
- return true;
757
- }
758
- async function runInstall() {
759
- ensureDirs();
760
- await installService(profileSpec());
761
- }
762
- async function installHostService() {
763
- ensureDirs();
764
- return installService(hostSpec());
765
- }
766
-
767
- // src/commands/profiles.ts
768
- import { existsSync as existsSync4, readdirSync, statSync } from "fs";
769
- import { execSync as execSync3 } from "child_process";
770
- import { join as join4 } from "path";
771
- import { platform as platform3 } from "os";
772
- function listProfiles() {
773
- const profiles = [];
774
- if (existsSync4(paths.PROFILES_DIR)) {
775
- for (const entry of readdirSync(paths.PROFILES_DIR)) {
776
- try {
777
- if (statSync(join4(paths.PROFILES_DIR, entry)).isDirectory()) {
778
- profiles.push(entry);
779
- }
780
- } catch {
781
- }
782
- }
783
- }
784
- return profiles;
785
- }
786
- function isServiceRunning2(profile) {
787
- try {
788
- if (platform3() === "darwin") {
789
- const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
790
- return out.split("\n").some((line) => line.endsWith(` ${serviceLabel(profile)}`));
791
- }
792
- if (platform3() === "linux") {
793
- execSync3(`systemctl --user is-active ${systemdUnitName(profile)}`, { stdio: "pipe" });
794
- return true;
795
- }
796
- } catch {
797
- }
798
- return false;
799
- }
800
-
801
- // src/auth/device-auth.ts
802
- import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
803
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
804
- import { dirname as dirname4, join as join5 } from "path";
805
- var DEFAULT_API_URL = "https://app.flyspace.ai";
806
- var EXPIRY_SKEW_MS = 6e4;
807
- var AuthApiError = class extends Error {
808
- constructor(message, status) {
809
- super(message);
810
- this.status = status;
811
- this.name = "AuthApiError";
812
- }
813
- };
814
- function apiUrl() {
815
- return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
816
- }
817
- function deviceKeyPath() {
818
- return join5(paths.BASE_CONFIG_DIR, "device.key");
819
- }
820
- function credentialsPath() {
821
- return join5(paths.BASE_CONFIG_DIR, "credentials.json");
822
- }
823
- function ensureDeviceKey(path = deviceKeyPath()) {
824
- let created = false;
825
- if (!existsSync5(path)) {
826
- const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
827
- mkdirSync3(dirname4(path), { recursive: true });
828
- writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
829
- created = true;
830
- }
831
- const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
832
- const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
833
- "base64"
834
- );
835
- return { privateKey, publicKeyBase64, created };
836
- }
837
- function loadCredentials(path = credentialsPath()) {
838
- if (!existsSync5(path)) return null;
839
- try {
840
- return JSON.parse(readFileSync5(path, "utf8"));
841
- } catch {
842
- return null;
843
- }
844
- }
845
- function saveCredentials(creds, path = credentialsPath()) {
846
- mkdirSync3(dirname4(path), { recursive: true });
847
- writeFileSync4(path, `${JSON.stringify(creds, null, 2)}
848
- `, { mode: 384 });
849
- }
850
- function clearLocalAuth() {
851
- rmSync2(credentialsPath(), { force: true });
852
- rmSync2(deviceKeyPath(), { force: true });
853
- }
854
- async function post(base, path, body, token) {
855
- return fetch(`${base}${path}`, {
856
- method: "POST",
857
- headers: {
858
- "content-type": "application/json",
859
- ...token ? { authorization: `Bearer ${token}` } : {}
860
- },
861
- body: JSON.stringify(body)
862
- });
863
- }
864
- async function readJson(res) {
865
- try {
866
- return await res.json();
867
- } catch {
868
- return {};
869
- }
870
- }
871
- async function expectOk(res, what) {
872
- const json = await readJson(res);
873
- if (!res.ok) {
874
- const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
875
- throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
876
- }
877
- return json;
878
- }
879
- function toSlugs(value) {
880
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
881
- }
882
- function toGrant(json) {
883
- const user = json["user"] ?? {};
884
- return {
885
- accessToken: String(json["access_token"]),
886
- expiresIn: Number(json["expires_in"]),
887
- deviceId: String(json["device_id"]),
888
- userEmail: String(user["email"] ?? ""),
889
- workspaces: toSlugs(json["workspaces"])
890
- };
891
- }
892
- async function requestDeviceCode(base, input2) {
893
- const res = await post(base, "/api/auth/device/code", {
894
- public_key: input2.publicKey,
895
- name: input2.name,
896
- platform: input2.platform
897
- });
898
- const json = await expectOk(res, "Requesting a device code");
899
- return {
900
- deviceCode: String(json["device_code"]),
901
- userCode: String(json["user_code"]),
902
- verificationUri: String(json["verification_uri"]),
903
- expiresIn: Number(json["expires_in"]),
904
- interval: Number(json["interval"] ?? 5)
905
- };
906
- }
907
- async function pollDeviceToken(base, deviceCode) {
908
- const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
909
- const json = await readJson(res);
910
- if (res.ok) return { status: "approved", grant: toGrant(json) };
911
- switch (json["error"]) {
912
- case "authorization_pending":
913
- return { status: "pending" };
914
- case "slow_down":
915
- return { status: "slow_down" };
916
- case "access_denied":
917
- return { status: "denied" };
918
- case "expired_token":
919
- return { status: "expired" };
920
- default:
921
- throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
1124
+ else if (os === "linux") await installLinux(spec);
1125
+ else {
1126
+ warn(`Unsupported platform: ${os}. You'll need to configure autostart manually.`);
1127
+ return false;
922
1128
  }
1129
+ return true;
923
1130
  }
924
- async function refreshAccessToken(creds, privateKey) {
925
- const challenge = await expectOk(
926
- await post(creds.apiUrl, "/api/auth/device/challenge", { device_id: creds.deviceId }),
927
- "Requesting a refresh challenge"
928
- );
929
- const nonce = String(challenge["nonce"]);
930
- const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
931
- const json = await expectOk(
932
- await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
933
- "Refreshing the access token"
934
- );
935
- return {
936
- ...creds,
937
- accessToken: String(json["access_token"]),
938
- accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
939
- };
1131
+ async function runInstall() {
1132
+ ensureDirs();
1133
+ await installService(profileSpec());
940
1134
  }
941
- async function fetchWhoAmI(base, token) {
942
- const res = await fetch(`${base}/api/auth/device/me`, { headers: { authorization: `Bearer ${token}` } });
943
- const json = await expectOk(res, "Checking the session");
944
- const user = json["user"] ?? {};
945
- const device = json["device"] ?? {};
946
- return {
947
- userEmail: String(user["email"] ?? ""),
948
- deviceId: String(device["id"] ?? ""),
949
- deviceName: String(device["name"] ?? ""),
950
- workspaces: toSlugs(json["workspaces"])
951
- };
1135
+ async function installHostService() {
1136
+ ensureDirs();
1137
+ return installService(hostSpec());
952
1138
  }
953
- async function boundWorkspaces() {
954
- const creds = loadCredentials();
955
- if (!creds) return null;
1139
+
1140
+ // src/commands/profiles.ts
1141
+ import { existsSync as existsSync5, readdirSync, statSync } from "fs";
1142
+ import { execSync as execSync3 } from "child_process";
1143
+ import { join as join6 } from "path";
1144
+ import { platform as platform3 } from "os";
1145
+ function listProfiles() {
1146
+ const profiles = [];
1147
+ if (existsSync5(paths.PROFILES_DIR)) {
1148
+ for (const entry of readdirSync(paths.PROFILES_DIR)) {
1149
+ try {
1150
+ if (statSync(join6(paths.PROFILES_DIR, entry)).isDirectory()) {
1151
+ profiles.push(entry);
1152
+ }
1153
+ } catch {
1154
+ }
1155
+ }
1156
+ }
1157
+ return profiles;
1158
+ }
1159
+ function isServiceRunning2(profile) {
956
1160
  try {
957
- const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
958
- saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
959
- return { workspaces: me.workspaces, fresh: true };
1161
+ if (platform3() === "darwin") {
1162
+ const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
1163
+ return out.split("\n").some((line) => line.endsWith(` ${serviceLabel(profile)}`));
1164
+ }
1165
+ if (platform3() === "linux") {
1166
+ execSync3(`systemctl --user is-active ${systemdUnitName(profile)}`, { stdio: "pipe" });
1167
+ return true;
1168
+ }
960
1169
  } catch {
961
- return { workspaces: creds.workspaces ?? [], fresh: false };
962
1170
  }
963
- }
964
- async function revokeDevice(base, token) {
965
- await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
966
- }
967
- async function getAccessToken() {
968
- const fromEnv = process.env["FLYSPACE_TOKEN"];
969
- if (fromEnv) return fromEnv;
970
- const creds = loadCredentials();
971
- if (!creds) return null;
972
- if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
973
- const { privateKey } = ensureDeviceKey();
974
- const refreshed = await refreshAccessToken(creds, privateKey);
975
- saveCredentials(refreshed);
976
- return refreshed.accessToken;
1171
+ return false;
977
1172
  }
978
1173
 
979
1174
  // src/dashboard/onboarding-scan.ts
980
- import { execFileSync } from "child_process";
1175
+ import { execFileSync as execFileSync2 } from "child_process";
981
1176
  import {
1177
+ cpSync,
982
1178
  existsSync as existsSync6,
983
1179
  lstatSync,
984
- mkdirSync as mkdirSync4,
1180
+ mkdirSync as mkdirSync5,
985
1181
  readdirSync as readdirSync2,
986
- readFileSync as readFileSync6,
1182
+ readFileSync as readFileSync7,
987
1183
  statSync as statSync2,
988
1184
  symlinkSync
989
1185
  } from "fs";
990
1186
  import { homedir as homedir3 } from "os";
991
- import { basename as basename2, join as join6, resolve as resolve3 } from "path";
1187
+ import { basename as basename2, join as join7, resolve as resolve3 } from "path";
992
1188
  var MAX_JSONL_HEAD_BYTES = 64 * 1024;
993
1189
  var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
994
1190
  function flyspaceRoot() {
995
- return process.env["FLYSPACE_ROOT"] || join6(homedir3(), "Flyspace");
1191
+ return process.env["FLYSPACE_ROOT"] || join7(homedir3(), "Flyspace");
996
1192
  }
997
1193
  function onPath(cmd) {
998
1194
  try {
999
- execFileSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
1195
+ execFileSync2(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
1000
1196
  return true;
1001
1197
  } catch {
1002
1198
  return false;
1003
1199
  }
1004
1200
  }
1005
1201
  function claudeDir() {
1006
- return process.env["CLAUDE_CONFIG_DIR"] || join6(homedir3(), ".claude");
1202
+ return process.env["CLAUDE_CONFIG_DIR"] || join7(homedir3(), ".claude");
1007
1203
  }
1008
1204
  function codexDir() {
1009
- return process.env["CODEX_HOME"] || join6(homedir3(), ".codex");
1205
+ return process.env["CODEX_HOME"] || join7(homedir3(), ".codex");
1010
1206
  }
1011
1207
  function claudeLoggedIn() {
1012
1208
  if (process.env["ANTHROPIC_API_KEY"]) return true;
1013
- if (existsSync6(join6(claudeDir(), ".credentials.json"))) return true;
1209
+ if (existsSync6(join7(claudeDir(), ".credentials.json"))) return true;
1014
1210
  try {
1015
- const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
1211
+ const raw = JSON.parse(readFileSync7(join7(homedir3(), ".claude.json"), "utf-8"));
1016
1212
  if (raw.oauthAccount) return true;
1017
1213
  } catch {
1018
1214
  }
1019
1215
  if (process.platform !== "darwin") return false;
1020
1216
  try {
1021
- execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
1217
+ execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
1022
1218
  stdio: "ignore"
1023
1219
  });
1024
1220
  return true;
@@ -1027,7 +1223,7 @@ function claudeLoggedIn() {
1027
1223
  }
1028
1224
  }
1029
1225
  function codexLoggedIn() {
1030
- return !!process.env["OPENAI_API_KEY"] || existsSync6(join6(codexDir(), "auth.json"));
1226
+ return !!process.env["OPENAI_API_KEY"] || existsSync6(join7(codexDir(), "auth.json"));
1031
1227
  }
1032
1228
  function jsonlFiles(dir, depth) {
1033
1229
  const out = [];
@@ -1038,7 +1234,7 @@ function jsonlFiles(dir, depth) {
1038
1234
  return out;
1039
1235
  }
1040
1236
  for (const e of entries2) {
1041
- const full = join6(dir, e.name);
1237
+ const full = join7(dir, e.name);
1042
1238
  if (e.isDirectory()) {
1043
1239
  if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
1044
1240
  } else if (e.name.endsWith(".jsonl")) {
@@ -1053,7 +1249,7 @@ function jsonlFiles(dir, depth) {
1053
1249
  function cwdFromTranscript(file) {
1054
1250
  let head;
1055
1251
  try {
1056
- head = readFileSync6(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1252
+ head = readFileSync7(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1057
1253
  } catch {
1058
1254
  return null;
1059
1255
  }
@@ -1070,7 +1266,7 @@ function cwdFromTranscript(file) {
1070
1266
  }
1071
1267
  function codexTrustedPaths() {
1072
1268
  try {
1073
- const toml = readFileSync6(join6(codexDir(), "config.toml"), "utf-8");
1269
+ const toml = readFileSync7(join7(codexDir(), "config.toml"), "utf-8");
1074
1270
  return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
1075
1271
  } catch {
1076
1272
  return [];
@@ -1093,7 +1289,7 @@ function gitRepos() {
1093
1289
  const home = homedir3();
1094
1290
  const found = [];
1095
1291
  for (const rootName of GIT_SCAN_ROOTS) {
1096
- const root = join6(home, rootName);
1292
+ const root = join7(home, rootName);
1097
1293
  let entries2;
1098
1294
  try {
1099
1295
  entries2 = readdirSync2(root, { withFileTypes: true });
@@ -1101,7 +1297,7 @@ function gitRepos() {
1101
1297
  continue;
1102
1298
  }
1103
1299
  for (const e of entries2) {
1104
- if (e.isDirectory() && existsSync6(join6(root, e.name, ".git"))) found.push(join6(root, e.name));
1300
+ if (e.isDirectory() && existsSync6(join7(root, e.name, ".git"))) found.push(join7(root, e.name));
1105
1301
  }
1106
1302
  }
1107
1303
  return found;
@@ -1145,11 +1341,11 @@ function scanOnboarding() {
1145
1341
  });
1146
1342
  }
1147
1343
  };
1148
- const claudeProjects = join6(claudeDir(), "projects");
1344
+ const claudeProjects = join7(claudeDir(), "projects");
1149
1345
  try {
1150
1346
  for (const e of readdirSync2(claudeProjects, { withFileTypes: true })) {
1151
1347
  if (!e.isDirectory()) continue;
1152
- const files = jsonlFiles(join6(claudeProjects, e.name), 0);
1348
+ const files = jsonlFiles(join7(claudeProjects, e.name), 0);
1153
1349
  if (!files.length) continue;
1154
1350
  history.claude = true;
1155
1351
  files.sort((a, b) => b.mtime - a.mtime);
@@ -1162,7 +1358,7 @@ function scanOnboarding() {
1162
1358
  }
1163
1359
  } catch {
1164
1360
  }
1165
- const codexSessions = jsonlFiles(join6(codexDir(), "sessions"), 4);
1361
+ const codexSessions = jsonlFiles(join7(codexDir(), "sessions"), 4);
1166
1362
  if (codexSessions.length) history.codex = true;
1167
1363
  const perCwd = /* @__PURE__ */ new Map();
1168
1364
  for (const f of codexSessions) {
@@ -1199,17 +1395,18 @@ function scanOnboarding() {
1199
1395
  fromGitScan
1200
1396
  };
1201
1397
  }
1398
+ var GENERAL_TEMPLATE_DIR = join7(paths.PACKAGE_ROOT, "templates", "general");
1202
1399
  function makeLink(target, linkPath) {
1203
1400
  symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
1204
1401
  }
1205
1402
  function linkProjectFolders(slug, items) {
1206
1403
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
1207
- const dir = join6(flyspaceRoot(), slug);
1208
- mkdirSync4(dir, { recursive: true });
1404
+ const dir = join7(flyspaceRoot(), slug);
1405
+ mkdirSync5(dir, { recursive: true });
1209
1406
  const results = [];
1210
1407
  for (const item of items) {
1211
1408
  if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
1212
- const dest = join6(dir, item.name);
1409
+ const dest = join7(dir, item.name);
1213
1410
  let exists = true;
1214
1411
  try {
1215
1412
  lstatSync(dest);
@@ -1222,7 +1419,8 @@ function linkProjectFolders(slug, items) {
1222
1419
  if (!statSync2(target).isDirectory()) throw new Error(`not a directory: ${target}`);
1223
1420
  makeLink(target, dest);
1224
1421
  } else {
1225
- mkdirSync4(dest, { recursive: true });
1422
+ mkdirSync5(dest, { recursive: true });
1423
+ if (item.name === "general") cpSync(GENERAL_TEMPLATE_DIR, dest, { recursive: true });
1226
1424
  }
1227
1425
  }
1228
1426
  results.push({ name: item.name, cwd: `${slug}/${item.name}` });
@@ -1540,7 +1738,7 @@ async function runAuthCmd(args) {
1540
1738
  }
1541
1739
 
1542
1740
  // src/version.ts
1543
- var CLI_VERSION = true ? "0.1.13" : null;
1741
+ var CLI_VERSION = true ? "0.1.14" : null;
1544
1742
  function isOlderVersion(a, b) {
1545
1743
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1546
1744
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -1654,8 +1852,8 @@ ${BOLD2}Flyspace Setup${RESET2}
1654
1852
  const anyReady = runners.some((r) => r.installed && r.loggedIn);
1655
1853
  for (const slug of workspaces) {
1656
1854
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) continue;
1657
- const dir = join7(flyspaceRoot(), slug);
1658
- mkdirSync5(dir, { recursive: true });
1855
+ const dir = join8(flyspaceRoot(), slug);
1856
+ mkdirSync6(dir, { recursive: true });
1659
1857
  ok(`Folder ${tilde2(dir)}`);
1660
1858
  }
1661
1859
  let hostRunning = false;
@@ -1818,10 +2016,10 @@ ${BOLD3}Flyspace Doctor${RESET3}
1818
2016
  }
1819
2017
 
1820
2018
  // src/commands/uninstall.ts
1821
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
2019
+ import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
1822
2020
  import { confirm as confirm3 } from "@inquirer/prompts";
1823
2021
  import { execSync as execSync5 } from "child_process";
1824
- import { basename as basename3, join as join8 } from "path";
2022
+ import { basename as basename3, join as join9 } from "path";
1825
2023
  import { homedir as homedir4, platform as platform5 } from "os";
1826
2024
  function log2(msg) {
1827
2025
  console.log(`
@@ -1864,7 +2062,7 @@ async function runServiceUninstall(profile = getProfile()) {
1864
2062
  run2(`systemctl --user disable --now ${unitName}`);
1865
2063
  } catch {
1866
2064
  }
1867
- const unitPath = join8(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
2065
+ const unitPath = join9(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
1868
2066
  try {
1869
2067
  unlinkSync2(unitPath);
1870
2068
  } catch {
@@ -1879,7 +2077,7 @@ async function runServiceUninstall(profile = getProfile()) {
1879
2077
  }
1880
2078
  }
1881
2079
  async function runUninstall(args = []) {
1882
- const prefix = process.env["FLYSPACE_PREFIX"] || join8(homedir4(), ".flyspace");
2080
+ const prefix = process.env["FLYSPACE_PREFIX"] || join9(homedir4(), ".flyspace");
1883
2081
  const targets = [paths.BASE_CONFIG_DIR, paths.BASE_DATA_DIR, prefix].filter(existsSync7);
1884
2082
  console.log("\n This removes Flyspace from this machine:");
1885
2083
  console.log(" - revokes this device and removes every background service");
@@ -1905,15 +2103,15 @@ async function runUninstall(args = []) {
1905
2103
  await runServiceUninstall(profile);
1906
2104
  }
1907
2105
  for (const t of targets) {
1908
- rmSync3(t, { recursive: true, force: true });
2106
+ rmSync4(t, { recursive: true, force: true });
1909
2107
  success2(`Deleted ${t}`);
1910
2108
  }
1911
- for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) => join8(homedir4(), f))) {
2109
+ for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) => join9(homedir4(), f))) {
1912
2110
  if (!existsSync7(rc)) continue;
1913
- const before = readFileSync7(rc, "utf-8");
2111
+ const before = readFileSync8(rc, "utf-8");
1914
2112
  const after = before.replace(/\n# Flyspace CLI\nexport PATH="[^"\n]*\.flyspace\/bin:\$PATH"\n?/g, "\n");
1915
2113
  if (after !== before) {
1916
- writeFileSync5(rc, after);
2114
+ writeFileSync6(rc, after);
1917
2115
  success2(`Removed PATH entry from ${rc}`);
1918
2116
  }
1919
2117
  }
@@ -1921,7 +2119,7 @@ async function runUninstall(args = []) {
1921
2119
  }
1922
2120
 
1923
2121
  // src/commands/profiles-cmd.ts
1924
- import { rmSync as rmSync4 } from "fs";
2122
+ import { rmSync as rmSync5 } from "fs";
1925
2123
  var USAGE = `
1926
2124
  Usage: flyspace profiles [list]
1927
2125
  flyspace profiles remove <name>
@@ -1978,7 +2176,7 @@ async function removeCmd(name) {
1978
2176
  }
1979
2177
  const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
1980
2178
  if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
1981
- rmSync4(configDir2, { recursive: true, force: true });
2179
+ rmSync5(configDir2, { recursive: true, force: true });
1982
2180
  console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
1983
2181
  console.log(`
1984
2182
  Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
@@ -1996,7 +2194,7 @@ async function runProfiles(args = []) {
1996
2194
  // src/commands/upgrade.ts
1997
2195
  import { execSync as execSync6 } from "child_process";
1998
2196
  import { realpathSync as realpathSync2 } from "fs";
1999
- import { join as join9 } from "path";
2197
+ import { join as join10 } from "path";
2000
2198
  function restartInstalledProfiles() {
2001
2199
  if (isHostServiceInstalled()) {
2002
2200
  console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
@@ -2029,10 +2227,10 @@ function npmInstallCommand() {
2029
2227
  }
2030
2228
  function npmEnv() {
2031
2229
  const home = flyspaceHome();
2032
- return home ? { ...process.env, PATH: `${join9(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2230
+ return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2033
2231
  }
2034
2232
  async function runUpgrade() {
2035
- const current = "0.1.13";
2233
+ const current = "0.1.14";
2036
2234
  console.log(`Current version: ${current}`);
2037
2235
  console.log("Checking for updates...\n");
2038
2236
  try {
@@ -2646,11 +2844,11 @@ async function runRestart() {
2646
2844
  // src/commands/logs.ts
2647
2845
  import { spawn as spawn3 } from "child_process";
2648
2846
  import { existsSync as existsSync9 } from "fs";
2649
- import { join as join10 } from "path";
2847
+ import { join as join11 } from "path";
2650
2848
  async function runLogs(args) {
2651
2849
  const showErrors = args.includes("--error") || args.includes("-e");
2652
2850
  const filename = showErrors ? "custie-error.log" : "custie.log";
2653
- const logFile = join10(paths.LOG_DIR, filename);
2851
+ const logFile = join11(paths.LOG_DIR, filename);
2654
2852
  if (!existsSync9(logFile)) {
2655
2853
  console.error(`Log file not found: ${logFile}`);
2656
2854
  console.error("Is the service installed? Run `flyspace install` first.");
@@ -2679,7 +2877,7 @@ import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts"
2679
2877
  import { WebClient as WebClient3 } from "@slack/web-api";
2680
2878
 
2681
2879
  // src/store/auth-links.ts
2682
- import { createClient } from "@supabase/supabase-js";
2880
+ import { createClient as createClient2 } from "@supabase/supabase-js";
2683
2881
  function toAuthLink(data) {
2684
2882
  const userId = data.user?.id;
2685
2883
  const actionLink = data.properties?.action_link;
@@ -2703,7 +2901,7 @@ async function mintSession(email) {
2703
2901
  if (error2) throw new Error(error2.message);
2704
2902
  const tokenHash = data.properties?.hashed_token;
2705
2903
  if (!tokenHash) throw new Error("Supabase returned no token hash");
2706
- const scratch = createClient(process.env["SUPABASE_URL"], process.env["SUPABASE_SECRET_KEY"], {
2904
+ const scratch = createClient2(process.env["SUPABASE_URL"], process.env["SUPABASE_SECRET_KEY"], {
2707
2905
  auth: { persistSession: false, autoRefreshToken: false }
2708
2906
  });
2709
2907
  const verified = await scratch.auth.verifyOtp({ token_hash: tokenHash, type: "magiclink" });
@@ -2729,7 +2927,7 @@ async function emailForToken(token, type) {
2729
2927
 
2730
2928
  // src/dashboard/profile-config.ts
2731
2929
  import { homedir as homedir5 } from "os";
2732
- import { join as join11 } from "path";
2930
+ import { join as join12 } from "path";
2733
2931
 
2734
2932
  // src/dashboard/profile-config-cache.ts
2735
2933
  var cache = /* @__PURE__ */ new Map();
@@ -2748,7 +2946,7 @@ function profileEnv(key) {
2748
2946
  function profileClaudeConfigDir() {
2749
2947
  const config = cachedProfileConfig();
2750
2948
  if (!config) return process.env["CLAUDE_CONFIG_DIR"] || void 0;
2751
- return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join11(homedir5(), ".claude");
2949
+ return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join12(homedir5(), ".claude");
2752
2950
  }
2753
2951
  function profileOauthToken() {
2754
2952
  const env = cachedProfileConfig()?.env ?? {};
@@ -2850,7 +3048,7 @@ async function sendEmail(email) {
2850
3048
  }
2851
3049
 
2852
3050
  // src/store/invite-store.ts
2853
- import { createHash, randomBytes } from "crypto";
3051
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
2854
3052
  var INVITE_TTL_DAYS = 7;
2855
3053
  var INVITE_MESSAGE_MAX = 500;
2856
3054
  function hashInviteToken(token) {
@@ -2860,7 +3058,7 @@ async function issueInvite(membershipId, message) {
2860
3058
  const db = getSupabaseClient();
2861
3059
  const { error: clearError } = await db.from("workspace_invites").delete().eq("membership_id", membershipId).is("accepted_at", null);
2862
3060
  if (clearError) throw new Error(`issueInvite(${membershipId}): ${clearError.message}`);
2863
- const token = randomBytes(32).toString("hex");
3061
+ const token = randomBytes2(32).toString("hex");
2864
3062
  const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
2865
3063
  const { error: error2 } = await db.from("workspace_invites").insert({
2866
3064
  membership_id: membershipId,
@@ -3300,9 +3498,9 @@ function workspaceOrigin(c, slug) {
3300
3498
  }
3301
3499
 
3302
3500
  // src/store/setup-invite-store.ts
3303
- import { randomBytes as randomBytes2 } from "crypto";
3501
+ import { randomBytes as randomBytes3 } from "crypto";
3304
3502
  async function issueSetupInvite(params) {
3305
- const token = randomBytes2(32).toString("hex");
3503
+ const token = randomBytes3(32).toString("hex");
3306
3504
  const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
3307
3505
  const { error: error2 } = await getSupabaseClient().from("workspace_setup_invites").insert({
3308
3506
  email: params.email,
@@ -3329,7 +3527,7 @@ async function reissueSetupInvite(id) {
3329
3527
  const { data, error: error2 } = await db.from("workspace_setup_invites").select("email, owner_name, message").eq("id", id).is("accepted_at", null).maybeSingle();
3330
3528
  if (error2) throw new Error(`reissueSetupInvite(${id}): ${error2.message}`);
3331
3529
  if (!data) throw new Error("no pending invite with that id");
3332
- const token = randomBytes2(32).toString("hex");
3530
+ const token = randomBytes3(32).toString("hex");
3333
3531
  const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
3334
3532
  const { error: updateError } = await db.from("workspace_setup_invites").update({ token_hash: hashInviteToken(token), expires_at: expiresAt.toISOString() }).eq("id", id);
3335
3533
  if (updateError) throw new Error(`reissueSetupInvite(${id}): ${updateError.message}`);
@@ -4208,7 +4406,7 @@ import { serve } from "@hono/node-server";
4208
4406
  import { serveStatic } from "@hono/node-server/serve-static";
4209
4407
 
4210
4408
  // src/reconcile.ts
4211
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
4409
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
4212
4410
  async function boundWorkspaces2() {
4213
4411
  return (await boundWorkspaces())?.workspaces ?? null;
4214
4412
  }
@@ -4225,7 +4423,7 @@ async function reconcileProfile(profile) {
4225
4423
  if (!hadConfigDir) log5.push(`config dir created (${paths.CONFIG_DIR})`);
4226
4424
  if (!hadDataDir) log5.push(`data dir created (${paths.DATA_DIR})`);
4227
4425
  if (!existsSync10(paths.CONFIG_FILE)) {
4228
- writeFileSync6(paths.CONFIG_FILE, `BOT_NAME=${profile}
4426
+ writeFileSync7(paths.CONFIG_FILE, `BOT_NAME=${profile}
4229
4427
  `);
4230
4428
  log5.push(`config.env written (${paths.CONFIG_FILE})`);
4231
4429
  }
@@ -4234,7 +4432,7 @@ async function reconcileProfile(profile) {
4234
4432
  if (!channel.cwd) continue;
4235
4433
  const cwd = expandPath(channel.cwd);
4236
4434
  if (isCwdUsable(cwd)) continue;
4237
- mkdirSync6(cwd, { recursive: true });
4435
+ mkdirSync7(cwd, { recursive: true });
4238
4436
  log5.push(`channel "${channel.name}" cwd created (${cwd})`);
4239
4437
  }
4240
4438
  return log5;
@@ -4396,8 +4594,10 @@ async function filterReadableDocsChannels(ctx, channelIds) {
4396
4594
  // src/dashboard/docs-access-gate.ts
4397
4595
  var requireDocsAccess = async (c, next) => {
4398
4596
  const auth = c.get("auth");
4597
+ const channelId = c.req.param("id") ?? "";
4598
+ if (findChannelById(channelId)?.profile !== getProfile()) return c.json({ error: "channel not found" }, 404);
4399
4599
  const need = c.req.method === "GET" ? "read" : "write";
4400
- const have = auth ? await resolveDocsAccess(auth, c.req.param("id") ?? "") : "none";
4600
+ const have = auth ? await resolveDocsAccess(auth, channelId) : "none";
4401
4601
  if (!docsAccessAtLeast(have, need)) return c.json({ error: "docs access required" }, 403);
4402
4602
  await next();
4403
4603
  };
@@ -4406,8 +4606,8 @@ var requireDocsAccess = async (c, next) => {
4406
4606
  import { Hono as Hono31 } from "hono";
4407
4607
 
4408
4608
  // src/dashboard/public-share-routes.ts
4409
- import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
4410
- import { join as join13 } from "path";
4609
+ import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
4610
+ import { join as join14 } from "path";
4411
4611
  import { Hono as Hono3 } from "hono";
4412
4612
  import { GetObjectCommand } from "@aws-sdk/client-s3";
4413
4613
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -4872,12 +5072,12 @@ function injectShareHead(html, head) {
4872
5072
 
4873
5073
  // src/dashboard/app-icon.ts
4874
5074
  import { createHash as createHash2 } from "crypto";
4875
- import { readFileSync as readFileSync8 } from "fs";
4876
- import { join as join12 } from "path";
5075
+ import { readFileSync as readFileSync9 } from "fs";
5076
+ import { join as join13 } from "path";
4877
5077
  import sharp from "sharp";
4878
5078
  var PLATE = "#0A0A0F";
4879
- var MARK_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
4880
- var MARK_SVG_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
5079
+ var MARK_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
5080
+ var MARK_SVG_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
4881
5081
  var LAYOUT = {
4882
5082
  standard: { logo: 0.7, logoCx: 0.5, logoCy: 0.48, badge: 0.27, badgeCx: 0.22, badgeCy: 0.76 },
4883
5083
  maskable: { logo: 0.56, logoCx: 0.5, logoCy: 0.48, badge: 0.23, badgeCx: 0.33, badgeCy: 0.66 }
@@ -4900,7 +5100,7 @@ function roundedPlate(size, fill = PLATE) {
4900
5100
  }
4901
5101
  async function renderBadge(box) {
4902
5102
  const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
4903
- const raster = await sharp(readFileSync8(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
5103
+ const raster = await sharp(readFileSync9(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
4904
5104
  return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
4905
5105
  }
4906
5106
  async function sampleBackground(logo) {
@@ -4955,7 +5155,7 @@ async function renderAppIcon({
4955
5155
  if (hit) return hit;
4956
5156
  const out = logo && await composeIcon(logo, size, maskable, plain).catch(() => null) || // No workspace logo to lead with — the stock mark is still the best icon,
4957
5157
  // either on its usual dark plate or, for a favicon, trimmed and bare.
4958
- (transparent ? await renderBadge(size) : await sharp(readFileSync8(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
5158
+ (transparent ? await renderBadge(size) : await sharp(readFileSync9(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
4959
5159
  cache3.set(key, out);
4960
5160
  return out;
4961
5161
  }
@@ -6173,7 +6373,7 @@ var shellCache = null;
6173
6373
  function readShell(path) {
6174
6374
  const { mtimeMs } = statSync3(path);
6175
6375
  if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
6176
- const html = readFileSync9(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6376
+ const html = readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6177
6377
  shellCache = { mtimeMs, html };
6178
6378
  return html;
6179
6379
  }
@@ -6193,17 +6393,17 @@ function serveBuildFile(c, webDist, dir, name) {
6193
6393
  if (!type || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes("..")) {
6194
6394
  return c.text("not found", 404);
6195
6395
  }
6196
- const path = join13(webDist, dir, name);
6396
+ const path = join14(webDist, dir, name);
6197
6397
  if (!existsSync11(path)) return c.text("not found", 404);
6198
6398
  c.header("Content-Type", type);
6199
6399
  c.header("Cache-Control", "public, max-age=31536000, immutable");
6200
6400
  if (ext === ".css") {
6201
- return c.body(readFileSync9(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6401
+ return c.body(readFileSync10(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6202
6402
  }
6203
6403
  if (ext === ".js") {
6204
- return c.body(readFileSync9(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6404
+ return c.body(readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6205
6405
  }
6206
- return c.body(new Uint8Array(readFileSync9(path)));
6406
+ return c.body(new Uint8Array(readFileSync10(path)));
6207
6407
  }
6208
6408
  var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
6209
6409
  function servePublishedHtml(c, share) {
@@ -6339,7 +6539,7 @@ ${analytics}` : head);
6339
6539
  const requested = c.req.param("lang");
6340
6540
  const lang = canonicalLanguage(requested);
6341
6541
  if (!isTranslatableLanguage(lang)) return next();
6342
- const shell = join13(webDist, "index.html");
6542
+ const shell = join14(webDist, "index.html");
6343
6543
  if (!existsSync11(shell)) return next();
6344
6544
  const share = await getDocShare(c.req.param("id"));
6345
6545
  if (!share) return next();
@@ -6375,7 +6575,7 @@ ${analytics}` : head);
6375
6575
  const share = await getDocShare(c.req.param("id"));
6376
6576
  if (!share) return next();
6377
6577
  if (share.kind === "html") return servePublishedHtml(c, share);
6378
- const shell = join13(webDist, "index.html");
6578
+ const shell = join14(webDist, "index.html");
6379
6579
  if (!existsSync11(shell)) return next();
6380
6580
  return readerPage(
6381
6581
  c,
@@ -6488,9 +6688,9 @@ function routeCookieWrites(all, label2, writes) {
6488
6688
  }
6489
6689
 
6490
6690
  // src/dashboard/supabase-server-client.ts
6491
- var REQUEST_TIMEOUT_MS = 5e3;
6691
+ var REQUEST_TIMEOUT_MS2 = 5e3;
6492
6692
  function fetchWithTimeout(input2, init) {
6493
- return fetch(input2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
6693
+ return fetch(input2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
6494
6694
  }
6495
6695
  function createRequestSupabaseClient(c) {
6496
6696
  const url = process.env["SUPABASE_URL"];
@@ -6642,13 +6842,13 @@ async function signAvatarUrl(key) {
6642
6842
 
6643
6843
  // src/dashboard/turnstile.ts
6644
6844
  var VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
6645
- var REQUEST_TIMEOUT_MS2 = 5e3;
6845
+ var REQUEST_TIMEOUT_MS3 = 5e3;
6646
6846
  function siteverify(secret, token) {
6647
6847
  return fetch(VERIFY_URL, {
6648
6848
  method: "POST",
6649
6849
  headers: { "content-type": "application/x-www-form-urlencoded" },
6650
6850
  body: new URLSearchParams({ secret, response: token }),
6651
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
6851
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
6652
6852
  });
6653
6853
  }
6654
6854
  async function verifyTurnstileToken(token) {
@@ -7001,12 +7201,12 @@ async function offlineAgentIdentities() {
7001
7201
  }
7002
7202
 
7003
7203
  // src/dashboard/host-api.ts
7004
- import { existsSync as existsSync20, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
7005
- import { join as join20 } from "path";
7204
+ import { existsSync as existsSync20, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
7205
+ import { join as join21 } from "path";
7006
7206
  import sharp3 from "sharp";
7007
7207
 
7008
7208
  // src/dashboard/channels-routes.ts
7009
- import { existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
7209
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
7010
7210
  import { Hono as Hono9 } from "hono";
7011
7211
 
7012
7212
  // src/dashboard/feature-flags-routes.ts
@@ -7154,7 +7354,7 @@ function coerceMemberEmails(raw) {
7154
7354
  }
7155
7355
  function ensureCwdDir(cwd) {
7156
7356
  const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
7157
- if (!existsSync12(expanded)) mkdirSync7(expanded, { recursive: true });
7357
+ if (!existsSync12(expanded)) mkdirSync8(expanded, { recursive: true });
7158
7358
  }
7159
7359
  function toResponse(channel) {
7160
7360
  return {
@@ -7305,24 +7505,24 @@ import {
7305
7505
  closeSync,
7306
7506
  existsSync as existsSync16,
7307
7507
  lstatSync as lstatSync2,
7308
- mkdirSync as mkdirSync8,
7508
+ mkdirSync as mkdirSync9,
7309
7509
  openSync,
7310
7510
  readSync,
7311
7511
  readdirSync as readdirSync4,
7312
- readFileSync as readFileSync12,
7512
+ readFileSync as readFileSync13,
7313
7513
  realpathSync as realpathSync3,
7314
7514
  renameSync,
7315
- rmSync as rmSync6,
7515
+ rmSync as rmSync7,
7316
7516
  statSync as statSync5,
7317
- writeFileSync as writeFileSync8
7517
+ writeFileSync as writeFileSync9
7318
7518
  } from "fs";
7319
- import { basename as basename6, dirname as dirname5, extname, join as join18, relative, resolve as resolve4, sep } from "path";
7519
+ import { basename as basename6, dirname as dirname5, extname, join as join19, relative, resolve as resolve4, sep } from "path";
7320
7520
  import { Hono as Hono10 } from "hono";
7321
7521
  import { streamSSE } from "hono/streaming";
7322
7522
 
7323
7523
  // src/dashboard/docs-watcher.ts
7324
7524
  import { existsSync as existsSync13, watch } from "fs";
7325
- import { basename as basename4, join as join14 } from "path";
7525
+ import { basename as basename4, join as join15 } from "path";
7326
7526
  var DEBOUNCE_MS = 200;
7327
7527
  var MAX_PENDING = 200;
7328
7528
  var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
@@ -7363,7 +7563,7 @@ function watchDocs(cwd, listener) {
7363
7563
  const current = entries.get(cwd);
7364
7564
  if (!current || filename === null) return;
7365
7565
  const rel = filename.split("\\").join("/");
7366
- if (rel === root && !existsSync13(join14(cwd, rel))) return;
7566
+ if (rel === root && !existsSync13(join15(cwd, rel))) return;
7367
7567
  if (isIgnoredDocPath(rel)) return;
7368
7568
  if (current.pending.size < MAX_PENDING) current.pending.add(rel);
7369
7569
  if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
@@ -7600,18 +7800,18 @@ async function readOrQueueDocAudio(input2) {
7600
7800
  }
7601
7801
 
7602
7802
  // src/dashboard/artifact-export.ts
7603
- import { readFileSync as readFileSync11 } from "fs";
7803
+ import { readFileSync as readFileSync12 } from "fs";
7604
7804
 
7605
7805
  // src/dashboard/chat-agent.ts
7606
7806
  import { homedir as homedir7 } from "os";
7607
7807
 
7608
7808
  // src/mcp/config.ts
7609
- import { mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
7809
+ import { mkdtempSync, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
7610
7810
  import { tmpdir as tmpdir2 } from "os";
7611
- import { join as join15 } from "path";
7811
+ import { join as join16 } from "path";
7612
7812
 
7613
7813
  // src/mcp/crypto.ts
7614
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
7814
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
7615
7815
  var ALGORITHM = "aes-256-gcm";
7616
7816
  var KEY_BYTES = 32;
7617
7817
  var IV_BYTES = 12;
@@ -7644,7 +7844,7 @@ function currentKeyVersion() {
7644
7844
  );
7645
7845
  }
7646
7846
  function encrypt(key, purpose, plaintext) {
7647
- const iv = randomBytes3(IV_BYTES);
7847
+ const iv = randomBytes4(IV_BYTES);
7648
7848
  const cipher = createCipheriv(ALGORITHM, key, iv);
7649
7849
  cipher.setAAD(Buffer.from(purpose, "utf8"));
7650
7850
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
@@ -7664,7 +7864,7 @@ function decrypt(key, purpose, blob) {
7664
7864
  }
7665
7865
  function createEnvelope() {
7666
7866
  const keyVersion = currentKeyVersion();
7667
- const key = randomBytes3(KEY_BYTES);
7867
+ const key = randomBytes4(KEY_BYTES);
7668
7868
  const wrappedDek = encrypt(
7669
7869
  readMasterKey(keyVersion),
7670
7870
  purposeForDek(keyVersion),
@@ -7730,7 +7930,7 @@ function secretsMatch(a, b) {
7730
7930
  }
7731
7931
 
7732
7932
  // src/mcp/oauth.ts
7733
- import { createHash as createHash7, randomBytes as randomBytes4 } from "crypto";
7933
+ import { createHash as createHash7, randomBytes as randomBytes5 } from "crypto";
7734
7934
  var RESERVED_AUTHORIZE_PARAMS = /* @__PURE__ */ new Set([
7735
7935
  "client_id",
7736
7936
  "redirect_uri",
@@ -7744,10 +7944,10 @@ function base64url(buffer) {
7744
7944
  return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7745
7945
  }
7746
7946
  function createStateToken() {
7747
- return base64url(randomBytes4(32));
7947
+ return base64url(randomBytes5(32));
7748
7948
  }
7749
7949
  function createPkcePair() {
7750
- const verifier = base64url(randomBytes4(32));
7950
+ const verifier = base64url(randomBytes5(32));
7751
7951
  const challenge = base64url(createHash7("sha256").update(verifier).digest());
7752
7952
  return { verifier, challenge };
7753
7953
  }
@@ -8294,12 +8494,12 @@ async function buildMcpConfig(profile, connectionIds) {
8294
8494
  return { config, skipped };
8295
8495
  }
8296
8496
  function writeMcpConfigFile(config) {
8297
- const dir = mkdtempSync(join15(tmpdir2(), "flyspace-mcp-"));
8298
- const path = join15(dir, "mcp-config.json");
8299
- writeFileSync7(path, JSON.stringify(config), { mode: 384 });
8497
+ const dir = mkdtempSync(join16(tmpdir2(), "flyspace-mcp-"));
8498
+ const path = join16(dir, "mcp-config.json");
8499
+ writeFileSync8(path, JSON.stringify(config), { mode: 384 });
8300
8500
  return {
8301
8501
  path,
8302
- cleanup: () => rmSync5(dir, { recursive: true, force: true })
8502
+ cleanup: () => rmSync6(dir, { recursive: true, force: true })
8303
8503
  };
8304
8504
  }
8305
8505
  async function withMcpConfig(profile, connectionIds, fn) {
@@ -8316,22 +8516,22 @@ async function withMcpConfig(profile, connectionIds, fn) {
8316
8516
  }
8317
8517
 
8318
8518
  // src/claude/agents.ts
8319
- import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
8519
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
8320
8520
  import { homedir as homedir6 } from "os";
8321
- import { basename as basename5, join as join16 } from "path";
8521
+ import { basename as basename5, join as join17 } from "path";
8322
8522
  import yaml from "js-yaml";
8323
8523
  function defaultConfigDir() {
8324
- return join16(homedir6(), ".claude");
8524
+ return join17(homedir6(), ".claude");
8325
8525
  }
8326
8526
  function expandHome(p) {
8327
8527
  if (p === "~") return homedir6();
8328
- if (p.startsWith("~/")) return join16(homedir6(), p.slice(2));
8528
+ if (p.startsWith("~/")) return join17(homedir6(), p.slice(2));
8329
8529
  return p;
8330
8530
  }
8331
8531
  function parseFrontmatter(file) {
8332
8532
  let content;
8333
8533
  try {
8334
- content = readFileSync10(file, "utf-8");
8534
+ content = readFileSync11(file, "utf-8");
8335
8535
  } catch {
8336
8536
  return {};
8337
8537
  }
@@ -8355,7 +8555,7 @@ function collectFrom(root, source) {
8355
8555
  return [];
8356
8556
  }
8357
8557
  return entries2.map((file) => {
8358
- const path = join16(root, file);
8558
+ const path = join17(root, file);
8359
8559
  const fm = parseFrontmatter(path);
8360
8560
  const name = fm.name?.trim() || basename5(file, ".md");
8361
8561
  return { name, description: (fm.description ?? "").trim(), source, path };
@@ -8364,8 +8564,8 @@ function collectFrom(root, source) {
8364
8564
  function listAgents(configDir2, cwd) {
8365
8565
  const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
8366
8566
  const collected = [
8367
- ...collectFrom(join16(cwd, ".claude", "agents"), "project"),
8368
- ...collectFrom(join16(base, "agents"), "user")
8567
+ ...collectFrom(join17(cwd, ".claude", "agents"), "project"),
8568
+ ...collectFrom(join17(base, "agents"), "user")
8369
8569
  ];
8370
8570
  const byName = /* @__PURE__ */ new Map();
8371
8571
  for (const agent of collected) {
@@ -8377,7 +8577,7 @@ function readAgentPersona(configDir2, cwd, name) {
8377
8577
  const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
8378
8578
  if (!agent) return void 0;
8379
8579
  try {
8380
- const content = readFileSync10(agent.path, "utf-8");
8580
+ const content = readFileSync11(agent.path, "utf-8");
8381
8581
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
8382
8582
  } catch {
8383
8583
  return void 0;
@@ -8477,11 +8677,11 @@ async function updateChannelMemory(channelId, memoryId, patch, actor) {
8477
8677
  async function searchChannelMemories(channelId, query) {
8478
8678
  const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
8479
8679
  if (!words.length) return [];
8480
- let request2 = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
8680
+ let request = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
8481
8681
  for (const word of words) {
8482
- request2 = request2.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
8682
+ request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
8483
8683
  }
8484
- const { data, error: error2 } = await request2.order("updated_at", { ascending: false }).limit(20);
8684
+ const { data, error: error2 } = await request.order("updated_at", { ascending: false }).limit(20);
8485
8685
  if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
8486
8686
  return (data ?? []).map(rowToMemory);
8487
8687
  }
@@ -8877,7 +9077,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
8877
9077
 
8878
9078
  // src/dashboard/attachment-downloader.ts
8879
9079
  import { writeFile } from "fs/promises";
8880
- import { join as join17 } from "path";
9080
+ import { join as join18 } from "path";
8881
9081
  import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
8882
9082
  var debug = process.env["DEBUG"] === "true";
8883
9083
  var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
@@ -8924,7 +9124,7 @@ async function downloadOne(key) {
8924
9124
  if (bytes.length > sizeLimit) {
8925
9125
  return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
8926
9126
  }
8927
- const localPath = join17(paths.UPLOADS_DIR, lastSegment);
9127
+ const localPath = join18(paths.UPLOADS_DIR, lastSegment);
8928
9128
  await writeFile(localPath, bytes);
8929
9129
  if (debug)
8930
9130
  console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
@@ -10211,7 +10411,7 @@ async function exportDocAsArtifact(channelId, docPath) {
10211
10411
  );
10212
10412
  const abs = resolveDocPath(channelConfig.cwd, docPath);
10213
10413
  if (!abs) throw new Error(`doc not found: ${docPath}`);
10214
- const content = readFileSync11(abs, "utf8");
10414
+ const content = readFileSync12(abs, "utf8");
10215
10415
  const title = extractTitle(abs, "md");
10216
10416
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10217
10417
  const prompt = `Publish the document at \`${docPath}\` (already read below) as a Claude Artifact, then record the result as a stub file under this channel's \`docs/artifacts/\` folder (create the folder if it doesn't exist).
@@ -10468,7 +10668,7 @@ function probeDir(absDir, showHidden) {
10468
10668
  return false;
10469
10669
  }
10470
10670
  if (!showHidden && entry.name.startsWith(".")) continue;
10471
- const abs = join18(dir, entry.name);
10671
+ const abs = join19(dir, entry.name);
10472
10672
  const kind = entryKind(entry, abs);
10473
10673
  if (kind === "dir") {
10474
10674
  if (SKIP_DIRS.has(entry.name)) hasOther = true;
@@ -10488,14 +10688,14 @@ function probeDir(absDir, showHidden) {
10488
10688
  }
10489
10689
  function readGitBranch(dir) {
10490
10690
  try {
10491
- let gitDir = join18(dir, ".git");
10691
+ let gitDir = join19(dir, ".git");
10492
10692
  if (statSync5(gitDir).isFile()) {
10493
- const pointer = readFileSync12(gitDir, "utf8").trim();
10693
+ const pointer = readFileSync13(gitDir, "utf8").trim();
10494
10694
  const match = pointer.match(/^gitdir:\s*(.+)$/);
10495
10695
  if (!match) return void 0;
10496
10696
  gitDir = resolve4(dir, match[1]);
10497
10697
  }
10498
- const head = readFileSync12(join18(gitDir, "HEAD"), "utf8").trim();
10698
+ const head = readFileSync13(join19(gitDir, "HEAD"), "utf8").trim();
10499
10699
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
10500
10700
  if (branchMatch) return branchMatch[1];
10501
10701
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -10517,14 +10717,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
10517
10717
  for (const entry of entries2) {
10518
10718
  if (dirs.length + docs.length >= MAX_ENTRIES) break;
10519
10719
  if (!showHidden && entry.name.startsWith(".")) continue;
10520
- const abs = join18(absDir, entry.name);
10720
+ const abs = join19(absDir, entry.name);
10521
10721
  const path = relative(root, abs).split(sep).join("/");
10522
10722
  const kind = entryKind(entry, abs);
10523
10723
  if (kind === "dir") {
10524
10724
  if (SKIP_DIRS.has(entry.name)) continue;
10525
10725
  const probe3 = probeDir(abs, showHidden);
10526
10726
  if (!probe3.hasDocs && probe3.hasOther) continue;
10527
- const isRepo = existsSync16(join18(abs, ".git"));
10727
+ const isRepo = existsSync16(join19(abs, ".git"));
10528
10728
  dirs.push({
10529
10729
  path,
10530
10730
  name: entry.name,
@@ -10560,7 +10760,7 @@ function flatDocs(root, {
10560
10760
  for (const entry of entries2) {
10561
10761
  if (hits.length >= MAX_FLAT_DOCS) return;
10562
10762
  if (!showHidden && entry.name.startsWith(".")) continue;
10563
- const abs = join18(dir, entry.name);
10763
+ const abs = join19(dir, entry.name);
10564
10764
  const kind = entryKind(entry, abs);
10565
10765
  if (kind === "dir") {
10566
10766
  if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
@@ -10602,7 +10802,7 @@ function writeDocContent(root, rel, content, baseMtime) {
10602
10802
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) {
10603
10803
  return { status: "too-large" };
10604
10804
  }
10605
- writeFileSync8(abs, content, "utf8");
10805
+ writeFileSync9(abs, content, "utf8");
10606
10806
  return { status: "ok", mtime: statSync5(abs).mtime.toISOString() };
10607
10807
  }
10608
10808
  function resolveRawPath(root, rel) {
@@ -10640,7 +10840,7 @@ function uniqueName(absDir, name) {
10640
10840
  let candidate = name;
10641
10841
  for (let n = 1; n < 1e3; n++) {
10642
10842
  try {
10643
- statSync5(join18(absDir, candidate));
10843
+ statSync5(join19(absDir, candidate));
10644
10844
  } catch {
10645
10845
  return candidate;
10646
10846
  }
@@ -10662,8 +10862,8 @@ function saveUploadedFile(root, relDir, name, data) {
10662
10862
  if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
10663
10863
  if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
10664
10864
  const final = uniqueName(absDir, safe);
10665
- const abs = join18(absDir, final);
10666
- writeFileSync8(abs, data);
10865
+ const abs = join19(absDir, final);
10866
+ writeFileSync9(abs, data);
10667
10867
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
10668
10868
  }
10669
10869
  function createDocFolder(root, relDir, name) {
@@ -10676,13 +10876,13 @@ function createDocFolder(root, relDir, name) {
10676
10876
  }
10677
10877
  const safe = sanitizeUploadName(name);
10678
10878
  if (!safe) return { status: "bad-name" };
10679
- const abs = join18(absDir, safe);
10879
+ const abs = join19(absDir, safe);
10680
10880
  try {
10681
10881
  statSync5(abs);
10682
10882
  return { status: "exists" };
10683
10883
  } catch {
10684
10884
  }
10685
- mkdirSync8(abs);
10885
+ mkdirSync9(abs);
10686
10886
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: safe };
10687
10887
  }
10688
10888
  function createDocFile(root, relDir, name, content) {
@@ -10699,9 +10899,9 @@ function createDocFile(root, relDir, name, content) {
10699
10899
  if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
10700
10900
  if (SKIP_FILES.has(safe)) return { status: "bad-name" };
10701
10901
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
10702
- const abs = join18(absDir, safe);
10902
+ const abs = join19(absDir, safe);
10703
10903
  try {
10704
- writeFileSync8(abs, content, { encoding: "utf8", flag: "wx" });
10904
+ writeFileSync9(abs, content, { encoding: "utf8", flag: "wx" });
10705
10905
  } catch (err) {
10706
10906
  if (err.code === "EEXIST") return { status: "exists" };
10707
10907
  throw err;
@@ -10737,7 +10937,7 @@ function moveDocEntry(root, from, to) {
10737
10937
  } catch {
10738
10938
  return { status: "bad-target" };
10739
10939
  }
10740
- const finalAbs = join18(absToDir, name);
10940
+ const finalAbs = join19(absToDir, name);
10741
10941
  if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
10742
10942
  return { status: "into-self" };
10743
10943
  }
@@ -10760,7 +10960,7 @@ function deleteDocEntry(root, path) {
10760
10960
  }
10761
10961
  const isDir2 = stat.isDirectory();
10762
10962
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10763
- rmSync6(abs, { recursive: isDir2 });
10963
+ rmSync7(abs, { recursive: isDir2 });
10764
10964
  return {
10765
10965
  status: "ok",
10766
10966
  path: relative(root, abs).split(sep).join("/"),
@@ -10799,7 +10999,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10799
10999
  }
10800
11000
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10801
11001
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10802
- const data = readFileSync12(resolved2.abs);
11002
+ const data = readFileSync13(resolved2.abs);
10803
11003
  const headers = { "Content-Type": resolved2.contentType };
10804
11004
  if (download) {
10805
11005
  const filename = basename6(resolved2.abs).replace(/"/g, "");
@@ -10819,7 +11019,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10819
11019
  }
10820
11020
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10821
11021
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10822
- return c.body(new Uint8Array(readFileSync12(abs)), 200, {
11022
+ return c.body(new Uint8Array(readFileSync13(abs)), 200, {
10823
11023
  "Content-Type": "text/html; charset=utf-8",
10824
11024
  "Content-Security-Policy": HTML_SHARE_CSP,
10825
11025
  "X-Content-Type-Options": "nosniff",
@@ -11004,7 +11204,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11004
11204
  title: kind ? extractTitle(abs, kind) : basename6(abs),
11005
11205
  mtime: stat.mtime.toISOString(),
11006
11206
  tooLarge: tooLarge || void 0,
11007
- content: tooLarge ? "" : readFileSync12(abs, "utf8")
11207
+ content: tooLarge ? "" : readFileSync13(abs, "utf8")
11008
11208
  });
11009
11209
  });
11010
11210
  app.put("/:id/docs/content", async (c) => {
@@ -11047,7 +11247,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11047
11247
  channel,
11048
11248
  rel,
11049
11249
  title: extractTitle(abs, "md"),
11050
- content: readFileSync12(abs, "utf8")
11250
+ content: readFileSync13(abs, "utf8")
11051
11251
  };
11052
11252
  };
11053
11253
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -11586,10 +11786,10 @@ function registerMirroredSkillRoutes(api, agent) {
11586
11786
  }
11587
11787
 
11588
11788
  // src/dashboard/providers.ts
11589
- import { execFileSync as execFileSync2 } from "child_process";
11789
+ import { execFileSync as execFileSync3 } from "child_process";
11590
11790
  import { existsSync as existsSync19 } from "fs";
11591
11791
  import { homedir as homedir8 } from "os";
11592
- import { join as join19, resolve as resolve5 } from "path";
11792
+ import { join as join20, resolve as resolve5 } from "path";
11593
11793
  var PROVIDER_LABELS = {
11594
11794
  claude: "Claude Code",
11595
11795
  codex: "Codex"
@@ -11598,7 +11798,7 @@ var TTL_MS2 = 6e4;
11598
11798
  var cache6 = /* @__PURE__ */ new Map();
11599
11799
  function binaryOnPath(bin) {
11600
11800
  try {
11601
- execFileSync2(bin, ["--version"], { stdio: "ignore" });
11801
+ execFileSync3(bin, ["--version"], { stdio: "ignore" });
11602
11802
  return true;
11603
11803
  } catch {
11604
11804
  return false;
@@ -11607,10 +11807,10 @@ function binaryOnPath(bin) {
11607
11807
  function claudeAuthAvailable() {
11608
11808
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
11609
11809
  const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
11610
- if (existsSync19(join19(configDir2, ".credentials.json"))) return true;
11810
+ if (existsSync19(join20(configDir2, ".credentials.json"))) return true;
11611
11811
  if (process.platform !== "darwin") return false;
11612
11812
  try {
11613
- execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
11813
+ execFileSync3("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
11614
11814
  stdio: "ignore"
11615
11815
  });
11616
11816
  return true;
@@ -11760,7 +11960,7 @@ function registerHostRoutes(api) {
11760
11960
  const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
11761
11961
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
11762
11962
  const filename = errors ? "custie-error.log" : "custie.log";
11763
- const file = join20(paths.LOG_DIR, filename);
11963
+ const file = join21(paths.LOG_DIR, filename);
11764
11964
  const exists = existsSync20(file);
11765
11965
  return c.json({
11766
11966
  file,
@@ -11772,7 +11972,7 @@ function registerHostRoutes(api) {
11772
11972
  }
11773
11973
  function tailFile(file, limit) {
11774
11974
  if (!existsSync20(file)) return [];
11775
- const lines = readFileSync13(file, "utf-8").split("\n");
11975
+ const lines = readFileSync14(file, "utf-8").split("\n");
11776
11976
  if (lines.at(-1) === "") lines.pop();
11777
11977
  return lines.slice(-limit);
11778
11978
  }
@@ -13445,7 +13645,7 @@ function createSearchRouter() {
13445
13645
  }
13446
13646
 
13447
13647
  // src/dashboard/uploads-routes.ts
13448
- import { randomUUID } from "crypto";
13648
+ import { randomUUID as randomUUID2 } from "crypto";
13449
13649
  import { Hono as Hono18 } from "hono";
13450
13650
  import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
13451
13651
  import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
@@ -13469,7 +13669,7 @@ function createUploadsRouter() {
13469
13669
  if (file.size > MAX_FILE_BYTES) {
13470
13670
  return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
13471
13671
  }
13472
- const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID()}-${sanitizeFilename(file.name)}`;
13672
+ const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
13473
13673
  const buffer = Buffer.from(await file.arrayBuffer());
13474
13674
  await getR2Client().send(
13475
13675
  new PutObjectCommand3({
@@ -13552,7 +13752,7 @@ function createTranscribeRouter() {
13552
13752
  }
13553
13753
 
13554
13754
  // src/dashboard/avatars-routes.ts
13555
- import { createHash as createHash10, randomUUID as randomUUID2 } from "crypto";
13755
+ import { createHash as createHash10, randomUUID as randomUUID3 } from "crypto";
13556
13756
  import { Hono as Hono20 } from "hono";
13557
13757
  import sharp4 from "sharp";
13558
13758
  import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
@@ -13591,7 +13791,7 @@ async function storeAvatar(file, owner) {
13591
13791
  if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
13592
13792
  const original = Buffer.from(await file.arrayBuffer());
13593
13793
  const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
13594
- const key = `${avatarKeyPrefix()}${owner}-${randomUUID2()}.png`;
13794
+ const key = `${avatarKeyPrefix()}${owner}-${randomUUID3()}.png`;
13595
13795
  await getR2Client().send(
13596
13796
  new PutObjectCommand4({
13597
13797
  Bucket: getR2Bucket(),
@@ -14955,6 +15155,14 @@ var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
14955
15155
  var GREETING_MODEL = "openai/gpt-oss-20b";
14956
15156
  var TIMEOUT_MS = 6e3;
14957
15157
  var MAX_LISTED_CHANNELS = 8;
15158
+ var GUIDES = [
15159
+ "Welcome to Flyspace",
15160
+ "Working with the agent",
15161
+ "Channels and folders",
15162
+ "Docs",
15163
+ "Inviting your team",
15164
+ "Automations"
15165
+ ];
14958
15166
  var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner sees, posted in their #general channel. Flyspace is a team chat where an AI agent works inside every channel.
14959
15167
 
14960
15168
  - Plain Markdown, 60 to 110 words, in English. No headings, no emoji, no em or en dashes, no sign-off.
@@ -14962,11 +15170,13 @@ var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner
14962
15170
  - Say that #general is public: everyone invited to the workspace can see it.
14963
15171
  - If project channels are listed, mention them as #name and say each one is linked to that project folder. Never invent a channel.
14964
15172
  - Mention the trial or the missing Host only if the facts say so.
15173
+ - If guides are listed, say they are in the Docs pane of #general and suggest starting with the first one.
14965
15174
  - Use only the facts given. Do not add buttons or a list of next steps: those are appended for you.`;
14966
15175
  function greetingButtons(input2) {
14967
15176
  const buttons = [];
14968
15177
  if (!input2.hostConnected) buttons.push("Connect a machine");
14969
15178
  if (input2.channels[0]) buttons.push(`Summarise #${input2.channels[0]} for me`);
15179
+ if (input2.hostConnected) buttons.push("Give me a quick tour of the guides");
14970
15180
  if (input2.memberCount <= 1) buttons.push("Invite my team");
14971
15181
  buttons.push("Set up a daily digest");
14972
15182
  return buttons.slice(0, 3).map((b) => b.replace(/[|\]]/g, ""));
@@ -14977,6 +15187,7 @@ function fallbackGreeting(input2) {
14977
15187
  `Welcome to ${input2.workspaceName}, ${input2.ownerName}.`,
14978
15188
  "This is #general. It is public, so everyone you invite to the workspace can see it.",
14979
15189
  channels.length ? `Your project channels are ready: ${channels.join(", ")}. Each one is linked to its project folder, so the agent there works on the real files.` : "",
15190
+ input2.hostConnected ? "A few short guides are waiting in the Docs pane of #general. Start with Welcome to Flyspace." : "",
14980
15191
  "Mention the agent in any channel to get started."
14981
15192
  ].filter(Boolean).join("\n\n");
14982
15193
  }
@@ -14987,7 +15198,8 @@ async function groqGreeting(input2, apiKey) {
14987
15198
  `Plan: ${input2.trialing ? "14-day free trial, just started" : "not on a trial"}`,
14988
15199
  `Host: ${input2.hostConnected ? "a machine is connected, agents can run" : "no machine connected yet, agents cannot run until one is"}`,
14989
15200
  `Project channels: ${input2.channels.slice(0, MAX_LISTED_CHANNELS).join(", ") || "(none yet)"}`,
14990
- `Members: ${input2.memberCount}`
15201
+ `Members: ${input2.memberCount}`,
15202
+ `Guides in #general: ${input2.hostConnected ? GUIDES.join(", ") : "(none, no machine connected)"}`
14991
15203
  ].join("\n");
14992
15204
  const res = await fetch(GROQ_API_URL8, {
14993
15205
  method: "POST",
@@ -15069,6 +15281,14 @@ function createOnboardingRouter(agent) {
15069
15281
  await markWorkspaceProjectsSetup(ctx.workspaceId);
15070
15282
  return c.json({ channels: created.map((ch) => ({ id: ch.id, name: ch.name })) }, 201);
15071
15283
  });
15284
+ app.get("/greeting", async (c) => {
15285
+ const ctx = await resolveAuthContext(c);
15286
+ if (ctx?.role !== "admin") return c.json({ pending: false });
15287
+ const greetedAt = await getWorkspaceGreetedAt(ctx.workspaceId);
15288
+ const pending3 = !!ctx.projectsSetupAt && !greetedAt;
15289
+ const general = pending3 ? await ensureGeneralChannel() : null;
15290
+ return c.json({ pending: pending3, channelId: general?.id ?? null });
15291
+ });
15072
15292
  app.post("/greeting", async (c) => {
15073
15293
  const ctx = await resolveAuthContext(c);
15074
15294
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -15077,6 +15297,8 @@ function createOnboardingRouter(agent) {
15077
15297
  const existing = () => supabase.from("threads").select("id").eq("client_key", ctx.workspaceId).maybeSingle();
15078
15298
  const found = await existing();
15079
15299
  if (found.data) return c.json({ channelId: general.id, threadId: found.data.id });
15300
+ if (await getWorkspaceGreetedAt(ctx.workspaceId))
15301
+ return c.json({ error: "already greeted" }, 409);
15080
15302
  const [branding, billing, members] = await Promise.all([
15081
15303
  getWorkspaceBranding(getProfile()).catch(() => null),
15082
15304
  getWorkspaceBilling(ctx.workspaceId).catch(() => null),
@@ -15107,6 +15329,7 @@ function createOnboardingRouter(agent) {
15107
15329
  await supabase.from("threads").delete().eq("id", thread.id);
15108
15330
  return c.json({ error: reply.error.message }, 500);
15109
15331
  }
15332
+ await markWorkspaceGreeted(ctx.workspaceId);
15110
15333
  return c.json({ channelId: general.id, threadId: thread.id }, 201);
15111
15334
  });
15112
15335
  return app;
@@ -15600,10 +15823,10 @@ import { WebSocket, WebSocketServer } from "ws";
15600
15823
 
15601
15824
  // src/dashboard/host-identity.ts
15602
15825
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
15603
- import { existsSync as existsSync23, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
15604
- import { dirname as dirname7, join as join21 } from "path";
15826
+ import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
15827
+ import { dirname as dirname7, join as join22 } from "path";
15605
15828
  function hostPrivateKeyPath() {
15606
- return process.env["FLYSPACE_HOST_KEY_PATH"] || join21(paths.BASE_CONFIG_DIR, "host.key");
15829
+ return process.env["FLYSPACE_HOST_KEY_PATH"] || join22(paths.BASE_CONFIG_DIR, "host.key");
15607
15830
  }
15608
15831
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15609
15832
  if (!existsSync23(path)) {
@@ -15611,7 +15834,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15611
15834
  `No Host identity at ${path}. Run \`pnpm run generate-host-key\` on this machine, then set FLYSPACE_HOST_PUBLIC_KEY on the Data process to the printed public key.`
15612
15835
  );
15613
15836
  }
15614
- return createPrivateKey2(readFileSync14(path, "utf8"));
15837
+ return createPrivateKey2(readFileSync15(path, "utf8"));
15615
15838
  }
15616
15839
  function loadHostPublicKey() {
15617
15840
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -15624,82 +15847,6 @@ function loadHostPublicKey() {
15624
15847
  }
15625
15848
  }
15626
15849
 
15627
- // src/dashboard/host-lock.ts
15628
- import { execFileSync as execFileSync3 } from "child_process";
15629
- import { randomBytes as randomBytes5 } from "crypto";
15630
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
15631
- import { join as join22 } from "path";
15632
- function hostLockPath() {
15633
- return join22(paths.BASE_DATA_DIR, "host.lock");
15634
- }
15635
- function isLiveFlyspace(pid) {
15636
- try {
15637
- process.kill(pid, 0);
15638
- const command = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
15639
- encoding: "utf8"
15640
- });
15641
- return command.includes("flyspace") || command.includes("host-server");
15642
- } catch {
15643
- return false;
15644
- }
15645
- }
15646
- function readLockPid(path) {
15647
- try {
15648
- const pid = Number(JSON.parse(readFileSync15(path, "utf8")).pid);
15649
- return Number.isInteger(pid) && pid > 0 ? pid : null;
15650
- } catch {
15651
- return null;
15652
- }
15653
- }
15654
- function acquireHostLock() {
15655
- const path = hostLockPath();
15656
- mkdirSync10(paths.BASE_DATA_DIR, { recursive: true });
15657
- const body = JSON.stringify({
15658
- pid: process.pid,
15659
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
15660
- execPath: process.argv[1] ?? ""
15661
- });
15662
- for (let attempt = 0; attempt < 2; attempt++) {
15663
- try {
15664
- writeFileSync10(path, body, { flag: "wx", mode: 384 });
15665
- const release2 = () => {
15666
- if (readLockPid(path) === process.pid) rmSync7(path, { force: true });
15667
- };
15668
- process.once("exit", release2);
15669
- return release2;
15670
- } catch (err) {
15671
- if (err.code !== "EEXIST") throw err;
15672
- const holder = readLockPid(path);
15673
- if (holder === process.pid) return () => {
15674
- };
15675
- if (holder && isLiveFlyspace(holder)) {
15676
- throw new Error(
15677
- `Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
15678
- );
15679
- }
15680
- rmSync7(path, { force: true });
15681
- }
15682
- }
15683
- throw new Error(`Could not take the Host lock at ${path}`);
15684
- }
15685
- function publishHostEndpoint(port) {
15686
- const path = hostLockPath();
15687
- if (readLockPid(path) !== process.pid) return null;
15688
- const token = randomBytes5(32).toString("hex");
15689
- const lock = JSON.parse(readFileSync15(path, "utf8"));
15690
- writeFileSync10(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
15691
- return token;
15692
- }
15693
- function readHostEndpoint() {
15694
- try {
15695
- const { pid, port, token } = JSON.parse(readFileSync15(hostLockPath(), "utf8"));
15696
- if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
15697
- return { port, token };
15698
- } catch {
15699
- return null;
15700
- }
15701
- }
15702
-
15703
15850
  // src/store/device-store.ts
15704
15851
  import { createHash as createHash11, createPublicKey as createPublicKey3, randomBytes as randomBytes6, randomInt } from "crypto";
15705
15852
  var DEVICE_CODE_TTL_MS = 10 * 6e4;
@@ -16490,98 +16637,6 @@ var forwardToSupabase = async (req) => {
16490
16637
  };
16491
16638
  };
16492
16639
 
16493
- // src/dashboard/host-db-client.ts
16494
- import { createClient as createClient2 } from "@supabase/supabase-js";
16495
- import { randomUUID as randomUUID3 } from "crypto";
16496
- var REQUEST_TIMEOUT_MS3 = 3e4;
16497
- var pending = /* @__PURE__ */ new Map();
16498
- var sender = null;
16499
- var onSender = [];
16500
- function setHostDbSender(fn) {
16501
- sender = fn;
16502
- if (!fn) return;
16503
- const waiting = onSender;
16504
- onSender = [];
16505
- for (const wake of waiting) wake();
16506
- }
16507
- function handleHostDbResult(id, res) {
16508
- const p = pending.get(id);
16509
- if (!p) return;
16510
- pending.delete(id);
16511
- clearTimeout(p.timer);
16512
- p.resolve(res);
16513
- }
16514
- function whenConnected() {
16515
- if (sender) return Promise.resolve(sender);
16516
- return new Promise((resolve7, reject) => {
16517
- const timer2 = setTimeout(() => {
16518
- onSender = onSender.filter((w) => w !== wake);
16519
- reject(new Error("Host is not connected to the Data API"));
16520
- }, REQUEST_TIMEOUT_MS3);
16521
- const wake = () => {
16522
- clearTimeout(timer2);
16523
- resolve7(sender);
16524
- };
16525
- onSender.push(wake);
16526
- });
16527
- }
16528
- async function request(req) {
16529
- const send2 = await whenConnected();
16530
- const id = randomUUID3();
16531
- return new Promise((resolve7, reject) => {
16532
- const timer2 = setTimeout(() => {
16533
- pending.delete(id);
16534
- reject(new Error("Data API did not answer in time"));
16535
- }, REQUEST_TIMEOUT_MS3);
16536
- pending.set(id, { resolve: resolve7, reject, timer: timer2 });
16537
- send2({ type: "db", id, req });
16538
- });
16539
- }
16540
- var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
16541
- var hostDbFetch = async (input2, init) => {
16542
- const req = new Request(input2, init);
16543
- const url = new URL(req.url);
16544
- const method = req.method.toUpperCase();
16545
- const res = await request({
16546
- method,
16547
- path: `${url.pathname}${url.search}`,
16548
- headers: Object.fromEntries(req.headers.entries()),
16549
- body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
16550
- });
16551
- const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
16552
- return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
16553
- };
16554
- function relayChannel(topic) {
16555
- const channel = {
16556
- on: () => channel,
16557
- subscribe: (callback) => {
16558
- queueMicrotask(() => callback?.("SUBSCRIBED"));
16559
- return channel;
16560
- },
16561
- send: async (msg) => {
16562
- if (msg.type !== "broadcast" || !sender) return "error";
16563
- sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
16564
- return "ok";
16565
- },
16566
- unsubscribe: async () => "ok"
16567
- };
16568
- return channel;
16569
- }
16570
- function createHostDbClient() {
16571
- const client2 = createClient2("https://host-db.invalid", "host-device", {
16572
- auth: { persistSession: false, autoRefreshToken: false },
16573
- global: { fetch: hostDbFetch }
16574
- });
16575
- return new Proxy(client2, {
16576
- get(target, prop) {
16577
- if (prop === "channel") return relayChannel;
16578
- if (prop === "removeChannel") return async () => "ok";
16579
- const value = Reflect.get(target, prop, target);
16580
- return typeof value === "function" ? value.bind(target) : value;
16581
- }
16582
- });
16583
- }
16584
-
16585
16640
  // src/dashboard/agent-bridge.ts
16586
16641
  var HOST_SOCKET_PATH = "/internal/host-socket";
16587
16642
  var DEFAULT_DATA_API_PORT = 4757;
@@ -16842,6 +16897,7 @@ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
16842
16897
  );
16843
16898
  return Promise.resolve(null);
16844
16899
  }
16900
+ if (method === "docsRequest") console.log(`[host-socket] docsRequest for "${profile}" -> device ${hosts.get(ws)?.deviceId ?? "legacy"}`);
16845
16901
  const id = randomUUID4();
16846
16902
  return new Promise((resolve7) => {
16847
16903
  const timer2 = setTimeout(() => {
@@ -17066,12 +17122,13 @@ function relayedAgentCallbacks(local) {
17066
17122
  ...Object.fromEntries(RELAYED_METHODS.map((method) => [method, relay3(method)]))
17067
17123
  };
17068
17124
  }
17069
- async function handleLocalRelay(req, callbacks, token) {
17125
+ function relayAuthorized(req, token) {
17070
17126
  const given = Buffer.from(req.headers.get("authorization") ?? "");
17071
17127
  const expected = Buffer.from(`Bearer ${token}`);
17072
- if (!token || given.length !== expected.length || !timingSafeEqual2(given, expected)) {
17073
- return Response.json({ error: "unauthorized" }, { status: 401 });
17074
- }
17128
+ return !!token && given.length === expected.length && timingSafeEqual2(given, expected);
17129
+ }
17130
+ async function handleLocalRelay(req, callbacks, token) {
17131
+ if (!relayAuthorized(req, token)) return Response.json({ error: "unauthorized" }, { status: 401 });
17075
17132
  const body = await req.json().catch(() => null);
17076
17133
  const method = RELAYED_METHODS.find((m) => m === body?.method);
17077
17134
  if (!body || !method || typeof body.profile !== "string" || !Array.isArray(body.args)) {
@@ -17738,6 +17795,20 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17738
17795
  let relayToken = null;
17739
17796
  if (mode === "host") {
17740
17797
  app.post(LOCAL_RELAY_PATH, (c) => handleLocalRelay(c.req.raw, agent, relayToken));
17798
+ if (earlyHostSocket) {
17799
+ app.post(LOCAL_DB_PATH, async (c) => {
17800
+ if (!relayAuthorized(c.req.raw, relayToken)) return c.json({ error: "unauthorized" }, 401);
17801
+ const req = await c.req.json().catch(() => null);
17802
+ if (!req || typeof req.method !== "string" || typeof req.path !== "string") {
17803
+ return c.json({ error: "bad request" }, 400);
17804
+ }
17805
+ try {
17806
+ return c.json(await requestHostDb(req));
17807
+ } catch (err) {
17808
+ return c.json({ error: err.message }, 503);
17809
+ }
17810
+ });
17811
+ }
17741
17812
  }
17742
17813
  app.route("/stripe", createStripeWebhookRouter());
17743
17814
  if (!servesHost) {
@@ -19731,7 +19802,7 @@ async function main() {
19731
19802
  break;
19732
19803
  case "-v":
19733
19804
  case "--version":
19734
- printRaw("0.1.13");
19805
+ printRaw("0.1.14");
19735
19806
  break;
19736
19807
  case "-h":
19737
19808
  case "--help":