flyspace-cli 0.1.7 → 0.1.10

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-UYLVSUD6.js";
154
156
  import {
155
157
  firstReplyBody,
156
158
  getThread,
@@ -437,9 +439,23 @@ 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";
441
- import { join as join2 } from "path";
442
+ import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
443
+ import { basename, dirname as dirname2, join as join2 } from "path";
442
444
  import { homedir, platform } from "os";
445
+ function flyspaceHome(entry = process.argv[1]) {
446
+ let cliPath;
447
+ try {
448
+ cliPath = realpathSync(entry ?? "");
449
+ } catch {
450
+ return void 0;
451
+ }
452
+ const at = cliPath.indexOf("/lib/node_modules/flyspace-cli/");
453
+ if (at <= 0) return void 0;
454
+ const npmPrefix = cliPath.slice(0, at);
455
+ if (basename(npmPrefix) !== "cli") return void 0;
456
+ const home = dirname2(npmPrefix);
457
+ return existsSync3(join2(home, "bin", "flyspace")) && existsSync3(join2(home, "node", "bin", "node")) ? home : void 0;
458
+ }
443
459
  function serviceLabel(profile) {
444
460
  return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
445
461
  }
@@ -466,6 +482,22 @@ function isServiceInstalled(profile) {
466
482
  if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
467
483
  return false;
468
484
  }
485
+ function serviceBinaryMissing(file) {
486
+ let text;
487
+ try {
488
+ text = readFileSync3(file, "utf-8");
489
+ } catch {
490
+ return false;
491
+ }
492
+ const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
493
+ return !!bin && !existsSync3(bin);
494
+ }
495
+ function profileServiceFile(profile) {
496
+ return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
497
+ }
498
+ function hostServiceFile() {
499
+ return platform() === "darwin" ? hostPlistPath() : join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
500
+ }
469
501
  function restartService(profile) {
470
502
  const os = platform();
471
503
  if (os === "darwin") {
@@ -497,10 +529,7 @@ function hostPlistPath() {
497
529
  return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
498
530
  }
499
531
  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
- }
532
+ if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
504
533
  return false;
505
534
  }
506
535
  function restartHostService() {
@@ -518,11 +547,31 @@ function restartHostService() {
518
547
  }
519
548
  return false;
520
549
  }
550
+ function serviceActive(label2, unit) {
551
+ try {
552
+ if (platform() === "darwin") {
553
+ const out = execSync(`launchctl print gui/${process.getuid?.()}/${label2}`, { encoding: "utf-8", stdio: "pipe" });
554
+ return /^\s*state = running$/m.test(out);
555
+ }
556
+ if (platform() === "linux") {
557
+ execSync(`systemctl --user is-active --quiet ${unit}`, { stdio: "pipe" });
558
+ return true;
559
+ }
560
+ } catch {
561
+ }
562
+ return false;
563
+ }
564
+ function isServiceRunning(profile) {
565
+ return serviceActive(serviceLabel(profile), systemdUnitName(profile));
566
+ }
567
+ function isHostServiceRunning() {
568
+ return serviceActive(HOST_SERVICE_LABEL, HOST_SYSTEMD_UNIT);
569
+ }
521
570
 
522
571
  // src/commands/install.ts
523
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync } from "fs";
572
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
524
573
  import { execSync as execSync2 } from "child_process";
525
- import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
574
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
526
575
  import { homedir as homedir2, platform as platform2 } from "os";
