flyspace-cli 0.1.6 → 0.1.9

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
@@ -52,6 +52,7 @@ import {
52
52
  expandPath,
53
53
  extractCategories,
54
54
  findChannel,
55
+ findChannelByAgentName,
55
56
  findChannelById,
56
57
  findSkillDetail,
57
58
  getAgentPaused,
@@ -106,6 +107,7 @@ import {
106
107
  mcpStrictConfigEnabled,
107
108
  mimetypeSizeLimit,
108
109
  modelLabel,
110
+ pinnedClaudeConfigDir,
109
111
  readSkillFile,
110
112
  recentRunsForWorkspace,
111
113
  refreshAutomationRegistry,
@@ -150,7 +152,7 @@ import {
150
152
  warnOnBrokenChannelCwds,
151
153
  workspaceSkillsDir,
152
154
  writeSkillFile
153
- } from "./chunk-BZYTXXF7.js";
155
+ } from "./chunk-JG6LNX5D.js";
154
156
  import {
155
157
  firstReplyBody,
156
158
  getThread,
@@ -430,13 +432,14 @@ async function runConfig(args) {
430
432
  }
431
433
 
432
434
  // src/commands/setup.ts
435
+ import { execSync as execSync4 } from "child_process";
433
436
  import { mkdirSync as mkdirSync5 } from "fs";
434
437
  import { join as join7 } from "path";
435
438
  import { confirm as confirm2 } from "@inquirer/prompts";
436
439
 
437
440
  // src/service.ts
438
441
  import { execSync } from "child_process";
439
- import { existsSync as existsSync3 } from "fs";
442
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
440
443
  import { join as join2 } from "path";
441
444
  import { homedir, platform } from "os";
442
445
  function serviceLabel(profile) {
@@ -465,6 +468,22 @@ function isServiceInstalled(profile) {
465
468
  if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
466
469
  return false;
467
470
  }
471
+ function serviceBinaryMissing(file) {
472
+ let text;
473
+ try {
474
+ text = readFileSync3(file, "utf-8");
475
+ } catch {
476
+ return false;
477
+ }
478
+ const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
479
+ return !!bin && !existsSync3(bin);
480
+ }
481
+ function profileServiceFile(profile) {
482
+ return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
483
+ }
484
+ function hostServiceFile() {
485
+ return platform() === "darwin" ? hostPlistPath() : join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
486
+ }
468
487
  function restartService(profile) {
469
488
  const os = platform();
470
489
  if (os === "darwin") {
@@ -496,10 +515,7 @@ function hostPlistPath() {
496
515
  return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
497
516
  }
498
517
  function isHostServiceInstalled() {
499
- if (platform() === "darwin") return existsSync3(hostPlistPath());
500
- if (platform() === "linux") {
501
- return existsSync3(join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`));
502
- }
518
+ if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
503
519
  return false;
504
520
  }
505
521
  function restartHostService() {
@@ -517,9 +533,29 @@ function restartHostService() {
517
533
  }
518
534
  return false;
519
535
  }
536
+ function serviceActive(label2, unit) {
537
+ try {
538
+ if (platform() === "darwin") {
539
+ const out = execSync(`launchctl print gui/${process.getuid?.()}/${label2}`, { encoding: "utf-8", stdio: "pipe" });
540
+ return /^\s*state = running$/m.test(out);
541
+ }
542
+ if (platform() === "linux") {
543
+ execSync(`systemctl --user is-active --quiet ${unit}`, { stdio: "pipe" });
544
+ return true;
545
+ }
546
+ } catch {
547
+ }
548
+ return false;
549
+ }
550
+ function isServiceRunning(profile) {
551
+ return serviceActive(serviceLabel(profile), systemdUnitName(profile));
552
+ }
553
+ function isHostServiceRunning() {
554
+ return serviceActive(HOST_SERVICE_LABEL, HOST_SYSTEMD_UNIT);
555
+ }
520
556
 
521
557
  // src/commands/install.ts
522
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync } from "fs";
558
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
523
559
  import { execSync as execSync2 } from "child_process";
524
560
  import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
525
561
  import { homedir as homedir2, platform as platform2 } from "os";
@@ -547,6 +583,11 @@ function detectClaudeBinDir() {
547
583
  return void 0;
548
584
  }
549
585
  }
586
+ function nodeShimDirs() {
587
+ return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
588
+ (d) => existsSync4(d)
589
+ );
590
+ }
550
591
  function detectCustieBin() {
551
592
  const argv1 = process.argv[1];
552
593
  if (argv1) {
@@ -561,7 +602,7 @@ function detectCustieBin() {
561
602
  function readEnvFile() {
562
603
  if (!existsSync4(paths.CONFIG_FILE)) return {};
563
604
  const vars = {};
564
- for (const line of readFileSync3(paths.CONFIG_FILE, "utf-8").split("\n")) {
605
+ for (const line of readFileSync4(paths.CONFIG_FILE, "utf-8").split("\n")) {
565
606
  const trimmed = line.trim();
566
607
  if (!trimmed || trimmed.startsWith("#")) continue;
567
608
  const eq = trimmed.indexOf("=");
@@ -609,12 +650,10 @@ async function installMacOS(spec) {
609
650
  mkdirSync2(logDir, { recursive: true });
610
651
  const envVars = spec.env;
611
652
  const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
612
- const asdfShims = join3(homedir2(), ".asdf", "shims");
613
- const asdfBin = existsSync4(asdfShims) ? asdfShims : void 0;
614
653
  const pathParts = [
615
654
  dirname2(nodePath),
616
655
  claudeBinDir,
617
- asdfBin,
656
+ ...nodeShimDirs(),
618
657
  homebrewBin,
619
658
  "/usr/local/bin",
620
659
  "/usr/bin",
@@ -683,6 +722,7 @@ async function installLinux(spec) {
683
722
  const pathParts = [
684
723
  dirname2(nodePath),
685
724
  claudeBinDir,
725
+ ...nodeShimDirs(),
686
726
  linuxbrewBin,
687
727
  "/usr/local/bin",
688
728
  "/usr/bin",
@@ -756,7 +796,7 @@ function listProfiles() {
756
796
  }
757
797
  return profiles;
758
798
  }
759
- function isServiceRunning(profile) {
799
+ function isServiceRunning2(profile) {
760
800
  try {
761
801
  if (platform3() === "darwin") {
762
802
  const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
@@ -770,19 +810,10 @@ function isServiceRunning(profile) {
770
810
  }
771
811
  return false;
772
812
  }
773
- async function runProfiles() {
774
- const profiles = listProfiles();
775
- console.log("\n Profiles:\n");
776
- for (const profile of profiles) {
777
- const status = isServiceRunning(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
778
- console.log(` ${profile.padEnd(20)} ${status}`);
779
- }
780
- console.log("\n Target a profile with --profile <name> on any command.\n");
781
- }
782
813
 
783
814
  // src/auth/device-auth.ts
784
815
  import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
785
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
816
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
786
817
  import { dirname as dirname3, join as join5 } from "path";
787
818
  var DEFAULT_API_URL = "https://app.flyspace.ai";
788
819
  var EXPIRY_SKEW_MS = 6e4;
@@ -810,7 +841,7 @@ function ensureDeviceKey(path = deviceKeyPath()) {
810
841
  writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
811
842
  created = true;
812
843
  }
813
- const privateKey = createPrivateKey(readFileSync4(path, "utf8"));
844
+ const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
814
845
  const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
815
846
  "base64"
816
847
  );
@@ -819,7 +850,7 @@ function ensureDeviceKey(path = deviceKeyPath()) {
819
850
  function loadCredentials(path = credentialsPath()) {
820
851
  if (!existsSync6(path)) return null;
821
852
  try {
822
- return JSON.parse(readFileSync4(path, "utf8"));
853
+ return JSON.parse(readFileSync5(path, "utf8"));
823
854
  } catch {
824
855
  return null;
825
856
  }
@@ -932,6 +963,17 @@ async function fetchWhoAmI(base, token) {
932
963
  workspaces: toSlugs(json["workspaces"])
933
964
  };
934
965
  }
966
+ async function boundWorkspaces() {
967
+ const creds = loadCredentials();
968
+ if (!creds) return null;
969
+ try {
970
+ const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
971
+ saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
972
+ return { workspaces: me.workspaces, fresh: true };
973
+ } catch {
974
+ return { workspaces: creds.workspaces ?? [], fresh: false };
975
+ }
976
+ }
935
977
  async function revokeDevice(base, token) {
936
978
  await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
937
979
  }
@@ -954,7 +996,7 @@ import {
954
996
  lstatSync,
955
997
  mkdirSync as mkdirSync4,
956
998
  readdirSync as readdirSync2,
957
- readFileSync as readFileSync5,
999
+ readFileSync as readFileSync6,
958
1000
  statSync as statSync2,
959
1001
  symlinkSync
960
1002
  } from "fs";
@@ -983,7 +1025,7 @@ function claudeLoggedIn() {
983
1025
  if (process.env["ANTHROPIC_API_KEY"]) return true;
984
1026
  if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
985
1027
  try {
986
- const raw = JSON.parse(readFileSync5(join6(homedir3(), ".claude.json"), "utf-8"));
1028
+ const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
987
1029
  if (raw.oauthAccount) return true;
988
1030
  } catch {
989
1031
  }
@@ -1024,7 +1066,7 @@ function jsonlFiles(dir, depth) {
1024
1066
  function cwdFromTranscript(file) {
1025
1067
  let head;
1026
1068
  try {
1027
- head = readFileSync5(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1069
+ head = readFileSync6(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1028
1070
  } catch {
1029
1071
  return null;
1030
1072
  }
@@ -1041,7 +1083,7 @@ function cwdFromTranscript(file) {
1041
1083
  }
1042
1084
  function codexTrustedPaths() {
1043
1085
  try {
1044
- const toml = readFileSync5(join6(codexDir(), "config.toml"), "utf-8");
1086
+ const toml = readFileSync6(join6(codexDir(), "config.toml"), "utf-8");
1045
1087
  return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
1046
1088
  } catch {
1047
1089
  return [];
@@ -1510,6 +1552,16 @@ async function runAuthCmd(args) {
1510
1552
  }
1511
1553
  }
1512
1554
 
1555
+ // src/version.ts
1556
+ var CLI_VERSION = true ? "0.1.9" : null;
1557
+ function isOlderVersion(a, b) {
1558
+ const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1559
+ const [pa, pb] = [parse2(a), parse2(b)];
1560
+ if (pa.length !== 3 || pb.length !== 3 || [...pa, ...pb].some(Number.isNaN)) return false;
1561
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i];
1562
+ return false;
1563
+ }
1564
+
1513
1565
  // src/commands/setup.ts
1514
1566
  var BOLD2 = "\x1B[1m";
1515
1567
  var DIM2 = "\x1B[2m";
@@ -1541,15 +1593,21 @@ var tilde2 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
1541
1593
  var ok = (msg) => console.log(` ${GREEN2}\u2713${RESET2} ${msg}`);
1542
1594
  var bad = (msg) => console.log(` ${YELLOW2}\u2717${RESET2} ${msg}`);
1543
1595
  async function servedWorkspaces() {
1544
- const creds = loadCredentials();
1596
+ const bound = await boundWorkspaces();
1597
+ if (!bound.fresh) console.log(` ${DIM2}Couldn't reach the server; using the workspaces saved at login.${RESET2}`);
1598
+ return bound.workspaces;
1599
+ }
1600
+ function checkLatestVersion() {
1601
+ if (!CLI_VERSION) return true;
1602
+ let latest = "";
1545
1603
  try {
1546
- const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
1547
- saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
1548
- return me.workspaces;
1604
+ latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
1549
1605
  } catch {
1550
- console.log(` ${DIM2}Couldn't reach the server; using the workspaces saved at login.${RESET2}`);
1551
- return creds.workspaces ?? [];
1606
+ return true;
1552
1607
  }
1608
+ if (!latest || !isOlderVersion(CLI_VERSION, latest)) return true;
1609
+ bad(`flyspace ${CLI_VERSION} is out of date (latest ${latest}) ${DIM2}run: flyspace upgrade${RESET2}`);
1610
+ return false;
1553
1611
  }
1554
1612
  function loginArgs(args) {
1555
1613
  const out = [];
@@ -1574,6 +1632,7 @@ async function runSetup(args) {
1574
1632
  console.log(`
1575
1633
  ${BOLD2}Flyspace Setup${RESET2}
1576
1634
  `);
1635
+ checkLatestVersion();
1577
1636
  const existing = loadCredentials();
1578
1637
  let workspaces;
1579
1638
  if (existing) {
@@ -1583,7 +1642,10 @@ ${BOLD2}Flyspace Setup${RESET2}
1583
1642
  console.log(` Device ${existing.deviceName}`);
1584
1643
  console.log(` Workspaces ${workspaces.length ? workspaces.join(", ") : "(none)"}`);
1585
1644
  const others = listProfiles().filter((p) => p !== "default" && !workspaces.includes(p));
1586
- if (others.length) console.log(` ${DIM2}Other local profiles (left alone): ${others.join(", ")}${RESET2}`);
1645
+ if (others.length) {
1646
+ console.log(` ${DIM2}Not bound here, nothing runs for them: ${others.join(", ")}${RESET2}`);
1647
+ console.log(` ${DIM2}See: flyspace profiles Clean up: flyspace profiles remove <name>${RESET2}`);
1648
+ }
1587
1649
  console.log("");
1588
1650
  const keep = args.includes("--yes") || !process.stdin.isTTY ? true : await confirm2({ message: "Keep using this account and device?", default: true });
1589
1651
  if (!keep) {
@@ -1611,7 +1673,12 @@ ${BOLD2}Flyspace Setup${RESET2}
1611
1673
  }
1612
1674
  let hostRunning = false;
1613
1675
  if (args.includes("--start-host") && workspaces.length) {
1614
- if (!isHostServiceInstalled()) {
1676
+ for (const slug of workspaces) {
1677
+ if (!serviceBinaryMissing(profileServiceFile(slug))) continue;
1678
+ await runWithProfile(slug, runInstall);
1679
+ ok(`${slug} scheduler reinstalled on the new CLI path`);
1680
+ }
1681
+ if (!isHostServiceInstalled() || serviceBinaryMissing(hostServiceFile())) {
1615
1682
  hostRunning = await installHostService();
1616
1683
  if (hostRunning) ok(`Host running in the background ${DIM2}logs: ${tilde2(paths.LOG_DIR)}/host.log${RESET2}`);
1617
1684
  } else {
@@ -1635,9 +1702,137 @@ ${BOLD2}Flyspace Setup${RESET2}
1635
1702
  if (!workspaces.length) process.exitCode = 1;
1636
1703
  }
1637
1704
 
1705
+ // src/commands/doctor.ts
1706
+ import { execFile } from "child_process";
1707
+ import { tmpdir } from "os";
1708
+ import { promisify } from "util";
1709
+ var execAsync = promisify(execFile);
1710
+ var exec = (cmd, args, opts) => {
1711
+ const run3 = execAsync(cmd, args, opts);
1712
+ run3.child.stdin?.end();
1713
+ return run3;
1714
+ };
1715
+ var BOLD3 = "\x1B[1m";
1716
+ var DIM3 = "\x1B[2m";
1717
+ var GREEN3 = "\x1B[32m";
1718
+ var YELLOW3 = "\x1B[33m";
1719
+ var RESET3 = "\x1B[0m";
1720
+ var HELP3 = `
1721
+ Usage: flyspace doctor
1722
+
1723
+ Checks this Host end to end: CLI version, server reachability, device login,
1724
+ a live turn with each installed agent CLI, and the background services.
1725
+ Exits non-zero when any check fails.
1726
+ `;
1727
+ var LOGIN_HINTS = {
1728
+ claude: "claude auth login",
1729
+ codex: "codex login"
1730
+ };
1731
+ var tilde3 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
1732
+ var failed = false;
1733
+ var ok2 = (msg) => console.log(` ${GREEN3}\u2713${RESET3} ${msg}`);
1734
+ var bad2 = (msg, hint) => {
1735
+ failed = true;
1736
+ console.log(` ${YELLOW3}\u2717${RESET3} ${msg}${hint ? ` ${DIM3}${hint}${RESET3}` : ""}`);
1737
+ };
1738
+ function errorLine(err) {
1739
+ const e = err;
1740
+ if (e.killed) return "timed out after 90s";
1741
+ const lines = `${e.stdout ?? ""}
1742
+ ${e.stderr ?? ""}`.split("\n").map((l) => l.trim()).filter(Boolean);
1743
+ const last = (re) => [...lines].reverse().find((l) => re.test(l));
1744
+ const line = last(/expired|unauthori[sz]ed|\b401\b|not logged in|authenticate/i) ?? last(/error|fail/i) ?? lines.at(-1) ?? e.message ?? "failed";
1745
+ return line.length > 160 ? `${line.slice(0, 157)}...` : line;
1746
+ }
1747
+ async function probe(id) {
1748
+ const env = { ...process.env };
1749
+ const opts = { cwd: tmpdir(), timeout: 9e4, encoding: "utf-8" };
1750
+ try {
1751
+ if (id === "claude") {
1752
+ const dir = pinnedClaudeConfigDir(process.env["CLAUDE_CONFIG_DIR"]);
1753
+ if (dir) env["CLAUDE_CONFIG_DIR"] = dir;
1754
+ else delete env["CLAUDE_CONFIG_DIR"];
1755
+ await exec("claude", ["-p", "Reply with OK", "--model", "haiku", "--max-turns", "1"], { ...opts, env });
1756
+ } else {
1757
+ await exec("codex", ["exec", "--skip-git-repo-check", "-s", "read-only", "Reply with OK"], { ...opts, env });
1758
+ }
1759
+ return null;
1760
+ } catch (err) {
1761
+ return errorLine(err);
1762
+ }
1763
+ }
1764
+ async function serverReachable() {
1765
+ try {
1766
+ const res = await fetch(`${apiUrl()}/api/health`, { signal: AbortSignal.timeout(1e4) });
1767
+ return res.ok ? null : `HTTP ${res.status}`;
1768
+ } catch (err) {
1769
+ return err.cause ? String(err.cause) : err.message;
1770
+ }
1771
+ }
1772
+ function checkService(name, installed, file, running, fix) {
1773
+ if (!installed) bad2(`${name} not installed`, `run: ${fix}`);
1774
+ else if (serviceBinaryMissing(file)) bad2(`${name} points at a CLI that no longer exists`, `run: ${fix}`);
1775
+ else if (!running) bad2(`${name} not running`, `logs: ${tilde3(paths.LOG_DIR)}`);
1776
+ else ok2(`${name} running`);
1777
+ }
1778
+ async function runDoctor(args) {
1779
+ if (args.includes("-h") || args.includes("--help")) return void console.log(HELP3);
1780
+ failed = false;
1781
+ console.log(`
1782
+ ${BOLD3}Flyspace Doctor${RESET3}
1783
+ `);
1784
+ if (checkLatestVersion()) ok2(`flyspace ${CLI_VERSION || "(dev build)"}`);
1785
+ else failed = true;
1786
+ const serverError = await serverReachable();
1787
+ if (serverError) bad2(`Can't reach ${apiUrl()}: ${serverError}`, "check the network or DNS");
1788
+ else ok2(`Server ${apiUrl()} reachable`);
1789
+ const creds = loadCredentials();
1790
+ let workspaces = [];
1791
+ if (!creds) {
1792
+ bad2("This device is not logged in", "run: flyspace setup");
1793
+ } else {
1794
+ const bound = await boundWorkspaces();
1795
+ workspaces = bound.workspaces;
1796
+ ok2(`Logged in as ${creds.userEmail} (device ${creds.deviceName})`);
1797
+ if (!bound.fresh) bad2("Couldn't refresh the device session; showing the workspaces saved at login");
1798
+ if (workspaces.length) ok2(`Serving ${workspaces.join(", ")}`);
1799
+ else bad2("Not serving any workspace", "a workspace admin has to add this Host");
1800
+ }
1801
+ const runners = detectRunners().filter((r) => r.installed);
1802
+ if (!runners.length) bad2("Neither claude nor codex is installed", "npm install -g @anthropic-ai/claude-code");
1803
+ const results = await Promise.all(runners.map((r) => probe(r.id)));
1804
+ runners.forEach((r, i) => {
1805
+ const error2 = results[i];
1806
+ if (error2) bad2(`${r.id} can't run a turn: ${error2}`, `run: ${LOGIN_HINTS[r.id]}`);
1807
+ else ok2(`${r.id} answered a test turn`);
1808
+ });
1809
+ if (workspaces.length) {
1810
+ checkService(
1811
+ "Host service",
1812
+ isHostServiceInstalled(),
1813
+ hostServiceFile(),
1814
+ isHostServiceRunning(),
1815
+ "flyspace setup --start-host"
1816
+ );
1817
+ for (const slug of workspaces) {
1818
+ checkService(
1819
+ `${slug} scheduler`,
1820
+ isServiceInstalled(slug),
1821
+ profileServiceFile(slug),
1822
+ isServiceRunning(slug),
1823
+ "flyspace setup --start-host"
1824
+ );
1825
+ }
1826
+ }
1827
+ console.log(failed ? "" : `
1828
+ ${GREEN3}All good.${RESET3}
1829
+ `);
1830
+ if (failed) process.exitCode = 1;
1831
+ }
1832
+
1638
1833
  // src/commands/uninstall.ts
1639
1834
  import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
1640
- import { execSync as execSync4 } from "child_process";
1835
+ import { execSync as execSync5 } from "child_process";
1641
1836
  import { basename as basename2, join as join8 } from "path";
1642
1837
  import { homedir as homedir4, platform as platform5 } from "os";
1643
1838
  function log2(msg) {
@@ -1651,7 +1846,7 @@ function warn2(msg) {
1651
1846
  console.log(`\x1B[33m!\x1B[0m ${msg}`);
1652
1847
  }
1653
1848
  function run2(cmd) {
1654
- execSync4(cmd, { encoding: "utf-8", stdio: "pipe" });
1849
+ execSync5(cmd, { encoding: "utf-8", stdio: "pipe" });
1655
1850
  }
1656
1851
  async function runUninstall() {
1657
1852
  const os = platform5();
@@ -1698,8 +1893,82 @@ async function runUninstall() {
1698
1893
  console.log("\n Config and data directories were NOT removed.");
1699
1894
  }
1700
1895
 
1896
+ // src/commands/profiles-cmd.ts
1897
+ import { rmSync as rmSync3 } from "fs";
1898
+ var USAGE = `
1899
+ Usage: flyspace profiles [list]
1900
+ flyspace profiles remove <name>
1901
+
1902
+ list Every local profile, whether this Host is bound to it, and the
1903
+ state of its scheduler service.
1904
+ remove Unload a profile's scheduler and delete its local config. Only for
1905
+ profiles this Host is not bound to; the Workspace itself, its
1906
+ channel folders and its data directory are left alone.
1907
+ `;
1908
+ async function boundHere() {
1909
+ const bound = await boundWorkspaces();
1910
+ if (bound && !bound.fresh) console.log(" \x1B[2mCouldn't reach the server; using the workspaces saved at login.\x1B[0m");
1911
+ return bound?.workspaces ?? null;
1912
+ }
1913
+ async function listCmd() {
1914
+ const bound = await boundHere();
1915
+ console.log("\n Profiles:\n");
1916
+ console.log(` ${"NAME".padEnd(24)} ${"BOUND HERE".padEnd(12)} SCHEDULER`);
1917
+ for (const profile of listProfiles()) {
1918
+ const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" : isServiceRunning2(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
1919
+ const here = bound === null ? "-".padEnd(12) : bound.includes(profile) ? "yes".padEnd(12) : `\x1B[33m${"no".padEnd(12)}\x1B[0m`;
1920
+ console.log(` ${profile.padEnd(24)} ${here} ${scheduler}`);
1921
+ }
1922
+ const missing = (bound ?? []).filter((w) => !listProfiles().includes(w));
1923
+ for (const w of missing) console.log(` ${w.padEnd(24)} ${"yes".padEnd(12)} \x1B[33mno local profile yet\x1B[0m`);
1924
+ if (bound?.length === 0) {
1925
+ console.log("\n This Host is not bound to any workspace, so no scheduler runs here. Run: flyspace setup");
1926
+ }
1927
+ if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
1928
+ console.log("\n Profiles not bound here run nothing on this machine. Remove one with: flyspace profiles remove <name>");
1929
+ }
1930
+ console.log("");
1931
+ }
1932
+ async function removeCmd(name) {
1933
+ if (!name) return void console.log(USAGE);
1934
+ if (name === DEFAULT_PROFILE) {
1935
+ console.error(" The default profile cannot be removed.");
1936
+ process.exit(1);
1937
+ }
1938
+ if (!listProfiles().includes(name)) {
1939
+ console.error(` No local profile named "${name}".`);
1940
+ process.exit(1);
1941
+ }
1942
+ const bound = await boundHere();
1943
+ if (bound === null) {
1944
+ console.error(" This machine has no device login, so it serves every workspace; removing a profile would be undone.");
1945
+ process.exit(1);
1946
+ }
1947
+ if (bound.includes(name)) {
1948
+ console.error(` This Host is still bound to "${name}" on the server, so the Host would reinstall it within a minute.`);
1949
+ console.error(" Unbinding a Host from a workspace is not available yet.");
1950
+ process.exit(1);
1951
+ }
1952
+ const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
1953
+ if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
1954
+ rmSync3(configDir2, { recursive: true, force: true });
1955
+ console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
1956
+ console.log(`
1957
+ Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
1958
+ console.log(` The workspace and its server-side records are unchanged; this only cleaned up this machine.
1959
+ `);
1960
+ }
1961
+ async function runProfiles(args = []) {
1962
+ loadEnvFiles();
1963
+ const [sub, name] = args;
1964
+ if (sub === "remove" || sub === "rm") return removeCmd(name);
1965
+ if (!sub || sub === "list" || sub === "ls") return listCmd();
1966
+ console.log(USAGE);
1967
+ }
1968
+
1701
1969
  // src/commands/upgrade.ts
1702
- import { execSync as execSync5 } from "child_process";
1970
+ import { execSync as execSync6 } from "child_process";
1971
+ import { realpathSync } from "fs";
1703
1972
  function restartInstalledProfiles() {
1704
1973
  if (isHostServiceInstalled()) {
1705
1974
  console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
@@ -1719,19 +1988,29 @@ Restarting ${installed.length} instance(s)...`);
1719
1988
  }
1720
1989
  }
1721
1990
  }
1991
+ function npmInstallCommand() {
1992
+ let prefix;
1993
+ try {
1994
+ const entry = realpathSync(process.argv[1] ?? "");
1995
+ const at = entry.indexOf(`/lib/node_modules/${"flyspace-cli"}/`);
1996
+ if (at > 0) prefix = entry.slice(0, at);
1997
+ } catch {
1998
+ }
1999
+ return `npm install -g${prefix ? ` --prefix "${prefix}"` : ""} ${"flyspace-cli"}@latest`;
2000
+ }
1722
2001
  async function runUpgrade() {
1723
- const current = "0.1.6";
2002
+ const current = "0.1.9";
1724
2003
  console.log(`Current version: ${current}`);
1725
2004
  console.log("Checking for updates...\n");
1726
2005
  try {
1727
- const latest = execSync5(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8" }).trim();
2006
+ const latest = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8" }).trim();
1728
2007
  if (latest === current) {
1729
2008
  console.log(`Already on the latest version (${current}).`);
1730
2009
  return;
1731
2010
  }
1732
2011
  console.log(`New version available: ${latest}`);
1733
2012
  console.log("Upgrading...\n");
1734
- execSync5(`npm install -g ${"flyspace-cli"}@latest`, { stdio: "inherit" });
2013
+ execSync6(npmInstallCommand(), { stdio: "inherit" });
1735
2014
  console.log(`
1736
2015
  Upgraded from ${current} to ${latest}.`);
1737
2016
  restartInstalledProfiles();
@@ -1740,14 +2019,14 @@ Upgraded from ${current} to ${latest}.`);
1740
2019
  } catch (err) {
1741
2020
  console.error("Upgrade failed:", err.message);
1742
2021
  console.error(`
1743
- You can upgrade manually with: npm install -g ${"flyspace-cli"}@latest`);
2022
+ You can upgrade manually with: ${npmInstallCommand()}`);
1744
2023
  process.exit(1);
1745
2024
  }
1746
2025
  }
1747
2026
 
1748
2027
  // src/commands/automation.ts
1749
2028
  import { WebClient } from "@slack/web-api";
1750
- var USAGE = `
2029
+ var USAGE2 = `
1751
2030
  Usage: flyspace automation <subcommand> [options]
1752
2031
 
1753
2032
  Subcommands:
@@ -2085,14 +2364,14 @@ async function runAutomationCmd(args) {
2085
2364
  await handleRun(manager, args[1]);
2086
2365
  break;
2087
2366
  default:
2088
- console.log(USAGE);
2367
+ console.log(USAGE2);
2089
2368
  break;
2090
2369
  }
2091
2370
  }
2092
2371
 
2093
2372
  // src/commands/usage.ts
2094
2373
  import { existsSync as existsSync9 } from "fs";
2095
- var HELP3 = `
2374
+ var HELP4 = `
2096
2375
  Usage: flyspace usage [options]
2097
2376
 
2098
2377
  Show agent token usage and notional cost for this workspace.
@@ -2214,7 +2493,7 @@ function printExplanation(explained) {
2214
2493
  async function runUsage(args) {
2215
2494
  const parsed = parseArgs(args);
2216
2495
  if (parsed === "help") {
2217
- console.log(HELP3);
2496
+ console.log(HELP4);
2218
2497
  return;
2219
2498
  }
2220
2499
  const { days, by, explain, json } = parsed;
@@ -2276,7 +2555,7 @@ function breakdown(store, by, days) {
2276
2555
  }
2277
2556
 
2278
2557
  // src/commands/stop.ts
2279
- import { execSync as execSync6 } from "child_process";
2558
+ import { execSync as execSync7 } from "child_process";
2280
2559
  import { platform as platform6 } from "os";
2281
2560
  function log3(msg) {
2282
2561
  console.log(`
@@ -2295,7 +2574,7 @@ async function runStop() {
2295
2574
  const plist = plistPath(profile);
2296
2575
  log3("Stopping Flyspace service...");
2297
2576
  try {
2298
- execSync6(`launchctl unload "${plist}"`, { stdio: "pipe" });
2577
+ execSync7(`launchctl unload "${plist}"`, { stdio: "pipe" });
2299
2578
  success3("Service stopped. Run `flyspace restart` to start again.");
2300
2579
  } catch {
2301
2580
  warn3("Service is not currently loaded.");
@@ -2303,7 +2582,7 @@ async function runStop() {
2303
2582
  } else if (os === "linux") {
2304
2583
  log3("Stopping Flyspace service...");
2305
2584
  try {
2306
- execSync6(`systemctl --user stop ${systemdUnitName(profile)}`, { stdio: "pipe" });
2585
+ execSync7(`systemctl --user stop ${systemdUnitName(profile)}`, { stdio: "pipe" });
2307
2586
  success3("Service stopped. Run `flyspace restart` to start again.");
2308
2587
  } catch {
2309
2588
  warn3("Service is not currently running.");
@@ -2360,9 +2639,9 @@ async function runLogs(args) {
2360
2639
 
2361
2640
  // src/commands/slack.ts
2362
2641
  import { execFileSync as execFileSync4 } from "child_process";
2363
- import { existsSync as existsSync27, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
2642
+ import { existsSync as existsSync27, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
2364
2643
  import { resolve as resolve7 } from "path";
2365
- import { tmpdir as tmpdir3, homedir as homedir9 } from "os";
2644
+ import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
2366
2645
  import { input, confirm as confirm3, select, checkbox } from "@inquirer/prompts";
2367
2646
  import { WebClient as WebClient3 } from "@slack/web-api";
2368
2647
 
@@ -3891,22 +4170,18 @@ async function deliverPasswordResetLink(email, redirectTo) {
3891
4170
  import { existsSync as existsSync26 } from "fs";
3892
4171
  import { createServer } from "net";
3893
4172
  import { join as join22 } from "path";
3894
- import { Hono as Hono32 } from "hono";
4173
+ import { Hono as Hono33 } from "hono";
3895
4174
  import { serve } from "@hono/node-server";
3896
4175
  import { serveStatic } from "@hono/node-server/serve-static";
3897
4176
 
3898
4177
  // src/reconcile.ts
3899
4178
  import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
3900
- async function boundWorkspaces() {
3901
- const creds = loadCredentials();
3902
- if (!creds) return null;
3903
- try {
3904
- const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
3905
- saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
3906
- return me.workspaces;
3907
- } catch {
3908
- return creds.workspaces ?? [];
3909
- }
4179
+ async function boundWorkspaces2() {
4180
+ return (await boundWorkspaces())?.workspaces ?? null;
4181
+ }
4182
+ async function servedWorkspaces2() {
4183
+ const bound = await boundWorkspaces2();
4184
+ return new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
3910
4185
  }
3911
4186
  async function reconcileProfile(profile) {
3912
4187
  return runWithProfile(profile, async () => {
@@ -3933,8 +4208,7 @@ async function reconcileProfile(profile) {
3933
4208
  });
3934
4209
  }
3935
4210
  async function reconcileAll() {
3936
- const bound = hasSupabaseCredentials() ? null : await boundWorkspaces();
3937
- const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4211
+ const slugs = await servedWorkspaces2();
3938
4212
  const results = /* @__PURE__ */ new Map();
3939
4213
  for (const slug of slugs) {
3940
4214
  try {
@@ -3947,8 +4221,7 @@ async function reconcileAll() {
3947
4221
  return results;
3948
4222
  }
3949
4223
  async function ensureServices() {
3950
- const bound = await boundWorkspaces();
3951
- const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4224
+ const slugs = await servedWorkspaces2();
3952
4225
  slugs.delete(DEFAULT_PROFILE);
3953
4226
  const installed = [];
3954
4227
  for (const slug of slugs) {
@@ -4097,10 +4370,10 @@ var requireDocsAccess = async (c, next) => {
4097
4370
  };
4098
4371
 
4099
4372
  // src/dashboard/api.ts
4100
- import { Hono as Hono29 } from "hono";
4373
+ import { Hono as Hono30 } from "hono";
4101
4374
 
4102
4375
  // src/dashboard/public-share-routes.ts
4103
- import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
4376
+ import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
4104
4377
  import { join as join12 } from "path";
4105
4378
  import { Hono as Hono3 } from "hono";
4106
4379
  import { GetObjectCommand } from "@aws-sdk/client-s3";
@@ -4241,15 +4514,15 @@ async function createDocShare(input2) {
4241
4514
  if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
4242
4515
  return rowToShare(data);
4243
4516
  }
4244
- async function updateDocShareSnapshot(id, snapshot2, features) {
4517
+ async function updateDocShareSnapshot(id, snapshot3, features) {
4245
4518
  const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
4246
- title: snapshot2.title,
4247
- content: snapshot2.content,
4248
- asset_keys: snapshot2.assetKeys,
4519
+ title: snapshot3.title,
4520
+ content: snapshot3.content,
4521
+ asset_keys: snapshot3.assetKeys,
4249
4522
  // Re-detected with the snapshot: a re-publish can carry a rewritten
4250
4523
  // article, and one written in another language than last time is exactly
4251
4524
  // the case a stale value would get wrong.
4252
- source_lang: snapshot2.sourceLang,
4525
+ source_lang: snapshot3.sourceLang,
4253
4526
  audio_enabled: features.audioEnabled,
4254
4527
  ask_enabled: features.askEnabled,
4255
4528
  social_enabled: features.socialEnabled,
@@ -4566,7 +4839,7 @@ function injectShareHead(html, head) {
4566
4839
 
4567
4840
  // src/dashboard/app-icon.ts
4568
4841
  import { createHash as createHash2 } from "crypto";
4569
- import { readFileSync as readFileSync6 } from "fs";
4842
+ import { readFileSync as readFileSync7 } from "fs";
4570
4843
  import { join as join11 } from "path";
4571
4844
  import sharp from "sharp";
4572
4845
  var PLATE = "#0A0A0F";
@@ -4594,7 +4867,7 @@ function roundedPlate(size, fill = PLATE) {
4594
4867
  }
4595
4868
  async function renderBadge(box) {
4596
4869
  const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
4597
- const raster = await sharp(readFileSync6(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
4870
+ const raster = await sharp(readFileSync7(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
4598
4871
  return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
4599
4872
  }
4600
4873
  async function sampleBackground(logo) {
@@ -4649,7 +4922,7 @@ async function renderAppIcon({
4649
4922
  if (hit) return hit;
4650
4923
  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,
4651
4924
  // either on its usual dark plate or, for a favicon, trimmed and bare.
4652
- (transparent ? await renderBadge(size) : await sharp(readFileSync6(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
4925
+ (transparent ? await renderBadge(size) : await sharp(readFileSync7(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
4653
4926
  cache3.set(key, out);
4654
4927
  return out;
4655
4928
  }
@@ -5867,7 +6140,7 @@ var shellCache = null;
5867
6140
  function readShell(path) {
5868
6141
  const { mtimeMs } = statSync3(path);
5869
6142
  if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
5870
- const html = readFileSync7(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6143
+ const html = readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
5871
6144
  shellCache = { mtimeMs, html };
5872
6145
  return html;
5873
6146
  }
@@ -5892,12 +6165,12 @@ function serveBuildFile(c, webDist, dir, name) {
5892
6165
  c.header("Content-Type", type);
5893
6166
  c.header("Cache-Control", "public, max-age=31536000, immutable");
5894
6167
  if (ext === ".css") {
5895
- return c.body(readFileSync7(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6168
+ return c.body(readFileSync8(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
5896
6169
  }
5897
6170
  if (ext === ".js") {
5898
- return c.body(readFileSync7(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6171
+ return c.body(readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
5899
6172
  }
5900
- return c.body(new Uint8Array(readFileSync7(path)));
6173
+ return c.body(new Uint8Array(readFileSync8(path)));
5901
6174
  }
5902
6175
  var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
5903
6176
  function servePublishedHtml(c, share) {
@@ -6659,8 +6932,48 @@ function createAdminRouter() {
6659
6932
  return admin;
6660
6933
  }
6661
6934
 
6935
+ // src/dashboard/agent-identities.ts
6936
+ var BOT_LABEL = "Workspace Agent";
6937
+ var BOT_DM_KEY = "claude";
6938
+ async function resolveAgentIdentities() {
6939
+ const settings = loadDashboardSettings();
6940
+ const { cwd: defaultCwd } = resolveChannelConfig("");
6941
+ const main2 = [
6942
+ {
6943
+ name: BOT_DM_KEY,
6944
+ label: BOT_LABEL,
6945
+ kind: "main",
6946
+ cwd: defaultCwd,
6947
+ model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
6948
+ ...await loadBotIdentity()
6949
+ }
6950
+ ];
6951
+ return Promise.all(
6952
+ main2.map(async ({ cwd, ...agent }) => {
6953
+ const dm = await ensureAgentDmChannel(agent.name, {
6954
+ name: agent.displayName ?? agent.label,
6955
+ cwd
6956
+ });
6957
+ return { ...agent, channelId: dm.id };
6958
+ })
6959
+ );
6960
+ }
6961
+ async function offlineAgentIdentities() {
6962
+ const dm = findChannelByAgentName(BOT_DM_KEY);
6963
+ if (!dm) return [];
6964
+ return [
6965
+ {
6966
+ name: BOT_DM_KEY,
6967
+ label: BOT_LABEL,
6968
+ kind: "main",
6969
+ channelId: dm.id,
6970
+ ...await loadBotIdentity()
6971
+ }
6972
+ ];
6973
+ }
6974
+
6662
6975
  // src/dashboard/host-api.ts
6663
- import { existsSync as existsSync21, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
6976
+ import { existsSync as existsSync21, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
6664
6977
  import { join as join19 } from "path";
6665
6978
  import sharp3 from "sharp";
6666
6979
 
@@ -6899,8 +7212,8 @@ function createChannelWriteRouter() {
6899
7212
  (id, position) => supabase.from("channels").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id)
6900
7213
  )
6901
7214
  );
6902
- const failed = results.find((r) => r.error);
6903
- if (failed?.error) return c.json({ error: failed.error.message }, 500);
7215
+ const failed2 = results.find((r) => r.error);
7216
+ if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
6904
7217
  await refreshChannelRegistry();
6905
7218
  return c.json({ ok: true });
6906
7219
  });
@@ -6958,10 +7271,10 @@ import {
6958
7271
  openSync,
6959
7272
  readSync,
6960
7273
  readdirSync as readdirSync4,
6961
- readFileSync as readFileSync10,
6962
- realpathSync,
7274
+ readFileSync as readFileSync11,
7275
+ realpathSync as realpathSync2,
6963
7276
  renameSync,
6964
- rmSync as rmSync4,
7277
+ rmSync as rmSync5,
6965
7278
  statSync as statSync5,
6966
7279
  writeFileSync as writeFileSync7
6967
7280
  } from "fs";
@@ -7249,14 +7562,14 @@ async function readOrQueueDocAudio(input2) {
7249
7562
  }
7250
7563
 
7251
7564
  // src/dashboard/artifact-export.ts
7252
- import { readFileSync as readFileSync9 } from "fs";
7565
+ import { readFileSync as readFileSync10 } from "fs";
7253
7566
 
7254
7567
  // src/dashboard/chat-agent.ts
7255
7568
  import { homedir as homedir7 } from "os";
7256
7569
 
7257
7570
  // src/mcp/config.ts
7258
- import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
7259
- import { tmpdir } from "os";
7571
+ import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
7572
+ import { tmpdir as tmpdir2 } from "os";
7260
7573
  import { join as join14 } from "path";
7261
7574
 
7262
7575
  // src/mcp/crypto.ts
@@ -7943,12 +8256,12 @@ async function buildMcpConfig(profile, connectionIds) {
7943
8256
  return { config, skipped };
7944
8257
  }
7945
8258
  function writeMcpConfigFile(config) {
7946
- const dir = mkdtempSync(join14(tmpdir(), "flyspace-mcp-"));
8259
+ const dir = mkdtempSync(join14(tmpdir2(), "flyspace-mcp-"));
7947
8260
  const path = join14(dir, "mcp-config.json");
7948
8261
  writeFileSync6(path, JSON.stringify(config), { mode: 384 });
7949
8262
  return {
7950
8263
  path,
7951
- cleanup: () => rmSync3(dir, { recursive: true, force: true })
8264
+ cleanup: () => rmSync4(dir, { recursive: true, force: true })
7952
8265
  };
7953
8266
  }
7954
8267
  async function withMcpConfig(profile, connectionIds, fn) {
@@ -7965,7 +8278,7 @@ async function withMcpConfig(profile, connectionIds, fn) {
7965
8278
  }
7966
8279
 
7967
8280
  // src/claude/agents.ts
7968
- import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
8281
+ import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
7969
8282
  import { homedir as homedir6 } from "os";
7970
8283
  import { basename as basename4, join as join15 } from "path";
7971
8284
  import yaml from "js-yaml";
@@ -7980,7 +8293,7 @@ function expandHome(p) {
7980
8293
  function parseFrontmatter(file) {
7981
8294
  let content;
7982
8295
  try {
7983
- content = readFileSync8(file, "utf-8");
8296
+ content = readFileSync9(file, "utf-8");
7984
8297
  } catch {
7985
8298
  return {};
7986
8299
  }
@@ -8026,16 +8339,241 @@ function readAgentPersona(configDir2, cwd, name) {
8026
8339
  const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
8027
8340
  if (!agent) return void 0;
8028
8341
  try {
8029
- const content = readFileSync8(agent.path, "utf-8");
8342
+ const content = readFileSync9(agent.path, "utf-8");
8030
8343
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
8031
8344
  } catch {
8032
8345
  return void 0;
8033
8346
  }
8034
8347
  }
8035
8348
 
8349
+ // src/store/memory-store.ts
8350
+ var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
8351
+ var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8352
+ var DESCRIPTION_MAX = 200;
8353
+ var BODY_MAX = 4e3;
8354
+ var MEMORY_INDEX_MAX_CHARS = 8e3;
8355
+ function isMemoryType(value) {
8356
+ return MEMORY_TYPES.includes(value);
8357
+ }
8358
+ function validateMemoryInput(input2) {
8359
+ if (!NAME_RE.test(input2.name))
8360
+ throw new Error("name must be a kebab-case slug, e.g. deploy-flow");
8361
+ if (!isMemoryType(input2.type)) throw new Error(`type must be one of ${MEMORY_TYPES.join(", ")}`);
8362
+ const description = input2.description.trim();
8363
+ if (!description || description.includes("\n") || description.length > DESCRIPTION_MAX) {
8364
+ throw new Error(`description must be one line of at most ${DESCRIPTION_MAX} characters`);
8365
+ }
8366
+ const body = input2.body.trim();
8367
+ if (!body || body.length > BODY_MAX) {
8368
+ throw new Error(`body must be 1 to ${BODY_MAX} characters`);
8369
+ }
8370
+ }
8371
+ function rowToMemory(row) {
8372
+ return {
8373
+ id: row["id"],
8374
+ channelId: row["channel_id"],
8375
+ name: row["name"],
8376
+ description: row["description"],
8377
+ type: row["type"],
8378
+ body: row["body"],
8379
+ sourceThreadId: row["source_thread_id"] ?? null,
8380
+ createdBy: row["created_by"],
8381
+ createdAt: row["created_at"],
8382
+ updatedAt: row["updated_at"]
8383
+ };
8384
+ }
8385
+ async function listChannelMemories(channelId) {
8386
+ const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
8387
+ if (error2) throw new Error(`listChannelMemories(${channelId}): ${error2.message}`);
8388
+ return (data ?? []).map(rowToMemory);
8389
+ }
8390
+ async function getChannelMemory(channelId, name) {
8391
+ const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
8392
+ if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
8393
+ return data ? rowToMemory(data) : null;
8394
+ }
8395
+ async function saveChannelMemory(input2, actor) {
8396
+ validateMemoryInput(input2);
8397
+ const supabase = getSupabaseClient();
8398
+ const existing = await getChannelMemory(input2.channelId, input2.name);
8399
+ const fields = {
8400
+ description: input2.description.trim(),
8401
+ type: input2.type,
8402
+ body: input2.body.trim(),
8403
+ source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
8404
+ created_by: input2.createdBy
8405
+ };
8406
+ if (existing && sameContent(existing, fields)) {
8407
+ return { memory: existing, created: false, changed: false };
8408
+ }
8409
+ const { data, error: error2 } = existing ? await supabase.from("memories").update({ ...fields, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id).select("*").single() : await supabase.from("memories").insert({ ...fields, scope: "channel", channel_id: input2.channelId, name: input2.name }).select("*").single();
8410
+ if (error2)
8411
+ throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
8412
+ const memory = rowToMemory(data);
8413
+ await logMemoryEvent({
8414
+ memory,
8415
+ action: existing ? "update" : "create",
8416
+ before: existing ? snapshot(existing) : null,
8417
+ after: snapshot(memory),
8418
+ actor,
8419
+ threadId: input2.sourceThreadId ?? null
8420
+ });
8421
+ return { memory, created: !existing, changed: true };
8422
+ }
8423
+ async function updateChannelMemory(channelId, memoryId, patch, actor) {
8424
+ const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
8425
+ if (!existing) return null;
8426
+ const { memory } = await saveChannelMemory(
8427
+ {
8428
+ channelId,
8429
+ name: existing.name,
8430
+ description: patch.description ?? existing.description,
8431
+ type: patch.type ?? existing.type,
8432
+ body: patch.body ?? existing.body,
8433
+ createdBy: existing.createdBy
8434
+ },
8435
+ actor
8436
+ );
8437
+ return memory;
8438
+ }
8439
+ async function searchChannelMemories(channelId, query) {
8440
+ const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
8441
+ if (!words.length) return [];
8442
+ let request2 = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
8443
+ for (const word of words) {
8444
+ request2 = request2.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
8445
+ }
8446
+ const { data, error: error2 } = await request2.order("updated_at", { ascending: false }).limit(20);
8447
+ if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
8448
+ return (data ?? []).map(rowToMemory);
8449
+ }
8450
+ async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
8451
+ const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
8452
+ if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
8453
+ const deleted = (data ?? []).map(rowToMemory);
8454
+ for (const memory of deleted) {
8455
+ await logMemoryEvent({
8456
+ memory,
8457
+ action: "delete",
8458
+ before: snapshot(memory),
8459
+ after: null,
8460
+ actor,
8461
+ threadId: threadId ?? null
8462
+ });
8463
+ }
8464
+ return deleted.length > 0;
8465
+ }
8466
+ function snapshot(m) {
8467
+ return {
8468
+ description: m.description,
8469
+ type: m.type,
8470
+ body: m.body,
8471
+ sourceThreadId: m.sourceThreadId,
8472
+ createdBy: m.createdBy
8473
+ };
8474
+ }
8475
+ function sameContent(m, other) {
8476
+ return m.description === other.description && m.type === other.type && m.body === other.body;
8477
+ }
8478
+ async function logMemoryEvent(e) {
8479
+ const { error: error2 } = await getSupabaseClient().from("memory_events").insert({
8480
+ channel_id: e.memory.channelId,
8481
+ memory_id: e.action === "delete" ? null : e.memory.id,
8482
+ thread_id: e.threadId,
8483
+ name: e.memory.name,
8484
+ action: e.action,
8485
+ before: e.before,
8486
+ after: e.after,
8487
+ actor: e.actor
8488
+ });
8489
+ if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
8490
+ }
8491
+ function rowToEvent(row) {
8492
+ return {
8493
+ id: row["id"],
8494
+ channelId: row["channel_id"],
8495
+ memoryId: row["memory_id"] ?? null,
8496
+ threadId: row["thread_id"] ?? null,
8497
+ name: row["name"],
8498
+ action: row["action"],
8499
+ before: row["before"] ?? null,
8500
+ after: row["after"] ?? null,
8501
+ actor: row["actor"],
8502
+ createdAt: row["created_at"],
8503
+ undoneAt: row["undone_at"] ?? null
8504
+ };
8505
+ }
8506
+ async function listThreadMemoryEvents(channelId, threadId) {
8507
+ const { data, error: error2 } = await getSupabaseClient().from("memory_events").select("*").eq("channel_id", channelId).eq("thread_id", threadId).order("created_at", { ascending: false }).limit(50);
8508
+ if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
8509
+ return (data ?? []).map(rowToEvent);
8510
+ }
8511
+ async function undoMemoryEvent(channelId, eventId) {
8512
+ const supabase = getSupabaseClient();
8513
+ const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
8514
+ if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
8515
+ if (!data) return "not-found";
8516
+ const event = rowToEvent(data);
8517
+ if (event.undoneAt) return "already-undone";
8518
+ const current = await getChannelMemory(channelId, event.name);
8519
+ const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
8520
+ if (!unchangedSince) return "conflict";
8521
+ const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
8522
+ if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
8523
+ if (!(claimed.data ?? []).length) return "already-undone";
8524
+ const before = event.before;
8525
+ const restore = before ? {
8526
+ description: before.description,
8527
+ type: before.type,
8528
+ body: before.body,
8529
+ source_thread_id: before.sourceThreadId,
8530
+ created_by: before.createdBy
8531
+ } : null;
8532
+ const { error: writeError } = !restore ? await supabase.from("memories").delete().eq("id", current.id) : current ? await supabase.from("memories").update({ ...restore, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", current.id) : await supabase.from("memories").insert({ ...restore, scope: "channel", channel_id: channelId, name: event.name });
8533
+ if (writeError) {
8534
+ await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
8535
+ throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
8536
+ }
8537
+ return "undone";
8538
+ }
8539
+ function isUuid(value) {
8540
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
8541
+ }
8542
+ function formatMemoryPrompt(channelId, memories) {
8543
+ const lines = [];
8544
+ let used = 0;
8545
+ let omitted = 0;
8546
+ for (const m of memories) {
8547
+ const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
8548
+ if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
8549
+ omitted++;
8550
+ continue;
8551
+ }
8552
+ lines.push(line);
8553
+ used += line.length + 1;
8554
+ }
8555
+ const index = lines.length ? lines.join("\n") + (omitted ? `
8556
+ - (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
8557
+ return [
8558
+ `## Channel memory`,
8559
+ ``,
8560
+ `This channel has a persistent memory shared by every thread in it and by every agent runner. Use it instead of any built-in memory directory of your own CLI.`,
8561
+ ``,
8562
+ `- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
8563
+ `- Search: \`flyspace memory search --channel ${channelId} <words>\``,
8564
+ `- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
8565
+ `- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
8566
+ ``,
8567
+ `Save when someone asks you to remember something, corrects you, or a decision is made that later threads will need. Save what is not derivable from the code, git history or docs: decisions and why, preferences, corrections, where external things live. Do not save one-off details of this conversation. Check the index first and update an existing memory rather than adding a near-duplicate. Write memories in English. Pass \`--thread\` with the thread id from the \`[context: ...]\` header, so the thread shows what changed and it can be undone there.`,
8568
+ ``,
8569
+ `Index:`,
8570
+ index
8571
+ ].join("\n");
8572
+ }
8573
+
8036
8574
  // src/dashboard/rollover-summary.ts
8037
8575
  import { spawn as spawn5 } from "child_process";
8038
- import { tmpdir as tmpdir2 } from "os";
8576
+ import { tmpdir as tmpdir3 } from "os";
8039
8577
  var HAIKU_MODEL = "haiku";
8040
8578
  var CJK_PATTERN = /[ -〿぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/;
8041
8579
  function estimateTokens(text) {
@@ -8098,7 +8636,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8098
8636
  // A stateless rewrite touches no files — tmpdir keeps it that way and
8099
8637
  // avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
8100
8638
  // unlike the channel's own cwd the main turn runs in.
8101
- cwd: tmpdir2(),
8639
+ cwd: tmpdir3(),
8102
8640
  env,
8103
8641
  stdio: ["ignore", "pipe", "pipe"]
8104
8642
  });
@@ -8894,6 +9432,13 @@ var ChatAgent = class {
8894
9432
  thread.channel_id
8895
9433
  );
8896
9434
  if (docsAccess && docsAccess !== "write") channelConfig = { ...channelConfig, docsAccess };
9435
+ if (docsAccess !== "none" && isUuid(thread.channel_id)) {
9436
+ const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
9437
+ console.error("[chat-agent] memory lookup failed:", err.message);
9438
+ return void 0;
9439
+ });
9440
+ if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
9441
+ }
8897
9442
  const store = this.openSessionStore();
8898
9443
  try {
8899
9444
  const channelKey = `chat:${thread.channel_id}`;
@@ -9301,7 +9846,8 @@ ${msg.body}` : msg.body;
9301
9846
  signal,
9302
9847
  mcpConfigPath,
9303
9848
  ...usageContext ? { usageContext } : {},
9304
- ...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {}
9849
+ ...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {},
9850
+ ...channelConfig.memoryPrompt ? { memoryPrompt: channelConfig.memoryPrompt } : {}
9305
9851
  },
9306
9852
  channelConfig.claudeConfigDir,
9307
9853
  sessionId,
@@ -9338,6 +9884,7 @@ ${msg.body}` : msg.body;
9338
9884
  channelConfig.prompt,
9339
9885
  resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
9340
9886
  ),
9887
+ channelConfig.memoryPrompt,
9341
9888
  docsAccessPromptNote(channelConfig.docsAccess)
9342
9889
  ].filter(Boolean).join("\n\n---\n\n"),
9343
9890
  // Codex has no per-tool deny list, so any restricted requester gets
@@ -9615,7 +10162,7 @@ async function exportDocAsArtifact(channelId, docPath) {
9615
10162
  );
9616
10163
  const abs = resolveDocPath(channelConfig.cwd, docPath);
9617
10164
  if (!abs) throw new Error(`doc not found: ${docPath}`);
9618
- const content = readFileSync9(abs, "utf8");
10165
+ const content = readFileSync10(abs, "utf8");
9619
10166
  const title = extractTitle(abs, "md");
9620
10167
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9621
10168
  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).
@@ -9894,12 +10441,12 @@ function readGitBranch(dir) {
9894
10441
  try {
9895
10442
  let gitDir = join17(dir, ".git");
9896
10443
  if (statSync5(gitDir).isFile()) {
9897
- const pointer = readFileSync10(gitDir, "utf8").trim();
10444
+ const pointer = readFileSync11(gitDir, "utf8").trim();
9898
10445
  const match = pointer.match(/^gitdir:\s*(.+)$/);
9899
10446
  if (!match) return void 0;
9900
10447
  gitDir = resolve5(dir, match[1]);
9901
10448
  }
9902
- const head = readFileSync10(join17(gitDir, "HEAD"), "utf8").trim();
10449
+ const head = readFileSync11(join17(gitDir, "HEAD"), "utf8").trim();
9903
10450
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
9904
10451
  if (branchMatch) return branchMatch[1];
9905
10452
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -9926,8 +10473,8 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
9926
10473
  const kind = entryKind(entry, abs);
9927
10474
  if (kind === "dir") {
9928
10475
  if (SKIP_DIRS.has(entry.name)) continue;
9929
- const probe2 = probeDir(abs, showHidden);
9930
- if (!probe2.hasDocs && probe2.hasOther) continue;
10476
+ const probe3 = probeDir(abs, showHidden);
10477
+ if (!probe3.hasDocs && probe3.hasOther) continue;
9931
10478
  const isRepo = existsSync17(join17(abs, ".git"));
9932
10479
  dirs.push({
9933
10480
  path,
@@ -10146,7 +10693,7 @@ function moveDocEntry(root, from, to) {
10146
10693
  return { status: "into-self" };
10147
10694
  }
10148
10695
  try {
10149
- if (realpathSync(finalAbs) !== realpathSync(absFrom)) return { status: "exists" };
10696
+ if (realpathSync2(finalAbs) !== realpathSync2(absFrom)) return { status: "exists" };
10150
10697
  } catch {
10151
10698
  }
10152
10699
  renameSync(absFrom, finalAbs);
@@ -10164,7 +10711,7 @@ function deleteDocEntry(root, path) {
10164
10711
  }
10165
10712
  const isDir2 = stat.isDirectory();
10166
10713
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10167
- rmSync4(abs, { recursive: isDir2 });
10714
+ rmSync5(abs, { recursive: isDir2 });
10168
10715
  return {
10169
10716
  status: "ok",
10170
10717
  path: relative(root, abs).split(sep).join("/"),
@@ -10203,7 +10750,7 @@ function createDocsRouter() {
10203
10750
  }
10204
10751
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10205
10752
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10206
- const data = readFileSync10(resolved2.abs);
10753
+ const data = readFileSync11(resolved2.abs);
10207
10754
  const headers = { "Content-Type": resolved2.contentType };
10208
10755
  if (download) {
10209
10756
  const filename = basename5(resolved2.abs).replace(/"/g, "");
@@ -10223,7 +10770,7 @@ function createDocsRouter() {
10223
10770
  }
10224
10771
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10225
10772
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10226
- return c.body(new Uint8Array(readFileSync10(abs)), 200, {
10773
+ return c.body(new Uint8Array(readFileSync11(abs)), 200, {
10227
10774
  "Content-Type": "text/html; charset=utf-8",
10228
10775
  "Content-Security-Policy": HTML_SHARE_CSP,
10229
10776
  "X-Content-Type-Options": "nosniff",
@@ -10239,10 +10786,10 @@ function createDocsRouter() {
10239
10786
  const files = form.getAll("file").filter((f) => f instanceof File);
10240
10787
  if (files.length === 0) return c.json({ error: "missing file" }, 400);
10241
10788
  const uploaded = [];
10242
- const failed = [];
10789
+ const failed2 = [];
10243
10790
  for (const file of files) {
10244
10791
  if (file.size > MAX_UPLOAD_BYTES) {
10245
- failed.push({
10792
+ failed2.push({
10246
10793
  name: file.name,
10247
10794
  error: `larger than ${MAX_UPLOAD_BYTES / (1024 * 1024)}MB`
10248
10795
  });
@@ -10256,16 +10803,16 @@ function createDocsRouter() {
10256
10803
  case "no-dir":
10257
10804
  return c.json({ error: "folder not found" }, 404);
10258
10805
  case "bad-type":
10259
- failed.push({ name: file.name, error: "file type not allowed" });
10806
+ failed2.push({ name: file.name, error: "file type not allowed" });
10260
10807
  break;
10261
10808
  case "too-large":
10262
- failed.push({ name: file.name, error: "file too large" });
10809
+ failed2.push({ name: file.name, error: "file too large" });
10263
10810
  break;
10264
10811
  default:
10265
- failed.push({ name: file.name, error: "invalid filename" });
10812
+ failed2.push({ name: file.name, error: "invalid filename" });
10266
10813
  }
10267
10814
  }
10268
- return c.json({ path: dir, uploaded, failed }, uploaded.length > 0 ? 201 : 400);
10815
+ return c.json({ path: dir, uploaded, failed: failed2 }, uploaded.length > 0 ? 201 : 400);
10269
10816
  });
10270
10817
  app.post("/:id/docs/folder", async (c) => {
10271
10818
  const { cwd } = resolveChannelConfig(c.req.param("id"));
@@ -10408,7 +10955,7 @@ function createDocsRouter() {
10408
10955
  title: kind ? extractTitle(abs, kind) : basename5(abs),
10409
10956
  mtime: stat.mtime.toISOString(),
10410
10957
  tooLarge: tooLarge || void 0,
10411
- content: tooLarge ? "" : readFileSync10(abs, "utf8")
10958
+ content: tooLarge ? "" : readFileSync11(abs, "utf8")
10412
10959
  });
10413
10960
  });
10414
10961
  app.put("/:id/docs/content", async (c) => {
@@ -10451,7 +10998,7 @@ function createDocsRouter() {
10451
10998
  channel,
10452
10999
  rel,
10453
11000
  title: extractTitle(abs, "md"),
10454
- content: readFileSync10(abs, "utf8")
11001
+ content: readFileSync11(abs, "utf8")
10455
11002
  };
10456
11003
  };
10457
11004
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -10674,7 +11221,7 @@ function ownSkills(scope, dir) {
10674
11221
  return [{ scope, name, description, body, path, plugin: null, mtime }];
10675
11222
  });
10676
11223
  }
10677
- function snapshot(scopes) {
11224
+ function snapshot2(scopes) {
10678
11225
  const wants = (scope) => !scopes || scopes.has(scope);
10679
11226
  const configDir2 = profileClaudeConfigDir();
10680
11227
  const rows = [];
@@ -10709,7 +11256,7 @@ async function pushSkillScopes(scopes) {
10709
11256
  if (!mirrorEnabled()) return;
10710
11257
  const workspace = getProfile();
10711
11258
  const supabase = getSupabaseClient();
10712
- const rows = snapshot(scopes ? new Set(scopes) : void 0);
11259
+ const rows = snapshot2(scopes ? new Set(scopes) : void 0);
10713
11260
  let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
10714
11261
  if (scopes) query = query.in("scope", scopes);
10715
11262
  const { data, error: error2 } = await query;
@@ -10989,32 +11536,6 @@ function registerMirroredSkillRoutes(api, agent) {
10989
11536
  mountSkillRoutes(api, "/channels/:id/skill-files", channelRef, backend);
10990
11537
  }
10991
11538
 
10992
- // src/dashboard/agent-identities.ts
10993
- var BOT_DM_KEY = "claude";
10994
- async function resolveAgentIdentities() {
10995
- const settings = loadDashboardSettings();
10996
- const { cwd: defaultCwd } = resolveChannelConfig("");
10997
- const main2 = [
10998
- {
10999
- name: BOT_DM_KEY,
11000
- label: "Workspace Agent",
11001
- kind: "main",
11002
- cwd: defaultCwd,
11003
- model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
11004
- ...await loadBotIdentity()
11005
- }
11006
- ];
11007
- return Promise.all(
11008
- main2.map(async ({ cwd, ...agent }) => {
11009
- const dm = await ensureAgentDmChannel(agent.name, {
11010
- name: agent.displayName ?? agent.label,
11011
- cwd
11012
- });
11013
- return { ...agent, channelId: dm.id };
11014
- })
11015
- );
11016
- }
11017
-
11018
11539
  // src/dashboard/providers.ts
11019
11540
  import { execFileSync as execFileSync2 } from "child_process";
11020
11541
  import { existsSync as existsSync20 } from "fs";
@@ -11048,7 +11569,7 @@ function claudeAuthAvailable() {
11048
11569
  return false;
11049
11570
  }
11050
11571
  }
11051
- function probe(runner) {
11572
+ function probe2(runner) {
11052
11573
  const label2 = PROVIDER_LABELS[runner];
11053
11574
  const bin = runner === "codex" ? "codex" : "claude";
11054
11575
  if (!binaryOnPath(bin)) {
@@ -11069,7 +11590,7 @@ function providerStatuses() {
11069
11590
  const key = getProfile();
11070
11591
  const hit = cache6.get(key);
11071
11592
  if (hit && Date.now() - hit.at < TTL_MS2) return hit.statuses;
11072
- const statuses = ["claude", "codex"].map(probe);
11593
+ const statuses = ["claude", "codex"].map(probe2);
11073
11594
  cache6.set(key, { at: Date.now(), statuses });
11074
11595
  return statuses;
11075
11596
  }
@@ -11192,7 +11713,7 @@ function registerHostRoutes(api) {
11192
11713
  }
11193
11714
  function tailFile(file, limit) {
11194
11715
  if (!existsSync21(file)) return [];
11195
- const lines = readFileSync11(file, "utf-8").split("\n");
11716
+ const lines = readFileSync12(file, "utf-8").split("\n");
11196
11717
  if (lines.at(-1) === "") lines.pop();
11197
11718
  return lines.slice(-limit);
11198
11719
  }
@@ -11304,8 +11825,8 @@ function createCategoryWriteRouter() {
11304
11825
  (id, position) => supabase.from("categories").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).eq("profile", getProfile())
11305
11826
  )
11306
11827
  );
11307
- const failed = results.find((r) => r.error);
11308
- if (failed?.error) return c.json({ error: failed.error.message }, 500);
11828
+ const failed2 = results.find((r) => r.error);
11829
+ if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
11309
11830
  return c.json({ ok: true });
11310
11831
  });
11311
11832
  app.put("/:id", async (c) => {
@@ -14259,8 +14780,80 @@ function createChannelMembersRouter() {
14259
14780
  return router;
14260
14781
  }
14261
14782
 
14262
- // src/dashboard/onboarding-routes.ts
14783
+ // src/dashboard/memories-routes.ts
14263
14784
  import { Hono as Hono27 } from "hono";
14785
+ var requireChannelVisible = async (c, next) => {
14786
+ const channel = findChannel(c.req.param("id"));
14787
+ if (!channel || !canSeeChannel(channel, c.get("auth"))) {
14788
+ return c.json({ error: "channel not found" }, 404);
14789
+ }
14790
+ c.set("memoryChannelId", channel.id);
14791
+ await next();
14792
+ };
14793
+ function createMemoriesRouter() {
14794
+ const router = new Hono27();
14795
+ for (const path of [
14796
+ "/:id/memories",
14797
+ "/:id/memories/*",
14798
+ "/:id/memory-events",
14799
+ "/:id/memory-events/*"
14800
+ ]) {
14801
+ router.use(path, requireChannelVisible, requireDocsAccess);
14802
+ }
14803
+ router.get("/:id/memories", async (c) => {
14804
+ return c.json({ memories: await listChannelMemories(c.get("memoryChannelId")) });
14805
+ });
14806
+ router.patch("/:id/memories/:memoryId", async (c) => {
14807
+ const memoryId = c.req.param("memoryId");
14808
+ if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
14809
+ const body = await c.req.json().catch(() => ({}));
14810
+ const patch = {};
14811
+ if (typeof body["description"] === "string") patch.description = body["description"];
14812
+ if (typeof body["body"] === "string") patch.body = body["body"];
14813
+ const type = body["type"];
14814
+ if (type !== void 0) {
14815
+ if (!isMemoryType(type)) return c.json({ error: "invalid type" }, 400);
14816
+ patch.type = type;
14817
+ }
14818
+ try {
14819
+ const memory = await updateChannelMemory(c.get("memoryChannelId"), memoryId, patch, "user");
14820
+ if (!memory) return c.json({ error: "memory not found" }, 404);
14821
+ return c.json({ memory });
14822
+ } catch (err) {
14823
+ return c.json({ error: err.message }, 400);
14824
+ }
14825
+ });
14826
+ router.delete("/:id/memories/:memoryId", async (c) => {
14827
+ const memoryId = c.req.param("memoryId");
14828
+ if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
14829
+ const deleted = await deleteChannelMemory(c.get("memoryChannelId"), memoryId, "user");
14830
+ if (!deleted) return c.json({ error: "memory not found" }, 404);
14831
+ return c.json({ ok: true });
14832
+ });
14833
+ router.get("/:id/memory-events", async (c) => {
14834
+ const threadId = c.req.query("thread") ?? "";
14835
+ if (!isUuid(threadId)) return c.json({ error: "thread query parameter required" }, 400);
14836
+ return c.json({ events: await listThreadMemoryEvents(c.get("memoryChannelId"), threadId) });
14837
+ });
14838
+ router.post("/:id/memory-events/:eventId/undo", async (c) => {
14839
+ const eventId = c.req.param("eventId");
14840
+ if (!isUuid(eventId)) return c.json({ error: "invalid event id" }, 400);
14841
+ const result = await undoMemoryEvent(c.get("memoryChannelId"), eventId);
14842
+ if (result === "not-found") return c.json({ error: "event not found" }, 404);
14843
+ if (result === "already-undone") return c.json({ error: "already undone" }, 409);
14844
+ if (result === "conflict") {
14845
+ return c.json(
14846
+ { error: "The memory has changed since, so this can no longer be undone." },
14847
+ 409
14848
+ );
14849
+ }
14850
+ return c.json({ ok: true });
14851
+ });
14852
+ return router;
14853
+ }
14854
+
14855
+ // src/dashboard/onboarding-routes.ts
14856
+ import { Hono as Hono28 } from "hono";
14264
14857
 
14265
14858
  // src/dashboard/unique-channel-name.ts
14266
14859
  function uniqueChannelName(raw, taken) {
@@ -14349,7 +14942,7 @@ async function buildGreeting(input2) {
14349
14942
  // src/dashboard/onboarding-routes.ts
14350
14943
  var MAX_FOLDERS = 20;
14351
14944
  function createOnboardingRouter(agent) {
14352
- const app = new Hono27();
14945
+ const app = new Hono28();
14353
14946
  app.get("/scan", async (c) => {
14354
14947
  const ctx = await resolveAuthContext(c);
14355
14948
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -14594,6 +15187,7 @@ function registerDataRoutes(api, agent = {}) {
14594
15187
  api.route("/channels", createDocShareWriteRouter(agent));
14595
15188
  api.route("/onboarding", createOnboardingRouter(agent));
14596
15189
  api.route("/channels", createChannelMembersRouter());
15190
+ api.route("/channels", createMemoriesRouter());
14597
15191
  api.route("/docs", createDocShareListRouter());
14598
15192
  api.get("/users", (c) => c.json({ users: listActiveUsers() }));
14599
15193
  api.route("/categories", createCategoryWriteRouter());
@@ -14844,17 +15438,17 @@ function registerDataRoutes(api, agent = {}) {
14844
15438
  // src/dashboard/device-auth-routes.ts
14845
15439
  import { verify } from "crypto";
14846
15440
  import { getConnInfo } from "@hono/node-server/conninfo";
14847
- import { Hono as Hono28 } from "hono";
15441
+ import { Hono as Hono29 } from "hono";
14848
15442
 
14849
15443
  // src/dashboard/agent-bridge.ts
14850
15444
  import { randomBytes as randomBytes8, randomUUID as randomUUID4, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
14851
- import { existsSync as existsSync25, readFileSync as readFileSync15 } from "fs";
15445
+ import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
14852
15446
  import dotenv from "dotenv";
14853
15447
  import { WebSocket, WebSocketServer } from "ws";
14854
15448
 
14855
15449
  // src/dashboard/host-identity.ts
14856
15450
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
14857
- import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
15451
+ import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
14858
15452
  import { dirname as dirname6, join as join20 } from "path";
14859
15453
  function hostPrivateKeyPath() {
14860
15454
  return process.env["FLYSPACE_HOST_KEY_PATH"] || join20(paths.BASE_CONFIG_DIR, "host.key");
@@ -14865,7 +15459,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
14865
15459
  `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.`
14866
15460
  );
14867
15461
  }
14868
- return createPrivateKey2(readFileSync12(path, "utf8"));
15462
+ return createPrivateKey2(readFileSync13(path, "utf8"));
14869
15463
  }
14870
15464
  function loadHostPublicKey() {
14871
15465
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -14881,7 +15475,7 @@ function loadHostPublicKey() {
14881
15475
  // src/dashboard/host-lock.ts
14882
15476
  import { execFileSync as execFileSync3 } from "child_process";
14883
15477
  import { randomBytes as randomBytes5 } from "crypto";
14884
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "fs";
15478
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
14885
15479
  import { join as join21 } from "path";
14886
15480
  function hostLockPath() {
14887
15481
  return join21(paths.BASE_DATA_DIR, "host.lock");
@@ -14899,7 +15493,7 @@ function isLiveFlyspace(pid) {
14899
15493
  }
14900
15494
  function readLockPid(path) {
14901
15495
  try {
14902
- const pid = Number(JSON.parse(readFileSync13(path, "utf8")).pid);
15496
+ const pid = Number(JSON.parse(readFileSync14(path, "utf8")).pid);
14903
15497
  return Number.isInteger(pid) && pid > 0 ? pid : null;
14904
15498
  } catch {
14905
15499
  return null;
@@ -14917,7 +15511,7 @@ function acquireHostLock() {
14917
15511
  try {
14918
15512
  writeFileSync9(path, body, { flag: "wx", mode: 384 });
14919
15513
  const release2 = () => {
14920
- if (readLockPid(path) === process.pid) rmSync5(path, { force: true });
15514
+ if (readLockPid(path) === process.pid) rmSync6(path, { force: true });
14921
15515
  };
14922
15516
  process.once("exit", release2);
14923
15517
  return release2;
@@ -14931,7 +15525,7 @@ function acquireHostLock() {
14931
15525
  `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.`
14932
15526
  );
14933
15527
  }
14934
- rmSync5(path, { force: true });
15528
+ rmSync6(path, { force: true });
14935
15529
  }
14936
15530
  }
14937
15531
  throw new Error(`Could not take the Host lock at ${path}`);
@@ -14940,13 +15534,13 @@ function publishHostEndpoint(port) {
14940
15534
  const path = hostLockPath();
14941
15535
  if (readLockPid(path) !== process.pid) return null;
14942
15536
  const token = randomBytes5(32).toString("hex");
14943
- const lock = JSON.parse(readFileSync13(path, "utf8"));
15537
+ const lock = JSON.parse(readFileSync14(path, "utf8"));
14944
15538
  writeFileSync9(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
14945
15539
  return token;
14946
15540
  }
14947
15541
  function readHostEndpoint() {
14948
15542
  try {
14949
- const { pid, port, token } = JSON.parse(readFileSync13(hostLockPath(), "utf8"));
15543
+ const { pid, port, token } = JSON.parse(readFileSync14(hostLockPath(), "utf8"));
14950
15544
  if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
14951
15545
  return { port, token };
14952
15546
  } catch {
@@ -15149,16 +15743,35 @@ async function listWorkspaceDevices(workspaceId) {
15149
15743
  if (error2) fail("listWorkspaceDevices", error2);
15150
15744
  return (data ?? []).map((r) => toDevice(r.devices)).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
15151
15745
  }
15746
+ async function touchDevice(deviceId) {
15747
+ const { error: error2 } = await getSupabaseClient().from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", deviceId);
15748
+ if (error2) fail("touchDevice", error2);
15749
+ }
15750
+ var STALE_HOST_MS = 14 * 24 * 60 * 6e4;
15751
+ var isStale = (lastSeenAt) => !!lastSeenAt && Date.now() - Date.parse(lastSeenAt) > STALE_HOST_MS;
15152
15752
  async function listWorkspaceHosts(workspaceIds, exceptPublicKey) {
15153
15753
  const hosts2 = /* @__PURE__ */ new Map();
15154
15754
  if (!workspaceIds.length) return hosts2;
15155
- const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("workspace_id, devices!inner(name, public_key, revoked_at)").in("workspace_id", workspaceIds).is("devices.revoked_at", null);
15755
+ const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("workspace_id, devices!inner(name, public_key, revoked_at, last_seen_at)").in("workspace_id", workspaceIds).is("devices.revoked_at", null);
15156
15756
  if (error2) fail("listWorkspaceHosts", error2);
15157
15757
  for (const r of data ?? []) {
15158
- if (r.devices.public_key !== exceptPublicKey) hosts2.set(r.workspace_id, r.devices.name);
15758
+ if (r.devices.public_key !== exceptPublicKey && !isStale(r.devices.last_seen_at)) {
15759
+ hosts2.set(r.workspace_id, r.devices.name);
15760
+ }
15159
15761
  }
15160
15762
  return hosts2;
15161
15763
  }
15764
+ async function releaseStaleHosts(workspaceIds, exceptPublicKey) {
15765
+ if (!workspaceIds.length) return;
15766
+ const db = getSupabaseClient();
15767
+ const { data, error: error2 } = await db.from("device_workspaces").select("device_id, workspace_id, devices!inner(public_key, last_seen_at)").in("workspace_id", workspaceIds);
15768
+ if (error2) fail("releaseStaleHosts", error2);
15769
+ for (const r of data ?? []) {
15770
+ if (r.devices.public_key === exceptPublicKey || !isStale(r.devices.last_seen_at)) continue;
15771
+ const { error: delError } = await db.from("device_workspaces").delete().eq("device_id", r.device_id).eq("workspace_id", r.workspace_id);
15772
+ if (delError) fail("releaseStaleHosts", delError);
15773
+ }
15774
+ }
15162
15775
  async function userEmail(userId) {
15163
15776
  const { data, error: error2 } = await getSupabaseClient().auth.admin.getUserById(userId);
15164
15777
  if (error2) return null;
@@ -15167,7 +15780,7 @@ async function userEmail(userId) {
15167
15780
 
15168
15781
  // src/dashboard/doc-share-publisher.ts
15169
15782
  import { randomBytes as randomBytes7 } from "crypto";
15170
- import { readFileSync as readFileSync14, statSync as statSync9 } from "fs";
15783
+ import { readFileSync as readFileSync15, statSync as statSync9 } from "fs";
15171
15784
  import { extname as extname2, posix } from "path";
15172
15785
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
15173
15786
 
@@ -15354,7 +15967,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15354
15967
  }
15355
15968
  if (!stat.isFile()) return { status: "not-found" };
15356
15969
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
15357
- const source = readFileSync14(abs, "utf8");
15970
+ const source = readFileSync15(abs, "utf8");
15358
15971
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
15359
15972
  if (kind === "excalidraw") {
15360
15973
  try {
@@ -15434,7 +16047,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15434
16047
  let text = null;
15435
16048
  try {
15436
16049
  const size = statSync9(docAbs).size;
15437
- if (size <= budget) text = readFileSync14(docAbs, "utf8");
16050
+ if (size <= budget) text = readFileSync15(docAbs, "utf8");
15438
16051
  } catch {
15439
16052
  continue;
15440
16053
  }
@@ -15451,7 +16064,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15451
16064
  let body;
15452
16065
  try {
15453
16066
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
15454
- body = readFileSync14(resolved2.abs);
16067
+ body = readFileSync15(resolved2.abs);
15455
16068
  } catch {
15456
16069
  continue;
15457
16070
  }
@@ -15493,7 +16106,7 @@ async function publishDocShare(input2) {
15493
16106
  ...HTML_FEATURES,
15494
16107
  brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
15495
16108
  } : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
15496
- const snapshot2 = {
16109
+ const snapshot3 = {
15497
16110
  ...built,
15498
16111
  sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
15499
16112
  };
@@ -15503,8 +16116,8 @@ async function publishDocShare(input2) {
15503
16116
  const renamed = await setDocShareSlug(existing.id, slug);
15504
16117
  if (!renamed) return { status: "not-found" };
15505
16118
  }
15506
- const share2 = await updateDocShareSnapshot(existing.id, snapshot2, features);
15507
- const stale = existing.assetKeys.filter((key) => !snapshot2.assetKeys.includes(key));
16119
+ const share2 = await updateDocShareSnapshot(existing.id, snapshot3, features);
16120
+ const stale = existing.assetKeys.filter((key) => !snapshot3.assetKeys.includes(key));
15508
16121
  await deleteShareAssets(stale);
15509
16122
  if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
15510
16123
  void queueShareAudio(share2);
@@ -15518,7 +16131,7 @@ async function publishDocShare(input2) {
15518
16131
  channelId: input2.channelId,
15519
16132
  sourcePath: input2.sourcePath,
15520
16133
  createdBy: input2.createdBy,
15521
- ...snapshot2,
16134
+ ...snapshot3,
15522
16135
  ...features
15523
16136
  });
15524
16137
  void queueShareAudio(share);
@@ -15633,8 +16246,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
15633
16246
  const scope = table === "skill_files" && isWrite ? assigned : workspaces;
15634
16247
  if (scope.length === 0) return deny(`This Host is not the assigned Host for any workspace it may write ${table} to.`);
15635
16248
  if (method === "POST") {
15636
- const ok2 = rows.length > 0 && rows.every((r) => typeof r[slugColumn] === "string" && scope.includes(r[slugColumn]));
15637
- if (!ok2) return deny(`Every ${table} row must belong to a workspace this Host is assigned.`);
16249
+ const ok3 = rows.length > 0 && rows.every((r) => typeof r[slugColumn] === "string" && scope.includes(r[slugColumn]));
16250
+ if (!ok3) return deny(`Every ${table} row must belong to a workspace this Host is assigned.`);
15638
16251
  }
15639
16252
  if (method === "PATCH") {
15640
16253
  const patch = bodyRows(req.body);
@@ -15648,8 +16261,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
15648
16261
  if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
15649
16262
  if (table === "threads") {
15650
16263
  if (method === "POST") {
15651
- const ok2 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
15652
- if (!ok2) return deny("A thread must belong to a channel of the device workspaces.");
16264
+ const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
16265
+ if (!ok3) return deny("A thread must belong to a channel of the device workspaces.");
15653
16266
  }
15654
16267
  if (method === "PATCH") {
15655
16268
  const patch = bodyRows(req.body);
@@ -15828,7 +16441,7 @@ function localAgentCallbacks(chatAgent) {
15828
16441
  regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
15829
16442
  listAgentRuns: () => chatAgent.runningThreadIds(),
15830
16443
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
15831
- listRunningProfiles: () => listProfiles().filter(isServiceRunning),
16444
+ listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
15832
16445
  getAgentIdentities: () => resolveAgentIdentities(),
15833
16446
  publishDocShare: (input2) => publishDocShare(input2),
15834
16447
  listChannelSkills: (args) => listSkills(args.claudeConfigDir, args.cwd),
@@ -15845,14 +16458,14 @@ function localAgentCallbacks(chatAgent) {
15845
16458
  }
15846
16459
  var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
15847
16460
  function buildHostConfigSnapshot() {
15848
- const snapshot2 = {};
16461
+ const snapshot3 = {};
15849
16462
  for (const profile of listProfiles()) {
15850
- snapshot2[profile] = runWithProfile(profile, () => ({
15851
- env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync15(paths.CONFIG_FILE, "utf-8")) : {},
16463
+ snapshot3[profile] = runWithProfile(profile, () => ({
16464
+ env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync16(paths.CONFIG_FILE, "utf-8")) : {},
15852
16465
  agentModel: loadDashboardSettings().agent
15853
16466
  }));
15854
16467
  }
15855
- return snapshot2;
16468
+ return snapshot3;
15856
16469
  }
15857
16470
  function startLocalProfileConfigHydration() {
15858
16471
  setProfileConfigCache(buildHostConfigSnapshot());
@@ -15941,9 +16554,21 @@ async function revalidateDeviceScopes() {
15941
16554
  continue;
15942
16555
  }
15943
16556
  const workspaces = (await listDeviceWorkspaces(device)).map((w) => w.slug);
15944
- for (const scope of hosts.values()) if (scope.deviceId === deviceId) scope.workspaces = workspaces;
16557
+ for (const [ws, scope] of hosts) {
16558
+ if (scope.deviceId !== deviceId) continue;
16559
+ const changed = (scope.workspaces ?? []).join() !== workspaces.join();
16560
+ scope.workspaces = workspaces;
16561
+ if (changed) send(ws, { type: "workspaces", workspaces });
16562
+ }
16563
+ if (!device.lastSeenAt || Date.now() - Date.parse(device.lastSeenAt) > LAST_SEEN_RESOLUTION_MS) {
16564
+ await touchDevice(deviceId);
16565
+ }
15945
16566
  }
15946
16567
  }
16568
+ var LAST_SEEN_RESOLUTION_MS = 60 * 6e4;
16569
+ function minCliVersion() {
16570
+ return process.env["FLYSPACE_MIN_CLI_VERSION"]?.trim() || null;
16571
+ }
15947
16572
  function attachHostSocket(server) {
15948
16573
  const wss = new WebSocketServer({ server, path: HOST_SOCKET_PATH });
15949
16574
  const legacyPublicKey = loadHostPublicKey();
@@ -15984,10 +16609,19 @@ function attachHostSocket(server) {
15984
16609
  ws.close(1008, "bad signature");
15985
16610
  return;
15986
16611
  }
16612
+ const min = minCliVersion();
16613
+ if (min && msg.cliVersion && isOlderVersion(msg.cliVersion, min)) {
16614
+ console.error(`[host-socket] rejecting Host on CLI ${msg.cliVersion} (minimum ${min})`);
16615
+ send(ws, { type: "auth-failed", reason: "cli too old", minCliVersion: min });
16616
+ ws.close(1008, "cli too old");
16617
+ return;
16618
+ }
15987
16619
  supersedeSameIdentity(scope2, ws);
15988
16620
  lastAuthFailure = null;
15989
16621
  addHost(ws, scope2);
15990
- send(ws, { type: "auth-ok" });
16622
+ send(ws, scope2.workspaces ? { type: "auth-ok", workspaces: scope2.workspaces } : { type: "auth-ok" });
16623
+ if (scope2.deviceId) void touchDevice(scope2.deviceId).catch(() => {
16624
+ });
15991
16625
  console.log(`[host-socket] Host authenticated (authPath: ${scope2.authPath})`);
15992
16626
  if (scope2.workspaces) void markAgentConnected(scope2.workspaces);
15993
16627
  })();
@@ -16073,7 +16707,8 @@ function remoteAgentCallbacks() {
16073
16707
  // Host connected, showing nobody as running is a reasonable default
16074
16708
  // rather than a false claim of certainty either way.
16075
16709
  listRunningProfiles: async () => await callHost("listRunningProfiles", []) ?? [],
16076
- getAgentIdentities: async () => await callHost("getAgentIdentities", []) ?? [],
16710
+ // `null` (no Host) falls back to `offlineAgentIdentities` in api.ts.
16711
+ getAgentIdentities: () => callHost("getAgentIdentities", []),
16077
16712
  // Reading the doc and copying its assets can only happen where the
16078
16713
  // channel's real folder is, so this always goes to the Host. `null` (no
16079
16714
  // Host connected) surfaces as `host-unavailable` — see
@@ -16110,7 +16745,7 @@ function remoteAgentCallbacks() {
16110
16745
  }
16111
16746
  var RECONNECT_BASE_MS = 1e3;
16112
16747
  var RECONNECT_MAX_MS = 3e4;
16113
- function connectHostSocket(url, callbacks, onAuthenticated) {
16748
+ function connectHostSocket(url, callbacks, onAuthenticated, onWorkspaces) {
16114
16749
  const credentials = loadCredentials();
16115
16750
  const deviceId = credentials?.deviceId;
16116
16751
  const privateKey = deviceId ? ensureDeviceKey().privateKey : loadHostPrivateKey();
@@ -16142,11 +16777,23 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
16142
16777
  if (!msg) return;
16143
16778
  if (msg.type === "challenge") {
16144
16779
  const signature = signRaw(null, Buffer.from(msg.nonce, "base64"), privateKey).toString("base64");
16145
- send(ws, deviceId ? { type: "auth", signature, deviceId } : { type: "auth", signature });
16780
+ const version = CLI_VERSION ? { cliVersion: CLI_VERSION } : {};
16781
+ send(ws, deviceId ? { type: "auth", signature, deviceId, ...version } : { type: "auth", signature, ...version });
16146
16782
  return;
16147
16783
  }
16148
16784
  if (msg.type === "auth-failed") {
16149
16785
  console.error(`[host-socket] auth rejected: ${msg.reason}`);
16786
+ if (msg.minCliVersion) {
16787
+ console.error(
16788
+ `[host-socket] This Host runs flyspace ${CLI_VERSION} but the Workspace needs ${msg.minCliVersion} or later.
16789
+ Upgrade, then restart the Host: flyspace upgrade`
16790
+ );
16791
+ stopped = true;
16792
+ }
16793
+ return;
16794
+ }
16795
+ if (msg.type === "workspaces") {
16796
+ onWorkspaces?.(msg.workspaces);
16150
16797
  return;
16151
16798
  }
16152
16799
  if (msg.type === "auth-ok") {
@@ -16158,6 +16805,7 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
16158
16805
  send(ws, { type: "host-config", configs: buildHostConfigSnapshot() });
16159
16806
  }, HOST_CONFIG_PUSH_INTERVAL_MS);
16160
16807
  onAuthenticated?.();
16808
+ if (msg.workspaces) onWorkspaces?.(msg.workspaces);
16161
16809
  return;
16162
16810
  }
16163
16811
  if (msg.type === "db-result") {
@@ -16344,7 +16992,7 @@ async function boundWorkspaceIds(publicKey, userId) {
16344
16992
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
16345
16993
  }
16346
16994
  function createDeviceAuthRouter() {
16347
- const app = new Hono28();
16995
+ const app = new Hono29();
16348
16996
  app.post("/code", async (c) => {
16349
16997
  const body = await readBody(c);
16350
16998
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -16407,8 +17055,8 @@ function createDeviceAuthRouter() {
16407
17055
  if (!device || device.revokedAt || !nonce || !signature) return c.json({ error: "invalid_grant" }, 401);
16408
17056
  if (!await consumeChallenge(device.id, nonce)) return c.json({ error: "invalid_grant" }, 401);
16409
17057
  const key = parseEd25519PublicKey(device.publicKey);
16410
- const ok2 = key && verify(null, Buffer.from(nonce), key, Buffer.from(signature, "base64"));
16411
- if (!ok2) return c.json({ error: "invalid_grant" }, 401);
17058
+ const ok3 = key && verify(null, Buffer.from(nonce), key, Buffer.from(signature, "base64"));
17059
+ if (!ok3) return c.json({ error: "invalid_grant" }, 401);
16412
17060
  const { token, expiresInS } = await issueAccessToken(device.id);
16413
17061
  return c.json({ access_token: token, token_type: "Bearer", expires_in: expiresInS });
16414
17062
  });
@@ -16500,6 +17148,7 @@ function createDeviceAuthRouter() {
16500
17148
  if (!decided) {
16501
17149
  return c.json({ error: "That code was already used." }, 409);
16502
17150
  }
17151
+ await releaseStaleHosts(workspaceIds, pending3.publicKey);
16503
17152
  return c.json({ ok: true });
16504
17153
  });
16505
17154
  return app;
@@ -16507,7 +17156,7 @@ function createDeviceAuthRouter() {
16507
17156
 
16508
17157
  // src/dashboard/api.ts
16509
17158
  function createApiRouter(agent = {}, mode = "all") {
16510
- const api = new Hono29();
17159
+ const api = new Hono30();
16511
17160
  api.use("*", async (c, next) => {
16512
17161
  const known = await listKnownWorkspaces();
16513
17162
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -16554,7 +17203,7 @@ function createApiRouter(agent = {}, mode = "all") {
16554
17203
  if (mode === "data") {
16555
17204
  api.get(
16556
17205
  "/settings/agents",
16557
- async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? [] })
17206
+ async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? await offlineAgentIdentities() })
16558
17207
  );
16559
17208
  api.get("/channels/:id/skills", async (c) => {
16560
17209
  const { cwd, claudeConfigDir } = resolveChannelConfig(c.req.param("id"));
@@ -16618,7 +17267,7 @@ function createApiRouter(agent = {}, mode = "all") {
16618
17267
  }
16619
17268
 
16620
17269
  // src/dashboard/app-icon-routes.ts
16621
- import { Hono as Hono30 } from "hono";
17270
+ import { Hono as Hono31 } from "hono";
16622
17271
  var NEUTRAL_MANIFEST = {
16623
17272
  id: "/",
16624
17273
  name: "Flyspace",
@@ -16656,7 +17305,7 @@ async function withResolvedProfile2(c, next) {
16656
17305
  return profile ? runWithProfile(profile, () => next()) : next();
16657
17306
  }
16658
17307
  function createAppIconRouter() {
16659
- const app = new Hono30();
17308
+ const app = new Hono31();
16660
17309
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
16661
17310
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
16662
17311
  const profile = getProfile();
@@ -16696,7 +17345,7 @@ function pngResponse(c, png) {
16696
17345
  }
16697
17346
 
16698
17347
  // src/dashboard/stripe-routes.ts
16699
- import { Hono as Hono31 } from "hono";
17348
+ import { Hono as Hono32 } from "hono";
16700
17349
  function planStatusFor(status) {
16701
17350
  const known = ["trialing", "active", "paused", "canceled"];
16702
17351
  return known.includes(status) ? status : null;
@@ -16739,7 +17388,7 @@ async function handleCardAdded(session) {
16739
17388
  }
16740
17389
  }
16741
17390
  function createStripeWebhookRouter() {
16742
- const stripeRouter = new Hono31();
17391
+ const stripeRouter = new Hono32();
16743
17392
  stripeRouter.post("/webhook", async (c) => {
16744
17393
  const secret = process.env["STRIPE_WEBHOOK_SECRET"];
16745
17394
  const signature = c.req.header("stripe-signature");
@@ -16855,10 +17504,10 @@ var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
16855
17504
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
16856
17505
  for (let port = start; port < start + attempts; port++) {
16857
17506
  const free = await new Promise((resolve8) => {
16858
- const probe2 = createServer();
16859
- probe2.once("error", () => resolve8(false));
16860
- probe2.once("listening", () => probe2.close(() => resolve8(true)));
16861
- probe2.listen(port, host);
17507
+ const probe3 = createServer();
17508
+ probe3.once("error", () => resolve8(false));
17509
+ probe3.once("listening", () => probe3.close(() => resolve8(true)));
17510
+ probe3.listen(port, host);
16862
17511
  });
16863
17512
  if (free) return port;
16864
17513
  }
@@ -16872,7 +17521,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
16872
17521
  const bootCallbacks = new Proxy({}, {
16873
17522
  get: (_t, prop) => lateCallbacks?.[prop]
16874
17523
  });
16875
- const earlyHostSocket = mode === "host" && !hasSupabaseCredentials() && loadCredentials() ? (setSupabaseClient(createHostDbClient()), connectHostSocket(dataSocketUrl(), bootCallbacks, () => onHostConnected?.())) : void 0;
17524
+ const earlyHostSocket = mode === "host" && !hasSupabaseCredentials() && loadCredentials() ? (setSupabaseClient(createHostDbClient()), connectHostSocket(dataSocketUrl(), bootCallbacks, () => onHostConnected?.(), onBindingsChanged)) : void 0;
16876
17525
  if (servesHost) {
16877
17526
  await reconcileAll().catch(
16878
17527
  (err) => console.error("[dashboard] reconcileAll failed:", err.message)
@@ -16899,7 +17548,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
16899
17548
  const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
16900
17549
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
16901
17550
  };
16902
- const app = new Hono32();
17551
+ const app = new Hono33();
16903
17552
  app.use("*", async (c, next) => {
16904
17553
  await next();
16905
17554
  if (c.req.path.startsWith("/assets/")) {
@@ -16947,7 +17596,7 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
16947
17596
  if (mode === "host") relayToken = publishHostEndpoint(port);
16948
17597
  if (mode === "data") attachHostSocket(server);
16949
17598
  const stopHostStatusBroadcast = mode === "data" ? startHostStatusBroadcast() : void 0;
16950
- const hostSocketClient = earlyHostSocket ?? (mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent), () => onHostConnected?.()) : void 0);
17599
+ const hostSocketClient = earlyHostSocket ?? (mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent), () => onHostConnected?.(), onBindingsChanged) : void 0);
16951
17600
  const stopPushWatcher = earlyHostSocket ? () => {
16952
17601
  } : startPushWatcher();
16953
17602
  const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
@@ -16968,6 +17617,13 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
16968
17617
  }
16969
17618
  };
16970
17619
  }
17620
+ function onBindingsChanged(workspaces) {
17621
+ const creds = loadCredentials();
17622
+ if (!creds || (creds.workspaces ?? []).join() === workspaces.join()) return;
17623
+ saveCredentials({ ...creds, workspaces });
17624
+ console.log(`[dashboard] now serving: ${workspaces.join(", ") || "(none)"}`);
17625
+ void reconcileAll().then(() => ensureServices()).catch((err) => console.error("[dashboard] applying new bindings failed:", err.message));
17626
+ }
16971
17627
  var SERVICE_CHECK_MS = 6e4;
16972
17628
  function watchServices() {
16973
17629
  const tick = () => void ensureServices().then((slugs) => slugs.length && console.log(`[dashboard] installed scheduler service for: ${slugs.join(", ")}`)).catch((err) => console.error("[dashboard] ensureServices failed:", err.message));
@@ -17177,7 +17833,7 @@ Done. ${totalThreads} threads, ${totalReplies} messages upserted.`);
17177
17833
  }
17178
17834
 
17179
17835
  // src/commands/slack.ts
17180
- var USAGE2 = `
17836
+ var USAGE3 = `
17181
17837
  Usage: flyspace slack <subcommand> [options]
17182
17838
 
17183
17839
  Subcommands:
@@ -17362,7 +18018,7 @@ async function threadReplies(client2, nameOrId, ts2) {
17362
18018
  async function postMessage(client2, args) {
17363
18019
  const channelArg = getArg2(args, "--channel");
17364
18020
  const textFilePath = getArg2(args, "--text-file");
17365
- const text = textFilePath ? readFileSync16(textFilePath, "utf8") : getArg2(args, "--text");
18021
+ const text = textFilePath ? readFileSync17(textFilePath, "utf8") : getArg2(args, "--text");
17366
18022
  const threadTs = getArg2(args, "--thread");
17367
18023
  const markdown = args.includes("--markdown");
17368
18024
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -17517,7 +18173,7 @@ async function ensureClaudeCodeReady() {
17517
18173
  }
17518
18174
  function getManifestYaml(botName) {
17519
18175
  const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
17520
- const raw = readFileSync16(manifestPath, "utf-8");
18176
+ const raw = readFileSync17(manifestPath, "utf-8");
17521
18177
  return raw.replace(/name: Custie/g, `name: ${botName}`);
17522
18178
  }
17523
18179
  async function stepPersonalise() {
@@ -17730,7 +18386,7 @@ async function stepBrowserTokens(botName) {
17730
18386
  })
17731
18387
  ));
17732
18388
  const DEBUG = process.argv.includes("--debug");
17733
- const USER_DATA_DIR = resolve7(tmpdir3(), "custie-playwright-profile");
18389
+ const USER_DATA_DIR = resolve7(tmpdir4(), "custie-playwright-profile");
17734
18390
  let context;
17735
18391
  try {
17736
18392
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -17951,7 +18607,7 @@ function copyDefaultPrompt(botName) {
17951
18607
  if (!existsSync27(paths.PROMPT_FILE)) {
17952
18608
  const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
17953
18609
  if (existsSync27(defaultPrompt)) {
17954
- const content = readFileSync16(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18610
+ const content = readFileSync17(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
17955
18611
  writeFileSync10(paths.PROMPT_FILE, content);
17956
18612
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
17957
18613
  }
@@ -18112,13 +18768,13 @@ async function runSlackCmd(args) {
18112
18768
  break;
18113
18769
  }
18114
18770
  default:
18115
- console.log(USAGE2);
18771
+ console.log(USAGE3);
18116
18772
  break;
18117
18773
  }
18118
18774
  }
18119
18775
 
18120
18776
  // src/commands/chat.ts
18121
- var USAGE3 = `
18777
+ var USAGE4 = `
18122
18778
  Usage: flyspace chat <subcommand> [options]
18123
18779
 
18124
18780
  Subcommands:
@@ -18264,17 +18920,131 @@ async function runChatCmd(args) {
18264
18920
  case "-h":
18265
18921
  case "--help":
18266
18922
  case void 0:
18267
- console.log(USAGE3);
18923
+ console.log(USAGE4);
18268
18924
  break;
18269
18925
  default:
18270
18926
  console.error(`Unknown subcommand: ${subcommand}`);
18271
- console.log(USAGE3);
18927
+ console.log(USAGE4);
18928
+ process.exit(1);
18929
+ }
18930
+ }
18931
+
18932
+ // src/commands/memory.ts
18933
+ import { readFileSync as readFileSync18 } from "fs";
18934
+ var USAGE5 = `
18935
+ Usage: flyspace memory <subcommand> --channel <name-or-id> [options]
18936
+
18937
+ A channel's persistent agent memory (docs/features/workspace-memory.md).
18938
+
18939
+ Subcommands:
18940
+ list Every memory in the channel (index form)
18941
+ get <name> One memory in full
18942
+ search <words...> Memories whose name, description or body match
18943
+ save --name <slug> --type <type> Create, or overwrite the memory with that name
18944
+ --description <one line>
18945
+ --body <text | -> "-" reads the body from stdin
18946
+ [--thread <id>] The thread the memory came from
18947
+ delete <name-or-id> [--thread <id>] Delete one memory
18948
+
18949
+ Types: ${MEMORY_TYPES.join(", ")}
18950
+ `;
18951
+ function getArg4(args, flag) {
18952
+ const idx = args.indexOf(flag);
18953
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
18954
+ }
18955
+ function positionals(args) {
18956
+ const out = [];
18957
+ for (let i = 0; i < args.length; i++) {
18958
+ if (args[i].startsWith("--")) i++;
18959
+ else out.push(args[i]);
18960
+ }
18961
+ return out;
18962
+ }
18963
+ function indexLine(m) {
18964
+ return `${m.name} (${m.type}, ${m.updatedAt.slice(0, 10)}) ${m.description}`;
18965
+ }
18966
+ async function runMemoryCmd(args) {
18967
+ loadEnvFiles();
18968
+ const subcommand = args[0];
18969
+ const rest = args.slice(1);
18970
+ if (!subcommand || subcommand === "-h" || subcommand === "--help") {
18971
+ console.log(USAGE5);
18972
+ return;
18973
+ }
18974
+ const channelArg = getArg4(rest, "--channel");
18975
+ if (!channelArg) throw new Error("--channel <name-or-id> is required");
18976
+ const channel = await resolveChannel(channelArg, getArg4(rest, "--profile-name"));
18977
+ const words = positionals(rest);
18978
+ switch (subcommand) {
18979
+ case "list": {
18980
+ const memories = await listChannelMemories(channel.id);
18981
+ console.log(memories.length ? memories.map(indexLine).join("\n") : "No memories yet.");
18982
+ break;
18983
+ }
18984
+ case "get": {
18985
+ const name = words[0];
18986
+ if (!name) throw new Error("Usage: flyspace memory get --channel <c> <name>");
18987
+ const memory = await getChannelMemory(channel.id, name);
18988
+ if (!memory) throw new Error(`No memory named ${name} in ${channel.name}.`);
18989
+ console.log(`# ${memory.name} (${memory.type})
18990
+
18991
+ ${memory.description}
18992
+
18993
+ ${memory.body}`);
18994
+ break;
18995
+ }
18996
+ case "search": {
18997
+ if (!words.length) throw new Error("Usage: flyspace memory search --channel <c> <words...>");
18998
+ const hits = await searchChannelMemories(channel.id, words.join(" "));
18999
+ console.log(hits.length ? hits.map(indexLine).join("\n") : "No matching memories.");
19000
+ break;
19001
+ }
19002
+ case "save": {
19003
+ const name = getArg4(rest, "--name");
19004
+ const type = getArg4(rest, "--type");
19005
+ const description = getArg4(rest, "--description");
19006
+ const bodyArg = getArg4(rest, "--body");
19007
+ if (!name || !type || !description || bodyArg === void 0) {
19008
+ throw new Error(
19009
+ "Usage: flyspace memory save --channel <c> --name <slug> --type <type> --description <one line> --body <text | -> [--thread <id>]"
19010
+ );
19011
+ }
19012
+ if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
19013
+ const body = bodyArg === "-" ? readFileSync18(0, "utf-8") : bodyArg;
19014
+ const thread = getArg4(rest, "--thread");
19015
+ const { created, changed } = await saveChannelMemory(
19016
+ {
19017
+ channelId: channel.id,
19018
+ name,
19019
+ type,
19020
+ description,
19021
+ body,
19022
+ createdBy: "agent",
19023
+ ...thread ? { sourceThreadId: thread } : {}
19024
+ },
19025
+ "agent"
19026
+ );
19027
+ const verb = created ? "Saved" : changed ? "Updated" : "Unchanged:";
19028
+ console.log(`${verb} memory ${name} in ${channel.name}.`);
19029
+ break;
19030
+ }
19031
+ case "delete": {
19032
+ const ref = words[0];
19033
+ if (!ref) throw new Error("Usage: flyspace memory delete --channel <c> <name-or-id>");
19034
+ const deleted = await deleteChannelMemory(channel.id, ref, "agent", getArg4(rest, "--thread"));
19035
+ if (!deleted) throw new Error(`No memory ${ref} in ${channel.name}.`);
19036
+ console.log(`Deleted memory ${ref}.`);
19037
+ break;
19038
+ }
19039
+ default:
19040
+ console.error(`Unknown subcommand: ${subcommand}`);
19041
+ console.log(USAGE5);
18272
19042
  process.exit(1);
18273
19043
  }
18274
19044
  }
18275
19045
 
18276
19046
  // src/commands/mcp.ts
18277
- var USAGE4 = `
19047
+ var USAGE6 = `
18278
19048
  Usage: flyspace mcp <server>
18279
19049
 
18280
19050
  Run one of Flyspace's own MCP servers on stdio. Spawned by the agent runtime
@@ -18291,7 +19061,7 @@ Usage: flyspace mcp <server>
18291
19061
  async function runMcpCmd(args) {
18292
19062
  const server = args[0];
18293
19063
  if (!server || server === "-h" || server === "--help") {
18294
- console.log(USAGE4.trim());
19064
+ console.log(USAGE6.trim());
18295
19065
  return;
18296
19066
  }
18297
19067
  restoreStdout();
@@ -18304,7 +19074,7 @@ async function runMcpCmd(args) {
18304
19074
  }
18305
19075
  default:
18306
19076
  console.error(`Unknown MCP server: ${server}`);
18307
- console.error(USAGE4.trim());
19077
+ console.error(USAGE6.trim());
18308
19078
  process.exit(1);
18309
19079
  }
18310
19080
  }
@@ -18380,7 +19150,7 @@ async function runDashboard(args) {
18380
19150
 
18381
19151
  // src/commands/workspace.ts
18382
19152
  import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
18383
- var USAGE5 = `
19153
+ var USAGE7 = `
18384
19154
  Usage: flyspace workspace <subcommand> [options]
18385
19155
 
18386
19156
  Subcommands:
@@ -18397,7 +19167,7 @@ var USAGE5 = `
18397
19167
  --no-invite Never send an invite email. Fails if the admin
18398
19168
  has no existing account.
18399
19169
  `;
18400
- function getArg4(args, flag) {
19170
+ function getArg5(args, flag) {
18401
19171
  const idx = args.indexOf(flag);
18402
19172
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
18403
19173
  }
@@ -18459,7 +19229,7 @@ async function bindThisHost(workspaceId) {
18459
19229
  return;
18460
19230
  }
18461
19231
  await bindDeviceWorkspace(device.id, workspaceId, device.userId);
18462
- console.log(` bound to Host "${device.name}" (restart it to pick up the new workspace)`);
19232
+ console.log(` bound to Host "${device.name}" (a running Host picks it up within a minute)`);
18463
19233
  }
18464
19234
  function provisionLocal(botName) {
18465
19235
  ensureDirs();
@@ -18493,10 +19263,10 @@ async function provisionChannels(specs) {
18493
19263
  async function handleCreate(args) {
18494
19264
  const slug = args[0];
18495
19265
  if (!slug || slug.startsWith("-")) fail2("Usage: flyspace workspace create <slug> --admin-email <email>");
18496
- const adminEmail = getArg4(args, "--admin-email");
19266
+ const adminEmail = getArg5(args, "--admin-email");
18497
19267
  if (!adminEmail?.includes("@")) fail2("--admin-email is required and must be an email address");
18498
- const botName = getArg4(args, "--bot-name") ?? slug;
18499
- const baseDomain = getArg4(args, "--base-domain") ?? "flyspace.ai";
19268
+ const botName = getArg5(args, "--bot-name") ?? slug;
19269
+ const baseDomain = getArg5(args, "--base-domain") ?? "flyspace.ai";
18500
19270
  const allowInvite = !args.includes("--no-invite");
18501
19271
  const channels = getAllArgs(args, "--channel").map(parseChannelSpec);
18502
19272
  loadEnvFiles();
@@ -18519,13 +19289,13 @@ async function runWorkspaceCmd(args) {
18519
19289
  await handleCreate(args.slice(1));
18520
19290
  break;
18521
19291
  default:
18522
- console.log(USAGE5);
19292
+ console.log(USAGE7);
18523
19293
  process.exit(args[0] ? 1 : 0);
18524
19294
  }
18525
19295
  }
18526
19296
 
18527
19297
  // src/commands/reconcile.ts
18528
- var USAGE6 = `
19298
+ var USAGE8 = `
18529
19299
  Usage: flyspace reconcile [<workspace> | --all]
18530
19300
 
18531
19301
  Repair a workspace's local filesystem state (profile dir, config.env,
@@ -18545,7 +19315,7 @@ function printLog(name, log5) {
18545
19315
  }
18546
19316
  async function runReconcileCmd(args) {
18547
19317
  if (args.includes("-h") || args.includes("--help")) {
18548
- console.log(USAGE6);
19318
+ console.log(USAGE8);
18549
19319
  return;
18550
19320
  }
18551
19321
  loadEnvFiles();
@@ -18561,12 +19331,13 @@ async function runReconcileCmd(args) {
18561
19331
 
18562
19332
  // src/cli.ts
18563
19333
  var XERO_USAGE = false ? " xero Connect to Xero and expose it as an MCP server\n" : "";
18564
- var USAGE7 = `
19334
+ var USAGE9 = `
18565
19335
  Usage: flyspace <command> [options]
18566
19336
 
18567
19337
  Commands:
18568
19338
  setup Set up this machine as a Host (login, agent CLIs, ~/Flyspace)
18569
19339
  auth Log in, check status, or log out this device
19340
+ doctor Check this Host end to end (login, agent CLIs, services)
18570
19341
  start Start the agent host (Slack only if configured)
18571
19342
  install Install as a system service (launchd / systemd)
18572
19343
  uninstall Remove the system service
@@ -18580,7 +19351,8 @@ var USAGE7 = `
18580
19351
  usage Show agent token usage and cost for this workspace
18581
19352
  slack Query Slack (channels, users, post messages)
18582
19353
  chat Inspect dashboard chat channels and move threads between them
18583
- profiles List instances and their service status
19354
+ memory Read and write a channel's persistent agent memory
19355
+ profiles List local profiles (bound here? scheduler?) or remove one
18584
19356
  workspace Provision a workspace (Supabase rows + local profile dirs)
18585
19357
  reconcile Repair a workspace's local files against Supabase
18586
19358
  ${XERO_USAGE} dashboard Start the read-only management dashboard (web UI)
@@ -18620,6 +19392,9 @@ async function main() {
18620
19392
  case "auth":
18621
19393
  await runAuthCmd(args.slice(1));
18622
19394
  break;
19395
+ case "doctor":
19396
+ await runDoctor(args.slice(1));
19397
+ break;
18623
19398
  case "start":
18624
19399
  await runStart();
18625
19400
  break;
@@ -18668,8 +19443,12 @@ async function main() {
18668
19443
  case "chat":
18669
19444
  await runChatCmd(args.slice(1));
18670
19445
  break;
19446
+ case "memory":
19447
+ await runMemoryCmd(args.slice(1));
19448
+ break;
19449
+ case "profile":
18671
19450
  case "profiles":
18672
- await runProfiles();
19451
+ await runProfiles(args.slice(1));
18673
19452
  break;
18674
19453
  case "dashboard":
18675
19454
  await runDashboard(args.slice(1));
@@ -18679,12 +19458,12 @@ async function main() {
18679
19458
  break;
18680
19459
  case "-v":
18681
19460
  case "--version":
18682
- printRaw("0.1.6");
19461
+ printRaw("0.1.9");
18683
19462
  break;
18684
19463
  case "-h":
18685
19464
  case "--help":
18686
19465
  case void 0:
18687
- printRaw(USAGE7);
19466
+ printRaw(USAGE9);
18688
19467
  break;
18689
19468
  case "xero":
18690
19469
  if (false) {
@@ -18695,7 +19474,7 @@ async function main() {
18695
19474
  // falls through
18696
19475
  default:
18697
19476
  printRaw(`Unknown command: ${command}`);
18698
- printRaw(USAGE7);
19477
+ printRaw(USAGE9);
18699
19478
  process.exit(1);
18700
19479
  }
18701
19480
  }