flyspace-cli 0.1.29 → 0.1.31

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.
Files changed (2) hide show
  1. package/dist/cli.js +370 -252
  2. package/package.json +7 -2
package/dist/cli.js CHANGED
@@ -642,6 +642,26 @@ async function requestHostDb(req) {
642
642
  send2({ type: "db", id, req });
643
643
  });
644
644
  }
645
+ var pendingUrls = /* @__PURE__ */ new Map();
646
+ function handleAttachmentUrlResult(id, url) {
647
+ pendingUrls.get(id)?.(url);
648
+ pendingUrls.delete(id);
649
+ }
650
+ async function requestAttachmentUrl(key) {
651
+ const send2 = await whenConnected();
652
+ const id = randomUUID();
653
+ return new Promise((resolve7, reject) => {
654
+ const timer2 = setTimeout(() => {
655
+ pendingUrls.delete(id);
656
+ reject(new Error("Data API did not answer in time"));
657
+ }, REQUEST_TIMEOUT_MS);
658
+ pendingUrls.set(id, (url) => {
659
+ clearTimeout(timer2);
660
+ resolve7(url);
661
+ });
662
+ send2({ type: "attachment-url", id, key });
663
+ });
664
+ }
645
665
  var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
646
666
  function dbFetch(send2) {
647
667
  return async (input2, init2) => {
@@ -861,14 +881,17 @@ function isServiceInstalled(profile) {
861
881
  return false;
862
882
  }
863
883
  function serviceBinaryMissing(file) {
884
+ const bin = serviceBinary(file);
885
+ return !!bin && !existsSync3(bin);
886
+ }
887
+ function serviceBinary(file) {
864
888
  let text;
865
889
  try {
866
890
  text = readFileSync5(file, "utf-8");
867
891
  } catch {
868
- return false;
892
+ return void 0;
869
893
  }
870
- const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
871
- return !!bin && !existsSync3(bin);
894
+ return text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
872
895
  }
873
896
  function profileServiceFile(profile) {
874
897
  return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
@@ -1835,7 +1858,7 @@ async function runAuthCmd(args) {
1835
1858
  }
1836
1859
 
1837
1860
  // ../api/src/version.ts
1838
- var CLI_VERSION = true ? "0.1.29" : null;
1861
+ var CLI_VERSION = true ? "0.1.31" : null;
1839
1862
  function isOlderVersion(a, b) {
1840
1863
  const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1841
1864
  const [pa, pb] = [parse(a), parse(b)];
@@ -2108,11 +2131,11 @@ async function serverReachable() {
2108
2131
  return err.cause ? String(err.cause) : err.message;
2109
2132
  }
2110
2133
  }
2111
- function checkService(name, installed, file, running, fix) {
2134
+ function checkService(name, installed, file, running2, fix) {
2112
2135
  if (!installed) bad2(`${name} not installed`, `run: ${fix}`);
2113
2136
  else if (serviceBinaryMissing(file))
2114
2137
  bad2(`${name} points at a CLI that no longer exists`, `run: ${fix}`);
2115
- else if (!running) bad2(`${name} not running`, `logs: ${tilde3(paths.LOG_DIR)}`);
2138
+ else if (!running2) bad2(`${name} not running`, `logs: ${tilde3(paths.LOG_DIR)}`);
2116
2139
  else ok2(`${name} running`);
2117
2140
  }
2118
2141
  async function runDoctor(args) {
@@ -2381,8 +2404,8 @@ async function runProfiles(args = []) {
2381
2404
 
2382
2405
  // src/commands/upgrade.ts
2383
2406
  import { execSync as execSync6 } from "child_process";
2384
- import { realpathSync as realpathSync3 } from "fs";
2385
- import { join as join11 } from "path";
2407
+ import { existsSync as existsSync9, readFileSync as readFileSync10, realpathSync as realpathSync3 } from "fs";
2408
+ import { dirname as dirname5, join as join11 } from "path";
2386
2409
  function restartInstalledProfiles() {
2387
2410
  if (isHostServiceInstalled()) {
2388
2411
  console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
@@ -2402,36 +2425,68 @@ Restarting ${installed.length} instance(s)...`);
2402
2425
  }
2403
2426
  }
2404
2427
  }
2405
- function npmInstallCommand() {
2406
- let prefix;
2428
+ function installTarget(entry) {
2429
+ const home2 = flyspaceHome(entry) ?? (entry.endsWith("/bin/flyspace") ? dirname5(dirname5(entry)) : void 0);
2430
+ if (home2 && existsSync9(join11(home2, "cli", "lib", "node_modules", "flyspace-cli"))) {
2431
+ return { prefix: join11(home2, "cli"), nodeBin: join11(home2, "node", "bin") };
2432
+ }
2407
2433
  try {
2408
- const entry = realpathSync3(process.argv[1] ?? "");
2409
- const at = entry.indexOf(`/lib/node_modules/${"flyspace-cli"}/`);
2410
- if (at > 0) prefix = entry.slice(0, at);
2434
+ const real = realpathSync3(entry);
2435
+ const at = real.indexOf(`/lib/node_modules/${"flyspace-cli"}/`);
2436
+ if (at > 0) return { prefix: real.slice(0, at), nodeBin: join11(real.slice(0, at), "bin") };
2411
2437
  } catch {
2412
2438
  }
2413
- const global = `-g${prefix ? ` --prefix "${prefix}"` : ""}`;
2439
+ return void 0;
2440
+ }
2441
+ function installTargets() {
2442
+ const files = [
2443
+ ...isHostServiceInstalled() ? [hostServiceFile()] : [],
2444
+ ...listProfiles().filter(isServiceInstalled).map(profileServiceFile)
2445
+ ];
2446
+ const entries2 = [process.argv[1] ?? "", ...files.map(serviceBinary).filter((b) => !!b)];
2447
+ const byPrefix = /* @__PURE__ */ new Map();
2448
+ for (const target of entries2.map(installTarget)) {
2449
+ if (target && !byPrefix.has(target.prefix)) byPrefix.set(target.prefix, target);
2450
+ }
2451
+ return [...byPrefix.values()];
2452
+ }
2453
+ function installedVersion(target) {
2454
+ try {
2455
+ const pkg = join11(target.prefix, "lib", "node_modules", "flyspace-cli", "package.json");
2456
+ return JSON.parse(readFileSync10(pkg, "utf-8")).version;
2457
+ } catch {
2458
+ return void 0;
2459
+ }
2460
+ }
2461
+ function npmInstallCommand(target) {
2462
+ const global = `-g${target ? ` --prefix "${target.prefix}"` : ""}`;
2414
2463
  return `npm install ${global} --ignore-scripts ${"flyspace-cli"}@latest && npm rebuild ${global} better-sqlite3`;
2415
2464
  }
2416
- function npmEnv() {
2465
+ function npmEnv(target) {
2417
2466
  const home2 = flyspaceHome();
2418
- return home2 ? { ...process.env, PATH: `${join11(home2, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2467
+ const nodeBin = target?.nodeBin ?? (home2 ? join11(home2, "node", "bin") : void 0);
2468
+ return nodeBin ? { ...process.env, PATH: `${nodeBin}:${process.env.PATH ?? ""}` } : process.env;
2419
2469
  }
2420
2470
  async function runUpgrade() {
2421
- const current = "0.1.29";
2471
+ const current = "0.1.31";
2422
2472
  console.log(`Current version: ${current}`);
2423
2473
  console.log("Checking for updates...\n");
2424
2474
  try {
2425
2475
  const latest2 = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", env: npmEnv() }).trim();
2426
- if (latest2 === current) {
2476
+ const stale = installTargets().filter((t) => installedVersion(t) !== latest2);
2477
+ if (stale.length === 0) {
2427
2478
  console.log(`Already on the latest version (${current}).`);
2428
2479
  return;
2429
2480
  }
2430
2481
  console.log(`New version available: ${latest2}`);
2431
- console.log("Upgrading...\n");
2432
- execSync6(npmInstallCommand(), { stdio: "inherit", env: npmEnv() });
2482
+ for (const target of stale) {
2483
+ console.log(`
2484
+ Upgrading ${target.prefix} (${installedVersion(target) ?? "unknown"})...
2485
+ `);
2486
+ execSync6(npmInstallCommand(target), { stdio: "inherit", env: npmEnv(target) });
2487
+ }
2433
2488
  console.log(`
2434
- Upgraded from ${current} to ${latest2}.`);
2489
+ Upgraded to ${latest2}.`);
2435
2490
  restartInstalledProfiles();
2436
2491
  console.log("\nReload your shell to use the new version:");
2437
2492
  console.log(" source ~/.zshrc # or source ~/.bashrc");
@@ -2447,13 +2502,13 @@ You can upgrade manually with: ${npmInstallCommand()}`);
2447
2502
  import { WebClient } from "@slack/web-api";
2448
2503
 
2449
2504
  // src/automations/config-file.ts
2450
- import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
2505
+ import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
2451
2506
  import yaml from "js-yaml";
2452
2507
  var EMPTY_CONFIG = { schedules: [], triggers: [], mention_triggers: [] };
2453
2508
  function loadAutomationsFile() {
2454
2509
  const filePath = paths.AUTOMATIONS_FILE;
2455
- if (!existsSync9(filePath)) return { ...EMPTY_CONFIG };
2456
- const raw = readFileSync10(filePath, "utf-8");
2510
+ if (!existsSync10(filePath)) return { ...EMPTY_CONFIG };
2511
+ const raw = readFileSync11(filePath, "utf-8");
2457
2512
  if (!raw.trim()) return { ...EMPTY_CONFIG };
2458
2513
  const parsed = yaml.load(raw);
2459
2514
  return {
@@ -2809,7 +2864,7 @@ async function runAutomationCmd(args) {
2809
2864
  }
2810
2865
 
2811
2866
  // src/commands/usage.ts
2812
- import { existsSync as existsSync10 } from "fs";
2867
+ import { existsSync as existsSync11 } from "fs";
2813
2868
  var HELP4 = `
2814
2869
  Usage: flyspace usage [options]
2815
2870
 
@@ -2936,7 +2991,7 @@ async function runUsage(args) {
2936
2991
  return;
2937
2992
  }
2938
2993
  const { days, by, explain, json } = parsed;
2939
- if (!existsSync10(paths.DB_FILE)) {
2994
+ if (!existsSync11(paths.DB_FILE)) {
2940
2995
  console.log("No usage recorded yet (the database does not exist).");
2941
2996
  return;
2942
2997
  }
@@ -3051,13 +3106,13 @@ async function runRestart() {
3051
3106
 
3052
3107
  // src/commands/logs.ts
3053
3108
  import { spawn as spawn3 } from "child_process";
3054
- import { existsSync as existsSync11 } from "fs";
3109
+ import { existsSync as existsSync12 } from "fs";
3055
3110
  import { join as join12 } from "path";
3056
3111
  async function runLogs(args) {
3057
3112
  const showErrors = args.includes("--error") || args.includes("-e");
3058
3113
  const filename = showErrors ? "custie-error.log" : "custie.log";
3059
3114
  const logFile = join12(paths.LOG_DIR, filename);
3060
- if (!existsSync11(logFile)) {
3115
+ if (!existsSync12(logFile)) {
3061
3116
  console.error(`Log file not found: ${logFile}`);
3062
3117
  console.error("Is the service installed? Run `flyspace install` first.");
3063
3118
  process.exit(1);
@@ -3078,7 +3133,7 @@ async function runLogs(args) {
3078
3133
 
3079
3134
  // src/commands/slack.ts
3080
3135
  import { execFileSync as execFileSync3 } from "child_process";
3081
- import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
3136
+ import { existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
3082
3137
  import { resolve as resolve4 } from "path";
3083
3138
  import { tmpdir as tmpdir2, homedir as homedir6 } from "os";
3084
3139
  import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
@@ -5164,7 +5219,7 @@ async function threadReplies(client2, nameOrId, ts2) {
5164
5219
  async function postMessage(client2, args) {
5165
5220
  const channelArg = getArg2(args, "--channel");
5166
5221
  const textFilePath = getArg2(args, "--text-file");
5167
- const text = textFilePath ? readFileSync11(textFilePath, "utf8") : getArg2(args, "--text");
5222
+ const text = textFilePath ? readFileSync12(textFilePath, "utf8") : getArg2(args, "--text");
5168
5223
  const threadTs = getArg2(args, "--thread");
5169
5224
  const markdown = args.includes("--markdown");
5170
5225
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -5262,7 +5317,7 @@ function defaultClaudeConfigDir() {
5262
5317
  return process.env.CLAUDE_CONFIG_DIR || resolve4(homedir6(), ".claude");
5263
5318
  }
5264
5319
  function hasClaudeCredentials(configDir2) {
5265
- if (existsSync12(resolve4(configDir2, ".credentials.json"))) {
5320
+ if (existsSync13(resolve4(configDir2, ".credentials.json"))) {
5266
5321
  return true;
5267
5322
  }
5268
5323
  if (process.platform === "darwin") {
@@ -5319,7 +5374,7 @@ async function ensureClaudeCodeReady() {
5319
5374
  }
5320
5375
  function getManifestYaml(botName) {
5321
5376
  const manifestPath = resolve4(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
5322
- const raw = readFileSync11(manifestPath, "utf-8");
5377
+ const raw = readFileSync12(manifestPath, "utf-8");
5323
5378
  return raw.replace(/name: Custie/g, `name: ${botName}`);
5324
5379
  }
5325
5380
  async function stepPersonalise() {
@@ -5748,10 +5803,10 @@ function printNextSteps() {
5748
5803
  console.log(" Run \x1B[1mflyspace service install\x1B[0m\n");
5749
5804
  }
5750
5805
  function copyDefaultPrompt(botName) {
5751
- if (!existsSync12(paths.PROMPT_FILE)) {
5806
+ if (!existsSync13(paths.PROMPT_FILE)) {
5752
5807
  const defaultPrompt = resolve4(paths.PACKAGE_ROOT, "system.default.md");
5753
- if (existsSync12(defaultPrompt)) {
5754
- const content = readFileSync11(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
5808
+ if (existsSync13(defaultPrompt)) {
5809
+ const content = readFileSync12(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
5755
5810
  writeFileSync7(paths.PROMPT_FILE, content);
5756
5811
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
5757
5812
  }
@@ -5764,7 +5819,7 @@ async function runSlackSetup(args) {
5764
5819
  const prereq = await ensureClaudeCodeReady();
5765
5820
  if (!prereq) return;
5766
5821
  const { claudeConfigDir } = prereq;
5767
- if (existsSync12(paths.CONFIG_FILE)) {
5822
+ if (existsSync13(paths.CONFIG_FILE)) {
5768
5823
  const reconfigure = await confirm4({
5769
5824
  message: `Config already exists at ${paths.CONFIG_FILE}. Reconfigure?`,
5770
5825
  default: false
@@ -6075,10 +6130,10 @@ async function runChatCmd(args) {
6075
6130
  }
6076
6131
 
6077
6132
  // src/commands/memory.ts
6078
- import { readFileSync as readFileSync13 } from "fs";
6133
+ import { readFileSync as readFileSync14 } from "fs";
6079
6134
 
6080
6135
  // src/store/claude-memory-import.ts
6081
- import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync12 } from "fs";
6136
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync13 } from "fs";
6082
6137
  import { homedir as homedir7 } from "os";
6083
6138
  import { basename as basename5, join as join14 } from "path";
6084
6139
  import dotenv from "dotenv";
@@ -6088,7 +6143,7 @@ function claudeMemoryDir(cwd, configDir2) {
6088
6143
  function profileClaudeConfigDirFor(profile) {
6089
6144
  const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join14(paths.PROFILES_DIR, profile);
6090
6145
  const file = join14(dir, "config.env");
6091
- const env = existsSync13(file) ? dotenv.parse(readFileSync12(file)) : {};
6146
+ const env = existsSync14(file) ? dotenv.parse(readFileSync13(file)) : {};
6092
6147
  return env["CLAUDE_CONFIG_DIR"]?.trim() || join14(homedir7(), ".claude");
6093
6148
  }
6094
6149
  var TYPE_MAP = {
@@ -6134,11 +6189,11 @@ function parseClaudeMemory(fileName, text, channelId) {
6134
6189
  return input2;
6135
6190
  }
6136
6191
  async function importClaudeMemories(dir, channelId, opts = {}) {
6137
- if (!existsSync13(dir)) throw new Error(`No Claude memory directory at ${dir}`);
6192
+ if (!existsSync14(dir)) throw new Error(`No Claude memory directory at ${dir}`);
6138
6193
  const files = readdirSync3(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
6139
6194
  const results = [];
6140
6195
  for (const file of files) {
6141
- const parsed = parseClaudeMemory(file, readFileSync12(join14(dir, file), "utf-8"), channelId);
6196
+ const parsed = parseClaudeMemory(file, readFileSync13(join14(dir, file), "utf-8"), channelId);
6142
6197
  if ("skip" in parsed) {
6143
6198
  results.push({ file, outcome: "skipped", reason: parsed.skip });
6144
6199
  continue;
@@ -6250,7 +6305,7 @@ ${memory.body}`);
6250
6305
  );
6251
6306
  }
6252
6307
  if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
6253
- const body = bodyArg === "-" ? readFileSync13(0, "utf-8") : bodyArg;
6308
+ const body = bodyArg === "-" ? readFileSync14(0, "utf-8") : bodyArg;
6254
6309
  const thread = getArg4(rest, "--thread");
6255
6310
  const { created, changed } = await saveMemory(
6256
6311
  target,
@@ -6348,13 +6403,13 @@ async function runMcpCmd(args) {
6348
6403
  }
6349
6404
 
6350
6405
  // src/commands/dashboard.ts
6351
- import { spawn as spawn7, spawnSync } from "child_process";
6352
- import { existsSync as existsSync32 } from "fs";
6406
+ import { spawn as spawn8, spawnSync } from "child_process";
6407
+ import { existsSync as existsSync33 } from "fs";
6353
6408
  import { platform as platform7 } from "os";
6354
- import { join as join28 } from "path";
6409
+ import { join as join29 } from "path";
6355
6410
 
6356
6411
  // src/reconcile.ts
6357
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
6412
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
6358
6413
  async function boundWorkspaces2() {
6359
6414
  return (await boundWorkspaces())?.workspaces ?? null;
6360
6415
  }
@@ -6365,12 +6420,12 @@ async function servedWorkspaces2() {
6365
6420
  async function reconcileProfile(profile) {
6366
6421
  return runWithProfile(profile, async () => {
6367
6422
  const log5 = [];
6368
- const hadConfigDir = existsSync14(paths.CONFIG_DIR);
6369
- const hadDataDir = existsSync14(paths.DATA_DIR);
6423
+ const hadConfigDir = existsSync15(paths.CONFIG_DIR);
6424
+ const hadDataDir = existsSync15(paths.DATA_DIR);
6370
6425
  ensureDirs();
6371
6426
  if (!hadConfigDir) log5.push(`config dir created (${paths.CONFIG_DIR})`);
6372
6427
  if (!hadDataDir) log5.push(`data dir created (${paths.DATA_DIR})`);
6373
- if (!existsSync14(paths.CONFIG_FILE)) {
6428
+ if (!existsSync15(paths.CONFIG_FILE)) {
6374
6429
  writeFileSync8(paths.CONFIG_FILE, `BOT_NAME=${profile}
6375
6430
  `);
6376
6431
  log5.push(`config.env written (${paths.CONFIG_FILE})`);
@@ -6527,7 +6582,7 @@ var requireDocsAccess = async (c, next) => {
6527
6582
  import { Hono as Hono29 } from "hono";
6528
6583
 
6529
6584
  // ../api/src/dashboard/public-share-routes.ts
6530
- import { existsSync as existsSync16, readFileSync as readFileSync15, statSync as statSync3 } from "fs";
6585
+ import { existsSync as existsSync17, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
6531
6586
  import { join as join16 } from "path";
6532
6587
  import { Hono as Hono5 } from "hono";
6533
6588
  import { GetObjectCommand } from "@aws-sdk/client-s3";
@@ -6993,11 +7048,11 @@ function injectShareHead(html, head) {
6993
7048
 
6994
7049
  // ../api/src/dashboard/app-icon.ts
6995
7050
  import { createHash as createHash2 } from "crypto";
6996
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
7051
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
6997
7052
  import { join as join15 } from "path";
6998
7053
  import sharp2 from "sharp";
6999
7054
  var PLATE = "#0A0A0F";
7000
- var BRAND_DIR = existsSync15(join15(paths.WEB_ROOT, "public")) ? join15(paths.WEB_ROOT, "public") : join15(paths.PACKAGE_ROOT, "dist", "brand");
7055
+ var BRAND_DIR = existsSync16(join15(paths.WEB_ROOT, "public")) ? join15(paths.WEB_ROOT, "public") : join15(paths.PACKAGE_ROOT, "dist", "brand");
7001
7056
  var MARK_FILE = join15(BRAND_DIR, "icons", "icon-512.png");
7002
7057
  var MARK_SVG_FILE = join15(BRAND_DIR, "favicon.svg");
7003
7058
  var LAYOUT = {
@@ -7022,7 +7077,7 @@ function roundedPlate(size, fill = PLATE) {
7022
7077
  }
7023
7078
  async function renderBadge(box) {
7024
7079
  const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
7025
- const raster = await sharp2(readFileSync14(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
7080
+ const raster = await sharp2(readFileSync15(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
7026
7081
  return sharp2(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
7027
7082
  }
7028
7083
  async function sampleBackground(logo) {
@@ -7077,7 +7132,7 @@ async function renderAppIcon({
7077
7132
  if (hit) return hit;
7078
7133
  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,
7079
7134
  // either on its usual dark plate or, for a favicon, trimmed and bare.
7080
- (transparent ? await renderBadge(size) : await sharp2(readFileSync14(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
7135
+ (transparent ? await renderBadge(size) : await sharp2(readFileSync15(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
7081
7136
  cache3.set(key, out);
7082
7137
  return out;
7083
7138
  }
@@ -7398,8 +7453,8 @@ var inFlight = /* @__PURE__ */ new Map();
7398
7453
  async function readOrCreateTranslation(input2) {
7399
7454
  const contentHash = translationHash(input2.title, input2.content);
7400
7455
  const key = `${input2.shareId}:${contentHash}:${input2.lang}`;
7401
- const running = inFlight.get(key);
7402
- if (running) return running;
7456
+ const running2 = inFlight.get(key);
7457
+ if (running2) return running2;
7403
7458
  const job = (async () => {
7404
7459
  const cached = await getDocShareTranslation(input2.shareId, input2.lang, contentHash);
7405
7460
  if (cached) return cached;
@@ -8295,7 +8350,7 @@ var shellCache = null;
8295
8350
  function readShell(path) {
8296
8351
  const { mtimeMs } = statSync3(path);
8297
8352
  if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
8298
- const html = readFileSync15(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
8353
+ const html = readFileSync16(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
8299
8354
  shellCache = { mtimeMs, html };
8300
8355
  return html;
8301
8356
  }
@@ -8316,16 +8371,16 @@ function serveBuildFile(c, webDist, dir, name) {
8316
8371
  return c.text("not found", 404);
8317
8372
  }
8318
8373
  const path = join16(webDist, dir, name);
8319
- if (!existsSync16(path)) return c.text("not found", 404);
8374
+ if (!existsSync17(path)) return c.text("not found", 404);
8320
8375
  c.header("Content-Type", type);
8321
8376
  c.header("Cache-Control", "public, max-age=31536000, immutable");
8322
8377
  if (ext === ".css") {
8323
- return c.body(readFileSync15(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
8378
+ return c.body(readFileSync16(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
8324
8379
  }
8325
8380
  if (ext === ".js") {
8326
- return c.body(readFileSync15(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
8381
+ return c.body(readFileSync16(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
8327
8382
  }
8328
- return c.body(new Uint8Array(readFileSync15(path)));
8383
+ return c.body(new Uint8Array(readFileSync16(path)));
8329
8384
  }
8330
8385
  var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
8331
8386
  function servePublishedHtml(c, share) {
@@ -8462,7 +8517,7 @@ ${analytics}` : head);
8462
8517
  const lang = canonicalLanguage(requested);
8463
8518
  if (!isTranslatableLanguage(lang)) return next();
8464
8519
  const shell = join16(webDist, "index.html");
8465
- if (!existsSync16(shell)) return next();
8520
+ if (!existsSync17(shell)) return next();
8466
8521
  const share = await getDocShare(c.req.param("id"));
8467
8522
  if (!share) return next();
8468
8523
  if (requested !== lang) return c.redirect(`/share/${share.id}/${lang}`, 302);
@@ -8498,7 +8553,7 @@ ${analytics}` : head);
8498
8553
  if (!share) return next();
8499
8554
  if (share.kind === "html") return servePublishedHtml(c, share);
8500
8555
  const shell = join16(webDist, "index.html");
8501
- if (!existsSync16(shell)) return next();
8556
+ if (!existsSync17(shell)) return next();
8502
8557
  return readerPage(
8503
8558
  c,
8504
8559
  share,
@@ -8564,7 +8619,7 @@ function createGuestRouter() {
8564
8619
  }
8565
8620
 
8566
8621
  // ../api/src/dashboard/auth-routes.ts
8567
- import { Hono as Hono8 } from "hono";
8622
+ import { Hono as Hono9 } from "hono";
8568
8623
 
8569
8624
  // ../api/src/dashboard/supabase-server-client.ts
8570
8625
  import { createServerClient, parseCookieHeader, serializeCookieHeader } from "@supabase/ssr";
@@ -8675,7 +8730,7 @@ function pruneExpired(now) {
8675
8730
  }
8676
8731
  }
8677
8732
  var SUPPORT_NAME = "Flyspace Support";
8678
- var SUPPORT_AVATAR_URL = "/logo/flyspace-mark.svg";
8733
+ var SUPPORT_AVATAR_URL = "/logo/flyspace-support.svg";
8679
8734
  async function resolveAuthContext(c, opts = {}) {
8680
8735
  const cookie = c.req?.header("Cookie") ?? "";
8681
8736
  if (!cookie) return resolveAuthContextUncached(c);
@@ -8805,6 +8860,9 @@ async function verifyTurnstileToken(token) {
8805
8860
  return data.action === void 0 || data.action === "login";
8806
8861
  }
8807
8862
 
8863
+ // ../api/src/dashboard/onboarding-routes.ts
8864
+ import { Hono as Hono8 } from "hono";
8865
+
8808
8866
  // ../api/src/store/device-store.ts
8809
8867
  import { createHash as createHash7, createPublicKey as createPublicKey2, randomBytes as randomBytes4, randomInt } from "crypto";
8810
8868
  var DEVICE_CODE_TTL_MS = 10 * 6e4;
@@ -9031,27 +9089,84 @@ async function userEmail(userId) {
9031
9089
  return data.user?.email ?? null;
9032
9090
  }
9033
9091
 
9034
- // ../api/src/dashboard/onboarding-routes.ts
9035
- import { Hono as Hono7 } from "hono";
9036
-
9037
9092
  // ../api/src/dashboard/agent-bridge-data.ts
9038
- import { randomBytes as randomBytes5, randomUUID as randomUUID2, verify as verifyRaw } from "crypto";
9093
+ import { randomBytes as randomBytes5, randomUUID as randomUUID3, verify as verifyRaw } from "crypto";
9039
9094
  import { WebSocket, WebSocketServer } from "ws";
9040
9095
 
9041
9096
  // ../api/src/dashboard/host-identity.ts
9042
9097
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey3, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
9043
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
9044
- import { dirname as dirname5, join as join17 } from "path";
9098
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
9099
+ import { dirname as dirname6, join as join17 } from "path";
9045
9100
  function hostPrivateKeyPath() {
9046
9101
  return process.env["FLYSPACE_HOST_KEY_PATH"] || join17(paths.BASE_CONFIG_DIR, "host.key");
9047
9102
  }
9048
9103
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
9049
- if (!existsSync17(path)) {
9104
+ if (!existsSync18(path)) {
9050
9105
  throw new Error(
9051
9106
  `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.`
9052
9107
  );
9053
9108
  }
9054
- return createPrivateKey2(readFileSync16(path, "utf8"));
9109
+ return createPrivateKey2(readFileSync17(path, "utf8"));
9110
+ }
9111
+
9112
+ // ../api/src/dashboard/uploads-routes.ts
9113
+ import { randomUUID as randomUUID2 } from "crypto";
9114
+ import { Hono as Hono7 } from "hono";
9115
+ import { PutObjectCommand as PutObjectCommand2, GetObjectCommand as GetObjectCommand3 } from "@aws-sdk/client-s3";
9116
+ import { getSignedUrl as getSignedUrl3 } from "@aws-sdk/s3-request-presigner";
9117
+ var MAX_FILE_BYTES = 10 * 1024 * 1024;
9118
+ var PRESIGN_TTL_SECONDS2 = 300;
9119
+ var KEY_PREFIX = "chat";
9120
+ var READABLE_PREFIXES = [KEY_PREFIX, SCRATCH_PREFIX];
9121
+ function sanitizeFilename(name) {
9122
+ const cleaned = name.replace(/[/\\]/g, "_").replace(/\s+/g, "-").replace(/[^\w.-]/g, "").trim();
9123
+ return (cleaned || "file").slice(-100);
9124
+ }
9125
+ function encodeKeyForUrl(key) {
9126
+ return key.split("/").map(encodeURIComponent).join("/");
9127
+ }
9128
+ async function presignAttachmentRead(key) {
9129
+ if (!READABLE_PREFIXES.some((prefix) => key.startsWith(`${prefix}/`))) return null;
9130
+ return getSignedUrl3(getR2Client(), new GetObjectCommand3({ Bucket: getR2Bucket(), Key: key }), {
9131
+ expiresIn: PRESIGN_TTL_SECONDS2
9132
+ });
9133
+ }
9134
+ function createUploadsRouter() {
9135
+ const app = new Hono7();
9136
+ app.post("/uploads", async (c) => {
9137
+ const body = await c.req.parseBody().catch(() => null);
9138
+ const file = body?.["file"];
9139
+ if (!(file instanceof File)) return c.json({ error: "missing file" }, 400);
9140
+ if (file.size > MAX_FILE_BYTES) {
9141
+ return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
9142
+ }
9143
+ const key = `${KEY_PREFIX}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
9144
+ const buffer = Buffer.from(await file.arrayBuffer());
9145
+ await getR2Client().send(
9146
+ new PutObjectCommand2({
9147
+ Bucket: getR2Bucket(),
9148
+ Key: key,
9149
+ Body: buffer,
9150
+ ContentType: file.type || "application/octet-stream"
9151
+ })
9152
+ );
9153
+ return c.json(
9154
+ {
9155
+ url: `/api/attachments/${encodeKeyForUrl(key)}`,
9156
+ name: file.name,
9157
+ type: file.type,
9158
+ size: file.size
9159
+ },
9160
+ 201
9161
+ );
9162
+ });
9163
+ app.get("/attachments/:key{.+}", async (c) => {
9164
+ const url = await presignAttachmentRead(c.req.param("key"));
9165
+ if (!url) return c.json({ error: "not found" }, 404);
9166
+ c.header("Cache-Control", `private, max-age=${PRESIGN_TTL_SECONDS2 - 60}`);
9167
+ return c.redirect(url, 302);
9168
+ });
9169
+ return app;
9055
9170
  }
9056
9171
 
9057
9172
  // ../core/src/bridge-protocol.ts
@@ -9207,11 +9322,16 @@ var CHANNEL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
9207
9322
  function isWorkspaceOwner(ctx) {
9208
9323
  return ctx?.role === "admin" && !ctx.isSupport;
9209
9324
  }
9325
+ async function isHostOwner(ctx) {
9326
+ if (ctx.role !== "admin") return false;
9327
+ if (ctx.isSupport) return true;
9328
+ return (await listWorkspaceDevices(ctx.workspaceId)).some((d) => d.userId === ctx.userId);
9329
+ }
9210
9330
  function hostUnavailable(c, agent, method) {
9211
9331
  return c.json({ error: "host offline", reason: agent.getHostCallFailure?.(method) ?? "offline" }, 503);
9212
9332
  }
9213
9333
  function createOnboardingRouter(agent) {
9214
- const app = new Hono7();
9334
+ const app = new Hono8();
9215
9335
  app.get("/scan", async (c) => {
9216
9336
  const ctx = await resolveAuthContext(c);
9217
9337
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -9349,7 +9469,7 @@ async function ensureGreetingThread(workspaceId, channelId, ownerName) {
9349
9469
 
9350
9470
  // ../api/src/dashboard/auth-routes.ts
9351
9471
  function createAuthRouter() {
9352
- const auth = new Hono8();
9472
+ const auth = new Hono9();
9353
9473
  const emailedLink = (deliver) => async (c) => {
9354
9474
  const body = await c.req.json().catch(() => ({}));
9355
9475
  const email = body.email?.trim().toLowerCase();
@@ -9389,7 +9509,7 @@ function createAuthRouter() {
9389
9509
  owner: isWorkspaceOwner(ctx),
9390
9510
  // Only the Host's owner can run `flyspace upgrade` on it, so other
9391
9511
  // admins never see the upgrade banner. Support staff can tell them to.
9392
- hostOwner: ctx.role === "admin" && (ctx.isSupport === true || (await listWorkspaceDevices(ctx.workspaceId)).some((d) => d.userId === ctx.userId))
9512
+ hostOwner: await isHostOwner(ctx)
9393
9513
  });
9394
9514
  });
9395
9515
  auth.patch("/handle", async (c) => {
@@ -9429,7 +9549,7 @@ var requireWorkspaceMembership = async (c, next) => {
9429
9549
  };
9430
9550
 
9431
9551
  // ../api/src/dashboard/billing-routes.ts
9432
- import { Hono as Hono9 } from "hono";
9552
+ import { Hono as Hono10 } from "hono";
9433
9553
  function isPlan(value) {
9434
9554
  return value === "local" || value === "cloud";
9435
9555
  }
@@ -9445,7 +9565,7 @@ async function requireActivePlan(c, next) {
9445
9565
  return next();
9446
9566
  }
9447
9567
  function createBillingRouter() {
9448
- const billing = new Hono9();
9568
+ const billing = new Hono10();
9449
9569
  billing.get("/", async (c) => {
9450
9570
  const auth = c.get("auth");
9451
9571
  const { planStatus, stripeSubscriptionId } = await getWorkspaceBilling(auth.workspaceId);
@@ -9501,7 +9621,7 @@ function createBillingRouter() {
9501
9621
  }
9502
9622
 
9503
9623
  // ../api/src/dashboard/admin-routes.ts
9504
- import { Hono as Hono10 } from "hono";
9624
+ import { Hono as Hono11 } from "hono";
9505
9625
 
9506
9626
  // ../api/src/dashboard/admin-middleware.ts
9507
9627
  function platformAdminEmails() {
@@ -9527,7 +9647,7 @@ var requirePlatformOperator = async (c, next) => {
9527
9647
  // ../api/src/dashboard/admin-routes.ts
9528
9648
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9529
9649
  function createAdminRouter() {
9530
- const admin = new Hono10();
9650
+ const admin = new Hono11();
9531
9651
  admin.use("*", requirePlatformOperator);
9532
9652
  admin.get("/workspaces", async (c) => {
9533
9653
  const summaries = await listWorkspaceSummaries();
@@ -9640,11 +9760,11 @@ var BOT_LABEL = "Workspace Agent";
9640
9760
  var BOT_DM_KEY = "claude";
9641
9761
 
9642
9762
  // ../api/src/dashboard/data-api.ts
9643
- import { existsSync as existsSync19 } from "fs";
9763
+ import { existsSync as existsSync20 } from "fs";
9644
9764
  import sharp5 from "sharp";
9645
9765
 
9646
9766
  // ../api/src/dashboard/automations-routes.ts
9647
- import { Hono as Hono11 } from "hono";
9767
+ import { Hono as Hono12 } from "hono";
9648
9768
  var TYPES2 = ["schedule", "trigger", "mention-trigger"];
9649
9769
  function isType(t) {
9650
9770
  return TYPES2.includes(t);
@@ -9661,7 +9781,7 @@ function statusFor(message) {
9661
9781
  return 400;
9662
9782
  }
9663
9783
  function createAutomationWriteRouter() {
9664
- const app = new Hono11();
9784
+ const app = new Hono12();
9665
9785
  const manager = () => new AutomationManager();
9666
9786
  app.get("/:type/:name", (c) => {
9667
9787
  const item = manager().get(c.req.param("name"));
@@ -9717,9 +9837,9 @@ function createAutomationWriteRouter() {
9717
9837
  }
9718
9838
 
9719
9839
  // ../api/src/dashboard/categories-routes.ts
9720
- import { Hono as Hono12 } from "hono";
9840
+ import { Hono as Hono13 } from "hono";
9721
9841
  function createCategoryWriteRouter() {
9722
- const app = new Hono12();
9842
+ const app = new Hono13();
9723
9843
  app.get("/", async (c) => {
9724
9844
  const { data, error: error2 } = await getSupabaseClient().from("categories").select("*").eq("profile", getProfile()).order("position", { ascending: true });
9725
9845
  if (error2) return c.json({ error: error2.message }, 500);
@@ -9773,7 +9893,7 @@ function createCategoryWriteRouter() {
9773
9893
  }
9774
9894
 
9775
9895
  // ../api/src/dashboard/chat-routes.ts
9776
- import { Hono as Hono13 } from "hono";
9896
+ import { Hono as Hono14 } from "hono";
9777
9897
 
9778
9898
  // ../api/src/dashboard/fork-summary.ts
9779
9899
  var GROQ_API_URL4 = "https://api.groq.com/openai/v1/chat/completions";
@@ -10057,8 +10177,8 @@ async function translateThread(threadId, lang, messages) {
10057
10177
  if (messages.length === 0) return [];
10058
10178
  const shape = createHash8("sha256").update(messages.map((m) => translationHash2(m.author, m.body)).join("\n")).digest("hex");
10059
10179
  const key = `${threadId}:${lang}:${shape}`;
10060
- const running = inFlight4.get(key);
10061
- if (running) return running;
10180
+ const running2 = inFlight4.get(key);
10181
+ if (running2) return running2;
10062
10182
  const job = (async () => {
10063
10183
  const hashes = messages.map((m) => ({
10064
10184
  replyId: m.id,
@@ -10260,7 +10380,7 @@ async function generateAutoTitle(threadId, channelId, body) {
10260
10380
  await supabase.from("threads").update({ auto_title: autoTitle }).eq("id", threadId).is("title", null);
10261
10381
  }
10262
10382
  function createChatRouter(agent = {}) {
10263
- const app = new Hono13();
10383
+ const app = new Hono14();
10264
10384
  const {
10265
10385
  onChatMessage: onMessage,
10266
10386
  isAgentThinking,
@@ -10774,7 +10894,7 @@ function createChatRouter(agent = {}) {
10774
10894
  }
10775
10895
 
10776
10896
  // ../api/src/dashboard/doc-shares-routes.ts
10777
- import { Hono as Hono14 } from "hono";
10897
+ import { Hono as Hono15 } from "hono";
10778
10898
 
10779
10899
  // ../api/src/dashboard/doc-share-features.ts
10780
10900
  import { DeleteObjectCommand as DeleteObjectCommand2 } from "@aws-sdk/client-s3";
@@ -10834,7 +10954,7 @@ async function updateShareFeatures(share, requested) {
10834
10954
 
10835
10955
  // ../api/src/dashboard/doc-share-publish.ts
10836
10956
  import { randomBytes as randomBytes6 } from "crypto";
10837
- import { CopyObjectCommand, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
10957
+ import { CopyObjectCommand, PutObjectCommand as PutObjectCommand3 } from "@aws-sdk/client-s3";
10838
10958
 
10839
10959
  // ../api/src/dashboard/bridge-types.ts
10840
10960
  var SHARE_ASSET_PLACEHOLDER = "/share/{{flyspace-share-id}}/assets/";
@@ -10930,7 +11050,7 @@ async function publishDocShare(input2, readBundle) {
10930
11050
  );
10931
11051
  } else {
10932
11052
  await client2.send(
10933
- new PutObjectCommand2({
11053
+ new PutObjectCommand3({
10934
11054
  Bucket: bucket,
10935
11055
  Key: key,
10936
11056
  Body: Buffer.from(asset.data, "base64"),
@@ -11109,7 +11229,7 @@ function resolveChannel2(c) {
11109
11229
  }
11110
11230
  var LIST_LIMIT = 500;
11111
11231
  function createDocShareListRouter() {
11112
- const app = new Hono14();
11232
+ const app = new Hono15();
11113
11233
  app.get("/shares", async (c) => {
11114
11234
  const viewer = c.get("auth");
11115
11235
  const readable = await filterReadableDocsChannels(
@@ -11153,7 +11273,7 @@ function createDocShareListRouter() {
11153
11273
  return app;
11154
11274
  }
11155
11275
  function createDocShareWriteRouter(agent = {}) {
11156
- const app = new Hono14();
11276
+ const app = new Hono15();
11157
11277
  app.use("/:id/docs/share", requireDocsAccess);
11158
11278
  app.use("/:id/docs/share/*", requireDocsAccess);
11159
11279
  app.use("/:id/docs/shares", requireDocsAccess);
@@ -11326,7 +11446,7 @@ function createDocShareWriteRouter(agent = {}) {
11326
11446
  }
11327
11447
 
11328
11448
  // ../api/src/dashboard/search-routes.ts
11329
- import { Hono as Hono15 } from "hono";
11449
+ import { Hono as Hono16 } from "hono";
11330
11450
  function matchScore(q, fields) {
11331
11451
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
11332
11452
  if (terms.length === 0) return 0;
@@ -11474,7 +11594,7 @@ function searchAutomations(q) {
11474
11594
  }
11475
11595
  var RESULT_LIMIT = 30;
11476
11596
  function createSearchRouter() {
11477
- const app = new Hono15();
11597
+ const app = new Hono16();
11478
11598
  app.get("/", async (c) => {
11479
11599
  const q = c.req.query("q")?.trim();
11480
11600
  if (!q) return c.json({ results: [] });
@@ -11490,14 +11610,14 @@ function createSearchRouter() {
11490
11610
  }
11491
11611
 
11492
11612
  // ../api/src/dashboard/feature-flags-routes.ts
11493
- import { Hono as Hono16 } from "hono";
11613
+ import { Hono as Hono17 } from "hono";
11494
11614
  async function isFeatureEnabled(c, flagId) {
11495
11615
  const auth = c.get("auth");
11496
11616
  const enabled = await listEnabledFeatureFlags(auth.workspaceId);
11497
11617
  return resolveFeatureFlags(enabled, isDevHost(c))[flagId];
11498
11618
  }
11499
11619
  function createFeatureFlagsRouter() {
11500
- const app = new Hono16();
11620
+ const app = new Hono17();
11501
11621
  app.get("/", async (c) => {
11502
11622
  const auth = c.get("auth");
11503
11623
  const devHost = isDevHost(c);
@@ -11522,67 +11642,6 @@ function createFeatureFlagsRouter() {
11522
11642
  return app;
11523
11643
  }
11524
11644
 
11525
- // ../api/src/dashboard/uploads-routes.ts
11526
- import { randomUUID as randomUUID3 } from "crypto";
11527
- import { Hono as Hono17 } from "hono";
11528
- import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand3 } from "@aws-sdk/client-s3";
11529
- import { getSignedUrl as getSignedUrl3 } from "@aws-sdk/s3-request-presigner";
11530
- var MAX_FILE_BYTES = 10 * 1024 * 1024;
11531
- var PRESIGN_TTL_SECONDS2 = 300;
11532
- var KEY_PREFIX = "chat";
11533
- var READABLE_PREFIXES = [KEY_PREFIX, SCRATCH_PREFIX];
11534
- function sanitizeFilename(name) {
11535
- const cleaned = name.replace(/[/\\]/g, "_").replace(/\s+/g, "-").replace(/[^\w.-]/g, "").trim();
11536
- return (cleaned || "file").slice(-100);
11537
- }
11538
- function encodeKeyForUrl(key) {
11539
- return key.split("/").map(encodeURIComponent).join("/");
11540
- }
11541
- function createUploadsRouter() {
11542
- const app = new Hono17();
11543
- app.post("/uploads", async (c) => {
11544
- const body = await c.req.parseBody().catch(() => null);
11545
- const file = body?.["file"];
11546
- if (!(file instanceof File)) return c.json({ error: "missing file" }, 400);
11547
- if (file.size > MAX_FILE_BYTES) {
11548
- return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
11549
- }
11550
- const key = `${KEY_PREFIX}/${getProfile()}/${randomUUID3()}-${sanitizeFilename(file.name)}`;
11551
- const buffer = Buffer.from(await file.arrayBuffer());
11552
- await getR2Client().send(
11553
- new PutObjectCommand3({
11554
- Bucket: getR2Bucket(),
11555
- Key: key,
11556
- Body: buffer,
11557
- ContentType: file.type || "application/octet-stream"
11558
- })
11559
- );
11560
- return c.json(
11561
- {
11562
- url: `/api/attachments/${encodeKeyForUrl(key)}`,
11563
- name: file.name,
11564
- type: file.type,
11565
- size: file.size
11566
- },
11567
- 201
11568
- );
11569
- });
11570
- app.get("/attachments/:key{.+}", async (c) => {
11571
- const key = c.req.param("key");
11572
- if (!READABLE_PREFIXES.some((prefix) => key.startsWith(`${prefix}/`))) {
11573
- return c.json({ error: "not found" }, 404);
11574
- }
11575
- const url = await getSignedUrl3(
11576
- getR2Client(),
11577
- new GetObjectCommand3({ Bucket: getR2Bucket(), Key: key }),
11578
- { expiresIn: PRESIGN_TTL_SECONDS2 }
11579
- );
11580
- c.header("Cache-Control", `private, max-age=${PRESIGN_TTL_SECONDS2 - 60}`);
11581
- return c.redirect(url, 302);
11582
- });
11583
- return app;
11584
- }
11585
-
11586
11645
  // ../api/src/dashboard/transcribe-routes.ts
11587
11646
  import { Hono as Hono18 } from "hono";
11588
11647
  var GROQ_TRANSCRIBE_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
@@ -13240,7 +13299,7 @@ async function sessionUsageForThreads(workspace, threadTsList) {
13240
13299
  }
13241
13300
 
13242
13301
  // ../api/src/dashboard/usage-report.ts
13243
- import { existsSync as existsSync18 } from "fs";
13302
+ import { existsSync as existsSync19 } from "fs";
13244
13303
  function mergeByLabel(buckets) {
13245
13304
  const merged = /* @__PURE__ */ new Map();
13246
13305
  for (const bucket of buckets) {
@@ -13286,7 +13345,7 @@ function toReport(days, rows, labelChannel) {
13286
13345
  };
13287
13346
  }
13288
13347
  function fromLocalStore(days, labelChannel) {
13289
- const store = existsSync18(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
13348
+ const store = existsSync19(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
13290
13349
  if (!store) return { ...EMPTY, days };
13291
13350
  const window = days === 0 ? void 0 : days;
13292
13351
  try {
@@ -13328,7 +13387,7 @@ async function buildChannelUsageReport(channelId, days, labelChannel) {
13328
13387
  const { totals, byDay } = await channelUsageForWorkspace(workspace, aliasIds, window);
13329
13388
  return { days, totals: totals.runs > 0 ? totals : null, byDay };
13330
13389
  } catch {
13331
- const store = existsSync18(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
13390
+ const store = existsSync19(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
13332
13391
  if (!store) return { days, totals: null, byDay: [] };
13333
13392
  try {
13334
13393
  const targetLabel = labelChannel(channelId);
@@ -13350,7 +13409,7 @@ async function buildThreadUsage(threadTs) {
13350
13409
  if (totals.runs === 0) return { threadTs, totals: null, byModel: [] };
13351
13410
  return { threadTs, totals, byModel };
13352
13411
  } catch {
13353
- const store = existsSync18(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
13412
+ const store = existsSync19(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
13354
13413
  if (!store) return { threadTs, totals: null, byModel: [] };
13355
13414
  try {
13356
13415
  const totals = store.threadTotals(threadTs);
@@ -13701,6 +13760,7 @@ async function resolveThreadLinks(sessions2) {
13701
13760
  }
13702
13761
 
13703
13762
  // ../api/src/dashboard/data-api.ts
13763
+ var SELF_UPGRADE_MIN_VERSION = "0.1.30";
13704
13764
  function registerDataRoutes(api, agent = {}) {
13705
13765
  api.get("/profile", (c) => c.json({ profile: getProfile() }));
13706
13766
  api.get("/host/status", async (c) => {
@@ -13714,7 +13774,20 @@ function registerDataRoutes(api, agent = {}) {
13714
13774
  const cliVersion = agent.getHostCliVersion?.(getProfile()) ?? null;
13715
13775
  const latest2 = cliVersion ? await latestCliVersion() : null;
13716
13776
  const outdated = !!(cliVersion && latest2 && isOlderVersion(cliVersion, latest2));
13717
- return c.json({ online: !paused, cliVersion, ...outdated ? { latestCliVersion: latest2 } : {} });
13777
+ const selfUpgrade = !!cliVersion && !isOlderVersion(cliVersion, SELF_UPGRADE_MIN_VERSION);
13778
+ return c.json({
13779
+ online: !paused,
13780
+ cliVersion,
13781
+ selfUpgrade,
13782
+ ...outdated ? { latestCliVersion: latest2 } : {}
13783
+ });
13784
+ });
13785
+ api.post("/host/upgrade", async (c) => {
13786
+ const auth = c.get("auth");
13787
+ if (!await isHostOwner(auth)) return c.json({ error: "only the Host owner can upgrade it" }, 403);
13788
+ const res = await agent.upgradeHost?.();
13789
+ if (!res) return c.json({ error: "Host is offline" }, 503);
13790
+ return c.json(res, 202);
13718
13791
  });
13719
13792
  api.get("/channels", async (c) => {
13720
13793
  const auth = c.get("auth");
@@ -13774,7 +13847,7 @@ function registerDataRoutes(api, agent = {}) {
13774
13847
  runs.set(name, { lastRun: run4.lastRunAt.toISOString(), lastStatus: run4.lastStatus });
13775
13848
  }
13776
13849
  } catch {
13777
- const runStore = existsSync19(paths.DB_FILE) ? getLocalCache().openAutomationRunStore(paths.DB_FILE) : null;
13850
+ const runStore = existsSync20(paths.DB_FILE) ? getLocalCache().openAutomationRunStore(paths.DB_FILE) : null;
13778
13851
  if (runStore) {
13779
13852
  try {
13780
13853
  for (const run4 of runStore.listLastRuns()) {
@@ -13836,7 +13909,7 @@ function registerDataRoutes(api, agent = {}) {
13836
13909
  const runs = (await recentRunsForWorkspace(getProfile(), 200)).map(toResponse5);
13837
13910
  return c.json({ runs });
13838
13911
  } catch {
13839
- const runStore = existsSync19(paths.DB_FILE) ? getLocalCache().openAutomationRunStore(paths.DB_FILE) : null;
13912
+ const runStore = existsSync20(paths.DB_FILE) ? getLocalCache().openAutomationRunStore(paths.DB_FILE) : null;
13840
13913
  if (!runStore) return c.json({ runs: [] });
13841
13914
  try {
13842
13915
  const runs = runStore.listRuns(200).map(toResponse5);
@@ -13902,9 +13975,9 @@ function registerDataRoutes(api, agent = {}) {
13902
13975
  void broadcastHostStatusChange();
13903
13976
  let cancelled = 0;
13904
13977
  if (body.paused) {
13905
- const running = await agent.listAgentRuns?.() ?? [];
13906
- if (running.length > 0) {
13907
- const { data } = await getSupabaseClient().from("threads").select("id, channel_id").in("id", running);
13978
+ const running2 = await agent.listAgentRuns?.() ?? [];
13979
+ if (running2.length > 0) {
13980
+ const { data } = await getSupabaseClient().from("threads").select("id, channel_id").in("id", running2);
13908
13981
  const profile = getProfile();
13909
13982
  for (const t of data ?? []) {
13910
13983
  if (findOwningProfile(t.channel_id) !== profile) continue;
@@ -13922,7 +13995,7 @@ function registerDataRoutes(api, agent = {}) {
13922
13995
  const host = trustedHost(c);
13923
13996
  const known = await listKnownWorkspaces();
13924
13997
  const hostProfile = profileFromHost(host, known);
13925
- const running = new Set(await agent.listRunningProfiles?.() ?? []);
13998
+ const running2 = new Set(await agent.listRunningProfiles?.() ?? []);
13926
13999
  const auth = c.get("auth");
13927
14000
  const [{ data: memberships }, workspaces] = await Promise.all([
13928
14001
  getSupabaseClient().from("memberships").select("workspace_id").eq("user_id", auth.userId),
@@ -13939,7 +14012,7 @@ function registerDataRoutes(api, agent = {}) {
13939
14012
  return {
13940
14013
  name,
13941
14014
  active: name === active,
13942
- running: running.has(name),
14015
+ running: running2.has(name),
13943
14016
  displayName: branding.displayName,
13944
14017
  logo: branding.logo,
13945
14018
  hostname: (hostProfile ? workspaceHostname(host, name) : null) ?? void 0
@@ -13958,7 +14031,7 @@ function registerDataRoutes(api, agent = {}) {
13958
14031
  } catch {
13959
14032
  latest2 = /* @__PURE__ */ new Map();
13960
14033
  totals = /* @__PURE__ */ new Map();
13961
- const usageStore = existsSync19(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
14034
+ const usageStore = existsSync20(paths.DB_FILE) ? getLocalCache().openUsageStore(paths.DB_FILE) : null;
13962
14035
  if (usageStore) {
13963
14036
  try {
13964
14037
  latest2 = usageStore.latestByThread(threadTsList);
@@ -14224,11 +14297,11 @@ function createDeviceAuthRouter() {
14224
14297
  }
14225
14298
 
14226
14299
  // ../api/src/dashboard/skills-routes.ts
14227
- import { dirname as dirname6 } from "path";
14300
+ import { dirname as dirname7 } from "path";
14228
14301
 
14229
14302
  // ../api/src/dashboard/skill-mirror.ts
14230
14303
  import { createHash as createHash11 } from "crypto";
14231
- import { existsSync as existsSync20, statSync as statSync4 } from "fs";
14304
+ import { existsSync as existsSync21, statSync as statSync4 } from "fs";
14232
14305
  var SYNC_INTERVAL_MS = 6e4;
14233
14306
  var UPSERT_BATCH = 50;
14234
14307
  function mirrorScopeOf(ref) {
@@ -14236,7 +14309,7 @@ function mirrorScopeOf(ref) {
14236
14309
  }
14237
14310
  function isDir(path) {
14238
14311
  try {
14239
- return existsSync20(path) && statSync4(path).isDirectory();
14312
+ return existsSync21(path) && statSync4(path).isDirectory();
14240
14313
  } catch {
14241
14314
  return false;
14242
14315
  }
@@ -14313,16 +14386,16 @@ function installSkillSync(profiles, options = {}) {
14313
14386
  if (!databaseReachable()) return () => {
14314
14387
  };
14315
14388
  const { intervalMs = SYNC_INTERVAL_MS, onError = (e) => console.error("[skill-sync]", e) } = options;
14316
- let running = false;
14389
+ let running2 = false;
14317
14390
  const tick = async () => {
14318
- if (running) return;
14319
- running = true;
14391
+ if (running2) return;
14392
+ running2 = true;
14320
14393
  try {
14321
14394
  for (const profile of profiles) {
14322
14395
  await runWithProfile(profile, () => pushSkillScopes()).catch(onError);
14323
14396
  }
14324
14397
  } finally {
14325
- running = false;
14398
+ running2 = false;
14326
14399
  }
14327
14400
  };
14328
14401
  void tick();
@@ -14561,12 +14634,12 @@ function createGatedRouter(agent, register) {
14561
14634
  }
14562
14635
 
14563
14636
  // src/dashboard/host-api.ts
14564
- import { existsSync as existsSync29, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
14637
+ import { existsSync as existsSync30, readFileSync as readFileSync24, statSync as statSync8 } from "fs";
14565
14638
  import { join as join26 } from "path";
14566
14639
  import sharp6 from "sharp";
14567
14640
 
14568
14641
  // src/dashboard/channels-routes.ts
14569
- import { existsSync as existsSync21, mkdirSync as mkdirSync10 } from "fs";
14642
+ import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
14570
14643
  import { Hono as Hono30 } from "hono";
14571
14644
  function coerceAccess(raw) {
14572
14645
  if (raw === "open" || raw === "*" || raw === "all") return raw;
@@ -14605,7 +14678,7 @@ function coerceMemberEmails(raw) {
14605
14678
  function ensureCwdDir(cwd) {
14606
14679
  const expanded = expandPath(cwd);
14607
14680
  if (isLogicalCwd(cwd) || isUnderLogicalRoot(expanded)) return;
14608
- if (!existsSync21(expanded)) mkdirSync10(expanded, { recursive: true });
14681
+ if (!existsSync22(expanded)) mkdirSync10(expanded, { recursive: true });
14609
14682
  }
14610
14683
  function toResponse3(channel) {
14611
14684
  return {
@@ -14755,13 +14828,13 @@ function createChannelWriteRouter() {
14755
14828
  import {
14756
14829
  closeSync as closeSync2,
14757
14830
  createReadStream,
14758
- existsSync as existsSync25,
14831
+ existsSync as existsSync26,
14759
14832
  lstatSync as lstatSync2,
14760
14833
  mkdirSync as mkdirSync11,
14761
14834
  openSync as openSync2,
14762
14835
  readSync as readSync2,
14763
14836
  readdirSync as readdirSync5,
14764
- readFileSync as readFileSync20,
14837
+ readFileSync as readFileSync21,
14765
14838
  realpathSync as realpathSync4,
14766
14839
  renameSync,
14767
14840
  rmSync as rmSync8,
@@ -14769,12 +14842,12 @@ import {
14769
14842
  writeFileSync as writeFileSync11
14770
14843
  } from "fs";
14771
14844
  import { Readable } from "stream";
14772
- import { basename as basename8, dirname as dirname7, extname, join as join23, relative, resolve as resolve5, sep } from "path";
14845
+ import { basename as basename8, dirname as dirname8, extname, join as join23, relative, resolve as resolve5, sep } from "path";
14773
14846
  import { Hono as Hono31 } from "hono";
14774
14847
  import { streamSSE as streamSSE2 } from "hono/streaming";
14775
14848
 
14776
14849
  // src/dashboard/docs-watcher.ts
14777
- import { existsSync as existsSync22, watch } from "fs";
14850
+ import { existsSync as existsSync23, watch } from "fs";
14778
14851
  import { basename as basename6, join as join18 } from "path";
14779
14852
  var DEBOUNCE_MS = 200;
14780
14853
  var MAX_PENDING = 200;
@@ -14816,7 +14889,7 @@ function watchDocs(cwd, listener) {
14816
14889
  const current = entries.get(cwd);
14817
14890
  if (!current || filename === null) return;
14818
14891
  const rel = filename.split("\\").join("/");
14819
- if (rel === root && !existsSync22(join18(cwd, rel))) return;
14892
+ if (rel === root && !existsSync23(join18(cwd, rel))) return;
14820
14893
  if (isIgnoredDocPath(rel)) return;
14821
14894
  if (current.pending.size < MAX_PENDING) current.pending.add(rel);
14822
14895
  if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
@@ -14874,8 +14947,8 @@ var inFlight5 = /* @__PURE__ */ new Map();
14874
14947
  async function readOrCreateDocTranslation(input2) {
14875
14948
  const contentHash = translationHash(input2.title, input2.content);
14876
14949
  const key = `${input2.channelId}:${input2.sourcePath}:${contentHash}:${input2.lang}`;
14877
- const running = inFlight5.get(key);
14878
- if (running) return running;
14950
+ const running2 = inFlight5.get(key);
14951
+ if (running2) return running2;
14879
14952
  const job = (async () => {
14880
14953
  const cached = await getDocTranslation(input2.channelId, input2.sourcePath, input2.lang, contentHash);
14881
14954
  if (cached) return cached;
@@ -15053,7 +15126,7 @@ async function readOrQueueDocAudio(input2) {
15053
15126
  }
15054
15127
 
15055
15128
  // src/dashboard/artifact-export.ts
15056
- import { readFileSync as readFileSync19 } from "fs";
15129
+ import { readFileSync as readFileSync20 } from "fs";
15057
15130
 
15058
15131
  // src/dashboard/chat-agent.ts
15059
15132
  import { homedir as homedir9 } from "os";
@@ -15271,7 +15344,7 @@ async function withMcpConfig(profile, connectionIds, fn) {
15271
15344
  }
15272
15345
 
15273
15346
  // src/claude/agents.ts
15274
- import { existsSync as existsSync23, readdirSync as readdirSync4, readFileSync as readFileSync17 } from "fs";
15347
+ import { existsSync as existsSync24, readdirSync as readdirSync4, readFileSync as readFileSync18 } from "fs";
15275
15348
  import { homedir as homedir8 } from "os";
15276
15349
  import { basename as basename7, join as join20 } from "path";
15277
15350
  import yaml2 from "js-yaml";
@@ -15286,7 +15359,7 @@ function expandHome(p) {
15286
15359
  function parseFrontmatter(file) {
15287
15360
  let content;
15288
15361
  try {
15289
- content = readFileSync17(file, "utf-8");
15362
+ content = readFileSync18(file, "utf-8");
15290
15363
  } catch {
15291
15364
  return {};
15292
15365
  }
@@ -15302,7 +15375,7 @@ function parseFrontmatter(file) {
15302
15375
  }
15303
15376
  }
15304
15377
  function collectFrom(root, source) {
15305
- if (!existsSync23(root)) return [];
15378
+ if (!existsSync24(root)) return [];
15306
15379
  let entries2;
15307
15380
  try {
15308
15381
  entries2 = readdirSync4(root).filter((f) => f.endsWith(".md"));
@@ -15332,7 +15405,7 @@ function readAgentPersona(configDir2, cwd, name) {
15332
15405
  const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
15333
15406
  if (!agent) return void 0;
15334
15407
  try {
15335
- const content = readFileSync17(agent.path, "utf-8");
15408
+ const content = readFileSync18(agent.path, "utf-8");
15336
15409
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
15337
15410
  } catch {
15338
15411
  return void 0;
@@ -15341,7 +15414,7 @@ function readAgentPersona(configDir2, cwd, name) {
15341
15414
 
15342
15415
  // src/dashboard/memory-extraction-runner.ts
15343
15416
  import { spawn as spawn4 } from "child_process";
15344
- import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
15417
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync19, rmSync as rmSync7 } from "fs";
15345
15418
  import { tmpdir as tmpdir4 } from "os";
15346
15419
  import { join as join21 } from "path";
15347
15420
  var CLAUDE_MODEL = "sonnet";
@@ -15433,7 +15506,7 @@ function run3(inv, cwd) {
15433
15506
  let raw = stdout;
15434
15507
  if (inv.outputFile) {
15435
15508
  try {
15436
- raw = readFileSync18(inv.outputFile, "utf-8");
15509
+ raw = readFileSync19(inv.outputFile, "utf-8");
15437
15510
  } catch {
15438
15511
  raw = "";
15439
15512
  }
@@ -15750,23 +15823,43 @@ function extractAttachmentKeys(body) {
15750
15823
  }
15751
15824
  return keys;
15752
15825
  }
15826
+ async function fetchObject(key) {
15827
+ if (process.env["CLOUDFLARE_R2_ACCESS_KEY_ID"]) {
15828
+ const response2 = await getR2Client().send(
15829
+ new GetObjectCommand5({ Bucket: getR2Bucket(), Key: key })
15830
+ );
15831
+ return {
15832
+ mimetype: response2.ContentType ?? "application/octet-stream",
15833
+ contentLength: response2.ContentLength,
15834
+ read: async () => response2.Body?.transformToByteArray()
15835
+ };
15836
+ }
15837
+ const url = await requestAttachmentUrl(key);
15838
+ if (!url) throw new Error("Data API refused the attachment");
15839
+ const response = await fetch(url);
15840
+ if (!response.ok) throw new Error(`presigned GET answered ${response.status}`);
15841
+ const length = response.headers.get("content-length");
15842
+ return {
15843
+ mimetype: response.headers.get("content-type") ?? "application/octet-stream",
15844
+ contentLength: length === null ? void 0 : Number(length),
15845
+ read: async () => new Uint8Array(await response.arrayBuffer())
15846
+ };
15847
+ }
15753
15848
  async function downloadOne(key) {
15754
15849
  const lastSegment = key.slice(key.lastIndexOf("/") + 1);
15755
15850
  const name = displayName(lastSegment);
15756
15851
  try {
15757
- const response = await getR2Client().send(
15758
- new GetObjectCommand5({ Bucket: getR2Bucket(), Key: key })
15759
- );
15760
- const mimetype = response.ContentType ?? "application/octet-stream";
15852
+ const response = await fetchObject(key);
15853
+ const mimetype = response.mimetype;
15761
15854
  const sizeLimit = mimetypeSizeLimit(mimetype);
15762
15855
  if (sizeLimit === null) {
15763
15856
  if (debug) console.log(`[attachment-downloader] unsupported mimetype: ${mimetype} (${key})`);
15764
15857
  return { kind: "skip", skipped: { name, mimetype, reason: "unsupported" } };
15765
15858
  }
15766
- if (response.ContentLength !== void 0 && response.ContentLength > sizeLimit) {
15859
+ if (response.contentLength !== void 0 && response.contentLength > sizeLimit) {
15767
15860
  return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
15768
15861
  }
15769
- const bytes = await response.Body?.transformToByteArray();
15862
+ const bytes = await response.read();
15770
15863
  if (!bytes) {
15771
15864
  return { kind: "skip", skipped: { name, mimetype, reason: "download_failed" } };
15772
15865
  }
@@ -15798,11 +15891,11 @@ async function downloadChatAttachments(body) {
15798
15891
  }
15799
15892
 
15800
15893
  // src/dashboard/doc-mention-guard.ts
15801
- import { existsSync as existsSync24, statSync as statSync5 } from "fs";
15894
+ import { existsSync as existsSync25, statSync as statSync5 } from "fs";
15802
15895
  var DOC_MENTION_RE = /(?<![\w@/`])@((?:[\w.-]+\/)+[\w.-]+\.[A-Za-z0-9]{1,6})(?::(\d+))?(?![\w-])/g;
15803
15896
  function isRealDoc(root, rel) {
15804
15897
  const abs = resolveDocPath(root, rel);
15805
- if (!abs || !existsSync24(abs)) return false;
15898
+ if (!abs || !existsSync25(abs)) return false;
15806
15899
  try {
15807
15900
  return statSync5(abs).isFile();
15808
15901
  } catch {
@@ -17072,7 +17165,7 @@ async function exportDocAsArtifact(channelId, docPath) {
17072
17165
  );
17073
17166
  const abs = resolveDocPath(channelConfig.cwd, docPath);
17074
17167
  if (!abs) throw new Error(`doc not found: ${docPath}`);
17075
- const content = readFileSync19(abs, "utf8");
17168
+ const content = readFileSync20(abs, "utf8");
17076
17169
  const title = extractTitle(abs, "md");
17077
17170
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
17078
17171
  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).
@@ -17353,12 +17446,12 @@ function readGitBranch(dir) {
17353
17446
  try {
17354
17447
  let gitDir = join23(dir, ".git");
17355
17448
  if (statSync6(gitDir).isFile()) {
17356
- const pointer = readFileSync20(gitDir, "utf8").trim();
17449
+ const pointer = readFileSync21(gitDir, "utf8").trim();
17357
17450
  const match = pointer.match(/^gitdir:\s*(.+)$/);
17358
17451
  if (!match) return void 0;
17359
17452
  gitDir = resolve5(dir, match[1]);
17360
17453
  }
17361
- const head = readFileSync20(join23(gitDir, "HEAD"), "utf8").trim();
17454
+ const head = readFileSync21(join23(gitDir, "HEAD"), "utf8").trim();
17362
17455
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
17363
17456
  if (branchMatch) return branchMatch[1];
17364
17457
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -17387,7 +17480,7 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
17387
17480
  if (SKIP_DIRS.has(entry.name)) continue;
17388
17481
  const probe3 = probeDir(abs, showHidden);
17389
17482
  if (!probe3.hasDocs && probe3.hasOther) continue;
17390
- const isRepo = existsSync25(join23(abs, ".git"));
17483
+ const isRepo = existsSync26(join23(abs, ".git"));
17391
17484
  dirs.push({
17392
17485
  path,
17393
17486
  name: entry.name,
@@ -17593,7 +17686,7 @@ function moveDocEntry(root, from, to) {
17593
17686
  return { status: "bad-target" };
17594
17687
  }
17595
17688
  }
17596
- const absToDir = dirname7(absTo);
17689
+ const absToDir = dirname8(absTo);
17597
17690
  if (resolveDirPath(root, relative(root, absToDir)) === null) return { status: "bad-target" };
17598
17691
  try {
17599
17692
  if (!statSync6(absToDir).isDirectory()) return { status: "bad-target" };
@@ -17684,7 +17777,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
17684
17777
  }
17685
17778
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
17686
17779
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
17687
- return c.body(withDocLinkBridge(readFileSync20(abs, "utf8")), 200, {
17780
+ return c.body(withDocLinkBridge(readFileSync21(abs, "utf8")), 200, {
17688
17781
  "Content-Type": "text/html; charset=utf-8",
17689
17782
  "Content-Security-Policy": HTML_SHARE_CSP,
17690
17783
  "X-Content-Type-Options": "nosniff",
@@ -17869,7 +17962,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
17869
17962
  title: kind ? extractTitle(abs, kind) : basename8(abs),
17870
17963
  mtime: stat.mtime.toISOString(),
17871
17964
  tooLarge: tooLarge || void 0,
17872
- content: tooLarge ? "" : readFileSync20(abs, "utf8")
17965
+ content: tooLarge ? "" : readFileSync21(abs, "utf8")
17873
17966
  });
17874
17967
  });
17875
17968
  app.put("/:id/docs/content", async (c) => {
@@ -17912,7 +18005,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
17912
18005
  channel,
17913
18006
  rel,
17914
18007
  title: extractTitle(abs, "md"),
17915
- content: readFileSync20(abs, "utf8")
18008
+ content: readFileSync21(abs, "utf8")
17916
18009
  };
17917
18010
  };
17918
18011
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -17967,8 +18060,8 @@ function withDocLinkBridge(html) {
17967
18060
  }
17968
18061
 
17969
18062
  // src/dashboard/skills-routes-host.ts
17970
- import { existsSync as existsSync26, statSync as statSync7 } from "fs";
17971
- import { dirname as dirname8 } from "path";
18063
+ import { existsSync as existsSync27, statSync as statSync7 } from "fs";
18064
+ import { dirname as dirname9 } from "path";
17972
18065
  import { Hono as Hono32 } from "hono";
17973
18066
  function workspaceScope() {
17974
18067
  const claudeConfigDir = profileClaudeConfigDir();
@@ -17976,7 +18069,7 @@ function workspaceScope() {
17976
18069
  return {
17977
18070
  dir: workspaceSkillsDir(claudeConfigDir),
17978
18071
  // `~/.claude` itself, which exists wherever the CLI has ever run.
17979
- root: dirname8(workspaceSkillsDir(claudeConfigDir)),
18072
+ root: dirname9(workspaceSkillsDir(claudeConfigDir)),
17980
18073
  cwd,
17981
18074
  claudeConfigDir,
17982
18075
  // Only plugin skills. Project skills belong to a specific channel's cwd, so
@@ -18002,7 +18095,7 @@ function inheritedSkills(scope, own) {
18002
18095
  }
18003
18096
  function rootExists(dir) {
18004
18097
  try {
18005
- return existsSync26(dir) && statSync7(dir).isDirectory();
18098
+ return existsSync27(dir) && statSync7(dir).isDirectory();
18006
18099
  } catch {
18007
18100
  return false;
18008
18101
  }
@@ -18112,7 +18205,7 @@ async function resolveAgentIdentities() {
18112
18205
 
18113
18206
  // ../api/src/dashboard/providers.ts
18114
18207
  import { execFileSync as execFileSync4 } from "child_process";
18115
- import { existsSync as existsSync27, readFileSync as readFileSync21 } from "fs";
18208
+ import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
18116
18209
  import { homedir as homedir10 } from "os";
18117
18210
  import { join as join24, resolve as resolve6 } from "path";
18118
18211
  var PROVIDER_LABELS = {
@@ -18135,7 +18228,7 @@ function binaryOnPath(bin) {
18135
18228
  function claudeAuthAvailable() {
18136
18229
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
18137
18230
  const configDir2 = profileClaudeConfigDir() || resolve6(homedir10(), ".claude");
18138
- if (existsSync27(join24(configDir2, ".credentials.json"))) return true;
18231
+ if (existsSync28(join24(configDir2, ".credentials.json"))) return true;
18139
18232
  if (process.platform !== "darwin") return false;
18140
18233
  try {
18141
18234
  execFileSync4("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
@@ -18181,7 +18274,7 @@ function claudeAccount() {
18181
18274
  function codexAccount() {
18182
18275
  const home2 = process.env["CODEX_HOME"]?.trim() || join24(homedir10(), ".codex");
18183
18276
  try {
18184
- const auth = JSON.parse(readFileSync21(join24(home2, "auth.json"), "utf8"));
18277
+ const auth = JSON.parse(readFileSync22(join24(home2, "auth.json"), "utf8"));
18185
18278
  const payload = auth.tokens?.id_token?.split(".")[1];
18186
18279
  if (!payload) return void 0;
18187
18280
  const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
@@ -18212,9 +18305,9 @@ function providerStatuses() {
18212
18305
  // src/dashboard/provider-login-host.ts
18213
18306
  import { spawn as spawn6 } from "child_process";
18214
18307
  import { randomUUID as randomUUID5 } from "crypto";
18215
- import { chmodSync, existsSync as existsSync28, mkdirSync as mkdirSync12, readFileSync as readFileSync22, writeFileSync as writeFileSync12 } from "fs";
18308
+ import { chmodSync, existsSync as existsSync29, mkdirSync as mkdirSync12, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
18216
18309
  import { createRequire } from "module";
18217
- import { dirname as dirname9, join as join25 } from "path";
18310
+ import { dirname as dirname10, join as join25 } from "path";
18218
18311
  import { Hono as Hono33 } from "hono";
18219
18312
  var SESSION_TTL_MS = 10 * 6e4;
18220
18313
  var URL_WAIT_MS = 3e4;
@@ -18277,10 +18370,10 @@ function waitFor(s, parse, ms) {
18277
18370
  }
18278
18371
  function loadPty() {
18279
18372
  const require2 = createRequire(import.meta.url);
18280
- const root = dirname9(require2.resolve("node-pty/package.json"));
18373
+ const root = dirname10(require2.resolve("node-pty/package.json"));
18281
18374
  const helper = join25(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
18282
18375
  try {
18283
- if (existsSync28(helper)) chmodSync(helper, 493);
18376
+ if (existsSync29(helper)) chmodSync(helper, 493);
18284
18377
  } catch {
18285
18378
  }
18286
18379
  return require2("node-pty");
@@ -18320,8 +18413,8 @@ async function startClaude() {
18320
18413
  return { id, url };
18321
18414
  }
18322
18415
  function saveClaudeToken(configFile, token) {
18323
- mkdirSync12(dirname9(configFile), { recursive: true });
18324
- const current = existsSync28(configFile) ? readFileSync22(configFile, "utf-8") : "";
18416
+ mkdirSync12(dirname10(configFile), { recursive: true });
18417
+ const current = existsSync29(configFile) ? readFileSync23(configFile, "utf-8") : "";
18325
18418
  writeFileSync12(configFile, upsertEnvLine(current, "CLAUDE_CODE_OAUTH_TOKEN", token), { mode: 384 });
18326
18419
  const cached = cachedProfileConfig();
18327
18420
  if (cached) cached.env["CLAUDE_CODE_OAUTH_TOKEN"] = token;
@@ -18540,7 +18633,7 @@ function registerHostRoutes(api) {
18540
18633
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
18541
18634
  const filename = errors ? "custie-error.log" : "custie.log";
18542
18635
  const file = join26(paths.LOG_DIR, filename);
18543
- const exists = existsSync29(file);
18636
+ const exists = existsSync30(file);
18544
18637
  return c.json({
18545
18638
  file,
18546
18639
  exists,
@@ -18550,8 +18643,8 @@ function registerHostRoutes(api) {
18550
18643
  });
18551
18644
  }
18552
18645
  function tailFile(file, limit) {
18553
- if (!existsSync29(file)) return [];
18554
- const lines = readFileSync23(file, "utf-8").split("\n");
18646
+ if (!existsSync30(file)) return [];
18647
+ const lines = readFileSync24(file, "utf-8").split("\n");
18555
18648
  if (lines.at(-1) === "") lines.pop();
18556
18649
  return lines.slice(-limit);
18557
18650
  }
@@ -18569,7 +18662,7 @@ function createHostApiRouter(agent = {}) {
18569
18662
 
18570
18663
  // src/dashboard/agent-bridge-host.ts
18571
18664
  import { sign as signRaw, timingSafeEqual as timingSafeEqual2 } from "crypto";
18572
- import { existsSync as existsSync30, readFileSync as readFileSync25 } from "fs";
18665
+ import { existsSync as existsSync31, readFileSync as readFileSync26 } from "fs";
18573
18666
  import dotenv2 from "dotenv";
18574
18667
  import { WebSocket as WebSocket2 } from "ws";
18575
18668
 
@@ -18603,7 +18696,7 @@ async function handleHostRequest(req) {
18603
18696
  }
18604
18697
 
18605
18698
  // src/dashboard/doc-share-publisher.ts
18606
- import { readFileSync as readFileSync24, statSync as statSync9 } from "fs";
18699
+ import { readFileSync as readFileSync25, statSync as statSync9 } from "fs";
18607
18700
  import { extname as extname2, posix } from "path";
18608
18701
  var MAX_ASSET_BYTES = 20 * 1024 * 1024;
18609
18702
  var MAX_CONTENT_BYTES2 = 2 * 1024 * 1024;
@@ -18718,7 +18811,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
18718
18811
  }
18719
18812
  if (!stat.isFile()) return { status: "not-found" };
18720
18813
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
18721
- const source = readFileSync24(abs, "utf8");
18814
+ const source = readFileSync25(abs, "utf8");
18722
18815
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
18723
18816
  if (kind === "excalidraw") {
18724
18817
  try {
@@ -18789,7 +18882,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
18789
18882
  let text = null;
18790
18883
  try {
18791
18884
  const size = statSync9(docAbs).size;
18792
- if (size <= budget) text = readFileSync24(docAbs, "utf8");
18885
+ if (size <= budget) text = readFileSync25(docAbs, "utf8");
18793
18886
  } catch {
18794
18887
  continue;
18795
18888
  }
@@ -18806,7 +18899,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
18806
18899
  let body;
18807
18900
  try {
18808
18901
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
18809
- body = readFileSync24(resolved2.abs);
18902
+ body = readFileSync25(resolved2.abs);
18810
18903
  } catch {
18811
18904
  continue;
18812
18905
  }
@@ -18832,6 +18925,26 @@ function readDocShareBundle({ cwd, sourcePath }) {
18832
18925
  return { status: "ok", kind, title, content: content + renderAttachments(attachments), assets };
18833
18926
  }
18834
18927
 
18928
+ // src/dashboard/host-upgrade.ts
18929
+ import { spawn as spawn7 } from "child_process";
18930
+ import { mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
18931
+ import { join as join27 } from "path";
18932
+ var running = false;
18933
+ function startHostUpgrade() {
18934
+ if (running) return { started: false };
18935
+ mkdirSync13(paths.LOG_DIR, { recursive: true });
18936
+ const log5 = openSync3(join27(paths.LOG_DIR, "host-upgrade.log"), "a");
18937
+ const child = spawn7(process.execPath, [process.argv[1] ?? "", "upgrade"], {
18938
+ detached: true,
18939
+ stdio: ["ignore", log5, log5]
18940
+ });
18941
+ running = true;
18942
+ child.on("exit", () => running = false);
18943
+ child.on("error", () => running = false);
18944
+ child.unref();
18945
+ return { started: true };
18946
+ }
18947
+
18835
18948
  // src/dashboard/agent-bridge-host.ts
18836
18949
  var HOSTED_DATA_SOCKET_ORIGIN = "wss://flyspace-api.fly.dev";
18837
18950
  function dataSocketUrl() {
@@ -18841,7 +18954,7 @@ function dataSocketUrl() {
18841
18954
  return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
18842
18955
  }
18843
18956
  function listLocalSessions() {
18844
- if (!existsSync30(paths.DB_FILE)) return [];
18957
+ if (!existsSync31(paths.DB_FILE)) return [];
18845
18958
  const store = new SessionStore(paths.DB_FILE);
18846
18959
  try {
18847
18960
  return store.listSessions();
@@ -18872,6 +18985,7 @@ function localAgentCallbacks(chatAgent) {
18872
18985
  docsRequest: (req) => handleDocsRequest(req),
18873
18986
  hostRequest: (req) => handleHostRequest(req),
18874
18987
  listSessions: () => listLocalSessions(),
18988
+ upgradeHost: () => startHostUpgrade(),
18875
18989
  scanOnboarding: (slug) => scanOnboarding(slug),
18876
18990
  linkProjectFolders: (slug, items) => linkProjectFolders(slug, items),
18877
18991
  isHostOnline: () => true
@@ -18882,7 +18996,7 @@ function buildHostConfigSnapshot() {
18882
18996
  const snapshot2 = {};
18883
18997
  for (const profile of listProfiles()) {
18884
18998
  snapshot2[profile] = runWithProfile(profile, () => ({
18885
- env: existsSync30(paths.CONFIG_FILE) ? dotenv2.parse(readFileSync25(paths.CONFIG_FILE, "utf-8")) : {},
18999
+ env: existsSync31(paths.CONFIG_FILE) ? dotenv2.parse(readFileSync26(paths.CONFIG_FILE, "utf-8")) : {},
18886
19000
  agentModel: loadDashboardSettings().agent
18887
19001
  }));
18888
19002
  }
@@ -18962,6 +19076,10 @@ function connectHostSocket(url, callbacks, onAuthenticated, onWorkspaces) {
18962
19076
  handleHostDbResult(msg.id, msg.res);
18963
19077
  return;
18964
19078
  }
19079
+ if (msg.type === "attachment-url-result") {
19080
+ handleAttachmentUrlResult(msg.id, msg.url);
19081
+ return;
19082
+ }
18965
19083
  if (msg.type === "call") {
18966
19084
  void handleCall(msg, callbacks, ws);
18967
19085
  }
@@ -19128,8 +19246,8 @@ function startPushWatcher() {
19128
19246
  }
19129
19247
 
19130
19248
  // src/dashboard/machine-server.ts
19131
- import { existsSync as existsSync31 } from "fs";
19132
- import { join as join27 } from "path";
19249
+ import { existsSync as existsSync32 } from "fs";
19250
+ import { join as join28 } from "path";
19133
19251
  import { serveStatic } from "@hono/node-server/serve-static";
19134
19252
 
19135
19253
  // src/dashboard/app-icon-routes.ts
@@ -19211,7 +19329,7 @@ function pngResponse(c, png) {
19211
19329
  }
19212
19330
 
19213
19331
  // src/dashboard/machine-server.ts
19214
- var WEB_DIST = join27(paths.WEB_ROOT, "dist");
19332
+ var WEB_DIST = join28(paths.WEB_ROOT, "dist");
19215
19333
  async function bootMachine() {
19216
19334
  await reconcileAll().catch(
19217
19335
  (err) => console.error("[dashboard] reconcileAll failed:", err.message)
@@ -19231,14 +19349,14 @@ async function bootMachine() {
19231
19349
  function mountSpa(app) {
19232
19350
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
19233
19351
  app.route("/", createAppIconRouter());
19234
- if (existsSync31(WEB_DIST) || existsSync31(join27(paths.WEB_ROOT, "package.json"))) {
19352
+ if (existsSync32(WEB_DIST) || existsSync32(join28(paths.WEB_ROOT, "package.json"))) {
19235
19353
  app.use("/*", serveStatic({ root: WEB_DIST }));
19236
19354
  }
19237
19355
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
19238
- const spaFallback = serveStatic({ path: join27(WEB_DIST, "index.html") });
19356
+ const spaFallback = serveStatic({ path: join28(WEB_DIST, "index.html") });
19239
19357
  app.get("*", async (c, next) => {
19240
- if (!existsSync31(join27(WEB_DIST, "index.html"))) {
19241
- const isCheckout = existsSync31(join27(paths.WEB_ROOT, "package.json"));
19358
+ if (!existsSync32(join28(WEB_DIST, "index.html"))) {
19359
+ const isCheckout = existsSync32(join28(paths.WEB_ROOT, "package.json"));
19242
19360
  return c.text(
19243
19361
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
19244
19362
  Run \`pnpm build:web\` from the repo root.
@@ -19367,13 +19485,13 @@ function flagValue(args, name) {
19367
19485
  function openBrowser2(url) {
19368
19486
  const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
19369
19487
  try {
19370
- spawn7(cmd, [url], { stdio: "ignore", detached: true }).unref();
19488
+ spawn8(cmd, [url], { stdio: "ignore", detached: true }).unref();
19371
19489
  } catch {
19372
19490
  }
19373
19491
  }
19374
19492
  function ensureWebBuild() {
19375
- if (existsSync32(join28(paths.WEB_ROOT, "dist", "index.html"))) return;
19376
- if (!existsSync32(join28(paths.WEB_ROOT, "package.json"))) return;
19493
+ if (existsSync33(join29(paths.WEB_ROOT, "dist", "index.html"))) return;
19494
+ if (!existsSync33(join29(paths.WEB_ROOT, "package.json"))) return;
19377
19495
  console.log(" packages/ui/dist missing, building the dashboard UI...");
19378
19496
  const result = spawnSync("pnpm", ["build"], {
19379
19497
  cwd: paths.WEB_ROOT,
@@ -19423,7 +19541,7 @@ async function runDashboard(args) {
19423
19541
  }
19424
19542
 
19425
19543
  // src/commands/workspace.ts
19426
- import { existsSync as existsSync33, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
19544
+ import { existsSync as existsSync34, mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
19427
19545
  var USAGE7 = `
19428
19546
  Usage: flyspace workspace <subcommand> [options]
19429
19547
 
@@ -19509,7 +19627,7 @@ function provisionLocal(botName) {
19509
19627
  ensureDirs();
19510
19628
  console.log(` config dir ready (${paths.CONFIG_DIR})`);
19511
19629
  console.log(` data dir ready (${paths.DATA_DIR})`);
19512
- if (existsSync33(paths.CONFIG_FILE)) {
19630
+ if (existsSync34(paths.CONFIG_FILE)) {
19513
19631
  console.log(" config.env exists");
19514
19632
  } else {
19515
19633
  writeFileSync13(paths.CONFIG_FILE, `BOT_NAME=${botName}
@@ -19526,8 +19644,8 @@ async function provisionChannels(specs) {
19526
19644
  continue;
19527
19645
  }
19528
19646
  const cwd = expandPath(spec.cwd);
19529
- if (!existsSync33(cwd)) {
19530
- mkdirSync13(cwd, { recursive: true });
19647
+ if (!existsSync34(cwd)) {
19648
+ mkdirSync14(cwd, { recursive: true });
19531
19649
  console.log(` created cwd ${cwd}`);
19532
19650
  }
19533
19651
  await createChannel({ name: spec.name, cwd, position: index });
@@ -19604,10 +19722,10 @@ async function runReconcileCmd(args) {
19604
19722
  }
19605
19723
 
19606
19724
  // src/cli.ts
19607
- import { join as join29 } from "path";
19725
+ import { join as join30 } from "path";
19608
19726
  var home = flyspaceHome();
19609
19727
  if (home) {
19610
- const bin = join29(home, "bin");
19728
+ const bin = join30(home, "bin");
19611
19729
  const path = process.env.PATH ?? "";
19612
19730
  if (!path.split(":").includes(bin)) process.env.PATH = path ? `${bin}:${path}` : bin;
19613
19731
  }
@@ -19745,7 +19863,7 @@ async function main() {
19745
19863
  break;
19746
19864
  case "-v":
19747
19865
  case "--version":
19748
- printRaw("0.1.29");
19866
+ printRaw("0.1.31");
19749
19867
  break;
19750
19868
  case "-h":
19751
19869
  case "--help":