flyspace-cli 0.1.15 → 0.1.18

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getSupabaseClient
4
- } from "./chunk-ZBMYF5LX.js";
4
+ } from "./chunk-L3I3BPLP.js";
5
5
 
6
6
  // src/store/chat-admin-store.ts
7
7
  async function listChannels(profile) {
@@ -157,6 +157,9 @@ function setSupabaseClient(replacement) {
157
157
  function hasSupabaseCredentials() {
158
158
  return !!process.env["SUPABASE_URL"] && !!process.env["SUPABASE_SECRET_KEY"];
159
159
  }
160
+ function databaseReachable() {
161
+ return !!client || hasSupabaseCredentials();
162
+ }
160
163
  function getSupabaseClient() {
161
164
  if (client) return client;
162
165
  const url = process.env["SUPABASE_URL"];
@@ -319,7 +322,7 @@ function deriveThreadTitle(text) {
319
322
  }
320
323
  async function syncPostToDashboard(opts) {
321
324
  const { channelId, ts, threadTs, text } = opts;
322
- if (!process.env["SUPABASE_URL"] || !process.env["SUPABASE_SECRET_KEY"]) {
325
+ if (!databaseReachable()) {
323
326
  console.warn("[dashboard-sync] Supabase not configured \u2014 skipping dashboard sync");
324
327
  return;
325
328
  }
@@ -430,6 +433,7 @@ export {
430
433
  sweepScratchDir,
431
434
  setSupabaseClient,
432
435
  hasSupabaseCredentials,
436
+ databaseReachable,
433
437
  getSupabaseClient,
434
438
  AGENT_AUTHOR,
435
439
  SYNCED_BOT_AUTHOR,
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  AGENT_AUTHOR,
4
4
  SYNCED_BOT_AUTHOR,
5
+ databaseReachable,
5
6
  ensureDirs,
6
7
  getProfile,
7
8
  getR2Bucket,
@@ -12,7 +13,7 @@ import {
12
13
  runWithProfile,
13
14
  sweepScratchDir,
14
15
  syncPostToDashboard
15
- } from "./chunk-ZBMYF5LX.js";
16
+ } from "./chunk-L3I3BPLP.js";
16
17
 
17
18
  // src/index.ts
18
19
  import { execFile, spawn as spawn4 } from "child_process";
@@ -263,7 +264,7 @@ function stopAutomationRegistryPolling() {
263
264
  }
264
265
 
265
266
  // src/channels.ts
266
- import { existsSync as existsSync2, mkdirSync, statSync } from "fs";
267
+ import { existsSync as existsSync2, mkdirSync, realpathSync, statSync } from "fs";
267
268
  import { homedir } from "os";
268
269
  import { isAbsolute, resolve as resolve2, sep } from "path";
269
270
  function isLogicalCwd(p) {
@@ -272,11 +273,22 @@ function isLogicalCwd(p) {
272
273
  function logicalRoot() {
273
274
  return process.env["FLYSPACE_ROOT"] || resolve2(homedir(), "Flyspace");
274
275
  }
276
+ var workspaceRoots = /* @__PURE__ */ new Map();
277
+ async function refreshWorkspaceRoots() {
278
+ const { data, error } = await getSupabaseClient().from("workspaces").select("slug, root_path");
279
+ if (error) return;
280
+ workspaceRoots = new Map(
281
+ (data ?? []).filter((w) => w.root_path).map((w) => [w.slug, expandPath(w.root_path)])
282
+ );
283
+ }
275
284
  function resolveLogicalCwd(p) {
276
- return resolve2(logicalRoot(), p);
285
+ const [slug, name] = p.split("/");
286
+ const root = workspaceRoots.get(slug);
287
+ return root ? resolve2(root, name) : resolve2(logicalRoot(), p);
277
288
  }
278
289
  function ensureLogicalCwdDir(cwd) {
279
- if (!cwd.startsWith(logicalRoot() + sep) || existsSync2(cwd)) return;
290
+ const roots = [logicalRoot(), ...workspaceRoots.values()];
291
+ if (!roots.some((r) => cwd.startsWith(r + sep)) || existsSync2(cwd)) return;
280
292
  mkdirSync(cwd, { recursive: true });
281
293
  }
282
294
  function expandPath(p) {
@@ -358,6 +370,9 @@ function indexRegistry(list) {
358
370
  );
359
371
  }
360
372
  async function refreshChannelRegistry() {
373
+ const rootsBefore = JSON.stringify([...workspaceRoots]);
374
+ await refreshWorkspaceRoots();
375
+ if (JSON.stringify([...workspaceRoots]) !== rootsBefore) lastSnapshot2 = void 0;
361
376
  const { data, error } = await getSupabaseClient().from("channels").select("*").order("position", { ascending: true });
362
377
  if (error) {
363
378
  console.error("[channels] refreshChannelRegistry failed:", error.message);
@@ -522,7 +537,44 @@ function toRow(input) {
522
537
  if (input.archivedAt !== void 0) row["archived_at"] = input.archivedAt;
523
538
  return row;
524
539
  }
540
+ var ChannelCwdError = class extends Error {
541
+ constructor(message) {
542
+ super(message);
543
+ this.name = "ChannelCwdError";
544
+ }
545
+ };
546
+ function realpathOrSelf(p) {
547
+ try {
548
+ return realpathSync(p);
549
+ } catch {
550
+ return p;
551
+ }
552
+ }
553
+ async function assertCwdInWorkspace(profile, cwd) {
554
+ if (isLogicalCwd(cwd)) {
555
+ if (cwd.split("/")[0] !== profile) {
556
+ throw new ChannelCwdError(`Folder "${cwd}" belongs to another workspace.`);
557
+ }
558
+ return cwd;
559
+ }
560
+ const { data, error } = await getSupabaseClient().from("workspaces").select("slug, root_path");
561
+ if (error) throw new Error(error.message);
562
+ const workspaces = data ?? [];
563
+ const ownRoot = workspaces.find((w) => w.slug === profile)?.root_path;
564
+ const home = ownRoot?.match(/^(\/Users\/[^/]+|\/home\/[^/]+|\/root)(?=\/|$)/)?.[1];
565
+ const absolute = home && (cwd === "~" || cwd.startsWith("~/")) ? resolve2(home, cwd.slice(2)) : expandPath(cwd);
566
+ const path = realpathOrSelf(absolute);
567
+ const roots = workspaces.filter((w) => w.root_path).map((w) => ({ slug: w.slug, root: realpathOrSelf(expandPath(w.root_path)) }));
568
+ const owner = roots.filter(({ root }) => path === root || path.startsWith(root + sep)).sort((a, b) => b.root.length - a.root.length)[0];
569
+ if (owner ? owner.slug !== profile : roots.some((r) => r.slug === profile)) {
570
+ throw new ChannelCwdError(
571
+ owner ? `Folder "${cwd}" belongs to the "${owner.slug}" workspace.` : `Folder "${cwd}" is outside this workspace's folder.`
572
+ );
573
+ }
574
+ return absolute;
575
+ }
525
576
  async function createChannel(input) {
577
+ input = { ...input, cwd: await assertCwdInWorkspace(getProfile(), input.cwd) };
526
578
  const { data, error } = await getSupabaseClient().from("channels").insert({ ...toRow(input), profile: getProfile() }).select().single();
527
579
  if (error) throw new Error(error.message);
528
580
  const channel = rowToChannel(data);
@@ -547,6 +599,9 @@ async function updateChannelById(id, patch) {
547
599
  if (existing?.isDefault && (patch.visibility === "private" || patch.archivedAt || patch.isDefault === false || patch.name !== void 0 && patch.name.trim() !== existing.name)) {
548
600
  throw new DefaultChannelError();
549
601
  }
602
+ if (patch.cwd !== void 0) {
603
+ patch = { ...patch, cwd: await assertCwdInWorkspace(existing?.profile ?? getProfile(), patch.cwd) };
604
+ }
550
605
  const { data, error } = await getSupabaseClient().from("channels").update({ ...toRow(patch), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).select().single();
551
606
  if (error) throw new Error(error.message);
552
607
  await refreshChannelRegistry();
@@ -1543,9 +1598,9 @@ function isAuthError(text) {
1543
1598
  }
1544
1599
  function authErrorMessage(raw) {
1545
1600
  return `The agent can't sign in to Claude right now (the Host's login expired and could not be refreshed).
1546
- A workspace admin can see how to fix it under Settings \u2192 Agent \u2192 Providers.
1601
+ To fix it, a workspace admin can open Settings \u2192 Agent \u2192 Providers and click Sign in next to Claude Code.
1547
1602
 
1548
- Original error: ${raw}`;
1603
+ > Original error: ${raw}`;
1549
1604
  }
1550
1605
  function isTransientError(text, apiErrorStatus) {
1551
1606
  if (apiErrorStatus == null) {
@@ -2111,7 +2166,7 @@ var featureFlags = {
2111
2166
  dictation: {
2112
2167
  title: "Dictation",
2113
2168
  description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
2114
- stage: "dev"
2169
+ stage: "beta"
2115
2170
  },
2116
2171
  "agent-memory": {
2117
2172
  title: "Agent memory",
@@ -2161,6 +2216,7 @@ async function disableFeatureFlag(workspaceId, flagId) {
2161
2216
 
2162
2217
  // src/store/memory-store.ts
2163
2218
  var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
2219
+ var channelTarget = (channelId) => ({ scope: "channel", channelId });
2164
2220
  var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2165
2221
  var DESCRIPTION_MAX = 200;
2166
2222
  var BODY_MAX = 16e3;
@@ -2184,7 +2240,10 @@ function validateMemoryInput(input) {
2184
2240
  function rowToMemory(row) {
2185
2241
  return {
2186
2242
  id: row["id"],
2187
- channelId: row["channel_id"],
2243
+ scope: row["scope"] ?? "channel",
2244
+ workspaceId: row["workspace_id"],
2245
+ channelId: row["channel_id"] ?? null,
2246
+ userId: row["user_id"] ?? null,
2188
2247
  name: row["name"],
2189
2248
  description: row["description"],
2190
2249
  type: row["type"],
@@ -2192,23 +2251,40 @@ function rowToMemory(row) {
2192
2251
  sourceThreadId: row["source_thread_id"] ?? null,
2193
2252
  createdBy: row["created_by"],
2194
2253
  createdAt: row["created_at"],
2195
- updatedAt: row["updated_at"]
2254
+ updatedAt: row["updated_at"],
2255
+ lastUsedAt: row["last_used_at"] ?? null
2196
2256
  };
2197
2257
  }
2198
- async function listChannelMemories(channelId) {
2199
- const { data, error } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
2200
- if (error) throw new Error(`listChannelMemories(${channelId}): ${error.message}`);
2258
+ function targetRow(target) {
2259
+ switch (target.scope) {
2260
+ case "channel":
2261
+ return { scope: "channel", channel_id: target.channelId };
2262
+ case "workspace":
2263
+ return { scope: "workspace", workspace_id: target.workspaceId };
2264
+ case "user":
2265
+ return { scope: "user", workspace_id: target.workspaceId, user_id: target.userId };
2266
+ }
2267
+ }
2268
+ function inTarget(query, target) {
2269
+ let q = query;
2270
+ for (const [column, value] of Object.entries(targetRow(target))) q = q.eq(column, value);
2271
+ return q;
2272
+ }
2273
+ var describeTarget = (t) => t.scope === "channel" ? t.channelId : t.scope === "workspace" ? `workspace ${t.workspaceId}` : `user ${t.userId}`;
2274
+ async function listMemories(target) {
2275
+ const { data, error } = await inTarget(getSupabaseClient().from("memories").select("*"), target).order("updated_at", { ascending: false });
2276
+ if (error) throw new Error(`listMemories(${describeTarget(target)}): ${error.message}`);
2201
2277
  return (data ?? []).map(rowToMemory);
2202
2278
  }
2203
- async function getChannelMemory(channelId, name) {
2204
- const { data, error } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
2205
- if (error) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error.message}`);
2279
+ async function getMemory(target, name) {
2280
+ const { data, error } = await inTarget(getSupabaseClient().from("memories").select("*"), target).eq("name", name).maybeSingle();
2281
+ if (error) throw new Error(`getMemory(${describeTarget(target)}, ${name}): ${error.message}`);
2206
2282
  return data ? rowToMemory(data) : null;
2207
2283
  }
2208
- async function saveChannelMemory(input, actor) {
2284
+ async function saveMemory(target, input, actor) {
2209
2285
  validateMemoryInput(input);
2210
2286
  const supabase = getSupabaseClient();
2211
- const existing = await getChannelMemory(input.channelId, input.name);
2287
+ const existing = await getMemory(target, input.name);
2212
2288
  const fields = {
2213
2289
  description: input.description.trim(),
2214
2290
  type: input.type,
@@ -2219,9 +2295,9 @@ async function saveChannelMemory(input, actor) {
2219
2295
  if (existing && sameContent(existing, fields)) {
2220
2296
  return { memory: existing, created: false, changed: false };
2221
2297
  }
2222
- const { data, error } = 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: input.channelId, name: input.name }).select("*").single();
2298
+ const { data, error } = 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, ...targetRow(target), name: input.name }).select("*").single();
2223
2299
  if (error)
2224
- throw new Error(`saveChannelMemory(${input.channelId}, ${input.name}): ${error.message}`);
2300
+ throw new Error(`saveMemory(${describeTarget(target)}, ${input.name}): ${error.message}`);
2225
2301
  const memory = rowToMemory(data);
2226
2302
  await logMemoryEvent({
2227
2303
  memory,
@@ -2233,12 +2309,12 @@ async function saveChannelMemory(input, actor) {
2233
2309
  });
2234
2310
  return { memory, created: !existing, changed: true };
2235
2311
  }
2236
- async function updateChannelMemory(channelId, memoryId, patch, actor) {
2237
- const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
2312
+ async function updateMemory(target, memoryId, patch, actor) {
2313
+ const existing = (await listMemories(target)).find((m) => m.id === memoryId);
2238
2314
  if (!existing) return null;
2239
- const { memory } = await saveChannelMemory(
2315
+ const { memory } = await saveMemory(
2316
+ target,
2240
2317
  {
2241
- channelId,
2242
2318
  name: existing.name,
2243
2319
  description: patch.description ?? existing.description,
2244
2320
  type: patch.type ?? existing.type,
@@ -2249,20 +2325,28 @@ async function updateChannelMemory(channelId, memoryId, patch, actor) {
2249
2325
  );
2250
2326
  return memory;
2251
2327
  }
2252
- async function searchChannelMemories(channelId, query) {
2328
+ async function searchMemories(target, query) {
2253
2329
  const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
2254
2330
  if (!words.length) return [];
2255
- let request = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
2331
+ let request = inTarget(getSupabaseClient().from("memories").select("*"), target);
2256
2332
  for (const word of words) {
2257
2333
  request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
2258
2334
  }
2259
2335
  const { data, error } = await request.order("updated_at", { ascending: false }).limit(20);
2260
- if (error) throw new Error(`searchChannelMemories(${channelId}): ${error.message}`);
2336
+ if (error) throw new Error(`searchMemories(${describeTarget(target)}): ${error.message}`);
2261
2337
  return (data ?? []).map(rowToMemory);
2262
2338
  }
2263
- async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
2264
- const { data, error } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
2265
- if (error) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error.message}`);
2339
+ var USED_GRANULARITY_MS = 24 * 60 * 6e4;
2340
+ async function markMemoryUsed(memory) {
2341
+ const cutoff = new Date(Date.now() - USED_GRANULARITY_MS).toISOString();
2342
+ if (memory.lastUsedAt && memory.lastUsedAt > cutoff) return;
2343
+ const { error } = await getSupabaseClient().from("memories").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", memory.id).or(`last_used_at.is.null,last_used_at.lt.${cutoff}`);
2344
+ if (error) console.error(`[memory] markMemoryUsed(${memory.id}) failed:`, error.message);
2345
+ }
2346
+ async function deleteMemory(target, idOrName, actor, threadId) {
2347
+ const { data, error } = await inTarget(getSupabaseClient().from("memories").delete(), target).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
2348
+ if (error)
2349
+ throw new Error(`deleteMemory(${describeTarget(target)}, ${idOrName}): ${error.message}`);
2266
2350
  const deleted = (data ?? []).map(rowToMemory);
2267
2351
  for (const memory of deleted) {
2268
2352
  await logMemoryEvent({
@@ -2276,6 +2360,25 @@ async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
2276
2360
  }
2277
2361
  return deleted.length > 0;
2278
2362
  }
2363
+ async function promoteChannelMemory(channelId, memoryId) {
2364
+ const supabase = getSupabaseClient();
2365
+ const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
2366
+ if (!existing) return { ok: false, reason: "not-found" };
2367
+ if (await getMemory({ scope: "workspace", workspaceId: existing.workspaceId }, existing.name)) {
2368
+ return { ok: false, reason: "name-taken" };
2369
+ }
2370
+ const { data, error } = await supabase.from("memories").update({ scope: "workspace", channel_id: null, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", memoryId).eq("scope", "channel").select("*").maybeSingle();
2371
+ if (error) throw new Error(`promoteChannelMemory(${memoryId}): ${error.message}`);
2372
+ if (!data) return { ok: false, reason: "not-found" };
2373
+ const memory = rowToMemory(data);
2374
+ await logMemoryEvent({ memory, action: "create", before: null, after: snapshot(memory), actor: "user", threadId: null });
2375
+ return { ok: true, memory };
2376
+ }
2377
+ var listChannelMemories = (channelId) => listMemories(channelTarget(channelId));
2378
+ var getChannelMemory = (channelId, name) => getMemory(channelTarget(channelId), name);
2379
+ var saveChannelMemory = ({ channelId, ...fields }, actor) => saveMemory(channelTarget(channelId), fields, actor);
2380
+ var updateChannelMemory = (channelId, memoryId, patch, actor) => updateMemory(channelTarget(channelId), memoryId, patch, actor);
2381
+ var deleteChannelMemory = (channelId, idOrName, actor, threadId) => deleteMemory(channelTarget(channelId), idOrName, actor, threadId);
2279
2382
  function unchangedSince(current, after) {
2280
2383
  return current && after ? sameContent(current, after) : !current && !after;
2281
2384
  }
@@ -2294,6 +2397,10 @@ function sameContent(m, other) {
2294
2397
  async function logMemoryEvent(e) {
2295
2398
  const { error } = await getSupabaseClient().from("memory_events").insert({
2296
2399
  channel_id: e.memory.channelId,
2400
+ // A channel event's workspace comes from its channel (0100 trigger).
2401
+ ...e.memory.channelId ? {} : { workspace_id: e.memory.workspaceId },
2402
+ scope: e.memory.scope,
2403
+ user_id: e.memory.userId,
2297
2404
  memory_id: e.action === "delete" ? null : e.memory.id,
2298
2405
  thread_id: e.threadId,
2299
2406
  name: e.memory.name,
@@ -2319,15 +2426,18 @@ function rowToEvent(row) {
2319
2426
  undoneAt: row["undone_at"] ?? null
2320
2427
  };
2321
2428
  }
2322
- async function listThreadMemoryEvents(channelId, threadId) {
2429
+ async function listMemoryEvents(channelId, filter = {}) {
2323
2430
  const supabase = getSupabaseClient();
2324
- const { data, error } = await supabase.from("memory_events").select("*").eq("channel_id", channelId).eq("thread_id", threadId).order("created_at", { ascending: false }).limit(50);
2325
- if (error) throw new Error(`listThreadMemoryEvents(${threadId}): ${error.message}`);
2431
+ let query = supabase.from("memory_events").select("*").eq("channel_id", channelId);
2432
+ if (filter.threadId) query = query.eq("thread_id", filter.threadId);
2433
+ if (filter.actor) query = query.eq("actor", filter.actor);
2434
+ const { data, error } = await query.order("created_at", { ascending: false }).limit(filter.limit ?? 50);
2435
+ if (error) throw new Error(`listMemoryEvents(${channelId}): ${error.message}`);
2326
2436
  const events = (data ?? []).map(rowToEvent);
2327
2437
  if (!events.length) return [];
2328
2438
  const names = [...new Set(events.map((e) => e.name))];
2329
2439
  const live = await supabase.from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).in("name", names);
2330
- if (live.error) throw new Error(`listThreadMemoryEvents(${threadId}): ${live.error.message}`);
2440
+ if (live.error) throw new Error(`listMemoryEvents(${channelId}): ${live.error.message}`);
2331
2441
  const byName = new Map(
2332
2442
  (live.data ?? []).map((r) => [
2333
2443
  r["name"],
@@ -2344,6 +2454,7 @@ async function listThreadMemoryEvents(channelId, threadId) {
2344
2454
  };
2345
2455
  });
2346
2456
  }
2457
+ var listThreadMemoryEvents = (channelId, threadId) => listMemoryEvents(channelId, { threadId, actor: "agent" });
2347
2458
  async function undoMemoryEvent(channelId, eventId) {
2348
2459
  const supabase = getSupabaseClient();
2349
2460
  const { data, error } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
@@ -2374,21 +2485,29 @@ async function undoMemoryEvent(channelId, eventId) {
2374
2485
  function isUuid(value) {
2375
2486
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
2376
2487
  }
2377
- function formatMemoryPrompt(channelId, memories) {
2378
- const lines = [];
2488
+ function formatMemoryPrompt(channelId, memories, extras = {}) {
2379
2489
  let used = 0;
2380
- let omitted = 0;
2381
- for (const m of memories) {
2382
- const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
2383
- if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
2384
- omitted++;
2385
- continue;
2490
+ const indexOf = (list, searchHint) => {
2491
+ const lines = [];
2492
+ let omitted = 0;
2493
+ for (const m of list) {
2494
+ const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
2495
+ if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
2496
+ omitted++;
2497
+ continue;
2498
+ }
2499
+ lines.push(line);
2500
+ used += line.length + 1;
2386
2501
  }
2387
- lines.push(line);
2388
- used += line.length + 1;
2389
- }
2390
- const index = lines.length ? lines.join("\n") + (omitted ? `
2391
- - (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
2502
+ if (!lines.length && !omitted) return "(none yet)";
2503
+ return lines.join("\n") + (omitted ? `
2504
+ - (${omitted} older memories not listed; find them with \`${searchHint}\`)` : "");
2505
+ };
2506
+ const user = extras.user;
2507
+ const userFlags2 = user ? `--scope user --user ${user.userId}` : "";
2508
+ const userIndex = user ? indexOf(user.memories, `memory search --channel ${channelId} ${userFlags2}`) : "";
2509
+ const channelIndex = indexOf(memories, "memory search");
2510
+ const workspaceIndex = extras.workspace?.length ? indexOf(extras.workspace, `memory search --channel ${channelId} --scope workspace`) : "";
2392
2511
  return [
2393
2512
  `## Channel memory`,
2394
2513
  ``,
@@ -2400,26 +2519,22 @@ function formatMemoryPrompt(channelId, memories) {
2400
2519
  `- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
2401
2520
  ``,
2402
2521
  `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.`,
2522
+ ...user ? [
2523
+ ``,
2524
+ `A personal preference of @${user.handle} (language, tone, reply length, time zone, how they like to work) belongs in their personal memory instead, which only their own turns see: add \`${userFlags2}\` to the same commands. Never save someone else's preferences there, and never put project knowledge there.`
2525
+ ] : [],
2526
+ ...workspaceIndex ? [
2527
+ ``,
2528
+ `Workspace memories below apply to every channel. They are read-only to you: read one with \`--scope workspace\` on \`get\`; a Workspace Admin manages them.`
2529
+ ] : [],
2403
2530
  ``,
2531
+ ...user ? [`Personal memory of @${user.handle}:`, userIndex, ``] : [],
2404
2532
  `Index:`,
2405
- index
2533
+ channelIndex,
2534
+ ...workspaceIndex ? [``, `Workspace memory:`, workspaceIndex] : []
2406
2535
  ].join("\n");
2407
2536
  }
2408
2537
 
2409
- // src/store/memory-context.ts
2410
- async function loadAgentMemory(profile, channelId, opts = {}) {
2411
- const enabled = await listEnabledFeatureFlagsBySlug(profile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
2412
- console.error("[memory] feature flag lookup failed:", err.message);
2413
- return false;
2414
- });
2415
- if (!enabled || opts.readable === false || !channelId || !isUuid(channelId)) return { enabled };
2416
- const memoryPrompt = await listChannelMemories(channelId).then((memories) => formatMemoryPrompt(channelId, memories)).catch((err) => {
2417
- console.error("[memory] memory lookup failed:", err.message);
2418
- return void 0;
2419
- });
2420
- return memoryPrompt ? { enabled, memoryPrompt } : { enabled };
2421
- }
2422
-
2423
2538
  // src/store/workspace-store.ts
2424
2539
  function rowToMembership(row) {
2425
2540
  return {
@@ -2431,7 +2546,8 @@ function rowToMembership(row) {
2431
2546
  role: row["role"],
2432
2547
  status: row["status"],
2433
2548
  invitedAt: row["invited_at"],
2434
- acceptedAt: row["accepted_at"] ?? null
2549
+ acceptedAt: row["accepted_at"] ?? null,
2550
+ memoryAutoPaused: row["memory_auto_paused"] === true
2435
2551
  };
2436
2552
  }
2437
2553
  var WORKSPACE_CACHE_TTL_MS = 6e4;
@@ -2504,9 +2620,6 @@ async function setWorkspaceStripeSubscription(workspaceId, fields) {
2504
2620
  }).eq("id", workspaceId);
2505
2621
  if (error) throw new Error(`setWorkspaceStripeSubscription(${workspaceId}): ${error.message}`);
2506
2622
  }
2507
- async function markWorkspaceAgentConnected(workspaceId) {
2508
- await stampOnce(workspaceId, "agent_connected_at");
2509
- }
2510
2623
  async function markWorkspaceProjectsSetup(workspaceId) {
2511
2624
  await stampOnce(workspaceId, "projects_setup_at");
2512
2625
  }
@@ -2587,6 +2700,10 @@ async function listMembershipsByEmail(email) {
2587
2700
  if (error) throw new Error(`listMembershipsByEmail: ${error.message}`);
2588
2701
  return (data ?? []).map(rowToMembership);
2589
2702
  }
2703
+ async function setMemoryAutoPaused(workspaceId, userId, paused) {
2704
+ const { error } = await getSupabaseClient().from("memberships").update({ memory_auto_paused: paused }).eq("workspace_id", workspaceId).eq("user_id", userId);
2705
+ if (error) throw new Error(`setMemoryAutoPaused(${workspaceId}): ${error.message}`);
2706
+ }
2590
2707
  async function listMemberships(workspaceId) {
2591
2708
  const { data, error } = await getSupabaseClient().from("memberships").select("*").eq("workspace_id", workspaceId).order("invited_at", { ascending: true });
2592
2709
  if (error) throw new Error(`listMemberships(${workspaceId}): ${error.message}`);
@@ -2710,6 +2827,37 @@ async function setAgentPaused(workspaceId, paused) {
2710
2827
  if (error) throw new Error(`setAgentPaused(${workspaceId}): ${error.message}`);
2711
2828
  }
2712
2829
 
2830
+ // src/store/memory-context.ts
2831
+ async function loadAgentMemory(profile, channelId, opts = {}) {
2832
+ const enabled = await listEnabledFeatureFlagsBySlug(profile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
2833
+ console.error("[memory] feature flag lookup failed:", err.message);
2834
+ return false;
2835
+ });
2836
+ if (!enabled || opts.readable === false || !channelId || !isUuid(channelId)) return { enabled };
2837
+ const memoryPrompt = await buildPrompt(profile, channelId, opts).catch((err) => {
2838
+ console.error("[memory] memory lookup failed:", err.message);
2839
+ return void 0;
2840
+ });
2841
+ return memoryPrompt ? { enabled, memoryPrompt } : { enabled };
2842
+ }
2843
+ async function buildPrompt(profile, channelId, opts) {
2844
+ const workspace = await getWorkspaceBySlug(profile);
2845
+ const handle = opts.senderHandle?.toLowerCase();
2846
+ const sender = workspace && handle ? (await listMemberships(workspace.id)).find(
2847
+ (m) => m.userId && m.handle.toLowerCase() === handle
2848
+ ) : void 0;
2849
+ const [channel, shared, own] = await Promise.all([
2850
+ listMemories({ scope: "channel", channelId }),
2851
+ workspace ? listMemories({ scope: "workspace", workspaceId: workspace.id }) : [],
2852
+ workspace && sender ? listMemories({ scope: "user", workspaceId: workspace.id, userId: sender.userId }) : []
2853
+ ]);
2854
+ const extras = {
2855
+ workspace: shared,
2856
+ ...sender ? { user: { userId: sender.userId, handle: sender.handle, memories: own } } : {}
2857
+ };
2858
+ return formatMemoryPrompt(channelId, channel, extras);
2859
+ }
2860
+
2713
2861
  // src/dashboard/settings.ts
2714
2862
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
2715
2863
  import { dirname, resolve as resolve5 } from "path";
@@ -4427,6 +4575,11 @@ import { CronExpressionParser } from "cron-parser";
4427
4575
  var Scheduler = class {
4428
4576
  jobs = /* @__PURE__ */ new Map();
4429
4577
  caughtUp = /* @__PURE__ */ new Set();
4578
+ // A run is only recorded when it finishes, so while one is in flight the
4579
+ // store still shows the previous tick. Without this guard, a registry
4580
+ // reload during a long run re-registers the schedule and catchup starts a
4581
+ // second, overlapping copy (sah-budget-fetch ran twice a day this way).
4582
+ running = /* @__PURE__ */ new Set();
4430
4583
  store;
4431
4584
  constructor(store) {
4432
4585
  this.store = store;
@@ -4478,6 +4631,11 @@ var Scheduler = class {
4478
4631
  wrappedRun();
4479
4632
  }
4480
4633
  async runAndRecord(name, runFn) {
4634
+ if (this.running.has(name)) {
4635
+ console.log(`[scheduler] ${name} is still running, skipping overlapping run`);
4636
+ return;
4637
+ }
4638
+ this.running.add(name);
4481
4639
  const startedAt = /* @__PURE__ */ new Date();
4482
4640
  const elapsed = () => Date.now() - startedAt.getTime();
4483
4641
  try {
@@ -4486,6 +4644,8 @@ var Scheduler = class {
4486
4644
  } catch (err) {
4487
4645
  console.error(`[scheduler] Error in ${name}:`, err);
4488
4646
  this.store?.recordRun(name, "error", startedAt, elapsed());
4647
+ } finally {
4648
+ this.running.delete(name);
4489
4649
  }
4490
4650
  }
4491
4651
  unregister(name) {
@@ -5352,7 +5512,7 @@ async function ownIdentityFor(client) {
5352
5512
  return own;
5353
5513
  }
5354
5514
  async function mirrorIncomingMessage(client, msg) {
5355
- if (!process.env["SUPABASE_URL"] || !process.env["SUPABASE_SECRET_KEY"]) return;
5515
+ if (!databaseReachable()) return;
5356
5516
  if (msg.subtype && SKIP_SUBTYPES.has(msg.subtype)) return;
5357
5517
  if (msg.subtype === "message_changed" || msg.subtype === "message_deleted") return;
5358
5518
  try {
@@ -8382,7 +8542,7 @@ function startUsageSync(store, workspace, options = {}) {
8382
8542
 
8383
8543
  // src/store/usage-recording.ts
8384
8544
  function installUsageRecording(dbPath, options = {}) {
8385
- const { sync = Boolean(process.env["SUPABASE_URL"] && process.env["SUPABASE_SECRET_KEY"]) } = options;
8545
+ const { sync = databaseReachable() } = options;
8386
8546
  const stores = /* @__PURE__ */ new Map();
8387
8547
  const stopSyncs = [];
8388
8548
  const storeFor = (profile) => {
@@ -8642,10 +8802,7 @@ async function startServer() {
8642
8802
  const store = new SessionStore(paths.DB_FILE);
8643
8803
  const runStore = new AutomationRunStore(paths.DB_FILE);
8644
8804
  installUsageRecording();
8645
- const hasSupabaseCreds = Boolean(
8646
- process.env["SUPABASE_URL"] && process.env["SUPABASE_SECRET_KEY"]
8647
- );
8648
- const stopRunSync = hasSupabaseCreds ? startAutomationRunSync(runStore, getProfile(), {
8805
+ const stopRunSync = databaseReachable() ? startAutomationRunSync(runStore, getProfile(), {
8649
8806
  onError: (error) => console.error(`[${ts2()}] [flyspace] automation run sync:`, error)
8650
8807
  }) : void 0;
8651
8808
  const reactionStore = new ReactionStore(paths.DB_FILE);
@@ -8710,7 +8867,6 @@ export {
8710
8867
  getChannelRegistry,
8711
8868
  getProfileChannelRegistry,
8712
8869
  findChannelById,
8713
- findChannelByAgentName,
8714
8870
  findChannel,
8715
8871
  startChannelRegistryPolling,
8716
8872
  isCwdUsable,
@@ -8718,6 +8874,7 @@ export {
8718
8874
  warnOnBrokenChannelCwds,
8719
8875
  resolveCwd,
8720
8876
  resolveChannelPrompt,
8877
+ ChannelCwdError,
8721
8878
  createChannel,
8722
8879
  DefaultChannelError,
8723
8880
  updateChannelById,
@@ -8745,6 +8902,7 @@ export {
8745
8902
  modelLabel,
8746
8903
  usageLimitedUntil,
8747
8904
  usageLimitedModels,
8905
+ clearClaudeAuthFailed,
8748
8906
  claudeAuthFailed,
8749
8907
  buildSystemPrompt,
8750
8908
  docsAccessPromptNote,
@@ -8761,16 +8919,23 @@ export {
8761
8919
  MEMORY_TYPES,
8762
8920
  isMemoryType,
8763
8921
  validateMemoryInput,
8922
+ listMemories,
8923
+ getMemory,
8924
+ saveMemory,
8925
+ updateMemory,
8926
+ searchMemories,
8927
+ markMemoryUsed,
8928
+ deleteMemory,
8929
+ promoteChannelMemory,
8764
8930
  listChannelMemories,
8765
8931
  getChannelMemory,
8766
8932
  saveChannelMemory,
8767
8933
  updateChannelMemory,
8768
- searchChannelMemories,
8769
8934
  deleteChannelMemory,
8935
+ listMemoryEvents,
8770
8936
  listThreadMemoryEvents,
8771
8937
  undoMemoryEvent,
8772
8938
  isUuid,
8773
- loadAgentMemory,
8774
8939
  getWorkspaceBySlug,
8775
8940
  getWorkspaceById,
8776
8941
  listWorkspaces,
@@ -8778,7 +8943,6 @@ export {
8778
8943
  createWorkspace,
8779
8944
  deleteWorkspace,
8780
8945
  setWorkspaceStripeSubscription,
8781
- markWorkspaceAgentConnected,
8782
8946
  markWorkspaceProjectsSetup,
8783
8947
  markWorkspaceGreeted,
8784
8948
  getWorkspaceGreetedAt,
@@ -8790,6 +8954,7 @@ export {
8790
8954
  getMembershipByEmail,
8791
8955
  getMembershipByUserId,
8792
8956
  listMembershipsByEmail,
8957
+ setMemoryAutoPaused,
8793
8958
  listMemberships,
8794
8959
  createMembership,
8795
8960
  HANDLE_PATTERN,
@@ -8806,12 +8971,11 @@ export {
8806
8971
  setAllowGuestAccess,
8807
8972
  getAgentPaused,
8808
8973
  setAgentPaused,
8809
- DEFAULT_TITLE_RULES,
8974
+ loadAgentMemory,
8810
8975
  extractCategories,
8811
8976
  buildAutoTitle,
8812
8977
  BRANDING_DEFAULTS,
8813
8978
  getWorkspaceBranding,
8814
- localDefaults,
8815
8979
  loadDashboardSettings,
8816
8980
  saveDashboardSettings,
8817
8981
  loadWorkspaceBranding,