flyspace-cli 0.1.13 → 0.1.14
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-VKTJBYA3.js → chunk-DA7SYYKU.js} +10 -0
- package/dist/cli.js +666 -595
- package/dist/index.js +1 -1
- package/package.json +2 -1
- package/templates/general/01-welcome.md +20 -0
- package/templates/general/02-working-with-the-agent.md +21 -0
- package/templates/general/03-channels-and-folders.md +19 -0
- package/templates/general/04-docs.md +14 -0
- package/templates/general/05-inviting-your-team.md +17 -0
- package/templates/general/06-automations.md +21 -0
package/dist/cli.js
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
getWorkspaceById,
|
|
69
69
|
getWorkspaceBySlug,
|
|
70
70
|
getWorkspaceByStripeCustomerId,
|
|
71
|
+
getWorkspaceGreetedAt,
|
|
71
72
|
getWorkspacePlanStatus,
|
|
72
73
|
installUsageRecording,
|
|
73
74
|
isAllowedModel,
|
|
@@ -103,6 +104,7 @@ import {
|
|
|
103
104
|
lookupById,
|
|
104
105
|
markInviteResent,
|
|
105
106
|
markWorkspaceAgentConnected,
|
|
107
|
+
markWorkspaceGreeted,
|
|
106
108
|
markWorkspaceProjectsSetup,
|
|
107
109
|
markdownToBlocks,
|
|
108
110
|
mcpStrictConfigEnabled,
|
|
@@ -153,7 +155,7 @@ import {
|
|
|
153
155
|
warnOnBrokenChannelCwds,
|
|
154
156
|
workspaceSkillsDir,
|
|
155
157
|
writeSkillFile
|
|
156
|
-
} from "./chunk-
|
|
158
|
+
} from "./chunk-DA7SYYKU.js";
|
|
157
159
|
import {
|
|
158
160
|
firstReplyBody,
|
|
159
161
|
getThread,
|
|
@@ -220,8 +222,8 @@ import { dirname, join } from "path";
|
|
|
220
222
|
var REQUIRED_ENV = ["SUPABASE_URL", "SUPABASE_SECRET_KEY"];
|
|
221
223
|
var RENOTIFY_AFTER_MS = 30 * 60 * 1e3;
|
|
222
224
|
var ts = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
223
|
-
function findMissingEnv(env = process.env) {
|
|
224
|
-
const required = slackConfigured(env) ? [
|
|
225
|
+
function findMissingEnv(env = process.env, supabaseViaHost = false) {
|
|
226
|
+
const required = [...slackConfigured(env) ? SLACK_ENV : [], ...supabaseViaHost ? [] : REQUIRED_ENV];
|
|
225
227
|
return required.filter((name) => !env[name]?.trim());
|
|
226
228
|
}
|
|
227
229
|
function shouldNotify(missing, statePath, now) {
|
|
@@ -290,7 +292,7 @@ async function preflight(opts = {}) {
|
|
|
290
292
|
const statePath = opts.statePath ?? join(paths.DATA_DIR, "preflight-alert.json");
|
|
291
293
|
const notify = opts.notify ?? notifyOwnerOverSlack;
|
|
292
294
|
const exit = opts.exit ?? ((code) => process.exit(code));
|
|
293
|
-
const missing = findMissingEnv(env);
|
|
295
|
+
const missing = findMissingEnv(env, opts.supabaseViaHost);
|
|
294
296
|
if (missing.length === 0) {
|
|
295
297
|
rmSync(statePath, { force: true });
|
|
296
298
|
return;
|
|
@@ -307,15 +309,386 @@ async function preflight(opts = {}) {
|
|
|
307
309
|
exit(1);
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
// src/auth/device-auth.ts
|
|
313
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
|
|
314
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
315
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
316
|
+
var DEFAULT_API_URL = "https://app.flyspace.ai";
|
|
317
|
+
var EXPIRY_SKEW_MS = 6e4;
|
|
318
|
+
var AuthApiError = class extends Error {
|
|
319
|
+
constructor(message, status) {
|
|
320
|
+
super(message);
|
|
321
|
+
this.status = status;
|
|
322
|
+
this.name = "AuthApiError";
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function apiUrl() {
|
|
326
|
+
return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
327
|
+
}
|
|
328
|
+
function deviceKeyPath() {
|
|
329
|
+
return join2(paths.BASE_CONFIG_DIR, "device.key");
|
|
330
|
+
}
|
|
331
|
+
function credentialsPath() {
|
|
332
|
+
return join2(paths.BASE_CONFIG_DIR, "credentials.json");
|
|
333
|
+
}
|
|
334
|
+
function ensureDeviceKey(path = deviceKeyPath()) {
|
|
335
|
+
let created = false;
|
|
336
|
+
if (!existsSync(path)) {
|
|
337
|
+
const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
|
|
338
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
339
|
+
writeFileSync2(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
|
|
340
|
+
created = true;
|
|
341
|
+
}
|
|
342
|
+
const privateKey = createPrivateKey(readFileSync2(path, "utf8"));
|
|
343
|
+
const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
|
|
344
|
+
"base64"
|
|
345
|
+
);
|
|
346
|
+
return { privateKey, publicKeyBase64, created };
|
|
347
|
+
}
|
|
348
|
+
function loadCredentials(path = credentialsPath()) {
|
|
349
|
+
if (!existsSync(path)) return null;
|
|
350
|
+
try {
|
|
351
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function saveCredentials(creds, path = credentialsPath()) {
|
|
357
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
358
|
+
writeFileSync2(path, `${JSON.stringify(creds, null, 2)}
|
|
359
|
+
`, { mode: 384 });
|
|
360
|
+
}
|
|
361
|
+
function clearLocalAuth() {
|
|
362
|
+
rmSync2(credentialsPath(), { force: true });
|
|
363
|
+
rmSync2(deviceKeyPath(), { force: true });
|
|
364
|
+
}
|
|
365
|
+
async function post(base, path, body, token) {
|
|
366
|
+
return fetch(`${base}${path}`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: {
|
|
369
|
+
"content-type": "application/json",
|
|
370
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
371
|
+
},
|
|
372
|
+
body: JSON.stringify(body)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async function readJson(res) {
|
|
376
|
+
try {
|
|
377
|
+
return await res.json();
|
|
378
|
+
} catch {
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function expectOk(res, what) {
|
|
383
|
+
const json = await readJson(res);
|
|
384
|
+
if (!res.ok) {
|
|
385
|
+
const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
|
|
386
|
+
throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
|
|
387
|
+
}
|
|
388
|
+
return json;
|
|
389
|
+
}
|
|
390
|
+
function toSlugs(value) {
|
|
391
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
392
|
+
}
|
|
393
|
+
function toGrant(json) {
|
|
394
|
+
const user = json["user"] ?? {};
|
|
395
|
+
return {
|
|
396
|
+
accessToken: String(json["access_token"]),
|
|
397
|
+
expiresIn: Number(json["expires_in"]),
|
|
398
|
+
deviceId: String(json["device_id"]),
|
|
399
|
+
userEmail: String(user["email"] ?? ""),
|
|
400
|
+
workspaces: toSlugs(json["workspaces"])
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async function requestDeviceCode(base, input2) {
|
|
404
|
+
const res = await post(base, "/api/auth/device/code", {
|
|
405
|
+
public_key: input2.publicKey,
|
|
406
|
+
name: input2.name,
|
|
407
|
+
platform: input2.platform
|
|
408
|
+
});
|
|
409
|
+
const json = await expectOk(res, "Requesting a device code");
|
|
410
|
+
return {
|
|
411
|
+
deviceCode: String(json["device_code"]),
|
|
412
|
+
userCode: String(json["user_code"]),
|
|
413
|
+
verificationUri: String(json["verification_uri"]),
|
|
414
|
+
expiresIn: Number(json["expires_in"]),
|
|
415
|
+
interval: Number(json["interval"] ?? 5)
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function pollDeviceToken(base, deviceCode) {
|
|
419
|
+
const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
|
|
420
|
+
const json = await readJson(res);
|
|
421
|
+
if (res.ok) return { status: "approved", grant: toGrant(json) };
|
|
422
|
+
switch (json["error"]) {
|
|
423
|
+
case "authorization_pending":
|
|
424
|
+
return { status: "pending" };
|
|
425
|
+
case "slow_down":
|
|
426
|
+
return { status: "slow_down" };
|
|
427
|
+
case "access_denied":
|
|
428
|
+
return { status: "denied" };
|
|
429
|
+
case "expired_token":
|
|
430
|
+
return { status: "expired" };
|
|
431
|
+
default:
|
|
432
|
+
throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function refreshAccessToken(creds, privateKey) {
|
|
436
|
+
const challenge = await expectOk(
|
|
437
|
+
await post(creds.apiUrl, "/api/auth/device/challenge", { device_id: creds.deviceId }),
|
|
438
|
+
"Requesting a refresh challenge"
|
|
439
|
+
);
|
|
440
|
+
const nonce = String(challenge["nonce"]);
|
|
441
|
+
const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
|
|
442
|
+
const json = await expectOk(
|
|
443
|
+
await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
|
|
444
|
+
"Refreshing the access token"
|
|
445
|
+
);
|
|
446
|
+
return {
|
|
447
|
+
...creds,
|
|
448
|
+
accessToken: String(json["access_token"]),
|
|
449
|
+
accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async function fetchWhoAmI(base, token) {
|
|
453
|
+
const res = await fetch(`${base}/api/auth/device/me`, { headers: { authorization: `Bearer ${token}` } });
|
|
454
|
+
const json = await expectOk(res, "Checking the session");
|
|
455
|
+
const user = json["user"] ?? {};
|
|
456
|
+
const device = json["device"] ?? {};
|
|
457
|
+
return {
|
|
458
|
+
userEmail: String(user["email"] ?? ""),
|
|
459
|
+
deviceId: String(device["id"] ?? ""),
|
|
460
|
+
deviceName: String(device["name"] ?? ""),
|
|
461
|
+
workspaces: toSlugs(json["workspaces"])
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
async function boundWorkspaces() {
|
|
465
|
+
const creds = loadCredentials();
|
|
466
|
+
if (!creds) return null;
|
|
467
|
+
try {
|
|
468
|
+
const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
|
|
469
|
+
saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
|
|
470
|
+
return { workspaces: me.workspaces, fresh: true };
|
|
471
|
+
} catch {
|
|
472
|
+
return { workspaces: creds.workspaces ?? [], fresh: false };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function revokeDevice(base, token) {
|
|
476
|
+
await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
|
|
477
|
+
}
|
|
478
|
+
async function getAccessToken() {
|
|
479
|
+
const fromEnv = process.env["FLYSPACE_TOKEN"];
|
|
480
|
+
if (fromEnv) return fromEnv;
|
|
481
|
+
const creds = loadCredentials();
|
|
482
|
+
if (!creds) return null;
|
|
483
|
+
if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
|
|
484
|
+
const { privateKey } = ensureDeviceKey();
|
|
485
|
+
const refreshed = await refreshAccessToken(creds, privateKey);
|
|
486
|
+
saveCredentials(refreshed);
|
|
487
|
+
return refreshed.accessToken;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/dashboard/host-db-client.ts
|
|
491
|
+
import { createClient } from "@supabase/supabase-js";
|
|
492
|
+
import { randomUUID } from "crypto";
|
|
493
|
+
|
|
494
|
+
// src/dashboard/host-lock.ts
|
|
495
|
+
import { execFileSync } from "child_process";
|
|
496
|
+
import { randomBytes } from "crypto";
|
|
497
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
498
|
+
import { join as join3 } from "path";
|
|
499
|
+
function hostLockPath() {
|
|
500
|
+
return join3(paths.BASE_DATA_DIR, "host.lock");
|
|
501
|
+
}
|
|
502
|
+
function isLiveFlyspace(pid) {
|
|
503
|
+
try {
|
|
504
|
+
process.kill(pid, 0);
|
|
505
|
+
const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
506
|
+
encoding: "utf8"
|
|
507
|
+
});
|
|
508
|
+
return command.includes("flyspace") || command.includes("host-server");
|
|
509
|
+
} catch {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function readLockPid(path) {
|
|
514
|
+
try {
|
|
515
|
+
const pid = Number(JSON.parse(readFileSync3(path, "utf8")).pid);
|
|
516
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function acquireHostLock() {
|
|
522
|
+
const path = hostLockPath();
|
|
523
|
+
mkdirSync3(paths.BASE_DATA_DIR, { recursive: true });
|
|
524
|
+
const body = JSON.stringify({
|
|
525
|
+
pid: process.pid,
|
|
526
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
527
|
+
execPath: process.argv[1] ?? ""
|
|
528
|
+
});
|
|
529
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
530
|
+
try {
|
|
531
|
+
writeFileSync3(path, body, { flag: "wx", mode: 384 });
|
|
532
|
+
const release2 = () => {
|
|
533
|
+
if (readLockPid(path) === process.pid) rmSync3(path, { force: true });
|
|
534
|
+
};
|
|
535
|
+
process.once("exit", release2);
|
|
536
|
+
return release2;
|
|
537
|
+
} catch (err) {
|
|
538
|
+
if (err.code !== "EEXIST") throw err;
|
|
539
|
+
const holder = readLockPid(path);
|
|
540
|
+
if (holder === process.pid) return () => {
|
|
541
|
+
};
|
|
542
|
+
if (holder && isLiveFlyspace(holder)) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
rmSync3(path, { force: true });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
throw new Error(`Could not take the Host lock at ${path}`);
|
|
551
|
+
}
|
|
552
|
+
function publishHostEndpoint(port) {
|
|
553
|
+
const path = hostLockPath();
|
|
554
|
+
if (readLockPid(path) !== process.pid) return null;
|
|
555
|
+
const token = randomBytes(32).toString("hex");
|
|
556
|
+
const lock = JSON.parse(readFileSync3(path, "utf8"));
|
|
557
|
+
writeFileSync3(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
|
|
558
|
+
return token;
|
|
559
|
+
}
|
|
560
|
+
function readHostEndpoint() {
|
|
561
|
+
try {
|
|
562
|
+
const { pid, port, token } = JSON.parse(readFileSync3(hostLockPath(), "utf8"));
|
|
563
|
+
if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
|
|
564
|
+
return { port, token };
|
|
565
|
+
} catch {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/dashboard/host-db-client.ts
|
|
571
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
572
|
+
var pending = /* @__PURE__ */ new Map();
|
|
573
|
+
var sender = null;
|
|
574
|
+
var onSender = [];
|
|
575
|
+
function setHostDbSender(fn) {
|
|
576
|
+
sender = fn;
|
|
577
|
+
if (!fn) return;
|
|
578
|
+
const waiting = onSender;
|
|
579
|
+
onSender = [];
|
|
580
|
+
for (const wake of waiting) wake();
|
|
581
|
+
}
|
|
582
|
+
function handleHostDbResult(id, res) {
|
|
583
|
+
const p = pending.get(id);
|
|
584
|
+
if (!p) return;
|
|
585
|
+
pending.delete(id);
|
|
586
|
+
clearTimeout(p.timer);
|
|
587
|
+
p.resolve(res);
|
|
588
|
+
}
|
|
589
|
+
function whenConnected() {
|
|
590
|
+
if (sender) return Promise.resolve(sender);
|
|
591
|
+
return new Promise((resolve7, reject) => {
|
|
592
|
+
const timer2 = setTimeout(() => {
|
|
593
|
+
onSender = onSender.filter((w) => w !== wake);
|
|
594
|
+
reject(new Error("Host is not connected to the Data API"));
|
|
595
|
+
}, REQUEST_TIMEOUT_MS);
|
|
596
|
+
const wake = () => {
|
|
597
|
+
clearTimeout(timer2);
|
|
598
|
+
resolve7(sender);
|
|
599
|
+
};
|
|
600
|
+
onSender.push(wake);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
async function requestHostDb(req) {
|
|
604
|
+
const send2 = await whenConnected();
|
|
605
|
+
const id = randomUUID();
|
|
606
|
+
return new Promise((resolve7, reject) => {
|
|
607
|
+
const timer2 = setTimeout(() => {
|
|
608
|
+
pending.delete(id);
|
|
609
|
+
reject(new Error("Data API did not answer in time"));
|
|
610
|
+
}, REQUEST_TIMEOUT_MS);
|
|
611
|
+
pending.set(id, { resolve: resolve7, reject, timer: timer2 });
|
|
612
|
+
send2({ type: "db", id, req });
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
616
|
+
function dbFetch(send2) {
|
|
617
|
+
return async (input2, init) => {
|
|
618
|
+
const req = new Request(input2, init);
|
|
619
|
+
const url = new URL(req.url);
|
|
620
|
+
const method = req.method.toUpperCase();
|
|
621
|
+
const res = await send2({
|
|
622
|
+
method,
|
|
623
|
+
path: `${url.pathname}${url.search}`,
|
|
624
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
625
|
+
body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
|
|
626
|
+
});
|
|
627
|
+
const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
|
|
628
|
+
return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
var hostDbFetch = dbFetch(requestHostDb);
|
|
632
|
+
var LOCAL_DB_PATH = "/internal/db";
|
|
633
|
+
async function requestViaLocalHost(req) {
|
|
634
|
+
const deadline = Date.now() + REQUEST_TIMEOUT_MS;
|
|
635
|
+
let endpoint = readHostEndpoint();
|
|
636
|
+
while (!endpoint && Date.now() < deadline) {
|
|
637
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
638
|
+
endpoint = readHostEndpoint();
|
|
639
|
+
}
|
|
640
|
+
if (!endpoint) throw new Error("No local Host to reach the Data API through");
|
|
641
|
+
const res = await fetch(`http://127.0.0.1:${endpoint.port}${LOCAL_DB_PATH}`, {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: { authorization: `Bearer ${endpoint.token}`, "content-type": "application/json" },
|
|
644
|
+
body: JSON.stringify(req),
|
|
645
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS + 5e3)
|
|
646
|
+
});
|
|
647
|
+
if (!res.ok) throw new Error(`Local Host DB relay answered ${res.status}`);
|
|
648
|
+
return await res.json();
|
|
649
|
+
}
|
|
650
|
+
function relayChannel(topic) {
|
|
651
|
+
const channel = {
|
|
652
|
+
on: () => channel,
|
|
653
|
+
subscribe: (callback) => {
|
|
654
|
+
queueMicrotask(() => callback?.("SUBSCRIBED"));
|
|
655
|
+
return channel;
|
|
656
|
+
},
|
|
657
|
+
send: async (msg) => {
|
|
658
|
+
if (msg.type !== "broadcast" || !sender) return "error";
|
|
659
|
+
sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
|
|
660
|
+
return "ok";
|
|
661
|
+
},
|
|
662
|
+
unsubscribe: async () => "ok"
|
|
663
|
+
};
|
|
664
|
+
return channel;
|
|
665
|
+
}
|
|
666
|
+
function createHostDbClient(via = "socket") {
|
|
667
|
+
const client2 = createClient("https://host-db.invalid", "host-device", {
|
|
668
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
669
|
+
global: { fetch: via === "socket" ? hostDbFetch : dbFetch(requestViaLocalHost) }
|
|
670
|
+
});
|
|
671
|
+
return new Proxy(client2, {
|
|
672
|
+
get(target, prop) {
|
|
673
|
+
if (prop === "channel") return relayChannel;
|
|
674
|
+
if (prop === "removeChannel") return async () => "ok";
|
|
675
|
+
const value = Reflect.get(target, prop, target);
|
|
676
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
310
681
|
// src/commands/start.ts
|
|
311
682
|
async function runStart() {
|
|
312
683
|
loadEnvFiles();
|
|
313
|
-
|
|
684
|
+
const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
|
|
685
|
+
if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
|
|
686
|
+
await preflight({ supabaseViaHost: viaHost });
|
|
314
687
|
await startServer();
|
|
315
688
|
}
|
|
316
689
|
|
|
317
690
|
// src/commands/config.ts
|
|
318
|
-
import { existsSync, readFileSync as
|
|
691
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
319
692
|
import { spawn } from "child_process";
|
|
320
693
|
import { resolve } from "path";
|
|
321
694
|
function maskToken(value) {
|
|
@@ -333,12 +706,12 @@ function printConfig() {
|
|
|
333
706
|
console.log("\n Paths:");
|
|
334
707
|
console.log(` Config dir: ${paths.CONFIG_DIR}`);
|
|
335
708
|
console.log(` Data dir: ${paths.DATA_DIR}`);
|
|
336
|
-
console.log(` Config file: ${paths.CONFIG_FILE} ${
|
|
337
|
-
console.log(` Prompt file: ${paths.PROMPT_FILE} ${
|
|
338
|
-
console.log(` Database: ${paths.DB_FILE} ${
|
|
709
|
+
console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync2(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
|
|
710
|
+
console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync2(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
|
|
711
|
+
console.log(` Database: ${paths.DB_FILE} ${existsSync2(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
|
|
339
712
|
console.log(` Log dir: ${paths.LOG_DIR}`);
|
|
340
713
|
const repoEnv = resolve(process.cwd(), ".env");
|
|
341
|
-
console.log(` Repo .env: ${repoEnv} ${
|
|
714
|
+
console.log(` Repo .env: ${repoEnv} ${existsSync2(repoEnv) ? "(exists)" : "(not found)"}`);
|
|
342
715
|
const envKeys = [
|
|
343
716
|
"SLACK_BOT_TOKEN",
|
|
344
717
|
"SLACK_APP_TOKEN",
|
|
@@ -362,13 +735,13 @@ function printConfig() {
|
|
|
362
735
|
}
|
|
363
736
|
function editConfig() {
|
|
364
737
|
ensureDirs();
|
|
365
|
-
if (!
|
|
738
|
+
if (!existsSync2(paths.CONFIG_FILE)) {
|
|
366
739
|
const defaultEnv = resolve(paths.PACKAGE_ROOT, ".env.example");
|
|
367
|
-
if (
|
|
368
|
-
const content =
|
|
369
|
-
|
|
740
|
+
if (existsSync2(defaultEnv)) {
|
|
741
|
+
const content = readFileSync4(defaultEnv, "utf-8");
|
|
742
|
+
writeFileSync4(paths.CONFIG_FILE, content);
|
|
370
743
|
} else {
|
|
371
|
-
|
|
744
|
+
writeFileSync4(paths.CONFIG_FILE, "");
|
|
372
745
|
}
|
|
373
746
|
console.log(`[flyspace] Created ${paths.CONFIG_FILE}`);
|
|
374
747
|
}
|
|
@@ -402,14 +775,14 @@ async function runConfig(args) {
|
|
|
402
775
|
|
|
403
776
|
// src/commands/setup.ts
|
|
404
777
|
import { execSync as execSync4 } from "child_process";
|
|
405
|
-
import { mkdirSync as
|
|
406
|
-
import { join as
|
|
778
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
779
|
+
import { join as join8 } from "path";
|
|
407
780
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
408
781
|
|
|
409
782
|
// src/service.ts
|
|
410
783
|
import { execSync } from "child_process";
|
|
411
|
-
import { existsSync as
|
|
412
|
-
import { basename, dirname as
|
|
784
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, realpathSync } from "fs";
|
|
785
|
+
import { basename, dirname as dirname3, join as join4 } from "path";
|
|
413
786
|
import { homedir, platform } from "os";
|
|
414
787
|
function flyspaceHome(entry = process.argv[1]) {
|
|
415
788
|
let cliPath;
|
|
@@ -422,8 +795,8 @@ function flyspaceHome(entry = process.argv[1]) {
|
|
|
422
795
|
if (at <= 0) return void 0;
|
|
423
796
|
const npmPrefix = cliPath.slice(0, at);
|
|
424
797
|
if (basename(npmPrefix) !== "cli") return void 0;
|
|
425
|
-
const home =
|
|
426
|
-
return
|
|
798
|
+
const home = dirname3(npmPrefix);
|
|
799
|
+
return existsSync3(join4(home, "bin", "flyspace")) && existsSync3(join4(home, "node", "bin", "node")) ? home : void 0;
|
|
427
800
|
}
|
|
428
801
|
function serviceLabel(profile) {
|
|
429
802
|
return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
|
|
@@ -432,40 +805,40 @@ function plistName(profile) {
|
|
|
432
805
|
return `${serviceLabel(profile)}.plist`;
|
|
433
806
|
}
|
|
434
807
|
function plistPath(profile) {
|
|
435
|
-
return
|
|
808
|
+
return join4(homedir(), "Library", "LaunchAgents", plistName(profile));
|
|
436
809
|
}
|
|
437
810
|
function legacyServiceLabel(profile) {
|
|
438
811
|
return profile === DEFAULT_PROFILE ? "io.flycoder.flyspace" : `io.flycoder.flyspace.${profile}`;
|
|
439
812
|
}
|
|
440
813
|
function legacyPlistPath(profile) {
|
|
441
|
-
return
|
|
814
|
+
return join4(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
|
|
442
815
|
}
|
|
443
816
|
function systemdUnitName(profile) {
|
|
444
817
|
return profile === DEFAULT_PROFILE ? "flyspace" : `flyspace-${profile}`;
|
|
445
818
|
}
|
|
446
819
|
function systemdUnitPath(profile) {
|
|
447
|
-
return
|
|
820
|
+
return join4(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
|
|
448
821
|
}
|
|
449
822
|
function isServiceInstalled(profile) {
|
|
450
|
-
if (platform() === "darwin") return
|
|
451
|
-
if (platform() === "linux") return
|
|
823
|
+
if (platform() === "darwin") return existsSync3(plistPath(profile));
|
|
824
|
+
if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
|
|
452
825
|
return false;
|
|
453
826
|
}
|
|
454
827
|
function serviceBinaryMissing(file) {
|
|
455
828
|
let text;
|
|
456
829
|
try {
|
|
457
|
-
text =
|
|
830
|
+
text = readFileSync5(file, "utf-8");
|
|
458
831
|
} catch {
|
|
459
832
|
return false;
|
|
460
833
|
}
|
|
461
834
|
const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
|
|
462
|
-
return !!bin && !
|
|
835
|
+
return !!bin && !existsSync3(bin);
|
|
463
836
|
}
|
|
464
837
|
function profileServiceFile(profile) {
|
|
465
838
|
return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
|
|
466
839
|
}
|
|
467
840
|
function hostServiceFile() {
|
|
468
|
-
return platform() === "darwin" ? hostPlistPath() :
|
|
841
|
+
return platform() === "darwin" ? hostPlistPath() : join4(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
|
|
469
842
|
}
|
|
470
843
|
function restartService(profile) {
|
|
471
844
|
const os = platform();
|
|
@@ -495,10 +868,10 @@ function restartService(profile) {
|
|
|
495
868
|
var HOST_SERVICE_LABEL = "ai.flyspace.dashboard";
|
|
496
869
|
var HOST_SYSTEMD_UNIT = "flyspace-host";
|
|
497
870
|
function hostPlistPath() {
|
|
498
|
-
return
|
|
871
|
+
return join4(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
|
|
499
872
|
}
|
|
500
873
|
function isHostServiceInstalled() {
|
|
501
|
-
if (platform() === "darwin" || platform() === "linux") return
|
|
874
|
+
if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
|
|
502
875
|
return false;
|
|
503
876
|
}
|
|
504
877
|
function restartHostService() {
|
|
@@ -538,9 +911,9 @@ function isHostServiceRunning() {
|
|
|
538
911
|
}
|
|
539
912
|
|
|
540
913
|
// src/commands/install.ts
|
|
541
|
-
import { existsSync as
|
|
914
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "fs";
|
|
542
915
|
import { execSync as execSync2 } from "child_process";
|
|
543
|
-
import { dirname as
|
|
916
|
+
import { dirname as dirname4, join as join5, resolve as resolve2 } from "path";
|
|
544
917
|
import { homedir as homedir2, platform as platform2 } from "os";
|
|
545
918
|
function log(msg) {
|
|
546
919
|
console.log(`
|
|
@@ -561,23 +934,23 @@ function run(cmd, opts = {}) {
|
|
|
561
934
|
function detectClaudeBinDir() {
|
|
562
935
|
try {
|
|
563
936
|
const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
564
|
-
return claudePath ?
|
|
937
|
+
return claudePath ? dirname4(claudePath) : void 0;
|
|
565
938
|
} catch {
|
|
566
939
|
return void 0;
|
|
567
940
|
}
|
|
568
941
|
}
|
|
569
942
|
function detectNodeBinDir() {
|
|
570
943
|
const home = flyspaceHome();
|
|
571
|
-
return home ?
|
|
944
|
+
return home ? join5(home, "node", "bin") : dirname4(process.execPath);
|
|
572
945
|
}
|
|
573
946
|
function nodeShimDirs() {
|
|
574
|
-
return [
|
|
575
|
-
(d) =>
|
|
947
|
+
return [join5(homedir2(), ".local", "share", "mise", "shims"), join5(homedir2(), ".asdf", "shims")].filter(
|
|
948
|
+
(d) => existsSync4(d)
|
|
576
949
|
);
|
|
577
950
|
}
|
|
578
951
|
function detectCustieBin() {
|
|
579
952
|
const home = flyspaceHome();
|
|
580
|
-
if (home) return
|
|
953
|
+
if (home) return join5(home, "bin", "flyspace");
|
|
581
954
|
const argv1 = process.argv[1];
|
|
582
955
|
if (argv1) {
|
|
583
956
|
return resolve2(argv1);
|
|
@@ -589,9 +962,9 @@ function detectCustieBin() {
|
|
|
589
962
|
}
|
|
590
963
|
}
|
|
591
964
|
function readEnvFile() {
|
|
592
|
-
if (!
|
|
965
|
+
if (!existsSync4(paths.CONFIG_FILE)) return {};
|
|
593
966
|
const vars = {};
|
|
594
|
-
for (const line of
|
|
967
|
+
for (const line of readFileSync6(paths.CONFIG_FILE, "utf-8").split("\n")) {
|
|
595
968
|
const trimmed = line.trim();
|
|
596
969
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
597
970
|
const eq = trimmed.indexOf("=");
|
|
@@ -635,9 +1008,9 @@ async function installMacOS(spec) {
|
|
|
635
1008
|
const custieBin = detectCustieBin();
|
|
636
1009
|
const claudeBinDir = detectClaudeBinDir();
|
|
637
1010
|
const logDir = paths.LOG_DIR;
|
|
638
|
-
|
|
1011
|
+
mkdirSync4(logDir, { recursive: true });
|
|
639
1012
|
const envVars = spec.env;
|
|
640
|
-
const homebrewBin =
|
|
1013
|
+
const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
|
|
641
1014
|
const pathParts = [
|
|
642
1015
|
detectNodeBinDir(),
|
|
643
1016
|
claudeBinDir,
|
|
@@ -676,14 +1049,14 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
|
|
|
676
1049
|
<true/>
|
|
677
1050
|
|
|
678
1051
|
<key>StandardOutPath</key>
|
|
679
|
-
<string>${
|
|
1052
|
+
<string>${join5(logDir, `${spec.logName}.log`)}</string>
|
|
680
1053
|
|
|
681
1054
|
<key>StandardErrorPath</key>
|
|
682
|
-
<string>${
|
|
1055
|
+
<string>${join5(logDir, `${spec.logName}-error.log`)}</string>
|
|
683
1056
|
</dict>
|
|
684
1057
|
</plist>`;
|
|
685
1058
|
const agentPlistPath = spec.plistPath;
|
|
686
|
-
|
|
1059
|
+
mkdirSync4(dirname4(agentPlistPath), { recursive: true });
|
|
687
1060
|
try {
|
|
688
1061
|
run(`launchctl unload "${agentPlistPath}"`, { silent: true });
|
|
689
1062
|
} catch {
|
|
@@ -692,7 +1065,7 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
|
|
|
692
1065
|
unlinkSync(agentPlistPath);
|
|
693
1066
|
} catch {
|
|
694
1067
|
}
|
|
695
|
-
|
|
1068
|
+
writeFileSync5(agentPlistPath, plist);
|
|
696
1069
|
run(`launchctl load "${agentPlistPath}"`, { silent: true });
|
|
697
1070
|
success(`LaunchAgent installed and loaded (${spec.label}).`);
|
|
698
1071
|
console.log(` Logs: ${logDir}`);
|
|
@@ -703,9 +1076,9 @@ async function installLinux(spec) {
|
|
|
703
1076
|
const custieBin = detectCustieBin();
|
|
704
1077
|
const claudeBinDir = detectClaudeBinDir();
|
|
705
1078
|
const logDir = paths.LOG_DIR;
|
|
706
|
-
|
|
1079
|
+
mkdirSync4(logDir, { recursive: true });
|
|
707
1080
|
const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
|
|
708
|
-
const linuxbrewBin =
|
|
1081
|
+
const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
|
|
709
1082
|
const pathParts = [
|
|
710
1083
|
detectNodeBinDir(),
|
|
711
1084
|
claudeBinDir,
|
|
@@ -729,16 +1102,16 @@ RestartSec=5
|
|
|
729
1102
|
${envLines}
|
|
730
1103
|
Environment="PATH=${pathParts.join(":")}"
|
|
731
1104
|
|
|
732
|
-
StandardOutput=append:${
|
|
733
|
-
StandardError=append:${
|
|
1105
|
+
StandardOutput=append:${join5(logDir, `${spec.logName}.log`)}
|
|
1106
|
+
StandardError=append:${join5(logDir, `${spec.logName}-error.log`)}
|
|
734
1107
|
|
|
735
1108
|
[Install]
|
|
736
1109
|
WantedBy=default.target
|
|
737
1110
|
`;
|
|
738
1111
|
const unitName = spec.unitName;
|
|
739
|
-
const unitDir =
|
|
740
|
-
|
|
741
|
-
|
|
1112
|
+
const unitDir = join5(homedir2(), ".config", "systemd", "user");
|
|
1113
|
+
mkdirSync4(unitDir, { recursive: true });
|
|
1114
|
+
writeFileSync5(join5(unitDir, `${unitName}.service`), unit);
|
|
742
1115
|
run("systemctl --user daemon-reload", { silent: true });
|
|
743
1116
|
run(`systemctl --user enable --now ${unitName}`, { silent: true });
|
|
744
1117
|
success(`systemd user service installed and started (${unitName}).`);
|
|
@@ -748,277 +1121,100 @@ WantedBy=default.target
|
|
|
748
1121
|
async function installService(spec) {
|
|
749
1122
|
const os = platform2();
|
|
750
1123
|
if (os === "darwin") await installMacOS(spec);
|
|
751
|
-
else if (os === "linux") await installLinux(spec);
|
|
752
|
-
else {
|
|
753
|
-
warn(`Unsupported platform: ${os}. You'll need to configure autostart manually.`);
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
return true;
|
|
757
|
-
}
|
|
758
|
-
async function runInstall() {
|
|
759
|
-
ensureDirs();
|
|
760
|
-
await installService(profileSpec());
|
|
761
|
-
}
|
|
762
|
-
async function installHostService() {
|
|
763
|
-
ensureDirs();
|
|
764
|
-
return installService(hostSpec());
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// src/commands/profiles.ts
|
|
768
|
-
import { existsSync as existsSync4, readdirSync, statSync } from "fs";
|
|
769
|
-
import { execSync as execSync3 } from "child_process";
|
|
770
|
-
import { join as join4 } from "path";
|
|
771
|
-
import { platform as platform3 } from "os";
|
|
772
|
-
function listProfiles() {
|
|
773
|
-
const profiles = [];
|
|
774
|
-
if (existsSync4(paths.PROFILES_DIR)) {
|
|
775
|
-
for (const entry of readdirSync(paths.PROFILES_DIR)) {
|
|
776
|
-
try {
|
|
777
|
-
if (statSync(join4(paths.PROFILES_DIR, entry)).isDirectory()) {
|
|
778
|
-
profiles.push(entry);
|
|
779
|
-
}
|
|
780
|
-
} catch {
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
return profiles;
|
|
785
|
-
}
|
|
786
|
-
function isServiceRunning2(profile) {
|
|
787
|
-
try {
|
|
788
|
-
if (platform3() === "darwin") {
|
|
789
|
-
const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
|
|
790
|
-
return out.split("\n").some((line) => line.endsWith(` ${serviceLabel(profile)}`));
|
|
791
|
-
}
|
|
792
|
-
if (platform3() === "linux") {
|
|
793
|
-
execSync3(`systemctl --user is-active ${systemdUnitName(profile)}`, { stdio: "pipe" });
|
|
794
|
-
return true;
|
|
795
|
-
}
|
|
796
|
-
} catch {
|
|
797
|
-
}
|
|
798
|
-
return false;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// src/auth/device-auth.ts
|
|
802
|
-
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
|
|
803
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
804
|
-
import { dirname as dirname4, join as join5 } from "path";
|
|
805
|
-
var DEFAULT_API_URL = "https://app.flyspace.ai";
|
|
806
|
-
var EXPIRY_SKEW_MS = 6e4;
|
|
807
|
-
var AuthApiError = class extends Error {
|
|
808
|
-
constructor(message, status) {
|
|
809
|
-
super(message);
|
|
810
|
-
this.status = status;
|
|
811
|
-
this.name = "AuthApiError";
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
function apiUrl() {
|
|
815
|
-
return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
816
|
-
}
|
|
817
|
-
function deviceKeyPath() {
|
|
818
|
-
return join5(paths.BASE_CONFIG_DIR, "device.key");
|
|
819
|
-
}
|
|
820
|
-
function credentialsPath() {
|
|
821
|
-
return join5(paths.BASE_CONFIG_DIR, "credentials.json");
|
|
822
|
-
}
|
|
823
|
-
function ensureDeviceKey(path = deviceKeyPath()) {
|
|
824
|
-
let created = false;
|
|
825
|
-
if (!existsSync5(path)) {
|
|
826
|
-
const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
|
|
827
|
-
mkdirSync3(dirname4(path), { recursive: true });
|
|
828
|
-
writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
|
|
829
|
-
created = true;
|
|
830
|
-
}
|
|
831
|
-
const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
|
|
832
|
-
const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
|
|
833
|
-
"base64"
|
|
834
|
-
);
|
|
835
|
-
return { privateKey, publicKeyBase64, created };
|
|
836
|
-
}
|
|
837
|
-
function loadCredentials(path = credentialsPath()) {
|
|
838
|
-
if (!existsSync5(path)) return null;
|
|
839
|
-
try {
|
|
840
|
-
return JSON.parse(readFileSync5(path, "utf8"));
|
|
841
|
-
} catch {
|
|
842
|
-
return null;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
function saveCredentials(creds, path = credentialsPath()) {
|
|
846
|
-
mkdirSync3(dirname4(path), { recursive: true });
|
|
847
|
-
writeFileSync4(path, `${JSON.stringify(creds, null, 2)}
|
|
848
|
-
`, { mode: 384 });
|
|
849
|
-
}
|
|
850
|
-
function clearLocalAuth() {
|
|
851
|
-
rmSync2(credentialsPath(), { force: true });
|
|
852
|
-
rmSync2(deviceKeyPath(), { force: true });
|
|
853
|
-
}
|
|
854
|
-
async function post(base, path, body, token) {
|
|
855
|
-
return fetch(`${base}${path}`, {
|
|
856
|
-
method: "POST",
|
|
857
|
-
headers: {
|
|
858
|
-
"content-type": "application/json",
|
|
859
|
-
...token ? { authorization: `Bearer ${token}` } : {}
|
|
860
|
-
},
|
|
861
|
-
body: JSON.stringify(body)
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
async function readJson(res) {
|
|
865
|
-
try {
|
|
866
|
-
return await res.json();
|
|
867
|
-
} catch {
|
|
868
|
-
return {};
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
async function expectOk(res, what) {
|
|
872
|
-
const json = await readJson(res);
|
|
873
|
-
if (!res.ok) {
|
|
874
|
-
const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
|
|
875
|
-
throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
|
|
876
|
-
}
|
|
877
|
-
return json;
|
|
878
|
-
}
|
|
879
|
-
function toSlugs(value) {
|
|
880
|
-
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
881
|
-
}
|
|
882
|
-
function toGrant(json) {
|
|
883
|
-
const user = json["user"] ?? {};
|
|
884
|
-
return {
|
|
885
|
-
accessToken: String(json["access_token"]),
|
|
886
|
-
expiresIn: Number(json["expires_in"]),
|
|
887
|
-
deviceId: String(json["device_id"]),
|
|
888
|
-
userEmail: String(user["email"] ?? ""),
|
|
889
|
-
workspaces: toSlugs(json["workspaces"])
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
async function requestDeviceCode(base, input2) {
|
|
893
|
-
const res = await post(base, "/api/auth/device/code", {
|
|
894
|
-
public_key: input2.publicKey,
|
|
895
|
-
name: input2.name,
|
|
896
|
-
platform: input2.platform
|
|
897
|
-
});
|
|
898
|
-
const json = await expectOk(res, "Requesting a device code");
|
|
899
|
-
return {
|
|
900
|
-
deviceCode: String(json["device_code"]),
|
|
901
|
-
userCode: String(json["user_code"]),
|
|
902
|
-
verificationUri: String(json["verification_uri"]),
|
|
903
|
-
expiresIn: Number(json["expires_in"]),
|
|
904
|
-
interval: Number(json["interval"] ?? 5)
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
async function pollDeviceToken(base, deviceCode) {
|
|
908
|
-
const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
|
|
909
|
-
const json = await readJson(res);
|
|
910
|
-
if (res.ok) return { status: "approved", grant: toGrant(json) };
|
|
911
|
-
switch (json["error"]) {
|
|
912
|
-
case "authorization_pending":
|
|
913
|
-
return { status: "pending" };
|
|
914
|
-
case "slow_down":
|
|
915
|
-
return { status: "slow_down" };
|
|
916
|
-
case "access_denied":
|
|
917
|
-
return { status: "denied" };
|
|
918
|
-
case "expired_token":
|
|
919
|
-
return { status: "expired" };
|
|
920
|
-
default:
|
|
921
|
-
throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
|
|
1124
|
+
else if (os === "linux") await installLinux(spec);
|
|
1125
|
+
else {
|
|
1126
|
+
warn(`Unsupported platform: ${os}. You'll need to configure autostart manually.`);
|
|
1127
|
+
return false;
|
|
922
1128
|
}
|
|
1129
|
+
return true;
|
|
923
1130
|
}
|
|
924
|
-
async function
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
"Requesting a refresh challenge"
|
|
928
|
-
);
|
|
929
|
-
const nonce = String(challenge["nonce"]);
|
|
930
|
-
const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
|
|
931
|
-
const json = await expectOk(
|
|
932
|
-
await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
|
|
933
|
-
"Refreshing the access token"
|
|
934
|
-
);
|
|
935
|
-
return {
|
|
936
|
-
...creds,
|
|
937
|
-
accessToken: String(json["access_token"]),
|
|
938
|
-
accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
|
|
939
|
-
};
|
|
1131
|
+
async function runInstall() {
|
|
1132
|
+
ensureDirs();
|
|
1133
|
+
await installService(profileSpec());
|
|
940
1134
|
}
|
|
941
|
-
async function
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const user = json["user"] ?? {};
|
|
945
|
-
const device = json["device"] ?? {};
|
|
946
|
-
return {
|
|
947
|
-
userEmail: String(user["email"] ?? ""),
|
|
948
|
-
deviceId: String(device["id"] ?? ""),
|
|
949
|
-
deviceName: String(device["name"] ?? ""),
|
|
950
|
-
workspaces: toSlugs(json["workspaces"])
|
|
951
|
-
};
|
|
1135
|
+
async function installHostService() {
|
|
1136
|
+
ensureDirs();
|
|
1137
|
+
return installService(hostSpec());
|
|
952
1138
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1139
|
+
|
|
1140
|
+
// src/commands/profiles.ts
|
|
1141
|
+
import { existsSync as existsSync5, readdirSync, statSync } from "fs";
|
|
1142
|
+
import { execSync as execSync3 } from "child_process";
|
|
1143
|
+
import { join as join6 } from "path";
|
|
1144
|
+
import { platform as platform3 } from "os";
|
|
1145
|
+
function listProfiles() {
|
|
1146
|
+
const profiles = [];
|
|
1147
|
+
if (existsSync5(paths.PROFILES_DIR)) {
|
|
1148
|
+
for (const entry of readdirSync(paths.PROFILES_DIR)) {
|
|
1149
|
+
try {
|
|
1150
|
+
if (statSync(join6(paths.PROFILES_DIR, entry)).isDirectory()) {
|
|
1151
|
+
profiles.push(entry);
|
|
1152
|
+
}
|
|
1153
|
+
} catch {
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return profiles;
|
|
1158
|
+
}
|
|
1159
|
+
function isServiceRunning2(profile) {
|
|
956
1160
|
try {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1161
|
+
if (platform3() === "darwin") {
|
|
1162
|
+
const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
|
|
1163
|
+
return out.split("\n").some((line) => line.endsWith(` ${serviceLabel(profile)}`));
|
|
1164
|
+
}
|
|
1165
|
+
if (platform3() === "linux") {
|
|
1166
|
+
execSync3(`systemctl --user is-active ${systemdUnitName(profile)}`, { stdio: "pipe" });
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
960
1169
|
} catch {
|
|
961
|
-
return { workspaces: creds.workspaces ?? [], fresh: false };
|
|
962
1170
|
}
|
|
963
|
-
|
|
964
|
-
async function revokeDevice(base, token) {
|
|
965
|
-
await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
|
|
966
|
-
}
|
|
967
|
-
async function getAccessToken() {
|
|
968
|
-
const fromEnv = process.env["FLYSPACE_TOKEN"];
|
|
969
|
-
if (fromEnv) return fromEnv;
|
|
970
|
-
const creds = loadCredentials();
|
|
971
|
-
if (!creds) return null;
|
|
972
|
-
if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
|
|
973
|
-
const { privateKey } = ensureDeviceKey();
|
|
974
|
-
const refreshed = await refreshAccessToken(creds, privateKey);
|
|
975
|
-
saveCredentials(refreshed);
|
|
976
|
-
return refreshed.accessToken;
|
|
1171
|
+
return false;
|
|
977
1172
|
}
|
|
978
1173
|
|
|
979
1174
|
// src/dashboard/onboarding-scan.ts
|
|
980
|
-
import { execFileSync } from "child_process";
|
|
1175
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
981
1176
|
import {
|
|
1177
|
+
cpSync,
|
|
982
1178
|
existsSync as existsSync6,
|
|
983
1179
|
lstatSync,
|
|
984
|
-
mkdirSync as
|
|
1180
|
+
mkdirSync as mkdirSync5,
|
|
985
1181
|
readdirSync as readdirSync2,
|
|
986
|
-
readFileSync as
|
|
1182
|
+
readFileSync as readFileSync7,
|
|
987
1183
|
statSync as statSync2,
|
|
988
1184
|
symlinkSync
|
|
989
1185
|
} from "fs";
|
|
990
1186
|
import { homedir as homedir3 } from "os";
|
|
991
|
-
import { basename as basename2, join as
|
|
1187
|
+
import { basename as basename2, join as join7, resolve as resolve3 } from "path";
|
|
992
1188
|
var MAX_JSONL_HEAD_BYTES = 64 * 1024;
|
|
993
1189
|
var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
|
|
994
1190
|
function flyspaceRoot() {
|
|
995
|
-
return process.env["FLYSPACE_ROOT"] ||
|
|
1191
|
+
return process.env["FLYSPACE_ROOT"] || join7(homedir3(), "Flyspace");
|
|
996
1192
|
}
|
|
997
1193
|
function onPath(cmd) {
|
|
998
1194
|
try {
|
|
999
|
-
|
|
1195
|
+
execFileSync2(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
1000
1196
|
return true;
|
|
1001
1197
|
} catch {
|
|
1002
1198
|
return false;
|
|
1003
1199
|
}
|
|
1004
1200
|
}
|
|
1005
1201
|
function claudeDir() {
|
|
1006
|
-
return process.env["CLAUDE_CONFIG_DIR"] ||
|
|
1202
|
+
return process.env["CLAUDE_CONFIG_DIR"] || join7(homedir3(), ".claude");
|
|
1007
1203
|
}
|
|
1008
1204
|
function codexDir() {
|
|
1009
|
-
return process.env["CODEX_HOME"] ||
|
|
1205
|
+
return process.env["CODEX_HOME"] || join7(homedir3(), ".codex");
|
|
1010
1206
|
}
|
|
1011
1207
|
function claudeLoggedIn() {
|
|
1012
1208
|
if (process.env["ANTHROPIC_API_KEY"]) return true;
|
|
1013
|
-
if (existsSync6(
|
|
1209
|
+
if (existsSync6(join7(claudeDir(), ".credentials.json"))) return true;
|
|
1014
1210
|
try {
|
|
1015
|
-
const raw = JSON.parse(
|
|
1211
|
+
const raw = JSON.parse(readFileSync7(join7(homedir3(), ".claude.json"), "utf-8"));
|
|
1016
1212
|
if (raw.oauthAccount) return true;
|
|
1017
1213
|
} catch {
|
|
1018
1214
|
}
|
|
1019
1215
|
if (process.platform !== "darwin") return false;
|
|
1020
1216
|
try {
|
|
1021
|
-
|
|
1217
|
+
execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
1022
1218
|
stdio: "ignore"
|
|
1023
1219
|
});
|
|
1024
1220
|
return true;
|
|
@@ -1027,7 +1223,7 @@ function claudeLoggedIn() {
|
|
|
1027
1223
|
}
|
|
1028
1224
|
}
|
|
1029
1225
|
function codexLoggedIn() {
|
|
1030
|
-
return !!process.env["OPENAI_API_KEY"] || existsSync6(
|
|
1226
|
+
return !!process.env["OPENAI_API_KEY"] || existsSync6(join7(codexDir(), "auth.json"));
|
|
1031
1227
|
}
|
|
1032
1228
|
function jsonlFiles(dir, depth) {
|
|
1033
1229
|
const out = [];
|
|
@@ -1038,7 +1234,7 @@ function jsonlFiles(dir, depth) {
|
|
|
1038
1234
|
return out;
|
|
1039
1235
|
}
|
|
1040
1236
|
for (const e of entries2) {
|
|
1041
|
-
const full =
|
|
1237
|
+
const full = join7(dir, e.name);
|
|
1042
1238
|
if (e.isDirectory()) {
|
|
1043
1239
|
if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
|
|
1044
1240
|
} else if (e.name.endsWith(".jsonl")) {
|
|
@@ -1053,7 +1249,7 @@ function jsonlFiles(dir, depth) {
|
|
|
1053
1249
|
function cwdFromTranscript(file) {
|
|
1054
1250
|
let head;
|
|
1055
1251
|
try {
|
|
1056
|
-
head =
|
|
1252
|
+
head = readFileSync7(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
|
|
1057
1253
|
} catch {
|
|
1058
1254
|
return null;
|
|
1059
1255
|
}
|
|
@@ -1070,7 +1266,7 @@ function cwdFromTranscript(file) {
|
|
|
1070
1266
|
}
|
|
1071
1267
|
function codexTrustedPaths() {
|
|
1072
1268
|
try {
|
|
1073
|
-
const toml =
|
|
1269
|
+
const toml = readFileSync7(join7(codexDir(), "config.toml"), "utf-8");
|
|
1074
1270
|
return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
|
|
1075
1271
|
} catch {
|
|
1076
1272
|
return [];
|
|
@@ -1093,7 +1289,7 @@ function gitRepos() {
|
|
|
1093
1289
|
const home = homedir3();
|
|
1094
1290
|
const found = [];
|
|
1095
1291
|
for (const rootName of GIT_SCAN_ROOTS) {
|
|
1096
|
-
const root =
|
|
1292
|
+
const root = join7(home, rootName);
|
|
1097
1293
|
let entries2;
|
|
1098
1294
|
try {
|
|
1099
1295
|
entries2 = readdirSync2(root, { withFileTypes: true });
|
|
@@ -1101,7 +1297,7 @@ function gitRepos() {
|
|
|
1101
1297
|
continue;
|
|
1102
1298
|
}
|
|
1103
1299
|
for (const e of entries2) {
|
|
1104
|
-
if (e.isDirectory() && existsSync6(
|
|
1300
|
+
if (e.isDirectory() && existsSync6(join7(root, e.name, ".git"))) found.push(join7(root, e.name));
|
|
1105
1301
|
}
|
|
1106
1302
|
}
|
|
1107
1303
|
return found;
|
|
@@ -1145,11 +1341,11 @@ function scanOnboarding() {
|
|
|
1145
1341
|
});
|
|
1146
1342
|
}
|
|
1147
1343
|
};
|
|
1148
|
-
const claudeProjects =
|
|
1344
|
+
const claudeProjects = join7(claudeDir(), "projects");
|
|
1149
1345
|
try {
|
|
1150
1346
|
for (const e of readdirSync2(claudeProjects, { withFileTypes: true })) {
|
|
1151
1347
|
if (!e.isDirectory()) continue;
|
|
1152
|
-
const files = jsonlFiles(
|
|
1348
|
+
const files = jsonlFiles(join7(claudeProjects, e.name), 0);
|
|
1153
1349
|
if (!files.length) continue;
|
|
1154
1350
|
history.claude = true;
|
|
1155
1351
|
files.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -1162,7 +1358,7 @@ function scanOnboarding() {
|
|
|
1162
1358
|
}
|
|
1163
1359
|
} catch {
|
|
1164
1360
|
}
|
|
1165
|
-
const codexSessions = jsonlFiles(
|
|
1361
|
+
const codexSessions = jsonlFiles(join7(codexDir(), "sessions"), 4);
|
|
1166
1362
|
if (codexSessions.length) history.codex = true;
|
|
1167
1363
|
const perCwd = /* @__PURE__ */ new Map();
|
|
1168
1364
|
for (const f of codexSessions) {
|
|
@@ -1199,17 +1395,18 @@ function scanOnboarding() {
|
|
|
1199
1395
|
fromGitScan
|
|
1200
1396
|
};
|
|
1201
1397
|
}
|
|
1398
|
+
var GENERAL_TEMPLATE_DIR = join7(paths.PACKAGE_ROOT, "templates", "general");
|
|
1202
1399
|
function makeLink(target, linkPath) {
|
|
1203
1400
|
symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
|
|
1204
1401
|
}
|
|
1205
1402
|
function linkProjectFolders(slug, items) {
|
|
1206
1403
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
|
|
1207
|
-
const dir =
|
|
1208
|
-
|
|
1404
|
+
const dir = join7(flyspaceRoot(), slug);
|
|
1405
|
+
mkdirSync5(dir, { recursive: true });
|
|
1209
1406
|
const results = [];
|
|
1210
1407
|
for (const item of items) {
|
|
1211
1408
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
|
|
1212
|
-
const dest =
|
|
1409
|
+
const dest = join7(dir, item.name);
|
|
1213
1410
|
let exists = true;
|
|
1214
1411
|
try {
|
|
1215
1412
|
lstatSync(dest);
|
|
@@ -1222,7 +1419,8 @@ function linkProjectFolders(slug, items) {
|
|
|
1222
1419
|
if (!statSync2(target).isDirectory()) throw new Error(`not a directory: ${target}`);
|
|
1223
1420
|
makeLink(target, dest);
|
|
1224
1421
|
} else {
|
|
1225
|
-
|
|
1422
|
+
mkdirSync5(dest, { recursive: true });
|
|
1423
|
+
if (item.name === "general") cpSync(GENERAL_TEMPLATE_DIR, dest, { recursive: true });
|
|
1226
1424
|
}
|
|
1227
1425
|
}
|
|
1228
1426
|
results.push({ name: item.name, cwd: `${slug}/${item.name}` });
|
|
@@ -1540,7 +1738,7 @@ async function runAuthCmd(args) {
|
|
|
1540
1738
|
}
|
|
1541
1739
|
|
|
1542
1740
|
// src/version.ts
|
|
1543
|
-
var CLI_VERSION = true ? "0.1.
|
|
1741
|
+
var CLI_VERSION = true ? "0.1.14" : null;
|
|
1544
1742
|
function isOlderVersion(a, b) {
|
|
1545
1743
|
const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
1546
1744
|
const [pa, pb] = [parse2(a), parse2(b)];
|
|
@@ -1654,8 +1852,8 @@ ${BOLD2}Flyspace Setup${RESET2}
|
|
|
1654
1852
|
const anyReady = runners.some((r) => r.installed && r.loggedIn);
|
|
1655
1853
|
for (const slug of workspaces) {
|
|
1656
1854
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) continue;
|
|
1657
|
-
const dir =
|
|
1658
|
-
|
|
1855
|
+
const dir = join8(flyspaceRoot(), slug);
|
|
1856
|
+
mkdirSync6(dir, { recursive: true });
|
|
1659
1857
|
ok(`Folder ${tilde2(dir)}`);
|
|
1660
1858
|
}
|
|
1661
1859
|
let hostRunning = false;
|
|
@@ -1818,10 +2016,10 @@ ${BOLD3}Flyspace Doctor${RESET3}
|
|
|
1818
2016
|
}
|
|
1819
2017
|
|
|
1820
2018
|
// src/commands/uninstall.ts
|
|
1821
|
-
import { existsSync as existsSync7, readFileSync as
|
|
2019
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
1822
2020
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1823
2021
|
import { execSync as execSync5 } from "child_process";
|
|
1824
|
-
import { basename as basename3, join as
|
|
2022
|
+
import { basename as basename3, join as join9 } from "path";
|
|
1825
2023
|
import { homedir as homedir4, platform as platform5 } from "os";
|
|
1826
2024
|
function log2(msg) {
|
|
1827
2025
|
console.log(`
|
|
@@ -1864,7 +2062,7 @@ async function runServiceUninstall(profile = getProfile()) {
|
|
|
1864
2062
|
run2(`systemctl --user disable --now ${unitName}`);
|
|
1865
2063
|
} catch {
|
|
1866
2064
|
}
|
|
1867
|
-
const unitPath =
|
|
2065
|
+
const unitPath = join9(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
|
|
1868
2066
|
try {
|
|
1869
2067
|
unlinkSync2(unitPath);
|
|
1870
2068
|
} catch {
|
|
@@ -1879,7 +2077,7 @@ async function runServiceUninstall(profile = getProfile()) {
|
|
|
1879
2077
|
}
|
|
1880
2078
|
}
|
|
1881
2079
|
async function runUninstall(args = []) {
|
|
1882
|
-
const prefix = process.env["FLYSPACE_PREFIX"] ||
|
|
2080
|
+
const prefix = process.env["FLYSPACE_PREFIX"] || join9(homedir4(), ".flyspace");
|
|
1883
2081
|
const targets = [paths.BASE_CONFIG_DIR, paths.BASE_DATA_DIR, prefix].filter(existsSync7);
|
|
1884
2082
|
console.log("\n This removes Flyspace from this machine:");
|
|
1885
2083
|
console.log(" - revokes this device and removes every background service");
|
|
@@ -1905,15 +2103,15 @@ async function runUninstall(args = []) {
|
|
|
1905
2103
|
await runServiceUninstall(profile);
|
|
1906
2104
|
}
|
|
1907
2105
|
for (const t of targets) {
|
|
1908
|
-
|
|
2106
|
+
rmSync4(t, { recursive: true, force: true });
|
|
1909
2107
|
success2(`Deleted ${t}`);
|
|
1910
2108
|
}
|
|
1911
|
-
for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) =>
|
|
2109
|
+
for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) => join9(homedir4(), f))) {
|
|
1912
2110
|
if (!existsSync7(rc)) continue;
|
|
1913
|
-
const before =
|
|
2111
|
+
const before = readFileSync8(rc, "utf-8");
|
|
1914
2112
|
const after = before.replace(/\n# Flyspace CLI\nexport PATH="[^"\n]*\.flyspace\/bin:\$PATH"\n?/g, "\n");
|
|
1915
2113
|
if (after !== before) {
|
|
1916
|
-
|
|
2114
|
+
writeFileSync6(rc, after);
|
|
1917
2115
|
success2(`Removed PATH entry from ${rc}`);
|
|
1918
2116
|
}
|
|
1919
2117
|
}
|
|
@@ -1921,7 +2119,7 @@ async function runUninstall(args = []) {
|
|
|
1921
2119
|
}
|
|
1922
2120
|
|
|
1923
2121
|
// src/commands/profiles-cmd.ts
|
|
1924
|
-
import { rmSync as
|
|
2122
|
+
import { rmSync as rmSync5 } from "fs";
|
|
1925
2123
|
var USAGE = `
|
|
1926
2124
|
Usage: flyspace profiles [list]
|
|
1927
2125
|
flyspace profiles remove <name>
|
|
@@ -1978,7 +2176,7 @@ async function removeCmd(name) {
|
|
|
1978
2176
|
}
|
|
1979
2177
|
const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
|
|
1980
2178
|
if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
|
|
1981
|
-
|
|
2179
|
+
rmSync5(configDir2, { recursive: true, force: true });
|
|
1982
2180
|
console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
|
|
1983
2181
|
console.log(`
|
|
1984
2182
|
Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
|
|
@@ -1996,7 +2194,7 @@ async function runProfiles(args = []) {
|
|
|
1996
2194
|
// src/commands/upgrade.ts
|
|
1997
2195
|
import { execSync as execSync6 } from "child_process";
|
|
1998
2196
|
import { realpathSync as realpathSync2 } from "fs";
|
|
1999
|
-
import { join as
|
|
2197
|
+
import { join as join10 } from "path";
|
|
2000
2198
|
function restartInstalledProfiles() {
|
|
2001
2199
|
if (isHostServiceInstalled()) {
|
|
2002
2200
|
console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
|
|
@@ -2029,10 +2227,10 @@ function npmInstallCommand() {
|
|
|
2029
2227
|
}
|
|
2030
2228
|
function npmEnv() {
|
|
2031
2229
|
const home = flyspaceHome();
|
|
2032
|
-
return home ? { ...process.env, PATH: `${
|
|
2230
|
+
return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
|
|
2033
2231
|
}
|
|
2034
2232
|
async function runUpgrade() {
|
|
2035
|
-
const current = "0.1.
|
|
2233
|
+
const current = "0.1.14";
|
|
2036
2234
|
console.log(`Current version: ${current}`);
|
|
2037
2235
|
console.log("Checking for updates...\n");
|
|
2038
2236
|
try {
|
|
@@ -2646,11 +2844,11 @@ async function runRestart() {
|
|
|
2646
2844
|
// src/commands/logs.ts
|
|
2647
2845
|
import { spawn as spawn3 } from "child_process";
|
|
2648
2846
|
import { existsSync as existsSync9 } from "fs";
|
|
2649
|
-
import { join as
|
|
2847
|
+
import { join as join11 } from "path";
|
|
2650
2848
|
async function runLogs(args) {
|
|
2651
2849
|
const showErrors = args.includes("--error") || args.includes("-e");
|
|
2652
2850
|
const filename = showErrors ? "custie-error.log" : "custie.log";
|
|
2653
|
-
const logFile =
|
|
2851
|
+
const logFile = join11(paths.LOG_DIR, filename);
|
|
2654
2852
|
if (!existsSync9(logFile)) {
|
|
2655
2853
|
console.error(`Log file not found: ${logFile}`);
|
|
2656
2854
|
console.error("Is the service installed? Run `flyspace install` first.");
|
|
@@ -2679,7 +2877,7 @@ import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts"
|
|
|
2679
2877
|
import { WebClient as WebClient3 } from "@slack/web-api";
|
|
2680
2878
|
|
|
2681
2879
|
// src/store/auth-links.ts
|
|
2682
|
-
import { createClient } from "@supabase/supabase-js";
|
|
2880
|
+
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
2683
2881
|
function toAuthLink(data) {
|
|
2684
2882
|
const userId = data.user?.id;
|
|
2685
2883
|
const actionLink = data.properties?.action_link;
|
|
@@ -2703,7 +2901,7 @@ async function mintSession(email) {
|
|
|
2703
2901
|
if (error2) throw new Error(error2.message);
|
|
2704
2902
|
const tokenHash = data.properties?.hashed_token;
|
|
2705
2903
|
if (!tokenHash) throw new Error("Supabase returned no token hash");
|
|
2706
|
-
const scratch =
|
|
2904
|
+
const scratch = createClient2(process.env["SUPABASE_URL"], process.env["SUPABASE_SECRET_KEY"], {
|
|
2707
2905
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
2708
2906
|
});
|
|
2709
2907
|
const verified = await scratch.auth.verifyOtp({ token_hash: tokenHash, type: "magiclink" });
|
|
@@ -2729,7 +2927,7 @@ async function emailForToken(token, type) {
|
|
|
2729
2927
|
|
|
2730
2928
|
// src/dashboard/profile-config.ts
|
|
2731
2929
|
import { homedir as homedir5 } from "os";
|
|
2732
|
-
import { join as
|
|
2930
|
+
import { join as join12 } from "path";
|
|
2733
2931
|
|
|
2734
2932
|
// src/dashboard/profile-config-cache.ts
|
|
2735
2933
|
var cache = /* @__PURE__ */ new Map();
|
|
@@ -2748,7 +2946,7 @@ function profileEnv(key) {
|
|
|
2748
2946
|
function profileClaudeConfigDir() {
|
|
2749
2947
|
const config = cachedProfileConfig();
|
|
2750
2948
|
if (!config) return process.env["CLAUDE_CONFIG_DIR"] || void 0;
|
|
2751
|
-
return config.env["CLAUDE_CONFIG_DIR"]?.trim() ||
|
|
2949
|
+
return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join12(homedir5(), ".claude");
|
|
2752
2950
|
}
|
|
2753
2951
|
function profileOauthToken() {
|
|
2754
2952
|
const env = cachedProfileConfig()?.env ?? {};
|
|
@@ -2850,7 +3048,7 @@ async function sendEmail(email) {
|
|
|
2850
3048
|
}
|
|
2851
3049
|
|
|
2852
3050
|
// src/store/invite-store.ts
|
|
2853
|
-
import { createHash, randomBytes } from "crypto";
|
|
3051
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
2854
3052
|
var INVITE_TTL_DAYS = 7;
|
|
2855
3053
|
var INVITE_MESSAGE_MAX = 500;
|
|
2856
3054
|
function hashInviteToken(token) {
|
|
@@ -2860,7 +3058,7 @@ async function issueInvite(membershipId, message) {
|
|
|
2860
3058
|
const db = getSupabaseClient();
|
|
2861
3059
|
const { error: clearError } = await db.from("workspace_invites").delete().eq("membership_id", membershipId).is("accepted_at", null);
|
|
2862
3060
|
if (clearError) throw new Error(`issueInvite(${membershipId}): ${clearError.message}`);
|
|
2863
|
-
const token =
|
|
3061
|
+
const token = randomBytes2(32).toString("hex");
|
|
2864
3062
|
const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
2865
3063
|
const { error: error2 } = await db.from("workspace_invites").insert({
|
|
2866
3064
|
membership_id: membershipId,
|
|
@@ -3300,9 +3498,9 @@ function workspaceOrigin(c, slug) {
|
|
|
3300
3498
|
}
|
|
3301
3499
|
|
|
3302
3500
|
// src/store/setup-invite-store.ts
|
|
3303
|
-
import { randomBytes as
|
|
3501
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3304
3502
|
async function issueSetupInvite(params) {
|
|
3305
|
-
const token =
|
|
3503
|
+
const token = randomBytes3(32).toString("hex");
|
|
3306
3504
|
const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
3307
3505
|
const { error: error2 } = await getSupabaseClient().from("workspace_setup_invites").insert({
|
|
3308
3506
|
email: params.email,
|
|
@@ -3329,7 +3527,7 @@ async function reissueSetupInvite(id) {
|
|
|
3329
3527
|
const { data, error: error2 } = await db.from("workspace_setup_invites").select("email, owner_name, message").eq("id", id).is("accepted_at", null).maybeSingle();
|
|
3330
3528
|
if (error2) throw new Error(`reissueSetupInvite(${id}): ${error2.message}`);
|
|
3331
3529
|
if (!data) throw new Error("no pending invite with that id");
|
|
3332
|
-
const token =
|
|
3530
|
+
const token = randomBytes3(32).toString("hex");
|
|
3333
3531
|
const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
3334
3532
|
const { error: updateError } = await db.from("workspace_setup_invites").update({ token_hash: hashInviteToken(token), expires_at: expiresAt.toISOString() }).eq("id", id);
|
|
3335
3533
|
if (updateError) throw new Error(`reissueSetupInvite(${id}): ${updateError.message}`);
|
|
@@ -4208,7 +4406,7 @@ import { serve } from "@hono/node-server";
|
|
|
4208
4406
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4209
4407
|
|
|
4210
4408
|
// src/reconcile.ts
|
|
4211
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
4409
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4212
4410
|
async function boundWorkspaces2() {
|
|
4213
4411
|
return (await boundWorkspaces())?.workspaces ?? null;
|
|
4214
4412
|
}
|
|
@@ -4225,7 +4423,7 @@ async function reconcileProfile(profile) {
|
|
|
4225
4423
|
if (!hadConfigDir) log5.push(`config dir created (${paths.CONFIG_DIR})`);
|
|
4226
4424
|
if (!hadDataDir) log5.push(`data dir created (${paths.DATA_DIR})`);
|
|
4227
4425
|
if (!existsSync10(paths.CONFIG_FILE)) {
|
|
4228
|
-
|
|
4426
|
+
writeFileSync7(paths.CONFIG_FILE, `BOT_NAME=${profile}
|
|
4229
4427
|
`);
|
|
4230
4428
|
log5.push(`config.env written (${paths.CONFIG_FILE})`);
|
|
4231
4429
|
}
|
|
@@ -4234,7 +4432,7 @@ async function reconcileProfile(profile) {
|
|
|
4234
4432
|
if (!channel.cwd) continue;
|
|
4235
4433
|
const cwd = expandPath(channel.cwd);
|
|
4236
4434
|
if (isCwdUsable(cwd)) continue;
|
|
4237
|
-
|
|
4435
|
+
mkdirSync7(cwd, { recursive: true });
|
|
4238
4436
|
log5.push(`channel "${channel.name}" cwd created (${cwd})`);
|
|
4239
4437
|
}
|
|
4240
4438
|
return log5;
|
|
@@ -4396,8 +4594,10 @@ async function filterReadableDocsChannels(ctx, channelIds) {
|
|
|
4396
4594
|
// src/dashboard/docs-access-gate.ts
|
|
4397
4595
|
var requireDocsAccess = async (c, next) => {
|
|
4398
4596
|
const auth = c.get("auth");
|
|
4597
|
+
const channelId = c.req.param("id") ?? "";
|
|
4598
|
+
if (findChannelById(channelId)?.profile !== getProfile()) return c.json({ error: "channel not found" }, 404);
|
|
4399
4599
|
const need = c.req.method === "GET" ? "read" : "write";
|
|
4400
|
-
const have = auth ? await resolveDocsAccess(auth,
|
|
4600
|
+
const have = auth ? await resolveDocsAccess(auth, channelId) : "none";
|
|
4401
4601
|
if (!docsAccessAtLeast(have, need)) return c.json({ error: "docs access required" }, 403);
|
|
4402
4602
|
await next();
|
|
4403
4603
|
};
|
|
@@ -4406,8 +4606,8 @@ var requireDocsAccess = async (c, next) => {
|
|
|
4406
4606
|
import { Hono as Hono31 } from "hono";
|
|
4407
4607
|
|
|
4408
4608
|
// src/dashboard/public-share-routes.ts
|
|
4409
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4410
|
-
import { join as
|
|
4609
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
|
|
4610
|
+
import { join as join14 } from "path";
|
|
4411
4611
|
import { Hono as Hono3 } from "hono";
|
|
4412
4612
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
4413
4613
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
@@ -4872,12 +5072,12 @@ function injectShareHead(html, head) {
|
|
|
4872
5072
|
|
|
4873
5073
|
// src/dashboard/app-icon.ts
|
|
4874
5074
|
import { createHash as createHash2 } from "crypto";
|
|
4875
|
-
import { readFileSync as
|
|
4876
|
-
import { join as
|
|
5075
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
5076
|
+
import { join as join13 } from "path";
|
|
4877
5077
|
import sharp from "sharp";
|
|
4878
5078
|
var PLATE = "#0A0A0F";
|
|
4879
|
-
var MARK_FILE =
|
|
4880
|
-
var MARK_SVG_FILE =
|
|
5079
|
+
var MARK_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
|
|
5080
|
+
var MARK_SVG_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
|
|
4881
5081
|
var LAYOUT = {
|
|
4882
5082
|
standard: { logo: 0.7, logoCx: 0.5, logoCy: 0.48, badge: 0.27, badgeCx: 0.22, badgeCy: 0.76 },
|
|
4883
5083
|
maskable: { logo: 0.56, logoCx: 0.5, logoCy: 0.48, badge: 0.23, badgeCx: 0.33, badgeCy: 0.66 }
|
|
@@ -4900,7 +5100,7 @@ function roundedPlate(size, fill = PLATE) {
|
|
|
4900
5100
|
}
|
|
4901
5101
|
async function renderBadge(box) {
|
|
4902
5102
|
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
4903
|
-
const raster = await sharp(
|
|
5103
|
+
const raster = await sharp(readFileSync9(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4904
5104
|
return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4905
5105
|
}
|
|
4906
5106
|
async function sampleBackground(logo) {
|
|
@@ -4955,7 +5155,7 @@ async function renderAppIcon({
|
|
|
4955
5155
|
if (hit) return hit;
|
|
4956
5156
|
const out = logo && await composeIcon(logo, size, maskable, plain).catch(() => null) || // No workspace logo to lead with — the stock mark is still the best icon,
|
|
4957
5157
|
// either on its usual dark plate or, for a favicon, trimmed and bare.
|
|
4958
|
-
(transparent ? await renderBadge(size) : await sharp(
|
|
5158
|
+
(transparent ? await renderBadge(size) : await sharp(readFileSync9(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
|
|
4959
5159
|
cache3.set(key, out);
|
|
4960
5160
|
return out;
|
|
4961
5161
|
}
|
|
@@ -6173,7 +6373,7 @@ var shellCache = null;
|
|
|
6173
6373
|
function readShell(path) {
|
|
6174
6374
|
const { mtimeMs } = statSync3(path);
|
|
6175
6375
|
if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
|
|
6176
|
-
const html =
|
|
6376
|
+
const html = readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
|
|
6177
6377
|
shellCache = { mtimeMs, html };
|
|
6178
6378
|
return html;
|
|
6179
6379
|
}
|
|
@@ -6193,17 +6393,17 @@ function serveBuildFile(c, webDist, dir, name) {
|
|
|
6193
6393
|
if (!type || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes("..")) {
|
|
6194
6394
|
return c.text("not found", 404);
|
|
6195
6395
|
}
|
|
6196
|
-
const path =
|
|
6396
|
+
const path = join14(webDist, dir, name);
|
|
6197
6397
|
if (!existsSync11(path)) return c.text("not found", 404);
|
|
6198
6398
|
c.header("Content-Type", type);
|
|
6199
6399
|
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
6200
6400
|
if (ext === ".css") {
|
|
6201
|
-
return c.body(
|
|
6401
|
+
return c.body(readFileSync10(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
|
|
6202
6402
|
}
|
|
6203
6403
|
if (ext === ".js") {
|
|
6204
|
-
return c.body(
|
|
6404
|
+
return c.body(readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
|
|
6205
6405
|
}
|
|
6206
|
-
return c.body(new Uint8Array(
|
|
6406
|
+
return c.body(new Uint8Array(readFileSync10(path)));
|
|
6207
6407
|
}
|
|
6208
6408
|
var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
|
|
6209
6409
|
function servePublishedHtml(c, share) {
|
|
@@ -6339,7 +6539,7 @@ ${analytics}` : head);
|
|
|
6339
6539
|
const requested = c.req.param("lang");
|
|
6340
6540
|
const lang = canonicalLanguage(requested);
|
|
6341
6541
|
if (!isTranslatableLanguage(lang)) return next();
|
|
6342
|
-
const shell =
|
|
6542
|
+
const shell = join14(webDist, "index.html");
|
|
6343
6543
|
if (!existsSync11(shell)) return next();
|
|
6344
6544
|
const share = await getDocShare(c.req.param("id"));
|
|
6345
6545
|
if (!share) return next();
|
|
@@ -6375,7 +6575,7 @@ ${analytics}` : head);
|
|
|
6375
6575
|
const share = await getDocShare(c.req.param("id"));
|
|
6376
6576
|
if (!share) return next();
|
|
6377
6577
|
if (share.kind === "html") return servePublishedHtml(c, share);
|
|
6378
|
-
const shell =
|
|
6578
|
+
const shell = join14(webDist, "index.html");
|
|
6379
6579
|
if (!existsSync11(shell)) return next();
|
|
6380
6580
|
return readerPage(
|
|
6381
6581
|
c,
|
|
@@ -6488,9 +6688,9 @@ function routeCookieWrites(all, label2, writes) {
|
|
|
6488
6688
|
}
|
|
6489
6689
|
|
|
6490
6690
|
// src/dashboard/supabase-server-client.ts
|
|
6491
|
-
var
|
|
6691
|
+
var REQUEST_TIMEOUT_MS2 = 5e3;
|
|
6492
6692
|
function fetchWithTimeout(input2, init) {
|
|
6493
|
-
return fetch(input2, { ...init, signal: AbortSignal.timeout(
|
|
6693
|
+
return fetch(input2, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
|
|
6494
6694
|
}
|
|
6495
6695
|
function createRequestSupabaseClient(c) {
|
|
6496
6696
|
const url = process.env["SUPABASE_URL"];
|
|
@@ -6642,13 +6842,13 @@ async function signAvatarUrl(key) {
|
|
|
6642
6842
|
|
|
6643
6843
|
// src/dashboard/turnstile.ts
|
|
6644
6844
|
var VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
6645
|
-
var
|
|
6845
|
+
var REQUEST_TIMEOUT_MS3 = 5e3;
|
|
6646
6846
|
function siteverify(secret, token) {
|
|
6647
6847
|
return fetch(VERIFY_URL, {
|
|
6648
6848
|
method: "POST",
|
|
6649
6849
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
6650
6850
|
body: new URLSearchParams({ secret, response: token }),
|
|
6651
|
-
signal: AbortSignal.timeout(
|
|
6851
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
|
|
6652
6852
|
});
|
|
6653
6853
|
}
|
|
6654
6854
|
async function verifyTurnstileToken(token) {
|
|
@@ -7001,12 +7201,12 @@ async function offlineAgentIdentities() {
|
|
|
7001
7201
|
}
|
|
7002
7202
|
|
|
7003
7203
|
// src/dashboard/host-api.ts
|
|
7004
|
-
import { existsSync as existsSync20, readFileSync as
|
|
7005
|
-
import { join as
|
|
7204
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14, statSync as statSync8 } from "fs";
|
|
7205
|
+
import { join as join21 } from "path";
|
|
7006
7206
|
import sharp3 from "sharp";
|
|
7007
7207
|
|
|
7008
7208
|
// src/dashboard/channels-routes.ts
|
|
7009
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
7209
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
|
|
7010
7210
|
import { Hono as Hono9 } from "hono";
|
|
7011
7211
|
|
|
7012
7212
|
// src/dashboard/feature-flags-routes.ts
|
|
@@ -7154,7 +7354,7 @@ function coerceMemberEmails(raw) {
|
|
|
7154
7354
|
}
|
|
7155
7355
|
function ensureCwdDir(cwd) {
|
|
7156
7356
|
const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
|
|
7157
|
-
if (!existsSync12(expanded))
|
|
7357
|
+
if (!existsSync12(expanded)) mkdirSync8(expanded, { recursive: true });
|
|
7158
7358
|
}
|
|
7159
7359
|
function toResponse(channel) {
|
|
7160
7360
|
return {
|
|
@@ -7305,24 +7505,24 @@ import {
|
|
|
7305
7505
|
closeSync,
|
|
7306
7506
|
existsSync as existsSync16,
|
|
7307
7507
|
lstatSync as lstatSync2,
|
|
7308
|
-
mkdirSync as
|
|
7508
|
+
mkdirSync as mkdirSync9,
|
|
7309
7509
|
openSync,
|
|
7310
7510
|
readSync,
|
|
7311
7511
|
readdirSync as readdirSync4,
|
|
7312
|
-
readFileSync as
|
|
7512
|
+
readFileSync as readFileSync13,
|
|
7313
7513
|
realpathSync as realpathSync3,
|
|
7314
7514
|
renameSync,
|
|
7315
|
-
rmSync as
|
|
7515
|
+
rmSync as rmSync7,
|
|
7316
7516
|
statSync as statSync5,
|
|
7317
|
-
writeFileSync as
|
|
7517
|
+
writeFileSync as writeFileSync9
|
|
7318
7518
|
} from "fs";
|
|
7319
|
-
import { basename as basename6, dirname as dirname5, extname, join as
|
|
7519
|
+
import { basename as basename6, dirname as dirname5, extname, join as join19, relative, resolve as resolve4, sep } from "path";
|
|
7320
7520
|
import { Hono as Hono10 } from "hono";
|
|
7321
7521
|
import { streamSSE } from "hono/streaming";
|
|
7322
7522
|
|
|
7323
7523
|
// src/dashboard/docs-watcher.ts
|
|
7324
7524
|
import { existsSync as existsSync13, watch } from "fs";
|
|
7325
|
-
import { basename as basename4, join as
|
|
7525
|
+
import { basename as basename4, join as join15 } from "path";
|
|
7326
7526
|
var DEBOUNCE_MS = 200;
|
|
7327
7527
|
var MAX_PENDING = 200;
|
|
7328
7528
|
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
@@ -7363,7 +7563,7 @@ function watchDocs(cwd, listener) {
|
|
|
7363
7563
|
const current = entries.get(cwd);
|
|
7364
7564
|
if (!current || filename === null) return;
|
|
7365
7565
|
const rel = filename.split("\\").join("/");
|
|
7366
|
-
if (rel === root && !existsSync13(
|
|
7566
|
+
if (rel === root && !existsSync13(join15(cwd, rel))) return;
|
|
7367
7567
|
if (isIgnoredDocPath(rel)) return;
|
|
7368
7568
|
if (current.pending.size < MAX_PENDING) current.pending.add(rel);
|
|
7369
7569
|
if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
|
|
@@ -7600,18 +7800,18 @@ async function readOrQueueDocAudio(input2) {
|
|
|
7600
7800
|
}
|
|
7601
7801
|
|
|
7602
7802
|
// src/dashboard/artifact-export.ts
|
|
7603
|
-
import { readFileSync as
|
|
7803
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
7604
7804
|
|
|
7605
7805
|
// src/dashboard/chat-agent.ts
|
|
7606
7806
|
import { homedir as homedir7 } from "os";
|
|
7607
7807
|
|
|
7608
7808
|
// src/mcp/config.ts
|
|
7609
|
-
import { mkdtempSync, rmSync as
|
|
7809
|
+
import { mkdtempSync, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
7610
7810
|
import { tmpdir as tmpdir2 } from "os";
|
|
7611
|
-
import { join as
|
|
7811
|
+
import { join as join16 } from "path";
|
|
7612
7812
|
|
|
7613
7813
|
// src/mcp/crypto.ts
|
|
7614
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
7814
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
|
|
7615
7815
|
var ALGORITHM = "aes-256-gcm";
|
|
7616
7816
|
var KEY_BYTES = 32;
|
|
7617
7817
|
var IV_BYTES = 12;
|
|
@@ -7644,7 +7844,7 @@ function currentKeyVersion() {
|
|
|
7644
7844
|
);
|
|
7645
7845
|
}
|
|
7646
7846
|
function encrypt(key, purpose, plaintext) {
|
|
7647
|
-
const iv =
|
|
7847
|
+
const iv = randomBytes4(IV_BYTES);
|
|
7648
7848
|
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
7649
7849
|
cipher.setAAD(Buffer.from(purpose, "utf8"));
|
|
7650
7850
|
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
@@ -7664,7 +7864,7 @@ function decrypt(key, purpose, blob) {
|
|
|
7664
7864
|
}
|
|
7665
7865
|
function createEnvelope() {
|
|
7666
7866
|
const keyVersion = currentKeyVersion();
|
|
7667
|
-
const key =
|
|
7867
|
+
const key = randomBytes4(KEY_BYTES);
|
|
7668
7868
|
const wrappedDek = encrypt(
|
|
7669
7869
|
readMasterKey(keyVersion),
|
|
7670
7870
|
purposeForDek(keyVersion),
|
|
@@ -7730,7 +7930,7 @@ function secretsMatch(a, b) {
|
|
|
7730
7930
|
}
|
|
7731
7931
|
|
|
7732
7932
|
// src/mcp/oauth.ts
|
|
7733
|
-
import { createHash as createHash7, randomBytes as
|
|
7933
|
+
import { createHash as createHash7, randomBytes as randomBytes5 } from "crypto";
|
|
7734
7934
|
var RESERVED_AUTHORIZE_PARAMS = /* @__PURE__ */ new Set([
|
|
7735
7935
|
"client_id",
|
|
7736
7936
|
"redirect_uri",
|
|
@@ -7744,10 +7944,10 @@ function base64url(buffer) {
|
|
|
7744
7944
|
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
7745
7945
|
}
|
|
7746
7946
|
function createStateToken() {
|
|
7747
|
-
return base64url(
|
|
7947
|
+
return base64url(randomBytes5(32));
|
|
7748
7948
|
}
|
|
7749
7949
|
function createPkcePair() {
|
|
7750
|
-
const verifier = base64url(
|
|
7950
|
+
const verifier = base64url(randomBytes5(32));
|
|
7751
7951
|
const challenge = base64url(createHash7("sha256").update(verifier).digest());
|
|
7752
7952
|
return { verifier, challenge };
|
|
7753
7953
|
}
|
|
@@ -8294,12 +8494,12 @@ async function buildMcpConfig(profile, connectionIds) {
|
|
|
8294
8494
|
return { config, skipped };
|
|
8295
8495
|
}
|
|
8296
8496
|
function writeMcpConfigFile(config) {
|
|
8297
|
-
const dir = mkdtempSync(
|
|
8298
|
-
const path =
|
|
8299
|
-
|
|
8497
|
+
const dir = mkdtempSync(join16(tmpdir2(), "flyspace-mcp-"));
|
|
8498
|
+
const path = join16(dir, "mcp-config.json");
|
|
8499
|
+
writeFileSync8(path, JSON.stringify(config), { mode: 384 });
|
|
8300
8500
|
return {
|
|
8301
8501
|
path,
|
|
8302
|
-
cleanup: () =>
|
|
8502
|
+
cleanup: () => rmSync6(dir, { recursive: true, force: true })
|
|
8303
8503
|
};
|
|
8304
8504
|
}
|
|
8305
8505
|
async function withMcpConfig(profile, connectionIds, fn) {
|
|
@@ -8316,22 +8516,22 @@ async function withMcpConfig(profile, connectionIds, fn) {
|
|
|
8316
8516
|
}
|
|
8317
8517
|
|
|
8318
8518
|
// src/claude/agents.ts
|
|
8319
|
-
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as
|
|
8519
|
+
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
|
|
8320
8520
|
import { homedir as homedir6 } from "os";
|
|
8321
|
-
import { basename as basename5, join as
|
|
8521
|
+
import { basename as basename5, join as join17 } from "path";
|
|
8322
8522
|
import yaml from "js-yaml";
|
|
8323
8523
|
function defaultConfigDir() {
|
|
8324
|
-
return
|
|
8524
|
+
return join17(homedir6(), ".claude");
|
|
8325
8525
|
}
|
|
8326
8526
|
function expandHome(p) {
|
|
8327
8527
|
if (p === "~") return homedir6();
|
|
8328
|
-
if (p.startsWith("~/")) return
|
|
8528
|
+
if (p.startsWith("~/")) return join17(homedir6(), p.slice(2));
|
|
8329
8529
|
return p;
|
|
8330
8530
|
}
|
|
8331
8531
|
function parseFrontmatter(file) {
|
|
8332
8532
|
let content;
|
|
8333
8533
|
try {
|
|
8334
|
-
content =
|
|
8534
|
+
content = readFileSync11(file, "utf-8");
|
|
8335
8535
|
} catch {
|
|
8336
8536
|
return {};
|
|
8337
8537
|
}
|
|
@@ -8355,7 +8555,7 @@ function collectFrom(root, source) {
|
|
|
8355
8555
|
return [];
|
|
8356
8556
|
}
|
|
8357
8557
|
return entries2.map((file) => {
|
|
8358
|
-
const path =
|
|
8558
|
+
const path = join17(root, file);
|
|
8359
8559
|
const fm = parseFrontmatter(path);
|
|
8360
8560
|
const name = fm.name?.trim() || basename5(file, ".md");
|
|
8361
8561
|
return { name, description: (fm.description ?? "").trim(), source, path };
|
|
@@ -8364,8 +8564,8 @@ function collectFrom(root, source) {
|
|
|
8364
8564
|
function listAgents(configDir2, cwd) {
|
|
8365
8565
|
const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
|
|
8366
8566
|
const collected = [
|
|
8367
|
-
...collectFrom(
|
|
8368
|
-
...collectFrom(
|
|
8567
|
+
...collectFrom(join17(cwd, ".claude", "agents"), "project"),
|
|
8568
|
+
...collectFrom(join17(base, "agents"), "user")
|
|
8369
8569
|
];
|
|
8370
8570
|
const byName = /* @__PURE__ */ new Map();
|
|
8371
8571
|
for (const agent of collected) {
|
|
@@ -8377,7 +8577,7 @@ function readAgentPersona(configDir2, cwd, name) {
|
|
|
8377
8577
|
const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
|
|
8378
8578
|
if (!agent) return void 0;
|
|
8379
8579
|
try {
|
|
8380
|
-
const content =
|
|
8580
|
+
const content = readFileSync11(agent.path, "utf-8");
|
|
8381
8581
|
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
|
|
8382
8582
|
} catch {
|
|
8383
8583
|
return void 0;
|
|
@@ -8477,11 +8677,11 @@ async function updateChannelMemory(channelId, memoryId, patch, actor) {
|
|
|
8477
8677
|
async function searchChannelMemories(channelId, query) {
|
|
8478
8678
|
const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
|
|
8479
8679
|
if (!words.length) return [];
|
|
8480
|
-
let
|
|
8680
|
+
let request = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
|
|
8481
8681
|
for (const word of words) {
|
|
8482
|
-
|
|
8682
|
+
request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
|
|
8483
8683
|
}
|
|
8484
|
-
const { data, error: error2 } = await
|
|
8684
|
+
const { data, error: error2 } = await request.order("updated_at", { ascending: false }).limit(20);
|
|
8485
8685
|
if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
|
|
8486
8686
|
return (data ?? []).map(rowToMemory);
|
|
8487
8687
|
}
|
|
@@ -8877,7 +9077,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
|
|
|
8877
9077
|
|
|
8878
9078
|
// src/dashboard/attachment-downloader.ts
|
|
8879
9079
|
import { writeFile } from "fs/promises";
|
|
8880
|
-
import { join as
|
|
9080
|
+
import { join as join18 } from "path";
|
|
8881
9081
|
import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
|
|
8882
9082
|
var debug = process.env["DEBUG"] === "true";
|
|
8883
9083
|
var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
|
|
@@ -8924,7 +9124,7 @@ async function downloadOne(key) {
|
|
|
8924
9124
|
if (bytes.length > sizeLimit) {
|
|
8925
9125
|
return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
|
|
8926
9126
|
}
|
|
8927
|
-
const localPath =
|
|
9127
|
+
const localPath = join18(paths.UPLOADS_DIR, lastSegment);
|
|
8928
9128
|
await writeFile(localPath, bytes);
|
|
8929
9129
|
if (debug)
|
|
8930
9130
|
console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
|
|
@@ -10211,7 +10411,7 @@ async function exportDocAsArtifact(channelId, docPath) {
|
|
|
10211
10411
|
);
|
|
10212
10412
|
const abs = resolveDocPath(channelConfig.cwd, docPath);
|
|
10213
10413
|
if (!abs) throw new Error(`doc not found: ${docPath}`);
|
|
10214
|
-
const content =
|
|
10414
|
+
const content = readFileSync12(abs, "utf8");
|
|
10215
10415
|
const title = extractTitle(abs, "md");
|
|
10216
10416
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
10217
10417
|
const prompt = `Publish the document at \`${docPath}\` (already read below) as a Claude Artifact, then record the result as a stub file under this channel's \`docs/artifacts/\` folder (create the folder if it doesn't exist).
|
|
@@ -10468,7 +10668,7 @@ function probeDir(absDir, showHidden) {
|
|
|
10468
10668
|
return false;
|
|
10469
10669
|
}
|
|
10470
10670
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10471
|
-
const abs =
|
|
10671
|
+
const abs = join19(dir, entry.name);
|
|
10472
10672
|
const kind = entryKind(entry, abs);
|
|
10473
10673
|
if (kind === "dir") {
|
|
10474
10674
|
if (SKIP_DIRS.has(entry.name)) hasOther = true;
|
|
@@ -10488,14 +10688,14 @@ function probeDir(absDir, showHidden) {
|
|
|
10488
10688
|
}
|
|
10489
10689
|
function readGitBranch(dir) {
|
|
10490
10690
|
try {
|
|
10491
|
-
let gitDir =
|
|
10691
|
+
let gitDir = join19(dir, ".git");
|
|
10492
10692
|
if (statSync5(gitDir).isFile()) {
|
|
10493
|
-
const pointer =
|
|
10693
|
+
const pointer = readFileSync13(gitDir, "utf8").trim();
|
|
10494
10694
|
const match = pointer.match(/^gitdir:\s*(.+)$/);
|
|
10495
10695
|
if (!match) return void 0;
|
|
10496
10696
|
gitDir = resolve4(dir, match[1]);
|
|
10497
10697
|
}
|
|
10498
|
-
const head =
|
|
10698
|
+
const head = readFileSync13(join19(gitDir, "HEAD"), "utf8").trim();
|
|
10499
10699
|
const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
10500
10700
|
if (branchMatch) return branchMatch[1];
|
|
10501
10701
|
return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
|
|
@@ -10517,14 +10717,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
|
|
|
10517
10717
|
for (const entry of entries2) {
|
|
10518
10718
|
if (dirs.length + docs.length >= MAX_ENTRIES) break;
|
|
10519
10719
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10520
|
-
const abs =
|
|
10720
|
+
const abs = join19(absDir, entry.name);
|
|
10521
10721
|
const path = relative(root, abs).split(sep).join("/");
|
|
10522
10722
|
const kind = entryKind(entry, abs);
|
|
10523
10723
|
if (kind === "dir") {
|
|
10524
10724
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
10525
10725
|
const probe3 = probeDir(abs, showHidden);
|
|
10526
10726
|
if (!probe3.hasDocs && probe3.hasOther) continue;
|
|
10527
|
-
const isRepo = existsSync16(
|
|
10727
|
+
const isRepo = existsSync16(join19(abs, ".git"));
|
|
10528
10728
|
dirs.push({
|
|
10529
10729
|
path,
|
|
10530
10730
|
name: entry.name,
|
|
@@ -10560,7 +10760,7 @@ function flatDocs(root, {
|
|
|
10560
10760
|
for (const entry of entries2) {
|
|
10561
10761
|
if (hits.length >= MAX_FLAT_DOCS) return;
|
|
10562
10762
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10563
|
-
const abs =
|
|
10763
|
+
const abs = join19(dir, entry.name);
|
|
10564
10764
|
const kind = entryKind(entry, abs);
|
|
10565
10765
|
if (kind === "dir") {
|
|
10566
10766
|
if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
|
|
@@ -10602,7 +10802,7 @@ function writeDocContent(root, rel, content, baseMtime) {
|
|
|
10602
10802
|
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) {
|
|
10603
10803
|
return { status: "too-large" };
|
|
10604
10804
|
}
|
|
10605
|
-
|
|
10805
|
+
writeFileSync9(abs, content, "utf8");
|
|
10606
10806
|
return { status: "ok", mtime: statSync5(abs).mtime.toISOString() };
|
|
10607
10807
|
}
|
|
10608
10808
|
function resolveRawPath(root, rel) {
|
|
@@ -10640,7 +10840,7 @@ function uniqueName(absDir, name) {
|
|
|
10640
10840
|
let candidate = name;
|
|
10641
10841
|
for (let n = 1; n < 1e3; n++) {
|
|
10642
10842
|
try {
|
|
10643
|
-
statSync5(
|
|
10843
|
+
statSync5(join19(absDir, candidate));
|
|
10644
10844
|
} catch {
|
|
10645
10845
|
return candidate;
|
|
10646
10846
|
}
|
|
@@ -10662,8 +10862,8 @@ function saveUploadedFile(root, relDir, name, data) {
|
|
|
10662
10862
|
if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
|
|
10663
10863
|
if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
|
|
10664
10864
|
const final = uniqueName(absDir, safe);
|
|
10665
|
-
const abs =
|
|
10666
|
-
|
|
10865
|
+
const abs = join19(absDir, final);
|
|
10866
|
+
writeFileSync9(abs, data);
|
|
10667
10867
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
|
|
10668
10868
|
}
|
|
10669
10869
|
function createDocFolder(root, relDir, name) {
|
|
@@ -10676,13 +10876,13 @@ function createDocFolder(root, relDir, name) {
|
|
|
10676
10876
|
}
|
|
10677
10877
|
const safe = sanitizeUploadName(name);
|
|
10678
10878
|
if (!safe) return { status: "bad-name" };
|
|
10679
|
-
const abs =
|
|
10879
|
+
const abs = join19(absDir, safe);
|
|
10680
10880
|
try {
|
|
10681
10881
|
statSync5(abs);
|
|
10682
10882
|
return { status: "exists" };
|
|
10683
10883
|
} catch {
|
|
10684
10884
|
}
|
|
10685
|
-
|
|
10885
|
+
mkdirSync9(abs);
|
|
10686
10886
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: safe };
|
|
10687
10887
|
}
|
|
10688
10888
|
function createDocFile(root, relDir, name, content) {
|
|
@@ -10699,9 +10899,9 @@ function createDocFile(root, relDir, name, content) {
|
|
|
10699
10899
|
if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
|
|
10700
10900
|
if (SKIP_FILES.has(safe)) return { status: "bad-name" };
|
|
10701
10901
|
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
|
|
10702
|
-
const abs =
|
|
10902
|
+
const abs = join19(absDir, safe);
|
|
10703
10903
|
try {
|
|
10704
|
-
|
|
10904
|
+
writeFileSync9(abs, content, { encoding: "utf8", flag: "wx" });
|
|
10705
10905
|
} catch (err) {
|
|
10706
10906
|
if (err.code === "EEXIST") return { status: "exists" };
|
|
10707
10907
|
throw err;
|
|
@@ -10737,7 +10937,7 @@ function moveDocEntry(root, from, to) {
|
|
|
10737
10937
|
} catch {
|
|
10738
10938
|
return { status: "bad-target" };
|
|
10739
10939
|
}
|
|
10740
|
-
const finalAbs =
|
|
10940
|
+
const finalAbs = join19(absToDir, name);
|
|
10741
10941
|
if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
|
|
10742
10942
|
return { status: "into-self" };
|
|
10743
10943
|
}
|
|
@@ -10760,7 +10960,7 @@ function deleteDocEntry(root, path) {
|
|
|
10760
10960
|
}
|
|
10761
10961
|
const isDir2 = stat.isDirectory();
|
|
10762
10962
|
if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
|
|
10763
|
-
|
|
10963
|
+
rmSync7(abs, { recursive: isDir2 });
|
|
10764
10964
|
return {
|
|
10765
10965
|
status: "ok",
|
|
10766
10966
|
path: relative(root, abs).split(sep).join("/"),
|
|
@@ -10799,7 +10999,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
10799
10999
|
}
|
|
10800
11000
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10801
11001
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10802
|
-
const data =
|
|
11002
|
+
const data = readFileSync13(resolved2.abs);
|
|
10803
11003
|
const headers = { "Content-Type": resolved2.contentType };
|
|
10804
11004
|
if (download) {
|
|
10805
11005
|
const filename = basename6(resolved2.abs).replace(/"/g, "");
|
|
@@ -10819,7 +11019,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
10819
11019
|
}
|
|
10820
11020
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10821
11021
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10822
|
-
return c.body(new Uint8Array(
|
|
11022
|
+
return c.body(new Uint8Array(readFileSync13(abs)), 200, {
|
|
10823
11023
|
"Content-Type": "text/html; charset=utf-8",
|
|
10824
11024
|
"Content-Security-Policy": HTML_SHARE_CSP,
|
|
10825
11025
|
"X-Content-Type-Options": "nosniff",
|
|
@@ -11004,7 +11204,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
11004
11204
|
title: kind ? extractTitle(abs, kind) : basename6(abs),
|
|
11005
11205
|
mtime: stat.mtime.toISOString(),
|
|
11006
11206
|
tooLarge: tooLarge || void 0,
|
|
11007
|
-
content: tooLarge ? "" :
|
|
11207
|
+
content: tooLarge ? "" : readFileSync13(abs, "utf8")
|
|
11008
11208
|
});
|
|
11009
11209
|
});
|
|
11010
11210
|
app.put("/:id/docs/content", async (c) => {
|
|
@@ -11047,7 +11247,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
11047
11247
|
channel,
|
|
11048
11248
|
rel,
|
|
11049
11249
|
title: extractTitle(abs, "md"),
|
|
11050
|
-
content:
|
|
11250
|
+
content: readFileSync13(abs, "utf8")
|
|
11051
11251
|
};
|
|
11052
11252
|
};
|
|
11053
11253
|
app.get("/:id/docs/translation/:lang", async (c) => {
|
|
@@ -11586,10 +11786,10 @@ function registerMirroredSkillRoutes(api, agent) {
|
|
|
11586
11786
|
}
|
|
11587
11787
|
|
|
11588
11788
|
// src/dashboard/providers.ts
|
|
11589
|
-
import { execFileSync as
|
|
11789
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
11590
11790
|
import { existsSync as existsSync19 } from "fs";
|
|
11591
11791
|
import { homedir as homedir8 } from "os";
|
|
11592
|
-
import { join as
|
|
11792
|
+
import { join as join20, resolve as resolve5 } from "path";
|
|
11593
11793
|
var PROVIDER_LABELS = {
|
|
11594
11794
|
claude: "Claude Code",
|
|
11595
11795
|
codex: "Codex"
|
|
@@ -11598,7 +11798,7 @@ var TTL_MS2 = 6e4;
|
|
|
11598
11798
|
var cache6 = /* @__PURE__ */ new Map();
|
|
11599
11799
|
function binaryOnPath(bin) {
|
|
11600
11800
|
try {
|
|
11601
|
-
|
|
11801
|
+
execFileSync3(bin, ["--version"], { stdio: "ignore" });
|
|
11602
11802
|
return true;
|
|
11603
11803
|
} catch {
|
|
11604
11804
|
return false;
|
|
@@ -11607,10 +11807,10 @@ function binaryOnPath(bin) {
|
|
|
11607
11807
|
function claudeAuthAvailable() {
|
|
11608
11808
|
if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
|
|
11609
11809
|
const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
|
|
11610
|
-
if (existsSync19(
|
|
11810
|
+
if (existsSync19(join20(configDir2, ".credentials.json"))) return true;
|
|
11611
11811
|
if (process.platform !== "darwin") return false;
|
|
11612
11812
|
try {
|
|
11613
|
-
|
|
11813
|
+
execFileSync3("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
11614
11814
|
stdio: "ignore"
|
|
11615
11815
|
});
|
|
11616
11816
|
return true;
|
|
@@ -11760,7 +11960,7 @@ function registerHostRoutes(api) {
|
|
|
11760
11960
|
const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
|
|
11761
11961
|
const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
|
|
11762
11962
|
const filename = errors ? "custie-error.log" : "custie.log";
|
|
11763
|
-
const file =
|
|
11963
|
+
const file = join21(paths.LOG_DIR, filename);
|
|
11764
11964
|
const exists = existsSync20(file);
|
|
11765
11965
|
return c.json({
|
|
11766
11966
|
file,
|
|
@@ -11772,7 +11972,7 @@ function registerHostRoutes(api) {
|
|
|
11772
11972
|
}
|
|
11773
11973
|
function tailFile(file, limit) {
|
|
11774
11974
|
if (!existsSync20(file)) return [];
|
|
11775
|
-
const lines =
|
|
11975
|
+
const lines = readFileSync14(file, "utf-8").split("\n");
|
|
11776
11976
|
if (lines.at(-1) === "") lines.pop();
|
|
11777
11977
|
return lines.slice(-limit);
|
|
11778
11978
|
}
|
|
@@ -13445,7 +13645,7 @@ function createSearchRouter() {
|
|
|
13445
13645
|
}
|
|
13446
13646
|
|
|
13447
13647
|
// src/dashboard/uploads-routes.ts
|
|
13448
|
-
import { randomUUID } from "crypto";
|
|
13648
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
13449
13649
|
import { Hono as Hono18 } from "hono";
|
|
13450
13650
|
import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
|
|
13451
13651
|
import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
|
|
@@ -13469,7 +13669,7 @@ function createUploadsRouter() {
|
|
|
13469
13669
|
if (file.size > MAX_FILE_BYTES) {
|
|
13470
13670
|
return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
|
|
13471
13671
|
}
|
|
13472
|
-
const key = `${KEY_PREFIX2}/${getProfile()}/${
|
|
13672
|
+
const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
|
|
13473
13673
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
13474
13674
|
await getR2Client().send(
|
|
13475
13675
|
new PutObjectCommand3({
|
|
@@ -13552,7 +13752,7 @@ function createTranscribeRouter() {
|
|
|
13552
13752
|
}
|
|
13553
13753
|
|
|
13554
13754
|
// src/dashboard/avatars-routes.ts
|
|
13555
|
-
import { createHash as createHash10, randomUUID as
|
|
13755
|
+
import { createHash as createHash10, randomUUID as randomUUID3 } from "crypto";
|
|
13556
13756
|
import { Hono as Hono20 } from "hono";
|
|
13557
13757
|
import sharp4 from "sharp";
|
|
13558
13758
|
import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
|
|
@@ -13591,7 +13791,7 @@ async function storeAvatar(file, owner) {
|
|
|
13591
13791
|
if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
|
|
13592
13792
|
const original = Buffer.from(await file.arrayBuffer());
|
|
13593
13793
|
const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
|
|
13594
|
-
const key = `${avatarKeyPrefix()}${owner}-${
|
|
13794
|
+
const key = `${avatarKeyPrefix()}${owner}-${randomUUID3()}.png`;
|
|
13595
13795
|
await getR2Client().send(
|
|
13596
13796
|
new PutObjectCommand4({
|
|
13597
13797
|
Bucket: getR2Bucket(),
|
|
@@ -14955,6 +15155,14 @@ var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
|
|
|
14955
15155
|
var GREETING_MODEL = "openai/gpt-oss-20b";
|
|
14956
15156
|
var TIMEOUT_MS = 6e3;
|
|
14957
15157
|
var MAX_LISTED_CHANNELS = 8;
|
|
15158
|
+
var GUIDES = [
|
|
15159
|
+
"Welcome to Flyspace",
|
|
15160
|
+
"Working with the agent",
|
|
15161
|
+
"Channels and folders",
|
|
15162
|
+
"Docs",
|
|
15163
|
+
"Inviting your team",
|
|
15164
|
+
"Automations"
|
|
15165
|
+
];
|
|
14958
15166
|
var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner sees, posted in their #general channel. Flyspace is a team chat where an AI agent works inside every channel.
|
|
14959
15167
|
|
|
14960
15168
|
- Plain Markdown, 60 to 110 words, in English. No headings, no emoji, no em or en dashes, no sign-off.
|
|
@@ -14962,11 +15170,13 @@ var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner
|
|
|
14962
15170
|
- Say that #general is public: everyone invited to the workspace can see it.
|
|
14963
15171
|
- If project channels are listed, mention them as #name and say each one is linked to that project folder. Never invent a channel.
|
|
14964
15172
|
- Mention the trial or the missing Host only if the facts say so.
|
|
15173
|
+
- If guides are listed, say they are in the Docs pane of #general and suggest starting with the first one.
|
|
14965
15174
|
- Use only the facts given. Do not add buttons or a list of next steps: those are appended for you.`;
|
|
14966
15175
|
function greetingButtons(input2) {
|
|
14967
15176
|
const buttons = [];
|
|
14968
15177
|
if (!input2.hostConnected) buttons.push("Connect a machine");
|
|
14969
15178
|
if (input2.channels[0]) buttons.push(`Summarise #${input2.channels[0]} for me`);
|
|
15179
|
+
if (input2.hostConnected) buttons.push("Give me a quick tour of the guides");
|
|
14970
15180
|
if (input2.memberCount <= 1) buttons.push("Invite my team");
|
|
14971
15181
|
buttons.push("Set up a daily digest");
|
|
14972
15182
|
return buttons.slice(0, 3).map((b) => b.replace(/[|\]]/g, ""));
|
|
@@ -14977,6 +15187,7 @@ function fallbackGreeting(input2) {
|
|
|
14977
15187
|
`Welcome to ${input2.workspaceName}, ${input2.ownerName}.`,
|
|
14978
15188
|
"This is #general. It is public, so everyone you invite to the workspace can see it.",
|
|
14979
15189
|
channels.length ? `Your project channels are ready: ${channels.join(", ")}. Each one is linked to its project folder, so the agent there works on the real files.` : "",
|
|
15190
|
+
input2.hostConnected ? "A few short guides are waiting in the Docs pane of #general. Start with Welcome to Flyspace." : "",
|
|
14980
15191
|
"Mention the agent in any channel to get started."
|
|
14981
15192
|
].filter(Boolean).join("\n\n");
|
|
14982
15193
|
}
|
|
@@ -14987,7 +15198,8 @@ async function groqGreeting(input2, apiKey) {
|
|
|
14987
15198
|
`Plan: ${input2.trialing ? "14-day free trial, just started" : "not on a trial"}`,
|
|
14988
15199
|
`Host: ${input2.hostConnected ? "a machine is connected, agents can run" : "no machine connected yet, agents cannot run until one is"}`,
|
|
14989
15200
|
`Project channels: ${input2.channels.slice(0, MAX_LISTED_CHANNELS).join(", ") || "(none yet)"}`,
|
|
14990
|
-
`Members: ${input2.memberCount}
|
|
15201
|
+
`Members: ${input2.memberCount}`,
|
|
15202
|
+
`Guides in #general: ${input2.hostConnected ? GUIDES.join(", ") : "(none, no machine connected)"}`
|
|
14991
15203
|
].join("\n");
|
|
14992
15204
|
const res = await fetch(GROQ_API_URL8, {
|
|
14993
15205
|
method: "POST",
|
|
@@ -15069,6 +15281,14 @@ function createOnboardingRouter(agent) {
|
|
|
15069
15281
|
await markWorkspaceProjectsSetup(ctx.workspaceId);
|
|
15070
15282
|
return c.json({ channels: created.map((ch) => ({ id: ch.id, name: ch.name })) }, 201);
|
|
15071
15283
|
});
|
|
15284
|
+
app.get("/greeting", async (c) => {
|
|
15285
|
+
const ctx = await resolveAuthContext(c);
|
|
15286
|
+
if (ctx?.role !== "admin") return c.json({ pending: false });
|
|
15287
|
+
const greetedAt = await getWorkspaceGreetedAt(ctx.workspaceId);
|
|
15288
|
+
const pending3 = !!ctx.projectsSetupAt && !greetedAt;
|
|
15289
|
+
const general = pending3 ? await ensureGeneralChannel() : null;
|
|
15290
|
+
return c.json({ pending: pending3, channelId: general?.id ?? null });
|
|
15291
|
+
});
|
|
15072
15292
|
app.post("/greeting", async (c) => {
|
|
15073
15293
|
const ctx = await resolveAuthContext(c);
|
|
15074
15294
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -15077,6 +15297,8 @@ function createOnboardingRouter(agent) {
|
|
|
15077
15297
|
const existing = () => supabase.from("threads").select("id").eq("client_key", ctx.workspaceId).maybeSingle();
|
|
15078
15298
|
const found = await existing();
|
|
15079
15299
|
if (found.data) return c.json({ channelId: general.id, threadId: found.data.id });
|
|
15300
|
+
if (await getWorkspaceGreetedAt(ctx.workspaceId))
|
|
15301
|
+
return c.json({ error: "already greeted" }, 409);
|
|
15080
15302
|
const [branding, billing, members] = await Promise.all([
|
|
15081
15303
|
getWorkspaceBranding(getProfile()).catch(() => null),
|
|
15082
15304
|
getWorkspaceBilling(ctx.workspaceId).catch(() => null),
|
|
@@ -15107,6 +15329,7 @@ function createOnboardingRouter(agent) {
|
|
|
15107
15329
|
await supabase.from("threads").delete().eq("id", thread.id);
|
|
15108
15330
|
return c.json({ error: reply.error.message }, 500);
|
|
15109
15331
|
}
|
|
15332
|
+
await markWorkspaceGreeted(ctx.workspaceId);
|
|
15110
15333
|
return c.json({ channelId: general.id, threadId: thread.id }, 201);
|
|
15111
15334
|
});
|
|
15112
15335
|
return app;
|
|
@@ -15600,10 +15823,10 @@ import { WebSocket, WebSocketServer } from "ws";
|
|
|
15600
15823
|
|
|
15601
15824
|
// src/dashboard/host-identity.ts
|
|
15602
15825
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
|
|
15603
|
-
import { existsSync as existsSync23, mkdirSync as
|
|
15604
|
-
import { dirname as dirname7, join as
|
|
15826
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
15827
|
+
import { dirname as dirname7, join as join22 } from "path";
|
|
15605
15828
|
function hostPrivateKeyPath() {
|
|
15606
|
-
return process.env["FLYSPACE_HOST_KEY_PATH"] ||
|
|
15829
|
+
return process.env["FLYSPACE_HOST_KEY_PATH"] || join22(paths.BASE_CONFIG_DIR, "host.key");
|
|
15607
15830
|
}
|
|
15608
15831
|
function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
15609
15832
|
if (!existsSync23(path)) {
|
|
@@ -15611,7 +15834,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
|
15611
15834
|
`No Host identity at ${path}. Run \`pnpm run generate-host-key\` on this machine, then set FLYSPACE_HOST_PUBLIC_KEY on the Data process to the printed public key.`
|
|
15612
15835
|
);
|
|
15613
15836
|
}
|
|
15614
|
-
return createPrivateKey2(
|
|
15837
|
+
return createPrivateKey2(readFileSync15(path, "utf8"));
|
|
15615
15838
|
}
|
|
15616
15839
|
function loadHostPublicKey() {
|
|
15617
15840
|
const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
|
|
@@ -15624,82 +15847,6 @@ function loadHostPublicKey() {
|
|
|
15624
15847
|
}
|
|
15625
15848
|
}
|
|
15626
15849
|
|
|
15627
|
-
// src/dashboard/host-lock.ts
|
|
15628
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
15629
|
-
import { randomBytes as randomBytes5 } from "crypto";
|
|
15630
|
-
import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
15631
|
-
import { join as join22 } from "path";
|
|
15632
|
-
function hostLockPath() {
|
|
15633
|
-
return join22(paths.BASE_DATA_DIR, "host.lock");
|
|
15634
|
-
}
|
|
15635
|
-
function isLiveFlyspace(pid) {
|
|
15636
|
-
try {
|
|
15637
|
-
process.kill(pid, 0);
|
|
15638
|
-
const command = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
|
|
15639
|
-
encoding: "utf8"
|
|
15640
|
-
});
|
|
15641
|
-
return command.includes("flyspace") || command.includes("host-server");
|
|
15642
|
-
} catch {
|
|
15643
|
-
return false;
|
|
15644
|
-
}
|
|
15645
|
-
}
|
|
15646
|
-
function readLockPid(path) {
|
|
15647
|
-
try {
|
|
15648
|
-
const pid = Number(JSON.parse(readFileSync15(path, "utf8")).pid);
|
|
15649
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
15650
|
-
} catch {
|
|
15651
|
-
return null;
|
|
15652
|
-
}
|
|
15653
|
-
}
|
|
15654
|
-
function acquireHostLock() {
|
|
15655
|
-
const path = hostLockPath();
|
|
15656
|
-
mkdirSync10(paths.BASE_DATA_DIR, { recursive: true });
|
|
15657
|
-
const body = JSON.stringify({
|
|
15658
|
-
pid: process.pid,
|
|
15659
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15660
|
-
execPath: process.argv[1] ?? ""
|
|
15661
|
-
});
|
|
15662
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
15663
|
-
try {
|
|
15664
|
-
writeFileSync10(path, body, { flag: "wx", mode: 384 });
|
|
15665
|
-
const release2 = () => {
|
|
15666
|
-
if (readLockPid(path) === process.pid) rmSync7(path, { force: true });
|
|
15667
|
-
};
|
|
15668
|
-
process.once("exit", release2);
|
|
15669
|
-
return release2;
|
|
15670
|
-
} catch (err) {
|
|
15671
|
-
if (err.code !== "EEXIST") throw err;
|
|
15672
|
-
const holder = readLockPid(path);
|
|
15673
|
-
if (holder === process.pid) return () => {
|
|
15674
|
-
};
|
|
15675
|
-
if (holder && isLiveFlyspace(holder)) {
|
|
15676
|
-
throw new Error(
|
|
15677
|
-
`Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
|
|
15678
|
-
);
|
|
15679
|
-
}
|
|
15680
|
-
rmSync7(path, { force: true });
|
|
15681
|
-
}
|
|
15682
|
-
}
|
|
15683
|
-
throw new Error(`Could not take the Host lock at ${path}`);
|
|
15684
|
-
}
|
|
15685
|
-
function publishHostEndpoint(port) {
|
|
15686
|
-
const path = hostLockPath();
|
|
15687
|
-
if (readLockPid(path) !== process.pid) return null;
|
|
15688
|
-
const token = randomBytes5(32).toString("hex");
|
|
15689
|
-
const lock = JSON.parse(readFileSync15(path, "utf8"));
|
|
15690
|
-
writeFileSync10(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
|
|
15691
|
-
return token;
|
|
15692
|
-
}
|
|
15693
|
-
function readHostEndpoint() {
|
|
15694
|
-
try {
|
|
15695
|
-
const { pid, port, token } = JSON.parse(readFileSync15(hostLockPath(), "utf8"));
|
|
15696
|
-
if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
|
|
15697
|
-
return { port, token };
|
|
15698
|
-
} catch {
|
|
15699
|
-
return null;
|
|
15700
|
-
}
|
|
15701
|
-
}
|
|
15702
|
-
|
|
15703
15850
|
// src/store/device-store.ts
|
|
15704
15851
|
import { createHash as createHash11, createPublicKey as createPublicKey3, randomBytes as randomBytes6, randomInt } from "crypto";
|
|
15705
15852
|
var DEVICE_CODE_TTL_MS = 10 * 6e4;
|
|
@@ -16490,98 +16637,6 @@ var forwardToSupabase = async (req) => {
|
|
|
16490
16637
|
};
|
|
16491
16638
|
};
|
|
16492
16639
|
|
|
16493
|
-
// src/dashboard/host-db-client.ts
|
|
16494
|
-
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
16495
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
16496
|
-
var REQUEST_TIMEOUT_MS3 = 3e4;
|
|
16497
|
-
var pending = /* @__PURE__ */ new Map();
|
|
16498
|
-
var sender = null;
|
|
16499
|
-
var onSender = [];
|
|
16500
|
-
function setHostDbSender(fn) {
|
|
16501
|
-
sender = fn;
|
|
16502
|
-
if (!fn) return;
|
|
16503
|
-
const waiting = onSender;
|
|
16504
|
-
onSender = [];
|
|
16505
|
-
for (const wake of waiting) wake();
|
|
16506
|
-
}
|
|
16507
|
-
function handleHostDbResult(id, res) {
|
|
16508
|
-
const p = pending.get(id);
|
|
16509
|
-
if (!p) return;
|
|
16510
|
-
pending.delete(id);
|
|
16511
|
-
clearTimeout(p.timer);
|
|
16512
|
-
p.resolve(res);
|
|
16513
|
-
}
|
|
16514
|
-
function whenConnected() {
|
|
16515
|
-
if (sender) return Promise.resolve(sender);
|
|
16516
|
-
return new Promise((resolve7, reject) => {
|
|
16517
|
-
const timer2 = setTimeout(() => {
|
|
16518
|
-
onSender = onSender.filter((w) => w !== wake);
|
|
16519
|
-
reject(new Error("Host is not connected to the Data API"));
|
|
16520
|
-
}, REQUEST_TIMEOUT_MS3);
|
|
16521
|
-
const wake = () => {
|
|
16522
|
-
clearTimeout(timer2);
|
|
16523
|
-
resolve7(sender);
|
|
16524
|
-
};
|
|
16525
|
-
onSender.push(wake);
|
|
16526
|
-
});
|
|
16527
|
-
}
|
|
16528
|
-
async function request(req) {
|
|
16529
|
-
const send2 = await whenConnected();
|
|
16530
|
-
const id = randomUUID3();
|
|
16531
|
-
return new Promise((resolve7, reject) => {
|
|
16532
|
-
const timer2 = setTimeout(() => {
|
|
16533
|
-
pending.delete(id);
|
|
16534
|
-
reject(new Error("Data API did not answer in time"));
|
|
16535
|
-
}, REQUEST_TIMEOUT_MS3);
|
|
16536
|
-
pending.set(id, { resolve: resolve7, reject, timer: timer2 });
|
|
16537
|
-
send2({ type: "db", id, req });
|
|
16538
|
-
});
|
|
16539
|
-
}
|
|
16540
|
-
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
16541
|
-
var hostDbFetch = async (input2, init) => {
|
|
16542
|
-
const req = new Request(input2, init);
|
|
16543
|
-
const url = new URL(req.url);
|
|
16544
|
-
const method = req.method.toUpperCase();
|
|
16545
|
-
const res = await request({
|
|
16546
|
-
method,
|
|
16547
|
-
path: `${url.pathname}${url.search}`,
|
|
16548
|
-
headers: Object.fromEntries(req.headers.entries()),
|
|
16549
|
-
body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
|
|
16550
|
-
});
|
|
16551
|
-
const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
|
|
16552
|
-
return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
|
|
16553
|
-
};
|
|
16554
|
-
function relayChannel(topic) {
|
|
16555
|
-
const channel = {
|
|
16556
|
-
on: () => channel,
|
|
16557
|
-
subscribe: (callback) => {
|
|
16558
|
-
queueMicrotask(() => callback?.("SUBSCRIBED"));
|
|
16559
|
-
return channel;
|
|
16560
|
-
},
|
|
16561
|
-
send: async (msg) => {
|
|
16562
|
-
if (msg.type !== "broadcast" || !sender) return "error";
|
|
16563
|
-
sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
|
|
16564
|
-
return "ok";
|
|
16565
|
-
},
|
|
16566
|
-
unsubscribe: async () => "ok"
|
|
16567
|
-
};
|
|
16568
|
-
return channel;
|
|
16569
|
-
}
|
|
16570
|
-
function createHostDbClient() {
|
|
16571
|
-
const client2 = createClient2("https://host-db.invalid", "host-device", {
|
|
16572
|
-
auth: { persistSession: false, autoRefreshToken: false },
|
|
16573
|
-
global: { fetch: hostDbFetch }
|
|
16574
|
-
});
|
|
16575
|
-
return new Proxy(client2, {
|
|
16576
|
-
get(target, prop) {
|
|
16577
|
-
if (prop === "channel") return relayChannel;
|
|
16578
|
-
if (prop === "removeChannel") return async () => "ok";
|
|
16579
|
-
const value = Reflect.get(target, prop, target);
|
|
16580
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
16581
|
-
}
|
|
16582
|
-
});
|
|
16583
|
-
}
|
|
16584
|
-
|
|
16585
16640
|
// src/dashboard/agent-bridge.ts
|
|
16586
16641
|
var HOST_SOCKET_PATH = "/internal/host-socket";
|
|
16587
16642
|
var DEFAULT_DATA_API_PORT = 4757;
|
|
@@ -16842,6 +16897,7 @@ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
|
|
|
16842
16897
|
);
|
|
16843
16898
|
return Promise.resolve(null);
|
|
16844
16899
|
}
|
|
16900
|
+
if (method === "docsRequest") console.log(`[host-socket] docsRequest for "${profile}" -> device ${hosts.get(ws)?.deviceId ?? "legacy"}`);
|
|
16845
16901
|
const id = randomUUID4();
|
|
16846
16902
|
return new Promise((resolve7) => {
|
|
16847
16903
|
const timer2 = setTimeout(() => {
|
|
@@ -17066,12 +17122,13 @@ function relayedAgentCallbacks(local) {
|
|
|
17066
17122
|
...Object.fromEntries(RELAYED_METHODS.map((method) => [method, relay3(method)]))
|
|
17067
17123
|
};
|
|
17068
17124
|
}
|
|
17069
|
-
|
|
17125
|
+
function relayAuthorized(req, token) {
|
|
17070
17126
|
const given = Buffer.from(req.headers.get("authorization") ?? "");
|
|
17071
17127
|
const expected = Buffer.from(`Bearer ${token}`);
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17128
|
+
return !!token && given.length === expected.length && timingSafeEqual2(given, expected);
|
|
17129
|
+
}
|
|
17130
|
+
async function handleLocalRelay(req, callbacks, token) {
|
|
17131
|
+
if (!relayAuthorized(req, token)) return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
17075
17132
|
const body = await req.json().catch(() => null);
|
|
17076
17133
|
const method = RELAYED_METHODS.find((m) => m === body?.method);
|
|
17077
17134
|
if (!body || !method || typeof body.profile !== "string" || !Array.isArray(body.args)) {
|
|
@@ -17738,6 +17795,20 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17738
17795
|
let relayToken = null;
|
|
17739
17796
|
if (mode === "host") {
|
|
17740
17797
|
app.post(LOCAL_RELAY_PATH, (c) => handleLocalRelay(c.req.raw, agent, relayToken));
|
|
17798
|
+
if (earlyHostSocket) {
|
|
17799
|
+
app.post(LOCAL_DB_PATH, async (c) => {
|
|
17800
|
+
if (!relayAuthorized(c.req.raw, relayToken)) return c.json({ error: "unauthorized" }, 401);
|
|
17801
|
+
const req = await c.req.json().catch(() => null);
|
|
17802
|
+
if (!req || typeof req.method !== "string" || typeof req.path !== "string") {
|
|
17803
|
+
return c.json({ error: "bad request" }, 400);
|
|
17804
|
+
}
|
|
17805
|
+
try {
|
|
17806
|
+
return c.json(await requestHostDb(req));
|
|
17807
|
+
} catch (err) {
|
|
17808
|
+
return c.json({ error: err.message }, 503);
|
|
17809
|
+
}
|
|
17810
|
+
});
|
|
17811
|
+
}
|
|
17741
17812
|
}
|
|
17742
17813
|
app.route("/stripe", createStripeWebhookRouter());
|
|
17743
17814
|
if (!servesHost) {
|
|
@@ -19731,7 +19802,7 @@ async function main() {
|
|
|
19731
19802
|
break;
|
|
19732
19803
|
case "-v":
|
|
19733
19804
|
case "--version":
|
|
19734
|
-
printRaw("0.1.
|
|
19805
|
+
printRaw("0.1.14");
|
|
19735
19806
|
break;
|
|
19736
19807
|
case "-h":
|
|
19737
19808
|
case "--help":
|