flyspace-cli 0.1.7 → 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,
@@ -437,7 +439,7 @@ import { confirm as confirm2 } from "@inquirer/prompts";
437
439
 
438
440
  // src/service.ts
439
441
  import { execSync } from "child_process";
440
- import { existsSync as existsSync3 } from "fs";
442
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
441
443
  import { join as join2 } from "path";
442
444
  import { homedir, platform } from "os";
443
445
  function serviceLabel(profile) {
@@ -466,6 +468,22 @@ function isServiceInstalled(profile) {
466
468
  if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
467
469
  return false;
468
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
+ }
469
487
  function restartService(profile) {
470
488
  const os = platform();
471
489
  if (os === "darwin") {
@@ -497,10 +515,7 @@ function hostPlistPath() {
497
515
  return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
498
516
  }
499
517
  function isHostServiceInstalled() {
500
- if (platform() === "darwin") return existsSync3(hostPlistPath());
501
- if (platform() === "linux") {
502
- return existsSync3(join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`));
503
- }
518
+ if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
504
519
  return false;
505
520
  }
506
521
  function restartHostService() {
@@ -518,9 +533,29 @@ function restartHostService() {
518
533
  }
519
534
  return false;
520
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
+ }
521
556
 
522
557
  // src/commands/install.ts
523
- 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";
524
559
  import { execSync as execSync2 } from "child_process";
525
560
  import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
526
561
  import { homedir as homedir2, platform as platform2 } from "os";
@@ -548,6 +583,11 @@ function detectClaudeBinDir() {
548
583
  return void 0;
549
584
  }
550
585
  }
586
+ function nodeShimDirs() {
587
+ return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
588
+ (d) => existsSync4(d)
589
+ );
590
+ }
551
591
  function detectCustieBin() {
552
592
  const argv1 = process.argv[1];
553
593
  if (argv1) {
@@ -562,7 +602,7 @@ function detectCustieBin() {
562
602
  function readEnvFile() {
563
603
  if (!existsSync4(paths.CONFIG_FILE)) return {};
564
604
  const vars = {};
565
- 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")) {
566
606
  const trimmed = line.trim();
567
607
  if (!trimmed || trimmed.startsWith("#")) continue;
568
608
  const eq = trimmed.indexOf("=");
@@ -610,12 +650,10 @@ async function installMacOS(spec) {
610
650
  mkdirSync2(logDir, { recursive: true });
611
651
  const envVars = spec.env;
612
652
  const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
613
- const asdfShims = join3(homedir2(), ".asdf", "shims");
614
- const asdfBin = existsSync4(asdfShims) ? asdfShims : void 0;
615
653
  const pathParts = [
616
654
  dirname2(nodePath),
617
655
  claudeBinDir,
618
- asdfBin,
656
+ ...nodeShimDirs(),
619
657
  homebrewBin,
620
658
  "/usr/local/bin",
621
659
  "/usr/bin",
@@ -684,6 +722,7 @@ async function installLinux(spec) {
684
722
  const pathParts = [
685
723
  dirname2(nodePath),
686
724
  claudeBinDir,
725
+ ...nodeShimDirs(),
687
726
  linuxbrewBin,
688
727
  "/usr/local/bin",
689
728
  "/usr/bin",
@@ -757,7 +796,7 @@ function listProfiles() {
757
796
  }
758
797
  return profiles;
759
798
  }
760
- function isServiceRunning(profile) {
799
+ function isServiceRunning2(profile) {
761
800
  try {
762
801
  if (platform3() === "darwin") {
763
802
  const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
@@ -774,7 +813,7 @@ function isServiceRunning(profile) {
774
813
 
775
814
  // src/auth/device-auth.ts
776
815
  import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
777
- 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";
778
817
  import { dirname as dirname3, join as join5 } from "path";
779
818
  var DEFAULT_API_URL = "https://app.flyspace.ai";
780
819
  var EXPIRY_SKEW_MS = 6e4;
@@ -802,7 +841,7 @@ function ensureDeviceKey(path = deviceKeyPath()) {
802
841
  writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
803
842
  created = true;
804
843
  }
805
- const privateKey = createPrivateKey(readFileSync4(path, "utf8"));
844
+ const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
806
845
  const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
807
846
  "base64"
808
847
  );
@@ -811,7 +850,7 @@ function ensureDeviceKey(path = deviceKeyPath()) {
811
850
  function loadCredentials(path = credentialsPath()) {
812
851
  if (!existsSync6(path)) return null;
813
852
  try {
814
- return JSON.parse(readFileSync4(path, "utf8"));
853
+ return JSON.parse(readFileSync5(path, "utf8"));
815
854
  } catch {
816
855
  return null;
817
856
  }
@@ -957,7 +996,7 @@ import {
957
996
  lstatSync,
958
997
  mkdirSync as mkdirSync4,
959
998
  readdirSync as readdirSync2,
960
- readFileSync as readFileSync5,
999
+ readFileSync as readFileSync6,
961
1000
  statSync as statSync2,
962
1001
  symlinkSync
963
1002
  } from "fs";
@@ -986,7 +1025,7 @@ function claudeLoggedIn() {
986
1025
  if (process.env["ANTHROPIC_API_KEY"]) return true;
987
1026
  if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
988
1027
  try {
989
- const raw = JSON.parse(readFileSync5(join6(homedir3(), ".claude.json"), "utf-8"));
1028
+ const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
990
1029
  if (raw.oauthAccount) return true;
991
1030
  } catch {
992
1031
  }
@@ -1027,7 +1066,7 @@ function jsonlFiles(dir, depth) {
1027
1066
  function cwdFromTranscript(file) {
1028
1067
  let head;
1029
1068
  try {
1030
- 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);
1031
1070
  } catch {
1032
1071
  return null;
1033
1072
  }
@@ -1044,7 +1083,7 @@ function cwdFromTranscript(file) {
1044
1083
  }
1045
1084
  function codexTrustedPaths() {
1046
1085
  try {
1047
- const toml = readFileSync5(join6(codexDir(), "config.toml"), "utf-8");
1086
+ const toml = readFileSync6(join6(codexDir(), "config.toml"), "utf-8");
1048
1087
  return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
1049
1088
  } catch {
1050
1089
  return [];
@@ -1514,7 +1553,7 @@ async function runAuthCmd(args) {
1514
1553
  }
1515
1554
 
1516
1555
  // src/version.ts
1517
- var CLI_VERSION = true ? "0.1.7" : null;
1556
+ var CLI_VERSION = true ? "0.1.9" : null;
1518
1557
  function isOlderVersion(a, b) {
1519
1558
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1520
1559
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -1559,15 +1598,16 @@ async function servedWorkspaces() {
1559
1598
  return bound.workspaces;
1560
1599
  }
1561
1600
  function checkLatestVersion() {
1562
- if (!CLI_VERSION) return;
1601
+ if (!CLI_VERSION) return true;
1563
1602
  let latest = "";
1564
1603
  try {
1565
1604
  latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
1566
1605
  } catch {
1567
- return;
1606
+ return true;
1568
1607
  }
1569
- if (!latest || !isOlderVersion(CLI_VERSION, latest)) return;
1608
+ if (!latest || !isOlderVersion(CLI_VERSION, latest)) return true;
1570
1609
  bad(`flyspace ${CLI_VERSION} is out of date (latest ${latest}) ${DIM2}run: flyspace upgrade${RESET2}`);
1610
+ return false;
1571
1611
  }
1572
1612
  function loginArgs(args) {
1573
1613
  const out = [];
@@ -1633,7 +1673,12 @@ ${BOLD2}Flyspace Setup${RESET2}
1633
1673
  }
1634
1674
  let hostRunning = false;
1635
1675
  if (args.includes("--start-host") && workspaces.length) {
1636
- 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())) {
1637
1682
  hostRunning = await installHostService();
1638
1683
  if (hostRunning) ok(`Host running in the background ${DIM2}logs: ${tilde2(paths.LOG_DIR)}/host.log${RESET2}`);
1639
1684
  } else {
@@ -1657,6 +1702,134 @@ ${BOLD2}Flyspace Setup${RESET2}
1657
1702
  if (!workspaces.length) process.exitCode = 1;
1658
1703
  }
1659
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
+
1660
1833
  // src/commands/uninstall.ts
1661
1834
  import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
1662
1835
  import { execSync as execSync5 } from "child_process";
@@ -1722,7 +1895,6 @@ async function runUninstall() {
1722
1895
 
1723
1896
  // src/commands/profiles-cmd.ts
1724
1897
  import { rmSync as rmSync3 } from "fs";
1725
- var OPERATOR_NOTE = "This machine is an operator Host (it has Supabase credentials), so it serves every workspace, bound or not.";
1726
1898
  var USAGE = `
1727
1899
  Usage: flyspace profiles [list]
1728
1900
  flyspace profiles remove <name>
@@ -1743,7 +1915,7 @@ async function listCmd() {
1743
1915
  console.log("\n Profiles:\n");
1744
1916
  console.log(` ${"NAME".padEnd(24)} ${"BOUND HERE".padEnd(12)} SCHEDULER`);
1745
1917
  for (const profile of listProfiles()) {
1746
- const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" : isServiceRunning(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
1918
+ const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" : isServiceRunning2(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
1747
1919
  const here = bound === null ? "-".padEnd(12) : bound.includes(profile) ? "yes".padEnd(12) : `\x1B[33m${"no".padEnd(12)}\x1B[0m`;
1748
1920
  console.log(` ${profile.padEnd(24)} ${here} ${scheduler}`);
1749
1921
  }
@@ -1752,10 +1924,7 @@ async function listCmd() {
1752
1924
  if (bound?.length === 0) {
1753
1925
  console.log("\n This Host is not bound to any workspace, so no scheduler runs here. Run: flyspace setup");
1754
1926
  }
1755
- if (hasSupabaseCredentials()) {
1756
- console.log(`
1757
- ${OPERATOR_NOTE}`);
1758
- } else if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
1927
+ if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
1759
1928
  console.log("\n Profiles not bound here run nothing on this machine. Remove one with: flyspace profiles remove <name>");
1760
1929
  }
1761
1930
  console.log("");
@@ -1770,10 +1939,6 @@ async function removeCmd(name) {
1770
1939
  console.error(` No local profile named "${name}".`);
1771
1940
  process.exit(1);
1772
1941
  }
1773
- if (hasSupabaseCredentials()) {
1774
- console.error(` ${OPERATOR_NOTE} Removing a profile would stop that workspace's schedules.`);
1775
- process.exit(1);
1776
- }
1777
1942
  const bound = await boundHere();
1778
1943
  if (bound === null) {
1779
1944
  console.error(" This machine has no device login, so it serves every workspace; removing a profile would be undone.");
@@ -1803,6 +1968,7 @@ async function runProfiles(args = []) {
1803
1968
 
1804
1969
  // src/commands/upgrade.ts
1805
1970
  import { execSync as execSync6 } from "child_process";
1971
+ import { realpathSync } from "fs";
1806
1972
  function restartInstalledProfiles() {
1807
1973
  if (isHostServiceInstalled()) {
1808
1974
  console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
@@ -1822,8 +1988,18 @@ Restarting ${installed.length} instance(s)...`);
1822
1988
  }
1823
1989
  }
1824
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
+ }
1825
2001
  async function runUpgrade() {
1826
- const current = "0.1.7";
2002
+ const current = "0.1.9";
1827
2003
  console.log(`Current version: ${current}`);
1828
2004
  console.log("Checking for updates...\n");
1829
2005
  try {
@@ -1834,7 +2010,7 @@ async function runUpgrade() {
1834
2010
  }
1835
2011
  console.log(`New version available: ${latest}`);
1836
2012
  console.log("Upgrading...\n");
1837
- execSync6(`npm install -g ${"flyspace-cli"}@latest`, { stdio: "inherit" });
2013
+ execSync6(npmInstallCommand(), { stdio: "inherit" });
1838
2014
  console.log(`
1839
2015
  Upgraded from ${current} to ${latest}.`);
1840
2016
  restartInstalledProfiles();
@@ -1843,7 +2019,7 @@ Upgraded from ${current} to ${latest}.`);
1843
2019
  } catch (err) {
1844
2020
  console.error("Upgrade failed:", err.message);
1845
2021
  console.error(`
1846
- You can upgrade manually with: npm install -g ${"flyspace-cli"}@latest`);
2022
+ You can upgrade manually with: ${npmInstallCommand()}`);
1847
2023
  process.exit(1);
1848
2024
  }
1849
2025
  }
@@ -2195,7 +2371,7 @@ async function runAutomationCmd(args) {
2195
2371
 
2196
2372
  // src/commands/usage.ts
2197
2373
  import { existsSync as existsSync9 } from "fs";
2198
- var HELP3 = `
2374
+ var HELP4 = `
2199
2375
  Usage: flyspace usage [options]
2200
2376
 
2201
2377
  Show agent token usage and notional cost for this workspace.
@@ -2317,7 +2493,7 @@ function printExplanation(explained) {
2317
2493
  async function runUsage(args) {
2318
2494
  const parsed = parseArgs(args);
2319
2495
  if (parsed === "help") {
2320
- console.log(HELP3);
2496
+ console.log(HELP4);
2321
2497
  return;
2322
2498
  }
2323
2499
  const { days, by, explain, json } = parsed;
@@ -2463,9 +2639,9 @@ async function runLogs(args) {
2463
2639
 
2464
2640
  // src/commands/slack.ts
2465
2641
  import { execFileSync as execFileSync4 } from "child_process";
2466
- 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";
2467
2643
  import { resolve as resolve7 } from "path";
2468
- import { tmpdir as tmpdir3, homedir as homedir9 } from "os";
2644
+ import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
2469
2645
  import { input, confirm as confirm3, select, checkbox } from "@inquirer/prompts";
2470
2646
  import { WebClient as WebClient3 } from "@slack/web-api";
2471
2647
 
@@ -3994,7 +4170,7 @@ async function deliverPasswordResetLink(email, redirectTo) {
3994
4170
  import { existsSync as existsSync26 } from "fs";
3995
4171
  import { createServer } from "net";
3996
4172
  import { join as join22 } from "path";
3997
- import { Hono as Hono32 } from "hono";
4173
+ import { Hono as Hono33 } from "hono";
3998
4174
  import { serve } from "@hono/node-server";
3999
4175
  import { serveStatic } from "@hono/node-server/serve-static";
4000
4176
 
@@ -4003,6 +4179,10 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as w
4003
4179
  async function boundWorkspaces2() {
4004
4180
  return (await boundWorkspaces())?.workspaces ?? null;
4005
4181
  }
4182
+ async function servedWorkspaces2() {
4183
+ const bound = await boundWorkspaces2();
4184
+ return new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4185
+ }
4006
4186
  async function reconcileProfile(profile) {
4007
4187
  return runWithProfile(profile, async () => {
4008
4188
  const log5 = [];
@@ -4028,8 +4208,7 @@ async function reconcileProfile(profile) {
4028
4208
  });
4029
4209
  }
4030
4210
  async function reconcileAll() {
4031
- const bound = hasSupabaseCredentials() ? null : await boundWorkspaces2();
4032
- const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4211
+ const slugs = await servedWorkspaces2();
4033
4212
  const results = /* @__PURE__ */ new Map();
4034
4213
  for (const slug of slugs) {
4035
4214
  try {
@@ -4042,8 +4221,7 @@ async function reconcileAll() {
4042
4221
  return results;
4043
4222
  }
4044
4223
  async function ensureServices() {
4045
- const bound = await boundWorkspaces2();
4046
- const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4224
+ const slugs = await servedWorkspaces2();
4047
4225
  slugs.delete(DEFAULT_PROFILE);
4048
4226
  const installed = [];
4049
4227
  for (const slug of slugs) {
@@ -4192,10 +4370,10 @@ var requireDocsAccess = async (c, next) => {
4192
4370
  };
4193
4371
 
4194
4372
  // src/dashboard/api.ts
4195
- import { Hono as Hono29 } from "hono";
4373
+ import { Hono as Hono30 } from "hono";
4196
4374
 
4197
4375
  // src/dashboard/public-share-routes.ts
4198
- 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";
4199
4377
  import { join as join12 } from "path";
4200
4378
  import { Hono as Hono3 } from "hono";
4201
4379
  import { GetObjectCommand } from "@aws-sdk/client-s3";
@@ -4336,15 +4514,15 @@ async function createDocShare(input2) {
4336
4514
  if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
4337
4515
  return rowToShare(data);
4338
4516
  }
4339
- async function updateDocShareSnapshot(id, snapshot2, features) {
4517
+ async function updateDocShareSnapshot(id, snapshot3, features) {
4340
4518
  const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
4341
- title: snapshot2.title,
4342
- content: snapshot2.content,
4343
- asset_keys: snapshot2.assetKeys,
4519
+ title: snapshot3.title,
4520
+ content: snapshot3.content,
4521
+ asset_keys: snapshot3.assetKeys,
4344
4522
  // Re-detected with the snapshot: a re-publish can carry a rewritten
4345
4523
  // article, and one written in another language than last time is exactly
4346
4524
  // the case a stale value would get wrong.
4347
- source_lang: snapshot2.sourceLang,
4525
+ source_lang: snapshot3.sourceLang,
4348
4526
  audio_enabled: features.audioEnabled,
4349
4527
  ask_enabled: features.askEnabled,
4350
4528
  social_enabled: features.socialEnabled,
@@ -4661,7 +4839,7 @@ function injectShareHead(html, head) {
4661
4839
 
4662
4840
  // src/dashboard/app-icon.ts
4663
4841
  import { createHash as createHash2 } from "crypto";
4664
- import { readFileSync as readFileSync6 } from "fs";
4842
+ import { readFileSync as readFileSync7 } from "fs";
4665
4843
  import { join as join11 } from "path";
4666
4844
  import sharp from "sharp";
4667
4845
  var PLATE = "#0A0A0F";
@@ -4689,7 +4867,7 @@ function roundedPlate(size, fill = PLATE) {
4689
4867
  }
4690
4868
  async function renderBadge(box) {
4691
4869
  const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
4692
- 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();
4693
4871
  return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
4694
4872
  }
4695
4873
  async function sampleBackground(logo) {
@@ -4744,7 +4922,7 @@ async function renderAppIcon({
4744
4922
  if (hit) return hit;
4745
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,
4746
4924
  // either on its usual dark plate or, for a favicon, trimmed and bare.
4747
- (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());
4748
4926
  cache3.set(key, out);
4749
4927
  return out;
4750
4928
  }
@@ -5962,7 +6140,7 @@ var shellCache = null;
5962
6140
  function readShell(path) {
5963
6141
  const { mtimeMs } = statSync3(path);
5964
6142
  if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
5965
- const html = readFileSync7(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6143
+ const html = readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
5966
6144
  shellCache = { mtimeMs, html };
5967
6145
  return html;
5968
6146
  }
@@ -5987,12 +6165,12 @@ function serveBuildFile(c, webDist, dir, name) {
5987
6165
  c.header("Content-Type", type);
5988
6166
  c.header("Cache-Control", "public, max-age=31536000, immutable");
5989
6167
  if (ext === ".css") {
5990
- 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/`));
5991
6169
  }
5992
6170
  if (ext === ".js") {
5993
- return c.body(readFileSync7(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6171
+ return c.body(readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
5994
6172
  }
5995
- return c.body(new Uint8Array(readFileSync7(path)));
6173
+ return c.body(new Uint8Array(readFileSync8(path)));
5996
6174
  }
5997
6175
  var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
5998
6176
  function servePublishedHtml(c, share) {
@@ -6754,8 +6932,48 @@ function createAdminRouter() {
6754
6932
  return admin;
6755
6933
  }
6756
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
+
6757
6975
  // src/dashboard/host-api.ts
6758
- 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";
6759
6977
  import { join as join19 } from "path";
6760
6978
  import sharp3 from "sharp";
6761
6979
 
@@ -6994,8 +7212,8 @@ function createChannelWriteRouter() {
6994
7212
  (id, position) => supabase.from("channels").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id)
6995
7213
  )
6996
7214
  );
6997
- const failed = results.find((r) => r.error);
6998
- 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);
6999
7217
  await refreshChannelRegistry();
7000
7218
  return c.json({ ok: true });
7001
7219
  });
@@ -7053,8 +7271,8 @@ import {
7053
7271
  openSync,
7054
7272
  readSync,
7055
7273
  readdirSync as readdirSync4,
7056
- readFileSync as readFileSync10,
7057
- realpathSync,
7274
+ readFileSync as readFileSync11,
7275
+ realpathSync as realpathSync2,
7058
7276
  renameSync,
7059
7277
  rmSync as rmSync5,
7060
7278
  statSync as statSync5,
@@ -7344,14 +7562,14 @@ async function readOrQueueDocAudio(input2) {
7344
7562
  }
7345
7563
 
7346
7564
  // src/dashboard/artifact-export.ts
7347
- import { readFileSync as readFileSync9 } from "fs";
7565
+ import { readFileSync as readFileSync10 } from "fs";
7348
7566
 
7349
7567
  // src/dashboard/chat-agent.ts
7350
7568
  import { homedir as homedir7 } from "os";
7351
7569
 
7352
7570
  // src/mcp/config.ts
7353
7571
  import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
7354
- import { tmpdir } from "os";
7572
+ import { tmpdir as tmpdir2 } from "os";
7355
7573
  import { join as join14 } from "path";
7356
7574
 
7357
7575
  // src/mcp/crypto.ts
@@ -8038,7 +8256,7 @@ async function buildMcpConfig(profile, connectionIds) {
8038
8256
  return { config, skipped };
8039
8257
  }
8040
8258
  function writeMcpConfigFile(config) {
8041
- const dir = mkdtempSync(join14(tmpdir(), "flyspace-mcp-"));
8259
+ const dir = mkdtempSync(join14(tmpdir2(), "flyspace-mcp-"));
8042
8260
  const path = join14(dir, "mcp-config.json");
8043
8261
  writeFileSync6(path, JSON.stringify(config), { mode: 384 });
8044
8262
  return {
@@ -8060,7 +8278,7 @@ async function withMcpConfig(profile, connectionIds, fn) {
8060
8278
  }
8061
8279
 
8062
8280
  // src/claude/agents.ts
8063
- 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";
8064
8282
  import { homedir as homedir6 } from "os";
8065
8283
  import { basename as basename4, join as join15 } from "path";
8066
8284
  import yaml from "js-yaml";
@@ -8075,7 +8293,7 @@ function expandHome(p) {
8075
8293
  function parseFrontmatter(file) {
8076
8294
  let content;
8077
8295
  try {
8078
- content = readFileSync8(file, "utf-8");
8296
+ content = readFileSync9(file, "utf-8");
8079
8297
  } catch {
8080
8298
  return {};
8081
8299
  }
@@ -8121,16 +8339,241 @@ function readAgentPersona(configDir2, cwd, name) {
8121
8339
  const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
8122
8340
  if (!agent) return void 0;
8123
8341
  try {
8124
- const content = readFileSync8(agent.path, "utf-8");
8342
+ const content = readFileSync9(agent.path, "utf-8");
8125
8343
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
8126
8344
  } catch {
8127
8345
  return void 0;
8128
8346
  }
8129
8347
  }
8130
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
+
8131
8574
  // src/dashboard/rollover-summary.ts
8132
8575
  import { spawn as spawn5 } from "child_process";
8133
- import { tmpdir as tmpdir2 } from "os";
8576
+ import { tmpdir as tmpdir3 } from "os";
8134
8577
  var HAIKU_MODEL = "haiku";
8135
8578
  var CJK_PATTERN = /[ -〿぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/;
8136
8579
  function estimateTokens(text) {
@@ -8193,7 +8636,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8193
8636
  // A stateless rewrite touches no files — tmpdir keeps it that way and
8194
8637
  // avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
8195
8638
  // unlike the channel's own cwd the main turn runs in.
8196
- cwd: tmpdir2(),
8639
+ cwd: tmpdir3(),
8197
8640
  env,
8198
8641
  stdio: ["ignore", "pipe", "pipe"]
8199
8642
  });
@@ -8989,6 +9432,13 @@ var ChatAgent = class {
8989
9432
  thread.channel_id
8990
9433
  );
8991
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
+ }
8992
9442
  const store = this.openSessionStore();
8993
9443
  try {
8994
9444
  const channelKey = `chat:${thread.channel_id}`;
@@ -9396,7 +9846,8 @@ ${msg.body}` : msg.body;
9396
9846
  signal,
9397
9847
  mcpConfigPath,
9398
9848
  ...usageContext ? { usageContext } : {},
9399
- ...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {}
9849
+ ...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {},
9850
+ ...channelConfig.memoryPrompt ? { memoryPrompt: channelConfig.memoryPrompt } : {}
9400
9851
  },
9401
9852
  channelConfig.claudeConfigDir,
9402
9853
  sessionId,
@@ -9433,6 +9884,7 @@ ${msg.body}` : msg.body;
9433
9884
  channelConfig.prompt,
9434
9885
  resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
9435
9886
  ),
9887
+ channelConfig.memoryPrompt,
9436
9888
  docsAccessPromptNote(channelConfig.docsAccess)
9437
9889
  ].filter(Boolean).join("\n\n---\n\n"),
9438
9890
  // Codex has no per-tool deny list, so any restricted requester gets
@@ -9710,7 +10162,7 @@ async function exportDocAsArtifact(channelId, docPath) {
9710
10162
  );
9711
10163
  const abs = resolveDocPath(channelConfig.cwd, docPath);
9712
10164
  if (!abs) throw new Error(`doc not found: ${docPath}`);
9713
- const content = readFileSync9(abs, "utf8");
10165
+ const content = readFileSync10(abs, "utf8");
9714
10166
  const title = extractTitle(abs, "md");
9715
10167
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9716
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).
@@ -9989,12 +10441,12 @@ function readGitBranch(dir) {
9989
10441
  try {
9990
10442
  let gitDir = join17(dir, ".git");
9991
10443
  if (statSync5(gitDir).isFile()) {
9992
- const pointer = readFileSync10(gitDir, "utf8").trim();
10444
+ const pointer = readFileSync11(gitDir, "utf8").trim();
9993
10445
  const match = pointer.match(/^gitdir:\s*(.+)$/);
9994
10446
  if (!match) return void 0;
9995
10447
  gitDir = resolve5(dir, match[1]);
9996
10448
  }
9997
- const head = readFileSync10(join17(gitDir, "HEAD"), "utf8").trim();
10449
+ const head = readFileSync11(join17(gitDir, "HEAD"), "utf8").trim();
9998
10450
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
9999
10451
  if (branchMatch) return branchMatch[1];
10000
10452
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -10021,8 +10473,8 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
10021
10473
  const kind = entryKind(entry, abs);
10022
10474
  if (kind === "dir") {
10023
10475
  if (SKIP_DIRS.has(entry.name)) continue;
10024
- const probe2 = probeDir(abs, showHidden);
10025
- if (!probe2.hasDocs && probe2.hasOther) continue;
10476
+ const probe3 = probeDir(abs, showHidden);
10477
+ if (!probe3.hasDocs && probe3.hasOther) continue;
10026
10478
  const isRepo = existsSync17(join17(abs, ".git"));
10027
10479
  dirs.push({
10028
10480
  path,
@@ -10241,7 +10693,7 @@ function moveDocEntry(root, from, to) {
10241
10693
  return { status: "into-self" };
10242
10694
  }
10243
10695
  try {
10244
- if (realpathSync(finalAbs) !== realpathSync(absFrom)) return { status: "exists" };
10696
+ if (realpathSync2(finalAbs) !== realpathSync2(absFrom)) return { status: "exists" };
10245
10697
  } catch {
10246
10698
  }
10247
10699
  renameSync(absFrom, finalAbs);
@@ -10298,7 +10750,7 @@ function createDocsRouter() {
10298
10750
  }
10299
10751
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10300
10752
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10301
- const data = readFileSync10(resolved2.abs);
10753
+ const data = readFileSync11(resolved2.abs);
10302
10754
  const headers = { "Content-Type": resolved2.contentType };
10303
10755
  if (download) {
10304
10756
  const filename = basename5(resolved2.abs).replace(/"/g, "");
@@ -10318,7 +10770,7 @@ function createDocsRouter() {
10318
10770
  }
10319
10771
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10320
10772
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10321
- return c.body(new Uint8Array(readFileSync10(abs)), 200, {
10773
+ return c.body(new Uint8Array(readFileSync11(abs)), 200, {
10322
10774
  "Content-Type": "text/html; charset=utf-8",
10323
10775
  "Content-Security-Policy": HTML_SHARE_CSP,
10324
10776
  "X-Content-Type-Options": "nosniff",
@@ -10334,10 +10786,10 @@ function createDocsRouter() {
10334
10786
  const files = form.getAll("file").filter((f) => f instanceof File);
10335
10787
  if (files.length === 0) return c.json({ error: "missing file" }, 400);
10336
10788
  const uploaded = [];
10337
- const failed = [];
10789
+ const failed2 = [];
10338
10790
  for (const file of files) {
10339
10791
  if (file.size > MAX_UPLOAD_BYTES) {
10340
- failed.push({
10792
+ failed2.push({
10341
10793
  name: file.name,
10342
10794
  error: `larger than ${MAX_UPLOAD_BYTES / (1024 * 1024)}MB`
10343
10795
  });
@@ -10351,16 +10803,16 @@ function createDocsRouter() {
10351
10803
  case "no-dir":
10352
10804
  return c.json({ error: "folder not found" }, 404);
10353
10805
  case "bad-type":
10354
- failed.push({ name: file.name, error: "file type not allowed" });
10806
+ failed2.push({ name: file.name, error: "file type not allowed" });
10355
10807
  break;
10356
10808
  case "too-large":
10357
- failed.push({ name: file.name, error: "file too large" });
10809
+ failed2.push({ name: file.name, error: "file too large" });
10358
10810
  break;
10359
10811
  default:
10360
- failed.push({ name: file.name, error: "invalid filename" });
10812
+ failed2.push({ name: file.name, error: "invalid filename" });
10361
10813
  }
10362
10814
  }
10363
- 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);
10364
10816
  });
10365
10817
  app.post("/:id/docs/folder", async (c) => {
10366
10818
  const { cwd } = resolveChannelConfig(c.req.param("id"));
@@ -10503,7 +10955,7 @@ function createDocsRouter() {
10503
10955
  title: kind ? extractTitle(abs, kind) : basename5(abs),
10504
10956
  mtime: stat.mtime.toISOString(),
10505
10957
  tooLarge: tooLarge || void 0,
10506
- content: tooLarge ? "" : readFileSync10(abs, "utf8")
10958
+ content: tooLarge ? "" : readFileSync11(abs, "utf8")
10507
10959
  });
10508
10960
  });
10509
10961
  app.put("/:id/docs/content", async (c) => {
@@ -10546,7 +10998,7 @@ function createDocsRouter() {
10546
10998
  channel,
10547
10999
  rel,
10548
11000
  title: extractTitle(abs, "md"),
10549
- content: readFileSync10(abs, "utf8")
11001
+ content: readFileSync11(abs, "utf8")
10550
11002
  };
10551
11003
  };
10552
11004
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -10769,7 +11221,7 @@ function ownSkills(scope, dir) {
10769
11221
  return [{ scope, name, description, body, path, plugin: null, mtime }];
10770
11222
  });
10771
11223
  }
10772
- function snapshot(scopes) {
11224
+ function snapshot2(scopes) {
10773
11225
  const wants = (scope) => !scopes || scopes.has(scope);
10774
11226
  const configDir2 = profileClaudeConfigDir();
10775
11227
  const rows = [];
@@ -10804,7 +11256,7 @@ async function pushSkillScopes(scopes) {
10804
11256
  if (!mirrorEnabled()) return;
10805
11257
  const workspace = getProfile();
10806
11258
  const supabase = getSupabaseClient();
10807
- const rows = snapshot(scopes ? new Set(scopes) : void 0);
11259
+ const rows = snapshot2(scopes ? new Set(scopes) : void 0);
10808
11260
  let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
10809
11261
  if (scopes) query = query.in("scope", scopes);
10810
11262
  const { data, error: error2 } = await query;
@@ -11084,32 +11536,6 @@ function registerMirroredSkillRoutes(api, agent) {
11084
11536
  mountSkillRoutes(api, "/channels/:id/skill-files", channelRef, backend);
11085
11537
  }
11086
11538
 
11087
- // src/dashboard/agent-identities.ts
11088
- var BOT_DM_KEY = "claude";
11089
- async function resolveAgentIdentities() {
11090
- const settings = loadDashboardSettings();
11091
- const { cwd: defaultCwd } = resolveChannelConfig("");
11092
- const main2 = [
11093
- {
11094
- name: BOT_DM_KEY,
11095
- label: "Workspace Agent",
11096
- kind: "main",
11097
- cwd: defaultCwd,
11098
- model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
11099
- ...await loadBotIdentity()
11100
- }
11101
- ];
11102
- return Promise.all(
11103
- main2.map(async ({ cwd, ...agent }) => {
11104
- const dm = await ensureAgentDmChannel(agent.name, {
11105
- name: agent.displayName ?? agent.label,
11106
- cwd
11107
- });
11108
- return { ...agent, channelId: dm.id };
11109
- })
11110
- );
11111
- }
11112
-
11113
11539
  // src/dashboard/providers.ts
11114
11540
  import { execFileSync as execFileSync2 } from "child_process";
11115
11541
  import { existsSync as existsSync20 } from "fs";
@@ -11143,7 +11569,7 @@ function claudeAuthAvailable() {
11143
11569
  return false;
11144
11570
  }
11145
11571
  }
11146
- function probe(runner) {
11572
+ function probe2(runner) {
11147
11573
  const label2 = PROVIDER_LABELS[runner];
11148
11574
  const bin = runner === "codex" ? "codex" : "claude";
11149
11575
  if (!binaryOnPath(bin)) {
@@ -11164,7 +11590,7 @@ function providerStatuses() {
11164
11590
  const key = getProfile();
11165
11591
  const hit = cache6.get(key);
11166
11592
  if (hit && Date.now() - hit.at < TTL_MS2) return hit.statuses;
11167
- const statuses = ["claude", "codex"].map(probe);
11593
+ const statuses = ["claude", "codex"].map(probe2);
11168
11594
  cache6.set(key, { at: Date.now(), statuses });
11169
11595
  return statuses;
11170
11596
  }
@@ -11287,7 +11713,7 @@ function registerHostRoutes(api) {
11287
11713
  }
11288
11714
  function tailFile(file, limit) {
11289
11715
  if (!existsSync21(file)) return [];
11290
- const lines = readFileSync11(file, "utf-8").split("\n");
11716
+ const lines = readFileSync12(file, "utf-8").split("\n");
11291
11717
  if (lines.at(-1) === "") lines.pop();
11292
11718
  return lines.slice(-limit);
11293
11719
  }
@@ -11399,8 +11825,8 @@ function createCategoryWriteRouter() {
11399
11825
  (id, position) => supabase.from("categories").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).eq("profile", getProfile())
11400
11826
  )
11401
11827
  );
11402
- const failed = results.find((r) => r.error);
11403
- 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);
11404
11830
  return c.json({ ok: true });
11405
11831
  });
11406
11832
  app.put("/:id", async (c) => {
@@ -14354,8 +14780,80 @@ function createChannelMembersRouter() {
14354
14780
  return router;
14355
14781
  }
14356
14782
 
14357
- // src/dashboard/onboarding-routes.ts
14783
+ // src/dashboard/memories-routes.ts
14358
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";
14359
14857
 
14360
14858
  // src/dashboard/unique-channel-name.ts
14361
14859
  function uniqueChannelName(raw, taken) {
@@ -14444,7 +14942,7 @@ async function buildGreeting(input2) {
14444
14942
  // src/dashboard/onboarding-routes.ts
14445
14943
  var MAX_FOLDERS = 20;
14446
14944
  function createOnboardingRouter(agent) {
14447
- const app = new Hono27();
14945
+ const app = new Hono28();
14448
14946
  app.get("/scan", async (c) => {
14449
14947
  const ctx = await resolveAuthContext(c);
14450
14948
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -14689,6 +15187,7 @@ function registerDataRoutes(api, agent = {}) {
14689
15187
  api.route("/channels", createDocShareWriteRouter(agent));
14690
15188
  api.route("/onboarding", createOnboardingRouter(agent));
14691
15189
  api.route("/channels", createChannelMembersRouter());
15190
+ api.route("/channels", createMemoriesRouter());
14692
15191
  api.route("/docs", createDocShareListRouter());
14693
15192
  api.get("/users", (c) => c.json({ users: listActiveUsers() }));
14694
15193
  api.route("/categories", createCategoryWriteRouter());
@@ -14939,17 +15438,17 @@ function registerDataRoutes(api, agent = {}) {
14939
15438
  // src/dashboard/device-auth-routes.ts
14940
15439
  import { verify } from "crypto";
14941
15440
  import { getConnInfo } from "@hono/node-server/conninfo";
14942
- import { Hono as Hono28 } from "hono";
15441
+ import { Hono as Hono29 } from "hono";
14943
15442
 
14944
15443
  // src/dashboard/agent-bridge.ts
14945
15444
  import { randomBytes as randomBytes8, randomUUID as randomUUID4, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
14946
- import { existsSync as existsSync25, readFileSync as readFileSync15 } from "fs";
15445
+ import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
14947
15446
  import dotenv from "dotenv";
14948
15447
  import { WebSocket, WebSocketServer } from "ws";
14949
15448
 
14950
15449
  // src/dashboard/host-identity.ts
14951
15450
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
14952
- 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";
14953
15452
  import { dirname as dirname6, join as join20 } from "path";
14954
15453
  function hostPrivateKeyPath() {
14955
15454
  return process.env["FLYSPACE_HOST_KEY_PATH"] || join20(paths.BASE_CONFIG_DIR, "host.key");
@@ -14960,7 +15459,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
14960
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.`
14961
15460
  );
14962
15461
  }
14963
- return createPrivateKey2(readFileSync12(path, "utf8"));
15462
+ return createPrivateKey2(readFileSync13(path, "utf8"));
14964
15463
  }
14965
15464
  function loadHostPublicKey() {
14966
15465
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -14976,7 +15475,7 @@ function loadHostPublicKey() {
14976
15475
  // src/dashboard/host-lock.ts
14977
15476
  import { execFileSync as execFileSync3 } from "child_process";
14978
15477
  import { randomBytes as randomBytes5 } from "crypto";
14979
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
15478
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
14980
15479
  import { join as join21 } from "path";
14981
15480
  function hostLockPath() {
14982
15481
  return join21(paths.BASE_DATA_DIR, "host.lock");
@@ -14994,7 +15493,7 @@ function isLiveFlyspace(pid) {
14994
15493
  }
14995
15494
  function readLockPid(path) {
14996
15495
  try {
14997
- const pid = Number(JSON.parse(readFileSync13(path, "utf8")).pid);
15496
+ const pid = Number(JSON.parse(readFileSync14(path, "utf8")).pid);
14998
15497
  return Number.isInteger(pid) && pid > 0 ? pid : null;
14999
15498
  } catch {
15000
15499
  return null;
@@ -15035,13 +15534,13 @@ function publishHostEndpoint(port) {
15035
15534
  const path = hostLockPath();
15036
15535
  if (readLockPid(path) !== process.pid) return null;
15037
15536
  const token = randomBytes5(32).toString("hex");
15038
- const lock = JSON.parse(readFileSync13(path, "utf8"));
15537
+ const lock = JSON.parse(readFileSync14(path, "utf8"));
15039
15538
  writeFileSync9(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
15040
15539
  return token;
15041
15540
  }
15042
15541
  function readHostEndpoint() {
15043
15542
  try {
15044
- const { pid, port, token } = JSON.parse(readFileSync13(hostLockPath(), "utf8"));
15543
+ const { pid, port, token } = JSON.parse(readFileSync14(hostLockPath(), "utf8"));
15045
15544
  if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
15046
15545
  return { port, token };
15047
15546
  } catch {
@@ -15281,7 +15780,7 @@ async function userEmail(userId) {
15281
15780
 
15282
15781
  // src/dashboard/doc-share-publisher.ts
15283
15782
  import { randomBytes as randomBytes7 } from "crypto";
15284
- import { readFileSync as readFileSync14, statSync as statSync9 } from "fs";
15783
+ import { readFileSync as readFileSync15, statSync as statSync9 } from "fs";
15285
15784
  import { extname as extname2, posix } from "path";
15286
15785
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
15287
15786
 
@@ -15468,7 +15967,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15468
15967
  }
15469
15968
  if (!stat.isFile()) return { status: "not-found" };
15470
15969
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
15471
- const source = readFileSync14(abs, "utf8");
15970
+ const source = readFileSync15(abs, "utf8");
15472
15971
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
15473
15972
  if (kind === "excalidraw") {
15474
15973
  try {
@@ -15548,7 +16047,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15548
16047
  let text = null;
15549
16048
  try {
15550
16049
  const size = statSync9(docAbs).size;
15551
- if (size <= budget) text = readFileSync14(docAbs, "utf8");
16050
+ if (size <= budget) text = readFileSync15(docAbs, "utf8");
15552
16051
  } catch {
15553
16052
  continue;
15554
16053
  }
@@ -15565,7 +16064,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15565
16064
  let body;
15566
16065
  try {
15567
16066
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
15568
- body = readFileSync14(resolved2.abs);
16067
+ body = readFileSync15(resolved2.abs);
15569
16068
  } catch {
15570
16069
  continue;
15571
16070
  }
@@ -15607,7 +16106,7 @@ async function publishDocShare(input2) {
15607
16106
  ...HTML_FEATURES,
15608
16107
  brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
15609
16108
  } : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
15610
- const snapshot2 = {
16109
+ const snapshot3 = {
15611
16110
  ...built,
15612
16111
  sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
15613
16112
  };
@@ -15617,8 +16116,8 @@ async function publishDocShare(input2) {
15617
16116
  const renamed = await setDocShareSlug(existing.id, slug);
15618
16117
  if (!renamed) return { status: "not-found" };
15619
16118
  }
15620
- const share2 = await updateDocShareSnapshot(existing.id, snapshot2, features);
15621
- 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));
15622
16121
  await deleteShareAssets(stale);
15623
16122
  if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
15624
16123
  void queueShareAudio(share2);
@@ -15632,7 +16131,7 @@ async function publishDocShare(input2) {
15632
16131
  channelId: input2.channelId,
15633
16132
  sourcePath: input2.sourcePath,
15634
16133
  createdBy: input2.createdBy,
15635
- ...snapshot2,
16134
+ ...snapshot3,
15636
16135
  ...features
15637
16136
  });
15638
16137
  void queueShareAudio(share);
@@ -15747,8 +16246,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
15747
16246
  const scope = table === "skill_files" && isWrite ? assigned : workspaces;
15748
16247
  if (scope.length === 0) return deny(`This Host is not the assigned Host for any workspace it may write ${table} to.`);
15749
16248
  if (method === "POST") {
15750
- const ok2 = rows.length > 0 && rows.every((r) => typeof r[slugColumn] === "string" && scope.includes(r[slugColumn]));
15751
- 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.`);
15752
16251
  }
15753
16252
  if (method === "PATCH") {
15754
16253
  const patch = bodyRows(req.body);
@@ -15762,8 +16261,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
15762
16261
  if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
15763
16262
  if (table === "threads") {
15764
16263
  if (method === "POST") {
15765
- const ok2 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
15766
- 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.");
15767
16266
  }
15768
16267
  if (method === "PATCH") {
15769
16268
  const patch = bodyRows(req.body);
@@ -15942,7 +16441,7 @@ function localAgentCallbacks(chatAgent) {
15942
16441
  regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
15943
16442
  listAgentRuns: () => chatAgent.runningThreadIds(),
15944
16443
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
15945
- listRunningProfiles: () => listProfiles().filter(isServiceRunning),
16444
+ listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
15946
16445
  getAgentIdentities: () => resolveAgentIdentities(),
15947
16446
  publishDocShare: (input2) => publishDocShare(input2),
15948
16447
  listChannelSkills: (args) => listSkills(args.claudeConfigDir, args.cwd),
@@ -15959,14 +16458,14 @@ function localAgentCallbacks(chatAgent) {
15959
16458
  }
15960
16459
  var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
15961
16460
  function buildHostConfigSnapshot() {
15962
- const snapshot2 = {};
16461
+ const snapshot3 = {};
15963
16462
  for (const profile of listProfiles()) {
15964
- snapshot2[profile] = runWithProfile(profile, () => ({
15965
- 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")) : {},
15966
16465
  agentModel: loadDashboardSettings().agent
15967
16466
  }));
15968
16467
  }
15969
- return snapshot2;
16468
+ return snapshot3;
15970
16469
  }
15971
16470
  function startLocalProfileConfigHydration() {
15972
16471
  setProfileConfigCache(buildHostConfigSnapshot());
@@ -16208,7 +16707,8 @@ function remoteAgentCallbacks() {
16208
16707
  // Host connected, showing nobody as running is a reasonable default
16209
16708
  // rather than a false claim of certainty either way.
16210
16709
  listRunningProfiles: async () => await callHost("listRunningProfiles", []) ?? [],
16211
- getAgentIdentities: async () => await callHost("getAgentIdentities", []) ?? [],
16710
+ // `null` (no Host) falls back to `offlineAgentIdentities` in api.ts.
16711
+ getAgentIdentities: () => callHost("getAgentIdentities", []),
16212
16712
  // Reading the doc and copying its assets can only happen where the
16213
16713
  // channel's real folder is, so this always goes to the Host. `null` (no
16214
16714
  // Host connected) surfaces as `host-unavailable` — see
@@ -16492,7 +16992,7 @@ async function boundWorkspaceIds(publicKey, userId) {
16492
16992
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
16493
16993
  }
16494
16994
  function createDeviceAuthRouter() {
16495
- const app = new Hono28();
16995
+ const app = new Hono29();
16496
16996
  app.post("/code", async (c) => {
16497
16997
  const body = await readBody(c);
16498
16998
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -16555,8 +17055,8 @@ function createDeviceAuthRouter() {
16555
17055
  if (!device || device.revokedAt || !nonce || !signature) return c.json({ error: "invalid_grant" }, 401);
16556
17056
  if (!await consumeChallenge(device.id, nonce)) return c.json({ error: "invalid_grant" }, 401);
16557
17057
  const key = parseEd25519PublicKey(device.publicKey);
16558
- const ok2 = key && verify(null, Buffer.from(nonce), key, Buffer.from(signature, "base64"));
16559
- 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);
16560
17060
  const { token, expiresInS } = await issueAccessToken(device.id);
16561
17061
  return c.json({ access_token: token, token_type: "Bearer", expires_in: expiresInS });
16562
17062
  });
@@ -16656,7 +17156,7 @@ function createDeviceAuthRouter() {
16656
17156
 
16657
17157
  // src/dashboard/api.ts
16658
17158
  function createApiRouter(agent = {}, mode = "all") {
16659
- const api = new Hono29();
17159
+ const api = new Hono30();
16660
17160
  api.use("*", async (c, next) => {
16661
17161
  const known = await listKnownWorkspaces();
16662
17162
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -16703,7 +17203,7 @@ function createApiRouter(agent = {}, mode = "all") {
16703
17203
  if (mode === "data") {
16704
17204
  api.get(
16705
17205
  "/settings/agents",
16706
- async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? [] })
17206
+ async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? await offlineAgentIdentities() })
16707
17207
  );
16708
17208
  api.get("/channels/:id/skills", async (c) => {
16709
17209
  const { cwd, claudeConfigDir } = resolveChannelConfig(c.req.param("id"));
@@ -16767,7 +17267,7 @@ function createApiRouter(agent = {}, mode = "all") {
16767
17267
  }
16768
17268
 
16769
17269
  // src/dashboard/app-icon-routes.ts
16770
- import { Hono as Hono30 } from "hono";
17270
+ import { Hono as Hono31 } from "hono";
16771
17271
  var NEUTRAL_MANIFEST = {
16772
17272
  id: "/",
16773
17273
  name: "Flyspace",
@@ -16805,7 +17305,7 @@ async function withResolvedProfile2(c, next) {
16805
17305
  return profile ? runWithProfile(profile, () => next()) : next();
16806
17306
  }
16807
17307
  function createAppIconRouter() {
16808
- const app = new Hono30();
17308
+ const app = new Hono31();
16809
17309
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
16810
17310
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
16811
17311
  const profile = getProfile();
@@ -16845,7 +17345,7 @@ function pngResponse(c, png) {
16845
17345
  }
16846
17346
 
16847
17347
  // src/dashboard/stripe-routes.ts
16848
- import { Hono as Hono31 } from "hono";
17348
+ import { Hono as Hono32 } from "hono";
16849
17349
  function planStatusFor(status) {
16850
17350
  const known = ["trialing", "active", "paused", "canceled"];
16851
17351
  return known.includes(status) ? status : null;
@@ -16888,7 +17388,7 @@ async function handleCardAdded(session) {
16888
17388
  }
16889
17389
  }
16890
17390
  function createStripeWebhookRouter() {
16891
- const stripeRouter = new Hono31();
17391
+ const stripeRouter = new Hono32();
16892
17392
  stripeRouter.post("/webhook", async (c) => {
16893
17393
  const secret = process.env["STRIPE_WEBHOOK_SECRET"];
16894
17394
  const signature = c.req.header("stripe-signature");
@@ -17004,10 +17504,10 @@ var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
17004
17504
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
17005
17505
  for (let port = start; port < start + attempts; port++) {
17006
17506
  const free = await new Promise((resolve8) => {
17007
- const probe2 = createServer();
17008
- probe2.once("error", () => resolve8(false));
17009
- probe2.once("listening", () => probe2.close(() => resolve8(true)));
17010
- 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);
17011
17511
  });
17012
17512
  if (free) return port;
17013
17513
  }
@@ -17048,7 +17548,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17048
17548
  const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
17049
17549
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
17050
17550
  };
17051
- const app = new Hono32();
17551
+ const app = new Hono33();
17052
17552
  app.use("*", async (c, next) => {
17053
17553
  await next();
17054
17554
  if (c.req.path.startsWith("/assets/")) {
@@ -17518,7 +18018,7 @@ async function threadReplies(client2, nameOrId, ts2) {
17518
18018
  async function postMessage(client2, args) {
17519
18019
  const channelArg = getArg2(args, "--channel");
17520
18020
  const textFilePath = getArg2(args, "--text-file");
17521
- const text = textFilePath ? readFileSync16(textFilePath, "utf8") : getArg2(args, "--text");
18021
+ const text = textFilePath ? readFileSync17(textFilePath, "utf8") : getArg2(args, "--text");
17522
18022
  const threadTs = getArg2(args, "--thread");
17523
18023
  const markdown = args.includes("--markdown");
17524
18024
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -17673,7 +18173,7 @@ async function ensureClaudeCodeReady() {
17673
18173
  }
17674
18174
  function getManifestYaml(botName) {
17675
18175
  const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
17676
- const raw = readFileSync16(manifestPath, "utf-8");
18176
+ const raw = readFileSync17(manifestPath, "utf-8");
17677
18177
  return raw.replace(/name: Custie/g, `name: ${botName}`);
17678
18178
  }
17679
18179
  async function stepPersonalise() {
@@ -17886,7 +18386,7 @@ async function stepBrowserTokens(botName) {
17886
18386
  })
17887
18387
  ));
17888
18388
  const DEBUG = process.argv.includes("--debug");
17889
- const USER_DATA_DIR = resolve7(tmpdir3(), "custie-playwright-profile");
18389
+ const USER_DATA_DIR = resolve7(tmpdir4(), "custie-playwright-profile");
17890
18390
  let context;
17891
18391
  try {
17892
18392
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -18107,7 +18607,7 @@ function copyDefaultPrompt(botName) {
18107
18607
  if (!existsSync27(paths.PROMPT_FILE)) {
18108
18608
  const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
18109
18609
  if (existsSync27(defaultPrompt)) {
18110
- const content = readFileSync16(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18610
+ const content = readFileSync17(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18111
18611
  writeFileSync10(paths.PROMPT_FILE, content);
18112
18612
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
18113
18613
  }
@@ -18429,8 +18929,122 @@ async function runChatCmd(args) {
18429
18929
  }
18430
18930
  }
18431
18931
 
18432
- // src/commands/mcp.ts
18932
+ // src/commands/memory.ts
18933
+ import { readFileSync as readFileSync18 } from "fs";
18433
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);
19042
+ process.exit(1);
19043
+ }
19044
+ }
19045
+
19046
+ // src/commands/mcp.ts
19047
+ var USAGE6 = `
18434
19048
  Usage: flyspace mcp <server>
18435
19049
 
18436
19050
  Run one of Flyspace's own MCP servers on stdio. Spawned by the agent runtime
@@ -18447,7 +19061,7 @@ Usage: flyspace mcp <server>
18447
19061
  async function runMcpCmd(args) {
18448
19062
  const server = args[0];
18449
19063
  if (!server || server === "-h" || server === "--help") {
18450
- console.log(USAGE5.trim());
19064
+ console.log(USAGE6.trim());
18451
19065
  return;
18452
19066
  }
18453
19067
  restoreStdout();
@@ -18460,7 +19074,7 @@ async function runMcpCmd(args) {
18460
19074
  }
18461
19075
  default:
18462
19076
  console.error(`Unknown MCP server: ${server}`);
18463
- console.error(USAGE5.trim());
19077
+ console.error(USAGE6.trim());
18464
19078
  process.exit(1);
18465
19079
  }
18466
19080
  }
@@ -18536,7 +19150,7 @@ async function runDashboard(args) {
18536
19150
 
18537
19151
  // src/commands/workspace.ts
18538
19152
  import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
18539
- var USAGE6 = `
19153
+ var USAGE7 = `
18540
19154
  Usage: flyspace workspace <subcommand> [options]
18541
19155
 
18542
19156
  Subcommands:
@@ -18553,7 +19167,7 @@ var USAGE6 = `
18553
19167
  --no-invite Never send an invite email. Fails if the admin
18554
19168
  has no existing account.
18555
19169
  `;
18556
- function getArg4(args, flag) {
19170
+ function getArg5(args, flag) {
18557
19171
  const idx = args.indexOf(flag);
18558
19172
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
18559
19173
  }
@@ -18649,10 +19263,10 @@ async function provisionChannels(specs) {
18649
19263
  async function handleCreate(args) {
18650
19264
  const slug = args[0];
18651
19265
  if (!slug || slug.startsWith("-")) fail2("Usage: flyspace workspace create <slug> --admin-email <email>");
18652
- const adminEmail = getArg4(args, "--admin-email");
19266
+ const adminEmail = getArg5(args, "--admin-email");
18653
19267
  if (!adminEmail?.includes("@")) fail2("--admin-email is required and must be an email address");
18654
- const botName = getArg4(args, "--bot-name") ?? slug;
18655
- 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";
18656
19270
  const allowInvite = !args.includes("--no-invite");
18657
19271
  const channels = getAllArgs(args, "--channel").map(parseChannelSpec);
18658
19272
  loadEnvFiles();
@@ -18675,13 +19289,13 @@ async function runWorkspaceCmd(args) {
18675
19289
  await handleCreate(args.slice(1));
18676
19290
  break;
18677
19291
  default:
18678
- console.log(USAGE6);
19292
+ console.log(USAGE7);
18679
19293
  process.exit(args[0] ? 1 : 0);
18680
19294
  }
18681
19295
  }
18682
19296
 
18683
19297
  // src/commands/reconcile.ts
18684
- var USAGE7 = `
19298
+ var USAGE8 = `
18685
19299
  Usage: flyspace reconcile [<workspace> | --all]
18686
19300
 
18687
19301
  Repair a workspace's local filesystem state (profile dir, config.env,
@@ -18701,7 +19315,7 @@ function printLog(name, log5) {
18701
19315
  }
18702
19316
  async function runReconcileCmd(args) {
18703
19317
  if (args.includes("-h") || args.includes("--help")) {
18704
- console.log(USAGE7);
19318
+ console.log(USAGE8);
18705
19319
  return;
18706
19320
  }
18707
19321
  loadEnvFiles();
@@ -18717,12 +19331,13 @@ async function runReconcileCmd(args) {
18717
19331
 
18718
19332
  // src/cli.ts
18719
19333
  var XERO_USAGE = false ? " xero Connect to Xero and expose it as an MCP server\n" : "";
18720
- var USAGE8 = `
19334
+ var USAGE9 = `
18721
19335
  Usage: flyspace <command> [options]
18722
19336
 
18723
19337
  Commands:
18724
19338
  setup Set up this machine as a Host (login, agent CLIs, ~/Flyspace)
18725
19339
  auth Log in, check status, or log out this device
19340
+ doctor Check this Host end to end (login, agent CLIs, services)
18726
19341
  start Start the agent host (Slack only if configured)
18727
19342
  install Install as a system service (launchd / systemd)
18728
19343
  uninstall Remove the system service
@@ -18736,6 +19351,7 @@ var USAGE8 = `
18736
19351
  usage Show agent token usage and cost for this workspace
18737
19352
  slack Query Slack (channels, users, post messages)
18738
19353
  chat Inspect dashboard chat channels and move threads between them
19354
+ memory Read and write a channel's persistent agent memory
18739
19355
  profiles List local profiles (bound here? scheduler?) or remove one
18740
19356
  workspace Provision a workspace (Supabase rows + local profile dirs)
18741
19357
  reconcile Repair a workspace's local files against Supabase
@@ -18776,6 +19392,9 @@ async function main() {
18776
19392
  case "auth":
18777
19393
  await runAuthCmd(args.slice(1));
18778
19394
  break;
19395
+ case "doctor":
19396
+ await runDoctor(args.slice(1));
19397
+ break;
18779
19398
  case "start":
18780
19399
  await runStart();
18781
19400
  break;
@@ -18824,6 +19443,9 @@ async function main() {
18824
19443
  case "chat":
18825
19444
  await runChatCmd(args.slice(1));
18826
19445
  break;
19446
+ case "memory":
19447
+ await runMemoryCmd(args.slice(1));
19448
+ break;
18827
19449
  case "profile":
18828
19450
  case "profiles":
18829
19451
  await runProfiles(args.slice(1));
@@ -18836,12 +19458,12 @@ async function main() {
18836
19458
  break;
18837
19459
  case "-v":
18838
19460
  case "--version":
18839
- printRaw("0.1.7");
19461
+ printRaw("0.1.9");
18840
19462
  break;
18841
19463
  case "-h":
18842
19464
  case "--help":
18843
19465
  case void 0:
18844
- printRaw(USAGE8);
19466
+ printRaw(USAGE9);
18845
19467
  break;
18846
19468
  case "xero":
18847
19469
  if (false) {
@@ -18852,7 +19474,7 @@ async function main() {
18852
19474
  // falls through
18853
19475
  default:
18854
19476
  printRaw(`Unknown command: ${command}`);
18855
- printRaw(USAGE8);
19477
+ printRaw(USAGE9);
18856
19478
  process.exit(1);
18857
19479
  }
18858
19480
  }