flyspace-cli 0.1.1 → 0.1.4
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-X7SJ653V.js → chunk-6PJ73XYW.js} +1 -1
- package/dist/{chunk-6D2PIFEA.js → chunk-BZYTXXF7.js} +268 -32
- package/dist/{chunk-QZIN7MWV.js → chunk-ZBMYF5LX.js} +15 -1
- package/dist/cli.js +2078 -648
- package/dist/index.js +2 -2
- package/dist/{threads-EQGJZF44.js → threads-PAG4PKHR.js} +12 -6
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
BUILT_IN_OUTPUT_STYLES,
|
|
8
8
|
DEFAULT_CODEX_MODEL,
|
|
9
9
|
DEFAULT_TIMEZONE,
|
|
10
|
+
DEFAULT_TITLE_RULES,
|
|
11
|
+
DefaultChannelError,
|
|
12
|
+
HANDLE_PATTERN,
|
|
13
|
+
HandleTakenError,
|
|
10
14
|
InvalidTransferError,
|
|
11
15
|
LastAdminError,
|
|
12
16
|
MessageQueue,
|
|
@@ -23,6 +27,7 @@ import {
|
|
|
23
27
|
authorFor,
|
|
24
28
|
blockToFallbackText,
|
|
25
29
|
bodyTextFor,
|
|
30
|
+
buildAutoTitle,
|
|
26
31
|
buildFilesPromptSection,
|
|
27
32
|
buildSystemPrompt,
|
|
28
33
|
canSeeChannel,
|
|
@@ -38,9 +43,14 @@ import {
|
|
|
38
43
|
deleteSkillFile,
|
|
39
44
|
deleteWorkspace,
|
|
40
45
|
displayNameFor,
|
|
46
|
+
docsAccessPromptNote,
|
|
41
47
|
ensureAgentDmChannel,
|
|
48
|
+
ensureGeneralChannel,
|
|
49
|
+
ensureLogicalCwdDir,
|
|
50
|
+
ensureMemberDmChannel,
|
|
42
51
|
ensureUsersCached,
|
|
43
52
|
expandPath,
|
|
53
|
+
extractCategories,
|
|
44
54
|
findChannel,
|
|
45
55
|
findChannelById,
|
|
46
56
|
findSkillDetail,
|
|
@@ -63,6 +73,7 @@ import {
|
|
|
63
73
|
isCwdUsable,
|
|
64
74
|
isEffort,
|
|
65
75
|
isLockedPlan,
|
|
76
|
+
isLogicalCwd,
|
|
66
77
|
isValidSkillName,
|
|
67
78
|
lastRunsForWorkspace,
|
|
68
79
|
listActiveUsers,
|
|
@@ -90,6 +101,7 @@ import {
|
|
|
90
101
|
lookupById,
|
|
91
102
|
markInviteResent,
|
|
92
103
|
markWorkspaceAgentConnected,
|
|
104
|
+
markWorkspaceProjectsSetup,
|
|
93
105
|
markdownToBlocks,
|
|
94
106
|
mcpStrictConfigEnabled,
|
|
95
107
|
mimetypeSizeLimit,
|
|
@@ -105,6 +117,7 @@ import {
|
|
|
105
117
|
resolveChannelId,
|
|
106
118
|
resolveChannelPrompt,
|
|
107
119
|
resolveCwd,
|
|
120
|
+
resolveLogicalCwd,
|
|
108
121
|
resolveSelectedOutputStyle,
|
|
109
122
|
resolveUserId,
|
|
110
123
|
runAutomation,
|
|
@@ -117,6 +130,7 @@ import {
|
|
|
117
130
|
saveWorkspacePrompt,
|
|
118
131
|
setAgentPaused,
|
|
119
132
|
setAllowGuestAccess,
|
|
133
|
+
setMembershipHandle,
|
|
120
134
|
setOutputStyleDefault,
|
|
121
135
|
setWorkspaceStripeSubscription,
|
|
122
136
|
slackConfigured,
|
|
@@ -136,7 +150,7 @@ import {
|
|
|
136
150
|
warnOnBrokenChannelCwds,
|
|
137
151
|
workspaceSkillsDir,
|
|
138
152
|
writeSkillFile
|
|
139
|
-
} from "./chunk-
|
|
153
|
+
} from "./chunk-BZYTXXF7.js";
|
|
140
154
|
import {
|
|
141
155
|
firstReplyBody,
|
|
142
156
|
getThread,
|
|
@@ -144,7 +158,7 @@ import {
|
|
|
144
158
|
listThreads,
|
|
145
159
|
moveThread,
|
|
146
160
|
resolveChannel
|
|
147
|
-
} from "./chunk-
|
|
161
|
+
} from "./chunk-6PJ73XYW.js";
|
|
148
162
|
import {
|
|
149
163
|
AGENT_AUTHOR,
|
|
150
164
|
DEFAULT_PROFILE,
|
|
@@ -154,15 +168,17 @@ import {
|
|
|
154
168
|
getR2Bucket,
|
|
155
169
|
getR2Client,
|
|
156
170
|
getSupabaseClient,
|
|
171
|
+
hasSupabaseCredentials,
|
|
157
172
|
isHumanAuthor,
|
|
158
173
|
paths,
|
|
159
174
|
postToDashboardChannel,
|
|
160
175
|
resolveDashboardChannelId,
|
|
161
176
|
runWithProfile,
|
|
177
|
+
setSupabaseClient,
|
|
162
178
|
sumR2PrefixBytes,
|
|
163
179
|
syncPostToDashboard,
|
|
164
180
|
uploadLocalImages
|
|
165
|
-
} from "./chunk-
|
|
181
|
+
} from "./chunk-ZBMYF5LX.js";
|
|
166
182
|
|
|
167
183
|
// src/logger.ts
|
|
168
184
|
var ALREADY_STAMPED = /^\[\d{4}-\d{2}-\d{2}T/;
|
|
@@ -852,7 +868,7 @@ Restarting ${installed.length} instance(s)...`);
|
|
|
852
868
|
}
|
|
853
869
|
}
|
|
854
870
|
async function runUpgrade() {
|
|
855
|
-
const current = "0.1.
|
|
871
|
+
const current = "0.1.4";
|
|
856
872
|
console.log(`Current version: ${current}`);
|
|
857
873
|
console.log("Checking for updates...\n");
|
|
858
874
|
try {
|
|
@@ -1491,10 +1507,10 @@ async function runLogs(args) {
|
|
|
1491
1507
|
}
|
|
1492
1508
|
|
|
1493
1509
|
// src/commands/slack.ts
|
|
1494
|
-
import { execFileSync as
|
|
1495
|
-
import { existsSync as existsSync28, readFileSync as
|
|
1510
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1511
|
+
import { existsSync as existsSync28, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
|
|
1496
1512
|
import { resolve as resolve7 } from "path";
|
|
1497
|
-
import { tmpdir as tmpdir3, homedir as
|
|
1513
|
+
import { tmpdir as tmpdir3, homedir as homedir9 } from "os";
|
|
1498
1514
|
import { input, confirm, select, checkbox } from "@inquirer/prompts";
|
|
1499
1515
|
import { WebClient as WebClient3 } from "@slack/web-api";
|
|
1500
1516
|
|
|
@@ -1547,6 +1563,10 @@ async function emailForToken(token, type) {
|
|
|
1547
1563
|
}
|
|
1548
1564
|
}
|
|
1549
1565
|
|
|
1566
|
+
// src/dashboard/profile-config.ts
|
|
1567
|
+
import { homedir as homedir4 } from "os";
|
|
1568
|
+
import { join as join7 } from "path";
|
|
1569
|
+
|
|
1550
1570
|
// src/dashboard/profile-config-cache.ts
|
|
1551
1571
|
var cache = /* @__PURE__ */ new Map();
|
|
1552
1572
|
function setProfileConfigCache(configs) {
|
|
@@ -1561,6 +1581,11 @@ function cachedProfileConfig(profile = getProfile()) {
|
|
|
1561
1581
|
function profileEnv(key) {
|
|
1562
1582
|
return cachedProfileConfig()?.env[key] || process.env[key] || void 0;
|
|
1563
1583
|
}
|
|
1584
|
+
function profileClaudeConfigDir() {
|
|
1585
|
+
const config = cachedProfileConfig();
|
|
1586
|
+
if (!config) return process.env["CLAUDE_CONFIG_DIR"] || void 0;
|
|
1587
|
+
return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join7(homedir4(), ".claude");
|
|
1588
|
+
}
|
|
1564
1589
|
function profileOauthToken() {
|
|
1565
1590
|
const env = cachedProfileConfig()?.env ?? {};
|
|
1566
1591
|
if (env["CLAUDE_CODE_OAUTH_TOKEN"]) return env["CLAUDE_CODE_OAUTH_TOKEN"];
|
|
@@ -1587,7 +1612,7 @@ function resolveChannelConfig(channelId) {
|
|
|
1587
1612
|
mounts: channel?.mounts ?? [],
|
|
1588
1613
|
prompt: channel?.prompt?.trim() || void 0,
|
|
1589
1614
|
codexModel: profileEnv("CUSTIE_CODEX_MODEL")?.trim() || DEFAULT_CODEX_MODEL,
|
|
1590
|
-
claudeConfigDir:
|
|
1615
|
+
claudeConfigDir: profileClaudeConfigDir(),
|
|
1591
1616
|
claudeCodeOauthToken: profileOauthToken(),
|
|
1592
1617
|
agentName: channel?.agentName ?? null,
|
|
1593
1618
|
profile: channel?.profile ?? getProfile(),
|
|
@@ -1758,7 +1783,10 @@ var THEME_ACCENTS = {
|
|
|
1758
1783
|
sanctuary: "#a13c4a",
|
|
1759
1784
|
light: "#3a66a3",
|
|
1760
1785
|
"sanctuary-light": "#a13c4a",
|
|
1761
|
-
folio: "#9c4a34"
|
|
1786
|
+
folio: "#9c4a34",
|
|
1787
|
+
// Cheer's yellow can't carry the white button text mail uses, so mail
|
|
1788
|
+
// takes the theme's dark ink instead.
|
|
1789
|
+
cheer: "#241c15"
|
|
1762
1790
|
};
|
|
1763
1791
|
function accentOf(id) {
|
|
1764
1792
|
return THEME_ACCENTS[id ?? "default"] ?? THEME_ACCENTS["default"];
|
|
@@ -1786,6 +1814,10 @@ var FONTS = {
|
|
|
1786
1814
|
stack: "system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang TC','Noto Sans TC',sans-serif"
|
|
1787
1815
|
},
|
|
1788
1816
|
serif: { stack: "Georgia,'Noto Serif TC','Times New Roman',serif" },
|
|
1817
|
+
inter: {
|
|
1818
|
+
stack: "Inter,'PingFang TC','Noto Sans TC',-apple-system,'Segoe UI',sans-serif",
|
|
1819
|
+
google: "Inter:wght@400;600;700"
|
|
1820
|
+
},
|
|
1789
1821
|
exo2: {
|
|
1790
1822
|
stack: "'Exo 2','PingFang TC','Noto Sans TC',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
|
|
1791
1823
|
google: "Exo+2:wght@400;500;600;700"
|
|
@@ -2032,6 +2064,13 @@ function trustedHost(c) {
|
|
|
2032
2064
|
const trusted = expected && forwarded && c.req.header("x-share-proxy-key") === expected ? forwarded : void 0;
|
|
2033
2065
|
return trusted ?? c.req.header("host");
|
|
2034
2066
|
}
|
|
2067
|
+
function isDevHost(c) {
|
|
2068
|
+
const expected = process.env["SHARE_PROXY_KEY"];
|
|
2069
|
+
const forwarded = c.req.header("x-forwarded-host");
|
|
2070
|
+
return Boolean(
|
|
2071
|
+
expected && forwarded?.startsWith("dev-") && c.req.header("x-share-proxy-key") === expected
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2035
2074
|
|
|
2036
2075
|
// src/dashboard/cors.ts
|
|
2037
2076
|
function allowedOrigins() {
|
|
@@ -2174,8 +2213,8 @@ async function listSetupInvites() {
|
|
|
2174
2213
|
|
|
2175
2214
|
// src/workspace-slug.ts
|
|
2176
2215
|
var SLUG_PATTERN = /^[a-z](?:[a-z0-9]|-(?!-))*[a-z0-9]$/;
|
|
2177
|
-
var SLUG_MIN =
|
|
2178
|
-
var SLUG_MAX =
|
|
2216
|
+
var SLUG_MIN = 5;
|
|
2217
|
+
var SLUG_MAX = 15;
|
|
2179
2218
|
var RESERVED = /* @__PURE__ */ new Set([
|
|
2180
2219
|
// Platform hosts and routes (`app`, `www`, `admin` are also host-profile.ts's RESERVED_LABELS).
|
|
2181
2220
|
"app",
|
|
@@ -2493,6 +2532,7 @@ async function provisionWorkspace(params) {
|
|
|
2493
2532
|
);
|
|
2494
2533
|
throw err;
|
|
2495
2534
|
}
|
|
2535
|
+
await runWithProfile(params.slug, ensureGeneralChannel).catch((err) => console.error(`[workspace-setup] general channel for ${params.slug} failed:`, err));
|
|
2496
2536
|
clearKnownWorkspacesCache();
|
|
2497
2537
|
return workspace;
|
|
2498
2538
|
}
|
|
@@ -2559,7 +2599,6 @@ body { margin:0; }
|
|
|
2559
2599
|
@media (min-width:800px) {
|
|
2560
2600
|
.fs-split { grid-template-columns:minmax(0,5fr) minmax(0,6fr); gap:56px; }
|
|
2561
2601
|
.fs-split > div + div { border-left:1px solid ${base.line}; padding-left:56px; }
|
|
2562
|
-
.fs-pair { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
2563
2602
|
}
|
|
2564
2603
|
</style>
|
|
2565
2604
|
</head>
|
|
@@ -2603,9 +2642,9 @@ function hostDiagram(display, accent) {
|
|
|
2603
2642
|
const agent = (x, title, path, fill) => {
|
|
2604
2643
|
const unit = 16 + 8 + title.length * 7;
|
|
2605
2644
|
const start = x + 67 - unit / 2;
|
|
2606
|
-
return `<rect x="${x}" y="
|
|
2645
|
+
return `<rect x="${x}" y="116" width="134" height="38" rx="8" fill="${DARK.bg}" stroke="${DARK.line}"/><svg x="${start}" y="127" width="16" height="16" viewBox="0 0 24 24"><path d="${path}" fill="${fill}" fill-rule="evenodd"/></svg><text x="${start + 24}" y="135" dominant-baseline="central" fill="${DARK.ink}" font-size="13" font-weight="600">${title}</text>`;
|
|
2607
2646
|
};
|
|
2608
|
-
return `<svg viewBox="0 0 320
|
|
2647
|
+
return `<svg viewBox="0 0 320 170" width="100%" role="img" aria-labelledby="fs-host-title" style="display:block;max-width:360px;margin:20px 0 0;font-family:${display};"><title id="fs-host-title">The Flyspace Host on your machine connects to your workspace over the Flyspace Socket and runs Claude Code or Codex CLI.</title><defs><marker id="fs-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="${accent}"/></marker></defs>` + box(10, "Your Workspace") + arrow(88, 51) + `<text x="172" y="73" fill="${DARK.dim}" font-size="11">Flyspace Socket</text><rect x="8" y="90" width="304" height="74" rx="10" fill="none" stroke="${DARK.dim}" stroke-dasharray="4 4"/><text x="20" y="108" fill="${DARK.dim}" font-size="10" font-weight="600" letter-spacing="0.06em">YOUR MACHINE</text>` + agent(20, "Claude Code", CLAUDE_PATH, "#D97757") + agent(166, "Codex CLI", CODEX_PATH, DARK.ink) + `</svg>`;
|
|
2609
2648
|
}
|
|
2610
2649
|
function setupPage(c, token, invite, form = {}) {
|
|
2611
2650
|
const display = fontOf(void 0).stack;
|
|
@@ -2613,7 +2652,7 @@ function setupPage(c, token, invite, form = {}) {
|
|
|
2613
2652
|
const host = trustedHost(c) ?? "";
|
|
2614
2653
|
const suffix = `.${host.replace(/:\d+$/, "").split(".").slice(1).join(".")}`;
|
|
2615
2654
|
return page(
|
|
2616
|
-
`<div class="fs-split"><div>` + masthead2("Flyspace", FLYSPACE_MARK, display, true) + `<div class="fs-ink" style="font:600 20px/1.3 ${display};color:${DARK.ink};margin-bottom:10px;">Create your workspace</div><p class="fs-ink" style="margin:0 0 8px;font:500 15px/1.5 ${display};color:${DARK.ink};">Welcome to the early access cohort.</p><p class="fs-muted" style="margin:0;font:400 14px/1.6 ${display};color:${DARK.muted};">Your agents run on your own machine. The Flyspace Host connects out to this workspace, so Claude Code or Codex works in each channel's real folder, with your own tools and keys.</p>` + hostDiagram(display, accent) + `</div><div><form id="setup" method="post" action="/auth/new-workspace"><input type="hidden" name="token" value="${escapeHtml(token)}"><div class="fs-dim" style="margin:0 0 14px;font:600 11px/1.4 ${display};letter-spacing:0.06em;text-transform:uppercase;color:${DARK.dim};">Your account</div><label for="username" class="fs-muted" style="display:block;font:400 13px/1.4 ${display};color:${DARK.muted};margin-bottom:4px;">You'll be its Owner, signed in as</label><input id="username" name="username" type="email" autocomplete="username" readonly tabindex="-1" value="${escapeHtml(invite.email)}" style="display:block;width:100%;box-sizing:border-box;border:0;outline:0;background:transparent;padding:0;margin-bottom:20px;font:600 15px/1.4 ${display};color:${DARK.ink};text-overflow:ellipsis;"><div
|
|
2655
|
+
`<div class="fs-split"><div>` + masthead2("Flyspace", FLYSPACE_MARK, display, true) + `<div class="fs-ink" style="font:600 20px/1.3 ${display};color:${DARK.ink};margin-bottom:10px;">Create your workspace</div><p class="fs-ink" style="margin:0 0 8px;font:500 15px/1.5 ${display};color:${DARK.ink};">Welcome to the early access cohort.</p><p class="fs-muted" style="margin:0;font:400 14px/1.6 ${display};color:${DARK.muted};">Your agents run on your own machine. The Flyspace Host connects out to this workspace, so Claude Code or Codex works in each channel's real folder, with your own tools and keys.</p>` + hostDiagram(display, accent) + `</div><div><form id="setup" method="post" action="/auth/new-workspace"><input type="hidden" name="token" value="${escapeHtml(token)}"><div class="fs-dim" style="margin:0 0 14px;font:600 11px/1.4 ${display};letter-spacing:0.06em;text-transform:uppercase;color:${DARK.dim};">Your account</div><label for="username" class="fs-muted" style="display:block;font:400 13px/1.4 ${display};color:${DARK.muted};margin-bottom:4px;">You'll be its Owner, signed in as</label><input id="username" name="username" type="email" autocomplete="username" readonly tabindex="-1" value="${escapeHtml(invite.email)}" style="display:block;width:100%;box-sizing:border-box;border:0;outline:0;background:transparent;padding:0;margin-bottom:20px;font:600 15px/1.4 ${display};color:${DARK.ink};text-overflow:ellipsis;"><div style="margin-bottom:14px;">` + passwordField("password", "Password", "new-password", display, !!form.passwordError) + `</div><div>` + passwordField("confirm", "Confirm password", "new-password", display, !!form.passwordError) + `</div>` + (form.passwordError ? `<p style="margin:-8px 0 16px;font:500 13px/1.5 ${display};color:#d33;">${escapeHtml(form.passwordError)}</p>` : `<p class="fs-dim" style="margin:-8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">At least ${PASSWORD_MIN} characters. You'll use it to sign in to your workspace.</p>`) + `<div style="border-top:1px solid ${DARK.line};margin:4px 0 24px;"></div><div class="fs-dim" style="margin:0 0 14px;font:600 11px/1.4 ${display};letter-spacing:0.06em;text-transform:uppercase;color:${DARK.dim};">Your workspace</div><label for="name" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">Workspace name</label><input id="name" name="name" required autocomplete="organization" maxlength="${DISPLAY_NAME_MAX}" value="${escapeHtml(form.name ?? "")}" placeholder="My Team" class="fs-card" style="display:block;width:100%;box-sizing:border-box;border:1px solid ${DARK.line};border-radius:8px;padding:12px;outline:0;background:transparent;font:500 15px/1.2 ${display};color:inherit;"><p class="fs-dim" style="margin:8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">Shown across your workspace. You can change it later.</p><label for="slug" class="fs-ink" style="display:block;font:600 13px/1.4 ${display};color:${DARK.ink};margin-bottom:6px;">Workspace URL</label><div class="fs-card" style="display:flex;align-items:center;border:1px solid ${form.error ? "#d33" : DARK.line};border-radius:8px;padding:0 12px;"><input id="slug" name="slug" required autocomplete="off" autocapitalize="none" spellcheck="false" minlength="${SLUG_MIN}" maxlength="${SLUG_MAX}" pattern="[a-z](?:[a-z0-9]|-(?!-))*[a-z0-9]" value="${escapeHtml(form.slug ?? "")}" placeholder="my-team" style="flex:1;min-width:0;border:0;outline:0;background:transparent;padding:12px 0;font:500 15px/1.2 ${display};color:inherit;"><span class="fs-dim" style="font:400 15px/1 ${display};color:${DARK.dim};white-space:nowrap;">${escapeHtml(suffix)}</span></div>` + (form.error ? `<p style="margin:8px 0 0;font:500 13px/1.5 ${display};color:#d33;">${escapeHtml(form.error)}</p>` : "") + `<p class="fs-dim" style="margin:8px 0 20px;font:400 12px/1.6 ${display};color:${DARK.dim};">${SLUG_MIN} to ${SLUG_MAX} lowercase letters, digits or hyphens. <strong style="font-weight:600;color:${DARK.ink};">Can't be changed later.</strong></p><div style="border-top:1px solid ${DARK.line};margin:4px 0 20px;"></div><label class="fs-dim" style="display:flex;align-items:flex-start;gap:10px;margin:0 0 16px;font:400 13px/1.5 ${display};color:${DARK.dim};cursor:pointer;"><input type="checkbox" name="terms" required style="margin:3px 0 0;flex:none;accent-color:${accent};"><span>I agree to the <a href="https://flyspace.ai/terms/" target="_blank" rel="noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">Terms</a> and <a href="https://flyspace.ai/privacy/" target="_blank" rel="noreferrer" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">Privacy Policy</a>.</span></label><button id="setup-submit" class="fs-btn" type="submit" style="background:${accent};color:${DARK.onAccent};border-radius:8px;padding:13px 24px;width:100%;font:600 15px/1 ${display};">Create workspace</button></form><div style="margin-top:20px;border-left:2px solid ${accent};padding:2px 0 2px 12px;"><p class="fs-muted" style="margin:0;font:400 13px/1.6 ${display};color:${DARK.muted};"><strong class="fs-ink" style="font-weight:600;color:${DARK.ink};">Next:</strong> we'll help you connect Claude Code or Codex CLI.</p></div></div></div><script>(()=>{const n=document.getElementById('name'),s=document.getElementById('slug');const K='fs-setup-draft';try{const d=JSON.parse(sessionStorage.getItem(K)||'{}');if(!n.value&&d.name)n.value=d.name;if(!s.value&&d.slug)s.value=d.slug}catch(e){}const save=()=>{try{sessionStorage.setItem(K,JSON.stringify({name:n.value,slug:s.value}))}catch(e){}};let touched=!!s.value;s.addEventListener('input',()=>{touched=true});n.addEventListener('input',()=>{if(touched)return;s.value=n.value.normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').replace(/^[^a-z]+/,'').slice(0,${SLUG_MAX}).replace(/-+$/,'')});n.addEventListener('input',save);s.addEventListener('input',save)})()</script><script>document.getElementById('setup').addEventListener('submit',()=>{const b=document.getElementById('setup-submit');b.disabled=true;b.style.opacity='0.7';b.style.cursor='progress';b.textContent='Creating workspace\u2026'})</script><script>document.querySelectorAll('[data-toggle-password]').forEach(b=>b.addEventListener('click',()=>{const i=document.getElementById(b.dataset.togglePassword),show=i.type==='password';i.type=show?'text':'password';b.setAttribute('aria-pressed',String(show));b.setAttribute('aria-label',show?'Hide password':'Show password');b.querySelector('[data-eye]').style.display=show?'none':'';b.querySelector('[data-eye-off]').style.display=show?'':'none'}))</script>`,
|
|
2617
2656
|
void 0,
|
|
2618
2657
|
void 0,
|
|
2619
2658
|
true,
|
|
@@ -2789,7 +2828,11 @@ function createAuthConfirmRouter() {
|
|
|
2789
2828
|
invite.email,
|
|
2790
2829
|
invite.ownerName ? { name: invite.ownerName } : {}
|
|
2791
2830
|
);
|
|
2792
|
-
const { error: error2 } = await getSupabaseClient().auth.admin.updateUserById(userId, {
|
|
2831
|
+
const { error: error2 } = await getSupabaseClient().auth.admin.updateUserById(userId, {
|
|
2832
|
+
password,
|
|
2833
|
+
// The owner just chose one: don't ask again during onboarding.
|
|
2834
|
+
user_metadata: { passwordSetAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2835
|
+
});
|
|
2793
2836
|
if (error2) {
|
|
2794
2837
|
const weak = error2.code === "weak_password";
|
|
2795
2838
|
if (!weak) console.error(`[new-workspace] setting the password for ${invite.email} failed:`, error2);
|
|
@@ -2849,9 +2892,7 @@ function createAuthConfirmRouter() {
|
|
|
2849
2892
|
const fragment = new URLSearchParams({
|
|
2850
2893
|
access_token: session.accessToken,
|
|
2851
2894
|
refresh_token: session.refreshToken,
|
|
2852
|
-
type: "magiclink"
|
|
2853
|
-
// AuthCallback lands a fresh owner on the connect-your-agent page.
|
|
2854
|
-
next: "connect"
|
|
2895
|
+
type: "magiclink"
|
|
2855
2896
|
});
|
|
2856
2897
|
return c.redirect(`${origin}/auth/callback#${fragment}`, 303);
|
|
2857
2898
|
} catch (err) {
|
|
@@ -2957,8 +2998,8 @@ async function deliverSignInLink(email, redirectTo) {
|
|
|
2957
2998
|
// src/dashboard/server.ts
|
|
2958
2999
|
import { existsSync as existsSync27 } from "fs";
|
|
2959
3000
|
import { createServer } from "net";
|
|
2960
|
-
import { join as
|
|
2961
|
-
import { Hono as
|
|
3001
|
+
import { join as join21 } from "path";
|
|
3002
|
+
import { Hono as Hono32 } from "hono";
|
|
2962
3003
|
import { serve } from "@hono/node-server";
|
|
2963
3004
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
2964
3005
|
|
|
@@ -3003,24 +3044,157 @@ async function reconcileAll() {
|
|
|
3003
3044
|
return results;
|
|
3004
3045
|
}
|
|
3005
3046
|
|
|
3047
|
+
// src/store/channel-members-store.ts
|
|
3048
|
+
function rowToChannelMember(row) {
|
|
3049
|
+
return {
|
|
3050
|
+
id: row["id"],
|
|
3051
|
+
channelId: row["channel_id"],
|
|
3052
|
+
membershipId: row["membership_id"],
|
|
3053
|
+
role: row["role"],
|
|
3054
|
+
addedAt: row["added_at"]
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
async function listChannelMembers(channelId) {
|
|
3058
|
+
const { data, error: error2 } = await getSupabaseClient().from("channel_members").select("*").eq("channel_id", channelId).order("added_at", { ascending: true });
|
|
3059
|
+
if (error2) throw new Error(`listChannelMembers(${channelId}): ${error2.message}`);
|
|
3060
|
+
return (data ?? []).map(rowToChannelMember);
|
|
3061
|
+
}
|
|
3062
|
+
async function upsertChannelMember(params) {
|
|
3063
|
+
const { data, error: error2 } = await getSupabaseClient().from("channel_members").upsert(
|
|
3064
|
+
{ channel_id: params.channelId, membership_id: params.membershipId, role: params.role },
|
|
3065
|
+
{ onConflict: "channel_id,membership_id" }
|
|
3066
|
+
).select("*").single();
|
|
3067
|
+
if (error2) {
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`upsertChannelMember(${params.channelId}, ${params.membershipId}): ${error2.message}`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
return rowToChannelMember(data);
|
|
3073
|
+
}
|
|
3074
|
+
async function removeChannelMember(channelId, membershipId) {
|
|
3075
|
+
const { data, error: error2 } = await getSupabaseClient().from("channel_members").delete().eq("channel_id", channelId).eq("membership_id", membershipId).select("id");
|
|
3076
|
+
if (error2) throw new Error(`removeChannelMember(${channelId}, ${membershipId}): ${error2.message}`);
|
|
3077
|
+
return data?.length ? true : false;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// src/store/channel-docs-access-store.ts
|
|
3081
|
+
var RANK = { none: 0, read: 1, write: 2 };
|
|
3082
|
+
function docsAccessAtLeast(have, need) {
|
|
3083
|
+
return RANK[have] >= RANK[need];
|
|
3084
|
+
}
|
|
3085
|
+
function isDocsGrant(value) {
|
|
3086
|
+
return value === "read" || value === "write";
|
|
3087
|
+
}
|
|
3088
|
+
function rowToGrant(row) {
|
|
3089
|
+
return {
|
|
3090
|
+
id: row["id"],
|
|
3091
|
+
channelId: row["channel_id"],
|
|
3092
|
+
membershipId: row["membership_id"],
|
|
3093
|
+
access: row["access"],
|
|
3094
|
+
grantedAt: row["granted_at"]
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
async function listChannelDocsAccess(channelId) {
|
|
3098
|
+
const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").select("*").eq("channel_id", channelId).order("granted_at", { ascending: true });
|
|
3099
|
+
if (error2) throw new Error(`listChannelDocsAccess(${channelId}): ${error2.message}`);
|
|
3100
|
+
return (data ?? []).map(rowToGrant);
|
|
3101
|
+
}
|
|
3102
|
+
async function getChannelDocsGrant(channelId, membershipId) {
|
|
3103
|
+
const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").select("*").eq("channel_id", channelId).eq("membership_id", membershipId).maybeSingle();
|
|
3104
|
+
if (error2) throw new Error(`getChannelDocsGrant(${channelId}, ${membershipId}): ${error2.message}`);
|
|
3105
|
+
return data ? rowToGrant(data) : null;
|
|
3106
|
+
}
|
|
3107
|
+
async function upsertChannelDocsAccess(params) {
|
|
3108
|
+
const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").upsert(
|
|
3109
|
+
{ channel_id: params.channelId, membership_id: params.membershipId, access: params.access },
|
|
3110
|
+
{ onConflict: "channel_id,membership_id" }
|
|
3111
|
+
).select("*").single();
|
|
3112
|
+
if (error2) {
|
|
3113
|
+
throw new Error(
|
|
3114
|
+
`upsertChannelDocsAccess(${params.channelId}, ${params.membershipId}): ${error2.message}`
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
return rowToGrant(data);
|
|
3118
|
+
}
|
|
3119
|
+
async function removeChannelDocsAccess(channelId, membershipId) {
|
|
3120
|
+
const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").delete().eq("channel_id", channelId).eq("membership_id", membershipId).select("id");
|
|
3121
|
+
if (error2) {
|
|
3122
|
+
throw new Error(`removeChannelDocsAccess(${channelId}, ${membershipId}): ${error2.message}`);
|
|
3123
|
+
}
|
|
3124
|
+
return data?.length ? true : false;
|
|
3125
|
+
}
|
|
3126
|
+
async function resolveForMembership(membership, channelId) {
|
|
3127
|
+
if (membership.role === "admin") return "write";
|
|
3128
|
+
const members = await listChannelMembers(channelId);
|
|
3129
|
+
if (members.some((m) => m.membershipId === membership.id && m.role === "channel_admin")) {
|
|
3130
|
+
return "write";
|
|
3131
|
+
}
|
|
3132
|
+
const grant = await getChannelDocsGrant(channelId, membership.id);
|
|
3133
|
+
return grant?.access ?? "none";
|
|
3134
|
+
}
|
|
3135
|
+
async function resolveDocsAccess(ctx, channelId) {
|
|
3136
|
+
if (ctx.role === "admin") return "write";
|
|
3137
|
+
const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
|
|
3138
|
+
if (!membership) return "none";
|
|
3139
|
+
return resolveForMembership(membership, channelId);
|
|
3140
|
+
}
|
|
3141
|
+
async function resolveDocsAccessByHandle(workspaceSlug, handle, channelId) {
|
|
3142
|
+
const workspace = await getWorkspaceBySlug(workspaceSlug);
|
|
3143
|
+
if (!workspace) return null;
|
|
3144
|
+
const wanted = handle.toLowerCase();
|
|
3145
|
+
const membership = (await listMemberships(workspace.id)).find(
|
|
3146
|
+
(m) => m.handle.toLowerCase() === wanted
|
|
3147
|
+
);
|
|
3148
|
+
if (!membership) return "none";
|
|
3149
|
+
return resolveForMembership(membership, channelId);
|
|
3150
|
+
}
|
|
3151
|
+
async function filterReadableDocsChannels(ctx, channelIds) {
|
|
3152
|
+
if (ctx.role === "admin") return new Set(channelIds);
|
|
3153
|
+
const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
|
|
3154
|
+
if (!membership) return /* @__PURE__ */ new Set();
|
|
3155
|
+
const db = getSupabaseClient();
|
|
3156
|
+
const [grants, roles] = await Promise.all([
|
|
3157
|
+
db.from("channel_docs_access").select("channel_id").eq("membership_id", membership.id),
|
|
3158
|
+
db.from("channel_members").select("channel_id, role").eq("membership_id", membership.id)
|
|
3159
|
+
]);
|
|
3160
|
+
if (grants.error) throw new Error(`filterReadableDocsChannels: ${grants.error.message}`);
|
|
3161
|
+
if (roles.error) throw new Error(`filterReadableDocsChannels: ${roles.error.message}`);
|
|
3162
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
3163
|
+
for (const r of grants.data ?? []) allowed.add(r.channel_id);
|
|
3164
|
+
for (const r of roles.data ?? []) {
|
|
3165
|
+
if (r.role === "channel_admin") allowed.add(r.channel_id);
|
|
3166
|
+
}
|
|
3167
|
+
return new Set(channelIds.filter((id) => allowed.has(id)));
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/dashboard/docs-access-gate.ts
|
|
3171
|
+
var requireDocsAccess = async (c, next) => {
|
|
3172
|
+
const auth = c.get("auth");
|
|
3173
|
+
const need = c.req.method === "GET" ? "read" : "write";
|
|
3174
|
+
const have = auth ? await resolveDocsAccess(auth, c.req.param("id") ?? "") : "none";
|
|
3175
|
+
if (!docsAccessAtLeast(have, need)) return c.json({ error: "docs access required" }, 403);
|
|
3176
|
+
await next();
|
|
3177
|
+
};
|
|
3178
|
+
|
|
3006
3179
|
// src/dashboard/api.ts
|
|
3007
|
-
import { Hono as
|
|
3180
|
+
import { Hono as Hono29 } from "hono";
|
|
3008
3181
|
|
|
3009
3182
|
// src/dashboard/public-share-routes.ts
|
|
3010
3183
|
import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
3011
|
-
import { join as
|
|
3184
|
+
import { join as join9 } from "path";
|
|
3012
3185
|
import { Hono as Hono3 } from "hono";
|
|
3013
3186
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
3014
3187
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3015
3188
|
|
|
3016
3189
|
// src/store/doc-share-store.ts
|
|
3017
3190
|
function rowToKind(value) {
|
|
3018
|
-
return value === "html"
|
|
3191
|
+
return value === "html" || value === "excalidraw" ? value : "markdown";
|
|
3019
3192
|
}
|
|
3020
3193
|
function rowToShare(row) {
|
|
3021
3194
|
return {
|
|
3022
3195
|
id: row["id"],
|
|
3023
3196
|
slug: row["slug"] ?? null,
|
|
3197
|
+
tags: Array.isArray(row["tags"]) ? row["tags"] : [],
|
|
3024
3198
|
workspaceId: row["workspace_id"],
|
|
3025
3199
|
profile: row["profile"],
|
|
3026
3200
|
channelId: row["channel_id"],
|
|
@@ -3087,11 +3261,12 @@ async function setDocShareSlug(id, slug) {
|
|
|
3087
3261
|
if (error2) throw new Error(`setDocShareSlug(${id}): ${error2.message}`);
|
|
3088
3262
|
return data ? rowToShare(data) : null;
|
|
3089
3263
|
}
|
|
3090
|
-
var SUMMARY_COLUMNS = "id, slug, profile, channel_id, source_path, title, kind, created_by, created_at, updated_at, audio_enabled, ask_enabled, social_enabled, branding_enabled, translation_enabled, translation_langs, audio_status";
|
|
3264
|
+
var SUMMARY_COLUMNS = "id, slug, tags, profile, channel_id, source_path, title, kind, created_by, created_at, updated_at, audio_enabled, ask_enabled, social_enabled, branding_enabled, translation_enabled, translation_langs, audio_status";
|
|
3091
3265
|
function rowToSummary(row) {
|
|
3092
3266
|
return {
|
|
3093
3267
|
id: row["id"],
|
|
3094
3268
|
slug: row["slug"] ?? null,
|
|
3269
|
+
tags: Array.isArray(row["tags"]) ? row["tags"] : [],
|
|
3095
3270
|
profile: row["profile"],
|
|
3096
3271
|
channelId: row["channel_id"],
|
|
3097
3272
|
sourcePath: row["source_path"],
|
|
@@ -3166,6 +3341,11 @@ async function updateDocShareSnapshot(id, snapshot2, features) {
|
|
|
3166
3341
|
if (error2) throw new Error(`updateDocShareSnapshot(${id}): ${error2.message}`);
|
|
3167
3342
|
return rowToShare(data);
|
|
3168
3343
|
}
|
|
3344
|
+
async function setDocShareTags(id, tags) {
|
|
3345
|
+
const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({ tags }).eq("id", id).is("revoked_at", null).select("*").maybeSingle();
|
|
3346
|
+
if (error2) throw new Error(`setDocShareTags(${id}): ${error2.message}`);
|
|
3347
|
+
return data ? rowToShare(data) : null;
|
|
3348
|
+
}
|
|
3169
3349
|
async function updateDocShareFeatures(id, features) {
|
|
3170
3350
|
const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
|
|
3171
3351
|
...features.audioEnabled !== void 0 ? { audio_enabled: features.audioEnabled } : {},
|
|
@@ -3467,11 +3647,11 @@ function injectShareHead(html, head) {
|
|
|
3467
3647
|
// src/dashboard/app-icon.ts
|
|
3468
3648
|
import { createHash as createHash2 } from "crypto";
|
|
3469
3649
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3470
|
-
import { join as
|
|
3650
|
+
import { join as join8 } from "path";
|
|
3471
3651
|
import sharp from "sharp";
|
|
3472
3652
|
var PLATE = "#0A0A0F";
|
|
3473
|
-
var MARK_FILE =
|
|
3474
|
-
var MARK_SVG_FILE =
|
|
3653
|
+
var MARK_FILE = join8(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
|
|
3654
|
+
var MARK_SVG_FILE = join8(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
|
|
3475
3655
|
var LAYOUT = {
|
|
3476
3656
|
standard: { logo: 0.7, logoCx: 0.5, logoCy: 0.48, badge: 0.27, badgeCx: 0.22, badgeCy: 0.76 },
|
|
3477
3657
|
maskable: { logo: 0.56, logoCx: 0.5, logoCy: 0.48, badge: 0.23, badgeCx: 0.33, badgeCy: 0.66 }
|
|
@@ -4636,6 +4816,9 @@ function shareLangAudio(share, lang, audio) {
|
|
|
4636
4816
|
truncated: isArticleTruncated(share.content)
|
|
4637
4817
|
};
|
|
4638
4818
|
}
|
|
4819
|
+
function shareDescription(share, shown) {
|
|
4820
|
+
return share.kind === "excalidraw" ? "" : excerptFromMarkdown(shown.content, shown.title);
|
|
4821
|
+
}
|
|
4639
4822
|
function createPublicShareApiRouter() {
|
|
4640
4823
|
const app = new Hono3();
|
|
4641
4824
|
app.get("/:id", async (c) => {
|
|
@@ -4784,7 +4967,7 @@ function serveBuildFile(c, webDist, dir, name) {
|
|
|
4784
4967
|
if (!type || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes("..")) {
|
|
4785
4968
|
return c.text("not found", 404);
|
|
4786
4969
|
}
|
|
4787
|
-
const path =
|
|
4970
|
+
const path = join9(webDist, dir, name);
|
|
4788
4971
|
if (!existsSync11(path)) return c.text("not found", 404);
|
|
4789
4972
|
c.header("Content-Type", type);
|
|
4790
4973
|
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
@@ -4812,7 +4995,7 @@ function createPublicShareAssetRouter(webDist) {
|
|
|
4812
4995
|
const workspace = await workspaceOf(share);
|
|
4813
4996
|
const png = await renderShareOgImage({
|
|
4814
4997
|
title: shown.title,
|
|
4815
|
-
description:
|
|
4998
|
+
description: shareDescription(share, shown),
|
|
4816
4999
|
// Branding off drops the card's header row too. An unfurl in Slack or on
|
|
4817
5000
|
// LinkedIn is the first thing a reader sees, so a page that doesn't sign
|
|
4818
5001
|
// itself and a card that does would be the same contradiction the flag
|
|
@@ -4876,7 +5059,7 @@ function createPublicShareAssetRouter(webDist) {
|
|
|
4876
5059
|
const id = c.req.param("file").replace(/\.md$/, "");
|
|
4877
5060
|
const share = await getDocShare(id);
|
|
4878
5061
|
if (!share) return c.json({ error: "not found" }, 404);
|
|
4879
|
-
if (share.kind
|
|
5062
|
+
if (share.kind !== "markdown") return c.json({ error: "not found" }, 404);
|
|
4880
5063
|
c.header("Content-Type", "text/markdown; charset=utf-8");
|
|
4881
5064
|
c.header("Content-Disposition", "inline");
|
|
4882
5065
|
c.header("Cache-Control", "no-store");
|
|
@@ -4890,7 +5073,7 @@ function createPublicShareAssetRouter(webDist) {
|
|
|
4890
5073
|
const writtenIn = opts.noindex ? share.sourceLang : opts.lang ?? share.sourceLang;
|
|
4891
5074
|
const head = buildShareHead({
|
|
4892
5075
|
title: shown.title,
|
|
4893
|
-
description:
|
|
5076
|
+
description: shareDescription(share, shown),
|
|
4894
5077
|
workspaceName: publisher,
|
|
4895
5078
|
pageUrl: opts.lang ? `${base}/${opts.lang}` : base,
|
|
4896
5079
|
// The card follows the page's language where there is one to follow (see
|
|
@@ -4912,11 +5095,9 @@ function createPublicShareAssetRouter(webDist) {
|
|
|
4912
5095
|
workspaceName: publisher,
|
|
4913
5096
|
published: formatPublished(share.updatedAt) || void 0
|
|
4914
5097
|
});
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
body
|
|
4919
|
-
);
|
|
5098
|
+
const headed = injectShareHead(readShell(shell), analytics ? `${head}
|
|
5099
|
+
${analytics}` : head);
|
|
5100
|
+
let html = share.kind === "excalidraw" ? headed : injectShareBody(headed, body);
|
|
4920
5101
|
if (writtenIn) html = setHtmlLang(html, writtenIn);
|
|
4921
5102
|
c.header("Content-Type", "text/html; charset=utf-8");
|
|
4922
5103
|
c.header("Cache-Control", "no-store");
|
|
@@ -4926,7 +5107,7 @@ ${analytics}` : head),
|
|
|
4926
5107
|
const requested = c.req.param("lang");
|
|
4927
5108
|
const lang = canonicalLanguage(requested);
|
|
4928
5109
|
if (!isTranslatableLanguage(lang)) return next();
|
|
4929
|
-
const shell =
|
|
5110
|
+
const shell = join9(webDist, "index.html");
|
|
4930
5111
|
if (!existsSync11(shell)) return next();
|
|
4931
5112
|
const share = await getDocShare(c.req.param("id"));
|
|
4932
5113
|
if (!share) return next();
|
|
@@ -4962,7 +5143,7 @@ ${analytics}` : head),
|
|
|
4962
5143
|
const share = await getDocShare(c.req.param("id"));
|
|
4963
5144
|
if (!share) return next();
|
|
4964
5145
|
if (share.kind === "html") return servePublishedHtml(c, share);
|
|
4965
|
-
const shell =
|
|
5146
|
+
const shell = join9(webDist, "index.html");
|
|
4966
5147
|
if (!existsSync11(shell)) return next();
|
|
4967
5148
|
return readerPage(
|
|
4968
5149
|
c,
|
|
@@ -5033,6 +5214,48 @@ import { Hono as Hono5 } from "hono";
|
|
|
5033
5214
|
|
|
5034
5215
|
// src/dashboard/supabase-server-client.ts
|
|
5035
5216
|
import { createServerClient, parseCookieHeader, serializeCookieHeader } from "@supabase/ssr";
|
|
5217
|
+
|
|
5218
|
+
// src/dashboard/scoped-auth-cookies.ts
|
|
5219
|
+
import { stringFromBase64URL } from "@supabase/ssr";
|
|
5220
|
+
var SESSION_NAME = /^(sb-.+-auth-token)(?:\.\d+)?$/;
|
|
5221
|
+
var SCOPE_MARK = "fsw_";
|
|
5222
|
+
var SCOPE_END = "__";
|
|
5223
|
+
function hostLabel(host) {
|
|
5224
|
+
return host.toLowerCase().replace(/:\d+$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5225
|
+
}
|
|
5226
|
+
var scopePrefix = (label2) => `${SCOPE_MARK}${label2}${SCOPE_END}`;
|
|
5227
|
+
function sessionUserId(cookies) {
|
|
5228
|
+
const base = cookies.map((c) => SESSION_NAME.exec(c.name)?.[1]).find(Boolean);
|
|
5229
|
+
if (!base) return null;
|
|
5230
|
+
const whole = cookies.find((c) => c.name === base)?.value;
|
|
5231
|
+
const raw = whole ?? cookies.filter((c) => c.name.startsWith(`${base}.`)).sort((a, b) => Number(a.name.split(".").pop()) - Number(b.name.split(".").pop())).map((c) => c.value).join("");
|
|
5232
|
+
if (!raw) return null;
|
|
5233
|
+
try {
|
|
5234
|
+
const json = raw.startsWith("base64-") ? stringFromBase64URL(raw.slice(7)) : raw;
|
|
5235
|
+
const id = JSON.parse(json).user?.id;
|
|
5236
|
+
return typeof id === "string" ? id : null;
|
|
5237
|
+
} catch {
|
|
5238
|
+
return null;
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
var isScoped = (name) => name.startsWith(SCOPE_MARK);
|
|
5242
|
+
function readScopedCookies(all, label2) {
|
|
5243
|
+
const prefix = scopePrefix(label2);
|
|
5244
|
+
const own = all.filter((c) => c.name.startsWith(prefix)).map((c) => ({ name: c.name.slice(prefix.length), value: c.value }));
|
|
5245
|
+
return own.length ? own : all.filter((c) => !isScoped(c.name));
|
|
5246
|
+
}
|
|
5247
|
+
function routeCookieWrites(all, label2, writes) {
|
|
5248
|
+
const prefix = scopePrefix(label2);
|
|
5249
|
+
const hasOwn = all.some((c) => c.name.startsWith(prefix));
|
|
5250
|
+
const incoming = sessionUserId(writes.filter((c) => c.value !== ""));
|
|
5251
|
+
const shared = sessionUserId(all.filter((c) => !isScoped(c.name)));
|
|
5252
|
+
const toScoped = hasOwn || incoming !== null && shared !== null && incoming !== shared;
|
|
5253
|
+
return writes.map(
|
|
5254
|
+
(c) => toScoped ? { name: prefix + c.name, value: c.value, scoped: true } : { ...c, scoped: false }
|
|
5255
|
+
);
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
// src/dashboard/supabase-server-client.ts
|
|
5036
5259
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
5037
5260
|
function fetchWithTimeout(input2, init) {
|
|
5038
5261
|
return fetch(input2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
@@ -5043,26 +5266,34 @@ function createRequestSupabaseClient(c) {
|
|
|
5043
5266
|
if (!url || !key) {
|
|
5044
5267
|
throw new Error("SUPABASE_URL / VITE_SUPABASE_PUBLISHABLE_KEY not set \u2014 check .env");
|
|
5045
5268
|
}
|
|
5269
|
+
const origin = c.req.header("Origin");
|
|
5270
|
+
const label2 = hostLabel(
|
|
5271
|
+
(origin ? URL.parse(origin)?.host : void 0) ?? c.req.header("X-Forwarded-Host") ?? c.req.header("Host") ?? ""
|
|
5272
|
+
);
|
|
5273
|
+
const rawCookies = () => parseCookieHeader(c.req.header("Cookie") ?? "").map(({ name, value }) => ({
|
|
5274
|
+
name,
|
|
5275
|
+
value: value ?? ""
|
|
5276
|
+
}));
|
|
5046
5277
|
return createServerClient(url, key, {
|
|
5047
5278
|
global: { fetch: fetchWithTimeout },
|
|
5048
5279
|
cookies: {
|
|
5049
5280
|
// `parseCookieHeader` types `value` as optional; the client requires a
|
|
5050
5281
|
// string, and a valueless cookie is the same as an empty one here.
|
|
5051
|
-
getAll: () =>
|
|
5052
|
-
name,
|
|
5053
|
-
value: value ?? ""
|
|
5054
|
-
})),
|
|
5282
|
+
getAll: () => readScopedCookies(rawCookies(), label2),
|
|
5055
5283
|
setAll: (cookies) => {
|
|
5056
5284
|
const domain = process.env["AUTH_COOKIE_DOMAIN"]?.trim();
|
|
5057
5285
|
const overrides = {
|
|
5058
5286
|
...domain ? { domain } : {},
|
|
5059
5287
|
...crossOriginShell(c) ? { sameSite: "none", secure: true } : {}
|
|
5060
5288
|
};
|
|
5061
|
-
|
|
5062
|
-
|
|
5289
|
+
const routed = routeCookieWrites(rawCookies(), label2, cookies);
|
|
5290
|
+
cookies.forEach(({ options }, i) => {
|
|
5291
|
+
const { name, value, scoped } = routed[i];
|
|
5292
|
+
const { domain: _domain, ...rest } = overrides;
|
|
5293
|
+
c.header("Set-Cookie", serializeCookieHeader(name, value, { ...options, ...scoped ? rest : overrides }), {
|
|
5063
5294
|
append: true
|
|
5064
5295
|
});
|
|
5065
|
-
}
|
|
5296
|
+
});
|
|
5066
5297
|
}
|
|
5067
5298
|
}
|
|
5068
5299
|
});
|
|
@@ -5071,6 +5302,14 @@ function createRequestSupabaseClient(c) {
|
|
|
5071
5302
|
// src/dashboard/auth-context.ts
|
|
5072
5303
|
import { HTTPException } from "hono/http-exception";
|
|
5073
5304
|
import { isAuthApiError, isAuthRetryableFetchError } from "@supabase/supabase-js";
|
|
5305
|
+
function onboardingPending(ctx) {
|
|
5306
|
+
const pending3 = [];
|
|
5307
|
+
if (!ctx.onboarded && !ctx.passwordSet) pending3.push("account");
|
|
5308
|
+
if (!ctx.onboarded) pending3.push("profile");
|
|
5309
|
+
if (ctx.role === "admin" && !ctx.agentConnectedAt) pending3.push("agent");
|
|
5310
|
+
if (ctx.role === "admin" && !ctx.projectsSetupAt) pending3.push("projects");
|
|
5311
|
+
return pending3;
|
|
5312
|
+
}
|
|
5074
5313
|
var inFlight3 = /* @__PURE__ */ new Map();
|
|
5075
5314
|
var CACHE_TTL_MS = 3e4;
|
|
5076
5315
|
var resolved = /* @__PURE__ */ new Map();
|
|
@@ -5082,12 +5321,13 @@ function pruneExpired(now) {
|
|
|
5082
5321
|
if (entry.expires <= now) resolved.delete(key);
|
|
5083
5322
|
}
|
|
5084
5323
|
}
|
|
5085
|
-
async function resolveAuthContext(c) {
|
|
5324
|
+
async function resolveAuthContext(c, opts = {}) {
|
|
5086
5325
|
const cookie = c.req?.header("Cookie") ?? "";
|
|
5087
5326
|
if (!cookie) return resolveAuthContextUncached(c);
|
|
5088
5327
|
const key = `${getProfile()}\0${cookie}`;
|
|
5089
5328
|
const now = Date.now();
|
|
5090
5329
|
pruneExpired(now);
|
|
5330
|
+
if (opts.fresh) resolved.delete(key);
|
|
5091
5331
|
const cached = resolved.get(key);
|
|
5092
5332
|
if (cached) return cached.value;
|
|
5093
5333
|
const inflight = inFlight3.get(key);
|
|
@@ -5142,7 +5382,9 @@ async function resolveAuthContextUncached(c) {
|
|
|
5142
5382
|
name: metadata.name,
|
|
5143
5383
|
avatarKey: metadata.avatarKey,
|
|
5144
5384
|
onboarded: Boolean(metadata.onboardedAt),
|
|
5145
|
-
|
|
5385
|
+
passwordSet: Boolean(metadata.passwordSetAt),
|
|
5386
|
+
agentConnectedAt: workspace.agentConnectedAt ?? null,
|
|
5387
|
+
projectsSetupAt: workspace.projectsSetupAt ?? null
|
|
5146
5388
|
};
|
|
5147
5389
|
}
|
|
5148
5390
|
|
|
@@ -5213,7 +5455,7 @@ function createAuthRouter() {
|
|
|
5213
5455
|
const body = await c.req.json().catch(() => ({}));
|
|
5214
5456
|
const email = body.email?.trim().toLowerCase();
|
|
5215
5457
|
if (!email) return c.json({ error: "email is required" }, 400);
|
|
5216
|
-
if (!await verifyTurnstileToken(body.captchaToken)) {
|
|
5458
|
+
if (!isDevHost(c) && !await verifyTurnstileToken(body.captchaToken)) {
|
|
5217
5459
|
return c.json({ error: "captcha check failed \u2014 please retry" }, 400);
|
|
5218
5460
|
}
|
|
5219
5461
|
const workspace = await getWorkspaceBySlug(getProfile());
|
|
@@ -5230,7 +5472,7 @@ function createAuthRouter() {
|
|
|
5230
5472
|
return c.json({ sent: true });
|
|
5231
5473
|
});
|
|
5232
5474
|
auth.get("/session", async (c) => {
|
|
5233
|
-
const ctx = await resolveAuthContext(c);
|
|
5475
|
+
const ctx = await resolveAuthContext(c, { fresh: c.req.query("fresh") === "1" });
|
|
5234
5476
|
if (!ctx) return c.json({ error: "not signed in" }, 401);
|
|
5235
5477
|
return c.json({
|
|
5236
5478
|
email: ctx.email,
|
|
@@ -5239,9 +5481,31 @@ function createAuthRouter() {
|
|
|
5239
5481
|
name: ctx.name,
|
|
5240
5482
|
avatarUrl: ctx.avatarKey ? await signAvatarUrl(ctx.avatarKey) : null,
|
|
5241
5483
|
onboarded: ctx.onboarded,
|
|
5242
|
-
|
|
5484
|
+
passwordSet: ctx.passwordSet,
|
|
5485
|
+
agentConnectedAt: ctx.agentConnectedAt,
|
|
5486
|
+
onboarding: { pending: onboardingPending(ctx) }
|
|
5243
5487
|
});
|
|
5244
5488
|
});
|
|
5489
|
+
auth.patch("/handle", async (c) => {
|
|
5490
|
+
const ctx = await resolveAuthContext(c);
|
|
5491
|
+
if (!ctx) return c.json({ error: "not signed in" }, 401);
|
|
5492
|
+
if (ctx.onboarded) return c.json({ error: "handle can no longer be changed" }, 403);
|
|
5493
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5494
|
+
const handle = body.handle?.trim().replace(/^@/, "").toLowerCase() ?? "";
|
|
5495
|
+
if (!HANDLE_PATTERN.test(handle)) {
|
|
5496
|
+
return c.json(
|
|
5497
|
+
{ error: "handle must be 1-32 characters: a-z, 0-9, _ or -, starting with a letter or digit" },
|
|
5498
|
+
400
|
|
5499
|
+
);
|
|
5500
|
+
}
|
|
5501
|
+
try {
|
|
5502
|
+
await setMembershipHandle(ctx.workspaceId, ctx.userId, handle);
|
|
5503
|
+
} catch (err) {
|
|
5504
|
+
if (err instanceof HandleTakenError) return c.json({ error: err.message }, 409);
|
|
5505
|
+
throw err;
|
|
5506
|
+
}
|
|
5507
|
+
return c.json({ handle });
|
|
5508
|
+
});
|
|
5245
5509
|
auth.post("/logout", async (c) => {
|
|
5246
5510
|
const supabase = createRequestSupabaseClient(c);
|
|
5247
5511
|
await supabase.auth.signOut();
|
|
@@ -5408,6 +5672,9 @@ function createAdminRouter() {
|
|
|
5408
5672
|
const proto = c.req.header("x-forwarded-proto") ?? "http";
|
|
5409
5673
|
const inviteUrl = new URL(`${proto}://${trustedHost(c)}/auth/new-workspace`);
|
|
5410
5674
|
inviteUrl.searchParams.set("token", token);
|
|
5675
|
+
if (body.sendEmail === false) {
|
|
5676
|
+
return c.json({ email: ownerEmail, inviteLink: inviteUrl.toString() }, 201);
|
|
5677
|
+
}
|
|
5411
5678
|
try {
|
|
5412
5679
|
await deliverWorkspaceSetupInvite({
|
|
5413
5680
|
email: ownerEmail,
|
|
@@ -5425,7 +5692,7 @@ function createAdminRouter() {
|
|
|
5425
5692
|
500
|
|
5426
5693
|
);
|
|
5427
5694
|
}
|
|
5428
|
-
return c.json({ email: ownerEmail }, 201);
|
|
5695
|
+
return c.json({ email: ownerEmail, inviteLink: inviteUrl.toString() }, 201);
|
|
5429
5696
|
});
|
|
5430
5697
|
admin.delete("/setup-invites/:id", async (c) => {
|
|
5431
5698
|
await revokeSetupInvite(c.req.param("id"));
|
|
@@ -5443,6 +5710,9 @@ function createAdminRouter() {
|
|
|
5443
5710
|
const proto = c.req.header("x-forwarded-proto") ?? "http";
|
|
5444
5711
|
const inviteUrl = new URL(`${proto}://${trustedHost(c)}/auth/new-workspace`);
|
|
5445
5712
|
inviteUrl.searchParams.set("token", token);
|
|
5713
|
+
if (c.req.query("email") === "false") {
|
|
5714
|
+
return c.json({ sent: false, inviteLink: inviteUrl.toString() });
|
|
5715
|
+
}
|
|
5446
5716
|
try {
|
|
5447
5717
|
await deliverWorkspaceSetupInvite({
|
|
5448
5718
|
email: invite.email,
|
|
@@ -5458,19 +5728,119 @@ function createAdminRouter() {
|
|
|
5458
5728
|
500
|
|
5459
5729
|
);
|
|
5460
5730
|
}
|
|
5461
|
-
return c.json({ sent: true });
|
|
5731
|
+
return c.json({ sent: true, inviteLink: inviteUrl.toString() });
|
|
5462
5732
|
});
|
|
5463
5733
|
return admin;
|
|
5464
5734
|
}
|
|
5465
5735
|
|
|
5466
5736
|
// src/dashboard/host-api.ts
|
|
5467
5737
|
import { existsSync as existsSync20, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
5468
|
-
import { join as
|
|
5738
|
+
import { join as join16 } from "path";
|
|
5469
5739
|
import sharp3 from "sharp";
|
|
5470
5740
|
|
|
5471
5741
|
// src/dashboard/channels-routes.ts
|
|
5472
5742
|
import { existsSync as existsSync12, mkdirSync as mkdirSync4 } from "fs";
|
|
5743
|
+
import { Hono as Hono9 } from "hono";
|
|
5744
|
+
|
|
5745
|
+
// src/dashboard/feature-flags-routes.ts
|
|
5473
5746
|
import { Hono as Hono8 } from "hono";
|
|
5747
|
+
|
|
5748
|
+
// src/feature-flags.ts
|
|
5749
|
+
var featureFlags = {
|
|
5750
|
+
"ai-cost": {
|
|
5751
|
+
title: "AI cost",
|
|
5752
|
+
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.",
|
|
5753
|
+
stage: "beta"
|
|
5754
|
+
},
|
|
5755
|
+
"read-aloud": {
|
|
5756
|
+
title: "Read aloud",
|
|
5757
|
+
description: "Listen to messages, threads and docs as audio. Still rough in places, and each playback uses text-to-speech.",
|
|
5758
|
+
stage: "beta"
|
|
5759
|
+
},
|
|
5760
|
+
excalidraw: {
|
|
5761
|
+
title: "Excalidraw",
|
|
5762
|
+
description: "Create and edit whiteboard drawings (.excalidraw files) in the Docs pane. Existing drawings stay viewable with this off.",
|
|
5763
|
+
stage: "beta"
|
|
5764
|
+
},
|
|
5765
|
+
sandbox: {
|
|
5766
|
+
title: "Sandbox runtime",
|
|
5767
|
+
description: "Run a channel agent in an isolated sandbox without Docker. Channels already on Sandbox keep working with this off.",
|
|
5768
|
+
stage: "beta"
|
|
5769
|
+
},
|
|
5770
|
+
dictation: {
|
|
5771
|
+
title: "Dictation",
|
|
5772
|
+
description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
|
|
5773
|
+
stage: "dev"
|
|
5774
|
+
}
|
|
5775
|
+
};
|
|
5776
|
+
var featureFlagIds = Object.keys(featureFlags);
|
|
5777
|
+
function isFeatureFlagId(id) {
|
|
5778
|
+
return Object.hasOwn(featureFlags, id);
|
|
5779
|
+
}
|
|
5780
|
+
function isFeatureVisible(stage, devHost) {
|
|
5781
|
+
return stage === "beta" || stage === "dev" && devHost;
|
|
5782
|
+
}
|
|
5783
|
+
function resolveFeatureFlags(enabledIds, devHost) {
|
|
5784
|
+
const enabled = new Set(enabledIds);
|
|
5785
|
+
const out = {};
|
|
5786
|
+
for (const id of featureFlagIds) {
|
|
5787
|
+
const stage = featureFlags[id].stage;
|
|
5788
|
+
out[id] = stage === "ga" || isFeatureVisible(stage, devHost) && enabled.has(id);
|
|
5789
|
+
}
|
|
5790
|
+
return out;
|
|
5791
|
+
}
|
|
5792
|
+
|
|
5793
|
+
// src/store/feature-flag-store.ts
|
|
5794
|
+
async function listEnabledFeatureFlags(workspaceId) {
|
|
5795
|
+
const { data, error: error2 } = await getSupabaseClient().from("feature_flags").select("flag_id").eq("workspace_id", workspaceId);
|
|
5796
|
+
if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
|
|
5797
|
+
return (data ?? []).map((r) => r.flag_id);
|
|
5798
|
+
}
|
|
5799
|
+
async function enableFeatureFlag(workspaceId, flagId, userId) {
|
|
5800
|
+
const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
|
|
5801
|
+
{ workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
|
|
5802
|
+
{ onConflict: "workspace_id,flag_id" }
|
|
5803
|
+
);
|
|
5804
|
+
if (error2) throw new Error(`enableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
|
|
5805
|
+
}
|
|
5806
|
+
async function disableFeatureFlag(workspaceId, flagId) {
|
|
5807
|
+
const { error: error2 } = await getSupabaseClient().from("feature_flags").delete().eq("workspace_id", workspaceId).eq("flag_id", flagId);
|
|
5808
|
+
if (error2) throw new Error(`disableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5811
|
+
// src/dashboard/feature-flags-routes.ts
|
|
5812
|
+
async function isFeatureEnabled(c, flagId) {
|
|
5813
|
+
const auth = c.get("auth");
|
|
5814
|
+
const enabled = await listEnabledFeatureFlags(auth.workspaceId);
|
|
5815
|
+
return resolveFeatureFlags(enabled, isDevHost(c))[flagId];
|
|
5816
|
+
}
|
|
5817
|
+
function createFeatureFlagsRouter() {
|
|
5818
|
+
const app = new Hono8();
|
|
5819
|
+
app.get("/", async (c) => {
|
|
5820
|
+
const auth = c.get("auth");
|
|
5821
|
+
const devHost = isDevHost(c);
|
|
5822
|
+
const flags = resolveFeatureFlags(await listEnabledFeatureFlags(auth.workspaceId), devHost);
|
|
5823
|
+
const labs = featureFlagIds.filter((id) => isFeatureVisible(featureFlags[id].stage, devHost)).map((id) => ({ id, ...featureFlags[id], enabled: flags[id] }));
|
|
5824
|
+
return c.json({ flags, labs });
|
|
5825
|
+
});
|
|
5826
|
+
const toggle = async (c, enabled) => {
|
|
5827
|
+
const auth = c.get("auth");
|
|
5828
|
+
if (auth.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
5829
|
+
const flagId = c.req.param("flagId") ?? "";
|
|
5830
|
+
if (!isFeatureFlagId(flagId)) return c.json({ error: "unknown feature flag" }, 404);
|
|
5831
|
+
if (!isFeatureVisible(featureFlags[flagId].stage, isDevHost(c))) {
|
|
5832
|
+
return c.json({ error: "this feature flag cannot be changed here" }, 403);
|
|
5833
|
+
}
|
|
5834
|
+
if (enabled) await enableFeatureFlag(auth.workspaceId, flagId, auth.userId);
|
|
5835
|
+
else await disableFeatureFlag(auth.workspaceId, flagId);
|
|
5836
|
+
return c.json({ id: flagId, enabled });
|
|
5837
|
+
};
|
|
5838
|
+
app.put("/:flagId", (c) => toggle(c, true));
|
|
5839
|
+
app.delete("/:flagId", (c) => toggle(c, false));
|
|
5840
|
+
return app;
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
// src/dashboard/channels-routes.ts
|
|
5474
5844
|
function coerceAccess(raw) {
|
|
5475
5845
|
if (raw === "open" || raw === "*" || raw === "all") return raw;
|
|
5476
5846
|
if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) return raw;
|
|
@@ -5506,7 +5876,7 @@ function coerceMemberEmails(raw) {
|
|
|
5506
5876
|
return raw.filter((v) => typeof v === "string").map((v) => v.trim().toLowerCase()).filter(Boolean);
|
|
5507
5877
|
}
|
|
5508
5878
|
function ensureCwdDir(cwd) {
|
|
5509
|
-
const expanded = expandPath(cwd);
|
|
5879
|
+
const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
|
|
5510
5880
|
if (!existsSync12(expanded)) mkdirSync4(expanded, { recursive: true });
|
|
5511
5881
|
}
|
|
5512
5882
|
function toResponse(channel) {
|
|
@@ -5526,11 +5896,13 @@ function toResponse(channel) {
|
|
|
5526
5896
|
runtime: channel.runtime,
|
|
5527
5897
|
mounts: channel.mounts,
|
|
5528
5898
|
agentName: channel.agentName,
|
|
5899
|
+
dmKey: channel.dmKey,
|
|
5900
|
+
isDefault: channel.isDefault,
|
|
5529
5901
|
archived: !!channel.archivedAt
|
|
5530
5902
|
};
|
|
5531
5903
|
}
|
|
5532
5904
|
function createChannelWriteRouter() {
|
|
5533
|
-
const app = new
|
|
5905
|
+
const app = new Hono9();
|
|
5534
5906
|
app.post("/", async (c) => {
|
|
5535
5907
|
const body = await c.req.json().catch(() => ({}));
|
|
5536
5908
|
const name = typeof body["name"] === "string" ? body["name"].trim() : "";
|
|
@@ -5558,7 +5930,7 @@ function createChannelWriteRouter() {
|
|
|
5558
5930
|
});
|
|
5559
5931
|
return c.json(toResponse(channel), 201);
|
|
5560
5932
|
} catch (err) {
|
|
5561
|
-
return c.json({ error: err.message }, 500);
|
|
5933
|
+
return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
|
|
5562
5934
|
}
|
|
5563
5935
|
});
|
|
5564
5936
|
app.put("/:id", async (c) => {
|
|
@@ -5569,6 +5941,8 @@ function createChannelWriteRouter() {
|
|
|
5569
5941
|
const cwd = typeof body["cwd"] === "string" ? body["cwd"].trim() : "";
|
|
5570
5942
|
if (!cwd) return c.json({ errors: ["cwd is required"] }, 400);
|
|
5571
5943
|
const model = typeof body["model"] === "string" ? body["model"].trim() : "";
|
|
5944
|
+
if (coerceRuntime(body["runtime"]) === "sandbox" && existing.runtime !== "sandbox" && !await isFeatureEnabled(c, "sandbox"))
|
|
5945
|
+
return c.json({ error: "feature not enabled" }, 403);
|
|
5572
5946
|
try {
|
|
5573
5947
|
ensureCwdDir(cwd);
|
|
5574
5948
|
const channel = await updateChannelById(id, {
|
|
@@ -5586,7 +5960,7 @@ function createChannelWriteRouter() {
|
|
|
5586
5960
|
dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
|
|
5587
5961
|
return c.json(toResponse(channel), 200);
|
|
5588
5962
|
} catch (err) {
|
|
5589
|
-
return c.json({ error: err.message }, 500);
|
|
5963
|
+
return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
|
|
5590
5964
|
}
|
|
5591
5965
|
});
|
|
5592
5966
|
app.patch("/reorder", async (c) => {
|
|
@@ -5629,6 +6003,8 @@ function createChannelWriteRouter() {
|
|
|
5629
6003
|
if ("memberEmails" in body) patch.memberEmails = coerceMemberEmails(body["memberEmails"]);
|
|
5630
6004
|
if ("runtime" in body) {
|
|
5631
6005
|
const r = coerceRuntime(body["runtime"]);
|
|
6006
|
+
if (r === "sandbox" && existing.runtime !== "sandbox" && !await isFeatureEnabled(c, "sandbox"))
|
|
6007
|
+
return c.json({ error: "feature not enabled" }, 403);
|
|
5632
6008
|
if (r) patch.runtime = r;
|
|
5633
6009
|
}
|
|
5634
6010
|
if ("mounts" in body) patch.mounts = coerceMounts(body["mounts"]);
|
|
@@ -5641,7 +6017,7 @@ function createChannelWriteRouter() {
|
|
|
5641
6017
|
dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
|
|
5642
6018
|
return c.json(toResponse(channel), 200);
|
|
5643
6019
|
} catch (err) {
|
|
5644
|
-
return c.json({ error: err.message }, 500);
|
|
6020
|
+
return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
|
|
5645
6021
|
}
|
|
5646
6022
|
});
|
|
5647
6023
|
return app;
|
|
@@ -5663,13 +6039,13 @@ import {
|
|
|
5663
6039
|
statSync as statSync4,
|
|
5664
6040
|
writeFileSync as writeFileSync6
|
|
5665
6041
|
} from "fs";
|
|
5666
|
-
import { basename as basename4, dirname as dirname3, extname, join as
|
|
5667
|
-
import { Hono as
|
|
6042
|
+
import { basename as basename4, dirname as dirname3, extname, join as join14, relative, resolve as resolve4, sep } from "path";
|
|
6043
|
+
import { Hono as Hono10 } from "hono";
|
|
5668
6044
|
import { streamSSE } from "hono/streaming";
|
|
5669
6045
|
|
|
5670
6046
|
// src/dashboard/docs-watcher.ts
|
|
5671
6047
|
import { existsSync as existsSync13, watch } from "fs";
|
|
5672
|
-
import { basename as basename2, join as
|
|
6048
|
+
import { basename as basename2, join as join10 } from "path";
|
|
5673
6049
|
var DEBOUNCE_MS = 200;
|
|
5674
6050
|
var MAX_PENDING = 200;
|
|
5675
6051
|
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
@@ -5710,7 +6086,7 @@ function watchDocs(cwd, listener) {
|
|
|
5710
6086
|
const current = entries.get(cwd);
|
|
5711
6087
|
if (!current || filename === null) return;
|
|
5712
6088
|
const rel = filename.split("\\").join("/");
|
|
5713
|
-
if (rel === root && !existsSync13(
|
|
6089
|
+
if (rel === root && !existsSync13(join10(cwd, rel))) return;
|
|
5714
6090
|
if (isIgnoredDocPath(rel)) return;
|
|
5715
6091
|
if (current.pending.size < MAX_PENDING) current.pending.add(rel);
|
|
5716
6092
|
if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
|
|
@@ -5950,12 +6326,12 @@ async function readOrQueueDocAudio(input2) {
|
|
|
5950
6326
|
import { readFileSync as readFileSync8 } from "fs";
|
|
5951
6327
|
|
|
5952
6328
|
// src/dashboard/chat-agent.ts
|
|
5953
|
-
import { homedir as
|
|
6329
|
+
import { homedir as homedir6 } from "os";
|
|
5954
6330
|
|
|
5955
6331
|
// src/mcp/config.ts
|
|
5956
6332
|
import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
5957
6333
|
import { tmpdir } from "os";
|
|
5958
|
-
import { join as
|
|
6334
|
+
import { join as join11 } from "path";
|
|
5959
6335
|
|
|
5960
6336
|
// src/mcp/crypto.ts
|
|
5961
6337
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
@@ -6641,8 +7017,8 @@ async function buildMcpConfig(profile, connectionIds) {
|
|
|
6641
7017
|
return { config, skipped };
|
|
6642
7018
|
}
|
|
6643
7019
|
function writeMcpConfigFile(config) {
|
|
6644
|
-
const dir = mkdtempSync(
|
|
6645
|
-
const path =
|
|
7020
|
+
const dir = mkdtempSync(join11(tmpdir(), "flyspace-mcp-"));
|
|
7021
|
+
const path = join11(dir, "mcp-config.json");
|
|
6646
7022
|
writeFileSync5(path, JSON.stringify(config), { mode: 384 });
|
|
6647
7023
|
return {
|
|
6648
7024
|
path,
|
|
@@ -6664,15 +7040,15 @@ async function withMcpConfig(profile, connectionIds, fn) {
|
|
|
6664
7040
|
|
|
6665
7041
|
// src/claude/agents.ts
|
|
6666
7042
|
import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
|
|
6667
|
-
import { homedir as
|
|
6668
|
-
import { basename as basename3, join as
|
|
7043
|
+
import { homedir as homedir5 } from "os";
|
|
7044
|
+
import { basename as basename3, join as join12 } from "path";
|
|
6669
7045
|
import yaml from "js-yaml";
|
|
6670
7046
|
function defaultConfigDir() {
|
|
6671
|
-
return
|
|
7047
|
+
return join12(homedir5(), ".claude");
|
|
6672
7048
|
}
|
|
6673
7049
|
function expandHome(p) {
|
|
6674
|
-
if (p === "~") return
|
|
6675
|
-
if (p.startsWith("~/")) return
|
|
7050
|
+
if (p === "~") return homedir5();
|
|
7051
|
+
if (p.startsWith("~/")) return join12(homedir5(), p.slice(2));
|
|
6676
7052
|
return p;
|
|
6677
7053
|
}
|
|
6678
7054
|
function parseFrontmatter(file) {
|
|
@@ -6702,7 +7078,7 @@ function collectFrom(root, source) {
|
|
|
6702
7078
|
return [];
|
|
6703
7079
|
}
|
|
6704
7080
|
return entries2.map((file) => {
|
|
6705
|
-
const path =
|
|
7081
|
+
const path = join12(root, file);
|
|
6706
7082
|
const fm = parseFrontmatter(path);
|
|
6707
7083
|
const name = fm.name?.trim() || basename3(file, ".md");
|
|
6708
7084
|
return { name, description: (fm.description ?? "").trim(), source, path };
|
|
@@ -6711,8 +7087,8 @@ function collectFrom(root, source) {
|
|
|
6711
7087
|
function listAgents(configDir2, cwd) {
|
|
6712
7088
|
const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
|
|
6713
7089
|
const collected = [
|
|
6714
|
-
...collectFrom(
|
|
6715
|
-
...collectFrom(
|
|
7090
|
+
...collectFrom(join12(cwd, ".claude", "agents"), "project"),
|
|
7091
|
+
...collectFrom(join12(base, "agents"), "user")
|
|
6716
7092
|
];
|
|
6717
7093
|
const byName = /* @__PURE__ */ new Map();
|
|
6718
7094
|
for (const agent of collected) {
|
|
@@ -6952,9 +7328,54 @@ ${incoming}`;
|
|
|
6952
7328
|
}
|
|
6953
7329
|
}
|
|
6954
7330
|
|
|
7331
|
+
// src/dashboard/should-resume.ts
|
|
7332
|
+
var GROQ_API_URL5 = "https://api.groq.com/openai/v1/chat/completions";
|
|
7333
|
+
var CLASSIFIER_MODEL3 = "openai/gpt-oss-20b";
|
|
7334
|
+
var SYSTEM_PROMPT3 = `An AI assistant was working in a chat thread when its process was restarted. Anything it was still doing, or had promised to do later, was lost. You are given the thread topic and its most recent messages; the last one is the assistant's own. Decide whether the assistant has to pick the work back up.
|
|
7335
|
+
|
|
7336
|
+
Reply YES only if the assistant's last message clearly leaves work it owes unfinished and nothing after it delivers that work, e.g. it says something is running or in progress and that it will report back, it announces a next step it has not done yet, or it stops partway through a list of things it was asked to do.
|
|
7337
|
+
|
|
7338
|
+
Reply NO for anything else: the assistant answered or finished the request, it is waiting on the human (asked a question, offered choices, needs a decision or approval), it reported a result or an error, or you are unsure. Staying quiet is the safe default: answering YES makes the assistant act without being asked.
|
|
7339
|
+
|
|
7340
|
+
Answer with only the single word YES or NO, nothing else.`;
|
|
7341
|
+
async function shouldResume(topic, recent) {
|
|
7342
|
+
const apiKey = process.env["GROQ_API_KEY"];
|
|
7343
|
+
if (!apiKey) return false;
|
|
7344
|
+
if (recent.length === 0) return false;
|
|
7345
|
+
const user = `[thread topic]
|
|
7346
|
+
${topic}
|
|
7347
|
+
|
|
7348
|
+
[most recent messages]
|
|
7349
|
+
${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
|
|
7350
|
+
try {
|
|
7351
|
+
const res = await fetch(GROQ_API_URL5, {
|
|
7352
|
+
method: "POST",
|
|
7353
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
7354
|
+
body: JSON.stringify({
|
|
7355
|
+
model: CLASSIFIER_MODEL3,
|
|
7356
|
+
temperature: 0,
|
|
7357
|
+
reasoning_effort: "low",
|
|
7358
|
+
// gpt-oss reasons before it answers, and those tokens count against
|
|
7359
|
+
// this cap (see should-interrupt.ts).
|
|
7360
|
+
max_tokens: 150,
|
|
7361
|
+
messages: [
|
|
7362
|
+
{ role: "system", content: SYSTEM_PROMPT3 },
|
|
7363
|
+
{ role: "user", content: user }
|
|
7364
|
+
]
|
|
7365
|
+
})
|
|
7366
|
+
});
|
|
7367
|
+
if (!res.ok) return false;
|
|
7368
|
+
const data = await res.json();
|
|
7369
|
+
const answer = data.choices?.[0]?.message?.content?.trim().toUpperCase() ?? "";
|
|
7370
|
+
return answer.startsWith("YES");
|
|
7371
|
+
} catch {
|
|
7372
|
+
return false;
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7375
|
+
|
|
6955
7376
|
// src/dashboard/attachment-downloader.ts
|
|
6956
7377
|
import { writeFile } from "fs/promises";
|
|
6957
|
-
import { join as
|
|
7378
|
+
import { join as join13 } from "path";
|
|
6958
7379
|
import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
|
|
6959
7380
|
var debug = process.env["DEBUG"] === "true";
|
|
6960
7381
|
var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
|
|
@@ -7001,7 +7422,7 @@ async function downloadOne(key) {
|
|
|
7001
7422
|
if (bytes.length > sizeLimit) {
|
|
7002
7423
|
return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
|
|
7003
7424
|
}
|
|
7004
|
-
const localPath =
|
|
7425
|
+
const localPath = join13(paths.UPLOADS_DIR, lastSegment);
|
|
7005
7426
|
await writeFile(localPath, bytes);
|
|
7006
7427
|
if (debug)
|
|
7007
7428
|
console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
|
|
@@ -7090,8 +7511,10 @@ function loadChatAgentConfig() {
|
|
|
7090
7511
|
};
|
|
7091
7512
|
}
|
|
7092
7513
|
var RETRY_DELAY_MS = 3e3;
|
|
7514
|
+
var MISSED_MESSAGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
7515
|
+
var MISSED_MESSAGE_THREAD_LIMIT = 100;
|
|
7093
7516
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7094
|
-
function stripPathPrefixes(label2, roots, home =
|
|
7517
|
+
function stripPathPrefixes(label2, roots, home = homedir6()) {
|
|
7095
7518
|
let out = label2;
|
|
7096
7519
|
for (const root of roots) {
|
|
7097
7520
|
if (root) out = out.replaceAll(`${root.replace(/\/+$/, "")}/`, "");
|
|
@@ -7109,6 +7532,7 @@ var ChatAgent = class {
|
|
|
7109
7532
|
this.supabase = deps.supabase ?? getSupabaseClient;
|
|
7110
7533
|
this.shouldAgentRespond = deps.shouldAgentRespond ?? shouldAgentRespond;
|
|
7111
7534
|
this.shouldInterrupt = deps.shouldInterrupt ?? shouldInterrupt;
|
|
7535
|
+
this.shouldResume = deps.shouldResume ?? shouldResume;
|
|
7112
7536
|
}
|
|
7113
7537
|
queue = new MessageQueue();
|
|
7114
7538
|
ask;
|
|
@@ -7118,6 +7542,7 @@ var ChatAgent = class {
|
|
|
7118
7542
|
supabase;
|
|
7119
7543
|
shouldAgentRespond;
|
|
7120
7544
|
shouldInterrupt;
|
|
7545
|
+
shouldResume;
|
|
7121
7546
|
/** Threads with a `run()` currently in flight, mapped to the tool-use
|
|
7122
7547
|
* activities observed so far plus the run's abort controller. The realtime
|
|
7123
7548
|
* "thinking"/"activity" broadcasts are ephemeral (no history for late
|
|
@@ -7139,6 +7564,10 @@ var ChatAgent = class {
|
|
|
7139
7564
|
* it's accepted until `doRegenerate` settles — see `regenerate()`'s doc
|
|
7140
7565
|
* comment for why this can't just be `activeRuns`. */
|
|
7141
7566
|
pendingRegenerate = /* @__PURE__ */ new Set();
|
|
7567
|
+
/** `replyId`s this process has already been handed, so a recovery pass
|
|
7568
|
+
* never re-runs a message that is queued, in flight, or was deliberately
|
|
7569
|
+
* left unanswered by the ambient classifier. */
|
|
7570
|
+
seenReplies = /* @__PURE__ */ new Set();
|
|
7142
7571
|
/** Workspace roots (workspaces.root_path) for path-prefix stripping in
|
|
7143
7572
|
* activity labels. Fetched once per process — roots change ~never, and a
|
|
7144
7573
|
* restart picks up edits. */
|
|
@@ -7146,6 +7575,7 @@ var ChatAgent = class {
|
|
|
7146
7575
|
/** Fire-and-forget entry point, called by chat-routes after every write. */
|
|
7147
7576
|
handleMessage(msg) {
|
|
7148
7577
|
if (msg.author === AGENT_AUTHOR) return;
|
|
7578
|
+
this.seenReplies.add(msg.replyId);
|
|
7149
7579
|
this.queue.enqueue(msg.threadId, () => this.maybeRun(msg));
|
|
7150
7580
|
void this.maybeSupersede(msg);
|
|
7151
7581
|
}
|
|
@@ -7180,11 +7610,81 @@ var ChatAgent = class {
|
|
|
7180
7610
|
this.supersedingReplies.add(msg.replyId);
|
|
7181
7611
|
run3.abort.abort();
|
|
7182
7612
|
}
|
|
7613
|
+
/**
|
|
7614
|
+
* Picks up messages nobody answered. `handleMessage` is fire-and-forget and
|
|
7615
|
+
* every bit of run state lives in this process, so a message is silently
|
|
7616
|
+
* lost when it is written while the Host is offline (the Data side's call
|
|
7617
|
+
* has nowhere to land) or when the process dies mid-run. The stored thread
|
|
7618
|
+
* is the only durable record of either, so this reads it back: any recently
|
|
7619
|
+
* active thread whose latest message is a human's has nothing after it, and
|
|
7620
|
+
* goes through the normal `handleMessage` path, which still decides
|
|
7621
|
+
* whether it warrants a reply at all (mention, lone human, or the ambient
|
|
7622
|
+
* classifier), and still honours the pause switch.
|
|
7623
|
+
*
|
|
7624
|
+
* A thread the agent had the last word in can be unfinished too: it said
|
|
7625
|
+
* "running, I'll report back" and the restart took the promise with it.
|
|
7626
|
+
* Nothing but the wording records that, so `shouldResume` reads the topic
|
|
7627
|
+
* and the recent replies and, on a yes, the agent is handed its own last
|
|
7628
|
+
* message to carry on from. At most once per human message: a thread whose
|
|
7629
|
+
* last two messages are both the agent's is left alone, so a resume that
|
|
7630
|
+
* itself ends on a promise can't re-trigger on every restart.
|
|
7631
|
+
*
|
|
7632
|
+
* Call after anything that could have opened such a gap: Host start, and
|
|
7633
|
+
* every Host socket reconnect. Idempotent within a process (`seenReplies`).
|
|
7634
|
+
* Returns how many messages were handed over.
|
|
7635
|
+
*/
|
|
7636
|
+
async recoverMissed() {
|
|
7637
|
+
const supabase = this.supabase();
|
|
7638
|
+
const since2 = new Date(Date.now() - MISSED_MESSAGE_WINDOW_MS).toISOString();
|
|
7639
|
+
const { data: threads, error: error2 } = await supabase.from("threads").select("id, channel_id, title").gte("updated_at", since2).is("archived_at", null).order("updated_at", { ascending: false }).limit(MISSED_MESSAGE_THREAD_LIMIT);
|
|
7640
|
+
if (error2) {
|
|
7641
|
+
console.error("[chat-agent] recoverMissed: failed to list threads:", error2.message);
|
|
7642
|
+
return 0;
|
|
7643
|
+
}
|
|
7644
|
+
let recovered = 0;
|
|
7645
|
+
for (const thread of threads ?? []) {
|
|
7646
|
+
if (!findChannel(thread.channel_id)) continue;
|
|
7647
|
+
const { data } = await supabase.from("thread_replies").select("id, author, body, model, effort, external_thread_replies_id").eq("thread_id", thread.id).is("archived_at", null).order("created_at", { ascending: false }).limit(CLASSIFIER_CONTEXT_SIZE);
|
|
7648
|
+
const tail = data ?? [];
|
|
7649
|
+
const last = tail[0];
|
|
7650
|
+
if (!last || last.external_thread_replies_id) continue;
|
|
7651
|
+
if (this.seenReplies.has(last.id)) continue;
|
|
7652
|
+
if (!isHumanAuthor(last.author)) {
|
|
7653
|
+
this.seenReplies.add(last.id);
|
|
7654
|
+
const asker = tail[1];
|
|
7655
|
+
if (!asker || !isHumanAuthor(asker.author)) continue;
|
|
7656
|
+
const transcript = [...tail].reverse().map((r) => ({ author: r.author, body: r.body }));
|
|
7657
|
+
if (!await this.shouldResume(thread.title ?? "", transcript)) continue;
|
|
7658
|
+
console.log(`[chat-agent] resuming unfinished work. thread=${thread.id}`);
|
|
7659
|
+
this.handleMessage({
|
|
7660
|
+
threadId: thread.id,
|
|
7661
|
+
replyId: last.id,
|
|
7662
|
+
author: asker.author,
|
|
7663
|
+
body: last.body,
|
|
7664
|
+
resumed: true
|
|
7665
|
+
});
|
|
7666
|
+
recovered++;
|
|
7667
|
+
continue;
|
|
7668
|
+
}
|
|
7669
|
+
console.log(`[chat-agent] recovering unanswered message. thread=${thread.id}`);
|
|
7670
|
+
this.handleMessage({
|
|
7671
|
+
threadId: thread.id,
|
|
7672
|
+
replyId: last.id,
|
|
7673
|
+
author: last.author,
|
|
7674
|
+
body: last.body,
|
|
7675
|
+
model: last.model ?? void 0,
|
|
7676
|
+
effort: last.effort && isEffort(last.effort) ? last.effort : void 0,
|
|
7677
|
+
recovered: true
|
|
7678
|
+
});
|
|
7679
|
+
recovered++;
|
|
7680
|
+
}
|
|
7681
|
+
return recovered;
|
|
7682
|
+
}
|
|
7183
7683
|
/** Decides whether this message should trigger a reply, then runs it. */
|
|
7184
7684
|
async maybeRun(msg) {
|
|
7185
7685
|
const superseding = this.supersedingReplies.delete(msg.replyId);
|
|
7186
7686
|
if (await this.isPausedForThread(msg.threadId)) return;
|
|
7187
|
-
if (superseding) {
|
|
7687
|
+
if (superseding || msg.resumed) {
|
|
7188
7688
|
await this.run(msg);
|
|
7189
7689
|
return;
|
|
7190
7690
|
}
|
|
@@ -7440,6 +7940,7 @@ var ChatAgent = class {
|
|
|
7440
7940
|
activeRunner: runnerForModel(loadDashboardSettings().agent)
|
|
7441
7941
|
})
|
|
7442
7942
|
);
|
|
7943
|
+
ensureLogicalCwdDir(baseConfig.cwd);
|
|
7443
7944
|
const pickedModel = msg.model?.trim();
|
|
7444
7945
|
let activeRunner = pickedModel ? runnerForModel(pickedModel) : defaultRunner;
|
|
7445
7946
|
let channelConfig = pickedModel ? {
|
|
@@ -7461,18 +7962,34 @@ var ChatAgent = class {
|
|
|
7461
7962
|
};
|
|
7462
7963
|
}
|
|
7463
7964
|
}
|
|
7965
|
+
const docsAccess = await this.resolveSenderDocsAccess(
|
|
7966
|
+
owningProfile,
|
|
7967
|
+
msg.author,
|
|
7968
|
+
thread.channel_id
|
|
7969
|
+
);
|
|
7970
|
+
if (docsAccess && docsAccess !== "write") channelConfig = { ...channelConfig, docsAccess };
|
|
7464
7971
|
const store = this.openSessionStore();
|
|
7465
7972
|
try {
|
|
7466
7973
|
const channelKey = `chat:${thread.channel_id}`;
|
|
7467
7974
|
const session = store.getSession(channelKey, msg.threadId);
|
|
7468
|
-
const
|
|
7975
|
+
const docsNote = docsAccessPromptNote(channelConfig.docsAccess);
|
|
7976
|
+
const header = (docsNote ? `[access: ${docsNote}]
|
|
7977
|
+
|
|
7978
|
+
` : "") + `[context: chat channel=${thread.channel_id}` + (thread.title ? `, thread="${thread.title}"` : "") + `, sender=${msg.author}. This is the Flyspace dashboard, not Slack \u2014 it renders full Markdown, so use normal paragraphs, line breaks, and real Markdown lists instead of Slack-style single-line replies. When you name a file that lives in this channel's folder, write it as @ + its cwd-relative path (@docs/spec.md, @web/src/App.tsx) as plain text, never inside backticks or a Markdown link \u2014 the dashboard turns that into a chip the reader clicks to open the file. A trailing line number (@src/app.ts:120) is fine. Paths outside the cwd stay in backticks.]
|
|
7469
7979
|
|
|
7470
7980
|
`;
|
|
7471
7981
|
const attachments = await downloadChatAttachments(msg.body);
|
|
7472
7982
|
const interruptionNote = carried ? `[note: you were still working on "${carried.interrupted.slice(0, 500)}" when ${msg.author} sent the message below. It was sent to correct or replace that request, so it wins. Whatever you already did is still in your context \u2014 keep what is still useful and adjust, rather than starting over or finishing the superseded request. Do not apologise for the interruption; just answer.]
|
|
7473
7983
|
|
|
7474
7984
|
` : "";
|
|
7475
|
-
const
|
|
7985
|
+
const recoveryNote = msg.recovered ? `[note: the message below went unanswered: the agent was offline when it was sent, or a restart cut short the run answering it. If that run got as far as changing files or starting work, check what is already done before redoing it. Do not apologise for the delay; just answer.]
|
|
7986
|
+
|
|
7987
|
+
` : "";
|
|
7988
|
+
const body = msg.resumed ? `[note: nobody sent a new message. The agent process restarted after your last reply in this thread, quoted below, which left work unfinished. Anything you had running or meant to do next may have died with it. Check what actually got done (a background job may also still be running, or have finished), then complete what is left and report the result. If there turns out to be nothing left to do, say so in one line. Do not promise another follow-up, and do not apologise for the gap.]
|
|
7989
|
+
|
|
7990
|
+
[your last reply]
|
|
7991
|
+
${msg.body}` : msg.body;
|
|
7992
|
+
const barePrompt = header + interruptionNote + recoveryNote + body + buildFilesPromptSection(attachments);
|
|
7476
7993
|
let resumableSessionId = session?.runner === activeRunner ? session.sessionId : void 0;
|
|
7477
7994
|
let prompt = barePrompt;
|
|
7478
7995
|
let rolledOver = false;
|
|
@@ -7748,9 +8265,9 @@ var ChatAgent = class {
|
|
|
7748
8265
|
}, activity: () => {
|
|
7749
8266
|
} };
|
|
7750
8267
|
}
|
|
7751
|
-
let
|
|
8268
|
+
let pending3 = ready;
|
|
7752
8269
|
const send2 = (event, payload, last = false) => {
|
|
7753
|
-
|
|
8270
|
+
pending3 = pending3.then(async (subscribed) => {
|
|
7754
8271
|
if (subscribed) {
|
|
7755
8272
|
const res = await channel.send({ type: "broadcast", event, payload }).catch((err) => err.message);
|
|
7756
8273
|
if (res !== "ok") {
|
|
@@ -7760,7 +8277,7 @@ var ChatAgent = class {
|
|
|
7760
8277
|
return subscribed;
|
|
7761
8278
|
}).catch(() => false);
|
|
7762
8279
|
if (last) {
|
|
7763
|
-
void
|
|
8280
|
+
void pending3.finally(() => void this.supabase().removeChannel(channel));
|
|
7764
8281
|
}
|
|
7765
8282
|
};
|
|
7766
8283
|
return {
|
|
@@ -7807,9 +8324,9 @@ var ChatAgent = class {
|
|
|
7807
8324
|
};
|
|
7808
8325
|
return this.channelActivitySend;
|
|
7809
8326
|
}
|
|
7810
|
-
let
|
|
8327
|
+
let pending3 = ready;
|
|
7811
8328
|
this.channelActivitySend = (event, payload) => {
|
|
7812
|
-
|
|
8329
|
+
pending3 = pending3.then(async (subscribed) => {
|
|
7813
8330
|
if (subscribed) {
|
|
7814
8331
|
const res = await channel.send({ type: "broadcast", event, payload }).catch((err) => err.message);
|
|
7815
8332
|
if (res !== "ok") {
|
|
@@ -7821,6 +8338,20 @@ var ChatAgent = class {
|
|
|
7821
8338
|
};
|
|
7822
8339
|
return this.channelActivitySend;
|
|
7823
8340
|
}
|
|
8341
|
+
/**
|
|
8342
|
+
* The sender's Docs level on `channelId`, or undefined when no restriction
|
|
8343
|
+
* applies (non-human author, or a profile with no Supabase workspace). A
|
|
8344
|
+
* lookup failure resolves to 'none': better to withhold files than leak them.
|
|
8345
|
+
*/
|
|
8346
|
+
async resolveSenderDocsAccess(profile, author, channelId) {
|
|
8347
|
+
if (!isHumanAuthor(author)) return void 0;
|
|
8348
|
+
try {
|
|
8349
|
+
return await resolveDocsAccessByHandle(profile, author, channelId) ?? void 0;
|
|
8350
|
+
} catch (err) {
|
|
8351
|
+
console.error("[chat-agent] docs access lookup failed:", err.message);
|
|
8352
|
+
return "none";
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
7824
8355
|
async tryAsk(channelConfig, prompt, sessionId, onActivity, signal, usageContext) {
|
|
7825
8356
|
const name = subAgentName(channelConfig.agentName);
|
|
7826
8357
|
const agentPersona = name ? readAgentPersona(channelConfig.claudeConfigDir, channelConfig.cwd, name) : void 0;
|
|
@@ -7843,7 +8374,8 @@ var ChatAgent = class {
|
|
|
7843
8374
|
onActivity,
|
|
7844
8375
|
signal,
|
|
7845
8376
|
mcpConfigPath,
|
|
7846
|
-
...usageContext ? { usageContext } : {}
|
|
8377
|
+
...usageContext ? { usageContext } : {},
|
|
8378
|
+
...channelConfig.docsAccess ? { docsAccess: channelConfig.docsAccess } : {}
|
|
7847
8379
|
},
|
|
7848
8380
|
channelConfig.claudeConfigDir,
|
|
7849
8381
|
sessionId,
|
|
@@ -7873,12 +8405,19 @@ var ChatAgent = class {
|
|
|
7873
8405
|
// the channel's own instructions ride in with the prompt. Without
|
|
7874
8406
|
// this a Codex turn answers as a generic coding agent: no house
|
|
7875
8407
|
// language rules, no Slack/dashboard conventions, no channel prompt.
|
|
7876
|
-
systemPrompt:
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
8408
|
+
systemPrompt: [
|
|
8409
|
+
buildSystemPrompt(
|
|
8410
|
+
this.config.botName,
|
|
8411
|
+
agentPersona,
|
|
8412
|
+
channelConfig.prompt,
|
|
8413
|
+
resolveSelectedOutputStyle(channelConfig.claudeConfigDir)?.body
|
|
8414
|
+
),
|
|
8415
|
+
docsAccessPromptNote(channelConfig.docsAccess)
|
|
8416
|
+
].filter(Boolean).join("\n\n---\n\n"),
|
|
8417
|
+
// Codex has no per-tool deny list, so any restricted requester gets
|
|
8418
|
+
// its read-only sandbox (no file writes). It can still read: see the
|
|
8419
|
+
// residual-gap note in docs/features/user-roles.md.
|
|
8420
|
+
...channelConfig.docsAccess === "read" || channelConfig.docsAccess === "none" ? { sandbox: "read-only" } : {},
|
|
7882
8421
|
...onActivity ? { onActivity } : {},
|
|
7883
8422
|
...signal ? { signal } : {},
|
|
7884
8423
|
...usageContext ? { usageContext } : {}
|
|
@@ -8198,7 +8737,9 @@ var DOC_EXTENSIONS = {
|
|
|
8198
8737
|
".htm": "html",
|
|
8199
8738
|
// A file whose whole content is a URL — the Docs pane renders it as a
|
|
8200
8739
|
// full-size iframe of that page instead of showing the link as text.
|
|
8201
|
-
".url": "url"
|
|
8740
|
+
".url": "url",
|
|
8741
|
+
// An Excalidraw drawing: JSON on disk, drawn and edited in the Docs pane.
|
|
8742
|
+
".excalidraw": "excalidraw"
|
|
8202
8743
|
};
|
|
8203
8744
|
var MEDIA_EXTENSIONS = {
|
|
8204
8745
|
".png": "image/png",
|
|
@@ -8261,7 +8802,7 @@ var CODE_EXTENSIONS = {
|
|
|
8261
8802
|
function highlightLanguageOf(path) {
|
|
8262
8803
|
const ext = extname(path).toLowerCase();
|
|
8263
8804
|
const doc = DOC_EXTENSIONS[ext];
|
|
8264
|
-
if (doc) return doc === "md" ? "markdown" : "xml";
|
|
8805
|
+
if (doc) return doc === "md" ? "markdown" : doc === "excalidraw" ? "json" : "xml";
|
|
8265
8806
|
return CODE_EXTENSIONS[ext] ?? null;
|
|
8266
8807
|
}
|
|
8267
8808
|
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
@@ -8363,7 +8904,7 @@ function readHead(absPath) {
|
|
|
8363
8904
|
}
|
|
8364
8905
|
}
|
|
8365
8906
|
function extractTitle(absPath, kind) {
|
|
8366
|
-
if (kind === "url") return basename4(absPath);
|
|
8907
|
+
if (kind === "url" || kind === "excalidraw") return basename4(absPath);
|
|
8367
8908
|
const head = readHead(absPath);
|
|
8368
8909
|
if (!head) return basename4(absPath);
|
|
8369
8910
|
if (kind === "md") {
|
|
@@ -8405,7 +8946,7 @@ function probeDir(absDir, showHidden) {
|
|
|
8405
8946
|
return false;
|
|
8406
8947
|
}
|
|
8407
8948
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
8408
|
-
const abs =
|
|
8949
|
+
const abs = join14(dir, entry.name);
|
|
8409
8950
|
const kind = entryKind(entry, abs);
|
|
8410
8951
|
if (kind === "dir") {
|
|
8411
8952
|
if (SKIP_DIRS.has(entry.name)) hasOther = true;
|
|
@@ -8425,14 +8966,14 @@ function probeDir(absDir, showHidden) {
|
|
|
8425
8966
|
}
|
|
8426
8967
|
function readGitBranch(dir) {
|
|
8427
8968
|
try {
|
|
8428
|
-
let gitDir =
|
|
8969
|
+
let gitDir = join14(dir, ".git");
|
|
8429
8970
|
if (statSync4(gitDir).isFile()) {
|
|
8430
8971
|
const pointer = readFileSync9(gitDir, "utf8").trim();
|
|
8431
8972
|
const match = pointer.match(/^gitdir:\s*(.+)$/);
|
|
8432
8973
|
if (!match) return void 0;
|
|
8433
8974
|
gitDir = resolve4(dir, match[1]);
|
|
8434
8975
|
}
|
|
8435
|
-
const head = readFileSync9(
|
|
8976
|
+
const head = readFileSync9(join14(gitDir, "HEAD"), "utf8").trim();
|
|
8436
8977
|
const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
8437
8978
|
if (branchMatch) return branchMatch[1];
|
|
8438
8979
|
return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
|
|
@@ -8454,14 +8995,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
|
|
|
8454
8995
|
for (const entry of entries2) {
|
|
8455
8996
|
if (dirs.length + docs.length >= MAX_ENTRIES) break;
|
|
8456
8997
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
8457
|
-
const abs =
|
|
8998
|
+
const abs = join14(absDir, entry.name);
|
|
8458
8999
|
const path = relative(root, abs).split(sep).join("/");
|
|
8459
9000
|
const kind = entryKind(entry, abs);
|
|
8460
9001
|
if (kind === "dir") {
|
|
8461
9002
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
8462
9003
|
const probe2 = probeDir(abs, showHidden);
|
|
8463
9004
|
if (!probe2.hasDocs && probe2.hasOther) continue;
|
|
8464
|
-
const isRepo = existsSync16(
|
|
9005
|
+
const isRepo = existsSync16(join14(abs, ".git"));
|
|
8465
9006
|
dirs.push({
|
|
8466
9007
|
path,
|
|
8467
9008
|
name: entry.name,
|
|
@@ -8497,7 +9038,7 @@ function flatDocs(root, {
|
|
|
8497
9038
|
for (const entry of entries2) {
|
|
8498
9039
|
if (hits.length >= MAX_FLAT_DOCS) return;
|
|
8499
9040
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
8500
|
-
const abs =
|
|
9041
|
+
const abs = join14(dir, entry.name);
|
|
8501
9042
|
const kind = entryKind(entry, abs);
|
|
8502
9043
|
if (kind === "dir") {
|
|
8503
9044
|
if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
|
|
@@ -8577,7 +9118,7 @@ function uniqueName(absDir, name) {
|
|
|
8577
9118
|
let candidate = name;
|
|
8578
9119
|
for (let n = 1; n < 1e3; n++) {
|
|
8579
9120
|
try {
|
|
8580
|
-
statSync4(
|
|
9121
|
+
statSync4(join14(absDir, candidate));
|
|
8581
9122
|
} catch {
|
|
8582
9123
|
return candidate;
|
|
8583
9124
|
}
|
|
@@ -8599,7 +9140,7 @@ function saveUploadedFile(root, relDir, name, data) {
|
|
|
8599
9140
|
if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
|
|
8600
9141
|
if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
|
|
8601
9142
|
const final = uniqueName(absDir, safe);
|
|
8602
|
-
const abs =
|
|
9143
|
+
const abs = join14(absDir, final);
|
|
8603
9144
|
writeFileSync6(abs, data);
|
|
8604
9145
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
|
|
8605
9146
|
}
|
|
@@ -8613,7 +9154,7 @@ function createDocFolder(root, relDir, name) {
|
|
|
8613
9154
|
}
|
|
8614
9155
|
const safe = sanitizeUploadName(name);
|
|
8615
9156
|
if (!safe) return { status: "bad-name" };
|
|
8616
|
-
const abs =
|
|
9157
|
+
const abs = join14(absDir, safe);
|
|
8617
9158
|
try {
|
|
8618
9159
|
statSync4(abs);
|
|
8619
9160
|
return { status: "exists" };
|
|
@@ -8636,7 +9177,7 @@ function createDocFile(root, relDir, name, content) {
|
|
|
8636
9177
|
if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
|
|
8637
9178
|
if (SKIP_FILES.has(safe)) return { status: "bad-name" };
|
|
8638
9179
|
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
|
|
8639
|
-
const abs =
|
|
9180
|
+
const abs = join14(absDir, safe);
|
|
8640
9181
|
try {
|
|
8641
9182
|
writeFileSync6(abs, content, { encoding: "utf8", flag: "wx" });
|
|
8642
9183
|
} catch (err) {
|
|
@@ -8674,7 +9215,7 @@ function moveDocEntry(root, from, to) {
|
|
|
8674
9215
|
} catch {
|
|
8675
9216
|
return { status: "bad-target" };
|
|
8676
9217
|
}
|
|
8677
|
-
const finalAbs =
|
|
9218
|
+
const finalAbs = join14(absToDir, name);
|
|
8678
9219
|
if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
|
|
8679
9220
|
return { status: "into-self" };
|
|
8680
9221
|
}
|
|
@@ -8705,7 +9246,9 @@ function deleteDocEntry(root, path) {
|
|
|
8705
9246
|
};
|
|
8706
9247
|
}
|
|
8707
9248
|
function createDocsRouter() {
|
|
8708
|
-
const app = new
|
|
9249
|
+
const app = new Hono10();
|
|
9250
|
+
app.use("/:id/docs", requireDocsAccess);
|
|
9251
|
+
app.use("/:id/docs/*", requireDocsAccess);
|
|
8709
9252
|
app.get("/:id/docs", (c) => {
|
|
8710
9253
|
const { cwd } = resolveChannelConfig(c.req.param("id"));
|
|
8711
9254
|
const dir = c.req.query("path") ?? "";
|
|
@@ -8821,6 +9364,8 @@ function createDocsRouter() {
|
|
|
8821
9364
|
if (!body || typeof body.name !== "string") return c.json({ error: "name is required" }, 400);
|
|
8822
9365
|
const dir = typeof body.path === "string" ? body.path : "";
|
|
8823
9366
|
const content = typeof body.content === "string" ? body.content : "";
|
|
9367
|
+
if (body.name.toLowerCase().endsWith(".excalidraw") && !await isFeatureEnabled(c, "excalidraw"))
|
|
9368
|
+
return c.json({ error: "feature not enabled" }, 403);
|
|
8824
9369
|
const result = createDocFile(cwd, dir, body.name, content);
|
|
8825
9370
|
switch (result.status) {
|
|
8826
9371
|
case "no-dir":
|
|
@@ -9010,6 +9555,8 @@ function createDocsRouter() {
|
|
|
9010
9555
|
if ("error" in resolved2) return resolved2.error;
|
|
9011
9556
|
const lang = canonicalLanguage(c.req.param("lang"));
|
|
9012
9557
|
if (!isTranslatableLanguage(lang)) return c.json({ error: "not found" }, 404);
|
|
9558
|
+
if (!await isFeatureEnabled(c, "read-aloud"))
|
|
9559
|
+
return c.json({ error: "feature not enabled" }, 403);
|
|
9013
9560
|
try {
|
|
9014
9561
|
const audio = await readOrQueueDocAudio({
|
|
9015
9562
|
channelId: resolved2.channel.id,
|
|
@@ -9028,7 +9575,7 @@ function createDocsRouter() {
|
|
|
9028
9575
|
}
|
|
9029
9576
|
|
|
9030
9577
|
// src/dashboard/output-styles-routes.ts
|
|
9031
|
-
import { Hono as
|
|
9578
|
+
import { Hono as Hono11 } from "hono";
|
|
9032
9579
|
function requireAdmin(auth) {
|
|
9033
9580
|
return auth?.role === "admin" ? null : "admin access required";
|
|
9034
9581
|
}
|
|
@@ -9042,13 +9589,13 @@ function toResponse3(style) {
|
|
|
9042
9589
|
updatedAt: style.updatedAt
|
|
9043
9590
|
};
|
|
9044
9591
|
}
|
|
9045
|
-
var configDir = () =>
|
|
9592
|
+
var configDir = () => profileClaudeConfigDir();
|
|
9046
9593
|
function failure(c, err) {
|
|
9047
9594
|
const message = err.message;
|
|
9048
9595
|
return c.json({ error: message }, err instanceof OutputStyleConflictError ? 409 : 500);
|
|
9049
9596
|
}
|
|
9050
9597
|
function createOutputStylesRouter() {
|
|
9051
|
-
const app = new
|
|
9598
|
+
const app = new Hono11();
|
|
9052
9599
|
app.get("/", (c) => {
|
|
9053
9600
|
const auth = c.get("auth");
|
|
9054
9601
|
const denied = requireAdmin(auth);
|
|
@@ -9139,11 +9686,44 @@ function createOutputStylesRouter() {
|
|
|
9139
9686
|
});
|
|
9140
9687
|
return app;
|
|
9141
9688
|
}
|
|
9689
|
+
async function handleOutputStylesRequest(req) {
|
|
9690
|
+
const app = new Hono11();
|
|
9691
|
+
app.use("*", async (c, next) => {
|
|
9692
|
+
c.set("auth", { role: "admin" });
|
|
9693
|
+
await next();
|
|
9694
|
+
});
|
|
9695
|
+
app.route("/", createOutputStylesRouter());
|
|
9696
|
+
const res = await app.request(req.path, {
|
|
9697
|
+
method: req.method,
|
|
9698
|
+
...req.body !== void 0 && {
|
|
9699
|
+
body: JSON.stringify(req.body),
|
|
9700
|
+
headers: { "content-type": "application/json" }
|
|
9701
|
+
}
|
|
9702
|
+
});
|
|
9703
|
+
return { status: res.status, body: await res.json().catch(() => null) };
|
|
9704
|
+
}
|
|
9705
|
+
function registerRelayedOutputStylesRoutes(api, agent) {
|
|
9706
|
+
api.all("/output-styles/*", relay(agent, (p) => p.slice(p.indexOf("/output-styles") + "/output-styles".length) || "/"));
|
|
9707
|
+
api.all("/output-styles", relay(agent, () => "/"));
|
|
9708
|
+
}
|
|
9709
|
+
function relay(agent, pathOf) {
|
|
9710
|
+
return async (c) => {
|
|
9711
|
+
const denied = requireAdmin(c.get("auth"));
|
|
9712
|
+
if (denied) return c.json({ error: denied }, 403);
|
|
9713
|
+
const method = c.req.method;
|
|
9714
|
+
const body = method === "GET" || method === "DELETE" ? void 0 : await c.req.json().catch(() => null);
|
|
9715
|
+
const result = await agent.outputStylesRequest?.({ method, path: pathOf(c.req.path), body });
|
|
9716
|
+
if (!result) {
|
|
9717
|
+
return c.json({ error: "Output styles need the Host machine, which is not connected right now." }, 503);
|
|
9718
|
+
}
|
|
9719
|
+
return c.json(result.body, result.status);
|
|
9720
|
+
};
|
|
9721
|
+
}
|
|
9142
9722
|
|
|
9143
9723
|
// src/dashboard/skills-routes.ts
|
|
9144
9724
|
import { existsSync as existsSync18, statSync as statSync6 } from "fs";
|
|
9145
9725
|
import { dirname as dirname4 } from "path";
|
|
9146
|
-
import { Hono as
|
|
9726
|
+
import { Hono as Hono12 } from "hono";
|
|
9147
9727
|
|
|
9148
9728
|
// src/dashboard/skill-mirror.ts
|
|
9149
9729
|
import { createHash as createHash8 } from "crypto";
|
|
@@ -9170,7 +9750,7 @@ function ownSkills(scope, dir) {
|
|
|
9170
9750
|
}
|
|
9171
9751
|
function snapshot(scopes) {
|
|
9172
9752
|
const wants = (scope) => !scopes || scopes.has(scope);
|
|
9173
|
-
const configDir2 =
|
|
9753
|
+
const configDir2 = profileClaudeConfigDir();
|
|
9174
9754
|
const rows = [];
|
|
9175
9755
|
if (wants("workspace")) rows.push(...ownSkills("workspace", workspaceSkillsDir(configDir2)));
|
|
9176
9756
|
if (wants("plugin")) {
|
|
@@ -9262,7 +9842,7 @@ async function readMirroredSkills(scopes, name) {
|
|
|
9262
9842
|
|
|
9263
9843
|
// src/dashboard/skills-routes.ts
|
|
9264
9844
|
function workspaceScope() {
|
|
9265
|
-
const claudeConfigDir =
|
|
9845
|
+
const claudeConfigDir = profileClaudeConfigDir();
|
|
9266
9846
|
const cwd = defaultClaudeCwd();
|
|
9267
9847
|
return {
|
|
9268
9848
|
dir: workspaceSkillsDir(claudeConfigDir),
|
|
@@ -9468,12 +10048,12 @@ function mountSkillRoutes(app, base, refOf, backend) {
|
|
|
9468
10048
|
var workspaceRef = () => ({ kind: "workspace" });
|
|
9469
10049
|
var channelRef = (c) => ({ kind: "channel", channelId: c.req.param("id") });
|
|
9470
10050
|
function createWorkspaceSkillsRouter() {
|
|
9471
|
-
const app = new
|
|
10051
|
+
const app = new Hono12();
|
|
9472
10052
|
mountSkillRoutes(app, "/skills", workspaceRef, diskBackend);
|
|
9473
10053
|
return app;
|
|
9474
10054
|
}
|
|
9475
10055
|
function createChannelSkillsRouter() {
|
|
9476
|
-
const app = new
|
|
10056
|
+
const app = new Hono12();
|
|
9477
10057
|
mountSkillRoutes(app, "/:id/skill-files", channelRef, diskBackend);
|
|
9478
10058
|
return app;
|
|
9479
10059
|
}
|
|
@@ -9491,7 +10071,7 @@ async function resolveAgentIdentities() {
|
|
|
9491
10071
|
const main2 = [
|
|
9492
10072
|
{
|
|
9493
10073
|
name: BOT_DM_KEY,
|
|
9494
|
-
label: "Workspace
|
|
10074
|
+
label: "Workspace Agent",
|
|
9495
10075
|
kind: "main",
|
|
9496
10076
|
cwd: defaultCwd,
|
|
9497
10077
|
model: settings.agentProfiles?.[BOT_DM_KEY]?.model,
|
|
@@ -9512,8 +10092,8 @@ async function resolveAgentIdentities() {
|
|
|
9512
10092
|
// src/dashboard/providers.ts
|
|
9513
10093
|
import { execFileSync } from "child_process";
|
|
9514
10094
|
import { existsSync as existsSync19 } from "fs";
|
|
9515
|
-
import { homedir as
|
|
9516
|
-
import { join as
|
|
10095
|
+
import { homedir as homedir7 } from "os";
|
|
10096
|
+
import { join as join15, resolve as resolve5 } from "path";
|
|
9517
10097
|
var PROVIDER_LABELS = {
|
|
9518
10098
|
claude: "Claude Code",
|
|
9519
10099
|
codex: "Codex"
|
|
@@ -9530,8 +10110,8 @@ function binaryOnPath(bin) {
|
|
|
9530
10110
|
}
|
|
9531
10111
|
function claudeAuthAvailable() {
|
|
9532
10112
|
if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
|
|
9533
|
-
const configDir2 =
|
|
9534
|
-
if (existsSync19(
|
|
10113
|
+
const configDir2 = profileClaudeConfigDir() || resolve5(homedir7(), ".claude");
|
|
10114
|
+
if (existsSync19(join15(configDir2, ".credentials.json"))) return true;
|
|
9535
10115
|
if (process.platform !== "darwin") return false;
|
|
9536
10116
|
try {
|
|
9537
10117
|
execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
@@ -9594,6 +10174,8 @@ function registerHostRoutes(api) {
|
|
|
9594
10174
|
agent,
|
|
9595
10175
|
displayName: body["displayName"] === null || typeof body["displayName"] === "string" ? body["displayName"] : void 0,
|
|
9596
10176
|
gaMeasurementId: body["gaMeasurementId"] === null || typeof body["gaMeasurementId"] === "string" ? body["gaMeasurementId"] : void 0,
|
|
10177
|
+
autoTitleEnabled: typeof body["autoTitleEnabled"] === "boolean" ? body["autoTitleEnabled"] : void 0,
|
|
10178
|
+
autoTitlePrompt: body["autoTitlePrompt"] === null || typeof body["autoTitlePrompt"] === "string" ? body["autoTitlePrompt"] : void 0,
|
|
9597
10179
|
fallbackModel: body["fallbackModel"] === null || typeof body["fallbackModel"] === "string" ? body["fallbackModel"] : void 0,
|
|
9598
10180
|
// Shape-checked only — which runners exist, whether a model belongs to
|
|
9599
10181
|
// the runner it's listed under, and what a missing/duplicate entry
|
|
@@ -9632,7 +10214,7 @@ function registerHostRoutes(api) {
|
|
|
9632
10214
|
api.delete("/settings/prompt", (c) => c.json(resetWorkspacePrompt()));
|
|
9633
10215
|
api.get(
|
|
9634
10216
|
"/settings/bot",
|
|
9635
|
-
async (c) => c.json({ name: BOT_PROFILE_KEY, label: "Workspace
|
|
10217
|
+
async (c) => c.json({ name: BOT_PROFILE_KEY, label: "Workspace Agent", ...await loadBotIdentity() })
|
|
9636
10218
|
);
|
|
9637
10219
|
api.get("/settings/agents", async (c) => c.json({ agents: await resolveAgentIdentities() }));
|
|
9638
10220
|
api.put("/settings/agents/:name", async (c) => {
|
|
@@ -9672,7 +10254,7 @@ function registerHostRoutes(api) {
|
|
|
9672
10254
|
const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
|
|
9673
10255
|
const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
|
|
9674
10256
|
const filename = errors ? "custie-error.log" : "custie.log";
|
|
9675
|
-
const file =
|
|
10257
|
+
const file = join16(paths.LOG_DIR, filename);
|
|
9676
10258
|
const exists = existsSync20(file);
|
|
9677
10259
|
return c.json({
|
|
9678
10260
|
file,
|
|
@@ -9690,11 +10272,11 @@ function tailFile(file, limit) {
|
|
|
9690
10272
|
}
|
|
9691
10273
|
|
|
9692
10274
|
// src/dashboard/data-api.ts
|
|
9693
|
-
import { existsSync as
|
|
10275
|
+
import { existsSync as existsSync22 } from "fs";
|
|
9694
10276
|
import sharp5 from "sharp";
|
|
9695
10277
|
|
|
9696
10278
|
// src/dashboard/automations-routes.ts
|
|
9697
|
-
import { Hono as
|
|
10279
|
+
import { Hono as Hono13 } from "hono";
|
|
9698
10280
|
var TYPES2 = ["schedule", "trigger", "mention-trigger"];
|
|
9699
10281
|
function isType(t) {
|
|
9700
10282
|
return TYPES2.includes(t);
|
|
@@ -9711,7 +10293,7 @@ function statusFor(message) {
|
|
|
9711
10293
|
return 400;
|
|
9712
10294
|
}
|
|
9713
10295
|
function createAutomationWriteRouter() {
|
|
9714
|
-
const app = new
|
|
10296
|
+
const app = new Hono13();
|
|
9715
10297
|
const manager = () => new AutomationManager();
|
|
9716
10298
|
app.get("/:type/:name", (c) => {
|
|
9717
10299
|
const item = manager().get(c.req.param("name"));
|
|
@@ -9767,9 +10349,9 @@ function createAutomationWriteRouter() {
|
|
|
9767
10349
|
}
|
|
9768
10350
|
|
|
9769
10351
|
// src/dashboard/categories-routes.ts
|
|
9770
|
-
import { Hono as
|
|
10352
|
+
import { Hono as Hono14 } from "hono";
|
|
9771
10353
|
function createCategoryWriteRouter() {
|
|
9772
|
-
const app = new
|
|
10354
|
+
const app = new Hono14();
|
|
9773
10355
|
app.get("/", async (c) => {
|
|
9774
10356
|
const { data, error: error2 } = await getSupabaseClient().from("categories").select("*").eq("profile", getProfile()).order("position", { ascending: true });
|
|
9775
10357
|
if (error2) return c.json({ error: error2.message }, 500);
|
|
@@ -9823,10 +10405,10 @@ function createCategoryWriteRouter() {
|
|
|
9823
10405
|
}
|
|
9824
10406
|
|
|
9825
10407
|
// src/dashboard/chat-routes.ts
|
|
9826
|
-
import { Hono as
|
|
10408
|
+
import { Hono as Hono15 } from "hono";
|
|
9827
10409
|
|
|
9828
10410
|
// src/dashboard/fork-summary.ts
|
|
9829
|
-
var
|
|
10411
|
+
var GROQ_API_URL6 = "https://api.groq.com/openai/v1/chat/completions";
|
|
9830
10412
|
var SUMMARY_MODEL = "openai/gpt-oss-120b";
|
|
9831
10413
|
var MAX_INPUT_CHARS2 = 24e3;
|
|
9832
10414
|
var TITLE_MAX_CHARS = 80;
|
|
@@ -9890,7 +10472,7 @@ async function buildForkTopic(transcript, fallbackText, mode = "condense") {
|
|
|
9890
10472
|
const apiKey = process.env["GROQ_API_KEY"];
|
|
9891
10473
|
if (!apiKey) return fallbackTopic(fallback);
|
|
9892
10474
|
try {
|
|
9893
|
-
const res = await fetch(
|
|
10475
|
+
const res = await fetch(GROQ_API_URL6, {
|
|
9894
10476
|
method: "POST",
|
|
9895
10477
|
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
9896
10478
|
body: JSON.stringify({
|
|
@@ -9990,7 +10572,7 @@ async function putThreadTranslations(lang, rows) {
|
|
|
9990
10572
|
}
|
|
9991
10573
|
|
|
9992
10574
|
// src/dashboard/thread-translate.ts
|
|
9993
|
-
var
|
|
10575
|
+
var GROQ_API_URL7 = "https://api.groq.com/openai/v1/chat/completions";
|
|
9994
10576
|
var TRANSLATION_MODEL2 = "openai/gpt-oss-120b";
|
|
9995
10577
|
var RECIPE_VERSION3 = 1;
|
|
9996
10578
|
var BATCH_CHAR_BUDGET = 16e3;
|
|
@@ -10058,7 +10640,7 @@ async function translateBatch(batch, context, lang) {
|
|
|
10058
10640
|
context: context.map((m) => ({ author: m.author, body: clip(m.body) })),
|
|
10059
10641
|
translate: batch.map((m, i) => ({ n: i + 1, author: m.author, body: clip(m.body) }))
|
|
10060
10642
|
};
|
|
10061
|
-
const res = await fetch(
|
|
10643
|
+
const res = await fetch(GROQ_API_URL7, {
|
|
10062
10644
|
method: "POST",
|
|
10063
10645
|
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
10064
10646
|
body: JSON.stringify({
|
|
@@ -10158,6 +10740,10 @@ function sanitizeModel(v) {
|
|
|
10158
10740
|
const model = v.trim().toLowerCase();
|
|
10159
10741
|
return isAllowedModel(model) ? model : null;
|
|
10160
10742
|
}
|
|
10743
|
+
async function threadModel(threadId) {
|
|
10744
|
+
const { data } = await getSupabaseClient().from("thread_replies").select("model").eq("thread_id", threadId).not("model", "is", null).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
10745
|
+
return sanitizeModel(data?.model);
|
|
10746
|
+
}
|
|
10161
10747
|
function requestedModel(c, model) {
|
|
10162
10748
|
return c.get("auth")?.role === "admin" ? sanitizeModel(model) : null;
|
|
10163
10749
|
}
|
|
@@ -10266,8 +10852,38 @@ async function withPreviews(supabase, threads) {
|
|
|
10266
10852
|
};
|
|
10267
10853
|
});
|
|
10268
10854
|
}
|
|
10855
|
+
async function labelThread(channelId, body, scope) {
|
|
10856
|
+
const channel = getProfileChannelRegistry().find(
|
|
10857
|
+
(ch) => ch.id === channelId || ch.externalChannelId === channelId
|
|
10858
|
+
);
|
|
10859
|
+
if (!channel) return null;
|
|
10860
|
+
const supabase = getSupabaseClient();
|
|
10861
|
+
const { data: recent } = await supabase.from("threads").select("auto_title, title").eq("channel_id", channelId).order("created_at", { ascending: false }).limit(50);
|
|
10862
|
+
const existingCategories = extractCategories(
|
|
10863
|
+
(recent ?? []).flatMap((t) => [
|
|
10864
|
+
t.auto_title,
|
|
10865
|
+
t.title
|
|
10866
|
+
])
|
|
10867
|
+
);
|
|
10868
|
+
return buildAutoTitle({
|
|
10869
|
+
channelName: channel.name,
|
|
10870
|
+
channelPrompt: channel.prompt,
|
|
10871
|
+
workspacePrompt: loadWorkspacePrompt().prompt,
|
|
10872
|
+
titlePrompt: (await loadWorkspaceBranding()).autoTitlePrompt,
|
|
10873
|
+
existingCategories,
|
|
10874
|
+
body,
|
|
10875
|
+
scope
|
|
10876
|
+
});
|
|
10877
|
+
}
|
|
10878
|
+
async function generateAutoTitle(threadId, channelId, body) {
|
|
10879
|
+
if ((await loadWorkspaceBranding()).autoTitleEnabled === false) return;
|
|
10880
|
+
const autoTitle = await labelThread(channelId, body);
|
|
10881
|
+
if (!autoTitle) return;
|
|
10882
|
+
const supabase = getSupabaseClient();
|
|
10883
|
+
await supabase.from("threads").update({ auto_title: autoTitle }).eq("id", threadId).is("title", null);
|
|
10884
|
+
}
|
|
10269
10885
|
function createChatRouter(agent = {}) {
|
|
10270
|
-
const app = new
|
|
10886
|
+
const app = new Hono15();
|
|
10271
10887
|
const {
|
|
10272
10888
|
onChatMessage: onMessage,
|
|
10273
10889
|
isAgentThinking,
|
|
@@ -10557,6 +11173,18 @@ function createChatRouter(agent = {}) {
|
|
|
10557
11173
|
if (error2) return c.json({ error: error2.message }, 500);
|
|
10558
11174
|
return c.json(thread);
|
|
10559
11175
|
});
|
|
11176
|
+
app.post("/threads/:id/suggest-title", async (c) => {
|
|
11177
|
+
const id = c.req.param("id");
|
|
11178
|
+
if (!await threadIsKnown(id, c)) return c.json({ error: "not found" }, 404);
|
|
11179
|
+
const { data: thread } = await getSupabaseClient().from("threads").select("channel_id").eq("id", id).single();
|
|
11180
|
+
if (!thread) return c.json({ error: "not found" }, 404);
|
|
11181
|
+
const { data: rows, error: error2 } = await getSupabaseClient().from("thread_replies").select("author, body").eq("thread_id", id).is("archived_at", null).order("created_at", { ascending: true });
|
|
11182
|
+
if (error2) return c.json({ error: error2.message }, 500);
|
|
11183
|
+
const transcript = (rows ?? []).map((r) => `${r.author}: ${slackMrkdwnToMarkdown(r.body)}`).join("\n\n");
|
|
11184
|
+
const title = await labelThread(thread.channel_id, transcript, "thread");
|
|
11185
|
+
if (!title) return c.json({ error: "could not generate a topic" }, 502);
|
|
11186
|
+
return c.json({ title });
|
|
11187
|
+
});
|
|
10560
11188
|
app.patch("/replies/:replyId", async (c) => {
|
|
10561
11189
|
const replyId = c.req.param("replyId");
|
|
10562
11190
|
const input2 = await c.req.json().catch(() => null);
|
|
@@ -10677,6 +11305,11 @@ function createChatRouter(agent = {}) {
|
|
|
10677
11305
|
effort: effortFromRow(reply["effort"])
|
|
10678
11306
|
});
|
|
10679
11307
|
}
|
|
11308
|
+
if (!replayed && thread["title"] == null) {
|
|
11309
|
+
void generateAutoTitle(thread.id, channelId, input2.body).catch(
|
|
11310
|
+
(err) => console.error("Auto title failed:", err)
|
|
11311
|
+
);
|
|
11312
|
+
}
|
|
10680
11313
|
return c.json({ ...thread, replies: [reply] }, replayed ? 200 : 201);
|
|
10681
11314
|
});
|
|
10682
11315
|
app.post("/replies/:replyId/fork", async (c) => {
|
|
@@ -10725,9 +11358,9 @@ function createChatRouter(agent = {}) {
|
|
|
10725
11358
|
return c.json({ errors: ["author and body are required"] }, 400);
|
|
10726
11359
|
}
|
|
10727
11360
|
const clientKey2 = isNonEmptyString(input2.clientKey) ? input2.clientKey : null;
|
|
10728
|
-
const model = requestedModel(c, input2.model);
|
|
10729
11361
|
const effort = requestedEffort(c, input2.effort);
|
|
10730
11362
|
if (!await threadIsKnown(threadId, c)) return c.json({ error: "not found" }, 404);
|
|
11363
|
+
const model = requestedModel(c, input2.model) ?? await threadModel(threadId);
|
|
10731
11364
|
const { data, error: error2 } = await getSupabaseClient().from("thread_replies").insert({
|
|
10732
11365
|
thread_id: threadId,
|
|
10733
11366
|
author: input2.author,
|
|
@@ -10757,7 +11390,7 @@ function createChatRouter(agent = {}) {
|
|
|
10757
11390
|
}
|
|
10758
11391
|
|
|
10759
11392
|
// src/dashboard/doc-shares-routes.ts
|
|
10760
|
-
import { Hono as
|
|
11393
|
+
import { Hono as Hono16 } from "hono";
|
|
10761
11394
|
|
|
10762
11395
|
// src/dashboard/doc-share-features.ts
|
|
10763
11396
|
import { DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
|
|
@@ -10802,7 +11435,7 @@ async function deleteShareAssets(keys) {
|
|
|
10802
11435
|
}
|
|
10803
11436
|
}
|
|
10804
11437
|
async function updateShareFeatures(share, requested) {
|
|
10805
|
-
if (share.kind === "html") return share;
|
|
11438
|
+
if (share.kind === "html" || share.kind === "excalidraw") return share;
|
|
10806
11439
|
const features = resolveFeatures(requested, share);
|
|
10807
11440
|
const updated = await updateDocShareFeatures(share.id, features);
|
|
10808
11441
|
if (!updated) return null;
|
|
@@ -10811,6 +11444,25 @@ async function updateShareFeatures(share, requested) {
|
|
|
10811
11444
|
return updated;
|
|
10812
11445
|
}
|
|
10813
11446
|
|
|
11447
|
+
// src/dashboard/share-tags.ts
|
|
11448
|
+
var MAX_TAGS = 10;
|
|
11449
|
+
var MAX_TAG_LENGTH = 30;
|
|
11450
|
+
function normalizeTag(raw) {
|
|
11451
|
+
const tag = raw.normalize("NFKC").trim().toLowerCase().replace(/^#+/, "").replace(/[\s_/\\,?#%]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_TAG_LENGTH).replace(/-+$/, "");
|
|
11452
|
+
return tag || null;
|
|
11453
|
+
}
|
|
11454
|
+
function normalizeTags(raw) {
|
|
11455
|
+
if (!Array.isArray(raw)) return null;
|
|
11456
|
+
const tags = /* @__PURE__ */ new Set();
|
|
11457
|
+
for (const item of raw) {
|
|
11458
|
+
if (typeof item !== "string") continue;
|
|
11459
|
+
const tag = normalizeTag(item);
|
|
11460
|
+
if (tag) tags.add(tag);
|
|
11461
|
+
if (tags.size >= MAX_TAGS) break;
|
|
11462
|
+
}
|
|
11463
|
+
return [...tags];
|
|
11464
|
+
}
|
|
11465
|
+
|
|
10814
11466
|
// src/dashboard/share-slug.ts
|
|
10815
11467
|
var MIN_SLUG = 2;
|
|
10816
11468
|
var MAX_SLUG = 64;
|
|
@@ -10861,6 +11513,7 @@ function shareJson(c, share) {
|
|
|
10861
11513
|
return {
|
|
10862
11514
|
id: share.id,
|
|
10863
11515
|
slug: share.slug,
|
|
11516
|
+
tags: share.tags,
|
|
10864
11517
|
url: shareUrl(c, share.slug ?? share.id),
|
|
10865
11518
|
path: share.sourcePath,
|
|
10866
11519
|
title: share.title,
|
|
@@ -10917,18 +11570,26 @@ function resolveChannel2(c) {
|
|
|
10917
11570
|
}
|
|
10918
11571
|
var LIST_LIMIT = 500;
|
|
10919
11572
|
function createDocShareListRouter() {
|
|
10920
|
-
const app = new
|
|
11573
|
+
const app = new Hono16();
|
|
10921
11574
|
app.get("/shares", async (c) => {
|
|
10922
11575
|
const viewer = c.get("auth");
|
|
11576
|
+
const readable = await filterReadableDocsChannels(
|
|
11577
|
+
viewer,
|
|
11578
|
+
getProfileChannelRegistry().map((ch) => ch.id)
|
|
11579
|
+
);
|
|
10923
11580
|
const channels = new Map(
|
|
10924
11581
|
getProfileChannelRegistry().filter((ch) => canSeeChannel(ch, viewer)).map((ch) => [ch.id, ch.name])
|
|
10925
11582
|
);
|
|
11583
|
+
for (const id of [...channels.keys()]) {
|
|
11584
|
+
if (!readable.has(id)) channels.delete(id);
|
|
11585
|
+
}
|
|
10926
11586
|
const shares = await listLiveDocShares([...channels.keys()], LIST_LIMIT + 1);
|
|
10927
11587
|
return c.json({
|
|
10928
11588
|
truncated: shares.length > LIST_LIMIT,
|
|
10929
11589
|
shares: shares.slice(0, LIST_LIMIT).map((s) => ({
|
|
10930
11590
|
id: s.id,
|
|
10931
11591
|
slug: s.slug,
|
|
11592
|
+
tags: s.tags,
|
|
10932
11593
|
url: shareUrl(c, s.slug ?? s.id),
|
|
10933
11594
|
path: s.sourcePath,
|
|
10934
11595
|
title: s.title,
|
|
@@ -10953,7 +11614,10 @@ function createDocShareListRouter() {
|
|
|
10953
11614
|
return app;
|
|
10954
11615
|
}
|
|
10955
11616
|
function createDocShareWriteRouter(agent = {}) {
|
|
10956
|
-
const app = new
|
|
11617
|
+
const app = new Hono16();
|
|
11618
|
+
app.use("/:id/docs/share", requireDocsAccess);
|
|
11619
|
+
app.use("/:id/docs/share/*", requireDocsAccess);
|
|
11620
|
+
app.use("/:id/docs/shares", requireDocsAccess);
|
|
10957
11621
|
app.get("/:id/docs/share", async (c) => {
|
|
10958
11622
|
const resolved2 = resolveChannel2(c);
|
|
10959
11623
|
if ("error" in resolved2) return resolved2.error;
|
|
@@ -11047,10 +11711,11 @@ function createDocShareWriteRouter(agent = {}) {
|
|
|
11047
11711
|
if ("invalid" in slug)
|
|
11048
11712
|
return c.json({ error: slugErrorMessage(slug.invalid), field: "slug" }, 400);
|
|
11049
11713
|
const features = readFeatures(body);
|
|
11050
|
-
|
|
11714
|
+
const tags = normalizeTags(body["tags"]);
|
|
11715
|
+
if (!("slug" in slug) && tags === null && features.audio === void 0 && features.ask === void 0 && features.social === void 0 && features.branding === void 0 && features.translation === void 0 && features.translationLangs === void 0) {
|
|
11051
11716
|
return c.json(
|
|
11052
11717
|
{
|
|
11053
|
-
error: "slug, audio, ask, social, branding, translation or translationLangs is required"
|
|
11718
|
+
error: "slug, tags, audio, ask, social, branding, translation or translationLangs is required"
|
|
11054
11719
|
},
|
|
11055
11720
|
400
|
|
11056
11721
|
);
|
|
@@ -11072,7 +11737,13 @@ function createDocShareWriteRouter(agent = {}) {
|
|
|
11072
11737
|
throw err;
|
|
11073
11738
|
}
|
|
11074
11739
|
}
|
|
11075
|
-
|
|
11740
|
+
let tagged = renamed;
|
|
11741
|
+
if (tags !== null) {
|
|
11742
|
+
const next = await setDocShareTags(renamed.id, tags);
|
|
11743
|
+
if (!next) return c.json({ error: "not shared" }, 404);
|
|
11744
|
+
tagged = next;
|
|
11745
|
+
}
|
|
11746
|
+
const updated = await updateShareFeatures(tagged, features);
|
|
11076
11747
|
if (!updated) return c.json({ error: "not shared" }, 404);
|
|
11077
11748
|
return c.json(shareJson(c, updated));
|
|
11078
11749
|
});
|
|
@@ -11100,7 +11771,7 @@ function createDocShareWriteRouter(agent = {}) {
|
|
|
11100
11771
|
}
|
|
11101
11772
|
|
|
11102
11773
|
// src/dashboard/search-routes.ts
|
|
11103
|
-
import { Hono as
|
|
11774
|
+
import { Hono as Hono17 } from "hono";
|
|
11104
11775
|
function matchScore(q, fields) {
|
|
11105
11776
|
const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
11106
11777
|
if (terms.length === 0) return 0;
|
|
@@ -11113,6 +11784,20 @@ function matchScore(q, fields) {
|
|
|
11113
11784
|
}
|
|
11114
11785
|
return score;
|
|
11115
11786
|
}
|
|
11787
|
+
var SNIPPET_LENGTH = 180;
|
|
11788
|
+
function repeatsTitle(title, snippet) {
|
|
11789
|
+
const norm = (s) => s.replace(/[*_`]/g, "").replace(/…$/, "").trim().toLowerCase();
|
|
11790
|
+
const t = norm(title);
|
|
11791
|
+
return t.length > 0 && norm(snippet).slice(0, t.length + 8).includes(t);
|
|
11792
|
+
}
|
|
11793
|
+
function snippetAround(text, q) {
|
|
11794
|
+
const lower = text.toLowerCase();
|
|
11795
|
+
const positions = q.toLowerCase().split(/\s+/).filter(Boolean).map((term) => lower.indexOf(term)).filter((i) => i >= 0);
|
|
11796
|
+
const hit = positions.length ? Math.min(...positions) : 0;
|
|
11797
|
+
const start = hit > 40 ? hit - 40 : 0;
|
|
11798
|
+
const end = Math.min(text.length, start + SNIPPET_LENGTH);
|
|
11799
|
+
return `${start > 0 ? "\u2026" : ""}${text.slice(start, end).trim()}${end < text.length ? "\u2026" : ""}`;
|
|
11800
|
+
}
|
|
11116
11801
|
async function searchThreads(q) {
|
|
11117
11802
|
const supabase = getSupabaseClient();
|
|
11118
11803
|
const { data, error: error2 } = await supabase.rpc("search_threads", { q, match_limit: 20 });
|
|
@@ -11120,19 +11805,34 @@ async function searchThreads(q) {
|
|
|
11120
11805
|
console.error("search_threads RPC failed:", error2.message);
|
|
11121
11806
|
return [];
|
|
11122
11807
|
}
|
|
11123
|
-
const
|
|
11808
|
+
const channelById = new Map(
|
|
11124
11809
|
getProfileChannelRegistry().flatMap(
|
|
11125
|
-
(ch) => ch.externalChannelId ? [
|
|
11810
|
+
(ch) => ch.externalChannelId ? [
|
|
11811
|
+
[ch.id, ch],
|
|
11812
|
+
[ch.externalChannelId, ch]
|
|
11813
|
+
] : [[ch.id, ch]]
|
|
11126
11814
|
)
|
|
11127
11815
|
);
|
|
11128
|
-
return (data ?? []).
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11816
|
+
return (data ?? []).flatMap((t) => {
|
|
11817
|
+
const channel = channelById.get(t.channel_id);
|
|
11818
|
+
if (!channel) return [];
|
|
11819
|
+
return [
|
|
11820
|
+
{
|
|
11821
|
+
type: "thread",
|
|
11822
|
+
id: t.id,
|
|
11823
|
+
title: t.title ?? t.auto_title ?? "",
|
|
11824
|
+
snippet: t.snippet && !repeatsTitle(t.title ?? t.auto_title ?? "", t.snippet) ? snippetAround(t.snippet, q) : void 0,
|
|
11825
|
+
url: `/chat/${encodeURIComponent(t.channel_id)}/${encodeURIComponent(t.id)}`,
|
|
11826
|
+
score: t.score,
|
|
11827
|
+
author: t.author,
|
|
11828
|
+
createdAt: t.created_at,
|
|
11829
|
+
channelId: channel.id,
|
|
11830
|
+
channelName: channel.name,
|
|
11831
|
+
boardStatus: t.board_status,
|
|
11832
|
+
replyCount: t.reply_count
|
|
11833
|
+
}
|
|
11834
|
+
];
|
|
11835
|
+
});
|
|
11136
11836
|
}
|
|
11137
11837
|
function searchChannels(q) {
|
|
11138
11838
|
const channels = getProfileChannelRegistry();
|
|
@@ -11219,7 +11919,7 @@ function searchAutomations(q) {
|
|
|
11219
11919
|
}
|
|
11220
11920
|
var RESULT_LIMIT = 30;
|
|
11221
11921
|
function createSearchRouter() {
|
|
11222
|
-
const app = new
|
|
11922
|
+
const app = new Hono17();
|
|
11223
11923
|
app.get("/", async (c) => {
|
|
11224
11924
|
const q = c.req.query("q")?.trim();
|
|
11225
11925
|
if (!q) return c.json({ results: [] });
|
|
@@ -11236,7 +11936,7 @@ function createSearchRouter() {
|
|
|
11236
11936
|
|
|
11237
11937
|
// src/dashboard/uploads-routes.ts
|
|
11238
11938
|
import { randomUUID } from "crypto";
|
|
11239
|
-
import { Hono as
|
|
11939
|
+
import { Hono as Hono18 } from "hono";
|
|
11240
11940
|
import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
|
|
11241
11941
|
import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
|
|
11242
11942
|
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
@@ -11251,7 +11951,7 @@ function encodeKeyForUrl(key) {
|
|
|
11251
11951
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
11252
11952
|
}
|
|
11253
11953
|
function createUploadsRouter() {
|
|
11254
|
-
const app = new
|
|
11954
|
+
const app = new Hono18();
|
|
11255
11955
|
app.post("/uploads", async (c) => {
|
|
11256
11956
|
const body = await c.req.parseBody().catch(() => null);
|
|
11257
11957
|
const file = body?.["file"];
|
|
@@ -11295,9 +11995,55 @@ function createUploadsRouter() {
|
|
|
11295
11995
|
return app;
|
|
11296
11996
|
}
|
|
11297
11997
|
|
|
11998
|
+
// src/dashboard/transcribe-routes.ts
|
|
11999
|
+
import { Hono as Hono19 } from "hono";
|
|
12000
|
+
var GROQ_TRANSCRIBE_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
12001
|
+
var MODEL = "whisper-large-v3-turbo";
|
|
12002
|
+
var MAX_AUDIO_BYTES = 5 * 1024 * 1024;
|
|
12003
|
+
var MAX_PROMPT_CHARS = 400;
|
|
12004
|
+
var NO_SPEECH_THRESHOLD = 0.6;
|
|
12005
|
+
function createTranscribeRouter() {
|
|
12006
|
+
const app = new Hono19();
|
|
12007
|
+
app.post("/transcribe", async (c) => {
|
|
12008
|
+
if (!await isFeatureEnabled(c, "dictation"))
|
|
12009
|
+
return c.json({ error: "feature not enabled" }, 403);
|
|
12010
|
+
const apiKey = process.env["GROQ_API_KEY"];
|
|
12011
|
+
if (!apiKey) return c.json({ error: "transcription is not configured" }, 503);
|
|
12012
|
+
const body = await c.req.parseBody().catch(() => null);
|
|
12013
|
+
const file = body?.["file"];
|
|
12014
|
+
if (!(file instanceof File)) return c.json({ error: "missing file" }, 400);
|
|
12015
|
+
if (file.size > MAX_AUDIO_BYTES) return c.json({ error: "audio segment too large" }, 400);
|
|
12016
|
+
const form = new FormData();
|
|
12017
|
+
form.append("file", file, file.name || "segment.wav");
|
|
12018
|
+
form.append("model", MODEL);
|
|
12019
|
+
form.append("temperature", "0");
|
|
12020
|
+
form.append("response_format", "verbose_json");
|
|
12021
|
+
const prompt = body?.["prompt"];
|
|
12022
|
+
if (typeof prompt === "string" && prompt)
|
|
12023
|
+
form.append("prompt", prompt.slice(-MAX_PROMPT_CHARS));
|
|
12024
|
+
const language = body?.["language"];
|
|
12025
|
+
if (typeof language === "string" && /^[a-z]{2}$/.test(language)) {
|
|
12026
|
+
form.append("language", language);
|
|
12027
|
+
}
|
|
12028
|
+
const res = await fetch(GROQ_TRANSCRIBE_URL, {
|
|
12029
|
+
method: "POST",
|
|
12030
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
12031
|
+
body: form
|
|
12032
|
+
});
|
|
12033
|
+
if (!res.ok) return c.json({ error: `transcription failed (${res.status})` }, 502);
|
|
12034
|
+
const data = await res.json();
|
|
12035
|
+
const segments = data.segments ?? [];
|
|
12036
|
+
if (segments.length > 0 && segments.every((s) => (s.no_speech_prob ?? 0) > NO_SPEECH_THRESHOLD)) {
|
|
12037
|
+
return c.json({ text: "" });
|
|
12038
|
+
}
|
|
12039
|
+
return c.json({ text: (data.text ?? "").trim() });
|
|
12040
|
+
});
|
|
12041
|
+
return app;
|
|
12042
|
+
}
|
|
12043
|
+
|
|
11298
12044
|
// src/dashboard/avatars-routes.ts
|
|
11299
12045
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
11300
|
-
import { Hono as
|
|
12046
|
+
import { Hono as Hono20 } from "hono";
|
|
11301
12047
|
import sharp4 from "sharp";
|
|
11302
12048
|
import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
|
|
11303
12049
|
var AVATAR_SIZE = 192;
|
|
@@ -11340,7 +12086,7 @@ function avatarKeyPrefix() {
|
|
|
11340
12086
|
return `${KEY_PREFIX3}/${getProfile()}/`;
|
|
11341
12087
|
}
|
|
11342
12088
|
function createAvatarsRouter() {
|
|
11343
|
-
const app = new
|
|
12089
|
+
const app = new Hono20();
|
|
11344
12090
|
app.get("/avatars/slack", async (c) => {
|
|
11345
12091
|
const ctx = await resolveAuthContext(c);
|
|
11346
12092
|
if (!ctx) return c.json({ error: "not signed in" }, 401);
|
|
@@ -11378,7 +12124,7 @@ function createAvatarsRouter() {
|
|
|
11378
12124
|
}
|
|
11379
12125
|
|
|
11380
12126
|
// src/dashboard/members-routes.ts
|
|
11381
|
-
import { Hono as
|
|
12127
|
+
import { Hono as Hono21 } from "hono";
|
|
11382
12128
|
function inviteLinkFor(c, token) {
|
|
11383
12129
|
const url = new URL("/auth/invite", callbackUrl(c));
|
|
11384
12130
|
url.searchParams.set("token", token);
|
|
@@ -11386,7 +12132,7 @@ function inviteLinkFor(c, token) {
|
|
|
11386
12132
|
}
|
|
11387
12133
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
11388
12134
|
function createMembersRouter() {
|
|
11389
|
-
const members = new
|
|
12135
|
+
const members = new Hono21();
|
|
11390
12136
|
members.get("/", async (c) => {
|
|
11391
12137
|
const ctx = await resolveAuthContext(c);
|
|
11392
12138
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -11415,6 +12161,33 @@ function createMembersRouter() {
|
|
|
11415
12161
|
);
|
|
11416
12162
|
return c.json({ members: members2 });
|
|
11417
12163
|
});
|
|
12164
|
+
members.get("/directory", async (c) => {
|
|
12165
|
+
const ctx = await resolveAuthContext(c);
|
|
12166
|
+
if (!ctx) return c.json({ error: "unauthorized" }, 401);
|
|
12167
|
+
const list = (await listMemberships(ctx.workspaceId)).filter((m) => m.status === "active");
|
|
12168
|
+
const directory = await Promise.all(
|
|
12169
|
+
list.map(async (m) => {
|
|
12170
|
+
const { data } = await getSupabaseClient().auth.admin.getUserById(m.userId);
|
|
12171
|
+
const metadata = data?.user?.user_metadata;
|
|
12172
|
+
const avatarUrl = metadata?.avatarKey ? await signAvatarUrl(metadata.avatarKey) : null;
|
|
12173
|
+
return { userId: m.userId, handle: m.handle, role: m.role, avatarUrl };
|
|
12174
|
+
})
|
|
12175
|
+
);
|
|
12176
|
+
return c.json({ members: directory });
|
|
12177
|
+
});
|
|
12178
|
+
members.post("/:userId/dm", async (c) => {
|
|
12179
|
+
const ctx = await resolveAuthContext(c);
|
|
12180
|
+
if (!ctx) return c.json({ error: "unauthorized" }, 401);
|
|
12181
|
+
const peerId = c.req.param("userId");
|
|
12182
|
+
if (peerId === ctx.userId) return c.json({ error: "cannot message yourself" }, 400);
|
|
12183
|
+
const peer = await getMembershipByUserId(ctx.workspaceId, peerId);
|
|
12184
|
+
if (!peer || peer.status !== "active") return c.json({ error: "member not found" }, 404);
|
|
12185
|
+
const channel = await ensureMemberDmChannel(
|
|
12186
|
+
{ userId: ctx.userId, email: ctx.email, handle: ctx.handle },
|
|
12187
|
+
{ userId: peer.userId, email: peer.email, handle: peer.handle }
|
|
12188
|
+
);
|
|
12189
|
+
return c.json({ channelId: channel.id });
|
|
12190
|
+
});
|
|
11418
12191
|
members.get("/check", async (c) => {
|
|
11419
12192
|
const ctx = await resolveAuthContext(c);
|
|
11420
12193
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -11570,7 +12343,7 @@ function createMembersRouter() {
|
|
|
11570
12343
|
}
|
|
11571
12344
|
|
|
11572
12345
|
// src/dashboard/notifications-routes.ts
|
|
11573
|
-
import { Hono as
|
|
12346
|
+
import { Hono as Hono22 } from "hono";
|
|
11574
12347
|
|
|
11575
12348
|
// src/store/notification-store.ts
|
|
11576
12349
|
function rowToNotification(row) {
|
|
@@ -11638,7 +12411,7 @@ async function markAllRead(channelIds, userId) {
|
|
|
11638
12411
|
|
|
11639
12412
|
// src/dashboard/notifications-routes.ts
|
|
11640
12413
|
function createNotificationsRouter() {
|
|
11641
|
-
const notifications = new
|
|
12414
|
+
const notifications = new Hono22();
|
|
11642
12415
|
notifications.get("/", async (c) => {
|
|
11643
12416
|
const auth = c.get("auth");
|
|
11644
12417
|
const channelIds = getProfileChannelRegistry().filter((ch) => canSeeChannel(ch, auth)).map((ch) => ch.id);
|
|
@@ -11665,7 +12438,7 @@ function createNotificationsRouter() {
|
|
|
11665
12438
|
return notifications;
|
|
11666
12439
|
}
|
|
11667
12440
|
function createThreadReadRouter() {
|
|
11668
|
-
const threads = new
|
|
12441
|
+
const threads = new Hono22();
|
|
11669
12442
|
threads.post("/threads/:threadId/read", async (c) => {
|
|
11670
12443
|
const { userId } = c.get("auth");
|
|
11671
12444
|
const threadId = c.req.param("threadId");
|
|
@@ -11677,7 +12450,7 @@ function createThreadReadRouter() {
|
|
|
11677
12450
|
}
|
|
11678
12451
|
|
|
11679
12452
|
// src/dashboard/push-routes.ts
|
|
11680
|
-
import { Hono as
|
|
12453
|
+
import { Hono as Hono23 } from "hono";
|
|
11681
12454
|
|
|
11682
12455
|
// src/push/send.ts
|
|
11683
12456
|
import webpush from "web-push";
|
|
@@ -11780,7 +12553,7 @@ async function pushToUser(userId, payload) {
|
|
|
11780
12553
|
|
|
11781
12554
|
// src/dashboard/push-routes.ts
|
|
11782
12555
|
function createPushRouter() {
|
|
11783
|
-
const push = new
|
|
12556
|
+
const push = new Hono23();
|
|
11784
12557
|
push.get("/key", (c) => c.json({ publicKey: getVapidPublicKey() }));
|
|
11785
12558
|
push.post("/subscribe", async (c) => {
|
|
11786
12559
|
const { userId } = c.get("auth");
|
|
@@ -11811,7 +12584,7 @@ function createPushRouter() {
|
|
|
11811
12584
|
}
|
|
11812
12585
|
|
|
11813
12586
|
// src/dashboard/mcp-oauth-routes.ts
|
|
11814
|
-
import { Hono as
|
|
12587
|
+
import { Hono as Hono24 } from "hono";
|
|
11815
12588
|
var CONNECTIONS_PAGE = "/settings/connections";
|
|
11816
12589
|
function callbackUri(c) {
|
|
11817
12590
|
const proto = c.req.header("x-forwarded-proto") ?? "http";
|
|
@@ -11826,7 +12599,7 @@ function backToDashboard(c, to, params) {
|
|
|
11826
12599
|
return c.redirect(`${target.pathname}${target.search}`);
|
|
11827
12600
|
}
|
|
11828
12601
|
function createMcpOAuthRouter() {
|
|
11829
|
-
const app = new
|
|
12602
|
+
const app = new Hono24();
|
|
11830
12603
|
app.get("/oauth/:provider/start", async (c) => {
|
|
11831
12604
|
const auth = c.get("auth");
|
|
11832
12605
|
if (auth.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -11953,7 +12726,7 @@ function createMcpOAuthRouter() {
|
|
|
11953
12726
|
}
|
|
11954
12727
|
|
|
11955
12728
|
// src/dashboard/mcp-routes.ts
|
|
11956
|
-
import { Hono as
|
|
12729
|
+
import { Hono as Hono25 } from "hono";
|
|
11957
12730
|
|
|
11958
12731
|
// src/mcp/transport.ts
|
|
11959
12732
|
var KINDS = ["inherit", "npx", "local", "http", "sse"];
|
|
@@ -12083,7 +12856,7 @@ function bindingsByConnection() {
|
|
|
12083
12856
|
return map;
|
|
12084
12857
|
}
|
|
12085
12858
|
function createMcpRouter() {
|
|
12086
|
-
const app = new
|
|
12859
|
+
const app = new Hono25();
|
|
12087
12860
|
app.get("/providers", async (c) => {
|
|
12088
12861
|
const denied = requireAdmin3(c.get("auth"));
|
|
12089
12862
|
if (denied) return c.json({ error: denied }, 403);
|
|
@@ -12465,42 +13238,7 @@ async function buildThreadUsage(threadTs) {
|
|
|
12465
13238
|
}
|
|
12466
13239
|
|
|
12467
13240
|
// src/dashboard/channel-members-routes.ts
|
|
12468
|
-
import { Hono as
|
|
12469
|
-
|
|
12470
|
-
// src/store/channel-members-store.ts
|
|
12471
|
-
function rowToChannelMember(row) {
|
|
12472
|
-
return {
|
|
12473
|
-
id: row["id"],
|
|
12474
|
-
channelId: row["channel_id"],
|
|
12475
|
-
membershipId: row["membership_id"],
|
|
12476
|
-
role: row["role"],
|
|
12477
|
-
addedAt: row["added_at"]
|
|
12478
|
-
};
|
|
12479
|
-
}
|
|
12480
|
-
async function listChannelMembers(channelId) {
|
|
12481
|
-
const { data, error: error2 } = await getSupabaseClient().from("channel_members").select("*").eq("channel_id", channelId).order("added_at", { ascending: true });
|
|
12482
|
-
if (error2) throw new Error(`listChannelMembers(${channelId}): ${error2.message}`);
|
|
12483
|
-
return (data ?? []).map(rowToChannelMember);
|
|
12484
|
-
}
|
|
12485
|
-
async function upsertChannelMember(params) {
|
|
12486
|
-
const { data, error: error2 } = await getSupabaseClient().from("channel_members").upsert(
|
|
12487
|
-
{ channel_id: params.channelId, membership_id: params.membershipId, role: params.role },
|
|
12488
|
-
{ onConflict: "channel_id,membership_id" }
|
|
12489
|
-
).select("*").single();
|
|
12490
|
-
if (error2) {
|
|
12491
|
-
throw new Error(
|
|
12492
|
-
`upsertChannelMember(${params.channelId}, ${params.membershipId}): ${error2.message}`
|
|
12493
|
-
);
|
|
12494
|
-
}
|
|
12495
|
-
return rowToChannelMember(data);
|
|
12496
|
-
}
|
|
12497
|
-
async function removeChannelMember(channelId, membershipId) {
|
|
12498
|
-
const { data, error: error2 } = await getSupabaseClient().from("channel_members").delete().eq("channel_id", channelId).eq("membership_id", membershipId).select("id");
|
|
12499
|
-
if (error2) throw new Error(`removeChannelMember(${channelId}, ${membershipId}): ${error2.message}`);
|
|
12500
|
-
return data?.length ? true : false;
|
|
12501
|
-
}
|
|
12502
|
-
|
|
12503
|
-
// src/dashboard/channel-members-routes.ts
|
|
13241
|
+
import { Hono as Hono26 } from "hono";
|
|
12504
13242
|
async function isChannelAdmin(ctx, channelId) {
|
|
12505
13243
|
if (ctx.role === "admin") return true;
|
|
12506
13244
|
const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
|
|
@@ -12509,7 +13247,7 @@ async function isChannelAdmin(ctx, channelId) {
|
|
|
12509
13247
|
return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
|
|
12510
13248
|
}
|
|
12511
13249
|
function createChannelMembersRouter() {
|
|
12512
|
-
const router = new
|
|
13250
|
+
const router = new Hono26();
|
|
12513
13251
|
router.get("/:id/members", async (c) => {
|
|
12514
13252
|
const ctx = await resolveAuthContext(c);
|
|
12515
13253
|
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
@@ -12545,277 +13283,143 @@ function createChannelMembersRouter() {
|
|
|
12545
13283
|
if (!removed) return c.json({ error: "no such channel member" }, 404);
|
|
12546
13284
|
return c.body(null, 204);
|
|
12547
13285
|
});
|
|
13286
|
+
router.get("/:id/docs-access/me", async (c) => {
|
|
13287
|
+
const ctx = await resolveAuthContext(c);
|
|
13288
|
+
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
13289
|
+
return c.json({ access: await resolveDocsAccess(ctx, c.req.param("id")) });
|
|
13290
|
+
});
|
|
13291
|
+
router.get("/:id/docs-access", async (c) => {
|
|
13292
|
+
const ctx = await resolveAuthContext(c);
|
|
13293
|
+
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
13294
|
+
const channelId = c.req.param("id");
|
|
13295
|
+
if (!await isChannelAdmin(ctx, channelId)) {
|
|
13296
|
+
return c.json({ error: "channel admin access required" }, 403);
|
|
13297
|
+
}
|
|
13298
|
+
return c.json({ grants: await listChannelDocsAccess(channelId) });
|
|
13299
|
+
});
|
|
13300
|
+
router.put("/:id/docs-access/:membershipId", async (c) => {
|
|
13301
|
+
const ctx = await resolveAuthContext(c);
|
|
13302
|
+
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
13303
|
+
const channelId = c.req.param("id");
|
|
13304
|
+
if (!await isChannelAdmin(ctx, channelId)) {
|
|
13305
|
+
return c.json({ error: "channel admin access required" }, 403);
|
|
13306
|
+
}
|
|
13307
|
+
const body = await c.req.json().catch(() => ({}));
|
|
13308
|
+
if (!isDocsGrant(body.access)) {
|
|
13309
|
+
return c.json({ error: "access must be 'read' or 'write'" }, 400);
|
|
13310
|
+
}
|
|
13311
|
+
const grant = await upsertChannelDocsAccess({
|
|
13312
|
+
channelId,
|
|
13313
|
+
membershipId: c.req.param("membershipId"),
|
|
13314
|
+
access: body.access
|
|
13315
|
+
});
|
|
13316
|
+
return c.json({ grant });
|
|
13317
|
+
});
|
|
13318
|
+
router.delete("/:id/docs-access/:membershipId", async (c) => {
|
|
13319
|
+
const ctx = await resolveAuthContext(c);
|
|
13320
|
+
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
13321
|
+
const channelId = c.req.param("id");
|
|
13322
|
+
if (!await isChannelAdmin(ctx, channelId)) {
|
|
13323
|
+
return c.json({ error: "channel admin access required" }, 403);
|
|
13324
|
+
}
|
|
13325
|
+
const removed = await removeChannelDocsAccess(channelId, c.req.param("membershipId"));
|
|
13326
|
+
if (!removed) return c.json({ error: "no such docs grant" }, 404);
|
|
13327
|
+
return c.body(null, 204);
|
|
13328
|
+
});
|
|
12548
13329
|
return router;
|
|
12549
13330
|
}
|
|
12550
13331
|
|
|
12551
13332
|
// src/dashboard/onboarding-routes.ts
|
|
12552
|
-
import { Hono as
|
|
13333
|
+
import { Hono as Hono27 } from "hono";
|
|
12553
13334
|
|
|
12554
|
-
// src/dashboard/
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
readFileSync as readFileSync11,
|
|
12562
|
-
statSync as statSync8,
|
|
12563
|
-
symlinkSync
|
|
12564
|
-
} from "fs";
|
|
12565
|
-
import { homedir as homedir7 } from "os";
|
|
12566
|
-
import { basename as basename5, join as join16, resolve as resolve6 } from "path";
|
|
12567
|
-
var MAX_JSONL_HEAD_BYTES = 64 * 1024;
|
|
12568
|
-
var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
|
|
12569
|
-
function flyspaceRoot() {
|
|
12570
|
-
return process.env["FLYSPACE_ROOT"] || join16(homedir7(), "Flyspace");
|
|
12571
|
-
}
|
|
12572
|
-
function onPath(cmd) {
|
|
12573
|
-
try {
|
|
12574
|
-
execFileSync2(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
12575
|
-
return true;
|
|
12576
|
-
} catch {
|
|
12577
|
-
return false;
|
|
12578
|
-
}
|
|
12579
|
-
}
|
|
12580
|
-
function claudeDir() {
|
|
12581
|
-
return process.env["CLAUDE_CONFIG_DIR"] || join16(homedir7(), ".claude");
|
|
12582
|
-
}
|
|
12583
|
-
function codexDir() {
|
|
12584
|
-
return process.env["CODEX_HOME"] || join16(homedir7(), ".codex");
|
|
12585
|
-
}
|
|
12586
|
-
function claudeLoggedIn() {
|
|
12587
|
-
if (process.env["ANTHROPIC_API_KEY"]) return true;
|
|
12588
|
-
if (existsSync22(join16(claudeDir(), ".credentials.json"))) return true;
|
|
12589
|
-
try {
|
|
12590
|
-
const raw = JSON.parse(readFileSync11(join16(homedir7(), ".claude.json"), "utf-8"));
|
|
12591
|
-
if (raw.oauthAccount) return true;
|
|
12592
|
-
} catch {
|
|
12593
|
-
}
|
|
12594
|
-
if (process.platform !== "darwin") return false;
|
|
12595
|
-
try {
|
|
12596
|
-
execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
12597
|
-
stdio: "ignore"
|
|
12598
|
-
});
|
|
12599
|
-
return true;
|
|
12600
|
-
} catch {
|
|
12601
|
-
return false;
|
|
12602
|
-
}
|
|
13335
|
+
// src/dashboard/unique-channel-name.ts
|
|
13336
|
+
function uniqueChannelName(raw, taken) {
|
|
13337
|
+
const base = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
13338
|
+
let name = base;
|
|
13339
|
+
for (let i = 2; taken.has(name); i += 1) name = `${base}-${i}`;
|
|
13340
|
+
taken.add(name);
|
|
13341
|
+
return name;
|
|
12603
13342
|
}
|
|
12604
|
-
|
|
12605
|
-
|
|
13343
|
+
|
|
13344
|
+
// src/dashboard/workspace-greeting.ts
|
|
13345
|
+
var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
|
|
13346
|
+
var GREETING_MODEL = "openai/gpt-oss-20b";
|
|
13347
|
+
var TIMEOUT_MS = 6e3;
|
|
13348
|
+
var MAX_LISTED_CHANNELS = 8;
|
|
13349
|
+
var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner sees, posted in their #general channel. Flyspace is a team chat where an AI agent works inside every channel.
|
|
13350
|
+
|
|
13351
|
+
- Plain Markdown, 60 to 110 words, in English. No headings, no emoji, no em or en dashes, no sign-off.
|
|
13352
|
+
- Greet the owner by name and name the workspace.
|
|
13353
|
+
- Say that #general is public: everyone invited to the workspace can see it.
|
|
13354
|
+
- If project channels are listed, mention them as #name and say each one is linked to that project folder. Never invent a channel.
|
|
13355
|
+
- Mention the trial or the missing Host only if the facts say so.
|
|
13356
|
+
- Use only the facts given. Do not add buttons or a list of next steps: those are appended for you.`;
|
|
13357
|
+
function greetingButtons(input2) {
|
|
13358
|
+
const buttons = [];
|
|
13359
|
+
if (!input2.hostConnected) buttons.push("Connect a machine");
|
|
13360
|
+
if (input2.channels[0]) buttons.push(`Summarise #${input2.channels[0]} for me`);
|
|
13361
|
+
if (input2.memberCount <= 1) buttons.push("Invite my team");
|
|
13362
|
+
buttons.push("Set up a daily digest");
|
|
13363
|
+
return buttons.slice(0, 3).map((b) => b.replace(/[|\]]/g, ""));
|
|
13364
|
+
}
|
|
13365
|
+
function fallbackGreeting(input2) {
|
|
13366
|
+
const channels = input2.channels.slice(0, MAX_LISTED_CHANNELS).map((c) => `#${c}`);
|
|
13367
|
+
return [
|
|
13368
|
+
`Welcome to ${input2.workspaceName}, ${input2.ownerName}.`,
|
|
13369
|
+
"This is #general. It is public, so everyone you invite to the workspace can see it.",
|
|
13370
|
+
channels.length ? `Your project channels are ready: ${channels.join(", ")}. Each one is linked to its project folder, so the agent there works on the real files.` : "",
|
|
13371
|
+
"Mention the agent in any channel to get started."
|
|
13372
|
+
].filter(Boolean).join("\n\n");
|
|
13373
|
+
}
|
|
13374
|
+
async function groqGreeting(input2, apiKey) {
|
|
13375
|
+
const facts = [
|
|
13376
|
+
`Owner: ${input2.ownerName}`,
|
|
13377
|
+
`Workspace: ${input2.workspaceName}`,
|
|
13378
|
+
`Plan: ${input2.trialing ? "14-day free trial, just started" : "not on a trial"}`,
|
|
13379
|
+
`Host: ${input2.hostConnected ? "a machine is connected, agents can run" : "no machine connected yet, agents cannot run until one is"}`,
|
|
13380
|
+
`Project channels: ${input2.channels.slice(0, MAX_LISTED_CHANNELS).join(", ") || "(none yet)"}`,
|
|
13381
|
+
`Members: ${input2.memberCount}`
|
|
13382
|
+
].join("\n");
|
|
13383
|
+
const res = await fetch(GROQ_API_URL8, {
|
|
13384
|
+
method: "POST",
|
|
13385
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
13386
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
13387
|
+
body: JSON.stringify({
|
|
13388
|
+
model: GREETING_MODEL,
|
|
13389
|
+
temperature: 0.6,
|
|
13390
|
+
// Reasoning model: the cap covers reasoning tokens too, so keep it roomy.
|
|
13391
|
+
reasoning_effort: "low",
|
|
13392
|
+
max_tokens: 900,
|
|
13393
|
+
messages: [
|
|
13394
|
+
{ role: "system", content: SYSTEM_PROMPT4 },
|
|
13395
|
+
{ role: "user", content: facts }
|
|
13396
|
+
]
|
|
13397
|
+
})
|
|
13398
|
+
});
|
|
13399
|
+
if (!res.ok) throw new Error(`Groq request failed (${res.status})`);
|
|
13400
|
+
const data = await res.json();
|
|
13401
|
+
const text = data.choices?.[0]?.message?.content?.replace(/\[BUTTONS:[^\]]*\]/g, "").replace(/\s*[–—]\s*/g, ", ").trim();
|
|
13402
|
+
return text || null;
|
|
12606
13403
|
}
|
|
12607
|
-
function
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
for (const e of entries2) {
|
|
12616
|
-
const full = join16(dir, e.name);
|
|
12617
|
-
if (e.isDirectory()) {
|
|
12618
|
-
if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
|
|
12619
|
-
} else if (e.name.endsWith(".jsonl")) {
|
|
12620
|
-
try {
|
|
12621
|
-
out.push({ file: full, mtime: statSync8(full).mtimeMs });
|
|
12622
|
-
} catch {
|
|
12623
|
-
}
|
|
13404
|
+
async function buildGreeting(input2) {
|
|
13405
|
+
let text = null;
|
|
13406
|
+
const apiKey = process.env["GROQ_API_KEY"];
|
|
13407
|
+
if (apiKey) {
|
|
13408
|
+
try {
|
|
13409
|
+
text = await groqGreeting(input2, apiKey);
|
|
13410
|
+
} catch (err) {
|
|
13411
|
+
console.error("Workspace greeting failed, using the fixed template:", err);
|
|
12624
13412
|
}
|
|
12625
13413
|
}
|
|
12626
|
-
return
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
let head;
|
|
12630
|
-
try {
|
|
12631
|
-
head = readFileSync11(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
|
|
12632
|
-
} catch {
|
|
12633
|
-
return null;
|
|
12634
|
-
}
|
|
12635
|
-
for (const line of head.split("\n")) {
|
|
12636
|
-
if (!line.includes('"cwd"')) continue;
|
|
12637
|
-
try {
|
|
12638
|
-
const obj = JSON.parse(line);
|
|
12639
|
-
const cwd = obj.cwd ?? obj.payload?.cwd;
|
|
12640
|
-
if (typeof cwd === "string" && cwd) return cwd;
|
|
12641
|
-
} catch {
|
|
12642
|
-
}
|
|
12643
|
-
}
|
|
12644
|
-
return null;
|
|
12645
|
-
}
|
|
12646
|
-
function codexTrustedPaths() {
|
|
12647
|
-
try {
|
|
12648
|
-
const toml = readFileSync11(join16(codexDir(), "config.toml"), "utf-8");
|
|
12649
|
-
return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
|
|
12650
|
-
} catch {
|
|
12651
|
-
return [];
|
|
12652
|
-
}
|
|
12653
|
-
}
|
|
12654
|
-
function isExcluded(path) {
|
|
12655
|
-
const home = homedir7();
|
|
12656
|
-
if (path === home || path === "/") return true;
|
|
12657
|
-
if (path === "/tmp" || path.startsWith("/tmp/")) return true;
|
|
12658
|
-
if (path === "/private/tmp" || path.startsWith("/private/tmp/")) return true;
|
|
12659
|
-
const root = flyspaceRoot();
|
|
12660
|
-
if (path === root || path.startsWith(`${root}/`)) return true;
|
|
12661
|
-
try {
|
|
12662
|
-
return !statSync8(path).isDirectory();
|
|
12663
|
-
} catch {
|
|
12664
|
-
return true;
|
|
12665
|
-
}
|
|
12666
|
-
}
|
|
12667
|
-
function gitRepos() {
|
|
12668
|
-
const home = homedir7();
|
|
12669
|
-
const found = [];
|
|
12670
|
-
for (const rootName of GIT_SCAN_ROOTS) {
|
|
12671
|
-
const root = join16(home, rootName);
|
|
12672
|
-
let entries2;
|
|
12673
|
-
try {
|
|
12674
|
-
entries2 = readdirSync4(root, { withFileTypes: true });
|
|
12675
|
-
} catch {
|
|
12676
|
-
continue;
|
|
12677
|
-
}
|
|
12678
|
-
for (const e of entries2) {
|
|
12679
|
-
if (e.isDirectory() && existsSync22(join16(root, e.name, ".git"))) found.push(join16(root, e.name));
|
|
12680
|
-
}
|
|
12681
|
-
}
|
|
12682
|
-
return found;
|
|
12683
|
-
}
|
|
12684
|
-
function detectRunners(history = { claude: false, codex: false }) {
|
|
12685
|
-
return [
|
|
12686
|
-
{
|
|
12687
|
-
id: "claude",
|
|
12688
|
-
installed: onPath("claude"),
|
|
12689
|
-
loggedIn: claudeLoggedIn(),
|
|
12690
|
-
hasHistory: history.claude
|
|
12691
|
-
},
|
|
12692
|
-
{
|
|
12693
|
-
id: "codex",
|
|
12694
|
-
installed: onPath("codex"),
|
|
12695
|
-
loggedIn: codexLoggedIn(),
|
|
12696
|
-
hasHistory: history.codex
|
|
12697
|
-
}
|
|
12698
|
-
];
|
|
12699
|
-
}
|
|
12700
|
-
function scanOnboarding() {
|
|
12701
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
12702
|
-
const history = { claude: false, codex: false };
|
|
12703
|
-
const add = (rawPath, source, mtime, sessions, trusted = false) => {
|
|
12704
|
-
const path = resolve6(rawPath);
|
|
12705
|
-
if (isExcluded(path)) return;
|
|
12706
|
-
const existing = byPath.get(path);
|
|
12707
|
-
if (existing) {
|
|
12708
|
-
if (!existing.sources.includes(source)) existing.sources.push(source);
|
|
12709
|
-
existing.lastUsed = Math.max(existing.lastUsed, mtime);
|
|
12710
|
-
existing.sessions += sessions;
|
|
12711
|
-
existing.trusted ||= trusted;
|
|
12712
|
-
} else {
|
|
12713
|
-
byPath.set(path, {
|
|
12714
|
-
path,
|
|
12715
|
-
name: basename5(path),
|
|
12716
|
-
sources: [source],
|
|
12717
|
-
lastUsed: mtime,
|
|
12718
|
-
sessions,
|
|
12719
|
-
trusted
|
|
12720
|
-
});
|
|
12721
|
-
}
|
|
12722
|
-
};
|
|
12723
|
-
const claudeProjects = join16(claudeDir(), "projects");
|
|
12724
|
-
try {
|
|
12725
|
-
for (const e of readdirSync4(claudeProjects, { withFileTypes: true })) {
|
|
12726
|
-
if (!e.isDirectory()) continue;
|
|
12727
|
-
const files = jsonlFiles(join16(claudeProjects, e.name), 0);
|
|
12728
|
-
if (!files.length) continue;
|
|
12729
|
-
history.claude = true;
|
|
12730
|
-
files.sort((a, b) => b.mtime - a.mtime);
|
|
12731
|
-
let cwd = null;
|
|
12732
|
-
for (const f of files.slice(0, 3)) {
|
|
12733
|
-
cwd = cwdFromTranscript(f.file);
|
|
12734
|
-
if (cwd) break;
|
|
12735
|
-
}
|
|
12736
|
-
if (cwd) add(cwd, "claude", files[0].mtime, files.length);
|
|
12737
|
-
}
|
|
12738
|
-
} catch {
|
|
12739
|
-
}
|
|
12740
|
-
const codexSessions = jsonlFiles(join16(codexDir(), "sessions"), 4);
|
|
12741
|
-
if (codexSessions.length) history.codex = true;
|
|
12742
|
-
const perCwd = /* @__PURE__ */ new Map();
|
|
12743
|
-
for (const f of codexSessions) {
|
|
12744
|
-
const cwd = cwdFromTranscript(f.file);
|
|
12745
|
-
if (!cwd) continue;
|
|
12746
|
-
const cur = perCwd.get(cwd) ?? { mtime: 0, count: 0 };
|
|
12747
|
-
cur.mtime = Math.max(cur.mtime, f.mtime);
|
|
12748
|
-
cur.count += 1;
|
|
12749
|
-
perCwd.set(cwd, cur);
|
|
12750
|
-
}
|
|
12751
|
-
for (const [cwd, v] of perCwd) add(cwd, "codex", v.mtime, v.count);
|
|
12752
|
-
for (const p of codexTrustedPaths()) add(p, "codex", 0, 0, true);
|
|
12753
|
-
let projects = [...byPath.values()];
|
|
12754
|
-
let fromGitScan = false;
|
|
12755
|
-
if (!projects.length) {
|
|
12756
|
-
fromGitScan = true;
|
|
12757
|
-
for (const repo of gitRepos()) {
|
|
12758
|
-
let mtime = 0;
|
|
12759
|
-
try {
|
|
12760
|
-
mtime = statSync8(repo).mtimeMs;
|
|
12761
|
-
} catch {
|
|
12762
|
-
}
|
|
12763
|
-
add(repo, "git", mtime, 0);
|
|
12764
|
-
}
|
|
12765
|
-
projects = [...byPath.values()];
|
|
12766
|
-
}
|
|
12767
|
-
projects.sort(
|
|
12768
|
-
(a, b) => Number(b.sources.length > 1) - Number(a.sources.length > 1) || Number(b.trusted) - Number(a.trusted) || b.lastUsed - a.lastUsed || b.sessions - a.sessions
|
|
12769
|
-
);
|
|
12770
|
-
return {
|
|
12771
|
-
root: flyspaceRoot(),
|
|
12772
|
-
runners: detectRunners(history),
|
|
12773
|
-
projects,
|
|
12774
|
-
fromGitScan
|
|
12775
|
-
};
|
|
12776
|
-
}
|
|
12777
|
-
function uniqueChannelName(raw, taken) {
|
|
12778
|
-
const base = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
12779
|
-
let name = base;
|
|
12780
|
-
for (let i = 2; taken.has(name); i += 1) name = `${base}-${i}`;
|
|
12781
|
-
taken.add(name);
|
|
12782
|
-
return name;
|
|
12783
|
-
}
|
|
12784
|
-
function makeLink(target, linkPath) {
|
|
12785
|
-
symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
|
|
12786
|
-
}
|
|
12787
|
-
function linkProjectFolders(slug, items) {
|
|
12788
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
|
|
12789
|
-
const dir = join16(flyspaceRoot(), slug);
|
|
12790
|
-
mkdirSync6(dir, { recursive: true });
|
|
12791
|
-
const results = [];
|
|
12792
|
-
for (const item of items) {
|
|
12793
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
|
|
12794
|
-
const dest = join16(dir, item.name);
|
|
12795
|
-
let exists = true;
|
|
12796
|
-
try {
|
|
12797
|
-
lstatSync2(dest);
|
|
12798
|
-
} catch {
|
|
12799
|
-
exists = false;
|
|
12800
|
-
}
|
|
12801
|
-
if (!exists) {
|
|
12802
|
-
if (item.path) {
|
|
12803
|
-
const target = resolve6(item.path);
|
|
12804
|
-
if (!statSync8(target).isDirectory()) throw new Error(`not a directory: ${target}`);
|
|
12805
|
-
makeLink(target, dest);
|
|
12806
|
-
} else {
|
|
12807
|
-
mkdirSync6(dest, { recursive: true });
|
|
12808
|
-
}
|
|
12809
|
-
}
|
|
12810
|
-
results.push({ name: item.name, cwd: `${slug}/${item.name}` });
|
|
12811
|
-
}
|
|
12812
|
-
return results;
|
|
13414
|
+
return `${text ?? fallbackGreeting(input2)}
|
|
13415
|
+
|
|
13416
|
+
[BUTTONS: ${greetingButtons(input2).join(" | ")}]`;
|
|
12813
13417
|
}
|
|
12814
13418
|
|
|
12815
13419
|
// src/dashboard/onboarding-routes.ts
|
|
12816
13420
|
var MAX_FOLDERS = 20;
|
|
12817
13421
|
function createOnboardingRouter(agent) {
|
|
12818
|
-
const app = new
|
|
13422
|
+
const app = new Hono27();
|
|
12819
13423
|
app.get("/scan", async (c) => {
|
|
12820
13424
|
const ctx = await resolveAuthContext(c);
|
|
12821
13425
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -12848,9 +13452,54 @@ function createOnboardingRouter(agent) {
|
|
|
12848
13452
|
await agent.saveLocalDashboardSettings?.({ providers: [{ runner }] });
|
|
12849
13453
|
}
|
|
12850
13454
|
const created = [];
|
|
12851
|
-
for (const l of linked)
|
|
13455
|
+
for (const l of linked) {
|
|
13456
|
+
created.push(
|
|
13457
|
+
l.name === "general" ? await ensureGeneralChannel() : await createChannel({ name: l.name, cwd: l.cwd })
|
|
13458
|
+
);
|
|
13459
|
+
}
|
|
13460
|
+
await markWorkspaceProjectsSetup(ctx.workspaceId);
|
|
12852
13461
|
return c.json({ channels: created.map((ch) => ({ id: ch.id, name: ch.name })) }, 201);
|
|
12853
13462
|
});
|
|
13463
|
+
app.post("/greeting", async (c) => {
|
|
13464
|
+
const ctx = await resolveAuthContext(c);
|
|
13465
|
+
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
13466
|
+
const supabase = getSupabaseClient();
|
|
13467
|
+
const general = await ensureGeneralChannel();
|
|
13468
|
+
const existing = () => supabase.from("threads").select("id").eq("client_key", ctx.workspaceId).maybeSingle();
|
|
13469
|
+
const found = await existing();
|
|
13470
|
+
if (found.data) return c.json({ channelId: general.id, threadId: found.data.id });
|
|
13471
|
+
const [branding, billing, members] = await Promise.all([
|
|
13472
|
+
getWorkspaceBranding(getProfile()).catch(() => null),
|
|
13473
|
+
getWorkspaceBilling(ctx.workspaceId).catch(() => null),
|
|
13474
|
+
listMemberships(ctx.workspaceId).catch(() => [])
|
|
13475
|
+
]);
|
|
13476
|
+
const workspaceName2 = branding?.displayName || getProfile();
|
|
13477
|
+
const body = await buildGreeting({
|
|
13478
|
+
ownerName: ctx.name?.trim() || ctx.handle,
|
|
13479
|
+
workspaceName: workspaceName2,
|
|
13480
|
+
trialing: billing?.planStatus === "trialing",
|
|
13481
|
+
hostConnected: !!ctx.agentConnectedAt,
|
|
13482
|
+
channels: getProfileChannelRegistry().filter((ch) => !ch.isDefault && !ch.dmKey && !ch.agentName && !ch.archivedAt).map((ch) => ch.name),
|
|
13483
|
+
memberCount: members.length
|
|
13484
|
+
});
|
|
13485
|
+
const { data: thread, error: error2 } = await supabase.from("threads").insert({
|
|
13486
|
+
channel_id: general.id,
|
|
13487
|
+
title: `Welcome to ${workspaceName2}`,
|
|
13488
|
+
author: AGENT_AUTHOR,
|
|
13489
|
+
client_key: ctx.workspaceId
|
|
13490
|
+
}).select("id").single();
|
|
13491
|
+
if (error2) {
|
|
13492
|
+
const raced = error2.code === "23505" ? (await existing()).data : null;
|
|
13493
|
+
if (!raced) return c.json({ error: error2.message }, 500);
|
|
13494
|
+
return c.json({ channelId: general.id, threadId: raced.id });
|
|
13495
|
+
}
|
|
13496
|
+
const reply = await supabase.from("thread_replies").insert({ thread_id: thread.id, author: AGENT_AUTHOR, body });
|
|
13497
|
+
if (reply.error) {
|
|
13498
|
+
await supabase.from("threads").delete().eq("id", thread.id);
|
|
13499
|
+
return c.json({ error: reply.error.message }, 500);
|
|
13500
|
+
}
|
|
13501
|
+
return c.json({ channelId: general.id, threadId: thread.id }, 201);
|
|
13502
|
+
});
|
|
12854
13503
|
return app;
|
|
12855
13504
|
}
|
|
12856
13505
|
|
|
@@ -12980,7 +13629,8 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
12980
13629
|
externalChannelId: ch.externalChannelId,
|
|
12981
13630
|
categoryId: ch.categoryId,
|
|
12982
13631
|
position: ch.position,
|
|
12983
|
-
|
|
13632
|
+
// A DM is stored as "<a>, <b>"; each side sees the other's handle.
|
|
13633
|
+
name: ch.dmKey ? ch.name.split(", ").filter((h) => h !== auth.handle).join(", ") || ch.name : ch.name,
|
|
12984
13634
|
configured: true,
|
|
12985
13635
|
cwd: ch.cwd,
|
|
12986
13636
|
// Surfaces a rotted working directory in the sidebar instead of
|
|
@@ -12993,18 +13643,20 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
12993
13643
|
visibility: ch.visibility,
|
|
12994
13644
|
memberEmails: ch.memberEmails,
|
|
12995
13645
|
agentName: ch.agentName,
|
|
13646
|
+
dmKey: ch.dmKey,
|
|
12996
13647
|
archived: !!ch.archivedAt
|
|
12997
13648
|
}));
|
|
12998
13649
|
return c.json({
|
|
12999
13650
|
channels: list,
|
|
13000
13651
|
defaultModel: defaultModel(),
|
|
13001
13652
|
defaultEffort: defaultEffort() ?? null,
|
|
13002
|
-
defaultCwd:
|
|
13653
|
+
defaultCwd: getProfile()
|
|
13003
13654
|
});
|
|
13004
13655
|
});
|
|
13005
13656
|
api.get("/channels/:id/usage", async (c) => {
|
|
13006
13657
|
const ctx = await resolveAuthContext(c);
|
|
13007
13658
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
13659
|
+
if (!await isFeatureEnabled(c, "ai-cost")) return c.json({ error: "feature not enabled" }, 403);
|
|
13008
13660
|
const raw = Number.parseInt(c.req.query("days") ?? "7", 10);
|
|
13009
13661
|
const days = Number.isFinite(raw) && raw >= 0 ? raw : 7;
|
|
13010
13662
|
return c.json(await buildChannelUsageReport(c.req.param("id"), days, channelLabel));
|
|
@@ -13023,7 +13675,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13023
13675
|
runs.set(name, { lastRun: run3.lastRunAt.toISOString(), lastStatus: run3.lastStatus });
|
|
13024
13676
|
}
|
|
13025
13677
|
} catch {
|
|
13026
|
-
if (
|
|
13678
|
+
if (existsSync22(paths.DB_FILE)) {
|
|
13027
13679
|
const runStore = new AutomationRunStore(paths.DB_FILE);
|
|
13028
13680
|
try {
|
|
13029
13681
|
for (const run3 of runStore.listLastRuns()) {
|
|
@@ -13085,7 +13737,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13085
13737
|
const runs = (await recentRunsForWorkspace(getProfile(), 200)).map(toResponse5);
|
|
13086
13738
|
return c.json({ runs });
|
|
13087
13739
|
} catch {
|
|
13088
|
-
if (!
|
|
13740
|
+
if (!existsSync22(paths.DB_FILE)) return c.json({ runs: [] });
|
|
13089
13741
|
const runStore = new AutomationRunStore(paths.DB_FILE);
|
|
13090
13742
|
try {
|
|
13091
13743
|
const runs = runStore.listRuns(200).map(toResponse5);
|
|
@@ -13105,13 +13757,19 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13105
13757
|
api.get("/usage/threads/:threadTs", async (c) => {
|
|
13106
13758
|
const ctx = await resolveAuthContext(c);
|
|
13107
13759
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
13760
|
+
if (!await isFeatureEnabled(c, "ai-cost")) return c.json({ error: "feature not enabled" }, 403);
|
|
13108
13761
|
return c.json(await buildThreadUsage(c.req.param("threadTs")));
|
|
13109
13762
|
});
|
|
13110
13763
|
api.route("/automations", createAutomationWriteRouter());
|
|
13111
13764
|
api.route("/billing", createBillingRouter());
|
|
13112
13765
|
api.route("/chat", createChatRouter(agent));
|
|
13113
13766
|
api.route("/search", createSearchRouter());
|
|
13767
|
+
api.use("/tts/*", async (c, next) => {
|
|
13768
|
+
if (!await isFeatureEnabled(c, "read-aloud")) return c.json({ error: "feature not enabled" }, 403);
|
|
13769
|
+
await next();
|
|
13770
|
+
});
|
|
13114
13771
|
api.route("/tts", createTtsRouter());
|
|
13772
|
+
api.route("/feature-flags", createFeatureFlagsRouter());
|
|
13115
13773
|
api.route("/members", createMembersRouter());
|
|
13116
13774
|
api.route("/notifications", createNotificationsRouter());
|
|
13117
13775
|
api.route("/push", createPushRouter());
|
|
@@ -13158,6 +13816,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13158
13816
|
return c.json({ paused: body.paused, cancelled });
|
|
13159
13817
|
});
|
|
13160
13818
|
api.route("/", createUploadsRouter());
|
|
13819
|
+
api.route("/", createTranscribeRouter());
|
|
13161
13820
|
api.route("/", createAvatarsRouter());
|
|
13162
13821
|
api.get("/profiles", async (c) => {
|
|
13163
13822
|
const active = getProfile();
|
|
@@ -13200,7 +13859,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13200
13859
|
} catch {
|
|
13201
13860
|
latest = /* @__PURE__ */ new Map();
|
|
13202
13861
|
totals = /* @__PURE__ */ new Map();
|
|
13203
|
-
if (
|
|
13862
|
+
if (existsSync22(paths.DB_FILE)) {
|
|
13204
13863
|
const usageStore = new UsageStore(paths.DB_FILE);
|
|
13205
13864
|
try {
|
|
13206
13865
|
latest = usageStore.latestByThread(threadTsList);
|
|
@@ -13210,65 +13869,321 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13210
13869
|
}
|
|
13211
13870
|
}
|
|
13212
13871
|
}
|
|
13213
|
-
const threadLinks = await resolveThreadLinks(raw);
|
|
13214
|
-
const sessions = raw.map((s) => {
|
|
13215
|
-
const run3 = latest.get(s.threadTs);
|
|
13216
|
-
const turns = run3?.numTurns ?? 1;
|
|
13217
|
-
const total = run3 ? run3.inputTokens + run3.cacheReadTokens + run3.cacheCreationTokens : void 0;
|
|
13218
|
-
const link = s.channelId.startsWith("chat:") ? threadLinks.chat.get(s.threadTs) : threadLinks.external.get(`${s.channelId}:${s.threadTs}`);
|
|
13219
|
-
return {
|
|
13220
|
-
...s,
|
|
13221
|
-
channelLabel: channelLabel(s.channelId),
|
|
13222
|
-
model: run3?.model,
|
|
13223
|
-
numTurns: run3?.numTurns,
|
|
13224
|
-
contextTokens: total !== void 0 ? Math.round(total / (turns > 0 ? turns : 1)) : void 0,
|
|
13225
|
-
rolledOver: run3?.label === "rollover",
|
|
13226
|
-
threadTitle: link?.title ?? void 0,
|
|
13227
|
-
chatChannelId: link?.chatChannelId,
|
|
13228
|
-
chatThreadId: link?.chatThreadId,
|
|
13229
|
-
costUsd: totals.get(s.threadTs)?.costUsd
|
|
13230
|
-
};
|
|
13231
|
-
});
|
|
13232
|
-
return c.json({ sessions });
|
|
13233
|
-
});
|
|
13234
|
-
const LOGO_SIZE = 256;
|
|
13235
|
-
const MAX_LOGO_BYTES = 2 * 1024 * 1024;
|
|
13236
|
-
const LOGO_MIME_TYPES = ["image/png", "image/jpeg"];
|
|
13237
|
-
api.post("/settings/logo", async (c) => {
|
|
13238
|
-
const body = await c.req.parseBody().catch(() => null);
|
|
13239
|
-
const file = body?.["file"];
|
|
13240
|
-
if (!(file instanceof File)) return c.json({ error: "missing file" }, 400);
|
|
13241
|
-
if (!LOGO_MIME_TYPES.includes(file.type)) {
|
|
13242
|
-
return c.json({ error: "logo must be a PNG or JPEG image" }, 400);
|
|
13872
|
+
const threadLinks = await resolveThreadLinks(raw);
|
|
13873
|
+
const sessions = raw.map((s) => {
|
|
13874
|
+
const run3 = latest.get(s.threadTs);
|
|
13875
|
+
const turns = run3?.numTurns ?? 1;
|
|
13876
|
+
const total = run3 ? run3.inputTokens + run3.cacheReadTokens + run3.cacheCreationTokens : void 0;
|
|
13877
|
+
const link = s.channelId.startsWith("chat:") ? threadLinks.chat.get(s.threadTs) : threadLinks.external.get(`${s.channelId}:${s.threadTs}`);
|
|
13878
|
+
return {
|
|
13879
|
+
...s,
|
|
13880
|
+
channelLabel: channelLabel(s.channelId),
|
|
13881
|
+
model: run3?.model,
|
|
13882
|
+
numTurns: run3?.numTurns,
|
|
13883
|
+
contextTokens: total !== void 0 ? Math.round(total / (turns > 0 ? turns : 1)) : void 0,
|
|
13884
|
+
rolledOver: run3?.label === "rollover",
|
|
13885
|
+
threadTitle: link?.title ?? void 0,
|
|
13886
|
+
chatChannelId: link?.chatChannelId,
|
|
13887
|
+
chatThreadId: link?.chatThreadId,
|
|
13888
|
+
costUsd: totals.get(s.threadTs)?.costUsd
|
|
13889
|
+
};
|
|
13890
|
+
});
|
|
13891
|
+
return c.json({ sessions });
|
|
13892
|
+
});
|
|
13893
|
+
const LOGO_SIZE = 256;
|
|
13894
|
+
const MAX_LOGO_BYTES = 2 * 1024 * 1024;
|
|
13895
|
+
const LOGO_MIME_TYPES = ["image/png", "image/jpeg"];
|
|
13896
|
+
api.post("/settings/logo", async (c) => {
|
|
13897
|
+
const body = await c.req.parseBody().catch(() => null);
|
|
13898
|
+
const file = body?.["file"];
|
|
13899
|
+
if (!(file instanceof File)) return c.json({ error: "missing file" }, 400);
|
|
13900
|
+
if (!LOGO_MIME_TYPES.includes(file.type)) {
|
|
13901
|
+
return c.json({ error: "logo must be a PNG or JPEG image" }, 400);
|
|
13902
|
+
}
|
|
13903
|
+
if (file.size > MAX_LOGO_BYTES) {
|
|
13904
|
+
return c.json({ error: "logo must be 2MB or smaller" }, 400);
|
|
13905
|
+
}
|
|
13906
|
+
const original = Buffer.from(await file.arrayBuffer());
|
|
13907
|
+
const resized = await sharp5(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
|
|
13908
|
+
const logo = `data:image/png;base64,${resized.toString("base64")}`;
|
|
13909
|
+
return c.json(await saveWorkspaceBranding({ logo }));
|
|
13910
|
+
});
|
|
13911
|
+
api.delete("/settings/logo", async (c) => c.json(await saveWorkspaceBranding({ logo: null })));
|
|
13912
|
+
}
|
|
13913
|
+
|
|
13914
|
+
// src/dashboard/device-auth-routes.ts
|
|
13915
|
+
import { verify } from "crypto";
|
|
13916
|
+
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
13917
|
+
import { Hono as Hono28 } from "hono";
|
|
13918
|
+
|
|
13919
|
+
// src/dashboard/agent-bridge.ts
|
|
13920
|
+
import { randomBytes as randomBytes8, randomUUID as randomUUID4, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
|
|
13921
|
+
|
|
13922
|
+
// src/dashboard/onboarding-scan.ts
|
|
13923
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
13924
|
+
import {
|
|
13925
|
+
existsSync as existsSync23,
|
|
13926
|
+
lstatSync as lstatSync2,
|
|
13927
|
+
mkdirSync as mkdirSync6,
|
|
13928
|
+
readdirSync as readdirSync4,
|
|
13929
|
+
readFileSync as readFileSync11,
|
|
13930
|
+
statSync as statSync8,
|
|
13931
|
+
symlinkSync
|
|
13932
|
+
} from "fs";
|
|
13933
|
+
import { homedir as homedir8 } from "os";
|
|
13934
|
+
import { basename as basename5, join as join17, resolve as resolve6 } from "path";
|
|
13935
|
+
var MAX_JSONL_HEAD_BYTES = 64 * 1024;
|
|
13936
|
+
var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
|
|
13937
|
+
function flyspaceRoot() {
|
|
13938
|
+
return process.env["FLYSPACE_ROOT"] || join17(homedir8(), "Flyspace");
|
|
13939
|
+
}
|
|
13940
|
+
function onPath(cmd) {
|
|
13941
|
+
try {
|
|
13942
|
+
execFileSync2(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
13943
|
+
return true;
|
|
13944
|
+
} catch {
|
|
13945
|
+
return false;
|
|
13946
|
+
}
|
|
13947
|
+
}
|
|
13948
|
+
function claudeDir() {
|
|
13949
|
+
return process.env["CLAUDE_CONFIG_DIR"] || join17(homedir8(), ".claude");
|
|
13950
|
+
}
|
|
13951
|
+
function codexDir() {
|
|
13952
|
+
return process.env["CODEX_HOME"] || join17(homedir8(), ".codex");
|
|
13953
|
+
}
|
|
13954
|
+
function claudeLoggedIn() {
|
|
13955
|
+
if (process.env["ANTHROPIC_API_KEY"]) return true;
|
|
13956
|
+
if (existsSync23(join17(claudeDir(), ".credentials.json"))) return true;
|
|
13957
|
+
try {
|
|
13958
|
+
const raw = JSON.parse(readFileSync11(join17(homedir8(), ".claude.json"), "utf-8"));
|
|
13959
|
+
if (raw.oauthAccount) return true;
|
|
13960
|
+
} catch {
|
|
13961
|
+
}
|
|
13962
|
+
if (process.platform !== "darwin") return false;
|
|
13963
|
+
try {
|
|
13964
|
+
execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
13965
|
+
stdio: "ignore"
|
|
13966
|
+
});
|
|
13967
|
+
return true;
|
|
13968
|
+
} catch {
|
|
13969
|
+
return false;
|
|
13970
|
+
}
|
|
13971
|
+
}
|
|
13972
|
+
function codexLoggedIn() {
|
|
13973
|
+
return !!process.env["OPENAI_API_KEY"] || existsSync23(join17(codexDir(), "auth.json"));
|
|
13974
|
+
}
|
|
13975
|
+
function jsonlFiles(dir, depth) {
|
|
13976
|
+
const out = [];
|
|
13977
|
+
let entries2;
|
|
13978
|
+
try {
|
|
13979
|
+
entries2 = readdirSync4(dir, { withFileTypes: true });
|
|
13980
|
+
} catch {
|
|
13981
|
+
return out;
|
|
13982
|
+
}
|
|
13983
|
+
for (const e of entries2) {
|
|
13984
|
+
const full = join17(dir, e.name);
|
|
13985
|
+
if (e.isDirectory()) {
|
|
13986
|
+
if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
|
|
13987
|
+
} else if (e.name.endsWith(".jsonl")) {
|
|
13988
|
+
try {
|
|
13989
|
+
out.push({ file: full, mtime: statSync8(full).mtimeMs });
|
|
13990
|
+
} catch {
|
|
13991
|
+
}
|
|
13992
|
+
}
|
|
13993
|
+
}
|
|
13994
|
+
return out;
|
|
13995
|
+
}
|
|
13996
|
+
function cwdFromTranscript(file) {
|
|
13997
|
+
let head;
|
|
13998
|
+
try {
|
|
13999
|
+
head = readFileSync11(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
|
|
14000
|
+
} catch {
|
|
14001
|
+
return null;
|
|
14002
|
+
}
|
|
14003
|
+
for (const line of head.split("\n")) {
|
|
14004
|
+
if (!line.includes('"cwd"')) continue;
|
|
14005
|
+
try {
|
|
14006
|
+
const obj = JSON.parse(line);
|
|
14007
|
+
const cwd = obj.cwd ?? obj.payload?.cwd;
|
|
14008
|
+
if (typeof cwd === "string" && cwd) return cwd;
|
|
14009
|
+
} catch {
|
|
14010
|
+
}
|
|
14011
|
+
}
|
|
14012
|
+
return null;
|
|
14013
|
+
}
|
|
14014
|
+
function codexTrustedPaths() {
|
|
14015
|
+
try {
|
|
14016
|
+
const toml = readFileSync11(join17(codexDir(), "config.toml"), "utf-8");
|
|
14017
|
+
return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
|
|
14018
|
+
} catch {
|
|
14019
|
+
return [];
|
|
14020
|
+
}
|
|
14021
|
+
}
|
|
14022
|
+
function isExcluded(path) {
|
|
14023
|
+
const home = homedir8();
|
|
14024
|
+
if (path === home || path === "/") return true;
|
|
14025
|
+
if (path === "/tmp" || path.startsWith("/tmp/")) return true;
|
|
14026
|
+
if (path === "/private/tmp" || path.startsWith("/private/tmp/")) return true;
|
|
14027
|
+
const root = flyspaceRoot();
|
|
14028
|
+
if (path === root || path.startsWith(`${root}/`)) return true;
|
|
14029
|
+
try {
|
|
14030
|
+
return !statSync8(path).isDirectory();
|
|
14031
|
+
} catch {
|
|
14032
|
+
return true;
|
|
14033
|
+
}
|
|
14034
|
+
}
|
|
14035
|
+
function gitRepos() {
|
|
14036
|
+
const home = homedir8();
|
|
14037
|
+
const found = [];
|
|
14038
|
+
for (const rootName of GIT_SCAN_ROOTS) {
|
|
14039
|
+
const root = join17(home, rootName);
|
|
14040
|
+
let entries2;
|
|
14041
|
+
try {
|
|
14042
|
+
entries2 = readdirSync4(root, { withFileTypes: true });
|
|
14043
|
+
} catch {
|
|
14044
|
+
continue;
|
|
14045
|
+
}
|
|
14046
|
+
for (const e of entries2) {
|
|
14047
|
+
if (e.isDirectory() && existsSync23(join17(root, e.name, ".git"))) found.push(join17(root, e.name));
|
|
14048
|
+
}
|
|
14049
|
+
}
|
|
14050
|
+
return found;
|
|
14051
|
+
}
|
|
14052
|
+
function detectRunners(history = { claude: false, codex: false }) {
|
|
14053
|
+
return [
|
|
14054
|
+
{
|
|
14055
|
+
id: "claude",
|
|
14056
|
+
installed: onPath("claude"),
|
|
14057
|
+
loggedIn: claudeLoggedIn(),
|
|
14058
|
+
hasHistory: history.claude
|
|
14059
|
+
},
|
|
14060
|
+
{
|
|
14061
|
+
id: "codex",
|
|
14062
|
+
installed: onPath("codex"),
|
|
14063
|
+
loggedIn: codexLoggedIn(),
|
|
14064
|
+
hasHistory: history.codex
|
|
14065
|
+
}
|
|
14066
|
+
];
|
|
14067
|
+
}
|
|
14068
|
+
function scanOnboarding() {
|
|
14069
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
14070
|
+
const history = { claude: false, codex: false };
|
|
14071
|
+
const add = (rawPath, source, mtime, sessions, trusted = false) => {
|
|
14072
|
+
const path = resolve6(rawPath);
|
|
14073
|
+
if (isExcluded(path)) return;
|
|
14074
|
+
const existing = byPath.get(path);
|
|
14075
|
+
if (existing) {
|
|
14076
|
+
if (!existing.sources.includes(source)) existing.sources.push(source);
|
|
14077
|
+
existing.lastUsed = Math.max(existing.lastUsed, mtime);
|
|
14078
|
+
existing.sessions += sessions;
|
|
14079
|
+
existing.trusted ||= trusted;
|
|
14080
|
+
} else {
|
|
14081
|
+
byPath.set(path, {
|
|
14082
|
+
path,
|
|
14083
|
+
name: basename5(path),
|
|
14084
|
+
sources: [source],
|
|
14085
|
+
lastUsed: mtime,
|
|
14086
|
+
sessions,
|
|
14087
|
+
trusted
|
|
14088
|
+
});
|
|
14089
|
+
}
|
|
14090
|
+
};
|
|
14091
|
+
const claudeProjects = join17(claudeDir(), "projects");
|
|
14092
|
+
try {
|
|
14093
|
+
for (const e of readdirSync4(claudeProjects, { withFileTypes: true })) {
|
|
14094
|
+
if (!e.isDirectory()) continue;
|
|
14095
|
+
const files = jsonlFiles(join17(claudeProjects, e.name), 0);
|
|
14096
|
+
if (!files.length) continue;
|
|
14097
|
+
history.claude = true;
|
|
14098
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
14099
|
+
let cwd = null;
|
|
14100
|
+
for (const f of files.slice(0, 3)) {
|
|
14101
|
+
cwd = cwdFromTranscript(f.file);
|
|
14102
|
+
if (cwd) break;
|
|
14103
|
+
}
|
|
14104
|
+
if (cwd) add(cwd, "claude", files[0].mtime, files.length);
|
|
14105
|
+
}
|
|
14106
|
+
} catch {
|
|
14107
|
+
}
|
|
14108
|
+
const codexSessions = jsonlFiles(join17(codexDir(), "sessions"), 4);
|
|
14109
|
+
if (codexSessions.length) history.codex = true;
|
|
14110
|
+
const perCwd = /* @__PURE__ */ new Map();
|
|
14111
|
+
for (const f of codexSessions) {
|
|
14112
|
+
const cwd = cwdFromTranscript(f.file);
|
|
14113
|
+
if (!cwd) continue;
|
|
14114
|
+
const cur = perCwd.get(cwd) ?? { mtime: 0, count: 0 };
|
|
14115
|
+
cur.mtime = Math.max(cur.mtime, f.mtime);
|
|
14116
|
+
cur.count += 1;
|
|
14117
|
+
perCwd.set(cwd, cur);
|
|
14118
|
+
}
|
|
14119
|
+
for (const [cwd, v] of perCwd) add(cwd, "codex", v.mtime, v.count);
|
|
14120
|
+
for (const p of codexTrustedPaths()) add(p, "codex", 0, 0, true);
|
|
14121
|
+
let projects = [...byPath.values()];
|
|
14122
|
+
let fromGitScan = false;
|
|
14123
|
+
if (!projects.length) {
|
|
14124
|
+
fromGitScan = true;
|
|
14125
|
+
for (const repo of gitRepos()) {
|
|
14126
|
+
let mtime = 0;
|
|
14127
|
+
try {
|
|
14128
|
+
mtime = statSync8(repo).mtimeMs;
|
|
14129
|
+
} catch {
|
|
14130
|
+
}
|
|
14131
|
+
add(repo, "git", mtime, 0);
|
|
14132
|
+
}
|
|
14133
|
+
projects = [...byPath.values()];
|
|
14134
|
+
}
|
|
14135
|
+
projects.sort(
|
|
14136
|
+
(a, b) => Number(b.sources.length > 1) - Number(a.sources.length > 1) || Number(b.trusted) - Number(a.trusted) || b.lastUsed - a.lastUsed || b.sessions - a.sessions
|
|
14137
|
+
);
|
|
14138
|
+
return {
|
|
14139
|
+
root: flyspaceRoot(),
|
|
14140
|
+
runners: detectRunners(history),
|
|
14141
|
+
projects,
|
|
14142
|
+
fromGitScan
|
|
14143
|
+
};
|
|
14144
|
+
}
|
|
14145
|
+
function makeLink(target, linkPath) {
|
|
14146
|
+
symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
|
|
14147
|
+
}
|
|
14148
|
+
function linkProjectFolders(slug, items) {
|
|
14149
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
|
|
14150
|
+
const dir = join17(flyspaceRoot(), slug);
|
|
14151
|
+
mkdirSync6(dir, { recursive: true });
|
|
14152
|
+
const results = [];
|
|
14153
|
+
for (const item of items) {
|
|
14154
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
|
|
14155
|
+
const dest = join17(dir, item.name);
|
|
14156
|
+
let exists = true;
|
|
14157
|
+
try {
|
|
14158
|
+
lstatSync2(dest);
|
|
14159
|
+
} catch {
|
|
14160
|
+
exists = false;
|
|
13243
14161
|
}
|
|
13244
|
-
if (
|
|
13245
|
-
|
|
14162
|
+
if (!exists) {
|
|
14163
|
+
if (item.path) {
|
|
14164
|
+
const target = resolve6(item.path);
|
|
14165
|
+
if (!statSync8(target).isDirectory()) throw new Error(`not a directory: ${target}`);
|
|
14166
|
+
makeLink(target, dest);
|
|
14167
|
+
} else {
|
|
14168
|
+
mkdirSync6(dest, { recursive: true });
|
|
14169
|
+
}
|
|
13246
14170
|
}
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
return c.json(await saveWorkspaceBranding({ logo }));
|
|
13251
|
-
});
|
|
13252
|
-
api.delete("/settings/logo", async (c) => c.json(await saveWorkspaceBranding({ logo: null })));
|
|
14171
|
+
results.push({ name: item.name, cwd: `${slug}/${item.name}` });
|
|
14172
|
+
}
|
|
14173
|
+
return results;
|
|
13253
14174
|
}
|
|
13254
14175
|
|
|
13255
|
-
// src/dashboard/device-auth-routes.ts
|
|
13256
|
-
import { verify } from "crypto";
|
|
13257
|
-
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
13258
|
-
import { Hono as Hono26 } from "hono";
|
|
13259
|
-
|
|
13260
14176
|
// src/dashboard/agent-bridge.ts
|
|
13261
|
-
import {
|
|
13262
|
-
import { existsSync as existsSync26, readFileSync as readFileSync15 } from "fs";
|
|
14177
|
+
import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
|
|
13263
14178
|
import dotenv from "dotenv";
|
|
13264
14179
|
import { WebSocket, WebSocketServer } from "ws";
|
|
13265
14180
|
|
|
13266
14181
|
// src/dashboard/host-identity.ts
|
|
13267
14182
|
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "crypto";
|
|
13268
14183
|
import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
13269
|
-
import { dirname as dirname5, join as
|
|
14184
|
+
import { dirname as dirname5, join as join18 } from "path";
|
|
13270
14185
|
function hostPrivateKeyPath() {
|
|
13271
|
-
return process.env["FLYSPACE_HOST_KEY_PATH"] ||
|
|
14186
|
+
return process.env["FLYSPACE_HOST_KEY_PATH"] || join18(paths.BASE_CONFIG_DIR, "host.key");
|
|
13272
14187
|
}
|
|
13273
14188
|
function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
13274
14189
|
if (!existsSync24(path)) {
|
|
@@ -13289,8 +14204,84 @@ function loadHostPublicKey() {
|
|
|
13289
14204
|
}
|
|
13290
14205
|
}
|
|
13291
14206
|
|
|
14207
|
+
// src/dashboard/host-lock.ts
|
|
14208
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
14209
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
14210
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync13, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
14211
|
+
import { join as join19 } from "path";
|
|
14212
|
+
function hostLockPath() {
|
|
14213
|
+
return join19(paths.BASE_DATA_DIR, "host.lock");
|
|
14214
|
+
}
|
|
14215
|
+
function isLiveFlyspace(pid) {
|
|
14216
|
+
try {
|
|
14217
|
+
process.kill(pid, 0);
|
|
14218
|
+
const command = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
|
|
14219
|
+
encoding: "utf8"
|
|
14220
|
+
});
|
|
14221
|
+
return command.includes("flyspace") || command.includes("host-server");
|
|
14222
|
+
} catch {
|
|
14223
|
+
return false;
|
|
14224
|
+
}
|
|
14225
|
+
}
|
|
14226
|
+
function readLockPid(path) {
|
|
14227
|
+
try {
|
|
14228
|
+
const pid = Number(JSON.parse(readFileSync13(path, "utf8")).pid);
|
|
14229
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
14230
|
+
} catch {
|
|
14231
|
+
return null;
|
|
14232
|
+
}
|
|
14233
|
+
}
|
|
14234
|
+
function acquireHostLock() {
|
|
14235
|
+
const path = hostLockPath();
|
|
14236
|
+
mkdirSync8(paths.BASE_DATA_DIR, { recursive: true });
|
|
14237
|
+
const body = JSON.stringify({
|
|
14238
|
+
pid: process.pid,
|
|
14239
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14240
|
+
execPath: process.argv[1] ?? ""
|
|
14241
|
+
});
|
|
14242
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
14243
|
+
try {
|
|
14244
|
+
writeFileSync8(path, body, { flag: "wx", mode: 384 });
|
|
14245
|
+
const release2 = () => {
|
|
14246
|
+
if (readLockPid(path) === process.pid) rmSync4(path, { force: true });
|
|
14247
|
+
};
|
|
14248
|
+
process.once("exit", release2);
|
|
14249
|
+
return release2;
|
|
14250
|
+
} catch (err) {
|
|
14251
|
+
if (err.code !== "EEXIST") throw err;
|
|
14252
|
+
const holder = readLockPid(path);
|
|
14253
|
+
if (holder === process.pid) return () => {
|
|
14254
|
+
};
|
|
14255
|
+
if (holder && isLiveFlyspace(holder)) {
|
|
14256
|
+
throw new Error(
|
|
14257
|
+
`Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
|
|
14258
|
+
);
|
|
14259
|
+
}
|
|
14260
|
+
rmSync4(path, { force: true });
|
|
14261
|
+
}
|
|
14262
|
+
}
|
|
14263
|
+
throw new Error(`Could not take the Host lock at ${path}`);
|
|
14264
|
+
}
|
|
14265
|
+
function publishHostEndpoint(port) {
|
|
14266
|
+
const path = hostLockPath();
|
|
14267
|
+
if (readLockPid(path) !== process.pid) return null;
|
|
14268
|
+
const token = randomBytes5(32).toString("hex");
|
|
14269
|
+
const lock = JSON.parse(readFileSync13(path, "utf8"));
|
|
14270
|
+
writeFileSync8(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
|
|
14271
|
+
return token;
|
|
14272
|
+
}
|
|
14273
|
+
function readHostEndpoint() {
|
|
14274
|
+
try {
|
|
14275
|
+
const { pid, port, token } = JSON.parse(readFileSync13(hostLockPath(), "utf8"));
|
|
14276
|
+
if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
|
|
14277
|
+
return { port, token };
|
|
14278
|
+
} catch {
|
|
14279
|
+
return null;
|
|
14280
|
+
}
|
|
14281
|
+
}
|
|
14282
|
+
|
|
13292
14283
|
// src/store/device-store.ts
|
|
13293
|
-
import { createHash as createHash10, createPublicKey as createPublicKey2, randomBytes as
|
|
14284
|
+
import { createHash as createHash10, createPublicKey as createPublicKey2, randomBytes as randomBytes6, randomInt } from "crypto";
|
|
13294
14285
|
var DEVICE_CODE_TTL_MS = 10 * 6e4;
|
|
13295
14286
|
var ACCESS_TOKEN_TTL_MS = 15 * 6e4;
|
|
13296
14287
|
var CHALLENGE_TTL_MS = 2 * 6e4;
|
|
@@ -13349,7 +14340,7 @@ function fail(what, error2) {
|
|
|
13349
14340
|
async function createDeviceCode(input2) {
|
|
13350
14341
|
const db = getSupabaseClient();
|
|
13351
14342
|
await db.from("device_codes").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
13352
|
-
const deviceCode =
|
|
14343
|
+
const deviceCode = randomBytes6(32).toString("hex");
|
|
13353
14344
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
13354
14345
|
const userCode = generateUserCode();
|
|
13355
14346
|
const { error: error2 } = await db.from("device_codes").insert({
|
|
@@ -13387,26 +14378,28 @@ async function decideDeviceCode(id, userId, decision) {
|
|
|
13387
14378
|
if (error2) fail("decideDeviceCode", error2);
|
|
13388
14379
|
return (data ?? []).length > 0;
|
|
13389
14380
|
}
|
|
13390
|
-
async function redeemDeviceCode(
|
|
14381
|
+
async function redeemDeviceCode(pending3) {
|
|
13391
14382
|
const db = getSupabaseClient();
|
|
13392
|
-
const { data: claimed, error: claimError } = await db.from("device_codes").delete().eq("id",
|
|
14383
|
+
const { data: claimed, error: claimError } = await db.from("device_codes").delete().eq("id", pending3.id).select("id");
|
|
13393
14384
|
if (claimError) fail("redeemDeviceCode", claimError);
|
|
13394
14385
|
if (!claimed?.length) return null;
|
|
13395
|
-
const
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
14386
|
+
const existing = pending3.approvedBy ? await findDeviceByKey(pending3.publicKey, pending3.approvedBy) : null;
|
|
14387
|
+
const { data, error: error2 } = existing ? await db.from("devices").update({ name: pending3.name, platform: pending3.platform }).eq("id", existing.id).select("*").single() : await db.from("devices").insert({
|
|
14388
|
+
user_id: pending3.approvedBy,
|
|
14389
|
+
name: pending3.name,
|
|
14390
|
+
platform: pending3.platform,
|
|
14391
|
+
public_key: pending3.publicKey
|
|
13400
14392
|
}).select("*").single();
|
|
13401
14393
|
if (error2) fail("redeemDeviceCode", error2);
|
|
13402
14394
|
const device = toDevice(data);
|
|
13403
|
-
if (
|
|
13404
|
-
const { error: bindError } = await db.from("device_workspaces").
|
|
13405
|
-
|
|
14395
|
+
if (pending3.workspaceIds.length) {
|
|
14396
|
+
const { error: bindError } = await db.from("device_workspaces").upsert(
|
|
14397
|
+
pending3.workspaceIds.map((workspaceId) => ({
|
|
13406
14398
|
device_id: device.id,
|
|
13407
14399
|
workspace_id: workspaceId,
|
|
13408
|
-
bound_by:
|
|
13409
|
-
}))
|
|
14400
|
+
bound_by: pending3.approvedBy
|
|
14401
|
+
})),
|
|
14402
|
+
{ onConflict: "device_id,workspace_id", ignoreDuplicates: true }
|
|
13410
14403
|
);
|
|
13411
14404
|
if (bindError) fail("redeemDeviceCode", bindError);
|
|
13412
14405
|
}
|
|
@@ -13417,8 +14410,13 @@ async function getDevice(id) {
|
|
|
13417
14410
|
if (error2) fail("getDevice", error2);
|
|
13418
14411
|
return data ? toDevice(data) : null;
|
|
13419
14412
|
}
|
|
14413
|
+
async function findDeviceByKey(publicKey, userId) {
|
|
14414
|
+
const { data, error: error2 } = await getSupabaseClient().from("devices").select("*").eq("public_key", publicKey).eq("user_id", userId).is("revoked_at", null).order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
14415
|
+
if (error2) fail("findDeviceByKey", error2);
|
|
14416
|
+
return data ? toDevice(data) : null;
|
|
14417
|
+
}
|
|
13420
14418
|
async function issueAccessToken(deviceId) {
|
|
13421
|
-
const token = `fsd_${
|
|
14419
|
+
const token = `fsd_${randomBytes6(32).toString("hex")}`;
|
|
13422
14420
|
const { error: error2 } = await getSupabaseClient().from("devices").update({
|
|
13423
14421
|
access_token_hash: sha256(token),
|
|
13424
14422
|
access_token_expires_at: new Date(Date.now() + ACCESS_TOKEN_TTL_MS).toISOString(),
|
|
@@ -13440,7 +14438,7 @@ async function revokeDevice(id) {
|
|
|
13440
14438
|
async function createChallenge(deviceId) {
|
|
13441
14439
|
const db = getSupabaseClient();
|
|
13442
14440
|
await db.from("device_challenges").delete().lt("expires_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
13443
|
-
const nonce =
|
|
14441
|
+
const nonce = randomBytes6(32).toString("base64url");
|
|
13444
14442
|
const { error: error2 } = await db.from("device_challenges").insert({
|
|
13445
14443
|
nonce,
|
|
13446
14444
|
device_id: deviceId,
|
|
@@ -13465,11 +14463,28 @@ async function listDeviceWorkspaces(device) {
|
|
|
13465
14463
|
const bound = new Set((data ?? []).map((r) => r.workspace_id));
|
|
13466
14464
|
return (await listAdminWorkspaces(device.userId)).filter((w) => bound.has(w.id));
|
|
13467
14465
|
}
|
|
14466
|
+
async function bindDeviceWorkspace(deviceId, workspaceId, boundBy) {
|
|
14467
|
+
const { error: error2 } = await getSupabaseClient().from("device_workspaces").upsert(
|
|
14468
|
+
{ device_id: deviceId, workspace_id: workspaceId, bound_by: boundBy },
|
|
14469
|
+
{ onConflict: "device_id,workspace_id", ignoreDuplicates: true }
|
|
14470
|
+
);
|
|
14471
|
+
if (error2) fail("bindDeviceWorkspace", error2);
|
|
14472
|
+
}
|
|
13468
14473
|
async function listWorkspaceDevices(workspaceId) {
|
|
13469
14474
|
const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("devices!inner(*)").eq("workspace_id", workspaceId).is("devices.revoked_at", null);
|
|
13470
14475
|
if (error2) fail("listWorkspaceDevices", error2);
|
|
13471
14476
|
return (data ?? []).map((r) => toDevice(r.devices)).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
13472
14477
|
}
|
|
14478
|
+
async function listWorkspaceHosts(workspaceIds, exceptPublicKey) {
|
|
14479
|
+
const hosts2 = /* @__PURE__ */ new Map();
|
|
14480
|
+
if (!workspaceIds.length) return hosts2;
|
|
14481
|
+
const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("workspace_id, devices!inner(name, public_key, revoked_at)").in("workspace_id", workspaceIds).is("devices.revoked_at", null);
|
|
14482
|
+
if (error2) fail("listWorkspaceHosts", error2);
|
|
14483
|
+
for (const r of data ?? []) {
|
|
14484
|
+
if (r.devices.public_key !== exceptPublicKey) hosts2.set(r.workspace_id, r.devices.name);
|
|
14485
|
+
}
|
|
14486
|
+
return hosts2;
|
|
14487
|
+
}
|
|
13473
14488
|
async function userEmail(userId) {
|
|
13474
14489
|
const { data, error: error2 } = await getSupabaseClient().auth.admin.getUserById(userId);
|
|
13475
14490
|
if (error2) return null;
|
|
@@ -13478,8 +14493,8 @@ async function userEmail(userId) {
|
|
|
13478
14493
|
|
|
13479
14494
|
// src/auth/device-auth.ts
|
|
13480
14495
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey3, generateKeyPairSync as generateKeyPairSync2, sign } from "crypto";
|
|
13481
|
-
import { existsSync as existsSync25, mkdirSync as
|
|
13482
|
-
import { dirname as dirname6, join as
|
|
14496
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync9, readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "fs";
|
|
14497
|
+
import { dirname as dirname6, join as join20 } from "path";
|
|
13483
14498
|
var DEFAULT_API_URL = "https://app.flyspace.ai";
|
|
13484
14499
|
var EXPIRY_SKEW_MS = 6e4;
|
|
13485
14500
|
var AuthApiError = class extends Error {
|
|
@@ -13493,20 +14508,20 @@ function apiUrl() {
|
|
|
13493
14508
|
return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
13494
14509
|
}
|
|
13495
14510
|
function deviceKeyPath() {
|
|
13496
|
-
return
|
|
14511
|
+
return join20(paths.BASE_CONFIG_DIR, "device.key");
|
|
13497
14512
|
}
|
|
13498
14513
|
function credentialsPath() {
|
|
13499
|
-
return
|
|
14514
|
+
return join20(paths.BASE_CONFIG_DIR, "credentials.json");
|
|
13500
14515
|
}
|
|
13501
14516
|
function ensureDeviceKey(path = deviceKeyPath()) {
|
|
13502
14517
|
let created = false;
|
|
13503
14518
|
if (!existsSync25(path)) {
|
|
13504
14519
|
const { privateKey: privateKey2 } = generateKeyPairSync2("ed25519");
|
|
13505
|
-
|
|
13506
|
-
|
|
14520
|
+
mkdirSync9(dirname6(path), { recursive: true });
|
|
14521
|
+
writeFileSync9(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
|
|
13507
14522
|
created = true;
|
|
13508
14523
|
}
|
|
13509
|
-
const privateKey = createPrivateKey2(
|
|
14524
|
+
const privateKey = createPrivateKey2(readFileSync14(path, "utf8"));
|
|
13510
14525
|
const publicKeyBase64 = createPublicKey3(privateKey).export({ type: "spki", format: "der" }).toString(
|
|
13511
14526
|
"base64"
|
|
13512
14527
|
);
|
|
@@ -13515,19 +14530,19 @@ function ensureDeviceKey(path = deviceKeyPath()) {
|
|
|
13515
14530
|
function loadCredentials(path = credentialsPath()) {
|
|
13516
14531
|
if (!existsSync25(path)) return null;
|
|
13517
14532
|
try {
|
|
13518
|
-
return JSON.parse(
|
|
14533
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
13519
14534
|
} catch {
|
|
13520
14535
|
return null;
|
|
13521
14536
|
}
|
|
13522
14537
|
}
|
|
13523
14538
|
function saveCredentials(creds, path = credentialsPath()) {
|
|
13524
|
-
|
|
13525
|
-
|
|
14539
|
+
mkdirSync9(dirname6(path), { recursive: true });
|
|
14540
|
+
writeFileSync9(path, `${JSON.stringify(creds, null, 2)}
|
|
13526
14541
|
`, { mode: 384 });
|
|
13527
14542
|
}
|
|
13528
14543
|
function clearLocalAuth() {
|
|
13529
|
-
|
|
13530
|
-
|
|
14544
|
+
rmSync5(credentialsPath(), { force: true });
|
|
14545
|
+
rmSync5(deviceKeyPath(), { force: true });
|
|
13531
14546
|
}
|
|
13532
14547
|
async function post(base, path, body, token) {
|
|
13533
14548
|
return fetch(`${base}${path}`, {
|
|
@@ -13644,14 +14659,14 @@ async function getAccessToken() {
|
|
|
13644
14659
|
}
|
|
13645
14660
|
|
|
13646
14661
|
// src/dashboard/doc-share-publisher.ts
|
|
13647
|
-
import { randomBytes as
|
|
13648
|
-
import { readFileSync as
|
|
14662
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
14663
|
+
import { readFileSync as readFileSync15, statSync as statSync9 } from "fs";
|
|
13649
14664
|
import { extname as extname2, posix } from "path";
|
|
13650
14665
|
import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
|
|
13651
14666
|
|
|
13652
14667
|
// src/dashboard/share-language.ts
|
|
13653
|
-
var
|
|
13654
|
-
var
|
|
14668
|
+
var GROQ_API_URL9 = "https://api.groq.com/openai/v1/chat/completions";
|
|
14669
|
+
var CLASSIFIER_MODEL4 = "openai/gpt-oss-20b";
|
|
13655
14670
|
var SAMPLE_CHARS = 4e3;
|
|
13656
14671
|
var NOISE = [
|
|
13657
14672
|
/```[\s\S]*?```/g,
|
|
@@ -13689,11 +14704,11 @@ async function detectSourceLanguage(title, content) {
|
|
|
13689
14704
|
const sample = languageSample(title, content);
|
|
13690
14705
|
if (sample.length < MIN_SAMPLE_CHARS) return null;
|
|
13691
14706
|
try {
|
|
13692
|
-
const res = await fetch(
|
|
14707
|
+
const res = await fetch(GROQ_API_URL9, {
|
|
13693
14708
|
method: "POST",
|
|
13694
14709
|
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
13695
14710
|
body: JSON.stringify({
|
|
13696
|
-
model:
|
|
14711
|
+
model: CLASSIFIER_MODEL4,
|
|
13697
14712
|
temperature: 0,
|
|
13698
14713
|
reasoning_effort: "low",
|
|
13699
14714
|
// gpt-oss reasons before it answers, and those tokens count against
|
|
@@ -13765,10 +14780,11 @@ function shareKindOf(absPath) {
|
|
|
13765
14780
|
const ext = extname2(absPath).toLowerCase();
|
|
13766
14781
|
if (ext === ".md" || ext === ".markdown") return "markdown";
|
|
13767
14782
|
if (ext === ".html" || ext === ".htm") return "html";
|
|
14783
|
+
if (ext === ".excalidraw") return "excalidraw";
|
|
13768
14784
|
return null;
|
|
13769
14785
|
}
|
|
13770
14786
|
function generateShareId() {
|
|
13771
|
-
return
|
|
14787
|
+
return randomBytes7(9).toString("base64url");
|
|
13772
14788
|
}
|
|
13773
14789
|
function isExternalHref(href) {
|
|
13774
14790
|
return /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("//") || href.startsWith("#");
|
|
@@ -13831,8 +14847,17 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
13831
14847
|
}
|
|
13832
14848
|
if (!stat.isFile()) return { status: "not-found" };
|
|
13833
14849
|
if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
|
|
13834
|
-
const source =
|
|
13835
|
-
const title = extractTitle(abs, kind === "
|
|
14850
|
+
const source = readFileSync15(abs, "utf8");
|
|
14851
|
+
const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
|
|
14852
|
+
if (kind === "excalidraw") {
|
|
14853
|
+
try {
|
|
14854
|
+
const parsed = JSON.parse(source);
|
|
14855
|
+
if (!parsed || typeof parsed !== "object") return { status: "unsupported" };
|
|
14856
|
+
} catch {
|
|
14857
|
+
return { status: "unsupported" };
|
|
14858
|
+
}
|
|
14859
|
+
return { kind, title, content: source, assetKeys: [] };
|
|
14860
|
+
}
|
|
13836
14861
|
const docDir = sourcePath.includes("/") ? sourcePath.slice(0, sourcePath.lastIndexOf("/")) : "";
|
|
13837
14862
|
const client2 = getR2Client();
|
|
13838
14863
|
const bucket = getR2Bucket();
|
|
@@ -13902,7 +14927,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
13902
14927
|
let text = null;
|
|
13903
14928
|
try {
|
|
13904
14929
|
const size = statSync9(docAbs).size;
|
|
13905
|
-
if (size <= budget) text =
|
|
14930
|
+
if (size <= budget) text = readFileSync15(docAbs, "utf8");
|
|
13906
14931
|
} catch {
|
|
13907
14932
|
continue;
|
|
13908
14933
|
}
|
|
@@ -13919,7 +14944,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
13919
14944
|
let body;
|
|
13920
14945
|
try {
|
|
13921
14946
|
if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
|
|
13922
|
-
body =
|
|
14947
|
+
body = readFileSync15(resolved2.abs);
|
|
13923
14948
|
} catch {
|
|
13924
14949
|
continue;
|
|
13925
14950
|
}
|
|
@@ -13956,7 +14981,7 @@ async function publishDocShare(input2) {
|
|
|
13956
14981
|
const shareId = existing?.id ?? generateShareId();
|
|
13957
14982
|
const built = await buildSnapshot(input2.cwd, input2.sourcePath, shareId);
|
|
13958
14983
|
if ("status" in built) return built;
|
|
13959
|
-
const html = built.kind
|
|
14984
|
+
const html = built.kind !== "markdown";
|
|
13960
14985
|
const features = html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
|
|
13961
14986
|
const snapshot2 = {
|
|
13962
14987
|
...built,
|
|
@@ -13990,6 +15015,281 @@ async function publishDocShare(input2) {
|
|
|
13990
15015
|
return { status: "ok", share };
|
|
13991
15016
|
}
|
|
13992
15017
|
|
|
15018
|
+
// src/dashboard/host-db-proxy.ts
|
|
15019
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15020
|
+
var SLUG_COLUMN = {
|
|
15021
|
+
workspaces: "slug",
|
|
15022
|
+
channels: "profile",
|
|
15023
|
+
automations: "profile",
|
|
15024
|
+
agent_usage: "workspace",
|
|
15025
|
+
skill_files: "workspace"
|
|
15026
|
+
};
|
|
15027
|
+
var ALL = ["GET", "HEAD", "POST", "PATCH", "DELETE"];
|
|
15028
|
+
var METHODS = {
|
|
15029
|
+
workspaces: ["GET", "HEAD"],
|
|
15030
|
+
channels: ALL,
|
|
15031
|
+
automations: ALL,
|
|
15032
|
+
agent_usage: ["GET", "HEAD", "POST"],
|
|
15033
|
+
skill_files: ALL,
|
|
15034
|
+
threads: ["GET", "HEAD", "POST", "PATCH"],
|
|
15035
|
+
thread_replies: ["GET", "HEAD", "POST", "PATCH"]
|
|
15036
|
+
};
|
|
15037
|
+
var FORWARDED_HEADERS = ["accept", "content-type", "prefer", "range"];
|
|
15038
|
+
var deny = (message, status = 403) => ({
|
|
15039
|
+
status,
|
|
15040
|
+
headers: { "content-type": "application/json" },
|
|
15041
|
+
body: JSON.stringify({ code: "FLYSPACE_DENIED", message, details: null, hint: null })
|
|
15042
|
+
});
|
|
15043
|
+
var inList = (values) => `in.(${values.map((v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",")})`;
|
|
15044
|
+
function filterValues(raw) {
|
|
15045
|
+
if (raw.startsWith("eq.")) return [raw.slice(3)];
|
|
15046
|
+
if (raw.startsWith("in.(") && raw.endsWith(")")) {
|
|
15047
|
+
const inner = raw.slice(4, -1);
|
|
15048
|
+
return inner === "" ? [] : inner.split(",").map((v) => v.replace(/^"|"$/g, ""));
|
|
15049
|
+
}
|
|
15050
|
+
return null;
|
|
15051
|
+
}
|
|
15052
|
+
function bodyRows(body) {
|
|
15053
|
+
if (!body) return [];
|
|
15054
|
+
try {
|
|
15055
|
+
const parsed = JSON.parse(body);
|
|
15056
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
15057
|
+
return rows.every((r) => r && typeof r === "object") ? rows : null;
|
|
15058
|
+
} catch {
|
|
15059
|
+
return null;
|
|
15060
|
+
}
|
|
15061
|
+
}
|
|
15062
|
+
async function getJson(forward, path) {
|
|
15063
|
+
const res = await forward({ method: "GET", path, headers: { accept: "application/json" } });
|
|
15064
|
+
if (res.status !== 200) return null;
|
|
15065
|
+
try {
|
|
15066
|
+
return JSON.parse(res.body);
|
|
15067
|
+
} catch {
|
|
15068
|
+
return null;
|
|
15069
|
+
}
|
|
15070
|
+
}
|
|
15071
|
+
async function ownedChannelIds(forward, slugs) {
|
|
15072
|
+
const rows = await getJson(
|
|
15073
|
+
forward,
|
|
15074
|
+
`/rest/v1/channels?select=id,external_channel_id&profile=${encodeURIComponent(inList(slugs))}`
|
|
15075
|
+
);
|
|
15076
|
+
if (!rows) return null;
|
|
15077
|
+
return rows.flatMap((r) => [r.id, r.external_channel_id].filter((v) => !!v));
|
|
15078
|
+
}
|
|
15079
|
+
async function threadsOwned(forward, threadIds, channelIds) {
|
|
15080
|
+
const wanted = [...new Set(threadIds)];
|
|
15081
|
+
if (wanted.length === 0 || channelIds.length === 0) return false;
|
|
15082
|
+
const rows = await getJson(
|
|
15083
|
+
forward,
|
|
15084
|
+
`/rest/v1/threads?select=id&id=${encodeURIComponent(inList(wanted))}&channel_id=${encodeURIComponent(inList(channelIds))}`
|
|
15085
|
+
);
|
|
15086
|
+
return !!rows && new Set(rows.map((r) => r.id)).size === wanted.length;
|
|
15087
|
+
}
|
|
15088
|
+
async function replyThreadIds(forward, method, params, rows) {
|
|
15089
|
+
if (method === "POST") {
|
|
15090
|
+
const ids = rows.map((r) => r["thread_id"]);
|
|
15091
|
+
return ids.length > 0 && ids.every((v) => typeof v === "string" && UUID.test(v)) ? ids : null;
|
|
15092
|
+
}
|
|
15093
|
+
const threadFilters = params.getAll("thread_id").map(filterValues);
|
|
15094
|
+
if (threadFilters.length > 0) {
|
|
15095
|
+
const ids = threadFilters.flatMap((v) => v ?? [""]);
|
|
15096
|
+
return ids.length > 0 && ids.every((v) => UUID.test(v)) ? ids : null;
|
|
15097
|
+
}
|
|
15098
|
+
const idFilters = params.getAll("id").map(filterValues);
|
|
15099
|
+
const replyIds = idFilters.flatMap((v) => v ?? [""]);
|
|
15100
|
+
if (replyIds.length === 0 || !replyIds.every((v) => UUID.test(v))) return null;
|
|
15101
|
+
const found = await getJson(
|
|
15102
|
+
forward,
|
|
15103
|
+
`/rest/v1/thread_replies?select=id,thread_id&id=${encodeURIComponent(inList(replyIds))}`
|
|
15104
|
+
);
|
|
15105
|
+
if (!found || found.length !== new Set(replyIds).size) return null;
|
|
15106
|
+
return found.map((r) => r.thread_id);
|
|
15107
|
+
}
|
|
15108
|
+
async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []) {
|
|
15109
|
+
if (!workspaces) return deny("This Host is not bound to a workspace.");
|
|
15110
|
+
if (workspaces.length === 0) return deny("This device has no workspace.");
|
|
15111
|
+
const url = new URL(req.path, "http://proxy.invalid");
|
|
15112
|
+
const match = /^\/rest\/v1\/([a-z_]+)$/.exec(url.pathname);
|
|
15113
|
+
const table = match?.[1];
|
|
15114
|
+
if (!table || !(table in METHODS)) return deny(`Table is not available to a Host: ${url.pathname}`);
|
|
15115
|
+
const method = req.method.toUpperCase();
|
|
15116
|
+
if (!METHODS[table].includes(method)) return deny(`${method} ${table} is not available to a Host.`);
|
|
15117
|
+
const params = url.searchParams;
|
|
15118
|
+
const isWrite = method === "POST" || method === "PATCH" || method === "DELETE";
|
|
15119
|
+
const rows = method === "POST" ? bodyRows(req.body) : [];
|
|
15120
|
+
if (rows === null) return deny("Body must be a JSON object or array.", 400);
|
|
15121
|
+
const slugColumn = SLUG_COLUMN[table];
|
|
15122
|
+
if (slugColumn) {
|
|
15123
|
+
const scope = table === "skill_files" && isWrite ? assigned : workspaces;
|
|
15124
|
+
if (scope.length === 0) return deny(`This Host is not the assigned Host for any workspace it may write ${table} to.`);
|
|
15125
|
+
if (method === "POST") {
|
|
15126
|
+
const ok = rows.length > 0 && rows.every((r) => typeof r[slugColumn] === "string" && scope.includes(r[slugColumn]));
|
|
15127
|
+
if (!ok) return deny(`Every ${table} row must belong to a workspace this Host is assigned.`);
|
|
15128
|
+
}
|
|
15129
|
+
if (method === "PATCH") {
|
|
15130
|
+
const patch = bodyRows(req.body);
|
|
15131
|
+
if (!patch || patch.some((r) => slugColumn in r && !scope.includes(r[slugColumn]))) {
|
|
15132
|
+
return deny(`A ${table} row cannot be moved to another workspace.`);
|
|
15133
|
+
}
|
|
15134
|
+
}
|
|
15135
|
+
params.append(slugColumn, inList(scope));
|
|
15136
|
+
} else {
|
|
15137
|
+
const channelIds = await ownedChannelIds(forward, workspaces);
|
|
15138
|
+
if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
|
|
15139
|
+
if (table === "threads") {
|
|
15140
|
+
if (method === "POST") {
|
|
15141
|
+
const ok = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
|
|
15142
|
+
if (!ok) return deny("A thread must belong to a channel of the device workspaces.");
|
|
15143
|
+
}
|
|
15144
|
+
if (method === "PATCH") {
|
|
15145
|
+
const patch = bodyRows(req.body);
|
|
15146
|
+
if (!patch || patch.some((r) => "channel_id" in r)) return deny("A thread cannot move channels.");
|
|
15147
|
+
}
|
|
15148
|
+
params.append("channel_id", inList(channelIds));
|
|
15149
|
+
} else {
|
|
15150
|
+
const threadIds = await replyThreadIds(forward, method, params, rows);
|
|
15151
|
+
if (!threadIds) return deny("thread_replies requests must name the thread or replies they touch.");
|
|
15152
|
+
if (!await threadsOwned(forward, threadIds, channelIds)) return deny("Thread is outside the device workspaces.");
|
|
15153
|
+
if (method === "PATCH") {
|
|
15154
|
+
const patch = bodyRows(req.body);
|
|
15155
|
+
if (!patch || patch.some((r) => "thread_id" in r)) return deny("A reply cannot move threads.");
|
|
15156
|
+
}
|
|
15157
|
+
}
|
|
15158
|
+
}
|
|
15159
|
+
const headers = {};
|
|
15160
|
+
for (const name of FORWARDED_HEADERS) {
|
|
15161
|
+
const value = req.headers[name];
|
|
15162
|
+
if (value !== void 0) headers[name] = value;
|
|
15163
|
+
}
|
|
15164
|
+
return forward({
|
|
15165
|
+
method,
|
|
15166
|
+
path: `${url.pathname}?${params.toString()}`,
|
|
15167
|
+
headers,
|
|
15168
|
+
body: isWrite ? req.body : void 0
|
|
15169
|
+
});
|
|
15170
|
+
}
|
|
15171
|
+
var THREAD_TOPIC = /^agent-thinking:([0-9a-f-]{36})$/i;
|
|
15172
|
+
async function hostMayBroadcast(workspaces, topic, forward) {
|
|
15173
|
+
if (!workspaces || workspaces.length === 0) return false;
|
|
15174
|
+
if (topic === "channel-activity" || topic === "host-status") return true;
|
|
15175
|
+
const thread = THREAD_TOPIC.exec(topic)?.[1];
|
|
15176
|
+
if (!thread || !UUID.test(thread)) return false;
|
|
15177
|
+
const channelIds = await ownedChannelIds(forward, workspaces);
|
|
15178
|
+
return !!channelIds && await threadsOwned(forward, [thread], channelIds);
|
|
15179
|
+
}
|
|
15180
|
+
var forwardToSupabase = async (req) => {
|
|
15181
|
+
const base = process.env["SUPABASE_URL"];
|
|
15182
|
+
const key = process.env["SUPABASE_SECRET_KEY"];
|
|
15183
|
+
if (!base || !key) return deny("Data API has no Supabase credentials.", 500);
|
|
15184
|
+
const res = await fetch(`${base}${req.path}`, {
|
|
15185
|
+
method: req.method,
|
|
15186
|
+
headers: { ...req.headers, apikey: key, authorization: `Bearer ${key}` },
|
|
15187
|
+
body: req.body
|
|
15188
|
+
});
|
|
15189
|
+
return {
|
|
15190
|
+
status: res.status,
|
|
15191
|
+
headers: Object.fromEntries(
|
|
15192
|
+
["content-type", "content-range", "content-profile"].flatMap((h) => {
|
|
15193
|
+
const v = res.headers.get(h);
|
|
15194
|
+
return v ? [[h, v]] : [];
|
|
15195
|
+
})
|
|
15196
|
+
),
|
|
15197
|
+
body: await res.text()
|
|
15198
|
+
};
|
|
15199
|
+
};
|
|
15200
|
+
|
|
15201
|
+
// src/dashboard/host-db-client.ts
|
|
15202
|
+
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
15203
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
15204
|
+
var REQUEST_TIMEOUT_MS3 = 3e4;
|
|
15205
|
+
var pending = /* @__PURE__ */ new Map();
|
|
15206
|
+
var sender = null;
|
|
15207
|
+
var onSender = [];
|
|
15208
|
+
function setHostDbSender(fn) {
|
|
15209
|
+
sender = fn;
|
|
15210
|
+
if (!fn) return;
|
|
15211
|
+
const waiting = onSender;
|
|
15212
|
+
onSender = [];
|
|
15213
|
+
for (const wake of waiting) wake();
|
|
15214
|
+
}
|
|
15215
|
+
function handleHostDbResult(id, res) {
|
|
15216
|
+
const p = pending.get(id);
|
|
15217
|
+
if (!p) return;
|
|
15218
|
+
pending.delete(id);
|
|
15219
|
+
clearTimeout(p.timer);
|
|
15220
|
+
p.resolve(res);
|
|
15221
|
+
}
|
|
15222
|
+
function whenConnected() {
|
|
15223
|
+
if (sender) return Promise.resolve(sender);
|
|
15224
|
+
return new Promise((resolve8, reject) => {
|
|
15225
|
+
const timer2 = setTimeout(() => {
|
|
15226
|
+
onSender = onSender.filter((w) => w !== wake);
|
|
15227
|
+
reject(new Error("Host is not connected to the Data API"));
|
|
15228
|
+
}, REQUEST_TIMEOUT_MS3);
|
|
15229
|
+
const wake = () => {
|
|
15230
|
+
clearTimeout(timer2);
|
|
15231
|
+
resolve8(sender);
|
|
15232
|
+
};
|
|
15233
|
+
onSender.push(wake);
|
|
15234
|
+
});
|
|
15235
|
+
}
|
|
15236
|
+
async function request(req) {
|
|
15237
|
+
const send2 = await whenConnected();
|
|
15238
|
+
const id = randomUUID3();
|
|
15239
|
+
return new Promise((resolve8, reject) => {
|
|
15240
|
+
const timer2 = setTimeout(() => {
|
|
15241
|
+
pending.delete(id);
|
|
15242
|
+
reject(new Error("Data API did not answer in time"));
|
|
15243
|
+
}, REQUEST_TIMEOUT_MS3);
|
|
15244
|
+
pending.set(id, { resolve: resolve8, reject, timer: timer2 });
|
|
15245
|
+
send2({ type: "db", id, req });
|
|
15246
|
+
});
|
|
15247
|
+
}
|
|
15248
|
+
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
15249
|
+
var hostDbFetch = async (input2, init) => {
|
|
15250
|
+
const req = new Request(input2, init);
|
|
15251
|
+
const url = new URL(req.url);
|
|
15252
|
+
const method = req.method.toUpperCase();
|
|
15253
|
+
const res = await request({
|
|
15254
|
+
method,
|
|
15255
|
+
path: `${url.pathname}${url.search}`,
|
|
15256
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
15257
|
+
body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
|
|
15258
|
+
});
|
|
15259
|
+
const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
|
|
15260
|
+
return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
|
|
15261
|
+
};
|
|
15262
|
+
function relayChannel(topic) {
|
|
15263
|
+
const channel = {
|
|
15264
|
+
on: () => channel,
|
|
15265
|
+
subscribe: (callback) => {
|
|
15266
|
+
queueMicrotask(() => callback?.("SUBSCRIBED"));
|
|
15267
|
+
return channel;
|
|
15268
|
+
},
|
|
15269
|
+
send: async (msg) => {
|
|
15270
|
+
if (msg.type !== "broadcast" || !sender) return "error";
|
|
15271
|
+
sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
|
|
15272
|
+
return "ok";
|
|
15273
|
+
},
|
|
15274
|
+
unsubscribe: async () => "ok"
|
|
15275
|
+
};
|
|
15276
|
+
return channel;
|
|
15277
|
+
}
|
|
15278
|
+
function createHostDbClient() {
|
|
15279
|
+
const client2 = createClient2("https://host-db.invalid", "host-device", {
|
|
15280
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
15281
|
+
global: { fetch: hostDbFetch }
|
|
15282
|
+
});
|
|
15283
|
+
return new Proxy(client2, {
|
|
15284
|
+
get(target, prop) {
|
|
15285
|
+
if (prop === "channel") return relayChannel;
|
|
15286
|
+
if (prop === "removeChannel") return async () => "ok";
|
|
15287
|
+
const value = Reflect.get(target, prop, target);
|
|
15288
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
15289
|
+
}
|
|
15290
|
+
});
|
|
15291
|
+
}
|
|
15292
|
+
|
|
13993
15293
|
// src/dashboard/agent-bridge.ts
|
|
13994
15294
|
var HOST_SOCKET_PATH = "/internal/host-socket";
|
|
13995
15295
|
var DEFAULT_DATA_API_PORT = 4757;
|
|
@@ -14026,6 +15326,7 @@ function localAgentCallbacks(chatAgent) {
|
|
|
14026
15326
|
getLocalDashboardSettings: () => loadDashboardSettings(),
|
|
14027
15327
|
saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
|
|
14028
15328
|
writeSkill: (ref, write) => writeSkillAndSync(ref, write),
|
|
15329
|
+
outputStylesRequest: (req) => handleOutputStylesRequest(req),
|
|
14029
15330
|
listSessions: () => listLocalSessions(),
|
|
14030
15331
|
scanOnboarding: () => scanOnboarding(),
|
|
14031
15332
|
linkProjectFolders: (slug, items) => linkProjectFolders(slug, items),
|
|
@@ -14037,7 +15338,7 @@ function buildHostConfigSnapshot() {
|
|
|
14037
15338
|
const snapshot2 = {};
|
|
14038
15339
|
for (const profile of listProfiles()) {
|
|
14039
15340
|
snapshot2[profile] = runWithProfile(profile, () => ({
|
|
14040
|
-
env: existsSync26(paths.CONFIG_FILE) ? dotenv.parse(
|
|
15341
|
+
env: existsSync26(paths.CONFIG_FILE) ? dotenv.parse(readFileSync16(paths.CONFIG_FILE, "utf-8")) : {},
|
|
14041
15342
|
agentModel: loadDashboardSettings().agent
|
|
14042
15343
|
}));
|
|
14043
15344
|
}
|
|
@@ -14052,9 +15353,9 @@ var CALL_TIMEOUT_MS = 15e3;
|
|
|
14052
15353
|
var AUTH_TIMEOUT_MS = 5e3;
|
|
14053
15354
|
var HEARTBEAT_INTERVAL_MS = 15e3;
|
|
14054
15355
|
var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
|
|
14055
|
-
var
|
|
15356
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14056
15357
|
var hosts = /* @__PURE__ */ new Map();
|
|
14057
|
-
var
|
|
15358
|
+
var pending2 = /* @__PURE__ */ new Map();
|
|
14058
15359
|
var hostStatusListeners = /* @__PURE__ */ new Set();
|
|
14059
15360
|
var lastAuthFailure = null;
|
|
14060
15361
|
function hostFor(profile) {
|
|
@@ -14153,7 +15454,7 @@ function attachHostSocket(server) {
|
|
|
14153
15454
|
wss.on("connection", (ws) => {
|
|
14154
15455
|
alive.add(ws);
|
|
14155
15456
|
ws.on("pong", () => alive.add(ws));
|
|
14156
|
-
const nonce =
|
|
15457
|
+
const nonce = randomBytes8(32).toString("base64");
|
|
14157
15458
|
const authTimer = setTimeout(() => {
|
|
14158
15459
|
console.error("[host-socket] auth timed out");
|
|
14159
15460
|
ws.close(1008, "auth timeout");
|
|
@@ -14165,32 +15466,48 @@ function attachHostSocket(server) {
|
|
|
14165
15466
|
if (msg.type === "auth") {
|
|
14166
15467
|
clearTimeout(authTimer);
|
|
14167
15468
|
void (async () => {
|
|
14168
|
-
const
|
|
14169
|
-
if (!
|
|
15469
|
+
const scope2 = await verifyHostAuth(nonce, msg, legacyPublicKey);
|
|
15470
|
+
if (!scope2) {
|
|
14170
15471
|
console.error("[host-socket] rejecting connection: bad signature or unknown/revoked device");
|
|
14171
15472
|
lastAuthFailure = { reason: "bad signature", since: Date.now() };
|
|
14172
15473
|
send(ws, { type: "auth-failed", reason: "bad signature" });
|
|
14173
15474
|
ws.close(1008, "bad signature");
|
|
14174
15475
|
return;
|
|
14175
15476
|
}
|
|
14176
|
-
supersedeSameIdentity(
|
|
15477
|
+
supersedeSameIdentity(scope2, ws);
|
|
14177
15478
|
lastAuthFailure = null;
|
|
14178
|
-
addHost(ws,
|
|
15479
|
+
addHost(ws, scope2);
|
|
14179
15480
|
send(ws, { type: "auth-ok" });
|
|
14180
|
-
console.log(`[host-socket] Host authenticated (authPath: ${
|
|
14181
|
-
if (
|
|
15481
|
+
console.log(`[host-socket] Host authenticated (authPath: ${scope2.authPath})`);
|
|
15482
|
+
if (scope2.workspaces) void markAgentConnected(scope2.workspaces);
|
|
14182
15483
|
})();
|
|
14183
15484
|
return;
|
|
14184
15485
|
}
|
|
14185
15486
|
if (msg.type === "result" || msg.type === "call-error") {
|
|
14186
|
-
const waiter =
|
|
15487
|
+
const waiter = pending2.get(msg.id);
|
|
14187
15488
|
if (!waiter) return;
|
|
14188
|
-
|
|
15489
|
+
pending2.delete(msg.id);
|
|
14189
15490
|
waiter.resolve(msg.type === "result" ? msg.value : null);
|
|
14190
15491
|
return;
|
|
14191
15492
|
}
|
|
14192
15493
|
if (msg.type === "host-config") {
|
|
14193
15494
|
setProfileConfigCache(msg.configs);
|
|
15495
|
+
return;
|
|
15496
|
+
}
|
|
15497
|
+
const scope = hosts.get(ws);
|
|
15498
|
+
if (!scope) return;
|
|
15499
|
+
if (msg.type === "db") {
|
|
15500
|
+
const assigned = (scope.workspaces ?? []).filter((p) => hostFor(p) === ws);
|
|
15501
|
+
void proxyHostDb(scope.workspaces, msg.req, forwardToSupabase, assigned).then(
|
|
15502
|
+
(res) => send(ws, { type: "db-result", id: msg.id, res })
|
|
15503
|
+
);
|
|
15504
|
+
return;
|
|
15505
|
+
}
|
|
15506
|
+
if (msg.type === "db-broadcast") {
|
|
15507
|
+
void (async () => {
|
|
15508
|
+
if (!await hostMayBroadcast(scope.workspaces, msg.topic, forwardToSupabase)) return;
|
|
15509
|
+
await getSupabaseClient().channel(msg.topic).httpSend(msg.event, msg.payload).catch((err) => console.error(`[host-socket] broadcast ${msg.topic} failed:`, err.message));
|
|
15510
|
+
})();
|
|
14194
15511
|
}
|
|
14195
15512
|
});
|
|
14196
15513
|
ws.on("close", () => {
|
|
@@ -14210,14 +15527,14 @@ function callHost(method, args) {
|
|
|
14210
15527
|
);
|
|
14211
15528
|
return Promise.resolve(null);
|
|
14212
15529
|
}
|
|
14213
|
-
const id =
|
|
15530
|
+
const id = randomUUID4();
|
|
14214
15531
|
return new Promise((resolve8) => {
|
|
14215
15532
|
const timer2 = setTimeout(() => {
|
|
14216
|
-
|
|
15533
|
+
pending2.delete(id);
|
|
14217
15534
|
console.error(`[host-socket] ${method} -> timed out`);
|
|
14218
15535
|
resolve8(null);
|
|
14219
15536
|
}, CALL_TIMEOUT_MS);
|
|
14220
|
-
|
|
15537
|
+
pending2.set(id, {
|
|
14221
15538
|
resolve: (value) => {
|
|
14222
15539
|
clearTimeout(timer2);
|
|
14223
15540
|
resolve8(value);
|
|
@@ -14268,6 +15585,8 @@ function remoteAgentCallbacks() {
|
|
|
14268
15585
|
saveLocalDashboardSettings: (patch) => callHost("saveLocalDashboardSettings", [patch]),
|
|
14269
15586
|
// Same posture as saveLocalDashboardSettings: `null` becomes a 503.
|
|
14270
15587
|
writeSkill: (ref, write) => callHost("writeSkill", [ref, write]),
|
|
15588
|
+
// Same posture as writeSkill: `null` becomes a 503.
|
|
15589
|
+
outputStylesRequest: (req) => callHost("outputStylesRequest", [req]),
|
|
14271
15590
|
// `null` (no Host connected) is passed through rather than defaulting to
|
|
14272
15591
|
// `[]` — see listSessions's doc comment on AgentCallbacks.
|
|
14273
15592
|
listSessions: () => callHost("listSessions", []),
|
|
@@ -14281,7 +15600,7 @@ function remoteAgentCallbacks() {
|
|
|
14281
15600
|
}
|
|
14282
15601
|
var RECONNECT_BASE_MS = 1e3;
|
|
14283
15602
|
var RECONNECT_MAX_MS = 3e4;
|
|
14284
|
-
function connectHostSocket(url, callbacks) {
|
|
15603
|
+
function connectHostSocket(url, callbacks, onAuthenticated) {
|
|
14285
15604
|
const credentials = loadCredentials();
|
|
14286
15605
|
const deviceId = credentials?.deviceId;
|
|
14287
15606
|
const privateKey = deviceId ? ensureDeviceKey().privateKey : loadHostPrivateKey();
|
|
@@ -14322,11 +15641,17 @@ function connectHostSocket(url, callbacks) {
|
|
|
14322
15641
|
}
|
|
14323
15642
|
if (msg.type === "auth-ok") {
|
|
14324
15643
|
console.log("[host-socket] authenticated");
|
|
15644
|
+
setHostDbSender((out) => send(ws, out));
|
|
14325
15645
|
send(ws, { type: "host-config", configs: buildHostConfigSnapshot() });
|
|
14326
15646
|
if (configPushTimer) clearInterval(configPushTimer);
|
|
14327
15647
|
configPushTimer = setInterval(() => {
|
|
14328
15648
|
send(ws, { type: "host-config", configs: buildHostConfigSnapshot() });
|
|
14329
15649
|
}, HOST_CONFIG_PUSH_INTERVAL_MS);
|
|
15650
|
+
onAuthenticated?.();
|
|
15651
|
+
return;
|
|
15652
|
+
}
|
|
15653
|
+
if (msg.type === "db-result") {
|
|
15654
|
+
handleHostDbResult(msg.id, msg.res);
|
|
14330
15655
|
return;
|
|
14331
15656
|
}
|
|
14332
15657
|
if (msg.type === "call") {
|
|
@@ -14334,6 +15659,7 @@ function connectHostSocket(url, callbacks) {
|
|
|
14334
15659
|
}
|
|
14335
15660
|
});
|
|
14336
15661
|
ws.on("close", () => {
|
|
15662
|
+
setHostDbSender(null);
|
|
14337
15663
|
if (configPushTimer) {
|
|
14338
15664
|
clearInterval(configPushTimer);
|
|
14339
15665
|
configPushTimer = null;
|
|
@@ -14375,6 +15701,56 @@ async function handleCall(msg, callbacks, ws) {
|
|
|
14375
15701
|
}
|
|
14376
15702
|
});
|
|
14377
15703
|
}
|
|
15704
|
+
var LOCAL_RELAY_PATH = "/internal/agent-call";
|
|
15705
|
+
var RELAYED_METHODS = [
|
|
15706
|
+
"onChatMessage",
|
|
15707
|
+
"isAgentThinking",
|
|
15708
|
+
"getAgentActivities",
|
|
15709
|
+
"cancelAgentRun",
|
|
15710
|
+
"regenerateAgentRun",
|
|
15711
|
+
"listAgentRuns"
|
|
15712
|
+
];
|
|
15713
|
+
function relayedAgentCallbacks(local) {
|
|
15714
|
+
const relay2 = (method) => async (...args) => {
|
|
15715
|
+
const endpoint = readHostEndpoint();
|
|
15716
|
+
if (endpoint) {
|
|
15717
|
+
try {
|
|
15718
|
+
const res = await fetch(`http://127.0.0.1:${endpoint.port}${LOCAL_RELAY_PATH}`, {
|
|
15719
|
+
method: "POST",
|
|
15720
|
+
headers: { authorization: `Bearer ${endpoint.token}`, "content-type": "application/json" },
|
|
15721
|
+
body: JSON.stringify({ method, profile: getProfile(), args }),
|
|
15722
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
|
|
15723
|
+
});
|
|
15724
|
+
if (res.ok) return (await res.json()).value;
|
|
15725
|
+
console.error(`[agent-relay] ${method} -> Host answered ${res.status}, running locally`);
|
|
15726
|
+
} catch (err) {
|
|
15727
|
+
console.error(`[agent-relay] ${method} -> ${err.message}, running locally`);
|
|
15728
|
+
}
|
|
15729
|
+
}
|
|
15730
|
+
return local[method]?.(...args);
|
|
15731
|
+
};
|
|
15732
|
+
return {
|
|
15733
|
+
...local,
|
|
15734
|
+
...Object.fromEntries(RELAYED_METHODS.map((method) => [method, relay2(method)]))
|
|
15735
|
+
};
|
|
15736
|
+
}
|
|
15737
|
+
async function handleLocalRelay(req, callbacks, token) {
|
|
15738
|
+
const given = Buffer.from(req.headers.get("authorization") ?? "");
|
|
15739
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
15740
|
+
if (!token || given.length !== expected.length || !timingSafeEqual2(given, expected)) {
|
|
15741
|
+
return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
15742
|
+
}
|
|
15743
|
+
const body = await req.json().catch(() => null);
|
|
15744
|
+
const method = RELAYED_METHODS.find((m) => m === body?.method);
|
|
15745
|
+
if (!body || !method || typeof body.profile !== "string" || !Array.isArray(body.args)) {
|
|
15746
|
+
return Response.json({ error: "bad request" }, { status: 400 });
|
|
15747
|
+
}
|
|
15748
|
+
const value = await runWithProfile(
|
|
15749
|
+
body.profile,
|
|
15750
|
+
async () => callbacks[method]?.(...body.args)
|
|
15751
|
+
);
|
|
15752
|
+
return Response.json({ value: value ?? null });
|
|
15753
|
+
}
|
|
14378
15754
|
function send(ws, msg) {
|
|
14379
15755
|
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
14380
15756
|
}
|
|
@@ -14387,7 +15763,7 @@ function parse(raw) {
|
|
|
14387
15763
|
}
|
|
14388
15764
|
async function verifyHostAuth(nonce, msg, legacyPublicKey) {
|
|
14389
15765
|
if (msg.deviceId) {
|
|
14390
|
-
if (!
|
|
15766
|
+
if (!UUID2.test(msg.deviceId)) return null;
|
|
14391
15767
|
const device = await getDevice(msg.deviceId);
|
|
14392
15768
|
if (!device || device.revokedAt) return null;
|
|
14393
15769
|
const key = parseEd25519PublicKey(device.publicKey);
|
|
@@ -14411,7 +15787,7 @@ function verifySignature(nonce, signatureBase64, publicKey) {
|
|
|
14411
15787
|
}
|
|
14412
15788
|
|
|
14413
15789
|
// src/dashboard/device-auth-routes.ts
|
|
14414
|
-
var
|
|
15790
|
+
var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14415
15791
|
function cleanText(value, max) {
|
|
14416
15792
|
if (typeof value !== "string") return null;
|
|
14417
15793
|
const trimmed = value.trim();
|
|
@@ -14440,7 +15816,7 @@ async function readBody(c) {
|
|
|
14440
15816
|
return {};
|
|
14441
15817
|
}
|
|
14442
15818
|
}
|
|
14443
|
-
async function
|
|
15819
|
+
async function sessionUserId2(c) {
|
|
14444
15820
|
try {
|
|
14445
15821
|
const { data } = await createRequestSupabaseClient(c).auth.getUser();
|
|
14446
15822
|
return data.user?.id ?? null;
|
|
@@ -14453,8 +15829,12 @@ async function bearerDevice(c) {
|
|
|
14453
15829
|
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length).trim() : "";
|
|
14454
15830
|
return token ? deviceForAccessToken(token) : null;
|
|
14455
15831
|
}
|
|
15832
|
+
async function boundWorkspaceIds(publicKey, userId) {
|
|
15833
|
+
const existing = await findDeviceByKey(publicKey, userId);
|
|
15834
|
+
return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
|
|
15835
|
+
}
|
|
14456
15836
|
function createDeviceAuthRouter() {
|
|
14457
|
-
const app = new
|
|
15837
|
+
const app = new Hono28();
|
|
14458
15838
|
app.post("/code", async (c) => {
|
|
14459
15839
|
const body = await readBody(c);
|
|
14460
15840
|
const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
|
|
@@ -14478,18 +15858,18 @@ function createDeviceAuthRouter() {
|
|
|
14478
15858
|
app.post("/token", async (c) => {
|
|
14479
15859
|
const body = await readBody(c);
|
|
14480
15860
|
const code = typeof body["device_code"] === "string" ? body["device_code"] : "";
|
|
14481
|
-
const
|
|
14482
|
-
if (!
|
|
15861
|
+
const pending3 = code ? await getDeviceCodeBySecret(code) : null;
|
|
15862
|
+
if (!pending3 || Date.parse(pending3.expiresAt) <= Date.now()) {
|
|
14483
15863
|
return c.json({ error: "expired_token" }, 400);
|
|
14484
15864
|
}
|
|
14485
|
-
if (
|
|
14486
|
-
if (!
|
|
14487
|
-
const last =
|
|
14488
|
-
await markPolled(
|
|
15865
|
+
if (pending3.deniedAt) return c.json({ error: "access_denied" }, 400);
|
|
15866
|
+
if (!pending3.approvedBy) {
|
|
15867
|
+
const last = pending3.lastPolledAt ? Date.parse(pending3.lastPolledAt) : 0;
|
|
15868
|
+
await markPolled(pending3.id);
|
|
14489
15869
|
const tooSoon = Date.now() - last < (POLL_INTERVAL_S - 1) * 1e3;
|
|
14490
15870
|
return c.json({ error: tooSoon ? "slow_down" : "authorization_pending" }, 400);
|
|
14491
15871
|
}
|
|
14492
|
-
const device = await redeemDeviceCode(
|
|
15872
|
+
const device = await redeemDeviceCode(pending3);
|
|
14493
15873
|
if (!device) return c.json({ error: "expired_token" }, 400);
|
|
14494
15874
|
const { token, expiresInS } = await issueAccessToken(device.id);
|
|
14495
15875
|
return c.json({
|
|
@@ -14503,14 +15883,14 @@ function createDeviceAuthRouter() {
|
|
|
14503
15883
|
});
|
|
14504
15884
|
app.post("/challenge", async (c) => {
|
|
14505
15885
|
const body = await readBody(c);
|
|
14506
|
-
const deviceId = typeof body["device_id"] === "string" &&
|
|
15886
|
+
const deviceId = typeof body["device_id"] === "string" && UUID3.test(body["device_id"]) ? body["device_id"] : "";
|
|
14507
15887
|
const device = deviceId ? await getDevice(deviceId) : null;
|
|
14508
15888
|
if (!device || device.revokedAt) return c.json({ error: "unknown_device" }, 401);
|
|
14509
15889
|
return c.json({ nonce: await createChallenge(device.id) });
|
|
14510
15890
|
});
|
|
14511
15891
|
app.post("/refresh", async (c) => {
|
|
14512
15892
|
const body = await readBody(c);
|
|
14513
|
-
const deviceId = typeof body["device_id"] === "string" &&
|
|
15893
|
+
const deviceId = typeof body["device_id"] === "string" && UUID3.test(body["device_id"]) ? body["device_id"] : "";
|
|
14514
15894
|
const nonce = typeof body["nonce"] === "string" ? body["nonce"] : "";
|
|
14515
15895
|
const signature = typeof body["signature"] === "string" ? body["signature"] : "";
|
|
14516
15896
|
const device = deviceId ? await getDevice(deviceId) : null;
|
|
@@ -14539,24 +15919,32 @@ function createDeviceAuthRouter() {
|
|
|
14539
15919
|
return c.json({ ok: true });
|
|
14540
15920
|
});
|
|
14541
15921
|
app.get("/lookup", async (c) => {
|
|
14542
|
-
const userId = await
|
|
15922
|
+
const userId = await sessionUserId2(c);
|
|
14543
15923
|
if (!userId) return c.json({ error: "not signed in" }, 401);
|
|
14544
15924
|
const userCode = normalizeUserCode(c.req.query("user_code") ?? "");
|
|
14545
|
-
const
|
|
14546
|
-
if (!
|
|
15925
|
+
const pending3 = userCode ? await getDeviceCodeByUserCode(userCode) : null;
|
|
15926
|
+
if (!pending3 || Date.parse(pending3.expiresAt) <= Date.now() || pending3.approvedBy || pending3.deniedAt) {
|
|
14547
15927
|
return c.json({ error: "That code is invalid, expired, or already used." }, 404);
|
|
14548
15928
|
}
|
|
15929
|
+
const workspaces = await listAdminWorkspaces(userId);
|
|
15930
|
+
const hosts2 = await listWorkspaceHosts(
|
|
15931
|
+
workspaces.map((w) => w.id),
|
|
15932
|
+
pending3.publicKey
|
|
15933
|
+
);
|
|
15934
|
+
const bound = await boundWorkspaceIds(pending3.publicKey, userId);
|
|
14549
15935
|
return c.json({
|
|
14550
|
-
user_code:
|
|
14551
|
-
name:
|
|
14552
|
-
platform:
|
|
14553
|
-
ip:
|
|
14554
|
-
created_at:
|
|
14555
|
-
|
|
15936
|
+
user_code: pending3.userCode,
|
|
15937
|
+
name: pending3.name,
|
|
15938
|
+
platform: pending3.platform,
|
|
15939
|
+
ip: pending3.requesterIp,
|
|
15940
|
+
created_at: pending3.createdAt,
|
|
15941
|
+
// `host`: another machine this workspace already runs on, so it can't be
|
|
15942
|
+
// picked. `bound`: this machine already serves it, so there's nothing to pick.
|
|
15943
|
+
workspaces: workspaces.map((w) => ({ ...w, host: hosts2.get(w.id) ?? null, bound: bound.has(w.id) }))
|
|
14556
15944
|
});
|
|
14557
15945
|
});
|
|
14558
15946
|
app.get("/workspace", async (c) => {
|
|
14559
|
-
const userId = await
|
|
15947
|
+
const userId = await sessionUserId2(c);
|
|
14560
15948
|
if (!userId) return c.json({ error: "not signed in" }, 401);
|
|
14561
15949
|
const slug = getProfile();
|
|
14562
15950
|
const workspace = (await listAdminWorkspaces(userId)).find((w) => w.slug === slug);
|
|
@@ -14569,28 +15957,33 @@ function createDeviceAuthRouter() {
|
|
|
14569
15957
|
});
|
|
14570
15958
|
});
|
|
14571
15959
|
app.post("/approve", async (c) => {
|
|
14572
|
-
const userId = await
|
|
15960
|
+
const userId = await sessionUserId2(c);
|
|
14573
15961
|
if (!userId) return c.json({ error: "not signed in" }, 401);
|
|
14574
15962
|
const body = await readBody(c);
|
|
14575
15963
|
const userCode = normalizeUserCode(typeof body["user_code"] === "string" ? body["user_code"] : "");
|
|
14576
15964
|
const decision = body["decision"];
|
|
14577
15965
|
if (!userCode || decision !== "approve" && decision !== "deny") return c.json({ error: "invalid request" }, 400);
|
|
15966
|
+
const pending3 = await getDeviceCodeByUserCode(userCode);
|
|
15967
|
+
if (!pending3 || Date.parse(pending3.expiresAt) <= Date.now()) {
|
|
15968
|
+
return c.json({ error: "That code is invalid or expired." }, 404);
|
|
15969
|
+
}
|
|
14578
15970
|
let workspaceIds = [];
|
|
14579
15971
|
if (decision === "approve") {
|
|
14580
15972
|
const requested = Array.isArray(body["workspace_ids"]) ? body["workspace_ids"] : [];
|
|
14581
15973
|
const allowed = new Set((await listAdminWorkspaces(userId)).map((w) => w.id));
|
|
14582
15974
|
workspaceIds = [...new Set(requested.filter((id) => typeof id === "string"))];
|
|
14583
|
-
if (!workspaceIds.length) return c.json({ error: "Pick at least one workspace." }, 400);
|
|
14584
15975
|
if (workspaceIds.some((id) => !allowed.has(id))) {
|
|
14585
15976
|
return c.json({ error: "You can only add a device to workspaces you are an admin of." }, 403);
|
|
14586
15977
|
}
|
|
15978
|
+
if (!workspaceIds.length && !(await boundWorkspaceIds(pending3.publicKey, userId)).size) {
|
|
15979
|
+
return c.json({ error: "Pick at least one workspace." }, 400);
|
|
15980
|
+
}
|
|
14587
15981
|
}
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
return c.json({ error: "That code is invalid or expired." }, 404);
|
|
15982
|
+
if ((await listWorkspaceHosts(workspaceIds, pending3.publicKey)).size) {
|
|
15983
|
+
return c.json({ error: "That workspace already has a Host. Its Host cannot be replaced." }, 409);
|
|
14591
15984
|
}
|
|
14592
15985
|
const decided = await decideDeviceCode(
|
|
14593
|
-
|
|
15986
|
+
pending3.id,
|
|
14594
15987
|
userId,
|
|
14595
15988
|
decision === "approve" ? { approve: true, workspaceIds } : { approve: false }
|
|
14596
15989
|
);
|
|
@@ -14604,7 +15997,7 @@ function createDeviceAuthRouter() {
|
|
|
14604
15997
|
|
|
14605
15998
|
// src/dashboard/api.ts
|
|
14606
15999
|
function createApiRouter(agent = {}, mode = "all") {
|
|
14607
|
-
const api = new
|
|
16000
|
+
const api = new Hono29();
|
|
14608
16001
|
api.use("*", async (c, next) => {
|
|
14609
16002
|
const known = await listKnownWorkspaces();
|
|
14610
16003
|
const fromHost = profileFromHost(trustedHost(c), known);
|
|
@@ -14649,13 +16042,17 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
14649
16042
|
if (mode !== "host") registerDataRoutes(api, agent);
|
|
14650
16043
|
if (mode !== "data") registerHostRoutes(api);
|
|
14651
16044
|
if (mode === "data") {
|
|
14652
|
-
api.get(
|
|
16045
|
+
api.get(
|
|
16046
|
+
"/settings/agents",
|
|
16047
|
+
async (c) => c.json({ agents: await agent.getAgentIdentities?.() ?? [] })
|
|
16048
|
+
);
|
|
14653
16049
|
api.get("/channels/:id/skills", async (c) => {
|
|
14654
16050
|
const { cwd, claudeConfigDir } = resolveChannelConfig(c.req.param("id"));
|
|
14655
16051
|
return c.json({ skills: await agent.listChannelSkills?.({ cwd, claudeConfigDir }) ?? [] });
|
|
14656
16052
|
});
|
|
14657
16053
|
registerMirroredSkillRoutes(api, agent);
|
|
14658
|
-
api
|
|
16054
|
+
registerRelayedOutputStylesRoutes(api, agent);
|
|
16055
|
+
api.get("/channels/:id/docs/flat", requireDocsAccess, async (c) => {
|
|
14659
16056
|
const { cwd } = resolveChannelConfig(c.req.param("id"));
|
|
14660
16057
|
const query = c.req.query("q") ?? "";
|
|
14661
16058
|
const sort = c.req.query("sort") === "oldest" ? "oldest" : "newest";
|
|
@@ -14668,7 +16065,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
14668
16065
|
loadWorkspaceBranding(),
|
|
14669
16066
|
Promise.resolve(agent.getLocalDashboardSettings?.()).then((v) => v ?? localDefaults())
|
|
14670
16067
|
]);
|
|
14671
|
-
return c.json({ ...branding, ...local });
|
|
16068
|
+
return c.json({ ...branding, ...local, autoTitleDefaultPrompt: DEFAULT_TITLE_RULES });
|
|
14672
16069
|
});
|
|
14673
16070
|
api.put("/settings", async (c) => {
|
|
14674
16071
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -14711,7 +16108,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
14711
16108
|
}
|
|
14712
16109
|
|
|
14713
16110
|
// src/dashboard/app-icon-routes.ts
|
|
14714
|
-
import { Hono as
|
|
16111
|
+
import { Hono as Hono30 } from "hono";
|
|
14715
16112
|
var NEUTRAL_MANIFEST = {
|
|
14716
16113
|
id: "/",
|
|
14717
16114
|
name: "Flyspace",
|
|
@@ -14749,7 +16146,7 @@ async function withResolvedProfile2(c, next) {
|
|
|
14749
16146
|
return profile ? runWithProfile(profile, () => next()) : next();
|
|
14750
16147
|
}
|
|
14751
16148
|
function createAppIconRouter() {
|
|
14752
|
-
const app = new
|
|
16149
|
+
const app = new Hono30();
|
|
14753
16150
|
app.get("/manifest.json", withResolvedProfile2, async (c) => {
|
|
14754
16151
|
if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
|
|
14755
16152
|
const profile = getProfile();
|
|
@@ -14789,7 +16186,7 @@ function pngResponse(c, png) {
|
|
|
14789
16186
|
}
|
|
14790
16187
|
|
|
14791
16188
|
// src/dashboard/stripe-routes.ts
|
|
14792
|
-
import { Hono as
|
|
16189
|
+
import { Hono as Hono31 } from "hono";
|
|
14793
16190
|
function planStatusFor(status) {
|
|
14794
16191
|
const known = ["trialing", "active", "paused", "canceled"];
|
|
14795
16192
|
return known.includes(status) ? status : null;
|
|
@@ -14832,7 +16229,7 @@ async function handleCardAdded(session) {
|
|
|
14832
16229
|
}
|
|
14833
16230
|
}
|
|
14834
16231
|
function createStripeWebhookRouter() {
|
|
14835
|
-
const stripeRouter = new
|
|
16232
|
+
const stripeRouter = new Hono31();
|
|
14836
16233
|
stripeRouter.post("/webhook", async (c) => {
|
|
14837
16234
|
const secret = process.env["STRIPE_WEBHOOK_SECRET"];
|
|
14838
16235
|
const signature = c.req.header("stripe-signature");
|
|
@@ -14943,7 +16340,7 @@ function startHostStatusBroadcast() {
|
|
|
14943
16340
|
|
|
14944
16341
|
// src/dashboard/server.ts
|
|
14945
16342
|
var DEFAULT_DASHBOARD_PORT = 4747;
|
|
14946
|
-
var WEB_DIST =
|
|
16343
|
+
var WEB_DIST = join21(paths.PACKAGE_ROOT, "web", "dist");
|
|
14947
16344
|
var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
|
|
14948
16345
|
async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
|
|
14949
16346
|
for (let port = start; port < start + attempts; port++) {
|
|
@@ -14960,6 +16357,12 @@ async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50)
|
|
|
14960
16357
|
async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host = DEFAULT_DASHBOARD_HOST, mode = "all") {
|
|
14961
16358
|
const servesHost = mode !== "data";
|
|
14962
16359
|
loadEnvFiles();
|
|
16360
|
+
let lateCallbacks;
|
|
16361
|
+
let onHostConnected;
|
|
16362
|
+
const bootCallbacks = new Proxy({}, {
|
|
16363
|
+
get: (_t, prop) => lateCallbacks?.[prop]
|
|
16364
|
+
});
|
|
16365
|
+
const earlyHostSocket = mode === "host" && !hasSupabaseCredentials() && loadCredentials() ? (setSupabaseClient(createHostDbClient()), connectHostSocket(dataSocketUrl(), bootCallbacks, () => onHostConnected?.())) : void 0;
|
|
14963
16366
|
if (servesHost) {
|
|
14964
16367
|
await reconcileAll().catch(
|
|
14965
16368
|
(err) => console.error("[dashboard] reconcileAll failed:", err.message)
|
|
@@ -14973,11 +16376,18 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
14973
16376
|
await refreshAutomationRegistry();
|
|
14974
16377
|
const stopAutomationPolling = startAutomationRegistryPolling();
|
|
14975
16378
|
const chatAgent = servesHost ? new ChatAgent(loadChatAgentConfig()) : void 0;
|
|
14976
|
-
const
|
|
16379
|
+
const local = chatAgent ? localAgentCallbacks(chatAgent) : void 0;
|
|
16380
|
+
const agent = !local ? remoteAgentCallbacks() : mode === "all" ? relayedAgentCallbacks(local) : local;
|
|
16381
|
+
if (chatAgent) lateCallbacks = agent;
|
|
16382
|
+
if (chatAgent && mode === "host") {
|
|
16383
|
+
onHostConnected = () => void chatAgent.recoverMissed().catch((err) => console.error("[chat-agent] recoverMissed failed:", err.message));
|
|
16384
|
+
if (earlyHostSocket) onHostConnected();
|
|
16385
|
+
}
|
|
14977
16386
|
if (servesHost) installUsageRecording();
|
|
14978
|
-
const
|
|
16387
|
+
const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
|
|
16388
|
+
const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
|
|
14979
16389
|
};
|
|
14980
|
-
const app = new
|
|
16390
|
+
const app = new Hono32();
|
|
14981
16391
|
app.use("*", async (c, next) => {
|
|
14982
16392
|
await next();
|
|
14983
16393
|
if (c.req.path.startsWith("/assets/")) {
|
|
@@ -14989,6 +16399,10 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
14989
16399
|
app.use("*", corsForShell);
|
|
14990
16400
|
app.route("/api", createApiRouter(agent, mode));
|
|
14991
16401
|
app.route("/auth", createAuthConfirmRouter());
|
|
16402
|
+
let relayToken = null;
|
|
16403
|
+
if (mode === "host") {
|
|
16404
|
+
app.post(LOCAL_RELAY_PATH, (c) => handleLocalRelay(c.req.raw, agent, relayToken));
|
|
16405
|
+
}
|
|
14992
16406
|
app.route("/stripe", createStripeWebhookRouter());
|
|
14993
16407
|
if (!servesHost) {
|
|
14994
16408
|
app.all(
|
|
@@ -15000,13 +16414,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
15000
16414
|
app.route("/", createAppIconRouter());
|
|
15001
16415
|
app.use("/*", serveStatic({ root: WEB_DIST }));
|
|
15002
16416
|
app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
|
|
15003
|
-
const spaFallback = serveStatic({ path:
|
|
16417
|
+
const spaFallback = serveStatic({ path: join21(WEB_DIST, "index.html") });
|
|
15004
16418
|
app.get("*", (c, next) => {
|
|
15005
|
-
if (!existsSync27(
|
|
16419
|
+
if (!existsSync27(join21(WEB_DIST, "index.html"))) {
|
|
16420
|
+
const isCheckout = existsSync27(join21(paths.PACKAGE_ROOT, "web", "package.json"));
|
|
15006
16421
|
return c.text(
|
|
15007
|
-
`
|
|
16422
|
+
isCheckout ? `Workspace UI build missing (${WEB_DIST}).
|
|
15008
16423
|
Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
|
|
15009
|
-
The JSON API is live under /api
|
|
16424
|
+
The JSON API is live under /api.` : "This install has no bundled Workspace UI.\nOpen the hosted Workspace at https://flyspace.ai instead.\nThe JSON API is live under /api.",
|
|
15010
16425
|
503
|
|
15011
16426
|
);
|
|
15012
16427
|
}
|
|
@@ -15015,10 +16430,12 @@ The JSON API is live under /api.`,
|
|
|
15015
16430
|
}
|
|
15016
16431
|
const port = await findFreePort(preferredPort, host);
|
|
15017
16432
|
const server = serve({ fetch: app.fetch, port, hostname: host });
|
|
16433
|
+
if (mode === "host") relayToken = publishHostEndpoint(port);
|
|
15018
16434
|
if (mode === "data") attachHostSocket(server);
|
|
15019
16435
|
const stopHostStatusBroadcast = mode === "data" ? startHostStatusBroadcast() : void 0;
|
|
15020
|
-
const hostSocketClient = mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent)) : void 0;
|
|
15021
|
-
const stopPushWatcher =
|
|
16436
|
+
const hostSocketClient = earlyHostSocket ?? (mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent), () => onHostConnected?.()) : void 0);
|
|
16437
|
+
const stopPushWatcher = earlyHostSocket ? () => {
|
|
16438
|
+
} : startPushWatcher();
|
|
15022
16439
|
const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
15023
16440
|
return {
|
|
15024
16441
|
port,
|
|
@@ -15058,7 +16475,7 @@ async function uploadImage(client2, { channel, filePath, threadTs, text }) {
|
|
|
15058
16475
|
|
|
15059
16476
|
// src/commands/slack-sync.ts
|
|
15060
16477
|
import { WebClient as WebClient2 } from "@slack/web-api";
|
|
15061
|
-
function
|
|
16478
|
+
function createClient3() {
|
|
15062
16479
|
loadEnvFiles();
|
|
15063
16480
|
const config = loadConfig();
|
|
15064
16481
|
return new WebClient2(requireSlackBotToken(config));
|
|
@@ -15188,7 +16605,7 @@ function parseSince(args) {
|
|
|
15188
16605
|
}
|
|
15189
16606
|
async function runSlackSync(args) {
|
|
15190
16607
|
const oldest = parseSince(args);
|
|
15191
|
-
const client2 =
|
|
16608
|
+
const client2 = createClient3();
|
|
15192
16609
|
const auth = await client2.auth.test();
|
|
15193
16610
|
const own = { userId: auth.user_id, botId: auth.bot_id };
|
|
15194
16611
|
try {
|
|
@@ -15264,7 +16681,7 @@ function getArg2(args, flag) {
|
|
|
15264
16681
|
const idx = args.indexOf(flag);
|
|
15265
16682
|
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
15266
16683
|
}
|
|
15267
|
-
function
|
|
16684
|
+
function createClient4() {
|
|
15268
16685
|
loadEnvFiles();
|
|
15269
16686
|
const config = loadConfig();
|
|
15270
16687
|
return new WebClient3(requireSlackBotToken(config));
|
|
@@ -15420,7 +16837,7 @@ async function threadReplies(client2, nameOrId, ts2) {
|
|
|
15420
16837
|
async function postMessage(client2, args) {
|
|
15421
16838
|
const channelArg = getArg2(args, "--channel");
|
|
15422
16839
|
const textFilePath = getArg2(args, "--text-file");
|
|
15423
|
-
const text = textFilePath ?
|
|
16840
|
+
const text = textFilePath ? readFileSync17(textFilePath, "utf8") : getArg2(args, "--text");
|
|
15424
16841
|
const threadTs = getArg2(args, "--thread");
|
|
15425
16842
|
const markdown = args.includes("--markdown");
|
|
15426
16843
|
const broadcast = args.includes("--broadcast") && Boolean(threadTs);
|
|
@@ -15510,14 +16927,14 @@ function mask(token) {
|
|
|
15510
16927
|
}
|
|
15511
16928
|
function hasClaudeBinary() {
|
|
15512
16929
|
try {
|
|
15513
|
-
|
|
16930
|
+
execFileSync4("claude", ["--version"], { stdio: "ignore" });
|
|
15514
16931
|
return true;
|
|
15515
16932
|
} catch {
|
|
15516
16933
|
return false;
|
|
15517
16934
|
}
|
|
15518
16935
|
}
|
|
15519
16936
|
function defaultClaudeConfigDir() {
|
|
15520
|
-
return process.env.CLAUDE_CONFIG_DIR || resolve7(
|
|
16937
|
+
return process.env.CLAUDE_CONFIG_DIR || resolve7(homedir9(), ".claude");
|
|
15521
16938
|
}
|
|
15522
16939
|
function hasClaudeCredentials(configDir2) {
|
|
15523
16940
|
if (existsSync28(resolve7(configDir2, ".credentials.json"))) {
|
|
@@ -15525,7 +16942,7 @@ function hasClaudeCredentials(configDir2) {
|
|
|
15525
16942
|
}
|
|
15526
16943
|
if (process.platform === "darwin") {
|
|
15527
16944
|
try {
|
|
15528
|
-
|
|
16945
|
+
execFileSync4(
|
|
15529
16946
|
"security",
|
|
15530
16947
|
["find-generic-password", "-s", "Claude Code-credentials"],
|
|
15531
16948
|
{ stdio: "ignore" }
|
|
@@ -15557,7 +16974,7 @@ async function ensureClaudeCodeReady() {
|
|
|
15557
16974
|
console.log(" Flyspace spawns Claude Code as a subprocess. Without credentials, the bot");
|
|
15558
16975
|
console.log(" will prompt /login inside Slack at runtime and you cannot complete the");
|
|
15559
16976
|
console.log(" browser sign-in from there.\n");
|
|
15560
|
-
if (claudeConfigDir !== resolve7(
|
|
16977
|
+
if (claudeConfigDir !== resolve7(homedir9(), ".claude")) {
|
|
15561
16978
|
console.log(" Tip: run with the same CLAUDE_CONFIG_DIR you use for this Claude Code install:");
|
|
15562
16979
|
console.log(` \x1B[1mCLAUDE_CONFIG_DIR=${claudeConfigDir} claude\x1B[0m
|
|
15563
16980
|
`);
|
|
@@ -15577,7 +16994,7 @@ async function ensureClaudeCodeReady() {
|
|
|
15577
16994
|
}
|
|
15578
16995
|
function getManifestYaml(botName) {
|
|
15579
16996
|
const manifestPath = resolve7(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
|
|
15580
|
-
const raw =
|
|
16997
|
+
const raw = readFileSync17(manifestPath, "utf-8");
|
|
15581
16998
|
return raw.replace(/name: Custie/g, `name: ${botName}`);
|
|
15582
16999
|
}
|
|
15583
17000
|
async function stepPersonalise() {
|
|
@@ -15994,7 +17411,7 @@ function writeConfigFile(config) {
|
|
|
15994
17411
|
`OWNER_USER_ID=${config.ownerUserId}`,
|
|
15995
17412
|
``
|
|
15996
17413
|
].join("\n");
|
|
15997
|
-
|
|
17414
|
+
writeFileSync10(paths.CONFIG_FILE, envContent);
|
|
15998
17415
|
success4(`Config written to ${paths.CONFIG_FILE}`);
|
|
15999
17416
|
}
|
|
16000
17417
|
function printNextSteps() {
|
|
@@ -16011,8 +17428,8 @@ function copyDefaultPrompt(botName) {
|
|
|
16011
17428
|
if (!existsSync28(paths.PROMPT_FILE)) {
|
|
16012
17429
|
const defaultPrompt = resolve7(paths.PACKAGE_ROOT, "system.default.md");
|
|
16013
17430
|
if (existsSync28(defaultPrompt)) {
|
|
16014
|
-
const content =
|
|
16015
|
-
|
|
17431
|
+
const content = readFileSync17(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
|
|
17432
|
+
writeFileSync10(paths.PROMPT_FILE, content);
|
|
16016
17433
|
success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
|
|
16017
17434
|
}
|
|
16018
17435
|
}
|
|
@@ -16099,12 +17516,12 @@ async function runSlackCmd(args) {
|
|
|
16099
17516
|
break;
|
|
16100
17517
|
}
|
|
16101
17518
|
case "channels": {
|
|
16102
|
-
const client2 =
|
|
17519
|
+
const client2 = createClient4();
|
|
16103
17520
|
await listChannels2(client2, args.includes("--refresh"));
|
|
16104
17521
|
break;
|
|
16105
17522
|
}
|
|
16106
17523
|
case "users": {
|
|
16107
|
-
const client2 =
|
|
17524
|
+
const client2 = createClient4();
|
|
16108
17525
|
await listUsers(client2, args.includes("--refresh"));
|
|
16109
17526
|
break;
|
|
16110
17527
|
}
|
|
@@ -16113,7 +17530,7 @@ async function runSlackCmd(args) {
|
|
|
16113
17530
|
console.error("Usage: flyspace slack channel-info <name-or-id>");
|
|
16114
17531
|
process.exit(1);
|
|
16115
17532
|
}
|
|
16116
|
-
const client2 =
|
|
17533
|
+
const client2 = createClient4();
|
|
16117
17534
|
await channelInfo(client2, args[1]);
|
|
16118
17535
|
break;
|
|
16119
17536
|
}
|
|
@@ -16122,7 +17539,7 @@ async function runSlackCmd(args) {
|
|
|
16122
17539
|
console.error("Usage: flyspace slack user-info <name-or-id>");
|
|
16123
17540
|
process.exit(1);
|
|
16124
17541
|
}
|
|
16125
|
-
const client2 =
|
|
17542
|
+
const client2 = createClient4();
|
|
16126
17543
|
await userInfo(client2, args[1]);
|
|
16127
17544
|
break;
|
|
16128
17545
|
}
|
|
@@ -16131,7 +17548,7 @@ async function runSlackCmd(args) {
|
|
|
16131
17548
|
console.error("Usage: flyspace slack history <name-or-id> [--today] [--limit n]");
|
|
16132
17549
|
process.exit(1);
|
|
16133
17550
|
}
|
|
16134
|
-
const client2 =
|
|
17551
|
+
const client2 = createClient4();
|
|
16135
17552
|
await channelHistory(client2, args[1], args.slice(2));
|
|
16136
17553
|
break;
|
|
16137
17554
|
}
|
|
@@ -16140,12 +17557,12 @@ async function runSlackCmd(args) {
|
|
|
16140
17557
|
console.error("Usage: flyspace slack thread <name-or-id> <ts>");
|
|
16141
17558
|
process.exit(1);
|
|
16142
17559
|
}
|
|
16143
|
-
const client2 =
|
|
17560
|
+
const client2 = createClient4();
|
|
16144
17561
|
await threadReplies(client2, args[1], args[2]);
|
|
16145
17562
|
break;
|
|
16146
17563
|
}
|
|
16147
17564
|
case "post": {
|
|
16148
|
-
const client2 =
|
|
17565
|
+
const client2 = createClient4();
|
|
16149
17566
|
await postMessage(client2, args.slice(1));
|
|
16150
17567
|
break;
|
|
16151
17568
|
}
|
|
@@ -16156,14 +17573,14 @@ async function runSlackCmd(args) {
|
|
|
16156
17573
|
console.error("Usage: flyspace slack delete --channel <channel-id> --ts <timestamp>");
|
|
16157
17574
|
process.exit(1);
|
|
16158
17575
|
}
|
|
16159
|
-
const client2 =
|
|
17576
|
+
const client2 = createClient4();
|
|
16160
17577
|
const channelId = await resolveChannelId2(client2, channel);
|
|
16161
17578
|
await client2.chat.delete({ channel: channelId, ts: ts2 });
|
|
16162
17579
|
console.log(`Message deleted (channel: ${channelId}, ts: ${ts2}).`);
|
|
16163
17580
|
break;
|
|
16164
17581
|
}
|
|
16165
17582
|
case "upload": {
|
|
16166
|
-
const client2 =
|
|
17583
|
+
const client2 = createClient4();
|
|
16167
17584
|
await uploadImageCmd(client2, args.slice(1));
|
|
16168
17585
|
break;
|
|
16169
17586
|
}
|
|
@@ -16358,7 +17775,7 @@ async function runMcpCmd(args) {
|
|
|
16358
17775
|
loadEnvFiles();
|
|
16359
17776
|
switch (server) {
|
|
16360
17777
|
case "threads": {
|
|
16361
|
-
const { startThreadsServer } = await import("./threads-
|
|
17778
|
+
const { startThreadsServer } = await import("./threads-PAG4PKHR.js");
|
|
16362
17779
|
await startThreadsServer();
|
|
16363
17780
|
return;
|
|
16364
17781
|
}
|
|
@@ -16373,7 +17790,7 @@ async function runMcpCmd(args) {
|
|
|
16373
17790
|
import { spawn as spawn5, spawnSync } from "child_process";
|
|
16374
17791
|
import { existsSync as existsSync29 } from "fs";
|
|
16375
17792
|
import { platform as platform6 } from "os";
|
|
16376
|
-
import { join as
|
|
17793
|
+
import { join as join22 } from "path";
|
|
16377
17794
|
function flagValue(args, name) {
|
|
16378
17795
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
16379
17796
|
if (eq) return eq.slice(`--${name}=`.length);
|
|
@@ -16388,7 +17805,8 @@ function openBrowser(url) {
|
|
|
16388
17805
|
}
|
|
16389
17806
|
}
|
|
16390
17807
|
function ensureWebBuild() {
|
|
16391
|
-
if (existsSync29(
|
|
17808
|
+
if (existsSync29(join22(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
|
|
17809
|
+
if (!existsSync29(join22(paths.PACKAGE_ROOT, "web", "package.json"))) return;
|
|
16392
17810
|
console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
|
|
16393
17811
|
const result = spawnSync("pnpm", ["--dir", "web", "build"], {
|
|
16394
17812
|
cwd: paths.PACKAGE_ROOT,
|
|
@@ -16411,6 +17829,7 @@ async function runDashboard(args) {
|
|
|
16411
17829
|
if (mode !== "all" && mode !== "host") {
|
|
16412
17830
|
throw new Error(`--mode must be "all" or "host" (got "${mode}")`);
|
|
16413
17831
|
}
|
|
17832
|
+
if (mode === "host") acquireHostLock();
|
|
16414
17833
|
ensureWebBuild();
|
|
16415
17834
|
const server = await startDashboardServer(preferredPort, host, mode);
|
|
16416
17835
|
const profile = getProfile();
|
|
@@ -16436,7 +17855,7 @@ async function runDashboard(args) {
|
|
|
16436
17855
|
}
|
|
16437
17856
|
|
|
16438
17857
|
// src/commands/workspace.ts
|
|
16439
|
-
import { existsSync as existsSync30, mkdirSync as
|
|
17858
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
16440
17859
|
var USAGE5 = `
|
|
16441
17860
|
Usage: flyspace workspace <subcommand> [options]
|
|
16442
17861
|
|
|
@@ -16483,6 +17902,7 @@ async function provisionSupabase(slug, adminEmail, baseDomain, allowInvite) {
|
|
|
16483
17902
|
console.log(
|
|
16484
17903
|
existing ? ` workspace row exists (${workspace.id})` : ` workspace row created (${workspace.id})`
|
|
16485
17904
|
);
|
|
17905
|
+
await bindThisHost(workspace.id);
|
|
16486
17906
|
if (await getMembershipByEmail(workspace.id, adminEmail)) {
|
|
16487
17907
|
console.log(` membership exists for ${adminEmail}`);
|
|
16488
17908
|
return;
|
|
@@ -16507,6 +17927,16 @@ async function provisionSupabase(slug, adminEmail, baseDomain, allowInvite) {
|
|
|
16507
17927
|
console.log(` sign-in link sent to ${adminEmail}`);
|
|
16508
17928
|
}
|
|
16509
17929
|
}
|
|
17930
|
+
async function bindThisHost(workspaceId) {
|
|
17931
|
+
const deviceId = loadCredentials()?.deviceId;
|
|
17932
|
+
const device = deviceId ? await getDevice(deviceId) : null;
|
|
17933
|
+
if (!device || device.revokedAt) {
|
|
17934
|
+
console.log(" no paired Host on this machine, skipping device binding");
|
|
17935
|
+
return;
|
|
17936
|
+
}
|
|
17937
|
+
await bindDeviceWorkspace(device.id, workspaceId, device.userId);
|
|
17938
|
+
console.log(` bound to Host "${device.name}" (restart it to pick up the new workspace)`);
|
|
17939
|
+
}
|
|
16510
17940
|
function provisionLocal(botName) {
|
|
16511
17941
|
ensureDirs();
|
|
16512
17942
|
console.log(` config dir ready (${paths.CONFIG_DIR})`);
|
|
@@ -16514,7 +17944,7 @@ function provisionLocal(botName) {
|
|
|
16514
17944
|
if (existsSync30(paths.CONFIG_FILE)) {
|
|
16515
17945
|
console.log(" config.env exists");
|
|
16516
17946
|
} else {
|
|
16517
|
-
|
|
17947
|
+
writeFileSync11(paths.CONFIG_FILE, `BOT_NAME=${botName}
|
|
16518
17948
|
`);
|
|
16519
17949
|
console.log(` config.env written (${paths.CONFIG_FILE})`);
|
|
16520
17950
|
}
|
|
@@ -16523,13 +17953,13 @@ async function provisionChannels(specs) {
|
|
|
16523
17953
|
if (specs.length === 0) return;
|
|
16524
17954
|
await refreshChannelRegistry();
|
|
16525
17955
|
for (const [index, spec] of specs.entries()) {
|
|
16526
|
-
if (
|
|
17956
|
+
if (getProfileChannelRegistry().some((c) => c.name === spec.name)) {
|
|
16527
17957
|
console.log(` channel "${spec.name}" exists`);
|
|
16528
17958
|
continue;
|
|
16529
17959
|
}
|
|
16530
17960
|
const cwd = expandPath(spec.cwd);
|
|
16531
17961
|
if (!existsSync30(cwd)) {
|
|
16532
|
-
|
|
17962
|
+
mkdirSync10(cwd, { recursive: true });
|
|
16533
17963
|
console.log(` created cwd ${cwd}`);
|
|
16534
17964
|
}
|
|
16535
17965
|
await createChannel({ name: spec.name, cwd, position: index });
|
|
@@ -17034,7 +18464,7 @@ async function main() {
|
|
|
17034
18464
|
break;
|
|
17035
18465
|
case "-v":
|
|
17036
18466
|
case "--version":
|
|
17037
|
-
printRaw("0.1.
|
|
18467
|
+
printRaw("0.1.4");
|
|
17038
18468
|
break;
|
|
17039
18469
|
case "-h":
|
|
17040
18470
|
case "--help":
|