flyspace-cli 0.1.14 → 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,
@@ -13,6 +14,7 @@ import {
13
14
  HandleTakenError,
14
15
  InvalidTransferError,
15
16
  LastAdminError,
17
+ MEMORY_TYPES,
16
18
  MessageQueue,
17
19
  OutputStyleConflictError,
18
20
  SKIP_SUBTYPES,
@@ -33,18 +35,23 @@ import {
33
35
  canSeeChannel,
34
36
  channelSkillsDir,
35
37
  claudeAuthFailed,
38
+ clearClaudeAuthFailed,
36
39
  codexAuthAvailable,
37
40
  createChannel,
38
41
  createMembership,
39
42
  createOutputStyle,
40
43
  createSkillFile,
41
44
  createWorkspace,
45
+ deleteChannelMemory,
42
46
  deleteMembership,
47
+ deleteMemory,
43
48
  deleteOutputStyle,
44
49
  deleteSkillFile,
45
50
  deleteWorkspace,
51
+ disableFeatureFlag,
46
52
  displayNameFor,
47
53
  docsAccessPromptNote,
54
+ enableFeatureFlag,
48
55
  ensureAgentDmChannel,
49
56
  ensureGeneralChannel,
50
57
  ensureLogicalCwdDir,
@@ -52,15 +59,19 @@ import {
52
59
  ensureUsersCached,
53
60
  expandPath,
54
61
  extractCategories,
62
+ featureFlagIds,
63
+ featureFlags,
55
64
  findChannel,
56
65
  findChannelByAgentName,
57
66
  findChannelById,
58
67
  findSkillDetail,
59
68
  getAgentPaused,
60
69
  getAllowGuestAccess,
70
+ getChannelMemory,
61
71
  getChannelRegistry,
62
72
  getMembershipByEmail,
63
73
  getMembershipByUserId,
74
+ getMemory,
64
75
  getOutputStyleDefault,
65
76
  getProfileChannelRegistry,
66
77
  getWorkspaceBilling,
@@ -75,21 +86,31 @@ import {
75
86
  isCodexModel,
76
87
  isCwdUsable,
77
88
  isEffort,
89
+ isFeatureFlagId,
90
+ isFeatureVisible,
78
91
  isLockedPlan,
79
92
  isLogicalCwd,
93
+ isMemoryType,
94
+ isUuid,
80
95
  isValidSkillName,
81
96
  lastRunsForWorkspace,
82
97
  listActiveUsers,
98
+ listChannelMemories,
99
+ listEnabledFeatureFlags,
83
100
  listMemberChannels,
84
101
  listMemberships,
85
102
  listMembershipsByEmail,
103
+ listMemories,
104
+ listMemoryEvents,
86
105
  listOutputStyles,
87
106
  listPluginSkillDetails,
88
107
  listSkillFiles,
89
108
  listSkills,
109
+ listThreadMemoryEvents,
90
110
  listUsersWithAvatars,
91
111
  listWorkspaceSummaries,
92
112
  listWorkspaces,
113
+ loadAgentMemory,
93
114
  loadAutomations,
94
115
  loadAutomationsFile,
95
116
  loadBotIdentity,
@@ -103,6 +124,7 @@ import {
103
124
  localDefaults,
104
125
  lookupById,
105
126
  markInviteResent,
127
+ markMemoryUsed,
106
128
  markWorkspaceAgentConnected,
107
129
  markWorkspaceGreeted,
108
130
  markWorkspaceProjectsSetup,
@@ -111,6 +133,7 @@ import {
111
133
  mimetypeSizeLimit,
112
134
  modelLabel,
113
135
  pinnedClaudeConfigDir,
136
+ promoteChannelMemory,
114
137
  readSkillFile,
115
138
  recentRunsForWorkspace,
116
139
  refreshAutomationRegistry,
@@ -122,6 +145,7 @@ import {
122
145
  resolveChannelId,
123
146
  resolveChannelPrompt,
124
147
  resolveCwd,
148
+ resolveFeatureFlags,
125
149
  resolveLogicalCwd,
126
150
  resolveSelectedOutputStyle,
127
151
  resolveUserId,
@@ -130,12 +154,16 @@ import {
130
154
  runnerForModel,
131
155
  saveAgentProfile,
132
156
  saveBotIdentity,
157
+ saveChannelMemory,
133
158
  saveDashboardSettings,
159
+ saveMemory,
134
160
  saveWorkspaceBranding,
135
161
  saveWorkspacePrompt,
162
+ searchMemories,
136
163
  setAgentPaused,
137
164
  setAllowGuestAccess,
138
165
  setMembershipHandle,
166
+ setMemoryAutoPaused,
139
167
  setOutputStyleDefault,
140
168
  setWorkspaceStripeSubscription,
141
169
  slackConfigured,
@@ -145,17 +173,21 @@ import {
145
173
  syncSlackAvatars,
146
174
  transferWorkspaceAdmin,
147
175
  tsToIso,
176
+ undoMemoryEvent,
148
177
  updateChannelById,
178
+ updateChannelMemory,
149
179
  updateChannelsMcpConnections,
150
180
  updateMembershipRole,
181
+ updateMemory,
151
182
  updateOutputStyle,
152
183
  updateWorkspacePlanStatus,
153
184
  usageLimitedModels,
154
185
  usageLimitedUntil,
186
+ validateMemoryInput,
155
187
  warnOnBrokenChannelCwds,
156
188
  workspaceSkillsDir,
157
189
  writeSkillFile
158
- } from "./chunk-DA7SYYKU.js";
190
+ } from "./chunk-2OGLCT7I.js";
159
191
  import {
160
192
  firstReplyBody,
161
193
  getThread,
@@ -614,8 +646,8 @@ async function requestHostDb(req) {
614
646
  }
615
647
  var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
616
648
  function dbFetch(send2) {
617
- return async (input2, init) => {
618
- const req = new Request(input2, init);
649
+ return async (input2, init2) => {
650
+ const req = new Request(input2, init2);
619
651
  const url = new URL(req.url);
620
652
  const method = req.method.toUpperCase();
621
653
  const res = await send2({
@@ -678,11 +710,17 @@ function createHostDbClient(via = "socket") {
678
710
  });
679
711
  }
680
712
 
713
+ // src/commands/supabase-via-host.ts
714
+ function useHostDbWithoutCredentials() {
715
+ const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
716
+ if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
717
+ return viaHost;
718
+ }
719
+
681
720
  // src/commands/start.ts
682
721
  async function runStart() {
683
722
  loadEnvFiles();
684
- const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
685
- if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
723
+ const viaHost = useHostDbWithoutCredentials();
686
724
  await preflight({ supabaseViaHost: viaHost });
687
725
  await startServer();
688
726
  }
@@ -1321,14 +1359,14 @@ function detectRunners(history = { claude: false, codex: false }) {
1321
1359
  function scanOnboarding() {
1322
1360
  const byPath = /* @__PURE__ */ new Map();
1323
1361
  const history = { claude: false, codex: false };
1324
- const add = (rawPath, source, mtime, sessions, trusted = false) => {
1362
+ const add = (rawPath, source, mtime, sessions2, trusted = false) => {
1325
1363
  const path = resolve3(rawPath);
1326
1364
  if (isExcluded(path)) return;
1327
1365
  const existing = byPath.get(path);
1328
1366
  if (existing) {
1329
1367
  if (!existing.sources.includes(source)) existing.sources.push(source);
1330
1368
  existing.lastUsed = Math.max(existing.lastUsed, mtime);
1331
- existing.sessions += sessions;
1369
+ existing.sessions += sessions2;
1332
1370
  existing.trusted ||= trusted;
1333
1371
  } else {
1334
1372
  byPath.set(path, {
@@ -1336,7 +1374,7 @@ function scanOnboarding() {
1336
1374
  name: basename2(path),
1337
1375
  sources: [source],
1338
1376
  lastUsed: mtime,
1339
- sessions,
1377
+ sessions: sessions2,
1340
1378
  trusted
1341
1379
  });
1342
1380
  }
@@ -1738,7 +1776,7 @@ async function runAuthCmd(args) {
1738
1776
  }
1739
1777
 
1740
1778
  // src/version.ts
1741
- var CLI_VERSION = true ? "0.1.14" : null;
1779
+ var CLI_VERSION = true ? "0.1.16" : null;
1742
1780
  function isOlderVersion(a, b) {
1743
1781
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1744
1782
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -1893,9 +1931,9 @@ import { tmpdir } from "os";
1893
1931
  import { promisify } from "util";
1894
1932
  var execAsync = promisify(execFile);
1895
1933
  var exec = (cmd, args, opts) => {
1896
- const run3 = execAsync(cmd, args, opts);
1897
- run3.child.stdin?.end();
1898
- return run3;
1934
+ const run4 = execAsync(cmd, args, opts);
1935
+ run4.child.stdin?.end();
1936
+ return run4;
1899
1937
  };
1900
1938
  var BOLD3 = "\x1B[1m";
1901
1939
  var DIM3 = "\x1B[2m";
@@ -2016,7 +2054,7 @@ ${BOLD3}Flyspace Doctor${RESET3}
2016
2054
  }
2017
2055
 
2018
2056
  // src/commands/uninstall.ts
2019
- 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";
2020
2058
  import { confirm as confirm3 } from "@inquirer/prompts";
2021
2059
  import { execSync as execSync5 } from "child_process";
2022
2060
  import { basename as basename3, join as join9 } from "path";
@@ -2083,12 +2121,31 @@ async function runUninstall(args = []) {
2083
2121
  console.log(" - revokes this device and removes every background service");
2084
2122
  for (const t of targets) console.log(` - deletes ${t}`);
2085
2123
  console.log(" Your channel folders (~/Flyspace) are kept.\n");
2086
- 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 });
2087
2129
  if (!await confirm3({ message: "Continue?", default: false })) {
2088
2130
  console.log(" Cancelled.");
2089
2131
  return;
2090
2132
  }
2091
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
+ }
2092
2149
  const creds = loadCredentials();
2093
2150
  if (creds) {
2094
2151
  try {
@@ -2115,7 +2172,11 @@ async function runUninstall(args = []) {
2115
2172
  success2(`Removed PATH entry from ${rc}`);
2116
2173
  }
2117
2174
  }
2118
- 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("");
2119
2180
  }
2120
2181
 
2121
2182
  // src/commands/profiles-cmd.ts
@@ -2230,7 +2291,7 @@ function npmEnv() {
2230
2291
  return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2231
2292
  }
2232
2293
  async function runUpgrade() {
2233
- const current = "0.1.14";
2294
+ const current = "0.1.16";
2234
2295
  console.log(`Current version: ${current}`);
2235
2296
  console.log("Checking for updates...\n");
2236
2297
  try {
@@ -2550,6 +2611,7 @@ async function handleImport(manager) {
2550
2611
  async function runAutomationCmd(args) {
2551
2612
  const subcommand = args[0];
2552
2613
  loadEnvFiles();
2614
+ useHostDbWithoutCredentials();
2553
2615
  await refreshChannelRegistry();
2554
2616
  await refreshAutomationRegistry();
2555
2617
  const manager = new AutomationManager();
@@ -2870,9 +2932,9 @@ async function runLogs(args) {
2870
2932
 
2871
2933
  // src/commands/slack.ts
2872
2934
  import { execFileSync as execFileSync4 } from "child_process";
2873
- import { existsSync as existsSync26, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
2935
+ import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
2874
2936
  import { resolve as resolve6 } from "path";
2875
- import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
2937
+ import { tmpdir as tmpdir5, homedir as homedir9 } from "os";
2876
2938
  import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
2877
2939
  import { WebClient as WebClient3 } from "@slack/web-api";
2878
2940
 
@@ -3427,6 +3489,13 @@ function profileFromHost(host, known) {
3427
3489
  const bare = label2.startsWith("api-") ? label2.slice(4) : label2.startsWith("dev-") ? label2.slice(4) : label2;
3428
3490
  return known.includes(bare) ? bare : null;
3429
3491
  }
3492
+ function isUnknownWorkspaceHost(host, known) {
3493
+ const name = bareHost(host);
3494
+ if (!name?.endsWith(".flyspace.ai")) return false;
3495
+ const label2 = subdomainLabel(host);
3496
+ if (!label2 || label2 === "dev-admin") return false;
3497
+ return profileFromHost(host, known) === null;
3498
+ }
3430
3499
  function workspaceHostname(host, profile) {
3431
3500
  const name = bareHost(host);
3432
3501
  if (!name || !subdomainLabel(host)) return null;
@@ -3837,7 +3906,9 @@ function getStripeClient() {
3837
3906
 
3838
3907
  // src/store/stripe-trial.ts
3839
3908
  var PLAN_INFO = {
3840
- local: { name: "Local Agent", priceUsd: 9 },
3909
+ // Single-member: a personal Claude subscription can't be shared (see
3910
+ // members-routes.ts, which blocks invites on this plan).
3911
+ local: { name: "Solo", priceUsd: 29 },
3841
3912
  cloud: { name: "Cloud Agent", priceUsd: 49 }
3842
3913
  };
3843
3914
  function priceIdForPlan(plan) {
@@ -3862,6 +3933,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
3862
3933
  metadata: { workspace_id: workspace.id, plan }
3863
3934
  });
3864
3935
  await setWorkspaceStripeSubscription(workspace.id, {
3936
+ plan,
3865
3937
  stripeCustomerId: customer.id,
3866
3938
  stripeSubscriptionId: subscription.id,
3867
3939
  planStatus: "trialing"
@@ -4398,15 +4470,27 @@ async function deliverPasswordResetLink(email, redirectTo) {
4398
4470
  }
4399
4471
 
4400
4472
  // src/dashboard/server.ts
4401
- import { existsSync as existsSync25 } from "fs";
4473
+ import { existsSync as existsSync26 } from "fs";
4402
4474
  import { createServer } from "net";
4403
- import { join as join23 } from "path";
4404
- import { Hono as Hono34 } from "hono";
4475
+ import { join as join25 } from "path";
4476
+ import { Hono as Hono35 } from "hono";
4477
+
4478
+ // src/dashboard/sentry.ts
4479
+ import * as Sentry from "@sentry/node";
4480
+ import { HTTPException } from "hono/http-exception";
4481
+ var reportingErrorHandler = (err, c) => {
4482
+ if (err instanceof HTTPException) return err.getResponse();
4483
+ Sentry.captureException(err, { extra: { method: c.req.method, path: c.req.path } });
4484
+ console.error(err);
4485
+ return c.text("Internal Server Error", 500);
4486
+ };
4487
+
4488
+ // src/dashboard/server.ts
4405
4489
  import { serve } from "@hono/node-server";
4406
4490
  import { serveStatic } from "@hono/node-server/serve-static";
4407
4491
 
4408
4492
  // src/reconcile.ts
4409
- 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";
4410
4494
  async function boundWorkspaces2() {
4411
4495
  return (await boundWorkspaces())?.workspaces ?? null;
4412
4496
  }
@@ -4432,7 +4516,7 @@ async function reconcileProfile(profile) {
4432
4516
  if (!channel.cwd) continue;
4433
4517
  const cwd = expandPath(channel.cwd);
4434
4518
  if (isCwdUsable(cwd)) continue;
4435
- mkdirSync7(cwd, { recursive: true });
4519
+ mkdirSync8(cwd, { recursive: true });
4436
4520
  log5.push(`channel "${channel.name}" cwd created (${cwd})`);
4437
4521
  }
4438
4522
  return log5;
@@ -4506,45 +4590,18 @@ var RANK = { none: 0, read: 1, write: 2 };
4506
4590
  function docsAccessAtLeast(have, need) {
4507
4591
  return RANK[have] >= RANK[need];
4508
4592
  }
4509
- function isDocsGrant(value) {
4510
- return value === "read" || value === "write";
4511
- }
4512
- function rowToGrant(row) {
4513
- return {
4514
- id: row["id"],
4515
- channelId: row["channel_id"],
4516
- membershipId: row["membership_id"],
4517
- access: row["access"],
4518
- grantedAt: row["granted_at"]
4519
- };
4520
- }
4521
- async function listChannelDocsAccess(channelId) {
4522
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").select("*").eq("channel_id", channelId).order("granted_at", { ascending: true });
4523
- if (error2) throw new Error(`listChannelDocsAccess(${channelId}): ${error2.message}`);
4524
- return (data ?? []).map(rowToGrant);
4593
+ function isDocsAccess(value) {
4594
+ return value === "none" || value === "read" || value === "write";
4525
4595
  }
4526
- async function getChannelDocsGrant(channelId, membershipId) {
4527
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").select("*").eq("channel_id", channelId).eq("membership_id", membershipId).maybeSingle();
4528
- if (error2) throw new Error(`getChannelDocsGrant(${channelId}, ${membershipId}): ${error2.message}`);
4529
- return data ? rowToGrant(data) : null;
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";
4530
4601
  }
4531
- async function upsertChannelDocsAccess(params) {
4532
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").upsert(
4533
- { channel_id: params.channelId, membership_id: params.membershipId, access: params.access },
4534
- { onConflict: "channel_id,membership_id" }
4535
- ).select("*").single();
4536
- if (error2) {
4537
- throw new Error(
4538
- `upsertChannelDocsAccess(${params.channelId}, ${params.membershipId}): ${error2.message}`
4539
- );
4540
- }
4541
- return rowToGrant(data);
4542
- }
4543
- async function removeChannelDocsAccess(channelId, membershipId) {
4544
- const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").delete().eq("channel_id", channelId).eq("membership_id", membershipId).select("id");
4545
- if (error2) {
4546
- throw new Error(`removeChannelDocsAccess(${channelId}, ${membershipId}): ${error2.message}`);
4547
- }
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}`);
4548
4605
  return data?.length ? true : false;
4549
4606
  }
4550
4607
  async function resolveForMembership(membership, channelId) {
@@ -4553,8 +4610,7 @@ async function resolveForMembership(membership, channelId) {
4553
4610
  if (members.some((m) => m.membershipId === membership.id && m.role === "channel_admin")) {
4554
4611
  return "write";
4555
4612
  }
4556
- const grant = await getChannelDocsGrant(channelId, membership.id);
4557
- return grant?.access ?? "none";
4613
+ return getChannelDocsMemberAccess(channelId);
4558
4614
  }
4559
4615
  async function resolveDocsAccess(ctx, channelId) {
4560
4616
  if (ctx.role === "admin") return "write";
@@ -4576,15 +4632,16 @@ async function filterReadableDocsChannels(ctx, channelIds) {
4576
4632
  if (ctx.role === "admin") return new Set(channelIds);
4577
4633
  const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
4578
4634
  if (!membership) return /* @__PURE__ */ new Set();
4635
+ if (channelIds.length === 0) return /* @__PURE__ */ new Set();
4579
4636
  const db = getSupabaseClient();
4580
- const [grants, roles] = await Promise.all([
4581
- 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"),
4582
4639
  db.from("channel_members").select("channel_id, role").eq("membership_id", membership.id)
4583
4640
  ]);
4584
- if (grants.error) throw new Error(`filterReadableDocsChannels: ${grants.error.message}`);
4641
+ if (open.error) throw new Error(`filterReadableDocsChannels: ${open.error.message}`);
4585
4642
  if (roles.error) throw new Error(`filterReadableDocsChannels: ${roles.error.message}`);
4586
4643
  const allowed = /* @__PURE__ */ new Set();
4587
- for (const r of grants.data ?? []) allowed.add(r.channel_id);
4644
+ for (const r of open.data ?? []) allowed.add(r.id);
4588
4645
  for (const r of roles.data ?? []) {
4589
4646
  if (r.role === "channel_admin") allowed.add(r.channel_id);
4590
4647
  }
@@ -4603,7 +4660,7 @@ var requireDocsAccess = async (c, next) => {
4603
4660
  };
4604
4661
 
4605
4662
  // src/dashboard/api.ts
4606
- import { Hono as Hono31 } from "hono";
4663
+ import { Hono as Hono32 } from "hono";
4607
4664
 
4608
4665
  // src/dashboard/public-share-routes.ts
4609
4666
  import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
@@ -4747,15 +4804,15 @@ async function createDocShare(input2) {
4747
4804
  if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
4748
4805
  return rowToShare(data);
4749
4806
  }
4750
- async function updateDocShareSnapshot(id, snapshot3, features) {
4807
+ async function updateDocShareSnapshot(id, snapshot2, features) {
4751
4808
  const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
4752
- title: snapshot3.title,
4753
- content: snapshot3.content,
4754
- asset_keys: snapshot3.assetKeys,
4809
+ title: snapshot2.title,
4810
+ content: snapshot2.content,
4811
+ asset_keys: snapshot2.assetKeys,
4755
4812
  // Re-detected with the snapshot: a re-publish can carry a rewritten
4756
4813
  // article, and one written in another language than last time is exactly
4757
4814
  // the case a stale value would get wrong.
4758
- source_lang: snapshot3.sourceLang,
4815
+ source_lang: snapshot2.sourceLang,
4759
4816
  audio_enabled: features.audioEnabled,
4760
4817
  ask_enabled: features.askEnabled,
4761
4818
  social_enabled: features.socialEnabled,
@@ -5683,9 +5740,9 @@ async function synthesizeWithRetry(script, voice) {
5683
5740
  }
5684
5741
  var renderQueue = Promise.resolve();
5685
5742
  function queued(job) {
5686
- const run3 = renderQueue.then(job, job);
5687
- renderQueue = run3.catch(() => void 0);
5688
- return run3;
5743
+ const run4 = renderQueue.then(job, job);
5744
+ renderQueue = run4.catch(() => void 0);
5745
+ return run4;
5689
5746
  }
5690
5747
  function renderArticleAudio(title, content, lang) {
5691
5748
  return queued(() => renderArticleAudioNow(title, content, lang));
@@ -5759,12 +5816,12 @@ function queueShareAudio(share) {
5759
5816
  }
5760
5817
  const current = inFlight2.get(share.id);
5761
5818
  if (current?.hash === hash) return current.done;
5762
- const run3 = () => runShareAudioJob(share, hash).catch((err) => {
5819
+ const run4 = () => runShareAudioJob(share, hash).catch((err) => {
5763
5820
  console.error(`share audio job crashed (${share.id}):`, err);
5764
5821
  }).finally(() => {
5765
5822
  if (inFlight2.get(share.id)?.hash === hash) inFlight2.delete(share.id);
5766
5823
  });
5767
- const done = current ? current.done.then(run3) : run3();
5824
+ const done = current ? current.done.then(run4) : run4();
5768
5825
  inFlight2.set(share.id, { hash, done });
5769
5826
  return done;
5770
5827
  }
@@ -6689,8 +6746,8 @@ function routeCookieWrites(all, label2, writes) {
6689
6746
 
6690
6747
  // src/dashboard/supabase-server-client.ts
6691
6748
  var REQUEST_TIMEOUT_MS2 = 5e3;
6692
- function fetchWithTimeout(input2, init) {
6693
- return fetch(input2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
6749
+ function fetchWithTimeout(input2, init2) {
6750
+ return fetch(input2, { ...init2, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
6694
6751
  }
6695
6752
  function createRequestSupabaseClient(c) {
6696
6753
  const url = process.env["SUPABASE_URL"];
@@ -6732,7 +6789,7 @@ function createRequestSupabaseClient(c) {
6732
6789
  }
6733
6790
 
6734
6791
  // src/dashboard/auth-context.ts
6735
- import { HTTPException } from "hono/http-exception";
6792
+ import { HTTPException as HTTPException2 } from "hono/http-exception";
6736
6793
  import { isAuthApiError, isAuthRetryableFetchError } from "@supabase/supabase-js";
6737
6794
  function onboardingPending(ctx) {
6738
6795
  const pending3 = [];
@@ -6789,10 +6846,10 @@ async function resolveAuthContextUncached(c) {
6789
6846
  } = await getUserWithRetry(supabase));
6790
6847
  } catch (err) {
6791
6848
  if (isAuthRetryableFetchError(err)) {
6792
- throw new HTTPException(503, { message: "auth service unreachable" });
6849
+ throw new HTTPException2(503, { message: "auth service unreachable" });
6793
6850
  }
6794
6851
  if (isAuthApiError(err) && err.status === 429) {
6795
- throw new HTTPException(503, { message: "auth service rate limited" });
6852
+ throw new HTTPException2(503, { message: "auth service rate limited" });
6796
6853
  }
6797
6854
  return null;
6798
6855
  }
@@ -7201,91 +7258,16 @@ async function offlineAgentIdentities() {
7201
7258
  }
7202
7259
 
7203
7260
  // src/dashboard/host-api.ts
7204
- import { existsSync as existsSync20, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
7205
- import { join as join21 } from "path";
7261
+ import { existsSync as existsSync21, readFileSync as readFileSync16, statSync as statSync8 } from "fs";
7262
+ import { join as join23 } from "path";
7206
7263
  import sharp3 from "sharp";
7207
7264
 
7208
7265
  // src/dashboard/channels-routes.ts
7209
- import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
7266
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
7210
7267
  import { Hono as Hono9 } from "hono";
7211
7268
 
7212
7269
  // src/dashboard/feature-flags-routes.ts
7213
7270
  import { Hono as Hono8 } from "hono";
7214
-
7215
- // src/feature-flags.ts
7216
- var featureFlags = {
7217
- "ai-cost": {
7218
- title: "AI cost",
7219
- description: "Shows admins what the agent has spent: a Cost section in a channel's AI overview and a per-thread cost in the thread menu. Estimated at API list rates, so on a Claude subscription it will not match your bill.",
7220
- stage: "beta"
7221
- },
7222
- "read-aloud": {
7223
- title: "Read aloud",
7224
- description: "Listen to messages, threads and docs as audio. Still rough in places, and each playback uses text-to-speech.",
7225
- stage: "beta"
7226
- },
7227
- excalidraw: {
7228
- title: "Excalidraw",
7229
- description: "Create and edit whiteboard drawings (.excalidraw files) in the Docs pane. Existing drawings stay viewable with this off.",
7230
- stage: "beta"
7231
- },
7232
- sandbox: {
7233
- title: "Sandbox runtime",
7234
- description: "Run a channel agent in an isolated sandbox without Docker. Channels already on Sandbox keep working with this off.",
7235
- stage: "beta"
7236
- },
7237
- dictation: {
7238
- title: "Dictation",
7239
- description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
7240
- stage: "dev"
7241
- },
7242
- "agent-memory": {
7243
- title: "Agent memory",
7244
- description: "Channel memory the agent reads every turn, saved by the agent and picked up from idle threads, with a Memory tab and an undo chip. While on, Claude's built-in auto memory is switched off so the two can be compared; turning this off keeps every memory.",
7245
- stage: "beta"
7246
- }
7247
- };
7248
- var featureFlagIds = Object.keys(featureFlags);
7249
- function isFeatureFlagId(id) {
7250
- return Object.hasOwn(featureFlags, id);
7251
- }
7252
- function isFeatureVisible(stage, devHost) {
7253
- return stage === "beta" || stage === "dev" && devHost;
7254
- }
7255
- function resolveFeatureFlags(enabledIds, devHost) {
7256
- const enabled = new Set(enabledIds);
7257
- const out = {};
7258
- for (const id of featureFlagIds) {
7259
- const stage = featureFlags[id].stage;
7260
- out[id] = stage === "ga" || isFeatureVisible(stage, devHost) && enabled.has(id);
7261
- }
7262
- return out;
7263
- }
7264
-
7265
- // src/store/feature-flag-store.ts
7266
- async function listEnabledFeatureFlags(workspaceId) {
7267
- const { data, error: error2 } = await getSupabaseClient().from("feature_flags").select("flag_id").eq("workspace_id", workspaceId);
7268
- if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
7269
- return (data ?? []).map((r) => r.flag_id);
7270
- }
7271
- async function listEnabledFeatureFlagsBySlug(slug) {
7272
- const { data, error: error2 } = await getSupabaseClient().from("workspaces").select("id").eq("slug", slug).maybeSingle();
7273
- if (error2) throw new Error(`listEnabledFeatureFlagsBySlug(${slug}): ${error2.message}`);
7274
- return data ? listEnabledFeatureFlags(data.id) : [];
7275
- }
7276
- async function enableFeatureFlag(workspaceId, flagId, userId) {
7277
- const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
7278
- { workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
7279
- { onConflict: "workspace_id,flag_id" }
7280
- );
7281
- if (error2) throw new Error(`enableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
7282
- }
7283
- async function disableFeatureFlag(workspaceId, flagId) {
7284
- const { error: error2 } = await getSupabaseClient().from("feature_flags").delete().eq("workspace_id", workspaceId).eq("flag_id", flagId);
7285
- if (error2) throw new Error(`disableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
7286
- }
7287
-
7288
- // src/dashboard/feature-flags-routes.ts
7289
7271
  async function isFeatureEnabled(c, flagId) {
7290
7272
  const auth = c.get("auth");
7291
7273
  const enabled = await listEnabledFeatureFlags(auth.workspaceId);
@@ -7354,7 +7336,7 @@ function coerceMemberEmails(raw) {
7354
7336
  }
7355
7337
  function ensureCwdDir(cwd) {
7356
7338
  const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
7357
- if (!existsSync12(expanded)) mkdirSync8(expanded, { recursive: true });
7339
+ if (!existsSync12(expanded)) mkdirSync9(expanded, { recursive: true });
7358
7340
  }
7359
7341
  function toResponse(channel) {
7360
7342
  return {
@@ -7392,7 +7374,6 @@ function createChannelWriteRouter() {
7392
7374
  const externalChannelId = typeof body["externalChannelId"] === "string" && body["externalChannelId"].trim() ? body["externalChannelId"].trim() : null;
7393
7375
  const categoryId = typeof body["categoryId"] === "string" && body["categoryId"].trim() ? body["categoryId"].trim() : null;
7394
7376
  try {
7395
- ensureCwdDir(cwd);
7396
7377
  const channel = await createChannel({
7397
7378
  name,
7398
7379
  cwd,
@@ -7405,9 +7386,10 @@ function createChannelWriteRouter() {
7405
7386
  externalChannelId,
7406
7387
  categoryId
7407
7388
  });
7389
+ ensureCwdDir(cwd);
7408
7390
  return c.json(toResponse(channel), 201);
7409
7391
  } catch (err) {
7410
- 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);
7411
7393
  }
7412
7394
  });
7413
7395
  app.put("/:id", async (c) => {
@@ -7421,7 +7403,6 @@ function createChannelWriteRouter() {
7421
7403
  if (coerceRuntime(body["runtime"]) === "sandbox" && existing.runtime !== "sandbox" && !await isFeatureEnabled(c, "sandbox"))
7422
7404
  return c.json({ error: "feature not enabled" }, 403);
7423
7405
  try {
7424
- ensureCwdDir(cwd);
7425
7406
  const channel = await updateChannelById(id, {
7426
7407
  name: typeof body["name"] === "string" ? body["name"] : void 0,
7427
7408
  cwd,
@@ -7434,10 +7415,11 @@ function createChannelWriteRouter() {
7434
7415
  runtime: coerceRuntime(body["runtime"]),
7435
7416
  mounts: coerceMounts(body["mounts"])
7436
7417
  });
7418
+ ensureCwdDir(cwd);
7437
7419
  dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
7438
7420
  return c.json(toResponse(channel), 200);
7439
7421
  } catch (err) {
7440
- 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);
7441
7423
  }
7442
7424
  });
7443
7425
  app.patch("/reorder", async (c) => {
@@ -7489,12 +7471,12 @@ function createChannelWriteRouter() {
7489
7471
  patch.archivedAt = body["archived"] ? (/* @__PURE__ */ new Date()).toISOString() : null;
7490
7472
  }
7491
7473
  try {
7492
- if (patch.cwd) ensureCwdDir(patch.cwd);
7493
7474
  const channel = await updateChannelById(id, patch);
7475
+ if (patch.cwd) ensureCwdDir(patch.cwd);
7494
7476
  dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
7495
7477
  return c.json(toResponse(channel), 200);
7496
7478
  } catch (err) {
7497
- 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);
7498
7480
  }
7499
7481
  });
7500
7482
  return app;
@@ -7503,20 +7485,22 @@ function createChannelWriteRouter() {
7503
7485
  // src/dashboard/docs-routes.ts
7504
7486
  import {
7505
7487
  closeSync,
7488
+ createReadStream,
7506
7489
  existsSync as existsSync16,
7507
7490
  lstatSync as lstatSync2,
7508
- mkdirSync as mkdirSync9,
7491
+ mkdirSync as mkdirSync10,
7509
7492
  openSync,
7510
7493
  readSync,
7511
7494
  readdirSync as readdirSync4,
7512
- readFileSync as readFileSync13,
7495
+ readFileSync as readFileSync14,
7513
7496
  realpathSync as realpathSync3,
7514
7497
  renameSync,
7515
- rmSync as rmSync7,
7498
+ rmSync as rmSync8,
7516
7499
  statSync as statSync5,
7517
7500
  writeFileSync as writeFileSync9
7518
7501
  } from "fs";
7519
- import { basename as basename6, dirname as dirname5, extname, join as join19, relative, resolve as resolve4, sep } from "path";
7502
+ import { Readable } from "stream";
7503
+ import { basename as basename6, dirname as dirname5, extname, join as join20, relative, resolve as resolve4, sep } from "path";
7520
7504
  import { Hono as Hono10 } from "hono";
7521
7505
  import { streamSSE } from "hono/streaming";
7522
7506
 
@@ -7800,7 +7784,7 @@ async function readOrQueueDocAudio(input2) {
7800
7784
  }
7801
7785
 
7802
7786
  // src/dashboard/artifact-export.ts
7803
- import { readFileSync as readFileSync12 } from "fs";
7787
+ import { readFileSync as readFileSync13 } from "fs";
7804
7788
 
7805
7789
  // src/dashboard/chat-agent.ts
7806
7790
  import { homedir as homedir7 } from "os";
@@ -8584,234 +8568,128 @@ function readAgentPersona(configDir2, cwd, name) {
8584
8568
  }
8585
8569
  }
8586
8570
 
8587
- // src/store/memory-store.ts
8588
- var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
8589
- var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8590
- var DESCRIPTION_MAX = 200;
8591
- var BODY_MAX = 4e3;
8592
- var MEMORY_INDEX_MAX_CHARS = 8e3;
8593
- function isMemoryType(value) {
8594
- return MEMORY_TYPES.includes(value);
8595
- }
8596
- function validateMemoryInput(input2) {
8597
- if (!NAME_RE.test(input2.name))
8598
- throw new Error("name must be a kebab-case slug, e.g. deploy-flow");
8599
- if (!isMemoryType(input2.type)) throw new Error(`type must be one of ${MEMORY_TYPES.join(", ")}`);
8600
- const description = input2.description.trim();
8601
- if (!description || description.includes("\n") || description.length > DESCRIPTION_MAX) {
8602
- throw new Error(`description must be one line of at most ${DESCRIPTION_MAX} characters`);
8603
- }
8604
- const body = input2.body.trim();
8605
- if (!body || body.length > BODY_MAX) {
8606
- throw new Error(`body must be 1 to ${BODY_MAX} characters`);
8571
+ // src/dashboard/memory-extraction-runner.ts
8572
+ import { spawn as spawn4 } from "child_process";
8573
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync7 } from "fs";
8574
+ import { tmpdir as tmpdir3 } from "os";
8575
+ import { join as join18 } from "path";
8576
+ var CLAUDE_MODEL = "sonnet";
8577
+ var CODEX_MODEL = "gpt-6-astra";
8578
+ var CODEX_EFFORT = "low";
8579
+ var TIMEOUT_MS = 15e4;
8580
+ function buildInvocation(runner, req, dir, auth, baseEnv = process.env) {
8581
+ const env = { ...baseEnv, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
8582
+ if (runner === "codex") {
8583
+ const outputFile = join18(dir, "last-message.txt");
8584
+ return {
8585
+ command: "codex",
8586
+ args: [
8587
+ "exec",
8588
+ "--ephemeral",
8589
+ // No MCP servers, profiles or hooks from the user's config.toml; auth
8590
+ // still comes from CODEX_HOME.
8591
+ "--ignore-user-config",
8592
+ "--skip-git-repo-check",
8593
+ "-s",
8594
+ "read-only",
8595
+ "-m",
8596
+ CODEX_MODEL,
8597
+ "-c",
8598
+ `model_reasoning_effort=${CODEX_EFFORT}`,
8599
+ "--output-last-message",
8600
+ outputFile,
8601
+ // Codex has no separate system prompt flag; `-` reads the prompt from stdin.
8602
+ "-"
8603
+ ],
8604
+ stdin: `${req.system}
8605
+
8606
+ ---
8607
+
8608
+ ${req.user}`,
8609
+ env,
8610
+ outputFile
8611
+ };
8607
8612
  }
8608
- }
8609
- function rowToMemory(row) {
8613
+ const pinned = pinnedClaudeConfigDir(auth.claudeConfigDir);
8614
+ if (pinned) env["CLAUDE_CONFIG_DIR"] = pinned;
8615
+ else delete env["CLAUDE_CONFIG_DIR"];
8616
+ if (auth.claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = auth.claudeCodeOauthToken;
8617
+ else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
8610
8618
  return {
8611
- id: row["id"],
8612
- channelId: row["channel_id"],
8613
- name: row["name"],
8614
- description: row["description"],
8615
- type: row["type"],
8616
- body: row["body"],
8617
- sourceThreadId: row["source_thread_id"] ?? null,
8618
- createdBy: row["created_by"],
8619
- createdAt: row["created_at"],
8620
- updatedAt: row["updated_at"]
8619
+ command: "claude",
8620
+ args: [
8621
+ "-p",
8622
+ "--model",
8623
+ CLAUDE_MODEL,
8624
+ "--no-session-persistence",
8625
+ "--tools",
8626
+ "",
8627
+ "--strict-mcp-config",
8628
+ "--setting-sources",
8629
+ "",
8630
+ "--system-prompt",
8631
+ req.system
8632
+ ],
8633
+ stdin: req.user,
8634
+ env
8621
8635
  };
8622
8636
  }
8623
- async function listChannelMemories(channelId) {
8624
- const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
8625
- if (error2) throw new Error(`listChannelMemories(${channelId}): ${error2.message}`);
8626
- return (data ?? []).map(rowToMemory);
8627
- }
8628
- async function getChannelMemory(channelId, name) {
8629
- const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
8630
- if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
8631
- return data ? rowToMemory(data) : null;
8632
- }
8633
- async function saveChannelMemory(input2, actor) {
8634
- validateMemoryInput(input2);
8635
- const supabase = getSupabaseClient();
8636
- const existing = await getChannelMemory(input2.channelId, input2.name);
8637
- const fields = {
8638
- description: input2.description.trim(),
8639
- type: input2.type,
8640
- body: input2.body.trim(),
8641
- source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
8642
- created_by: input2.createdBy
8643
- };
8644
- if (existing && sameContent(existing, fields)) {
8645
- return { memory: existing, created: false, changed: false };
8646
- }
8647
- const { data, error: error2 } = existing ? await supabase.from("memories").update({ ...fields, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id).select("*").single() : await supabase.from("memories").insert({ ...fields, scope: "channel", channel_id: input2.channelId, name: input2.name }).select("*").single();
8648
- if (error2)
8649
- throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
8650
- const memory = rowToMemory(data);
8651
- await logMemoryEvent({
8652
- memory,
8653
- action: existing ? "update" : "create",
8654
- before: existing ? snapshot(existing) : null,
8655
- after: snapshot(memory),
8656
- actor,
8657
- threadId: input2.sourceThreadId ?? null
8658
- });
8659
- return { memory, created: !existing, changed: true };
8660
- }
8661
- async function updateChannelMemory(channelId, memoryId, patch, actor) {
8662
- const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
8663
- if (!existing) return null;
8664
- const { memory } = await saveChannelMemory(
8665
- {
8666
- channelId,
8667
- name: existing.name,
8668
- description: patch.description ?? existing.description,
8669
- type: patch.type ?? existing.type,
8670
- body: patch.body ?? existing.body,
8671
- createdBy: existing.createdBy
8672
- },
8673
- actor
8674
- );
8675
- return memory;
8676
- }
8677
- async function searchChannelMemories(channelId, query) {
8678
- const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
8679
- if (!words.length) return [];
8680
- let request = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
8681
- for (const word of words) {
8682
- request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
8683
- }
8684
- const { data, error: error2 } = await request.order("updated_at", { ascending: false }).limit(20);
8685
- if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
8686
- return (data ?? []).map(rowToMemory);
8687
- }
8688
- async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
8689
- const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
8690
- if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
8691
- const deleted = (data ?? []).map(rowToMemory);
8692
- for (const memory of deleted) {
8693
- await logMemoryEvent({
8694
- memory,
8695
- action: "delete",
8696
- before: snapshot(memory),
8697
- after: null,
8698
- actor,
8699
- threadId: threadId ?? null
8637
+ function run3(inv, cwd) {
8638
+ return new Promise((resolve7) => {
8639
+ const child = spawn4(inv.command, inv.args, {
8640
+ cwd,
8641
+ env: inv.env,
8642
+ stdio: ["pipe", "pipe", "pipe"]
8700
8643
  });
8701
- }
8702
- return deleted.length > 0;
8703
- }
8704
- function snapshot(m) {
8705
- return {
8706
- description: m.description,
8707
- type: m.type,
8708
- body: m.body,
8709
- sourceThreadId: m.sourceThreadId,
8710
- createdBy: m.createdBy
8711
- };
8712
- }
8713
- function sameContent(m, other) {
8714
- return m.description === other.description && m.type === other.type && m.body === other.body;
8715
- }
8716
- async function logMemoryEvent(e) {
8717
- const { error: error2 } = await getSupabaseClient().from("memory_events").insert({
8718
- channel_id: e.memory.channelId,
8719
- memory_id: e.action === "delete" ? null : e.memory.id,
8720
- thread_id: e.threadId,
8721
- name: e.memory.name,
8722
- action: e.action,
8723
- before: e.before,
8724
- after: e.after,
8725
- actor: e.actor
8644
+ let stdout = "";
8645
+ let stderr = "";
8646
+ const timer2 = setTimeout(() => child.kill("SIGTERM"), TIMEOUT_MS);
8647
+ child.stdout.on("data", (chunk3) => stdout += chunk3.toString());
8648
+ child.stderr.on("data", (chunk3) => stderr += chunk3.toString());
8649
+ child.on("error", (err) => {
8650
+ clearTimeout(timer2);
8651
+ console.error(`[memory-extraction] ${inv.command} failed to start:`, err.message);
8652
+ resolve7({ ok: false });
8653
+ });
8654
+ child.on("close", (code) => {
8655
+ clearTimeout(timer2);
8656
+ if (code !== 0) {
8657
+ const detail = (stderr || stdout).trim().split("\n").at(-1) ?? "";
8658
+ console.error(`[memory-extraction] ${inv.command} exited ${code}: ${detail.slice(0, 200)}`);
8659
+ resolve7({ ok: false });
8660
+ return;
8661
+ }
8662
+ let raw = stdout;
8663
+ if (inv.outputFile) {
8664
+ try {
8665
+ raw = readFileSync12(inv.outputFile, "utf-8");
8666
+ } catch {
8667
+ raw = "";
8668
+ }
8669
+ }
8670
+ resolve7({ ok: true, raw });
8671
+ });
8672
+ child.stdin.end(inv.stdin);
8726
8673
  });
8727
- if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
8728
8674
  }
8729
- function rowToEvent(row) {
8730
- return {
8731
- id: row["id"],
8732
- channelId: row["channel_id"],
8733
- memoryId: row["memory_id"] ?? null,
8734
- threadId: row["thread_id"] ?? null,
8735
- name: row["name"],
8736
- action: row["action"],
8737
- before: row["before"] ?? null,
8738
- after: row["after"] ?? null,
8739
- actor: row["actor"],
8740
- createdAt: row["created_at"],
8741
- undoneAt: row["undone_at"] ?? null
8742
- };
8743
- }
8744
- async function listThreadMemoryEvents(channelId, threadId) {
8745
- const { data, error: error2 } = await getSupabaseClient().from("memory_events").select("*").eq("channel_id", channelId).eq("thread_id", threadId).order("created_at", { ascending: false }).limit(50);
8746
- if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
8747
- return (data ?? []).map(rowToEvent);
8748
- }
8749
- async function undoMemoryEvent(channelId, eventId) {
8750
- const supabase = getSupabaseClient();
8751
- const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
8752
- if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
8753
- if (!data) return "not-found";
8754
- const event = rowToEvent(data);
8755
- if (event.undoneAt) return "already-undone";
8756
- const current = await getChannelMemory(channelId, event.name);
8757
- const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
8758
- if (!unchangedSince) return "conflict";
8759
- const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
8760
- if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
8761
- if (!(claimed.data ?? []).length) return "already-undone";
8762
- const before = event.before;
8763
- const restore = before ? {
8764
- description: before.description,
8765
- type: before.type,
8766
- body: before.body,
8767
- source_thread_id: before.sourceThreadId,
8768
- created_by: before.createdBy
8769
- } : null;
8770
- const { error: writeError } = !restore ? await supabase.from("memories").delete().eq("id", current.id) : current ? await supabase.from("memories").update({ ...restore, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", current.id) : await supabase.from("memories").insert({ ...restore, scope: "channel", channel_id: channelId, name: event.name });
8771
- if (writeError) {
8772
- await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
8773
- throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
8774
- }
8775
- return "undone";
8776
- }
8777
- function isUuid(value) {
8778
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
8779
- }
8780
- function formatMemoryPrompt(channelId, memories) {
8781
- const lines = [];
8782
- let used = 0;
8783
- let omitted = 0;
8784
- for (const m of memories) {
8785
- const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
8786
- if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
8787
- omitted++;
8788
- continue;
8789
- }
8790
- lines.push(line);
8791
- used += line.length + 1;
8675
+ async function completeMemoryExtraction(req) {
8676
+ const runner = runnerForModel(loadDashboardSettings().agent);
8677
+ const config = resolveChannelConfig(req.channelId);
8678
+ const dir = mkdtempSync2(join18(tmpdir3(), "flyspace-memory-"));
8679
+ try {
8680
+ const inv = buildInvocation(runner, req, dir, {
8681
+ ...config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {},
8682
+ ...config.claudeCodeOauthToken ? { claudeCodeOauthToken: config.claudeCodeOauthToken } : {}
8683
+ });
8684
+ return await run3(inv, dir);
8685
+ } finally {
8686
+ rmSync7(dir, { recursive: true, force: true });
8792
8687
  }
8793
- const index = lines.length ? lines.join("\n") + (omitted ? `
8794
- - (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
8795
- return [
8796
- `## Channel memory`,
8797
- ``,
8798
- `This channel has a persistent memory shared by every thread in it and by every agent runner. Use it instead of any built-in memory directory of your own CLI.`,
8799
- ``,
8800
- `- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
8801
- `- Search: \`flyspace memory search --channel ${channelId} <words>\``,
8802
- `- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
8803
- `- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
8804
- ``,
8805
- `Save when someone asks you to remember something, corrects you, or a decision is made that later threads will need. Save what is not derivable from the code, git history or docs: decisions and why, preferences, corrections, where external things live. Do not save one-off details of this conversation. Check the index first and update an existing memory rather than adding a near-duplicate. Write memories in English. Pass \`--thread\` with the thread id from the \`[context: ...]\` header, so the thread shows what changed and it can be undone there.`,
8806
- ``,
8807
- `Index:`,
8808
- index
8809
- ].join("\n");
8810
8688
  }
8811
8689
 
8812
8690
  // src/dashboard/rollover-summary.ts
8813
- import { spawn as spawn4 } from "child_process";
8814
- import { tmpdir as tmpdir3 } from "os";
8691
+ import { spawn as spawn5 } from "child_process";
8692
+ import { tmpdir as tmpdir4 } from "os";
8815
8693
  var HAIKU_MODEL = "haiku";
8816
8694
  var CJK_PATTERN = /[ -〿぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/;
8817
8695
  function estimateTokens(text) {
@@ -8870,11 +8748,11 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8870
8748
  if (claudeConfigDir) env["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
8871
8749
  if (claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = claudeCodeOauthToken;
8872
8750
  else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
8873
- const child = spawn4("claude", args, {
8751
+ const child = spawn5("claude", args, {
8874
8752
  // A stateless rewrite touches no files — tmpdir keeps it that way and
8875
8753
  // avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
8876
8754
  // unlike the channel's own cwd the main turn runs in.
8877
- cwd: tmpdir3(),
8755
+ cwd: tmpdir4(),
8878
8756
  env,
8879
8757
  stdio: ["ignore", "pipe", "pipe"]
8880
8758
  });
@@ -9077,7 +8955,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
9077
8955
 
9078
8956
  // src/dashboard/attachment-downloader.ts
9079
8957
  import { writeFile } from "fs/promises";
9080
- import { join as join18 } from "path";
8958
+ import { join as join19 } from "path";
9081
8959
  import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
9082
8960
  var debug = process.env["DEBUG"] === "true";
9083
8961
  var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
@@ -9124,7 +9002,7 @@ async function downloadOne(key) {
9124
9002
  if (bytes.length > sizeLimit) {
9125
9003
  return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
9126
9004
  }
9127
- const localPath = join18(paths.UPLOADS_DIR, lastSegment);
9005
+ const localPath = join19(paths.UPLOADS_DIR, lastSegment);
9128
9006
  await writeFile(localPath, bytes);
9129
9007
  if (debug)
9130
9008
  console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
@@ -9296,21 +9174,21 @@ var ChatAgent = class {
9296
9174
  * kill.
9297
9175
  */
9298
9176
  async maybeSupersede(msg) {
9299
- const run3 = this.activeRuns.get(msg.threadId);
9300
- if (!run3 || run3.msg.author !== msg.author || run3.msg.replyId === msg.replyId) return;
9177
+ const run4 = this.activeRuns.get(msg.threadId);
9178
+ if (!run4 || run4.msg.author !== msg.author || run4.msg.replyId === msg.replyId) return;
9301
9179
  try {
9302
- if (!await this.shouldInterrupt(run3.msg.body, msg.body)) return;
9180
+ if (!await this.shouldInterrupt(run4.msg.body, msg.body)) return;
9303
9181
  } catch (err) {
9304
9182
  console.error("[chat-agent] interrupt classifier failed:", err.message);
9305
9183
  return;
9306
9184
  }
9307
- if (this.activeRuns.get(msg.threadId) !== run3) return;
9185
+ if (this.activeRuns.get(msg.threadId) !== run4) return;
9308
9186
  console.log(
9309
9187
  `[chat-agent] follow-up supersedes in-flight run \u2014 aborting. thread=${msg.threadId}`
9310
9188
  );
9311
- run3.superseded = true;
9189
+ run4.superseded = true;
9312
9190
  this.supersedingReplies.add(msg.replyId);
9313
- run3.abort.abort();
9191
+ run4.abort.abort();
9314
9192
  }
9315
9193
  /**
9316
9194
  * Picks up messages nobody answered. `handleMessage` is fire-and-forget and
@@ -9473,6 +9351,11 @@ var ChatAgent = class {
9473
9351
  return data.filter((r) => r.id !== excludeReplyId).reverse().slice(-limit).map((r) => ({ author: r.author, body: r.body }));
9474
9352
  }
9475
9353
  /** Whether a reply run is currently in flight for this thread. */
9354
+ /** One idle memory extraction call (see memory-extraction-runner.ts). Not a
9355
+ * turn: no thread, session, queue slot or usage row. */
9356
+ completeMemoryExtraction(req) {
9357
+ return completeMemoryExtraction(req);
9358
+ }
9476
9359
  isThinking(threadId) {
9477
9360
  return this.activeRuns.has(threadId);
9478
9361
  }
@@ -9508,9 +9391,9 @@ var ChatAgent = class {
9508
9391
  * continue with `--resume`. Returns whether a run was actually in flight —
9509
9392
  * a queued-but-not-started message is not affected. */
9510
9393
  cancel(threadId) {
9511
- const run3 = this.activeRuns.get(threadId);
9512
- if (!run3) return false;
9513
- run3.abort.abort();
9394
+ const run4 = this.activeRuns.get(threadId);
9395
+ if (!run4) return false;
9396
+ run4.abort.abort();
9514
9397
  return true;
9515
9398
  }
9516
9399
  /**
@@ -9672,18 +9555,12 @@ var ChatAgent = class {
9672
9555
  ...docsAccessUnverified ? { docsAccessUnverified } : {}
9673
9556
  };
9674
9557
  }
9675
- const agentMemory = await listEnabledFeatureFlagsBySlug(owningProfile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
9676
- console.error("[chat-agent] feature flag lookup failed:", err.message);
9677
- return false;
9558
+ const memory = await loadAgentMemory(owningProfile, thread.channel_id, {
9559
+ readable: docsAccess !== "none",
9560
+ ...isHumanAuthor(msg.author) ? { senderHandle: msg.author } : {}
9678
9561
  });
9679
- if (agentMemory) channelConfig = { ...channelConfig, disableAutoMemory: true };
9680
- if (agentMemory && docsAccess !== "none" && isUuid(thread.channel_id)) {
9681
- const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
9682
- console.error("[chat-agent] memory lookup failed:", err.message);
9683
- return void 0;
9684
- });
9685
- if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
9686
- }
9562
+ if (memory.enabled) channelConfig = { ...channelConfig, disableAutoMemory: true };
9563
+ if (memory.memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt: memory.memoryPrompt };
9687
9564
  const store = this.openSessionStore();
9688
9565
  try {
9689
9566
  const channelKey = `chat:${thread.channel_id}`;
@@ -10411,7 +10288,7 @@ async function exportDocAsArtifact(channelId, docPath) {
10411
10288
  );
10412
10289
  const abs = resolveDocPath(channelConfig.cwd, docPath);
10413
10290
  if (!abs) throw new Error(`doc not found: ${docPath}`);
10414
- const content = readFileSync12(abs, "utf8");
10291
+ const content = readFileSync13(abs, "utf8");
10415
10292
  const title = extractTitle(abs, "md");
10416
10293
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10417
10294
  const prompt = `Publish the document at \`${docPath}\` (already read below) as a Claude Artifact, then record the result as a stub file under this channel's \`docs/artifacts/\` folder (create the folder if it doesn't exist).
@@ -10479,7 +10356,7 @@ var MEDIA_EXTENSIONS = {
10479
10356
  ".csv": "text/csv; charset=utf-8",
10480
10357
  ".json": "application/json"
10481
10358
  };
10482
- var MAX_RAW_BYTES = 20 * 1024 * 1024;
10359
+ var MAX_RAW_BYTES = 100 * 1024 * 1024;
10483
10360
  var ASSET_LIST_EXTENSIONS = /* @__PURE__ */ new Set([
10484
10361
  ".png",
10485
10362
  ".jpg",
@@ -10495,7 +10372,7 @@ var ASSET_LIST_EXTENSIONS = /* @__PURE__ */ new Set([
10495
10372
  ".txt",
10496
10373
  ".csv"
10497
10374
  ]);
10498
- var MAX_UPLOAD_BYTES = MAX_RAW_BYTES;
10375
+ var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
10499
10376
  var CODE_EXTENSIONS = {
10500
10377
  ".js": "javascript",
10501
10378
  ".jsx": "javascript",
@@ -10668,7 +10545,7 @@ function probeDir(absDir, showHidden) {
10668
10545
  return false;
10669
10546
  }
10670
10547
  if (!showHidden && entry.name.startsWith(".")) continue;
10671
- const abs = join19(dir, entry.name);
10548
+ const abs = join20(dir, entry.name);
10672
10549
  const kind = entryKind(entry, abs);
10673
10550
  if (kind === "dir") {
10674
10551
  if (SKIP_DIRS.has(entry.name)) hasOther = true;
@@ -10688,14 +10565,14 @@ function probeDir(absDir, showHidden) {
10688
10565
  }
10689
10566
  function readGitBranch(dir) {
10690
10567
  try {
10691
- let gitDir = join19(dir, ".git");
10568
+ let gitDir = join20(dir, ".git");
10692
10569
  if (statSync5(gitDir).isFile()) {
10693
- const pointer = readFileSync13(gitDir, "utf8").trim();
10570
+ const pointer = readFileSync14(gitDir, "utf8").trim();
10694
10571
  const match = pointer.match(/^gitdir:\s*(.+)$/);
10695
10572
  if (!match) return void 0;
10696
10573
  gitDir = resolve4(dir, match[1]);
10697
10574
  }
10698
- const head = readFileSync13(join19(gitDir, "HEAD"), "utf8").trim();
10575
+ const head = readFileSync14(join20(gitDir, "HEAD"), "utf8").trim();
10699
10576
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
10700
10577
  if (branchMatch) return branchMatch[1];
10701
10578
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -10717,14 +10594,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
10717
10594
  for (const entry of entries2) {
10718
10595
  if (dirs.length + docs.length >= MAX_ENTRIES) break;
10719
10596
  if (!showHidden && entry.name.startsWith(".")) continue;
10720
- const abs = join19(absDir, entry.name);
10597
+ const abs = join20(absDir, entry.name);
10721
10598
  const path = relative(root, abs).split(sep).join("/");
10722
10599
  const kind = entryKind(entry, abs);
10723
10600
  if (kind === "dir") {
10724
10601
  if (SKIP_DIRS.has(entry.name)) continue;
10725
10602
  const probe3 = probeDir(abs, showHidden);
10726
10603
  if (!probe3.hasDocs && probe3.hasOther) continue;
10727
- const isRepo = existsSync16(join19(abs, ".git"));
10604
+ const isRepo = existsSync16(join20(abs, ".git"));
10728
10605
  dirs.push({
10729
10606
  path,
10730
10607
  name: entry.name,
@@ -10760,7 +10637,7 @@ function flatDocs(root, {
10760
10637
  for (const entry of entries2) {
10761
10638
  if (hits.length >= MAX_FLAT_DOCS) return;
10762
10639
  if (!showHidden && entry.name.startsWith(".")) continue;
10763
- const abs = join19(dir, entry.name);
10640
+ const abs = join20(dir, entry.name);
10764
10641
  const kind = entryKind(entry, abs);
10765
10642
  if (kind === "dir") {
10766
10643
  if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
@@ -10840,7 +10717,7 @@ function uniqueName(absDir, name) {
10840
10717
  let candidate = name;
10841
10718
  for (let n = 1; n < 1e3; n++) {
10842
10719
  try {
10843
- statSync5(join19(absDir, candidate));
10720
+ statSync5(join20(absDir, candidate));
10844
10721
  } catch {
10845
10722
  return candidate;
10846
10723
  }
@@ -10862,7 +10739,7 @@ function saveUploadedFile(root, relDir, name, data) {
10862
10739
  if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
10863
10740
  if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
10864
10741
  const final = uniqueName(absDir, safe);
10865
- const abs = join19(absDir, final);
10742
+ const abs = join20(absDir, final);
10866
10743
  writeFileSync9(abs, data);
10867
10744
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
10868
10745
  }
@@ -10876,13 +10753,13 @@ function createDocFolder(root, relDir, name) {
10876
10753
  }
10877
10754
  const safe = sanitizeUploadName(name);
10878
10755
  if (!safe) return { status: "bad-name" };
10879
- const abs = join19(absDir, safe);
10756
+ const abs = join20(absDir, safe);
10880
10757
  try {
10881
10758
  statSync5(abs);
10882
10759
  return { status: "exists" };
10883
10760
  } catch {
10884
10761
  }
10885
- mkdirSync9(abs);
10762
+ mkdirSync10(abs);
10886
10763
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: safe };
10887
10764
  }
10888
10765
  function createDocFile(root, relDir, name, content) {
@@ -10899,7 +10776,7 @@ function createDocFile(root, relDir, name, content) {
10899
10776
  if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
10900
10777
  if (SKIP_FILES.has(safe)) return { status: "bad-name" };
10901
10778
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
10902
- const abs = join19(absDir, safe);
10779
+ const abs = join20(absDir, safe);
10903
10780
  try {
10904
10781
  writeFileSync9(abs, content, { encoding: "utf8", flag: "wx" });
10905
10782
  } catch (err) {
@@ -10937,7 +10814,7 @@ function moveDocEntry(root, from, to) {
10937
10814
  } catch {
10938
10815
  return { status: "bad-target" };
10939
10816
  }
10940
- const finalAbs = join19(absToDir, name);
10817
+ const finalAbs = join20(absToDir, name);
10941
10818
  if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
10942
10819
  return { status: "into-self" };
10943
10820
  }
@@ -10960,7 +10837,7 @@ function deleteDocEntry(root, path) {
10960
10837
  }
10961
10838
  const isDir2 = stat.isDirectory();
10962
10839
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10963
- rmSync7(abs, { recursive: isDir2 });
10840
+ rmSync8(abs, { recursive: isDir2 });
10964
10841
  return {
10965
10842
  status: "ok",
10966
10843
  path: relative(root, abs).split(sep).join("/"),
@@ -10999,13 +10876,15 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10999
10876
  }
11000
10877
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
11001
10878
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
11002
- const data = readFileSync13(resolved2.abs);
11003
- const headers = { "Content-Type": resolved2.contentType };
11004
- if (download) {
11005
- const filename = basename6(resolved2.abs).replace(/"/g, "");
11006
- headers["Content-Disposition"] = `attachment; filename="${filename}"`;
11007
- }
11008
- 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
+ });
11009
10888
  });
11010
10889
  app.get("/:id/docs/html", (c) => {
11011
10890
  const { cwd } = resolveChannelConfig(c.req.param("id"));
@@ -11019,7 +10898,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11019
10898
  }
11020
10899
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
11021
10900
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
11022
- return c.body(new Uint8Array(readFileSync13(abs)), 200, {
10901
+ return c.body(new Uint8Array(readFileSync14(abs)), 200, {
11023
10902
  "Content-Type": "text/html; charset=utf-8",
11024
10903
  "Content-Security-Policy": HTML_SHARE_CSP,
11025
10904
  "X-Content-Type-Options": "nosniff",
@@ -11204,7 +11083,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11204
11083
  title: kind ? extractTitle(abs, kind) : basename6(abs),
11205
11084
  mtime: stat.mtime.toISOString(),
11206
11085
  tooLarge: tooLarge || void 0,
11207
- content: tooLarge ? "" : readFileSync13(abs, "utf8")
11086
+ content: tooLarge ? "" : readFileSync14(abs, "utf8")
11208
11087
  });
11209
11088
  });
11210
11089
  app.put("/:id/docs/content", async (c) => {
@@ -11247,7 +11126,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11247
11126
  channel,
11248
11127
  rel,
11249
11128
  title: extractTitle(abs, "md"),
11250
- content: readFileSync13(abs, "utf8")
11129
+ content: readFileSync14(abs, "utf8")
11251
11130
  };
11252
11131
  };
11253
11132
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -11470,7 +11349,7 @@ function ownSkills(scope, dir) {
11470
11349
  return [{ scope, name, description, body, path, plugin: null, mtime }];
11471
11350
  });
11472
11351
  }
11473
- function snapshot2(scopes) {
11352
+ function snapshot(scopes) {
11474
11353
  const wants = (scope) => !scopes || scopes.has(scope);
11475
11354
  const configDir2 = profileClaudeConfigDir();
11476
11355
  const rows = [];
@@ -11505,7 +11384,7 @@ async function pushSkillScopes(scopes) {
11505
11384
  if (!mirrorEnabled()) return;
11506
11385
  const workspace = getProfile();
11507
11386
  const supabase = getSupabaseClient();
11508
- const rows = snapshot2(scopes ? new Set(scopes) : void 0);
11387
+ const rows = snapshot(scopes ? new Set(scopes) : void 0);
11509
11388
  let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
11510
11389
  if (scopes) query = query.in("scope", scopes);
11511
11390
  const { data, error: error2 } = await query;
@@ -11789,13 +11668,16 @@ function registerMirroredSkillRoutes(api, agent) {
11789
11668
  import { execFileSync as execFileSync3 } from "child_process";
11790
11669
  import { existsSync as existsSync19 } from "fs";
11791
11670
  import { homedir as homedir8 } from "os";
11792
- import { join as join20, resolve as resolve5 } from "path";
11671
+ import { join as join21, resolve as resolve5 } from "path";
11793
11672
  var PROVIDER_LABELS = {
11794
11673
  claude: "Claude Code",
11795
11674
  codex: "Codex"
11796
11675
  };
11797
11676
  var TTL_MS2 = 6e4;
11798
11677
  var cache6 = /* @__PURE__ */ new Map();
11678
+ function clearProviderStatusCache() {
11679
+ cache6.clear();
11680
+ }
11799
11681
  function binaryOnPath(bin) {
11800
11682
  try {
11801
11683
  execFileSync3(bin, ["--version"], { stdio: "ignore" });
@@ -11807,7 +11689,7 @@ function binaryOnPath(bin) {
11807
11689
  function claudeAuthAvailable() {
11808
11690
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
11809
11691
  const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
11810
- if (existsSync19(join20(configDir2, ".credentials.json"))) return true;
11692
+ if (existsSync19(join21(configDir2, ".credentials.json"))) return true;
11811
11693
  if (process.platform !== "darwin") return false;
11812
11694
  try {
11813
11695
  execFileSync3("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
@@ -11830,7 +11712,7 @@ function probe2(runner) {
11830
11712
  runner,
11831
11713
  label: label2,
11832
11714
  connected: false,
11833
- 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.`
11834
11716
  };
11835
11717
  }
11836
11718
  return { runner, label: label2, connected: true };
@@ -11840,7 +11722,7 @@ function withAuthFailure(status) {
11840
11722
  return {
11841
11723
  ...status,
11842
11724
  connected: false,
11843
- 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."
11844
11726
  };
11845
11727
  }
11846
11728
  function providerStatuses() {
@@ -11854,6 +11736,232 @@ function providerStatuses() {
11854
11736
  return statuses.map((st) => st.runner === "claude" && st.connected ? withAuthFailure(st) : st);
11855
11737
  }
11856
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
+
11857
11965
  // src/dashboard/host-api.ts
11858
11966
  function registerHostRoutes(api) {
11859
11967
  api.get("/channels/:id/skills", (c) => {
@@ -11902,6 +12010,7 @@ function registerHostRoutes(api) {
11902
12010
  return c.json({ ...await saveWorkspaceBranding(patch), ...local });
11903
12011
  });
11904
12012
  api.get("/settings/providers", (c) => c.json({ providers: providerStatuses() }));
12013
+ api.route("/settings/providers", createProviderLoginRouter());
11905
12014
  api.get(
11906
12015
  "/settings/usage-limit",
11907
12016
  (c) => c.json({
@@ -11960,8 +12069,8 @@ function registerHostRoutes(api) {
11960
12069
  const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
11961
12070
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
11962
12071
  const filename = errors ? "custie-error.log" : "custie.log";
11963
- const file = join21(paths.LOG_DIR, filename);
11964
- const exists = existsSync20(file);
12072
+ const file = join23(paths.LOG_DIR, filename);
12073
+ const exists = existsSync21(file);
11965
12074
  return c.json({
11966
12075
  file,
11967
12076
  exists,
@@ -11971,18 +12080,18 @@ function registerHostRoutes(api) {
11971
12080
  });
11972
12081
  }
11973
12082
  function tailFile(file, limit) {
11974
- if (!existsSync20(file)) return [];
11975
- const lines = readFileSync14(file, "utf-8").split("\n");
12083
+ if (!existsSync21(file)) return [];
12084
+ const lines = readFileSync16(file, "utf-8").split("\n");
11976
12085
  if (lines.at(-1) === "") lines.pop();
11977
12086
  return lines.slice(-limit);
11978
12087
  }
11979
12088
 
11980
12089
  // src/dashboard/data-api.ts
11981
- import { existsSync as existsSync22 } from "fs";
12090
+ import { existsSync as existsSync23 } from "fs";
11982
12091
  import sharp5 from "sharp";
11983
12092
 
11984
12093
  // src/dashboard/automations-routes.ts
11985
- import { Hono as Hono13 } from "hono";
12094
+ import { Hono as Hono14 } from "hono";
11986
12095
  var TYPES2 = ["schedule", "trigger", "mention-trigger"];
11987
12096
  function isType(t) {
11988
12097
  return TYPES2.includes(t);
@@ -11999,7 +12108,7 @@ function statusFor(message) {
11999
12108
  return 400;
12000
12109
  }
12001
12110
  function createAutomationWriteRouter() {
12002
- const app = new Hono13();
12111
+ const app = new Hono14();
12003
12112
  const manager = () => new AutomationManager();
12004
12113
  app.get("/:type/:name", (c) => {
12005
12114
  const item = manager().get(c.req.param("name"));
@@ -12055,9 +12164,9 @@ function createAutomationWriteRouter() {
12055
12164
  }
12056
12165
 
12057
12166
  // src/dashboard/categories-routes.ts
12058
- import { Hono as Hono14 } from "hono";
12167
+ import { Hono as Hono15 } from "hono";
12059
12168
  function createCategoryWriteRouter() {
12060
- const app = new Hono14();
12169
+ const app = new Hono15();
12061
12170
  app.get("/", async (c) => {
12062
12171
  const { data, error: error2 } = await getSupabaseClient().from("categories").select("*").eq("profile", getProfile()).order("position", { ascending: true });
12063
12172
  if (error2) return c.json({ error: error2.message }, 500);
@@ -12111,7 +12220,7 @@ function createCategoryWriteRouter() {
12111
12220
  }
12112
12221
 
12113
12222
  // src/dashboard/chat-routes.ts
12114
- import { Hono as Hono15 } from "hono";
12223
+ import { Hono as Hono16 } from "hono";
12115
12224
 
12116
12225
  // src/dashboard/fork-summary.ts
12117
12226
  var GROQ_API_URL6 = "https://api.groq.com/openai/v1/chat/completions";
@@ -12531,7 +12640,7 @@ async function withPreviews(supabase, threads) {
12531
12640
  threads.map((t) => t.id),
12532
12641
  THREAD_CHUNK_SIZE
12533
12642
  ).map(
12534
- (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 })
12535
12644
  )
12536
12645
  );
12537
12646
  const previewByThread = /* @__PURE__ */ new Map();
@@ -12541,8 +12650,15 @@ async function withPreviews(supabase, threads) {
12541
12650
  if (entry) {
12542
12651
  entry.count += 1;
12543
12652
  entry.lastAuthor = r.author;
12653
+ entry.participants.delete(r.author);
12654
+ entry.participants.set(r.author, r.external_user_id ?? null);
12544
12655
  } else
12545
- 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
+ });
12546
12662
  }
12547
12663
  }
12548
12664
  return threads.map((t) => {
@@ -12554,7 +12670,9 @@ async function withPreviews(supabase, threads) {
12554
12670
  // Who spoke last, counting only replies AFTER the opening message —
12555
12671
  // the reader's own handle here means there is nothing new for them to
12556
12672
  // read, on this browser or any other (see web/src/lib/thread-unread).
12557
- 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 }))
12558
12676
  };
12559
12677
  });
12560
12678
  }
@@ -12589,7 +12707,7 @@ async function generateAutoTitle(threadId, channelId, body) {
12589
12707
  await supabase.from("threads").update({ auto_title: autoTitle }).eq("id", threadId).is("title", null);
12590
12708
  }
12591
12709
  function createChatRouter(agent = {}) {
12592
- const app = new Hono15();
12710
+ const app = new Hono16();
12593
12711
  const {
12594
12712
  onChatMessage: onMessage,
12595
12713
  isAgentThinking,
@@ -13096,7 +13214,7 @@ function createChatRouter(agent = {}) {
13096
13214
  }
13097
13215
 
13098
13216
  // src/dashboard/doc-shares-routes.ts
13099
- import { Hono as Hono16 } from "hono";
13217
+ import { Hono as Hono17 } from "hono";
13100
13218
 
13101
13219
  // src/dashboard/doc-share-features.ts
13102
13220
  import { DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
@@ -13280,7 +13398,7 @@ function resolveChannel2(c) {
13280
13398
  }
13281
13399
  var LIST_LIMIT = 500;
13282
13400
  function createDocShareListRouter() {
13283
- const app = new Hono16();
13401
+ const app = new Hono17();
13284
13402
  app.get("/shares", async (c) => {
13285
13403
  const viewer = c.get("auth");
13286
13404
  const readable = await filterReadableDocsChannels(
@@ -13324,7 +13442,7 @@ function createDocShareListRouter() {
13324
13442
  return app;
13325
13443
  }
13326
13444
  function createDocShareWriteRouter(agent = {}) {
13327
- const app = new Hono16();
13445
+ const app = new Hono17();
13328
13446
  app.use("/:id/docs/share", requireDocsAccess);
13329
13447
  app.use("/:id/docs/share/*", requireDocsAccess);
13330
13448
  app.use("/:id/docs/shares", requireDocsAccess);
@@ -13481,7 +13599,7 @@ function createDocShareWriteRouter(agent = {}) {
13481
13599
  }
13482
13600
 
13483
13601
  // src/dashboard/search-routes.ts
13484
- import { Hono as Hono17 } from "hono";
13602
+ import { Hono as Hono18 } from "hono";
13485
13603
  function matchScore(q, fields) {
13486
13604
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
13487
13605
  if (terms.length === 0) return 0;
@@ -13629,7 +13747,7 @@ function searchAutomations(q) {
13629
13747
  }
13630
13748
  var RESULT_LIMIT = 30;
13631
13749
  function createSearchRouter() {
13632
- const app = new Hono17();
13750
+ const app = new Hono18();
13633
13751
  app.get("/", async (c) => {
13634
13752
  const q = c.req.query("q")?.trim();
13635
13753
  if (!q) return c.json({ results: [] });
@@ -13645,8 +13763,8 @@ function createSearchRouter() {
13645
13763
  }
13646
13764
 
13647
13765
  // src/dashboard/uploads-routes.ts
13648
- import { randomUUID as randomUUID2 } from "crypto";
13649
- import { Hono as Hono18 } from "hono";
13766
+ import { randomUUID as randomUUID3 } from "crypto";
13767
+ import { Hono as Hono19 } from "hono";
13650
13768
  import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
13651
13769
  import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
13652
13770
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
@@ -13661,7 +13779,7 @@ function encodeKeyForUrl(key) {
13661
13779
  return key.split("/").map(encodeURIComponent).join("/");
13662
13780
  }
13663
13781
  function createUploadsRouter() {
13664
- const app = new Hono18();
13782
+ const app = new Hono19();
13665
13783
  app.post("/uploads", async (c) => {
13666
13784
  const body = await c.req.parseBody().catch(() => null);
13667
13785
  const file = body?.["file"];
@@ -13669,7 +13787,7 @@ function createUploadsRouter() {
13669
13787
  if (file.size > MAX_FILE_BYTES) {
13670
13788
  return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
13671
13789
  }
13672
- const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
13790
+ const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID3()}-${sanitizeFilename(file.name)}`;
13673
13791
  const buffer = Buffer.from(await file.arrayBuffer());
13674
13792
  await getR2Client().send(
13675
13793
  new PutObjectCommand3({
@@ -13706,14 +13824,14 @@ function createUploadsRouter() {
13706
13824
  }
13707
13825
 
13708
13826
  // src/dashboard/transcribe-routes.ts
13709
- import { Hono as Hono19 } from "hono";
13827
+ import { Hono as Hono20 } from "hono";
13710
13828
  var GROQ_TRANSCRIBE_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
13711
13829
  var MODEL = "whisper-large-v3-turbo";
13712
13830
  var MAX_AUDIO_BYTES = 5 * 1024 * 1024;
13713
13831
  var MAX_PROMPT_CHARS = 400;
13714
13832
  var NO_SPEECH_THRESHOLD = 0.6;
13715
13833
  function createTranscribeRouter() {
13716
- const app = new Hono19();
13834
+ const app = new Hono20();
13717
13835
  app.post("/transcribe", async (c) => {
13718
13836
  if (!await isFeatureEnabled(c, "dictation"))
13719
13837
  return c.json({ error: "feature not enabled" }, 403);
@@ -13752,8 +13870,8 @@ function createTranscribeRouter() {
13752
13870
  }
13753
13871
 
13754
13872
  // src/dashboard/avatars-routes.ts
13755
- import { createHash as createHash10, randomUUID as randomUUID3 } from "crypto";
13756
- import { Hono as Hono20 } from "hono";
13873
+ import { createHash as createHash10, randomUUID as randomUUID4 } from "crypto";
13874
+ import { Hono as Hono21 } from "hono";
13757
13875
  import sharp4 from "sharp";
13758
13876
  import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
13759
13877
  var AVATAR_SIZE = 192;
@@ -13791,7 +13909,7 @@ async function storeAvatar(file, owner) {
13791
13909
  if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
13792
13910
  const original = Buffer.from(await file.arrayBuffer());
13793
13911
  const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
13794
- const key = `${avatarKeyPrefix()}${owner}-${randomUUID3()}.png`;
13912
+ const key = `${avatarKeyPrefix()}${owner}-${randomUUID4()}.png`;
13795
13913
  await getR2Client().send(
13796
13914
  new PutObjectCommand4({
13797
13915
  Bucket: getR2Bucket(),
@@ -13806,7 +13924,7 @@ function avatarKeyPrefix() {
13806
13924
  return `${KEY_PREFIX3}/${getProfile()}/`;
13807
13925
  }
13808
13926
  function createAvatarsRouter() {
13809
- const app = new Hono20();
13927
+ const app = new Hono21();
13810
13928
  app.get("/avatars/slack", async (c) => {
13811
13929
  const ctx = await resolveAuthContext(c);
13812
13930
  if (!ctx) return c.json({ error: "not signed in" }, 401);
@@ -13855,7 +13973,7 @@ function createAvatarsRouter() {
13855
13973
  }
13856
13974
 
13857
13975
  // src/dashboard/members-routes.ts
13858
- import { Hono as Hono21 } from "hono";
13976
+ import { Hono as Hono22 } from "hono";
13859
13977
  function inviteLinkFor(c, token) {
13860
13978
  const url = new URL("/auth/invite", callbackUrl(c));
13861
13979
  url.searchParams.set("token", token);
@@ -13863,7 +13981,7 @@ function inviteLinkFor(c, token) {
13863
13981
  }
13864
13982
  var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
13865
13983
  function createMembersRouter() {
13866
- const members = new Hono21();
13984
+ const members = new Hono22();
13867
13985
  members.get("/", async (c) => {
13868
13986
  const ctx = await resolveAuthContext(c);
13869
13987
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -13904,7 +14022,7 @@ function createMembersRouter() {
13904
14022
  return { userId: m.userId, handle: m.handle, role: m.role, avatarUrl };
13905
14023
  })
13906
14024
  );
13907
- return c.json({ members: directory });
14025
+ return c.json({ workspaceId: ctx.workspaceId, members: directory });
13908
14026
  });
13909
14027
  members.post("/:userId/dm", async (c) => {
13910
14028
  const ctx = await resolveAuthContext(c);
@@ -13956,6 +14074,10 @@ function createMembersRouter() {
13956
14074
  if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
13957
14075
  return c.json({ error: "unknown avatar" }, 400);
13958
14076
  }
14077
+ const { plan } = await getWorkspaceBilling(ctx.workspaceId);
14078
+ if (plan === "local") {
14079
+ return c.json({ error: "The Solo plan is for one person. Upgrade to invite members." }, 403);
14080
+ }
13959
14081
  const existing = await getMembershipByEmail(ctx.workspaceId, email);
13960
14082
  if (existing) return c.json({ error: "already a member of this workspace" }, 409);
13961
14083
  let membership;
@@ -14074,7 +14196,7 @@ function createMembersRouter() {
14074
14196
  }
14075
14197
 
14076
14198
  // src/dashboard/notifications-routes.ts
14077
- import { Hono as Hono22 } from "hono";
14199
+ import { Hono as Hono23 } from "hono";
14078
14200
 
14079
14201
  // src/store/notification-store.ts
14080
14202
  function rowToNotification(row) {
@@ -14142,7 +14264,7 @@ async function markAllRead(channelIds, userId) {
14142
14264
 
14143
14265
  // src/dashboard/notifications-routes.ts
14144
14266
  function createNotificationsRouter() {
14145
- const notifications = new Hono22();
14267
+ const notifications = new Hono23();
14146
14268
  notifications.get("/", async (c) => {
14147
14269
  const auth = c.get("auth");
14148
14270
  const channelIds = getProfileChannelRegistry().filter((ch) => canSeeChannel(ch, auth)).map((ch) => ch.id);
@@ -14169,7 +14291,7 @@ function createNotificationsRouter() {
14169
14291
  return notifications;
14170
14292
  }
14171
14293
  function createThreadReadRouter() {
14172
- const threads = new Hono22();
14294
+ const threads = new Hono23();
14173
14295
  threads.post("/threads/:threadId/read", async (c) => {
14174
14296
  const { userId } = c.get("auth");
14175
14297
  const threadId = c.req.param("threadId");
@@ -14181,7 +14303,7 @@ function createThreadReadRouter() {
14181
14303
  }
14182
14304
 
14183
14305
  // src/dashboard/push-routes.ts
14184
- import { Hono as Hono23 } from "hono";
14306
+ import { Hono as Hono24 } from "hono";
14185
14307
 
14186
14308
  // src/push/send.ts
14187
14309
  import webpush from "web-push";
@@ -14284,7 +14406,7 @@ async function pushToUser(userId, payload) {
14284
14406
 
14285
14407
  // src/dashboard/push-routes.ts
14286
14408
  function createPushRouter() {
14287
- const push = new Hono23();
14409
+ const push = new Hono24();
14288
14410
  push.get("/key", (c) => c.json({ publicKey: getVapidPublicKey() }));
14289
14411
  push.post("/subscribe", async (c) => {
14290
14412
  const { userId } = c.get("auth");
@@ -14315,7 +14437,7 @@ function createPushRouter() {
14315
14437
  }
14316
14438
 
14317
14439
  // src/dashboard/mcp-oauth-routes.ts
14318
- import { Hono as Hono24 } from "hono";
14440
+ import { Hono as Hono25 } from "hono";
14319
14441
  var CONNECTIONS_PAGE = "/settings/connections";
14320
14442
  function callbackUri(c) {
14321
14443
  const proto = c.req.header("x-forwarded-proto") ?? "http";
@@ -14330,7 +14452,7 @@ function backToDashboard(c, to, params) {
14330
14452
  return c.redirect(`${target.pathname}${target.search}`);
14331
14453
  }
14332
14454
  function createMcpOAuthRouter() {
14333
- const app = new Hono24();
14455
+ const app = new Hono25();
14334
14456
  app.get("/oauth/:provider/start", async (c) => {
14335
14457
  const auth = c.get("auth");
14336
14458
  if (auth.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -14457,7 +14579,7 @@ function createMcpOAuthRouter() {
14457
14579
  }
14458
14580
 
14459
14581
  // src/dashboard/mcp-routes.ts
14460
- import { Hono as Hono25 } from "hono";
14582
+ import { Hono as Hono26 } from "hono";
14461
14583
 
14462
14584
  // src/mcp/transport.ts
14463
14585
  var KINDS = ["inherit", "npx", "local", "http", "sse"];
@@ -14551,7 +14673,7 @@ function fromServerCommand(command) {
14551
14673
 
14552
14674
  // src/dashboard/mcp-routes.ts
14553
14675
  var CUSTOM_PROVIDER = "custom";
14554
- function requireAdmin3(auth) {
14676
+ function requireAdmin4(auth) {
14555
14677
  return auth?.role === "admin" ? null : "admin access required";
14556
14678
  }
14557
14679
  function toResponse4(connection, channelIds, providerCommand = null) {
@@ -14587,9 +14709,9 @@ function bindingsByConnection() {
14587
14709
  return map;
14588
14710
  }
14589
14711
  function createMcpRouter() {
14590
- const app = new Hono25();
14712
+ const app = new Hono26();
14591
14713
  app.get("/providers", async (c) => {
14592
- const denied = requireAdmin3(c.get("auth"));
14714
+ const denied = requireAdmin4(c.get("auth"));
14593
14715
  if (denied) return c.json({ error: denied }, 403);
14594
14716
  const providers = await listMcpProviders();
14595
14717
  return c.json({
@@ -14607,7 +14729,7 @@ function createMcpRouter() {
14607
14729
  });
14608
14730
  app.get("/connections", async (c) => {
14609
14731
  const auth = c.get("auth");
14610
- const denied = requireAdmin3(auth);
14732
+ const denied = requireAdmin4(auth);
14611
14733
  if (denied) return c.json({ error: denied }, 403);
14612
14734
  const connections = await listMcpConnections(auth.workspaceId);
14613
14735
  const commands = new Map(
@@ -14622,7 +14744,7 @@ function createMcpRouter() {
14622
14744
  });
14623
14745
  app.post("/connections", async (c) => {
14624
14746
  const auth = c.get("auth");
14625
- const denied = requireAdmin3(auth);
14747
+ const denied = requireAdmin4(auth);
14626
14748
  if (denied) return c.json({ error: denied }, 403);
14627
14749
  const body = await c.req.json().catch(() => null);
14628
14750
  const label2 = typeof body?.label === "string" ? body.label.trim() : "";
@@ -14659,7 +14781,7 @@ function createMcpRouter() {
14659
14781
  });
14660
14782
  app.patch("/connections/:id", async (c) => {
14661
14783
  const auth = c.get("auth");
14662
- const denied = requireAdmin3(auth);
14784
+ const denied = requireAdmin4(auth);
14663
14785
  if (denied) return c.json({ error: denied }, 403);
14664
14786
  const body = await c.req.json().catch(() => null);
14665
14787
  if (!body) return c.json({ error: "a body is required" }, 400);
@@ -14690,7 +14812,7 @@ function createMcpRouter() {
14690
14812
  });
14691
14813
  app.put("/connections/:id/credential", async (c) => {
14692
14814
  const auth = c.get("auth");
14693
- const denied = requireAdmin3(auth);
14815
+ const denied = requireAdmin4(auth);
14694
14816
  if (denied) return c.json({ error: denied }, 403);
14695
14817
  const body = await c.req.json().catch(() => null);
14696
14818
  const raw = body?.apiKey;
@@ -14708,7 +14830,7 @@ function createMcpRouter() {
14708
14830
  });
14709
14831
  app.put("/connections/:id/channels", async (c) => {
14710
14832
  const auth = c.get("auth");
14711
- const denied = requireAdmin3(auth);
14833
+ const denied = requireAdmin4(auth);
14712
14834
  if (denied) return c.json({ error: denied }, 403);
14713
14835
  const id = c.req.param("id");
14714
14836
  const connection = await getMcpConnection(auth.workspaceId, id);
@@ -14735,7 +14857,7 @@ function createMcpRouter() {
14735
14857
  });
14736
14858
  app.delete("/connections/:id", async (c) => {
14737
14859
  const auth = c.get("auth");
14738
- const denied = requireAdmin3(auth);
14860
+ const denied = requireAdmin4(auth);
14739
14861
  if (denied) return c.json({ error: denied }, 403);
14740
14862
  const deleted = await deleteMcpConnection(auth.workspaceId, c.req.param("id"));
14741
14863
  if (!deleted) return c.json({ error: "connection not found" }, 404);
@@ -14846,7 +14968,7 @@ async function sessionUsageForThreads(workspace, threadTsList) {
14846
14968
  }
14847
14969
 
14848
14970
  // src/dashboard/usage-report.ts
14849
- import { existsSync as existsSync21 } from "fs";
14971
+ import { existsSync as existsSync22 } from "fs";
14850
14972
  function mergeByLabel(buckets) {
14851
14973
  const merged = /* @__PURE__ */ new Map();
14852
14974
  for (const bucket of buckets) {
@@ -14892,7 +15014,7 @@ function toReport(days, rows, labelChannel) {
14892
15014
  };
14893
15015
  }
14894
15016
  function fromLocalStore(days, labelChannel) {
14895
- if (!existsSync21(paths.DB_FILE)) return { ...EMPTY, days };
15017
+ if (!existsSync22(paths.DB_FILE)) return { ...EMPTY, days };
14896
15018
  const window = days === 0 ? void 0 : days;
14897
15019
  const store = new UsageStore(paths.DB_FILE);
14898
15020
  try {
@@ -14934,7 +15056,7 @@ async function buildChannelUsageReport(channelId, days, labelChannel) {
14934
15056
  const { totals, byDay } = await channelUsageForWorkspace(workspace, aliasIds, window);
14935
15057
  return { days, totals: totals.runs > 0 ? totals : null, byDay };
14936
15058
  } catch {
14937
- if (!existsSync21(paths.DB_FILE)) return { days, totals: null, byDay: [] };
15059
+ if (!existsSync22(paths.DB_FILE)) return { days, totals: null, byDay: [] };
14938
15060
  const store = new UsageStore(paths.DB_FILE);
14939
15061
  try {
14940
15062
  const targetLabel = labelChannel(channelId);
@@ -14956,7 +15078,7 @@ async function buildThreadUsage(threadTs) {
14956
15078
  if (totals.runs === 0) return { threadTs, totals: null, byModel: [] };
14957
15079
  return { threadTs, totals, byModel };
14958
15080
  } catch {
14959
- if (!existsSync21(paths.DB_FILE)) return { threadTs, totals: null, byModel: [] };
15081
+ if (!existsSync22(paths.DB_FILE)) return { threadTs, totals: null, byModel: [] };
14960
15082
  const store = new UsageStore(paths.DB_FILE);
14961
15083
  try {
14962
15084
  const totals = store.threadTotals(threadTs);
@@ -14969,7 +15091,7 @@ async function buildThreadUsage(threadTs) {
14969
15091
  }
14970
15092
 
14971
15093
  // src/dashboard/channel-members-routes.ts
14972
- import { Hono as Hono26 } from "hono";
15094
+ import { Hono as Hono27 } from "hono";
14973
15095
  async function isChannelAdmin(ctx, channelId) {
14974
15096
  if (ctx.role === "admin") return true;
14975
15097
  const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
@@ -14978,7 +15100,7 @@ async function isChannelAdmin(ctx, channelId) {
14978
15100
  return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
14979
15101
  }
14980
15102
  function createChannelMembersRouter() {
14981
- const router = new Hono26();
15103
+ const router = new Hono27();
14982
15104
  router.get("/:id/members", async (c) => {
14983
15105
  const ctx = await resolveAuthContext(c);
14984
15106
  if (!ctx) return c.json({ error: "workspace access required" }, 403);
@@ -15026,9 +15148,9 @@ function createChannelMembersRouter() {
15026
15148
  if (!await isChannelAdmin(ctx, channelId)) {
15027
15149
  return c.json({ error: "channel admin access required" }, 403);
15028
15150
  }
15029
- return c.json({ grants: await listChannelDocsAccess(channelId) });
15151
+ return c.json({ memberAccess: await getChannelDocsMemberAccess(channelId) });
15030
15152
  });
15031
- router.put("/:id/docs-access/:membershipId", async (c) => {
15153
+ router.put("/:id/docs-access", async (c) => {
15032
15154
  const ctx = await resolveAuthContext(c);
15033
15155
  if (!ctx) return c.json({ error: "workspace access required" }, 403);
15034
15156
  const channelId = c.req.param("id");
@@ -15036,32 +15158,31 @@ function createChannelMembersRouter() {
15036
15158
  return c.json({ error: "channel admin access required" }, 403);
15037
15159
  }
15038
15160
  const body = await c.req.json().catch(() => ({}));
15039
- if (!isDocsGrant(body.access)) {
15040
- 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);
15041
15163
  }
15042
- const grant = await upsertChannelDocsAccess({
15043
- channelId,
15044
- membershipId: c.req.param("membershipId"),
15045
- access: body.access
15046
- });
15047
- return c.json({ grant });
15048
- });
15049
- router.delete("/:id/docs-access/:membershipId", async (c) => {
15050
- const ctx = await resolveAuthContext(c);
15051
- if (!ctx) return c.json({ error: "workspace access required" }, 403);
15052
- const channelId = c.req.param("id");
15053
- if (!await isChannelAdmin(ctx, channelId)) {
15054
- 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);
15055
15166
  }
15056
- const removed = await removeChannelDocsAccess(channelId, c.req.param("membershipId"));
15057
- if (!removed) return c.json({ error: "no such docs grant" }, 404);
15058
- return c.body(null, 204);
15167
+ return c.json({ memberAccess: body.memberAccess });
15059
15168
  });
15060
15169
  return router;
15061
15170
  }
15062
15171
 
15063
15172
  // src/dashboard/memories-routes.ts
15064
- 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
+ }
15065
15186
  var requireChannelVisible = async (c, next) => {
15066
15187
  const channel = findChannel(c.req.param("id"));
15067
15188
  if (!channel || !canSeeChannel(channel, c.get("auth"))) {
@@ -15077,7 +15198,7 @@ var requireAgentMemory = async (c, next) => {
15077
15198
  await next();
15078
15199
  };
15079
15200
  function createMemoriesRouter() {
15080
- const router = new Hono27();
15201
+ const router = new Hono28();
15081
15202
  for (const path of [
15082
15203
  "/:id/memories",
15083
15204
  "/:id/memories/*",
@@ -15092,15 +15213,8 @@ function createMemoriesRouter() {
15092
15213
  router.patch("/:id/memories/:memoryId", async (c) => {
15093
15214
  const memoryId = c.req.param("memoryId");
15094
15215
  if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
15095
- const body = await c.req.json().catch(() => ({}));
15096
- const patch = {};
15097
- if (typeof body["description"] === "string") patch.description = body["description"];
15098
- if (typeof body["body"] === "string") patch.body = body["body"];
15099
- const type = body["type"];
15100
- if (type !== void 0) {
15101
- if (!isMemoryType(type)) return c.json({ error: "invalid type" }, 400);
15102
- patch.type = type;
15103
- }
15216
+ const patch = await readPatch(c);
15217
+ if (typeof patch === "string") return c.json({ error: patch }, 400);
15104
15218
  try {
15105
15219
  const memory = await updateChannelMemory(c.get("memoryChannelId"), memoryId, patch, "user");
15106
15220
  if (!memory) return c.json({ error: "memory not found" }, 404);
@@ -15116,9 +15230,28 @@ function createMemoriesRouter() {
15116
15230
  if (!deleted) return c.json({ error: "memory not found" }, 404);
15117
15231
  return c.json({ ok: true });
15118
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
+ });
15119
15249
  router.get("/:id/memory-events", async (c) => {
15120
- const threadId = c.req.query("thread") ?? "";
15121
- 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);
15122
15255
  return c.json({ events: await listThreadMemoryEvents(c.get("memoryChannelId"), threadId) });
15123
15256
  });
15124
15257
  router.post("/:id/memory-events/:eventId/undo", async (c) => {
@@ -15137,9 +15270,68 @@ function createMemoriesRouter() {
15137
15270
  });
15138
15271
  return router;
15139
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
+ }
15140
15332
 
15141
15333
  // src/dashboard/onboarding-routes.ts
15142
- import { Hono as Hono28 } from "hono";
15334
+ import { Hono as Hono29 } from "hono";
15143
15335
 
15144
15336
  // src/dashboard/unique-channel-name.ts
15145
15337
  function uniqueChannelName(raw, taken) {
@@ -15153,7 +15345,7 @@ function uniqueChannelName(raw, taken) {
15153
15345
  // src/dashboard/workspace-greeting.ts
15154
15346
  var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
15155
15347
  var GREETING_MODEL = "openai/gpt-oss-20b";
15156
- var TIMEOUT_MS = 6e3;
15348
+ var TIMEOUT_MS2 = 6e3;
15157
15349
  var MAX_LISTED_CHANNELS = 8;
15158
15350
  var GUIDES = [
15159
15351
  "Welcome to Flyspace",
@@ -15203,7 +15395,7 @@ async function groqGreeting(input2, apiKey) {
15203
15395
  ].join("\n");
15204
15396
  const res = await fetch(GROQ_API_URL8, {
15205
15397
  method: "POST",
15206
- signal: AbortSignal.timeout(TIMEOUT_MS),
15398
+ signal: AbortSignal.timeout(TIMEOUT_MS2),
15207
15399
  headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
15208
15400
  body: JSON.stringify({
15209
15401
  model: GREETING_MODEL,
@@ -15240,7 +15432,7 @@ async function buildGreeting(input2) {
15240
15432
  // src/dashboard/onboarding-routes.ts
15241
15433
  var MAX_FOLDERS = 20;
15242
15434
  function createOnboardingRouter(agent) {
15243
- const app = new Hono28();
15435
+ const app = new Hono29();
15244
15436
  app.get("/scan", async (c) => {
15245
15437
  const ctx = await resolveAuthContext(c);
15246
15438
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -15406,13 +15598,13 @@ function firstSentence(body) {
15406
15598
  function threadLabel(row) {
15407
15599
  return row.title?.trim() || firstSentence(row.thread_replies?.[0]?.body);
15408
15600
  }
15409
- async function resolveThreadLinks(sessions) {
15601
+ async function resolveThreadLinks(sessions2) {
15410
15602
  const chatIds = [
15411
- ...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))
15412
15604
  ];
15413
15605
  const externalKeys = [
15414
15606
  ...new Set(
15415
- 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}`)
15416
15608
  )
15417
15609
  ];
15418
15610
  const chat = /* @__PURE__ */ new Map();
@@ -15445,8 +15637,8 @@ function registerDataRoutes(api, agent = {}) {
15445
15637
  api.get("/host/status", async (c) => {
15446
15638
  const online = agent.isHostOnline?.(getProfile()) ?? false;
15447
15639
  if (!online) {
15448
- const failure2 = agent.getHostOfflineReason?.() ?? null;
15449
- 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 });
15450
15642
  }
15451
15643
  const auth = c.get("auth");
15452
15644
  const paused = await getAgentPaused(auth.workspaceId);
@@ -15497,6 +15689,7 @@ function registerDataRoutes(api, agent = {}) {
15497
15689
  api.route("/onboarding", createOnboardingRouter(agent));
15498
15690
  api.route("/channels", createChannelMembersRouter());
15499
15691
  api.route("/channels", createMemoriesRouter());
15692
+ api.route("/memories", createScopedMemoriesRouter());
15500
15693
  api.route("/docs", createDocShareListRouter());
15501
15694
  api.get("/users", (c) => c.json({ users: listActiveUsers() }));
15502
15695
  api.route("/categories", createCategoryWriteRouter());
@@ -15504,17 +15697,17 @@ function registerDataRoutes(api, agent = {}) {
15504
15697
  const auto = loadEffectiveAutomations();
15505
15698
  const runs = /* @__PURE__ */ new Map();
15506
15699
  try {
15507
- for (const [name, run3] of await lastRunsForWorkspace(getProfile())) {
15508
- runs.set(name, { lastRun: run3.lastRunAt.toISOString(), lastStatus: run3.lastStatus });
15700
+ for (const [name, run4] of await lastRunsForWorkspace(getProfile())) {
15701
+ runs.set(name, { lastRun: run4.lastRunAt.toISOString(), lastStatus: run4.lastStatus });
15509
15702
  }
15510
15703
  } catch {
15511
- if (existsSync22(paths.DB_FILE)) {
15704
+ if (existsSync23(paths.DB_FILE)) {
15512
15705
  const runStore = new AutomationRunStore(paths.DB_FILE);
15513
15706
  try {
15514
- for (const run3 of runStore.listLastRuns()) {
15515
- runs.set(run3.name, {
15516
- lastRun: run3.lastRunAt.toISOString(),
15517
- lastStatus: run3.lastStatus
15707
+ for (const run4 of runStore.listLastRuns()) {
15708
+ runs.set(run4.name, {
15709
+ lastRun: run4.lastRunAt.toISOString(),
15710
+ lastStatus: run4.lastStatus
15518
15711
  });
15519
15712
  }
15520
15713
  } finally {
@@ -15570,7 +15763,7 @@ function registerDataRoutes(api, agent = {}) {
15570
15763
  const runs = (await recentRunsForWorkspace(getProfile(), 200)).map(toResponse5);
15571
15764
  return c.json({ runs });
15572
15765
  } catch {
15573
- if (!existsSync22(paths.DB_FILE)) return c.json({ runs: [] });
15766
+ if (!existsSync23(paths.DB_FILE)) return c.json({ runs: [] });
15574
15767
  const runStore = new AutomationRunStore(paths.DB_FILE);
15575
15768
  try {
15576
15769
  const runs = runStore.listRuns(200).map(toResponse5);
@@ -15692,7 +15885,7 @@ function registerDataRoutes(api, agent = {}) {
15692
15885
  } catch {
15693
15886
  latest = /* @__PURE__ */ new Map();
15694
15887
  totals = /* @__PURE__ */ new Map();
15695
- if (existsSync22(paths.DB_FILE)) {
15888
+ if (existsSync23(paths.DB_FILE)) {
15696
15889
  const usageStore = new UsageStore(paths.DB_FILE);
15697
15890
  try {
15698
15891
  latest = usageStore.latestByThread(threadTsList);
@@ -15703,25 +15896,25 @@ function registerDataRoutes(api, agent = {}) {
15703
15896
  }
15704
15897
  }
15705
15898
  const threadLinks = await resolveThreadLinks(raw);
15706
- const sessions = raw.map((s) => {
15707
- const run3 = latest.get(s.threadTs);
15708
- const turns = run3?.numTurns ?? 1;
15709
- const total = run3 ? run3.inputTokens + run3.cacheReadTokens + run3.cacheCreationTokens : void 0;
15899
+ const sessions2 = raw.map((s) => {
15900
+ const run4 = latest.get(s.threadTs);
15901
+ const turns = run4?.numTurns ?? 1;
15902
+ const total = run4 ? run4.inputTokens + run4.cacheReadTokens + run4.cacheCreationTokens : void 0;
15710
15903
  const link = s.channelId.startsWith("chat:") ? threadLinks.chat.get(s.threadTs) : threadLinks.external.get(`${s.channelId}:${s.threadTs}`);
15711
15904
  return {
15712
15905
  ...s,
15713
15906
  channelLabel: channelLabel(s.channelId),
15714
- model: run3?.model,
15715
- numTurns: run3?.numTurns,
15907
+ model: run4?.model,
15908
+ numTurns: run4?.numTurns,
15716
15909
  contextTokens: total !== void 0 ? Math.round(total / (turns > 0 ? turns : 1)) : void 0,
15717
- rolledOver: run3?.label === "rollover",
15910
+ rolledOver: run4?.label === "rollover",
15718
15911
  threadTitle: link?.title ?? void 0,
15719
15912
  chatChannelId: link?.chatChannelId,
15720
15913
  chatThreadId: link?.chatThreadId,
15721
15914
  costUsd: totals.get(s.threadTs)?.costUsd
15722
15915
  };
15723
15916
  });
15724
- return c.json({ sessions });
15917
+ return c.json({ sessions: sessions2 });
15725
15918
  });
15726
15919
  const LOGO_SIZE = 256;
15727
15920
  const MAX_LOGO_BYTES = 2 * 1024 * 1024;
@@ -15747,18 +15940,18 @@ function registerDataRoutes(api, agent = {}) {
15747
15940
  // src/dashboard/device-auth-routes.ts
15748
15941
  import { verify } from "crypto";
15749
15942
  import { getConnInfo } from "@hono/node-server/conninfo";
15750
- import { Hono as Hono30 } from "hono";
15943
+ import { Hono as Hono31 } from "hono";
15751
15944
 
15752
15945
  // src/dashboard/agent-bridge.ts
15753
- 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";
15754
15947
 
15755
15948
  // src/dashboard/docs-relay.ts
15756
- import { Hono as Hono29 } from "hono";
15949
+ import { Hono as Hono30 } from "hono";
15757
15950
  import { streamSSE as streamSSE2 } from "hono/streaming";
15758
15951
  var FORWARDED_HEADERS = ["content-type", "host", "accept-language"];
15759
15952
  var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
15760
15953
  async function handleDocsRequest(req) {
15761
- const app = new Hono29();
15954
+ const app = new Hono30();
15762
15955
  app.use("*", async (c, next) => {
15763
15956
  c.set("auth", req.auth);
15764
15957
  await next();
@@ -15817,24 +16010,24 @@ function relay2(agent) {
15817
16010
  }
15818
16011
 
15819
16012
  // src/dashboard/agent-bridge.ts
15820
- import { existsSync as existsSync24, readFileSync as readFileSync17 } from "fs";
16013
+ import { existsSync as existsSync25, readFileSync as readFileSync19 } from "fs";
15821
16014
  import dotenv from "dotenv";
15822
16015
  import { WebSocket, WebSocketServer } from "ws";
15823
16016
 
15824
16017
  // src/dashboard/host-identity.ts
15825
16018
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
15826
- import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
15827
- import { dirname as dirname7, join as join22 } from "path";
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";
15828
16021
  function hostPrivateKeyPath() {
15829
- return process.env["FLYSPACE_HOST_KEY_PATH"] || join22(paths.BASE_CONFIG_DIR, "host.key");
16022
+ return process.env["FLYSPACE_HOST_KEY_PATH"] || join24(paths.BASE_CONFIG_DIR, "host.key");
15830
16023
  }
15831
16024
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15832
- if (!existsSync23(path)) {
16025
+ if (!existsSync24(path)) {
15833
16026
  throw new Error(
15834
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.`
15835
16028
  );
15836
16029
  }
15837
- return createPrivateKey2(readFileSync15(path, "utf8"));
16030
+ return createPrivateKey2(readFileSync17(path, "utf8"));
15838
16031
  }
15839
16032
  function loadHostPublicKey() {
15840
16033
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -16079,7 +16272,7 @@ async function userEmail(userId) {
16079
16272
 
16080
16273
  // src/dashboard/doc-share-publisher.ts
16081
16274
  import { randomBytes as randomBytes7 } from "crypto";
16082
- import { readFileSync as readFileSync16, statSync as statSync9 } from "fs";
16275
+ import { readFileSync as readFileSync18, statSync as statSync9 } from "fs";
16083
16276
  import { extname as extname2, posix } from "path";
16084
16277
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
16085
16278
 
@@ -16266,7 +16459,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16266
16459
  }
16267
16460
  if (!stat.isFile()) return { status: "not-found" };
16268
16461
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
16269
- const source = readFileSync16(abs, "utf8");
16462
+ const source = readFileSync18(abs, "utf8");
16270
16463
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
16271
16464
  if (kind === "excalidraw") {
16272
16465
  try {
@@ -16346,7 +16539,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16346
16539
  let text = null;
16347
16540
  try {
16348
16541
  const size = statSync9(docAbs).size;
16349
- if (size <= budget) text = readFileSync16(docAbs, "utf8");
16542
+ if (size <= budget) text = readFileSync18(docAbs, "utf8");
16350
16543
  } catch {
16351
16544
  continue;
16352
16545
  }
@@ -16363,7 +16556,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16363
16556
  let body;
16364
16557
  try {
16365
16558
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
16366
- body = readFileSync16(resolved2.abs);
16559
+ body = readFileSync18(resolved2.abs);
16367
16560
  } catch {
16368
16561
  continue;
16369
16562
  }
@@ -16405,7 +16598,7 @@ async function publishDocShare(input2) {
16405
16598
  ...HTML_FEATURES,
16406
16599
  brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
16407
16600
  } : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
16408
- const snapshot3 = {
16601
+ const snapshot2 = {
16409
16602
  ...built,
16410
16603
  sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
16411
16604
  };
@@ -16415,8 +16608,8 @@ async function publishDocShare(input2) {
16415
16608
  const renamed = await setDocShareSlug(existing.id, slug);
16416
16609
  if (!renamed) return { status: "not-found" };
16417
16610
  }
16418
- const share2 = await updateDocShareSnapshot(existing.id, snapshot3, features);
16419
- const stale = existing.assetKeys.filter((key) => !snapshot3.assetKeys.includes(key));
16611
+ const share2 = await updateDocShareSnapshot(existing.id, snapshot2, features);
16612
+ const stale = existing.assetKeys.filter((key) => !snapshot2.assetKeys.includes(key));
16420
16613
  await deleteShareAssets(stale);
16421
16614
  if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
16422
16615
  void queueShareAudio(share2);
@@ -16430,7 +16623,7 @@ async function publishDocShare(input2) {
16430
16623
  channelId: input2.channelId,
16431
16624
  sourcePath: input2.sourcePath,
16432
16625
  createdBy: input2.createdBy,
16433
- ...snapshot3,
16626
+ ...snapshot2,
16434
16627
  ...features
16435
16628
  });
16436
16629
  void queueShareAudio(share);
@@ -16456,10 +16649,14 @@ var METHODS = {
16456
16649
  threads: ["GET", "HEAD", "POST", "PATCH"],
16457
16650
  thread_replies: ["GET", "HEAD", "POST", "PATCH"],
16458
16651
  memberships: ["GET", "HEAD"],
16652
+ feature_flags: ["GET", "HEAD"],
16459
16653
  channel_members: ["GET", "HEAD"],
16460
- channel_docs_access: ["GET", "HEAD"]
16654
+ memories: ALL,
16655
+ memory_events: ["GET", "HEAD", "POST", "PATCH"]
16461
16656
  };
16462
- var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members", "channel_docs_access"]);
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"];
16463
16660
  var FORWARDED_HEADERS2 = ["accept", "content-type", "prefer", "range"];
16464
16661
  var deny = (message, status = 403) => ({
16465
16662
  status,
@@ -16566,7 +16763,30 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
16566
16763
  }
16567
16764
  }
16568
16765
  params.append(slugColumn, inList(scope));
16569
- } 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") {
16570
16790
  const workspaceIds = await ownedWorkspaceIds(forward, workspaces);
16571
16791
  if (!workspaceIds) return deny("Could not resolve the device workspaces.", 502);
16572
16792
  params.append("workspace_id", inList(workspaceIds));
@@ -16648,7 +16868,7 @@ function dataSocketUrl() {
16648
16868
  return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
16649
16869
  }
16650
16870
  function listLocalSessions() {
16651
- if (!existsSync24(paths.DB_FILE)) return [];
16871
+ if (!existsSync25(paths.DB_FILE)) return [];
16652
16872
  const store = new SessionStore(paths.DB_FILE);
16653
16873
  try {
16654
16874
  return store.listSessions();
@@ -16664,6 +16884,7 @@ function localAgentCallbacks(chatAgent) {
16664
16884
  cancelAgentRun: (threadId) => chatAgent.cancel(threadId),
16665
16885
  regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
16666
16886
  listAgentRuns: () => chatAgent.runningThreadIds(),
16887
+ completeMemoryExtraction: (req) => chatAgent.completeMemoryExtraction(req),
16667
16888
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
16668
16889
  listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
16669
16890
  getAgentIdentities: () => resolveAgentIdentities(),
@@ -16674,6 +16895,7 @@ function localAgentCallbacks(chatAgent) {
16674
16895
  saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
16675
16896
  writeSkill: (ref, write) => writeSkillAndSync(ref, write),
16676
16897
  outputStylesRequest: (req) => handleOutputStylesRequest(req),
16898
+ providerLoginRequest: (req) => handleProviderLoginRequest(req),
16677
16899
  docsRequest: (req) => handleDocsRequest(req),
16678
16900
  listSessions: () => listLocalSessions(),
16679
16901
  scanOnboarding: () => scanOnboarding(),
@@ -16683,14 +16905,14 @@ function localAgentCallbacks(chatAgent) {
16683
16905
  }
16684
16906
  var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
16685
16907
  function buildHostConfigSnapshot() {
16686
- const snapshot3 = {};
16908
+ const snapshot2 = {};
16687
16909
  for (const profile of listProfiles()) {
16688
- snapshot3[profile] = runWithProfile(profile, () => ({
16689
- env: existsSync24(paths.CONFIG_FILE) ? dotenv.parse(readFileSync17(paths.CONFIG_FILE, "utf-8")) : {},
16910
+ snapshot2[profile] = runWithProfile(profile, () => ({
16911
+ env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync19(paths.CONFIG_FILE, "utf-8")) : {},
16690
16912
  agentModel: loadDashboardSettings().agent
16691
16913
  }));
16692
16914
  }
16693
- return snapshot3;
16915
+ return snapshot2;
16694
16916
  }
16695
16917
  function startLocalProfileConfigHydration() {
16696
16918
  setProfileConfigCache(buildHostConfigSnapshot());
@@ -16699,6 +16921,7 @@ function startLocalProfileConfigHydration() {
16699
16921
  }
16700
16922
  var CALL_TIMEOUT_MS = 15e3;
16701
16923
  var DOCS_CALL_TIMEOUT_MS = 12e4;
16924
+ var MEMORY_EXTRACTION_TIMEOUT_MS = 165e3;
16702
16925
  var AUTH_TIMEOUT_MS = 5e3;
16703
16926
  var HEARTBEAT_INTERVAL_MS = 15e3;
16704
16927
  var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
@@ -16898,7 +17121,7 @@ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
16898
17121
  return Promise.resolve(null);
16899
17122
  }
16900
17123
  if (method === "docsRequest") console.log(`[host-socket] docsRequest for "${profile}" -> device ${hosts.get(ws)?.deviceId ?? "legacy"}`);
16901
- const id = randomUUID4();
17124
+ const id = randomUUID5();
16902
17125
  return new Promise((resolve7) => {
16903
17126
  const timer2 = setTimeout(() => {
16904
17127
  pending2.delete(id);
@@ -16926,6 +17149,9 @@ function remoteAgentCallbacks() {
16926
17149
  cancelAgentRun: async (threadId) => await callHost("cancelAgentRun", [threadId]) ?? false,
16927
17150
  regenerateAgentRun: async (threadId) => await callHost("regenerateAgentRun", [threadId]) ?? false,
16928
17151
  listAgentRuns: async () => await callHost("listAgentRuns", []) ?? [],
17152
+ // `null` (no Host, or no answer in time) passes through: the extractor
17153
+ // reads it as a failed call and retries the thread next sweep.
17154
+ completeMemoryExtraction: (req) => callHost("completeMemoryExtraction", [req], MEMORY_EXTRACTION_TIMEOUT_MS),
16929
17155
  // Deliberately passes `null` through rather than defaulting to `[]`: with
16930
17156
  // no Host connected, claiming every folder is fine would be a guess.
16931
17157
  listMissingCwds: () => callHost("listMissingCwds", []),
@@ -16959,6 +17185,8 @@ function remoteAgentCallbacks() {
16959
17185
  writeSkill: (ref, write) => callHost("writeSkill", [ref, write]),
16960
17186
  // Same posture as writeSkill: `null` becomes a 503.
16961
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),
16962
17190
  // Same posture again. Longer timeout: a first translation of a doc runs a
16963
17191
  // model call on the Host before it answers.
16964
17192
  docsRequest: (req) => callHost("docsRequest", [req], DOCS_CALL_TIMEOUT_MS),
@@ -17223,7 +17451,7 @@ async function boundWorkspaceIds(publicKey, userId) {
17223
17451
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
17224
17452
  }
17225
17453
  function createDeviceAuthRouter() {
17226
- const app = new Hono30();
17454
+ const app = new Hono31();
17227
17455
  app.post("/code", async (c) => {
17228
17456
  const body = await readBody(c);
17229
17457
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -17387,7 +17615,7 @@ function createDeviceAuthRouter() {
17387
17615
 
17388
17616
  // src/dashboard/api.ts
17389
17617
  function createApiRouter(agent = {}, mode = "all") {
17390
- const api = new Hono31();
17618
+ const api = new Hono32();
17391
17619
  api.use("*", async (c, next) => {
17392
17620
  const known = await listKnownWorkspaces();
17393
17621
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -17411,6 +17639,7 @@ function createApiRouter(agent = {}, mode = "all") {
17411
17639
  const resolved2 = requested && known.includes(requested) ? requested : profileFromHost(trustedHost(c), known);
17412
17640
  if (!resolved2) {
17413
17641
  return c.json({
17642
+ unknownWorkspace: isUnknownWorkspaceHost(trustedHost(c), known),
17414
17643
  theme: BRANDING_DEFAULTS.theme,
17415
17644
  font: BRANDING_DEFAULTS.font,
17416
17645
  displayName: "Flyspace",
@@ -17442,6 +17671,7 @@ function createApiRouter(agent = {}, mode = "all") {
17442
17671
  });
17443
17672
  registerMirroredSkillRoutes(api, agent);
17444
17673
  registerRelayedOutputStylesRoutes(api, agent);
17674
+ registerRelayedProviderLoginRoutes(api, agent);
17445
17675
  api.get("/channels/:id/docs/flat", requireDocsAccess, async (c) => {
17446
17676
  const { cwd } = resolveChannelConfig(c.req.param("id"));
17447
17677
  const query = c.req.query("q") ?? "";
@@ -17499,7 +17729,7 @@ function createApiRouter(agent = {}, mode = "all") {
17499
17729
  }
17500
17730
 
17501
17731
  // src/dashboard/app-icon-routes.ts
17502
- import { Hono as Hono32 } from "hono";
17732
+ import { Hono as Hono33 } from "hono";
17503
17733
  var NEUTRAL_MANIFEST = {
17504
17734
  id: "/",
17505
17735
  name: "Flyspace",
@@ -17537,7 +17767,7 @@ async function withResolvedProfile2(c, next) {
17537
17767
  return profile ? runWithProfile(profile, () => next()) : next();
17538
17768
  }
17539
17769
  function createAppIconRouter() {
17540
- const app = new Hono32();
17770
+ const app = new Hono33();
17541
17771
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
17542
17772
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
17543
17773
  const profile = getProfile();
@@ -17577,7 +17807,7 @@ function pngResponse(c, png) {
17577
17807
  }
17578
17808
 
17579
17809
  // src/dashboard/stripe-routes.ts
17580
- import { Hono as Hono33 } from "hono";
17810
+ import { Hono as Hono34 } from "hono";
17581
17811
  function planStatusFor(status) {
17582
17812
  const known = ["trialing", "active", "paused", "canceled"];
17583
17813
  return known.includes(status) ? status : null;
@@ -17620,7 +17850,7 @@ async function handleCardAdded(session) {
17620
17850
  }
17621
17851
  }
17622
17852
  function createStripeWebhookRouter() {
17623
- const stripeRouter = new Hono33();
17853
+ const stripeRouter = new Hono34();
17624
17854
  stripeRouter.post("/webhook", async (c) => {
17625
17855
  const secret = process.env["STRIPE_WEBHOOK_SECRET"];
17626
17856
  const signature = c.req.header("stripe-signature");
@@ -17731,7 +17961,7 @@ function startHostStatusBroadcast() {
17731
17961
 
17732
17962
  // src/dashboard/server.ts
17733
17963
  var DEFAULT_DASHBOARD_PORT = 4747;
17734
- var WEB_DIST = join23(paths.PACKAGE_ROOT, "web", "dist");
17964
+ var WEB_DIST = join25(paths.PACKAGE_ROOT, "web", "dist");
17735
17965
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
17736
17966
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
17737
17967
  for (let port = start; port < start + attempts; port++) {
@@ -17780,7 +18010,8 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17780
18010
  const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
17781
18011
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
17782
18012
  };
17783
- const app = new Hono34();
18013
+ const app = new Hono35();
18014
+ app.onError(reportingErrorHandler);
17784
18015
  app.use("*", async (c, next) => {
17785
18016
  await next();
17786
18017
  if (c.req.path.startsWith("/assets/")) {
@@ -17819,14 +18050,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17819
18050
  } else {
17820
18051
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
17821
18052
  app.route("/", createAppIconRouter());
17822
- if (existsSync25(WEB_DIST) || existsSync25(join23(paths.PACKAGE_ROOT, "web", "package.json"))) {
18053
+ if (existsSync26(WEB_DIST) || existsSync26(join25(paths.PACKAGE_ROOT, "web", "package.json"))) {
17823
18054
  app.use("/*", serveStatic({ root: WEB_DIST }));
17824
18055
  }
17825
18056
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
17826
- const spaFallback = serveStatic({ path: join23(WEB_DIST, "index.html") });
18057
+ const spaFallback = serveStatic({ path: join25(WEB_DIST, "index.html") });
17827
18058
  app.get("*", async (c, next) => {
17828
- if (!existsSync25(join23(WEB_DIST, "index.html"))) {
17829
- const isCheckout = existsSync25(join23(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"));
17830
18061
  return c.text(
17831
18062
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
17832
18063
  Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
@@ -18264,7 +18495,7 @@ async function threadReplies(client2, nameOrId, ts2) {
18264
18495
  async function postMessage(client2, args) {
18265
18496
  const channelArg = getArg2(args, "--channel");
18266
18497
  const textFilePath = getArg2(args, "--text-file");
18267
- const text = textFilePath ? readFileSync18(textFilePath, "utf8") : getArg2(args, "--text");
18498
+ const text = textFilePath ? readFileSync20(textFilePath, "utf8") : getArg2(args, "--text");
18268
18499
  const threadTs = getArg2(args, "--thread");
18269
18500
  const markdown = args.includes("--markdown");
18270
18501
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -18362,7 +18593,7 @@ function defaultClaudeConfigDir() {
18362
18593
  return process.env.CLAUDE_CONFIG_DIR || resolve6(homedir9(), ".claude");
18363
18594
  }
18364
18595
  function hasClaudeCredentials(configDir2) {
18365
- if (existsSync26(resolve6(configDir2, ".credentials.json"))) {
18596
+ if (existsSync27(resolve6(configDir2, ".credentials.json"))) {
18366
18597
  return true;
18367
18598
  }
18368
18599
  if (process.platform === "darwin") {
@@ -18419,7 +18650,7 @@ async function ensureClaudeCodeReady() {
18419
18650
  }
18420
18651
  function getManifestYaml(botName) {
18421
18652
  const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
18422
- const raw = readFileSync18(manifestPath, "utf-8");
18653
+ const raw = readFileSync20(manifestPath, "utf-8");
18423
18654
  return raw.replace(/name: Custie/g, `name: ${botName}`);
18424
18655
  }
18425
18656
  async function stepPersonalise() {
@@ -18632,7 +18863,7 @@ async function stepBrowserTokens(botName) {
18632
18863
  })
18633
18864
  ));
18634
18865
  const DEBUG = process.argv.includes("--debug");
18635
- const USER_DATA_DIR = resolve6(tmpdir4(), "custie-playwright-profile");
18866
+ const USER_DATA_DIR = resolve6(tmpdir5(), "custie-playwright-profile");
18636
18867
  let context;
18637
18868
  try {
18638
18869
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -18726,7 +18957,7 @@ async function stepBrowserTokens(botName) {
18726
18957
  let settled = false;
18727
18958
  let interval;
18728
18959
  let timeout;
18729
- const finish = (action) => {
18960
+ const finish2 = (action) => {
18730
18961
  if (settled) return;
18731
18962
  settled = true;
18732
18963
  clearInterval(interval);
@@ -18735,7 +18966,7 @@ async function stepBrowserTokens(botName) {
18735
18966
  };
18736
18967
  const checkUrl = (p) => {
18737
18968
  if (p.url().includes("install-on-team?success=1")) {
18738
- finish(() => resolve7(p));
18969
+ finish2(() => resolve7(p));
18739
18970
  }
18740
18971
  };
18741
18972
  context.on("page", (newPage) => {
@@ -18746,7 +18977,7 @@ async function stepBrowserTokens(botName) {
18746
18977
  for (const p of context.pages()) checkUrl(p);
18747
18978
  }, 1e3);
18748
18979
  timeout = setTimeout(() => {
18749
- 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.")));
18750
18981
  }, 12e4);
18751
18982
  });
18752
18983
  success4("App installed! Extracting Bot Token...");
@@ -18836,7 +19067,7 @@ function writeConfigFile(config) {
18836
19067
  `OWNER_USER_ID=${config.ownerUserId}`,
18837
19068
  ``
18838
19069
  ].join("\n");
18839
- writeFileSync11(paths.CONFIG_FILE, envContent);
19070
+ writeFileSync12(paths.CONFIG_FILE, envContent);
18840
19071
  success4(`Config written to ${paths.CONFIG_FILE}`);
18841
19072
  }
18842
19073
  function printNextSteps() {
@@ -18848,11 +19079,11 @@ function printNextSteps() {
18848
19079
  console.log(" Run \x1B[1mflyspace service install\x1B[0m\n");
18849
19080
  }
18850
19081
  function copyDefaultPrompt(botName) {
18851
- if (!existsSync26(paths.PROMPT_FILE)) {
19082
+ if (!existsSync27(paths.PROMPT_FILE)) {
18852
19083
  const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
18853
- if (existsSync26(defaultPrompt)) {
18854
- const content = readFileSync18(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18855
- 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);
18856
19087
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
18857
19088
  }
18858
19089
  }
@@ -18864,7 +19095,7 @@ async function runSlackSetup(args) {
18864
19095
  const prereq = await ensureClaudeCodeReady();
18865
19096
  if (!prereq) return;
18866
19097
  const { claudeConfigDir } = prereq;
18867
- if (existsSync26(paths.CONFIG_FILE)) {
19098
+ if (existsSync27(paths.CONFIG_FILE)) {
18868
19099
  const reconfigure = await confirm4({
18869
19100
  message: `Config already exists at ${paths.CONFIG_FILE}. Reconfigure?`,
18870
19101
  default: false
@@ -19146,6 +19377,7 @@ async function runPost(args) {
19146
19377
  }
19147
19378
  async function runChatCmd(args) {
19148
19379
  loadEnvFiles();
19380
+ useHostDbWithoutCredentials();
19149
19381
  const subcommand = args[0];
19150
19382
  const rest = args.slice(1);
19151
19383
  switch (subcommand) {
@@ -19174,21 +19406,21 @@ async function runChatCmd(args) {
19174
19406
  }
19175
19407
 
19176
19408
  // src/commands/memory.ts
19177
- import { readFileSync as readFileSync20 } from "fs";
19409
+ import { readFileSync as readFileSync22 } from "fs";
19178
19410
 
19179
19411
  // src/store/claude-memory-import.ts
19180
- import { existsSync as existsSync27, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "fs";
19412
+ import { existsSync as existsSync28, readdirSync as readdirSync5, readFileSync as readFileSync21 } from "fs";
19181
19413
  import { homedir as homedir10 } from "os";
19182
- import { basename as basename8, join as join24 } from "path";
19414
+ import { basename as basename8, join as join26 } from "path";
19183
19415
  import dotenv2 from "dotenv";
19184
19416
  function claudeMemoryDir(cwd, configDir2) {
19185
- return join24(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
19417
+ return join26(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
19186
19418
  }
19187
19419
  function profileClaudeConfigDirFor(profile) {
19188
- const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join24(paths.PROFILES_DIR, profile);
19189
- const file = join24(dir, "config.env");
19190
- const env = existsSync27(file) ? dotenv2.parse(readFileSync19(file)) : {};
19191
- return env["CLAUDE_CONFIG_DIR"]?.trim() || join24(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");
19192
19424
  }
19193
19425
  var TYPE_MAP = {
19194
19426
  user: "preference",
@@ -19196,7 +19428,7 @@ var TYPE_MAP = {
19196
19428
  project: "fact",
19197
19429
  reference: "reference"
19198
19430
  };
19199
- var DESCRIPTION_MAX2 = 200;
19431
+ var DESCRIPTION_MAX = 200;
19200
19432
  function unquote2(value) {
19201
19433
  const v = value.trim();
19202
19434
  return /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
@@ -19214,8 +19446,8 @@ function parseClaudeMemory(fileName, text, channelId) {
19214
19446
  }
19215
19447
  const name = slugify(fields["name"] || basename8(fileName, ".md"));
19216
19448
  let description = (fields["description"] ?? "").replaceAll(/\s+/g, " ").trim();
19217
- if (description.length > DESCRIPTION_MAX2) {
19218
- description = `${description.slice(0, DESCRIPTION_MAX2 - 3).trimEnd()}...`;
19449
+ if (description.length > DESCRIPTION_MAX) {
19450
+ description = `${description.slice(0, DESCRIPTION_MAX - 3).trimEnd()}...`;
19219
19451
  }
19220
19452
  const input2 = {
19221
19453
  channelId,
@@ -19233,11 +19465,11 @@ function parseClaudeMemory(fileName, text, channelId) {
19233
19465
  return input2;
19234
19466
  }
19235
19467
  async function importClaudeMemories(dir, channelId, opts = {}) {
19236
- 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}`);
19237
19469
  const files = readdirSync5(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
19238
19470
  const results = [];
19239
19471
  for (const file of files) {
19240
- const parsed = parseClaudeMemory(file, readFileSync19(join24(dir, file), "utf-8"), channelId);
19472
+ const parsed = parseClaudeMemory(file, readFileSync21(join26(dir, file), "utf-8"), channelId);
19241
19473
  if ("skip" in parsed) {
19242
19474
  results.push({ file, outcome: "skipped", reason: parsed.skip });
19243
19475
  continue;
@@ -19272,6 +19504,12 @@ var USAGE5 = `
19272
19504
  Defaults to the channel profile's CLAUDE_CONFIG_DIR
19273
19505
  (or ~/.claude)/projects/<cwd slug>/memory
19274
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
+
19275
19513
  Types: ${MEMORY_TYPES.join(", ")}
19276
19514
  `;
19277
19515
  function getArg4(args, flag) {
@@ -19291,6 +19529,7 @@ function indexLine(m) {
19291
19529
  }
19292
19530
  async function runMemoryCmd(args) {
19293
19531
  loadEnvFiles();
19532
+ useHostDbWithoutCredentials();
19294
19533
  const subcommand = args[0];
19295
19534
  const rest = args.slice(1);
19296
19535
  if (!subcommand || subcommand === "-h" || subcommand === "--help") {
@@ -19301,17 +19540,23 @@ async function runMemoryCmd(args) {
19301
19540
  if (!channelArg) throw new Error("--channel <name-or-id> is required");
19302
19541
  const channel = await resolveChannel(channelArg, getArg4(rest, "--profile-name"));
19303
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
+ }
19304
19548
  switch (subcommand) {
19305
19549
  case "list": {
19306
- const memories = await listChannelMemories(channel.id);
19550
+ const memories = await listMemories(target);
19307
19551
  console.log(memories.length ? memories.map(indexLine).join("\n") : "No memories yet.");
19308
19552
  break;
19309
19553
  }
19310
19554
  case "get": {
19311
19555
  const name = words[0];
19312
19556
  if (!name) throw new Error("Usage: flyspace memory get --channel <c> <name>");
19313
- const memory = await getChannelMemory(channel.id, name);
19314
- 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);
19315
19560
  console.log(`# ${memory.name} (${memory.type})
19316
19561
 
19317
19562
  ${memory.description}
@@ -19321,7 +19566,7 @@ ${memory.body}`);
19321
19566
  }
19322
19567
  case "search": {
19323
19568
  if (!words.length) throw new Error("Usage: flyspace memory search --channel <c> <words...>");
19324
- const hits = await searchChannelMemories(channel.id, words.join(" "));
19569
+ const hits = await searchMemories(target, words.join(" "));
19325
19570
  console.log(hits.length ? hits.map(indexLine).join("\n") : "No matching memories.");
19326
19571
  break;
19327
19572
  }
@@ -19336,11 +19581,11 @@ ${memory.body}`);
19336
19581
  );
19337
19582
  }
19338
19583
  if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
19339
- const body = bodyArg === "-" ? readFileSync20(0, "utf-8") : bodyArg;
19584
+ const body = bodyArg === "-" ? readFileSync22(0, "utf-8") : bodyArg;
19340
19585
  const thread = getArg4(rest, "--thread");
19341
- const { created, changed } = await saveChannelMemory(
19586
+ const { created, changed } = await saveMemory(
19587
+ target,
19342
19588
  {
19343
- channelId: channel.id,
19344
19589
  name,
19345
19590
  type,
19346
19591
  description,
@@ -19351,18 +19596,19 @@ ${memory.body}`);
19351
19596
  "agent"
19352
19597
  );
19353
19598
  const verb = created ? "Saved" : changed ? "Updated" : "Unchanged:";
19354
- console.log(`${verb} memory ${name} in ${channel.name}.`);
19599
+ console.log(`${verb} memory ${name} in ${where}.`);
19355
19600
  break;
19356
19601
  }
19357
19602
  case "delete": {
19358
19603
  const ref = words[0];
19359
19604
  if (!ref) throw new Error("Usage: flyspace memory delete --channel <c> <name-or-id>");
19360
- const deleted = await deleteChannelMemory(channel.id, ref, "agent", getArg4(rest, "--thread"));
19361
- 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}.`);
19362
19607
  console.log(`Deleted memory ${ref}.`);
19363
19608
  break;
19364
19609
  }
19365
19610
  case "import": {
19611
+ if (target.scope !== "channel") throw new Error("import only fills a channel memory");
19366
19612
  const dir = getArg4(rest, "--dir") ?? claudeMemoryDir(channel.cwd, profileClaudeConfigDirFor(channel.profile));
19367
19613
  const dryRun = rest.includes("--dry-run");
19368
19614
  const results = await importClaudeMemories(dir, channel.id, { dryRun });
@@ -19382,6 +19628,19 @@ ${memory.body}`);
19382
19628
  process.exit(1);
19383
19629
  }
19384
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
+ }
19385
19644
 
19386
19645
  // src/commands/mcp.ts
19387
19646
  var USAGE6 = `
@@ -19420,10 +19679,10 @@ async function runMcpCmd(args) {
19420
19679
  }
19421
19680
 
19422
19681
  // src/commands/dashboard.ts
19423
- import { spawn as spawn5, spawnSync } from "child_process";
19424
- import { existsSync as existsSync28 } from "fs";
19682
+ import { spawn as spawn7, spawnSync } from "child_process";
19683
+ import { existsSync as existsSync29 } from "fs";
19425
19684
  import { platform as platform7 } from "os";
19426
- import { join as join25 } from "path";
19685
+ import { join as join27 } from "path";
19427
19686
  function flagValue(args, name) {
19428
19687
  const eq = args.find((a) => a.startsWith(`--${name}=`));
19429
19688
  if (eq) return eq.slice(`--${name}=`.length);
@@ -19433,13 +19692,13 @@ function flagValue(args, name) {
19433
19692
  function openBrowser2(url) {
19434
19693
  const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
19435
19694
  try {
19436
- spawn5(cmd, [url], { stdio: "ignore", detached: true }).unref();
19695
+ spawn7(cmd, [url], { stdio: "ignore", detached: true }).unref();
19437
19696
  } catch {
19438
19697
  }
19439
19698
  }
19440
19699
  function ensureWebBuild() {
19441
- if (existsSync28(join25(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19442
- if (!existsSync28(join25(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;
19443
19702
  console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
19444
19703
  const result = spawnSync("pnpm", ["--dir", "web", "build"], {
19445
19704
  cwd: paths.PACKAGE_ROOT,
@@ -19489,7 +19748,7 @@ async function runDashboard(args) {
19489
19748
  }
19490
19749
 
19491
19750
  // src/commands/workspace.ts
19492
- 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";
19493
19752
  var USAGE7 = `
19494
19753
  Usage: flyspace workspace <subcommand> [options]
19495
19754
 
@@ -19575,10 +19834,10 @@ function provisionLocal(botName) {
19575
19834
  ensureDirs();
19576
19835
  console.log(` config dir ready (${paths.CONFIG_DIR})`);
19577
19836
  console.log(` data dir ready (${paths.DATA_DIR})`);
19578
- if (existsSync29(paths.CONFIG_FILE)) {
19837
+ if (existsSync30(paths.CONFIG_FILE)) {
19579
19838
  console.log(" config.env exists");
19580
19839
  } else {
19581
- writeFileSync12(paths.CONFIG_FILE, `BOT_NAME=${botName}
19840
+ writeFileSync13(paths.CONFIG_FILE, `BOT_NAME=${botName}
19582
19841
  `);
19583
19842
  console.log(` config.env written (${paths.CONFIG_FILE})`);
19584
19843
  }
@@ -19592,8 +19851,8 @@ async function provisionChannels(specs) {
19592
19851
  continue;
19593
19852
  }
19594
19853
  const cwd = expandPath(spec.cwd);
19595
- if (!existsSync29(cwd)) {
19596
- mkdirSync11(cwd, { recursive: true });
19854
+ if (!existsSync30(cwd)) {
19855
+ mkdirSync13(cwd, { recursive: true });
19597
19856
  console.log(` created cwd ${cwd}`);
19598
19857
  }
19599
19858
  await createChannel({ name: spec.name, cwd, position: index });
@@ -19802,7 +20061,7 @@ async function main() {
19802
20061
  break;
19803
20062
  case "-v":
19804
20063
  case "--version":
19805
- printRaw("0.1.14");
20064
+ printRaw("0.1.16");
19806
20065
  break;
19807
20066
  case "-h":
19808
20067
  case "--help":