flyspace-cli 0.1.31 → 0.1.33
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/{chunk-6YFDWUNF.js → chunk-KGX44L3V.js} +116 -17
- package/dist/cli.js +415 -82
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
|
@@ -356,6 +361,7 @@ function rowToChannel(row) {
|
|
|
356
361
|
isDefault: row.is_default === true,
|
|
357
362
|
profile: row.profile ?? "default",
|
|
358
363
|
archivedAt: row.archived_at,
|
|
364
|
+
greetedAt: row.greeted_at,
|
|
359
365
|
createdAt: row.created_at,
|
|
360
366
|
updatedAt: row.updated_at
|
|
361
367
|
};
|
|
@@ -616,6 +622,12 @@ async function updateChannelById(id, patch) {
|
|
|
616
622
|
await refreshChannelRegistry();
|
|
617
623
|
return rowToChannel(data);
|
|
618
624
|
}
|
|
625
|
+
async function markChannelGreeted(id) {
|
|
626
|
+
const { data, error } = await getSupabaseClient().from("channels").update({ greeted_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).is("greeted_at", null).select("id");
|
|
627
|
+
if (error) throw new Error(error.message);
|
|
628
|
+
await refreshChannelRegistry();
|
|
629
|
+
return (data ?? []).length > 0;
|
|
630
|
+
}
|
|
619
631
|
async function updateChannelsMcpConnections(updates) {
|
|
620
632
|
if (!updates.length) return;
|
|
621
633
|
const supabase = getSupabaseClient();
|
|
@@ -2575,14 +2587,15 @@ var workspaceCache = /* @__PURE__ */ new Map();
|
|
|
2575
2587
|
async function getWorkspaceBySlug(slug) {
|
|
2576
2588
|
const cached = workspaceCache.get(slug);
|
|
2577
2589
|
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();
|
|
2590
|
+
const { data, error } = await getSupabaseClient().from("workspaces").select("id, slug, agent_connected_at, projects_setup_at, plan, archived_at").eq("slug", slug).maybeSingle();
|
|
2579
2591
|
if (error) throw new Error(`getWorkspaceBySlug(${slug}): ${error.message}`);
|
|
2580
2592
|
const workspace = data ? {
|
|
2581
2593
|
id: data.id,
|
|
2582
2594
|
slug: data.slug,
|
|
2583
2595
|
agentConnectedAt: data.agent_connected_at,
|
|
2584
2596
|
projectsSetupAt: data.projects_setup_at,
|
|
2585
|
-
plan: data.plan ?? null
|
|
2597
|
+
plan: data.plan ?? null,
|
|
2598
|
+
archivedAt: data.archived_at ?? null
|
|
2586
2599
|
} : null;
|
|
2587
2600
|
workspaceCache.set(slug, { workspace, expiresAt: Date.now() + WORKSPACE_CACHE_TTL_MS });
|
|
2588
2601
|
return workspace;
|
|
@@ -2597,29 +2610,70 @@ async function listWorkspaces() {
|
|
|
2597
2610
|
if (error) throw new Error(`listWorkspaces: ${error.message}`);
|
|
2598
2611
|
return data ?? [];
|
|
2599
2612
|
}
|
|
2613
|
+
var TRIAL_DAYS = 14;
|
|
2600
2614
|
async function listWorkspaceSummaries() {
|
|
2601
2615
|
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"),
|
|
2616
|
+
const [workspaces, settings, channels, memberships, activity] = await Promise.all([
|
|
2617
|
+
db.from("workspaces").select("id, slug, created_at, agent_connected_at, plan, plan_status, archived_at"),
|
|
2618
|
+
db.from("workspace_settings").select("profile, display_name, logo"),
|
|
2605
2619
|
db.from("channels").select("profile").is("archived_at", null),
|
|
2606
|
-
db.from("memberships").select("workspace_id").eq("status", "active")
|
|
2620
|
+
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 }),
|
|
2621
|
+
db.rpc("workspace_last_activity")
|
|
2607
2622
|
]);
|
|
2608
|
-
for (const r of [workspaces, settings, channels, memberships]) {
|
|
2623
|
+
for (const r of [workspaces, settings, channels, memberships, activity]) {
|
|
2609
2624
|
if (r.error) throw new Error(`listWorkspaceSummaries: ${r.error.message}`);
|
|
2610
2625
|
}
|
|
2611
2626
|
const displayNames = new Map(
|
|
2612
2627
|
(settings.data ?? []).map((s) => [s.profile, s.display_name])
|
|
2613
2628
|
);
|
|
2629
|
+
const logos = new Map(
|
|
2630
|
+
(settings.data ?? []).map((s) => [s.profile, s.logo])
|
|
2631
|
+
);
|
|
2632
|
+
const lastActivity = new Map(
|
|
2633
|
+
(activity.data ?? []).map((a) => [
|
|
2634
|
+
a.profile,
|
|
2635
|
+
a.last_activity_at
|
|
2636
|
+
])
|
|
2637
|
+
);
|
|
2614
2638
|
const tally = (keys) => keys.reduce((m, k) => m.set(k, (m.get(k) ?? 0) + 1), /* @__PURE__ */ new Map());
|
|
2615
2639
|
const channelCounts = tally((channels.data ?? []).map((c) => c.profile));
|
|
2616
2640
|
const memberCounts = tally((memberships.data ?? []).map((m) => m.workspace_id));
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2641
|
+
const owners = /* @__PURE__ */ new Map();
|
|
2642
|
+
for (const m of memberships.data ?? []) {
|
|
2643
|
+
if (m.role !== "admin" || owners.has(m.workspace_id)) continue;
|
|
2644
|
+
owners.set(m.workspace_id, {
|
|
2645
|
+
userId: m.user_id,
|
|
2646
|
+
email: m.email,
|
|
2647
|
+
handle: m.handle,
|
|
2648
|
+
joinedAt: m.accepted_at ?? null
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
return (workspaces.data ?? []).map((w) => {
|
|
2652
|
+
const createdAt = w.created_at;
|
|
2653
|
+
const planStatus = w.plan_status ?? null;
|
|
2654
|
+
return {
|
|
2655
|
+
id: w.id,
|
|
2656
|
+
slug: w.slug,
|
|
2657
|
+
createdAt,
|
|
2658
|
+
agentConnectedAt: w.agent_connected_at,
|
|
2659
|
+
plan: w.plan ?? null,
|
|
2660
|
+
planStatus,
|
|
2661
|
+
trialEndsAt: planStatus === "trialing" ? new Date(new Date(createdAt).getTime() + TRIAL_DAYS * 864e5).toISOString() : null,
|
|
2662
|
+
archivedAt: w.archived_at ?? null,
|
|
2663
|
+
displayName: displayNames.get(w.slug) || null,
|
|
2664
|
+
logo: logos.get(w.slug) || null,
|
|
2665
|
+
channelCount: channelCounts.get(w.slug) ?? 0,
|
|
2666
|
+
memberCount: memberCounts.get(w.id) ?? 0,
|
|
2667
|
+
lastActivityAt: lastActivity.get(w.slug) ?? null,
|
|
2668
|
+
owner: owners.get(w.id) ?? null
|
|
2669
|
+
};
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
async function setWorkspaceArchived(workspaceId, archived) {
|
|
2673
|
+
const { data, error } = await getSupabaseClient().from("workspaces").update({ archived_at: archived ? (/* @__PURE__ */ new Date()).toISOString() : null }).eq("id", workspaceId).select("slug");
|
|
2674
|
+
if (error) throw new Error(`setWorkspaceArchived(${workspaceId}): ${error.message}`);
|
|
2675
|
+
if (!data?.length) throw new Error("workspace not found");
|
|
2676
|
+
for (const row of data) workspaceCache.delete(row.slug);
|
|
2623
2677
|
}
|
|
2624
2678
|
async function createWorkspace(slug) {
|
|
2625
2679
|
const { data, error } = await getSupabaseClient().from("workspaces").insert({ slug }).select("id, slug").single();
|
|
@@ -5383,6 +5437,29 @@ async function forkThreadWithSummary(ctx, deps) {
|
|
|
5383
5437
|
return { forked: true, newThreadTs };
|
|
5384
5438
|
}
|
|
5385
5439
|
|
|
5440
|
+
// src/slack/reply-language.ts
|
|
5441
|
+
var CJK = /[-ヿ㐀-䶿一-鿿가-豈-]/g;
|
|
5442
|
+
var CJK_THRESHOLD = 10;
|
|
5443
|
+
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.]";
|
|
5444
|
+
function isEnglishOnlyChannel(config, channelId) {
|
|
5445
|
+
return config.slackEnglishOnly && !config.slackEnglishOnlyExemptChannelIds.has(channelId);
|
|
5446
|
+
}
|
|
5447
|
+
function isNonEnglishReply(text) {
|
|
5448
|
+
const prose = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
|
|
5449
|
+
return (prose.match(CJK)?.length ?? 0) >= CJK_THRESHOLD;
|
|
5450
|
+
}
|
|
5451
|
+
async function enforceEnglishReply(text, rewrite) {
|
|
5452
|
+
if (!isNonEnglishReply(text)) return text;
|
|
5453
|
+
try {
|
|
5454
|
+
const rewritten = await rewrite(REWRITE_PROMPT);
|
|
5455
|
+
if (rewritten?.trim()) return rewritten;
|
|
5456
|
+
console.error("[reply-language] rewrite returned nothing \u2014 posting the original");
|
|
5457
|
+
} catch (err) {
|
|
5458
|
+
console.error("[reply-language] rewrite failed \u2014 posting the original:", err);
|
|
5459
|
+
}
|
|
5460
|
+
return text;
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5386
5463
|
// src/store/identity-map.ts
|
|
5387
5464
|
function identityHandleFor(slackUserId) {
|
|
5388
5465
|
const raw = process.env.SLACK_IDENTITY_MAP;
|
|
@@ -6549,7 +6626,12 @@ ${trimmedDetail.slice(0, 1500)}
|
|
|
6549
6626
|
if (codexResponse.sessionId) {
|
|
6550
6627
|
store.saveSession(channelId, sessionKey, codexResponse.sessionId, "codex");
|
|
6551
6628
|
}
|
|
6552
|
-
const
|
|
6629
|
+
const codexSessionId = codexResponse.sessionId;
|
|
6630
|
+
const codexText = codexSessionId && isEnglishOnlyChannel(config, channelId) ? await enforceEnglishReply(codexResponse.text, async (p) => {
|
|
6631
|
+
const r = await askCodex(p, cwd, codexOptions, codexSessionId);
|
|
6632
|
+
return r.isError ? null : r.text;
|
|
6633
|
+
}) : codexResponse.text;
|
|
6634
|
+
const { cleanedText: cleanedText2, buttons: buttons2 } = extractButtons(codexText);
|
|
6553
6635
|
const actionsBlock2 = buttons2 ? buildActionsBlock(buttons2) : null;
|
|
6554
6636
|
const rtBlocks2 = markdownToBlocks(cleanedText2);
|
|
6555
6637
|
const messages2 = rtBlocks2.length > 0 ? rtBlocks2.map((b) => ({ blocks: [b], text: blockToFallbackText(b) })) : splitMessage(toSlackMarkdown(cleanedText2)).map((text) => ({ text }));
|
|
@@ -6740,7 +6822,21 @@ retry: ${retryDetail}`);
|
|
|
6740
6822
|
} else {
|
|
6741
6823
|
store.saveSession(channelId, sessionKey, resolved.sessionId);
|
|
6742
6824
|
}
|
|
6743
|
-
const
|
|
6825
|
+
const replyText = !resolved.isError && resolved.sessionId && isEnglishOnlyChannel(config, channelId) ? await enforceEnglishReply(resolved.text, async (p) => {
|
|
6826
|
+
const r = await askClaude(
|
|
6827
|
+
p,
|
|
6828
|
+
cwd,
|
|
6829
|
+
botName,
|
|
6830
|
+
askOptions,
|
|
6831
|
+
claudeConfigDir,
|
|
6832
|
+
resolved.sessionId,
|
|
6833
|
+
claudeCodeOauthToken
|
|
6834
|
+
);
|
|
6835
|
+
if (r.isError) return null;
|
|
6836
|
+
store.saveSession(channelId, sessionKey, r.sessionId);
|
|
6837
|
+
return r.text;
|
|
6838
|
+
}) : resolved.text;
|
|
6839
|
+
const { cleanedText, buttons } = extractButtons(replyText);
|
|
6744
6840
|
const actionsBlock = buttons ? buildActionsBlock(buttons) : null;
|
|
6745
6841
|
const rtBlocks = markdownToBlocks(cleanedText);
|
|
6746
6842
|
const messages = rtBlocks.length > 0 ? rtBlocks.map((b) => ({ blocks: [b], text: blockToFallbackText(b) })) : splitMessage(toSlackMarkdown(cleanedText)).map((text) => ({ text }));
|
|
@@ -6757,7 +6853,7 @@ retry: ${retryDetail}`);
|
|
|
6757
6853
|
}
|
|
6758
6854
|
if (debug4) {
|
|
6759
6855
|
console.log(
|
|
6760
|
-
`[response] length=${
|
|
6856
|
+
`[response] length=${replyText.length} messages=${messages.length} buttons=${buttons?.length ?? 0}`
|
|
6761
6857
|
);
|
|
6762
6858
|
}
|
|
6763
6859
|
await clearReaction();
|
|
@@ -8975,6 +9071,7 @@ export {
|
|
|
8975
9071
|
createChannel,
|
|
8976
9072
|
DefaultChannelError,
|
|
8977
9073
|
updateChannelById,
|
|
9074
|
+
markChannelGreeted,
|
|
8978
9075
|
updateChannelsMcpConnections,
|
|
8979
9076
|
ensureAgentDmChannel,
|
|
8980
9077
|
ensureGeneralChannel,
|
|
@@ -9035,7 +9132,9 @@ export {
|
|
|
9035
9132
|
getWorkspaceBySlug,
|
|
9036
9133
|
getWorkspaceById,
|
|
9037
9134
|
listWorkspaces,
|
|
9135
|
+
TRIAL_DAYS,
|
|
9038
9136
|
listWorkspaceSummaries,
|
|
9137
|
+
setWorkspaceArchived,
|
|
9039
9138
|
createWorkspace,
|
|
9040
9139
|
deleteWorkspace,
|
|
9041
9140
|
setWorkspaceStripeSubscription,
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SKIP_SUBTYPES,
|
|
19
19
|
SLACK_ENV,
|
|
20
20
|
SessionStore,
|
|
21
|
+
TRIAL_DAYS,
|
|
21
22
|
UsageStore,
|
|
22
23
|
activateMembership,
|
|
23
24
|
agentLabel,
|
|
@@ -118,6 +119,7 @@ import {
|
|
|
118
119
|
loadFullDashboardSettings,
|
|
119
120
|
loadWorkspaceBranding,
|
|
120
121
|
loadWorkspacePrompt,
|
|
122
|
+
markChannelGreeted,
|
|
121
123
|
markInviteResent,
|
|
122
124
|
markMemoryUsed,
|
|
123
125
|
markWorkspaceGreeted,
|
|
@@ -159,6 +161,7 @@ import {
|
|
|
159
161
|
setMembershipHandle,
|
|
160
162
|
setMemoryAutoPaused,
|
|
161
163
|
setOutputStyleDefault,
|
|
164
|
+
setWorkspaceArchived,
|
|
162
165
|
setWorkspaceStripeSubscription,
|
|
163
166
|
slackConfigured,
|
|
164
167
|
startAutomationRegistryPolling,
|
|
@@ -184,7 +187,7 @@ import {
|
|
|
184
187
|
warnOnBrokenChannelCwds,
|
|
185
188
|
workspaceSkillsDir,
|
|
186
189
|
writeSkillFile
|
|
187
|
-
} from "./chunk-
|
|
190
|
+
} from "./chunk-KGX44L3V.js";
|
|
188
191
|
import {
|
|
189
192
|
firstReplyBody,
|
|
190
193
|
getThread,
|
|
@@ -1858,7 +1861,7 @@ async function runAuthCmd(args) {
|
|
|
1858
1861
|
}
|
|
1859
1862
|
|
|
1860
1863
|
// ../api/src/version.ts
|
|
1861
|
-
var CLI_VERSION = true ? "0.1.
|
|
1864
|
+
var CLI_VERSION = true ? "0.1.33" : null;
|
|
1862
1865
|
function isOlderVersion(a, b) {
|
|
1863
1866
|
const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
1864
1867
|
const [pa, pb] = [parse(a), parse(b)];
|
|
@@ -2468,7 +2471,7 @@ function npmEnv(target) {
|
|
|
2468
2471
|
return nodeBin ? { ...process.env, PATH: `${nodeBin}:${process.env.PATH ?? ""}` } : process.env;
|
|
2469
2472
|
}
|
|
2470
2473
|
async function runUpgrade() {
|
|
2471
|
-
const current = "0.1.
|
|
2474
|
+
const current = "0.1.33";
|
|
2472
2475
|
console.log(`Current version: ${current}`);
|
|
2473
2476
|
console.log("Checking for updates...\n");
|
|
2474
2477
|
try {
|
|
@@ -3108,10 +3111,14 @@ async function runRestart() {
|
|
|
3108
3111
|
import { spawn as spawn3 } from "child_process";
|
|
3109
3112
|
import { existsSync as existsSync12 } from "fs";
|
|
3110
3113
|
import { join as join12 } from "path";
|
|
3114
|
+
function serviceLogFile(errors) {
|
|
3115
|
+
const suffix = errors ? "-error.log" : ".log";
|
|
3116
|
+
const candidates = ["host", "custie"].map((name) => join12(paths.LOG_DIR, `${name}${suffix}`));
|
|
3117
|
+
return candidates.find((file) => existsSync12(file)) ?? candidates[0];
|
|
3118
|
+
}
|
|
3111
3119
|
async function runLogs(args) {
|
|
3112
3120
|
const showErrors = args.includes("--error") || args.includes("-e");
|
|
3113
|
-
const
|
|
3114
|
-
const logFile = join12(paths.LOG_DIR, filename);
|
|
3121
|
+
const logFile = serviceLogFile(showErrors);
|
|
3115
3122
|
if (!existsSync12(logFile)) {
|
|
3116
3123
|
console.error(`Log file not found: ${logFile}`);
|
|
3117
3124
|
console.error("Is the service installed? Run `flyspace install` first.");
|
|
@@ -4140,7 +4147,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
|
|
|
4140
4147
|
const subscription = await stripe.subscriptions.create({
|
|
4141
4148
|
customer: customer.id,
|
|
4142
4149
|
items: [{ price: priceId }],
|
|
4143
|
-
trial_period_days:
|
|
4150
|
+
trial_period_days: TRIAL_DAYS,
|
|
4144
4151
|
trial_settings: { end_behavior: { missing_payment_method: "pause" } },
|
|
4145
4152
|
metadata: { workspace_id: workspace.id, plan }
|
|
4146
4153
|
});
|
|
@@ -6406,7 +6413,7 @@ async function runMcpCmd(args) {
|
|
|
6406
6413
|
import { spawn as spawn8, spawnSync } from "child_process";
|
|
6407
6414
|
import { existsSync as existsSync33 } from "fs";
|
|
6408
6415
|
import { platform as platform7 } from "os";
|
|
6409
|
-
import { join as
|
|
6416
|
+
import { join as join28 } from "path";
|
|
6410
6417
|
|
|
6411
6418
|
// src/reconcile.ts
|
|
6412
6419
|
import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
@@ -6579,7 +6586,7 @@ var requireDocsAccess = async (c, next) => {
|
|
|
6579
6586
|
};
|
|
6580
6587
|
|
|
6581
6588
|
// ../api/src/dashboard/api.ts
|
|
6582
|
-
import { Hono as
|
|
6589
|
+
import { Hono as Hono30 } from "hono";
|
|
6583
6590
|
|
|
6584
6591
|
// ../api/src/dashboard/public-share-routes.ts
|
|
6585
6592
|
import { existsSync as existsSync17, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
|
|
@@ -8731,7 +8738,17 @@ function pruneExpired(now) {
|
|
|
8731
8738
|
}
|
|
8732
8739
|
var SUPPORT_NAME = "Flyspace Support";
|
|
8733
8740
|
var SUPPORT_AVATAR_URL = "/logo/flyspace-support.svg";
|
|
8741
|
+
function workspaceArchivedError() {
|
|
8742
|
+
return new HTTPException2(403, {
|
|
8743
|
+
res: Response.json({ error: "workspace archived", archived: true }, { status: 403 })
|
|
8744
|
+
});
|
|
8745
|
+
}
|
|
8734
8746
|
async function resolveAuthContext(c, opts = {}) {
|
|
8747
|
+
const ctx = await resolveAuthContextCached(c, opts);
|
|
8748
|
+
if (ctx?.archived && !ctx.isSupport) throw workspaceArchivedError();
|
|
8749
|
+
return ctx;
|
|
8750
|
+
}
|
|
8751
|
+
async function resolveAuthContextCached(c, opts) {
|
|
8735
8752
|
const cookie = c.req?.header("Cookie") ?? "";
|
|
8736
8753
|
if (!cookie) return resolveAuthContextUncached(c);
|
|
8737
8754
|
const key = `${getProfile()}\0${cookie}`;
|
|
@@ -8796,7 +8813,8 @@ async function resolveAuthContextUncached(c) {
|
|
|
8796
8813
|
passwordSet: Boolean(metadata.passwordSetAt),
|
|
8797
8814
|
agentConnectedAt: workspace.agentConnectedAt ?? null,
|
|
8798
8815
|
projectsSetupAt: workspace.projectsSetupAt ?? null,
|
|
8799
|
-
plan: workspace.plan ?? null
|
|
8816
|
+
plan: workspace.plan ?? null,
|
|
8817
|
+
...workspace.archivedAt ? { archived: true } : {}
|
|
8800
8818
|
};
|
|
8801
8819
|
}
|
|
8802
8820
|
|
|
@@ -9112,11 +9130,19 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
|
9112
9130
|
// ../api/src/dashboard/uploads-routes.ts
|
|
9113
9131
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
9114
9132
|
import { Hono as Hono7 } from "hono";
|
|
9133
|
+
import sharp4 from "sharp";
|
|
9115
9134
|
import { PutObjectCommand as PutObjectCommand2, GetObjectCommand as GetObjectCommand3 } from "@aws-sdk/client-s3";
|
|
9116
9135
|
import { getSignedUrl as getSignedUrl3 } from "@aws-sdk/s3-request-presigner";
|
|
9117
9136
|
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
9118
9137
|
var PRESIGN_TTL_SECONDS2 = 300;
|
|
9119
9138
|
var KEY_PREFIX = "chat";
|
|
9139
|
+
var RESIZABLE = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/avif"]);
|
|
9140
|
+
var FULL_EDGE = 2560;
|
|
9141
|
+
var THUMB_EDGE = 480;
|
|
9142
|
+
var WEBP_QUALITY = 80;
|
|
9143
|
+
function toWebp(input2, edge) {
|
|
9144
|
+
return sharp4(input2).rotate().resize({ width: edge, height: edge, fit: "inside", withoutEnlargement: true }).webp({ quality: WEBP_QUALITY }).toBuffer();
|
|
9145
|
+
}
|
|
9120
9146
|
var READABLE_PREFIXES = [KEY_PREFIX, SCRATCH_PREFIX];
|
|
9121
9147
|
function sanitizeFilename(name) {
|
|
9122
9148
|
const cleaned = name.replace(/[/\\]/g, "_").replace(/\s+/g, "-").replace(/[^\w.-]/g, "").trim();
|
|
@@ -9140,22 +9166,33 @@ function createUploadsRouter() {
|
|
|
9140
9166
|
if (file.size > MAX_FILE_BYTES) {
|
|
9141
9167
|
return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
|
|
9142
9168
|
}
|
|
9143
|
-
const
|
|
9144
|
-
const
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
)
|
|
9169
|
+
const id = `${KEY_PREFIX}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
|
|
9170
|
+
const original = Buffer.from(await file.arrayBuffer());
|
|
9171
|
+
const put = (Key, Body, ContentType) => getR2Client().send(new PutObjectCommand2({ Bucket: getR2Bucket(), Key, Body, ContentType }));
|
|
9172
|
+
let key = id;
|
|
9173
|
+
let size = file.size;
|
|
9174
|
+
let type = file.type;
|
|
9175
|
+
const renditions = RESIZABLE.has(file.type) ? await Promise.all([toWebp(original, FULL_EDGE), toWebp(original, THUMB_EDGE)]).catch(
|
|
9176
|
+
() => null
|
|
9177
|
+
) : null;
|
|
9178
|
+
if (renditions) {
|
|
9179
|
+
const [full, thumb] = renditions;
|
|
9180
|
+
key = `${id.replace(/\.[^./]*$/, "")}.webp`;
|
|
9181
|
+
size = full.length;
|
|
9182
|
+
type = "image/webp";
|
|
9183
|
+
await Promise.all([
|
|
9184
|
+
put(key, full, type),
|
|
9185
|
+
put(key.replace(/\.webp$/, ".thumb.webp"), thumb, type)
|
|
9186
|
+
]);
|
|
9187
|
+
} else {
|
|
9188
|
+
await put(key, original, file.type || "application/octet-stream");
|
|
9189
|
+
}
|
|
9153
9190
|
return c.json(
|
|
9154
9191
|
{
|
|
9155
9192
|
url: `/api/attachments/${encodeKeyForUrl(key)}`,
|
|
9156
9193
|
name: file.name,
|
|
9157
|
-
type
|
|
9158
|
-
size
|
|
9194
|
+
type,
|
|
9195
|
+
size
|
|
9159
9196
|
},
|
|
9160
9197
|
201
|
|
9161
9198
|
);
|
|
@@ -9540,12 +9577,94 @@ function createAuthRouter() {
|
|
|
9540
9577
|
return auth;
|
|
9541
9578
|
}
|
|
9542
9579
|
|
|
9580
|
+
// ../api/src/store/support-store.ts
|
|
9581
|
+
var SUPPORT_HANDLE = "flyspace-support";
|
|
9582
|
+
async function logSupportAction(entry) {
|
|
9583
|
+
const { error: error2 } = await getSupabaseClient().from("support_access_log").insert({
|
|
9584
|
+
workspace_id: entry.workspaceId,
|
|
9585
|
+
staff_user_id: entry.staffUserId,
|
|
9586
|
+
staff_email: entry.staffEmail,
|
|
9587
|
+
action: entry.action,
|
|
9588
|
+
detail: entry.detail ?? null
|
|
9589
|
+
});
|
|
9590
|
+
if (error2) throw new Error(`logSupportAction(${entry.action}): ${error2.message}`);
|
|
9591
|
+
}
|
|
9592
|
+
async function listSupportWorkspaceIds(userId) {
|
|
9593
|
+
const { data, error: error2 } = await getSupabaseClient().from("memberships").select("workspace_id").eq("user_id", userId).eq("is_support", true);
|
|
9594
|
+
if (error2) throw new Error(`listSupportWorkspaceIds: ${error2.message}`);
|
|
9595
|
+
return new Set((data ?? []).map((r) => r.workspace_id));
|
|
9596
|
+
}
|
|
9597
|
+
async function grantSupportMembership(params) {
|
|
9598
|
+
const db = getSupabaseClient();
|
|
9599
|
+
const { data: existing, error: readError } = await db.from("memberships").select("is_support").eq("workspace_id", params.workspaceId).eq("user_id", params.userId).maybeSingle();
|
|
9600
|
+
if (readError) throw new Error(`grantSupportMembership: ${readError.message}`);
|
|
9601
|
+
if (existing) {
|
|
9602
|
+
if (existing.is_support === true) return "existing";
|
|
9603
|
+
throw new Error("already a regular member of this workspace");
|
|
9604
|
+
}
|
|
9605
|
+
const { data: taken } = await db.from("memberships").select("id").eq("workspace_id", params.workspaceId).eq("handle", SUPPORT_HANDLE).maybeSingle();
|
|
9606
|
+
const handle = taken ? `${SUPPORT_HANDLE}-${params.userId.slice(0, 8)}` : SUPPORT_HANDLE;
|
|
9607
|
+
const { error: error2 } = await db.from("memberships").insert({
|
|
9608
|
+
workspace_id: params.workspaceId,
|
|
9609
|
+
user_id: params.userId,
|
|
9610
|
+
email: params.email,
|
|
9611
|
+
handle,
|
|
9612
|
+
role: "admin",
|
|
9613
|
+
status: "active",
|
|
9614
|
+
is_support: true,
|
|
9615
|
+
accepted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9616
|
+
});
|
|
9617
|
+
if (error2) throw new Error(`grantSupportMembership: ${error2.message}`);
|
|
9618
|
+
return "granted";
|
|
9619
|
+
}
|
|
9620
|
+
async function revokeSupportMembership(workspaceId, userId) {
|
|
9621
|
+
const { data, error: error2 } = await getSupabaseClient().from("memberships").delete().eq("workspace_id", workspaceId).eq("user_id", userId).eq("is_support", true).select("id");
|
|
9622
|
+
if (error2) throw new Error(`revokeSupportMembership: ${error2.message}`);
|
|
9623
|
+
return (data ?? []).length > 0;
|
|
9624
|
+
}
|
|
9625
|
+
async function listSupportLog(filter) {
|
|
9626
|
+
let query = getSupabaseClient().from("support_access_log").select("*").order("created_at", { ascending: false }).limit(Math.min(filter.limit ?? 200, 1e3));
|
|
9627
|
+
if (filter.workspaceId) query = query.eq("workspace_id", filter.workspaceId);
|
|
9628
|
+
const { data, error: error2 } = await query;
|
|
9629
|
+
if (error2) throw new Error(`listSupportLog: ${error2.message}`);
|
|
9630
|
+
return (data ?? []).map((r) => ({
|
|
9631
|
+
id: r.id,
|
|
9632
|
+
workspaceId: r.workspace_id,
|
|
9633
|
+
staffUserId: r.staff_user_id,
|
|
9634
|
+
staffEmail: r.staff_email,
|
|
9635
|
+
action: r.action,
|
|
9636
|
+
detail: r.detail ?? null,
|
|
9637
|
+
createdAt: r.created_at
|
|
9638
|
+
}));
|
|
9639
|
+
}
|
|
9640
|
+
|
|
9543
9641
|
// ../api/src/dashboard/auth-middleware.ts
|
|
9642
|
+
var ENTER_WINDOW_MS = 8 * 60 * 60 * 1e3;
|
|
9643
|
+
var lastEnter = /* @__PURE__ */ new Map();
|
|
9644
|
+
function auditSupport(ctx, action, detail) {
|
|
9645
|
+
logSupportAction({
|
|
9646
|
+
workspaceId: ctx.workspaceId,
|
|
9647
|
+
staffUserId: ctx.userId,
|
|
9648
|
+
staffEmail: ctx.email,
|
|
9649
|
+
action,
|
|
9650
|
+
detail
|
|
9651
|
+
}).catch((err) => console.error("[support-audit]", err.message));
|
|
9652
|
+
}
|
|
9544
9653
|
var requireWorkspaceMembership = async (c, next) => {
|
|
9545
9654
|
const ctx = await resolveAuthContext(c);
|
|
9546
9655
|
if (!ctx) return c.json({ error: "not signed in" }, 401);
|
|
9547
9656
|
c.set("auth", ctx);
|
|
9548
|
-
return next();
|
|
9657
|
+
if (!ctx.isSupport) return next();
|
|
9658
|
+
const key = `${ctx.userId}:${ctx.workspaceId}`;
|
|
9659
|
+
const now = Date.now();
|
|
9660
|
+
if ((lastEnter.get(key) ?? 0) + ENTER_WINDOW_MS < now) {
|
|
9661
|
+
lastEnter.set(key, now);
|
|
9662
|
+
auditSupport(ctx, "enter");
|
|
9663
|
+
}
|
|
9664
|
+
await next();
|
|
9665
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(c.req.method)) {
|
|
9666
|
+
auditSupport(ctx, "write", { method: c.req.method, path: c.req.path, status: c.res.status });
|
|
9667
|
+
}
|
|
9549
9668
|
};
|
|
9550
9669
|
|
|
9551
9670
|
// ../api/src/dashboard/billing-routes.ts
|
|
@@ -9650,7 +9769,14 @@ function createAdminRouter() {
|
|
|
9650
9769
|
const admin = new Hono11();
|
|
9651
9770
|
admin.use("*", requirePlatformOperator);
|
|
9652
9771
|
admin.get("/workspaces", async (c) => {
|
|
9653
|
-
const
|
|
9772
|
+
const operator = c.get("platformOperator");
|
|
9773
|
+
const [summaries, supportIds, users] = await Promise.all([
|
|
9774
|
+
listWorkspaceSummaries(),
|
|
9775
|
+
listSupportWorkspaceIds(operator.userId),
|
|
9776
|
+
// One call for every owner's profile metadata instead of one per row.
|
|
9777
|
+
getSupabaseClient().auth.admin.listUsers({ perPage: 1e3 })
|
|
9778
|
+
]);
|
|
9779
|
+
const usersById = new Map((users.data?.users ?? []).map((u) => [u.id, u]));
|
|
9654
9780
|
const workspaces = await Promise.all(
|
|
9655
9781
|
summaries.map(async (w) => {
|
|
9656
9782
|
const agentModel = cachedProfileConfig(w.slug)?.agentModel;
|
|
@@ -9660,11 +9786,23 @@ function createAdminRouter() {
|
|
|
9660
9786
|
console.error(`[admin] R2 usage for ${w.slug}:`, err.message);
|
|
9661
9787
|
return null;
|
|
9662
9788
|
});
|
|
9789
|
+
const metadata = w.owner ? usersById.get(w.owner.userId)?.user_metadata : void 0;
|
|
9790
|
+
const owner = w.owner && {
|
|
9791
|
+
...w.owner,
|
|
9792
|
+
role: "admin",
|
|
9793
|
+
avatarUrl: metadata?.avatarKey ? await signAvatarUrl(metadata.avatarKey) : null,
|
|
9794
|
+
name: metadata?.name ?? null,
|
|
9795
|
+
title: metadata?.title ?? null,
|
|
9796
|
+
bio: metadata?.bio ?? null,
|
|
9797
|
+
timeZone: metadata?.timeZone ?? null
|
|
9798
|
+
};
|
|
9663
9799
|
return {
|
|
9664
9800
|
...w,
|
|
9801
|
+
owner,
|
|
9665
9802
|
agent: agentModel ? { runner: runnerForModel(agentModel), model: agentModel } : null,
|
|
9666
9803
|
r2Bytes,
|
|
9667
|
-
flyVolumeBytes: null
|
|
9804
|
+
flyVolumeBytes: null,
|
|
9805
|
+
supportAccess: supportIds.has(w.id)
|
|
9668
9806
|
};
|
|
9669
9807
|
})
|
|
9670
9808
|
);
|
|
@@ -9752,6 +9890,63 @@ function createAdminRouter() {
|
|
|
9752
9890
|
}
|
|
9753
9891
|
return c.json({ sent: true, inviteLink: inviteUrl.toString() });
|
|
9754
9892
|
});
|
|
9893
|
+
admin.post("/workspaces/:id/support-access", async (c) => {
|
|
9894
|
+
const operator = c.get("platformOperator");
|
|
9895
|
+
const workspaceId = c.req.param("id");
|
|
9896
|
+
let result;
|
|
9897
|
+
try {
|
|
9898
|
+
result = await grantSupportMembership({
|
|
9899
|
+
workspaceId,
|
|
9900
|
+
userId: operator.userId,
|
|
9901
|
+
email: operator.email
|
|
9902
|
+
});
|
|
9903
|
+
} catch (err) {
|
|
9904
|
+
return c.json({ error: err instanceof Error ? err.message : "grant failed" }, 409);
|
|
9905
|
+
}
|
|
9906
|
+
if (result === "granted") {
|
|
9907
|
+
await logSupportAction({
|
|
9908
|
+
workspaceId,
|
|
9909
|
+
staffUserId: operator.userId,
|
|
9910
|
+
staffEmail: operator.email,
|
|
9911
|
+
action: "grant"
|
|
9912
|
+
});
|
|
9913
|
+
}
|
|
9914
|
+
invalidateAuthContextCache();
|
|
9915
|
+
return c.json({ supportAccess: true });
|
|
9916
|
+
});
|
|
9917
|
+
admin.delete("/workspaces/:id/support-access", async (c) => {
|
|
9918
|
+
const operator = c.get("platformOperator");
|
|
9919
|
+
const workspaceId = c.req.param("id");
|
|
9920
|
+
if (await revokeSupportMembership(workspaceId, operator.userId)) {
|
|
9921
|
+
await logSupportAction({
|
|
9922
|
+
workspaceId,
|
|
9923
|
+
staffUserId: operator.userId,
|
|
9924
|
+
staffEmail: operator.email,
|
|
9925
|
+
action: "leave"
|
|
9926
|
+
});
|
|
9927
|
+
}
|
|
9928
|
+
invalidateAuthContextCache();
|
|
9929
|
+
return c.json({ supportAccess: false });
|
|
9930
|
+
});
|
|
9931
|
+
const archiveRoute = (archived) => async (c) => {
|
|
9932
|
+
try {
|
|
9933
|
+
await setWorkspaceArchived(c.req.param("id"), archived);
|
|
9934
|
+
} catch (err) {
|
|
9935
|
+
const message = err instanceof Error ? err.message : "archive failed";
|
|
9936
|
+
return c.json({ error: message }, message === "workspace not found" ? 404 : 500);
|
|
9937
|
+
}
|
|
9938
|
+
invalidateAuthContextCache();
|
|
9939
|
+
return c.json({ archived });
|
|
9940
|
+
};
|
|
9941
|
+
admin.post("/workspaces/:id/archive", archiveRoute(true));
|
|
9942
|
+
admin.delete("/workspaces/:id/archive", archiveRoute(false));
|
|
9943
|
+
admin.get("/support-log", async (c) => {
|
|
9944
|
+
const entries2 = await listSupportLog({
|
|
9945
|
+
workspaceId: c.req.query("workspace") || void 0,
|
|
9946
|
+
limit: Number(c.req.query("limit")) || void 0
|
|
9947
|
+
});
|
|
9948
|
+
return c.json({ entries: entries2 });
|
|
9949
|
+
});
|
|
9755
9950
|
return admin;
|
|
9756
9951
|
}
|
|
9757
9952
|
|
|
@@ -9761,7 +9956,7 @@ var BOT_DM_KEY = "claude";
|
|
|
9761
9956
|
|
|
9762
9957
|
// ../api/src/dashboard/data-api.ts
|
|
9763
9958
|
import { existsSync as existsSync20 } from "fs";
|
|
9764
|
-
import
|
|
9959
|
+
import sharp6 from "sharp";
|
|
9765
9960
|
|
|
9766
9961
|
// ../api/src/dashboard/automations-routes.ts
|
|
9767
9962
|
import { Hono as Hono12 } from "hono";
|
|
@@ -10255,6 +10450,25 @@ var BOARD_STATUSES = [
|
|
|
10255
10450
|
"cancelled"
|
|
10256
10451
|
];
|
|
10257
10452
|
var NO_STATUS_COLUMN = "none";
|
|
10453
|
+
function isBoardStatus(v) {
|
|
10454
|
+
return typeof v === "string" && BOARD_STATUSES.includes(v);
|
|
10455
|
+
}
|
|
10456
|
+
async function callerMembershipId(c) {
|
|
10457
|
+
const viewer = c.get("auth");
|
|
10458
|
+
if (!viewer) return null;
|
|
10459
|
+
return (await getMembershipByUserId(viewer.workspaceId, viewer.userId))?.id ?? null;
|
|
10460
|
+
}
|
|
10461
|
+
async function rememberComposerStatus(c, channelId, status) {
|
|
10462
|
+
const membershipId = await callerMembershipId(c);
|
|
10463
|
+
if (!membershipId) return;
|
|
10464
|
+
const { error: error2 } = await getSupabaseClient().from("channel_member_prefs").upsert({
|
|
10465
|
+
channel_id: channelId,
|
|
10466
|
+
membership_id: membershipId,
|
|
10467
|
+
last_board_status: status,
|
|
10468
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10469
|
+
});
|
|
10470
|
+
if (error2) throw new Error(error2.message);
|
|
10471
|
+
}
|
|
10258
10472
|
function boardColumnParam(status) {
|
|
10259
10473
|
if (status === NO_STATUS_COLUMN) return NO_STATUS_COLUMN;
|
|
10260
10474
|
return BOARD_STATUSES.includes(status) ? status : null;
|
|
@@ -10320,7 +10534,9 @@ async function withPreviews(supabase, threads) {
|
|
|
10320
10534
|
for (const { data } of chunkResults) {
|
|
10321
10535
|
for (const r of data ?? []) {
|
|
10322
10536
|
const entry = previewByThread.get(r.thread_id);
|
|
10537
|
+
const images = r.body.includes("![") ? markdownImageUrls(slackMrkdwnToMarkdown(r.body)) : [];
|
|
10323
10538
|
if (entry) {
|
|
10539
|
+
entry.images.push(...images);
|
|
10324
10540
|
entry.count += 1;
|
|
10325
10541
|
entry.lastAuthor = r.author;
|
|
10326
10542
|
entry.participants.delete(r.author);
|
|
@@ -10330,7 +10546,8 @@ async function withPreviews(supabase, threads) {
|
|
|
10330
10546
|
count: 1,
|
|
10331
10547
|
firstBody: r.body,
|
|
10332
10548
|
lastAuthor: r.author,
|
|
10333
|
-
participants: /* @__PURE__ */ new Map()
|
|
10549
|
+
participants: /* @__PURE__ */ new Map(),
|
|
10550
|
+
images
|
|
10334
10551
|
});
|
|
10335
10552
|
}
|
|
10336
10553
|
}
|
|
@@ -10345,10 +10562,19 @@ async function withPreviews(supabase, threads) {
|
|
|
10345
10562
|
// read, on this browser or any other (see web/src/lib/thread-unread).
|
|
10346
10563
|
last_reply_author: entry && entry.count > 1 ? entry.lastAuthor : null,
|
|
10347
10564
|
// Everyone who replied, most recent first, for the row's avatar stack.
|
|
10348
|
-
participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id }))
|
|
10565
|
+
participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id })),
|
|
10566
|
+
// Every image in the thread, in order, so the row can show a thumbnail
|
|
10567
|
+
// strip; only the first few URLs are sent, the rest are just counted.
|
|
10568
|
+
images: (entry?.images ?? []).slice(0, ROW_IMAGE_LIMIT),
|
|
10569
|
+
image_count: entry?.images.length ?? 0
|
|
10349
10570
|
};
|
|
10350
10571
|
});
|
|
10351
10572
|
}
|
|
10573
|
+
var ROW_IMAGE_LIMIT = 3;
|
|
10574
|
+
var MARKDOWN_IMAGE_RE = /!\[[^\]]*\]\(\s*<?([^\s)>]+)>?(?:\s+"[^"]*")?\s*\)/g;
|
|
10575
|
+
function markdownImageUrls(markdown) {
|
|
10576
|
+
return [...markdown.matchAll(MARKDOWN_IMAGE_RE)].map((m) => m[1]);
|
|
10577
|
+
}
|
|
10352
10578
|
async function labelThread(channelId, body, scope) {
|
|
10353
10579
|
const channel = getProfileChannelRegistry().find(
|
|
10354
10580
|
(ch) => ch.id === channelId || ch.externalChannelId === channelId
|
|
@@ -10758,6 +10984,13 @@ function createChatRouter(agent = {}) {
|
|
|
10758
10984
|
const clientKey2 = isNonEmptyString(input2.clientKey) ? input2.clientKey : null;
|
|
10759
10985
|
const model = requestedModel(c, input2.model);
|
|
10760
10986
|
const effort = requestedEffort(c, input2.effort);
|
|
10987
|
+
const pickedStatus = input2.board_status;
|
|
10988
|
+
if (pickedStatus != null && !isBoardStatus(pickedStatus)) {
|
|
10989
|
+
return c.json(
|
|
10990
|
+
{ error: `board_status must be null or one of: ${BOARD_STATUSES.join(", ")}` },
|
|
10991
|
+
400
|
|
10992
|
+
);
|
|
10993
|
+
}
|
|
10761
10994
|
const supabase = getSupabaseClient();
|
|
10762
10995
|
let thread;
|
|
10763
10996
|
let replayed = false;
|
|
@@ -10768,7 +11001,8 @@ function createChatRouter(agent = {}) {
|
|
|
10768
11001
|
// opening message at display time (see threadCrumbLabel).
|
|
10769
11002
|
title: isNonEmptyString(input2.title) ? input2.title.trim() : null,
|
|
10770
11003
|
author: input2.author,
|
|
10771
|
-
client_key: clientKey2
|
|
11004
|
+
client_key: clientKey2,
|
|
11005
|
+
board_status: pickedStatus ?? null
|
|
10772
11006
|
}).select().single();
|
|
10773
11007
|
if (threadError) {
|
|
10774
11008
|
if (!clientKey2 || threadError.code !== "23505") {
|
|
@@ -10809,6 +11043,11 @@ function createChatRouter(agent = {}) {
|
|
|
10809
11043
|
effort: effortFromRow(reply["effort"])
|
|
10810
11044
|
});
|
|
10811
11045
|
}
|
|
11046
|
+
if (!replayed && pickedStatus !== void 0) {
|
|
11047
|
+
void rememberComposerStatus(c, channelId, pickedStatus).catch(
|
|
11048
|
+
(err) => console.error("Remember composer status failed:", err)
|
|
11049
|
+
);
|
|
11050
|
+
}
|
|
10812
11051
|
if (!replayed && thread["title"] == null) {
|
|
10813
11052
|
void generateAutoTitle(thread.id, channelId, input2.body).catch(
|
|
10814
11053
|
(err) => console.error("Auto title failed:", err)
|
|
@@ -10816,6 +11055,15 @@ function createChatRouter(agent = {}) {
|
|
|
10816
11055
|
}
|
|
10817
11056
|
return c.json({ ...thread, replies: [reply] }, replayed ? 200 : 201);
|
|
10818
11057
|
});
|
|
11058
|
+
app.get("/channels/:channelId/composer-status", async (c) => {
|
|
11059
|
+
const channelId = c.req.param("channelId");
|
|
11060
|
+
if (!ownChannelIds(c).has(channelId)) return c.json({ error: "unknown channel" }, 404);
|
|
11061
|
+
const membershipId = await callerMembershipId(c);
|
|
11062
|
+
if (!membershipId) return c.json({ lastStatus: null });
|
|
11063
|
+
const { data: pref, error: error2 } = await getSupabaseClient().from("channel_member_prefs").select("last_board_status").eq("channel_id", channelId).eq("membership_id", membershipId).maybeSingle();
|
|
11064
|
+
if (error2) return c.json({ error: error2.message }, 500);
|
|
11065
|
+
return c.json({ lastStatus: pref?.last_board_status ?? null });
|
|
11066
|
+
});
|
|
10819
11067
|
app.post("/replies/:replyId/fork", async (c) => {
|
|
10820
11068
|
const replyId = c.req.param("replyId");
|
|
10821
11069
|
const input2 = await c.req.json().catch(() => null);
|
|
@@ -11031,7 +11279,7 @@ function generateShareId() {
|
|
|
11031
11279
|
return randomBytes6(9).toString("base64url");
|
|
11032
11280
|
}
|
|
11033
11281
|
async function publishDocShare(input2, readBundle) {
|
|
11034
|
-
const bundle = await readBundle({ cwd: input2.cwd, sourcePath: input2.sourcePath });
|
|
11282
|
+
const bundle = await readBundle({ channelId: input2.channelId, cwd: input2.cwd, sourcePath: input2.sourcePath });
|
|
11035
11283
|
if (bundle.status !== "ok") return bundle;
|
|
11036
11284
|
const existing = await findDocShareBySource(input2.workspaceId, input2.channelId, input2.sourcePath);
|
|
11037
11285
|
const shareId = existing?.id ?? generateShareId();
|
|
@@ -11691,7 +11939,7 @@ function createTranscribeRouter() {
|
|
|
11691
11939
|
// ../api/src/dashboard/avatars-routes.ts
|
|
11692
11940
|
import { createHash as createHash9, randomUUID as randomUUID4 } from "crypto";
|
|
11693
11941
|
import { Hono as Hono19 } from "hono";
|
|
11694
|
-
import
|
|
11942
|
+
import sharp5 from "sharp";
|
|
11695
11943
|
import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
|
|
11696
11944
|
var AVATAR_SIZE = 192;
|
|
11697
11945
|
var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
@@ -11727,7 +11975,7 @@ async function storeAvatar(file, owner) {
|
|
|
11727
11975
|
return { error: "avatar must be a PNG or JPEG image" };
|
|
11728
11976
|
if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
|
|
11729
11977
|
const original = Buffer.from(await file.arrayBuffer());
|
|
11730
|
-
const resized = await
|
|
11978
|
+
const resized = await sharp5(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
|
|
11731
11979
|
const key = `${avatarKeyPrefix()}${owner}-${randomUUID4()}.png`;
|
|
11732
11980
|
await getR2Client().send(
|
|
11733
11981
|
new PutObjectCommand4({
|
|
@@ -11905,10 +12153,6 @@ function createMembersRouter() {
|
|
|
11905
12153
|
if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
|
|
11906
12154
|
return c.json({ error: "unknown avatar" }, 400);
|
|
11907
12155
|
}
|
|
11908
|
-
const { plan } = await getWorkspaceBilling(ctx.workspaceId);
|
|
11909
|
-
if (plan === "local") {
|
|
11910
|
-
return c.json({ error: "The Solo plan is for one person. Upgrade to invite members." }, 403);
|
|
11911
|
-
}
|
|
11912
12156
|
const existing = await getMembershipByEmail(ctx.workspaceId, email);
|
|
11913
12157
|
if (existing) return c.json({ error: "already a member of this workspace" }, 409);
|
|
11914
12158
|
let membership;
|
|
@@ -13421,8 +13665,95 @@ async function buildThreadUsage(threadTs) {
|
|
|
13421
13665
|
}
|
|
13422
13666
|
}
|
|
13423
13667
|
|
|
13424
|
-
// ../api/src/dashboard/channel-
|
|
13668
|
+
// ../api/src/dashboard/channel-greeting.ts
|
|
13425
13669
|
import { Hono as Hono25 } from "hono";
|
|
13670
|
+
function eligible(ch) {
|
|
13671
|
+
return !!ch && !ch.isDefault && !ch.dmKey && !ch.agentName && !ch.archivedAt;
|
|
13672
|
+
}
|
|
13673
|
+
function buildIntroPrompt(ch) {
|
|
13674
|
+
const automations = [
|
|
13675
|
+
...(ch.automations?.schedules ?? []).map((s) => s.name),
|
|
13676
|
+
...(ch.automations?.triggers ?? []).map((t) => t.name)
|
|
13677
|
+
].filter(Boolean);
|
|
13678
|
+
return [
|
|
13679
|
+
`[channel intro request] Write the one-time intro message for the #${ch.name} channel, into this thread. It is the first thing a visitor sees here, so it must be grounded in what this folder actually contains.`,
|
|
13680
|
+
"",
|
|
13681
|
+
"Read-only: inspect the folder (file listing, README.md, AGENTS.md / CLAUDE.md, docs/, git status, .claude/skills and any SKILL.md files), but do not create, modify or delete anything, and do not run anything with side effects.",
|
|
13682
|
+
"",
|
|
13683
|
+
"Language: match how this project is usually worked on, in this order of evidence: (1) the user messages in this folder's recent agent sessions (Claude Code: ~/.claude/projects/<encoded cwd>/*.jsonl; Codex: ~/.codex/sessions/**/*.jsonl with a matching cwd): read only enough to judge the language, never quote them; (2) the main language of README.md / AGENTS.md / docs/; (3) English. For Chinese use Traditional Chinese (Taiwan), full-width punctuation.",
|
|
13684
|
+
"",
|
|
13685
|
+
"Shape (pyramid, at most ~80 Chinese characters / ~60 English words in total):",
|
|
13686
|
+
"1. One sentence: what this channel/project is, and what you can best help with here.",
|
|
13687
|
+
`2. Three one-line bullets: what already exists in the folder (repo, docs); 1-2 skills that are really available and how to invoke them; one concrete automation suggestion with frequency and content (e.g. "every Monday 9am, summarise last week's commits here").`,
|
|
13688
|
+
"3. End with exactly one line `[BUTTONS: a | b | c]` whose three buttons correspond to the three bullets and read as directly executable requests.",
|
|
13689
|
+
"",
|
|
13690
|
+
"Rules: never invent skills or automations that are not actually there: skip that bullet instead. " + (automations.length ? `This channel already has automations (${automations.join(", ")}): introduce the existing one(s) instead of suggesting a new one. ` : "") + "Do not explain Flyspace itself. If the folder is empty, instead ask what this channel will be used for and offer `[BUTTONS: Help me set up this channel]`. Output only the message itself, nothing else."
|
|
13691
|
+
].join("\n");
|
|
13692
|
+
}
|
|
13693
|
+
var FALLBACK_BODY = "I could not write this channel's intro automatically. Ask me below and I'll take a look at the folder.\n\n[BUTTONS: Introduce this channel]";
|
|
13694
|
+
function introOpening(channelName) {
|
|
13695
|
+
return `Welcome to #${channelName}. I'm reading this project's folder, docs, skills and automations to work out how I can help here. Below you'll find a short intro and a few first steps you can click to try.`;
|
|
13696
|
+
}
|
|
13697
|
+
function createChannelGreetingRouter(agent) {
|
|
13698
|
+
const app = new Hono25();
|
|
13699
|
+
app.get("/:id/greeting", async (c) => {
|
|
13700
|
+
const ctx = await resolveAuthContext(c);
|
|
13701
|
+
const ch = findChannelById(c.req.param("id"));
|
|
13702
|
+
const pending2 = isWorkspaceOwner(ctx) && eligible(ch) && !ch.greetedAt && (agent.isHostOnline?.(ch.profile) ?? false);
|
|
13703
|
+
if (!pending2) return c.json({ pending: false, threadId: null });
|
|
13704
|
+
const threadId = await ensureIntroThread(ch);
|
|
13705
|
+
return c.json({ pending: !!threadId, threadId });
|
|
13706
|
+
});
|
|
13707
|
+
app.post("/:id/greeting", async (c) => {
|
|
13708
|
+
const ctx = await resolveAuthContext(c);
|
|
13709
|
+
if (!isWorkspaceOwner(ctx)) return c.json({ error: "workspace owner access required" }, 403);
|
|
13710
|
+
const ch = findChannelById(c.req.param("id"));
|
|
13711
|
+
if (!eligible(ch)) return c.json({ error: "channel has no intro" }, 404);
|
|
13712
|
+
const threadId = await ensureIntroThread(ch);
|
|
13713
|
+
if (!threadId) return c.json({ error: "could not create the intro thread" }, 500);
|
|
13714
|
+
if (!await markChannelGreeted(ch.id)) return c.json({ threadId, error: "already greeted" }, 409);
|
|
13715
|
+
try {
|
|
13716
|
+
if (!(agent.isHostOnline?.(ch.profile) ?? false)) throw new Error("host offline");
|
|
13717
|
+
await agent.onChatMessage?.({
|
|
13718
|
+
threadId,
|
|
13719
|
+
// Synthetic: no thread_replies row has this id, so the prompt is
|
|
13720
|
+
// never shown and never counted as thread context.
|
|
13721
|
+
replyId: crypto.randomUUID(),
|
|
13722
|
+
author: ctx.handle,
|
|
13723
|
+
body: buildIntroPrompt(ch)
|
|
13724
|
+
});
|
|
13725
|
+
} catch (err) {
|
|
13726
|
+
console.error(`[channel-greeting] dispatch failed for #${ch.name}:`, err);
|
|
13727
|
+
await getSupabaseClient().from("thread_replies").insert({ thread_id: threadId, author: AGENT_AUTHOR, body: FALLBACK_BODY });
|
|
13728
|
+
}
|
|
13729
|
+
return c.json({ threadId }, 201);
|
|
13730
|
+
});
|
|
13731
|
+
return app;
|
|
13732
|
+
}
|
|
13733
|
+
async function ensureIntroThread(ch) {
|
|
13734
|
+
const supabase = getSupabaseClient();
|
|
13735
|
+
const existing = () => supabase.from("threads").select("id").eq("client_key", ch.id).maybeSingle();
|
|
13736
|
+
const found = await existing();
|
|
13737
|
+
if (found.data) return found.data.id;
|
|
13738
|
+
const { data, error: error2 } = await supabase.from("threads").insert({
|
|
13739
|
+
channel_id: ch.id,
|
|
13740
|
+
title: `Getting started in #${ch.name}`,
|
|
13741
|
+
author: AGENT_AUTHOR,
|
|
13742
|
+
client_key: ch.id
|
|
13743
|
+
}).select("id").single();
|
|
13744
|
+
if (data) {
|
|
13745
|
+
await supabase.from("thread_replies").insert({
|
|
13746
|
+
thread_id: data.id,
|
|
13747
|
+
author: AGENT_AUTHOR,
|
|
13748
|
+
body: introOpening(ch.name)
|
|
13749
|
+
});
|
|
13750
|
+
return data.id;
|
|
13751
|
+
}
|
|
13752
|
+
return error2?.code === "23505" ? (await existing()).data?.id ?? null : null;
|
|
13753
|
+
}
|
|
13754
|
+
|
|
13755
|
+
// ../api/src/dashboard/channel-members-routes.ts
|
|
13756
|
+
import { Hono as Hono26 } from "hono";
|
|
13426
13757
|
async function isChannelAdmin(ctx, channelId) {
|
|
13427
13758
|
if (ctx.role === "admin") return true;
|
|
13428
13759
|
const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
|
|
@@ -13431,7 +13762,7 @@ async function isChannelAdmin(ctx, channelId) {
|
|
|
13431
13762
|
return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
|
|
13432
13763
|
}
|
|
13433
13764
|
function createChannelMembersRouter() {
|
|
13434
|
-
const router = new
|
|
13765
|
+
const router = new Hono26();
|
|
13435
13766
|
router.get("/:id/members", async (c) => {
|
|
13436
13767
|
const ctx = await resolveAuthContext(c);
|
|
13437
13768
|
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
@@ -13501,7 +13832,7 @@ function createChannelMembersRouter() {
|
|
|
13501
13832
|
}
|
|
13502
13833
|
|
|
13503
13834
|
// ../api/src/dashboard/memories-routes.ts
|
|
13504
|
-
import { Hono as
|
|
13835
|
+
import { Hono as Hono27 } from "hono";
|
|
13505
13836
|
async function readPatch(c) {
|
|
13506
13837
|
const body = await c.req.json().catch(() => ({}));
|
|
13507
13838
|
const patch = {};
|
|
@@ -13529,7 +13860,7 @@ var requireAgentMemory = async (c, next) => {
|
|
|
13529
13860
|
await next();
|
|
13530
13861
|
};
|
|
13531
13862
|
function createMemoriesRouter() {
|
|
13532
|
-
const router = new
|
|
13863
|
+
const router = new Hono27();
|
|
13533
13864
|
for (const path of [
|
|
13534
13865
|
"/:id/memories",
|
|
13535
13866
|
"/:id/memories/*",
|
|
@@ -13602,7 +13933,7 @@ function createMemoriesRouter() {
|
|
|
13602
13933
|
return router;
|
|
13603
13934
|
}
|
|
13604
13935
|
function createScopedMemoriesRouter() {
|
|
13605
|
-
const router = new
|
|
13936
|
+
const router = new Hono27();
|
|
13606
13937
|
router.use("*", requireAgentMemory);
|
|
13607
13938
|
const workspace = (c) => ({
|
|
13608
13939
|
scope: "workspace",
|
|
@@ -13833,6 +14164,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13833
14164
|
});
|
|
13834
14165
|
api.route("/channels", createDocShareWriteRouter(agent));
|
|
13835
14166
|
api.route("/onboarding", createOnboardingRouter(agent));
|
|
14167
|
+
api.route("/channels", createChannelGreetingRouter(agent));
|
|
13836
14168
|
api.route("/channels", createChannelMembersRouter());
|
|
13837
14169
|
api.route("/channels", createMemoriesRouter());
|
|
13838
14170
|
api.route("/memories", createScopedMemoriesRouter());
|
|
@@ -14076,7 +14408,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14076
14408
|
return c.json({ error: "logo must be 2MB or smaller" }, 400);
|
|
14077
14409
|
}
|
|
14078
14410
|
const original = Buffer.from(await file.arrayBuffer());
|
|
14079
|
-
const resized = await
|
|
14411
|
+
const resized = await sharp6(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
|
|
14080
14412
|
const logo = `data:image/png;base64,${resized.toString("base64")}`;
|
|
14081
14413
|
return c.json(await saveWorkspaceBranding({ logo }));
|
|
14082
14414
|
});
|
|
@@ -14086,7 +14418,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14086
14418
|
// ../api/src/dashboard/device-auth-routes.ts
|
|
14087
14419
|
import { verify } from "crypto";
|
|
14088
14420
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
14089
|
-
import { Hono as
|
|
14421
|
+
import { Hono as Hono28 } from "hono";
|
|
14090
14422
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14091
14423
|
function cleanText(value, max) {
|
|
14092
14424
|
if (typeof value !== "string") return null;
|
|
@@ -14134,7 +14466,7 @@ async function boundWorkspaceIds(publicKey, userId) {
|
|
|
14134
14466
|
return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
|
|
14135
14467
|
}
|
|
14136
14468
|
function createDeviceAuthRouter() {
|
|
14137
|
-
const app = new
|
|
14469
|
+
const app = new Hono28();
|
|
14138
14470
|
app.post("/code", async (c) => {
|
|
14139
14471
|
const body = await readBody(c);
|
|
14140
14472
|
const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
|
|
@@ -14453,7 +14785,7 @@ var workspaceRef = () => ({ kind: "workspace" });
|
|
|
14453
14785
|
var channelRef = (c) => ({ kind: "channel", channelId: c.req.param("id") });
|
|
14454
14786
|
|
|
14455
14787
|
// ../api/src/dashboard/output-styles-routes.ts
|
|
14456
|
-
import { Hono as
|
|
14788
|
+
import { Hono as Hono29 } from "hono";
|
|
14457
14789
|
function requireAdmin3(auth) {
|
|
14458
14790
|
return auth?.role === "admin" ? null : "admin access required";
|
|
14459
14791
|
}
|
|
@@ -14473,7 +14805,7 @@ function failure(c, err) {
|
|
|
14473
14805
|
return c.json({ error: message }, err instanceof OutputStyleConflictError ? 409 : 500);
|
|
14474
14806
|
}
|
|
14475
14807
|
function createOutputStylesRouter() {
|
|
14476
|
-
const app = new
|
|
14808
|
+
const app = new Hono29();
|
|
14477
14809
|
app.get("/", (c) => {
|
|
14478
14810
|
const auth = c.get("auth");
|
|
14479
14811
|
const denied = requireAdmin3(auth);
|
|
@@ -14565,7 +14897,7 @@ function createOutputStylesRouter() {
|
|
|
14565
14897
|
return app;
|
|
14566
14898
|
}
|
|
14567
14899
|
async function handleOutputStylesRequest(req) {
|
|
14568
|
-
const app = new
|
|
14900
|
+
const app = new Hono29();
|
|
14569
14901
|
app.use("*", async (c, next) => {
|
|
14570
14902
|
c.set("auth", { role: "admin" });
|
|
14571
14903
|
await next();
|
|
@@ -14586,7 +14918,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
14586
14918
|
|
|
14587
14919
|
// ../api/src/dashboard/api.ts
|
|
14588
14920
|
function createGatedRouter(agent, register) {
|
|
14589
|
-
const api = new
|
|
14921
|
+
const api = new Hono30();
|
|
14590
14922
|
api.use("*", async (c, next) => {
|
|
14591
14923
|
const known = await listKnownWorkspaces();
|
|
14592
14924
|
const fromHost = profileFromHost(trustedHost(c), known);
|
|
@@ -14635,12 +14967,11 @@ function createGatedRouter(agent, register) {
|
|
|
14635
14967
|
|
|
14636
14968
|
// src/dashboard/host-api.ts
|
|
14637
14969
|
import { existsSync as existsSync30, readFileSync as readFileSync24, statSync as statSync8 } from "fs";
|
|
14638
|
-
import
|
|
14639
|
-
import sharp6 from "sharp";
|
|
14970
|
+
import sharp7 from "sharp";
|
|
14640
14971
|
|
|
14641
14972
|
// src/dashboard/channels-routes.ts
|
|
14642
14973
|
import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
|
|
14643
|
-
import { Hono as
|
|
14974
|
+
import { Hono as Hono31 } from "hono";
|
|
14644
14975
|
function coerceAccess(raw) {
|
|
14645
14976
|
if (raw === "open" || raw === "*" || raw === "all") return raw;
|
|
14646
14977
|
if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) return raw;
|
|
@@ -14703,7 +15034,7 @@ function toResponse3(channel) {
|
|
|
14703
15034
|
};
|
|
14704
15035
|
}
|
|
14705
15036
|
function createChannelWriteRouter() {
|
|
14706
|
-
const app = new
|
|
15037
|
+
const app = new Hono31();
|
|
14707
15038
|
app.post("/", async (c) => {
|
|
14708
15039
|
const body = await c.req.json().catch(() => ({}));
|
|
14709
15040
|
const name = typeof body["name"] === "string" ? body["name"].trim() : "";
|
|
@@ -14843,7 +15174,7 @@ import {
|
|
|
14843
15174
|
} from "fs";
|
|
14844
15175
|
import { Readable } from "stream";
|
|
14845
15176
|
import { basename as basename8, dirname as dirname8, extname, join as join23, relative, resolve as resolve5, sep } from "path";
|
|
14846
|
-
import { Hono as
|
|
15177
|
+
import { Hono as Hono32 } from "hono";
|
|
14847
15178
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
14848
15179
|
|
|
14849
15180
|
// src/dashboard/docs-watcher.ts
|
|
@@ -17724,7 +18055,7 @@ function deleteDocEntry(root, path) {
|
|
|
17724
18055
|
};
|
|
17725
18056
|
}
|
|
17726
18057
|
function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
17727
|
-
const app = new
|
|
18058
|
+
const app = new Hono32();
|
|
17728
18059
|
app.use("/:id/docs", gate);
|
|
17729
18060
|
app.use("/:id/docs/*", gate);
|
|
17730
18061
|
app.get("/:id/docs", (c) => {
|
|
@@ -18062,7 +18393,7 @@ function withDocLinkBridge(html) {
|
|
|
18062
18393
|
// src/dashboard/skills-routes-host.ts
|
|
18063
18394
|
import { existsSync as existsSync27, statSync as statSync7 } from "fs";
|
|
18064
18395
|
import { dirname as dirname9 } from "path";
|
|
18065
|
-
import { Hono as
|
|
18396
|
+
import { Hono as Hono33 } from "hono";
|
|
18066
18397
|
function workspaceScope() {
|
|
18067
18398
|
const claudeConfigDir = profileClaudeConfigDir();
|
|
18068
18399
|
const cwd = defaultClaudeCwd();
|
|
@@ -18168,12 +18499,12 @@ var diskBackend = {
|
|
|
18168
18499
|
write: writeSkillAndSync
|
|
18169
18500
|
};
|
|
18170
18501
|
function createWorkspaceSkillsRouter() {
|
|
18171
|
-
const app = new
|
|
18502
|
+
const app = new Hono33();
|
|
18172
18503
|
mountSkillRoutes(app, "/skills", workspaceRef, diskBackend);
|
|
18173
18504
|
return app;
|
|
18174
18505
|
}
|
|
18175
18506
|
function createChannelSkillsRouter() {
|
|
18176
|
-
const app = new
|
|
18507
|
+
const app = new Hono33();
|
|
18177
18508
|
mountSkillRoutes(app, "/:id/skill-files", channelRef, diskBackend);
|
|
18178
18509
|
return app;
|
|
18179
18510
|
}
|
|
@@ -18308,7 +18639,7 @@ import { randomUUID as randomUUID5 } from "crypto";
|
|
|
18308
18639
|
import { chmodSync, existsSync as existsSync29, mkdirSync as mkdirSync12, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
|
|
18309
18640
|
import { createRequire } from "module";
|
|
18310
18641
|
import { dirname as dirname10, join as join25 } from "path";
|
|
18311
|
-
import { Hono as
|
|
18642
|
+
import { Hono as Hono34 } from "hono";
|
|
18312
18643
|
var SESSION_TTL_MS = 10 * 6e4;
|
|
18313
18644
|
var URL_WAIT_MS = 3e4;
|
|
18314
18645
|
var TOKEN_WAIT_MS = 6e4;
|
|
@@ -18465,7 +18796,7 @@ function failure2(c, err) {
|
|
|
18465
18796
|
return c.json({ error: err.message }, 500);
|
|
18466
18797
|
}
|
|
18467
18798
|
function createProviderLoginRouter() {
|
|
18468
|
-
const app = new
|
|
18799
|
+
const app = new Hono34();
|
|
18469
18800
|
app.use("*", async (c, next) => {
|
|
18470
18801
|
const denied = requireAdmin4(c.get("auth"));
|
|
18471
18802
|
if (denied) return c.json({ error: denied }, 403);
|
|
@@ -18509,7 +18840,7 @@ function createProviderLoginRouter() {
|
|
|
18509
18840
|
return app;
|
|
18510
18841
|
}
|
|
18511
18842
|
async function handleProviderLoginRequest(req) {
|
|
18512
|
-
const app = new
|
|
18843
|
+
const app = new Hono34();
|
|
18513
18844
|
app.use("*", async (c, next) => {
|
|
18514
18845
|
c.set("auth", { role: "admin" });
|
|
18515
18846
|
await next();
|
|
@@ -18617,7 +18948,7 @@ function registerHostRoutes(api) {
|
|
|
18617
18948
|
return c.json({ error: "avatar must be 2MB or smaller" }, 400);
|
|
18618
18949
|
}
|
|
18619
18950
|
const original = Buffer.from(await file.arrayBuffer());
|
|
18620
|
-
const resized = await
|
|
18951
|
+
const resized = await sharp7(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
|
|
18621
18952
|
const avatar = `data:image/png;base64,${resized.toString("base64")}`;
|
|
18622
18953
|
const name = c.req.param("name");
|
|
18623
18954
|
if (name === BOT_PROFILE_KEY) return c.json(await saveBotIdentity({ avatar }));
|
|
@@ -18631,8 +18962,7 @@ function registerHostRoutes(api) {
|
|
|
18631
18962
|
api.get("/logs", (c) => {
|
|
18632
18963
|
const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
|
|
18633
18964
|
const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
|
|
18634
|
-
const
|
|
18635
|
-
const file = join26(paths.LOG_DIR, filename);
|
|
18965
|
+
const file = serviceLogFile(errors);
|
|
18636
18966
|
const exists = existsSync30(file);
|
|
18637
18967
|
return c.json({
|
|
18638
18968
|
file,
|
|
@@ -18667,13 +18997,13 @@ import dotenv2 from "dotenv";
|
|
|
18667
18997
|
import { WebSocket as WebSocket2 } from "ws";
|
|
18668
18998
|
|
|
18669
18999
|
// src/dashboard/docs-relay-host.ts
|
|
18670
|
-
import { Hono as
|
|
19000
|
+
import { Hono as Hono35 } from "hono";
|
|
18671
19001
|
var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
|
|
18672
19002
|
async function handleDocsRequest(req) {
|
|
18673
19003
|
return runRelayed(req, (app) => app.route("/channels", createDocsRouter({ gate: async (_c, next) => next() })));
|
|
18674
19004
|
}
|
|
18675
19005
|
async function runRelayed(req, register) {
|
|
18676
|
-
const app = new
|
|
19006
|
+
const app = new Hono35();
|
|
18677
19007
|
app.use("*", async (c, next) => {
|
|
18678
19008
|
c.set("auth", req.auth);
|
|
18679
19009
|
await next();
|
|
@@ -18928,12 +19258,12 @@ function readDocShareBundle({ cwd, sourcePath }) {
|
|
|
18928
19258
|
// src/dashboard/host-upgrade.ts
|
|
18929
19259
|
import { spawn as spawn7 } from "child_process";
|
|
18930
19260
|
import { mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
|
|
18931
|
-
import { join as
|
|
19261
|
+
import { join as join26 } from "path";
|
|
18932
19262
|
var running = false;
|
|
18933
19263
|
function startHostUpgrade() {
|
|
18934
19264
|
if (running) return { started: false };
|
|
18935
19265
|
mkdirSync13(paths.LOG_DIR, { recursive: true });
|
|
18936
|
-
const log5 = openSync3(
|
|
19266
|
+
const log5 = openSync3(join26(paths.LOG_DIR, "host-upgrade.log"), "a");
|
|
18937
19267
|
const child = spawn7(process.execPath, [process.argv[1] ?? "", "upgrade"], {
|
|
18938
19268
|
detached: true,
|
|
18939
19269
|
stdio: ["ignore", log5, log5]
|
|
@@ -18974,9 +19304,12 @@ function localAgentCallbacks(chatAgent) {
|
|
|
18974
19304
|
listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
|
|
18975
19305
|
listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
|
|
18976
19306
|
getAgentIdentities: () => resolveAgentIdentities(),
|
|
18977
|
-
readDocShareBundle: (req) => readDocShareBundle(req),
|
|
18978
|
-
listChannelSkills: (args) =>
|
|
18979
|
-
|
|
19307
|
+
readDocShareBundle: (req) => readDocShareBundle(req.channelId ? { ...req, cwd: resolveChannelConfig(req.channelId).cwd } : req),
|
|
19308
|
+
listChannelSkills: (args) => {
|
|
19309
|
+
const { cwd, claudeConfigDir } = args.channelId ? resolveChannelConfig(args.channelId) : args;
|
|
19310
|
+
return listSkills(claudeConfigDir, cwd);
|
|
19311
|
+
},
|
|
19312
|
+
listFlatDocs: (args) => flatDocs(args.channelId ? resolveChannelConfig(args.channelId).cwd : args.cwd, { query: args.query, sort: args.sort, showHidden: args.showHidden }),
|
|
18980
19313
|
getLocalDashboardSettings: () => loadDashboardSettings(),
|
|
18981
19314
|
saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
|
|
18982
19315
|
writeSkill: (ref, write) => writeSkillAndSync(ref, write),
|
|
@@ -19247,11 +19580,11 @@ function startPushWatcher() {
|
|
|
19247
19580
|
|
|
19248
19581
|
// src/dashboard/machine-server.ts
|
|
19249
19582
|
import { existsSync as existsSync32 } from "fs";
|
|
19250
|
-
import { join as
|
|
19583
|
+
import { join as join27 } from "path";
|
|
19251
19584
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
19252
19585
|
|
|
19253
19586
|
// src/dashboard/app-icon-routes.ts
|
|
19254
|
-
import { Hono as
|
|
19587
|
+
import { Hono as Hono36 } from "hono";
|
|
19255
19588
|
var NEUTRAL_MANIFEST = {
|
|
19256
19589
|
id: "/",
|
|
19257
19590
|
name: "Flyspace",
|
|
@@ -19289,7 +19622,7 @@ async function withResolvedProfile2(c, next) {
|
|
|
19289
19622
|
return profile ? runWithProfile(profile, () => next()) : next();
|
|
19290
19623
|
}
|
|
19291
19624
|
function createAppIconRouter() {
|
|
19292
|
-
const app = new
|
|
19625
|
+
const app = new Hono36();
|
|
19293
19626
|
app.get("/manifest.json", withResolvedProfile2, async (c) => {
|
|
19294
19627
|
if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
|
|
19295
19628
|
const profile = getProfile();
|
|
@@ -19329,7 +19662,7 @@ function pngResponse(c, png) {
|
|
|
19329
19662
|
}
|
|
19330
19663
|
|
|
19331
19664
|
// src/dashboard/machine-server.ts
|
|
19332
|
-
var WEB_DIST =
|
|
19665
|
+
var WEB_DIST = join27(paths.WEB_ROOT, "dist");
|
|
19333
19666
|
async function bootMachine() {
|
|
19334
19667
|
await reconcileAll().catch(
|
|
19335
19668
|
(err) => console.error("[dashboard] reconcileAll failed:", err.message)
|
|
@@ -19349,14 +19682,14 @@ async function bootMachine() {
|
|
|
19349
19682
|
function mountSpa(app) {
|
|
19350
19683
|
app.route("/share", createPublicShareAssetRouter(WEB_DIST));
|
|
19351
19684
|
app.route("/", createAppIconRouter());
|
|
19352
|
-
if (existsSync32(WEB_DIST) || existsSync32(
|
|
19685
|
+
if (existsSync32(WEB_DIST) || existsSync32(join27(paths.WEB_ROOT, "package.json"))) {
|
|
19353
19686
|
app.use("/*", serveStatic({ root: WEB_DIST }));
|
|
19354
19687
|
}
|
|
19355
19688
|
app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
|
|
19356
|
-
const spaFallback = serveStatic({ path:
|
|
19689
|
+
const spaFallback = serveStatic({ path: join27(WEB_DIST, "index.html") });
|
|
19357
19690
|
app.get("*", async (c, next) => {
|
|
19358
|
-
if (!existsSync32(
|
|
19359
|
-
const isCheckout = existsSync32(
|
|
19691
|
+
if (!existsSync32(join27(WEB_DIST, "index.html"))) {
|
|
19692
|
+
const isCheckout = existsSync32(join27(paths.WEB_ROOT, "package.json"));
|
|
19360
19693
|
return c.text(
|
|
19361
19694
|
isCheckout ? `Workspace UI build missing (${WEB_DIST}).
|
|
19362
19695
|
Run \`pnpm build:web\` from the repo root.
|
|
@@ -19490,8 +19823,8 @@ function openBrowser2(url) {
|
|
|
19490
19823
|
}
|
|
19491
19824
|
}
|
|
19492
19825
|
function ensureWebBuild() {
|
|
19493
|
-
if (existsSync33(
|
|
19494
|
-
if (!existsSync33(
|
|
19826
|
+
if (existsSync33(join28(paths.WEB_ROOT, "dist", "index.html"))) return;
|
|
19827
|
+
if (!existsSync33(join28(paths.WEB_ROOT, "package.json"))) return;
|
|
19495
19828
|
console.log(" packages/ui/dist missing, building the dashboard UI...");
|
|
19496
19829
|
const result = spawnSync("pnpm", ["build"], {
|
|
19497
19830
|
cwd: paths.WEB_ROOT,
|
|
@@ -19722,10 +20055,10 @@ async function runReconcileCmd(args) {
|
|
|
19722
20055
|
}
|
|
19723
20056
|
|
|
19724
20057
|
// src/cli.ts
|
|
19725
|
-
import { join as
|
|
20058
|
+
import { join as join29 } from "path";
|
|
19726
20059
|
var home = flyspaceHome();
|
|
19727
20060
|
if (home) {
|
|
19728
|
-
const bin =
|
|
20061
|
+
const bin = join29(home, "bin");
|
|
19729
20062
|
const path = process.env.PATH ?? "";
|
|
19730
20063
|
if (!path.split(":").includes(bin)) process.env.PATH = path ? `${bin}:${path}` : bin;
|
|
19731
20064
|
}
|
|
@@ -19863,7 +20196,7 @@ async function main() {
|
|
|
19863
20196
|
break;
|
|
19864
20197
|
case "-v":
|
|
19865
20198
|
case "--version":
|
|
19866
|
-
printRaw("0.1.
|
|
20199
|
+
printRaw("0.1.33");
|
|
19867
20200
|
break;
|
|
19868
20201
|
case "-h":
|
|
19869
20202
|
case "--help":
|
package/dist/index.js
CHANGED