flyspace-cli 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  BOT_PROFILE_KEY,
6
6
  BRANDING_DEFAULTS,
7
7
  BUILT_IN_OUTPUT_STYLES,
8
+ ChannelCwdError,
8
9
  DEFAULT_CODEX_MODEL,
9
10
  DEFAULT_TIMEZONE,
10
11
  DEFAULT_TITLE_RULES,
@@ -34,6 +35,7 @@ import {
34
35
  canSeeChannel,
35
36
  channelSkillsDir,
36
37
  claudeAuthFailed,
38
+ clearClaudeAuthFailed,
37
39
  codexAuthAvailable,
38
40
  createChannel,
39
41
  createMembership,
@@ -42,6 +44,7 @@ import {
42
44
  createWorkspace,
43
45
  deleteChannelMemory,
44
46
  deleteMembership,
47
+ deleteMemory,
45
48
  deleteOutputStyle,
46
49
  deleteSkillFile,
47
50
  deleteWorkspace,
@@ -68,6 +71,7 @@ import {
68
71
  getChannelRegistry,
69
72
  getMembershipByEmail,
70
73
  getMembershipByUserId,
74
+ getMemory,
71
75
  getOutputStyleDefault,
72
76
  getProfileChannelRegistry,
73
77
  getWorkspaceBilling,
@@ -96,6 +100,8 @@ import {
96
100
  listMemberChannels,
97
101
  listMemberships,
98
102
  listMembershipsByEmail,
103
+ listMemories,
104
+ listMemoryEvents,
99
105
  listOutputStyles,
100
106
  listPluginSkillDetails,
101
107
  listSkillFiles,
@@ -118,6 +124,7 @@ import {
118
124
  localDefaults,
119
125
  lookupById,
120
126
  markInviteResent,
127
+ markMemoryUsed,
121
128
  markWorkspaceAgentConnected,
122
129
  markWorkspaceGreeted,
123
130
  markWorkspaceProjectsSetup,
@@ -126,6 +133,7 @@ import {
126
133
  mimetypeSizeLimit,
127
134
  modelLabel,
128
135
  pinnedClaudeConfigDir,
136
+ promoteChannelMemory,
129
137
  readSkillFile,
130
138
  recentRunsForWorkspace,
131
139
  refreshAutomationRegistry,
@@ -148,12 +156,14 @@ import {
148
156
  saveBotIdentity,
149
157
  saveChannelMemory,
150
158
  saveDashboardSettings,
159
+ saveMemory,
151
160
  saveWorkspaceBranding,
152
161
  saveWorkspacePrompt,
153
- searchChannelMemories,
162
+ searchMemories,
154
163
  setAgentPaused,
155
164
  setAllowGuestAccess,
156
165
  setMembershipHandle,
166
+ setMemoryAutoPaused,
157
167
  setOutputStyleDefault,
158
168
  setWorkspaceStripeSubscription,
159
169
  slackConfigured,
@@ -168,6 +178,7 @@ import {
168
178
  updateChannelMemory,
169
179
  updateChannelsMcpConnections,
170
180
  updateMembershipRole,
181
+ updateMemory,
171
182
  updateOutputStyle,
172
183
  updateWorkspacePlanStatus,
173
184
  usageLimitedModels,
@@ -176,7 +187,7 @@ import {
176
187
  warnOnBrokenChannelCwds,
177
188
  workspaceSkillsDir,
178
189
  writeSkillFile
179
- } from "./chunk-KFT2YP5W.js";
190
+ } from "./chunk-2OGLCT7I.js";
180
191
  import {
181
192
  firstReplyBody,
182
193
  getThread,
@@ -1348,14 +1359,14 @@ function detectRunners(history = { claude: false, codex: false }) {
1348
1359
  function scanOnboarding() {
1349
1360
  const byPath = /* @__PURE__ */ new Map();
1350
1361
  const history = { claude: false, codex: false };
1351
- const add = (rawPath, source, mtime, sessions, trusted = false) => {
1362
+ const add = (rawPath, source, mtime, sessions2, trusted = false) => {
1352
1363
  const path = resolve3(rawPath);
1353
1364
  if (isExcluded(path)) return;
1354
1365
  const existing = byPath.get(path);
1355
1366
  if (existing) {
1356
1367
  if (!existing.sources.includes(source)) existing.sources.push(source);
1357
1368
  existing.lastUsed = Math.max(existing.lastUsed, mtime);
1358
- existing.sessions += sessions;
1369
+ existing.sessions += sessions2;
1359
1370
  existing.trusted ||= trusted;
1360
1371
  } else {
1361
1372
  byPath.set(path, {
@@ -1363,7 +1374,7 @@ function scanOnboarding() {
1363
1374
  name: basename2(path),
1364
1375
  sources: [source],
1365
1376
  lastUsed: mtime,
1366
- sessions,
1377
+ sessions: sessions2,
1367
1378
  trusted
1368
1379
  });
1369
1380
  }
@@ -1765,7 +1776,7 @@ async function runAuthCmd(args) {
1765
1776
  }
1766
1777
 
1767
1778
  // src/version.ts
1768
- var CLI_VERSION = true ? "0.1.15" : null;
1779
+ var CLI_VERSION = true ? "0.1.16" : null;
1769
1780
  function isOlderVersion(a, b) {
1770
1781
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1771
1782
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -2043,7 +2054,7 @@ ${BOLD3}Flyspace Doctor${RESET3}
2043
2054
  }
2044
2055
 
2045
2056
  // src/commands/uninstall.ts
2046
- import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
2057
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
2047
2058
  import { confirm as confirm3 } from "@inquirer/prompts";
2048
2059
  import { execSync as execSync5 } from "child_process";
2049
2060
  import { basename as basename3, join as join9 } from "path";
@@ -2110,12 +2121,31 @@ async function runUninstall(args = []) {
2110
2121
  console.log(" - revokes this device and removes every background service");
2111
2122
  for (const t of targets) console.log(` - deletes ${t}`);
2112
2123
  console.log(" Your channel folders (~/Flyspace) are kept.\n");
2113
- if (!args.includes("--yes") && !args.includes("-y")) {
2124
+ const backupSources = [paths.BASE_CONFIG_DIR, paths.BASE_DATA_DIR].filter(existsSync7);
2125
+ const interactive = !args.includes("--yes") && !args.includes("-y");
2126
+ let backup = !args.includes("--no-backup") && backupSources.length > 0;
2127
+ if (interactive) {
2128
+ if (backup) backup = await confirm3({ message: "Back up local data first?", default: true });
2114
2129
  if (!await confirm3({ message: "Continue?", default: false })) {
2115
2130
  console.log(" Cancelled.");
2116
2131
  return;
2117
2132
  }
2118
2133
  }
2134
+ let backupFile;
2135
+ if (backup) {
2136
+ const dir = join9(homedir4(), "Flyspace", ".uninstall-backups");
2137
+ mkdirSync7(dir, { recursive: true });
2138
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2139
+ backupFile = join9(dir, `flyspace-${stamp}.tar.gz`);
2140
+ const rel = backupSources.map((s) => `"${s.replace(/^\//, "")}"`).join(" ");
2141
+ try {
2142
+ run2(`tar -czf "${backupFile}" -C / ${rel}`);
2143
+ success2(`Backup saved to ${backupFile}`);
2144
+ } catch (err) {
2145
+ warn2(`Backup failed, nothing was removed: ${err.message}`);
2146
+ return;
2147
+ }
2148
+ }
2119
2149
  const creds = loadCredentials();
2120
2150
  if (creds) {
2121
2151
  try {
@@ -2142,7 +2172,11 @@ async function runUninstall(args = []) {
2142
2172
  success2(`Removed PATH entry from ${rc}`);
2143
2173
  }
2144
2174
  }
2145
- console.log("\n Flyspace is uninstalled. Open a new terminal to finish.\n");
2175
+ console.log("\n Flyspace is uninstalled. Open a new terminal to finish.");
2176
+ if (backupFile) {
2177
+ console.log(` To restore your local data after reinstalling: tar -xzf "${backupFile}" -C /`);
2178
+ }
2179
+ console.log("");
2146
2180
  }
2147
2181
 
2148
2182
  // src/commands/profiles-cmd.ts
@@ -2257,7 +2291,7 @@ function npmEnv() {
2257
2291
  return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2258
2292
  }
2259
2293
  async function runUpgrade() {
2260
- const current = "0.1.15";
2294
+ const current = "0.1.16";
2261
2295
  console.log(`Current version: ${current}`);
2262
2296
  console.log("Checking for updates...\n");
2263
2297
  try {
@@ -2898,7 +2932,7 @@ async function runLogs(args) {
2898
2932
 
2899
2933
  // src/commands/slack.ts
2900
2934
  import { execFileSync as execFileSync4 } from "child_process";
2901
- import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
2935
+ import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
2902
2936
  import { resolve as resolve6 } from "path";
2903
2937
  import { tmpdir as tmpdir5, homedir as homedir9 } from "os";
2904
2938
  import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
@@ -4436,10 +4470,10 @@ async function deliverPasswordResetLink(email, redirectTo) {
4436
4470
  }
4437
4471
 
4438
4472
  // src/dashboard/server.ts
4439
- import { existsSync as existsSync25 } from "fs";
4473
+ import { existsSync as existsSync26 } from "fs";
4440
4474
  import { createServer } from "net";
4441
- import { join as join24 } from "path";
4442
- import { Hono as Hono34 } from "hono";
4475
+ import { join as join25 } from "path";
4476
+ import { Hono as Hono35 } from "hono";
4443
4477
 
4444
4478
  // src/dashboard/sentry.ts
4445
4479
  import * as Sentry from "@sentry/node";
@@ -4456,7 +4490,7 @@ import { serve } from "@hono/node-server";
4456
4490
  import { serveStatic } from "@hono/node-server/serve-static";
4457
4491
 
4458
4492
  // src/reconcile.ts
4459
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
4493
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
4460
4494
  async function boundWorkspaces2() {
4461
4495
  return (await boundWorkspaces())?.workspaces ?? null;
4462
4496
  }
@@ -4482,7 +4516,7 @@ async function reconcileProfile(profile) {
4482
4516
  if (!channel.cwd) continue;
4483
4517
  const cwd = expandPath(channel.cwd);
4484
4518
  if (isCwdUsable(cwd)) continue;
4485
- mkdirSync7(cwd, { recursive: true });
4519
+ mkdirSync8(cwd, { recursive: true });
4486
4520
  log5.push(`channel "${channel.name}" cwd created (${cwd})`);
4487
4521
  }
4488
4522
  return log5;
@@ -4556,45 +4590,18 @@ var RANK = { none: 0, read: 1, write: 2 };
4556
4590
  function docsAccessAtLeast(have, need) {
4557
4591
  return RANK[have] >= RANK[need];
4558
4592
  }
4559
- function isDocsGrant(value) {
4560
- return value === "read" || value === "write";
4593
+ function isDocsAccess(value) {
4594
+ return value === "none" || value === "read" || value === "write";
4561
4595
  }
4562
- function rowToGrant(row) {
4563
- return {
4564
- id: row["id"],
4565
- channelId: row["channel_id"],
4566
- membershipId: row["membership_id"],
4567
- access: row["access"],
4568
- grantedAt: row["granted_at"]
4569
- };
4570
- }
4571
- async function listChannelDocsAccess(channelId) {
4572
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").select("*").eq("channel_id", channelId).order("granted_at", { ascending: true });
4573
- if (error2) throw new Error(`listChannelDocsAccess(${channelId}): ${error2.message}`);
4574
- return (data ?? []).map(rowToGrant);
4596
+ async function getChannelDocsMemberAccess(channelId) {
4597
+ const { data, error: error2 } = await getSupabaseClient().from("channels").select("docs_member_access").eq("id", channelId).maybeSingle();
4598
+ if (error2) throw new Error(`getChannelDocsMemberAccess(${channelId}): ${error2.message}`);
4599
+ const level = data?.docs_member_access;
4600
+ return isDocsAccess(level) ? level : "none";
4575
4601
  }
4576
- async function getChannelDocsGrant(channelId, membershipId) {
4577
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").select("*").eq("channel_id", channelId).eq("membership_id", membershipId).maybeSingle();
4578
- if (error2) throw new Error(`getChannelDocsGrant(${channelId}, ${membershipId}): ${error2.message}`);
4579
- return data ? rowToGrant(data) : null;
4580
- }
4581
- async function upsertChannelDocsAccess(params) {
4582
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").upsert(
4583
- { channel_id: params.channelId, membership_id: params.membershipId, access: params.access },
4584
- { onConflict: "channel_id,membership_id" }
4585
- ).select("*").single();
4586
- if (error2) {
4587
- throw new Error(
4588
- `upsertChannelDocsAccess(${params.channelId}, ${params.membershipId}): ${error2.message}`
4589
- );
4590
- }
4591
- return rowToGrant(data);
4592
- }
4593
- async function removeChannelDocsAccess(channelId, membershipId) {
4594
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").delete().eq("channel_id", channelId).eq("membership_id", membershipId).select("id");
4595
- if (error2) {
4596
- throw new Error(`removeChannelDocsAccess(${channelId}, ${membershipId}): ${error2.message}`);
4597
- }
4602
+ async function setChannelDocsMemberAccess(channelId, access) {
4603
+ const { data, error: error2 } = await getSupabaseClient().from("channels").update({ docs_member_access: access }).eq("id", channelId).select("id");
4604
+ if (error2) throw new Error(`setChannelDocsMemberAccess(${channelId}): ${error2.message}`);
4598
4605
  return data?.length ? true : false;
4599
4606
  }
4600
4607
  async function resolveForMembership(membership, channelId) {
@@ -4603,8 +4610,7 @@ async function resolveForMembership(membership, channelId) {
4603
4610
  if (members.some((m) => m.membershipId === membership.id && m.role === "channel_admin")) {
4604
4611
  return "write";
4605
4612
  }
4606
- const grant = await getChannelDocsGrant(channelId, membership.id);
4607
- return grant?.access ?? "none";
4613
+ return getChannelDocsMemberAccess(channelId);
4608
4614
  }
4609
4615
  async function resolveDocsAccess(ctx, channelId) {
4610
4616
  if (ctx.role === "admin") return "write";
@@ -4626,15 +4632,16 @@ async function filterReadableDocsChannels(ctx, channelIds) {
4626
4632
  if (ctx.role === "admin") return new Set(channelIds);
4627
4633
  const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
4628
4634
  if (!membership) return /* @__PURE__ */ new Set();
4635
+ if (channelIds.length === 0) return /* @__PURE__ */ new Set();
4629
4636
  const db = getSupabaseClient();
4630
- const [grants, roles] = await Promise.all([
4631
- db.from("channel_docs_access").select("channel_id").eq("membership_id", membership.id),
4637
+ const [open, roles] = await Promise.all([
4638
+ db.from("channels").select("id").in("id", channelIds).neq("docs_member_access", "none"),
4632
4639
  db.from("channel_members").select("channel_id, role").eq("membership_id", membership.id)
4633
4640
  ]);
4634
- if (grants.error) throw new Error(`filterReadableDocsChannels: ${grants.error.message}`);
4641
+ if (open.error) throw new Error(`filterReadableDocsChannels: ${open.error.message}`);
4635
4642
  if (roles.error) throw new Error(`filterReadableDocsChannels: ${roles.error.message}`);
4636
4643
  const allowed = /* @__PURE__ */ new Set();
4637
- for (const r of grants.data ?? []) allowed.add(r.channel_id);
4644
+ for (const r of open.data ?? []) allowed.add(r.id);
4638
4645
  for (const r of roles.data ?? []) {
4639
4646
  if (r.role === "channel_admin") allowed.add(r.channel_id);
4640
4647
  }
@@ -4653,7 +4660,7 @@ var requireDocsAccess = async (c, next) => {
4653
4660
  };
4654
4661
 
4655
4662
  // src/dashboard/api.ts
4656
- import { Hono as Hono31 } from "hono";
4663
+ import { Hono as Hono32 } from "hono";
4657
4664
 
4658
4665
  // src/dashboard/public-share-routes.ts
4659
4666
  import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
@@ -7251,12 +7258,12 @@ async function offlineAgentIdentities() {
7251
7258
  }
7252
7259
 
7253
7260
  // src/dashboard/host-api.ts
7254
- import { existsSync as existsSync20, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
7255
- import { join as join22 } from "path";
7261
+ import { existsSync as existsSync21, readFileSync as readFileSync16, statSync as statSync8 } from "fs";
7262
+ import { join as join23 } from "path";
7256
7263
  import sharp3 from "sharp";
7257
7264
 
7258
7265
  // src/dashboard/channels-routes.ts
7259
- import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
7266
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
7260
7267
  import { Hono as Hono9 } from "hono";
7261
7268
 
7262
7269
  // src/dashboard/feature-flags-routes.ts
@@ -7329,7 +7336,7 @@ function coerceMemberEmails(raw) {
7329
7336
  }
7330
7337
  function ensureCwdDir(cwd) {
7331
7338
  const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
7332
- if (!existsSync12(expanded)) mkdirSync8(expanded, { recursive: true });
7339
+ if (!existsSync12(expanded)) mkdirSync9(expanded, { recursive: true });
7333
7340
  }
7334
7341
  function toResponse(channel) {
7335
7342
  return {
@@ -7367,7 +7374,6 @@ function createChannelWriteRouter() {
7367
7374
  const externalChannelId = typeof body["externalChannelId"] === "string" && body["externalChannelId"].trim() ? body["externalChannelId"].trim() : null;
7368
7375
  const categoryId = typeof body["categoryId"] === "string" && body["categoryId"].trim() ? body["categoryId"].trim() : null;
7369
7376
  try {
7370
- ensureCwdDir(cwd);
7371
7377
  const channel = await createChannel({
7372
7378
  name,
7373
7379
  cwd,
@@ -7380,9 +7386,10 @@ function createChannelWriteRouter() {
7380
7386
  externalChannelId,
7381
7387
  categoryId
7382
7388
  });
7389
+ ensureCwdDir(cwd);
7383
7390
  return c.json(toResponse(channel), 201);
7384
7391
  } catch (err) {
7385
- return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
7392
+ return c.json({ error: err.message }, err instanceof DefaultChannelError || err instanceof ChannelCwdError ? 400 : 500);
7386
7393
  }
7387
7394
  });
7388
7395
  app.put("/:id", async (c) => {
@@ -7396,7 +7403,6 @@ function createChannelWriteRouter() {
7396
7403
  if (coerceRuntime(body["runtime"]) === "sandbox" && existing.runtime !== "sandbox" && !await isFeatureEnabled(c, "sandbox"))
7397
7404
  return c.json({ error: "feature not enabled" }, 403);
7398
7405
  try {
7399
- ensureCwdDir(cwd);
7400
7406
  const channel = await updateChannelById(id, {
7401
7407
  name: typeof body["name"] === "string" ? body["name"] : void 0,
7402
7408
  cwd,
@@ -7409,10 +7415,11 @@ function createChannelWriteRouter() {
7409
7415
  runtime: coerceRuntime(body["runtime"]),
7410
7416
  mounts: coerceMounts(body["mounts"])
7411
7417
  });
7418
+ ensureCwdDir(cwd);
7412
7419
  dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
7413
7420
  return c.json(toResponse(channel), 200);
7414
7421
  } catch (err) {
7415
- return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
7422
+ return c.json({ error: err.message }, err instanceof DefaultChannelError || err instanceof ChannelCwdError ? 400 : 500);
7416
7423
  }
7417
7424
  });
7418
7425
  app.patch("/reorder", async (c) => {
@@ -7464,12 +7471,12 @@ function createChannelWriteRouter() {
7464
7471
  patch.archivedAt = body["archived"] ? (/* @__PURE__ */ new Date()).toISOString() : null;
7465
7472
  }
7466
7473
  try {
7467
- if (patch.cwd) ensureCwdDir(patch.cwd);
7468
7474
  const channel = await updateChannelById(id, patch);
7475
+ if (patch.cwd) ensureCwdDir(patch.cwd);
7469
7476
  dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
7470
7477
  return c.json(toResponse(channel), 200);
7471
7478
  } catch (err) {
7472
- return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
7479
+ return c.json({ error: err.message }, err instanceof DefaultChannelError || err instanceof ChannelCwdError ? 400 : 500);
7473
7480
  }
7474
7481
  });
7475
7482
  return app;
@@ -7478,9 +7485,10 @@ function createChannelWriteRouter() {
7478
7485
  // src/dashboard/docs-routes.ts
7479
7486
  import {
7480
7487
  closeSync,
7488
+ createReadStream,
7481
7489
  existsSync as existsSync16,
7482
7490
  lstatSync as lstatSync2,
7483
- mkdirSync as mkdirSync9,
7491
+ mkdirSync as mkdirSync10,
7484
7492
  openSync,
7485
7493
  readSync,
7486
7494
  readdirSync as readdirSync4,
@@ -7491,6 +7499,7 @@ import {
7491
7499
  statSync as statSync5,
7492
7500
  writeFileSync as writeFileSync9
7493
7501
  } from "fs";
7502
+ import { Readable } from "stream";
7494
7503
  import { basename as basename6, dirname as dirname5, extname, join as join20, relative, resolve as resolve4, sep } from "path";
7495
7504
  import { Hono as Hono10 } from "hono";
7496
7505
  import { streamSSE } from "hono/streaming";
@@ -9547,7 +9556,8 @@ var ChatAgent = class {
9547
9556
  };
9548
9557
  }
9549
9558
  const memory = await loadAgentMemory(owningProfile, thread.channel_id, {
9550
- readable: docsAccess !== "none"
9559
+ readable: docsAccess !== "none",
9560
+ ...isHumanAuthor(msg.author) ? { senderHandle: msg.author } : {}
9551
9561
  });
9552
9562
  if (memory.enabled) channelConfig = { ...channelConfig, disableAutoMemory: true };
9553
9563
  if (memory.memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt: memory.memoryPrompt };
@@ -10346,7 +10356,7 @@ var MEDIA_EXTENSIONS = {
10346
10356
  ".csv": "text/csv; charset=utf-8",
10347
10357
  ".json": "application/json"
10348
10358
  };
10349
- var MAX_RAW_BYTES = 20 * 1024 * 1024;
10359
+ var MAX_RAW_BYTES = 100 * 1024 * 1024;
10350
10360
  var ASSET_LIST_EXTENSIONS = /* @__PURE__ */ new Set([
10351
10361
  ".png",
10352
10362
  ".jpg",
@@ -10362,7 +10372,7 @@ var ASSET_LIST_EXTENSIONS = /* @__PURE__ */ new Set([
10362
10372
  ".txt",
10363
10373
  ".csv"
10364
10374
  ]);
10365
- var MAX_UPLOAD_BYTES = MAX_RAW_BYTES;
10375
+ var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
10366
10376
  var CODE_EXTENSIONS = {
10367
10377
  ".js": "javascript",
10368
10378
  ".jsx": "javascript",
@@ -10749,7 +10759,7 @@ function createDocFolder(root, relDir, name) {
10749
10759
  return { status: "exists" };
10750
10760
  } catch {
10751
10761
  }
10752
- mkdirSync9(abs);
10762
+ mkdirSync10(abs);
10753
10763
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: safe };
10754
10764
  }
10755
10765
  function createDocFile(root, relDir, name, content) {
@@ -10866,13 +10876,15 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10866
10876
  }
10867
10877
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10868
10878
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10869
- const data = readFileSync14(resolved2.abs);
10870
- const headers = { "Content-Type": resolved2.contentType };
10871
- if (download) {
10872
- const filename = basename6(resolved2.abs).replace(/"/g, "");
10873
- headers["Content-Disposition"] = `attachment; filename="${filename}"`;
10874
- }
10875
- return c.body(new Uint8Array(data), 200, headers);
10879
+ const name = basename6(resolved2.abs);
10880
+ const asciiName = name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
10881
+ const disposition = download ? "attachment" : "inline";
10882
+ const stream = Readable.toWeb(createReadStream(resolved2.abs));
10883
+ return c.body(stream, 200, {
10884
+ "Content-Type": resolved2.contentType,
10885
+ "Content-Length": String(stat.size),
10886
+ "Content-Disposition": `${disposition}; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(name)}`
10887
+ });
10876
10888
  });
10877
10889
  app.get("/:id/docs/html", (c) => {
10878
10890
  const { cwd } = resolveChannelConfig(c.req.param("id"));
@@ -11663,6 +11675,9 @@ var PROVIDER_LABELS = {
11663
11675
  };
11664
11676
  var TTL_MS2 = 6e4;
11665
11677
  var cache6 = /* @__PURE__ */ new Map();
11678
+ function clearProviderStatusCache() {
11679
+ cache6.clear();
11680
+ }
11666
11681
  function binaryOnPath(bin) {
11667
11682
  try {
11668
11683
  execFileSync3(bin, ["--version"], { stdio: "ignore" });
@@ -11697,7 +11712,7 @@ function probe2(runner) {
11697
11712
  runner,
11698
11713
  label: label2,
11699
11714
  connected: false,
11700
- reason: `${label2} is installed but signed out \u2014 run ${bin} once in a terminal to log in.`
11715
+ reason: `${label2} is installed but signed out. Use Sign in below.`
11701
11716
  };
11702
11717
  }
11703
11718
  return { runner, label: label2, connected: true };
@@ -11707,7 +11722,7 @@ function withAuthFailure(status) {
11707
11722
  return {
11708
11723
  ...status,
11709
11724
  connected: false,
11710
- reason: `Sign-in expired and could not be refreshed. On the Host, run claude setup-token, add the token to ~/.config/flyspace/profiles/${getProfile()}/config.env as CLAUDE_CODE_OAUTH_TOKEN, then restart the Host.`
11725
+ reason: "Sign-in expired and could not be refreshed. Use Sign in again below."
11711
11726
  };
11712
11727
  }
11713
11728
  function providerStatuses() {
@@ -11721,6 +11736,232 @@ function providerStatuses() {
11721
11736
  return statuses.map((st) => st.runner === "claude" && st.connected ? withAuthFailure(st) : st);
11722
11737
  }
11723
11738
 
11739
+ // src/dashboard/provider-login.ts
11740
+ import { spawn as spawn6 } from "child_process";
11741
+ import { randomUUID as randomUUID2 } from "crypto";
11742
+ import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
11743
+ import { createRequire } from "module";
11744
+ import { dirname as dirname7, join as join22 } from "path";
11745
+ import { Hono as Hono13 } from "hono";
11746
+ var SESSION_TTL_MS = 10 * 6e4;
11747
+ var URL_WAIT_MS = 3e4;
11748
+ var TOKEN_WAIT_MS = 6e4;
11749
+ var sessions = /* @__PURE__ */ new Map();
11750
+ function stripAnsi(raw) {
11751
+ return raw.replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b\[\d*G/g, " ").replace(/\x1b\[[0-9;?<>=]*[A-Za-z~]/g, "").replace(/\x1b[78=>]/g, "");
11752
+ }
11753
+ function parseClaudeAuthUrl(raw) {
11754
+ return stripAnsi(raw).match(/https:\/\/\S+\/oauth\/authorize\?\S+/)?.[0];
11755
+ }
11756
+ function parseClaudeToken(raw) {
11757
+ return stripAnsi(raw).match(/sk-ant-oat[A-Za-z0-9_-]+/)?.[0];
11758
+ }
11759
+ function parseCodexDeviceAuth(raw) {
11760
+ const text = stripAnsi(raw);
11761
+ const url = text.match(/https:\/\/\S+\/device\b\S*/)?.[0];
11762
+ const code = text.match(/\b([A-Z0-9]{4,}-[A-Z0-9]{4,})\b/)?.[1];
11763
+ return url && code ? { url, code } : void 0;
11764
+ }
11765
+ function upsertEnvLine(content, key, value) {
11766
+ const line = `${key}=${value}`;
11767
+ const re = new RegExp(`^\\s*(?:export\\s+)?${key}=.*$`, "m");
11768
+ if (re.test(content)) return content.replace(re, line);
11769
+ return `${content}${content && !content.endsWith("\n") ? "\n" : ""}${line}
11770
+ `;
11771
+ }
11772
+ function finish(id, status, error2) {
11773
+ const s = sessions.get(id);
11774
+ if (!s || s.status !== "pending") return;
11775
+ s.status = status;
11776
+ s.error = error2;
11777
+ s.kill();
11778
+ clearTimeout(s.timer);
11779
+ s.timer = setTimeout(() => sessions.delete(id), 6e4);
11780
+ clearProviderStatusCache();
11781
+ }
11782
+ function track(session) {
11783
+ const id = randomUUID2();
11784
+ const s = {
11785
+ ...session,
11786
+ status: "pending",
11787
+ output: "",
11788
+ timer: setTimeout(() => finish(id, "failed", "Sign-in timed out. Start again."), SESSION_TTL_MS)
11789
+ };
11790
+ sessions.set(id, s);
11791
+ return [id, s];
11792
+ }
11793
+ function waitFor(s, parse2, ms) {
11794
+ return new Promise((resolve7) => {
11795
+ const started = Date.now();
11796
+ const tick = setInterval(() => {
11797
+ const v = parse2();
11798
+ if (v !== void 0 || s.status !== "pending" || Date.now() - started > ms) {
11799
+ clearInterval(tick);
11800
+ resolve7(v);
11801
+ }
11802
+ }, 200);
11803
+ });
11804
+ }
11805
+ function loadPty() {
11806
+ const require2 = createRequire(import.meta.url);
11807
+ const root = dirname7(require2.resolve("node-pty/package.json"));
11808
+ const helper = join22(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
11809
+ try {
11810
+ if (existsSync20(helper)) chmodSync(helper, 493);
11811
+ } catch {
11812
+ }
11813
+ return require2("node-pty");
11814
+ }
11815
+ function cliEnv() {
11816
+ const configDir2 = profileClaudeConfigDir();
11817
+ const codexHome = profileEnv("CODEX_HOME");
11818
+ return {
11819
+ ...process.env,
11820
+ // Never pop a browser on the Host: the person signing in isn't there.
11821
+ BROWSER: "true",
11822
+ ...configDir2 ? { CLAUDE_CONFIG_DIR: configDir2 } : {},
11823
+ ...codexHome ? { CODEX_HOME: codexHome } : {}
11824
+ };
11825
+ }
11826
+ async function startClaude() {
11827
+ const pty = loadPty().spawn("claude", ["setup-token"], {
11828
+ // Wide enough that the URL and token are never wrapped.
11829
+ cols: 2e3,
11830
+ rows: 50,
11831
+ env: cliEnv()
11832
+ });
11833
+ const [id, s] = track({ runner: "claude", kill: () => pty.kill(), configFile: paths.CONFIG_FILE, pty });
11834
+ pty.onData((d) => {
11835
+ s.output += d;
11836
+ });
11837
+ pty.onExit(() => {
11838
+ if (s.status === "pending" && !parseClaudeToken(s.output)) {
11839
+ finish(id, "failed", "The claude CLI exited before sign-in finished.");
11840
+ }
11841
+ });
11842
+ const url = await waitFor(s, () => parseClaudeAuthUrl(s.output), URL_WAIT_MS);
11843
+ if (!url) {
11844
+ finish(id, "failed");
11845
+ throw new Error("claude setup-token did not print a sign-in URL.");
11846
+ }
11847
+ return { id, url };
11848
+ }
11849
+ async function submitClaudeCode(id, code) {
11850
+ const s = sessions.get(id);
11851
+ if (!s || s.runner !== "claude" || !s.pty) throw new Error("Sign-in session not found. Start again.");
11852
+ if (s.status !== "pending") throw new Error(s.error ?? "Sign-in already finished.");
11853
+ s.output = "";
11854
+ s.pty.write(`${code.trim()}\r`);
11855
+ const token = await waitFor(s, () => parseClaudeToken(s.output), TOKEN_WAIT_MS);
11856
+ if (!token) {
11857
+ finish(id, "failed", "Claude did not accept that code. Start again and paste the full code.");
11858
+ throw new Error("Claude did not accept that code. Start again and paste the full code.");
11859
+ }
11860
+ mkdirSync11(dirname7(s.configFile), { recursive: true });
11861
+ const current = existsSync20(s.configFile) ? readFileSync15(s.configFile, "utf-8") : "";
11862
+ writeFileSync10(s.configFile, upsertEnvLine(current, "CLAUDE_CODE_OAUTH_TOKEN", token), { mode: 384 });
11863
+ const cached = cachedProfileConfig();
11864
+ if (cached) cached.env["CLAUDE_CODE_OAUTH_TOKEN"] = token;
11865
+ clearClaudeAuthFailed();
11866
+ finish(id, "done");
11867
+ }
11868
+ async function startCodex() {
11869
+ const child = spawn6("codex", ["login", "--device-auth"], {
11870
+ env: cliEnv(),
11871
+ stdio: ["ignore", "pipe", "pipe"]
11872
+ });
11873
+ const [id, s] = track({ runner: "codex", kill: () => child.kill(), configFile: paths.CONFIG_FILE });
11874
+ const onData = (d) => {
11875
+ s.output += d.toString();
11876
+ };
11877
+ child.stdout?.on("data", onData);
11878
+ child.stderr?.on("data", onData);
11879
+ child.on("error", () => finish(id, "failed", "Could not run the codex CLI on the Host."));
11880
+ child.on(
11881
+ "exit",
11882
+ (code) => finish(id, code === 0 ? "done" : "failed", code === 0 ? void 0 : "Codex sign-in did not complete.")
11883
+ );
11884
+ const auth = await waitFor(s, () => parseCodexDeviceAuth(s.output), URL_WAIT_MS);
11885
+ if (!auth) {
11886
+ finish(id, "failed");
11887
+ throw new Error("codex login did not print a device code.");
11888
+ }
11889
+ return { id, ...auth };
11890
+ }
11891
+ function requireAdmin3(auth) {
11892
+ return auth?.role === "admin" ? null : "admin access required";
11893
+ }
11894
+ function failure2(c, err) {
11895
+ return c.json({ error: err.message }, 500);
11896
+ }
11897
+ function createProviderLoginRouter() {
11898
+ const app = new Hono13();
11899
+ app.use("*", async (c, next) => {
11900
+ const denied = requireAdmin3(c.get("auth"));
11901
+ if (denied) return c.json({ error: denied }, 403);
11902
+ await next();
11903
+ });
11904
+ app.post("/:runner/login", async (c) => {
11905
+ const runner = c.req.param("runner");
11906
+ try {
11907
+ if (runner === "claude") return c.json(await startClaude());
11908
+ if (runner === "codex") return c.json(await startCodex());
11909
+ } catch (err) {
11910
+ return failure2(c, err);
11911
+ }
11912
+ return c.json({ error: "unknown provider" }, 404);
11913
+ });
11914
+ app.post("/claude/login/:id/code", async (c) => {
11915
+ const body = await c.req.json().catch(() => ({}));
11916
+ const code = body["code"];
11917
+ if (typeof code !== "string" || !code.trim()) return c.json({ error: "code is required" }, 400);
11918
+ try {
11919
+ await submitClaudeCode(c.req.param("id"), code);
11920
+ } catch (err) {
11921
+ return c.json({ error: err.message }, 400);
11922
+ }
11923
+ return c.json({ status: "done" });
11924
+ });
11925
+ app.get("/:runner/login/:id", (c) => {
11926
+ const s = sessions.get(c.req.param("id"));
11927
+ if (!s || s.runner !== c.req.param("runner")) return c.json({ error: "session not found" }, 404);
11928
+ return c.json({ status: s.status, ...s.error ? { error: s.error } : {} });
11929
+ });
11930
+ return app;
11931
+ }
11932
+ async function handleProviderLoginRequest(req) {
11933
+ const app = new Hono13();
11934
+ app.use("*", async (c, next) => {
11935
+ c.set("auth", { role: "admin" });
11936
+ await next();
11937
+ });
11938
+ app.route("/", createProviderLoginRouter());
11939
+ const res = await app.request(req.path, {
11940
+ method: req.method,
11941
+ ...req.body !== void 0 && {
11942
+ body: JSON.stringify(req.body),
11943
+ headers: { "content-type": "application/json" }
11944
+ }
11945
+ });
11946
+ return { status: res.status, body: await res.json().catch(() => null) };
11947
+ }
11948
+ function registerRelayedProviderLoginRoutes(api, agent) {
11949
+ const relay3 = async (c) => {
11950
+ const denied = requireAdmin3(c.get("auth"));
11951
+ if (denied) return c.json({ error: denied }, 403);
11952
+ const method = c.req.method;
11953
+ const body = method === "GET" ? void 0 : await c.req.json().catch(() => null);
11954
+ const path = c.req.path.slice(c.req.path.indexOf("/settings/providers") + "/settings/providers".length);
11955
+ const result = await agent.providerLoginRequest?.({ method, path, body });
11956
+ if (!result) {
11957
+ return c.json({ error: "Signing in needs the Host machine, which is not connected right now." }, 503);
11958
+ }
11959
+ return c.json(result.body, result.status);
11960
+ };
11961
+ api.all("/settings/providers/:runner/login", relay3);
11962
+ api.all("/settings/providers/:runner/login/*", relay3);
11963
+ }
11964
+
11724
11965
  // src/dashboard/host-api.ts
11725
11966
  function registerHostRoutes(api) {
11726
11967
  api.get("/channels/:id/skills", (c) => {
@@ -11769,6 +12010,7 @@ function registerHostRoutes(api) {
11769
12010
  return c.json({ ...await saveWorkspaceBranding(patch), ...local });
11770
12011
  });
11771
12012
  api.get("/settings/providers", (c) => c.json({ providers: providerStatuses() }));
12013
+ api.route("/settings/providers", createProviderLoginRouter());
11772
12014
  api.get(
11773
12015
  "/settings/usage-limit",
11774
12016
  (c) => c.json({
@@ -11827,8 +12069,8 @@ function registerHostRoutes(api) {
11827
12069
  const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
11828
12070
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
11829
12071
  const filename = errors ? "custie-error.log" : "custie.log";
11830
- const file = join22(paths.LOG_DIR, filename);
11831
- const exists = existsSync20(file);
12072
+ const file = join23(paths.LOG_DIR, filename);
12073
+ const exists = existsSync21(file);
11832
12074
  return c.json({
11833
12075
  file,
11834
12076
  exists,
@@ -11838,18 +12080,18 @@ function registerHostRoutes(api) {
11838
12080
  });
11839
12081
  }
11840
12082
  function tailFile(file, limit) {
11841
- if (!existsSync20(file)) return [];
11842
- const lines = readFileSync15(file, "utf-8").split("\n");
12083
+ if (!existsSync21(file)) return [];
12084
+ const lines = readFileSync16(file, "utf-8").split("\n");
11843
12085
  if (lines.at(-1) === "") lines.pop();
11844
12086
  return lines.slice(-limit);
11845
12087
  }
11846
12088
 
11847
12089
  // src/dashboard/data-api.ts
11848
- import { existsSync as existsSync22 } from "fs";
12090
+ import { existsSync as existsSync23 } from "fs";
11849
12091
  import sharp5 from "sharp";
11850
12092
 
11851
12093
  // src/dashboard/automations-routes.ts
11852
- import { Hono as Hono13 } from "hono";
12094
+ import { Hono as Hono14 } from "hono";
11853
12095
  var TYPES2 = ["schedule", "trigger", "mention-trigger"];
11854
12096
  function isType(t) {
11855
12097
  return TYPES2.includes(t);
@@ -11866,7 +12108,7 @@ function statusFor(message) {
11866
12108
  return 400;
11867
12109
  }
11868
12110
  function createAutomationWriteRouter() {
11869
- const app = new Hono13();
12111
+ const app = new Hono14();
11870
12112
  const manager = () => new AutomationManager();
11871
12113
  app.get("/:type/:name", (c) => {
11872
12114
  const item = manager().get(c.req.param("name"));
@@ -11922,9 +12164,9 @@ function createAutomationWriteRouter() {
11922
12164
  }
11923
12165
 
11924
12166
  // src/dashboard/categories-routes.ts
11925
- import { Hono as Hono14 } from "hono";
12167
+ import { Hono as Hono15 } from "hono";
11926
12168
  function createCategoryWriteRouter() {
11927
- const app = new Hono14();
12169
+ const app = new Hono15();
11928
12170
  app.get("/", async (c) => {
11929
12171
  const { data, error: error2 } = await getSupabaseClient().from("categories").select("*").eq("profile", getProfile()).order("position", { ascending: true });
11930
12172
  if (error2) return c.json({ error: error2.message }, 500);
@@ -11978,7 +12220,7 @@ function createCategoryWriteRouter() {
11978
12220
  }
11979
12221
 
11980
12222
  // src/dashboard/chat-routes.ts
11981
- import { Hono as Hono15 } from "hono";
12223
+ import { Hono as Hono16 } from "hono";
11982
12224
 
11983
12225
  // src/dashboard/fork-summary.ts
11984
12226
  var GROQ_API_URL6 = "https://api.groq.com/openai/v1/chat/completions";
@@ -12398,7 +12640,7 @@ async function withPreviews(supabase, threads) {
12398
12640
  threads.map((t) => t.id),
12399
12641
  THREAD_CHUNK_SIZE
12400
12642
  ).map(
12401
- (ids) => supabase.from("thread_replies").select("thread_id, body, author").in("thread_id", ids).is("archived_at", null).order("created_at", { ascending: true })
12643
+ (ids) => supabase.from("thread_replies").select("thread_id, body, author, external_user_id").in("thread_id", ids).is("archived_at", null).order("created_at", { ascending: true })
12402
12644
  )
12403
12645
  );
12404
12646
  const previewByThread = /* @__PURE__ */ new Map();
@@ -12408,8 +12650,15 @@ async function withPreviews(supabase, threads) {
12408
12650
  if (entry) {
12409
12651
  entry.count += 1;
12410
12652
  entry.lastAuthor = r.author;
12653
+ entry.participants.delete(r.author);
12654
+ entry.participants.set(r.author, r.external_user_id ?? null);
12411
12655
  } else
12412
- previewByThread.set(r.thread_id, { count: 1, firstBody: r.body, lastAuthor: r.author });
12656
+ previewByThread.set(r.thread_id, {
12657
+ count: 1,
12658
+ firstBody: r.body,
12659
+ lastAuthor: r.author,
12660
+ participants: /* @__PURE__ */ new Map()
12661
+ });
12413
12662
  }
12414
12663
  }
12415
12664
  return threads.map((t) => {
@@ -12421,7 +12670,9 @@ async function withPreviews(supabase, threads) {
12421
12670
  // Who spoke last, counting only replies AFTER the opening message —
12422
12671
  // the reader's own handle here means there is nothing new for them to
12423
12672
  // read, on this browser or any other (see web/src/lib/thread-unread).
12424
- last_reply_author: entry && entry.count > 1 ? entry.lastAuthor : null
12673
+ last_reply_author: entry && entry.count > 1 ? entry.lastAuthor : null,
12674
+ // Everyone who replied, most recent first, for the row's avatar stack.
12675
+ participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id }))
12425
12676
  };
12426
12677
  });
12427
12678
  }
@@ -12456,7 +12707,7 @@ async function generateAutoTitle(threadId, channelId, body) {
12456
12707
  await supabase.from("threads").update({ auto_title: autoTitle }).eq("id", threadId).is("title", null);
12457
12708
  }
12458
12709
  function createChatRouter(agent = {}) {
12459
- const app = new Hono15();
12710
+ const app = new Hono16();
12460
12711
  const {
12461
12712
  onChatMessage: onMessage,
12462
12713
  isAgentThinking,
@@ -12963,7 +13214,7 @@ function createChatRouter(agent = {}) {
12963
13214
  }
12964
13215
 
12965
13216
  // src/dashboard/doc-shares-routes.ts
12966
- import { Hono as Hono16 } from "hono";
13217
+ import { Hono as Hono17 } from "hono";
12967
13218
 
12968
13219
  // src/dashboard/doc-share-features.ts
12969
13220
  import { DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
@@ -13147,7 +13398,7 @@ function resolveChannel2(c) {
13147
13398
  }
13148
13399
  var LIST_LIMIT = 500;
13149
13400
  function createDocShareListRouter() {
13150
- const app = new Hono16();
13401
+ const app = new Hono17();
13151
13402
  app.get("/shares", async (c) => {
13152
13403
  const viewer = c.get("auth");
13153
13404
  const readable = await filterReadableDocsChannels(
@@ -13191,7 +13442,7 @@ function createDocShareListRouter() {
13191
13442
  return app;
13192
13443
  }
13193
13444
  function createDocShareWriteRouter(agent = {}) {
13194
- const app = new Hono16();
13445
+ const app = new Hono17();
13195
13446
  app.use("/:id/docs/share", requireDocsAccess);
13196
13447
  app.use("/:id/docs/share/*", requireDocsAccess);
13197
13448
  app.use("/:id/docs/shares", requireDocsAccess);
@@ -13348,7 +13599,7 @@ function createDocShareWriteRouter(agent = {}) {
13348
13599
  }
13349
13600
 
13350
13601
  // src/dashboard/search-routes.ts
13351
- import { Hono as Hono17 } from "hono";
13602
+ import { Hono as Hono18 } from "hono";
13352
13603
  function matchScore(q, fields) {
13353
13604
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
13354
13605
  if (terms.length === 0) return 0;
@@ -13496,7 +13747,7 @@ function searchAutomations(q) {
13496
13747
  }
13497
13748
  var RESULT_LIMIT = 30;
13498
13749
  function createSearchRouter() {
13499
- const app = new Hono17();
13750
+ const app = new Hono18();
13500
13751
  app.get("/", async (c) => {
13501
13752
  const q = c.req.query("q")?.trim();
13502
13753
  if (!q) return c.json({ results: [] });
@@ -13512,8 +13763,8 @@ function createSearchRouter() {
13512
13763
  }
13513
13764
 
13514
13765
  // src/dashboard/uploads-routes.ts
13515
- import { randomUUID as randomUUID2 } from "crypto";
13516
- import { Hono as Hono18 } from "hono";
13766
+ import { randomUUID as randomUUID3 } from "crypto";
13767
+ import { Hono as Hono19 } from "hono";
13517
13768
  import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
13518
13769
  import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
13519
13770
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
@@ -13528,7 +13779,7 @@ function encodeKeyForUrl(key) {
13528
13779
  return key.split("/").map(encodeURIComponent).join("/");
13529
13780
  }
13530
13781
  function createUploadsRouter() {
13531
- const app = new Hono18();
13782
+ const app = new Hono19();
13532
13783
  app.post("/uploads", async (c) => {
13533
13784
  const body = await c.req.parseBody().catch(() => null);
13534
13785
  const file = body?.["file"];
@@ -13536,7 +13787,7 @@ function createUploadsRouter() {
13536
13787
  if (file.size > MAX_FILE_BYTES) {
13537
13788
  return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
13538
13789
  }
13539
- const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
13790
+ const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID3()}-${sanitizeFilename(file.name)}`;
13540
13791
  const buffer = Buffer.from(await file.arrayBuffer());
13541
13792
  await getR2Client().send(
13542
13793
  new PutObjectCommand3({
@@ -13573,14 +13824,14 @@ function createUploadsRouter() {
13573
13824
  }
13574
13825
 
13575
13826
  // src/dashboard/transcribe-routes.ts
13576
- import { Hono as Hono19 } from "hono";
13827
+ import { Hono as Hono20 } from "hono";
13577
13828
  var GROQ_TRANSCRIBE_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
13578
13829
  var MODEL = "whisper-large-v3-turbo";
13579
13830
  var MAX_AUDIO_BYTES = 5 * 1024 * 1024;
13580
13831
  var MAX_PROMPT_CHARS = 400;
13581
13832
  var NO_SPEECH_THRESHOLD = 0.6;
13582
13833
  function createTranscribeRouter() {
13583
- const app = new Hono19();
13834
+ const app = new Hono20();
13584
13835
  app.post("/transcribe", async (c) => {
13585
13836
  if (!await isFeatureEnabled(c, "dictation"))
13586
13837
  return c.json({ error: "feature not enabled" }, 403);
@@ -13619,8 +13870,8 @@ function createTranscribeRouter() {
13619
13870
  }
13620
13871
 
13621
13872
  // src/dashboard/avatars-routes.ts
13622
- import { createHash as createHash10, randomUUID as randomUUID3 } from "crypto";
13623
- import { Hono as Hono20 } from "hono";
13873
+ import { createHash as createHash10, randomUUID as randomUUID4 } from "crypto";
13874
+ import { Hono as Hono21 } from "hono";
13624
13875
  import sharp4 from "sharp";
13625
13876
  import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
13626
13877
  var AVATAR_SIZE = 192;
@@ -13658,7 +13909,7 @@ async function storeAvatar(file, owner) {
13658
13909
  if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
13659
13910
  const original = Buffer.from(await file.arrayBuffer());
13660
13911
  const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
13661
- const key = `${avatarKeyPrefix()}${owner}-${randomUUID3()}.png`;
13912
+ const key = `${avatarKeyPrefix()}${owner}-${randomUUID4()}.png`;
13662
13913
  await getR2Client().send(
13663
13914
  new PutObjectCommand4({
13664
13915
  Bucket: getR2Bucket(),
@@ -13673,7 +13924,7 @@ function avatarKeyPrefix() {
13673
13924
  return `${KEY_PREFIX3}/${getProfile()}/`;
13674
13925
  }
13675
13926
  function createAvatarsRouter() {
13676
- const app = new Hono20();
13927
+ const app = new Hono21();
13677
13928
  app.get("/avatars/slack", async (c) => {
13678
13929
  const ctx = await resolveAuthContext(c);
13679
13930
  if (!ctx) return c.json({ error: "not signed in" }, 401);
@@ -13722,7 +13973,7 @@ function createAvatarsRouter() {
13722
13973
  }
13723
13974
 
13724
13975
  // src/dashboard/members-routes.ts
13725
- import { Hono as Hono21 } from "hono";
13976
+ import { Hono as Hono22 } from "hono";
13726
13977
  function inviteLinkFor(c, token) {
13727
13978
  const url = new URL("/auth/invite", callbackUrl(c));
13728
13979
  url.searchParams.set("token", token);
@@ -13730,7 +13981,7 @@ function inviteLinkFor(c, token) {
13730
13981
  }
13731
13982
  var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
13732
13983
  function createMembersRouter() {
13733
- const members = new Hono21();
13984
+ const members = new Hono22();
13734
13985
  members.get("/", async (c) => {
13735
13986
  const ctx = await resolveAuthContext(c);
13736
13987
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -13771,7 +14022,7 @@ function createMembersRouter() {
13771
14022
  return { userId: m.userId, handle: m.handle, role: m.role, avatarUrl };
13772
14023
  })
13773
14024
  );
13774
- return c.json({ members: directory });
14025
+ return c.json({ workspaceId: ctx.workspaceId, members: directory });
13775
14026
  });
13776
14027
  members.post("/:userId/dm", async (c) => {
13777
14028
  const ctx = await resolveAuthContext(c);
@@ -13945,7 +14196,7 @@ function createMembersRouter() {
13945
14196
  }
13946
14197
 
13947
14198
  // src/dashboard/notifications-routes.ts
13948
- import { Hono as Hono22 } from "hono";
14199
+ import { Hono as Hono23 } from "hono";
13949
14200
 
13950
14201
  // src/store/notification-store.ts
13951
14202
  function rowToNotification(row) {
@@ -14013,7 +14264,7 @@ async function markAllRead(channelIds, userId) {
14013
14264
 
14014
14265
  // src/dashboard/notifications-routes.ts
14015
14266
  function createNotificationsRouter() {
14016
- const notifications = new Hono22();
14267
+ const notifications = new Hono23();
14017
14268
  notifications.get("/", async (c) => {
14018
14269
  const auth = c.get("auth");
14019
14270
  const channelIds = getProfileChannelRegistry().filter((ch) => canSeeChannel(ch, auth)).map((ch) => ch.id);
@@ -14040,7 +14291,7 @@ function createNotificationsRouter() {
14040
14291
  return notifications;
14041
14292
  }
14042
14293
  function createThreadReadRouter() {
14043
- const threads = new Hono22();
14294
+ const threads = new Hono23();
14044
14295
  threads.post("/threads/:threadId/read", async (c) => {
14045
14296
  const { userId } = c.get("auth");
14046
14297
  const threadId = c.req.param("threadId");
@@ -14052,7 +14303,7 @@ function createThreadReadRouter() {
14052
14303
  }
14053
14304
 
14054
14305
  // src/dashboard/push-routes.ts
14055
- import { Hono as Hono23 } from "hono";
14306
+ import { Hono as Hono24 } from "hono";
14056
14307
 
14057
14308
  // src/push/send.ts
14058
14309
  import webpush from "web-push";
@@ -14155,7 +14406,7 @@ async function pushToUser(userId, payload) {
14155
14406
 
14156
14407
  // src/dashboard/push-routes.ts
14157
14408
  function createPushRouter() {
14158
- const push = new Hono23();
14409
+ const push = new Hono24();
14159
14410
  push.get("/key", (c) => c.json({ publicKey: getVapidPublicKey() }));
14160
14411
  push.post("/subscribe", async (c) => {
14161
14412
  const { userId } = c.get("auth");
@@ -14186,7 +14437,7 @@ function createPushRouter() {
14186
14437
  }
14187
14438
 
14188
14439
  // src/dashboard/mcp-oauth-routes.ts
14189
- import { Hono as Hono24 } from "hono";
14440
+ import { Hono as Hono25 } from "hono";
14190
14441
  var CONNECTIONS_PAGE = "/settings/connections";
14191
14442
  function callbackUri(c) {
14192
14443
  const proto = c.req.header("x-forwarded-proto") ?? "http";
@@ -14201,7 +14452,7 @@ function backToDashboard(c, to, params) {
14201
14452
  return c.redirect(`${target.pathname}${target.search}`);
14202
14453
  }
14203
14454
  function createMcpOAuthRouter() {
14204
- const app = new Hono24();
14455
+ const app = new Hono25();
14205
14456
  app.get("/oauth/:provider/start", async (c) => {
14206
14457
  const auth = c.get("auth");
14207
14458
  if (auth.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -14328,7 +14579,7 @@ function createMcpOAuthRouter() {
14328
14579
  }
14329
14580
 
14330
14581
  // src/dashboard/mcp-routes.ts
14331
- import { Hono as Hono25 } from "hono";
14582
+ import { Hono as Hono26 } from "hono";
14332
14583
 
14333
14584
  // src/mcp/transport.ts
14334
14585
  var KINDS = ["inherit", "npx", "local", "http", "sse"];
@@ -14422,7 +14673,7 @@ function fromServerCommand(command) {
14422
14673
 
14423
14674
  // src/dashboard/mcp-routes.ts
14424
14675
  var CUSTOM_PROVIDER = "custom";
14425
- function requireAdmin3(auth) {
14676
+ function requireAdmin4(auth) {
14426
14677
  return auth?.role === "admin" ? null : "admin access required";
14427
14678
  }
14428
14679
  function toResponse4(connection, channelIds, providerCommand = null) {
@@ -14458,9 +14709,9 @@ function bindingsByConnection() {
14458
14709
  return map;
14459
14710
  }
14460
14711
  function createMcpRouter() {
14461
- const app = new Hono25();
14712
+ const app = new Hono26();
14462
14713
  app.get("/providers", async (c) => {
14463
- const denied = requireAdmin3(c.get("auth"));
14714
+ const denied = requireAdmin4(c.get("auth"));
14464
14715
  if (denied) return c.json({ error: denied }, 403);
14465
14716
  const providers = await listMcpProviders();
14466
14717
  return c.json({
@@ -14478,7 +14729,7 @@ function createMcpRouter() {
14478
14729
  });
14479
14730
  app.get("/connections", async (c) => {
14480
14731
  const auth = c.get("auth");
14481
- const denied = requireAdmin3(auth);
14732
+ const denied = requireAdmin4(auth);
14482
14733
  if (denied) return c.json({ error: denied }, 403);
14483
14734
  const connections = await listMcpConnections(auth.workspaceId);
14484
14735
  const commands = new Map(
@@ -14493,7 +14744,7 @@ function createMcpRouter() {
14493
14744
  });
14494
14745
  app.post("/connections", async (c) => {
14495
14746
  const auth = c.get("auth");
14496
- const denied = requireAdmin3(auth);
14747
+ const denied = requireAdmin4(auth);
14497
14748
  if (denied) return c.json({ error: denied }, 403);
14498
14749
  const body = await c.req.json().catch(() => null);
14499
14750
  const label2 = typeof body?.label === "string" ? body.label.trim() : "";
@@ -14530,7 +14781,7 @@ function createMcpRouter() {
14530
14781
  });
14531
14782
  app.patch("/connections/:id", async (c) => {
14532
14783
  const auth = c.get("auth");
14533
- const denied = requireAdmin3(auth);
14784
+ const denied = requireAdmin4(auth);
14534
14785
  if (denied) return c.json({ error: denied }, 403);
14535
14786
  const body = await c.req.json().catch(() => null);
14536
14787
  if (!body) return c.json({ error: "a body is required" }, 400);
@@ -14561,7 +14812,7 @@ function createMcpRouter() {
14561
14812
  });
14562
14813
  app.put("/connections/:id/credential", async (c) => {
14563
14814
  const auth = c.get("auth");
14564
- const denied = requireAdmin3(auth);
14815
+ const denied = requireAdmin4(auth);
14565
14816
  if (denied) return c.json({ error: denied }, 403);
14566
14817
  const body = await c.req.json().catch(() => null);
14567
14818
  const raw = body?.apiKey;
@@ -14579,7 +14830,7 @@ function createMcpRouter() {
14579
14830
  });
14580
14831
  app.put("/connections/:id/channels", async (c) => {
14581
14832
  const auth = c.get("auth");
14582
- const denied = requireAdmin3(auth);
14833
+ const denied = requireAdmin4(auth);
14583
14834
  if (denied) return c.json({ error: denied }, 403);
14584
14835
  const id = c.req.param("id");
14585
14836
  const connection = await getMcpConnection(auth.workspaceId, id);
@@ -14606,7 +14857,7 @@ function createMcpRouter() {
14606
14857
  });
14607
14858
  app.delete("/connections/:id", async (c) => {
14608
14859
  const auth = c.get("auth");
14609
- const denied = requireAdmin3(auth);
14860
+ const denied = requireAdmin4(auth);
14610
14861
  if (denied) return c.json({ error: denied }, 403);
14611
14862
  const deleted = await deleteMcpConnection(auth.workspaceId, c.req.param("id"));
14612
14863
  if (!deleted) return c.json({ error: "connection not found" }, 404);
@@ -14717,7 +14968,7 @@ async function sessionUsageForThreads(workspace, threadTsList) {
14717
14968
  }
14718
14969
 
14719
14970
  // src/dashboard/usage-report.ts
14720
- import { existsSync as existsSync21 } from "fs";
14971
+ import { existsSync as existsSync22 } from "fs";
14721
14972
  function mergeByLabel(buckets) {
14722
14973
  const merged = /* @__PURE__ */ new Map();
14723
14974
  for (const bucket of buckets) {
@@ -14763,7 +15014,7 @@ function toReport(days, rows, labelChannel) {
14763
15014
  };
14764
15015
  }
14765
15016
  function fromLocalStore(days, labelChannel) {
14766
- if (!existsSync21(paths.DB_FILE)) return { ...EMPTY, days };
15017
+ if (!existsSync22(paths.DB_FILE)) return { ...EMPTY, days };
14767
15018
  const window = days === 0 ? void 0 : days;
14768
15019
  const store = new UsageStore(paths.DB_FILE);
14769
15020
  try {
@@ -14805,7 +15056,7 @@ async function buildChannelUsageReport(channelId, days, labelChannel) {
14805
15056
  const { totals, byDay } = await channelUsageForWorkspace(workspace, aliasIds, window);
14806
15057
  return { days, totals: totals.runs > 0 ? totals : null, byDay };
14807
15058
  } catch {
14808
- if (!existsSync21(paths.DB_FILE)) return { days, totals: null, byDay: [] };
15059
+ if (!existsSync22(paths.DB_FILE)) return { days, totals: null, byDay: [] };
14809
15060
  const store = new UsageStore(paths.DB_FILE);
14810
15061
  try {
14811
15062
  const targetLabel = labelChannel(channelId);
@@ -14827,7 +15078,7 @@ async function buildThreadUsage(threadTs) {
14827
15078
  if (totals.runs === 0) return { threadTs, totals: null, byModel: [] };
14828
15079
  return { threadTs, totals, byModel };
14829
15080
  } catch {
14830
- if (!existsSync21(paths.DB_FILE)) return { threadTs, totals: null, byModel: [] };
15081
+ if (!existsSync22(paths.DB_FILE)) return { threadTs, totals: null, byModel: [] };
14831
15082
  const store = new UsageStore(paths.DB_FILE);
14832
15083
  try {
14833
15084
  const totals = store.threadTotals(threadTs);
@@ -14840,7 +15091,7 @@ async function buildThreadUsage(threadTs) {
14840
15091
  }
14841
15092
 
14842
15093
  // src/dashboard/channel-members-routes.ts
14843
- import { Hono as Hono26 } from "hono";
15094
+ import { Hono as Hono27 } from "hono";
14844
15095
  async function isChannelAdmin(ctx, channelId) {
14845
15096
  if (ctx.role === "admin") return true;
14846
15097
  const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
@@ -14849,7 +15100,7 @@ async function isChannelAdmin(ctx, channelId) {
14849
15100
  return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
14850
15101
  }
14851
15102
  function createChannelMembersRouter() {
14852
- const router = new Hono26();
15103
+ const router = new Hono27();
14853
15104
  router.get("/:id/members", async (c) => {
14854
15105
  const ctx = await resolveAuthContext(c);
14855
15106
  if (!ctx) return c.json({ error: "workspace access required" }, 403);
@@ -14897,9 +15148,9 @@ function createChannelMembersRouter() {
14897
15148
  if (!await isChannelAdmin(ctx, channelId)) {
14898
15149
  return c.json({ error: "channel admin access required" }, 403);
14899
15150
  }
14900
- return c.json({ grants: await listChannelDocsAccess(channelId) });
15151
+ return c.json({ memberAccess: await getChannelDocsMemberAccess(channelId) });
14901
15152
  });
14902
- router.put("/:id/docs-access/:membershipId", async (c) => {
15153
+ router.put("/:id/docs-access", async (c) => {
14903
15154
  const ctx = await resolveAuthContext(c);
14904
15155
  if (!ctx) return c.json({ error: "workspace access required" }, 403);
14905
15156
  const channelId = c.req.param("id");
@@ -14907,32 +15158,31 @@ function createChannelMembersRouter() {
14907
15158
  return c.json({ error: "channel admin access required" }, 403);
14908
15159
  }
14909
15160
  const body = await c.req.json().catch(() => ({}));
14910
- if (!isDocsGrant(body.access)) {
14911
- return c.json({ error: "access must be 'read' or 'write'" }, 400);
15161
+ if (!isDocsAccess(body.memberAccess)) {
15162
+ return c.json({ error: "memberAccess must be 'none', 'read' or 'write'" }, 400);
14912
15163
  }
14913
- const grant = await upsertChannelDocsAccess({
14914
- channelId,
14915
- membershipId: c.req.param("membershipId"),
14916
- access: body.access
14917
- });
14918
- return c.json({ grant });
14919
- });
14920
- router.delete("/:id/docs-access/:membershipId", async (c) => {
14921
- const ctx = await resolveAuthContext(c);
14922
- if (!ctx) return c.json({ error: "workspace access required" }, 403);
14923
- const channelId = c.req.param("id");
14924
- if (!await isChannelAdmin(ctx, channelId)) {
14925
- return c.json({ error: "channel admin access required" }, 403);
15164
+ if (!await setChannelDocsMemberAccess(channelId, body.memberAccess)) {
15165
+ return c.json({ error: "no such channel" }, 404);
14926
15166
  }
14927
- const removed = await removeChannelDocsAccess(channelId, c.req.param("membershipId"));
14928
- if (!removed) return c.json({ error: "no such docs grant" }, 404);
14929
- return c.body(null, 204);
15167
+ return c.json({ memberAccess: body.memberAccess });
14930
15168
  });
14931
15169
  return router;
14932
15170
  }
14933
15171
 
14934
15172
  // src/dashboard/memories-routes.ts
14935
- import { Hono as Hono27 } from "hono";
15173
+ import { Hono as Hono28 } from "hono";
15174
+ async function readPatch(c) {
15175
+ const body = await c.req.json().catch(() => ({}));
15176
+ const patch = {};
15177
+ if (typeof body["description"] === "string") patch.description = body["description"];
15178
+ if (typeof body["body"] === "string") patch.body = body["body"];
15179
+ const type = body["type"];
15180
+ if (type !== void 0) {
15181
+ if (!isMemoryType(type)) return "invalid type";
15182
+ patch.type = type;
15183
+ }
15184
+ return patch;
15185
+ }
14936
15186
  var requireChannelVisible = async (c, next) => {
14937
15187
  const channel = findChannel(c.req.param("id"));
14938
15188
  if (!channel || !canSeeChannel(channel, c.get("auth"))) {
@@ -14948,7 +15198,7 @@ var requireAgentMemory = async (c, next) => {
14948
15198
  await next();
14949
15199
  };
14950
15200
  function createMemoriesRouter() {
14951
- const router = new Hono27();
15201
+ const router = new Hono28();
14952
15202
  for (const path of [
14953
15203
  "/:id/memories",
14954
15204
  "/:id/memories/*",
@@ -14963,15 +15213,8 @@ function createMemoriesRouter() {
14963
15213
  router.patch("/:id/memories/:memoryId", async (c) => {
14964
15214
  const memoryId = c.req.param("memoryId");
14965
15215
  if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
14966
- const body = await c.req.json().catch(() => ({}));
14967
- const patch = {};
14968
- if (typeof body["description"] === "string") patch.description = body["description"];
14969
- if (typeof body["body"] === "string") patch.body = body["body"];
14970
- const type = body["type"];
14971
- if (type !== void 0) {
14972
- if (!isMemoryType(type)) return c.json({ error: "invalid type" }, 400);
14973
- patch.type = type;
14974
- }
15216
+ const patch = await readPatch(c);
15217
+ if (typeof patch === "string") return c.json({ error: patch }, 400);
14975
15218
  try {
14976
15219
  const memory = await updateChannelMemory(c.get("memoryChannelId"), memoryId, patch, "user");
14977
15220
  if (!memory) return c.json({ error: "memory not found" }, 404);
@@ -14987,9 +15230,28 @@ function createMemoriesRouter() {
14987
15230
  if (!deleted) return c.json({ error: "memory not found" }, 404);
14988
15231
  return c.json({ ok: true });
14989
15232
  });
15233
+ router.post("/:id/memories/:memoryId/promote", async (c) => {
15234
+ const memoryId = c.req.param("memoryId");
15235
+ if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
15236
+ if (c.get("auth").role !== "admin") {
15237
+ return c.json({ error: "Only a Workspace Admin can move a memory to the workspace." }, 403);
15238
+ }
15239
+ if (findChannel(c.get("memoryChannelId"))?.visibility !== "public") {
15240
+ return c.json({ error: "Memories of a private channel stay in that channel." }, 403);
15241
+ }
15242
+ const result = await promoteChannelMemory(c.get("memoryChannelId"), memoryId);
15243
+ if (!result.ok && result.reason === "not-found") return c.json({ error: "memory not found" }, 404);
15244
+ if (!result.ok) {
15245
+ return c.json({ error: "The workspace already has a memory with this name." }, 409);
15246
+ }
15247
+ return c.json({ memory: result.memory });
15248
+ });
14990
15249
  router.get("/:id/memory-events", async (c) => {
14991
- const threadId = c.req.query("thread") ?? "";
14992
- if (!isUuid(threadId)) return c.json({ error: "thread query parameter required" }, 400);
15250
+ const threadId = c.req.query("thread");
15251
+ if (threadId === void 0) {
15252
+ return c.json({ events: await listMemoryEvents(c.get("memoryChannelId"), { limit: 20 }) });
15253
+ }
15254
+ if (!isUuid(threadId)) return c.json({ error: "invalid thread id" }, 400);
14993
15255
  return c.json({ events: await listThreadMemoryEvents(c.get("memoryChannelId"), threadId) });
14994
15256
  });
14995
15257
  router.post("/:id/memory-events/:eventId/undo", async (c) => {
@@ -15008,9 +15270,68 @@ function createMemoriesRouter() {
15008
15270
  });
15009
15271
  return router;
15010
15272
  }
15273
+ function createScopedMemoriesRouter() {
15274
+ const router = new Hono28();
15275
+ router.use("*", requireAgentMemory);
15276
+ const workspace = (c) => ({
15277
+ scope: "workspace",
15278
+ workspaceId: c.get("auth").workspaceId
15279
+ });
15280
+ const mine = (c) => ({
15281
+ scope: "user",
15282
+ workspaceId: c.get("auth").workspaceId,
15283
+ userId: c.get("auth").userId
15284
+ });
15285
+ router.on(["PATCH", "DELETE"], "/workspace/*", async (c, next) => {
15286
+ if (c.get("auth").role !== "admin") {
15287
+ return c.json({ error: "Only a Workspace Admin can change workspace memory." }, 403);
15288
+ }
15289
+ await next();
15290
+ });
15291
+ for (const [path, target] of [
15292
+ ["/workspace", workspace],
15293
+ ["/me", mine]
15294
+ ]) {
15295
+ router.get(path, async (c) => c.json({ memories: await listMemories(target(c)) }));
15296
+ router.patch(`${path}/:memoryId`, async (c) => {
15297
+ const memoryId = c.req.param("memoryId") ?? "";
15298
+ if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
15299
+ const patch = await readPatch(c);
15300
+ if (typeof patch === "string") return c.json({ error: patch }, 400);
15301
+ try {
15302
+ const memory = await updateMemory(target(c), memoryId, patch, "user");
15303
+ if (!memory) return c.json({ error: "memory not found" }, 404);
15304
+ return c.json({ memory });
15305
+ } catch (err) {
15306
+ return c.json({ error: err.message }, 400);
15307
+ }
15308
+ });
15309
+ router.delete(`${path}/:memoryId`, async (c) => {
15310
+ const memoryId = c.req.param("memoryId") ?? "";
15311
+ if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
15312
+ if (!await deleteMemory(target(c), memoryId, "user")) {
15313
+ return c.json({ error: "memory not found" }, 404);
15314
+ }
15315
+ return c.json({ ok: true });
15316
+ });
15317
+ }
15318
+ router.get("/me-settings", async (c) => {
15319
+ const auth = c.get("auth");
15320
+ const membership = await getMembershipByUserId(auth.workspaceId, auth.userId);
15321
+ return c.json({ autoPaused: membership?.memoryAutoPaused ?? false });
15322
+ });
15323
+ router.put("/me-settings", async (c) => {
15324
+ const body = await c.req.json().catch(() => ({}));
15325
+ if (typeof body["autoPaused"] !== "boolean") return c.json({ error: "autoPaused must be a boolean" }, 400);
15326
+ const auth = c.get("auth");
15327
+ await setMemoryAutoPaused(auth.workspaceId, auth.userId, body["autoPaused"]);
15328
+ return c.json({ autoPaused: body["autoPaused"] });
15329
+ });
15330
+ return router;
15331
+ }
15011
15332
 
15012
15333
  // src/dashboard/onboarding-routes.ts
15013
- import { Hono as Hono28 } from "hono";
15334
+ import { Hono as Hono29 } from "hono";
15014
15335
 
15015
15336
  // src/dashboard/unique-channel-name.ts
15016
15337
  function uniqueChannelName(raw, taken) {
@@ -15111,7 +15432,7 @@ async function buildGreeting(input2) {
15111
15432
  // src/dashboard/onboarding-routes.ts
15112
15433
  var MAX_FOLDERS = 20;
15113
15434
  function createOnboardingRouter(agent) {
15114
- const app = new Hono28();
15435
+ const app = new Hono29();
15115
15436
  app.get("/scan", async (c) => {
15116
15437
  const ctx = await resolveAuthContext(c);
15117
15438
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -15277,13 +15598,13 @@ function firstSentence(body) {
15277
15598
  function threadLabel(row) {
15278
15599
  return row.title?.trim() || firstSentence(row.thread_replies?.[0]?.body);
15279
15600
  }
15280
- async function resolveThreadLinks(sessions) {
15601
+ async function resolveThreadLinks(sessions2) {
15281
15602
  const chatIds = [
15282
- ...new Set(sessions.filter((s) => s.channelId.startsWith("chat:")).map((s) => s.threadTs))
15603
+ ...new Set(sessions2.filter((s) => s.channelId.startsWith("chat:")).map((s) => s.threadTs))
15283
15604
  ];
15284
15605
  const externalKeys = [
15285
15606
  ...new Set(
15286
- sessions.filter((s) => !s.channelId.startsWith("chat:")).map((s) => `${s.channelId}:${s.threadTs}`)
15607
+ sessions2.filter((s) => !s.channelId.startsWith("chat:")).map((s) => `${s.channelId}:${s.threadTs}`)
15287
15608
  )
15288
15609
  ];
15289
15610
  const chat = /* @__PURE__ */ new Map();
@@ -15316,8 +15637,8 @@ function registerDataRoutes(api, agent = {}) {
15316
15637
  api.get("/host/status", async (c) => {
15317
15638
  const online = agent.isHostOnline?.(getProfile()) ?? false;
15318
15639
  if (!online) {
15319
- const failure2 = agent.getHostOfflineReason?.() ?? null;
15320
- return c.json({ online: false, reason: failure2?.reason ?? null, since: failure2?.since ?? null });
15640
+ const failure3 = agent.getHostOfflineReason?.() ?? null;
15641
+ return c.json({ online: false, reason: failure3?.reason ?? null, since: failure3?.since ?? null });
15321
15642
  }
15322
15643
  const auth = c.get("auth");
15323
15644
  const paused = await getAgentPaused(auth.workspaceId);
@@ -15368,6 +15689,7 @@ function registerDataRoutes(api, agent = {}) {
15368
15689
  api.route("/onboarding", createOnboardingRouter(agent));
15369
15690
  api.route("/channels", createChannelMembersRouter());
15370
15691
  api.route("/channels", createMemoriesRouter());
15692
+ api.route("/memories", createScopedMemoriesRouter());
15371
15693
  api.route("/docs", createDocShareListRouter());
15372
15694
  api.get("/users", (c) => c.json({ users: listActiveUsers() }));
15373
15695
  api.route("/categories", createCategoryWriteRouter());
@@ -15379,7 +15701,7 @@ function registerDataRoutes(api, agent = {}) {
15379
15701
  runs.set(name, { lastRun: run4.lastRunAt.toISOString(), lastStatus: run4.lastStatus });
15380
15702
  }
15381
15703
  } catch {
15382
- if (existsSync22(paths.DB_FILE)) {
15704
+ if (existsSync23(paths.DB_FILE)) {
15383
15705
  const runStore = new AutomationRunStore(paths.DB_FILE);
15384
15706
  try {
15385
15707
  for (const run4 of runStore.listLastRuns()) {
@@ -15441,7 +15763,7 @@ function registerDataRoutes(api, agent = {}) {
15441
15763
  const runs = (await recentRunsForWorkspace(getProfile(), 200)).map(toResponse5);
15442
15764
  return c.json({ runs });
15443
15765
  } catch {
15444
- if (!existsSync22(paths.DB_FILE)) return c.json({ runs: [] });
15766
+ if (!existsSync23(paths.DB_FILE)) return c.json({ runs: [] });
15445
15767
  const runStore = new AutomationRunStore(paths.DB_FILE);
15446
15768
  try {
15447
15769
  const runs = runStore.listRuns(200).map(toResponse5);
@@ -15563,7 +15885,7 @@ function registerDataRoutes(api, agent = {}) {
15563
15885
  } catch {
15564
15886
  latest = /* @__PURE__ */ new Map();
15565
15887
  totals = /* @__PURE__ */ new Map();
15566
- if (existsSync22(paths.DB_FILE)) {
15888
+ if (existsSync23(paths.DB_FILE)) {
15567
15889
  const usageStore = new UsageStore(paths.DB_FILE);
15568
15890
  try {
15569
15891
  latest = usageStore.latestByThread(threadTsList);
@@ -15574,7 +15896,7 @@ function registerDataRoutes(api, agent = {}) {
15574
15896
  }
15575
15897
  }
15576
15898
  const threadLinks = await resolveThreadLinks(raw);
15577
- const sessions = raw.map((s) => {
15899
+ const sessions2 = raw.map((s) => {
15578
15900
  const run4 = latest.get(s.threadTs);
15579
15901
  const turns = run4?.numTurns ?? 1;
15580
15902
  const total = run4 ? run4.inputTokens + run4.cacheReadTokens + run4.cacheCreationTokens : void 0;
@@ -15592,7 +15914,7 @@ function registerDataRoutes(api, agent = {}) {
15592
15914
  costUsd: totals.get(s.threadTs)?.costUsd
15593
15915
  };
15594
15916
  });
15595
- return c.json({ sessions });
15917
+ return c.json({ sessions: sessions2 });
15596
15918
  });
15597
15919
  const LOGO_SIZE = 256;
15598
15920
  const MAX_LOGO_BYTES = 2 * 1024 * 1024;
@@ -15618,18 +15940,18 @@ function registerDataRoutes(api, agent = {}) {
15618
15940
  // src/dashboard/device-auth-routes.ts
15619
15941
  import { verify } from "crypto";
15620
15942
  import { getConnInfo } from "@hono/node-server/conninfo";
15621
- import { Hono as Hono30 } from "hono";
15943
+ import { Hono as Hono31 } from "hono";
15622
15944
 
15623
15945
  // src/dashboard/agent-bridge.ts
15624
- import { randomBytes as randomBytes8, randomUUID as randomUUID4, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
15946
+ import { randomBytes as randomBytes8, randomUUID as randomUUID5, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
15625
15947
 
15626
15948
  // src/dashboard/docs-relay.ts
15627
- import { Hono as Hono29 } from "hono";
15949
+ import { Hono as Hono30 } from "hono";
15628
15950
  import { streamSSE as streamSSE2 } from "hono/streaming";
15629
15951
  var FORWARDED_HEADERS = ["content-type", "host", "accept-language"];
15630
15952
  var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
15631
15953
  async function handleDocsRequest(req) {
15632
- const app = new Hono29();
15954
+ const app = new Hono30();
15633
15955
  app.use("*", async (c, next) => {
15634
15956
  c.set("auth", req.auth);
15635
15957
  await next();
@@ -15688,24 +16010,24 @@ function relay2(agent) {
15688
16010
  }
15689
16011
 
15690
16012
  // src/dashboard/agent-bridge.ts
15691
- import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
16013
+ import { existsSync as existsSync25, readFileSync as readFileSync19 } from "fs";
15692
16014
  import dotenv from "dotenv";
15693
16015
  import { WebSocket, WebSocketServer } from "ws";
15694
16016
 
15695
16017
  // src/dashboard/host-identity.ts
15696
16018
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
15697
- import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
15698
- import { dirname as dirname7, join as join23 } from "path";
16019
+ import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
16020
+ import { dirname as dirname8, join as join24 } from "path";
15699
16021
  function hostPrivateKeyPath() {
15700
- return process.env["FLYSPACE_HOST_KEY_PATH"] || join23(paths.BASE_CONFIG_DIR, "host.key");
16022
+ return process.env["FLYSPACE_HOST_KEY_PATH"] || join24(paths.BASE_CONFIG_DIR, "host.key");
15701
16023
  }
15702
16024
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15703
- if (!existsSync23(path)) {
16025
+ if (!existsSync24(path)) {
15704
16026
  throw new Error(
15705
16027
  `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.`
15706
16028
  );
15707
16029
  }
15708
- return createPrivateKey2(readFileSync16(path, "utf8"));
16030
+ return createPrivateKey2(readFileSync17(path, "utf8"));
15709
16031
  }
15710
16032
  function loadHostPublicKey() {
15711
16033
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -15950,7 +16272,7 @@ async function userEmail(userId) {
15950
16272
 
15951
16273
  // src/dashboard/doc-share-publisher.ts
15952
16274
  import { randomBytes as randomBytes7 } from "crypto";
15953
- import { readFileSync as readFileSync17, statSync as statSync9 } from "fs";
16275
+ import { readFileSync as readFileSync18, statSync as statSync9 } from "fs";
15954
16276
  import { extname as extname2, posix } from "path";
15955
16277
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
15956
16278
 
@@ -16137,7 +16459,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16137
16459
  }
16138
16460
  if (!stat.isFile()) return { status: "not-found" };
16139
16461
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
16140
- const source = readFileSync17(abs, "utf8");
16462
+ const source = readFileSync18(abs, "utf8");
16141
16463
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
16142
16464
  if (kind === "excalidraw") {
16143
16465
  try {
@@ -16217,7 +16539,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16217
16539
  let text = null;
16218
16540
  try {
16219
16541
  const size = statSync9(docAbs).size;
16220
- if (size <= budget) text = readFileSync17(docAbs, "utf8");
16542
+ if (size <= budget) text = readFileSync18(docAbs, "utf8");
16221
16543
  } catch {
16222
16544
  continue;
16223
16545
  }
@@ -16234,7 +16556,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16234
16556
  let body;
16235
16557
  try {
16236
16558
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
16237
- body = readFileSync17(resolved2.abs);
16559
+ body = readFileSync18(resolved2.abs);
16238
16560
  } catch {
16239
16561
  continue;
16240
16562
  }
@@ -16327,13 +16649,14 @@ var METHODS = {
16327
16649
  threads: ["GET", "HEAD", "POST", "PATCH"],
16328
16650
  thread_replies: ["GET", "HEAD", "POST", "PATCH"],
16329
16651
  memberships: ["GET", "HEAD"],
16652
+ feature_flags: ["GET", "HEAD"],
16330
16653
  channel_members: ["GET", "HEAD"],
16331
- channel_docs_access: ["GET", "HEAD"],
16332
16654
  memories: ALL,
16333
16655
  memory_events: ["GET", "HEAD", "POST", "PATCH"]
16334
16656
  };
16335
- var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members", "channel_docs_access"]);
16336
- var CHANNEL_OWNED = /* @__PURE__ */ new Set(["memories", "memory_events"]);
16657
+ var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members"]);
16658
+ var MEMORY_TABLES = /* @__PURE__ */ new Set(["memories", "memory_events"]);
16659
+ var MEMORY_KEYS = ["channel_id", "workspace_id", "user_id", "scope"];
16337
16660
  var FORWARDED_HEADERS2 = ["accept", "content-type", "prefer", "range"];
16338
16661
  var deny = (message, status = 403) => ({
16339
16662
  status,
@@ -16440,7 +16763,30 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
16440
16763
  }
16441
16764
  }
16442
16765
  params.append(slugColumn, inList(scope));
16443
- } else if (table === "memberships") {
16766
+ } else if (MEMORY_TABLES.has(table)) {
16767
+ const [workspaceIds, channelIds] = await Promise.all([
16768
+ ownedWorkspaceIds(forward, workspaces),
16769
+ ownedChannelIds(forward, workspaces)
16770
+ ]);
16771
+ if (!workspaceIds || !channelIds) return deny("Could not resolve the device workspaces.", 502);
16772
+ if (method === "POST") {
16773
+ const ok3 = rows.length > 0 && rows.every((r) => {
16774
+ const channel = r["channel_id"];
16775
+ if (channel !== void 0 && channel !== null) {
16776
+ return typeof channel === "string" && channelIds.includes(channel);
16777
+ }
16778
+ return typeof r["workspace_id"] === "string" && workspaceIds.includes(r["workspace_id"]);
16779
+ });
16780
+ if (!ok3) return deny(`Every ${table} row must belong to a channel or workspace of the device.`);
16781
+ }
16782
+ if (method === "PATCH") {
16783
+ const patch = bodyRows(req.body);
16784
+ if (!patch || patch.some((r) => MEMORY_KEYS.some((k) => k in r))) {
16785
+ return deny(`A ${table} row cannot move to another channel, workspace or person.`);
16786
+ }
16787
+ }
16788
+ params.append("workspace_id", inList(workspaceIds));
16789
+ } else if (table === "memberships" || table === "feature_flags") {
16444
16790
  const workspaceIds = await ownedWorkspaceIds(forward, workspaces);
16445
16791
  if (!workspaceIds) return deny("Could not resolve the device workspaces.", 502);
16446
16792
  params.append("workspace_id", inList(workspaceIds));
@@ -16449,16 +16795,6 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
16449
16795
  if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
16450
16796
  if (CHANNEL_SCOPED.has(table)) {
16451
16797
  params.append("channel_id", inList(channelIds));
16452
- } else if (CHANNEL_OWNED.has(table)) {
16453
- if (method === "POST") {
16454
- const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
16455
- if (!ok3) return deny(`Every ${table} row must belong to a channel of the device workspaces.`);
16456
- }
16457
- if (method === "PATCH") {
16458
- const patch = bodyRows(req.body);
16459
- if (!patch || patch.some((r) => "channel_id" in r)) return deny(`A ${table} row cannot move channels.`);
16460
- }
16461
- params.append("channel_id", inList(channelIds));
16462
16798
  } else if (table === "threads") {
16463
16799
  if (method === "POST") {
16464
16800
  const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
@@ -16532,7 +16868,7 @@ function dataSocketUrl() {
16532
16868
  return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
16533
16869
  }
16534
16870
  function listLocalSessions() {
16535
- if (!existsSync24(paths.DB_FILE)) return [];
16871
+ if (!existsSync25(paths.DB_FILE)) return [];
16536
16872
  const store = new SessionStore(paths.DB_FILE);
16537
16873
  try {
16538
16874
  return store.listSessions();
@@ -16559,6 +16895,7 @@ function localAgentCallbacks(chatAgent) {
16559
16895
  saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
16560
16896
  writeSkill: (ref, write) => writeSkillAndSync(ref, write),
16561
16897
  outputStylesRequest: (req) => handleOutputStylesRequest(req),
16898
+ providerLoginRequest: (req) => handleProviderLoginRequest(req),
16562
16899
  docsRequest: (req) => handleDocsRequest(req),
16563
16900
  listSessions: () => listLocalSessions(),
16564
16901
  scanOnboarding: () => scanOnboarding(),
@@ -16571,7 +16908,7 @@ function buildHostConfigSnapshot() {
16571
16908
  const snapshot2 = {};
16572
16909
  for (const profile of listProfiles()) {
16573
16910
  snapshot2[profile] = runWithProfile(profile, () => ({
16574
- env: existsSync24(paths.CONFIG_FILE) ? dotenv.parse(readFileSync18(paths.CONFIG_FILE, "utf-8")) : {},
16911
+ env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync19(paths.CONFIG_FILE, "utf-8")) : {},
16575
16912
  agentModel: loadDashboardSettings().agent
16576
16913
  }));
16577
16914
  }
@@ -16784,7 +17121,7 @@ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
16784
17121
  return Promise.resolve(null);
16785
17122
  }
16786
17123
  if (method === "docsRequest") console.log(`[host-socket] docsRequest for "${profile}" -> device ${hosts.get(ws)?.deviceId ?? "legacy"}`);
16787
- const id = randomUUID4();
17124
+ const id = randomUUID5();
16788
17125
  return new Promise((resolve7) => {
16789
17126
  const timer2 = setTimeout(() => {
16790
17127
  pending2.delete(id);
@@ -16848,6 +17185,8 @@ function remoteAgentCallbacks() {
16848
17185
  writeSkill: (ref, write) => callHost("writeSkill", [ref, write]),
16849
17186
  // Same posture as writeSkill: `null` becomes a 503.
16850
17187
  outputStylesRequest: (req) => callHost("outputStylesRequest", [req]),
17188
+ // Submitting a Claude code waits on the CLI to mint the token.
17189
+ providerLoginRequest: (req) => callHost("providerLoginRequest", [req], DOCS_CALL_TIMEOUT_MS),
16851
17190
  // Same posture again. Longer timeout: a first translation of a doc runs a
16852
17191
  // model call on the Host before it answers.
16853
17192
  docsRequest: (req) => callHost("docsRequest", [req], DOCS_CALL_TIMEOUT_MS),
@@ -17112,7 +17451,7 @@ async function boundWorkspaceIds(publicKey, userId) {
17112
17451
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
17113
17452
  }
17114
17453
  function createDeviceAuthRouter() {
17115
- const app = new Hono30();
17454
+ const app = new Hono31();
17116
17455
  app.post("/code", async (c) => {
17117
17456
  const body = await readBody(c);
17118
17457
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -17276,7 +17615,7 @@ function createDeviceAuthRouter() {
17276
17615
 
17277
17616
  // src/dashboard/api.ts
17278
17617
  function createApiRouter(agent = {}, mode = "all") {
17279
- const api = new Hono31();
17618
+ const api = new Hono32();
17280
17619
  api.use("*", async (c, next) => {
17281
17620
  const known = await listKnownWorkspaces();
17282
17621
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -17332,6 +17671,7 @@ function createApiRouter(agent = {}, mode = "all") {
17332
17671
  });
17333
17672
  registerMirroredSkillRoutes(api, agent);
17334
17673
  registerRelayedOutputStylesRoutes(api, agent);
17674
+ registerRelayedProviderLoginRoutes(api, agent);
17335
17675
  api.get("/channels/:id/docs/flat", requireDocsAccess, async (c) => {
17336
17676
  const { cwd } = resolveChannelConfig(c.req.param("id"));
17337
17677
  const query = c.req.query("q") ?? "";
@@ -17389,7 +17729,7 @@ function createApiRouter(agent = {}, mode = "all") {
17389
17729
  }
17390
17730
 
17391
17731
  // src/dashboard/app-icon-routes.ts
17392
- import { Hono as Hono32 } from "hono";
17732
+ import { Hono as Hono33 } from "hono";
17393
17733
  var NEUTRAL_MANIFEST = {
17394
17734
  id: "/",
17395
17735
  name: "Flyspace",
@@ -17427,7 +17767,7 @@ async function withResolvedProfile2(c, next) {
17427
17767
  return profile ? runWithProfile(profile, () => next()) : next();
17428
17768
  }
17429
17769
  function createAppIconRouter() {
17430
- const app = new Hono32();
17770
+ const app = new Hono33();
17431
17771
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
17432
17772
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
17433
17773
  const profile = getProfile();
@@ -17467,7 +17807,7 @@ function pngResponse(c, png) {
17467
17807
  }
17468
17808
 
17469
17809
  // src/dashboard/stripe-routes.ts
17470
- import { Hono as Hono33 } from "hono";
17810
+ import { Hono as Hono34 } from "hono";
17471
17811
  function planStatusFor(status) {
17472
17812
  const known = ["trialing", "active", "paused", "canceled"];
17473
17813
  return known.includes(status) ? status : null;
@@ -17510,7 +17850,7 @@ async function handleCardAdded(session) {
17510
17850
  }
17511
17851
  }
17512
17852
  function createStripeWebhookRouter() {
17513
- const stripeRouter = new Hono33();
17853
+ const stripeRouter = new Hono34();
17514
17854
  stripeRouter.post("/webhook", async (c) => {
17515
17855
  const secret = process.env["STRIPE_WEBHOOK_SECRET"];
17516
17856
  const signature = c.req.header("stripe-signature");
@@ -17621,7 +17961,7 @@ function startHostStatusBroadcast() {
17621
17961
 
17622
17962
  // src/dashboard/server.ts
17623
17963
  var DEFAULT_DASHBOARD_PORT = 4747;
17624
- var WEB_DIST = join24(paths.PACKAGE_ROOT, "web", "dist");
17964
+ var WEB_DIST = join25(paths.PACKAGE_ROOT, "web", "dist");
17625
17965
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
17626
17966
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
17627
17967
  for (let port = start; port < start + attempts; port++) {
@@ -17670,7 +18010,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17670
18010
  const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
17671
18011
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
17672
18012
  };
17673
- const app = new Hono34();
18013
+ const app = new Hono35();
17674
18014
  app.onError(reportingErrorHandler);
17675
18015
  app.use("*", async (c, next) => {
17676
18016
  await next();
@@ -17710,14 +18050,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17710
18050
  } else {
17711
18051
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
17712
18052
  app.route("/", createAppIconRouter());
17713
- if (existsSync25(WEB_DIST) || existsSync25(join24(paths.PACKAGE_ROOT, "web", "package.json"))) {
18053
+ if (existsSync26(WEB_DIST) || existsSync26(join25(paths.PACKAGE_ROOT, "web", "package.json"))) {
17714
18054
  app.use("/*", serveStatic({ root: WEB_DIST }));
17715
18055
  }
17716
18056
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
17717
- const spaFallback = serveStatic({ path: join24(WEB_DIST, "index.html") });
18057
+ const spaFallback = serveStatic({ path: join25(WEB_DIST, "index.html") });
17718
18058
  app.get("*", async (c, next) => {
17719
- if (!existsSync25(join24(WEB_DIST, "index.html"))) {
17720
- const isCheckout = existsSync25(join24(paths.PACKAGE_ROOT, "web", "package.json"));
18059
+ if (!existsSync26(join25(WEB_DIST, "index.html"))) {
18060
+ const isCheckout = existsSync26(join25(paths.PACKAGE_ROOT, "web", "package.json"));
17721
18061
  return c.text(
17722
18062
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
17723
18063
  Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
@@ -18155,7 +18495,7 @@ async function threadReplies(client2, nameOrId, ts2) {
18155
18495
  async function postMessage(client2, args) {
18156
18496
  const channelArg = getArg2(args, "--channel");
18157
18497
  const textFilePath = getArg2(args, "--text-file");
18158
- const text = textFilePath ? readFileSync19(textFilePath, "utf8") : getArg2(args, "--text");
18498
+ const text = textFilePath ? readFileSync20(textFilePath, "utf8") : getArg2(args, "--text");
18159
18499
  const threadTs = getArg2(args, "--thread");
18160
18500
  const markdown = args.includes("--markdown");
18161
18501
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -18253,7 +18593,7 @@ function defaultClaudeConfigDir() {
18253
18593
  return process.env.CLAUDE_CONFIG_DIR || resolve6(homedir9(), ".claude");
18254
18594
  }
18255
18595
  function hasClaudeCredentials(configDir2) {
18256
- if (existsSync26(resolve6(configDir2, ".credentials.json"))) {
18596
+ if (existsSync27(resolve6(configDir2, ".credentials.json"))) {
18257
18597
  return true;
18258
18598
  }
18259
18599
  if (process.platform === "darwin") {
@@ -18310,7 +18650,7 @@ async function ensureClaudeCodeReady() {
18310
18650
  }
18311
18651
  function getManifestYaml(botName) {
18312
18652
  const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
18313
- const raw = readFileSync19(manifestPath, "utf-8");
18653
+ const raw = readFileSync20(manifestPath, "utf-8");
18314
18654
  return raw.replace(/name: Custie/g, `name: ${botName}`);
18315
18655
  }
18316
18656
  async function stepPersonalise() {
@@ -18617,7 +18957,7 @@ async function stepBrowserTokens(botName) {
18617
18957
  let settled = false;
18618
18958
  let interval;
18619
18959
  let timeout;
18620
- const finish = (action) => {
18960
+ const finish2 = (action) => {
18621
18961
  if (settled) return;
18622
18962
  settled = true;
18623
18963
  clearInterval(interval);
@@ -18626,7 +18966,7 @@ async function stepBrowserTokens(botName) {
18626
18966
  };
18627
18967
  const checkUrl = (p) => {
18628
18968
  if (p.url().includes("install-on-team?success=1")) {
18629
- finish(() => resolve7(p));
18969
+ finish2(() => resolve7(p));
18630
18970
  }
18631
18971
  };
18632
18972
  context.on("page", (newPage) => {
@@ -18637,7 +18977,7 @@ async function stepBrowserTokens(botName) {
18637
18977
  for (const p of context.pages()) checkUrl(p);
18638
18978
  }, 1e3);
18639
18979
  timeout = setTimeout(() => {
18640
- finish(() => reject(new Error("Timed out waiting for the app to be installed.")));
18980
+ finish2(() => reject(new Error("Timed out waiting for the app to be installed.")));
18641
18981
  }, 12e4);
18642
18982
  });
18643
18983
  success4("App installed! Extracting Bot Token...");
@@ -18727,7 +19067,7 @@ function writeConfigFile(config) {
18727
19067
  `OWNER_USER_ID=${config.ownerUserId}`,
18728
19068
  ``
18729
19069
  ].join("\n");
18730
- writeFileSync11(paths.CONFIG_FILE, envContent);
19070
+ writeFileSync12(paths.CONFIG_FILE, envContent);
18731
19071
  success4(`Config written to ${paths.CONFIG_FILE}`);
18732
19072
  }
18733
19073
  function printNextSteps() {
@@ -18739,11 +19079,11 @@ function printNextSteps() {
18739
19079
  console.log(" Run \x1B[1mflyspace service install\x1B[0m\n");
18740
19080
  }
18741
19081
  function copyDefaultPrompt(botName) {
18742
- if (!existsSync26(paths.PROMPT_FILE)) {
19082
+ if (!existsSync27(paths.PROMPT_FILE)) {
18743
19083
  const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
18744
- if (existsSync26(defaultPrompt)) {
18745
- const content = readFileSync19(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18746
- writeFileSync11(paths.PROMPT_FILE, content);
19084
+ if (existsSync27(defaultPrompt)) {
19085
+ const content = readFileSync20(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
19086
+ writeFileSync12(paths.PROMPT_FILE, content);
18747
19087
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
18748
19088
  }
18749
19089
  }
@@ -18755,7 +19095,7 @@ async function runSlackSetup(args) {
18755
19095
  const prereq = await ensureClaudeCodeReady();
18756
19096
  if (!prereq) return;
18757
19097
  const { claudeConfigDir } = prereq;
18758
- if (existsSync26(paths.CONFIG_FILE)) {
19098
+ if (existsSync27(paths.CONFIG_FILE)) {
18759
19099
  const reconfigure = await confirm4({
18760
19100
  message: `Config already exists at ${paths.CONFIG_FILE}. Reconfigure?`,
18761
19101
  default: false
@@ -19066,21 +19406,21 @@ async function runChatCmd(args) {
19066
19406
  }
19067
19407
 
19068
19408
  // src/commands/memory.ts
19069
- import { readFileSync as readFileSync21 } from "fs";
19409
+ import { readFileSync as readFileSync22 } from "fs";
19070
19410
 
19071
19411
  // src/store/claude-memory-import.ts
19072
- import { existsSync as existsSync27, readdirSync as readdirSync5, readFileSync as readFileSync20 } from "fs";
19412
+ import { existsSync as existsSync28, readdirSync as readdirSync5, readFileSync as readFileSync21 } from "fs";
19073
19413
  import { homedir as homedir10 } from "os";
19074
- import { basename as basename8, join as join25 } from "path";
19414
+ import { basename as basename8, join as join26 } from "path";
19075
19415
  import dotenv2 from "dotenv";
19076
19416
  function claudeMemoryDir(cwd, configDir2) {
19077
- return join25(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
19417
+ return join26(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
19078
19418
  }
19079
19419
  function profileClaudeConfigDirFor(profile) {
19080
- const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join25(paths.PROFILES_DIR, profile);
19081
- const file = join25(dir, "config.env");
19082
- const env = existsSync27(file) ? dotenv2.parse(readFileSync20(file)) : {};
19083
- return env["CLAUDE_CONFIG_DIR"]?.trim() || join25(homedir10(), ".claude");
19420
+ const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join26(paths.PROFILES_DIR, profile);
19421
+ const file = join26(dir, "config.env");
19422
+ const env = existsSync28(file) ? dotenv2.parse(readFileSync21(file)) : {};
19423
+ return env["CLAUDE_CONFIG_DIR"]?.trim() || join26(homedir10(), ".claude");
19084
19424
  }
19085
19425
  var TYPE_MAP = {
19086
19426
  user: "preference",
@@ -19125,11 +19465,11 @@ function parseClaudeMemory(fileName, text, channelId) {
19125
19465
  return input2;
19126
19466
  }
19127
19467
  async function importClaudeMemories(dir, channelId, opts = {}) {
19128
- if (!existsSync27(dir)) throw new Error(`No Claude memory directory at ${dir}`);
19468
+ if (!existsSync28(dir)) throw new Error(`No Claude memory directory at ${dir}`);
19129
19469
  const files = readdirSync5(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
19130
19470
  const results = [];
19131
19471
  for (const file of files) {
19132
- const parsed = parseClaudeMemory(file, readFileSync20(join25(dir, file), "utf-8"), channelId);
19472
+ const parsed = parseClaudeMemory(file, readFileSync21(join26(dir, file), "utf-8"), channelId);
19133
19473
  if ("skip" in parsed) {
19134
19474
  results.push({ file, outcome: "skipped", reason: parsed.skip });
19135
19475
  continue;
@@ -19164,6 +19504,12 @@ var USAGE5 = `
19164
19504
  Defaults to the channel profile's CLAUDE_CONFIG_DIR
19165
19505
  (or ~/.claude)/projects/<cwd slug>/memory
19166
19506
 
19507
+ Scope (list, get, search, save, delete; default: the channel's memory):
19508
+ --scope user --user <user-id> That member's personal memory, in the channel's
19509
+ workspace. Only ever the current sender's.
19510
+ --scope workspace The workspace's memory. Read-only here: only a
19511
+ Workspace Admin changes it, from Settings.
19512
+
19167
19513
  Types: ${MEMORY_TYPES.join(", ")}
19168
19514
  `;
19169
19515
  function getArg4(args, flag) {
@@ -19194,17 +19540,23 @@ async function runMemoryCmd(args) {
19194
19540
  if (!channelArg) throw new Error("--channel <name-or-id> is required");
19195
19541
  const channel = await resolveChannel(channelArg, getArg4(rest, "--profile-name"));
19196
19542
  const words = positionals(rest);
19543
+ const target = await resolveTarget(rest, channel);
19544
+ const where = target.scope === "channel" ? channel.name : `${target.scope} memory`;
19545
+ if (target.scope === "workspace" && (subcommand === "save" || subcommand === "delete")) {
19546
+ throw new Error("Workspace memory is read-only to the agent; a Workspace Admin manages it.");
19547
+ }
19197
19548
  switch (subcommand) {
19198
19549
  case "list": {
19199
- const memories = await listChannelMemories(channel.id);
19550
+ const memories = await listMemories(target);
19200
19551
  console.log(memories.length ? memories.map(indexLine).join("\n") : "No memories yet.");
19201
19552
  break;
19202
19553
  }
19203
19554
  case "get": {
19204
19555
  const name = words[0];
19205
19556
  if (!name) throw new Error("Usage: flyspace memory get --channel <c> <name>");
19206
- const memory = await getChannelMemory(channel.id, name);
19207
- if (!memory) throw new Error(`No memory named ${name} in ${channel.name}.`);
19557
+ const memory = await getMemory(target, name);
19558
+ if (!memory) throw new Error(`No memory named ${name} in ${where}.`);
19559
+ await markMemoryUsed(memory);
19208
19560
  console.log(`# ${memory.name} (${memory.type})
19209
19561
 
19210
19562
  ${memory.description}
@@ -19214,7 +19566,7 @@ ${memory.body}`);
19214
19566
  }
19215
19567
  case "search": {
19216
19568
  if (!words.length) throw new Error("Usage: flyspace memory search --channel <c> <words...>");
19217
- const hits = await searchChannelMemories(channel.id, words.join(" "));
19569
+ const hits = await searchMemories(target, words.join(" "));
19218
19570
  console.log(hits.length ? hits.map(indexLine).join("\n") : "No matching memories.");
19219
19571
  break;
19220
19572
  }
@@ -19229,11 +19581,11 @@ ${memory.body}`);
19229
19581
  );
19230
19582
  }
19231
19583
  if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
19232
- const body = bodyArg === "-" ? readFileSync21(0, "utf-8") : bodyArg;
19584
+ const body = bodyArg === "-" ? readFileSync22(0, "utf-8") : bodyArg;
19233
19585
  const thread = getArg4(rest, "--thread");
19234
- const { created, changed } = await saveChannelMemory(
19586
+ const { created, changed } = await saveMemory(
19587
+ target,
19235
19588
  {
19236
- channelId: channel.id,
19237
19589
  name,
19238
19590
  type,
19239
19591
  description,
@@ -19244,18 +19596,19 @@ ${memory.body}`);
19244
19596
  "agent"
19245
19597
  );
19246
19598
  const verb = created ? "Saved" : changed ? "Updated" : "Unchanged:";
19247
- console.log(`${verb} memory ${name} in ${channel.name}.`);
19599
+ console.log(`${verb} memory ${name} in ${where}.`);
19248
19600
  break;
19249
19601
  }
19250
19602
  case "delete": {
19251
19603
  const ref = words[0];
19252
19604
  if (!ref) throw new Error("Usage: flyspace memory delete --channel <c> <name-or-id>");
19253
- const deleted = await deleteChannelMemory(channel.id, ref, "agent", getArg4(rest, "--thread"));
19254
- if (!deleted) throw new Error(`No memory ${ref} in ${channel.name}.`);
19605
+ const deleted = await deleteMemory(target, ref, "agent", getArg4(rest, "--thread"));
19606
+ if (!deleted) throw new Error(`No memory ${ref} in ${where}.`);
19255
19607
  console.log(`Deleted memory ${ref}.`);
19256
19608
  break;
19257
19609
  }
19258
19610
  case "import": {
19611
+ if (target.scope !== "channel") throw new Error("import only fills a channel memory");
19259
19612
  const dir = getArg4(rest, "--dir") ?? claudeMemoryDir(channel.cwd, profileClaudeConfigDirFor(channel.profile));
19260
19613
  const dryRun = rest.includes("--dry-run");
19261
19614
  const results = await importClaudeMemories(dir, channel.id, { dryRun });
@@ -19275,6 +19628,19 @@ ${memory.body}`);
19275
19628
  process.exit(1);
19276
19629
  }
19277
19630
  }
19631
+ async function resolveTarget(args, channel) {
19632
+ const scope = getArg4(args, "--scope") ?? "channel";
19633
+ if (scope === "channel") return { scope: "channel", channelId: channel.id };
19634
+ if (scope !== "user" && scope !== "workspace") {
19635
+ throw new Error("--scope must be channel, user or workspace");
19636
+ }
19637
+ const workspace = await getWorkspaceBySlug(channel.profile);
19638
+ if (!workspace) throw new Error(`No workspace for ${channel.profile}.`);
19639
+ if (scope === "workspace") return { scope, workspaceId: workspace.id };
19640
+ const userId = getArg4(args, "--user");
19641
+ if (!userId) throw new Error("--scope user needs --user <user-id>");
19642
+ return { scope, workspaceId: workspace.id, userId };
19643
+ }
19278
19644
 
19279
19645
  // src/commands/mcp.ts
19280
19646
  var USAGE6 = `
@@ -19313,10 +19679,10 @@ async function runMcpCmd(args) {
19313
19679
  }
19314
19680
 
19315
19681
  // src/commands/dashboard.ts
19316
- import { spawn as spawn6, spawnSync } from "child_process";
19317
- import { existsSync as existsSync28 } from "fs";
19682
+ import { spawn as spawn7, spawnSync } from "child_process";
19683
+ import { existsSync as existsSync29 } from "fs";
19318
19684
  import { platform as platform7 } from "os";
19319
- import { join as join26 } from "path";
19685
+ import { join as join27 } from "path";
19320
19686
  function flagValue(args, name) {
19321
19687
  const eq = args.find((a) => a.startsWith(`--${name}=`));
19322
19688
  if (eq) return eq.slice(`--${name}=`.length);
@@ -19326,13 +19692,13 @@ function flagValue(args, name) {
19326
19692
  function openBrowser2(url) {
19327
19693
  const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
19328
19694
  try {
19329
- spawn6(cmd, [url], { stdio: "ignore", detached: true }).unref();
19695
+ spawn7(cmd, [url], { stdio: "ignore", detached: true }).unref();
19330
19696
  } catch {
19331
19697
  }
19332
19698
  }
19333
19699
  function ensureWebBuild() {
19334
- if (existsSync28(join26(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19335
- if (!existsSync28(join26(paths.PACKAGE_ROOT, "web", "package.json"))) return;
19700
+ if (existsSync29(join27(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19701
+ if (!existsSync29(join27(paths.PACKAGE_ROOT, "web", "package.json"))) return;
19336
19702
  console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
19337
19703
  const result = spawnSync("pnpm", ["--dir", "web", "build"], {
19338
19704
  cwd: paths.PACKAGE_ROOT,
@@ -19382,7 +19748,7 @@ async function runDashboard(args) {
19382
19748
  }
19383
19749
 
19384
19750
  // src/commands/workspace.ts
19385
- import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
19751
+ import { existsSync as existsSync30, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
19386
19752
  var USAGE7 = `
19387
19753
  Usage: flyspace workspace <subcommand> [options]
19388
19754
 
@@ -19468,10 +19834,10 @@ function provisionLocal(botName) {
19468
19834
  ensureDirs();
19469
19835
  console.log(` config dir ready (${paths.CONFIG_DIR})`);
19470
19836
  console.log(` data dir ready (${paths.DATA_DIR})`);
19471
- if (existsSync29(paths.CONFIG_FILE)) {
19837
+ if (existsSync30(paths.CONFIG_FILE)) {
19472
19838
  console.log(" config.env exists");
19473
19839
  } else {
19474
- writeFileSync12(paths.CONFIG_FILE, `BOT_NAME=${botName}
19840
+ writeFileSync13(paths.CONFIG_FILE, `BOT_NAME=${botName}
19475
19841
  `);
19476
19842
  console.log(` config.env written (${paths.CONFIG_FILE})`);
19477
19843
  }
@@ -19485,8 +19851,8 @@ async function provisionChannels(specs) {
19485
19851
  continue;
19486
19852
  }
19487
19853
  const cwd = expandPath(spec.cwd);
19488
- if (!existsSync29(cwd)) {
19489
- mkdirSync11(cwd, { recursive: true });
19854
+ if (!existsSync30(cwd)) {
19855
+ mkdirSync13(cwd, { recursive: true });
19490
19856
  console.log(` created cwd ${cwd}`);
19491
19857
  }
19492
19858
  await createChannel({ name: spec.name, cwd, position: index });
@@ -19695,7 +20061,7 @@ async function main() {
19695
20061
  break;
19696
20062
  case "-v":
19697
20063
  case "--version":
19698
- printRaw("0.1.15");
20064
+ printRaw("0.1.16");
19699
20065
  break;
19700
20066
  case "-h":
19701
20067
  case "--help":