flyspace-cli 0.1.32 → 0.1.34

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.
@@ -102,6 +102,9 @@ function loadConfig() {
102
102
  const autoRespondChannelIds = new Set(
103
103
  (process.env["AUTO_RESPOND_CHANNEL_IDS"] ?? "").split(",").map((s) => s.trim()).filter(Boolean)
104
104
  );
105
+ const slackEnglishOnlyExemptChannelIds = new Set(
106
+ (process.env["SLACK_ENGLISH_ONLY_EXEMPT_CHANNEL_IDS"] ?? "").split(",").map((s) => s.trim()).filter(Boolean)
107
+ );
105
108
  const slack = slackConfigured();
106
109
  return {
107
110
  slackBotToken: slack ? requireEnv("SLACK_BOT_TOKEN") : void 0,
@@ -118,7 +121,9 @@ function loadConfig() {
118
121
  maxBudgetUsd: parseBudget(process.env["MAX_BUDGET_USD"]),
119
122
  ownerUserId,
120
123
  autoRespondChannelIds,
121
- codexModel: process.env["CUSTIE_CODEX_MODEL"]?.trim() || DEFAULT_CODEX_MODEL
124
+ codexModel: process.env["CUSTIE_CODEX_MODEL"]?.trim() || DEFAULT_CODEX_MODEL,
125
+ slackEnglishOnly: process.env["SLACK_ENGLISH_ONLY"] === "true",
126
+ slackEnglishOnlyExemptChannelIds
122
127
  };
123
128
  }
124
129
 
@@ -290,6 +295,15 @@ function resolveLogicalCwd(p) {
290
295
  function isUnderLogicalRoot(cwd) {
291
296
  return [logicalRoot(), ...workspaceRoots.values()].some((r) => cwd.startsWith(r + sep));
292
297
  }
298
+ function toLogicalCwd(cwd) {
299
+ const fromRoot = relative(logicalRoot(), cwd).split(sep).join("/");
300
+ if (isLogicalCwd(fromRoot)) return fromRoot;
301
+ for (const [slug, root] of workspaceRoots) {
302
+ const name = relative(root, cwd);
303
+ if (isLogicalCwd(`${slug}/${name}`)) return `${slug}/${name}`;
304
+ }
305
+ return cwd;
306
+ }
293
307
  function ensureLogicalCwdDir(cwd) {
294
308
  if (!isUnderLogicalRoot(cwd) || existsSync2(cwd)) return;
295
309
  mkdirSync(cwd, { recursive: true });
@@ -356,6 +370,7 @@ function rowToChannel(row) {
356
370
  isDefault: row.is_default === true,
357
371
  profile: row.profile ?? "default",
358
372
  archivedAt: row.archived_at,
373
+ greetedAt: row.greeted_at,
359
374
  createdAt: row.created_at,
360
375
  updatedAt: row.updated_at
361
376
  };
@@ -516,7 +531,7 @@ function resolveEffort(explicitEffort, channelId, fallback) {
516
531
  function toRow(input) {
517
532
  const row = {};
518
533
  if (input.name !== void 0) row["name"] = input.name;
519
- if (input.cwd !== void 0) row["cwd"] = isLogicalCwd(input.cwd) ? input.cwd : expandPath(input.cwd);
534
+ if (input.cwd !== void 0) row["cwd"] = isLogicalCwd(input.cwd) ? input.cwd : toLogicalCwd(expandPath(input.cwd));
520
535
  if (input.categoryId !== void 0) row["category_id"] = input.categoryId;
521
536
  if (input.externalChannelId !== void 0) row["external_channel_id"] = input.externalChannelId;
522
537
  if (input.model !== void 0) row["model"] = input.model;
@@ -616,6 +631,12 @@ async function updateChannelById(id, patch) {
616
631
  await refreshChannelRegistry();
617
632
  return rowToChannel(data);
618
633
  }
634
+ async function markChannelGreeted(id) {
635
+ const { data, error } = await getSupabaseClient().from("channels").update({ greeted_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).is("greeted_at", null).select("id");
636
+ if (error) throw new Error(error.message);
637
+ await refreshChannelRegistry();
638
+ return (data ?? []).length > 0;
639
+ }
619
640
  async function updateChannelsMcpConnections(updates) {
620
641
  if (!updates.length) return;
621
642
  const supabase = getSupabaseClient();
@@ -2575,14 +2596,15 @@ var workspaceCache = /* @__PURE__ */ new Map();
2575
2596
  async function getWorkspaceBySlug(slug) {
2576
2597
  const cached = workspaceCache.get(slug);
2577
2598
  if (cached && cached.expiresAt > Date.now()) return cached.workspace;
2578
- const { data, error } = await getSupabaseClient().from("workspaces").select("id, slug, agent_connected_at, projects_setup_at, plan").eq("slug", slug).maybeSingle();
2599
+ const { data, error } = await getSupabaseClient().from("workspaces").select("id, slug, agent_connected_at, projects_setup_at, plan, archived_at").eq("slug", slug).maybeSingle();
2579
2600
  if (error) throw new Error(`getWorkspaceBySlug(${slug}): ${error.message}`);
2580
2601
  const workspace = data ? {
2581
2602
  id: data.id,
2582
2603
  slug: data.slug,
2583
2604
  agentConnectedAt: data.agent_connected_at,
2584
2605
  projectsSetupAt: data.projects_setup_at,
2585
- plan: data.plan ?? null
2606
+ plan: data.plan ?? null,
2607
+ archivedAt: data.archived_at ?? null
2586
2608
  } : null;
2587
2609
  workspaceCache.set(slug, { workspace, expiresAt: Date.now() + WORKSPACE_CACHE_TTL_MS });
2588
2610
  return workspace;
@@ -2597,29 +2619,70 @@ async function listWorkspaces() {
2597
2619
  if (error) throw new Error(`listWorkspaces: ${error.message}`);
2598
2620
  return data ?? [];
2599
2621
  }
2622
+ var TRIAL_DAYS = 14;
2600
2623
  async function listWorkspaceSummaries() {
2601
2624
  const db = getSupabaseClient();
2602
- const [workspaces, settings, channels, memberships] = await Promise.all([
2603
- db.from("workspaces").select("id, slug"),
2604
- db.from("workspace_settings").select("profile, display_name"),
2625
+ const [workspaces, settings, channels, memberships, activity] = await Promise.all([
2626
+ db.from("workspaces").select("id, slug, created_at, agent_connected_at, plan, plan_status, archived_at"),
2627
+ db.from("workspace_settings").select("profile, display_name, logo"),
2605
2628
  db.from("channels").select("profile").is("archived_at", null),
2606
- db.from("memberships").select("workspace_id").eq("status", "active")
2629
+ db.from("memberships").select("workspace_id, user_id, email, handle, role, accepted_at").eq("status", "active").eq("is_support", false).order("invited_at", { ascending: true }),
2630
+ db.rpc("workspace_last_activity")
2607
2631
  ]);
2608
- for (const r of [workspaces, settings, channels, memberships]) {
2632
+ for (const r of [workspaces, settings, channels, memberships, activity]) {
2609
2633
  if (r.error) throw new Error(`listWorkspaceSummaries: ${r.error.message}`);
2610
2634
  }
2611
2635
  const displayNames = new Map(
2612
2636
  (settings.data ?? []).map((s) => [s.profile, s.display_name])
2613
2637
  );
2638
+ const logos = new Map(
2639
+ (settings.data ?? []).map((s) => [s.profile, s.logo])
2640
+ );
2641
+ const lastActivity = new Map(
2642
+ (activity.data ?? []).map((a) => [
2643
+ a.profile,
2644
+ a.last_activity_at
2645
+ ])
2646
+ );
2614
2647
  const tally = (keys) => keys.reduce((m, k) => m.set(k, (m.get(k) ?? 0) + 1), /* @__PURE__ */ new Map());
2615
2648
  const channelCounts = tally((channels.data ?? []).map((c) => c.profile));
2616
2649
  const memberCounts = tally((memberships.data ?? []).map((m) => m.workspace_id));
2617
- return (workspaces.data ?? []).map((w) => ({
2618
- ...w,
2619
- displayName: displayNames.get(w.slug) || null,
2620
- channelCount: channelCounts.get(w.slug) ?? 0,
2621
- memberCount: memberCounts.get(w.id) ?? 0
2622
- }));
2650
+ const owners = /* @__PURE__ */ new Map();
2651
+ for (const m of memberships.data ?? []) {
2652
+ if (m.role !== "admin" || owners.has(m.workspace_id)) continue;
2653
+ owners.set(m.workspace_id, {
2654
+ userId: m.user_id,
2655
+ email: m.email,
2656
+ handle: m.handle,
2657
+ joinedAt: m.accepted_at ?? null
2658
+ });
2659
+ }
2660
+ return (workspaces.data ?? []).map((w) => {
2661
+ const createdAt = w.created_at;
2662
+ const planStatus = w.plan_status ?? null;
2663
+ return {
2664
+ id: w.id,
2665
+ slug: w.slug,
2666
+ createdAt,
2667
+ agentConnectedAt: w.agent_connected_at,
2668
+ plan: w.plan ?? null,
2669
+ planStatus,
2670
+ trialEndsAt: planStatus === "trialing" ? new Date(new Date(createdAt).getTime() + TRIAL_DAYS * 864e5).toISOString() : null,
2671
+ archivedAt: w.archived_at ?? null,
2672
+ displayName: displayNames.get(w.slug) || null,
2673
+ logo: logos.get(w.slug) || null,
2674
+ channelCount: channelCounts.get(w.slug) ?? 0,
2675
+ memberCount: memberCounts.get(w.id) ?? 0,
2676
+ lastActivityAt: lastActivity.get(w.slug) ?? null,
2677
+ owner: owners.get(w.id) ?? null
2678
+ };
2679
+ });
2680
+ }
2681
+ async function setWorkspaceArchived(workspaceId, archived) {
2682
+ const { data, error } = await getSupabaseClient().from("workspaces").update({ archived_at: archived ? (/* @__PURE__ */ new Date()).toISOString() : null }).eq("id", workspaceId).select("slug");
2683
+ if (error) throw new Error(`setWorkspaceArchived(${workspaceId}): ${error.message}`);
2684
+ if (!data?.length) throw new Error("workspace not found");
2685
+ for (const row of data) workspaceCache.delete(row.slug);
2623
2686
  }
2624
2687
  async function createWorkspace(slug) {
2625
2688
  const { data, error } = await getSupabaseClient().from("workspaces").insert({ slug }).select("id, slug").single();
@@ -5383,6 +5446,29 @@ async function forkThreadWithSummary(ctx, deps) {
5383
5446
  return { forked: true, newThreadTs };
5384
5447
  }
5385
5448
 
5449
+ // src/slack/reply-language.ts
5450
+ var CJK = /[぀-ヿ㐀-䶿一-鿿가-힯豈-﫿]/g;
5451
+ var CJK_THRESHOLD = 10;
5452
+ var REWRITE_PROMPT = "[system: this Slack channel is English-only. Your previous reply was NOT posted because it was not in English. Rewrite that same reply in English (en-AU spelling): same content, same structure, and keep any [BUTTONS: ...] marker with its labels translated. Do not run any tools and do not mention this rewrite. Output only the rewritten reply.]";
5453
+ function isEnglishOnlyChannel(config, channelId) {
5454
+ return config.slackEnglishOnly && !config.slackEnglishOnlyExemptChannelIds.has(channelId);
5455
+ }
5456
+ function isNonEnglishReply(text) {
5457
+ const prose = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
5458
+ return (prose.match(CJK)?.length ?? 0) >= CJK_THRESHOLD;
5459
+ }
5460
+ async function enforceEnglishReply(text, rewrite) {
5461
+ if (!isNonEnglishReply(text)) return text;
5462
+ try {
5463
+ const rewritten = await rewrite(REWRITE_PROMPT);
5464
+ if (rewritten?.trim()) return rewritten;
5465
+ console.error("[reply-language] rewrite returned nothing \u2014 posting the original");
5466
+ } catch (err) {
5467
+ console.error("[reply-language] rewrite failed \u2014 posting the original:", err);
5468
+ }
5469
+ return text;
5470
+ }
5471
+
5386
5472
  // src/store/identity-map.ts
5387
5473
  function identityHandleFor(slackUserId) {
5388
5474
  const raw = process.env.SLACK_IDENTITY_MAP;
@@ -6549,7 +6635,12 @@ ${trimmedDetail.slice(0, 1500)}
6549
6635
  if (codexResponse.sessionId) {
6550
6636
  store.saveSession(channelId, sessionKey, codexResponse.sessionId, "codex");
6551
6637
  }
6552
- const { cleanedText: cleanedText2, buttons: buttons2 } = extractButtons(codexResponse.text);
6638
+ const codexSessionId = codexResponse.sessionId;
6639
+ const codexText = codexSessionId && isEnglishOnlyChannel(config, channelId) ? await enforceEnglishReply(codexResponse.text, async (p) => {
6640
+ const r = await askCodex(p, cwd, codexOptions, codexSessionId);
6641
+ return r.isError ? null : r.text;
6642
+ }) : codexResponse.text;
6643
+ const { cleanedText: cleanedText2, buttons: buttons2 } = extractButtons(codexText);
6553
6644
  const actionsBlock2 = buttons2 ? buildActionsBlock(buttons2) : null;
6554
6645
  const rtBlocks2 = markdownToBlocks(cleanedText2);
6555
6646
  const messages2 = rtBlocks2.length > 0 ? rtBlocks2.map((b) => ({ blocks: [b], text: blockToFallbackText(b) })) : splitMessage(toSlackMarkdown(cleanedText2)).map((text) => ({ text }));
@@ -6740,7 +6831,21 @@ retry: ${retryDetail}`);
6740
6831
  } else {
6741
6832
  store.saveSession(channelId, sessionKey, resolved.sessionId);
6742
6833
  }
6743
- const { cleanedText, buttons } = extractButtons(resolved.text);
6834
+ const replyText = !resolved.isError && resolved.sessionId && isEnglishOnlyChannel(config, channelId) ? await enforceEnglishReply(resolved.text, async (p) => {
6835
+ const r = await askClaude(
6836
+ p,
6837
+ cwd,
6838
+ botName,
6839
+ askOptions,
6840
+ claudeConfigDir,
6841
+ resolved.sessionId,
6842
+ claudeCodeOauthToken
6843
+ );
6844
+ if (r.isError) return null;
6845
+ store.saveSession(channelId, sessionKey, r.sessionId);
6846
+ return r.text;
6847
+ }) : resolved.text;
6848
+ const { cleanedText, buttons } = extractButtons(replyText);
6744
6849
  const actionsBlock = buttons ? buildActionsBlock(buttons) : null;
6745
6850
  const rtBlocks = markdownToBlocks(cleanedText);
6746
6851
  const messages = rtBlocks.length > 0 ? rtBlocks.map((b) => ({ blocks: [b], text: blockToFallbackText(b) })) : splitMessage(toSlackMarkdown(cleanedText)).map((text) => ({ text }));
@@ -6757,7 +6862,7 @@ retry: ${retryDetail}`);
6757
6862
  }
6758
6863
  if (debug4) {
6759
6864
  console.log(
6760
- `[response] length=${resolved.text.length} messages=${messages.length} buttons=${buttons?.length ?? 0}`
6865
+ `[response] length=${replyText.length} messages=${messages.length} buttons=${buttons?.length ?? 0}`
6761
6866
  );
6762
6867
  }
6763
6868
  await clearReaction();
@@ -8975,6 +9080,7 @@ export {
8975
9080
  createChannel,
8976
9081
  DefaultChannelError,
8977
9082
  updateChannelById,
9083
+ markChannelGreeted,
8978
9084
  updateChannelsMcpConnections,
8979
9085
  ensureAgentDmChannel,
8980
9086
  ensureGeneralChannel,
@@ -9035,7 +9141,9 @@ export {
9035
9141
  getWorkspaceBySlug,
9036
9142
  getWorkspaceById,
9037
9143
  listWorkspaces,
9144
+ TRIAL_DAYS,
9038
9145
  listWorkspaceSummaries,
9146
+ setWorkspaceArchived,
9039
9147
  createWorkspace,
9040
9148
  deleteWorkspace,
9041
9149
  setWorkspaceStripeSubscription,