flyspace-cli 0.1.32 → 0.1.34

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
@@ -18,6 +18,7 @@ import {
18
18
  SKIP_SUBTYPES,
19
19
  SLACK_ENV,
20
20
  SessionStore,
21
+ TRIAL_DAYS,
21
22
  UsageStore,
22
23
  activateMembership,
23
24
  agentLabel,
@@ -118,6 +119,7 @@ import {
118
119
  loadFullDashboardSettings,
119
120
  loadWorkspaceBranding,
120
121
  loadWorkspacePrompt,
122
+ markChannelGreeted,
121
123
  markInviteResent,
122
124
  markMemoryUsed,
123
125
  markWorkspaceGreeted,
@@ -159,6 +161,7 @@ import {
159
161
  setMembershipHandle,
160
162
  setMemoryAutoPaused,
161
163
  setOutputStyleDefault,
164
+ setWorkspaceArchived,
162
165
  setWorkspaceStripeSubscription,
163
166
  slackConfigured,
164
167
  startAutomationRegistryPolling,
@@ -184,7 +187,7 @@ import {
184
187
  warnOnBrokenChannelCwds,
185
188
  workspaceSkillsDir,
186
189
  writeSkillFile
187
- } from "./chunk-6YFDWUNF.js";
190
+ } from "./chunk-KTMAS4TP.js";
188
191
  import {
189
192
  firstReplyBody,
190
193
  getThread,
@@ -618,14 +621,14 @@ function handleHostDbResult(id, res) {
618
621
  }
619
622
  function whenConnected() {
620
623
  if (sender) return Promise.resolve(sender);
621
- return new Promise((resolve7, reject) => {
624
+ return new Promise((resolve8, reject) => {
622
625
  const timer2 = setTimeout(() => {
623
626
  onSender = onSender.filter((w) => w !== wake);
624
627
  reject(new Error("Host is not connected to the Data API"));
625
628
  }, REQUEST_TIMEOUT_MS);
626
629
  const wake = () => {
627
630
  clearTimeout(timer2);
628
- resolve7(sender);
631
+ resolve8(sender);
629
632
  };
630
633
  onSender.push(wake);
631
634
  });
@@ -633,12 +636,12 @@ function whenConnected() {
633
636
  async function requestHostDb(req) {
634
637
  const send2 = await whenConnected();
635
638
  const id = randomUUID();
636
- return new Promise((resolve7, reject) => {
639
+ return new Promise((resolve8, reject) => {
637
640
  const timer2 = setTimeout(() => {
638
641
  pending.delete(id);
639
642
  reject(new Error("Data API did not answer in time"));
640
643
  }, REQUEST_TIMEOUT_MS);
641
- pending.set(id, { resolve: resolve7, reject, timer: timer2 });
644
+ pending.set(id, { resolve: resolve8, reject, timer: timer2 });
642
645
  send2({ type: "db", id, req });
643
646
  });
644
647
  }
@@ -650,14 +653,14 @@ function handleAttachmentUrlResult(id, url) {
650
653
  async function requestAttachmentUrl(key) {
651
654
  const send2 = await whenConnected();
652
655
  const id = randomUUID();
653
- return new Promise((resolve7, reject) => {
656
+ return new Promise((resolve8, reject) => {
654
657
  const timer2 = setTimeout(() => {
655
658
  pendingUrls.delete(id);
656
659
  reject(new Error("Data API did not answer in time"));
657
660
  }, REQUEST_TIMEOUT_MS);
658
661
  pendingUrls.set(id, (url) => {
659
662
  clearTimeout(timer2);
660
- resolve7(url);
663
+ resolve8(url);
661
664
  });
662
665
  send2({ type: "attachment-url", id, key });
663
666
  });
@@ -804,10 +807,10 @@ function editConfig() {
804
807
  const editor = process.env["EDITOR"] || "vi";
805
808
  console.log(`[flyspace] Opening ${paths.CONFIG_FILE} in ${editor}...`);
806
809
  const child = spawn(editor, [paths.CONFIG_FILE], { stdio: "inherit" });
807
- return new Promise((resolve7, reject) => {
810
+ return new Promise((resolve8, reject) => {
808
811
  child.on("close", (code) => {
809
812
  if (code === 0) {
810
- resolve7();
813
+ resolve8();
811
814
  } else {
812
815
  reject(new Error(`Editor exited with code ${code}`));
813
816
  }
@@ -1858,7 +1861,7 @@ async function runAuthCmd(args) {
1858
1861
  }
1859
1862
 
1860
1863
  // ../api/src/version.ts
1861
- var CLI_VERSION = true ? "0.1.32" : null;
1864
+ var CLI_VERSION = true ? "0.1.34" : null;
1862
1865
  function isOlderVersion(a, b) {
1863
1866
  const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1864
1867
  const [pa, pb] = [parse(a), parse(b)];
@@ -2468,14 +2471,19 @@ function npmEnv(target) {
2468
2471
  return nodeBin ? { ...process.env, PATH: `${nodeBin}:${process.env.PATH ?? ""}` } : process.env;
2469
2472
  }
2470
2473
  async function runUpgrade() {
2471
- const current = "0.1.32";
2474
+ const current = "0.1.34";
2472
2475
  console.log(`Current version: ${current}`);
2473
2476
  console.log("Checking for updates...\n");
2474
2477
  try {
2475
2478
  const latest2 = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", env: npmEnv() }).trim();
2476
2479
  const stale = installTargets().filter((t) => installedVersion(t) !== latest2);
2477
2480
  if (stale.length === 0) {
2478
- console.log(`Already on the latest version (${current}).`);
2481
+ if (current === latest2) {
2482
+ console.log(`Already on the latest version (${current}).`);
2483
+ return;
2484
+ }
2485
+ console.log(`${latest2} is installed but this process runs ${current}. Restarting...`);
2486
+ restartInstalledProfiles();
2479
2487
  return;
2480
2488
  }
2481
2489
  console.log(`New version available: ${latest2}`);
@@ -4144,7 +4152,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
4144
4152
  const subscription = await stripe.subscriptions.create({
4145
4153
  customer: customer.id,
4146
4154
  items: [{ price: priceId }],
4147
- trial_period_days: 14,
4155
+ trial_period_days: TRIAL_DAYS,
4148
4156
  trial_settings: { end_behavior: { missing_payment_method: "pause" } },
4149
4157
  metadata: { workspace_id: workspace.id, plan }
4150
4158
  });
@@ -4792,10 +4800,10 @@ var DEFAULT_DASHBOARD_PORT = 4747;
4792
4800
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
4793
4801
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
4794
4802
  for (let port = start; port < start + attempts; port++) {
4795
- const free = await new Promise((resolve7) => {
4803
+ const free = await new Promise((resolve8) => {
4796
4804
  const probe3 = createServer();
4797
- probe3.once("error", () => resolve7(false));
4798
- probe3.once("listening", () => probe3.close(() => resolve7(true)));
4805
+ probe3.once("error", () => resolve8(false));
4806
+ probe3.once("listening", () => probe3.close(() => resolve8(true)));
4799
4807
  probe3.listen(port, host);
4800
4808
  });
4801
4809
  if (free) return port;
@@ -5623,14 +5631,14 @@ async function stepBrowserTokens(botName) {
5623
5631
  const appLink = page2.locator('a[href*="/apps/A"]').first();
5624
5632
  const hasExistingApp = await appLink.isVisible({ timeout: 3e3 }).catch(() => false);
5625
5633
  if (hasExistingApp) {
5626
- const tokenPromise = new Promise((resolve7) => {
5634
+ const tokenPromise = new Promise((resolve8) => {
5627
5635
  page2.on("request", (req) => {
5628
5636
  if (token) return;
5629
5637
  const postData = req.postData() || "";
5630
5638
  const match = postData.match(/xoxc-[a-f0-9-]+/);
5631
5639
  if (match) {
5632
5640
  token = match[0];
5633
- resolve7(token);
5641
+ resolve8(token);
5634
5642
  }
5635
5643
  });
5636
5644
  });
@@ -5681,7 +5689,7 @@ async function stepBrowserTokens(botName) {
5681
5689
  log4("Almost done! The browser will now open an install page.\n");
5682
5690
  console.log(' \x1B[1mPlease click "Install to Workspace", then click "Allow".\x1B[0m\n');
5683
5691
  await page2.goto(installUrl);
5684
- const installPage = await new Promise((resolve7, reject) => {
5692
+ const installPage = await new Promise((resolve8, reject) => {
5685
5693
  let settled = false;
5686
5694
  let interval;
5687
5695
  let timeout;
@@ -5694,7 +5702,7 @@ async function stepBrowserTokens(botName) {
5694
5702
  };
5695
5703
  const checkUrl = (p) => {
5696
5704
  if (p.url().includes("install-on-team?success=1")) {
5697
- finish2(() => resolve7(p));
5705
+ finish2(() => resolve8(p));
5698
5706
  }
5699
5707
  };
5700
5708
  context.on("page", (newPage) => {
@@ -6408,9 +6416,9 @@ async function runMcpCmd(args) {
6408
6416
 
6409
6417
  // src/commands/dashboard.ts
6410
6418
  import { spawn as spawn8, spawnSync } from "child_process";
6411
- import { existsSync as existsSync33 } from "fs";
6419
+ import { existsSync as existsSync34 } from "fs";
6412
6420
  import { platform as platform7 } from "os";
6413
- import { join as join28 } from "path";
6421
+ import { join as join29 } from "path";
6414
6422
 
6415
6423
  // src/reconcile.ts
6416
6424
  import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
@@ -6583,7 +6591,7 @@ var requireDocsAccess = async (c, next) => {
6583
6591
  };
6584
6592
 
6585
6593
  // ../api/src/dashboard/api.ts
6586
- import { Hono as Hono29 } from "hono";
6594
+ import { Hono as Hono31 } from "hono";
6587
6595
 
6588
6596
  // ../api/src/dashboard/public-share-routes.ts
6589
6597
  import { existsSync as existsSync17, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
@@ -7648,7 +7656,7 @@ function truncationNotice(lang, voice) {
7648
7656
  if (lang === "en" || lang.startsWith("en-")) return TRUNCATION_NOTICE.en;
7649
7657
  return null;
7650
7658
  }
7651
- var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7659
+ var sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
7652
7660
  async function synthesizeWithRetry(script, voice) {
7653
7661
  let lastError;
7654
7662
  for (let attempt = 1; attempt <= SYNTH_ATTEMPTS; attempt += 1) {
@@ -8735,7 +8743,17 @@ function pruneExpired(now) {
8735
8743
  }
8736
8744
  var SUPPORT_NAME = "Flyspace Support";
8737
8745
  var SUPPORT_AVATAR_URL = "/logo/flyspace-support.svg";
8746
+ function workspaceArchivedError() {
8747
+ return new HTTPException2(403, {
8748
+ res: Response.json({ error: "workspace archived", archived: true }, { status: 403 })
8749
+ });
8750
+ }
8738
8751
  async function resolveAuthContext(c, opts = {}) {
8752
+ const ctx = await resolveAuthContextCached(c, opts);
8753
+ if (ctx?.archived && !ctx.isSupport) throw workspaceArchivedError();
8754
+ return ctx;
8755
+ }
8756
+ async function resolveAuthContextCached(c, opts) {
8739
8757
  const cookie = c.req?.header("Cookie") ?? "";
8740
8758
  if (!cookie) return resolveAuthContextUncached(c);
8741
8759
  const key = `${getProfile()}\0${cookie}`;
@@ -8800,7 +8818,8 @@ async function resolveAuthContextUncached(c) {
8800
8818
  passwordSet: Boolean(metadata.passwordSetAt),
8801
8819
  agentConnectedAt: workspace.agentConnectedAt ?? null,
8802
8820
  projectsSetupAt: workspace.projectsSetupAt ?? null,
8803
- plan: workspace.plan ?? null
8821
+ plan: workspace.plan ?? null,
8822
+ ...workspace.archivedAt ? { archived: true } : {}
8804
8823
  };
8805
8824
  }
8806
8825
 
@@ -9116,11 +9135,19 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
9116
9135
  // ../api/src/dashboard/uploads-routes.ts
9117
9136
  import { randomUUID as randomUUID2 } from "crypto";
9118
9137
  import { Hono as Hono7 } from "hono";
9138
+ import sharp4 from "sharp";
9119
9139
  import { PutObjectCommand as PutObjectCommand2, GetObjectCommand as GetObjectCommand3 } from "@aws-sdk/client-s3";
9120
9140
  import { getSignedUrl as getSignedUrl3 } from "@aws-sdk/s3-request-presigner";
9121
9141
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
9122
9142
  var PRESIGN_TTL_SECONDS2 = 300;
9123
9143
  var KEY_PREFIX = "chat";
9144
+ var RESIZABLE = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/avif"]);
9145
+ var FULL_EDGE = 2560;
9146
+ var THUMB_EDGE = 480;
9147
+ var WEBP_QUALITY = 80;
9148
+ function toWebp(input2, edge) {
9149
+ return sharp4(input2).rotate().resize({ width: edge, height: edge, fit: "inside", withoutEnlargement: true }).webp({ quality: WEBP_QUALITY }).toBuffer();
9150
+ }
9124
9151
  var READABLE_PREFIXES = [KEY_PREFIX, SCRATCH_PREFIX];
9125
9152
  function sanitizeFilename(name) {
9126
9153
  const cleaned = name.replace(/[/\\]/g, "_").replace(/\s+/g, "-").replace(/[^\w.-]/g, "").trim();
@@ -9144,22 +9171,33 @@ function createUploadsRouter() {
9144
9171
  if (file.size > MAX_FILE_BYTES) {
9145
9172
  return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
9146
9173
  }
9147
- const key = `${KEY_PREFIX}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
9148
- const buffer = Buffer.from(await file.arrayBuffer());
9149
- await getR2Client().send(
9150
- new PutObjectCommand2({
9151
- Bucket: getR2Bucket(),
9152
- Key: key,
9153
- Body: buffer,
9154
- ContentType: file.type || "application/octet-stream"
9155
- })
9156
- );
9174
+ const id = `${KEY_PREFIX}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
9175
+ const original = Buffer.from(await file.arrayBuffer());
9176
+ const put = (Key, Body, ContentType) => getR2Client().send(new PutObjectCommand2({ Bucket: getR2Bucket(), Key, Body, ContentType }));
9177
+ let key = id;
9178
+ let size = file.size;
9179
+ let type = file.type;
9180
+ const renditions = RESIZABLE.has(file.type) ? await Promise.all([toWebp(original, FULL_EDGE), toWebp(original, THUMB_EDGE)]).catch(
9181
+ () => null
9182
+ ) : null;
9183
+ if (renditions) {
9184
+ const [full, thumb] = renditions;
9185
+ key = `${id.replace(/\.[^./]*$/, "")}.webp`;
9186
+ size = full.length;
9187
+ type = "image/webp";
9188
+ await Promise.all([
9189
+ put(key, full, type),
9190
+ put(key.replace(/\.webp$/, ".thumb.webp"), thumb, type)
9191
+ ]);
9192
+ } else {
9193
+ await put(key, original, file.type || "application/octet-stream");
9194
+ }
9157
9195
  return c.json(
9158
9196
  {
9159
9197
  url: `/api/attachments/${encodeKeyForUrl(key)}`,
9160
9198
  name: file.name,
9161
- type: file.type,
9162
- size: file.size
9199
+ type,
9200
+ size
9163
9201
  },
9164
9202
  201
9165
9203
  );
@@ -9544,12 +9582,94 @@ function createAuthRouter() {
9544
9582
  return auth;
9545
9583
  }
9546
9584
 
9585
+ // ../api/src/store/support-store.ts
9586
+ var SUPPORT_HANDLE = "flyspace-support";
9587
+ async function logSupportAction(entry) {
9588
+ const { error: error2 } = await getSupabaseClient().from("support_access_log").insert({
9589
+ workspace_id: entry.workspaceId,
9590
+ staff_user_id: entry.staffUserId,
9591
+ staff_email: entry.staffEmail,
9592
+ action: entry.action,
9593
+ detail: entry.detail ?? null
9594
+ });
9595
+ if (error2) throw new Error(`logSupportAction(${entry.action}): ${error2.message}`);
9596
+ }
9597
+ async function listSupportWorkspaceIds(userId) {
9598
+ const { data, error: error2 } = await getSupabaseClient().from("memberships").select("workspace_id").eq("user_id", userId).eq("is_support", true);
9599
+ if (error2) throw new Error(`listSupportWorkspaceIds: ${error2.message}`);
9600
+ return new Set((data ?? []).map((r) => r.workspace_id));
9601
+ }
9602
+ async function grantSupportMembership(params) {
9603
+ const db = getSupabaseClient();
9604
+ const { data: existing, error: readError } = await db.from("memberships").select("is_support").eq("workspace_id", params.workspaceId).eq("user_id", params.userId).maybeSingle();
9605
+ if (readError) throw new Error(`grantSupportMembership: ${readError.message}`);
9606
+ if (existing) {
9607
+ if (existing.is_support === true) return "existing";
9608
+ throw new Error("already a regular member of this workspace");
9609
+ }
9610
+ const { data: taken } = await db.from("memberships").select("id").eq("workspace_id", params.workspaceId).eq("handle", SUPPORT_HANDLE).maybeSingle();
9611
+ const handle = taken ? `${SUPPORT_HANDLE}-${params.userId.slice(0, 8)}` : SUPPORT_HANDLE;
9612
+ const { error: error2 } = await db.from("memberships").insert({
9613
+ workspace_id: params.workspaceId,
9614
+ user_id: params.userId,
9615
+ email: params.email,
9616
+ handle,
9617
+ role: "admin",
9618
+ status: "active",
9619
+ is_support: true,
9620
+ accepted_at: (/* @__PURE__ */ new Date()).toISOString()
9621
+ });
9622
+ if (error2) throw new Error(`grantSupportMembership: ${error2.message}`);
9623
+ return "granted";
9624
+ }
9625
+ async function revokeSupportMembership(workspaceId, userId) {
9626
+ const { data, error: error2 } = await getSupabaseClient().from("memberships").delete().eq("workspace_id", workspaceId).eq("user_id", userId).eq("is_support", true).select("id");
9627
+ if (error2) throw new Error(`revokeSupportMembership: ${error2.message}`);
9628
+ return (data ?? []).length > 0;
9629
+ }
9630
+ async function listSupportLog(filter) {
9631
+ let query = getSupabaseClient().from("support_access_log").select("*").order("created_at", { ascending: false }).limit(Math.min(filter.limit ?? 200, 1e3));
9632
+ if (filter.workspaceId) query = query.eq("workspace_id", filter.workspaceId);
9633
+ const { data, error: error2 } = await query;
9634
+ if (error2) throw new Error(`listSupportLog: ${error2.message}`);
9635
+ return (data ?? []).map((r) => ({
9636
+ id: r.id,
9637
+ workspaceId: r.workspace_id,
9638
+ staffUserId: r.staff_user_id,
9639
+ staffEmail: r.staff_email,
9640
+ action: r.action,
9641
+ detail: r.detail ?? null,
9642
+ createdAt: r.created_at
9643
+ }));
9644
+ }
9645
+
9547
9646
  // ../api/src/dashboard/auth-middleware.ts
9647
+ var ENTER_WINDOW_MS = 8 * 60 * 60 * 1e3;
9648
+ var lastEnter = /* @__PURE__ */ new Map();
9649
+ function auditSupport(ctx, action, detail) {
9650
+ logSupportAction({
9651
+ workspaceId: ctx.workspaceId,
9652
+ staffUserId: ctx.userId,
9653
+ staffEmail: ctx.email,
9654
+ action,
9655
+ detail
9656
+ }).catch((err) => console.error("[support-audit]", err.message));
9657
+ }
9548
9658
  var requireWorkspaceMembership = async (c, next) => {
9549
9659
  const ctx = await resolveAuthContext(c);
9550
9660
  if (!ctx) return c.json({ error: "not signed in" }, 401);
9551
9661
  c.set("auth", ctx);
9552
- return next();
9662
+ if (!ctx.isSupport) return next();
9663
+ const key = `${ctx.userId}:${ctx.workspaceId}`;
9664
+ const now = Date.now();
9665
+ if ((lastEnter.get(key) ?? 0) + ENTER_WINDOW_MS < now) {
9666
+ lastEnter.set(key, now);
9667
+ auditSupport(ctx, "enter");
9668
+ }
9669
+ await next();
9670
+ if (!["GET", "HEAD", "OPTIONS"].includes(c.req.method)) {
9671
+ auditSupport(ctx, "write", { method: c.req.method, path: c.req.path, status: c.res.status });
9672
+ }
9553
9673
  };
9554
9674
 
9555
9675
  // ../api/src/dashboard/billing-routes.ts
@@ -9654,7 +9774,14 @@ function createAdminRouter() {
9654
9774
  const admin = new Hono11();
9655
9775
  admin.use("*", requirePlatformOperator);
9656
9776
  admin.get("/workspaces", async (c) => {
9657
- const summaries = await listWorkspaceSummaries();
9777
+ const operator = c.get("platformOperator");
9778
+ const [summaries, supportIds, users] = await Promise.all([
9779
+ listWorkspaceSummaries(),
9780
+ listSupportWorkspaceIds(operator.userId),
9781
+ // One call for every owner's profile metadata instead of one per row.
9782
+ getSupabaseClient().auth.admin.listUsers({ perPage: 1e3 })
9783
+ ]);
9784
+ const usersById = new Map((users.data?.users ?? []).map((u) => [u.id, u]));
9658
9785
  const workspaces = await Promise.all(
9659
9786
  summaries.map(async (w) => {
9660
9787
  const agentModel = cachedProfileConfig(w.slug)?.agentModel;
@@ -9664,11 +9791,23 @@ function createAdminRouter() {
9664
9791
  console.error(`[admin] R2 usage for ${w.slug}:`, err.message);
9665
9792
  return null;
9666
9793
  });
9794
+ const metadata = w.owner ? usersById.get(w.owner.userId)?.user_metadata : void 0;
9795
+ const owner = w.owner && {
9796
+ ...w.owner,
9797
+ role: "admin",
9798
+ avatarUrl: metadata?.avatarKey ? await signAvatarUrl(metadata.avatarKey) : null,
9799
+ name: metadata?.name ?? null,
9800
+ title: metadata?.title ?? null,
9801
+ bio: metadata?.bio ?? null,
9802
+ timeZone: metadata?.timeZone ?? null
9803
+ };
9667
9804
  return {
9668
9805
  ...w,
9806
+ owner,
9669
9807
  agent: agentModel ? { runner: runnerForModel(agentModel), model: agentModel } : null,
9670
9808
  r2Bytes,
9671
- flyVolumeBytes: null
9809
+ flyVolumeBytes: null,
9810
+ supportAccess: supportIds.has(w.id)
9672
9811
  };
9673
9812
  })
9674
9813
  );
@@ -9756,6 +9895,63 @@ function createAdminRouter() {
9756
9895
  }
9757
9896
  return c.json({ sent: true, inviteLink: inviteUrl.toString() });
9758
9897
  });
9898
+ admin.post("/workspaces/:id/support-access", async (c) => {
9899
+ const operator = c.get("platformOperator");
9900
+ const workspaceId = c.req.param("id");
9901
+ let result;
9902
+ try {
9903
+ result = await grantSupportMembership({
9904
+ workspaceId,
9905
+ userId: operator.userId,
9906
+ email: operator.email
9907
+ });
9908
+ } catch (err) {
9909
+ return c.json({ error: err instanceof Error ? err.message : "grant failed" }, 409);
9910
+ }
9911
+ if (result === "granted") {
9912
+ await logSupportAction({
9913
+ workspaceId,
9914
+ staffUserId: operator.userId,
9915
+ staffEmail: operator.email,
9916
+ action: "grant"
9917
+ });
9918
+ }
9919
+ invalidateAuthContextCache();
9920
+ return c.json({ supportAccess: true });
9921
+ });
9922
+ admin.delete("/workspaces/:id/support-access", async (c) => {
9923
+ const operator = c.get("platformOperator");
9924
+ const workspaceId = c.req.param("id");
9925
+ if (await revokeSupportMembership(workspaceId, operator.userId)) {
9926
+ await logSupportAction({
9927
+ workspaceId,
9928
+ staffUserId: operator.userId,
9929
+ staffEmail: operator.email,
9930
+ action: "leave"
9931
+ });
9932
+ }
9933
+ invalidateAuthContextCache();
9934
+ return c.json({ supportAccess: false });
9935
+ });
9936
+ const archiveRoute = (archived) => async (c) => {
9937
+ try {
9938
+ await setWorkspaceArchived(c.req.param("id"), archived);
9939
+ } catch (err) {
9940
+ const message = err instanceof Error ? err.message : "archive failed";
9941
+ return c.json({ error: message }, message === "workspace not found" ? 404 : 500);
9942
+ }
9943
+ invalidateAuthContextCache();
9944
+ return c.json({ archived });
9945
+ };
9946
+ admin.post("/workspaces/:id/archive", archiveRoute(true));
9947
+ admin.delete("/workspaces/:id/archive", archiveRoute(false));
9948
+ admin.get("/support-log", async (c) => {
9949
+ const entries2 = await listSupportLog({
9950
+ workspaceId: c.req.query("workspace") || void 0,
9951
+ limit: Number(c.req.query("limit")) || void 0
9952
+ });
9953
+ return c.json({ entries: entries2 });
9954
+ });
9759
9955
  return admin;
9760
9956
  }
9761
9957
 
@@ -9765,7 +9961,7 @@ var BOT_DM_KEY = "claude";
9765
9961
 
9766
9962
  // ../api/src/dashboard/data-api.ts
9767
9963
  import { existsSync as existsSync20 } from "fs";
9768
- import sharp5 from "sharp";
9964
+ import sharp6 from "sharp";
9769
9965
 
9770
9966
  // ../api/src/dashboard/automations-routes.ts
9771
9967
  import { Hono as Hono12 } from "hono";
@@ -10259,6 +10455,25 @@ var BOARD_STATUSES = [
10259
10455
  "cancelled"
10260
10456
  ];
10261
10457
  var NO_STATUS_COLUMN = "none";
10458
+ function isBoardStatus(v) {
10459
+ return typeof v === "string" && BOARD_STATUSES.includes(v);
10460
+ }
10461
+ async function callerMembershipId(c) {
10462
+ const viewer = c.get("auth");
10463
+ if (!viewer) return null;
10464
+ return (await getMembershipByUserId(viewer.workspaceId, viewer.userId))?.id ?? null;
10465
+ }
10466
+ async function rememberComposerStatus(c, channelId, status) {
10467
+ const membershipId = await callerMembershipId(c);
10468
+ if (!membershipId) return;
10469
+ const { error: error2 } = await getSupabaseClient().from("channel_member_prefs").upsert({
10470
+ channel_id: channelId,
10471
+ membership_id: membershipId,
10472
+ last_board_status: status,
10473
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
10474
+ });
10475
+ if (error2) throw new Error(error2.message);
10476
+ }
10262
10477
  function boardColumnParam(status) {
10263
10478
  if (status === NO_STATUS_COLUMN) return NO_STATUS_COLUMN;
10264
10479
  return BOARD_STATUSES.includes(status) ? status : null;
@@ -10324,7 +10539,9 @@ async function withPreviews(supabase, threads) {
10324
10539
  for (const { data } of chunkResults) {
10325
10540
  for (const r of data ?? []) {
10326
10541
  const entry = previewByThread.get(r.thread_id);
10542
+ const images = r.body.includes("![") ? markdownImageUrls(slackMrkdwnToMarkdown(r.body)) : [];
10327
10543
  if (entry) {
10544
+ entry.images.push(...images);
10328
10545
  entry.count += 1;
10329
10546
  entry.lastAuthor = r.author;
10330
10547
  entry.participants.delete(r.author);
@@ -10334,7 +10551,8 @@ async function withPreviews(supabase, threads) {
10334
10551
  count: 1,
10335
10552
  firstBody: r.body,
10336
10553
  lastAuthor: r.author,
10337
- participants: /* @__PURE__ */ new Map()
10554
+ participants: /* @__PURE__ */ new Map(),
10555
+ images
10338
10556
  });
10339
10557
  }
10340
10558
  }
@@ -10349,10 +10567,19 @@ async function withPreviews(supabase, threads) {
10349
10567
  // read, on this browser or any other (see web/src/lib/thread-unread).
10350
10568
  last_reply_author: entry && entry.count > 1 ? entry.lastAuthor : null,
10351
10569
  // Everyone who replied, most recent first, for the row's avatar stack.
10352
- participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id }))
10570
+ participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id })),
10571
+ // Every image in the thread, in order, so the row can show a thumbnail
10572
+ // strip; only the first few URLs are sent, the rest are just counted.
10573
+ images: (entry?.images ?? []).slice(0, ROW_IMAGE_LIMIT),
10574
+ image_count: entry?.images.length ?? 0
10353
10575
  };
10354
10576
  });
10355
10577
  }
10578
+ var ROW_IMAGE_LIMIT = 3;
10579
+ var MARKDOWN_IMAGE_RE = /!\[[^\]]*\]\(\s*<?([^\s)>]+)>?(?:\s+"[^"]*")?\s*\)/g;
10580
+ function markdownImageUrls(markdown) {
10581
+ return [...markdown.matchAll(MARKDOWN_IMAGE_RE)].map((m) => m[1]);
10582
+ }
10356
10583
  async function labelThread(channelId, body, scope) {
10357
10584
  const channel = getProfileChannelRegistry().find(
10358
10585
  (ch) => ch.id === channelId || ch.externalChannelId === channelId
@@ -10762,6 +10989,13 @@ function createChatRouter(agent = {}) {
10762
10989
  const clientKey2 = isNonEmptyString(input2.clientKey) ? input2.clientKey : null;
10763
10990
  const model = requestedModel(c, input2.model);
10764
10991
  const effort = requestedEffort(c, input2.effort);
10992
+ const pickedStatus = input2.board_status;
10993
+ if (pickedStatus != null && !isBoardStatus(pickedStatus)) {
10994
+ return c.json(
10995
+ { error: `board_status must be null or one of: ${BOARD_STATUSES.join(", ")}` },
10996
+ 400
10997
+ );
10998
+ }
10765
10999
  const supabase = getSupabaseClient();
10766
11000
  let thread;
10767
11001
  let replayed = false;
@@ -10772,7 +11006,8 @@ function createChatRouter(agent = {}) {
10772
11006
  // opening message at display time (see threadCrumbLabel).
10773
11007
  title: isNonEmptyString(input2.title) ? input2.title.trim() : null,
10774
11008
  author: input2.author,
10775
- client_key: clientKey2
11009
+ client_key: clientKey2,
11010
+ board_status: pickedStatus ?? null
10776
11011
  }).select().single();
10777
11012
  if (threadError) {
10778
11013
  if (!clientKey2 || threadError.code !== "23505") {
@@ -10813,6 +11048,11 @@ function createChatRouter(agent = {}) {
10813
11048
  effort: effortFromRow(reply["effort"])
10814
11049
  });
10815
11050
  }
11051
+ if (!replayed && pickedStatus !== void 0) {
11052
+ void rememberComposerStatus(c, channelId, pickedStatus).catch(
11053
+ (err) => console.error("Remember composer status failed:", err)
11054
+ );
11055
+ }
10816
11056
  if (!replayed && thread["title"] == null) {
10817
11057
  void generateAutoTitle(thread.id, channelId, input2.body).catch(
10818
11058
  (err) => console.error("Auto title failed:", err)
@@ -10820,6 +11060,15 @@ function createChatRouter(agent = {}) {
10820
11060
  }
10821
11061
  return c.json({ ...thread, replies: [reply] }, replayed ? 200 : 201);
10822
11062
  });
11063
+ app.get("/channels/:channelId/composer-status", async (c) => {
11064
+ const channelId = c.req.param("channelId");
11065
+ if (!ownChannelIds(c).has(channelId)) return c.json({ error: "unknown channel" }, 404);
11066
+ const membershipId = await callerMembershipId(c);
11067
+ if (!membershipId) return c.json({ lastStatus: null });
11068
+ const { data: pref, error: error2 } = await getSupabaseClient().from("channel_member_prefs").select("last_board_status").eq("channel_id", channelId).eq("membership_id", membershipId).maybeSingle();
11069
+ if (error2) return c.json({ error: error2.message }, 500);
11070
+ return c.json({ lastStatus: pref?.last_board_status ?? null });
11071
+ });
10823
11072
  app.post("/replies/:replyId/fork", async (c) => {
10824
11073
  const replyId = c.req.param("replyId");
10825
11074
  const input2 = await c.req.json().catch(() => null);
@@ -11035,7 +11284,7 @@ function generateShareId() {
11035
11284
  return randomBytes6(9).toString("base64url");
11036
11285
  }
11037
11286
  async function publishDocShare(input2, readBundle) {
11038
- const bundle = await readBundle({ cwd: input2.cwd, sourcePath: input2.sourcePath });
11287
+ const bundle = await readBundle({ channelId: input2.channelId, cwd: input2.cwd, sourcePath: input2.sourcePath });
11039
11288
  if (bundle.status !== "ok") return bundle;
11040
11289
  const existing = await findDocShareBySource(input2.workspaceId, input2.channelId, input2.sourcePath);
11041
11290
  const shareId = existing?.id ?? generateShareId();
@@ -11695,7 +11944,7 @@ function createTranscribeRouter() {
11695
11944
  // ../api/src/dashboard/avatars-routes.ts
11696
11945
  import { createHash as createHash9, randomUUID as randomUUID4 } from "crypto";
11697
11946
  import { Hono as Hono19 } from "hono";
11698
- import sharp4 from "sharp";
11947
+ import sharp5 from "sharp";
11699
11948
  import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
11700
11949
  var AVATAR_SIZE = 192;
11701
11950
  var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
@@ -11731,7 +11980,7 @@ async function storeAvatar(file, owner) {
11731
11980
  return { error: "avatar must be a PNG or JPEG image" };
11732
11981
  if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
11733
11982
  const original = Buffer.from(await file.arrayBuffer());
11734
- const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
11983
+ const resized = await sharp5(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
11735
11984
  const key = `${avatarKeyPrefix()}${owner}-${randomUUID4()}.png`;
11736
11985
  await getR2Client().send(
11737
11986
  new PutObjectCommand4({
@@ -11909,10 +12158,6 @@ function createMembersRouter() {
11909
12158
  if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
11910
12159
  return c.json({ error: "unknown avatar" }, 400);
11911
12160
  }
11912
- const { plan } = await getWorkspaceBilling(ctx.workspaceId);
11913
- if (plan === "local") {
11914
- return c.json({ error: "The Solo plan is for one person. Upgrade to invite members." }, 403);
11915
- }
11916
12161
  const existing = await getMembershipByEmail(ctx.workspaceId, email);
11917
12162
  if (existing) return c.json({ error: "already a member of this workspace" }, 409);
11918
12163
  let membership;
@@ -13425,8 +13670,95 @@ async function buildThreadUsage(threadTs) {
13425
13670
  }
13426
13671
  }
13427
13672
 
13428
- // ../api/src/dashboard/channel-members-routes.ts
13673
+ // ../api/src/dashboard/channel-greeting.ts
13429
13674
  import { Hono as Hono25 } from "hono";
13675
+ function eligible(ch) {
13676
+ return !!ch && !ch.isDefault && !ch.dmKey && !ch.agentName && !ch.archivedAt;
13677
+ }
13678
+ function buildIntroPrompt(ch) {
13679
+ const automations = [
13680
+ ...(ch.automations?.schedules ?? []).map((s) => s.name),
13681
+ ...(ch.automations?.triggers ?? []).map((t) => t.name)
13682
+ ].filter(Boolean);
13683
+ return [
13684
+ `[channel intro request] Write the one-time intro message for the #${ch.name} channel, into this thread. It is the first thing a visitor sees here, so it must be grounded in what this folder actually contains.`,
13685
+ "",
13686
+ "Read-only: inspect the folder (file listing, README.md, AGENTS.md / CLAUDE.md, docs/, git status, .claude/skills and any SKILL.md files), but do not create, modify or delete anything, and do not run anything with side effects.",
13687
+ "",
13688
+ "Language: match how this project is usually worked on, in this order of evidence: (1) the user messages in this folder's recent agent sessions (Claude Code: ~/.claude/projects/<encoded cwd>/*.jsonl; Codex: ~/.codex/sessions/**/*.jsonl with a matching cwd): read only enough to judge the language, never quote them; (2) the main language of README.md / AGENTS.md / docs/; (3) English. For Chinese use Traditional Chinese (Taiwan), full-width punctuation.",
13689
+ "",
13690
+ "Shape (pyramid, at most ~80 Chinese characters / ~60 English words in total):",
13691
+ "1. One sentence: what this channel/project is, and what you can best help with here.",
13692
+ `2. Three one-line bullets: what already exists in the folder (repo, docs); 1-2 skills that are really available and how to invoke them; one concrete automation suggestion with frequency and content (e.g. "every Monday 9am, summarise last week's commits here").`,
13693
+ "3. End with exactly one line `[BUTTONS: a | b | c]` whose three buttons correspond to the three bullets and read as directly executable requests.",
13694
+ "",
13695
+ "Rules: never invent skills or automations that are not actually there: skip that bullet instead. " + (automations.length ? `This channel already has automations (${automations.join(", ")}): introduce the existing one(s) instead of suggesting a new one. ` : "") + "Do not explain Flyspace itself. If the folder is empty, instead ask what this channel will be used for and offer `[BUTTONS: Help me set up this channel]`. Output only the message itself, nothing else."
13696
+ ].join("\n");
13697
+ }
13698
+ var FALLBACK_BODY = "I could not write this channel's intro automatically. Ask me below and I'll take a look at the folder.\n\n[BUTTONS: Introduce this channel]";
13699
+ function introOpening(channelName) {
13700
+ return `Welcome to #${channelName}. I'm reading this project's folder, docs, skills and automations to work out how I can help here. Below you'll find a short intro and a few first steps you can click to try.`;
13701
+ }
13702
+ function createChannelGreetingRouter(agent) {
13703
+ const app = new Hono25();
13704
+ app.get("/:id/greeting", async (c) => {
13705
+ const ctx = await resolveAuthContext(c);
13706
+ const ch = findChannelById(c.req.param("id"));
13707
+ const pending2 = isWorkspaceOwner(ctx) && eligible(ch) && !ch.greetedAt && (agent.isHostOnline?.(ch.profile) ?? false);
13708
+ if (!pending2) return c.json({ pending: false, threadId: null });
13709
+ const threadId = await ensureIntroThread(ch);
13710
+ return c.json({ pending: !!threadId, threadId });
13711
+ });
13712
+ app.post("/:id/greeting", async (c) => {
13713
+ const ctx = await resolveAuthContext(c);
13714
+ if (!isWorkspaceOwner(ctx)) return c.json({ error: "workspace owner access required" }, 403);
13715
+ const ch = findChannelById(c.req.param("id"));
13716
+ if (!eligible(ch)) return c.json({ error: "channel has no intro" }, 404);
13717
+ const threadId = await ensureIntroThread(ch);
13718
+ if (!threadId) return c.json({ error: "could not create the intro thread" }, 500);
13719
+ if (!await markChannelGreeted(ch.id)) return c.json({ threadId, error: "already greeted" }, 409);
13720
+ try {
13721
+ if (!(agent.isHostOnline?.(ch.profile) ?? false)) throw new Error("host offline");
13722
+ await agent.onChatMessage?.({
13723
+ threadId,
13724
+ // Synthetic: no thread_replies row has this id, so the prompt is
13725
+ // never shown and never counted as thread context.
13726
+ replyId: crypto.randomUUID(),
13727
+ author: ctx.handle,
13728
+ body: buildIntroPrompt(ch)
13729
+ });
13730
+ } catch (err) {
13731
+ console.error(`[channel-greeting] dispatch failed for #${ch.name}:`, err);
13732
+ await getSupabaseClient().from("thread_replies").insert({ thread_id: threadId, author: AGENT_AUTHOR, body: FALLBACK_BODY });
13733
+ }
13734
+ return c.json({ threadId }, 201);
13735
+ });
13736
+ return app;
13737
+ }
13738
+ async function ensureIntroThread(ch) {
13739
+ const supabase = getSupabaseClient();
13740
+ const existing = () => supabase.from("threads").select("id").eq("client_key", ch.id).maybeSingle();
13741
+ const found = await existing();
13742
+ if (found.data) return found.data.id;
13743
+ const { data, error: error2 } = await supabase.from("threads").insert({
13744
+ channel_id: ch.id,
13745
+ title: `Getting started in #${ch.name}`,
13746
+ author: AGENT_AUTHOR,
13747
+ client_key: ch.id
13748
+ }).select("id").single();
13749
+ if (data) {
13750
+ await supabase.from("thread_replies").insert({
13751
+ thread_id: data.id,
13752
+ author: AGENT_AUTHOR,
13753
+ body: introOpening(ch.name)
13754
+ });
13755
+ return data.id;
13756
+ }
13757
+ return error2?.code === "23505" ? (await existing()).data?.id ?? null : null;
13758
+ }
13759
+
13760
+ // ../api/src/dashboard/channel-members-routes.ts
13761
+ import { Hono as Hono26 } from "hono";
13430
13762
  async function isChannelAdmin(ctx, channelId) {
13431
13763
  if (ctx.role === "admin") return true;
13432
13764
  const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
@@ -13435,7 +13767,7 @@ async function isChannelAdmin(ctx, channelId) {
13435
13767
  return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
13436
13768
  }
13437
13769
  function createChannelMembersRouter() {
13438
- const router = new Hono25();
13770
+ const router = new Hono26();
13439
13771
  router.get("/:id/members", async (c) => {
13440
13772
  const ctx = await resolveAuthContext(c);
13441
13773
  if (!ctx) return c.json({ error: "workspace access required" }, 403);
@@ -13505,7 +13837,7 @@ function createChannelMembersRouter() {
13505
13837
  }
13506
13838
 
13507
13839
  // ../api/src/dashboard/memories-routes.ts
13508
- import { Hono as Hono26 } from "hono";
13840
+ import { Hono as Hono27 } from "hono";
13509
13841
  async function readPatch(c) {
13510
13842
  const body = await c.req.json().catch(() => ({}));
13511
13843
  const patch = {};
@@ -13533,7 +13865,7 @@ var requireAgentMemory = async (c, next) => {
13533
13865
  await next();
13534
13866
  };
13535
13867
  function createMemoriesRouter() {
13536
- const router = new Hono26();
13868
+ const router = new Hono27();
13537
13869
  for (const path of [
13538
13870
  "/:id/memories",
13539
13871
  "/:id/memories/*",
@@ -13606,7 +13938,7 @@ function createMemoriesRouter() {
13606
13938
  return router;
13607
13939
  }
13608
13940
  function createScopedMemoriesRouter() {
13609
- const router = new Hono26();
13941
+ const router = new Hono27();
13610
13942
  router.use("*", requireAgentMemory);
13611
13943
  const workspace = (c) => ({
13612
13944
  scope: "workspace",
@@ -13665,14 +13997,244 @@ function createScopedMemoriesRouter() {
13665
13997
  return router;
13666
13998
  }
13667
13999
 
14000
+ // ../api/src/dashboard/session-import-routes.ts
14001
+ import { Hono as Hono28 } from "hono";
14002
+ var MAX_FOLDERS2 = 20;
14003
+ var DAY_MS = 24 * 60 * 60 * 1e3;
14004
+ var IMPORT_RANGES = ["all", "7d", "latest5"];
14005
+ async function ask(agent, req) {
14006
+ const res = await agent.sessionImportRequest?.(req);
14007
+ return res && res.op === req.op ? res : null;
14008
+ }
14009
+ function hostUnavailable2(c, agent) {
14010
+ return c.json(
14011
+ {
14012
+ error: "host offline",
14013
+ reason: agent.getHostCallFailure?.("sessionImportRequest") ?? "offline"
14014
+ },
14015
+ 503
14016
+ );
14017
+ }
14018
+ function pickSessions(sessions2, range, now = Date.now()) {
14019
+ if (range === "latest5") return sessions2.slice(0, 5);
14020
+ if (range === "7d") return sessions2.filter((s) => s.lastUsed >= now - 7 * DAY_MS);
14021
+ return sessions2;
14022
+ }
14023
+ function channelForFolder(folder, channels) {
14024
+ return folder.linkedAs ? channels.find((ch) => ch.name === folder.linkedAs && !ch.archivedAt) : void 0;
14025
+ }
14026
+ async function importRows(channelIds) {
14027
+ if (!channelIds.length) return [];
14028
+ const { data, error: error2 } = await getSupabaseClient().from("session_imports").select("channel_id, agent, session_id, thread_id, last_message_uuid, message_count").in("channel_id", channelIds);
14029
+ if (error2) throw new Error(error2.message);
14030
+ return data ?? [];
14031
+ }
14032
+ async function importableFolders(folders, channels) {
14033
+ const mapped = folders.map((f) => ({ folder: f, channel: channelForFolder(f, channels) }));
14034
+ const rows = await importRows(mapped.flatMap((m) => m.channel ? [m.channel.id] : []));
14035
+ const byKey = new Map(rows.map((r) => [rowKey(r.channel_id, r.agent, r.session_id), r]));
14036
+ return mapped.map(({ folder, channel }) => {
14037
+ const sessions2 = folder.sessions.flatMap((s) => {
14038
+ const row = channel ? byKey.get(rowKey(channel.id, s.agent, s.sessionId)) : void 0;
14039
+ return s.bound && !row ? [] : [{ ...s, row }];
14040
+ });
14041
+ return { folder: { ...folder, sessions: sessions2 }, channel };
14042
+ }).filter((m) => m.folder.sessions.length);
14043
+ }
14044
+ var rowKey = (channelId, agent, sessionId) => `${channelId}:${agent}:${sessionId}`;
14045
+ function replyRows(threadId, messages, author, fallbackAt) {
14046
+ return messages.map((m, i) => ({
14047
+ thread_id: threadId,
14048
+ author: m.role === "user" ? author : AGENT_AUTHOR,
14049
+ body: m.text,
14050
+ created_at: m.at ?? new Date(fallbackAt + i).toISOString()
14051
+ }));
14052
+ }
14053
+ async function syncSession(agent, channelId, session, author, existing) {
14054
+ const supabase = getSupabaseClient();
14055
+ const read = await ask(agent, {
14056
+ op: "read",
14057
+ agent: session.agent,
14058
+ sessionId: session.sessionId,
14059
+ ...existing?.last_message_uuid ? { afterUuid: existing.last_message_uuid } : {}
14060
+ });
14061
+ if (!read) return null;
14062
+ const messages = read.messages ?? [];
14063
+ if (!messages.length) return 0;
14064
+ const last = messages[messages.length - 1];
14065
+ if (existing) {
14066
+ const { error: error3 } = await supabase.from("thread_replies").insert(replyRows(existing.thread_id, messages, author, Date.now()));
14067
+ if (error3) throw new Error(error3.message);
14068
+ await supabase.from("session_imports").update({
14069
+ last_message_uuid: last.uuid,
14070
+ message_count: existing.message_count + messages.length,
14071
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
14072
+ }).eq("channel_id", channelId).eq("agent", session.agent).eq("session_id", session.sessionId);
14073
+ return messages.length;
14074
+ }
14075
+ const startedAt = messages[0].at ?? (/* @__PURE__ */ new Date()).toISOString();
14076
+ const { data: thread, error: threadError } = await supabase.from("threads").insert({
14077
+ channel_id: channelId,
14078
+ title: null,
14079
+ author,
14080
+ imported_agent: session.agent,
14081
+ created_at: startedAt
14082
+ }).select("id").single();
14083
+ if (threadError || !thread) throw new Error(threadError?.message ?? "could not create thread");
14084
+ const threadId = thread.id;
14085
+ const { error: claimError } = await supabase.from("session_imports").insert({
14086
+ channel_id: channelId,
14087
+ agent: session.agent,
14088
+ session_id: session.sessionId,
14089
+ thread_id: threadId,
14090
+ last_message_uuid: last.uuid,
14091
+ message_count: messages.length
14092
+ });
14093
+ if (claimError) {
14094
+ await supabase.from("threads").delete().eq("id", threadId);
14095
+ if (claimError.code === "23505") return 0;
14096
+ throw new Error(claimError.message);
14097
+ }
14098
+ const { error: error2 } = await supabase.from("thread_replies").insert(replyRows(threadId, messages, author, Date.parse(startedAt)));
14099
+ if (error2) throw new Error(error2.message);
14100
+ await ask(agent, {
14101
+ op: "bind",
14102
+ items: [{ channelId, threadId, agent: session.agent, sessionId: session.sessionId }]
14103
+ });
14104
+ return messages.length;
14105
+ }
14106
+ function createSessionImportRouter(agent) {
14107
+ const app = new Hono28();
14108
+ const scanFolders = async () => {
14109
+ const scan = await ask(agent, { op: "scan", slug: getProfile() });
14110
+ return scan && importableFolders(scan.folders, getProfileChannelRegistry());
14111
+ };
14112
+ const channelParam = (c) => getProfileChannelRegistry().find((ch) => ch.id === c.req.param("channelId"));
14113
+ app.get("/scan", async (c) => {
14114
+ const ctx = await resolveAuthContext(c);
14115
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14116
+ const folders = await scanFolders();
14117
+ if (!folders) return hostUnavailable2(c, agent);
14118
+ return c.json({
14119
+ folders: folders.map(({ folder, channel }) => ({
14120
+ ...folder,
14121
+ channel: channel ? { id: channel.id, name: channel.name } : null,
14122
+ sessions: folder.sessions.map(({ row, bound: _bound, ...s }) => ({
14123
+ ...s,
14124
+ imported: !!row
14125
+ }))
14126
+ }))
14127
+ });
14128
+ });
14129
+ app.post("/import", async (c) => {
14130
+ const ctx = await resolveAuthContext(c);
14131
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14132
+ const body = await c.req.json().catch(() => ({}));
14133
+ const picks = (Array.isArray(body.folders) ? body.folders : []).filter((f) => typeof f?.path === "string").map((f) => ({
14134
+ path: f.path,
14135
+ range: IMPORT_RANGES.includes(f.range ?? "") ? f.range : "all"
14136
+ }));
14137
+ if (!picks.length) return c.json({ error: "no folders chosen" }, 400);
14138
+ if (picks.length > MAX_FOLDERS2) return c.json({ error: `at most ${MAX_FOLDERS2} folders` }, 400);
14139
+ const folders = await scanFolders();
14140
+ if (!folders) return hostUnavailable2(c, agent);
14141
+ const offered = new Map(folders.map((m) => [m.folder.path, m]));
14142
+ if (picks.some((p) => !offered.has(p.path)))
14143
+ return c.json({ error: "folder was not offered by the scan" }, 400);
14144
+ const needLink = picks.filter((p) => !offered.get(p.path).channel);
14145
+ if (needLink.length && !isWorkspaceOwner(ctx)) {
14146
+ return c.json({ error: "workspace owner access required to add new folders" }, 403);
14147
+ }
14148
+ const taken = new Set(getProfileChannelRegistry().map((ch) => ch.name));
14149
+ const items = needLink.map((p) => ({
14150
+ name: uniqueChannelName(offered.get(p.path).folder.name, taken),
14151
+ path: p.path
14152
+ }));
14153
+ const linked = items.length ? await agent.linkProjectFolders?.(getProfile(), items) : [];
14154
+ if (!linked) return hostUnavailable2(c, agent);
14155
+ const created = /* @__PURE__ */ new Map();
14156
+ for (const [i, l] of linked.entries())
14157
+ created.set(items[i].path, await createChannel({ name: l.name, cwd: l.cwd }));
14158
+ const results = [];
14159
+ for (const p of picks) {
14160
+ const { folder, channel: existing } = offered.get(p.path);
14161
+ const channel = existing ?? created.get(p.path);
14162
+ let threads = 0;
14163
+ let messages = 0;
14164
+ for (const s of pickSessions(folder.sessions, p.range)) {
14165
+ if (s.row) continue;
14166
+ const n = await syncSession(agent, channel.id, s, ctx.handle, void 0);
14167
+ if (n === null) return hostUnavailable2(c, agent);
14168
+ if (n > 0) threads += 1;
14169
+ messages += n;
14170
+ }
14171
+ results.push({
14172
+ path: p.path,
14173
+ channel: { id: channel.id, name: channel.name },
14174
+ created: !existing,
14175
+ threads,
14176
+ messages
14177
+ });
14178
+ }
14179
+ return c.json({ results }, 201);
14180
+ });
14181
+ app.get("/channels/:channelId", async (c) => {
14182
+ const ctx = await resolveAuthContext(c);
14183
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14184
+ const channel = channelParam(c);
14185
+ if (!channel) return c.json({ error: "channel not found" }, 404);
14186
+ const folders = await scanFolders();
14187
+ if (!folders) return hostUnavailable2(c, agent);
14188
+ const folder = folders.find((m) => m.channel?.id === channel.id)?.folder;
14189
+ const sessions2 = [];
14190
+ for (const { row, bound: _bound, ...s } of folder?.sessions ?? []) {
14191
+ let newMessages = 0;
14192
+ if (row) {
14193
+ const read = await ask(agent, {
14194
+ op: "read",
14195
+ agent: s.agent,
14196
+ sessionId: s.sessionId,
14197
+ ...row.last_message_uuid ? { afterUuid: row.last_message_uuid } : {}
14198
+ });
14199
+ newMessages = read?.messages?.length ?? 0;
14200
+ }
14201
+ const status = !row ? "not-imported" : newMessages ? "new" : "synced";
14202
+ sessions2.push({ ...s, threadId: row?.thread_id ?? null, status, newMessages });
14203
+ }
14204
+ return c.json({ folder: folder?.path ?? null, sessions: sessions2 });
14205
+ });
14206
+ app.post("/channels/:channelId/sync", async (c) => {
14207
+ const ctx = await resolveAuthContext(c);
14208
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14209
+ const channel = channelParam(c);
14210
+ if (!channel) return c.json({ error: "channel not found" }, 404);
14211
+ const body = await c.req.json().catch(() => ({}));
14212
+ const only = Array.isArray(body.sessionIds) ? new Set(body.sessionIds.filter((s) => typeof s === "string")) : null;
14213
+ const folders = await scanFolders();
14214
+ if (!folders) return hostUnavailable2(c, agent);
14215
+ const folder = folders.find((m) => m.channel?.id === channel.id)?.folder;
14216
+ let threads = 0;
14217
+ let messages = 0;
14218
+ for (const s of folder?.sessions ?? []) {
14219
+ if (only && !only.has(s.sessionId)) continue;
14220
+ const n = await syncSession(agent, channel.id, s, ctx.handle, s.row);
14221
+ if (n === null) return hostUnavailable2(c, agent);
14222
+ if (!s.row && n > 0) threads += 1;
14223
+ messages += n;
14224
+ }
14225
+ return c.json({ threads, messages });
14226
+ });
14227
+ return app;
14228
+ }
14229
+
13668
14230
  // ../api/src/dashboard/host-status-signal.ts
13669
14231
  var HOST_STATUS_CHANNEL = "host-status";
13670
14232
  var channelReady = null;
13671
14233
  function getHostStatusChannel() {
13672
- channelReady ??= new Promise((resolve7) => {
14234
+ channelReady ??= new Promise((resolve8) => {
13673
14235
  const channel = getSupabaseClient().channel(HOST_STATUS_CHANNEL);
13674
14236
  channel.subscribe((state) => {
13675
- if (state === "SUBSCRIBED") resolve7(channel);
14237
+ if (state === "SUBSCRIBED") resolve8(channel);
13676
14238
  });
13677
14239
  });
13678
14240
  return channelReady;
@@ -13837,6 +14399,8 @@ function registerDataRoutes(api, agent = {}) {
13837
14399
  });
13838
14400
  api.route("/channels", createDocShareWriteRouter(agent));
13839
14401
  api.route("/onboarding", createOnboardingRouter(agent));
14402
+ api.route("/session-import", createSessionImportRouter(agent));
14403
+ api.route("/channels", createChannelGreetingRouter(agent));
13840
14404
  api.route("/channels", createChannelMembersRouter());
13841
14405
  api.route("/channels", createMemoriesRouter());
13842
14406
  api.route("/memories", createScopedMemoriesRouter());
@@ -14080,7 +14644,7 @@ function registerDataRoutes(api, agent = {}) {
14080
14644
  return c.json({ error: "logo must be 2MB or smaller" }, 400);
14081
14645
  }
14082
14646
  const original = Buffer.from(await file.arrayBuffer());
14083
- const resized = await sharp5(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
14647
+ const resized = await sharp6(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
14084
14648
  const logo = `data:image/png;base64,${resized.toString("base64")}`;
14085
14649
  return c.json(await saveWorkspaceBranding({ logo }));
14086
14650
  });
@@ -14090,7 +14654,7 @@ function registerDataRoutes(api, agent = {}) {
14090
14654
  // ../api/src/dashboard/device-auth-routes.ts
14091
14655
  import { verify } from "crypto";
14092
14656
  import { getConnInfo } from "@hono/node-server/conninfo";
14093
- import { Hono as Hono27 } from "hono";
14657
+ import { Hono as Hono29 } from "hono";
14094
14658
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14095
14659
  function cleanText(value, max) {
14096
14660
  if (typeof value !== "string") return null;
@@ -14138,7 +14702,7 @@ async function boundWorkspaceIds(publicKey, userId) {
14138
14702
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
14139
14703
  }
14140
14704
  function createDeviceAuthRouter() {
14141
- const app = new Hono27();
14705
+ const app = new Hono29();
14142
14706
  app.post("/code", async (c) => {
14143
14707
  const body = await readBody(c);
14144
14708
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -14457,7 +15021,7 @@ var workspaceRef = () => ({ kind: "workspace" });
14457
15021
  var channelRef = (c) => ({ kind: "channel", channelId: c.req.param("id") });
14458
15022
 
14459
15023
  // ../api/src/dashboard/output-styles-routes.ts
14460
- import { Hono as Hono28 } from "hono";
15024
+ import { Hono as Hono30 } from "hono";
14461
15025
  function requireAdmin3(auth) {
14462
15026
  return auth?.role === "admin" ? null : "admin access required";
14463
15027
  }
@@ -14477,7 +15041,7 @@ function failure(c, err) {
14477
15041
  return c.json({ error: message }, err instanceof OutputStyleConflictError ? 409 : 500);
14478
15042
  }
14479
15043
  function createOutputStylesRouter() {
14480
- const app = new Hono28();
15044
+ const app = new Hono30();
14481
15045
  app.get("/", (c) => {
14482
15046
  const auth = c.get("auth");
14483
15047
  const denied = requireAdmin3(auth);
@@ -14569,7 +15133,7 @@ function createOutputStylesRouter() {
14569
15133
  return app;
14570
15134
  }
14571
15135
  async function handleOutputStylesRequest(req) {
14572
- const app = new Hono28();
15136
+ const app = new Hono30();
14573
15137
  app.use("*", async (c, next) => {
14574
15138
  c.set("auth", { role: "admin" });
14575
15139
  await next();
@@ -14590,7 +15154,7 @@ import { streamSSE } from "hono/streaming";
14590
15154
 
14591
15155
  // ../api/src/dashboard/api.ts
14592
15156
  function createGatedRouter(agent, register) {
14593
- const api = new Hono29();
15157
+ const api = new Hono31();
14594
15158
  api.use("*", async (c, next) => {
14595
15159
  const known = await listKnownWorkspaces();
14596
15160
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -14639,11 +15203,11 @@ function createGatedRouter(agent, register) {
14639
15203
 
14640
15204
  // src/dashboard/host-api.ts
14641
15205
  import { existsSync as existsSync30, readFileSync as readFileSync24, statSync as statSync8 } from "fs";
14642
- import sharp6 from "sharp";
15206
+ import sharp7 from "sharp";
14643
15207
 
14644
15208
  // src/dashboard/channels-routes.ts
14645
15209
  import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
14646
- import { Hono as Hono30 } from "hono";
15210
+ import { Hono as Hono32 } from "hono";
14647
15211
  function coerceAccess(raw) {
14648
15212
  if (raw === "open" || raw === "*" || raw === "all") return raw;
14649
15213
  if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) return raw;
@@ -14706,7 +15270,7 @@ function toResponse3(channel) {
14706
15270
  };
14707
15271
  }
14708
15272
  function createChannelWriteRouter() {
14709
- const app = new Hono30();
15273
+ const app = new Hono32();
14710
15274
  app.post("/", async (c) => {
14711
15275
  const body = await c.req.json().catch(() => ({}));
14712
15276
  const name = typeof body["name"] === "string" ? body["name"].trim() : "";
@@ -14846,7 +15410,7 @@ import {
14846
15410
  } from "fs";
14847
15411
  import { Readable } from "stream";
14848
15412
  import { basename as basename8, dirname as dirname8, extname, join as join23, relative, resolve as resolve5, sep } from "path";
14849
- import { Hono as Hono31 } from "hono";
15413
+ import { Hono as Hono33 } from "hono";
14850
15414
  import { streamSSE as streamSSE2 } from "hono/streaming";
14851
15415
 
14852
15416
  // src/dashboard/docs-watcher.ts
@@ -15148,7 +15712,7 @@ function needsRefresh(expiresAt, now = Date.now()) {
15148
15712
  if (!expiresAt) return false;
15149
15713
  return new Date(expiresAt).getTime() - now < REFRESH_SKEW_MS;
15150
15714
  }
15151
- var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
15715
+ var sleep3 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
15152
15716
  async function refreshConnection(params) {
15153
15717
  const { workspaceId, connection, provider } = params;
15154
15718
  const deps = params.deps ?? {};
@@ -15482,7 +16046,7 @@ ${req.user}`,
15482
16046
  };
15483
16047
  }
15484
16048
  function run3(inv, cwd) {
15485
- return new Promise((resolve7) => {
16049
+ return new Promise((resolve8) => {
15486
16050
  const child = spawn4(inv.command, inv.args, {
15487
16051
  cwd,
15488
16052
  env: inv.env,
@@ -15496,14 +16060,14 @@ function run3(inv, cwd) {
15496
16060
  child.on("error", (err) => {
15497
16061
  clearTimeout(timer2);
15498
16062
  console.error(`[memory-extraction] ${inv.command} failed to start:`, err.message);
15499
- resolve7({ ok: false });
16063
+ resolve8({ ok: false });
15500
16064
  });
15501
16065
  child.on("close", (code) => {
15502
16066
  clearTimeout(timer2);
15503
16067
  if (code !== 0) {
15504
16068
  const detail = (stderr || stdout).trim().split("\n").at(-1) ?? "";
15505
16069
  console.error(`[memory-extraction] ${inv.command} exited ${code}: ${detail.slice(0, 200)}`);
15506
- resolve7({ ok: false });
16070
+ resolve8({ ok: false });
15507
16071
  return;
15508
16072
  }
15509
16073
  let raw = stdout;
@@ -15514,7 +16078,7 @@ function run3(inv, cwd) {
15514
16078
  raw = "";
15515
16079
  }
15516
16080
  }
15517
- resolve7({ ok: true, raw });
16081
+ resolve8({ ok: true, raw });
15518
16082
  });
15519
16083
  child.stdin.end(inv.stdin);
15520
16084
  });
@@ -15576,7 +16140,7 @@ Rules:
15576
16140
  - No preamble like "Summary:" \u2014 start directly with the summary text.`;
15577
16141
  var HAIKU_TIMEOUT_MS = 6e4;
15578
16142
  function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
15579
- return new Promise((resolve7, reject) => {
16143
+ return new Promise((resolve8, reject) => {
15580
16144
  const args = [
15581
16145
  "--print",
15582
16146
  "--model",
@@ -15627,7 +16191,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
15627
16191
  reject(new Error("claude -p --model haiku returned empty output"));
15628
16192
  return;
15629
16193
  }
15630
- resolve7(text);
16194
+ resolve8(text);
15631
16195
  });
15632
16196
  });
15633
16197
  }
@@ -16023,6 +16587,15 @@ var ChatAgent = class {
16023
16587
  * restart picks up edits. */
16024
16588
  workspaceRoots = null;
16025
16589
  /** Fire-and-forget entry point, called by chat-routes after every write. */
16590
+ /** The CLI of the session already bound to this thread, if any. */
16591
+ boundRunner(channelId, threadId) {
16592
+ const store = this.openSessionStore();
16593
+ try {
16594
+ return store.getSession(`chat:${channelId}`, threadId)?.runner;
16595
+ } finally {
16596
+ store.close();
16597
+ }
16598
+ }
16026
16599
  handleMessage(msg) {
16027
16600
  if (msg.author === AGENT_AUTHOR) return;
16028
16601
  this.seenReplies.add(msg.replyId);
@@ -16403,7 +16976,7 @@ var ChatAgent = class {
16403
16976
  );
16404
16977
  ensureLogicalCwdDir(baseConfig.cwd);
16405
16978
  const pickedModel = msg.model?.trim();
16406
- let activeRunner = pickedModel ? runnerForModel(pickedModel) : defaultRunner;
16979
+ let activeRunner = pickedModel ? runnerForModel(pickedModel) : this.boundRunner(thread.channel_id, msg.threadId) ?? defaultRunner;
16407
16980
  let channelConfig = pickedModel ? {
16408
16981
  ...baseConfig,
16409
16982
  ...activeRunner === "codex" ? { model: pickedModel, codexModel: pickedModel } : { model: pickedModel }
@@ -16709,22 +17282,22 @@ ${msg.body}` : msg.body;
16709
17282
  let ready;
16710
17283
  try {
16711
17284
  channel = this.supabase().channel(`agent-thinking:${threadId}`);
16712
- ready = new Promise((resolve7) => {
17285
+ ready = new Promise((resolve8) => {
16713
17286
  const timeout = setTimeout(() => {
16714
17287
  console.error(`[chat-agent] thinking-channel ${threadId}: subscribe timed out after 10s`);
16715
- resolve7(false);
17288
+ resolve8(false);
16716
17289
  }, 1e4);
16717
17290
  channel.subscribe((status, err) => {
16718
17291
  if (status === "SUBSCRIBED") {
16719
17292
  clearTimeout(timeout);
16720
- resolve7(true);
17293
+ resolve8(true);
16721
17294
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
16722
17295
  console.error(
16723
17296
  `[chat-agent] thinking-channel ${threadId}: ${status}`,
16724
17297
  err?.message ?? ""
16725
17298
  );
16726
17299
  clearTimeout(timeout);
16727
- resolve7(false);
17300
+ resolve8(false);
16728
17301
  }
16729
17302
  });
16730
17303
  });
@@ -16771,19 +17344,19 @@ ${msg.body}` : msg.body;
16771
17344
  let ready;
16772
17345
  try {
16773
17346
  channel = this.supabase().channel("channel-activity");
16774
- ready = new Promise((resolve7) => {
17347
+ ready = new Promise((resolve8) => {
16775
17348
  const timeout = setTimeout(() => {
16776
17349
  console.error("[chat-agent] channel-activity: subscribe timed out after 10s");
16777
- resolve7(false);
17350
+ resolve8(false);
16778
17351
  }, 1e4);
16779
17352
  channel.subscribe((status, err) => {
16780
17353
  if (status === "SUBSCRIBED") {
16781
17354
  clearTimeout(timeout);
16782
- resolve7(true);
17355
+ resolve8(true);
16783
17356
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
16784
17357
  console.error(`[chat-agent] channel-activity: ${status}`, err?.message ?? "");
16785
17358
  clearTimeout(timeout);
16786
- resolve7(false);
17359
+ resolve8(false);
16787
17360
  }
16788
17361
  });
16789
17362
  });
@@ -17727,7 +18300,7 @@ function deleteDocEntry(root, path) {
17727
18300
  };
17728
18301
  }
17729
18302
  function createDocsRouter({ gate = requireDocsAccess } = {}) {
17730
- const app = new Hono31();
18303
+ const app = new Hono33();
17731
18304
  app.use("/:id/docs", gate);
17732
18305
  app.use("/:id/docs/*", gate);
17733
18306
  app.get("/:id/docs", (c) => {
@@ -18065,7 +18638,7 @@ function withDocLinkBridge(html) {
18065
18638
  // src/dashboard/skills-routes-host.ts
18066
18639
  import { existsSync as existsSync27, statSync as statSync7 } from "fs";
18067
18640
  import { dirname as dirname9 } from "path";
18068
- import { Hono as Hono32 } from "hono";
18641
+ import { Hono as Hono34 } from "hono";
18069
18642
  function workspaceScope() {
18070
18643
  const claudeConfigDir = profileClaudeConfigDir();
18071
18644
  const cwd = defaultClaudeCwd();
@@ -18171,12 +18744,12 @@ var diskBackend = {
18171
18744
  write: writeSkillAndSync
18172
18745
  };
18173
18746
  function createWorkspaceSkillsRouter() {
18174
- const app = new Hono32();
18747
+ const app = new Hono34();
18175
18748
  mountSkillRoutes(app, "/skills", workspaceRef, diskBackend);
18176
18749
  return app;
18177
18750
  }
18178
18751
  function createChannelSkillsRouter() {
18179
- const app = new Hono32();
18752
+ const app = new Hono34();
18180
18753
  mountSkillRoutes(app, "/:id/skill-files", channelRef, diskBackend);
18181
18754
  return app;
18182
18755
  }
@@ -18311,7 +18884,7 @@ import { randomUUID as randomUUID5 } from "crypto";
18311
18884
  import { chmodSync, existsSync as existsSync29, mkdirSync as mkdirSync12, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
18312
18885
  import { createRequire } from "module";
18313
18886
  import { dirname as dirname10, join as join25 } from "path";
18314
- import { Hono as Hono33 } from "hono";
18887
+ import { Hono as Hono35 } from "hono";
18315
18888
  var SESSION_TTL_MS = 10 * 6e4;
18316
18889
  var URL_WAIT_MS = 3e4;
18317
18890
  var TOKEN_WAIT_MS = 6e4;
@@ -18360,13 +18933,13 @@ function track(session) {
18360
18933
  return [id, s];
18361
18934
  }
18362
18935
  function waitFor(s, parse, ms) {
18363
- return new Promise((resolve7) => {
18936
+ return new Promise((resolve8) => {
18364
18937
  const started = Date.now();
18365
18938
  const tick = setInterval(() => {
18366
18939
  const v = parse();
18367
18940
  if (v !== void 0 || s.status !== "pending" || Date.now() - started > ms) {
18368
18941
  clearInterval(tick);
18369
- resolve7(v);
18942
+ resolve8(v);
18370
18943
  }
18371
18944
  }, 200);
18372
18945
  });
@@ -18468,7 +19041,7 @@ function failure2(c, err) {
18468
19041
  return c.json({ error: err.message }, 500);
18469
19042
  }
18470
19043
  function createProviderLoginRouter() {
18471
- const app = new Hono33();
19044
+ const app = new Hono35();
18472
19045
  app.use("*", async (c, next) => {
18473
19046
  const denied = requireAdmin4(c.get("auth"));
18474
19047
  if (denied) return c.json({ error: denied }, 403);
@@ -18512,7 +19085,7 @@ function createProviderLoginRouter() {
18512
19085
  return app;
18513
19086
  }
18514
19087
  async function handleProviderLoginRequest(req) {
18515
- const app = new Hono33();
19088
+ const app = new Hono35();
18516
19089
  app.use("*", async (c, next) => {
18517
19090
  c.set("auth", { role: "admin" });
18518
19091
  await next();
@@ -18620,7 +19193,7 @@ function registerHostRoutes(api) {
18620
19193
  return c.json({ error: "avatar must be 2MB or smaller" }, 400);
18621
19194
  }
18622
19195
  const original = Buffer.from(await file.arrayBuffer());
18623
- const resized = await sharp6(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
19196
+ const resized = await sharp7(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
18624
19197
  const avatar = `data:image/png;base64,${resized.toString("base64")}`;
18625
19198
  const name = c.req.param("name");
18626
19199
  if (name === BOT_PROFILE_KEY) return c.json(await saveBotIdentity({ avatar }));
@@ -18664,18 +19237,253 @@ function createHostApiRouter(agent = {}) {
18664
19237
 
18665
19238
  // src/dashboard/agent-bridge-host.ts
18666
19239
  import { sign as signRaw, timingSafeEqual as timingSafeEqual2 } from "crypto";
18667
- import { existsSync as existsSync31, readFileSync as readFileSync26 } from "fs";
19240
+ import { existsSync as existsSync32, readFileSync as readFileSync27 } from "fs";
18668
19241
  import dotenv2 from "dotenv";
18669
19242
  import { WebSocket as WebSocket2 } from "ws";
18670
19243
 
19244
+ // src/dashboard/session-import-host.ts
19245
+ import {
19246
+ closeSync as closeSync3,
19247
+ existsSync as existsSync31,
19248
+ openSync as openSync3,
19249
+ readdirSync as readdirSync6,
19250
+ readFileSync as readFileSync25,
19251
+ readSync as readSync3,
19252
+ realpathSync as realpathSync5
19253
+ } from "fs";
19254
+ import { basename as basename9, join as join26, resolve as resolve7 } from "path";
19255
+ var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
19256
+ var HEAD_BYTES = 64 * 1024;
19257
+ var TITLE_MAX = 80;
19258
+ function readHead2(file) {
19259
+ let fd;
19260
+ try {
19261
+ fd = openSync3(file, "r");
19262
+ const buf = Buffer.alloc(HEAD_BYTES);
19263
+ return buf.toString("utf-8", 0, readSync3(fd, buf, 0, buf.length, 0));
19264
+ } catch {
19265
+ return "";
19266
+ } finally {
19267
+ if (fd !== void 0) closeSync3(fd);
19268
+ }
19269
+ }
19270
+ function parseLines(text) {
19271
+ const out = [];
19272
+ for (const line of text.split("\n")) {
19273
+ if (!line) continue;
19274
+ try {
19275
+ out.push(JSON.parse(line));
19276
+ } catch {
19277
+ }
19278
+ }
19279
+ return out;
19280
+ }
19281
+ function cleanText2(text) {
19282
+ const t = text.trim();
19283
+ if (!t || t.startsWith("<")) return null;
19284
+ return t;
19285
+ }
19286
+ function blocksText(content, kinds) {
19287
+ if (typeof content === "string") return cleanText2(content);
19288
+ if (!Array.isArray(content)) return null;
19289
+ const parts = content.filter(
19290
+ (b) => kinds.includes(b?.type) && typeof b.text === "string"
19291
+ ).map((b) => b.text);
19292
+ return parts.length ? cleanText2(parts.join("\n\n")) : null;
19293
+ }
19294
+ function boundSessionIds() {
19295
+ if (!existsSync31(paths.DB_FILE)) return /* @__PURE__ */ new Set();
19296
+ const store = new SessionStore(paths.DB_FILE);
19297
+ try {
19298
+ return new Set(store.listSessions().map((s) => s.sessionId));
19299
+ } finally {
19300
+ store.close();
19301
+ }
19302
+ }
19303
+ function claudeMessages(lines) {
19304
+ const out = [];
19305
+ for (const o of lines) {
19306
+ if (o["type"] !== "user" && o["type"] !== "assistant" || o["isMeta"] || o["isSidechain"])
19307
+ continue;
19308
+ if (o["entrypoint"] === "sdk-cli") continue;
19309
+ const role = o["type"];
19310
+ const text = blocksText(o["message"]?.content, ["text"]);
19311
+ if (!text || typeof o["uuid"] !== "string") continue;
19312
+ const at = typeof o["timestamp"] === "string" ? o["timestamp"] : null;
19313
+ const last = out[out.length - 1];
19314
+ if (role === "assistant" && last?.role === "assistant") {
19315
+ last.text += `
19316
+
19317
+ ${text}`;
19318
+ last.uuid = o["uuid"];
19319
+ } else {
19320
+ out.push({ uuid: o["uuid"], role, text, at });
19321
+ }
19322
+ }
19323
+ return out;
19324
+ }
19325
+ function codexMessages(lines) {
19326
+ const out = [];
19327
+ lines.forEach((o, i) => {
19328
+ const p = o["payload"];
19329
+ if (o["type"] !== "response_item" || p?.type !== "message") return;
19330
+ if (p.role !== "user" && p.role !== "assistant") return;
19331
+ const text = blocksText(p.content, ["input_text", "output_text"]);
19332
+ if (!text) return;
19333
+ out.push({
19334
+ uuid: `line:${i}`,
19335
+ role: p.role,
19336
+ text,
19337
+ at: typeof o["timestamp"] === "string" ? o["timestamp"] : null
19338
+ });
19339
+ });
19340
+ return out;
19341
+ }
19342
+ function claudeSummary(file, mtime) {
19343
+ const head = parseLines(readHead2(file));
19344
+ if (head.some((o) => o["entrypoint"] === "sdk-cli")) return null;
19345
+ const first = claudeMessages(head).find((m) => m.role === "user");
19346
+ if (!first) return null;
19347
+ return {
19348
+ agent: "claude",
19349
+ sessionId: basename9(file, ".jsonl"),
19350
+ title: first.text.slice(0, TITLE_MAX),
19351
+ lastUsed: mtime
19352
+ };
19353
+ }
19354
+ function codexSummary(file, mtime) {
19355
+ const head = parseLines(readHead2(file));
19356
+ const meta = head.find((o) => o["type"] === "session_meta")?.["payload"];
19357
+ if (!meta?.id || !meta.cwd || meta.originator === "codex_exec") return null;
19358
+ const first = codexMessages(head).find((m) => m.role === "user");
19359
+ if (!first) return null;
19360
+ return {
19361
+ cwd: meta.cwd,
19362
+ session: {
19363
+ agent: "codex",
19364
+ sessionId: meta.id,
19365
+ title: first.text.slice(0, TITLE_MAX),
19366
+ lastUsed: mtime
19367
+ }
19368
+ };
19369
+ }
19370
+ function enabledAgents() {
19371
+ return new Set(
19372
+ detectRunners().filter((r) => r.installed && r.loggedIn).map((r) => r.id)
19373
+ );
19374
+ }
19375
+ function scanSessionFolders(slug) {
19376
+ const agents = enabledAgents();
19377
+ const bound = boundSessionIds();
19378
+ const since2 = Date.now() - MAX_AGE_MS;
19379
+ const byPath = /* @__PURE__ */ new Map();
19380
+ const add = (cwd, s) => {
19381
+ const path = resolve7(cwd);
19382
+ if (isExcluded(path)) return;
19383
+ byPath.set(path, [
19384
+ ...byPath.get(path) ?? [],
19385
+ bound.has(s.sessionId) ? { ...s, bound: true } : s
19386
+ ]);
19387
+ };
19388
+ if (agents.has("claude")) {
19389
+ const root = join26(claudeDir(), "projects");
19390
+ let dirs = [];
19391
+ try {
19392
+ dirs = readdirSync6(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
19393
+ } catch {
19394
+ }
19395
+ for (const dir of dirs) {
19396
+ const files = jsonlFiles(join26(root, dir), 0).filter((f) => f.mtime >= since2);
19397
+ let cwd = null;
19398
+ for (const f of files) {
19399
+ cwd ??= cwdFromTranscript(f.file);
19400
+ if (cwd) break;
19401
+ }
19402
+ if (!cwd) continue;
19403
+ for (const f of files) {
19404
+ const s = claudeSummary(f.file, f.mtime);
19405
+ if (s) add(cwd, s);
19406
+ }
19407
+ }
19408
+ }
19409
+ if (agents.has("codex")) {
19410
+ for (const f of jsonlFiles(join26(codexDir(), "sessions"), 4)) {
19411
+ if (f.mtime < since2) continue;
19412
+ const s = codexSummary(f.file, f.mtime);
19413
+ if (s) add(s.cwd, s.session);
19414
+ }
19415
+ }
19416
+ const linked = linkedTargets(slug);
19417
+ const folders = [...byPath].map(([path, sessions2]) => {
19418
+ let real = path;
19419
+ try {
19420
+ real = realpathSync5(path);
19421
+ } catch {
19422
+ }
19423
+ sessions2.sort((a, b) => b.lastUsed - a.lastUsed);
19424
+ return {
19425
+ path,
19426
+ name: basename9(path),
19427
+ lastUsed: sessions2[0].lastUsed,
19428
+ sessions: sessions2,
19429
+ ...linked.has(real) ? { linkedAs: linked.get(real) } : {}
19430
+ };
19431
+ });
19432
+ return folders.sort((a, b) => b.lastUsed - a.lastUsed);
19433
+ }
19434
+ function findTranscript(agent, sessionId) {
19435
+ if (!/^[A-Za-z0-9-]+$/.test(sessionId)) return null;
19436
+ if (agent === "claude") {
19437
+ const root = join26(claudeDir(), "projects");
19438
+ try {
19439
+ for (const e of readdirSync6(root, { withFileTypes: true })) {
19440
+ const file = join26(root, e.name, `${sessionId}.jsonl`);
19441
+ if (e.isDirectory() && existsSync31(file)) return file;
19442
+ }
19443
+ } catch {
19444
+ }
19445
+ return null;
19446
+ }
19447
+ return jsonlFiles(join26(codexDir(), "sessions"), 4).find((f) => f.file.endsWith(`${sessionId}.jsonl`))?.file ?? null;
19448
+ }
19449
+ function readSessionMessages(agent, sessionId, afterUuid) {
19450
+ const file = findTranscript(agent, sessionId);
19451
+ if (!file) return null;
19452
+ const lines = parseLines(readFileSync25(file, "utf-8"));
19453
+ const messages = agent === "claude" ? claudeMessages(lines) : codexMessages(lines);
19454
+ if (!afterUuid) return messages;
19455
+ const idx = messages.findIndex((m) => m.uuid === afterUuid);
19456
+ return idx === -1 ? [] : messages.slice(idx + 1);
19457
+ }
19458
+ function bindSessions(items) {
19459
+ const store = new SessionStore(paths.DB_FILE);
19460
+ try {
19461
+ for (const i of items)
19462
+ store.saveSession(`chat:${i.channelId}`, i.threadId, i.sessionId, i.agent);
19463
+ } finally {
19464
+ store.close();
19465
+ }
19466
+ return items.length;
19467
+ }
19468
+ function handleSessionImportRequest(req) {
19469
+ switch (req.op) {
19470
+ case "scan":
19471
+ return { op: "scan", folders: scanSessionFolders(req.slug) };
19472
+ case "read":
19473
+ return { op: "read", messages: readSessionMessages(req.agent, req.sessionId, req.afterUuid) };
19474
+ case "bind":
19475
+ return { op: "bind", bound: bindSessions(req.items) };
19476
+ }
19477
+ }
19478
+
18671
19479
  // src/dashboard/docs-relay-host.ts
18672
- import { Hono as Hono34 } from "hono";
19480
+ import { Hono as Hono36 } from "hono";
18673
19481
  var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
18674
19482
  async function handleDocsRequest(req) {
18675
19483
  return runRelayed(req, (app) => app.route("/channels", createDocsRouter({ gate: async (_c, next) => next() })));
18676
19484
  }
18677
19485
  async function runRelayed(req, register) {
18678
- const app = new Hono34();
19486
+ const app = new Hono36();
18679
19487
  app.use("*", async (c, next) => {
18680
19488
  c.set("auth", req.auth);
18681
19489
  await next();
@@ -18698,7 +19506,7 @@ async function handleHostRequest(req) {
18698
19506
  }
18699
19507
 
18700
19508
  // src/dashboard/doc-share-publisher.ts
18701
- import { readFileSync as readFileSync25, statSync as statSync9 } from "fs";
19509
+ import { readFileSync as readFileSync26, statSync as statSync9 } from "fs";
18702
19510
  import { extname as extname2, posix } from "path";
18703
19511
  var MAX_ASSET_BYTES = 20 * 1024 * 1024;
18704
19512
  var MAX_CONTENT_BYTES2 = 2 * 1024 * 1024;
@@ -18813,7 +19621,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
18813
19621
  }
18814
19622
  if (!stat.isFile()) return { status: "not-found" };
18815
19623
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
18816
- const source = readFileSync25(abs, "utf8");
19624
+ const source = readFileSync26(abs, "utf8");
18817
19625
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
18818
19626
  if (kind === "excalidraw") {
18819
19627
  try {
@@ -18884,7 +19692,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
18884
19692
  let text = null;
18885
19693
  try {
18886
19694
  const size = statSync9(docAbs).size;
18887
- if (size <= budget) text = readFileSync25(docAbs, "utf8");
19695
+ if (size <= budget) text = readFileSync26(docAbs, "utf8");
18888
19696
  } catch {
18889
19697
  continue;
18890
19698
  }
@@ -18901,7 +19709,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
18901
19709
  let body;
18902
19710
  try {
18903
19711
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
18904
- body = readFileSync25(resolved2.abs);
19712
+ body = readFileSync26(resolved2.abs);
18905
19713
  } catch {
18906
19714
  continue;
18907
19715
  }
@@ -18929,13 +19737,13 @@ function readDocShareBundle({ cwd, sourcePath }) {
18929
19737
 
18930
19738
  // src/dashboard/host-upgrade.ts
18931
19739
  import { spawn as spawn7 } from "child_process";
18932
- import { mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
18933
- import { join as join26 } from "path";
19740
+ import { mkdirSync as mkdirSync13, openSync as openSync4 } from "fs";
19741
+ import { join as join27 } from "path";
18934
19742
  var running = false;
18935
19743
  function startHostUpgrade() {
18936
19744
  if (running) return { started: false };
18937
19745
  mkdirSync13(paths.LOG_DIR, { recursive: true });
18938
- const log5 = openSync3(join26(paths.LOG_DIR, "host-upgrade.log"), "a");
19746
+ const log5 = openSync4(join27(paths.LOG_DIR, "host-upgrade.log"), "a");
18939
19747
  const child = spawn7(process.execPath, [process.argv[1] ?? "", "upgrade"], {
18940
19748
  detached: true,
18941
19749
  stdio: ["ignore", log5, log5]
@@ -18956,7 +19764,7 @@ function dataSocketUrl() {
18956
19764
  return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
18957
19765
  }
18958
19766
  function listLocalSessions() {
18959
- if (!existsSync31(paths.DB_FILE)) return [];
19767
+ if (!existsSync32(paths.DB_FILE)) return [];
18960
19768
  const store = new SessionStore(paths.DB_FILE);
18961
19769
  try {
18962
19770
  return store.listSessions();
@@ -18976,9 +19784,12 @@ function localAgentCallbacks(chatAgent) {
18976
19784
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
18977
19785
  listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
18978
19786
  getAgentIdentities: () => resolveAgentIdentities(),
18979
- readDocShareBundle: (req) => readDocShareBundle(req),
18980
- listChannelSkills: (args) => listSkills(args.claudeConfigDir, args.cwd),
18981
- listFlatDocs: (args) => flatDocs(args.cwd, { query: args.query, sort: args.sort, showHidden: args.showHidden }),
19787
+ readDocShareBundle: (req) => readDocShareBundle(req.channelId ? { ...req, cwd: resolveChannelConfig(req.channelId).cwd } : req),
19788
+ listChannelSkills: (args) => {
19789
+ const { cwd, claudeConfigDir } = args.channelId ? resolveChannelConfig(args.channelId) : args;
19790
+ return listSkills(claudeConfigDir, cwd);
19791
+ },
19792
+ listFlatDocs: (args) => flatDocs(args.channelId ? resolveChannelConfig(args.channelId).cwd : args.cwd, { query: args.query, sort: args.sort, showHidden: args.showHidden }),
18982
19793
  getLocalDashboardSettings: () => loadDashboardSettings(),
18983
19794
  saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
18984
19795
  writeSkill: (ref, write) => writeSkillAndSync(ref, write),
@@ -18990,6 +19801,7 @@ function localAgentCallbacks(chatAgent) {
18990
19801
  upgradeHost: () => startHostUpgrade(),
18991
19802
  scanOnboarding: (slug) => scanOnboarding(slug),
18992
19803
  linkProjectFolders: (slug, items) => linkProjectFolders(slug, items),
19804
+ sessionImportRequest: (req) => handleSessionImportRequest(req),
18993
19805
  isHostOnline: () => true
18994
19806
  };
18995
19807
  }
@@ -18998,7 +19810,7 @@ function buildHostConfigSnapshot() {
18998
19810
  const snapshot2 = {};
18999
19811
  for (const profile of listProfiles()) {
19000
19812
  snapshot2[profile] = runWithProfile(profile, () => ({
19001
- env: existsSync31(paths.CONFIG_FILE) ? dotenv2.parse(readFileSync26(paths.CONFIG_FILE, "utf-8")) : {},
19813
+ env: existsSync32(paths.CONFIG_FILE) ? dotenv2.parse(readFileSync27(paths.CONFIG_FILE, "utf-8")) : {},
19002
19814
  agentModel: loadDashboardSettings().agent
19003
19815
  }));
19004
19816
  }
@@ -19248,12 +20060,12 @@ function startPushWatcher() {
19248
20060
  }
19249
20061
 
19250
20062
  // src/dashboard/machine-server.ts
19251
- import { existsSync as existsSync32 } from "fs";
19252
- import { join as join27 } from "path";
20063
+ import { existsSync as existsSync33 } from "fs";
20064
+ import { join as join28 } from "path";
19253
20065
  import { serveStatic } from "@hono/node-server/serve-static";
19254
20066
 
19255
20067
  // src/dashboard/app-icon-routes.ts
19256
- import { Hono as Hono35 } from "hono";
20068
+ import { Hono as Hono37 } from "hono";
19257
20069
  var NEUTRAL_MANIFEST = {
19258
20070
  id: "/",
19259
20071
  name: "Flyspace",
@@ -19291,7 +20103,7 @@ async function withResolvedProfile2(c, next) {
19291
20103
  return profile ? runWithProfile(profile, () => next()) : next();
19292
20104
  }
19293
20105
  function createAppIconRouter() {
19294
- const app = new Hono35();
20106
+ const app = new Hono37();
19295
20107
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
19296
20108
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
19297
20109
  const profile = getProfile();
@@ -19331,7 +20143,7 @@ function pngResponse(c, png) {
19331
20143
  }
19332
20144
 
19333
20145
  // src/dashboard/machine-server.ts
19334
- var WEB_DIST = join27(paths.WEB_ROOT, "dist");
20146
+ var WEB_DIST = join28(paths.WEB_ROOT, "dist");
19335
20147
  async function bootMachine() {
19336
20148
  await reconcileAll().catch(
19337
20149
  (err) => console.error("[dashboard] reconcileAll failed:", err.message)
@@ -19351,14 +20163,14 @@ async function bootMachine() {
19351
20163
  function mountSpa(app) {
19352
20164
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
19353
20165
  app.route("/", createAppIconRouter());
19354
- if (existsSync32(WEB_DIST) || existsSync32(join27(paths.WEB_ROOT, "package.json"))) {
20166
+ if (existsSync33(WEB_DIST) || existsSync33(join28(paths.WEB_ROOT, "package.json"))) {
19355
20167
  app.use("/*", serveStatic({ root: WEB_DIST }));
19356
20168
  }
19357
20169
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
19358
- const spaFallback = serveStatic({ path: join27(WEB_DIST, "index.html") });
20170
+ const spaFallback = serveStatic({ path: join28(WEB_DIST, "index.html") });
19359
20171
  app.get("*", async (c, next) => {
19360
- if (!existsSync32(join27(WEB_DIST, "index.html"))) {
19361
- const isCheckout = existsSync32(join27(paths.WEB_ROOT, "package.json"));
20172
+ if (!existsSync33(join28(WEB_DIST, "index.html"))) {
20173
+ const isCheckout = existsSync33(join28(paths.WEB_ROOT, "package.json"));
19362
20174
  return c.text(
19363
20175
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
19364
20176
  Run \`pnpm build:web\` from the repo root.
@@ -19492,8 +20304,8 @@ function openBrowser2(url) {
19492
20304
  }
19493
20305
  }
19494
20306
  function ensureWebBuild() {
19495
- if (existsSync33(join28(paths.WEB_ROOT, "dist", "index.html"))) return;
19496
- if (!existsSync33(join28(paths.WEB_ROOT, "package.json"))) return;
20307
+ if (existsSync34(join29(paths.WEB_ROOT, "dist", "index.html"))) return;
20308
+ if (!existsSync34(join29(paths.WEB_ROOT, "package.json"))) return;
19497
20309
  console.log(" packages/ui/dist missing, building the dashboard UI...");
19498
20310
  const result = spawnSync("pnpm", ["build"], {
19499
20311
  cwd: paths.WEB_ROOT,
@@ -19543,7 +20355,7 @@ async function runDashboard(args) {
19543
20355
  }
19544
20356
 
19545
20357
  // src/commands/workspace.ts
19546
- import { existsSync as existsSync34, mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
20358
+ import { existsSync as existsSync35, mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
19547
20359
  var USAGE7 = `
19548
20360
  Usage: flyspace workspace <subcommand> [options]
19549
20361
 
@@ -19629,7 +20441,7 @@ function provisionLocal(botName) {
19629
20441
  ensureDirs();
19630
20442
  console.log(` config dir ready (${paths.CONFIG_DIR})`);
19631
20443
  console.log(` data dir ready (${paths.DATA_DIR})`);
19632
- if (existsSync34(paths.CONFIG_FILE)) {
20444
+ if (existsSync35(paths.CONFIG_FILE)) {
19633
20445
  console.log(" config.env exists");
19634
20446
  } else {
19635
20447
  writeFileSync13(paths.CONFIG_FILE, `BOT_NAME=${botName}
@@ -19646,7 +20458,7 @@ async function provisionChannels(specs) {
19646
20458
  continue;
19647
20459
  }
19648
20460
  const cwd = expandPath(spec.cwd);
19649
- if (!existsSync34(cwd)) {
20461
+ if (!existsSync35(cwd)) {
19650
20462
  mkdirSync14(cwd, { recursive: true });
19651
20463
  console.log(` created cwd ${cwd}`);
19652
20464
  }
@@ -19724,10 +20536,10 @@ async function runReconcileCmd(args) {
19724
20536
  }
19725
20537
 
19726
20538
  // src/cli.ts
19727
- import { join as join29 } from "path";
20539
+ import { join as join30 } from "path";
19728
20540
  var home = flyspaceHome();
19729
20541
  if (home) {
19730
- const bin = join29(home, "bin");
20542
+ const bin = join30(home, "bin");
19731
20543
  const path = process.env.PATH ?? "";
19732
20544
  if (!path.split(":").includes(bin)) process.env.PATH = path ? `${bin}:${path}` : bin;
19733
20545
  }
@@ -19865,7 +20677,7 @@ async function main() {
19865
20677
  break;
19866
20678
  case "-v":
19867
20679
  case "--version":
19868
- printRaw("0.1.32");
20680
+ printRaw("0.1.34");
19869
20681
  break;
19870
20682
  case "-h":
19871
20683
  case "--help":