flyspace-cli 0.1.13 → 0.1.15

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
@@ -13,6 +13,7 @@ import {
13
13
  HandleTakenError,
14
14
  InvalidTransferError,
15
15
  LastAdminError,
16
+ MEMORY_TYPES,
16
17
  MessageQueue,
17
18
  OutputStyleConflictError,
18
19
  SKIP_SUBTYPES,
@@ -39,12 +40,15 @@ import {
39
40
  createOutputStyle,
40
41
  createSkillFile,
41
42
  createWorkspace,
43
+ deleteChannelMemory,
42
44
  deleteMembership,
43
45
  deleteOutputStyle,
44
46
  deleteSkillFile,
45
47
  deleteWorkspace,
48
+ disableFeatureFlag,
46
49
  displayNameFor,
47
50
  docsAccessPromptNote,
51
+ enableFeatureFlag,
48
52
  ensureAgentDmChannel,
49
53
  ensureGeneralChannel,
50
54
  ensureLogicalCwdDir,
@@ -52,12 +56,15 @@ import {
52
56
  ensureUsersCached,
53
57
  expandPath,
54
58
  extractCategories,
59
+ featureFlagIds,
60
+ featureFlags,
55
61
  findChannel,
56
62
  findChannelByAgentName,
57
63
  findChannelById,
58
64
  findSkillDetail,
59
65
  getAgentPaused,
60
66
  getAllowGuestAccess,
67
+ getChannelMemory,
61
68
  getChannelRegistry,
62
69
  getMembershipByEmail,
63
70
  getMembershipByUserId,
@@ -68,17 +75,24 @@ import {
68
75
  getWorkspaceById,
69
76
  getWorkspaceBySlug,
70
77
  getWorkspaceByStripeCustomerId,
78
+ getWorkspaceGreetedAt,
71
79
  getWorkspacePlanStatus,
72
80
  installUsageRecording,
73
81
  isAllowedModel,
74
82
  isCodexModel,
75
83
  isCwdUsable,
76
84
  isEffort,
85
+ isFeatureFlagId,
86
+ isFeatureVisible,
77
87
  isLockedPlan,
78
88
  isLogicalCwd,
89
+ isMemoryType,
90
+ isUuid,
79
91
  isValidSkillName,
80
92
  lastRunsForWorkspace,
81
93
  listActiveUsers,
94
+ listChannelMemories,
95
+ listEnabledFeatureFlags,
82
96
  listMemberChannels,
83
97
  listMemberships,
84
98
  listMembershipsByEmail,
@@ -86,9 +100,11 @@ import {
86
100
  listPluginSkillDetails,
87
101
  listSkillFiles,
88
102
  listSkills,
103
+ listThreadMemoryEvents,
89
104
  listUsersWithAvatars,
90
105
  listWorkspaceSummaries,
91
106
  listWorkspaces,
107
+ loadAgentMemory,
92
108
  loadAutomations,
93
109
  loadAutomationsFile,
94
110
  loadBotIdentity,
@@ -103,6 +119,7 @@ import {
103
119
  lookupById,
104
120
  markInviteResent,
105
121
  markWorkspaceAgentConnected,
122
+ markWorkspaceGreeted,
106
123
  markWorkspaceProjectsSetup,
107
124
  markdownToBlocks,
108
125
  mcpStrictConfigEnabled,
@@ -120,6 +137,7 @@ import {
120
137
  resolveChannelId,
121
138
  resolveChannelPrompt,
122
139
  resolveCwd,
140
+ resolveFeatureFlags,
123
141
  resolveLogicalCwd,
124
142
  resolveSelectedOutputStyle,
125
143
  resolveUserId,
@@ -128,9 +146,11 @@ import {
128
146
  runnerForModel,
129
147
  saveAgentProfile,
130
148
  saveBotIdentity,
149
+ saveChannelMemory,
131
150
  saveDashboardSettings,
132
151
  saveWorkspaceBranding,
133
152
  saveWorkspacePrompt,
153
+ searchChannelMemories,
134
154
  setAgentPaused,
135
155
  setAllowGuestAccess,
136
156
  setMembershipHandle,
@@ -143,17 +163,20 @@ import {
143
163
  syncSlackAvatars,
144
164
  transferWorkspaceAdmin,
145
165
  tsToIso,
166
+ undoMemoryEvent,
146
167
  updateChannelById,
168
+ updateChannelMemory,
147
169
  updateChannelsMcpConnections,
148
170
  updateMembershipRole,
149
171
  updateOutputStyle,
150
172
  updateWorkspacePlanStatus,
151
173
  usageLimitedModels,
152
174
  usageLimitedUntil,
175
+ validateMemoryInput,
153
176
  warnOnBrokenChannelCwds,
154
177
  workspaceSkillsDir,
155
178
  writeSkillFile
156
- } from "./chunk-VKTJBYA3.js";
179
+ } from "./chunk-KFT2YP5W.js";
157
180
  import {
158
181
  firstReplyBody,
159
182
  getThread,
@@ -220,8 +243,8 @@ import { dirname, join } from "path";
220
243
  var REQUIRED_ENV = ["SUPABASE_URL", "SUPABASE_SECRET_KEY"];
221
244
  var RENOTIFY_AFTER_MS = 30 * 60 * 1e3;
222
245
  var ts = () => (/* @__PURE__ */ new Date()).toISOString();
223
- function findMissingEnv(env = process.env) {
224
- const required = slackConfigured(env) ? [...SLACK_ENV, ...REQUIRED_ENV] : REQUIRED_ENV;
246
+ function findMissingEnv(env = process.env, supabaseViaHost = false) {
247
+ const required = [...slackConfigured(env) ? SLACK_ENV : [], ...supabaseViaHost ? [] : REQUIRED_ENV];
225
248
  return required.filter((name) => !env[name]?.trim());
226
249
  }
227
250
  function shouldNotify(missing, statePath, now) {
@@ -290,7 +313,7 @@ async function preflight(opts = {}) {
290
313
  const statePath = opts.statePath ?? join(paths.DATA_DIR, "preflight-alert.json");
291
314
  const notify = opts.notify ?? notifyOwnerOverSlack;
292
315
  const exit = opts.exit ?? ((code) => process.exit(code));
293
- const missing = findMissingEnv(env);
316
+ const missing = findMissingEnv(env, opts.supabaseViaHost);
294
317
  if (missing.length === 0) {
295
318
  rmSync(statePath, { force: true });
296
319
  return;
@@ -307,15 +330,392 @@ async function preflight(opts = {}) {
307
330
  exit(1);
308
331
  }
309
332
 
333
+ // src/auth/device-auth.ts
334
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
335
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
336
+ import { dirname as dirname2, join as join2 } from "path";
337
+ var DEFAULT_API_URL = "https://app.flyspace.ai";
338
+ var EXPIRY_SKEW_MS = 6e4;
339
+ var AuthApiError = class extends Error {
340
+ constructor(message, status) {
341
+ super(message);
342
+ this.status = status;
343
+ this.name = "AuthApiError";
344
+ }
345
+ };
346
+ function apiUrl() {
347
+ return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
348
+ }
349
+ function deviceKeyPath() {
350
+ return join2(paths.BASE_CONFIG_DIR, "device.key");
351
+ }
352
+ function credentialsPath() {
353
+ return join2(paths.BASE_CONFIG_DIR, "credentials.json");
354
+ }
355
+ function ensureDeviceKey(path = deviceKeyPath()) {
356
+ let created = false;
357
+ if (!existsSync(path)) {
358
+ const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
359
+ mkdirSync2(dirname2(path), { recursive: true });
360
+ writeFileSync2(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
361
+ created = true;
362
+ }
363
+ const privateKey = createPrivateKey(readFileSync2(path, "utf8"));
364
+ const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
365
+ "base64"
366
+ );
367
+ return { privateKey, publicKeyBase64, created };
368
+ }
369
+ function loadCredentials(path = credentialsPath()) {
370
+ if (!existsSync(path)) return null;
371
+ try {
372
+ return JSON.parse(readFileSync2(path, "utf8"));
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+ function saveCredentials(creds, path = credentialsPath()) {
378
+ mkdirSync2(dirname2(path), { recursive: true });
379
+ writeFileSync2(path, `${JSON.stringify(creds, null, 2)}
380
+ `, { mode: 384 });
381
+ }
382
+ function clearLocalAuth() {
383
+ rmSync2(credentialsPath(), { force: true });
384
+ rmSync2(deviceKeyPath(), { force: true });
385
+ }
386
+ async function post(base, path, body, token) {
387
+ return fetch(`${base}${path}`, {
388
+ method: "POST",
389
+ headers: {
390
+ "content-type": "application/json",
391
+ ...token ? { authorization: `Bearer ${token}` } : {}
392
+ },
393
+ body: JSON.stringify(body)
394
+ });
395
+ }
396
+ async function readJson(res) {
397
+ try {
398
+ return await res.json();
399
+ } catch {
400
+ return {};
401
+ }
402
+ }
403
+ async function expectOk(res, what) {
404
+ const json = await readJson(res);
405
+ if (!res.ok) {
406
+ const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
407
+ throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
408
+ }
409
+ return json;
410
+ }
411
+ function toSlugs(value) {
412
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
413
+ }
414
+ function toGrant(json) {
415
+ const user = json["user"] ?? {};
416
+ return {
417
+ accessToken: String(json["access_token"]),
418
+ expiresIn: Number(json["expires_in"]),
419
+ deviceId: String(json["device_id"]),
420
+ userEmail: String(user["email"] ?? ""),
421
+ workspaces: toSlugs(json["workspaces"])
422
+ };
423
+ }
424
+ async function requestDeviceCode(base, input2) {
425
+ const res = await post(base, "/api/auth/device/code", {
426
+ public_key: input2.publicKey,
427
+ name: input2.name,
428
+ platform: input2.platform
429
+ });
430
+ const json = await expectOk(res, "Requesting a device code");
431
+ return {
432
+ deviceCode: String(json["device_code"]),
433
+ userCode: String(json["user_code"]),
434
+ verificationUri: String(json["verification_uri"]),
435
+ expiresIn: Number(json["expires_in"]),
436
+ interval: Number(json["interval"] ?? 5)
437
+ };
438
+ }
439
+ async function pollDeviceToken(base, deviceCode) {
440
+ const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
441
+ const json = await readJson(res);
442
+ if (res.ok) return { status: "approved", grant: toGrant(json) };
443
+ switch (json["error"]) {
444
+ case "authorization_pending":
445
+ return { status: "pending" };
446
+ case "slow_down":
447
+ return { status: "slow_down" };
448
+ case "access_denied":
449
+ return { status: "denied" };
450
+ case "expired_token":
451
+ return { status: "expired" };
452
+ default:
453
+ throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
454
+ }
455
+ }
456
+ async function refreshAccessToken(creds, privateKey) {
457
+ const challenge = await expectOk(
458
+ await post(creds.apiUrl, "/api/auth/device/challenge", { device_id: creds.deviceId }),
459
+ "Requesting a refresh challenge"
460
+ );
461
+ const nonce = String(challenge["nonce"]);
462
+ const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
463
+ const json = await expectOk(
464
+ await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
465
+ "Refreshing the access token"
466
+ );
467
+ return {
468
+ ...creds,
469
+ accessToken: String(json["access_token"]),
470
+ accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
471
+ };
472
+ }
473
+ async function fetchWhoAmI(base, token) {
474
+ const res = await fetch(`${base}/api/auth/device/me`, { headers: { authorization: `Bearer ${token}` } });
475
+ const json = await expectOk(res, "Checking the session");
476
+ const user = json["user"] ?? {};
477
+ const device = json["device"] ?? {};
478
+ return {
479
+ userEmail: String(user["email"] ?? ""),
480
+ deviceId: String(device["id"] ?? ""),
481
+ deviceName: String(device["name"] ?? ""),
482
+ workspaces: toSlugs(json["workspaces"])
483
+ };
484
+ }
485
+ async function boundWorkspaces() {
486
+ const creds = loadCredentials();
487
+ if (!creds) return null;
488
+ try {
489
+ const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
490
+ saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
491
+ return { workspaces: me.workspaces, fresh: true };
492
+ } catch {
493
+ return { workspaces: creds.workspaces ?? [], fresh: false };
494
+ }
495
+ }
496
+ async function revokeDevice(base, token) {
497
+ await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
498
+ }
499
+ async function getAccessToken() {
500
+ const fromEnv = process.env["FLYSPACE_TOKEN"];
501
+ if (fromEnv) return fromEnv;
502
+ const creds = loadCredentials();
503
+ if (!creds) return null;
504
+ if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
505
+ const { privateKey } = ensureDeviceKey();
506
+ const refreshed = await refreshAccessToken(creds, privateKey);
507
+ saveCredentials(refreshed);
508
+ return refreshed.accessToken;
509
+ }
510
+
511
+ // src/dashboard/host-db-client.ts
512
+ import { createClient } from "@supabase/supabase-js";
513
+ import { randomUUID } from "crypto";
514
+
515
+ // src/dashboard/host-lock.ts
516
+ import { execFileSync } from "child_process";
517
+ import { randomBytes } from "crypto";
518
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
519
+ import { join as join3 } from "path";
520
+ function hostLockPath() {
521
+ return join3(paths.BASE_DATA_DIR, "host.lock");
522
+ }
523
+ function isLiveFlyspace(pid) {
524
+ try {
525
+ process.kill(pid, 0);
526
+ const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
527
+ encoding: "utf8"
528
+ });
529
+ return command.includes("flyspace") || command.includes("host-server");
530
+ } catch {
531
+ return false;
532
+ }
533
+ }
534
+ function readLockPid(path) {
535
+ try {
536
+ const pid = Number(JSON.parse(readFileSync3(path, "utf8")).pid);
537
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
538
+ } catch {
539
+ return null;
540
+ }
541
+ }
542
+ function acquireHostLock() {
543
+ const path = hostLockPath();
544
+ mkdirSync3(paths.BASE_DATA_DIR, { recursive: true });
545
+ const body = JSON.stringify({
546
+ pid: process.pid,
547
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
548
+ execPath: process.argv[1] ?? ""
549
+ });
550
+ for (let attempt = 0; attempt < 2; attempt++) {
551
+ try {
552
+ writeFileSync3(path, body, { flag: "wx", mode: 384 });
553
+ const release2 = () => {
554
+ if (readLockPid(path) === process.pid) rmSync3(path, { force: true });
555
+ };
556
+ process.once("exit", release2);
557
+ return release2;
558
+ } catch (err) {
559
+ if (err.code !== "EEXIST") throw err;
560
+ const holder = readLockPid(path);
561
+ if (holder === process.pid) return () => {
562
+ };
563
+ if (holder && isLiveFlyspace(holder)) {
564
+ throw new Error(
565
+ `Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
566
+ );
567
+ }
568
+ rmSync3(path, { force: true });
569
+ }
570
+ }
571
+ throw new Error(`Could not take the Host lock at ${path}`);
572
+ }
573
+ function publishHostEndpoint(port) {
574
+ const path = hostLockPath();
575
+ if (readLockPid(path) !== process.pid) return null;
576
+ const token = randomBytes(32).toString("hex");
577
+ const lock = JSON.parse(readFileSync3(path, "utf8"));
578
+ writeFileSync3(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
579
+ return token;
580
+ }
581
+ function readHostEndpoint() {
582
+ try {
583
+ const { pid, port, token } = JSON.parse(readFileSync3(hostLockPath(), "utf8"));
584
+ if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
585
+ return { port, token };
586
+ } catch {
587
+ return null;
588
+ }
589
+ }
590
+
591
+ // src/dashboard/host-db-client.ts
592
+ var REQUEST_TIMEOUT_MS = 3e4;
593
+ var pending = /* @__PURE__ */ new Map();
594
+ var sender = null;
595
+ var onSender = [];
596
+ function setHostDbSender(fn) {
597
+ sender = fn;
598
+ if (!fn) return;
599
+ const waiting = onSender;
600
+ onSender = [];
601
+ for (const wake of waiting) wake();
602
+ }
603
+ function handleHostDbResult(id, res) {
604
+ const p = pending.get(id);
605
+ if (!p) return;
606
+ pending.delete(id);
607
+ clearTimeout(p.timer);
608
+ p.resolve(res);
609
+ }
610
+ function whenConnected() {
611
+ if (sender) return Promise.resolve(sender);
612
+ return new Promise((resolve7, reject) => {
613
+ const timer2 = setTimeout(() => {
614
+ onSender = onSender.filter((w) => w !== wake);
615
+ reject(new Error("Host is not connected to the Data API"));
616
+ }, REQUEST_TIMEOUT_MS);
617
+ const wake = () => {
618
+ clearTimeout(timer2);
619
+ resolve7(sender);
620
+ };
621
+ onSender.push(wake);
622
+ });
623
+ }
624
+ async function requestHostDb(req) {
625
+ const send2 = await whenConnected();
626
+ const id = randomUUID();
627
+ return new Promise((resolve7, reject) => {
628
+ const timer2 = setTimeout(() => {
629
+ pending.delete(id);
630
+ reject(new Error("Data API did not answer in time"));
631
+ }, REQUEST_TIMEOUT_MS);
632
+ pending.set(id, { resolve: resolve7, reject, timer: timer2 });
633
+ send2({ type: "db", id, req });
634
+ });
635
+ }
636
+ var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
637
+ function dbFetch(send2) {
638
+ return async (input2, init2) => {
639
+ const req = new Request(input2, init2);
640
+ const url = new URL(req.url);
641
+ const method = req.method.toUpperCase();
642
+ const res = await send2({
643
+ method,
644
+ path: `${url.pathname}${url.search}`,
645
+ headers: Object.fromEntries(req.headers.entries()),
646
+ body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
647
+ });
648
+ const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
649
+ return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
650
+ };
651
+ }
652
+ var hostDbFetch = dbFetch(requestHostDb);
653
+ var LOCAL_DB_PATH = "/internal/db";
654
+ async function requestViaLocalHost(req) {
655
+ const deadline = Date.now() + REQUEST_TIMEOUT_MS;
656
+ let endpoint = readHostEndpoint();
657
+ while (!endpoint && Date.now() < deadline) {
658
+ await new Promise((r) => setTimeout(r, 1e3));
659
+ endpoint = readHostEndpoint();
660
+ }
661
+ if (!endpoint) throw new Error("No local Host to reach the Data API through");
662
+ const res = await fetch(`http://127.0.0.1:${endpoint.port}${LOCAL_DB_PATH}`, {
663
+ method: "POST",
664
+ headers: { authorization: `Bearer ${endpoint.token}`, "content-type": "application/json" },
665
+ body: JSON.stringify(req),
666
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS + 5e3)
667
+ });
668
+ if (!res.ok) throw new Error(`Local Host DB relay answered ${res.status}`);
669
+ return await res.json();
670
+ }
671
+ function relayChannel(topic) {
672
+ const channel = {
673
+ on: () => channel,
674
+ subscribe: (callback) => {
675
+ queueMicrotask(() => callback?.("SUBSCRIBED"));
676
+ return channel;
677
+ },
678
+ send: async (msg) => {
679
+ if (msg.type !== "broadcast" || !sender) return "error";
680
+ sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
681
+ return "ok";
682
+ },
683
+ unsubscribe: async () => "ok"
684
+ };
685
+ return channel;
686
+ }
687
+ function createHostDbClient(via = "socket") {
688
+ const client2 = createClient("https://host-db.invalid", "host-device", {
689
+ auth: { persistSession: false, autoRefreshToken: false },
690
+ global: { fetch: via === "socket" ? hostDbFetch : dbFetch(requestViaLocalHost) }
691
+ });
692
+ return new Proxy(client2, {
693
+ get(target, prop) {
694
+ if (prop === "channel") return relayChannel;
695
+ if (prop === "removeChannel") return async () => "ok";
696
+ const value = Reflect.get(target, prop, target);
697
+ return typeof value === "function" ? value.bind(target) : value;
698
+ }
699
+ });
700
+ }
701
+
702
+ // src/commands/supabase-via-host.ts
703
+ function useHostDbWithoutCredentials() {
704
+ const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
705
+ if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
706
+ return viaHost;
707
+ }
708
+
310
709
  // src/commands/start.ts
311
710
  async function runStart() {
312
711
  loadEnvFiles();
313
- await preflight();
712
+ const viaHost = useHostDbWithoutCredentials();
713
+ await preflight({ supabaseViaHost: viaHost });
314
714
  await startServer();
315
715
  }
316
716
 
317
717
  // src/commands/config.ts
318
- import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
718
+ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
319
719
  import { spawn } from "child_process";
320
720
  import { resolve } from "path";
321
721
  function maskToken(value) {
@@ -333,12 +733,12 @@ function printConfig() {
333
733
  console.log("\n Paths:");
334
734
  console.log(` Config dir: ${paths.CONFIG_DIR}`);
335
735
  console.log(` Data dir: ${paths.DATA_DIR}`);
336
- console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
337
- console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
338
- console.log(` Database: ${paths.DB_FILE} ${existsSync(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
736
+ console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync2(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
737
+ console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync2(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
738
+ console.log(` Database: ${paths.DB_FILE} ${existsSync2(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
339
739
  console.log(` Log dir: ${paths.LOG_DIR}`);
340
740
  const repoEnv = resolve(process.cwd(), ".env");
341
- console.log(` Repo .env: ${repoEnv} ${existsSync(repoEnv) ? "(exists)" : "(not found)"}`);
741
+ console.log(` Repo .env: ${repoEnv} ${existsSync2(repoEnv) ? "(exists)" : "(not found)"}`);
342
742
  const envKeys = [
343
743
  "SLACK_BOT_TOKEN",
344
744
  "SLACK_APP_TOKEN",
@@ -362,13 +762,13 @@ function printConfig() {
362
762
  }
363
763
  function editConfig() {
364
764
  ensureDirs();
365
- if (!existsSync(paths.CONFIG_FILE)) {
765
+ if (!existsSync2(paths.CONFIG_FILE)) {
366
766
  const defaultEnv = resolve(paths.PACKAGE_ROOT, ".env.example");
367
- if (existsSync(defaultEnv)) {
368
- const content = readFileSync2(defaultEnv, "utf-8");
369
- writeFileSync2(paths.CONFIG_FILE, content);
767
+ if (existsSync2(defaultEnv)) {
768
+ const content = readFileSync4(defaultEnv, "utf-8");
769
+ writeFileSync4(paths.CONFIG_FILE, content);
370
770
  } else {
371
- writeFileSync2(paths.CONFIG_FILE, "");
771
+ writeFileSync4(paths.CONFIG_FILE, "");
372
772
  }
373
773
  console.log(`[flyspace] Created ${paths.CONFIG_FILE}`);
374
774
  }
@@ -402,14 +802,14 @@ async function runConfig(args) {
402
802
 
403
803
  // src/commands/setup.ts
404
804
  import { execSync as execSync4 } from "child_process";
405
- import { mkdirSync as mkdirSync5 } from "fs";
406
- import { join as join7 } from "path";
805
+ import { mkdirSync as mkdirSync6 } from "fs";
806
+ import { join as join8 } from "path";
407
807
  import { confirm as confirm2 } from "@inquirer/prompts";
408
808
 
409
809
  // src/service.ts
410
810
  import { execSync } from "child_process";
411
- import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
412
- import { basename, dirname as dirname2, join as join2 } from "path";
811
+ import { existsSync as existsSync3, readFileSync as readFileSync5, realpathSync } from "fs";
812
+ import { basename, dirname as dirname3, join as join4 } from "path";
413
813
  import { homedir, platform } from "os";
414
814
  function flyspaceHome(entry = process.argv[1]) {
415
815
  let cliPath;
@@ -422,8 +822,8 @@ function flyspaceHome(entry = process.argv[1]) {
422
822
  if (at <= 0) return void 0;
423
823
  const npmPrefix = cliPath.slice(0, at);
424
824
  if (basename(npmPrefix) !== "cli") return void 0;
425
- const home = dirname2(npmPrefix);
426
- return existsSync2(join2(home, "bin", "flyspace")) && existsSync2(join2(home, "node", "bin", "node")) ? home : void 0;
825
+ const home = dirname3(npmPrefix);
826
+ return existsSync3(join4(home, "bin", "flyspace")) && existsSync3(join4(home, "node", "bin", "node")) ? home : void 0;
427
827
  }
428
828
  function serviceLabel(profile) {
429
829
  return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
@@ -432,40 +832,40 @@ function plistName(profile) {
432
832
  return `${serviceLabel(profile)}.plist`;
433
833
  }
434
834
  function plistPath(profile) {
435
- return join2(homedir(), "Library", "LaunchAgents", plistName(profile));
835
+ return join4(homedir(), "Library", "LaunchAgents", plistName(profile));
436
836
  }
437
837
  function legacyServiceLabel(profile) {
438
838
  return profile === DEFAULT_PROFILE ? "io.flycoder.flyspace" : `io.flycoder.flyspace.${profile}`;
439
839
  }
440
840
  function legacyPlistPath(profile) {
441
- return join2(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
841
+ return join4(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
442
842
  }
443
843
  function systemdUnitName(profile) {
444
844
  return profile === DEFAULT_PROFILE ? "flyspace" : `flyspace-${profile}`;
445
845
  }
446
846
  function systemdUnitPath(profile) {
447
- return join2(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
847
+ return join4(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
448
848
  }
449
849
  function isServiceInstalled(profile) {
450
- if (platform() === "darwin") return existsSync2(plistPath(profile));
451
- if (platform() === "linux") return existsSync2(systemdUnitPath(profile));
850
+ if (platform() === "darwin") return existsSync3(plistPath(profile));
851
+ if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
452
852
  return false;
453
853
  }
454
854
  function serviceBinaryMissing(file) {
455
855
  let text;
456
856
  try {
457
- text = readFileSync3(file, "utf-8");
857
+ text = readFileSync5(file, "utf-8");
458
858
  } catch {
459
859
  return false;
460
860
  }
461
861
  const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
462
- return !!bin && !existsSync2(bin);
862
+ return !!bin && !existsSync3(bin);
463
863
  }
464
864
  function profileServiceFile(profile) {
465
865
  return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
466
866
  }
467
867
  function hostServiceFile() {
468
- return platform() === "darwin" ? hostPlistPath() : join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
868
+ return platform() === "darwin" ? hostPlistPath() : join4(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
469
869
  }
470
870
  function restartService(profile) {
471
871
  const os = platform();
@@ -495,10 +895,10 @@ function restartService(profile) {
495
895
  var HOST_SERVICE_LABEL = "ai.flyspace.dashboard";
496
896
  var HOST_SYSTEMD_UNIT = "flyspace-host";
497
897
  function hostPlistPath() {
498
- return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
898
+ return join4(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
499
899
  }
500
900
  function isHostServiceInstalled() {
501
- if (platform() === "darwin" || platform() === "linux") return existsSync2(hostServiceFile());
901
+ if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
502
902
  return false;
503
903
  }
504
904
  function restartHostService() {
@@ -538,9 +938,9 @@ function isHostServiceRunning() {
538
938
  }
539
939
 
540
940
  // src/commands/install.ts
541
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync } from "fs";
941
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "fs";
542
942
  import { execSync as execSync2 } from "child_process";
543
- import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
943
+ import { dirname as dirname4, join as join5, resolve as resolve2 } from "path";
544
944
  import { homedir as homedir2, platform as platform2 } from "os";
545
945
  function log(msg) {
546
946
  console.log(`
@@ -561,23 +961,23 @@ function run(cmd, opts = {}) {
561
961
  function detectClaudeBinDir() {
562
962
  try {
563
963
  const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
564
- return claudePath ? dirname3(claudePath) : void 0;
964
+ return claudePath ? dirname4(claudePath) : void 0;
565
965
  } catch {
566
966
  return void 0;
567
967
  }
568
968
  }
569
969
  function detectNodeBinDir() {
570
970
  const home = flyspaceHome();
571
- return home ? join3(home, "node", "bin") : dirname3(process.execPath);
971
+ return home ? join5(home, "node", "bin") : dirname4(process.execPath);
572
972
  }
573
973
  function nodeShimDirs() {
574
- return [join3(homedir2(), ".local", "share", "mise", "shims"), join3(homedir2(), ".asdf", "shims")].filter(
575
- (d) => existsSync3(d)
974
+ return [join5(homedir2(), ".local", "share", "mise", "shims"), join5(homedir2(), ".asdf", "shims")].filter(
975
+ (d) => existsSync4(d)
576
976
  );
577
977
  }
578
978
  function detectCustieBin() {
579
979
  const home = flyspaceHome();
580
- if (home) return join3(home, "bin", "flyspace");
980
+ if (home) return join5(home, "bin", "flyspace");
581
981
  const argv1 = process.argv[1];
582
982
  if (argv1) {
583
983
  return resolve2(argv1);
@@ -589,9 +989,9 @@ function detectCustieBin() {
589
989
  }
590
990
  }
591
991
  function readEnvFile() {
592
- if (!existsSync3(paths.CONFIG_FILE)) return {};
992
+ if (!existsSync4(paths.CONFIG_FILE)) return {};
593
993
  const vars = {};
594
- for (const line of readFileSync4(paths.CONFIG_FILE, "utf-8").split("\n")) {
994
+ for (const line of readFileSync6(paths.CONFIG_FILE, "utf-8").split("\n")) {
595
995
  const trimmed = line.trim();
596
996
  if (!trimmed || trimmed.startsWith("#")) continue;
597
997
  const eq = trimmed.indexOf("=");
@@ -635,9 +1035,9 @@ async function installMacOS(spec) {
635
1035
  const custieBin = detectCustieBin();
636
1036
  const claudeBinDir = detectClaudeBinDir();
637
1037
  const logDir = paths.LOG_DIR;
638
- mkdirSync2(logDir, { recursive: true });
1038
+ mkdirSync4(logDir, { recursive: true });
639
1039
  const envVars = spec.env;
640
- const homebrewBin = existsSync3("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
1040
+ const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
641
1041
  const pathParts = [
642
1042
  detectNodeBinDir(),
643
1043
  claudeBinDir,
@@ -676,14 +1076,14 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
676
1076
  <true/>
677
1077
 
678
1078
  <key>StandardOutPath</key>
679
- <string>${join3(logDir, `${spec.logName}.log`)}</string>
1079
+ <string>${join5(logDir, `${spec.logName}.log`)}</string>
680
1080
 
681
1081
  <key>StandardErrorPath</key>
682
- <string>${join3(logDir, `${spec.logName}-error.log`)}</string>
1082
+ <string>${join5(logDir, `${spec.logName}-error.log`)}</string>
683
1083
  </dict>
684
1084
  </plist>`;
685
1085
  const agentPlistPath = spec.plistPath;
686
- mkdirSync2(dirname3(agentPlistPath), { recursive: true });
1086
+ mkdirSync4(dirname4(agentPlistPath), { recursive: true });
687
1087
  try {
688
1088
  run(`launchctl unload "${agentPlistPath}"`, { silent: true });
689
1089
  } catch {
@@ -692,7 +1092,7 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
692
1092
  unlinkSync(agentPlistPath);
693
1093
  } catch {
694
1094
  }
695
- writeFileSync3(agentPlistPath, plist);
1095
+ writeFileSync5(agentPlistPath, plist);
696
1096
  run(`launchctl load "${agentPlistPath}"`, { silent: true });
697
1097
  success(`LaunchAgent installed and loaded (${spec.label}).`);
698
1098
  console.log(` Logs: ${logDir}`);
@@ -703,9 +1103,9 @@ async function installLinux(spec) {
703
1103
  const custieBin = detectCustieBin();
704
1104
  const claudeBinDir = detectClaudeBinDir();
705
1105
  const logDir = paths.LOG_DIR;
706
- mkdirSync2(logDir, { recursive: true });
1106
+ mkdirSync4(logDir, { recursive: true });
707
1107
  const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
708
- const linuxbrewBin = existsSync3("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
1108
+ const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
709
1109
  const pathParts = [
710
1110
  detectNodeBinDir(),
711
1111
  claudeBinDir,
@@ -729,16 +1129,16 @@ RestartSec=5
729
1129
  ${envLines}
730
1130
  Environment="PATH=${pathParts.join(":")}"
731
1131
 
732
- StandardOutput=append:${join3(logDir, `${spec.logName}.log`)}
733
- StandardError=append:${join3(logDir, `${spec.logName}-error.log`)}
1132
+ StandardOutput=append:${join5(logDir, `${spec.logName}.log`)}
1133
+ StandardError=append:${join5(logDir, `${spec.logName}-error.log`)}
734
1134
 
735
1135
  [Install]
736
1136
  WantedBy=default.target
737
1137
  `;
738
1138
  const unitName = spec.unitName;
739
- const unitDir = join3(homedir2(), ".config", "systemd", "user");
740
- mkdirSync2(unitDir, { recursive: true });
741
- writeFileSync3(join3(unitDir, `${unitName}.service`), unit);
1139
+ const unitDir = join5(homedir2(), ".config", "systemd", "user");
1140
+ mkdirSync4(unitDir, { recursive: true });
1141
+ writeFileSync5(join5(unitDir, `${unitName}.service`), unit);
742
1142
  run("systemctl --user daemon-reload", { silent: true });
743
1143
  run(`systemctl --user enable --now ${unitName}`, { silent: true });
744
1144
  success(`systemd user service installed and started (${unitName}).`);
@@ -765,16 +1165,16 @@ async function installHostService() {
765
1165
  }
766
1166
 
767
1167
  // src/commands/profiles.ts
768
- import { existsSync as existsSync4, readdirSync, statSync } from "fs";
1168
+ import { existsSync as existsSync5, readdirSync, statSync } from "fs";
769
1169
  import { execSync as execSync3 } from "child_process";
770
- import { join as join4 } from "path";
1170
+ import { join as join6 } from "path";
771
1171
  import { platform as platform3 } from "os";
772
1172
  function listProfiles() {
773
1173
  const profiles = [];
774
- if (existsSync4(paths.PROFILES_DIR)) {
1174
+ if (existsSync5(paths.PROFILES_DIR)) {
775
1175
  for (const entry of readdirSync(paths.PROFILES_DIR)) {
776
1176
  try {
777
- if (statSync(join4(paths.PROFILES_DIR, entry)).isDirectory()) {
1177
+ if (statSync(join6(paths.PROFILES_DIR, entry)).isDirectory()) {
778
1178
  profiles.push(entry);
779
1179
  }
780
1180
  } catch {
@@ -798,227 +1198,50 @@ function isServiceRunning2(profile) {
798
1198
  return false;
799
1199
  }
800
1200
 
801
- // src/auth/device-auth.ts
802
- import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
803
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
804
- import { dirname as dirname4, join as join5 } from "path";
805
- var DEFAULT_API_URL = "https://app.flyspace.ai";
806
- var EXPIRY_SKEW_MS = 6e4;
807
- var AuthApiError = class extends Error {
808
- constructor(message, status) {
809
- super(message);
810
- this.status = status;
811
- this.name = "AuthApiError";
812
- }
813
- };
814
- function apiUrl() {
815
- return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
816
- }
817
- function deviceKeyPath() {
818
- return join5(paths.BASE_CONFIG_DIR, "device.key");
819
- }
820
- function credentialsPath() {
821
- return join5(paths.BASE_CONFIG_DIR, "credentials.json");
822
- }
823
- function ensureDeviceKey(path = deviceKeyPath()) {
824
- let created = false;
825
- if (!existsSync5(path)) {
826
- const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
827
- mkdirSync3(dirname4(path), { recursive: true });
828
- writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
829
- created = true;
830
- }
831
- const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
832
- const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
833
- "base64"
834
- );
835
- return { privateKey, publicKeyBase64, created };
836
- }
837
- function loadCredentials(path = credentialsPath()) {
838
- if (!existsSync5(path)) return null;
839
- try {
840
- return JSON.parse(readFileSync5(path, "utf8"));
841
- } catch {
842
- return null;
843
- }
844
- }
845
- function saveCredentials(creds, path = credentialsPath()) {
846
- mkdirSync3(dirname4(path), { recursive: true });
847
- writeFileSync4(path, `${JSON.stringify(creds, null, 2)}
848
- `, { mode: 384 });
849
- }
850
- function clearLocalAuth() {
851
- rmSync2(credentialsPath(), { force: true });
852
- rmSync2(deviceKeyPath(), { force: true });
853
- }
854
- async function post(base, path, body, token) {
855
- return fetch(`${base}${path}`, {
856
- method: "POST",
857
- headers: {
858
- "content-type": "application/json",
859
- ...token ? { authorization: `Bearer ${token}` } : {}
860
- },
861
- body: JSON.stringify(body)
862
- });
863
- }
864
- async function readJson(res) {
865
- try {
866
- return await res.json();
867
- } catch {
868
- return {};
869
- }
870
- }
871
- async function expectOk(res, what) {
872
- const json = await readJson(res);
873
- if (!res.ok) {
874
- const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
875
- throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
876
- }
877
- return json;
878
- }
879
- function toSlugs(value) {
880
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
881
- }
882
- function toGrant(json) {
883
- const user = json["user"] ?? {};
884
- return {
885
- accessToken: String(json["access_token"]),
886
- expiresIn: Number(json["expires_in"]),
887
- deviceId: String(json["device_id"]),
888
- userEmail: String(user["email"] ?? ""),
889
- workspaces: toSlugs(json["workspaces"])
890
- };
891
- }
892
- async function requestDeviceCode(base, input2) {
893
- const res = await post(base, "/api/auth/device/code", {
894
- public_key: input2.publicKey,
895
- name: input2.name,
896
- platform: input2.platform
897
- });
898
- const json = await expectOk(res, "Requesting a device code");
899
- return {
900
- deviceCode: String(json["device_code"]),
901
- userCode: String(json["user_code"]),
902
- verificationUri: String(json["verification_uri"]),
903
- expiresIn: Number(json["expires_in"]),
904
- interval: Number(json["interval"] ?? 5)
905
- };
906
- }
907
- async function pollDeviceToken(base, deviceCode) {
908
- const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
909
- const json = await readJson(res);
910
- if (res.ok) return { status: "approved", grant: toGrant(json) };
911
- switch (json["error"]) {
912
- case "authorization_pending":
913
- return { status: "pending" };
914
- case "slow_down":
915
- return { status: "slow_down" };
916
- case "access_denied":
917
- return { status: "denied" };
918
- case "expired_token":
919
- return { status: "expired" };
920
- default:
921
- throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
922
- }
923
- }
924
- async function refreshAccessToken(creds, privateKey) {
925
- const challenge = await expectOk(
926
- await post(creds.apiUrl, "/api/auth/device/challenge", { device_id: creds.deviceId }),
927
- "Requesting a refresh challenge"
928
- );
929
- const nonce = String(challenge["nonce"]);
930
- const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
931
- const json = await expectOk(
932
- await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
933
- "Refreshing the access token"
934
- );
935
- return {
936
- ...creds,
937
- accessToken: String(json["access_token"]),
938
- accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
939
- };
940
- }
941
- async function fetchWhoAmI(base, token) {
942
- const res = await fetch(`${base}/api/auth/device/me`, { headers: { authorization: `Bearer ${token}` } });
943
- const json = await expectOk(res, "Checking the session");
944
- const user = json["user"] ?? {};
945
- const device = json["device"] ?? {};
946
- return {
947
- userEmail: String(user["email"] ?? ""),
948
- deviceId: String(device["id"] ?? ""),
949
- deviceName: String(device["name"] ?? ""),
950
- workspaces: toSlugs(json["workspaces"])
951
- };
952
- }
953
- async function boundWorkspaces() {
954
- const creds = loadCredentials();
955
- if (!creds) return null;
956
- try {
957
- const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
958
- saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
959
- return { workspaces: me.workspaces, fresh: true };
960
- } catch {
961
- return { workspaces: creds.workspaces ?? [], fresh: false };
962
- }
963
- }
964
- async function revokeDevice(base, token) {
965
- await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
966
- }
967
- async function getAccessToken() {
968
- const fromEnv = process.env["FLYSPACE_TOKEN"];
969
- if (fromEnv) return fromEnv;
970
- const creds = loadCredentials();
971
- if (!creds) return null;
972
- if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
973
- const { privateKey } = ensureDeviceKey();
974
- const refreshed = await refreshAccessToken(creds, privateKey);
975
- saveCredentials(refreshed);
976
- return refreshed.accessToken;
977
- }
978
-
979
1201
  // src/dashboard/onboarding-scan.ts
980
- import { execFileSync } from "child_process";
1202
+ import { execFileSync as execFileSync2 } from "child_process";
981
1203
  import {
1204
+ cpSync,
982
1205
  existsSync as existsSync6,
983
1206
  lstatSync,
984
- mkdirSync as mkdirSync4,
1207
+ mkdirSync as mkdirSync5,
985
1208
  readdirSync as readdirSync2,
986
- readFileSync as readFileSync6,
1209
+ readFileSync as readFileSync7,
987
1210
  statSync as statSync2,
988
1211
  symlinkSync
989
1212
  } from "fs";
990
1213
  import { homedir as homedir3 } from "os";
991
- import { basename as basename2, join as join6, resolve as resolve3 } from "path";
1214
+ import { basename as basename2, join as join7, resolve as resolve3 } from "path";
992
1215
  var MAX_JSONL_HEAD_BYTES = 64 * 1024;
993
1216
  var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
994
1217
  function flyspaceRoot() {
995
- return process.env["FLYSPACE_ROOT"] || join6(homedir3(), "Flyspace");
1218
+ return process.env["FLYSPACE_ROOT"] || join7(homedir3(), "Flyspace");
996
1219
  }
997
1220
  function onPath(cmd) {
998
1221
  try {
999
- execFileSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
1222
+ execFileSync2(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
1000
1223
  return true;
1001
1224
  } catch {
1002
1225
  return false;
1003
1226
  }
1004
1227
  }
1005
1228
  function claudeDir() {
1006
- return process.env["CLAUDE_CONFIG_DIR"] || join6(homedir3(), ".claude");
1229
+ return process.env["CLAUDE_CONFIG_DIR"] || join7(homedir3(), ".claude");
1007
1230
  }
1008
1231
  function codexDir() {
1009
- return process.env["CODEX_HOME"] || join6(homedir3(), ".codex");
1232
+ return process.env["CODEX_HOME"] || join7(homedir3(), ".codex");
1010
1233
  }
1011
1234
  function claudeLoggedIn() {
1012
1235
  if (process.env["ANTHROPIC_API_KEY"]) return true;
1013
- if (existsSync6(join6(claudeDir(), ".credentials.json"))) return true;
1236
+ if (existsSync6(join7(claudeDir(), ".credentials.json"))) return true;
1014
1237
  try {
1015
- const raw = JSON.parse(readFileSync6(join6(homedir3(), ".claude.json"), "utf-8"));
1238
+ const raw = JSON.parse(readFileSync7(join7(homedir3(), ".claude.json"), "utf-8"));
1016
1239
  if (raw.oauthAccount) return true;
1017
1240
  } catch {
1018
1241
  }
1019
1242
  if (process.platform !== "darwin") return false;
1020
1243
  try {
1021
- execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
1244
+ execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
1022
1245
  stdio: "ignore"
1023
1246
  });
1024
1247
  return true;
@@ -1027,7 +1250,7 @@ function claudeLoggedIn() {
1027
1250
  }
1028
1251
  }
1029
1252
  function codexLoggedIn() {
1030
- return !!process.env["OPENAI_API_KEY"] || existsSync6(join6(codexDir(), "auth.json"));
1253
+ return !!process.env["OPENAI_API_KEY"] || existsSync6(join7(codexDir(), "auth.json"));
1031
1254
  }
1032
1255
  function jsonlFiles(dir, depth) {
1033
1256
  const out = [];
@@ -1038,7 +1261,7 @@ function jsonlFiles(dir, depth) {
1038
1261
  return out;
1039
1262
  }
1040
1263
  for (const e of entries2) {
1041
- const full = join6(dir, e.name);
1264
+ const full = join7(dir, e.name);
1042
1265
  if (e.isDirectory()) {
1043
1266
  if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
1044
1267
  } else if (e.name.endsWith(".jsonl")) {
@@ -1053,7 +1276,7 @@ function jsonlFiles(dir, depth) {
1053
1276
  function cwdFromTranscript(file) {
1054
1277
  let head;
1055
1278
  try {
1056
- head = readFileSync6(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1279
+ head = readFileSync7(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1057
1280
  } catch {
1058
1281
  return null;
1059
1282
  }
@@ -1070,7 +1293,7 @@ function cwdFromTranscript(file) {
1070
1293
  }
1071
1294
  function codexTrustedPaths() {
1072
1295
  try {
1073
- const toml = readFileSync6(join6(codexDir(), "config.toml"), "utf-8");
1296
+ const toml = readFileSync7(join7(codexDir(), "config.toml"), "utf-8");
1074
1297
  return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
1075
1298
  } catch {
1076
1299
  return [];
@@ -1093,7 +1316,7 @@ function gitRepos() {
1093
1316
  const home = homedir3();
1094
1317
  const found = [];
1095
1318
  for (const rootName of GIT_SCAN_ROOTS) {
1096
- const root = join6(home, rootName);
1319
+ const root = join7(home, rootName);
1097
1320
  let entries2;
1098
1321
  try {
1099
1322
  entries2 = readdirSync2(root, { withFileTypes: true });
@@ -1101,7 +1324,7 @@ function gitRepos() {
1101
1324
  continue;
1102
1325
  }
1103
1326
  for (const e of entries2) {
1104
- if (e.isDirectory() && existsSync6(join6(root, e.name, ".git"))) found.push(join6(root, e.name));
1327
+ if (e.isDirectory() && existsSync6(join7(root, e.name, ".git"))) found.push(join7(root, e.name));
1105
1328
  }
1106
1329
  }
1107
1330
  return found;
@@ -1145,11 +1368,11 @@ function scanOnboarding() {
1145
1368
  });
1146
1369
  }
1147
1370
  };
1148
- const claudeProjects = join6(claudeDir(), "projects");
1371
+ const claudeProjects = join7(claudeDir(), "projects");
1149
1372
  try {
1150
1373
  for (const e of readdirSync2(claudeProjects, { withFileTypes: true })) {
1151
1374
  if (!e.isDirectory()) continue;
1152
- const files = jsonlFiles(join6(claudeProjects, e.name), 0);
1375
+ const files = jsonlFiles(join7(claudeProjects, e.name), 0);
1153
1376
  if (!files.length) continue;
1154
1377
  history.claude = true;
1155
1378
  files.sort((a, b) => b.mtime - a.mtime);
@@ -1162,7 +1385,7 @@ function scanOnboarding() {
1162
1385
  }
1163
1386
  } catch {
1164
1387
  }
1165
- const codexSessions = jsonlFiles(join6(codexDir(), "sessions"), 4);
1388
+ const codexSessions = jsonlFiles(join7(codexDir(), "sessions"), 4);
1166
1389
  if (codexSessions.length) history.codex = true;
1167
1390
  const perCwd = /* @__PURE__ */ new Map();
1168
1391
  for (const f of codexSessions) {
@@ -1199,17 +1422,18 @@ function scanOnboarding() {
1199
1422
  fromGitScan
1200
1423
  };
1201
1424
  }
1425
+ var GENERAL_TEMPLATE_DIR = join7(paths.PACKAGE_ROOT, "templates", "general");
1202
1426
  function makeLink(target, linkPath) {
1203
1427
  symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
1204
1428
  }
1205
1429
  function linkProjectFolders(slug, items) {
1206
1430
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
1207
- const dir = join6(flyspaceRoot(), slug);
1208
- mkdirSync4(dir, { recursive: true });
1431
+ const dir = join7(flyspaceRoot(), slug);
1432
+ mkdirSync5(dir, { recursive: true });
1209
1433
  const results = [];
1210
1434
  for (const item of items) {
1211
1435
  if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
1212
- const dest = join6(dir, item.name);
1436
+ const dest = join7(dir, item.name);
1213
1437
  let exists = true;
1214
1438
  try {
1215
1439
  lstatSync(dest);
@@ -1222,7 +1446,8 @@ function linkProjectFolders(slug, items) {
1222
1446
  if (!statSync2(target).isDirectory()) throw new Error(`not a directory: ${target}`);
1223
1447
  makeLink(target, dest);
1224
1448
  } else {
1225
- mkdirSync4(dest, { recursive: true });
1449
+ mkdirSync5(dest, { recursive: true });
1450
+ if (item.name === "general") cpSync(GENERAL_TEMPLATE_DIR, dest, { recursive: true });
1226
1451
  }
1227
1452
  }
1228
1453
  results.push({ name: item.name, cwd: `${slug}/${item.name}` });
@@ -1540,7 +1765,7 @@ async function runAuthCmd(args) {
1540
1765
  }
1541
1766
 
1542
1767
  // src/version.ts
1543
- var CLI_VERSION = true ? "0.1.13" : null;
1768
+ var CLI_VERSION = true ? "0.1.15" : null;
1544
1769
  function isOlderVersion(a, b) {
1545
1770
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1546
1771
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -1654,8 +1879,8 @@ ${BOLD2}Flyspace Setup${RESET2}
1654
1879
  const anyReady = runners.some((r) => r.installed && r.loggedIn);
1655
1880
  for (const slug of workspaces) {
1656
1881
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) continue;
1657
- const dir = join7(flyspaceRoot(), slug);
1658
- mkdirSync5(dir, { recursive: true });
1882
+ const dir = join8(flyspaceRoot(), slug);
1883
+ mkdirSync6(dir, { recursive: true });
1659
1884
  ok(`Folder ${tilde2(dir)}`);
1660
1885
  }
1661
1886
  let hostRunning = false;
@@ -1695,9 +1920,9 @@ import { tmpdir } from "os";
1695
1920
  import { promisify } from "util";
1696
1921
  var execAsync = promisify(execFile);
1697
1922
  var exec = (cmd, args, opts) => {
1698
- const run3 = execAsync(cmd, args, opts);
1699
- run3.child.stdin?.end();
1700
- return run3;
1923
+ const run4 = execAsync(cmd, args, opts);
1924
+ run4.child.stdin?.end();
1925
+ return run4;
1701
1926
  };
1702
1927
  var BOLD3 = "\x1B[1m";
1703
1928
  var DIM3 = "\x1B[2m";
@@ -1818,10 +2043,10 @@ ${BOLD3}Flyspace Doctor${RESET3}
1818
2043
  }
1819
2044
 
1820
2045
  // src/commands/uninstall.ts
1821
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
2046
+ import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
1822
2047
  import { confirm as confirm3 } from "@inquirer/prompts";
1823
2048
  import { execSync as execSync5 } from "child_process";
1824
- import { basename as basename3, join as join8 } from "path";
2049
+ import { basename as basename3, join as join9 } from "path";
1825
2050
  import { homedir as homedir4, platform as platform5 } from "os";
1826
2051
  function log2(msg) {
1827
2052
  console.log(`
@@ -1864,7 +2089,7 @@ async function runServiceUninstall(profile = getProfile()) {
1864
2089
  run2(`systemctl --user disable --now ${unitName}`);
1865
2090
  } catch {
1866
2091
  }
1867
- const unitPath = join8(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
2092
+ const unitPath = join9(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
1868
2093
  try {
1869
2094
  unlinkSync2(unitPath);
1870
2095
  } catch {
@@ -1879,7 +2104,7 @@ async function runServiceUninstall(profile = getProfile()) {
1879
2104
  }
1880
2105
  }
1881
2106
  async function runUninstall(args = []) {
1882
- const prefix = process.env["FLYSPACE_PREFIX"] || join8(homedir4(), ".flyspace");
2107
+ const prefix = process.env["FLYSPACE_PREFIX"] || join9(homedir4(), ".flyspace");
1883
2108
  const targets = [paths.BASE_CONFIG_DIR, paths.BASE_DATA_DIR, prefix].filter(existsSync7);
1884
2109
  console.log("\n This removes Flyspace from this machine:");
1885
2110
  console.log(" - revokes this device and removes every background service");
@@ -1905,15 +2130,15 @@ async function runUninstall(args = []) {
1905
2130
  await runServiceUninstall(profile);
1906
2131
  }
1907
2132
  for (const t of targets) {
1908
- rmSync3(t, { recursive: true, force: true });
2133
+ rmSync4(t, { recursive: true, force: true });
1909
2134
  success2(`Deleted ${t}`);
1910
2135
  }
1911
- for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) => join8(homedir4(), f))) {
2136
+ for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) => join9(homedir4(), f))) {
1912
2137
  if (!existsSync7(rc)) continue;
1913
- const before = readFileSync7(rc, "utf-8");
2138
+ const before = readFileSync8(rc, "utf-8");
1914
2139
  const after = before.replace(/\n# Flyspace CLI\nexport PATH="[^"\n]*\.flyspace\/bin:\$PATH"\n?/g, "\n");
1915
2140
  if (after !== before) {
1916
- writeFileSync5(rc, after);
2141
+ writeFileSync6(rc, after);
1917
2142
  success2(`Removed PATH entry from ${rc}`);
1918
2143
  }
1919
2144
  }
@@ -1921,7 +2146,7 @@ async function runUninstall(args = []) {
1921
2146
  }
1922
2147
 
1923
2148
  // src/commands/profiles-cmd.ts
1924
- import { rmSync as rmSync4 } from "fs";
2149
+ import { rmSync as rmSync5 } from "fs";
1925
2150
  var USAGE = `
1926
2151
  Usage: flyspace profiles [list]
1927
2152
  flyspace profiles remove <name>
@@ -1978,7 +2203,7 @@ async function removeCmd(name) {
1978
2203
  }
1979
2204
  const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
1980
2205
  if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
1981
- rmSync4(configDir2, { recursive: true, force: true });
2206
+ rmSync5(configDir2, { recursive: true, force: true });
1982
2207
  console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
1983
2208
  console.log(`
1984
2209
  Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
@@ -1996,7 +2221,7 @@ async function runProfiles(args = []) {
1996
2221
  // src/commands/upgrade.ts
1997
2222
  import { execSync as execSync6 } from "child_process";
1998
2223
  import { realpathSync as realpathSync2 } from "fs";
1999
- import { join as join9 } from "path";
2224
+ import { join as join10 } from "path";
2000
2225
  function restartInstalledProfiles() {
2001
2226
  if (isHostServiceInstalled()) {
2002
2227
  console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
@@ -2029,10 +2254,10 @@ function npmInstallCommand() {
2029
2254
  }
2030
2255
  function npmEnv() {
2031
2256
  const home = flyspaceHome();
2032
- return home ? { ...process.env, PATH: `${join9(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2257
+ return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2033
2258
  }
2034
2259
  async function runUpgrade() {
2035
- const current = "0.1.13";
2260
+ const current = "0.1.15";
2036
2261
  console.log(`Current version: ${current}`);
2037
2262
  console.log("Checking for updates...\n");
2038
2263
  try {
@@ -2352,6 +2577,7 @@ async function handleImport(manager) {
2352
2577
  async function runAutomationCmd(args) {
2353
2578
  const subcommand = args[0];
2354
2579
  loadEnvFiles();
2580
+ useHostDbWithoutCredentials();
2355
2581
  await refreshChannelRegistry();
2356
2582
  await refreshAutomationRegistry();
2357
2583
  const manager = new AutomationManager();
@@ -2646,11 +2872,11 @@ async function runRestart() {
2646
2872
  // src/commands/logs.ts
2647
2873
  import { spawn as spawn3 } from "child_process";
2648
2874
  import { existsSync as existsSync9 } from "fs";
2649
- import { join as join10 } from "path";
2875
+ import { join as join11 } from "path";
2650
2876
  async function runLogs(args) {
2651
2877
  const showErrors = args.includes("--error") || args.includes("-e");
2652
2878
  const filename = showErrors ? "custie-error.log" : "custie.log";
2653
- const logFile = join10(paths.LOG_DIR, filename);
2879
+ const logFile = join11(paths.LOG_DIR, filename);
2654
2880
  if (!existsSync9(logFile)) {
2655
2881
  console.error(`Log file not found: ${logFile}`);
2656
2882
  console.error("Is the service installed? Run `flyspace install` first.");
@@ -2672,14 +2898,14 @@ async function runLogs(args) {
2672
2898
 
2673
2899
  // src/commands/slack.ts
2674
2900
  import { execFileSync as execFileSync4 } from "child_process";
2675
- import { existsSync as existsSync26, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
2901
+ import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
2676
2902
  import { resolve as resolve6 } from "path";
2677
- import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
2903
+ import { tmpdir as tmpdir5, homedir as homedir9 } from "os";
2678
2904
  import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
2679
2905
  import { WebClient as WebClient3 } from "@slack/web-api";
2680
2906
 
2681
2907
  // src/store/auth-links.ts
2682
- import { createClient } from "@supabase/supabase-js";
2908
+ import { createClient as createClient2 } from "@supabase/supabase-js";
2683
2909
  function toAuthLink(data) {
2684
2910
  const userId = data.user?.id;
2685
2911
  const actionLink = data.properties?.action_link;
@@ -2703,7 +2929,7 @@ async function mintSession(email) {
2703
2929
  if (error2) throw new Error(error2.message);
2704
2930
  const tokenHash = data.properties?.hashed_token;
2705
2931
  if (!tokenHash) throw new Error("Supabase returned no token hash");
2706
- const scratch = createClient(process.env["SUPABASE_URL"], process.env["SUPABASE_SECRET_KEY"], {
2932
+ const scratch = createClient2(process.env["SUPABASE_URL"], process.env["SUPABASE_SECRET_KEY"], {
2707
2933
  auth: { persistSession: false, autoRefreshToken: false }
2708
2934
  });
2709
2935
  const verified = await scratch.auth.verifyOtp({ token_hash: tokenHash, type: "magiclink" });
@@ -2729,7 +2955,7 @@ async function emailForToken(token, type) {
2729
2955
 
2730
2956
  // src/dashboard/profile-config.ts
2731
2957
  import { homedir as homedir5 } from "os";
2732
- import { join as join11 } from "path";
2958
+ import { join as join12 } from "path";
2733
2959
 
2734
2960
  // src/dashboard/profile-config-cache.ts
2735
2961
  var cache = /* @__PURE__ */ new Map();
@@ -2748,7 +2974,7 @@ function profileEnv(key) {
2748
2974
  function profileClaudeConfigDir() {
2749
2975
  const config = cachedProfileConfig();
2750
2976
  if (!config) return process.env["CLAUDE_CONFIG_DIR"] || void 0;
2751
- return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join11(homedir5(), ".claude");
2977
+ return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join12(homedir5(), ".claude");
2752
2978
  }
2753
2979
  function profileOauthToken() {
2754
2980
  const env = cachedProfileConfig()?.env ?? {};
@@ -2850,7 +3076,7 @@ async function sendEmail(email) {
2850
3076
  }
2851
3077
 
2852
3078
  // src/store/invite-store.ts
2853
- import { createHash, randomBytes } from "crypto";
3079
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
2854
3080
  var INVITE_TTL_DAYS = 7;
2855
3081
  var INVITE_MESSAGE_MAX = 500;
2856
3082
  function hashInviteToken(token) {
@@ -2860,7 +3086,7 @@ async function issueInvite(membershipId, message) {
2860
3086
  const db = getSupabaseClient();
2861
3087
  const { error: clearError } = await db.from("workspace_invites").delete().eq("membership_id", membershipId).is("accepted_at", null);
2862
3088
  if (clearError) throw new Error(`issueInvite(${membershipId}): ${clearError.message}`);
2863
- const token = randomBytes(32).toString("hex");
3089
+ const token = randomBytes2(32).toString("hex");
2864
3090
  const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
2865
3091
  const { error: error2 } = await db.from("workspace_invites").insert({
2866
3092
  membership_id: membershipId,
@@ -3229,6 +3455,13 @@ function profileFromHost(host, known) {
3229
3455
  const bare = label2.startsWith("api-") ? label2.slice(4) : label2.startsWith("dev-") ? label2.slice(4) : label2;
3230
3456
  return known.includes(bare) ? bare : null;
3231
3457
  }
3458
+ function isUnknownWorkspaceHost(host, known) {
3459
+ const name = bareHost(host);
3460
+ if (!name?.endsWith(".flyspace.ai")) return false;
3461
+ const label2 = subdomainLabel(host);
3462
+ if (!label2 || label2 === "dev-admin") return false;
3463
+ return profileFromHost(host, known) === null;
3464
+ }
3232
3465
  function workspaceHostname(host, profile) {
3233
3466
  const name = bareHost(host);
3234
3467
  if (!name || !subdomainLabel(host)) return null;
@@ -3300,9 +3533,9 @@ function workspaceOrigin(c, slug) {
3300
3533
  }
3301
3534
 
3302
3535
  // src/store/setup-invite-store.ts
3303
- import { randomBytes as randomBytes2 } from "crypto";
3536
+ import { randomBytes as randomBytes3 } from "crypto";
3304
3537
  async function issueSetupInvite(params) {
3305
- const token = randomBytes2(32).toString("hex");
3538
+ const token = randomBytes3(32).toString("hex");
3306
3539
  const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
3307
3540
  const { error: error2 } = await getSupabaseClient().from("workspace_setup_invites").insert({
3308
3541
  email: params.email,
@@ -3329,7 +3562,7 @@ async function reissueSetupInvite(id) {
3329
3562
  const { data, error: error2 } = await db.from("workspace_setup_invites").select("email, owner_name, message").eq("id", id).is("accepted_at", null).maybeSingle();
3330
3563
  if (error2) throw new Error(`reissueSetupInvite(${id}): ${error2.message}`);
3331
3564
  if (!data) throw new Error("no pending invite with that id");
3332
- const token = randomBytes2(32).toString("hex");
3565
+ const token = randomBytes3(32).toString("hex");
3333
3566
  const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
3334
3567
  const { error: updateError } = await db.from("workspace_setup_invites").update({ token_hash: hashInviteToken(token), expires_at: expiresAt.toISOString() }).eq("id", id);
3335
3568
  if (updateError) throw new Error(`reissueSetupInvite(${id}): ${updateError.message}`);
@@ -3639,7 +3872,9 @@ function getStripeClient() {
3639
3872
 
3640
3873
  // src/store/stripe-trial.ts
3641
3874
  var PLAN_INFO = {
3642
- local: { name: "Local Agent", priceUsd: 9 },
3875
+ // Single-member: a personal Claude subscription can't be shared (see
3876
+ // members-routes.ts, which blocks invites on this plan).
3877
+ local: { name: "Solo", priceUsd: 29 },
3643
3878
  cloud: { name: "Cloud Agent", priceUsd: 49 }
3644
3879
  };
3645
3880
  function priceIdForPlan(plan) {
@@ -3664,6 +3899,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
3664
3899
  metadata: { workspace_id: workspace.id, plan }
3665
3900
  });
3666
3901
  await setWorkspaceStripeSubscription(workspace.id, {
3902
+ plan,
3667
3903
  stripeCustomerId: customer.id,
3668
3904
  stripeSubscriptionId: subscription.id,
3669
3905
  planStatus: "trialing"
@@ -4202,13 +4438,25 @@ async function deliverPasswordResetLink(email, redirectTo) {
4202
4438
  // src/dashboard/server.ts
4203
4439
  import { existsSync as existsSync25 } from "fs";
4204
4440
  import { createServer } from "net";
4205
- import { join as join23 } from "path";
4441
+ import { join as join24 } from "path";
4206
4442
  import { Hono as Hono34 } from "hono";
4443
+
4444
+ // src/dashboard/sentry.ts
4445
+ import * as Sentry from "@sentry/node";
4446
+ import { HTTPException } from "hono/http-exception";
4447
+ var reportingErrorHandler = (err, c) => {
4448
+ if (err instanceof HTTPException) return err.getResponse();
4449
+ Sentry.captureException(err, { extra: { method: c.req.method, path: c.req.path } });
4450
+ console.error(err);
4451
+ return c.text("Internal Server Error", 500);
4452
+ };
4453
+
4454
+ // src/dashboard/server.ts
4207
4455
  import { serve } from "@hono/node-server";
4208
4456
  import { serveStatic } from "@hono/node-server/serve-static";
4209
4457
 
4210
4458
  // src/reconcile.ts
4211
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
4459
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
4212
4460
  async function boundWorkspaces2() {
4213
4461
  return (await boundWorkspaces())?.workspaces ?? null;
4214
4462
  }
@@ -4225,7 +4473,7 @@ async function reconcileProfile(profile) {
4225
4473
  if (!hadConfigDir) log5.push(`config dir created (${paths.CONFIG_DIR})`);
4226
4474
  if (!hadDataDir) log5.push(`data dir created (${paths.DATA_DIR})`);
4227
4475
  if (!existsSync10(paths.CONFIG_FILE)) {
4228
- writeFileSync6(paths.CONFIG_FILE, `BOT_NAME=${profile}
4476
+ writeFileSync7(paths.CONFIG_FILE, `BOT_NAME=${profile}
4229
4477
  `);
4230
4478
  log5.push(`config.env written (${paths.CONFIG_FILE})`);
4231
4479
  }
@@ -4234,7 +4482,7 @@ async function reconcileProfile(profile) {
4234
4482
  if (!channel.cwd) continue;
4235
4483
  const cwd = expandPath(channel.cwd);
4236
4484
  if (isCwdUsable(cwd)) continue;
4237
- mkdirSync6(cwd, { recursive: true });
4485
+ mkdirSync7(cwd, { recursive: true });
4238
4486
  log5.push(`channel "${channel.name}" cwd created (${cwd})`);
4239
4487
  }
4240
4488
  return log5;
@@ -4396,8 +4644,10 @@ async function filterReadableDocsChannels(ctx, channelIds) {
4396
4644
  // src/dashboard/docs-access-gate.ts
4397
4645
  var requireDocsAccess = async (c, next) => {
4398
4646
  const auth = c.get("auth");
4647
+ const channelId = c.req.param("id") ?? "";
4648
+ if (findChannelById(channelId)?.profile !== getProfile()) return c.json({ error: "channel not found" }, 404);
4399
4649
  const need = c.req.method === "GET" ? "read" : "write";
4400
- const have = auth ? await resolveDocsAccess(auth, c.req.param("id") ?? "") : "none";
4650
+ const have = auth ? await resolveDocsAccess(auth, channelId) : "none";
4401
4651
  if (!docsAccessAtLeast(have, need)) return c.json({ error: "docs access required" }, 403);
4402
4652
  await next();
4403
4653
  };
@@ -4406,8 +4656,8 @@ var requireDocsAccess = async (c, next) => {
4406
4656
  import { Hono as Hono31 } from "hono";
4407
4657
 
4408
4658
  // src/dashboard/public-share-routes.ts
4409
- import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
4410
- import { join as join13 } from "path";
4659
+ import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
4660
+ import { join as join14 } from "path";
4411
4661
  import { Hono as Hono3 } from "hono";
4412
4662
  import { GetObjectCommand } from "@aws-sdk/client-s3";
4413
4663
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -4547,15 +4797,15 @@ async function createDocShare(input2) {
4547
4797
  if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
4548
4798
  return rowToShare(data);
4549
4799
  }
4550
- async function updateDocShareSnapshot(id, snapshot3, features) {
4800
+ async function updateDocShareSnapshot(id, snapshot2, features) {
4551
4801
  const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
4552
- title: snapshot3.title,
4553
- content: snapshot3.content,
4554
- asset_keys: snapshot3.assetKeys,
4802
+ title: snapshot2.title,
4803
+ content: snapshot2.content,
4804
+ asset_keys: snapshot2.assetKeys,
4555
4805
  // Re-detected with the snapshot: a re-publish can carry a rewritten
4556
4806
  // article, and one written in another language than last time is exactly
4557
4807
  // the case a stale value would get wrong.
4558
- source_lang: snapshot3.sourceLang,
4808
+ source_lang: snapshot2.sourceLang,
4559
4809
  audio_enabled: features.audioEnabled,
4560
4810
  ask_enabled: features.askEnabled,
4561
4811
  social_enabled: features.socialEnabled,
@@ -4872,12 +5122,12 @@ function injectShareHead(html, head) {
4872
5122
 
4873
5123
  // src/dashboard/app-icon.ts
4874
5124
  import { createHash as createHash2 } from "crypto";
4875
- import { readFileSync as readFileSync8 } from "fs";
4876
- import { join as join12 } from "path";
5125
+ import { readFileSync as readFileSync9 } from "fs";
5126
+ import { join as join13 } from "path";
4877
5127
  import sharp from "sharp";
4878
5128
  var PLATE = "#0A0A0F";
4879
- var MARK_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
4880
- var MARK_SVG_FILE = join12(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
5129
+ var MARK_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
5130
+ var MARK_SVG_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
4881
5131
  var LAYOUT = {
4882
5132
  standard: { logo: 0.7, logoCx: 0.5, logoCy: 0.48, badge: 0.27, badgeCx: 0.22, badgeCy: 0.76 },
4883
5133
  maskable: { logo: 0.56, logoCx: 0.5, logoCy: 0.48, badge: 0.23, badgeCx: 0.33, badgeCy: 0.66 }
@@ -4900,7 +5150,7 @@ function roundedPlate(size, fill = PLATE) {
4900
5150
  }
4901
5151
  async function renderBadge(box) {
4902
5152
  const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
4903
- const raster = await sharp(readFileSync8(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
5153
+ const raster = await sharp(readFileSync9(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
4904
5154
  return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
4905
5155
  }
4906
5156
  async function sampleBackground(logo) {
@@ -4955,7 +5205,7 @@ async function renderAppIcon({
4955
5205
  if (hit) return hit;
4956
5206
  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,
4957
5207
  // either on its usual dark plate or, for a favicon, trimmed and bare.
4958
- (transparent ? await renderBadge(size) : await sharp(readFileSync8(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
5208
+ (transparent ? await renderBadge(size) : await sharp(readFileSync9(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
4959
5209
  cache3.set(key, out);
4960
5210
  return out;
4961
5211
  }
@@ -5483,9 +5733,9 @@ async function synthesizeWithRetry(script, voice) {
5483
5733
  }
5484
5734
  var renderQueue = Promise.resolve();
5485
5735
  function queued(job) {
5486
- const run3 = renderQueue.then(job, job);
5487
- renderQueue = run3.catch(() => void 0);
5488
- return run3;
5736
+ const run4 = renderQueue.then(job, job);
5737
+ renderQueue = run4.catch(() => void 0);
5738
+ return run4;
5489
5739
  }
5490
5740
  function renderArticleAudio(title, content, lang) {
5491
5741
  return queued(() => renderArticleAudioNow(title, content, lang));
@@ -5559,12 +5809,12 @@ function queueShareAudio(share) {
5559
5809
  }
5560
5810
  const current = inFlight2.get(share.id);
5561
5811
  if (current?.hash === hash) return current.done;
5562
- const run3 = () => runShareAudioJob(share, hash).catch((err) => {
5812
+ const run4 = () => runShareAudioJob(share, hash).catch((err) => {
5563
5813
  console.error(`share audio job crashed (${share.id}):`, err);
5564
5814
  }).finally(() => {
5565
5815
  if (inFlight2.get(share.id)?.hash === hash) inFlight2.delete(share.id);
5566
5816
  });
5567
- const done = current ? current.done.then(run3) : run3();
5817
+ const done = current ? current.done.then(run4) : run4();
5568
5818
  inFlight2.set(share.id, { hash, done });
5569
5819
  return done;
5570
5820
  }
@@ -6173,7 +6423,7 @@ var shellCache = null;
6173
6423
  function readShell(path) {
6174
6424
  const { mtimeMs } = statSync3(path);
6175
6425
  if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
6176
- const html = readFileSync9(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6426
+ const html = readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
6177
6427
  shellCache = { mtimeMs, html };
6178
6428
  return html;
6179
6429
  }
@@ -6193,17 +6443,17 @@ function serveBuildFile(c, webDist, dir, name) {
6193
6443
  if (!type || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes("..")) {
6194
6444
  return c.text("not found", 404);
6195
6445
  }
6196
- const path = join13(webDist, dir, name);
6446
+ const path = join14(webDist, dir, name);
6197
6447
  if (!existsSync11(path)) return c.text("not found", 404);
6198
6448
  c.header("Content-Type", type);
6199
6449
  c.header("Cache-Control", "public, max-age=31536000, immutable");
6200
6450
  if (ext === ".css") {
6201
- return c.body(readFileSync9(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6451
+ return c.body(readFileSync10(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
6202
6452
  }
6203
6453
  if (ext === ".js") {
6204
- return c.body(readFileSync9(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6454
+ return c.body(readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
6205
6455
  }
6206
- return c.body(new Uint8Array(readFileSync9(path)));
6456
+ return c.body(new Uint8Array(readFileSync10(path)));
6207
6457
  }
6208
6458
  var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
6209
6459
  function servePublishedHtml(c, share) {
@@ -6339,7 +6589,7 @@ ${analytics}` : head);
6339
6589
  const requested = c.req.param("lang");
6340
6590
  const lang = canonicalLanguage(requested);
6341
6591
  if (!isTranslatableLanguage(lang)) return next();
6342
- const shell = join13(webDist, "index.html");
6592
+ const shell = join14(webDist, "index.html");
6343
6593
  if (!existsSync11(shell)) return next();
6344
6594
  const share = await getDocShare(c.req.param("id"));
6345
6595
  if (!share) return next();
@@ -6375,7 +6625,7 @@ ${analytics}` : head);
6375
6625
  const share = await getDocShare(c.req.param("id"));
6376
6626
  if (!share) return next();
6377
6627
  if (share.kind === "html") return servePublishedHtml(c, share);
6378
- const shell = join13(webDist, "index.html");
6628
+ const shell = join14(webDist, "index.html");
6379
6629
  if (!existsSync11(shell)) return next();
6380
6630
  return readerPage(
6381
6631
  c,
@@ -6488,9 +6738,9 @@ function routeCookieWrites(all, label2, writes) {
6488
6738
  }
6489
6739
 
6490
6740
  // src/dashboard/supabase-server-client.ts
6491
- var REQUEST_TIMEOUT_MS = 5e3;
6492
- function fetchWithTimeout(input2, init) {
6493
- return fetch(input2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
6741
+ var REQUEST_TIMEOUT_MS2 = 5e3;
6742
+ function fetchWithTimeout(input2, init2) {
6743
+ return fetch(input2, { ...init2, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
6494
6744
  }
6495
6745
  function createRequestSupabaseClient(c) {
6496
6746
  const url = process.env["SUPABASE_URL"];
@@ -6532,7 +6782,7 @@ function createRequestSupabaseClient(c) {
6532
6782
  }
6533
6783
 
6534
6784
  // src/dashboard/auth-context.ts
6535
- import { HTTPException } from "hono/http-exception";
6785
+ import { HTTPException as HTTPException2 } from "hono/http-exception";
6536
6786
  import { isAuthApiError, isAuthRetryableFetchError } from "@supabase/supabase-js";
6537
6787
  function onboardingPending(ctx) {
6538
6788
  const pending3 = [];
@@ -6589,10 +6839,10 @@ async function resolveAuthContextUncached(c) {
6589
6839
  } = await getUserWithRetry(supabase));
6590
6840
  } catch (err) {
6591
6841
  if (isAuthRetryableFetchError(err)) {
6592
- throw new HTTPException(503, { message: "auth service unreachable" });
6842
+ throw new HTTPException2(503, { message: "auth service unreachable" });
6593
6843
  }
6594
6844
  if (isAuthApiError(err) && err.status === 429) {
6595
- throw new HTTPException(503, { message: "auth service rate limited" });
6845
+ throw new HTTPException2(503, { message: "auth service rate limited" });
6596
6846
  }
6597
6847
  return null;
6598
6848
  }
@@ -6642,13 +6892,13 @@ async function signAvatarUrl(key) {
6642
6892
 
6643
6893
  // src/dashboard/turnstile.ts
6644
6894
  var VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
6645
- var REQUEST_TIMEOUT_MS2 = 5e3;
6895
+ var REQUEST_TIMEOUT_MS3 = 5e3;
6646
6896
  function siteverify(secret, token) {
6647
6897
  return fetch(VERIFY_URL, {
6648
6898
  method: "POST",
6649
6899
  headers: { "content-type": "application/x-www-form-urlencoded" },
6650
6900
  body: new URLSearchParams({ secret, response: token }),
6651
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
6901
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
6652
6902
  });
6653
6903
  }
6654
6904
  async function verifyTurnstileToken(token) {
@@ -6991,101 +7241,26 @@ async function resolveAgentIdentities() {
6991
7241
  })
6992
7242
  );
6993
7243
  }
6994
- async function offlineAgentIdentities() {
6995
- const identity = await loadBotIdentity();
6996
- const dm = findChannelByAgentName(BOT_DM_KEY) ?? await ensureAgentDmChannel(BOT_DM_KEY, {
6997
- name: identity.displayName ?? BOT_LABEL,
6998
- cwd: resolveChannelConfig("").cwd
6999
- });
7000
- return [{ name: BOT_DM_KEY, label: BOT_LABEL, kind: "main", channelId: dm.id, ...identity }];
7001
- }
7002
-
7003
- // src/dashboard/host-api.ts
7004
- import { existsSync as existsSync20, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
7005
- import { join as join20 } from "path";
7006
- import sharp3 from "sharp";
7007
-
7008
- // src/dashboard/channels-routes.ts
7009
- import { existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
7010
- import { Hono as Hono9 } from "hono";
7011
-
7012
- // src/dashboard/feature-flags-routes.ts
7013
- import { Hono as Hono8 } from "hono";
7014
-
7015
- // src/feature-flags.ts
7016
- var featureFlags = {
7017
- "ai-cost": {
7018
- title: "AI cost",
7019
- description: "Shows admins what the agent has spent: a Cost section in a channel's AI overview and a per-thread cost in the thread menu. Estimated at API list rates, so on a Claude subscription it will not match your bill.",
7020
- stage: "beta"
7021
- },
7022
- "read-aloud": {
7023
- title: "Read aloud",
7024
- description: "Listen to messages, threads and docs as audio. Still rough in places, and each playback uses text-to-speech.",
7025
- stage: "beta"
7026
- },
7027
- excalidraw: {
7028
- title: "Excalidraw",
7029
- description: "Create and edit whiteboard drawings (.excalidraw files) in the Docs pane. Existing drawings stay viewable with this off.",
7030
- stage: "beta"
7031
- },
7032
- sandbox: {
7033
- title: "Sandbox runtime",
7034
- description: "Run a channel agent in an isolated sandbox without Docker. Channels already on Sandbox keep working with this off.",
7035
- stage: "beta"
7036
- },
7037
- dictation: {
7038
- title: "Dictation",
7039
- description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
7040
- stage: "dev"
7041
- },
7042
- "agent-memory": {
7043
- title: "Agent memory",
7044
- 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.",
7045
- stage: "beta"
7046
- }
7047
- };
7048
- var featureFlagIds = Object.keys(featureFlags);
7049
- function isFeatureFlagId(id) {
7050
- return Object.hasOwn(featureFlags, id);
7051
- }
7052
- function isFeatureVisible(stage, devHost) {
7053
- return stage === "beta" || stage === "dev" && devHost;
7054
- }
7055
- function resolveFeatureFlags(enabledIds, devHost) {
7056
- const enabled = new Set(enabledIds);
7057
- const out = {};
7058
- for (const id of featureFlagIds) {
7059
- const stage = featureFlags[id].stage;
7060
- out[id] = stage === "ga" || isFeatureVisible(stage, devHost) && enabled.has(id);
7061
- }
7062
- return out;
7063
- }
7064
-
7065
- // src/store/feature-flag-store.ts
7066
- async function listEnabledFeatureFlags(workspaceId) {
7067
- const { data, error: error2 } = await getSupabaseClient().from("feature_flags").select("flag_id").eq("workspace_id", workspaceId);
7068
- if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
7069
- return (data ?? []).map((r) => r.flag_id);
7070
- }
7071
- async function listEnabledFeatureFlagsBySlug(slug) {
7072
- const { data, error: error2 } = await getSupabaseClient().from("workspaces").select("id").eq("slug", slug).maybeSingle();
7073
- if (error2) throw new Error(`listEnabledFeatureFlagsBySlug(${slug}): ${error2.message}`);
7074
- return data ? listEnabledFeatureFlags(data.id) : [];
7075
- }
7076
- async function enableFeatureFlag(workspaceId, flagId, userId) {
7077
- const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
7078
- { workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
7079
- { onConflict: "workspace_id,flag_id" }
7080
- );
7081
- if (error2) throw new Error(`enableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
7082
- }
7083
- async function disableFeatureFlag(workspaceId, flagId) {
7084
- const { error: error2 } = await getSupabaseClient().from("feature_flags").delete().eq("workspace_id", workspaceId).eq("flag_id", flagId);
7085
- if (error2) throw new Error(`disableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
7244
+ async function offlineAgentIdentities() {
7245
+ const identity = await loadBotIdentity();
7246
+ const dm = findChannelByAgentName(BOT_DM_KEY) ?? await ensureAgentDmChannel(BOT_DM_KEY, {
7247
+ name: identity.displayName ?? BOT_LABEL,
7248
+ cwd: resolveChannelConfig("").cwd
7249
+ });
7250
+ return [{ name: BOT_DM_KEY, label: BOT_LABEL, kind: "main", channelId: dm.id, ...identity }];
7086
7251
  }
7087
7252
 
7253
+ // src/dashboard/host-api.ts
7254
+ import { existsSync as existsSync20, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
7255
+ import { join as join22 } from "path";
7256
+ import sharp3 from "sharp";
7257
+
7258
+ // src/dashboard/channels-routes.ts
7259
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
7260
+ import { Hono as Hono9 } from "hono";
7261
+
7088
7262
  // src/dashboard/feature-flags-routes.ts
7263
+ import { Hono as Hono8 } from "hono";
7089
7264
  async function isFeatureEnabled(c, flagId) {
7090
7265
  const auth = c.get("auth");
7091
7266
  const enabled = await listEnabledFeatureFlags(auth.workspaceId);
@@ -7154,7 +7329,7 @@ function coerceMemberEmails(raw) {
7154
7329
  }
7155
7330
  function ensureCwdDir(cwd) {
7156
7331
  const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
7157
- if (!existsSync12(expanded)) mkdirSync7(expanded, { recursive: true });
7332
+ if (!existsSync12(expanded)) mkdirSync8(expanded, { recursive: true });
7158
7333
  }
7159
7334
  function toResponse(channel) {
7160
7335
  return {
@@ -7305,24 +7480,24 @@ import {
7305
7480
  closeSync,
7306
7481
  existsSync as existsSync16,
7307
7482
  lstatSync as lstatSync2,
7308
- mkdirSync as mkdirSync8,
7483
+ mkdirSync as mkdirSync9,
7309
7484
  openSync,
7310
7485
  readSync,
7311
7486
  readdirSync as readdirSync4,
7312
- readFileSync as readFileSync12,
7487
+ readFileSync as readFileSync14,
7313
7488
  realpathSync as realpathSync3,
7314
7489
  renameSync,
7315
- rmSync as rmSync6,
7490
+ rmSync as rmSync8,
7316
7491
  statSync as statSync5,
7317
- writeFileSync as writeFileSync8
7492
+ writeFileSync as writeFileSync9
7318
7493
  } from "fs";
7319
- import { basename as basename6, dirname as dirname5, extname, join as join18, relative, resolve as resolve4, sep } from "path";
7494
+ import { basename as basename6, dirname as dirname5, extname, join as join20, relative, resolve as resolve4, sep } from "path";
7320
7495
  import { Hono as Hono10 } from "hono";
7321
7496
  import { streamSSE } from "hono/streaming";
7322
7497
 
7323
7498
  // src/dashboard/docs-watcher.ts
7324
7499
  import { existsSync as existsSync13, watch } from "fs";
7325
- import { basename as basename4, join as join14 } from "path";
7500
+ import { basename as basename4, join as join15 } from "path";
7326
7501
  var DEBOUNCE_MS = 200;
7327
7502
  var MAX_PENDING = 200;
7328
7503
  var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
@@ -7363,7 +7538,7 @@ function watchDocs(cwd, listener) {
7363
7538
  const current = entries.get(cwd);
7364
7539
  if (!current || filename === null) return;
7365
7540
  const rel = filename.split("\\").join("/");
7366
- if (rel === root && !existsSync13(join14(cwd, rel))) return;
7541
+ if (rel === root && !existsSync13(join15(cwd, rel))) return;
7367
7542
  if (isIgnoredDocPath(rel)) return;
7368
7543
  if (current.pending.size < MAX_PENDING) current.pending.add(rel);
7369
7544
  if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
@@ -7600,18 +7775,18 @@ async function readOrQueueDocAudio(input2) {
7600
7775
  }
7601
7776
 
7602
7777
  // src/dashboard/artifact-export.ts
7603
- import { readFileSync as readFileSync11 } from "fs";
7778
+ import { readFileSync as readFileSync13 } from "fs";
7604
7779
 
7605
7780
  // src/dashboard/chat-agent.ts
7606
7781
  import { homedir as homedir7 } from "os";
7607
7782
 
7608
7783
  // src/mcp/config.ts
7609
- import { mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
7784
+ import { mkdtempSync, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
7610
7785
  import { tmpdir as tmpdir2 } from "os";
7611
- import { join as join15 } from "path";
7786
+ import { join as join16 } from "path";
7612
7787
 
7613
7788
  // src/mcp/crypto.ts
7614
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
7789
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
7615
7790
  var ALGORITHM = "aes-256-gcm";
7616
7791
  var KEY_BYTES = 32;
7617
7792
  var IV_BYTES = 12;
@@ -7644,7 +7819,7 @@ function currentKeyVersion() {
7644
7819
  );
7645
7820
  }
7646
7821
  function encrypt(key, purpose, plaintext) {
7647
- const iv = randomBytes3(IV_BYTES);
7822
+ const iv = randomBytes4(IV_BYTES);
7648
7823
  const cipher = createCipheriv(ALGORITHM, key, iv);
7649
7824
  cipher.setAAD(Buffer.from(purpose, "utf8"));
7650
7825
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
@@ -7664,7 +7839,7 @@ function decrypt(key, purpose, blob) {
7664
7839
  }
7665
7840
  function createEnvelope() {
7666
7841
  const keyVersion = currentKeyVersion();
7667
- const key = randomBytes3(KEY_BYTES);
7842
+ const key = randomBytes4(KEY_BYTES);
7668
7843
  const wrappedDek = encrypt(
7669
7844
  readMasterKey(keyVersion),
7670
7845
  purposeForDek(keyVersion),
@@ -7730,7 +7905,7 @@ function secretsMatch(a, b) {
7730
7905
  }
7731
7906
 
7732
7907
  // src/mcp/oauth.ts
7733
- import { createHash as createHash7, randomBytes as randomBytes4 } from "crypto";
7908
+ import { createHash as createHash7, randomBytes as randomBytes5 } from "crypto";
7734
7909
  var RESERVED_AUTHORIZE_PARAMS = /* @__PURE__ */ new Set([
7735
7910
  "client_id",
7736
7911
  "redirect_uri",
@@ -7744,10 +7919,10 @@ function base64url(buffer) {
7744
7919
  return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7745
7920
  }
7746
7921
  function createStateToken() {
7747
- return base64url(randomBytes4(32));
7922
+ return base64url(randomBytes5(32));
7748
7923
  }
7749
7924
  function createPkcePair() {
7750
- const verifier = base64url(randomBytes4(32));
7925
+ const verifier = base64url(randomBytes5(32));
7751
7926
  const challenge = base64url(createHash7("sha256").update(verifier).digest());
7752
7927
  return { verifier, challenge };
7753
7928
  }
@@ -8294,12 +8469,12 @@ async function buildMcpConfig(profile, connectionIds) {
8294
8469
  return { config, skipped };
8295
8470
  }
8296
8471
  function writeMcpConfigFile(config) {
8297
- const dir = mkdtempSync(join15(tmpdir2(), "flyspace-mcp-"));
8298
- const path = join15(dir, "mcp-config.json");
8299
- writeFileSync7(path, JSON.stringify(config), { mode: 384 });
8472
+ const dir = mkdtempSync(join16(tmpdir2(), "flyspace-mcp-"));
8473
+ const path = join16(dir, "mcp-config.json");
8474
+ writeFileSync8(path, JSON.stringify(config), { mode: 384 });
8300
8475
  return {
8301
8476
  path,
8302
- cleanup: () => rmSync5(dir, { recursive: true, force: true })
8477
+ cleanup: () => rmSync6(dir, { recursive: true, force: true })
8303
8478
  };
8304
8479
  }
8305
8480
  async function withMcpConfig(profile, connectionIds, fn) {
@@ -8316,22 +8491,22 @@ async function withMcpConfig(profile, connectionIds, fn) {
8316
8491
  }
8317
8492
 
8318
8493
  // src/claude/agents.ts
8319
- import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
8494
+ import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
8320
8495
  import { homedir as homedir6 } from "os";
8321
- import { basename as basename5, join as join16 } from "path";
8496
+ import { basename as basename5, join as join17 } from "path";
8322
8497
  import yaml from "js-yaml";
8323
8498
  function defaultConfigDir() {
8324
- return join16(homedir6(), ".claude");
8499
+ return join17(homedir6(), ".claude");
8325
8500
  }
8326
8501
  function expandHome(p) {
8327
8502
  if (p === "~") return homedir6();
8328
- if (p.startsWith("~/")) return join16(homedir6(), p.slice(2));
8503
+ if (p.startsWith("~/")) return join17(homedir6(), p.slice(2));
8329
8504
  return p;
8330
8505
  }
8331
8506
  function parseFrontmatter(file) {
8332
8507
  let content;
8333
8508
  try {
8334
- content = readFileSync10(file, "utf-8");
8509
+ content = readFileSync11(file, "utf-8");
8335
8510
  } catch {
8336
8511
  return {};
8337
8512
  }
@@ -8355,7 +8530,7 @@ function collectFrom(root, source) {
8355
8530
  return [];
8356
8531
  }
8357
8532
  return entries2.map((file) => {
8358
- const path = join16(root, file);
8533
+ const path = join17(root, file);
8359
8534
  const fm = parseFrontmatter(path);
8360
8535
  const name = fm.name?.trim() || basename5(file, ".md");
8361
8536
  return { name, description: (fm.description ?? "").trim(), source, path };
@@ -8364,8 +8539,8 @@ function collectFrom(root, source) {
8364
8539
  function listAgents(configDir2, cwd) {
8365
8540
  const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
8366
8541
  const collected = [
8367
- ...collectFrom(join16(cwd, ".claude", "agents"), "project"),
8368
- ...collectFrom(join16(base, "agents"), "user")
8542
+ ...collectFrom(join17(cwd, ".claude", "agents"), "project"),
8543
+ ...collectFrom(join17(base, "agents"), "user")
8369
8544
  ];
8370
8545
  const byName = /* @__PURE__ */ new Map();
8371
8546
  for (const agent of collected) {
@@ -8377,241 +8552,135 @@ function readAgentPersona(configDir2, cwd, name) {
8377
8552
  const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
8378
8553
  if (!agent) return void 0;
8379
8554
  try {
8380
- const content = readFileSync10(agent.path, "utf-8");
8555
+ const content = readFileSync11(agent.path, "utf-8");
8381
8556
  return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
8382
8557
  } catch {
8383
8558
  return void 0;
8384
8559
  }
8385
8560
  }
8386
8561
 
8387
- // src/store/memory-store.ts
8388
- var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
8389
- var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8390
- var DESCRIPTION_MAX = 200;
8391
- var BODY_MAX = 4e3;
8392
- var MEMORY_INDEX_MAX_CHARS = 8e3;
8393
- function isMemoryType(value) {
8394
- return MEMORY_TYPES.includes(value);
8395
- }
8396
- function validateMemoryInput(input2) {
8397
- if (!NAME_RE.test(input2.name))
8398
- throw new Error("name must be a kebab-case slug, e.g. deploy-flow");
8399
- if (!isMemoryType(input2.type)) throw new Error(`type must be one of ${MEMORY_TYPES.join(", ")}`);
8400
- const description = input2.description.trim();
8401
- if (!description || description.includes("\n") || description.length > DESCRIPTION_MAX) {
8402
- throw new Error(`description must be one line of at most ${DESCRIPTION_MAX} characters`);
8403
- }
8404
- const body = input2.body.trim();
8405
- if (!body || body.length > BODY_MAX) {
8406
- throw new Error(`body must be 1 to ${BODY_MAX} characters`);
8562
+ // src/dashboard/memory-extraction-runner.ts
8563
+ import { spawn as spawn4 } from "child_process";
8564
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync7 } from "fs";
8565
+ import { tmpdir as tmpdir3 } from "os";
8566
+ import { join as join18 } from "path";
8567
+ var CLAUDE_MODEL = "sonnet";
8568
+ var CODEX_MODEL = "gpt-6-astra";
8569
+ var CODEX_EFFORT = "low";
8570
+ var TIMEOUT_MS = 15e4;
8571
+ function buildInvocation(runner, req, dir, auth, baseEnv = process.env) {
8572
+ const env = { ...baseEnv, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
8573
+ if (runner === "codex") {
8574
+ const outputFile = join18(dir, "last-message.txt");
8575
+ return {
8576
+ command: "codex",
8577
+ args: [
8578
+ "exec",
8579
+ "--ephemeral",
8580
+ // No MCP servers, profiles or hooks from the user's config.toml; auth
8581
+ // still comes from CODEX_HOME.
8582
+ "--ignore-user-config",
8583
+ "--skip-git-repo-check",
8584
+ "-s",
8585
+ "read-only",
8586
+ "-m",
8587
+ CODEX_MODEL,
8588
+ "-c",
8589
+ `model_reasoning_effort=${CODEX_EFFORT}`,
8590
+ "--output-last-message",
8591
+ outputFile,
8592
+ // Codex has no separate system prompt flag; `-` reads the prompt from stdin.
8593
+ "-"
8594
+ ],
8595
+ stdin: `${req.system}
8596
+
8597
+ ---
8598
+
8599
+ ${req.user}`,
8600
+ env,
8601
+ outputFile
8602
+ };
8407
8603
  }
8408
- }
8409
- function rowToMemory(row) {
8604
+ const pinned = pinnedClaudeConfigDir(auth.claudeConfigDir);
8605
+ if (pinned) env["CLAUDE_CONFIG_DIR"] = pinned;
8606
+ else delete env["CLAUDE_CONFIG_DIR"];
8607
+ if (auth.claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = auth.claudeCodeOauthToken;
8608
+ else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
8410
8609
  return {
8411
- id: row["id"],
8412
- channelId: row["channel_id"],
8413
- name: row["name"],
8414
- description: row["description"],
8415
- type: row["type"],
8416
- body: row["body"],
8417
- sourceThreadId: row["source_thread_id"] ?? null,
8418
- createdBy: row["created_by"],
8419
- createdAt: row["created_at"],
8420
- updatedAt: row["updated_at"]
8610
+ command: "claude",
8611
+ args: [
8612
+ "-p",
8613
+ "--model",
8614
+ CLAUDE_MODEL,
8615
+ "--no-session-persistence",
8616
+ "--tools",
8617
+ "",
8618
+ "--strict-mcp-config",
8619
+ "--setting-sources",
8620
+ "",
8621
+ "--system-prompt",
8622
+ req.system
8623
+ ],
8624
+ stdin: req.user,
8625
+ env
8421
8626
  };
8422
8627
  }
8423
- async function listChannelMemories(channelId) {
8424
- const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
8425
- if (error2) throw new Error(`listChannelMemories(${channelId}): ${error2.message}`);
8426
- return (data ?? []).map(rowToMemory);
8427
- }
8428
- async function getChannelMemory(channelId, name) {
8429
- const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
8430
- if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
8431
- return data ? rowToMemory(data) : null;
8432
- }
8433
- async function saveChannelMemory(input2, actor) {
8434
- validateMemoryInput(input2);
8435
- const supabase = getSupabaseClient();
8436
- const existing = await getChannelMemory(input2.channelId, input2.name);
8437
- const fields = {
8438
- description: input2.description.trim(),
8439
- type: input2.type,
8440
- body: input2.body.trim(),
8441
- source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
8442
- created_by: input2.createdBy
8443
- };
8444
- if (existing && sameContent(existing, fields)) {
8445
- return { memory: existing, created: false, changed: false };
8446
- }
8447
- 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();
8448
- if (error2)
8449
- throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
8450
- const memory = rowToMemory(data);
8451
- await logMemoryEvent({
8452
- memory,
8453
- action: existing ? "update" : "create",
8454
- before: existing ? snapshot(existing) : null,
8455
- after: snapshot(memory),
8456
- actor,
8457
- threadId: input2.sourceThreadId ?? null
8458
- });
8459
- return { memory, created: !existing, changed: true };
8460
- }
8461
- async function updateChannelMemory(channelId, memoryId, patch, actor) {
8462
- const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
8463
- if (!existing) return null;
8464
- const { memory } = await saveChannelMemory(
8465
- {
8466
- channelId,
8467
- name: existing.name,
8468
- description: patch.description ?? existing.description,
8469
- type: patch.type ?? existing.type,
8470
- body: patch.body ?? existing.body,
8471
- createdBy: existing.createdBy
8472
- },
8473
- actor
8474
- );
8475
- return memory;
8476
- }
8477
- async function searchChannelMemories(channelId, query) {
8478
- const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
8479
- if (!words.length) return [];
8480
- let request2 = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
8481
- for (const word of words) {
8482
- request2 = request2.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
8483
- }
8484
- const { data, error: error2 } = await request2.order("updated_at", { ascending: false }).limit(20);
8485
- if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
8486
- return (data ?? []).map(rowToMemory);
8487
- }
8488
- async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
8489
- const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
8490
- if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
8491
- const deleted = (data ?? []).map(rowToMemory);
8492
- for (const memory of deleted) {
8493
- await logMemoryEvent({
8494
- memory,
8495
- action: "delete",
8496
- before: snapshot(memory),
8497
- after: null,
8498
- actor,
8499
- threadId: threadId ?? null
8628
+ function run3(inv, cwd) {
8629
+ return new Promise((resolve7) => {
8630
+ const child = spawn4(inv.command, inv.args, {
8631
+ cwd,
8632
+ env: inv.env,
8633
+ stdio: ["pipe", "pipe", "pipe"]
8500
8634
  });
8501
- }
8502
- return deleted.length > 0;
8503
- }
8504
- function snapshot(m) {
8505
- return {
8506
- description: m.description,
8507
- type: m.type,
8508
- body: m.body,
8509
- sourceThreadId: m.sourceThreadId,
8510
- createdBy: m.createdBy
8511
- };
8512
- }
8513
- function sameContent(m, other) {
8514
- return m.description === other.description && m.type === other.type && m.body === other.body;
8515
- }
8516
- async function logMemoryEvent(e) {
8517
- const { error: error2 } = await getSupabaseClient().from("memory_events").insert({
8518
- channel_id: e.memory.channelId,
8519
- memory_id: e.action === "delete" ? null : e.memory.id,
8520
- thread_id: e.threadId,
8521
- name: e.memory.name,
8522
- action: e.action,
8523
- before: e.before,
8524
- after: e.after,
8525
- actor: e.actor
8635
+ let stdout = "";
8636
+ let stderr = "";
8637
+ const timer2 = setTimeout(() => child.kill("SIGTERM"), TIMEOUT_MS);
8638
+ child.stdout.on("data", (chunk3) => stdout += chunk3.toString());
8639
+ child.stderr.on("data", (chunk3) => stderr += chunk3.toString());
8640
+ child.on("error", (err) => {
8641
+ clearTimeout(timer2);
8642
+ console.error(`[memory-extraction] ${inv.command} failed to start:`, err.message);
8643
+ resolve7({ ok: false });
8644
+ });
8645
+ child.on("close", (code) => {
8646
+ clearTimeout(timer2);
8647
+ if (code !== 0) {
8648
+ const detail = (stderr || stdout).trim().split("\n").at(-1) ?? "";
8649
+ console.error(`[memory-extraction] ${inv.command} exited ${code}: ${detail.slice(0, 200)}`);
8650
+ resolve7({ ok: false });
8651
+ return;
8652
+ }
8653
+ let raw = stdout;
8654
+ if (inv.outputFile) {
8655
+ try {
8656
+ raw = readFileSync12(inv.outputFile, "utf-8");
8657
+ } catch {
8658
+ raw = "";
8659
+ }
8660
+ }
8661
+ resolve7({ ok: true, raw });
8662
+ });
8663
+ child.stdin.end(inv.stdin);
8526
8664
  });
8527
- if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
8528
- }
8529
- function rowToEvent(row) {
8530
- return {
8531
- id: row["id"],
8532
- channelId: row["channel_id"],
8533
- memoryId: row["memory_id"] ?? null,
8534
- threadId: row["thread_id"] ?? null,
8535
- name: row["name"],
8536
- action: row["action"],
8537
- before: row["before"] ?? null,
8538
- after: row["after"] ?? null,
8539
- actor: row["actor"],
8540
- createdAt: row["created_at"],
8541
- undoneAt: row["undone_at"] ?? null
8542
- };
8543
8665
  }
8544
- async function listThreadMemoryEvents(channelId, threadId) {
8545
- 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);
8546
- if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
8547
- return (data ?? []).map(rowToEvent);
8548
- }
8549
- async function undoMemoryEvent(channelId, eventId) {
8550
- const supabase = getSupabaseClient();
8551
- const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
8552
- if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
8553
- if (!data) return "not-found";
8554
- const event = rowToEvent(data);
8555
- if (event.undoneAt) return "already-undone";
8556
- const current = await getChannelMemory(channelId, event.name);
8557
- const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
8558
- if (!unchangedSince) return "conflict";
8559
- const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
8560
- if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
8561
- if (!(claimed.data ?? []).length) return "already-undone";
8562
- const before = event.before;
8563
- const restore = before ? {
8564
- description: before.description,
8565
- type: before.type,
8566
- body: before.body,
8567
- source_thread_id: before.sourceThreadId,
8568
- created_by: before.createdBy
8569
- } : null;
8570
- 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 });
8571
- if (writeError) {
8572
- await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
8573
- throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
8574
- }
8575
- return "undone";
8576
- }
8577
- function isUuid(value) {
8578
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
8579
- }
8580
- function formatMemoryPrompt(channelId, memories) {
8581
- const lines = [];
8582
- let used = 0;
8583
- let omitted = 0;
8584
- for (const m of memories) {
8585
- const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
8586
- if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
8587
- omitted++;
8588
- continue;
8589
- }
8590
- lines.push(line);
8591
- used += line.length + 1;
8666
+ async function completeMemoryExtraction(req) {
8667
+ const runner = runnerForModel(loadDashboardSettings().agent);
8668
+ const config = resolveChannelConfig(req.channelId);
8669
+ const dir = mkdtempSync2(join18(tmpdir3(), "flyspace-memory-"));
8670
+ try {
8671
+ const inv = buildInvocation(runner, req, dir, {
8672
+ ...config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {},
8673
+ ...config.claudeCodeOauthToken ? { claudeCodeOauthToken: config.claudeCodeOauthToken } : {}
8674
+ });
8675
+ return await run3(inv, dir);
8676
+ } finally {
8677
+ rmSync7(dir, { recursive: true, force: true });
8592
8678
  }
8593
- const index = lines.length ? lines.join("\n") + (omitted ? `
8594
- - (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
8595
- return [
8596
- `## Channel memory`,
8597
- ``,
8598
- `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.`,
8599
- ``,
8600
- `- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
8601
- `- Search: \`flyspace memory search --channel ${channelId} <words>\``,
8602
- `- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
8603
- `- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
8604
- ``,
8605
- `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.`,
8606
- ``,
8607
- `Index:`,
8608
- index
8609
- ].join("\n");
8610
8679
  }
8611
8680
 
8612
8681
  // src/dashboard/rollover-summary.ts
8613
- import { spawn as spawn4 } from "child_process";
8614
- import { tmpdir as tmpdir3 } from "os";
8682
+ import { spawn as spawn5 } from "child_process";
8683
+ import { tmpdir as tmpdir4 } from "os";
8615
8684
  var HAIKU_MODEL = "haiku";
8616
8685
  var CJK_PATTERN = /[ -〿぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/;
8617
8686
  function estimateTokens(text) {
@@ -8670,11 +8739,11 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8670
8739
  if (claudeConfigDir) env["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
8671
8740
  if (claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = claudeCodeOauthToken;
8672
8741
  else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
8673
- const child = spawn4("claude", args, {
8742
+ const child = spawn5("claude", args, {
8674
8743
  // A stateless rewrite touches no files — tmpdir keeps it that way and
8675
8744
  // avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
8676
8745
  // unlike the channel's own cwd the main turn runs in.
8677
- cwd: tmpdir3(),
8746
+ cwd: tmpdir4(),
8678
8747
  env,
8679
8748
  stdio: ["ignore", "pipe", "pipe"]
8680
8749
  });
@@ -8877,7 +8946,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
8877
8946
 
8878
8947
  // src/dashboard/attachment-downloader.ts
8879
8948
  import { writeFile } from "fs/promises";
8880
- import { join as join17 } from "path";
8949
+ import { join as join19 } from "path";
8881
8950
  import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
8882
8951
  var debug = process.env["DEBUG"] === "true";
8883
8952
  var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
@@ -8924,7 +8993,7 @@ async function downloadOne(key) {
8924
8993
  if (bytes.length > sizeLimit) {
8925
8994
  return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
8926
8995
  }
8927
- const localPath = join17(paths.UPLOADS_DIR, lastSegment);
8996
+ const localPath = join19(paths.UPLOADS_DIR, lastSegment);
8928
8997
  await writeFile(localPath, bytes);
8929
8998
  if (debug)
8930
8999
  console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
@@ -9096,21 +9165,21 @@ var ChatAgent = class {
9096
9165
  * kill.
9097
9166
  */
9098
9167
  async maybeSupersede(msg) {
9099
- const run3 = this.activeRuns.get(msg.threadId);
9100
- if (!run3 || run3.msg.author !== msg.author || run3.msg.replyId === msg.replyId) return;
9168
+ const run4 = this.activeRuns.get(msg.threadId);
9169
+ if (!run4 || run4.msg.author !== msg.author || run4.msg.replyId === msg.replyId) return;
9101
9170
  try {
9102
- if (!await this.shouldInterrupt(run3.msg.body, msg.body)) return;
9171
+ if (!await this.shouldInterrupt(run4.msg.body, msg.body)) return;
9103
9172
  } catch (err) {
9104
9173
  console.error("[chat-agent] interrupt classifier failed:", err.message);
9105
9174
  return;
9106
9175
  }
9107
- if (this.activeRuns.get(msg.threadId) !== run3) return;
9176
+ if (this.activeRuns.get(msg.threadId) !== run4) return;
9108
9177
  console.log(
9109
9178
  `[chat-agent] follow-up supersedes in-flight run \u2014 aborting. thread=${msg.threadId}`
9110
9179
  );
9111
- run3.superseded = true;
9180
+ run4.superseded = true;
9112
9181
  this.supersedingReplies.add(msg.replyId);
9113
- run3.abort.abort();
9182
+ run4.abort.abort();
9114
9183
  }
9115
9184
  /**
9116
9185
  * Picks up messages nobody answered. `handleMessage` is fire-and-forget and
@@ -9273,6 +9342,11 @@ var ChatAgent = class {
9273
9342
  return data.filter((r) => r.id !== excludeReplyId).reverse().slice(-limit).map((r) => ({ author: r.author, body: r.body }));
9274
9343
  }
9275
9344
  /** Whether a reply run is currently in flight for this thread. */
9345
+ /** One idle memory extraction call (see memory-extraction-runner.ts). Not a
9346
+ * turn: no thread, session, queue slot or usage row. */
9347
+ completeMemoryExtraction(req) {
9348
+ return completeMemoryExtraction(req);
9349
+ }
9276
9350
  isThinking(threadId) {
9277
9351
  return this.activeRuns.has(threadId);
9278
9352
  }
@@ -9308,9 +9382,9 @@ var ChatAgent = class {
9308
9382
  * continue with `--resume`. Returns whether a run was actually in flight —
9309
9383
  * a queued-but-not-started message is not affected. */
9310
9384
  cancel(threadId) {
9311
- const run3 = this.activeRuns.get(threadId);
9312
- if (!run3) return false;
9313
- run3.abort.abort();
9385
+ const run4 = this.activeRuns.get(threadId);
9386
+ if (!run4) return false;
9387
+ run4.abort.abort();
9314
9388
  return true;
9315
9389
  }
9316
9390
  /**
@@ -9472,18 +9546,11 @@ var ChatAgent = class {
9472
9546
  ...docsAccessUnverified ? { docsAccessUnverified } : {}
9473
9547
  };
9474
9548
  }
9475
- const agentMemory = await listEnabledFeatureFlagsBySlug(owningProfile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
9476
- console.error("[chat-agent] feature flag lookup failed:", err.message);
9477
- return false;
9549
+ const memory = await loadAgentMemory(owningProfile, thread.channel_id, {
9550
+ readable: docsAccess !== "none"
9478
9551
  });
9479
- if (agentMemory) channelConfig = { ...channelConfig, disableAutoMemory: true };
9480
- if (agentMemory && docsAccess !== "none" && isUuid(thread.channel_id)) {
9481
- const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
9482
- console.error("[chat-agent] memory lookup failed:", err.message);
9483
- return void 0;
9484
- });
9485
- if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
9486
- }
9552
+ if (memory.enabled) channelConfig = { ...channelConfig, disableAutoMemory: true };
9553
+ if (memory.memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt: memory.memoryPrompt };
9487
9554
  const store = this.openSessionStore();
9488
9555
  try {
9489
9556
  const channelKey = `chat:${thread.channel_id}`;
@@ -10211,7 +10278,7 @@ async function exportDocAsArtifact(channelId, docPath) {
10211
10278
  );
10212
10279
  const abs = resolveDocPath(channelConfig.cwd, docPath);
10213
10280
  if (!abs) throw new Error(`doc not found: ${docPath}`);
10214
- const content = readFileSync11(abs, "utf8");
10281
+ const content = readFileSync13(abs, "utf8");
10215
10282
  const title = extractTitle(abs, "md");
10216
10283
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10217
10284
  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).
@@ -10468,7 +10535,7 @@ function probeDir(absDir, showHidden) {
10468
10535
  return false;
10469
10536
  }
10470
10537
  if (!showHidden && entry.name.startsWith(".")) continue;
10471
- const abs = join18(dir, entry.name);
10538
+ const abs = join20(dir, entry.name);
10472
10539
  const kind = entryKind(entry, abs);
10473
10540
  if (kind === "dir") {
10474
10541
  if (SKIP_DIRS.has(entry.name)) hasOther = true;
@@ -10488,14 +10555,14 @@ function probeDir(absDir, showHidden) {
10488
10555
  }
10489
10556
  function readGitBranch(dir) {
10490
10557
  try {
10491
- let gitDir = join18(dir, ".git");
10558
+ let gitDir = join20(dir, ".git");
10492
10559
  if (statSync5(gitDir).isFile()) {
10493
- const pointer = readFileSync12(gitDir, "utf8").trim();
10560
+ const pointer = readFileSync14(gitDir, "utf8").trim();
10494
10561
  const match = pointer.match(/^gitdir:\s*(.+)$/);
10495
10562
  if (!match) return void 0;
10496
10563
  gitDir = resolve4(dir, match[1]);
10497
10564
  }
10498
- const head = readFileSync12(join18(gitDir, "HEAD"), "utf8").trim();
10565
+ const head = readFileSync14(join20(gitDir, "HEAD"), "utf8").trim();
10499
10566
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
10500
10567
  if (branchMatch) return branchMatch[1];
10501
10568
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -10517,14 +10584,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
10517
10584
  for (const entry of entries2) {
10518
10585
  if (dirs.length + docs.length >= MAX_ENTRIES) break;
10519
10586
  if (!showHidden && entry.name.startsWith(".")) continue;
10520
- const abs = join18(absDir, entry.name);
10587
+ const abs = join20(absDir, entry.name);
10521
10588
  const path = relative(root, abs).split(sep).join("/");
10522
10589
  const kind = entryKind(entry, abs);
10523
10590
  if (kind === "dir") {
10524
10591
  if (SKIP_DIRS.has(entry.name)) continue;
10525
10592
  const probe3 = probeDir(abs, showHidden);
10526
10593
  if (!probe3.hasDocs && probe3.hasOther) continue;
10527
- const isRepo = existsSync16(join18(abs, ".git"));
10594
+ const isRepo = existsSync16(join20(abs, ".git"));
10528
10595
  dirs.push({
10529
10596
  path,
10530
10597
  name: entry.name,
@@ -10560,7 +10627,7 @@ function flatDocs(root, {
10560
10627
  for (const entry of entries2) {
10561
10628
  if (hits.length >= MAX_FLAT_DOCS) return;
10562
10629
  if (!showHidden && entry.name.startsWith(".")) continue;
10563
- const abs = join18(dir, entry.name);
10630
+ const abs = join20(dir, entry.name);
10564
10631
  const kind = entryKind(entry, abs);
10565
10632
  if (kind === "dir") {
10566
10633
  if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
@@ -10602,7 +10669,7 @@ function writeDocContent(root, rel, content, baseMtime) {
10602
10669
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) {
10603
10670
  return { status: "too-large" };
10604
10671
  }
10605
- writeFileSync8(abs, content, "utf8");
10672
+ writeFileSync9(abs, content, "utf8");
10606
10673
  return { status: "ok", mtime: statSync5(abs).mtime.toISOString() };
10607
10674
  }
10608
10675
  function resolveRawPath(root, rel) {
@@ -10640,7 +10707,7 @@ function uniqueName(absDir, name) {
10640
10707
  let candidate = name;
10641
10708
  for (let n = 1; n < 1e3; n++) {
10642
10709
  try {
10643
- statSync5(join18(absDir, candidate));
10710
+ statSync5(join20(absDir, candidate));
10644
10711
  } catch {
10645
10712
  return candidate;
10646
10713
  }
@@ -10662,8 +10729,8 @@ function saveUploadedFile(root, relDir, name, data) {
10662
10729
  if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
10663
10730
  if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
10664
10731
  const final = uniqueName(absDir, safe);
10665
- const abs = join18(absDir, final);
10666
- writeFileSync8(abs, data);
10732
+ const abs = join20(absDir, final);
10733
+ writeFileSync9(abs, data);
10667
10734
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
10668
10735
  }
10669
10736
  function createDocFolder(root, relDir, name) {
@@ -10676,13 +10743,13 @@ function createDocFolder(root, relDir, name) {
10676
10743
  }
10677
10744
  const safe = sanitizeUploadName(name);
10678
10745
  if (!safe) return { status: "bad-name" };
10679
- const abs = join18(absDir, safe);
10746
+ const abs = join20(absDir, safe);
10680
10747
  try {
10681
10748
  statSync5(abs);
10682
10749
  return { status: "exists" };
10683
10750
  } catch {
10684
10751
  }
10685
- mkdirSync8(abs);
10752
+ mkdirSync9(abs);
10686
10753
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: safe };
10687
10754
  }
10688
10755
  function createDocFile(root, relDir, name, content) {
@@ -10699,9 +10766,9 @@ function createDocFile(root, relDir, name, content) {
10699
10766
  if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
10700
10767
  if (SKIP_FILES.has(safe)) return { status: "bad-name" };
10701
10768
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
10702
- const abs = join18(absDir, safe);
10769
+ const abs = join20(absDir, safe);
10703
10770
  try {
10704
- writeFileSync8(abs, content, { encoding: "utf8", flag: "wx" });
10771
+ writeFileSync9(abs, content, { encoding: "utf8", flag: "wx" });
10705
10772
  } catch (err) {
10706
10773
  if (err.code === "EEXIST") return { status: "exists" };
10707
10774
  throw err;
@@ -10737,7 +10804,7 @@ function moveDocEntry(root, from, to) {
10737
10804
  } catch {
10738
10805
  return { status: "bad-target" };
10739
10806
  }
10740
- const finalAbs = join18(absToDir, name);
10807
+ const finalAbs = join20(absToDir, name);
10741
10808
  if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
10742
10809
  return { status: "into-self" };
10743
10810
  }
@@ -10760,7 +10827,7 @@ function deleteDocEntry(root, path) {
10760
10827
  }
10761
10828
  const isDir2 = stat.isDirectory();
10762
10829
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10763
- rmSync6(abs, { recursive: isDir2 });
10830
+ rmSync8(abs, { recursive: isDir2 });
10764
10831
  return {
10765
10832
  status: "ok",
10766
10833
  path: relative(root, abs).split(sep).join("/"),
@@ -10799,7 +10866,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10799
10866
  }
10800
10867
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10801
10868
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10802
- const data = readFileSync12(resolved2.abs);
10869
+ const data = readFileSync14(resolved2.abs);
10803
10870
  const headers = { "Content-Type": resolved2.contentType };
10804
10871
  if (download) {
10805
10872
  const filename = basename6(resolved2.abs).replace(/"/g, "");
@@ -10819,7 +10886,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10819
10886
  }
10820
10887
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
10821
10888
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
10822
- return c.body(new Uint8Array(readFileSync12(abs)), 200, {
10889
+ return c.body(new Uint8Array(readFileSync14(abs)), 200, {
10823
10890
  "Content-Type": "text/html; charset=utf-8",
10824
10891
  "Content-Security-Policy": HTML_SHARE_CSP,
10825
10892
  "X-Content-Type-Options": "nosniff",
@@ -11004,7 +11071,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11004
11071
  title: kind ? extractTitle(abs, kind) : basename6(abs),
11005
11072
  mtime: stat.mtime.toISOString(),
11006
11073
  tooLarge: tooLarge || void 0,
11007
- content: tooLarge ? "" : readFileSync12(abs, "utf8")
11074
+ content: tooLarge ? "" : readFileSync14(abs, "utf8")
11008
11075
  });
11009
11076
  });
11010
11077
  app.put("/:id/docs/content", async (c) => {
@@ -11047,7 +11114,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11047
11114
  channel,
11048
11115
  rel,
11049
11116
  title: extractTitle(abs, "md"),
11050
- content: readFileSync12(abs, "utf8")
11117
+ content: readFileSync14(abs, "utf8")
11051
11118
  };
11052
11119
  };
11053
11120
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -11270,7 +11337,7 @@ function ownSkills(scope, dir) {
11270
11337
  return [{ scope, name, description, body, path, plugin: null, mtime }];
11271
11338
  });
11272
11339
  }
11273
- function snapshot2(scopes) {
11340
+ function snapshot(scopes) {
11274
11341
  const wants = (scope) => !scopes || scopes.has(scope);
11275
11342
  const configDir2 = profileClaudeConfigDir();
11276
11343
  const rows = [];
@@ -11305,7 +11372,7 @@ async function pushSkillScopes(scopes) {
11305
11372
  if (!mirrorEnabled()) return;
11306
11373
  const workspace = getProfile();
11307
11374
  const supabase = getSupabaseClient();
11308
- const rows = snapshot2(scopes ? new Set(scopes) : void 0);
11375
+ const rows = snapshot(scopes ? new Set(scopes) : void 0);
11309
11376
  let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
11310
11377
  if (scopes) query = query.in("scope", scopes);
11311
11378
  const { data, error: error2 } = await query;
@@ -11586,10 +11653,10 @@ function registerMirroredSkillRoutes(api, agent) {
11586
11653
  }
11587
11654
 
11588
11655
  // src/dashboard/providers.ts
11589
- import { execFileSync as execFileSync2 } from "child_process";
11656
+ import { execFileSync as execFileSync3 } from "child_process";
11590
11657
  import { existsSync as existsSync19 } from "fs";
11591
11658
  import { homedir as homedir8 } from "os";
11592
- import { join as join19, resolve as resolve5 } from "path";
11659
+ import { join as join21, resolve as resolve5 } from "path";
11593
11660
  var PROVIDER_LABELS = {
11594
11661
  claude: "Claude Code",
11595
11662
  codex: "Codex"
@@ -11598,7 +11665,7 @@ var TTL_MS2 = 6e4;
11598
11665
  var cache6 = /* @__PURE__ */ new Map();
11599
11666
  function binaryOnPath(bin) {
11600
11667
  try {
11601
- execFileSync2(bin, ["--version"], { stdio: "ignore" });
11668
+ execFileSync3(bin, ["--version"], { stdio: "ignore" });
11602
11669
  return true;
11603
11670
  } catch {
11604
11671
  return false;
@@ -11607,10 +11674,10 @@ function binaryOnPath(bin) {
11607
11674
  function claudeAuthAvailable() {
11608
11675
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
11609
11676
  const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
11610
- if (existsSync19(join19(configDir2, ".credentials.json"))) return true;
11677
+ if (existsSync19(join21(configDir2, ".credentials.json"))) return true;
11611
11678
  if (process.platform !== "darwin") return false;
11612
11679
  try {
11613
- execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
11680
+ execFileSync3("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
11614
11681
  stdio: "ignore"
11615
11682
  });
11616
11683
  return true;
@@ -11760,7 +11827,7 @@ function registerHostRoutes(api) {
11760
11827
  const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
11761
11828
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
11762
11829
  const filename = errors ? "custie-error.log" : "custie.log";
11763
- const file = join20(paths.LOG_DIR, filename);
11830
+ const file = join22(paths.LOG_DIR, filename);
11764
11831
  const exists = existsSync20(file);
11765
11832
  return c.json({
11766
11833
  file,
@@ -11772,7 +11839,7 @@ function registerHostRoutes(api) {
11772
11839
  }
11773
11840
  function tailFile(file, limit) {
11774
11841
  if (!existsSync20(file)) return [];
11775
- const lines = readFileSync13(file, "utf-8").split("\n");
11842
+ const lines = readFileSync15(file, "utf-8").split("\n");
11776
11843
  if (lines.at(-1) === "") lines.pop();
11777
11844
  return lines.slice(-limit);
11778
11845
  }
@@ -13445,7 +13512,7 @@ function createSearchRouter() {
13445
13512
  }
13446
13513
 
13447
13514
  // src/dashboard/uploads-routes.ts
13448
- import { randomUUID } from "crypto";
13515
+ import { randomUUID as randomUUID2 } from "crypto";
13449
13516
  import { Hono as Hono18 } from "hono";
13450
13517
  import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
13451
13518
  import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
@@ -13469,7 +13536,7 @@ function createUploadsRouter() {
13469
13536
  if (file.size > MAX_FILE_BYTES) {
13470
13537
  return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
13471
13538
  }
13472
- const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID()}-${sanitizeFilename(file.name)}`;
13539
+ const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
13473
13540
  const buffer = Buffer.from(await file.arrayBuffer());
13474
13541
  await getR2Client().send(
13475
13542
  new PutObjectCommand3({
@@ -13552,7 +13619,7 @@ function createTranscribeRouter() {
13552
13619
  }
13553
13620
 
13554
13621
  // src/dashboard/avatars-routes.ts
13555
- import { createHash as createHash10, randomUUID as randomUUID2 } from "crypto";
13622
+ import { createHash as createHash10, randomUUID as randomUUID3 } from "crypto";
13556
13623
  import { Hono as Hono20 } from "hono";
13557
13624
  import sharp4 from "sharp";
13558
13625
  import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
@@ -13591,7 +13658,7 @@ async function storeAvatar(file, owner) {
13591
13658
  if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
13592
13659
  const original = Buffer.from(await file.arrayBuffer());
13593
13660
  const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
13594
- const key = `${avatarKeyPrefix()}${owner}-${randomUUID2()}.png`;
13661
+ const key = `${avatarKeyPrefix()}${owner}-${randomUUID3()}.png`;
13595
13662
  await getR2Client().send(
13596
13663
  new PutObjectCommand4({
13597
13664
  Bucket: getR2Bucket(),
@@ -13756,6 +13823,10 @@ function createMembersRouter() {
13756
13823
  if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
13757
13824
  return c.json({ error: "unknown avatar" }, 400);
13758
13825
  }
13826
+ const { plan } = await getWorkspaceBilling(ctx.workspaceId);
13827
+ if (plan === "local") {
13828
+ return c.json({ error: "The Solo plan is for one person. Upgrade to invite members." }, 403);
13829
+ }
13759
13830
  const existing = await getMembershipByEmail(ctx.workspaceId, email);
13760
13831
  if (existing) return c.json({ error: "already a member of this workspace" }, 409);
13761
13832
  let membership;
@@ -14953,8 +15024,16 @@ function uniqueChannelName(raw, taken) {
14953
15024
  // src/dashboard/workspace-greeting.ts
14954
15025
  var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
14955
15026
  var GREETING_MODEL = "openai/gpt-oss-20b";
14956
- var TIMEOUT_MS = 6e3;
15027
+ var TIMEOUT_MS2 = 6e3;
14957
15028
  var MAX_LISTED_CHANNELS = 8;
15029
+ var GUIDES = [
15030
+ "Welcome to Flyspace",
15031
+ "Working with the agent",
15032
+ "Channels and folders",
15033
+ "Docs",
15034
+ "Inviting your team",
15035
+ "Automations"
15036
+ ];
14958
15037
  var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner sees, posted in their #general channel. Flyspace is a team chat where an AI agent works inside every channel.
14959
15038
 
14960
15039
  - Plain Markdown, 60 to 110 words, in English. No headings, no emoji, no em or en dashes, no sign-off.
@@ -14962,11 +15041,13 @@ var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner
14962
15041
  - Say that #general is public: everyone invited to the workspace can see it.
14963
15042
  - If project channels are listed, mention them as #name and say each one is linked to that project folder. Never invent a channel.
14964
15043
  - Mention the trial or the missing Host only if the facts say so.
15044
+ - If guides are listed, say they are in the Docs pane of #general and suggest starting with the first one.
14965
15045
  - Use only the facts given. Do not add buttons or a list of next steps: those are appended for you.`;
14966
15046
  function greetingButtons(input2) {
14967
15047
  const buttons = [];
14968
15048
  if (!input2.hostConnected) buttons.push("Connect a machine");
14969
15049
  if (input2.channels[0]) buttons.push(`Summarise #${input2.channels[0]} for me`);
15050
+ if (input2.hostConnected) buttons.push("Give me a quick tour of the guides");
14970
15051
  if (input2.memberCount <= 1) buttons.push("Invite my team");
14971
15052
  buttons.push("Set up a daily digest");
14972
15053
  return buttons.slice(0, 3).map((b) => b.replace(/[|\]]/g, ""));
@@ -14977,6 +15058,7 @@ function fallbackGreeting(input2) {
14977
15058
  `Welcome to ${input2.workspaceName}, ${input2.ownerName}.`,
14978
15059
  "This is #general. It is public, so everyone you invite to the workspace can see it.",
14979
15060
  channels.length ? `Your project channels are ready: ${channels.join(", ")}. Each one is linked to its project folder, so the agent there works on the real files.` : "",
15061
+ input2.hostConnected ? "A few short guides are waiting in the Docs pane of #general. Start with Welcome to Flyspace." : "",
14980
15062
  "Mention the agent in any channel to get started."
14981
15063
  ].filter(Boolean).join("\n\n");
14982
15064
  }
@@ -14987,11 +15069,12 @@ async function groqGreeting(input2, apiKey) {
14987
15069
  `Plan: ${input2.trialing ? "14-day free trial, just started" : "not on a trial"}`,
14988
15070
  `Host: ${input2.hostConnected ? "a machine is connected, agents can run" : "no machine connected yet, agents cannot run until one is"}`,
14989
15071
  `Project channels: ${input2.channels.slice(0, MAX_LISTED_CHANNELS).join(", ") || "(none yet)"}`,
14990
- `Members: ${input2.memberCount}`
15072
+ `Members: ${input2.memberCount}`,
15073
+ `Guides in #general: ${input2.hostConnected ? GUIDES.join(", ") : "(none, no machine connected)"}`
14991
15074
  ].join("\n");
14992
15075
  const res = await fetch(GROQ_API_URL8, {
14993
15076
  method: "POST",
14994
- signal: AbortSignal.timeout(TIMEOUT_MS),
15077
+ signal: AbortSignal.timeout(TIMEOUT_MS2),
14995
15078
  headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
14996
15079
  body: JSON.stringify({
14997
15080
  model: GREETING_MODEL,
@@ -15069,6 +15152,14 @@ function createOnboardingRouter(agent) {
15069
15152
  await markWorkspaceProjectsSetup(ctx.workspaceId);
15070
15153
  return c.json({ channels: created.map((ch) => ({ id: ch.id, name: ch.name })) }, 201);
15071
15154
  });
15155
+ app.get("/greeting", async (c) => {
15156
+ const ctx = await resolveAuthContext(c);
15157
+ if (ctx?.role !== "admin") return c.json({ pending: false });
15158
+ const greetedAt = await getWorkspaceGreetedAt(ctx.workspaceId);
15159
+ const pending3 = !!ctx.projectsSetupAt && !greetedAt;
15160
+ const general = pending3 ? await ensureGeneralChannel() : null;
15161
+ return c.json({ pending: pending3, channelId: general?.id ?? null });
15162
+ });
15072
15163
  app.post("/greeting", async (c) => {
15073
15164
  const ctx = await resolveAuthContext(c);
15074
15165
  if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
@@ -15077,6 +15168,8 @@ function createOnboardingRouter(agent) {
15077
15168
  const existing = () => supabase.from("threads").select("id").eq("client_key", ctx.workspaceId).maybeSingle();
15078
15169
  const found = await existing();
15079
15170
  if (found.data) return c.json({ channelId: general.id, threadId: found.data.id });
15171
+ if (await getWorkspaceGreetedAt(ctx.workspaceId))
15172
+ return c.json({ error: "already greeted" }, 409);
15080
15173
  const [branding, billing, members] = await Promise.all([
15081
15174
  getWorkspaceBranding(getProfile()).catch(() => null),
15082
15175
  getWorkspaceBilling(ctx.workspaceId).catch(() => null),
@@ -15107,6 +15200,7 @@ function createOnboardingRouter(agent) {
15107
15200
  await supabase.from("threads").delete().eq("id", thread.id);
15108
15201
  return c.json({ error: reply.error.message }, 500);
15109
15202
  }
15203
+ await markWorkspaceGreeted(ctx.workspaceId);
15110
15204
  return c.json({ channelId: general.id, threadId: thread.id }, 201);
15111
15205
  });
15112
15206
  return app;
@@ -15281,17 +15375,17 @@ function registerDataRoutes(api, agent = {}) {
15281
15375
  const auto = loadEffectiveAutomations();
15282
15376
  const runs = /* @__PURE__ */ new Map();
15283
15377
  try {
15284
- for (const [name, run3] of await lastRunsForWorkspace(getProfile())) {
15285
- runs.set(name, { lastRun: run3.lastRunAt.toISOString(), lastStatus: run3.lastStatus });
15378
+ for (const [name, run4] of await lastRunsForWorkspace(getProfile())) {
15379
+ runs.set(name, { lastRun: run4.lastRunAt.toISOString(), lastStatus: run4.lastStatus });
15286
15380
  }
15287
15381
  } catch {
15288
15382
  if (existsSync22(paths.DB_FILE)) {
15289
15383
  const runStore = new AutomationRunStore(paths.DB_FILE);
15290
15384
  try {
15291
- for (const run3 of runStore.listLastRuns()) {
15292
- runs.set(run3.name, {
15293
- lastRun: run3.lastRunAt.toISOString(),
15294
- lastStatus: run3.lastStatus
15385
+ for (const run4 of runStore.listLastRuns()) {
15386
+ runs.set(run4.name, {
15387
+ lastRun: run4.lastRunAt.toISOString(),
15388
+ lastStatus: run4.lastStatus
15295
15389
  });
15296
15390
  }
15297
15391
  } finally {
@@ -15481,17 +15575,17 @@ function registerDataRoutes(api, agent = {}) {
15481
15575
  }
15482
15576
  const threadLinks = await resolveThreadLinks(raw);
15483
15577
  const sessions = raw.map((s) => {
15484
- const run3 = latest.get(s.threadTs);
15485
- const turns = run3?.numTurns ?? 1;
15486
- const total = run3 ? run3.inputTokens + run3.cacheReadTokens + run3.cacheCreationTokens : void 0;
15578
+ const run4 = latest.get(s.threadTs);
15579
+ const turns = run4?.numTurns ?? 1;
15580
+ const total = run4 ? run4.inputTokens + run4.cacheReadTokens + run4.cacheCreationTokens : void 0;
15487
15581
  const link = s.channelId.startsWith("chat:") ? threadLinks.chat.get(s.threadTs) : threadLinks.external.get(`${s.channelId}:${s.threadTs}`);
15488
15582
  return {
15489
15583
  ...s,
15490
15584
  channelLabel: channelLabel(s.channelId),
15491
- model: run3?.model,
15492
- numTurns: run3?.numTurns,
15585
+ model: run4?.model,
15586
+ numTurns: run4?.numTurns,
15493
15587
  contextTokens: total !== void 0 ? Math.round(total / (turns > 0 ? turns : 1)) : void 0,
15494
- rolledOver: run3?.label === "rollover",
15588
+ rolledOver: run4?.label === "rollover",
15495
15589
  threadTitle: link?.title ?? void 0,
15496
15590
  chatChannelId: link?.chatChannelId,
15497
15591
  chatThreadId: link?.chatThreadId,
@@ -15594,16 +15688,16 @@ function relay2(agent) {
15594
15688
  }
15595
15689
 
15596
15690
  // src/dashboard/agent-bridge.ts
15597
- import { existsSync as existsSync24, readFileSync as readFileSync17 } from "fs";
15691
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
15598
15692
  import dotenv from "dotenv";
15599
15693
  import { WebSocket, WebSocketServer } from "ws";
15600
15694
 
15601
15695
  // src/dashboard/host-identity.ts
15602
15696
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
15603
- import { existsSync as existsSync23, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
15604
- import { dirname as dirname7, join as join21 } from "path";
15697
+ import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
15698
+ import { dirname as dirname7, join as join23 } from "path";
15605
15699
  function hostPrivateKeyPath() {
15606
- return process.env["FLYSPACE_HOST_KEY_PATH"] || join21(paths.BASE_CONFIG_DIR, "host.key");
15700
+ return process.env["FLYSPACE_HOST_KEY_PATH"] || join23(paths.BASE_CONFIG_DIR, "host.key");
15607
15701
  }
15608
15702
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15609
15703
  if (!existsSync23(path)) {
@@ -15611,7 +15705,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15611
15705
  `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.`
15612
15706
  );
15613
15707
  }
15614
- return createPrivateKey2(readFileSync14(path, "utf8"));
15708
+ return createPrivateKey2(readFileSync16(path, "utf8"));
15615
15709
  }
15616
15710
  function loadHostPublicKey() {
15617
15711
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -15624,82 +15718,6 @@ function loadHostPublicKey() {
15624
15718
  }
15625
15719
  }
15626
15720
 
15627
- // src/dashboard/host-lock.ts
15628
- import { execFileSync as execFileSync3 } from "child_process";
15629
- import { randomBytes as randomBytes5 } from "crypto";
15630
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
15631
- import { join as join22 } from "path";
15632
- function hostLockPath() {
15633
- return join22(paths.BASE_DATA_DIR, "host.lock");
15634
- }
15635
- function isLiveFlyspace(pid) {
15636
- try {
15637
- process.kill(pid, 0);
15638
- const command = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
15639
- encoding: "utf8"
15640
- });
15641
- return command.includes("flyspace") || command.includes("host-server");
15642
- } catch {
15643
- return false;
15644
- }
15645
- }
15646
- function readLockPid(path) {
15647
- try {
15648
- const pid = Number(JSON.parse(readFileSync15(path, "utf8")).pid);
15649
- return Number.isInteger(pid) && pid > 0 ? pid : null;
15650
- } catch {
15651
- return null;
15652
- }
15653
- }
15654
- function acquireHostLock() {
15655
- const path = hostLockPath();
15656
- mkdirSync10(paths.BASE_DATA_DIR, { recursive: true });
15657
- const body = JSON.stringify({
15658
- pid: process.pid,
15659
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
15660
- execPath: process.argv[1] ?? ""
15661
- });
15662
- for (let attempt = 0; attempt < 2; attempt++) {
15663
- try {
15664
- writeFileSync10(path, body, { flag: "wx", mode: 384 });
15665
- const release2 = () => {
15666
- if (readLockPid(path) === process.pid) rmSync7(path, { force: true });
15667
- };
15668
- process.once("exit", release2);
15669
- return release2;
15670
- } catch (err) {
15671
- if (err.code !== "EEXIST") throw err;
15672
- const holder = readLockPid(path);
15673
- if (holder === process.pid) return () => {
15674
- };
15675
- if (holder && isLiveFlyspace(holder)) {
15676
- throw new Error(
15677
- `Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
15678
- );
15679
- }
15680
- rmSync7(path, { force: true });
15681
- }
15682
- }
15683
- throw new Error(`Could not take the Host lock at ${path}`);
15684
- }
15685
- function publishHostEndpoint(port) {
15686
- const path = hostLockPath();
15687
- if (readLockPid(path) !== process.pid) return null;
15688
- const token = randomBytes5(32).toString("hex");
15689
- const lock = JSON.parse(readFileSync15(path, "utf8"));
15690
- writeFileSync10(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
15691
- return token;
15692
- }
15693
- function readHostEndpoint() {
15694
- try {
15695
- const { pid, port, token } = JSON.parse(readFileSync15(hostLockPath(), "utf8"));
15696
- if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
15697
- return { port, token };
15698
- } catch {
15699
- return null;
15700
- }
15701
- }
15702
-
15703
15721
  // src/store/device-store.ts
15704
15722
  import { createHash as createHash11, createPublicKey as createPublicKey3, randomBytes as randomBytes6, randomInt } from "crypto";
15705
15723
  var DEVICE_CODE_TTL_MS = 10 * 6e4;
@@ -15932,7 +15950,7 @@ async function userEmail(userId) {
15932
15950
 
15933
15951
  // src/dashboard/doc-share-publisher.ts
15934
15952
  import { randomBytes as randomBytes7 } from "crypto";
15935
- import { readFileSync as readFileSync16, statSync as statSync9 } from "fs";
15953
+ import { readFileSync as readFileSync17, statSync as statSync9 } from "fs";
15936
15954
  import { extname as extname2, posix } from "path";
15937
15955
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
15938
15956
 
@@ -16119,7 +16137,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16119
16137
  }
16120
16138
  if (!stat.isFile()) return { status: "not-found" };
16121
16139
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
16122
- const source = readFileSync16(abs, "utf8");
16140
+ const source = readFileSync17(abs, "utf8");
16123
16141
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
16124
16142
  if (kind === "excalidraw") {
16125
16143
  try {
@@ -16199,7 +16217,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16199
16217
  let text = null;
16200
16218
  try {
16201
16219
  const size = statSync9(docAbs).size;
16202
- if (size <= budget) text = readFileSync16(docAbs, "utf8");
16220
+ if (size <= budget) text = readFileSync17(docAbs, "utf8");
16203
16221
  } catch {
16204
16222
  continue;
16205
16223
  }
@@ -16216,7 +16234,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16216
16234
  let body;
16217
16235
  try {
16218
16236
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
16219
- body = readFileSync16(resolved2.abs);
16237
+ body = readFileSync17(resolved2.abs);
16220
16238
  } catch {
16221
16239
  continue;
16222
16240
  }
@@ -16258,7 +16276,7 @@ async function publishDocShare(input2) {
16258
16276
  ...HTML_FEATURES,
16259
16277
  brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
16260
16278
  } : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
16261
- const snapshot3 = {
16279
+ const snapshot2 = {
16262
16280
  ...built,
16263
16281
  sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
16264
16282
  };
@@ -16268,8 +16286,8 @@ async function publishDocShare(input2) {
16268
16286
  const renamed = await setDocShareSlug(existing.id, slug);
16269
16287
  if (!renamed) return { status: "not-found" };
16270
16288
  }
16271
- const share2 = await updateDocShareSnapshot(existing.id, snapshot3, features);
16272
- const stale = existing.assetKeys.filter((key) => !snapshot3.assetKeys.includes(key));
16289
+ const share2 = await updateDocShareSnapshot(existing.id, snapshot2, features);
16290
+ const stale = existing.assetKeys.filter((key) => !snapshot2.assetKeys.includes(key));
16273
16291
  await deleteShareAssets(stale);
16274
16292
  if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
16275
16293
  void queueShareAudio(share2);
@@ -16283,7 +16301,7 @@ async function publishDocShare(input2) {
16283
16301
  channelId: input2.channelId,
16284
16302
  sourcePath: input2.sourcePath,
16285
16303
  createdBy: input2.createdBy,
16286
- ...snapshot3,
16304
+ ...snapshot2,
16287
16305
  ...features
16288
16306
  });
16289
16307
  void queueShareAudio(share);
@@ -16310,9 +16328,12 @@ var METHODS = {
16310
16328
  thread_replies: ["GET", "HEAD", "POST", "PATCH"],
16311
16329
  memberships: ["GET", "HEAD"],
16312
16330
  channel_members: ["GET", "HEAD"],
16313
- channel_docs_access: ["GET", "HEAD"]
16331
+ channel_docs_access: ["GET", "HEAD"],
16332
+ memories: ALL,
16333
+ memory_events: ["GET", "HEAD", "POST", "PATCH"]
16314
16334
  };
16315
16335
  var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members", "channel_docs_access"]);
16336
+ var CHANNEL_OWNED = /* @__PURE__ */ new Set(["memories", "memory_events"]);
16316
16337
  var FORWARDED_HEADERS2 = ["accept", "content-type", "prefer", "range"];
16317
16338
  var deny = (message, status = 403) => ({
16318
16339
  status,
@@ -16428,6 +16449,16 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
16428
16449
  if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
16429
16450
  if (CHANNEL_SCOPED.has(table)) {
16430
16451
  params.append("channel_id", inList(channelIds));
16452
+ } else if (CHANNEL_OWNED.has(table)) {
16453
+ if (method === "POST") {
16454
+ const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
16455
+ if (!ok3) return deny(`Every ${table} row must belong to a channel of the device workspaces.`);
16456
+ }
16457
+ if (method === "PATCH") {
16458
+ const patch = bodyRows(req.body);
16459
+ if (!patch || patch.some((r) => "channel_id" in r)) return deny(`A ${table} row cannot move channels.`);
16460
+ }
16461
+ params.append("channel_id", inList(channelIds));
16431
16462
  } else if (table === "threads") {
16432
16463
  if (method === "POST") {
16433
16464
  const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
@@ -16490,98 +16521,6 @@ var forwardToSupabase = async (req) => {
16490
16521
  };
16491
16522
  };
16492
16523
 
16493
- // src/dashboard/host-db-client.ts
16494
- import { createClient as createClient2 } from "@supabase/supabase-js";
16495
- import { randomUUID as randomUUID3 } from "crypto";
16496
- var REQUEST_TIMEOUT_MS3 = 3e4;
16497
- var pending = /* @__PURE__ */ new Map();
16498
- var sender = null;
16499
- var onSender = [];
16500
- function setHostDbSender(fn) {
16501
- sender = fn;
16502
- if (!fn) return;
16503
- const waiting = onSender;
16504
- onSender = [];
16505
- for (const wake of waiting) wake();
16506
- }
16507
- function handleHostDbResult(id, res) {
16508
- const p = pending.get(id);
16509
- if (!p) return;
16510
- pending.delete(id);
16511
- clearTimeout(p.timer);
16512
- p.resolve(res);
16513
- }
16514
- function whenConnected() {
16515
- if (sender) return Promise.resolve(sender);
16516
- return new Promise((resolve7, reject) => {
16517
- const timer2 = setTimeout(() => {
16518
- onSender = onSender.filter((w) => w !== wake);
16519
- reject(new Error("Host is not connected to the Data API"));
16520
- }, REQUEST_TIMEOUT_MS3);
16521
- const wake = () => {
16522
- clearTimeout(timer2);
16523
- resolve7(sender);
16524
- };
16525
- onSender.push(wake);
16526
- });
16527
- }
16528
- async function request(req) {
16529
- const send2 = await whenConnected();
16530
- const id = randomUUID3();
16531
- return new Promise((resolve7, reject) => {
16532
- const timer2 = setTimeout(() => {
16533
- pending.delete(id);
16534
- reject(new Error("Data API did not answer in time"));
16535
- }, REQUEST_TIMEOUT_MS3);
16536
- pending.set(id, { resolve: resolve7, reject, timer: timer2 });
16537
- send2({ type: "db", id, req });
16538
- });
16539
- }
16540
- var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
16541
- var hostDbFetch = async (input2, init) => {
16542
- const req = new Request(input2, init);
16543
- const url = new URL(req.url);
16544
- const method = req.method.toUpperCase();
16545
- const res = await request({
16546
- method,
16547
- path: `${url.pathname}${url.search}`,
16548
- headers: Object.fromEntries(req.headers.entries()),
16549
- body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
16550
- });
16551
- const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
16552
- return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
16553
- };
16554
- function relayChannel(topic) {
16555
- const channel = {
16556
- on: () => channel,
16557
- subscribe: (callback) => {
16558
- queueMicrotask(() => callback?.("SUBSCRIBED"));
16559
- return channel;
16560
- },
16561
- send: async (msg) => {
16562
- if (msg.type !== "broadcast" || !sender) return "error";
16563
- sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
16564
- return "ok";
16565
- },
16566
- unsubscribe: async () => "ok"
16567
- };
16568
- return channel;
16569
- }
16570
- function createHostDbClient() {
16571
- const client2 = createClient2("https://host-db.invalid", "host-device", {
16572
- auth: { persistSession: false, autoRefreshToken: false },
16573
- global: { fetch: hostDbFetch }
16574
- });
16575
- return new Proxy(client2, {
16576
- get(target, prop) {
16577
- if (prop === "channel") return relayChannel;
16578
- if (prop === "removeChannel") return async () => "ok";
16579
- const value = Reflect.get(target, prop, target);
16580
- return typeof value === "function" ? value.bind(target) : value;
16581
- }
16582
- });
16583
- }
16584
-
16585
16524
  // src/dashboard/agent-bridge.ts
16586
16525
  var HOST_SOCKET_PATH = "/internal/host-socket";
16587
16526
  var DEFAULT_DATA_API_PORT = 4757;
@@ -16609,6 +16548,7 @@ function localAgentCallbacks(chatAgent) {
16609
16548
  cancelAgentRun: (threadId) => chatAgent.cancel(threadId),
16610
16549
  regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
16611
16550
  listAgentRuns: () => chatAgent.runningThreadIds(),
16551
+ completeMemoryExtraction: (req) => chatAgent.completeMemoryExtraction(req),
16612
16552
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
16613
16553
  listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
16614
16554
  getAgentIdentities: () => resolveAgentIdentities(),
@@ -16628,14 +16568,14 @@ function localAgentCallbacks(chatAgent) {
16628
16568
  }
16629
16569
  var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
16630
16570
  function buildHostConfigSnapshot() {
16631
- const snapshot3 = {};
16571
+ const snapshot2 = {};
16632
16572
  for (const profile of listProfiles()) {
16633
- snapshot3[profile] = runWithProfile(profile, () => ({
16634
- env: existsSync24(paths.CONFIG_FILE) ? dotenv.parse(readFileSync17(paths.CONFIG_FILE, "utf-8")) : {},
16573
+ snapshot2[profile] = runWithProfile(profile, () => ({
16574
+ env: existsSync24(paths.CONFIG_FILE) ? dotenv.parse(readFileSync18(paths.CONFIG_FILE, "utf-8")) : {},
16635
16575
  agentModel: loadDashboardSettings().agent
16636
16576
  }));
16637
16577
  }
16638
- return snapshot3;
16578
+ return snapshot2;
16639
16579
  }
16640
16580
  function startLocalProfileConfigHydration() {
16641
16581
  setProfileConfigCache(buildHostConfigSnapshot());
@@ -16644,6 +16584,7 @@ function startLocalProfileConfigHydration() {
16644
16584
  }
16645
16585
  var CALL_TIMEOUT_MS = 15e3;
16646
16586
  var DOCS_CALL_TIMEOUT_MS = 12e4;
16587
+ var MEMORY_EXTRACTION_TIMEOUT_MS = 165e3;
16647
16588
  var AUTH_TIMEOUT_MS = 5e3;
16648
16589
  var HEARTBEAT_INTERVAL_MS = 15e3;
16649
16590
  var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
@@ -16842,6 +16783,7 @@ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
16842
16783
  );
16843
16784
  return Promise.resolve(null);
16844
16785
  }
16786
+ if (method === "docsRequest") console.log(`[host-socket] docsRequest for "${profile}" -> device ${hosts.get(ws)?.deviceId ?? "legacy"}`);
16845
16787
  const id = randomUUID4();
16846
16788
  return new Promise((resolve7) => {
16847
16789
  const timer2 = setTimeout(() => {
@@ -16870,6 +16812,9 @@ function remoteAgentCallbacks() {
16870
16812
  cancelAgentRun: async (threadId) => await callHost("cancelAgentRun", [threadId]) ?? false,
16871
16813
  regenerateAgentRun: async (threadId) => await callHost("regenerateAgentRun", [threadId]) ?? false,
16872
16814
  listAgentRuns: async () => await callHost("listAgentRuns", []) ?? [],
16815
+ // `null` (no Host, or no answer in time) passes through: the extractor
16816
+ // reads it as a failed call and retries the thread next sweep.
16817
+ completeMemoryExtraction: (req) => callHost("completeMemoryExtraction", [req], MEMORY_EXTRACTION_TIMEOUT_MS),
16873
16818
  // Deliberately passes `null` through rather than defaulting to `[]`: with
16874
16819
  // no Host connected, claiming every folder is fine would be a guess.
16875
16820
  listMissingCwds: () => callHost("listMissingCwds", []),
@@ -17066,12 +17011,13 @@ function relayedAgentCallbacks(local) {
17066
17011
  ...Object.fromEntries(RELAYED_METHODS.map((method) => [method, relay3(method)]))
17067
17012
  };
17068
17013
  }
17069
- async function handleLocalRelay(req, callbacks, token) {
17014
+ function relayAuthorized(req, token) {
17070
17015
  const given = Buffer.from(req.headers.get("authorization") ?? "");
17071
17016
  const expected = Buffer.from(`Bearer ${token}`);
17072
- if (!token || given.length !== expected.length || !timingSafeEqual2(given, expected)) {
17073
- return Response.json({ error: "unauthorized" }, { status: 401 });
17074
- }
17017
+ return !!token && given.length === expected.length && timingSafeEqual2(given, expected);
17018
+ }
17019
+ async function handleLocalRelay(req, callbacks, token) {
17020
+ if (!relayAuthorized(req, token)) return Response.json({ error: "unauthorized" }, { status: 401 });
17075
17021
  const body = await req.json().catch(() => null);
17076
17022
  const method = RELAYED_METHODS.find((m) => m === body?.method);
17077
17023
  if (!body || !method || typeof body.profile !== "string" || !Array.isArray(body.args)) {
@@ -17354,6 +17300,7 @@ function createApiRouter(agent = {}, mode = "all") {
17354
17300
  const resolved2 = requested && known.includes(requested) ? requested : profileFromHost(trustedHost(c), known);
17355
17301
  if (!resolved2) {
17356
17302
  return c.json({
17303
+ unknownWorkspace: isUnknownWorkspaceHost(trustedHost(c), known),
17357
17304
  theme: BRANDING_DEFAULTS.theme,
17358
17305
  font: BRANDING_DEFAULTS.font,
17359
17306
  displayName: "Flyspace",
@@ -17674,7 +17621,7 @@ function startHostStatusBroadcast() {
17674
17621
 
17675
17622
  // src/dashboard/server.ts
17676
17623
  var DEFAULT_DASHBOARD_PORT = 4747;
17677
- var WEB_DIST = join23(paths.PACKAGE_ROOT, "web", "dist");
17624
+ var WEB_DIST = join24(paths.PACKAGE_ROOT, "web", "dist");
17678
17625
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
17679
17626
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
17680
17627
  for (let port = start; port < start + attempts; port++) {
@@ -17724,6 +17671,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17724
17671
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
17725
17672
  };
17726
17673
  const app = new Hono34();
17674
+ app.onError(reportingErrorHandler);
17727
17675
  app.use("*", async (c, next) => {
17728
17676
  await next();
17729
17677
  if (c.req.path.startsWith("/assets/")) {
@@ -17738,6 +17686,20 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17738
17686
  let relayToken = null;
17739
17687
  if (mode === "host") {
17740
17688
  app.post(LOCAL_RELAY_PATH, (c) => handleLocalRelay(c.req.raw, agent, relayToken));
17689
+ if (earlyHostSocket) {
17690
+ app.post(LOCAL_DB_PATH, async (c) => {
17691
+ if (!relayAuthorized(c.req.raw, relayToken)) return c.json({ error: "unauthorized" }, 401);
17692
+ const req = await c.req.json().catch(() => null);
17693
+ if (!req || typeof req.method !== "string" || typeof req.path !== "string") {
17694
+ return c.json({ error: "bad request" }, 400);
17695
+ }
17696
+ try {
17697
+ return c.json(await requestHostDb(req));
17698
+ } catch (err) {
17699
+ return c.json({ error: err.message }, 503);
17700
+ }
17701
+ });
17702
+ }
17741
17703
  }
17742
17704
  app.route("/stripe", createStripeWebhookRouter());
17743
17705
  if (!servesHost) {
@@ -17748,14 +17710,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17748
17710
  } else {
17749
17711
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
17750
17712
  app.route("/", createAppIconRouter());
17751
- if (existsSync25(WEB_DIST) || existsSync25(join23(paths.PACKAGE_ROOT, "web", "package.json"))) {
17713
+ if (existsSync25(WEB_DIST) || existsSync25(join24(paths.PACKAGE_ROOT, "web", "package.json"))) {
17752
17714
  app.use("/*", serveStatic({ root: WEB_DIST }));
17753
17715
  }
17754
17716
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
17755
- const spaFallback = serveStatic({ path: join23(WEB_DIST, "index.html") });
17717
+ const spaFallback = serveStatic({ path: join24(WEB_DIST, "index.html") });
17756
17718
  app.get("*", async (c, next) => {
17757
- if (!existsSync25(join23(WEB_DIST, "index.html"))) {
17758
- const isCheckout = existsSync25(join23(paths.PACKAGE_ROOT, "web", "package.json"));
17719
+ if (!existsSync25(join24(WEB_DIST, "index.html"))) {
17720
+ const isCheckout = existsSync25(join24(paths.PACKAGE_ROOT, "web", "package.json"));
17759
17721
  return c.text(
17760
17722
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
17761
17723
  Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
@@ -18193,7 +18155,7 @@ async function threadReplies(client2, nameOrId, ts2) {
18193
18155
  async function postMessage(client2, args) {
18194
18156
  const channelArg = getArg2(args, "--channel");
18195
18157
  const textFilePath = getArg2(args, "--text-file");
18196
- const text = textFilePath ? readFileSync18(textFilePath, "utf8") : getArg2(args, "--text");
18158
+ const text = textFilePath ? readFileSync19(textFilePath, "utf8") : getArg2(args, "--text");
18197
18159
  const threadTs = getArg2(args, "--thread");
18198
18160
  const markdown = args.includes("--markdown");
18199
18161
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -18348,7 +18310,7 @@ async function ensureClaudeCodeReady() {
18348
18310
  }
18349
18311
  function getManifestYaml(botName) {
18350
18312
  const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
18351
- const raw = readFileSync18(manifestPath, "utf-8");
18313
+ const raw = readFileSync19(manifestPath, "utf-8");
18352
18314
  return raw.replace(/name: Custie/g, `name: ${botName}`);
18353
18315
  }
18354
18316
  async function stepPersonalise() {
@@ -18561,7 +18523,7 @@ async function stepBrowserTokens(botName) {
18561
18523
  })
18562
18524
  ));
18563
18525
  const DEBUG = process.argv.includes("--debug");
18564
- const USER_DATA_DIR = resolve6(tmpdir4(), "custie-playwright-profile");
18526
+ const USER_DATA_DIR = resolve6(tmpdir5(), "custie-playwright-profile");
18565
18527
  let context;
18566
18528
  try {
18567
18529
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -18780,7 +18742,7 @@ function copyDefaultPrompt(botName) {
18780
18742
  if (!existsSync26(paths.PROMPT_FILE)) {
18781
18743
  const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
18782
18744
  if (existsSync26(defaultPrompt)) {
18783
- const content = readFileSync18(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18745
+ const content = readFileSync19(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18784
18746
  writeFileSync11(paths.PROMPT_FILE, content);
18785
18747
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
18786
18748
  }
@@ -19075,6 +19037,7 @@ async function runPost(args) {
19075
19037
  }
19076
19038
  async function runChatCmd(args) {
19077
19039
  loadEnvFiles();
19040
+ useHostDbWithoutCredentials();
19078
19041
  const subcommand = args[0];
19079
19042
  const rest = args.slice(1);
19080
19043
  switch (subcommand) {
@@ -19103,21 +19066,21 @@ async function runChatCmd(args) {
19103
19066
  }
19104
19067
 
19105
19068
  // src/commands/memory.ts
19106
- import { readFileSync as readFileSync20 } from "fs";
19069
+ import { readFileSync as readFileSync21 } from "fs";
19107
19070
 
19108
19071
  // src/store/claude-memory-import.ts
19109
- import { existsSync as existsSync27, readdirSync as readdirSync5, readFileSync as readFileSync19 } from "fs";
19072
+ import { existsSync as existsSync27, readdirSync as readdirSync5, readFileSync as readFileSync20 } from "fs";
19110
19073
  import { homedir as homedir10 } from "os";
19111
- import { basename as basename8, join as join24 } from "path";
19074
+ import { basename as basename8, join as join25 } from "path";
19112
19075
  import dotenv2 from "dotenv";
19113
19076
  function claudeMemoryDir(cwd, configDir2) {
19114
- return join24(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
19077
+ return join25(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
19115
19078
  }
19116
19079
  function profileClaudeConfigDirFor(profile) {
19117
- const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join24(paths.PROFILES_DIR, profile);
19118
- const file = join24(dir, "config.env");
19119
- const env = existsSync27(file) ? dotenv2.parse(readFileSync19(file)) : {};
19120
- return env["CLAUDE_CONFIG_DIR"]?.trim() || join24(homedir10(), ".claude");
19080
+ const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join25(paths.PROFILES_DIR, profile);
19081
+ const file = join25(dir, "config.env");
19082
+ const env = existsSync27(file) ? dotenv2.parse(readFileSync20(file)) : {};
19083
+ return env["CLAUDE_CONFIG_DIR"]?.trim() || join25(homedir10(), ".claude");
19121
19084
  }
19122
19085
  var TYPE_MAP = {
19123
19086
  user: "preference",
@@ -19125,7 +19088,7 @@ var TYPE_MAP = {
19125
19088
  project: "fact",
19126
19089
  reference: "reference"
19127
19090
  };
19128
- var DESCRIPTION_MAX2 = 200;
19091
+ var DESCRIPTION_MAX = 200;
19129
19092
  function unquote2(value) {
19130
19093
  const v = value.trim();
19131
19094
  return /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
@@ -19143,8 +19106,8 @@ function parseClaudeMemory(fileName, text, channelId) {
19143
19106
  }
19144
19107
  const name = slugify(fields["name"] || basename8(fileName, ".md"));
19145
19108
  let description = (fields["description"] ?? "").replaceAll(/\s+/g, " ").trim();
19146
- if (description.length > DESCRIPTION_MAX2) {
19147
- description = `${description.slice(0, DESCRIPTION_MAX2 - 3).trimEnd()}...`;
19109
+ if (description.length > DESCRIPTION_MAX) {
19110
+ description = `${description.slice(0, DESCRIPTION_MAX - 3).trimEnd()}...`;
19148
19111
  }
19149
19112
  const input2 = {
19150
19113
  channelId,
@@ -19166,7 +19129,7 @@ async function importClaudeMemories(dir, channelId, opts = {}) {
19166
19129
  const files = readdirSync5(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
19167
19130
  const results = [];
19168
19131
  for (const file of files) {
19169
- const parsed = parseClaudeMemory(file, readFileSync19(join24(dir, file), "utf-8"), channelId);
19132
+ const parsed = parseClaudeMemory(file, readFileSync20(join25(dir, file), "utf-8"), channelId);
19170
19133
  if ("skip" in parsed) {
19171
19134
  results.push({ file, outcome: "skipped", reason: parsed.skip });
19172
19135
  continue;
@@ -19220,6 +19183,7 @@ function indexLine(m) {
19220
19183
  }
19221
19184
  async function runMemoryCmd(args) {
19222
19185
  loadEnvFiles();
19186
+ useHostDbWithoutCredentials();
19223
19187
  const subcommand = args[0];
19224
19188
  const rest = args.slice(1);
19225
19189
  if (!subcommand || subcommand === "-h" || subcommand === "--help") {
@@ -19265,7 +19229,7 @@ ${memory.body}`);
19265
19229
  );
19266
19230
  }
19267
19231
  if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
19268
- const body = bodyArg === "-" ? readFileSync20(0, "utf-8") : bodyArg;
19232
+ const body = bodyArg === "-" ? readFileSync21(0, "utf-8") : bodyArg;
19269
19233
  const thread = getArg4(rest, "--thread");
19270
19234
  const { created, changed } = await saveChannelMemory(
19271
19235
  {
@@ -19349,10 +19313,10 @@ async function runMcpCmd(args) {
19349
19313
  }
19350
19314
 
19351
19315
  // src/commands/dashboard.ts
19352
- import { spawn as spawn5, spawnSync } from "child_process";
19316
+ import { spawn as spawn6, spawnSync } from "child_process";
19353
19317
  import { existsSync as existsSync28 } from "fs";
19354
19318
  import { platform as platform7 } from "os";
19355
- import { join as join25 } from "path";
19319
+ import { join as join26 } from "path";
19356
19320
  function flagValue(args, name) {
19357
19321
  const eq = args.find((a) => a.startsWith(`--${name}=`));
19358
19322
  if (eq) return eq.slice(`--${name}=`.length);
@@ -19362,13 +19326,13 @@ function flagValue(args, name) {
19362
19326
  function openBrowser2(url) {
19363
19327
  const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
19364
19328
  try {
19365
- spawn5(cmd, [url], { stdio: "ignore", detached: true }).unref();
19329
+ spawn6(cmd, [url], { stdio: "ignore", detached: true }).unref();
19366
19330
  } catch {
19367
19331
  }
19368
19332
  }
19369
19333
  function ensureWebBuild() {
19370
- if (existsSync28(join25(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19371
- if (!existsSync28(join25(paths.PACKAGE_ROOT, "web", "package.json"))) return;
19334
+ if (existsSync28(join26(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19335
+ if (!existsSync28(join26(paths.PACKAGE_ROOT, "web", "package.json"))) return;
19372
19336
  console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
19373
19337
  const result = spawnSync("pnpm", ["--dir", "web", "build"], {
19374
19338
  cwd: paths.PACKAGE_ROOT,
@@ -19731,7 +19695,7 @@ async function main() {
19731
19695
  break;
19732
19696
  case "-v":
19733
19697
  case "--version":
19734
- printRaw("0.1.13");
19698
+ printRaw("0.1.15");
19735
19699
  break;
19736
19700
  case "-h":
19737
19701
  case "--help":