flyspace-cli 0.1.10 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  buildSystemPrompt,
33
33
  canSeeChannel,
34
34
  channelSkillsDir,
35
+ claudeAuthFailed,
35
36
  codexAuthAvailable,
36
37
  createChannel,
37
38
  createMembership,
@@ -152,7 +153,7 @@ import {
152
153
  warnOnBrokenChannelCwds,
153
154
  workspaceSkillsDir,
154
155
  writeSkillFile
155
- } from "./chunk-UYLVSUD6.js";
156
+ } from "./chunk-VKTJBYA3.js";
156
157
  import {
157
158
  firstReplyBody,
158
159
  getThread,
@@ -313,42 +314,10 @@ async function runStart() {
313
314
  await startServer();
314
315
  }
315
316
 
316
- // src/commands/prompt.ts
317
- import { existsSync, copyFileSync } from "fs";
317
+ // src/commands/config.ts
318
+ import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
318
319
  import { spawn } from "child_process";
319
320
  import { resolve } from "path";
320
- async function runPrompt() {
321
- ensureDirs();
322
- if (!existsSync(paths.PROMPT_FILE)) {
323
- const defaultPrompt = resolve(paths.PACKAGE_ROOT, "system.default.md");
324
- if (!existsSync(defaultPrompt)) {
325
- console.error("[flyspace] system.default.md not found in package root.");
326
- process.exit(1);
327
- }
328
- copyFileSync(defaultPrompt, paths.PROMPT_FILE);
329
- console.log(`[flyspace] Copied default prompt to ${paths.PROMPT_FILE}`);
330
- }
331
- const editor = process.env["EDITOR"] || "vi";
332
- console.log(`[flyspace] Opening ${paths.PROMPT_FILE} in ${editor}...`);
333
- const child = spawn(editor, [paths.PROMPT_FILE], { stdio: "inherit" });
334
- return new Promise((resolve8, reject) => {
335
- child.on("close", (code) => {
336
- if (code === 0) {
337
- resolve8();
338
- } else {
339
- reject(new Error(`Editor exited with code ${code}`));
340
- }
341
- });
342
- child.on("error", (err) => {
343
- reject(new Error(`Failed to open editor: ${err.message}`));
344
- });
345
- });
346
- }
347
-
348
- // src/commands/config.ts
349
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
350
- import { spawn as spawn2 } from "child_process";
351
- import { resolve as resolve2 } from "path";
352
321
  function maskToken(value) {
353
322
  if (!value || value.length < 12) return "****";
354
323
  return value.slice(0, 8) + "..." + value.slice(-4);
@@ -364,12 +333,12 @@ function printConfig() {
364
333
  console.log("\n Paths:");
365
334
  console.log(` Config dir: ${paths.CONFIG_DIR}`);
366
335
  console.log(` Data dir: ${paths.DATA_DIR}`);
367
- console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync2(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
368
- console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync2(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
369
- console.log(` Database: ${paths.DB_FILE} ${existsSync2(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
336
+ console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
337
+ console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
338
+ console.log(` Database: ${paths.DB_FILE} ${existsSync(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
370
339
  console.log(` Log dir: ${paths.LOG_DIR}`);
371
- const repoEnv = resolve2(process.cwd(), ".env");
372
- console.log(` Repo .env: ${repoEnv} ${existsSync2(repoEnv) ? "(exists)" : "(not found)"}`);
340
+ const repoEnv = resolve(process.cwd(), ".env");
341
+ console.log(` Repo .env: ${repoEnv} ${existsSync(repoEnv) ? "(exists)" : "(not found)"}`);
373
342
  const envKeys = [
374
343
  "SLACK_BOT_TOKEN",
375
344
  "SLACK_APP_TOKEN",
@@ -393,9 +362,9 @@ function printConfig() {
393
362
  }
394
363
  function editConfig() {
395
364
  ensureDirs();
396
- if (!existsSync2(paths.CONFIG_FILE)) {
397
- const defaultEnv = resolve2(paths.PACKAGE_ROOT, ".env.example");
398
- if (existsSync2(defaultEnv)) {
365
+ if (!existsSync(paths.CONFIG_FILE)) {
366
+ const defaultEnv = resolve(paths.PACKAGE_ROOT, ".env.example");
367
+ if (existsSync(defaultEnv)) {
399
368
  const content = readFileSync2(defaultEnv, "utf-8");
400
369
  writeFileSync2(paths.CONFIG_FILE, content);
401
370
  } else {
@@ -405,11 +374,11 @@ function editConfig() {
405
374
  }
406
375
  const editor = process.env["EDITOR"] || "vi";
407
376
  console.log(`[flyspace] Opening ${paths.CONFIG_FILE} in ${editor}...`);
408
- const child = spawn2(editor, [paths.CONFIG_FILE], { stdio: "inherit" });
409
- return new Promise((resolve8, reject) => {
377
+ const child = spawn(editor, [paths.CONFIG_FILE], { stdio: "inherit" });
378
+ return new Promise((resolve7, reject) => {
410
379
  child.on("close", (code) => {
411
380
  if (code === 0) {
412
- resolve8();
381
+ resolve7();
413
382
  } else {
414
383
  reject(new Error(`Editor exited with code ${code}`));
415
384
  }
@@ -439,7 +408,7 @@ import { confirm as confirm2 } from "@inquirer/prompts";
439
408
 
440
409
  // src/service.ts
441
410
  import { execSync } from "child_process";
442
- import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
411
+ import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
443
412
  import { basename, dirname as dirname2, join as join2 } from "path";
444
413
  import { homedir, platform } from "os";
445
414
  function flyspaceHome(entry = process.argv[1]) {
@@ -454,7 +423,7 @@ function flyspaceHome(entry = process.argv[1]) {
454
423
  const npmPrefix = cliPath.slice(0, at);
455
424
  if (basename(npmPrefix) !== "cli") return void 0;
456
425
  const home = dirname2(npmPrefix);
457
- return existsSync3(join2(home, "bin", "flyspace")) && existsSync3(join2(home, "node", "bin", "node")) ? home : void 0;
426
+ return existsSync2(join2(home, "bin", "flyspace")) && existsSync2(join2(home, "node", "bin", "node")) ? home : void 0;
458
427
  }
459
428
  function serviceLabel(profile) {
460
429
  return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
@@ -478,8 +447,8 @@ function systemdUnitPath(profile) {
478
447
  return join2(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
479
448
  }
480
449
  function isServiceInstalled(profile) {
481
- if (platform() === "darwin") return existsSync3(plistPath(profile));
482
- if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
450
+ if (platform() === "darwin") return existsSync2(plistPath(profile));
451
+ if (platform() === "linux") return existsSync2(systemdUnitPath(profile));
483
452
  return false;
484
453
  }
485
454
  function serviceBinaryMissing(file) {
@@ -490,7 +459,7 @@ function serviceBinaryMissing(file) {
490
459
  return false;
491
460
  }
492
461
  const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
493
- return !!bin && !existsSync3(bin);
462
+ return !!bin && !existsSync2(bin);
494
463
  }
495
464
  function profileServiceFile(profile) {
496
465
  return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
@@ -529,7 +498,7 @@ function hostPlistPath() {
529
498
  return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
530
499
  }
531
500
  function isHostServiceInstalled() {
532
- if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
501
+ if (platform() === "darwin" || platform() === "linux") return existsSync2(hostServiceFile());
533
502
  return false;
534
503
  }
535
504
  function restartHostService() {
@@ -569,9 +538,9 @@ function isHostServiceRunning() {
569
538
  }
570
539
 
571
540
  // src/commands/install.ts
572
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
541
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
573
542
  import { execSync as execSync2 } from "child_process";
574
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
543
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
575
544
  import { homedir as homedir2, platform as platform2 } from "os";
576
545
  function log(msg) {
577
546
  console.log(`
@@ -603,7 +572,7 @@ function detectNodeBinDir() {
603
572
  }
604
573
  function nodeShimDirs() {
605
574
  return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
606
- (d) => existsSync4(d)
575
+ (d) => existsSync3(d)
607
576
  );
608
577
  }
609
578
  function detectCustieBin() {
@@ -611,7 +580,7 @@ function detectCustieBin() {
611
580
  if (home) return join3(home, "bin", "flyspace");
612
581
  const argv1 = process.argv[1];
613
582
  if (argv1) {
614
- return resolve3(argv1);
583
+ return resolve2(argv1);
615
584
  }
616
585
  try {
617
586
  return run("which flyspace", { silent: true });
@@ -620,7 +589,7 @@ function detectCustieBin() {
620
589
  }
621
590
  }
622
591
  function readEnvFile() {
623
- if (!existsSync4(paths.CONFIG_FILE)) return {};
592
+ if (!existsSync3(paths.CONFIG_FILE)) return {};
624
593
  const vars = {};
625
594
  for (const line of readFileSync4(paths.CONFIG_FILE, "utf-8").split("\n")) {
626
595
  const trimmed = line.trim();
@@ -668,7 +637,7 @@ async function installMacOS(spec) {
668
637
  const logDir = paths.LOG_DIR;
669
638
  mkdirSync2(logDir, { recursive: true });
670
639
  const envVars = spec.env;
671
- const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
640
+ const homebrewBin = existsSync3("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
672
641
  const pathParts = [
673
642
  detectNodeBinDir(),
674
643
  claudeBinDir,
@@ -736,7 +705,7 @@ async function installLinux(spec) {
736
705
  const logDir = paths.LOG_DIR;
737
706
  mkdirSync2(logDir, { recursive: true });
738
707
  const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
739
- const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
708
+ const linuxbrewBin = existsSync3("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
740
709
  const pathParts = [
741
710
  detectNodeBinDir(),
742
711
  claudeBinDir,
@@ -796,13 +765,13 @@ async function installHostService() {
796
765
  }
797
766
 
798
767
  // src/commands/profiles.ts
799
- import { existsSync as existsSync5, readdirSync, statSync } from "fs";
768
+ import { existsSync as existsSync4, readdirSync, statSync } from "fs";
800
769
  import { execSync as execSync3 } from "child_process";
801
770
  import { join as join4 } from "path";
802
771
  import { platform as platform3 } from "os";
803
772
  function listProfiles() {
804
773
  const profiles = [];
805
- if (existsSync5(paths.PROFILES_DIR)) {
774
+ if (existsSync4(paths.PROFILES_DIR)) {
806
775
  for (const entry of readdirSync(paths.PROFILES_DIR)) {
807
776
  try {
808
777
  if (statSync(join4(paths.PROFILES_DIR, entry)).isDirectory()) {
@@ -831,7 +800,7 @@ function isServiceRunning2(profile) {
831
800
 
832
801
  // src/auth/device-auth.ts
833
802
  import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
834
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
803
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
835
804
  import { dirname as dirname4, join as join5 } from "path";
836
805
  var DEFAULT_API_URL = "https://app.flyspace.ai";
837
806
  var EXPIRY_SKEW_MS = 6e4;
@@ -853,7 +822,7 @@ function credentialsPath() {
853
822
  }
854
823
  function ensureDeviceKey(path = deviceKeyPath()) {
855
824
  let created = false;
856
- if (!existsSync6(path)) {
825
+ if (!existsSync5(path)) {
857
826
  const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
858
827
  mkdirSync3(dirname4(path), { recursive: true });
859
828
  writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
@@ -866,7 +835,7 @@ function ensureDeviceKey(path = deviceKeyPath()) {
866
835
  return { privateKey, publicKeyBase64, created };
867
836
  }
868
837
  function loadCredentials(path = credentialsPath()) {
869
- if (!existsSync6(path)) return null;
838
+ if (!existsSync5(path)) return null;
870
839
  try {
871
840
  return JSON.parse(readFileSync5(path, "utf8"));
872
841
  } catch {
@@ -1010,7 +979,7 @@ async function getAccessToken() {
1010
979
  // src/dashboard/onboarding-scan.ts
1011
980
  import { execFileSync } from "child_process";
1012
981
  import {
1013
- existsSync as existsSync7,
982
+ existsSync as existsSync6,
1014
983
  lstatSync,
1015
984
  mkdirSync as mkdirSync4,
1016
985
  readdirSync as readdirSync2,
@@ -1019,7 +988,7 @@ import {
1019
988
  symlinkSync
1020
989
  } from "fs";
1021
990
  import { homedir as homedir3 } from "os";
1022
- import { basename as basename2, join as join6, resolve as resolve4 } from "path";
991
+ import { basename as basename2, join as join6, resolve as resolve3 } from "path";
1023
992
  var MAX_JSONL_HEAD_BYTES = 64 * 1024;
1024
993
  var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
1025
994
  function flyspaceRoot() {
@@ -1041,7 +1010,7 @@ function codexDir() {
1041
1010
  }
1042
1011
  function claudeLoggedIn() {
1043
1012
  if (process.env["ANTHROPIC_API_KEY"]) return true;
1044
- if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
1013
+ if (existsSync6(join6(claudeDir(), ".credentials.json"))) return true;
1045
1014
  try {
1046
1015
  const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
1047
1016
  if (raw.oauthAccount) return true;
@@ -1058,7 +1027,7 @@ function claudeLoggedIn() {
1058
1027
  }
1059
1028
  }
1060
1029
  function codexLoggedIn() {
1061
- return !!process.env["OPENAI_API_KEY"] || existsSync7(join6(codexDir(), "auth.json"));
1030
+ return !!process.env["OPENAI_API_KEY"] || existsSync6(join6(codexDir(), "auth.json"));
1062
1031
  }
1063
1032
  function jsonlFiles(dir, depth) {
1064
1033
  const out = [];
@@ -1132,7 +1101,7 @@ function gitRepos() {
1132
1101
  continue;
1133
1102
  }
1134
1103
  for (const e of entries2) {
1135
- if (e.isDirectory() && existsSync7(join6(root, e.name, ".git"))) found.push(join6(root, e.name));
1104
+ if (e.isDirectory() && existsSync6(join6(root, e.name, ".git"))) found.push(join6(root, e.name));
1136
1105
  }
1137
1106
  }
1138
1107
  return found;
@@ -1157,7 +1126,7 @@ function scanOnboarding() {
1157
1126
  const byPath = /* @__PURE__ */ new Map();
1158
1127
  const history = { claude: false, codex: false };
1159
1128
  const add = (rawPath, source, mtime, sessions, trusted = false) => {
1160
- const path = resolve4(rawPath);
1129
+ const path = resolve3(rawPath);
1161
1130
  if (isExcluded(path)) return;
1162
1131
  const existing = byPath.get(path);
1163
1132
  if (existing) {
@@ -1249,7 +1218,7 @@ function linkProjectFolders(slug, items) {
1249
1218
  }
1250
1219
  if (!exists) {
1251
1220
  if (item.path) {
1252
- const target = resolve4(item.path);
1221
+ const target = resolve3(item.path);
1253
1222
  if (!statSync2(target).isDirectory()) throw new Error(`not a directory: ${target}`);
1254
1223
  makeLink(target, dest);
1255
1224
  } else {
@@ -1262,7 +1231,7 @@ function linkProjectFolders(slug, items) {
1262
1231
  }
1263
1232
 
1264
1233
  // src/commands/auth.ts
1265
- import { spawn as spawn3 } from "child_process";
1234
+ import { spawn as spawn2 } from "child_process";
1266
1235
  import { confirm } from "@inquirer/prompts";
1267
1236
  import { hostname, platform as platform4, release } from "os";
1268
1237
  var BOLD = "\x1B[1m";
@@ -1309,7 +1278,7 @@ function openBrowser(url) {
1309
1278
  if (process.env["SSH_CONNECTION"] || process.env["SSH_TTY"]) return false;
1310
1279
  const cmd = platform4() === "darwin" ? "open" : platform4() === "win32" ? "explorer" : "xdg-open";
1311
1280
  try {
1312
- const child = spawn3(cmd, [url], { stdio: "ignore", detached: true });
1281
+ const child = spawn2(cmd, [url], { stdio: "ignore", detached: true });
1313
1282
  child.on("error", () => {
1314
1283
  });
1315
1284
  child.unref();
@@ -1454,7 +1423,7 @@ async function runStatus() {
1454
1423
  const printLocal = (workspaces) => {
1455
1424
  console.log("");
1456
1425
  printIdentity({ ...creds, workspaces }, creds.deviceName);
1457
- console.log(` ${DIM}API${RESET} ${creds.apiUrl}`);
1426
+ if (creds.apiUrl !== DEFAULT_API_URL) console.log(` ${DIM}API${RESET} ${creds.apiUrl}`);
1458
1427
  console.log(` ${DIM}Key${RESET} ${tilde(deviceKeyPath())}`);
1459
1428
  console.log(` ${DIM}Credentials${RESET} ${tilde(credentialsPath())}`);
1460
1429
  };
@@ -1571,7 +1540,7 @@ async function runAuthCmd(args) {
1571
1540
  }
1572
1541
 
1573
1542
  // src/version.ts
1574
- var CLI_VERSION = true ? "0.1.10" : null;
1543
+ var CLI_VERSION = true ? "0.1.13" : null;
1575
1544
  function isOlderVersion(a, b) {
1576
1545
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1577
1546
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -1849,7 +1818,8 @@ ${BOLD3}Flyspace Doctor${RESET3}
1849
1818
  }
1850
1819
 
1851
1820
  // src/commands/uninstall.ts
1852
- import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
1821
+ import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
1822
+ import { confirm as confirm3 } from "@inquirer/prompts";
1853
1823
  import { execSync as execSync5 } from "child_process";
1854
1824
  import { basename as basename3, join as join8 } from "path";
1855
1825
  import { homedir as homedir4, platform as platform5 } from "os";
@@ -1866,14 +1836,13 @@ function warn2(msg) {
1866
1836
  function run2(cmd) {
1867
1837
  execSync5(cmd, { encoding: "utf-8", stdio: "pipe" });
1868
1838
  }
1869
- async function runUninstall() {
1839
+ async function runServiceUninstall(profile = getProfile()) {
1870
1840
  const os = platform5();
1871
- const profile = getProfile();
1872
1841
  log2("Uninstalling Flyspace service...");
1873
1842
  if (os === "darwin") {
1874
1843
  const removed = [];
1875
1844
  for (const plist of [plistPath(profile), legacyPlistPath(profile)]) {
1876
- if (!existsSync8(plist)) continue;
1845
+ if (!existsSync7(plist)) continue;
1877
1846
  try {
1878
1847
  run2(`launchctl unload "${plist}"`);
1879
1848
  } catch {
@@ -1908,11 +1877,51 @@ async function runUninstall() {
1908
1877
  } else {
1909
1878
  warn2(`Unsupported platform: ${os}`);
1910
1879
  }
1911
- console.log("\n Config and data directories were NOT removed.");
1880
+ }
1881
+ async function runUninstall(args = []) {
1882
+ const prefix = process.env["FLYSPACE_PREFIX"] || join8(homedir4(), ".flyspace");
1883
+ const targets = [paths.BASE_CONFIG_DIR, paths.BASE_DATA_DIR, prefix].filter(existsSync7);
1884
+ console.log("\n This removes Flyspace from this machine:");
1885
+ console.log(" - revokes this device and removes every background service");
1886
+ for (const t of targets) console.log(` - deletes ${t}`);
1887
+ console.log(" Your channel folders (~/Flyspace) are kept.\n");
1888
+ if (!args.includes("--yes") && !args.includes("-y")) {
1889
+ if (!await confirm3({ message: "Continue?", default: false })) {
1890
+ console.log(" Cancelled.");
1891
+ return;
1892
+ }
1893
+ }
1894
+ const creds = loadCredentials();
1895
+ if (creds) {
1896
+ try {
1897
+ const token = await getAccessToken();
1898
+ if (token) await revokeDevice(creds.apiUrl, token);
1899
+ success2("Device revoked.");
1900
+ } catch {
1901
+ warn2("Could not reach the server to revoke this device. Remove it from Settings later.");
1902
+ }
1903
+ }
1904
+ for (const profile of ["default", ...listProfiles()]) {
1905
+ await runServiceUninstall(profile);
1906
+ }
1907
+ for (const t of targets) {
1908
+ rmSync3(t, { recursive: true, force: true });
1909
+ success2(`Deleted ${t}`);
1910
+ }
1911
+ for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) => join8(homedir4(), f))) {
1912
+ if (!existsSync7(rc)) continue;
1913
+ const before = readFileSync7(rc, "utf-8");
1914
+ const after = before.replace(/\n# Flyspace CLI\nexport PATH="[^"\n]*\.flyspace\/bin:\$PATH"\n?/g, "\n");
1915
+ if (after !== before) {
1916
+ writeFileSync5(rc, after);
1917
+ success2(`Removed PATH entry from ${rc}`);
1918
+ }
1919
+ }
1920
+ console.log("\n Flyspace is uninstalled. Open a new terminal to finish.\n");
1912
1921
  }
1913
1922
 
1914
1923
  // src/commands/profiles-cmd.ts
1915
- import { rmSync as rmSync3 } from "fs";
1924
+ import { rmSync as rmSync4 } from "fs";
1916
1925
  var USAGE = `
1917
1926
  Usage: flyspace profiles [list]
1918
1927
  flyspace profiles remove <name>
@@ -1969,7 +1978,7 @@ async function removeCmd(name) {
1969
1978
  }
1970
1979
  const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
1971
1980
  if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
1972
- rmSync3(configDir2, { recursive: true, force: true });
1981
+ rmSync4(configDir2, { recursive: true, force: true });
1973
1982
  console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
1974
1983
  console.log(`
1975
1984
  Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
@@ -2023,7 +2032,7 @@ function npmEnv() {
2023
2032
  return home ? { ...process.env, PATH: `${join9(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2024
2033
  }
2025
2034
  async function runUpgrade() {
2026
- const current = "0.1.10";
2035
+ const current = "0.1.13";
2027
2036
  console.log(`Current version: ${current}`);
2028
2037
  console.log("Checking for updates...\n");
2029
2038
  try {
@@ -2394,7 +2403,7 @@ async function runAutomationCmd(args) {
2394
2403
  }
2395
2404
 
2396
2405
  // src/commands/usage.ts
2397
- import { existsSync as existsSync9 } from "fs";
2406
+ import { existsSync as existsSync8 } from "fs";
2398
2407
  var HELP4 = `
2399
2408
  Usage: flyspace usage [options]
2400
2409
 
@@ -2521,7 +2530,7 @@ async function runUsage(args) {
2521
2530
  return;
2522
2531
  }
2523
2532
  const { days, by, explain, json } = parsed;
2524
- if (!existsSync9(paths.DB_FILE)) {
2533
+ if (!existsSync8(paths.DB_FILE)) {
2525
2534
  console.log("No usage recorded yet (the database does not exist).");
2526
2535
  return;
2527
2536
  }
@@ -2635,21 +2644,21 @@ async function runRestart() {
2635
2644
  }
2636
2645
 
2637
2646
  // src/commands/logs.ts
2638
- import { spawn as spawn4 } from "child_process";
2639
- import { existsSync as existsSync10 } from "fs";
2647
+ import { spawn as spawn3 } from "child_process";
2648
+ import { existsSync as existsSync9 } from "fs";
2640
2649
  import { join as join10 } from "path";
2641
2650
  async function runLogs(args) {
2642
2651
  const showErrors = args.includes("--error") || args.includes("-e");
2643
2652
  const filename = showErrors ? "custie-error.log" : "custie.log";
2644
2653
  const logFile = join10(paths.LOG_DIR, filename);
2645
- if (!existsSync10(logFile)) {
2654
+ if (!existsSync9(logFile)) {
2646
2655
  console.error(`Log file not found: ${logFile}`);
2647
2656
  console.error("Is the service installed? Run `flyspace install` first.");
2648
2657
  process.exit(1);
2649
2658
  }
2650
2659
  console.log(`Tailing ${logFile} (Ctrl+C to stop)
2651
2660
  `);
2652
- const tail = spawn4("tail", ["-f", "-n", "50", logFile], {
2661
+ const tail = spawn3("tail", ["-f", "-n", "50", logFile], {
2653
2662
  stdio: "inherit"
2654
2663
  });
2655
2664
  tail.on("error", (err) => {
@@ -2663,10 +2672,10 @@ async function runLogs(args) {
2663
2672
 
2664
2673
  // src/commands/slack.ts
2665
2674
  import { execFileSync as execFileSync4 } from "child_process";
2666
- import { existsSync as existsSync27, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
2667
- import { resolve as resolve7 } from "path";
2675
+ import { existsSync as existsSync26, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
2676
+ import { resolve as resolve6 } from "path";
2668
2677
  import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
2669
- import { input, confirm as confirm3, select, checkbox } from "@inquirer/prompts";
2678
+ import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
2670
2679
  import { WebClient as WebClient3 } from "@slack/web-api";
2671
2680
 
2672
2681
  // src/store/auth-links.ts
@@ -3833,8 +3842,8 @@ function setupPage(c, token, invite, form = {}) {
3833
3842
  );
3834
3843
  }
3835
3844
  function createAuthConfirmRouter() {
3836
- const confirm4 = new Hono();
3837
- confirm4.get("/confirm", withResolvedProfile, async (c) => {
3845
+ const confirm5 = new Hono();
3846
+ confirm5.get("/confirm", withResolvedProfile, async (c) => {
3838
3847
  const settings = await loadWorkspaceBranding();
3839
3848
  const display = fontOf(settings.font).stack;
3840
3849
  const name = settings.displayName?.trim() || getProfile();
@@ -3871,7 +3880,7 @@ function createAuthConfirmRouter() {
3871
3880
  )
3872
3881
  );
3873
3882
  });
3874
- confirm4.post("/confirm", withResolvedProfile, async (c) => {
3883
+ confirm5.post("/confirm", withResolvedProfile, async (c) => {
3875
3884
  const form = await c.req.parseBody();
3876
3885
  const params = readParams({
3877
3886
  token: typeof form["token"] === "string" ? form["token"] : void 0,
@@ -3882,7 +3891,7 @@ function createAuthConfirmRouter() {
3882
3891
  if (!target) return c.text("SUPABASE_URL is not configured", 500);
3883
3892
  return c.redirect(target, 303);
3884
3893
  });
3885
- confirm4.get("/invite", withResolvedProfile, async (c) => {
3894
+ confirm5.get("/invite", withResolvedProfile, async (c) => {
3886
3895
  const token = c.req.query("token")?.trim() ?? "";
3887
3896
  const lookup = await findInvite(token);
3888
3897
  const brandingWorkspace = lookup.state === "valid" ? await getWorkspaceById(lookup.invite.workspaceId) : null;
@@ -3928,7 +3937,7 @@ function createAuthConfirmRouter() {
3928
3937
  )
3929
3938
  );
3930
3939
  });
3931
- confirm4.post("/invite", withResolvedProfile, async (c) => {
3940
+ confirm5.post("/invite", withResolvedProfile, async (c) => {
3932
3941
  const form = await c.req.parseBody();
3933
3942
  const token = typeof form["token"] === "string" ? form["token"].trim() : "";
3934
3943
  const back = `/auth/invite?token=${encodeURIComponent(token)}`;
@@ -3945,7 +3954,7 @@ function createAuthConfirmRouter() {
3945
3954
  if (!await consumeInvite(lookup.invite.id)) return c.redirect(back, 303);
3946
3955
  return c.redirect(target, 303);
3947
3956
  });
3948
- confirm4.get("/new-workspace", async (c) => {
3957
+ confirm5.get("/new-workspace", async (c) => {
3949
3958
  const token = c.req.query("token")?.trim() ?? "";
3950
3959
  const lookup = INVITE_TOKEN.test(token) ? await lookupSetupInvite(token) : { state: "unknown" };
3951
3960
  if (lookup.state === "valid") return c.html(setupPage(c, token, lookup.invite));
@@ -3973,7 +3982,7 @@ function createAuthConfirmRouter() {
3973
3982
  status
3974
3983
  );
3975
3984
  });
3976
- confirm4.post("/new-workspace", async (c) => {
3985
+ confirm5.post("/new-workspace", async (c) => {
3977
3986
  const form = await c.req.parseBody();
3978
3987
  const token = typeof form["token"] === "string" ? form["token"].trim() : "";
3979
3988
  const slug = typeof form["slug"] === "string" ? form["slug"].trim().toLowerCase() : "";
@@ -4080,7 +4089,7 @@ function createAuthConfirmRouter() {
4080
4089
  }
4081
4090
  return c.redirect(`${origin}/login?email=${encodeURIComponent(invite.email)}`, 303);
4082
4091
  });
4083
- return confirm4;
4092
+ return confirm5;
4084
4093
  }
4085
4094
  function confirmLinkFor(origin, actionLink, intent) {
4086
4095
  try {
@@ -4191,15 +4200,15 @@ async function deliverPasswordResetLink(email, redirectTo) {
4191
4200
  }
4192
4201
 
4193
4202
  // src/dashboard/server.ts
4194
- import { existsSync as existsSync26 } from "fs";
4203
+ import { existsSync as existsSync25 } from "fs";
4195
4204
  import { createServer } from "net";
4196
4205
  import { join as join23 } from "path";
4197
- import { Hono as Hono33 } from "hono";
4206
+ import { Hono as Hono34 } from "hono";
4198
4207
  import { serve } from "@hono/node-server";
4199
4208
  import { serveStatic } from "@hono/node-server/serve-static";
4200
4209
 
4201
4210
  // src/reconcile.ts
4202
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
4211
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
4203
4212
  async function boundWorkspaces2() {
4204
4213
  return (await boundWorkspaces())?.workspaces ?? null;
4205
4214
  }
@@ -4210,13 +4219,13 @@ async function servedWorkspaces2() {
4210
4219
  async function reconcileProfile(profile) {
4211
4220
  return runWithProfile(profile, async () => {
4212
4221
  const log5 = [];
4213
- const hadConfigDir = existsSync11(paths.CONFIG_DIR);
4214
- const hadDataDir = existsSync11(paths.DATA_DIR);
4222
+ const hadConfigDir = existsSync10(paths.CONFIG_DIR);
4223
+ const hadDataDir = existsSync10(paths.DATA_DIR);
4215
4224
  ensureDirs();
4216
4225
  if (!hadConfigDir) log5.push(`config dir created (${paths.CONFIG_DIR})`);
4217
4226
  if (!hadDataDir) log5.push(`data dir created (${paths.DATA_DIR})`);
4218
- if (!existsSync11(paths.CONFIG_FILE)) {
4219
- writeFileSync5(paths.CONFIG_FILE, `BOT_NAME=${profile}
4227
+ if (!existsSync10(paths.CONFIG_FILE)) {
4228
+ writeFileSync6(paths.CONFIG_FILE, `BOT_NAME=${profile}
4220
4229
  `);
4221
4230
  log5.push(`config.env written (${paths.CONFIG_FILE})`);
4222
4231
  }
@@ -4394,10 +4403,10 @@ var requireDocsAccess = async (c, next) => {
4394
4403
  };
4395
4404
 
4396
4405
  // src/dashboard/api.ts
4397
- import { Hono as Hono30 } from "hono";
4406
+ import { Hono as Hono31 } from "hono";
4398
4407
 
4399
4408
  // src/dashboard/public-share-routes.ts
4400
- import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
4409
+ import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
4401
4410
  import { join as join13 } from "path";
4402
4411
  import { Hono as Hono3 } from "hono";
4403
4412
  import { GetObjectCommand } from "@aws-sdk/client-s3";
@@ -4863,7 +4872,7 @@ function injectShareHead(html, head) {
4863
4872
 
4864
4873
  // src/dashboard/app-icon.ts
4865
4874
  import { createHash as createHash2 } from "crypto";
4866
- import { readFileSync as readFileSync7 } from "fs";
4875
+ import { readFileSync as readFileSync8 } from "fs";
4867
4876
  import { join as join12 } from "path";
4868
4877
  import sharp from "sharp";
4869
4878
  var PLATE = "#0A0A0F";
@@ -4891,7 +4900,7 @@ function roundedPlate(size, fill = PLATE) {
4891
4900
  }
4892
4901
  async function renderBadge(box) {
4893
4902
  const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
4894
- const raster = await sharp(readFileSync7(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
4903
+ const raster = await sharp(readFileSync8(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
4895
4904
  return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
4896
4905
  }
4897
4906
  async function sampleBackground(logo) {
@@ -4946,7 +4955,7 @@ async function renderAppIcon({
4946
4955
  if (hit) return hit;
4947
4956
  const out = logo && await composeIcon(logo, size, maskable, plain).catch(() => null) || // No workspace logo to lead with — the stock mark is still the best icon,
4948
4957
  // either on its usual dark plate or, for a favicon, trimmed and bare.
4949
- (transparent ? await renderBadge(size) : await sharp(readFileSync7(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
4958
+ (transparent ? await renderBadge(size) : await sharp(readFileSync8(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
4950
4959
  cache3.set(key, out);
4951
4960
  return out;
4952
4961
  }
@@ -5458,7 +5467,7 @@ function truncationNotice(lang, voice) {
5458
5467
  if (lang === "en" || lang.startsWith("en-")) return TRUNCATION_NOTICE.en;
5459
5468
  return null;
5460
5469
  }
5461
- var sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
5470
+ var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
5462
5471
  async function synthesizeWithRetry(script, voice) {
5463
5472
  let lastError;
5464
5473
  for (let attempt = 1; attempt <= SYNTH_ATTEMPTS; attempt += 1) {
@@ -6164,7 +6173,7 @@ var shellCache = null;
6164
6173
  function readShell(path) {
6165
6174
  const { mtimeMs } = statSync3(path);
6166
6175
  if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
6167
- const html = readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6176
+ const html = readFileSync9(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6168
6177
  shellCache = { mtimeMs, html };
6169
6178
  return html;
6170
6179
  }
@@ -6185,16 +6194,16 @@ function serveBuildFile(c, webDist, dir, name) {
6185
6194
  return c.text("not found", 404);
6186
6195
  }
6187
6196
  const path = join13(webDist, dir, name);
6188
- if (!existsSync12(path)) return c.text("not found", 404);
6197
+ if (!existsSync11(path)) return c.text("not found", 404);
6189
6198
  c.header("Content-Type", type);
6190
6199
  c.header("Cache-Control", "public, max-age=31536000, immutable");
6191
6200
  if (ext === ".css") {
6192
- return c.body(readFileSync8(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6201
+ return c.body(readFileSync9(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6193
6202
  }
6194
6203
  if (ext === ".js") {
6195
- return c.body(readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6204
+ return c.body(readFileSync9(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6196
6205
  }
6197
- return c.body(new Uint8Array(readFileSync8(path)));
6206
+ return c.body(new Uint8Array(readFileSync9(path)));
6198
6207
  }
6199
6208
  var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
6200
6209
  function servePublishedHtml(c, share) {
@@ -6331,7 +6340,7 @@ ${analytics}` : head);
6331
6340
  const lang = canonicalLanguage(requested);
6332
6341
  if (!isTranslatableLanguage(lang)) return next();
6333
6342
  const shell = join13(webDist, "index.html");
6334
- if (!existsSync12(shell)) return next();
6343
+ if (!existsSync11(shell)) return next();
6335
6344
  const share = await getDocShare(c.req.param("id"));
6336
6345
  if (!share) return next();
6337
6346
  if (requested !== lang) return c.redirect(`/share/${share.id}/${lang}`, 302);
@@ -6367,7 +6376,7 @@ ${analytics}` : head);
6367
6376
  if (!share) return next();
6368
6377
  if (share.kind === "html") return servePublishedHtml(c, share);
6369
6378
  const shell = join13(webDist, "index.html");
6370
- if (!existsSync12(shell)) return next();
6379
+ if (!existsSync11(shell)) return next();
6371
6380
  return readerPage(
6372
6381
  c,
6373
6382
  share,
@@ -6983,26 +6992,21 @@ async function resolveAgentIdentities() {
6983
6992
  );
6984
6993
  }
6985
6994
  async function offlineAgentIdentities() {
6986
- const dm = findChannelByAgentName(BOT_DM_KEY);
6987
- if (!dm) return [];
6988
- return [
6989
- {
6990
- name: BOT_DM_KEY,
6991
- label: BOT_LABEL,
6992
- kind: "main",
6993
- channelId: dm.id,
6994
- ...await loadBotIdentity()
6995
- }
6996
- ];
6995
+ const identity = await loadBotIdentity();
6996
+ const dm = findChannelByAgentName(BOT_DM_KEY) ?? await ensureAgentDmChannel(BOT_DM_KEY, {
6997
+ name: identity.displayName ?? BOT_LABEL,
6998
+ cwd: resolveChannelConfig("").cwd
6999
+ });
7000
+ return [{ name: BOT_DM_KEY, label: BOT_LABEL, kind: "main", channelId: dm.id, ...identity }];
6997
7001
  }
6998
7002
 
6999
7003
  // src/dashboard/host-api.ts
7000
- import { existsSync as existsSync21, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
7004
+ import { existsSync as existsSync20, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
7001
7005
  import { join as join20 } from "path";
7002
7006
  import sharp3 from "sharp";
7003
7007
 
7004
7008
  // src/dashboard/channels-routes.ts
7005
- import { existsSync as existsSync13, mkdirSync as mkdirSync7 } from "fs";
7009
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
7006
7010
  import { Hono as Hono9 } from "hono";
7007
7011
 
7008
7012
  // src/dashboard/feature-flags-routes.ts
@@ -7150,7 +7154,7 @@ function coerceMemberEmails(raw) {
7150
7154
  }
7151
7155
  function ensureCwdDir(cwd) {
7152
7156
  const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
7153
- if (!existsSync13(expanded)) mkdirSync7(expanded, { recursive: true });
7157
+ if (!existsSync12(expanded)) mkdirSync7(expanded, { recursive: true });
7154
7158
  }
7155
7159
  function toResponse(channel) {
7156
7160
  return {
@@ -7299,25 +7303,25 @@ function createChannelWriteRouter() {
7299
7303
  // src/dashboard/docs-routes.ts
7300
7304
  import {
7301
7305
  closeSync,
7302
- existsSync as existsSync17,
7306
+ existsSync as existsSync16,
7303
7307
  lstatSync as lstatSync2,
7304
7308
  mkdirSync as mkdirSync8,
7305
7309
  openSync,
7306
7310
  readSync,
7307
7311
  readdirSync as readdirSync4,
7308
- readFileSync as readFileSync11,
7312
+ readFileSync as readFileSync12,
7309
7313
  realpathSync as realpathSync3,
7310
7314
  renameSync,
7311
- rmSync as rmSync5,
7315
+ rmSync as rmSync6,
7312
7316
  statSync as statSync5,
7313
- writeFileSync as writeFileSync7
7317
+ writeFileSync as writeFileSync8
7314
7318
  } from "fs";
7315
- import { basename as basename6, dirname as dirname5, extname, join as join18, relative, resolve as resolve5, sep } from "path";
7319
+ import { basename as basename6, dirname as dirname5, extname, join as join18, relative, resolve as resolve4, sep } from "path";
7316
7320
  import { Hono as Hono10 } from "hono";
7317
7321
  import { streamSSE } from "hono/streaming";
7318
7322
 
7319
7323
  // src/dashboard/docs-watcher.ts
7320
- import { existsSync as existsSync14, watch } from "fs";
7324
+ import { existsSync as existsSync13, watch } from "fs";
7321
7325
  import { basename as basename4, join as join14 } from "path";
7322
7326
  var DEBOUNCE_MS = 200;
7323
7327
  var MAX_PENDING = 200;
@@ -7359,7 +7363,7 @@ function watchDocs(cwd, listener) {
7359
7363
  const current = entries.get(cwd);
7360
7364
  if (!current || filename === null) return;
7361
7365
  const rel = filename.split("\\").join("/");
7362
- if (rel === root && !existsSync14(join14(cwd, rel))) return;
7366
+ if (rel === root && !existsSync13(join14(cwd, rel))) return;
7363
7367
  if (isIgnoredDocPath(rel)) return;
7364
7368
  if (current.pending.size < MAX_PENDING) current.pending.add(rel);
7365
7369
  if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
@@ -7596,13 +7600,13 @@ async function readOrQueueDocAudio(input2) {
7596
7600
  }
7597
7601
 
7598
7602
  // src/dashboard/artifact-export.ts
7599
- import { readFileSync as readFileSync10 } from "fs";
7603
+ import { readFileSync as readFileSync11 } from "fs";
7600
7604
 
7601
7605
  // src/dashboard/chat-agent.ts
7602
7606
  import { homedir as homedir7 } from "os";
7603
7607
 
7604
7608
  // src/mcp/config.ts
7605
- import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
7609
+ import { mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
7606
7610
  import { tmpdir as tmpdir2 } from "os";
7607
7611
  import { join as join15 } from "path";
7608
7612
 
@@ -8113,7 +8117,7 @@ function needsRefresh(expiresAt, now = Date.now()) {
8113
8117
  if (!expiresAt) return false;
8114
8118
  return new Date(expiresAt).getTime() - now < REFRESH_SKEW_MS;
8115
8119
  }
8116
- var sleep3 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
8120
+ var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
8117
8121
  async function refreshConnection(params) {
8118
8122
  const { workspaceId, connection, provider } = params;
8119
8123
  const deps = params.deps ?? {};
@@ -8292,10 +8296,10 @@ async function buildMcpConfig(profile, connectionIds) {
8292
8296
  function writeMcpConfigFile(config) {
8293
8297
  const dir = mkdtempSync(join15(tmpdir2(), "flyspace-mcp-"));
8294
8298
  const path = join15(dir, "mcp-config.json");
8295
- writeFileSync6(path, JSON.stringify(config), { mode: 384 });
8299
+ writeFileSync7(path, JSON.stringify(config), { mode: 384 });
8296
8300
  return {
8297
8301
  path,
8298
- cleanup: () => rmSync4(dir, { recursive: true, force: true })
8302
+ cleanup: () => rmSync5(dir, { recursive: true, force: true })
8299
8303
  };
8300
8304
  }
8301
8305
  async function withMcpConfig(profile, connectionIds, fn) {
@@ -8312,7 +8316,7 @@ async function withMcpConfig(profile, connectionIds, fn) {
8312
8316
  }
8313
8317
 
8314
8318
  // src/claude/agents.ts
8315
- import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
8319
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
8316
8320
  import { homedir as homedir6 } from "os";
8317
8321
  import { basename as basename5, join as join16 } from "path";
8318
8322
  import yaml from "js-yaml";
@@ -8327,7 +8331,7 @@ function expandHome(p) {
8327
8331
  function parseFrontmatter(file) {
8328
8332
  let content;
8329
8333
  try {
8330
- content = readFileSync9(file, "utf-8");
8334
+ content = readFileSync10(file, "utf-8");
8331
8335
  } catch {
8332
8336
  return {};
8333
8337
  }
@@ -8343,7 +8347,7 @@ function parseFrontmatter(file) {
8343
8347
  }
8344
8348
  }
8345
8349
  function collectFrom(root, source) {
8346
- if (!existsSync15(root)) return [];
8350
+ if (!existsSync14(root)) return [];
8347
8351
  let entries2;
8348
8352
  try {
8349
8353
  entries2 = readdirSync3(root).filter((f) => f.endsWith(".md"));
@@ -8373,7 +8377,7 @@ function readAgentPersona(configDir2, cwd, name) {
8373
8377
  const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
8374
8378
  if (!agent) return void 0;
8375
8379
  try {
8376
- const content = readFileSync9(agent.path, "utf-8");
8380
+ const content = readFileSync10(agent.path, "utf-8");
8377
8381
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
8378
8382
  } catch {
8379
8383
  return void 0;
@@ -8606,7 +8610,7 @@ function formatMemoryPrompt(channelId, memories) {
8606
8610
  }
8607
8611
 
8608
8612
  // src/dashboard/rollover-summary.ts
8609
- import { spawn as spawn5 } from "child_process";
8613
+ import { spawn as spawn4 } from "child_process";
8610
8614
  import { tmpdir as tmpdir3 } from "os";
8611
8615
  var HAIKU_MODEL = "haiku";
8612
8616
  var CJK_PATTERN = /[ -〿぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/;
@@ -8647,7 +8651,7 @@ Rules:
8647
8651
  - No preamble like "Summary:" \u2014 start directly with the summary text.`;
8648
8652
  var HAIKU_TIMEOUT_MS = 6e4;
8649
8653
  function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8650
- return new Promise((resolve8, reject) => {
8654
+ return new Promise((resolve7, reject) => {
8651
8655
  const args = [
8652
8656
  "--print",
8653
8657
  "--model",
@@ -8666,7 +8670,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8666
8670
  if (claudeConfigDir) env["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
8667
8671
  if (claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = claudeCodeOauthToken;
8668
8672
  else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
8669
- const child = spawn5("claude", args, {
8673
+ const child = spawn4("claude", args, {
8670
8674
  // A stateless rewrite touches no files — tmpdir keeps it that way and
8671
8675
  // avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
8672
8676
  // unlike the channel's own cwd the main turn runs in.
@@ -8698,7 +8702,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8698
8702
  reject(new Error("claude -p --model haiku returned empty output"));
8699
8703
  return;
8700
8704
  }
8701
- resolve8(text);
8705
+ resolve7(text);
8702
8706
  });
8703
8707
  });
8704
8708
  }
@@ -8945,11 +8949,11 @@ async function downloadChatAttachments(body) {
8945
8949
  }
8946
8950
 
8947
8951
  // src/dashboard/doc-mention-guard.ts
8948
- import { existsSync as existsSync16, statSync as statSync4 } from "fs";
8952
+ import { existsSync as existsSync15, statSync as statSync4 } from "fs";
8949
8953
  var DOC_MENTION_RE = /(?<![\w@/`])@((?:[\w.-]+\/)+[\w.-]+\.[A-Za-z0-9]{1,6})(?::(\d+))?(?![\w-])/g;
8950
8954
  function isRealDoc(root, rel) {
8951
8955
  const abs = resolveDocPath(root, rel);
8952
- if (!abs || !existsSync16(abs)) return false;
8956
+ if (!abs || !existsSync15(abs)) return false;
8953
8957
  try {
8954
8958
  return statSync4(abs).isFile();
8955
8959
  } catch {
@@ -9460,12 +9464,14 @@ var ChatAgent = class {
9460
9464
  };
9461
9465
  }
9462
9466
  }
9463
- const docsAccess = await this.resolveSenderDocsAccess(
9464
- owningProfile,
9465
- msg.author,
9466
- thread.channel_id
9467
- );
9468
- if (docsAccess && docsAccess !== "write") channelConfig = { ...channelConfig, docsAccess };
9467
+ const { access: docsAccess, unverified: docsAccessUnverified } = await this.resolveSenderDocsAccess(owningProfile, msg.author, thread.channel_id);
9468
+ if (docsAccess && docsAccess !== "write") {
9469
+ channelConfig = {
9470
+ ...channelConfig,
9471
+ docsAccess,
9472
+ ...docsAccessUnverified ? { docsAccessUnverified } : {}
9473
+ };
9474
+ }
9469
9475
  const agentMemory = await listEnabledFeatureFlagsBySlug(owningProfile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
9470
9476
  console.error("[chat-agent] feature flag lookup failed:", err.message);
9471
9477
  return false;
@@ -9482,7 +9488,7 @@ var ChatAgent = class {
9482
9488
  try {
9483
9489
  const channelKey = `chat:${thread.channel_id}`;
9484
9490
  const session = store.getSession(channelKey, msg.threadId);
9485
- const docsNote = docsAccessPromptNote(channelConfig.docsAccess);
9491
+ const docsNote = docsAccessPromptNote(channelConfig.docsAccess, channelConfig.docsAccessUnverified);
9486
9492
  const header = (docsNote ? `[access: ${docsNote}]
9487
9493
 
9488
9494
  ` : "") + `[context: chat channel=${thread.channel_id}` + (thread.title ? `, thread="${thread.title}"` : "") + `, sender=${msg.author}. This is the Flyspace dashboard, not Slack \u2014 it renders full Markdown, so use normal paragraphs, line breaks, and real Markdown lists instead of Slack-style single-line replies. When you name a file that lives in this channel's folder, write it as @ + its cwd-relative path (@docs/spec.md, @web/src/App.tsx) as plain text, never inside backticks or a Markdown link \u2014 the dashboard turns that into a chip the reader clicks to open the file. A trailing line number (@src/app.ts:120) is fine. Paths outside the cwd stay in backticks.]
@@ -9750,22 +9756,22 @@ ${msg.body}` : msg.body;
9750
9756
  let ready;
9751
9757
  try {
9752
9758
  channel = this.supabase().channel(`agent-thinking:${threadId}`);
9753
- ready = new Promise((resolve8) => {
9759
+ ready = new Promise((resolve7) => {
9754
9760
  const timeout = setTimeout(() => {
9755
9761
  console.error(`[chat-agent] thinking-channel ${threadId}: subscribe timed out after 10s`);
9756
- resolve8(false);
9762
+ resolve7(false);
9757
9763
  }, 1e4);
9758
9764
  channel.subscribe((status, err) => {
9759
9765
  if (status === "SUBSCRIBED") {
9760
9766
  clearTimeout(timeout);
9761
- resolve8(true);
9767
+ resolve7(true);
9762
9768
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
9763
9769
  console.error(
9764
9770
  `[chat-agent] thinking-channel ${threadId}: ${status}`,
9765
9771
  err?.message ?? ""
9766
9772
  );
9767
9773
  clearTimeout(timeout);
9768
- resolve8(false);
9774
+ resolve7(false);
9769
9775
  }
9770
9776
  });
9771
9777
  });
@@ -9812,19 +9818,19 @@ ${msg.body}` : msg.body;
9812
9818
  let ready;
9813
9819
  try {
9814
9820
  channel = this.supabase().channel("channel-activity");
9815
- ready = new Promise((resolve8) => {
9821
+ ready = new Promise((resolve7) => {
9816
9822
  const timeout = setTimeout(() => {
9817
9823
  console.error("[chat-agent] channel-activity: subscribe timed out after 10s");
9818
- resolve8(false);
9824
+ resolve7(false);
9819
9825
  }, 1e4);
9820
9826
  channel.subscribe((status, err) => {
9821
9827
  if (status === "SUBSCRIBED") {
9822
9828
  clearTimeout(timeout);
9823
- resolve8(true);
9829
+ resolve7(true);
9824
9830
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
9825
9831
  console.error(`[chat-agent] channel-activity: ${status}`, err?.message ?? "");
9826
9832
  clearTimeout(timeout);
9827
- resolve8(false);
9833
+ resolve7(false);
9828
9834
  }
9829
9835
  });
9830
9836
  });
@@ -9851,15 +9857,17 @@ ${msg.body}` : msg.body;
9851
9857
  /**
9852
9858
  * The sender's Docs level on `channelId`, or undefined when no restriction
9853
9859
  * applies (non-human author, or a profile with no Supabase workspace). A
9854
- * lookup failure resolves to 'none': better to withhold files than leak them.
9860
+ * lookup failure resolves to 'none' (better to withhold files than leak
9861
+ * them), flagged `unverified` so the agent reports a system error instead of
9862
+ * telling the sender they lack access.
9855
9863
  */
9856
9864
  async resolveSenderDocsAccess(profile, author, channelId) {
9857
- if (!isHumanAuthor(author)) return void 0;
9865
+ if (!isHumanAuthor(author)) return {};
9858
9866
  try {
9859
- return await resolveDocsAccessByHandle(profile, author, channelId) ?? void 0;
9867
+ return { access: await resolveDocsAccessByHandle(profile, author, channelId) ?? void 0 };
9860
9868
  } catch (err) {
9861
9869
  console.error("[chat-agent] docs access lookup failed:", err.message);
9862
- return "none";
9870
+ return { access: "none", unverified: true };
9863
9871
  }
9864
9872
  }
9865
9873
  async tryAsk(channelConfig, prompt, sessionId, onActivity, signal, usageContext) {
@@ -9886,6 +9894,7 @@ ${msg.body}` : msg.body;
9886
9894
  mcpConfigPath,
9887
9895
  ...usageContext ? { usageContext } : {},
9888
9896
  ...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {},
9897
+ ...channelConfig.docsAccessUnverified ? { docsAccessUnverified: true } : {},
9889
9898
  ...channelConfig.memoryPrompt ? { memoryPrompt: channelConfig.memoryPrompt } : {},
9890
9899
  ...channelConfig.disableAutoMemory ? { disableAutoMemory: true } : {}
9891
9900
  },
@@ -9925,7 +9934,7 @@ ${msg.body}` : msg.body;
9925
9934
  resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
9926
9935
  ),
9927
9936
  channelConfig.memoryPrompt,
9928
- docsAccessPromptNote(channelConfig.docsAccess)
9937
+ docsAccessPromptNote(channelConfig.docsAccess, channelConfig.docsAccessUnverified)
9929
9938
  ].filter(Boolean).join("\n\n---\n\n"),
9930
9939
  // Codex has no per-tool deny list, so any restricted requester gets
9931
9940
  // its read-only sandbox (no file writes). It can still read: see the
@@ -10202,7 +10211,7 @@ async function exportDocAsArtifact(channelId, docPath) {
10202
10211
  );
10203
10212
  const abs = resolveDocPath(channelConfig.cwd, docPath);
10204
10213
  if (!abs) throw new Error(`doc not found: ${docPath}`);
10205
- const content = readFileSync10(abs, "utf8");
10214
+ const content = readFileSync11(abs, "utf8");
10206
10215
  const title = extractTitle(abs, "md");
10207
10216
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10208
10217
  const prompt = `Publish the document at \`${docPath}\` (already read below) as a Claude Artifact, then record the result as a stub file under this channel's \`docs/artifacts/\` folder (create the folder if it doesn't exist).
@@ -10431,7 +10440,7 @@ function extractTitle(absPath, kind) {
10431
10440
  }
10432
10441
  function resolveDirPath(root, rel) {
10433
10442
  if (rel.includes("\0")) return null;
10434
- const abs = resolve5(root, rel);
10443
+ const abs = resolve4(root, rel);
10435
10444
  if (abs !== root && !abs.startsWith(root + sep)) return null;
10436
10445
  return abs;
10437
10446
  }
@@ -10481,12 +10490,12 @@ function readGitBranch(dir) {
10481
10490
  try {
10482
10491
  let gitDir = join18(dir, ".git");
10483
10492
  if (statSync5(gitDir).isFile()) {
10484
- const pointer = readFileSync11(gitDir, "utf8").trim();
10493
+ const pointer = readFileSync12(gitDir, "utf8").trim();
10485
10494
  const match = pointer.match(/^gitdir:\s*(.+)$/);
10486
10495
  if (!match) return void 0;
10487
- gitDir = resolve5(dir, match[1]);
10496
+ gitDir = resolve4(dir, match[1]);
10488
10497
  }
10489
- const head = readFileSync11(join18(gitDir, "HEAD"), "utf8").trim();
10498
+ const head = readFileSync12(join18(gitDir, "HEAD"), "utf8").trim();
10490
10499
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
10491
10500
  if (branchMatch) return branchMatch[1];
10492
10501
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -10515,7 +10524,7 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
10515
10524
  if (SKIP_DIRS.has(entry.name)) continue;
10516
10525
  const probe3 = probeDir(abs, showHidden);
10517
10526
  if (!probe3.hasDocs && probe3.hasOther) continue;
10518
- const isRepo = existsSync17(join18(abs, ".git"));
10527
+ const isRepo = existsSync16(join18(abs, ".git"));
10519
10528
  dirs.push({
10520
10529
  path,
10521
10530
  name: entry.name,
@@ -10571,7 +10580,7 @@ function flatDocs(root, {
10571
10580
  }
10572
10581
  function resolveDocPath(root, rel) {
10573
10582
  if (!rel || rel.includes("\0")) return null;
10574
- const abs = resolve5(root, rel);
10583
+ const abs = resolve4(root, rel);
10575
10584
  if (abs !== root && !abs.startsWith(root + sep)) return null;
10576
10585
  const ext = extname(abs).toLowerCase();
10577
10586
  if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return null;
@@ -10593,12 +10602,12 @@ function writeDocContent(root, rel, content, baseMtime) {
10593
10602
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) {
10594
10603
  return { status: "too-large" };
10595
10604
  }
10596
- writeFileSync7(abs, content, "utf8");
10605
+ writeFileSync8(abs, content, "utf8");
10597
10606
  return { status: "ok", mtime: statSync5(abs).mtime.toISOString() };
10598
10607
  }
10599
10608
  function resolveRawPath(root, rel) {
10600
10609
  if (!rel || rel.includes("\0")) return null;
10601
- const abs = resolve5(root, rel);
10610
+ const abs = resolve4(root, rel);
10602
10611
  if (abs !== root && !abs.startsWith(root + sep)) return null;
10603
10612
  const contentType = MEDIA_EXTENSIONS[extname(abs).toLowerCase()];
10604
10613
  if (!contentType) return null;
@@ -10606,14 +10615,14 @@ function resolveRawPath(root, rel) {
10606
10615
  }
10607
10616
  function resolveHtmlPath(root, rel) {
10608
10617
  if (!rel || rel.includes("\0")) return null;
10609
- const abs = resolve5(root, rel);
10618
+ const abs = resolve4(root, rel);
10610
10619
  if (abs !== root && !abs.startsWith(root + sep)) return null;
10611
10620
  if (DOC_EXTENSIONS[extname(abs).toLowerCase()] !== "html") return null;
10612
10621
  return abs;
10613
10622
  }
10614
10623
  function resolveDownloadPath(root, rel) {
10615
10624
  if (!rel || rel.includes("\0")) return null;
10616
- const abs = resolve5(root, rel);
10625
+ const abs = resolve4(root, rel);
10617
10626
  if (abs !== root && !abs.startsWith(root + sep)) return null;
10618
10627
  if (!isListableFile(basename6(abs))) return null;
10619
10628
  const contentType = MEDIA_EXTENSIONS[extname(abs).toLowerCase()] ?? "application/octet-stream";
@@ -10654,7 +10663,7 @@ function saveUploadedFile(root, relDir, name, data) {
10654
10663
  if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
10655
10664
  const final = uniqueName(absDir, safe);
10656
10665
  const abs = join18(absDir, final);
10657
- writeFileSync7(abs, data);
10666
+ writeFileSync8(abs, data);
10658
10667
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
10659
10668
  }
10660
10669
  function createDocFolder(root, relDir, name) {
@@ -10692,7 +10701,7 @@ function createDocFile(root, relDir, name, content) {
10692
10701
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
10693
10702
  const abs = join18(absDir, safe);
10694
10703
  try {
10695
- writeFileSync7(abs, content, { encoding: "utf8", flag: "wx" });
10704
+ writeFileSync8(abs, content, { encoding: "utf8", flag: "wx" });
10696
10705
  } catch (err) {
10697
10706
  if (err.code === "EEXIST") return { status: "exists" };
10698
10707
  throw err;
@@ -10701,7 +10710,7 @@ function createDocFile(root, relDir, name, content) {
10701
10710
  }
10702
10711
  function moveDocEntry(root, from, to) {
10703
10712
  if (!from || !to || from.includes("\0") || to.includes("\0")) return { status: "not-found" };
10704
- const absFrom = resolve5(root, from);
10713
+ const absFrom = resolve4(root, from);
10705
10714
  if (!absFrom.startsWith(root + sep)) return { status: "not-found" };
10706
10715
  let stat;
10707
10716
  try {
@@ -10711,7 +10720,7 @@ function moveDocEntry(root, from, to) {
10711
10720
  }
10712
10721
  const isDir2 = stat.isDirectory();
10713
10722
  if (!isDir2 && !stat.isFile()) return { status: "not-found" };
10714
- const absTo = resolve5(root, to);
10723
+ const absTo = resolve4(root, to);
10715
10724
  if (!absTo.startsWith(root + sep)) return { status: "bad-target" };
10716
10725
  const name = sanitizeUploadName(basename6(absTo));
10717
10726
  if (!name) return { status: "bad-target" };
@@ -10741,7 +10750,7 @@ function moveDocEntry(root, from, to) {
10741
10750
  }
10742
10751
  function deleteDocEntry(root, path) {
10743
10752
  if (!path || path.includes("\0")) return { status: "not-found" };
10744
- const abs = resolve5(root, path);
10753
+ const abs = resolve4(root, path);
10745
10754
  if (!abs.startsWith(root + sep)) return { status: "not-found" };
10746
10755
  let stat;
10747
10756
  try {
@@ -10751,17 +10760,17 @@ function deleteDocEntry(root, path) {
10751
10760
  }
10752
10761
  const isDir2 = stat.isDirectory();
10753
10762
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10754
- rmSync5(abs, { recursive: isDir2 });
10763
+ rmSync6(abs, { recursive: isDir2 });
10755
10764
  return {
10756
10765
  status: "ok",
10757
10766
  path: relative(root, abs).split(sep).join("/"),
10758
10767
  type: isDir2 ? "dir" : "file"
10759
10768
  };
10760
10769
  }
10761
- function createDocsRouter() {
10770
+ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10762
10771
  const app = new Hono10();
10763
- app.use("/:id/docs", requireDocsAccess);
10764
- app.use("/:id/docs/*", requireDocsAccess);
10772
+ app.use("/:id/docs", gate);
10773
+ app.use("/:id/docs/*", gate);
10765
10774
  app.get("/:id/docs", (c) => {
10766
10775
  const { cwd } = resolveChannelConfig(c.req.param("id"));
10767
10776
  const dir = c.req.query("path") ?? "";
@@ -10790,7 +10799,7 @@ function createDocsRouter() {
10790
10799
  }
10791
10800
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10792
10801
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10793
- const data = readFileSync11(resolved2.abs);
10802
+ const data = readFileSync12(resolved2.abs);
10794
10803
  const headers = { "Content-Type": resolved2.contentType };
10795
10804
  if (download) {
10796
10805
  const filename = basename6(resolved2.abs).replace(/"/g, "");
@@ -10810,7 +10819,7 @@ function createDocsRouter() {
10810
10819
  }
10811
10820
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10812
10821
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10813
- return c.body(new Uint8Array(readFileSync11(abs)), 200, {
10822
+ return c.body(new Uint8Array(readFileSync12(abs)), 200, {
10814
10823
  "Content-Type": "text/html; charset=utf-8",
10815
10824
  "Content-Security-Policy": HTML_SHARE_CSP,
10816
10825
  "X-Content-Type-Options": "nosniff",
@@ -10995,7 +11004,7 @@ function createDocsRouter() {
10995
11004
  title: kind ? extractTitle(abs, kind) : basename6(abs),
10996
11005
  mtime: stat.mtime.toISOString(),
10997
11006
  tooLarge: tooLarge || void 0,
10998
- content: tooLarge ? "" : readFileSync11(abs, "utf8")
11007
+ content: tooLarge ? "" : readFileSync12(abs, "utf8")
10999
11008
  });
11000
11009
  });
11001
11010
  app.put("/:id/docs/content", async (c) => {
@@ -11038,7 +11047,7 @@ function createDocsRouter() {
11038
11047
  channel,
11039
11048
  rel,
11040
11049
  title: extractTitle(abs, "md"),
11041
- content: readFileSync11(abs, "utf8")
11050
+ content: readFileSync12(abs, "utf8")
11042
11051
  };
11043
11052
  };
11044
11053
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -11234,13 +11243,13 @@ function relay(agent, pathOf) {
11234
11243
  }
11235
11244
 
11236
11245
  // src/dashboard/skills-routes.ts
11237
- import { existsSync as existsSync19, statSync as statSync7 } from "fs";
11246
+ import { existsSync as existsSync18, statSync as statSync7 } from "fs";
11238
11247
  import { dirname as dirname6 } from "path";
11239
11248
  import { Hono as Hono12 } from "hono";
11240
11249
 
11241
11250
  // src/dashboard/skill-mirror.ts
11242
11251
  import { createHash as createHash8 } from "crypto";
11243
- import { existsSync as existsSync18, statSync as statSync6 } from "fs";
11252
+ import { existsSync as existsSync17, statSync as statSync6 } from "fs";
11244
11253
  var SYNC_INTERVAL_MS = 6e4;
11245
11254
  var UPSERT_BATCH = 50;
11246
11255
  function mirrorScopeOf(ref) {
@@ -11248,7 +11257,7 @@ function mirrorScopeOf(ref) {
11248
11257
  }
11249
11258
  function isDir(path) {
11250
11259
  try {
11251
- return existsSync18(path) && statSync6(path).isDirectory();
11260
+ return existsSync17(path) && statSync6(path).isDirectory();
11252
11261
  } catch {
11253
11262
  return false;
11254
11263
  }
@@ -11389,7 +11398,7 @@ function requireAdmin2(auth) {
11389
11398
  }
11390
11399
  function rootExists(dir) {
11391
11400
  try {
11392
- return existsSync19(dir) && statSync7(dir).isDirectory();
11401
+ return existsSync18(dir) && statSync7(dir).isDirectory();
11393
11402
  } catch {
11394
11403
  return false;
11395
11404
  }
@@ -11578,9 +11587,9 @@ function registerMirroredSkillRoutes(api, agent) {
11578
11587
 
11579
11588
  // src/dashboard/providers.ts
11580
11589
  import { execFileSync as execFileSync2 } from "child_process";
11581
- import { existsSync as existsSync20 } from "fs";
11590
+ import { existsSync as existsSync19 } from "fs";
11582
11591
  import { homedir as homedir8 } from "os";
11583
- import { join as join19, resolve as resolve6 } from "path";
11592
+ import { join as join19, resolve as resolve5 } from "path";
11584
11593
  var PROVIDER_LABELS = {
11585
11594
  claude: "Claude Code",
11586
11595
  codex: "Codex"
@@ -11597,8 +11606,8 @@ function binaryOnPath(bin) {
11597
11606
  }
11598
11607
  function claudeAuthAvailable() {
11599
11608
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
11600
- const configDir2 = profileClaudeConfigDir() || resolve6(homedir8(), ".claude");
11601
- if (existsSync20(join19(configDir2, ".credentials.json"))) return true;
11609
+ const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
11610
+ if (existsSync19(join19(configDir2, ".credentials.json"))) return true;
11602
11611
  if (process.platform !== "darwin") return false;
11603
11612
  try {
11604
11613
  execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
@@ -11626,13 +11635,23 @@ function probe2(runner) {
11626
11635
  }
11627
11636
  return { runner, label: label2, connected: true };
11628
11637
  }
11638
+ function withAuthFailure(status) {
11639
+ if (!claudeAuthFailed()) return status;
11640
+ return {
11641
+ ...status,
11642
+ connected: false,
11643
+ reason: `Sign-in expired and could not be refreshed. On the Host, run claude setup-token, add the token to ~/.config/flyspace/profiles/${getProfile()}/config.env as CLAUDE_CODE_OAUTH_TOKEN, then restart the Host.`
11644
+ };
11645
+ }
11629
11646
  function providerStatuses() {
11630
11647
  const key = getProfile();
11631
11648
  const hit = cache6.get(key);
11632
- if (hit && Date.now() - hit.at < TTL_MS2) return hit.statuses;
11633
- const statuses = ["claude", "codex"].map(probe2);
11634
- cache6.set(key, { at: Date.now(), statuses });
11635
- return statuses;
11649
+ let statuses = hit && Date.now() - hit.at < TTL_MS2 ? hit.statuses : void 0;
11650
+ if (!statuses) {
11651
+ statuses = ["claude", "codex"].map(probe2);
11652
+ cache6.set(key, { at: Date.now(), statuses });
11653
+ }
11654
+ return statuses.map((st) => st.runner === "claude" && st.connected ? withAuthFailure(st) : st);
11636
11655
  }
11637
11656
 
11638
11657
  // src/dashboard/host-api.ts
@@ -11742,7 +11761,7 @@ function registerHostRoutes(api) {
11742
11761
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
11743
11762
  const filename = errors ? "custie-error.log" : "custie.log";
11744
11763
  const file = join20(paths.LOG_DIR, filename);
11745
- const exists = existsSync21(file);
11764
+ const exists = existsSync20(file);
11746
11765
  return c.json({
11747
11766
  file,
11748
11767
  exists,
@@ -11752,14 +11771,14 @@ function registerHostRoutes(api) {
11752
11771
  });
11753
11772
  }
11754
11773
  function tailFile(file, limit) {
11755
- if (!existsSync21(file)) return [];
11756
- const lines = readFileSync12(file, "utf-8").split("\n");
11774
+ if (!existsSync20(file)) return [];
11775
+ const lines = readFileSync13(file, "utf-8").split("\n");
11757
11776
  if (lines.at(-1) === "") lines.pop();
11758
11777
  return lines.slice(-limit);
11759
11778
  }
11760
11779
 
11761
11780
  // src/dashboard/data-api.ts
11762
- import { existsSync as existsSync23 } from "fs";
11781
+ import { existsSync as existsSync22 } from "fs";
11763
11782
  import sharp5 from "sharp";
11764
11783
 
11765
11784
  // src/dashboard/automations-routes.ts
@@ -13533,7 +13552,7 @@ function createTranscribeRouter() {
13533
13552
  }
13534
13553
 
13535
13554
  // src/dashboard/avatars-routes.ts
13536
- import { randomUUID as randomUUID2 } from "crypto";
13555
+ import { createHash as createHash10, randomUUID as randomUUID2 } from "crypto";
13537
13556
  import { Hono as Hono20 } from "hono";
13538
13557
  import sharp4 from "sharp";
13539
13558
  import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
@@ -13541,6 +13560,16 @@ var AVATAR_SIZE = 192;
13541
13560
  var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
13542
13561
  var AVATAR_MIME_TYPES = ["image/png", "image/jpeg"];
13543
13562
  var KEY_PREFIX3 = "avatar";
13563
+ var GRAVATAR_TIMEOUT_MS = 5e3;
13564
+ async function fetchGravatar(email) {
13565
+ const hash = createHash10("sha256").update(email.trim().toLowerCase()).digest("hex");
13566
+ const res = await fetch(`https://gravatar.com/avatar/${hash}?s=${AVATAR_SIZE}&d=404`, {
13567
+ signal: AbortSignal.timeout(GRAVATAR_TIMEOUT_MS)
13568
+ }).catch(() => null);
13569
+ if (!res?.ok) return null;
13570
+ const type = res.headers.get("content-type")?.split(";")[0] ?? "";
13571
+ return new File([await res.arrayBuffer()], "gravatar", { type });
13572
+ }
13544
13573
  async function saveAvatarKey(userId, avatarKey) {
13545
13574
  const { data, error: getError } = await getSupabaseClient().auth.admin.getUserById(userId);
13546
13575
  if (getError || !data.user) {
@@ -13602,6 +13631,17 @@ function createAvatarsRouter() {
13602
13631
  }
13603
13632
  return c.json({ avatarUrl: await signAvatarUrl(key) }, 201);
13604
13633
  });
13634
+ app.post("/avatars/gravatar", async (c) => {
13635
+ const ctx = await resolveAuthContext(c);
13636
+ if (!ctx) return c.json({ error: "not signed in" }, 401);
13637
+ if (ctx.avatarKey) return c.json({ avatarUrl: await signAvatarUrl(ctx.avatarKey) });
13638
+ const file = await fetchGravatar(ctx.email);
13639
+ if (!file) return c.json({ avatarUrl: null });
13640
+ const stored = await storeAvatar(file, ctx.userId);
13641
+ if ("error" in stored) return c.json({ avatarUrl: null });
13642
+ await saveAvatarKey(ctx.userId, stored.key);
13643
+ return c.json({ avatarUrl: await signAvatarUrl(stored.key) }, 201);
13644
+ });
13605
13645
  app.delete("/avatars", async (c) => {
13606
13646
  const ctx = await resolveAuthContext(c);
13607
13647
  if (!ctx) return c.json({ error: "not signed in" }, 401);
@@ -14606,7 +14646,7 @@ async function sessionUsageForThreads(workspace, threadTsList) {
14606
14646
  }
14607
14647
 
14608
14648
  // src/dashboard/usage-report.ts
14609
- import { existsSync as existsSync22 } from "fs";
14649
+ import { existsSync as existsSync21 } from "fs";
14610
14650
  function mergeByLabel(buckets) {
14611
14651
  const merged = /* @__PURE__ */ new Map();
14612
14652
  for (const bucket of buckets) {
@@ -14652,7 +14692,7 @@ function toReport(days, rows, labelChannel) {
14652
14692
  };
14653
14693
  }
14654
14694
  function fromLocalStore(days, labelChannel) {
14655
- if (!existsSync22(paths.DB_FILE)) return { ...EMPTY, days };
14695
+ if (!existsSync21(paths.DB_FILE)) return { ...EMPTY, days };
14656
14696
  const window = days === 0 ? void 0 : days;
14657
14697
  const store = new UsageStore(paths.DB_FILE);
14658
14698
  try {
@@ -14694,7 +14734,7 @@ async function buildChannelUsageReport(channelId, days, labelChannel) {
14694
14734
  const { totals, byDay } = await channelUsageForWorkspace(workspace, aliasIds, window);
14695
14735
  return { days, totals: totals.runs > 0 ? totals : null, byDay };
14696
14736
  } catch {
14697
- if (!existsSync22(paths.DB_FILE)) return { days, totals: null, byDay: [] };
14737
+ if (!existsSync21(paths.DB_FILE)) return { days, totals: null, byDay: [] };
14698
14738
  const store = new UsageStore(paths.DB_FILE);
14699
14739
  try {
14700
14740
  const targetLabel = labelChannel(channelId);
@@ -14716,7 +14756,7 @@ async function buildThreadUsage(threadTs) {
14716
14756
  if (totals.runs === 0) return { threadTs, totals: null, byModel: [] };
14717
14757
  return { threadTs, totals, byModel };
14718
14758
  } catch {
14719
- if (!existsSync22(paths.DB_FILE)) return { threadTs, totals: null, byModel: [] };
14759
+ if (!existsSync21(paths.DB_FILE)) return { threadTs, totals: null, byModel: [] };
14720
14760
  const store = new UsageStore(paths.DB_FILE);
14721
14761
  try {
14722
14762
  const totals = store.threadTotals(threadTs);
@@ -15076,10 +15116,10 @@ function createOnboardingRouter(agent) {
15076
15116
  var HOST_STATUS_CHANNEL = "host-status";
15077
15117
  var channelReady = null;
15078
15118
  function getHostStatusChannel() {
15079
- channelReady ??= new Promise((resolve8) => {
15119
+ channelReady ??= new Promise((resolve7) => {
15080
15120
  const channel = getSupabaseClient().channel(HOST_STATUS_CHANNEL);
15081
15121
  channel.subscribe((state) => {
15082
- if (state === "SUBSCRIBED") resolve8(channel);
15122
+ if (state === "SUBSCRIBED") resolve7(channel);
15083
15123
  });
15084
15124
  });
15085
15125
  return channelReady;
@@ -15245,7 +15285,7 @@ function registerDataRoutes(api, agent = {}) {
15245
15285
  runs.set(name, { lastRun: run3.lastRunAt.toISOString(), lastStatus: run3.lastStatus });
15246
15286
  }
15247
15287
  } catch {
15248
- if (existsSync23(paths.DB_FILE)) {
15288
+ if (existsSync22(paths.DB_FILE)) {
15249
15289
  const runStore = new AutomationRunStore(paths.DB_FILE);
15250
15290
  try {
15251
15291
  for (const run3 of runStore.listLastRuns()) {
@@ -15307,7 +15347,7 @@ function registerDataRoutes(api, agent = {}) {
15307
15347
  const runs = (await recentRunsForWorkspace(getProfile(), 200)).map(toResponse5);
15308
15348
  return c.json({ runs });
15309
15349
  } catch {
15310
- if (!existsSync23(paths.DB_FILE)) return c.json({ runs: [] });
15350
+ if (!existsSync22(paths.DB_FILE)) return c.json({ runs: [] });
15311
15351
  const runStore = new AutomationRunStore(paths.DB_FILE);
15312
15352
  try {
15313
15353
  const runs = runStore.listRuns(200).map(toResponse5);
@@ -15429,7 +15469,7 @@ function registerDataRoutes(api, agent = {}) {
15429
15469
  } catch {
15430
15470
  latest = /* @__PURE__ */ new Map();
15431
15471
  totals = /* @__PURE__ */ new Map();
15432
- if (existsSync23(paths.DB_FILE)) {
15472
+ if (existsSync22(paths.DB_FILE)) {
15433
15473
  const usageStore = new UsageStore(paths.DB_FILE);
15434
15474
  try {
15435
15475
  latest = usageStore.latestByThread(threadTsList);
@@ -15484,28 +15524,94 @@ function registerDataRoutes(api, agent = {}) {
15484
15524
  // src/dashboard/device-auth-routes.ts
15485
15525
  import { verify } from "crypto";
15486
15526
  import { getConnInfo } from "@hono/node-server/conninfo";
15487
- import { Hono as Hono29 } from "hono";
15527
+ import { Hono as Hono30 } from "hono";
15488
15528
 
15489
15529
  // src/dashboard/agent-bridge.ts
15490
15530
  import { randomBytes as randomBytes8, randomUUID as randomUUID4, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
15491
- import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
15531
+
15532
+ // src/dashboard/docs-relay.ts
15533
+ import { Hono as Hono29 } from "hono";
15534
+ import { streamSSE as streamSSE2 } from "hono/streaming";
15535
+ var FORWARDED_HEADERS = ["content-type", "host", "accept-language"];
15536
+ var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
15537
+ async function handleDocsRequest(req) {
15538
+ const app = new Hono29();
15539
+ app.use("*", async (c, next) => {
15540
+ c.set("auth", req.auth);
15541
+ await next();
15542
+ });
15543
+ app.route("/channels", createDocsRouter({ gate: async (_c, next) => next() }));
15544
+ const res = await app.request(req.path, {
15545
+ method: req.method,
15546
+ headers: req.headers,
15547
+ ...req.body !== void 0 && { body: Buffer.from(req.body, "base64") }
15548
+ });
15549
+ const headers = {};
15550
+ for (const name of RETURNED_HEADERS) {
15551
+ const value = res.headers.get(name);
15552
+ if (value) headers[name] = value;
15553
+ }
15554
+ return { status: res.status, headers, body: Buffer.from(await res.arrayBuffer()).toString("base64") };
15555
+ }
15556
+ function registerRelayedDocsRoutes(api, agent) {
15557
+ api.get(
15558
+ "/channels/:id/docs/watch",
15559
+ requireDocsAccess,
15560
+ (c) => streamSSE2(c, async (stream) => {
15561
+ await stream.writeSSE({ event: "unavailable", data: "{}" });
15562
+ })
15563
+ );
15564
+ api.all("/channels/:id/docs", requireDocsAccess, relay2(agent));
15565
+ api.all("/channels/:id/docs/*", requireDocsAccess, relay2(agent));
15566
+ }
15567
+ function relay2(agent) {
15568
+ return async (c) => {
15569
+ const url = new URL(c.req.url);
15570
+ const path = url.pathname.slice(url.pathname.indexOf("/channels/")) + url.search;
15571
+ const headers = {};
15572
+ for (const name of FORWARDED_HEADERS) {
15573
+ const value = c.req.header(name);
15574
+ if (value) headers[name] = value;
15575
+ }
15576
+ const hasBody = c.req.method !== "GET" && c.req.method !== "HEAD";
15577
+ const body = hasBody ? Buffer.from(await c.req.arrayBuffer()).toString("base64") : void 0;
15578
+ const result = await agent.docsRequest?.({
15579
+ method: c.req.method,
15580
+ path,
15581
+ headers,
15582
+ body,
15583
+ auth: c.get("auth")
15584
+ });
15585
+ if (!result) {
15586
+ return c.json({ error: "Docs need the Host machine, which is not connected right now." }, 503);
15587
+ }
15588
+ const empty = c.req.method === "HEAD" || result.status === 204 || result.status === 304;
15589
+ return new Response(empty ? null : Buffer.from(result.body, "base64"), {
15590
+ status: result.status,
15591
+ headers: result.headers
15592
+ });
15593
+ };
15594
+ }
15595
+
15596
+ // src/dashboard/agent-bridge.ts
15597
+ import { existsSync as existsSync24, readFileSync as readFileSync17 } from "fs";
15492
15598
  import dotenv from "dotenv";
15493
15599
  import { WebSocket, WebSocketServer } from "ws";
15494
15600
 
15495
15601
  // src/dashboard/host-identity.ts
15496
15602
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
15497
- import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
15603
+ import { existsSync as existsSync23, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
15498
15604
  import { dirname as dirname7, join as join21 } from "path";
15499
15605
  function hostPrivateKeyPath() {
15500
15606
  return process.env["FLYSPACE_HOST_KEY_PATH"] || join21(paths.BASE_CONFIG_DIR, "host.key");
15501
15607
  }
15502
15608
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15503
- if (!existsSync24(path)) {
15609
+ if (!existsSync23(path)) {
15504
15610
  throw new Error(
15505
15611
  `No Host identity at ${path}. Run \`pnpm run generate-host-key\` on this machine, then set FLYSPACE_HOST_PUBLIC_KEY on the Data process to the printed public key.`
15506
15612
  );
15507
15613
  }
15508
- return createPrivateKey2(readFileSync13(path, "utf8"));
15614
+ return createPrivateKey2(readFileSync14(path, "utf8"));
15509
15615
  }
15510
15616
  function loadHostPublicKey() {
15511
15617
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -15521,7 +15627,7 @@ function loadHostPublicKey() {
15521
15627
  // src/dashboard/host-lock.ts
15522
15628
  import { execFileSync as execFileSync3 } from "child_process";
15523
15629
  import { randomBytes as randomBytes5 } from "crypto";
15524
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
15630
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
15525
15631
  import { join as join22 } from "path";
15526
15632
  function hostLockPath() {
15527
15633
  return join22(paths.BASE_DATA_DIR, "host.lock");
@@ -15539,7 +15645,7 @@ function isLiveFlyspace(pid) {
15539
15645
  }
15540
15646
  function readLockPid(path) {
15541
15647
  try {
15542
- const pid = Number(JSON.parse(readFileSync14(path, "utf8")).pid);
15648
+ const pid = Number(JSON.parse(readFileSync15(path, "utf8")).pid);
15543
15649
  return Number.isInteger(pid) && pid > 0 ? pid : null;
15544
15650
  } catch {
15545
15651
  return null;
@@ -15555,9 +15661,9 @@ function acquireHostLock() {
15555
15661
  });
15556
15662
  for (let attempt = 0; attempt < 2; attempt++) {
15557
15663
  try {
15558
- writeFileSync9(path, body, { flag: "wx", mode: 384 });
15664
+ writeFileSync10(path, body, { flag: "wx", mode: 384 });
15559
15665
  const release2 = () => {
15560
- if (readLockPid(path) === process.pid) rmSync6(path, { force: true });
15666
+ if (readLockPid(path) === process.pid) rmSync7(path, { force: true });
15561
15667
  };
15562
15668
  process.once("exit", release2);
15563
15669
  return release2;
@@ -15571,7 +15677,7 @@ function acquireHostLock() {
15571
15677
  `Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
15572
15678
  );
15573
15679
  }
15574
- rmSync6(path, { force: true });
15680
+ rmSync7(path, { force: true });
15575
15681
  }
15576
15682
  }
15577
15683
  throw new Error(`Could not take the Host lock at ${path}`);
@@ -15580,13 +15686,13 @@ function publishHostEndpoint(port) {
15580
15686
  const path = hostLockPath();
15581
15687
  if (readLockPid(path) !== process.pid) return null;
15582
15688
  const token = randomBytes5(32).toString("hex");
15583
- const lock = JSON.parse(readFileSync14(path, "utf8"));
15584
- writeFileSync9(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
15689
+ const lock = JSON.parse(readFileSync15(path, "utf8"));
15690
+ writeFileSync10(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
15585
15691
  return token;
15586
15692
  }
15587
15693
  function readHostEndpoint() {
15588
15694
  try {
15589
- const { pid, port, token } = JSON.parse(readFileSync14(hostLockPath(), "utf8"));
15695
+ const { pid, port, token } = JSON.parse(readFileSync15(hostLockPath(), "utf8"));
15590
15696
  if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
15591
15697
  return { port, token };
15592
15698
  } catch {
@@ -15595,12 +15701,12 @@ function readHostEndpoint() {
15595
15701
  }
15596
15702
 
15597
15703
  // src/store/device-store.ts
15598
- import { createHash as createHash10, createPublicKey as createPublicKey3, randomBytes as randomBytes6, randomInt } from "crypto";
15704
+ import { createHash as createHash11, createPublicKey as createPublicKey3, randomBytes as randomBytes6, randomInt } from "crypto";
15599
15705
  var DEVICE_CODE_TTL_MS = 10 * 6e4;
15600
15706
  var ACCESS_TOKEN_TTL_MS = 15 * 6e4;
15601
15707
  var CHALLENGE_TTL_MS = 2 * 6e4;
15602
15708
  var POLL_INTERVAL_S = 5;
15603
- var sha256 = (s) => createHash10("sha256").update(s).digest("hex");
15709
+ var sha256 = (s) => createHash11("sha256").update(s).digest("hex");
15604
15710
  function parseEd25519PublicKey(b64) {
15605
15711
  if (typeof b64 !== "string" || b64.length > 200) return null;
15606
15712
  try {
@@ -15826,7 +15932,7 @@ async function userEmail(userId) {
15826
15932
 
15827
15933
  // src/dashboard/doc-share-publisher.ts
15828
15934
  import { randomBytes as randomBytes7 } from "crypto";
15829
- import { readFileSync as readFileSync15, statSync as statSync9 } from "fs";
15935
+ import { readFileSync as readFileSync16, statSync as statSync9 } from "fs";
15830
15936
  import { extname as extname2, posix } from "path";
15831
15937
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
15832
15938
 
@@ -16013,7 +16119,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16013
16119
  }
16014
16120
  if (!stat.isFile()) return { status: "not-found" };
16015
16121
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
16016
- const source = readFileSync15(abs, "utf8");
16122
+ const source = readFileSync16(abs, "utf8");
16017
16123
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
16018
16124
  if (kind === "excalidraw") {
16019
16125
  try {
@@ -16093,7 +16199,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16093
16199
  let text = null;
16094
16200
  try {
16095
16201
  const size = statSync9(docAbs).size;
16096
- if (size <= budget) text = readFileSync15(docAbs, "utf8");
16202
+ if (size <= budget) text = readFileSync16(docAbs, "utf8");
16097
16203
  } catch {
16098
16204
  continue;
16099
16205
  }
@@ -16110,7 +16216,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16110
16216
  let body;
16111
16217
  try {
16112
16218
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
16113
- body = readFileSync15(resolved2.abs);
16219
+ body = readFileSync16(resolved2.abs);
16114
16220
  } catch {
16115
16221
  continue;
16116
16222
  }
@@ -16201,9 +16307,13 @@ var METHODS = {
16201
16307
  agent_usage: ["GET", "HEAD", "POST"],
16202
16308
  skill_files: ALL,
16203
16309
  threads: ["GET", "HEAD", "POST", "PATCH"],
16204
- thread_replies: ["GET", "HEAD", "POST", "PATCH"]
16310
+ thread_replies: ["GET", "HEAD", "POST", "PATCH"],
16311
+ memberships: ["GET", "HEAD"],
16312
+ channel_members: ["GET", "HEAD"],
16313
+ channel_docs_access: ["GET", "HEAD"]
16205
16314
  };
16206
- var FORWARDED_HEADERS = ["accept", "content-type", "prefer", "range"];
16315
+ var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members", "channel_docs_access"]);
16316
+ var FORWARDED_HEADERS2 = ["accept", "content-type", "prefer", "range"];
16207
16317
  var deny = (message, status = 403) => ({
16208
16318
  status,
16209
16319
  headers: { "content-type": "application/json" },
@@ -16245,6 +16355,13 @@ async function ownedChannelIds(forward, slugs) {
16245
16355
  if (!rows) return null;
16246
16356
  return rows.flatMap((r) => [r.id, r.external_channel_id].filter((v) => !!v));
16247
16357
  }
16358
+ async function ownedWorkspaceIds(forward, slugs) {
16359
+ const rows = await getJson(
16360
+ forward,
16361
+ `/rest/v1/workspaces?select=id&slug=${encodeURIComponent(inList(slugs))}`
16362
+ );
16363
+ return rows ? rows.map((r) => r.id) : null;
16364
+ }
16248
16365
  async function threadsOwned(forward, threadIds, channelIds) {
16249
16366
  const wanted = [...new Set(threadIds)];
16250
16367
  if (wanted.length === 0 || channelIds.length === 0) return false;
@@ -16302,10 +16419,16 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
16302
16419
  }
16303
16420
  }
16304
16421
  params.append(slugColumn, inList(scope));
16422
+ } else if (table === "memberships") {
16423
+ const workspaceIds = await ownedWorkspaceIds(forward, workspaces);
16424
+ if (!workspaceIds) return deny("Could not resolve the device workspaces.", 502);
16425
+ params.append("workspace_id", inList(workspaceIds));
16305
16426
  } else {
16306
16427
  const channelIds = await ownedChannelIds(forward, workspaces);
16307
16428
  if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
16308
- if (table === "threads") {
16429
+ if (CHANNEL_SCOPED.has(table)) {
16430
+ params.append("channel_id", inList(channelIds));
16431
+ } else if (table === "threads") {
16309
16432
  if (method === "POST") {
16310
16433
  const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
16311
16434
  if (!ok3) return deny("A thread must belong to a channel of the device workspaces.");
@@ -16326,7 +16449,7 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
16326
16449
  }
16327
16450
  }
16328
16451
  const headers = {};
16329
- for (const name of FORWARDED_HEADERS) {
16452
+ for (const name of FORWARDED_HEADERS2) {
16330
16453
  const value = req.headers[name];
16331
16454
  if (value !== void 0) headers[name] = value;
16332
16455
  }
@@ -16390,14 +16513,14 @@ function handleHostDbResult(id, res) {
16390
16513
  }
16391
16514
  function whenConnected() {
16392
16515
  if (sender) return Promise.resolve(sender);
16393
- return new Promise((resolve8, reject) => {
16516
+ return new Promise((resolve7, reject) => {
16394
16517
  const timer2 = setTimeout(() => {
16395
16518
  onSender = onSender.filter((w) => w !== wake);
16396
16519
  reject(new Error("Host is not connected to the Data API"));
16397
16520
  }, REQUEST_TIMEOUT_MS3);
16398
16521
  const wake = () => {
16399
16522
  clearTimeout(timer2);
16400
- resolve8(sender);
16523
+ resolve7(sender);
16401
16524
  };
16402
16525
  onSender.push(wake);
16403
16526
  });
@@ -16405,12 +16528,12 @@ function whenConnected() {
16405
16528
  async function request(req) {
16406
16529
  const send2 = await whenConnected();
16407
16530
  const id = randomUUID3();
16408
- return new Promise((resolve8, reject) => {
16531
+ return new Promise((resolve7, reject) => {
16409
16532
  const timer2 = setTimeout(() => {
16410
16533
  pending.delete(id);
16411
16534
  reject(new Error("Data API did not answer in time"));
16412
16535
  }, REQUEST_TIMEOUT_MS3);
16413
- pending.set(id, { resolve: resolve8, reject, timer: timer2 });
16536
+ pending.set(id, { resolve: resolve7, reject, timer: timer2 });
16414
16537
  send2({ type: "db", id, req });
16415
16538
  });
16416
16539
  }
@@ -16470,7 +16593,7 @@ function dataSocketUrl() {
16470
16593
  return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
16471
16594
  }
16472
16595
  function listLocalSessions() {
16473
- if (!existsSync25(paths.DB_FILE)) return [];
16596
+ if (!existsSync24(paths.DB_FILE)) return [];
16474
16597
  const store = new SessionStore(paths.DB_FILE);
16475
16598
  try {
16476
16599
  return store.listSessions();
@@ -16496,6 +16619,7 @@ function localAgentCallbacks(chatAgent) {
16496
16619
  saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
16497
16620
  writeSkill: (ref, write) => writeSkillAndSync(ref, write),
16498
16621
  outputStylesRequest: (req) => handleOutputStylesRequest(req),
16622
+ docsRequest: (req) => handleDocsRequest(req),
16499
16623
  listSessions: () => listLocalSessions(),
16500
16624
  scanOnboarding: () => scanOnboarding(),
16501
16625
  linkProjectFolders: (slug, items) => linkProjectFolders(slug, items),
@@ -16507,7 +16631,7 @@ function buildHostConfigSnapshot() {
16507
16631
  const snapshot3 = {};
16508
16632
  for (const profile of listProfiles()) {
16509
16633
  snapshot3[profile] = runWithProfile(profile, () => ({
16510
- env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync16(paths.CONFIG_FILE, "utf-8")) : {},
16634
+ env: existsSync24(paths.CONFIG_FILE) ? dotenv.parse(readFileSync17(paths.CONFIG_FILE, "utf-8")) : {},
16511
16635
  agentModel: loadDashboardSettings().agent
16512
16636
  }));
16513
16637
  }
@@ -16519,6 +16643,7 @@ function startLocalProfileConfigHydration() {
16519
16643
  return () => clearInterval(timer2);
16520
16644
  }
16521
16645
  var CALL_TIMEOUT_MS = 15e3;
16646
+ var DOCS_CALL_TIMEOUT_MS = 12e4;
16522
16647
  var AUTH_TIMEOUT_MS = 5e3;
16523
16648
  var HEARTBEAT_INTERVAL_MS = 15e3;
16524
16649
  var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
@@ -16708,7 +16833,7 @@ function attachHostSocket(server) {
16708
16833
  });
16709
16834
  return wss;
16710
16835
  }
16711
- function callHost(method, args) {
16836
+ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
16712
16837
  const profile = getProfile();
16713
16838
  const ws = hostFor(profile);
16714
16839
  if (!ws) {
@@ -16718,16 +16843,16 @@ function callHost(method, args) {
16718
16843
  return Promise.resolve(null);
16719
16844
  }
16720
16845
  const id = randomUUID4();
16721
- return new Promise((resolve8) => {
16846
+ return new Promise((resolve7) => {
16722
16847
  const timer2 = setTimeout(() => {
16723
16848
  pending2.delete(id);
16724
16849
  console.error(`[host-socket] ${method} -> timed out`);
16725
- resolve8(null);
16726
- }, CALL_TIMEOUT_MS);
16850
+ resolve7(null);
16851
+ }, timeoutMs);
16727
16852
  pending2.set(id, {
16728
16853
  resolve: (value) => {
16729
16854
  clearTimeout(timer2);
16730
- resolve8(value);
16855
+ resolve7(value);
16731
16856
  }
16732
16857
  });
16733
16858
  send(ws, { type: "call", id, method, profile, args });
@@ -16778,6 +16903,9 @@ function remoteAgentCallbacks() {
16778
16903
  writeSkill: (ref, write) => callHost("writeSkill", [ref, write]),
16779
16904
  // Same posture as writeSkill: `null` becomes a 503.
16780
16905
  outputStylesRequest: (req) => callHost("outputStylesRequest", [req]),
16906
+ // Same posture again. Longer timeout: a first translation of a doc runs a
16907
+ // model call on the Host before it answers.
16908
+ docsRequest: (req) => callHost("docsRequest", [req], DOCS_CALL_TIMEOUT_MS),
16781
16909
  // `null` (no Host connected) is passed through rather than defaulting to
16782
16910
  // `[]` — see listSessions's doc comment on AgentCallbacks.
16783
16911
  listSessions: () => callHost("listSessions", []),
@@ -16915,7 +17043,7 @@ var RELAYED_METHODS = [
16915
17043
  "listAgentRuns"
16916
17044
  ];
16917
17045
  function relayedAgentCallbacks(local) {
16918
- const relay2 = (method) => async (...args) => {
17046
+ const relay3 = (method) => async (...args) => {
16919
17047
  const endpoint = readHostEndpoint();
16920
17048
  if (endpoint) {
16921
17049
  try {
@@ -16935,7 +17063,7 @@ function relayedAgentCallbacks(local) {
16935
17063
  };
16936
17064
  return {
16937
17065
  ...local,
16938
- ...Object.fromEntries(RELAYED_METHODS.map((method) => [method, relay2(method)]))
17066
+ ...Object.fromEntries(RELAYED_METHODS.map((method) => [method, relay3(method)]))
16939
17067
  };
16940
17068
  }
16941
17069
  async function handleLocalRelay(req, callbacks, token) {
@@ -17038,7 +17166,7 @@ async function boundWorkspaceIds(publicKey, userId) {
17038
17166
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
17039
17167
  }
17040
17168
  function createDeviceAuthRouter() {
17041
- const app = new Hono29();
17169
+ const app = new Hono30();
17042
17170
  app.post("/code", async (c) => {
17043
17171
  const body = await readBody(c);
17044
17172
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -17202,7 +17330,7 @@ function createDeviceAuthRouter() {
17202
17330
 
17203
17331
  // src/dashboard/api.ts
17204
17332
  function createApiRouter(agent = {}, mode = "all") {
17205
- const api = new Hono30();
17333
+ const api = new Hono31();
17206
17334
  api.use("*", async (c, next) => {
17207
17335
  const known = await listKnownWorkspaces();
17208
17336
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -17265,6 +17393,7 @@ function createApiRouter(agent = {}, mode = "all") {
17265
17393
  const entries2 = await agent.listFlatDocs?.({ cwd, query, sort, showHidden }) ?? [];
17266
17394
  return c.json({ root: cwd, query, entries: entries2 });
17267
17395
  });
17396
+ registerRelayedDocsRoutes(api, agent);
17268
17397
  api.get("/settings", async (c) => {
17269
17398
  const [branding, local] = await Promise.all([
17270
17399
  loadWorkspaceBranding(),
@@ -17313,7 +17442,7 @@ function createApiRouter(agent = {}, mode = "all") {
17313
17442
  }
17314
17443
 
17315
17444
  // src/dashboard/app-icon-routes.ts
17316
- import { Hono as Hono31 } from "hono";
17445
+ import { Hono as Hono32 } from "hono";
17317
17446
  var NEUTRAL_MANIFEST = {
17318
17447
  id: "/",
17319
17448
  name: "Flyspace",
@@ -17351,7 +17480,7 @@ async function withResolvedProfile2(c, next) {
17351
17480
  return profile ? runWithProfile(profile, () => next()) : next();
17352
17481
  }
17353
17482
  function createAppIconRouter() {
17354
- const app = new Hono31();
17483
+ const app = new Hono32();
17355
17484
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
17356
17485
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
17357
17486
  const profile = getProfile();
@@ -17391,7 +17520,7 @@ function pngResponse(c, png) {
17391
17520
  }
17392
17521
 
17393
17522
  // src/dashboard/stripe-routes.ts
17394
- import { Hono as Hono32 } from "hono";
17523
+ import { Hono as Hono33 } from "hono";
17395
17524
  function planStatusFor(status) {
17396
17525
  const known = ["trialing", "active", "paused", "canceled"];
17397
17526
  return known.includes(status) ? status : null;
@@ -17434,7 +17563,7 @@ async function handleCardAdded(session) {
17434
17563
  }
17435
17564
  }
17436
17565
  function createStripeWebhookRouter() {
17437
- const stripeRouter = new Hono32();
17566
+ const stripeRouter = new Hono33();
17438
17567
  stripeRouter.post("/webhook", async (c) => {
17439
17568
  const secret = process.env["STRIPE_WEBHOOK_SECRET"];
17440
17569
  const signature = c.req.header("stripe-signature");
@@ -17549,10 +17678,10 @@ var WEB_DIST = join23(paths.PACKAGE_ROOT, "web", "dist");
17549
17678
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
17550
17679
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
17551
17680
  for (let port = start; port < start + attempts; port++) {
17552
- const free = await new Promise((resolve8) => {
17681
+ const free = await new Promise((resolve7) => {
17553
17682
  const probe3 = createServer();
17554
- probe3.once("error", () => resolve8(false));
17555
- probe3.once("listening", () => probe3.close(() => resolve8(true)));
17683
+ probe3.once("error", () => resolve7(false));
17684
+ probe3.once("listening", () => probe3.close(() => resolve7(true)));
17556
17685
  probe3.listen(port, host);
17557
17686
  });
17558
17687
  if (free) return port;
@@ -17594,7 +17723,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17594
17723
  const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
17595
17724
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
17596
17725
  };
17597
- const app = new Hono33();
17726
+ const app = new Hono34();
17598
17727
  app.use("*", async (c, next) => {
17599
17728
  await next();
17600
17729
  if (c.req.path.startsWith("/assets/")) {
@@ -17619,14 +17748,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17619
17748
  } else {
17620
17749
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
17621
17750
  app.route("/", createAppIconRouter());
17622
- if (existsSync26(WEB_DIST) || existsSync26(join23(paths.PACKAGE_ROOT, "web", "package.json"))) {
17751
+ if (existsSync25(WEB_DIST) || existsSync25(join23(paths.PACKAGE_ROOT, "web", "package.json"))) {
17623
17752
  app.use("/*", serveStatic({ root: WEB_DIST }));
17624
17753
  }
17625
17754
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
17626
17755
  const spaFallback = serveStatic({ path: join23(WEB_DIST, "index.html") });
17627
17756
  app.get("*", async (c, next) => {
17628
- if (!existsSync26(join23(WEB_DIST, "index.html"))) {
17629
- const isCheckout = existsSync26(join23(paths.PACKAGE_ROOT, "web", "package.json"));
17757
+ if (!existsSync25(join23(WEB_DIST, "index.html"))) {
17758
+ const isCheckout = existsSync25(join23(paths.PACKAGE_ROOT, "web", "package.json"));
17630
17759
  return c.text(
17631
17760
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
17632
17761
  Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
@@ -18064,7 +18193,7 @@ async function threadReplies(client2, nameOrId, ts2) {
18064
18193
  async function postMessage(client2, args) {
18065
18194
  const channelArg = getArg2(args, "--channel");
18066
18195
  const textFilePath = getArg2(args, "--text-file");
18067
- const text = textFilePath ? readFileSync17(textFilePath, "utf8") : getArg2(args, "--text");
18196
+ const text = textFilePath ? readFileSync18(textFilePath, "utf8") : getArg2(args, "--text");
18068
18197
  const threadTs = getArg2(args, "--thread");
18069
18198
  const markdown = args.includes("--markdown");
18070
18199
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -18159,10 +18288,10 @@ function hasClaudeBinary() {
18159
18288
  }
18160
18289
  }
18161
18290
  function defaultClaudeConfigDir() {
18162
- return process.env.CLAUDE_CONFIG_DIR || resolve7(homedir9(), ".claude");
18291
+ return process.env.CLAUDE_CONFIG_DIR || resolve6(homedir9(), ".claude");
18163
18292
  }
18164
18293
  function hasClaudeCredentials(configDir2) {
18165
- if (existsSync27(resolve7(configDir2, ".credentials.json"))) {
18294
+ if (existsSync26(resolve6(configDir2, ".credentials.json"))) {
18166
18295
  return true;
18167
18296
  }
18168
18297
  if (process.platform === "darwin") {
@@ -18199,7 +18328,7 @@ async function ensureClaudeCodeReady() {
18199
18328
  console.log(" Flyspace spawns Claude Code as a subprocess. Without credentials, the bot");
18200
18329
  console.log(" will prompt /login inside Slack at runtime and you cannot complete the");
18201
18330
  console.log(" browser sign-in from there.\n");
18202
- if (claudeConfigDir !== resolve7(homedir9(), ".claude")) {
18331
+ if (claudeConfigDir !== resolve6(homedir9(), ".claude")) {
18203
18332
  console.log(" Tip: run with the same CLAUDE_CONFIG_DIR you use for this Claude Code install:");
18204
18333
  console.log(` \x1B[1mCLAUDE_CONFIG_DIR=${claudeConfigDir} claude\x1B[0m
18205
18334
  `);
@@ -18207,7 +18336,7 @@ async function ensureClaudeCodeReady() {
18207
18336
  console.log(" Open a new terminal and run \x1B[1mclaude\x1B[0m to complete the sign-in flow,");
18208
18337
  console.log(" then come back here.\n");
18209
18338
  }
18210
- const proceed = await confirm3({
18339
+ const proceed = await confirm4({
18211
18340
  message: "Continue setup anyway?",
18212
18341
  default: false
18213
18342
  });
@@ -18218,8 +18347,8 @@ async function ensureClaudeCodeReady() {
18218
18347
  return { claudeConfigDir };
18219
18348
  }
18220
18349
  function getManifestYaml(botName) {
18221
- const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
18222
- const raw = readFileSync17(manifestPath, "utf-8");
18350
+ const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
18351
+ const raw = readFileSync18(manifestPath, "utf-8");
18223
18352
  return raw.replace(/name: Custie/g, `name: ${botName}`);
18224
18353
  }
18225
18354
  async function stepPersonalise() {
@@ -18432,7 +18561,7 @@ async function stepBrowserTokens(botName) {
18432
18561
  })
18433
18562
  ));
18434
18563
  const DEBUG = process.argv.includes("--debug");
18435
- const USER_DATA_DIR = resolve7(tmpdir4(), "custie-playwright-profile");
18564
+ const USER_DATA_DIR = resolve6(tmpdir4(), "custie-playwright-profile");
18436
18565
  let context;
18437
18566
  try {
18438
18567
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -18464,14 +18593,14 @@ async function stepBrowserTokens(botName) {
18464
18593
  const appLink = page2.locator('a[href*="/apps/A"]').first();
18465
18594
  const hasExistingApp = await appLink.isVisible({ timeout: 3e3 }).catch(() => false);
18466
18595
  if (hasExistingApp) {
18467
- const tokenPromise = new Promise((resolve8) => {
18596
+ const tokenPromise = new Promise((resolve7) => {
18468
18597
  page2.on("request", (req) => {
18469
18598
  if (token) return;
18470
18599
  const postData = req.postData() || "";
18471
18600
  const match = postData.match(/xoxc-[a-f0-9-]+/);
18472
18601
  if (match) {
18473
18602
  token = match[0];
18474
- resolve8(token);
18603
+ resolve7(token);
18475
18604
  }
18476
18605
  });
18477
18606
  });
@@ -18522,7 +18651,7 @@ async function stepBrowserTokens(botName) {
18522
18651
  log4("Almost done! The browser will now open an install page.\n");
18523
18652
  console.log(' \x1B[1mPlease click "Install to Workspace", then click "Allow".\x1B[0m\n');
18524
18653
  await page2.goto(installUrl);
18525
- const installPage = await new Promise((resolve8, reject) => {
18654
+ const installPage = await new Promise((resolve7, reject) => {
18526
18655
  let settled = false;
18527
18656
  let interval;
18528
18657
  let timeout;
@@ -18535,7 +18664,7 @@ async function stepBrowserTokens(botName) {
18535
18664
  };
18536
18665
  const checkUrl = (p) => {
18537
18666
  if (p.url().includes("install-on-team?success=1")) {
18538
- finish(() => resolve8(p));
18667
+ finish(() => resolve7(p));
18539
18668
  }
18540
18669
  };
18541
18670
  context.on("page", (newPage) => {
@@ -18594,7 +18723,7 @@ async function stepBrowserTokens(botName) {
18594
18723
  try {
18595
18724
  const pages = context?.pages();
18596
18725
  if (pages?.[0]) {
18597
- const screenshotPath = resolve7(paths.PACKAGE_ROOT, "setup-error.png");
18726
+ const screenshotPath = resolve6(paths.PACKAGE_ROOT, "setup-error.png");
18598
18727
  await pages[0].screenshot({ path: screenshotPath, fullPage: true });
18599
18728
  warn4(`Screenshot saved to: ${screenshotPath}`);
18600
18729
  }
@@ -18636,25 +18765,23 @@ function writeConfigFile(config) {
18636
18765
  `OWNER_USER_ID=${config.ownerUserId}`,
18637
18766
  ``
18638
18767
  ].join("\n");
18639
- writeFileSync10(paths.CONFIG_FILE, envContent);
18768
+ writeFileSync11(paths.CONFIG_FILE, envContent);
18640
18769
  success4(`Config written to ${paths.CONFIG_FILE}`);
18641
18770
  }
18642
18771
  function printNextSteps() {
18643
18772
  log4("Setup complete! Next steps:\n");
18644
- console.log(" 1. Customise the system prompt (optional):");
18645
- console.log(` Run \x1B[1mflyspace prompt\x1B[0m to edit ${paths.PROMPT_FILE}
18646
- `);
18773
+ console.log(" 1. Customise the system prompt (optional) in Settings \u2192 Prompt.\n");
18647
18774
  console.log(" 2. Start the bot:");
18648
18775
  console.log(" Run \x1B[1mflyspace start\x1B[0m\n");
18649
18776
  console.log(" 3. Install as a background service (optional):");
18650
- console.log(" Run \x1B[1mflyspace install\x1B[0m\n");
18777
+ console.log(" Run \x1B[1mflyspace service install\x1B[0m\n");
18651
18778
  }
18652
18779
  function copyDefaultPrompt(botName) {
18653
- if (!existsSync27(paths.PROMPT_FILE)) {
18654
- const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
18655
- if (existsSync27(defaultPrompt)) {
18656
- const content = readFileSync17(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18657
- writeFileSync10(paths.PROMPT_FILE, content);
18780
+ if (!existsSync26(paths.PROMPT_FILE)) {
18781
+ const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
18782
+ if (existsSync26(defaultPrompt)) {
18783
+ const content = readFileSync18(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18784
+ writeFileSync11(paths.PROMPT_FILE, content);
18658
18785
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
18659
18786
  }
18660
18787
  }
@@ -18666,8 +18793,8 @@ async function runSlackSetup(args) {
18666
18793
  const prereq = await ensureClaudeCodeReady();
18667
18794
  if (!prereq) return;
18668
18795
  const { claudeConfigDir } = prereq;
18669
- if (existsSync27(paths.CONFIG_FILE)) {
18670
- const reconfigure = await confirm3({
18796
+ if (existsSync26(paths.CONFIG_FILE)) {
18797
+ const reconfigure = await confirm4({
18671
18798
  message: `Config already exists at ${paths.CONFIG_FILE}. Reconfigure?`,
18672
18799
  default: false
18673
18800
  });
@@ -18976,7 +19103,85 @@ async function runChatCmd(args) {
18976
19103
  }
18977
19104
 
18978
19105
  // src/commands/memory.ts
18979
- import { readFileSync as readFileSync18 } from "fs";
19106
+ import { readFileSync as readFileSync20 } from "fs";
19107
+
19108
+ // src/store/claude-memory-import.ts
19109
+ import { existsSync as existsSync27, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "fs";
19110
+ import { homedir as homedir10 } from "os";
19111
+ import { basename as basename8, join as join24 } from "path";
19112
+ import dotenv2 from "dotenv";
19113
+ function claudeMemoryDir(cwd, configDir2) {
19114
+ return join24(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
19115
+ }
19116
+ function profileClaudeConfigDirFor(profile) {
19117
+ const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join24(paths.PROFILES_DIR, profile);
19118
+ const file = join24(dir, "config.env");
19119
+ const env = existsSync27(file) ? dotenv2.parse(readFileSync19(file)) : {};
19120
+ return env["CLAUDE_CONFIG_DIR"]?.trim() || join24(homedir10(), ".claude");
19121
+ }
19122
+ var TYPE_MAP = {
19123
+ user: "preference",
19124
+ feedback: "preference",
19125
+ project: "fact",
19126
+ reference: "reference"
19127
+ };
19128
+ var DESCRIPTION_MAX2 = 200;
19129
+ function unquote2(value) {
19130
+ const v = value.trim();
19131
+ return /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
19132
+ }
19133
+ function slugify(value) {
19134
+ return value.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
19135
+ }
19136
+ function parseClaudeMemory(fileName, text, channelId) {
19137
+ const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text.replaceAll("\r\n", "\n"));
19138
+ if (!match) return { skip: "no frontmatter" };
19139
+ const fields = {};
19140
+ for (const line of match[1].split("\n")) {
19141
+ const kv = /^\s*([A-Za-z_]+):\s*(.*)$/.exec(line);
19142
+ if (kv && kv[2] && !(kv[1] in fields)) fields[kv[1]] = unquote2(kv[2]);
19143
+ }
19144
+ const name = slugify(fields["name"] || basename8(fileName, ".md"));
19145
+ let description = (fields["description"] ?? "").replaceAll(/\s+/g, " ").trim();
19146
+ if (description.length > DESCRIPTION_MAX2) {
19147
+ description = `${description.slice(0, DESCRIPTION_MAX2 - 3).trimEnd()}...`;
19148
+ }
19149
+ const input2 = {
19150
+ channelId,
19151
+ name,
19152
+ description,
19153
+ type: TYPE_MAP[fields["type"] ?? ""] ?? "fact",
19154
+ body: match[2].trim(),
19155
+ createdBy: "agent"
19156
+ };
19157
+ try {
19158
+ validateMemoryInput(input2);
19159
+ } catch (err) {
19160
+ return { skip: err.message };
19161
+ }
19162
+ return input2;
19163
+ }
19164
+ async function importClaudeMemories(dir, channelId, opts = {}) {
19165
+ if (!existsSync27(dir)) throw new Error(`No Claude memory directory at ${dir}`);
19166
+ const files = readdirSync5(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
19167
+ const results = [];
19168
+ for (const file of files) {
19169
+ const parsed = parseClaudeMemory(file, readFileSync19(join24(dir, file), "utf-8"), channelId);
19170
+ if ("skip" in parsed) {
19171
+ results.push({ file, outcome: "skipped", reason: parsed.skip });
19172
+ continue;
19173
+ }
19174
+ if (await getChannelMemory(channelId, parsed.name)) {
19175
+ results.push({ file, name: parsed.name, outcome: "exists" });
19176
+ continue;
19177
+ }
19178
+ if (!opts.dryRun) await saveChannelMemory(parsed, "agent");
19179
+ results.push({ file, name: parsed.name, outcome: "imported" });
19180
+ }
19181
+ return results;
19182
+ }
19183
+
19184
+ // src/commands/memory.ts
18980
19185
  var USAGE5 = `
18981
19186
  Usage: flyspace memory <subcommand> --channel <name-or-id> [options]
18982
19187
 
@@ -18991,6 +19196,10 @@ var USAGE5 = `
18991
19196
  --body <text | -> "-" reads the body from stdin
18992
19197
  [--thread <id>] The thread the memory came from
18993
19198
  delete <name-or-id> [--thread <id>] Delete one memory
19199
+ import [--dir <path>] [--dry-run] Copy Claude Code's native auto memory for the
19200
+ channel's folder in; names already present are kept.
19201
+ Defaults to the channel profile's CLAUDE_CONFIG_DIR
19202
+ (or ~/.claude)/projects/<cwd slug>/memory
18994
19203
 
18995
19204
  Types: ${MEMORY_TYPES.join(", ")}
18996
19205
  `;
@@ -19056,7 +19265,7 @@ ${memory.body}`);
19056
19265
  );
19057
19266
  }
19058
19267
  if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
19059
- const body = bodyArg === "-" ? readFileSync18(0, "utf-8") : bodyArg;
19268
+ const body = bodyArg === "-" ? readFileSync20(0, "utf-8") : bodyArg;
19060
19269
  const thread = getArg4(rest, "--thread");
19061
19270
  const { created, changed } = await saveChannelMemory(
19062
19271
  {
@@ -19082,6 +19291,20 @@ ${memory.body}`);
19082
19291
  console.log(`Deleted memory ${ref}.`);
19083
19292
  break;
19084
19293
  }
19294
+ case "import": {
19295
+ const dir = getArg4(rest, "--dir") ?? claudeMemoryDir(channel.cwd, profileClaudeConfigDirFor(channel.profile));
19296
+ const dryRun = rest.includes("--dry-run");
19297
+ const results = await importClaudeMemories(dir, channel.id, { dryRun });
19298
+ for (const r of results) {
19299
+ const detail = r.outcome === "skipped" ? r.reason : r.name;
19300
+ console.log(`${r.outcome.padEnd(8)} ${r.file} ${detail}`);
19301
+ }
19302
+ const count = (o) => results.filter((r) => r.outcome === o).length;
19303
+ console.log(
19304
+ `${dryRun ? "Would import" : "Imported"} ${count("imported")}, ${count("exists")} already present, ${count("skipped")} skipped, from ${dir}.`
19305
+ );
19306
+ break;
19307
+ }
19085
19308
  default:
19086
19309
  console.error(`Unknown subcommand: ${subcommand}`);
19087
19310
  console.log(USAGE5);
@@ -19126,10 +19349,10 @@ async function runMcpCmd(args) {
19126
19349
  }
19127
19350
 
19128
19351
  // src/commands/dashboard.ts
19129
- import { spawn as spawn6, spawnSync } from "child_process";
19352
+ import { spawn as spawn5, spawnSync } from "child_process";
19130
19353
  import { existsSync as existsSync28 } from "fs";
19131
19354
  import { platform as platform7 } from "os";
19132
- import { join as join24 } from "path";
19355
+ import { join as join25 } from "path";
19133
19356
  function flagValue(args, name) {
19134
19357
  const eq = args.find((a) => a.startsWith(`--${name}=`));
19135
19358
  if (eq) return eq.slice(`--${name}=`.length);
@@ -19139,13 +19362,13 @@ function flagValue(args, name) {
19139
19362
  function openBrowser2(url) {
19140
19363
  const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
19141
19364
  try {
19142
- spawn6(cmd, [url], { stdio: "ignore", detached: true }).unref();
19365
+ spawn5(cmd, [url], { stdio: "ignore", detached: true }).unref();
19143
19366
  } catch {
19144
19367
  }
19145
19368
  }
19146
19369
  function ensureWebBuild() {
19147
- if (existsSync28(join24(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19148
- if (!existsSync28(join24(paths.PACKAGE_ROOT, "web", "package.json"))) return;
19370
+ if (existsSync28(join25(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19371
+ if (!existsSync28(join25(paths.PACKAGE_ROOT, "web", "package.json"))) return;
19149
19372
  console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
19150
19373
  const result = spawnSync("pnpm", ["--dir", "web", "build"], {
19151
19374
  cwd: paths.PACKAGE_ROOT,
@@ -19195,7 +19418,7 @@ async function runDashboard(args) {
19195
19418
  }
19196
19419
 
19197
19420
  // src/commands/workspace.ts
19198
- import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
19421
+ import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
19199
19422
  var USAGE7 = `
19200
19423
  Usage: flyspace workspace <subcommand> [options]
19201
19424
 
@@ -19284,7 +19507,7 @@ function provisionLocal(botName) {
19284
19507
  if (existsSync29(paths.CONFIG_FILE)) {
19285
19508
  console.log(" config.env exists");
19286
19509
  } else {
19287
- writeFileSync11(paths.CONFIG_FILE, `BOT_NAME=${botName}
19510
+ writeFileSync12(paths.CONFIG_FILE, `BOT_NAME=${botName}
19288
19511
  `);
19289
19512
  console.log(` config.env written (${paths.CONFIG_FILE})`);
19290
19513
  }
@@ -19384,18 +19607,16 @@ var USAGE9 = `
19384
19607
  setup Set up this machine as a Host (login, agent CLIs, ~/Flyspace)
19385
19608
  auth Log in, check status, or log out this device
19386
19609
  doctor Check this Host end to end (login, agent CLIs, services)
19387
- start Start the agent host (Slack only if configured)
19388
- install Install as a system service (launchd / systemd)
19389
- uninstall Remove the system service
19610
+ start Start the agent host in the foreground
19611
+ service Run the host in the background (install / uninstall)
19390
19612
  stop Stop the service (without uninstalling)
19391
19613
  restart Restart the service
19392
19614
  logs Tail the service log (-e for error log)
19393
19615
  upgrade Upgrade Flyspace to the latest version
19394
- prompt Edit the system prompt in $EDITOR
19616
+ uninstall Remove Flyspace from this machine (CLI, services, config)
19395
19617
  config Show resolved config (--edit to edit, --path for file path)
19396
19618
  automation Manage scheduled tasks and triggers
19397
19619
  usage Show agent token usage and cost for this workspace
19398
- slack Query Slack (channels, users, post messages)
19399
19620
  chat Inspect dashboard chat channels and move threads between them
19400
19621
  memory Read and write a channel's persistent agent memory
19401
19622
  profiles List local profiles (bound here? scheduler?) or remove one
@@ -19453,11 +19674,20 @@ async function main() {
19453
19674
  case "reconcile":
19454
19675
  await runReconcileCmd(args.slice(1));
19455
19676
  break;
19677
+ case "service":
19678
+ if (args[1] === "install") await runInstall();
19679
+ else if (args[1] === "uninstall") await runServiceUninstall();
19680
+ else {
19681
+ printRaw("Usage: flyspace service <install|uninstall>");
19682
+ process.exit(1);
19683
+ }
19684
+ break;
19685
+ // Pre-rename spelling, kept so existing docs and scripts keep working.
19456
19686
  case "install":
19457
19687
  await runInstall();
19458
19688
  break;
19459
19689
  case "uninstall":
19460
- await runUninstall();
19690
+ await runUninstall(args.slice(1));
19461
19691
  break;
19462
19692
  case "stop":
19463
19693
  await runStop();
@@ -19471,9 +19701,6 @@ async function main() {
19471
19701
  case "upgrade":
19472
19702
  await runUpgrade();
19473
19703
  break;
19474
- case "prompt":
19475
- await runPrompt();
19476
- break;
19477
19704
  case "config":
19478
19705
  await runConfig(args.slice(1));
19479
19706
  break;
@@ -19504,7 +19731,7 @@ async function main() {
19504
19731
  break;
19505
19732
  case "-v":
19506
19733
  case "--version":
19507
- printRaw("0.1.10");
19734
+ printRaw("0.1.13");
19508
19735
  break;
19509
19736
  case "-h":
19510
19737
  case "--help":