flyspace-cli 0.1.0 → 0.1.1

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
@@ -4,11 +4,13 @@ import {
4
4
  AutomationRunStore,
5
5
  BOT_PROFILE_KEY,
6
6
  BRANDING_DEFAULTS,
7
+ BUILT_IN_OUTPUT_STYLES,
7
8
  DEFAULT_CODEX_MODEL,
8
9
  DEFAULT_TIMEZONE,
9
10
  InvalidTransferError,
10
11
  LastAdminError,
11
12
  MessageQueue,
13
+ OutputStyleConflictError,
12
14
  SKIP_SUBTYPES,
13
15
  SLACK_ENV,
14
16
  SessionStore,
@@ -28,6 +30,7 @@ import {
28
30
  codexAuthAvailable,
29
31
  createChannel,
30
32
  createMembership,
33
+ createOutputStyle,
31
34
  createSkillFile,
32
35
  createWorkspace,
33
36
  deleteMembership,
@@ -46,16 +49,14 @@ import {
46
49
  getChannelRegistry,
47
50
  getMembershipByEmail,
48
51
  getMembershipByUserId,
49
- getOutputStyle,
52
+ getOutputStyleDefault,
50
53
  getProfileChannelRegistry,
51
54
  getWorkspaceBilling,
52
55
  getWorkspaceBranding,
53
56
  getWorkspaceById,
54
57
  getWorkspaceBySlug,
55
58
  getWorkspaceByStripeCustomerId,
56
- getWorkspaceOutputStyleDefault,
57
59
  getWorkspacePlanStatus,
58
- insertOutputStyle,
59
60
  installUsageRecording,
60
61
  isAllowedModel,
61
62
  isCodexModel,
@@ -104,7 +105,7 @@ import {
104
105
  resolveChannelId,
105
106
  resolveChannelPrompt,
106
107
  resolveCwd,
107
- resolveOutputStyleForProfile,
108
+ resolveSelectedOutputStyle,
108
109
  resolveUserId,
109
110
  runAutomation,
110
111
  runScriptAutomation,
@@ -116,7 +117,7 @@ import {
116
117
  saveWorkspacePrompt,
117
118
  setAgentPaused,
118
119
  setAllowGuestAccess,
119
- setWorkspaceOutputStyleDefault,
120
+ setOutputStyleDefault,
120
121
  setWorkspaceStripeSubscription,
121
122
  slackConfigured,
122
123
  startAutomationRegistryPolling,
@@ -135,7 +136,7 @@ import {
135
136
  warnOnBrokenChannelCwds,
136
137
  workspaceSkillsDir,
137
138
  writeSkillFile
138
- } from "./chunk-DCCECKHH.js";
139
+ } from "./chunk-6D2PIFEA.js";
139
140
  import {
140
141
  firstReplyBody,
141
142
  getThread,
@@ -312,10 +313,10 @@ async function runPrompt() {
312
313
  const editor = process.env["EDITOR"] || "vi";
313
314
  console.log(`[flyspace] Opening ${paths.PROMPT_FILE} in ${editor}...`);
314
315
  const child = spawn(editor, [paths.PROMPT_FILE], { stdio: "inherit" });
315
- return new Promise((resolve7, reject) => {
316
+ return new Promise((resolve8, reject) => {
316
317
  child.on("close", (code) => {
317
318
  if (code === 0) {
318
- resolve7();
319
+ resolve8();
319
320
  } else {
320
321
  reject(new Error(`Editor exited with code ${code}`));
321
322
  }
@@ -387,10 +388,10 @@ function editConfig() {
387
388
  const editor = process.env["EDITOR"] || "vi";
388
389
  console.log(`[flyspace] Opening ${paths.CONFIG_FILE} in ${editor}...`);
389
390
  const child = spawn2(editor, [paths.CONFIG_FILE], { stdio: "inherit" });
390
- return new Promise((resolve7, reject) => {
391
+ return new Promise((resolve8, reject) => {
391
392
  child.on("close", (code) => {
392
393
  if (code === 0) {
393
- resolve7();
394
+ resolve8();
394
395
  } else {
395
396
  reject(new Error(`Editor exited with code ${code}`));
396
397
  }
@@ -851,7 +852,7 @@ Restarting ${installed.length} instance(s)...`);
851
852
  }
852
853
  }
853
854
  async function runUpgrade() {
854
- const current = "0.1.0";
855
+ const current = "0.1.1";
855
856
  console.log(`Current version: ${current}`);
856
857
  console.log("Checking for updates...\n");
857
858
  try {
@@ -1490,10 +1491,10 @@ async function runLogs(args) {
1490
1491
  }
1491
1492
 
1492
1493
  // src/commands/slack.ts
1493
- import { execFileSync as execFileSync2 } from "child_process";
1494
- import { existsSync as existsSync27, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
1495
- import { resolve as resolve6 } from "path";
1496
- import { tmpdir as tmpdir3, homedir as homedir7 } from "os";
1494
+ import { execFileSync as execFileSync3 } from "child_process";
1495
+ import { existsSync as existsSync28, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
1496
+ import { resolve as resolve7 } from "path";
1497
+ import { tmpdir as tmpdir3, homedir as homedir8 } from "os";
1497
1498
  import { input, confirm, select, checkbox } from "@inquirer/prompts";
1498
1499
  import { WebClient as WebClient3 } from "@slack/web-api";
1499
1500
 
@@ -1931,13 +1932,14 @@ function inviteEmail({
1931
1932
  }
1932
1933
  function workspaceSetupEmail({
1933
1934
  actionLink,
1935
+ logoUrl,
1934
1936
  invitedBy,
1935
1937
  message
1936
1938
  }) {
1937
1939
  const who = invitedBy?.trim();
1938
1940
  const note = message?.trim();
1939
1941
  return render({
1940
- brand: { name: "Flyspace" },
1942
+ brand: { name: "Flyspace", ...logoUrl ? { logoUrl } : {} },
1941
1943
  subject: who ? `${who} invited you to create a Flyspace workspace` : "Create your Flyspace workspace",
1942
1944
  heading: "Create your workspace",
1943
1945
  paragraphs: [
@@ -2178,6 +2180,7 @@ var RESERVED = /* @__PURE__ */ new Set([
2178
2180
  // Platform hosts and routes (`app`, `www`, `admin` are also host-profile.ts's RESERVED_LABELS).
2179
2181
  "app",
2180
2182
  "www",
2183
+ "get",
2181
2184
  "admin",
2182
2185
  "api",
2183
2186
  "auth",
@@ -2532,7 +2535,7 @@ var COPY = {
2532
2535
  cta: "Continue"
2533
2536
  }
2534
2537
  };
2535
- function page(body, font, theme, dark = false, maxWidth = 420) {
2538
+ function page(body, font, theme, dark = false, maxWidth = 420, title = "Confirm sign-in") {
2536
2539
  const base = dark ? DARK : LIGHT;
2537
2540
  return `<!doctype html>
2538
2541
  <html lang="en">
@@ -2544,7 +2547,10 @@ function page(body, font, theme, dark = false, maxWidth = 420) {
2544
2547
  <!-- The token sits in this page's URL, so it must not travel to Supabase (or
2545
2548
  anywhere else) in a Referer header. -->
2546
2549
  <meta name="referrer" content="no-referrer">
2547
- <title>Confirm sign-in</title>
2550
+ <title>${title}</title>
2551
+ <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
2552
+ <link rel="icon" type="image/png" sizes="64x64" href="/icons/favicon-64.png">
2553
+ <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
2548
2554
  ${fontLink(font)}
2549
2555
  <style>${dark ? "" : darkModeCss(accentOf(theme))}
2550
2556
  body { margin:0; }
@@ -2587,14 +2593,18 @@ async function findInvite(token) {
2587
2593
  }
2588
2594
  var PASSWORD_MIN = 8;
2589
2595
  function passwordField(name, label2, autocomplete, display, invalid) {
2590
- return `<label for="${name}" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">${label2}</label><input id="${name}" name="${name}" type="password" required${name === "password" ? " autofocus" : ""} minlength="${PASSWORD_MIN}" autocomplete="${autocomplete}" style="display:block;box-sizing:border-box;width:100%;margin-bottom:16px;border:1px solid ${invalid ? "#d33" : DARK.line};border-radius:8px;background:transparent;padding:12px;font:500 15px/1.2 ${display};color:inherit;outline:0;">`;
2596
+ return `<label for="${name}" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">${label2}</label><div style="position:relative;margin-bottom:16px;"><input id="${name}" name="${name}" type="password" required${name === "password" ? " autofocus" : ""} minlength="${PASSWORD_MIN}" autocomplete="${autocomplete}" style="display:block;box-sizing:border-box;width:100%;border:1px solid ${invalid ? "#d33" : DARK.line};border-radius:8px;background:transparent;padding:12px 44px 12px 12px;font:500 15px/1.2 ${display};color:inherit;outline:0;"><button type="button" data-toggle-password="${name}" aria-label="Show password" aria-pressed="false" class="fs-dim" style="position:absolute;top:0;right:0;height:100%;width:44px;display:flex;align-items:center;justify-content:center;border:0;background:transparent;color:${DARK.dim};cursor:pointer;"><svg data-eye viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg><svg data-eye-off viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none"><path d="M17.94 17.94A10.9 10.9 0 0 1 12 19c-6.5 0-10-7-10-7a18.5 18.5 0 0 1 5.06-5.94M9.9 4.24A10.9 10.9 0 0 1 12 5c6.5 0 10 7 10 7a18.5 18.5 0 0 1-2.16 3.19M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg></button></div>`;
2591
2597
  }
2592
2598
  var CLAUDE_PATH = "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z";
2593
2599
  var CODEX_PATH = "M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z";
2594
2600
  function hostDiagram(display, accent) {
2595
- const box = (y, title) => `<rect x="20" y="${y}" width="280" height="38" rx="8" fill="${DARK.bg}" stroke="${DARK.line}"/><text x="36" y="${y + 24}" fill="${DARK.ink}" font-size="13" font-weight="600">${title}</text>`;
2601
+ const box = (y, title) => `<rect x="20" y="${y}" width="280" height="38" rx="8" fill="${DARK.bg}" stroke="${DARK.line}"/><text x="160" y="${y + 19}" text-anchor="middle" dominant-baseline="central" fill="${DARK.ink}" font-size="13" font-weight="600">${title}</text>`;
2596
2602
  const arrow = (from, to, x = 160) => `<line x1="${x}" y1="${from}" x2="${x}" y2="${to}" stroke="${accent}" stroke-width="1.5" marker-end="url(#fs-arrow)"/>`;
2597
- const agent = (x, title, path, fill) => `<rect x="${x}" y="182" width="134" height="38" rx="8" fill="${DARK.bg}" stroke="${DARK.line}"/><svg x="${x + 12}" y="193" width="16" height="16" viewBox="0 0 24 24"><path d="${path}" fill="${fill}" fill-rule="evenodd"/></svg><text x="${x + 36}" y="206" fill="${DARK.ink}" font-size="13" font-weight="600">${title}</text>`;
2603
+ const agent = (x, title, path, fill) => {
2604
+ const unit = 16 + 8 + title.length * 7;
2605
+ const start = x + 67 - unit / 2;
2606
+ return `<rect x="${x}" y="182" width="134" height="38" rx="8" fill="${DARK.bg}" stroke="${DARK.line}"/><svg x="${start}" y="193" width="16" height="16" viewBox="0 0 24 24"><path d="${path}" fill="${fill}" fill-rule="evenodd"/></svg><text x="${start + 24}" y="201" dominant-baseline="central" fill="${DARK.ink}" font-size="13" font-weight="600">${title}</text>`;
2607
+ };
2598
2608
  return `<svg viewBox="0 0 320 232" width="100%" role="img" aria-labelledby="fs-host-title" style="display:block;max-width:360px;margin:20px 0 0;font-family:${display};"><title id="fs-host-title">The Flyspace Host on your machine connects to your workspace over a WebSocket and runs Claude Code or Codex CLI.</title><defs><marker id="fs-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="${accent}"/></marker></defs>` + box(10, "Your Workspace") + arrow(88, 51) + `<text x="172" y="73" fill="${DARK.dim}" font-size="11">WebSocket</text><rect x="8" y="90" width="304" height="136" rx="10" fill="none" stroke="${DARK.dim}" stroke-dasharray="4 4"/><text x="20" y="108" fill="${DARK.dim}" font-size="10" font-weight="600" letter-spacing="0.06em">YOUR MACHINE</text>` + box(116, "Flyspace Host") + arrow(156, 180, 87) + arrow(156, 180, 233) + agent(20, "Claude Code", CLAUDE_PATH, "#D97757") + agent(166, "Codex CLI", CODEX_PATH, DARK.ink) + `</svg>`;
2599
2609
  }
2600
2610
  function setupPage(c, token, invite, form = {}) {
@@ -2603,11 +2613,12 @@ function setupPage(c, token, invite, form = {}) {
2603
2613
  const host = trustedHost(c) ?? "";
2604
2614
  const suffix = `.${host.replace(/:\d+$/, "").split(".").slice(1).join(".")}`;
2605
2615
  return page(
2606
- `<div class="fs-split"><div>` + masthead2("Flyspace", FLYSPACE_MARK, display, true) + `<div class="fs-ink" style="font:600 20px/1.3 ${display};color:${DARK.ink};margin-bottom:10px;">Create your workspace</div><p class="fs-ink" style="margin:0 0 8px;font:500 15px/1.5 ${display};color:${DARK.ink};">Welcome to the early access cohort.</p><p class="fs-muted" style="margin:0;font:400 14px/1.6 ${display};color:${DARK.muted};">Your agents run on your own machine. The Flyspace Host connects out to this workspace, so Claude Code or Codex works in each channel's real folder, with your own tools and keys.</p>` + hostDiagram(display, accent) + `</div><div><form method="post" action="/auth/new-workspace"><input type="hidden" name="token" value="${escapeHtml(token)}"><div class="fs-dim" style="margin:0 0 14px;font:600 11px/1.4 ${display};letter-spacing:0.06em;text-transform:uppercase;color:${DARK.dim};">Your account</div><label for="username" class="fs-muted" style="display:block;font:400 13px/1.4 ${display};color:${DARK.muted};margin-bottom:4px;">You'll be its Owner, signed in as</label><input id="username" name="username" type="email" autocomplete="username" readonly tabindex="-1" value="${escapeHtml(invite.email)}" style="display:block;width:100%;box-sizing:border-box;border:0;outline:0;background:transparent;padding:0;margin-bottom:20px;font:600 15px/1.4 ${display};color:${DARK.ink};text-overflow:ellipsis;"><div class="fs-pair"><div>` + passwordField("password", "Password", "new-password", display, !!form.passwordError) + `</div><div>` + passwordField("confirm", "Confirm password", "new-password", display, !!form.passwordError) + `</div></div>` + (form.passwordError ? `<p style="margin:-8px 0 16px;font:500 13px/1.5 ${display};color:#d33;">${escapeHtml(form.passwordError)}</p>` : `<p class="fs-dim" style="margin:-8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">At least ${PASSWORD_MIN} characters. You'll use it to sign in to your workspace.</p>`) + `<div style="border-top:1px solid ${DARK.line};margin:4px 0 24px;"></div><div class="fs-dim" style="margin:0 0 14px;font:600 11px/1.4 ${display};letter-spacing:0.06em;text-transform:uppercase;color:${DARK.dim};">Your workspace</div><label for="name" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">Workspace name</label><input id="name" name="name" required autocomplete="organization" maxlength="${DISPLAY_NAME_MAX}" value="${escapeHtml(form.name ?? "")}" placeholder="My Team" class="fs-card" style="display:block;width:100%;box-sizing:border-box;border:1px solid ${DARK.line};border-radius:8px;padding:12px;outline:0;background:transparent;font:500 15px/1.2 ${display};color:inherit;"><p class="fs-dim" style="margin:8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">Shown across your workspace. You can change it later.</p><label for="slug" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">Workspace URL</label><div class="fs-card" style="display:flex;align-items:center;border:1px solid ${form.error ? "#d33" : DARK.line};border-radius:8px;padding:0 12px;"><input id="slug" name="slug" required autocomplete="off" autocapitalize="none" spellcheck="false" minlength="${SLUG_MIN}" maxlength="${SLUG_MAX}" pattern="[a-z](?:[a-z0-9]|-(?!-))*[a-z0-9]" value="${escapeHtml(form.slug ?? "")}" placeholder="my-team" style="flex:1;min-width:0;border:0;outline:0;background:transparent;padding:12px 0;font:500 15px/1.2 ${display};color:inherit;"><span class="fs-dim" style="font:400 15px/1 ${display};color:${DARK.dim};white-space:nowrap;">${escapeHtml(suffix)}</span></div>` + (form.error ? `<p style="margin:8px 0 0;font:500 13px/1.5 ${display};color:#d33;">${escapeHtml(form.error)}</p>` : "") + `<p class="fs-dim" style="margin:8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">Lowercase letters, digits and hyphens, ${SLUG_MIN} to ${SLUG_MAX} characters. <strong style="font-weight:600;color:${DARK.ink};">It can't be changed later.</strong></p><button class="fs-btn" type="submit" style="background:${accent};color:${DARK.onAccent};border-radius:8px;padding:13px 24px;width:100%;font:600 15px/1 ${display};">Create workspace</button></form><div style="margin-top:20px;border-left:2px solid ${accent};padding:2px 0 2px 12px;"><p class="fs-muted" style="margin:0;font:400 13px/1.6 ${display};color:${DARK.muted};"><strong class="fs-ink" style="font-weight:600;color:${DARK.ink};">Next:</strong> we'll help you connect Claude Code or Codex CLI.</p></div><p class="fs-dim" style="margin:16px 0 0;font:400 12px/1.6 ${display};color:${DARK.dim};">By creating a workspace, you agree to the <a href="https://flyspace.ai/terms/" target="_blank" rel="noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">Terms</a> and <a href="https://flyspace.ai/privacy/" target="_blank" rel="noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">Privacy Policy</a>.</p></div></div><script>(()=>{const n=document.getElementById('name'),s=document.getElementById('slug');let touched=!!s.value;s.addEventListener('input',()=>{touched=true});n.addEventListener('input',()=>{if(touched)return;s.value=n.value.normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').replace(/^[^a-z]+/,'').slice(0,${SLUG_MAX}).replace(/-+$/,'')})})()</script>`,
2616
+ `<div class="fs-split"><div>` + masthead2("Flyspace", FLYSPACE_MARK, display, true) + `<div class="fs-ink" style="font:600 20px/1.3 ${display};color:${DARK.ink};margin-bottom:10px;">Create your workspace</div><p class="fs-ink" style="margin:0 0 8px;font:500 15px/1.5 ${display};color:${DARK.ink};">Welcome to the early access cohort.</p><p class="fs-muted" style="margin:0;font:400 14px/1.6 ${display};color:${DARK.muted};">Your agents run on your own machine. The Flyspace Host connects out to this workspace, so Claude Code or Codex works in each channel's real folder, with your own tools and keys.</p>` + hostDiagram(display, accent) + `</div><div><form id="setup" method="post" action="/auth/new-workspace"><input type="hidden" name="token" value="${escapeHtml(token)}"><div class="fs-dim" style="margin:0 0 14px;font:600 11px/1.4 ${display};letter-spacing:0.06em;text-transform:uppercase;color:${DARK.dim};">Your account</div><label for="username" class="fs-muted" style="display:block;font:400 13px/1.4 ${display};color:${DARK.muted};margin-bottom:4px;">You'll be its Owner, signed in as</label><input id="username" name="username" type="email" autocomplete="username" readonly tabindex="-1" value="${escapeHtml(invite.email)}" style="display:block;width:100%;box-sizing:border-box;border:0;outline:0;background:transparent;padding:0;margin-bottom:20px;font:600 15px/1.4 ${display};color:${DARK.ink};text-overflow:ellipsis;"><div class="fs-pair"><div>` + passwordField("password", "Password", "new-password", display, !!form.passwordError) + `</div><div>` + passwordField("confirm", "Confirm password", "new-password", display, !!form.passwordError) + `</div></div>` + (form.passwordError ? `<p style="margin:-8px 0 16px;font:500 13px/1.5 ${display};color:#d33;">${escapeHtml(form.passwordError)}</p>` : `<p class="fs-dim" style="margin:-8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">At least ${PASSWORD_MIN} characters. You'll use it to sign in to your workspace.</p>`) + `<div style="border-top:1px solid ${DARK.line};margin:4px 0 24px;"></div><div class="fs-dim" style="margin:0 0 14px;font:600 11px/1.4 ${display};letter-spacing:0.06em;text-transform:uppercase;color:${DARK.dim};">Your workspace</div><label for="name" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">Workspace name</label><input id="name" name="name" required autocomplete="organization" maxlength="${DISPLAY_NAME_MAX}" value="${escapeHtml(form.name ?? "")}" placeholder="My Team" class="fs-card" style="display:block;width:100%;box-sizing:border-box;border:1px solid ${DARK.line};border-radius:8px;padding:12px;outline:0;background:transparent;font:500 15px/1.2 ${display};color:inherit;"><p class="fs-dim" style="margin:8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">Shown across your workspace. You can change it later.</p><label for="slug" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">Workspace URL</label><div class="fs-card" style="display:flex;align-items:center;border:1px solid ${form.error ? "#d33" : DARK.line};border-radius:8px;padding:0 12px;"><input id="slug" name="slug" required autocomplete="off" autocapitalize="none" spellcheck="false" minlength="${SLUG_MIN}" maxlength="${SLUG_MAX}" pattern="[a-z](?:[a-z0-9]|-(?!-))*[a-z0-9]" value="${escapeHtml(form.slug ?? "")}" placeholder="my-team" style="flex:1;min-width:0;border:0;outline:0;background:transparent;padding:12px 0;font:500 15px/1.2 ${display};color:inherit;"><span class="fs-dim" style="font:400 15px/1 ${display};color:${DARK.dim};white-space:nowrap;">${escapeHtml(suffix)}</span></div>` + (form.error ? `<p style="margin:8px 0 0;font:500 13px/1.5 ${display};color:#d33;">${escapeHtml(form.error)}</p>` : "") + `<p class="fs-dim" style="margin:8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">Lowercase letters, digits and hyphens, ${SLUG_MIN} to ${SLUG_MAX} characters. <strong style="font-weight:600;color:${DARK.ink};">It can't be changed later.</strong></p><button id="setup-submit" class="fs-btn" type="submit" style="background:${accent};color:${DARK.onAccent};border-radius:8px;padding:13px 24px;width:100%;font:600 15px/1 ${display};">Create workspace</button></form><div style="margin-top:20px;border-left:2px solid ${accent};padding:2px 0 2px 12px;"><p class="fs-muted" style="margin:0;font:400 13px/1.6 ${display};color:${DARK.muted};"><strong class="fs-ink" style="font-weight:600;color:${DARK.ink};">Next:</strong> we'll help you connect Claude Code or Codex CLI.</p></div><p class="fs-dim" style="margin:16px 0 0;font:400 12px/1.6 ${display};color:${DARK.dim};">By creating a workspace, you agree to the <a href="https://flyspace.ai/terms/" target="_blank" rel="noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">Terms</a> and <a href="https://flyspace.ai/privacy/" target="_blank" rel="noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">Privacy Policy</a>.</p></div></div><script>(()=>{const n=document.getElementById('name'),s=document.getElementById('slug');let touched=!!s.value;s.addEventListener('input',()=>{touched=true});n.addEventListener('input',()=>{if(touched)return;s.value=n.value.normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').replace(/^[^a-z]+/,'').slice(0,${SLUG_MAX}).replace(/-+$/,'')})})()</script><script>document.getElementById('setup').addEventListener('submit',()=>{const b=document.getElementById('setup-submit');b.disabled=true;b.style.opacity='0.7';b.style.cursor='progress';b.textContent='Creating workspace\u2026'})</script><script>document.querySelectorAll('[data-toggle-password]').forEach(b=>b.addEventListener('click',()=>{const i=document.getElementById(b.dataset.togglePassword),show=i.type==='password';i.type=show?'text':'password';b.setAttribute('aria-pressed',String(show));b.setAttribute('aria-label',show?'Hide password':'Show password');b.querySelector('[data-eye]').style.display=show?'none':'';b.querySelector('[data-eye-off]').style.display=show?'':'none'}))</script>`,
2607
2617
  void 0,
2608
2618
  void 0,
2609
2619
  true,
2610
- 960
2620
+ 960,
2621
+ "Create your workspace \xB7 Flyspace"
2611
2622
  );
2612
2623
  }
2613
2624
  function createAuthConfirmRouter() {
@@ -2773,6 +2784,35 @@ function createAuthConfirmRouter() {
2773
2784
  const passwordError = password.length < PASSWORD_MIN ? `Password must be at least ${PASSWORD_MIN} characters.` : password !== form["confirm"] ? "Passwords do not match." : null;
2774
2785
  if (passwordError)
2775
2786
  return c.html(setupPage(c, token, invite, { name, slug, passwordError }), 400);
2787
+ try {
2788
+ const { userId } = await ensureInviteUser(
2789
+ invite.email,
2790
+ invite.ownerName ? { name: invite.ownerName } : {}
2791
+ );
2792
+ const { error: error2 } = await getSupabaseClient().auth.admin.updateUserById(userId, { password });
2793
+ if (error2) {
2794
+ const weak = error2.code === "weak_password";
2795
+ if (!weak) console.error(`[new-workspace] setting the password for ${invite.email} failed:`, error2);
2796
+ return c.html(
2797
+ setupPage(c, token, invite, {
2798
+ name,
2799
+ slug,
2800
+ passwordError: weak ? "This password is too easy to guess or has appeared in a data breach. Please choose another." : "We could not set your password. Please try again in a moment."
2801
+ }),
2802
+ weak ? 400 : 500
2803
+ );
2804
+ }
2805
+ } catch (err) {
2806
+ console.error(`[new-workspace] preparing the owner account for ${invite.email} failed:`, err);
2807
+ return c.html(
2808
+ setupPage(c, token, invite, {
2809
+ name,
2810
+ slug,
2811
+ error: "Something went wrong creating your workspace. Please try again in a moment."
2812
+ }),
2813
+ 500
2814
+ );
2815
+ }
2776
2816
  if (!await claimSetupInvite(invite.id)) return c.redirect(back, 303);
2777
2817
  let workspace;
2778
2818
  try {
@@ -2804,16 +2844,6 @@ function createAuthConfirmRouter() {
2804
2844
  );
2805
2845
  }
2806
2846
  const origin = workspaceOrigin(c, workspace.slug);
2807
- let passwordSet = false;
2808
- try {
2809
- const userId = await authUserIdForEmail(invite.email);
2810
- if (!userId) throw new Error("owner has no auth user");
2811
- const { error: error2 } = await getSupabaseClient().auth.admin.updateUserById(userId, { password });
2812
- if (error2) throw new Error(error2.message);
2813
- passwordSet = true;
2814
- } catch (err) {
2815
- console.error(`[new-workspace] setting the password for ${workspace.slug} failed:`, err);
2816
- }
2817
2847
  try {
2818
2848
  const session = await mintSession(invite.email);
2819
2849
  const fragment = new URLSearchParams({
@@ -2827,8 +2857,7 @@ function createAuthConfirmRouter() {
2827
2857
  } catch (err) {
2828
2858
  console.error(`[new-workspace] signing in the owner of ${workspace.slug} failed:`, err);
2829
2859
  }
2830
- const login = passwordSet ? `?email=${encodeURIComponent(invite.email)}` : "";
2831
- return c.redirect(`${origin}/login${login}`, 303);
2860
+ return c.redirect(`${origin}/login?email=${encodeURIComponent(invite.email)}`, 303);
2832
2861
  });
2833
2862
  return confirm3;
2834
2863
  }
@@ -2887,10 +2916,20 @@ async function deliverInvite({
2887
2916
  });
2888
2917
  await sendEmail({ to: email, ...content });
2889
2918
  }
2919
+ function flyspaceLogo(origin) {
2920
+ try {
2921
+ const url = new URL(origin);
2922
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1";
2923
+ return url.protocol === "https:" && !local ? { logoUrl: `${url.origin}/icons/icon-192.png` } : {};
2924
+ } catch {
2925
+ return {};
2926
+ }
2927
+ }
2890
2928
  async function deliverWorkspaceSetupInvite(params) {
2891
2929
  if (!isMailConfigured()) throw new MailNotConfiguredError();
2892
2930
  const content = workspaceSetupEmail({
2893
2931
  actionLink: params.inviteLink,
2932
+ ...flyspaceLogo(originOf(params.inviteLink)),
2894
2933
  ...params.invitedBy ? { invitedBy: params.invitedBy } : {},
2895
2934
  ...params.message?.trim() ? { message: params.message } : {}
2896
2935
  });
@@ -2916,10 +2955,10 @@ async function deliverSignInLink(email, redirectTo) {
2916
2955
  }
2917
2956
 
2918
2957
  // src/dashboard/server.ts
2919
- import { existsSync as existsSync26 } from "fs";
2958
+ import { existsSync as existsSync27 } from "fs";
2920
2959
  import { createServer } from "net";
2921
- import { join as join18 } from "path";
2922
- import { Hono as Hono29 } from "hono";
2960
+ import { join as join19 } from "path";
2961
+ import { Hono as Hono30 } from "hono";
2923
2962
  import { serve } from "@hono/node-server";
2924
2963
  import { serveStatic } from "@hono/node-server/serve-static";
2925
2964
 
@@ -2965,7 +3004,7 @@ async function reconcileAll() {
2965
3004
  }
2966
3005
 
2967
3006
  // src/dashboard/api.ts
2968
- import { Hono as Hono26 } from "hono";
3007
+ import { Hono as Hono27 } from "hono";
2969
3008
 
2970
3009
  // src/dashboard/public-share-routes.ts
2971
3010
  import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
@@ -4022,7 +4061,7 @@ function truncationNotice(lang, voice) {
4022
4061
  if (lang === "en" || lang.startsWith("en-")) return TRUNCATION_NOTICE.en;
4023
4062
  return null;
4024
4063
  }
4025
- var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
4064
+ var sleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
4026
4065
  async function synthesizeWithRetry(script, voice) {
4027
4066
  let lastError;
4028
4067
  for (let attempt = 1; attempt <= SYNTH_ATTEMPTS; attempt += 1) {
@@ -5035,6 +5074,9 @@ import { isAuthApiError, isAuthRetryableFetchError } from "@supabase/supabase-js
5035
5074
  var inFlight3 = /* @__PURE__ */ new Map();
5036
5075
  var CACHE_TTL_MS = 3e4;
5037
5076
  var resolved = /* @__PURE__ */ new Map();
5077
+ function invalidateAuthContextCache() {
5078
+ resolved.clear();
5079
+ }
5038
5080
  function pruneExpired(now) {
5039
5081
  for (const [key, entry] of resolved) {
5040
5082
  if (entry.expires <= now) resolved.delete(key);
@@ -5138,7 +5180,10 @@ function siteverify(secret, token) {
5138
5180
  async function verifyTurnstileToken(token) {
5139
5181
  const secret = profileEnv("TURNSTILE_SECRET_KEY")?.trim();
5140
5182
  if (!secret) return true;
5141
- if (!token) return false;
5183
+ if (!token) {
5184
+ console.warn("[turnstile] no token sent (widget failed to load or errored in the browser)");
5185
+ return false;
5186
+ }
5142
5187
  let res;
5143
5188
  try {
5144
5189
  res = await siteverify(secret, token);
@@ -5149,9 +5194,15 @@ async function verifyTurnstileToken(token) {
5149
5194
  return false;
5150
5195
  }
5151
5196
  }
5152
- if (!res.ok) return false;
5197
+ if (!res.ok) {
5198
+ console.warn("[turnstile] siteverify HTTP", res.status);
5199
+ return false;
5200
+ }
5153
5201
  const data = await res.json().catch(() => ({}));
5154
- if (data.success !== true) return false;
5202
+ if (data.success !== true) {
5203
+ console.warn("[turnstile] rejected", data["error-codes"] ?? [], data.hostname ?? "");
5204
+ return false;
5205
+ }
5155
5206
  return data.action === void 0 || data.action === "login";
5156
5207
  }
5157
5208
 
@@ -6413,7 +6464,7 @@ function needsRefresh(expiresAt, now = Date.now()) {
6413
6464
  if (!expiresAt) return false;
6414
6465
  return new Date(expiresAt).getTime() - now < REFRESH_SKEW_MS;
6415
6466
  }
6416
- var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
6467
+ var sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
6417
6468
  async function refreshConnection(params) {
6418
6469
  const { workspaceId, connection, provider } = params;
6419
6470
  const deps = params.deps ?? {};
@@ -6657,8 +6708,8 @@ function collectFrom(root, source) {
6657
6708
  return { name, description: (fm.description ?? "").trim(), source, path };
6658
6709
  });
6659
6710
  }
6660
- function listAgents(configDir, cwd) {
6661
- const base = configDir ? expandHome(configDir) : defaultConfigDir();
6711
+ function listAgents(configDir2, cwd) {
6712
+ const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
6662
6713
  const collected = [
6663
6714
  ...collectFrom(join11(cwd, ".claude", "agents"), "project"),
6664
6715
  ...collectFrom(join11(base, "agents"), "user")
@@ -6669,8 +6720,8 @@ function listAgents(configDir, cwd) {
6669
6720
  }
6670
6721
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
6671
6722
  }
6672
- function readAgentPersona(configDir, cwd, name) {
6673
- const agent = listAgents(configDir, cwd).find((a) => a.name === name);
6723
+ function readAgentPersona(configDir2, cwd, name) {
6724
+ const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
6674
6725
  if (!agent) return void 0;
6675
6726
  try {
6676
6727
  const content = readFileSync7(agent.path, "utf-8");
@@ -6722,7 +6773,7 @@ Rules:
6722
6773
  - No preamble like "Summary:" \u2014 start directly with the summary text.`;
6723
6774
  var HAIKU_TIMEOUT_MS = 6e4;
6724
6775
  function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
6725
- return new Promise((resolve7, reject) => {
6776
+ return new Promise((resolve8, reject) => {
6726
6777
  const args = [
6727
6778
  "--print",
6728
6779
  "--model",
@@ -6773,7 +6824,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
6773
6824
  reject(new Error("claude -p --model haiku returned empty output"));
6774
6825
  return;
6775
6826
  }
6776
- resolve7(text);
6827
+ resolve8(text);
6777
6828
  });
6778
6829
  });
6779
6830
  }
@@ -7672,22 +7723,22 @@ var ChatAgent = class {
7672
7723
  let ready;
7673
7724
  try {
7674
7725
  channel = this.supabase().channel(`agent-thinking:${threadId}`);
7675
- ready = new Promise((resolve7) => {
7726
+ ready = new Promise((resolve8) => {
7676
7727
  const timeout = setTimeout(() => {
7677
7728
  console.error(`[chat-agent] thinking-channel ${threadId}: subscribe timed out after 10s`);
7678
- resolve7(false);
7729
+ resolve8(false);
7679
7730
  }, 1e4);
7680
7731
  channel.subscribe((status, err) => {
7681
7732
  if (status === "SUBSCRIBED") {
7682
7733
  clearTimeout(timeout);
7683
- resolve7(true);
7734
+ resolve8(true);
7684
7735
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
7685
7736
  console.error(
7686
7737
  `[chat-agent] thinking-channel ${threadId}: ${status}`,
7687
7738
  err?.message ?? ""
7688
7739
  );
7689
7740
  clearTimeout(timeout);
7690
- resolve7(false);
7741
+ resolve8(false);
7691
7742
  }
7692
7743
  });
7693
7744
  });
@@ -7734,19 +7785,19 @@ var ChatAgent = class {
7734
7785
  let ready;
7735
7786
  try {
7736
7787
  channel = this.supabase().channel("channel-activity");
7737
- ready = new Promise((resolve7) => {
7788
+ ready = new Promise((resolve8) => {
7738
7789
  const timeout = setTimeout(() => {
7739
7790
  console.error("[chat-agent] channel-activity: subscribe timed out after 10s");
7740
- resolve7(false);
7791
+ resolve8(false);
7741
7792
  }, 1e4);
7742
7793
  channel.subscribe((status, err) => {
7743
7794
  if (status === "SUBSCRIBED") {
7744
7795
  clearTimeout(timeout);
7745
- resolve7(true);
7796
+ resolve8(true);
7746
7797
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
7747
7798
  console.error(`[chat-agent] channel-activity: ${status}`, err?.message ?? "");
7748
7799
  clearTimeout(timeout);
7749
- resolve7(false);
7800
+ resolve8(false);
7750
7801
  }
7751
7802
  });
7752
7803
  });
@@ -7773,7 +7824,6 @@ var ChatAgent = class {
7773
7824
  async tryAsk(channelConfig, prompt, sessionId, onActivity, signal, usageContext) {
7774
7825
  const name = subAgentName(channelConfig.agentName);
7775
7826
  const agentPersona = name ? readAgentPersona(channelConfig.claudeConfigDir, channelConfig.cwd, name) : void 0;
7776
- const outputStyle = await resolveOutputStyleForProfile(channelConfig.profile);
7777
7827
  try {
7778
7828
  return await withMcpConfig(
7779
7829
  channelConfig.profile,
@@ -7790,7 +7840,6 @@ var ChatAgent = class {
7790
7840
  maxBudgetUsd: this.config.maxBudgetUsd,
7791
7841
  agentPersona,
7792
7842
  channelPrompt: channelConfig.prompt,
7793
- outputStyle,
7794
7843
  onActivity,
7795
7844
  signal,
7796
7845
  mcpConfigPath,
@@ -7809,7 +7858,6 @@ var ChatAgent = class {
7809
7858
  async tryAskCodex(channelConfig, prompt, sessionId, signal, onActivity, usageContext) {
7810
7859
  const name = subAgentName(channelConfig.agentName);
7811
7860
  const agentPersona = name ? readAgentPersona(channelConfig.claudeConfigDir, channelConfig.cwd, name) : void 0;
7812
- const outputStyle = await resolveOutputStyleForProfile(channelConfig.profile);
7813
7861
  try {
7814
7862
  return await askCodex(
7815
7863
  prompt,
@@ -7829,7 +7877,7 @@ var ChatAgent = class {
7829
7877
  this.config.botName,
7830
7878
  agentPersona,
7831
7879
  channelConfig.prompt,
7832
- outputStyle?.body
7880
+ resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
7833
7881
  ),
7834
7882
  ...onActivity ? { onActivity } : {},
7835
7883
  ...signal ? { signal } : {},
@@ -8096,10 +8144,10 @@ ${lines}
8096
8144
  // src/dashboard/artifact-export.ts
8097
8145
  async function exportDocAsArtifact(channelId, docPath) {
8098
8146
  const owningProfile = findOwningProfile(channelId);
8099
- const { channelConfig, outputStyle } = await runWithProfile(owningProfile, async () => ({
8100
- channelConfig: resolveChannelConfig(channelId),
8101
- outputStyle: await resolveOutputStyleForProfile(getProfile())
8102
- }));
8147
+ const channelConfig = await runWithProfile(
8148
+ owningProfile,
8149
+ async () => resolveChannelConfig(channelId)
8150
+ );
8103
8151
  const abs = resolveDocPath(channelConfig.cwd, docPath);
8104
8152
  if (!abs) throw new Error(`doc not found: ${docPath}`);
8105
8153
  const content = readFileSync8(abs, "utf8");
@@ -8133,7 +8181,6 @@ ${content}`;
8133
8181
  runtime: channelConfig.runtime,
8134
8182
  mounts: channelConfig.mounts,
8135
8183
  channelPrompt: resolveChannelPrompt(channelId),
8136
- outputStyle,
8137
8184
  usageContext: { source: "automation", channelId, label: "export-artifact" }
8138
8185
  },
8139
8186
  channelConfig.claudeConfigDir,
@@ -8980,10 +9027,123 @@ function createDocsRouter() {
8980
9027
  return app;
8981
9028
  }
8982
9029
 
9030
+ // src/dashboard/output-styles-routes.ts
9031
+ import { Hono as Hono10 } from "hono";
9032
+ function requireAdmin(auth) {
9033
+ return auth?.role === "admin" ? null : "admin access required";
9034
+ }
9035
+ function toResponse3(style) {
9036
+ return {
9037
+ id: style.id,
9038
+ name: style.name,
9039
+ description: style.description,
9040
+ body: style.body,
9041
+ createdAt: style.createdAt,
9042
+ updatedAt: style.updatedAt
9043
+ };
9044
+ }
9045
+ var configDir = () => profileEnv("CLAUDE_CONFIG_DIR");
9046
+ function failure(c, err) {
9047
+ const message = err.message;
9048
+ return c.json({ error: message }, err instanceof OutputStyleConflictError ? 409 : 500);
9049
+ }
9050
+ function createOutputStylesRouter() {
9051
+ const app = new Hono10();
9052
+ app.get("/", (c) => {
9053
+ const auth = c.get("auth");
9054
+ const denied = requireAdmin(auth);
9055
+ if (denied) return c.json({ error: denied }, 403);
9056
+ try {
9057
+ return c.json({
9058
+ builtIn: BUILT_IN_OUTPUT_STYLES,
9059
+ custom: listOutputStyles(configDir()).map(toResponse3),
9060
+ defaultId: getOutputStyleDefault(configDir())
9061
+ });
9062
+ } catch (err) {
9063
+ return failure(c, err);
9064
+ }
9065
+ });
9066
+ app.post("/", async (c) => {
9067
+ const auth = c.get("auth");
9068
+ const denied = requireAdmin(auth);
9069
+ if (denied) return c.json({ error: denied }, 403);
9070
+ const body = await c.req.json().catch(() => null);
9071
+ const name = typeof body?.name === "string" ? body.name.trim() : "";
9072
+ if (!name) return c.json({ error: "name is required" }, 400);
9073
+ const description = typeof body?.description === "string" ? body.description.trim() : "";
9074
+ const content = typeof body?.body === "string" ? body.body : "";
9075
+ try {
9076
+ return c.json(
9077
+ toResponse3(createOutputStyle(configDir(), { name, description, body: content })),
9078
+ 201
9079
+ );
9080
+ } catch (err) {
9081
+ return failure(c, err);
9082
+ }
9083
+ });
9084
+ app.put("/default", async (c) => {
9085
+ const auth = c.get("auth");
9086
+ const denied = requireAdmin(auth);
9087
+ if (denied) return c.json({ error: denied }, 403);
9088
+ const body = await c.req.json().catch(() => null);
9089
+ const id = typeof body?.id === "string" ? body.id : "";
9090
+ if (!id) return c.json({ error: "id is required" }, 400);
9091
+ try {
9092
+ if (!setOutputStyleDefault(configDir(), id)) {
9093
+ return c.json({ error: "output style not found" }, 404);
9094
+ }
9095
+ } catch (err) {
9096
+ return failure(c, err);
9097
+ }
9098
+ return c.json({ defaultId: id });
9099
+ });
9100
+ app.put("/:id", async (c) => {
9101
+ const auth = c.get("auth");
9102
+ const denied = requireAdmin(auth);
9103
+ if (denied) return c.json({ error: denied }, 403);
9104
+ const body = await c.req.json().catch(() => null);
9105
+ if (!body) return c.json({ error: "a body is required" }, 400);
9106
+ const patch = {};
9107
+ if (body.name !== void 0) {
9108
+ const name = typeof body.name === "string" ? body.name.trim() : "";
9109
+ if (!name) return c.json({ error: "name cannot be empty" }, 400);
9110
+ patch.name = name;
9111
+ }
9112
+ if (body.description !== void 0) {
9113
+ patch.description = typeof body.description === "string" ? body.description.trim() : "";
9114
+ }
9115
+ if (body.body !== void 0) {
9116
+ patch.body = typeof body.body === "string" ? body.body : "";
9117
+ }
9118
+ if (!Object.keys(patch).length) return c.json({ error: "nothing to update" }, 400);
9119
+ try {
9120
+ const updated = updateOutputStyle(configDir(), c.req.param("id"), patch);
9121
+ if (!updated) return c.json({ error: "output style not found" }, 404);
9122
+ return c.json(toResponse3(updated));
9123
+ } catch (err) {
9124
+ return failure(c, err);
9125
+ }
9126
+ });
9127
+ app.delete("/:id", async (c) => {
9128
+ const auth = c.get("auth");
9129
+ const denied = requireAdmin(auth);
9130
+ if (denied) return c.json({ error: denied }, 403);
9131
+ try {
9132
+ if (!deleteOutputStyle(configDir(), c.req.param("id"))) {
9133
+ return c.json({ error: "output style not found" }, 404);
9134
+ }
9135
+ } catch (err) {
9136
+ return failure(c, err);
9137
+ }
9138
+ return c.json({ ok: true });
9139
+ });
9140
+ return app;
9141
+ }
9142
+
8983
9143
  // src/dashboard/skills-routes.ts
8984
9144
  import { existsSync as existsSync18, statSync as statSync6 } from "fs";
8985
9145
  import { dirname as dirname4 } from "path";
8986
- import { Hono as Hono10 } from "hono";
9146
+ import { Hono as Hono11 } from "hono";
8987
9147
 
8988
9148
  // src/dashboard/skill-mirror.ts
8989
9149
  import { createHash as createHash8 } from "crypto";
@@ -9010,11 +9170,11 @@ function ownSkills(scope, dir) {
9010
9170
  }
9011
9171
  function snapshot(scopes) {
9012
9172
  const wants = (scope) => !scopes || scopes.has(scope);
9013
- const configDir = profileEnv("CLAUDE_CONFIG_DIR");
9173
+ const configDir2 = profileEnv("CLAUDE_CONFIG_DIR");
9014
9174
  const rows = [];
9015
- if (wants("workspace")) rows.push(...ownSkills("workspace", workspaceSkillsDir(configDir)));
9175
+ if (wants("workspace")) rows.push(...ownSkills("workspace", workspaceSkillsDir(configDir2)));
9016
9176
  if (wants("plugin")) {
9017
- for (const s of listPluginSkillDetails(configDir, defaultClaudeCwd())) {
9177
+ for (const s of listPluginSkillDetails(configDir2, defaultClaudeCwd())) {
9018
9178
  rows.push({
9019
9179
  scope: "plugin",
9020
9180
  name: s.name,
@@ -9131,7 +9291,7 @@ function channelScope(channelId) {
9131
9291
  function inheritedSkills(scope, own) {
9132
9292
  return listSkills(scope.claudeConfigDir, scope.cwd).filter((s) => scope.inheritFrom.includes(s.source) && !own.has(s.name)).sort((a, b) => a.name.localeCompare(b.name));
9133
9293
  }
9134
- function requireAdmin(auth) {
9294
+ function requireAdmin2(auth) {
9135
9295
  return auth?.role === "admin" ? null : "admin access required";
9136
9296
  }
9137
9297
  function rootExists(dir) {
@@ -9271,7 +9431,7 @@ function mountSkillRoutes(app, base, refOf, backend) {
9271
9431
  return detail ? c.json(detail) : c.json({ error: "not found" }, 404);
9272
9432
  });
9273
9433
  app.post(base, async (c) => {
9274
- const denied = requireAdmin(c.get("auth"));
9434
+ const denied = requireAdmin2(c.get("auth"));
9275
9435
  if (denied) return c.json({ error: denied }, 403);
9276
9436
  const body = await c.req.json().catch(() => null);
9277
9437
  const name = typeof body?.name === "string" ? body.name.trim() : "";
@@ -9283,7 +9443,7 @@ function mountSkillRoutes(app, base, refOf, backend) {
9283
9443
  return c.json(result.body, result.status);
9284
9444
  });
9285
9445
  app.put(`${base}/:name`, async (c) => {
9286
- const denied = requireAdmin(c.get("auth"));
9446
+ const denied = requireAdmin2(c.get("auth"));
9287
9447
  if (denied) return c.json({ error: denied }, 403);
9288
9448
  const body = await c.req.json().catch(() => null);
9289
9449
  if (!body || typeof body.body !== "string" || typeof body.description !== "string") {
@@ -9299,7 +9459,7 @@ function mountSkillRoutes(app, base, refOf, backend) {
9299
9459
  return c.json(result.body, result.status);
9300
9460
  });
9301
9461
  app.delete(`${base}/:name`, async (c) => {
9302
- const denied = requireAdmin(c.get("auth"));
9462
+ const denied = requireAdmin2(c.get("auth"));
9303
9463
  if (denied) return c.json({ error: denied }, 403);
9304
9464
  const result = await backend.write(refOf(c), { op: "delete", name: c.req.param("name") });
9305
9465
  return c.json(result.body, result.status);
@@ -9308,12 +9468,12 @@ function mountSkillRoutes(app, base, refOf, backend) {
9308
9468
  var workspaceRef = () => ({ kind: "workspace" });
9309
9469
  var channelRef = (c) => ({ kind: "channel", channelId: c.req.param("id") });
9310
9470
  function createWorkspaceSkillsRouter() {
9311
- const app = new Hono10();
9471
+ const app = new Hono11();
9312
9472
  mountSkillRoutes(app, "/skills", workspaceRef, diskBackend);
9313
9473
  return app;
9314
9474
  }
9315
9475
  function createChannelSkillsRouter() {
9316
- const app = new Hono10();
9476
+ const app = new Hono11();
9317
9477
  mountSkillRoutes(app, "/:id/skill-files", channelRef, diskBackend);
9318
9478
  return app;
9319
9479
  }
@@ -9370,8 +9530,8 @@ function binaryOnPath(bin) {
9370
9530
  }
9371
9531
  function claudeAuthAvailable() {
9372
9532
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
9373
- const configDir = profileEnv("CLAUDE_CONFIG_DIR")?.trim() || resolve5(homedir6(), ".claude");
9374
- if (existsSync19(join14(configDir, ".credentials.json"))) return true;
9533
+ const configDir2 = profileEnv("CLAUDE_CONFIG_DIR")?.trim() || resolve5(homedir6(), ".claude");
9534
+ if (existsSync19(join14(configDir2, ".credentials.json"))) return true;
9375
9535
  if (process.platform !== "darwin") return false;
9376
9536
  try {
9377
9537
  execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
@@ -9420,6 +9580,7 @@ function registerHostRoutes(api) {
9420
9580
  });
9421
9581
  api.route("/channels", createChannelSkillsRouter());
9422
9582
  api.route("/", createWorkspaceSkillsRouter());
9583
+ api.route("/output-styles", createOutputStylesRouter());
9423
9584
  api.route("/channels", createDocsRouter());
9424
9585
  api.route("/channels", createChannelWriteRouter());
9425
9586
  api.get("/settings", async (c) => c.json(await loadFullDashboardSettings()));
@@ -9529,11 +9690,11 @@ function tailFile(file, limit) {
9529
9690
  }
9530
9691
 
9531
9692
  // src/dashboard/data-api.ts
9532
- import { existsSync as existsSync22 } from "fs";
9693
+ import { existsSync as existsSync23 } from "fs";
9533
9694
  import sharp5 from "sharp";
9534
9695
 
9535
9696
  // src/dashboard/automations-routes.ts
9536
- import { Hono as Hono11 } from "hono";
9697
+ import { Hono as Hono12 } from "hono";
9537
9698
  var TYPES2 = ["schedule", "trigger", "mention-trigger"];
9538
9699
  function isType(t) {
9539
9700
  return TYPES2.includes(t);
@@ -9550,7 +9711,7 @@ function statusFor(message) {
9550
9711
  return 400;
9551
9712
  }
9552
9713
  function createAutomationWriteRouter() {
9553
- const app = new Hono11();
9714
+ const app = new Hono12();
9554
9715
  const manager = () => new AutomationManager();
9555
9716
  app.get("/:type/:name", (c) => {
9556
9717
  const item = manager().get(c.req.param("name"));
@@ -9606,9 +9767,9 @@ function createAutomationWriteRouter() {
9606
9767
  }
9607
9768
 
9608
9769
  // src/dashboard/categories-routes.ts
9609
- import { Hono as Hono12 } from "hono";
9770
+ import { Hono as Hono13 } from "hono";
9610
9771
  function createCategoryWriteRouter() {
9611
- const app = new Hono12();
9772
+ const app = new Hono13();
9612
9773
  app.get("/", async (c) => {
9613
9774
  const { data, error: error2 } = await getSupabaseClient().from("categories").select("*").eq("profile", getProfile()).order("position", { ascending: true });
9614
9775
  if (error2) return c.json({ error: error2.message }, 500);
@@ -9662,7 +9823,7 @@ function createCategoryWriteRouter() {
9662
9823
  }
9663
9824
 
9664
9825
  // src/dashboard/chat-routes.ts
9665
- import { Hono as Hono13 } from "hono";
9826
+ import { Hono as Hono14 } from "hono";
9666
9827
 
9667
9828
  // src/dashboard/fork-summary.ts
9668
9829
  var GROQ_API_URL5 = "https://api.groq.com/openai/v1/chat/completions";
@@ -10106,7 +10267,7 @@ async function withPreviews(supabase, threads) {
10106
10267
  });
10107
10268
  }
10108
10269
  function createChatRouter(agent = {}) {
10109
- const app = new Hono13();
10270
+ const app = new Hono14();
10110
10271
  const {
10111
10272
  onChatMessage: onMessage,
10112
10273
  isAgentThinking,
@@ -10596,7 +10757,7 @@ function createChatRouter(agent = {}) {
10596
10757
  }
10597
10758
 
10598
10759
  // src/dashboard/doc-shares-routes.ts
10599
- import { Hono as Hono14 } from "hono";
10760
+ import { Hono as Hono15 } from "hono";
10600
10761
 
10601
10762
  // src/dashboard/doc-share-features.ts
10602
10763
  import { DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
@@ -10756,7 +10917,7 @@ function resolveChannel2(c) {
10756
10917
  }
10757
10918
  var LIST_LIMIT = 500;
10758
10919
  function createDocShareListRouter() {
10759
- const app = new Hono14();
10920
+ const app = new Hono15();
10760
10921
  app.get("/shares", async (c) => {
10761
10922
  const viewer = c.get("auth");
10762
10923
  const channels = new Map(
@@ -10792,7 +10953,7 @@ function createDocShareListRouter() {
10792
10953
  return app;
10793
10954
  }
10794
10955
  function createDocShareWriteRouter(agent = {}) {
10795
- const app = new Hono14();
10956
+ const app = new Hono15();
10796
10957
  app.get("/:id/docs/share", async (c) => {
10797
10958
  const resolved2 = resolveChannel2(c);
10798
10959
  if ("error" in resolved2) return resolved2.error;
@@ -10939,7 +11100,7 @@ function createDocShareWriteRouter(agent = {}) {
10939
11100
  }
10940
11101
 
10941
11102
  // src/dashboard/search-routes.ts
10942
- import { Hono as Hono15 } from "hono";
11103
+ import { Hono as Hono16 } from "hono";
10943
11104
  function matchScore(q, fields) {
10944
11105
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
10945
11106
  if (terms.length === 0) return 0;
@@ -11058,7 +11219,7 @@ function searchAutomations(q) {
11058
11219
  }
11059
11220
  var RESULT_LIMIT = 30;
11060
11221
  function createSearchRouter() {
11061
- const app = new Hono15();
11222
+ const app = new Hono16();
11062
11223
  app.get("/", async (c) => {
11063
11224
  const q = c.req.query("q")?.trim();
11064
11225
  if (!q) return c.json({ results: [] });
@@ -11075,7 +11236,7 @@ function createSearchRouter() {
11075
11236
 
11076
11237
  // src/dashboard/uploads-routes.ts
11077
11238
  import { randomUUID } from "crypto";
11078
- import { Hono as Hono16 } from "hono";
11239
+ import { Hono as Hono17 } from "hono";
11079
11240
  import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
11080
11241
  import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
11081
11242
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
@@ -11090,7 +11251,7 @@ function encodeKeyForUrl(key) {
11090
11251
  return key.split("/").map(encodeURIComponent).join("/");
11091
11252
  }
11092
11253
  function createUploadsRouter() {
11093
- const app = new Hono16();
11254
+ const app = new Hono17();
11094
11255
  app.post("/uploads", async (c) => {
11095
11256
  const body = await c.req.parseBody().catch(() => null);
11096
11257
  const file = body?.["file"];
@@ -11136,7 +11297,7 @@ function createUploadsRouter() {
11136
11297
 
11137
11298
  // src/dashboard/avatars-routes.ts
11138
11299
  import { randomUUID as randomUUID2 } from "crypto";
11139
- import { Hono as Hono17 } from "hono";
11300
+ import { Hono as Hono18 } from "hono";
11140
11301
  import sharp4 from "sharp";
11141
11302
  import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
11142
11303
  var AVATAR_SIZE = 192;
@@ -11155,10 +11316,12 @@ async function saveAvatarKey(userId, avatarKey) {
11155
11316
  user_metadata: metadata
11156
11317
  });
11157
11318
  if (error2) throw new Error(`saveAvatarKey(${userId}): ${error2.message}`);
11319
+ invalidateAuthContextCache();
11158
11320
  }
11159
11321
  async function storeAvatar(file, owner) {
11160
11322
  if (!(file instanceof File)) return { error: "missing file" };
11161
- if (!AVATAR_MIME_TYPES.includes(file.type)) return { error: "avatar must be a PNG or JPEG image" };
11323
+ if (!AVATAR_MIME_TYPES.includes(file.type))
11324
+ return { error: "avatar must be a PNG or JPEG image" };
11162
11325
  if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
11163
11326
  const original = Buffer.from(await file.arrayBuffer());
11164
11327
  const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
@@ -11177,7 +11340,7 @@ function avatarKeyPrefix() {
11177
11340
  return `${KEY_PREFIX3}/${getProfile()}/`;
11178
11341
  }
11179
11342
  function createAvatarsRouter() {
11180
- const app = new Hono17();
11343
+ const app = new Hono18();
11181
11344
  app.get("/avatars/slack", async (c) => {
11182
11345
  const ctx = await resolveAuthContext(c);
11183
11346
  if (!ctx) return c.json({ error: "not signed in" }, 401);
@@ -11215,7 +11378,7 @@ function createAvatarsRouter() {
11215
11378
  }
11216
11379
 
11217
11380
  // src/dashboard/members-routes.ts
11218
- import { Hono as Hono18 } from "hono";
11381
+ import { Hono as Hono19 } from "hono";
11219
11382
  function inviteLinkFor(c, token) {
11220
11383
  const url = new URL("/auth/invite", callbackUrl(c));
11221
11384
  url.searchParams.set("token", token);
@@ -11223,7 +11386,7 @@ function inviteLinkFor(c, token) {
11223
11386
  }
11224
11387
  var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
11225
11388
  function createMembersRouter() {
11226
- const members = new Hono18();
11389
+ const members = new Hono19();
11227
11390
  members.get("/", async (c) => {
11228
11391
  const ctx = await resolveAuthContext(c);
11229
11392
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -11407,7 +11570,7 @@ function createMembersRouter() {
11407
11570
  }
11408
11571
 
11409
11572
  // src/dashboard/notifications-routes.ts
11410
- import { Hono as Hono19 } from "hono";
11573
+ import { Hono as Hono20 } from "hono";
11411
11574
 
11412
11575
  // src/store/notification-store.ts
11413
11576
  function rowToNotification(row) {
@@ -11475,7 +11638,7 @@ async function markAllRead(channelIds, userId) {
11475
11638
 
11476
11639
  // src/dashboard/notifications-routes.ts
11477
11640
  function createNotificationsRouter() {
11478
- const notifications = new Hono19();
11641
+ const notifications = new Hono20();
11479
11642
  notifications.get("/", async (c) => {
11480
11643
  const auth = c.get("auth");
11481
11644
  const channelIds = getProfileChannelRegistry().filter((ch) => canSeeChannel(ch, auth)).map((ch) => ch.id);
@@ -11502,7 +11665,7 @@ function createNotificationsRouter() {
11502
11665
  return notifications;
11503
11666
  }
11504
11667
  function createThreadReadRouter() {
11505
- const threads = new Hono19();
11668
+ const threads = new Hono20();
11506
11669
  threads.post("/threads/:threadId/read", async (c) => {
11507
11670
  const { userId } = c.get("auth");
11508
11671
  const threadId = c.req.param("threadId");
@@ -11514,7 +11677,7 @@ function createThreadReadRouter() {
11514
11677
  }
11515
11678
 
11516
11679
  // src/dashboard/push-routes.ts
11517
- import { Hono as Hono20 } from "hono";
11680
+ import { Hono as Hono21 } from "hono";
11518
11681
 
11519
11682
  // src/push/send.ts
11520
11683
  import webpush from "web-push";
@@ -11617,7 +11780,7 @@ async function pushToUser(userId, payload) {
11617
11780
 
11618
11781
  // src/dashboard/push-routes.ts
11619
11782
  function createPushRouter() {
11620
- const push = new Hono20();
11783
+ const push = new Hono21();
11621
11784
  push.get("/key", (c) => c.json({ publicKey: getVapidPublicKey() }));
11622
11785
  push.post("/subscribe", async (c) => {
11623
11786
  const { userId } = c.get("auth");
@@ -11648,7 +11811,7 @@ function createPushRouter() {
11648
11811
  }
11649
11812
 
11650
11813
  // src/dashboard/mcp-oauth-routes.ts
11651
- import { Hono as Hono21 } from "hono";
11814
+ import { Hono as Hono22 } from "hono";
11652
11815
  var CONNECTIONS_PAGE = "/settings/connections";
11653
11816
  function callbackUri(c) {
11654
11817
  const proto = c.req.header("x-forwarded-proto") ?? "http";
@@ -11663,7 +11826,7 @@ function backToDashboard(c, to, params) {
11663
11826
  return c.redirect(`${target.pathname}${target.search}`);
11664
11827
  }
11665
11828
  function createMcpOAuthRouter() {
11666
- const app = new Hono21();
11829
+ const app = new Hono22();
11667
11830
  app.get("/oauth/:provider/start", async (c) => {
11668
11831
  const auth = c.get("auth");
11669
11832
  if (auth.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -11790,7 +11953,7 @@ function createMcpOAuthRouter() {
11790
11953
  }
11791
11954
 
11792
11955
  // src/dashboard/mcp-routes.ts
11793
- import { Hono as Hono22 } from "hono";
11956
+ import { Hono as Hono23 } from "hono";
11794
11957
 
11795
11958
  // src/mcp/transport.ts
11796
11959
  var KINDS = ["inherit", "npx", "local", "http", "sse"];
@@ -11884,10 +12047,10 @@ function fromServerCommand(command) {
11884
12047
 
11885
12048
  // src/dashboard/mcp-routes.ts
11886
12049
  var CUSTOM_PROVIDER = "custom";
11887
- function requireAdmin2(auth) {
12050
+ function requireAdmin3(auth) {
11888
12051
  return auth?.role === "admin" ? null : "admin access required";
11889
12052
  }
11890
- function toResponse3(connection, channelIds, providerCommand = null) {
12053
+ function toResponse4(connection, channelIds, providerCommand = null) {
11891
12054
  return {
11892
12055
  id: connection.id,
11893
12056
  providerSlug: connection.providerSlug,
@@ -11920,9 +12083,9 @@ function bindingsByConnection() {
11920
12083
  return map;
11921
12084
  }
11922
12085
  function createMcpRouter() {
11923
- const app = new Hono22();
12086
+ const app = new Hono23();
11924
12087
  app.get("/providers", async (c) => {
11925
- const denied = requireAdmin2(c.get("auth"));
12088
+ const denied = requireAdmin3(c.get("auth"));
11926
12089
  if (denied) return c.json({ error: denied }, 403);
11927
12090
  const providers = await listMcpProviders();
11928
12091
  return c.json({
@@ -11940,7 +12103,7 @@ function createMcpRouter() {
11940
12103
  });
11941
12104
  app.get("/connections", async (c) => {
11942
12105
  const auth = c.get("auth");
11943
- const denied = requireAdmin2(auth);
12106
+ const denied = requireAdmin3(auth);
11944
12107
  if (denied) return c.json({ error: denied }, 403);
11945
12108
  const connections = await listMcpConnections(auth.workspaceId);
11946
12109
  const commands = new Map(
@@ -11949,13 +12112,13 @@ function createMcpRouter() {
11949
12112
  const bindings = bindingsByConnection();
11950
12113
  return c.json({
11951
12114
  connections: connections.map(
11952
- (conn) => toResponse3(conn, bindings.get(conn.id) ?? [], commands.get(conn.providerSlug) ?? null)
12115
+ (conn) => toResponse4(conn, bindings.get(conn.id) ?? [], commands.get(conn.providerSlug) ?? null)
11953
12116
  )
11954
12117
  });
11955
12118
  });
11956
12119
  app.post("/connections", async (c) => {
11957
12120
  const auth = c.get("auth");
11958
- const denied = requireAdmin2(auth);
12121
+ const denied = requireAdmin3(auth);
11959
12122
  if (denied) return c.json({ error: denied }, 403);
11960
12123
  const body = await c.req.json().catch(() => null);
11961
12124
  const label2 = typeof body?.label === "string" ? body.label.trim() : "";
@@ -11981,7 +12144,7 @@ function createMcpRouter() {
11981
12144
  serverCommand: toServerCommand(parsed.transport),
11982
12145
  createdBy: auth.userId ?? null
11983
12146
  });
11984
- return c.json(toResponse3(created, []), 201);
12147
+ return c.json(toResponse4(created, []), 201);
11985
12148
  } catch (err) {
11986
12149
  const message = err.message;
11987
12150
  if (/duplicate key|unique/i.test(message)) {
@@ -11992,7 +12155,7 @@ function createMcpRouter() {
11992
12155
  });
11993
12156
  app.patch("/connections/:id", async (c) => {
11994
12157
  const auth = c.get("auth");
11995
- const denied = requireAdmin2(auth);
12158
+ const denied = requireAdmin3(auth);
11996
12159
  if (denied) return c.json({ error: denied }, 403);
11997
12160
  const body = await c.req.json().catch(() => null);
11998
12161
  if (!body) return c.json({ error: "a body is required" }, 400);
@@ -12018,12 +12181,12 @@ function createMcpRouter() {
12018
12181
  const provider = await getMcpProvider(updated.providerSlug);
12019
12182
  const bindings = bindingsByConnection();
12020
12183
  return c.json(
12021
- toResponse3(updated, bindings.get(updated.id) ?? [], provider?.serverCommand ?? null)
12184
+ toResponse4(updated, bindings.get(updated.id) ?? [], provider?.serverCommand ?? null)
12022
12185
  );
12023
12186
  });
12024
12187
  app.put("/connections/:id/credential", async (c) => {
12025
12188
  const auth = c.get("auth");
12026
- const denied = requireAdmin2(auth);
12189
+ const denied = requireAdmin3(auth);
12027
12190
  if (denied) return c.json({ error: denied }, 403);
12028
12191
  const body = await c.req.json().catch(() => null);
12029
12192
  const raw = body?.apiKey;
@@ -12036,12 +12199,12 @@ function createMcpRouter() {
12036
12199
  const provider = await getMcpProvider(updated.providerSlug);
12037
12200
  const bindings = bindingsByConnection();
12038
12201
  return c.json(
12039
- toResponse3(updated, bindings.get(updated.id) ?? [], provider?.serverCommand ?? null)
12202
+ toResponse4(updated, bindings.get(updated.id) ?? [], provider?.serverCommand ?? null)
12040
12203
  );
12041
12204
  });
12042
12205
  app.put("/connections/:id/channels", async (c) => {
12043
12206
  const auth = c.get("auth");
12044
- const denied = requireAdmin2(auth);
12207
+ const denied = requireAdmin3(auth);
12045
12208
  if (denied) return c.json({ error: denied }, 403);
12046
12209
  const id = c.req.param("id");
12047
12210
  const connection = await getMcpConnection(auth.workspaceId, id);
@@ -12064,11 +12227,11 @@ function createMcpRouter() {
12064
12227
  } catch (err) {
12065
12228
  return c.json({ error: err.message }, 500);
12066
12229
  }
12067
- return c.json(toResponse3(connection, [...wanted]));
12230
+ return c.json(toResponse4(connection, [...wanted]));
12068
12231
  });
12069
12232
  app.delete("/connections/:id", async (c) => {
12070
12233
  const auth = c.get("auth");
12071
- const denied = requireAdmin2(auth);
12234
+ const denied = requireAdmin3(auth);
12072
12235
  if (denied) return c.json({ error: denied }, 403);
12073
12236
  const deleted = await deleteMcpConnection(auth.workspaceId, c.req.param("id"));
12074
12237
  if (!deleted) return c.json({ error: "connection not found" }, 404);
@@ -12078,143 +12241,6 @@ function createMcpRouter() {
12078
12241
  return app;
12079
12242
  }
12080
12243
 
12081
- // src/dashboard/output-styles-routes.ts
12082
- import { Hono as Hono23 } from "hono";
12083
- var BUILT_IN_OUTPUT_STYLES = [
12084
- {
12085
- slug: "default",
12086
- name: "Default",
12087
- description: "Balanced detail: enough context to act on, without padding."
12088
- },
12089
- {
12090
- slug: "concise",
12091
- name: "Concise",
12092
- description: "The shortest answer that fully addresses the question."
12093
- },
12094
- {
12095
- slug: "explanatory",
12096
- name: "Explanatory",
12097
- description: "Walks through the reasoning and trade-offs, not just the result."
12098
- },
12099
- {
12100
- slug: "formal",
12101
- name: "Formal",
12102
- description: "Structured, businesslike phrasing with no casual asides."
12103
- }
12104
- ];
12105
- function requireAdmin3(auth) {
12106
- return auth?.role === "admin" ? null : "admin access required";
12107
- }
12108
- function toResponse4(style) {
12109
- return {
12110
- id: style.id,
12111
- name: style.name,
12112
- description: style.description,
12113
- body: style.body,
12114
- createdAt: style.createdAt,
12115
- updatedAt: style.updatedAt
12116
- };
12117
- }
12118
- function createOutputStylesRouter() {
12119
- const app = new Hono23();
12120
- app.get("/", async (c) => {
12121
- const auth = c.get("auth");
12122
- const denied = requireAdmin3(auth);
12123
- if (denied) return c.json({ error: denied }, 403);
12124
- const [custom, defaultId] = await Promise.all([
12125
- listOutputStyles(auth.workspaceId),
12126
- getWorkspaceOutputStyleDefault(auth.workspaceId)
12127
- ]);
12128
- return c.json({
12129
- builtIn: BUILT_IN_OUTPUT_STYLES,
12130
- custom: custom.map(toResponse4),
12131
- defaultId
12132
- });
12133
- });
12134
- app.post("/", async (c) => {
12135
- const auth = c.get("auth");
12136
- const denied = requireAdmin3(auth);
12137
- if (denied) return c.json({ error: denied }, 403);
12138
- const body = await c.req.json().catch(() => null);
12139
- const name = typeof body?.name === "string" ? body.name.trim() : "";
12140
- if (!name) return c.json({ error: "name is required" }, 400);
12141
- const description = typeof body?.description === "string" ? body.description.trim() : "";
12142
- const content = typeof body?.body === "string" ? body.body : "";
12143
- try {
12144
- const created = await insertOutputStyle({
12145
- workspaceId: auth.workspaceId,
12146
- name,
12147
- description,
12148
- body: content
12149
- });
12150
- return c.json(toResponse4(created), 201);
12151
- } catch (err) {
12152
- const message = err.message;
12153
- if (/duplicate key|unique/i.test(message)) {
12154
- return c.json({ error: `an output style named "${name}" already exists` }, 409);
12155
- }
12156
- return c.json({ error: message }, 500);
12157
- }
12158
- });
12159
- app.put("/default", async (c) => {
12160
- const auth = c.get("auth");
12161
- const denied = requireAdmin3(auth);
12162
- if (denied) return c.json({ error: denied }, 403);
12163
- const body = await c.req.json().catch(() => null);
12164
- const id = typeof body?.id === "string" ? body.id : "";
12165
- if (!id) return c.json({ error: "id is required" }, 400);
12166
- const isBuiltIn = BUILT_IN_OUTPUT_STYLES.some((s) => s.slug === id);
12167
- if (!isBuiltIn && !await getOutputStyle(auth.workspaceId, id)) {
12168
- return c.json({ error: "output style not found" }, 404);
12169
- }
12170
- await setWorkspaceOutputStyleDefault(auth.workspaceId, id);
12171
- return c.json({ defaultId: id });
12172
- });
12173
- app.put("/:id", async (c) => {
12174
- const auth = c.get("auth");
12175
- const denied = requireAdmin3(auth);
12176
- if (denied) return c.json({ error: denied }, 403);
12177
- const body = await c.req.json().catch(() => null);
12178
- if (!body) return c.json({ error: "a body is required" }, 400);
12179
- const patch = {};
12180
- if (body.name !== void 0) {
12181
- const name = typeof body.name === "string" ? body.name.trim() : "";
12182
- if (!name) return c.json({ error: "name cannot be empty" }, 400);
12183
- patch.name = name;
12184
- }
12185
- if (body.description !== void 0) {
12186
- patch.description = typeof body.description === "string" ? body.description.trim() : "";
12187
- }
12188
- if (body.body !== void 0) {
12189
- patch.body = typeof body.body === "string" ? body.body : "";
12190
- }
12191
- if (!Object.keys(patch).length) return c.json({ error: "nothing to update" }, 400);
12192
- try {
12193
- const updated = await updateOutputStyle(auth.workspaceId, c.req.param("id"), patch);
12194
- if (!updated) return c.json({ error: "output style not found" }, 404);
12195
- return c.json(toResponse4(updated));
12196
- } catch (err) {
12197
- const message = err.message;
12198
- if (/duplicate key|unique/i.test(message)) {
12199
- return c.json({ error: `an output style named "${patch.name}" already exists` }, 409);
12200
- }
12201
- return c.json({ error: message }, 500);
12202
- }
12203
- });
12204
- app.delete("/:id", async (c) => {
12205
- const auth = c.get("auth");
12206
- const denied = requireAdmin3(auth);
12207
- if (denied) return c.json({ error: denied }, 403);
12208
- const id = c.req.param("id");
12209
- const currentDefault = await getWorkspaceOutputStyleDefault(auth.workspaceId);
12210
- const deleted = await deleteOutputStyle(auth.workspaceId, id);
12211
- if (!deleted) return c.json({ error: "output style not found" }, 404);
12212
- if (currentDefault === id) await setWorkspaceOutputStyleDefault(auth.workspaceId, "default");
12213
- return c.json({ ok: true });
12214
- });
12215
- return app;
12216
- }
12217
-
12218
12244
  // src/store/usage-query.ts
12219
12245
  var COLUMNS = "ran_at, channel_id, thread_ts, user_id, source, model, provider, label, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, cost_usd, num_turns, is_error";
12220
12246
  var EMPTY_TOTALS = {
@@ -12522,14 +12548,320 @@ function createChannelMembersRouter() {
12522
12548
  return router;
12523
12549
  }
12524
12550
 
12551
+ // src/dashboard/onboarding-routes.ts
12552
+ import { Hono as Hono25 } from "hono";
12553
+
12554
+ // src/dashboard/onboarding-scan.ts
12555
+ import { execFileSync as execFileSync2 } from "child_process";
12556
+ import {
12557
+ existsSync as existsSync22,
12558
+ lstatSync as lstatSync2,
12559
+ mkdirSync as mkdirSync6,
12560
+ readdirSync as readdirSync4,
12561
+ readFileSync as readFileSync11,
12562
+ statSync as statSync8,
12563
+ symlinkSync
12564
+ } from "fs";
12565
+ import { homedir as homedir7 } from "os";
12566
+ import { basename as basename5, join as join16, resolve as resolve6 } from "path";
12567
+ var MAX_JSONL_HEAD_BYTES = 64 * 1024;
12568
+ var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
12569
+ function flyspaceRoot() {
12570
+ return process.env["FLYSPACE_ROOT"] || join16(homedir7(), "Flyspace");
12571
+ }
12572
+ function onPath(cmd) {
12573
+ try {
12574
+ execFileSync2(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
12575
+ return true;
12576
+ } catch {
12577
+ return false;
12578
+ }
12579
+ }
12580
+ function claudeDir() {
12581
+ return process.env["CLAUDE_CONFIG_DIR"] || join16(homedir7(), ".claude");
12582
+ }
12583
+ function codexDir() {
12584
+ return process.env["CODEX_HOME"] || join16(homedir7(), ".codex");
12585
+ }
12586
+ function claudeLoggedIn() {
12587
+ if (process.env["ANTHROPIC_API_KEY"]) return true;
12588
+ if (existsSync22(join16(claudeDir(), ".credentials.json"))) return true;
12589
+ try {
12590
+ const raw = JSON.parse(readFileSync11(join16(homedir7(), ".claude.json"), "utf-8"));
12591
+ if (raw.oauthAccount) return true;
12592
+ } catch {
12593
+ }
12594
+ if (process.platform !== "darwin") return false;
12595
+ try {
12596
+ execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
12597
+ stdio: "ignore"
12598
+ });
12599
+ return true;
12600
+ } catch {
12601
+ return false;
12602
+ }
12603
+ }
12604
+ function codexLoggedIn() {
12605
+ return !!process.env["OPENAI_API_KEY"] || existsSync22(join16(codexDir(), "auth.json"));
12606
+ }
12607
+ function jsonlFiles(dir, depth) {
12608
+ const out = [];
12609
+ let entries2;
12610
+ try {
12611
+ entries2 = readdirSync4(dir, { withFileTypes: true });
12612
+ } catch {
12613
+ return out;
12614
+ }
12615
+ for (const e of entries2) {
12616
+ const full = join16(dir, e.name);
12617
+ if (e.isDirectory()) {
12618
+ if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
12619
+ } else if (e.name.endsWith(".jsonl")) {
12620
+ try {
12621
+ out.push({ file: full, mtime: statSync8(full).mtimeMs });
12622
+ } catch {
12623
+ }
12624
+ }
12625
+ }
12626
+ return out;
12627
+ }
12628
+ function cwdFromTranscript(file) {
12629
+ let head;
12630
+ try {
12631
+ head = readFileSync11(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
12632
+ } catch {
12633
+ return null;
12634
+ }
12635
+ for (const line of head.split("\n")) {
12636
+ if (!line.includes('"cwd"')) continue;
12637
+ try {
12638
+ const obj = JSON.parse(line);
12639
+ const cwd = obj.cwd ?? obj.payload?.cwd;
12640
+ if (typeof cwd === "string" && cwd) return cwd;
12641
+ } catch {
12642
+ }
12643
+ }
12644
+ return null;
12645
+ }
12646
+ function codexTrustedPaths() {
12647
+ try {
12648
+ const toml = readFileSync11(join16(codexDir(), "config.toml"), "utf-8");
12649
+ return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
12650
+ } catch {
12651
+ return [];
12652
+ }
12653
+ }
12654
+ function isExcluded(path) {
12655
+ const home = homedir7();
12656
+ if (path === home || path === "/") return true;
12657
+ if (path === "/tmp" || path.startsWith("/tmp/")) return true;
12658
+ if (path === "/private/tmp" || path.startsWith("/private/tmp/")) return true;
12659
+ const root = flyspaceRoot();
12660
+ if (path === root || path.startsWith(`${root}/`)) return true;
12661
+ try {
12662
+ return !statSync8(path).isDirectory();
12663
+ } catch {
12664
+ return true;
12665
+ }
12666
+ }
12667
+ function gitRepos() {
12668
+ const home = homedir7();
12669
+ const found = [];
12670
+ for (const rootName of GIT_SCAN_ROOTS) {
12671
+ const root = join16(home, rootName);
12672
+ let entries2;
12673
+ try {
12674
+ entries2 = readdirSync4(root, { withFileTypes: true });
12675
+ } catch {
12676
+ continue;
12677
+ }
12678
+ for (const e of entries2) {
12679
+ if (e.isDirectory() && existsSync22(join16(root, e.name, ".git"))) found.push(join16(root, e.name));
12680
+ }
12681
+ }
12682
+ return found;
12683
+ }
12684
+ function detectRunners(history = { claude: false, codex: false }) {
12685
+ return [
12686
+ {
12687
+ id: "claude",
12688
+ installed: onPath("claude"),
12689
+ loggedIn: claudeLoggedIn(),
12690
+ hasHistory: history.claude
12691
+ },
12692
+ {
12693
+ id: "codex",
12694
+ installed: onPath("codex"),
12695
+ loggedIn: codexLoggedIn(),
12696
+ hasHistory: history.codex
12697
+ }
12698
+ ];
12699
+ }
12700
+ function scanOnboarding() {
12701
+ const byPath = /* @__PURE__ */ new Map();
12702
+ const history = { claude: false, codex: false };
12703
+ const add = (rawPath, source, mtime, sessions, trusted = false) => {
12704
+ const path = resolve6(rawPath);
12705
+ if (isExcluded(path)) return;
12706
+ const existing = byPath.get(path);
12707
+ if (existing) {
12708
+ if (!existing.sources.includes(source)) existing.sources.push(source);
12709
+ existing.lastUsed = Math.max(existing.lastUsed, mtime);
12710
+ existing.sessions += sessions;
12711
+ existing.trusted ||= trusted;
12712
+ } else {
12713
+ byPath.set(path, {
12714
+ path,
12715
+ name: basename5(path),
12716
+ sources: [source],
12717
+ lastUsed: mtime,
12718
+ sessions,
12719
+ trusted
12720
+ });
12721
+ }
12722
+ };
12723
+ const claudeProjects = join16(claudeDir(), "projects");
12724
+ try {
12725
+ for (const e of readdirSync4(claudeProjects, { withFileTypes: true })) {
12726
+ if (!e.isDirectory()) continue;
12727
+ const files = jsonlFiles(join16(claudeProjects, e.name), 0);
12728
+ if (!files.length) continue;
12729
+ history.claude = true;
12730
+ files.sort((a, b) => b.mtime - a.mtime);
12731
+ let cwd = null;
12732
+ for (const f of files.slice(0, 3)) {
12733
+ cwd = cwdFromTranscript(f.file);
12734
+ if (cwd) break;
12735
+ }
12736
+ if (cwd) add(cwd, "claude", files[0].mtime, files.length);
12737
+ }
12738
+ } catch {
12739
+ }
12740
+ const codexSessions = jsonlFiles(join16(codexDir(), "sessions"), 4);
12741
+ if (codexSessions.length) history.codex = true;
12742
+ const perCwd = /* @__PURE__ */ new Map();
12743
+ for (const f of codexSessions) {
12744
+ const cwd = cwdFromTranscript(f.file);
12745
+ if (!cwd) continue;
12746
+ const cur = perCwd.get(cwd) ?? { mtime: 0, count: 0 };
12747
+ cur.mtime = Math.max(cur.mtime, f.mtime);
12748
+ cur.count += 1;
12749
+ perCwd.set(cwd, cur);
12750
+ }
12751
+ for (const [cwd, v] of perCwd) add(cwd, "codex", v.mtime, v.count);
12752
+ for (const p of codexTrustedPaths()) add(p, "codex", 0, 0, true);
12753
+ let projects = [...byPath.values()];
12754
+ let fromGitScan = false;
12755
+ if (!projects.length) {
12756
+ fromGitScan = true;
12757
+ for (const repo of gitRepos()) {
12758
+ let mtime = 0;
12759
+ try {
12760
+ mtime = statSync8(repo).mtimeMs;
12761
+ } catch {
12762
+ }
12763
+ add(repo, "git", mtime, 0);
12764
+ }
12765
+ projects = [...byPath.values()];
12766
+ }
12767
+ projects.sort(
12768
+ (a, b) => Number(b.sources.length > 1) - Number(a.sources.length > 1) || Number(b.trusted) - Number(a.trusted) || b.lastUsed - a.lastUsed || b.sessions - a.sessions
12769
+ );
12770
+ return {
12771
+ root: flyspaceRoot(),
12772
+ runners: detectRunners(history),
12773
+ projects,
12774
+ fromGitScan
12775
+ };
12776
+ }
12777
+ function uniqueChannelName(raw, taken) {
12778
+ const base = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
12779
+ let name = base;
12780
+ for (let i = 2; taken.has(name); i += 1) name = `${base}-${i}`;
12781
+ taken.add(name);
12782
+ return name;
12783
+ }
12784
+ function makeLink(target, linkPath) {
12785
+ symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
12786
+ }
12787
+ function linkProjectFolders(slug, items) {
12788
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
12789
+ const dir = join16(flyspaceRoot(), slug);
12790
+ mkdirSync6(dir, { recursive: true });
12791
+ const results = [];
12792
+ for (const item of items) {
12793
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
12794
+ const dest = join16(dir, item.name);
12795
+ let exists = true;
12796
+ try {
12797
+ lstatSync2(dest);
12798
+ } catch {
12799
+ exists = false;
12800
+ }
12801
+ if (!exists) {
12802
+ if (item.path) {
12803
+ const target = resolve6(item.path);
12804
+ if (!statSync8(target).isDirectory()) throw new Error(`not a directory: ${target}`);
12805
+ makeLink(target, dest);
12806
+ } else {
12807
+ mkdirSync6(dest, { recursive: true });
12808
+ }
12809
+ }
12810
+ results.push({ name: item.name, cwd: `${slug}/${item.name}` });
12811
+ }
12812
+ return results;
12813
+ }
12814
+
12815
+ // src/dashboard/onboarding-routes.ts
12816
+ var MAX_FOLDERS = 20;
12817
+ function createOnboardingRouter(agent) {
12818
+ const app = new Hono25();
12819
+ app.get("/scan", async (c) => {
12820
+ const ctx = await resolveAuthContext(c);
12821
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
12822
+ const scan = await agent.scanOnboarding?.();
12823
+ if (!scan) return c.json({ error: "host offline" }, 503);
12824
+ return c.json(scan);
12825
+ });
12826
+ app.post("/channels", async (c) => {
12827
+ const ctx = await resolveAuthContext(c);
12828
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
12829
+ const body = await c.req.json().catch(() => ({}));
12830
+ const runner = body["agent"];
12831
+ const folders = Array.isArray(body["folders"]) ? body["folders"].filter((f) => typeof f === "string" && f.length > 0) : [];
12832
+ if (folders.length > MAX_FOLDERS) return c.json({ error: `at most ${MAX_FOLDERS} folders` }, 400);
12833
+ const scan = await agent.scanOnboarding?.();
12834
+ if (!scan) return c.json({ error: "host offline" }, 503);
12835
+ const offered = new Map(scan.projects.map((p) => [p.path, p]));
12836
+ const unknown = folders.find((f) => !offered.has(f));
12837
+ if (unknown) return c.json({ error: "folder was not offered by the scan" }, 400);
12838
+ const taken = new Set(getProfileChannelRegistry().map((ch) => ch.name));
12839
+ const items = [];
12840
+ if (!taken.has("general")) {
12841
+ taken.add("general");
12842
+ items.push({ name: "general" });
12843
+ }
12844
+ for (const path of folders) items.push({ name: uniqueChannelName(offered.get(path).name, taken), path });
12845
+ const linked = await agent.linkProjectFolders?.(getProfile(), items);
12846
+ if (!linked) return c.json({ error: "host offline" }, 503);
12847
+ if (runner === "claude" || runner === "codex") {
12848
+ await agent.saveLocalDashboardSettings?.({ providers: [{ runner }] });
12849
+ }
12850
+ const created = [];
12851
+ for (const l of linked) created.push(await createChannel({ name: l.name, cwd: l.cwd }));
12852
+ return c.json({ channels: created.map((ch) => ({ id: ch.id, name: ch.name })) }, 201);
12853
+ });
12854
+ return app;
12855
+ }
12856
+
12525
12857
  // src/dashboard/host-status-signal.ts
12526
12858
  var HOST_STATUS_CHANNEL = "host-status";
12527
12859
  var channelReady = null;
12528
12860
  function getHostStatusChannel() {
12529
- channelReady ??= new Promise((resolve7) => {
12861
+ channelReady ??= new Promise((resolve8) => {
12530
12862
  const channel = getSupabaseClient().channel(HOST_STATUS_CHANNEL);
12531
12863
  channel.subscribe((state) => {
12532
- if (state === "SUBSCRIBED") resolve7(channel);
12864
+ if (state === "SUBSCRIBED") resolve8(channel);
12533
12865
  });
12534
12866
  });
12535
12867
  return channelReady;
@@ -12630,10 +12962,10 @@ async function resolveThreadLinks(sessions) {
12630
12962
  function registerDataRoutes(api, agent = {}) {
12631
12963
  api.get("/profile", (c) => c.json({ profile: getProfile() }));
12632
12964
  api.get("/host/status", async (c) => {
12633
- const online = agent.isHostOnline?.() ?? false;
12965
+ const online = agent.isHostOnline?.(getProfile()) ?? false;
12634
12966
  if (!online) {
12635
- const failure = agent.getHostOfflineReason?.() ?? null;
12636
- return c.json({ online: false, reason: failure?.reason ?? null, since: failure?.since ?? null });
12967
+ const failure2 = agent.getHostOfflineReason?.() ?? null;
12968
+ return c.json({ online: false, reason: failure2?.reason ?? null, since: failure2?.since ?? null });
12637
12969
  }
12638
12970
  const auth = c.get("auth");
12639
12971
  const paused = await getAgentPaused(auth.workspaceId);
@@ -12678,6 +13010,7 @@ function registerDataRoutes(api, agent = {}) {
12678
13010
  return c.json(await buildChannelUsageReport(c.req.param("id"), days, channelLabel));
12679
13011
  });
12680
13012
  api.route("/channels", createDocShareWriteRouter(agent));
13013
+ api.route("/onboarding", createOnboardingRouter(agent));
12681
13014
  api.route("/channels", createChannelMembersRouter());
12682
13015
  api.route("/docs", createDocShareListRouter());
12683
13016
  api.get("/users", (c) => c.json({ users: listActiveUsers() }));
@@ -12690,7 +13023,7 @@ function registerDataRoutes(api, agent = {}) {
12690
13023
  runs.set(name, { lastRun: run3.lastRunAt.toISOString(), lastStatus: run3.lastStatus });
12691
13024
  }
12692
13025
  } catch {
12693
- if (existsSync22(paths.DB_FILE)) {
13026
+ if (existsSync23(paths.DB_FILE)) {
12694
13027
  const runStore = new AutomationRunStore(paths.DB_FILE);
12695
13028
  try {
12696
13029
  for (const run3 of runStore.listLastRuns()) {
@@ -12752,7 +13085,7 @@ function registerDataRoutes(api, agent = {}) {
12752
13085
  const runs = (await recentRunsForWorkspace(getProfile(), 200)).map(toResponse5);
12753
13086
  return c.json({ runs });
12754
13087
  } catch {
12755
- if (!existsSync22(paths.DB_FILE)) return c.json({ runs: [] });
13088
+ if (!existsSync23(paths.DB_FILE)) return c.json({ runs: [] });
12756
13089
  const runStore = new AutomationRunStore(paths.DB_FILE);
12757
13090
  try {
12758
13091
  const runs = runStore.listRuns(200).map(toResponse5);
@@ -12799,7 +13132,6 @@ function registerDataRoutes(api, agent = {}) {
12799
13132
  });
12800
13133
  api.route("/mcp", createMcpOAuthRouter());
12801
13134
  api.route("/mcp", createMcpRouter());
12802
- api.route("/output-styles", createOutputStylesRouter());
12803
13135
  api.get("/agent-pause", async (c) => {
12804
13136
  const auth = c.get("auth");
12805
13137
  return c.json({ paused: await getAgentPaused(auth.workspaceId) });
@@ -12868,7 +13200,7 @@ function registerDataRoutes(api, agent = {}) {
12868
13200
  } catch {
12869
13201
  latest = /* @__PURE__ */ new Map();
12870
13202
  totals = /* @__PURE__ */ new Map();
12871
- if (existsSync22(paths.DB_FILE)) {
13203
+ if (existsSync23(paths.DB_FILE)) {
12872
13204
  const usageStore = new UsageStore(paths.DB_FILE);
12873
13205
  try {
12874
13206
  latest = usageStore.latestByThread(threadTsList);
@@ -12923,28 +13255,28 @@ function registerDataRoutes(api, agent = {}) {
12923
13255
  // src/dashboard/device-auth-routes.ts
12924
13256
  import { verify } from "crypto";
12925
13257
  import { getConnInfo } from "@hono/node-server/conninfo";
12926
- import { Hono as Hono25 } from "hono";
13258
+ import { Hono as Hono26 } from "hono";
12927
13259
 
12928
13260
  // src/dashboard/agent-bridge.ts
12929
13261
  import { randomBytes as randomBytes7, randomUUID as randomUUID3, sign as signRaw, verify as verifyRaw } from "crypto";
12930
- import { existsSync as existsSync25, readFileSync as readFileSync14 } from "fs";
13262
+ import { existsSync as existsSync26, readFileSync as readFileSync15 } from "fs";
12931
13263
  import dotenv from "dotenv";
12932
13264
  import { WebSocket, WebSocketServer } from "ws";
12933
13265
 
12934
13266
  // src/dashboard/host-identity.ts
12935
13267
  import { createPrivateKey, createPublicKey, generateKeyPairSync } from "crypto";
12936
- import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
12937
- import { dirname as dirname5, join as join16 } from "path";
13268
+ import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
13269
+ import { dirname as dirname5, join as join17 } from "path";
12938
13270
  function hostPrivateKeyPath() {
12939
- return process.env["FLYSPACE_HOST_KEY_PATH"] || join16(paths.BASE_CONFIG_DIR, "host.key");
13271
+ return process.env["FLYSPACE_HOST_KEY_PATH"] || join17(paths.BASE_CONFIG_DIR, "host.key");
12940
13272
  }
12941
13273
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
12942
- if (!existsSync23(path)) {
13274
+ if (!existsSync24(path)) {
12943
13275
  throw new Error(
12944
13276
  `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.`
12945
13277
  );
12946
13278
  }
12947
- return createPrivateKey(readFileSync11(path, "utf8"));
13279
+ return createPrivateKey(readFileSync12(path, "utf8"));
12948
13280
  }
12949
13281
  function loadHostPublicKey() {
12950
13282
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -13146,8 +13478,8 @@ async function userEmail(userId) {
13146
13478
 
13147
13479
  // src/auth/device-auth.ts
13148
13480
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey3, generateKeyPairSync as generateKeyPairSync2, sign } from "crypto";
13149
- import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync12, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
13150
- import { dirname as dirname6, join as join17 } from "path";
13481
+ import { existsSync as existsSync25, mkdirSync as mkdirSync8, readFileSync as readFileSync13, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
13482
+ import { dirname as dirname6, join as join18 } from "path";
13151
13483
  var DEFAULT_API_URL = "https://app.flyspace.ai";
13152
13484
  var EXPIRY_SKEW_MS = 6e4;
13153
13485
  var AuthApiError = class extends Error {
@@ -13161,35 +13493,35 @@ function apiUrl() {
13161
13493
  return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
13162
13494
  }
13163
13495
  function deviceKeyPath() {
13164
- return join17(paths.BASE_CONFIG_DIR, "device.key");
13496
+ return join18(paths.BASE_CONFIG_DIR, "device.key");
13165
13497
  }
13166
13498
  function credentialsPath() {
13167
- return join17(paths.BASE_CONFIG_DIR, "credentials.json");
13499
+ return join18(paths.BASE_CONFIG_DIR, "credentials.json");
13168
13500
  }
13169
13501
  function ensureDeviceKey(path = deviceKeyPath()) {
13170
13502
  let created = false;
13171
- if (!existsSync24(path)) {
13503
+ if (!existsSync25(path)) {
13172
13504
  const { privateKey: privateKey2 } = generateKeyPairSync2("ed25519");
13173
- mkdirSync7(dirname6(path), { recursive: true });
13505
+ mkdirSync8(dirname6(path), { recursive: true });
13174
13506
  writeFileSync8(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
13175
13507
  created = true;
13176
13508
  }
13177
- const privateKey = createPrivateKey2(readFileSync12(path, "utf8"));
13509
+ const privateKey = createPrivateKey2(readFileSync13(path, "utf8"));
13178
13510
  const publicKeyBase64 = createPublicKey3(privateKey).export({ type: "spki", format: "der" }).toString(
13179
13511
  "base64"
13180
13512
  );
13181
13513
  return { privateKey, publicKeyBase64, created };
13182
13514
  }
13183
13515
  function loadCredentials(path = credentialsPath()) {
13184
- if (!existsSync24(path)) return null;
13516
+ if (!existsSync25(path)) return null;
13185
13517
  try {
13186
- return JSON.parse(readFileSync12(path, "utf8"));
13518
+ return JSON.parse(readFileSync13(path, "utf8"));
13187
13519
  } catch {
13188
13520
  return null;
13189
13521
  }
13190
13522
  }
13191
13523
  function saveCredentials(creds, path = credentialsPath()) {
13192
- mkdirSync7(dirname6(path), { recursive: true });
13524
+ mkdirSync8(dirname6(path), { recursive: true });
13193
13525
  writeFileSync8(path, `${JSON.stringify(creds, null, 2)}
13194
13526
  `, { mode: 384 });
13195
13527
  }
@@ -13313,7 +13645,7 @@ async function getAccessToken() {
13313
13645
 
13314
13646
  // src/dashboard/doc-share-publisher.ts
13315
13647
  import { randomBytes as randomBytes6 } from "crypto";
13316
- import { readFileSync as readFileSync13, statSync as statSync8 } from "fs";
13648
+ import { readFileSync as readFileSync14, statSync as statSync9 } from "fs";
13317
13649
  import { extname as extname2, posix } from "path";
13318
13650
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
13319
13651
 
@@ -13493,13 +13825,13 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
13493
13825
  if (!kind) return { status: "unsupported" };
13494
13826
  let stat;
13495
13827
  try {
13496
- stat = statSync8(abs);
13828
+ stat = statSync9(abs);
13497
13829
  } catch {
13498
13830
  return { status: "not-found" };
13499
13831
  }
13500
13832
  if (!stat.isFile()) return { status: "not-found" };
13501
13833
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
13502
- const source = readFileSync13(abs, "utf8");
13834
+ const source = readFileSync14(abs, "utf8");
13503
13835
  const title = extractTitle(abs, kind === "html" ? "html" : "md");
13504
13836
  const docDir = sourcePath.includes("/") ? sourcePath.slice(0, sourcePath.lastIndexOf("/")) : "";
13505
13837
  const client2 = getR2Client();
@@ -13569,8 +13901,8 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
13569
13901
  const budget = Math.min(MAX_INLINE_BYTES, MAX_TOTAL_INLINE_BYTES - inlinedBytes);
13570
13902
  let text = null;
13571
13903
  try {
13572
- const size = statSync8(docAbs).size;
13573
- if (size <= budget) text = readFileSync13(docAbs, "utf8");
13904
+ const size = statSync9(docAbs).size;
13905
+ if (size <= budget) text = readFileSync14(docAbs, "utf8");
13574
13906
  } catch {
13575
13907
  continue;
13576
13908
  }
@@ -13586,8 +13918,8 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
13586
13918
  if (!resolved2) continue;
13587
13919
  let body;
13588
13920
  try {
13589
- if (statSync8(resolved2.abs).size > MAX_ASSET_BYTES) continue;
13590
- body = readFileSync13(resolved2.abs);
13921
+ if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
13922
+ body = readFileSync14(resolved2.abs);
13591
13923
  } catch {
13592
13924
  continue;
13593
13925
  }
@@ -13661,11 +13993,15 @@ async function publishDocShare(input2) {
13661
13993
  // src/dashboard/agent-bridge.ts
13662
13994
  var HOST_SOCKET_PATH = "/internal/host-socket";
13663
13995
  var DEFAULT_DATA_API_PORT = 4757;
13996
+ var HOSTED_DATA_SOCKET_ORIGIN = "wss://flyspace-api.fly.dev";
13664
13997
  function dataSocketUrl() {
13665
- return process.env["FLYSPACE_DATA_SOCKET_URL"] || `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
13998
+ const explicit = process.env["FLYSPACE_DATA_SOCKET_URL"];
13999
+ if (explicit) return explicit;
14000
+ if (!process.env["FLYSPACE_DATA_API_PORT"] && loadCredentials()) return `${HOSTED_DATA_SOCKET_ORIGIN}${HOST_SOCKET_PATH}`;
14001
+ return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
13666
14002
  }
13667
14003
  function listLocalSessions() {
13668
- if (!existsSync25(paths.DB_FILE)) return [];
14004
+ if (!existsSync26(paths.DB_FILE)) return [];
13669
14005
  const store = new SessionStore(paths.DB_FILE);
13670
14006
  try {
13671
14007
  return store.listSessions();
@@ -13691,6 +14027,8 @@ function localAgentCallbacks(chatAgent) {
13691
14027
  saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
13692
14028
  writeSkill: (ref, write) => writeSkillAndSync(ref, write),
13693
14029
  listSessions: () => listLocalSessions(),
14030
+ scanOnboarding: () => scanOnboarding(),
14031
+ linkProjectFolders: (slug, items) => linkProjectFolders(slug, items),
13694
14032
  isHostOnline: () => true
13695
14033
  };
13696
14034
  }
@@ -13699,7 +14037,7 @@ function buildHostConfigSnapshot() {
13699
14037
  const snapshot2 = {};
13700
14038
  for (const profile of listProfiles()) {
13701
14039
  snapshot2[profile] = runWithProfile(profile, () => ({
13702
- env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync14(paths.CONFIG_FILE, "utf-8")) : {},
14040
+ env: existsSync26(paths.CONFIG_FILE) ? dotenv.parse(readFileSync15(paths.CONFIG_FILE, "utf-8")) : {},
13703
14041
  agentModel: loadDashboardSettings().agent
13704
14042
  }));
13705
14043
  }
@@ -13715,13 +14053,24 @@ var AUTH_TIMEOUT_MS = 5e3;
13715
14053
  var HEARTBEAT_INTERVAL_MS = 15e3;
13716
14054
  var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
13717
14055
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13718
- var activeHost = null;
13719
- var activeHostScope = null;
14056
+ var hosts = /* @__PURE__ */ new Map();
13720
14057
  var pending = /* @__PURE__ */ new Map();
13721
14058
  var hostStatusListeners = /* @__PURE__ */ new Set();
13722
14059
  var lastAuthFailure = null;
13723
- function isHostConnected() {
13724
- return activeHost !== null && activeHost.readyState === WebSocket.OPEN;
14060
+ function hostFor(profile) {
14061
+ let legacy = null;
14062
+ let scoped = null;
14063
+ for (const [ws, scope] of hosts) {
14064
+ if (ws.readyState !== WebSocket.OPEN) continue;
14065
+ if (scope.workspaces === null) legacy = ws;
14066
+ else if (scope.workspaces.includes(profile)) scoped = ws;
14067
+ }
14068
+ return scoped ?? legacy;
14069
+ }
14070
+ function isHostConnected(profile) {
14071
+ if (profile) return hostFor(profile) !== null;
14072
+ for (const ws of hosts.keys()) if (ws.readyState === WebSocket.OPEN) return true;
14073
+ return false;
13725
14074
  }
13726
14075
  function getHostOfflineReason() {
13727
14076
  return lastAuthFailure;
@@ -13730,11 +14079,26 @@ function onHostStatusChange(listener) {
13730
14079
  hostStatusListeners.add(listener);
13731
14080
  return () => hostStatusListeners.delete(listener);
13732
14081
  }
13733
- function setActiveHost(ws, scope = null) {
13734
- const was = activeHost !== null;
13735
- activeHost = ws;
13736
- activeHostScope = ws ? scope : null;
13737
- if (was !== (ws !== null)) for (const l of hostStatusListeners) l(ws !== null);
14082
+ function notifyHostStatus() {
14083
+ const online = hosts.size > 0;
14084
+ for (const l of hostStatusListeners) l(online);
14085
+ }
14086
+ function addHost(ws, scope) {
14087
+ hosts.set(ws, scope);
14088
+ notifyHostStatus();
14089
+ }
14090
+ function removeHost(ws) {
14091
+ if (hosts.delete(ws)) notifyHostStatus();
14092
+ }
14093
+ function supersedeSameIdentity(scope, except) {
14094
+ for (const [ws, other] of hosts) {
14095
+ if (ws === except) continue;
14096
+ const same = scope.deviceId ? other.deviceId === scope.deviceId : other.workspaces === null;
14097
+ if (!same) continue;
14098
+ console.log("[host-socket] new Host connection replacing the previous one");
14099
+ removeHost(ws);
14100
+ ws.close(1e3, "superseded");
14101
+ }
13738
14102
  }
13739
14103
  async function markAgentConnected(slugs) {
13740
14104
  for (const slug of slugs) {
@@ -13747,20 +14111,27 @@ async function markAgentConnected(slugs) {
13747
14111
  }
13748
14112
  }
13749
14113
  function disconnectHostDevice(deviceId) {
13750
- if (activeHost && activeHostScope?.deviceId === deviceId) {
14114
+ for (const [ws, scope] of hosts) {
14115
+ if (scope.deviceId !== deviceId) continue;
13751
14116
  console.log(`[host-socket] closing connection for revoked device ${deviceId}`);
13752
- activeHost.close(1008, "device revoked");
13753
- setActiveHost(null);
14117
+ removeHost(ws);
14118
+ ws.close(1008, "device revoked");
13754
14119
  }
13755
14120
  }
13756
- async function revalidateActiveDeviceScope() {
13757
- if (!activeHost || activeHostScope?.authPath !== "device" || !activeHostScope.deviceId) return;
13758
- const device = await getDevice(activeHostScope.deviceId);
13759
- if (!device || device.revokedAt) {
13760
- disconnectHostDevice(activeHostScope.deviceId);
13761
- return;
14121
+ async function revalidateDeviceScopes() {
14122
+ const deviceIds = /* @__PURE__ */ new Set();
14123
+ for (const scope of hosts.values()) {
14124
+ if (scope.authPath === "device" && scope.deviceId) deviceIds.add(scope.deviceId);
14125
+ }
14126
+ for (const deviceId of deviceIds) {
14127
+ const device = await getDevice(deviceId);
14128
+ if (!device || device.revokedAt) {
14129
+ disconnectHostDevice(deviceId);
14130
+ continue;
14131
+ }
14132
+ const workspaces = (await listDeviceWorkspaces(device)).map((w) => w.slug);
14133
+ for (const scope of hosts.values()) if (scope.deviceId === deviceId) scope.workspaces = workspaces;
13762
14134
  }
13763
- activeHostScope.workspaces = (await listDeviceWorkspaces(device)).map((w) => w.slug);
13764
14135
  }
13765
14136
  function attachHostSocket(server) {
13766
14137
  const wss = new WebSocketServer({ server, path: HOST_SOCKET_PATH });
@@ -13775,7 +14146,7 @@ function attachHostSocket(server) {
13775
14146
  alive.delete(ws);
13776
14147
  ws.ping();
13777
14148
  }
13778
- void revalidateActiveDeviceScope();
14149
+ void revalidateDeviceScopes();
13779
14150
  }, HEARTBEAT_INTERVAL_MS);
13780
14151
  heartbeat.unref();
13781
14152
  server.on("close", () => clearInterval(heartbeat));
@@ -13802,12 +14173,9 @@ function attachHostSocket(server) {
13802
14173
  ws.close(1008, "bad signature");
13803
14174
  return;
13804
14175
  }
13805
- if (activeHost && activeHost !== ws && activeHost.readyState === WebSocket.OPEN) {
13806
- console.log("[host-socket] new Host connection replacing the previous one");
13807
- activeHost.close(1e3, "superseded");
13808
- }
14176
+ supersedeSameIdentity(scope, ws);
13809
14177
  lastAuthFailure = null;
13810
- setActiveHost(ws, scope);
14178
+ addHost(ws, scope);
13811
14179
  send(ws, { type: "auth-ok" });
13812
14180
  console.log(`[host-socket] Host authenticated (authPath: ${scope.authPath})`);
13813
14181
  if (scope.workspaces) void markAgentConnected(scope.workspaces);
@@ -13827,34 +14195,32 @@ function attachHostSocket(server) {
13827
14195
  });
13828
14196
  ws.on("close", () => {
13829
14197
  clearTimeout(authTimer);
13830
- if (activeHost === ws) setActiveHost(null);
14198
+ removeHost(ws);
13831
14199
  });
13832
14200
  ws.on("error", (err) => console.error("[host-socket] connection error:", err.message));
13833
14201
  });
13834
14202
  return wss;
13835
14203
  }
13836
14204
  function callHost(method, args) {
13837
- if (!activeHost || activeHost.readyState !== WebSocket.OPEN) {
13838
- console.error(`[host-socket] ${method} -> no Host connected`);
13839
- return Promise.resolve(null);
13840
- }
13841
14205
  const profile = getProfile();
13842
- if (activeHostScope && activeHostScope.workspaces !== null && !activeHostScope.workspaces.includes(profile)) {
13843
- console.error(`[host-socket] ${method} -> Host not scoped to workspace "${profile}"`);
14206
+ const ws = hostFor(profile);
14207
+ if (!ws) {
14208
+ console.error(
14209
+ isHostConnected() ? `[host-socket] ${method} -> not scoped to workspace "${profile}"` : `[host-socket] ${method} -> no Host connected`
14210
+ );
13844
14211
  return Promise.resolve(null);
13845
14212
  }
13846
14213
  const id = randomUUID3();
13847
- const ws = activeHost;
13848
- return new Promise((resolve7) => {
14214
+ return new Promise((resolve8) => {
13849
14215
  const timer2 = setTimeout(() => {
13850
14216
  pending.delete(id);
13851
14217
  console.error(`[host-socket] ${method} -> timed out`);
13852
- resolve7(null);
14218
+ resolve8(null);
13853
14219
  }, CALL_TIMEOUT_MS);
13854
14220
  pending.set(id, {
13855
14221
  resolve: (value) => {
13856
14222
  clearTimeout(timer2);
13857
- resolve7(value);
14223
+ resolve8(value);
13858
14224
  }
13859
14225
  });
13860
14226
  send(ws, { type: "call", id, method, profile, args });
@@ -13905,7 +14271,11 @@ function remoteAgentCallbacks() {
13905
14271
  // `null` (no Host connected) is passed through rather than defaulting to
13906
14272
  // `[]` — see listSessions's doc comment on AgentCallbacks.
13907
14273
  listSessions: () => callHost("listSessions", []),
13908
- isHostOnline: isHostConnected,
14274
+ // Both need this machine's disk and CLI history, so `null` (no Host)
14275
+ // surfaces as a 503 rather than an empty scan or a fake success.
14276
+ scanOnboarding: () => callHost("scanOnboarding", []),
14277
+ linkProjectFolders: (slug, items) => callHost("linkProjectFolders", [slug, items]),
14278
+ isHostOnline: (profile) => isHostConnected(profile),
13909
14279
  getHostOfflineReason
13910
14280
  };
13911
14281
  }
@@ -14084,7 +14454,7 @@ async function bearerDevice(c) {
14084
14454
  return token ? deviceForAccessToken(token) : null;
14085
14455
  }
14086
14456
  function createDeviceAuthRouter() {
14087
- const app = new Hono25();
14457
+ const app = new Hono26();
14088
14458
  app.post("/code", async (c) => {
14089
14459
  const body = await readBody(c);
14090
14460
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -14234,7 +14604,7 @@ function createDeviceAuthRouter() {
14234
14604
 
14235
14605
  // src/dashboard/api.ts
14236
14606
  function createApiRouter(agent = {}, mode = "all") {
14237
- const api = new Hono26();
14607
+ const api = new Hono27();
14238
14608
  api.use("*", async (c, next) => {
14239
14609
  const known = await listKnownWorkspaces();
14240
14610
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -14341,7 +14711,7 @@ function createApiRouter(agent = {}, mode = "all") {
14341
14711
  }
14342
14712
 
14343
14713
  // src/dashboard/app-icon-routes.ts
14344
- import { Hono as Hono27 } from "hono";
14714
+ import { Hono as Hono28 } from "hono";
14345
14715
  var NEUTRAL_MANIFEST = {
14346
14716
  id: "/",
14347
14717
  name: "Flyspace",
@@ -14379,7 +14749,7 @@ async function withResolvedProfile2(c, next) {
14379
14749
  return profile ? runWithProfile(profile, () => next()) : next();
14380
14750
  }
14381
14751
  function createAppIconRouter() {
14382
- const app = new Hono27();
14752
+ const app = new Hono28();
14383
14753
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
14384
14754
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
14385
14755
  const profile = getProfile();
@@ -14419,7 +14789,7 @@ function pngResponse(c, png) {
14419
14789
  }
14420
14790
 
14421
14791
  // src/dashboard/stripe-routes.ts
14422
- import { Hono as Hono28 } from "hono";
14792
+ import { Hono as Hono29 } from "hono";
14423
14793
  function planStatusFor(status) {
14424
14794
  const known = ["trialing", "active", "paused", "canceled"];
14425
14795
  return known.includes(status) ? status : null;
@@ -14462,7 +14832,7 @@ async function handleCardAdded(session) {
14462
14832
  }
14463
14833
  }
14464
14834
  function createStripeWebhookRouter() {
14465
- const stripeRouter = new Hono28();
14835
+ const stripeRouter = new Hono29();
14466
14836
  stripeRouter.post("/webhook", async (c) => {
14467
14837
  const secret = process.env["STRIPE_WEBHOOK_SECRET"];
14468
14838
  const signature = c.req.header("stripe-signature");
@@ -14573,14 +14943,14 @@ function startHostStatusBroadcast() {
14573
14943
 
14574
14944
  // src/dashboard/server.ts
14575
14945
  var DEFAULT_DASHBOARD_PORT = 4747;
14576
- var WEB_DIST = join18(paths.PACKAGE_ROOT, "web", "dist");
14946
+ var WEB_DIST = join19(paths.PACKAGE_ROOT, "web", "dist");
14577
14947
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
14578
14948
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
14579
14949
  for (let port = start; port < start + attempts; port++) {
14580
- const free = await new Promise((resolve7) => {
14950
+ const free = await new Promise((resolve8) => {
14581
14951
  const probe2 = createServer();
14582
- probe2.once("error", () => resolve7(false));
14583
- probe2.once("listening", () => probe2.close(() => resolve7(true)));
14952
+ probe2.once("error", () => resolve8(false));
14953
+ probe2.once("listening", () => probe2.close(() => resolve8(true)));
14584
14954
  probe2.listen(port, host);
14585
14955
  });
14586
14956
  if (free) return port;
@@ -14607,7 +14977,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
14607
14977
  if (servesHost) installUsageRecording();
14608
14978
  const stopSkillSync = servesHost ? installSkillSync(listProfiles()) : () => {
14609
14979
  };
14610
- const app = new Hono29();
14980
+ const app = new Hono30();
14611
14981
  app.use("*", async (c, next) => {
14612
14982
  await next();
14613
14983
  if (c.req.path.startsWith("/assets/")) {
@@ -14630,9 +15000,9 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
14630
15000
  app.route("/", createAppIconRouter());
14631
15001
  app.use("/*", serveStatic({ root: WEB_DIST }));
14632
15002
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
14633
- const spaFallback = serveStatic({ path: join18(WEB_DIST, "index.html") });
15003
+ const spaFallback = serveStatic({ path: join19(WEB_DIST, "index.html") });
14634
15004
  app.get("*", (c, next) => {
14635
- if (!existsSync26(join18(WEB_DIST, "index.html"))) {
15005
+ if (!existsSync27(join19(WEB_DIST, "index.html"))) {
14636
15006
  return c.text(
14637
15007
  `Dashboard UI build missing (${WEB_DIST}).
14638
15008
  Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
@@ -14669,7 +15039,7 @@ The JSON API is live under /api.`,
14669
15039
 
14670
15040
  // src/slack/image-uploader.ts
14671
15041
  import { readFile } from "fs/promises";
14672
- import { basename as basename5 } from "path";
15042
+ import { basename as basename6 } from "path";
14673
15043
  import sharp6 from "sharp";
14674
15044
  var MAX_WIDTH = 800;
14675
15045
  async function uploadImage(client2, { channel, filePath, threadTs, text }) {
@@ -14678,7 +15048,7 @@ async function uploadImage(client2, { channel, filePath, threadTs, text }) {
14678
15048
  const base = {
14679
15049
  channel_id: channel,
14680
15050
  file: resized,
14681
- filename: basename5(filePath),
15051
+ filename: basename6(filePath),
14682
15052
  ...text ? { initial_comment: text } : {}
14683
15053
  };
14684
15054
  const result = await client2.filesUploadV2(threadTs ? { ...base, thread_ts: threadTs } : base);
@@ -15050,7 +15420,7 @@ async function threadReplies(client2, nameOrId, ts2) {
15050
15420
  async function postMessage(client2, args) {
15051
15421
  const channelArg = getArg2(args, "--channel");
15052
15422
  const textFilePath = getArg2(args, "--text-file");
15053
- const text = textFilePath ? readFileSync15(textFilePath, "utf8") : getArg2(args, "--text");
15423
+ const text = textFilePath ? readFileSync16(textFilePath, "utf8") : getArg2(args, "--text");
15054
15424
  const threadTs = getArg2(args, "--thread");
15055
15425
  const markdown = args.includes("--markdown");
15056
15426
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -15140,22 +15510,22 @@ function mask(token) {
15140
15510
  }
15141
15511
  function hasClaudeBinary() {
15142
15512
  try {
15143
- execFileSync2("claude", ["--version"], { stdio: "ignore" });
15513
+ execFileSync3("claude", ["--version"], { stdio: "ignore" });
15144
15514
  return true;
15145
15515
  } catch {
15146
15516
  return false;
15147
15517
  }
15148
15518
  }
15149
15519
  function defaultClaudeConfigDir() {
15150
- return process.env.CLAUDE_CONFIG_DIR || resolve6(homedir7(), ".claude");
15520
+ return process.env.CLAUDE_CONFIG_DIR || resolve7(homedir8(), ".claude");
15151
15521
  }
15152
- function hasClaudeCredentials(configDir) {
15153
- if (existsSync27(resolve6(configDir, ".credentials.json"))) {
15522
+ function hasClaudeCredentials(configDir2) {
15523
+ if (existsSync28(resolve7(configDir2, ".credentials.json"))) {
15154
15524
  return true;
15155
15525
  }
15156
15526
  if (process.platform === "darwin") {
15157
15527
  try {
15158
- execFileSync2(
15528
+ execFileSync3(
15159
15529
  "security",
15160
15530
  ["find-generic-password", "-s", "Claude Code-credentials"],
15161
15531
  { stdio: "ignore" }
@@ -15187,7 +15557,7 @@ async function ensureClaudeCodeReady() {
15187
15557
  console.log(" Flyspace spawns Claude Code as a subprocess. Without credentials, the bot");
15188
15558
  console.log(" will prompt /login inside Slack at runtime and you cannot complete the");
15189
15559
  console.log(" browser sign-in from there.\n");
15190
- if (claudeConfigDir !== resolve6(homedir7(), ".claude")) {
15560
+ if (claudeConfigDir !== resolve7(homedir8(), ".claude")) {
15191
15561
  console.log(" Tip: run with the same CLAUDE_CONFIG_DIR you use for this Claude Code install:");
15192
15562
  console.log(` \x1B[1mCLAUDE_CONFIG_DIR=${claudeConfigDir} claude\x1B[0m
15193
15563
  `);
@@ -15206,8 +15576,8 @@ async function ensureClaudeCodeReady() {
15206
15576
  return { claudeConfigDir };
15207
15577
  }
15208
15578
  function getManifestYaml(botName) {
15209
- const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
15210
- const raw = readFileSync15(manifestPath, "utf-8");
15579
+ const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
15580
+ const raw = readFileSync16(manifestPath, "utf-8");
15211
15581
  return raw.replace(/name: Custie/g, `name: ${botName}`);
15212
15582
  }
15213
15583
  async function stepPersonalise() {
@@ -15420,7 +15790,7 @@ async function stepBrowserTokens(botName) {
15420
15790
  })
15421
15791
  ));
15422
15792
  const DEBUG = process.argv.includes("--debug");
15423
- const USER_DATA_DIR = resolve6(tmpdir3(), "custie-playwright-profile");
15793
+ const USER_DATA_DIR = resolve7(tmpdir3(), "custie-playwright-profile");
15424
15794
  let context;
15425
15795
  try {
15426
15796
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -15452,14 +15822,14 @@ async function stepBrowserTokens(botName) {
15452
15822
  const appLink = page2.locator('a[href*="/apps/A"]').first();
15453
15823
  const hasExistingApp = await appLink.isVisible({ timeout: 3e3 }).catch(() => false);
15454
15824
  if (hasExistingApp) {
15455
- const tokenPromise = new Promise((resolve7) => {
15825
+ const tokenPromise = new Promise((resolve8) => {
15456
15826
  page2.on("request", (req) => {
15457
15827
  if (token) return;
15458
15828
  const postData = req.postData() || "";
15459
15829
  const match = postData.match(/xoxc-[a-f0-9-]+/);
15460
15830
  if (match) {
15461
15831
  token = match[0];
15462
- resolve7(token);
15832
+ resolve8(token);
15463
15833
  }
15464
15834
  });
15465
15835
  });
@@ -15510,7 +15880,7 @@ async function stepBrowserTokens(botName) {
15510
15880
  log4("Almost done! The browser will now open an install page.\n");
15511
15881
  console.log(' \x1B[1mPlease click "Install to Workspace", then click "Allow".\x1B[0m\n');
15512
15882
  await page2.goto(installUrl);
15513
- const installPage = await new Promise((resolve7, reject) => {
15883
+ const installPage = await new Promise((resolve8, reject) => {
15514
15884
  let settled = false;
15515
15885
  let interval;
15516
15886
  let timeout;
@@ -15523,7 +15893,7 @@ async function stepBrowserTokens(botName) {
15523
15893
  };
15524
15894
  const checkUrl = (p) => {
15525
15895
  if (p.url().includes("install-on-team?success=1")) {
15526
- finish(() => resolve7(p));
15896
+ finish(() => resolve8(p));
15527
15897
  }
15528
15898
  };
15529
15899
  context.on("page", (newPage) => {
@@ -15582,7 +15952,7 @@ async function stepBrowserTokens(botName) {
15582
15952
  try {
15583
15953
  const pages = context?.pages();
15584
15954
  if (pages?.[0]) {
15585
- const screenshotPath = resolve6(paths.PACKAGE_ROOT, "setup-error.png");
15955
+ const screenshotPath = resolve7(paths.PACKAGE_ROOT, "setup-error.png");
15586
15956
  await pages[0].screenshot({ path: screenshotPath, fullPage: true });
15587
15957
  warn4(`Screenshot saved to: ${screenshotPath}`);
15588
15958
  }
@@ -15638,10 +16008,10 @@ function printNextSteps() {
15638
16008
  console.log(" Run \x1B[1mflyspace install\x1B[0m\n");
15639
16009
  }
15640
16010
  function copyDefaultPrompt(botName) {
15641
- if (!existsSync27(paths.PROMPT_FILE)) {
15642
- const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
15643
- if (existsSync27(defaultPrompt)) {
15644
- const content = readFileSync15(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
16011
+ if (!existsSync28(paths.PROMPT_FILE)) {
16012
+ const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
16013
+ if (existsSync28(defaultPrompt)) {
16014
+ const content = readFileSync16(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
15645
16015
  writeFileSync9(paths.PROMPT_FILE, content);
15646
16016
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
15647
16017
  }
@@ -15654,7 +16024,7 @@ async function runSlackSetup(args) {
15654
16024
  const prereq = await ensureClaudeCodeReady();
15655
16025
  if (!prereq) return;
15656
16026
  const { claudeConfigDir } = prereq;
15657
- if (existsSync27(paths.CONFIG_FILE)) {
16027
+ if (existsSync28(paths.CONFIG_FILE)) {
15658
16028
  const reconfigure = await confirm({
15659
16029
  message: `Config already exists at ${paths.CONFIG_FILE}. Reconfigure?`,
15660
16030
  default: false
@@ -16001,9 +16371,9 @@ async function runMcpCmd(args) {
16001
16371
 
16002
16372
  // src/commands/dashboard.ts
16003
16373
  import { spawn as spawn5, spawnSync } from "child_process";
16004
- import { existsSync as existsSync28 } from "fs";
16374
+ import { existsSync as existsSync29 } from "fs";
16005
16375
  import { platform as platform6 } from "os";
16006
- import { join as join19 } from "path";
16376
+ import { join as join20 } from "path";
16007
16377
  function flagValue(args, name) {
16008
16378
  const eq = args.find((a) => a.startsWith(`--${name}=`));
16009
16379
  if (eq) return eq.slice(`--${name}=`.length);
@@ -16018,7 +16388,7 @@ function openBrowser(url) {
16018
16388
  }
16019
16389
  }
16020
16390
  function ensureWebBuild() {
16021
- if (existsSync28(join19(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
16391
+ if (existsSync29(join20(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
16022
16392
  console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
16023
16393
  const result = spawnSync("pnpm", ["--dir", "web", "build"], {
16024
16394
  cwd: paths.PACKAGE_ROOT,
@@ -16066,7 +16436,7 @@ async function runDashboard(args) {
16066
16436
  }
16067
16437
 
16068
16438
  // src/commands/workspace.ts
16069
- import { existsSync as existsSync29, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
16439
+ import { existsSync as existsSync30, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
16070
16440
  var USAGE5 = `
16071
16441
  Usage: flyspace workspace <subcommand> [options]
16072
16442
 
@@ -16141,7 +16511,7 @@ function provisionLocal(botName) {
16141
16511
  ensureDirs();
16142
16512
  console.log(` config dir ready (${paths.CONFIG_DIR})`);
16143
16513
  console.log(` data dir ready (${paths.DATA_DIR})`);
16144
- if (existsSync29(paths.CONFIG_FILE)) {
16514
+ if (existsSync30(paths.CONFIG_FILE)) {
16145
16515
  console.log(" config.env exists");
16146
16516
  } else {
16147
16517
  writeFileSync10(paths.CONFIG_FILE, `BOT_NAME=${botName}
@@ -16158,8 +16528,8 @@ async function provisionChannels(specs) {
16158
16528
  continue;
16159
16529
  }
16160
16530
  const cwd = expandPath(spec.cwd);
16161
- if (!existsSync29(cwd)) {
16162
- mkdirSync8(cwd, { recursive: true });
16531
+ if (!existsSync30(cwd)) {
16532
+ mkdirSync9(cwd, { recursive: true });
16163
16533
  console.log(` created cwd ${cwd}`);
16164
16534
  }
16165
16535
  await createChannel({ name: spec.name, cwd, position: index });
@@ -16664,7 +17034,7 @@ async function main() {
16664
17034
  break;
16665
17035
  case "-v":
16666
17036
  case "--version":
16667
- printRaw("0.1.0");
17037
+ printRaw("0.1.1");
16668
17038
  break;
16669
17039
  case "-h":
16670
17040
  case "--help":