flyspace-cli 0.1.14 → 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,
@@ -75,11 +82,17 @@ import {
75
82
  isCodexModel,
76
83
  isCwdUsable,
77
84
  isEffort,
85
+ isFeatureFlagId,
86
+ isFeatureVisible,
78
87
  isLockedPlan,
79
88
  isLogicalCwd,
89
+ isMemoryType,
90
+ isUuid,
80
91
  isValidSkillName,
81
92
  lastRunsForWorkspace,
82
93
  listActiveUsers,
94
+ listChannelMemories,
95
+ listEnabledFeatureFlags,
83
96
  listMemberChannels,
84
97
  listMemberships,
85
98
  listMembershipsByEmail,
@@ -87,9 +100,11 @@ import {
87
100
  listPluginSkillDetails,
88
101
  listSkillFiles,
89
102
  listSkills,
103
+ listThreadMemoryEvents,
90
104
  listUsersWithAvatars,
91
105
  listWorkspaceSummaries,
92
106
  listWorkspaces,
107
+ loadAgentMemory,
93
108
  loadAutomations,
94
109
  loadAutomationsFile,
95
110
  loadBotIdentity,
@@ -122,6 +137,7 @@ import {
122
137
  resolveChannelId,
123
138
  resolveChannelPrompt,
124
139
  resolveCwd,
140
+ resolveFeatureFlags,
125
141
  resolveLogicalCwd,
126
142
  resolveSelectedOutputStyle,
127
143
  resolveUserId,
@@ -130,9 +146,11 @@ import {
130
146
  runnerForModel,
131
147
  saveAgentProfile,
132
148
  saveBotIdentity,
149
+ saveChannelMemory,
133
150
  saveDashboardSettings,
134
151
  saveWorkspaceBranding,
135
152
  saveWorkspacePrompt,
153
+ searchChannelMemories,
136
154
  setAgentPaused,
137
155
  setAllowGuestAccess,
138
156
  setMembershipHandle,
@@ -145,17 +163,20 @@ import {
145
163
  syncSlackAvatars,
146
164
  transferWorkspaceAdmin,
147
165
  tsToIso,
166
+ undoMemoryEvent,
148
167
  updateChannelById,
168
+ updateChannelMemory,
149
169
  updateChannelsMcpConnections,
150
170
  updateMembershipRole,
151
171
  updateOutputStyle,
152
172
  updateWorkspacePlanStatus,
153
173
  usageLimitedModels,
154
174
  usageLimitedUntil,
175
+ validateMemoryInput,
155
176
  warnOnBrokenChannelCwds,
156
177
  workspaceSkillsDir,
157
178
  writeSkillFile
158
- } from "./chunk-DA7SYYKU.js";
179
+ } from "./chunk-KFT2YP5W.js";
159
180
  import {
160
181
  firstReplyBody,
161
182
  getThread,
@@ -614,8 +635,8 @@ async function requestHostDb(req) {
614
635
  }
615
636
  var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
616
637
  function dbFetch(send2) {
617
- return async (input2, init) => {
618
- const req = new Request(input2, init);
638
+ return async (input2, init2) => {
639
+ const req = new Request(input2, init2);
619
640
  const url = new URL(req.url);
620
641
  const method = req.method.toUpperCase();
621
642
  const res = await send2({
@@ -678,11 +699,17 @@ function createHostDbClient(via = "socket") {
678
699
  });
679
700
  }
680
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
+
681
709
  // src/commands/start.ts
682
710
  async function runStart() {
683
711
  loadEnvFiles();
684
- const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
685
- if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
712
+ const viaHost = useHostDbWithoutCredentials();
686
713
  await preflight({ supabaseViaHost: viaHost });
687
714
  await startServer();
688
715
  }
@@ -1738,7 +1765,7 @@ async function runAuthCmd(args) {
1738
1765
  }
1739
1766
 
1740
1767
  // src/version.ts
1741
- var CLI_VERSION = true ? "0.1.14" : null;
1768
+ var CLI_VERSION = true ? "0.1.15" : null;
1742
1769
  function isOlderVersion(a, b) {
1743
1770
  const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1744
1771
  const [pa, pb] = [parse2(a), parse2(b)];
@@ -1893,9 +1920,9 @@ import { tmpdir } from "os";
1893
1920
  import { promisify } from "util";
1894
1921
  var execAsync = promisify(execFile);
1895
1922
  var exec = (cmd, args, opts) => {
1896
- const run3 = execAsync(cmd, args, opts);
1897
- run3.child.stdin?.end();
1898
- return run3;
1923
+ const run4 = execAsync(cmd, args, opts);
1924
+ run4.child.stdin?.end();
1925
+ return run4;
1899
1926
  };
1900
1927
  var BOLD3 = "\x1B[1m";
1901
1928
  var DIM3 = "\x1B[2m";
@@ -2230,7 +2257,7 @@ function npmEnv() {
2230
2257
  return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
2231
2258
  }
2232
2259
  async function runUpgrade() {
2233
- const current = "0.1.14";
2260
+ const current = "0.1.15";
2234
2261
  console.log(`Current version: ${current}`);
2235
2262
  console.log("Checking for updates...\n");
2236
2263
  try {
@@ -2550,6 +2577,7 @@ async function handleImport(manager) {
2550
2577
  async function runAutomationCmd(args) {
2551
2578
  const subcommand = args[0];
2552
2579
  loadEnvFiles();
2580
+ useHostDbWithoutCredentials();
2553
2581
  await refreshChannelRegistry();
2554
2582
  await refreshAutomationRegistry();
2555
2583
  const manager = new AutomationManager();
@@ -2870,9 +2898,9 @@ async function runLogs(args) {
2870
2898
 
2871
2899
  // src/commands/slack.ts
2872
2900
  import { execFileSync as execFileSync4 } from "child_process";
2873
- 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";
2874
2902
  import { resolve as resolve6 } from "path";
2875
- import { tmpdir as tmpdir4, homedir as homedir9 } from "os";
2903
+ import { tmpdir as tmpdir5, homedir as homedir9 } from "os";
2876
2904
  import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
2877
2905
  import { WebClient as WebClient3 } from "@slack/web-api";
2878
2906
 
@@ -3427,6 +3455,13 @@ function profileFromHost(host, known) {
3427
3455
  const bare = label2.startsWith("api-") ? label2.slice(4) : label2.startsWith("dev-") ? label2.slice(4) : label2;
3428
3456
  return known.includes(bare) ? bare : null;
3429
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
+ }
3430
3465
  function workspaceHostname(host, profile) {
3431
3466
  const name = bareHost(host);
3432
3467
  if (!name || !subdomainLabel(host)) return null;
@@ -3837,7 +3872,9 @@ function getStripeClient() {
3837
3872
 
3838
3873
  // src/store/stripe-trial.ts
3839
3874
  var PLAN_INFO = {
3840
- 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 },
3841
3878
  cloud: { name: "Cloud Agent", priceUsd: 49 }
3842
3879
  };
3843
3880
  function priceIdForPlan(plan) {
@@ -3862,6 +3899,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
3862
3899
  metadata: { workspace_id: workspace.id, plan }
3863
3900
  });
3864
3901
  await setWorkspaceStripeSubscription(workspace.id, {
3902
+ plan,
3865
3903
  stripeCustomerId: customer.id,
3866
3904
  stripeSubscriptionId: subscription.id,
3867
3905
  planStatus: "trialing"
@@ -4400,8 +4438,20 @@ async function deliverPasswordResetLink(email, redirectTo) {
4400
4438
  // src/dashboard/server.ts
4401
4439
  import { existsSync as existsSync25 } from "fs";
4402
4440
  import { createServer } from "net";
4403
- import { join as join23 } from "path";
4441
+ import { join as join24 } from "path";
4404
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
4405
4455
  import { serve } from "@hono/node-server";
4406
4456
  import { serveStatic } from "@hono/node-server/serve-static";
4407
4457
 
@@ -4747,15 +4797,15 @@ async function createDocShare(input2) {
4747
4797
  if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
4748
4798
  return rowToShare(data);
4749
4799
  }
4750
- async function updateDocShareSnapshot(id, snapshot3, features) {
4800
+ async function updateDocShareSnapshot(id, snapshot2, features) {
4751
4801
  const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
4752
- title: snapshot3.title,
4753
- content: snapshot3.content,
4754
- asset_keys: snapshot3.assetKeys,
4802
+ title: snapshot2.title,
4803
+ content: snapshot2.content,
4804
+ asset_keys: snapshot2.assetKeys,
4755
4805
  // Re-detected with the snapshot: a re-publish can carry a rewritten
4756
4806
  // article, and one written in another language than last time is exactly
4757
4807
  // the case a stale value would get wrong.
4758
- source_lang: snapshot3.sourceLang,
4808
+ source_lang: snapshot2.sourceLang,
4759
4809
  audio_enabled: features.audioEnabled,
4760
4810
  ask_enabled: features.askEnabled,
4761
4811
  social_enabled: features.socialEnabled,
@@ -5683,9 +5733,9 @@ async function synthesizeWithRetry(script, voice) {
5683
5733
  }
5684
5734
  var renderQueue = Promise.resolve();
5685
5735
  function queued(job) {
5686
- const run3 = renderQueue.then(job, job);
5687
- renderQueue = run3.catch(() => void 0);
5688
- return run3;
5736
+ const run4 = renderQueue.then(job, job);
5737
+ renderQueue = run4.catch(() => void 0);
5738
+ return run4;
5689
5739
  }
5690
5740
  function renderArticleAudio(title, content, lang) {
5691
5741
  return queued(() => renderArticleAudioNow(title, content, lang));
@@ -5759,12 +5809,12 @@ function queueShareAudio(share) {
5759
5809
  }
5760
5810
  const current = inFlight2.get(share.id);
5761
5811
  if (current?.hash === hash) return current.done;
5762
- const run3 = () => runShareAudioJob(share, hash).catch((err) => {
5812
+ const run4 = () => runShareAudioJob(share, hash).catch((err) => {
5763
5813
  console.error(`share audio job crashed (${share.id}):`, err);
5764
5814
  }).finally(() => {
5765
5815
  if (inFlight2.get(share.id)?.hash === hash) inFlight2.delete(share.id);
5766
5816
  });
5767
- const done = current ? current.done.then(run3) : run3();
5817
+ const done = current ? current.done.then(run4) : run4();
5768
5818
  inFlight2.set(share.id, { hash, done });
5769
5819
  return done;
5770
5820
  }
@@ -6689,8 +6739,8 @@ function routeCookieWrites(all, label2, writes) {
6689
6739
 
6690
6740
  // src/dashboard/supabase-server-client.ts
6691
6741
  var REQUEST_TIMEOUT_MS2 = 5e3;
6692
- function fetchWithTimeout(input2, init) {
6693
- return fetch(input2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
6742
+ function fetchWithTimeout(input2, init2) {
6743
+ return fetch(input2, { ...init2, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
6694
6744
  }
6695
6745
  function createRequestSupabaseClient(c) {
6696
6746
  const url = process.env["SUPABASE_URL"];
@@ -6732,7 +6782,7 @@ function createRequestSupabaseClient(c) {
6732
6782
  }
6733
6783
 
6734
6784
  // src/dashboard/auth-context.ts
6735
- import { HTTPException } from "hono/http-exception";
6785
+ import { HTTPException as HTTPException2 } from "hono/http-exception";
6736
6786
  import { isAuthApiError, isAuthRetryableFetchError } from "@supabase/supabase-js";
6737
6787
  function onboardingPending(ctx) {
6738
6788
  const pending3 = [];
@@ -6789,10 +6839,10 @@ async function resolveAuthContextUncached(c) {
6789
6839
  } = await getUserWithRetry(supabase));
6790
6840
  } catch (err) {
6791
6841
  if (isAuthRetryableFetchError(err)) {
6792
- throw new HTTPException(503, { message: "auth service unreachable" });
6842
+ throw new HTTPException2(503, { message: "auth service unreachable" });
6793
6843
  }
6794
6844
  if (isAuthApiError(err) && err.status === 429) {
6795
- throw new HTTPException(503, { message: "auth service rate limited" });
6845
+ throw new HTTPException2(503, { message: "auth service rate limited" });
6796
6846
  }
6797
6847
  return null;
6798
6848
  }
@@ -7201,8 +7251,8 @@ async function offlineAgentIdentities() {
7201
7251
  }
7202
7252
 
7203
7253
  // src/dashboard/host-api.ts
7204
- import { existsSync as existsSync20, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
7205
- import { join as join21 } from "path";
7254
+ import { existsSync as existsSync20, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
7255
+ import { join as join22 } from "path";
7206
7256
  import sharp3 from "sharp";
7207
7257
 
7208
7258
  // src/dashboard/channels-routes.ts
@@ -7211,81 +7261,6 @@ import { Hono as Hono9 } from "hono";
7211
7261
 
7212
7262
  // src/dashboard/feature-flags-routes.ts
7213
7263
  import { Hono as Hono8 } from "hono";
7214
-
7215
- // src/feature-flags.ts
7216
- var featureFlags = {
7217
- "ai-cost": {
7218
- title: "AI cost",
7219
- 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.",
7220
- stage: "beta"
7221
- },
7222
- "read-aloud": {
7223
- title: "Read aloud",
7224
- description: "Listen to messages, threads and docs as audio. Still rough in places, and each playback uses text-to-speech.",
7225
- stage: "beta"
7226
- },
7227
- excalidraw: {
7228
- title: "Excalidraw",
7229
- description: "Create and edit whiteboard drawings (.excalidraw files) in the Docs pane. Existing drawings stay viewable with this off.",
7230
- stage: "beta"
7231
- },
7232
- sandbox: {
7233
- title: "Sandbox runtime",
7234
- description: "Run a channel agent in an isolated sandbox without Docker. Channels already on Sandbox keep working with this off.",
7235
- stage: "beta"
7236
- },
7237
- dictation: {
7238
- title: "Dictation",
7239
- description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
7240
- stage: "dev"
7241
- },
7242
- "agent-memory": {
7243
- title: "Agent memory",
7244
- 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.",
7245
- stage: "beta"
7246
- }
7247
- };
7248
- var featureFlagIds = Object.keys(featureFlags);
7249
- function isFeatureFlagId(id) {
7250
- return Object.hasOwn(featureFlags, id);
7251
- }
7252
- function isFeatureVisible(stage, devHost) {
7253
- return stage === "beta" || stage === "dev" && devHost;
7254
- }
7255
- function resolveFeatureFlags(enabledIds, devHost) {
7256
- const enabled = new Set(enabledIds);
7257
- const out = {};
7258
- for (const id of featureFlagIds) {
7259
- const stage = featureFlags[id].stage;
7260
- out[id] = stage === "ga" || isFeatureVisible(stage, devHost) && enabled.has(id);
7261
- }
7262
- return out;
7263
- }
7264
-
7265
- // src/store/feature-flag-store.ts
7266
- async function listEnabledFeatureFlags(workspaceId) {
7267
- const { data, error: error2 } = await getSupabaseClient().from("feature_flags").select("flag_id").eq("workspace_id", workspaceId);
7268
- if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
7269
- return (data ?? []).map((r) => r.flag_id);
7270
- }
7271
- async function listEnabledFeatureFlagsBySlug(slug) {
7272
- const { data, error: error2 } = await getSupabaseClient().from("workspaces").select("id").eq("slug", slug).maybeSingle();
7273
- if (error2) throw new Error(`listEnabledFeatureFlagsBySlug(${slug}): ${error2.message}`);
7274
- return data ? listEnabledFeatureFlags(data.id) : [];
7275
- }
7276
- async function enableFeatureFlag(workspaceId, flagId, userId) {
7277
- const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
7278
- { workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
7279
- { onConflict: "workspace_id,flag_id" }
7280
- );
7281
- if (error2) throw new Error(`enableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
7282
- }
7283
- async function disableFeatureFlag(workspaceId, flagId) {
7284
- const { error: error2 } = await getSupabaseClient().from("feature_flags").delete().eq("workspace_id", workspaceId).eq("flag_id", flagId);
7285
- if (error2) throw new Error(`disableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
7286
- }
7287
-
7288
- // src/dashboard/feature-flags-routes.ts
7289
7264
  async function isFeatureEnabled(c, flagId) {
7290
7265
  const auth = c.get("auth");
7291
7266
  const enabled = await listEnabledFeatureFlags(auth.workspaceId);
@@ -7509,14 +7484,14 @@ import {
7509
7484
  openSync,
7510
7485
  readSync,
7511
7486
  readdirSync as readdirSync4,
7512
- readFileSync as readFileSync13,
7487
+ readFileSync as readFileSync14,
7513
7488
  realpathSync as realpathSync3,
7514
7489
  renameSync,
7515
- rmSync as rmSync7,
7490
+ rmSync as rmSync8,
7516
7491
  statSync as statSync5,
7517
7492
  writeFileSync as writeFileSync9
7518
7493
  } from "fs";
7519
- import { basename as basename6, dirname as dirname5, extname, join as join19, 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";
7520
7495
  import { Hono as Hono10 } from "hono";
7521
7496
  import { streamSSE } from "hono/streaming";
7522
7497
 
@@ -7800,7 +7775,7 @@ async function readOrQueueDocAudio(input2) {
7800
7775
  }
7801
7776
 
7802
7777
  // src/dashboard/artifact-export.ts
7803
- import { readFileSync as readFileSync12 } from "fs";
7778
+ import { readFileSync as readFileSync13 } from "fs";
7804
7779
 
7805
7780
  // src/dashboard/chat-agent.ts
7806
7781
  import { homedir as homedir7 } from "os";
@@ -8584,234 +8559,128 @@ function readAgentPersona(configDir2, cwd, name) {
8584
8559
  }
8585
8560
  }
8586
8561
 
8587
- // src/store/memory-store.ts
8588
- var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
8589
- var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8590
- var DESCRIPTION_MAX = 200;
8591
- var BODY_MAX = 4e3;
8592
- var MEMORY_INDEX_MAX_CHARS = 8e3;
8593
- function isMemoryType(value) {
8594
- return MEMORY_TYPES.includes(value);
8595
- }
8596
- function validateMemoryInput(input2) {
8597
- if (!NAME_RE.test(input2.name))
8598
- throw new Error("name must be a kebab-case slug, e.g. deploy-flow");
8599
- if (!isMemoryType(input2.type)) throw new Error(`type must be one of ${MEMORY_TYPES.join(", ")}`);
8600
- const description = input2.description.trim();
8601
- if (!description || description.includes("\n") || description.length > DESCRIPTION_MAX) {
8602
- throw new Error(`description must be one line of at most ${DESCRIPTION_MAX} characters`);
8603
- }
8604
- const body = input2.body.trim();
8605
- if (!body || body.length > BODY_MAX) {
8606
- 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
+ };
8607
8603
  }
8608
- }
8609
- 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"];
8610
8609
  return {
8611
- id: row["id"],
8612
- channelId: row["channel_id"],
8613
- name: row["name"],
8614
- description: row["description"],
8615
- type: row["type"],
8616
- body: row["body"],
8617
- sourceThreadId: row["source_thread_id"] ?? null,
8618
- createdBy: row["created_by"],
8619
- createdAt: row["created_at"],
8620
- 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
8621
8626
  };
8622
8627
  }
8623
- async function listChannelMemories(channelId) {
8624
- const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
8625
- if (error2) throw new Error(`listChannelMemories(${channelId}): ${error2.message}`);
8626
- return (data ?? []).map(rowToMemory);
8627
- }
8628
- async function getChannelMemory(channelId, name) {
8629
- const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
8630
- if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
8631
- return data ? rowToMemory(data) : null;
8632
- }
8633
- async function saveChannelMemory(input2, actor) {
8634
- validateMemoryInput(input2);
8635
- const supabase = getSupabaseClient();
8636
- const existing = await getChannelMemory(input2.channelId, input2.name);
8637
- const fields = {
8638
- description: input2.description.trim(),
8639
- type: input2.type,
8640
- body: input2.body.trim(),
8641
- source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
8642
- created_by: input2.createdBy
8643
- };
8644
- if (existing && sameContent(existing, fields)) {
8645
- return { memory: existing, created: false, changed: false };
8646
- }
8647
- 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();
8648
- if (error2)
8649
- throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
8650
- const memory = rowToMemory(data);
8651
- await logMemoryEvent({
8652
- memory,
8653
- action: existing ? "update" : "create",
8654
- before: existing ? snapshot(existing) : null,
8655
- after: snapshot(memory),
8656
- actor,
8657
- threadId: input2.sourceThreadId ?? null
8658
- });
8659
- return { memory, created: !existing, changed: true };
8660
- }
8661
- async function updateChannelMemory(channelId, memoryId, patch, actor) {
8662
- const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
8663
- if (!existing) return null;
8664
- const { memory } = await saveChannelMemory(
8665
- {
8666
- channelId,
8667
- name: existing.name,
8668
- description: patch.description ?? existing.description,
8669
- type: patch.type ?? existing.type,
8670
- body: patch.body ?? existing.body,
8671
- createdBy: existing.createdBy
8672
- },
8673
- actor
8674
- );
8675
- return memory;
8676
- }
8677
- async function searchChannelMemories(channelId, query) {
8678
- const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
8679
- if (!words.length) return [];
8680
- let request = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
8681
- for (const word of words) {
8682
- request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
8683
- }
8684
- const { data, error: error2 } = await request.order("updated_at", { ascending: false }).limit(20);
8685
- if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
8686
- return (data ?? []).map(rowToMemory);
8687
- }
8688
- async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
8689
- const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
8690
- if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
8691
- const deleted = (data ?? []).map(rowToMemory);
8692
- for (const memory of deleted) {
8693
- await logMemoryEvent({
8694
- memory,
8695
- action: "delete",
8696
- before: snapshot(memory),
8697
- after: null,
8698
- actor,
8699
- 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"]
8700
8634
  });
8701
- }
8702
- return deleted.length > 0;
8703
- }
8704
- function snapshot(m) {
8705
- return {
8706
- description: m.description,
8707
- type: m.type,
8708
- body: m.body,
8709
- sourceThreadId: m.sourceThreadId,
8710
- createdBy: m.createdBy
8711
- };
8712
- }
8713
- function sameContent(m, other) {
8714
- return m.description === other.description && m.type === other.type && m.body === other.body;
8715
- }
8716
- async function logMemoryEvent(e) {
8717
- const { error: error2 } = await getSupabaseClient().from("memory_events").insert({
8718
- channel_id: e.memory.channelId,
8719
- memory_id: e.action === "delete" ? null : e.memory.id,
8720
- thread_id: e.threadId,
8721
- name: e.memory.name,
8722
- action: e.action,
8723
- before: e.before,
8724
- after: e.after,
8725
- 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);
8726
8664
  });
8727
- if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
8728
8665
  }
8729
- function rowToEvent(row) {
8730
- return {
8731
- id: row["id"],
8732
- channelId: row["channel_id"],
8733
- memoryId: row["memory_id"] ?? null,
8734
- threadId: row["thread_id"] ?? null,
8735
- name: row["name"],
8736
- action: row["action"],
8737
- before: row["before"] ?? null,
8738
- after: row["after"] ?? null,
8739
- actor: row["actor"],
8740
- createdAt: row["created_at"],
8741
- undoneAt: row["undone_at"] ?? null
8742
- };
8743
- }
8744
- async function listThreadMemoryEvents(channelId, threadId) {
8745
- 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);
8746
- if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
8747
- return (data ?? []).map(rowToEvent);
8748
- }
8749
- async function undoMemoryEvent(channelId, eventId) {
8750
- const supabase = getSupabaseClient();
8751
- const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
8752
- if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
8753
- if (!data) return "not-found";
8754
- const event = rowToEvent(data);
8755
- if (event.undoneAt) return "already-undone";
8756
- const current = await getChannelMemory(channelId, event.name);
8757
- const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
8758
- if (!unchangedSince) return "conflict";
8759
- const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
8760
- if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
8761
- if (!(claimed.data ?? []).length) return "already-undone";
8762
- const before = event.before;
8763
- const restore = before ? {
8764
- description: before.description,
8765
- type: before.type,
8766
- body: before.body,
8767
- source_thread_id: before.sourceThreadId,
8768
- created_by: before.createdBy
8769
- } : null;
8770
- 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 });
8771
- if (writeError) {
8772
- await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
8773
- throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
8774
- }
8775
- return "undone";
8776
- }
8777
- function isUuid(value) {
8778
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
8779
- }
8780
- function formatMemoryPrompt(channelId, memories) {
8781
- const lines = [];
8782
- let used = 0;
8783
- let omitted = 0;
8784
- for (const m of memories) {
8785
- const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
8786
- if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
8787
- omitted++;
8788
- continue;
8789
- }
8790
- lines.push(line);
8791
- 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 });
8792
8678
  }
8793
- const index = lines.length ? lines.join("\n") + (omitted ? `
8794
- - (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
8795
- return [
8796
- `## Channel memory`,
8797
- ``,
8798
- `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.`,
8799
- ``,
8800
- `- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
8801
- `- Search: \`flyspace memory search --channel ${channelId} <words>\``,
8802
- `- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
8803
- `- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
8804
- ``,
8805
- `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.`,
8806
- ``,
8807
- `Index:`,
8808
- index
8809
- ].join("\n");
8810
8679
  }
8811
8680
 
8812
8681
  // src/dashboard/rollover-summary.ts
8813
- import { spawn as spawn4 } from "child_process";
8814
- import { tmpdir as tmpdir3 } from "os";
8682
+ import { spawn as spawn5 } from "child_process";
8683
+ import { tmpdir as tmpdir4 } from "os";
8815
8684
  var HAIKU_MODEL = "haiku";
8816
8685
  var CJK_PATTERN = /[ -〿぀-ヿ㐀-䶿一-鿿豈-﫿＀-￯]/;
8817
8686
  function estimateTokens(text) {
@@ -8870,11 +8739,11 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
8870
8739
  if (claudeConfigDir) env["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
8871
8740
  if (claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = claudeCodeOauthToken;
8872
8741
  else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
8873
- const child = spawn4("claude", args, {
8742
+ const child = spawn5("claude", args, {
8874
8743
  // A stateless rewrite touches no files — tmpdir keeps it that way and
8875
8744
  // avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
8876
8745
  // unlike the channel's own cwd the main turn runs in.
8877
- cwd: tmpdir3(),
8746
+ cwd: tmpdir4(),
8878
8747
  env,
8879
8748
  stdio: ["ignore", "pipe", "pipe"]
8880
8749
  });
@@ -9077,7 +8946,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
9077
8946
 
9078
8947
  // src/dashboard/attachment-downloader.ts
9079
8948
  import { writeFile } from "fs/promises";
9080
- import { join as join18 } from "path";
8949
+ import { join as join19 } from "path";
9081
8950
  import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
9082
8951
  var debug = process.env["DEBUG"] === "true";
9083
8952
  var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
@@ -9124,7 +8993,7 @@ async function downloadOne(key) {
9124
8993
  if (bytes.length > sizeLimit) {
9125
8994
  return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
9126
8995
  }
9127
- const localPath = join18(paths.UPLOADS_DIR, lastSegment);
8996
+ const localPath = join19(paths.UPLOADS_DIR, lastSegment);
9128
8997
  await writeFile(localPath, bytes);
9129
8998
  if (debug)
9130
8999
  console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
@@ -9296,21 +9165,21 @@ var ChatAgent = class {
9296
9165
  * kill.
9297
9166
  */
9298
9167
  async maybeSupersede(msg) {
9299
- const run3 = this.activeRuns.get(msg.threadId);
9300
- 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;
9301
9170
  try {
9302
- if (!await this.shouldInterrupt(run3.msg.body, msg.body)) return;
9171
+ if (!await this.shouldInterrupt(run4.msg.body, msg.body)) return;
9303
9172
  } catch (err) {
9304
9173
  console.error("[chat-agent] interrupt classifier failed:", err.message);
9305
9174
  return;
9306
9175
  }
9307
- if (this.activeRuns.get(msg.threadId) !== run3) return;
9176
+ if (this.activeRuns.get(msg.threadId) !== run4) return;
9308
9177
  console.log(
9309
9178
  `[chat-agent] follow-up supersedes in-flight run \u2014 aborting. thread=${msg.threadId}`
9310
9179
  );
9311
- run3.superseded = true;
9180
+ run4.superseded = true;
9312
9181
  this.supersedingReplies.add(msg.replyId);
9313
- run3.abort.abort();
9182
+ run4.abort.abort();
9314
9183
  }
9315
9184
  /**
9316
9185
  * Picks up messages nobody answered. `handleMessage` is fire-and-forget and
@@ -9473,6 +9342,11 @@ var ChatAgent = class {
9473
9342
  return data.filter((r) => r.id !== excludeReplyId).reverse().slice(-limit).map((r) => ({ author: r.author, body: r.body }));
9474
9343
  }
9475
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
+ }
9476
9350
  isThinking(threadId) {
9477
9351
  return this.activeRuns.has(threadId);
9478
9352
  }
@@ -9508,9 +9382,9 @@ var ChatAgent = class {
9508
9382
  * continue with `--resume`. Returns whether a run was actually in flight —
9509
9383
  * a queued-but-not-started message is not affected. */
9510
9384
  cancel(threadId) {
9511
- const run3 = this.activeRuns.get(threadId);
9512
- if (!run3) return false;
9513
- run3.abort.abort();
9385
+ const run4 = this.activeRuns.get(threadId);
9386
+ if (!run4) return false;
9387
+ run4.abort.abort();
9514
9388
  return true;
9515
9389
  }
9516
9390
  /**
@@ -9672,18 +9546,11 @@ var ChatAgent = class {
9672
9546
  ...docsAccessUnverified ? { docsAccessUnverified } : {}
9673
9547
  };
9674
9548
  }
9675
- const agentMemory = await listEnabledFeatureFlagsBySlug(owningProfile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
9676
- console.error("[chat-agent] feature flag lookup failed:", err.message);
9677
- return false;
9549
+ const memory = await loadAgentMemory(owningProfile, thread.channel_id, {
9550
+ readable: docsAccess !== "none"
9678
9551
  });
9679
- if (agentMemory) channelConfig = { ...channelConfig, disableAutoMemory: true };
9680
- if (agentMemory && docsAccess !== "none" && isUuid(thread.channel_id)) {
9681
- const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
9682
- console.error("[chat-agent] memory lookup failed:", err.message);
9683
- return void 0;
9684
- });
9685
- if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
9686
- }
9552
+ if (memory.enabled) channelConfig = { ...channelConfig, disableAutoMemory: true };
9553
+ if (memory.memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt: memory.memoryPrompt };
9687
9554
  const store = this.openSessionStore();
9688
9555
  try {
9689
9556
  const channelKey = `chat:${thread.channel_id}`;
@@ -10411,7 +10278,7 @@ async function exportDocAsArtifact(channelId, docPath) {
10411
10278
  );
10412
10279
  const abs = resolveDocPath(channelConfig.cwd, docPath);
10413
10280
  if (!abs) throw new Error(`doc not found: ${docPath}`);
10414
- const content = readFileSync12(abs, "utf8");
10281
+ const content = readFileSync13(abs, "utf8");
10415
10282
  const title = extractTitle(abs, "md");
10416
10283
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10417
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).
@@ -10668,7 +10535,7 @@ function probeDir(absDir, showHidden) {
10668
10535
  return false;
10669
10536
  }
10670
10537
  if (!showHidden && entry.name.startsWith(".")) continue;
10671
- const abs = join19(dir, entry.name);
10538
+ const abs = join20(dir, entry.name);
10672
10539
  const kind = entryKind(entry, abs);
10673
10540
  if (kind === "dir") {
10674
10541
  if (SKIP_DIRS.has(entry.name)) hasOther = true;
@@ -10688,14 +10555,14 @@ function probeDir(absDir, showHidden) {
10688
10555
  }
10689
10556
  function readGitBranch(dir) {
10690
10557
  try {
10691
- let gitDir = join19(dir, ".git");
10558
+ let gitDir = join20(dir, ".git");
10692
10559
  if (statSync5(gitDir).isFile()) {
10693
- const pointer = readFileSync13(gitDir, "utf8").trim();
10560
+ const pointer = readFileSync14(gitDir, "utf8").trim();
10694
10561
  const match = pointer.match(/^gitdir:\s*(.+)$/);
10695
10562
  if (!match) return void 0;
10696
10563
  gitDir = resolve4(dir, match[1]);
10697
10564
  }
10698
- const head = readFileSync13(join19(gitDir, "HEAD"), "utf8").trim();
10565
+ const head = readFileSync14(join20(gitDir, "HEAD"), "utf8").trim();
10699
10566
  const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
10700
10567
  if (branchMatch) return branchMatch[1];
10701
10568
  return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
@@ -10717,14 +10584,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
10717
10584
  for (const entry of entries2) {
10718
10585
  if (dirs.length + docs.length >= MAX_ENTRIES) break;
10719
10586
  if (!showHidden && entry.name.startsWith(".")) continue;
10720
- const abs = join19(absDir, entry.name);
10587
+ const abs = join20(absDir, entry.name);
10721
10588
  const path = relative(root, abs).split(sep).join("/");
10722
10589
  const kind = entryKind(entry, abs);
10723
10590
  if (kind === "dir") {
10724
10591
  if (SKIP_DIRS.has(entry.name)) continue;
10725
10592
  const probe3 = probeDir(abs, showHidden);
10726
10593
  if (!probe3.hasDocs && probe3.hasOther) continue;
10727
- const isRepo = existsSync16(join19(abs, ".git"));
10594
+ const isRepo = existsSync16(join20(abs, ".git"));
10728
10595
  dirs.push({
10729
10596
  path,
10730
10597
  name: entry.name,
@@ -10760,7 +10627,7 @@ function flatDocs(root, {
10760
10627
  for (const entry of entries2) {
10761
10628
  if (hits.length >= MAX_FLAT_DOCS) return;
10762
10629
  if (!showHidden && entry.name.startsWith(".")) continue;
10763
- const abs = join19(dir, entry.name);
10630
+ const abs = join20(dir, entry.name);
10764
10631
  const kind = entryKind(entry, abs);
10765
10632
  if (kind === "dir") {
10766
10633
  if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
@@ -10840,7 +10707,7 @@ function uniqueName(absDir, name) {
10840
10707
  let candidate = name;
10841
10708
  for (let n = 1; n < 1e3; n++) {
10842
10709
  try {
10843
- statSync5(join19(absDir, candidate));
10710
+ statSync5(join20(absDir, candidate));
10844
10711
  } catch {
10845
10712
  return candidate;
10846
10713
  }
@@ -10862,7 +10729,7 @@ function saveUploadedFile(root, relDir, name, data) {
10862
10729
  if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
10863
10730
  if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
10864
10731
  const final = uniqueName(absDir, safe);
10865
- const abs = join19(absDir, final);
10732
+ const abs = join20(absDir, final);
10866
10733
  writeFileSync9(abs, data);
10867
10734
  return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
10868
10735
  }
@@ -10876,7 +10743,7 @@ function createDocFolder(root, relDir, name) {
10876
10743
  }
10877
10744
  const safe = sanitizeUploadName(name);
10878
10745
  if (!safe) return { status: "bad-name" };
10879
- const abs = join19(absDir, safe);
10746
+ const abs = join20(absDir, safe);
10880
10747
  try {
10881
10748
  statSync5(abs);
10882
10749
  return { status: "exists" };
@@ -10899,7 +10766,7 @@ function createDocFile(root, relDir, name, content) {
10899
10766
  if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
10900
10767
  if (SKIP_FILES.has(safe)) return { status: "bad-name" };
10901
10768
  if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
10902
- const abs = join19(absDir, safe);
10769
+ const abs = join20(absDir, safe);
10903
10770
  try {
10904
10771
  writeFileSync9(abs, content, { encoding: "utf8", flag: "wx" });
10905
10772
  } catch (err) {
@@ -10937,7 +10804,7 @@ function moveDocEntry(root, from, to) {
10937
10804
  } catch {
10938
10805
  return { status: "bad-target" };
10939
10806
  }
10940
- const finalAbs = join19(absToDir, name);
10807
+ const finalAbs = join20(absToDir, name);
10941
10808
  if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
10942
10809
  return { status: "into-self" };
10943
10810
  }
@@ -10960,7 +10827,7 @@ function deleteDocEntry(root, path) {
10960
10827
  }
10961
10828
  const isDir2 = stat.isDirectory();
10962
10829
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10963
- rmSync7(abs, { recursive: isDir2 });
10830
+ rmSync8(abs, { recursive: isDir2 });
10964
10831
  return {
10965
10832
  status: "ok",
10966
10833
  path: relative(root, abs).split(sep).join("/"),
@@ -10999,7 +10866,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
10999
10866
  }
11000
10867
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
11001
10868
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
11002
- const data = readFileSync13(resolved2.abs);
10869
+ const data = readFileSync14(resolved2.abs);
11003
10870
  const headers = { "Content-Type": resolved2.contentType };
11004
10871
  if (download) {
11005
10872
  const filename = basename6(resolved2.abs).replace(/"/g, "");
@@ -11019,7 +10886,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11019
10886
  }
11020
10887
  if (!stat.isFile()) return c.json({ error: "not found" }, 404);
11021
10888
  if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
11022
- return c.body(new Uint8Array(readFileSync13(abs)), 200, {
10889
+ return c.body(new Uint8Array(readFileSync14(abs)), 200, {
11023
10890
  "Content-Type": "text/html; charset=utf-8",
11024
10891
  "Content-Security-Policy": HTML_SHARE_CSP,
11025
10892
  "X-Content-Type-Options": "nosniff",
@@ -11204,7 +11071,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11204
11071
  title: kind ? extractTitle(abs, kind) : basename6(abs),
11205
11072
  mtime: stat.mtime.toISOString(),
11206
11073
  tooLarge: tooLarge || void 0,
11207
- content: tooLarge ? "" : readFileSync13(abs, "utf8")
11074
+ content: tooLarge ? "" : readFileSync14(abs, "utf8")
11208
11075
  });
11209
11076
  });
11210
11077
  app.put("/:id/docs/content", async (c) => {
@@ -11247,7 +11114,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
11247
11114
  channel,
11248
11115
  rel,
11249
11116
  title: extractTitle(abs, "md"),
11250
- content: readFileSync13(abs, "utf8")
11117
+ content: readFileSync14(abs, "utf8")
11251
11118
  };
11252
11119
  };
11253
11120
  app.get("/:id/docs/translation/:lang", async (c) => {
@@ -11470,7 +11337,7 @@ function ownSkills(scope, dir) {
11470
11337
  return [{ scope, name, description, body, path, plugin: null, mtime }];
11471
11338
  });
11472
11339
  }
11473
- function snapshot2(scopes) {
11340
+ function snapshot(scopes) {
11474
11341
  const wants = (scope) => !scopes || scopes.has(scope);
11475
11342
  const configDir2 = profileClaudeConfigDir();
11476
11343
  const rows = [];
@@ -11505,7 +11372,7 @@ async function pushSkillScopes(scopes) {
11505
11372
  if (!mirrorEnabled()) return;
11506
11373
  const workspace = getProfile();
11507
11374
  const supabase = getSupabaseClient();
11508
- const rows = snapshot2(scopes ? new Set(scopes) : void 0);
11375
+ const rows = snapshot(scopes ? new Set(scopes) : void 0);
11509
11376
  let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
11510
11377
  if (scopes) query = query.in("scope", scopes);
11511
11378
  const { data, error: error2 } = await query;
@@ -11789,7 +11656,7 @@ function registerMirroredSkillRoutes(api, agent) {
11789
11656
  import { execFileSync as execFileSync3 } from "child_process";
11790
11657
  import { existsSync as existsSync19 } from "fs";
11791
11658
  import { homedir as homedir8 } from "os";
11792
- import { join as join20, resolve as resolve5 } from "path";
11659
+ import { join as join21, resolve as resolve5 } from "path";
11793
11660
  var PROVIDER_LABELS = {
11794
11661
  claude: "Claude Code",
11795
11662
  codex: "Codex"
@@ -11807,7 +11674,7 @@ function binaryOnPath(bin) {
11807
11674
  function claudeAuthAvailable() {
11808
11675
  if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
11809
11676
  const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
11810
- if (existsSync19(join20(configDir2, ".credentials.json"))) return true;
11677
+ if (existsSync19(join21(configDir2, ".credentials.json"))) return true;
11811
11678
  if (process.platform !== "darwin") return false;
11812
11679
  try {
11813
11680
  execFileSync3("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
@@ -11960,7 +11827,7 @@ function registerHostRoutes(api) {
11960
11827
  const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
11961
11828
  const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
11962
11829
  const filename = errors ? "custie-error.log" : "custie.log";
11963
- const file = join21(paths.LOG_DIR, filename);
11830
+ const file = join22(paths.LOG_DIR, filename);
11964
11831
  const exists = existsSync20(file);
11965
11832
  return c.json({
11966
11833
  file,
@@ -11972,7 +11839,7 @@ function registerHostRoutes(api) {
11972
11839
  }
11973
11840
  function tailFile(file, limit) {
11974
11841
  if (!existsSync20(file)) return [];
11975
- const lines = readFileSync14(file, "utf-8").split("\n");
11842
+ const lines = readFileSync15(file, "utf-8").split("\n");
11976
11843
  if (lines.at(-1) === "") lines.pop();
11977
11844
  return lines.slice(-limit);
11978
11845
  }
@@ -13956,6 +13823,10 @@ function createMembersRouter() {
13956
13823
  if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
13957
13824
  return c.json({ error: "unknown avatar" }, 400);
13958
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
+ }
13959
13830
  const existing = await getMembershipByEmail(ctx.workspaceId, email);
13960
13831
  if (existing) return c.json({ error: "already a member of this workspace" }, 409);
13961
13832
  let membership;
@@ -15153,7 +15024,7 @@ function uniqueChannelName(raw, taken) {
15153
15024
  // src/dashboard/workspace-greeting.ts
15154
15025
  var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
15155
15026
  var GREETING_MODEL = "openai/gpt-oss-20b";
15156
- var TIMEOUT_MS = 6e3;
15027
+ var TIMEOUT_MS2 = 6e3;
15157
15028
  var MAX_LISTED_CHANNELS = 8;
15158
15029
  var GUIDES = [
15159
15030
  "Welcome to Flyspace",
@@ -15203,7 +15074,7 @@ async function groqGreeting(input2, apiKey) {
15203
15074
  ].join("\n");
15204
15075
  const res = await fetch(GROQ_API_URL8, {
15205
15076
  method: "POST",
15206
- signal: AbortSignal.timeout(TIMEOUT_MS),
15077
+ signal: AbortSignal.timeout(TIMEOUT_MS2),
15207
15078
  headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
15208
15079
  body: JSON.stringify({
15209
15080
  model: GREETING_MODEL,
@@ -15504,17 +15375,17 @@ function registerDataRoutes(api, agent = {}) {
15504
15375
  const auto = loadEffectiveAutomations();
15505
15376
  const runs = /* @__PURE__ */ new Map();
15506
15377
  try {
15507
- for (const [name, run3] of await lastRunsForWorkspace(getProfile())) {
15508
- 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 });
15509
15380
  }
15510
15381
  } catch {
15511
15382
  if (existsSync22(paths.DB_FILE)) {
15512
15383
  const runStore = new AutomationRunStore(paths.DB_FILE);
15513
15384
  try {
15514
- for (const run3 of runStore.listLastRuns()) {
15515
- runs.set(run3.name, {
15516
- lastRun: run3.lastRunAt.toISOString(),
15517
- lastStatus: run3.lastStatus
15385
+ for (const run4 of runStore.listLastRuns()) {
15386
+ runs.set(run4.name, {
15387
+ lastRun: run4.lastRunAt.toISOString(),
15388
+ lastStatus: run4.lastStatus
15518
15389
  });
15519
15390
  }
15520
15391
  } finally {
@@ -15704,17 +15575,17 @@ function registerDataRoutes(api, agent = {}) {
15704
15575
  }
15705
15576
  const threadLinks = await resolveThreadLinks(raw);
15706
15577
  const sessions = raw.map((s) => {
15707
- const run3 = latest.get(s.threadTs);
15708
- const turns = run3?.numTurns ?? 1;
15709
- 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;
15710
15581
  const link = s.channelId.startsWith("chat:") ? threadLinks.chat.get(s.threadTs) : threadLinks.external.get(`${s.channelId}:${s.threadTs}`);
15711
15582
  return {
15712
15583
  ...s,
15713
15584
  channelLabel: channelLabel(s.channelId),
15714
- model: run3?.model,
15715
- numTurns: run3?.numTurns,
15585
+ model: run4?.model,
15586
+ numTurns: run4?.numTurns,
15716
15587
  contextTokens: total !== void 0 ? Math.round(total / (turns > 0 ? turns : 1)) : void 0,
15717
- rolledOver: run3?.label === "rollover",
15588
+ rolledOver: run4?.label === "rollover",
15718
15589
  threadTitle: link?.title ?? void 0,
15719
15590
  chatChannelId: link?.chatChannelId,
15720
15591
  chatThreadId: link?.chatThreadId,
@@ -15817,16 +15688,16 @@ function relay2(agent) {
15817
15688
  }
15818
15689
 
15819
15690
  // src/dashboard/agent-bridge.ts
15820
- import { existsSync as existsSync24, readFileSync as readFileSync17 } from "fs";
15691
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
15821
15692
  import dotenv from "dotenv";
15822
15693
  import { WebSocket, WebSocketServer } from "ws";
15823
15694
 
15824
15695
  // src/dashboard/host-identity.ts
15825
15696
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
15826
- import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
15827
- import { dirname as dirname7, join as join22 } 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";
15828
15699
  function hostPrivateKeyPath() {
15829
- return process.env["FLYSPACE_HOST_KEY_PATH"] || join22(paths.BASE_CONFIG_DIR, "host.key");
15700
+ return process.env["FLYSPACE_HOST_KEY_PATH"] || join23(paths.BASE_CONFIG_DIR, "host.key");
15830
15701
  }
15831
15702
  function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15832
15703
  if (!existsSync23(path)) {
@@ -15834,7 +15705,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
15834
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.`
15835
15706
  );
15836
15707
  }
15837
- return createPrivateKey2(readFileSync15(path, "utf8"));
15708
+ return createPrivateKey2(readFileSync16(path, "utf8"));
15838
15709
  }
15839
15710
  function loadHostPublicKey() {
15840
15711
  const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
@@ -16079,7 +15950,7 @@ async function userEmail(userId) {
16079
15950
 
16080
15951
  // src/dashboard/doc-share-publisher.ts
16081
15952
  import { randomBytes as randomBytes7 } from "crypto";
16082
- import { readFileSync as readFileSync16, statSync as statSync9 } from "fs";
15953
+ import { readFileSync as readFileSync17, statSync as statSync9 } from "fs";
16083
15954
  import { extname as extname2, posix } from "path";
16084
15955
  import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
16085
15956
 
@@ -16266,7 +16137,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16266
16137
  }
16267
16138
  if (!stat.isFile()) return { status: "not-found" };
16268
16139
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
16269
- const source = readFileSync16(abs, "utf8");
16140
+ const source = readFileSync17(abs, "utf8");
16270
16141
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
16271
16142
  if (kind === "excalidraw") {
16272
16143
  try {
@@ -16346,7 +16217,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16346
16217
  let text = null;
16347
16218
  try {
16348
16219
  const size = statSync9(docAbs).size;
16349
- if (size <= budget) text = readFileSync16(docAbs, "utf8");
16220
+ if (size <= budget) text = readFileSync17(docAbs, "utf8");
16350
16221
  } catch {
16351
16222
  continue;
16352
16223
  }
@@ -16363,7 +16234,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
16363
16234
  let body;
16364
16235
  try {
16365
16236
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
16366
- body = readFileSync16(resolved2.abs);
16237
+ body = readFileSync17(resolved2.abs);
16367
16238
  } catch {
16368
16239
  continue;
16369
16240
  }
@@ -16405,7 +16276,7 @@ async function publishDocShare(input2) {
16405
16276
  ...HTML_FEATURES,
16406
16277
  brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
16407
16278
  } : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
16408
- const snapshot3 = {
16279
+ const snapshot2 = {
16409
16280
  ...built,
16410
16281
  sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
16411
16282
  };
@@ -16415,8 +16286,8 @@ async function publishDocShare(input2) {
16415
16286
  const renamed = await setDocShareSlug(existing.id, slug);
16416
16287
  if (!renamed) return { status: "not-found" };
16417
16288
  }
16418
- const share2 = await updateDocShareSnapshot(existing.id, snapshot3, features);
16419
- 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));
16420
16291
  await deleteShareAssets(stale);
16421
16292
  if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
16422
16293
  void queueShareAudio(share2);
@@ -16430,7 +16301,7 @@ async function publishDocShare(input2) {
16430
16301
  channelId: input2.channelId,
16431
16302
  sourcePath: input2.sourcePath,
16432
16303
  createdBy: input2.createdBy,
16433
- ...snapshot3,
16304
+ ...snapshot2,
16434
16305
  ...features
16435
16306
  });
16436
16307
  void queueShareAudio(share);
@@ -16457,9 +16328,12 @@ var METHODS = {
16457
16328
  thread_replies: ["GET", "HEAD", "POST", "PATCH"],
16458
16329
  memberships: ["GET", "HEAD"],
16459
16330
  channel_members: ["GET", "HEAD"],
16460
- channel_docs_access: ["GET", "HEAD"]
16331
+ channel_docs_access: ["GET", "HEAD"],
16332
+ memories: ALL,
16333
+ memory_events: ["GET", "HEAD", "POST", "PATCH"]
16461
16334
  };
16462
16335
  var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members", "channel_docs_access"]);
16336
+ var CHANNEL_OWNED = /* @__PURE__ */ new Set(["memories", "memory_events"]);
16463
16337
  var FORWARDED_HEADERS2 = ["accept", "content-type", "prefer", "range"];
16464
16338
  var deny = (message, status = 403) => ({
16465
16339
  status,
@@ -16575,6 +16449,16 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
16575
16449
  if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
16576
16450
  if (CHANNEL_SCOPED.has(table)) {
16577
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));
16578
16462
  } else if (table === "threads") {
16579
16463
  if (method === "POST") {
16580
16464
  const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
@@ -16664,6 +16548,7 @@ function localAgentCallbacks(chatAgent) {
16664
16548
  cancelAgentRun: (threadId) => chatAgent.cancel(threadId),
16665
16549
  regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
16666
16550
  listAgentRuns: () => chatAgent.runningThreadIds(),
16551
+ completeMemoryExtraction: (req) => chatAgent.completeMemoryExtraction(req),
16667
16552
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
16668
16553
  listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
16669
16554
  getAgentIdentities: () => resolveAgentIdentities(),
@@ -16683,14 +16568,14 @@ function localAgentCallbacks(chatAgent) {
16683
16568
  }
16684
16569
  var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
16685
16570
  function buildHostConfigSnapshot() {
16686
- const snapshot3 = {};
16571
+ const snapshot2 = {};
16687
16572
  for (const profile of listProfiles()) {
16688
- snapshot3[profile] = runWithProfile(profile, () => ({
16689
- 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")) : {},
16690
16575
  agentModel: loadDashboardSettings().agent
16691
16576
  }));
16692
16577
  }
16693
- return snapshot3;
16578
+ return snapshot2;
16694
16579
  }
16695
16580
  function startLocalProfileConfigHydration() {
16696
16581
  setProfileConfigCache(buildHostConfigSnapshot());
@@ -16699,6 +16584,7 @@ function startLocalProfileConfigHydration() {
16699
16584
  }
16700
16585
  var CALL_TIMEOUT_MS = 15e3;
16701
16586
  var DOCS_CALL_TIMEOUT_MS = 12e4;
16587
+ var MEMORY_EXTRACTION_TIMEOUT_MS = 165e3;
16702
16588
  var AUTH_TIMEOUT_MS = 5e3;
16703
16589
  var HEARTBEAT_INTERVAL_MS = 15e3;
16704
16590
  var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
@@ -16926,6 +16812,9 @@ function remoteAgentCallbacks() {
16926
16812
  cancelAgentRun: async (threadId) => await callHost("cancelAgentRun", [threadId]) ?? false,
16927
16813
  regenerateAgentRun: async (threadId) => await callHost("regenerateAgentRun", [threadId]) ?? false,
16928
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),
16929
16818
  // Deliberately passes `null` through rather than defaulting to `[]`: with
16930
16819
  // no Host connected, claiming every folder is fine would be a guess.
16931
16820
  listMissingCwds: () => callHost("listMissingCwds", []),
@@ -17411,6 +17300,7 @@ function createApiRouter(agent = {}, mode = "all") {
17411
17300
  const resolved2 = requested && known.includes(requested) ? requested : profileFromHost(trustedHost(c), known);
17412
17301
  if (!resolved2) {
17413
17302
  return c.json({
17303
+ unknownWorkspace: isUnknownWorkspaceHost(trustedHost(c), known),
17414
17304
  theme: BRANDING_DEFAULTS.theme,
17415
17305
  font: BRANDING_DEFAULTS.font,
17416
17306
  displayName: "Flyspace",
@@ -17731,7 +17621,7 @@ function startHostStatusBroadcast() {
17731
17621
 
17732
17622
  // src/dashboard/server.ts
17733
17623
  var DEFAULT_DASHBOARD_PORT = 4747;
17734
- var WEB_DIST = join23(paths.PACKAGE_ROOT, "web", "dist");
17624
+ var WEB_DIST = join24(paths.PACKAGE_ROOT, "web", "dist");
17735
17625
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
17736
17626
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
17737
17627
  for (let port = start; port < start + attempts; port++) {
@@ -17781,6 +17671,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17781
17671
  const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
17782
17672
  };
17783
17673
  const app = new Hono34();
17674
+ app.onError(reportingErrorHandler);
17784
17675
  app.use("*", async (c, next) => {
17785
17676
  await next();
17786
17677
  if (c.req.path.startsWith("/assets/")) {
@@ -17819,14 +17710,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
17819
17710
  } else {
17820
17711
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
17821
17712
  app.route("/", createAppIconRouter());
17822
- 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"))) {
17823
17714
  app.use("/*", serveStatic({ root: WEB_DIST }));
17824
17715
  }
17825
17716
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
17826
- const spaFallback = serveStatic({ path: join23(WEB_DIST, "index.html") });
17717
+ const spaFallback = serveStatic({ path: join24(WEB_DIST, "index.html") });
17827
17718
  app.get("*", async (c, next) => {
17828
- if (!existsSync25(join23(WEB_DIST, "index.html"))) {
17829
- 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"));
17830
17721
  return c.text(
17831
17722
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
17832
17723
  Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
@@ -18264,7 +18155,7 @@ async function threadReplies(client2, nameOrId, ts2) {
18264
18155
  async function postMessage(client2, args) {
18265
18156
  const channelArg = getArg2(args, "--channel");
18266
18157
  const textFilePath = getArg2(args, "--text-file");
18267
- const text = textFilePath ? readFileSync18(textFilePath, "utf8") : getArg2(args, "--text");
18158
+ const text = textFilePath ? readFileSync19(textFilePath, "utf8") : getArg2(args, "--text");
18268
18159
  const threadTs = getArg2(args, "--thread");
18269
18160
  const markdown = args.includes("--markdown");
18270
18161
  const broadcast = args.includes("--broadcast") && Boolean(threadTs);
@@ -18419,7 +18310,7 @@ async function ensureClaudeCodeReady() {
18419
18310
  }
18420
18311
  function getManifestYaml(botName) {
18421
18312
  const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
18422
- const raw = readFileSync18(manifestPath, "utf-8");
18313
+ const raw = readFileSync19(manifestPath, "utf-8");
18423
18314
  return raw.replace(/name: Custie/g, `name: ${botName}`);
18424
18315
  }
18425
18316
  async function stepPersonalise() {
@@ -18632,7 +18523,7 @@ async function stepBrowserTokens(botName) {
18632
18523
  })
18633
18524
  ));
18634
18525
  const DEBUG = process.argv.includes("--debug");
18635
- const USER_DATA_DIR = resolve6(tmpdir4(), "custie-playwright-profile");
18526
+ const USER_DATA_DIR = resolve6(tmpdir5(), "custie-playwright-profile");
18636
18527
  let context;
18637
18528
  try {
18638
18529
  log4("Opening browser \u2014 a Slack login page will appear.\n");
@@ -18851,7 +18742,7 @@ function copyDefaultPrompt(botName) {
18851
18742
  if (!existsSync26(paths.PROMPT_FILE)) {
18852
18743
  const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
18853
18744
  if (existsSync26(defaultPrompt)) {
18854
- const content = readFileSync18(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18745
+ const content = readFileSync19(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
18855
18746
  writeFileSync11(paths.PROMPT_FILE, content);
18856
18747
  success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
18857
18748
  }
@@ -19146,6 +19037,7 @@ async function runPost(args) {
19146
19037
  }
19147
19038
  async function runChatCmd(args) {
19148
19039
  loadEnvFiles();
19040
+ useHostDbWithoutCredentials();
19149
19041
  const subcommand = args[0];
19150
19042
  const rest = args.slice(1);
19151
19043
  switch (subcommand) {
@@ -19174,21 +19066,21 @@ async function runChatCmd(args) {
19174
19066
  }
19175
19067
 
19176
19068
  // src/commands/memory.ts
19177
- import { readFileSync as readFileSync20 } from "fs";
19069
+ import { readFileSync as readFileSync21 } from "fs";
19178
19070
 
19179
19071
  // src/store/claude-memory-import.ts
19180
- 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";
19181
19073
  import { homedir as homedir10 } from "os";
19182
- import { basename as basename8, join as join24 } from "path";
19074
+ import { basename as basename8, join as join25 } from "path";
19183
19075
  import dotenv2 from "dotenv";
19184
19076
  function claudeMemoryDir(cwd, configDir2) {
19185
- 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");
19186
19078
  }
19187
19079
  function profileClaudeConfigDirFor(profile) {
19188
- const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join24(paths.PROFILES_DIR, profile);
19189
- const file = join24(dir, "config.env");
19190
- const env = existsSync27(file) ? dotenv2.parse(readFileSync19(file)) : {};
19191
- 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");
19192
19084
  }
19193
19085
  var TYPE_MAP = {
19194
19086
  user: "preference",
@@ -19196,7 +19088,7 @@ var TYPE_MAP = {
19196
19088
  project: "fact",
19197
19089
  reference: "reference"
19198
19090
  };
19199
- var DESCRIPTION_MAX2 = 200;
19091
+ var DESCRIPTION_MAX = 200;
19200
19092
  function unquote2(value) {
19201
19093
  const v = value.trim();
19202
19094
  return /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
@@ -19214,8 +19106,8 @@ function parseClaudeMemory(fileName, text, channelId) {
19214
19106
  }
19215
19107
  const name = slugify(fields["name"] || basename8(fileName, ".md"));
19216
19108
  let description = (fields["description"] ?? "").replaceAll(/\s+/g, " ").trim();
19217
- if (description.length > DESCRIPTION_MAX2) {
19218
- description = `${description.slice(0, DESCRIPTION_MAX2 - 3).trimEnd()}...`;
19109
+ if (description.length > DESCRIPTION_MAX) {
19110
+ description = `${description.slice(0, DESCRIPTION_MAX - 3).trimEnd()}...`;
19219
19111
  }
19220
19112
  const input2 = {
19221
19113
  channelId,
@@ -19237,7 +19129,7 @@ async function importClaudeMemories(dir, channelId, opts = {}) {
19237
19129
  const files = readdirSync5(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
19238
19130
  const results = [];
19239
19131
  for (const file of files) {
19240
- const parsed = parseClaudeMemory(file, readFileSync19(join24(dir, file), "utf-8"), channelId);
19132
+ const parsed = parseClaudeMemory(file, readFileSync20(join25(dir, file), "utf-8"), channelId);
19241
19133
  if ("skip" in parsed) {
19242
19134
  results.push({ file, outcome: "skipped", reason: parsed.skip });
19243
19135
  continue;
@@ -19291,6 +19183,7 @@ function indexLine(m) {
19291
19183
  }
19292
19184
  async function runMemoryCmd(args) {
19293
19185
  loadEnvFiles();
19186
+ useHostDbWithoutCredentials();
19294
19187
  const subcommand = args[0];
19295
19188
  const rest = args.slice(1);
19296
19189
  if (!subcommand || subcommand === "-h" || subcommand === "--help") {
@@ -19336,7 +19229,7 @@ ${memory.body}`);
19336
19229
  );
19337
19230
  }
19338
19231
  if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
19339
- const body = bodyArg === "-" ? readFileSync20(0, "utf-8") : bodyArg;
19232
+ const body = bodyArg === "-" ? readFileSync21(0, "utf-8") : bodyArg;
19340
19233
  const thread = getArg4(rest, "--thread");
19341
19234
  const { created, changed } = await saveChannelMemory(
19342
19235
  {
@@ -19420,10 +19313,10 @@ async function runMcpCmd(args) {
19420
19313
  }
19421
19314
 
19422
19315
  // src/commands/dashboard.ts
19423
- import { spawn as spawn5, spawnSync } from "child_process";
19316
+ import { spawn as spawn6, spawnSync } from "child_process";
19424
19317
  import { existsSync as existsSync28 } from "fs";
19425
19318
  import { platform as platform7 } from "os";
19426
- import { join as join25 } from "path";
19319
+ import { join as join26 } from "path";
19427
19320
  function flagValue(args, name) {
19428
19321
  const eq = args.find((a) => a.startsWith(`--${name}=`));
19429
19322
  if (eq) return eq.slice(`--${name}=`.length);
@@ -19433,13 +19326,13 @@ function flagValue(args, name) {
19433
19326
  function openBrowser2(url) {
19434
19327
  const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
19435
19328
  try {
19436
- spawn5(cmd, [url], { stdio: "ignore", detached: true }).unref();
19329
+ spawn6(cmd, [url], { stdio: "ignore", detached: true }).unref();
19437
19330
  } catch {
19438
19331
  }
19439
19332
  }
19440
19333
  function ensureWebBuild() {
19441
- if (existsSync28(join25(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
19442
- 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;
19443
19336
  console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
19444
19337
  const result = spawnSync("pnpm", ["--dir", "web", "build"], {
19445
19338
  cwd: paths.PACKAGE_ROOT,
@@ -19802,7 +19695,7 @@ async function main() {
19802
19695
  break;
19803
19696
  case "-v":
19804
19697
  case "--version":
19805
- printRaw("0.1.14");
19698
+ printRaw("0.1.15");
19806
19699
  break;
19807
19700
  case "-h":
19808
19701
  case "--help":