flyspace-cli 0.1.1 → 0.1.4

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-QZIN7MWV.js";
4
+ } from "./chunk-ZBMYF5LX.js";
5
5
 
6
6
  // src/store/chat-admin-store.ts
7
7
  async function listChannels(profile) {
@@ -12,7 +12,7 @@ import {
12
12
  runWithProfile,
13
13
  sweepScratchDir,
14
14
  syncPostToDashboard
15
- } from "./chunk-QZIN7MWV.js";
15
+ } from "./chunk-ZBMYF5LX.js";
16
16
 
17
17
  // src/index.ts
18
18
  import { execFile, spawn as spawn4 } from "child_process";
@@ -265,12 +265,19 @@ function stopAutomationRegistryPolling() {
265
265
  // src/channels.ts
266
266
  import { existsSync as existsSync2, mkdirSync, statSync } from "fs";
267
267
  import { homedir } from "os";
268
- import { isAbsolute, resolve as resolve2 } from "path";
268
+ import { isAbsolute, resolve as resolve2, sep } from "path";
269
269
  function isLogicalCwd(p) {
270
270
  return /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/.test(p);
271
271
  }
272
+ function logicalRoot() {
273
+ return process.env["FLYSPACE_ROOT"] || resolve2(homedir(), "Flyspace");
274
+ }
272
275
  function resolveLogicalCwd(p) {
273
- return resolve2(process.env["FLYSPACE_ROOT"] || resolve2(homedir(), "Flyspace"), p);
276
+ return resolve2(logicalRoot(), p);
277
+ }
278
+ function ensureLogicalCwdDir(cwd) {
279
+ if (!cwd.startsWith(logicalRoot() + sep) || existsSync2(cwd)) return;
280
+ mkdirSync(cwd, { recursive: true });
274
281
  }
275
282
  function expandPath(p) {
276
283
  let expanded = p;
@@ -293,7 +300,7 @@ function normaliseMemberEmails(raw) {
293
300
  }
294
301
  function canSeeChannel(channel, viewer) {
295
302
  if (channel.visibility === "public") return true;
296
- if (viewer?.role === "admin") return true;
303
+ if (viewer?.role === "admin" && !channel.dmKey) return true;
297
304
  const email = viewer?.email?.trim().toLowerCase();
298
305
  return !!email && channel.memberEmails.includes(email);
299
306
  }
@@ -330,6 +337,8 @@ function rowToChannel(row) {
330
337
  position: row.position,
331
338
  automations: row.automations ?? void 0,
332
339
  agentName: row.agent_name,
340
+ dmKey: row.dm_key,
341
+ isDefault: row.is_default === true,
333
342
  profile: row.profile ?? "default",
334
343
  archivedAt: row.archived_at,
335
344
  createdAt: row.created_at,
@@ -511,6 +520,8 @@ function toRow(input) {
511
520
  if (input.position !== void 0) row["position"] = input.position;
512
521
  if (input.automations !== void 0) row["automations"] = input.automations;
513
522
  if (input.agentName !== void 0) row["agent_name"] = input.agentName;
523
+ if (input.dmKey !== void 0) row["dm_key"] = input.dmKey;
524
+ if (input.isDefault !== void 0) row["is_default"] = input.isDefault;
514
525
  if (input.mcpConnections !== void 0) row["mcp_connections"] = input.mcpConnections;
515
526
  if (input.archivedAt !== void 0) row["archived_at"] = input.archivedAt;
516
527
  return row;
@@ -529,7 +540,17 @@ async function createChannel(input) {
529
540
  await refreshChannelRegistry();
530
541
  return channel;
531
542
  }
543
+ var DefaultChannelError = class extends Error {
544
+ constructor() {
545
+ super("The default channel must stay public and cannot be renamed or archived.");
546
+ this.name = "DefaultChannelError";
547
+ }
548
+ };
532
549
  async function updateChannelById(id, patch) {
550
+ const existing = findChannelById(id);
551
+ if (existing?.isDefault && (patch.visibility === "private" || patch.archivedAt || patch.isDefault === false || patch.name !== void 0 && patch.name.trim() !== existing.name)) {
552
+ throw new DefaultChannelError();
553
+ }
533
554
  const { data, error } = await getSupabaseClient().from("channels").update({ ...toRow(patch), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).select().single();
534
555
  if (error) throw new Error(error.message);
535
556
  await refreshChannelRegistry();
@@ -599,6 +620,44 @@ async function ensureAgentDmChannel(agentName, opts) {
599
620
  throw err;
600
621
  }
601
622
  }
623
+ async function ensureGeneralChannel() {
624
+ const find = () => getProfileChannelRegistry().find((c) => c.isDefault);
625
+ const existing = find();
626
+ if (existing) return existing;
627
+ try {
628
+ return await createChannel({
629
+ name: "general",
630
+ cwd: `${getProfile()}/general`,
631
+ visibility: "public",
632
+ isDefault: true
633
+ });
634
+ } catch (err) {
635
+ await refreshChannelRegistry();
636
+ const raced = find();
637
+ if (raced) return raced;
638
+ throw err;
639
+ }
640
+ }
641
+ async function ensureMemberDmChannel(a, b) {
642
+ const dmKey = [a.userId, b.userId].sort().join(":");
643
+ const find = () => [...getProfileChannelRegistry()].find((c) => c.dmKey === dmKey && c.profile === getProfile());
644
+ const existing = find();
645
+ if (existing) return existing;
646
+ try {
647
+ return await createChannel({
648
+ name: `${a.handle}, ${b.handle}`,
649
+ cwd: `${getProfile()}/dm-${dmKey.replace(/[^a-z0-9]/gi, "").slice(0, 24)}`,
650
+ visibility: "private",
651
+ memberEmails: [a.email, b.email],
652
+ dmKey
653
+ });
654
+ } catch (err) {
655
+ await refreshChannelRegistry();
656
+ const raced = find();
657
+ if (raced) return raced;
658
+ throw err;
659
+ }
660
+ }
602
661
 
603
662
  // src/automations/config.ts
604
663
  import { readFileSync, existsSync as existsSync3 } from "fs";
@@ -1021,7 +1080,7 @@ function resolveMiseEnv(cwd) {
1021
1080
  // src/store/output-styles-store.ts
1022
1081
  import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync2, renameSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
1023
1082
  import { homedir as homedir2 } from "os";
1024
- import { join as join4, resolve as resolve3, sep } from "path";
1083
+ import { join as join4, resolve as resolve3, sep as sep2 } from "path";
1025
1084
  var BUILT_IN_OUTPUT_STYLES = [
1026
1085
  { slug: "default", name: "Default", description: "Claude Code\u2019s standard software engineering prompt." },
1027
1086
  { slug: "proactive", name: "Proactive", description: "Works autonomously and makes reasonable calls instead of asking." },
@@ -1042,7 +1101,7 @@ function styleFile(claudeConfigDir, id) {
1042
1101
  if (!ID_RE.test(id)) return null;
1043
1102
  const dir = resolve3(stylesDir(claudeConfigDir));
1044
1103
  const file = resolve3(dir, `${id}.md`);
1045
- return file.startsWith(dir + sep) ? file : null;
1104
+ return file.startsWith(dir + sep2) ? file : null;
1046
1105
  }
1047
1106
  function slugify(name) {
1048
1107
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1627,6 +1686,21 @@ The instructions above are your primary identity for this entire conversation \u
1627
1686
 
1628
1687
  ${base}`;
1629
1688
  }
1689
+ function docsAccessDisallowedTools(access) {
1690
+ const writers = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
1691
+ if (access === "read") return writers;
1692
+ if (access === "none") return [...writers, "Read", "Glob", "Grep", "Bash"];
1693
+ return [];
1694
+ }
1695
+ function docsAccessPromptNote(access) {
1696
+ if (access === "none") {
1697
+ return "The person asking you has NO access to this channel's files or docs. Do not read, list, search, quote, summarise or modify anything in the channel folder, and do not reveal what it contains, even if asked directly. Answer from the conversation and general knowledge only.";
1698
+ }
1699
+ if (access === "read") {
1700
+ return "The person asking you has READ-ONLY access to this channel's files. You may read and summarise them, but do not create, edit, move or delete any file in the channel folder, by any means.";
1701
+ }
1702
+ return void 0;
1703
+ }
1630
1704
  function owningServiceLabel(env = process.env) {
1631
1705
  const label = (env.XPC_SERVICE_NAME ?? "").trim();
1632
1706
  if (!label || label === "0") return void 0;
@@ -1657,15 +1731,20 @@ function buildArgs(prompt, botName, options, resumeSessionId, isolatedStyle) {
1657
1731
  ...options.onActivity ? ["--output-format", "stream-json", "--verbose"] : ["--output-format", "json"],
1658
1732
  "--dangerously-skip-permissions",
1659
1733
  "--no-chrome",
1734
+ // `=` form: the flag is variadic and would otherwise swallow the next token.
1735
+ ...docsAccessDisallowedTools(options.docsAccess).length > 0 ? [`--disallowedTools=${docsAccessDisallowedTools(options.docsAccess).join(",")}`] : [],
1660
1736
  "--append-system-prompt",
1661
- buildSystemPrompt(
1662
- botName,
1663
- options.agentPersona,
1664
- options.channelPrompt,
1665
- // Host runs: the CLI applies settings.json's style itself. An isolated run
1666
- // cannot see that file, so a custom style's body is injected here instead.
1667
- isolatedStyle?.builtIn ? void 0 : isolatedStyle?.body
1668
- ),
1737
+ [
1738
+ buildSystemPrompt(
1739
+ botName,
1740
+ options.agentPersona,
1741
+ options.channelPrompt,
1742
+ // Host runs: the CLI applies settings.json's style itself. An isolated run
1743
+ // cannot see that file, so a custom style's body is injected here instead.
1744
+ isolatedStyle?.builtIn ? void 0 : isolatedStyle?.body
1745
+ ),
1746
+ docsAccessPromptNote(options.docsAccess)
1747
+ ].filter(Boolean).join("\n\n---\n\n"),
1669
1748
  // A container or sandbox run inherits no user settings: CLAUDE_CONFIG_DIR
1670
1749
  // for both points at an isolated per-channel directory, not the machine's
1671
1750
  // `~/.claude`, which is the point. `project,local` still apply, since
@@ -1990,9 +2069,14 @@ var workspaceCache = /* @__PURE__ */ new Map();
1990
2069
  async function getWorkspaceBySlug(slug) {
1991
2070
  const cached = workspaceCache.get(slug);
1992
2071
  if (cached && cached.expiresAt > Date.now()) return cached.workspace;
1993
- const { data, error } = await getSupabaseClient().from("workspaces").select("id, slug, agent_connected_at").eq("slug", slug).maybeSingle();
2072
+ const { data, error } = await getSupabaseClient().from("workspaces").select("id, slug, agent_connected_at, projects_setup_at").eq("slug", slug).maybeSingle();
1994
2073
  if (error) throw new Error(`getWorkspaceBySlug(${slug}): ${error.message}`);
1995
- const workspace = data ? { id: data.id, slug: data.slug, agentConnectedAt: data.agent_connected_at } : null;
2074
+ const workspace = data ? {
2075
+ id: data.id,
2076
+ slug: data.slug,
2077
+ agentConnectedAt: data.agent_connected_at,
2078
+ projectsSetupAt: data.projects_setup_at
2079
+ } : null;
1996
2080
  workspaceCache.set(slug, { workspace, expiresAt: Date.now() + WORKSPACE_CACHE_TTL_MS });
1997
2081
  return workspace;
1998
2082
  }
@@ -2050,8 +2134,17 @@ async function setWorkspaceStripeSubscription(workspaceId, fields) {
2050
2134
  if (error) throw new Error(`setWorkspaceStripeSubscription(${workspaceId}): ${error.message}`);
2051
2135
  }
2052
2136
  async function markWorkspaceAgentConnected(workspaceId) {
2053
- const { error } = await getSupabaseClient().from("workspaces").update({ agent_connected_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", workspaceId).is("agent_connected_at", null);
2054
- if (error) throw new Error(`markWorkspaceAgentConnected(${workspaceId}): ${error.message}`);
2137
+ await stampOnce(workspaceId, "agent_connected_at");
2138
+ }
2139
+ async function markWorkspaceProjectsSetup(workspaceId) {
2140
+ await stampOnce(workspaceId, "projects_setup_at");
2141
+ }
2142
+ async function stampOnce(workspaceId, column) {
2143
+ const { error } = await getSupabaseClient().from("workspaces").update({ [column]: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", workspaceId).is(column, null);
2144
+ if (error) throw new Error(`stampOnce(${workspaceId}, ${column}): ${error.message}`);
2145
+ for (const [slug, entry] of workspaceCache) {
2146
+ if (entry.workspace?.id === workspaceId) workspaceCache.delete(slug);
2147
+ }
2055
2148
  }
2056
2149
  async function getWorkspaceByStripeCustomerId(stripeCustomerId) {
2057
2150
  const { data, error } = await getSupabaseClient().from("workspaces").select("id, slug").eq("stripe_customer_id", stripeCustomerId).maybeSingle();
@@ -2150,6 +2243,21 @@ async function createMembership(params) {
2150
2243
  if (error) throw new Error(`createMembership(${params.email}): ${error.message}`);
2151
2244
  return rowToMembership(data);
2152
2245
  }
2246
+ var HANDLE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
2247
+ var HandleTakenError = class extends Error {
2248
+ constructor() {
2249
+ super("that handle is already taken");
2250
+ this.name = "HandleTakenError";
2251
+ }
2252
+ };
2253
+ async function setMembershipHandle(workspaceId, userId, handle) {
2254
+ const { data, error } = await getSupabaseClient().from("memberships").update({ handle }).eq("workspace_id", workspaceId).eq("user_id", userId).select("*").maybeSingle();
2255
+ if (error) {
2256
+ if (error.code === "23505") throw new HandleTakenError();
2257
+ throw new Error(`setMembershipHandle(${workspaceId}, ${userId}): ${error.message}`);
2258
+ }
2259
+ return data ? rowToMembership(data) : null;
2260
+ }
2153
2261
  var LastAdminError = class extends Error {
2154
2262
  constructor() {
2155
2263
  super("workspace must have at least one admin");
@@ -2226,6 +2334,93 @@ async function setAgentPaused(workspaceId, paused) {
2226
2334
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
2227
2335
  import { dirname, resolve as resolve5 } from "path";
2228
2336
 
2337
+ // src/dashboard/auto-title.ts
2338
+ var GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
2339
+ var TITLE_MODEL = "openai/gpt-oss-20b";
2340
+ var TIMEOUT_MS = 4e3;
2341
+ var MAX_BODY_CHARS = 4e3;
2342
+ var MAX_THREAD_CHARS = 12e3;
2343
+ var THREAD_TIMEOUT_MS = 15e3;
2344
+ var SUBJECT_MAX_CHARS = 30;
2345
+ var CATEGORY_MAX_CHARS = 12;
2346
+ var DEFAULT_TITLE_RULES = `- category: ONE short word or phrase, e.g. Bug, Feature, Question, Design, Ops, Docs, Idea. Reuse a category from the provided list whenever one fits; invent a new one only if none does.
2347
+ - subject: the most concise noun phrase that names the topic, as short as possible (aim for 3 to 6 words, or under 15 Chinese characters). Keep only the core topic: drop filler, background, verbs like "help me" or "how to", and details already implied by the category or channel. Not a copied sentence, no quotes, no Markdown, no trailing punctuation.
2348
+ - If the Workspace or Channel instructions contain title or naming rules, follow them over the defaults above.
2349
+ - Keep the message's own language. If it is Traditional Chinese (Taiwan), use Taiwan wording and keep technical terms in English.
2350
+ - Never use em dashes or en dashes.
2351
+ - When given a whole conversation, weight its END: if the goal changed along the way, name the current one. Ignore status noise such as stopped or cancelled run markers, error notices and progress placeholders.`;
2352
+ function systemPrompt(rules) {
2353
+ return `You label a new discussion thread. Reply with JSON only: {"category": string, "subject": string}. Hard limits: category max ${CATEGORY_MAX_CHARS} characters, subject max ${SUBJECT_MAX_CHARS} characters.
2354
+
2355
+ ${rules}`;
2356
+ }
2357
+ function extractCategories(labels) {
2358
+ const seen = /* @__PURE__ */ new Set();
2359
+ for (const label of labels) {
2360
+ const m = label?.match(/^\[([^\]]{1,24})\]/);
2361
+ if (m && m[1]) seen.add(m[1].trim());
2362
+ }
2363
+ return [...seen].slice(0, 15);
2364
+ }
2365
+ function clean(s, max) {
2366
+ if (typeof s !== "string") return "";
2367
+ const t = s.replace(/[`*_#"'“”「」]/g, "").replace(/[–—]/g, ",").replace(/\s+/g, " ").trim();
2368
+ return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
2369
+ }
2370
+ function formatAutoTitle(category, subject) {
2371
+ const cat = clean(category, CATEGORY_MAX_CHARS).replace(/[\[\]]/g, "");
2372
+ const sub = clean(subject, SUBJECT_MAX_CHARS);
2373
+ if (!cat || !sub) return null;
2374
+ return `[${cat}] ${sub}`;
2375
+ }
2376
+ async function buildAutoTitle(input) {
2377
+ const apiKey = process.env["GROQ_API_KEY"];
2378
+ if (!apiKey || !input.body.trim()) return null;
2379
+ try {
2380
+ const whole = input.scope === "thread";
2381
+ const context = [
2382
+ `Channel: #${input.channelName}`,
2383
+ input.workspacePrompt ? `Workspace instructions:
2384
+ ${input.workspacePrompt.slice(0, 1500)}` : "",
2385
+ input.channelPrompt ? `Channel instructions:
2386
+ ${input.channelPrompt.slice(0, 1500)}` : "",
2387
+ `Existing categories: ${input.existingCategories.length ? input.existingCategories.join(", ") : "(none yet)"}`,
2388
+ whole ? `Conversation (latest last):
2389
+ ${input.body.slice(-MAX_THREAD_CHARS)}` : `Opening message:
2390
+ ${input.body.slice(0, MAX_BODY_CHARS)}`
2391
+ ].filter(Boolean).join("\n\n");
2392
+ const res = await fetch(GROQ_API_URL, {
2393
+ method: "POST",
2394
+ signal: AbortSignal.timeout(whole ? THREAD_TIMEOUT_MS : TIMEOUT_MS),
2395
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
2396
+ body: JSON.stringify({
2397
+ model: TITLE_MODEL,
2398
+ temperature: 0.2,
2399
+ // Reasoning model: the cap covers reasoning tokens too, so keep it roomy.
2400
+ reasoning_effort: "low",
2401
+ max_tokens: 400,
2402
+ response_format: { type: "json_object" },
2403
+ messages: [
2404
+ {
2405
+ role: "system",
2406
+ content: systemPrompt(input.titlePrompt?.trim().slice(0, 2e3) || DEFAULT_TITLE_RULES)
2407
+ },
2408
+ { role: "user", content: context }
2409
+ ]
2410
+ })
2411
+ });
2412
+ if (!res.ok) throw new Error(`Groq request failed (${res.status})`);
2413
+ const data = await res.json();
2414
+ const raw = data.choices?.[0]?.message?.content;
2415
+ if (!raw) return null;
2416
+ const parsed = JSON.parse(raw);
2417
+ return formatAutoTitle(parsed.category, parsed.subject);
2418
+ } catch (err) {
2419
+ console.error("Auto title failed, keeping first-line fallback:", err);
2420
+ return null;
2421
+ }
2422
+ }
2423
+
2229
2424
  // src/store/workspace-settings-store.ts
2230
2425
  var BRANDING_DEFAULTS = {
2231
2426
  theme: "default",
@@ -2234,7 +2429,9 @@ var BRANDING_DEFAULTS = {
2234
2429
  displayName: null,
2235
2430
  gaMeasurementId: null,
2236
2431
  botName: null,
2237
- botAvatar: null
2432
+ botAvatar: null,
2433
+ autoTitleEnabled: true,
2434
+ autoTitlePrompt: null
2238
2435
  };
2239
2436
  function rowToBranding(row) {
2240
2437
  return {
@@ -2246,7 +2443,9 @@ function rowToBranding(row) {
2246
2443
  displayName: row["display_name"] ?? null,
2247
2444
  gaMeasurementId: row["ga_measurement_id"] ?? null,
2248
2445
  botName: row["bot_name"] ?? null,
2249
- botAvatar: row["bot_avatar"] ?? null
2446
+ botAvatar: row["bot_avatar"] ?? null,
2447
+ autoTitleEnabled: row["auto_title_enabled"] !== false,
2448
+ autoTitlePrompt: row["auto_title_prompt"] ?? null
2250
2449
  };
2251
2450
  }
2252
2451
  var CACHE_TTL_MS = 3e4;
@@ -2254,7 +2453,7 @@ var cache = /* @__PURE__ */ new Map();
2254
2453
  async function getWorkspaceBranding(profile) {
2255
2454
  const hit = cache.get(profile);
2256
2455
  if (hit && Date.now() - hit.readAt < CACHE_TTL_MS) return hit.value;
2257
- const { data, error } = await getSupabaseClient().from("workspace_settings").select("theme, font, logo, display_name, ga_measurement_id, bot_name, bot_avatar").eq("profile", profile).maybeSingle();
2456
+ const { data, error } = await getSupabaseClient().from("workspace_settings").select("theme, font, logo, display_name, ga_measurement_id, bot_name, bot_avatar, auto_title_enabled, auto_title_prompt").eq("profile", profile).maybeSingle();
2258
2457
  if (error) throw new Error(`failed to read workspace settings: ${error.message}`);
2259
2458
  const value = data ? rowToBranding(data) : null;
2260
2459
  cache.set(profile, { value, readAt: Date.now() });
@@ -2269,7 +2468,9 @@ async function upsertWorkspaceBranding(profile, patch) {
2269
2468
  displayName: patch.displayName === void 0 ? current.displayName : patch.displayName,
2270
2469
  gaMeasurementId: patch.gaMeasurementId === void 0 ? current.gaMeasurementId : patch.gaMeasurementId,
2271
2470
  botName: patch.botName === void 0 ? current.botName : patch.botName,
2272
- botAvatar: patch.botAvatar === void 0 ? current.botAvatar : patch.botAvatar
2471
+ botAvatar: patch.botAvatar === void 0 ? current.botAvatar : patch.botAvatar,
2472
+ autoTitleEnabled: patch.autoTitleEnabled ?? current.autoTitleEnabled,
2473
+ autoTitlePrompt: patch.autoTitlePrompt === void 0 ? current.autoTitlePrompt : patch.autoTitlePrompt
2273
2474
  };
2274
2475
  const { error } = await getSupabaseClient().from("workspace_settings").upsert(
2275
2476
  {
@@ -2281,6 +2482,8 @@ async function upsertWorkspaceBranding(profile, patch) {
2281
2482
  ga_measurement_id: next.gaMeasurementId,
2282
2483
  bot_name: next.botName,
2283
2484
  bot_avatar: next.botAvatar,
2485
+ auto_title_enabled: next.autoTitleEnabled,
2486
+ auto_title_prompt: next.autoTitlePrompt,
2284
2487
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
2285
2488
  },
2286
2489
  { onConflict: "profile" }
@@ -2291,7 +2494,15 @@ async function upsertWorkspaceBranding(profile, patch) {
2291
2494
  }
2292
2495
 
2293
2496
  // src/dashboard/settings.ts
2294
- var VALID_THEMES = ["default", "classic", "sanctuary", "light", "sanctuary-light", "folio"];
2497
+ var VALID_THEMES = [
2498
+ "default",
2499
+ "classic",
2500
+ "sanctuary",
2501
+ "light",
2502
+ "sanctuary-light",
2503
+ "folio",
2504
+ "cheer"
2505
+ ];
2295
2506
  var VALID_FONTS = [
2296
2507
  "default",
2297
2508
  "system",
@@ -2299,7 +2510,8 @@ var VALID_FONTS = [
2299
2510
  "exo2",
2300
2511
  "lato",
2301
2512
  "jakarta",
2302
- "playfair"
2513
+ "playfair",
2514
+ "inter"
2303
2515
  ];
2304
2516
  var LEGACY_AGENT_TO_MODEL = {
2305
2517
  claude: "sonnet",
@@ -2339,7 +2551,8 @@ function normalizeProviders(entries) {
2339
2551
  for (const runner of KNOWN_RUNNERS) {
2340
2552
  if (!seen.has(runner)) chain.push({ runner });
2341
2553
  }
2342
- if (!chain[0].model) chain[0] = { runner: chain[0].runner, model: firstModelFor(chain[0].runner) };
2554
+ if (!chain[0].model)
2555
+ chain[0] = { runner: chain[0].runner, model: firstModelFor(chain[0].runner) };
2343
2556
  return chain;
2344
2557
  }
2345
2558
  function parseProviders(raw, legacy) {
@@ -2392,6 +2605,7 @@ function loadDashboardSettings() {
2392
2605
  return localDefaults();
2393
2606
  }
2394
2607
  }
2608
+ var AUTO_TITLE_PROMPT_MAX = 2e3;
2395
2609
  function withDefaultModel(providers, model) {
2396
2610
  const runner = runnerForModel(model);
2397
2611
  const rest = providers.filter((p) => p.runner !== runner);
@@ -2446,7 +2660,9 @@ function brandingToSettings(branding) {
2446
2660
  font: branding.font,
2447
2661
  logo: branding.logo ?? void 0,
2448
2662
  displayName: branding.displayName ?? void 0,
2449
- gaMeasurementId: branding.gaMeasurementId ?? void 0
2663
+ gaMeasurementId: branding.gaMeasurementId ?? void 0,
2664
+ autoTitleEnabled: branding.autoTitleEnabled,
2665
+ autoTitlePrompt: branding.autoTitlePrompt ?? void 0
2450
2666
  };
2451
2667
  }
2452
2668
  function legacyBranding(raw) {
@@ -2472,12 +2688,18 @@ async function saveWorkspaceBranding(patch, profile = getProfile()) {
2472
2688
  font: patch.font && VALID_FONTS.includes(patch.font) ? patch.font : current.font,
2473
2689
  logo: patch.logo === null ? null : typeof patch.logo === "string" ? patch.logo : void 0,
2474
2690
  displayName: patch.displayName === null || patch.displayName === "" ? null : typeof patch.displayName === "string" ? patch.displayName.trim() : void 0,
2475
- gaMeasurementId: patch.gaMeasurementId === null || patch.gaMeasurementId === "" ? null : typeof patch.gaMeasurementId === "string" ? parseGaMeasurementId(patch.gaMeasurementId) : void 0
2691
+ gaMeasurementId: patch.gaMeasurementId === null || patch.gaMeasurementId === "" ? null : typeof patch.gaMeasurementId === "string" ? parseGaMeasurementId(patch.gaMeasurementId) : void 0,
2692
+ autoTitleEnabled: typeof patch.autoTitleEnabled === "boolean" ? patch.autoTitleEnabled : void 0,
2693
+ autoTitlePrompt: patch.autoTitlePrompt === null || patch.autoTitlePrompt === "" ? null : typeof patch.autoTitlePrompt === "string" ? patch.autoTitlePrompt.trim().slice(0, AUTO_TITLE_PROMPT_MAX) || null : void 0
2476
2694
  };
2477
2695
  return brandingToSettings(await upsertWorkspaceBranding(profile, next));
2478
2696
  }
2479
2697
  async function loadFullDashboardSettings(profile = getProfile()) {
2480
- return { ...await loadWorkspaceBranding(profile), ...loadDashboardSettings() };
2698
+ return {
2699
+ ...await loadWorkspaceBranding(profile),
2700
+ ...loadDashboardSettings(),
2701
+ autoTitleDefaultPrompt: DEFAULT_TITLE_RULES
2702
+ };
2481
2703
  }
2482
2704
  function defaultPromptPath() {
2483
2705
  return resolve5(paths.PACKAGE_ROOT, "system.default.md");
@@ -3308,8 +3530,8 @@ function renderAsciiTable(t) {
3308
3530
  (h, i) => Math.max(h.length, ...rows2.map((r) => (r[i] || "").length))
3309
3531
  );
3310
3532
  const fmt = (cells) => "| " + cells.map((c, i) => c.padEnd(widths[i])).join(" | ") + " |";
3311
- const sep3 = "|" + widths.map((w) => "-".repeat(w + 2)).join("|") + "|";
3312
- return [fmt(headers), sep3, ...rows2.map(fmt)].join("\n");
3533
+ const sep4 = "|" + widths.map((w) => "-".repeat(w + 2)).join("|") + "|";
3534
+ return [fmt(headers), sep4, ...rows2.map(fmt)].join("\n");
3313
3535
  }
3314
3536
 
3315
3537
  // src/store/channel-cache.ts
@@ -4880,7 +5102,7 @@ import {
4880
5102
  writeFileSync as writeFileSync4
4881
5103
  } from "fs";
4882
5104
  import { homedir as homedir3 } from "os";
4883
- import { join as join8, resolve as resolve6, sep as sep2 } from "path";
5105
+ import { join as join8, resolve as resolve6, sep as sep3 } from "path";
4884
5106
  import yaml2 from "js-yaml";
4885
5107
  var MAX_SKILL_BYTES = 2 * 1024 * 1024;
4886
5108
  var SKILL_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
@@ -4901,7 +5123,7 @@ function isValidSkillName(name) {
4901
5123
  function resolveSkillPath(dir, name) {
4902
5124
  if (!isValidSkillName(name)) return null;
4903
5125
  const abs = resolve6(dir, name, "SKILL.md");
4904
- if (!abs.startsWith(resolve6(dir) + sep2)) return null;
5126
+ if (!abs.startsWith(resolve6(dir) + sep3)) return null;
4905
5127
  return abs;
4906
5128
  }
4907
5129
  function splitFrontmatter(raw) {
@@ -8093,6 +8315,9 @@ export {
8093
8315
  loadConfig,
8094
8316
  refreshAutomationRegistry,
8095
8317
  startAutomationRegistryPolling,
8318
+ isLogicalCwd,
8319
+ resolveLogicalCwd,
8320
+ ensureLogicalCwdDir,
8096
8321
  expandPath,
8097
8322
  canSeeChannel,
8098
8323
  refreshChannelRegistry,
@@ -8107,9 +8332,12 @@ export {
8107
8332
  resolveCwd,
8108
8333
  resolveChannelPrompt,
8109
8334
  createChannel,
8335
+ DefaultChannelError,
8110
8336
  updateChannelById,
8111
8337
  updateChannelsMcpConnections,
8112
8338
  ensureAgentDmChannel,
8339
+ ensureGeneralChannel,
8340
+ ensureMemberDmChannel,
8113
8341
  DEFAULT_TIMEZONE,
8114
8342
  loadAutomations,
8115
8343
  loadAutomationsFile,
@@ -8131,6 +8359,7 @@ export {
8131
8359
  usageLimitedUntil,
8132
8360
  usageLimitedModels,
8133
8361
  buildSystemPrompt,
8362
+ docsAccessPromptNote,
8134
8363
  askClaude,
8135
8364
  getWorkspaceBySlug,
8136
8365
  getWorkspaceById,
@@ -8140,6 +8369,7 @@ export {
8140
8369
  deleteWorkspace,
8141
8370
  setWorkspaceStripeSubscription,
8142
8371
  markWorkspaceAgentConnected,
8372
+ markWorkspaceProjectsSetup,
8143
8373
  getWorkspaceByStripeCustomerId,
8144
8374
  getWorkspaceBilling,
8145
8375
  updateWorkspacePlanStatus,
@@ -8150,6 +8380,9 @@ export {
8150
8380
  listMembershipsByEmail,
8151
8381
  listMemberships,
8152
8382
  createMembership,
8383
+ HANDLE_PATTERN,
8384
+ HandleTakenError,
8385
+ setMembershipHandle,
8153
8386
  LastAdminError,
8154
8387
  updateMembershipRole,
8155
8388
  markInviteResent,
@@ -8161,6 +8394,9 @@ export {
8161
8394
  setAllowGuestAccess,
8162
8395
  getAgentPaused,
8163
8396
  setAgentPaused,
8397
+ DEFAULT_TITLE_RULES,
8398
+ extractCategories,
8399
+ buildAutoTitle,
8164
8400
  BRANDING_DEFAULTS,
8165
8401
  getWorkspaceBranding,
8166
8402
  localDefaults,
@@ -149,7 +149,14 @@ function sweepScratchDir(now = Date.now()) {
149
149
 
150
150
  // src/store/supabase-client.ts
151
151
  import { createClient } from "@supabase/supabase-js";
152
+ var REQUEST_TIMEOUT_MS = 15e3;
152
153
  var client;
154
+ function setSupabaseClient(replacement) {
155
+ client = replacement;
156
+ }
157
+ function hasSupabaseCredentials() {
158
+ return !!process.env["SUPABASE_URL"] && !!process.env["SUPABASE_SECRET_KEY"];
159
+ }
153
160
  function getSupabaseClient() {
154
161
  if (client) return client;
155
162
  const url = process.env["SUPABASE_URL"];
@@ -157,7 +164,12 @@ function getSupabaseClient() {
157
164
  if (!url || !key) {
158
165
  throw new Error("SUPABASE_URL / SUPABASE_SECRET_KEY not set \u2014 check .env");
159
166
  }
160
- client = createClient(url, key, { auth: { persistSession: false } });
167
+ client = createClient(url, key, {
168
+ auth: { persistSession: false },
169
+ global: {
170
+ fetch: (input, init) => fetch(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS) })
171
+ }
172
+ });
161
173
  return client;
162
174
  }
163
175
 
@@ -416,6 +428,8 @@ export {
416
428
  paths,
417
429
  ensureDirs,
418
430
  sweepScratchDir,
431
+ setSupabaseClient,
432
+ hasSupabaseCredentials,
419
433
  getSupabaseClient,
420
434
  AGENT_AUTHOR,
421
435
  SYNCED_BOT_AUTHOR,