flyspace-cli 0.1.0 → 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.
@@ -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,7 +265,20 @@ 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
+ function isLogicalCwd(p) {
270
+ return /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/.test(p);
271
+ }
272
+ function logicalRoot() {
273
+ return process.env["FLYSPACE_ROOT"] || resolve2(homedir(), "Flyspace");
274
+ }
275
+ function resolveLogicalCwd(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 });
281
+ }
269
282
  function expandPath(p) {
270
283
  let expanded = p;
271
284
  if (expanded === "~") {
@@ -287,7 +300,7 @@ function normaliseMemberEmails(raw) {
287
300
  }
288
301
  function canSeeChannel(channel, viewer) {
289
302
  if (channel.visibility === "public") return true;
290
- if (viewer?.role === "admin") return true;
303
+ if (viewer?.role === "admin" && !channel.dmKey) return true;
291
304
  const email = viewer?.email?.trim().toLowerCase();
292
305
  return !!email && channel.memberEmails.includes(email);
293
306
  }
@@ -296,7 +309,7 @@ function rowToChannel(row) {
296
309
  id: row.id,
297
310
  name: row.name,
298
311
  categoryId: row.category_id,
299
- cwd: expandPath(row.cwd),
312
+ cwd: isLogicalCwd(row.cwd) ? resolveLogicalCwd(row.cwd) : expandPath(row.cwd),
300
313
  externalChannelId: row.external_channel_id,
301
314
  model: row.model,
302
315
  effort: row.effort && isEffort(row.effort) ? row.effort : null,
@@ -324,6 +337,8 @@ function rowToChannel(row) {
324
337
  position: row.position,
325
338
  automations: row.automations ?? void 0,
326
339
  agentName: row.agent_name,
340
+ dmKey: row.dm_key,
341
+ isDefault: row.is_default === true,
327
342
  profile: row.profile ?? "default",
328
343
  archivedAt: row.archived_at,
329
344
  createdAt: row.created_at,
@@ -487,7 +502,7 @@ function resolveEffort(explicitEffort, channelId, fallback) {
487
502
  function toRow(input) {
488
503
  const row = {};
489
504
  if (input.name !== void 0) row["name"] = input.name;
490
- if (input.cwd !== void 0) row["cwd"] = expandPath(input.cwd);
505
+ if (input.cwd !== void 0) row["cwd"] = isLogicalCwd(input.cwd) ? input.cwd : expandPath(input.cwd);
491
506
  if (input.categoryId !== void 0) row["category_id"] = input.categoryId;
492
507
  if (input.externalChannelId !== void 0) row["external_channel_id"] = input.externalChannelId;
493
508
  if (input.model !== void 0) row["model"] = input.model;
@@ -505,6 +520,8 @@ function toRow(input) {
505
520
  if (input.position !== void 0) row["position"] = input.position;
506
521
  if (input.automations !== void 0) row["automations"] = input.automations;
507
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;
508
525
  if (input.mcpConnections !== void 0) row["mcp_connections"] = input.mcpConnections;
509
526
  if (input.archivedAt !== void 0) row["archived_at"] = input.archivedAt;
510
527
  return row;
@@ -513,7 +530,7 @@ async function createChannel(input) {
513
530
  const { data, error } = await getSupabaseClient().from("channels").insert({ ...toRow(input), profile: getProfile() }).select().single();
514
531
  if (error) throw new Error(error.message);
515
532
  const channel = rowToChannel(data);
516
- if (channel.cwd && !isCwdUsable(channel.cwd)) {
533
+ if (channel.cwd && !isLogicalCwd(input.cwd) && !isCwdUsable(channel.cwd)) {
517
534
  try {
518
535
  mkdirSync(channel.cwd, { recursive: true });
519
536
  } catch (err) {
@@ -523,7 +540,17 @@ async function createChannel(input) {
523
540
  await refreshChannelRegistry();
524
541
  return channel;
525
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
+ };
526
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
+ }
527
554
  const { data, error } = await getSupabaseClient().from("channels").update({ ...toRow(patch), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).select().single();
528
555
  if (error) throw new Error(error.message);
529
556
  await refreshChannelRegistry();
@@ -593,6 +620,44 @@ async function ensureAgentDmChannel(agentName, opts) {
593
620
  throw err;
594
621
  }
595
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
+ }
596
661
 
597
662
  // src/automations/config.ts
598
663
  import { readFileSync, existsSync as existsSync3 } from "fs";
@@ -696,8 +761,8 @@ function loadEffectiveAutomations() {
696
761
 
697
762
  // src/claude/agent.ts
698
763
  import { spawn } from "child_process";
699
- import { existsSync as existsSync7, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
700
- import { resolve as resolve3 } from "path";
764
+ import { existsSync as existsSync8, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
765
+ import { resolve as resolve4 } from "path";
701
766
  import { randomUUID } from "crypto";
702
767
 
703
768
  // src/claude/container.ts
@@ -1012,6 +1077,181 @@ function resolveMiseEnv(cwd) {
1012
1077
  }
1013
1078
  }
1014
1079
 
1080
+ // src/store/output-styles-store.ts
1081
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync2, renameSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
1082
+ import { homedir as homedir2 } from "os";
1083
+ import { join as join4, resolve as resolve3, sep as sep2 } from "path";
1084
+ var BUILT_IN_OUTPUT_STYLES = [
1085
+ { slug: "default", name: "Default", description: "Claude Code\u2019s standard software engineering prompt." },
1086
+ { slug: "proactive", name: "Proactive", description: "Works autonomously and makes reasonable calls instead of asking." },
1087
+ { slug: "concise", name: "Concise", description: "The shortest answer that fully addresses the question." },
1088
+ { slug: "explanatory", name: "Explanatory", description: "Walks through the reasoning and trade-offs, not just the result." },
1089
+ { slug: "learning", name: "Learning", description: "Collaborative mode that asks you to write small pieces of code." }
1090
+ ];
1091
+ var OutputStyleConflictError = class extends Error {
1092
+ };
1093
+ var ID_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
1094
+ function resolveDir(claudeConfigDir) {
1095
+ return claudeConfigDir || join4(homedir2(), ".claude");
1096
+ }
1097
+ function stylesDir(claudeConfigDir) {
1098
+ return join4(resolveDir(claudeConfigDir), "output-styles");
1099
+ }
1100
+ function styleFile(claudeConfigDir, id) {
1101
+ if (!ID_RE.test(id)) return null;
1102
+ const dir = resolve3(stylesDir(claudeConfigDir));
1103
+ const file = resolve3(dir, `${id}.md`);
1104
+ return file.startsWith(dir + sep2) ? file : null;
1105
+ }
1106
+ function slugify(name) {
1107
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1108
+ }
1109
+ function unquote(raw) {
1110
+ const v = raw.trim();
1111
+ if (v.startsWith('"')) {
1112
+ try {
1113
+ return JSON.parse(v);
1114
+ } catch {
1115
+ return v.slice(1, v.endsWith('"') ? -1 : void 0);
1116
+ }
1117
+ }
1118
+ if (v.startsWith("'") && v.endsWith("'") && v.length > 1) return v.slice(1, -1).replaceAll("''", "'");
1119
+ return v;
1120
+ }
1121
+ function parseStyle(text, fallbackName) {
1122
+ const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/.exec(text);
1123
+ if (!m) return { name: fallbackName, description: "", body: text.trim(), extra: [] };
1124
+ let name = "";
1125
+ let description = "";
1126
+ const extra = [];
1127
+ for (const line of m[1].split(/\r?\n/)) {
1128
+ const kv = /^(name|description):(.*)$/.exec(line);
1129
+ if (kv) {
1130
+ if (kv[1] === "name") name = unquote(kv[2]);
1131
+ else description = unquote(kv[2]);
1132
+ } else if (line.trim()) extra.push(line);
1133
+ }
1134
+ return { name: name || fallbackName, description, body: m[2].trim(), extra };
1135
+ }
1136
+ function serialize(p) {
1137
+ const head = [`name: ${JSON.stringify(p.name)}`, `description: ${JSON.stringify(p.description)}`, ...p.extra];
1138
+ return `---
1139
+ ${head.join("\n")}
1140
+ ---
1141
+
1142
+ ${p.body.trim()}
1143
+ `;
1144
+ }
1145
+ function atomicWrite(file, content) {
1146
+ mkdirSync5(resolve3(file, ".."), { recursive: true });
1147
+ const tmp = `${file}.${process.pid}.tmp`;
1148
+ writeFileSync2(tmp, content);
1149
+ renameSync(tmp, file);
1150
+ }
1151
+ function readStyle(claudeConfigDir, id) {
1152
+ const file = styleFile(claudeConfigDir, id);
1153
+ if (!file || !existsSync7(file)) return null;
1154
+ const stat = statSync2(file);
1155
+ const p = parseStyle(readFileSync2(file, "utf8"), id);
1156
+ return {
1157
+ id,
1158
+ name: p.name,
1159
+ description: p.description,
1160
+ body: p.body,
1161
+ createdAt: stat.birthtime.toISOString(),
1162
+ updatedAt: stat.mtime.toISOString()
1163
+ };
1164
+ }
1165
+ function listOutputStyles(claudeConfigDir) {
1166
+ const dir = stylesDir(claudeConfigDir);
1167
+ if (!existsSync7(dir)) return [];
1168
+ return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => readStyle(claudeConfigDir, f.slice(0, -3))).filter((s) => !!s).sort((a, b) => a.name.localeCompare(b.name));
1169
+ }
1170
+ function assertNameFree(claudeConfigDir, name, exceptId) {
1171
+ const lower = name.toLowerCase();
1172
+ const taken = BUILT_IN_OUTPUT_STYLES.some((s) => s.name.toLowerCase() === lower) || listOutputStyles(claudeConfigDir).some((s) => s.id !== exceptId && s.name.toLowerCase() === lower);
1173
+ if (taken) throw new OutputStyleConflictError(`an output style named "${name}" already exists`);
1174
+ }
1175
+ function createOutputStyle(claudeConfigDir, params) {
1176
+ const id = slugify(params.name);
1177
+ if (!id) throw new Error("name must contain letters or digits");
1178
+ assertNameFree(claudeConfigDir, params.name);
1179
+ const file = styleFile(claudeConfigDir, id);
1180
+ if (existsSync7(file) || BUILT_IN_OUTPUT_STYLES.some((s) => s.slug === id)) {
1181
+ throw new OutputStyleConflictError(`an output style named "${params.name}" already exists`);
1182
+ }
1183
+ atomicWrite(file, serialize({ ...params, extra: [] }));
1184
+ return readStyle(claudeConfigDir, id);
1185
+ }
1186
+ function updateOutputStyle(claudeConfigDir, id, patch) {
1187
+ const file = styleFile(claudeConfigDir, id);
1188
+ if (!file || !existsSync7(file)) return null;
1189
+ const current = parseStyle(readFileSync2(file, "utf8"), id);
1190
+ if (patch.name !== void 0 && patch.name !== current.name) assertNameFree(claudeConfigDir, patch.name, id);
1191
+ const next = { ...current, ...patch };
1192
+ atomicWrite(file, serialize(next));
1193
+ if (next.name !== current.name && readSettings(claudeConfigDir)["outputStyle"] === current.name) {
1194
+ writeDefaultName(claudeConfigDir, next.name);
1195
+ }
1196
+ return readStyle(claudeConfigDir, id);
1197
+ }
1198
+ function deleteOutputStyle(claudeConfigDir, id) {
1199
+ const file = styleFile(claudeConfigDir, id);
1200
+ if (!file || !existsSync7(file)) return false;
1201
+ const name = parseStyle(readFileSync2(file, "utf8"), id).name;
1202
+ rmSync(file);
1203
+ if (readSettings(claudeConfigDir)["outputStyle"] === name) writeDefaultName(claudeConfigDir, void 0);
1204
+ return true;
1205
+ }
1206
+ function settingsFile(claudeConfigDir) {
1207
+ return join4(resolveDir(claudeConfigDir), "settings.json");
1208
+ }
1209
+ function readSettings(claudeConfigDir) {
1210
+ const file = settingsFile(claudeConfigDir);
1211
+ if (!existsSync7(file)) return {};
1212
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
1213
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1214
+ throw new Error(`${file} is not a JSON object`);
1215
+ }
1216
+ return parsed;
1217
+ }
1218
+ function writeDefaultName(claudeConfigDir, name) {
1219
+ const settings = readSettings(claudeConfigDir);
1220
+ if (name === void 0) delete settings["outputStyle"];
1221
+ else settings["outputStyle"] = name;
1222
+ atomicWrite(settingsFile(claudeConfigDir), `${JSON.stringify(settings, null, 2)}
1223
+ `);
1224
+ }
1225
+ function resolveSelectedOutputStyle(claudeConfigDir) {
1226
+ let value;
1227
+ try {
1228
+ value = readSettings(claudeConfigDir)["outputStyle"];
1229
+ } catch {
1230
+ return void 0;
1231
+ }
1232
+ if (typeof value !== "string") return void 0;
1233
+ const builtIn = BUILT_IN_OUTPUT_STYLES.find((s) => s.name.toLowerCase() === value.toLowerCase());
1234
+ if (builtIn) {
1235
+ return builtIn.slug === "default" ? void 0 : { id: builtIn.slug, name: builtIn.name, builtIn: true };
1236
+ }
1237
+ const custom = listOutputStyles(claudeConfigDir).find((s) => s.name === value);
1238
+ return custom ? { id: custom.id, name: custom.name, builtIn: false, body: custom.body } : void 0;
1239
+ }
1240
+ function getOutputStyleDefault(claudeConfigDir) {
1241
+ return resolveSelectedOutputStyle(claudeConfigDir)?.id ?? "default";
1242
+ }
1243
+ function setOutputStyleDefault(claudeConfigDir, id) {
1244
+ const builtIn = BUILT_IN_OUTPUT_STYLES.find((s) => s.slug === id);
1245
+ if (builtIn) {
1246
+ writeDefaultName(claudeConfigDir, builtIn.slug === "default" ? void 0 : builtIn.name);
1247
+ return true;
1248
+ }
1249
+ const style = readStyle(claudeConfigDir, id);
1250
+ if (!style) return false;
1251
+ writeDefaultName(claudeConfigDir, style.name);
1252
+ return true;
1253
+ }
1254
+
1015
1255
  // src/claude/models.ts
1016
1256
  var MODEL_OPTIONS = [
1017
1257
  { value: "opus", label: "Opus", provider: "claude" },
@@ -1408,14 +1648,14 @@ function getMaxDurationMs(streaming) {
1408
1648
  }
1409
1649
  function loadSystemPrompt() {
1410
1650
  const customPath = paths.PROMPT_FILE;
1411
- const defaultPath = resolve3(paths.PACKAGE_ROOT, "system.default.md");
1412
- const filePath = existsSync7(customPath) ? customPath : defaultPath;
1413
- return readFileSync2(filePath, "utf-8").trim();
1651
+ const defaultPath = resolve4(paths.PACKAGE_ROOT, "system.default.md");
1652
+ const filePath = existsSync8(customPath) ? customPath : defaultPath;
1653
+ return readFileSync3(filePath, "utf-8").trim();
1414
1654
  }
1415
1655
  function loadCapabilities() {
1416
- const capabilitiesPath = resolve3(paths.PACKAGE_ROOT, "system.capabilities.md");
1417
- if (!existsSync7(capabilitiesPath)) return "";
1418
- return readFileSync2(capabilitiesPath, "utf-8").trim();
1656
+ const capabilitiesPath = resolve4(paths.PACKAGE_ROOT, "system.capabilities.md");
1657
+ if (!existsSync8(capabilitiesPath)) return "";
1658
+ return readFileSync3(capabilitiesPath, "utf-8").trim();
1419
1659
  }
1420
1660
  function buildSystemPrompt(botName, agentPersona, channelPrompt, outputStyle) {
1421
1661
  const prompt = loadSystemPrompt().replaceAll("{{botName}}", botName);
@@ -1446,17 +1686,32 @@ The instructions above are your primary identity for this entire conversation \u
1446
1686
 
1447
1687
  ${base}`;
1448
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
+ }
1449
1704
  function owningServiceLabel(env = process.env) {
1450
1705
  const label = (env.XPC_SERVICE_NAME ?? "").trim();
1451
1706
  if (!label || label === "0") return void 0;
1452
1707
  return /^[A-Za-z0-9._-]+$/.test(label) ? label : void 0;
1453
1708
  }
1454
- function buildHookSettings(runtime, nativeStyleName) {
1455
- const guardScript = runtime === "container" ? `${CONTAINER_PATHS.hooks}/bash-guard.mjs` : resolve3(paths.PACKAGE_ROOT, "hooks/bash-guard.mjs");
1709
+ function buildHookSettings(runtime, builtInStyleName) {
1710
+ const guardScript = runtime === "container" ? `${CONTAINER_PATHS.hooks}/bash-guard.mjs` : resolve4(paths.PACKAGE_ROOT, "hooks/bash-guard.mjs");
1456
1711
  const selfLabel = runtime === "container" ? void 0 : owningServiceLabel();
1457
1712
  const guardEnv = selfLabel ? `FLYSPACE_SELF_SERVICE_LABEL='${selfLabel}' ` : "";
1458
1713
  return JSON.stringify({
1459
- ...nativeStyleName ? { outputStyle: nativeStyleName } : {},
1714
+ ...builtInStyleName ? { outputStyle: builtInStyleName } : {},
1460
1715
  hooks: {
1461
1716
  PreToolUse: [
1462
1717
  {
@@ -1467,7 +1722,7 @@ function buildHookSettings(runtime, nativeStyleName) {
1467
1722
  }
1468
1723
  });
1469
1724
  }
1470
- function buildArgs(prompt, botName, options, resumeSessionId) {
1725
+ function buildArgs(prompt, botName, options, resumeSessionId, isolatedStyle) {
1471
1726
  const args = [
1472
1727
  "--print",
1473
1728
  // stream-json emits NDJSON events as they happen (needed for live
@@ -1476,16 +1731,20 @@ function buildArgs(prompt, botName, options, resumeSessionId) {
1476
1731
  ...options.onActivity ? ["--output-format", "stream-json", "--verbose"] : ["--output-format", "json"],
1477
1732
  "--dangerously-skip-permissions",
1478
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(",")}`] : [],
1479
1736
  "--append-system-prompt",
1480
- buildSystemPrompt(
1481
- botName,
1482
- options.agentPersona,
1483
- options.channelPrompt,
1484
- // Skip the body when a native style name is set below -- the CLI
1485
- // applies that style's own prompt text itself, so injecting our
1486
- // approximation of the same thing too would just be redundant.
1487
- options.outputStyle?.nativeStyleName ? void 0 : options.outputStyle?.body
1488
- ),
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"),
1489
1748
  // A container or sandbox run inherits no user settings: CLAUDE_CONFIG_DIR
1490
1749
  // for both points at an isolated per-channel directory, not the machine's
1491
1750
  // `~/.claude`, which is the point. `project,local` still apply, since
@@ -1494,7 +1753,7 @@ function buildArgs(prompt, botName, options, resumeSessionId) {
1494
1753
  "--setting-sources",
1495
1754
  options.runtime === "container" || options.runtime === "sandbox" ? "project,local" : "user,project,local",
1496
1755
  "--settings",
1497
- buildHookSettings(options.runtime ?? "host", options.outputStyle?.nativeStyleName)
1756
+ buildHookSettings(options.runtime ?? "host", isolatedStyle?.builtIn ? isolatedStyle.name : void 0)
1498
1757
  ];
1499
1758
  if (mcpStrictConfigEnabled()) {
1500
1759
  args.push("--strict-mcp-config");
@@ -1528,8 +1787,15 @@ function recordUsage2(response, options) {
1528
1787
  });
1529
1788
  }
1530
1789
  function runCli(prompt, cwd, botName, options, claudeConfigDir, resumeSessionId, claudeCodeOauthToken) {
1531
- return new Promise((resolve6, reject) => {
1532
- const args = buildArgs(prompt, botName, options, resumeSessionId);
1790
+ return new Promise((resolve7, reject) => {
1791
+ const isolated = options.runtime === "container" || options.runtime === "sandbox";
1792
+ const args = buildArgs(
1793
+ prompt,
1794
+ botName,
1795
+ options,
1796
+ resumeSessionId,
1797
+ isolated ? resolveSelectedOutputStyle(claudeConfigDir) : void 0
1798
+ );
1533
1799
  const env = resolveMiseEnv(cwd);
1534
1800
  env["FLYSPACE_SCRATCH_DIR"] = paths.SCRATCH_DIR;
1535
1801
  env["CLAUDE_CODE_ARTIFACT"] = "1";
@@ -1668,7 +1934,7 @@ function runCli(prompt, cwd, botName, options, claudeConfigDir, resumeSessionId,
1668
1934
  console.log(`[agent] stderr: ${stderr.trim()}`);
1669
1935
  }
1670
1936
  if (cancelled) {
1671
- resolve6({
1937
+ resolve7({
1672
1938
  sessionId: liveSessionId ?? resumeSessionId ?? "",
1673
1939
  text: "",
1674
1940
  isCancelled: true
@@ -1677,7 +1943,7 @@ function runCli(prompt, cwd, botName, options, claudeConfigDir, resumeSessionId,
1677
1943
  }
1678
1944
  if (timedOut) {
1679
1945
  const text = timeoutKind === "wall-clock" && streaming ? `This task ran for over ${Math.round(maxDurationMs / 6e4)} minutes in total, so I was terminated as a runaway backstop. I was still making progress \u2014 the task is just too big for one turn. Consider splitting it into smaller steps, or continue and I'll pick up where I left off.` : `I got stuck for over ${Math.round((timeoutKind === "idle" ? idleTimeoutMs : maxDurationMs) / 6e4)} minutes with no activity and had to be terminated. Most likely cause: a Bash tool call to a long-running process (dev server, watcher, \`tail -f\`, etc.) that never exits. If you wanted to start a server, ask me to run it in the background (\`nohup ... > /tmp/log 2>&1 & disown\`) and I'll come back with the PID.`;
1680
- resolve6({
1946
+ resolve7({
1681
1947
  sessionId: liveSessionId ?? resumeSessionId ?? "",
1682
1948
  text,
1683
1949
  isError: true,
@@ -1705,16 +1971,16 @@ function runCli(prompt, cwd, botName, options, claudeConfigDir, resumeSessionId,
1705
1971
  options.model
1706
1972
  );
1707
1973
  recordUsage2(response, options);
1708
- resolve6(response);
1974
+ resolve7(response);
1709
1975
  } catch {
1710
1976
  const fallback = onLine ? "" : stdout.trim();
1711
- resolve6({ sessionId: resumeSessionId ?? "", text: fallback || "No response" });
1977
+ resolve7({ sessionId: resumeSessionId ?? "", text: fallback || "No response" });
1712
1978
  }
1713
1979
  });
1714
1980
  });
1715
1981
  }
1716
1982
  async function askClaude(prompt, cwd, botName, options, claudeConfigDir, resumeSessionId, claudeCodeOauthToken) {
1717
- if (!existsSync7(cwd) || !statSync2(cwd).isDirectory()) {
1983
+ if (!existsSync8(cwd) || !statSync3(cwd).isDirectory()) {
1718
1984
  console.error(`[agent] working directory does not exist: ${cwd}`);
1719
1985
  return {
1720
1986
  sessionId: resumeSessionId ?? "",
@@ -1803,9 +2069,14 @@ var workspaceCache = /* @__PURE__ */ new Map();
1803
2069
  async function getWorkspaceBySlug(slug) {
1804
2070
  const cached = workspaceCache.get(slug);
1805
2071
  if (cached && cached.expiresAt > Date.now()) return cached.workspace;
1806
- 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();
1807
2073
  if (error) throw new Error(`getWorkspaceBySlug(${slug}): ${error.message}`);
1808
- 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;
1809
2080
  workspaceCache.set(slug, { workspace, expiresAt: Date.now() + WORKSPACE_CACHE_TTL_MS });
1810
2081
  return workspace;
1811
2082
  }
@@ -1863,8 +2134,17 @@ async function setWorkspaceStripeSubscription(workspaceId, fields) {
1863
2134
  if (error) throw new Error(`setWorkspaceStripeSubscription(${workspaceId}): ${error.message}`);
1864
2135
  }
1865
2136
  async function markWorkspaceAgentConnected(workspaceId) {
1866
- const { error } = await getSupabaseClient().from("workspaces").update({ agent_connected_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", workspaceId).is("agent_connected_at", null);
1867
- 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
+ }
1868
2148
  }
1869
2149
  async function getWorkspaceByStripeCustomerId(stripeCustomerId) {
1870
2150
  const { data, error } = await getSupabaseClient().from("workspaces").select("id, slug").eq("stripe_customer_id", stripeCustomerId).maybeSingle();
@@ -1963,6 +2243,21 @@ async function createMembership(params) {
1963
2243
  if (error) throw new Error(`createMembership(${params.email}): ${error.message}`);
1964
2244
  return rowToMembership(data);
1965
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
+ }
1966
2261
  var LastAdminError = class extends Error {
1967
2262
  constructor() {
1968
2263
  super("workspace must have at least one admin");
@@ -2035,98 +2330,97 @@ async function setAgentPaused(workspaceId, paused) {
2035
2330
  if (error) throw new Error(`setAgentPaused(${workspaceId}): ${error.message}`);
2036
2331
  }
2037
2332
 
2038
- // src/store/output-styles-store.ts
2039
- var NATIVE_OUTPUT_STYLE_NAMES = {
2040
- concise: "Concise",
2041
- explanatory: "Explanatory"
2042
- };
2043
- var BUILT_IN_STYLE_BODIES = {
2044
- concise: "Lead with the result. Skip preamble and narration, and keep responses short by default. When asked for an explanation or more detail, answer in full.",
2045
- explanatory: 'Alongside completing the task, share brief "Insights" on the reasoning behind implementation choices and the codebase patterns involved.',
2046
- formal: "Respond in structured, businesslike prose. No casual asides, contractions, or emoji."
2047
- };
2048
- function rowToOutputStyle(row) {
2049
- return {
2050
- id: row["id"],
2051
- workspaceId: row["workspace_id"],
2052
- name: row["name"],
2053
- description: row["description"] ?? "",
2054
- body: row["body"] ?? "",
2055
- createdAt: row["created_at"],
2056
- updatedAt: row["updated_at"]
2057
- };
2058
- }
2059
- async function listOutputStyles(workspaceId) {
2060
- const { data, error } = await getSupabaseClient().from("output_styles").select("*").eq("workspace_id", workspaceId).order("created_at", { ascending: true });
2061
- if (error) throw new Error(`Failed to list output styles: ${error.message}`);
2062
- return (data ?? []).map(rowToOutputStyle);
2063
- }
2064
- async function getOutputStyle(workspaceId, id) {
2065
- const { data, error } = await getSupabaseClient().from("output_styles").select("*").eq("workspace_id", workspaceId).eq("id", id).maybeSingle();
2066
- if (error) throw new Error(`Failed to load output style ${id}: ${error.message}`);
2067
- return data ? rowToOutputStyle(data) : null;
2068
- }
2069
- async function insertOutputStyle(params) {
2070
- const { data, error } = await getSupabaseClient().from("output_styles").insert({
2071
- workspace_id: params.workspaceId,
2072
- name: params.name,
2073
- description: params.description,
2074
- body: params.body
2075
- }).select("*").single();
2076
- if (error) throw new Error(`Failed to create output style: ${error.message}`);
2077
- return rowToOutputStyle(data);
2078
- }
2079
- async function updateOutputStyle(workspaceId, id, patch) {
2080
- const update = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
2081
- if (patch.name !== void 0) update["name"] = patch.name;
2082
- if (patch.description !== void 0) update["description"] = patch.description;
2083
- if (patch.body !== void 0) update["body"] = patch.body;
2084
- const { data, error } = await getSupabaseClient().from("output_styles").update(update).eq("id", id).eq("workspace_id", workspaceId).select("*").maybeSingle();
2085
- if (error) throw new Error(`Failed to update output style: ${error.message}`);
2086
- return data ? rowToOutputStyle(data) : null;
2087
- }
2088
- async function deleteOutputStyle(workspaceId, id) {
2089
- const { data, error } = await getSupabaseClient().from("output_styles").delete().eq("id", id).eq("workspace_id", workspaceId).select("id").maybeSingle();
2090
- if (error) throw new Error(`Failed to delete output style: ${error.message}`);
2091
- return !!data;
2092
- }
2093
- async function getWorkspaceOutputStyleDefault(workspaceId) {
2094
- const { data, error } = await getSupabaseClient().from("workspaces").select("output_style_default").eq("id", workspaceId).maybeSingle();
2095
- if (error) {
2096
- throw new Error(`Failed to load the workspace's default output style: ${error.message}`);
2097
- }
2098
- return data?.["output_style_default"] ?? "default";
2099
- }
2100
- async function setWorkspaceOutputStyleDefault(workspaceId, id) {
2101
- const { error } = await getSupabaseClient().from("workspaces").update({ output_style_default: id }).eq("id", workspaceId);
2102
- if (error) {
2103
- throw new Error(`Failed to set the workspace's default output style: ${error.message}`);
2104
- }
2105
- }
2106
- async function getEffectiveOutputStyle(workspaceId) {
2107
- const defaultId = await getWorkspaceOutputStyleDefault(workspaceId);
2108
- if (defaultId === "default") return void 0;
2109
- const nativeStyleName = NATIVE_OUTPUT_STYLE_NAMES[defaultId];
2110
- if (nativeStyleName) return { nativeStyleName, body: BUILT_IN_STYLE_BODIES[defaultId] };
2111
- if (defaultId in BUILT_IN_STYLE_BODIES) return { body: BUILT_IN_STYLE_BODIES[defaultId] };
2112
- const style = await getOutputStyle(workspaceId, defaultId);
2113
- return style?.body ? { body: style.body } : void 0;
2333
+ // src/dashboard/settings.ts
2334
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
2335
+ import { dirname, resolve as resolve5 } from "path";
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}`;
2114
2356
  }
2115
- async function resolveOutputStyleForProfile(profile) {
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;
2116
2379
  try {
2117
- const workspace = await getWorkspaceBySlug(profile);
2118
- if (!workspace) return void 0;
2119
- return await getEffectiveOutputStyle(workspace.id);
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);
2120
2418
  } catch (err) {
2121
- console.error("[output-styles] resolveOutputStyleForProfile failed:", err.message);
2122
- return void 0;
2419
+ console.error("Auto title failed, keeping first-line fallback:", err);
2420
+ return null;
2123
2421
  }
2124
2422
  }
2125
2423
 
2126
- // src/dashboard/settings.ts
2127
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, rmSync } from "fs";
2128
- import { dirname, resolve as resolve4 } from "path";
2129
-
2130
2424
  // src/store/workspace-settings-store.ts
2131
2425
  var BRANDING_DEFAULTS = {
2132
2426
  theme: "default",
@@ -2135,7 +2429,9 @@ var BRANDING_DEFAULTS = {
2135
2429
  displayName: null,
2136
2430
  gaMeasurementId: null,
2137
2431
  botName: null,
2138
- botAvatar: null
2432
+ botAvatar: null,
2433
+ autoTitleEnabled: true,
2434
+ autoTitlePrompt: null
2139
2435
  };
2140
2436
  function rowToBranding(row) {
2141
2437
  return {
@@ -2147,7 +2443,9 @@ function rowToBranding(row) {
2147
2443
  displayName: row["display_name"] ?? null,
2148
2444
  gaMeasurementId: row["ga_measurement_id"] ?? null,
2149
2445
  botName: row["bot_name"] ?? null,
2150
- 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
2151
2449
  };
2152
2450
  }
2153
2451
  var CACHE_TTL_MS = 3e4;
@@ -2155,7 +2453,7 @@ var cache = /* @__PURE__ */ new Map();
2155
2453
  async function getWorkspaceBranding(profile) {
2156
2454
  const hit = cache.get(profile);
2157
2455
  if (hit && Date.now() - hit.readAt < CACHE_TTL_MS) return hit.value;
2158
- 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();
2159
2457
  if (error) throw new Error(`failed to read workspace settings: ${error.message}`);
2160
2458
  const value = data ? rowToBranding(data) : null;
2161
2459
  cache.set(profile, { value, readAt: Date.now() });
@@ -2170,7 +2468,9 @@ async function upsertWorkspaceBranding(profile, patch) {
2170
2468
  displayName: patch.displayName === void 0 ? current.displayName : patch.displayName,
2171
2469
  gaMeasurementId: patch.gaMeasurementId === void 0 ? current.gaMeasurementId : patch.gaMeasurementId,
2172
2470
  botName: patch.botName === void 0 ? current.botName : patch.botName,
2173
- 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
2174
2474
  };
2175
2475
  const { error } = await getSupabaseClient().from("workspace_settings").upsert(
2176
2476
  {
@@ -2182,6 +2482,8 @@ async function upsertWorkspaceBranding(profile, patch) {
2182
2482
  ga_measurement_id: next.gaMeasurementId,
2183
2483
  bot_name: next.botName,
2184
2484
  bot_avatar: next.botAvatar,
2485
+ auto_title_enabled: next.autoTitleEnabled,
2486
+ auto_title_prompt: next.autoTitlePrompt,
2185
2487
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
2186
2488
  },
2187
2489
  { onConflict: "profile" }
@@ -2192,7 +2494,15 @@ async function upsertWorkspaceBranding(profile, patch) {
2192
2494
  }
2193
2495
 
2194
2496
  // src/dashboard/settings.ts
2195
- 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
+ ];
2196
2506
  var VALID_FONTS = [
2197
2507
  "default",
2198
2508
  "system",
@@ -2200,7 +2510,8 @@ var VALID_FONTS = [
2200
2510
  "exo2",
2201
2511
  "lato",
2202
2512
  "jakarta",
2203
- "playfair"
2513
+ "playfair",
2514
+ "inter"
2204
2515
  ];
2205
2516
  var LEGACY_AGENT_TO_MODEL = {
2206
2517
  claude: "sonnet",
@@ -2240,7 +2551,8 @@ function normalizeProviders(entries) {
2240
2551
  for (const runner of KNOWN_RUNNERS) {
2241
2552
  if (!seen.has(runner)) chain.push({ runner });
2242
2553
  }
2243
- 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) };
2244
2556
  return chain;
2245
2557
  }
2246
2558
  function parseProviders(raw, legacy) {
@@ -2277,9 +2589,9 @@ function parseAgentProfiles(raw) {
2277
2589
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
2278
2590
  }
2279
2591
  function loadDashboardSettings() {
2280
- if (!existsSync8(paths.DASHBOARD_SETTINGS_FILE)) return localDefaults();
2592
+ if (!existsSync9(paths.DASHBOARD_SETTINGS_FILE)) return localDefaults();
2281
2593
  try {
2282
- const raw = JSON.parse(readFileSync3(paths.DASHBOARD_SETTINGS_FILE, "utf-8"));
2594
+ const raw = JSON.parse(readFileSync4(paths.DASHBOARD_SETTINGS_FILE, "utf-8"));
2283
2595
  const providers = parseProviders(raw.providers, {
2284
2596
  agent: normalizeDefaultModel(raw.agent, LOCAL_DEFAULTS.agent),
2285
2597
  fallbackModel: typeof raw.fallbackModel === "string" && isAllowedModel(raw.fallbackModel) ? raw.fallbackModel : void 0
@@ -2293,6 +2605,7 @@ function loadDashboardSettings() {
2293
2605
  return localDefaults();
2294
2606
  }
2295
2607
  }
2608
+ var AUTO_TITLE_PROMPT_MAX = 2e3;
2296
2609
  function withDefaultModel(providers, model) {
2297
2610
  const runner = runnerForModel(model);
2298
2611
  const rest = providers.filter((p) => p.runner !== runner);
@@ -2306,18 +2619,18 @@ function withFallbackModel(providers, model) {
2306
2619
  return [cleared[0], { runner, model }, ...cleared.slice(1).filter((p) => p.runner !== runner)];
2307
2620
  }
2308
2621
  function readSettingsFile() {
2309
- if (!existsSync8(paths.DASHBOARD_SETTINGS_FILE)) return null;
2622
+ if (!existsSync9(paths.DASHBOARD_SETTINGS_FILE)) return null;
2310
2623
  try {
2311
- const raw = JSON.parse(readFileSync3(paths.DASHBOARD_SETTINGS_FILE, "utf-8"));
2624
+ const raw = JSON.parse(readFileSync4(paths.DASHBOARD_SETTINGS_FILE, "utf-8"));
2312
2625
  return raw && typeof raw === "object" ? raw : null;
2313
2626
  } catch {
2314
2627
  return null;
2315
2628
  }
2316
2629
  }
2317
2630
  function writeSettings(next) {
2318
- mkdirSync5(dirname(paths.DASHBOARD_SETTINGS_FILE), { recursive: true });
2631
+ mkdirSync6(dirname(paths.DASHBOARD_SETTINGS_FILE), { recursive: true });
2319
2632
  const merged = { ...readSettingsFile() ?? {}, ...next };
2320
- writeFileSync2(paths.DASHBOARD_SETTINGS_FILE, JSON.stringify(merged, null, 2), "utf-8");
2633
+ writeFileSync3(paths.DASHBOARD_SETTINGS_FILE, JSON.stringify(merged, null, 2), "utf-8");
2321
2634
  return next;
2322
2635
  }
2323
2636
  function saveDashboardSettings(patch) {
@@ -2347,7 +2660,9 @@ function brandingToSettings(branding) {
2347
2660
  font: branding.font,
2348
2661
  logo: branding.logo ?? void 0,
2349
2662
  displayName: branding.displayName ?? void 0,
2350
- gaMeasurementId: branding.gaMeasurementId ?? void 0
2663
+ gaMeasurementId: branding.gaMeasurementId ?? void 0,
2664
+ autoTitleEnabled: branding.autoTitleEnabled,
2665
+ autoTitlePrompt: branding.autoTitlePrompt ?? void 0
2351
2666
  };
2352
2667
  }
2353
2668
  function legacyBranding(raw) {
@@ -2373,25 +2688,31 @@ async function saveWorkspaceBranding(patch, profile = getProfile()) {
2373
2688
  font: patch.font && VALID_FONTS.includes(patch.font) ? patch.font : current.font,
2374
2689
  logo: patch.logo === null ? null : typeof patch.logo === "string" ? patch.logo : void 0,
2375
2690
  displayName: patch.displayName === null || patch.displayName === "" ? null : typeof patch.displayName === "string" ? patch.displayName.trim() : void 0,
2376
- 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
2377
2694
  };
2378
2695
  return brandingToSettings(await upsertWorkspaceBranding(profile, next));
2379
2696
  }
2380
2697
  async function loadFullDashboardSettings(profile = getProfile()) {
2381
- return { ...await loadWorkspaceBranding(profile), ...loadDashboardSettings() };
2698
+ return {
2699
+ ...await loadWorkspaceBranding(profile),
2700
+ ...loadDashboardSettings(),
2701
+ autoTitleDefaultPrompt: DEFAULT_TITLE_RULES
2702
+ };
2382
2703
  }
2383
2704
  function defaultPromptPath() {
2384
- return resolve4(paths.PACKAGE_ROOT, "system.default.md");
2705
+ return resolve5(paths.PACKAGE_ROOT, "system.default.md");
2385
2706
  }
2386
2707
  function readDefaultPrompt() {
2387
2708
  const path = defaultPromptPath();
2388
- return existsSync8(path) ? readFileSync3(path, "utf-8").trim() : "";
2709
+ return existsSync9(path) ? readFileSync4(path, "utf-8").trim() : "";
2389
2710
  }
2390
2711
  function loadWorkspacePrompt() {
2391
2712
  const defaultPrompt = readDefaultPrompt();
2392
- const custom = existsSync8(paths.PROMPT_FILE);
2713
+ const custom = existsSync9(paths.PROMPT_FILE);
2393
2714
  return {
2394
- prompt: custom ? readFileSync3(paths.PROMPT_FILE, "utf-8").trim() : defaultPrompt,
2715
+ prompt: custom ? readFileSync4(paths.PROMPT_FILE, "utf-8").trim() : defaultPrompt,
2395
2716
  defaultPrompt,
2396
2717
  custom
2397
2718
  };
@@ -2399,13 +2720,13 @@ function loadWorkspacePrompt() {
2399
2720
  function saveWorkspacePrompt(prompt) {
2400
2721
  const trimmed = prompt.trim();
2401
2722
  if (!trimmed || trimmed === readDefaultPrompt()) return resetWorkspacePrompt();
2402
- mkdirSync5(dirname(paths.PROMPT_FILE), { recursive: true });
2403
- writeFileSync2(paths.PROMPT_FILE, `${trimmed}
2723
+ mkdirSync6(dirname(paths.PROMPT_FILE), { recursive: true });
2724
+ writeFileSync3(paths.PROMPT_FILE, `${trimmed}
2404
2725
  `, "utf-8");
2405
2726
  return loadWorkspacePrompt();
2406
2727
  }
2407
2728
  function resetWorkspacePrompt() {
2408
- rmSync(paths.PROMPT_FILE, { force: true });
2729
+ rmSync2(paths.PROMPT_FILE, { force: true });
2409
2730
  return loadWorkspacePrompt();
2410
2731
  }
2411
2732
  function saveAgentProfile(name, patch) {
@@ -2445,9 +2766,9 @@ async function saveBotIdentity(patch, profile = getProfile()) {
2445
2766
 
2446
2767
  // src/codex/agent.ts
2447
2768
  import { spawn as spawn2 } from "child_process";
2448
- import { existsSync as existsSync9, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync3 } from "fs";
2769
+ import { existsSync as existsSync10, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync4 } from "fs";
2449
2770
  import { tmpdir } from "os";
2450
- import { join as join4 } from "path";
2771
+ import { join as join5 } from "path";
2451
2772
  var AUTH_ERROR_PATTERNS = [
2452
2773
  /401 unauthorized/i,
2453
2774
  /missing bearer or basic authentication/i,
@@ -2578,8 +2899,8 @@ function getMaxDurationMs2() {
2578
2899
  }
2579
2900
  function codexAuthAvailable(env = process.env) {
2580
2901
  if (env["OPENAI_API_KEY"]?.trim()) return true;
2581
- const home = env["CODEX_HOME"]?.trim() || join4(env["HOME"] ?? "", ".codex");
2582
- return existsSync9(join4(home, "auth.json"));
2902
+ const home = env["CODEX_HOME"]?.trim() || join5(env["HOME"] ?? "", ".codex");
2903
+ return existsSync10(join5(home, "auth.json"));
2583
2904
  }
2584
2905
  function applySystemPrompt(prompt, options, resumeSessionId) {
2585
2906
  if (resumeSessionId || !options.systemPrompt?.trim()) return prompt;
@@ -2628,13 +2949,13 @@ function recordCodexUsage(response, options) {
2628
2949
  });
2629
2950
  }
2630
2951
  function runCodexCli(prompt, cwd, options, resumeSessionId) {
2631
- return new Promise((resolve6, reject) => {
2952
+ return new Promise((resolve7, reject) => {
2632
2953
  const finish = (response) => {
2633
2954
  recordCodexUsage(response, options);
2634
- resolve6(response);
2955
+ resolve7(response);
2635
2956
  };
2636
- const scratch = mkdtempSync(join4(tmpdir(), "flyspace-codex-"));
2637
- const lastMessageFile = join4(scratch, "last-message.txt");
2957
+ const scratch = mkdtempSync(join5(tmpdir(), "flyspace-codex-"));
2958
+ const lastMessageFile = join5(scratch, "last-message.txt");
2638
2959
  const args = buildArgs2(prompt, options, lastMessageFile, resumeSessionId);
2639
2960
  console.log(`[codex] spawning codex CLI in ${cwd}`);
2640
2961
  console.log(`[codex] args: ${args.slice(0, -1).join(" ")}`);
@@ -2704,7 +3025,7 @@ function runCodexCli(prompt, cwd, options, resumeSessionId) {
2704
3025
  const cleanup = () => {
2705
3026
  clearTimeout(timeoutHandle);
2706
3027
  if (signal) signal.removeEventListener("abort", onAbort);
2707
- rmSync2(scratch, { recursive: true, force: true });
3028
+ rmSync3(scratch, { recursive: true, force: true });
2708
3029
  };
2709
3030
  child.stdout.on("data", (data) => {
2710
3031
  const chunk = data.toString();
@@ -2725,7 +3046,7 @@ function runCodexCli(prompt, cwd, options, resumeSessionId) {
2725
3046
  child.on("close", (code) => {
2726
3047
  let lastMessage = "";
2727
3048
  try {
2728
- lastMessage = readFileSync4(lastMessageFile, "utf8").trim();
3049
+ lastMessage = readFileSync5(lastMessageFile, "utf8").trim();
2729
3050
  } catch {
2730
3051
  lastMessage = "";
2731
3052
  }
@@ -2761,7 +3082,7 @@ function runCodexCli(prompt, cwd, options, resumeSessionId) {
2761
3082
  });
2762
3083
  }
2763
3084
  async function askCodex(prompt, cwd, options = {}, resumeSessionId) {
2764
- if (!existsSync9(cwd) || !statSync3(cwd).isDirectory()) {
3085
+ if (!existsSync10(cwd) || !statSync4(cwd).isDirectory()) {
2765
3086
  console.error(`[codex] working directory does not exist: ${cwd}`);
2766
3087
  return {
2767
3088
  sessionId: resumeSessionId ?? "",
@@ -3209,12 +3530,12 @@ function renderAsciiTable(t) {
3209
3530
  (h, i) => Math.max(h.length, ...rows2.map((r) => (r[i] || "").length))
3210
3531
  );
3211
3532
  const fmt = (cells) => "| " + cells.map((c, i) => c.padEnd(widths[i])).join(" | ") + " |";
3212
- const sep2 = "|" + widths.map((w) => "-".repeat(w + 2)).join("|") + "|";
3213
- return [fmt(headers), sep2, ...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");
3214
3535
  }
3215
3536
 
3216
3537
  // src/store/channel-cache.ts
3217
- import { mkdirSync as mkdirSync6 } from "fs";
3538
+ import { mkdirSync as mkdirSync7 } from "fs";
3218
3539
 
3219
3540
  // src/store/sqlite.ts
3220
3541
  import Database from "better-sqlite3";
@@ -3238,7 +3559,7 @@ function getDb() {
3238
3559
  const file = paths.DB_FILE;
3239
3560
  const existing = dbs.get(file);
3240
3561
  if (existing) return existing;
3241
- mkdirSync6(paths.DATA_DIR, { recursive: true });
3562
+ mkdirSync7(paths.DATA_DIR, { recursive: true });
3242
3563
  const db = openLocalDatabase(file);
3243
3564
  if (!db) return null;
3244
3565
  db.exec(`
@@ -3374,7 +3695,6 @@ async function runAutomation(opts) {
3374
3695
 
3375
3696
  `;
3376
3697
  try {
3377
- const outputStyle = await resolveOutputStyleForProfile(getProfile());
3378
3698
  const useCodex = (explicitModel ? runnerForModel(explicitModel) === "codex" : runnerForModel(loadDashboardSettings().agent) === "codex") && !requiresClaudeAutomationRunner(prompt);
3379
3699
  const response = useCodex ? await askCodex(automationPrefix + prompt, cwd, {
3380
3700
  ...explicitModel && runnerForModel(explicitModel) === "codex" ? { model: explicitModel } : codexModel?.trim() ? { model: codexModel.trim() } : {},
@@ -3384,7 +3704,7 @@ async function runAutomation(opts) {
3384
3704
  botName,
3385
3705
  void 0,
3386
3706
  resolveChannelPrompt(channel),
3387
- outputStyle?.body
3707
+ resolveSelectedOutputStyle(claudeConfigDir)?.body
3388
3708
  ),
3389
3709
  // Same attribution as the Claude branch below — no userId, the
3390
3710
  // schedule's name is the label.
@@ -3399,7 +3719,6 @@ async function runAutomation(opts) {
3399
3719
  model,
3400
3720
  maxBudgetUsd,
3401
3721
  channelPrompt: resolveChannelPrompt(channel),
3402
- outputStyle,
3403
3722
  // No userId: nobody triggered this. The schedule's name is the
3404
3723
  // label, which is what makes "which automation is burning tokens"
3405
3724
  // answerable without joining anything.
@@ -3446,7 +3765,7 @@ var DEFAULT_TIMEOUT_MS = 12e4;
3446
3765
  var KILL_GRACE_MS3 = 5e3;
3447
3766
  function runScriptAutomation(opts) {
3448
3767
  const { name, script, cwd, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
3449
- return new Promise((resolve6, reject) => {
3768
+ return new Promise((resolve7, reject) => {
3450
3769
  const child = spawn3("/bin/bash", [script], { cwd, env: resolveMiseEnv(cwd), stdio: ["ignore", "pipe", "pipe"] });
3451
3770
  let stdout = "";
3452
3771
  let stderr = "";
@@ -3479,7 +3798,7 @@ function runScriptAutomation(opts) {
3479
3798
  } else if (code !== 0) {
3480
3799
  reject(new Error(`script exited with code ${code}: ${stderr.trim()}`));
3481
3800
  } else {
3482
- resolve6();
3801
+ resolve7();
3483
3802
  }
3484
3803
  });
3485
3804
  });
@@ -3835,17 +4154,17 @@ var TriggerEngine = class {
3835
4154
  };
3836
4155
 
3837
4156
  // src/store/user-cache.ts
3838
- import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
3839
- import { join as join5 } from "path";
4157
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
4158
+ import { join as join6 } from "path";
3840
4159
  function jsonUserCachePath() {
3841
- const file = join5(paths.CONFIG_DIR, "slack_users.json");
3842
- return existsSync10(file) ? file : void 0;
4160
+ const file = join6(paths.CONFIG_DIR, "slack_users.json");
4161
+ return existsSync11(file) ? file : void 0;
3843
4162
  }
3844
4163
  function readJsonUserCache() {
3845
4164
  const file = jsonUserCachePath();
3846
4165
  if (!file) return void 0;
3847
4166
  try {
3848
- const parsed = JSON.parse(readFileSync5(file, "utf8"));
4167
+ const parsed = JSON.parse(readFileSync6(file, "utf8"));
3849
4168
  return parsed.users;
3850
4169
  } catch {
3851
4170
  return void 0;
@@ -4237,8 +4556,8 @@ var MessageQueue = class {
4237
4556
  const chains = Array.from(this.chains.values());
4238
4557
  if (chains.length === 0) return true;
4239
4558
  let timer;
4240
- const timeout = new Promise((resolve6) => {
4241
- timer = setTimeout(() => resolve6(false), timeoutMs);
4559
+ const timeout = new Promise((resolve7) => {
4560
+ timer = setTimeout(() => resolve7(false), timeoutMs);
4242
4561
  });
4243
4562
  const done = Promise.all(chains).then(() => true);
4244
4563
  const result = await Promise.race([done, timeout]);
@@ -4252,7 +4571,7 @@ var MessageQueue = class {
4252
4571
 
4253
4572
  // src/slack/file-downloader.ts
4254
4573
  import { writeFile } from "fs/promises";
4255
- import { join as join6, extname } from "path";
4574
+ import { join as join7, extname } from "path";
4256
4575
  var debug3 = process.env["DEBUG"] === "true";
4257
4576
  var SUPPORTED_IMAGE_MIMETYPES = /* @__PURE__ */ new Set([
4258
4577
  "image/png",
@@ -4352,7 +4671,7 @@ async function downloadOne(file, botToken) {
4352
4671
  const ext = extname(originalName) || `.${file.filetype ?? "bin"}`;
4353
4672
  const safeName = sanitizeFilename(originalName.replace(ext, ""));
4354
4673
  const filename = `${Date.now()}-${file.id ?? "unnamed"}-${safeName}${ext}`;
4355
- const localPath = join6(paths.UPLOADS_DIR, filename);
4674
+ const localPath = join7(paths.UPLOADS_DIR, filename);
4356
4675
  await writeFile(localPath, buffer);
4357
4676
  if (debug3)
4358
4677
  console.log(`[file-downloader] saved ${file.id} -> ${localPath} (${buffer.length} bytes)`);
@@ -4767,44 +5086,44 @@ async function summariseThreadTranscript(transcript, opts, deps = {}) {
4767
5086
  }
4768
5087
 
4769
5088
  // src/claude/skills.ts
4770
- import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
4771
- import { homedir as homedir3 } from "os";
4772
- import { basename as basename2, dirname as dirname2, join as join8 } from "path";
5089
+ import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
5090
+ import { homedir as homedir4 } from "os";
5091
+ import { basename as basename2, dirname as dirname2, join as join9 } from "path";
4773
5092
  import yaml3 from "js-yaml";
4774
5093
 
4775
5094
  // src/claude/skill-files.ts
4776
5095
  import {
4777
- existsSync as existsSync11,
4778
- mkdirSync as mkdirSync7,
4779
- readFileSync as readFileSync6,
4780
- readdirSync,
4781
- rmSync as rmSync3,
4782
- statSync as statSync4,
4783
- writeFileSync as writeFileSync3
5096
+ existsSync as existsSync12,
5097
+ mkdirSync as mkdirSync8,
5098
+ readFileSync as readFileSync7,
5099
+ readdirSync as readdirSync2,
5100
+ rmSync as rmSync4,
5101
+ statSync as statSync5,
5102
+ writeFileSync as writeFileSync4
4784
5103
  } from "fs";
4785
- import { homedir as homedir2 } from "os";
4786
- import { join as join7, resolve as resolve5, sep } from "path";
5104
+ import { homedir as homedir3 } from "os";
5105
+ import { join as join8, resolve as resolve6, sep as sep3 } from "path";
4787
5106
  import yaml2 from "js-yaml";
4788
5107
  var MAX_SKILL_BYTES = 2 * 1024 * 1024;
4789
5108
  var SKILL_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
4790
5109
  function expandHome(p) {
4791
- if (p === "~") return homedir2();
4792
- if (p.startsWith("~/")) return join7(homedir2(), p.slice(2));
5110
+ if (p === "~") return homedir3();
5111
+ if (p.startsWith("~/")) return join8(homedir3(), p.slice(2));
4793
5112
  return p;
4794
5113
  }
4795
5114
  function workspaceSkillsDir(configDir) {
4796
- return join7(configDir ? expandHome(configDir) : join7(homedir2(), ".claude"), "skills");
5115
+ return join8(configDir ? expandHome(configDir) : join8(homedir3(), ".claude"), "skills");
4797
5116
  }
4798
5117
  function channelSkillsDir(cwd) {
4799
- return join7(cwd, ".claude", "skills");
5118
+ return join8(cwd, ".claude", "skills");
4800
5119
  }
4801
5120
  function isValidSkillName(name) {
4802
5121
  return SKILL_NAME.test(name);
4803
5122
  }
4804
5123
  function resolveSkillPath(dir, name) {
4805
5124
  if (!isValidSkillName(name)) return null;
4806
- const abs = resolve5(dir, name, "SKILL.md");
4807
- if (!abs.startsWith(resolve5(dir) + sep)) return null;
5125
+ const abs = resolve6(dir, name, "SKILL.md");
5126
+ if (!abs.startsWith(resolve6(dir) + sep3)) return null;
4808
5127
  return abs;
4809
5128
  }
4810
5129
  function splitFrontmatter(raw) {
@@ -4831,7 +5150,7 @@ ${body.replace(/^\n+/, "").trimEnd()}
4831
5150
  }
4832
5151
  function readMeta(abs) {
4833
5152
  try {
4834
- const { fm } = splitFrontmatter(readFileSync6(abs, "utf-8"));
5153
+ const { fm } = splitFrontmatter(readFileSync7(abs, "utf-8"));
4835
5154
  return {
4836
5155
  name: typeof fm["name"] === "string" ? fm["name"] : void 0,
4837
5156
  description: typeof fm["description"] === "string" ? fm["description"] : void 0
@@ -4841,20 +5160,20 @@ function readMeta(abs) {
4841
5160
  }
4842
5161
  }
4843
5162
  function listSkillFiles(dir) {
4844
- if (!existsSync11(dir)) return [];
5163
+ if (!existsSync12(dir)) return [];
4845
5164
  let entries;
4846
5165
  try {
4847
- entries = readdirSync(dir);
5166
+ entries = readdirSync2(dir);
4848
5167
  } catch {
4849
5168
  return [];
4850
5169
  }
4851
5170
  const rows2 = [];
4852
5171
  for (const entry of entries) {
4853
5172
  if (!isValidSkillName(entry)) continue;
4854
- const abs = join7(dir, entry, "SKILL.md");
5173
+ const abs = join8(dir, entry, "SKILL.md");
4855
5174
  let stat;
4856
5175
  try {
4857
- stat = statSync4(abs);
5176
+ stat = statSync5(abs);
4858
5177
  } catch {
4859
5178
  continue;
4860
5179
  }
@@ -4875,12 +5194,12 @@ function readSkillFile(dir, name) {
4875
5194
  if (!abs) return null;
4876
5195
  let stat;
4877
5196
  try {
4878
- stat = statSync4(abs);
5197
+ stat = statSync5(abs);
4879
5198
  } catch {
4880
5199
  return null;
4881
5200
  }
4882
5201
  if (!stat.isFile() || stat.size > MAX_SKILL_BYTES) return null;
4883
- const { fm, body } = splitFrontmatter(readFileSync6(abs, "utf-8"));
5202
+ const { fm, body } = splitFrontmatter(readFileSync7(abs, "utf-8"));
4884
5203
  return {
4885
5204
  name,
4886
5205
  description: typeof fm["description"] === "string" ? fm["description"].trim() : "",
@@ -4894,7 +5213,7 @@ function writeSkillFile(dir, name, patch, baseMtime) {
4894
5213
  if (!abs) return { status: "not-found" };
4895
5214
  let stat;
4896
5215
  try {
4897
- stat = statSync4(abs);
5216
+ stat = statSync5(abs);
4898
5217
  } catch {
4899
5218
  return { status: "not-found" };
4900
5219
  }
@@ -4902,18 +5221,18 @@ function writeSkillFile(dir, name, patch, baseMtime) {
4902
5221
  if (baseMtime && stat.mtime.toISOString() !== baseMtime) {
4903
5222
  return { status: "conflict", mtime: stat.mtime.toISOString() };
4904
5223
  }
4905
- const { fm } = splitFrontmatter(readFileSync6(abs, "utf-8"));
5224
+ const { fm } = splitFrontmatter(readFileSync7(abs, "utf-8"));
4906
5225
  const content = buildSkillFile({ ...fm, name, description: patch.description }, patch.body);
4907
5226
  if (Buffer.byteLength(content, "utf8") > MAX_SKILL_BYTES) return { status: "too-large" };
4908
- writeFileSync3(abs, content, "utf8");
4909
- return { status: "ok", mtime: statSync4(abs).mtime.toISOString() };
5227
+ writeFileSync4(abs, content, "utf8");
5228
+ return { status: "ok", mtime: statSync5(abs).mtime.toISOString() };
4910
5229
  }
4911
5230
  function createSkillFile(dir, name, description) {
4912
5231
  const abs = resolveSkillPath(dir, name);
4913
5232
  if (!abs) return { status: "bad-name" };
4914
- if (existsSync11(abs)) return { status: "exists" };
4915
- mkdirSync7(join7(dir, name), { recursive: true });
4916
- writeFileSync3(
5233
+ if (existsSync12(abs)) return { status: "exists" };
5234
+ mkdirSync8(join8(dir, name), { recursive: true });
5235
+ writeFileSync4(
4917
5236
  abs,
4918
5237
  buildSkillFile(
4919
5238
  { name, description },
@@ -4929,24 +5248,24 @@ Describe what this skill does and when to use it.
4929
5248
  }
4930
5249
  function deleteSkillFile(dir, name) {
4931
5250
  const abs = resolveSkillPath(dir, name);
4932
- if (!abs || !existsSync11(abs)) return { status: "not-found" };
4933
- rmSync3(join7(dir, name), { recursive: true, force: true });
5251
+ if (!abs || !existsSync12(abs)) return { status: "not-found" };
5252
+ rmSync4(join8(dir, name), { recursive: true, force: true });
4934
5253
  return { status: "ok" };
4935
5254
  }
4936
5255
 
4937
5256
  // src/claude/skills.ts
4938
5257
  function defaultConfigDir() {
4939
- return join8(homedir3(), ".claude");
5258
+ return join9(homedir4(), ".claude");
4940
5259
  }
4941
5260
  function expandHome2(p) {
4942
- if (p === "~") return homedir3();
4943
- if (p.startsWith("~/")) return join8(homedir3(), p.slice(2));
5261
+ if (p === "~") return homedir4();
5262
+ if (p.startsWith("~/")) return join9(homedir4(), p.slice(2));
4944
5263
  return p;
4945
5264
  }
4946
5265
  function parseFrontmatter(file) {
4947
5266
  let content;
4948
5267
  try {
4949
- content = readFileSync7(file, "utf-8");
5268
+ content = readFileSync8(file, "utf-8");
4950
5269
  } catch {
4951
5270
  return {};
4952
5271
  }
@@ -4964,25 +5283,25 @@ function parseFrontmatter(file) {
4964
5283
  function isSymlinkedDir(root, entry) {
4965
5284
  if (!entry.isSymbolicLink()) return false;
4966
5285
  try {
4967
- return statSync5(join8(root, entry.name)).isDirectory();
5286
+ return statSync6(join9(root, entry.name)).isDirectory();
4968
5287
  } catch {
4969
5288
  return false;
4970
5289
  }
4971
5290
  }
4972
5291
  function findSkillFiles(root, maxDepth, out) {
4973
- if (maxDepth < 0 || !existsSync12(root)) return;
5292
+ if (maxDepth < 0 || !existsSync13(root)) return;
4974
5293
  let entries;
4975
5294
  try {
4976
- entries = readdirSync2(root, { withFileTypes: true });
5295
+ entries = readdirSync3(root, { withFileTypes: true });
4977
5296
  } catch {
4978
5297
  return;
4979
5298
  }
4980
5299
  for (const entry of entries) {
4981
5300
  if (entry.isFile()) {
4982
- if (entry.name === "SKILL.md") out.push(join8(root, entry.name));
5301
+ if (entry.name === "SKILL.md") out.push(join9(root, entry.name));
4983
5302
  } else if (entry.isDirectory() || isSymlinkedDir(root, entry)) {
4984
5303
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
4985
- findSkillFiles(join8(root, entry.name), maxDepth - 1, out);
5304
+ findSkillFiles(join9(root, entry.name), maxDepth - 1, out);
4986
5305
  }
4987
5306
  }
4988
5307
  }
@@ -4991,7 +5310,7 @@ function pluginNameFor(file, root) {
4991
5310
  while (dir.startsWith(root) && dir !== dirname2(dir)) {
4992
5311
  try {
4993
5312
  const manifest = JSON.parse(
4994
- readFileSync7(join8(dir, ".claude-plugin", "plugin.json"), "utf-8")
5313
+ readFileSync8(join9(dir, ".claude-plugin", "plugin.json"), "utf-8")
4995
5314
  );
4996
5315
  if (typeof manifest.name === "string" && manifest.name.trim()) return manifest.name.trim();
4997
5316
  } catch {
@@ -5020,10 +5339,10 @@ function collectFrom(root, source, maxDepth, namePrefix) {
5020
5339
  function collectAll(configDir, cwd) {
5021
5340
  const base = configDir ? expandHome2(configDir) : defaultConfigDir();
5022
5341
  const collected = [
5023
- ...collectFrom(join8(cwd, ".claude", "skills"), "project", 2),
5342
+ ...collectFrom(join9(cwd, ".claude", "skills"), "project", 2),
5024
5343
  ...nestedProjectRoots(cwd).flatMap(({ dir, prefix }) => collectFrom(dir, "project", 2, prefix)),
5025
- ...collectFrom(join8(base, "skills"), "user", 2),
5026
- ...collectFrom(join8(base, "plugins"), "plugin", 8)
5344
+ ...collectFrom(join9(base, "skills"), "user", 2),
5345
+ ...collectFrom(join9(base, "plugins"), "plugin", 8)
5027
5346
  ];
5028
5347
  const byName = /* @__PURE__ */ new Map();
5029
5348
  for (const skill of collected) {
@@ -5034,7 +5353,7 @@ function collectAll(configDir, cwd) {
5034
5353
  function nestedProjectRoots(cwd) {
5035
5354
  let entries;
5036
5355
  try {
5037
- entries = readdirSync2(cwd, { withFileTypes: true });
5356
+ entries = readdirSync3(cwd, { withFileTypes: true });
5038
5357
  } catch {
5039
5358
  return [];
5040
5359
  }
@@ -5042,8 +5361,8 @@ function nestedProjectRoots(cwd) {
5042
5361
  for (const entry of entries) {
5043
5362
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
5044
5363
  if (!entry.isDirectory() && !isSymlinkedDir(cwd, entry)) continue;
5045
- const skillsDir = join8(cwd, entry.name, ".claude", "skills");
5046
- if (existsSync12(skillsDir)) roots.push({ dir: skillsDir, prefix: entry.name });
5364
+ const skillsDir = join9(cwd, entry.name, ".claude", "skills");
5365
+ if (existsSync13(skillsDir)) roots.push({ dir: skillsDir, prefix: entry.name });
5047
5366
  }
5048
5367
  return roots;
5049
5368
  }
@@ -5055,8 +5374,8 @@ function listPluginSkillDetails(configDir, cwd) {
5055
5374
  for (const { file, ...skill } of collectAll(configDir, cwd)) {
5056
5375
  if (skill.source !== "plugin") continue;
5057
5376
  try {
5058
- const { body } = splitFrontmatter(readFileSync7(file, "utf-8"));
5059
- details.push({ ...skill, body, path: file, mtime: statSync5(file).mtime.toISOString() });
5377
+ const { body } = splitFrontmatter(readFileSync8(file, "utf-8"));
5378
+ details.push({ ...skill, body, path: file, mtime: statSync6(file).mtime.toISOString() });
5060
5379
  } catch {
5061
5380
  }
5062
5381
  }
@@ -5065,15 +5384,15 @@ function listPluginSkillDetails(configDir, cwd) {
5065
5384
  function findSkillDetail(configDir, cwd, name, sources) {
5066
5385
  const base = configDir ? expandHome2(configDir) : defaultConfigDir();
5067
5386
  const roots = [
5068
- [join8(cwd, ".claude", "skills"), "project", 2],
5387
+ [join9(cwd, ".claude", "skills"), "project", 2],
5069
5388
  ...nestedProjectRoots(cwd).map(({ dir, prefix }) => [
5070
5389
  dir,
5071
5390
  "project",
5072
5391
  2,
5073
5392
  prefix
5074
5393
  ]),
5075
- [join8(base, "skills"), "user", 2],
5076
- [join8(base, "plugins"), "plugin", 8]
5394
+ [join9(base, "skills"), "user", 2],
5395
+ [join9(base, "plugins"), "plugin", 8]
5077
5396
  ];
5078
5397
  for (const [root, source, maxDepth, namePrefix] of roots) {
5079
5398
  if (!sources.includes(source)) continue;
@@ -5087,8 +5406,8 @@ function findSkillDetail(configDir, cwd, name, sources) {
5087
5406
  let raw;
5088
5407
  let mtime;
5089
5408
  try {
5090
- raw = readFileSync7(file, "utf-8");
5091
- mtime = statSync5(file).mtime.toISOString();
5409
+ raw = readFileSync8(file, "utf-8");
5410
+ mtime = statSync6(file).mtime.toISOString();
5092
5411
  } catch {
5093
5412
  continue;
5094
5413
  }
@@ -5579,7 +5898,6 @@ ${trimmedDetail.slice(0, 1500)}
5579
5898
  const threadModelOverride = store.getModelOverride(channelId, sessionKey);
5580
5899
  const effectiveModel = resolveModel(threadModelOverride, channelId, model);
5581
5900
  const runner = threadModelOverride ? runnerForModel(threadModelOverride) : runnerForModel(loadDashboardSettings().agent);
5582
- const outputStyle = await resolveOutputStyleForProfile(getProfile());
5583
5901
  if (runner === "codex") {
5584
5902
  const stored = store.getSession(channelId, sessionKey);
5585
5903
  const resumeId = stored?.runner === "codex" ? stored.sessionId : void 0;
@@ -5593,7 +5911,7 @@ ${trimmedDetail.slice(0, 1500)}
5593
5911
  botName,
5594
5912
  void 0,
5595
5913
  resolveChannelPrompt(channelId),
5596
- outputStyle?.body
5914
+ resolveSelectedOutputStyle(claudeConfigDir)?.body
5597
5915
  ),
5598
5916
  signal: abortController.signal,
5599
5917
  // Same attribution as the Claude path's `askOptions.usageContext`
@@ -5686,7 +6004,6 @@ ${trimmedDetail.slice(0, 1500)}
5686
6004
  // workspace prompt for this channel only — same source the dashboard
5687
6005
  // chat path reads through resolveChannelConfig().
5688
6006
  channelPrompt: resolveChannelPrompt(channelId),
5689
- outputStyle,
5690
6007
  signal: abortController.signal,
5691
6008
  // Attribution for the usage row each spawn writes. Sharing one options
5692
6009
  // object across every attempt is deliberate: the retries below are
@@ -7848,13 +8165,13 @@ async function recoverStaleReactions(client, reactionStore) {
7848
8165
  var ts2 = () => (/* @__PURE__ */ new Date()).toISOString();
7849
8166
  var POWER_POLL_MS = 3e4;
7850
8167
  function isOnACPower() {
7851
- return new Promise((resolve6) => {
8168
+ return new Promise((resolve7) => {
7852
8169
  execFile("pmset", ["-g", "ps"], { timeout: 2e3 }, (err, stdout) => {
7853
8170
  if (err) {
7854
- resolve6(true);
8171
+ resolve7(true);
7855
8172
  return;
7856
8173
  }
7857
- resolve6(stdout.includes("'AC Power'"));
8174
+ resolve7(stdout.includes("'AC Power'"));
7858
8175
  });
7859
8176
  });
7860
8177
  }
@@ -7998,6 +8315,9 @@ export {
7998
8315
  loadConfig,
7999
8316
  refreshAutomationRegistry,
8000
8317
  startAutomationRegistryPolling,
8318
+ isLogicalCwd,
8319
+ resolveLogicalCwd,
8320
+ ensureLogicalCwdDir,
8001
8321
  expandPath,
8002
8322
  canSeeChannel,
8003
8323
  refreshChannelRegistry,
@@ -8012,13 +8332,25 @@ export {
8012
8332
  resolveCwd,
8013
8333
  resolveChannelPrompt,
8014
8334
  createChannel,
8335
+ DefaultChannelError,
8015
8336
  updateChannelById,
8016
8337
  updateChannelsMcpConnections,
8017
8338
  ensureAgentDmChannel,
8339
+ ensureGeneralChannel,
8340
+ ensureMemberDmChannel,
8018
8341
  DEFAULT_TIMEZONE,
8019
8342
  loadAutomations,
8020
8343
  loadAutomationsFile,
8021
8344
  loadEffectiveAutomations,
8345
+ BUILT_IN_OUTPUT_STYLES,
8346
+ OutputStyleConflictError,
8347
+ listOutputStyles,
8348
+ createOutputStyle,
8349
+ updateOutputStyle,
8350
+ deleteOutputStyle,
8351
+ resolveSelectedOutputStyle,
8352
+ getOutputStyleDefault,
8353
+ setOutputStyleDefault,
8022
8354
  runnerForModel,
8023
8355
  isCodexModel,
8024
8356
  agentLabel,
@@ -8027,6 +8359,7 @@ export {
8027
8359
  usageLimitedUntil,
8028
8360
  usageLimitedModels,
8029
8361
  buildSystemPrompt,
8362
+ docsAccessPromptNote,
8030
8363
  askClaude,
8031
8364
  getWorkspaceBySlug,
8032
8365
  getWorkspaceById,
@@ -8036,6 +8369,7 @@ export {
8036
8369
  deleteWorkspace,
8037
8370
  setWorkspaceStripeSubscription,
8038
8371
  markWorkspaceAgentConnected,
8372
+ markWorkspaceProjectsSetup,
8039
8373
  getWorkspaceByStripeCustomerId,
8040
8374
  getWorkspaceBilling,
8041
8375
  updateWorkspacePlanStatus,
@@ -8046,6 +8380,9 @@ export {
8046
8380
  listMembershipsByEmail,
8047
8381
  listMemberships,
8048
8382
  createMembership,
8383
+ HANDLE_PATTERN,
8384
+ HandleTakenError,
8385
+ setMembershipHandle,
8049
8386
  LastAdminError,
8050
8387
  updateMembershipRole,
8051
8388
  markInviteResent,
@@ -8057,14 +8394,9 @@ export {
8057
8394
  setAllowGuestAccess,
8058
8395
  getAgentPaused,
8059
8396
  setAgentPaused,
8060
- listOutputStyles,
8061
- getOutputStyle,
8062
- insertOutputStyle,
8063
- updateOutputStyle,
8064
- deleteOutputStyle,
8065
- getWorkspaceOutputStyleDefault,
8066
- setWorkspaceOutputStyleDefault,
8067
- resolveOutputStyleForProfile,
8397
+ DEFAULT_TITLE_RULES,
8398
+ extractCategories,
8399
+ buildAutoTitle,
8068
8400
  BRANDING_DEFAULTS,
8069
8401
  getWorkspaceBranding,
8070
8402
  localDefaults,