flyspace-cli 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-6YFDWUNF.js → chunk-KTMAS4TP.js} +126 -18
- package/dist/cli.js +939 -127
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SKIP_SUBTYPES,
|
|
19
19
|
SLACK_ENV,
|
|
20
20
|
SessionStore,
|
|
21
|
+
TRIAL_DAYS,
|
|
21
22
|
UsageStore,
|
|
22
23
|
activateMembership,
|
|
23
24
|
agentLabel,
|
|
@@ -118,6 +119,7 @@ import {
|
|
|
118
119
|
loadFullDashboardSettings,
|
|
119
120
|
loadWorkspaceBranding,
|
|
120
121
|
loadWorkspacePrompt,
|
|
122
|
+
markChannelGreeted,
|
|
121
123
|
markInviteResent,
|
|
122
124
|
markMemoryUsed,
|
|
123
125
|
markWorkspaceGreeted,
|
|
@@ -159,6 +161,7 @@ import {
|
|
|
159
161
|
setMembershipHandle,
|
|
160
162
|
setMemoryAutoPaused,
|
|
161
163
|
setOutputStyleDefault,
|
|
164
|
+
setWorkspaceArchived,
|
|
162
165
|
setWorkspaceStripeSubscription,
|
|
163
166
|
slackConfigured,
|
|
164
167
|
startAutomationRegistryPolling,
|
|
@@ -184,7 +187,7 @@ import {
|
|
|
184
187
|
warnOnBrokenChannelCwds,
|
|
185
188
|
workspaceSkillsDir,
|
|
186
189
|
writeSkillFile
|
|
187
|
-
} from "./chunk-
|
|
190
|
+
} from "./chunk-KTMAS4TP.js";
|
|
188
191
|
import {
|
|
189
192
|
firstReplyBody,
|
|
190
193
|
getThread,
|
|
@@ -618,14 +621,14 @@ function handleHostDbResult(id, res) {
|
|
|
618
621
|
}
|
|
619
622
|
function whenConnected() {
|
|
620
623
|
if (sender) return Promise.resolve(sender);
|
|
621
|
-
return new Promise((
|
|
624
|
+
return new Promise((resolve8, reject) => {
|
|
622
625
|
const timer2 = setTimeout(() => {
|
|
623
626
|
onSender = onSender.filter((w) => w !== wake);
|
|
624
627
|
reject(new Error("Host is not connected to the Data API"));
|
|
625
628
|
}, REQUEST_TIMEOUT_MS);
|
|
626
629
|
const wake = () => {
|
|
627
630
|
clearTimeout(timer2);
|
|
628
|
-
|
|
631
|
+
resolve8(sender);
|
|
629
632
|
};
|
|
630
633
|
onSender.push(wake);
|
|
631
634
|
});
|
|
@@ -633,12 +636,12 @@ function whenConnected() {
|
|
|
633
636
|
async function requestHostDb(req) {
|
|
634
637
|
const send2 = await whenConnected();
|
|
635
638
|
const id = randomUUID();
|
|
636
|
-
return new Promise((
|
|
639
|
+
return new Promise((resolve8, reject) => {
|
|
637
640
|
const timer2 = setTimeout(() => {
|
|
638
641
|
pending.delete(id);
|
|
639
642
|
reject(new Error("Data API did not answer in time"));
|
|
640
643
|
}, REQUEST_TIMEOUT_MS);
|
|
641
|
-
pending.set(id, { resolve:
|
|
644
|
+
pending.set(id, { resolve: resolve8, reject, timer: timer2 });
|
|
642
645
|
send2({ type: "db", id, req });
|
|
643
646
|
});
|
|
644
647
|
}
|
|
@@ -650,14 +653,14 @@ function handleAttachmentUrlResult(id, url) {
|
|
|
650
653
|
async function requestAttachmentUrl(key) {
|
|
651
654
|
const send2 = await whenConnected();
|
|
652
655
|
const id = randomUUID();
|
|
653
|
-
return new Promise((
|
|
656
|
+
return new Promise((resolve8, reject) => {
|
|
654
657
|
const timer2 = setTimeout(() => {
|
|
655
658
|
pendingUrls.delete(id);
|
|
656
659
|
reject(new Error("Data API did not answer in time"));
|
|
657
660
|
}, REQUEST_TIMEOUT_MS);
|
|
658
661
|
pendingUrls.set(id, (url) => {
|
|
659
662
|
clearTimeout(timer2);
|
|
660
|
-
|
|
663
|
+
resolve8(url);
|
|
661
664
|
});
|
|
662
665
|
send2({ type: "attachment-url", id, key });
|
|
663
666
|
});
|
|
@@ -804,10 +807,10 @@ function editConfig() {
|
|
|
804
807
|
const editor = process.env["EDITOR"] || "vi";
|
|
805
808
|
console.log(`[flyspace] Opening ${paths.CONFIG_FILE} in ${editor}...`);
|
|
806
809
|
const child = spawn(editor, [paths.CONFIG_FILE], { stdio: "inherit" });
|
|
807
|
-
return new Promise((
|
|
810
|
+
return new Promise((resolve8, reject) => {
|
|
808
811
|
child.on("close", (code) => {
|
|
809
812
|
if (code === 0) {
|
|
810
|
-
|
|
813
|
+
resolve8();
|
|
811
814
|
} else {
|
|
812
815
|
reject(new Error(`Editor exited with code ${code}`));
|
|
813
816
|
}
|
|
@@ -1858,7 +1861,7 @@ async function runAuthCmd(args) {
|
|
|
1858
1861
|
}
|
|
1859
1862
|
|
|
1860
1863
|
// ../api/src/version.ts
|
|
1861
|
-
var CLI_VERSION = true ? "0.1.
|
|
1864
|
+
var CLI_VERSION = true ? "0.1.34" : null;
|
|
1862
1865
|
function isOlderVersion(a, b) {
|
|
1863
1866
|
const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
1864
1867
|
const [pa, pb] = [parse(a), parse(b)];
|
|
@@ -2468,14 +2471,19 @@ function npmEnv(target) {
|
|
|
2468
2471
|
return nodeBin ? { ...process.env, PATH: `${nodeBin}:${process.env.PATH ?? ""}` } : process.env;
|
|
2469
2472
|
}
|
|
2470
2473
|
async function runUpgrade() {
|
|
2471
|
-
const current = "0.1.
|
|
2474
|
+
const current = "0.1.34";
|
|
2472
2475
|
console.log(`Current version: ${current}`);
|
|
2473
2476
|
console.log("Checking for updates...\n");
|
|
2474
2477
|
try {
|
|
2475
2478
|
const latest2 = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", env: npmEnv() }).trim();
|
|
2476
2479
|
const stale = installTargets().filter((t) => installedVersion(t) !== latest2);
|
|
2477
2480
|
if (stale.length === 0) {
|
|
2478
|
-
|
|
2481
|
+
if (current === latest2) {
|
|
2482
|
+
console.log(`Already on the latest version (${current}).`);
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
console.log(`${latest2} is installed but this process runs ${current}. Restarting...`);
|
|
2486
|
+
restartInstalledProfiles();
|
|
2479
2487
|
return;
|
|
2480
2488
|
}
|
|
2481
2489
|
console.log(`New version available: ${latest2}`);
|
|
@@ -4144,7 +4152,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
|
|
|
4144
4152
|
const subscription = await stripe.subscriptions.create({
|
|
4145
4153
|
customer: customer.id,
|
|
4146
4154
|
items: [{ price: priceId }],
|
|
4147
|
-
trial_period_days:
|
|
4155
|
+
trial_period_days: TRIAL_DAYS,
|
|
4148
4156
|
trial_settings: { end_behavior: { missing_payment_method: "pause" } },
|
|
4149
4157
|
metadata: { workspace_id: workspace.id, plan }
|
|
4150
4158
|
});
|
|
@@ -4792,10 +4800,10 @@ var DEFAULT_DASHBOARD_PORT = 4747;
|
|
|
4792
4800
|
var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
|
|
4793
4801
|
async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
|
|
4794
4802
|
for (let port = start; port < start + attempts; port++) {
|
|
4795
|
-
const free = await new Promise((
|
|
4803
|
+
const free = await new Promise((resolve8) => {
|
|
4796
4804
|
const probe3 = createServer();
|
|
4797
|
-
probe3.once("error", () =>
|
|
4798
|
-
probe3.once("listening", () => probe3.close(() =>
|
|
4805
|
+
probe3.once("error", () => resolve8(false));
|
|
4806
|
+
probe3.once("listening", () => probe3.close(() => resolve8(true)));
|
|
4799
4807
|
probe3.listen(port, host);
|
|
4800
4808
|
});
|
|
4801
4809
|
if (free) return port;
|
|
@@ -5623,14 +5631,14 @@ async function stepBrowserTokens(botName) {
|
|
|
5623
5631
|
const appLink = page2.locator('a[href*="/apps/A"]').first();
|
|
5624
5632
|
const hasExistingApp = await appLink.isVisible({ timeout: 3e3 }).catch(() => false);
|
|
5625
5633
|
if (hasExistingApp) {
|
|
5626
|
-
const tokenPromise = new Promise((
|
|
5634
|
+
const tokenPromise = new Promise((resolve8) => {
|
|
5627
5635
|
page2.on("request", (req) => {
|
|
5628
5636
|
if (token) return;
|
|
5629
5637
|
const postData = req.postData() || "";
|
|
5630
5638
|
const match = postData.match(/xoxc-[a-f0-9-]+/);
|
|
5631
5639
|
if (match) {
|
|
5632
5640
|
token = match[0];
|
|
5633
|
-
|
|
5641
|
+
resolve8(token);
|
|
5634
5642
|
}
|
|
5635
5643
|
});
|
|
5636
5644
|
});
|
|
@@ -5681,7 +5689,7 @@ async function stepBrowserTokens(botName) {
|
|
|
5681
5689
|
log4("Almost done! The browser will now open an install page.\n");
|
|
5682
5690
|
console.log(' \x1B[1mPlease click "Install to Workspace", then click "Allow".\x1B[0m\n');
|
|
5683
5691
|
await page2.goto(installUrl);
|
|
5684
|
-
const installPage = await new Promise((
|
|
5692
|
+
const installPage = await new Promise((resolve8, reject) => {
|
|
5685
5693
|
let settled = false;
|
|
5686
5694
|
let interval;
|
|
5687
5695
|
let timeout;
|
|
@@ -5694,7 +5702,7 @@ async function stepBrowserTokens(botName) {
|
|
|
5694
5702
|
};
|
|
5695
5703
|
const checkUrl = (p) => {
|
|
5696
5704
|
if (p.url().includes("install-on-team?success=1")) {
|
|
5697
|
-
finish2(() =>
|
|
5705
|
+
finish2(() => resolve8(p));
|
|
5698
5706
|
}
|
|
5699
5707
|
};
|
|
5700
5708
|
context.on("page", (newPage) => {
|
|
@@ -6408,9 +6416,9 @@ async function runMcpCmd(args) {
|
|
|
6408
6416
|
|
|
6409
6417
|
// src/commands/dashboard.ts
|
|
6410
6418
|
import { spawn as spawn8, spawnSync } from "child_process";
|
|
6411
|
-
import { existsSync as
|
|
6419
|
+
import { existsSync as existsSync34 } from "fs";
|
|
6412
6420
|
import { platform as platform7 } from "os";
|
|
6413
|
-
import { join as
|
|
6421
|
+
import { join as join29 } from "path";
|
|
6414
6422
|
|
|
6415
6423
|
// src/reconcile.ts
|
|
6416
6424
|
import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
@@ -6583,7 +6591,7 @@ var requireDocsAccess = async (c, next) => {
|
|
|
6583
6591
|
};
|
|
6584
6592
|
|
|
6585
6593
|
// ../api/src/dashboard/api.ts
|
|
6586
|
-
import { Hono as
|
|
6594
|
+
import { Hono as Hono31 } from "hono";
|
|
6587
6595
|
|
|
6588
6596
|
// ../api/src/dashboard/public-share-routes.ts
|
|
6589
6597
|
import { existsSync as existsSync17, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
|
|
@@ -7648,7 +7656,7 @@ function truncationNotice(lang, voice) {
|
|
|
7648
7656
|
if (lang === "en" || lang.startsWith("en-")) return TRUNCATION_NOTICE.en;
|
|
7649
7657
|
return null;
|
|
7650
7658
|
}
|
|
7651
|
-
var sleep2 = (ms) => new Promise((
|
|
7659
|
+
var sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
7652
7660
|
async function synthesizeWithRetry(script, voice) {
|
|
7653
7661
|
let lastError;
|
|
7654
7662
|
for (let attempt = 1; attempt <= SYNTH_ATTEMPTS; attempt += 1) {
|
|
@@ -8735,7 +8743,17 @@ function pruneExpired(now) {
|
|
|
8735
8743
|
}
|
|
8736
8744
|
var SUPPORT_NAME = "Flyspace Support";
|
|
8737
8745
|
var SUPPORT_AVATAR_URL = "/logo/flyspace-support.svg";
|
|
8746
|
+
function workspaceArchivedError() {
|
|
8747
|
+
return new HTTPException2(403, {
|
|
8748
|
+
res: Response.json({ error: "workspace archived", archived: true }, { status: 403 })
|
|
8749
|
+
});
|
|
8750
|
+
}
|
|
8738
8751
|
async function resolveAuthContext(c, opts = {}) {
|
|
8752
|
+
const ctx = await resolveAuthContextCached(c, opts);
|
|
8753
|
+
if (ctx?.archived && !ctx.isSupport) throw workspaceArchivedError();
|
|
8754
|
+
return ctx;
|
|
8755
|
+
}
|
|
8756
|
+
async function resolveAuthContextCached(c, opts) {
|
|
8739
8757
|
const cookie = c.req?.header("Cookie") ?? "";
|
|
8740
8758
|
if (!cookie) return resolveAuthContextUncached(c);
|
|
8741
8759
|
const key = `${getProfile()}\0${cookie}`;
|
|
@@ -8800,7 +8818,8 @@ async function resolveAuthContextUncached(c) {
|
|
|
8800
8818
|
passwordSet: Boolean(metadata.passwordSetAt),
|
|
8801
8819
|
agentConnectedAt: workspace.agentConnectedAt ?? null,
|
|
8802
8820
|
projectsSetupAt: workspace.projectsSetupAt ?? null,
|
|
8803
|
-
plan: workspace.plan ?? null
|
|
8821
|
+
plan: workspace.plan ?? null,
|
|
8822
|
+
...workspace.archivedAt ? { archived: true } : {}
|
|
8804
8823
|
};
|
|
8805
8824
|
}
|
|
8806
8825
|
|
|
@@ -9116,11 +9135,19 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
|
9116
9135
|
// ../api/src/dashboard/uploads-routes.ts
|
|
9117
9136
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
9118
9137
|
import { Hono as Hono7 } from "hono";
|
|
9138
|
+
import sharp4 from "sharp";
|
|
9119
9139
|
import { PutObjectCommand as PutObjectCommand2, GetObjectCommand as GetObjectCommand3 } from "@aws-sdk/client-s3";
|
|
9120
9140
|
import { getSignedUrl as getSignedUrl3 } from "@aws-sdk/s3-request-presigner";
|
|
9121
9141
|
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
9122
9142
|
var PRESIGN_TTL_SECONDS2 = 300;
|
|
9123
9143
|
var KEY_PREFIX = "chat";
|
|
9144
|
+
var RESIZABLE = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/avif"]);
|
|
9145
|
+
var FULL_EDGE = 2560;
|
|
9146
|
+
var THUMB_EDGE = 480;
|
|
9147
|
+
var WEBP_QUALITY = 80;
|
|
9148
|
+
function toWebp(input2, edge) {
|
|
9149
|
+
return sharp4(input2).rotate().resize({ width: edge, height: edge, fit: "inside", withoutEnlargement: true }).webp({ quality: WEBP_QUALITY }).toBuffer();
|
|
9150
|
+
}
|
|
9124
9151
|
var READABLE_PREFIXES = [KEY_PREFIX, SCRATCH_PREFIX];
|
|
9125
9152
|
function sanitizeFilename(name) {
|
|
9126
9153
|
const cleaned = name.replace(/[/\\]/g, "_").replace(/\s+/g, "-").replace(/[^\w.-]/g, "").trim();
|
|
@@ -9144,22 +9171,33 @@ function createUploadsRouter() {
|
|
|
9144
9171
|
if (file.size > MAX_FILE_BYTES) {
|
|
9145
9172
|
return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
|
|
9146
9173
|
}
|
|
9147
|
-
const
|
|
9148
|
-
const
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
)
|
|
9174
|
+
const id = `${KEY_PREFIX}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
|
|
9175
|
+
const original = Buffer.from(await file.arrayBuffer());
|
|
9176
|
+
const put = (Key, Body, ContentType) => getR2Client().send(new PutObjectCommand2({ Bucket: getR2Bucket(), Key, Body, ContentType }));
|
|
9177
|
+
let key = id;
|
|
9178
|
+
let size = file.size;
|
|
9179
|
+
let type = file.type;
|
|
9180
|
+
const renditions = RESIZABLE.has(file.type) ? await Promise.all([toWebp(original, FULL_EDGE), toWebp(original, THUMB_EDGE)]).catch(
|
|
9181
|
+
() => null
|
|
9182
|
+
) : null;
|
|
9183
|
+
if (renditions) {
|
|
9184
|
+
const [full, thumb] = renditions;
|
|
9185
|
+
key = `${id.replace(/\.[^./]*$/, "")}.webp`;
|
|
9186
|
+
size = full.length;
|
|
9187
|
+
type = "image/webp";
|
|
9188
|
+
await Promise.all([
|
|
9189
|
+
put(key, full, type),
|
|
9190
|
+
put(key.replace(/\.webp$/, ".thumb.webp"), thumb, type)
|
|
9191
|
+
]);
|
|
9192
|
+
} else {
|
|
9193
|
+
await put(key, original, file.type || "application/octet-stream");
|
|
9194
|
+
}
|
|
9157
9195
|
return c.json(
|
|
9158
9196
|
{
|
|
9159
9197
|
url: `/api/attachments/${encodeKeyForUrl(key)}`,
|
|
9160
9198
|
name: file.name,
|
|
9161
|
-
type
|
|
9162
|
-
size
|
|
9199
|
+
type,
|
|
9200
|
+
size
|
|
9163
9201
|
},
|
|
9164
9202
|
201
|
|
9165
9203
|
);
|
|
@@ -9544,12 +9582,94 @@ function createAuthRouter() {
|
|
|
9544
9582
|
return auth;
|
|
9545
9583
|
}
|
|
9546
9584
|
|
|
9585
|
+
// ../api/src/store/support-store.ts
|
|
9586
|
+
var SUPPORT_HANDLE = "flyspace-support";
|
|
9587
|
+
async function logSupportAction(entry) {
|
|
9588
|
+
const { error: error2 } = await getSupabaseClient().from("support_access_log").insert({
|
|
9589
|
+
workspace_id: entry.workspaceId,
|
|
9590
|
+
staff_user_id: entry.staffUserId,
|
|
9591
|
+
staff_email: entry.staffEmail,
|
|
9592
|
+
action: entry.action,
|
|
9593
|
+
detail: entry.detail ?? null
|
|
9594
|
+
});
|
|
9595
|
+
if (error2) throw new Error(`logSupportAction(${entry.action}): ${error2.message}`);
|
|
9596
|
+
}
|
|
9597
|
+
async function listSupportWorkspaceIds(userId) {
|
|
9598
|
+
const { data, error: error2 } = await getSupabaseClient().from("memberships").select("workspace_id").eq("user_id", userId).eq("is_support", true);
|
|
9599
|
+
if (error2) throw new Error(`listSupportWorkspaceIds: ${error2.message}`);
|
|
9600
|
+
return new Set((data ?? []).map((r) => r.workspace_id));
|
|
9601
|
+
}
|
|
9602
|
+
async function grantSupportMembership(params) {
|
|
9603
|
+
const db = getSupabaseClient();
|
|
9604
|
+
const { data: existing, error: readError } = await db.from("memberships").select("is_support").eq("workspace_id", params.workspaceId).eq("user_id", params.userId).maybeSingle();
|
|
9605
|
+
if (readError) throw new Error(`grantSupportMembership: ${readError.message}`);
|
|
9606
|
+
if (existing) {
|
|
9607
|
+
if (existing.is_support === true) return "existing";
|
|
9608
|
+
throw new Error("already a regular member of this workspace");
|
|
9609
|
+
}
|
|
9610
|
+
const { data: taken } = await db.from("memberships").select("id").eq("workspace_id", params.workspaceId).eq("handle", SUPPORT_HANDLE).maybeSingle();
|
|
9611
|
+
const handle = taken ? `${SUPPORT_HANDLE}-${params.userId.slice(0, 8)}` : SUPPORT_HANDLE;
|
|
9612
|
+
const { error: error2 } = await db.from("memberships").insert({
|
|
9613
|
+
workspace_id: params.workspaceId,
|
|
9614
|
+
user_id: params.userId,
|
|
9615
|
+
email: params.email,
|
|
9616
|
+
handle,
|
|
9617
|
+
role: "admin",
|
|
9618
|
+
status: "active",
|
|
9619
|
+
is_support: true,
|
|
9620
|
+
accepted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9621
|
+
});
|
|
9622
|
+
if (error2) throw new Error(`grantSupportMembership: ${error2.message}`);
|
|
9623
|
+
return "granted";
|
|
9624
|
+
}
|
|
9625
|
+
async function revokeSupportMembership(workspaceId, userId) {
|
|
9626
|
+
const { data, error: error2 } = await getSupabaseClient().from("memberships").delete().eq("workspace_id", workspaceId).eq("user_id", userId).eq("is_support", true).select("id");
|
|
9627
|
+
if (error2) throw new Error(`revokeSupportMembership: ${error2.message}`);
|
|
9628
|
+
return (data ?? []).length > 0;
|
|
9629
|
+
}
|
|
9630
|
+
async function listSupportLog(filter) {
|
|
9631
|
+
let query = getSupabaseClient().from("support_access_log").select("*").order("created_at", { ascending: false }).limit(Math.min(filter.limit ?? 200, 1e3));
|
|
9632
|
+
if (filter.workspaceId) query = query.eq("workspace_id", filter.workspaceId);
|
|
9633
|
+
const { data, error: error2 } = await query;
|
|
9634
|
+
if (error2) throw new Error(`listSupportLog: ${error2.message}`);
|
|
9635
|
+
return (data ?? []).map((r) => ({
|
|
9636
|
+
id: r.id,
|
|
9637
|
+
workspaceId: r.workspace_id,
|
|
9638
|
+
staffUserId: r.staff_user_id,
|
|
9639
|
+
staffEmail: r.staff_email,
|
|
9640
|
+
action: r.action,
|
|
9641
|
+
detail: r.detail ?? null,
|
|
9642
|
+
createdAt: r.created_at
|
|
9643
|
+
}));
|
|
9644
|
+
}
|
|
9645
|
+
|
|
9547
9646
|
// ../api/src/dashboard/auth-middleware.ts
|
|
9647
|
+
var ENTER_WINDOW_MS = 8 * 60 * 60 * 1e3;
|
|
9648
|
+
var lastEnter = /* @__PURE__ */ new Map();
|
|
9649
|
+
function auditSupport(ctx, action, detail) {
|
|
9650
|
+
logSupportAction({
|
|
9651
|
+
workspaceId: ctx.workspaceId,
|
|
9652
|
+
staffUserId: ctx.userId,
|
|
9653
|
+
staffEmail: ctx.email,
|
|
9654
|
+
action,
|
|
9655
|
+
detail
|
|
9656
|
+
}).catch((err) => console.error("[support-audit]", err.message));
|
|
9657
|
+
}
|
|
9548
9658
|
var requireWorkspaceMembership = async (c, next) => {
|
|
9549
9659
|
const ctx = await resolveAuthContext(c);
|
|
9550
9660
|
if (!ctx) return c.json({ error: "not signed in" }, 401);
|
|
9551
9661
|
c.set("auth", ctx);
|
|
9552
|
-
return next();
|
|
9662
|
+
if (!ctx.isSupport) return next();
|
|
9663
|
+
const key = `${ctx.userId}:${ctx.workspaceId}`;
|
|
9664
|
+
const now = Date.now();
|
|
9665
|
+
if ((lastEnter.get(key) ?? 0) + ENTER_WINDOW_MS < now) {
|
|
9666
|
+
lastEnter.set(key, now);
|
|
9667
|
+
auditSupport(ctx, "enter");
|
|
9668
|
+
}
|
|
9669
|
+
await next();
|
|
9670
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(c.req.method)) {
|
|
9671
|
+
auditSupport(ctx, "write", { method: c.req.method, path: c.req.path, status: c.res.status });
|
|
9672
|
+
}
|
|
9553
9673
|
};
|
|
9554
9674
|
|
|
9555
9675
|
// ../api/src/dashboard/billing-routes.ts
|
|
@@ -9654,7 +9774,14 @@ function createAdminRouter() {
|
|
|
9654
9774
|
const admin = new Hono11();
|
|
9655
9775
|
admin.use("*", requirePlatformOperator);
|
|
9656
9776
|
admin.get("/workspaces", async (c) => {
|
|
9657
|
-
const
|
|
9777
|
+
const operator = c.get("platformOperator");
|
|
9778
|
+
const [summaries, supportIds, users] = await Promise.all([
|
|
9779
|
+
listWorkspaceSummaries(),
|
|
9780
|
+
listSupportWorkspaceIds(operator.userId),
|
|
9781
|
+
// One call for every owner's profile metadata instead of one per row.
|
|
9782
|
+
getSupabaseClient().auth.admin.listUsers({ perPage: 1e3 })
|
|
9783
|
+
]);
|
|
9784
|
+
const usersById = new Map((users.data?.users ?? []).map((u) => [u.id, u]));
|
|
9658
9785
|
const workspaces = await Promise.all(
|
|
9659
9786
|
summaries.map(async (w) => {
|
|
9660
9787
|
const agentModel = cachedProfileConfig(w.slug)?.agentModel;
|
|
@@ -9664,11 +9791,23 @@ function createAdminRouter() {
|
|
|
9664
9791
|
console.error(`[admin] R2 usage for ${w.slug}:`, err.message);
|
|
9665
9792
|
return null;
|
|
9666
9793
|
});
|
|
9794
|
+
const metadata = w.owner ? usersById.get(w.owner.userId)?.user_metadata : void 0;
|
|
9795
|
+
const owner = w.owner && {
|
|
9796
|
+
...w.owner,
|
|
9797
|
+
role: "admin",
|
|
9798
|
+
avatarUrl: metadata?.avatarKey ? await signAvatarUrl(metadata.avatarKey) : null,
|
|
9799
|
+
name: metadata?.name ?? null,
|
|
9800
|
+
title: metadata?.title ?? null,
|
|
9801
|
+
bio: metadata?.bio ?? null,
|
|
9802
|
+
timeZone: metadata?.timeZone ?? null
|
|
9803
|
+
};
|
|
9667
9804
|
return {
|
|
9668
9805
|
...w,
|
|
9806
|
+
owner,
|
|
9669
9807
|
agent: agentModel ? { runner: runnerForModel(agentModel), model: agentModel } : null,
|
|
9670
9808
|
r2Bytes,
|
|
9671
|
-
flyVolumeBytes: null
|
|
9809
|
+
flyVolumeBytes: null,
|
|
9810
|
+
supportAccess: supportIds.has(w.id)
|
|
9672
9811
|
};
|
|
9673
9812
|
})
|
|
9674
9813
|
);
|
|
@@ -9756,6 +9895,63 @@ function createAdminRouter() {
|
|
|
9756
9895
|
}
|
|
9757
9896
|
return c.json({ sent: true, inviteLink: inviteUrl.toString() });
|
|
9758
9897
|
});
|
|
9898
|
+
admin.post("/workspaces/:id/support-access", async (c) => {
|
|
9899
|
+
const operator = c.get("platformOperator");
|
|
9900
|
+
const workspaceId = c.req.param("id");
|
|
9901
|
+
let result;
|
|
9902
|
+
try {
|
|
9903
|
+
result = await grantSupportMembership({
|
|
9904
|
+
workspaceId,
|
|
9905
|
+
userId: operator.userId,
|
|
9906
|
+
email: operator.email
|
|
9907
|
+
});
|
|
9908
|
+
} catch (err) {
|
|
9909
|
+
return c.json({ error: err instanceof Error ? err.message : "grant failed" }, 409);
|
|
9910
|
+
}
|
|
9911
|
+
if (result === "granted") {
|
|
9912
|
+
await logSupportAction({
|
|
9913
|
+
workspaceId,
|
|
9914
|
+
staffUserId: operator.userId,
|
|
9915
|
+
staffEmail: operator.email,
|
|
9916
|
+
action: "grant"
|
|
9917
|
+
});
|
|
9918
|
+
}
|
|
9919
|
+
invalidateAuthContextCache();
|
|
9920
|
+
return c.json({ supportAccess: true });
|
|
9921
|
+
});
|
|
9922
|
+
admin.delete("/workspaces/:id/support-access", async (c) => {
|
|
9923
|
+
const operator = c.get("platformOperator");
|
|
9924
|
+
const workspaceId = c.req.param("id");
|
|
9925
|
+
if (await revokeSupportMembership(workspaceId, operator.userId)) {
|
|
9926
|
+
await logSupportAction({
|
|
9927
|
+
workspaceId,
|
|
9928
|
+
staffUserId: operator.userId,
|
|
9929
|
+
staffEmail: operator.email,
|
|
9930
|
+
action: "leave"
|
|
9931
|
+
});
|
|
9932
|
+
}
|
|
9933
|
+
invalidateAuthContextCache();
|
|
9934
|
+
return c.json({ supportAccess: false });
|
|
9935
|
+
});
|
|
9936
|
+
const archiveRoute = (archived) => async (c) => {
|
|
9937
|
+
try {
|
|
9938
|
+
await setWorkspaceArchived(c.req.param("id"), archived);
|
|
9939
|
+
} catch (err) {
|
|
9940
|
+
const message = err instanceof Error ? err.message : "archive failed";
|
|
9941
|
+
return c.json({ error: message }, message === "workspace not found" ? 404 : 500);
|
|
9942
|
+
}
|
|
9943
|
+
invalidateAuthContextCache();
|
|
9944
|
+
return c.json({ archived });
|
|
9945
|
+
};
|
|
9946
|
+
admin.post("/workspaces/:id/archive", archiveRoute(true));
|
|
9947
|
+
admin.delete("/workspaces/:id/archive", archiveRoute(false));
|
|
9948
|
+
admin.get("/support-log", async (c) => {
|
|
9949
|
+
const entries2 = await listSupportLog({
|
|
9950
|
+
workspaceId: c.req.query("workspace") || void 0,
|
|
9951
|
+
limit: Number(c.req.query("limit")) || void 0
|
|
9952
|
+
});
|
|
9953
|
+
return c.json({ entries: entries2 });
|
|
9954
|
+
});
|
|
9759
9955
|
return admin;
|
|
9760
9956
|
}
|
|
9761
9957
|
|
|
@@ -9765,7 +9961,7 @@ var BOT_DM_KEY = "claude";
|
|
|
9765
9961
|
|
|
9766
9962
|
// ../api/src/dashboard/data-api.ts
|
|
9767
9963
|
import { existsSync as existsSync20 } from "fs";
|
|
9768
|
-
import
|
|
9964
|
+
import sharp6 from "sharp";
|
|
9769
9965
|
|
|
9770
9966
|
// ../api/src/dashboard/automations-routes.ts
|
|
9771
9967
|
import { Hono as Hono12 } from "hono";
|
|
@@ -10259,6 +10455,25 @@ var BOARD_STATUSES = [
|
|
|
10259
10455
|
"cancelled"
|
|
10260
10456
|
];
|
|
10261
10457
|
var NO_STATUS_COLUMN = "none";
|
|
10458
|
+
function isBoardStatus(v) {
|
|
10459
|
+
return typeof v === "string" && BOARD_STATUSES.includes(v);
|
|
10460
|
+
}
|
|
10461
|
+
async function callerMembershipId(c) {
|
|
10462
|
+
const viewer = c.get("auth");
|
|
10463
|
+
if (!viewer) return null;
|
|
10464
|
+
return (await getMembershipByUserId(viewer.workspaceId, viewer.userId))?.id ?? null;
|
|
10465
|
+
}
|
|
10466
|
+
async function rememberComposerStatus(c, channelId, status) {
|
|
10467
|
+
const membershipId = await callerMembershipId(c);
|
|
10468
|
+
if (!membershipId) return;
|
|
10469
|
+
const { error: error2 } = await getSupabaseClient().from("channel_member_prefs").upsert({
|
|
10470
|
+
channel_id: channelId,
|
|
10471
|
+
membership_id: membershipId,
|
|
10472
|
+
last_board_status: status,
|
|
10473
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10474
|
+
});
|
|
10475
|
+
if (error2) throw new Error(error2.message);
|
|
10476
|
+
}
|
|
10262
10477
|
function boardColumnParam(status) {
|
|
10263
10478
|
if (status === NO_STATUS_COLUMN) return NO_STATUS_COLUMN;
|
|
10264
10479
|
return BOARD_STATUSES.includes(status) ? status : null;
|
|
@@ -10324,7 +10539,9 @@ async function withPreviews(supabase, threads) {
|
|
|
10324
10539
|
for (const { data } of chunkResults) {
|
|
10325
10540
|
for (const r of data ?? []) {
|
|
10326
10541
|
const entry = previewByThread.get(r.thread_id);
|
|
10542
|
+
const images = r.body.includes("![") ? markdownImageUrls(slackMrkdwnToMarkdown(r.body)) : [];
|
|
10327
10543
|
if (entry) {
|
|
10544
|
+
entry.images.push(...images);
|
|
10328
10545
|
entry.count += 1;
|
|
10329
10546
|
entry.lastAuthor = r.author;
|
|
10330
10547
|
entry.participants.delete(r.author);
|
|
@@ -10334,7 +10551,8 @@ async function withPreviews(supabase, threads) {
|
|
|
10334
10551
|
count: 1,
|
|
10335
10552
|
firstBody: r.body,
|
|
10336
10553
|
lastAuthor: r.author,
|
|
10337
|
-
participants: /* @__PURE__ */ new Map()
|
|
10554
|
+
participants: /* @__PURE__ */ new Map(),
|
|
10555
|
+
images
|
|
10338
10556
|
});
|
|
10339
10557
|
}
|
|
10340
10558
|
}
|
|
@@ -10349,10 +10567,19 @@ async function withPreviews(supabase, threads) {
|
|
|
10349
10567
|
// read, on this browser or any other (see web/src/lib/thread-unread).
|
|
10350
10568
|
last_reply_author: entry && entry.count > 1 ? entry.lastAuthor : null,
|
|
10351
10569
|
// Everyone who replied, most recent first, for the row's avatar stack.
|
|
10352
|
-
participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id }))
|
|
10570
|
+
participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id })),
|
|
10571
|
+
// Every image in the thread, in order, so the row can show a thumbnail
|
|
10572
|
+
// strip; only the first few URLs are sent, the rest are just counted.
|
|
10573
|
+
images: (entry?.images ?? []).slice(0, ROW_IMAGE_LIMIT),
|
|
10574
|
+
image_count: entry?.images.length ?? 0
|
|
10353
10575
|
};
|
|
10354
10576
|
});
|
|
10355
10577
|
}
|
|
10578
|
+
var ROW_IMAGE_LIMIT = 3;
|
|
10579
|
+
var MARKDOWN_IMAGE_RE = /!\[[^\]]*\]\(\s*<?([^\s)>]+)>?(?:\s+"[^"]*")?\s*\)/g;
|
|
10580
|
+
function markdownImageUrls(markdown) {
|
|
10581
|
+
return [...markdown.matchAll(MARKDOWN_IMAGE_RE)].map((m) => m[1]);
|
|
10582
|
+
}
|
|
10356
10583
|
async function labelThread(channelId, body, scope) {
|
|
10357
10584
|
const channel = getProfileChannelRegistry().find(
|
|
10358
10585
|
(ch) => ch.id === channelId || ch.externalChannelId === channelId
|
|
@@ -10762,6 +10989,13 @@ function createChatRouter(agent = {}) {
|
|
|
10762
10989
|
const clientKey2 = isNonEmptyString(input2.clientKey) ? input2.clientKey : null;
|
|
10763
10990
|
const model = requestedModel(c, input2.model);
|
|
10764
10991
|
const effort = requestedEffort(c, input2.effort);
|
|
10992
|
+
const pickedStatus = input2.board_status;
|
|
10993
|
+
if (pickedStatus != null && !isBoardStatus(pickedStatus)) {
|
|
10994
|
+
return c.json(
|
|
10995
|
+
{ error: `board_status must be null or one of: ${BOARD_STATUSES.join(", ")}` },
|
|
10996
|
+
400
|
|
10997
|
+
);
|
|
10998
|
+
}
|
|
10765
10999
|
const supabase = getSupabaseClient();
|
|
10766
11000
|
let thread;
|
|
10767
11001
|
let replayed = false;
|
|
@@ -10772,7 +11006,8 @@ function createChatRouter(agent = {}) {
|
|
|
10772
11006
|
// opening message at display time (see threadCrumbLabel).
|
|
10773
11007
|
title: isNonEmptyString(input2.title) ? input2.title.trim() : null,
|
|
10774
11008
|
author: input2.author,
|
|
10775
|
-
client_key: clientKey2
|
|
11009
|
+
client_key: clientKey2,
|
|
11010
|
+
board_status: pickedStatus ?? null
|
|
10776
11011
|
}).select().single();
|
|
10777
11012
|
if (threadError) {
|
|
10778
11013
|
if (!clientKey2 || threadError.code !== "23505") {
|
|
@@ -10813,6 +11048,11 @@ function createChatRouter(agent = {}) {
|
|
|
10813
11048
|
effort: effortFromRow(reply["effort"])
|
|
10814
11049
|
});
|
|
10815
11050
|
}
|
|
11051
|
+
if (!replayed && pickedStatus !== void 0) {
|
|
11052
|
+
void rememberComposerStatus(c, channelId, pickedStatus).catch(
|
|
11053
|
+
(err) => console.error("Remember composer status failed:", err)
|
|
11054
|
+
);
|
|
11055
|
+
}
|
|
10816
11056
|
if (!replayed && thread["title"] == null) {
|
|
10817
11057
|
void generateAutoTitle(thread.id, channelId, input2.body).catch(
|
|
10818
11058
|
(err) => console.error("Auto title failed:", err)
|
|
@@ -10820,6 +11060,15 @@ function createChatRouter(agent = {}) {
|
|
|
10820
11060
|
}
|
|
10821
11061
|
return c.json({ ...thread, replies: [reply] }, replayed ? 200 : 201);
|
|
10822
11062
|
});
|
|
11063
|
+
app.get("/channels/:channelId/composer-status", async (c) => {
|
|
11064
|
+
const channelId = c.req.param("channelId");
|
|
11065
|
+
if (!ownChannelIds(c).has(channelId)) return c.json({ error: "unknown channel" }, 404);
|
|
11066
|
+
const membershipId = await callerMembershipId(c);
|
|
11067
|
+
if (!membershipId) return c.json({ lastStatus: null });
|
|
11068
|
+
const { data: pref, error: error2 } = await getSupabaseClient().from("channel_member_prefs").select("last_board_status").eq("channel_id", channelId).eq("membership_id", membershipId).maybeSingle();
|
|
11069
|
+
if (error2) return c.json({ error: error2.message }, 500);
|
|
11070
|
+
return c.json({ lastStatus: pref?.last_board_status ?? null });
|
|
11071
|
+
});
|
|
10823
11072
|
app.post("/replies/:replyId/fork", async (c) => {
|
|
10824
11073
|
const replyId = c.req.param("replyId");
|
|
10825
11074
|
const input2 = await c.req.json().catch(() => null);
|
|
@@ -11035,7 +11284,7 @@ function generateShareId() {
|
|
|
11035
11284
|
return randomBytes6(9).toString("base64url");
|
|
11036
11285
|
}
|
|
11037
11286
|
async function publishDocShare(input2, readBundle) {
|
|
11038
|
-
const bundle = await readBundle({ cwd: input2.cwd, sourcePath: input2.sourcePath });
|
|
11287
|
+
const bundle = await readBundle({ channelId: input2.channelId, cwd: input2.cwd, sourcePath: input2.sourcePath });
|
|
11039
11288
|
if (bundle.status !== "ok") return bundle;
|
|
11040
11289
|
const existing = await findDocShareBySource(input2.workspaceId, input2.channelId, input2.sourcePath);
|
|
11041
11290
|
const shareId = existing?.id ?? generateShareId();
|
|
@@ -11695,7 +11944,7 @@ function createTranscribeRouter() {
|
|
|
11695
11944
|
// ../api/src/dashboard/avatars-routes.ts
|
|
11696
11945
|
import { createHash as createHash9, randomUUID as randomUUID4 } from "crypto";
|
|
11697
11946
|
import { Hono as Hono19 } from "hono";
|
|
11698
|
-
import
|
|
11947
|
+
import sharp5 from "sharp";
|
|
11699
11948
|
import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
|
|
11700
11949
|
var AVATAR_SIZE = 192;
|
|
11701
11950
|
var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
@@ -11731,7 +11980,7 @@ async function storeAvatar(file, owner) {
|
|
|
11731
11980
|
return { error: "avatar must be a PNG or JPEG image" };
|
|
11732
11981
|
if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
|
|
11733
11982
|
const original = Buffer.from(await file.arrayBuffer());
|
|
11734
|
-
const resized = await
|
|
11983
|
+
const resized = await sharp5(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
|
|
11735
11984
|
const key = `${avatarKeyPrefix()}${owner}-${randomUUID4()}.png`;
|
|
11736
11985
|
await getR2Client().send(
|
|
11737
11986
|
new PutObjectCommand4({
|
|
@@ -11909,10 +12158,6 @@ function createMembersRouter() {
|
|
|
11909
12158
|
if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
|
|
11910
12159
|
return c.json({ error: "unknown avatar" }, 400);
|
|
11911
12160
|
}
|
|
11912
|
-
const { plan } = await getWorkspaceBilling(ctx.workspaceId);
|
|
11913
|
-
if (plan === "local") {
|
|
11914
|
-
return c.json({ error: "The Solo plan is for one person. Upgrade to invite members." }, 403);
|
|
11915
|
-
}
|
|
11916
12161
|
const existing = await getMembershipByEmail(ctx.workspaceId, email);
|
|
11917
12162
|
if (existing) return c.json({ error: "already a member of this workspace" }, 409);
|
|
11918
12163
|
let membership;
|
|
@@ -13425,8 +13670,95 @@ async function buildThreadUsage(threadTs) {
|
|
|
13425
13670
|
}
|
|
13426
13671
|
}
|
|
13427
13672
|
|
|
13428
|
-
// ../api/src/dashboard/channel-
|
|
13673
|
+
// ../api/src/dashboard/channel-greeting.ts
|
|
13429
13674
|
import { Hono as Hono25 } from "hono";
|
|
13675
|
+
function eligible(ch) {
|
|
13676
|
+
return !!ch && !ch.isDefault && !ch.dmKey && !ch.agentName && !ch.archivedAt;
|
|
13677
|
+
}
|
|
13678
|
+
function buildIntroPrompt(ch) {
|
|
13679
|
+
const automations = [
|
|
13680
|
+
...(ch.automations?.schedules ?? []).map((s) => s.name),
|
|
13681
|
+
...(ch.automations?.triggers ?? []).map((t) => t.name)
|
|
13682
|
+
].filter(Boolean);
|
|
13683
|
+
return [
|
|
13684
|
+
`[channel intro request] Write the one-time intro message for the #${ch.name} channel, into this thread. It is the first thing a visitor sees here, so it must be grounded in what this folder actually contains.`,
|
|
13685
|
+
"",
|
|
13686
|
+
"Read-only: inspect the folder (file listing, README.md, AGENTS.md / CLAUDE.md, docs/, git status, .claude/skills and any SKILL.md files), but do not create, modify or delete anything, and do not run anything with side effects.",
|
|
13687
|
+
"",
|
|
13688
|
+
"Language: match how this project is usually worked on, in this order of evidence: (1) the user messages in this folder's recent agent sessions (Claude Code: ~/.claude/projects/<encoded cwd>/*.jsonl; Codex: ~/.codex/sessions/**/*.jsonl with a matching cwd): read only enough to judge the language, never quote them; (2) the main language of README.md / AGENTS.md / docs/; (3) English. For Chinese use Traditional Chinese (Taiwan), full-width punctuation.",
|
|
13689
|
+
"",
|
|
13690
|
+
"Shape (pyramid, at most ~80 Chinese characters / ~60 English words in total):",
|
|
13691
|
+
"1. One sentence: what this channel/project is, and what you can best help with here.",
|
|
13692
|
+
`2. Three one-line bullets: what already exists in the folder (repo, docs); 1-2 skills that are really available and how to invoke them; one concrete automation suggestion with frequency and content (e.g. "every Monday 9am, summarise last week's commits here").`,
|
|
13693
|
+
"3. End with exactly one line `[BUTTONS: a | b | c]` whose three buttons correspond to the three bullets and read as directly executable requests.",
|
|
13694
|
+
"",
|
|
13695
|
+
"Rules: never invent skills or automations that are not actually there: skip that bullet instead. " + (automations.length ? `This channel already has automations (${automations.join(", ")}): introduce the existing one(s) instead of suggesting a new one. ` : "") + "Do not explain Flyspace itself. If the folder is empty, instead ask what this channel will be used for and offer `[BUTTONS: Help me set up this channel]`. Output only the message itself, nothing else."
|
|
13696
|
+
].join("\n");
|
|
13697
|
+
}
|
|
13698
|
+
var FALLBACK_BODY = "I could not write this channel's intro automatically. Ask me below and I'll take a look at the folder.\n\n[BUTTONS: Introduce this channel]";
|
|
13699
|
+
function introOpening(channelName) {
|
|
13700
|
+
return `Welcome to #${channelName}. I'm reading this project's folder, docs, skills and automations to work out how I can help here. Below you'll find a short intro and a few first steps you can click to try.`;
|
|
13701
|
+
}
|
|
13702
|
+
function createChannelGreetingRouter(agent) {
|
|
13703
|
+
const app = new Hono25();
|
|
13704
|
+
app.get("/:id/greeting", async (c) => {
|
|
13705
|
+
const ctx = await resolveAuthContext(c);
|
|
13706
|
+
const ch = findChannelById(c.req.param("id"));
|
|
13707
|
+
const pending2 = isWorkspaceOwner(ctx) && eligible(ch) && !ch.greetedAt && (agent.isHostOnline?.(ch.profile) ?? false);
|
|
13708
|
+
if (!pending2) return c.json({ pending: false, threadId: null });
|
|
13709
|
+
const threadId = await ensureIntroThread(ch);
|
|
13710
|
+
return c.json({ pending: !!threadId, threadId });
|
|
13711
|
+
});
|
|
13712
|
+
app.post("/:id/greeting", async (c) => {
|
|
13713
|
+
const ctx = await resolveAuthContext(c);
|
|
13714
|
+
if (!isWorkspaceOwner(ctx)) return c.json({ error: "workspace owner access required" }, 403);
|
|
13715
|
+
const ch = findChannelById(c.req.param("id"));
|
|
13716
|
+
if (!eligible(ch)) return c.json({ error: "channel has no intro" }, 404);
|
|
13717
|
+
const threadId = await ensureIntroThread(ch);
|
|
13718
|
+
if (!threadId) return c.json({ error: "could not create the intro thread" }, 500);
|
|
13719
|
+
if (!await markChannelGreeted(ch.id)) return c.json({ threadId, error: "already greeted" }, 409);
|
|
13720
|
+
try {
|
|
13721
|
+
if (!(agent.isHostOnline?.(ch.profile) ?? false)) throw new Error("host offline");
|
|
13722
|
+
await agent.onChatMessage?.({
|
|
13723
|
+
threadId,
|
|
13724
|
+
// Synthetic: no thread_replies row has this id, so the prompt is
|
|
13725
|
+
// never shown and never counted as thread context.
|
|
13726
|
+
replyId: crypto.randomUUID(),
|
|
13727
|
+
author: ctx.handle,
|
|
13728
|
+
body: buildIntroPrompt(ch)
|
|
13729
|
+
});
|
|
13730
|
+
} catch (err) {
|
|
13731
|
+
console.error(`[channel-greeting] dispatch failed for #${ch.name}:`, err);
|
|
13732
|
+
await getSupabaseClient().from("thread_replies").insert({ thread_id: threadId, author: AGENT_AUTHOR, body: FALLBACK_BODY });
|
|
13733
|
+
}
|
|
13734
|
+
return c.json({ threadId }, 201);
|
|
13735
|
+
});
|
|
13736
|
+
return app;
|
|
13737
|
+
}
|
|
13738
|
+
async function ensureIntroThread(ch) {
|
|
13739
|
+
const supabase = getSupabaseClient();
|
|
13740
|
+
const existing = () => supabase.from("threads").select("id").eq("client_key", ch.id).maybeSingle();
|
|
13741
|
+
const found = await existing();
|
|
13742
|
+
if (found.data) return found.data.id;
|
|
13743
|
+
const { data, error: error2 } = await supabase.from("threads").insert({
|
|
13744
|
+
channel_id: ch.id,
|
|
13745
|
+
title: `Getting started in #${ch.name}`,
|
|
13746
|
+
author: AGENT_AUTHOR,
|
|
13747
|
+
client_key: ch.id
|
|
13748
|
+
}).select("id").single();
|
|
13749
|
+
if (data) {
|
|
13750
|
+
await supabase.from("thread_replies").insert({
|
|
13751
|
+
thread_id: data.id,
|
|
13752
|
+
author: AGENT_AUTHOR,
|
|
13753
|
+
body: introOpening(ch.name)
|
|
13754
|
+
});
|
|
13755
|
+
return data.id;
|
|
13756
|
+
}
|
|
13757
|
+
return error2?.code === "23505" ? (await existing()).data?.id ?? null : null;
|
|
13758
|
+
}
|
|
13759
|
+
|
|
13760
|
+
// ../api/src/dashboard/channel-members-routes.ts
|
|
13761
|
+
import { Hono as Hono26 } from "hono";
|
|
13430
13762
|
async function isChannelAdmin(ctx, channelId) {
|
|
13431
13763
|
if (ctx.role === "admin") return true;
|
|
13432
13764
|
const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
|
|
@@ -13435,7 +13767,7 @@ async function isChannelAdmin(ctx, channelId) {
|
|
|
13435
13767
|
return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
|
|
13436
13768
|
}
|
|
13437
13769
|
function createChannelMembersRouter() {
|
|
13438
|
-
const router = new
|
|
13770
|
+
const router = new Hono26();
|
|
13439
13771
|
router.get("/:id/members", async (c) => {
|
|
13440
13772
|
const ctx = await resolveAuthContext(c);
|
|
13441
13773
|
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
@@ -13505,7 +13837,7 @@ function createChannelMembersRouter() {
|
|
|
13505
13837
|
}
|
|
13506
13838
|
|
|
13507
13839
|
// ../api/src/dashboard/memories-routes.ts
|
|
13508
|
-
import { Hono as
|
|
13840
|
+
import { Hono as Hono27 } from "hono";
|
|
13509
13841
|
async function readPatch(c) {
|
|
13510
13842
|
const body = await c.req.json().catch(() => ({}));
|
|
13511
13843
|
const patch = {};
|
|
@@ -13533,7 +13865,7 @@ var requireAgentMemory = async (c, next) => {
|
|
|
13533
13865
|
await next();
|
|
13534
13866
|
};
|
|
13535
13867
|
function createMemoriesRouter() {
|
|
13536
|
-
const router = new
|
|
13868
|
+
const router = new Hono27();
|
|
13537
13869
|
for (const path of [
|
|
13538
13870
|
"/:id/memories",
|
|
13539
13871
|
"/:id/memories/*",
|
|
@@ -13606,7 +13938,7 @@ function createMemoriesRouter() {
|
|
|
13606
13938
|
return router;
|
|
13607
13939
|
}
|
|
13608
13940
|
function createScopedMemoriesRouter() {
|
|
13609
|
-
const router = new
|
|
13941
|
+
const router = new Hono27();
|
|
13610
13942
|
router.use("*", requireAgentMemory);
|
|
13611
13943
|
const workspace = (c) => ({
|
|
13612
13944
|
scope: "workspace",
|
|
@@ -13665,14 +13997,244 @@ function createScopedMemoriesRouter() {
|
|
|
13665
13997
|
return router;
|
|
13666
13998
|
}
|
|
13667
13999
|
|
|
14000
|
+
// ../api/src/dashboard/session-import-routes.ts
|
|
14001
|
+
import { Hono as Hono28 } from "hono";
|
|
14002
|
+
var MAX_FOLDERS2 = 20;
|
|
14003
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
14004
|
+
var IMPORT_RANGES = ["all", "7d", "latest5"];
|
|
14005
|
+
async function ask(agent, req) {
|
|
14006
|
+
const res = await agent.sessionImportRequest?.(req);
|
|
14007
|
+
return res && res.op === req.op ? res : null;
|
|
14008
|
+
}
|
|
14009
|
+
function hostUnavailable2(c, agent) {
|
|
14010
|
+
return c.json(
|
|
14011
|
+
{
|
|
14012
|
+
error: "host offline",
|
|
14013
|
+
reason: agent.getHostCallFailure?.("sessionImportRequest") ?? "offline"
|
|
14014
|
+
},
|
|
14015
|
+
503
|
|
14016
|
+
);
|
|
14017
|
+
}
|
|
14018
|
+
function pickSessions(sessions2, range, now = Date.now()) {
|
|
14019
|
+
if (range === "latest5") return sessions2.slice(0, 5);
|
|
14020
|
+
if (range === "7d") return sessions2.filter((s) => s.lastUsed >= now - 7 * DAY_MS);
|
|
14021
|
+
return sessions2;
|
|
14022
|
+
}
|
|
14023
|
+
function channelForFolder(folder, channels) {
|
|
14024
|
+
return folder.linkedAs ? channels.find((ch) => ch.name === folder.linkedAs && !ch.archivedAt) : void 0;
|
|
14025
|
+
}
|
|
14026
|
+
async function importRows(channelIds) {
|
|
14027
|
+
if (!channelIds.length) return [];
|
|
14028
|
+
const { data, error: error2 } = await getSupabaseClient().from("session_imports").select("channel_id, agent, session_id, thread_id, last_message_uuid, message_count").in("channel_id", channelIds);
|
|
14029
|
+
if (error2) throw new Error(error2.message);
|
|
14030
|
+
return data ?? [];
|
|
14031
|
+
}
|
|
14032
|
+
async function importableFolders(folders, channels) {
|
|
14033
|
+
const mapped = folders.map((f) => ({ folder: f, channel: channelForFolder(f, channels) }));
|
|
14034
|
+
const rows = await importRows(mapped.flatMap((m) => m.channel ? [m.channel.id] : []));
|
|
14035
|
+
const byKey = new Map(rows.map((r) => [rowKey(r.channel_id, r.agent, r.session_id), r]));
|
|
14036
|
+
return mapped.map(({ folder, channel }) => {
|
|
14037
|
+
const sessions2 = folder.sessions.flatMap((s) => {
|
|
14038
|
+
const row = channel ? byKey.get(rowKey(channel.id, s.agent, s.sessionId)) : void 0;
|
|
14039
|
+
return s.bound && !row ? [] : [{ ...s, row }];
|
|
14040
|
+
});
|
|
14041
|
+
return { folder: { ...folder, sessions: sessions2 }, channel };
|
|
14042
|
+
}).filter((m) => m.folder.sessions.length);
|
|
14043
|
+
}
|
|
14044
|
+
var rowKey = (channelId, agent, sessionId) => `${channelId}:${agent}:${sessionId}`;
|
|
14045
|
+
function replyRows(threadId, messages, author, fallbackAt) {
|
|
14046
|
+
return messages.map((m, i) => ({
|
|
14047
|
+
thread_id: threadId,
|
|
14048
|
+
author: m.role === "user" ? author : AGENT_AUTHOR,
|
|
14049
|
+
body: m.text,
|
|
14050
|
+
created_at: m.at ?? new Date(fallbackAt + i).toISOString()
|
|
14051
|
+
}));
|
|
14052
|
+
}
|
|
14053
|
+
async function syncSession(agent, channelId, session, author, existing) {
|
|
14054
|
+
const supabase = getSupabaseClient();
|
|
14055
|
+
const read = await ask(agent, {
|
|
14056
|
+
op: "read",
|
|
14057
|
+
agent: session.agent,
|
|
14058
|
+
sessionId: session.sessionId,
|
|
14059
|
+
...existing?.last_message_uuid ? { afterUuid: existing.last_message_uuid } : {}
|
|
14060
|
+
});
|
|
14061
|
+
if (!read) return null;
|
|
14062
|
+
const messages = read.messages ?? [];
|
|
14063
|
+
if (!messages.length) return 0;
|
|
14064
|
+
const last = messages[messages.length - 1];
|
|
14065
|
+
if (existing) {
|
|
14066
|
+
const { error: error3 } = await supabase.from("thread_replies").insert(replyRows(existing.thread_id, messages, author, Date.now()));
|
|
14067
|
+
if (error3) throw new Error(error3.message);
|
|
14068
|
+
await supabase.from("session_imports").update({
|
|
14069
|
+
last_message_uuid: last.uuid,
|
|
14070
|
+
message_count: existing.message_count + messages.length,
|
|
14071
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
14072
|
+
}).eq("channel_id", channelId).eq("agent", session.agent).eq("session_id", session.sessionId);
|
|
14073
|
+
return messages.length;
|
|
14074
|
+
}
|
|
14075
|
+
const startedAt = messages[0].at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
14076
|
+
const { data: thread, error: threadError } = await supabase.from("threads").insert({
|
|
14077
|
+
channel_id: channelId,
|
|
14078
|
+
title: null,
|
|
14079
|
+
author,
|
|
14080
|
+
imported_agent: session.agent,
|
|
14081
|
+
created_at: startedAt
|
|
14082
|
+
}).select("id").single();
|
|
14083
|
+
if (threadError || !thread) throw new Error(threadError?.message ?? "could not create thread");
|
|
14084
|
+
const threadId = thread.id;
|
|
14085
|
+
const { error: claimError } = await supabase.from("session_imports").insert({
|
|
14086
|
+
channel_id: channelId,
|
|
14087
|
+
agent: session.agent,
|
|
14088
|
+
session_id: session.sessionId,
|
|
14089
|
+
thread_id: threadId,
|
|
14090
|
+
last_message_uuid: last.uuid,
|
|
14091
|
+
message_count: messages.length
|
|
14092
|
+
});
|
|
14093
|
+
if (claimError) {
|
|
14094
|
+
await supabase.from("threads").delete().eq("id", threadId);
|
|
14095
|
+
if (claimError.code === "23505") return 0;
|
|
14096
|
+
throw new Error(claimError.message);
|
|
14097
|
+
}
|
|
14098
|
+
const { error: error2 } = await supabase.from("thread_replies").insert(replyRows(threadId, messages, author, Date.parse(startedAt)));
|
|
14099
|
+
if (error2) throw new Error(error2.message);
|
|
14100
|
+
await ask(agent, {
|
|
14101
|
+
op: "bind",
|
|
14102
|
+
items: [{ channelId, threadId, agent: session.agent, sessionId: session.sessionId }]
|
|
14103
|
+
});
|
|
14104
|
+
return messages.length;
|
|
14105
|
+
}
|
|
14106
|
+
function createSessionImportRouter(agent) {
|
|
14107
|
+
const app = new Hono28();
|
|
14108
|
+
const scanFolders = async () => {
|
|
14109
|
+
const scan = await ask(agent, { op: "scan", slug: getProfile() });
|
|
14110
|
+
return scan && importableFolders(scan.folders, getProfileChannelRegistry());
|
|
14111
|
+
};
|
|
14112
|
+
const channelParam = (c) => getProfileChannelRegistry().find((ch) => ch.id === c.req.param("channelId"));
|
|
14113
|
+
app.get("/scan", async (c) => {
|
|
14114
|
+
const ctx = await resolveAuthContext(c);
|
|
14115
|
+
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
14116
|
+
const folders = await scanFolders();
|
|
14117
|
+
if (!folders) return hostUnavailable2(c, agent);
|
|
14118
|
+
return c.json({
|
|
14119
|
+
folders: folders.map(({ folder, channel }) => ({
|
|
14120
|
+
...folder,
|
|
14121
|
+
channel: channel ? { id: channel.id, name: channel.name } : null,
|
|
14122
|
+
sessions: folder.sessions.map(({ row, bound: _bound, ...s }) => ({
|
|
14123
|
+
...s,
|
|
14124
|
+
imported: !!row
|
|
14125
|
+
}))
|
|
14126
|
+
}))
|
|
14127
|
+
});
|
|
14128
|
+
});
|
|
14129
|
+
app.post("/import", async (c) => {
|
|
14130
|
+
const ctx = await resolveAuthContext(c);
|
|
14131
|
+
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
14132
|
+
const body = await c.req.json().catch(() => ({}));
|
|
14133
|
+
const picks = (Array.isArray(body.folders) ? body.folders : []).filter((f) => typeof f?.path === "string").map((f) => ({
|
|
14134
|
+
path: f.path,
|
|
14135
|
+
range: IMPORT_RANGES.includes(f.range ?? "") ? f.range : "all"
|
|
14136
|
+
}));
|
|
14137
|
+
if (!picks.length) return c.json({ error: "no folders chosen" }, 400);
|
|
14138
|
+
if (picks.length > MAX_FOLDERS2) return c.json({ error: `at most ${MAX_FOLDERS2} folders` }, 400);
|
|
14139
|
+
const folders = await scanFolders();
|
|
14140
|
+
if (!folders) return hostUnavailable2(c, agent);
|
|
14141
|
+
const offered = new Map(folders.map((m) => [m.folder.path, m]));
|
|
14142
|
+
if (picks.some((p) => !offered.has(p.path)))
|
|
14143
|
+
return c.json({ error: "folder was not offered by the scan" }, 400);
|
|
14144
|
+
const needLink = picks.filter((p) => !offered.get(p.path).channel);
|
|
14145
|
+
if (needLink.length && !isWorkspaceOwner(ctx)) {
|
|
14146
|
+
return c.json({ error: "workspace owner access required to add new folders" }, 403);
|
|
14147
|
+
}
|
|
14148
|
+
const taken = new Set(getProfileChannelRegistry().map((ch) => ch.name));
|
|
14149
|
+
const items = needLink.map((p) => ({
|
|
14150
|
+
name: uniqueChannelName(offered.get(p.path).folder.name, taken),
|
|
14151
|
+
path: p.path
|
|
14152
|
+
}));
|
|
14153
|
+
const linked = items.length ? await agent.linkProjectFolders?.(getProfile(), items) : [];
|
|
14154
|
+
if (!linked) return hostUnavailable2(c, agent);
|
|
14155
|
+
const created = /* @__PURE__ */ new Map();
|
|
14156
|
+
for (const [i, l] of linked.entries())
|
|
14157
|
+
created.set(items[i].path, await createChannel({ name: l.name, cwd: l.cwd }));
|
|
14158
|
+
const results = [];
|
|
14159
|
+
for (const p of picks) {
|
|
14160
|
+
const { folder, channel: existing } = offered.get(p.path);
|
|
14161
|
+
const channel = existing ?? created.get(p.path);
|
|
14162
|
+
let threads = 0;
|
|
14163
|
+
let messages = 0;
|
|
14164
|
+
for (const s of pickSessions(folder.sessions, p.range)) {
|
|
14165
|
+
if (s.row) continue;
|
|
14166
|
+
const n = await syncSession(agent, channel.id, s, ctx.handle, void 0);
|
|
14167
|
+
if (n === null) return hostUnavailable2(c, agent);
|
|
14168
|
+
if (n > 0) threads += 1;
|
|
14169
|
+
messages += n;
|
|
14170
|
+
}
|
|
14171
|
+
results.push({
|
|
14172
|
+
path: p.path,
|
|
14173
|
+
channel: { id: channel.id, name: channel.name },
|
|
14174
|
+
created: !existing,
|
|
14175
|
+
threads,
|
|
14176
|
+
messages
|
|
14177
|
+
});
|
|
14178
|
+
}
|
|
14179
|
+
return c.json({ results }, 201);
|
|
14180
|
+
});
|
|
14181
|
+
app.get("/channels/:channelId", async (c) => {
|
|
14182
|
+
const ctx = await resolveAuthContext(c);
|
|
14183
|
+
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
14184
|
+
const channel = channelParam(c);
|
|
14185
|
+
if (!channel) return c.json({ error: "channel not found" }, 404);
|
|
14186
|
+
const folders = await scanFolders();
|
|
14187
|
+
if (!folders) return hostUnavailable2(c, agent);
|
|
14188
|
+
const folder = folders.find((m) => m.channel?.id === channel.id)?.folder;
|
|
14189
|
+
const sessions2 = [];
|
|
14190
|
+
for (const { row, bound: _bound, ...s } of folder?.sessions ?? []) {
|
|
14191
|
+
let newMessages = 0;
|
|
14192
|
+
if (row) {
|
|
14193
|
+
const read = await ask(agent, {
|
|
14194
|
+
op: "read",
|
|
14195
|
+
agent: s.agent,
|
|
14196
|
+
sessionId: s.sessionId,
|
|
14197
|
+
...row.last_message_uuid ? { afterUuid: row.last_message_uuid } : {}
|
|
14198
|
+
});
|
|
14199
|
+
newMessages = read?.messages?.length ?? 0;
|
|
14200
|
+
}
|
|
14201
|
+
const status = !row ? "not-imported" : newMessages ? "new" : "synced";
|
|
14202
|
+
sessions2.push({ ...s, threadId: row?.thread_id ?? null, status, newMessages });
|
|
14203
|
+
}
|
|
14204
|
+
return c.json({ folder: folder?.path ?? null, sessions: sessions2 });
|
|
14205
|
+
});
|
|
14206
|
+
app.post("/channels/:channelId/sync", async (c) => {
|
|
14207
|
+
const ctx = await resolveAuthContext(c);
|
|
14208
|
+
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
14209
|
+
const channel = channelParam(c);
|
|
14210
|
+
if (!channel) return c.json({ error: "channel not found" }, 404);
|
|
14211
|
+
const body = await c.req.json().catch(() => ({}));
|
|
14212
|
+
const only = Array.isArray(body.sessionIds) ? new Set(body.sessionIds.filter((s) => typeof s === "string")) : null;
|
|
14213
|
+
const folders = await scanFolders();
|
|
14214
|
+
if (!folders) return hostUnavailable2(c, agent);
|
|
14215
|
+
const folder = folders.find((m) => m.channel?.id === channel.id)?.folder;
|
|
14216
|
+
let threads = 0;
|
|
14217
|
+
let messages = 0;
|
|
14218
|
+
for (const s of folder?.sessions ?? []) {
|
|
14219
|
+
if (only && !only.has(s.sessionId)) continue;
|
|
14220
|
+
const n = await syncSession(agent, channel.id, s, ctx.handle, s.row);
|
|
14221
|
+
if (n === null) return hostUnavailable2(c, agent);
|
|
14222
|
+
if (!s.row && n > 0) threads += 1;
|
|
14223
|
+
messages += n;
|
|
14224
|
+
}
|
|
14225
|
+
return c.json({ threads, messages });
|
|
14226
|
+
});
|
|
14227
|
+
return app;
|
|
14228
|
+
}
|
|
14229
|
+
|
|
13668
14230
|
// ../api/src/dashboard/host-status-signal.ts
|
|
13669
14231
|
var HOST_STATUS_CHANNEL = "host-status";
|
|
13670
14232
|
var channelReady = null;
|
|
13671
14233
|
function getHostStatusChannel() {
|
|
13672
|
-
channelReady ??= new Promise((
|
|
14234
|
+
channelReady ??= new Promise((resolve8) => {
|
|
13673
14235
|
const channel = getSupabaseClient().channel(HOST_STATUS_CHANNEL);
|
|
13674
14236
|
channel.subscribe((state) => {
|
|
13675
|
-
if (state === "SUBSCRIBED")
|
|
14237
|
+
if (state === "SUBSCRIBED") resolve8(channel);
|
|
13676
14238
|
});
|
|
13677
14239
|
});
|
|
13678
14240
|
return channelReady;
|
|
@@ -13837,6 +14399,8 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
13837
14399
|
});
|
|
13838
14400
|
api.route("/channels", createDocShareWriteRouter(agent));
|
|
13839
14401
|
api.route("/onboarding", createOnboardingRouter(agent));
|
|
14402
|
+
api.route("/session-import", createSessionImportRouter(agent));
|
|
14403
|
+
api.route("/channels", createChannelGreetingRouter(agent));
|
|
13840
14404
|
api.route("/channels", createChannelMembersRouter());
|
|
13841
14405
|
api.route("/channels", createMemoriesRouter());
|
|
13842
14406
|
api.route("/memories", createScopedMemoriesRouter());
|
|
@@ -14080,7 +14644,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14080
14644
|
return c.json({ error: "logo must be 2MB or smaller" }, 400);
|
|
14081
14645
|
}
|
|
14082
14646
|
const original = Buffer.from(await file.arrayBuffer());
|
|
14083
|
-
const resized = await
|
|
14647
|
+
const resized = await sharp6(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
|
|
14084
14648
|
const logo = `data:image/png;base64,${resized.toString("base64")}`;
|
|
14085
14649
|
return c.json(await saveWorkspaceBranding({ logo }));
|
|
14086
14650
|
});
|
|
@@ -14090,7 +14654,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
14090
14654
|
// ../api/src/dashboard/device-auth-routes.ts
|
|
14091
14655
|
import { verify } from "crypto";
|
|
14092
14656
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
14093
|
-
import { Hono as
|
|
14657
|
+
import { Hono as Hono29 } from "hono";
|
|
14094
14658
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14095
14659
|
function cleanText(value, max) {
|
|
14096
14660
|
if (typeof value !== "string") return null;
|
|
@@ -14138,7 +14702,7 @@ async function boundWorkspaceIds(publicKey, userId) {
|
|
|
14138
14702
|
return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
|
|
14139
14703
|
}
|
|
14140
14704
|
function createDeviceAuthRouter() {
|
|
14141
|
-
const app = new
|
|
14705
|
+
const app = new Hono29();
|
|
14142
14706
|
app.post("/code", async (c) => {
|
|
14143
14707
|
const body = await readBody(c);
|
|
14144
14708
|
const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
|
|
@@ -14457,7 +15021,7 @@ var workspaceRef = () => ({ kind: "workspace" });
|
|
|
14457
15021
|
var channelRef = (c) => ({ kind: "channel", channelId: c.req.param("id") });
|
|
14458
15022
|
|
|
14459
15023
|
// ../api/src/dashboard/output-styles-routes.ts
|
|
14460
|
-
import { Hono as
|
|
15024
|
+
import { Hono as Hono30 } from "hono";
|
|
14461
15025
|
function requireAdmin3(auth) {
|
|
14462
15026
|
return auth?.role === "admin" ? null : "admin access required";
|
|
14463
15027
|
}
|
|
@@ -14477,7 +15041,7 @@ function failure(c, err) {
|
|
|
14477
15041
|
return c.json({ error: message }, err instanceof OutputStyleConflictError ? 409 : 500);
|
|
14478
15042
|
}
|
|
14479
15043
|
function createOutputStylesRouter() {
|
|
14480
|
-
const app = new
|
|
15044
|
+
const app = new Hono30();
|
|
14481
15045
|
app.get("/", (c) => {
|
|
14482
15046
|
const auth = c.get("auth");
|
|
14483
15047
|
const denied = requireAdmin3(auth);
|
|
@@ -14569,7 +15133,7 @@ function createOutputStylesRouter() {
|
|
|
14569
15133
|
return app;
|
|
14570
15134
|
}
|
|
14571
15135
|
async function handleOutputStylesRequest(req) {
|
|
14572
|
-
const app = new
|
|
15136
|
+
const app = new Hono30();
|
|
14573
15137
|
app.use("*", async (c, next) => {
|
|
14574
15138
|
c.set("auth", { role: "admin" });
|
|
14575
15139
|
await next();
|
|
@@ -14590,7 +15154,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
14590
15154
|
|
|
14591
15155
|
// ../api/src/dashboard/api.ts
|
|
14592
15156
|
function createGatedRouter(agent, register) {
|
|
14593
|
-
const api = new
|
|
15157
|
+
const api = new Hono31();
|
|
14594
15158
|
api.use("*", async (c, next) => {
|
|
14595
15159
|
const known = await listKnownWorkspaces();
|
|
14596
15160
|
const fromHost = profileFromHost(trustedHost(c), known);
|
|
@@ -14639,11 +15203,11 @@ function createGatedRouter(agent, register) {
|
|
|
14639
15203
|
|
|
14640
15204
|
// src/dashboard/host-api.ts
|
|
14641
15205
|
import { existsSync as existsSync30, readFileSync as readFileSync24, statSync as statSync8 } from "fs";
|
|
14642
|
-
import
|
|
15206
|
+
import sharp7 from "sharp";
|
|
14643
15207
|
|
|
14644
15208
|
// src/dashboard/channels-routes.ts
|
|
14645
15209
|
import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
|
|
14646
|
-
import { Hono as
|
|
15210
|
+
import { Hono as Hono32 } from "hono";
|
|
14647
15211
|
function coerceAccess(raw) {
|
|
14648
15212
|
if (raw === "open" || raw === "*" || raw === "all") return raw;
|
|
14649
15213
|
if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) return raw;
|
|
@@ -14706,7 +15270,7 @@ function toResponse3(channel) {
|
|
|
14706
15270
|
};
|
|
14707
15271
|
}
|
|
14708
15272
|
function createChannelWriteRouter() {
|
|
14709
|
-
const app = new
|
|
15273
|
+
const app = new Hono32();
|
|
14710
15274
|
app.post("/", async (c) => {
|
|
14711
15275
|
const body = await c.req.json().catch(() => ({}));
|
|
14712
15276
|
const name = typeof body["name"] === "string" ? body["name"].trim() : "";
|
|
@@ -14846,7 +15410,7 @@ import {
|
|
|
14846
15410
|
} from "fs";
|
|
14847
15411
|
import { Readable } from "stream";
|
|
14848
15412
|
import { basename as basename8, dirname as dirname8, extname, join as join23, relative, resolve as resolve5, sep } from "path";
|
|
14849
|
-
import { Hono as
|
|
15413
|
+
import { Hono as Hono33 } from "hono";
|
|
14850
15414
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
14851
15415
|
|
|
14852
15416
|
// src/dashboard/docs-watcher.ts
|
|
@@ -15148,7 +15712,7 @@ function needsRefresh(expiresAt, now = Date.now()) {
|
|
|
15148
15712
|
if (!expiresAt) return false;
|
|
15149
15713
|
return new Date(expiresAt).getTime() - now < REFRESH_SKEW_MS;
|
|
15150
15714
|
}
|
|
15151
|
-
var sleep3 = (ms) => new Promise((
|
|
15715
|
+
var sleep3 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
15152
15716
|
async function refreshConnection(params) {
|
|
15153
15717
|
const { workspaceId, connection, provider } = params;
|
|
15154
15718
|
const deps = params.deps ?? {};
|
|
@@ -15482,7 +16046,7 @@ ${req.user}`,
|
|
|
15482
16046
|
};
|
|
15483
16047
|
}
|
|
15484
16048
|
function run3(inv, cwd) {
|
|
15485
|
-
return new Promise((
|
|
16049
|
+
return new Promise((resolve8) => {
|
|
15486
16050
|
const child = spawn4(inv.command, inv.args, {
|
|
15487
16051
|
cwd,
|
|
15488
16052
|
env: inv.env,
|
|
@@ -15496,14 +16060,14 @@ function run3(inv, cwd) {
|
|
|
15496
16060
|
child.on("error", (err) => {
|
|
15497
16061
|
clearTimeout(timer2);
|
|
15498
16062
|
console.error(`[memory-extraction] ${inv.command} failed to start:`, err.message);
|
|
15499
|
-
|
|
16063
|
+
resolve8({ ok: false });
|
|
15500
16064
|
});
|
|
15501
16065
|
child.on("close", (code) => {
|
|
15502
16066
|
clearTimeout(timer2);
|
|
15503
16067
|
if (code !== 0) {
|
|
15504
16068
|
const detail = (stderr || stdout).trim().split("\n").at(-1) ?? "";
|
|
15505
16069
|
console.error(`[memory-extraction] ${inv.command} exited ${code}: ${detail.slice(0, 200)}`);
|
|
15506
|
-
|
|
16070
|
+
resolve8({ ok: false });
|
|
15507
16071
|
return;
|
|
15508
16072
|
}
|
|
15509
16073
|
let raw = stdout;
|
|
@@ -15514,7 +16078,7 @@ function run3(inv, cwd) {
|
|
|
15514
16078
|
raw = "";
|
|
15515
16079
|
}
|
|
15516
16080
|
}
|
|
15517
|
-
|
|
16081
|
+
resolve8({ ok: true, raw });
|
|
15518
16082
|
});
|
|
15519
16083
|
child.stdin.end(inv.stdin);
|
|
15520
16084
|
});
|
|
@@ -15576,7 +16140,7 @@ Rules:
|
|
|
15576
16140
|
- No preamble like "Summary:" \u2014 start directly with the summary text.`;
|
|
15577
16141
|
var HAIKU_TIMEOUT_MS = 6e4;
|
|
15578
16142
|
function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
|
|
15579
|
-
return new Promise((
|
|
16143
|
+
return new Promise((resolve8, reject) => {
|
|
15580
16144
|
const args = [
|
|
15581
16145
|
"--print",
|
|
15582
16146
|
"--model",
|
|
@@ -15627,7 +16191,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
|
|
|
15627
16191
|
reject(new Error("claude -p --model haiku returned empty output"));
|
|
15628
16192
|
return;
|
|
15629
16193
|
}
|
|
15630
|
-
|
|
16194
|
+
resolve8(text);
|
|
15631
16195
|
});
|
|
15632
16196
|
});
|
|
15633
16197
|
}
|
|
@@ -16023,6 +16587,15 @@ var ChatAgent = class {
|
|
|
16023
16587
|
* restart picks up edits. */
|
|
16024
16588
|
workspaceRoots = null;
|
|
16025
16589
|
/** Fire-and-forget entry point, called by chat-routes after every write. */
|
|
16590
|
+
/** The CLI of the session already bound to this thread, if any. */
|
|
16591
|
+
boundRunner(channelId, threadId) {
|
|
16592
|
+
const store = this.openSessionStore();
|
|
16593
|
+
try {
|
|
16594
|
+
return store.getSession(`chat:${channelId}`, threadId)?.runner;
|
|
16595
|
+
} finally {
|
|
16596
|
+
store.close();
|
|
16597
|
+
}
|
|
16598
|
+
}
|
|
16026
16599
|
handleMessage(msg) {
|
|
16027
16600
|
if (msg.author === AGENT_AUTHOR) return;
|
|
16028
16601
|
this.seenReplies.add(msg.replyId);
|
|
@@ -16403,7 +16976,7 @@ var ChatAgent = class {
|
|
|
16403
16976
|
);
|
|
16404
16977
|
ensureLogicalCwdDir(baseConfig.cwd);
|
|
16405
16978
|
const pickedModel = msg.model?.trim();
|
|
16406
|
-
let activeRunner = pickedModel ? runnerForModel(pickedModel) : defaultRunner;
|
|
16979
|
+
let activeRunner = pickedModel ? runnerForModel(pickedModel) : this.boundRunner(thread.channel_id, msg.threadId) ?? defaultRunner;
|
|
16407
16980
|
let channelConfig = pickedModel ? {
|
|
16408
16981
|
...baseConfig,
|
|
16409
16982
|
...activeRunner === "codex" ? { model: pickedModel, codexModel: pickedModel } : { model: pickedModel }
|
|
@@ -16709,22 +17282,22 @@ ${msg.body}` : msg.body;
|
|
|
16709
17282
|
let ready;
|
|
16710
17283
|
try {
|
|
16711
17284
|
channel = this.supabase().channel(`agent-thinking:${threadId}`);
|
|
16712
|
-
ready = new Promise((
|
|
17285
|
+
ready = new Promise((resolve8) => {
|
|
16713
17286
|
const timeout = setTimeout(() => {
|
|
16714
17287
|
console.error(`[chat-agent] thinking-channel ${threadId}: subscribe timed out after 10s`);
|
|
16715
|
-
|
|
17288
|
+
resolve8(false);
|
|
16716
17289
|
}, 1e4);
|
|
16717
17290
|
channel.subscribe((status, err) => {
|
|
16718
17291
|
if (status === "SUBSCRIBED") {
|
|
16719
17292
|
clearTimeout(timeout);
|
|
16720
|
-
|
|
17293
|
+
resolve8(true);
|
|
16721
17294
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
16722
17295
|
console.error(
|
|
16723
17296
|
`[chat-agent] thinking-channel ${threadId}: ${status}`,
|
|
16724
17297
|
err?.message ?? ""
|
|
16725
17298
|
);
|
|
16726
17299
|
clearTimeout(timeout);
|
|
16727
|
-
|
|
17300
|
+
resolve8(false);
|
|
16728
17301
|
}
|
|
16729
17302
|
});
|
|
16730
17303
|
});
|
|
@@ -16771,19 +17344,19 @@ ${msg.body}` : msg.body;
|
|
|
16771
17344
|
let ready;
|
|
16772
17345
|
try {
|
|
16773
17346
|
channel = this.supabase().channel("channel-activity");
|
|
16774
|
-
ready = new Promise((
|
|
17347
|
+
ready = new Promise((resolve8) => {
|
|
16775
17348
|
const timeout = setTimeout(() => {
|
|
16776
17349
|
console.error("[chat-agent] channel-activity: subscribe timed out after 10s");
|
|
16777
|
-
|
|
17350
|
+
resolve8(false);
|
|
16778
17351
|
}, 1e4);
|
|
16779
17352
|
channel.subscribe((status, err) => {
|
|
16780
17353
|
if (status === "SUBSCRIBED") {
|
|
16781
17354
|
clearTimeout(timeout);
|
|
16782
|
-
|
|
17355
|
+
resolve8(true);
|
|
16783
17356
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
16784
17357
|
console.error(`[chat-agent] channel-activity: ${status}`, err?.message ?? "");
|
|
16785
17358
|
clearTimeout(timeout);
|
|
16786
|
-
|
|
17359
|
+
resolve8(false);
|
|
16787
17360
|
}
|
|
16788
17361
|
});
|
|
16789
17362
|
});
|
|
@@ -17727,7 +18300,7 @@ function deleteDocEntry(root, path) {
|
|
|
17727
18300
|
};
|
|
17728
18301
|
}
|
|
17729
18302
|
function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
17730
|
-
const app = new
|
|
18303
|
+
const app = new Hono33();
|
|
17731
18304
|
app.use("/:id/docs", gate);
|
|
17732
18305
|
app.use("/:id/docs/*", gate);
|
|
17733
18306
|
app.get("/:id/docs", (c) => {
|
|
@@ -18065,7 +18638,7 @@ function withDocLinkBridge(html) {
|
|
|
18065
18638
|
// src/dashboard/skills-routes-host.ts
|
|
18066
18639
|
import { existsSync as existsSync27, statSync as statSync7 } from "fs";
|
|
18067
18640
|
import { dirname as dirname9 } from "path";
|
|
18068
|
-
import { Hono as
|
|
18641
|
+
import { Hono as Hono34 } from "hono";
|
|
18069
18642
|
function workspaceScope() {
|
|
18070
18643
|
const claudeConfigDir = profileClaudeConfigDir();
|
|
18071
18644
|
const cwd = defaultClaudeCwd();
|
|
@@ -18171,12 +18744,12 @@ var diskBackend = {
|
|
|
18171
18744
|
write: writeSkillAndSync
|
|
18172
18745
|
};
|
|
18173
18746
|
function createWorkspaceSkillsRouter() {
|
|
18174
|
-
const app = new
|
|
18747
|
+
const app = new Hono34();
|
|
18175
18748
|
mountSkillRoutes(app, "/skills", workspaceRef, diskBackend);
|
|
18176
18749
|
return app;
|
|
18177
18750
|
}
|
|
18178
18751
|
function createChannelSkillsRouter() {
|
|
18179
|
-
const app = new
|
|
18752
|
+
const app = new Hono34();
|
|
18180
18753
|
mountSkillRoutes(app, "/:id/skill-files", channelRef, diskBackend);
|
|
18181
18754
|
return app;
|
|
18182
18755
|
}
|
|
@@ -18311,7 +18884,7 @@ import { randomUUID as randomUUID5 } from "crypto";
|
|
|
18311
18884
|
import { chmodSync, existsSync as existsSync29, mkdirSync as mkdirSync12, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
|
|
18312
18885
|
import { createRequire } from "module";
|
|
18313
18886
|
import { dirname as dirname10, join as join25 } from "path";
|
|
18314
|
-
import { Hono as
|
|
18887
|
+
import { Hono as Hono35 } from "hono";
|
|
18315
18888
|
var SESSION_TTL_MS = 10 * 6e4;
|
|
18316
18889
|
var URL_WAIT_MS = 3e4;
|
|
18317
18890
|
var TOKEN_WAIT_MS = 6e4;
|
|
@@ -18360,13 +18933,13 @@ function track(session) {
|
|
|
18360
18933
|
return [id, s];
|
|
18361
18934
|
}
|
|
18362
18935
|
function waitFor(s, parse, ms) {
|
|
18363
|
-
return new Promise((
|
|
18936
|
+
return new Promise((resolve8) => {
|
|
18364
18937
|
const started = Date.now();
|
|
18365
18938
|
const tick = setInterval(() => {
|
|
18366
18939
|
const v = parse();
|
|
18367
18940
|
if (v !== void 0 || s.status !== "pending" || Date.now() - started > ms) {
|
|
18368
18941
|
clearInterval(tick);
|
|
18369
|
-
|
|
18942
|
+
resolve8(v);
|
|
18370
18943
|
}
|
|
18371
18944
|
}, 200);
|
|
18372
18945
|
});
|
|
@@ -18468,7 +19041,7 @@ function failure2(c, err) {
|
|
|
18468
19041
|
return c.json({ error: err.message }, 500);
|
|
18469
19042
|
}
|
|
18470
19043
|
function createProviderLoginRouter() {
|
|
18471
|
-
const app = new
|
|
19044
|
+
const app = new Hono35();
|
|
18472
19045
|
app.use("*", async (c, next) => {
|
|
18473
19046
|
const denied = requireAdmin4(c.get("auth"));
|
|
18474
19047
|
if (denied) return c.json({ error: denied }, 403);
|
|
@@ -18512,7 +19085,7 @@ function createProviderLoginRouter() {
|
|
|
18512
19085
|
return app;
|
|
18513
19086
|
}
|
|
18514
19087
|
async function handleProviderLoginRequest(req) {
|
|
18515
|
-
const app = new
|
|
19088
|
+
const app = new Hono35();
|
|
18516
19089
|
app.use("*", async (c, next) => {
|
|
18517
19090
|
c.set("auth", { role: "admin" });
|
|
18518
19091
|
await next();
|
|
@@ -18620,7 +19193,7 @@ function registerHostRoutes(api) {
|
|
|
18620
19193
|
return c.json({ error: "avatar must be 2MB or smaller" }, 400);
|
|
18621
19194
|
}
|
|
18622
19195
|
const original = Buffer.from(await file.arrayBuffer());
|
|
18623
|
-
const resized = await
|
|
19196
|
+
const resized = await sharp7(original).resize({ width: LOGO_SIZE, height: LOGO_SIZE, fit: "cover" }).png().toBuffer();
|
|
18624
19197
|
const avatar = `data:image/png;base64,${resized.toString("base64")}`;
|
|
18625
19198
|
const name = c.req.param("name");
|
|
18626
19199
|
if (name === BOT_PROFILE_KEY) return c.json(await saveBotIdentity({ avatar }));
|
|
@@ -18664,18 +19237,253 @@ function createHostApiRouter(agent = {}) {
|
|
|
18664
19237
|
|
|
18665
19238
|
// src/dashboard/agent-bridge-host.ts
|
|
18666
19239
|
import { sign as signRaw, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
18667
|
-
import { existsSync as
|
|
19240
|
+
import { existsSync as existsSync32, readFileSync as readFileSync27 } from "fs";
|
|
18668
19241
|
import dotenv2 from "dotenv";
|
|
18669
19242
|
import { WebSocket as WebSocket2 } from "ws";
|
|
18670
19243
|
|
|
19244
|
+
// src/dashboard/session-import-host.ts
|
|
19245
|
+
import {
|
|
19246
|
+
closeSync as closeSync3,
|
|
19247
|
+
existsSync as existsSync31,
|
|
19248
|
+
openSync as openSync3,
|
|
19249
|
+
readdirSync as readdirSync6,
|
|
19250
|
+
readFileSync as readFileSync25,
|
|
19251
|
+
readSync as readSync3,
|
|
19252
|
+
realpathSync as realpathSync5
|
|
19253
|
+
} from "fs";
|
|
19254
|
+
import { basename as basename9, join as join26, resolve as resolve7 } from "path";
|
|
19255
|
+
var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
19256
|
+
var HEAD_BYTES = 64 * 1024;
|
|
19257
|
+
var TITLE_MAX = 80;
|
|
19258
|
+
function readHead2(file) {
|
|
19259
|
+
let fd;
|
|
19260
|
+
try {
|
|
19261
|
+
fd = openSync3(file, "r");
|
|
19262
|
+
const buf = Buffer.alloc(HEAD_BYTES);
|
|
19263
|
+
return buf.toString("utf-8", 0, readSync3(fd, buf, 0, buf.length, 0));
|
|
19264
|
+
} catch {
|
|
19265
|
+
return "";
|
|
19266
|
+
} finally {
|
|
19267
|
+
if (fd !== void 0) closeSync3(fd);
|
|
19268
|
+
}
|
|
19269
|
+
}
|
|
19270
|
+
function parseLines(text) {
|
|
19271
|
+
const out = [];
|
|
19272
|
+
for (const line of text.split("\n")) {
|
|
19273
|
+
if (!line) continue;
|
|
19274
|
+
try {
|
|
19275
|
+
out.push(JSON.parse(line));
|
|
19276
|
+
} catch {
|
|
19277
|
+
}
|
|
19278
|
+
}
|
|
19279
|
+
return out;
|
|
19280
|
+
}
|
|
19281
|
+
function cleanText2(text) {
|
|
19282
|
+
const t = text.trim();
|
|
19283
|
+
if (!t || t.startsWith("<")) return null;
|
|
19284
|
+
return t;
|
|
19285
|
+
}
|
|
19286
|
+
function blocksText(content, kinds) {
|
|
19287
|
+
if (typeof content === "string") return cleanText2(content);
|
|
19288
|
+
if (!Array.isArray(content)) return null;
|
|
19289
|
+
const parts = content.filter(
|
|
19290
|
+
(b) => kinds.includes(b?.type) && typeof b.text === "string"
|
|
19291
|
+
).map((b) => b.text);
|
|
19292
|
+
return parts.length ? cleanText2(parts.join("\n\n")) : null;
|
|
19293
|
+
}
|
|
19294
|
+
function boundSessionIds() {
|
|
19295
|
+
if (!existsSync31(paths.DB_FILE)) return /* @__PURE__ */ new Set();
|
|
19296
|
+
const store = new SessionStore(paths.DB_FILE);
|
|
19297
|
+
try {
|
|
19298
|
+
return new Set(store.listSessions().map((s) => s.sessionId));
|
|
19299
|
+
} finally {
|
|
19300
|
+
store.close();
|
|
19301
|
+
}
|
|
19302
|
+
}
|
|
19303
|
+
function claudeMessages(lines) {
|
|
19304
|
+
const out = [];
|
|
19305
|
+
for (const o of lines) {
|
|
19306
|
+
if (o["type"] !== "user" && o["type"] !== "assistant" || o["isMeta"] || o["isSidechain"])
|
|
19307
|
+
continue;
|
|
19308
|
+
if (o["entrypoint"] === "sdk-cli") continue;
|
|
19309
|
+
const role = o["type"];
|
|
19310
|
+
const text = blocksText(o["message"]?.content, ["text"]);
|
|
19311
|
+
if (!text || typeof o["uuid"] !== "string") continue;
|
|
19312
|
+
const at = typeof o["timestamp"] === "string" ? o["timestamp"] : null;
|
|
19313
|
+
const last = out[out.length - 1];
|
|
19314
|
+
if (role === "assistant" && last?.role === "assistant") {
|
|
19315
|
+
last.text += `
|
|
19316
|
+
|
|
19317
|
+
${text}`;
|
|
19318
|
+
last.uuid = o["uuid"];
|
|
19319
|
+
} else {
|
|
19320
|
+
out.push({ uuid: o["uuid"], role, text, at });
|
|
19321
|
+
}
|
|
19322
|
+
}
|
|
19323
|
+
return out;
|
|
19324
|
+
}
|
|
19325
|
+
function codexMessages(lines) {
|
|
19326
|
+
const out = [];
|
|
19327
|
+
lines.forEach((o, i) => {
|
|
19328
|
+
const p = o["payload"];
|
|
19329
|
+
if (o["type"] !== "response_item" || p?.type !== "message") return;
|
|
19330
|
+
if (p.role !== "user" && p.role !== "assistant") return;
|
|
19331
|
+
const text = blocksText(p.content, ["input_text", "output_text"]);
|
|
19332
|
+
if (!text) return;
|
|
19333
|
+
out.push({
|
|
19334
|
+
uuid: `line:${i}`,
|
|
19335
|
+
role: p.role,
|
|
19336
|
+
text,
|
|
19337
|
+
at: typeof o["timestamp"] === "string" ? o["timestamp"] : null
|
|
19338
|
+
});
|
|
19339
|
+
});
|
|
19340
|
+
return out;
|
|
19341
|
+
}
|
|
19342
|
+
function claudeSummary(file, mtime) {
|
|
19343
|
+
const head = parseLines(readHead2(file));
|
|
19344
|
+
if (head.some((o) => o["entrypoint"] === "sdk-cli")) return null;
|
|
19345
|
+
const first = claudeMessages(head).find((m) => m.role === "user");
|
|
19346
|
+
if (!first) return null;
|
|
19347
|
+
return {
|
|
19348
|
+
agent: "claude",
|
|
19349
|
+
sessionId: basename9(file, ".jsonl"),
|
|
19350
|
+
title: first.text.slice(0, TITLE_MAX),
|
|
19351
|
+
lastUsed: mtime
|
|
19352
|
+
};
|
|
19353
|
+
}
|
|
19354
|
+
function codexSummary(file, mtime) {
|
|
19355
|
+
const head = parseLines(readHead2(file));
|
|
19356
|
+
const meta = head.find((o) => o["type"] === "session_meta")?.["payload"];
|
|
19357
|
+
if (!meta?.id || !meta.cwd || meta.originator === "codex_exec") return null;
|
|
19358
|
+
const first = codexMessages(head).find((m) => m.role === "user");
|
|
19359
|
+
if (!first) return null;
|
|
19360
|
+
return {
|
|
19361
|
+
cwd: meta.cwd,
|
|
19362
|
+
session: {
|
|
19363
|
+
agent: "codex",
|
|
19364
|
+
sessionId: meta.id,
|
|
19365
|
+
title: first.text.slice(0, TITLE_MAX),
|
|
19366
|
+
lastUsed: mtime
|
|
19367
|
+
}
|
|
19368
|
+
};
|
|
19369
|
+
}
|
|
19370
|
+
function enabledAgents() {
|
|
19371
|
+
return new Set(
|
|
19372
|
+
detectRunners().filter((r) => r.installed && r.loggedIn).map((r) => r.id)
|
|
19373
|
+
);
|
|
19374
|
+
}
|
|
19375
|
+
function scanSessionFolders(slug) {
|
|
19376
|
+
const agents = enabledAgents();
|
|
19377
|
+
const bound = boundSessionIds();
|
|
19378
|
+
const since2 = Date.now() - MAX_AGE_MS;
|
|
19379
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
19380
|
+
const add = (cwd, s) => {
|
|
19381
|
+
const path = resolve7(cwd);
|
|
19382
|
+
if (isExcluded(path)) return;
|
|
19383
|
+
byPath.set(path, [
|
|
19384
|
+
...byPath.get(path) ?? [],
|
|
19385
|
+
bound.has(s.sessionId) ? { ...s, bound: true } : s
|
|
19386
|
+
]);
|
|
19387
|
+
};
|
|
19388
|
+
if (agents.has("claude")) {
|
|
19389
|
+
const root = join26(claudeDir(), "projects");
|
|
19390
|
+
let dirs = [];
|
|
19391
|
+
try {
|
|
19392
|
+
dirs = readdirSync6(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
19393
|
+
} catch {
|
|
19394
|
+
}
|
|
19395
|
+
for (const dir of dirs) {
|
|
19396
|
+
const files = jsonlFiles(join26(root, dir), 0).filter((f) => f.mtime >= since2);
|
|
19397
|
+
let cwd = null;
|
|
19398
|
+
for (const f of files) {
|
|
19399
|
+
cwd ??= cwdFromTranscript(f.file);
|
|
19400
|
+
if (cwd) break;
|
|
19401
|
+
}
|
|
19402
|
+
if (!cwd) continue;
|
|
19403
|
+
for (const f of files) {
|
|
19404
|
+
const s = claudeSummary(f.file, f.mtime);
|
|
19405
|
+
if (s) add(cwd, s);
|
|
19406
|
+
}
|
|
19407
|
+
}
|
|
19408
|
+
}
|
|
19409
|
+
if (agents.has("codex")) {
|
|
19410
|
+
for (const f of jsonlFiles(join26(codexDir(), "sessions"), 4)) {
|
|
19411
|
+
if (f.mtime < since2) continue;
|
|
19412
|
+
const s = codexSummary(f.file, f.mtime);
|
|
19413
|
+
if (s) add(s.cwd, s.session);
|
|
19414
|
+
}
|
|
19415
|
+
}
|
|
19416
|
+
const linked = linkedTargets(slug);
|
|
19417
|
+
const folders = [...byPath].map(([path, sessions2]) => {
|
|
19418
|
+
let real = path;
|
|
19419
|
+
try {
|
|
19420
|
+
real = realpathSync5(path);
|
|
19421
|
+
} catch {
|
|
19422
|
+
}
|
|
19423
|
+
sessions2.sort((a, b) => b.lastUsed - a.lastUsed);
|
|
19424
|
+
return {
|
|
19425
|
+
path,
|
|
19426
|
+
name: basename9(path),
|
|
19427
|
+
lastUsed: sessions2[0].lastUsed,
|
|
19428
|
+
sessions: sessions2,
|
|
19429
|
+
...linked.has(real) ? { linkedAs: linked.get(real) } : {}
|
|
19430
|
+
};
|
|
19431
|
+
});
|
|
19432
|
+
return folders.sort((a, b) => b.lastUsed - a.lastUsed);
|
|
19433
|
+
}
|
|
19434
|
+
function findTranscript(agent, sessionId) {
|
|
19435
|
+
if (!/^[A-Za-z0-9-]+$/.test(sessionId)) return null;
|
|
19436
|
+
if (agent === "claude") {
|
|
19437
|
+
const root = join26(claudeDir(), "projects");
|
|
19438
|
+
try {
|
|
19439
|
+
for (const e of readdirSync6(root, { withFileTypes: true })) {
|
|
19440
|
+
const file = join26(root, e.name, `${sessionId}.jsonl`);
|
|
19441
|
+
if (e.isDirectory() && existsSync31(file)) return file;
|
|
19442
|
+
}
|
|
19443
|
+
} catch {
|
|
19444
|
+
}
|
|
19445
|
+
return null;
|
|
19446
|
+
}
|
|
19447
|
+
return jsonlFiles(join26(codexDir(), "sessions"), 4).find((f) => f.file.endsWith(`${sessionId}.jsonl`))?.file ?? null;
|
|
19448
|
+
}
|
|
19449
|
+
function readSessionMessages(agent, sessionId, afterUuid) {
|
|
19450
|
+
const file = findTranscript(agent, sessionId);
|
|
19451
|
+
if (!file) return null;
|
|
19452
|
+
const lines = parseLines(readFileSync25(file, "utf-8"));
|
|
19453
|
+
const messages = agent === "claude" ? claudeMessages(lines) : codexMessages(lines);
|
|
19454
|
+
if (!afterUuid) return messages;
|
|
19455
|
+
const idx = messages.findIndex((m) => m.uuid === afterUuid);
|
|
19456
|
+
return idx === -1 ? [] : messages.slice(idx + 1);
|
|
19457
|
+
}
|
|
19458
|
+
function bindSessions(items) {
|
|
19459
|
+
const store = new SessionStore(paths.DB_FILE);
|
|
19460
|
+
try {
|
|
19461
|
+
for (const i of items)
|
|
19462
|
+
store.saveSession(`chat:${i.channelId}`, i.threadId, i.sessionId, i.agent);
|
|
19463
|
+
} finally {
|
|
19464
|
+
store.close();
|
|
19465
|
+
}
|
|
19466
|
+
return items.length;
|
|
19467
|
+
}
|
|
19468
|
+
function handleSessionImportRequest(req) {
|
|
19469
|
+
switch (req.op) {
|
|
19470
|
+
case "scan":
|
|
19471
|
+
return { op: "scan", folders: scanSessionFolders(req.slug) };
|
|
19472
|
+
case "read":
|
|
19473
|
+
return { op: "read", messages: readSessionMessages(req.agent, req.sessionId, req.afterUuid) };
|
|
19474
|
+
case "bind":
|
|
19475
|
+
return { op: "bind", bound: bindSessions(req.items) };
|
|
19476
|
+
}
|
|
19477
|
+
}
|
|
19478
|
+
|
|
18671
19479
|
// src/dashboard/docs-relay-host.ts
|
|
18672
|
-
import { Hono as
|
|
19480
|
+
import { Hono as Hono36 } from "hono";
|
|
18673
19481
|
var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
|
|
18674
19482
|
async function handleDocsRequest(req) {
|
|
18675
19483
|
return runRelayed(req, (app) => app.route("/channels", createDocsRouter({ gate: async (_c, next) => next() })));
|
|
18676
19484
|
}
|
|
18677
19485
|
async function runRelayed(req, register) {
|
|
18678
|
-
const app = new
|
|
19486
|
+
const app = new Hono36();
|
|
18679
19487
|
app.use("*", async (c, next) => {
|
|
18680
19488
|
c.set("auth", req.auth);
|
|
18681
19489
|
await next();
|
|
@@ -18698,7 +19506,7 @@ async function handleHostRequest(req) {
|
|
|
18698
19506
|
}
|
|
18699
19507
|
|
|
18700
19508
|
// src/dashboard/doc-share-publisher.ts
|
|
18701
|
-
import { readFileSync as
|
|
19509
|
+
import { readFileSync as readFileSync26, statSync as statSync9 } from "fs";
|
|
18702
19510
|
import { extname as extname2, posix } from "path";
|
|
18703
19511
|
var MAX_ASSET_BYTES = 20 * 1024 * 1024;
|
|
18704
19512
|
var MAX_CONTENT_BYTES2 = 2 * 1024 * 1024;
|
|
@@ -18813,7 +19621,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
|
|
|
18813
19621
|
}
|
|
18814
19622
|
if (!stat.isFile()) return { status: "not-found" };
|
|
18815
19623
|
if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
|
|
18816
|
-
const source =
|
|
19624
|
+
const source = readFileSync26(abs, "utf8");
|
|
18817
19625
|
const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
|
|
18818
19626
|
if (kind === "excalidraw") {
|
|
18819
19627
|
try {
|
|
@@ -18884,7 +19692,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
|
|
|
18884
19692
|
let text = null;
|
|
18885
19693
|
try {
|
|
18886
19694
|
const size = statSync9(docAbs).size;
|
|
18887
|
-
if (size <= budget) text =
|
|
19695
|
+
if (size <= budget) text = readFileSync26(docAbs, "utf8");
|
|
18888
19696
|
} catch {
|
|
18889
19697
|
continue;
|
|
18890
19698
|
}
|
|
@@ -18901,7 +19709,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
|
|
|
18901
19709
|
let body;
|
|
18902
19710
|
try {
|
|
18903
19711
|
if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
|
|
18904
|
-
body =
|
|
19712
|
+
body = readFileSync26(resolved2.abs);
|
|
18905
19713
|
} catch {
|
|
18906
19714
|
continue;
|
|
18907
19715
|
}
|
|
@@ -18929,13 +19737,13 @@ function readDocShareBundle({ cwd, sourcePath }) {
|
|
|
18929
19737
|
|
|
18930
19738
|
// src/dashboard/host-upgrade.ts
|
|
18931
19739
|
import { spawn as spawn7 } from "child_process";
|
|
18932
|
-
import { mkdirSync as mkdirSync13, openSync as
|
|
18933
|
-
import { join as
|
|
19740
|
+
import { mkdirSync as mkdirSync13, openSync as openSync4 } from "fs";
|
|
19741
|
+
import { join as join27 } from "path";
|
|
18934
19742
|
var running = false;
|
|
18935
19743
|
function startHostUpgrade() {
|
|
18936
19744
|
if (running) return { started: false };
|
|
18937
19745
|
mkdirSync13(paths.LOG_DIR, { recursive: true });
|
|
18938
|
-
const log5 =
|
|
19746
|
+
const log5 = openSync4(join27(paths.LOG_DIR, "host-upgrade.log"), "a");
|
|
18939
19747
|
const child = spawn7(process.execPath, [process.argv[1] ?? "", "upgrade"], {
|
|
18940
19748
|
detached: true,
|
|
18941
19749
|
stdio: ["ignore", log5, log5]
|
|
@@ -18956,7 +19764,7 @@ function dataSocketUrl() {
|
|
|
18956
19764
|
return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
|
|
18957
19765
|
}
|
|
18958
19766
|
function listLocalSessions() {
|
|
18959
|
-
if (!
|
|
19767
|
+
if (!existsSync32(paths.DB_FILE)) return [];
|
|
18960
19768
|
const store = new SessionStore(paths.DB_FILE);
|
|
18961
19769
|
try {
|
|
18962
19770
|
return store.listSessions();
|
|
@@ -18976,9 +19784,12 @@ function localAgentCallbacks(chatAgent) {
|
|
|
18976
19784
|
listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
|
|
18977
19785
|
listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
|
|
18978
19786
|
getAgentIdentities: () => resolveAgentIdentities(),
|
|
18979
|
-
readDocShareBundle: (req) => readDocShareBundle(req),
|
|
18980
|
-
listChannelSkills: (args) =>
|
|
18981
|
-
|
|
19787
|
+
readDocShareBundle: (req) => readDocShareBundle(req.channelId ? { ...req, cwd: resolveChannelConfig(req.channelId).cwd } : req),
|
|
19788
|
+
listChannelSkills: (args) => {
|
|
19789
|
+
const { cwd, claudeConfigDir } = args.channelId ? resolveChannelConfig(args.channelId) : args;
|
|
19790
|
+
return listSkills(claudeConfigDir, cwd);
|
|
19791
|
+
},
|
|
19792
|
+
listFlatDocs: (args) => flatDocs(args.channelId ? resolveChannelConfig(args.channelId).cwd : args.cwd, { query: args.query, sort: args.sort, showHidden: args.showHidden }),
|
|
18982
19793
|
getLocalDashboardSettings: () => loadDashboardSettings(),
|
|
18983
19794
|
saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
|
|
18984
19795
|
writeSkill: (ref, write) => writeSkillAndSync(ref, write),
|
|
@@ -18990,6 +19801,7 @@ function localAgentCallbacks(chatAgent) {
|
|
|
18990
19801
|
upgradeHost: () => startHostUpgrade(),
|
|
18991
19802
|
scanOnboarding: (slug) => scanOnboarding(slug),
|
|
18992
19803
|
linkProjectFolders: (slug, items) => linkProjectFolders(slug, items),
|
|
19804
|
+
sessionImportRequest: (req) => handleSessionImportRequest(req),
|
|
18993
19805
|
isHostOnline: () => true
|
|
18994
19806
|
};
|
|
18995
19807
|
}
|
|
@@ -18998,7 +19810,7 @@ function buildHostConfigSnapshot() {
|
|
|
18998
19810
|
const snapshot2 = {};
|
|
18999
19811
|
for (const profile of listProfiles()) {
|
|
19000
19812
|
snapshot2[profile] = runWithProfile(profile, () => ({
|
|
19001
|
-
env:
|
|
19813
|
+
env: existsSync32(paths.CONFIG_FILE) ? dotenv2.parse(readFileSync27(paths.CONFIG_FILE, "utf-8")) : {},
|
|
19002
19814
|
agentModel: loadDashboardSettings().agent
|
|
19003
19815
|
}));
|
|
19004
19816
|
}
|
|
@@ -19248,12 +20060,12 @@ function startPushWatcher() {
|
|
|
19248
20060
|
}
|
|
19249
20061
|
|
|
19250
20062
|
// src/dashboard/machine-server.ts
|
|
19251
|
-
import { existsSync as
|
|
19252
|
-
import { join as
|
|
20063
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20064
|
+
import { join as join28 } from "path";
|
|
19253
20065
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
19254
20066
|
|
|
19255
20067
|
// src/dashboard/app-icon-routes.ts
|
|
19256
|
-
import { Hono as
|
|
20068
|
+
import { Hono as Hono37 } from "hono";
|
|
19257
20069
|
var NEUTRAL_MANIFEST = {
|
|
19258
20070
|
id: "/",
|
|
19259
20071
|
name: "Flyspace",
|
|
@@ -19291,7 +20103,7 @@ async function withResolvedProfile2(c, next) {
|
|
|
19291
20103
|
return profile ? runWithProfile(profile, () => next()) : next();
|
|
19292
20104
|
}
|
|
19293
20105
|
function createAppIconRouter() {
|
|
19294
|
-
const app = new
|
|
20106
|
+
const app = new Hono37();
|
|
19295
20107
|
app.get("/manifest.json", withResolvedProfile2, async (c) => {
|
|
19296
20108
|
if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
|
|
19297
20109
|
const profile = getProfile();
|
|
@@ -19331,7 +20143,7 @@ function pngResponse(c, png) {
|
|
|
19331
20143
|
}
|
|
19332
20144
|
|
|
19333
20145
|
// src/dashboard/machine-server.ts
|
|
19334
|
-
var WEB_DIST =
|
|
20146
|
+
var WEB_DIST = join28(paths.WEB_ROOT, "dist");
|
|
19335
20147
|
async function bootMachine() {
|
|
19336
20148
|
await reconcileAll().catch(
|
|
19337
20149
|
(err) => console.error("[dashboard] reconcileAll failed:", err.message)
|
|
@@ -19351,14 +20163,14 @@ async function bootMachine() {
|
|
|
19351
20163
|
function mountSpa(app) {
|
|
19352
20164
|
app.route("/share", createPublicShareAssetRouter(WEB_DIST));
|
|
19353
20165
|
app.route("/", createAppIconRouter());
|
|
19354
|
-
if (
|
|
20166
|
+
if (existsSync33(WEB_DIST) || existsSync33(join28(paths.WEB_ROOT, "package.json"))) {
|
|
19355
20167
|
app.use("/*", serveStatic({ root: WEB_DIST }));
|
|
19356
20168
|
}
|
|
19357
20169
|
app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
|
|
19358
|
-
const spaFallback = serveStatic({ path:
|
|
20170
|
+
const spaFallback = serveStatic({ path: join28(WEB_DIST, "index.html") });
|
|
19359
20171
|
app.get("*", async (c, next) => {
|
|
19360
|
-
if (!
|
|
19361
|
-
const isCheckout =
|
|
20172
|
+
if (!existsSync33(join28(WEB_DIST, "index.html"))) {
|
|
20173
|
+
const isCheckout = existsSync33(join28(paths.WEB_ROOT, "package.json"));
|
|
19362
20174
|
return c.text(
|
|
19363
20175
|
isCheckout ? `Workspace UI build missing (${WEB_DIST}).
|
|
19364
20176
|
Run \`pnpm build:web\` from the repo root.
|
|
@@ -19492,8 +20304,8 @@ function openBrowser2(url) {
|
|
|
19492
20304
|
}
|
|
19493
20305
|
}
|
|
19494
20306
|
function ensureWebBuild() {
|
|
19495
|
-
if (
|
|
19496
|
-
if (!
|
|
20307
|
+
if (existsSync34(join29(paths.WEB_ROOT, "dist", "index.html"))) return;
|
|
20308
|
+
if (!existsSync34(join29(paths.WEB_ROOT, "package.json"))) return;
|
|
19497
20309
|
console.log(" packages/ui/dist missing, building the dashboard UI...");
|
|
19498
20310
|
const result = spawnSync("pnpm", ["build"], {
|
|
19499
20311
|
cwd: paths.WEB_ROOT,
|
|
@@ -19543,7 +20355,7 @@ async function runDashboard(args) {
|
|
|
19543
20355
|
}
|
|
19544
20356
|
|
|
19545
20357
|
// src/commands/workspace.ts
|
|
19546
|
-
import { existsSync as
|
|
20358
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
|
|
19547
20359
|
var USAGE7 = `
|
|
19548
20360
|
Usage: flyspace workspace <subcommand> [options]
|
|
19549
20361
|
|
|
@@ -19629,7 +20441,7 @@ function provisionLocal(botName) {
|
|
|
19629
20441
|
ensureDirs();
|
|
19630
20442
|
console.log(` config dir ready (${paths.CONFIG_DIR})`);
|
|
19631
20443
|
console.log(` data dir ready (${paths.DATA_DIR})`);
|
|
19632
|
-
if (
|
|
20444
|
+
if (existsSync35(paths.CONFIG_FILE)) {
|
|
19633
20445
|
console.log(" config.env exists");
|
|
19634
20446
|
} else {
|
|
19635
20447
|
writeFileSync13(paths.CONFIG_FILE, `BOT_NAME=${botName}
|
|
@@ -19646,7 +20458,7 @@ async function provisionChannels(specs) {
|
|
|
19646
20458
|
continue;
|
|
19647
20459
|
}
|
|
19648
20460
|
const cwd = expandPath(spec.cwd);
|
|
19649
|
-
if (!
|
|
20461
|
+
if (!existsSync35(cwd)) {
|
|
19650
20462
|
mkdirSync14(cwd, { recursive: true });
|
|
19651
20463
|
console.log(` created cwd ${cwd}`);
|
|
19652
20464
|
}
|
|
@@ -19724,10 +20536,10 @@ async function runReconcileCmd(args) {
|
|
|
19724
20536
|
}
|
|
19725
20537
|
|
|
19726
20538
|
// src/cli.ts
|
|
19727
|
-
import { join as
|
|
20539
|
+
import { join as join30 } from "path";
|
|
19728
20540
|
var home = flyspaceHome();
|
|
19729
20541
|
if (home) {
|
|
19730
|
-
const bin =
|
|
20542
|
+
const bin = join30(home, "bin");
|
|
19731
20543
|
const path = process.env.PATH ?? "";
|
|
19732
20544
|
if (!path.split(":").includes(bin)) process.env.PATH = path ? `${bin}:${path}` : bin;
|
|
19733
20545
|
}
|
|
@@ -19865,7 +20677,7 @@ async function main() {
|
|
|
19865
20677
|
break;
|
|
19866
20678
|
case "-v":
|
|
19867
20679
|
case "--version":
|
|
19868
|
-
printRaw("0.1.
|
|
20680
|
+
printRaw("0.1.34");
|
|
19869
20681
|
break;
|
|
19870
20682
|
case "-h":
|
|
19871
20683
|
case "--help":
|