527
576
  function log(msg) {
528
577
  console.log(`
@@ -543,12 +592,23 @@ function run(cmd, opts = {}) {
543
592
  function detectClaudeBinDir() {
544
593
  try {
545
594
  const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
546
- return claudePath ? dirname2(claudePath) : void 0;
595
+ return claudePath ? dirname3(claudePath) : void 0;
547
596
  } catch {
548
597
  return void 0;
549
598
  }
550
599
  }
600
+ function detectNodeBinDir() {
601
+ const home = flyspaceHome();
602
+ return home ? join3(home, "node", "bin") : dirname3(process.execPath);
603
+ }
604
+ function nodeShimDirs() {
605
+ return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
606
+ (d) => existsSync4(d)
607
+ );
608
+ }
551
609
  function detectCustieBin() {
610
+ const home = flyspaceHome();
611
+ if (home) return join3(home, "bin", "flyspace");
552
612
  const argv1 = process.argv[1];
553
613
  if (argv1) {
554
614
  return resolve3(argv1);
@@ -562,7 +622,7 @@ function detectCustieBin() {
562
622
  function readEnvFile() {
563
623
  if (!existsSync4(paths.CONFIG_FILE)) return {};
564
624
  const vars = {};
565
- for (const line of readFileSync3(paths.CONFIG_FILE, "utf-8").split("\n")) {
625
+ for (const line of readFileSync4(paths.CONFIG_FILE, "utf-8").split("\n")) {
566
626
  const trimmed = line.trim();
567
627
  if (!trimmed || trimmed.startsWith("#")) continue;
568
628
  const eq = trimmed.indexOf("=");
@@ -604,18 +664,15 @@ function hostSpec() {
604
664
  async function installMacOS(spec) {
605
665
  log("Setting up macOS LaunchAgent...");
606
666
  const custieBin = detectCustieBin();
607
- const nodePath = process.execPath;
608
667
  const claudeBinDir = detectClaudeBinDir();
609
668
  const logDir = paths.LOG_DIR;
610
669
  mkdirSync2(logDir, { recursive: true });
611
670
  const envVars = spec.env;
612
671
  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
672
  const pathParts = [
616
- dirname2(nodePath),
673
+ detectNodeBinDir(),
617
674
  claudeBinDir,
618
- asdfBin,
675
+ ...nodeShimDirs(),
619
676
  homebrewBin,
620
677
  "/usr/local/bin",
621
678
  "/usr/bin",
@@ -657,7 +714,7 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
657
714
  </dict>
658
715
  </plist>`;
659
716
  const agentPlistPath = spec.plistPath;
660
- mkdirSync2(dirname2(agentPlistPath), { recursive: true });
717
+ mkdirSync2(dirname3(agentPlistPath), { recursive: true });
661
718
  try {
662
719
  run(`launchctl unload "${agentPlistPath}"`, { silent: true });
663
720
  } catch {
@@ -675,15 +732,15 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
675
732
  async function installLinux(spec) {
676
733
  log("Setting up systemd user service...");
677
734
  const custieBin = detectCustieBin();
678
- const nodePath = process.execPath;
679
735
  const claudeBinDir = detectClaudeBinDir();
680
736
  const logDir = paths.LOG_DIR;
681
737
  mkdirSync2(logDir, { recursive: true });
682
738
  const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
683
739
  const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
684
740
  const pathParts = [
685
- dirname2(nodePath),
741
+ detectNodeBinDir(),
686
742
  claudeBinDir,
743
+ ...nodeShimDirs(),
687
744
  linuxbrewBin,
688
745
  "/usr/local/bin",
689
746
  "/usr/bin",
@@ -757,7 +814,7 @@ function listProfiles() {
757
814
  }
758
815
  return profiles;
759
816
  }
760
- function isServiceRunning(profile) {
817
+ function isServiceRunning2(profile) {
761
818
  try {
762
819
  if (platform3() === "darwin") {
763
820
  const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
@@ -774,8 +831,8 @@ function isServiceRunning(profile) {
774
831
 
775
832
  // src/auth/device-auth.ts
776
833
  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";
778
- import { dirname as dirname3, join as join5 } from "path";
834
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
835
+ import { dirname as dirname4, join as join5 } from "path";
779
836
  var DEFAULT_API_URL = "https://app.flyspace.ai";
780
837
  var EXPIRY_SKEW_MS = 6e4;
781
838
  var AuthApiError = class extends Error {
@@ -798,11 +855,11 @@ function ensureDeviceKey(path = deviceKeyPath()) {
798
855
  let created = false;
799
856
  if (!existsSync6(path)) {
800
857
  const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
801
- mkdirSync3(dirname3(path), { recursive: true });
858
+ mkdirSync3(dirname4(path), { recursive: true });
802
859
  writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
803
860
  created = true;
804
861
  }
805
- const privateKey = createPrivateKey(readFileSync4(path, "utf8"));
862
+ const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
806
863
  const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
807
864
  "base64"
808
865
  );
@@ -811,13 +868,13 @@ function ensureDeviceKey(path = deviceKeyPath()) {
811
868
  function loadCredentials(path = credentialsPath()) {
812
869
  if (!existsSync6(path)) return null;
813
870
  try {
814
- return JSON.parse(readFileSync4(path, "utf8"));
871
+ return JSON.parse(readFileSync5(path, "utf8"));
815
872
  } catch {
816
873
  return null;
817
874
  }
818
875
  }
819
876
  function saveCredentials(creds, path = credentialsPath()) {
820
- mkdirSync3(dirname3(path), { recursive: true });
877
+ mkdirSync3(dirname4(path), { recursive: true });
821
878
  writeFileSync4(path, `${JSON.stringify(creds, null, 2)}
822
879
  `, { mode: 384 });
823
880
  }
@@ -957,12 +1014,12 @@ import {
957
1014
  lstatSync,
958
1015
  mkdirSync as mkdirSync4,
959
1016
  readdirSync as readdirSync2,
960
- readFileSync as readFileSync5,
1017
+ readFileSync as readFileSync6,
961
1018
  statSync as statSync2,
962
1019
  symlinkSync
963
1020
  } from "fs";
964
1021
  import { homedir as homedir3 } from "os";
965
- import { basename, join as join6, resolve as resolve4 } from "path";
1022
+ import { basename as basename2, join as join6, resolve as resolve4 } from "path";
966
1023
  var MAX_JSONL_HEAD_BYTES = 64 * 1024;
967
1024
  var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
968
1025
  function flyspaceRoot() {
@@ -986,7 +1043,7 @@ function claudeLoggedIn() {
986
1043
  if (process.env["ANTHROPIC_API_KEY"]) return true;
987
1044
  if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
988
1045
  try {
989
- const raw = JSON.parse(readFileSync5(join6(homedir3(), ".claude.json"), "utf-8"));
1046
+ const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
990
1047
  if (raw.oauthAccount) return true;
991
1048
  } catch {
992
1049
  }
@@ -1027,7 +1084,7 @@ function jsonlFiles(dir, depth) {
1027
1084
  function cwdFromTranscript(file) {
1028
1085
  let head;
1029
1086
  try {
1030
- head = readFileSync5(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1087
+ head = readFileSync6(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1031
1088
  } catch {
1032
1089
  return null;
1033
1090
  }
@@ -1044,7 +1101,7 @@ function cwdFromTranscript(file) {
1044
1101
  }
1045
1102
  function codexTrustedPaths() {
1046
1103
  try {
1047
- const toml = readFileSync5(join6(codexDir(), "config.toml"), "utf-8");
1104
+ const toml = readFileSync6(join6(codexDir(), "config.toml"), "utf-8");
1048
1105
  return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
1049
1106
  } catch {
1050
1107
  return [];
@@ -1111,7 +1168,7 @@ function scanOnboarding() {
1111
1168
  } else {
1112
1169
  byPath.set(path, {
1113
1170
  path,
1114
- name: basename(path),
1171
+ name: basename2(path),
1115
1172
  sources: [source],
1116
1173
  lastUsed: mtime,
1117
1174
  sessions,
@@ -1514,7 +1571,7 @@ async function runAuthCmd(args) {
1514
1571
  }
1515
1572
 
1516
1573
  // src/version.ts
1517
- var CLI_VERSION = true ? "0.1.7" : null;
1574
+ var CLI_VERSION = true ? "0.1.10" : null;
1518
1575
  function isOlderVersion(a, b) {
1519
1576
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1520
1577
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -1559,15 +1616,16 @@ async function servedWorkspaces() {
1559
1616
  return bound.workspaces;
1560
1617
  }
1561
1618
  function checkLatestVersion() {
1562
- if (!CLI_VERSION) return;
1619
+ if (!CLI_VERSION) return true;
1563
1620
  let latest = "";
1564
1621
  try {
1565
1622
  latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
1566
1623
  } catch {
1567
- return;
1624
+ return true;
1568
1625
  }
1569
- if (!latest || !isOlderVersion(CLI_VERSION, latest)) return;
1626
+ if (!latest || !isOlderVersion(CLI_VERSION, latest)) return true;
1570
1627
  bad(`flyspace ${CLI_VERSION} is out of date (latest ${latest}) ${DIM2}run: flyspace upgrade${RESET2}`);
1628
+ return false;
1571
1629
  }
1572
1630
  function loginArgs(args) {
1573
1631
  const out = [];
@@ -1633,7 +1691,12 @@ ${BOLD2}Flyspace Setup${RESET2}
1633
1691
  }
1634
1692
  let hostRunning = false;
1635
1693
  if (args.includes("--start-host") && workspaces.length) {
1636
- if (!isHostServiceInstalled()) {
1694
+ for (const slug of workspaces) {
1695
+ if (!serviceBinaryMissing(profileServiceFile(slug))) continue;
1696
+ await runWithProfile(slug, runInstall);
1697
+ ok(`${slug} scheduler reinstalled on the new CLI path`);
1698
+ }
1699
+ if (!isHostServiceInstalled() || serviceBinaryMissing(hostServiceFile())) {
1637
1700
  hostRunning = await installHostService();
1638
1701
  if (hostRunning) ok(`Host running in the background ${DIM2}logs: ${tilde2(paths.LOG_DIR)}/host.log${RESET2}`);
1639
1702
  } else {
@@ -1657,10 +1720,138 @@ ${BOLD2}Flyspace Setup${RESET2}
1657
1720
  if (!workspaces.length) process.exitCode = 1;
1658
1721
  }
1659
1722
 
1723
+ // src/commands/doctor.ts
1724
+ import { execFile } from "child_process";
1725
+ import { tmpdir } from "os";
1726
+ import { promisify } from "util";
1727
+ var execAsync = promisify(execFile);
1728
+ var exec = (cmd, args, opts) => {
1729
+ const run3 = execAsync(cmd, args, opts);
1730
+ run3.child.stdin?.end();
1731
+ return run3;
1732
+ };
1733
+ var BOLD3 = "\x1B[1m";
1734
+ var DIM3 = "\x1B[2m";
1735
+ var GREEN3 = "\x1B[32m";
1736
+ var YELLOW3 = "\x1B[33m";
1737
+ var RESET3 = "\x1B[0m";
1738
+ var HELP3 = `
1739
+ Usage: flyspace doctor
1740
+
1741
+ Checks this Host end to end: CLI version, server reachability, device login,
1742
+ a live turn with each installed agent CLI, and the background services.
1743
+ Exits non-zero when any check fails.
1744
+ `;
1745
+ var LOGIN_HINTS = {
1746
+ claude: "claude auth login",
1747
+ codex: "codex login"
1748
+ };
1749
+ var tilde3 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
1750
+ var failed = false;
1751
+ var ok2 = (msg) => console.log(` ${GREEN3}\u2713${RESET3} ${msg}`);
1752
+ var bad2 = (msg, hint) => {
1753
+ failed = true;
1754
+ console.log(` ${YELLOW3}\u2717${RESET3} ${msg}${hint ? ` ${DIM3}${hint}${RESET3}` : ""}`);
1755
+ };
1756
+ function errorLine(err) {
1757
+ const e = err;
1758
+ if (e.killed) return "timed out after 90s";
1759
+ const lines = `${e.stdout ?? ""}
1760
+ ${e.stderr ?? ""}`.split("\n").map((l) => l.trim()).filter(Boolean);
1761
+ const last = (re) => [...lines].reverse().find((l) => re.test(l));
1762
+ const line = last(/expired|unauthori[sz]ed|\b401\b|not logged in|authenticate/i) ?? last(/error|fail/i) ?? lines.at(-1) ?? e.message ?? "failed";
1763
+ return line.length > 160 ? `${line.slice(0, 157)}...` : line;
1764
+ }
1765
+ async function probe(id) {
1766
+ const env = { ...process.env };
1767
+ const opts = { cwd: tmpdir(), timeout: 9e4, encoding: "utf-8" };
1768
+ try {
1769
+ if (id === "claude") {
1770
+ const dir = pinnedClaudeConfigDir(process.env["CLAUDE_CONFIG_DIR"]);
1771
+ if (dir) env["CLAUDE_CONFIG_DIR"] = dir;
1772
+ else delete env["CLAUDE_CONFIG_DIR"];
1773
+ await exec("claude", ["-p", "Reply with OK", "--model", "haiku", "--max-turns", "1"], { ...opts, env });
1774
+ } else {
1775
+ await exec("codex", ["exec", "--skip-git-repo-check", "-s", "read-only", "Reply with OK"], { ...opts, env });
1776
+ }
1777
+ return null;
1778
+ } catch (err) {
1779
+ return errorLine(err);
1780
+ }
1781
+ }
1782
+ async function serverReachable() {
1783
+ try {
1784
+ const res = await fetch(`${apiUrl()}/api/health`, { signal: AbortSignal.timeout(1e4) });
1785
+ return res.ok ? null : `HTTP ${res.status}`;
1786
+ } catch (err) {
1787
+ return err.cause ? String(err.cause) : err.message;
1788
+ }
1789
+ }
1790
+ function checkService(name, installed, file, running, fix) {
1791
+ if (!installed) bad2(`${name} not installed`, `run: ${fix}`);
1792
+ else if (serviceBinaryMissing(file)) bad2(`${name} points at a CLI that no longer exists`, `run: ${fix}`);
1793
+ else if (!running) bad2(`${name} not running`, `logs: ${tilde3(paths.LOG_DIR)}`);
1794
+ else ok2(`${name} running`);
1795
+ }
1796
+ async function runDoctor(args) {
1797
+ if (args.includes("-h") || args.includes("--help")) return void console.log(HELP3);
1798
+ failed = false;
1799
+ console.log(`
1800
+ ${BOLD3}Flyspace Doctor${RESET3}
1801
+ `);
1802
+ if (checkLatestVersion()) ok2(`flyspace ${CLI_VERSION || "(dev build)"}`);
1803
+ else failed = true;
1804
+ const serverError = await serverReachable();
1805
+ if (serverError) bad2(`Can't reach ${apiUrl()}: ${serverError}`, "check the network or DNS");
1806
+ else ok2(`Server ${apiUrl()} reachable`);
1807
+ const creds = loadCredentials();
1808
+ let workspaces = [];
1809
+ if (!creds) {
1810
+ bad2("This device is not logged in", "run: flyspace setup");
1811
+ } else {
1812
+ const bound = await boundWorkspaces();
1813
+ workspaces = bound.workspaces;
1814
+ ok2(`Logged in as ${creds.userEmail} (device ${creds.deviceName})`);
1815
+ if (!bound.fresh) bad2("Couldn't refresh the device session; showing the workspaces saved at login");
1816
+ if (workspaces.length) ok2(`Serving ${workspaces.join(", ")}`);
1817
+ else bad2("Not serving any workspace", "a workspace admin has to add this Host");
1818
+ }
1819
+ const runners = detectRunners().filter((r) => r.installed);
1820
+ if (!runners.length) bad2("Neither claude nor codex is installed", "npm install -g @anthropic-ai/claude-code");
1821
+ const results = await Promise.all(runners.map((r) => probe(r.id)));
1822
+ runners.forEach((r, i) => {
1823
+ const error2 = results[i];
1824
+ if (error2) bad2(`${r.id} can't run a turn: ${error2}`, `run: ${LOGIN_HINTS[r.id]}`);
1825
+ else ok2(`${r.id} answered a test turn`);
1826
+ });
1827
+ if (workspaces.length) {
1828
+ checkService(
1829
+ "Host service",
1830
+ isHostServiceInstalled(),
1831
+ hostServiceFile(),
1832
+ isHostServiceRunning(),
1833
+ "flyspace setup --start-host"
1834
+ );
1835
+ for (const slug of workspaces) {
1836
+ checkService(
1837
+ `${slug} scheduler`,
1838
+ isServiceInstalled(slug),
1839
+ profileServiceFile(slug),
1840
+ isServiceRunning(slug),
1841
+ "flyspace setup --start-host"
1842
+ );
1843
+ }
1844
+ }
1845
+ console.log(failed ? "" : `
1846
+ ${GREEN3}All good.${RESET3}
1847
+ `);
1848
+ if (failed) process.exitCode = 1;
1849
+ }
1850
+
1660
1851
  // src/commands/uninstall.ts
1661
1852
  import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
1662
1853
  import { execSync as execSync5 } from "child_process";
1663
- import { basename as basename2, join as join8 } from "path";
1854
+ import { basename as basename3, join as join8 } from "path";
1664
1855
  import { homedir as homedir4, platform as platform5 } from "os";
1665
1856
  function log2(msg) {
1666
1857
  console.log(`
@@ -1689,7 +1880,7 @@ async function runUninstall() {
1689
1880
  }
1690
1881
  try {
1691
1882
  unlinkSync2(plist);
1692
- removed.push(basename2(plist));
1883
+ removed.push(basename3(plist));
1693
1884
  } catch {
1694
1885
  }
1695
1886
  }
@@ -1722,7 +1913,6 @@ async function runUninstall() {
1722
1913
 
1723
1914
  // src/commands/profiles-cmd.ts
1724
1915
  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
1916
  var USAGE = `
1727
1917
  Usage: flyspace profiles [list]
1728
1918
  flyspace profiles remove <name>
@@ -1743,7 +1933,7 @@ async function listCmd() {
1743
1933
  console.log("\n Profiles:\n");
1744
1934
  console.log(` ${"NAME".padEnd(24)} ${"BOUND HERE".padEnd(12)} SCHEDULER`);
1745
1935
  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";
1936
+ const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" : isServiceRunning2(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
1747
1937
  const here = bound === null ? "-".padEnd(12) : bound.includes(profile) ? "yes".padEnd(12) : `\x1B[33m${"no".padEnd(12)}\x1B[0m`;
1748
1938
  console.log(` ${profile.padEnd(24)} ${here} ${scheduler}`);
1749
1939
  }
@@ -1752,10 +1942,7 @@ async function listCmd() {
1752
1942
  if (bound?.length === 0) {
1753
1943
  console.log("\n This Host is not bound to any workspace, so no scheduler runs here. Run: flyspace setup");
1754
1944
  }
1755
- if (hasSupabaseCredentials()) {
1756
- console.log(`
1757
- ${OPERATOR_NOTE}`);
1758
- } else if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
1945
+ if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
1759
1946
  console.log("\n Profiles not bound here run nothing on this machine. Remove one with: flyspace profiles remove <name>");
1760
1947
  }
1761
1948
  console.log("");
@@ -1770,10 +1957,6 @@ async function removeCmd(name) {
1770
1957
  console.error(` No local profile named "${name}".`);
1771
1958
  process.exit(1);
1772
1959
  }
1773
- if (hasSupabaseCredentials()) {
1774
- console.error(` ${OPERATOR_NOTE} Removing a profile would stop that workspace's schedules.`);
1775
- process.exit(1);
1776
- }
1777
1960
  const bound = await boundHere();
1778
1961
  if (bound === null) {
1779
1962
  console.error(" This machine has no device login, so it serves every workspace; removing a profile would be undone.");
@@ -1803,6 +1986,8 @@ async function runProfiles(args = []) {
1803
1986
 
1804
1987
  // src/commands/upgrade.ts
1805
1988
  import { execSync as execSync6 } from "child_process";
1989
+ import { realpathSync as realpathSync2 } from "fs";
1990
+ import { join as join9 } from "path";
1806
1991
  function restartInstalledProfiles() {
1807
1992
  if (isHostServiceInstalled()) {
1808
1993
  console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
@@ -1822,19 +2007,34 @@ Restarting ${installed.length} instance(s)...`);
1822
2007
  }
1823
2008
  }
1824
2009
  }
2010
+ function npmInstallCommand() {
2011
+ let prefix;
2012
+ try {
2013
+ const entry = realpathSync2(process.argv[1] ?? "");
2014
+ const at = entry.indexOf(`/lib/node_modules/${"flyspace-cli"}/`);
2015
+ if (at > 0) prefix = entry.slice(0, at);
2016
+ } catch {
2017
+ }
2018
+ const global = `-g${prefix ? ` --prefix "${prefix}"` : ""}`;
2019
+ return `npm install ${global} --ignore-scripts ${"flyspace-cli"}@latest && npm rebuild ${global} better-sqlite3`;
2020
+ }
2021
+ function npmEnv() {
2022
+ const home = flyspaceHome();
2023
+ return home ? { ...process.env, PATH: `${join9(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2024
+ }
1825
2025
  async function runUpgrade() {
1826
- const current = "0.1.7";
2026
+ const current = "0.1.10";
1827
2027
  console.log(`Current version: ${current}`);
1828
2028
  console.log("Checking for updates...\n");
1829
2029
  try {
1830
- const latest = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8" }).trim();
2030
+ const latest = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", env: npmEnv() }).trim();
1831
2031
  if (latest === current) {
1832
2032
  console.log(`Already on the latest version (${current}).`);
1833
2033
  return;
1834
2034
  }
1835
2035
  console.log(`New version available: ${latest}`);
1836
2036
  console.log("Upgrading...\n");
1837
- execSync6(`npm install -g ${"flyspace-cli"}@latest`, { stdio: "inherit" });
2037
+ execSync6(npmInstallCommand(), { stdio: "inherit", env: npmEnv() });
1838
2038
  console.log(`
1839
2039
  Upgraded from ${current} to ${latest}.`);
1840
2040
  restartInstalledProfiles();
@@ -1843,7 +2043,7 @@ Upgraded from ${current} to ${latest}.`);
1843
2043
  } catch (err) {
1844
2044
  console.error("Upgrade failed:", err.message);
1845
2045
  console.error(`
1846
- You can upgrade manually with: npm install -g ${"flyspace-cli"}@latest`);
2046
+ You can upgrade manually with: ${npmInstallCommand()}`);
1847
2047
  process.exit(1);
1848
2048
  }
1849
2049
  }
@@ -2195,7 +2395,7 @@ async function runAutomationCmd(args) {
2195
2395
 
2196
2396
  // src/commands/usage.ts
2197
2397
  import { existsSync as existsSync9 } from "fs";
2198
- var HELP3 = `
2398
+ var HELP4 = `
2199
2399
  Usage: flyspace usage [options]
2200
2400
 
2201
2401
  Show agent token usage and notional cost for this workspace.
@@ -2317,7 +2517,7 @@ function printExplanation(explained) {
2317
2517
  async function runUsage(args) {
2318
2518
  const parsed = parseArgs(args);
2319
2519
  if (parsed === "help") {
2320
- console.log(HELP3);
2520
+ console.log(HELP4);
2321
2521
  return;
2322
2522
  }
2323
2523
  const { days, by, explain, json } = parsed;
@@ -2437,11 +2637,11 @@ async function runRestart() {
2437
2637
  // src/commands/logs.ts
2438
2638
  import { spawn as spawn4 } from "child_process";
2439
2639
  import { existsSync as existsSync10 } from "fs";
2440
- import { join as join9 } from "path";
2640
+ import { join as join10 } from "path";
2441
2641
  async function runLogs(args) {
2442
2642
  const showErrors = args.includes("--error") || args.includes("-e");
2443
2643
  const filename = showErrors ? "custie-error.log" : "custie.log";
2444
- const logFile = join9(paths.LOG_DIR, filename);
2644
+ const logFile = join10(paths.LOG_DIR, filename);
2445
2645
  if (!existsSync10(logFile)) {
2446
2646
  console.error(`Log file not found: ${logFile}`);
2447
2647
  console.error("Is the service installed? Run `flyspace install` first.");
@@ -2463,9 +2663,9 @@ async function runLogs(args) {
2463
2663
 
2464
2664
  // src/commands/slack.ts
2465
2665
  import { execFileSync as execFileSync4 } from "child_process";
2466
- import { existsSync as existsSync27, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
2666
+ import { existsSync as existsSync27, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
2467
2667
  import { resolve as resolve7 } from "path";
2468
- import { tmpdir as tmpdir3, homedir as homedir9 } from "os";
2668
+ import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
2469
2669
  import { input, confirm as confirm3, select, checkbox } from "@inquirer/prompts";
2470
2670
  import { WebClient as WebClient3 } from "@slack/web-api";
2471
2671
 
@@ -2520,7 +2720,7 @@ async function emailForToken(token, type) {
2520
2720
 
2521
2721
  // src/dashboard/profile-config.ts
2522
2722
  import { homedir as homedir5 } from "os";
2523
- import { join as join10 } from "path";
2723
+ import { join as join11 } from "path";
2524
2724
 
2525
2725
  // src/dashboard/profile-config-cache.ts
2526
2726
  var cache = /* @__PURE__ */ new Map();
@@ -2539,7 +2739,7 @@ function profileEnv(key) {
2539
2739
  function profileClaudeConfigDir() {
2540
2740
  const config = cachedProfileConfig();
2541
2741
  if (!config) return process.env["CLAUDE_CONFIG_DIR"] || void 0;
2542
- return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join10(homedir5(), ".claude");
2742
+ return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join11(homedir5(), ".claude");
2543
2743
  }
2544
2744
  function profileOauthToken() {
2545
2745
  const env = cachedProfileConfig()?.env ?? {};
@@ -3993,8 +4193,8 @@ async function deliverPasswordResetLink(email, redirectTo) {
3993
4193
  // src/dashboard/server.ts
3994
4194
  import { existsSync as existsSync26 } from "fs";
3995
4195
  import { createServer } from "net";
3996
- import { join as join22 } from "path";
3997
- import { Hono as Hono32 } from "hono";
4196
+ import { join as join23 } from "path";
4197
+ import { Hono as Hono33 } from "hono";
3998
4198
  import { serve } from "@hono/node-server";
3999
4199
  import { serveStatic } from "@hono/node-server/serve-static";
4000
4200
 
@@ -4003,6 +4203,10 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as w
4003
4203
  async function boundWorkspaces2() {
4004
4204
  return (await boundWorkspaces())?.workspaces ?? null;
4005
4205
  }
4206
+ async function servedWorkspaces2() {
4207
+ const bound = await boundWorkspaces2();
4208
+ return new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4209
+ }
4006
4210
  async function reconcileProfile(profile) {
4007
4211
  return runWithProfile(profile, async () => {
4008
4212
  const log5 = [];
@@ -4028,8 +4232,7 @@ async function reconcileProfile(profile) {
4028
4232
  });
4029
4233
  }
4030
4234
  async function reconcileAll() {
4031
- const bound = hasSupabaseCredentials() ? null : await boundWorkspaces2();
4032
- const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4235
+ const slugs = await servedWorkspaces2();
4033
4236
  const results = /* @__PURE__ */ new Map();
4034
4237
  for (const slug of slugs) {
4035
4238
  try {
@@ -4042,8 +4245,7 @@ async function reconcileAll() {
4042
4245
  return results;
4043
4246
  }
4044
4247
  async function ensureServices() {
4045
- const bound = await boundWorkspaces2();
4046
- const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4248
+ const slugs = await servedWorkspaces2();
4047
4249
  slugs.delete(DEFAULT_PROFILE);
4048
4250
  const installed = [];
4049
4251
  for (const slug of slugs) {
@@ -4192,11 +4394,11 @@ var requireDocsAccess = async (c, next) => {
4192
4394
  };
4193
4395
 
4194
4396
  // src/dashboard/api.ts
4195
- import { Hono as Hono29 } from "hono";
4397
+ import { Hono as Hono30 } from "hono";
4196
4398
 
4197
4399
  // src/dashboard/public-share-routes.ts
4198
- import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
4199
- import { join as join12 } from "path";
4400
+ import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
4401
+ import { join as join13 } from "path";
4200
4402
  import { Hono as Hono3 } from "hono";
4201
4403
  import { GetObjectCommand } from "@aws-sdk/client-s3";
4202
4404
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -4336,15 +4538,15 @@ async function createDocShare(input2) {
4336
4538
  if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
4337
4539
  return rowToShare(data);
4338
4540
  }
4339
- async function updateDocShareSnapshot(id, snapshot2, features) {
4541
+ async function updateDocShareSnapshot(id, snapshot3, features) {
4340
4542
  const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
4341
- title: snapshot2.title,
4342
- content: snapshot2.content,
4343
- asset_keys: snapshot2.assetKeys,
4543
+ title: snapshot3.title,
4544
+ content: snapshot3.content,
4545
+ asset_keys: snapshot3.assetKeys,
4344
4546
  // Re-detected with the snapshot: a re-publish can carry a rewritten
4345
4547
  // article, and one written in another language than last time is exactly
4346
4548
  // the case a stale value would get wrong.
4347
- source_lang: snapshot2.sourceLang,
4549
+ source_lang: snapshot3.sourceLang,
4348
4550
  audio_enabled: features.audioEnabled,
4349
4551
  ask_enabled: features.askEnabled,
4350
4552
  social_enabled: features.socialEnabled,
@@ -4661,12 +4863,12 @@ function injectShareHead(html, head) {
4661
4863
 
4662
4864
  // src/dashboard/app-icon.ts
4663
4865
  import { createHash as createHash2 } from "crypto";
4664
- import { readFileSync as readFileSync6 } from "fs";
4665
- import { join as join11 } from "path";
4866
+ import { readFileSync as readFileSync7 } from "fs";
4867
+ import { join as join12 } from "path";
4666
4868
  import sharp from "sharp";
4667
4869
  var PLATE = "#0A0A0F";
4668
- var MARK_FILE = join11(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
4669
- var MARK_SVG_FILE = join11(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
4870
+ var MARK_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
4871
+ var MARK_SVG_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
4670
4872
  var LAYOUT = {
4671
4873
  standard: { logo: 0.7, logoCx: 0.5, logoCy: 0.48, badge: 0.27, badgeCx: 0.22, badgeCy: 0.76 },
4672
4874
  maskable: { logo: 0.56, logoCx: 0.5, logoCy: 0.48, badge: 0.23, badgeCx: 0.33, badgeCy: 0.66 }
@@ -4689,7 +4891,7 @@ function roundedPlate(size, fill = PLATE) {
4689
4891
  }
4690
4892
  async function renderBadge(box) {
4691
4893
  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();
4894
+ const raster = await sharp(readFileSync7(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
4693
4895
  return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
4694
4896
  }
4695
4897
  async function sampleBackground(logo) {
@@ -4744,7 +4946,7 @@ async function renderAppIcon({
4744
4946
  if (hit) return hit;
4745
4947
  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
4948
  // 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());
4949
+ (transparent ? await renderBadge(size) : await sharp(readFileSync7(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
4748
4950
  cache3.set(key, out);
4749
4951
  return out;
4750
4952
  }
@@ -5962,7 +6164,7 @@ var shellCache = null;
5962
6164
  function readShell(path) {
5963
6165
  const { mtimeMs } = statSync3(path);
5964
6166
  if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
5965
- const html = readFileSync7(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6167
+ const html = readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
5966
6168
  shellCache = { mtimeMs, html };
5967
6169
  return html;
5968
6170
  }
@@ -5982,17 +6184,17 @@ function serveBuildFile(c, webDist, dir, name) {
5982
6184
  if (!type || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes("..")) {
5983
6185
  return c.text("not found", 404);
5984
6186
  }
5985
- const path = join12(webDist, dir, name);
6187
+ const path = join13(webDist, dir, name);
5986
6188
  if (!existsSync12(path)) return c.text("not found", 404);
5987
6189
  c.header("Content-Type", type);
5988
6190
  c.header("Cache-Control", "public, max-age=31536000, immutable");
5989
6191
  if (ext === ".css") {
5990
- return c.body(readFileSync7(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6192
+ return c.body(readFileSync8(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
5991
6193
  }
5992
6194
  if (ext === ".js") {
5993
- return c.body(readFileSync7(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6195
+ return c.body(readFileSync8(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
5994
6196
  }
5995
- return c.body(new Uint8Array(readFileSync7(path)));
6197
+ return c.body(new Uint8Array(readFileSync8(path)));
5996
6198
  }
5997
6199
  var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
5998
6200
  function servePublishedHtml(c, share) {
@@ -6128,7 +6330,7 @@ ${analytics}` : head);
6128
6330
  const requested = c.req.param("lang");
6129
6331
  const lang = canonicalLanguage(requested);
6130
6332
  if (!isTranslatableLanguage(lang)) return next();
6131
- const shell = join12(webDist, "index.html");
6333
+ const shell = join13(webDist, "index.html");
6132
6334
  if (!existsSync12(shell)) return next();
6133
6335
  const share = await getDocShare(c.req.param("id"));
6134
6336
  if (!share) return next();
@@ -6164,7 +6366,7 @@ ${analytics}` : head);
6164
6366
  const share = await getDocShare(c.req.param("id"));
6165
6367
  if (!share) return next();
6166
6368
  if (share.kind === "html") return servePublishedHtml(c, share);
6167
- const shell = join12(webDist, "index.html");
6369
+ const shell = join13(webDist, "index.html");
6168
6370
  if (!existsSync12(shell)) return next();
6169
6371
  return readerPage(
6170
6372
  c,
@@ -6754,9 +6956,49 @@ function createAdminRouter() {
6754
6956
  return admin;
6755
6957
  }
6756
6958
 
6959
+ // src/dashboard/agent-identities.ts
6960
+ var BOT_LABEL = "Workspace Agent";
6961
+ var BOT_DM_KEY = "claude";
6962
+ async function resolveAgentIdentities() {
6963
+ const settings = loadDashboardSettings();
6964
+ const { cwd: defaultCwd } = resolveChannelConfig("");
6965
+ const main2 = [
6966
+ {
6967
+ name: BOT_DM_KEY,
6968
+ label: BOT_LABEL,
6969
+ kind: "main",
6970
+ cwd: defaultCwd,
6971
+ model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
6972
+ ...await loadBotIdentity()
6973
+ }
6974
+ ];
6975
+ return Promise.all(
6976
+ main2.map(async ({ cwd, ...agent }) => {
6977
+ const dm = await ensureAgentDmChannel(agent.name, {
6978
+ name: agent.displayName ?? agent.label,
6979
+ cwd
6980
+ });
6981
+ return { ...agent, channelId: dm.id };
6982
+ })
6983
+ );
6984
+ }
6985
+ async function offlineAgentIdentities() {
6986
+ const dm = findChannelByAgentName(BOT_DM_KEY);
6987
+ if (!dm) return [];
6988
+ return [
6989
+ {
6990
+ name: BOT_DM_KEY,
6991
+ label: BOT_LABEL,
6992
+ kind: "main",
6993
+ channelId: dm.id,
6994
+ ...await loadBotIdentity()
6995
+ }
6996
+ ];
6997
+ }
6998
+
6757
6999
  // src/dashboard/host-api.ts
6758
- import { existsSync as existsSync21, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
6759
- import { join as join19 } from "path";
7000
+ import { existsSync as existsSync21, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
7001
+ import { join as join20 } from "path";
6760
7002
  import sharp3 from "sharp";
6761
7003
 
6762
7004
  // src/dashboard/channels-routes.ts
@@ -6792,6 +7034,11 @@ var featureFlags = {
6792
7034
  title: "Dictation",
6793
7035
  description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
6794
7036
  stage: "dev"
7037
+ },
7038
+ "agent-memory": {
7039
+ title: "Agent memory",
7040
+ description: "Channel memory the agent reads every turn, saved by the agent and picked up from idle threads, with a Memory tab and an undo chip. While on, Claude's built-in auto memory is switched off so the two can be compared; turning this off keeps every memory.",
7041
+ stage: "beta"
6795
7042
  }
6796
7043
  };
6797
7044
  var featureFlagIds = Object.keys(featureFlags);
@@ -6817,6 +7064,11 @@ async function listEnabledFeatureFlags(workspaceId) {
6817
7064
  if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
6818
7065
  return (data ?? []).map((r) => r.flag_id);
6819
7066
  }
7067
+ async function listEnabledFeatureFlagsBySlug(slug) {
7068
+ const { data, error: error2 } = await getSupabaseClient().from("workspaces").select("id").eq("slug", slug).maybeSingle();
7069
+ if (error2) throw new Error(`listEnabledFeatureFlagsBySlug(${slug}): ${error2.message}`);
7070
+ return data ? listEnabledFeatureFlags(data.id) : [];
7071
+ }
6820
7072
  async function enableFeatureFlag(workspaceId, flagId, userId) {
6821
7073
  const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
6822
7074
  { workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
@@ -6994,8 +7246,8 @@ function createChannelWriteRouter() {
6994
7246
  (id, position) => supabase.from("channels").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id)
6995
7247
  )
6996
7248
  );
6997
- const failed = results.find((r) => r.error);
6998
- if (failed?.error) return c.json({ error: failed.error.message }, 500);
7249
+ const failed2 = results.find((r) => r.error);
7250
+ if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
6999
7251
  await refreshChannelRegistry();
7000
7252
  return c.json({ ok: true });
7001
7253
  });
@@ -7053,20 +7305,20 @@ import {
7053
7305
  openSync,
7054
7306
  readSync,
7055
7307
  readdirSync as readdirSync4,
7056
- readFileSync as readFileSync10,
7057
- realpathSync,
7308
+ readFileSync as readFileSync11,
7309
+ realpathSync as realpathSync3,
7058
7310
  renameSync,
7059
7311
  rmSync as rmSync5,
7060
7312
  statSync as statSync5,
7061
7313
  writeFileSync as writeFileSync7
7062
7314
  } from "fs";
7063
- import { basename as basename5, dirname as dirname4, extname, join as join17, relative, resolve as resolve5, sep } from "path";
7315
+ import { basename as basename6, dirname as dirname5, extname, join as join18, relative, resolve as resolve5, sep } from "path";
7064
7316
  import { Hono as Hono10 } from "hono";
7065
7317
  import { streamSSE } from "hono/streaming";
7066
7318
 
7067
7319
  // src/dashboard/docs-watcher.ts
7068
7320
  import { existsSync as existsSync14, watch } from "fs";
7069
- import { basename as basename3, join as join13 } from "path";
7321
+ import { basename as basename4, join as join14 } from "path";
7070
7322
  var DEBOUNCE_MS = 200;
7071
7323
  var MAX_PENDING = 200;
7072
7324
  var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
@@ -7102,12 +7354,12 @@ function flush(cwd) {
7102
7354
  function watchDocs(cwd, listener) {
7103
7355
  let entry = entries.get(cwd);
7104
7356
  if (!entry) {
7105
- const root = basename3(cwd);
7357
+ const root = basename4(cwd);
7106
7358
  const watcher = watch(cwd, { recursive: true, persistent: false }, (_event, filename) => {
7107
7359
  const current = entries.get(cwd);
7108
7360
  if (!current || filename === null) return;
7109
7361
  const rel = filename.split("\\").join("/");
7110
- if (rel === root && !existsSync14(join13(cwd, rel))) return;
7362
+ if (rel === root && !existsSync14(join14(cwd, rel))) return;
7111
7363
  if (isIgnoredDocPath(rel)) return;
7112
7364
  if (current.pending.size < MAX_PENDING) current.pending.add(rel);
7113
7365
  if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
@@ -7344,15 +7596,15 @@ async function readOrQueueDocAudio(input2) {
7344
7596
  }
7345
7597
 
7346
7598
  // src/dashboard/artifact-export.ts
7347
- import { readFileSync as readFileSync9 } from "fs";
7599
+ import { readFileSync as readFileSync10 } from "fs";
7348
7600
 
7349
7601
  // src/dashboard/chat-agent.ts
7350
7602
  import { homedir as homedir7 } from "os";
7351
7603
 
7352
7604
  // src/mcp/config.ts
7353
7605
  import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
7354
- import { tmpdir } from "os";
7355
- import { join as join14 } from "path";
7606
+ import { tmpdir as tmpdir2 } from "os";
7607
+ import { join as join15 } from "path";
7356
7608
 
7357
7609
  // src/mcp/crypto.ts
7358
7610
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
@@ -8038,8 +8290,8 @@ async function buildMcpConfig(profile, connectionIds) {
8038
8290
  return { config, skipped };
8039
8291
  }
8040
8292
  function writeMcpConfigFile(config) {
8041
- const dir = mkdtempSync(join14(tmpdir(), "flyspace-mcp-"));
8042
- const path = join14(dir, "mcp-config.json");
8293
+ const dir = mkdtempSync(join15(tmpdir2(), "flyspace-mcp-"));
8294
+ const path = join15(dir, "mcp-config.json");
8043
8295
  writeFileSync6(path, JSON.stringify(config), { mode: 384 });
8044
8296
  return {
8045
8297
  path,
@@ -8060,22 +8312,22 @@ async function withMcpConfig(profile, connectionIds, fn) {
8060
8312
  }
8061
8313
 
8062
8314
  // src/claude/agents.ts
8063
- import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
8315
+ import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
8064
8316
  import { homedir as homedir6 } from "os";
8065
- import { basename as basename4, join as join15 } from "path";
8317
+ import { basename as basename5, join as join16 } from "path";
8066
8318
  import yaml from "js-yaml";
8067
8319
  function defaultConfigDir() {
8068
- return join15(homedir6(), ".claude");
8320
+ return join16(homedir6(), ".claude");
8069
8321
  }
8070
8322
  function expandHome(p) {
8071
8323
  if (p === "~") return homedir6();
8072
- if (p.startsWith("~/")) return join15(homedir6(), p.slice(2));
8324
+ if (p.startsWith("~/")) return join16(homedir6(), p.slice(2));
8073
8325
  return p;
8074
8326
  }
8075
8327
  function parseFrontmatter(file) {
8076
8328
  let content;
8077
8329
  try {
8078
- content = readFileSync8(file, "utf-8");
8330
+ content = readFileSync9(file, "utf-8");
8079
8331
  } catch {
8080
8332
  return {};
8081
8333
  }
@@ -8099,17 +8351,17 @@ function collectFrom(root, source) {
8099
8351
  return [];
8100
8352
  }
8101
8353
  return entries2.map((file) => {
8102
- const path = join15(root, file);
8354
+ const path = join16(root, file);
8103
8355
  const fm = parseFrontmatter(path);
8104
- const name = fm.name?.trim() || basename4(file, ".md");
8356
+ const name = fm.name?.trim() || basename5(file, ".md");
8105
8357
  return { name, description: (fm.description ?? "").trim(), source, path };
8106
8358
  });
8107
8359
  }
8108
8360
  function listAgents(configDir2, cwd) {
8109
8361
  const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
8110
8362
  const collected = [
8111
- ...collectFrom(join15(cwd, ".claude", "agents"), "project"),
8112
- ...collectFrom(join15(base, "agents"), "user")
8363
+ ...collectFrom(join16(cwd, ".claude", "agents"), "project"),
8364
+ ...collectFrom(join16(base, "agents"), "user")
8113
8365
  ];
8114
8366
  const byName = /* @__PURE__ */ new Map();
8115
8367
  for (const agent of collected) {
@@ -8121,16 +8373,241 @@ function readAgentPersona(configDir2, cwd, name) {
8121
8373
  const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
8122
8374
  if (!agent) return void 0;
8123
8375
  try {
8124
- const content = readFileSync8(agent.path, "utf-8");
8376
+ const content = readFileSync9(agent.path, "utf-8");
8125
8377
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
8126
8378
  } catch {
8127
8379
  return void 0;
8128
8380
  }
8129
8381
  }
8130
8382
 
8383
+ // src/store/memory-store.ts
8384
+ var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
8385
+ var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8386
+ var DESCRIPTION_MAX = 200;
8387
+ var BODY_MAX = 4e3;
8388
+ var MEMORY_INDEX_MAX_CHARS = 8e3;
8389
+ function isMemoryType(value) {
8390
+ return MEMORY_TYPES.includes(value);
8391
+ }
8392
+ function validateMemoryInput(input2) {
8393
+ if (!NAME_RE.test(input2.name))
8394
+ throw new Error("name must be a kebab-case slug, e.g. deploy-flow");
8395
+ if (!isMemoryType(input2.type)) throw new Error(`type must be one of ${MEMORY_TYPES.join(", ")}`);
8396
+ const description = input2.description.trim();
8397
+ if (!description || description.includes("\n") || description.length > DESCRIPTION_MAX) {
8398
+ throw new Error(`description must be one line of at most ${DESCRIPTION_MAX} characters`);
8399
+ }
8400
+ const body = input2.body.trim();
8401
+ if (!body || body.length > BODY_MAX) {
8402
+ throw new Error(`body must be 1 to ${BODY_MAX} characters`);
8403
+ }
8404
+ }
8405
+ function rowToMemory(row) {
8406
+ return {
8407
+ id: row["id"],
8408
+ channelId: row["channel_id"],
8409
+ name: row["name"],
8410
+ description: row["description"],
8411
+ type: row["type"],
8412
+ body: row["body"],
8413
+ sourceThreadId: row["source_thread_id"] ?? null,
8414
+ createdBy: row["created_by"],
8415
+ createdAt: row["created_at"],
8416
+ updatedAt: row["updated_at"]
8417
+ };
8418
+ }
8419
+ async function listChannelMemories(channelId) {
8420
+ const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
8421
+ if (error2) throw new Error(`listChannelMemories(${channelId}): ${error2.message}`);
8422
+ return (data ?? []).map(rowToMemory);
8423
+ }
8424
+ async function getChannelMemory(channelId, name) {
8425
+ const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
8426
+ if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
8427
+ return data ? rowToMemory(data) : null;
8428
+ }
8429
+ async function saveChannelMemory(input2, actor) {
8430
+ validateMemoryInput(input2);
8431
+ const supabase = getSupabaseClient();
8432
+ const existing = await getChannelMemory(input2.channelId, input2.name);
8433
+ const fields = {
8434
+ description: input2.description.trim(),
8435
+ type: input2.type,
8436
+ body: input2.body.trim(),
8437
+ source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
8438
+ created_by: input2.createdBy
8439
+ };
8440
+ if (existing && sameContent(existing, fields)) {
8441
+ return { memory: existing, created: false, changed: false };
8442
+ }
8443
+ 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();
8444
+ if (error2)
8445
+ throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
8446
+ const memory = rowToMemory(data);
8447
+ await logMemoryEvent({
8448
+ memory,
8449
+ action: existing ? "update" : "create",
8450
+ before: existing ? snapshot(existing) : null,
8451
+ after: snapshot(memory),
8452
+ actor,
8453
+ threadId: input2.sourceThreadId ?? null
8454
+ });
8455
+ return { memory, created: !existing, changed: true };
8456
+ }
8457
+ async function updateChannelMemory(channelId, memoryId, patch, actor) {
8458
+ const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
8459
+ if (!existing) return null;
8460
+ const { memory } = await saveChannelMemory(
8461
+ {
8462
+ channelId,
8463
+ name: existing.name,
8464
+ description: patch.description ?? existing.description,
8465
+ type: patch.type ?? existing.type,
8466
+ body: patch.body ?? existing.body,
8467
+ createdBy: existing.createdBy
8468
+ },
8469
+ actor
8470
+ );
8471
+ return memory;
8472
+ }
8473
+ async function searchChannelMemories(channelId, query) {
8474
+ const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
8475
+ if (!words.length) return [];
8476
+ let request2 = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
8477
+ for (const word of words) {
8478
+ request2 = request2.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
8479
+ }
8480
+ const { data, error: error2 } = await request2.order("updated_at", { ascending: false }).limit(20);
8481
+ if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
8482
+ return (data ?? []).map(rowToMemory);
8483
+ }
8484
+ async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
8485
+ const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
8486
+ if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
8487
+ const deleted = (data ?? []).map(rowToMemory);
8488
+ for (const memory of deleted) {
8489
+ await logMemoryEvent({
8490
+ memory,
8491
+ action: "delete",
8492
+ before: snapshot(memory),
8493
+ after: null,
8494
+ actor,
8495
+ threadId: threadId ?? null
8496
+ });
8497
+ }
8498
+ return deleted.length > 0;
8499
+ }
8500
+ function snapshot(m) {
8501
+ return {
8502
+ description: m.description,
8503
+ type: m.type,
8504
+ body: m.body,
8505
+ sourceThreadId: m.sourceThreadId,
8506
+ createdBy: m.createdBy
8507
+ };
8508
+ }
8509
+ function sameContent(m, other) {
8510
+ return m.description === other.description && m.type === other.type && m.body === other.body;
8511
+ }
8512
+ async function logMemoryEvent(e) {
8513
+ const { error: error2 } = await getSupabaseClient().from("memory_events").insert({
8514
+ channel_id: e.memory.channelId,
8515
+ memory_id: e.action === "delete" ? null : e.memory.id,
8516
+ thread_id: e.threadId,
8517
+ name: e.memory.name,
8518
+ action: e.action,
8519
+ before: e.before,
8520
+ after: e.after,
8521
+ actor: e.actor
8522
+ });
8523
+ if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
8524
+ }
8525
+ function rowToEvent(row) {
8526
+ return {
8527
+ id: row["id"],
8528
+ channelId: row["channel_id"],
8529
+ memoryId: row["memory_id"] ?? null,
8530
+ threadId: row["thread_id"] ?? null,
8531
+ name: row["name"],
8532
+ action: row["action"],
8533
+ before: row["before"] ?? null,
8534
+ after: row["after"] ?? null,
8535
+ actor: row["actor"],
8536
+ createdAt: row["created_at"],
8537
+ undoneAt: row["undone_at"] ?? null
8538
+ };
8539
+ }
8540
+ async function listThreadMemoryEvents(channelId, threadId) {
8541
+ 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);
8542
+ if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
8543
+ return (data ?? []).map(rowToEvent);
8544
+ }
8545
+ async function undoMemoryEvent(channelId, eventId) {
8546
+ const supabase = getSupabaseClient();
8547
+ const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
8548
+ if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
8549
+ if (!data) return "not-found";
8550
+ const event = rowToEvent(data);
8551
+ if (event.undoneAt) return "already-undone";
8552
+ const current = await getChannelMemory(channelId, event.name);
8553
+ const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
8554
+ if (!unchangedSince) return "conflict";
8555
+ const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
8556
+ if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
8557
+ if (!(claimed.data ?? []).length) return "already-undone";
8558
+ const before = event.before;
8559
+ const restore = before ? {
8560
+ description: before.description,
8561
+ type: before.type,
8562
+ body: before.body,
8563
+ source_thread_id: before.sourceThreadId,
8564
+ created_by: before.createdBy
8565
+ } : null;
8566
+ 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 });
8567
+ if (writeError) {
8568
+ await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
8569
+ throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
8570
+ }
8571
+ return "undone";
8572
+ }
8573
+ function isUuid(value) {
8574
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
8575
+ }
8576
+ function formatMemoryPrompt(channelId, memories) {
8577
+ const lines = [];
8578
+ let used = 0;
8579
+ let omitted = 0;
8580
+ for (const m of memories) {
8581
+ const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
8582
+ if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
8583
+ omitted++;
8584
+ continue;
8585
+ }
8586
+ lines.push(line);
8587
+ used += line.length + 1;
8588
+ }
8589
+ const index = lines.length ? lines.join("\n") + (omitted ? `
8590
+ - (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
8591
+ return [
8592
+ `## Channel memory`,
8593
+ ``,
8594
+ `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.`,
8595
+ ``,
8596
+ `- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
8597
+ `- Search: \`flyspace memory search --channel ${channelId} <words>\``,
8598
+ `- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
8599
+ `- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
8600
+ ``,
8601
+ `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.`,
8602
+ ``,
8603
+ `Index:`,
8604
+ index
8605
+ ].join("\n");
8606
+ }
8607
+
8131
8608
  // src/dashboard/rollover-summary.ts
8132
8609
  import { spawn as spawn5 } from "child_process";
8133
- import { tmpdir as tmpdir2 } from "os";
8610
+ import { tmpdir as tmpdir3 } from "os";
8134
8611
  var HAIKU_MODEL = "haiku";
8135
8612
  var CJK_PATTERN = /[ -〿぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/;
8136
8613
  function estimateTokens(text) {
@@ -8193,7 +8670,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8193
8670
  // A stateless rewrite touches no files — tmpdir keeps it that way and
8194
8671
  // avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
8195
8672
  // unlike the channel's own cwd the main turn runs in.
8196
- cwd: tmpdir2(),
8673
+ cwd: tmpdir3(),
8197
8674
  env,
8198
8675
  stdio: ["ignore", "pipe", "pipe"]
8199
8676
  });
@@ -8396,7 +8873,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
8396
8873
 
8397
8874
  // src/dashboard/attachment-downloader.ts
8398
8875
  import { writeFile } from "fs/promises";
8399
- import { join as join16 } from "path";
8876
+ import { join as join17 } from "path";
8400
8877
  import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
8401
8878
  var debug = process.env["DEBUG"] === "true";
8402
8879
  var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
@@ -8443,7 +8920,7 @@ async function downloadOne(key) {
8443
8920
  if (bytes.length > sizeLimit) {
8444
8921
  return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
8445
8922
  }
8446
- const localPath = join16(paths.UPLOADS_DIR, lastSegment);
8923
+ const localPath = join17(paths.UPLOADS_DIR, lastSegment);
8447
8924
  await writeFile(localPath, bytes);
8448
8925
  if (debug)
8449
8926
  console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
@@ -8989,6 +9466,18 @@ var ChatAgent = class {
8989
9466
  thread.channel_id
8990
9467
  );
8991
9468
  if (docsAccess && docsAccess !== "write") channelConfig = { ...channelConfig, docsAccess };
9469
+ const agentMemory = await listEnabledFeatureFlagsBySlug(owningProfile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
9470
+ console.error("[chat-agent] feature flag lookup failed:", err.message);
9471
+ return false;
9472
+ });
9473
+ if (agentMemory) channelConfig = { ...channelConfig, disableAutoMemory: true };
9474
+ if (agentMemory && docsAccess !== "none" && isUuid(thread.channel_id)) {
9475
+ const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
9476
+ console.error("[chat-agent] memory lookup failed:", err.message);
9477
+ return void 0;
9478
+ });
9479
+ if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
9480
+ }
8992
9481
  const store = this.openSessionStore();
8993
9482
  try {
8994
9483
  const channelKey = `chat:${thread.channel_id}`;
@@ -9396,7 +9885,9 @@ ${msg.body}` : msg.body;
9396
9885
  signal,
9397
9886
  mcpConfigPath,
9398
9887
  ...usageContext ? { usageContext } : {},
9399
- ...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {}
9888
+ ...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {},
9889
+ ...channelConfig.memoryPrompt ? { memoryPrompt: channelConfig.memoryPrompt } : {},
9890
+ ...channelConfig.disableAutoMemory ? { disableAutoMemory: true } : {}
9400
9891
  },
9401
9892
  channelConfig.claudeConfigDir,
9402
9893
  sessionId,
@@ -9433,6 +9924,7 @@ ${msg.body}` : msg.body;
9433
9924
  channelConfig.prompt,
9434
9925
  resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
9435
9926
  ),
9927
+ channelConfig.memoryPrompt,
9436
9928
  docsAccessPromptNote(channelConfig.docsAccess)
9437
9929
  ].filter(Boolean).join("\n\n---\n\n"),
9438
9930
  // Codex has no per-tool deny list, so any restricted requester gets
@@ -9710,7 +10202,7 @@ async function exportDocAsArtifact(channelId, docPath) {
9710
10202
  );
9711
10203
  const abs = resolveDocPath(channelConfig.cwd, docPath);
9712
10204
  if (!abs) throw new Error(`doc not found: ${docPath}`);
9713
- const content = readFileSync9(abs, "utf8");
10205
+ const content = readFileSync10(abs, "utf8");
9714
10206
  const title = extractTitle(abs, "md");
9715
10207
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9716
10208
  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).
@@ -9925,9 +10417,9 @@ function readHead(absPath) {
9925
10417
  }
9926
10418
  }
9927
10419
  function extractTitle(absPath, kind) {
9928
- if (kind === "url" || kind === "excalidraw") return basename5(absPath);
10420
+ if (kind === "url" || kind === "excalidraw") return basename6(absPath);
9929
10421
  const head = readHead(absPath);
9930
- if (!head) return basename5(absPath);
10422
+ if (!head) return basename6(absPath);
9931
10423
  if (kind === "md") {
9932
10424
  const m = head.match(/^#{1,3}\s+(.+?)\s*$/m);
9933
10425
  if (m) return m[1].replace(/[*_`]/g, "").trim();
@@ -9935,7 +10427,7 @@ function extractTitle(absPath, kind) {
9935
10427
  const t = head.match(/<title[^>]*>([^<]+)<\/title>/i) ?? head.match(/<h1[^>]*>([^<]+)<\/h1>/i);
9936
10428
  if (t) return t[1].trim();
9937
10429
  }
9938
- return basename5(absPath);
10430
+ return basename6(absPath);
9939
10431
  }
9940
10432
  function resolveDirPath(root, rel) {
9941
10433
  if (rel.includes("\0")) return null;
@@ -9967,7 +10459,7 @@ function probeDir(absDir, showHidden) {
9967
10459
  return false;
9968
10460
  }
9969
10461
  if (!showHidden && entry.name.startsWith(".")) continue;
9970
- const abs = join17(dir, entry.name);
10462
+ const abs = join18(dir, entry.name);
9971
10463
  const kind = entryKind(entry, abs);
9972
10464
  if (kind === "dir") {
9973
10465
  if (SKIP_DIRS.has(entry.name)) hasOther = true;
@@ -9987,14 +10479,14 @@ function probeDir(absDir, showHidden) {
9987
10479
  }
9988
10480
  function readGitBranch(dir) {
9989
10481
  try {
9990
- let gitDir = join17(dir, ".git");
10482
+ let gitDir = join18(dir, ".git");
9991
10483
  if (statSync5(gitDir).isFile()) {
9992
- const pointer = readFileSync10(gitDir, "utf8").trim();
10484
+ const pointer = readFileSync11(gitDir, "utf8").trim();
9993
10485
  const match = pointer.match(/^gitdir:\s*(.+)$/);
9994
10486
  if (!match) return void 0;
9995
10487
  gitDir = resolve5(dir, match[1]);
9996
10488
  }
9997
- const head = readFileSync10(join17(gitDir, "HEAD"), "utf8").trim();
10489
+ const head = readFileSync11(join18(gitDir, "HEAD"), "utf8").trim();
9998
10490
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
9999
10491
  if (branchMatch) return branchMatch[1];
10000
10492
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -10016,14 +10508,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
10016
10508
  for (const entry of entries2) {
10017
10509
  if (dirs.length + docs.length >= MAX_ENTRIES) break;
10018
10510
  if (!showHidden && entry.name.startsWith(".")) continue;
10019
- const abs = join17(absDir, entry.name);
10511
+ const abs = join18(absDir, entry.name);
10020
10512
  const path = relative(root, abs).split(sep).join("/");
10021
10513
  const kind = entryKind(entry, abs);
10022
10514
  if (kind === "dir") {
10023
10515
  if (SKIP_DIRS.has(entry.name)) continue;
10024
- const probe2 = probeDir(abs, showHidden);
10025
- if (!probe2.hasDocs && probe2.hasOther) continue;
10026
- const isRepo = existsSync17(join17(abs, ".git"));
10516
+ const probe3 = probeDir(abs, showHidden);
10517
+ if (!probe3.hasDocs && probe3.hasOther) continue;
10518
+ const isRepo = existsSync17(join18(abs, ".git"));
10027
10519
  dirs.push({
10028
10520
  path,
10029
10521
  name: entry.name,
@@ -10059,7 +10551,7 @@ function flatDocs(root, {
10059
10551
  for (const entry of entries2) {
10060
10552
  if (hits.length >= MAX_FLAT_DOCS) return;
10061
10553
  if (!showHidden && entry.name.startsWith(".")) continue;
10062
- const abs = join17(dir, entry.name);
10554
+ const abs = join18(dir, entry.name);
10063
10555
  const kind = entryKind(entry, abs);
10064
10556
  if (kind === "dir") {
10065
10557
  if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
@@ -10123,7 +10615,7 @@ function resolveDownloadPath(root, rel) {
10123
10615
  if (!rel || rel.includes("\0")) return null;
10124
10616
  const abs = resolve5(root, rel);
10125
10617
  if (abs !== root && !abs.startsWith(root + sep)) return null;
10126
- if (!isListableFile(basename5(abs))) return null;
10618
+ if (!isListableFile(basename6(abs))) return null;
10127
10619
  const contentType = MEDIA_EXTENSIONS[extname(abs).toLowerCase()] ?? "application/octet-stream";
10128
10620
  return { abs, contentType };
10129
10621
  }
@@ -10139,7 +10631,7 @@ function uniqueName(absDir, name) {
10139
10631
  let candidate = name;
10140
10632
  for (let n = 1; n < 1e3; n++) {
10141
10633
  try {
10142
- statSync5(join17(absDir, candidate));
10634
+ statSync5(join18(absDir, candidate));
10143
10635
  } catch {
10144
10636
  return candidate;
10145
10637
  }
@@ -10161,7 +10653,7 @@ function saveUploadedFile(root, relDir, name, data) {
10161
10653
  if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
10162
10654
  if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
10163
10655
  const final = uniqueName(absDir, safe);
10164
- const abs = join17(absDir, final);
10656
+ const abs = join18(absDir, final);
10165
10657
  writeFileSync7(abs, data);
10166
10658
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
10167
10659
  }
@@ -10175,7 +10667,7 @@ function createDocFolder(root, relDir, name) {
10175
10667
  }
10176
10668
  const safe = sanitizeUploadName(name);
10177
10669
  if (!safe) return { status: "bad-name" };
10178
- const abs = join17(absDir, safe);
10670
+ const abs = join18(absDir, safe);
10179
10671
  try {
10180
10672
  statSync5(abs);
10181
10673
  return { status: "exists" };
@@ -10198,7 +10690,7 @@ function createDocFile(root, relDir, name, content) {
10198
10690
  if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
10199
10691
  if (SKIP_FILES.has(safe)) return { status: "bad-name" };
10200
10692
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
10201
- const abs = join17(absDir, safe);
10693
+ const abs = join18(absDir, safe);
10202
10694
  try {
10203
10695
  writeFileSync7(abs, content, { encoding: "utf8", flag: "wx" });
10204
10696
  } catch (err) {
@@ -10221,7 +10713,7 @@ function moveDocEntry(root, from, to) {
10221
10713
  if (!isDir2 && !stat.isFile()) return { status: "not-found" };
10222
10714
  const absTo = resolve5(root, to);
10223
10715
  if (!absTo.startsWith(root + sep)) return { status: "bad-target" };
10224
- const name = sanitizeUploadName(basename5(absTo));
10716
+ const name = sanitizeUploadName(basename6(absTo));
10225
10717
  if (!name) return { status: "bad-target" };
10226
10718
  if (!isDir2) {
10227
10719
  const ext = extname(name).toLowerCase();
@@ -10229,19 +10721,19 @@ function moveDocEntry(root, from, to) {
10229
10721
  return { status: "bad-target" };
10230
10722
  }
10231
10723
  }
10232
- const absToDir = dirname4(absTo);
10724
+ const absToDir = dirname5(absTo);
10233
10725
  if (resolveDirPath(root, relative(root, absToDir)) === null) return { status: "bad-target" };
10234
10726
  try {
10235
10727
  if (!statSync5(absToDir).isDirectory()) return { status: "bad-target" };
10236
10728
  } catch {
10237
10729
  return { status: "bad-target" };
10238
10730
  }
10239
- const finalAbs = join17(absToDir, name);
10731
+ const finalAbs = join18(absToDir, name);
10240
10732
  if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
10241
10733
  return { status: "into-self" };
10242
10734
  }
10243
10735
  try {
10244
- if (realpathSync(finalAbs) !== realpathSync(absFrom)) return { status: "exists" };
10736
+ if (realpathSync3(finalAbs) !== realpathSync3(absFrom)) return { status: "exists" };
10245
10737
  } catch {
10246
10738
  }
10247
10739
  renameSync(absFrom, finalAbs);
@@ -10298,10 +10790,10 @@ function createDocsRouter() {
10298
10790
  }
10299
10791
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10300
10792
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10301
- const data = readFileSync10(resolved2.abs);
10793
+ const data = readFileSync11(resolved2.abs);
10302
10794
  const headers = { "Content-Type": resolved2.contentType };
10303
10795
  if (download) {
10304
- const filename = basename5(resolved2.abs).replace(/"/g, "");
10796
+ const filename = basename6(resolved2.abs).replace(/"/g, "");
10305
10797
  headers["Content-Disposition"] = `attachment; filename="${filename}"`;
10306
10798
  }
10307
10799
  return c.body(new Uint8Array(data), 200, headers);
@@ -10318,7 +10810,7 @@ function createDocsRouter() {
10318
10810
  }
10319
10811
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10320
10812
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10321
- return c.body(new Uint8Array(readFileSync10(abs)), 200, {
10813
+ return c.body(new Uint8Array(readFileSync11(abs)), 200, {
10322
10814
  "Content-Type": "text/html; charset=utf-8",
10323
10815
  "Content-Security-Policy": HTML_SHARE_CSP,
10324
10816
  "X-Content-Type-Options": "nosniff",
@@ -10334,10 +10826,10 @@ function createDocsRouter() {
10334
10826
  const files = form.getAll("file").filter((f) => f instanceof File);
10335
10827
  if (files.length === 0) return c.json({ error: "missing file" }, 400);
10336
10828
  const uploaded = [];
10337
- const failed = [];
10829
+ const failed2 = [];
10338
10830
  for (const file of files) {
10339
10831
  if (file.size > MAX_UPLOAD_BYTES) {
10340
- failed.push({
10832
+ failed2.push({
10341
10833
  name: file.name,
10342
10834
  error: `larger than ${MAX_UPLOAD_BYTES / (1024 * 1024)}MB`
10343
10835
  });
@@ -10351,16 +10843,16 @@ function createDocsRouter() {
10351
10843
  case "no-dir":
10352
10844
  return c.json({ error: "folder not found" }, 404);
10353
10845
  case "bad-type":
10354
- failed.push({ name: file.name, error: "file type not allowed" });
10846
+ failed2.push({ name: file.name, error: "file type not allowed" });
10355
10847
  break;
10356
10848
  case "too-large":
10357
- failed.push({ name: file.name, error: "file too large" });
10849
+ failed2.push({ name: file.name, error: "file too large" });
10358
10850
  break;
10359
10851
  default:
10360
- failed.push({ name: file.name, error: "invalid filename" });
10852
+ failed2.push({ name: file.name, error: "invalid filename" });
10361
10853
  }
10362
10854
  }
10363
- return c.json({ path: dir, uploaded, failed }, uploaded.length > 0 ? 201 : 400);
10855
+ return c.json({ path: dir, uploaded, failed: failed2 }, uploaded.length > 0 ? 201 : 400);
10364
10856
  });
10365
10857
  app.post("/:id/docs/folder", async (c) => {
10366
10858
  const { cwd } = resolveChannelConfig(c.req.param("id"));
@@ -10500,10 +10992,10 @@ function createDocsRouter() {
10500
10992
  // by; only docs get a parsed title.
10501
10993
  kind: kind ?? "code",
10502
10994
  lang: kind ? void 0 : CODE_EXTENSIONS[ext],
10503
- title: kind ? extractTitle(abs, kind) : basename5(abs),
10995
+ title: kind ? extractTitle(abs, kind) : basename6(abs),
10504
10996
  mtime: stat.mtime.toISOString(),
10505
10997
  tooLarge: tooLarge || void 0,
10506
- content: tooLarge ? "" : readFileSync10(abs, "utf8")
10998
+ content: tooLarge ? "" : readFileSync11(abs, "utf8")
10507
10999
  });
10508
11000
  });
10509
11001
  app.put("/:id/docs/content", async (c) => {
@@ -10546,7 +11038,7 @@ function createDocsRouter() {
10546
11038
  channel,
10547
11039
  rel,
10548
11040
  title: extractTitle(abs, "md"),
10549
- content: readFileSync10(abs, "utf8")
11041
+ content: readFileSync11(abs, "utf8")
10550
11042
  };
10551
11043
  };
10552
11044
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -10743,7 +11235,7 @@ function relay(agent, pathOf) {
10743
11235
 
10744
11236
  // src/dashboard/skills-routes.ts
10745
11237
  import { existsSync as existsSync19, statSync as statSync7 } from "fs";
10746
- import { dirname as dirname5 } from "path";
11238
+ import { dirname as dirname6 } from "path";
10747
11239
  import { Hono as Hono12 } from "hono";
10748
11240
 
10749
11241
  // src/dashboard/skill-mirror.ts
@@ -10769,7 +11261,7 @@ function ownSkills(scope, dir) {
10769
11261
  return [{ scope, name, description, body, path, plugin: null, mtime }];
10770
11262
  });
10771
11263
  }
10772
- function snapshot(scopes) {
11264
+ function snapshot2(scopes) {
10773
11265
  const wants = (scope) => !scopes || scopes.has(scope);
10774
11266
  const configDir2 = profileClaudeConfigDir();
10775
11267
  const rows = [];
@@ -10804,7 +11296,7 @@ async function pushSkillScopes(scopes) {
10804
11296
  if (!mirrorEnabled()) return;
10805
11297
  const workspace = getProfile();
10806
11298
  const supabase = getSupabaseClient();
10807
- const rows = snapshot(scopes ? new Set(scopes) : void 0);
11299
+ const rows = snapshot2(scopes ? new Set(scopes) : void 0);
10808
11300
  let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
10809
11301
  if (scopes) query = query.in("scope", scopes);
10810
11302
  const { data, error: error2 } = await query;
@@ -10868,7 +11360,7 @@ function workspaceScope() {
10868
11360
  return {
10869
11361
  dir: workspaceSkillsDir(claudeConfigDir),
10870
11362
  // `~/.claude` itself, which exists wherever the CLI has ever run.
10871
- root: dirname5(workspaceSkillsDir(claudeConfigDir)),
11363
+ root: dirname6(workspaceSkillsDir(claudeConfigDir)),
10872
11364
  cwd,
10873
11365
  claudeConfigDir,
10874
11366
  // Only plugin skills. Project skills belong to a specific channel's cwd, so
@@ -10998,7 +11490,7 @@ function mirrorBackend(agent) {
10998
11490
  }
10999
11491
  }
11000
11492
  return {
11001
- dir: own[0] ? dirname5(dirname5(own[0].path)) : "",
11493
+ dir: own[0] ? dirname6(dirname6(own[0].path)) : "",
11002
11494
  skills: own.map(({ name, description, mtime }) => ({ name, description, mtime })),
11003
11495
  inherited: inherited.sort(byName)
11004
11496
  };
@@ -11084,37 +11576,11 @@ function registerMirroredSkillRoutes(api, agent) {
11084
11576
  mountSkillRoutes(api, "/channels/:id/skill-files", channelRef, backend);
11085
11577
  }
11086
11578
 
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
11579
  // src/dashboard/providers.ts
11114
11580
  import { execFileSync as execFileSync2 } from "child_process";
11115
11581
  import { existsSync as existsSync20 } from "fs";
11116
11582
  import { homedir as homedir8 } from "os";
11117
- import { join as join18, resolve as resolve6 } from "path";
11583
+ import { join as join19, resolve as resolve6 } from "path";
11118
11584
  var PROVIDER_LABELS = {
11119
11585
  claude: "Claude Code",
11120
11586
  codex: "Codex"
@@ -11132,7 +11598,7 @@ function binaryOnPath(bin) {
11132
11598
  function claudeAuthAvailable() {
11133
11599
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
11134
11600
  const configDir2 = profileClaudeConfigDir() || resolve6(homedir8(), ".claude");
11135
- if (existsSync20(join18(configDir2, ".credentials.json"))) return true;
11601
+ if (existsSync20(join19(configDir2, ".credentials.json"))) return true;
11136
11602
  if (process.platform !== "darwin") return false;
11137
11603
  try {
11138
11604
  execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
@@ -11143,7 +11609,7 @@ function claudeAuthAvailable() {
11143
11609
  return false;
11144
11610
  }
11145
11611
  }
11146
- function probe(runner) {
11612
+ function probe2(runner) {
11147
11613
  const label2 = PROVIDER_LABELS[runner];
11148
11614
  const bin = runner === "codex" ? "codex" : "claude";
11149
11615
  if (!binaryOnPath(bin)) {
@@ -11164,7 +11630,7 @@ function providerStatuses() {
11164
11630
  const key = getProfile();
11165
11631
  const hit = cache6.get(key);
11166
11632
  if (hit && Date.now() - hit.at < TTL_MS2) return hit.statuses;
11167
- const statuses = ["claude", "codex"].map(probe);
11633
+ const statuses = ["claude", "codex"].map(probe2);
11168
11634
  cache6.set(key, { at: Date.now(), statuses });
11169
11635
  return statuses;
11170
11636
  }
@@ -11275,7 +11741,7 @@ function registerHostRoutes(api) {
11275
11741
  const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
11276
11742
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
11277
11743
  const filename = errors ? "custie-error.log" : "custie.log";
11278
- const file = join19(paths.LOG_DIR, filename);
11744
+ const file = join20(paths.LOG_DIR, filename);
11279
11745
  const exists = existsSync21(file);
11280
11746
  return c.json({
11281
11747
  file,
@@ -11287,7 +11753,7 @@ function registerHostRoutes(api) {
11287
11753
  }
11288
11754
  function tailFile(file, limit) {
11289
11755
  if (!existsSync21(file)) return [];
11290
- const lines = readFileSync11(file, "utf-8").split("\n");
11756
+ const lines = readFileSync12(file, "utf-8").split("\n");
11291
11757
  if (lines.at(-1) === "") lines.pop();
11292
11758
  return lines.slice(-limit);
11293
11759
  }
@@ -11399,8 +11865,8 @@ function createCategoryWriteRouter() {
11399
11865
  (id, position) => supabase.from("categories").update({ position, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).eq("profile", getProfile())
11400
11866
  )
11401
11867
  );
11402
- const failed = results.find((r) => r.error);
11403
- if (failed?.error) return c.json({ error: failed.error.message }, 500);
11868
+ const failed2 = results.find((r) => r.error);
11869
+ if (failed2?.error) return c.json({ error: failed2.error.message }, 500);
11404
11870
  return c.json({ ok: true });
11405
11871
  });
11406
11872
  app.put("/:id", async (c) => {
@@ -14354,8 +14820,86 @@ function createChannelMembersRouter() {
14354
14820
  return router;
14355
14821
  }
14356
14822
 
14357
- // src/dashboard/onboarding-routes.ts
14823
+ // src/dashboard/memories-routes.ts
14358
14824
  import { Hono as Hono27 } from "hono";
14825
+ var requireChannelVisible = async (c, next) => {
14826
+ const channel = findChannel(c.req.param("id"));
14827
+ if (!channel || !canSeeChannel(channel, c.get("auth"))) {
14828
+ return c.json({ error: "channel not found" }, 404);
14829
+ }
14830
+ c.set("memoryChannelId", channel.id);
14831
+ await next();
14832
+ };
14833
+ var requireAgentMemory = async (c, next) => {
14834
+ if (!await isFeatureEnabled(c, "agent-memory")) {
14835
+ return c.json({ error: "feature not enabled" }, 403);
14836
+ }
14837
+ await next();
14838
+ };
14839
+ function createMemoriesRouter() {
14840
+ const router = new Hono27();
14841
+ for (const path of [
14842
+ "/:id/memories",
14843
+ "/:id/memories/*",
14844
+ "/:id/memory-events",
14845
+ "/:id/memory-events/*"
14846
+ ]) {
14847
+ router.use(path, requireAgentMemory, requireChannelVisible, requireDocsAccess);
14848
+ }
14849
+ router.get("/:id/memories", async (c) => {
14850
+ return c.json({ memories: await listChannelMemories(c.get("memoryChannelId")) });
14851
+ });
14852
+ router.patch("/:id/memories/:memoryId", async (c) => {
14853
+ const memoryId = c.req.param("memoryId");
14854
+ if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
14855
+ const body = await c.req.json().catch(() => ({}));
14856
+ const patch = {};
14857
+ if (typeof body["description"] === "string") patch.description = body["description"];
14858
+ if (typeof body["body"] === "string") patch.body = body["body"];
14859
+ const type = body["type"];
14860
+ if (type !== void 0) {
14861
+ if (!isMemoryType(type)) return c.json({ error: "invalid type" }, 400);
14862
+ patch.type = type;
14863
+ }
14864
+ try {
14865
+ const memory = await updateChannelMemory(c.get("memoryChannelId"), memoryId, patch, "user");
14866
+ if (!memory) return c.json({ error: "memory not found" }, 404);
14867
+ return c.json({ memory });
14868
+ } catch (err) {
14869
+ return c.json({ error: err.message }, 400);
14870
+ }
14871
+ });
14872
+ router.delete("/:id/memories/:memoryId", async (c) => {
14873
+ const memoryId = c.req.param("memoryId");
14874
+ if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
14875
+ const deleted = await deleteChannelMemory(c.get("memoryChannelId"), memoryId, "user");
14876
+ if (!deleted) return c.json({ error: "memory not found" }, 404);
14877
+ return c.json({ ok: true });
14878
+ });
14879
+ router.get("/:id/memory-events", async (c) => {
14880
+ const threadId = c.req.query("thread") ?? "";
14881
+ if (!isUuid(threadId)) return c.json({ error: "thread query parameter required" }, 400);
14882
+ return c.json({ events: await listThreadMemoryEvents(c.get("memoryChannelId"), threadId) });
14883
+ });
14884
+ router.post("/:id/memory-events/:eventId/undo", async (c) => {
14885
+ const eventId = c.req.param("eventId");
14886
+ if (!isUuid(eventId)) return c.json({ error: "invalid event id" }, 400);
14887
+ const result = await undoMemoryEvent(c.get("memoryChannelId"), eventId);
14888
+ if (result === "not-found") return c.json({ error: "event not found" }, 404);
14889
+ if (result === "already-undone") return c.json({ error: "already undone" }, 409);
14890
+ if (result === "conflict") {
14891
+ return c.json(
14892
+ { error: "The memory has changed since, so this can no longer be undone." },
14893
+ 409
14894
+ );
14895
+ }
14896
+ return c.json({ ok: true });
14897
+ });
14898
+ return router;
14899
+ }
14900
+
14901
+ // src/dashboard/onboarding-routes.ts
14902
+ import { Hono as Hono28 } from "hono";
14359
14903
 
14360
14904
  // src/dashboard/unique-channel-name.ts
14361
14905
  function uniqueChannelName(raw, taken) {
@@ -14444,7 +14988,7 @@ async function buildGreeting(input2) {
14444
14988
  // src/dashboard/onboarding-routes.ts
14445
14989
  var MAX_FOLDERS = 20;
14446
14990
  function createOnboardingRouter(agent) {
14447
- const app = new Hono27();
14991
+ const app = new Hono28();
14448
14992
  app.get("/scan", async (c) => {
14449
14993
  const ctx = await resolveAuthContext(c);
14450
14994
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -14689,6 +15233,7 @@ function registerDataRoutes(api, agent = {}) {
14689
15233
  api.route("/channels", createDocShareWriteRouter(agent));
14690
15234
  api.route("/onboarding", createOnboardingRouter(agent));
14691
15235
  api.route("/channels", createChannelMembersRouter());
15236
+ api.route("/channels", createMemoriesRouter());
14692
15237
  api.route("/docs", createDocShareListRouter());
14693
15238
  api.get("/users", (c) => c.json({ users: listActiveUsers() }));
14694
15239
  api.route("/categories", createCategoryWriteRouter());
@@ -14939,20 +15484,20 @@ function registerDataRoutes(api, agent = {}) {
14939
15484
  // src/dashboard/device-auth-routes.ts
14940
15485
  import { verify } from "crypto";
14941
15486
  import { getConnInfo } from "@hono/node-server/conninfo";
14942
- import { Hono as Hono28 } from "hono";
15487
+ import { Hono as Hono29 } from "hono";
14943
15488
 
14944
15489
  // src/dashboard/agent-bridge.ts
14945
15490
  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";
15491
+ import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
14947
15492
  import dotenv from "dotenv";
14948
15493
  import { WebSocket, WebSocketServer } from "ws";
14949
15494
 
14950
15495
  // src/dashboard/host-identity.ts
14951
15496
  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";
14953
- import { dirname as dirname6, join as join20 } from "path";
15497
+ import { existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
15498
+ import { dirname as dirname7, join as join21 } from "path";
14954
15499
  function hostPrivateKeyPath() {
14955
- return process.env["FLYSPACE_HOST_KEY_PATH"] || join20(paths.BASE_CONFIG_DIR, "host.key");
15500
+ return process.env["FLYSPACE_HOST_KEY_PATH"] || join21(paths.BASE_CONFIG_DIR, "host.key");
14956
15501
  }
14957
15502
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
14958
15503
  if (!existsSync24(path)) {
@@ -14960,7 +15505,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
14960
15505
  `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
15506
  );
14962
15507
  }
14963
- return createPrivateKey2(readFileSync12(path, "utf8"));
15508
+ return createPrivateKey2(readFileSync13(path, "utf8"));
14964
15509
  }
14965
15510
  function loadHostPublicKey() {
14966
15511
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -14976,10 +15521,10 @@ function loadHostPublicKey() {
14976
15521
  // src/dashboard/host-lock.ts
14977
15522
  import { execFileSync as execFileSync3 } from "child_process";
14978
15523
  import { randomBytes as randomBytes5 } from "crypto";
14979
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
14980
- import { join as join21 } from "path";
15524
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
15525
+ import { join as join22 } from "path";
14981
15526
  function hostLockPath() {
14982
- return join21(paths.BASE_DATA_DIR, "host.lock");
15527
+ return join22(paths.BASE_DATA_DIR, "host.lock");
14983
15528
  }
14984
15529
  function isLiveFlyspace(pid) {
14985
15530
  try {
@@ -14994,7 +15539,7 @@ function isLiveFlyspace(pid) {
14994
15539
  }
14995
15540
  function readLockPid(path) {
14996
15541
  try {
14997
- const pid = Number(JSON.parse(readFileSync13(path, "utf8")).pid);
15542
+ const pid = Number(JSON.parse(readFileSync14(path, "utf8")).pid);
14998
15543
  return Number.isInteger(pid) && pid > 0 ? pid : null;
14999
15544
  } catch {
15000
15545
  return null;
@@ -15035,13 +15580,13 @@ function publishHostEndpoint(port) {
15035
15580
  const path = hostLockPath();
15036
15581
  if (readLockPid(path) !== process.pid) return null;
15037
15582
  const token = randomBytes5(32).toString("hex");
15038
- const lock = JSON.parse(readFileSync13(path, "utf8"));
15583
+ const lock = JSON.parse(readFileSync14(path, "utf8"));
15039
15584
  writeFileSync9(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
15040
15585
  return token;
15041
15586
  }
15042
15587
  function readHostEndpoint() {
15043
15588
  try {
15044
- const { pid, port, token } = JSON.parse(readFileSync13(hostLockPath(), "utf8"));
15589
+ const { pid, port, token } = JSON.parse(readFileSync14(hostLockPath(), "utf8"));
15045
15590
  if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
15046
15591
  return { port, token };
15047
15592
  } catch {
@@ -15281,7 +15826,7 @@ async function userEmail(userId) {
15281
15826
 
15282
15827
  // src/dashboard/doc-share-publisher.ts
15283
15828
  import { randomBytes as randomBytes7 } from "crypto";
15284
- import { readFileSync as readFileSync14, statSync as statSync9 } from "fs";
15829
+ import { readFileSync as readFileSync15, statSync as statSync9 } from "fs";
15285
15830
  import { extname as extname2, posix } from "path";
15286
15831
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
15287
15832
 
@@ -15468,7 +16013,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15468
16013
  }
15469
16014
  if (!stat.isFile()) return { status: "not-found" };
15470
16015
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
15471
- const source = readFileSync14(abs, "utf8");
16016
+ const source = readFileSync15(abs, "utf8");
15472
16017
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
15473
16018
  if (kind === "excalidraw") {
15474
16019
  try {
@@ -15548,7 +16093,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15548
16093
  let text = null;
15549
16094
  try {
15550
16095
  const size = statSync9(docAbs).size;
15551
- if (size <= budget) text = readFileSync14(docAbs, "utf8");
16096
+ if (size <= budget) text = readFileSync15(docAbs, "utf8");
15552
16097
  } catch {
15553
16098
  continue;
15554
16099
  }
@@ -15565,7 +16110,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
15565
16110
  let body;
15566
16111
  try {
15567
16112
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
15568
- body = readFileSync14(resolved2.abs);
16113
+ body = readFileSync15(resolved2.abs);
15569
16114
  } catch {
15570
16115
  continue;
15571
16116
  }
@@ -15607,7 +16152,7 @@ async function publishDocShare(input2) {
15607
16152
  ...HTML_FEATURES,
15608
16153
  brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
15609
16154
  } : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
15610
- const snapshot2 = {
16155
+ const snapshot3 = {
15611
16156
  ...built,
15612
16157
  sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
15613
16158
  };
@@ -15617,8 +16162,8 @@ async function publishDocShare(input2) {
15617
16162
  const renamed = await setDocShareSlug(existing.id, slug);
15618
16163
  if (!renamed) return { status: "not-found" };
15619
16164
  }
15620
- const share2 = await updateDocShareSnapshot(existing.id, snapshot2, features);
15621
- const stale = existing.assetKeys.filter((key) => !snapshot2.assetKeys.includes(key));
16165
+ const share2 = await updateDocShareSnapshot(existing.id, snapshot3, features);
16166
+ const stale = existing.assetKeys.filter((key) => !snapshot3.assetKeys.includes(key));
15622
16167
  await deleteShareAssets(stale);
15623
16168
  if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
15624
16169
  void queueShareAudio(share2);
@@ -15632,7 +16177,7 @@ async function publishDocShare(input2) {
15632
16177
  channelId: input2.channelId,
15633
16178
  sourcePath: input2.sourcePath,
15634
16179
  createdBy: input2.createdBy,
15635
- ...snapshot2,
16180
+ ...snapshot3,
15636
16181
  ...features
15637
16182
  });
15638
16183
  void queueShareAudio(share);
@@ -15747,8 +16292,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
15747
16292
  const scope = table === "skill_files" && isWrite ? assigned : workspaces;
15748
16293
  if (scope.length === 0) return deny(`This Host is not the assigned Host for any workspace it may write ${table} to.`);
15749
16294
  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.`);
16295
+ const ok3 = rows.length > 0 && rows.every((r) => typeof r[slugColumn] === "string" && scope.includes(r[slugColumn]));
16296
+ if (!ok3) return deny(`Every ${table} row must belong to a workspace this Host is assigned.`);
15752
16297
  }
15753
16298
  if (method === "PATCH") {
15754
16299
  const patch = bodyRows(req.body);
@@ -15762,8 +16307,8 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
15762
16307
  if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
15763
16308
  if (table === "threads") {
15764
16309
  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.");
16310
+ const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
16311
+ if (!ok3) return deny("A thread must belong to a channel of the device workspaces.");
15767
16312
  }
15768
16313
  if (method === "PATCH") {
15769
16314
  const patch = bodyRows(req.body);
@@ -15942,7 +16487,7 @@ function localAgentCallbacks(chatAgent) {
15942
16487
  regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
15943
16488
  listAgentRuns: () => chatAgent.runningThreadIds(),
15944
16489
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
15945
- listRunningProfiles: () => listProfiles().filter(isServiceRunning),
16490
+ listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
15946
16491
  getAgentIdentities: () => resolveAgentIdentities(),
15947
16492
  publishDocShare: (input2) => publishDocShare(input2),
15948
16493
  listChannelSkills: (args) => listSkills(args.claudeConfigDir, args.cwd),
@@ -15959,14 +16504,14 @@ function localAgentCallbacks(chatAgent) {
15959
16504
  }
15960
16505
  var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
15961
16506
  function buildHostConfigSnapshot() {
15962
- const snapshot2 = {};
16507
+ const snapshot3 = {};
15963
16508
  for (const profile of listProfiles()) {
15964
- snapshot2[profile] = runWithProfile(profile, () => ({
15965
- env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync15(paths.CONFIG_FILE, "utf-8")) : {},
16509
+ snapshot3[profile] = runWithProfile(profile, () => ({
16510
+ env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync16(paths.CONFIG_FILE, "utf-8")) : {},
15966
16511
  agentModel: loadDashboardSettings().agent
15967
16512
  }));
15968
16513
  }
15969
- return snapshot2;
16514
+ return snapshot3;
15970
16515
  }
15971
16516
  function startLocalProfileConfigHydration() {
15972
16517
  setProfileConfigCache(buildHostConfigSnapshot());
@@ -16208,7 +16753,8 @@ function remoteAgentCallbacks() {
16208
16753
  // Host connected, showing nobody as running is a reasonable default
16209
16754
  // rather than a false claim of certainty either way.
16210
16755
  listRunningProfiles: async () => await callHost("listRunningProfiles", []) ?? [],
16211
- getAgentIdentities: async () => await callHost("getAgentIdentities", []) ?? [],
16756
+ // `null` (no Host) falls back to `offlineAgentIdentities` in api.ts.
16757
+ getAgentIdentities: () => callHost("getAgentIdentities", []),
16212
16758
  // Reading the doc and copying its assets can only happen where the
16213
16759
  // channel's real folder is, so this always goes to the Host. `null` (no
16214
16760
  // Host connected) surfaces as `host-unavailable` — see
@@ -16492,7 +17038,7 @@ async function boundWorkspaceIds(publicKey, userId) {
16492
17038
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
16493
17039
  }
16494
17040
  function createDeviceAuthRouter() {
16495
- const app = new Hono28();
17041
+ const app = new Hono29();
16496
17042
  app.post("/code", async (c) => {
16497
17043
  const body = await readBody(c);
16498
17044
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -16555,8 +17101,8 @@ function createDeviceAuthRouter() {
16555
17101
  if (!device || device.revokedAt || !nonce || !signature) return c.json({ error: "invalid_grant" }, 401);
16556
17102
  if (!await consumeChallenge(device.id, nonce)) return c.json({ error: "invalid_grant" }, 401);
16557
17103
  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);
17104
+ const ok3 = key && verify(null, Buffer.from(nonce), key, Buffer.from(signature, "base64"));
17105
+ if (!ok3) return c.json({ error: "invalid_grant" }, 401);
16560
17106
  const { token, expiresInS } = await issueAccessToken(device.id);
16561
17107
  return c.json({ access_token: token, token_type: "Bearer", expires_in: expiresInS });
16562
17108
  });
@@ -16656,7 +17202,7 @@ function createDeviceAuthRouter() {
16656
17202
 
16657
17203
  // src/dashboard/api.ts
16658
17204
  function createApiRouter(agent = {}, mode = "all") {
16659
- const api = new Hono29();
17205
+ const api = new Hono30();
16660
17206
  api.use("*", async (c, next) => {
16661
17207
  const known = await listKnownWorkspaces();
16662
17208
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -16703,7 +17249,7 @@ function createApiRouter(agent = {}, mode = "all") {
16703
17249
  if (mode === "data") {
16704
17250
  api.get(
16705
17251
  "/settings/agents",
16706
- async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? [] })
17252
+ async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? await offlineAgentIdentities() })
16707
17253
  );
16708
17254
  api.get("/channels/:id/skills", async (c) => {
16709
17255
  const { cwd, claudeConfigDir } = resolveChannelConfig(c.req.param("id"));
@@ -16767,7 +17313,7 @@ function createApiRouter(agent = {}, mode = "all") {
16767
17313
  }
16768
17314
 
16769
17315
  // src/dashboard/app-icon-routes.ts
16770
- import { Hono as Hono30 } from "hono";
17316
+ import { Hono as Hono31 } from "hono";
16771
17317
  var NEUTRAL_MANIFEST = {
16772
17318
  id: "/",
16773
17319
  name: "Flyspace",
@@ -16805,7 +17351,7 @@ async function withResolvedProfile2(c, next) {
16805
17351
  return profile ? runWithProfile(profile, () => next()) : next();
16806
17352
  }
16807
17353
  function createAppIconRouter() {
16808
- const app = new Hono30();
17354
+ const app = new Hono31();
16809
17355
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
16810
17356
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
16811
17357
  const profile = getProfile();
@@ -16845,7 +17391,7 @@ function pngResponse(c, png) {
16845
17391
  }
16846
17392
 
16847
17393
  // src/dashboard/stripe-routes.ts
16848
- import { Hono as Hono31 } from "hono";
17394
+ import { Hono as Hono32 } from "hono";
16849
17395
  function planStatusFor(status) {
16850
17396
  const known = ["trialing", "active", "paused", "canceled"];
16851
17397
  return known.includes(status) ? status : null;
@@ -16888,7 +17434,7 @@ async function handleCardAdded(session) {
16888
17434
  }
16889
17435
  }
16890
17436
  function createStripeWebhookRouter() {
16891
- const stripeRouter = new Hono31();
17437
+ const stripeRouter = new Hono32();
16892
17438
  stripeRouter.post("/webhook", async (c) => {
16893
17439
  const secret = process.env["STRIPE_WEBHOOK_SECRET"];
16894
17440
  const signature = c.req.header("stripe-signature");
@@ -16999,15 +17545,15 @@ function startHostStatusBroadcast() {
16999
17545
 
17000
17546
  // src/dashboard/server.ts
17001
17547
  var DEFAULT_DASHBOARD_PORT = 4747;
17002
- var WEB_DIST = join22(paths.PACKAGE_ROOT, "web", "dist");
17548
+ var WEB_DIST = join23(paths.PACKAGE_ROOT, "web", "dist");
17003
17549
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
17004
17550
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
17005
17551
  for (let port = start; port < start + attempts; port++) {
17006
17552
  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);
17553
+ const probe3 = createServer();
17554
+ probe3.once("error", () => resolve8(false));
17555
+ probe3.once("listening", () => probe3.close(() => resolve8(true)));
17556
+ probe3.listen(port, host);
17011
17557
  });
17012
17558
  if (free) return port;
17013
17559
  }
@@ -17048,7 +17594,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17048
17594
  const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
17049
17595
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
17050
17596
  };
17051
- const app = new Hono32();
17597
+ const app = new Hono33();
17052
17598
  app.use("*", async (c, next) => {
17053
17599
  await next();
17054
17600
  if (c.req.path.startsWith("/assets/")) {
@@ -17073,14 +17619,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17073
17619
  } else {
17074
17620
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
17075
17621
  app.route("/", createAppIconRouter());
17076
- if (existsSync26(WEB_DIST) || existsSync26(join22(paths.PACKAGE_ROOT, "web", "package.json"))) {
17622
+ if (existsSync26(WEB_DIST) || existsSync26(join23(paths.PACKAGE_ROOT, "web", "package.json"))) {
17077
17623
  app.use("/*", serveStatic({ root: WEB_DIST }));
17078
17624
  }
17079
17625
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
17080
- const spaFallback = serveStatic({ path: join22(WEB_DIST, "index.html") });
17626
+ const spaFallback = serveStatic({ path: join23(WEB_DIST, "index.html") });
17081
17627
  app.get("*", async (c, next) => {
17082
- if (!existsSync26(join22(WEB_DIST, "index.html"))) {
17083
- const isCheckout = existsSync26(join22(paths.PACKAGE_ROOT, "web", "package.json"));
17628
+ if (!existsSync26(join23(WEB_DIST, "index.html"))) {
17629
+ const isCheckout = existsSync26(join23(paths.PACKAGE_ROOT, "web", "package.json"));
17084
17630
  return c.text(
17085
17631
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
17086
17632
  Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
@@ -17135,7 +17681,7 @@ function watchServices() {
17135
17681
 
17136
17682
  // src/slack/image-uploader.ts
17137
17683
  import { readFile } from "fs/promises";
17138
- import { basename as basename6 } from "path";
17684
+ import { basename as basename7 } from "path";
17139
17685
  import sharp6 from "sharp";
17140
17686
  var MAX_WIDTH = 800;
17141
17687
  async function uploadImage(client2, { channel, filePath, threadTs, text }) {
@@ -17144,7 +17690,7 @@ async function uploadImage(client2, { channel, filePath, threadTs, text }) {
17144
17690
  const base = {
17145
17691
  channel_id: channel,
17146
17692
  file: resized,
17147
- filename: basename6(filePath),
17693
+ filename: basename7(filePath),
17148
17694
  ...text ? { initial_comment: text } : {}
17149
17695
  };
17150
17696
  const result = await client2.filesUploadV2(threadTs ? { ...base, thread_ts: threadTs } : base);
@@ -17518,7 +18064,7 @@ async function threadReplies(client2, nameOrId, ts2) {
17518
18064
  async function postMessage(client2, args) {
17519
18065
  const channelArg = getArg2(args, "--channel");
17520
18066
  const textFilePath = getArg2(args, "--text-file");
17521
- const text = textFilePath ? readFileSync16(textFilePath, "utf8") : getArg2(args, "--text");
18067
+ const text = textFilePath ? readFileSync17(textFilePath, "utf8") : getArg2(args, "--text");
17522
18068
  const threadTs = getArg2(args, "--thread");
17523
18069
  const markdown = args.includes("--markdown");
17524
18070
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -17673,7 +18219,7 @@ async function ensureClaudeCodeReady() {
17673
18219
  }
17674
18220
  function getManifestYaml(botName) {
17675
18221
  const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
17676
- const raw = readFileSync16(manifestPath, "utf-8");
18222
+ const raw = readFileSync17(manifestPath, "utf-8");
17677
18223
  return raw.replace(/name: Custie/g, `name: ${botName}`);
17678
18224
  }
17679
18225
  async function stepPersonalise() {
@@ -17886,7 +18432,7 @@ async function stepBrowserTokens(botName) {
17886
18432
  })
17887
18433
  ));
17888
18434
  const DEBUG = process.argv.includes("--debug");
17889
- const USER_DATA_DIR = resolve7(tmpdir3(), "custie-playwright-profile");
18435
+ const USER_DATA_DIR = resolve7(tmpdir4(), "custie-playwright-profile");
17890
18436
  let context;
17891
18437
  try {
17892
18438
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -18107,7 +18653,7 @@ function copyDefaultPrompt(botName) {
18107
18653
  if (!existsSync27(paths.PROMPT_FILE)) {
18108
18654
  const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
18109
18655
  if (existsSync27(defaultPrompt)) {
18110
- const content = readFileSync16(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18656
+ const content = readFileSync17(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18111
18657
  writeFileSync10(paths.PROMPT_FILE, content);
18112
18658
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
18113
18659
  }
@@ -18429,8 +18975,122 @@ async function runChatCmd(args) {
18429
18975
  }
18430
18976
  }
18431
18977
 
18432
- // src/commands/mcp.ts
18978
+ // src/commands/memory.ts
18979
+ import { readFileSync as readFileSync18 } from "fs";
18433
18980
  var USAGE5 = `
18981
+ Usage: flyspace memory <subcommand> --channel <name-or-id> [options]
18982
+
18983
+ A channel's persistent agent memory (docs/features/workspace-memory.md).
18984
+
18985
+ Subcommands:
18986
+ list Every memory in the channel (index form)
18987
+ get <name> One memory in full
18988
+ search <words...> Memories whose name, description or body match
18989
+ save --name <slug> --type <type> Create, or overwrite the memory with that name
18990
+ --description <one line>
18991
+ --body <text | -> "-" reads the body from stdin
18992
+ [--thread <id>] The thread the memory came from
18993
+ delete <name-or-id> [--thread <id>] Delete one memory
18994
+
18995
+ Types: ${MEMORY_TYPES.join(", ")}
18996
+ `;
18997
+ function getArg4(args, flag) {
18998
+ const idx = args.indexOf(flag);
18999
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
19000
+ }
19001
+ function positionals(args) {
19002
+ const out = [];
19003
+ for (let i = 0; i < args.length; i++) {
19004
+ if (args[i].startsWith("--")) i++;
19005
+ else out.push(args[i]);
19006
+ }
19007
+ return out;
19008
+ }
19009
+ function indexLine(m) {
19010
+ return `${m.name} (${m.type}, ${m.updatedAt.slice(0, 10)}) ${m.description}`;
19011
+ }
19012
+ async function runMemoryCmd(args) {
19013
+ loadEnvFiles();
19014
+ const subcommand = args[0];
19015
+ const rest = args.slice(1);
19016
+ if (!subcommand || subcommand === "-h" || subcommand === "--help") {
19017
+ console.log(USAGE5);
19018
+ return;
19019
+ }
19020
+ const channelArg = getArg4(rest, "--channel");
19021
+ if (!channelArg) throw new Error("--channel <name-or-id> is required");
19022
+ const channel = await resolveChannel(channelArg, getArg4(rest, "--profile-name"));
19023
+ const words = positionals(rest);
19024
+ switch (subcommand) {
19025
+ case "list": {
19026
+ const memories = await listChannelMemories(channel.id);
19027
+ console.log(memories.length ? memories.map(indexLine).join("\n") : "No memories yet.");
19028
+ break;
19029
+ }
19030
+ case "get": {
19031
+ const name = words[0];
19032
+ if (!name) throw new Error("Usage: flyspace memory get --channel <c> <name>");
19033
+ const memory = await getChannelMemory(channel.id, name);
19034
+ if (!memory) throw new Error(`No memory named ${name} in ${channel.name}.`);
19035
+ console.log(`# ${memory.name} (${memory.type})
19036
+
19037
+ ${memory.description}
19038
+
19039
+ ${memory.body}`);
19040
+ break;
19041
+ }
19042
+ case "search": {
19043
+ if (!words.length) throw new Error("Usage: flyspace memory search --channel <c> <words...>");
19044
+ const hits = await searchChannelMemories(channel.id, words.join(" "));
19045
+ console.log(hits.length ? hits.map(indexLine).join("\n") : "No matching memories.");
19046
+ break;
19047
+ }
19048
+ case "save": {
19049
+ const name = getArg4(rest, "--name");
19050
+ const type = getArg4(rest, "--type");
19051
+ const description = getArg4(rest, "--description");
19052
+ const bodyArg = getArg4(rest, "--body");
19053
+ if (!name || !type || !description || bodyArg === void 0) {
19054
+ throw new Error(
19055
+ "Usage: flyspace memory save --channel <c> --name <slug> --type <type> --description <one line> --body <text | -> [--thread <id>]"
19056
+ );
19057
+ }
19058
+ if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
19059
+ const body = bodyArg === "-" ? readFileSync18(0, "utf-8") : bodyArg;
19060
+ const thread = getArg4(rest, "--thread");
19061
+ const { created, changed } = await saveChannelMemory(
19062
+ {
19063
+ channelId: channel.id,
19064
+ name,
19065
+ type,
19066
+ description,
19067
+ body,
19068
+ createdBy: "agent",
19069
+ ...thread ? { sourceThreadId: thread } : {}
19070
+ },
19071
+ "agent"
19072
+ );
19073
+ const verb = created ? "Saved" : changed ? "Updated" : "Unchanged:";
19074
+ console.log(`${verb} memory ${name} in ${channel.name}.`);
19075
+ break;
19076
+ }
19077
+ case "delete": {
19078
+ const ref = words[0];
19079
+ if (!ref) throw new Error("Usage: flyspace memory delete --channel <c> <name-or-id>");
19080
+ const deleted = await deleteChannelMemory(channel.id, ref, "agent", getArg4(rest, "--thread"));
19081
+ if (!deleted) throw new Error(`No memory ${ref} in ${channel.name}.`);
19082
+ console.log(`Deleted memory ${ref}.`);
19083
+ break;
19084
+ }
19085
+ default:
19086
+ console.error(`Unknown subcommand: ${subcommand}`);
19087
+ console.log(USAGE5);
19088
+ process.exit(1);
19089
+ }
19090
+ }
19091
+
19092
+ // src/commands/mcp.ts
19093
+ var USAGE6 = `
18434
19094
  Usage: flyspace mcp <server>
18435
19095
 
18436
19096
  Run one of Flyspace's own MCP servers on stdio. Spawned by the agent runtime
@@ -18447,7 +19107,7 @@ Usage: flyspace mcp <server>
18447
19107
  async function runMcpCmd(args) {
18448
19108
  const server = args[0];
18449
19109
  if (!server || server === "-h" || server === "--help") {
18450
- console.log(USAGE5.trim());
19110
+ console.log(USAGE6.trim());
18451
19111
  return;
18452
19112
  }
18453
19113
  restoreStdout();
@@ -18460,7 +19120,7 @@ async function runMcpCmd(args) {
18460
19120
  }
18461
19121
  default:
18462
19122
  console.error(`Unknown MCP server: ${server}`);
18463
- console.error(USAGE5.trim());
19123
+ console.error(USAGE6.trim());
18464
19124
  process.exit(1);
18465
19125
  }
18466
19126
  }
@@ -18469,7 +19129,7 @@ async function runMcpCmd(args) {
18469
19129
  import { spawn as spawn6, spawnSync } from "child_process";
18470
19130
  import { existsSync as existsSync28 } from "fs";
18471
19131
  import { platform as platform7 } from "os";
18472
- import { join as join23 } from "path";
19132
+ import { join as join24 } from "path";
18473
19133
  function flagValue(args, name) {
18474
19134
  const eq = args.find((a) => a.startsWith(`--${name}=`));
18475
19135
  if (eq) return eq.slice(`--${name}=`.length);
@@ -18484,8 +19144,8 @@ function openBrowser2(url) {
18484
19144
  }
18485
19145
  }
18486
19146
  function ensureWebBuild() {
18487
- if (existsSync28(join23(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
18488
- if (!existsSync28(join23(paths.PACKAGE_ROOT, "web", "package.json"))) return;
19147
+ if (existsSync28(join24(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19148
+ if (!existsSync28(join24(paths.PACKAGE_ROOT, "web", "package.json"))) return;
18489
19149
  console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
18490
19150
  const result = spawnSync("pnpm", ["--dir", "web", "build"], {
18491
19151
  cwd: paths.PACKAGE_ROOT,
@@ -18536,7 +19196,7 @@ async function runDashboard(args) {
18536
19196
 
18537
19197
  // src/commands/workspace.ts
18538
19198
  import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
18539
- var USAGE6 = `
19199
+ var USAGE7 = `
18540
19200
  Usage: flyspace workspace <subcommand> [options]
18541
19201
 
18542
19202
  Subcommands:
@@ -18553,7 +19213,7 @@ var USAGE6 = `
18553
19213
  --no-invite Never send an invite email. Fails if the admin
18554
19214
  has no existing account.
18555
19215
  `;
18556
- function getArg4(args, flag) {
19216
+ function getArg5(args, flag) {
18557
19217
  const idx = args.indexOf(flag);
18558
19218
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
18559
19219
  }
@@ -18649,10 +19309,10 @@ async function provisionChannels(specs) {
18649
19309
  async function handleCreate(args) {
18650
19310
  const slug = args[0];
18651
19311
  if (!slug || slug.startsWith("-")) fail2("Usage: flyspace workspace create <slug> --admin-email <email>");
18652
- const adminEmail = getArg4(args, "--admin-email");
19312
+ const adminEmail = getArg5(args, "--admin-email");
18653
19313
  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";
19314
+ const botName = getArg5(args, "--bot-name") ?? slug;
19315
+ const baseDomain = getArg5(args, "--base-domain") ?? "flyspace.ai";
18656
19316
  const allowInvite = !args.includes("--no-invite");
18657
19317
  const channels = getAllArgs(args, "--channel").map(parseChannelSpec);
18658
19318
  loadEnvFiles();
@@ -18675,13 +19335,13 @@ async function runWorkspaceCmd(args) {
18675
19335
  await handleCreate(args.slice(1));
18676
19336
  break;
18677
19337
  default:
18678
- console.log(USAGE6);
19338
+ console.log(USAGE7);
18679
19339
  process.exit(args[0] ? 1 : 0);
18680
19340
  }
18681
19341
  }
18682
19342
 
18683
19343
  // src/commands/reconcile.ts
18684
- var USAGE7 = `
19344
+ var USAGE8 = `
18685
19345
  Usage: flyspace reconcile [<workspace> | --all]
18686
19346
 
18687
19347
  Repair a workspace's local filesystem state (profile dir, config.env,
@@ -18701,7 +19361,7 @@ function printLog(name, log5) {
18701
19361
  }
18702
19362
  async function runReconcileCmd(args) {
18703
19363
  if (args.includes("-h") || args.includes("--help")) {
18704
- console.log(USAGE7);
19364
+ console.log(USAGE8);
18705
19365
  return;
18706
19366
  }
18707
19367
  loadEnvFiles();
@@ -18717,12 +19377,13 @@ async function runReconcileCmd(args) {
18717
19377
 
18718
19378
  // src/cli.ts
18719
19379
  var XERO_USAGE = false ? " xero Connect to Xero and expose it as an MCP server\n" : "";
18720
- var USAGE8 = `
19380
+ var USAGE9 = `
18721
19381
  Usage: flyspace <command> [options]
18722
19382
 
18723
19383
  Commands:
18724
19384
  setup Set up this machine as a Host (login, agent CLIs, ~/Flyspace)
18725
19385
  auth Log in, check status, or log out this device
19386
+ doctor Check this Host end to end (login, agent CLIs, services)
18726
19387
  start Start the agent host (Slack only if configured)
18727
19388
  install Install as a system service (launchd / systemd)
18728
19389
  uninstall Remove the system service
@@ -18736,6 +19397,7 @@ var USAGE8 = `
18736
19397
  usage Show agent token usage and cost for this workspace
18737
19398
  slack Query Slack (channels, users, post messages)
18738
19399
  chat Inspect dashboard chat channels and move threads between them
19400
+ memory Read and write a channel's persistent agent memory
18739
19401
  profiles List local profiles (bound here? scheduler?) or remove one
18740
19402
  workspace Provision a workspace (Supabase rows + local profile dirs)
18741
19403
  reconcile Repair a workspace's local files against Supabase
@@ -18776,6 +19438,9 @@ async function main() {
18776
19438
  case "auth":
18777
19439
  await runAuthCmd(args.slice(1));
18778
19440
  break;
19441
+ case "doctor":
19442
+ await runDoctor(args.slice(1));
19443
+ break;
18779
19444
  case "start":
18780
19445
  await runStart();
18781
19446
  break;
@@ -18824,6 +19489,9 @@ async function main() {
18824
19489
  case "chat":
18825
19490
  await runChatCmd(args.slice(1));
18826
19491
  break;
19492
+ case "memory":
19493
+ await runMemoryCmd(args.slice(1));
19494
+ break;
18827
19495
  case "profile":
18828
19496
  case "profiles":
18829
19497
  await runProfiles(args.slice(1));
@@ -18836,12 +19504,12 @@ async function main() {
18836
19504
  break;
18837
19505
  case "-v":
18838
19506
  case "--version":
18839
- printRaw("0.1.7");
19507
+ printRaw("0.1.10");
18840
19508
  break;
18841
19509
  case "-h":
18842
19510
  case "--help":
18843
19511
  case void 0:
18844
- printRaw(USAGE8);
19512
+ printRaw(USAGE9);
18845
19513
  break;
18846
19514
  case "xero":
18847
19515
  if (false) {
@@ -18852,7 +19520,7 @@ async function main() {
18852
19520
  // falls through
18853
19521
  default:
18854
19522
  printRaw(`Unknown command: ${command}`);
18855
- printRaw(USAGE8);
19523
+ printRaw(USAGE9);
18856
19524
  process.exit(1);
18857
19525
  }
18858
19526
  }